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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a60e1eac7173d0034f4846b2142a5b4168d0af023653b8b301e868d9dda7138f
4
- data.tar.gz: f1911651b4d7f3014212b130e03e79bee1cb324aeee201e0e59d87bc5afedfc7
3
+ metadata.gz: 3d928b45252e84a40709cad7cf3f07865ba9bb0cd2836b7d50a926be8c808f78
4
+ data.tar.gz: a7f438eba68f12e06f3f428be9d253ba5558d8e12ebf17d4e1998a81972c1cd8
5
5
  SHA512:
6
- metadata.gz: 3150585226505f0c29d8244f4c82c109a17d83e1a51e9cbe9f29368d2a9deddeda41603e2248508cd0fa372fb1d6361b36ed0e9175877e1d24ca2f23e338868f
7
- data.tar.gz: 972340ac07969de576167bedf99918875727ae6877bbf55178b087b191c6bc1915b4d9378c01cd144e79d46b2e81d386a7fb71fa8c067f95cef91fde196a6e19
6
+ metadata.gz: 3844b1bf0a840dd0321160d4f96f04057fbb9a09448242874785f8f030486f3c86fde4859812e25494ee6e4454cebc21944ace0fe7bfaa55802ad1bd46bce0a7
7
+ data.tar.gz: a59e7a77a2cd1deef32f5a57d44ad111e56d5b7120c66c2d8c0c69836b08fc167bf33f3fd31b9a0936f4875a09408524747695e989320d8c333163c0fcb40076
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.0
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.0.0",
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 'n' roll",
498
- "note": "spec 0.5.0: de-CH has no cited quotes.elisionIdioms entry for this idiom (quotes.md §7 item 8 only en-US does, via Chicago Manual of Style Online and the American Heritage Dictionary). The general ambiguous-medial-span veto (quotes.md 3.2) preserves both marks as literal U+0027 rather than pairing them as an ordinary quotation, closing the false positive AUDIT_REMEDIATION_AND_RELEASE_PLAN.md 3.1 named for \"equivalent cases in several locales.\""
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.0.0",
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 'n' roll",
544
- "note": "spec 0.5.0: de-DE has no cited quotes.elisionIdioms entry for this idiom (quotes.md §7 item 8 only en-US does, via Chicago Manual of Style Online and the American Heritage Dictionary). The general ambiguous-medial-span veto (quotes.md 3.2) preserves both marks as literal U+0027 rather than pairing them as an ordinary quotation, closing the false positive AUDIT_REMEDIATION_AND_RELEASE_PLAN.md 3.1 named for \"equivalent cases in several locales.\""
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.0.0",
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: the inner 'όχι' (3 Greek letters, space-flanked) is preserved by the general ambiguous-medial-span veto el has no elisionIdioms entry for this shape."
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 0.5.0: same as el-quotes-nested — 'όχι' is preserved by the general ambiguous-medial-span veto rather than pairing as an ordinary same-width quotation."
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 'n' roll",
225
- "note": "spec 0.5.0: el has no cited quotes.elisionIdioms entry for this idiom (quotes.md §7 item 8 only en-US does, via Chicago Manual of Style Online and the American Heritage Dictionary). The general ambiguous-medial-span veto (quotes.md 3.2) preserves both marks as literal U+0027 rather than pairing them as an ordinary quotation, closing the false positive AUDIT_REMEDIATION_AND_RELEASE_PLAN.md 3.1 named for \"equivalent cases in several locales.\""
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.0.0",
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 '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. The inner 'no' (2 letters, space-flanked, no matching elisionIdioms entry en-GB has none) is preserved by the general ambiguous-medial-span veto (quotes.md §3.2) rather than paired as a nested quotation: a documented false negative, not an unresolved gap (History: this was open per quotes.md §7 item 8 through spec 0.4.1, closed by the general veto in spec 0.5.0)."
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 again0.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 'n' roll",
716
- "note": "quotes.md §7.1. The output is accidentally right here not because a secondary level was selected, but because en-GB declares the single pair as PRIMARY and the pair is at depth 1. spec 0.5.0: en-GB has no cited elisionIdioms entry for this idiom (quotes.md §7 item 8 elevated ambiguity risk, since en-GB's primary pair is itself the single quote). The general ambiguous-medial-span veto now preserves the marks as literal U+0027 instead of pairing them as an ordinary quotation, which is the pre-0.5.0 behaviour this fixture used to assert. AUDIT_REMEDIATION_AND_RELEASE_PLAN.md §3.1 named exactly this locale as an affected case."
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.0.0",
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 'no' to me,” she noted.",
759
- "note": "quotes.md §6 case 2: one stack, inner pair at depth 2 → secondary. spec 0.5.0: the inner 'no' is now an ambiguous medial span (2 letters, space-flanked, no matching en-US elisionIdioms entry the idiom list holds only rock/n/roll) and is preserved as literal U+0027 rather than paired as a nested quotation (quotes.md 3.2, general ambiguous-medial-span veto). A documented false negative, not a regression: quotes.md 3.2 explains why a structural, portable predicate cannot distinguish this from rock 'n' roll's shape."
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. Contrast en-GB/fi/sv, which have no citable elisionIdioms entry: the general ambiguous-medial-span veto (quotes.md §3.2) preserves the same bytes unconverted there instead — see en-gb-quotes-rock-n-roll. Matching elided content alone (a bare word list) was tried first and withdrawn: it falsely elided ordinary quotations of the letter n — see en-us-quotes-letter-n-not-elided and en-us-quotes-nested-quote-n-not-elided."
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 0.5.0's general ambiguous-medial-span 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."
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, §6 row P4: U+2019 is NARROW, so the veto fires identically on already-correct input — a fixed point. Discharges the idempotency argument's second-pipeline-pass case for this construction."
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-not-elided",
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 'n' is common.",
1664
- "note": "quotes.md §3.2: no `left`/`right` context matches (\"letter\" is not \"rock\"), so the listed-idiom veto does not fire — but 'n' (1 letter, space-flanked) is preserved by the general ambiguous-medial-span veto regardless (quotes.md §3.2), which does not require a citable idiom, only the structural shape. Pinned because this exact input was falsely elided by the first, context-free `elisionForms` design (spec 0.4.0, withdrawn before release) see quotes.md §7 item 8. Contrast en-us-quotes-rock-n-roll, where the full left/elided/right context does match a cited idiom and apostrophe independently converts both marks."
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-not-elided",
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 'n' now”.",
1672
- "note": "quotes.md §3.2: same reason as en-us-quotes-letter-n-not-elided (\"press\" is not \"rock\"), nested one level deeper inside a resolved outer quotation. The inner 'n' (1 letter, space-flanked) is preserved by the general ambiguous-medial-span veto — quotes.md §3.2's predicate is local to the mark's own immediate neighbours, not aware of enclosing pairs, so nesting depth is irrelevant to it. Also pinned as a regression witness for the withdrawn context-free `elisionForms` design (quotes.md §7 item 8)."
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-not-elided",
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 'n' pop",
1680
- "note": "quotes.md §3.2, §6 row N3: `right` is \"pop\", not \"roll\" no configured idiom matches. 'n' (1 letter, space-flanked) is preserved by the general ambiguous-medial-span veto instead (quotes.md §3.2), which does not require a citable idiom to fire, only the structural shape a documented false negative rather than a pairing."
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-not-elided",
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 'n' chips",
1688
- "note": "quotes.md §3.2, §6 row N4: not sourced or listed. en-US.json's AHD citation attests only rock/n/roll; no { left: \"fish\", elided: \"n\", right: \"chips\" } entry exists. 'n' is preserved by the general ambiguous-medial-span veto instead (quotes.md §3.2), the same mechanism as en-us-quotes-rock-n-pop-not-elided no idiom is required for it to fire."
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-not-elided",
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 'N' roll",
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 `left`/`right`. `N` ≠ `n`, so no idiom matches (the en-US elisionIdioms entry requires exactly lower-case \"n\"). 'N' is preserved by the general ambiguous-medial-span veto instead (quotes.md §3.2), which is case-insensitive to the enclosed letter this fixture is the witness that idiom-matching stays case-sensitive even though the general veto covering the same bytes is not."
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> '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 veto does not fire matches text mode's own boundary behaviour on split input. spec 0.5.0: the general ambiguous-medial-span veto's left-context test only reads the single INLINE-SPACE code point immediately before the mark (quotes.md 3.2), which sits inside the same prose span as the mark itself unlike idiom word-matching (wordEndsAt), it does not need to see past the span-boundary marker to \"rock\" in the preceding <em> span, so it fires here even though the idiom match (pre-0.5.0 behaviour, still correctly not firing) does not."
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 marksso 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 'n' <em>roll</em></p>",
1720
- "note": "quotes.md §3.2, §6 row H2: mirror case on `right`. spec 0.5.0: symmetric case to the left-split row above the general veto's right-context test reads the INLINE-SPACE immediately after the mark, inside the same span, and fires regardless of \"roll\" sitting in a following <em> span."
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* 'n' roll\n",
1729
- "note": "quotes.md §3.2, §6 row H3: `left`'s word sits inside an emphasis span; the boundary marker between spans is not INLINE-SPACE, so the veto does not cross it. spec 0.5.0: same mechanism as the HTML left-split rowMarkdown emphasis creates a span boundary before \"rock\", but the general veto's left-context test only needs the immediately-preceding INLINE-SPACE, which is in the same span as the mark."
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 'A' today",
1737
- "note": "spec 0.5.0, general ambiguous-medial-span veto (quotes.md 3.2): 1 LETTER enclosed, space-flanked on both sides, no matching elisionIdioms entry preserved as literal U+0027 on both marks. A documented false negative: this reads as a plausible genuine quotation of the letter A, but the structural predicate cannot distinguish it from rock 'n' roll's shape without reasoning about meaning, which is out of scope (AUDIT_REMEDIATION_AND_RELEASE_PLAN.md 3.1: false negatives are preferable to text damage)."
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 'no' yesterday",
1745
- "note": "spec 0.5.0, general ambiguous-medial-span veto: 2 LETTERs enclosed, same reasoning as en-us-quotes-ambiguous-single-letter."
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 'yes' now",
1753
- "note": "spec 0.5.0, general ambiguous-medial-span veto: 3 LETTERs enclosed — the upper bound of the shape (quotes.md 3.2, MAX_ENCLOSED). Preserved."
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 '𐐀' now",
1769
- "note": "spec 0.5.0: the enclosed code point is U+10400 (DESERET CAPITAL LETTER LONG A, astral plane) general category Lu, a member of LETTER. The veto's LETTER test (engine/unicode.js isLetter) operates on full code points, not UTF-16 units, so this is treated identically to an ASCII letter and preserved."
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.5.0: the enclosed code points are DIGIT, not LETTER, so the general ambiguous-medial-span veto does not fire digits must not accidentally qualify as a LETTER sequence (quotes.md 3.2). Pairs as an ordinary quotation exactly as before 0.5.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.5.0: the general ambiguous-medial-span veto matches straight ASCII U+0027 pairs only (quotes.md 3.2) an already-curly U+2019 pair is out of its scope by construction, so this falls through to ordinary NARROW-pair quotation exactly as it did before 0.5.0. Documents the chosen contract deliberately, so the ASCII-only scope is not silently broadened later."
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.0.0",
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 'n' roll",
756
- "note": "quotes.md §7.1, recorded as a known defect: the two elision marks pair at depth 1 and take the Finnish primary glyphs. spec 0.5.0: fi's own dictionary writes this loanword closed up (rock'n'roll, no spaces) — quotes.md §7 item 8 — so a citation for the spaced form would be evidenced-inert and none was added. The general ambiguous-medial-span veto now preserves the spaced ASCII input as-is rather than pairing it as an ordinary quotation, which is the pre-0.5.0 behaviour this fixture used to assert."
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.0.0",
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 'oui' hier. »",
84
- "note": "spec 0.5.0: same as fr-quotes-nested — 'oui' is preserved by the general ambiguous-medial-span veto."
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 'n' roll",
254
- "note": "spec 0.5.0: fr-CA has no cited quotes.elisionIdioms entry for this idiom (quotes.md §7 item 8 only en-US does, via Chicago Manual of Style Online and the American Heritage Dictionary). The general ambiguous-medial-span veto (quotes.md 3.2) preserves both marks as literal U+0027 rather than pairing them as an ordinary quotation, closing the false positive AUDIT_REMEDIATION_AND_RELEASE_PLAN.md 3.1 named for \"equivalent cases in several locales.\""
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.0.0",
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 'oui' hier. »",
239
- "note": "Depth 1 primary « », depth 2 secondary “ ” whose innerSpace is none. spec 0.5.0: the inner 'oui' (3 letters, space-flanked) is preserved by the general ambiguous-medial-span veto fr has no elisionIdioms entry for this shape."
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 'n' roll",
589
- "note": "spec 0.5.0: fr has no cited quotes.elisionIdioms entry for this idiom (quotes.md §7 item 8 only en-US does, via Chicago Manual of Style Online and the American Heritage Dictionary). The general ambiguous-medial-span veto (quotes.md 3.2) preserves both marks as literal U+0027 rather than pairing them as an ordinary quotation, closing the false positive AUDIT_REMEDIATION_AND_RELEASE_PLAN.md 3.1 named for \"equivalent cases in several locales.\""
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.0.0",
2
+ "spec": "1.1.0",
3
3
  "cases": [
4
4
  {
5
5
  "id": "exact-en-us",
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.0.0",
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 0.5.0: the inner 'моё' (3 Cyrillic letters, space-flanked) is preserved by the general ambiguous-medial-span veto, which reads Unicode LETTER generally, not only ASCIIru has no elisionIdioms entry for this shape."
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-flankeda 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 'n' roll",
685
- "note": "spec 0.5.0: ru has no cited quotes.elisionIdioms entry for this idiom (quotes.md §7 item 8 only en-US does, via Chicago Manual of Style Online and the American Heritage Dictionary). The general ambiguous-medial-span veto (quotes.md 3.2) preserves both marks as literal U+0027 rather than pairing them as an ordinary quotation, closing the false positive AUDIT_REMEDIATION_AND_RELEASE_PLAN.md 3.1 named for \"equivalent cases in several locales.\""
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.0.0",
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 'n' roll",
756
- "note": "quotes.md §7.1, a known defect: the elision marks pair at depth 1 and take the Swedish primary glyphs. spec 0.5.0: same reasoning as fi-quotes-rock-n-roll sv also writes the loanword closed up, no citation exists for the spaced form, and the general ambiguous-medial-span veto now preserves the ASCII input."
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.0.0",
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.0.0",
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 an ambiguous medial short single-quoted span (spec 0.5.0) unless a cited `quotes.elisionIdioms` entry matches."
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": ["quotes"],
59
- "summary": "Remaining straight apostrophes to U+2019 without corrupting contractions. As of spec 0.5.0, also reads `quotes.elisionIdioms` (indirectly, via the shared ambiguity predicate) to keep a straight-ASCII mark `quotes` deliberately preserved from being independently curled by this rule's own structural case ladder."
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 0.5.0 this rule additionally skips a small, precisely-defined set of positions
21
- # entirely (apostrophe.md 3.4) -- the shared ambiguous-medial-span preserve set,
22
- # QuoteAmbiguity.compute_preserve_indices -- rather than applying its case ladder to them.
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, locale_data, _ctx)
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
- # STRAIGHT_APOSTROPHE is the GENERAL ambiguous-shape veto's own trigger glyph -- U+0027
30
- # only. An already-curly U+2018/U+2019 pair is out of this predicate's scope by
31
- # construction.
32
- STRAIGHT_APOSTROPHE = 0x27
33
-
34
- # NARROW -- quotes.md 3.1 NARROW -- every glyph an elision idiom's marks may appear as
35
- # across pipeline passes (straight, or already curled by an earlier pass). Shared with
36
- # quotes.rb so the two rules cannot define two slightly different NARROW sets.
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
- # (U+0027 only) enclosing 1-3 LETTER code points, with at least one INLINE-SPACE code
191
- # point immediately outside each mark. Both mark positions are returned for every match.
192
- # A superset of compute_idiom_matched_indices's output whenever an idiom's elided field
193
- # is itself 1-3 letters (true of every idiom shipped so far), but computed independently
194
- # rather than assumed, since a future idiom's elided field is not required to be that
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) == STRAIGHT_APOSTROPHE
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
- k = 0
207
- k += 1 while k < MAX_ENCLOSED && UnicodeUtil.letter?(at(cp, i + 1 + k))
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 + 1 + k
211
- next unless at(cp, j) == STRAIGHT_APOSTROPHE
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polytypo
4
- VERSION = "1.0.1"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polytypo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Iurii Rogulia