Repository navigation
v2.5.1
All fixes. The three items the 2.5.0 backlog had left open, each verified as a real defect before being touched — two of the seven candidates turned out not to be, and are recorded as such rather than fixed.
- A resumed smooth stream revealed broken grapheme clusters.
finish(),snap()and a finishedflush()confirm the source's end unconditionally, which is right at the time: a finished stream owes the caller every byte it has. But a round ends between tool calls and then continues, and an offset that was a source end need not be a cluster boundary of the longer string — whilehold, which exists to protect confirmed boundaries, never saw these ones. Three left-context rules broke there: a lone high surrogate completed by the next round ("👩👧👦x"resumed at 4 segmented as["\uDC67", "👦", "x"]and revealed the family in visibly broken pieces), a regional-indicator run resumed at an odd position (GB12/GB13 pair flags off from the START of the run, so"🇺🇸🇬🇧🇫🇷"resumed at 2 read 🇸 and 🇬 as one flag), and a leading ZWJ or combining mark. Rather than enumerate UAX #29's left-context rules, a resume now discards the scheduled backlog and re-segments fromvisibleEnd— the only settled offset — through a bounded lookback window; grapheme breaking is a finite-state process, so a window that starts mid-cluster re-synchronises within a cluster or two. Regional indicators are the one rule a window cannot recover, so the anchor walks their whole run. Steady-state streaming pays nothing. - One well-formed HTML table disabled freezing for the rest of the document. A stray table part (
<td>,<tr>,<col>…) outside a table re-routes how parse5 builds every later table, so 2.4.4 taught the scanner to poison its candidates from such a tag onwards. Neither that check nor the splice's asked whether the part was actually stray — so<table><tr><td>a</td></tr></table>took the boundary to 0 from the table onwards (43 for the same prose without it) and to zero incremental frames on every schedule, with no way back:phasePoisonedAtonly ever moves down. A<details>wrapping a table did it too. Both checks are now positional — the scanner againsttagBalance, the open-element model the straddle bail already trusts, read only to SUPPRESS a poison so a miscount leaves the old over-blocking behaviour in place; the splice carrying table depth across raw values, sincehast-util-rawfeeds them all to one parse5. Five table shapes now run 142–183 incremental frames each. - Resolving a cross-chunk footnote left every later footnote numbered wrong. The block cache's footnote rank models
state.footnoteOrder, whose position is what a coordinated render bakes into each mark aslocalNumber. Phantom labels never enter footnoteOrder, but the rank counted them: with[^A]supplied by another chunk and[^B]local, B's baked number is 1 while A is phantom and 2 once A's definition arrives — and B's rank, counting A both times, stayed 1. Same raw, same offset, same key, so the cache kept serving a superscript reading 1 under a footer numbering it 2. - perf: the swallowed-extent taint test is a binary search instead of a scan (it was O(taint nodes × raw-HTML blocks) on the streaming hot path — 14% of
buildBlockson a document with 200 definitions and 120 containers). - Examined and kept, with the reasoning now in the code so reviews stop re-filing them: the mermaid popup's same-origin blob URL (script that reaches the DOM already holds the page's origin — inline
on*handlers inserted viainnerHTMLare not inert, only<script>is, so the popup adds nothing a DOMPurify bypass would not already have; thenoopenercomment no longer claims to answer this); and==highlight==keeping CommonMark's unpatched flanking, where**and~~pair next to full-width punctuation and==does not —==is neither CommonMark nor GFM, and its value is rendering the same way in Obsidian and VitePress, which relaxing it here would break.docs/cjk-typography.mddocuments the limitation and three working shapes. - Also measured and NOT changed: merging the two walks over a raw-HTML container's subtree. Replacing one walk with a bare traversal costs the same as removing it outright (0.199 ms vs 0.203 ms), so the traversal is free and the cost is entirely per-node work — a merge saves nothing.
Verification: every fixed shape is a deterministic pin, red on 2.5.0; release soak (splice fuzz 50k / direction battery 20k / K=4 census) clean on the final engine; full preflight at 1414 tests.