polytypo 1.8.1 → 1.9.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: 2d0c292d4d4d7848c37d1b537a94a9b4ed57f2b19f02976572f87c17679b6653
4
- data.tar.gz: 8ae273d9d48fe370fd406549bcfeffb3ad43dc2f4791a29cee83d791e1e481d5
3
+ metadata.gz: 1cbf7a3df3731842997237add29466678684ff9e0cc645389008bd5263dae02c
4
+ data.tar.gz: ff61539488d6a7a3dd84db11e05db7096511f3bbaba8a3fd8fb4fa3b9d45a616
5
5
  SHA512:
6
- metadata.gz: d805f12ef4d099d3080d1d2af38406ef031e55006d6a31aba70cd546b771f3c944d4e00ace219e7535aa8f641fa95ad20c1e529f1ea183cc52863dcd00202778
7
- data.tar.gz: 3e20237ab9491e16ac14f07826a227e74b72d882c966630839bcec5c2cceb8bb3707bdf975fa538b8b3ec6cd8a21dc1ea47e90d9b3e5a9abb14ba4d498e233c6
6
+ metadata.gz: 2dd0a932acc4441b112d8efe4152b43fe2cab631a1afca3e457b45cd739240685bd022167f6cc241c5eeb580f734c2e64a5bc5c05843fa354236a54400d612d5
7
+ data.tar.gz: 88e51e4b4b5c59c9c3ad1d8a5299abb6c7eb5f53634f45dbb9b44878034d8bae40201e2ec8e19da6c674cc718fa4fc0799455d8f9896e2fbc20a49a250e63617
data/README.md CHANGED
@@ -104,9 +104,9 @@ Polytypo.transform(
104
104
  ```
105
105
 
106
106
  Without `frontmatter_keys:` nothing in the block changes, and a key you do not name never does. The
107
- block is read with the same scan `yaml` mode uses, so it refuses the same constructs — a
108
- single-quoted scalar containing `''` among them, which is how an apostrophe is written inside single
109
- quotes, so `title: 'It''s a test'` comes back untouched. The block is also processed separately from
107
+ block is read with the same scan `yaml` mode uses, so it refuses the same constructs, and
108
+ reads a doubled apostrophe in a single-quoted scalar as an opaque unit: in `title: 'It''s a test'`
109
+ the `''` is left alone and the text on either side is processed. The block is also processed separately from
110
110
  the body, which shows up in exactly one place: an unbalanced quotation mark in `title` cannot pair
111
111
  with a mark in your first paragraph.
112
112
 
@@ -1 +1 @@
1
- 1.8.1
1
+ 1.9.0
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "cs",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "de-CH",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "de-DE",
4
4
  "cases": [
5
5
  {
@@ -776,6 +776,17 @@
776
776
  "in": "---\ntitle: Er sagte \"hallo\" laut\nsummary: Noch \"eins\" hier\nslug: \"er-sagte-hallo\"\n---\n\nIm Text \"so\" hier.\n",
777
777
  "out": "---\ntitle: Er sagte „hallo“ laut\nsummary: Noch „eins“ hier\nslug: \"er-sagte-hallo\"\n---\n\nIm Text „so“ hier.\n",
778
778
  "note": "modes.md §3.7.4: two listed plain scalars in a locale whose quote glyphs differ from English, with an unlisted slug between the block and the body. Each listed scalar is its own span and the marks pair inside it, so the German pair appears three times and the slug keeps its straight quotes."
779
+ },
780
+ {
781
+ "id": "de-de-yaml-single-quoted-escape-growth-declined",
782
+ "rule": "dashes",
783
+ "mode": "yaml",
784
+ "keys": [
785
+ "description"
786
+ ],
787
+ "in": "description: 'a''--''b'\n",
788
+ "out": "description: 'a''--''b'\n",
789
+ "note": "modes.md §3.8.6 and §3.4: de-DE is a `-spaced` locale, so the dash would grow against a unit and the edge-growth rule discards it, as in fr. A second locale so that an implementation that does not split cannot pass on one cell alone."
779
790
  }
780
791
  ]
781
792
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "el",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "en-GB",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "en-US",
4
4
  "cases": [
5
5
  {
@@ -1974,16 +1974,16 @@
1974
1974
  "note": "modes.md §3.7.4 inherits every bail of §3.8.6: a double-quoted scalar whose content contains U+005C yields no spans, because the source spells the content with more characters than it has. The listed key is not enough on its own."
1975
1975
  },
1976
1976
  {
1977
- "id": "en-us-markdown-commonmark-frontmatter-keys-single-quote-bail",
1978
- "rule": "apostrophe",
1977
+ "id": "en-us-markdown-commonmark-frontmatter-keys-single-quote-escape",
1978
+ "rule": "ellipsis",
1979
1979
  "mode": "markdown",
1980
1980
  "dialect": "commonmark",
1981
1981
  "frontmatterKeys": [
1982
1982
  "title"
1983
1983
  ],
1984
- "in": "---\ntitle: 'It''s a test'\n---\n\nBody it's here.\n",
1985
- "out": "---\ntitle: 'It''s a test'\n---\n\nBody it’s here.\n",
1986
- "note": "modes.md §3.7.4, and the cost §7.13 measures: a single-quoted scalar containing two consecutive U+0027 yields no spans, and an apostrophe in a single-quoted scalar is written exactly that way. Half the prose values of the §3.7.4.1 corpus bail here when re-emitted in this style."
1984
+ "in": "---\ntitle: 'It''s a test...'\n---\n\nBody it's here.\n",
1985
+ "out": "---\ntitle: 'It''s a test…'\n---\n\nBody it’s here.\n",
1986
+ "note": "modes.md §3.7.4 inherits §3.8.6's split of `''` (spec 1.9.0): the value converts on both sides of the unit and the apostrophe it spells stays straight, while the body's own apostrophe converts. Until 1.9.0 the whole value yielded no spans."
1987
1987
  },
1988
1988
  {
1989
1989
  "id": "en-us-markdown-commonmark-frontmatter-keys-separate-unit",
@@ -2545,15 +2545,70 @@
2545
2545
  "note": "modes.md §3.8.6: a single-quoted scalar's content is one span; the quotes themselves are outside it."
2546
2546
  },
2547
2547
  {
2548
- "id": "en-us-yaml-single-quoted-escape-untouched",
2548
+ "id": "en-us-yaml-single-quoted-escape-split",
2549
2549
  "rule": "ellipsis",
2550
2550
  "mode": "yaml",
2551
2551
  "keys": [
2552
2552
  "description"
2553
2553
  ],
2554
2554
  "in": "description: 'it''s one...two'\n",
2555
- "out": "description: 'it''s one...two'\n",
2556
- "note": "modes.md §3.8.6: `''` makes the source spell the content with more characters than it has, so the offset model of §3.1 cannot hold and the scalar yields no spans."
2555
+ "out": "description: 'it''s one…two'\n",
2556
+ "note": "modes.md §3.8.6 (spec 1.9.0): `''` is one apostrophe of content spelled with two code points, so it is an opaque skipped unit and the scalar is split around it. Everything on either side converts; the apostrophe it spells is outside every span and stays U+0027. Until 1.9.0 the whole scalar yielded no spans."
2557
+ },
2558
+ {
2559
+ "id": "en-us-yaml-single-quoted-escape-quotes",
2560
+ "rule": "quotes",
2561
+ "mode": "yaml",
2562
+ "keys": [
2563
+ "description"
2564
+ ],
2565
+ "in": "description: 'It''s -- he said \"hi\" to me...'\n",
2566
+ "out": "description: 'It''s—he said “hi” to me…'\n",
2567
+ "note": "modes.md §3.8.6: the same scalar with a double-quoted run after the unit. The run pairs and converts, the dash and ellipsis convert, and the apostrophe stays straight."
2568
+ },
2569
+ {
2570
+ "id": "en-us-yaml-single-quoted-escape-at-ends",
2571
+ "rule": "ellipsis",
2572
+ "mode": "yaml",
2573
+ "keys": [
2574
+ "description"
2575
+ ],
2576
+ "in": "description: '''one...two'''\n",
2577
+ "out": "description: '''one…two'''\n",
2578
+ "note": "modes.md §3.8.6: a unit directly after the opening quote and directly before the closing one. The scan claims each pair as it is met, so `'''` is the unit followed by content and `'''` at the end is the unit followed by the closing quote."
2579
+ },
2580
+ {
2581
+ "id": "en-us-yaml-single-quoted-escape-only",
2582
+ "rule": "ellipsis",
2583
+ "mode": "yaml",
2584
+ "keys": [
2585
+ "description"
2586
+ ],
2587
+ "in": "description: ''''\n",
2588
+ "out": "description: ''''\n",
2589
+ "note": "modes.md §3.8.6: a scalar that is nothing but one unit yields no non-empty segment, so no span, and comes back byte for byte."
2590
+ },
2591
+ {
2592
+ "id": "en-us-yaml-single-quoted-escape-contraction-at-unit",
2593
+ "rule": "dashes",
2594
+ "mode": "yaml",
2595
+ "keys": [
2596
+ "description"
2597
+ ],
2598
+ "in": "description: 'a''--''b'\n",
2599
+ "out": "description: 'a''—''b'\n",
2600
+ "note": "modes.md §3.8.6 and §3.4: the dash sits between two units, so it is a whole span. A contraction (2 → 1) is applied at a span extremity, exactly as it is against `:` and `#` in a plain scalar."
2601
+ },
2602
+ {
2603
+ "id": "en-us-yaml-single-quoted-escape-unterminated",
2604
+ "rule": "ellipsis",
2605
+ "mode": "yaml",
2606
+ "keys": [
2607
+ "description"
2608
+ ],
2609
+ "in": "description: 'it''s one...two\n",
2610
+ "out": "description: 'it''s one...two\n",
2611
+ "note": "modes.md §3.8.6: after a unit the scan keeps looking for a closing quote on the same line, finds none, and the value yields no spans — a trailing unit is not mistaken for a close."
2557
2612
  },
2558
2613
  {
2559
2614
  "id": "en-us-yaml-double-quoted-escape-untouched",
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "es",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "fi",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "fr-CA",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "fr",
4
4
  "cases": [
5
5
  {
@@ -961,7 +961,7 @@
961
961
  "note": "modes.md §3.8.6 in a locale where the apostrophe is elision rather than possession: the same cell as en-us-yaml-plain-apostrophe, and §8 item 4's requirement that a construct be pinned in more than one locale rather than the same English output with the locale field changed."
962
962
  },
963
963
  {
964
- "id": "fr-yaml-single-quoted-escape-untouched",
964
+ "id": "fr-yaml-single-quoted-escape-elision-stays",
965
965
  "rule": "apostrophe",
966
966
  "mode": "yaml",
967
967
  "keys": [
@@ -969,7 +969,29 @@
969
969
  ],
970
970
  "in": "description: 'l''idee et l''autre'\n",
971
971
  "out": "description: 'l''idee et l''autre'\n",
972
- "note": "modes.md §3.8.6: two consecutive U+0027 in a single-quoted scalar yield no spans, in every locale — the source spells the content with more characters than it has, and that is a property of YAML, not of the language. This is the cell §7.13 measures at 1040 of 1858 values on a real corpus."
972
+ "note": "modes.md §3.8.6: in a locale where the apostrophe is elision, the unit is the elision, and it is outside every span. The scalar has nothing else to convert and comes back unchanged — the cost §7.13 measures, pinned so that it is a documented property and not a surprise."
973
+ },
974
+ {
975
+ "id": "fr-yaml-single-quoted-escape-split",
976
+ "rule": "dashes",
977
+ "mode": "yaml",
978
+ "keys": [
979
+ "description"
980
+ ],
981
+ "in": "description: 'l''idee -- et ''l''autre...'\n",
982
+ "out": "description: 'l''idee — et ''l''autre…'\n",
983
+ "note": "modes.md §3.8.6: a spaced dash and an ellipsis interior to a span convert, either side of units; the elisions stay straight."
984
+ },
985
+ {
986
+ "id": "fr-yaml-single-quoted-escape-growth-declined",
987
+ "rule": "dashes",
988
+ "mode": "yaml",
989
+ "keys": [
990
+ "description"
991
+ ],
992
+ "in": "description: 'a''--''b'\n",
993
+ "out": "description: 'a''--''b'\n",
994
+ "note": "modes.md §3.8.6 and §3.4: `fr` is a `-spaced` locale, so the dash would grow from 2 to 3 code points against a unit. The edge-growth rule discards it, as it does against `:` and `#`. The en-US case with the same input is applied, which is what makes this one discriminate."
973
995
  },
974
996
  {
975
997
  "id": "fr-yaml-single-quoted-no-escape",
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "it",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "cases": [
4
4
  {
5
5
  "id": "exact-en-us",
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "nl",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "pl",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "pt-BR",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "pt-PT",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "ru",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "sv",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "tr",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "locale": "uk",
4
4
  "cases": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.0",
3
3
  "$comment": "Locale resolution input. Algorithm is specified in spec/rules/locale-resolution.md and is identical in every runtime — never delegate it to a platform locale-negotiation library. \"spec\" here must track spec/VERSION exactly — it is not itself the global version source; scripts/validate-spec.mjs enforces the match.",
4
4
  "locales": [
5
5
  "en-US",
@@ -7,13 +7,14 @@ for all five runtimes and is parser-agnostic by construction: `parse5`, `nokogir
7
7
  `golang.org/x/net/html` and PHP's DOM disagree about almost everything this document does not
8
8
  forbid them from doing. `yaml` mode is parser-**free** rather than parser-agnostic, for the
9
9
  reason §3.8.1 measures.
10
- **Spec version:** 1.8.0 (0.1.0 for everything except §3.3's class-membership table rows for
10
+ **Spec version:** 1.9.0 (0.1.0 for everything except §3.3's class-membership table rows for
11
11
  `nbsp` and `apostrophe`, split in 1.2.0, §3.8, added in 1.3.0, §3.3's note on `quotes`'
12
12
  span-boundary elision veto reading the marker as a trigger, added in 1.4.0 — which changes no
13
13
  row of the table it follows — §3.3's `CLOSEDELIM` entry, added in 1.5.0, and §3.7.4 with the
14
14
  amendments it carries to §3.1's definitions, §3.2's Model C, §3.5 step 3, §3.7.3's frontmatter
15
- bullet, §3.8.6's accepted-cost paragraph, §5, §6 and §7, added in 1.7.0, and §3.7.3a, added in
16
- 1.8.0).
15
+ bullet, §3.8.6's accepted-cost paragraph, §5, §6 and §7, added in 1.7.0, §3.7.3a, added in
16
+ 1.8.0, and §3.8.6's treatment of `''` in a single-quoted scalar, which replaces the bail it had
17
+ until 1.9.0, with the matching amendments to §3.7.4.1, §3.8.7, §5, §7.13 and §8).
17
18
 
18
19
  ---
19
20
 
@@ -859,14 +860,13 @@ and it is the only way this corpus can show a conversion at all.
859
860
  name `"HTPBE?"` becomes `"HTPBE ?"`. The other two are improvements a caller might well want
860
861
  (`"Niamh O'Sullivan"` → `"Niamh O’Sullivan"`), which is the point: only the caller can tell
861
862
  those apart, and that is the same argument §3.8.2 makes.
862
- - **The inherited escape bail, measured.** Re-emitting each of the 247 convertible values in
863
- one quoting style and running the scan over it: **single-quoted, 130 of 247 yield no spans**;
864
- double-quoted, none do. The reason is §3.8.6's — an apostrophe inside a single-quoted scalar
865
- is written `''`, which spells content with more characters than it has. Every listed scalar in
866
- this corpus as written is double-quoted, so the bail never fires on it; a caller whose YAML
867
- style is single quotes gets nothing on half of their prose, silently. That number is the
868
- accepted cost of reusing §3.8's scan rather than a defect of this section, and §7.13 records it
869
- where a reader will look for it.
863
+ - **The inherited escape bail, measured — and removed in 1.9.0.** Re-emitting each of the 247
864
+ convertible values in one quoting style and running the scan over it gave **single-quoted, 130 of
865
+ 247 yield no spans**; double-quoted, none. The reason was §3.8.6's bail on `''`: an apostrophe
866
+ inside a single-quoted scalar is written that way. Every listed scalar in this corpus as written
867
+ is double-quoted, so the bail never fired on it; a caller whose YAML style is single quotes got
868
+ nothing on half of their prose, silently. 1.9.0 replaced the bail with a split at each `''`
869
+ (§3.8.6), and §7.13 records what that did and did not recover.
870
870
 
871
871
  ### 3.8 Skip list — `yaml`
872
872
 
@@ -1091,16 +1091,43 @@ that line the value yields no spans, and its continuation lines are consumed by
1091
1091
  rather than rescanned. After the closing quote only U+0020 and an optional `#` comment may
1092
1092
  follow. The span is the content between the quotes, exclusive of both.
1093
1093
 
1094
- Two bails make source characters and content characters the same thing, which §3.1's offset model
1095
- requires:
1094
+ The offset model of §3.1 requires that source characters and content characters be the same thing,
1095
+ and two constructs break that:
1096
1096
 
1097
1097
  - a double-quoted scalar whose content contains U+005C yields no spans — `\n`, `\"` and `é`
1098
1098
  are content the source spells with more characters than it has;
1099
- - a single-quoted scalar whose content contains two consecutive U+0027 yields no spans, for the
1100
- same reason.
1101
-
1102
- This is the treatment `html` gives a character reference (§3.6), reached from the same constraint
1103
- rather than by analogy. **No colon test applies to a quoted scalar** — quoting neutralises the
1099
+ - in a single-quoted scalar, **two consecutive U+0027 are one apostrophe of content, spelled with
1100
+ two code points.** Since 1.9.0 that pair is an **opaque skipped unit**, treated as §3.8.6 treats
1101
+ `:` and `#` in a plain scalar: the scalar's range is split around it, and the unit lies outside
1102
+ every span. Until 1.9.0 the whole scalar yielded no spans instead.
1103
+
1104
+ Neither is a mapping from source to content. A backslash escape has dozens of forms and each would
1105
+ need its own decoding table in five runtimes; `''` is one fixed pair, and a skipped unit needs no
1106
+ table at all. That is the treatment `html` gives a character reference (§3.6), reached from the
1107
+ same constraint rather than by analogy.
1108
+
1109
+ > **The single-quoted scan, normative.** Starting after the opening quote, advance `j` one code
1110
+ > point at a time. A U+0027 at `j` followed by another U+0027 is a unit: end the current segment
1111
+ > before `j`, begin the next at `j + 2`, and continue at `j + 2`. A U+0027 at `j` not followed by
1112
+ > another U+0027 is the closing quote. If the line ends first, the value yields no spans, as for
1113
+ > any unterminated quoted scalar. The segments, each non-empty one a span, are what remains once
1114
+ > the units are removed. The scan is left to right and each pair is claimed as it is met, so
1115
+ > `'''a'` is the unit followed by `a`, and `''''` is one unit and the closing quote.
1116
+
1117
+ **A span may begin or end at a unit, and §3.4 decides what that allows — identically to `:` and
1118
+ `#`.** Nothing about `''` needs a rule of its own. A replacement that is longer than what it
1119
+ replaced is discarded at an extremity, so `fr`'s spaced dash does not grow against a unit
1120
+ (`'a''--''b'` is left alone in `fr` and `de-DE`), while a contraction is applied (`'a''--''b'` →
1121
+ `'a''—''b'` in `en-US`). The unit is a boundary marker (§3.2) between the two spans it separates:
1122
+ no rule edits a code point that lies outside every span, so the apostrophe it spells **stays
1123
+ U+0027**. That is the accepted cost of the split, and it is not small: in `'l''idee'` everything
1124
+ but the elision converts, and a value whose only convertible character was that apostrophe still
1125
+ comes back unchanged. §7.13 measures it.
1126
+
1127
+ **Why not convert the apostrophe too.** Replacing `''` with U+2019 outright would convert it and
1128
+ the result still parses, since U+2019 is ordinary content in a single-quoted scalar. It is
1129
+ rejected for the reason §3.8.1 gives: that is decode-then-remap, the offset mapping is no longer
1130
+ the identity, and five runtimes would each invent it. **No colon test applies to a quoted scalar** — quoting neutralises the
1104
1131
  colon, and applying the plain-scalar test here is what made the first draft decline
1105
1132
  `title: "Chapter 1: the beginning"`.
1106
1133
 
@@ -1177,6 +1204,21 @@ clause reverted, the same run reports **480 structural changes** — `k: a:---a`
1177
1204
  measurement that passes both with and without the thing it is supposed to protect proves nothing,
1178
1205
  and two earlier versions of this sweep were in exactly that position.
1179
1206
 
1207
+ **The same sweep, extended for `''` (1.9.0).** The alphabet gained `''` as one token beside the
1208
+ lone U+0027, and this run used four single-quoted templates alone, because the apostrophe check below cannot hold on a plain scalar, where `apostrophe` legitimately converts `a'a`. They are the payload alone, followed by a
1209
+ comment, between fixed `x''` and `''y`, and as a value in a sequence of mappings. Payloads of
1210
+ length 1–4, 19 locales, the sources that are not valid YAML discarded before the run,
1211
+ **872 328 cases over those four templates, 65 033 of them changed by the transform: no parse failure, no structural change, no idempotency failure, and the number of U+0027 in the output
1212
+ equal to the number in the source in every case** — the last check is what a structure-and-type
1213
+ comparison cannot see, since a `''` turned into `’’` leaves the document's shape intact. It
1214
+ discriminates, but narrowly, and that should be said: with the unit's width off by one (the
1215
+ second U+0027 left inside the span) the same run reports failures — `k: '''a'` becoming
1216
+ `k: ''’a'`, in `cs` and others — whereas an implementation that does not split at all, and hands
1217
+ the pair to the rules as two ordinary characters, **also passes**, because `quotes`' same-kind
1218
+ adjacency veto leaves an adjacent pair alone. The split is therefore specified for the reason
1219
+ §3.8.1 gives, that it needs no mapping and puts the pair out of every rule's reach by construction,
1220
+ and not because the sweep shows the unsplit alternative to be unsafe at payload length four.
1221
+
1180
1222
  **Three things it could not find**, each worth naming because each cost something.
1181
1223
 
1182
1224
  - It did not find the workflow damage, because a shell script inside a block scalar is a string
@@ -1372,7 +1414,10 @@ tests none of this document. In `yaml` the sweep alphabet must include `:`, `#`,
1372
1414
  as one token**, since those are the characters whose adjacency the argument above turns on and a
1373
1415
  three-dash run is not reachable from single dashes at a bounded payload length; and it must
1374
1416
  include U+0022 and U+005C, without which the sweep cannot reach a quoted scalar's delimiters at
1375
- all. Since 1.7.0 the `markdown` run must also carry a template with a **frontmatter block and
1417
+ all. Since 1.9.0 it must also include **`''` as one token** next to a lone U+0027, and its
1418
+ single-quoted templates must carry the pair at the start, in the middle and at the end of the
1419
+ payload, because a unit's behaviour at a span extremity is a different path from its behaviour
1420
+ between two letters. Since 1.7.0 the `markdown` run must also carry a template with a **frontmatter block and
1376
1421
  `frontmatterKeys` naming a key in it** — a document with two text units (§3.1) tests a composition
1377
1422
  the single-unit templates cannot reach. §3.8.7 records what each run did and did not establish — including that a sweep comparing
1378
1423
  structure and types is blind to a string whose content is damaged, and that an alphabet without
@@ -1628,19 +1673,25 @@ rule-local.
1628
1673
  The block is still skipped, so nothing machine-read is typeset either way. A lone-U+000D
1629
1674
  document is one line to §3.8.4 and therefore loses the whole block, which is the price of
1630
1675
  not widening that section here.
1631
- - **§3.8.6's single-quoted bail costs more here than anywhere it has been measured before.**
1632
- Of the 1858 corpus values a locale would convert across eight locales, **1040 yield no spans
1633
- when written as a single-quoted scalar** — 130 of 247 in `en-GB` alone, the figure 1.7.0
1634
- shipped with, and `scripts/miss-census.mjs` in this repository is what reproduces either on
1635
- demand. An apostrophe inside such a scalar is spelled `''`, and an apostrophe
1636
- is exactly what `apostrophe` and `quotes` convert, so the bail falls hardest on the values
1637
- the option exists for. Double-quoted, none bail. The corpus as authored is entirely
1638
- double-quoted, so its own author never meets this; a caller whose YAML style is single
1639
- quotes gets nothing on half their prose, and gets it silently. The repair is not this
1640
- section's to make: §3.8.6 is ratified and measured, and the obvious extension — treating
1641
- `''` as an opaque two-code-point unit, exactly as §3.8.6 already treats `:` and `#` inside a
1642
- plain scalar — changes `yaml` mode for every caller and needs its own measurement and its
1643
- own sign-off.
1676
+ - **§3.8.6's treatment of `''` in a single-quoted scalar: the cost, before and after 1.9.0.**
1677
+ Until 1.9.0 a single-quoted scalar containing `''` yielded no spans. An apostrophe inside
1678
+ such a scalar is spelled `''`, and an apostrophe is exactly what `apostrophe` and `quotes`
1679
+ convert, so the bail fell hardest on the values the option exists for. On the author's own
1680
+ 207-file corpus, de-typeset and re-emitted as single-quoted, in eight locales: **1104 of
1681
+ 1913 values a locale would convert came back unchanged**. Since 1.9.0 the scalar is split at
1682
+ each `''` (§3.8.6) and **672 of 1913** do — 35.1% against 57.7%. The remainder is not one
1683
+ cost but two. **602 of the 672 have nothing else to convert**: the apostrophe is the only
1684
+ thing a locale would have changed, it lies outside every span by construction, and it stays
1685
+ straight. **The other 70 also change when their `'` are already curly**, so something besides
1686
+ an apostrophe is lost — in the samples read, a phrase written `'like this'`, which a locale
1687
+ would pair as a quotation, and whose two marks are units here. Not all 70 were read.
1688
+ Double-quoted and plain scalars are unaffected, and the corpus as authored is entirely
1689
+ double-quoted, so its own author never met either figure.
1690
+ The "before" figure is what `scripts/miss-census.mjs` prints with a 1.8.x engine
1691
+ installed; the "after" figure is the same run with the 1.9.0 engine, and the 602/70 split
1692
+ is a separate run of that comparison on values with every `'` already curly, which the
1693
+ script does not perform. The corpus has grown since 1.7.0 measured it (187 files, 1040 of
1694
+ 1858), which is why the denominator differs from that figure.
1644
1695
 
1645
1696
  ## 8. Fixture coverage strategy (non-normative)
1646
1697
 
@@ -1677,7 +1728,7 @@ changes.
1677
1728
  blocks, and the JSX-vs-skipped-element case distinction (`tests/modes/markdown.test.ts`).
1678
1729
  3a. **YAML span selection** — the three scalar forms, the `keys` option (a listed key, an
1679
1730
  unlisted one, a quoted one, the same name at two depths), block-scalar indentation and
1680
- chomping, the `:`/`#` split of §3.8.6, step 7's consumption of an inline value's continuation
1731
+ chomping, the `:`/`#` split of §3.8.6, the `''` split of §3.8.6 (since 1.9.0), step 7's consumption of an inline value's continuation
1681
1732
  lines, and every bail of §3.8.4. `yaml` carries a heavier fixture burden than the other two
1682
1733
  for a reason §8's opening argument does not cover: its span selection is **specified rather
1683
1734
  than delegated** (§3.8.1), so a fixture is the only thing standing between five hand-written
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "1.8.1",
2
+ "spec": "1.9.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
  {
@@ -246,21 +246,30 @@ module Polytypo
246
246
  end
247
247
  private_class_method :block_scalar
248
248
 
249
- # 3.8.6. The span is the content between the quotes. Both bails exist so that source
250
- # characters and content characters are the same thing, which the offset model of 3.1
251
- # requires -- the same constraint that makes an HTML character reference an opaque unit in
252
- # 3.6. No colon test applies here: quoting neutralises the colon, and applying the
253
- # plain-scalar test would decline `title: "Chapter 1: the beginning"`.
249
+ # 3.8.6. The span is the content between the quotes. A backslash in a double-quoted scalar
250
+ # bails, so that source characters and content characters are the same thing, which the
251
+ # offset model of 3.1 requires. In a single-quoted scalar each doubled apostrophe is an
252
+ # opaque two-code-point unit that lies outside every span, the same treatment an HTML
253
+ # character reference gets in 3.6; the scalar's range is split around it. No colon test
254
+ # applies here: quoting neutralises the colon, and applying the plain-scalar test would
255
+ # decline `title: "Chapter 1: the beginning"`.
254
256
  def self.quoted_scalar(chars, line, v, spans)
255
257
  quote = chars[v]
258
+ starts = []
256
259
  close = -1
260
+ segment = v + 1
257
261
  j = v + 1
258
262
  while j < line.end
259
263
  c = chars[j]
260
264
  return if quote == '"' && c == "\\"
261
- return if quote == "'" && c == "'" && j + 1 < line.end && chars[j + 1] == "'"
262
265
 
263
266
  if c == quote
267
+ if quote == "'" && j + 1 < line.end && chars[j + 1] == "'"
268
+ starts << [segment, j] if j > segment
269
+ segment = j + 2
270
+ j += 2
271
+ next
272
+ end
264
273
  close = j
265
274
  break
266
275
  end
@@ -272,7 +281,8 @@ module Polytypo
272
281
  after += 1 while after < line.end && chars[after] == SPACE
273
282
  return if after < line.end && chars[after] != "#"
274
283
 
275
- spans << Spans::Span.new(v + 1, close) if close > v + 1
284
+ starts << [segment, close] if close > segment
285
+ starts.each { |from, to| spans << Spans::Span.new(from, to) }
276
286
  end
277
287
  private_class_method :quoted_scalar
278
288
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polytypo
4
- VERSION = "1.8.1"
4
+ VERSION = "1.9.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.8.1
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Iurii Rogulia