flux-md 0.5.0 → 0.5.5

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,179 @@ Notable changes to flux-md. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/); this project aims to follow
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## 0.5.5 — 2026-05-28
8
+
9
+ ### Performance
10
+
11
+ - 1× memcpy in the paragraph / container cache assembly (was 2×). Both caches
12
+ were building the block HTML in two stages — concatenate
13
+ `committed + active` into an intermediate `String`, then concatenate
14
+ `<p>` + that into the output — so a long open paragraph or container did two
15
+ memcpys of the committed inner per append. The fix builds directly into the
16
+ output buffer and trims trailing whitespace in-place; the container case
17
+ backs out a provisional `<p>` opener if the body content turns out to be
18
+ empty (preserving the empty-body fix from 0.5.4). Output is byte-identical.
19
+
20
+ 200 KB bench (best of 7), chunk=16:
21
+
22
+ | shape | 0.5.4 | 0.5.5 | speedup |
23
+ |-----------------|---------:|---------:|--------:|
24
+ | `long_paragraph`| 142 ms | **96 ms**| 1.48× |
25
+ | `emphasis_para` | 170 ms | **116 ms**| 1.47× |
26
+ | `big_blockquote`| 213 ms | **157 ms**| 1.36× |
27
+ | `big_alert` | 343 ms | **237 ms**| 1.45× |
28
+
29
+ Modest wins at every chunk size for the affected caches; the
30
+ table / list / fence caches are unchanged (they were already 1× memcpy).
31
+
32
+ ## 0.5.4 — 2026-05-28
33
+
34
+ ### Fixed (mid-stream rendering)
35
+
36
+ - **GFM tables now form during streaming, not just at finalize.** Streaming a
37
+ table char-by-char (or in any chunking where the delimiter row's `\n` lands
38
+ in a different chunk than the row's content) used to leave the block as a
39
+ `<p>` spanning both lines until `.finalize()` ran. The paragraph cache's
40
+ delimiter-detection walked from the line AFTER the cut and so missed a
41
+ delimiter row that completed inside the line the cut had advanced into. The
42
+ fix re-checks the line containing the cut whenever it has just completed,
43
+ guarded by a cheap `bytes[cut..].contains('\n')` so long open paragraphs
44
+ without interior `\n` still take the O(new bytes) per-call path.
45
+ - **Open alerts/blockquotes with an empty body no longer render an empty
46
+ `<p></p>`.** A `> [!NOTE]\n` shown mid-stream now matches the full renderer:
47
+ `<div class="markdown-alert ...">…<p class="...title">Note</p></div>` with
48
+ no empty body paragraph. The container cache was wrapping the body in
49
+ `<p>…</p>` unconditionally, even when the body was empty.
50
+
51
+ Both bugs only manifested *before* `finalize()`. The post-finalize output —
52
+ what every existing parity test checks — was already correct, which is why
53
+ neither was caught earlier. A new `tests/midstream_parity.rs` asserts that the
54
+ streamed view of an open block matches what one-shot parsing produces for the
55
+ same prefix (tables, alerts, blockquotes, lists, code fences, math fences).
56
+
57
+ ### Performance
58
+
59
+ - `big_table` at the artificial `chunk=16` stress case is ~280 ms (was ~145 ms
60
+ in 0.5.3). The 145 ms was the *incorrect* path: the paragraph cache treated
61
+ the whole 200 KB table as a single growing paragraph until finalize, never
62
+ engaging the table cache. The 280 ms is the cost of correctly emitting the
63
+ table mid-stream at the smallest chunk size. Every realistic LLM streaming
64
+ chunk size (≥64 bytes) is unchanged — `big_table` at chunk=64 is 73 ms,
65
+ chunk=256 is 38 ms, etc.
66
+
67
+ ## 0.5.3 — 2026-05-28
68
+
69
+ ### Performance
70
+
71
+ - **Streaming long open resumable containers is now O(n).** A long
72
+ `> [!NOTE]` alert, a `>`-quoted explanation, or a flat bullet/ordered list
73
+ used to re-run scan + inline render over the whole growing inner on every
74
+ append (O(n²)). Three new tail caches mirror the existing fence/table
75
+ pattern:
76
+
77
+ - `ContainerCache` — single-paragraph blockquote / GitHub alert. Wraps
78
+ the existing paragraph-cache (inline-boundary commit) with a
79
+ `>`-stripped inner buffer; the wrapper HTML (`<blockquote>` /
80
+ alert `<div>`) is built once at arm time, each new `> ` line is
81
+ stripped once into the inner buffer, only the unsettled inline tail is
82
+ re-rendered. Bails on a blank `>`-line (paragraph break inside the
83
+ container), lazy continuation, or `\r`.
84
+
85
+ - `ListCache` — tight, flat list (the LLM-emit shape: one sibling marker
86
+ per line, no blanks, no continuation, no nesting). Opener
87
+ (`<ul>` / `<ol start=N>`) pre-rendered at arm time; each new sibling
88
+ line renders directly into the cache as a tight `<li>…</li>` (GFM
89
+ task-list `[ ] `/`[x] ` supported). Bails on the first blank line
90
+ (loose-list signal), non-marker line, over-edge marker (nested), or
91
+ foreign-family marker — the full path handles those.
92
+
93
+ Measured at 50 KB (best of 7), before → after:
94
+
95
+ | shape | chunk=16 | chunk=256 |
96
+ |-----------------|-------------------|-----------------|
97
+ | `big_blockquote`| 5164 → **22 ms** | 332 → **8.5 ms**|
98
+ | `big_list` | 6141 → **18 ms** | 391 → **7.4 ms**|
99
+ | `big_alert` | 6298 → **28 ms** | 404 → **11 ms** |
100
+
101
+ At 200 KB, `big_list` chunk=256 was extrapolating to ~6.2 s before the
102
+ cache; now **36 ms** (~170×). Every realistic streaming shape now has a
103
+ flat chunk-size curve.
104
+
105
+ Output is byte-identical. Parity gated by `tests/container_cache.rs`
106
+ (blockquote + all five alert kinds, dir_auto, CRLF, lazy continuation,
107
+ multi-paragraph fallback, 400-line stress) and `tests/list_cache.rs` (5
108
+ marker families, ordered with non-default start, dir_auto, CRLF, loose /
109
+ nested / multi-line fallback, 400-item stress).
110
+
111
+ ### Documentation
112
+
113
+ - Reworded the "future plugin slot" comments in `renderers/Math.tsx` and
114
+ `renderers/Mermaid.tsx`. The actual extension path is the
115
+ `components.MathBlock` / `components.Mermaid` overrides, which already
116
+ works end-to-end.
117
+
118
+ ### Known limitations
119
+
120
+ - The three new caches disarm when `gfmFootnotes` is on, mirroring
121
+ `TableCache` from 0.5.2: cell-level `[^x]` occurrence ids would diverge
122
+ across the cache vs. full-reparse boundary. Footnotes + a long container
123
+ / table stays on the full O(n²) path — rare combination, may be lifted
124
+ in a later release by tracking per-cache footnote-occ deltas.
125
+ - The blockquote/alert cache covers the *single-paragraph* inner case (the
126
+ realistic LLM shape). A long open container with a multi-block inner
127
+ (lists inside, fenced code inside, etc.) still routes through the full
128
+ path. The bench's `big_blockquote` / `big_alert` are single-paragraph
129
+ shapes — what these caches were built for.
130
+
131
+ ## 0.5.2 — 2026-05-28
132
+
133
+ ### Performance
134
+
135
+ - **Streaming a long GFM table is now O(n) at every chunk size.** Tables already
136
+ rendered visually incrementally (header at the delimiter row, rows append as
137
+ they arrive) — but `render_table` re-walked every row on every append, so the
138
+ total work was O(n²) once chunks exceeded ~30 bytes (a row). The fix is an
139
+ incremental `TableCache` that mirrors the existing code/math `FenceCache`:
140
+ `<thead>` is pre-rendered once, each newly-complete `<tr>` is folded into the
141
+ cached prefix, and only the trailing partial row is re-rendered each append.
142
+ Output is byte-identical; parity gated by `tests/table_cache.rs` (every chunk
143
+ size 1..=9 × char-by-char against one-shot, with alignments, inline markdown,
144
+ link refs, CRLF fallback, and a 400-row stress case).
145
+
146
+ Measured on a 200 KB table (best of 7 — chunk varies on each row):
147
+
148
+ | chunk | before | after | speedup |
149
+ |------:|---------:|------:|--------:|
150
+ | 16 | 143 ms | 145 ms | ~1× (was already fast) |
151
+ | 64 | 20807 ms | 78 ms | **267×** |
152
+ | 128 | 10414 ms | 54 ms | **193×** |
153
+ | 256 | 5373 ms | 40 ms | **134×** |
154
+ | 512 | 2608 ms | 34 ms | **77×** |
155
+ | 1024 | 1322 ms | 31 ms | **43×** |
156
+
157
+ The pre-fix bench printed only chunks 16 and 256, which hid the regression
158
+ (16 was fine, 256 was the cliff floor). The bench now sweeps 16/64/128/256/
159
+ 512/1024 so the next regression in this shape can't slip in unnoticed.
160
+
161
+ Footnotes are the one combination still on the full O(n²) path: the
162
+ cell-level `[^x]` occurrence counter would diverge across the
163
+ cache/full-reparse boundary, so the cache disarms when `gfmFootnotes` is on
164
+ (rare enough to defer to a later release).
165
+
166
+ ## 0.5.1 — 2026-05-27
167
+
168
+ ### Performance
169
+
170
+ - A document with a very large number of link-reference definitions is now O(n)
171
+ instead of O(n²). The committed reference table was cloned on every append
172
+ (O(refs) per chunk); it's now shared into each render via an `Rc` (O(1)) with a
173
+ two-level lookup (committed, then the uncommitted tail), and folded in place
174
+ via `Rc::make_mut` once the render's clone is dropped. A 235 KB
175
+ reference-definition stream at 16-byte chunks: **~1,395 ms → ~53 ms** (~26×).
176
+ This was believed to be the last remaining O(n²) streaming shape; in fact a
177
+ long open GFM table was still O(n²) (fixed in 0.5.2 — `big_table` at
178
+ chunk=256 went from ~5,400 ms to ~40 ms). Output is unchanged.
179
+
7
180
  ## 0.5.0 — 2026-05-27
8
181
 
9
182
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flux-md",
3
- "version": "0.5.0",
3
+ "version": "0.5.5",
4
4
  "description": "Zero-dep streaming markdown for the browser. Rust→WASM core, Web Worker per stream, incremental parse with speculative closure.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -1,9 +1,11 @@
1
1
  import { memo } from "react";
2
2
 
3
3
  /**
4
- * Math block — preformatted-text only. flux-md is zero-dep, so we don't
5
- * ship KaTeX/MathJax. If you want rendered math, drop in a renderer via
6
- * the (future) plugin slot and override this component.
4
+ * Default math block — emits the LaTeX inside a `<div class="math
5
+ * math-display">` (or `<span class="math math-inline">` for inline). flux-md
6
+ * stays zero-dep, so it does not ship KaTeX/MathJax: bring your own typesetter
7
+ * (run it over the rendered `.math` nodes once a block closes), or override
8
+ * this slot via `components.MathBlock` to render the LaTeX yourself.
7
9
  */
8
10
 
9
11
  interface Props {
@@ -1,9 +1,10 @@
1
1
  import { memo } from "react";
2
2
 
3
3
  /**
4
- * Mermaid block — preformatted-text only. flux-md is zero-dep, so we don't
5
- * ship the Mermaid runtime. The diagram source is shown in a code block;
6
- * plug in your own renderer at this slot if you want SVG output.
4
+ * Default mermaid block — renders the diagram source verbatim in a code-like
5
+ * container. flux-md stays zero-dep and does not ship the Mermaid runtime:
6
+ * override this slot via `components.Mermaid` to render to SVG with your own
7
+ * Mermaid build (typically `mermaid.run` over the closed-block source text).
7
8
  */
8
9
 
9
10
  interface Props {
Binary file