polytypo 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +109 -0
- data/lib/polytypo/data/README.md +20 -0
- data/lib/polytypo/data/UNICODE +1 -0
- data/lib/polytypo/data/VERSION +1 -0
- data/lib/polytypo/data/fixtures/de-CH.json +501 -0
- data/lib/polytypo/data/fixtures/de-DE.json +547 -0
- data/lib/polytypo/data/fixtures/el.json +239 -0
- data/lib/polytypo/data/fixtures/en-GB.json +1274 -0
- data/lib/polytypo/data/fixtures/en-US.json +1807 -0
- data/lib/polytypo/data/fixtures/fi.json +1306 -0
- data/lib/polytypo/data/fixtures/fr-CA.json +268 -0
- data/lib/polytypo/data/fixtures/fr.json +603 -0
- data/lib/polytypo/data/fixtures/locale-resolution.json +209 -0
- data/lib/polytypo/data/fixtures/ru.json +688 -0
- data/lib/polytypo/data/fixtures/sv.json +1290 -0
- data/lib/polytypo/data/locales/de-CH.json +77 -0
- data/lib/polytypo/data/locales/de-DE.json +76 -0
- data/lib/polytypo/data/locales/el.json +90 -0
- data/lib/polytypo/data/locales/en-GB.json +115 -0
- data/lib/polytypo/data/locales/en-US.json +133 -0
- data/lib/polytypo/data/locales/fi.json +136 -0
- data/lib/polytypo/data/locales/fr-CA.json +78 -0
- data/lib/polytypo/data/locales/fr.json +84 -0
- data/lib/polytypo/data/locales/registry.json +9 -0
- data/lib/polytypo/data/locales/ru.json +112 -0
- data/lib/polytypo/data/locales/sv.json +124 -0
- data/lib/polytypo/data/rules/dashes.md +1238 -0
- data/lib/polytypo/data/rules/order.json +78 -0
- data/lib/polytypo/data/schema/fixtures.schema.json +79 -0
- data/lib/polytypo/data/schema/locale.schema.json +235 -0
- data/lib/polytypo/data/schema/registry.schema.json +29 -0
- data/lib/polytypo/data/schema/resolution.schema.json +50 -0
- data/lib/polytypo/engine/codepoints.rb +24 -0
- data/lib/polytypo/engine/edits.rb +64 -0
- data/lib/polytypo/engine/locale.rb +138 -0
- data/lib/polytypo/engine/pipeline.rb +61 -0
- data/lib/polytypo/engine/registry.rb +47 -0
- data/lib/polytypo/engine/rules/apostrophe.rb +127 -0
- data/lib/polytypo/engine/rules/dash_shared.rb +342 -0
- data/lib/polytypo/engine/rules/dashes.rb +125 -0
- data/lib/polytypo/engine/rules/ellipsis.rb +100 -0
- data/lib/polytypo/engine/rules/hyphen.rb +207 -0
- data/lib/polytypo/engine/rules/nbsp.rb +616 -0
- data/lib/polytypo/engine/rules/quote_ambiguity.rb +241 -0
- data/lib/polytypo/engine/rules/quotes.rb +420 -0
- data/lib/polytypo/engine/rules/ranges.rb +124 -0
- data/lib/polytypo/engine/rules/spaces.rb +232 -0
- data/lib/polytypo/engine/rules/symbols.rb +291 -0
- data/lib/polytypo/engine/rules.rb +19 -0
- data/lib/polytypo/engine/sentinels.rb +23 -0
- data/lib/polytypo/engine/unicode_util.rb +390 -0
- data/lib/polytypo/errors.rb +24 -0
- data/lib/polytypo/modes/html.rb +233 -0
- data/lib/polytypo/modes/markdown.rb +187 -0
- data/lib/polytypo/modes/parse_error.rb +19 -0
- data/lib/polytypo/modes/runner.rb +57 -0
- data/lib/polytypo/modes/spans.rb +132 -0
- data/lib/polytypo/version.rb +5 -0
- data/lib/polytypo.rb +91 -0
- data/polytypo.gemspec +37 -0
- metadata +122 -0
|
@@ -0,0 +1,1807 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "1.0.0",
|
|
3
|
+
"locale": "en-US",
|
|
4
|
+
"cases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "en-us-spaces-collapse-run",
|
|
7
|
+
"rule": "spaces",
|
|
8
|
+
"mode": "text",
|
|
9
|
+
"in": "Hello world.",
|
|
10
|
+
"out": "Hello world.",
|
|
11
|
+
"note": "spaces.md §6 case 1."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "en-us-spaces-strip-before-punctuation",
|
|
15
|
+
"rule": "spaces",
|
|
16
|
+
"mode": "text",
|
|
17
|
+
"in": "Hello , world !",
|
|
18
|
+
"out": "Hello, world!",
|
|
19
|
+
"note": "spaces.md §6 case 2: `,` and `!` are in STRIP-BEFORE."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "en-us-spaces-bracket-inner",
|
|
23
|
+
"rule": "spaces",
|
|
24
|
+
"mode": "text",
|
|
25
|
+
"in": "( ok ) and [ x ]",
|
|
26
|
+
"out": "(ok) and [x]",
|
|
27
|
+
"note": "spaces.md §6 case 3."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "en-us-spaces-gfm-checkbox",
|
|
31
|
+
"rule": "spaces",
|
|
32
|
+
"mode": "text",
|
|
33
|
+
"in": "- [ ] buy milk",
|
|
34
|
+
"out": "- [ ] buy milk",
|
|
35
|
+
"note": "spaces.md §6 case 4: empty-bracket guard, replacement length 1 and the run is already 1."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "en-us-spaces-empty-bracket-collapse",
|
|
39
|
+
"rule": "spaces",
|
|
40
|
+
"mode": "text",
|
|
41
|
+
"in": "( )",
|
|
42
|
+
"out": "( )",
|
|
43
|
+
"note": "spaces.md §6 case 4b: the guard forces length 1, it does not skip."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "en-us-spaces-markdown-hard-break",
|
|
47
|
+
"rule": "spaces",
|
|
48
|
+
"mode": "text",
|
|
49
|
+
"in": "line one \nline two",
|
|
50
|
+
"out": "line one \nline two",
|
|
51
|
+
"note": "spaces.md §6 case 5: the run is followed by a BREAK."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "en-us-spaces-leading-indent",
|
|
55
|
+
"rule": "spaces",
|
|
56
|
+
"mode": "text",
|
|
57
|
+
"in": " indented code",
|
|
58
|
+
"out": " indented code",
|
|
59
|
+
"note": "spaces.md §6 case 6."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "en-us-spaces-nbsp-neighbour",
|
|
63
|
+
"rule": "spaces",
|
|
64
|
+
"mode": "text",
|
|
65
|
+
"in": "5 km",
|
|
66
|
+
"out": "5 km",
|
|
67
|
+
"note": "spaces.md §6 case 7: the U+00A0 is CONTENT and bounds the run; only the U+0020 run collapses. The unit is not bound either, because nbsp N5 requires a DIGIT two positions left of the unit."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "en-us-spaces-nbsp-run-untouched",
|
|
71
|
+
"rule": "spaces",
|
|
72
|
+
"mode": "text",
|
|
73
|
+
"in": "a b",
|
|
74
|
+
"out": "a b",
|
|
75
|
+
"note": "spaces.md §6 case 8: no U+0020 anywhere."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "en-us-spaces-terminal-punctuation",
|
|
79
|
+
"rule": "spaces",
|
|
80
|
+
"mode": "text",
|
|
81
|
+
"in": "Bonjour ! Ça va ?",
|
|
82
|
+
"out": "Bonjour! Ça va?",
|
|
83
|
+
"note": "spaces.md §6 case 9, transcribed verbatim. The rule is locale-independent; this locale lists nothing in nbsp.narrowBeforePunctuation, so nothing is restored."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "en-us-spaces-before-final-stop",
|
|
87
|
+
"rule": "spaces",
|
|
88
|
+
"mode": "text",
|
|
89
|
+
"in": "See p. 12 .",
|
|
90
|
+
"out": "See p. 12.",
|
|
91
|
+
"note": "spaces.md §6 case 10: a lone dot, so the lone-dot condition (§3.4) holds and the run is deleted."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "en-us-spaces-lone-dot-condition-typed-ellipsis",
|
|
95
|
+
"rule": "spaces",
|
|
96
|
+
"mode": "text",
|
|
97
|
+
"in": "Wait ...",
|
|
98
|
+
"out": "Wait …",
|
|
99
|
+
"note": "spaces.md §6 case 10c. The dot run has length 3, so the lone-dot condition (§3.4) fails and `spaces` keeps the U+0020; `ellipsis` then collapses the run. The surviving space is DELIBERATE — a tight `Wait…` was rejected because restoring it reopens `See ../docs`. The output is a fixed point only because U+2026 was also removed from STRIP-BEFORE."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "en-us-spaces-lone-dot-condition-spaced-ellipsis",
|
|
103
|
+
"rule": "spaces",
|
|
104
|
+
"mode": "text",
|
|
105
|
+
"in": "Hello . . .",
|
|
106
|
+
"out": "Hello…",
|
|
107
|
+
"note": "spaces.md §6 case 10d, and the case worth breaking a port on. Runs are maximal IN THE INPUT ARRAY and the condition is evaluated against the input, so every dot here is a LONE dot at decision time — each is followed by a space, not by another dot — and all three spaces still strip. The dots merge to `Hello...` and `ellipsis` converts them. An implementation that applies each edit as it scans, instead of collecting edits and letting the pipeline apply them (ARCHITECTURE.md §7.1), sees a growing dot run and keeps the later spaces; it passes every other fixture and fails this one."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "en-us-spaces-break-both-sides",
|
|
111
|
+
"rule": "spaces",
|
|
112
|
+
"mode": "text",
|
|
113
|
+
"in": "foo \n bar",
|
|
114
|
+
"out": "foo \n bar",
|
|
115
|
+
"note": "spaces.md §6 case 11."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "en-us-spaces-mixed",
|
|
119
|
+
"rule": "spaces",
|
|
120
|
+
"mode": "text",
|
|
121
|
+
"in": "Q: why ? Because .",
|
|
122
|
+
"out": "Q: why? Because.",
|
|
123
|
+
"note": "spaces.md §6 case 12."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "en-us-spaces-single-space-noop",
|
|
127
|
+
"rule": "spaces",
|
|
128
|
+
"mode": "text",
|
|
129
|
+
"in": "a b",
|
|
130
|
+
"out": "a b",
|
|
131
|
+
"note": "spaces.md §4: a single space in an ordinary position is not a candidate."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "en-us-spaces-protected-spaces",
|
|
135
|
+
"rule": "spaces",
|
|
136
|
+
"mode": "text",
|
|
137
|
+
"in": "Tab\there and thin space and nbsp here",
|
|
138
|
+
"out": "Tab\there and thin space and nbsp here",
|
|
139
|
+
"note": "spaces.md §4: U+0009, U+2009 and U+00A0 are never collapsed, removed or converted."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "en-us-spaces-emoticon-with-nose",
|
|
143
|
+
"rule": "spaces",
|
|
144
|
+
"mode": "text",
|
|
145
|
+
"in": "Sounds good :-)",
|
|
146
|
+
"out": "Sounds good :-)",
|
|
147
|
+
"note": "spaces.md §6 case 13 / §3.6: the emoticon guard. `:` is EMOTICON-EYE, `-` is EMOTICON-NOSE, `)` is EMOTICON-MOUTH, nothing follows — the space before the eye survives instead of being stripped as ordinary STRIP-BEFORE punctuation."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "en-us-spaces-emoticon-no-nose",
|
|
151
|
+
"rule": "spaces",
|
|
152
|
+
"mode": "text",
|
|
153
|
+
"in": "Sounds good :)",
|
|
154
|
+
"out": "Sounds good :)",
|
|
155
|
+
"note": "spaces.md §6 case 13a: same guard, no nose — EMOTICON-NOSE is optional."
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": "en-us-spaces-emoticon-semicolon-wink",
|
|
159
|
+
"rule": "spaces",
|
|
160
|
+
"mode": "text",
|
|
161
|
+
"in": "Sure thing ;-)",
|
|
162
|
+
"out": "Sure thing ;-)",
|
|
163
|
+
"note": "spaces.md §3.6: U+003B is EMOTICON-EYE too, not only U+003A."
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"id": "en-us-spaces-emoticon-not-before-word",
|
|
167
|
+
"rule": "spaces",
|
|
168
|
+
"mode": "text",
|
|
169
|
+
"in": "Hello :Deal with it",
|
|
170
|
+
"out": "Hello:Deal with it",
|
|
171
|
+
"note": "spaces.md §6 case 13b: `D` is EMOTICON-MOUTH, but the letter `e` immediately after it means the guard does not fire — this is a colon before a capitalised word, not a face, so ordinary STRIP-BEFORE strips the space."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "en-us-spaces-colon-not-emoticon-mouth",
|
|
175
|
+
"rule": "spaces",
|
|
176
|
+
"mode": "text",
|
|
177
|
+
"in": "See you at 10 : 30",
|
|
178
|
+
"out": "See you at 10: 30",
|
|
179
|
+
"note": "spaces.md §6 case 13d: the code point after the colon is a space, not EMOTICON-MOUTH, so the guard does not fire and ordinary STRIP-BEFORE behaviour is unaffected by its introduction."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"id": "en-us-spaces-emoticon-mouth-keeps-following-space",
|
|
183
|
+
"rule": "spaces",
|
|
184
|
+
"mode": "text",
|
|
185
|
+
"in": "Sorry :( it happens",
|
|
186
|
+
"out": "Sorry :( it happens",
|
|
187
|
+
"note": "spaces.md §6 case 13e / §3.6 mouth side: `(` is the mouth of a recognised emoticon, so the OPEN-BRACKET clause does not delete the space after it. This produced `Sorry :(it happens` before the mouth side existed, and `Sorry:(it happens` on a second pass."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "en-us-spaces-emoticon-mouth-square-bracket",
|
|
191
|
+
"rule": "spaces",
|
|
192
|
+
"mode": "text",
|
|
193
|
+
"in": "Hmm :[ well",
|
|
194
|
+
"out": "Hmm :[ well",
|
|
195
|
+
"note": "spaces.md §6 case 13f: U+005B is the other EMOTICON-MOUTH member that is also an OPEN-BRACKET member, so it has the identical defect and the identical fix."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": "en-us-spaces-emoticon-mouth-with-nose",
|
|
199
|
+
"rule": "spaces",
|
|
200
|
+
"mode": "text",
|
|
201
|
+
"in": "Well :-( then",
|
|
202
|
+
"out": "Well :-( then",
|
|
203
|
+
"note": "spaces.md §6 case 13g: the backward walk steps over the nose and finds EMOTICON-EYE at cp[s-3]."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": "en-us-spaces-emoticon-mouth-semicolon",
|
|
207
|
+
"rule": "spaces",
|
|
208
|
+
"mode": "text",
|
|
209
|
+
"in": "Sure ;( again",
|
|
210
|
+
"out": "Sure ;( again",
|
|
211
|
+
"note": "spaces.md §3.6: U+003B is EMOTICON-EYE on the mouth side too, exactly as it is on the eye side."
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"id": "en-us-spaces-nose-without-eye-not-emoticon",
|
|
215
|
+
"rule": "spaces",
|
|
216
|
+
"mode": "text",
|
|
217
|
+
"in": "a -( b",
|
|
218
|
+
"out": "a -(b",
|
|
219
|
+
"note": "spaces.md §6 case 13h: a nose with no eye behind it is not a face, so the mouth side does not fire and the ordinary OPEN-BRACKET clause deletes the space. The discriminating negative case for the backward walk."
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "en-us-spaces-bracket-inner-not-a-mouth",
|
|
223
|
+
"rule": "spaces",
|
|
224
|
+
"mode": "text",
|
|
225
|
+
"in": "word ( note )",
|
|
226
|
+
"out": "word (note)",
|
|
227
|
+
"note": "spaces.md §6 case 13i: the mouth side changes nothing for an ordinary bracket pair — this `(` is preceded by a space, not by an eye."
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"id": "en-us-spaces-emoticon-mouth-eye-attached-to-word",
|
|
231
|
+
"rule": "spaces",
|
|
232
|
+
"mode": "text",
|
|
233
|
+
"in": "Note:( x )",
|
|
234
|
+
"out": "Note:( x)",
|
|
235
|
+
"note": "spaces.md §6 case 13j / §7 item 11: the mouth side puts no condition on what precedes the eye, so it fires here too and the space after the mouth survives while the one before the closer still goes. The asymmetry is the deliberate, conservative reading — a port that required the eye to start a token would pass every other fixture and diverge here."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"id": "en-us-ellipsis-three-dots",
|
|
239
|
+
"rule": "ellipsis",
|
|
240
|
+
"mode": "text",
|
|
241
|
+
"in": "Wait... what?",
|
|
242
|
+
"out": "Wait… what?",
|
|
243
|
+
"note": "ellipsis.md §6 case 1."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"id": "en-us-ellipsis-mixed-run",
|
|
247
|
+
"rule": "ellipsis",
|
|
248
|
+
"mode": "text",
|
|
249
|
+
"in": "Wait…… what?",
|
|
250
|
+
"out": "Wait… what?",
|
|
251
|
+
"note": "ellipsis.md §6 case 2: a mixed/repeated DOTLIKE run normalises to one U+2026."
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"id": "en-us-ellipsis-two-dots-after-terminal",
|
|
255
|
+
"rule": "ellipsis",
|
|
256
|
+
"mode": "text",
|
|
257
|
+
"in": "Really?..",
|
|
258
|
+
"out": "Really?…",
|
|
259
|
+
"note": "ellipsis.md §6 case 3: abbreviatedAfterTerminal is false, so `?..` is a typing slip for `?…`."
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": "en-us-ellipsis-relative-path",
|
|
263
|
+
"rule": "ellipsis",
|
|
264
|
+
"mode": "text",
|
|
265
|
+
"in": "../docs is one level up",
|
|
266
|
+
"out": "../docs is one level up",
|
|
267
|
+
"note": "ellipsis.md §6 case 4 / §4: a run of exactly two full stops is left alone. Written without a preceding space on purpose — see the `spaced-path` case."
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"id": "en-us-ellipsis-numeric-range",
|
|
271
|
+
"rule": "ellipsis",
|
|
272
|
+
"mode": "text",
|
|
273
|
+
"in": "Version 1..5",
|
|
274
|
+
"out": "Version 1..5",
|
|
275
|
+
"note": "ellipsis.md §6 case 5: left neighbour of the two-dot run is a digit."
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"id": "en-us-ellipsis-already-ellipsis",
|
|
279
|
+
"rule": "ellipsis",
|
|
280
|
+
"mode": "text",
|
|
281
|
+
"in": "He left…",
|
|
282
|
+
"out": "He left…",
|
|
283
|
+
"note": "ellipsis.md §6 case 6."
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"id": "en-us-ellipsis-five-dots",
|
|
287
|
+
"rule": "ellipsis",
|
|
288
|
+
"mode": "text",
|
|
289
|
+
"in": "Hmm.....",
|
|
290
|
+
"out": "Hmm…",
|
|
291
|
+
"note": "ellipsis.md §6 case 7."
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"id": "en-us-ellipsis-sentence-stops",
|
|
295
|
+
"rule": "ellipsis",
|
|
296
|
+
"mode": "text",
|
|
297
|
+
"in": "Yes. No.",
|
|
298
|
+
"out": "Yes. No.",
|
|
299
|
+
"note": "ellipsis.md §6 case 8: two independent runs of length 1."
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"id": "en-us-ellipsis-two-dot-leader",
|
|
303
|
+
"rule": "ellipsis",
|
|
304
|
+
"mode": "text",
|
|
305
|
+
"in": "Two dot leader ‥ stays",
|
|
306
|
+
"out": "Two dot leader ‥ stays",
|
|
307
|
+
"note": "ellipsis.md §4: U+2025 is in no class and is never produced or consumed."
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"id": "en-us-ellipsis-spaced-path",
|
|
311
|
+
"rule": "ellipsis",
|
|
312
|
+
"mode": "text",
|
|
313
|
+
"in": "See ../docs",
|
|
314
|
+
"out": "See ../docs",
|
|
315
|
+
"note": "spaces.md §6 case 10a. Protected by the LONE-DOT CONDITION (spaces.md §3.4): `right` is U+002E but the maximal DOTLIKE run beginning there has length 2, so the replacement length is 1 and the space survives. `ellipsis` then leaves the two-dot run alone. A port that strips before any STRIP-BEFORE member without checking the run length produces `See../docs` here."
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"id": "en-us-ellipsis-spaced-two-dots",
|
|
319
|
+
"rule": "ellipsis",
|
|
320
|
+
"mode": "text",
|
|
321
|
+
"in": "e.g. ..",
|
|
322
|
+
"out": "e.g. ..",
|
|
323
|
+
"note": "spaces.md §6 case 10b, and the reason ellipsis.md §4 can promise `e.g. ..` end to end. Without the lone-dot condition `spaces` merged the run with the abbreviation dot into three U+002E, which `ellipsis` then legitimately converted to `e.g…`. Both rules were right; the condition is what makes the composition right."
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"id": "en-us-dashes-parenthetical-spaced-input",
|
|
327
|
+
"rule": "dashes",
|
|
328
|
+
"mode": "text",
|
|
329
|
+
"in": "The plan - if there is one - fails.",
|
|
330
|
+
"out": "The plan—if there is one—fails.",
|
|
331
|
+
"note": "dashes.md §6 case 1: em-tight collapses the outer spacing."
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"id": "en-us-dashes-parenthetical-double-hyphen",
|
|
335
|
+
"rule": "dashes",
|
|
336
|
+
"mode": "text",
|
|
337
|
+
"in": "The plan--if there is one--fails.",
|
|
338
|
+
"out": "The plan—if there is one—fails.",
|
|
339
|
+
"note": "dashes.md §6 case 2: guard P3."
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"id": "en-us-ranges",
|
|
343
|
+
"rule": "ranges",
|
|
344
|
+
"mode": "text",
|
|
345
|
+
"in": "1914-1918 and pp. 34-36",
|
|
346
|
+
"out": "1914–1918 and pp. 34–36",
|
|
347
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). ranges.md §6 row 3 (relocated from dashes.md §6 case 3 through spec 0.4.1): range branch, G4 equal-length branch and G5 pass. ranges.md §3.3.1 (relocated from dashes.md §3.3.1 through spec 0.4.1): a TIGHT range is emitted as JOINER dash JOINER (U+2060 on each side), because UAX #14 gives U+2013 the line-break class BA and a renderer would otherwise be free to break the range after the dash. The joiner is zero-width, so this is invisible on screen and invisible in a plain diff — read spec/fixtures/.escaped/ to review it. A spaced range form is NOT bound, and the parenthetical branch never emits a joiner.",
|
|
348
|
+
"rules": {
|
|
349
|
+
"ranges": true
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"id": "en-us-dashes-compound-hyphens",
|
|
354
|
+
"rule": "dashes",
|
|
355
|
+
"mode": "text",
|
|
356
|
+
"in": "A well-known e-mail address",
|
|
357
|
+
"out": "A well-known e-mail address",
|
|
358
|
+
"note": "dashes.md §6 case 4: guard P1."
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"id": "en-us-dashes-hyphen-glyph-parenthetical",
|
|
362
|
+
"rule": "dashes",
|
|
363
|
+
"mode": "text",
|
|
364
|
+
"in": "x ‐ y",
|
|
365
|
+
"out": "x—y",
|
|
366
|
+
"note": "dashes.md §3.1 (spec 0.2.0): U+2010 (hyphen) joined `DASH`. Spaced and flanked by letters — not a compound-word shape — it converts to the locale's parenthetical form exactly as U+002D would."
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"id": "en-us-dashes-hyphen-glyph-compound-guarded",
|
|
370
|
+
"rule": "dashes",
|
|
371
|
+
"mode": "text",
|
|
372
|
+
"in": "well‐known",
|
|
373
|
+
"out": "well‐known",
|
|
374
|
+
"note": "dashes.md §3.4 P1 (spec 0.2.0): P1's hyphen-shaped set now includes U+2010, so a tight, unspaced U+2010 between letters is guarded as a compound word exactly like U+002D — `well‐known` stays untouched."
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"id": "en-us-dashes-minus-sign-glyph-parenthetical",
|
|
378
|
+
"rule": "dashes",
|
|
379
|
+
"mode": "text",
|
|
380
|
+
"in": "x − y",
|
|
381
|
+
"out": "x—y",
|
|
382
|
+
"note": "dashes.md §3.1 (spec 0.2.0): U+2212 (minus sign) joined `DASH`. Spaced and flanked by letters, it converts to the locale's parenthetical form — this is the operator's explicit example (\"минус\") of a dash-like glyph that should no longer be silently ignored."
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"id": "en-us-ranges-minus-sign-glyph-range",
|
|
386
|
+
"rule": "ranges",
|
|
387
|
+
"mode": "text",
|
|
388
|
+
"in": "1990−2000",
|
|
389
|
+
"out": "1990–2000",
|
|
390
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). dashes.md §3.1 (spec 0.2.0): U+2212 between two equal-length digit runs is a range candidate, subject to the same G1-G5 guards as U+002D, and gets en-US's `en-tight` form with joiner binding (ranges.md §3.3.1, relocated from dashes.md §3.3.1 through spec 0.4.1).",
|
|
391
|
+
"rules": {
|
|
392
|
+
"ranges": true
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"id": "en-us-dashes-minus-sign-glyph-compound-guarded",
|
|
397
|
+
"rule": "dashes",
|
|
398
|
+
"mode": "text",
|
|
399
|
+
"in": "well−known",
|
|
400
|
+
"out": "well−known",
|
|
401
|
+
"note": "dashes.md §3.4 P1 (spec 0.2.0): P1's hyphen-shaped set includes U+2212, so a tight, unspaced minus sign between letters is guarded as a compound-word shape exactly like U+002D, on the view that this reads the same way `well-known` does — stays untouched."
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"id": "en-us-dashes-covid",
|
|
405
|
+
"rule": "dashes",
|
|
406
|
+
"mode": "text",
|
|
407
|
+
"in": "COVID-19",
|
|
408
|
+
"out": "COVID-19",
|
|
409
|
+
"note": "spec 0.5.0 ownership correction: split from the former combined `en-us-dashes-covid-and-iso` case (which bundled a genuine `dashes` witness with a genuine `ranges` witness in one string, letting one no-op stand in for two independent guarantees). This half tests `dashes` alone: `cp[L]` is the letter `D`, so the hyphen is a mixed letter/digit token, P1's compound-word guard declines it (dashes.md §3.4) — never a range candidate, since a range candidate needs a `DIGIT` on *both* sides (dashes.md §3.3, first paragraph). See `en-us-ranges-iso-8859` for the range-guard half."
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"id": "en-us-ranges-iso-8859",
|
|
413
|
+
"rule": "ranges",
|
|
414
|
+
"mode": "text",
|
|
415
|
+
"in": "ISO 8859-1",
|
|
416
|
+
"out": "ISO 8859-1",
|
|
417
|
+
"rules": {
|
|
418
|
+
"ranges": true
|
|
419
|
+
},
|
|
420
|
+
"note": "spec 0.5.0 ownership correction: split from the former combined `en-us-dashes-covid-and-iso` case. This half tests `ranges` alone, explicitly enabled: `8859-1` is digit-flanked, a genuine range candidate — G4 (equal-run-length, or the (1,2) branch) fails on (4,1), so it is declined (ranges.md §3.2). Without `rules: {ranges: true}` this input is already a no-op for the unrelated reason that `dashes` never reaches a digit-flanked token at all (dashes.md §1) — asserting it under `rule: \"dashes\"` would prove nothing about G4."
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"id": "en-us-ranges-iso-date-and-isbn",
|
|
424
|
+
"rule": "ranges",
|
|
425
|
+
"mode": "text",
|
|
426
|
+
"in": "Released 2026-08-15, ISBN 978-3-16-148410-0",
|
|
427
|
+
"out": "Released 2026-08-15, ISBN 978-3-16-148410-0",
|
|
428
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 6: guard G2 and the cluster guard. ranges.md §3.2's G2 (chain) is the current normative guard rejecting this.",
|
|
429
|
+
"rules": {
|
|
430
|
+
"ranges": true
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"id": "en-us-ranges-phone-number",
|
|
435
|
+
"rule": "ranges",
|
|
436
|
+
"mode": "text",
|
|
437
|
+
"in": "Call 212-555-1234",
|
|
438
|
+
"out": "Call 212-555-1234",
|
|
439
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 7. ranges.md §3.2's G2 (chain) is the current normative guard rejecting this.",
|
|
440
|
+
"rules": {
|
|
441
|
+
"ranges": true
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"id": "en-us-dashes-cli-flag",
|
|
446
|
+
"rule": "dashes",
|
|
447
|
+
"mode": "text",
|
|
448
|
+
"in": "run --force to override",
|
|
449
|
+
"out": "run --force to override",
|
|
450
|
+
"note": "dashes.md §6 case 8: asymmetric spacing."
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"id": "en-us-dashes-list-item",
|
|
454
|
+
"rule": "dashes",
|
|
455
|
+
"mode": "text",
|
|
456
|
+
"in": "- first item",
|
|
457
|
+
"out": "- first item",
|
|
458
|
+
"note": "dashes.md §6 case 9: no content to the left on the line."
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
"id": "en-us-ranges-descending-score",
|
|
462
|
+
"rule": "ranges",
|
|
463
|
+
"mode": "text",
|
|
464
|
+
"in": "Scores: 20-10",
|
|
465
|
+
"out": "Scores: 20-10",
|
|
466
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 10: guard G5. ranges.md §3.2's G5 (non-decreasing) is the current normative guard rejecting this.",
|
|
467
|
+
"rules": {
|
|
468
|
+
"ranges": true
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"id": "en-us-dashes-dash-space-dash",
|
|
473
|
+
"rule": "dashes",
|
|
474
|
+
"mode": "text",
|
|
475
|
+
"in": "a- - a",
|
|
476
|
+
"out": "a- - a",
|
|
477
|
+
"note": "dashes.md §6 case 19, idempotency defect (b): §3.2 step 6 rejects a DASH neighbour."
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
"id": "en-us-dashes-spaced-dash-pair",
|
|
481
|
+
"rule": "dashes",
|
|
482
|
+
"mode": "text",
|
|
483
|
+
"in": "a - - b",
|
|
484
|
+
"out": "a - - b",
|
|
485
|
+
"note": "dashes.md §6 case 21: the shape that would otherwise produce overlapping edit spans."
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
"id": "en-us-dashes-cluster-multi-run",
|
|
489
|
+
"rule": "dashes",
|
|
490
|
+
"mode": "text",
|
|
491
|
+
"in": "1914-1918--annexation",
|
|
492
|
+
"out": "1914-1918--annexation",
|
|
493
|
+
"note": "dashes.md §6 case 20: one cluster, three dash runs → entirely inert."
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"id": "en-us-ranges-cluster-multi-run",
|
|
497
|
+
"rule": "ranges",
|
|
498
|
+
"mode": "text",
|
|
499
|
+
"in": "1914-1918--annexation",
|
|
500
|
+
"out": "1914-1918--annexation",
|
|
501
|
+
"rules": {
|
|
502
|
+
"ranges": true
|
|
503
|
+
},
|
|
504
|
+
"note": "spec 0.5.0 ownership correction: companion to `en-us-dashes-cluster-multi-run`, covering the range-candidate half of the same cluster honestly. Explicitly opted in (`rules: {ranges: true}`): `1914-1918` sits in the same three-dash-run cluster as the `--` before `annexation`, so `ranges`' own cluster guard declines it too (ranges.md §3.1)."
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"id": "en-us-dashes-cluster-tight-sibling",
|
|
508
|
+
"rule": "dashes",
|
|
509
|
+
"mode": "text",
|
|
510
|
+
"in": "a–1-1",
|
|
511
|
+
"out": "a–1-1",
|
|
512
|
+
"note": "dashes.md §3.2 step 7: one cluster (–, 1, -, 1) with two dash runs, so the whole cluster is inert. Since the narrowing the authored `–` is additionally declined at step 2a, which reaches it first; both guards hold and either alone is sufficient."
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"id": "en-us-ranges-cluster-tight-sibling",
|
|
516
|
+
"rule": "ranges",
|
|
517
|
+
"mode": "text",
|
|
518
|
+
"in": "a–1-1",
|
|
519
|
+
"out": "a–1-1",
|
|
520
|
+
"rules": {
|
|
521
|
+
"ranges": true
|
|
522
|
+
},
|
|
523
|
+
"note": "spec 0.5.0 ownership correction: companion to `en-us-dashes-cluster-tight-sibling`. Explicitly opted in (`rules: {ranges: true}`): `1-1` is a genuine range candidate; the whole `–1-1` span is one cluster with two dash runs, so `ranges`' own cluster guard declines it too (ranges.md §3.1)."
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
"id": "en-us-dashes-t1-inapplicable-em-tight-converts-freely",
|
|
527
|
+
"rule": "dashes",
|
|
528
|
+
"mode": "text",
|
|
529
|
+
"in": "a--1 - 1",
|
|
530
|
+
"out": "a—1 - 1",
|
|
531
|
+
"note": "dashes.md §6 case 27, spec 0.2.0, RENAMED and re-witnessed spec 0.6.0 (Correction pass). Was `en-us-dashes-em-tight-converts-freely` with an EN witness (`a–1 - 1`); that witness is now decided by P5 (§3.4/§8.8) alone regardless of T1, which tests P5, not T1. This ASCII hyphen-run witness (`a--1`, P3, k=2) is untouched by P5 and still demonstrates the original claim: en-US's parenthetical is `em-tight` — not spaced — so T1 (which only guards a tight-to-spaced transition, dashes.md §3.2 step 8) never engages here, and the token converts its length freely to `—`. The trailing `1 - 1` is separately declined at the `dashes`/`ranges` ownership boundary (both digit-flanked, `ranges` off by default) — unrelated to T1."
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"id": "en-us-ranges-far-dash-still-rejects-g2",
|
|
535
|
+
"rule": "ranges",
|
|
536
|
+
"mode": "text",
|
|
537
|
+
"in": "a--1 - 1",
|
|
538
|
+
"out": "a—1 - 1",
|
|
539
|
+
"rules": {
|
|
540
|
+
"ranges": true
|
|
541
|
+
},
|
|
542
|
+
"note": "spec 0.6.0 correction (Correction pass): the original witness (`a–1 - 1`) used an authored EN dash for the first token, which P5 (dashes.md §3.4/§8.8) now also declines — leaving two independent reasons for the range to stay unbound and no longer isolating G2 as the decisive one. Re-witnessed with an ASCII hyphen run (`a--1`, P3, k=2) instead: P5 never applies to it (not a pure single U+2013), so `dashes` freely converts it to `a—1` on its own ordinary path, exactly as `en-us-dashes-t1-inapplicable-em-tight-converts-freely` demonstrates. G2 is therefore the *sole* reason the trailing `1 - 1` range candidate — explicitly opted in via `rules: {ranges: true}` — stays unbound: its effective left neighbour (`ranges` runs at order 25, before `dashes` at order 30, so it reads the original hyphen-minus, still `DASH`-class) triggers G2's \"no chain\" rejection (ranges.md §3.2)."
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
"id": "en-us-dashes-t2-not-needed-em-tight",
|
|
546
|
+
"rule": "dashes",
|
|
547
|
+
"mode": "text",
|
|
548
|
+
"in": ".--.",
|
|
549
|
+
"out": ".—.",
|
|
550
|
+
"note": "dashes.md §6 case 32: a tight form emits no U+0020, so guard T2 never applies and there is nothing for `spaces` to undo."
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"id": "en-us-dashes-authored-em-dash-respaced",
|
|
554
|
+
"rule": "dashes",
|
|
555
|
+
"mode": "text",
|
|
556
|
+
"in": "documents — PDFs, invoices — and you",
|
|
557
|
+
"out": "documents—PDFs, invoices—and you",
|
|
558
|
+
"note": "dashes.md §6 case 38, THE M4 BLOCKER, verbatim, and its final repair (spec 0.2.0). en-US declares `em-tight`; this input's dash is already EM, so only spacing changes — length happens to already match. STALE CLAIM CORRECTED (spec 0.6.0): this note used to say the EN-dash variant (`documents – PDFs, invoices – and you`) converts to the same output. It no longer does — P5 (dashes.md §3.4, §8.8) declines a pure single authored U+2013 unconditionally, so that EN-dash input is now returned unchanged; see `en-us-dashes-authored-en-dash-declined`. This fixture's own EM-dash input is unaffected by P5 (it never applies to U+2014) and still converts exactly as before. See `en-us-dashes-mixed-run-converts` for a run mixing dash glyphs (also unaffected by P5, since it is not a pure single-U+2013 run), and `en-us-dashes-already-correct-range-unbound` for why an already-exactly-correct range still gets no joiners (§3.3.1's invisible-edit test, unrelated to authorship)."
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"id": "en-us-dashes-authored-en-dash-declined",
|
|
562
|
+
"rule": "dashes",
|
|
563
|
+
"mode": "text",
|
|
564
|
+
"in": "documents – PDFs, invoices – and you",
|
|
565
|
+
"out": "documents – PDFs, invoices – and you",
|
|
566
|
+
"note": "dashes.md §3.4 P5 (spec 0.6.0): a pure single authored U+2013 is declined unconditionally, even in en-US (`em-tight`), where P2 would otherwise promote it to U+2014 exactly as the em-dash sibling `en-us-dashes-authored-em-dash-respaced` converts. Fresh M4 corpus evidence (§8.8) found 8/8 such conversions in real prose were range or joint-name damage and 0/8589 other accepted edits touched an authored en-dash at all — the bounded false negative this fixture pins is the deliberate cost."
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"id": "en-us-dashes-authored-en-dash-tight-declined",
|
|
570
|
+
"rule": "dashes",
|
|
571
|
+
"mode": "text",
|
|
572
|
+
"in": "a word–word pair",
|
|
573
|
+
"out": "a word–word pair",
|
|
574
|
+
"note": "dashes.md §3.4 P5: the tight form of the same veto — `k = 1`, `cp[s] = U+2013`, `lsp = rsp = 0`. Structurally identical to the corpus witnesses (`18 Oct–2 Nov`, `Fowler–Noll–Vo`): P5 does not distinguish a genuine range/name from ordinary tight prose use, by design (§8.8) — it protects the glyph unconditionally rather than guessing at semantics."
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
"id": "en-us-dashes-mixed-run-converts",
|
|
578
|
+
"rule": "dashes",
|
|
579
|
+
"mode": "text",
|
|
580
|
+
"in": "a-–b",
|
|
581
|
+
"out": "a—b",
|
|
582
|
+
"note": "dashes.md §6 case 42 (spec 0.2.0). A run mixing `DASH` glyphs — U+002D and U+2013 here — is a single token, treated exactly like the same run typed entirely with hyphens (`a--b`): P3 admits a `k = 2` run regardless of spacing, and the whole run is promoted to the locale's form. Before 0.2.0's authored-dash guard was retired, any run containing U+2013/U+2014 was declined whole; a port implementing spec 0.1.0's step 2a must not do that under 0.2.0. P5 (spec 0.6.0) does not apply here either — it only ever covers a *pure* single-U+2013 run (`k = 1`), never a mixed one."
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"id": "en-us-ranges-already-correct-range-unbound",
|
|
586
|
+
"rule": "ranges",
|
|
587
|
+
"mode": "text",
|
|
588
|
+
"in": "1914–1918",
|
|
589
|
+
"out": "1914–1918",
|
|
590
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 43: en-US's range is `en-tight`, and this token is already exactly correct — glyph, length and spacing — so it is left alone. The general principle, which is worth carrying into any new rule: THIS RULE NEVER MAKES AN EDIT WHOSE ENTIRE CONTENT IS INVISIBLE. A joiner may ride along with a visible conversion, but it must never be the only thing an edit does — an invisible-only edit to already-correct text cannot be reviewed by hand at all, which is exactly what the M4 gate is. This is decided by §3.3.1's invisible-edit test (compute the unbound form; bind only if that would itself change something visible), not by whether the original dash was authored — `1941—1945` (em dash) in this same locale converts to a bound en dash, because its length is visibly wrong and the edit is not invisible-only. ranges.md §3.3.1's invisible-edit test is the current normative source for why this stays unbound.",
|
|
591
|
+
"rules": {
|
|
592
|
+
"ranges": true
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
"id": "en-us-ranges-hyphen-range-bound",
|
|
597
|
+
"rule": "ranges",
|
|
598
|
+
"mode": "text",
|
|
599
|
+
"in": "1914-1918",
|
|
600
|
+
"out": "1914–1918",
|
|
601
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). ranges.md §6 row 44 (relocated from dashes.md §6 case 44 through spec 0.4.1), the pair to the previous case. A hyphen-typed range still converts AND still binds, because the joiners ride along with a visible conversion. The two outputs are visually identical yet only one may break across lines — §7.14 calls this the least comfortable consequence of the narrowing and the one most likely to be reported as a bug, so it is pinned in the suite rather than left to be discovered.",
|
|
602
|
+
"rules": {
|
|
603
|
+
"ranges": true
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"id": "en-us-ranges-one-two-digits",
|
|
608
|
+
"rule": "ranges",
|
|
609
|
+
"mode": "text",
|
|
610
|
+
"in": "Takes 5-10 days",
|
|
611
|
+
"out": "Takes 5–10 days",
|
|
612
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). ranges.md §6 row 3a (relocated from dashes.md §6 case 3a through spec 0.4.1): G4's (1,2) branch — Lrun is one digit, Rrun is two digits with no leading zero. A port that widens G4 but transcribes G5 literally silently DECLINES this case, because the lexicographic comparison it inherited yields \"5\" > \"1\". G5 is vacuous in the (1,2) branch — a one-digit run is at most 9 and a two-digit run without a leading zero is at least 10 — and dashes.md §6 cases 3a–3d exist to catch exactly that port.",
|
|
613
|
+
"rules": {
|
|
614
|
+
"ranges": true
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
"id": "en-us-ranges-nine-ten",
|
|
619
|
+
"rule": "ranges",
|
|
620
|
+
"mode": "text",
|
|
621
|
+
"in": "aged 9-10 years",
|
|
622
|
+
"out": "aged 9–10 years",
|
|
623
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). ranges.md §6 row 3b (relocated from dashes.md §6 case 3b through spec 0.4.1): the largest one-digit run against the smallest two-digit run, the tightest point of the (1,2) branch. A port that widens G4 but transcribes G5 literally silently DECLINES this case, because the lexicographic comparison it inherited yields \"5\" > \"1\". G5 is vacuous in the (1,2) branch — a one-digit run is at most 9 and a two-digit run without a leading zero is at least 10 — and dashes.md §6 cases 3a–3d exist to catch exactly that port.",
|
|
624
|
+
"rules": {
|
|
625
|
+
"ranges": true
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
"id": "en-us-ranges-one-twelve",
|
|
630
|
+
"rule": "ranges",
|
|
631
|
+
"mode": "text",
|
|
632
|
+
"in": "chapters 1-12",
|
|
633
|
+
"out": "chapters 1–12",
|
|
634
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). ranges.md §6 row 3c (relocated from dashes.md §6 case 3c through spec 0.4.1): (1,2). A port that widens G4 but transcribes G5 literally silently DECLINES this case, because the lexicographic comparison it inherited yields \"5\" > \"1\". G5 is vacuous in the (1,2) branch — a one-digit run is at most 9 and a two-digit run without a leading zero is at least 10 — and dashes.md §6 cases 3a–3d exist to catch exactly that port.",
|
|
635
|
+
"rules": {
|
|
636
|
+
"ranges": true
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
"id": "en-us-ranges-zero-sixty",
|
|
641
|
+
"rule": "ranges",
|
|
642
|
+
"mode": "text",
|
|
643
|
+
"in": "0-60 in six seconds",
|
|
644
|
+
"out": "0–60 in six seconds",
|
|
645
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). ranges.md §6 row 3d (relocated from dashes.md §6 case 3d through spec 0.4.1): Lrun may be `0` — the leading-zero clause constrains Rrun only. `before` is NONE here, so G1 and G3 pass. A port that widens G4 but transcribes G5 literally silently DECLINES this case, because the lexicographic comparison it inherited yields \"5\" > \"1\". G5 is vacuous in the (1,2) branch — a one-digit run is at most 9 and a two-digit run without a leading zero is at least 10 — and dashes.md §6 cases 3a–3d exist to catch exactly that port.",
|
|
646
|
+
"rules": {
|
|
647
|
+
"ranges": true
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
"id": "en-us-ranges-two-one-digits-rejected",
|
|
652
|
+
"rule": "ranges",
|
|
653
|
+
"mode": "text",
|
|
654
|
+
"in": "won 10-7",
|
|
655
|
+
"out": "won 10-7",
|
|
656
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 3e: (2,1). G4's new branch is DIRECTIONAL and does not admit it — G4 gets there before G5. A port that reads the branch as \"small magnitudes are fine\" and also allows (2,1) breaks G5 without touching it: G5's left-to-right comparison would conclude \"10\" < \"7\" because '1' < '7', and nothing in G5 would look wrong (dashes.md §3.3, G4). ranges.md §3.2's G5 (non-decreasing) is the current normative guard rejecting this.",
|
|
657
|
+
"rules": {
|
|
658
|
+
"ranges": true
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
"id": "en-us-ranges-leading-zero-rejected",
|
|
663
|
+
"rule": "ranges",
|
|
664
|
+
"mode": "text",
|
|
665
|
+
"in": "code 9-05",
|
|
666
|
+
"out": "code 9-05",
|
|
667
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 3f: (1,2) but Rrun begins with U+0030. This is the pair G5 could not have caught — `05` has the value 5, so the pair is descending, and G5 is specified only for equal-length runs. The leading-zero clause is what keeps G5 vacuous in the new branch. ranges.md §3.2's G4 (run lengths) is the current normative guard rejecting this.",
|
|
668
|
+
"rules": {
|
|
669
|
+
"ranges": true
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
"id": "en-us-ranges-phone-seven-digits",
|
|
674
|
+
"rule": "ranges",
|
|
675
|
+
"mode": "text",
|
|
676
|
+
"in": "Call 555-1234",
|
|
677
|
+
"out": "Call 555-1234",
|
|
678
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 3g: (3,4), neither branch. The phone-number case G4 exists for. ranges.md §3.2's G2 (chain) is the current normative guard rejecting this.",
|
|
679
|
+
"rules": {
|
|
680
|
+
"ranges": true
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
"id": "en-us-ranges-one-three-digits-rejected",
|
|
685
|
+
"rule": "ranges",
|
|
686
|
+
"mode": "text",
|
|
687
|
+
"in": "Call 1-800 now",
|
|
688
|
+
"out": "Call 1-800 now",
|
|
689
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 3h: (1,3). The new branch requires length(Rrun) = 2 exactly, so a toll-free number is still protected. ranges.md §3.2's G4 (run lengths) is the current normative guard rejecting this.",
|
|
690
|
+
"rules": {
|
|
691
|
+
"ranges": true
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
"id": "en-us-ranges-abbreviated-years-rejected",
|
|
696
|
+
"rule": "ranges",
|
|
697
|
+
"mode": "text",
|
|
698
|
+
"in": "the 2020-24 season",
|
|
699
|
+
"out": "the 2020-24 season",
|
|
700
|
+
"note": "spec 0.5.0 ownership correction: moved from `dashes` to `ranges` (`rules: {ranges: true}` explicit — `ranges` defaults off) — this case tests G1-G5/cluster/binding behaviour that `dashes` no longer evaluates at all for a digit-flanked token; it was vacuous under `rule: \"dashes\"` after the split. dashes.md §6 case 3i: (4,2). The abbreviated year range remains a recorded miss (dashes.md §7.3); the widening is directional and does not reach it. ranges.md §3.2's G4 (run lengths) is the current normative guard rejecting this.",
|
|
701
|
+
"rules": {
|
|
702
|
+
"ranges": true
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"id": "en-us-dashes-range-compound-label-safe-default",
|
|
707
|
+
"rule": "dashes",
|
|
708
|
+
"mode": "text",
|
|
709
|
+
"in": "Figure 5-10",
|
|
710
|
+
"out": "Figure 5-10",
|
|
711
|
+
"note": "spec 0.5.0: compound labels (`chapter 5, figure 10`, not a range) are no longer converted with default options. `dashes` never processes a digit-flanked token (dashes.md 1, 3.4), and `ranges` — the only rule that could convert this shape — is off by default (ranges.md 1). Canonical fixtures no longer assert the converted form as a successful output under any option set; what the explicit opt-in (`rules: {ranges: true}`) does with this same input is a documented limitation of that rule, characterized in tests/rules/ranges.test.ts, not portable conformance evidence."
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
"id": "en-us-hyphen-empty-lists-noop",
|
|
715
|
+
"rule": "hyphen",
|
|
716
|
+
"mode": "text",
|
|
717
|
+
"in": "A well-known e-mail address, COVID-19, Jean-Luc",
|
|
718
|
+
"out": "A well-known e-mail address, COVID-19, Jean-Luc",
|
|
719
|
+
"note": "hyphen.md §6 case 13: this locale's prefixes/suffixes/compounds are all empty, so the rule is a provable total no-op. A port that implements it anyway must fail here."
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
"id": "en-us-hyphen-empty-lists-prose",
|
|
723
|
+
"rule": "hyphen",
|
|
724
|
+
"mode": "text",
|
|
725
|
+
"in": "A first-class, well-thought-out plan for the mid-1990s.",
|
|
726
|
+
"out": "A first-class, well-thought-out plan for the mid-1990s.",
|
|
727
|
+
"note": "hyphen.md §6 case 14 / §2: with three empty lists the rule emits nothing for any input."
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
"id": "en-us-hyphen-existing-nbhy-roundtrip",
|
|
731
|
+
"rule": "hyphen",
|
|
732
|
+
"mode": "text",
|
|
733
|
+
"in": "A well‑known e‑mail address",
|
|
734
|
+
"out": "A well‑known e‑mail address",
|
|
735
|
+
"note": "hyphen.md §5: an author-written U+2011 is never examined and round-trips byte-identically."
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
"id": "en-us-hyphen-inert-dash-family",
|
|
739
|
+
"rule": "hyphen",
|
|
740
|
+
"mode": "text",
|
|
741
|
+
"in": "softhyphen, minus −5, figure‒dash, bar―here",
|
|
742
|
+
"out": "softhyphen, minus −5, figure‒dash, bar―here",
|
|
743
|
+
"note": "hyphen.md §4 and dashes.md §3.1: U+00AD, U+2012 and U+2015 remain INERT-DASH, never read and never produced. U+2212 became an ordinary DASH candidate in spec 0.2.0, but this specific token still declines — 'minus −5' has a space before the minus and none after, so `dashes`' symmetry guard (§3.2 step 4) rejects it before glyph membership matters. See dashes.md §7 for a fixture pinning U+2212 as a converted candidate."
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
"id": "en-us-quotes-basic",
|
|
747
|
+
"rule": "quotes",
|
|
748
|
+
"mode": "text",
|
|
749
|
+
"in": "She said \"hello\" twice.",
|
|
750
|
+
"out": "She said “hello” twice.",
|
|
751
|
+
"note": "quotes.md §6 case 1: depth 1 → primary, which is the double pair in en-US."
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
"id": "en-us-quotes-nested",
|
|
755
|
+
"rule": "quotes",
|
|
756
|
+
"mode": "text",
|
|
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."
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"id": "en-us-quotes-nested-inverted-input",
|
|
763
|
+
"rule": "quotes",
|
|
764
|
+
"mode": "text",
|
|
765
|
+
"in": "'He said \"no\" to me,' she noted.",
|
|
766
|
+
"out": "“He said ‘no’ to me,” she noted.",
|
|
767
|
+
"note": "quotes.md §6 case 3: depth, not the kind of the typed mark, selects the pair. Identical output to the previous case."
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
"id": "en-us-quotes-unbalanced",
|
|
771
|
+
"rule": "quotes",
|
|
772
|
+
"mode": "text",
|
|
773
|
+
"in": "He said \"hi. She said \"bye.\"",
|
|
774
|
+
"out": "He said \"hi. She said “bye.”",
|
|
775
|
+
"note": "quotes.md §6 case 4 and §3.6: the first mark survives as U+0022 rather than being guessed at."
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
"id": "en-us-quotes-stray-closer",
|
|
779
|
+
"rule": "quotes",
|
|
780
|
+
"mode": "text",
|
|
781
|
+
"in": "He left the room\" without opening one.",
|
|
782
|
+
"out": "He left the room\" without opening one.",
|
|
783
|
+
"note": "quotes.md §3.6: a closing mark with nothing open is left alone; leaving a straight mark is honest and recoverable."
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
"id": "en-us-quotes-apostrophes-and-dash",
|
|
787
|
+
"rule": "quotes",
|
|
788
|
+
"mode": "text",
|
|
789
|
+
"in": "Don't touch it — it's the '90s.",
|
|
790
|
+
"out": "Don’t touch it—it’s the ’90s.",
|
|
791
|
+
"note": "quotes.md §6 case 5 for the three apostrophes: none is claimed as a quotation, and `apostrophe` renders all three as U+2019. THE DASH IS THE M4 CLASS ITSELF. Spec 0.2.0 fully retired `dashes`' authored-dash guard (dashes.md §3.2 step 2a): en-US declares `em-tight`, and the author's dash is already EM, so this input happens to need only a spacing fix — but the same input with an EN dash converts its length too (compare `en-gb-quotes-apostrophes-and-dash`, where en-GB's `en-spaced` parenthetical converts this exact em dash to a spaced en dash). §7.14 records the M4 finding and the operator decision to reopen it: a dash's length is no longer treated as reliably authorial."
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
"id": "en-us-quotes-foot-and-inch",
|
|
795
|
+
"rule": "quotes",
|
|
796
|
+
"mode": "text",
|
|
797
|
+
"in": "He is 6' 2\" tall.",
|
|
798
|
+
"out": "He is 6' 2\" tall.",
|
|
799
|
+
"note": "quotes.md §6 case 6 and §4: no pairing is possible and both marks survive."
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
"id": "en-us-quotes-doubled-single",
|
|
803
|
+
"rule": "quotes",
|
|
804
|
+
"mode": "text",
|
|
805
|
+
"in": "The letter ''",
|
|
806
|
+
"out": "The letter ''",
|
|
807
|
+
"note": "quotes.md §6 case 7: two identical adjacent straight marks are vetoed in pass 1."
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
"id": "en-us-quotes-veto-witness",
|
|
811
|
+
"rule": "quotes",
|
|
812
|
+
"mode": "text",
|
|
813
|
+
"in": "\"\"\"a\"\"",
|
|
814
|
+
"out": "\"\"\"a\"\"",
|
|
815
|
+
"note": "quotes.md §6 case 7b, the §5.4 witness. All five marks have an identical straight neighbour, so none is a candidate. Previously produced “”“a”” and then “““a””."
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
"id": "en-us-quotes-veto-inside-pair",
|
|
819
|
+
"rule": "quotes",
|
|
820
|
+
"mode": "text",
|
|
821
|
+
"in": "\"a \"\" b\"",
|
|
822
|
+
"out": "“a \"\" b”",
|
|
823
|
+
"note": "quotes.md §6 case 7c: the inner `\"\"` is vetoed and stays straight; the outer pair converts normally."
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
"id": "en-us-quotes-existing-curly-ignored",
|
|
827
|
+
"rule": "quotes",
|
|
828
|
+
"mode": "text",
|
|
829
|
+
"in": "“Already curly,” he said, and \"this too.\"",
|
|
830
|
+
"out": "“Already curly,” he said, and “this too.”",
|
|
831
|
+
"note": "quotes.md §6 case 8, updated for spec 0.3.0 mandate 1: existing curly glyphs are now candidates too. The curly pair here is already the depth-1 primary pair, so it is a no-op by the invisible-edit test (§3.6); the straight pair is also depth 1 (the two pairs are siblings, neither enclosing the other) and converts. Same output as 0.1.0, for a different reason — see quotes.md §0."
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
"id": "en-us-quotes-rock-n-roll",
|
|
835
|
+
"rule": "quotes",
|
|
836
|
+
"mode": "text",
|
|
837
|
+
"in": "rock 'n' roll",
|
|
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."
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
"id": "en-us-quotes-elision-inside-quotation",
|
|
843
|
+
"rule": "quotes",
|
|
844
|
+
"mode": "text",
|
|
845
|
+
"in": "She said \"it's the '90s\" and left.",
|
|
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."
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
"id": "en-us-quotes-adjacent-to-punctuation",
|
|
851
|
+
"rule": "quotes",
|
|
852
|
+
"mode": "text",
|
|
853
|
+
"in": "\"Hi\", she said, \"bye\".",
|
|
854
|
+
"out": "“Hi”, she said, “bye”.",
|
|
855
|
+
"note": "A closing mark whose right neighbour is in CLOSEISH (`,` and `.`) is canClose. No spacing is touched: quotes.md §3.5 makes every edit 1:1."
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
"id": "en-us-quotes-quotation-at-clause-end",
|
|
859
|
+
"rule": "quotes",
|
|
860
|
+
"mode": "text",
|
|
861
|
+
"in": "A quote at the end: \"done.\"",
|
|
862
|
+
"out": "A quote at the end: “done.”",
|
|
863
|
+
"note": "The opening mark's left neighbour is a space and the closing mark's right neighbour is NONE."
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
"id": "en-us-quotes-foreign-glyphs-untouched",
|
|
867
|
+
"rule": "quotes",
|
|
868
|
+
"mode": "text",
|
|
869
|
+
"in": "He wrote ‘this’ and «that» and „so“.",
|
|
870
|
+
"out": "He wrote “this” and “that” and “so”.",
|
|
871
|
+
"note": "quotes.md §4, updated for spec 0.3.0 mandate 1: withdrawn. Every quote glyph — this locale's own or foreign — is now a re-typesetting candidate. All three pairs here are depth 1 and are rewritten to en-US's own primary pair. Compare en-us-quotes-existing-curly-ignored, where the curly pair happens to already be en-US's own primary glyphs."
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
"id": "en-us-quotes-already-correct",
|
|
875
|
+
"rule": "quotes",
|
|
876
|
+
"mode": "text",
|
|
877
|
+
"in": "“Yes,” she said—it was 1914–1918, about 5 km.",
|
|
878
|
+
"out": "“Yes,” she said—it was 1914–1918, about 5 km.",
|
|
879
|
+
"note": "Round-trip: text that is already correct in every rule comes out byte-identical (ARCHITECTURE.md §6.3 item 4). NOTE that \"already correct\" now includes the two U+2060 word joiners around the tight range (dashes.md §3.3.1) — a bare `1914–1918` is NOT a fixed point, because the rule still has the binding to add."
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
"id": "en-us-apostrophe-contraction",
|
|
883
|
+
"rule": "apostrophe",
|
|
884
|
+
"mode": "text",
|
|
885
|
+
"in": "don't",
|
|
886
|
+
"out": "don’t",
|
|
887
|
+
"note": "apostrophe.md §6 case 1: letters on both sides."
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
"id": "en-us-apostrophe-elision",
|
|
891
|
+
"rule": "apostrophe",
|
|
892
|
+
"mode": "text",
|
|
893
|
+
"in": "l'été",
|
|
894
|
+
"out": "l’été",
|
|
895
|
+
"note": "apostrophe.md §6 case 2."
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
"id": "en-us-apostrophe-possessive-plural",
|
|
899
|
+
"rule": "apostrophe",
|
|
900
|
+
"mode": "text",
|
|
901
|
+
"in": "the dogs' bowls",
|
|
902
|
+
"out": "the dogs’ bowls",
|
|
903
|
+
"note": "apostrophe.md §6 case 3: `quotes` leaves the mark unmatched, then case 3 converts it."
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
"id": "en-us-apostrophe-decade-elision",
|
|
907
|
+
"rule": "apostrophe",
|
|
908
|
+
"mode": "text",
|
|
909
|
+
"in": "Back in the '90s",
|
|
910
|
+
"out": "Back in the ’90s",
|
|
911
|
+
"note": "apostrophe.md §6 case 4: U+2019, never U+2018."
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
"id": "en-us-apostrophe-leading-elision",
|
|
915
|
+
"rule": "apostrophe",
|
|
916
|
+
"mode": "text",
|
|
917
|
+
"in": "'Tis the season",
|
|
918
|
+
"out": "’Tis the season",
|
|
919
|
+
"note": "apostrophe.md §6 case 5."
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
"id": "en-us-apostrophe-foot-and-inch",
|
|
923
|
+
"rule": "apostrophe",
|
|
924
|
+
"mode": "text",
|
|
925
|
+
"in": "He is 6' 2\" tall.",
|
|
926
|
+
"out": "He is 6' 2\" tall.",
|
|
927
|
+
"note": "apostrophe.md §6 case 6 and quotes.md §4: the prime guard keeps the foot mark, and the inch mark is an unmatched quote candidate."
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
"id": "en-us-apostrophe-feet-inches-tight",
|
|
931
|
+
"rule": "apostrophe",
|
|
932
|
+
"mode": "text",
|
|
933
|
+
"in": "6'2\"",
|
|
934
|
+
"out": "6'2\"",
|
|
935
|
+
"note": "apostrophe.md §6 case 7: digit left, digit right → prime guard."
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
"id": "en-us-apostrophe-isolated-mark",
|
|
939
|
+
"rule": "apostrophe",
|
|
940
|
+
"mode": "text",
|
|
941
|
+
"in": "a ' b",
|
|
942
|
+
"out": "a ' b",
|
|
943
|
+
"note": "apostrophe.md §6 case 9 and §7.4: an isolated straight mark survives into the output on purpose."
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
"id": "en-us-apostrophe-doubled-mark",
|
|
947
|
+
"rule": "apostrophe",
|
|
948
|
+
"mode": "text",
|
|
949
|
+
"in": "dogs''",
|
|
950
|
+
"out": "dogs''",
|
|
951
|
+
"note": "apostrophe.md §6 case 12: neither mark has the neighbour class any converting case requires; `quotes` vetoed both in pass 1."
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
"id": "en-us-apostrophe-two-medial",
|
|
955
|
+
"rule": "apostrophe",
|
|
956
|
+
"mode": "text",
|
|
957
|
+
"in": "O'Brien's",
|
|
958
|
+
"out": "O’Brien’s",
|
|
959
|
+
"note": "apostrophe.md §6 case 13."
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
"id": "en-us-apostrophe-three-medial",
|
|
963
|
+
"rule": "apostrophe",
|
|
964
|
+
"mode": "text",
|
|
965
|
+
"in": "Ma'am, it's 5 o'clock",
|
|
966
|
+
"out": "Ma’am, it’s 5 o’clock",
|
|
967
|
+
"note": "apostrophe.md §6 case 14."
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
"id": "en-us-apostrophe-digit-then-letter",
|
|
971
|
+
"rule": "apostrophe",
|
|
972
|
+
"mode": "text",
|
|
973
|
+
"in": "The 1990's were loud",
|
|
974
|
+
"out": "The 1990’s were loud",
|
|
975
|
+
"note": "apostrophe.md §6 case 8: the prime guard does not fire because a letter follows."
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
"id": "en-us-apostrophe-already-curly",
|
|
979
|
+
"rule": "apostrophe",
|
|
980
|
+
"mode": "text",
|
|
981
|
+
"in": "“He said ’tis so,” she noted.",
|
|
982
|
+
"out": "“He said ’tis so,” she noted.",
|
|
983
|
+
"note": "apostrophe.md §6 case 10: no U+0027 remains, and existing curly glyphs are not candidates for `quotes` either."
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
"id": "en-us-apostrophe-modifier-letters-kept",
|
|
987
|
+
"rule": "apostrophe",
|
|
988
|
+
"mode": "text",
|
|
989
|
+
"in": "Hawai'i, ʼokina ʼ and ʼ kept",
|
|
990
|
+
"out": "Hawai’i, ʼokina ʼ and ʼ kept",
|
|
991
|
+
"note": "apostrophe.md §4 and §7.5: U+02BC is a letter in several orthographies and is never converted; the U+0027 in `Hawai'i` is converted by case 2, which is the documented wrong-but-conventional behaviour."
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
"id": "en-us-apostrophe-backtick-and-acute",
|
|
995
|
+
"rule": "apostrophe",
|
|
996
|
+
"mode": "text",
|
|
997
|
+
"in": "The ``LaTeX'' idiom and a ´mark´.",
|
|
998
|
+
"out": "The ``LaTeX'' idiom and a ´mark´.",
|
|
999
|
+
"note": "apostrophe.md §4 / quotes.md §4: U+0060 and U+00B4 are not candidates, and the LaTeX `''` idiom is vetoed by the same-kind adjacency rule."
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
"id": "en-us-symbols-copyright",
|
|
1003
|
+
"rule": "symbols",
|
|
1004
|
+
"mode": "text",
|
|
1005
|
+
"in": "Copyright (c) 2026 Iurii Rogulia",
|
|
1006
|
+
"out": "Copyright © 2026 Iurii Rogulia",
|
|
1007
|
+
"note": "symbols.md §6 case 1."
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
"id": "en-us-symbols-tm-and-registered",
|
|
1011
|
+
"rule": "symbols",
|
|
1012
|
+
"mode": "text",
|
|
1013
|
+
"in": "Acme(tm) and Acme (R)",
|
|
1014
|
+
"out": "Acme™ and Acme ®",
|
|
1015
|
+
"note": "symbols.md §6 case 2: the (tm) rows are exempt from guard S1."
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"id": "en-us-symbols-s1-asymmetry",
|
|
1019
|
+
"rule": "symbols",
|
|
1020
|
+
"mode": "text",
|
|
1021
|
+
"in": "f(c) and f(tm)",
|
|
1022
|
+
"out": "f(c) and f™",
|
|
1023
|
+
"note": "symbols.md §6 case 2b: S1 applies to the (c)/(r) rows only. `f(tm)` becoming `f™` is deliberate."
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"id": "en-us-symbols-optional-first-letter",
|
|
1027
|
+
"rule": "symbols",
|
|
1028
|
+
"mode": "text",
|
|
1029
|
+
"in": "The (r)evolution will not be televised",
|
|
1030
|
+
"out": "The (r)evolution will not be televised",
|
|
1031
|
+
"note": "symbols.md §6 case 3: guard S2."
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
"id": "en-us-symbols-call-position",
|
|
1035
|
+
"rule": "symbols",
|
|
1036
|
+
"mode": "text",
|
|
1037
|
+
"in": "f(c) returns c",
|
|
1038
|
+
"out": "f(c) returns c",
|
|
1039
|
+
"note": "symbols.md §6 case 4: guard S1."
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
"id": "en-us-symbols-nesting",
|
|
1043
|
+
"rule": "symbols",
|
|
1044
|
+
"mode": "text",
|
|
1045
|
+
"in": "((c))",
|
|
1046
|
+
"out": "((c))",
|
|
1047
|
+
"note": "symbols.md §6 case 11: guard S3."
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
"id": "en-us-symbols-adjacent-signs",
|
|
1051
|
+
"rule": "symbols",
|
|
1052
|
+
"mode": "text",
|
|
1053
|
+
"in": "(c)(r)",
|
|
1054
|
+
"out": "©(r)",
|
|
1055
|
+
"note": "symbols.md §5: S1 lists U+00A9/U+00AE/U+2122 so that this converges in one run rather than two."
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
"id": "en-us-symbols-multiplication",
|
|
1059
|
+
"rule": "symbols",
|
|
1060
|
+
"mode": "text",
|
|
1061
|
+
"in": "A 3x5 card, 10 x 20 grid",
|
|
1062
|
+
"out": "A 3×5 card, 10 × 20 grid",
|
|
1063
|
+
"note": "symbols.md §6 case 5: the tight and the symmetric-spaced form in one string. The trailing word is deliberately not unit-like — §6 now requires each row to exercise only the rule it documents, so this case asserts `symbols` and nothing else."
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
"id": "en-us-symbols-multiplication-with-unit",
|
|
1067
|
+
"rule": "symbols",
|
|
1068
|
+
"mode": "text",
|
|
1069
|
+
"in": "A 3x5 card, 10 x 20 cm",
|
|
1070
|
+
"out": "A 3×5 card, 10 × 20 cm",
|
|
1071
|
+
"note": "PIPELINE INTERACTION, not a §6 row: the earlier wording of case 5 ended in `cm`. The U+00D7 conversions come from `symbols`, but the U+00A0 before `cm` comes from `nbsp` (order 70) binding a beforeUnits entry to the preceding number. Kept as a separate case because the `symbols` + `nbsp.beforeUnits` interaction is real and is covered nowhere else; a port whose `symbols` is correct but whose N5 boundary test is not will pass the row above and fail this one."
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
"id": "en-us-symbols-hexadecimal",
|
|
1075
|
+
"rule": "symbols",
|
|
1076
|
+
"mode": "text",
|
|
1077
|
+
"in": "Colour 0x1F, mask 0xFF, offset 0x10",
|
|
1078
|
+
"out": "Colour 0x1F, mask 0xFF, offset 0x10",
|
|
1079
|
+
"note": "symbols.md §6 case 6: guards M3 and M4."
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
"id": "en-us-symbols-resolution",
|
|
1083
|
+
"rule": "symbols",
|
|
1084
|
+
"mode": "text",
|
|
1085
|
+
"in": "Resolution 1920x1080",
|
|
1086
|
+
"out": "Resolution 1920×1080",
|
|
1087
|
+
"note": "symbols.md §6 case 7."
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
"id": "en-us-symbols-variable-x",
|
|
1091
|
+
"rule": "symbols",
|
|
1092
|
+
"mode": "text",
|
|
1093
|
+
"in": "Let x = 5 and solve for x",
|
|
1094
|
+
"out": "Let x = 5 and solve for x",
|
|
1095
|
+
"note": "symbols.md §6 case 8: no digit neighbour."
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
"id": "en-us-symbols-chain",
|
|
1099
|
+
"rule": "symbols",
|
|
1100
|
+
"mode": "text",
|
|
1101
|
+
"in": "A 2x3x4 box",
|
|
1102
|
+
"out": "A 2×3×4 box",
|
|
1103
|
+
"note": "symbols.md §6 case 5e: the Latin chain, both links converted in ONE PASS. The branch is a scan of a whole chain `DIGIT+ (X DIGIT+)+` (§3.3), not a test on one letter between two numbers, and the former guard M5 was REMOVED rather than extended (§3.3 step 7). A pairwise implementation converts one operator per pass — `2×3x4`, then `2×3×4` — which is the exact non-idempotency §7.10 measures in Lebedev's Typograf on `5х4х3`. That failure is invisible to a test that runs the transform only once, which is why the runner also asserts transform(out) == out."
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
"id": "en-us-symbols-chain-cyrillic",
|
|
1107
|
+
"rule": "symbols",
|
|
1108
|
+
"mode": "text",
|
|
1109
|
+
"in": "5х4х3",
|
|
1110
|
+
"out": "5×4×3",
|
|
1111
|
+
"note": "symbols.md §6 case 5d: an all-Cyrillic chain (U+0445), both links converted in one pass. This is the literal input §7.10 records Typograf turning into `5×4х3` on the first pass and `5×4×3` only on the second — a document that never converges under repeated processing."
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
"id": "en-us-symbols-chain-mixed-alphabets",
|
|
1115
|
+
"rule": "symbols",
|
|
1116
|
+
"mode": "text",
|
|
1117
|
+
"in": "5x4х3",
|
|
1118
|
+
"out": "5×4×3",
|
|
1119
|
+
"note": "symbols.md §6 case 5f: one Latin `x`, one Cyrillic `х`, ONE chain, both links convert. The four MUL-LETTER members are interchangeable and no guard distinguishes them. Declining on mixture would make behaviour depend on which keyboard layout the author's finger slipped to — undiscoverable from the output, because the two letters are visually identical in every font."
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
"id": "en-us-symbols-chain-three-links",
|
|
1123
|
+
"rule": "symbols",
|
|
1124
|
+
"mode": "text",
|
|
1125
|
+
"in": "Стол 120х80х75",
|
|
1126
|
+
"out": "Стол 120×80×75",
|
|
1127
|
+
"note": "symbols.md §6 case 5g: a three-link chain over multi-digit runs. M2 and M3 apply to the OUTER boundaries of the chain, not per link — in the pairwise form they applied per pair, and the letter on the far side of a middle digit run was another MUL-LETTER, which is what made a chain reject itself."
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
"id": "en-us-symbols-chain-spaced",
|
|
1131
|
+
"rule": "symbols",
|
|
1132
|
+
"mode": "text",
|
|
1133
|
+
"in": "10 x 20 x 30",
|
|
1134
|
+
"out": "10 × 20 × 30",
|
|
1135
|
+
"note": "symbols.md §6 case 5h: a spaced chain. `sp = 1` uniformly and each side keeps its own space code point."
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
"id": "en-us-symbols-chain-mixed-spacing-declined",
|
|
1139
|
+
"rule": "symbols",
|
|
1140
|
+
"mode": "text",
|
|
1141
|
+
"in": "5x4 x 3",
|
|
1142
|
+
"out": "5x4 x 3",
|
|
1143
|
+
"note": "symbols.md §6 case 5i: guard M1 requires the spacing to be symmetric AND uniform across every link. Link 1 is tight, link 2 is spaced, so the chain is ambiguous input and is declined WHOLE rather than half-converted."
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
"id": "en-us-symbols-chain-resumes-after-sign",
|
|
1147
|
+
"rule": "symbols",
|
|
1148
|
+
"mode": "text",
|
|
1149
|
+
"in": "5×4x3",
|
|
1150
|
+
"out": "5×4×3",
|
|
1151
|
+
"note": "symbols.md §6 case 5j: U+00D7 is not in MUL-LETTER, so an already-converted operator ENDS a chain. This reads as the digit run `5`, then the sign, then the fresh chain `4x3`, which converts. Sub-chains do not need to be joined."
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
"id": "en-us-symbols-cyrillic-single-link",
|
|
1155
|
+
"rule": "symbols",
|
|
1156
|
+
"mode": "text",
|
|
1157
|
+
"in": "Размер 5х4",
|
|
1158
|
+
"out": "Размер 5×4",
|
|
1159
|
+
"note": "symbols.md §6 case 5a: Cyrillic U+0445 between two ASCII digits. Identical in appearance to the Latin form and INVISIBLE IN A DIFF, so the M4 dogfooding review cannot catch it by eye — which is §3.1's argument for doing the conversion by algorithm at all."
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
"id": "en-us-symbols-cyrillic-capital",
|
|
1163
|
+
"rule": "symbols",
|
|
1164
|
+
"mode": "text",
|
|
1165
|
+
"in": "Размер 5Х4",
|
|
1166
|
+
"out": "Размер 5×4",
|
|
1167
|
+
"note": "symbols.md §6 case 5b: Cyrillic capital U+0425. MUL-LETTER is exactly four enumerated code points and is never case-folded (ARCHITECTURE.md §4.4)."
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
"id": "en-us-symbols-cyrillic-letter-neighbours",
|
|
1171
|
+
"rule": "symbols",
|
|
1172
|
+
"mode": "text",
|
|
1173
|
+
"in": "хорошо и их",
|
|
1174
|
+
"out": "хорошо и их",
|
|
1175
|
+
"note": "symbols.md §6 case 5c: Cyrillic `х` as an ordinary letter. The chain scan starts only at a digit run, so a `х` with letter neighbours is never even examined. This is the case that makes the unconditional Cyrillic addition safe: the risk is carried by the NUMERIC context, not by the language, so the rule needs no locale data."
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
"id": "en-us-symbols-plus-minus-tight",
|
|
1179
|
+
"rule": "symbols",
|
|
1180
|
+
"mode": "text",
|
|
1181
|
+
"in": "Tolerance +/-5 points",
|
|
1182
|
+
"out": "Tolerance ±5 points",
|
|
1183
|
+
"note": "symbols.md §6 case 5k: the literal three code points U+002B U+002F U+002D with an immediately following digit. `points` is deliberately not a beforeUnits entry, so this row asserts `symbols` alone."
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
"id": "en-us-symbols-plus-minus-spaced",
|
|
1187
|
+
"rule": "symbols",
|
|
1188
|
+
"mode": "text",
|
|
1189
|
+
"in": "Tolerance +/- 5 points",
|
|
1190
|
+
"out": "Tolerance ± 5 points",
|
|
1191
|
+
"note": "symbols.md §6 case 5l: one intervening U+0020 is allowed, and the space itself is left exactly as it was — the edit replaces only the three code points of `+/-`."
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
"id": "en-us-symbols-plus-minus-with-unit",
|
|
1195
|
+
"rule": "symbols",
|
|
1196
|
+
"mode": "text",
|
|
1197
|
+
"in": "Tolerance +/-5 mm",
|
|
1198
|
+
"out": "Tolerance ±5 mm",
|
|
1199
|
+
"note": "PIPELINE INTERACTION, not a §6 row: the earlier wording of case 5k ended in `mm`. The U+00B1 comes from `symbols`; the U+00A0 comes from `nbsp` N5 binding `mm`, a beforeUnits entry, to the digit run before it. Kept so the interaction stays covered now that the normative row no longer carries a unit."
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
"id": "en-us-symbols-plus-minus-spaced-with-unit",
|
|
1203
|
+
"rule": "symbols",
|
|
1204
|
+
"mode": "text",
|
|
1205
|
+
"in": "Tolerance +/- 5 mm",
|
|
1206
|
+
"out": "Tolerance ± 5 mm",
|
|
1207
|
+
"note": "PIPELINE INTERACTION, not a §6 row: the earlier wording of case 5l. Note that the U+0020 between `±` and `5` survives while the one between `5` and `mm` becomes U+00A0 — the first is inside the plus-minus edit's neighbourhood and untouched by both rules, the second is N5's."
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
"id": "en-us-symbols-plus-minus-after-digit",
|
|
1211
|
+
"rule": "symbols",
|
|
1212
|
+
"mode": "text",
|
|
1213
|
+
"in": "Погрешность 5+/-3",
|
|
1214
|
+
"out": "Погрешность 5±3",
|
|
1215
|
+
"note": "symbols.md §6 case 5m: a digit on the left is not an obstacle — guard F1 rejects only U+005B."
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
"id": "en-us-symbols-plus-minus-prose-declined",
|
|
1219
|
+
"rule": "symbols",
|
|
1220
|
+
"mode": "text",
|
|
1221
|
+
"in": "lines marked +/- were edited",
|
|
1222
|
+
"out": "lines marked +/- were edited",
|
|
1223
|
+
"note": "symbols.md §6 case 5n: guard F2 requires a following digit (modulo one space). Prose ABOUT the characters is not converted."
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
"id": "en-us-symbols-plus-minus-character-class-declined",
|
|
1227
|
+
"rule": "symbols",
|
|
1228
|
+
"mode": "text",
|
|
1229
|
+
"in": "match [+/-] once",
|
|
1230
|
+
"out": "match [+/-] once",
|
|
1231
|
+
"note": "symbols.md §6 case 5o: guard F1. `[+/-]` is a regular-expression character class, the one code-like context that survives into prose about code — and in `text` mode nothing is skipped."
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
"id": "en-us-symbols-plus-minus-bare-declined",
|
|
1235
|
+
"rule": "symbols",
|
|
1236
|
+
"mode": "text",
|
|
1237
|
+
"in": "Range 5+-3",
|
|
1238
|
+
"out": "Range 5+-3",
|
|
1239
|
+
"note": "symbols.md §6 case 5p: bare `+-` is NEVER converted, in any context. Only `+/-` is unambiguous; `+-` occurs in diff listings, ASCII table borders, regex classes and as two adjacent operators in source. §7.11 exists so this is not added later \"for symmetry\"."
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
"id": "en-us-symbols-asymmetric-x",
|
|
1243
|
+
"rule": "symbols",
|
|
1244
|
+
"mode": "text",
|
|
1245
|
+
"in": "Version 1080x",
|
|
1246
|
+
"out": "Version 1080x",
|
|
1247
|
+
"note": "symbols.md §6 case 12: guard M1."
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
"id": "en-us-symbols-already-signs",
|
|
1251
|
+
"rule": "symbols",
|
|
1252
|
+
"mode": "text",
|
|
1253
|
+
"in": "© 2026, ® and ™",
|
|
1254
|
+
"out": "© 2026, ® and ™",
|
|
1255
|
+
"note": "symbols.md §6 case 13: existing signs are not candidates."
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
"id": "en-us-symbols-nbsp-preserved-around-times",
|
|
1259
|
+
"rule": "symbols",
|
|
1260
|
+
"mode": "text",
|
|
1261
|
+
"in": "10 × 20",
|
|
1262
|
+
"out": "10 × 20",
|
|
1263
|
+
"note": "symbols.md §6 case 10: already converted, and each side keeps the exact space code point it had."
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
"id": "en-us-symbols-parenthesised-letters",
|
|
1267
|
+
"rule": "symbols",
|
|
1268
|
+
"mode": "text",
|
|
1269
|
+
"in": "(s)he chose (a) and (e)",
|
|
1270
|
+
"out": "(s)he chose (a) and (e)",
|
|
1271
|
+
"note": "symbols.md §4: the trademark table is exhaustive."
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
"id": "en-us-symbols-ascii-art",
|
|
1275
|
+
"rule": "symbols",
|
|
1276
|
+
"mode": "text",
|
|
1277
|
+
"in": "Smile (x) and (^_^)",
|
|
1278
|
+
"out": "Smile (x) and (^_^)",
|
|
1279
|
+
"note": "symbols.md §4: no table entry and no numeral context."
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
"id": "en-us-nbsp-unit",
|
|
1283
|
+
"rule": "nbsp",
|
|
1284
|
+
"mode": "text",
|
|
1285
|
+
"in": "It is 20 km to the coast",
|
|
1286
|
+
"out": "It is 20 km to the coast",
|
|
1287
|
+
"note": "nbsp N5: `km` is in this locale's beforeUnits, the separator is U+0020 and a digit run precedes it."
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
"id": "en-us-nbsp-unit-no-space",
|
|
1291
|
+
"rule": "nbsp",
|
|
1292
|
+
"mode": "text",
|
|
1293
|
+
"in": "It is 20km to the coast",
|
|
1294
|
+
"out": "It is 20km to the coast",
|
|
1295
|
+
"note": "nbsp.md §6 case 17 and §7.2: N5 converts, it never inserts."
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
"id": "en-us-nbsp-unit-already-bound",
|
|
1299
|
+
"rule": "nbsp",
|
|
1300
|
+
"mode": "text",
|
|
1301
|
+
"in": "It is 20 km to the coast",
|
|
1302
|
+
"out": "It is 20 km to the coast",
|
|
1303
|
+
"note": "nbsp.md §5: N5's already-correct branch emits nothing."
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
"id": "en-us-nbsp-percent-spaced",
|
|
1307
|
+
"rule": "nbsp",
|
|
1308
|
+
"mode": "text",
|
|
1309
|
+
"in": "Prices rose 5 %",
|
|
1310
|
+
"out": "Prices rose 5 %",
|
|
1311
|
+
"note": "`%` is in beforeUnits. Chicago sets percentages closed up, so this only binds a space the author already typed."
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
"id": "en-us-nbsp-percent-closed",
|
|
1315
|
+
"rule": "nbsp",
|
|
1316
|
+
"mode": "text",
|
|
1317
|
+
"in": "Prices rose 5%",
|
|
1318
|
+
"out": "Prices rose 5%",
|
|
1319
|
+
"note": "Never inserts."
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
"id": "en-us-nbsp-formula-not-a-unit",
|
|
1323
|
+
"rule": "nbsp",
|
|
1324
|
+
"mode": "text",
|
|
1325
|
+
"in": "H2 O is not a value",
|
|
1326
|
+
"out": "H2 O is not a value",
|
|
1327
|
+
"note": "nbsp.md §6 case 18: N5 step 4 rejects a digit run preceded by a letter."
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
"id": "en-us-nbsp-initials",
|
|
1331
|
+
"rule": "nbsp",
|
|
1332
|
+
"mode": "text",
|
|
1333
|
+
"in": "E. B. White wrote it",
|
|
1334
|
+
"out": "E. B. White wrote it",
|
|
1335
|
+
"note": "nbsp N7 clause C1, twice: `E.`→`B.` is the between-initials case (unconditional in every mode), `B.`→`White` binds because `chain` mode (spec 0.6.0, cited to Chicago's own \"two or more initials\") confirms `B.` is itself preceded by another initial (`E.`). en-GB has initialBinding: \"none\", so the same input is unchanged there."
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
"id": "en-us-nbsp-initial-sentence-boundary-declined",
|
|
1339
|
+
"rule": "nbsp",
|
|
1340
|
+
"mode": "text",
|
|
1341
|
+
"in": "take the top N. It runs across four relationship types",
|
|
1342
|
+
"out": "take the top N. It runs across four relationship types",
|
|
1343
|
+
"note": "The exact M4 reject witness (Pass A, reviewed run). `N.` is a lone initial with no preceding initial (the character before it is a space from \"top\", not another initial's full stop), so `chain` mode's C1 does not bind the space before `It` — nbsp.md §3.9, spec 0.6.0. Under the old boolean `bindInitials: true`, C1 checked only \"is the right side uppercase,\" which fired here exactly as it does for a genuine name, producing a false positive across a sentence boundary."
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
"id": "en-us-nbsp-isolated-initial-declined",
|
|
1347
|
+
"rule": "nbsp",
|
|
1348
|
+
"mode": "text",
|
|
1349
|
+
"in": "A. Smith built the original prototype.",
|
|
1350
|
+
"out": "A. Smith built the original prototype.",
|
|
1351
|
+
"note": "nbsp.md §3.9: an isolated single initial does not bind to a following word under `chain` mode — the deliberate false negative this mode accepts, matching Chicago's own \"two or more initials\" wording rather than the broader shape the old boolean allowed."
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
"id": "en-us-nbsp-initials-already-bound",
|
|
1355
|
+
"rule": "nbsp",
|
|
1356
|
+
"mode": "text",
|
|
1357
|
+
"in": "E. B. White wrote it",
|
|
1358
|
+
"out": "E. B. White wrote it",
|
|
1359
|
+
"note": "N7's already-correct branch."
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
"id": "en-us-nbsp-empty-lists",
|
|
1363
|
+
"rule": "nbsp",
|
|
1364
|
+
"mode": "text",
|
|
1365
|
+
"in": "Really? Yes: here; now!",
|
|
1366
|
+
"out": "Really? Yes: here; now!",
|
|
1367
|
+
"note": "beforePunctuation, narrowBeforePunctuation, afterShortWords, abbreviations, beforeNumber, beforeWord and afterSymbols are all empty for this locale, so nothing binds."
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
"id": "en-us-nbsp-quotes-innerspace-none",
|
|
1371
|
+
"rule": "nbsp",
|
|
1372
|
+
"mode": "text",
|
|
1373
|
+
"in": "She said “hello”.",
|
|
1374
|
+
"out": "She said “hello”.",
|
|
1375
|
+
"note": "nbsp.md §3.10: quotes.innerSpace is `none` for both pairs, so N8 does nothing at all — in particular it does not remove or add a space."
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
"id": "en-us-nbsp-prose-everything",
|
|
1379
|
+
"rule": "nbsp",
|
|
1380
|
+
"mode": "text",
|
|
1381
|
+
"in": "\"We walked 5 km -- maybe more...\" she said, \"in 1914-1918 weather.\"",
|
|
1382
|
+
"out": "“We walked 5 km—maybe more…” she said, “in 1914–1918 weather.”",
|
|
1383
|
+
"note": "Real prose exercising quotes, dashes, ellipsis and a number-plus-unit at once. The same input appears in en-GB.json with single-first quotes and a spaced en dash. spec 0.5.0: explicit `rules: {ranges: true}` added — this fixture's embedded numeric range (e.g. 1914-1918) is off by default now that range detection moved to the `ranges` rule; the fixture keeps its original comprehensive nbsp-positioning coverage by opting in explicitly rather than dropping the range from the sentence.",
|
|
1384
|
+
"rules": {
|
|
1385
|
+
"ranges": true
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
"id": "en-us-nbsp-prose-report",
|
|
1390
|
+
"rule": "nbsp",
|
|
1391
|
+
"mode": "text",
|
|
1392
|
+
"in": "The report -- published 2026-08-15 -- says \"growth was 5 %\", but...",
|
|
1393
|
+
"out": "The report—published 2026-08-15—says “growth was 5 %”, but…",
|
|
1394
|
+
"note": "Second prose case: the ISO date is protected by the chain guard while the parenthetical dashes around it convert."
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
"id": "en-us-quotes-030-nesting-swap",
|
|
1398
|
+
"rule": "quotes",
|
|
1399
|
+
"mode": "text",
|
|
1400
|
+
"in": "'He said \"no\" to me,' she noted.",
|
|
1401
|
+
"out": "“He said ‘no’ to me,” she noted.",
|
|
1402
|
+
"note": "quotes.md §6 row 3: depth, not width, selects the pair; the certification gate certifies despite both streams swapping width."
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
"id": "en-us-quotes-030-first-mark-survives",
|
|
1406
|
+
"rule": "quotes",
|
|
1407
|
+
"mode": "text",
|
|
1408
|
+
"in": "He said \"hi. She said \"bye.\"",
|
|
1409
|
+
"out": "He said \"hi. She said “bye.”",
|
|
1410
|
+
"note": "quotes.md §6 row 4: the first mark's closeRight is the letter h, so it is canOpen only and cannot swallow the real quotation."
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
"id": "en-us-quotes-030-medial-vetoes",
|
|
1414
|
+
"rule": "quotes",
|
|
1415
|
+
"mode": "text",
|
|
1416
|
+
"in": "Don't touch it — it's the '90s.",
|
|
1417
|
+
"out": "Don’t touch it—it’s the ’90s.",
|
|
1418
|
+
"note": "quotes.md §6 row 5: two medial vetoes and an unmatched leading elision, none claimed by `quotes`. Output shown is the full pipeline's: `apostrophe` renders all three straight marks as U+2019 and `dashes` tightens the spaced em dash, neither of which this case is really about — it exists to pin that `quotes` itself declines all three marks."
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
"id": "en-us-quotes-030-foot-inch",
|
|
1422
|
+
"rule": "quotes",
|
|
1423
|
+
"mode": "text",
|
|
1424
|
+
"in": "He is 6' 2\" tall.",
|
|
1425
|
+
"out": "He is 6' 2\" tall.",
|
|
1426
|
+
"note": "quotes.md §6 row 6: both marks canClose only, both stacks empty."
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
"id": "en-us-quotes-030-run-of-five",
|
|
1430
|
+
"rule": "quotes",
|
|
1431
|
+
"mode": "text",
|
|
1432
|
+
"in": "\"\"\"a\"\"",
|
|
1433
|
+
"out": "\"\"\"a\"\"",
|
|
1434
|
+
"note": "quotes.md §6 row 7b: every mark has an identical literal neighbour, V1 drops all five."
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
"id": "en-us-quotes-030-vetoed-inner-pair",
|
|
1438
|
+
"rule": "quotes",
|
|
1439
|
+
"mode": "text",
|
|
1440
|
+
"in": "\"a \"\" b\"",
|
|
1441
|
+
"out": "“a \"\" b”",
|
|
1442
|
+
"note": "quotes.md §6 row 7c: the inner \"\" is vetoed; the outer pair converts; the gate certifies."
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
"id": "en-us-quotes-030-already-correct-nesting",
|
|
1446
|
+
"rule": "quotes",
|
|
1447
|
+
"mode": "text",
|
|
1448
|
+
"in": "“He said ‘no’ twice.”",
|
|
1449
|
+
"out": "“He said ‘no’ twice.”",
|
|
1450
|
+
"note": "quotes.md §6 row A: already-correct nesting is a no-op — required scenario (a), the case 0.1.0 §3.8 declared impossible to even attempt."
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
"id": "en-us-quotes-030-mandate2-leading-space",
|
|
1454
|
+
"rule": "quotes",
|
|
1455
|
+
"mode": "text",
|
|
1456
|
+
"in": "\" hello\"",
|
|
1457
|
+
"out": "“hello”",
|
|
1458
|
+
"note": "quotes.md §6 row M2a: mandate 2. canOpen reads Rskip = h; the inner run's landing is h ∈ ALNUM, so it is deleted."
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
"id": "en-us-quotes-030-mandate2-trailing-space",
|
|
1462
|
+
"rule": "quotes",
|
|
1463
|
+
"mode": "text",
|
|
1464
|
+
"in": "\"hello \"",
|
|
1465
|
+
"out": "“hello”",
|
|
1466
|
+
"note": "quotes.md §6 row M2b: the mirror case, by the same Lskip/landing mechanism."
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
"id": "en-us-quotes-030-declined-deletion-dash",
|
|
1470
|
+
"rule": "quotes",
|
|
1471
|
+
"mode": "text",
|
|
1472
|
+
"in": "\" --x\"",
|
|
1473
|
+
"out": "“ --x”",
|
|
1474
|
+
"note": "quotes.md §6 row D1, and the pinned pipeline-idempotency.md §6 witness \" --x\": the inner run's landing is -, outside DELETE-LANDING, so the deletion is declined while the glyphs still convert. Deleting would revive the dash token for `dashes` on the next pass (I₃)."
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
"id": "en-us-quotes-030-empty-pair-space",
|
|
1478
|
+
"rule": "quotes",
|
|
1479
|
+
"mode": "text",
|
|
1480
|
+
"in": "\" \"",
|
|
1481
|
+
"out": "\" \"",
|
|
1482
|
+
"note": "quotes.md §3.3's vacuity condition and the pinned pipeline-idempotency.md §6 witness \" \": a pair enclosing nothing but spaces is never formed."
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
"id": "en-us-quotes-030-empty-pair-guillemets",
|
|
1486
|
+
"rule": "quotes",
|
|
1487
|
+
"mode": "text",
|
|
1488
|
+
"in": "«»",
|
|
1489
|
+
"out": "«»",
|
|
1490
|
+
"note": "quotes.md §3.3's vacuity condition and the pinned pipeline-idempotency.md §6 witness «»: two different WIDE code points with nothing between them do not pair."
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
"id": "en-us-quotes-030-elision-vs-closer-curly",
|
|
1494
|
+
"rule": "quotes",
|
|
1495
|
+
"mode": "text",
|
|
1496
|
+
"in": "’90s were fun,’ he said",
|
|
1497
|
+
"out": "“90s were fun,” he said",
|
|
1498
|
+
"note": "quotes.md §4's residual-risk note and §6 row E1: an already-curly leading elision can be paired as an opener by a later unrelated closer. Verified, not hand-derived: this is the same pre-existing rock 'n' roll-family ambiguity as row E1s, now also reachable via curly input under mandate 1. No U+2019-specific restriction is applied (quotes.md §5 Corollary A1) because it would break fi/sv's same-glyph secondary pair."
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
"id": "en-us-quotes-030-elision-vs-closer-straight",
|
|
1502
|
+
"rule": "quotes",
|
|
1503
|
+
"mode": "text",
|
|
1504
|
+
"in": "'90s were fun,' he said",
|
|
1505
|
+
"out": "“90s were fun,” he said",
|
|
1506
|
+
"note": "quotes.md §6 row E1s, the straight-input control for E1: the identical corruption already existed under spec 0.1.0's per-kind stacks (both marks are SQ and pair on the same stack), confirming E1 is not new damage introduced by mandate 1."
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
"id": "en-us-markdown-commonmark-prose",
|
|
1510
|
+
"rule": "quotes",
|
|
1511
|
+
"mode": "markdown",
|
|
1512
|
+
"dialect": "commonmark",
|
|
1513
|
+
"in": "Is this \"polytypo\"?\n",
|
|
1514
|
+
"out": "Is this “polytypo”?\n",
|
|
1515
|
+
"note": "modes.md §3.5: ordinary prose is a processable span and converts exactly as text mode does."
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
"id": "en-us-markdown-commonmark-inline-code",
|
|
1519
|
+
"rule": "quotes",
|
|
1520
|
+
"mode": "markdown",
|
|
1521
|
+
"dialect": "commonmark",
|
|
1522
|
+
"in": "Use `\"raw\"` here but not \"this\" here.\n",
|
|
1523
|
+
"out": "Use `\"raw\"` here but not “this” here.\n",
|
|
1524
|
+
"note": "modes.md §3.7.3: an inline code span, including its backticks, is skipped whole; surrounding prose still converts."
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
"id": "en-us-markdown-commonmark-fenced-code",
|
|
1528
|
+
"rule": "quotes",
|
|
1529
|
+
"mode": "markdown",
|
|
1530
|
+
"dialect": "commonmark",
|
|
1531
|
+
"in": "```js\nconst s = \"raw\";\n```\n\nBut \"this\" converts.\n",
|
|
1532
|
+
"out": "```js\nconst s = \"raw\";\n```\n\nBut “this” converts.\n",
|
|
1533
|
+
"note": "modes.md §3.7.3: a fenced code block, its info string and its fences are skipped whole."
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
"id": "en-us-markdown-commonmark-link-text-and-destination",
|
|
1537
|
+
"rule": "ellipsis",
|
|
1538
|
+
"mode": "markdown",
|
|
1539
|
+
"dialect": "commonmark",
|
|
1540
|
+
"in": "[te...xt](http://a...b \"ti...tle\")\n",
|
|
1541
|
+
"out": "[te…xt](http://a...b \"ti...tle\")\n",
|
|
1542
|
+
"note": "modes.md §3.7.3: link text is processable; the destination and title in parentheses are skipped whole. The destination and title each contain \"...\" that would become the ellipsis character if processed, so an unconverted \"...\" there proves the skip rather than merely surviving because the characters happened not to change."
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
"id": "en-us-markdown-commonmark-autolink",
|
|
1546
|
+
"rule": "ellipsis",
|
|
1547
|
+
"mode": "markdown",
|
|
1548
|
+
"dialect": "commonmark",
|
|
1549
|
+
"in": "See <https://example.com/a...b> and also a...b here.\n",
|
|
1550
|
+
"out": "See <https://example.com/a...b> and also a…b here.\n",
|
|
1551
|
+
"note": "modes.md §3.7.3: an autolink is CommonMark-only and is skipped whole; identical dots outside it still convert to the ellipsis character."
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
"id": "en-us-markdown-commonmark-frontmatter",
|
|
1555
|
+
"rule": "quotes",
|
|
1556
|
+
"mode": "markdown",
|
|
1557
|
+
"dialect": "commonmark",
|
|
1558
|
+
"in": "---\ntitle: \"Une note\"\n---\n\nBody has \"quotes\" here.\n",
|
|
1559
|
+
"out": "---\ntitle: \"Une note\"\n---\n\nBody has “quotes” here.\n",
|
|
1560
|
+
"note": "modes.md §3.7.3: a frontmatter block delimited by --- is skipped whole, including its delimiters, so the machine-read title field is left untouched while body prose converts."
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
"id": "en-us-markdown-commonmark-raw-html",
|
|
1564
|
+
"rule": "quotes",
|
|
1565
|
+
"mode": "markdown",
|
|
1566
|
+
"dialect": "commonmark",
|
|
1567
|
+
"in": "<div class=\"x\">\n Body \"text\" inside.\n</div>\n\nAlso <b>\"kept\"</b> outside \"here\".\n",
|
|
1568
|
+
"out": "<div class=\"x\">\n Body “text” inside.\n</div>\n\nAlso <b>“kept”</b> outside “here”.\n",
|
|
1569
|
+
"note": "modes.md §3.7.3: raw HTML blocks and inline HTML are handed to the html skip list of §3.6 — attributes are skipped, element text content converts."
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
"id": "en-us-markdown-commonmark-boundary-nested-quotes",
|
|
1573
|
+
"rule": "quotes",
|
|
1574
|
+
"mode": "markdown",
|
|
1575
|
+
"dialect": "commonmark",
|
|
1576
|
+
"in": "\"He said *'hi'* loudly\"\n",
|
|
1577
|
+
"out": "“He said *‘hi’* loudly”\n",
|
|
1578
|
+
"note": "modes.md §3.2 Model C, the worked \"He said <em>'hi'</em> loudly\" example restated for markdown emphasis instead of an HTML element: the outer double-quote pair spans the emphasis boundary and becomes primary, the inner single-quote pair nests inside it at depth 2 and becomes secondary. Per-span processing (Model A) would get this wrong."
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
"id": "en-us-markdown-commonmark-noop",
|
|
1582
|
+
"rule": "dashes",
|
|
1583
|
+
"mode": "markdown",
|
|
1584
|
+
"dialect": "commonmark",
|
|
1585
|
+
"in": "# A Title\n\nA paragraph that needs nothing done to it—already correct.\n\n> A quoted line with “nothing” to fix.\n",
|
|
1586
|
+
"out": "# A Title\n\nA paragraph that needs nothing done to it—already correct.\n\n> A quoted line with “nothing” to fix.\n",
|
|
1587
|
+
"note": "modes.md §4: an already-typeset document is returned byte for byte, including the em-tight parenthetical dash en-US uses and the already-curly quotes in the blockquote."
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
"id": "en-us-markdown-commonmark-indented-code-block",
|
|
1591
|
+
"rule": "ellipsis",
|
|
1592
|
+
"mode": "markdown",
|
|
1593
|
+
"dialect": "commonmark",
|
|
1594
|
+
"in": "para...graph\n\n indented... code\n",
|
|
1595
|
+
"out": "para…graph\n\n indented... code\n",
|
|
1596
|
+
"note": "modes.md §3.7.3: an indented code block exists only in commonmark and is skipped; compare en-us-markdown-mdx-indented-code-block-is-prose, the identical bytes under \"mdx\"."
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
"id": "en-us-markdown-mdx-indented-code-block-is-prose",
|
|
1600
|
+
"rule": "ellipsis",
|
|
1601
|
+
"mode": "markdown",
|
|
1602
|
+
"dialect": "mdx",
|
|
1603
|
+
"in": "para...graph\n\n indented... code\n",
|
|
1604
|
+
"out": "para…graph\n\n indented… code\n",
|
|
1605
|
+
"note": "modes.md §3.7.1/§3.7.3: MDX has no indented code blocks, so the identical bytes that are skipped under \"commonmark\" (see en-us-markdown-commonmark-indented-code-block) are ordinary prose here and convert. This is the concrete case §3.7.1 gives for why dialect must never be detected."
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
"id": "en-us-markdown-mdx-jsx-expression-and-attributes",
|
|
1609
|
+
"rule": "quotes",
|
|
1610
|
+
"mode": "markdown",
|
|
1611
|
+
"dialect": "mdx",
|
|
1612
|
+
"in": "<Callout title=\"a'b\" n={1}>Some \"text\" here</Callout>\n",
|
|
1613
|
+
"out": "<Callout title=\"a'b\" n={1}>Some “text” here</Callout>\n",
|
|
1614
|
+
"note": "modes.md §3.7.3: MDX-only. JSX attributes and {…} expression containers are skipped whole; JSX element children are processable prose."
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
"id": "en-us-markdown-mdx-malformed-throws",
|
|
1618
|
+
"rule": "ellipsis",
|
|
1619
|
+
"mode": "markdown",
|
|
1620
|
+
"dialect": "mdx",
|
|
1621
|
+
"in": "text with {an unterminated expression...\n",
|
|
1622
|
+
"throws": "POLYTYPO_MALFORMED_INPUT",
|
|
1623
|
+
"note": "modes.md §3.7.2: a document that does not parse as its declared dialect throws POLYTYPO_MALFORMED_INPUT with the parser's own error type never escaping. Reachable only for dialect \"mdx\", which embeds JavaScript; every byte sequence is valid CommonMark."
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
"id": "en-us-markdown-mdx-noop",
|
|
1627
|
+
"rule": "quotes",
|
|
1628
|
+
"mode": "markdown",
|
|
1629
|
+
"dialect": "mdx",
|
|
1630
|
+
"in": "<Callout title=\"a\">Some text—already correct.</Callout>\n",
|
|
1631
|
+
"out": "<Callout title=\"a\">Some text—already correct.</Callout>\n",
|
|
1632
|
+
"note": "modes.md §4: an already-typeset MDX document, including an untouched attribute value, is returned byte for byte."
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
"id": "en-us-quotes-rock-n-roll-capitalized",
|
|
1636
|
+
"rule": "quotes",
|
|
1637
|
+
"mode": "text",
|
|
1638
|
+
"in": "Rock 'n' roll is great.",
|
|
1639
|
+
"out": "Rock ’n’ roll is great.",
|
|
1640
|
+
"note": "quotes.md §3.2, §6 row P2: sentence-initial capital on `left` matches via the first-code-point leniency (nbsp.afterShortWords precedent); `elided` and `right` still match exactly."
|
|
1641
|
+
},
|
|
1642
|
+
{
|
|
1643
|
+
"id": "en-us-quotes-rock-n-roll-trailing-punctuation",
|
|
1644
|
+
"rule": "quotes",
|
|
1645
|
+
"mode": "text",
|
|
1646
|
+
"in": "I love rock 'n' roll.",
|
|
1647
|
+
"out": "I love rock ’n’ roll.",
|
|
1648
|
+
"note": "quotes.md §3.2, §6 row P3: trailing punctuation after `right` does not block the whole-word boundary test — the code point after `roll` need only be non-LETTER/non-DIGIT, and a full stop satisfies that."
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
"id": "en-us-quotes-rock-n-roll-already-curly",
|
|
1652
|
+
"rule": "quotes",
|
|
1653
|
+
"mode": "text",
|
|
1654
|
+
"in": "rock ’n’ roll",
|
|
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."
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
"id": "en-us-quotes-letter-n-not-elided",
|
|
1660
|
+
"rule": "quotes",
|
|
1661
|
+
"mode": "text",
|
|
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."
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
"id": "en-us-quotes-nested-quote-n-not-elided",
|
|
1668
|
+
"rule": "quotes",
|
|
1669
|
+
"mode": "text",
|
|
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)."
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
"id": "en-us-quotes-rock-n-pop-not-elided",
|
|
1676
|
+
"rule": "quotes",
|
|
1677
|
+
"mode": "text",
|
|
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."
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
"id": "en-us-quotes-fish-n-chips-not-elided",
|
|
1684
|
+
"rule": "quotes",
|
|
1685
|
+
"mode": "text",
|
|
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."
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
"id": "en-us-quotes-rock-cap-n-roll-not-elided",
|
|
1692
|
+
"rule": "quotes",
|
|
1693
|
+
"mode": "text",
|
|
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."
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
"id": "en-us-quotes-rock-n-roll-html",
|
|
1700
|
+
"rule": "quotes",
|
|
1701
|
+
"mode": "html",
|
|
1702
|
+
"in": "<p>rock 'n' roll</p>",
|
|
1703
|
+
"out": "<p>rock ’n’ roll</p>",
|
|
1704
|
+
"note": "quotes.md §3.2, §6 row H0: idiom whole within one text node — positive control for the html-mode boundary cases below."
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
"id": "en-us-quotes-rock-n-roll-html-left-split",
|
|
1708
|
+
"rule": "quotes",
|
|
1709
|
+
"mode": "html",
|
|
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."
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
"id": "en-us-quotes-rock-n-roll-html-right-split",
|
|
1716
|
+
"rule": "quotes",
|
|
1717
|
+
"mode": "html",
|
|
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."
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
"id": "en-us-quotes-rock-n-roll-markdown-left-split",
|
|
1724
|
+
"rule": "quotes",
|
|
1725
|
+
"mode": "markdown",
|
|
1726
|
+
"dialect": "commonmark",
|
|
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 row — Markdown 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."
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
"id": "en-us-quotes-ambiguous-single-letter",
|
|
1733
|
+
"rule": "quotes",
|
|
1734
|
+
"mode": "text",
|
|
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)."
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
"id": "en-us-quotes-ambiguous-two-letter",
|
|
1741
|
+
"rule": "quotes",
|
|
1742
|
+
"mode": "text",
|
|
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."
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
"id": "en-us-quotes-ambiguous-three-letter",
|
|
1749
|
+
"rule": "quotes",
|
|
1750
|
+
"mode": "text",
|
|
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."
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
"id": "en-us-quotes-ambiguous-four-letter-not-caught",
|
|
1757
|
+
"rule": "quotes",
|
|
1758
|
+
"mode": "text",
|
|
1759
|
+
"in": "say 'like' now",
|
|
1760
|
+
"out": "say “like” now",
|
|
1761
|
+
"note": "spec 0.5.0: 4 LETTERs enclosed is past the veto's bound (1-3, quotes.md 3.2) — a deliberate, documented false negative in the other direction: this is the boundary case the veto does NOT catch, so the marks pair as an ordinary quotation exactly as before 0.5.0. Pinned so the exact boundary is visible in the conformance suite."
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
"id": "en-us-quotes-ambiguous-astral-letter",
|
|
1765
|
+
"rule": "quotes",
|
|
1766
|
+
"mode": "text",
|
|
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."
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
"id": "en-us-quotes-ambiguous-digit-not-letter",
|
|
1773
|
+
"rule": "quotes",
|
|
1774
|
+
"mode": "text",
|
|
1775
|
+
"in": "say '12' now",
|
|
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."
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
"id": "en-us-quotes-ambiguous-punctuation-not-letter",
|
|
1781
|
+
"rule": "quotes",
|
|
1782
|
+
"mode": "text",
|
|
1783
|
+
"in": "say '@' now",
|
|
1784
|
+
"out": "say “@” now",
|
|
1785
|
+
"note": "spec 0.5.0: the enclosed code point is punctuation, not LETTER, so the veto does not fire — punctuation must not accidentally qualify as a LETTER sequence (quotes.md 3.2). Pairs as an ordinary quotation."
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
"id": "en-us-quotes-ambiguous-already-curly-no-idiom",
|
|
1789
|
+
"rule": "quotes",
|
|
1790
|
+
"mode": "text",
|
|
1791
|
+
"in": "She chose ’A’ today",
|
|
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."
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
"id": "en-us-ranges-decimal-and-path-rejected",
|
|
1797
|
+
"rule": "ranges",
|
|
1798
|
+
"mode": "text",
|
|
1799
|
+
"in": "prices 1.5-2.5 and paths 01/02-03/04",
|
|
1800
|
+
"out": "prices 1.5-2.5 and paths 01/02-03/04",
|
|
1801
|
+
"rules": {
|
|
1802
|
+
"ranges": true
|
|
1803
|
+
},
|
|
1804
|
+
"note": "ranges.md §3.2, G3 — not part of a decimal or a path. `before` is U+002E (.) for the first candidate and U+002F (/) for the second; `after` is U+002F (/) for the second. This is currently the only canonical fixture exercising G3 — previously untested by any canonical case even before the spec 0.5.0 ownership correction (dashes.test.ts's engine-level 'must not touch' list covered it, but no fixture did)."
|
|
1805
|
+
}
|
|
1806
|
+
]
|
|
1807
|
+
}
|