brookmd 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +1229 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1265 -0
  4. package/dist/block-props.d.ts +18 -0
  5. package/dist/block-props.js +75 -0
  6. package/dist/client.d.ts +370 -0
  7. package/dist/client.js +754 -0
  8. package/dist/decorate.d.ts +24 -0
  9. package/dist/decorate.js +71 -0
  10. package/dist/dom.d.ts +130 -0
  11. package/dist/dom.js +627 -0
  12. package/dist/element.d.ts +20 -0
  13. package/dist/element.js +288 -0
  14. package/dist/hi.d.ts +12 -0
  15. package/dist/hi.js +215 -0
  16. package/dist/html-to-react.d.ts +61 -0
  17. package/dist/html-to-react.js +338 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.js +18 -0
  20. package/dist/morph.d.ts +28 -0
  21. package/dist/morph.js +166 -0
  22. package/dist/react.d.ts +236 -0
  23. package/dist/react.js +539 -0
  24. package/dist/renderers/CodeBlock.d.ts +7 -0
  25. package/dist/renderers/CodeBlock.js +75 -0
  26. package/dist/renderers/Math.d.ts +14 -0
  27. package/dist/renderers/Math.js +15 -0
  28. package/dist/renderers/Mermaid.d.ts +13 -0
  29. package/dist/renderers/Mermaid.js +15 -0
  30. package/dist/server-react.d.ts +32 -0
  31. package/dist/server-react.js +48 -0
  32. package/dist/server.d.ts +31 -0
  33. package/dist/server.js +82 -0
  34. package/dist/solid.d.ts +104 -0
  35. package/dist/solid.js +54 -0
  36. package/dist/styles.css +188 -0
  37. package/dist/svelte.d.ts +80 -0
  38. package/dist/svelte.js +59 -0
  39. package/dist/types-core.d.ts +436 -0
  40. package/dist/types-core.js +0 -0
  41. package/dist/types-react.d.ts +13 -0
  42. package/dist/types-react.js +0 -0
  43. package/dist/types.d.ts +2 -0
  44. package/dist/types.js +2 -0
  45. package/dist/url-safety.d.ts +12 -0
  46. package/dist/url-safety.js +45 -0
  47. package/dist/vue.d.ts +94 -0
  48. package/dist/vue.js +79 -0
  49. package/dist/wasm/LICENSE +21 -0
  50. package/dist/wasm/README.md +71 -0
  51. package/dist/wasm/brook_md_core.d.ts +166 -0
  52. package/dist/wasm/brook_md_core.js +512 -0
  53. package/dist/wasm/brook_md_core_bg.wasm +0 -0
  54. package/dist/wasm/brook_md_core_bg.wasm.d.ts +26 -0
  55. package/dist/worker-core.d.ts +65 -0
  56. package/dist/worker-core.js +155 -0
  57. package/dist/worker.d.ts +1 -0
  58. package/dist/worker.js +49 -0
  59. package/package.json +87 -0
@@ -0,0 +1,436 @@
1
+ export type BlockKindTag = "Paragraph" | "Heading" | "CodeBlock" | "MathBlock" | "Mermaid" | "List" | "Blockquote" | "Alert" | "Table" | "Rule" | "Html" | "Component";
2
+ export interface BlockKind {
3
+ type: BlockKindTag;
4
+ data?: unknown;
5
+ }
6
+ /**
7
+ * The node type a {@link Decorator} (or `wrapLink`) builds. Kept `unknown` here
8
+ * so this framework-neutral types module stays React-free: the React binding
9
+ * treats it as `ReactNode`, the DOM binding (`brookmd/dom`) as `Node | string`.
10
+ */
11
+ export type BrookNode = unknown;
12
+ /**
13
+ * Wrap or replace matched inline **text** while streaming, in O(n). A decorator
14
+ * runs POST-PARSE on real inline TEXT nodes only (after the core renders a block
15
+ * to HTML and the walker parses it), once per committed block — so it never sees
16
+ * URLs, code, or markup, and a value split by inline markup (e.g.
17
+ * `$2.<em>5</em>B`) is two text nodes and won't match across them.
18
+ *
19
+ * **Trusted surface (read this).** A decorator's `replace` output is spliced
20
+ * directly into the render tree and does **NOT** pass through brookmd's attribute
21
+ * sanitizer (that only runs on attributes the trusted core emitted). React and
22
+ * the DOM both happily render a `javascript:` href. Treat `decorators` exactly
23
+ * like `components`: only build trusted nodes, and route any link href through
24
+ * the exported `safeUrl` (or use the `wrapLink` helper, which does it for you).
25
+ *
26
+ * **Stability matters (the #1 footgun).** Pass a HOISTED / memoized array — a
27
+ * fresh `decorators` identity every render busts the per-block memo, so every
28
+ * committed block re-parses and re-decorates on every patch (O(n²)). The React
29
+ * binding emits a one-time dev warning if the identity changes.
30
+ */
31
+ export interface Decorator {
32
+ /** Tested against each inline TEXT node's string only (never URLs/code/markup). */
33
+ match: RegExp | string;
34
+ /** PURE fn building the replacement for ONE match. Returns framework nodes. */
35
+ replace: (matchText: string, groups: string[]) => BrookNode;
36
+ /** Ancestor tags to skip. Default `['a','code','pre','kbd']`. */
37
+ skipInside?: string[];
38
+ }
39
+ /**
40
+ * Rewrite a URL attribute (`href`/`src`/`poster`) as a block renders — e.g. to
41
+ * proxy images or add UTM params. Applied O(1) per attribute. The renderer
42
+ * re-sanitizes the OUTPUT (`safeUrl(urlTransform(safeUrl(value)))`), so a buggy
43
+ * or hostile transform can never emit a `javascript:` / `data:text/html` URL
44
+ * that reaches the DOM. Like `decorators`, pass a HOISTED / memoized function so
45
+ * the per-block memo holds.
46
+ */
47
+ export type UrlTransform = (url: string, ctx: {
48
+ tag: string;
49
+ attr: "href" | "src" | "poster";
50
+ }) => string;
51
+ /** Column alignment from the `|:--|:-:|--:|` delimiter row; `null` = unset. */
52
+ export type Align = "left" | "center" | "right" | null;
53
+ /**
54
+ * One table cell as STRUCTURED DATA (opt-in via {@link ParserConfig.blockData}).
55
+ * `text` is the inline-stripped plaintext — sort/filter/CSV/chart from DATA,
56
+ * with no HTML re-parse. `html` is the inline-rendered display markup, byte-for-
57
+ * byte the inline content inside the matching `<td>`/`<th>` of `block.html`.
58
+ */
59
+ export interface TableCell {
60
+ text: string;
61
+ html: string;
62
+ }
63
+ /**
64
+ * A Table block's `kind.data` when {@link ParserConfig.blockData} is on. Lets a
65
+ * consumer build a sort/filter/transpose/chart/CSV toolbar from DATA alone —
66
+ * no HAST tree, no HTML re-parse. `aligns[i]` is column `i`'s alignment.
67
+ */
68
+ export interface TableData {
69
+ headers: TableCell[];
70
+ rows: TableCell[][];
71
+ aligns: Align[];
72
+ }
73
+ /**
74
+ * A Heading block's `kind.data` when {@link ParserConfig.blockData} is on. Lets a
75
+ * consumer build a table of contents — nested by `level`, anchored by `id` — from
76
+ * DATA alone, with no HTML re-parse. `text` is the inline-stripped plaintext (the
77
+ * heading rendered to plain text, e.g. `## **Bold** & x` → `"Bold & x"`); `id` is
78
+ * a GitHub-style anchor slug of that text (`"bold-x"`) for `#`-links. When
79
+ * `blockData` is off, a Heading's `kind.data` is instead the bare level `number`
80
+ * (byte-identical to before), so consumers reading `kind.data` must accept the
81
+ * `number | HeadingData` union.
82
+ *
83
+ * v1: duplicate heading texts produce identical slugs (no document-wide dedup
84
+ * counter yet) — give same-named headings distinct text if unique anchors matter.
85
+ */
86
+ export interface HeadingData {
87
+ level: number;
88
+ text: string;
89
+ id: string;
90
+ }
91
+ /**
92
+ * A CodeBlock's `kind.data` when {@link ParserConfig.blockData} is on. `lang` is
93
+ * the always-on info-string language (`null` for none); `code` is the opt-in
94
+ * DECODED source inside `<pre><code>…</code></pre>` (only present when `blockData`
95
+ * is on). Build a copy-to-clipboard string / re-highlight from `code` alone — no
96
+ * HTML re-parse, no entity-decode. When `blockData` is off, `code` is absent and
97
+ * `kind.data` is just `{ lang }`, byte-identical to before.
98
+ */
99
+ export interface CodeBlockData {
100
+ lang: string | null;
101
+ code?: string;
102
+ }
103
+ /**
104
+ * A MathBlock's `kind.data` when {@link ParserConfig.blockData} is on. `latex` is
105
+ * the DECODED LaTeX source (the display-math body, entity-decoded). Re-render with
106
+ * KaTeX from `latex` alone — no HTML re-parse. When `blockData` is off, a
107
+ * MathBlock has no `kind.data` at all (byte-identical to before).
108
+ */
109
+ export interface MathBlockData {
110
+ latex: string;
111
+ }
112
+ /**
113
+ * One list item in {@link ListData.items}. `html` is the inline-rendered inner
114
+ * HTML of the item's `<li>` (byte-identical to the content between the matching
115
+ * `<li…>`/`</li>` in `block.html`), so a keyed renderer can stamp one node per
116
+ * item and reuse the unchanged items while the list streams.
117
+ */
118
+ export interface ListItemData {
119
+ html: string;
120
+ }
121
+ /**
122
+ * A List's `kind.data` when {@link ParserConfig.blockData} is on. `ordered` is the
123
+ * always-on flag; `start` is the opt-in ordered-list start number (the `start="N"`
124
+ * HTML attribute; `1` for an unordered list), only present when `blockData` is on.
125
+ * `items` carries each item's inner `<li>` HTML — present (and non-empty) only when
126
+ * `blockData` is on — so a keyed renderer can re-render only the items that changed
127
+ * since the last patch instead of the whole list's HTML. Renumber / continue a
128
+ * split list from `start` alone — no HTML re-parse. When `blockData` is off, `start`
129
+ * and `items` are absent and `kind.data` is just `{ ordered }`, byte-identical.
130
+ */
131
+ export interface ListData {
132
+ ordered: boolean;
133
+ start?: number;
134
+ items?: ListItemData[];
135
+ }
136
+ /**
137
+ * One inner sub-block of a `Blockquote` / `Alert` as STRUCTURED DATA (opt-in via
138
+ * {@link ParserConfig.blockData}). `html` is that sub-block's pre-rendered display
139
+ * markup (e.g. `<p>…</p>`), byte-for-byte the matching fragment inside the
140
+ * container's `block.html` wrapper.
141
+ */
142
+ export interface NestedBlock {
143
+ html: string;
144
+ }
145
+ /**
146
+ * A `Blockquote`'s `kind.data` (and the `nested` carrier inside an `Alert`'s data)
147
+ * when {@link ParserConfig.blockData} is on. `nested` is the ordered list of the
148
+ * container's inner sub-blocks, each as its own pre-rendered HTML. A
149
+ * `components.Blockquote` / `components.Alert` override can render these KEYED (one
150
+ * node per entry) so that while the container streams only its last (open) inner
151
+ * block re-renders each tick — committed inner blocks have stable HTML and memoize.
152
+ * When `blockData` is off, a Blockquote has no `kind.data` and an Alert's is just
153
+ * `{ kind }` (byte-identical to before).
154
+ */
155
+ export interface ContainerData {
156
+ nested: NestedBlock[];
157
+ }
158
+ export interface Block {
159
+ id: number;
160
+ kind: BlockKind;
161
+ start: number;
162
+ end: number;
163
+ html: string;
164
+ open: boolean;
165
+ speculative: boolean;
166
+ }
167
+ export interface Patch {
168
+ newly_committed: Block[];
169
+ active: Block[];
170
+ }
171
+ /**
172
+ * Per-block render-churn sample passed to an {@link RenderMetricsHook}. Lets you
173
+ * measure how often each block actually re-renders / rebuilds (committed blocks
174
+ * memo-skip, so they fire exactly once; the streaming tail fires per patch).
175
+ */
176
+ export interface RenderMetrics {
177
+ /** How many times THIS block has actually rendered/rebuilt so far (≥ 1). */
178
+ renderCount: number;
179
+ /** How many times this block's `speculative` flag flipped between renders. */
180
+ speculativeToggleCount: number;
181
+ /** Wall-clock duration of this render's body in ms (0 if `performance` absent). */
182
+ lastRenderMs: number;
183
+ /** The block's kind (`"Paragraph"`, `"CodeBlock"`, …). */
184
+ kind: string;
185
+ }
186
+ /**
187
+ * Optional observability probe. When supplied to the React renderer (the
188
+ * `onRenderMetrics` prop) or the DOM renderer ({@link MountOptions.onRenderMetrics}),
189
+ * it fires once per ACTUAL render/rebuild of a block — never for a committed
190
+ * block that memo-skips. Zero overhead when absent (no counters advance, the hook
191
+ * path is never entered).
192
+ */
193
+ export type RenderMetricsHook = (blockId: number, m: RenderMetrics) => void;
194
+ /** Props passed to a block-kind override (e.g. `components.CodeBlock`). */
195
+ export interface BlockComponentProps {
196
+ /** The full parsed block, including `kind` (with `kind.data`) and offsets. */
197
+ block: Block;
198
+ /**
199
+ * Rendered, XSS-safe HTML for this block. For `Component` blocks this is the
200
+ * **inner** rendered-markdown HTML (not the `<tag>…</tag>` wrapper). NOTE: a
201
+ * `Component` override that ignores both `html` and `children` renders empty —
202
+ * use {@link children} (the easy path) or `dangerouslySetInnerHTML={{__html:
203
+ * html}}`.
204
+ */
205
+ html: string;
206
+ /**
207
+ * React only: this block's inner content already parsed to a React node tree
208
+ * (markdown rendered, nested tag/inline-component overrides applied). For a
209
+ * `Component` block it is the inner markdown — render it directly
210
+ * (`return <Chip {...attrs}>{children}</Chip>`) instead of dangerously setting
211
+ * `html`. Populated by `<BrookMarkdown>` / `<BrookMarkdownStatic>` when a
212
+ * `components` map is supplied; DOM and other bindings leave it `undefined`
213
+ * (they consume `html`). Typed `unknown` to keep this surface framework-neutral
214
+ * — cast to `ReactNode` in a React override.
215
+ */
216
+ children?: unknown;
217
+ /** True while the block is still streaming (its HTML may still change). */
218
+ open: boolean;
219
+ /** True if the block was closed speculatively and may yet be revised. */
220
+ speculative: boolean;
221
+ /** Decoded source text — present for `CodeBlock` / `MathBlock`. */
222
+ text?: string;
223
+ /** Info-string language — present for `CodeBlock` (from `kind.data.lang`). */
224
+ language?: string;
225
+ /** Component tag name — present for `Component` blocks (from `kind.data.tag`). */
226
+ tag?: string;
227
+ /**
228
+ * Sanitized attributes — present for `Component` blocks. The name-form depends
229
+ * on the consumer: the JSX renderer maps `class`→`className`/`for`→`htmlFor`
230
+ * so `{...attrs}` spreads cleanly onto an element; the DOM renderer keeps the
231
+ * literal HTML names (`class`/`for`) because it applies them via
232
+ * `setAttribute`. For `Component` blocks, `html` is the **inner**
233
+ * rendered-markdown HTML (not the `<tag>…</tag>` wrapper), so an override can
234
+ * wrap it itself.
235
+ */
236
+ attrs?: Record<string, string>;
237
+ /**
238
+ * Structured table data — present for `Table` blocks when
239
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). Equivalent to
240
+ * `block.kind.data`, given a typed, documented name. `{ headers, rows, aligns }`
241
+ * with each cell carrying `text` (plaintext, for sort/filter/CSV/chart) and
242
+ * `html` (display). Build a sort/filter/transpose/chart/CSV toolbar from DATA —
243
+ * no HTML re-parse, no HAST tree.
244
+ */
245
+ table?: TableData;
246
+ /**
247
+ * Structured heading data — present for `Heading` blocks when
248
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ level, text,
249
+ * id }` with `text` the inline-stripped plaintext and `id` a GitHub-style anchor
250
+ * slug. Build a table of contents (nested by `level`, anchored by `id`) from
251
+ * DATA — no HTML re-parse.
252
+ */
253
+ heading?: HeadingData;
254
+ /**
255
+ * Structured code data — present for `CodeBlock` blocks when
256
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ lang, code }`
257
+ * with `code` the DECODED source. Build a copy-to-clipboard string / re-highlight
258
+ * from `code` — no HTML re-parse, no entity-decode. (`props.text` / `props.language`
259
+ * carry the same source / lang and stay populated even when off, via the HTML
260
+ * regex fallback.)
261
+ */
262
+ code?: CodeBlockData;
263
+ /**
264
+ * Structured math data — present for `MathBlock` blocks when
265
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ latex }` — the
266
+ * DECODED LaTeX source. Re-render with KaTeX from `latex` — no HTML re-parse.
267
+ * (`props.text` carries the same source and stays populated even when off, via
268
+ * the HTML regex fallback.)
269
+ */
270
+ math?: MathBlockData;
271
+ /**
272
+ * Structured list data — present for `List` blocks when
273
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ ordered,
274
+ * start }` — renumber / continue a split list from `start` (the ordered-list
275
+ * start number) without re-parsing the `<ol start=…>` attribute.
276
+ */
277
+ list?: ListData;
278
+ /**
279
+ * Structured container data — present for `Blockquote` / `Alert` blocks when
280
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ nested }` —
281
+ * the ordered pre-rendered HTML of each inner sub-block. The default renderers
282
+ * use this to render the children KEYED (one node per entry) so that while the
283
+ * container streams, only its open last inner block re-renders each tick.
284
+ */
285
+ container?: ContainerData;
286
+ }
287
+ /**
288
+ * Per-stream parser configuration. Omitted fields use the library defaults
289
+ * (autolinks + alerts on, raw HTML escaped, footnotes off) — so the default
290
+ * `new BrookClient()` behaves exactly as before. Config is applied when the
291
+ * stream's parser is created and is **immutable** for that stream's lifetime
292
+ * (a `reset()` keeps it; use a new client for different flags).
293
+ */
294
+ export interface ParserConfig {
295
+ /** GFM extended autolinks (bare www./http(s)://ftp:// + emails). Default true. */
296
+ gfmAutolinks?: boolean;
297
+ /** GitHub alerts (`> [!NOTE]` → callouts). Default true. */
298
+ gfmAlerts?: boolean;
299
+ /**
300
+ * GFM "Disallowed Raw HTML" (tagfilter): with `unsafeHtml` on, the nine
301
+ * disallowed tags (`<title>`, `<textarea>`, `<style>`, `<xmp>`, `<iframe>`,
302
+ * `<noembed>`, `<noframes>`, `<script>`, `<plaintext>`) get their leading
303
+ * `<` escaped so they display as text instead of taking effect. Default
304
+ * false (strict CommonMark passes them through under `unsafeHtml`); no
305
+ * effect while raw HTML is escaped (default) or sanitized — already inert.
306
+ */
307
+ gfmTagfilter?: boolean;
308
+ /** GFM footnotes (`[^1]` + `[^1]:` → footnote section). Default false. */
309
+ gfmFootnotes?: boolean;
310
+ /**
311
+ * Math: `$…$` / `\(…\)` inline and `$$…$$` / `\[…\]` display. Default false
312
+ * (so `$` in prose / currency stays literal). Emits KaTeX-ready markup
313
+ * (`<span class="math math-inline">` / `<div class="math math-display">`)
314
+ * carrying the LaTeX — bring your own KaTeX pass (brookmd stays zero-dep).
315
+ */
316
+ gfmMath?: boolean;
317
+ /**
318
+ * Emit `dir="auto"` on block-level text elements (`p`, `h1`–`h6`,
319
+ * `blockquote`, `ul`/`ol`/`li`, `table`) so the browser detects each block's
320
+ * direction independently — correct for documents mixing English with
321
+ * Arabic/Hebrew. Default false; code blocks always stay LTR. Recommended for
322
+ * apps that render RTL or mixed-direction content.
323
+ */
324
+ dirAuto?: boolean;
325
+ /**
326
+ * Opt-in accessibility markup that deviates from strict GFM byte-output:
327
+ * wraps a task-list checkbox + its text in a `<label>` (programmatic
328
+ * association for screen readers) and adds `scope="col"` to table header
329
+ * cells. Default false (so CommonMark/GFM conformance output is unchanged).
330
+ */
331
+ a11y?: boolean;
332
+ /** Pass raw HTML through unescaped. Default false. **Never enable for untrusted input.** */
333
+ unsafeHtml?: boolean;
334
+ /**
335
+ * Opt-in allowlist of custom component tag names (e.g. `["Thinking",
336
+ * "Callout"]`). A `<Tag>…</Tag>` whose name is listed renders as a component
337
+ * whose inner content is parsed as **markdown** — safely, without `unsafeHtml`
338
+ * (the tag is allowlisted and its attributes are sanitized: event handlers
339
+ * dropped, dangerous URL schemes neutralized). The block is dispatched by the
340
+ * renderer via `components[tag]` (or `components.Component`). Empty/omitted =
341
+ * off. Names match case-sensitively.
342
+ */
343
+ componentTags?: string[];
344
+ /**
345
+ * Opt-in allowlist of INLINE component tag names (e.g. `["tik", "cite"]`). An
346
+ * allowlisted `<tik>…</tik>` (or self-closing `<tik/>`) anywhere in inline
347
+ * content — paragraphs, headings, table cells, list items — renders as a real
348
+ * custom element with **markdown** inner content and sanitized attributes
349
+ * (event handlers dropped, dangerous URL schemes neutralized) — XSS-safe
350
+ * without `unsafeHtml`. The React renderer dispatches it via `components[tag]`,
351
+ * with the inner markdown as the component's `children` and the sanitized
352
+ * attributes as props. Separate from `componentTags` (block containers): list a
353
+ * tag here for inline chips (tickers, citations, @mentions), or in both lists
354
+ * to allow both positions. Names match **case-sensitively** and dispatch
355
+ * verbatim to `components[tag]` (e.g. `"Cite"` → `components.Cite`), same as
356
+ * `componentTags`. Empty/omitted = off.
357
+ */
358
+ inlineComponentTags?: string[];
359
+ /**
360
+ * Opt-in **safe raw-HTML allowlist**. Setting this (even to `[]`) engages a
361
+ * sanitizer that renders a safe subset of *inline* raw HTML **without**
362
+ * `unsafeHtml`: an **empty** array means "allow all tags except a built-in
363
+ * dangerous set" (`script`, `style`, `iframe`, `object`, `embed`, `form`,
364
+ * `input`, `svg`, …); a **non-empty** array renders only those tags (e.g.
365
+ * `["br","sub","sup"]`) and escapes the rest. Every rendered tag's attributes
366
+ * are sanitized (event handlers dropped, dangerous URL schemes → `#`), and HTML
367
+ * comments are dropped. Block-level raw HTML stays escaped (sanitize is
368
+ * inline-scoped for now). Unset/omitted = off (raw HTML handling unchanged).
369
+ * Matching is case-insensitive. See also {@link dropHtmlTags}.
370
+ */
371
+ htmlAllowlist?: string[];
372
+ /**
373
+ * Tags removed entirely (markup dropped; any text between an open/close pair
374
+ * stays as inert text) — e.g. app marker tags, or belt-and-suspenders
375
+ * `["script","style"]`. Setting this (even to `[]`) also engages the safe
376
+ * raw-HTML sanitizer (see {@link htmlAllowlist}). Case-insensitive.
377
+ */
378
+ dropHtmlTags?: string[];
379
+ /**
380
+ * Opt-in structured table data. When on, a `Table` block's `kind.data` is
381
+ * populated with `{ headers, rows, aligns }` (each cell `{ text, html }`) so a
382
+ * consumer can build a sort/filter/transpose/chart/CSV toolbar from DATA — no
383
+ * HTML re-parse, no HAST tree. Default false (non-users pay zero allocation /
384
+ * serde bytes; output and the `kind` serde shape stay byte-identical when off).
385
+ */
386
+ blockData?: boolean;
387
+ }
388
+ export type ToWorker = {
389
+ type: "append";
390
+ streamId: number;
391
+ chunk: string;
392
+ config?: ParserConfig;
393
+ epoch?: number;
394
+ } | {
395
+ type: "finalize";
396
+ streamId: number;
397
+ config?: ParserConfig;
398
+ epoch?: number;
399
+ } | {
400
+ type: "reset";
401
+ streamId: number;
402
+ epoch?: number;
403
+ } | {
404
+ type: "dispose";
405
+ streamId: number;
406
+ };
407
+ export type FromWorker = {
408
+ type: "ready";
409
+ } | {
410
+ type: "patch";
411
+ streamId: number;
412
+ patch: string;
413
+ appendedBytes: number;
414
+ parseMicros: number;
415
+ retainedBytes: number;
416
+ wasmMemoryBytes: number;
417
+ final?: boolean;
418
+ epoch?: number;
419
+ } | {
420
+ type: "error";
421
+ streamId: number;
422
+ message: string;
423
+ fatal?: boolean;
424
+ };
425
+ /**
426
+ * Minimal structural interface satisfied by the DOM `Worker`. Injectable so the
427
+ * pool's routing/lifecycle logic can be unit-tested with a fake worker — no
428
+ * real Worker or WASM required.
429
+ */
430
+ export interface WorkerLike {
431
+ postMessage(msg: ToWorker): void;
432
+ addEventListener(type: "message", listener: (ev: {
433
+ data: FromWorker;
434
+ }) => void): void;
435
+ terminate(): void;
436
+ }
File without changes
@@ -0,0 +1,13 @@
1
+ import type { ComponentType } from "react";
2
+ /**
3
+ * Override map for {@link BrookMarkdown}. Keys are either lowercase HTML tag
4
+ * names (`table`, `a`, `code`, `h1`… — react-markdown style, applied inside a
5
+ * block's HTML) or capitalized block-kind names (`BlockKindTag`, e.g.
6
+ * `CodeBlock`, `Table` — replace the whole block renderer). Values are a React
7
+ * component or an HTML tag string.
8
+ *
9
+ * Tag-level components receive the element's parsed attributes (with
10
+ * `class`→`className`, `style` as an object) plus `children`. Block-kind
11
+ * components receive `BlockComponentProps`. There is no `node` prop.
12
+ */
13
+ export type Components = Record<string, ComponentType<any> | string>;
File without changes
@@ -0,0 +1,2 @@
1
+ export * from "./types-core.js";
2
+ export * from "./types-react.js";
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./types-core.js";
2
+ export * from "./types-react.js";
@@ -0,0 +1,12 @@
1
+ /** Decode the (small, known) set of entities the core emits, plus numeric refs. */
2
+ export declare function decodeEntities(s: string): string;
3
+ /** Replace a dangerous-scheme URL with "#". Mirrors the Rust `is_dangerous_scheme`:
4
+ * strip control chars (C0, DEL, C1 — matching Rust char::is_control),
5
+ * lowercase, then match. The strip affects only the probe, never output.
6
+ *
7
+ * Exported as the SAFE URL path for user `decorators` / `urlTransform`: their
8
+ * output is a TRUSTED surface that does NOT pass through the attribute
9
+ * sanitizer, and React/the DOM happily render a `javascript:` href, so a
10
+ * decorator that builds a link must route its href through this (see
11
+ * `wrapLink`), and `urlTransform` output is re-run through it by the renderer. */
12
+ export declare function safeUrl(value: string): string;
@@ -0,0 +1,45 @@
1
+ const NAMED_ENTITIES = {
2
+ amp: "&",
3
+ lt: "<",
4
+ gt: ">",
5
+ quot: '"',
6
+ apos: "'",
7
+ nbsp: " ",
8
+ copy: "\xA9",
9
+ reg: "\xAE",
10
+ hellip: "\u2026",
11
+ mdash: "\u2014",
12
+ ndash: "\u2013"
13
+ };
14
+ function decodeEntities(s) {
15
+ if (s.indexOf("&") === -1) return s;
16
+ return s.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]*);/g, (m, body) => {
17
+ if (body[0] === "#") {
18
+ const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
19
+ if (Number.isNaN(code) || code < 0 || code > 1114111) return m;
20
+ try {
21
+ return String.fromCodePoint(code);
22
+ } catch {
23
+ return m;
24
+ }
25
+ }
26
+ const named = NAMED_ENTITIES[body];
27
+ return named === void 0 ? m : named;
28
+ });
29
+ }
30
+ function safeUrl(value) {
31
+ let decoded = value;
32
+ for (let i = 0, prev = ""; i < 8 && decoded !== prev; i++) {
33
+ prev = decoded;
34
+ decoded = decodeEntities(decoded);
35
+ }
36
+ const probe = decoded.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").replace(/^\s+/, "").toLowerCase();
37
+ if (probe.startsWith("javascript:") || probe.startsWith("vbscript:") || probe.startsWith("data:text/html") || probe.startsWith("data:text/javascript")) {
38
+ return "#";
39
+ }
40
+ return value;
41
+ }
42
+ export {
43
+ decodeEntities,
44
+ safeUrl
45
+ };
package/dist/vue.d.ts ADDED
@@ -0,0 +1,94 @@
1
+ import type { DefineComponent, Ref } from "vue";
2
+ import { BrookClient } from "./client.js";
3
+ import type { ParserConfig } from "./types-core.js";
4
+ import { type DomComponents, type MountOptions } from "./dom.js";
5
+ /**
6
+ * Vue 3 bindings for {@link mountBrookMarkdown}. Thin lifecycle glue: mount the
7
+ * framework-neutral DOM renderer on `onMounted`, tear it down on `onUnmounted`.
8
+ *
9
+ * The renderer owns all subscribe/diffing; this layer never re-implements it
10
+ * and — per the renderer's contract — never calls `client.destroy()` (the
11
+ * caller owns the worker/stream). Shipped as plain `.ts` (no SFC compiler in
12
+ * the pipeline) via `defineComponent` + `h()`.
13
+ */
14
+ /** Everything `mountBrookMarkdown` accepts, plus the client to subscribe to. */
15
+ export type UseBrookMarkdownOptions = {
16
+ client: BrookClient;
17
+ } & MountOptions;
18
+ /**
19
+ * Composable that mounts the renderer into a container ref. Returns
20
+ * `{ container }` — bind it as the `ref` of the element you want filled.
21
+ *
22
+ * `getOpts` must read its fields lazily (e.g. `() => ({ client: props.client,
23
+ * ... })`) so the watcher sees live prop identities. We watch the five
24
+ * identities individually — `[client, components, sanitize, virtualize,
25
+ * stickToBottom]` — rather than a freshly-composed object, which would change
26
+ * identity every call and remount on every patch. On any of those changing we
27
+ * destroy and remount; `batch`/`highlightCode` still flow through to the mount
28
+ * but are intentionally not remount triggers.
29
+ */
30
+ export declare function useBrookMarkdown(getOpts: () => UseBrookMarkdownOptions): {
31
+ container: Ref<HTMLElement | null>;
32
+ };
33
+ /**
34
+ * A fine-grained `Ref` to the streaming **tail** block id — the one block that
35
+ * may still re-render — driven by Vue's reactivity. Subscribes to the client
36
+ * once and writes a `shallowRef` only when the tail id changes, so a `computed`
37
+ * or `watch` keyed off it re-evaluates *only* for the tail, never for the
38
+ * committed body. Reading it renders nothing: {@link useBrookMarkdown} draws the
39
+ * document; this mirrors {@link MountHandle.openBlockId} through Vue's primitive
40
+ * for any extra tail-scoped work the caller schedules. Auto-unsubscribes on the
41
+ * owning component's unmount.
42
+ */
43
+ export declare function useTailBlockId(client: BrookClient): Ref<number | null>;
44
+ /** Public props of the {@link BrookMarkdown} Vue component. */
45
+ export interface BrookMarkdownVueProps {
46
+ client: BrookClient;
47
+ components?: DomComponents;
48
+ sanitize?: (html: string) => string;
49
+ virtualize?: boolean;
50
+ stickToBottom?: boolean;
51
+ }
52
+ /**
53
+ * Component wrapper around {@link useBrookMarkdown}. Renders a single `<div>`
54
+ * whose ref is the mount container.
55
+ *
56
+ * The return type is annotated with an explicit, single-type-argument
57
+ * `DefineComponent<BrookMarkdownVueProps>` instead of letting `tsc` inline
58
+ * `defineComponent`'s inferred type. The inferred form bakes the *build-time*
59
+ * Vue version's `DefineComponent` arity into the emitted `.d.ts`, which breaks
60
+ * consumers on an older Vue within the declared `vue >=3` peer range (TS2707).
61
+ * A single explicit type arg is portable across all of Vue 3.x.
62
+ */
63
+ export declare const BrookMarkdown: DefineComponent<BrookMarkdownVueProps>;
64
+ /**
65
+ * Own a {@link BrookClient} driven by a CONTROLLED full string — the Vue analogue
66
+ * of React's `useBrookMarkdownString`, for UIs that hold a streaming message as a
67
+ * single growing string (a `ref`/computed) rather than as a stream. Pass a getter
68
+ * for the whole document-so-far; on every change {@link BrookClient.setContent}
69
+ * diffs it and does the minimal work (prefix-extension appends only the delta;
70
+ * any divergence resets and reparses).
71
+ *
72
+ * Pass `streaming: false` (via `getOptions`) once the content is final to
73
+ * finalize the stream and commit its last block. If `streaming` is omitted or
74
+ * `true` the stream is left OPEN — inferring "done" from an absent flag is
75
+ * deliberately avoided (it would re-finalize on every token for callers that
76
+ * grow the string without the flag — an O(n²) reparse trap). `config` is read
77
+ * once at construction and is immutable thereafter, so it is not a change
78
+ * trigger.
79
+ *
80
+ * **Returns the owned client** — a deliberate divergence from {@link useBrookMarkdown}
81
+ * (which returns `{ container }`). Mirroring React's hook, this composes with the
82
+ * component as `<BrookMarkdown :client="client" />` (and lets you read
83
+ * `outline()` / `getMetrics()` off it). The client is created in the composable
84
+ * body (constructor is worker-free → SSR-safe) and destroyed on unmount.
85
+ *
86
+ * SSR-safety: `setContent` is what spawns a Worker (via `append`), so it is
87
+ * called ONLY in `onMounted` and a NON-immediate `watch` — never during the
88
+ * server render path (`setup` constructs the client but neither lifecycle hook
89
+ * nor the non-immediate watch fires on the server).
90
+ */
91
+ export declare function useBrookMarkdownString(getContent: () => string, getOptions?: () => {
92
+ config?: ParserConfig;
93
+ streaming?: boolean;
94
+ }): BrookClient;