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,501 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "1.0.0",
|
|
3
|
+
"locale": "de-CH",
|
|
4
|
+
"cases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "de-ch-spaces-collapse-run",
|
|
7
|
+
"rule": "spaces",
|
|
8
|
+
"mode": "text",
|
|
9
|
+
"in": "Hallo Welt.",
|
|
10
|
+
"out": "Hallo Welt.",
|
|
11
|
+
"note": "spaces.md §6 case 1."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "de-ch-spaces-before-punctuation",
|
|
15
|
+
"rule": "spaces",
|
|
16
|
+
"mode": "text",
|
|
17
|
+
"in": "See p. 12 .",
|
|
18
|
+
"out": "See p. 12.",
|
|
19
|
+
"note": "spaces.md §6 case 10."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "de-ch-spaces-checkbox",
|
|
23
|
+
"rule": "spaces",
|
|
24
|
+
"mode": "text",
|
|
25
|
+
"in": "- [ ] Milch kaufen",
|
|
26
|
+
"out": "- [ ] Milch kaufen",
|
|
27
|
+
"note": "spaces.md §6 case 4: the empty-bracket guard keeps the GFM task-list marker intact."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "de-ch-spaces-nobreak-not-collapsed",
|
|
31
|
+
"rule": "spaces",
|
|
32
|
+
"mode": "text",
|
|
33
|
+
"in": "a b",
|
|
34
|
+
"out": "a b",
|
|
35
|
+
"note": "spaces.md §6 case 8: two no-break spaces are never collapsed — this rule only ever removes U+0020."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "de-ch-ellipsis-three-dots",
|
|
39
|
+
"rule": "ellipsis",
|
|
40
|
+
"mode": "text",
|
|
41
|
+
"in": "Warte... was?",
|
|
42
|
+
"out": "Warte… was?",
|
|
43
|
+
"note": "ellipsis.md §6 case 1."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "de-ch-ellipsis-mixed-run",
|
|
47
|
+
"rule": "ellipsis",
|
|
48
|
+
"mode": "text",
|
|
49
|
+
"in": "Warte…… was",
|
|
50
|
+
"out": "Warte… was",
|
|
51
|
+
"note": "ellipsis.md §6 case 2."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "de-ch-ellipsis-two-dots-after-terminal",
|
|
55
|
+
"rule": "ellipsis",
|
|
56
|
+
"mode": "text",
|
|
57
|
+
"in": "Wirklich?..",
|
|
58
|
+
"out": "Wirklich?…",
|
|
59
|
+
"note": "ellipsis.md §6 case 3: abbreviatedAfterTerminal = false."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "de-ch-ellipsis-existing",
|
|
63
|
+
"rule": "ellipsis",
|
|
64
|
+
"mode": "text",
|
|
65
|
+
"in": "Er ging…",
|
|
66
|
+
"out": "Er ging…",
|
|
67
|
+
"note": "ellipsis.md §6 case 6."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "de-ch-dashes-parenthetical-tight-input",
|
|
71
|
+
"rule": "dashes",
|
|
72
|
+
"mode": "text",
|
|
73
|
+
"in": "Der Plan--falls es einen gibt--scheitert.",
|
|
74
|
+
"out": "Der Plan – falls es einen gibt – scheitert.",
|
|
75
|
+
"note": "dash.parenthetical = en-spaced — Bundeskanzlei Rz. 231/237/238: der Gedankenstrich ist der Halbgeviertstrich, mit Leerzeichen davor und danach."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "de-ch-dashes-authored-em-dash-converts",
|
|
79
|
+
"rule": "dashes",
|
|
80
|
+
"mode": "text",
|
|
81
|
+
"in": "Das Jahr — ein langes — endet.",
|
|
82
|
+
"out": "Das Jahr – ein langes – endet.",
|
|
83
|
+
"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: de-CH'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. Bundeskanzlei Rz. 236 states the Geviertstrich (U+2014) is not permitted in Swiss usage; the author's em dash is now corrected to the prescribed en dash rather than merely respaced or left alone. This fixture holds only for U+2014 — see `de-ch-dashes-authored-en-dash-tight-declined` for why U+2013 does not follow the same path since spec 0.6.0."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "de-ch-dashes-authored-en-dash-tight-declined",
|
|
87
|
+
"rule": "dashes",
|
|
88
|
+
"mode": "text",
|
|
89
|
+
"in": "Das Jahr–ein langes–endet.",
|
|
90
|
+
"out": "Das Jahr–ein langes–endet.",
|
|
91
|
+
"note": "dashes.md §3.4 P5 (spec 0.6.0): a pure single authored U+2013 is declined unconditionally, even though de-CH's `dash.parenthetical` target glyph is already `en-spaced` (U+2013) and Bundeskanzlei Rz. 236 forbids the Geviertstrich outright. Before P5 this tight authored en-dash would have been re-spaced; P5 protects the whole token, spacing included (§8.8)."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "de-ch-ranges-years",
|
|
95
|
+
"rule": "ranges",
|
|
96
|
+
"mode": "text",
|
|
97
|
+
"in": "Die Jahre 1939-1945",
|
|
98
|
+
"out": "Die Jahre 1939–1945",
|
|
99
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). dash.range = en-tight — Bundeskanzlei Rz. 234: der Bis-Strich steht ohne Leerzeichen, „die Jahre 1939–1945“. The hyphen-typed range converts and is bound with U+2060 word joiners (ranges.md §3.3.1), which are zero-width and visible only in spec/fixtures/.escaped/.",
|
|
100
|
+
"rules": {
|
|
101
|
+
"ranges": true
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "de-ch-ranges-hours",
|
|
106
|
+
"rule": "ranges",
|
|
107
|
+
"mode": "text",
|
|
108
|
+
"in": "16-17 Uhr",
|
|
109
|
+
"out": "16–17 Uhr",
|
|
110
|
+
"note": "Moved from `dashes` to `ranges` (spec 0.5.0, opt-in — `rules: {ranges: true}` explicit here since `ranges` defaults off). Rz. 234, second cited example, with the same U+2060 binding (ranges.md §3.3.1).",
|
|
111
|
+
"rules": {
|
|
112
|
+
"ranges": true
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "de-ch-ranges-roundtrip",
|
|
117
|
+
"rule": "ranges",
|
|
118
|
+
"mode": "text",
|
|
119
|
+
"in": "die Artikel 10–12",
|
|
120
|
+
"out": "die Artikel 10–12",
|
|
121
|
+
"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. An AUTHORED range: byte-identical out, and specifically with no U+2060 joiners, where the hyphen-typed `1939-1945` above gets them (§3.3.1, §7.14). Both forms look the same on screen, so nothing in the rendered document explains why one may break across lines and the other may not — the asymmetry is recorded rather than hidden. It follows from one principle: the rule never makes an edit whose entire content is invisible, because an invisible-only change to already-correct text cannot be reviewed by hand. ranges.md §3-§4 is the current normative source for this rule's own behaviour here.",
|
|
122
|
+
"rules": {
|
|
123
|
+
"ranges": true
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"id": "de-ch-ranges-iso-date",
|
|
128
|
+
"rule": "ranges",
|
|
129
|
+
"mode": "text",
|
|
130
|
+
"in": "Erschienen 2026-08-15",
|
|
131
|
+
"out": "Erschienen 2026-08-15",
|
|
132
|
+
"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. Guard G2 and the cluster guard. ranges.md §3.2's G2 (chain) is the current normative guard rejecting this.",
|
|
133
|
+
"rules": {
|
|
134
|
+
"ranges": true
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "de-ch-ranges-decreasing-range",
|
|
139
|
+
"rule": "ranges",
|
|
140
|
+
"mode": "text",
|
|
141
|
+
"in": "Resultat 20-10",
|
|
142
|
+
"out": "Resultat 20-10",
|
|
143
|
+
"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, the left value must not exceed the right. ranges.md §3.2's G5 (non-decreasing) is the current normative guard rejecting this.",
|
|
144
|
+
"rules": {
|
|
145
|
+
"ranges": true
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "de-ch-dashes-compound-hyphen",
|
|
150
|
+
"rule": "dashes",
|
|
151
|
+
"mode": "text",
|
|
152
|
+
"in": "Ein wohl-bekannter E-Mail-Adressat",
|
|
153
|
+
"out": "Ein wohl-bekannter E-Mail-Adressat",
|
|
154
|
+
"note": "Guard P1."
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "de-ch-dashes-composition-guard-t2",
|
|
158
|
+
"rule": "dashes",
|
|
159
|
+
"mode": "text",
|
|
160
|
+
"in": ".--.",
|
|
161
|
+
"out": ".--.",
|
|
162
|
+
"note": "dashes.md §6 case 28: guard T2, since de-CH is also an en-spaced locale."
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "de-ch-hyphen-noop-compounds",
|
|
166
|
+
"rule": "hyphen",
|
|
167
|
+
"mode": "text",
|
|
168
|
+
"in": "Ein E-Mail-Konto und der Nord-Süd-Verkehr",
|
|
169
|
+
"out": "Ein E-Mail-Konto und der Nord-Süd-Verkehr",
|
|
170
|
+
"note": "hyphen.md §6 case 13: all three hyphen lists are empty (Bundeskanzlei Rz. 223/224 — the only documented protected hyphen is the open-ended Ergänzungsstrich, which no literal list can express). A provable total no-op; a port that implements the rule anyway must fail here."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "de-ch-hyphen-noop-supplementary",
|
|
174
|
+
"rule": "hyphen",
|
|
175
|
+
"mode": "text",
|
|
176
|
+
"in": "Papierproduktion und -handel",
|
|
177
|
+
"out": "Papierproduktion und -handel",
|
|
178
|
+
"note": "The Ergänzungsstrich of Rz. 224 itself: normatively unbreakable and deliberately still untouched."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"id": "de-ch-quotes-primary-no-inner-space",
|
|
182
|
+
"rule": "quotes",
|
|
183
|
+
"mode": "text",
|
|
184
|
+
"in": "Er sagte \"hallo\" zweimal.",
|
|
185
|
+
"out": "Er sagte «hallo» zweimal.",
|
|
186
|
+
"note": "The headline Swiss case, and it contradicts what most references and most tools assume: Swiss German uses guillemets « », and there is NO space inside them. Bundeskanzlei Rz. 201–203 prescribes « » (and forbids the German » « for official texts); Rz. 202 regulates only the space BEFORE the opening and AFTER the closing mark, and none of its examples («Zukunft für Schweizer Fahrende») sets an inner space. Hence quotes.primary.innerSpace = none and N8 does nothing — unlike fr, where the same glyphs take U+00A0 inside."
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "de-ch-quotes-primary-roundtrip",
|
|
190
|
+
"rule": "quotes",
|
|
191
|
+
"mode": "text",
|
|
192
|
+
"in": "Er sagte «hallo» zweimal.",
|
|
193
|
+
"out": "Er sagte «hallo» zweimal.",
|
|
194
|
+
"note": "The fixed point of the previous case: no space is inserted on a second pass either."
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"id": "de-ch-quotes-inner-space-removed",
|
|
198
|
+
"rule": "quotes",
|
|
199
|
+
"mode": "text",
|
|
200
|
+
"in": "Er sagte « hallo » zweimal.",
|
|
201
|
+
"out": "Er sagte «hallo» zweimal.",
|
|
202
|
+
"note": "quotes.md §3.7, updated for spec 0.3.0 mandate 2: a space touching a quote mark is sloppiness, not evidence. Under 0.1.0 an already-correctly-guillemetted pair with a stray inner space was left untouched by `quotes` (no straight marks to act on). Under mandate 2 the pair is recognised on the first application and, since de-CH's innerSpace is none, the inner space is deleted by `quotes` itself rather than surviving as a typo. Renamed from de-ch-quotes-inner-space-not-removed, whose old id described 0.1.0 behaviour that mandate 2 reverses."
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "de-ch-quotes-nested",
|
|
206
|
+
"rule": "quotes",
|
|
207
|
+
"mode": "text",
|
|
208
|
+
"in": "\"Er sagte 'nein' zu mir\", notierte sie.",
|
|
209
|
+
"out": "«Er sagte ‹nein› zu mir», notierte sie.",
|
|
210
|
+
"note": "Depth 2 takes the halbe Anführungszeichen ‹ › (Rz. 201–203), not the German ‚ ‘."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": "de-ch-quotes-unbalanced",
|
|
214
|
+
"rule": "quotes",
|
|
215
|
+
"mode": "text",
|
|
216
|
+
"in": "Er sagte \"hallo",
|
|
217
|
+
"out": "Er sagte \"hallo",
|
|
218
|
+
"note": "quotes.md §3.6."
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"id": "de-ch-apostrophe-medial",
|
|
222
|
+
"rule": "apostrophe",
|
|
223
|
+
"mode": "text",
|
|
224
|
+
"in": "Ich hab's gesehen",
|
|
225
|
+
"out": "Ich hab’s gesehen",
|
|
226
|
+
"note": "apostrophe.md §6 case 1."
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"id": "de-ch-apostrophe-existing",
|
|
230
|
+
"rule": "apostrophe",
|
|
231
|
+
"mode": "text",
|
|
232
|
+
"in": "Ich hab’s gesehen",
|
|
233
|
+
"out": "Ich hab’s gesehen",
|
|
234
|
+
"note": "U+2019 is never examined."
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"id": "de-ch-apostrophe-isolated-mark",
|
|
238
|
+
"rule": "apostrophe",
|
|
239
|
+
"mode": "text",
|
|
240
|
+
"in": "a ' b",
|
|
241
|
+
"out": "a ' b",
|
|
242
|
+
"note": "apostrophe.md §6 case 9 and §7.4: nothing can be inferred from an isolated mark, so it stays straight — output text may legitimately still contain U+0027."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"id": "de-ch-symbols-copyright",
|
|
246
|
+
"rule": "symbols",
|
|
247
|
+
"mode": "text",
|
|
248
|
+
"in": "Copyright (c) 2026 Iurii Rogulia",
|
|
249
|
+
"out": "Copyright © 2026 Iurii Rogulia",
|
|
250
|
+
"note": "symbols.md §6 case 1."
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": "de-ch-symbols-adjacent-signs",
|
|
254
|
+
"rule": "symbols",
|
|
255
|
+
"mode": "text",
|
|
256
|
+
"in": "(c)(r)",
|
|
257
|
+
"out": "©(r)",
|
|
258
|
+
"note": "symbols.md §5: guard S1 lists ©, ® and ™ precisely so that `(c)(r)` converges in one run instead of two."
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"id": "de-ch-symbols-multiplication-spaced",
|
|
262
|
+
"rule": "symbols",
|
|
263
|
+
"mode": "text",
|
|
264
|
+
"in": "A 3x5 card, 10 x 20 grid",
|
|
265
|
+
"out": "A 3×5 card, 10 × 20 grid",
|
|
266
|
+
"note": "symbols.md §6 case 5 (verbatim): the tight and the symmetric-spaced form in one string, with nothing after either chain that another rule would claim."
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"id": "de-ch-symbols-multiplication-followed-by-unit",
|
|
270
|
+
"rule": "symbols",
|
|
271
|
+
"mode": "text",
|
|
272
|
+
"in": "Format 10 x 20 cm",
|
|
273
|
+
"out": "Format 10 × 20 cm",
|
|
274
|
+
"note": "The cross-rule case: the U+00D7 and its two ordinary spaces are `symbols` (§3.3 step 8 carries each side's space code point across unchanged), while the U+00A0 before `cm` is `nbsp` N5, because de-CH lists `cm` in beforeUnits. Two rules, two different space decisions in one string."
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"id": "de-ch-symbols-chain-converted",
|
|
278
|
+
"rule": "symbols",
|
|
279
|
+
"mode": "text",
|
|
280
|
+
"in": "Eine 2x3x4 Schachtel",
|
|
281
|
+
"out": "Eine 2×3×4 Schachtel",
|
|
282
|
+
"note": "symbols.md §6 case 5e: the whole chain converts in ONE pass. The branch is a scan of `DIGIT+ (X DIGIT+)+` (§3.3), not a pairwise test — a pairwise implementation converts one operator per pass and never converges, which is the non-idempotency §7.10 measures in Lebedev's Typograf. The former chain guard M5 was removed rather than extended, because M2/M3 are what made a pairwise chain reject itself."
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"id": "de-ch-symbols-plusminus-tight",
|
|
286
|
+
"rule": "symbols",
|
|
287
|
+
"mode": "text",
|
|
288
|
+
"in": "Tolerance +/-5 points",
|
|
289
|
+
"out": "Tolerance ±5 points",
|
|
290
|
+
"note": "symbols.md §6 case 5k (verbatim): the literal three code points `+/-` become U+00B1 when a digit follows."
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"id": "de-ch-symbols-plusminus-spaced",
|
|
294
|
+
"rule": "symbols",
|
|
295
|
+
"mode": "text",
|
|
296
|
+
"in": "Tolerance +/- 5 points",
|
|
297
|
+
"out": "Tolerance ± 5 points",
|
|
298
|
+
"note": "symbols.md §6 case 5l (verbatim): one intervening U+0020 is allowed by guard F2, and the space itself is left exactly as it was — it stays U+0020 here, because nothing else claims it."
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"id": "de-ch-symbols-plusminus-followed-by-unit",
|
|
302
|
+
"rule": "symbols",
|
|
303
|
+
"mode": "text",
|
|
304
|
+
"in": "Tolerance +/-5 mm",
|
|
305
|
+
"out": "Tolerance ±5 mm",
|
|
306
|
+
"note": "The cross-rule case for the tight form: U+00B1 from `symbols`, U+00A0 from `nbsp` N5 (beforeUnits `mm`). `symbols` never emits a no-break space."
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"id": "de-ch-symbols-plusminus-spaced-followed-by-unit",
|
|
310
|
+
"rule": "symbols",
|
|
311
|
+
"mode": "text",
|
|
312
|
+
"in": "Tolerance +/- 5 mm",
|
|
313
|
+
"out": "Tolerance ± 5 mm",
|
|
314
|
+
"note": "And for the spaced form. Note the two spaces get different verdicts in the same token: the one between ± and 5 is left as U+0020 by guard F2, while the one between 5 and mm becomes U+00A0 under nbsp N5."
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"id": "de-ch-symbols-plusminus-prose",
|
|
318
|
+
"rule": "symbols",
|
|
319
|
+
"mode": "text",
|
|
320
|
+
"in": "lines marked +/- were edited",
|
|
321
|
+
"out": "lines marked +/- were edited",
|
|
322
|
+
"note": "symbols.md §6 case 5n, and the reason guard F2 requires a digit at all: prose that NAMES the characters rather than using them must survive. A changelog line or a legend is exactly the content this package runs on, and converting it would silently corrupt a sentence about the ASCII symbols themselves."
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"id": "de-ch-symbols-plusminus-character-class",
|
|
326
|
+
"rule": "symbols",
|
|
327
|
+
"mode": "text",
|
|
328
|
+
"in": "match [+/-] once",
|
|
329
|
+
"out": "match [+/-] once",
|
|
330
|
+
"note": "symbols.md §6 case 5o: guard F1, a regular-expression character class — the one code-like context that survives into prose about code."
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"id": "de-ch-symbols-plusminus-bare-not-converted",
|
|
334
|
+
"rule": "symbols",
|
|
335
|
+
"mode": "text",
|
|
336
|
+
"in": "Range 5+-3",
|
|
337
|
+
"out": "Range 5+-3",
|
|
338
|
+
"note": "symbols.md §6 case 5p, §3.4 and §7.11: bare `+-` is NEVER converted, in any context, and the asymmetry with `+/-` is the design rather than an oversight — `+-` occurs in diff listings, ASCII table borders, regex classes and adjacent source operators, none of which is skipped in text mode. Do not add it later for symmetry."
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"id": "de-ch-nbsp-abbreviation-zb",
|
|
342
|
+
"rule": "nbsp",
|
|
343
|
+
"mode": "text",
|
|
344
|
+
"in": "z. B. so",
|
|
345
|
+
"out": "z. B. so",
|
|
346
|
+
"note": "N4 — Bundeskanzlei Rz. 438, mehrgliedrige Abkürzungen."
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"id": "de-ch-nbsp-abbreviation-zb-place",
|
|
350
|
+
"rule": "nbsp",
|
|
351
|
+
"mode": "text",
|
|
352
|
+
"in": "z. B. Bern",
|
|
353
|
+
"out": "z. B. Bern",
|
|
354
|
+
"note": "nbsp.md §6 case 14 in de-CH: guard C1-a keeps the initials sub-rule from gluing `B.` to the following capitalised word. The same false positive existed here as in de-DE."
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"id": "de-ch-nbsp-abbreviation-four-part",
|
|
358
|
+
"rule": "nbsp",
|
|
359
|
+
"mode": "text",
|
|
360
|
+
"in": "m. a. W. so",
|
|
361
|
+
"out": "m. a. W. so",
|
|
362
|
+
"note": "N4 binds every internal space of the listed form — Rz. 438 lists m. a. W. and m. w. H. explicitly."
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"id": "de-ch-nbsp-before-word-sankt",
|
|
366
|
+
"rule": "nbsp",
|
|
367
|
+
"mode": "text",
|
|
368
|
+
"in": "St. Gallen und St. Moritz",
|
|
369
|
+
"out": "St. Gallen und St. Moritz",
|
|
370
|
+
"note": "N10 — Rz. 438 names exactly this case: die eingliedrige Abkürzung „St.“ für „Sankt“ in Ortsnamen. Three code points, so guard G-D does not apply."
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"id": "de-ch-nbsp-units",
|
|
374
|
+
"rule": "nbsp",
|
|
375
|
+
"mode": "text",
|
|
376
|
+
"in": "Es sind 20 km und 5 %",
|
|
377
|
+
"out": "Es sind 20 km und 5 %",
|
|
378
|
+
"note": "N5 twice — Rz. 251, 549–550 (Zahl und Einheit) and Rz. 554 (vor % und ‰)."
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"id": "de-ch-nbsp-unit-never-inserts",
|
|
382
|
+
"rule": "nbsp",
|
|
383
|
+
"mode": "text",
|
|
384
|
+
"in": "Es sind 20km",
|
|
385
|
+
"out": "Es sind 20km",
|
|
386
|
+
"note": "nbsp.md §7.2: N5 converts, never inserts."
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"id": "de-ch-nbsp-before-number",
|
|
390
|
+
"rule": "nbsp",
|
|
391
|
+
"mode": "text",
|
|
392
|
+
"in": "siehe S. 17 und Ziff. 3",
|
|
393
|
+
"out": "siehe S. 17 und Ziff. 3",
|
|
394
|
+
"note": "N9 — Rz. 251 („S. 17 f.“) and Rz. 727 (abgekürzte Gliederungseinheiten Kap., Art., Abs., Ziff.)."
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"id": "de-ch-nbsp-before-number-abs-kap",
|
|
398
|
+
"rule": "nbsp",
|
|
399
|
+
"mode": "text",
|
|
400
|
+
"in": "Abs. 2 und Kap. 4",
|
|
401
|
+
"out": "Abs. 2 und Kap. 4",
|
|
402
|
+
"note": "N9 on the remaining two Gliederungseinheiten."
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"id": "de-ch-nbsp-symbol-number",
|
|
406
|
+
"rule": "nbsp",
|
|
407
|
+
"mode": "text",
|
|
408
|
+
"in": "Art. 35 und § 12 und Nr. 5",
|
|
409
|
+
"out": "Art. 35 und § 12 und Nr. 5",
|
|
410
|
+
"note": "Three abbreviation-and-number forms that look alike and are reached three different ways, which is the point of the case. `Art.` binds through N9 (beforeNumber): the Schreibweisungen define Begriffszeichen in Rz. 439 as a closed list — Ziffern, %, ‰, Gedankenstrich, Schrägstrich, §, Währungszeichen, mathematische Zeichen, Einheitenzeichen — and `Art.` is not in it, while Rz. 727 puts it on the abbreviation side beside Abs., Ziff. and Kap. `§` binds through N6 (afterSymbols), where Rz. 439 does list it. `Nr.` binds through neither: it was removed from this file for want of any source that states a spacing rule for it. The difference between the three is evidence, not how German looks."
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"id": "de-ch-nbsp-nr-unbound-pending-citation",
|
|
414
|
+
"rule": "nbsp",
|
|
415
|
+
"mode": "text",
|
|
416
|
+
"in": "siehe Nr. 5",
|
|
417
|
+
"out": "siehe Nr. 5",
|
|
418
|
+
"note": "Pinned separately so a future contributor knows exactly which fixture to flip: if DIN 5008 (paywalled, unread) turns out to state the binding, `Nr.` goes into nbsp.beforeNumber — not afterSymbols, since Rz. 439's closed list excludes it and Duden classifies it as an abbreviation — and this `out` becomes `siehe Nr.` + U+00A0 + `5`."
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"id": "de-ch-nbsp-before-number-left-boundary",
|
|
422
|
+
"rule": "nbsp",
|
|
423
|
+
"mode": "text",
|
|
424
|
+
"in": "Fig.Art. 5 und Muster-Art. 5",
|
|
425
|
+
"out": "Fig.Art. 5 und Muster-Art. 5",
|
|
426
|
+
"note": "Why the afterSymbols → beforeNumber move for `Art.` is not cosmetic. N6 asks only that the code point before the entry is not ALNUM, so both of these bound while `Art.` was an afterSymbols entry. N9's left guard is much stronger — cp[a-1] must be NONE, SPACELIKE, OPENISH or SENTENCE-DASH (nbsp.md §3.12 step 2) — so a preceding full stop or hyphen now declines the match, and neither of these binds."
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"id": "de-ch-nbsp-initials",
|
|
430
|
+
"rule": "nbsp",
|
|
431
|
+
"mode": "text",
|
|
432
|
+
"in": "J. K. Rowling",
|
|
433
|
+
"out": "J. K. Rowling",
|
|
434
|
+
"note": "N7 clause C1 twice, confirmed chain of two initials before the surname. initialBinding: \"chain\" (spec 0.6.0) is a derivation in this locale file, not a literal Weisung — Rz. 251 names „eine Abkürzung und ein Ortsname“ and Rz. 438 the parts of multi-token abbreviations."
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"id": "de-ch-nbsp-no-french-spacing",
|
|
438
|
+
"rule": "nbsp",
|
|
439
|
+
"mode": "text",
|
|
440
|
+
"in": "Wirklich ? Ja !",
|
|
441
|
+
"out": "Wirklich? Ja!",
|
|
442
|
+
"note": "Both punctuation lists are empty: Swiss German is not French, even though it shares the guillemets."
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"id": "de-ch-nbsp-plain-sentence",
|
|
446
|
+
"rule": "nbsp",
|
|
447
|
+
"mode": "text",
|
|
448
|
+
"in": "Er ging einfach nach Hause",
|
|
449
|
+
"out": "Er ging einfach nach Hause",
|
|
450
|
+
"note": "No short word list, no abbreviation, no unit, no initial."
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"id": "de-ch-roundtrip-typeset-sentence",
|
|
454
|
+
"rule": "nbsp",
|
|
455
|
+
"mode": "text",
|
|
456
|
+
"in": "St. Gallen liegt 20 km entfernt – siehe S. 17.",
|
|
457
|
+
"out": "St. Gallen liegt 20 km entfernt – siehe S. 17.",
|
|
458
|
+
"note": "Byte-identical round trip: a bound place-name abbreviation, a bound unit, an en-spaced dash and a bound page reference."
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
"id": "de-ch-roundtrip-quotes-and-range",
|
|
462
|
+
"rule": "quotes",
|
|
463
|
+
"mode": "text",
|
|
464
|
+
"in": "«Die Jahre 1939–1945», sagte er.",
|
|
465
|
+
"out": "«Die Jahre 1939–1945», sagte er.",
|
|
466
|
+
"note": "Correct Swiss guillemets without inner space, plus a tight en-dash range already exactly in de-CH's `en-tight` form. §3.3.1's invisible-edit test computes the unbound replacement first; it is identical to the input, so no joiners are added and nothing is emitted at all (§7.14) — this holds regardless of whether the dash was hyphen- or en-typed (spec 0.2.0 retired the authored-dash guard). Correctly typeset Swiss copy round-trips byte-for-byte, including the invisible layer."
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"id": "de-ch-roundtrip-plain-prose",
|
|
470
|
+
"rule": "spaces",
|
|
471
|
+
"mode": "text",
|
|
472
|
+
"in": "Ein ganz gewöhnlicher Satz bleibt unverändert.",
|
|
473
|
+
"out": "Ein ganz gewöhnlicher Satz bleibt unverändert.",
|
|
474
|
+
"note": "No candidate for any rule."
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"id": "de-ch-combined-sentence",
|
|
478
|
+
"rule": "nbsp",
|
|
479
|
+
"mode": "text",
|
|
480
|
+
"in": "Er sagte: \"Die Strecke -- rund 20 km -- war lang...\" und ging nach St. Gallen.",
|
|
481
|
+
"out": "Er sagte: «Die Strecke – rund 20 km – war lang…» und ging nach St. Gallen.",
|
|
482
|
+
"note": "A quotation, a parenthetical dash, an ellipsis and a number with a unit in one sentence: guillemets without inner space, en-spaced dashes (never an em dash), U+2026, U+00A0 between 20 and km, and St. bound to Gallen."
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
"id": "de-ch-quotes-041-cobug-apostrophe-v1",
|
|
486
|
+
"rule": "quotes",
|
|
487
|
+
"mode": "text",
|
|
488
|
+
"in": "\"B'’\"‹",
|
|
489
|
+
"out": "«B’’»‹",
|
|
490
|
+
"note": "Implementation-discovered composition-obligation violation against I5 by apostrophe (R6), closed by quotes.md §3.2's V1ID normalisation (spec 0.4.1). Before the fix: on pass 1, quotes' raw pairing finds a crossing WIDE pair (the two U+0022) and a NARROW pair (U+2019/U+2039), both assigned depth 1 (primary), and the certification gate rejects the whole set down to the empty set because rendering both to the same primary glyph creates a new V1-adjacency the re-derivation cannot reproduce -- so quotes makes no edit, and apostrophe then converts the sole unmatched U+0027 to U+2019. On pass 2 the input has two literally-identical adjacent U+2019 where pass 1 had one U+0027 and one U+2019 (different code points); V1's old literal comparison newly fires and drops both from candidacy, removing the crossing NARROW pair and letting the WIDE pair certify cleanly -- a different, non-idempotent result. V1ID makes U+0027 and U+2019 the same identity for V1's own comparison on both passes, so pass 1 already reaches the fixed point this fixture asserts. Found by the fast-check idempotency property sweep; reproduces deterministically (not a flaky/seed-dependent finding). See quotes.md §5 Lemma A / Corollary A1 and apostrophe.md §5's Against I5 section."
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"id": "de-ch-quotes-rock-n-roll",
|
|
494
|
+
"rule": "quotes",
|
|
495
|
+
"mode": "text",
|
|
496
|
+
"in": "rock 'n' roll",
|
|
497
|
+
"out": "rock 'n' roll",
|
|
498
|
+
"note": "spec 0.5.0: de-CH has no cited quotes.elisionIdioms entry for this idiom (quotes.md §7 item 8 — only en-US does, via Chicago Manual of Style Online and the American Heritage Dictionary). The general ambiguous-medial-span veto (quotes.md 3.2) preserves both marks as literal U+0027 rather than pairing them as an ordinary quotation, closing the false positive AUDIT_REMEDIATION_AND_RELEASE_PLAN.md 3.1 named for \"equivalent cases in several locales.\""
|
|
499
|
+
}
|
|
500
|
+
]
|
|
501
|
+
}
|