Permalink to InstallationInstallation
Permalink to Quick TakeQuick Take
Permalink to ExamplesExamples
- Convert a three-dot ellipsis
- Fix apostrophes following inline code nodes
- Convert a multiplication marker
- Report document progress and completion
- Convert straight quotes and apostrophes
- Keep the last two words together
- Convert a spaced dash
Fix English typography in Markdown
remark-typography is a Remark plugin that improves English typography while
preserving Markdown structure. It converts contextual quotes and apostrophes,
dashes, ellipses, multiplication markers, and the final eligible word break in
each supported block.
The plugin emits raw Unicode characters. It does not encode punctuation as HTML entities.
Quick start
Await .process() to finish the pipeline before reading the output:
import { remark } from "remark";
import fixTypography from "remark-typography";
const file = await remark()
.use(fixTypography)
.process("Yes that's true but...");
console.log(file.toString().trim());
// => Yes that’s true but…
The space between the final two words in that output is U+00A0, a non-breaking space.
When you assemble a Unified pipeline yourself, run this MDAST plugin after
syntax extensions such as remark-gfm and before remark-rehype:
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import fixTypography from "remark-typography";
import { unified } from "unified";
const processor = unified()
.use(remarkParse)
.use(remarkGfm)
.use(fixTypography)
.use(remarkRehype);
Running it after remark-rehype is too late because the tree is HAST by then.
Typography rules
| Input pattern | Result | Notes |
|---|---|---|
'single', "double", that's | ||
'single', "double", that's | ‘single’, “double”, that’s | Quote direction is chosen from the surrounding phrasing text. |
word - word | ||
word - word | word — word | Spaced prose dashes become em dashes. |
1-2, 3 - 4 | ||
1-2, 3 - 4 | 1–2, 3 – 4 | Numeric ranges use en dashes. |
... | ||
... | … | Every run of exactly three dots is converted; longer dot runs are retained. |
3 x 4, -3.5kg x +4m | ||
3 x 4, -3.5kg x +4m | 3 × 4, -3.5kg × +4m | Both operands must be unambiguous ASCII-style quantities separated from x by horizontal whitespace. |
| A qualifying final word break | ||
| A qualifying final word break | U+00A0 | Widow prevention runs once per supported block or line. |
The multiplication rule leaves ambiguous prose, currency expressions, Unicode digits, identifier fragments, and line-spanning expressions unchanged.
Markdown boundaries
Typography is calculated over each paragraph, heading, and GFM table cell. A
paragraph nested in a list item or blockquote is therefore handled normally.
Hard breaks and <br> elements bound line-level widow handling.
Visible phrasing context crosses text nodes inside emphasis, strong text, and
link labels. For example, The **compiler**'s output receives the same
apostrophe as its unformatted equivalent.
The plugin changes only mutable MDAST text values. It preserves node identity,
tree structure, and existing source positions. Inline and fenced code, link and
image destinations, titles, image alt text, and definitions remain unchanged.
Inline code and image alt text can still supply immutable context for adjacent
punctuation. Literal HTML inside inline code or escaped text is visible text,
so it does not become an HTML attribute range. A final inline-code span stays
unchanged while its eligible preceding mutable space receives widow prevention. This
also applies after earlier replacements in another line of the same block.
API
The package has one runtime export: the default fixTypography Unified plugin.
Both calls below are valid:
remark().use(fixTypography);
remark().use(fixTypography, {});
Options
| Option | Type | Default | Description |
|---|---|---|---|
reportProgressFuncType: function, false, or nullDefault: null | |||
reportProgressFunc | function, false, or null | null | Receive finite, strictly increasing integer percentages. |
reportProgressFuncFromType: integer from 0 through 100Default: 0 | |||
reportProgressFuncFrom | integer from 0 through 100 | 0 | Set the inclusive start of the reported range. |
reportProgressFuncToType: integer from 0 through 100Default: 100 | |||
reportProgressFuncTo | integer from 0 through 100 | 100 | Set the inclusive end of the reported range. |
The start cannot exceed the end. The callback receives both configured endpoints; equal endpoints are reported once. Callback exceptions propagate and abort the transform.
Use the range options when this plugin is one stage in a larger progress sequence:
const progress = [];
const file = remark()
.use(fixTypography, {
reportProgressFunc: (percentageDone) => progress.push(percentageDone),
reportProgressFuncFrom: 20,
reportProgressFuncTo: 40,
})
.processSync("Wait...");
console.log(progress.at(0), progress.at(-1));
// => 20 40
Completion data
After transformation, file.data.remarkTypography contains a plain,
JSON-representable completion record:
| Field | Description |
|---|---|
blocksProcessed | |
blocksProcessed | Supported paragraph, heading, and table-cell blocks visited. |
textNodesProcessed | |
textNodesProcessed | Mutable text nodes inspected inside those blocks. |
charactersProcessed | |
charactersProcessed | UTF-16 code units inspected in the blocks’ logical phrasing streams. |
textNodesChanged | |
textNodesChanged | Text nodes whose values changed. |
replacementsApplied | |
replacementsApplied | Total accepted replacement ranges. |
apostrophesConverted | |
apostrophesConverted | Quote or apostrophe replacements applied. |
dashesConverted | |
dashesConverted | En-dash and em-dash replacements applied. |
ellipsesConverted | |
ellipsesConverted | Exact three-dot runs converted. |
multiplicationSignsConverted | |
multiplicationSignsConverted | Quantity separators changed from x to ×. |
widowMeasuresAdded | |
widowMeasuresAdded | Non-breaking widow measures inserted. |
timeTakenInMilliseconds | |
timeTakenInMilliseconds | Best-effort elapsed transform time. |
The final progress endpoint is emitted only after mutations and completion data are available.
Exported types
import type {
Opts,
RemarkTypographyCompletion,
} from "remark-typography";