Skip to the text

Real quotation marks, dashes and spaces, in 26 languages.

A library that fixes the typography of running text. Give it a string and a language. It gives the string back with the right quotation marks, dashes where there were hyphens, an ellipsis, and no-break spaces where a line should not break.

Install

Package manager
npm install @mokhniuk/typography

Version 0.11.0, a pre-release. Not on npm yet; it will be there at 1.0. Until then, pick git and install a release tag. The repository is private, so the machine needs SSH access.

Read the docs Try it in the playground

“Curiouser and curiouser!” cried Alice (she was so much surprised, that for the moment she quite forgot how to speak good English); “now I’m opening out like the largest telescope that ever was! Good-bye, feet!” — for when she looked down at her feet, they seemed to be almost out of sight…

From Alice’s Adventures in Wonderland. Edit it, or paste your own.

What it does

The engine is one function over a string. Everything else is a way of getting your text into it, or a way of seeing what it did.

Every format your text lives in
Plain text, an HTML string, a Markdown document, a rehype or hast tree, React components, the DOM, and a command line for files and CI. One engine under all of them. The API
A pack per language, and no rule that names one
Quotation marks and how they nest, the dash, the spacing habits, the one-letter words that may not end a line: each is data in a language pack, which is why adding a language is writing one file. The languages
Two registers
Display text is the default: interfaces, marketing, event pages. The literary register sets book-like text — British quotes in English, a dash for a line of dialogue where the language writes dialogue that way.
It tells you what it changed
explain returns every change with the rule that made it and the text before and after, so a surprise is a question with an answer, not a mystery. The rules
Hanging punctuation, measured
An opening mark hangs in the margin so the first letter lines up with the lines under it. Each mark’s width is measured in the browser once; nothing re-measures when lines move. How it works
It knows what your font cannot draw
A rule that would insert a character the font has no glyph for leaves the text as you typed it — better a straight quote than a box. In a browser the list writes itself from the page’s own font. The font checker
Safe on what is not prose
Web addresses, emails and code spans come back byte for byte; tags, attributes and the contents of pre and code are never touched. An unknown rule name throws instead of passing quietly.
Proved, not promised
Property tests on seeded random text in every language: no letter or digit is lost or added, and a second pass changes nothing. A differential harness proves a speed change rewrote no output. Known limits

Other languages

Each language has its own pack: its quotation marks and how they nest, its dash, its habits with spaces. These four are not translations of one sentence; each is written the way that language is.

Curly quotes and apostrophes, a spaced dash, a real ellipsis, an en dash in ranges, and a number kept with its month.

Before
"It's five o'clock," she said - "and we're late..." The workshop runs 21-26 September, 10:00-12:00, in room 4B.
After 22 changes
“It’s five o’clock,” she said — “and we’re late…” The workshop runs 21–26 September, 10:00–12:00, in room 4B.

Ukrainian guillemets, the dialogue dash with its no-break space, and initials kept with the surname.

Before
- Зачекаємо? - спитав професор К. Новак. - Сказали: "вже п'ята година"... Квитки зі знижкою 15%.
After 14 changes
— Зачекаємо? — спитав професор К. Новак. — Сказали: «вже п’ята година»… Квитки зі знижкою 15%.

German marks, nested the German way, and a range that is a range.

Before
Sie sagte: "Er rief 'Halt' - und blieb stehen" ... Seiten 12-14, ca. 20 Min. am Tag.
After 16 changes
Sie sagte: »Er rief ›Halt‹ – und blieb stehen« … Seiten 12–14, ca. 20 Min. am Tag.

Polish low-and-high quotes, and one-letter words held to the next word so no line ends on them.

Before
Powiedział: "Idę do domu" - i poszedł. W sali nr 5 jest 20 osób, a w drugiej a w trzeciej 12.
After 20 changes
Powiedział: „Idę do domu” — i poszedł. W sali nr 5 jest 20 osób, a w drugiej a w trzeciej 12.
Set by the engine when this page was built. Marked: what the engine replaced, and what it put in its place; a boxed space is a no-break space.

What it changes

22 rules run by default, and 11 more you switch on. Here are some of them on one line each. A boxed space is a no-break space.

RuleYou writeYou get
quote "Hello" “Hello”
apostrophe don't don’t
ellipsis Wait... Wait…
dash.range pages 21-26 pages 21–26
dash.sentence Wait - what? Wait — what?
dash.dialogue - Привіт, - сказала вона. — Привіт, — сказала вона.
space.number 10 km 10 km
space.initial T. H. Shevchenko T. H. Shevchenko
space.orphan Jadę z Anią w góry Jadę z Anią w góry
symbol.legal (c) 2026 © 2026
symbol.multiply 1920x1080 1920×1080
symbol.minus -5 C −5 C
number.group opt-in 1000000 1,000,000

All 33 rules, with their reasons

Using it

One function takes a string and a language. The other entry points do the same for HTML, Markdown, rehype trees, React and the DOM.

import { typographic, explain } from '@mokhniuk/typography';

typographic('He said "we\'re here" - and left.', 'en', { quoteStyle: 'us' });
// He said “we’re here” — and left.

explain('He said "hi"... 21-26', 'en', { quoteStyle: 'us' }).changes;
// quote “ · quote ” · ellipsis … · dash.range –
ImportFor
@mokhniuk/typographyA string. explain lists each change and the rule behind it.
…/htmlAn HTML string. Tags and code are untouched.
…/markdownA Markdown document.
…/rehypeA unified or rehype pipeline, or any hast tree.
…/reactTypeset and TypographyRoot, with hanging punctuation.
…/domHanging punctuation in the browser, without a framework.
typoThe command line, for files and CI.

The API reference

What isn’t finished

Before you depend on it, you should know where it is weak.

  • No language has been read by a native speaker. The packs follow published style guides, and some of those we could only read in part. Each language page lists what it was written from.
  • Where the engine and Unicode’s CLDR disagree about quotation marks (English, German, French, Swiss German), the decision record says so, instead of choosing quietly.
  • English sets “” by default. That is this project’s house style. Pass quoteStyle: 'us' for “”.
  • Some input can’t be read with certainty. An inch mark inside a quotation (a 5" hole) is one. Such cases are listed as limits, not guessed at.

Known limits · Languages and their review status

Why it matters

“Typography is what language looks like. A straight quote, a hyphen standing in for a dash, a number torn from the word it counts: each is a small lie about the text, and a reader feels all of them at once without being able to name one.”

The opening mark of that quotation hangs in the margin, where the engine put it. Turn it off in the playground and watch the first line step inward. Why this is hard to automate

Articles

Where each decision came from, and what a reader should know about the marks themselves. Written while building the engine.

  1. · 3 min read

    Typing typographic characters on Windows

    Alt codes on the numeric keypad, the Win + . symbol panel, and Word’s own shortcuts — three ways to type every dash, quote and sign, with the full code table.

  2. · 3 min read

    Why typographic correction is hard to automate

    Replacing straight quotes with curly ones looks like a dozen regexes. Here is what the dozen regexes get wrong, and what an engine has to do instead.

  3. · 2 min read

    The no-break space does the quiet work

    The most useful typographic character is invisible. Where a no-break space belongs, why CSS cannot place it for you, and the rules that set it.

All the articles

Support it

Typography Engine is a solo project by Oleg Mokhniuk: the rules, the language packs, the docs and this site are one person’s work.

If it is useful and you want to give something back, skip the coffee. The best way to support this project is a donation to Come Back Alive, the Ukrainian foundation that equips the people defending Ukraine.

Donate to Come Back Alive

Or support the author directly: paypal.me/mokhniuk.