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,1290 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "1.0.0",
|
|
3
|
+
"locale": "sv",
|
|
4
|
+
"cases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-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": "sv-ellipsis-three-dots",
|
|
143
|
+
"rule": "ellipsis",
|
|
144
|
+
"mode": "text",
|
|
145
|
+
"in": "Wait... what?",
|
|
146
|
+
"out": "Wait… what?",
|
|
147
|
+
"note": "ellipsis.md §6 case 1."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "sv-ellipsis-mixed-run",
|
|
151
|
+
"rule": "ellipsis",
|
|
152
|
+
"mode": "text",
|
|
153
|
+
"in": "Wait…… what?",
|
|
154
|
+
"out": "Wait… what?",
|
|
155
|
+
"note": "ellipsis.md §6 case 2: a mixed/repeated DOTLIKE run normalises to one U+2026."
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": "sv-ellipsis-two-dots-after-terminal",
|
|
159
|
+
"rule": "ellipsis",
|
|
160
|
+
"mode": "text",
|
|
161
|
+
"in": "Really?..",
|
|
162
|
+
"out": "Really?…",
|
|
163
|
+
"note": "ellipsis.md §6 case 3: abbreviatedAfterTerminal is false, so `?..` is a typing slip for `?…`."
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"id": "sv-ellipsis-relative-path",
|
|
167
|
+
"rule": "ellipsis",
|
|
168
|
+
"mode": "text",
|
|
169
|
+
"in": "../docs is one level up",
|
|
170
|
+
"out": "../docs is one level up",
|
|
171
|
+
"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."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "sv-ellipsis-numeric-range",
|
|
175
|
+
"rule": "ellipsis",
|
|
176
|
+
"mode": "text",
|
|
177
|
+
"in": "Version 1..5",
|
|
178
|
+
"out": "Version 1..5",
|
|
179
|
+
"note": "ellipsis.md §6 case 5: left neighbour of the two-dot run is a digit."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"id": "sv-ellipsis-already-ellipsis",
|
|
183
|
+
"rule": "ellipsis",
|
|
184
|
+
"mode": "text",
|
|
185
|
+
"in": "He left…",
|
|
186
|
+
"out": "He left…",
|
|
187
|
+
"note": "ellipsis.md §6 case 6."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "sv-ellipsis-five-dots",
|
|
191
|
+
"rule": "ellipsis",
|
|
192
|
+
"mode": "text",
|
|
193
|
+
"in": "Hmm.....",
|
|
194
|
+
"out": "Hmm…",
|
|
195
|
+
"note": "ellipsis.md §6 case 7."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": "sv-ellipsis-sentence-stops",
|
|
199
|
+
"rule": "ellipsis",
|
|
200
|
+
"mode": "text",
|
|
201
|
+
"in": "Yes. No.",
|
|
202
|
+
"out": "Yes. No.",
|
|
203
|
+
"note": "ellipsis.md §6 case 8: two independent runs of length 1."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": "sv-ellipsis-two-dot-leader",
|
|
207
|
+
"rule": "ellipsis",
|
|
208
|
+
"mode": "text",
|
|
209
|
+
"in": "Two dot leader ‥ stays",
|
|
210
|
+
"out": "Two dot leader ‥ stays",
|
|
211
|
+
"note": "ellipsis.md §4: U+2025 is in no class and is never produced or consumed."
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"id": "sv-ellipsis-spaced-path",
|
|
215
|
+
"rule": "ellipsis",
|
|
216
|
+
"mode": "text",
|
|
217
|
+
"in": "See ../docs",
|
|
218
|
+
"out": "See ../docs",
|
|
219
|
+
"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."
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "sv-ellipsis-spaced-two-dots",
|
|
223
|
+
"rule": "ellipsis",
|
|
224
|
+
"mode": "text",
|
|
225
|
+
"in": "e.g. ..",
|
|
226
|
+
"out": "e.g. ..",
|
|
227
|
+
"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."
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"id": "sv-dashes-parenthetical-spaced-input",
|
|
231
|
+
"rule": "dashes",
|
|
232
|
+
"mode": "text",
|
|
233
|
+
"in": "Planen - om det finns en - misslyckas.",
|
|
234
|
+
"out": "Planen – om det finns en – misslyckas.",
|
|
235
|
+
"note": "dashes.md §3.4: en-spaced keeps the spacing and normalises the stroke."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"id": "sv-dashes-parenthetical-double-hyphen",
|
|
239
|
+
"rule": "dashes",
|
|
240
|
+
"mode": "text",
|
|
241
|
+
"in": "Planen--om det finns en--misslyckas.",
|
|
242
|
+
"out": "Planen – om det finns en – misslyckas.",
|
|
243
|
+
"note": "dashes.md §6 case 11 (same locale shape as de-DE): en-spaced inserts the spacing the tight input lacked."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"id": "sv-dashes-parenthetical-already-correct",
|
|
247
|
+
"rule": "dashes",
|
|
248
|
+
"mode": "text",
|
|
249
|
+
"in": "Planen – om det finns en – misslyckas.",
|
|
250
|
+
"out": "Planen – om det finns en – misslyckas.",
|
|
251
|
+
"note": "dashes.md §6 case 12: already the target form, byte-identical. The token is admitted (spec 0.2.0 retired the unconditional authored-dash guard), but its glyph and spacing already match the locale, so §3.3.1's invisible-edit test computes an unbound replacement identical to the input and emits nothing — a true recomputation-to-fixed-point, not an early decline. A port that instead declines any token holding U+2013/U+2014 outright reaches the same output here by coincidence, but diverges on a mis-spaced or wrong-length authored dash, which this locale's other fixtures now exercise."
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"id": "sv-ranges",
|
|
255
|
+
"rule": "ranges",
|
|
256
|
+
"mode": "text",
|
|
257
|
+
"in": "1914-1918 and pp. 34-36",
|
|
258
|
+
"out": "1914–1918 and pp. 34–36",
|
|
259
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). ranges.md §6 rows 3 and 13 (relocated from dashes.md §6 cases 3 and 13 through spec 0.4.1): range branch, en-tight — note that this locale's PARENTHETICAL form is en-spaced and takes no joiner, while its range form is tight and does. 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.",
|
|
260
|
+
"rules": {
|
|
261
|
+
"ranges": true
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"id": "sv-dashes-compound-hyphens",
|
|
266
|
+
"rule": "dashes",
|
|
267
|
+
"mode": "text",
|
|
268
|
+
"in": "A well-known e-mail address",
|
|
269
|
+
"out": "A well-known e-mail address",
|
|
270
|
+
"note": "dashes.md §6 case 4: guard P1."
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"id": "sv-dashes-covid",
|
|
274
|
+
"rule": "dashes",
|
|
275
|
+
"mode": "text",
|
|
276
|
+
"in": "COVID-19",
|
|
277
|
+
"out": "COVID-19",
|
|
278
|
+
"note": "spec 0.5.0 ownership correction: split from the former combined `sv-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 `sv-ranges-iso-8859` for the range-guard half."
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"id": "sv-ranges-iso-8859",
|
|
282
|
+
"rule": "ranges",
|
|
283
|
+
"mode": "text",
|
|
284
|
+
"in": "ISO 8859-1",
|
|
285
|
+
"out": "ISO 8859-1",
|
|
286
|
+
"rules": {
|
|
287
|
+
"ranges": true
|
|
288
|
+
},
|
|
289
|
+
"note": "spec 0.5.0 ownership correction: split from the former combined `sv-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."
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"id": "sv-ranges-iso-date-and-isbn",
|
|
293
|
+
"rule": "ranges",
|
|
294
|
+
"mode": "text",
|
|
295
|
+
"in": "Released 2026-08-15, ISBN 978-3-16-148410-0",
|
|
296
|
+
"out": "Released 2026-08-15, ISBN 978-3-16-148410-0",
|
|
297
|
+
"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. ranges.md §3.2's G2 (chain) is the current normative guard rejecting this.",
|
|
298
|
+
"rules": {
|
|
299
|
+
"ranges": true
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"id": "sv-ranges-phone-number",
|
|
304
|
+
"rule": "ranges",
|
|
305
|
+
"mode": "text",
|
|
306
|
+
"in": "Call 212-555-1234",
|
|
307
|
+
"out": "Call 212-555-1234",
|
|
308
|
+
"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.",
|
|
309
|
+
"rules": {
|
|
310
|
+
"ranges": true
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"id": "sv-dashes-cli-flag",
|
|
315
|
+
"rule": "dashes",
|
|
316
|
+
"mode": "text",
|
|
317
|
+
"in": "run --force to override",
|
|
318
|
+
"out": "run --force to override",
|
|
319
|
+
"note": "dashes.md §6 case 8."
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"id": "sv-dashes-list-item",
|
|
323
|
+
"rule": "dashes",
|
|
324
|
+
"mode": "text",
|
|
325
|
+
"in": "- first item",
|
|
326
|
+
"out": "- first item",
|
|
327
|
+
"note": "dashes.md §6 case 9."
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"id": "sv-ranges-descending-score",
|
|
331
|
+
"rule": "ranges",
|
|
332
|
+
"mode": "text",
|
|
333
|
+
"in": "Scores: 20-10",
|
|
334
|
+
"out": "Scores: 20-10",
|
|
335
|
+
"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.",
|
|
336
|
+
"rules": {
|
|
337
|
+
"ranges": true
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"id": "sv-dashes-dash-space-dash",
|
|
342
|
+
"rule": "dashes",
|
|
343
|
+
"mode": "text",
|
|
344
|
+
"in": "a- - a",
|
|
345
|
+
"out": "a- - a",
|
|
346
|
+
"note": "dashes.md §6 case 19, idempotency defect (b)."
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"id": "sv-dashes-spaced-dash-pair",
|
|
350
|
+
"rule": "dashes",
|
|
351
|
+
"mode": "text",
|
|
352
|
+
"in": "a - - b",
|
|
353
|
+
"out": "a - - b",
|
|
354
|
+
"note": "dashes.md §6 case 21."
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"id": "sv-dashes-cluster-multi-run",
|
|
358
|
+
"rule": "dashes",
|
|
359
|
+
"mode": "text",
|
|
360
|
+
"in": "1914-1918--annexation",
|
|
361
|
+
"out": "1914-1918--annexation",
|
|
362
|
+
"note": "dashes.md §6 case 20."
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"id": "sv-ranges-cluster-multi-run",
|
|
366
|
+
"rule": "ranges",
|
|
367
|
+
"mode": "text",
|
|
368
|
+
"in": "1914-1918--annexation",
|
|
369
|
+
"out": "1914-1918--annexation",
|
|
370
|
+
"rules": {
|
|
371
|
+
"ranges": true
|
|
372
|
+
},
|
|
373
|
+
"note": "spec 0.5.0 ownership correction: companion to `sv-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)."
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"id": "sv-dashes-t1-spacing-transition",
|
|
377
|
+
"rule": "dashes",
|
|
378
|
+
"mode": "text",
|
|
379
|
+
"in": "a–1 - 1",
|
|
380
|
+
"out": "a–1 - 1",
|
|
381
|
+
"note": "dashes.md §6 case 24, idempotency defect (c). §5.2 names this locale explicitly: every locale whose parenthetical is -spaced and whose range is not `none` reproduced it. Guard T1 (§3.2 step 8) makes the tight `–` inert at distance 2, regardless of whether the dash was hyphen- or en/em-typed — spec 0.2.0 retired the authored-dash guard, so T1 alone is what declines this token now."
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"id": "sv-ranges-t1-spacing-transition",
|
|
385
|
+
"rule": "ranges",
|
|
386
|
+
"mode": "text",
|
|
387
|
+
"in": "a–1 - 1",
|
|
388
|
+
"out": "a–1 - 1",
|
|
389
|
+
"rules": {
|
|
390
|
+
"ranges": true
|
|
391
|
+
},
|
|
392
|
+
"note": "spec 0.5.0 ownership correction: companion to `sv-dashes-t1-spacing-transition`. Explicitly opted in (`rules: {ranges: true}`): the trailing `1 - 1` is a genuine range candidate, declined independently by G2 (ranges.md §3.2) regardless of `dashes`' own T1 guard on the first token."
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"id": "sv-dashes-t1-tight-sibling",
|
|
396
|
+
"rule": "dashes",
|
|
397
|
+
"mode": "text",
|
|
398
|
+
"in": "a–1-1",
|
|
399
|
+
"out": "a–1-1",
|
|
400
|
+
"note": "dashes.md §6 case 26: the tight sibling, guard T1 firing at distance 1 — and also one cluster with two dash runs (step 7). Both guards read DASH membership, not authorship, so they hold exactly as before spec 0.2.0 retired the authored-dash guard; the case is retained because a port may implement either guard and must still produce this output."
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
"id": "sv-ranges-t1-tight-sibling",
|
|
404
|
+
"rule": "ranges",
|
|
405
|
+
"mode": "text",
|
|
406
|
+
"in": "a–1-1",
|
|
407
|
+
"out": "a–1-1",
|
|
408
|
+
"rules": {
|
|
409
|
+
"ranges": true
|
|
410
|
+
},
|
|
411
|
+
"note": "spec 0.5.0 ownership correction: companion to `sv-dashes-t1-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)."
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
"id": "sv-dashes-t2-before-full-stop",
|
|
415
|
+
"rule": "dashes",
|
|
416
|
+
"mode": "text",
|
|
417
|
+
"in": ".--.",
|
|
418
|
+
"out": ".--.",
|
|
419
|
+
"note": "dashes.md §6 case 28, composition defect family 1: guard T2 declines because cp[R] is U+002E, a position `spaces` deletes a U+0020 from."
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"id": "sv-dashes-t2-after-open-bracket",
|
|
423
|
+
"rule": "dashes",
|
|
424
|
+
"mode": "text",
|
|
425
|
+
"in": "(--a",
|
|
426
|
+
"out": "(--a",
|
|
427
|
+
"note": "dashes.md §6 case 30: guard T2 on the left."
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"id": "sv-dashes-t2-before-close-bracket",
|
|
431
|
+
"rule": "dashes",
|
|
432
|
+
"mode": "text",
|
|
433
|
+
"in": "a--)",
|
|
434
|
+
"out": "a--)",
|
|
435
|
+
"note": "dashes.md §6 case 31: guard T2 on the right."
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
"id": "sv-dashes-authored-em-dash-converts",
|
|
439
|
+
"rule": "dashes",
|
|
440
|
+
"mode": "text",
|
|
441
|
+
"in": "dokument — PDF-filer — och du",
|
|
442
|
+
"out": "dokument – PDF-filer – och du",
|
|
443
|
+
"note": "dashes.md §6 case 41, spec 0.2.0. Spec 0.1.0's authored-dash guard (and a briefly-shipped 0.2.0 narrowing that preserved only length) is fully retired: sv's parenthetical is `en-spaced`, so the author's em dash is promoted to it exactly as a hyphen-typed one would be — length included. See dashes.md §7 item 14 for why length stopped being treated as reliably authorial. Språkrådet's tankstreck is the en dash; the author's em dash is now corrected to it rather than surviving untouched. This fixture holds only for U+2014 — see `sv-dashes-authored-en-dash-tight-declined` for why U+2013 does not follow the same path since spec 0.6.0."
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"id": "sv-dashes-authored-en-dash-tight-declined",
|
|
447
|
+
"rule": "dashes",
|
|
448
|
+
"mode": "text",
|
|
449
|
+
"in": "dokument–PDF-filer–och du",
|
|
450
|
+
"out": "dokument–PDF-filer–och du",
|
|
451
|
+
"note": "dashes.md §3.4 P5 (spec 0.6.0): a pure single authored U+2013 is declined unconditionally, even though sv's `dash.parenthetical` target glyph is already `en-spaced` (U+2013). Before P5 this tight authored en-dash would have been re-spaced; P5 protects the whole token, spacing included (§8.8)."
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"id": "sv-dashes-mixed-run-converts",
|
|
455
|
+
"rule": "dashes",
|
|
456
|
+
"mode": "text",
|
|
457
|
+
"in": "a-–b",
|
|
458
|
+
"out": "a – b",
|
|
459
|
+
"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, promoted exactly as the equivalent hyphen run (`a--b`) would be — P3 admits a `k = 2` run regardless of spacing. Spec 0.1.0's authored-dash guard, which declined any run containing U+2013/U+2014 outright, is retired in 0.2.0 (dashes.md §3.2 step 2a)."
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"id": "sv-ranges-authored-range-unbound",
|
|
463
|
+
"rule": "ranges",
|
|
464
|
+
"mode": "text",
|
|
465
|
+
"in": "1914–1918",
|
|
466
|
+
"out": "1914–1918",
|
|
467
|
+
"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: an AUTHORED range gets NO joiners. 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 conversion the author asked for, 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. ranges.md §3.3.1's invisible-edit test is the current normative source for why this stays unbound.",
|
|
468
|
+
"rules": {
|
|
469
|
+
"ranges": true
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"id": "sv-ranges-hyphen-range-bound",
|
|
474
|
+
"rule": "ranges",
|
|
475
|
+
"mode": "text",
|
|
476
|
+
"in": "1914-1918",
|
|
477
|
+
"out": "1914–1918",
|
|
478
|
+
"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.",
|
|
479
|
+
"rules": {
|
|
480
|
+
"ranges": true
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"id": "sv-ranges-one-two-digits",
|
|
485
|
+
"rule": "ranges",
|
|
486
|
+
"mode": "text",
|
|
487
|
+
"in": "Tar 5-10 dagar",
|
|
488
|
+
"out": "Tar 5–10 dagar",
|
|
489
|
+
"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.",
|
|
490
|
+
"rules": {
|
|
491
|
+
"ranges": true
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
"id": "sv-ranges-nine-ten",
|
|
496
|
+
"rule": "ranges",
|
|
497
|
+
"mode": "text",
|
|
498
|
+
"in": "i åldern 9-10 år",
|
|
499
|
+
"out": "i åldern 9–10 år",
|
|
500
|
+
"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.",
|
|
501
|
+
"rules": {
|
|
502
|
+
"ranges": true
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"id": "sv-ranges-one-twelve",
|
|
507
|
+
"rule": "ranges",
|
|
508
|
+
"mode": "text",
|
|
509
|
+
"in": "kapitlen 1-12",
|
|
510
|
+
"out": "kapitlen 1–12",
|
|
511
|
+
"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.",
|
|
512
|
+
"rules": {
|
|
513
|
+
"ranges": true
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"id": "sv-ranges-zero-sixty",
|
|
518
|
+
"rule": "ranges",
|
|
519
|
+
"mode": "text",
|
|
520
|
+
"in": "0-60 på sex sekunder",
|
|
521
|
+
"out": "0–60 på sex sekunder",
|
|
522
|
+
"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.",
|
|
523
|
+
"rules": {
|
|
524
|
+
"ranges": true
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"id": "sv-ranges-two-one-digits-rejected",
|
|
529
|
+
"rule": "ranges",
|
|
530
|
+
"mode": "text",
|
|
531
|
+
"in": "vann 10-7",
|
|
532
|
+
"out": "vann 10-7",
|
|
533
|
+
"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.",
|
|
534
|
+
"rules": {
|
|
535
|
+
"ranges": true
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
"id": "sv-ranges-leading-zero-rejected",
|
|
540
|
+
"rule": "ranges",
|
|
541
|
+
"mode": "text",
|
|
542
|
+
"in": "kod 9-05",
|
|
543
|
+
"out": "kod 9-05",
|
|
544
|
+
"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.",
|
|
545
|
+
"rules": {
|
|
546
|
+
"ranges": true
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
"id": "sv-ranges-phone-seven-digits",
|
|
551
|
+
"rule": "ranges",
|
|
552
|
+
"mode": "text",
|
|
553
|
+
"in": "Ring 555-1234",
|
|
554
|
+
"out": "Ring 555-1234",
|
|
555
|
+
"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.",
|
|
556
|
+
"rules": {
|
|
557
|
+
"ranges": true
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"id": "sv-ranges-one-three-digits-rejected",
|
|
562
|
+
"rule": "ranges",
|
|
563
|
+
"mode": "text",
|
|
564
|
+
"in": "Ring 1-800 nu",
|
|
565
|
+
"out": "Ring 1-800 nu",
|
|
566
|
+
"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.",
|
|
567
|
+
"rules": {
|
|
568
|
+
"ranges": true
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"id": "sv-ranges-abbreviated-years-rejected",
|
|
573
|
+
"rule": "ranges",
|
|
574
|
+
"mode": "text",
|
|
575
|
+
"in": "säsongen 2020-24",
|
|
576
|
+
"out": "säsongen 2020-24",
|
|
577
|
+
"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.",
|
|
578
|
+
"rules": {
|
|
579
|
+
"ranges": true
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
"id": "sv-dashes-range-compound-label-safe-default",
|
|
584
|
+
"rule": "dashes",
|
|
585
|
+
"mode": "text",
|
|
586
|
+
"in": "Figur 5-10",
|
|
587
|
+
"out": "Figur 5-10",
|
|
588
|
+
"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."
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
"id": "sv-hyphen-empty-lists-noop",
|
|
592
|
+
"rule": "hyphen",
|
|
593
|
+
"mode": "text",
|
|
594
|
+
"in": "A well-known e-mail address, COVID-19, Jean-Luc",
|
|
595
|
+
"out": "A well-known e-mail address, COVID-19, Jean-Luc",
|
|
596
|
+
"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."
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
"id": "sv-hyphen-empty-lists-prose",
|
|
600
|
+
"rule": "hyphen",
|
|
601
|
+
"mode": "text",
|
|
602
|
+
"in": "Det här är en helt vanlig svensk mening.",
|
|
603
|
+
"out": "Det här är en helt vanlig svensk mening.",
|
|
604
|
+
"note": "hyphen.md §6 case 14 / §2: with three empty lists the rule emits nothing for any input."
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"id": "sv-hyphen-existing-nbhy-roundtrip",
|
|
608
|
+
"rule": "hyphen",
|
|
609
|
+
"mode": "text",
|
|
610
|
+
"in": "A well‑known e‑mail address",
|
|
611
|
+
"out": "A well‑known e‑mail address",
|
|
612
|
+
"note": "hyphen.md §5: an author-written U+2011 is never examined and round-trips byte-identically."
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
"id": "sv-hyphen-inert-dash-family",
|
|
616
|
+
"rule": "hyphen",
|
|
617
|
+
"mode": "text",
|
|
618
|
+
"in": "softhyphen, minus −5, figure‒dash, bar―here",
|
|
619
|
+
"out": "softhyphen, minus −5, figure‒dash, bar―here",
|
|
620
|
+
"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."
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
"id": "sv-quotes-basic",
|
|
624
|
+
"rule": "quotes",
|
|
625
|
+
"mode": "text",
|
|
626
|
+
"in": "Han sa \"hej\" och gick.",
|
|
627
|
+
"out": "Han sa ”hej” och gick.",
|
|
628
|
+
"note": "Språkrådet: Swedish quotation marks have the same shape at the beginning and the end, U+201D on both sides. The rule knew which mark was which from the pairing, not from the glyph."
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
"id": "sv-quotes-roundtrip-same-glyph",
|
|
632
|
+
"rule": "quotes",
|
|
633
|
+
"mode": "text",
|
|
634
|
+
"in": "Han sa ”hej” och gick.",
|
|
635
|
+
"out": "Han sa ”hej” och gick.",
|
|
636
|
+
"note": "quotes.md §6 case 10, Swedish: the previous case's output re-processed. No STRAIGHT marks remain. This is the case a flip-flop state machine gets wrong."
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
"id": "sv-quotes-nested",
|
|
640
|
+
"rule": "quotes",
|
|
641
|
+
"mode": "text",
|
|
642
|
+
"in": "\"Han sa 'hej'\", sa jag.",
|
|
643
|
+
"out": "”Han sa ’hej’”, sa jag.",
|
|
644
|
+
"note": "quotes.md §6 case 11, Swedish: depth 2 → secondary (U+2019 on both sides). The trailing `'` and `\"` are of different kinds, so the pass 1 veto does not apply."
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
"id": "sv-quotes-nested-roundtrip",
|
|
648
|
+
"rule": "quotes",
|
|
649
|
+
"mode": "text",
|
|
650
|
+
"in": "”Han sa ’hej’”, sa jag.",
|
|
651
|
+
"out": "”Han sa ’hej’”, sa jag.",
|
|
652
|
+
"note": "The nested same-glyph output is a fixed point."
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"id": "sv-quotes-nesting-with-question",
|
|
656
|
+
"rule": "quotes",
|
|
657
|
+
"mode": "text",
|
|
658
|
+
"in": "\"Hon frågade: 'vad nu?'\"",
|
|
659
|
+
"out": "”Hon frågade: ’vad nu?’”",
|
|
660
|
+
"note": "Nesting where both closers stack up at the end of the text unit."
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"id": "sv-quotes-unbalanced-opener",
|
|
664
|
+
"rule": "quotes",
|
|
665
|
+
"mode": "text",
|
|
666
|
+
"in": "Han började \"citatet men slöt det aldrig.",
|
|
667
|
+
"out": "Han började \"citatet men slöt det aldrig.",
|
|
668
|
+
"note": "quotes.md §3.6: an unmatchable opening mark stays U+0022 rather than being guessed at."
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
"id": "sv-quotes-unbalanced-closer",
|
|
672
|
+
"rule": "quotes",
|
|
673
|
+
"mode": "text",
|
|
674
|
+
"in": "Han lämnade rummet\" utan att öppna ett citat.",
|
|
675
|
+
"out": "Han lämnade rummet\" utan att öppna ett citat.",
|
|
676
|
+
"note": "quotes.md §3.6: a closing mark with nothing open."
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
"id": "sv-quotes-unbalanced-mixed",
|
|
680
|
+
"rule": "quotes",
|
|
681
|
+
"mode": "text",
|
|
682
|
+
"in": "Han sa \"hej. Han sa \"hejdå.\"",
|
|
683
|
+
"out": "Han sa \"hej. Han sa ”hejdå.”",
|
|
684
|
+
"note": "quotes.md §6 case 4 in a same-glyph locale: the resolved pair is at depth 1, because §3.4 counts enclosing pairs rather than stack height."
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
"id": "sv-quotes-adjacent-to-punctuation",
|
|
688
|
+
"rule": "quotes",
|
|
689
|
+
"mode": "text",
|
|
690
|
+
"in": "\"Hej\", sa han, \"hejdå\".",
|
|
691
|
+
"out": "”Hej”, sa han, ”hejdå”.",
|
|
692
|
+
"note": "A quotation adjacent to punctuation: `,` and `.` are in CLOSEISH, so both closing marks are canClose."
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
"id": "sv-quotes-terminal-inside-quotation",
|
|
696
|
+
"rule": "quotes",
|
|
697
|
+
"mode": "text",
|
|
698
|
+
"in": "\"Hej!\" ropade han.",
|
|
699
|
+
"out": "”Hej!” ropade han.",
|
|
700
|
+
"note": "The closing mark's left neighbour is `!`."
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
"id": "sv-quotes-two-quotations",
|
|
704
|
+
"rule": "quotes",
|
|
705
|
+
"mode": "text",
|
|
706
|
+
"in": "Han sa ”hej” och ”hejdå”.",
|
|
707
|
+
"out": "Han sa ”hej” och ”hejdå”.",
|
|
708
|
+
"note": "Two already-correct same-glyph quotations, byte-identical."
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
"id": "sv-quotes-foot-and-inch",
|
|
712
|
+
"rule": "quotes",
|
|
713
|
+
"mode": "text",
|
|
714
|
+
"in": "He is 6' 2\" tall.",
|
|
715
|
+
"out": "He is 6' 2\" tall.",
|
|
716
|
+
"note": "quotes.md §6 case 6 / §4."
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
"id": "sv-quotes-doubled-single",
|
|
720
|
+
"rule": "quotes",
|
|
721
|
+
"mode": "text",
|
|
722
|
+
"in": "Bokstaven ''",
|
|
723
|
+
"out": "Bokstaven ''",
|
|
724
|
+
"note": "quotes.md §6 case 7."
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
"id": "sv-quotes-veto-witness",
|
|
728
|
+
"rule": "quotes",
|
|
729
|
+
"mode": "text",
|
|
730
|
+
"in": "\"\"\"a\"\"",
|
|
731
|
+
"out": "\"\"\"a\"\"",
|
|
732
|
+
"note": "quotes.md §6 case 7b, the §5.4 witness."
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
"id": "sv-quotes-veto-inside-pair",
|
|
736
|
+
"rule": "quotes",
|
|
737
|
+
"mode": "text",
|
|
738
|
+
"in": "\"a \"\" b\"",
|
|
739
|
+
"out": "”a \"\" b”",
|
|
740
|
+
"note": "quotes.md §6 case 7c."
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
"id": "sv-quotes-existing-curly-ignored",
|
|
744
|
+
"rule": "quotes",
|
|
745
|
+
"mode": "text",
|
|
746
|
+
"in": "“Redan böjda,” sa han, och \"detta också.\"",
|
|
747
|
+
"out": "”Redan böjda,” sa han, och ”detta också.”",
|
|
748
|
+
"note": "quotes.md §6 case 8, updated for spec 0.3.0 mandate 1, mirroring fi's own case."
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
"id": "sv-quotes-rock-n-roll",
|
|
752
|
+
"rule": "quotes",
|
|
753
|
+
"mode": "text",
|
|
754
|
+
"in": "rock 'n' roll",
|
|
755
|
+
"out": "rock 'n' roll",
|
|
756
|
+
"note": "quotes.md §7.1, a known defect: the elision marks pair at depth 1 and take the Swedish primary glyphs. spec 0.5.0: same reasoning as fi-quotes-rock-n-roll — sv also writes the loanword closed up, no citation exists for the spaced form, and the general ambiguous-medial-span veto now preserves the ASCII input."
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"id": "sv-quotes-already-correct",
|
|
760
|
+
"rule": "quotes",
|
|
761
|
+
"mode": "text",
|
|
762
|
+
"in": "”Ja”, sa hon – åren 1914–1918, ungefär 5 km.",
|
|
763
|
+
"out": "”Ja”, sa hon – åren 1914–1918, ungefär 5 km.",
|
|
764
|
+
"note": "Round-trip: already-correct Swedish text comes out byte-identical, including the two U+2060 joiners around the tight range (dashes.md §3.3.1)."
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
"id": "sv-apostrophe-contraction",
|
|
768
|
+
"rule": "apostrophe",
|
|
769
|
+
"mode": "text",
|
|
770
|
+
"in": "don't",
|
|
771
|
+
"out": "don’t",
|
|
772
|
+
"note": "apostrophe.md §6 case 1: letters on both sides."
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
"id": "sv-apostrophe-elision",
|
|
776
|
+
"rule": "apostrophe",
|
|
777
|
+
"mode": "text",
|
|
778
|
+
"in": "l'été",
|
|
779
|
+
"out": "l’été",
|
|
780
|
+
"note": "apostrophe.md §6 case 2."
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
"id": "sv-apostrophe-possessive-plural",
|
|
784
|
+
"rule": "apostrophe",
|
|
785
|
+
"mode": "text",
|
|
786
|
+
"in": "the dogs' bowls",
|
|
787
|
+
"out": "the dogs’ bowls",
|
|
788
|
+
"note": "apostrophe.md §6 case 3: `quotes` leaves the mark unmatched, then case 3 converts it."
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
"id": "sv-apostrophe-decade-elision",
|
|
792
|
+
"rule": "apostrophe",
|
|
793
|
+
"mode": "text",
|
|
794
|
+
"in": "Back in the '90s",
|
|
795
|
+
"out": "Back in the ’90s",
|
|
796
|
+
"note": "apostrophe.md §6 case 4: U+2019, never U+2018."
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
"id": "sv-apostrophe-leading-elision",
|
|
800
|
+
"rule": "apostrophe",
|
|
801
|
+
"mode": "text",
|
|
802
|
+
"in": "'Tis the season",
|
|
803
|
+
"out": "’Tis the season",
|
|
804
|
+
"note": "apostrophe.md §6 case 5."
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
"id": "sv-apostrophe-foot-and-inch",
|
|
808
|
+
"rule": "apostrophe",
|
|
809
|
+
"mode": "text",
|
|
810
|
+
"in": "He is 6' 2\" tall.",
|
|
811
|
+
"out": "He is 6' 2\" tall.",
|
|
812
|
+
"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."
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"id": "sv-apostrophe-feet-inches-tight",
|
|
816
|
+
"rule": "apostrophe",
|
|
817
|
+
"mode": "text",
|
|
818
|
+
"in": "6'2\"",
|
|
819
|
+
"out": "6'2\"",
|
|
820
|
+
"note": "apostrophe.md §6 case 7: digit left, digit right → prime guard."
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
"id": "sv-apostrophe-isolated-mark",
|
|
824
|
+
"rule": "apostrophe",
|
|
825
|
+
"mode": "text",
|
|
826
|
+
"in": "a ' b",
|
|
827
|
+
"out": "a ' b",
|
|
828
|
+
"note": "apostrophe.md §6 case 9 and §7.4: an isolated straight mark survives into the output on purpose."
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
"id": "sv-apostrophe-doubled-mark",
|
|
832
|
+
"rule": "apostrophe",
|
|
833
|
+
"mode": "text",
|
|
834
|
+
"in": "dogs''",
|
|
835
|
+
"out": "dogs''",
|
|
836
|
+
"note": "apostrophe.md §6 case 12: neither mark has the neighbour class any converting case requires; `quotes` vetoed both in pass 1."
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
"id": "sv-apostrophe-two-medial",
|
|
840
|
+
"rule": "apostrophe",
|
|
841
|
+
"mode": "text",
|
|
842
|
+
"in": "O'Brien's",
|
|
843
|
+
"out": "O’Brien’s",
|
|
844
|
+
"note": "apostrophe.md §6 case 13."
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
"id": "sv-apostrophe-three-medial",
|
|
848
|
+
"rule": "apostrophe",
|
|
849
|
+
"mode": "text",
|
|
850
|
+
"in": "Ma'am, it's 5 o'clock",
|
|
851
|
+
"out": "Ma’am, it’s 5 o’clock",
|
|
852
|
+
"note": "apostrophe.md §6 case 14."
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
"id": "sv-apostrophe-digit-then-letter",
|
|
856
|
+
"rule": "apostrophe",
|
|
857
|
+
"mode": "text",
|
|
858
|
+
"in": "The 1990's were loud",
|
|
859
|
+
"out": "The 1990’s were loud",
|
|
860
|
+
"note": "apostrophe.md §6 case 8: the prime guard does not fire because a letter follows."
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
"id": "sv-apostrophe-already-curly",
|
|
864
|
+
"rule": "apostrophe",
|
|
865
|
+
"mode": "text",
|
|
866
|
+
"in": "“He said ’tis so,” she noted.",
|
|
867
|
+
"out": "”He said ’tis so,” she noted.",
|
|
868
|
+
"note": "apostrophe.md §6 case 10, updated for quotes.md spec 0.3.0 mandate 1: the foreign U+201C/U+201D pair is now a `quotes` candidate (order 40, before `apostrophe`) and is rewritten to this locale's own same-glyph primary (U+201D both sides) before `apostrophe` ever sees the U+2019 elision, which was and remains this case's actual point — no U+0027 survives to reach `apostrophe`."
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
"id": "sv-apostrophe-modifier-letters-kept",
|
|
872
|
+
"rule": "apostrophe",
|
|
873
|
+
"mode": "text",
|
|
874
|
+
"in": "Hawai'i, ʼokina ʼ and ʼ kept",
|
|
875
|
+
"out": "Hawai’i, ʼokina ʼ and ʼ kept",
|
|
876
|
+
"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."
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"id": "sv-apostrophe-backtick-and-acute",
|
|
880
|
+
"rule": "apostrophe",
|
|
881
|
+
"mode": "text",
|
|
882
|
+
"in": "The ``LaTeX'' idiom and a ´mark´.",
|
|
883
|
+
"out": "The ``LaTeX'' idiom and a ´mark´.",
|
|
884
|
+
"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."
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
"id": "sv-symbols-copyright",
|
|
888
|
+
"rule": "symbols",
|
|
889
|
+
"mode": "text",
|
|
890
|
+
"in": "Copyright (c) 2026 Iurii Rogulia",
|
|
891
|
+
"out": "Copyright © 2026 Iurii Rogulia",
|
|
892
|
+
"note": "symbols.md §6 case 1."
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
"id": "sv-symbols-tm-and-registered",
|
|
896
|
+
"rule": "symbols",
|
|
897
|
+
"mode": "text",
|
|
898
|
+
"in": "Acme(tm) and Acme (R)",
|
|
899
|
+
"out": "Acme™ and Acme ®",
|
|
900
|
+
"note": "symbols.md §6 case 2: the (tm) rows are exempt from guard S1."
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
"id": "sv-symbols-s1-asymmetry",
|
|
904
|
+
"rule": "symbols",
|
|
905
|
+
"mode": "text",
|
|
906
|
+
"in": "f(c) and f(tm)",
|
|
907
|
+
"out": "f(c) and f™",
|
|
908
|
+
"note": "symbols.md §6 case 2b: S1 applies to the (c)/(r) rows only. `f(tm)` becoming `f™` is deliberate."
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
"id": "sv-symbols-optional-first-letter",
|
|
912
|
+
"rule": "symbols",
|
|
913
|
+
"mode": "text",
|
|
914
|
+
"in": "The (r)evolution will not be televised",
|
|
915
|
+
"out": "The (r)evolution will not be televised",
|
|
916
|
+
"note": "symbols.md §6 case 3: guard S2."
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
"id": "sv-symbols-call-position",
|
|
920
|
+
"rule": "symbols",
|
|
921
|
+
"mode": "text",
|
|
922
|
+
"in": "f(c) returns c",
|
|
923
|
+
"out": "f(c) returns c",
|
|
924
|
+
"note": "symbols.md §6 case 4: guard S1."
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
"id": "sv-symbols-nesting",
|
|
928
|
+
"rule": "symbols",
|
|
929
|
+
"mode": "text",
|
|
930
|
+
"in": "((c))",
|
|
931
|
+
"out": "((c))",
|
|
932
|
+
"note": "symbols.md §6 case 11: guard S3."
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
"id": "sv-symbols-adjacent-signs",
|
|
936
|
+
"rule": "symbols",
|
|
937
|
+
"mode": "text",
|
|
938
|
+
"in": "(c)(r)",
|
|
939
|
+
"out": "©(r)",
|
|
940
|
+
"note": "symbols.md §5: S1 lists U+00A9/U+00AE/U+2122 so that this converges in one run rather than two."
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
"id": "sv-symbols-multiplication",
|
|
944
|
+
"rule": "symbols",
|
|
945
|
+
"mode": "text",
|
|
946
|
+
"in": "A 3x5 card, 10 x 20 grid",
|
|
947
|
+
"out": "A 3×5 card, 10 × 20 grid",
|
|
948
|
+
"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."
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
"id": "sv-symbols-multiplication-with-unit",
|
|
952
|
+
"rule": "symbols",
|
|
953
|
+
"mode": "text",
|
|
954
|
+
"in": "A 3x5 card, 10 x 20 cm",
|
|
955
|
+
"out": "A 3×5 card, 10 × 20 cm",
|
|
956
|
+
"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."
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
"id": "sv-symbols-hexadecimal",
|
|
960
|
+
"rule": "symbols",
|
|
961
|
+
"mode": "text",
|
|
962
|
+
"in": "Colour 0x1F, mask 0xFF, offset 0x10",
|
|
963
|
+
"out": "Colour 0x1F, mask 0xFF, offset 0x10",
|
|
964
|
+
"note": "symbols.md §6 case 6: guards M3 and M4."
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
"id": "sv-symbols-resolution",
|
|
968
|
+
"rule": "symbols",
|
|
969
|
+
"mode": "text",
|
|
970
|
+
"in": "Resolution 1920x1080",
|
|
971
|
+
"out": "Resolution 1920×1080",
|
|
972
|
+
"note": "symbols.md §6 case 7."
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
"id": "sv-symbols-variable-x",
|
|
976
|
+
"rule": "symbols",
|
|
977
|
+
"mode": "text",
|
|
978
|
+
"in": "Let x = 5 and solve for x",
|
|
979
|
+
"out": "Let x = 5 and solve for x",
|
|
980
|
+
"note": "symbols.md §6 case 8: no digit neighbour."
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
"id": "sv-symbols-chain",
|
|
984
|
+
"rule": "symbols",
|
|
985
|
+
"mode": "text",
|
|
986
|
+
"in": "A 2x3x4 box",
|
|
987
|
+
"out": "A 2×3×4 box",
|
|
988
|
+
"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."
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
"id": "sv-symbols-chain-cyrillic",
|
|
992
|
+
"rule": "symbols",
|
|
993
|
+
"mode": "text",
|
|
994
|
+
"in": "5х4х3",
|
|
995
|
+
"out": "5×4×3",
|
|
996
|
+
"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."
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
"id": "sv-symbols-chain-mixed-alphabets",
|
|
1000
|
+
"rule": "symbols",
|
|
1001
|
+
"mode": "text",
|
|
1002
|
+
"in": "5x4х3",
|
|
1003
|
+
"out": "5×4×3",
|
|
1004
|
+
"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."
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
"id": "sv-symbols-chain-three-links",
|
|
1008
|
+
"rule": "symbols",
|
|
1009
|
+
"mode": "text",
|
|
1010
|
+
"in": "Стол 120х80х75",
|
|
1011
|
+
"out": "Стол 120×80×75",
|
|
1012
|
+
"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."
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
"id": "sv-symbols-chain-spaced",
|
|
1016
|
+
"rule": "symbols",
|
|
1017
|
+
"mode": "text",
|
|
1018
|
+
"in": "10 x 20 x 30",
|
|
1019
|
+
"out": "10 × 20 × 30",
|
|
1020
|
+
"note": "symbols.md §6 case 5h: a spaced chain. `sp = 1` uniformly and each side keeps its own space code point."
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
"id": "sv-symbols-chain-mixed-spacing-declined",
|
|
1024
|
+
"rule": "symbols",
|
|
1025
|
+
"mode": "text",
|
|
1026
|
+
"in": "5x4 x 3",
|
|
1027
|
+
"out": "5x4 x 3",
|
|
1028
|
+
"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."
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
"id": "sv-symbols-chain-resumes-after-sign",
|
|
1032
|
+
"rule": "symbols",
|
|
1033
|
+
"mode": "text",
|
|
1034
|
+
"in": "5×4x3",
|
|
1035
|
+
"out": "5×4×3",
|
|
1036
|
+
"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."
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"id": "sv-symbols-cyrillic-single-link",
|
|
1040
|
+
"rule": "symbols",
|
|
1041
|
+
"mode": "text",
|
|
1042
|
+
"in": "Размер 5х4",
|
|
1043
|
+
"out": "Размер 5×4",
|
|
1044
|
+
"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."
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"id": "sv-symbols-cyrillic-capital",
|
|
1048
|
+
"rule": "symbols",
|
|
1049
|
+
"mode": "text",
|
|
1050
|
+
"in": "Размер 5Х4",
|
|
1051
|
+
"out": "Размер 5×4",
|
|
1052
|
+
"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)."
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
"id": "sv-symbols-cyrillic-letter-neighbours",
|
|
1056
|
+
"rule": "symbols",
|
|
1057
|
+
"mode": "text",
|
|
1058
|
+
"in": "хорошо и их",
|
|
1059
|
+
"out": "хорошо и их",
|
|
1060
|
+
"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."
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
"id": "sv-symbols-plus-minus-tight",
|
|
1064
|
+
"rule": "symbols",
|
|
1065
|
+
"mode": "text",
|
|
1066
|
+
"in": "Tolerance +/-5 points",
|
|
1067
|
+
"out": "Tolerance ±5 points",
|
|
1068
|
+
"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."
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
"id": "sv-symbols-plus-minus-spaced",
|
|
1072
|
+
"rule": "symbols",
|
|
1073
|
+
"mode": "text",
|
|
1074
|
+
"in": "Tolerance +/- 5 points",
|
|
1075
|
+
"out": "Tolerance ± 5 points",
|
|
1076
|
+
"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 `+/-`."
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
"id": "sv-symbols-plus-minus-with-unit",
|
|
1080
|
+
"rule": "symbols",
|
|
1081
|
+
"mode": "text",
|
|
1082
|
+
"in": "Tolerance +/-5 mm",
|
|
1083
|
+
"out": "Tolerance ±5 mm",
|
|
1084
|
+
"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."
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
"id": "sv-symbols-plus-minus-spaced-with-unit",
|
|
1088
|
+
"rule": "symbols",
|
|
1089
|
+
"mode": "text",
|
|
1090
|
+
"in": "Tolerance +/- 5 mm",
|
|
1091
|
+
"out": "Tolerance ± 5 mm",
|
|
1092
|
+
"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."
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
"id": "sv-symbols-plus-minus-after-digit",
|
|
1096
|
+
"rule": "symbols",
|
|
1097
|
+
"mode": "text",
|
|
1098
|
+
"in": "Погрешность 5+/-3",
|
|
1099
|
+
"out": "Погрешность 5±3",
|
|
1100
|
+
"note": "symbols.md §6 case 5m: a digit on the left is not an obstacle — guard F1 rejects only U+005B."
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
"id": "sv-symbols-plus-minus-prose-declined",
|
|
1104
|
+
"rule": "symbols",
|
|
1105
|
+
"mode": "text",
|
|
1106
|
+
"in": "lines marked +/- were edited",
|
|
1107
|
+
"out": "lines marked +/- were edited",
|
|
1108
|
+
"note": "symbols.md §6 case 5n: guard F2 requires a following digit (modulo one space). Prose ABOUT the characters is not converted."
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
"id": "sv-symbols-plus-minus-character-class-declined",
|
|
1112
|
+
"rule": "symbols",
|
|
1113
|
+
"mode": "text",
|
|
1114
|
+
"in": "match [+/-] once",
|
|
1115
|
+
"out": "match [+/-] once",
|
|
1116
|
+
"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."
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
"id": "sv-symbols-plus-minus-bare-declined",
|
|
1120
|
+
"rule": "symbols",
|
|
1121
|
+
"mode": "text",
|
|
1122
|
+
"in": "Range 5+-3",
|
|
1123
|
+
"out": "Range 5+-3",
|
|
1124
|
+
"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\"."
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
"id": "sv-symbols-asymmetric-x",
|
|
1128
|
+
"rule": "symbols",
|
|
1129
|
+
"mode": "text",
|
|
1130
|
+
"in": "Version 1080x",
|
|
1131
|
+
"out": "Version 1080x",
|
|
1132
|
+
"note": "symbols.md §6 case 12: guard M1."
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
"id": "sv-symbols-already-signs",
|
|
1136
|
+
"rule": "symbols",
|
|
1137
|
+
"mode": "text",
|
|
1138
|
+
"in": "© 2026, ® and ™",
|
|
1139
|
+
"out": "© 2026, ® and ™",
|
|
1140
|
+
"note": "symbols.md §6 case 13: existing signs are not candidates."
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
"id": "sv-symbols-nbsp-preserved-around-times",
|
|
1144
|
+
"rule": "symbols",
|
|
1145
|
+
"mode": "text",
|
|
1146
|
+
"in": "10 × 20",
|
|
1147
|
+
"out": "10 × 20",
|
|
1148
|
+
"note": "symbols.md §6 case 10: already converted, and each side keeps the exact space code point it had."
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
"id": "sv-symbols-parenthesised-letters",
|
|
1152
|
+
"rule": "symbols",
|
|
1153
|
+
"mode": "text",
|
|
1154
|
+
"in": "(s)he chose (a) and (e)",
|
|
1155
|
+
"out": "(s)he chose (a) and (e)",
|
|
1156
|
+
"note": "symbols.md §4: the trademark table is exhaustive."
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
"id": "sv-symbols-ascii-art",
|
|
1160
|
+
"rule": "symbols",
|
|
1161
|
+
"mode": "text",
|
|
1162
|
+
"in": "Smile (x) and (^_^)",
|
|
1163
|
+
"out": "Smile (x) and (^_^)",
|
|
1164
|
+
"note": "symbols.md §4: no table entry and no numeral context."
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
"id": "sv-nbsp-unit",
|
|
1168
|
+
"rule": "nbsp",
|
|
1169
|
+
"mode": "text",
|
|
1170
|
+
"in": "Vi gick 5 km i dag",
|
|
1171
|
+
"out": "Vi gick 5 km i dag",
|
|
1172
|
+
"note": "nbsp N5, cited to Språkrådet §4.3.4 and BIPM."
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
"id": "sv-nbsp-unit-no-space",
|
|
1176
|
+
"rule": "nbsp",
|
|
1177
|
+
"mode": "text",
|
|
1178
|
+
"in": "Vi gick 5km i dag",
|
|
1179
|
+
"out": "Vi gick 5km i dag",
|
|
1180
|
+
"note": "N5 converts, never inserts."
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
"id": "sv-nbsp-unit-already-bound",
|
|
1184
|
+
"rule": "nbsp",
|
|
1185
|
+
"mode": "text",
|
|
1186
|
+
"in": "Vi gick 5 km i dag",
|
|
1187
|
+
"out": "Vi gick 5 km i dag",
|
|
1188
|
+
"note": "Already-correct branch."
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
"id": "sv-nbsp-currency-unit",
|
|
1192
|
+
"rule": "nbsp",
|
|
1193
|
+
"mode": "text",
|
|
1194
|
+
"in": "Det kostar 100 kr",
|
|
1195
|
+
"out": "Det kostar 100 kr",
|
|
1196
|
+
"note": "Språkrådet lists `kr` among the Swedish abbreviations that behave like units, so it binds to the preceding number."
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
"id": "sv-nbsp-percent",
|
|
1200
|
+
"rule": "nbsp",
|
|
1201
|
+
"mode": "text",
|
|
1202
|
+
"in": "Priset steg 5 %",
|
|
1203
|
+
"out": "Priset steg 5 %",
|
|
1204
|
+
"note": "Språkrådet §1.16: `%` is written with a space, and fast mellanslag keeps the pair on one line."
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
"id": "sv-nbsp-section-sign",
|
|
1208
|
+
"rule": "nbsp",
|
|
1209
|
+
"mode": "text",
|
|
1210
|
+
"in": "se § 7 i lagen",
|
|
1211
|
+
"out": "se § 7 i lagen",
|
|
1212
|
+
"note": "THE fi/sv DIVERGENCE ON `§`: Språkrådet's example is `§ 7`, sign before number, so `§` is in sv's afterSymbols (N6). Finnish writes `6 §` and puts the same character in beforeUnits (N5)."
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
"id": "sv-nbsp-section-sign-already-bound",
|
|
1216
|
+
"rule": "nbsp",
|
|
1217
|
+
"mode": "text",
|
|
1218
|
+
"in": "se § 7 i lagen",
|
|
1219
|
+
"out": "se § 7 i lagen",
|
|
1220
|
+
"note": "N6's already-correct branch."
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
"id": "sv-nbsp-initials-not-bound",
|
|
1224
|
+
"rule": "nbsp",
|
|
1225
|
+
"mode": "text",
|
|
1226
|
+
"in": "A. B. Nilsson skrev den",
|
|
1227
|
+
"out": "A. B. Nilsson skrev den",
|
|
1228
|
+
"note": "initialBinding is \"none\" for sv (spec 0.6.0: the field used to be the boolean bindInitials, false): §1.16 recommends fast mellanslag only for signs and digits, not for personal initials."
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
"id": "sv-nbsp-formula-not-a-unit",
|
|
1232
|
+
"rule": "nbsp",
|
|
1233
|
+
"mode": "text",
|
|
1234
|
+
"in": "H2 O är inte ett värde",
|
|
1235
|
+
"out": "H2 O är inte ett värde",
|
|
1236
|
+
"note": "nbsp.md §6 case 18."
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
"id": "sv-nbsp-empty-lists",
|
|
1240
|
+
"rule": "nbsp",
|
|
1241
|
+
"mode": "text",
|
|
1242
|
+
"in": "Verkligen? Ja: här; nu!",
|
|
1243
|
+
"out": "Verkligen? Ja: här; nu!",
|
|
1244
|
+
"note": "beforePunctuation, narrowBeforePunctuation, afterShortWords, abbreviations, beforeNumber and beforeWord are all empty for sv — Swedish multi-word abbreviations are written closed up (t.ex., bl.a.), so there is no internal space to bind."
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
"id": "sv-nbsp-closed-abbreviation",
|
|
1248
|
+
"rule": "nbsp",
|
|
1249
|
+
"mode": "text",
|
|
1250
|
+
"in": "t.ex. bl.a. och m.m.",
|
|
1251
|
+
"out": "t.ex. bl.a. och m.m.",
|
|
1252
|
+
"note": "Språkrådet §4.5: the current recommendation writes these closed up, so `abbreviations` is empty and nothing binds."
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
"id": "sv-nbsp-quotes-innerspace-none",
|
|
1256
|
+
"rule": "nbsp",
|
|
1257
|
+
"mode": "text",
|
|
1258
|
+
"in": "Han sa ”hej”.",
|
|
1259
|
+
"out": "Han sa ”hej”.",
|
|
1260
|
+
"note": "nbsp.md §3.10 and §7.5: innerSpace is `none`, and N8 does nothing for a pair whose open and close are the same code point."
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
"id": "sv-nbsp-prose-everything",
|
|
1264
|
+
"rule": "nbsp",
|
|
1265
|
+
"mode": "text",
|
|
1266
|
+
"in": "Hon sa: \"Vi gick 5 km -- kanske mer...\" åren 1914-1918.",
|
|
1267
|
+
"out": "Hon sa: ”Vi gick 5 km – kanske mer…” åren 1914–1918.",
|
|
1268
|
+
"note": "Real prose exercising a same-glyph quotation, a spaced en dash, an ellipsis and a number-plus-unit at once. 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.",
|
|
1269
|
+
"rules": {
|
|
1270
|
+
"ranges": true
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
"id": "sv-quotes-030-elision-vs-closer-curly",
|
|
1275
|
+
"rule": "quotes",
|
|
1276
|
+
"mode": "text",
|
|
1277
|
+
"in": "’90s were fun,’ he said",
|
|
1278
|
+
"out": "”90s were fun,” he said",
|
|
1279
|
+
"note": "quotes.md §6 row E4: elision-vs-closer, curly input — see fi's own row for the reason no U+2019 restriction is applied."
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
"id": "sv-quotes-030-elision-vs-closer-straight",
|
|
1283
|
+
"rule": "quotes",
|
|
1284
|
+
"mode": "text",
|
|
1285
|
+
"in": "'90s were fun,' he said",
|
|
1286
|
+
"out": "”90s were fun,” he said",
|
|
1287
|
+
"note": "quotes.md §6 row E4s: the straight-input control for E4."
|
|
1288
|
+
}
|
|
1289
|
+
]
|
|
1290
|
+
}
|