flux-md 0.16.2 → 0.18.0

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