brookmd 0.25.1 → 0.26.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 +274 -0
- package/README.md +205 -22
- package/dist/block-props.js +2 -1
- package/dist/element.js +20 -2
- package/dist/react.js +2 -1
- package/dist/server.d.ts +15 -4
- package/dist/server.js +11 -1
- package/dist/types-core.d.ts +121 -13
- package/dist/wasm/README.md +2 -2
- package/dist/wasm/brook_md_core.d.ts +44 -0
- package/dist/wasm/brook_md_core.js +54 -0
- package/dist/wasm/brook_md_core_bg.wasm +0 -0
- package/dist/wasm/brook_md_core_bg.wasm.d.ts +4 -0
- package/dist/worker.js +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,271 @@ 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.26.0 — 2026-07-30
|
|
8
|
+
|
|
9
|
+
**Rendered HTML bytes change in this release.** Everything new below is opt-in
|
|
10
|
+
and default-off — with the flags off those paths are byte-identical to 0.25.2 —
|
|
11
|
+
but the output-fidelity work is unconditional: container framing newlines,
|
|
12
|
+
paragraph whitespace, table line breaks, fenced-code indentation, and the
|
|
13
|
+
task-list checkbox form now match the CommonMark and GFM reference renderers
|
|
14
|
+
exactly. No API is removed, and the wire envelope, ids, and commit semantics are
|
|
15
|
+
untouched — but HTML snapshots taken against 0.25.x will need regenerating.
|
|
16
|
+
That, not the new features, is why this is a minor bump.
|
|
17
|
+
|
|
18
|
+
What it buys: **652/652 CommonMark 0.31 and 24/24 GFM, byte-exact** against the
|
|
19
|
+
reference renderers, where before the suites passed only after structural
|
|
20
|
+
normalization. Byte-exactness is now the harnesses' default floor and is pinned
|
|
21
|
+
in CI, so one regressed byte fails the build.
|
|
22
|
+
|
|
23
|
+
Requires `brookmd-core` 0.25.0.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`softBreaks` — a soft line break renders as `<br>`.** Strict CommonMark
|
|
28
|
+
treats a bare `\n` inside a paragraph as whitespace, so a model that writes one
|
|
29
|
+
thought per line gets one reflowed blob. This is the `remark-breaks` /
|
|
30
|
+
chat-comment convention where one Enter is one visual line, and it is what most
|
|
31
|
+
chat UIs actually want. Off by default; it only ever *adds* breaks (a hard break
|
|
32
|
+
is `<br>` either way), so no existing output loses a line.
|
|
33
|
+
- **`allowSchemes` — un-block a URL scheme brookmd blocks by default.** Bare
|
|
34
|
+
scheme names, no colon (`allowSchemes: ["file"]`), matched case-insensitively.
|
|
35
|
+
It reaches exactly one tier: the *overridable*-blocked schemes, today `file:`.
|
|
36
|
+
The script-executing tier (`javascript:`, `data:text/html`, …) is
|
|
37
|
+
non-overridable — listing one is a silent no-op, not an escape hatch — and the
|
|
38
|
+
encoded-evasion neutralization (percent- and entity-encoded scheme prefixes) is
|
|
39
|
+
unchanged and runs before the check. This exists for privileged embedders —
|
|
40
|
+
Electron shells, extensions, editor preview panes — that intercept link clicks
|
|
41
|
+
instead of navigating.
|
|
42
|
+
- **`lenientLists` — rescue an over-indented list item from becoming a code
|
|
43
|
+
block.** CommonMark §5.2 says a marker followed by 5+ columns of whitespace
|
|
44
|
+
starts an indented code block, so a model emitting `- const value = 1;`
|
|
45
|
+
renders a `<pre><code>` instead of a list item. With the flag on, a marker
|
|
46
|
+
followed by **6 or more columns of literal spaces** absorbs the padding into the
|
|
47
|
+
content column and the text parses as the item's own markdown. Deliberately
|
|
48
|
+
narrow: exactly 5 spaces still opens code (that column is the spec boundary
|
|
49
|
+
itself), a fence on the marker line stays a fence, an indent on a *later* line
|
|
50
|
+
stays code, and **tab** padding stays code — tabs are an authoring choice, model
|
|
51
|
+
over-indentation is always literal spaces. Excluding tabs is what holds the
|
|
52
|
+
divergence to a single spec example (274) and only while the flag is on; the
|
|
53
|
+
conformance suites run strict and are unaffected.
|
|
54
|
+
- **`blockHtml` — block-level raw HTML through the safe sanitizer.** A
|
|
55
|
+
`<details><summary>…</summary>…</details>` block rendered as an escaped code
|
|
56
|
+
block before; now it renders as real elements, sanitized. Only CommonMark HTML
|
|
57
|
+
block **types 6 and 7** qualify — types 1–5 (`<script>`/`<pre>`/`<style>`/
|
|
58
|
+
`<textarea>`, comments, processing instructions, declarations, CDATA) stay
|
|
59
|
+
escaped by design, so the constructs that can execute or swallow the rest of
|
|
60
|
+
the document never take effect.
|
|
61
|
+
It shares its token core with the inline raw-HTML path (one policy, not two),
|
|
62
|
+
tracks an open-tag stack with speculative closers so a half-streamed
|
|
63
|
+
`<details` never leaks a broken element, carries a void-element table so `<hr>`
|
|
64
|
+
and friends are not pushed onto that stack, and caps nesting depth at 100.
|
|
65
|
+
Half-streamed tags are suppressed while open and settle at finalize. Takes
|
|
66
|
+
effect only when the sanitizer is engaged (`htmlAllowlist` or `dropHtmlTags`);
|
|
67
|
+
on its own it does nothing.
|
|
68
|
+
- **`CodeBlockData.meta` — the fence info string's remainder.**
|
|
69
|
+
```` ```ts title="src/main.ts" ```` now yields `lang: "ts"` **and**
|
|
70
|
+
`meta: 'title="src/main.ts"'` on the data channel and as a `meta` prop on
|
|
71
|
+
`components.CodeBlock`. Always-on like `lang` (no `blockData` needed) and
|
|
72
|
+
omitted entirely when the fence carried none, so a fence without meta is
|
|
73
|
+
byte-identical to before. It is deliberately **not** in the rendered HTML —
|
|
74
|
+
there is no `data-meta` attribute — because a filename header is a component's
|
|
75
|
+
job, not the parser's. While streaming it appears only once it can no longer
|
|
76
|
+
change (the opening fence line terminated by a newline, or at finalize), so a
|
|
77
|
+
header never flickers through a half-typed `title="src/ma`.
|
|
78
|
+
- **`ListItemData.start` — a source byte offset per list item.** Under
|
|
79
|
+
`blockData`, each top-level `items[]` entry carries the document-absolute offset
|
|
80
|
+
of the byte where its marker begins, same origin as `Block.start` and stable as
|
|
81
|
+
the document grows. That is enough to build task-checkbox writeback: locate the
|
|
82
|
+
`[ ]` from the item's offset and flip it in the original markdown, no HTML
|
|
83
|
+
round-trip. **Nested** items carry no offset rather than a wrong one — a nested
|
|
84
|
+
list is not a separate block, it renders against a synthesized de-indented
|
|
85
|
+
string with no document offset — and that limitation is documented on the type.
|
|
86
|
+
- Together `meta` and `start` bump the **wire contract to 1.3.0** (WIRE.md §9):
|
|
87
|
+
purely additive optional `data` keys, byte-identical when unexercised.
|
|
88
|
+
- All four flags are plumbed through every surface: client/worker config, the
|
|
89
|
+
server renderer, the `<brook-markdown>` element (`soft-breaks`,
|
|
90
|
+
`lenient-lists`, `block-html`, `allow-schemes`), React Native, Kotlin, Swift,
|
|
91
|
+
and the Flutter hand-written config.
|
|
92
|
+
- **Byte-exact conformance mode in the spec harnesses**, with default ratchet
|
|
93
|
+
floors `CMARK_MIN_EXACT=652` / `GFM_MIN_EXACT=24` — pinned explicitly in both
|
|
94
|
+
the CI and publish workflows alongside the older normalized floors. The four
|
|
95
|
+
deliberate differences from the reference (`target`/`rel` on links, `data-lang`
|
|
96
|
+
on code blocks, HTML5 void `<br>`, `style="text-align:…"` instead of GFM's
|
|
97
|
+
deprecated `align`) are folded by a documented `canonicalize` step applied to
|
|
98
|
+
**both** sides — the only transform on the byte-exact path, so it can erase our
|
|
99
|
+
intentional extras but never hide a structural divergence.
|
|
100
|
+
- **An unconditional `bindings` CI job** — FFI and C-ABI crate tests plus
|
|
101
|
+
regeneration-freshness checks for the React Native, Kotlin, and Swift bindings.
|
|
102
|
+
The existing binding workflows only ran on `pull_request` behind `paths:`
|
|
103
|
+
filters, so a push to main or a JS-only PR gated on nothing; that is how stale
|
|
104
|
+
binding goldens survived.
|
|
105
|
+
|
|
106
|
+
### Changed
|
|
107
|
+
|
|
108
|
+
- **Container inner newlines now match the reference exactly** (cmark's `cr()`
|
|
109
|
+
rule): `<li>` / `<blockquote>` / alert framing newlines land where the
|
|
110
|
+
reference puts them, an empty blockquote gets its newline, and a `<li>` opening
|
|
111
|
+
with a tight paragraph no longer emits a spurious `\n`.
|
|
112
|
+
- **Paragraph whitespace follows the spec line by line.** Leading indentation is
|
|
113
|
+
stripped on *every* line, not just the first; trailing spaces and tabs are
|
|
114
|
+
dropped before a soft break; a hard-break line sheds the *next* line's indent.
|
|
115
|
+
Lazy continuation lines in blockquotes and lists keep their `\n` instead of
|
|
116
|
+
being glued with a space.
|
|
117
|
+
- **GFM tables emit one element per line**, matching the reference's framing, and
|
|
118
|
+
task-list checkboxes emit the reference byte-form
|
|
119
|
+
(`checked="" disabled="" type="checkbox"`).
|
|
120
|
+
- **Fenced code**: the body is de-indented by the opening fence's own indent (so
|
|
121
|
+
a fence at columns 1–3 no longer carries that indent into every line);
|
|
122
|
+
significant trailing spaces inside the last line are preserved; interior blank
|
|
123
|
+
lines before the closer are preserved. Tab-column arithmetic is fixed after
|
|
124
|
+
blockquote markers, across item-content boundaries, and after list markers.
|
|
125
|
+
- **Document assembly is now defined and documented** ([WIRE.md
|
|
126
|
+
§12](../../crates/brookmd-core/WIRE.md)): a block's `html` carries **no
|
|
127
|
+
trailing newline** — the terminator after a top-level block belongs to the
|
|
128
|
+
document, not the block — and concatenating blocks is a `cr()`-join.
|
|
129
|
+
`renderToString` follows that rule, so a server-rendered document and a
|
|
130
|
+
reference render agree byte-for-byte.
|
|
131
|
+
- **Native `BrookConfig` gained four fields** (`soft_breaks`, `allow_schemes`,
|
|
132
|
+
`lenient_lists`, `block_html`), appended in a newly documented **append-only
|
|
133
|
+
zone**: uniffi serializes a record's fields *positionally*, so inserting a field
|
|
134
|
+
anywhere above shifts every later read. The wire was verified to be
|
|
135
|
+
exact-consumption — a binding built against the old record fails loudly on a
|
|
136
|
+
version mismatch rather than silently mis-decoding — and the RN, Kotlin, and
|
|
137
|
+
Swift bindings are regenerated, with the Flutter hand-written config synced
|
|
138
|
+
(it also gained the previously-missing `wire_delta`). Stale wire goldens across
|
|
139
|
+
all four languages were re-synced. `brookmd-ffi` and `brookmd-cabi` go to
|
|
140
|
+
**0.3.0**: the native library and its generated bindings must ship in lockstep.
|
|
141
|
+
|
|
142
|
+
### Performance
|
|
143
|
+
|
|
144
|
+
Every item below is pinned by a **wall-time** regression guard in
|
|
145
|
+
`crates/brookmd-core/tests/scaling.rs`, each paired with a flush-control twin
|
|
146
|
+
that isolates the shape from its chunking. That pairing is the point: work
|
|
147
|
+
counters alone provably miss the allocation-class quadratics below — the parser
|
|
148
|
+
was doing O(n) *counted* work while re-allocating O(n) bytes per append, so only
|
|
149
|
+
the clock could see it.
|
|
150
|
+
|
|
151
|
+
| Shape | Measured as | Before | After |
|
|
152
|
+
| --- | --- | --- | --- |
|
|
153
|
+
| growing fence opener line | growth over an 8× input span | 50.5× | **9.1×** |
|
|
154
|
+
| indented continuation lines | vs the unindented control | 304.6× | **1.1×** |
|
|
155
|
+
| ragged chunks parked on a blank partial line | vs control | 110.2× | **1.1×** |
|
|
156
|
+
| the same, inside a list item | work counters | 212× | **12.8×** |
|
|
157
|
+
| block-HTML sanitize | vs the escaped control | 247× | **1.3×** |
|
|
158
|
+
|
|
159
|
+
- **Fence opener-line growth** re-sliced the whole opener on every append —
|
|
160
|
+
quadratic in the info string's length. Now linear-ish: 9.1× over an 8× span.
|
|
161
|
+
- **Indented continuation lines** made `is_boundary` produce *zero* cut
|
|
162
|
+
candidates, so every append re-rendered the entire paragraph from its start. A
|
|
163
|
+
new indent-led boundary rule (with a hard-break-straddle exclusion) restores
|
|
164
|
+
cutting; the shape now costs 1.1× its unindented control.
|
|
165
|
+
- **Ragged chunking that parks on a whitespace-only partial line** dropped the
|
|
166
|
+
paragraph cache outright, forcing a full rebuild per append. The cache now
|
|
167
|
+
**suspends instead of dropping**: the closed view stays byte-identical and the
|
|
168
|
+
parse resumes when the line completes. List items inherit the fix through the
|
|
169
|
+
nested parser.
|
|
170
|
+
- **Block-level raw HTML** would otherwise re-sanitize its whole body per append
|
|
171
|
+
(measured 247×); the sanitize cache folds at token boundaries, shipping at 1.3×
|
|
172
|
+
the cost of the escaped control. Container, table-cell, and heading caches were
|
|
173
|
+
proven structurally immune to the same class and are pinned so they stay that
|
|
174
|
+
way.
|
|
175
|
+
|
|
176
|
+
### Security
|
|
177
|
+
|
|
178
|
+
- **The raw-HTML attribute policy is now an explicit dropped-attribute table**
|
|
179
|
+
rather than scheme-checking alone: `srcdoc`, `is`, `autofocus`,
|
|
180
|
+
`contenteditable`, the DOM-clobbering `id` / `name`, the shadow-piercing
|
|
181
|
+
`slot` / `part` / `exportparts`, the `form*` family, the `xmlns:` / `xlink:`
|
|
182
|
+
prefixes, and `ping`. `formaction` and `ping` are **dropped outright** — for a
|
|
183
|
+
URL carrier that re-targets a form or fires a background beacon, dropping is
|
|
184
|
+
strictly stronger than validating its scheme. Component-tag props stay
|
|
185
|
+
permissive by design (they are consumer-mediated — your component decides what
|
|
186
|
+
to do with them, and they never become DOM attributes); that asymmetry is now
|
|
187
|
+
documented rather than incidental.
|
|
188
|
+
- **The web component's `gfm-tagfilter` attribute was never observed.** It was in
|
|
189
|
+
the element's config map but missing from `observedAttributes`, so it applied at
|
|
190
|
+
mount and then silently ignored every later change — a security-relevant flag
|
|
191
|
+
that could not be turned on after the fact. Fixed, and the list now carries a
|
|
192
|
+
comment tying it to the map so the next flag cannot repeat it.
|
|
193
|
+
|
|
194
|
+
### Fixed
|
|
195
|
+
|
|
196
|
+
- **A mid-stream committed-view divergence**: a cut taken after a *single*
|
|
197
|
+
inter-word space could straddle the hard-break lookbehind, so a paragraph
|
|
198
|
+
containing entity-produced spaces rendered differently mid-stream than it did
|
|
199
|
+
one-shot. The streamed view now matches the one-shot render at every chunk
|
|
200
|
+
boundary again.
|
|
201
|
+
|
|
202
|
+
## brookmd-react-native 0.1.7 — 2026-07-30
|
|
203
|
+
|
|
204
|
+
### Added
|
|
205
|
+
|
|
206
|
+
- `softBreaks`, `allowSchemes`, `lenientLists`, and `blockHtml` on the native
|
|
207
|
+
`BrookConfig`, reaching the on-device parser through the same JSI path as the
|
|
208
|
+
existing flags. The generated TS/C++ bindings are regenerated from
|
|
209
|
+
`brookmd-ffi` 0.3.0 and are now freshness-checked on every push by the new CI
|
|
210
|
+
`bindings` job.
|
|
211
|
+
|
|
212
|
+
### Changed
|
|
213
|
+
|
|
214
|
+
- `brookmd` dependency range `^0.25.0` → `^0.26.0`, and the vendored native
|
|
215
|
+
binaries are built against `brookmd-core` 0.25.0 — so the on-device renderer
|
|
216
|
+
produces the same reference-exact bytes as the browser. Native and JS must move
|
|
217
|
+
together here: uniffi records are positional, and the four new `BrookConfig`
|
|
218
|
+
fields land in the record's append-only zone.
|
|
219
|
+
|
|
220
|
+
## 0.25.2 — 2026-07-27
|
|
221
|
+
|
|
222
|
+
Performance only. Rendered output is unchanged — byte-identical mid-stream, not
|
|
223
|
+
just at finalize.
|
|
224
|
+
|
|
225
|
+
### Fixed
|
|
226
|
+
|
|
227
|
+
- **An open component block no longer re-scans its whole body on every blank
|
|
228
|
+
line — O(n²) → O(n).** `try_incremental_component` bailed whenever the fed
|
|
229
|
+
buffer ended on a blank line, and because the cache had already been taken the
|
|
230
|
+
bail dropped it, forcing a full tail re-scan, a full re-render, and a fresh
|
|
231
|
+
nested parser over the entire body. Blank lines are legal component-body
|
|
232
|
+
content, so that fired once per body paragraph.
|
|
233
|
+
|
|
234
|
+
The bail existed for a real reason: when the buffer ends blank the full rescan
|
|
235
|
+
renders the component and all its sub-blocks with `open_tail = false`, which
|
|
236
|
+
the nested parser's `force_open_tail = true` commits can never match. Rather
|
|
237
|
+
than work around it with a trigger-byte heuristic (measured: still 3.9×/doubling
|
|
238
|
+
on any body containing a backtick or bracket — i.e. all real ones), the cache
|
|
239
|
+
now carries a **settled twin**: a second nested parser with `force_open_tail`
|
|
240
|
+
off, fed lazily only on the appends that read it, so each catch-up spans just
|
|
241
|
+
the bytes since the previous blank line. A body with no blank lines never
|
|
242
|
+
allocates it. The cache now arms once per stream instead of once per paragraph.
|
|
243
|
+
|
|
244
|
+
Streaming a 64 KB `<Thinking>` body, scan work and wall time:
|
|
245
|
+
|
|
246
|
+
| body | before | after |
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| plain paragraphs | 117,333,095 B / 11,954 ms | 229,837 B / **269 ms** |
|
|
249
|
+
| with inline code | 101,046,887 B / 9,316 ms | 224,977 B / **226 ms** |
|
|
250
|
+
| with links | 86,362,310 B / 6,837 ms | 220,835 B / **180 ms** |
|
|
251
|
+
| with `$` and `<` | 117,333,095 B / 7,333 ms | 229,837 B / **388 ms** |
|
|
252
|
+
|
|
253
|
+
Scan and render work are now 2.00×/doubling (dead linear); the residual
|
|
254
|
+
3.3–3.6×/doubling in wall time is a separate, pre-existing cliff —
|
|
255
|
+
`assemble_wrapped_body` re-materializes an open block's HTML every append —
|
|
256
|
+
which the already-registered `open-block-html-reemit` shape sits in too.
|
|
257
|
+
|
|
258
|
+
**Trade-off:** the twin holds a second copy of an open component's body bytes
|
|
259
|
+
plus one rendered-HTML set, freed when the block closes. Roughly 2× transient
|
|
260
|
+
memory for open component blocks *that contain blank lines*; blank-free bodies
|
|
261
|
+
are unaffected.
|
|
262
|
+
|
|
263
|
+
### Added
|
|
264
|
+
|
|
265
|
+
- `tests/scaling.rs` gains `component_multi_para` and `component_multi_para_rich`
|
|
266
|
+
(backticks, brackets, `$`, `<`), both gated `Linear`. The existing
|
|
267
|
+
`component_block_open` shape generates no blank lines and so was structurally
|
|
268
|
+
blind to this: its `scanned` is a flat 249 B from 8 KB to 64 KB. The new shapes
|
|
269
|
+
measure 252.4× on the pre-fix parser (gate fails) and 16.0× over a 16× span
|
|
270
|
+
after.
|
|
271
|
+
|
|
7
272
|
## 0.25.1 — 2026-07-27
|
|
8
273
|
|
|
9
274
|
A correction release. 0.25.0 shipped one user-visible regression and a dev-gate
|
|
@@ -49,6 +314,15 @@ memory leak on the opt-in `childMemo` path. Upgrade from 0.25.0.
|
|
|
49
314
|
id, kind, override keys, HTML excerpt, the error) and moves only the
|
|
50
315
|
explanatory prose behind the dev gate.
|
|
51
316
|
|
|
317
|
+
## brookmd-react-native 0.1.6 — 2026-07-27
|
|
318
|
+
|
|
319
|
+
### Fixed
|
|
320
|
+
|
|
321
|
+
- Vendored native binaries rebuilt against `brookmd-core` 0.24.2, which makes an
|
|
322
|
+
open component block's streaming cost linear in its body. This matters more
|
|
323
|
+
on-device than in the browser: the pre-fix path spent ~24 s of native CPU on a
|
|
324
|
+
64 KB token-streamed `<Thinking>` body. No JS changes.
|
|
325
|
+
|
|
52
326
|
## brookmd-react-native 0.1.5 — 2026-07-27
|
|
53
327
|
|
|
54
328
|
### Fixed
|
package/README.md
CHANGED
|
@@ -461,6 +461,17 @@ export default function Doc({ md }: { md: string }) {
|
|
|
461
461
|
- **`renderToString(md, { config })`** — synchronous HTML string, **zero React
|
|
462
462
|
dependency** (imports cleanly with no `react` installed).
|
|
463
463
|
- **`parseToBlocks(md, { config })`** — the block array, for custom rendering.
|
|
464
|
+
|
|
465
|
+
**Document assembly.** A `Block.html` never ends with a newline — the terminator
|
|
466
|
+
that follows a top-level block belongs to the *document*, not the block.
|
|
467
|
+
`renderToString` therefore joins blocks with cmark's `cr()` rule: insert `\n`
|
|
468
|
+
before a block only when the output doesn't already end with one, and end the
|
|
469
|
+
document with one `\n`. An unconditional `"\n".join(...)` would double the
|
|
470
|
+
newline a raw HTML block serializes for itself. **If you assemble
|
|
471
|
+
`parseToBlocks` output into a document string yourself, use the same rule** —
|
|
472
|
+
it's what makes the output byte-identical to a reference CommonMark/GFM renderer
|
|
473
|
+
(652/652 CommonMark 0.31 and 24/24 GFM extension examples, byte-for-byte). See
|
|
474
|
+
`WIRE.md` §12.
|
|
464
475
|
- **`<BrookMarkdownStatic content config components />`** (from
|
|
465
476
|
`brookmd/server/react`) — synchronous React tree for **render-once** contexts;
|
|
466
477
|
render it with your framework's server renderer
|
|
@@ -614,12 +625,16 @@ const client = new BrookClient({
|
|
|
614
625
|
gfmFootnotes: true, // [^1] + [^1]: → footnote section (default false)
|
|
615
626
|
gfmMath: true, // $…$ / \(…\) inline + $$…$$ / \[…\] display math (default false)
|
|
616
627
|
dirAuto: true, // per-block dir="auto" for RTL/bidi text (default false)
|
|
628
|
+
softBreaks: true, // a single \n renders as <br> (remark-breaks / chat convention; default false)
|
|
629
|
+
lenientLists: true, // marker + 6+ SPACES → item text, not indented code (default false)
|
|
617
630
|
a11y: true, // task-list <label> + <th scope="col"> a11y markup (default false)
|
|
618
631
|
unsafeHtml: false, // pass raw HTML through (default false — keep it false for untrusted input)
|
|
619
632
|
componentTags: ["Thinking", "Callout"], // BLOCK custom tags w/ markdown inside (default none)
|
|
620
633
|
inlineComponentTags: ["tik", "cite"], // INLINE custom tags (chips/citations) w/ markdown inside (default none)
|
|
621
634
|
htmlAllowlist: ["br", "sub", "sup"], // safe raw-HTML sanitizer: [] = allow all but dangerous; list = only those (default off)
|
|
622
635
|
dropHtmlTags: [], // tags removed entirely (comments always dropped when sanitizing; default off)
|
|
636
|
+
blockHtml: true, // extend the sanitizer to BLOCK raw HTML (<details>…); needs a list above (default false)
|
|
637
|
+
allowSchemes: ["file"], // un-block a default-blocked URL scheme (default none — see "Security")
|
|
623
638
|
blockData: true, // opt-in structured kind.data per block (default false — see "Structured block data")
|
|
624
639
|
},
|
|
625
640
|
});
|
|
@@ -640,6 +655,32 @@ When to enable each flag:
|
|
|
640
655
|
definitions. Off by default; see the footnote streaming caveat above.
|
|
641
656
|
- `dirAuto: true` — when content can be RTL / mixed-direction. Emits per-block
|
|
642
657
|
`dir="auto"` so the browser detects direction independently per block.
|
|
658
|
+
- `lenientLists: true` — when your LLM over-indents after a list marker. Strict
|
|
659
|
+
CommonMark (§5.2) says a marker followed by **5 or more** columns of
|
|
660
|
+
whitespace starts an indented code block, so a model writing
|
|
661
|
+
`- const value = 1;` renders as a `<pre><code>` block instead of a list
|
|
662
|
+
item. This flag raises that cutoff to **6 columns of literal spaces**: at 6+
|
|
663
|
+
the padding is absorbed into the item's content column and the text renders
|
|
664
|
+
as the item's own markdown (inline formatting, links, and nested lists all
|
|
665
|
+
parse normally). Off by default, so strict-CommonMark output is unchanged.
|
|
666
|
+
|
|
667
|
+
Four cases stay strictly conformant **by design** — the flag is deliberately
|
|
668
|
+
narrow, not a general "fix my indentation" pass:
|
|
669
|
+
|
|
670
|
+
| Input | Stays | Why |
|
|
671
|
+
| --- | --- | --- |
|
|
672
|
+
| `-` + exactly 5 spaces | code block | 5 columns is the §5.2 boundary itself; relaxing it would swallow genuine one-space-past-the-minimum code |
|
|
673
|
+
| `` - ```js `` (fence on the marker line) | fenced code | the marker line opens a real fence — there is no over-indentation to undo |
|
|
674
|
+
| `-` then code indented on a **later** line | code block | the decision reads only the marker's own line; a later-line indent is unambiguous authored code |
|
|
675
|
+
| `-\t\tfoo` (tab padding) | code block | tab padding is a deliberate authoring choice, unlike model over-indentation which is always literal spaces |
|
|
676
|
+
|
|
677
|
+
Excluding tabs is what keeps the divergence from CommonMark down to a single
|
|
678
|
+
spec example (274, `1.` + 6 spaces). Everything else in the 652-example suite
|
|
679
|
+
renders identically with the flag on or off; the conformance suites
|
|
680
|
+
themselves run in strict mode and are unaffected.
|
|
681
|
+
|
|
682
|
+
The rule is a pure per-line comparison made when the marker is first scanned,
|
|
683
|
+
so it costs no lookahead and no re-parse while streaming.
|
|
643
684
|
- `a11y: true` — opt-in accessibility markup that deviates from strict GFM
|
|
644
685
|
byte-output: wraps task-list checkboxes in a `<label>` (screen-reader
|
|
645
686
|
association) and adds `scope="col"` to table headers. Off by default so
|
|
@@ -664,6 +705,15 @@ When to enable each flag:
|
|
|
664
705
|
- `htmlAllowlist` / `dropHtmlTags` — render a **safe subset of raw HTML** (e.g.
|
|
665
706
|
`<br>`, `<sub>`, `<sup>`) natively without `unsafeHtml`, drop specific tags, and
|
|
666
707
|
drop HTML comments. See [Safe raw HTML](#safe-raw-html).
|
|
708
|
+
- `blockHtml: true` — extend that sanitizer to **block-level** raw HTML, so a
|
|
709
|
+
`<details><summary>…</summary>…</details>` block renders as real elements
|
|
710
|
+
instead of an escaped code block. Needs one of the two lists above to be set;
|
|
711
|
+
`<script>`/`<pre>`/`<style>`/`<textarea>` blocks stay escaped. See
|
|
712
|
+
[Block-level raw HTML](#block-level-raw-html-blockhtml).
|
|
713
|
+
- `allowSchemes: ["file"]` — un-block a URL scheme brookmd blocks by default,
|
|
714
|
+
for privileged hosts (Electron, extensions) that intercept link clicks instead
|
|
715
|
+
of navigating. Script-executing schemes can never be re-enabled. See
|
|
716
|
+
[Un-blocking a scheme](#un-blocking-a-scheme--allowschemes).
|
|
667
717
|
|
|
668
718
|
**Footnotes** (`gfmFootnotes`) work in streaming with one honest caveat: a
|
|
669
719
|
`[^1]` reference renders speculatively the moment it's seen (committed blocks
|
|
@@ -763,8 +813,9 @@ attributes (with `class`→`className` and `style` as an object) plus `children`
|
|
|
763
813
|
**Block-kind** keys (`CodeBlock`, `Mermaid`, `MathBlock`, `Alert`, `Paragraph`,
|
|
764
814
|
`Heading`, `List`, `Blockquote`, `Table`, `Rule`, `Html`) replace the entire
|
|
765
815
|
block. The component receives [`BlockComponentProps`](#types): `{ block, html,
|
|
766
|
-
open, speculative }`, plus `text`/`language` for code/math blocks
|
|
767
|
-
|
|
816
|
+
open, speculative }`, plus `text`/`language` for code/math blocks — and `meta`,
|
|
817
|
+
the rest of a fence's info string (```` ```ts title="src/main.ts" ````), for a
|
|
818
|
+
filename header (the alert type is at `block.kind.data.kind`).
|
|
768
819
|
|
|
769
820
|
> **One map, two prop contracts — the single biggest footgun.** The keys above
|
|
770
821
|
> are looked up by TWO dispatchers. The block-kind dispatcher passes
|
|
@@ -866,7 +917,7 @@ byte-identical, so non-users pay nothing.
|
|
|
866
917
|
|------|-------------------|------|-----|
|
|
867
918
|
| `Table` | `{ headers, rows, aligns }`, cells `{ text, html }` | `props.table` | sort / filter / transpose / CSV / chart |
|
|
868
919
|
| `Heading` | `{ level, text, id }` | `props.heading` | table of contents with anchors |
|
|
869
|
-
| `CodeBlock` | `{ lang, code }` | `props.code` | decoded source (copy / run) |
|
|
920
|
+
| `CodeBlock` | `{ lang, meta?, code }` | `props.code` | decoded source (copy / run) |
|
|
870
921
|
| `MathBlock` | `{ latex }` | `props.math` | LaTeX source (re-render) |
|
|
871
922
|
| `List` | `{ ordered, start }` | `props.list` | ordered-list numbering |
|
|
872
923
|
|
|
@@ -1004,11 +1055,77 @@ new BrookClient({ config: { htmlAllowlist: [] } });
|
|
|
1004
1055
|
text stays as inert text).
|
|
1005
1056
|
- Every rendered tag's **attributes are sanitized**: `on*` handlers and `style`
|
|
1006
1057
|
(a CSS beacon / clickjacking vector) are dropped, and dangerous URL schemes
|
|
1007
|
-
(`javascript:`, …, including multi-encoded) become
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1058
|
+
(`javascript:`, …, including multi-encoded) become `#` — in `href`, `src`,
|
|
1059
|
+
`srcset`, `poster`, `cite`, `action`, `data`, `longdesc` and `background`.
|
|
1060
|
+
- A further set of **DOM-hazard attributes is dropped outright** (case-insensitive)
|
|
1061
|
+
— they neither execute nor carry a URL, but each lets authored markup reach
|
|
1062
|
+
past the text it should be:
|
|
1063
|
+
`srcdoc` (inline document injection), `is` (customized-built-in upgrade),
|
|
1064
|
+
`autofocus` / `contenteditable` (focus-steal, UI spoof), `id` / `name` (DOM
|
|
1065
|
+
clobbering — an element shadowing `document.getElementById` or a global),
|
|
1066
|
+
`slot` / `part` / `exportparts` (shadow-DOM injection), `form`, `formaction`,
|
|
1067
|
+
`formenctype`, `formmethod`, `formnovalidate`, `formtarget` (form hijack),
|
|
1068
|
+
`xmlns` / `xlink:*` (namespace escape hatch), and `ping` (tracking beacon).
|
|
1069
|
+
Some are inert *today* only because the tag that gives them meaning is already
|
|
1070
|
+
in the dangerous set — they are dropped anyway so the policy never depends on
|
|
1071
|
+
that coincidence. `class`, `title`, `alt`, `target`, `rel`, `data-*` and
|
|
1072
|
+
`aria-*` are unaffected.
|
|
1073
|
+
- **This applies to raw HTML only.** [Component tags](#component-tags) stay
|
|
1074
|
+
permissive: their attributes become framework *props* on `components[tag]`, so
|
|
1075
|
+
`<Tab id="x">` keeps `id` — the consumer's component decides whether it ever
|
|
1076
|
+
reaches the DOM. `on*`, `style` and dangerous URL schemes are filtered there too.
|
|
1077
|
+
- **Scope:** *inline* raw HTML by default. Block-level raw HTML stays escaped
|
|
1078
|
+
unless you also set `blockHtml` (below). Tag matching is case-insensitive.
|
|
1079
|
+
|
|
1080
|
+
#### Block-level raw HTML (`blockHtml`)
|
|
1081
|
+
|
|
1082
|
+
A model that emits a disclosure widget on its own lines —
|
|
1083
|
+
|
|
1084
|
+
```html
|
|
1085
|
+
<details>
|
|
1086
|
+
<summary>Sources</summary>
|
|
1087
|
+
Three filings and a transcript.
|
|
1088
|
+
</details>
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
— produces an *HTML block*, not inline HTML, so the sanitizer above leaves it
|
|
1092
|
+
escaped. Opt in with `blockHtml` and it renders as real elements:
|
|
1093
|
+
|
|
1094
|
+
```ts
|
|
1095
|
+
new BrookClient({ config: { htmlAllowlist: [], blockHtml: true } });
|
|
1096
|
+
// or restrict it:
|
|
1097
|
+
new BrookClient({ config: { htmlAllowlist: ["details", "summary"], blockHtml: true } });
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
- **Only meaningful with the sanitizer engaged.** `blockHtml` on its own does
|
|
1101
|
+
nothing; it extends `htmlAllowlist` / `dropHtmlTags` to block level. Default
|
|
1102
|
+
`false`, so existing sanitizer users keep escaped block HTML until they opt in.
|
|
1103
|
+
- **Same policy, no exceptions.** Tags go through the same allow / drop /
|
|
1104
|
+
non-overridable-dangerous decision and the same hardened attribute policy as
|
|
1105
|
+
inline raw HTML — a block-level `<div onclick=… id=… srcdoc=…>` renders as a
|
|
1106
|
+
bare `<div>`.
|
|
1107
|
+
- **Scope: CommonMark HTML block types 6 and 7** — a known block-level tag
|
|
1108
|
+
(`<details>`, `<div>`, `<table>`, `<section>`, …) or any other complete tag
|
|
1109
|
+
alone on its line. **Types 1–5 stay escaped/dropped**, as with the flag off:
|
|
1110
|
+
type 1 is the raw-text family (`<script>`, `<pre>`, `<style>`, `<textarea>`),
|
|
1111
|
+
where a browser reads everything after the opening tag as unparsed text — so a
|
|
1112
|
+
*speculative* close mid-stream is an mXSS vector — and types 2–5 (comments,
|
|
1113
|
+
processing instructions, CDATA, declarations) carry no renderable element at
|
|
1114
|
+
all. A block `<script>` is escaped in every configuration, including with
|
|
1115
|
+
`script` explicitly allowlisted and `unsafeHtml` also on.
|
|
1116
|
+
- **Streaming: speculative closers.** While the block is still arriving, every
|
|
1117
|
+
still-open element gets a closer appended, so the HTML you have received *so
|
|
1118
|
+
far* is always a complete tree — `<div>\n<b>bol` renders as
|
|
1119
|
+
`<div><b>bol</b></div>`, and the closers simply stop being speculative when the
|
|
1120
|
+
author's own `</b></div>` lands (the emitted bytes don't change). A half-arrived
|
|
1121
|
+
tag (`<spa`, `<a href="htt`) renders as **nothing** until it completes, the same
|
|
1122
|
+
pending-invisible contract as a streaming markdown link's URL; if the stream
|
|
1123
|
+
ends on one, it settles as escaped text. Mis-nesting is repaired rather than
|
|
1124
|
+
propagated: `<b><i></b>` emits `<b><i></i></b>`, and a close tag matching
|
|
1125
|
+
nothing open is dropped. A type-6/7 block ends at a blank line even with tags
|
|
1126
|
+
open — the closers land there.
|
|
1127
|
+
- **Markdown inside the HTML is not parsed** (the body is text + tags). That —
|
|
1128
|
+
full `rehype-raw` semantics — is a later stage.
|
|
1012
1129
|
|
|
1013
1130
|
### Types
|
|
1014
1131
|
|
|
@@ -1033,7 +1150,8 @@ interface BlockComponentProps {
|
|
|
1033
1150
|
open: boolean;
|
|
1034
1151
|
speculative: boolean;
|
|
1035
1152
|
text?: string; // decoded source — CodeBlock / MathBlock
|
|
1036
|
-
language?: string; // info string — CodeBlock
|
|
1153
|
+
language?: string; // info string, first word — CodeBlock
|
|
1154
|
+
meta?: string; // info string, the rest (`title="src/main.ts"`) — CodeBlock
|
|
1037
1155
|
}
|
|
1038
1156
|
```
|
|
1039
1157
|
|
|
@@ -1051,14 +1169,35 @@ const html = highlight("const x = 1;", "ts");
|
|
|
1051
1169
|
|
|
1052
1170
|
## Coverage
|
|
1053
1171
|
|
|
1054
|
-
**CommonMark 0.31: 100% (652/652 spec examples)** — every section,
|
|
1055
|
-
the hard ones (nested/loose lists, link reference definitions, link
|
|
1056
|
-
lazy blockquote continuation). Plus GFM extensions
|
|
1057
|
-
lists, extended autolinks, GitHub
|
|
1058
|
-
footnotes (`[^1]` + `[^1]:`), and math
|
|
1059
|
-
Autolinks and alerts are on by default;
|
|
1060
|
-
|
|
1061
|
-
|
|
1172
|
+
**CommonMark 0.31: 100% (652/652 spec examples), byte-exact** — every section,
|
|
1173
|
+
including the hard ones (nested/loose lists, link reference definitions, link
|
|
1174
|
+
precedence, lazy blockquote continuation). Plus GFM extensions, also **24/24
|
|
1175
|
+
byte-exact**: tables, strikethrough, task lists, extended autolinks, GitHub
|
|
1176
|
+
alerts (`> [!NOTE]` → styled callouts), footnotes (`[^1]` + `[^1]:`), and math
|
|
1177
|
+
(`$…$`, `$$…$$`, `\(…\)`, `\[…\]`). Autolinks and alerts are on by default;
|
|
1178
|
+
footnotes and math are opt-in per stream (see
|
|
1179
|
+
[Per-stream config](#per-stream-config)).
|
|
1180
|
+
|
|
1181
|
+
*Byte-exact* means the full output string matches the reference renderer's
|
|
1182
|
+
byte-for-byte — not a structurally normalized or whitespace-forgiving compare.
|
|
1183
|
+
Both counts are the harnesses' **default** floors (`CMARK_MIN_EXACT=652`,
|
|
1184
|
+
`GFM_MIN_EXACT=24`, pinned explicitly in the CI workflows), so a single byte of
|
|
1185
|
+
regression fails the build even when the normalized tally stays green.
|
|
1186
|
+
|
|
1187
|
+
The only deviations from the reference output are deliberate brookmd choices,
|
|
1188
|
+
folded by a documented `canonicalize` step applied to **both** sides before
|
|
1189
|
+
comparison — the only transform on the byte-exact path, so it can erase our
|
|
1190
|
+
intentional extras but never hide a structural divergence:
|
|
1191
|
+
|
|
1192
|
+
| Deliberate difference | Reference emits |
|
|
1193
|
+
|---|---|
|
|
1194
|
+
| `target="_blank" rel="noopener noreferrer nofollow"` on links (security-only) | no such attrs |
|
|
1195
|
+
| `data-lang="…"` on code blocks (alongside `class="language-…"`) | `class` only |
|
|
1196
|
+
| HTML5 void elements (`<br>`) | XHTML self-closing (`<br />`) |
|
|
1197
|
+
| `style="text-align:…"` on table cells | GFM's deprecated `align="…"` |
|
|
1198
|
+
|
|
1199
|
+
See `crates/brookmd-core/tests/{cmark_spec,gfm_spec,footnotes,math}.rs` for the
|
|
1200
|
+
runners, the `canonicalize` source, and the floors.
|
|
1062
1201
|
|
|
1063
1202
|
GitHub alerts render to GitHub-compatible markup
|
|
1064
1203
|
(`<div class="markdown-alert markdown-alert-note">…`), so existing markdown CSS
|
|
@@ -1125,17 +1264,61 @@ brookmd is XSS-safe by default — its HTML output is meant to be injected via
|
|
|
1125
1264
|
- **Raw HTML is escaped** (the `unsafeHtml: true` config flag disables this;
|
|
1126
1265
|
**never enable it for untrusted input without a `sanitize` hook**).
|
|
1127
1266
|
- **Dangerous URL schemes are neutralized** in `<a href>` and `<img src>` —
|
|
1128
|
-
`javascript:`, `vbscript:`, `data:text/html`, `data:text/javascript`
|
|
1129
|
-
`#`. The check runs on the *decoded* URL and strips
|
|
1130
|
-
ignore in the scheme, so obfuscations like
|
|
1131
|
-
`javascript\:…`, `javascript:…`, and embedded
|
|
1132
|
-
not just the literal form. (See
|
|
1267
|
+
`javascript:`, `vbscript:`, `data:text/html`, `data:text/javascript` (and
|
|
1268
|
+
`file:`, see below) become `#`. The check runs on the *decoded* URL and strips
|
|
1269
|
+
characters browsers ignore in the scheme, so obfuscations like
|
|
1270
|
+
`javascript:…`, `javascript\:…`, `javascript:…`, and embedded
|
|
1271
|
+
tabs/newlines are caught, not just the literal form. (See
|
|
1272
|
+
`crates/brookmd-core/tests/security.rs`.)
|
|
1273
|
+
- **The opt-in raw-HTML sanitizer removes DOM-hazard attributes.** Beyond `on*`,
|
|
1274
|
+
`style` and URL schemes, [Safe raw HTML](#safe-raw-html) drops `srcdoc`, `is`,
|
|
1275
|
+
`autofocus`, `contenteditable`, `id`/`name` (DOM clobbering), `slot`/`part`/
|
|
1276
|
+
`exportparts`, the `form*` family and `ping` from raw tags — a denylist by
|
|
1277
|
+
*policy*, not one that happens to be safe because some other tag is blocked.
|
|
1278
|
+
brookmd's own generated ids (footnote `fn-N`/`fnref-N`, heading slugs) are
|
|
1279
|
+
emitted by the renderer and never pass through the sanitizer, so they are
|
|
1280
|
+
unaffected. Component-tag props are exempt (see that section).
|
|
1133
1281
|
- **`htmlToReact` defends in depth**: it drops inline `on*` event-handler
|
|
1134
1282
|
attributes and runs URL attributes through the same scheme filter. It's
|
|
1135
1283
|
intended for brookmd's own (already-sanitized) HTML; if you hand it arbitrary
|
|
1136
1284
|
third-party HTML, these guards are your only line of defense — prefer a
|
|
1137
1285
|
dedicated HTML sanitizer for genuinely hostile input.
|
|
1138
1286
|
|
|
1287
|
+
### Un-blocking a scheme — `allowSchemes`
|
|
1288
|
+
|
|
1289
|
+
`file:` is blocked by default. It can't execute script, but it has no legitimate
|
|
1290
|
+
use in untrusted/LLM markdown, and in a **privileged** host — Electron, a browser
|
|
1291
|
+
extension, a page on a `file://` origin — a live `file:` href is a
|
|
1292
|
+
local-resource-disclosure / phishing vector. In an ordinary browser tab blocking
|
|
1293
|
+
it costs nothing anyway: a page simply **refuses to navigate** to a `file:` URL,
|
|
1294
|
+
so the href is inert there either way.
|
|
1295
|
+
|
|
1296
|
+
Some hosts genuinely need it. A coding-agent UI whose model links to local paths
|
|
1297
|
+
wants the href present so it can **intercept the click** and open the file in an
|
|
1298
|
+
editor or preview pane. `allowSchemes` is the opt-in for exactly that:
|
|
1299
|
+
|
|
1300
|
+
```ts
|
|
1301
|
+
const client = new BrookClient({ config: { allowSchemes: ["file"] } });
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
- Takes **bare scheme names, no colon** (`["file"]`), matched
|
|
1305
|
+
case-insensitively.
|
|
1306
|
+
- It **never restricts** anything — this is not a general allowlist. Schemes
|
|
1307
|
+
outside the built-in blocklist (`vscode:`, `ftp:`, `mailto:`, …) already render
|
|
1308
|
+
today and are unaffected by this setting.
|
|
1309
|
+
- The **script-executing tier is non-overridable**: `javascript:`, `vbscript:`,
|
|
1310
|
+
`data:text/html`, `data:text/javascript`, and the scriptable `data:` media
|
|
1311
|
+
types (`data:image/svg`, `data:application/xhtml`, `data:text/xml`, …) stay
|
|
1312
|
+
blocked no matter what you list. Naming one is a silent no-op — the same rule
|
|
1313
|
+
as `htmlAllowlist`, where allowlisting `<script>` still cannot re-enable it.
|
|
1314
|
+
- Applies uniformly to links, URI autolinks, images, and sanitized URL
|
|
1315
|
+
attributes.
|
|
1316
|
+
|
|
1317
|
+
Turning it on moves the risk to you: **intercept link clicks rather than letting
|
|
1318
|
+
navigation happen**, and treat the path as untrusted input at the point you act
|
|
1319
|
+
on it. In a privileged host, whether a model-authored `file:///…` is allowed to
|
|
1320
|
+
reach a real file is the embedder's decision, not brookmd's.
|
|
1321
|
+
|
|
1139
1322
|
### Rendering untrusted / LLM HTML safely
|
|
1140
1323
|
|
|
1141
1324
|
If you enable `unsafeHtml` to render HTML from an untrusted source (e.g. an LLM
|
package/dist/block-props.js
CHANGED
|
@@ -38,8 +38,9 @@ function blockProps(block) {
|
|
|
38
38
|
if (block.kind.type === "CodeBlock") {
|
|
39
39
|
props.text = data?.code ?? decodeCodeText(block.html);
|
|
40
40
|
props.language = data?.lang ?? "";
|
|
41
|
+
if (typeof data?.meta === "string") props.meta = data.meta;
|
|
41
42
|
if (typeof data?.code === "string") {
|
|
42
|
-
props.code = { lang: data.lang ?? null, code: data.code };
|
|
43
|
+
props.code = { lang: data.lang ?? null, meta: data.meta, code: data.code };
|
|
43
44
|
}
|
|
44
45
|
} else if (block.kind.type === "MathBlock") {
|
|
45
46
|
props.text = data?.latex ?? decodeMathText(block.html);
|
package/dist/element.js
CHANGED
|
@@ -9,18 +9,25 @@ function parseTriBool(value) {
|
|
|
9
9
|
const CONFIG_ATTRS = [
|
|
10
10
|
"gfm-autolinks",
|
|
11
11
|
"gfm-alerts",
|
|
12
|
+
// Must mirror the `set(...)` map below: this list feeds `observedAttributes`,
|
|
13
|
+
// so an attribute missing here is read once at mount and then never reacts to
|
|
14
|
+
// a change. `gfm-tagfilter` was in the map but not in this list.
|
|
15
|
+
"gfm-tagfilter",
|
|
12
16
|
"gfm-footnotes",
|
|
13
17
|
"gfm-math",
|
|
14
18
|
"dir-auto",
|
|
19
|
+
"lenient-lists",
|
|
20
|
+
"soft-breaks",
|
|
15
21
|
"a11y",
|
|
16
|
-
"unsafe-html"
|
|
22
|
+
"unsafe-html",
|
|
23
|
+
"block-html"
|
|
17
24
|
];
|
|
18
25
|
function defineBrookMarkdown(tag = "brook-markdown") {
|
|
19
26
|
if (typeof customElements === "undefined") return;
|
|
20
27
|
if (customElements.get(tag)) return;
|
|
21
28
|
class BrookMarkdownElement extends HTMLElement {
|
|
22
29
|
static get observedAttributes() {
|
|
23
|
-
return ["markdown", "src", "component-tags", ...CONFIG_ATTRS];
|
|
30
|
+
return ["markdown", "src", "component-tags", "allow-schemes", ...CONFIG_ATTRS];
|
|
24
31
|
}
|
|
25
32
|
#client = null;
|
|
26
33
|
#ownsClient = false;
|
|
@@ -150,8 +157,11 @@ function defineBrookMarkdown(tag = "brook-markdown") {
|
|
|
150
157
|
set("gfm-footnotes", "gfmFootnotes");
|
|
151
158
|
set("gfm-math", "gfmMath");
|
|
152
159
|
set("dir-auto", "dirAuto");
|
|
160
|
+
set("lenient-lists", "lenientLists");
|
|
161
|
+
set("soft-breaks", "softBreaks");
|
|
153
162
|
set("a11y", "a11y");
|
|
154
163
|
set("unsafe-html", "unsafeHtml");
|
|
164
|
+
set("block-html", "blockHtml");
|
|
155
165
|
const tags = this.getAttribute("component-tags");
|
|
156
166
|
if (tags !== null) {
|
|
157
167
|
const list = tags.split(/[\s,]+/).filter(Boolean);
|
|
@@ -160,6 +170,14 @@ function defineBrookMarkdown(tag = "brook-markdown") {
|
|
|
160
170
|
any = true;
|
|
161
171
|
}
|
|
162
172
|
}
|
|
173
|
+
const schemes = this.getAttribute("allow-schemes");
|
|
174
|
+
if (schemes !== null) {
|
|
175
|
+
const list = schemes.split(/[\s,]+/).filter(Boolean);
|
|
176
|
+
if (list.length > 0) {
|
|
177
|
+
cfg.allowSchemes = list;
|
|
178
|
+
any = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
163
181
|
return any ? cfg : void 0;
|
|
164
182
|
}
|
|
165
183
|
// Lazily create the internal client from config attributes (self-owned).
|
package/dist/react.js
CHANGED
|
@@ -218,8 +218,9 @@ function blockKindProps(block, components) {
|
|
|
218
218
|
if (block.kind.type === "CodeBlock") {
|
|
219
219
|
props.text = data?.code ?? decodeCodeText(block.html);
|
|
220
220
|
props.language = data?.lang ?? "";
|
|
221
|
+
if (typeof data?.meta === "string") props.meta = data.meta;
|
|
221
222
|
if (typeof data?.code === "string") {
|
|
222
|
-
props.code = { lang: data.lang ?? null, code: data.code };
|
|
223
|
+
props.code = { lang: data.lang ?? null, meta: data.meta, code: data.code };
|
|
223
224
|
}
|
|
224
225
|
} else if (block.kind.type === "MathBlock") {
|
|
225
226
|
props.text = data?.latex ?? decodeMathText(block.html);
|
package/dist/server.d.ts
CHANGED
|
@@ -21,10 +21,21 @@ export declare function parseToBlocks(markdown: string, opts?: {
|
|
|
21
21
|
}): Block[];
|
|
22
22
|
/**
|
|
23
23
|
* Render a complete markdown string to an HTML string synchronously — no worker,
|
|
24
|
-
* no React. The
|
|
25
|
-
* For component dispatch / a `<BrookMarkdown>`-matching React
|
|
26
|
-
* `BrookMarkdownStatic` from `brookmd/server/react` with your
|
|
27
|
-
* renderer instead.
|
|
24
|
+
* no React. The per-block HTML joined into one document (XSS-safe with
|
|
25
|
+
* `unsafeHtml` off). For component dispatch / a `<BrookMarkdown>`-matching React
|
|
26
|
+
* tree, use `BrookMarkdownStatic` from `brookmd/server/react` with your
|
|
27
|
+
* framework's server renderer instead.
|
|
28
|
+
*
|
|
29
|
+
* **Document assembly** (WIRE.md §12): a block's `html` never ends with a
|
|
30
|
+
* newline — that terminator belongs to the document, not the block — so joining
|
|
31
|
+
* blocks inserts one `\n` between them, `cr()`-style: only when the previous
|
|
32
|
+
* block does not ALREADY end with one. A raw HTML block serializes its own
|
|
33
|
+
* trailing newline, so an unconditional join would double it. The document ends
|
|
34
|
+
* with a final newline. This is exactly what a reference CommonMark/GFM renderer
|
|
35
|
+
* emits, and what the spec harnesses measure byte-for-byte against.
|
|
36
|
+
*
|
|
37
|
+
* If you assemble `Block.html` yourself (rather than calling this), use the same
|
|
38
|
+
* rule.
|
|
28
39
|
*/
|
|
29
40
|
export declare function renderToString(markdown: string, opts?: {
|
|
30
41
|
config?: ParserConfig;
|
package/dist/server.js
CHANGED
|
@@ -40,6 +40,8 @@ function makeParser(config) {
|
|
|
40
40
|
p.setGfmFootnotes(config?.gfmFootnotes ?? false);
|
|
41
41
|
p.setGfmMath(config?.gfmMath ?? false);
|
|
42
42
|
p.setDirAuto(config?.dirAuto ?? false);
|
|
43
|
+
p.setLenientLists(config?.lenientLists ?? false);
|
|
44
|
+
p.setSoftBreaks(config?.softBreaks ?? false);
|
|
43
45
|
p.setA11y(config?.a11y ?? false);
|
|
44
46
|
p.setUnsafeHtml(config?.unsafeHtml ?? false);
|
|
45
47
|
p.setComponentTags(config?.componentTags ?? []);
|
|
@@ -49,6 +51,8 @@ function makeParser(config) {
|
|
|
49
51
|
config?.htmlAllowlist ?? [],
|
|
50
52
|
config?.dropHtmlTags ?? []
|
|
51
53
|
);
|
|
54
|
+
p.setBlockHtml(config?.blockHtml ?? false);
|
|
55
|
+
p.setAllowSchemes(config?.allowSchemes ?? []);
|
|
52
56
|
p.setBlockData(config?.blockData ?? false);
|
|
53
57
|
return p;
|
|
54
58
|
}
|
|
@@ -71,7 +75,13 @@ function parseToBlocks(markdown, opts) {
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
function renderToString(markdown, opts) {
|
|
74
|
-
|
|
78
|
+
let out = "";
|
|
79
|
+
for (const b of parseToBlocks(markdown, opts)) {
|
|
80
|
+
if (out.length > 0 && !out.endsWith("\n")) out += "\n";
|
|
81
|
+
out += b.html;
|
|
82
|
+
}
|
|
83
|
+
if (out.length > 0 && !out.endsWith("\n")) out += "\n";
|
|
84
|
+
return out;
|
|
75
85
|
}
|
|
76
86
|
export {
|
|
77
87
|
initBrook,
|
package/dist/types-core.d.ts
CHANGED
|
@@ -89,15 +89,24 @@ export interface HeadingData {
|
|
|
89
89
|
id: string;
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
|
-
* A CodeBlock's `kind.data`
|
|
93
|
-
* the
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* `
|
|
92
|
+
* A CodeBlock's `kind.data`. `lang` is the always-on info-string language (`null`
|
|
93
|
+
* for none) — the info string's first word; `meta` is the always-on REMAINDER of
|
|
94
|
+
* that same info string, trimmed (```` ```ts title="src/main.ts" ```` ⇒
|
|
95
|
+
* `lang: "ts"`, `meta: 'title="src/main.ts"'`), absent when the fence carried
|
|
96
|
+
* none. `code` is the opt-in DECODED source inside `<pre><code>…</code></pre>`
|
|
97
|
+
* (only present when `blockData` is on). Build a copy-to-clipboard string /
|
|
98
|
+
* re-highlight from `code` alone — no HTML re-parse, no entity-decode. When
|
|
99
|
+
* `blockData` is off, `code` is absent and `kind.data` is just `{ lang }` (plus
|
|
100
|
+
* `meta` if the fence had one), byte-identical to before.
|
|
101
|
+
*
|
|
102
|
+
* Both halves are the RAW info-string text (backslash escapes / entity references
|
|
103
|
+
* left undecoded), and only `lang` appears in the rendered HTML
|
|
104
|
+
* (`class="language-…" data-lang="…"`) — there is deliberately no `data-meta`
|
|
105
|
+
* attribute, so a filename header needs a `components.CodeBlock` override.
|
|
98
106
|
*/
|
|
99
107
|
export interface CodeBlockData {
|
|
100
108
|
lang: string | null;
|
|
109
|
+
meta?: string;
|
|
101
110
|
code?: string;
|
|
102
111
|
}
|
|
103
112
|
/**
|
|
@@ -117,6 +126,24 @@ export interface MathBlockData {
|
|
|
117
126
|
*/
|
|
118
127
|
export interface ListItemData {
|
|
119
128
|
html: string;
|
|
129
|
+
/**
|
|
130
|
+
* The item's DOCUMENT-ABSOLUTE source byte offset — the index, in the markdown
|
|
131
|
+
* fed so far, of the byte where this item's marker (`-`, `*`, `1.`, …) begins.
|
|
132
|
+
* Same origin as {@link Block.start}, and stable as the document grows (the
|
|
133
|
+
* parser's buffer is append-only), so `source.slice(item.start)` always begins
|
|
134
|
+
* at this item's marker. Use it to read or rewrite the item in place — e.g.
|
|
135
|
+
* find the task-list checkbox with your own `findTaskListMarkerOffset(source,
|
|
136
|
+
* item.start)` and flip `[ ]` ⇄ `[x]` in the original string.
|
|
137
|
+
*
|
|
138
|
+
* Present only when {@link ParserConfig.blockData} is on.
|
|
139
|
+
*
|
|
140
|
+
* KNOWN LIMITATION — absent for NESTED list items. A nested list is not a
|
|
141
|
+
* separate block: its items live inside the parent item's `html` and never
|
|
142
|
+
* reach the `items` channel, and the nested render runs against a synthesized
|
|
143
|
+
* de-indented string with no document offset. Nested items therefore carry no
|
|
144
|
+
* offset rather than a wrong one. Only top-level list items get a `start`.
|
|
145
|
+
*/
|
|
146
|
+
start?: number;
|
|
120
147
|
}
|
|
121
148
|
/**
|
|
122
149
|
* A List's `kind.data` when {@link ParserConfig.blockData} is on. `ordered` is the
|
|
@@ -243,6 +270,18 @@ export interface BlockComponentProps {
|
|
|
243
270
|
text?: string;
|
|
244
271
|
/** Info-string language — present for `CodeBlock` (from `kind.data.lang`). */
|
|
245
272
|
language?: string;
|
|
273
|
+
/**
|
|
274
|
+
* Info-string META — everything after the language word, trimmed (from
|
|
275
|
+
* `kind.data.meta`), e.g. `title="src/main.ts"` or a bare `src/main.ts`.
|
|
276
|
+
* Always-on like `language` (no `blockData` needed); `undefined` when the
|
|
277
|
+
* fence carried none. Deliberately absent from the rendered HTML, so render a
|
|
278
|
+
* filename header from this prop.
|
|
279
|
+
*
|
|
280
|
+
* While streaming it appears once it can no longer change — when the opening
|
|
281
|
+
* fence line is terminated by a newline, or at finalize — so a header never
|
|
282
|
+
* flickers through a half-typed `title="src/ma`.
|
|
283
|
+
*/
|
|
284
|
+
meta?: string;
|
|
246
285
|
/** Component tag name — present for `Component` blocks (from `kind.data.tag`). */
|
|
247
286
|
tag?: string;
|
|
248
287
|
/**
|
|
@@ -274,11 +313,12 @@ export interface BlockComponentProps {
|
|
|
274
313
|
heading?: HeadingData;
|
|
275
314
|
/**
|
|
276
315
|
* Structured code data — present for `CodeBlock` blocks when
|
|
277
|
-
* {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ lang,
|
|
278
|
-
* with `code` the DECODED source. Build a copy-to-clipboard string /
|
|
279
|
-
* from `code` — no HTML re-parse, no entity-decode. (`props.text` /
|
|
280
|
-
* carry the same source / lang and stay populated even when off,
|
|
281
|
-
* regex fallback.
|
|
316
|
+
* {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ lang, meta?,
|
|
317
|
+
* code }` with `code` the DECODED source. Build a copy-to-clipboard string /
|
|
318
|
+
* re-highlight from `code` — no HTML re-parse, no entity-decode. (`props.text` /
|
|
319
|
+
* `props.language` carry the same source / lang and stay populated even when off,
|
|
320
|
+
* via the HTML regex fallback; `props.meta` carries the same meta and is
|
|
321
|
+
* always-on, since it has no HTML form to fall back to.)
|
|
282
322
|
*/
|
|
283
323
|
code?: CodeBlockData;
|
|
284
324
|
/**
|
|
@@ -343,6 +383,28 @@ export interface ParserConfig {
|
|
|
343
383
|
* apps that render RTL or mixed-direction content.
|
|
344
384
|
*/
|
|
345
385
|
dirAuto?: boolean;
|
|
386
|
+
/**
|
|
387
|
+
* Lenient list indentation: a list marker followed by 6 or more columns of
|
|
388
|
+
* SPACE padding yields the item's text, where strict CommonMark (§5.2) keeps
|
|
389
|
+
* one column and renders the rest as an indented code block. Default false.
|
|
390
|
+
*
|
|
391
|
+
* Aimed at model output, which routinely over-indents after a bullet
|
|
392
|
+
* (`- const value = 1;`). Four cases stay strictly conformant: exactly
|
|
393
|
+
* 5 columns of padding, a fenced code block opened on the marker line itself,
|
|
394
|
+
* indented code that starts on a line AFTER the marker, and tab-padded
|
|
395
|
+
* markers (`-\t\tfoo`).
|
|
396
|
+
*/
|
|
397
|
+
lenientLists?: boolean;
|
|
398
|
+
/**
|
|
399
|
+
* Render a CommonMark SOFT line break (a bare `\n` inside inline content) as
|
|
400
|
+
* a `<br>` — the `remark-breaks` / "GitHub comment" convention, where one
|
|
401
|
+
* Enter is one visual line. Default false (strict CommonMark: a soft break is
|
|
402
|
+
* whitespace). Hard breaks (two trailing spaces, or a trailing `\`) are `<br>`
|
|
403
|
+
* either way, so enabling this only ADDS breaks — it never removes one. Chat
|
|
404
|
+
* UIs streaming model output usually want this on, since models emit single
|
|
405
|
+
* newlines expecting a visible break.
|
|
406
|
+
*/
|
|
407
|
+
softBreaks?: boolean;
|
|
346
408
|
/**
|
|
347
409
|
* Opt-in accessibility markup that deviates from strict GFM byte-output:
|
|
348
410
|
* wraps a task-list checkbox + its text in a `<label>` (programmatic
|
|
@@ -385,8 +447,8 @@ export interface ParserConfig {
|
|
|
385
447
|
* `input`, `svg`, …); a **non-empty** array renders only those tags (e.g.
|
|
386
448
|
* `["br","sub","sup"]`) and escapes the rest. Every rendered tag's attributes
|
|
387
449
|
* are sanitized (event handlers dropped, dangerous URL schemes → `#`), and HTML
|
|
388
|
-
* comments are dropped. Block-level raw HTML stays escaped
|
|
389
|
-
*
|
|
450
|
+
* comments are dropped. Block-level raw HTML stays escaped unless you also set
|
|
451
|
+
* {@link blockHtml}. Unset/omitted = off (raw HTML handling unchanged).
|
|
390
452
|
* Matching is case-insensitive. See also {@link dropHtmlTags}.
|
|
391
453
|
*/
|
|
392
454
|
htmlAllowlist?: string[];
|
|
@@ -397,6 +459,52 @@ export interface ParserConfig {
|
|
|
397
459
|
* raw-HTML sanitizer (see {@link htmlAllowlist}). Case-insensitive.
|
|
398
460
|
*/
|
|
399
461
|
dropHtmlTags?: string[];
|
|
462
|
+
/**
|
|
463
|
+
* Extend the safe raw-HTML sanitizer to **block-level** raw HTML, so a model
|
|
464
|
+
* emitting `<details><summary>…</summary>…</details>` on its own lines renders
|
|
465
|
+
* as real elements instead of an escaped code block. Only takes effect when
|
|
466
|
+
* the sanitizer is engaged ({@link htmlAllowlist} / {@link dropHtmlTags}); on
|
|
467
|
+
* its own it does nothing. Default false — existing sanitizer users keep
|
|
468
|
+
* escaped block HTML until they opt in.
|
|
469
|
+
*
|
|
470
|
+
* Scope is CommonMark HTML block **types 6 and 7**: a known block-level tag
|
|
471
|
+
* (`<details>`, `<div>`, `<table>`, …) or any other complete tag alone on its
|
|
472
|
+
* line. Types 1–5 stay escaped/dropped: type 1 is the raw-text family
|
|
473
|
+
* (`<script>`, `<pre>`, `<style>`, `<textarea>`) — a browser reads everything
|
|
474
|
+
* after such a tag as unparsed text, so a speculative mid-stream close is
|
|
475
|
+
* mXSS-prone — and types 2–5 (comments, PIs, CDATA, declarations) carry no
|
|
476
|
+
* renderable element. The tag allow/drop/dangerous decision and the hardened
|
|
477
|
+
* attribute policy are exactly the inline sanitizer's.
|
|
478
|
+
*
|
|
479
|
+
* While the block streams, still-open elements get **speculative closers**, so
|
|
480
|
+
* what the reader has seen so far is a complete tree at every append; a
|
|
481
|
+
* half-arrived tag stays invisible until it completes. Markdown *inside* the
|
|
482
|
+
* HTML is not parsed (the body is text + tags).
|
|
483
|
+
*/
|
|
484
|
+
blockHtml?: boolean;
|
|
485
|
+
/**
|
|
486
|
+
* Opt-in **un-blocklist** for URL schemes that brookmd blocks by default.
|
|
487
|
+
* Bare scheme names, **without** the colon (`["file"]`), matched
|
|
488
|
+
* case-insensitively. Empty/omitted = the built-in policy is unchanged.
|
|
489
|
+
*
|
|
490
|
+
* This never *restricts* anything — it is not a general allowlist. Schemes
|
|
491
|
+
* outside the built-in blocklist (`vscode:`, `ftp:`, `mailto:`, …) already
|
|
492
|
+
* render today and are unaffected. The only tier it can reach is the
|
|
493
|
+
* overridable-blocked one, currently just `file:`.
|
|
494
|
+
*
|
|
495
|
+
* The script-executing tier — `javascript:`, `vbscript:`, `data:text/html`,
|
|
496
|
+
* `data:text/javascript`, and the scriptable `data:` media types
|
|
497
|
+
* (`data:image/svg`, `data:application/xhtml`, …) — is **non-overridable**:
|
|
498
|
+
* listing one here is a silent no-op, exactly as allowlisting `<script>`
|
|
499
|
+
* cannot re-enable it via {@link htmlAllowlist}.
|
|
500
|
+
*
|
|
501
|
+
* Only enable `file:` in a host that intercepts link clicks instead of
|
|
502
|
+
* navigating (an Electron / extension UI that opens the path in an editor);
|
|
503
|
+
* local-resource disclosure then becomes the embedder's responsibility.
|
|
504
|
+
* Applies uniformly to links, URI autolinks, images, and sanitized URL
|
|
505
|
+
* attributes.
|
|
506
|
+
*/
|
|
507
|
+
allowSchemes?: string[];
|
|
400
508
|
/**
|
|
401
509
|
* Opt-in structured table data. When on, a `Table` block's `kind.data` is
|
|
402
510
|
* populated with `{ headers, rows, aligns }` (each cell `{ text, html }`) so a
|
package/dist/wasm/README.md
CHANGED
|
@@ -52,13 +52,13 @@ entirely:
|
|
|
52
52
|
|
|
53
53
|
```toml
|
|
54
54
|
[dependencies]
|
|
55
|
-
brookmd-core = { version = "0.
|
|
55
|
+
brookmd-core = { version = "0.25", default-features = false }
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
## Wire format
|
|
59
59
|
|
|
60
60
|
Blocks and patches serialize to a stable, language-agnostic JSON wire format —
|
|
61
|
-
see [WIRE.md](WIRE.md) (wire contract v1.
|
|
61
|
+
see [WIRE.md](WIRE.md) (wire contract v1.3.0). Native consumers can produce the
|
|
62
62
|
same bytes as the WASM/JS boundary via `wire::patch_to_json` / `wire::blocks_to_json`.
|
|
63
63
|
|
|
64
64
|
Contract v1.2.0 adds the opt-in **wire delta mode**
|
|
@@ -38,6 +38,18 @@ export class BrookParser {
|
|
|
38
38
|
* to table header cells. Off by default (conformance output unchanged).
|
|
39
39
|
*/
|
|
40
40
|
setA11y(on: boolean): void;
|
|
41
|
+
/**
|
|
42
|
+
* Un-block specific URL schemes that are blocked by DEFAULT — bare scheme
|
|
43
|
+
* names without the colon (`["file"]`), matched case-insensitively. Empty
|
|
44
|
+
* by default (built-in policy unchanged). This never RESTRICTS anything:
|
|
45
|
+
* schemes outside the built-in blocklist (`vscode:`, `ftp:`, …) already
|
|
46
|
+
* pass. The script-executing tier (`javascript:`, `vbscript:`,
|
|
47
|
+
* `data:text/html`, `data:text/javascript`, scriptable `data:` media types)
|
|
48
|
+
* is non-overridable — listing one here is a no-op. Only enable `file:` in
|
|
49
|
+
* a host that intercepts link clicks instead of navigating (Electron,
|
|
50
|
+
* extensions); local-resource disclosure is then the embedder's call.
|
|
51
|
+
*/
|
|
52
|
+
setAllowSchemes(schemes: string[]): void;
|
|
41
53
|
/**
|
|
42
54
|
* Opt-in structured `kind.data` channel for Table blocks: a Table then
|
|
43
55
|
* carries `{ headers, rows, aligns }` (per-cell `{ text, html }`) so a
|
|
@@ -46,6 +58,17 @@ export class BrookParser {
|
|
|
46
58
|
* as `{"type":"Table"}` (no `data` key) and output is byte-identical.
|
|
47
59
|
*/
|
|
48
60
|
setBlockData(on: boolean): void;
|
|
61
|
+
/**
|
|
62
|
+
* Extend the safe raw-HTML sanitizer to BLOCK-level raw HTML — a
|
|
63
|
+
* `<details><summary>…` block renders as real elements instead of escaping
|
|
64
|
+
* into a code block. Takes effect ONLY when the sanitizer is engaged
|
|
65
|
+
* (`setHtmlSanitize`), and only for CommonMark HTML block types 6 and 7.
|
|
66
|
+
* Types 1–5 (`<script>`/`<pre>`/`<style>`/`<textarea>`, comments, PIs,
|
|
67
|
+
* CDATA, declarations) stay escaped/dropped. Still-open elements get
|
|
68
|
+
* speculative closers while the block streams, so the emitted HTML is a
|
|
69
|
+
* complete tree at every prefix. Off by default (output unchanged).
|
|
70
|
+
*/
|
|
71
|
+
setBlockHtml(on: boolean): void;
|
|
49
72
|
/**
|
|
50
73
|
* Set the opt-in component-tag allowlist (e.g. `["Thinking", "Callout"]`).
|
|
51
74
|
* A `<Tag>…</Tag>` whose name is listed renders as a component whose inner
|
|
@@ -107,6 +130,23 @@ export class BrookParser {
|
|
|
107
130
|
* cells, and list items. Empty by default (inline output unchanged).
|
|
108
131
|
*/
|
|
109
132
|
setInlineComponentTags(tags: string[]): void;
|
|
133
|
+
/**
|
|
134
|
+
* Lenient list indentation: a list marker followed by 6+ columns of SPACE
|
|
135
|
+
* padding yields the item's text instead of an indented code block. Off by
|
|
136
|
+
* default (strict CommonMark §5.2). Useful for model output, which routinely
|
|
137
|
+
* over-indents after a bullet. Exactly-5-column padding, a fence opened on
|
|
138
|
+
* the marker line, indented code starting on a later line, and tab-padded
|
|
139
|
+
* markers all stay strictly conformant.
|
|
140
|
+
*/
|
|
141
|
+
setLenientLists(on: boolean): void;
|
|
142
|
+
/**
|
|
143
|
+
* Render a CommonMark SOFT line break (a bare `\n` in inline content) as a
|
|
144
|
+
* `<br>` — the `remark-breaks` convention, where one Enter is one visual
|
|
145
|
+
* line. Off by default (strict CommonMark: a soft break is whitespace).
|
|
146
|
+
* Hard breaks (two trailing spaces / trailing `\`) are `<br>` either way,
|
|
147
|
+
* so turning this on only ADDS breaks; it never removes one.
|
|
148
|
+
*/
|
|
149
|
+
setSoftBreaks(on: boolean): void;
|
|
110
150
|
/**
|
|
111
151
|
* Enable or disable raw-HTML pass-through. Default off. Do not enable
|
|
112
152
|
* when rendering untrusted input — bypasses XSS protection.
|
|
@@ -135,7 +175,9 @@ export interface InitOutput {
|
|
|
135
175
|
readonly brookparser_new: () => number;
|
|
136
176
|
readonly brookparser_retainedBytes: (a: number) => number;
|
|
137
177
|
readonly brookparser_setA11y: (a: number, b: number) => void;
|
|
178
|
+
readonly brookparser_setAllowSchemes: (a: number, b: number, c: number) => void;
|
|
138
179
|
readonly brookparser_setBlockData: (a: number, b: number) => void;
|
|
180
|
+
readonly brookparser_setBlockHtml: (a: number, b: number) => void;
|
|
139
181
|
readonly brookparser_setComponentTags: (a: number, b: number, c: number) => void;
|
|
140
182
|
readonly brookparser_setDirAuto: (a: number, b: number) => void;
|
|
141
183
|
readonly brookparser_setGfmAlerts: (a: number, b: number) => void;
|
|
@@ -145,6 +187,8 @@ export interface InitOutput {
|
|
|
145
187
|
readonly brookparser_setGfmTagfilter: (a: number, b: number) => void;
|
|
146
188
|
readonly brookparser_setHtmlSanitize: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
|
147
189
|
readonly brookparser_setInlineComponentTags: (a: number, b: number, c: number) => void;
|
|
190
|
+
readonly brookparser_setLenientLists: (a: number, b: number) => void;
|
|
191
|
+
readonly brookparser_setSoftBreaks: (a: number, b: number) => void;
|
|
148
192
|
readonly brookparser_setUnsafeHtml: (a: number, b: number) => void;
|
|
149
193
|
readonly brookparser_setWireDelta: (a: number, b: number) => void;
|
|
150
194
|
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
@@ -138,6 +138,23 @@ export class BrookParser {
|
|
|
138
138
|
setA11y(on) {
|
|
139
139
|
wasm.brookparser_setA11y(this.__wbg_ptr, on);
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Un-block specific URL schemes that are blocked by DEFAULT — bare scheme
|
|
143
|
+
* names without the colon (`["file"]`), matched case-insensitively. Empty
|
|
144
|
+
* by default (built-in policy unchanged). This never RESTRICTS anything:
|
|
145
|
+
* schemes outside the built-in blocklist (`vscode:`, `ftp:`, …) already
|
|
146
|
+
* pass. The script-executing tier (`javascript:`, `vbscript:`,
|
|
147
|
+
* `data:text/html`, `data:text/javascript`, scriptable `data:` media types)
|
|
148
|
+
* is non-overridable — listing one here is a no-op. Only enable `file:` in
|
|
149
|
+
* a host that intercepts link clicks instead of navigating (Electron,
|
|
150
|
+
* extensions); local-resource disclosure is then the embedder's call.
|
|
151
|
+
* @param {string[]} schemes
|
|
152
|
+
*/
|
|
153
|
+
setAllowSchemes(schemes) {
|
|
154
|
+
const ptr0 = passArrayJsValueToWasm0(schemes, wasm.__wbindgen_export);
|
|
155
|
+
const len0 = WASM_VECTOR_LEN;
|
|
156
|
+
wasm.brookparser_setAllowSchemes(this.__wbg_ptr, ptr0, len0);
|
|
157
|
+
}
|
|
141
158
|
/**
|
|
142
159
|
* Opt-in structured `kind.data` channel for Table blocks: a Table then
|
|
143
160
|
* carries `{ headers, rows, aligns }` (per-cell `{ text, html }`) so a
|
|
@@ -149,6 +166,20 @@ export class BrookParser {
|
|
|
149
166
|
setBlockData(on) {
|
|
150
167
|
wasm.brookparser_setBlockData(this.__wbg_ptr, on);
|
|
151
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Extend the safe raw-HTML sanitizer to BLOCK-level raw HTML — a
|
|
171
|
+
* `<details><summary>…` block renders as real elements instead of escaping
|
|
172
|
+
* into a code block. Takes effect ONLY when the sanitizer is engaged
|
|
173
|
+
* (`setHtmlSanitize`), and only for CommonMark HTML block types 6 and 7.
|
|
174
|
+
* Types 1–5 (`<script>`/`<pre>`/`<style>`/`<textarea>`, comments, PIs,
|
|
175
|
+
* CDATA, declarations) stay escaped/dropped. Still-open elements get
|
|
176
|
+
* speculative closers while the block streams, so the emitted HTML is a
|
|
177
|
+
* complete tree at every prefix. Off by default (output unchanged).
|
|
178
|
+
* @param {boolean} on
|
|
179
|
+
*/
|
|
180
|
+
setBlockHtml(on) {
|
|
181
|
+
wasm.brookparser_setBlockHtml(this.__wbg_ptr, on);
|
|
182
|
+
}
|
|
152
183
|
/**
|
|
153
184
|
* Set the opt-in component-tag allowlist (e.g. `["Thinking", "Callout"]`).
|
|
154
185
|
* A `<Tag>…</Tag>` whose name is listed renders as a component whose inner
|
|
@@ -247,6 +278,29 @@ export class BrookParser {
|
|
|
247
278
|
const len0 = WASM_VECTOR_LEN;
|
|
248
279
|
wasm.brookparser_setInlineComponentTags(this.__wbg_ptr, ptr0, len0);
|
|
249
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Lenient list indentation: a list marker followed by 6+ columns of SPACE
|
|
283
|
+
* padding yields the item's text instead of an indented code block. Off by
|
|
284
|
+
* default (strict CommonMark §5.2). Useful for model output, which routinely
|
|
285
|
+
* over-indents after a bullet. Exactly-5-column padding, a fence opened on
|
|
286
|
+
* the marker line, indented code starting on a later line, and tab-padded
|
|
287
|
+
* markers all stay strictly conformant.
|
|
288
|
+
* @param {boolean} on
|
|
289
|
+
*/
|
|
290
|
+
setLenientLists(on) {
|
|
291
|
+
wasm.brookparser_setLenientLists(this.__wbg_ptr, on);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Render a CommonMark SOFT line break (a bare `\n` in inline content) as a
|
|
295
|
+
* `<br>` — the `remark-breaks` convention, where one Enter is one visual
|
|
296
|
+
* line. Off by default (strict CommonMark: a soft break is whitespace).
|
|
297
|
+
* Hard breaks (two trailing spaces / trailing `\`) are `<br>` either way,
|
|
298
|
+
* so turning this on only ADDS breaks; it never removes one.
|
|
299
|
+
* @param {boolean} on
|
|
300
|
+
*/
|
|
301
|
+
setSoftBreaks(on) {
|
|
302
|
+
wasm.brookparser_setSoftBreaks(this.__wbg_ptr, on);
|
|
303
|
+
}
|
|
250
304
|
/**
|
|
251
305
|
* Enable or disable raw-HTML pass-through. Default off. Do not enable
|
|
252
306
|
* when rendering untrusted input — bypasses XSS protection.
|
|
Binary file
|
|
@@ -9,7 +9,9 @@ export const brookparser_finalize: (a: number, b: number) => void;
|
|
|
9
9
|
export const brookparser_new: () => number;
|
|
10
10
|
export const brookparser_retainedBytes: (a: number) => number;
|
|
11
11
|
export const brookparser_setA11y: (a: number, b: number) => void;
|
|
12
|
+
export const brookparser_setAllowSchemes: (a: number, b: number, c: number) => void;
|
|
12
13
|
export const brookparser_setBlockData: (a: number, b: number) => void;
|
|
14
|
+
export const brookparser_setBlockHtml: (a: number, b: number) => void;
|
|
13
15
|
export const brookparser_setComponentTags: (a: number, b: number, c: number) => void;
|
|
14
16
|
export const brookparser_setDirAuto: (a: number, b: number) => void;
|
|
15
17
|
export const brookparser_setGfmAlerts: (a: number, b: number) => void;
|
|
@@ -19,6 +21,8 @@ export const brookparser_setGfmMath: (a: number, b: number) => void;
|
|
|
19
21
|
export const brookparser_setGfmTagfilter: (a: number, b: number) => void;
|
|
20
22
|
export const brookparser_setHtmlSanitize: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
|
21
23
|
export const brookparser_setInlineComponentTags: (a: number, b: number, c: number) => void;
|
|
24
|
+
export const brookparser_setLenientLists: (a: number, b: number) => void;
|
|
25
|
+
export const brookparser_setSoftBreaks: (a: number, b: number) => void;
|
|
22
26
|
export const brookparser_setUnsafeHtml: (a: number, b: number) => void;
|
|
23
27
|
export const brookparser_setWireDelta: (a: number, b: number) => void;
|
|
24
28
|
export const __wbindgen_export: (a: number, b: number) => number;
|
package/dist/worker.js
CHANGED
|
@@ -15,6 +15,8 @@ const core = new WorkerCore({
|
|
|
15
15
|
p.setGfmFootnotes(c?.gfmFootnotes ?? false);
|
|
16
16
|
p.setGfmMath(c?.gfmMath ?? false);
|
|
17
17
|
p.setDirAuto(c?.dirAuto ?? false);
|
|
18
|
+
p.setLenientLists(c?.lenientLists ?? false);
|
|
19
|
+
p.setSoftBreaks(c?.softBreaks ?? false);
|
|
18
20
|
p.setA11y(c?.a11y ?? false);
|
|
19
21
|
p.setUnsafeHtml(c?.unsafeHtml ?? false);
|
|
20
22
|
p.setComponentTags(c?.componentTags ?? []);
|
|
@@ -24,6 +26,8 @@ const core = new WorkerCore({
|
|
|
24
26
|
c?.htmlAllowlist ?? [],
|
|
25
27
|
c?.dropHtmlTags ?? []
|
|
26
28
|
);
|
|
29
|
+
p.setBlockHtml(c?.blockHtml ?? false);
|
|
30
|
+
p.setAllowSchemes(c?.allowSchemes ?? []);
|
|
27
31
|
p.setBlockData(c?.blockData ?? false);
|
|
28
32
|
p.setWireDelta(true);
|
|
29
33
|
return p;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brookmd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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
|
"sideEffects": ["./dist/worker.js", "./dist/styles.css"],
|