brookmd 0.29.0 → 0.30.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,175 @@ Notable changes to brookmd (formerly `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.30.0 — 2026-09-07
8
+
9
+ **Agent-ready, chat-ready.** brookmd now ships as an installable agent skill,
10
+ and the surface a chat UI needs on day one — a styled theme for the code
11
+ chrome, a streaming caret, a link-click hook, a highlighter you can teach new
12
+ languages, and copy-pasteable recipes for the AI SDK, KaTeX, Mermaid, and
13
+ table toolbars — is in the package and the docs instead of the demo app.
14
+ Requires `brookmd-core` 0.27.0 (parser-side performance fix below; the wire
15
+ contract is unchanged at v1.2.0).
16
+
17
+ ### Added
18
+
19
+ - **Agent skill.** `npx skills add siinghd/brookmd` installs a `brookmd` skill
20
+ into any coding agent that reads the `SKILL.md` format (Claude Code, Cursor,
21
+ Codex, …); `/plugin marketplace add siinghd/brookmd` does the same through
22
+ Claude Code's plugin marketplace. The skill (`skills/brookmd/`) carries a
23
+ trigger-listing description, quick setup, the chat-UI defaults, a props
24
+ table with real defaults, twelve gotchas, six reference documents (API,
25
+ frameworks, styling, security, recipes, troubleshooting) and seven typed
26
+ examples. `llms.txt` at the repo root and `llms.txt` / `llms-full.txt` on
27
+ the demo site give agents an index without cloning. Every API fact in the
28
+ skill was verified against source, not the README — and stays that way: CI
29
+ typechecks the examples against `src/`, and `test/skill-docs.test.ts` fails
30
+ on malformed frontmatter, a dead relative link, an import of a
31
+ `brookmd/<subpath>` the package does not export, or a public doc that names
32
+ another rendering library.
33
+ - **`onLinkClick`** on `<BrookMarkdown>` (React), `mountBrookMarkdown`
34
+ (`MountOptions`), the Vue / Svelte / Solid bindings and the
35
+ `<brook-markdown>` element (`.onLinkClick` property): one delegated listener
36
+ on the `.brook-md` root hands back `(event, { href, text, element })`
37
+ (`LinkClickInfo`, exported from `brookmd`, `brookmd/types` and
38
+ `brookmd/dom`); `event.preventDefault()` cancels navigation, so link
39
+ interstitials and in-app routing need no per-anchor override. Links still
40
+ streaming their URL (`<a data-brook-pending>`) are never reported. The hook
41
+ adds zero per-block or per-anchor work — a changing handler identity
42
+ re-renders no blocks (pinned by test).
43
+ - **`registerLanguage(names, { pats, kw })`** on `brookmd/highlight` (also
44
+ re-exported from `brookmd`): teach the built-in streaming highlighter a
45
+ language at runtime, under one name or several aliases. Regexes must be
46
+ sticky, token classes must be ones the stylesheet colours (`ident` is the
47
+ class a `kw` set refines), and re-registering a name replaces it. A
48
+ registered language highlights identically to a built-in one once its block
49
+ closes; while streaming it re-tokenizes instead of growing a frozen prefix,
50
+ since a caller's table does not say which of its forms can run past a
51
+ newline.
52
+ - **Twelve more built-in language families**: yaml/yml, toml, diff, java, c,
53
+ cpp/c++, cs/csharp, php, rb/ruby, swift, kt/kotlin and dockerfile — 36
54
+ language keys in total. Language lookup is now a prototype-free table, so a
55
+ fence tagged `constructor` or `__proto__` is an ordinary miss. The ReDoS
56
+ suite runs 17 adversarial inputs against *every* supported language under a
57
+ per-input time budget, so a new table cannot land a quadratic pattern.
58
+ - **The theme finally styles the fenced-block chrome.** `brookmd/styles.css`
59
+ now gives the built-in code, math and mermaid renderers a bordered
60
+ container, a header bar with the language label, a ghost copy button (hover,
61
+ focus ring, copied state) and a pulsing "streaming" pill — previously the
62
+ theme left a bare button stack above every fence. Also new: GitHub-style
63
+ alerts for all five kinds (`--brook-alert-*` variables in light and dark),
64
+ footnote sections and references, block/inline math, disabled task-list
65
+ checkboxes. A contract test collects every `brook-*` class the renderers
66
+ emit and fails if the theme does not style it.
67
+ - **Opt-in streaming caret.** Add `brook-caret` to the root
68
+ (`<BrookMarkdown className="brook-caret" />`) and the block still streaming
69
+ gets a blinking cursor at its tail (`--brook-caret` retints it). Fences show
70
+ the streaming pill instead. Both animations route through
71
+ `--brook-caret-anim` / `--brook-pill-anim` and switch off under
72
+ `prefers-reduced-motion: reduce`.
73
+ - **Block-state classes are now a documented, stable styling contract**:
74
+ `brook-block`, `brook-block-<kind>`, `brook-open`, `brook-speculative`, and
75
+ `brook-streaming` on the code/math/mermaid slots.
76
+ - `<brook-markdown>` honours the **`stick-to-bottom`** and **`virtualize`**
77
+ attributes the README had advertised; changing either after mount re-applies
78
+ it, including with a caller-owned client.
79
+
80
+ ### Fixed
81
+
82
+ - **Streaming parity: a table delimiter row still being typed no longer
83
+ freezes the paragraph above it** (`brookmd-core` 0.27.0). With
84
+ `| name | value |` / `value |` / `|:-----|--`, the moment the buffer held
85
+ `|:-` the parser saw a one-column delimiter under the one-column line
86
+ `value |`, formed a table, and committed `<p>| name | value |</p>` as a
87
+ separate block; one byte later the delimiter widened, the table dissolved,
88
+ but the commit could not be taken back — so the finalized document differed
89
+ from the one-shot parse (one paragraph). A table whose delimiter row sits on
90
+ the buffer's unterminated final line is now held speculative until that line
91
+ completes, matching the existing guard for provisionally classified blocks.
92
+ Found by the coverage-guided parity fuzzer; pinned by a regression test
93
+ across every chunking of 1–8 bytes; a 240 s fuzz run afterwards found
94
+ nothing new.
95
+ - The generic block wrapper was eating the theme's spacing rules: the last
96
+ block's margin escaped the root and the first heading sat indented from the
97
+ top. `.brook-bottom-anchor` (the `stickToBottom` sentinel) no longer occupies
98
+ a full block gap of dead space at the end of the document.
99
+ - README corrections, each verified against source: `<brook-markdown
100
+ stick-to-bottom>` (now real — see above), the root class is `brook-md` (not
101
+ `brookmd`), `new BrookClient({ pool })` (not `new BrookClient(pool)`),
102
+ `MountHandle` also has `openBlockId()`, the real `Components` and
103
+ `BlockComponentProps` declarations, the Blockquote/Alert `{ nested }` →
104
+ `props.container` and `ListData.items` / `ListItemData.start` rows in the
105
+ `blockData` table, and the previously undocumented surface — `deferTail`,
106
+ `childMemo`, `onRenderMetrics`, `onStreamError`, `streamConfig`, `coalesce`,
107
+ `reattach()`, `retainCommittedHtml`, `bootTimeoutMs`, `supportedLangs()`,
108
+ and the `brookmd/types`, `brookmd/html-to-react`, `brookmd/block-props`,
109
+ `brookmd/worker-core` entry points. `SHIPPING.md` no longer claims the
110
+ package ships as source (compiled ESM since 0.17.0).
111
+ - `brookmd-react-native` now depends on `brookmd ^0.30.0` (it pinned `^0.26.0`,
112
+ which a 0.x caret resolves to `<0.27`, so its workspace install had been
113
+ fetching an old brookmd from the registry instead of the sibling package).
114
+
115
+ ### Performance
116
+
117
+ - **Open blockquote / alert / component bodies keep a settled HTML prefix**
118
+ (`brookmd-core` 0.27.0). The assembler for a structured container or a
119
+ component block used to re-walk every committed inner sub-block and re-copy
120
+ its HTML on *every* append — the residual wall-clock cliff documented since
121
+ 0.28.0. Each committed sub-block is now folded into a per-cache prefix once
122
+ (one prefix per nested-parser twin on the component path, since the settled
123
+ twin's committed HTML differs) and re-emitted as one contiguous copy; only
124
+ the active tail is rebuilt. A streaming component body of paragraphs is
125
+ 1.9–5.3× faster at 256 KB (2,048-paragraph body 152 ms → 66 ms; dense
126
+ small-paragraph body 571 ms → 107 ms, growth 34× → 11× over 32→256 KB).
127
+ Output is byte-identical: 652/652 CommonMark + 24/24 GFM byte-exact, every
128
+ parity suite green, and a differential harness hashing the full document
129
+ after every append found no difference from the previous implementation
130
+ across 896 hand-written cases and 20,000 randomized container/component
131
+ documents × 4 chunkings. A new wall-clock scaling guard
132
+ (`wrapper_body_prefix_is_wall_linear`, control-twin primary, retry-once)
133
+ pins the shape — the work counters are structurally blind to this class —
134
+ and was verified to fail on the pre-fix tree. Blockquote/alert bodies whose
135
+ nested parser commits nothing (one giant list) see no change; their residual
136
+ cost is the two unavoidable O(body) `Block.html` materializations per append.
137
+
138
+ ### Docs
139
+
140
+ - New README sections written for someone building a chat UI: **Chat UI
141
+ defaults** (`softBreaks`, `dirAuto`, `a11y`, `blockData`, `gfmMath`, hoisted
142
+ `components`, `warm()`, the caret), **With the Vercel AI SDK (`useChat`)**
143
+ including the `streaming: false`-on-finish rule and why brookmd refuses to
144
+ infer it, **Accessible chat**, **Tailwind / design systems** (and why
145
+ `@tailwindcss/typography` users should skip the theme import), and recipes
146
+ for KaTeX, Mermaid, a custom highlighter, interactive task lists, lazy
147
+ images, and a CSV/copy table toolbar over `props.table`.
148
+
149
+ ## 0.29.1 — 2026-07-31
150
+
151
+ Performance only; rendered bytes unchanged (asserted: settled markup identical
152
+ with streaming highlight on and off, and mid-stream parity suites untouched).
153
+
154
+ ### Performance
155
+
156
+ - **Two quadratic terms removed from the streaming-highlight hot path.** An
157
+ external benchmark of 0.27 showed streaming highlighting costing +36%
158
+ main-thread time on a code-heavy stream. Most of that was the 0.27 DOM
159
+ write pattern, already fixed in 0.28 (the on/off DOM-write delta is 151×
160
+ smaller at this release). Decomposing what remained found two genuinely
161
+ quadratic terms: the revision guard re-scanned the block character-by-
162
+ character on *every* patch to find a divergence point whose exact value
163
+ was never used (~2,070× the source in `charCodeAt` calls — replaced with
164
+ two `startsWith` questions against a cached prefix), and the fence body
165
+ was re-decoded out of its rendered HTML on every patch through a lazy
166
+ regex plus five unconditional entity passes (~5,169× the source — replaced
167
+ with landmark indexing and an `&`-gated entity chain). Streaming a 32 KB
168
+ code block, highlight-only JS cost drops **782 ms → 163 ms**, and both
169
+ terms now scale linearly. A scale-free regression gate
170
+ (`test/streaming-highlight-cost.test.ts`) pins ratios against source and
171
+ markup size — verified to fail on the pre-fix tree.
172
+ - Evaluated and rejected on measurement: computing the highlight in the
173
+ worker (the worker forwards patches as opaque strings; parsing them there
174
+ costs more than the work it would relocate).
175
+
7
176
  ## 0.29.0 — 2026-07-31
8
177
 
9
178
  **Sublinear resources.** Total parse work has an Ω(n) floor and stays exactly
@@ -206,7 +375,7 @@ Requires `brookmd-core` 0.25.0.
206
375
 
207
376
  - **`softBreaks` — a soft line break renders as `<br>`.** Strict CommonMark
208
377
  treats a bare `\n` inside a paragraph as whitespace, so a model that writes one
209
- thought per line gets one reflowed blob. This is the `remark-breaks` /
378
+ thought per line gets one reflowed blob. This is the
210
379
  chat-comment convention where one Enter is one visual line, and it is what most
211
380
  chat UIs actually want. Off by default; it only ever *adds* breaks (a hard break
212
381
  is `<br>` either way), so no existing output loses a line.