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,688 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "1.0.0",
|
|
3
|
+
"locale": "ru",
|
|
4
|
+
"cases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "ru-spaces-collapse-run",
|
|
7
|
+
"rule": "spaces",
|
|
8
|
+
"mode": "text",
|
|
9
|
+
"in": "Привет мир.",
|
|
10
|
+
"out": "Привет мир.",
|
|
11
|
+
"note": "spaces.md §6 case 1."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "ru-spaces-before-punctuation",
|
|
15
|
+
"rule": "spaces",
|
|
16
|
+
"mode": "text",
|
|
17
|
+
"in": "Привет , мир !",
|
|
18
|
+
"out": "Привет, мир!",
|
|
19
|
+
"note": "spaces.md §6 case 2: the comma and the exclamation mark are in STRIP-BEFORE. Russian has no French spacing, so nbsp puts nothing back."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "ru-spaces-brackets",
|
|
23
|
+
"rule": "spaces",
|
|
24
|
+
"mode": "text",
|
|
25
|
+
"in": "текст ( сноска ) далее",
|
|
26
|
+
"out": "текст (сноска) далее",
|
|
27
|
+
"note": "spaces.md §3.4: bracket-inner runs are deleted, the outer ones collapse to one."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "ru-spaces-leading-indentation",
|
|
31
|
+
"rule": "spaces",
|
|
32
|
+
"mode": "text",
|
|
33
|
+
"in": " отступ сохраняется",
|
|
34
|
+
"out": " отступ сохраняется",
|
|
35
|
+
"note": "spaces.md §4: a run at index 0 is skipped."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "ru-ellipsis-abbreviated-after-question",
|
|
39
|
+
"rule": "ellipsis",
|
|
40
|
+
"mode": "text",
|
|
41
|
+
"in": "Что?...",
|
|
42
|
+
"out": "Что?..",
|
|
43
|
+
"note": "ellipsis.md §6 case 9. Russian writes two dots after ? or !, because the terminal mark already occupies the first position of the three-dot group — Правила русской орфографии и пунктуации (1956), «Многоточие»: «Сколько жить ещё на свете?..»."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "ru-ellipsis-abbreviated-from-existing",
|
|
47
|
+
"rule": "ellipsis",
|
|
48
|
+
"mode": "text",
|
|
49
|
+
"in": "Что?…",
|
|
50
|
+
"out": "Что?..",
|
|
51
|
+
"note": "ellipsis.md §6 case 10: an existing U+2026 after a terminal mark is rewritten to the abbreviated form."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "ru-ellipsis-abbreviated-roundtrip",
|
|
55
|
+
"rule": "ellipsis",
|
|
56
|
+
"mode": "text",
|
|
57
|
+
"in": "Что?..",
|
|
58
|
+
"out": "Что?..",
|
|
59
|
+
"note": "ellipsis.md §6 case 11: the target form is inert, unconditionally — this is what stops the ?.. / ?… oscillation that the naive formulation produces."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "ru-ellipsis-abbreviated-after-exclamation",
|
|
63
|
+
"rule": "ellipsis",
|
|
64
|
+
"mode": "text",
|
|
65
|
+
"in": "Как играли!...",
|
|
66
|
+
"out": "Как играли!..",
|
|
67
|
+
"note": "The `!..` form, same rule: «А как вы вчера играли!..»."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "ru-ellipsis-after-interrobang",
|
|
71
|
+
"rule": "ellipsis",
|
|
72
|
+
"mode": "text",
|
|
73
|
+
"in": "Что?!...",
|
|
74
|
+
"out": "Что?!..",
|
|
75
|
+
"note": "ellipsis.md §3.4 and §6 case 12: the single code point left of the run is U+0021, so the abbreviated form fires — «Что?!..»."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "ru-ellipsis-ordinary",
|
|
79
|
+
"rule": "ellipsis",
|
|
80
|
+
"mode": "text",
|
|
81
|
+
"in": "Он ушёл...",
|
|
82
|
+
"out": "Он ушёл…",
|
|
83
|
+
"note": "ellipsis.md §6 case 13: the left neighbour is a letter, so this is an ordinary U+2026."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "ru-ellipsis-two-dot-run-inert",
|
|
87
|
+
"rule": "ellipsis",
|
|
88
|
+
"mode": "text",
|
|
89
|
+
"in": "Версия 1..5",
|
|
90
|
+
"out": "Версия 1..5",
|
|
91
|
+
"note": "ellipsis.md §6 case 14: in an abbreviating locale a two-dot run is inert unconditionally."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "ru-dashes-parenthetical-spaced",
|
|
95
|
+
"rule": "dashes",
|
|
96
|
+
"mode": "text",
|
|
97
|
+
"in": "Москва - столица",
|
|
98
|
+
"out": "Москва — столица",
|
|
99
|
+
"note": "dashes.md §6 case 14: dash.parenthetical = em-spaced. Russian sets an em dash (U+2014), spaced on both sides — Мильчин/Розенталь."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "ru-dashes-parenthetical-tight-input",
|
|
103
|
+
"rule": "dashes",
|
|
104
|
+
"mode": "text",
|
|
105
|
+
"in": "Москва--столица",
|
|
106
|
+
"out": "Москва — столица",
|
|
107
|
+
"note": "P3: a run of two qualifies regardless of spacing, and em-spaced inserts the spacing the input lacked."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "ru-dashes-authored-en-dash-declined",
|
|
111
|
+
"rule": "dashes",
|
|
112
|
+
"mode": "text",
|
|
113
|
+
"in": "Москва–столица",
|
|
114
|
+
"out": "Москва–столица",
|
|
115
|
+
"note": "dashes.md §3.4 P5 (spec 0.6.0): a pure single authored U+2013 is declined unconditionally, even though ru's `dash.parenthetical` is `em-spaced` and would otherwise both promote the glyph to U+2014 and add spacing (as `ru-dashes-parenthetical-tight-input` does for a hyphen run). No existing ru fixture exercised authored-en-dash promotion before this change (checked against every `dashes`-rule fixture in this file)."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "ru-ranges-em-tight",
|
|
119
|
+
"rule": "ranges",
|
|
120
|
+
"mode": "text",
|
|
121
|
+
"in": "Годы 1941-1945 были тяжёлыми",
|
|
122
|
+
"out": "Годы 1941—1945 были тяжёлыми",
|
|
123
|
+
"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 17 and §3.3.1 (relocated from dashes.md §6 case 17 and §3.3.1 through spec 0.4.1). Two things at once. (1) The glyph: Russian sets an EM dash without spacing in an interval («1941—1945 гг.»), which is what forced dash.range to carry em-* — a port that hard-codes U+2013 for ranges fails here. (2) The binding: a hyphen-typed range that converts is wrapped in U+2060 word joiners, because UAX #14 gives U+2013/U+2014 the class BA and a renderer would otherwise be free to break after the dash. U+2060 is zero-width, so this case is unreadable on screen — check it in spec/fixtures/.escaped/.",
|
|
124
|
+
"rules": {
|
|
125
|
+
"ranges": true
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "ru-dashes-range-roundtrip",
|
|
130
|
+
"rule": "dashes",
|
|
131
|
+
"mode": "text",
|
|
132
|
+
"in": "в XV—XVII веках",
|
|
133
|
+
"out": "в XV—XVII веках",
|
|
134
|
+
"note": "dashes.md §6 case 17a. Guard P4, the Roman-numeral veto: a TIGHT dash whose neighbours are maximal runs of the seven uppercase Roman-numeral letters, each bounded on its outer side by a non-LETTER, is declined outright. Without P4 the range branch cannot see this token (§3.3 admits a range candidate only when a DIGIT stands on each side), it falls through to the parenthetical branch, and em-spaced DAMAGES input that was already correct: `в XV — XVII веках`. A port whose output spaces this dash out is missing P4. The U+00A0 after `в` is nbsp N3 and is unrelated."
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "ru-dashes-nobreak-space-preserved",
|
|
138
|
+
"rule": "dashes",
|
|
139
|
+
"mode": "text",
|
|
140
|
+
"in": "слово — столица",
|
|
141
|
+
"out": "слово — столица",
|
|
142
|
+
"note": "dashes.md §3.6 and §6 case 34: a U+00A0 on the left of the token counts as spacing and is preserved exactly, never downgraded to U+0020."
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "ru-dashes-cluster-inert",
|
|
146
|
+
"rule": "dashes",
|
|
147
|
+
"mode": "text",
|
|
148
|
+
"in": "a—0–0",
|
|
149
|
+
"out": "a—0–0",
|
|
150
|
+
"note": "dashes.md §6 case 18, shipped idempotency defect (a): one cluster with two dash runs is inert in its entirety."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "ru-ranges-cluster-inert",
|
|
154
|
+
"rule": "ranges",
|
|
155
|
+
"mode": "text",
|
|
156
|
+
"in": "a—0–0",
|
|
157
|
+
"out": "a—0–0",
|
|
158
|
+
"rules": {
|
|
159
|
+
"ranges": true
|
|
160
|
+
},
|
|
161
|
+
"note": "spec 0.5.0 ownership correction: companion to `ru-dashes-cluster-inert`. Explicitly opted in (`rules: {ranges: true}`): `0–0` is a genuine range candidate; the whole `—0–0` span is one cluster with two dash runs, so `ranges`' own cluster guard declines it too (ranges.md §3.1)."
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"id": "ru-dashes-spacing-transition-guard",
|
|
165
|
+
"rule": "dashes",
|
|
166
|
+
"mode": "text",
|
|
167
|
+
"in": "a–1 - 1",
|
|
168
|
+
"out": "a–1 - 1",
|
|
169
|
+
"note": "dashes.md §6 case 25, shipped idempotency defect (c): guard T1 keeps the tight en dash from becoming spaced against a digit run that has another dash beyond it."
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "ru-ranges-spacing-transition-guard",
|
|
173
|
+
"rule": "ranges",
|
|
174
|
+
"mode": "text",
|
|
175
|
+
"in": "a–1 - 1",
|
|
176
|
+
"out": "a–1 - 1",
|
|
177
|
+
"rules": {
|
|
178
|
+
"ranges": true
|
|
179
|
+
},
|
|
180
|
+
"note": "spec 0.5.0 ownership correction: companion to `ru-dashes-spacing-transition-guard`. 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."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"id": "ru-dashes-composition-guard-t2",
|
|
184
|
+
"rule": "dashes",
|
|
185
|
+
"mode": "text",
|
|
186
|
+
"in": "a--.",
|
|
187
|
+
"out": "a--.",
|
|
188
|
+
"note": "dashes.md §6 case 29, composition defect family 1: guard T2 declines an em-spaced form whose right neighbour is a code point `spaces` strips a U+0020 before."
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"id": "ru-dashes-compound-not-touched",
|
|
192
|
+
"rule": "dashes",
|
|
193
|
+
"mode": "text",
|
|
194
|
+
"in": "научно-технический прогресс",
|
|
195
|
+
"out": "научно-технический прогресс",
|
|
196
|
+
"note": "Guard P1: a bare unspaced hyphen inside a word is not a dash — and this compound is in none of the hyphen lists either."
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"id": "ru-ranges-phone-number",
|
|
200
|
+
"rule": "ranges",
|
|
201
|
+
"mode": "text",
|
|
202
|
+
"in": "Телефон 212-555-1234",
|
|
203
|
+
"out": "Телефон 212-555-1234",
|
|
204
|
+
"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. Cluster guard and G2: a chain of dashes is inert. ranges.md §3.2's G2 (chain) is the current normative guard rejecting this.",
|
|
205
|
+
"rules": {
|
|
206
|
+
"ranges": true
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "ru-hyphen-compound-izpod",
|
|
211
|
+
"rule": "hyphen",
|
|
212
|
+
"mode": "text",
|
|
213
|
+
"in": "Достал из-под стола",
|
|
214
|
+
"out": "Достал из‑под стола",
|
|
215
|
+
"note": "hyphen.md §6 case 1: `из-под` is a listed compound, so its hyphen becomes U+2011 and the word can no longer break after `из-`."
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"id": "ru-hyphen-compound-capitalised",
|
|
219
|
+
"rule": "hyphen",
|
|
220
|
+
"mode": "text",
|
|
221
|
+
"in": "Из-под стола донёсся звук",
|
|
222
|
+
"out": "Из‑под стола донёсся звук",
|
|
223
|
+
"note": "hyphen.md §6 case 2: first-character leniency, via the Unicode simple uppercase mapping of `и` — not a host-locale toUpperCase()."
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"id": "ru-hyphen-compound-izza",
|
|
227
|
+
"rule": "hyphen",
|
|
228
|
+
"mode": "text",
|
|
229
|
+
"in": "Москва — из-за дождя",
|
|
230
|
+
"out": "Москва — из‑за дождя",
|
|
231
|
+
"note": "hyphen.md §6 case 11: the em dash belongs to `dashes` and is untouched here; the compound binds. The space after `за` stays an ordinary U+0020, and that is nbsp N3 step 2: the left boundary of a short word is NONE, SPACELIKE, OPENISH or SENTENCE-DASH = {U+2013, U+2014} — a hyphen, U+002D or U+2011, deliberately fails it, because a hyphen marks an intra-word position and `за` here is the tail of the compound preposition `из-за`, not a preposition of its own. A port whose short-word boundary accepts any dash binds this and is wrong; see the next case for what the narrowing had to preserve."
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"id": "ru-hyphen-prefix-koe",
|
|
235
|
+
"rule": "hyphen",
|
|
236
|
+
"mode": "text",
|
|
237
|
+
"in": "Пришёл кое-кто важный",
|
|
238
|
+
"out": "Пришёл кое‑кто важный",
|
|
239
|
+
"note": "hyphen.md §6 case 4: the prefix `кое-` binds because a letter follows — Лопатин §135."
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"id": "ru-hyphen-suffix-to",
|
|
243
|
+
"rule": "hyphen",
|
|
244
|
+
"mode": "text",
|
|
245
|
+
"in": "Он сделал что-то важное",
|
|
246
|
+
"out": "Он сделал что‑то важное",
|
|
247
|
+
"note": "The suffix `-то`, matched at its own hyphen with a letter to the left — Лопатин §135."
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"id": "ru-hyphen-suffix-taki",
|
|
251
|
+
"rule": "hyphen",
|
|
252
|
+
"mode": "text",
|
|
253
|
+
"in": "Он всё-таки пришёл",
|
|
254
|
+
"out": "Он всё‑таки пришёл",
|
|
255
|
+
"note": "hyphen.md §6 case 5: the particle `-таки` — Лопатин §143."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"id": "ru-hyphen-suffix-nibud",
|
|
259
|
+
"rule": "hyphen",
|
|
260
|
+
"mode": "text",
|
|
261
|
+
"in": "что-нибудь или что-либо",
|
|
262
|
+
"out": "что‑нибудь или что‑либо",
|
|
263
|
+
"note": "hyphen.md §6 case 6: two suffixes in one string."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"id": "ru-nbsp-shortword-after-em-dash",
|
|
267
|
+
"rule": "nbsp",
|
|
268
|
+
"mode": "text",
|
|
269
|
+
"in": "— в Москве",
|
|
270
|
+
"out": "— в Москве",
|
|
271
|
+
"note": "nbsp.md §6 case 13h, the counterpart of the previous case: SENTENCE-DASH is exactly {U+2013, U+2014}, so an em dash still opens a phrase and a genuine one-letter preposition after it binds normally. The narrowing of N3's left boundary had to keep this working; a port that drops dashes from the boundary altogether fails here."
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"id": "ru-dashes-roman-range-hyphen-not-converted",
|
|
275
|
+
"rule": "dashes",
|
|
276
|
+
"mode": "text",
|
|
277
|
+
"in": "в XV-XVII веках",
|
|
278
|
+
"out": "в XV-XVII веках",
|
|
279
|
+
"note": "dashes.md §6 case 17b and §7.10: a Roman-numeral range typed with a hyphen-minus is NOT converted to an em dash. P4 tests the token's spacing and its neighbours, not which dash was typed, so it vetoes this token too. That is deliberate — P4 is a veto, never a range-enabler, because admitting ROMAN runs as range candidates would also fire on all-caps words built from the same letters (MIX, CIVIL, VIVID), and converting is the direction that can damage. A recorded miss, not a defect: do not silently \"fix\" it."
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"id": "ru-hyphen-roundtrip",
|
|
283
|
+
"rule": "hyphen",
|
|
284
|
+
"mode": "text",
|
|
285
|
+
"in": "Достал из‑под стола",
|
|
286
|
+
"out": "Достал из‑под стола",
|
|
287
|
+
"note": "hyphen.md §6 case 7, the round-trip case: matching is hyphen-lenient, so the rule recognises its own output and emits nothing."
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"id": "ru-hyphen-allcaps-not-matched",
|
|
291
|
+
"rule": "hyphen",
|
|
292
|
+
"mode": "text",
|
|
293
|
+
"in": "ИЗ-ПОД СТОЛА",
|
|
294
|
+
"out": "ИЗ-ПОД СТОЛА",
|
|
295
|
+
"note": "hyphen.md §6 case 3 and §7.1: only the first character is case-lenient, so an all-capitals heading is deliberately not matched."
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"id": "ru-hyphen-not-listed",
|
|
299
|
+
"rule": "hyphen",
|
|
300
|
+
"mode": "text",
|
|
301
|
+
"in": "научно-технический прогресс",
|
|
302
|
+
"out": "научно-технический прогресс",
|
|
303
|
+
"note": "hyphen.md §6 case 8: the rule has a list, not a morphology."
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"id": "ru-hyphen-suffix-at-line-start",
|
|
307
|
+
"rule": "hyphen",
|
|
308
|
+
"mode": "text",
|
|
309
|
+
"in": "-таки в начале строки",
|
|
310
|
+
"out": "-таки в начале строки",
|
|
311
|
+
"note": "hyphen.md §6 case 12: suffix guard S1 needs a letter to the left, and there is none, so the hyphen stays U+002D. The U+00A0 after `в` is nbsp N3."
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"id": "ru-hyphen-inside-longer-word",
|
|
315
|
+
"rule": "hyphen",
|
|
316
|
+
"mode": "text",
|
|
317
|
+
"in": "квазииз-подный",
|
|
318
|
+
"out": "квазииз-подный",
|
|
319
|
+
"note": "hyphen.md §6 case 10: compound guard C1, the code point to the left is in WORDISH."
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"id": "ru-quotes-nested",
|
|
323
|
+
"rule": "quotes",
|
|
324
|
+
"mode": "text",
|
|
325
|
+
"in": "Он сказал: \"это 'моё' дело\".",
|
|
326
|
+
"out": "Он сказал: «это 'моё' дело».",
|
|
327
|
+
"note": "quotes.md §6 case 14: depth 1 takes the ёлочки « », depth 2 the лапки „ “ — Лопатин 2006. Note the secondary pair's closing glyph is U+201C, which is in OPENISH; the class says nothing about the role. spec 0.5.0: the inner 'моё' (3 Cyrillic letters, space-flanked) is preserved by the general ambiguous-medial-span veto, which reads Unicode LETTER generally, not only ASCII — ru has no elisionIdioms entry for this shape."
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"id": "ru-quotes-roundtrip",
|
|
331
|
+
"rule": "quotes",
|
|
332
|
+
"mode": "text",
|
|
333
|
+
"in": "Он сказал: «это „моё“ дело».",
|
|
334
|
+
"out": "Он сказал: «это „моё“ дело».",
|
|
335
|
+
"note": "Existing curly glyphs are never re-paired (quotes.md §3.8)."
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"id": "ru-quotes-no-inner-space",
|
|
339
|
+
"rule": "quotes",
|
|
340
|
+
"mode": "text",
|
|
341
|
+
"in": "Роман «Война и мир»",
|
|
342
|
+
"out": "Роман «Война и мир»",
|
|
343
|
+
"note": "Russian guillemets take no inner space (quotes.innerSpace = none), unlike French — N8 does nothing. The U+00A0 is nbsp N3 binding the conjunction `и`."
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"id": "ru-quotes-unbalanced",
|
|
347
|
+
"rule": "quotes",
|
|
348
|
+
"mode": "text",
|
|
349
|
+
"in": "Он сказал: \"это моё",
|
|
350
|
+
"out": "Он сказал: \"это моё",
|
|
351
|
+
"note": "quotes.md §3.6: an unmatched straight mark is left alone rather than guessed at."
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"id": "ru-apostrophe-medial",
|
|
355
|
+
"rule": "apostrophe",
|
|
356
|
+
"mode": "text",
|
|
357
|
+
"in": "Джон О'Брайен",
|
|
358
|
+
"out": "Джон О’Брайен",
|
|
359
|
+
"note": "apostrophe.md §6 case 13: letters on both sides."
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"id": "ru-apostrophe-prime-guard",
|
|
363
|
+
"rule": "apostrophe",
|
|
364
|
+
"mode": "text",
|
|
365
|
+
"in": "Рост 6' 2\" дюймов",
|
|
366
|
+
"out": "Рост 6' 2\" дюймов",
|
|
367
|
+
"note": "apostrophe.md §6 case 6: the prime guard leaves a foot mark alone, and `quotes` could not pair the lone U+0022 either."
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"id": "ru-symbols-copyright",
|
|
371
|
+
"rule": "symbols",
|
|
372
|
+
"mode": "text",
|
|
373
|
+
"in": "Все права защищены (c) 2026",
|
|
374
|
+
"out": "Все права защищены © 2026",
|
|
375
|
+
"note": "symbols.md §6 case 1."
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"id": "ru-symbols-multiplication-with-unit",
|
|
379
|
+
"rule": "symbols",
|
|
380
|
+
"mode": "text",
|
|
381
|
+
"in": "Размер 10 x 20 см",
|
|
382
|
+
"out": "Размер 10 × 20 см",
|
|
383
|
+
"note": "symbols.md §3.3: the symmetric spaced form keeps its space code points; nbsp then binds the unit `см` to the number."
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"id": "ru-symbols-hex-literal",
|
|
387
|
+
"rule": "symbols",
|
|
388
|
+
"mode": "text",
|
|
389
|
+
"in": "Цвет 0x1F",
|
|
390
|
+
"out": "Цвет 0x1F",
|
|
391
|
+
"note": "Guard M3: a letter follows the right digit run."
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"id": "ru-symbols-cyrillic-x-converted",
|
|
395
|
+
"rule": "symbols",
|
|
396
|
+
"mode": "text",
|
|
397
|
+
"in": "Размер 5х4",
|
|
398
|
+
"out": "Размер 5×4",
|
|
399
|
+
"note": "symbols.md §3.1 and §6 case 5a (verbatim; the chain ends the text unit, so `after` is NONE — a valid path through guard M3 and worth pinning on its own): the Cyrillic х (U+0445) is a MUL-LETTER alongside the Latin x, universally rather than as locale data — the risk is carried entirely by the numeric context, and U+0445 between two ASCII digits has no second reading in any language. This is the one conversion in the package that the M4 hand-review structurally cannot catch: `5х4` and `5x4` are identical in every font, so a proof-reader never finds the wrong letter and the machine finds it every time."
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
"id": "ru-symbols-cyrillic-x-capital",
|
|
403
|
+
"rule": "symbols",
|
|
404
|
+
"mode": "text",
|
|
405
|
+
"in": "Размер 5Х4",
|
|
406
|
+
"out": "Размер 5×4",
|
|
407
|
+
"note": "symbols.md §6 case 5b (verbatim): the Cyrillic capital Х (U+0425) is a MUL-LETTER too, and `after` is NONE."
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"id": "ru-symbols-chain-followed-by-unit",
|
|
411
|
+
"rule": "symbols",
|
|
412
|
+
"mode": "text",
|
|
413
|
+
"in": "Размер 5Х4 см",
|
|
414
|
+
"out": "Размер 5×4 см",
|
|
415
|
+
"note": "The cross-rule case, kept deliberately separate from the §6 row above: the U+00D7 is `symbols`, but the U+00A0 comes from `nbsp` N5, because `см` is in the ru locale file's beforeUnits and the code point two positions left of it is a digit. `symbols` never emits a no-break space. A port that binds nothing here has an incomplete beforeUnits list; a port that attributes the U+00A0 to the multiplication branch has mislayered the pipeline."
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"id": "ru-symbols-cyrillic-x-letter-context",
|
|
419
|
+
"rule": "symbols",
|
|
420
|
+
"mode": "text",
|
|
421
|
+
"in": "хорошо и их",
|
|
422
|
+
"out": "хорошо и их",
|
|
423
|
+
"note": "symbols.md §6 case 5c: adding the Cyrillic letters costs nothing in ordinary Russian prose, because the branch requires an ASCII digit on each side of the chain. Both `х` here have letter neighbours. The U+00A0 is nbsp N3 on the conjunction `и`."
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
"id": "ru-symbols-chain-cyrillic",
|
|
427
|
+
"rule": "symbols",
|
|
428
|
+
"mode": "text",
|
|
429
|
+
"in": "5х4х3",
|
|
430
|
+
"out": "5×4×3",
|
|
431
|
+
"note": "symbols.md §6 case 5d: an all-Cyrillic chain, both links converted in ONE pass, because §3.3 specifies the branch as a scan of a whole chain `DIGIT+ (X DIGIT+)+` rather than a pairwise test. §7.10 records the contrast: Lebedev's Typograf converts one operator per pass on this exact input (`5х4х3` → `5×4х3` → `5×4×3`), so its output never converges under re-processing."
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"id": "ru-symbols-chain-mixed-alphabet",
|
|
435
|
+
"rule": "symbols",
|
|
436
|
+
"mode": "text",
|
|
437
|
+
"in": "5x4х3",
|
|
438
|
+
"out": "5×4×3",
|
|
439
|
+
"note": "symbols.md §6 case 5f: one Latin x, one Cyrillic х, one chain, both links convert. Declining on mixture would make the output depend on which keyboard layout the author's finger slipped to — the least discoverable criterion available, since the two letters are visually identical."
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"id": "ru-symbols-chain-mixed-spacing-declined",
|
|
443
|
+
"rule": "symbols",
|
|
444
|
+
"mode": "text",
|
|
445
|
+
"in": "5x4 x 3",
|
|
446
|
+
"out": "5x4 x 3",
|
|
447
|
+
"note": "symbols.md §6 case 5i: guard M1 requires the spacing to be symmetric AND uniform across every link. Link 1 is tight and link 2 is spaced, so the chain is declined WHOLE rather than half-converted — an implementation that converts the first link here is producing output no author asked for."
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
"id": "ru-symbols-chain-resumes-after-sign",
|
|
451
|
+
"rule": "symbols",
|
|
452
|
+
"mode": "text",
|
|
453
|
+
"in": "5×4x3",
|
|
454
|
+
"out": "5×4×3",
|
|
455
|
+
"note": "symbols.md §6 case 5j: U+00D7 is not in MUL-LETTER, so it ends a chain; the remaining sub-chain `4x3` converts on its own and sub-chains never need joining. This is also the idempotency case for partially-converted input."
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"id": "ru-symbols-chain-three-links",
|
|
459
|
+
"rule": "symbols",
|
|
460
|
+
"mode": "text",
|
|
461
|
+
"in": "Стол 120х80х75",
|
|
462
|
+
"out": "Стол 120×80×75",
|
|
463
|
+
"note": "symbols.md §6 case 5g (verbatim): a three-run chain with multi-digit runs, ending the text unit."
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"id": "ru-symbols-chain-three-links-followed-by-unit",
|
|
467
|
+
"rule": "symbols",
|
|
468
|
+
"mode": "text",
|
|
469
|
+
"in": "Стол 120х80х75 см",
|
|
470
|
+
"out": "Стол 120×80×75 см",
|
|
471
|
+
"note": "The same chain with a unit after it. Both edits are real and they belong to different rules: three U+00D7 from `symbols`, one U+00A0 from `nbsp` N5 (beforeUnits `см`)."
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
"id": "ru-symbols-plusminus-after-digit",
|
|
475
|
+
"rule": "symbols",
|
|
476
|
+
"mode": "text",
|
|
477
|
+
"in": "Погрешность 5+/-3",
|
|
478
|
+
"out": "Погрешность 5±3",
|
|
479
|
+
"note": "symbols.md §6 case 5m: a digit on the left is no obstacle — guard F1 rejects only a preceding U+005B."
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
"id": "ru-nbsp-short-word",
|
|
483
|
+
"rule": "nbsp",
|
|
484
|
+
"mode": "text",
|
|
485
|
+
"in": "Он живёт в Москве",
|
|
486
|
+
"out": "Он живёт в Москве",
|
|
487
|
+
"note": "nbsp.md §6 case 7: N3, one-letter preposition — Мильчин/Чельцова."
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"id": "ru-nbsp-short-word-roundtrip",
|
|
491
|
+
"rule": "nbsp",
|
|
492
|
+
"mode": "text",
|
|
493
|
+
"in": "Он живёт в Москве",
|
|
494
|
+
"out": "Он живёт в Москве",
|
|
495
|
+
"note": "nbsp.md §6 case 8: already correct."
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
"id": "ru-nbsp-short-word-capitalised",
|
|
499
|
+
"rule": "nbsp",
|
|
500
|
+
"mode": "text",
|
|
501
|
+
"in": "В Москве идёт дождь",
|
|
502
|
+
"out": "В Москве идёт дождь",
|
|
503
|
+
"note": "N3 step 1: first-character leniency via the simple uppercase mapping."
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"id": "ru-nbsp-short-word-two-letter",
|
|
507
|
+
"rule": "nbsp",
|
|
508
|
+
"mode": "text",
|
|
509
|
+
"in": "Он вышел из дома на улицу",
|
|
510
|
+
"out": "Он вышел из дома на улицу",
|
|
511
|
+
"note": "The two-letter prepositions listed in the ru locale file bind the same way."
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
"id": "ru-nbsp-abbreviation-itd",
|
|
515
|
+
"rule": "nbsp",
|
|
516
|
+
"mode": "text",
|
|
517
|
+
"in": "фрукты, овощи и т. д.",
|
|
518
|
+
"out": "фрукты, овощи и т. д.",
|
|
519
|
+
"note": "nbsp.md §6 case 9: N4 owns every internal space of the listed abbreviation `и т. д.`."
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"id": "ru-nbsp-abbreviation-te",
|
|
523
|
+
"rule": "nbsp",
|
|
524
|
+
"mode": "text",
|
|
525
|
+
"in": "т. е. вот так",
|
|
526
|
+
"out": "т. е. вот так",
|
|
527
|
+
"note": "N4 on `т. е.`; the space after the abbreviation is not bound."
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"id": "ru-nbsp-initials-before-surname",
|
|
531
|
+
"rule": "nbsp",
|
|
532
|
+
"mode": "text",
|
|
533
|
+
"in": "А. С. Пушкин",
|
|
534
|
+
"out": "А. С. Пушкин",
|
|
535
|
+
"note": "nbsp.md §6 case 10: N7 clause C1 twice — Мильчин/Чельцова, «А. С. Пушкин»."
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"id": "ru-nbsp-initials-after-surname",
|
|
539
|
+
"rule": "nbsp",
|
|
540
|
+
"mode": "text",
|
|
541
|
+
"in": "Пушкин А. С.",
|
|
542
|
+
"out": "Пушкин А. С.",
|
|
543
|
+
"note": "nbsp.md §6 case 11: the first space by clause C2 (two initials follow), the second by C1."
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"id": "ru-nbsp-symbol-number",
|
|
547
|
+
"rule": "nbsp",
|
|
548
|
+
"mode": "text",
|
|
549
|
+
"in": "см. № 5 и § 12",
|
|
550
|
+
"out": "см. № 5 и § 12",
|
|
551
|
+
"note": "nbsp.md §6 case 12: N6, «Технические правила набора» п. 4д — знаки № и § не отделяются от следующих цифр. Note `и` is not bound here: N3 requires the following token to be ALNUM or OPENISH and it is `§`."
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"id": "ru-nbsp-before-word-street",
|
|
555
|
+
"rule": "nbsp",
|
|
556
|
+
"mode": "text",
|
|
557
|
+
"in": "г. Москва, ул. Ленина",
|
|
558
|
+
"out": "г. Москва, ул. Ленина",
|
|
559
|
+
"note": "N10 binds `ул.`, which the ru locale file lists as an unambiguous toponymic abbreviation (п. 4б). `г.` is deliberately NOT in beforeWord — see the next two cases."
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"id": "ru-nbsp-year-binds-leftwards",
|
|
563
|
+
"rule": "nbsp",
|
|
564
|
+
"mode": "text",
|
|
565
|
+
"in": "Это было в 2020 г.",
|
|
566
|
+
"out": "Это было в 2020 г.",
|
|
567
|
+
"note": "`г.` is listed in beforeUnits, so it binds to the NUMBER on its left (N5) — «Технические правила набора» п. 4г, «2010 г.»."
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
"id": "ru-nbsp-year-does-not-bind-rightwards",
|
|
571
|
+
"rule": "nbsp",
|
|
572
|
+
"mode": "text",
|
|
573
|
+
"in": "В 1147 г. Москва была основана.",
|
|
574
|
+
"out": "В 1147 г. Москва была основана.",
|
|
575
|
+
"note": "And the point of leaving `г.` out of beforeWord: here it means «год» and ends the clause, so the space before `Москва` must stay breakable. A literal list cannot tell «1147 г.» from «г. Москва», and the ru locale file resolves that by binding only leftwards."
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
"id": "ru-nbsp-units",
|
|
579
|
+
"rule": "nbsp",
|
|
580
|
+
"mode": "text",
|
|
581
|
+
"in": "5 млн человек и 20 км пути",
|
|
582
|
+
"out": "5 млн человек и 20 км пути",
|
|
583
|
+
"note": "N5 twice, plus N3 on the conjunction `и`."
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
"id": "ru-nbsp-percent",
|
|
587
|
+
"rule": "nbsp",
|
|
588
|
+
"mode": "text",
|
|
589
|
+
"in": "Рост 50 % за год",
|
|
590
|
+
"out": "Рост 50 % за год",
|
|
591
|
+
"note": "N5 on the percent sign, N3 on the two-letter preposition `за`."
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
"id": "ru-nbsp-unit-never-inserts",
|
|
595
|
+
"rule": "nbsp",
|
|
596
|
+
"mode": "text",
|
|
597
|
+
"in": "Рост 50% за год",
|
|
598
|
+
"out": "Рост 50% за год",
|
|
599
|
+
"note": "nbsp.md §7.2: N5 converts, never inserts — `50%` keeps its typed form. N3 still binds the preposition `за`."
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"id": "ru-nbsp-no-french-spacing",
|
|
603
|
+
"rule": "nbsp",
|
|
604
|
+
"mode": "text",
|
|
605
|
+
"in": "Что это ?",
|
|
606
|
+
"out": "Что это?",
|
|
607
|
+
"note": "Russian declares both punctuation lists empty, so `spaces` deletes the space before the question mark and nothing puts one back. The same input in fr gains U+202F — that contrast is the whole point of per-locale data."
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
"id": "ru-nbsp-plain-sentence",
|
|
611
|
+
"rule": "nbsp",
|
|
612
|
+
"mode": "text",
|
|
613
|
+
"in": "Иван пошёл домой",
|
|
614
|
+
"out": "Иван пошёл домой",
|
|
615
|
+
"note": "nbsp.md §6 case 13: no short word, no abbreviation, no unit, no initial."
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
"id": "ru-roundtrip-typeset-sentence",
|
|
619
|
+
"rule": "nbsp",
|
|
620
|
+
"mode": "text",
|
|
621
|
+
"in": "А. С. Пушкин родился в 1799 г.",
|
|
622
|
+
"out": "А. С. Пушкин родился в 1799 г.",
|
|
623
|
+
"note": "Byte-identical round trip: bound initials, a bound preposition and a bound year abbreviation."
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
"id": "ru-roundtrip-plain-prose",
|
|
627
|
+
"rule": "spaces",
|
|
628
|
+
"mode": "text",
|
|
629
|
+
"in": "Обычный текст не меняется.",
|
|
630
|
+
"out": "Обычный текст не меняется.",
|
|
631
|
+
"note": "Nothing here is a candidate for any rule."
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
"id": "ru-roundtrip-hyphen-and-dash",
|
|
635
|
+
"rule": "dashes",
|
|
636
|
+
"mode": "text",
|
|
637
|
+
"in": "Из‑под стола — 1941—1945 гг.",
|
|
638
|
+
"out": "Из‑под стола — 1941—1945 гг.",
|
|
639
|
+
"note": "Correct output of four rules at once: a bound morphological hyphen, a spaced parenthetical em dash, a tight range em dash and `гг.` bound to the interval it follows (beforeUnits). Both dashes here were typed by the author, so `dashes` leaves them exactly as written (§3.2 step 2a) — and note what that means for the range: it gets NO U+2060 joiners, unlike the hyphen-typed `1941-1945` two cases above. The rule never makes an edit whose entire content is invisible (§3.3.1, §7.14): a joiner may ride along with a conversion the author asked for, but it is never the only thing an edit does, because an invisible-only change to already-correct text cannot be reviewed by hand — and hand review is exactly the M4 criterion. spec 0.5.0 ownership correction: `1941—1945` is a genuine range candidate already in its exact target form (em-tight) — untouched here for the unrelated reason that `dashes` never reaches a digit-flanked token at all (dashes.md §1), not because any range guard evaluated and passed it. See the companion `ru-ranges-roundtrip-already-correct`, which explicitly enables `ranges` on the same sentence and proves the range stays a genuine fixed point (ranges.md §3.3.1's invisible-edit test), not merely unreached."
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
"id": "ru-ranges-roundtrip-already-correct",
|
|
643
|
+
"rule": "ranges",
|
|
644
|
+
"mode": "text",
|
|
645
|
+
"in": "Из‑под стола — 1941—1945 гг.",
|
|
646
|
+
"out": "Из‑под стола — 1941—1945 гг.",
|
|
647
|
+
"rules": {
|
|
648
|
+
"ranges": true
|
|
649
|
+
},
|
|
650
|
+
"note": "spec 0.5.0 ownership correction: companion to `ru-roundtrip-hyphen-and-dash`, explicitly opted in (`rules: {ranges: true}`). `1941—1945` is already exactly `em-tight` — glyph, length and spacing all correct — so ranges.md §3.3.1's invisible-edit test leaves it unbound and unchanged: a genuine fixed point, not an absence of evaluation."
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
"id": "ru-combined-sentence",
|
|
654
|
+
"rule": "nbsp",
|
|
655
|
+
"mode": "text",
|
|
656
|
+
"in": "В книге сказано: \"Дорога длиной 20 км -- это не предел...\" (годы 1941-1945).",
|
|
657
|
+
"out": "В книге сказано: «Дорога длиной 20 км — это не предел…» (годы 1941—1945).",
|
|
658
|
+
"note": "A quotation, a parenthetical dash, an ellipsis, a number with a unit and a range in one sentence: ёлочки without inner space, a spaced em dash, a tight em dash for the interval, U+00A0 after the preposition and before the unit. 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.",
|
|
659
|
+
"rules": {
|
|
660
|
+
"ranges": true
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
"id": "ru-quotes-030-bug1-witness",
|
|
665
|
+
"rule": "quotes",
|
|
666
|
+
"mode": "text",
|
|
667
|
+
"in": "'\"‘",
|
|
668
|
+
"out": "'\"‘",
|
|
669
|
+
"note": "quotes.md §6 row B1, and the pinned pipeline-idempotency.md §6 witness '\"‘: pass 2 forms (\", ‘) as a pair, but rendering it to «» puts all three marks on the same WIDE stack and the re-derivation finds no candidate set at all — the gate declines to ∅ in one round rather than accept the unstable pairing."
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
"id": "ru-quotes-030-adversarial-interleaved",
|
|
673
|
+
"rule": "quotes",
|
|
674
|
+
"mode": "text",
|
|
675
|
+
"in": "«a„b«c»d“e»",
|
|
676
|
+
"out": "«a„b«c»d“e»",
|
|
677
|
+
"note": "quotes.md §6, ru/el/fr/fr-CA adversarial sweep: already-curly interleaved WIDE-only text (ru's primary « » and secondary „ “ are both WIDE). Already correct nesting; no edits from `quotes`, verified idempotent."
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
"id": "ru-quotes-rock-n-roll",
|
|
681
|
+
"rule": "quotes",
|
|
682
|
+
"mode": "text",
|
|
683
|
+
"in": "rock 'n' roll",
|
|
684
|
+
"out": "rock 'n' roll",
|
|
685
|
+
"note": "spec 0.5.0: ru 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.\""
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
}
|