Documentation

Rule order

The fixed order in which the nine polytypo rules run, why each sits where it does, why ranges is off by default, and how options never reorder them.

Updated · spec 1.12.0

transform runs nine rules in one fixed order. The order is part of the specification, not an implementation detail, and it is identical in all five runtimes.

The order #

OrderRuleDefaultRole
10spacesoncollapse repeated spaces, strip spaces before punctuation and inside brackets
20ellipsisonthree dots to the ellipsis character
25rangesoffnumeric and date range dash
30dashesonparenthetical dash
35hyphenonnon-breaking hyphen in listed forms
40quotesonstraight quotes to the locale's pairs
50apostropheonremaining straight apostrophes
60symbolson(c), (r), (tm) and the multiplication sign
70nbsponno-break and narrow no-break spaces

Why the order matters #

Each rule reads the output of the ones before it, so some orderings are required rather than chosen:

  • spaces is first so every later rule sees clean spacing.
  • ranges runs before dashes. A digit-hyphen-digit shape belongs to ranges alone, whether or not ranges is enabled; dashes never reinterprets it as a parenthetical dash.
  • dashes runs before hyphen. By the time hyphen runs, dashes has already declined every hyphen that sits between letters.
  • quotes runs before apostrophe. A straight apostrophe can be evidence that a single quote is part of a quotation. quotes uses that evidence first; apostrophe then converts whatever is left.
  • nbsp is last so every other rule has settled the characters around the spaces it binds.

The order also underpins idempotency: a rule may create work for a later rule but never for an earlier one.

Examples #

The input holds a digit range and a parenthetical dash. ranges is off, so the range keeps its hyphen while dashes converts the --:

transform(input, { locale: "en-US" })

Input

5-10 -- "x"

Output

5-10—“x”

Turning ranges on also converts the range, bound with word joiners, and dashes still handles the rest:

transform(input, { locale: "en-US", rules: { ranges: true } })

Input

5-10 -- "x"

Output

5⁠–⁠10—“x”

Options and order #

ranges is the only rule that is off by default. A range and a compound label such as Figure 5-10 have the same shape, and telling them apart would need the preceding word, which the rules never read. It is an explicit opt-in.

The rules option switches rules on or off by id. It never reorders them. A disabled rule is removed from the sequence and the rest keep their positions, and the order in which you list ids does not matter. Here dashes is off, so the spaced -- stays while ranges still runs:

transform(input, { locale: "en-US", rules: { ranges: true, dashes: false } })

Input

5-10 -- "x"

Output

5⁠–⁠10 -- “x”

Edge cases #

  • Rule ids are public API. They appear in the rules option. An unknown id raises POLYTYPO_UNKNOWN_RULE.
  • Which rules a locale affects varies. spaces, apostrophe and symbols read no locale data; the others read the fields listed on their pages.

Normative text: spec/rules/order.json.

Already using polytypo? Add our badge

Two lines: a script tag and a span. No per-embed network call. Pick a language and a theme, then copy the code into your footer or credits. Embedding notes cover Content Security Policy. Sites with the badge are listed on Sites using polytypo.

<script async src="https://polytypo.dev/badge.js"></script>
<span data-polytypo-lang="en" data-polytypo-theme="light"></span>

Search runs in your browser. Nothing is sent anywhere.