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,1306 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "1.0.0",
|
|
3
|
+
"locale": "fi",
|
|
4
|
+
"cases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-dashes-parenthetical-spaced-input",
|
|
231
|
+
"rule": "dashes",
|
|
232
|
+
"mode": "text",
|
|
233
|
+
"in": "Suunnitelma - jos sellainen on - kaatuu.",
|
|
234
|
+
"out": "Suunnitelma – jos sellainen on – kaatuu.",
|
|
235
|
+
"note": "dashes.md §3.4: en-spaced keeps the spacing and normalises the stroke."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"id": "fi-dashes-parenthetical-double-hyphen",
|
|
239
|
+
"rule": "dashes",
|
|
240
|
+
"mode": "text",
|
|
241
|
+
"in": "Suunnitelma--jos sellainen on--kaatuu.",
|
|
242
|
+
"out": "Suunnitelma – jos sellainen on – kaatuu.",
|
|
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": "fi-dashes-parenthetical-already-correct",
|
|
247
|
+
"rule": "dashes",
|
|
248
|
+
"mode": "text",
|
|
249
|
+
"in": "Suunnitelma – jos sellainen on – kaatuu.",
|
|
250
|
+
"out": "Suunnitelma – jos sellainen on – kaatuu.",
|
|
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": "fi-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": "fi-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": "fi-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 `fi-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 `fi-ranges-iso-8859` for the range-guard half."
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"id": "fi-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 `fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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 `fi-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": "fi-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": "fi-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 `fi-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": "fi-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": "fi-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 `fi-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": "fi-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": "fi-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": "fi-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": "fi-dashes-authored-em-dash-converts",
|
|
439
|
+
"rule": "dashes",
|
|
440
|
+
"mode": "text",
|
|
441
|
+
"in": "asiakirjat — PDF-tiedostot — ja sinä",
|
|
442
|
+
"out": "asiakirjat – PDF-tiedostot – ja sinä",
|
|
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: fi'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. Finnish now pulls the author's em dash to the en dash Kotus prescribes, rather than leaving its glyph untouched. This fixture holds only for U+2014 — see `fi-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": "fi-dashes-authored-en-dash-tight-declined",
|
|
447
|
+
"rule": "dashes",
|
|
448
|
+
"mode": "text",
|
|
449
|
+
"in": "asiakirjat–PDF-tiedostot–ja sinä",
|
|
450
|
+
"out": "asiakirjat–PDF-tiedostot–ja sinä",
|
|
451
|
+
"note": "dashes.md §3.4 P5 (spec 0.6.0): a pure single authored U+2013 is declined unconditionally, even though fi'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": "fi-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": "fi-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": "fi-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": "fi-ranges-one-two-digits",
|
|
485
|
+
"rule": "ranges",
|
|
486
|
+
"mode": "text",
|
|
487
|
+
"in": "Kestää 5-10 päivää",
|
|
488
|
+
"out": "Kestää 5–10 päivää",
|
|
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": "fi-ranges-nine-ten",
|
|
496
|
+
"rule": "ranges",
|
|
497
|
+
"mode": "text",
|
|
498
|
+
"in": "iältään 9-10 vuotta",
|
|
499
|
+
"out": "iältään 9–10 vuotta",
|
|
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": "fi-ranges-one-twelve",
|
|
507
|
+
"rule": "ranges",
|
|
508
|
+
"mode": "text",
|
|
509
|
+
"in": "luvut 1-12",
|
|
510
|
+
"out": "luvut 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": "fi-ranges-zero-sixty",
|
|
518
|
+
"rule": "ranges",
|
|
519
|
+
"mode": "text",
|
|
520
|
+
"in": "0-60 kuudessa sekunnissa",
|
|
521
|
+
"out": "0–60 kuudessa sekunnissa",
|
|
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": "fi-ranges-two-one-digits-rejected",
|
|
529
|
+
"rule": "ranges",
|
|
530
|
+
"mode": "text",
|
|
531
|
+
"in": "voitti 10-7",
|
|
532
|
+
"out": "voitti 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": "fi-ranges-leading-zero-rejected",
|
|
540
|
+
"rule": "ranges",
|
|
541
|
+
"mode": "text",
|
|
542
|
+
"in": "koodi 9-05",
|
|
543
|
+
"out": "koodi 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": "fi-ranges-phone-seven-digits",
|
|
551
|
+
"rule": "ranges",
|
|
552
|
+
"mode": "text",
|
|
553
|
+
"in": "Soita 555-1234",
|
|
554
|
+
"out": "Soita 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": "fi-ranges-one-three-digits-rejected",
|
|
562
|
+
"rule": "ranges",
|
|
563
|
+
"mode": "text",
|
|
564
|
+
"in": "Soita 1-800 nyt",
|
|
565
|
+
"out": "Soita 1-800 nyt",
|
|
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": "fi-ranges-abbreviated-years-rejected",
|
|
573
|
+
"rule": "ranges",
|
|
574
|
+
"mode": "text",
|
|
575
|
+
"in": "kausi 2020-24",
|
|
576
|
+
"out": "kausi 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": "fi-dashes-range-compound-label-safe-default",
|
|
584
|
+
"rule": "dashes",
|
|
585
|
+
"mode": "text",
|
|
586
|
+
"in": "Kuva 5-10",
|
|
587
|
+
"out": "Kuva 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": "fi-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": "fi-hyphen-empty-lists-prose",
|
|
600
|
+
"rule": "hyphen",
|
|
601
|
+
"mode": "text",
|
|
602
|
+
"in": "Tämä on tavallinen suomenkielinen virke.",
|
|
603
|
+
"out": "Tämä on tavallinen suomenkielinen virke.",
|
|
604
|
+
"note": "hyphen.md §6 case 14 / §2: with three empty lists the rule emits nothing for any input."
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"id": "fi-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": "fi-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": "fi-quotes-basic",
|
|
624
|
+
"rule": "quotes",
|
|
625
|
+
"mode": "text",
|
|
626
|
+
"in": "Hän sanoi \"moi\" ja lähti.",
|
|
627
|
+
"out": "Hän sanoi ”moi” ja lähti.",
|
|
628
|
+
"note": "quotes.md §6 case 9: the opening and the closing glyph are the SAME code point U+201D (Kotus). The rule knew which was which from the pairing, not from the glyph."
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
"id": "fi-quotes-roundtrip-same-glyph",
|
|
632
|
+
"rule": "quotes",
|
|
633
|
+
"mode": "text",
|
|
634
|
+
"in": "Hän sanoi ”moi” ja lähti.",
|
|
635
|
+
"out": "Hän sanoi ”moi” ja lähti.",
|
|
636
|
+
"note": "quotes.md §6 case 10: the output of the previous case re-processed. No STRAIGHT marks remain, so there is nothing to do. This is the case a flip-flop state machine gets wrong."
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
"id": "fi-quotes-nested",
|
|
640
|
+
"rule": "quotes",
|
|
641
|
+
"mode": "text",
|
|
642
|
+
"in": "\"Hän sanoi 'moi'\", totesin.",
|
|
643
|
+
"out": "”Hän sanoi ’moi’”, totesin.",
|
|
644
|
+
"note": "quotes.md §6 case 11: depth 2 → secondary, also same-glyph on both sides. The `'` and `\"` at the end are adjacent but of DIFFERENT kinds, so the pass 1 veto does not touch them."
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
"id": "fi-quotes-nested-roundtrip",
|
|
648
|
+
"rule": "quotes",
|
|
649
|
+
"mode": "text",
|
|
650
|
+
"in": "”Hän sanoi ’moi’”, totesin.",
|
|
651
|
+
"out": "”Hän sanoi ’moi’”, totesin.",
|
|
652
|
+
"note": "The nested same-glyph output is a fixed point: three U+2019/U+201D glyphs in a row are not candidates at all."
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"id": "fi-quotes-nesting-with-question",
|
|
656
|
+
"rule": "quotes",
|
|
657
|
+
"mode": "text",
|
|
658
|
+
"in": "\"Hän kysyi: 'mitä nyt?'\"",
|
|
659
|
+
"out": "”Hän kysyi: ’mitä nyt?’”",
|
|
660
|
+
"note": "Nesting where the inner quotation ends in terminal punctuation and both closers stack up at the end of the text unit."
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"id": "fi-quotes-unbalanced-opener",
|
|
664
|
+
"rule": "quotes",
|
|
665
|
+
"mode": "text",
|
|
666
|
+
"in": "Hän aloitti \"lainauksen mutta ei sulkenut sitä.",
|
|
667
|
+
"out": "Hän aloitti \"lainauksen mutta ei sulkenut sitä.",
|
|
668
|
+
"note": "quotes.md §3.6: an unmatchable opening mark is left as U+0022. In a same-glyph locale guessing would be unrecoverable, because the reader cannot tell an opener from a closer afterwards."
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
"id": "fi-quotes-unbalanced-closer",
|
|
672
|
+
"rule": "quotes",
|
|
673
|
+
"mode": "text",
|
|
674
|
+
"in": "Hän poistui huoneesta\" avaamatta lainausta.",
|
|
675
|
+
"out": "Hän poistui huoneesta\" avaamatta lainausta.",
|
|
676
|
+
"note": "quotes.md §3.6: a closing mark with nothing open."
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
"id": "fi-quotes-unbalanced-mixed",
|
|
680
|
+
"rule": "quotes",
|
|
681
|
+
"mode": "text",
|
|
682
|
+
"in": "Hän sanoi \"moi. Hän sanoi \"hei.\"",
|
|
683
|
+
"out": "Hän sanoi \"moi. Hän sanoi ”hei.”",
|
|
684
|
+
"note": "quotes.md §6 case 4 in a same-glyph locale: the first mark survives unmatched and the second pair resolves at depth 1 (§3.4 counts enclosing PAIRS, not stack height)."
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
"id": "fi-quotes-adjacent-to-punctuation",
|
|
688
|
+
"rule": "quotes",
|
|
689
|
+
"mode": "text",
|
|
690
|
+
"in": "\"Moi\", hän sanoi, \"hei\".",
|
|
691
|
+
"out": "”Moi”, hän sanoi, ”hei”.",
|
|
692
|
+
"note": "A quotation adjacent to punctuation: the closing marks are canClose because `,` and `.` are in CLOSEISH. Four identical U+201D glyphs in the output, two of which are openers."
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
"id": "fi-quotes-terminal-inside-quotation",
|
|
696
|
+
"rule": "quotes",
|
|
697
|
+
"mode": "text",
|
|
698
|
+
"in": "\"Moi!\" hän huusi.",
|
|
699
|
+
"out": "”Moi!” hän huusi.",
|
|
700
|
+
"note": "The closing mark's left neighbour is `!`, which is not SPACELIKE, so canClose holds."
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
"id": "fi-quotes-two-quotations",
|
|
704
|
+
"rule": "quotes",
|
|
705
|
+
"mode": "text",
|
|
706
|
+
"in": "Hän sanoi ”moi” ja ”hei”.",
|
|
707
|
+
"out": "Hän sanoi ”moi” ja ”hei”.",
|
|
708
|
+
"note": "Two already-correct same-glyph quotations in one sentence, byte-identical."
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
"id": "fi-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": "fi-quotes-doubled-single",
|
|
720
|
+
"rule": "quotes",
|
|
721
|
+
"mode": "text",
|
|
722
|
+
"in": "Kirjain ''",
|
|
723
|
+
"out": "Kirjain ''",
|
|
724
|
+
"note": "quotes.md §6 case 7: identical adjacent straight marks are vetoed in pass 1."
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
"id": "fi-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": "fi-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: the vetoed inner pair stays straight inside a converted same-glyph quotation."
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
"id": "fi-quotes-existing-curly-ignored",
|
|
744
|
+
"rule": "quotes",
|
|
745
|
+
"mode": "text",
|
|
746
|
+
"in": "“Jo kaareva,” hän sanoi, ja \"tämäkin.\"",
|
|
747
|
+
"out": "”Jo kaareva,” hän sanoi, ja ”tämäkin.”",
|
|
748
|
+
"note": "quotes.md §6 case 8, updated for spec 0.3.0 mandate 1: the curly WIDE pair (U+201C/U+201D, a foreign shape for fi, whose own primary is U+201D on both sides) is now a candidate and is rewritten to fi's own primary glyph; the straight pair converts the same way."
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
"id": "fi-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, recorded as a known defect: the two elision marks pair at depth 1 and take the Finnish primary glyphs. spec 0.5.0: fi's own dictionary writes this loanword closed up (rock'n'roll, no spaces) — quotes.md §7 item 8 — so a citation for the spaced form would be evidenced-inert and none was added. The general ambiguous-medial-span veto now preserves the spaced ASCII input as-is rather than pairing it as an ordinary quotation, which is the pre-0.5.0 behaviour this fixture used to assert."
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"id": "fi-quotes-already-correct",
|
|
760
|
+
"rule": "quotes",
|
|
761
|
+
"mode": "text",
|
|
762
|
+
"in": "”Kyllä”, hän sanoi – vuosina 1914–1918, noin 5 km.",
|
|
763
|
+
"out": "”Kyllä”, hän sanoi – vuosina 1914–1918, noin 5 km.",
|
|
764
|
+
"note": "Round-trip: already-correct Finnish text comes out byte-identical, including the two U+2060 joiners around the tight range (dashes.md §3.3.1)."
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
"id": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-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": "fi-nbsp-unit",
|
|
1168
|
+
"rule": "nbsp",
|
|
1169
|
+
"mode": "text",
|
|
1170
|
+
"in": "Matkaa on 20 km rannikolle",
|
|
1171
|
+
"out": "Matkaa on 20 km rannikolle",
|
|
1172
|
+
"note": "nbsp N5, cited to Kotus and BIPM."
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
"id": "fi-nbsp-unit-no-space",
|
|
1176
|
+
"rule": "nbsp",
|
|
1177
|
+
"mode": "text",
|
|
1178
|
+
"in": "Matkaa on 20km rannikolle",
|
|
1179
|
+
"out": "Matkaa on 20km rannikolle",
|
|
1180
|
+
"note": "N5 converts, never inserts."
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
"id": "fi-nbsp-unit-already-bound",
|
|
1184
|
+
"rule": "nbsp",
|
|
1185
|
+
"mode": "text",
|
|
1186
|
+
"in": "Matkaa on 20 km rannikolle",
|
|
1187
|
+
"out": "Matkaa on 20 km rannikolle",
|
|
1188
|
+
"note": "Already-correct branch."
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
"id": "fi-nbsp-percent",
|
|
1192
|
+
"rule": "nbsp",
|
|
1193
|
+
"mode": "text",
|
|
1194
|
+
"in": "Hinta nousi 10,5 %",
|
|
1195
|
+
"out": "Hinta nousi 10,5 %",
|
|
1196
|
+
"note": "Finnish writes a space between the numeral and `%` (Kotus), so this binding is the normal Finnish form rather than a scientific variant."
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
"id": "fi-nbsp-section-sign",
|
|
1200
|
+
"rule": "nbsp",
|
|
1201
|
+
"mode": "text",
|
|
1202
|
+
"in": "Katso 6 § tarkemmin",
|
|
1203
|
+
"out": "Katso 6 § tarkemmin",
|
|
1204
|
+
"note": "Kotus sets the section sign AFTER the number, so `§` is in beforeUnits for Finnish — the opposite arrangement from Swedish, where Språkrådet's example is `§ 7` and the sign is in afterSymbols."
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
"id": "fi-nbsp-abbreviation",
|
|
1208
|
+
"rule": "nbsp",
|
|
1209
|
+
"mode": "text",
|
|
1210
|
+
"in": "fil. maist. Virtanen",
|
|
1211
|
+
"out": "fil. maist. Virtanen",
|
|
1212
|
+
"note": "nbsp N4: `fil. maist.` is the one space-bearing abbreviation the fi locale file lists verbatim from Kotus. Only the space INSIDE the abbreviation binds; the space before the surname does not, because beforeWord is empty and initialBinding is \"none\" (spec 0.6.0: the field used to be the boolean bindInitials)."
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
"id": "fi-nbsp-abbreviation-already-bound",
|
|
1216
|
+
"rule": "nbsp",
|
|
1217
|
+
"mode": "text",
|
|
1218
|
+
"in": "fil. maist. Virtanen",
|
|
1219
|
+
"out": "fil. maist. Virtanen",
|
|
1220
|
+
"note": "nbsp.md §3.6: N4 matches space-leniently, so its own output still matches and emits nothing."
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
"id": "fi-nbsp-initials-not-bound",
|
|
1224
|
+
"rule": "nbsp",
|
|
1225
|
+
"mode": "text",
|
|
1226
|
+
"in": "J. K. Paasikivi oli presidentti",
|
|
1227
|
+
"out": "J. K. Paasikivi oli presidentti",
|
|
1228
|
+
"note": "initialBinding is \"none\" for fi (spec 0.6.0: the field used to be the boolean bindInitials, false): Kotus prescribes a space between initials but no consulted page states it must be non-breaking, so the stronger claim is not made."
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
"id": "fi-nbsp-formula-not-a-unit",
|
|
1232
|
+
"rule": "nbsp",
|
|
1233
|
+
"mode": "text",
|
|
1234
|
+
"in": "H2 O ei ole arvo",
|
|
1235
|
+
"out": "H2 O ei ole arvo",
|
|
1236
|
+
"note": "nbsp.md §6 case 18: N5 step 4 rejects a digit run preceded by a letter."
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
"id": "fi-nbsp-empty-lists",
|
|
1240
|
+
"rule": "nbsp",
|
|
1241
|
+
"mode": "text",
|
|
1242
|
+
"in": "Todellako? Kyllä: tässä; nyt!",
|
|
1243
|
+
"out": "Todellako? Kyllä: tässä; nyt!",
|
|
1244
|
+
"note": "beforePunctuation, narrowBeforePunctuation, afterShortWords, beforeNumber, beforeWord and afterSymbols are all empty for fi."
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
"id": "fi-nbsp-quotes-innerspace-none",
|
|
1248
|
+
"rule": "nbsp",
|
|
1249
|
+
"mode": "text",
|
|
1250
|
+
"in": "Hän sanoi ”moi”.",
|
|
1251
|
+
"out": "Hän sanoi ”moi”.",
|
|
1252
|
+
"note": "nbsp.md §3.10 and §7.5: innerSpace is `none`, and in any case N8 does nothing for a pair whose open and close are the same code point."
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
"id": "fi-nbsp-prose-everything",
|
|
1256
|
+
"rule": "nbsp",
|
|
1257
|
+
"mode": "text",
|
|
1258
|
+
"in": "Hän sanoi: \"Kävelimme 5 km -- ehkä enemmän...\" vuosina 1914-1918.",
|
|
1259
|
+
"out": "Hän sanoi: ”Kävelimme 5 km – ehkä enemmän…” vuosina 1914–1918.",
|
|
1260
|
+
"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.",
|
|
1261
|
+
"rules": {
|
|
1262
|
+
"ranges": true
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
"id": "fi-quotes-030-same-glyph-nesting-noop",
|
|
1267
|
+
"rule": "quotes",
|
|
1268
|
+
"mode": "text",
|
|
1269
|
+
"in": "”Hän sanoi ’moi’”, totesin.",
|
|
1270
|
+
"out": "”Hän sanoi ’moi’”, totesin.",
|
|
1271
|
+
"note": "quotes.md §6 row B: same-glyph already-correct nesting is a no-op — the row 0.1.0 §3.8 said was impossible. The two ” marks are told apart not by glyph but by outer context plus stack alternation."
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
"id": "fi-quotes-030-same-glyph-from-straight",
|
|
1275
|
+
"rule": "quotes",
|
|
1276
|
+
"mode": "text",
|
|
1277
|
+
"in": "\"Hän sanoi 'moi'\", totesin.",
|
|
1278
|
+
"out": "”Hän sanoi ’moi’”, totesin.",
|
|
1279
|
+
"note": "quotes.md §6 row 11: row B's input reached from straight marks."
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
"id": "fi-quotes-030-unbalanced-same-glyph",
|
|
1283
|
+
"rule": "quotes",
|
|
1284
|
+
"mode": "text",
|
|
1285
|
+
"in": "”Hän sanoi ”moi” ja lähti.",
|
|
1286
|
+
"out": "”Hän sanoi ”moi” ja lähti.",
|
|
1287
|
+
"note": "quotes.md §6 row U1: unbalanced same-glyph. Three ”, the stray leading one is left alone — the same-glyph ambiguity genuinely cannot be resolved, and the answer is the one §3.6 has always given."
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
"id": "fi-quotes-030-elision-vs-closer-curly",
|
|
1291
|
+
"rule": "quotes",
|
|
1292
|
+
"mode": "text",
|
|
1293
|
+
"in": "’90s were fun,’ he said",
|
|
1294
|
+
"out": "”90s were fun,” he said",
|
|
1295
|
+
"note": "quotes.md §6 row E3: elision-vs-closer, curly input, fi's same-glyph secondary pair is U+2019 on both sides, which is exactly why Corollary A1 applies no U+2019 restriction — forcing canOpen=false for U+2019 would make fi's own secondary pair unrecognisable."
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
"id": "fi-quotes-030-elision-vs-closer-straight",
|
|
1299
|
+
"rule": "quotes",
|
|
1300
|
+
"mode": "text",
|
|
1301
|
+
"in": "'90s were fun,' he said",
|
|
1302
|
+
"out": "”90s were fun,” he said",
|
|
1303
|
+
"note": "quotes.md §6 row E3s: the straight-input control for E3."
|
|
1304
|
+
}
|
|
1305
|
+
]
|
|
1306
|
+
}
|