brookmd 0.22.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +1229 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1265 -0
  4. package/dist/block-props.d.ts +18 -0
  5. package/dist/block-props.js +75 -0
  6. package/dist/client.d.ts +370 -0
  7. package/dist/client.js +754 -0
  8. package/dist/decorate.d.ts +24 -0
  9. package/dist/decorate.js +71 -0
  10. package/dist/dom.d.ts +130 -0
  11. package/dist/dom.js +627 -0
  12. package/dist/element.d.ts +20 -0
  13. package/dist/element.js +288 -0
  14. package/dist/hi.d.ts +12 -0
  15. package/dist/hi.js +215 -0
  16. package/dist/html-to-react.d.ts +61 -0
  17. package/dist/html-to-react.js +338 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.js +18 -0
  20. package/dist/morph.d.ts +28 -0
  21. package/dist/morph.js +166 -0
  22. package/dist/react.d.ts +236 -0
  23. package/dist/react.js +539 -0
  24. package/dist/renderers/CodeBlock.d.ts +7 -0
  25. package/dist/renderers/CodeBlock.js +75 -0
  26. package/dist/renderers/Math.d.ts +14 -0
  27. package/dist/renderers/Math.js +15 -0
  28. package/dist/renderers/Mermaid.d.ts +13 -0
  29. package/dist/renderers/Mermaid.js +15 -0
  30. package/dist/server-react.d.ts +32 -0
  31. package/dist/server-react.js +48 -0
  32. package/dist/server.d.ts +31 -0
  33. package/dist/server.js +82 -0
  34. package/dist/solid.d.ts +104 -0
  35. package/dist/solid.js +54 -0
  36. package/dist/styles.css +188 -0
  37. package/dist/svelte.d.ts +80 -0
  38. package/dist/svelte.js +59 -0
  39. package/dist/types-core.d.ts +436 -0
  40. package/dist/types-core.js +0 -0
  41. package/dist/types-react.d.ts +13 -0
  42. package/dist/types-react.js +0 -0
  43. package/dist/types.d.ts +2 -0
  44. package/dist/types.js +2 -0
  45. package/dist/url-safety.d.ts +12 -0
  46. package/dist/url-safety.js +45 -0
  47. package/dist/vue.d.ts +94 -0
  48. package/dist/vue.js +79 -0
  49. package/dist/wasm/LICENSE +21 -0
  50. package/dist/wasm/README.md +71 -0
  51. package/dist/wasm/brook_md_core.d.ts +166 -0
  52. package/dist/wasm/brook_md_core.js +512 -0
  53. package/dist/wasm/brook_md_core_bg.wasm +0 -0
  54. package/dist/wasm/brook_md_core_bg.wasm.d.ts +26 -0
  55. package/dist/worker-core.d.ts +65 -0
  56. package/dist/worker-core.js +155 -0
  57. package/dist/worker.d.ts +1 -0
  58. package/dist/worker.js +49 -0
  59. package/package.json +87 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,1229 @@
1
+ # Changelog
2
+
3
+ Notable changes to brookmd (formerly `flux-md`). Format based on
4
+ [Keep a Changelog](https://keepachangelog.com/); this project aims to follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## 0.22.0 — 2026-07-20
8
+
9
+ ### Changed
10
+
11
+ - **The project has a new name: `flux-md` is now `brookmd`.** Same engine, same
12
+ wire contract, same APIs — only the name changed. The lineage continues from
13
+ 0.21.0 (this is 0.22.0), and the rendered HTML is byte-identical apart from the
14
+ one renamed marker below. Install `brookmd` and update your imports:
15
+
16
+ | Was (`flux-md`) | Now (`brookmd`) |
17
+ | --- | --- |
18
+ | npm package `flux-md` | npm package `brookmd` |
19
+ | `import { FluxMarkdown } from "flux-md"` | `import { BrookMarkdown } from "brookmd"` |
20
+ | `FluxClient` | `BrookClient` |
21
+ | `FluxPool` | `BrookPool` |
22
+ | `FluxMarkdown`, `FluxMarkdownStatic` | `BrookMarkdown`, `BrookMarkdownStatic` |
23
+ | `useFluxMarkdown`, `useFluxMarkdownString` | `useBrookMarkdown`, `useBrookMarkdownString` |
24
+ | `initFlux`, `initFluxSync` | `initBrook`, `initBrookSync` |
25
+ | `flux-md/server`, `flux-md/dom`, `flux-md/styles.css`, … | `brookmd/server`, `brookmd/dom`, `brookmd/styles.css`, … |
26
+ | `<flux-markdown>` custom element | `<brook-markdown>` custom element |
27
+ | `.flux-md` root class, `.flux-block*`, `flux-dark`/`flux-light` | `.brook-md`, `.brook-block*`, `brook-dark`/`brook-light` |
28
+ | `--flux-*` CSS variables | `--brook-*` CSS variables |
29
+ | `data-flux-pending` pending-link marker | `data-brook-pending` |
30
+
31
+ The Rust crate `flux-md-core` is likewise now `brookmd-core`; the wire contract
32
+ ([`WIRE.md`](../../crates/brookmd-core/WIRE.md)) moves to **v1.1.0** for the
33
+ single marker rename (envelope structure unchanged).
34
+
35
+ - The old `flux-md` npm package is **deprecated and frozen at 0.21.0**; all future
36
+ releases ship as `brookmd`.
37
+
38
+ ## 0.21.0 — 2026-07-20
39
+
40
+ ### Added
41
+
42
+ - **New subpath exports** — `flux-md/html-to-react`, `flux-md/block-props`,
43
+ and `flux-md/worker-core` expose the pure HTML tokenizer, block-prop
44
+ helpers, and the backend-agnostic worker state machine for custom
45
+ renderers. These are the building blocks the upcoming React Native
46
+ package consumes; no behavior change for existing imports.
47
+ - **Native bindings in the repository** (experimental, not yet published as
48
+ packages): Swift (SPM, iOS + macOS), Kotlin/Android, a plain C ABI crate,
49
+ and a Flutter/Dart scaffold — every boundary emits the same versioned wire
50
+ JSON (`WIRE.md` v1.0.0), pinned by golden byte-equality tests per language.
51
+
52
+ ### Performance
53
+
54
+ - **Streaming worst-case campaign: five previously quadratic shapes are now
55
+ linear**, with byte-identical output (streamed views equal one-shot renders
56
+ at every chunk boundary; verified by the perf gate, mid-stream parity
57
+ sweeps, and fuzzing):
58
+ - an open, unclosed `$…` inline-math span (a streaming formula before its
59
+ closer): 64 KB in 1 KB chunks 154 ms → 10 ms;
60
+ - a growing all-alphanumeric word with autolinks on: 103 ms → 5.5 ms;
61
+ - an unterminated raw tag with `unsafeHtml`: 78 ms → 2.6 ms;
62
+ - emphasis soups held open by the CommonMark mod-3 rule: 247 ms → 5.3 ms;
63
+ - ever-deepening blockquote nesting: depth-110 staircase 341 ms → 1.0 ms;
64
+ - bonus: blockquotes/alerts whose first line hasn't completed now engage
65
+ their incremental caches mid-line (1005 ms → 487 ms at 64 KB).
66
+
67
+ ## 0.20.1 – 0.20.3 — 2026-07-15 *(backfilled)*
68
+
69
+ - 0.20.1: streaming divergence swaps preserve the rendered view during
70
+ incremental re-merges (no collapse until final); `doFinalize` wire-loss
71
+ fix; fuzz-caught dangling-`[text](` mis-parse and reattach re-feed fixes.
72
+ - 0.20.3: an open raw `<a href="…` under `unsafeHtml`/sanitize no longer
73
+ flashes the URL while streaming; a changed block keeps its position's id
74
+ across a divergence swap, so stateful component overrides survive.
75
+
76
+ ## 0.20.0 — 2026-07-02
77
+
78
+ ### Added
79
+
80
+ - **Streaming links render cleanly from the first character.** An open link's
81
+ label used to show as raw bracketed text (`[Earnings Call](`) until the URL
82
+ started arriving, then snap into an anchor. The label now renders inside an
83
+ inert pending anchor from its first character — no brackets, no raw URL, and
84
+ the pending HTML is byte-stable through the whole URL so completion is a
85
+ single attribute swap (no DOM churn). Pending anchors carry
86
+ `data-flux-pending=""` (gone the moment `href` lands, never in final output)
87
+ so you can style them like settled links immediately; the optional theme
88
+ does this out of the box. Deliberate exclusions so common non-link brackets
89
+ never flash as links: footnote refs (`[^1]`), task checkboxes, alert
90
+ markers, and all-digit citation labels (`[1]`, `[12]`).
91
+ - **`gfmTagfilter`** — the GFM "Disallowed Raw HTML" extension, opt-in like
92
+ the other GFM options (`gfm-tagfilter` on the custom element,
93
+ `with_gfm_tagfilter` in the core). With raw HTML enabled, the nine
94
+ page-hijacking tags (`title`, `textarea`, `style`, `xmp`, `iframe`,
95
+ `noembed`, `noframes`, `script`, `plaintext`) get their leading `<`
96
+ escaped, matching GitHub's rendering. The GFM extension suite is now 24/24.
97
+
98
+ ### Performance
99
+
100
+ - **Fourteen classes of streaming O(n²) eliminated.** An adversarial
101
+ multi-agent audit probed 137 document shapes, confirmed 17 quadratic
102
+ root-cause groups at the work-counter level, and this release fixes 14 —
103
+ every one now streams in O(new bytes) and is pinned linear by the
104
+ deterministic complexity gate (now three counters: slow-path scans,
105
+ inline-render bytes, emitted bytes). Highlights at 512 KB streamed in
106
+ 256-byte chunks unless noted:
107
+ - **CRLF input** made every incremental cache bail — a plain list cost
108
+ 49.4 s; now line endings normalize at ingest and CRLF streams cost the
109
+ same as LF (210 ms).
110
+ - **Footnotes**: a no-blank run of definitions stalled commits and every
111
+ append recloned the footnote maps — a list with per-item refs cost 48 s
112
+ at 256 KB; now 200 ms.
113
+ - **Link-reference definitions inside blockquotes** armed no cache
114
+ (44 s → 134 ms); lazy continuation lines no longer disarm quote caches.
115
+ - **Open list items with multi-line bodies** re-rendered whole every
116
+ append (23 s → 0.3 s); legal interior blank lines no longer permanently
117
+ disarm the list/indented-code caches (5 s → 54 ms).
118
+ - **Tables**: the growing trailing row re-split and re-rendered every
119
+ append (23.8 s → 254 ms; a thousand-column row 25.4 s → 65 ms).
120
+ - **`blockData` mode** disabled the container cache outright (a 256 KB
121
+ alert cost 41.7 s → 226 ms, 185×) and rebuilt code/math/list/table data
122
+ channels from scratch per append (512 KB math fence 7.4 s → 132 ms).
123
+ - **Streaming component blocks** (`<Chart>` bodies), giant headings,
124
+ thematic breaks, and growing fence info strings had no incremental cache
125
+ at all (a 256 KB component body 48 s → 345 ms).
126
+ - **Inline engine**: emphasis edits now apply in one forward pass instead
127
+ of per-pair splicing, unpairable delimiters no longer rescan the whole
128
+ stack (the CommonMark mod-3 pathology was 54 s for a one-shot 256 KB
129
+ render — now 51 ms, and it hit server-side rendering too), unmatched `$`
130
+ with math enabled no longer rescans to end-of-input per candidate
131
+ (57.7 s → 33 ms), and space-free text (entities, long tokens) commits
132
+ incrementally instead of pinning the whole paragraph.
133
+ - HTML blocks whose appends landed exactly on line boundaries dropped
134
+ their cache every append (7.7 s → 59 ms).
135
+ - WASM binary: 197 KB (+28 KB), the cost of seven new incremental caches.
136
+
137
+ ### Fixed
138
+
139
+ - CRLF documents no longer leak raw `\r` bytes into rendered code blocks;
140
+ CRLF output is byte-identical to the LF equivalent (line endings are
141
+ equivalent per CommonMark).
142
+ - Two latent mid-stream parity divergences (speculative rendering at
143
+ whitespace-only tails; a link-reference definition on an alert's first
144
+ body line) now match the one-shot render exactly.
145
+ - A link awaiting its title (`[label](url "ti…`) no longer flashes literal
146
+ source mid-stream.
147
+
148
+ ## 0.19.0 — 2026-06-30
149
+
150
+ ### Added
151
+
152
+ - **`decorators` — wrap/replace matched inline text while streaming.** A
153
+ declarative matcher list (`{ match: RegExp | string, replace: (text, groups) =>
154
+ node, skipInside?: string[] }`) on `<FluxMarkdown>` (React) and the DOM mount
155
+ options, applied to inline **text nodes only** after parsing — so it never sees
156
+ link URLs, code, or markup (no avoidance rules to hand-roll), and it runs once
157
+ per committed block, staying linear over a stream. Wrapping matched figures
158
+ (e.g. `$2.5B`, `10-15%`) is a one-liner. Decorator output is a **trusted**
159
+ surface (like `components`); `safeUrl` is now exported and `wrapLink(text, {
160
+ href })` ships as the safe link path. The `decorators` prop must be
161
+ referentially stable (hoist/memoize) — a dev-mode warning fires if it isn't,
162
+ since an unstable prop would re-decorate every committed block each tick.
163
+ - **`urlTransform`** — rewrite `href`/`src`/`poster` URLs (image proxy, allowlist,
164
+ relative resolution). The output is re-sanitized through the same scheme filter,
165
+ so a transform can't introduce a dangerous URL.
166
+
167
+ ### Performance
168
+
169
+ - **Nested lists now stream in O(n) instead of O(n²).** A loose outer list with
170
+ indented sub-bullets — and any list whose items have multi-line or nested-block
171
+ bodies — used to make the incremental list cache bail to a full reparse on every
172
+ appended chunk (re-scanning the whole growing list). It now renders each item's
173
+ full body, nested sub-lists included, through the shared item renderer, so it
174
+ stays linear. Streamed and one-shot output are byte-identical. (WASM −0.3 KB.)
175
+
176
+ ## 0.18.5 — 2026-06-30
177
+
178
+ ### Performance
179
+
180
+ - **Blockquotes and GFM alerts with structured bodies now stream in O(n) instead
181
+ of O(n²).** When a `>` blockquote or `> [!NOTE]` alert contains a list, table,
182
+ nested quote, heading, or code block, the incremental container cache used to
183
+ bail to a full reparse on every appended chunk — re-scanning and re-rendering
184
+ the whole growing block, so a long quoted list or alert-with-list went
185
+ quadratic (a 256 KB body streamed in small chunks did ~250× the parse work of a
186
+ 16 KB one). It now renders the `>`-stripped inner through a recursive nested
187
+ parser, committing settled inner blocks and re-rendering only the open tail, so
188
+ the work is linear in document size. Streamed and one-shot output stay
189
+ byte-identical. (WASM +3.8 KB.)
190
+
191
+ ### Internal
192
+
193
+ - A deterministic complexity-scaling gate (`cargo test --features perf_counters
194
+ --test scaling`), a proptest chunk-independence parity suite, and a cargo-fuzz
195
+ parity target now run in CI to catch O(n²) streaming regressions and chunk-
196
+ boundary divergences before they ship. The container regression above was
197
+ surfaced by the new gate on its first run.
198
+
199
+ ## 0.18.4 — 2026-06-29
200
+
201
+ ### Fixed
202
+
203
+ - **Blockquote / alert inner content flattened mid-stream (same flicker class as
204
+ 0.18.3's nested lists).** The container (blockquote / GFM alert) cache rendered
205
+ ALL inner content as plain paragraph text while streaming, so a list, nested
206
+ blockquote, heading, setext heading, fenced or indented code, table, thematic
207
+ break, HTML block, ordered list (incl. `start ≠ 1`), or link-reference
208
+ definition inside a `>` block showed as escaped paragraph text until finalize,
209
+ then snapped into its real structure. The cache now bails to the full reparse
210
+ whenever an inner line is anything other than plain paragraph prose. Found by
211
+ fuzzing the streaming prefix-parity invariant (the streamed view must equal a
212
+ one-shot parse at **every** prefix) over ~15k construct interactions plus an
213
+ adversarial corpus; streamed output now matches one-shot at every prefix for
214
+ these shapes.
215
+
216
+ ### Internal
217
+
218
+ - Removed a dead struct field and an unnecessary `mut` left by recent changes
219
+ (clean build, no warnings).
220
+
221
+ ## 0.18.3 — 2026-06-29
222
+
223
+ ### Fixed
224
+
225
+ - **Nested bullets flattened mid-stream (a visible list reflow).** While
226
+ streaming a *loose* outer list (items separated by a blank line) whose items
227
+ contain indented nested sub-bullets, the incremental list fast path treated a
228
+ 2-space-indented sub-bullet marker as a top-level **sibling** (it accepted any
229
+ marker within `edge + 3` columns). So the moment the outer list's second item
230
+ began streaming, the first item's nested `<ul>` **collapsed into flat top-level
231
+ items**, then re-nested at finalize — a jarring "indentation disappears then
232
+ comes back" flicker. The sibling test now uses the first item's content column,
233
+ so a marker at or past it correctly nests (the cache bails to the full reparse,
234
+ which renders the nesting). Streamed output now matches a one-shot parse at
235
+ **every prefix**; the only remaining list change while streaming is the
236
+ inherent tight→loose spacing, which a non-streaming parser shows too.
237
+
238
+ ## 0.18.2 — 2026-06-29
239
+
240
+ ### Fixed
241
+
242
+ - **Streaming O(n²) cliff on a paragraph followed by a long link-reference /
243
+ footnote definition run** (e.g. reference-heavy LLM output: prose, then a
244
+ block of `[id]: url` definitions). The paragraph stayed speculative until
245
+ `finalize()` — a definition is not a renderable block, so the paragraph never
246
+ became "the last block" and `committed_offset` stalled, re-scanning the whole
247
+ growing definition run on every append. A 235 KB document streamed at a
248
+ 256-byte chunk took **~59 s**; it now takes **~20 ms**, and streaming is linear
249
+ in document size across all chunk sizes. A renderable block followed by a
250
+ definition run now commits (a definition only parses at a block boundary, so
251
+ the block is closed). Narrow behavior note, within the existing
252
+ forward-reference limitation: the single paragraph immediately before such a
253
+ run now commits before the later definitions, so a *forward* reference from it
254
+ renders literally instead of resolving at finalize — consistent with every
255
+ earlier paragraph, which already commits mid-stream.
256
+
257
+ ## 0.18.1 — 2026-06-29
258
+
259
+ Performance + size pass. No API or output changes — CommonMark 652/652 and
260
+ GFM 23/24 are byte-for-byte unchanged.
261
+
262
+ ### Changed
263
+
264
+ - **WASM binary −9.6 KB (175.1 KB → 165.5 KB, −5.4%).** Three levers, measured:
265
+ a compact stable merge sort replaces the standard library's general-purpose
266
+ stable sort (driftsort) at the two sort sites (−7.3 KB incl. simpler escape
267
+ codegen); `wasm-opt` switches from `-O3` to `-Oz` (−2.3 KB) — and since the
268
+ Rust codegen is already `opt-level=z`, `-Oz` is a Pareto win (equal-or-slightly
269
+ faster parse, never slower, in a Node WASM A/B).
270
+ - **Faster HTML escaping.** `escape_html` / `escape_attr` now scan bytes and copy
271
+ plain runs with one `push_str` (a memcpy) instead of decoding + re-encoding
272
+ every character. Output is byte-identical (only ASCII `< > & " '` are
273
+ rewritten). Measured **+9–23%** parse throughput on escape-heavy documents —
274
+ large fenced code, display math, and HTML/list-heavy content (the common
275
+ LLM-output shape); prose is unchanged.
276
+ - **Fewer allocations on the render path.** Paragraphs, headings, and list items
277
+ render their inline content directly into the output buffer and trim in place,
278
+ dropping one temporary `String` + copy per block (helps the SSR / one-shot
279
+ `renderToString` / `parseToBlocks` path).
280
+ - **One fewer React render per patch (default path).** `<FluxMarkdown>` fed a
281
+ changing value to `useDeferredValue` even when tail deferral was off (the
282
+ default), so React scheduled a throwaway low-priority catch-up render every
283
+ patch. It now feeds a stable value unless `deferTail` is set, so the default
284
+ path renders exactly once per patch.
285
+
286
+ ## 0.18.0 — 2026-06-29
287
+
288
+ ### Added
289
+
290
+ - **`flux-md/server/react` subpath.** Exports `FluxMarkdownStatic` (the hookless
291
+ RSC / SSR React component), moved here from `flux-md/server` so that the core
292
+ server entry stays React-free (see Changed).
293
+
294
+ ### Changed
295
+
296
+ - **`FluxMarkdownStatic` moved from `flux-md/server` to `flux-md/server/react`.**
297
+ `flux-md/server` (`initFlux` / `initFluxSync` / `isFluxReady` / `parseToBlocks`
298
+ / `renderToString`) is now genuinely **React-free**: it imports no framework, so
299
+ a non-React build step or a Vue/Svelte SSR app can
300
+ `import { renderToString } from "flux-md/server"` even when `react` is not
301
+ installed. (Previously the entry failed to load without `react`, because the
302
+ component pulled it in eagerly — contradicting the "zero React dependency"
303
+ promise.) Update RSC/SSR imports to
304
+ `import { FluxMarkdownStatic } from "flux-md/server/react"`.
305
+
306
+ ### Fixed
307
+
308
+ - **Streaming finalize divergence (correctness).** A document streamed
309
+ char-by-char could finalize to different HTML than the same bytes parsed in one
310
+ shot, when the still-growing final line transiently looked like a block start
311
+ (`#…`, `</p…`, a lone `*` / `-`) and then completed into a lazy continuation of
312
+ the previous block (`#hashtag`, `</pre>`, `*emph*`). The penultimate block was
313
+ committed too early and frozen, permanently splitting a block the one-shot parse
314
+ keeps whole. The streaming commit boundary now keeps the penultimate block
315
+ speculative across such a provisional final line.
316
+ - **Coalesced completion deferred a frame.** Under the React hooks' rAF
317
+ coalescing (default since 0.17.0), the terminal `finalize()` patch could be
318
+ delivered one frame late — its synchronous-flush signal was consumed by an
319
+ earlier in-flight append patch — briefly showing a finished code block without
320
+ its highlight / copy button. The terminal patch is now tagged `final` at the
321
+ worker, so the sync flush binds to it regardless of how many append patches
322
+ precede it.
323
+ - **`reset()` ghost blocks.** Swapping a streaming source mid-flight (e.g. a React
324
+ "regenerate") could leave stale blocks from the previous content in the store,
325
+ because an in-flight patch raced the `reset()`. A per-stream generation counter
326
+ now drops pre-reset patches before they reach the cleared store.
327
+ - **Worker-pool robustness.** A fatally-failed worker (WASM-init failure, or a
328
+ trap that poisoned the shared instance) is now terminated and removed from the
329
+ pool — previously it lingered and could bypass the pool cap, leaking a worker
330
+ per stream. A WASM trap is escalated to a fatal worker error (the stream then
331
+ recovers onto a fresh worker) instead of being mishandled as a recoverable
332
+ per-stream error, and `free()` on a poisoned instance is guarded so teardown
333
+ can't throw out of the message loop.
334
+
335
+ ### Security
336
+
337
+ - **O(n²) entity-decode DoS.** The numeric character-reference scan (`&#…`) was
338
+ unbounded; input like `&#&#&#…` (no terminator) re-scanned to end-of-input on
339
+ every `&`, freezing the single-threaded parser for seconds on a few hundred KB.
340
+ The scan is now bounded to the longest valid reference (7 decimal / 6 hex
341
+ digits), matching the already-bounded named-entity branch.
342
+ - **Incomplete `data:` link blocklist.** Script-capable `data:` media types
343
+ (`image/svg+xml`, `application/xhtml+xml`, `text/xml`, `application/xml`,
344
+ `application/javascript`, …) could render as a live link / autolink /
345
+ component-attribute `href` — a browser navigating to one runs its script. They
346
+ are now blocked on the href path. Inert `data:image/…` raster images via
347
+ `![]()` are unaffected (an `<img>`-loaded SVG cannot run script).
348
+
349
+ ## 0.17.0 — 2026-06-27
350
+
351
+ ### Added
352
+
353
+ - **Compiled `dist/`.** The package now ships compiled, non-minified ESM
354
+ (`dist/*.js` + `.d.ts`) instead of raw TypeScript source — fixing consumers that
355
+ don't transpile `node_modules` (e.g. Next.js no longer needs
356
+ `transpilePackages`) and the Socket "unusual packaging" signal. The worker and
357
+ WASM remain separate assets so a consumer bundler still re-emits the worker
358
+ chunk and fetches the `.wasm`.
359
+
360
+ ### Changed
361
+
362
+ - WASM shadow stack reduced from 1 MB to 256 KB, cutting the WASM initial-memory
363
+ floor from ~1088 KB to ~320 KB (memory stays growable for large documents).
364
+ - Worker→main wire format is now a JSON string (a string structured-clones far
365
+ cheaper than an object graph); dropped `serde-wasm-bindgen` (smaller binary).
366
+ - React `useFluxStream` / `useFluxMarkdownString` default to rAF coalescing (one
367
+ render per frame), matching the framework-neutral DOM adapter.
368
+
369
+ ### Fixed
370
+
371
+ - Bounded three recursive descents in the parser (block render, link-reference
372
+ sweep, inline-component tags) at depth 100. With the smaller shadow stack an
373
+ unbounded descent on deeply nested input could trap and poison the worker;
374
+ beyond the cap, content is preserved as escaped text.
375
+
376
+ ## 0.16.2 — 2026-06-26
377
+
378
+ ### Fixed
379
+
380
+ - **Retryable WASM init.** A transient failure fetching the `.wasm` asset (web
381
+ path) no longer poisons every subsequent `initFlux()` / `renderToString()` —
382
+ the cached rejected promise is dropped so the next call retries.
383
+ - **Defensive `blockData` guards.** A malformed/drifted keyed-list `items` field
384
+ or table `rows`/`aligns`/`headers` now falls back to the full-HTML render path
385
+ instead of crashing the streaming render. The start-only ordered-list
386
+ renumber path is unaffected.
387
+
388
+ ### Changed
389
+
390
+ - `<flux-markdown>` stream-failure logging now logs only the error *message*,
391
+ not the raw `src` URL or the full error object (avoids a console forwarder
392
+ shipping a tokenized URL / bulky error body to monitoring).
393
+ - Micro-perf: memoized the components normalization and hoisted `parseOpenTag`'s
394
+ single-char regexes to module scope on the React render path.
395
+
396
+ ## 0.16.1 — 2026-06-25
397
+
398
+ ### Fixed
399
+
400
+ - **Streaming flash for incomplete inline links, code, and math.** While an
401
+ inline construct is still streaming in (no closing delimiter yet), it no
402
+ longer flashes its raw markdown source. A half-typed link renders just its
403
+ label as an inert (non-navigable) `<a>` with the destination hidden until the
404
+ closing `)` lands (then only `href` is added — the element is reused, not
405
+ remounted); inline code shows `<code>…</code>` with the backtick hidden;
406
+ inline math (`$…$`, `\(…\)`, `\[…\]`) shows the rendered `<span class="math
407
+ …">` with the `$`/`\(` hidden. Previously these showed `[label](https://… `,
408
+ `` `code… ``, and `$x^2 +…` as raw text until the closer arrived.
409
+ Final output is unchanged, and an inline construct that never closes still
410
+ finalizes to literal text, byte-identical to a one-shot parse (pinned by
411
+ truncate-at-every-offset streaming-parity fuzz). Images, emphasis/strong, and
412
+ reference links intentionally still render literally while open.
413
+
414
+ ## 0.16.0 — 2026-06-25
415
+
416
+ ### Added
417
+
418
+ - **Keyed streaming renderers (opt-in via `blockData`).** Tables, lists, and
419
+ blockquote/alert containers now render keyed sub-blocks (`<tr>` / `<li>` /
420
+ inner blocks), so while a block streams only the growing tail row/item
421
+ re-renders instead of the whole block — committed rows keep their DOM
422
+ identity (scroll/selection survive). React + vanilla DOM. Backed by new
423
+ `ListData.items` and `ContainerData` block-data channels.
424
+ - **`onRenderMetrics` hook + render counters.** Opt-in per-block render-churn
425
+ probe; `getMetrics()` gains `renderCount` / `rebuildCount`. Zero cost when
426
+ unused.
427
+ - **Opt-in render/scheduling knobs (all default off):** `coalesce` (rAF patch
428
+ coalescing for the React/store path), `deferTail` (`useDeferredValue`),
429
+ `childMemo` (fine-grained `htmlToReact` reuse), `morphOpenBlocks` (in-place
430
+ DOM morph of open blocks), a DOM prefix-extension tail-append fast path, and
431
+ fine-grained tail-block signals for Solid/Vue/Svelte.
432
+
433
+ ### Performance
434
+
435
+ - **Footnotes no longer disable the streaming caches.** The paragraph, list,
436
+ table, and blockquote/alert caches now stay armed when `gfm_footnotes` is on,
437
+ via placeholder occurrence-id tokens resolved on commit — closing the O(n²)
438
+ tail re-scan for footnote-bearing streamed blocks. Output is byte-identical
439
+ to a one-shot render.
440
+ - **Huge unclosed blocks stream in O(new bytes).** New incremental caches for
441
+ open indented-code and raw-HTML blocks remove their O(n²) tail re-scan.
442
+ - Single-pass URL scheme probe and memoized keyed-table header sniffs trim two
443
+ hot paths.
444
+
445
+ ### Build & size
446
+
447
+ - The published tarball is ~32 KB gzip smaller. The WASM core is rebuilt with
448
+ `-Z build-std` + `panic=immediate-abort` (~219 → ~178 KB), and `CHANGELOG.md`
449
+ + a stray wasm-pack `package.json` no longer ship. **Note:** building the
450
+ WASM now requires the nightly Rust toolchain + `rust-src`; consumers are
451
+ unaffected (the prebuilt binary ships), and `build:wasm:stable` remains for
452
+ stable toolchains.
453
+
454
+ ### Security
455
+
456
+ - Footnote occurrence-id placeholder tokens can never leak into rendered HTML
457
+ (defensive guard + a debug assertion exercised by the streaming fuzz corpus).
458
+
459
+ ## 0.15.1 — 2026-06-22
460
+
461
+ ### Security
462
+
463
+ - **XSS — dangerous-scheme autolinks are neutralized.** A CommonMark URI autolink
464
+ (`<javascript:alert(1)>`, `<vbscript:…>`, `<file:…>`) previously emitted a live
465
+ `href`, because autolinks bypassed the scheme allowlist that regular links go
466
+ through. They now route through the same decode-stable dangerous-scheme filter:
467
+ the `href` becomes `#` while the visible link text is unchanged. `file:` is now
468
+ blocked everywhere (links, autolinks, URL attributes) — it has no legitimate use
469
+ in rendered untrusted markdown and is a local-resource / phishing vector in
470
+ privileged contexts (Electron, extensions, `file://` origins).
471
+ - **Component-tag / `htmlToReact` attribute hardening.** Sanitized attributes now
472
+ also drop React-meaningful names (`dangerouslySetInnerHTML`, `ref`, `key`,
473
+ `defaultValue`, `defaultChecked`, `suppressHydrationWarning`, …) so a hostile
474
+ attribute can't crash the render tree or smuggle in a prop. Attribute→prop
475
+ lookup maps are prototype-free (`Object.create(null)`), and only HTML / `data-`
476
+ / `aria-` attribute names are forwarded to React.
477
+
478
+ ### Fixed
479
+
480
+ - **ReDoS / quadratic blow-ups on untrusted input.**
481
+ - Highlighter (`hi.ts`): the JS/TS regex-literal and bash double-quoted-string
482
+ patterns could backtrack quadratically on crafted code blocks; both rewritten
483
+ to linear forms, plus a 50 KB per-block size guard.
484
+ - URL scheme check: the decode-to-fixpoint loop (Rust `is_dangerous_scheme` and
485
+ JS `safeUrl`) is capped at 8 passes — still catches multi-encoded
486
+ `javascript&amp;amp;#58;` payloads, no longer O(n²) on `&amp;`-spam.
487
+ - Inline parser: nested / unbalanced link-bracket scanning is bounded
488
+ (depth + length caps); GFM extended-autolink trailing-paren trimming is now
489
+ linear instead of recounting the span each iteration.
490
+
491
+ ### Changed
492
+
493
+ - **`flux-md/server` uses a literal `import("node:fs/promises")`** instead of a
494
+ variable specifier, resolving the `dynamicRequire` supply-chain signal. Behavior
495
+ is unchanged — still a Node-only, `file:`-guarded branch.
496
+ - Added a **`## Security`** / supply-chain-transparency section to the README and a
497
+ documented **`socket.yml`** covering the inherent `nativeCode` / `networkAccess`
498
+ / `filesystemAccess` signals (the WebAssembly core and the opt-in
499
+ `<flux-markdown src>` fetch).
500
+
501
+ ### Performance
502
+
503
+ - **No redundant re-renders / rebuilds on no-op updates.**
504
+ - `<flux-markdown>` ignores a `setAttribute` whose value didn't change (a host
505
+ framework re-applying identical attributes no longer tears down the self-owned
506
+ client and reparses the whole document), and the `components` / `sanitize`
507
+ property setters skip the remount when assigned the same identity.
508
+ - `FluxClient.reset()` no longer notifies subscribers when the store was already
509
+ empty — skips a wasted, output-identical render pass.
510
+ - Documented that `sanitize` (like `components`) should be memoized/hoisted in
511
+ React, so a fresh closure each render doesn't bust the per-block memo.
512
+ - Added render-count / node-reuse / no-remount regression tests across the React,
513
+ DOM, store, custom-element, and Vue bindings, locking in that committed blocks
514
+ never re-render or rebuild as the stream grows (only the streaming tail does).
515
+
516
+ ### Known limitations
517
+
518
+ - Streaming a single very large **unclosed** block (a multi-megabyte indented code
519
+ block, open HTML block, or footnote-disarmed list delivered across many chunks)
520
+ is still O(n²) in the uncommitted-tail length. A bounded incremental cache for
521
+ these resumable containers is tracked as follow-up; finalized / closed blocks and
522
+ all other inputs are unaffected.
523
+
524
+ ## 0.15.0 — 2026-06-17
525
+
526
+ ### Added
527
+
528
+ - **Safe raw-HTML sanitizer (`htmlAllowlist` / `dropHtmlTags`)** — render a safe
529
+ subset of *inline* raw HTML (`<br>`, `<sub>`, `<sup>`, `<mark>`, …) **without**
530
+ `unsafeHtml`. Setting either list (even to `[]`) engages it: `htmlAllowlist`
531
+ non-empty renders only those tags (others escaped); **empty allows all tags
532
+ except a built-in, non-overridable dangerous set** (`script`, `style`,
533
+ `iframe`, `object`, `embed`, `form`, `svg`, `xmp`, `plaintext`, …);
534
+ `dropHtmlTags` removes tags entirely. Every rendered tag's attributes are
535
+ sanitized — `on*` handlers and `style` (a CSS beacon / clickjacking vector)
536
+ dropped, dangerous URL schemes (incl. multi-encoded) → `#`. Inline-scoped;
537
+ block-level raw HTML stays escaped. Matching is case-insensitive.
538
+
539
+ ### Fixed
540
+
541
+ - **HTML comments are dropped instead of escaped to visible text.** `<!--mk:id-->`
542
+ (a common LLM marker) previously rendered as a literal `&lt;!--…--&gt;` run or a
543
+ `<pre><code>` block; it now has no visible representation, in every mode except
544
+ bare `unsafeHtml` pass-through (which keeps it verbatim for CommonMark fidelity —
545
+ the browser ignores it either way). A comment-led block with trailing content
546
+ keeps that content (only comment-*only* blocks are dropped).
547
+
548
+ ### Security
549
+
550
+ - The dangerous-tag set is **non-overridable** (allowlisting `script`/`iframe`/`svg`
551
+ still drops them), `style` is stripped from every sanitized/component tag, and
552
+ raw-text elements (`xmp`/`plaintext`/`noembed`/`noframes`/`listing`) are blocked
553
+ in allow-all mode — closing CSS-exfiltration / clickjacking / DOM-corruption
554
+ vectors found in adversarial review. The React `htmlToReact` path mirrors the
555
+ `style` value-filter as defense-in-depth (safe declarations like `text-align`
556
+ still pass).
557
+
558
+ Feature-off output is byte-identical except HTML comments now drop (the
559
+ CommonMark/GFM suites run with `unsafeHtml` on, so the 652/GFM floors are
560
+ unaffected).
561
+
562
+ ## 0.14.0 — 2026-06-17
563
+
564
+ ### Added
565
+
566
+ - **Inline custom component tags (`inlineComponentTags`)** — the headline gap for
567
+ rich apps. An allowlisted inline tag like `<tik symbol="AAPL">AAPL</tik>` (or
568
+ self-closing `<tik/>`) **anywhere inline** — paragraphs, headings, list items,
569
+ and **table cells** — renders as a real custom element with its inner parsed as
570
+ **inline markdown** and its attributes sanitized (event handlers dropped,
571
+ dangerous URL schemes → `#`). The React renderer dispatches it to
572
+ `components[tag]` with the inner markdown as `children` and the attributes as
573
+ props — **XSS-safe without `unsafeHtml`**. Independent of `componentTags`
574
+ (block containers): list a tag under either or both. Use lowercase tag names.
575
+ - **`children` on `Component` block overrides** — a `Component` override now also
576
+ receives the inner content pre-parsed to a React tree (`children`), so you can
577
+ `return <Chip {...attrs}>{children}</Chip>` instead of
578
+ `dangerouslySetInnerHTML`-ing `html`. The html-vs-children contract is now loud
579
+ in the types and docs (an override that renders neither shows empty).
580
+ - **`flux-md/server` — worker-free synchronous SSR / RSC rendering.** The Rust→
581
+ WASM core is a plain synchronous parser, so finished markdown renders on the
582
+ server with no worker: `initFlux()` (async, idempotent — reads the co-located
583
+ `.wasm` in Node, or `initFluxSync(bytes)` on edge), `renderToString(md, {
584
+ config })` (sync HTML string, zero React dep), `parseToBlocks(md, { config })`,
585
+ and `<FluxMarkdownStatic content config components />` — a hookless, RSC-safe
586
+ React component that emits the same `flux-md` tree a client `<FluxMarkdown>`
587
+ hydrates, with the same overrides (inline/block component tags dispatch on the
588
+ server too).
589
+ - **`FluxParser.allBlocks()` (WASM)** — returns the whole parsed document as a
590
+ block array, the one-shot render primitive used by `flux-md/server`.
591
+
592
+ ### Fixed
593
+
594
+ - **Data-loss: a block component tag used inline swallowed sibling blocks.** With
595
+ e.g. `componentTags: ["tik"]`, an inline occurrence such as
596
+ `<tik>AAPL</tik> is up.` on a line with following content opened a block
597
+ container that consumed the rest of the document (the paragraph and a following
598
+ table vanished). A block component open tag must now be the **whole line** (only
599
+ trailing whitespace after `>`); otherwise it's treated as inline and degrades
600
+ inertly — it never eats surrounding content.
601
+
602
+ ### Changed
603
+
604
+ - The React HTML→tree converter (`htmlToReact` / `parseTrustedHtml`) now preserves
605
+ a tag's original **case** for component dispatch (so a capitalized inline tag
606
+ like `<Cite>` maps to `components.Cite`); HTML semantics (void elements, `input`,
607
+ close-tag matching) still compare case-insensitively, so standard output is
608
+ unchanged.
609
+
610
+ Feature-off output is byte-identical (CommonMark 652 + GFM floors hold); both
611
+ allowlists are empty by default.
612
+
613
+ ## 0.13.0 — 2026-06-04
614
+
615
+ ### Added
616
+
617
+ - **`FluxClient.setContent(content, { done })` + controlled-string helpers for
618
+ every binding** — a first-class bridge for UIs that hold a streaming message as
619
+ a single growing/controlled string prop (rather than a stream). setContent diffs
620
+ against the last value: a **prefix-extension** appends only the delta (committed
621
+ blocks stay put); any **divergence** (e.g. a finished message swapped for a
622
+ re-processed final string) resets and reparses. No hand-rolled diff, no
623
+ readiness gate. Pass `{ done: true }` / `streaming: false` to finalize. The
624
+ framework-neutral `setContent` is wrapped by an idiomatic, client-owning helper
625
+ per framework — React `useFluxMarkdownString`, Vue `useFluxMarkdownString`
626
+ (composable), Solid `createFluxMarkdownString`, Svelte `fluxMarkdownString`
627
+ (action) — each SSR-safe (feeds only in the client-only lifecycle hook). Vanilla
628
+ / `<flux-markdown>` use a caller-owned client + `setContent` directly.
629
+ - **`FluxPool.warm()`** — eagerly initialize one worker (`getDefaultPool().warm()`
630
+ on app load) so the one-time WASM init is off the first-token critical path; the
631
+ warm worker is the one the first stream attaches to, so the work isn't wasted.
632
+ - **Custom-component & `sanitize` overrides now apply to the OPEN (streaming)
633
+ block**, not just settled ones — a design-system renderer (Tailwind classes on
634
+ `p`/`ul`/`li`, inline `<a>`/`<code>` overrides) stays styled mid-stream instead
635
+ of only after a block commits. This also closes a gap where a supplied
636
+ `sanitize` previously bypassed component-rendered blocks; it now runs on every
637
+ block. The no-`components` path is unchanged (byte-identical `innerHTML`).
638
+
639
+ ### Fixed
640
+
641
+ - **Worker no longer drops the first chunk(s) under a slow WASM load.** The
642
+ worker buffered appends but did not gate parser creation on WASM readiness, so
643
+ an append that arrived before `init()` resolved would call `new FluxParser()`
644
+ against an uninitialized module — throwing `fluxparser_new of undefined` and
645
+ silently losing that chunk. Appends now accumulate (and `finalize` defers)
646
+ until init completes, then drain in order. Surfaced on a fresh Next.js /
647
+ Turbopack production load, where the worker+WASM fetch is slow enough to lose
648
+ the race; the fix is bundler-agnostic. The worker's message/readiness state
649
+ machine was extracted to `worker-core.ts` (dependency-injected, like
650
+ `FluxPool`'s worker factory) and now has a unit test (`worker-core.test.ts`)
651
+ covering the gate — buffer-until-ready, drain order, finalize/reset before
652
+ ready — so the regression can't silently return.
653
+ - **React 19 / Next.js type compatibility.** The shipped source used the global
654
+ `JSX.Element`, which React 19's `@types/react` removed — a consumer's
655
+ `next build` type-checks flux-md's source (it ships as `.tsx`) and failed with
656
+ *"Cannot find namespace 'JSX'"*. Now uses `ReactElement`, which type-checks
657
+ under `@types/react` 18 **and** 19.
658
+
659
+ ### Docs
660
+
661
+ - **Next.js (App Router) is now documented and verified** (Turbopack + webpack,
662
+ Next.js 16, `next dev` and `next build`): add flux-md to `transpilePackages`
663
+ and use it from a `"use client"` component. See the README's Next.js callout.
664
+
665
+ ## 0.12.0 — 2026-05-30
666
+
667
+ ### Added
668
+
669
+ - **Optional default theme — `import "flux-md/styles.css"`.** A drop-in stylesheet
670
+ for good-looking output out of the box, **including the built-in syntax
671
+ highlighter's colors** (without any CSS, `highlight()` output is uncolored).
672
+ Scoped to `.flux-md`, driven by `--flux-*` CSS variables (re-theme by overriding
673
+ a few), light by default with automatic dark via `prefers-color-scheme` (force
674
+ with `class="flux-md flux-dark"` / `flux-light`). Opt-in and zero-runtime — the
675
+ rendered HTML is unchanged; skip the import to bring your own CSS.
676
+
677
+ ## 0.11.0 — 2026-05-30
678
+
679
+ ### Added
680
+
681
+ - **Opt-in live region + root attributes** on `<FluxMarkdown>` and
682
+ `mountFluxMarkdown`. The root accepts `className` (appended to `flux-md`),
683
+ `id`, `role`, and `aria-live` / `aria-atomic`. Set `aria-live="polite"` to
684
+ announce streamed content to screen readers — `polite` coalesces rapid updates
685
+ and does **not** read every token. Off by default; covers React and the DOM
686
+ mount (so the Web Component and the Vue/Svelte/Solid adapters too).
687
+
688
+ ### Docs
689
+
690
+ - A repository root README, a "Structured block data" guide in the package
691
+ README, and a runnable **Data Studio** demo in the playground — a
692
+ sort/filter/CSV table and a live table of contents built entirely from
693
+ `block.data`, mid-stream.
694
+
695
+ ## 0.10.0 — 2026-05-30
696
+
697
+ Server-side rendering safety, plus an opt-in structured-data channel so consumers
698
+ build toolbars / tables of contents / charts from **data** instead of re-parsing
699
+ rendered HTML (no hast tree, no rehype).
700
+
701
+ ### Added
702
+
703
+ - **SSR-safe.** `new FluxClient()` and `renderToString(<FluxMarkdown …/>)` no
704
+ longer touch a Web Worker during construction or server render — worker
705
+ creation is deferred to the first `append`/`pipeFrom` (client-side) — so the
706
+ library imports and server-renders cleanly across React / Vue / Solid / Svelte.
707
+ A fresh-process SSR cold-import check guards it in CI.
708
+ - **Structured block data — `blockData: true`** (per-stream config; opt-in,
709
+ default off — output and CommonMark/GFM conformance are **byte-identical** when
710
+ off). When on, `block.kind.data` carries typed structured data per kind, also
711
+ surfaced as typed `BlockComponentProps` fields, and it **streams** in lock-step
712
+ with the HTML:
713
+ - **Table** → `{ headers, rows, aligns }`, cells `{ text, html }` (`props.table`)
714
+ — sort / filter / transpose / CSV / chart.
715
+ - **Heading** → `{ level, text, id }` (`props.heading`) — TOC with anchors.
716
+ - **CodeBlock** → `{ lang, code }` (`props.code`) — decoded source.
717
+ - **MathBlock** → `{ latex }` (`props.math`) — LaTeX source.
718
+ - **List** → `{ ordered, start }` (`props.list`).
719
+
720
+ ### Fixed
721
+
722
+ - Packaging: the published tarball ships the WASM deterministically on every npm
723
+ version (build removes wasm-pack's nested `.gitignore`), with a tarball tripwire
724
+ in CI and the publish workflow.
725
+
726
+ ## 0.9.0 — 2026-05-29
727
+
728
+ Kills the React streaming boilerplate. The common case — render an LLM stream —
729
+ goes from ~17 lines of hand-rolled lifecycle to one:
730
+
731
+ ```tsx
732
+ <FluxMarkdown stream={stream} />
733
+ ```
734
+
735
+ ### Added
736
+
737
+ - **`stream` prop on React `<FluxMarkdown>`** — pass an `AsyncIterable<string>`
738
+ (SSE deltas), a `Response`, or a `ReadableStream<Uint8Array>` and the
739
+ component owns an internal client, pipes the stream, supersedes it on change,
740
+ and destroys it on unmount. The `client` prop is unchanged (now optional);
741
+ passing a `client` keeps the existing caller-owned behavior.
742
+ - **`useFluxStream(stream, options?)` hook (React)** — same lifecycle, returns
743
+ the owned `FluxClient` (so you can read `outline()` / `getMetrics()` or pass it
744
+ to `<FluxMarkdown client={…} />`).
745
+ - **`pipeFrom` now also accepts an `AsyncIterable<string>`** and an optional
746
+ `{ signal }` — the signal is checked every iteration, so an aborted stream
747
+ appends no further chunks and is **not** finalized (and a byte reader is
748
+ `cancel()`'d). Existing `pipeFrom(Response | ReadableStream)` calls are
749
+ unchanged.
750
+
751
+ ### Notes
752
+
753
+ - A stream is single-use, so React StrictMode's dev-only double-mount may
754
+ truncate it in development; production mounts once and is unaffected (the
755
+ prior manual `useEffect` form had the same caveat).
756
+ - Rules of Hooks are respected — `<FluxMarkdown>` dispatches to one of two
757
+ sibling components, never a conditional hook.
758
+
759
+ ## 0.8.0 — 2026-05-29
760
+
761
+ A self-review of 0.7.0 (adversarial multi-agent pass) fixed two robustness gaps
762
+ in the worker pool and added two small, streaming-native conveniences.
763
+
764
+ ### Added
765
+
766
+ - **`FluxClient.pipeFrom(src)`** — hand it a `Response` or a
767
+ `ReadableStream<Uint8Array>` and it reads the body, `append()`s each decoded
768
+ chunk, and `finalize()`s. The LLM-native one-liner:
769
+ `await client.pipeFrom(await fetch("/api/chat"))`.
770
+ - **`onBlock` option** — `new FluxClient({ onBlock })` fires once per block as it
771
+ commits (document order), for side effects like lazily highlighting a finished
772
+ code block or analytics. Committed blocks never re-fire.
773
+
774
+ ### Fixed
775
+
776
+ - **Worker pool: a throwing stream handler no longer breaks sibling streams.** A
777
+ user `onError` (or any handler) that threw could abort the fatal-error fan-out
778
+ mid-loop and escape the worker message listener; dispatch is now isolated.
779
+ - **Worker pool: a fatally-failed worker is no longer re-assigned.** `pick()`
780
+ skipped the `failed` flag, so after a WASM-init failure a new stream could be
781
+ routed onto the dead worker and hang (a client that didn't `await whenReady()`
782
+ had no safety net). Failed workers are now excluded from selection.
783
+ - **`<flux-markdown>`: manual `append()`/`finalize()` supersede an in-flight
784
+ `src` fetch** (mirroring `reset()`), so mixing the two can't interleave.
785
+ - Hardened the CI/publish tarball check (explicit failure if `npm pack` yields
786
+ no tarball) and documented the `htmlToText` core-HTML-only invariant.
787
+
788
+ ## 0.7.0 — 2026-05-29
789
+
790
+ DX, robustness, and accessibility round — the streaming core (perf, CommonMark
791
+ 652/652, GFM) was already comprehensive, so this release sharpens the surface
792
+ around it.
793
+
794
+ ### Added
795
+
796
+ - **`onError` on `FluxClient`** — `new FluxClient({ onError })` receives worker
797
+ and parse errors (previously only `console.error`'d). A **WASM-init failure**
798
+ now also surfaces: `whenReady()` **rejects** instead of hanging forever, and
799
+ `onError` fires with `{ fatal: true }`.
800
+ - **`a11y` parser option** (`ParserConfig.a11y` / `setA11y` / `<flux-markdown
801
+ a11y>`) — opt-in accessibility markup that intentionally deviates from strict
802
+ GFM byte-output: wraps a task-list checkbox + its text in a `<label>` (so the
803
+ box is programmatically associated for screen readers), and adds
804
+ `scope="col"` to table header cells. **Off by default** (conformance output
805
+ unchanged). Streaming output stays byte-identical to one-shot.
806
+ - **`FluxClient.outline()`** — a heading table-of-contents (level / text /
807
+ stable id) from the current snapshot, in document order; works mid-stream.
808
+ - **`FluxClient.toPlaintext()`** — the rendered document as plain text (tags
809
+ stripped, entities decoded, blocks blank-line separated) for search indexing
810
+ / summaries.
811
+
812
+ ### Fixed
813
+
814
+ - **`<flux-markdown>` `src` race** — rapidly changing `src` (or switching
815
+ between a `src` URL and inline `markdown`/`textContent`) could interleave two
816
+ fetch streams into one parser, corrupting the parse tree. The element now
817
+ supersedes any in-flight fetch (monotonic token + `AbortController`) at a
818
+ single chokepoint.
819
+
820
+ ### Docs / packaging
821
+
822
+ - README documents the one-line Vite `optimizeDeps.exclude` requirement.
823
+ - `"sideEffects": ["./src/worker.ts"]` so bundlers can drop unused framework
824
+ adapters from the export surface.
825
+ - CI now publishes via a tag-triggered workflow with `npm publish --provenance`,
826
+ and asserts every published tarball ships a non-empty WASM artifact.
827
+
828
+ ## 0.6.0 — 2026-05-28
829
+
830
+ ### Added — flux-md is no longer React-only
831
+
832
+ The core (`FluxClient` + the WASM worker) was always framework-neutral; only
833
+ the renderer was React-bound. This release adds five new entry points, each
834
+ **thin lifecycle glue** over one new framework-agnostic DOM renderer — none
835
+ re-implements the subscribe/diff loop, and none destroys your client (you own
836
+ the worker/stream).
837
+
838
+ - **`flux-md/dom`** — the foundation. `mountFluxMarkdown(client, container,
839
+ options?) → { destroy(), refresh() }` incrementally patches a DOM subtree
840
+ using the parser's stable block IDs: a committed block's node is never
841
+ recreated (so one-shot work like syntax highlighting and the copy-button
842
+ listener runs exactly once), only the streaming tail re-renders. Reuses the
843
+ in-house highlighter for deferred code, applies your `sanitize` hook to the
844
+ open/speculative tail, and batches patches per `requestAnimationFrame`.
845
+ Block-kind overrides via `components` (`(props) => HTMLElement | string`);
846
+ tag-level overrides remain React-only.
847
+ - **`flux-md/element`** — `defineFluxMarkdown(tag = "flux-markdown")` defines a
848
+ `<flux-markdown>` custom element. Light DOM (your markdown CSS applies),
849
+ SSR-safe (no auto-register), and usable three ways: a caller-owned `client`
850
+ property, a self-owned client driven by `append()`/`finalize()`, or zero-JS
851
+ via a `src` URL it fetch-streams / inline text / a `markdown` attribute.
852
+ Config flags map to tri-state attributes (`gfm-math`, `dir-auto`, …). Covers
853
+ **Angular** with `CUSTOM_ELEMENTS_SCHEMA` — no separate package.
854
+ - **`flux-md/vue`** — a `<FluxMarkdown>` component + `useFluxMarkdown`
855
+ composable (Vue 3, optional peer dep).
856
+ - **`flux-md/svelte`** — a `fluxMarkdown` action, `use:fluxMarkdown={{ client }}`
857
+ (Svelte 4 and 5, optional peer dep).
858
+ - **`flux-md/solid`** — a `<FluxMarkdown>` component (Solid, optional peer dep).
859
+ Newest binding: its mount/teardown glue is tested, but the JSX component shell
860
+ has only been exercised via a real `vite-plugin-solid` build, not in CI — the
861
+ `flux-md/dom` mount inside `onMount`/`onCleanup` is the fallback if your Solid
862
+ toolchain trips on it.
863
+
864
+ Purely additive — existing `flux-md` / `flux-md/react` / `flux-md/client` users
865
+ are unaffected (the React renderer and core are byte-identical; the only change
866
+ to existing code was a type-only import repoint so the neutral entry points
867
+ typecheck without React). `vue`, `svelte`, and `solid-js` join `react` as
868
+ optional peer dependencies — import only the binding you need. See the new
869
+ "Framework bindings" section in the README. 65 → 85 tests.
870
+
871
+ ## 0.5.6 — 2026-05-28
872
+
873
+ ### Performance
874
+
875
+ - **`ContainerCache` now handles multi-paragraph inner content.** A blockquote
876
+ or GitHub alert with blank `>` lines inside (`> [!NOTE]\n> Para one.\n>\n>
877
+ Para two.\n`) used to drop the cache and fall back to the O(n²) full path
878
+ the moment the first blank arrived. The cache now closes the current
879
+ paragraph on a blank `>` and starts a new one, preserving the
880
+ streaming-O(new bytes) shape across multi-paragraph inner content. Each
881
+ completed inner paragraph is pre-rendered into a growing
882
+ `committed_paras_html` string; the single-paragraph fast path (the bench's
883
+ `big_blockquote` / `big_alert`) is unchanged within noise.
884
+
885
+ - **`ListCache` now handles loose lists.** A flat list with blank lines
886
+ between siblings (`- one\n\n- two\n\n- three\n`) is a CommonMark "loose"
887
+ list — every item body gets wrapped in `<p>…</p>` — and the cache used to
888
+ bail on the first blank. The cache now flips to loose on the first
889
+ blank-then-marker sequence, re-renders prior cached items with `<p>`
890
+ wrappers from stored source spans (one-time O(items)), and continues the
891
+ streaming-O(new bytes) shape from there. Tight→loose is sticky.
892
+
893
+ 50 KB loose-list bench, before-fix → after-fix:
894
+
895
+ | chunk | before | after | speedup |
896
+ |------:|---------:|--------:|--------:|
897
+ | 16 | 5593 ms | 21 ms | ~272× |
898
+ | 256 | 355 ms | 7 ms | ~49× |
899
+
900
+ Tight `big_list` perf is unchanged within bench noise.
901
+
902
+ ### Added
903
+
904
+ - **React `CodeBlock` default renderer ships a copy-to-clipboard button.**
905
+ Closed code blocks now show an icon + "Copy" in their header (the existing
906
+ "streaming" pill takes that slot until close, so streaming code is never
907
+ copy-clickable mid-arrival). Click → copies the decoded source via
908
+ `navigator.clipboard.writeText` → swaps to a checkmark + "Copied" for
909
+ 1.5 s → reverts. Native `<button>` (keyboard-reachable), `aria-label`
910
+ toggles between "Copy code" and "Copied" with `aria-live="polite"`,
911
+ guards against `navigator.clipboard` being absent (SSR / insecure context)
912
+ and rejected `writeText` promises (permission denied) — both leave the
913
+ button silently usable. No new dependency.
914
+
915
+ ### Documentation
916
+
917
+ - README quickstart now uses `useState(() => new FluxClient())` + an
918
+ unmount-only destroy effect instead of `useMemo(() => new FluxClient(),
919
+ [])` + cleanup-on-stream-change (which destroyed the client when the
920
+ `stream` prop changed, leaking a freed parser on the next append).
921
+ - New "when to enable each flag" guide for `ParserConfig` with concrete
922
+ LLM-output triggers (`gfmMath` when `$…$` arrives, `componentTags` for
923
+ `<Thinking>` blocks, etc.) — so a reader picks flags without reading the
924
+ full reference further down.
925
+ - `Alert` block-kind override example added to the `components` docs.
926
+ - `sanitize` example mirrors the realistic memoize-at-module-scope pattern
927
+ from the live demo (a fresh arrow each render busts the per-block memo).
928
+ - New "Performance" section pointing to CHANGELOG / `examples/bench.rs` for
929
+ numbers (no numbers baked into the README — those rot).
930
+
931
+ ## 0.5.5 — 2026-05-28
932
+
933
+ ### Performance
934
+
935
+ - 1× memcpy in the paragraph / container cache assembly (was 2×). Both caches
936
+ were building the block HTML in two stages — concatenate
937
+ `committed + active` into an intermediate `String`, then concatenate
938
+ `<p>` + that into the output — so a long open paragraph or container did two
939
+ memcpys of the committed inner per append. The fix builds directly into the
940
+ output buffer and trims trailing whitespace in-place; the container case
941
+ backs out a provisional `<p>` opener if the body content turns out to be
942
+ empty (preserving the empty-body fix from 0.5.4). Output is byte-identical.
943
+
944
+ 200 KB bench (best of 7), chunk=16:
945
+
946
+ | shape | 0.5.4 | 0.5.5 | speedup |
947
+ |-----------------|---------:|---------:|--------:|
948
+ | `long_paragraph`| 142 ms | **96 ms**| 1.48× |
949
+ | `emphasis_para` | 170 ms | **116 ms**| 1.47× |
950
+ | `big_blockquote`| 213 ms | **157 ms**| 1.36× |
951
+ | `big_alert` | 343 ms | **237 ms**| 1.45× |
952
+
953
+ Modest wins at every chunk size for the affected caches; the
954
+ table / list / fence caches are unchanged (they were already 1× memcpy).
955
+
956
+ ## 0.5.4 — 2026-05-28
957
+
958
+ ### Fixed (mid-stream rendering)
959
+
960
+ - **GFM tables now form during streaming, not just at finalize.** Streaming a
961
+ table char-by-char (or in any chunking where the delimiter row's `\n` lands
962
+ in a different chunk than the row's content) used to leave the block as a
963
+ `<p>` spanning both lines until `.finalize()` ran. The paragraph cache's
964
+ delimiter-detection walked from the line AFTER the cut and so missed a
965
+ delimiter row that completed inside the line the cut had advanced into. The
966
+ fix re-checks the line containing the cut whenever it has just completed,
967
+ guarded by a cheap `bytes[cut..].contains('\n')` so long open paragraphs
968
+ without interior `\n` still take the O(new bytes) per-call path.
969
+ - **Open alerts/blockquotes with an empty body no longer render an empty
970
+ `<p></p>`.** A `> [!NOTE]\n` shown mid-stream now matches the full renderer:
971
+ `<div class="markdown-alert ...">…<p class="...title">Note</p></div>` with
972
+ no empty body paragraph. The container cache was wrapping the body in
973
+ `<p>…</p>` unconditionally, even when the body was empty.
974
+
975
+ Both bugs only manifested *before* `finalize()`. The post-finalize output —
976
+ what every existing parity test checks — was already correct, which is why
977
+ neither was caught earlier. A new `tests/midstream_parity.rs` asserts that the
978
+ streamed view of an open block matches what one-shot parsing produces for the
979
+ same prefix (tables, alerts, blockquotes, lists, code fences, math fences).
980
+
981
+ ### Performance
982
+
983
+ - `big_table` at the artificial `chunk=16` stress case is ~280 ms (was ~145 ms
984
+ in 0.5.3). The 145 ms was the *incorrect* path: the paragraph cache treated
985
+ the whole 200 KB table as a single growing paragraph until finalize, never
986
+ engaging the table cache. The 280 ms is the cost of correctly emitting the
987
+ table mid-stream at the smallest chunk size. Every realistic LLM streaming
988
+ chunk size (≥64 bytes) is unchanged — `big_table` at chunk=64 is 73 ms,
989
+ chunk=256 is 38 ms, etc.
990
+
991
+ ## 0.5.3 — 2026-05-28
992
+
993
+ ### Performance
994
+
995
+ - **Streaming long open resumable containers is now O(n).** A long
996
+ `> [!NOTE]` alert, a `>`-quoted explanation, or a flat bullet/ordered list
997
+ used to re-run scan + inline render over the whole growing inner on every
998
+ append (O(n²)). Three new tail caches mirror the existing fence/table
999
+ pattern:
1000
+
1001
+ - `ContainerCache` — single-paragraph blockquote / GitHub alert. Wraps
1002
+ the existing paragraph-cache (inline-boundary commit) with a
1003
+ `>`-stripped inner buffer; the wrapper HTML (`<blockquote>` /
1004
+ alert `<div>`) is built once at arm time, each new `> ` line is
1005
+ stripped once into the inner buffer, only the unsettled inline tail is
1006
+ re-rendered. Bails on a blank `>`-line (paragraph break inside the
1007
+ container), lazy continuation, or `\r`.
1008
+
1009
+ - `ListCache` — tight, flat list (the LLM-emit shape: one sibling marker
1010
+ per line, no blanks, no continuation, no nesting). Opener
1011
+ (`<ul>` / `<ol start=N>`) pre-rendered at arm time; each new sibling
1012
+ line renders directly into the cache as a tight `<li>…</li>` (GFM
1013
+ task-list `[ ] `/`[x] ` supported). Bails on the first blank line
1014
+ (loose-list signal), non-marker line, over-edge marker (nested), or
1015
+ foreign-family marker — the full path handles those.
1016
+
1017
+ Measured at 50 KB (best of 7), before → after:
1018
+
1019
+ | shape | chunk=16 | chunk=256 |
1020
+ |-----------------|-------------------|-----------------|
1021
+ | `big_blockquote`| 5164 → **22 ms** | 332 → **8.5 ms**|
1022
+ | `big_list` | 6141 → **18 ms** | 391 → **7.4 ms**|
1023
+ | `big_alert` | 6298 → **28 ms** | 404 → **11 ms** |
1024
+
1025
+ At 200 KB, `big_list` chunk=256 was extrapolating to ~6.2 s before the
1026
+ cache; now **36 ms** (~170×). Every realistic streaming shape now has a
1027
+ flat chunk-size curve.
1028
+
1029
+ Output is byte-identical. Parity gated by `tests/container_cache.rs`
1030
+ (blockquote + all five alert kinds, dir_auto, CRLF, lazy continuation,
1031
+ multi-paragraph fallback, 400-line stress) and `tests/list_cache.rs` (5
1032
+ marker families, ordered with non-default start, dir_auto, CRLF, loose /
1033
+ nested / multi-line fallback, 400-item stress).
1034
+
1035
+ ### Documentation
1036
+
1037
+ - Reworded the "future plugin slot" comments in `renderers/Math.tsx` and
1038
+ `renderers/Mermaid.tsx`. The actual extension path is the
1039
+ `components.MathBlock` / `components.Mermaid` overrides, which already
1040
+ works end-to-end.
1041
+
1042
+ ### Known limitations
1043
+
1044
+ - The three new caches disarm when `gfmFootnotes` is on, mirroring
1045
+ `TableCache` from 0.5.2: cell-level `[^x]` occurrence ids would diverge
1046
+ across the cache vs. full-reparse boundary. Footnotes + a long container
1047
+ / table stays on the full O(n²) path — rare combination, may be lifted
1048
+ in a later release by tracking per-cache footnote-occ deltas.
1049
+ - The blockquote/alert cache covers the *single-paragraph* inner case (the
1050
+ realistic LLM shape). A long open container with a multi-block inner
1051
+ (lists inside, fenced code inside, etc.) still routes through the full
1052
+ path. The bench's `big_blockquote` / `big_alert` are single-paragraph
1053
+ shapes — what these caches were built for.
1054
+
1055
+ ## 0.5.2 — 2026-05-28
1056
+
1057
+ ### Performance
1058
+
1059
+ - **Streaming a long GFM table is now O(n) at every chunk size.** Tables already
1060
+ rendered visually incrementally (header at the delimiter row, rows append as
1061
+ they arrive) — but `render_table` re-walked every row on every append, so the
1062
+ total work was O(n²) once chunks exceeded ~30 bytes (a row). The fix is an
1063
+ incremental `TableCache` that mirrors the existing code/math `FenceCache`:
1064
+ `<thead>` is pre-rendered once, each newly-complete `<tr>` is folded into the
1065
+ cached prefix, and only the trailing partial row is re-rendered each append.
1066
+ Output is byte-identical; parity gated by `tests/table_cache.rs` (every chunk
1067
+ size 1..=9 × char-by-char against one-shot, with alignments, inline markdown,
1068
+ link refs, CRLF fallback, and a 400-row stress case).
1069
+
1070
+ Measured on a 200 KB table (best of 7 — chunk varies on each row):
1071
+
1072
+ | chunk | before | after | speedup |
1073
+ |------:|---------:|------:|--------:|
1074
+ | 16 | 143 ms | 145 ms | ~1× (was already fast) |
1075
+ | 64 | 20807 ms | 78 ms | **267×** |
1076
+ | 128 | 10414 ms | 54 ms | **193×** |
1077
+ | 256 | 5373 ms | 40 ms | **134×** |
1078
+ | 512 | 2608 ms | 34 ms | **77×** |
1079
+ | 1024 | 1322 ms | 31 ms | **43×** |
1080
+
1081
+ The pre-fix bench printed only chunks 16 and 256, which hid the regression
1082
+ (16 was fine, 256 was the cliff floor). The bench now sweeps 16/64/128/256/
1083
+ 512/1024 so the next regression in this shape can't slip in unnoticed.
1084
+
1085
+ Footnotes are the one combination still on the full O(n²) path: the
1086
+ cell-level `[^x]` occurrence counter would diverge across the
1087
+ cache/full-reparse boundary, so the cache disarms when `gfmFootnotes` is on
1088
+ (rare enough to defer to a later release).
1089
+
1090
+ ## 0.5.1 — 2026-05-27
1091
+
1092
+ ### Performance
1093
+
1094
+ - A document with a very large number of link-reference definitions is now O(n)
1095
+ instead of O(n²). The committed reference table was cloned on every append
1096
+ (O(refs) per chunk); it's now shared into each render via an `Rc` (O(1)) with a
1097
+ two-level lookup (committed, then the uncommitted tail), and folded in place
1098
+ via `Rc::make_mut` once the render's clone is dropped. A 235 KB
1099
+ reference-definition stream at 16-byte chunks: **~1,395 ms → ~53 ms** (~26×).
1100
+ This was believed to be the last remaining O(n²) streaming shape; in fact a
1101
+ long open GFM table was still O(n²) (fixed in 0.5.2 — `big_table` at
1102
+ chunk=256 went from ~5,400 ms to ~40 ms). Output is unchanged.
1103
+
1104
+ ## 0.5.0 — 2026-05-27
1105
+
1106
+ ### Fixed
1107
+
1108
+ - **Streaming GFM tables now render incrementally.** A table no longer waits for
1109
+ the whole block to arrive: the header renders the moment the delimiter row
1110
+ (`|---|`) streams in, and each body row appends as it arrives. Previously the
1111
+ incremental paragraph fast-path kept extending the header line as a paragraph
1112
+ and only formed the table on a full reparse, so a streaming table appeared all
1113
+ at once. The fast-path now bails (like it does for a setext underline) when a
1114
+ delimiter row forms a table with its preceding header. Output is unchanged for
1115
+ one-shot parsing; streamed output now matches one-shot at every prefix.
1116
+
1117
+ ### Added
1118
+
1119
+ - **`<FluxMarkdown sanitize={fn} />`** — an optional HTML sanitizer hook. When
1120
+ provided, flux-md runs every block's HTML through it before injecting via
1121
+ `innerHTML`, **including the streaming (open/speculative) tail** that the raw
1122
+ fast path would otherwise expose. Bring your own sanitizer (e.g.
1123
+ `DOMPurify.sanitize`) to render untrusted / LLM HTML with `unsafeHtml` on;
1124
+ flux-md stays zero-dep. Built-in code/math renderers (already-escaped content)
1125
+ are not run through it, so highlighting and math markup are preserved. Omitting
1126
+ the prop is byte-identical and zero-cost.
1127
+
1128
+ ## 0.4.0 — 2026-05-27
1129
+
1130
+ ### Added
1131
+
1132
+ - **`componentTags`** — opt-in custom component tags. List tag names (e.g.
1133
+ `componentTags: ['Thinking', 'Callout']`) and a `<Thinking>…</Thinking>` in the
1134
+ stream renders as a component whose **inner content is parsed as markdown** —
1135
+ safely, **without `unsafeHtml`**: the tag is allowlisted and its attributes are
1136
+ sanitized (event handlers dropped, dangerous URL schemes neutralized). The
1137
+ container spans blank lines (unlike a raw HTML block) up to its matching close
1138
+ tag, supports nesting, and ignores a `</Tag>` inside a code fence. Each renders
1139
+ as a `Component` block dispatched on the React side via `components[tag]` (e.g.
1140
+ `components.Thinking`) or the generic `components.Component`, receiving `{ tag,
1141
+ attrs, … }`. Off unless configured; tag names match case-sensitively.
1142
+
1143
+ ### Performance
1144
+
1145
+ - Streaming a long open display-math block (`$$…$$` / `\[…\]`) is now O(n)
1146
+ instead of O(n²). The incremental fence cache that already covered code fences
1147
+ was generalized to math fences: an append only escapes the newly arrived lines
1148
+ instead of re-scanning and re-escaping the whole growing body. Measured on a
1149
+ 200 KB `$$…$$` block at 16-byte chunks: **16,271 ms → ~93 ms** (~174×). Output
1150
+ is byte-identical (gated by `tests/math_fence_cache.rs`).
1151
+ - A long trailing run of link-reference / footnote definitions now commits
1152
+ incrementally instead of being re-scanned on every append. Previously such a
1153
+ run produced no renderable blocks, so the committed offset never advanced. A
1154
+ document ending in a large reference section streams ~10× faster (235 KB at
1155
+ 16-byte chunks: **13,799 ms → ~1,380 ms**). Output is byte-identical (gated by
1156
+ `tests/ref_defs_streaming.rs`).
1157
+
1158
+ ## 0.3.2 — 2026-05-27
1159
+
1160
+ ### Documentation
1161
+
1162
+ - Rewrote the README to describe flux-md on its own terms and removed all
1163
+ references to and comparisons with other libraries. No code changes — the
1164
+ published API and behavior are identical to 0.3.1.
1165
+ - Fixed the React quick-start example: import `useEffect` and guard the async
1166
+ append loop so it can't run after unmount or a stream change.
1167
+
1168
+ ## 0.3.1 — 2026-05-27
1169
+
1170
+ ### Performance
1171
+
1172
+ - Streaming a long unbroken paragraph is now O(n) instead of O(n²) — including
1173
+ paragraphs **dense with inline constructs** (emphasis, code spans, links,
1174
+ inline math), not just plain text. The open paragraph commits its settled
1175
+ prefix and re-renders only the short active tail. Because inline output isn't
1176
+ prefix-stable (a late `*` re-emphasizes earlier text, a late backtick opens a
1177
+ code span), the stable boundary is computed inside the inline renderer itself:
1178
+ it tracks unmatched openers, unpaired forward-pairable emphasis, and resolved
1179
+ emphasis spans, and commits only up to the largest provably-final cut. Output
1180
+ is byte-identical. Measured on 200 KB single paragraphs at 16-byte chunks:
1181
+ plain **34,167 ms → ~130 ms** (~260×); emphasis-rich **60,569 ms → ~157 ms**
1182
+ (~386×).
1183
+ - The open-code-fence fast path no longer clones the accumulated escaped body on
1184
+ every append; it assembles the block HTML directly from the cached pieces,
1185
+ dropping one full O(body) copy per append. A 200 KB fence streams in **~82 ms**
1186
+ at 16-byte chunks (was ~154 ms, ~1.9×). Output is byte-identical.
1187
+
1188
+ ## 0.3.0
1189
+
1190
+ ### Added
1191
+
1192
+ - **`gfmMath`** — opt-in math. Inline `$…$` and `\(…\)`; display `$$…$$` and
1193
+ `\[…\]`. Inline `$` uses the pandoc rule, so currency like `$5 and $10` stays
1194
+ literal. Emits KaTeX-ready markup (`<span class="math math-inline">` /
1195
+ `<div class="math math-display">`) carrying the LaTeX as text content — bring
1196
+ your own KaTeX (flux-md stays zero-dep) or override `components.MathBlock`
1197
+ (which receives the LaTeX as `text`). Display fences are blank-line tolerant
1198
+ and stream incrementally. Off by default.
1199
+ - **`dirAuto`** — opt-in per-block `dir="auto"` on block-level text elements
1200
+ (`p`, `h1`–`h6`, `blockquote`, `ul`/`ol`/`li`, `table`, alerts, footnotes), so
1201
+ the browser detects each block's direction (RTL/LTR) independently in
1202
+ mixed-language documents. Code blocks stay LTR. Off by default.
1203
+
1204
+ ### Performance
1205
+
1206
+ - Streaming a long fenced code block is now **O(n) instead of O(n²)**: an open
1207
+ code fence caches its escaped body and extends it by only the newly arrived
1208
+ lines. Measured on a 200 KB fence — **14,278 ms → 230 ms** at 16-byte chunks,
1209
+ **898 ms → 22 ms** at 256-byte chunks. Output is byte-identical.
1210
+ - Dropped a redundant per-append clone of the link-reference table.
1211
+
1212
+ ### Known limitations
1213
+
1214
+ - Streaming a very long **unbroken** paragraph (no blank lines) is still O(n²):
1215
+ inline rendering re-runs over the whole paragraph each chunk, and unlike code
1216
+ it can't be prefix-cached (a late `*` can emphasize earlier text). Tracked for
1217
+ a future release; breaking the text into paragraphs avoids it.
1218
+
1219
+ ### Internal
1220
+
1221
+ - Added a Rust streaming-throughput benchmark (`cargo run --release --example
1222
+ bench`) plus char-by-char streaming-parity tests for the code-fence cache,
1223
+ math, and bidi paths.
1224
+
1225
+ ## 0.2.0
1226
+
1227
+ - Initial public release: zero-dep streaming markdown, Rust→WASM core, one Web
1228
+ Worker per stream, CommonMark 0.31 (652/652) + GFM (tables, strikethrough,
1229
+ task lists, extended autolinks, GitHub alerts, footnotes).