polytypo 1.4.1 → 1.5.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.
- checksums.yaml +4 -4
- data/lib/polytypo/data/VERSION +1 -1
- data/lib/polytypo/data/fixtures/cs.json +1 -1
- data/lib/polytypo/data/fixtures/de-CH.json +1 -1
- data/lib/polytypo/data/fixtures/de-DE.json +1 -1
- data/lib/polytypo/data/fixtures/el.json +1 -1
- data/lib/polytypo/data/fixtures/en-GB.json +50 -1
- data/lib/polytypo/data/fixtures/en-US.json +41 -1
- data/lib/polytypo/data/fixtures/es.json +1 -1
- data/lib/polytypo/data/fixtures/fi.json +1 -1
- data/lib/polytypo/data/fixtures/fr-CA.json +1 -1
- data/lib/polytypo/data/fixtures/fr.json +9 -1
- data/lib/polytypo/data/fixtures/it.json +1 -1
- data/lib/polytypo/data/fixtures/locale-resolution.json +1 -1
- data/lib/polytypo/data/fixtures/nl.json +1 -1
- data/lib/polytypo/data/fixtures/pl.json +1 -1
- data/lib/polytypo/data/fixtures/pt-BR.json +1 -1
- data/lib/polytypo/data/fixtures/pt-PT.json +1 -1
- data/lib/polytypo/data/fixtures/ru.json +1 -1
- data/lib/polytypo/data/fixtures/sv.json +1 -1
- data/lib/polytypo/data/fixtures/uk.json +1 -1
- data/lib/polytypo/data/locales/registry.json +1 -1
- data/lib/polytypo/data/rules/apostrophe.md +183 -20
- data/lib/polytypo/data/rules/modes.md +29 -4
- data/lib/polytypo/data/rules/nbsp.md +20 -6
- data/lib/polytypo/data/rules/order.json +2 -2
- data/lib/polytypo/data/rules/pipeline-idempotency.md +3 -2
- data/lib/polytypo/data/rules/quotes.md +234 -9
- data/lib/polytypo/data/rules/symbols.md +25 -5
- data/lib/polytypo/engine/rules/apostrophe.rb +14 -1
- data/lib/polytypo/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ff5ce01ef6e41610e4b170c65bec8d01fda6ec6bb9ade51270a99533f289454
|
|
4
|
+
data.tar.gz: 0b8c207111b01b21546d93a49fc0393b3cf040642073d2a95063c06de8cbff2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78259af3e3d2db20353b7f8aa2a8adab72473ac18a3a54a442fb2f5670928a8ccb954c144852ebaf6ec6466efd2b21c71aefe6a5cd97570e293082fa051b63cd
|
|
7
|
+
data.tar.gz: 6425e0eabb69b66ea85953c5edd6ef57112f8054ec9c6224bfd7618a7b5a4eace5e421115646b3a841abde0ae56ac1a95295be0c483a6fad91a45f1c80bbc3c9
|
data/lib/polytypo/data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.5.0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.5.0",
|
|
3
3
|
"locale": "en-GB",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -1339,6 +1339,55 @@
|
|
|
1339
1339
|
"in": "'a'<code>x</code>'b'",
|
|
1340
1340
|
"out": "‘a’<code>x</code>‘b’",
|
|
1341
1341
|
"note": "The negative control, and the reason the veto reads a cited fragment rather than the marker itself. modes.md §3.3 puts the marker in OPENISH precisely so the second pair here can open flush after a span; letting the marker satisfy the medial-elision veto's ALNUM test would have broken this row and en-us-markdown-commonmark-boundary-nested-quotes with it. The run b is not a listed fragment, so the quotation stands."
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
"id": "en-gb-apostrophe-closing-guillemets-agree",
|
|
1345
|
+
"rule": "apostrophe",
|
|
1346
|
+
"mode": "text",
|
|
1347
|
+
"in": "»Wort«'s, ‹Wort›'s and «Wort»'s.",
|
|
1348
|
+
"out": "»Wort«’s, ‹Wort›’s and «Wort»’s.",
|
|
1349
|
+
"note": "apostrophe.md §6 row 21 — the asymmetry case 2a removes, in one line. The first form 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+00BB and U+203A are CLOSEISH members and no left-hand test read that class. Same shape, same reading, three glyphs, now one verdict. Foreign glyphs in en-GB by design: under quotes.md §0 mandate 1 they are re-typesetting candidates, and en-GB does re-typeset them when they form a pair — «Wort» on its own becomes ‘Wort’. None of the three is paired here, and the reason is quotes' V1 same-code-point veto reading V1ID(U+0027) = U+2019 (quotes.md §5), so this case is a V1ID witness as well as a case 2a one."
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
"id": "en-gb-markdown-commonmark-possessive-after-paren-past-code-span",
|
|
1353
|
+
"rule": "apostrophe",
|
|
1354
|
+
"mode": "markdown",
|
|
1355
|
+
"dialect": "commonmark",
|
|
1356
|
+
"in": "The `nbsp` (R₈)'s emission alphabet is one character.\n",
|
|
1357
|
+
"out": "The `nbsp` (R₈)’s emission alphabet is one character.\n",
|
|
1358
|
+
"note": "Case 2a across a mode adapter: the code span is skipped, the parenthetical is prose, and the mark's left neighbour in the processable text is the literal U+0029 rather than modes.md §3.2's marker. The marker case is case 4's and is pinned separately."
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
"id": "en-gb-apostrophe-closeish-punctuation-declined",
|
|
1362
|
+
"rule": "apostrophe",
|
|
1363
|
+
"mode": "text",
|
|
1364
|
+
"in": "He said,'yes' and left.",
|
|
1365
|
+
"out": "He said,'yes’ and left.",
|
|
1366
|
+
"note": "apostrophe.md §6 row 23: U+002C is deliberately excluded from CLOSEDELIM. `quotes` declined the pairing because 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 stays U+0027, recoverable per §7 item 4, and the trailing mark is case 3. The row's output is the same before and after spec 1.5.0 and is pinned so the exclusion stays deliberate."
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
"id": "en-gb-apostrophe-prime-after-superscript-untouched",
|
|
1370
|
+
"rule": "apostrophe",
|
|
1371
|
+
"mode": "text",
|
|
1372
|
+
"in": "f'(x) = 2 and f²'(x) = 4.",
|
|
1373
|
+
"out": "f'(x) = 2 and f²'(x) = 4.",
|
|
1374
|
+
"note": "Both primes survive spec 1.5.0, and for a reason worth naming: case 2a rejects them on its LEFT-test, not the case 1 prime guard, which reads DIGIT on the left only. The left neighbours here are the letter f and U+00B2, general category No, and neither is in CLOSEDELIM. The right-test rejects them as well — U+0028 is in no right-hand class — so nothing any converting case accepts is present on either side."
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
"id": "en-gb-apostrophe-possessive-after-closing-single-quote",
|
|
1378
|
+
"rule": "apostrophe",
|
|
1379
|
+
"mode": "text",
|
|
1380
|
+
"in": "A ‘quoted’'s meaning.",
|
|
1381
|
+
"out": "A ‘quoted’’s meaning.",
|
|
1382
|
+
"note": "U+2019 is the CLOSEDELIM member the idempotency argument turns on (apostrophe.md §5), and the one a port is most likely to leave out: it is both this rule's only emission and a member of a class the rule now reads on the left. The doubled glyph is what the ladder specifies rather than an artefact of it: the left neighbour is a closing delimiter and the right is a letter, so case 2a converts, and this rule reorders and removes nothing. Whether an editor would rather see the construction rephrased is outside its remit — it decides what the author typed, not whether they should have (§7 item 9). The case is a fixed point, which is what §5's left-hand vacuity argument predicts. The two contiguous U+2019 it leaves are §7 item 10: CMOS's own editors would set a separating space there, no rule in order.json inserts one, and this rule cannot — every edit is one code point for one."
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
"id": "en-gb-apostrophe-opening-mark-after-closing-delimiter",
|
|
1386
|
+
"rule": "apostrophe",
|
|
1387
|
+
"mode": "text",
|
|
1388
|
+
"in": "(aside)'quoted' here",
|
|
1389
|
+
"out": "(aside)’quoted’ here",
|
|
1390
|
+
"note": "apostrophe.md §6 row 25 and §7 item 9 — case 2a's accepted cost, pinned so no port can narrow or widen it silently. The author meant a quotation and both marks now read as closing glyphs. It was mismatched before spec 1.5.0 too, in the other direction: 1.4.0 gave (aside)'quoted’ here, case 3 having curled the trailing mark while the leading one matched no case. `quotes` declines the leading mark because canOpen's right-test rejects a CLOSEISH left neighbour, so a quotation opening flush after a closing delimiter is exactly the shape it cannot pair, and two neighbours cannot separate that from a possessive."
|
|
1342
1391
|
}
|
|
1343
1392
|
]
|
|
1344
1393
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.5.0",
|
|
3
3
|
"locale": "en-US",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -2530,6 +2530,46 @@
|
|
|
2530
2530
|
"in": "He said <em>'s'</em> loudly.",
|
|
2531
2531
|
"out": "He said <em>’s’</em> loudly.",
|
|
2532
2532
|
"note": "Accepted false positive, recorded in the suite rather than left in someone's content: a quotation inside an inline span whose content is EXACTLY a listed fragment is read as an elision. Strictly narrower than, and the same family as, the universal medial-n veto's accepted The letter 'n' is common. — it needs the span boundary as well as the single-fragment content. '<em>x</em>', '<em>no</em>' and '<em>fine</em>' in the same position are unaffected. quotes.md §3.2."
|
|
2533
|
+
},
|
|
2534
|
+
{
|
|
2535
|
+
"id": "en-us-apostrophe-possessive-after-closing-paren",
|
|
2536
|
+
"rule": "apostrophe",
|
|
2537
|
+
"mode": "text",
|
|
2538
|
+
"in": "The pipeline (order 90)'s own output is stable.",
|
|
2539
|
+
"out": "The pipeline (order 90)’s own output is stable.",
|
|
2540
|
+
"note": "apostrophe.md §6 row 19, case 2a (spec 1.5.0): U+0029 is in CLOSEDELIM, so a possessive attaching to a parenthetical converts. Through spec 1.4.0 this was case 5 — the ladder's right side accepted every CLOSEISH member but its left side accepted no closing delimiter at all."
|
|
2541
|
+
},
|
|
2542
|
+
{
|
|
2543
|
+
"id": "en-us-apostrophe-possessive-after-closing-quote",
|
|
2544
|
+
"rule": "apostrophe",
|
|
2545
|
+
"mode": "text",
|
|
2546
|
+
"in": "“Hamlet”'s first line is the question.",
|
|
2547
|
+
"out": "“Hamlet”’s first line is the question.",
|
|
2548
|
+
"note": "apostrophe.md §6 row 20, case 2a: U+201D is en-US's own primary closing glyph. `quotes` leaves both existing glyphs in place (they are already this locale's pair) and declines the U+0027, which then reaches case 2a. The case pins the mark's identity, not the construction's merit — CMOS's reachable answer on the possessive of a quoted title steers to an attributive rephrasing and endorses none of the possessive forms, which is advice about what to write rather than a claim about what the mark is (case 2a, §7 item 10)."
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
"id": "en-us-apostrophe-possessive-after-closing-brace-and-bracket",
|
|
2552
|
+
"rule": "apostrophe",
|
|
2553
|
+
"mode": "text",
|
|
2554
|
+
"in": "{user}'s account and footnote [3]'s author.",
|
|
2555
|
+
"out": "{user}’s account and footnote [3]’s author.",
|
|
2556
|
+
"note": "apostrophe.md §6 row 22, case 2a for U+007D and U+005D. A template placeholder closes a group the way any bracket does; this rule has no notion of interpolation syntax and needs none."
|
|
2557
|
+
},
|
|
2558
|
+
{
|
|
2559
|
+
"id": "en-us-apostrophe-symbol-left-declined",
|
|
2560
|
+
"rule": "apostrophe",
|
|
2561
|
+
"mode": "text",
|
|
2562
|
+
"in": "10%'u 24m²'ye 50°'lik",
|
|
2563
|
+
"out": "10%'u 24m²'ye 50°'lik",
|
|
2564
|
+
"note": "apostrophe.md §6 row 24 and §7 item 8: no symbol is in CLOSEDELIM — not U+0025, not U+00B0, not a superscript digit — so every mark here is case 5 and the input is a fixed point. Pinned because issue #28 asked for exactly this widening and it was declined on evidence: TDK's own rules attest the apostrophe after abbreviations and numerals, where case 2 already converts it, and Turkish writes the percent sign before the number."
|
|
2565
|
+
},
|
|
2566
|
+
{
|
|
2567
|
+
"id": "en-us-apostrophe-closing-paren-deleted-by-symbols",
|
|
2568
|
+
"rule": "apostrophe",
|
|
2569
|
+
"mode": "text",
|
|
2570
|
+
"in": "(c)'. and (c)'s",
|
|
2571
|
+
"out": "©'. and ©’s",
|
|
2572
|
+
"note": "symbols.md §5's I₆ discharge, deletion half (spec 1.5.0): §3.2 step 6 deletes the U+0029 that case 2a reads. Both directions in one case. The first mark does not convert — its right neighbour is U+002E, not ALNUM — and after the replacement its left neighbour is the emitted sign, which is in no class, so it does not convert on a second pass either. The second mark converts here, one rule before symbols shortens the span, and is U+2019 by the time symbols runs, which its own S2 guard accepts. Through spec 1.4.0 the second mark stayed straight."
|
|
2533
2573
|
}
|
|
2534
2574
|
]
|
|
2535
2575
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.5.0",
|
|
3
3
|
"locale": "fr",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -928,6 +928,14 @@
|
|
|
928
928
|
"in": "Il dit <em>'l'</em> ici.",
|
|
929
929
|
"out": "Il dit <em>’l’</em> ici.",
|
|
930
930
|
"note": "The accepted false positive in the locale with the widest exposure to it: fr lists thirteen fragments, eight of them a single letter, so a quotation whose whole content is one of them is read as an elision here more readily than anywhere else. Same class as en-us-html-span-boundary-listed-fragment-quoted and as the universal medial-n veto's accepted The letter 'n' is common. — pinned in both locales so the exposure sits in the suite rather than in someone's content. quotes.md §3.2, §6 row S7."
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
"id": "fr-apostrophe-closing-guillemet-past-an-nbsp-insertion",
|
|
934
|
+
"rule": "apostrophe",
|
|
935
|
+
"mode": "text",
|
|
936
|
+
"in": "Le «mot»'s résumé.",
|
|
937
|
+
"out": "Le « mot »’s résumé.",
|
|
938
|
+
"note": "The one configuration where a rule running AFTER apostrophe touches a CLOSEDELIM neighbour, so nbsp.md §5's I₆ discharge has a witness rather than only prose. N1/N2 put fr's inner no-break space on the glyph's INNER side, so the U+00BB that case 2a reads stays immediately left of the mark and the verdict is identical before and after the insertion. Synthetic rather than natural French — a possessive 's is not French — and deliberately so, in the standing quotes.md §6 gives its own adversarial de-CH witness: what is being pinned is the rule interaction, not an idiom."
|
|
931
939
|
}
|
|
932
940
|
]
|
|
933
941
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.5.0",
|
|
3
3
|
"$comment": "Locale resolution input. Algorithm is specified in spec/rules/locale-resolution.md and is identical in every runtime — never delegate it to a platform locale-negotiation library. \"spec\" here must track spec/VERSION exactly — it is not itself the global version source; scripts/validate-spec.mjs enforces the match.",
|
|
4
4
|
"locales": [
|
|
5
5
|
"en-US",
|
|
@@ -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.
|
|
5
|
-
withdrawal of the shared ambiguity preserve set (1.1.0),
|
|
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
|
|
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
|
-
|
|
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`,
|
|
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`
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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`
|
|
249
|
-
that mark's **left** — but its left is `u`, which is U+0027, not `ALNUM`. Cases 3 and 3a
|
|
250
|
-
`LETTER` on its left
|
|
251
|
-
never edited, and no surviving
|
|
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)
|
|
277
|
-
are replaced, and
|
|
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
|
|
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.
|
|
11
|
-
`nbsp` and `apostrophe`, split in 1.2.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`)
|
|
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.
|
|
5
|
-
§3.3's span-boundary paragraphs (1.2.0)
|
|
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 `
|
|
895
|
-
could newly satisfy require a `SPACELIKE` **left**
|
|
896
|
-
— and case 4 also requires an `ALNUM` right
|
|
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.
|
|
2
|
+
"spec": "1.5.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.
|
|
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
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Rule: `quotes`
|
|
2
2
|
|
|
3
3
|
**Order:** 40. **Default:** on. **Modes:** text, html, markdown, yaml.
|
|
4
|
-
**Spec version:** 1.
|
|
5
|
-
described in §3.2 and the History
|
|
4
|
+
**Spec version:** 1.4.0 (0.4.1 for everything except the universal medial-`n` elision veto
|
|
5
|
+
(1.1.0) and the span-boundary elision veto (1.4.0), both described in §3.2 and in the History
|
|
6
|
+
section below).
|
|
6
7
|
|
|
7
8
|
---
|
|
8
9
|
|
|
@@ -86,6 +87,62 @@ normalising the spacing immediately inside each pair.
|
|
|
86
87
|
imports (`scripts/lib/is-letter.mjs`), so validation and the runtime engine cannot classify a
|
|
87
88
|
code point differently.
|
|
88
89
|
|
|
90
|
+
- `quotes.elisionClitics` (spec 1.4.0) — an object with two arrays, `before` and `after`, each
|
|
91
|
+
a list of literal lowercase strings, consulted only by the **span-boundary elision veto**
|
|
92
|
+
(§3.2) to **decline** pairing a `NARROW` mark as a quotation when the mark is flush against an
|
|
93
|
+
inline span boundary on one side and a listed word fragment on the other. Both lists may be
|
|
94
|
+
empty, and empty is the default for a locale with no citable closed set of such fragments.
|
|
95
|
+
|
|
96
|
+
**Both lists are defined positionally, not by what the fragment attaches to.** `before` is
|
|
97
|
+
matched against the maximal `LETTER` run ending immediately *before* the mark; `after` against
|
|
98
|
+
the run beginning immediately *after* it. Attachment direction is the linguistic motivation for
|
|
99
|
+
an entry, never the test — and the two do not coincide here, because the whole point of this
|
|
100
|
+
veto is that the word the fragment attaches to may be on the far side of a span boundary and
|
|
101
|
+
therefore absent from the rule's input. In `` `x`'s `` the possessive attaches back to the `x`
|
|
102
|
+
inside the code span, yet what stands to the mark's left is the `MARKER`; the fragment this
|
|
103
|
+
field must name is the `s` on the **right**. Reading the lists as attachment claims makes
|
|
104
|
+
`after` look inert (a fragment with a `LETTER` on each side is already vetoed as medial) and is
|
|
105
|
+
the one misreading to guard against.
|
|
106
|
+
|
|
107
|
+
- **`before`** — French `l'`, `d'`, `qu'`, `jusqu'` give `l`, `d`, `qu`, `jusqu`; Portuguese
|
|
108
|
+
`d'`, `n'`, `pel'`, `Sant'` give `d`, `n`, `pel`, `sant`; Italian `l'`, `un'`, `d'`.
|
|
109
|
+
- **`after`** — English `x's` gives `s`; Dutch `'s morgens`, `'t was`, `'ns kijken` give `s`,
|
|
110
|
+
`t`, `ns`.
|
|
111
|
+
|
|
112
|
+
**An entry is the whole maximal run, never a prefix of one.** French needs `jusqu`, `lorsqu`,
|
|
113
|
+
`puisqu` and `quoiqu` as entries of their own: the run in `jusqu'à` is `jusqu`, so an entry
|
|
114
|
+
`qu` cannot match it. The same arithmetic is why Italian's `l` does not cover `dell'arte` (the
|
|
115
|
+
run is `dell`), which makes a partial list honest rather than misleadingly narrow.
|
|
116
|
+
|
|
117
|
+
"Elision" here covers the possessive, exactly as `apostrophe.md` §3.3 case 3 does in its own
|
|
118
|
+
name ("Trailing elision or possessive") — English `'s` is not an elision and is the one entry
|
|
119
|
+
that locale has.
|
|
120
|
+
|
|
121
|
+
**Every code point of every entry must be a `LETTER`**, and an entry must be non-empty. The
|
|
122
|
+
constraint is enforced at the data boundary by `locale.schema.json` and by
|
|
123
|
+
`scripts/validate-spec.mjs`, for the reason §3.2 gives: the matcher walks a maximal `LETTER`
|
|
124
|
+
run, so an entry containing anything else could never match and would be dead data rather than
|
|
125
|
+
a rejected claim. Entries are authored **lowercase**; §3.2's matcher folds only the first code
|
|
126
|
+
point, and only ASCII `A`–`Z`, so `L'` matches an entry `l` and `QU'` does not match `qu`.
|
|
127
|
+
What is *enforced* at the data boundary is exactly that much and no more: an entry whose first
|
|
128
|
+
code point is ASCII `A`–`Z` is rejected, because the fold makes it unmatchable by
|
|
129
|
+
construction. An entry capitalised in a script the fold does not reach is not rejected — it is
|
|
130
|
+
simply matched as written, which is well-defined and identical in every runtime, so the
|
|
131
|
+
authoring convention is stated as a convention rather than as a validated constraint.
|
|
132
|
+
|
|
133
|
+
**The leniency is therefore Latin-only in effect, and that is a scoping fact, not an
|
|
134
|
+
oversight.** A Cyrillic or Greek entry cannot serve: `ru`'s own cited example is `Д'Аламбер`
|
|
135
|
+
with a capital U+0414, which a lowercase entry `д` will not match, while an uppercase entry
|
|
136
|
+
would miss `д'Артаньян` in the same citation. Widening the fold is `ARCHITECTURE.md` §4.4's
|
|
137
|
+
banned territory (no locale-dependent case mapping — Turkish dotless ı), so a locale whose
|
|
138
|
+
script the fold does not reach is `[]` on mechanism grounds, before its orthography is even
|
|
139
|
+
consulted. `ru`, `uk` and `el` are `[]` for exactly this reason as well as their own.
|
|
140
|
+
|
|
141
|
+
A decline-only pair of lists can never widen what this rule pairs, only narrow it — the same
|
|
142
|
+
principle `elisionIdioms` above rests on, stated normatively in `nbsp.md` §2.1 and governing
|
|
143
|
+
every list-valued field in `locale.schema.json`. Both lists empty makes the veto a total
|
|
144
|
+
no-op and classifies every mark exactly as spec 1.3.1 did.
|
|
145
|
+
|
|
89
146
|
`innerSpace` is read and acted on in **one direction only**: `quotes` *deletes* an inner space
|
|
90
147
|
when the assigned pair's `innerSpace` is `"none"`, and never inserts or converts one. Insertion
|
|
91
148
|
and conversion stay with `nbsp` (order 70), exactly as 0.1.0 always said. See §3.7.
|
|
@@ -446,6 +503,107 @@ over these bytes could. The trade is deliberate and was made with the numbers in
|
|
|
446
503
|
for this same shape by declining **every** short quotation in **every** locale, a far larger and
|
|
447
504
|
far commoner class of error than quoting the single letter *n*.
|
|
448
505
|
|
|
506
|
+
**Span-boundary elision veto (spec 1.4.0, `NARROW` marks only, locale data
|
|
507
|
+
`quotes.elisionClitics`, literal reads).** The two vetoes above both need a real code point on
|
|
508
|
+
each side of the mark. `modes.md` §3.2's boundary marker is not one, so in `html` and `markdown`
|
|
509
|
+
a mark written flush against an inline span — `` `x`'s ``, `<code>x</code>'s`, `*x*'s`,
|
|
510
|
+
`l'<em>idée</em>` — has a `MARKER` where the attaching word would be, escapes both, and is
|
|
511
|
+
classified as an ordinary quotation candidate. `Llit = MARKER` is in `OPENISH`, so the mark
|
|
512
|
+
`canOpen`; inside a quotation it has a partner to take, and the accepted pairing inverts:
|
|
513
|
+
`He says 'avoid `` `x` ``'s printer.' Done.` was typeset with the possessive opening the
|
|
514
|
+
quotation and the author's opening mark demoted to a closing glyph, in every locale (issue #53,
|
|
515
|
+
measured on 1.3.1).
|
|
516
|
+
|
|
517
|
+
This veto reads the attaching word instead of the missing code point:
|
|
518
|
+
|
|
519
|
+
> For a mark at `i` with `cp[i] ∈ NARROW`, **both** tests below are evaluated and either firing
|
|
520
|
+
> sets both capabilities false.
|
|
521
|
+
>
|
|
522
|
+
> - **`Llit = MARKER`** (the mark is flush against a span boundary on its left). Let `R` be the
|
|
523
|
+
> maximal run of `LETTER` code points starting at `i+1`, and `outer` the code point after that
|
|
524
|
+
> run, or `NONE`. If `R` is non-empty, `outer ∉ ALNUM`, and `R` equals an entry of
|
|
525
|
+
> `quotes.elisionClitics.after` — **every code point exact, except the first, which folds
|
|
526
|
+
> ASCII `A`–`Z` to `a`–`z` by code point and nothing else** — then veto.
|
|
527
|
+
> - **`Rlit = MARKER`.** Symmetric: `L` is the maximal `LETTER` run *ending* at `i-1`, `outer`
|
|
528
|
+
> the code point before it, and the comparison is against `quotes.elisionClitics.before`.
|
|
529
|
+
>
|
|
530
|
+
> The folding is the one `elisionIdioms` already specifies, for the same reason and by the same
|
|
531
|
+
> mechanism — never a platform locale function (`ARCHITECTURE.md` §4.4).
|
|
532
|
+
|
|
533
|
+
**One literal neighbour must be the marker, and that is the whole scope of this veto.** With a
|
|
534
|
+
real code point on both sides the medial-elision veto above already declines the same shape
|
|
535
|
+
(`x's` needs no span boundary to be a possessive), so this one adds nothing there and is not
|
|
536
|
+
consulted. In `text` mode that makes the veto vacuous: no marker is produced at all, so a
|
|
537
|
+
locale's lists cannot change what `text` does.
|
|
538
|
+
|
|
539
|
+
**In `yaml` the veto is reachable only if the scan ever puts two spans on one line, and it must
|
|
540
|
+
not be special-cased either way.** `modes.md` §3.5 step 2 says the −2 line marker is the
|
|
541
|
+
*common* case there, not the only one, so this document does not claim the veto is unreachable
|
|
542
|
+
in `yaml`. What it does claim is narrower and checkable: the scan yields no spans at all inside
|
|
543
|
+
a flow collection (`modes.md` §3.8.4 step 9), and a block mapping carries one key per line, so no
|
|
544
|
+
construction has yet been exhibited in which two `yaml` spans are separated by a gap without a
|
|
545
|
+
line terminator — and a −2 marker is in `BREAK`, which neither test reads. Whether a −1 is
|
|
546
|
+
constructible in `yaml` is **open**, and nothing here depends on the answer, because the trigger
|
|
547
|
+
is the marker and not the mode: **a runtime must not implement this veto behind a
|
|
548
|
+
`mode == "yaml"` test, or any other mode test.** Two modes agreeing on identical characters is
|
|
549
|
+
`modes.md` §7.4's requirement, and a mode conditional here would break it the moment such a
|
|
550
|
+
construction is found.
|
|
551
|
+
|
|
552
|
+
**The run is compared whole, never as a prefix.** `<em>'sure'</em>` mid-sentence has the run
|
|
553
|
+
`sure`, which is not an entry, and pairs as the quotation it is; a prefix test would have matched
|
|
554
|
+
the entry `s` and eaten it. The `outer ∉ ALNUM` test is what makes the run maximal in the
|
|
555
|
+
direction it grows — without it `` `x`'st `` would match `s`.
|
|
556
|
+
|
|
557
|
+
**What was tried and rejected: letting the `MARKER` count as content in the medial-elision
|
|
558
|
+
veto.** The obvious one-line fix — treat `MARKER` as satisfying that veto's `ALNUM` test, on the
|
|
559
|
+
marker-adjacent side or on both — was implemented and measured, and it is wrong. It cannot
|
|
560
|
+
separate a possessive from a quotation that legitimately *begins or ends* at a span boundary,
|
|
561
|
+
which is the shape `modes.md` §3.3 put the marker in `OPENISH` and `CLOSEISH` to support in the
|
|
562
|
+
first place. Its witnesses are the `NARROW` forms of those rows — `<em>'fine'</em>` and
|
|
563
|
+
`'a'<code>x</code>'b'` — and only those: this veto and the medial one are both stated over
|
|
564
|
+
`NARROW`, so the `WIDE` rows `modes.md` §3.3 writes with U+0022 are untouched by either variant
|
|
565
|
+
and are witnesses for a different mistake (removing the marker from the classes outright).
|
|
566
|
+
Measured on polytypo-js: both variants break the released conformance fixture
|
|
567
|
+
`en-us-markdown-commonmark-boundary-nested-quotes` (`*'hi'*` ⟶ `*’hi’*`, where `*‘hi’*` is
|
|
568
|
+
pinned), and the symmetric variant additionally breaks every quotation whose closing mark abuts a
|
|
569
|
+
span — `<em>He said 'hi'</em>`. A clitic list is narrower than the marker: it fires on
|
|
570
|
+
`` `x`'s `` and not on `` `x`'fine' ``, which no test over the marker alone can do, because the
|
|
571
|
+
marker is the same code point in both.
|
|
572
|
+
|
|
573
|
+
**Accepted false positive, recorded rather than tolerated.** A quotation inside an inline span
|
|
574
|
+
whose content is *exactly* a listed fragment is read as an elision:
|
|
575
|
+
`He said <em>'s'</em> loudly.` becomes `He said <em>’s’</em> loudly.`, and `Il dit <em>'l'</em>
|
|
576
|
+
ici.` likewise in `fr`. This is the same exposure, and strictly narrower than, the universal
|
|
577
|
+
medial-`n` veto's accepted `The letter 'n' is common.` — it needs the span boundary as well as
|
|
578
|
+
the single-fragment content. `'x'`, `'no'`, `'fine'` and `'sure thing'` in the same position are
|
|
579
|
+
unaffected; the class is bounded by the lists, which are short and cited. Pinned as fixtures.
|
|
580
|
+
|
|
581
|
+
**What this veto does not close, and why no veto can.** A *plural* possessive after a span —
|
|
582
|
+
`` `xs`' printer `` — has the marker on its left and a space on its right, so its attaching run
|
|
583
|
+
is empty and neither test above applies. That shape is also, code point for code point, a
|
|
584
|
+
quotation's closing mark after a span (`'<em>hello</em>'`, a `modes.md` §3.3 row), so no test
|
|
585
|
+
over these neighbours can separate them. It remains a closing candidate, and because pairing is
|
|
586
|
+
resolved per document it remains able to close a mark far away: with `en-GB`,
|
|
587
|
+
|
|
588
|
+
```
|
|
589
|
+
A 'stray mark here.
|
|
590
|
+
|
|
591
|
+
Then 'inner' text.
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
typesets as `A ’stray mark here.` / `Then ‘inner’ text.`, and appending `See `` `x`' `` data.`
|
|
595
|
+
re-pairs the first mark with the new one, making the middle pair nested and changing a line the
|
|
596
|
+
edit did not touch. Measured on 1.3.1 and unchanged by this veto.
|
|
597
|
+
|
|
598
|
+
**This is issue #53 §3, tracked as issue #54, and it stays open.** The report's own trigger lines are
|
|
599
|
+
`` `quotes`' locale data `` and `` `dashes`' own admissibility `` — plural possessives, exactly
|
|
600
|
+
this shape — so the cross-paragraph effect it documents in a real article is *not* repaired by
|
|
601
|
+
spec 1.4.0. Measured on this change, `markdown`/`commonmark`, `en-GB`:
|
|
602
|
+
`It is 'the `` `quotes`' `` locale data' here.` gives
|
|
603
|
+
`It is ‘the `` `quotes`’ `` locale data’ here.` — the plural possessive closes the quotation two
|
|
604
|
+
words early and the author's own closing mark is left to `apostrophe` as a stray U+2019. §1 of
|
|
605
|
+
that issue is closed and §3 is not; the two must not be conflated when the issue is triaged.
|
|
606
|
+
|
|
449
607
|
**V1 — same-V1-identity adjacency veto** (both widths):
|
|
450
608
|
|
|
451
609
|
> Define `V1ID(c) = U+2019 if c = U+0027, else c` — the **V1 identity** of a code point. `V1ID`
|
|
@@ -774,6 +932,11 @@ rule that can falsify it named), per `pipeline-idempotency.md` §5.2.
|
|
|
774
932
|
|
|
775
933
|
- **[P] A medial apostrophe:** `don't`, `l'été`, `O'Brien`, `Hawai'i`, `1990's` — and their
|
|
776
934
|
U+2019 forms on a second pass. Vetoed in §3.2.
|
|
935
|
+
- **[P] A possessive or elision written flush against an inline span boundary** (spec 1.4.0):
|
|
936
|
+
`` `x`'s ``, `<code>x</code>'s`, `*x*'s`, `l'<em>idée</em>` — where the attaching fragment is a
|
|
937
|
+
cited `quotes.elisionClitics` entry. Vetoed in §3.2's span-boundary elision veto and converted
|
|
938
|
+
by `apostrophe`. **[P]** rather than **[R]** because the guarantee is the joint outcome of two
|
|
939
|
+
rules: this one declines to pair, `apostrophe` (order 50) emits the U+2019.
|
|
777
940
|
- **[R] A leading elision that finds no partner** (`'90s`, `'tis`) and **[R] a trailing
|
|
778
941
|
possessive that finds no partner** (`the dogs' bowls`). Handed to `apostrophe`.
|
|
779
942
|
- **[R] A foot or inch mark:** `6' 2"`. Both marks are `canClose` only with an empty stack. The
|
|
@@ -808,6 +971,13 @@ revision introduces; §5's Corollary A1 explains why no U+2019-specific restrict
|
|
|
808
971
|
`spec/fixtures/` pins the verified behaviour across `en-GB`, `en-US`, `fi` and `sv` (§6, rows
|
|
809
972
|
E1–E4) so the tradeoff is a citable fact rather than an assumption.
|
|
810
973
|
|
|
974
|
+
**The same family, reachable through a span boundary, and likewise not closed (spec 1.4.0).** A
|
|
975
|
+
*plural* possessive after a span — `` `xs`' printer `` — is byte-identical to a quotation's
|
|
976
|
+
closing mark after a span, so §3.2's span-boundary elision veto cannot reach it and no test over
|
|
977
|
+
those neighbours could: its attaching fragment is empty on both sides. It stays a `canClose`
|
|
978
|
+
candidate able to close a mark arbitrarily far away, which is item 3's non-locality with a span
|
|
979
|
+
boundary standing in for the elision. §3.2 carries the measured five-line witness.
|
|
980
|
+
|
|
811
981
|
---
|
|
812
982
|
|
|
813
983
|
## 5. Idempotency argument
|
|
@@ -824,11 +994,14 @@ can *gain* a capability. Claim 3 rested on both. Three replacements follow.
|
|
|
824
994
|
> candidate at any index `i ≠ j`.
|
|
825
995
|
|
|
826
996
|
*Proof.* A candidate's four neighbour reads are compared against `NONE`, `SPACELIKE`, `OPENISH`,
|
|
827
|
-
`CLOSEISH`, `DASHISH`, `QUOTEMARK`, `MARKER
|
|
828
|
-
|
|
829
|
-
`
|
|
830
|
-
|
|
831
|
-
|
|
997
|
+
`CLOSEISH`, `DASHISH`, `QUOTEMARK`, `MARKER`, `ALNUM` (the medial veto) and `LETTER` (the
|
|
998
|
+
span-boundary veto's run walk, spec 1.4.0), and V1 compares `V1ID` of the relevant code points
|
|
999
|
+
against `V1ID(g)` (spec 0.4.1). Every `QUOTEMARK` is in `OPENISH`, in `CLOSEISH`, in
|
|
1000
|
+
`QUOTEMARK`, in none of `SPACELIKE`, `DASHISH`, `ALNUM`, `LETTER`, and is neither `MARKER` nor
|
|
1001
|
+
`NONE`. All eight class tests are therefore **invariant**, not merely monotone. The skip walks
|
|
1002
|
+
are invariant because `QUOTEMARK ∩ INLINE-SPACE = ∅`, and the span-boundary veto's `LETTER` runs
|
|
1003
|
+
are invariant for the same reason — no quote glyph is a `LETTER`, so substituting one can neither
|
|
1004
|
+
extend nor truncate a run, nor change the `outer ∉ ALNUM` test that bounds it.
|
|
832
1005
|
|
|
833
1006
|
**V1 is invariant too, as of spec 0.4.1, and this required changing V1 itself, not just arguing
|
|
834
1007
|
about it.** Before 0.4.1, V1 compared raw code points, and replacing a neighbour's `U+0027` with
|
|
@@ -871,7 +1044,10 @@ by the identical literal `rock`/`n`/`roll` context — the veto fires identicall
|
|
|
871
1044
|
vetoed again, `quotes` makes no pairing, and `apostrophe` does not act on U+2019 at all (§4).
|
|
872
1045
|
The construction is a fixed point (§6 row P4 pins it).
|
|
873
1046
|
|
|
874
|
-
**Modes.** `text` is the base case above
|
|
1047
|
+
**Modes.** `text` is the base case above — and for the span-boundary veto that base case is
|
|
1048
|
+
vacuous, since `text` produces no marker at all and neither of its two tests can fire there. For
|
|
1049
|
+
`yaml` see §3.2: the veto is marker-triggered rather than mode-triggered, so it needs no separate
|
|
1050
|
+
argument and must not be given a mode conditional. In `html` and `markdown`, `modes.md` §3.2's
|
|
875
1051
|
concatenation-with-marker model means the veto's bounded lookaround can land on a `MARKER` (a
|
|
876
1052
|
negative integer, in none of `LETTER`, `INLINE-SPACE`, `NARROW`) exactly where a real code
|
|
877
1053
|
point would otherwise be — a context word split from its mark by an element or span boundary
|
|
@@ -888,7 +1064,12 @@ rest of this rule's determinism.
|
|
|
888
1064
|
> *neighbour*, Lemma A applies — **including its V1 clause**, now that V1 itself compares `V1ID`
|
|
889
1065
|
> rather than raw code points. As the mark *itself*: both `U+0027` and `U+2019` are in `NARROW`,
|
|
890
1066
|
> so the stack partition is unchanged; the medial veto is stated over `NARROW`, so its verdict is
|
|
891
|
-
> unchanged;
|
|
1067
|
+
> unchanged; the span-boundary veto (spec 1.4.0) is stated over `NARROW` too and reads only the
|
|
1068
|
+
> `LETTER` run beyond the mark and the `MARKER` beside it, neither of which `apostrophe` touches,
|
|
1069
|
+
> so its verdict is unchanged — which is exactly what makes the fix it performs a fixed point: a
|
|
1070
|
+
> vetoed mark survives pass 2 as U+0027, `apostrophe` emits U+2019 in its place (case 4 across a
|
|
1071
|
+
> `MARKER` on the left, case 3 across one on the right, both already specified), U+2019 is in
|
|
1072
|
+
> `NARROW`, and the next pass vetoes the same index again; neither is in `SPACE-RIGHT`/`SPACE-LEFT`, by constraint **Q-A**; and `V1ID` maps
|
|
892
1073
|
> both to the same value, so V1's verdict on the mark's own candidacy is unchanged too. Every
|
|
893
1074
|
> verdict is identical. This is a genuine **CO-S** discharge in the sense of
|
|
894
1075
|
> `pipeline-idempotency.md` §5.1a — `E(apostrophe) = {U+2019}` is wholly inert for this rule,
|
|
@@ -1138,6 +1319,22 @@ reason as N1–N5 above; they are verified against `spec/fixtures/en-US.json`.
|
|
|
1138
1319
|
| H2 | `html` | `<p>rock 'n' <em>roll</em></p>` | `<p>rock ’n’ <em>roll</em></p>` | mirror case on `right` |
|
|
1139
1320
|
| H3 | `markdown` (`commonmark`) | `*rock* 'n' roll\n` | `*rock* ’n’ roll\n` | `left` word is inside an emphasis span; same reasoning as H1, and the result matches `text` mode on the same characters split the same way |
|
|
1140
1321
|
|
|
1322
|
+
#### Span-boundary elision veto — `html`, `markdown` (spec 1.4.0)
|
|
1323
|
+
|
|
1324
|
+
Every row is measured output, and each is pinned as a conformance fixture; the `Why` column is
|
|
1325
|
+
what a port should be able to re-derive from §3.2 alone.
|
|
1326
|
+
|
|
1327
|
+
| # | Locale | Input | Output | Why |
|
|
1328
|
+
| --- | --- | --- | --- | --- |
|
|
1329
|
+
| S1 | `en-GB` | `` He says 'avoid `x`'s printer.' Done. `` | `` He says ‘avoid `x`’s printer.’ Done. `` | `Llit` is the `MARKER`, the run right of the mark is `s`, and `s` is a cited `after` entry. Vetoed; `apostrophe` case 4 emits U+2019; the author's own pair keeps the primary glyphs. Through 1.3.1 this gave `` ’avoid `x`‘s printer.’ `` |
|
|
1330
|
+
| S2 | `fr` | `Il dit 'l'<em>idée</em> est bonne.' Fin.` | `Il dit «⍽l’<em>idée</em> est bonne.⍽» Fin.` | the mirror direction: `Rlit` is the `MARKER`, the run left of the mark is `l`, a cited `before` entry. Through 1.3.1 this gave `Il dit «⍽l⍽»<em>idée</em> est bonne.' Fin.` — guillemets around one letter, the real closing mark abandoned |
|
|
1331
|
+
| S3 | `fr` | `Il dit 'jusqu'<em>ici</em> tout va bien.' Fin.` | `Il dit «⍽jusqu’<em>ici</em> tout va bien.⍽» Fin.` | the run is compared **whole**: it is `jusqu`, so an entry `qu` could not match it. This is why `jusqu`, `lorsqu`, `puisqu` and `quoiqu` are listed in their own right |
|
|
1332
|
+
| S4 | `fr` | `Il dit <em>'oui'</em> ici.` | `Il dit <em>«⍽oui⍽»</em> ici.` | the negative control the mechanism exists to preserve. `Llit` is the `MARKER` here too — what separates this from S2 is only that `oui` is not a listed fragment |
|
|
1333
|
+
| S5 | `en-GB` | `` He says 'avoid `xs`' printer.' Done. `` | `` He says ‘avoid `xs`’ printer.' Done. `` | **not closed.** The plural possessive's run is empty, so neither test applies, and these code points are also a closing mark after a span. The second mark takes the pairing, the third is abandoned as U+0027. §4, §7 item 10, issue #54 |
|
|
1334
|
+
| S6 | `fr` | `Il dit 'QU'<em>il</em> vienne.' Fin.` | `Il dit «⍽QU⍽»<em>il</em> vienne.' Fin.` | **not closed.** The fold reaches only the run's first code point, and only ASCII `A`–`Z`, so `QU` does not match `qu` and the inversion survives. Same limit as the listed veto's context words |
|
|
1335
|
+
| S7 | `en-US` | `He said <em>'s'</em> loudly.` | `He said <em>’s’</em> loudly.` | the accepted false positive: a quotation inside a span whose whole content is a listed fragment. Narrower than the medial-`n` veto's own accepted `The letter 'n' is common.`, since it needs the boundary as well |
|
|
1336
|
+
| S8 | `nl` | `Hij zegt 'ik kom <em>vroeg </em>'s avonds terug.' Klaar.` | `Hij zegt “ik kom <em>vroeg </em>’s avonds terug.” Klaar.` | `'s` is a word-*initial* omission, so its run lies to the mark's right and the entry is an `after` one — the clearest case for reading both lists positionally (§2) |
|
|
1337
|
+
|
|
1141
1338
|
### `en-GB` — primary `‘ ’`, secondary `“ ”`
|
|
1142
1339
|
|
|
1143
1340
|
| # | Input | Output | Why |
|
|
@@ -1276,6 +1473,19 @@ Ordered by how much this matters.
|
|
|
1276
1473
|
text. §6's dedicated adversarial sweep found no failures, but the protection that remains is
|
|
1277
1474
|
the vetoes and the gate, not an independent structural guarantee.
|
|
1278
1475
|
|
|
1476
|
+
10. **The span-boundary defect is closed for a cited fragment and open for everything else
|
|
1477
|
+
(spec 1.4.0).** §3.2's span-boundary elision veto reads `quotes.elisionClitics`, so a locale
|
|
1478
|
+
with empty lists is classified exactly as 1.3.1 classified it — issue #53's inversion is
|
|
1479
|
+
still reachable there, and the fix for that locale is a cited entry, not a change to this
|
|
1480
|
+
rule. Two shapes stay open in **every** locale, both for the same reason: the attaching
|
|
1481
|
+
fragment is not there to read. A plural possessive after a span (`` `xs`' ``) has an empty
|
|
1482
|
+
run and is byte-identical to a closing mark after a span (§4) — **this is issue #53 §3, the
|
|
1483
|
+
cross-paragraph witness that motivated the report; it is open and tracked as issue #54**; and a fragment written in
|
|
1484
|
+
capitals (`QU'<em>il</em>`) fails the first-code-point folding this rule shares with item 8's
|
|
1485
|
+
listed veto. Neither is a candidate for a wider mechanism: widening the folding is
|
|
1486
|
+
`ARCHITECTURE.md` §4.4's banned territory, and the plural possessive has no local evidence at
|
|
1487
|
+
all.
|
|
1488
|
+
|
|
1279
1489
|
---
|
|
1280
1490
|
|
|
1281
1491
|
## History
|
|
@@ -1322,3 +1532,18 @@ every locale. Three things change together, and none of them works without the o
|
|
|
1322
1532
|
The cost accepted, knowingly and with §6's N1/N2 rows kept as its permanent witnesses, is that a
|
|
1323
1533
|
genuine quotation of the letter *n* is elided. §7 item 8 records why that is the smaller error
|
|
1324
1534
|
than the class 0.5.0 traded it for.
|
|
1535
|
+
|
|
1536
|
+
1.4.0 adds the **span-boundary elision veto** (§2, §3.2): `quotes.elisionClitics`, two
|
|
1537
|
+
locale-data lists of the word fragments that attach across an elision or possessive apostrophe,
|
|
1538
|
+
consulted only when the mark is flush against an inline span boundary. It closes **issue #53
|
|
1539
|
+
§1** — a possessive or elision written against a span (`` `x`'s ``, `l'<em>idée</em>`) was
|
|
1540
|
+
classified as a quotation candidate, took the pairing from the author's own mark inside a
|
|
1541
|
+
quotation, and inverted the pair in every locale. **It does not close issue #53 §3**, the
|
|
1542
|
+
cross-paragraph damage that motivated the report, which is tracked separately as issue #54: that witness is a *plural* possessive after a
|
|
1543
|
+
span, and §3.2 records why no veto over these neighbours can reach it. The simpler design — letting the boundary marker satisfy the
|
|
1544
|
+
medial-elision veto's `ALNUM` test — was implemented, measured, and rejected before any release:
|
|
1545
|
+
it breaks a quotation that legitimately begins or ends at a span boundary, including the
|
|
1546
|
+
released fixture `en-us-markdown-commonmark-boundary-nested-quotes` and the `NARROW` forms of
|
|
1547
|
+
`modes.md` §3.3's normative rows — not their `WIDE` forms, which both vetoes leave alone. §3.2 records that measurement, the accepted false positive (a quotation whose
|
|
1548
|
+
whole content is one listed fragment), and the one shape no veto over these neighbours can
|
|
1549
|
+
reach — the plural possessive, which is byte-identical to a closing mark after a span.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Rule: `symbols`
|
|
2
2
|
|
|
3
3
|
**Order:** 60. **Default:** on. **Modes:** text, html, markdown, yaml.
|
|
4
|
-
**Spec version:** 0.1.0
|
|
4
|
+
**Spec version:** 1.5.0 (0.1.0 for everything except §5's `I₆` discharge against `apostrophe`'s
|
|
5
|
+
`CLOSEDELIM` (1.5.0) — which adds no step and changes nothing this rule computes).
|
|
5
6
|
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -352,10 +353,29 @@ class, so no verdict moves.
|
|
|
352
353
|
points and the emitted signs are not in `WORDISH`, so no listed hyphen form's word boundary
|
|
353
354
|
changes.
|
|
354
355
|
|
|
355
|
-
**Against `I₅`/`I₆` (`quotes`, `apostrophe`).** Discharged
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
356
|
+
**Against `I₅`/`I₆` (`quotes`, `apostrophe`).** Discharged, in two halves — the emission and the
|
|
357
|
+
**deletion** — because this rule is the only one after `apostrophe` that shortens a span.
|
|
358
|
+
|
|
359
|
+
*The emission.* This rule emits nothing in `STRAIGHT`, and the signs it does emit (U+00A9,
|
|
360
|
+
U+00AE, U+2122, U+00D7) are in none of `apostrophe`'s classes and in neither `SPACELIKE` nor
|
|
361
|
+
`ALNUM`, so a surviving straight mark adjacent to one keeps every capability it had. For `quotes`
|
|
362
|
+
the discharge is `quotes.md` §5's **Lemma A and Corollary A1**, which are stated
|
|
363
|
+
position-universally over `QUOTEMARK` substitution; the pre-0.4.1 appeal to "`quotes` Claim 3,
|
|
364
|
+
which needs only that capabilities do not _increase_" is withdrawn — Claim 3 rested on Claims 1
|
|
365
|
+
and 2, which `quotes.md` §0's mandate 1 withdrew, and `apostrophe.md` §5 records that citing it
|
|
366
|
+
is itself a latent defect.
|
|
367
|
+
|
|
368
|
+
*The deletion (spec 1.5.0).* §3.2 step 6 replaces `(c)`, `(r)` and `(tm)` with one code point,
|
|
369
|
+
which **deletes a U+0029** from the text. Through spec 1.4.0 that was invisible to `apostrophe`,
|
|
370
|
+
because U+0029 was in no class its ladder read on a mark's left. From 1.5.0 it is a `CLOSEDELIM`
|
|
371
|
+
member (`apostrophe.md` §3.1), so the deletion removes a code point from a position the next pass
|
|
372
|
+
reads. It is discharged in the candidacy-**removing** direction and cannot flip a verdict:
|
|
373
|
+
a U+0027 that survived this pass with `)` immediately on its left did **not** convert, which by
|
|
374
|
+
case 2a means its right neighbour was not `ALNUM`; after the replacement its left neighbour is
|
|
375
|
+
the emitted sign, in no class at all, so it does not convert on the next pass either. `(c)'.`
|
|
376
|
+
gives `©'.` and holds. The reverse — a mark that converted this pass — is already U+2019 and is
|
|
377
|
+
not a candidate again (`apostrophe.md` §5). If this rule ever emits a bracket or a quotation
|
|
378
|
+
glyph, or deletes one from the *right* of a mark, this paragraph must be re-derived.
|
|
359
379
|
|
|
360
380
|
---
|
|
361
381
|
|
|
@@ -9,7 +9,7 @@ require_relative "../registry"
|
|
|
9
9
|
module Polytypo
|
|
10
10
|
module Engine
|
|
11
11
|
module Rules
|
|
12
|
-
# spec/rules/apostrophe.md (spec 1.
|
|
12
|
+
# spec/rules/apostrophe.md (spec 1.5.0), order 50.
|
|
13
13
|
#
|
|
14
14
|
# Converts a straight U+0027 to U+2019 where it is genuinely an apostrophe: a contraction,
|
|
15
15
|
# an elision, a possessive, or a decade elision. Runs immediately after `quotes` (order 40)
|
|
@@ -51,6 +51,14 @@ module Polytypo
|
|
|
51
51
|
# without its brackets, its dashes and Engine::MARKER. Read only by case 3a.
|
|
52
52
|
OPENQUOTE = [0xAB, 0x2018, 0x201A, 0x201B, 0x201C, 0x201E, 0x201F, 0x2039].freeze
|
|
53
53
|
|
|
54
|
+
# CLOSEDELIM is apostrophe.md 3.1 CLOSEDELIM (spec 1.5.0): the bracket and quotation
|
|
55
|
+
# members of CLOSEISH, without its sentence punctuation and without the dashes OPENISH
|
|
56
|
+
# already carries. These are exactly the closing delimiters case 3 has always accepted on
|
|
57
|
+
# the mark's RIGHT; before 1.5.0 no left-hand test accepted any of them. Engine::MARKER is
|
|
58
|
+
# not a member (modes.md 3.3): it is in OPENISH, so a mark against a span boundary already
|
|
59
|
+
# reaches case 4 and emits the same U+2019. Read only by case 2a.
|
|
60
|
+
CLOSEDELIM = [0x29, 0x5D, 0x7D, 0xBB, 0x2019, 0x201D, 0x203A].freeze
|
|
61
|
+
|
|
54
62
|
BREAK = [0x0A, 0x0D, 0x0B, 0x0C, 0x85, 0x2028, 0x2029, Engine::LINE_MARKER].freeze
|
|
55
63
|
|
|
56
64
|
# SPACELIKE, including Engine::LINE_MARKER as a member of BREAK for every rule everywhere
|
|
@@ -78,6 +86,11 @@ module Polytypo
|
|
|
78
86
|
# Case 2 -- medial apostrophe: `don't`, `l'été`, `O'Brien`, `1990's`.
|
|
79
87
|
return true if alnum?(left) && alnum?(right)
|
|
80
88
|
|
|
89
|
+
# Case 2a (spec 1.5.0) -- suffix or possessive after a closing delimiter:
|
|
90
|
+
# `(order 90)'s`, `“Hamlet”'s`, `{user}'s`. Disjoint from every other case, so its
|
|
91
|
+
# position in the ladder carries no behaviour.
|
|
92
|
+
return true if CLOSEDELIM.include?(left) && alnum?(right)
|
|
93
|
+
|
|
81
94
|
# Case 3 -- trailing elision or possessive: `the dogs' bowls`, `Jesus'`, `rock 'n'` (the
|
|
82
95
|
# trailing mark).
|
|
83
96
|
if left != Engine::NONE && UnicodeUtil.letter?(left) &&
|
data/lib/polytypo/version.rb
CHANGED