Skip to content

Fixers

phil/typography

Each fixer implements Fixer — fix(Run $run): Run — and can be used alone, in a Typographer of your own or through a preset. They are listed here in the order the presets run them; the order matters: quotes are made before French spaces are placed around them, and hyphenation comes last.

Name in presetsClassWhat it does
unicode_normalizationUnicodeNormalizationNormalization form C: e and a combining accent become é
ellipsisEllipsis... and . . . become …
dimensionDimension1920 x 1080, 20cm x 30cm, 6' x 8' take ×; 0x1F stays
dashDash--- → —, -- → –; 1914-1918 → 1914–1918; - → the dash of the style
smart_quotesSmartQuotes"…" and '…' become the quotation marks of the language
curly_quoteCurlyQuoteEvery remaining ' becomes the apostrophe ’
no_space_before_commaNoSpaceBeforeCommaword , word and word ,word become word, word
space_before_punctuationSpaceBeforePunctuationThe French spaces of : ; ! ? and « »
trademarkTrademark(tm) → ™, (r) → ®, (c) 2026 → © 2026, (p) 1999 → ℗ 1999
unitUnitThe space of 50 km, 20 °C, 75 %, 12,50 $ becomes a no-break space
hyphenHyphenSoft hyphens in words of five letters or more, where the patterns allow
word_breakWordBreakZero-width spaces between Thai words

Ellipsis

Three full stops close together or spaced by single spaces. Four stops are an ellipsis and a full stop.

Dimension

The letter x or X, with or without a space on either side, between a number and a number. The first number may carry a unit — mm, cm, dm, m, km, in, ft, px, pt, em, rem — or a foot or inch mark. A chain such as 10x20x30 is fixed throughout. A hexadecimal number (0x1F), a number after a letter (A4x5) or after a decimal mark is left alone.

Dash

WrittenBecomes
---em dash —
--en dash –
1914-1918, 12-15, (5-7)en dash between numbers of one to four digits
2026-11-06, 555-123-4567, 1.5-2unchanged: a chain or a decimal number is not a range
this - thatthis – that (DashStyle::Spaced) or this—that (DashStyle::Closed)

A phone number written in two groups, 555-1234, looks like a range and gets an en dash; write it with a no-break hyphen (U+2011) or leave the fixer out where such numbers are common.

Smart quotes

A double quote opens after the start of the block, a space or an opening mark — ( [ { « “ ‘ „ ‹ - – — / > or another quote — when a character follows it; otherwise it closes. > counts as an opening mark because a tag escaped as text, &lt;b&gt;"word", ends with it, as does a marker of quoted text. A single quote is an inner quotation mark only when it opens in that position and a matching single quote closes before a space, a punctuation mark or <; every other single quote is an apostrophe, left to CurlyQuote. The marks come from a QuoteStyle, English by default:

new SmartQuotes(new QuoteStyle('«', '»', '‹', '›'));

Known limits: rock 'n' roll is read as an inner quotation of n; a double quote inside a double quotation is a quotation of its own, never an inner one.

Curly quote

A plain replacement of ' by ’: placed after SmartQuotes, it turns the apostrophes that fixer left. Used alone, it turns every straight single quote.

No space before a comma

Spaces before a comma are removed; when a letter follows the comma directly, a space is put after it. 1,5 and a ,5 keep their digits together.

French spacing

SpaceBeforePunctuation writes the space before high punctuation that ends a word — whatever the author typed: none, a space, a no-break space, a thin space — and inside guillemets. See Locales for the rules of France and of Canada. The colon of 12:30 or of https:// is not followed by a space and is left alone; ?! takes a single space, before the first mark.

Trademark

(tm) and (r), in either case, are attached to the word before them. (c) and (p) become © and ℗ only when a year follows, held to it by a no-break space, so the item (c) of a list stays as written; © 2026 gets its no-break space too.

Unit

Only an existing space is changed — the fixer never adds one. The units are listed in Unit::UNITS: SI units and their common multiples, data sizes, typographic units, speeds, temperatures, percentages and currency signs. A word that merely begins like a unit (5 mice) is left alone.

Hyphen

Hyphen takes a Hyphenator and a minimum length, five characters by default. A word is a run of letters and combining marks; digits, emoji and symbols end it. A word that already holds a soft hyphen is left as its author wrote it, so fixing twice gives the same text. See Hyphenation.

Word break

WordBreak puts a zero-width space (U+200B) between the words of each run of Thai characters, at the line-break opportunities ICU finds with its Thai dictionary, never before the repetition mark ๆ. Other scripts are left alone, and a zero-width space already written ends a run, so fixing twice gives the same text. See Hyphenation.

Writing a fixer

final readonly class NoBreakAfterShortWords implements Fixer
{
    public function fix(Run $run): Run
    {
        return $run->replace(static fn (string $text): string => preg_replace('/\b(\p{L}{1,2}) /u', "\$1\u{A0}", $text) ?? $text);
    }
}

Run::replace() gives the fix the part between its neighbouring characters and keeps the result only if they are unchanged; Run::map() gives the part and its neighbours separately; Run::characters() gives every character of the run at once. A fixer must only rewrite text: what it returns is written into text nodes, so < stays a character.