polytypo 1.0.1 → 1.1.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/de-CH.json +3 -3
- data/lib/polytypo/data/fixtures/de-DE.json +3 -3
- data/lib/polytypo/data/fixtures/el.json +7 -7
- data/lib/polytypo/data/fixtures/en-GB.json +5 -5
- data/lib/polytypo/data/fixtures/en-US.json +37 -37
- data/lib/polytypo/data/fixtures/fi.json +11 -3
- data/lib/polytypo/data/fixtures/fr-CA.json +5 -5
- data/lib/polytypo/data/fixtures/fr.json +5 -5
- data/lib/polytypo/data/fixtures/locale-resolution.json +1 -1
- data/lib/polytypo/data/fixtures/ru.json +13 -5
- data/lib/polytypo/data/fixtures/sv.json +3 -3
- data/lib/polytypo/data/locales/registry.json +1 -1
- data/lib/polytypo/data/rules/order.json +4 -4
- data/lib/polytypo/engine/rules/apostrophe.rb +5 -12
- data/lib/polytypo/engine/rules/quote_ambiguity.rb +22 -39
- 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: 3d928b45252e84a40709cad7cf3f07865ba9bb0cd2836b7d50a926be8c808f78
|
|
4
|
+
data.tar.gz: a7f438eba68f12e06f3f428be9d253ba5558d8e12ebf17d4e1998a81972c1cd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3844b1bf0a840dd0321160d4f96f04057fbb9a09448242874785f8f030486f3c86fde4859812e25494ee6e4454cebc21944ace0fe7bfaa55802ad1bd46bce0a7
|
|
7
|
+
data.tar.gz: a59e7a77a2cd1deef32f5a57d44ad111e56d5b7120c66c2d8c0c69836b08fc167bf33f3fd31b9a0936f4875a09408524747695e989320d8c333163c0fcb40076
|
data/lib/polytypo/data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.1.0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "de-CH",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -494,8 +494,8 @@
|
|
|
494
494
|
"rule": "quotes",
|
|
495
495
|
"mode": "text",
|
|
496
496
|
"in": "rock 'n' roll",
|
|
497
|
-
"out": "rock
|
|
498
|
-
"note": "spec
|
|
497
|
+
"out": "rock ’n’ roll",
|
|
498
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs."
|
|
499
499
|
}
|
|
500
500
|
]
|
|
501
501
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "de-DE",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -540,8 +540,8 @@
|
|
|
540
540
|
"rule": "quotes",
|
|
541
541
|
"mode": "text",
|
|
542
542
|
"in": "rock 'n' roll",
|
|
543
|
-
"out": "rock
|
|
544
|
-
"note": "spec
|
|
543
|
+
"out": "rock ’n’ roll",
|
|
544
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs."
|
|
545
545
|
}
|
|
546
546
|
]
|
|
547
547
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "el",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
"rule": "quotes",
|
|
134
134
|
"mode": "text",
|
|
135
135
|
"in": "\"Είπε 'όχι' σε μένα\", σημείωσε.",
|
|
136
|
-
"out": "«Είπε
|
|
137
|
-
"note": "quotes.md §6: one stack, inner pair at depth 2 takes the secondary pair U+201C/U+201D. Greek combines guillemets with curly double quotes, which no other v1 locale does. spec 0.5.0
|
|
136
|
+
"out": "«Είπε “όχι” σε μένα», σημείωσε.",
|
|
137
|
+
"note": "quotes.md §6: one stack, inner pair at depth 2 takes the secondary pair U+201C/U+201D. Greek combines guillemets with curly double quotes, which no other v1 locale does. spec 1.1.0 restores the nesting this case was written for: 0.5.0's general ambiguous-medial-span veto had preserved the inner 'όχι' as literal U+0027 because it was 3 letters, space-flanked; the veto that replaced it matches only a medial n (quotes.md §3.2), so an ordinary short quotation pairs again."
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"id": "el-quotes-already-guillemets",
|
|
@@ -205,8 +205,8 @@
|
|
|
205
205
|
"rule": "quotes",
|
|
206
206
|
"mode": "text",
|
|
207
207
|
"in": "\"Είπε 'όχι' σε μένα\", σημείωσε.",
|
|
208
|
-
"out": "«Είπε
|
|
209
|
-
"note": "quotes.md §6 row 16: width drift with no gate intervention — el's secondary pair is WIDE (“ ”), so the inner NARROW pair drifts to WIDE on render and all four marks land on one stack in the gate's re-scan; the certification gate certifies without declining anything, evidence the gate's cost is near zero for the common case. spec
|
|
208
|
+
"out": "«Είπε “όχι” σε μένα», σημείωσε.",
|
|
209
|
+
"note": "quotes.md §6 row 16: width drift with no gate intervention — el's secondary pair is WIDE (“ ”), so the inner NARROW pair drifts to WIDE on render and all four marks land on one stack in the gate's re-scan; the certification gate certifies without declining anything, evidence the gate's cost is near zero for the common case. spec 1.1.0: same as el-quotes-nested — the width-drift path this case exists to exercise is reachable again now that a 3-letter medial span is no longer vetoed (quotes.md §3.2). Under 0.5.0 the inner marks never paired, so nothing drifted and the case proved nothing about the gate."
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
212
|
"id": "el-quotes-030-adversarial-interleaved",
|
|
@@ -221,8 +221,8 @@
|
|
|
221
221
|
"rule": "quotes",
|
|
222
222
|
"mode": "text",
|
|
223
223
|
"in": "rock 'n' roll",
|
|
224
|
-
"out": "rock
|
|
225
|
-
"note": "spec 0.
|
|
224
|
+
"out": "rock ’n’ roll",
|
|
225
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs."
|
|
226
226
|
},
|
|
227
227
|
{
|
|
228
228
|
"id": "el-ranges-none",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "en-GB",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -632,8 +632,8 @@
|
|
|
632
632
|
"rule": "quotes",
|
|
633
633
|
"mode": "text",
|
|
634
634
|
"in": "\"He said 'no' to me,\" she noted.",
|
|
635
|
-
"out": "‘He said
|
|
636
|
-
"note": "quotes.md §6 case 2, inverted: depth 1 → single, depth 2 → double. Compare en-
|
|
635
|
+
"out": "‘He said “no” to me,’ she noted.",
|
|
636
|
+
"note": "quotes.md §6 case 2, inverted: depth 1 → single, depth 2 → double. Compare en-us-quotes-nested, same input. spec 1.1.0: the inner 'no' pairs as a nested quotation again — 0.5.0's general ambiguous-medial-span veto had preserved it as literal U+0027, and the universal medial-n veto that replaced it does not match a two-letter span (quotes.md §3.2)."
|
|
637
637
|
},
|
|
638
638
|
{
|
|
639
639
|
"id": "en-gb-quotes-nested-inverted-input",
|
|
@@ -712,8 +712,8 @@
|
|
|
712
712
|
"rule": "quotes",
|
|
713
713
|
"mode": "text",
|
|
714
714
|
"in": "rock 'n' roll",
|
|
715
|
-
"out": "rock
|
|
716
|
-
"note": "quotes.md §
|
|
715
|
+
"out": "rock ’n’ roll",
|
|
716
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs. en-GB is the locale quotes.md §7 item 8 flags as carrying the highest genuine-dialogue collision risk, because its primary pair is itself the single quote — that risk is why no elisionIdioms entry was ever cited here, and 1.1.0's veto deliberately does not depend on one."
|
|
717
717
|
},
|
|
718
718
|
{
|
|
719
719
|
"id": "en-gb-quotes-elision-inside-quotation",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "en-US",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -755,8 +755,8 @@
|
|
|
755
755
|
"rule": "quotes",
|
|
756
756
|
"mode": "text",
|
|
757
757
|
"in": "\"He said 'no' to me,\" she noted.",
|
|
758
|
-
"out": "“He said
|
|
759
|
-
"note": "quotes.md §6 case 2: one stack, inner pair at depth 2 → secondary. spec
|
|
758
|
+
"out": "“He said ‘no’ to me,” she noted.",
|
|
759
|
+
"note": "quotes.md §6 case 2: one stack, inner pair at depth 2 → secondary. spec 1.1.0 restores the nesting this case was written for: 0.5.0's general ambiguous-medial-span veto had preserved the inner 'no' as literal U+0027 because it was 2 letters and space-flanked, and the veto that replaced it matches only a medial n (quotes.md §3.2), so an ordinary short quotation pairs again. Compare en-gb-quotes-nested, same input, inverted glyphs."
|
|
760
760
|
},
|
|
761
761
|
{
|
|
762
762
|
"id": "en-us-quotes-nested-inverted-input",
|
|
@@ -836,7 +836,7 @@
|
|
|
836
836
|
"mode": "text",
|
|
837
837
|
"in": "rock 'n' roll",
|
|
838
838
|
"out": "rock ’n’ roll",
|
|
839
|
-
"note": "quotes.md §3.2's listed elision veto (spec 0.4.0): quotes.elisionIdioms = [{ left: \"rock\", elided: \"n\", right: \"roll\" }] (en-US.json sources: Chicago Manual of Style Online on the mark's function, American Heritage Dictionary on the spaced variant) makes both marks decline pairing when the full left/elided/right context matches, so they survive to apostrophe (order 50), which independently renders each as U+2019 — leading elision (case 4) then trailing elision (case 3). apostrophe.md §6 row 11a.
|
|
839
|
+
"note": "quotes.md §3.2's listed elision veto (spec 0.4.0): quotes.elisionIdioms = [{ left: \"rock\", elided: \"n\", right: \"roll\" }] (en-US.json sources: Chicago Manual of Style Online on the mark's function, American Heritage Dictionary on the spaced variant) makes both marks decline pairing when the full left/elided/right context matches, so they survive to apostrophe (order 50), which independently renders each as U+2019 — leading elision (case 4) then trailing elision (case 3). apostrophe.md §6 row 11a. This output has been constant since 0.4.0 and is the model every other locale was brought into line with in spec 1.1.0, whose universal medial-n veto (quotes.md §3.2) now vetoes a superset of these positions — see ru-quotes-rock-n-roll for the same output reached without a citation. Matching elided content alone (a bare word list) was tried and withdrawn in 0.4.0; 1.1.0 readmits that cost knowingly and only for a single code point — see en-us-quotes-letter-n-elided."
|
|
840
840
|
},
|
|
841
841
|
{
|
|
842
842
|
"id": "en-us-quotes-elision-inside-quotation",
|
|
@@ -844,7 +844,7 @@
|
|
|
844
844
|
"mode": "text",
|
|
845
845
|
"in": "She said \"it's the '90s\" and left.",
|
|
846
846
|
"out": "She said “it’s the ’90s” and left.",
|
|
847
|
-
"note": "quotes.md §3.3 and §5.5: pass 2 keeps TWO STACKS, one per kind, and a candidate only ever touches the stack for its own kind — no reader opens with `\"` and closes with `'`, in any language. So the two DQ marks pair with each other, and the SQ elision marks find no SQ partner, survive unmatched, and `apostrophe` renders them U+2019. A PORT WITH ONE SHARED STACK PRODUCES `She said \"it’s the “90s” and left.` — the `'` of `'90s` is canClose, it pops the opening `\"`, and the real closing `\"` is stranded. That is damage rather than a miss (§5.5 calls it a release blocker). Contrast the `rock 'n' roll` case, which is a MEDIAL elision between two SQ marks, not this rule's leading/trailing single-mark elision shape — see en-us-quotes-rock-n-roll for how spec
|
|
847
|
+
"note": "quotes.md §3.3 and §5.5: pass 2 keeps TWO STACKS, one per kind, and a candidate only ever touches the stack for its own kind — no reader opens with `\"` and closes with `'`, in any language. So the two DQ marks pair with each other, and the SQ elision marks find no SQ partner, survive unmatched, and `apostrophe` renders them U+2019. A PORT WITH ONE SHARED STACK PRODUCES `She said \"it’s the “90s” and left.` — the `'` of `'90s` is canClose, it pops the opening `\"`, and the real closing `\"` is stranded. That is damage rather than a miss (§5.5 calls it a release blocker). Contrast the `rock 'n' roll` case, which is a MEDIAL elision between two SQ marks, not this rule's leading/trailing single-mark elision shape — see en-us-quotes-rock-n-roll for how spec 1.1.0's universal medial-n veto handles that different shape. History: before the per-kind stack split landed (pre-0.3.0), this exact case's canonical fixture asserted the single-shared-stack damage shown above (the stranded-quote output) as its expected `out` — a previous implementation defect, not a documented limitation; the two-stack design fixed it, and the `out` actually asserted above (the correct, two-stack form) has been correct ever since."
|
|
848
848
|
},
|
|
849
849
|
{
|
|
850
850
|
"id": "en-us-quotes-adjacent-to-punctuation",
|
|
@@ -1653,47 +1653,47 @@
|
|
|
1653
1653
|
"mode": "text",
|
|
1654
1654
|
"in": "rock ’n’ roll",
|
|
1655
1655
|
"out": "rock ’n’ roll",
|
|
1656
|
-
"note": "quotes.md §3.2/§5
|
|
1656
|
+
"note": "quotes.md §3.2/§5: U+2019 is NARROW, so the veto fires identically on already-correct input — a fixed point. This is the witness for spec 1.1.0's widening of the predicate from straight ASCII to the whole NARROW class, which is an idempotency obligation rather than a preference: the veto now lets apostrophe CONVERT both marks, so a straight-ASCII-only predicate would not recognise its own output and pass 2 would pair rock ’n’ roll as an ordinary NARROW quotation on the next pipeline run. See ru-quotes-rock-n-roll-already-curly and fi-quotes-rock-n-roll-already-curly, which fail loudly under a straight-only predicate where this en-US case would not."
|
|
1657
1657
|
},
|
|
1658
1658
|
{
|
|
1659
|
-
"id": "en-us-quotes-letter-n-
|
|
1659
|
+
"id": "en-us-quotes-letter-n-elided",
|
|
1660
1660
|
"rule": "quotes",
|
|
1661
1661
|
"mode": "text",
|
|
1662
1662
|
"in": "The letter 'n' is common.",
|
|
1663
|
-
"out": "The letter
|
|
1664
|
-
"note": "quotes.md §3.2: no
|
|
1663
|
+
"out": "The letter ’n’ is common.",
|
|
1664
|
+
"note": "quotes.md §3.2, §6 row N1: no left/right context matches (\"letter\" is not \"rock\"), so the listed-idiom veto does not fire — but the universal medial-n veto does, and apostrophe converts both marks. **This is spec 1.1.0's accepted false positive**: a genuine quotation of the letter n comes out as an elision. It is pinned precisely because it is the input that sank the context-free `elisionForms` design in 0.4.0; 1.1.0 accepts it deliberately, against the measured alternative recorded in quotes.md §7 item 8 — spec 0.5.0 avoided this one case by declining every short quotation in every locale, a far larger error class. Through spec 0.5.0 this case asserted the unconverted input and was named en-us-quotes-letter-n-not-elided."
|
|
1665
1665
|
},
|
|
1666
1666
|
{
|
|
1667
|
-
"id": "en-us-quotes-nested-quote-n-
|
|
1667
|
+
"id": "en-us-quotes-nested-quote-n-elided",
|
|
1668
1668
|
"rule": "quotes",
|
|
1669
1669
|
"mode": "text",
|
|
1670
1670
|
"in": "He said \"press 'n' now\".",
|
|
1671
|
-
"out": "He said “press
|
|
1672
|
-
"note": "quotes.md §3.2: same
|
|
1671
|
+
"out": "He said “press ’n’ now”.",
|
|
1672
|
+
"note": "quotes.md §3.2, §6 row N2: the same shape as en-us-quotes-letter-n-elided, one level deeper inside a resolved outer quotation. §3.2's predicate reads a mark's immediate neighbours and knows nothing of enclosing pairs, so nesting depth never reaches it and the inner marks are converted while the outer pair resolves normally. The second half of spec 1.1.0's accepted false positive, pinned for the same reason. Through spec 0.5.0 this case asserted the unconverted inner marks and was named en-us-quotes-nested-quote-n-not-elided."
|
|
1673
1673
|
},
|
|
1674
1674
|
{
|
|
1675
|
-
"id": "en-us-quotes-rock-n-pop-
|
|
1675
|
+
"id": "en-us-quotes-rock-n-pop-elided",
|
|
1676
1676
|
"rule": "quotes",
|
|
1677
1677
|
"mode": "text",
|
|
1678
1678
|
"in": "rock 'n' pop",
|
|
1679
|
-
"out": "rock
|
|
1680
|
-
"note": "quotes.md §3.2, §6 row N3: `right` is \"pop\", not \"roll\"
|
|
1679
|
+
"out": "rock ’n’ pop",
|
|
1680
|
+
"note": "quotes.md §3.2, §6 row N3: `right` is \"pop\", not \"roll\", so no configured idiom matches — and since spec 1.1.0 none is needed. The universal medial-n veto fires on the bare medial n and apostrophe converts both marks. Named en-us-quotes-rock-n-pop-not-elided through spec 0.5.0, when the same input was preserved unconverted."
|
|
1681
1681
|
},
|
|
1682
1682
|
{
|
|
1683
|
-
"id": "en-us-quotes-fish-n-chips-
|
|
1683
|
+
"id": "en-us-quotes-fish-n-chips-elided",
|
|
1684
1684
|
"rule": "quotes",
|
|
1685
1685
|
"mode": "text",
|
|
1686
1686
|
"in": "fish 'n' chips",
|
|
1687
|
-
"out": "fish
|
|
1688
|
-
"note": "quotes.md §3.2, §6 row N4:
|
|
1687
|
+
"out": "fish ’n’ chips",
|
|
1688
|
+
"note": "quotes.md §3.2, §6 row N4: en-US.json's AHD citation attests only rock/n/roll, and no { left: \"fish\", elided: \"n\", right: \"chips\" } entry exists — the universal medial-n veto covers it anyway, which is the practical reason spec 1.1.0 stopped requiring a citation per phrase: the long tail of X 'n' Y borrowings cannot be enumerated one citation at a time. Named en-us-quotes-fish-n-chips-not-elided through spec 0.5.0."
|
|
1689
1689
|
},
|
|
1690
1690
|
{
|
|
1691
|
-
"id": "en-us-quotes-rock-cap-n-roll-
|
|
1691
|
+
"id": "en-us-quotes-rock-cap-n-roll-elided",
|
|
1692
1692
|
"rule": "quotes",
|
|
1693
1693
|
"mode": "text",
|
|
1694
1694
|
"in": "rock 'N' roll",
|
|
1695
|
-
"out": "rock
|
|
1696
|
-
"note": "quotes.md §3.2, §6 row N5: `elided` is matched exactly, with no case leniency at all — the first-code-point leniency applies only to
|
|
1695
|
+
"out": "rock ’N’ roll",
|
|
1696
|
+
"note": "quotes.md §3.2, §6 row N5: `elided` is matched exactly by the LISTED mechanism, with no case leniency at all — the first-code-point leniency applies only to left/right — so `N` ≠ `n` still fails the cited en-US idiom. The universal medial-n veto matches U+004E as well as U+006E and converts both marks. This fixture is the witness that idiom-matching stays case-sensitive even though the veto covering the same bytes is not. Named en-us-quotes-rock-cap-n-roll-not-elided through spec 0.5.0."
|
|
1697
1697
|
},
|
|
1698
1698
|
{
|
|
1699
1699
|
"id": "en-us-quotes-rock-n-roll-html",
|
|
@@ -1708,16 +1708,16 @@
|
|
|
1708
1708
|
"rule": "quotes",
|
|
1709
1709
|
"mode": "html",
|
|
1710
1710
|
"in": "<p><em>rock</em> 'n' roll</p>",
|
|
1711
|
-
"out": "<p><em>rock</em>
|
|
1712
|
-
"note": "quotes.md §3.2, §6 row H1: modes.md §3.2's boundary marker is not INLINE-SPACE, so `left`'s word is unreachable across the <em> span boundary and the
|
|
1711
|
+
"out": "<p><em>rock</em> ’n’ roll</p>",
|
|
1712
|
+
"note": "quotes.md §3.2, §6 row H1: modes.md §3.2's boundary marker is not INLINE-SPACE, so `left`'s word is unreachable across the <em> span boundary and the LISTED idiom does not match. The universal medial-n veto (spec 1.1.0) reads no context word at all — only the enclosed code point and the single space immediately outside each mark, both of which sit in the same prose span as the marks — so it fires here and apostrophe converts both marks, converging with the unsplit en-us-quotes-rock-n-roll-html. Through spec 0.5.0 this case asserted the unconverted input."
|
|
1713
1713
|
},
|
|
1714
1714
|
{
|
|
1715
1715
|
"id": "en-us-quotes-rock-n-roll-html-right-split",
|
|
1716
1716
|
"rule": "quotes",
|
|
1717
1717
|
"mode": "html",
|
|
1718
1718
|
"in": "<p>rock 'n' <em>roll</em></p>",
|
|
1719
|
-
"out": "<p>rock
|
|
1720
|
-
"note": "quotes.md §3.2, §6 row H2: mirror
|
|
1719
|
+
"out": "<p>rock ’n’ <em>roll</em></p>",
|
|
1720
|
+
"note": "quotes.md §3.2, §6 row H2: the mirror of en-us-quotes-rock-n-roll-html-left-split, with `right`'s word inside the following <em> span. modes.md §3.2's boundary marker is not INLINE-SPACE, so `right` is unreachable and the LISTED idiom does not match. The universal medial-n veto (spec 1.1.0) reads no context word at all — only the enclosed code point and the single space immediately outside each mark, both in the same prose span as the marks — so it fires and apostrophe converts both, converging with the unsplit en-us-quotes-rock-n-roll-html. Through spec 0.5.0 this case asserted the unconverted input."
|
|
1721
1721
|
},
|
|
1722
1722
|
{
|
|
1723
1723
|
"id": "en-us-quotes-rock-n-roll-markdown-left-split",
|
|
@@ -1725,32 +1725,32 @@
|
|
|
1725
1725
|
"mode": "markdown",
|
|
1726
1726
|
"dialect": "commonmark",
|
|
1727
1727
|
"in": "*rock* 'n' roll\n",
|
|
1728
|
-
"out": "*rock*
|
|
1729
|
-
"note": "quotes.md §3.2, §6 row H3: `left`'s word sits inside an emphasis span
|
|
1728
|
+
"out": "*rock* ’n’ roll\n",
|
|
1729
|
+
"note": "quotes.md §3.2, §6 row H3: `left`'s word sits inside an emphasis span, so the LISTED idiom's word-boundary test cannot reach it across the span marker (modes.md §3.2) and no idiom matches. The universal medial-n veto (spec 1.1.0) needs no context word — the mark's own immediately-preceding INLINE-SPACE is in the same span as the mark — so it fires and apostrophe converts both marks, matching text mode on the same characters split the same way. Through spec 0.5.0 this case asserted the unconverted input."
|
|
1730
1730
|
},
|
|
1731
1731
|
{
|
|
1732
1732
|
"id": "en-us-quotes-ambiguous-single-letter",
|
|
1733
1733
|
"rule": "quotes",
|
|
1734
1734
|
"mode": "text",
|
|
1735
1735
|
"in": "She chose 'A' today",
|
|
1736
|
-
"out": "She chose
|
|
1737
|
-
"note": "spec
|
|
1736
|
+
"out": "She chose “A” today",
|
|
1737
|
+
"note": "spec 1.1.0, quotes.md §3.2: 'A' is a single quoted letter and pairs as an ordinary quotation. Under spec 0.5.0 the general ambiguous-medial-span veto preserved it as literal U+0027, because its shape test admitted any 1 to 3 LETTER code points and could not tell this from rock 'n' roll. This fixture flipped role in 1.1.0: it pinned that veto's reach, and now pins that the replacement veto does NOT reach a span whose enclosed code point is not n or N."
|
|
1738
1738
|
},
|
|
1739
1739
|
{
|
|
1740
1740
|
"id": "en-us-quotes-ambiguous-two-letter",
|
|
1741
1741
|
"rule": "quotes",
|
|
1742
1742
|
"mode": "text",
|
|
1743
1743
|
"in": "They said 'no' yesterday",
|
|
1744
|
-
"out": "They said
|
|
1745
|
-
"note": "spec
|
|
1744
|
+
"out": "They said “no” yesterday",
|
|
1745
|
+
"note": "spec 1.1.0, quotes.md §3.2: 2 letters enclosed — not a medial n, so the universal veto does not fire and the span pairs as an ordinary quotation. Same reasoning and same role reversal as en-us-quotes-ambiguous-single-letter."
|
|
1746
1746
|
},
|
|
1747
1747
|
{
|
|
1748
1748
|
"id": "en-us-quotes-ambiguous-three-letter",
|
|
1749
1749
|
"rule": "quotes",
|
|
1750
1750
|
"mode": "text",
|
|
1751
1751
|
"in": "say 'yes' now",
|
|
1752
|
-
"out": "say
|
|
1753
|
-
"note": "spec
|
|
1752
|
+
"out": "say “yes” now",
|
|
1753
|
+
"note": "spec 1.1.0, quotes.md §3.2: 3 letters enclosed — the upper bound of spec 0.5.0's withdrawn shape test, kept as a fixture precisely because it was that test's boundary case. The universal medial-n veto does not fire, and the span pairs as an ordinary quotation."
|
|
1754
1754
|
},
|
|
1755
1755
|
{
|
|
1756
1756
|
"id": "en-us-quotes-ambiguous-four-letter-not-caught",
|
|
@@ -1765,8 +1765,8 @@
|
|
|
1765
1765
|
"rule": "quotes",
|
|
1766
1766
|
"mode": "text",
|
|
1767
1767
|
"in": "say '𐐀' now",
|
|
1768
|
-
"out": "say
|
|
1769
|
-
"note": "spec 0.
|
|
1768
|
+
"out": "say “𐐀” now",
|
|
1769
|
+
"note": "spec 1.1.0, quotes.md §3.2: a single astral-plane LETTER (U+10400, two UTF-16 units, one code point). It pairs as an ordinary quotation because it is not n or N — the veto compares code points, so an astral letter is neither specially admitted nor specially excluded. Retained from spec 0.5.0, where it pinned that the withdrawn shape test counted code points rather than UTF-16 units; the code-point discipline it tests (ARCHITECTURE.md §4.2) is unchanged."
|
|
1770
1770
|
},
|
|
1771
1771
|
{
|
|
1772
1772
|
"id": "en-us-quotes-ambiguous-digit-not-letter",
|
|
@@ -1774,7 +1774,7 @@
|
|
|
1774
1774
|
"mode": "text",
|
|
1775
1775
|
"in": "say '12' now",
|
|
1776
1776
|
"out": "say “12” now",
|
|
1777
|
-
"note": "spec 0.
|
|
1777
|
+
"note": "spec 1.1.0, quotes.md §3.2: the enclosed code points are digits, not n or N, so the veto does not fire and the span pairs as an ordinary quotation. This output has been constant across 0.5.0 and 1.1.0 — under 0.5.0 because digits were not LETTER, now because they are not the one code point the veto matches."
|
|
1778
1778
|
},
|
|
1779
1779
|
{
|
|
1780
1780
|
"id": "en-us-quotes-ambiguous-punctuation-not-letter",
|
|
@@ -1790,7 +1790,7 @@
|
|
|
1790
1790
|
"mode": "text",
|
|
1791
1791
|
"in": "She chose ’A’ today",
|
|
1792
1792
|
"out": "She chose “A” today",
|
|
1793
|
-
"note": "spec 0.
|
|
1793
|
+
"note": "spec 1.1.0, quotes.md §3.2: 'A' is not n or N, so the veto does not fire and the already-curly pair is re-typeset as an ordinary quotation. **The reason this output is unchanged is not the reason it was unchanged in 0.5.0.** 0.5.0's veto matched straight ASCII U+0027 only, so a curly pair was out of scope by construction; 1.1.0's matches the whole NARROW class — an idempotency obligation, since the veto now produces U+2019 — and declines this span on its enclosed code point instead. See en-us-quotes-rock-n-roll-already-curly for a curly pair the veto does match."
|
|
1794
1794
|
},
|
|
1795
1795
|
{
|
|
1796
1796
|
"id": "en-us-ranges-decimal-and-path-rejected",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "fi",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -752,8 +752,16 @@
|
|
|
752
752
|
"rule": "quotes",
|
|
753
753
|
"mode": "text",
|
|
754
754
|
"in": "rock 'n' roll",
|
|
755
|
-
"out": "rock
|
|
756
|
-
"note": "quotes.md §
|
|
755
|
+
"out": "rock ’n’ roll",
|
|
756
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs. fi's own dictionary writes this loanword closed up (rock'n'roll), so a citation for the spaced form would have been evidenced-inert and none was ever added — quotes.md §7 item 8. The universal veto covers the spaced form regardless, which is the point of moving the mechanism out of locale data. See fi-quotes-rock-n-roll-already-curly for the second-pass fixed point this output depends on."
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"id": "fi-quotes-rock-n-roll-already-curly",
|
|
760
|
+
"rule": "quotes",
|
|
761
|
+
"mode": "text",
|
|
762
|
+
"in": "rock ’n’ roll",
|
|
763
|
+
"out": "rock ’n’ roll",
|
|
764
|
+
"note": "quotes.md §3.2, the sharpest regression witness for spec 1.1.0's widening of the veto to the whole NARROW class: fi's SECONDARY pair is U+2019 on both sides, so the marks this rule's own output leaves behind are indistinguishable from fi's own prescribed nested-quotation glyphs. A port that matches U+0027 only does not recognise them, pairs them, and turns the output of fi-quotes-rock-n-roll into `rock ”n” roll` — fi's primary pair at depth 1. Companion to ru-quotes-rock-n-roll-already-curly, which fails the same way in a locale whose primary is WIDE; both are measured, not hypothesised."
|
|
757
765
|
},
|
|
758
766
|
{
|
|
759
767
|
"id": "fi-quotes-already-correct",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "fr-CA",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"rule": "quotes",
|
|
81
81
|
"mode": "text",
|
|
82
82
|
"in": "\"Il a dit 'oui' hier.\"",
|
|
83
|
-
"out": "« Il a dit
|
|
84
|
-
"note": "spec
|
|
83
|
+
"out": "« Il a dit “oui” hier. »",
|
|
84
|
+
"note": "spec 1.1.0: same as fr-quotes-nested — the inner 'oui' pairs as a nested quotation again now that the veto matches only a medial n (quotes.md §3.2), where 0.5.0's shape test preserved every 1-to-3-letter span."
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"id": "fr-ca-apostrophe-elision",
|
|
@@ -250,8 +250,8 @@
|
|
|
250
250
|
"rule": "quotes",
|
|
251
251
|
"mode": "text",
|
|
252
252
|
"in": "rock 'n' roll",
|
|
253
|
-
"out": "rock
|
|
254
|
-
"note": "spec
|
|
253
|
+
"out": "rock ’n’ roll",
|
|
254
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs."
|
|
255
255
|
},
|
|
256
256
|
{
|
|
257
257
|
"id": "fr-ca-ranges-none",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "fr",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -235,8 +235,8 @@
|
|
|
235
235
|
"rule": "quotes",
|
|
236
236
|
"mode": "text",
|
|
237
237
|
"in": "\"Il a dit 'oui' hier.\"",
|
|
238
|
-
"out": "« Il a dit
|
|
239
|
-
"note": "Depth 1 primary « », depth 2 secondary “ ” whose innerSpace is none. spec
|
|
238
|
+
"out": "« Il a dit “oui” hier. »",
|
|
239
|
+
"note": "Depth 1 primary « », depth 2 secondary “ ” whose innerSpace is none. spec 1.1.0: the inner 'oui' pairs again — 0.5.0's general ambiguous-medial-span veto had preserved it as literal U+0027 for being 3 letters and space-flanked, and the universal medial-n veto that replaced it matches only a single n (quotes.md §3.2)."
|
|
240
240
|
},
|
|
241
241
|
{
|
|
242
242
|
"id": "fr-quotes-unbalanced",
|
|
@@ -585,8 +585,8 @@
|
|
|
585
585
|
"rule": "quotes",
|
|
586
586
|
"mode": "text",
|
|
587
587
|
"in": "rock 'n' roll",
|
|
588
|
-
"out": "rock
|
|
589
|
-
"note": "spec 0.
|
|
588
|
+
"out": "rock ’n’ roll",
|
|
589
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs."
|
|
590
590
|
},
|
|
591
591
|
{
|
|
592
592
|
"id": "fr-ranges-none",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "ru",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -323,8 +323,8 @@
|
|
|
323
323
|
"rule": "quotes",
|
|
324
324
|
"mode": "text",
|
|
325
325
|
"in": "Он сказал: \"это 'моё' дело\".",
|
|
326
|
-
"out": "Он сказал: «это
|
|
327
|
-
"note": "quotes.md §6 case 14: depth 1 takes the ёлочки « », depth 2 the лапки „ “ — Лопатин 2006. Note the secondary pair's closing glyph is U+201C, which is in OPENISH; the class says nothing about the role. spec
|
|
326
|
+
"out": "Он сказал: «это „моё“ дело».",
|
|
327
|
+
"note": "quotes.md §6 case 14: depth 1 takes the ёлочки « », depth 2 the лапки „ “ — Лопатин 2006. Note the secondary pair's closing glyph is U+201C, which is in OPENISH; the class says nothing about the role. spec 1.1.0: the inner 'моё' pairs as a nested quotation again. Under 0.5.0 the general ambiguous-medial-span veto preserved it as literal U+0027 for being 3 LETTER code points and space-flanked — a count that also depended on the input's normalization form, since LETTER includes Mn and the decomposed ё is two code points. The universal medial-n veto that replaced it compares one code point against a fixed pair (quotes.md §3.2) and has neither problem."
|
|
328
328
|
},
|
|
329
329
|
{
|
|
330
330
|
"id": "ru-quotes-roundtrip",
|
|
@@ -681,8 +681,16 @@
|
|
|
681
681
|
"rule": "quotes",
|
|
682
682
|
"mode": "text",
|
|
683
683
|
"in": "rock 'n' roll",
|
|
684
|
-
"out": "rock
|
|
685
|
-
"note": "spec 0.
|
|
684
|
+
"out": "rock ’n’ roll",
|
|
685
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs. See ru-quotes-rock-n-roll-already-curly for the second-pass fixed point this output depends on."
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
"id": "ru-quotes-rock-n-roll-already-curly",
|
|
689
|
+
"rule": "quotes",
|
|
690
|
+
"mode": "text",
|
|
691
|
+
"in": "rock ’n’ roll",
|
|
692
|
+
"out": "rock ’n’ roll",
|
|
693
|
+
"note": "quotes.md §3.2, the regression witness for spec 1.1.0's widening of the veto from straight ASCII to the whole NARROW class. This is the output of ru-quotes-rock-n-roll fed back in, so it is that case's idempotency obligation made explicit — and it is the case a naive implementation gets wrong. A port that matches U+0027 only (as spec 0.5.0's withdrawn predicate did, correctly for its own preserve-then-stop outcome) does not recognise the U+2019 marks it just produced: the pair falls through to ordinary NARROW quotation and this input becomes `rock «n» roll`, ru's primary pair at depth 1. Measured, not hypothesised. `apostrophe` emits U+2019 only in place of U+0027 (apostrophe.md §1), so once the veto matches, nothing edits these marks and the form is stable."
|
|
686
694
|
}
|
|
687
695
|
]
|
|
688
696
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.0",
|
|
3
3
|
"locale": "sv",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -752,8 +752,8 @@
|
|
|
752
752
|
"rule": "quotes",
|
|
753
753
|
"mode": "text",
|
|
754
754
|
"in": "rock 'n' roll",
|
|
755
|
-
"out": "rock
|
|
756
|
-
"note": "quotes.md §
|
|
755
|
+
"out": "rock ’n’ roll",
|
|
756
|
+
"note": "spec 1.1.0, quotes.md §3.2's universal medial-n elision veto: by operator decision the idiom is international, so `quotes` declines the pairing in every locale rather than only where a cited quotes.elisionIdioms entry exists, and `apostrophe` (order 50) converts each mark by its own case ladder — leading elision (apostrophe.md §3.3 case 4), then trailing elision (case 3). Same output and same two case decisions as en-us-quotes-rock-n-roll, which reaches them through the cited idiom instead. History: spec 0.5.0's general ambiguous-medial-span veto preserved both marks here as literal U+0027; before 0.5.0 `quotes` paired them as an ordinary depth-1 quotation in this locale's primary glyphs. sv writes the loanword closed up in its own dictionaries, exactly as fi does, so no spaced-form citation exists here either — see fi-quotes-rock-n-roll."
|
|
757
757
|
},
|
|
758
758
|
{
|
|
759
759
|
"id": "sv-quotes-already-correct",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.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": ["en-US", "en-GB", "de-DE", "de-CH", "fr", "fr-CA", "ru", "fi", "sv", "el"],
|
|
5
5
|
"aliases": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.1.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
|
{
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"default": "on",
|
|
49
49
|
"modes": ["text", "html", "markdown"],
|
|
50
50
|
"localeData": ["quotes"],
|
|
51
|
-
"summary": "Straight quotes to locale primary/secondary pairs with nesting resolution. Runs before `apostrophe` so that a straight apostrophe is still available as quote evidence. Declines to pair
|
|
51
|
+
"summary": "Straight quotes to locale primary/secondary pairs with nesting resolution. Runs before `apostrophe` so that a straight apostrophe is still available as quote evidence. Declines to pair a medial single-quoted `n` (spec 1.1.0, every locale) or a span matching a cited `quotes.elisionIdioms` entry, so that `apostrophe` converts both marks instead."
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
"id": "apostrophe",
|
|
55
55
|
"order": 50,
|
|
56
56
|
"default": "on",
|
|
57
57
|
"modes": ["text", "html", "markdown"],
|
|
58
|
-
"localeData": [
|
|
59
|
-
"summary": "Remaining straight apostrophes to U+2019 without corrupting contractions.
|
|
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."
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"id": "symbols",
|
|
@@ -17,9 +17,10 @@ module Polytypo
|
|
|
17
17
|
# replacing one code point; the rule never inserts, never deletes, and never touches U+2019
|
|
18
18
|
# itself.
|
|
19
19
|
#
|
|
20
|
-
# As of spec
|
|
21
|
-
#
|
|
22
|
-
#
|
|
20
|
+
# As of spec 1.1.0 this rule reads no locale data and skips no position (apostrophe.md
|
|
21
|
+
# 3.4). Spec 0.5.0's preserve set existed to stop the case ladder from converting the marks
|
|
22
|
+
# quotes had vetoed; conversion is now the specified outcome for exactly those marks --
|
|
23
|
+
# cases 4 and 3 are what turn `rock 'n' roll` into `rock ’n’ roll`, in every locale.
|
|
23
24
|
module Apostrophe
|
|
24
25
|
SQ = 0x27
|
|
25
26
|
RIGHT_SINGLE = 0x2019
|
|
@@ -92,20 +93,12 @@ module Polytypo
|
|
|
92
93
|
false
|
|
93
94
|
end
|
|
94
95
|
|
|
95
|
-
def self.scan(cp,
|
|
96
|
+
def self.scan(cp, _locale_data, _ctx)
|
|
96
97
|
n = cp.length
|
|
97
|
-
# apostrophe.md 3.4, spec 0.5.0: positions in the shared ambiguous-medial-span preserve
|
|
98
|
-
# set (an ambiguous shape with no cited elisionIdioms match) are skipped entirely,
|
|
99
|
-
# before left/right are even read -- this rule's own case ladder would otherwise curl
|
|
100
|
-
# both marks of e.g. `rock 'n' roll` independently and silently defeat quotes'
|
|
101
|
-
# deliberate veto.
|
|
102
|
-
idioms = (locale_data["quotes"] || {})["elisionIdioms"] || []
|
|
103
|
-
preserve = QuoteAmbiguity.compute_preserve_indices(cp, idioms)
|
|
104
98
|
|
|
105
99
|
edits = []
|
|
106
100
|
(0...n).each do |i|
|
|
107
101
|
next unless cp[i] == SQ
|
|
108
|
-
next if preserve.key?(i)
|
|
109
102
|
|
|
110
103
|
left = QuoteAmbiguity.at(cp, i - 1)
|
|
111
104
|
right = QuoteAmbiguity.at(cp, i + 1)
|
|
@@ -26,14 +26,17 @@ module Polytypo
|
|
|
26
26
|
# the right one as a trailing possessive/elision, apostrophe.md 3.3 cases 3/4) must not
|
|
27
27
|
# convert them either.
|
|
28
28
|
module QuoteAmbiguity
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
29
|
+
# LOWER_N / UPPER_N -- quotes.md 3.2's one enclosed code point, in either case.
|
|
30
|
+
LOWER_N = 0x6E
|
|
31
|
+
UPPER_N = 0x4E
|
|
32
|
+
|
|
33
|
+
# NARROW -- quotes.md 3.1 NARROW -- every glyph an elision mark may appear as across
|
|
34
|
+
# pipeline passes (straight, or already curled by an earlier pass). Shared with quotes.rb
|
|
35
|
+
# so the two rules cannot define two slightly different NARROW sets. For the universal
|
|
36
|
+
# medial-n veto, matching the whole class is an IDEMPOTENCY obligation rather than a
|
|
37
|
+
# preference: its marks are converted to U+2019 by apostrophe, so a straight-ASCII-only
|
|
38
|
+
# predicate would not recognise its own output and pass 2 would pair `rock ’n’ roll`
|
|
39
|
+
# as an ordinary NARROW quotation on the next run.
|
|
37
40
|
NARROW = [0x27, 0x2018, 0x2019, 0x201A, 0x201B, 0x2039, 0x203A].freeze
|
|
38
41
|
|
|
39
42
|
# INLINE_SPACE -- quotes.md 3.1 INLINE-SPACE, deliberately excluding BREAK/MARKER/
|
|
@@ -41,9 +44,6 @@ module Polytypo
|
|
|
41
44
|
# same anchor the elisionIdioms matcher already uses.
|
|
42
45
|
INLINE_SPACE = [0x20, 0x09, 0xA0, 0x202F, 0x2007, 0x2009, 0x200A].freeze
|
|
43
46
|
|
|
44
|
-
MIN_ENCLOSED = 1
|
|
45
|
-
MAX_ENCLOSED = 3
|
|
46
|
-
|
|
47
47
|
def self.narrow?(cp)
|
|
48
48
|
NARROW.include?(cp)
|
|
49
49
|
end
|
|
@@ -187,28 +187,27 @@ module Polytypo
|
|
|
187
187
|
|
|
188
188
|
# compute_ambiguous_shape_indices is the general ambiguous-medial-span shape,
|
|
189
189
|
# locale-independent (quotes.md 3.2, spec 0.5.0): a pair of straight ASCII single quotes
|
|
190
|
-
# (
|
|
191
|
-
# point immediately outside each mark. Both
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
# short.
|
|
190
|
+
# (spec 1.1.0) a pair of NARROW marks enclosing exactly one code point, U+006E or
|
|
191
|
+
# U+004E, with at least one INLINE-SPACE code point immediately outside each mark. Both
|
|
192
|
+
# mark positions are returned for every match. A superset of
|
|
193
|
+
# compute_idiom_matched_indices's output for every idiom whose elided field is a single
|
|
194
|
+
# n (true of every idiom shipped so far), but computed independently rather than assumed,
|
|
195
|
+
# since a future idiom's elided field is not required to be that short.
|
|
196
196
|
def self.compute_ambiguous_shape_indices(cp)
|
|
197
197
|
ambiguous = {}
|
|
198
198
|
n = cp.length
|
|
199
199
|
|
|
200
200
|
(0...n).each do |i|
|
|
201
|
-
next unless at(cp, i)
|
|
201
|
+
next unless narrow?(at(cp, i))
|
|
202
202
|
|
|
203
203
|
l_lit = at(cp, i - 1)
|
|
204
204
|
next if l_lit == Engine::NONE || !inline_space?(l_lit)
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
next if k < MIN_ENCLOSED
|
|
206
|
+
enclosed = at(cp, i + 1)
|
|
207
|
+
next unless [LOWER_N, UPPER_N].include?(enclosed)
|
|
209
208
|
|
|
210
|
-
j = i +
|
|
211
|
-
next unless at(cp, j)
|
|
209
|
+
j = i + 2
|
|
210
|
+
next unless narrow?(at(cp, j))
|
|
212
211
|
|
|
213
212
|
r_lit = at(cp, j + 1)
|
|
214
213
|
next if r_lit == Engine::NONE || !inline_space?(r_lit)
|
|
@@ -219,22 +218,6 @@ module Polytypo
|
|
|
219
218
|
|
|
220
219
|
ambiguous
|
|
221
220
|
end
|
|
222
|
-
|
|
223
|
-
# compute_preserve_indices is the set of straight-ASCII-quote index positions
|
|
224
|
-
# apostrophe.md 3.4 requires `apostrophe` to skip byte-identically: ambiguous-shaped, but
|
|
225
|
-
# with no matching cited idiom. A position with a matching idiom is not in this set --
|
|
226
|
-
# apostrophe's ordinary case ladder still curls it, exactly as spec 0.4.0-0.4.1 did.
|
|
227
|
-
def self.compute_preserve_indices(cp, idioms)
|
|
228
|
-
ambiguous = compute_ambiguous_shape_indices(cp)
|
|
229
|
-
return ambiguous if ambiguous.empty?
|
|
230
|
-
|
|
231
|
-
idiom_matched = compute_idiom_matched_indices(cp, idioms)
|
|
232
|
-
return ambiguous if idiom_matched.empty?
|
|
233
|
-
|
|
234
|
-
ambiguous.each_with_object({}) do |(idx, _), preserve|
|
|
235
|
-
preserve[idx] = true unless idiom_matched.key?(idx)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
221
|
end
|
|
239
222
|
end
|
|
240
223
|
end
|
data/lib/polytypo/version.rb
CHANGED