What changed, and what it costs
Every released version, and what it does to text you already have.
Versions here are spec versions, and every runtime publishes the same number:
1.19.0 on npm, PyPI, pkg.go.dev, RubyGems and Packagist is the same
spec and the same bytes out. Only released versions appear — what sits unreleased in the
repository is not a changelog entry.
Each entry is a behaviour change with its cost stated: a rule that starts firing where it did
not before also fires somewhere you may not want it, and both sides are pinned by fixtures. In
the tables below, ⍽ stands for U+00A0 NO-BREAK SPACE and · for U+202F
NARROW NO-BREAK SPACE; neither is in the output itself — showing them is the only way a
changelog can say what the nbsp rule actually did.
Greek parenthetical dashes are converted: an em dash with a space outside the pair, none inside.
- Until now
elleft a spaced hyphen or a double hyphen alone, soΠερπατήσαμε -- σχεδόν 3 χλμ -- και το βρήκαμε κλειστό.came back unchanged. It now comes back asΠερπατήσαμε —σχεδόν 3 χλμ— και το βρήκαμε κλειστό., andΤο σχέδιο - αν υπάρχει - αποτυγχάνει.asΤο σχέδιο —αν υπάρχει— αποτυγχάνει.. This is the form the Greek edition of the EU Interinstitutional Style Guide describes: an em dash, a space before the first and after the second, none inside. A pair typed closed up or with spaces inside is brought to the same form, and a pair already written that way is left as typed. - The em dash follows that guide. The state school grammar appears to use an en dash in the same role; the two sources disagree and none of them ranks the other, so the cited one was chosen.
- The rule works as it does for Spanish since 1.18.0: it converts a sentence only when that sentence holds exactly two dashes, and leaves the rest as typed. A lone spaced hyphen such as
Αθήνα - η πρωτεύουσα της Ελλάδας.is not touched. The accepted cost, not measured on a Greek corpus: a list of exactly three items joined by spaced hyphens is read as a pair, soΑθήνα - Θεσσαλονίκη - ΠάτραbecomesΑθήνα —Θεσσαλονίκη— Πάτρα. Numeric ranges are still not touched in Greek.
| Locale | Mode | In | Before | After |
|---|---|---|---|---|
| el | text | Το σχέδιο - αν υπάρχει - αποτυγχάνει. | Το σχέδιο - αν υπάρχει - αποτυγχάνει. | Το σχέδιο —αν υπάρχει— αποτυγχάνει. |
| el | text | Περπατήσαμε -- σχεδόν 3 χλμ -- και το βρήκαμε κλειστό. | Περπατήσαμε -- σχεδόν 3 χλμ -- και το βρήκαμε κλειστό. | Περπατήσαμε —σχεδόν 3 χλμ— και το βρήκαμε κλειστό. |
| el | text | Αθήνα - η πρωτεύουσα της Ελλάδας. | Αθήνα - η πρωτεύουσα της Ελλάδας. | Αθήνα - η πρωτεύουσα της Ελλάδας. |
| el | text | Αθήνα - Θεσσαλονίκη - Πάτρα | Αθήνα - Θεσσαλονίκη - Πάτρα | Αθήνα —Θεσσαλονίκη— Πάτρα |
Spanish parenthetical dashes are written the way the DPD describes them: a space outside the pair, none inside.
- Since 1.17.1
esgave a spaced hyphen or double hyphen an em dash with a space on each side of every dash, which left spaces inside the pair that the DPD of the RAE and ASALE does not allow. Measured on 1.17.5:El plan - si existe - fracasa.came back asEl plan — si existe — fracasa.. It now comes back asEl plan —si existe— fracasa., andCaminamos -- casi 3 km -- y estaba cerrada.asCaminamos —casi 3 km— y estaba cerrada.. A pair typed closed up,El plan—si existe—fracasa., or with spaces inside,Llegó — por fin — a casa., is brought to the same form, and a pair already writtenLlegó —por fin— a casa.is left as typed. - The rule can only tell which dash opens and which closes by counting, so it converts a sentence only when that sentence holds exactly two dashes. Anything else is left as typed: a lone spaced hyphen such as
Madrid - la capital de España.(1.17.5 turned it intoMadrid — la capital de España., which the DPD does not describe), a sentence with two or more pairs, a pair split by a full stop followed by a space such asp. ej., a pair split by a line break, and any sentence that also holds a dialogue dash such as—Hola —dijo—.. A sentence ends at.,?,!or…followed by a space. - The accepted cost, measured on 24,945 sentences from 50 long articles of the Spanish Wikipedia. The 202 sentences that carried a typed pair were not changed in their dashes. With the pair retyped as spaced hyphens, the form it was written in was restored in 94 of the 95 sentences the sample could settle. Against that, 18 of the 194 sentences with a spaced hyphen and no em dash were converted, and most of those are lists, not asides:
vía Echternach - Larochelle - Merschbecamevía Echternach —Larochelle— Mersch, and 12 of the 18 came from the itineraries of one article. A list of exactly three items joined by spaced hyphens is read as a pair. - Greek is unchanged. Its source asks for the same spacing, but the sources disagree on whether the dash is an em or an en dash, and that is not settled. Numeric ranges and compounds are not touched, as before.
| Locale | Mode | In | Before | After |
|---|---|---|---|---|
| es | text | El plan - si existe - fracasa. | El plan — si existe — fracasa. | El plan —si existe— fracasa. |
| es | text | Caminamos -- casi 3 km -- y estaba cerrada. | Caminamos — casi 3⍽km — y estaba cerrada. | Caminamos —casi 3⍽km— y estaba cerrada. |
| es | text | Madrid - la capital de España. | Madrid — la capital de España. | Madrid - la capital de España. |
| es | text | Madrid - Barcelona - Sevilla | Madrid — Barcelona — Sevilla | Madrid —Barcelona— Sevilla |
| es | text | Juan - mi primo - llegó y - según dicen - durmió. | Juan — mi primo — llegó y — según dicen — durmió. | Juan - mi primo - llegó y - según dicen - durmió. |
Deleting a space no longer creates a task-list marker or a footnote reference in `markdown` mode.
- A space inside square brackets is deleted, and when what was left read as
[x]or[^label], a Markdown parser with the GFM extensions read it as structure. Measured on 1.17.4 (en-GB,markdown):- [ x] itemcame back as- [x] item, a checked task-list item;- [x ] itemdid the same from the other side;- [ ] itemcame back as- [ ] item, an unchecked one;a [ ^1] bcame back asa [^1] b, a footnote reference when a definition[^1]: …exists. These deletions are now left unmade. The same deletion in[ word ]or( x )still happens.text,htmlandyamlmodes are unchanged. polytypo-pythonread an unmatched[…]as two boundaries instead of text, so spaces inside brackets that match no definition were never tightened. Measured on 1.17.4 (en-GB,markdown):a [ word ] bcame back asa [ word ] b. It now comes back asa [word] b, as it already did in the other packages.- The accepted cost: the check looks at the shape, not at the document, so
a [ x] bin the middle of a sentence keeps its space although no list item is there, and so do[ x ],[ ^1]and[ ]in prose. A footnote label that holds an inline element, such as[^*a*b ], is not recognised and is still tightened.
Deleting a space no longer creates a backslash escape or a character reference in `markdown` and `html` modes.
- A space before punctuation is deleted, and when the character before it was a backslash or the start of a character reference, the text that came back read differently. Measured on 1.17.3 (
en-GB,markdown):foo\ , barcame back asfoo\, bar, where the backslash now escapes the comma and the text renders without it;AT& ;Tcame back asAT&T, which renders asAT&T. Inhtml,<p>a &hellip ;b</p>came back as<p>a …b</p>, which renders asa …b. These deletions are now left unmade; the same deletion beside ordinary text still happens.textandyamlmodes are unchanged. - The Go and Ruby packages also read a backslash escape as ordinary text, so a rule could act on the escaped character. Measured on 1.17.3: in Ruby
it\'s finecame back asit\’s fineand"x \" y"(en-GB) as‘x \’ y"; in Gox\--ycame back asx\ – y. An escaped character, and a character reference the parser recognises, is now left as typed inmarkdownmode in both. - The accepted costs: the check looks at the shape of a reference (
&, letters or digits,;), not at the list of names, soAT&T ;xkeeps its space inen-USalthough&T;is no reference; a space after a literal backslash and before punctuation is kept, also inside an HTML block in a Markdown document, where the backslash is not an escape.
No change in output: the test files for `en-US`, `de-DE` and `fr` were tidied.
- Housekeeping release. The conformance fixtures for
en-US,de-DEandfrwere reformatted with the project's code formatter: short lists now sit on one line and long ones are wrapped. Every case, every input and every expected output is the same, and so is each rule and each mode. Text you run through polytypo comes out exactly as it did on 1.17.2.
Markdown mode no longer changes how a document parses: unpaired `*`, `_` and `~`, bare web addresses and reference links are left alone where a fix would alter them.
- In
markdownmode three kinds of edit could make the output parse differently from the input. Measured on 1.17.1:see booking._ , emails.booking_ here(en-GB) came back assee booking._, emails.booking_ here, and the two underscores now formed emphasis that the text never had.x _ -- y_inen-USand a double asterisk before!infrdid the same. These edits are now left unmade when they would change whether a space stands beside an unpaired*,_or~. The same edit beside ordinary text is still made. - A quotation mark, an apostrophe or
...directly after a bare web address (http://,https://,www.) was converted to a character that the parser then read as part of the link. Measured on 1.17.1:Visit "https://example.com/a" nowbecameVisit ‘https://example.com/a’ now, and the link address ended in ’. The address and everything attached to it, up to the next space, is now left as typed, and so is a pair of quotation marks around it. A spaced hyphen after the address still becomes the locale's dash; a dash that would remove the space after the address, and the no-break space before:,;,!or?infr, are left as typed. - The text of a shortcut or collapsed reference link,
[Smith's book]or[Smith's book][]with its definition[Smith's book]: …, is the label that is matched against the definition. Converting its apostrophe made it match nothing, and the link disappeared. The text, the alt text of such an image, and the labels of footnote references and definitions are now left as typed. Brackets that match no definition, and the text of a full reference[text][ref], are processed as before. - The accepted costs: the check looks at whether a space is beside the character, not at the full rules for emphasis, so it also leaves alone an edit that would have been harmless (
«~25infrkeeps its tight form); text that matched no definition and matches one after conversion ([it's]with[it’s]: …elsewhere) becomes a link.textandhtmlmodes are unchanged.
Spanish text gets its parenthetical dashes: a hyphen or double hyphen between spaces becomes an em dash.
- Until now
esleft a typed-or--between spaces exactly as it was. Measured on 1.16.0:El plan - si existe - fracasa.andCaminamos -- casi 3 km -- y estaba cerrada.came back unchanged.esnow givesEl plan — si existe — fracasa.andCaminamos — casi 3 km — y estaba cerrada., with an em dash (U+2014) as the DPD of the RAE and ASALE describes the raya. - The spacing is an approximation, and this is the accepted cost. The DPD writes the pair closed up on the inside and spaced on the outside,
Llegó —por fin— a casa. Locale data can only say "a space on both sides of every dash" or "on neither", so the output carries spaces inside the pair that the DPD does not allow. Text already written in the DPD form,Llegó —por fin— a casa., and dialogue lines such as—Hola —dijo—. ¿Qué tal?are left as typed. - Two more inputs change that the norm would write differently. A tight pair,
El plan—si existe—fracasa., becomesEl plan — si existe — fracasa.. A single spaced hyphen,Madrid - la capital de España., becomesMadrid — la capital de España., although the DPD does not use the raya between subject and predicate. - Numeric ranges and compounds are not touched:
las páginas 23-45andhispano-árabestay as typed, because the DPD writes both with the hyphen. Greek is the other locale the spacing limit affects, and its dashes are unchanged.
Lithuanian joins as the twenty-seventh locale, cited to the Lithuanian punctuation rules published with the approval of the State Commission of the Lithuanian Language.
- Lithuanian ships as
lt. Until now Lithuanian text had to be skipped or run through a foreign locale, and the second has a cost. Measured:Jis pasakė: "Tai 'citata' iš knygos" - žinoma - ir išėjo.came back fromen-GBasJis pasakė: ‘Tai “citata” iš knygos’ – žinoma – ir išėjo., the English marks and the nesting inverted.ltgivesJis pasakė: „Tai „citata“ iš knygos“ – žinoma – ir išėjo.: the low-high pair„…“for both levels, as Lithuanian prints it, no space inside, and an en dash with spaces. - Both quotation levels use the same pair, so when the inner mark touches the outer one, as in
"Tai 'citata'", the engine declines the pair: the text is left as typed or converted only in part. Lithuanian writes the colliding closing marks once, which locale data cannot express. - After
?or!an ellipsis is written with two dots, as inKą pasakysiu?... Two inputs the norm treats differently come out the same way:?!...becomes?!..where the norm gives?!., and!!!...becomes!!!..where the norm keeps three dots. - With the
rangesrule on, a numeric range is closed up with an en dash and no-break joiners either side,1999–2000 m.. A range whose members are already joined by a dash,2010–2011 – 2012–2013, is left as it is. - No-break spaces are put between a number and
%,proc.,°C,kg,km,cm,mm,EUR,Eur,€,mln.,val.,min.,m.andd., insidet. y.,t. t.andš. m., and after§. These come from the style guides of the European Commission and of Vilnius University, which state the intent; the rules consulted give no no-break rule. Single-letter units such asmare left alone, because they are also common words. Initials, which the same guides bind, are left alone as a house-style rule only; one-letter prepositions and hyphenated forms are not bound, since no source gives a rule for them. - Pass
lt(or a region form such aslt-LT). Script-tagged identifiers are refused withPOLYTYPO_UNKNOWN_LOCALE, as before.
Slovenian joins as the twenty-sixth language, cited to the Slovenski pravopis of the Slovenian Academy of Sciences and Arts.
- Slovenian ships as
sl. Until now Slovenian text had to be skipped or run through a foreign locale, and the second has a cost. Measured:Rekel je: "To je 'citat' iz knjige" - seveda - in odšel.came back fromen-GBasRekel je: ‘To je “citat” iz knjige’ – seveda – in odšel., the English marks and the nesting inverted.slgivesRekel je: »To je ›citat‹ iz knjige« – seveda – in odšel.:»…«for the first level, the single mark›…‹for the second, no space inside, and an en dash with spaces. Text that already carries»…«, or the guillemets the other way round, or the low-high pair„…“, comes out in the same form. - With the
rangesrule on, a numeric range is closed up with an en dash and no-break joiners either side,vojna 1941–1945, as the Pravopis writes it. - No-break spaces are put between a number and
%,‰,€,°C,kgorkm, insidet. i., and after§. The‰,€and§bindings come from the draft of the next Pravopis, not from the edition in force, which shows only the spaced form. Single-letter units such asmare left alone, because they are also common words and initials. One-letter prepositions are not bound, and no hyphenated form is protected: the Pravopis gives no rule for either. - The Pravopis prescribes
»…«for print and assigns the low-high pair„…“to handwriting; the choice of›…‹for the second level is a pick among forms the Pravopis lists, since its own example nests other marks. An ellipsis stays breakable after an author's space; the draft's advice to bind it is not adopted, because it would break an ellipsis inside a word. - Pass
sl(or a region form such assl-SI). Script-tagged identifiers are refused withPOLYTYPO_UNKNOWN_LOCALE, as before.
Serbian joins as the twenty-fifth language, cited to the Pravopis of Matica srpska as analysed in a University of Belgrade monograph.
- Serbian ships as
sr. Until now Serbian text had to be skipped or run through a foreign locale, and the second has a cost. Measured:Рекао је: "Ово је 'цитат' из књиге" - наравно - и отишао.came back fromen-GBasРекао је: ‘Ово је “цитат” из књиге’ – наравно – и отишао., the nesting inverted and the native marks gone.srgivesРекао је: „Ово је ’цитат’ из књиге” – наравно – и отишао.:„…”for the first level, the single mark’…’for the second, no space inside, and an en dash with spaces. The Latin spelling gets the same marks, because no rule found depends on the script. - With the
rangesrule on, a numeric range is closed up with an en dash and no-break joiners either side,рат 1941–1945. The accepted cost: a range whose members have several words is spaced in the Pravopis (рат 6. IV 1941 – 9. V 1945), and locale data cannot tell the two apart. - What Serbian does not do, because no source consulted says it: no no-break space is put between a number and a unit or percent sign, after one-letter words, or inside initials, and no hyphenated form is protected. The Pravopis itself is a paid text and was read through a University of Belgrade monograph on Serbian punctuation, so the quotation marks and dashes rest on that analysis. The Pravopis prescribes
„…”; the dictionaries of Matica srpska and of the Serbian Academy use„…“, andsrfollows the Pravopis. - Pass
sr(or a region form such assr-RS).sr-Latnandsr-Cyrlare refused withPOLYTYPO_UNKNOWN_LOCALE, as every script-tagged identifier already is.
`protect` names the parts of a string polytypo must not touch, so template literals and ICU messages keep their context.
transformandanalyzeintextmode take aprotectoption: a list of{ start, end }ranges of the input, in code points,[start, end), that are never edited and that the rules read as content. Until now a string with interpolation had to be split at every hole and each fragment run alone, which loses the neighbour on the far side. Measured: the fragment- Rulesafter${x}came back unchanged inen-USanden-GB; the whole literal${x} - Ruleswithprotect: [{ start: 0, end: 4 }]comes back${x}—Rulesinen-USand${x} – Rulesinen-GB, as the unsplit string does.- It covers ICU MessageFormat, where an apostrophe next to a brace is an escape.
a '{b}' cinen-GBcame backa ‘{b}’ c, which is a placeholder between curly quotes and not the literal text{b}. Withprotect: [{ start: 2, end: 7 }]over'{b}'it comes back unchanged, and the text around it is still typeset. There is noicumode; you mark the ranges and polytypo reads no host syntax. - Offsets are Unicode code points, not UTF-16 units and not bytes, so an emoji before a hole does not shift them. The list must be ascending and non-overlapping, an empty range or one past the end of the input is refused, and so is a non-empty
protectinhtml,markdownoryamlmode, all withPOLYTYPO_INVALID_OPTION. An empty list is the same as no option. The protected text is returned exactly as written. - The accepted cost: text next to a hole converts less than the same text would unsplit, never more. A character polytypo would insert beside the hole is not written, so
Bonjour ${name}!infrstays as typed where the unprotected string gets a narrow no-break space before the!, and${a}--${b}inde-DEstays--where the unprotected string becomes${a} – ${b}. A hole that contains a line break counts as a line break to the rules, so a dash afterx ${\n y\n}is left alone. - Running the output through again is a fixed point only if you move the ranges to where the edits left them:
...becoming…shortens the text before a hole.transformdoes not return the moved ranges. analyzereported the end of a change beside a skipped element too far on. Inhtmlmode<p>x--<code>cc</code>y</p>inen-USreported the--as offsets 4 to 21, running across the<code>element; it now reports 4 to 6. The same applies inmarkdownandyamlmode, and to a change beside a protected range.transformoutput is unchanged.
Romanian, Hungarian, Bulgarian and Croatian join, taking polytypo to twenty-four languages, each cited to its national orthography.
- Romanian ships as
ro, cited to the Academia Română's Îndreptar (ÎOOP6) and DOOM3. Until now Romanian text had to be skipped or run through a foreign locale, and the second has a cost. Measured:Ea a spus: "Citește 'Luceafărul' acum" - sigur - și a plecat.came back fromen-GBasEa a spus: ‘Citește “Luceafărul” acum’ – sigur – și a plecat., the nesting inverted and the native marks gone.rogivesEa a spus: „Citește «Luceafărul» acum” – sigur – și a plecat.:„…”for the first level, the guillemets«…»for the second, no space inside, and an en dash with spaces.km,kg,cm,mm,km/hand°Cafter a number,art.andnr.before one, and a single initial before a surname get a no-break space. - Hungarian ships as
hu, cited to A magyar helyesírás szabályai (AkH12). Measured:Azt mondta: "olvasd az 'Egri csillagok'-at" - biztosan - és elment.came back fromen-GBasAzt mondta: ‘olvasd az “Egri csillagok”-at’ – biztosan – és elment.;hugivesAzt mondta: „olvasd az »Egri csillagok«-at” – biztosan – és elment., the Hungarian marks„…”and»…«. - Bulgarian ships as
bg, cited to the Institute for Bulgarian Language (BAS) and the Официален правописен речник (ОПР 2012). Measured:Той каза: "не" - сигурно - и си тръгна.came back fromen-GBasТой каза: ‘не’ – сигурно – и си тръгна.;bggivesТой каза: „не“ – сигурно – и си тръгна.. The closing mark is U+201C, not U+201D as in Romanian and Hungarian, and a quotation inside a quotation takes the same„…“pair, as in ОПР. A numeric range, when therangesrule is on, is spaced:2000 – 2010 г.. - The accepted cost of Bulgarian's identical pair: a nested quotation whose inner mark touches the outer one, such as
"това е 'Славия'"with the inner quotation at the very end, is not converted as a nest, and the outer pair stays straight. Measured:Той каза: "това е 'Славия'".comes back with only the inner opening mark changed, to U+2019:Той каза: "това е ’Славия'"., because the apostrophe rule still reads it;"Кафене "Славия""stays entirely straight. The same text typed with the typographic marks is left alone. - Croatian ships as
hr, cited to the Hrvatski pravopis of the Institut za hrvatski jezik i jezikoslovlje. Measured:Rekao je: "pjesma 'Lijepa naša' je himna" - sigurno - i otišao.came back fromen-GBasRekao je: ‘pjesma “Lijepa naša” je himna’ – sigurno – i otišao.;hrgivesRekao je: „pjesma ‘Lijepa naša’ je himna” – sigurno – i otišao.:„…”for the first level and‘…’for the second, no space inside, and an en dash with spaces.%,‰,€,°Candkgafter a number, the section sign before one, and the abbreviationsdipl. ing.andd. o. o.get a no-break space. - The accepted cost of Croatian: the pravopis writes page ranges in bibliographies closed up,
str. 145–162, and prints other ranges spaced. Locale data cannot tell the two apart, so with therangesrule on,str. 145-162becomesstr. 145 – 162. The rule is off by default. A year range typed2010.-2012.is left as typed, because the period after the year is not a digit. - What the four do not do, because the sources say nothing: short words such as
u,s,kandiare not bound to the word after them, and no hyphenated-word list ships. Where a source gives two unranked forms, the choice is recorded in the locale file and is not presented as a citation.
Slovak joins as the twentieth language, cited to the Jazykovedný ústav Ľudovíta Štúra; in YAML, a blank line after a value no longer stops polytypo from typesetting it, and text inside a block or quoted scalar is no longer mistaken for a listed key.
- Slovak ships as
sk. Until now Slovak content had to be skipped or run through a foreign locale, and the second has a cost. Measured:Povedal: "Toto je 'citát' z knihy" - prirodzene - a odišiel.came back fromen-GBasPovedal: ‘Toto je “citát” z knihy’ – prirodzene – a odišiel., the nesting inverted and the native marks gone, and an already-correct„ahoj“became‘ahoj’.skgivesPovedal: „Toto je ‚citát‘ z knihy“ – prirodzene – a odišiel., the marks the Pravidlá slovenského pravopisu prescribe:„…“for the first level and‚…‘for the second, with no space inside. Dashes are en dashes with spaces, and, when therangesrule is on, a numeric range is spaced as well,1980 – 1990, where Czech closes it up. Beside a spaced dash a range is left as typed:a - 5-10givesa – 5-10. The spaced range is where the Czech data would have been wrong for Slovak, so it was read from the source, not borrowed. - What Slovak does not do, because the source does not say it: short words such as
v,s,kanduare not bound to the word after them, and initials are not bound to the surname. The Pravidlá have no such rule and the paid norm behind them could not be consulted, so nothing is added where nothing is cited. Abbreviationst. j.,a pod.anda. s., the referencesč.,ods.ands.before a number, the paragraph sign, and%,‰,kg,km,m/s,km/sandminafter a number do get a no-break space. The unit list rests on a university's published reading of the norm plus the SI brochure rather than on the Pravidlá themselves, and says so in the locale file. - In
yamlmode, a value followed by a blank line was left exactly as it was.summary: the one -- hereconverted, and the same file with a blank line after it came back untouched; a file that ends in a blank line, or one that separates its entries with blank lines, was silently skipped value by value. Both now convert:title: "a -- b"followed by a blank line comes back astitle: "a—b", inen-US, for plain, single-quoted and double-quoted values. A blank line followed by a more-indented line is still a continuation of the value above it and is still left alone. - The same applies to the YAML block at the top of a Markdown file when
frontmatterKeysis given: blank lines between entries and before the closing---no longer stop a listed key. - Also in
yamlmode: a carriage return that is not part of a normal line ending, inside a block scalar of a key you did not list, no longer lets polytypo rewrite the lines after it.run: |followed byecho a, a line starting with a bare carriage return, andtitle: c -- dcame back withtitle: c—deven thoughrunis not a listed key and that text is a script. A line with a stray carriage return now leaves every line after it alone, down to the next unindented line, inyamlmode and in the frontmatter block of a Markdown file withfrontmatterKeys. Ordinary\r\nfiles are unaffected. - Also in
yamlmode: a block scalar or a quoted scalar that starts on a line with no key — a list entry, or a header on a line of its own — no longer exposes its content lines.- |followed bytitle: c -- dis a block scalar whose content is the texttitle: c -- d, yet polytypo read the second line as atitlekey and came back withtitle: c—d; the same happened for- "xcontinued on a second line, for a quoted key, for a flow collection, an anchored or tagged block and an explicit?key, and- 'xcontinued on a second line even had its closing quote turned into a typographic apostrophe. These lines now stay exactly as written, inyamlmode and in the frontmatter block of a Markdown file withfrontmatterKeys. A line containing a tab, or a document marker followed by a node (--- |), is treated the same way. A key in quotes followed by its own nested keys, a list entry with nothing after the dash, and OpenAPI paths such as/users/{id}:are unaffected and still convert. - The accepted costs: a frontmatter block whose last line is a lone carriage return on a line of its own now converts the key above it, where before that key was left alone as a side effect of the same bug. In a nested YAML document with a stray carriage return, everything up to the next unindented line is left alone, including real listed keys. A quoted value followed by an indented line of any kind, including a comment, is still left alone, and so is the second key of a list item that holds a mapping (
- title: "x"thenslug: y). Measured on the author's own 367-file content directory, no file contains a stray carriage return. A line that holds only an anchor or a tag over a nested mapping now absorbs that mapping (- &xthentitle: a -- b), a line that starts with an anchor, a tag or a flow collection and holds a key absorbs the key after it, an explicit?key inside a list entry does the same, and a flow collection, a quote or a block header on a line of its own leaves everything up to the next unindented line alone, including real listed keys in a nested document; at the top level of a document that is the rest of the document, and so is a---line followed by a tag or a node. Nothing in a document is changed that was not already a listed value.
A stray carriage return no longer ends up inside a YAML value polytypo rewrites.
- In
yamlmode, a line holding a carriage return that is not part of a normal line ending is now left exactly as it was. Until now polytypo converted the text on that line and carried the carriage return along inside the part it rewrote:title: a<CR>b -- ccame back astitle: a<CR>b—c, andtitle: a<CR>#c -- drewrote text that YAML reads as a comment. Both now come back unchanged. The rule polytypo states for itself — line endings stay outside everything it rewrites — was not true of this case. - The same applies to a block scalar (
|or>): a carriage return inside one of its lines leaves the whole block alone, the way a tab already did. Ordinary line endings are unaffected —\r\nfiles, and a file whose last line ends in a bare\r, convert as before. Two carriage returns in a row at the end of a line count as one line ending plus a stray one, so that line is left alone. - The accepted cost: a value on a line with a lone carriage return, which is how old Mac files ended every line, is no longer typeset. Measured on the author's own 367-file content directory, no file contains one. In
markdownmode withfrontmatterKeysthe same decline already applied, so the two agree on a stray carriage return inside a line.
Single-quoted YAML now gets typeset — the apostrophe is the one thing it leaves straight.
- In
yamlmode, and in the frontmatter block ofmarkdownmode when you passfrontmatterKeys, a single-quoted value containing an apostrophe used to come back untouched, whole. YAML writes an apostrophe inside single quotes as two,'', and polytypo declined any value that held one. Nothing told you: the value looked like one that needed no changes. - It now converts everything around the
''and leaves the''itself alone.description: 'It''s a "quiet" year -- really...'inen-GBused to return unchanged and now returnsdescription: 'It''s a ‘quiet’ year – really…'. Infr,description: 'l''idee -- et ... fin'becomesdescription: 'l''idee — et … fin'. Double-quoted, plain and block scalars are unaffected. - The accepted cost: the apostrophe that
''spells stays straight, so a single-quoted value whose only convertible character was that apostrophe still comes back unchanged. Measured on 1913 values from a real 207-file site, de-typeset and re-written as single-quoted scalars in eight languages: 1104 came back unchanged before this version and 672 do now, 602 of them because the apostrophe was all there was to convert. The other 70 that remain include phrases written in single quotation marks, which are the same''seen from the other side. - Converting the apostrophe as well would mean polytypo decoding the YAML string and mapping positions back to your source, which five language builds would each have to do their own way. It is left straight, deliberately.
No change to what polytypo does to your text — the French locale now says which French it is.
- Nothing in this version changes your output. Run the same text through 1.8.0 and 1.8.1 and you get the same bytes, in every language and every mode.
- The one change is a name. The
frlocale was labelled “French” whilefr-CAwas “French (Canada)”, which read as iffrwere the neutral base of both. Its rules are those of France, cited from the French style guides in the locale file, and its label is now “French (France)”, the wayde-DEis “German (Germany)”. The locale code is unchanged, solocale: "fr"keeps working. - The accepted cost is on the reading side only: anything that displays the locale's
namefield, such as a locale picker built from the spec data, now shows the longer label.
Your frontmatter is metadata again — polytypo now decides where the block is, instead of asking four different parsers.
- Until now each language's build of polytypo found your frontmatter block by asking whatever Markdown parser it already had. Those parsers disagree, so the same file could come back with its metadata typeset in one language and untouched in another. Measured across twenty documents and four languages before this release: eleven disagreed somewhere.
- The most ordinary case is a trailing space.
---with one space after the dashes — which any editor that does not trim whitespace produces — used to be "not a frontmatter block" for the Go and Ruby builds, sotitle: "Une note"came back astitle: “Une note”anddate: "2026-09-26"grew quotation marks with it. It is a block now, in all of them, and its contents are left alone. - Same for a file that starts with a byte-order mark, which Windows editors add without asking: the Go and Ruby builds typeset the metadata behind it. And for the JavaScript build, a file with a mark and no frontmatter at all came back completely unconverted —
Wait for it...stayed as it was written. Both are fixed. - A fenced code block inside a metadata value no longer reaches the rest of your document. If a value contained three backticks, the Go build paired them with your body's own fence, typeset the contents of your code block, and skipped the prose after it. The block is now blanked out before the parser sees the file, so nothing inside it can open or close anything outside it.
- Files with old Mac line endings — a lone carriage return, no line feed — are handled rather than mishandled. The Ruby build used to raise a parse error on them; the Go build typeset the metadata. One accepted cost, stated: inside such a block
frontmatterKeysconverts nothing, because the part of polytypo that reads the block's contents still splits lines on line feeds alone. Your metadata is safe either way; it is the conversion you lose. A stray carriage return inside a single value costs that value and not the rest of the block. - Nothing here changes a document that has no frontmatter, and nothing changes what
frontmatterKeysdoes to the values you name — only which files it recognises as having a block at all.
Frontmatter you can opt into, key by key — the one string on a page that polytypo used to refuse.
markdownmode takes an optionalfrontmatterKeyslist: the frontmatter keys whose values are prose.transform(src, { locale: "en-GB", mode: "markdown", dialect: "mdx", frontmatterKeys: ["title", "description"] }). Without it nothing changes — the block is skipped whole, exactly as before — and there is no default list, because only you know which of your fields a reader sees and which a machine reads.- Why it was worth adding: on a site whose frontmatter carries the headline, the single most visible string on the page was the one string that came back untouched.
title: He said "hello" oncenow becomestitle: He said “hello” oncewhen you nametitle, whileslug,dateand everything else in the block keep every byte. - The block is processed separately from the body, which is not a detail you can see until it matters: an unbalanced quotation mark in
titlecannot reach into your first paragraph and pair with a mark there. Measured —---\ntitle: He said "hello\n---\n\nworld" she saidcomes back with both marks straight, where one pass over the whole document would have curled them into a quotation spanning the metadata and the prose. - It reads your frontmatter with the same scanner
yamlmode uses, so it inherits that mode's refusals rather than inventing new ones. The one worth knowing: a single-quoted scalar containing''is left alone, and an apostrophe inside single quotes is written exactly that way — sotitle: 'It''s a test'gets nothing. Double-quoted and plain scalars are unaffected. On a real 187-file site, 130 of the 247 convertible values would be skipped if they were written with single quotes and none are as written. - A TOML block (
+++) is still skipped whole whether you pass the option or not, and a key listed at any depth matches —titleis processed wherever it appears in the block, including nested underseo.
No change to what polytypo does to your text — the explanation corrected yesterday's correction was itself blaming the wrong mechanism.
- Nothing in this version changes your output. Run the same text through 1.6.2 and 1.6.3 and you get the same bytes. One paragraph of
quotes.mdchanged, and it is the third pass over the same paragraph today; this one is the mechanism rather than the measurement. - 1.6.2 established that a possessive apostrophe written flush against an inline element keeps its typewriter form when polytypo declines to pair it. That is measured and still true. What it got wrong was why. It said the apostrophe rule cannot see past an element boundary. It can, and it is documented doing so:
The <code>xs</code>'s printer works.comes back asThe <code>xs</code>’s printer works.The rule reaches that mark and curls it. What it does not reach is the plural,The <code>xs</code>' printer works., and the reason is what sits on the other side: the case that accepts an element boundary on its left needs a letter or digit on its right, and a plural possessive has a space there. The original explanation blamed a character class that plays no part in it, and in doing so contradicted a note in the apostrophe rule's own document, shipped in the same packages. - That is the failure this whole day was about, in miniature: prose that nothing checks, drifting from behaviour nobody re-measured, in documents four of the five packages ship. The documents are now verified against this repository on every push and again before every publish, which is how two of today's three corrections were found at all.
No change to what polytypo does to your text — the rule that explains one decision was measuring it wrong in its own favour.
- Nothing in this version changes your output. Run the same text through 1.6.1 and 1.6.2 and you get the same bytes. What changed is one passage of normative prose in
quotes.md, and both things it got wrong made a decision look cheaper than it is. - The decision is what polytypo does with a possessive apostrophe written flush against an inline element, as in
The <code>xs</code>' printer works.Nothing can tell that mark apart from a quotation closing in the same place, so it is read as one. The passage said the character a reader sees there is right either way, and that only the pairing is spent. Measured on the published package: it is not. Declining to pair the mark leaves it a typewriter apostrophe, because the rule that would curl it needs a letter on the far side and finds a space. So both options leave one straight mark in that sentence, in different places — the choice moves it rather than removing it. - The same passage priced the alternative at "7 of 2801 conformance cases". 2801 is the test count of one implementation's conformance runner, which no other implementation can reproduce. The real figure is six of the suite's 1366 cases. Five are Turkish, where an apostrophe carrying a case suffix is grammar rather than style, and the sixth is the case that pins this behaviour: the alternative closes the pair at the author's own mark, which is what you want, and leaves the typewriter apostrophe on the possessive instead, which is not. A count in a rule document is a count of conformance cases from now on, never one implementation's tests.
No change to what polytypo does to your text — an apostrophe rule that rested a decision on an authority which had not made it.
- Nothing in this version changes your output. Run the same text through 1.6.0 and 1.6.1 and you get the same bytes; every rule, locale, mode and option behaves identically. What changed is normative prose — the documents that say why the engine does what it does — in two of the nine rules.
apostrophe.mdexplains why nothing is inserted between a closing quotation mark and a possessive apostrophe immediately after it, as inthe “Times”’s editor. It rested that on The Chicago Manual of Style, under a heading saying the authority named both the remedy and the character. It had not. The separator and its code points come from a post on the manual's own CMOS Shop Talk blog describing how CMOS Online typesets its own website, and the section that post cross-references is about nested quotation marks and calls a space there not strictly required — that section's text is quoted verbatim inen-US's citations, and has been since the locale landed. Nothing found addresses this ordering at all. The two marks are set side by side, in every locale, exactly as before; the rule now says so as its own choice rather than someone else's.quotes.mdcovers a possessive apostrophe that lands directly after an inline element. Character for character,The <code>xs</code>' printer works.is indistinguishable from a quotation closing in that position, so polytypo reads it as one. On its own the sentence comes back untouched; inside a real quotation the same mark closes it early, andIt is 'the <code>quotes</code>' locale data' here.becomesIt is ‘the <code>quotes</code>’ locale data’ here.— the pair ends two words short and the author's own closing mark is left over. The rule now states what the alternative costs instead of leaving it implied: no test over the neighbouring characters separates the two readings, and the narrowest test that tries makes Turkish worse, where an apostrophe carrying a case suffix is grammar rather than style.
Turkish, cited to the Türk Dil Kurumu — the nineteenth language, and the first whose apostrophe is grammar rather than punctuation.
- Turkish ships as
tr. Until now a caller with Turkish content had two options: skip the language, or run it through a foreign locale. The second is not free, and this is what it cost, measured:Sordu: "Bu, 'köşedeki' dedikleri dükkân mı?"came back fromen-GBasSordu: ‘Bu, “köşedeki” dedikleri dükkân mı?’— the nesting inverted, singles outside and doubles inside.trgivesSordu: “Bu, ‘köşedeki’ dedikleri dükkân mı?”, which is what the TDK rule prescribes. - The quotation marks are U+201C and U+201D at the first level and U+2018 and U+2019 at the second. That sounds unremarkable and was the hardest fact in the file: tdk.gov.tr serves its examples with plain ASCII marks, so the website cannot say which code points it means. They were read out of the text layer of TDK's own journal, in the passage whose footnote is the Yazım Kılavuzu itself, and independently confirmed against the Unicode Consortium's CLDR data for
tr. A production report had described Turkish quotes as the«…»family; the same journal article shows that form as the preference of some writers rather than the institute's norm, so it is re-typeset like any other foreign pair. - An ellipsis after
?or!may be written with two dots, and TDK puts it as sufficient rather than as required.Nasıl da akşam oldu?...becomesNasıl da akşam oldu?.., where every other locale but Russian and Ukrainian completes it to?…. This is the field's other direction, and it matters: had Turkish shipped with the default, TDK's own printed form would have been rewritten into one the guide never shows. - Turkish attaches case suffixes to names, abbreviations and numbers with an apostrophe —
TBMM'nin,1985'te,Ankara'da— so the mark is grammar, not punctuation, and it is everywhere. Inhtmlandmarkdownthat ran into the defect 1.4.0 fixed for English: written against a span,'<b>TBMM</b>'nin kararı doğru.'had the suffix open the quotation and the author's own marks come out inverted. Ten suffix fragments are now cited from TDK's own printed examples, each one evidenced as a whole word fragment rather than inferred from the paradigm, and the sentence comes out as“<b>TBMM</b>’nin kararı doğru.” - The accepted cost of those ten, and the two things they do not reach, are all pinned as conformance cases rather than left to be discovered. A quotation whose entire content is one of the fragments, written against a span, is read as a suffix:
<em>'de'</em>gives<em>’de’</em>, and the risk is higher than English's becausedeis also a separate conjunction. An uppercase suffix does not match, because the fold is ASCII-only and Turkishİis U+0130 — widening it is the one thing the portability rules forbid outright. And a suffix TDK's read pages never print whole, such as thetaofIrak'ta, is simply not in the list. - Three things Turkish deliberately does not get, each for a stated reason. No dash convention: TDK sets a parenthetical with the short hyphen, closed up, which is neither an en nor an em dash, so writing either would produce a transformation the guide contradicts —
tris the third locale, after Greek and Spanish, for which the dash rule is a provable no-op. No range dash, for the same reason. And the percent sign is not bound to its number, because Turkish writes it before the number and closed up:%25stays exactly as typed.
A possessive written straight after a closing bracket or a closing quotation mark is no longer left as a typewriter apostrophe.
- A possessive or a suffix written directly after a closing bracket or a closing quotation mark kept its straight mark.
The pipeline (order 90)'s own output,“Hamlet”'s first line,{user}'s account,the footnote [3]'s author— every one of them came out with the typewriter apostrophe thatdon'tandthe dogs' bowlshave not had since 1.0.0. This repository's own documentation contained four of them. - The cause was an asymmetry inside the rule, not a language nobody had got round to. The apostrophe decision reads exactly one character on each side of the mark. Its right-hand side has accepted a closing bracket or quotation mark since 1.0.0 —
dogs')anddogs'”convert — while its left-hand side accepted a letter, a digit, a space or an opening delimiter, and no closing one. Which side of the word the delimiter happened to sit on decided the answer. - It also depended on the glyph, which is the half worth knowing if you write a language other than English.
»Wort«'sconverted and«Wort»'sdid not, because the guillemet that opens a quotation was in one internal set and the one that closes it was in another. That put every locale whose quotation marks close with»,›,”or’on the losing side of a coin flip — which is most of them, English included. The three forms now give one answer. - The accepted cost, measured rather than assumed: a straight mark after a closing bracket that was meant to open a quotation now closes instead of opening.
(aside)'quoted' heregives(aside)’quoted’ here, where 1.4.0 gave(aside)'quoted’ hereand left the pair mismatched. Nothing local tells a possessive apart from an unmatched opening quotation mark in that position, and it is the same trade the rule already makes before a space and before a quotation glyph. - Two things this release was asked for and deliberately does not do. An apostrophe after a symbol stays straight —
10%'u,24 m²'ye,50°'lik— because nothing attests it: Turkish, the language the request came from, writes the percent sign before the number, so%50'siwas already converting, and its own authority attests the apostrophe after abbreviations and numerals, which were converting too. An apostrophe after a comma or a full stop stays straight as well: inHe said,'yes'the first mark is as likely to be an opening quotation mark, and Chicago spells the abbreviationsUSandPhD, sothe U.S.'s economyis not a shape that orthography produces.
| Locale | Mode | In | Before | After |
|---|---|---|---|---|
| en-US | text | The pipeline (order 90)'s own output is stable. | The pipeline (order 90)'s own output is stable. | The pipeline (order 90)’s own output is stable. |
| en-US | text | “Hamlet”'s first line is the question. | “Hamlet”'s first line is the question. | “Hamlet”’s first line is the question. |
| en-GB | text | (aside)'quoted' here | (aside)'quoted’ here | (aside)’quoted’ here |
An apostrophe written against inline code, emphasis or a link no longer turns the sentence's quotation marks inside out.
- In
htmlandmarkdown, a possessive or an elision written directly against a span —x's,<code>x</code>'s,l'<em>idée</em>— was read as a quotation mark opening a quotation. If the sentence around it was already quoted, the apostrophe took the pair and your own opening mark was demoted to a closing one. The sentence came out with its quotation marks inverted, and running polytypo again did not fix it: the wrong version was a stable result, so a second pass reproduced it. - This is the shape technical writing hits constantly, because possessivising a name set in code is ordinary English:
useEffect's dependency array. French and Italian hit the mirror of it, where an elided article sits against an emphasised word:l'<em>idée</em>produced guillemets around the single letterland abandoned the real closing mark. - The fix reads a new piece of locale data — the short word fragments that attach across an apostrophe in that language, each one cited to the language's own authority. English carries the possessive
s; French carriesl,d,qu,jusquand nine more; Italian, Portuguese and Dutch carry their own. A language with no citable list of them behaves exactly as it did in 1.3.1, so nothing changes where nothing was evidenced. - The accepted cost, measured rather than assumed: inside a span, a quotation whose entire content is one of those fragments is now read as an apostrophe.
He said <em>'s'</em> loudly.gives<em>’s’</em>. Quoting a real word —<em>'no'</em>,<em>'fine'</em>— is unaffected, and both exposures are pinned as conformance cases. - One related shape is still wrong and is tracked separately: a plural possessive after a span,
xs' printer, is written with exactly the same characters as a closing quotation mark after a span, so nothing local can tell them apart. It can still close a quotation opened paragraphs earlier.
| Locale | Mode | In | Before | After |
|---|---|---|---|---|
| en-GB | html | He says 'avoid <code>x</code>'s printer.' Done. | He says ’avoid <code>x</code>‘s printer.’ Done. | He says ‘avoid <code>x</code>’s printer.’ Done. |
| fr | html | Il dit 'l'<em>idée</em> est bonne.' Fin. | Il dit « l »<em>idée</em> est bonne.' Fin. | Il dit «⍽l’<em>idée</em> est bonne.⍽» Fin. |
No change to what polytypo does to your text — two conformance cases that pin behaviour 1.3.0 already specified but never proved.
- Nothing in this version changes your output. Run the same text through 1.3.0 and 1.3.1 and you get the same bytes; every rule, locale, mode and option behaves identically. What changed is the conformance suite, which is the executable half of the spec: it gained two
yamlcases, so a runtime claiming 1.3.1 has proved something a runtime claiming 1.3.0 only promised. - In
yamlmode, a value written without quotes or a|cannot contain a colon followed by a space —description: She asked: one...twocomes back untouched even thoughdescriptionis named inkeys. That is not a limitation polytypo invented: YAML reads the second colon as a nested mapping, and a real parser rejects such a document outright. The engine declines the line rather than rewrite text it could not read back. The prose said so in 1.3.0; now a fixture proves it, next to its opposite — the same sentence in a block scalar, which is processed.
A fourth mode for YAML, eight more languages, and a dash fix that had been quietly putting spaces inside your markup.
yamlis a fourth mode. It takes a requiredkeyslist naming which mapping keys hold prose — there is no default and no guess, because nothing in YAML's syntax separates adescription:from arun:. Quoting, indentation, anchors and a block scalar's chomping indicator are never decoded and rewritten, so everything you did not name comes back byte for byte. The accepted cost is that a document quoting its keys gets nothing, and a scalar spread over several lines is left alone.dashes— Inhtmlandmarkdown, three dashes at the edge of an element no longer put spaces inside it.a<em>---</em>bin a locale that spaces its dashes used to come back asa<em> – </em>b— an element beginning and ending with a space it never contained, invisible until it carried a border or a background. Two dashes were already left alone there; three now are too. The accepted cost is that such a dash is not converted at all in those positions, which is the same trade the two-dash case already made.nbsp— A character reference survivestextmode. In French, and«used to come back with a narrow no-break space wedged before their semicolon, which destroyed the reference. They are now left alone. Separately,Nr.in German andn°in French bind to the number that follows them.narrowNbsp: "nbsp"makes the engine emit U+00A0 everywhere it would emit U+202F, for faces that do not carry the narrow no-break space. It moves the rule's target rather than cleaning up afterwards, so the result is still a fixed point.ranges— A range may repeat a symbol closed up on both members:$15-$20is a range,$15 - $20is not.rangesstill defaults to off.analyze()reports whattransform()would do without doing it — one record per edit, with the rule that made it and code-point offsets into the input you passed. It is a report, not a patch.- Eight more languages: Spanish, Italian, European and Brazilian Portuguese, Dutch, Polish, Ukrainian and Czech. Portuguese ships as
pt-PTandpt-BRrather than one neutralpt, because they do not agree;ptresolves topt-PT.
| Locale | Mode | In | Before | After |
|---|---|---|---|---|
| de-DE | html | a<em>---</em>b | a<em> – </em>b | a<em>---</em>b |
| fr | text | oui ! | oui ·;! | oui ! |
| de-DE | text | Nr. 5 | Nr. 5 | Nr.⍽5 |
| de-DE | markdown | x *---* y | x * – * y | x *---* y |
Three fixes from production reports: file extensions keep their space, French spacing survives an inline element, and an elision before a quotation mark is set.
spaces— A space before a token that begins with a dot is kept. Before 1.2.0 it was deleted, soRequires .NET 8came back asRequires.NET 8anda .gitignore filelost its space too. The rule now looks at the code point after the dot: a letter or a digit means the dot starts a token, not ends a sentence. The accepted cost is that a stray space before a real sentence stop followed by a capitalised word,end .Next, is now kept rather than removed.nbsp— Inhtmlandmarkdownmodes, French spacing survives an inline element boundary.<strong>Label :</strong>kept its space before 1.2.0 only if the colon sat in the middle of a text run; at the end of a span the space was dropped entirely. The accepted cost is that a colon immediately followed by an opening tag,12:<b>30</b>, now gains a no-break space before it.apostrophe— An elision apostrophe directly before an opening quotation mark becomes U+2019.d'«idée»andl'“idée”kept a straight mark before 1.2.0, in French and Italian the commonest shape there is. Brackets stay excluded, so a prime inf'(x)is still left alone.
| Locale | Mode | In | Before | After |
|---|---|---|---|---|
| en-US | text | Requires .NET 8 and a .gitignore file. | Requires.NET 8 and a.gitignore file. | Requires .NET 8 and a .gitignore file. |
| fr | html | <strong>Label :</strong> oui | <strong>Label:</strong> oui | <strong>Label⍽:</strong> oui |
| fr | text | d'«idée» | d'«⍽idée⍽» | d’«⍽idée⍽» |
Short quotations convert again, and rock 'n' roll is elided in every locale.
apostrophe— Up to 1.0.0 the rule refused to convert any one-to-three-letter run between straight single quotes, because it could not tell the'n'idiom from an ordinary short quotation. That declined both:He said 'no' to me.kept straight marks, and so did«это 'моё' дело». The test is now exactly one code point,norN, and the outcome inverts: that one shape becomes U+2019 on both sides in every locale, and every other short quotation converts like any other quotation.apostrophe— The accepted cost, pinned by fixtures: a genuine quotation of the letter n,The letter 'n' is common., is elided. The class it replaces — every short quotation in every locale — was larger.
| Locale | Mode | In | Before | After |
|---|---|---|---|---|
| en-US | text | He said 'no' to me. | He said 'no' to me. | He said “no” to me. |
| fi | text | rock 'n' roll | rock 'n' roll | rock ’n’ roll |
The first frozen spec, and the first published runtime.
- Nine rules in a fixed order, ten locales, three modes, seven error codes, and idempotency as a contract rather than a hope: running
transformover its own output returns the same bytes. Everything after this entry is a change against that baseline.
Patch releases
Patch releases carry no behaviour change and no spec change. polytypo-js 1.0.1 moved npm publishing to trusted publishing (OIDC) and rewrote its README; 1.0.2, along with polytypo-python 1.0.1, polytypo-go 1.0.1 and polytypo-ruby 1.0.1, dropped the word “microtypography” from the published package description. The spec stayed at 1.0.0 throughout, and output was byte-identical. All five runtimes' 1.4.1 stopped shipping the locale citations: they are mandatory in the spec and unchanged there, but no rule reads them and they were 94% of the embedded locale data by size, so every install carried a quarter-megabyte of bibliography. The browser bundle's smallest entry dropped from 88.7 KB to 19.7 KB gzipped. Read the citations where they belong — on the Locales page, generated from the spec files that hold them. polytypo-python 1.6.4 is spec 1.6.3 with one metadata fix: polytypo.__version__ reported 0.0.0 in every release up to and including 1.6.3, while pip show polytypo reported the real number. It now comes from the installed distribution, so the two agree.
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>