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/src/types-core.ts DELETED
@@ -1,398 +0,0 @@
1
- export type BlockKindTag =
2
- | "Paragraph"
3
- | "Heading"
4
- | "CodeBlock"
5
- | "MathBlock"
6
- | "Mermaid"
7
- | "List"
8
- | "Blockquote"
9
- | "Alert"
10
- | "Table"
11
- | "Rule"
12
- | "Html"
13
- | "Component";
14
-
15
- export interface BlockKind {
16
- type: BlockKindTag;
17
- data?: unknown;
18
- }
19
-
20
- /** Column alignment from the `|:--|:-:|--:|` delimiter row; `null` = unset. */
21
- export type Align = "left" | "center" | "right" | null;
22
-
23
- /**
24
- * One table cell as STRUCTURED DATA (opt-in via {@link ParserConfig.blockData}).
25
- * `text` is the inline-stripped plaintext — sort/filter/CSV/chart from DATA,
26
- * with no HTML re-parse. `html` is the inline-rendered display markup, byte-for-
27
- * byte the inline content inside the matching `<td>`/`<th>` of `block.html`.
28
- */
29
- export interface TableCell {
30
- text: string;
31
- html: string;
32
- }
33
-
34
- /**
35
- * A Table block's `kind.data` when {@link ParserConfig.blockData} is on. Lets a
36
- * consumer build a sort/filter/transpose/chart/CSV toolbar from DATA alone —
37
- * no HAST tree, no HTML re-parse. `aligns[i]` is column `i`'s alignment.
38
- */
39
- export interface TableData {
40
- headers: TableCell[];
41
- rows: TableCell[][];
42
- aligns: Align[];
43
- }
44
-
45
- /**
46
- * A Heading block's `kind.data` when {@link ParserConfig.blockData} is on. Lets a
47
- * consumer build a table of contents — nested by `level`, anchored by `id` — from
48
- * DATA alone, with no HTML re-parse. `text` is the inline-stripped plaintext (the
49
- * heading rendered to plain text, e.g. `## **Bold** & x` → `"Bold & x"`); `id` is
50
- * a GitHub-style anchor slug of that text (`"bold-x"`) for `#`-links. When
51
- * `blockData` is off, a Heading's `kind.data` is instead the bare level `number`
52
- * (byte-identical to before), so consumers reading `kind.data` must accept the
53
- * `number | HeadingData` union.
54
- *
55
- * v1: duplicate heading texts produce identical slugs (no document-wide dedup
56
- * counter yet) — give same-named headings distinct text if unique anchors matter.
57
- */
58
- export interface HeadingData {
59
- level: number;
60
- text: string;
61
- id: string;
62
- }
63
-
64
- /**
65
- * A CodeBlock's `kind.data` when {@link ParserConfig.blockData} is on. `lang` is
66
- * the always-on info-string language (`null` for none); `code` is the opt-in
67
- * DECODED source inside `<pre><code>…</code></pre>` (only present when `blockData`
68
- * is on). Build a copy-to-clipboard string / re-highlight from `code` alone — no
69
- * HTML re-parse, no entity-decode. When `blockData` is off, `code` is absent and
70
- * `kind.data` is just `{ lang }`, byte-identical to before.
71
- */
72
- export interface CodeBlockData {
73
- lang: string | null;
74
- code?: string;
75
- }
76
-
77
- /**
78
- * A MathBlock's `kind.data` when {@link ParserConfig.blockData} is on. `latex` is
79
- * the DECODED LaTeX source (the display-math body, entity-decoded). Re-render with
80
- * KaTeX from `latex` alone — no HTML re-parse. When `blockData` is off, a
81
- * MathBlock has no `kind.data` at all (byte-identical to before).
82
- */
83
- export interface MathBlockData {
84
- latex: string;
85
- }
86
-
87
- /**
88
- * One list item in {@link ListData.items}. `html` is the inline-rendered inner
89
- * HTML of the item's `<li>` (byte-identical to the content between the matching
90
- * `<li…>`/`</li>` in `block.html`), so a keyed renderer can stamp one node per
91
- * item and reuse the unchanged items while the list streams.
92
- */
93
- export interface ListItemData {
94
- html: string;
95
- }
96
-
97
- /**
98
- * A List's `kind.data` when {@link ParserConfig.blockData} is on. `ordered` is the
99
- * always-on flag; `start` is the opt-in ordered-list start number (the `start="N"`
100
- * HTML attribute; `1` for an unordered list), only present when `blockData` is on.
101
- * `items` carries each item's inner `<li>` HTML — present (and non-empty) only when
102
- * `blockData` is on — so a keyed renderer can re-render only the items that changed
103
- * since the last patch instead of the whole list's HTML. Renumber / continue a
104
- * split list from `start` alone — no HTML re-parse. When `blockData` is off, `start`
105
- * and `items` are absent and `kind.data` is just `{ ordered }`, byte-identical.
106
- */
107
- export interface ListData {
108
- ordered: boolean;
109
- start?: number;
110
- items?: ListItemData[];
111
- }
112
-
113
- /**
114
- * One inner sub-block of a `Blockquote` / `Alert` as STRUCTURED DATA (opt-in via
115
- * {@link ParserConfig.blockData}). `html` is that sub-block's pre-rendered display
116
- * markup (e.g. `<p>…</p>`), byte-for-byte the matching fragment inside the
117
- * container's `block.html` wrapper.
118
- */
119
- export interface NestedBlock {
120
- html: string;
121
- }
122
-
123
- /**
124
- * A `Blockquote`'s `kind.data` (and the `nested` carrier inside an `Alert`'s data)
125
- * when {@link ParserConfig.blockData} is on. `nested` is the ordered list of the
126
- * container's inner sub-blocks, each as its own pre-rendered HTML. A
127
- * `components.Blockquote` / `components.Alert` override can render these KEYED (one
128
- * node per entry) so that while the container streams only its last (open) inner
129
- * block re-renders each tick — committed inner blocks have stable HTML and memoize.
130
- * When `blockData` is off, a Blockquote has no `kind.data` and an Alert's is just
131
- * `{ kind }` (byte-identical to before).
132
- */
133
- export interface ContainerData {
134
- nested: NestedBlock[];
135
- }
136
-
137
- export interface Block {
138
- id: number;
139
- kind: BlockKind;
140
- start: number;
141
- end: number;
142
- html: string;
143
- open: boolean;
144
- speculative: boolean;
145
- }
146
-
147
- export interface Patch {
148
- newly_committed: Block[];
149
- active: Block[];
150
- }
151
-
152
- /**
153
- * Per-block render-churn sample passed to an {@link RenderMetricsHook}. Lets you
154
- * measure how often each block actually re-renders / rebuilds (committed blocks
155
- * memo-skip, so they fire exactly once; the streaming tail fires per patch).
156
- */
157
- export interface RenderMetrics {
158
- /** How many times THIS block has actually rendered/rebuilt so far (≥ 1). */
159
- renderCount: number;
160
- /** How many times this block's `speculative` flag flipped between renders. */
161
- speculativeToggleCount: number;
162
- /** Wall-clock duration of this render's body in ms (0 if `performance` absent). */
163
- lastRenderMs: number;
164
- /** The block's kind (`"Paragraph"`, `"CodeBlock"`, …). */
165
- kind: string;
166
- }
167
-
168
- /**
169
- * Optional observability probe. When supplied to the React renderer (the
170
- * `onRenderMetrics` prop) or the DOM renderer ({@link MountOptions.onRenderMetrics}),
171
- * it fires once per ACTUAL render/rebuild of a block — never for a committed
172
- * block that memo-skips. Zero overhead when absent (no counters advance, the hook
173
- * path is never entered).
174
- */
175
- export type RenderMetricsHook = (blockId: number, m: RenderMetrics) => void;
176
-
177
- /** Props passed to a block-kind override (e.g. `components.CodeBlock`). */
178
- export interface BlockComponentProps {
179
- /** The full parsed block, including `kind` (with `kind.data`) and offsets. */
180
- block: Block;
181
- /**
182
- * Rendered, XSS-safe HTML for this block. For `Component` blocks this is the
183
- * **inner** rendered-markdown HTML (not the `<tag>…</tag>` wrapper). NOTE: a
184
- * `Component` override that ignores both `html` and `children` renders empty —
185
- * use {@link children} (the easy path) or `dangerouslySetInnerHTML={{__html:
186
- * html}}`.
187
- */
188
- html: string;
189
- /**
190
- * React only: this block's inner content already parsed to a React node tree
191
- * (markdown rendered, nested tag/inline-component overrides applied). For a
192
- * `Component` block it is the inner markdown — render it directly
193
- * (`return <Chip {...attrs}>{children}</Chip>`) instead of dangerously setting
194
- * `html`. Populated by `<FluxMarkdown>` / `<FluxMarkdownStatic>` when a
195
- * `components` map is supplied; DOM and other bindings leave it `undefined`
196
- * (they consume `html`). Typed `unknown` to keep this surface framework-neutral
197
- * — cast to `ReactNode` in a React override.
198
- */
199
- children?: unknown;
200
- /** True while the block is still streaming (its HTML may still change). */
201
- open: boolean;
202
- /** True if the block was closed speculatively and may yet be revised. */
203
- speculative: boolean;
204
- /** Decoded source text — present for `CodeBlock` / `MathBlock`. */
205
- text?: string;
206
- /** Info-string language — present for `CodeBlock` (from `kind.data.lang`). */
207
- language?: string;
208
- /** Component tag name — present for `Component` blocks (from `kind.data.tag`). */
209
- tag?: string;
210
- /**
211
- * Sanitized attributes — present for `Component` blocks. The name-form depends
212
- * on the consumer: the JSX renderer maps `class`→`className`/`for`→`htmlFor`
213
- * so `{...attrs}` spreads cleanly onto an element; the DOM renderer keeps the
214
- * literal HTML names (`class`/`for`) because it applies them via
215
- * `setAttribute`. For `Component` blocks, `html` is the **inner**
216
- * rendered-markdown HTML (not the `<tag>…</tag>` wrapper), so an override can
217
- * wrap it itself.
218
- */
219
- attrs?: Record<string, string>;
220
- /**
221
- * Structured table data — present for `Table` blocks when
222
- * {@link ParserConfig.blockData} is on (otherwise `undefined`). Equivalent to
223
- * `block.kind.data`, given a typed, documented name. `{ headers, rows, aligns }`
224
- * with each cell carrying `text` (plaintext, for sort/filter/CSV/chart) and
225
- * `html` (display). Build a sort/filter/transpose/chart/CSV toolbar from DATA —
226
- * no HTML re-parse, no HAST tree.
227
- */
228
- table?: TableData;
229
- /**
230
- * Structured heading data — present for `Heading` blocks when
231
- * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ level, text,
232
- * id }` with `text` the inline-stripped plaintext and `id` a GitHub-style anchor
233
- * slug. Build a table of contents (nested by `level`, anchored by `id`) from
234
- * DATA — no HTML re-parse.
235
- */
236
- heading?: HeadingData;
237
- /**
238
- * Structured code data — present for `CodeBlock` blocks when
239
- * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ lang, code }`
240
- * with `code` the DECODED source. Build a copy-to-clipboard string / re-highlight
241
- * from `code` — no HTML re-parse, no entity-decode. (`props.text` / `props.language`
242
- * carry the same source / lang and stay populated even when off, via the HTML
243
- * regex fallback.)
244
- */
245
- code?: CodeBlockData;
246
- /**
247
- * Structured math data — present for `MathBlock` blocks when
248
- * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ latex }` — the
249
- * DECODED LaTeX source. Re-render with KaTeX from `latex` — no HTML re-parse.
250
- * (`props.text` carries the same source and stays populated even when off, via
251
- * the HTML regex fallback.)
252
- */
253
- math?: MathBlockData;
254
- /**
255
- * Structured list data — present for `List` blocks when
256
- * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ ordered,
257
- * start }` — renumber / continue a split list from `start` (the ordered-list
258
- * start number) without re-parsing the `<ol start=…>` attribute.
259
- */
260
- list?: ListData;
261
- /**
262
- * Structured container data — present for `Blockquote` / `Alert` blocks when
263
- * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ nested }` —
264
- * the ordered pre-rendered HTML of each inner sub-block. The default renderers
265
- * use this to render the children KEYED (one node per entry) so that while the
266
- * container streams, only its open last inner block re-renders each tick.
267
- */
268
- container?: ContainerData;
269
- }
270
-
271
- /**
272
- * Per-stream parser configuration. Omitted fields use the library defaults
273
- * (autolinks + alerts on, raw HTML escaped, footnotes off) — so the default
274
- * `new FluxClient()` behaves exactly as before. Config is applied when the
275
- * stream's parser is created and is **immutable** for that stream's lifetime
276
- * (a `reset()` keeps it; use a new client for different flags).
277
- */
278
- export interface ParserConfig {
279
- /** GFM extended autolinks (bare www./http(s)://ftp:// + emails). Default true. */
280
- gfmAutolinks?: boolean;
281
- /** GitHub alerts (`> [!NOTE]` → callouts). Default true. */
282
- gfmAlerts?: boolean;
283
- /** GFM footnotes (`[^1]` + `[^1]:` → footnote section). Default false. */
284
- gfmFootnotes?: boolean;
285
- /**
286
- * Math: `$…$` / `\(…\)` inline and `$$…$$` / `\[…\]` display. Default false
287
- * (so `$` in prose / currency stays literal). Emits KaTeX-ready markup
288
- * (`<span class="math math-inline">` / `<div class="math math-display">`)
289
- * carrying the LaTeX — bring your own KaTeX pass (flux-md stays zero-dep).
290
- */
291
- gfmMath?: boolean;
292
- /**
293
- * Emit `dir="auto"` on block-level text elements (`p`, `h1`–`h6`,
294
- * `blockquote`, `ul`/`ol`/`li`, `table`) so the browser detects each block's
295
- * direction independently — correct for documents mixing English with
296
- * Arabic/Hebrew. Default false; code blocks always stay LTR. Recommended for
297
- * apps that render RTL or mixed-direction content.
298
- */
299
- dirAuto?: boolean;
300
- /**
301
- * Opt-in accessibility markup that deviates from strict GFM byte-output:
302
- * wraps a task-list checkbox + its text in a `<label>` (programmatic
303
- * association for screen readers) and adds `scope="col"` to table header
304
- * cells. Default false (so CommonMark/GFM conformance output is unchanged).
305
- */
306
- a11y?: boolean;
307
- /** Pass raw HTML through unescaped. Default false. **Never enable for untrusted input.** */
308
- unsafeHtml?: boolean;
309
- /**
310
- * Opt-in allowlist of custom component tag names (e.g. `["Thinking",
311
- * "Callout"]`). A `<Tag>…</Tag>` whose name is listed renders as a component
312
- * whose inner content is parsed as **markdown** — safely, without `unsafeHtml`
313
- * (the tag is allowlisted and its attributes are sanitized: event handlers
314
- * dropped, dangerous URL schemes neutralized). The block is dispatched by the
315
- * renderer via `components[tag]` (or `components.Component`). Empty/omitted =
316
- * off. Names match case-sensitively.
317
- */
318
- componentTags?: string[];
319
- /**
320
- * Opt-in allowlist of INLINE component tag names (e.g. `["tik", "cite"]`). An
321
- * allowlisted `<tik>…</tik>` (or self-closing `<tik/>`) anywhere in inline
322
- * content — paragraphs, headings, table cells, list items — renders as a real
323
- * custom element with **markdown** inner content and sanitized attributes
324
- * (event handlers dropped, dangerous URL schemes neutralized) — XSS-safe
325
- * without `unsafeHtml`. The React renderer dispatches it via `components[tag]`,
326
- * with the inner markdown as the component's `children` and the sanitized
327
- * attributes as props. Separate from `componentTags` (block containers): list a
328
- * tag here for inline chips (tickers, citations, @mentions), or in both lists
329
- * to allow both positions. Names match **case-sensitively** and dispatch
330
- * verbatim to `components[tag]` (e.g. `"Cite"` → `components.Cite`), same as
331
- * `componentTags`. Empty/omitted = off.
332
- */
333
- inlineComponentTags?: string[];
334
- /**
335
- * Opt-in **safe raw-HTML allowlist**. Setting this (even to `[]`) engages a
336
- * sanitizer that renders a safe subset of *inline* raw HTML **without**
337
- * `unsafeHtml`: an **empty** array means "allow all tags except a built-in
338
- * dangerous set" (`script`, `style`, `iframe`, `object`, `embed`, `form`,
339
- * `input`, `svg`, …); a **non-empty** array renders only those tags (e.g.
340
- * `["br","sub","sup"]`) and escapes the rest. Every rendered tag's attributes
341
- * are sanitized (event handlers dropped, dangerous URL schemes → `#`), and HTML
342
- * comments are dropped. Block-level raw HTML stays escaped (sanitize is
343
- * inline-scoped for now). Unset/omitted = off (raw HTML handling unchanged).
344
- * Matching is case-insensitive. See also {@link dropHtmlTags}.
345
- */
346
- htmlAllowlist?: string[];
347
- /**
348
- * Tags removed entirely (markup dropped; any text between an open/close pair
349
- * stays as inert text) — e.g. app marker tags, or belt-and-suspenders
350
- * `["script","style"]`. Setting this (even to `[]`) also engages the safe
351
- * raw-HTML sanitizer (see {@link htmlAllowlist}). Case-insensitive.
352
- */
353
- dropHtmlTags?: string[];
354
- /**
355
- * Opt-in structured table data. When on, a `Table` block's `kind.data` is
356
- * populated with `{ headers, rows, aligns }` (each cell `{ text, html }`) so a
357
- * consumer can build a sort/filter/transpose/chart/CSV toolbar from DATA — no
358
- * HTML re-parse, no HAST tree. Default false (non-users pay zero allocation /
359
- * serde bytes; output and the `kind` serde shape stay byte-identical when off).
360
- */
361
- blockData?: boolean;
362
- }
363
-
364
- // Each message carries a `streamId` so one worker can multiplex many parsers
365
- // (the worker pool). `ready` is the exception — it's worker-level (WASM loaded),
366
- // not stream-level. The first message for a stream may carry `config`, applied
367
- // when that stream's parser is created.
368
- export type ToWorker =
369
- | { type: "append"; streamId: number; chunk: string; config?: ParserConfig }
370
- | { type: "finalize"; streamId: number; config?: ParserConfig }
371
- | { type: "reset"; streamId: number }
372
- | { type: "dispose"; streamId: number };
373
-
374
- export type FromWorker =
375
- | { type: "ready" }
376
- | {
377
- type: "patch";
378
- streamId: number;
379
- patch: Patch;
380
- appendedBytes: number;
381
- parseMicros: number;
382
- retainedBytes: number;
383
- wasmMemoryBytes: number;
384
- }
385
- // `fatal` marks a worker-level failure (WASM init) that dooms every stream on
386
- // the worker — not a single parse error. It carries no meaningful streamId.
387
- | { type: "error"; streamId: number; message: string; fatal?: boolean };
388
-
389
- /**
390
- * Minimal structural interface satisfied by the DOM `Worker`. Injectable so the
391
- * pool's routing/lifecycle logic can be unit-tested with a fake worker — no
392
- * real Worker or WASM required.
393
- */
394
- export interface WorkerLike {
395
- postMessage(msg: ToWorker): void;
396
- addEventListener(type: "message", listener: (ev: { data: FromWorker }) => void): void;
397
- terminate(): void;
398
- }
package/src/types.ts DELETED
@@ -1,7 +0,0 @@
1
- // Public type surface, split so framework-neutral consumers (`flux-md/client`,
2
- // `flux-md/dom`) typecheck without resolving react: the neutral types live in
3
- // ./types-core, the lone React-coupled `Components` type in ./types-react.
4
- // Re-exported here so `flux-md/types`, index.ts, and every existing import see
5
- // the identical surface as before.
6
- export * from "./types-core";
7
- export * from "./types-react";
package/src/vue.ts DELETED
@@ -1,184 +0,0 @@
1
- import { defineComponent, h, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
2
- import type { PropType, Ref } from "vue";
3
- import { FluxClient } from "./client";
4
- import type { ParserConfig } from "./types-core";
5
- import {
6
- mountFluxMarkdown,
7
- tailOpenBlockId,
8
- type DomComponents,
9
- type MountHandle,
10
- type MountOptions,
11
- } from "./dom";
12
-
13
- /**
14
- * Vue 3 bindings for {@link mountFluxMarkdown}. Thin lifecycle glue: mount the
15
- * framework-neutral DOM renderer on `onMounted`, tear it down on `onUnmounted`.
16
- *
17
- * The renderer owns all subscribe/diffing; this layer never re-implements it
18
- * and — per the renderer's contract — never calls `client.destroy()` (the
19
- * caller owns the worker/stream). Shipped as plain `.ts` (no SFC compiler in
20
- * the pipeline) via `defineComponent` + `h()`.
21
- */
22
-
23
- /** Everything `mountFluxMarkdown` accepts, plus the client to subscribe to. */
24
- export type UseFluxMarkdownOptions = { client: FluxClient } & MountOptions;
25
-
26
- /**
27
- * Composable that mounts the renderer into a container ref. Returns
28
- * `{ container }` — bind it as the `ref` of the element you want filled.
29
- *
30
- * `getOpts` must read its fields lazily (e.g. `() => ({ client: props.client,
31
- * ... })`) so the watcher sees live prop identities. We watch the five
32
- * identities individually — `[client, components, sanitize, virtualize,
33
- * stickToBottom]` — rather than a freshly-composed object, which would change
34
- * identity every call and remount on every patch. On any of those changing we
35
- * destroy and remount; `batch`/`highlightCode` still flow through to the mount
36
- * but are intentionally not remount triggers.
37
- */
38
- export function useFluxMarkdown(getOpts: () => UseFluxMarkdownOptions): {
39
- container: Ref<HTMLElement | null>;
40
- } {
41
- const container = ref<HTMLElement | null>(null);
42
- let handle: MountHandle | null = null;
43
-
44
- function mount(): void {
45
- if (!container.value) return;
46
- const { client, ...mountOptions } = getOpts();
47
- handle = mountFluxMarkdown(client, container.value, mountOptions);
48
- }
49
-
50
- function teardown(): void {
51
- // handle.destroy() is the ONLY teardown — it unsubscribes and removes the
52
- // renderer root. The caller owns client.destroy(); we never call it.
53
- handle?.destroy();
54
- handle = null;
55
- }
56
-
57
- onMounted(mount);
58
-
59
- watch(
60
- [
61
- () => getOpts().client,
62
- () => getOpts().components,
63
- () => getOpts().sanitize,
64
- () => getOpts().virtualize,
65
- () => getOpts().stickToBottom,
66
- ],
67
- () => {
68
- // Only after the initial onMounted has run does `handle` exist; before
69
- // that the watcher firing (it won't, being lazy) would no-op anyway.
70
- teardown();
71
- mount();
72
- },
73
- );
74
-
75
- // Vue auto-stops this watcher when the owning component unmounts, so a manual
76
- // stop is unnecessary; we only need to drop the renderer.
77
- onUnmounted(teardown);
78
-
79
- return { container };
80
- }
81
-
82
- /**
83
- * A fine-grained `Ref` to the streaming **tail** block id — the one block that
84
- * may still re-render — driven by Vue's reactivity. Subscribes to the client
85
- * once and writes a `shallowRef` only when the tail id changes, so a `computed`
86
- * or `watch` keyed off it re-evaluates *only* for the tail, never for the
87
- * committed body. Reading it renders nothing: {@link useFluxMarkdown} draws the
88
- * document; this mirrors {@link MountHandle.openBlockId} through Vue's primitive
89
- * for any extra tail-scoped work the caller schedules. Auto-unsubscribes on the
90
- * owning component's unmount.
91
- */
92
- export function useTailBlockId(client: FluxClient): Ref<number | null> {
93
- const tail = shallowRef<number | null>(tailOpenBlockId(client.getSnapshot()));
94
- // A shallowRef assignment only triggers when the value actually changes, so
95
- // pure tail-html growth that keeps the same open id never re-fires watchers.
96
- const unsubscribe = client.subscribe(() => {
97
- tail.value = tailOpenBlockId(client.getSnapshot());
98
- });
99
- onUnmounted(unsubscribe);
100
- return tail;
101
- }
102
-
103
- /**
104
- * Component wrapper around {@link useFluxMarkdown}. Renders a single `<div>`
105
- * whose ref is the mount container.
106
- */
107
- export const FluxMarkdown = defineComponent({
108
- name: "FluxMarkdown",
109
- props: {
110
- client: { type: Object as PropType<FluxClient>, required: true },
111
- components: { type: Object as PropType<DomComponents>, default: undefined },
112
- sanitize: { type: Function as PropType<(html: string) => string>, default: undefined },
113
- virtualize: { type: Boolean, default: undefined },
114
- stickToBottom: { type: Boolean, default: undefined },
115
- },
116
- setup(props) {
117
- // Read props inside the getter so the watch tracks their live identities;
118
- // destructuring here would snapshot them and the watcher would never fire.
119
- const { container } = useFluxMarkdown(() => ({
120
- client: props.client,
121
- components: props.components,
122
- sanitize: props.sanitize,
123
- virtualize: props.virtualize,
124
- stickToBottom: props.stickToBottom,
125
- }));
126
- return () => h("div", { ref: container });
127
- },
128
- });
129
-
130
- /**
131
- * Own a {@link FluxClient} driven by a CONTROLLED full string — the Vue analogue
132
- * of React's `useFluxMarkdownString`, for UIs that hold a streaming message as a
133
- * single growing string (a `ref`/computed) rather than as a stream. Pass a getter
134
- * for the whole document-so-far; on every change {@link FluxClient.setContent}
135
- * diffs it and does the minimal work (prefix-extension appends only the delta;
136
- * any divergence resets and reparses).
137
- *
138
- * Pass `streaming: false` (via `getOptions`) once the content is final to
139
- * finalize the stream and commit its last block. If `streaming` is omitted or
140
- * `true` the stream is left OPEN — inferring "done" from an absent flag is
141
- * deliberately avoided (it would re-finalize on every token for callers that
142
- * grow the string without the flag — an O(n²) reparse trap). `config` is read
143
- * once at construction and is immutable thereafter, so it is not a change
144
- * trigger.
145
- *
146
- * **Returns the owned client** — a deliberate divergence from {@link useFluxMarkdown}
147
- * (which returns `{ container }`). Mirroring React's hook, this composes with the
148
- * component as `<FluxMarkdown :client="client" />` (and lets you read
149
- * `outline()` / `getMetrics()` off it). The client is created in the composable
150
- * body (constructor is worker-free → SSR-safe) and destroyed on unmount.
151
- *
152
- * SSR-safety: `setContent` is what spawns a Worker (via `append`), so it is
153
- * called ONLY in `onMounted` and a NON-immediate `watch` — never during the
154
- * server render path (`setup` constructs the client but neither lifecycle hook
155
- * nor the non-immediate watch fires on the server).
156
- */
157
- export function useFluxMarkdownString(
158
- getContent: () => string,
159
- getOptions?: () => { config?: ParserConfig; streaming?: boolean },
160
- ): FluxClient {
161
- // One client per composable instance. Constructor is worker-free, so this is
162
- // safe to run in setup() during SSR; config is read once and is immutable.
163
- const client = new FluxClient({ config: getOptions?.()?.config });
164
-
165
- // Reconcile the parser to the controlled string. setContent diffs internally,
166
- // so this is correct whether `content` grows by a token or is swapped wholesale.
167
- // `streaming === false` (never `!streaming`) → only an explicit false finalizes;
168
- // an absent/true flag leaves the stream open.
169
- const apply = (): void => {
170
- client.setContent(getContent(), { done: getOptions?.()?.streaming === false });
171
- };
172
-
173
- // Initial feed + every change. NOT { immediate: true }: an immediate watch runs
174
- // in setup() — i.e. during SSR — and would spawn a Worker on the server. The
175
- // initial feed is onMounted (client-only); the watch covers later changes.
176
- onMounted(apply);
177
- watch([getContent, () => getOptions?.()?.streaming], apply);
178
-
179
- // This composable OWNS the client (unlike useFluxMarkdown, which takes one), so
180
- // it destroys it here. Vue auto-stops the watcher on unmount.
181
- onUnmounted(() => client.destroy());
182
-
183
- return client;
184
- }
Binary file