polytypo 1.17.5 → 1.18.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 +4 -4
- data/lib/polytypo/data/VERSION +1 -1
- data/lib/polytypo/data/fixtures/bg.json +1 -1
- data/lib/polytypo/data/fixtures/cs.json +1 -1
- data/lib/polytypo/data/fixtures/de-CH.json +1 -1
- data/lib/polytypo/data/fixtures/de-DE.json +1 -1
- data/lib/polytypo/data/fixtures/el.json +1 -1
- data/lib/polytypo/data/fixtures/en-GB.json +1 -1
- data/lib/polytypo/data/fixtures/en-US.json +1 -1
- data/lib/polytypo/data/fixtures/es.json +272 -7
- data/lib/polytypo/data/fixtures/fi.json +1 -1
- data/lib/polytypo/data/fixtures/fr-CA.json +1 -1
- data/lib/polytypo/data/fixtures/fr.json +1 -1
- data/lib/polytypo/data/fixtures/hr.json +1 -1
- data/lib/polytypo/data/fixtures/hu.json +1 -1
- data/lib/polytypo/data/fixtures/it.json +1 -1
- data/lib/polytypo/data/fixtures/locale-resolution.json +1 -1
- data/lib/polytypo/data/fixtures/lt.json +1 -1
- data/lib/polytypo/data/fixtures/nl.json +1 -1
- data/lib/polytypo/data/fixtures/pl.json +1 -1
- data/lib/polytypo/data/fixtures/pt-BR.json +1 -1
- data/lib/polytypo/data/fixtures/pt-PT.json +1 -1
- data/lib/polytypo/data/fixtures/ro.json +1 -1
- data/lib/polytypo/data/fixtures/ru.json +1 -1
- data/lib/polytypo/data/fixtures/sk.json +1 -1
- data/lib/polytypo/data/fixtures/sl.json +1 -1
- data/lib/polytypo/data/fixtures/sr.json +1 -1
- data/lib/polytypo/data/fixtures/sv.json +1 -1
- data/lib/polytypo/data/fixtures/tr.json +1 -1
- data/lib/polytypo/data/fixtures/uk.json +1 -1
- data/lib/polytypo/data/locales/el.json +1 -1
- data/lib/polytypo/data/locales/es.json +2 -2
- data/lib/polytypo/data/locales/registry.json +1 -1
- data/lib/polytypo/data/rules/dashes.md +108 -16
- data/lib/polytypo/data/rules/order.json +2 -2
- data/lib/polytypo/data/rules/pipeline-idempotency.md +7 -0
- data/lib/polytypo/data/schema/locale.schema.json +2 -2
- data/lib/polytypo/engine/rules/dash_shared.rb +32 -21
- data/lib/polytypo/engine/rules/dashes.rb +4 -1
- data/lib/polytypo/engine/rules/dashes_paired.rb +160 -0
- data/lib/polytypo/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b78196aba8f371ed14077dbbb0a1642276cf567628d2fc4dfc9c467e2f543f02
|
|
4
|
+
data.tar.gz: e9a2b2df572deb0eaf529813d63dc8d72f378e06c541f8567f81b1451219e557
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92a5ca301117e7baa8647b1b0148d1e2980c506a3efda57fbd36b973fdcb4dae31c7e011e53973dd39954539e12090651860bff1de4b11223bc11bbc401c873f
|
|
7
|
+
data.tar.gz: 534cc7cedb87c00a83cee59e2cba5a6fe396b8c5b8bc512ccc5576ac5c271286cad15af3e10c53e40d9c452a594236202c2894f2cfde8edd9efe6ec7bf1f91c0
|
data/lib/polytypo/data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.18.0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.18.0",
|
|
3
3
|
"locale": "es",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -43,20 +43,285 @@
|
|
|
43
43
|
"note": "abbreviatedAfterTerminal = false: la forma de dos puntos tras «?» que usa el ruso no existe en español, así que los dos puntos se completan a U+2026 en vez de conservarse."
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
|
-
"id": "es-dashes-
|
|
46
|
+
"id": "es-dashes-paired-spaced-hyphens",
|
|
47
47
|
"rule": "dashes",
|
|
48
48
|
"mode": "text",
|
|
49
49
|
"in": "El plan - si existe - fracasa.",
|
|
50
|
-
"out": "El plan —
|
|
51
|
-
"note": "
|
|
50
|
+
"out": "El plan —si existe— fracasa.",
|
|
51
|
+
"note": "Guiones espaciados a ambos lados: el par se escribe «—si existe—», con espacio fuera del par y ninguno dentro (DPD «raya», 2)."
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
|
-
"id": "es-dashes-double-hyphen
|
|
54
|
+
"id": "es-dashes-paired-double-hyphen",
|
|
55
55
|
"rule": "dashes",
|
|
56
56
|
"mode": "text",
|
|
57
57
|
"in": "Caminamos -- casi 3 km -- y estaba cerrada.",
|
|
58
|
-
"out": "Caminamos —
|
|
59
|
-
"note": "
|
|
58
|
+
"out": "Caminamos —casi 3 km— y estaba cerrada.",
|
|
59
|
+
"note": "La forma «--» que se teclea a menudo converge a la misma forma del DPD."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "es-dashes-paired-tight-em",
|
|
63
|
+
"rule": "dashes",
|
|
64
|
+
"mode": "text",
|
|
65
|
+
"in": "El plan—si existe—fracasa.",
|
|
66
|
+
"out": "El plan —si existe— fracasa.",
|
|
67
|
+
"note": "Rayas pegadas por ambos lados: se añade el espacio exterior y no se toca el interior."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "es-dashes-paired-already-correct",
|
|
71
|
+
"rule": "dashes",
|
|
72
|
+
"mode": "text",
|
|
73
|
+
"in": "Llegó —por fin— a casa.",
|
|
74
|
+
"out": "Llegó —por fin— a casa.",
|
|
75
|
+
"note": "Forma del DPD ya correcta: punto fijo."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "es-dashes-paired-spaced-em",
|
|
79
|
+
"rule": "dashes",
|
|
80
|
+
"mode": "text",
|
|
81
|
+
"in": "Llegó — por fin — a casa.",
|
|
82
|
+
"out": "Llegó —por fin— a casa.",
|
|
83
|
+
"note": "Salida de las versiones 1.17.x (espacios interiores): se cierran los interiores."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "es-dashes-paired-half-correct",
|
|
87
|
+
"rule": "dashes",
|
|
88
|
+
"mode": "text",
|
|
89
|
+
"in": "Llegó —por fin — a casa.",
|
|
90
|
+
"out": "Llegó —por fin— a casa.",
|
|
91
|
+
"note": "Raya de cierre con espacio interior: se cierra."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "es-dashes-paired-before-full-stop",
|
|
95
|
+
"rule": "dashes",
|
|
96
|
+
"mode": "text",
|
|
97
|
+
"in": "Llegó a casa--por fin--.",
|
|
98
|
+
"out": "Llegó a casa —por fin—.",
|
|
99
|
+
"note": "El cierre queda pegado al punto: no se inserta espacio donde `spaces` lo borraría."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "es-dashes-paired-guillemets",
|
|
103
|
+
"rule": "dashes",
|
|
104
|
+
"mode": "text",
|
|
105
|
+
"in": "«Llegó - por fin - a casa»",
|
|
106
|
+
"out": "«Llegó —por fin— a casa»",
|
|
107
|
+
"note": "Las comillas angulares no cambian el reparto del par."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "es-dashes-paired-compound-inside",
|
|
111
|
+
"rule": "dashes",
|
|
112
|
+
"mode": "text",
|
|
113
|
+
"in": "Llegó - por fin - a la zona hispano-árabe.",
|
|
114
|
+
"out": "Llegó —por fin— a la zona hispano-árabe.",
|
|
115
|
+
"note": "Un guion de palabra compuesta dentro del par no cuenta como raya."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "es-dashes-paired-digit-inside",
|
|
119
|
+
"rule": "dashes",
|
|
120
|
+
"mode": "text",
|
|
121
|
+
"in": "Llegó - tras 3 - a casa.",
|
|
122
|
+
"out": "Llegó —tras 3— a casa.",
|
|
123
|
+
"note": "Un número dentro del par no impide la conversión."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "es-dashes-paired-range-inside",
|
|
127
|
+
"rule": "dashes",
|
|
128
|
+
"mode": "text",
|
|
129
|
+
"in": "Llegó - tras 3-5 - a casa.",
|
|
130
|
+
"out": "Llegó —tras 3-5— a casa.",
|
|
131
|
+
"note": "Un rango con guion dentro del par queda como está (lo gestiona `ranges`)."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "es-dashes-dialogue-untouched",
|
|
135
|
+
"rule": "dashes",
|
|
136
|
+
"mode": "text",
|
|
137
|
+
"in": "—Hola —dijo—. ¿Vienes? —preguntó María.",
|
|
138
|
+
"out": "—Hola —dijo—. ¿Vienes? —preguntó María.",
|
|
139
|
+
"note": "Las rayas de diálogo no se tocan: la primera abre la línea y el segmento tiene más de dos rayas."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "es-dashes-dialogue-open",
|
|
143
|
+
"rule": "dashes",
|
|
144
|
+
"mode": "text",
|
|
145
|
+
"in": "—Hola —dijo.",
|
|
146
|
+
"out": "—Hola —dijo.",
|
|
147
|
+
"note": "Diálogo con una sola raya de incisa: se deja como está."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "es-dashes-dialogue-hyphens",
|
|
151
|
+
"rule": "dashes",
|
|
152
|
+
"mode": "text",
|
|
153
|
+
"in": "—Hola - dijo - y se fue.",
|
|
154
|
+
"out": "—Hola - dijo - y se fue.",
|
|
155
|
+
"note": "Una raya de diálogo al inicio hace que el segmento tenga tres rayas: ninguna se convierte."
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": "es-dashes-single-hyphen-untouched",
|
|
159
|
+
"rule": "dashes",
|
|
160
|
+
"mode": "text",
|
|
161
|
+
"in": "Madrid - la capital de España.",
|
|
162
|
+
"out": "Madrid - la capital de España.",
|
|
163
|
+
"note": "Un solo guion espaciado no forma par: se deja tal como está (el DPD no describe este uso)."
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"id": "es-dashes-two-sentences-untouched",
|
|
167
|
+
"rule": "dashes",
|
|
168
|
+
"mode": "text",
|
|
169
|
+
"in": "Madrid - la capital. Sevilla - la bella.",
|
|
170
|
+
"out": "Madrid - la capital. Sevilla - la bella.",
|
|
171
|
+
"note": "Cada frase tiene una sola raya: no hay par y no se toca ninguna."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "es-dashes-three-untouched",
|
|
175
|
+
"rule": "dashes",
|
|
176
|
+
"mode": "text",
|
|
177
|
+
"in": "Juan - mi primo - llegó y - según dicen - durmió.",
|
|
178
|
+
"out": "Juan - mi primo - llegó y - según dicen - durmió.",
|
|
179
|
+
"note": "Cuatro rayas en una frase: no se empareja ninguna."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"id": "es-dashes-across-line-untouched",
|
|
183
|
+
"rule": "dashes",
|
|
184
|
+
"mode": "text",
|
|
185
|
+
"in": "Llegó - por\nfin - a casa.",
|
|
186
|
+
"out": "Llegó - por\nfin - a casa.",
|
|
187
|
+
"note": "El par no cruza un salto de línea."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "es-dashes-affix-hyphens-untouched",
|
|
191
|
+
"rule": "dashes",
|
|
192
|
+
"mode": "text",
|
|
193
|
+
"in": "el sufijo -mente y el prefijo pre- son",
|
|
194
|
+
"out": "el sufijo -mente y el prefijo pre- son",
|
|
195
|
+
"note": "Guiones de afijo, pegados por un solo lado, no son rayas."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": "es-dashes-option-flag-untouched",
|
|
199
|
+
"rule": "dashes",
|
|
200
|
+
"mode": "text",
|
|
201
|
+
"in": "usa --force y -v",
|
|
202
|
+
"out": "usa --force y -v",
|
|
203
|
+
"note": "Opciones de línea de comandos: no son rayas."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": "es-dashes-misplaced-spacing-untouched",
|
|
207
|
+
"rule": "dashes",
|
|
208
|
+
"mode": "text",
|
|
209
|
+
"in": "Llegó— por fin —a casa.",
|
|
210
|
+
"out": "Llegó— por fin —a casa.",
|
|
211
|
+
"note": "Espacios invertidos respecto a la forma del DPD: no se reinterpretan."
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"id": "es-dashes-question-inside-paired",
|
|
215
|
+
"rule": "dashes",
|
|
216
|
+
"mode": "text",
|
|
217
|
+
"in": "Llegó —¿quién lo diría?— a casa.",
|
|
218
|
+
"out": "Llegó —¿quién lo diría?— a casa.",
|
|
219
|
+
"note": "El signo de interrogación dentro del par no corta la frase."
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "es-dashes-abbreviation-splits-pair",
|
|
223
|
+
"rule": "dashes",
|
|
224
|
+
"mode": "text",
|
|
225
|
+
"in": "Llegó - p. ej. ayer - a casa.",
|
|
226
|
+
"out": "Llegó - p. ej. ayer - a casa.",
|
|
227
|
+
"note": "El punto de la abreviatura cierra la frase: cada mitad tiene una sola raya, no hay par y no se toca ninguna (el espacio duro U+00A0 tras «p.» lo pone nbsp)."
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"id": "es-dashes-lone-en-dash-untouched",
|
|
231
|
+
"rule": "dashes",
|
|
232
|
+
"mode": "text",
|
|
233
|
+
"in": "Llegó – por fin – a casa.",
|
|
234
|
+
"out": "Llegó – por fin – a casa.",
|
|
235
|
+
"note": "P5 de dashes.md: una semirraya sola escrita por el autor no se reinterpreta."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"id": "es-dashes-digit-far-dash-untouched",
|
|
239
|
+
"rule": "dashes",
|
|
240
|
+
"mode": "text",
|
|
241
|
+
"in": "Vimos 3 - 4--por fin--bien.",
|
|
242
|
+
"out": "Vimos 3 - 4--por fin--bien.",
|
|
243
|
+
"note": "T1 de dashes.md: no se inserta un espacio junto a una cifra que tiene otra raya al otro lado."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"id": "es-dashes-roman-range-before-pair",
|
|
247
|
+
"rule": "dashes",
|
|
248
|
+
"mode": "text",
|
|
249
|
+
"in": "En los siglos XV—XVII - según dicen - hubo guerras.",
|
|
250
|
+
"out": "En los siglos XV—XVII - según dicen - hubo guerras.",
|
|
251
|
+
"note": "Un rango en cifras romanas cuenta como raya del segmento: tres rayas, no se toca ninguna."
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"id": "es-dashes-abbreviation-title-splits-pair",
|
|
255
|
+
"rule": "dashes",
|
|
256
|
+
"mode": "text",
|
|
257
|
+
"in": "Llegó - dijo el Sr. Pérez - a casa.",
|
|
258
|
+
"out": "Llegó - dijo el Sr. Pérez - a casa.",
|
|
259
|
+
"note": "El punto de «Sr.» cierra la frase aunque `nbsp` ponga después un espacio duro: cada mitad tiene una sola raya y no se toca ninguna. Debe dar lo mismo en la segunda pasada."
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"id": "es-dashes-plus-minus-outside-pair",
|
|
263
|
+
"rule": "dashes",
|
|
264
|
+
"mode": "text",
|
|
265
|
+
"in": "Llegó - con +/- 5 kg - a casa.",
|
|
266
|
+
"out": "Llegó —con ± 5 kg— a casa.",
|
|
267
|
+
"note": "El guion de «+/-» no es una raya: `symbols` lo convierte en «±» después, de modo que no debe contar para el par."
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"id": "es-dashes-roman-range-beside-single",
|
|
271
|
+
"rule": "dashes",
|
|
272
|
+
"mode": "text",
|
|
273
|
+
"in": "Era XV—XVII - dicen.",
|
|
274
|
+
"out": "Era XV—XVII - dicen.",
|
|
275
|
+
"note": "P4: la raya pegada entre dos números romanos es un rango ya correcto y cuenta como raya del segmento; con la otra son dos y no se convierte ninguna."
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"id": "es-dashes-digit-inside-pair-cluster",
|
|
279
|
+
"rule": "dashes",
|
|
280
|
+
"mode": "text",
|
|
281
|
+
"in": "Salió - 5 - de casa.",
|
|
282
|
+
"out": "Salió —5— de casa.",
|
|
283
|
+
"note": "El número solo dentro del par convierte; en la segunda pasada las dos rayas forman un grupo con la cifra y se rechazan, de modo que el resultado no cambia."
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"id": "es-dashes-nbsp-beside-stroke-untouched",
|
|
287
|
+
"rule": "dashes",
|
|
288
|
+
"mode": "text",
|
|
289
|
+
"in": "Llegó - por fin - a casa.",
|
|
290
|
+
"out": "Llegó - por fin - a casa.",
|
|
291
|
+
"note": "Un espacio duro junto a una raya la deja fuera: el segmento queda como está."
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"id": "es-dashes-asymmetric-range-inside-pair",
|
|
295
|
+
"rule": "dashes",
|
|
296
|
+
"mode": "text",
|
|
297
|
+
"in": "Llegó - a 5 -10 km - a casa.",
|
|
298
|
+
"out": "Llegó —a 5 -10 km— a casa.",
|
|
299
|
+
"note": "Un rango de cifras escrito con espacio solo a un lado es un candidato de `ranges`, sea simétrico o no, y no cuenta como raya."
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"id": "es-dashes-question-space-splits-pair",
|
|
303
|
+
"rule": "dashes",
|
|
304
|
+
"mode": "text",
|
|
305
|
+
"in": "Llegó - ¿cómo? ya - a casa.",
|
|
306
|
+
"out": "Llegó - ¿cómo? ya - a casa.",
|
|
307
|
+
"note": "Un «?» seguido de espacio cierra la frase: cada mitad tiene una sola raya."
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"id": "es-dashes-marker-splits-pair-html",
|
|
311
|
+
"rule": "dashes",
|
|
312
|
+
"mode": "html",
|
|
313
|
+
"in": "<p>Llegó - ayer.<em>Hoy</em> - a casa.</p>",
|
|
314
|
+
"out": "<p>Llegó - ayer.<em>Hoy</em> - a casa.</p>",
|
|
315
|
+
"note": "Un punto antes de un límite de marcado cierra la frase: cada mitad tiene una sola raya."
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"id": "es-dashes-marker-splits-pair-markdown",
|
|
319
|
+
"rule": "dashes",
|
|
320
|
+
"mode": "markdown",
|
|
321
|
+
"in": "Llegó - ayer.*Hoy* - a casa.",
|
|
322
|
+
"out": "Llegó - ayer.*Hoy* - a casa.",
|
|
323
|
+
"note": "Un punto antes de un límite de marcado cierra la frase: cada mitad tiene una sola raya.",
|
|
324
|
+
"dialect": "commonmark"
|
|
60
325
|
},
|
|
61
326
|
{
|
|
62
327
|
"id": "es-dashes-compound-hyphen",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"rule": "dashes",
|
|
53
53
|
"cite": "Διοργανικό εγχειρίδιο σύνταξης κειμένων (EL, 2011), §10.1.8 «Ενωτικό — Παύλα μεσαίου μεγέθους — Μεγάλη παύλα»: για τα αριθμητικά διαστήματα, «Παύλα μεσαίου μεγέθους ή μείον (–) […] γ) για να δηλώσει το διάστημα μεταξύ δύο ορίων (π.χ. άτομα ηλικίας 25–45 ετών) […] δ) στην αναφορά σε περιόδους τουλάχιστον δύο πλήρων ετών (π.χ. 1989–1991) […] Ωστόσο, και στην περίπτωση αυτή μπορεί να χρησιμοποιηθεί και ενωτικό». Ο ίδιος κανόνας για τα αριθμητικά διαστήματα εμφανίζεται ΔΥΟ ΦΟΡΕΣ στην §10.1.8, μία στην υποενότητα «Ενωτικό (-)» και μία στην υποενότητα «Παύλα μεσαίου μεγέθους (–)», και κάθε φορά η υποενότητα παραχωρεί ρητά το άλλο σημείο (paraphrase of the «Ενωτικό» occurrence — its verbatim text was not captured at review time; the «Παύλα» occurrence is quoted above verbatim)· για την παρενθετική χρήση, «Όπως η παρένθεση, η διπλή παύλα δεν χωρίζεται με κενά διαστήματα από τη λέξη, φράση ή πρόταση που περικλείει· αντίθετα, μπαίνουν διαστήματα πριν από την πρώτη και μετά τη δεύτερη παύλα»",
|
|
54
54
|
"url": "http://publications.europa.eu/resource/cellar/e774ea2a-ef84-4bf6-be92-c9ebebf91c1b.0015.03/DOC_2",
|
|
55
|
-
"note": "Justifies \"none\" on BOTH fields, which no other locale has — but for TWO DIFFERENT REASONS, and conflating them would misrepresent the source. Range: genuinely ambivalent. The rule appears twice in §10.1.8, once under «Ενωτικό (-)» and once under «Παύλα μεσαίου μεγέθους (–)», and each occurrence explicitly concedes the other mark. That mutual concession is the proof — a source that names both forms in both places is not being vague, it is declining to rank them, and substituting either would express a preference the citation does not carry. Parenthetical: NOT ambivalent. The guide prescribes a U+2014 pair (its own header gives «Alt 0151») with ordinary spaces outside the pair and none on the inner edges. \"none\" here is forced by
|
|
55
|
+
"note": "Justifies \"none\" on BOTH fields, which no other locale has — but for TWO DIFFERENT REASONS, and conflating them would misrepresent the source. Range: genuinely ambivalent. The rule appears twice in §10.1.8, once under «Ενωτικό (-)» and once under «Παύλα μεσαίου μεγέθους (–)», and each occurrence explicitly concedes the other mark. That mutual concession is the proof — a source that names both forms in both places is not being vague, it is declining to rank them, and substituting either would express a preference the citation does not carry. Parenthetical: NOT ambivalent. The guide prescribes a U+2014 pair (its own header gives «Alt 0151») with ordinary spaces outside the pair and none on the inner edges. \"none\" here is forced by the SOURCE CONFLICT below, not by a schema limit: the schema has expressed the spacing this sentence prescribes since \"em-paired\" (spec 1.18.0), but that value is an em dash, and the choice between em and en for Greek is not settled. See spec/rules/dashes.md §6 «el», which states the two justifications separately. SOURCE CONFLICT, recorded and deliberately not settled: this guide prescribes U+2014 for the parenthetical dash, while ΥΠΕΠΘ/ΙΤΥΕ Γραμματική Α΄–Γ΄ Γυμνασίου §3.3 appears to set U+2013 in the same role. The sources array has no way to represent a conflict — it models agreement, not disagreement — so it is recorded here in prose. Resolving it is with the operator and needs a source that ranks the two, not a third that adds a form. Retrieved 2026-08-15."
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"rule": "ellipsis",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dash": {
|
|
22
|
-
"parenthetical": "em-
|
|
22
|
+
"parenthetical": "em-paired",
|
|
23
23
|
"range": "none"
|
|
24
24
|
},
|
|
25
25
|
"ellipsis": {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"rule": "dashes",
|
|
68
68
|
"cite": "RAE y ASALE, Diccionario panhispánico de dudas, entrada «raya», apartado 2: «Como el resto de los signos dobles, se escriben pegadas a la primera y a la última palabra del periodo que enmarcan, y separadas por un espacio de la palabra o signo que las precede o las sigue»",
|
|
69
69
|
"url": "https://www.rae.es/dpd/raya",
|
|
70
|
-
"note": "parenthetical = \"em-
|
|
70
|
+
"note": "parenthetical = \"em-paired\": la raya en U+2014 escrita como par, con un espacio ORDINARIO fuera del par y ninguno en los bordes interiores («Llegó —por fin— a casa»), que es lo que prescribe la fuente. La regla solo convierte una frase que contiene exactamente dos rayas candidatas; un guion suelto («Madrid - la capital») o una frase con más de dos rayas se deja como está, porque el DPD no describe el uso de la raya en esas posiciones y nada permite decidir cuál abre y cuál cierra. Las rayas de diálogo («—Hola —dijo—.») no se tocan. Hasta spec 1.17.5 el valor era \"em-spaced\", una aproximación que dejaba dos espacios interiores que el DPD no admite; hasta spec 1.16.0 era \"none\". Consultado el 18.09.2026."
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"rule": "ranges",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.18.0",
|
|
3
3
|
"$comment": "Locale resolution input. Algorithm is specified in spec/rules/locale-resolution.md and is identical in every runtime — never delegate it to a platform locale-negotiation library. \"spec\" here must track spec/VERSION exactly — it is not itself the global version source; scripts/validate-spec.mjs enforces the match.",
|
|
4
4
|
"locales": [
|
|
5
5
|
"en-US",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
**Spec version:** 0.6.0 (0.2.0 for everything except the 0.5.0/0.6.0 changes noted inline and in
|
|
5
5
|
§8 History), amended in **1.3.0**: §1 and §3.4's statement of what belongs to `ranges`, §3.2a's
|
|
6
6
|
re-entry condition, and §3.2 steps 7 and 8, all for [ranges.md](ranges.md) §3.2a's closed-up
|
|
7
|
-
symbols.
|
|
7
|
+
symbols, and again in **1.18.0**: the value `"em-paired"` and its own algorithm, §3.4a.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -39,14 +39,17 @@ in every prior spec version, since the two branches were always mutually exclusi
|
|
|
39
39
|
0.5.0 split makes that exclusivity a boundary between two rules instead of two branches of one.
|
|
40
40
|
|
|
41
41
|
The parenthetical form is governed by one enum in the locale file: a dash _length_ (`em` or `en`)
|
|
42
|
-
and a _spacing_ (`tight` or `spaced`),
|
|
42
|
+
and a _spacing_ (`tight` or `spaced`), the opt-out value `none`, or — since spec 1.18.0 — the one
|
|
43
|
+
value that is neither symmetric nor decided token by token, `em-paired` (§3.4a).
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
46
47
|
## 2. Locale data consumed
|
|
47
48
|
|
|
48
|
-
- `dash.parenthetical` — one of `"em-tight"`, `"em-spaced"`, `"
|
|
49
|
-
`"none"`.
|
|
49
|
+
- `dash.parenthetical` — one of `"em-tight"`, `"em-spaced"`, `"em-paired"`, `"en-tight"`,
|
|
50
|
+
`"en-spaced"`, `"none"`. `"em-paired"` (spec 1.18.0) writes the pair as an em dash with a space
|
|
51
|
+
**outside** the pair and none inside it (`a —b— c`); it is specified in §3.4a, because it is the
|
|
52
|
+
one value whose result depends on which stroke opens the pair and which closes it.
|
|
50
53
|
|
|
51
54
|
**As of spec 0.5.0, `dashes` no longer reads `dash.range`** — that field is now read exclusively
|
|
52
55
|
by `ranges` ([ranges.md](ranges.md) §2), under the same key, with the same values and the same
|
|
@@ -164,7 +167,9 @@ U+2011 they meant it.
|
|
|
164
167
|
then "counts on the left only") each fixed the witness in front of them and left the next
|
|
165
168
|
one reachable; §8.2 defect (d) records how.
|
|
166
169
|
|
|
167
|
-
4. **Symmetry guard.** If `lsp ≠ rsp`, emit nothing and continue.
|
|
170
|
+
4. **Symmetry guard.** If `lsp ≠ rsp`, emit nothing and continue. (`"em-paired"` is the one
|
|
171
|
+
exception: §3.4a counts such a stroke and, when it is a single U+2014, reads it as half of a
|
|
172
|
+
pair.) A stroke that is spaced on
|
|
168
173
|
one side and tight on the other is not a dash: it is `--force`, `-5 °C`, a Markdown bullet
|
|
169
174
|
`- item`, a signature `-- Iurii`, or an arrow `->`. This single guard removes the large
|
|
170
175
|
majority of false positives and it applies to both the range and the parenthetical
|
|
@@ -500,6 +505,7 @@ Emit one edit replacing `cp[s - lsp … e - 1 + rsp]` with:
|
|
|
500
505
|
| -------------------- | ------------------------------------------------------- |
|
|
501
506
|
| `"em-tight"` | U+2014 |
|
|
502
507
|
| `"em-spaced"` | U+0020 U+2014 U+0020 |
|
|
508
|
+
| `"em-paired"` | _(not a per-token replacement — see §3.4a)_ |
|
|
503
509
|
| `"en-tight"` | U+2013 |
|
|
504
510
|
| `"en-spaced"` | U+0020 U+2013 U+0020 |
|
|
505
511
|
| `"none"` | _(emit nothing — the token is left exactly as written)_ |
|
|
@@ -507,6 +513,55 @@ Emit one edit replacing `cp[s - lsp … e - 1 + rsp]` with:
|
|
|
507
513
|
**Subject to guards T1 and T2 (§3.2 steps 8 and 9), evaluated now that the form is known.** If the replacement is identical to the span it replaces, emit
|
|
508
514
|
nothing.
|
|
509
515
|
|
|
516
|
+
### 3.4a `"em-paired"` — the pair as a unit (spec 1.18.0)
|
|
517
|
+
|
|
518
|
+
When `dash.parenthetical` is `"em-paired"`, §3.4 is replaced by this section and the token loop
|
|
519
|
+
of §3.2 is replaced by a loop over **segments**. Everything else a stroke must satisfy — steps 2,
|
|
520
|
+
5, 6 and 7 of §3.2, §3.2a, P1, P4 and P5 — still applies, because those guards decide whether a
|
|
521
|
+
stroke is a dash at all.
|
|
522
|
+
|
|
523
|
+
**Segment.** A segment is a stretch of the text unit bounded by the unit's start and end and by
|
|
524
|
+
a *terminal*: U+002E, U+003F, U+0021 or U+2026 that is followed by U+0020, a `NOBREAK-SPACE`, a
|
|
525
|
+
`BREAK`, a marker, or the end of the unit. A `NOBREAK-SPACE` counts because `nbsp` (order 70) puts
|
|
526
|
+
one after a terminal (`Sr. Pérez`, `p. ej.`); if only U+0020 closed a segment, that edit would
|
|
527
|
+
move the boundary and a second pass would pair strokes the first had kept apart. A terminal followed by anything else (`3.5`, `—¿quién?—`) does not end a
|
|
528
|
+
segment. A `BREAK` is **not** a segment boundary.
|
|
529
|
+
|
|
530
|
+
**Counted strokes.** Every maximal `DASH` run of §3.2 step 1 in a segment is classified:
|
|
531
|
+
|
|
532
|
+
- *Not a stroke*, and not counted: a run with `k > 3`; a range candidate (§3.3, ranges.md §3.2),
|
|
533
|
+
decided by its flanks alone, **whether or not the run is symmetric** (`5 -10` is a candidate);
|
|
534
|
+
a single hyphen-shaped run (U+002D, U+2010, U+2212) with `lsp = rsp = 0`, which is a compound;
|
|
535
|
+
and the U+002D of the literal sequence `+/-`, which `symbols` (order 80) turns into `±`.
|
|
536
|
+
- *Counted and eligible:* every other run that passes steps 5, 6 and 7, §3.2a, P4 and P5 and is
|
|
537
|
+
either symmetric (`lsp = rsp`) or a **single U+2014** spaced on one side only.
|
|
538
|
+
- *Counted and ineligible:* every other run — including a stroke at the start or end of a line
|
|
539
|
+
(the dialogue dash), one next to a `NOBREAK-SPACE`, `-mente`, `pre-`, `--force`, a Markdown
|
|
540
|
+
bullet and a lone U+2013.
|
|
541
|
+
|
|
542
|
+
**Decision.** A segment is converted if and only if **all** of the following hold; otherwise the
|
|
543
|
+
whole segment is left exactly as typed:
|
|
544
|
+
|
|
545
|
+
1. it has **exactly two** counted strokes, and both are eligible;
|
|
546
|
+
2. no `BREAK` lies between them;
|
|
547
|
+
3. the first is not shaped like a closer (`lsp = 0`, `rsp = 1`) and the second is not shaped like
|
|
548
|
+
an opener (`lsp = 1`, `rsp = 0`);
|
|
549
|
+
4. T1 (§3.2 step 8) does not block a side on which a space is inserted (step 5 below).
|
|
550
|
+
|
|
551
|
+
**Emission.** For the first stroke (the opener) and the second (the closer), the glyph is U+2014
|
|
552
|
+
and the **inner** side — to the right of the opener, to the left of the closer — is made tight,
|
|
553
|
+
removing a U+0020 if there was one. The **outer** side keeps a U+0020 if it had one; if it had
|
|
554
|
+
none, a U+0020 is inserted when the neighbouring code point `cp[L]` (opener) or `cp[R]` (closer)
|
|
555
|
+
is in `LETTER` or `DIGIT`, and otherwise the side stays tight, so `(—`, `«—`, `—»` and
|
|
556
|
+
`—por fin—.` are produced without a space the `spaces` rule would remove. T2 (§3.2 step 9) is
|
|
557
|
+
therefore vacuous: a space is only ever inserted beside a letter or a digit. A replacement
|
|
558
|
+
identical to the span it replaces is not emitted.
|
|
559
|
+
|
|
560
|
+
**What is left alone, by construction.** A lone stroke (`Madrid - la capital`); three or more
|
|
561
|
+
strokes in a segment; a pair split by a terminal (`p. ej.`) or by a line break; the dialogue dash
|
|
562
|
+
and any segment holding it (`—Hola —dijo—.`); affix hyphens; a pair whose strokes are separated by
|
|
563
|
+
a `NOBREAK-SPACE`. These are the accepted cost of not guessing which stroke opens which (§7.8).
|
|
564
|
+
|
|
510
565
|
### 3.5 Continue, and the non-overlap guarantee
|
|
511
566
|
|
|
512
567
|
Set `i` to the index just past the token's run in the **input** array and continue from §3.2
|
|
@@ -689,6 +744,33 @@ Every input to a declined token's verdict is therefore unaffected by any edit `T
|
|
|
689
744
|
pass: `T(T(x))` declines exactly where `T(x)` declined. The historical counterexamples that led to
|
|
690
745
|
this guard set — before it closed every gap — are summarised in §8.2.
|
|
691
746
|
|
|
747
|
+
### 5.2a `"em-paired"` — the segment verdict is stable (spec 1.18.0)
|
|
748
|
+
|
|
749
|
+
Under `"em-paired"` the verdict is made per segment (§3.4a), not per token, so §5.1's table does
|
|
750
|
+
not apply and the argument is rebuilt on the three facts a segment verdict reads.
|
|
751
|
+
|
|
752
|
+
- **Fixed points.** The output form has two U+2014 strokes, each spaced on its outer side only (or
|
|
753
|
+
tight beside a non-letter), which is eligible and correctly shaped, and the replacement of each
|
|
754
|
+
is byte-identical to its span: nothing is emitted.
|
|
755
|
+
- **The segment boundaries do not move.** Edits touch only a dash run and the U+0020 beside it.
|
|
756
|
+
A U+0020 is inserted only after a letter, a digit or a dash, never after a terminal, and the
|
|
757
|
+
first stroke after a terminal is an opener whose outer U+0020 is kept, so a terminal followed by
|
|
758
|
+
a space stays followed by one. No edit creates or removes a terminal.
|
|
759
|
+
- **The counted set does not change in the dangerous direction.** A converted segment can become
|
|
760
|
+
declined on the next pass (`a - 5 - b` gives `a —5— b`, whose two strokes then share a
|
|
761
|
+
cluster with the digit, §3.2 step 7), and that is harmless: a declined segment emits nothing, so
|
|
762
|
+
the text is already a fixed point. What would break idempotency is "declined → converted" on a
|
|
763
|
+
later pass, which needs a counted stroke to disappear, or the segment to change. A stroke that
|
|
764
|
+
was declined stays declined for the reasons of §5.2.
|
|
765
|
+
- **Composition obligation on later rules.** Every rule after `dashes` must leave the count of
|
|
766
|
+
counted strokes in a segment alone. `hyphen` (order 35) emits only `INERT-DASH`, which is not a
|
|
767
|
+
`DASH`; `symbols` rewrites `+/-` into `±`, which is why that hyphen is *not a stroke* above;
|
|
768
|
+
`nbsp` changes only U+0020 into `NOBREAK-SPACE`, which can make a stroke ineligible but moves
|
|
769
|
+
no segment boundary, because a terminal closes a segment before either.
|
|
770
|
+
|
|
771
|
+
The one direction that would break idempotency is "declined → converted" on the second pass. It
|
|
772
|
+
needs a counted stroke to disappear or a segment boundary to move; neither can happen.
|
|
773
|
+
|
|
692
774
|
### 5.3 Interaction with `ranges` is stable
|
|
693
775
|
|
|
694
776
|
`ranges` (order 25) runs before `dashes` (order 30) in every pass. Three guarantees hold between
|
|
@@ -903,8 +985,8 @@ never reaches a digit-flanked token at all.
|
|
|
903
985
|
The first locale with `"none"` on **both** fields, so the rule is a **total no-op** for it in the
|
|
904
986
|
same provable sense `hyphen` is a no-op for a locale with empty lists. It is no longer the only
|
|
905
987
|
one: `tr` (spec 1.6.0) is both-`none` too, and `es` was from spec 1.3.0 until 1.17.1, each for its
|
|
906
|
-
own reason and each recorded in its own locale file. (`es`
|
|
907
|
-
|
|
988
|
+
own reason and each recorded in its own locale file. (`es` sets `parenthetical` to `"em-paired"` since spec 1.18.0 — it was
|
|
989
|
+
`"em-spaced"`, an approximation, from 1.17.0 — and keeps `range` at `"none"`.) This section stays written about `el` because its two fields are
|
|
908
990
|
`"none"` for two *different* reasons, which is what makes it worth reading. Tokens are still
|
|
909
991
|
classified — §3.3's "a range token is never reconsidered as a parenthetical" still holds — but no
|
|
910
992
|
classification has an emission to make. The rows below are therefore all "no change" rows by
|
|
@@ -922,18 +1004,18 @@ the guide is explicit.
|
|
|
922
1004
|
that names both forms in both places is not being vague; it is declining to rank them.
|
|
923
1005
|
Emitting either would express a preference the citation does not carry, so the rule emits
|
|
924
1006
|
nothing.
|
|
925
|
-
- **`parenthetical: "none"` — a
|
|
1007
|
+
- **`parenthetical: "none"` — a source conflict, no longer a schema limit.** The guide prescribes a U+2014
|
|
926
1008
|
pair (its own header gives «Alt 0151») and states the spacing exactly: «Όπως η παρένθεση, η
|
|
927
1009
|
διπλή παύλα δεν χωρίζεται με κενά διαστήματα από τη λέξη, φράση ή πρόταση που περικλείει·
|
|
928
1010
|
αντίθετα, μπαίνουν διαστήματα πριν από την πρώτη και μετά τη δεύτερη παύλα.» — ordinary spaces
|
|
929
|
-
**outside** the pair, none on the **inner** edges.
|
|
930
|
-
that asymmetry
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
In both cases `"none"` is the honest encoding, not a placeholder awaiting a default (§2) —
|
|
936
|
-
|
|
1011
|
+
**outside** the pair, none on the **inner** edges. Until spec 1.18.0 the enum had no value for
|
|
1012
|
+
that asymmetry; `"em-paired"` (§3.4a) is that value. `el` stays `"none"` all the same, because
|
|
1013
|
+
`"em-paired"` is an em dash and the state school grammar appears to set an en dash in the same
|
|
1014
|
+
role: choosing between them needs a source that ranks the two, and is not a licence to
|
|
1015
|
+
approximate.
|
|
1016
|
+
|
|
1017
|
+
In both cases `"none"` is the honest encoding, not a placeholder awaiting a default (§2) — and
|
|
1018
|
+
both are now waiting on a _source_. `spec/locales/el.json`'s `dashes.note` carries the
|
|
937
1019
|
detail, including a **source conflict** this document does not try to settle: the EU guide sets
|
|
938
1020
|
U+2014 parenthetically while the state school grammar appears to set U+2013.
|
|
939
1021
|
|
|
@@ -1038,6 +1120,16 @@ development, the compound-label widening's original acceptance) moved to §8.
|
|
|
1038
1120
|
exists, a port could transcribe either set and stay green. (T2 is shared with `ranges`, §3.3
|
|
1039
1121
|
there — the same gap applies to a `-spaced` range replacement before U+2026.)
|
|
1040
1122
|
|
|
1123
|
+
8. **`"em-paired"` converts only a segment with exactly two strokes (spec 1.18.0).** The rule has
|
|
1124
|
+
no syntax to read, so which stroke opens and which closes can only be inferred from there being
|
|
1125
|
+
two. The price, measured on fixtures and not yet on a corpus, is that these stay as typed:
|
|
1126
|
+
a lone spaced hyphen (`Madrid - la capital`), a sentence with two incisos (four strokes), an
|
|
1127
|
+
inciso holding an abbreviation or a `?`/`!` followed by a space, an inciso split by a line break,
|
|
1128
|
+
and any sentence that also holds a dialogue dash. The converse risk is a list written with
|
|
1129
|
+
spaced hyphens: `Madrid - Barcelona - Sevilla` becomes `Madrid —Barcelona— Sevilla`. The DPD
|
|
1130
|
+
names neither case, so both are recorded here and pinned by fixtures in `fixtures/es.json`
|
|
1131
|
+
rather than decided by a heuristic.
|
|
1132
|
+
|
|
1041
1133
|
---
|
|
1042
1134
|
|
|
1043
1135
|
## 8. History
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.
|
|
2
|
+
"spec": "1.18.0",
|
|
3
3
|
"$comment": "Single source of truth for pipeline order. Rules run in ascending `order`. Disabling a rule removes it from the sequence and never reorders the rest. Rule ids are public API (see docs/ARCHITECTURE.md section 5). \"spec\" here must track spec/VERSION exactly — it is not itself the global version source; scripts/validate-spec.mjs enforces the match.",
|
|
4
4
|
"rules": [
|
|
5
5
|
{
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"default": "on",
|
|
33
33
|
"modes": ["text", "html", "markdown", "yaml"],
|
|
34
34
|
"localeData": ["dash"],
|
|
35
|
-
"summary": "Parenthetical dash per locale convention (dash.parenthetical). Never reinterprets a range candidate as a parenthetical dash — that shape belongs to `ranges` alone, whether or not `ranges` is enabled (spec 0.5.0; range detection moved out of this rule). A candidate is digit-flanked, or flanked by a closed-up symbol matched on the opposite member (spec 1.3.0; ranges.md §3.2a)."
|
|
35
|
+
"summary": "Parenthetical dash per locale convention (dash.parenthetical). Never reinterprets a range candidate as a parenthetical dash — that shape belongs to `ranges` alone, whether or not `ranges` is enabled (spec 0.5.0; range detection moved out of this rule). A candidate is digit-flanked, or flanked by a closed-up symbol matched on the opposite member (spec 1.3.0; ranges.md §3.2a). The value `em-paired` (spec 1.18.0) is decided per sentence rather than per token: it converts a sentence holding exactly two eligible strokes (dashes.md §3.4a)."
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"id": "hyphen",
|
|
@@ -190,6 +190,13 @@ or G3.
|
|
|
190
190
|
violates `I₃` when it changes the **spacing** around a U+002D/U+2013/U+2014, because spacing
|
|
191
191
|
is what `dashes` reads to decide whether a stroke is a parenthetical dash at all.
|
|
192
192
|
|
|
193
|
+
Under `dash.parenthetical = "em-paired"` (spec 1.18.0) the verdict is no longer local to a token:
|
|
194
|
+
`dashes` decides per segment, from the count and eligibility of the strokes in it (`dashes.md`
|
|
195
|
+
§3.4a, §5.2a). A later rule violates `I₃` there only by changing the count of counted strokes
|
|
196
|
+
or moving a segment boundary. `nbsp` changes U+0020 into a `NOBREAK-SPACE`, which dashes.md §3.4a
|
|
197
|
+
treats as closing a segment exactly as U+0020 does; `symbols` consumes the hyphen of `+/-`, which
|
|
198
|
+
§3.4a already excludes; `hyphen` emits only an `INERT-DASH`.
|
|
199
|
+
|
|
193
200
|
**Who can violate it.** Nobody, since spec 0.1.0 — and it is worth recording that this line
|
|
194
201
|
previously read _"`nbsp`, which inserts and converts space-like code points"_, which was true
|
|
195
202
|
and was the source of two defects. `dashes` now treats both U+00A0 and U+202F as making an
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"required": ["parenthetical", "range"],
|
|
39
39
|
"properties": {
|
|
40
40
|
"parenthetical": {
|
|
41
|
-
"enum": ["em-tight", "em-spaced", "en-tight", "en-spaced", "none"],
|
|
42
|
-
"description": "Dash used for a parenthetical break. `-tight` means no surrounding spaces, `-spaced` means a regular space on each side (the leading one may later be made non-breaking by the nbsp rule). `none` means the rule must not substitute anything: either the locale has no verified convention, or the cited sources attest more than one form and rank none."
|
|
41
|
+
"enum": ["em-tight", "em-spaced", "em-paired", "en-tight", "en-spaced", "none"],
|
|
42
|
+
"description": "Dash used for a parenthetical break. `-tight` means no surrounding spaces, `-spaced` means a regular space on each side (the leading one may later be made non-breaking by the nbsp rule). `em-paired` is the em dash written as a pair, with a space outside the pair and none inside it (`a —b— c`). `none` means the rule must not substitute anything: either the locale has no verified convention, or the cited sources attest more than one form and rank none."
|
|
43
43
|
},
|
|
44
44
|
"range": {
|
|
45
45
|
"enum": ["em-tight", "em-spaced", "en-tight", "en-spaced", "none"],
|
|
@@ -131,7 +131,7 @@ module Polytypo
|
|
|
131
131
|
|
|
132
132
|
# The target dash glyph for a style value.
|
|
133
133
|
def dash_code_point(style)
|
|
134
|
-
style == "em-tight" || style == "em-spaced" ? EM_DASH : EN_DASH
|
|
134
|
+
style == "em-tight" || style == "em-spaced" || style == "em-paired" ? EM_DASH : EN_DASH
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
# Whether cp[start...end] is exactly `next`, code point for code point.
|
|
@@ -285,36 +285,47 @@ module Polytypo
|
|
|
285
285
|
# `dashes` token becoming spaced can insert a space next to a `ranges` token's digit run,
|
|
286
286
|
# and vice versa.
|
|
287
287
|
def spacing_transition_blocked?(cp, left, right)
|
|
288
|
+
spacing_transition_blocked_left?(cp, left) || spacing_transition_blocked_right?(cp, right)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# The left half of T1, for a form that inserts a space on one side only (`em-paired`).
|
|
292
|
+
def spacing_transition_blocked_left?(cp, left)
|
|
288
293
|
n = cp.length
|
|
289
294
|
|
|
290
295
|
# Spec 1.3.0, position p1: step over a CLOSED-SYMBOL between the token and the run.
|
|
291
296
|
left -= 1 if left >= 0 && left < n && closed_up_symbol?(cp[left]) && left.positive? &&
|
|
292
297
|
digit?(cp[left - 1])
|
|
298
|
+
|
|
299
|
+
return false unless left >= 0 && left < n && digit?(cp[left])
|
|
300
|
+
|
|
301
|
+
d = left
|
|
302
|
+
d -= 1 while d.positive? && digit?(cp[d - 1])
|
|
303
|
+
# Position p2: and over one at the far end of the run.
|
|
304
|
+
i1 = effective_index(cp, skip_closed_up_symbol(cp, d - 1, -1), -1)
|
|
305
|
+
one = i1.nil? ? Polytypo::Engine::NONE : cp[i1]
|
|
306
|
+
two = i1.nil? ? Polytypo::Engine::NONE : effective_neighbour(cp, i1 - 1, -1)
|
|
307
|
+
return true if dash_union?(one)
|
|
308
|
+
|
|
309
|
+
(one == SPACE || no_break_space?(one)) && dash_union?(two)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# The right half of T1; see spacing_transition_blocked_left?.
|
|
313
|
+
def spacing_transition_blocked_right?(cp, right)
|
|
314
|
+
n = cp.length
|
|
315
|
+
|
|
293
316
|
right += 1 if right >= 0 && right < n && closed_up_symbol?(cp[right]) &&
|
|
294
317
|
right + 1 < n && digit?(cp[right + 1])
|
|
295
318
|
|
|
296
|
-
|
|
297
|
-
d = left
|
|
298
|
-
d -= 1 while d.positive? && digit?(cp[d - 1])
|
|
299
|
-
# Position p2: and over one at the far end of the run.
|
|
300
|
-
i1 = effective_index(cp, skip_closed_up_symbol(cp, d - 1, -1), -1)
|
|
301
|
-
one = i1.nil? ? Polytypo::Engine::NONE : cp[i1]
|
|
302
|
-
two = i1.nil? ? Polytypo::Engine::NONE : effective_neighbour(cp, i1 - 1, -1)
|
|
303
|
-
return true if dash_union?(one)
|
|
304
|
-
return true if (one == SPACE || no_break_space?(one)) && dash_union?(two)
|
|
305
|
-
end
|
|
319
|
+
return false unless right >= 0 && right < n && digit?(cp[right])
|
|
306
320
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return true if dash_union?(one)
|
|
314
|
-
return true if (one == SPACE || no_break_space?(one)) && dash_union?(two)
|
|
315
|
-
end
|
|
321
|
+
d = right
|
|
322
|
+
d += 1 while d + 1 < n && digit?(cp[d + 1])
|
|
323
|
+
i1 = effective_index(cp, skip_closed_up_symbol(cp, d + 1, 1), 1)
|
|
324
|
+
one = i1.nil? ? Polytypo::Engine::NONE : cp[i1]
|
|
325
|
+
two = i1.nil? ? Polytypo::Engine::NONE : effective_neighbour(cp, i1 + 1, 1)
|
|
326
|
+
return true if dash_union?(one)
|
|
316
327
|
|
|
317
|
-
|
|
328
|
+
(one == SPACE || no_break_space?(one)) && dash_union?(two)
|
|
318
329
|
end
|
|
319
330
|
|
|
320
331
|
# dashes.md 3.2 steps 1-7 and 3.2a, exactly as they read before the `ranges` split -- the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "dash_shared"
|
|
4
|
+
require_relative "dashes_paired"
|
|
4
5
|
require_relative "../edits"
|
|
5
6
|
require_relative "../registry"
|
|
6
7
|
require_relative "../unicode_util"
|
|
@@ -66,8 +67,10 @@ module Polytypo
|
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def scan(cp, locale_data, _ctx)
|
|
69
|
-
edits = []
|
|
70
70
|
style = locale_data["dash"]["parenthetical"]
|
|
71
|
+
return DashesPaired.scan(cp) if style == "em-paired"
|
|
72
|
+
|
|
73
|
+
edits = []
|
|
71
74
|
|
|
72
75
|
DashShared.find_tokens(cp).each do |token|
|
|
73
76
|
# A digit-flanked token is `ranges`' territory, never `dashes`' -- declined
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dash_shared"
|
|
4
|
+
require_relative "../edits"
|
|
5
|
+
require_relative "../sentinels"
|
|
6
|
+
require_relative "../unicode_util"
|
|
7
|
+
|
|
8
|
+
module Polytypo
|
|
9
|
+
module Engine
|
|
10
|
+
module Rules
|
|
11
|
+
# `dash.parenthetical = "em-paired"` -- spec/rules/dashes.md 3.4a. The pair is written with a
|
|
12
|
+
# space outside it and none inside (`Llego —por fin— a casa`), which no per-token rule can
|
|
13
|
+
# express: which stroke opens and which closes is a property of the sentence, not of the
|
|
14
|
+
# token. A sentence is converted only when it holds exactly two counted strokes and both are
|
|
15
|
+
# eligible; anything else is left as typed. Mirrors dashes-paired.ts in the JS reference
|
|
16
|
+
# implementation.
|
|
17
|
+
module DashesPaired
|
|
18
|
+
# One counted DASH run of a segment: its span [s, e), outer spacing, the flanking indices
|
|
19
|
+
# after the joiner walk, and whether it may take part in a conversion.
|
|
20
|
+
Run = Struct.new(:s, :e, :lsp, :rsp, :left, :right, :eligible, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
TERMINALS = [0x2e, 0x3f, 0x21, 0x2026].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def terminal?(cp)
|
|
27
|
+
TERMINALS.include?(cp)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A terminal ends a segment only before a space (U+0020 or a no-break space, which `nbsp`
|
|
31
|
+
# puts there later), a line break, a marker or the end of input.
|
|
32
|
+
def ends_segment?(cp, i)
|
|
33
|
+
return false unless terminal?(cp[i])
|
|
34
|
+
return true if i + 1 >= cp.length
|
|
35
|
+
|
|
36
|
+
nxt = cp[i + 1]
|
|
37
|
+
nxt == DashShared::SPACE || DashShared.no_break_space?(nxt) || DashShared.break?(nxt) ||
|
|
38
|
+
Polytypo::Engine.marker?(nxt)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def wants_space?(cp)
|
|
42
|
+
Polytypo::Engine::UnicodeUtil.letter?(cp) || DashShared.digit?(cp)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# dashes.md 3.2 steps 5-7: content on the same line, no dash or space beside the stroke's
|
|
46
|
+
# outer spacing, and not part of a cluster holding another dash run.
|
|
47
|
+
def neighbours_clean?(cp, s, e, left, right)
|
|
48
|
+
left_cp = cp[left]
|
|
49
|
+
right_cp = cp[right]
|
|
50
|
+
return false if DashShared.break?(left_cp) || DashShared.break?(right_cp)
|
|
51
|
+
return false if DashShared.inert_dash?(left_cp) || DashShared.inert_dash?(right_cp)
|
|
52
|
+
return false if DashShared.dash?(left_cp) || DashShared.dash?(right_cp)
|
|
53
|
+
return false if left_cp == DashShared::SPACE || DashShared.no_break_space?(left_cp)
|
|
54
|
+
return false if right_cp == DashShared::SPACE || DashShared.no_break_space?(right_cp)
|
|
55
|
+
|
|
56
|
+
!DashShared.cluster_inert?(cp, s, e)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns nil when the run is not a stroke at all, otherwise a Run (eligible or not).
|
|
60
|
+
def classify(cp, s, e)
|
|
61
|
+
n = cp.length
|
|
62
|
+
k = e - s
|
|
63
|
+
return nil if k > 3
|
|
64
|
+
|
|
65
|
+
lsp = s.positive? && cp[s - 1] == DashShared::SPACE ? 1 : 0
|
|
66
|
+
rsp = e < n && cp[e] == DashShared::SPACE ? 1 : 0
|
|
67
|
+
first = cp[s]
|
|
68
|
+
is_hyphen_shaped = first == DashShared::HYPHEN_MINUS || first == DashShared::HYPHEN ||
|
|
69
|
+
first == DashShared::MINUS_SIGN
|
|
70
|
+
# A hyphen inside a word is a compound, not a stroke.
|
|
71
|
+
return nil if k == 1 && is_hyphen_shaped && lsp.zero? && rsp.zero?
|
|
72
|
+
# The hyphen of `+/-`, which `symbols` (order 80) turns into a plus-minus sign: counting
|
|
73
|
+
# it would let that later rule change the number of strokes in the segment.
|
|
74
|
+
return nil if k == 1 && first == DashShared::HYPHEN_MINUS && s >= 2 && cp[s - 1] == 0x2f && cp[s - 2] == 0x2b
|
|
75
|
+
|
|
76
|
+
left = s - 1 - lsp
|
|
77
|
+
right = e + rsp
|
|
78
|
+
counted = lambda do |eligible|
|
|
79
|
+
Run.new(s: s, e: e, lsp: lsp, rsp: rsp, left: left, right: right, eligible: eligible)
|
|
80
|
+
end
|
|
81
|
+
return counted.call(false) if left.negative? || right >= n
|
|
82
|
+
|
|
83
|
+
join_start = left + 1
|
|
84
|
+
join_end = right
|
|
85
|
+
left -= 1 while left >= 0 && cp[left] == DashShared::WORD_JOINER
|
|
86
|
+
right += 1 while right < n && cp[right] == DashShared::WORD_JOINER
|
|
87
|
+
return counted.call(false) if left.negative? || right >= n
|
|
88
|
+
|
|
89
|
+
crossed_joiner = left + 1 != join_start || right != join_end
|
|
90
|
+
|
|
91
|
+
return nil unless DashShared.range_flanks(cp, left, right).nil?
|
|
92
|
+
return counted.call(false) if crossed_joiner
|
|
93
|
+
|
|
94
|
+
return counted.call(false) unless neighbours_clean?(cp, s, e, left, right)
|
|
95
|
+
|
|
96
|
+
# A stroke spaced on one side only is eligible solely as the one em dash that is already
|
|
97
|
+
# half of the target form; `-mente`, `pre-`, `--force` and `- item` are not dashes.
|
|
98
|
+
return counted.call(false) if lsp != rsp && !(k == 1 && first == DashShared::EM_DASH)
|
|
99
|
+
# P4: a tight stroke between two Roman numerals is a range already in its correct form.
|
|
100
|
+
return counted.call(false) if lsp.zero? && rsp.zero? && Dashes.roman_flanked?(cp, left, right)
|
|
101
|
+
# P5: an authored lone en dash is declined in every locale.
|
|
102
|
+
return counted.call(false) if k == 1 && first == DashShared::EN_DASH
|
|
103
|
+
|
|
104
|
+
counted.call(true)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def scan(cp)
|
|
108
|
+
n = cp.length
|
|
109
|
+
edits = []
|
|
110
|
+
segment = []
|
|
111
|
+
i = 0
|
|
112
|
+
|
|
113
|
+
while i < n
|
|
114
|
+
if DashShared.dash?(cp[i])
|
|
115
|
+
s = i
|
|
116
|
+
e = s
|
|
117
|
+
e += 1 while e < n && DashShared.dash?(cp[e])
|
|
118
|
+
i = e
|
|
119
|
+
run = classify(cp, s, e)
|
|
120
|
+
segment << run unless run.nil?
|
|
121
|
+
next
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if ends_segment?(cp, i)
|
|
125
|
+
emit_pair(cp, segment[0], segment[1], edits) if segment.length == 2
|
|
126
|
+
segment = []
|
|
127
|
+
end
|
|
128
|
+
i += 1
|
|
129
|
+
end
|
|
130
|
+
emit_pair(cp, segment[0], segment[1], edits) if segment.length == 2
|
|
131
|
+
|
|
132
|
+
edits
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def emit_pair(cp, a, b, edits)
|
|
136
|
+
return unless a.eligible && b.eligible
|
|
137
|
+
return if a.lsp.zero? && a.rsp == 1 # the first stroke is shaped like a closer
|
|
138
|
+
return if b.lsp == 1 && b.rsp.zero? # the second stroke is shaped like an opener
|
|
139
|
+
return if (a.e...b.s).any? { |j| DashShared.break?(cp[j]) }
|
|
140
|
+
|
|
141
|
+
insert_before = a.lsp.zero? && wants_space?(cp[a.left])
|
|
142
|
+
insert_after = b.rsp.zero? && wants_space?(cp[b.right])
|
|
143
|
+
return if insert_before && DashShared.spacing_transition_blocked_left?(cp, a.left)
|
|
144
|
+
return if insert_after && DashShared.spacing_transition_blocked_right?(cp, b.right)
|
|
145
|
+
|
|
146
|
+
opener = a.lsp == 1 || insert_before ? [DashShared::SPACE, DashShared::EM_DASH] : [DashShared::EM_DASH]
|
|
147
|
+
closer = b.rsp == 1 || insert_after ? [DashShared::EM_DASH, DashShared::SPACE] : [DashShared::EM_DASH]
|
|
148
|
+
|
|
149
|
+
[[a, opener], [b, closer]].each do |run, replacement|
|
|
150
|
+
span_start = run.s - run.lsp
|
|
151
|
+
span_end = run.e + run.rsp
|
|
152
|
+
next if DashShared.same_content?(cp, span_start, span_end, replacement)
|
|
153
|
+
|
|
154
|
+
edits << Polytypo::Engine::Edit.new(span_start, span_end, replacement, "dashes")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/polytypo/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: polytypo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Iurii Rogulia
|
|
@@ -125,6 +125,7 @@ files:
|
|
|
125
125
|
- lib/polytypo/engine/rules/apostrophe.rb
|
|
126
126
|
- lib/polytypo/engine/rules/dash_shared.rb
|
|
127
127
|
- lib/polytypo/engine/rules/dashes.rb
|
|
128
|
+
- lib/polytypo/engine/rules/dashes_paired.rb
|
|
128
129
|
- lib/polytypo/engine/rules/ellipsis.rb
|
|
129
130
|
- lib/polytypo/engine/rules/hyphen.rb
|
|
130
131
|
- lib/polytypo/engine/rules/nbsp.rb
|