polytypo 1.17.2 → 1.17.4
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 +9 -26
- data/lib/polytypo/data/fixtures/el.json +1 -1
- data/lib/polytypo/data/fixtures/en-GB.json +225 -1
- data/lib/polytypo/data/fixtures/en-US.json +172 -516
- data/lib/polytypo/data/fixtures/es.json +1 -1
- 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 +12 -37
- 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/registry.json +1 -1
- data/lib/polytypo/data/rules/modes.md +71 -6
- data/lib/polytypo/data/rules/order.json +1 -1
- data/lib/polytypo/modes/html.rb +0 -1
- data/lib/polytypo/modes/markdown.rb +67 -12
- data/lib/polytypo/modes/runner.rb +2 -3
- data/lib/polytypo/modes/spans.rb +59 -5
- data/lib/polytypo/version.rb +1 -1
- metadata +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.17.
|
|
2
|
+
"spec": "1.17.4",
|
|
3
3
|
"locale": "fr",
|
|
4
4
|
"cases": [
|
|
5
5
|
{
|
|
@@ -752,9 +752,7 @@
|
|
|
752
752
|
"rule": "nbsp",
|
|
753
753
|
"mode": "markdown",
|
|
754
754
|
"dialect": "commonmark",
|
|
755
|
-
"frontmatterKeys": [
|
|
756
|
-
"title"
|
|
757
|
-
],
|
|
755
|
+
"frontmatterKeys": ["title"],
|
|
758
756
|
"in": "---\ntitle: \"Chapitre 1: le début\"\ndate: \"2026-09-25\"\n---\n\nVoici: le corps.\n",
|
|
759
757
|
"out": "---\ntitle: \"Chapitre 1 : le début\"\ndate: \"2026-09-25\"\n---\n\nVoici : le corps.\n",
|
|
760
758
|
"note": "modes.md §3.7.4 with the locale that made §3.7.3 skip the block in the first place: inside a listed quoted scalar a colon is content, so fr inserts its narrow no-break space, while the unlisted date field and the key itself keep every byte. That contrast is the whole argument for a caller-named list."
|
|
@@ -863,9 +861,7 @@
|
|
|
863
861
|
"id": "fr-yaml-narrow-space-in-quoted",
|
|
864
862
|
"rule": "nbsp",
|
|
865
863
|
"mode": "yaml",
|
|
866
|
-
"keys": [
|
|
867
|
-
"description"
|
|
868
|
-
],
|
|
864
|
+
"keys": ["description"],
|
|
869
865
|
"in": "description: \"Une note !\"\n",
|
|
870
866
|
"out": "description: \"Une note !\"\n",
|
|
871
867
|
"note": "modes.md §3.8.6: a quoted scalar's content is one span, and French spacing applies inside it exactly as in text mode."
|
|
@@ -874,9 +870,7 @@
|
|
|
874
870
|
"id": "fr-yaml-no-space-across-colon-split",
|
|
875
871
|
"rule": "nbsp",
|
|
876
872
|
"mode": "yaml",
|
|
877
|
-
"keys": [
|
|
878
|
-
"description"
|
|
879
|
-
],
|
|
873
|
+
"keys": ["description"],
|
|
880
874
|
"in": "description: prose:more prose\n",
|
|
881
875
|
"out": "description: prose:more prose\n",
|
|
882
876
|
"note": "modes.md §3.8.6: the `:` is outside every span, so the insertion `nbsp` would make before it lands on a span edge and is discarded by §3.4. This is the accepted cost of the split, and it is what keeps the scalar parseable."
|
|
@@ -885,9 +879,7 @@
|
|
|
885
879
|
"id": "fr-yaml-guillemets-across-block-lines",
|
|
886
880
|
"rule": "quotes",
|
|
887
881
|
"mode": "yaml",
|
|
888
|
-
"keys": [
|
|
889
|
-
"description"
|
|
890
|
-
],
|
|
882
|
+
"keys": ["description"],
|
|
891
883
|
"in": "description: |\n Il a dit \"bonjour\n monsieur\" et il est parti\n",
|
|
892
884
|
"out": "description: |\n Il a dit « bonjour\n monsieur » et il est parti\n",
|
|
893
885
|
"note": "modes.md §3.2 model C in a locale whose quotation output is guillemets plus U+00A0 rather than curly quotes — structurally different from the en-US case of the same shape."
|
|
@@ -953,9 +945,7 @@
|
|
|
953
945
|
"id": "fr-yaml-plain-elision",
|
|
954
946
|
"rule": "apostrophe",
|
|
955
947
|
"mode": "yaml",
|
|
956
|
-
"keys": [
|
|
957
|
-
"description"
|
|
958
|
-
],
|
|
948
|
+
"keys": ["description"],
|
|
959
949
|
"in": "description: l'idee et l'autre\n",
|
|
960
950
|
"out": "description: l’idee et l’autre\n",
|
|
961
951
|
"note": "modes.md §3.8.6 in a locale where the apostrophe is elision rather than possession: the same cell as en-us-yaml-plain-apostrophe, and §8 item 4's requirement that a construct be pinned in more than one locale rather than the same English output with the locale field changed."
|
|
@@ -964,9 +954,7 @@
|
|
|
964
954
|
"id": "fr-yaml-single-quoted-escape-elision-stays",
|
|
965
955
|
"rule": "apostrophe",
|
|
966
956
|
"mode": "yaml",
|
|
967
|
-
"keys": [
|
|
968
|
-
"description"
|
|
969
|
-
],
|
|
957
|
+
"keys": ["description"],
|
|
970
958
|
"in": "description: 'l''idee et l''autre'\n",
|
|
971
959
|
"out": "description: 'l''idee et l''autre'\n",
|
|
972
960
|
"note": "modes.md §3.8.6: in a locale where the apostrophe is elision, the unit is the elision, and it is outside every span. The scalar has nothing else to convert and comes back unchanged — the cost §7.13 measures, pinned so that it is a documented property and not a surprise."
|
|
@@ -975,9 +963,7 @@
|
|
|
975
963
|
"id": "fr-yaml-single-quoted-escape-split",
|
|
976
964
|
"rule": "dashes",
|
|
977
965
|
"mode": "yaml",
|
|
978
|
-
"keys": [
|
|
979
|
-
"description"
|
|
980
|
-
],
|
|
966
|
+
"keys": ["description"],
|
|
981
967
|
"in": "description: 'l''idee -- et ''l''autre...'\n",
|
|
982
968
|
"out": "description: 'l''idee — et ''l''autre…'\n",
|
|
983
969
|
"note": "modes.md §3.8.6: a spaced dash and an ellipsis interior to a span convert, either side of units; the elisions stay straight."
|
|
@@ -986,9 +972,7 @@
|
|
|
986
972
|
"id": "fr-yaml-single-quoted-escape-growth-declined",
|
|
987
973
|
"rule": "dashes",
|
|
988
974
|
"mode": "yaml",
|
|
989
|
-
"keys": [
|
|
990
|
-
"description"
|
|
991
|
-
],
|
|
975
|
+
"keys": ["description"],
|
|
992
976
|
"in": "description: 'a''--''b'\n",
|
|
993
977
|
"out": "description: 'a''--''b'\n",
|
|
994
978
|
"note": "modes.md §3.8.6 and §3.4: `fr` is a `-spaced` locale, so the dash would grow from 2 to 3 code points against a unit. The edge-growth rule discards it, as it does against `:` and `#`. The en-US case with the same input is applied, which is what makes this one discriminate."
|
|
@@ -997,9 +981,7 @@
|
|
|
997
981
|
"id": "fr-yaml-single-quoted-no-escape",
|
|
998
982
|
"rule": "dashes",
|
|
999
983
|
"mode": "yaml",
|
|
1000
|
-
"keys": [
|
|
1001
|
-
"description"
|
|
1002
|
-
],
|
|
984
|
+
"keys": ["description"],
|
|
1003
985
|
"in": "description: 'deux -- trois'\n",
|
|
1004
986
|
"out": "description: 'deux — trois'\n",
|
|
1005
987
|
"note": "modes.md §3.8.6: a single-quoted scalar with nothing doubled inside it is claimed like any other. The style itself never disables the mode — only the doubling does, which is what the neighbouring case pins."
|
|
@@ -1018,10 +1000,7 @@
|
|
|
1018
1000
|
"rule": "quotes",
|
|
1019
1001
|
"mode": "markdown",
|
|
1020
1002
|
"dialect": "commonmark",
|
|
1021
|
-
"frontmatterKeys": [
|
|
1022
|
-
"title",
|
|
1023
|
-
"slug"
|
|
1024
|
-
],
|
|
1003
|
+
"frontmatterKeys": ["title", "slug"],
|
|
1025
1004
|
"in": "---\ntitle: \"a\rb\"\nslug: c \"d\"\n---\n\nBody \"q\" here.\n",
|
|
1026
1005
|
"out": "---\ntitle: \"a\rb\"\nslug: c « d »\n---\n\nBody « q » here.\n",
|
|
1027
1006
|
"note": "modes.md §3.7.4 per-line U+000D bail in a locale whose quotation marks carry their own spacing, so the declined line and the converted one are visibly different constructions rather than the same English output relabelled (§8 item 4)."
|
|
@@ -1030,11 +1009,7 @@
|
|
|
1030
1009
|
"id": "fr-yaml-lone-cr-costs-one-line",
|
|
1031
1010
|
"rule": "quotes",
|
|
1032
1011
|
"mode": "yaml",
|
|
1033
|
-
"keys": [
|
|
1034
|
-
"title",
|
|
1035
|
-
"slug",
|
|
1036
|
-
"summary"
|
|
1037
|
-
],
|
|
1012
|
+
"keys": ["title", "slug", "summary"],
|
|
1038
1013
|
"in": "title: a \"b\"\nslug: c\rd \"e\"\n",
|
|
1039
1014
|
"out": "title: a « b »\nslug: c\rd \"e\"\n",
|
|
1040
1015
|
"note": "modes.md §3.8.4 step 9 in a locale whose quotation marks carry their own spacing: the line with the lone U+000D is declined and the line before it converts, so the cost is one line and not the document."
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.17.
|
|
2
|
+
"spec": "1.17.4",
|
|
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",
|
|
@@ -19,7 +19,7 @@ step 9 with §3.8.5's content-line condition, added in 1.10.0, §3.8.4's carriag
|
|
|
19
19
|
continuation line with the bails of §3.8.6 that use it, added in 1.11.0, and §3.8.4's node lines,
|
|
20
20
|
declined keys, node markers and own-line region, with the changes to steps 1, 3, 4, 5, 6, 7 and 8
|
|
21
21
|
that go with them, added in 1.11.0, and §3.9 with the amendments it carries to §3.1, §3.5 step 2,
|
|
22
|
-
§4, §5, §7 item 7 and §8, added in 1.13.0, and §3.4's delimiter-neighbour rule with §3.7.3's autolink-literal tail, terminator guard and reference labels, and the amendments they carry to §3.4's deletion paragraph, §5 items 2 and 3, its fifth-part note and testing obligation, and §6, added in 1.17.2).
|
|
22
|
+
§4, §5, §7 item 7 and §8, added in 1.13.0, and §3.4's delimiter-neighbour rule with §3.7.3's autolink-literal tail, terminator guard and reference labels, and the amendments they carry to §3.4's deletion paragraph, §5 items 2 and 3, its fifth-part note and testing obligation, and §6, added in 1.17.2, and §3.4's escape-and-reference rule with the amendments it carries to §3.6, §3.7.3, §5 item 2's fifth-part note, §5 item 3 and the testing obligation, added in 1.17.4).
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
@@ -412,6 +412,54 @@ the rule tests whitespace alone. A parser that implements an older definition (p
|
|
|
412
412
|
would classify them differently, and two fixtures (`en-us-markdown-commonmark-delimiter-neighbour-symbols-kept`)
|
|
413
413
|
make such a port fail rather than drift. No rule emits a member of `DELIM`.
|
|
414
414
|
|
|
415
|
+
**The escape-and-reference rule (`markdown` and `html`, spec 1.17.4).** The delimiter-neighbour rule
|
|
416
|
+
guards a change in how a parser _classifies_ a token. A second class changes the _text_ a parser
|
|
417
|
+
produces while the parse tree keeps its shape, which a comparison of shapes cannot see. A U+005C
|
|
418
|
+
that stands in a span as literal text has no escape in it, because the character after it is a
|
|
419
|
+
space; a `&` followed by letters and a space is no character reference (§3.6 keeps a `&` that
|
|
420
|
+
begins no well-formed reference inside its span). Deleting the space joins the two to what comes
|
|
421
|
+
next:
|
|
422
|
+
|
|
423
|
+
| Mode | Input | Locale | Output before 1.17.4 | What changed |
|
|
424
|
+
| ---------- | ------------ | ------- | -------------------- | ----------------------------------------------------------------- |
|
|
425
|
+
| `markdown` | `foo\ , bar` | `en-GB` | `foo\, bar` | the backslash now escapes the comma; it renders as `foo, bar` |
|
|
426
|
+
| `markdown` | `AT& ;T` | `en-GB` | `AT&T` | a character reference now exists; it renders as `AT&T` |
|
|
427
|
+
| `html` | `<p>a &hellip ;b</p>` | `en-GB` | `<p>a …b</p>` | a character reference now exists; it renders as `a …b` |
|
|
428
|
+
|
|
429
|
+
A backslash escape or a well-formed reference that the parser already read lies outside every span
|
|
430
|
+
(§3.6, §3.7.3), so only a **literal** U+005C or `&` inside a span can be affected, and no parity
|
|
431
|
+
of backslashes needs counting. The rule applies to every span of a document, those that come from
|
|
432
|
+
inline HTML or an HTML block in a Markdown document included. With `ASCIIPUNCT` = U+0021–U+002F, U+003A–U+0040, U+005B–U+0060 and
|
|
433
|
+
U+007B–U+007E, and `p`, `q`, `w`, `r`, `old` and `new` as in the delimiter-neighbour rule:
|
|
434
|
+
|
|
435
|
+
> **The edit is discarded if either of these holds, evaluated on the array the rule read. Item 1
|
|
436
|
+
> applies to `markdown` only, since a backslash is literal in HTML; item 2 applies to `markdown`
|
|
437
|
+
> and `html`.**
|
|
438
|
+
>
|
|
439
|
+
> 1. `cp[p−1]` is U+005C, `old = cp[p]`, `new = w[0]` when `r > 0` and otherwise `cp[q+1]`, and
|
|
440
|
+
> exactly one of `old` and `new` is in `ASCIIPUNCT`.
|
|
441
|
+
> 2. The edit is a deletion (`r = 0`) and, with `a = cp[p−1]` and `b = cp[q+1]` made adjacent, the
|
|
442
|
+
> array then holds a **reference shape** that contains both: U+0026, an optional U+0023, one or
|
|
443
|
+
> more ASCII letters or digits, and U+003B (`a` may be the U+0026 and `b` the U+0023). A shape is read as a left-to-right scan over those
|
|
444
|
+
> classes; it asks whether the _name_ is a known one no more than the delimiter-neighbour rule
|
|
445
|
+
> asks whether a flank is a valid one.
|
|
446
|
+
|
|
447
|
+
A marker belongs to no class, a position outside the array is none, and the test reads no other
|
|
448
|
+
code point and no parser state, so it is a function of the array alone and fires identically on
|
|
449
|
+
every run (§5 item 3). It applies to `markdown` and `html` and to no other mode: `text` has neither
|
|
450
|
+
escapes nor references, and `yaml` selects spans by a scan that reads none (§3.8).
|
|
451
|
+
|
|
452
|
+
**Its premise is that no rule emits an ASCII letter, a digit, U+0023, U+0026, U+003B or
|
|
453
|
+
U+005C** (`ellipsis` emits U+002E, which is none of these), so item 2 has nothing to read in a
|
|
454
|
+
replacement and tests deletions only, and, outside raw HTML, a literal U+005C is never followed by ASCII
|
|
455
|
+
punctuation in the input, since that pair would be an escape. Inside raw HTML a backslash is literal
|
|
456
|
+
and item 1 still applies to it: that is **accepted** conservatism (a quotation mark after a backslash
|
|
457
|
+
in an HTML block is left as typed), deterministic and identical in every runtime. `spaces` is the only rule that deletes. The rule is deliberately a test of _shape_, not a
|
|
458
|
+
lookup of the reference names an HTML5 parser knows, because a name table would be a data file
|
|
459
|
+
in five runtimes; it is therefore stricter than a parser: `AT&T ;x` (en-US) keeps its space,
|
|
460
|
+
though `&T;` is no reference. **That is the accepted cost**, together with a literal backslash
|
|
461
|
+
before a space and punctuation, which is rare in prose.
|
|
462
|
+
|
|
415
463
|
### 3.5 Applying the result
|
|
416
464
|
|
|
417
465
|
1. Extract the span sequence from the source. Record each span's **source offsets**.
|
|
@@ -444,7 +492,8 @@ Skipped, exhaustively and exactly:
|
|
|
444
492
|
unit. A text node containing one is split into spans around it, so the reference's spelling is
|
|
445
493
|
preserved exactly, which is the only way ` ` does not become a literal U+00A0 and back.
|
|
446
494
|
**A bare `&` that begins no well-formed reference stays inside its span.** No rule emits or
|
|
447
|
-
deletes `&`, so it is in no danger
|
|
495
|
+
deletes `&`, so it is in no danger itself (a space deleted beside it can complete a reference,
|
|
496
|
+
which §3.4's escape-and-reference rule, 1.17.4, closes); lifting it out would create a boundary that suppresses
|
|
448
497
|
conversions on both sides of it for nothing — `Tom & Jerry's "book"` would lose the pairing of
|
|
449
498
|
its quotation marks. Well-formedness, not the presence of an ampersand, is what makes a
|
|
450
499
|
skipped region;
|
|
@@ -547,6 +596,17 @@ Skipped, exhaustively:
|
|
|
547
596
|
- fenced code blocks, including the info string and the fences;
|
|
548
597
|
- indented code blocks — **`commonmark` only**; MDX has none;
|
|
549
598
|
- inline code spans, including the backticks;
|
|
599
|
+
- **every backslash escape and every character reference the parser recognises (spec 1.17.4)**, as
|
|
600
|
+
an opaque unit, exactly as `html` treats a well-formed reference (§3.6): the span is split
|
|
601
|
+
around it and the unit lies outside every span. A port that took spans from text-node positions
|
|
602
|
+
would put `\,` and `&` inside a span and disagree with the others about every deletion
|
|
603
|
+
beside them (`x & , y` keeps its space). **"Recognises" is CommonMark 0.31's definition
|
|
604
|
+
(§2.5, with GFM), and it differs from §3.6's:** `&` followed by a name from the HTML5 named
|
|
605
|
+
character reference list, **case-sensitively** (`&` and `Ă` are references; `&Amp;`
|
|
606
|
+
and `&foo;` are not), or by `#` and 1 to 7 decimal digits, or by `#x` or `#X` and 1 to 6
|
|
607
|
+
hexadecimal digits, then `;`. A value out of Unicode's range (`�`, `�`) is still a
|
|
608
|
+
reference. `html` mode tests the _shape_ of a reference (§3.6), not its name, so `&foo;` is
|
|
609
|
+
opaque there and a span's content here;
|
|
550
610
|
- autolinks `<https://…>` — **`commonmark` only**;
|
|
551
611
|
- **link and image destinations and titles** — the `(…)` of `[text](url "title")`, and the
|
|
552
612
|
definition line of a reference link. The **link text** is processable;
|
|
@@ -1769,7 +1829,8 @@ they do not carry it over for free either. Write `M` for the whole mode transfor
|
|
|
1769
1829
|
same reason. **This list is not claimed to be complete.** A deletion
|
|
1770
1830
|
can also create a backslash escape or a character reference from literal text (`foo\ ,`
|
|
1771
1831
|
becomes `foo\,` and `AT& ;T` becomes `AT&T`): the parse tree keeps its shape and its
|
|
1772
|
-
text changes, which a shape comparison does not see. That class
|
|
1832
|
+
text changes, which a shape comparison does not see. That class was not closed by 1.17.2 and
|
|
1833
|
+
is closed by §3.4's escape-and-reference rule (spec 1.17.4).
|
|
1773
1834
|
- **Deletion** is only ever of a U+0020, and structural whitespace borders a `BREAK` — a real
|
|
1774
1835
|
one in `text` mode, a −2 marker in the others — where `spaces` refuses to act
|
|
1775
1836
|
(`spaces.md` §3.2 step 4). A deletion therefore cannot turn `- item` into `-item`, collapse
|
|
@@ -1841,8 +1902,9 @@ they do not carry it over for free either. Write `M` for the whole mode transfor
|
|
|
1841
1902
|
that would place code points at a span extremity which were not there before — an insertion at
|
|
1842
1903
|
a boundary, or a replacement longer than what it replaced — is discarded rather than assigned
|
|
1843
1904
|
to a side. The discard is a pure function of the array and its span table (since 1.17.2: of
|
|
1844
|
-
`(p, q, r, s₀, s₁)`, of the code points beside the edit for §3.4's delimiter-neighbour rule,
|
|
1845
|
-
|
|
1905
|
+
`(p, q, r, s₀, s₁)`, of the code points beside the edit for §3.4's delimiter-neighbour rule, of
|
|
1906
|
+
the same code points and the run of letters and digits around the join for §3.4's
|
|
1907
|
+
escape-and-reference rule (since 1.17.4), and of whether the span begins at the end of a web address for §3.7.3's terminator guard), so it
|
|
1846
1908
|
happens identically on every run: a rule that was declined at an edge once is declined there always, and the output
|
|
1847
1909
|
contains nothing for the next run to reconsider.
|
|
1848
1910
|
|
|
@@ -1879,7 +1941,10 @@ quotes are curly, because each reaches a different rule. The tree to compare is
|
|
|
1879
1941
|
runtime has another parser, that parser's as well), not only the tokenizer's own: the tokenizer reads
|
|
1880
1942
|
no link in `"www.example.com"`, mdast does, and that is the class §3.7.3's scan closes. Templates
|
|
1881
1943
|
must include an address with `_`, `*` and `~` inside it after `"`, `'` and `(`, and an address
|
|
1882
|
-
directly after a digit. **
|
|
1944
|
+
directly after a digit. **Since 1.17.4 the comparison also covers the text values of the tree**, not only its shape: the
|
|
1945
|
+
defect behind the escape-and-reference rule left the shape unchanged. The alphabet must then include
|
|
1946
|
+
U+005C, `&`, `#`, `;`, `,` and a letter, and the templates a literal backslash and a literal
|
|
1947
|
+
`&name` each followed by a space and by each punctuation mark. **Two classes are carved out because they are accepted, not because
|
|
1883
1948
|
they are untested**: a text that matched no definition in the input and matches one in the output
|
|
1884
1949
|
(§3.7.3's accepted case), and an e-mail address, whose end only the parser decides.
|
|
1885
1950
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"spec": "1.17.
|
|
2
|
+
"spec": "1.17.4",
|
|
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
|
{
|
data/lib/polytypo/modes/html.rb
CHANGED
|
@@ -45,7 +45,7 @@ module Polytypo
|
|
|
45
45
|
# breaks (no prose content). Frontmatter is NOT in this list and is not a node type here:
|
|
46
46
|
# comrak's front_matter_delimiter extension is never enabled, because since spec 1.8.0 the
|
|
47
47
|
# block's extent is modes.md 3.7.3a's scan and not a parser's opinion of it.
|
|
48
|
-
SKIP_WHOLE_TYPES = %i[code code_block thematic_break].freeze
|
|
48
|
+
SKIP_WHOLE_TYPES = %i[code code_block thematic_break escaped].freeze
|
|
49
49
|
|
|
50
50
|
# A leaf whose Segment/source_position directly delineates literal, processable prose --
|
|
51
51
|
# comrak's AST is segment-based like goldmark's: structural bytes (emphasis delimiters,
|
|
@@ -287,7 +287,10 @@ module Polytypo
|
|
|
287
287
|
parser_source = normalize_lone_cr(mask_frontmatter(source, block))
|
|
288
288
|
|
|
289
289
|
spans = []
|
|
290
|
-
state = {
|
|
290
|
+
state = {
|
|
291
|
+
stack: [], frontmatter_end: block.nil? ? 0 : block.end, labels: [], references: [],
|
|
292
|
+
url_ends: Set.new
|
|
293
|
+
}
|
|
291
294
|
ParseError.wrap do
|
|
292
295
|
doc = Commonmarker.parse(
|
|
293
296
|
parser_source,
|
|
@@ -295,7 +298,8 @@ module Polytypo
|
|
|
295
298
|
)
|
|
296
299
|
walk(doc, chars, line_starts, spans, state)
|
|
297
300
|
end
|
|
298
|
-
|
|
301
|
+
kept = remove_ranges(spans, state[:labels] + state[:references])
|
|
302
|
+
trim_url_tails(chars, Spans.normalize_spans(kept), state[:url_ends])
|
|
299
303
|
end
|
|
300
304
|
|
|
301
305
|
URL_PREFIXES = ["www.", "http://", "https://"].freeze
|
|
@@ -352,21 +356,41 @@ module Polytypo
|
|
|
352
356
|
def self.remove_ranges(spans, ranges)
|
|
353
357
|
return spans if ranges.empty?
|
|
354
358
|
|
|
359
|
+
merged = merge_ranges(ranges)
|
|
360
|
+
starts = merged.map(&:first)
|
|
355
361
|
spans.flat_map do |span|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
# The first merged range that ends after the span starts is the one before the first
|
|
363
|
+
# start beyond it, or that one's successor; later ranges are visited in order.
|
|
364
|
+
index = [(starts.bsearch_index { |s| s > span.start } || starts.length) - 1, 0].max
|
|
365
|
+
cursor = span.start
|
|
366
|
+
pieces = []
|
|
367
|
+
while index < merged.length && merged[index].first < span.end
|
|
368
|
+
from, to = merged[index]
|
|
369
|
+
if to > cursor
|
|
370
|
+
pieces << Spans::Span.new(cursor, from) if from > cursor
|
|
371
|
+
cursor = to
|
|
364
372
|
end
|
|
373
|
+
index += 1
|
|
365
374
|
end
|
|
375
|
+
next [span] if cursor == span.start && pieces.empty?
|
|
376
|
+
|
|
377
|
+
pieces << Spans::Span.new(cursor, span.end) if cursor < span.end
|
|
378
|
+
pieces
|
|
366
379
|
end
|
|
367
380
|
end
|
|
368
381
|
private_class_method :remove_ranges
|
|
369
382
|
|
|
383
|
+
def self.merge_ranges(ranges)
|
|
384
|
+
ranges.map { |r| [r.start, r.end] }.sort.each_with_object([]) do |(from, to), out|
|
|
385
|
+
if out.any? && from <= out[-1][1]
|
|
386
|
+
out[-1][1] = to if to > out[-1][1]
|
|
387
|
+
else
|
|
388
|
+
out << [from, to]
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
private_class_method :merge_ranges
|
|
393
|
+
|
|
370
394
|
# modes.md 3.7.3 (spec 1.17.2): the text of a shortcut or collapsed reference is its label.
|
|
371
395
|
# A node exists only when the parser formed it from a definition, so the kind is read off the
|
|
372
396
|
# source after the closing bracket of the text: nothing (shortcut), "[]" (collapsed); a
|
|
@@ -426,6 +450,34 @@ module Polytypo
|
|
|
426
450
|
end
|
|
427
451
|
private_class_method :emit_span
|
|
428
452
|
|
|
453
|
+
# modes.md 3.7.3 (spec 1.17.4): a character reference the parser recognises is an opaque unit
|
|
454
|
+
# outside every span, as in html (3.6). comrak folds a reference into its text node, so the
|
|
455
|
+
# candidates are found by shape in the node's source and each is kept only when the parser
|
|
456
|
+
# does recognise it: a numeric one within CommonMark's digit limits, a named one when parsing
|
|
457
|
+
# it alone yields something other than its own spelling.
|
|
458
|
+
def self.note_references(chars, start_offset, end_offset, state)
|
|
459
|
+
slice = chars[start_offset...end_offset]
|
|
460
|
+
Html.find_entity_refs(slice).each do |from, to|
|
|
461
|
+
text = slice[from...to].join
|
|
462
|
+
next unless recognised_reference?(text)
|
|
463
|
+
|
|
464
|
+
state[:references] << Spans::Span.new(start_offset + from, start_offset + to)
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
private_class_method :note_references
|
|
468
|
+
|
|
469
|
+
def self.recognised_reference?(text)
|
|
470
|
+
case text
|
|
471
|
+
when /\A&#[xX](\h{1,6});\z/o, /\A&#(\d{1,7});\z/o
|
|
472
|
+
true
|
|
473
|
+
when /\A&#/
|
|
474
|
+
false
|
|
475
|
+
else
|
|
476
|
+
Commonmarker.parse(text).first_child.first_child.string_content != text
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
private_class_method :recognised_reference?
|
|
480
|
+
|
|
429
481
|
def self.walk(node, chars, line_starts, spans, state)
|
|
430
482
|
type = node.type
|
|
431
483
|
return if SKIP_WHOLE_TYPES.include?(type)
|
|
@@ -466,7 +518,10 @@ module Polytypo
|
|
|
466
518
|
sp = node.source_position
|
|
467
519
|
start_offset = offset_of(line_starts, sp[:start_line], sp[:start_column])
|
|
468
520
|
end_offset = offset_of(line_starts, sp[:end_line], sp[:end_column]) + 1
|
|
469
|
-
|
|
521
|
+
if state[:stack].empty?
|
|
522
|
+
emit_span(spans, start_offset, end_offset, state)
|
|
523
|
+
note_references(chars, start_offset, end_offset, state)
|
|
524
|
+
end
|
|
470
525
|
return
|
|
471
526
|
end
|
|
472
527
|
|
|
@@ -15,13 +15,12 @@ module Polytypo
|
|
|
15
15
|
# modes.md 3.4 interposed.
|
|
16
16
|
def self.run_rules_over_spans(cp, plan, locale_data, ctx, guarded = [])
|
|
17
17
|
current = cp
|
|
18
|
-
markdown = ctx.mode == "markdown"
|
|
19
18
|
plan.each do |rule_id|
|
|
20
19
|
fn = Engine::Registry.rule(rule_id)
|
|
21
20
|
edits = fn.call(current, locale_data, ctx)
|
|
22
21
|
filtered = Spans.filter_boundary_edits(
|
|
23
22
|
current, edits, Spans.span_ranges_of(current),
|
|
24
|
-
|
|
23
|
+
mode: ctx.mode, guarded: guarded
|
|
25
24
|
)
|
|
26
25
|
next if filtered.empty?
|
|
27
26
|
|
|
@@ -104,7 +103,7 @@ module Polytypo
|
|
|
104
103
|
source_cp.length
|
|
105
104
|
) do |current, edits|
|
|
106
105
|
Spans.filter_boundary_edits(current, edits, Spans.span_ranges_of(current),
|
|
107
|
-
|
|
106
|
+
mode: ctx.mode, guarded: guarded)
|
|
108
107
|
end
|
|
109
108
|
end
|
|
110
109
|
end
|