polytypo 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/polytypo/data/VERSION +1 -1
  3. data/lib/polytypo/data/fixtures/cs.json +1 -1
  4. data/lib/polytypo/data/fixtures/de-CH.json +1 -1
  5. data/lib/polytypo/data/fixtures/de-DE.json +1 -1
  6. data/lib/polytypo/data/fixtures/el.json +1 -1
  7. data/lib/polytypo/data/fixtures/en-GB.json +50 -1
  8. data/lib/polytypo/data/fixtures/en-US.json +41 -1
  9. data/lib/polytypo/data/fixtures/es.json +1 -1
  10. data/lib/polytypo/data/fixtures/fi.json +1 -1
  11. data/lib/polytypo/data/fixtures/fr-CA.json +1 -1
  12. data/lib/polytypo/data/fixtures/fr.json +9 -1
  13. data/lib/polytypo/data/fixtures/it.json +1 -1
  14. data/lib/polytypo/data/fixtures/locale-resolution.json +13 -1
  15. data/lib/polytypo/data/fixtures/nl.json +1 -1
  16. data/lib/polytypo/data/fixtures/pl.json +1 -1
  17. data/lib/polytypo/data/fixtures/pt-BR.json +1 -1
  18. data/lib/polytypo/data/fixtures/pt-PT.json +1 -1
  19. data/lib/polytypo/data/fixtures/ru.json +1 -1
  20. data/lib/polytypo/data/fixtures/sv.json +1 -1
  21. data/lib/polytypo/data/fixtures/tr.json +250 -0
  22. data/lib/polytypo/data/fixtures/uk.json +1 -1
  23. data/lib/polytypo/data/locales/registry.json +3 -2
  24. data/lib/polytypo/data/locales/tr.json +72 -0
  25. data/lib/polytypo/data/rules/apostrophe.md +183 -20
  26. data/lib/polytypo/data/rules/modes.md +29 -4
  27. data/lib/polytypo/data/rules/nbsp.md +20 -6
  28. data/lib/polytypo/data/rules/order.json +2 -2
  29. data/lib/polytypo/data/rules/pipeline-idempotency.md +3 -2
  30. data/lib/polytypo/data/rules/quotes.md +234 -9
  31. data/lib/polytypo/data/rules/symbols.md +25 -5
  32. data/lib/polytypo/engine/rules/apostrophe.rb +14 -1
  33. data/lib/polytypo/version.rb +1 -1
  34. metadata +3 -1
@@ -0,0 +1,72 @@
1
+ {
2
+ "locale": "tr",
3
+ "name": "Turkish",
4
+ "quotes": {
5
+ "primary": {
6
+ "open": "“",
7
+ "close": "”",
8
+ "innerSpace": "none"
9
+ },
10
+ "secondary": {
11
+ "open": "‘",
12
+ "close": "’",
13
+ "innerSpace": "none"
14
+ },
15
+ "elisionIdioms": [],
16
+ "elisionClitics": {
17
+ "before": [],
18
+ "after": [
19
+ "nin",
20
+ "nın",
21
+ "de",
22
+ "da",
23
+ "te",
24
+ "ye",
25
+ "yle",
26
+ "nı",
27
+ "dan",
28
+ "den",
29
+ "lik",
30
+ "nci",
31
+ "üm"
32
+ ]
33
+ }
34
+ },
35
+ "dash": {
36
+ "parenthetical": "none",
37
+ "range": "none"
38
+ },
39
+ "ellipsis": {
40
+ "abbreviatedAfterTerminal": true
41
+ },
42
+ "hyphen": {
43
+ "prefixes": [],
44
+ "suffixes": [],
45
+ "compounds": []
46
+ },
47
+ "nbsp": {
48
+ "beforePunctuation": [],
49
+ "narrowBeforePunctuation": [],
50
+ "afterShortWords": [],
51
+ "abbreviations": [
52
+ "Kur. Bşk.",
53
+ "Nö. Sb."
54
+ ],
55
+ "beforeUnits": [
56
+ "mm",
57
+ "cm",
58
+ "km",
59
+ "kg",
60
+ "mg",
61
+ "hl",
62
+ "m²",
63
+ "cm²",
64
+ "°C",
65
+ "ton"
66
+ ],
67
+ "beforeNumber": [],
68
+ "beforeWord": [],
69
+ "afterSymbols": [],
70
+ "initialBinding": "none"
71
+ }
72
+ }
@@ -1,9 +1,11 @@
1
1
  # Rule: `apostrophe`
2
2
 
3
3
  **Order:** 50. **Default:** on. **Modes:** text, html, markdown, yaml.
4
- **Spec version:** 1.2.0 (0.4.1 for everything except §2, §3.4 and the §6/§7 updates for the
5
- withdrawal of the shared ambiguity preserve set (1.1.0), and §3.1's `OPENQUOTE` with §3.3's case 3a
6
- (1.2.0)).
4
+ **Spec version:** 1.5.0 (0.4.1 for everything except §2, §3.4 and the §6/§7 updates for the
5
+ withdrawal of the shared ambiguity preserve set (1.1.0), §3.1's `OPENQUOTE` with §3.3's case 3a
6
+ (1.2.0), §3.3 case 4's note on the traffic `quotes`' span-boundary elision veto sends it
7
+ (1.4.0) — which adds no case and changes nothing this rule computes — and §3.1's `CLOSEDELIM`
8
+ with §3.3's case 2a (1.5.0)).
7
9
 
8
10
  ---
9
11
 
@@ -59,12 +61,13 @@ Input is a code-point array `cp[0 … n-1]`.
59
61
  | `OPENISH` | U+0028 `(` U+005B `[` U+007B `{` U+00AB `«` U+2018 U+201A U+201B U+201C U+201E U+201F U+2039 `‹`, U+002D, U+2011, U+2013, U+2014 |
60
62
  | `CLOSEISH` | U+0029 `)` U+005D `]` U+007D `}` U+00BB `»` U+2019 U+201D U+203A `›` U+002C U+002E U+003B U+003A U+0021 U+003F U+2026, U+002D, U+2011, U+2013, U+2014 |
61
63
  | `OPENQUOTE` | U+00AB `«` U+2018 U+201A U+201B U+201C U+201E U+201F U+2039 `‹` — the quotation glyphs of `OPENISH`, without its brackets and dashes (spec 1.2.0, case 3a) |
64
+ | `CLOSEDELIM` | U+0029 `)` U+005D `]` U+007D `}` U+00BB `»` U+2019 U+201D U+203A `›` — the bracket and quotation members of `CLOSEISH`, without its sentence punctuation and without the dashes `OPENISH` already carries (spec 1.5.0, case 2a) |
62
65
  | `NONE` | index out of range |
63
66
 
64
67
  **Unicode version.** The general categories this rule reads are those of the UCD version pinned in `spec/UNICODE` (`17.0`). The pin is normative for the **derived tables**, not for the host runtime — see [pipeline-idempotency.md](pipeline-idempotency.md) §6a, which also specifies the canary fixtures that make it detectable.
65
68
 
66
- Note that U+2019 is a member of `CLOSEISH` and U+0027 is a member of neither. This matters for
67
- idempotency and is argued in §5. U+2011 is listed alongside U+002D because `hyphen` (order 35)
69
+ Note that U+2019 is a member of `CLOSEISH` and of `CLOSEDELIM`, and that U+0027 is a member of
70
+ no class in this table at all. This matters for idempotency and is argued in §5. U+2011 is listed alongside U+002D because `hyphen` (order 35)
68
71
  converts one to the other and a class that held only U+002D would make a neighbouring
69
72
  apostrophe's verdict depend on whether `hyphen` had already run (`hyphen.md` §3.2).
70
73
 
@@ -99,6 +102,55 @@ and take the **first** matching case:
99
102
  `1990's`, `d'accord`, `Hawai'i`, `can't`.
100
103
  (Note that case 1 has already removed the `digit` + `non-letter` combination, so the
101
104
  `DIGIT`-left half of this case only fires for `1990's`-style forms where a letter follows.)
105
+ 2a. **Suffix or possessive after a closing delimiter (spec 1.5.0).** If `left` is in
106
+ `CLOSEDELIM` **and** `right` is in `ALNUM` → emit an edit replacing `cp[i]` with U+2019.
107
+ Covers `(order 90)'s own output`, `“Hamlet”'s first line`, `the footnote [3]'s author`,
108
+ `{user}'s account`.
109
+
110
+ **This closes an asymmetry between the two sides of the ladder, not a gap in a language.**
111
+ The **right** side has accepted a closing delimiter since 0.4.1 — case 3 takes every member of
112
+ `CLOSEISH`, so `dogs')` and `dogs'”` convert — while the **left** side accepted only `ALNUM`
113
+ (cases 1, 2, 3, 3a) and `NONE`/`SPACELIKE`/`OPENISH` (case 4). Nothing chose that: it made a
114
+ possessive's verdict depend on which of two tables a closing glyph happens to sit in. U+00AB
115
+ and U+2039 are `OPENISH` members, so `»Wort«'s` already converted through case 4, while
116
+ `‹Wort›'s` and `«Wort»'s` — the same shape with the other guillemet — did not. Case 2a accepts
117
+ on the left exactly the closing counterparts the right side already accepts, and the three
118
+ forms now agree.
119
+
120
+ **`CLOSEISH`'s sentence punctuation is excluded.** U+002C U+002E U+003B U+003A U+0021 U+003F
121
+ U+2026 are **not** in `CLOSEDELIM`. A U+0027 after one of them is at least as likely to be an
122
+ opening quotation mark `quotes` could not pair — `He said,'yes'`, where `canOpen`'s right-test
123
+ rejects the `CLOSEISH` left neighbour — as a possessive, and this rule cannot tell which. The
124
+ English shapes that would have argued for U+002E are the ones Chicago's own orthography
125
+ removes: `CMOS` 10.4(c) writes `US` and `PhD`, not `U.S.'s` and `Ph.D.'s`, so the sequence
126
+ U+002E U+0027 U+0073 does not arise in that orthography. Excluded on that evidence, with the
127
+ shape recorded at §6 row 23 so it stays visible.
128
+
129
+ **Every symbol is excluded too, for a stronger reason: nothing attests one.** `%`, `°`,
130
+ U+2030 and the superscript digits are not in `CLOSEDELIM`. §7 item 8 records the measurement.
131
+
132
+ **The accepted cost is §7 item 9**, and it is the mirror of the trade case 3a accepts: a mark
133
+ in this position is either a possessive or an *opening* quotation mark `quotes` could not pair,
134
+ and two neighbours cannot tell them apart.
135
+
136
+ **What this case does not claim.** It decides the identity of a mark the author has already
137
+ typed; it does not endorse the construction. `CMOS`'s reachable statement on the possessive of
138
+ a quoted title answers this exact shape by steering to an attributive rephrasing — *the "Wild
139
+ Horses" bass line* — and endorses none of the possessive forms it was offered (Q&A,
140
+ "Quotations and Dialogue" #12). That is advice about what to write, not a claim about what the
141
+ mark is: nothing consulted reads a U+0027 in this position as anything other than an
142
+ apostrophe, and this rule has no licence to rephrase anyone's sentence. §7 item 10 records the
143
+ one place where following the same authority further would mean *inserting*, which this rule
144
+ cannot do.
145
+
146
+ **`MARKER` is not in `CLOSEDELIM`** (`modes.md` §3.3) and does not need to be: the marker is
147
+ in `OPENISH`, so a possessive written flush against an inline span already reaches case 4 and
148
+ emits the same U+2019 (case 4's note). `modes.md` needs no new row for this case — only the
149
+ `no` entry recording that the marker stays out of it.
150
+
151
+ **`(`, `[` and `{` stay out of every right-hand test**, so `f'(x)` is still a prime — case 5,
152
+ for the reason case 3a's bracket exclusion gives. Case 2a reads the *closing* brackets on the
153
+ **left**, where a bracket closes a group rather than opening one.
102
154
  3. **Trailing elision or possessive.** If `left` is in `LETTER` **and**
103
155
  (`right` is `NONE`, or `right` is in `SPACELIKE`, or `right` is in `CLOSEISH`) → emit an
104
156
  edit replacing `cp[i]` with U+2019. Covers `the dogs' bowls`, `les élèves' cahiers`,
@@ -124,6 +176,16 @@ and take the **first** matching case:
124
176
  4. **Leading elision.** If (`left` is `NONE`, or `left` is in `SPACELIKE`, or `left` is in
125
177
  `OPENISH`) **and** (`right` is in `ALNUM`) → emit an edit replacing `cp[i]` with U+2019.
126
178
  Covers `’90s`, `’tis`, `’em`, `’cause`, `’n'` (the leading mark), `(’tis)`.
179
+
180
+ **It also carries a traffic its name does not describe (spec 1.4.0): a possessive or elision
181
+ written flush against an inline span boundary.** `modes.md` §3.2's marker is in this rule's
182
+ `OPENISH` (`modes.md` §3.3), so `` `x`'s `` and `<code>x</code>'s` match here rather than at
183
+ case 2, whose `ALNUM` left-read the marker fails. The glyph is the same U+2019 either way, so
184
+ the outcome is correct and no case is added — but the reason those marks now *reach* this
185
+ ladder is `quotes`' span-boundary elision veto (`quotes.md` §3.2, spec 1.4.0), which declines
186
+ to pair them. Before 1.4.0 `quotes` claimed them and inverted the enclosing pair. The mirror
187
+ shape, `l'<em>idée</em>`, reaches **case 3** by the same route: the marker is in `CLOSEISH`
188
+ too. Neither case may be narrowed to exclude the marker without reopening that defect.
127
189
  **The replacement is U+2019 — never U+2018.** A leading elision is a raised comma marking
128
190
  removed characters, not an opening quotation mark. Getting this backwards is the single
129
191
  most common apostrophe bug in existing tools, and it is visually obvious in a serif face.
@@ -191,7 +253,8 @@ of being falsified by another rule, which is then named.
191
253
  - **[P] An isolated U+0027** with `SPACELIKE` on both sides, or at the very start or end of the
192
254
  text unit with `SPACELIKE` on the inner side. Case 5.
193
255
  - **[P] `''`** — two adjacent U+0027 (a typewriter double quote, or a LaTeX close-quote idiom).
194
- Neither has `ALNUM` on the relevant side, so case 5 applies to both.
256
+ U+0027 is in no class this rule tests (§3.1), so neither mark has, on the side facing the
257
+ other, anything a converting case accepts; case 5 applies to both.
195
258
  - **[P] U+0060 (`), U+00B4 (´), U+02BC (ʼ), U+02B9, U+2032 (′).** None is in `SQ`. In particular
196
259
  U+02BC is a *letter* in several orthographies and converting it would be a data mutation.
197
260
  Converting U+0060 or U+00B4 is a separate normalisation concern that `order.json` does not
@@ -225,17 +288,21 @@ points this rule changed are former U+0027 marks that became U+2019. So the ques
225
288
  a surviving candidate's `left` or `right` have changed class in a way that flips its
226
289
  outcome?
227
290
 
228
- - U+0027 is in **none** of `ALNUM`, `SPACELIKE`, `OPENISH`, `CLOSEISH`, `OPENQUOTE`, `DIGIT`,
229
- `LETTER`.
230
- - U+2019 is in `CLOSEISH` and in none of the others.
291
+ - U+0027 is in **none** of `ALNUM`, `SPACELIKE`, `OPENISH`, `CLOSEISH`, `OPENQUOTE`,
292
+ `CLOSEDELIM`, `DIGIT`, `LETTER`.
293
+ - U+2019 is in `CLOSEISH` and `CLOSEDELIM`, and in none of the others.
231
294
 
232
295
  Neither code point is in `OPENQUOTE`, so a neighbour's edit cannot change case 3a's right-test.
233
296
  The argument below needs no new branch for it.
234
297
 
235
- So a neighbour changing from U+0027 to U+2019 can only _add_ `CLOSEISH` membership. Where
236
- does `CLOSEISH` appear in the decision? Only in case 3's right-test. So the only possible
237
- flip is: a candidate `u` whose right neighbour was U+0027 (giving no case-3 match) and is
238
- now U+2019 (giving a case-3 match), where additionally `u`'s left neighbour is a `LETTER`.
298
+ So a neighbour changing from U+0027 to U+2019 can only _add_ `CLOSEISH` and `CLOSEDELIM`
299
+ membership. Those two classes appear in exactly two places in the decision: `CLOSEISH` in case
300
+ 3's right-test, and `CLOSEDELIM` in case 2a's left-test (spec 1.5.0). So there are two possible
301
+ flips, one on each side, and **both are vacuous, by the same argument mirrored**.
302
+
303
+ The right-hand flip would be: a candidate `u` whose right neighbour was U+0027 (giving no case-3
304
+ match) and is now U+2019 (giving a case-3 match), where additionally `u`'s left neighbour is a
305
+ `LETTER`.
239
306
 
240
307
  Concretely that shape is `LETTER` U+0027 U+0027 — for example `dogs''`. On run 1: the first
241
308
  mark has `left` = `s` (letter), `right` = U+0027, which is in none of `NONE`/`SPACELIKE`/
@@ -245,10 +312,21 @@ The second mark has `left` = U+0027 (not `ALNUM`, not `LETTER`, not `SPACELIKE`,
245
312
  the flip cannot occur. The premise is vacuous.
246
313
 
247
314
  More generally: the flip requires the _right_ neighbour to have been edited, i.e. the right
248
- neighbour was a U+0027 that matched one of cases 2, 3, 3a, 4. Cases 2 and 4 require `ALNUM` on
249
- that mark's **left** — but its left is `u`, which is U+0027, not `ALNUM`. Cases 3 and 3a require
250
- `LETTER` on its left — same contradiction. So the right neighbour of a surviving U+0027 is
251
- never edited, and no surviving candidate's classification changes.
315
+ neighbour was a U+0027 that matched one of cases 2, 2a, 3, 3a, 4. Cases 2 and 4 require `ALNUM`
316
+ on that mark's **left** — but its left is `u`, which is U+0027, not `ALNUM`. Cases 3 and 3a
317
+ require `LETTER` on its left, and case 2a requires `CLOSEDELIM` — the same contradiction three
318
+ more times. So the right neighbour of a surviving U+0027 is never edited, and no surviving
319
+ candidate's classification changes.
320
+
321
+ **The left-hand flip (case 2a) is vacuous for the mirrored reason.** It would require a surviving
322
+ candidate `u` whose _left_ neighbour was U+0027 and is now U+2019, with `ALNUM` on `u`'s right.
323
+ So the left neighbour must have been edited — and its own right neighbour is `u`, a U+0027.
324
+ Cases 2, 2a and 4 require `ALNUM` on the right (U+0027 is not), case 3 requires `NONE`,
325
+ `SPACELIKE` or `CLOSEISH` (U+0027 is in none of them), and case 3a requires `OPENQUOTE` (U+0027
326
+ is not). So the left neighbour of a surviving U+0027 is never edited either. Concretely `x''s`,
327
+ the shape the flip would need: on run 1 the first mark's `right` is U+0027 and the second mark's
328
+ `left` is U+0027, so neither converts, no U+2019 appears between them, and the premise is again
329
+ vacuous.
252
330
 
253
331
  Hence `T(T(x)) = T(x)`.
254
332
 
@@ -273,8 +351,8 @@ Per [pipeline-idempotency.md](pipeline-idempotency.md) §5. This rule is **R₆*
273
351
  runs against `spaces`, `ellipsis`, `dashes`, `hyphen` and `quotes`.
274
352
 
275
353
  **What this rule emits.** One U+2019 replacing one U+0027 at the same index. Nothing else, ever
276
- — no insertion, no deletion, no length change. Case 3a (spec 1.2.0) changes *which* U+0027 marks
277
- are replaced, and does not change what is emitted. Every discharge below is written against the
354
+ — no insertion, no deletion, no length change. Case 3a (spec 1.2.0) and case 2a (spec 1.5.0)
355
+ change *which* U+0027 marks are replaced, and do not change what is emitted. Every discharge below is written against the
278
356
  emission, and `quotes`' V1 already treats every U+0027 as a possible U+2019 (`V1ID`, below), so
279
357
  none of them needs re-deriving.
280
358
 
@@ -359,8 +437,16 @@ are shown as they arrive at this rule, i.e. after `quotes` has run.
359
437
  | 16 | `l'“idea”` | `l’“idea”` | 3a | same, with U+201C |
360
438
  | 17 | `„Hans'“` | `„Hans’“` | 3a | U+201C closes in `de-DE`, and 3a does not need to know that: a letter followed by U+0027 and a quotation glyph is an elision or a possessive either way |
361
439
  | 18 | `f'(x) = 2` | ⟶ | 5 | `(` is not in `OPENQUOTE`. A prime on a function name is not an apostrophe |
440
+ | 19 | `The pipeline (order 90)'s own output` | `The pipeline (order 90)’s own output` | 2a | closing bracket left, letter right. **Spec 1.5.0**; previously case 5 left it straight |
441
+ | 20 | `“Hamlet”'s first line` | `“Hamlet”’s first line` | 2a | U+201D left — `en-US`'s own closing glyph. The rule decides what the author typed, not whether they should have: a U+0027 here is a possessive or an unpaired opening mark (§7 item 9), and never a prime. `CMOS` would rephrase the sentence instead — see case 2a and §7 item 10 |
442
+ | 21 | `»Wort«'s, ‹Wort›'s and «Wort»'s` | `»Wort«’s, ‹Wort›’s and «Wort»’s` | 4, 2a, 2a | **the asymmetry case 2a removes.** The first form already converted before 1.5.0, because U+00AB is an `OPENISH` member and case 4 reads `OPENISH` on the left; the other two stayed straight because U+203A and U+00BB are in `CLOSEISH`, which no left-hand test read. Same shape, same reading, three glyphs — now one verdict |
443
+ | 22 | `{user}'s account` | `{user}’s account` | 2a | a template placeholder closes a group the way any bracket does. U+007D is in `CLOSEDELIM` |
444
+ | 23 | `He said,'yes' and left.` | `He said,'yes’ and left.` | 5, 3 | **U+002C is deliberately not in `CLOSEDELIM`.** `quotes` declined the pairing (`canOpen`'s right-test rejects a `CLOSEISH` left neighbour), and this rule cannot tell an opening quotation mark after a comma from a possessive, so the leading mark is left recoverable as U+0027 (§7 item 4). The trailing mark is case 3, exactly as before 1.5.0 — the row is unchanged by 1.5.0 and is here to pin that |
445
+ | 24 | `10%'u 24m²'ye 50°'lik` | ⟶ | 5 | no symbol is in `CLOSEDELIM` — not `%`, not U+00B0, not a superscript digit. §7 item 8 |
446
+
447
+ | 25 | `(aside)'quoted' here` | `(aside)’quoted’ here` | 2a, 3 | **the accepted cost, §7 item 9.** The author meant a quotation, and both marks now read as closing glyphs. It was already mismatched before 1.5.0 in the other direction — 1.4.0 gave `(aside)'quoted’ here`, case 3 having curled the trailing mark while the leading one had no case at all |
362
448
 
363
- Cases 6, 7, 9, 10, 12 and 18 are "no change" cases.
449
+ Cases 6, 7, 9, 10, 12, 18 and 24 are "no change" cases.
364
450
 
365
451
  ---
366
452
 
@@ -430,3 +516,80 @@ layouts and in text pasted from older systems. Converting them is not authorised
430
516
  accepted the same trade: it curls an unmatched closing quote before a space. Case 3a extends it
431
517
  to a quotation glyph. The issue that motivated it (French and Italian elision before `«` and
432
518
  `“`) is common, and the counter-case is a malformed quotation.
519
+ 8. **(Spec 1.5.0.) A U+0027 after a symbol — `%`, U+00B0, U+2030, a superscript digit — is left
520
+ straight, and that is a decision rather than an omission.** Issue #28 asked for the left-hand
521
+ class to be widened to these on the strength of a production report of Turkish text
522
+ (`24 m²'ye`, `10%'u`, `{price}'den`) run through `en-GB`. The `{price}'den` half is now case 2a;
523
+ the symbol half is declined, on three findings:
524
+
525
+ - **The Turkish premise does not hold.** TDK, *Yazım Kuralları*, "Kesme İşareti", attests the
526
+ apostrophe for suffixes after abbreviations (rule 3: `TBMM'nin`, `TDK'nin`, `BM'de`, `ABD'de`,
527
+ `TV'ye`) and after numerals (rule 4: `1985'te`, `8'inci madde`, `7,65'lik`, `657'yle`). None
528
+ of its seven rules mentions the percent sign or a unit symbol, and every attested example has
529
+ `ALNUM` on the left of the mark, so **case 2 already converts all of them**. Turkish also
530
+ writes the percent sign *before* the number — `%50'si` — which puts a digit, not `%`, on the
531
+ mark's left. `10%'u` is not Turkish notation.
532
+ - **No English authority attests the shape either.** `CMOS` Q&A "Possessives and Attributives"
533
+ #41 (citing `CMOS` 7.17) and "Plurals" #8 (citing 7.15) put the apostrophe after a letter in
534
+ every example — `CBS's`, `HHS's`, `PDFs'` — and `CMOS` Q&A "Abbreviations" #112 advises
535
+ spelling units of measurement out in non-technical prose rather than possessivising the
536
+ symbol. `New Hart's Rules`, the AP Stylebook and Merriam-Webster were not reachable and are
537
+ recorded as unknown rather than as silence.
538
+ - **Measured: the shape has no witness.** Adding `%`, U+00B0, U+2030 and U+2070–U+2079 to
539
+ `CLOSEDELIM` changes nothing over this repository's own English prose — every `.md` file, in
540
+ `markdown` mode, `en-GB` — while case 2a as specified changes exactly four spans there, all
541
+ of them genuine possessives after a closing parenthesis.
542
+
543
+ Recorded so that a later report of the same shape is met with this evidence rather than with a
544
+ second design pass. What would reopen it: a normative citation that the mark is an apostrophe
545
+ after a symbol in some language, which is then a locale question in the sense of PLAN §6.2,
546
+ not a class-table preference.
547
+ 9. **(Spec 1.5.0.) Case 2a can curl an unpaired *opening* single quote — the mirror of item 7, and
548
+ the more visible of the two.** `(aside)'quoted' here` becomes `(aside)’quoted’ here`: the
549
+ author meant a quotation, and both marks now read as closing glyphs. The reasoning is item 7's,
550
+ one class along. `quotes` has already declined the mark — `canOpen`'s right-test rejects a
551
+ `CLOSEISH` left neighbour, so a quotation opening flush after `)` or `”` is exactly the shape
552
+ `quotes` cannot pair — and two neighbours cannot separate that from a possessive.
553
+
554
+ **It is a change in which half is wrong, not a new wrong.** Measured against the published
555
+ 1.4.0 package: that input already came out mismatched, as `(aside)'quoted’ here`. Case 3 curled
556
+ the trailing mark, because a letter on the left and a space on the right is a trailing
557
+ possessive by every test this rule has; the leading mark matched no case and stayed straight.
558
+ So 1.4.0 produced one curled and one straight mark, and 1.5.0 produces two curled ones. Neither
559
+ is the quotation the author wrote.
560
+
561
+ **This is not the U+2018 bug case 4 warns about.** The rule still never emits U+2018 anywhere
562
+ (§5). What is lost here is a genuine *opening* quotation mark, in the one position `quotes`
563
+ declines to claim it — not a leading elision rendered backwards.
564
+
565
+ The trade is accepted for the reason cases 3, 3a and 2a all accept theirs: the possessive is
566
+ ordinary and frequent, the counter-case is a quotation that `quotes` could not resolve, and
567
+ recoverability is one character. Pinned at §6 row 25 and as a conformance fixture, so no port
568
+ can narrow or widen it without the suite noticing.
569
+ 10. **(Spec 1.5.0.) Case 2a can produce two contiguous U+2019, and nothing in the pipeline
570
+ separates them.** In `en-GB`, whose primary pair closes with U+2019, `A ‘quoted’'s meaning`
571
+ gives `A ‘quoted’’s meaning`: the closing quotation mark and the possessive end up flush, and
572
+ at text size the pair reads as one double quote.
573
+
574
+ **The authority names both the remedy and the character.** `CMOS`'s own editors describe the
575
+ fix as adding a space between the contiguous marks, and enumerate it by code point — U+00A0,
576
+ or a thin space U+2009 or hair space U+200A in print, or U+202F, which *CMOS* Online itself
577
+ now sets between a quotation mark and an apostrophe (18th ed. §6.11, as described in *CMOS
578
+ Shop Talk*, "When Quotation Marks and Apostrophes Collide", updated
579
+ 2025-12-16).
580
+
581
+ **polytypo emits the right character and inserts nothing.** This rule *cannot* insert: §1 and
582
+ §4 make every edit one code point replacing one code point at the same index, and that is
583
+ load-bearing for §5's idempotency argument, not an accident. Insertion is `nbsp`'s work
584
+ (order 70), and `nbsp` has no sub-rule for two adjacent quotation marks. So the spacing
585
+ between them is unaddressed by every rule in `order.json` — recorded here as a decision
586
+ rather than left as an omission, in the standing of items 2 and 3.
587
+
588
+ **A future `nbsp` sub-rule would need its own citation, not this one.** The attested passage
589
+ is one mark-order away from the shape case 2a produces: it separates a title's *own* trailing
590
+ apostrophe from a following closing quotation mark — its example is the song title *Ain't
591
+ Misbehavin'* set in single quotation marks, so the two marks there are the title's own
592
+ apostrophe and then the closing quote, apostrophe first. The possessive ordering — closing mark, then apostrophe, then `s` —
593
+ is addressed by nothing retrieved, and `CMOS` §7.29 ("Possessive with italicized or quoted
594
+ terms"), the paragraph that governs it, is behind a subscription and unread. Borrowing the
595
+ citation across that difference is exactly the move this project settles by evidence instead.
@@ -7,8 +7,10 @@ for all five runtimes and is parser-agnostic by construction: `parse5`, `nokogir
7
7
  `golang.org/x/net/html` and PHP's DOM disagree about almost everything this document does not
8
8
  forbid them from doing. `yaml` mode is parser-**free** rather than parser-agnostic, for the
9
9
  reason §3.8.1 measures.
10
- **Spec version:** 1.3.0 (0.1.0 for everything except §3.3's class-membership table rows for
11
- `nbsp` and `apostrophe`, split in 1.2.0, and §3.8, added in 1.3.0).
10
+ **Spec version:** 1.5.0 (0.1.0 for everything except §3.3's class-membership table rows for
11
+ `nbsp` and `apostrophe`, split in 1.2.0, §3.8, added in 1.3.0, §3.3's note on `quotes`'
12
+ span-boundary elision veto reading the marker as a trigger, added in 1.4.0 — which changes no
13
+ row of the table it follows — and §3.3's `CLOSEDELIM` entry, added in 1.5.0).
12
14
 
13
15
  ---
14
16
 
@@ -150,13 +152,32 @@ simply a member of `BREAK` and of nothing else, so it needs no table: every rule
150
152
  | any literal matching list (`abbreviations`, `beforeUnits`, `hyphen.*`, the trademark table) | **no** — the marker never matches a literal |
151
153
  | `OPENISH` **and** `CLOSEISH` (`quotes`, `apostrophe`) | **yes, both** |
152
154
  | `CLOSEISH` (`nbsp`) | **yes** |
153
- | `OPENISH` (`nbsp`), `OPENQUOTE` (`apostrophe`) | **no** |
155
+ | `OPENISH` (`nbsp`), `OPENQUOTE` and `CLOSEDELIM` (`apostrophe`) | **no** |
154
156
 
155
157
  and one exemption:
156
158
 
157
159
  > In `quotes` §3.2's `canOpen` right-test, which rejects a `right` in `CLOSEISH`, the marker
158
160
  > receives **the same exemption `STRAIGHT` receives** and does not disqualify.
159
161
 
162
+ **One rule reads the marker as a trigger rather than as a class member (spec 1.4.0).** `quotes`'
163
+ span-boundary elision veto (`quotes.md` §3.2) fires only when a `NARROW` mark's literal left or
164
+ right neighbour *is* the −1 marker, and then reads the `LETTER` run on the mark's other side
165
+ against a cited locale list. That is not a class membership and adds no row above: the marker
166
+ stays in exactly the classes this table gives it. It is recorded here because the veto exists
167
+ precisely to repair what those memberships cost — `MARKER` in `OPENISH` and in `CLOSEISH` is
168
+ what let `` `x`'s `` and `l'<em>idée</em>` be classified as quotation candidates, inverting the
169
+ enclosing pair (canonical issue #53). **The memberships themselves must not be narrowed to fix
170
+ that**: the rows below (`"<em>hello</em>"`, `"a"<code>x</code>"b"`) depend on them, and removing
171
+ the marker from either class breaks every quotation that begins or ends at a span boundary, at
172
+ both widths.
173
+
174
+ **The two mistakes are not the same size, and conflating them misleads a port reviewer.** The
175
+ other tempting fix — letting the marker satisfy the medial-elision veto's `ALNUM` test — is
176
+ stated over `NARROW` (`quotes.md` §3.2), so it cannot touch the two `WIDE` rows below at all.
177
+ Its witnesses are their `NARROW` forms: `<em>'fine'</em>` and `'a'<code>x</code>'b'`. Measured,
178
+ not argued — that variant breaks the released fixture
179
+ `en-us-markdown-commonmark-boundary-nested-quotes`, whose input is `*'hi'*`.
180
+
160
181
  Everywhere else the marker is opaque content: it is "a content character" and nothing more —
161
182
  **with one exception, which is normative and which resolves a contradiction between this
162
183
  document and `spaces.md`.**
@@ -339,7 +360,11 @@ reaches the edge-growth rule. `--` is the live carrier.)
339
360
  1. Extract the span sequence from the source. Record each span's **source offsets**.
340
361
  2. Build the code-point array `S₁ ⌢ [m₁] ⌢ S₂ ⌢ [m₂] ⌢ … ⌢ Sₘ`, where each `mₖ` is **−1 or −2
341
362
  by §3.2's test on the raw source of that gap** — not always −1. In `yaml` the −2 is the common
342
- case, since a block scalar's spans are separated by a line terminator.
363
+ case, since a block scalar's spans are separated by a line terminator. **Whether a −1 can
364
+ arise in `yaml` at all is open** — no construction has been exhibited, and §3.8.4 step 9
365
+ gives flow collections no spans — see `quotes.md` §3.2, which depends on the answer for
366
+ nothing but states the obligation it creates: a rule must key off the marker, never off the
367
+ mode.
343
368
  3. Run the pipeline **once**, in `order.json` order, over that array.
344
369
  4. Each edit lies wholly within one span (§3.4). Map it back to source offsets.
345
370
  5. Emit the **original source bytes**, with those replacements applied and nothing else changed.
@@ -1,9 +1,10 @@
1
1
  # Rule: `nbsp`
2
2
 
3
3
  **Order:** 70 (last). **Default:** on. **Modes:** text, html, markdown, yaml.
4
- **Spec version:** 1.3.0 (0.1.0 for everything except §3.9's `initialBinding` change (0.6.0),
5
- §3.3's span-boundary paragraphs (1.2.0) and §3.3's character-reference guard (1.3.0), noted
6
- inline).
4
+ **Spec version:** 1.5.0 (0.1.0 for everything except §3.9's `initialBinding` change (0.6.0),
5
+ §3.3's span-boundary paragraphs (1.2.0), §3.3's character-reference guard (1.3.0) and §5's `I₆`
6
+ discharge against `apostrophe`'s `CLOSEDELIM` (1.5.0), noted inline — 1.5.0 adds no sub-rule and
7
+ changes nothing this rule computes).
7
8
 
8
9
  ---
9
10
 
@@ -891,9 +892,22 @@ must be re-derived.
891
892
  over the neighbour tests; the `quotes` half is the one that matters and is written out in
892
893
  `quotes.md` §5.6, which shows that neither insertion site can _add_ a capability to a surviving
893
894
  straight mark. `apostrophe`'s case ladder reads `ALNUM`, `LETTER`, `DIGIT`, `SPACELIKE`,
894
- `OPENISH`, `OPENQUOTE` and `CLOSEISH`; an inserted no-break space is `SPACELIKE`, and the only cases it
895
- could newly satisfy require a `SPACELIKE` **left** neighbour, which is case 4 (leading elision)
896
- — and case 4 also requires an `ALNUM` right neighbour, which an insertion cannot create.
895
+ `OPENISH`, `OPENQUOTE`, `CLOSEISH` and — from spec 1.5.0 — `CLOSEDELIM`; an inserted no-break
896
+ space is `SPACELIKE`, and the only cases it could newly satisfy require a `SPACELIKE` **left**
897
+ neighbour, which is case 4 (leading elision) — and case 4 also requires an `ALNUM` right
898
+ neighbour, which an insertion cannot create.
899
+
900
+ `CLOSEDELIM` (spec 1.5.0) needs its own line because this rule inserts *beside quote glyphs*, and
901
+ six of that class's seven members are closing brackets and closing quotation marks. Two facts
902
+ discharge it. First, U+00A0 and U+202F are in no `apostrophe` class but `SPACELIKE`, so an
903
+ insertion can only ever *remove* a `CLOSEDELIM` member from a neighbour position, never create
904
+ one — and case 2a is the only case reading that class, so removal can only decline. Second, the
905
+ one shape where the two rules are genuinely adjacent is an inner space beside a quotation glyph
906
+ (N1/N2): in `fr`, `«mot»'s` gains its U+00A0 on the glyph's **inner** side, giving `« mot »'s`,
907
+ so the `»` that case 2a reads stays immediately left of the mark and the verdict is identical
908
+ before and after. Pinned as a conformance fixture rather than left as prose. If a future
909
+ sub-rule inserts between a closing delimiter and a following mark, this paragraph must be
910
+ re-derived.
897
911
  `symbols` reads `ALNUM`, `DIGIT` and symmetric spacing; a no-break space is already accepted as
898
912
  spacing there (`symbols.md` §3.3 step 1), and converting U+0020 to U+00A0 leaves `lsp`/`rsp` unchanged.
899
913
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.4.0",
2
+ "spec": "1.6.0",
3
3
  "$comment": "Single source of truth for pipeline order. Rules run in ascending `order`. Disabling a rule removes it from the sequence and never reorders the rest. Rule ids are public API (see docs/ARCHITECTURE.md section 5). \"spec\" here must track spec/VERSION exactly — it is not itself the global version source; scripts/validate-spec.mjs enforces the match.",
4
4
  "rules": [
5
5
  {
@@ -56,7 +56,7 @@
56
56
  "default": "on",
57
57
  "modes": ["text", "html", "markdown", "yaml"],
58
58
  "localeData": [],
59
- "summary": "Remaining straight apostrophes to U+2019 without corrupting contractions. Reads no locale data: spec 0.5.0's `quotes.elisionIdioms` lookup existed only for the preserve set withdrawn in spec 1.1.0, so every mark `quotes` declined to claim now reaches this rule's ordinary structural case ladder."
59
+ "summary": "Remaining straight apostrophes to U+2019 without corrupting contractions. Reads no locale data: spec 0.5.0's `quotes.elisionIdioms` lookup existed only for the preserve set withdrawn in spec 1.1.0, so every mark `quotes` declined to claim now reaches this rule's ordinary structural case ladder. Spec 1.5.0 accepts a closing bracket or closing quotation glyph on the mark's left — the counterparts the right-hand tests had accepted since 0.4.1 — so a possessive after `)`, `]`, `}`, `\u00bb`, `\u201d` or `\u203a` converts."
60
60
  },
61
61
  {
62
62
  "id": "symbols",
@@ -4,7 +4,8 @@
4
4
  states the invariant that `transform` as a whole must satisfy, proves that per-rule
5
5
  idempotency does not imply it, and defines the obligation each rule must discharge so that it
6
6
  does.
7
- **Spec version:** 1.2.0 (0.1.0 for everything except S-b's word-start clause, added in 1.2.0).
7
+ **Spec version:** 1.5.0 (0.1.0 for everything except S-b's word-start clause, added in 1.2.0,
8
+ and §6's consumed-alphabet minimum, which gained U+0029 in 1.5.0).
8
9
 
9
10
  ---
10
11
 
@@ -451,7 +452,7 @@ The idempotency property test must include, in every runtime:
451
452
 
452
453
  | | |
453
454
  | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
454
- | **consumed** | `"` `'` `-` U+0020 `.` `1` `a`, plus U+2010 (hyphen) and U+2212 (minus sign) — spec 0.2.0 added both to `dashes`' `DASH` class (`dashes.md` §3.1) |
455
+ | **consumed** | `"` `'` `-` U+0020 `.` `1` `a`, plus U+2010 (hyphen) and U+2212 (minus sign) — spec 0.2.0 added both to `dashes`' `DASH` class (`dashes.md` §3.1) — and U+0029 `)`, which spec 1.5.0 made a left-hand class member for `apostrophe` (`apostrophe.md` §3.1 `CLOSEDELIM`, case 2a); `)` `'` `a` is three code points, well inside the wide tier's length bound |
455
456
  | **emitted** | every locale `quotes.*.open`/`close` glyph in the registry — at minimum `«` `»` `“` `”` `„` `‘` `’` — plus U+2013, U+2014, U+2026, U+00A0, U+202F, U+2011 |
456
457
 
457
458
  This is a normative requirement and it was learned the expensive way. **Idempotency is a