flux-md 0.16.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +950 -0
  2. package/README.md +111 -82
  3. package/dist/block-props.d.ts +18 -0
  4. package/dist/block-props.js +75 -0
  5. package/dist/client.d.ts +311 -0
  6. package/{src/client.ts → dist/client.js} +143 -325
  7. package/dist/dom.d.ts +110 -0
  8. package/dist/dom.js +576 -0
  9. package/dist/element.d.ts +20 -0
  10. package/dist/element.js +287 -0
  11. package/dist/hi.d.ts +12 -0
  12. package/{src/hi.ts → dist/hi.js} +42 -74
  13. package/dist/html-to-react.d.ts +40 -0
  14. package/dist/html-to-react.js +344 -0
  15. package/{src/index.ts → dist/index.d.ts} +5 -25
  16. package/dist/index.js +16 -0
  17. package/dist/morph.d.ts +28 -0
  18. package/dist/morph.js +166 -0
  19. package/dist/react.d.ts +204 -0
  20. package/dist/react.js +490 -0
  21. package/dist/renderers/CodeBlock.d.ts +7 -0
  22. package/dist/renderers/CodeBlock.js +75 -0
  23. package/dist/renderers/Math.d.ts +14 -0
  24. package/dist/renderers/Math.js +15 -0
  25. package/dist/renderers/Mermaid.d.ts +13 -0
  26. package/dist/renderers/Mermaid.js +15 -0
  27. package/dist/server-react.d.ts +32 -0
  28. package/dist/server-react.js +48 -0
  29. package/dist/server.d.ts +31 -0
  30. package/dist/server.js +81 -0
  31. package/{src/solid.tsx → dist/solid.d.ts} +19 -95
  32. package/dist/solid.js +54 -0
  33. package/dist/svelte.d.ts +80 -0
  34. package/dist/svelte.js +59 -0
  35. package/dist/types-core.d.ts +382 -0
  36. package/dist/types-core.js +0 -0
  37. package/{src/types-react.ts → dist/types-react.d.ts} +0 -1
  38. package/dist/types-react.js +0 -0
  39. package/dist/types.d.ts +2 -0
  40. package/dist/types.js +2 -0
  41. package/dist/vue.d.ts +94 -0
  42. package/dist/vue.js +79 -0
  43. package/{src → dist}/wasm/flux_md_core.d.ts +18 -6
  44. package/{src → dist}/wasm/flux_md_core.js +50 -55
  45. package/dist/wasm/flux_md_core_bg.wasm +0 -0
  46. package/{src → dist}/wasm/flux_md_core_bg.wasm.d.ts +1 -0
  47. package/dist/worker-core.d.ts +65 -0
  48. package/dist/worker-core.js +154 -0
  49. package/dist/worker.d.ts +1 -0
  50. package/dist/worker.js +48 -0
  51. package/package.json +25 -19
  52. package/src/block-props.ts +0 -141
  53. package/src/dom.ts +0 -946
  54. package/src/element.ts +0 -400
  55. package/src/html-to-react.ts +0 -455
  56. package/src/morph.ts +0 -253
  57. package/src/react.tsx +0 -1020
  58. package/src/renderers/CodeBlock.tsx +0 -116
  59. package/src/renderers/Math.tsx +0 -28
  60. package/src/renderers/Mermaid.tsx +0 -27
  61. package/src/server.tsx +0 -221
  62. package/src/svelte.ts +0 -179
  63. package/src/types-core.ts +0 -398
  64. package/src/types.ts +0 -7
  65. package/src/vue.ts +0 -184
  66. package/src/wasm/flux_md_core_bg.wasm +0 -0
  67. package/src/worker-core.ts +0 -174
  68. package/src/worker.ts +0 -72
  69. /package/{src → dist}/styles.css +0 -0
package/dist/svelte.js ADDED
@@ -0,0 +1,59 @@
1
+ import { readable } from "svelte/store";
2
+ import { FluxClient } from "./client.js";
3
+ import { mountFluxMarkdown, tailOpenBlockId } from "./dom.js";
4
+ function fluxMarkdown(node, params) {
5
+ let { client, ...options } = params;
6
+ let handle = mountFluxMarkdown(client, node, options);
7
+ return {
8
+ update(next) {
9
+ if (next.client === client && next.components === options.components && next.sanitize === options.sanitize && next.virtualize === options.virtualize && next.stickToBottom === options.stickToBottom) {
10
+ return;
11
+ }
12
+ handle.destroy();
13
+ ({ client, ...options } = next);
14
+ handle = mountFluxMarkdown(client, node, options);
15
+ },
16
+ destroy() {
17
+ handle.destroy();
18
+ }
19
+ };
20
+ }
21
+ function tailBlockId(client) {
22
+ return readable(tailOpenBlockId(client.getSnapshot()), (set) => {
23
+ const unsubscribe = client.subscribe(() => set(tailOpenBlockId(client.getSnapshot())));
24
+ return unsubscribe;
25
+ });
26
+ }
27
+ function mountOptionsOf(p) {
28
+ const { content: _c, streaming: _s, config: _cfg, ...rest } = p;
29
+ void _c;
30
+ void _s;
31
+ void _cfg;
32
+ return rest;
33
+ }
34
+ function fluxMarkdownString(node, params) {
35
+ let options = mountOptionsOf(params);
36
+ const client = new FluxClient({ config: params.config });
37
+ let handle = mountFluxMarkdown(client, node, options);
38
+ client.setContent(params.content, { done: params.streaming === false });
39
+ return {
40
+ update(next) {
41
+ client.setContent(next.content, { done: next.streaming === false });
42
+ if (next.components === options.components && next.sanitize === options.sanitize && next.virtualize === options.virtualize && next.stickToBottom === options.stickToBottom) {
43
+ return;
44
+ }
45
+ handle.destroy();
46
+ options = mountOptionsOf(next);
47
+ handle = mountFluxMarkdown(client, node, options);
48
+ },
49
+ destroy() {
50
+ handle.destroy();
51
+ client.destroy();
52
+ }
53
+ };
54
+ }
55
+ export {
56
+ fluxMarkdown,
57
+ fluxMarkdownString,
58
+ tailBlockId
59
+ };
@@ -0,0 +1,382 @@
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
+ /** Column alignment from the `|:--|:-:|--:|` delimiter row; `null` = unset. */
7
+ export type Align = "left" | "center" | "right" | null;
8
+ /**
9
+ * One table cell as STRUCTURED DATA (opt-in via {@link ParserConfig.blockData}).
10
+ * `text` is the inline-stripped plaintext — sort/filter/CSV/chart from DATA,
11
+ * with no HTML re-parse. `html` is the inline-rendered display markup, byte-for-
12
+ * byte the inline content inside the matching `<td>`/`<th>` of `block.html`.
13
+ */
14
+ export interface TableCell {
15
+ text: string;
16
+ html: string;
17
+ }
18
+ /**
19
+ * A Table block's `kind.data` when {@link ParserConfig.blockData} is on. Lets a
20
+ * consumer build a sort/filter/transpose/chart/CSV toolbar from DATA alone —
21
+ * no HAST tree, no HTML re-parse. `aligns[i]` is column `i`'s alignment.
22
+ */
23
+ export interface TableData {
24
+ headers: TableCell[];
25
+ rows: TableCell[][];
26
+ aligns: Align[];
27
+ }
28
+ /**
29
+ * A Heading block's `kind.data` when {@link ParserConfig.blockData} is on. Lets a
30
+ * consumer build a table of contents — nested by `level`, anchored by `id` — from
31
+ * DATA alone, with no HTML re-parse. `text` is the inline-stripped plaintext (the
32
+ * heading rendered to plain text, e.g. `## **Bold** & x` → `"Bold & x"`); `id` is
33
+ * a GitHub-style anchor slug of that text (`"bold-x"`) for `#`-links. When
34
+ * `blockData` is off, a Heading's `kind.data` is instead the bare level `number`
35
+ * (byte-identical to before), so consumers reading `kind.data` must accept the
36
+ * `number | HeadingData` union.
37
+ *
38
+ * v1: duplicate heading texts produce identical slugs (no document-wide dedup
39
+ * counter yet) — give same-named headings distinct text if unique anchors matter.
40
+ */
41
+ export interface HeadingData {
42
+ level: number;
43
+ text: string;
44
+ id: string;
45
+ }
46
+ /**
47
+ * A CodeBlock's `kind.data` when {@link ParserConfig.blockData} is on. `lang` is
48
+ * the always-on info-string language (`null` for none); `code` is the opt-in
49
+ * DECODED source inside `<pre><code>…</code></pre>` (only present when `blockData`
50
+ * is on). Build a copy-to-clipboard string / re-highlight from `code` alone — no
51
+ * HTML re-parse, no entity-decode. When `blockData` is off, `code` is absent and
52
+ * `kind.data` is just `{ lang }`, byte-identical to before.
53
+ */
54
+ export interface CodeBlockData {
55
+ lang: string | null;
56
+ code?: string;
57
+ }
58
+ /**
59
+ * A MathBlock's `kind.data` when {@link ParserConfig.blockData} is on. `latex` is
60
+ * the DECODED LaTeX source (the display-math body, entity-decoded). Re-render with
61
+ * KaTeX from `latex` alone — no HTML re-parse. When `blockData` is off, a
62
+ * MathBlock has no `kind.data` at all (byte-identical to before).
63
+ */
64
+ export interface MathBlockData {
65
+ latex: string;
66
+ }
67
+ /**
68
+ * One list item in {@link ListData.items}. `html` is the inline-rendered inner
69
+ * HTML of the item's `<li>` (byte-identical to the content between the matching
70
+ * `<li…>`/`</li>` in `block.html`), so a keyed renderer can stamp one node per
71
+ * item and reuse the unchanged items while the list streams.
72
+ */
73
+ export interface ListItemData {
74
+ html: string;
75
+ }
76
+ /**
77
+ * A List's `kind.data` when {@link ParserConfig.blockData} is on. `ordered` is the
78
+ * always-on flag; `start` is the opt-in ordered-list start number (the `start="N"`
79
+ * HTML attribute; `1` for an unordered list), only present when `blockData` is on.
80
+ * `items` carries each item's inner `<li>` HTML — present (and non-empty) only when
81
+ * `blockData` is on — so a keyed renderer can re-render only the items that changed
82
+ * since the last patch instead of the whole list's HTML. Renumber / continue a
83
+ * split list from `start` alone — no HTML re-parse. When `blockData` is off, `start`
84
+ * and `items` are absent and `kind.data` is just `{ ordered }`, byte-identical.
85
+ */
86
+ export interface ListData {
87
+ ordered: boolean;
88
+ start?: number;
89
+ items?: ListItemData[];
90
+ }
91
+ /**
92
+ * One inner sub-block of a `Blockquote` / `Alert` as STRUCTURED DATA (opt-in via
93
+ * {@link ParserConfig.blockData}). `html` is that sub-block's pre-rendered display
94
+ * markup (e.g. `<p>…</p>`), byte-for-byte the matching fragment inside the
95
+ * container's `block.html` wrapper.
96
+ */
97
+ export interface NestedBlock {
98
+ html: string;
99
+ }
100
+ /**
101
+ * A `Blockquote`'s `kind.data` (and the `nested` carrier inside an `Alert`'s data)
102
+ * when {@link ParserConfig.blockData} is on. `nested` is the ordered list of the
103
+ * container's inner sub-blocks, each as its own pre-rendered HTML. A
104
+ * `components.Blockquote` / `components.Alert` override can render these KEYED (one
105
+ * node per entry) so that while the container streams only its last (open) inner
106
+ * block re-renders each tick — committed inner blocks have stable HTML and memoize.
107
+ * When `blockData` is off, a Blockquote has no `kind.data` and an Alert's is just
108
+ * `{ kind }` (byte-identical to before).
109
+ */
110
+ export interface ContainerData {
111
+ nested: NestedBlock[];
112
+ }
113
+ export interface Block {
114
+ id: number;
115
+ kind: BlockKind;
116
+ start: number;
117
+ end: number;
118
+ html: string;
119
+ open: boolean;
120
+ speculative: boolean;
121
+ }
122
+ export interface Patch {
123
+ newly_committed: Block[];
124
+ active: Block[];
125
+ }
126
+ /**
127
+ * Per-block render-churn sample passed to an {@link RenderMetricsHook}. Lets you
128
+ * measure how often each block actually re-renders / rebuilds (committed blocks
129
+ * memo-skip, so they fire exactly once; the streaming tail fires per patch).
130
+ */
131
+ export interface RenderMetrics {
132
+ /** How many times THIS block has actually rendered/rebuilt so far (≥ 1). */
133
+ renderCount: number;
134
+ /** How many times this block's `speculative` flag flipped between renders. */
135
+ speculativeToggleCount: number;
136
+ /** Wall-clock duration of this render's body in ms (0 if `performance` absent). */
137
+ lastRenderMs: number;
138
+ /** The block's kind (`"Paragraph"`, `"CodeBlock"`, …). */
139
+ kind: string;
140
+ }
141
+ /**
142
+ * Optional observability probe. When supplied to the React renderer (the
143
+ * `onRenderMetrics` prop) or the DOM renderer ({@link MountOptions.onRenderMetrics}),
144
+ * it fires once per ACTUAL render/rebuild of a block — never for a committed
145
+ * block that memo-skips. Zero overhead when absent (no counters advance, the hook
146
+ * path is never entered).
147
+ */
148
+ export type RenderMetricsHook = (blockId: number, m: RenderMetrics) => void;
149
+ /** Props passed to a block-kind override (e.g. `components.CodeBlock`). */
150
+ export interface BlockComponentProps {
151
+ /** The full parsed block, including `kind` (with `kind.data`) and offsets. */
152
+ block: Block;
153
+ /**
154
+ * Rendered, XSS-safe HTML for this block. For `Component` blocks this is the
155
+ * **inner** rendered-markdown HTML (not the `<tag>…</tag>` wrapper). NOTE: a
156
+ * `Component` override that ignores both `html` and `children` renders empty —
157
+ * use {@link children} (the easy path) or `dangerouslySetInnerHTML={{__html:
158
+ * html}}`.
159
+ */
160
+ html: string;
161
+ /**
162
+ * React only: this block's inner content already parsed to a React node tree
163
+ * (markdown rendered, nested tag/inline-component overrides applied). For a
164
+ * `Component` block it is the inner markdown — render it directly
165
+ * (`return <Chip {...attrs}>{children}</Chip>`) instead of dangerously setting
166
+ * `html`. Populated by `<FluxMarkdown>` / `<FluxMarkdownStatic>` when a
167
+ * `components` map is supplied; DOM and other bindings leave it `undefined`
168
+ * (they consume `html`). Typed `unknown` to keep this surface framework-neutral
169
+ * — cast to `ReactNode` in a React override.
170
+ */
171
+ children?: unknown;
172
+ /** True while the block is still streaming (its HTML may still change). */
173
+ open: boolean;
174
+ /** True if the block was closed speculatively and may yet be revised. */
175
+ speculative: boolean;
176
+ /** Decoded source text — present for `CodeBlock` / `MathBlock`. */
177
+ text?: string;
178
+ /** Info-string language — present for `CodeBlock` (from `kind.data.lang`). */
179
+ language?: string;
180
+ /** Component tag name — present for `Component` blocks (from `kind.data.tag`). */
181
+ tag?: string;
182
+ /**
183
+ * Sanitized attributes — present for `Component` blocks. The name-form depends
184
+ * on the consumer: the JSX renderer maps `class`→`className`/`for`→`htmlFor`
185
+ * so `{...attrs}` spreads cleanly onto an element; the DOM renderer keeps the
186
+ * literal HTML names (`class`/`for`) because it applies them via
187
+ * `setAttribute`. For `Component` blocks, `html` is the **inner**
188
+ * rendered-markdown HTML (not the `<tag>…</tag>` wrapper), so an override can
189
+ * wrap it itself.
190
+ */
191
+ attrs?: Record<string, string>;
192
+ /**
193
+ * Structured table data — present for `Table` blocks when
194
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). Equivalent to
195
+ * `block.kind.data`, given a typed, documented name. `{ headers, rows, aligns }`
196
+ * with each cell carrying `text` (plaintext, for sort/filter/CSV/chart) and
197
+ * `html` (display). Build a sort/filter/transpose/chart/CSV toolbar from DATA —
198
+ * no HTML re-parse, no HAST tree.
199
+ */
200
+ table?: TableData;
201
+ /**
202
+ * Structured heading data — present for `Heading` blocks when
203
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ level, text,
204
+ * id }` with `text` the inline-stripped plaintext and `id` a GitHub-style anchor
205
+ * slug. Build a table of contents (nested by `level`, anchored by `id`) from
206
+ * DATA — no HTML re-parse.
207
+ */
208
+ heading?: HeadingData;
209
+ /**
210
+ * Structured code data — present for `CodeBlock` blocks when
211
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ lang, code }`
212
+ * with `code` the DECODED source. Build a copy-to-clipboard string / re-highlight
213
+ * from `code` — no HTML re-parse, no entity-decode. (`props.text` / `props.language`
214
+ * carry the same source / lang and stay populated even when off, via the HTML
215
+ * regex fallback.)
216
+ */
217
+ code?: CodeBlockData;
218
+ /**
219
+ * Structured math data — present for `MathBlock` blocks when
220
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ latex }` — the
221
+ * DECODED LaTeX source. Re-render with KaTeX from `latex` — no HTML re-parse.
222
+ * (`props.text` carries the same source and stays populated even when off, via
223
+ * the HTML regex fallback.)
224
+ */
225
+ math?: MathBlockData;
226
+ /**
227
+ * Structured list data — present for `List` blocks when
228
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ ordered,
229
+ * start }` — renumber / continue a split list from `start` (the ordered-list
230
+ * start number) without re-parsing the `<ol start=…>` attribute.
231
+ */
232
+ list?: ListData;
233
+ /**
234
+ * Structured container data — present for `Blockquote` / `Alert` blocks when
235
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ nested }` —
236
+ * the ordered pre-rendered HTML of each inner sub-block. The default renderers
237
+ * use this to render the children KEYED (one node per entry) so that while the
238
+ * container streams, only its open last inner block re-renders each tick.
239
+ */
240
+ container?: ContainerData;
241
+ }
242
+ /**
243
+ * Per-stream parser configuration. Omitted fields use the library defaults
244
+ * (autolinks + alerts on, raw HTML escaped, footnotes off) — so the default
245
+ * `new FluxClient()` behaves exactly as before. Config is applied when the
246
+ * stream's parser is created and is **immutable** for that stream's lifetime
247
+ * (a `reset()` keeps it; use a new client for different flags).
248
+ */
249
+ export interface ParserConfig {
250
+ /** GFM extended autolinks (bare www./http(s)://ftp:// + emails). Default true. */
251
+ gfmAutolinks?: boolean;
252
+ /** GitHub alerts (`> [!NOTE]` → callouts). Default true. */
253
+ gfmAlerts?: boolean;
254
+ /** GFM footnotes (`[^1]` + `[^1]:` → footnote section). Default false. */
255
+ gfmFootnotes?: boolean;
256
+ /**
257
+ * Math: `$…$` / `\(…\)` inline and `$$…$$` / `\[…\]` display. Default false
258
+ * (so `$` in prose / currency stays literal). Emits KaTeX-ready markup
259
+ * (`<span class="math math-inline">` / `<div class="math math-display">`)
260
+ * carrying the LaTeX — bring your own KaTeX pass (flux-md stays zero-dep).
261
+ */
262
+ gfmMath?: boolean;
263
+ /**
264
+ * Emit `dir="auto"` on block-level text elements (`p`, `h1`–`h6`,
265
+ * `blockquote`, `ul`/`ol`/`li`, `table`) so the browser detects each block's
266
+ * direction independently — correct for documents mixing English with
267
+ * Arabic/Hebrew. Default false; code blocks always stay LTR. Recommended for
268
+ * apps that render RTL or mixed-direction content.
269
+ */
270
+ dirAuto?: boolean;
271
+ /**
272
+ * Opt-in accessibility markup that deviates from strict GFM byte-output:
273
+ * wraps a task-list checkbox + its text in a `<label>` (programmatic
274
+ * association for screen readers) and adds `scope="col"` to table header
275
+ * cells. Default false (so CommonMark/GFM conformance output is unchanged).
276
+ */
277
+ a11y?: boolean;
278
+ /** Pass raw HTML through unescaped. Default false. **Never enable for untrusted input.** */
279
+ unsafeHtml?: boolean;
280
+ /**
281
+ * Opt-in allowlist of custom component tag names (e.g. `["Thinking",
282
+ * "Callout"]`). A `<Tag>…</Tag>` whose name is listed renders as a component
283
+ * whose inner content is parsed as **markdown** — safely, without `unsafeHtml`
284
+ * (the tag is allowlisted and its attributes are sanitized: event handlers
285
+ * dropped, dangerous URL schemes neutralized). The block is dispatched by the
286
+ * renderer via `components[tag]` (or `components.Component`). Empty/omitted =
287
+ * off. Names match case-sensitively.
288
+ */
289
+ componentTags?: string[];
290
+ /**
291
+ * Opt-in allowlist of INLINE component tag names (e.g. `["tik", "cite"]`). An
292
+ * allowlisted `<tik>…</tik>` (or self-closing `<tik/>`) anywhere in inline
293
+ * content — paragraphs, headings, table cells, list items — renders as a real
294
+ * custom element with **markdown** inner content and sanitized attributes
295
+ * (event handlers dropped, dangerous URL schemes neutralized) — XSS-safe
296
+ * without `unsafeHtml`. The React renderer dispatches it via `components[tag]`,
297
+ * with the inner markdown as the component's `children` and the sanitized
298
+ * attributes as props. Separate from `componentTags` (block containers): list a
299
+ * tag here for inline chips (tickers, citations, @mentions), or in both lists
300
+ * to allow both positions. Names match **case-sensitively** and dispatch
301
+ * verbatim to `components[tag]` (e.g. `"Cite"` → `components.Cite`), same as
302
+ * `componentTags`. Empty/omitted = off.
303
+ */
304
+ inlineComponentTags?: string[];
305
+ /**
306
+ * Opt-in **safe raw-HTML allowlist**. Setting this (even to `[]`) engages a
307
+ * sanitizer that renders a safe subset of *inline* raw HTML **without**
308
+ * `unsafeHtml`: an **empty** array means "allow all tags except a built-in
309
+ * dangerous set" (`script`, `style`, `iframe`, `object`, `embed`, `form`,
310
+ * `input`, `svg`, …); a **non-empty** array renders only those tags (e.g.
311
+ * `["br","sub","sup"]`) and escapes the rest. Every rendered tag's attributes
312
+ * are sanitized (event handlers dropped, dangerous URL schemes → `#`), and HTML
313
+ * comments are dropped. Block-level raw HTML stays escaped (sanitize is
314
+ * inline-scoped for now). Unset/omitted = off (raw HTML handling unchanged).
315
+ * Matching is case-insensitive. See also {@link dropHtmlTags}.
316
+ */
317
+ htmlAllowlist?: string[];
318
+ /**
319
+ * Tags removed entirely (markup dropped; any text between an open/close pair
320
+ * stays as inert text) — e.g. app marker tags, or belt-and-suspenders
321
+ * `["script","style"]`. Setting this (even to `[]`) also engages the safe
322
+ * raw-HTML sanitizer (see {@link htmlAllowlist}). Case-insensitive.
323
+ */
324
+ dropHtmlTags?: string[];
325
+ /**
326
+ * Opt-in structured table data. When on, a `Table` block's `kind.data` is
327
+ * populated with `{ headers, rows, aligns }` (each cell `{ text, html }`) so a
328
+ * consumer can build a sort/filter/transpose/chart/CSV toolbar from DATA — no
329
+ * HTML re-parse, no HAST tree. Default false (non-users pay zero allocation /
330
+ * serde bytes; output and the `kind` serde shape stay byte-identical when off).
331
+ */
332
+ blockData?: boolean;
333
+ }
334
+ export type ToWorker = {
335
+ type: "append";
336
+ streamId: number;
337
+ chunk: string;
338
+ config?: ParserConfig;
339
+ epoch?: number;
340
+ } | {
341
+ type: "finalize";
342
+ streamId: number;
343
+ config?: ParserConfig;
344
+ epoch?: number;
345
+ } | {
346
+ type: "reset";
347
+ streamId: number;
348
+ epoch?: number;
349
+ } | {
350
+ type: "dispose";
351
+ streamId: number;
352
+ };
353
+ export type FromWorker = {
354
+ type: "ready";
355
+ } | {
356
+ type: "patch";
357
+ streamId: number;
358
+ patch: string;
359
+ appendedBytes: number;
360
+ parseMicros: number;
361
+ retainedBytes: number;
362
+ wasmMemoryBytes: number;
363
+ final?: boolean;
364
+ epoch?: number;
365
+ } | {
366
+ type: "error";
367
+ streamId: number;
368
+ message: string;
369
+ fatal?: boolean;
370
+ };
371
+ /**
372
+ * Minimal structural interface satisfied by the DOM `Worker`. Injectable so the
373
+ * pool's routing/lifecycle logic can be unit-tested with a fake worker — no
374
+ * real Worker or WASM required.
375
+ */
376
+ export interface WorkerLike {
377
+ postMessage(msg: ToWorker): void;
378
+ addEventListener(type: "message", listener: (ev: {
379
+ data: FromWorker;
380
+ }) => void): void;
381
+ terminate(): void;
382
+ }
File without changes
@@ -1,5 +1,4 @@
1
1
  import type { ComponentType } from "react";
2
-
3
2
  /**
4
3
  * Override map for {@link FluxMarkdown}. Keys are either lowercase HTML tag
5
4
  * names (`table`, `a`, `code`, `h1`… — react-markdown style, applied inside a
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";
package/dist/vue.d.ts ADDED
@@ -0,0 +1,94 @@
1
+ import type { DefineComponent, Ref } from "vue";
2
+ import { FluxClient } 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 mountFluxMarkdown}. 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 `mountFluxMarkdown` accepts, plus the client to subscribe to. */
15
+ export type UseFluxMarkdownOptions = {
16
+ client: FluxClient;
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 useFluxMarkdown(getOpts: () => UseFluxMarkdownOptions): {
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 useFluxMarkdown} 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: FluxClient): Ref<number | null>;
44
+ /** Public props of the {@link FluxMarkdown} Vue component. */
45
+ export interface FluxMarkdownVueProps {
46
+ client: FluxClient;
47
+ components?: DomComponents;
48
+ sanitize?: (html: string) => string;
49
+ virtualize?: boolean;
50
+ stickToBottom?: boolean;
51
+ }
52
+ /**
53
+ * Component wrapper around {@link useFluxMarkdown}. 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<FluxMarkdownVueProps>` 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 FluxMarkdown: DefineComponent<FluxMarkdownVueProps>;
64
+ /**
65
+ * Own a {@link FluxClient} driven by a CONTROLLED full string — the Vue analogue
66
+ * of React's `useFluxMarkdownString`, 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 FluxClient.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 useFluxMarkdown}
81
+ * (which returns `{ container }`). Mirroring React's hook, this composes with the
82
+ * component as `<FluxMarkdown :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 useFluxMarkdownString(getContent: () => string, getOptions?: () => {
92
+ config?: ParserConfig;
93
+ streaming?: boolean;
94
+ }): FluxClient;
package/dist/vue.js ADDED
@@ -0,0 +1,79 @@
1
+ import { defineComponent, h, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
2
+ import { FluxClient } from "./client.js";
3
+ import {
4
+ mountFluxMarkdown,
5
+ tailOpenBlockId
6
+ } from "./dom.js";
7
+ function useFluxMarkdown(getOpts) {
8
+ const container = ref(null);
9
+ let handle = null;
10
+ function mount() {
11
+ if (!container.value) return;
12
+ const { client, ...mountOptions } = getOpts();
13
+ handle = mountFluxMarkdown(client, container.value, mountOptions);
14
+ }
15
+ function teardown() {
16
+ handle?.destroy();
17
+ handle = null;
18
+ }
19
+ onMounted(mount);
20
+ watch(
21
+ [
22
+ () => getOpts().client,
23
+ () => getOpts().components,
24
+ () => getOpts().sanitize,
25
+ () => getOpts().virtualize,
26
+ () => getOpts().stickToBottom
27
+ ],
28
+ () => {
29
+ teardown();
30
+ mount();
31
+ }
32
+ );
33
+ onUnmounted(teardown);
34
+ return { container };
35
+ }
36
+ function useTailBlockId(client) {
37
+ const tail = shallowRef(tailOpenBlockId(client.getSnapshot()));
38
+ const unsubscribe = client.subscribe(() => {
39
+ tail.value = tailOpenBlockId(client.getSnapshot());
40
+ });
41
+ onUnmounted(unsubscribe);
42
+ return tail;
43
+ }
44
+ const FluxMarkdown = defineComponent({
45
+ name: "FluxMarkdown",
46
+ props: {
47
+ client: { type: Object, required: true },
48
+ components: { type: Object, default: void 0 },
49
+ sanitize: { type: Function, default: void 0 },
50
+ virtualize: { type: Boolean, default: void 0 },
51
+ stickToBottom: { type: Boolean, default: void 0 }
52
+ },
53
+ setup(props) {
54
+ const { container } = useFluxMarkdown(() => ({
55
+ client: props.client,
56
+ components: props.components,
57
+ sanitize: props.sanitize,
58
+ virtualize: props.virtualize,
59
+ stickToBottom: props.stickToBottom
60
+ }));
61
+ return () => h("div", { ref: container });
62
+ }
63
+ });
64
+ function useFluxMarkdownString(getContent, getOptions) {
65
+ const client = new FluxClient({ config: getOptions?.()?.config });
66
+ const apply = () => {
67
+ client.setContent(getContent(), { done: getOptions?.()?.streaming === false });
68
+ };
69
+ onMounted(apply);
70
+ watch([getContent, () => getOptions?.()?.streaming], apply);
71
+ onUnmounted(() => client.destroy());
72
+ return client;
73
+ }
74
+ export {
75
+ FluxMarkdown,
76
+ useFluxMarkdown,
77
+ useFluxMarkdownString,
78
+ useTailBlockId
79
+ };
@@ -6,14 +6,25 @@ export class FluxParser {
6
6
  [Symbol.dispose](): void;
7
7
  /**
8
8
  * All blocks currently parsed (committed + active), in document order — the
9
- * whole rendered document as a JS array of `Block`. The one-shot /
10
- * server-side render primitive: feed the full markdown via `append`, call
11
- * `finalize`, then read `allBlocks()` (no worker, no patch accumulation).
9
+ * whole rendered document as a **JSON string** of a `Block[]` (parse with
10
+ * `JSON.parse`). The one-shot / server-side render primitive: feed the full
11
+ * markdown via `append`, call `finalize`, then read `allBlocks()` (no worker,
12
+ * no patch accumulation).
12
13
  */
13
- allBlocks(): any;
14
- append(chunk: string): any;
14
+ allBlocks(): string;
15
+ /**
16
+ * Returns the patch as a **JSON string** (parsed with `JSON.parse` on the
17
+ * main thread), not a live JS object. This is deliberate: serializing once to
18
+ * a string in Rust avoids serde-wasm-bindgen's per-node boundary calls, and a
19
+ * string is far cheaper than an object graph to `structuredClone` across the
20
+ * worker→main `postMessage` (the worker forwards the string verbatim).
21
+ */
22
+ append(chunk: string): string;
15
23
  bufferLen(): number;
16
- finalize(): any;
24
+ /**
25
+ * JSON-string patch — see [`FluxParser::append`].
26
+ */
27
+ finalize(): string;
17
28
  constructor();
18
29
  /**
19
30
  * Total bytes the parser is retaining: source buffer + all rendered
@@ -120,6 +131,7 @@ export interface InitOutput {
120
131
  readonly __wbindgen_export: (a: number, b: number) => number;
121
132
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
122
133
  readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
134
+ readonly __wbindgen_export3: (a: number, b: number, c: number) => void;
123
135
  }
124
136
 
125
137
  export type SyncInitInput = BufferSource | WebAssembly.Module;