Skip to content

MD088 - Normalize to ASCII quotes and dashes

Aliases: quotes-dashes

Disabled by default. This rule is opt-in: enable it explicitly with extend-enable.

What this rule does

This rule detects Unicode quotes and dashes that look like their ASCII equivalents, and replaces them with plain ASCII characters.

By default, it will only replace multiple styles of single and double quotes by their simple ASCII equivalents.

You can optionally enable replacement for dashes and for non-ASCII spaces.

Characters covered by this rule:

Unicode Character Name Character Codepoint ASCII Replacement
Left Single Quotation Mark ‘ U+2018 '
Right Single Quotation Mark ’ U+2019 '
Single Low-9 Quotation Mark ‚ U+201A '
Single High-Reversed-9 Quotation Mark ‛ U+201B '
Prime ′ U+2032 '
Left Double Quotation Mark “ U+201C "
Right Double Quotation Mark ” U+201D "
Double Low-9 Quotation Mark „ U+201E "
Double High-Reversed-9 Quotation Mark ‟ U+201F "
Double Prime ″ U+2033 "
Hyphen ‐ U+2010 -
Non-Breaking Hyphen ‑ U+2011 -
Figure Dash ‒ U+2012 -
En Dash – U+2013 -
Em Dash — U+2014 -
Horizontal Bar ― U+2015 -

With normalize-spaces enabled, these non-ASCII spaces are replaced by a plain ASCII space (U+0020):

Unicode Character Name Codepoint
No-Break Space U+00A0
En Quad, Em Quad U+2000, U+2001
En Space, Em Space U+2002, U+2003
Three-Per-Em Space U+2004
Four-Per-Em Space U+2005
Six-Per-Em Space U+2006
Figure Space U+2007
Punctuation Space U+2008
Thin Space U+2009
Hair Space U+200A
Narrow No-Break Space U+202F
Medium Mathematical Space U+205F
Ideographic Space U+3000

Spaces are invisible, so the diagnostic names the codepoint (for example U+00A0 (No-Break Space)) instead of printing the character. The zero-width characters belong to MD084, the line and paragraph separators (U+2028, U+2029, U+0085) are line breaks rather than spaces, and U+1680 (Ogham space mark) is specific to one script, so none of them are covered.

Many of these spaces are intentional: French and Dutch typography puts a no-break space or a narrow no-break space before :;?!, and CJK text uses the ideographic space. That is why the option is off by default; list the ones to keep in allow. The HTML entities   and   are not affected, as they are written in ASCII in the source.

Characters that a language spells words with are not covered, because replacing them would change the word rather than its typography. That includes the modifier letters ʼ (U+02BC, Crimean Tatar, Nenets, Chechen) and ʻ (U+02BB, the Hawaiian okina), and the guillemets « and », which are the quotation marks of several languages rather than look-alikes for ASCII ones.

This rule checks only prose content and skips:

  • YAML front matter
  • fenced code blocks
  • indented code blocks
  • inline code spans
  • math, both $$ ... $$ blocks and inline $ ... $ spans, where ′ and ″ are the prime and double prime of a derivative or a unit

Why this matters

While more typographically correct, those characters only offer a very marginal readability gain.

Fancy quotes and dashes often come from smart punctuation features or copy/paste from rich text sources. These characters can create variability, noisy diffs and unexpected behavior in tooling that expects ASCII punctuation.

Bad handling of Unicode characters in editors/IDEs is also a source of "Mojibake", which are sequences of strange characters generated by decoding a Unicode-encoded character with another charset like ISO-8859-x or CP125x. See MD083 for a rule that detects such Mojibake.

Normalizing quotes and dashes to ASCII keeps Markdown content predictable and avoids hidden character drift across contributors and platforms.

Examples

Incorrect

It’s “quoted” text with ‛variants’.

Correct

It's "quoted" text with 'variants'.

Auto-fix behavior for spaces

CommonMark gives only an ASCII space or tab a structural meaning, so replacing a non-ASCII space can turn a paragraph into something else. rumdl therefore parses the document both as written and with the replacement applied, and only rewrites a space when the Markdown structure is identical: same blocks, same inline elements, same link and heading attributes. If the replacement would change the structure, for example by creating a heading, a list item, a task marker, a closing # sequence, a table, a link reference definition, a hard break or an HTML block, or by changing emphasis or a link, the space is reported without an automatic fix. Spaces whose replacements only matter together are all left unfixed. Each top-level block (and each item of a top-level list) is checked on its own text, then the spaces judged safe are checked together against the whole document. The check has a fixed parsing budget proportional to the size of the document; a space the budget cannot cover is reported without a fix, never fixed unchecked. For example, these lines are paragraphs today and stay reported only (~ stands for a no-break space):

#~Not a heading
1.~Not a list item
trailing text~
~Leading space

Auto-fix behavior in code

Prose ’quote’ and `code ’quote’`

Fixes to:

Prose 'quote' and `code ’quote’`

The inline code span is intentionally unchanged.

Configuration

[MD088]
normalize-quotes = true
normalize-dashes = false
normalize-spaces = false
allow = []
  • normalize-quotes: if enabled (by default), replace Unicode quotes by simple ASCII ones.
  • normalize-dashes: if enabled, replace Unicode dashes by simple ASCII ones.
  • normalize-spaces: if enabled, replace non-ASCII spaces by a plain ASCII space (default: false). Spaces where the replacement could change the Markdown structure are reported without an automatic fix.
  • allow: A list of codepoints to preserve, written as the direct Unicode character, or as U+XXXX, U+XXXXX, or U+XXXXXX (for example U+2019). Any allowed codepoint is ignored by MD088 and will not be replaced.

Example:

[MD088]
enabled = true
normalize-quotes = true
normalize-dashes = true
allow = ["U+2032", "U+2033"]

With this configuration, the rule is enabled, quotes and dashes are replaced, excepted "prime" ′ and "double prime" ″.