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.
- package/CHANGELOG.md +950 -0
- package/README.md +111 -82
- package/dist/block-props.d.ts +18 -0
- package/dist/block-props.js +75 -0
- package/dist/client.d.ts +311 -0
- package/{src/client.ts → dist/client.js} +143 -325
- package/dist/dom.d.ts +110 -0
- package/dist/dom.js +576 -0
- package/dist/element.d.ts +20 -0
- package/dist/element.js +287 -0
- package/dist/hi.d.ts +12 -0
- package/{src/hi.ts → dist/hi.js} +42 -74
- package/dist/html-to-react.d.ts +40 -0
- package/dist/html-to-react.js +344 -0
- package/{src/index.ts → dist/index.d.ts} +5 -25
- package/dist/index.js +16 -0
- package/dist/morph.d.ts +28 -0
- package/dist/morph.js +166 -0
- package/dist/react.d.ts +204 -0
- package/dist/react.js +490 -0
- package/dist/renderers/CodeBlock.d.ts +7 -0
- package/dist/renderers/CodeBlock.js +75 -0
- package/dist/renderers/Math.d.ts +14 -0
- package/dist/renderers/Math.js +15 -0
- package/dist/renderers/Mermaid.d.ts +13 -0
- package/dist/renderers/Mermaid.js +15 -0
- package/dist/server-react.d.ts +32 -0
- package/dist/server-react.js +48 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +81 -0
- package/{src/solid.tsx → dist/solid.d.ts} +19 -95
- package/dist/solid.js +54 -0
- package/dist/svelte.d.ts +80 -0
- package/dist/svelte.js +59 -0
- package/dist/types-core.d.ts +382 -0
- package/dist/types-core.js +0 -0
- package/{src/types-react.ts → dist/types-react.d.ts} +0 -1
- package/dist/types-react.js +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/vue.d.ts +94 -0
- package/dist/vue.js +79 -0
- package/{src → dist}/wasm/flux_md_core.d.ts +18 -6
- package/{src → dist}/wasm/flux_md_core.js +50 -55
- package/dist/wasm/flux_md_core_bg.wasm +0 -0
- package/{src → dist}/wasm/flux_md_core_bg.wasm.d.ts +1 -0
- package/dist/worker-core.d.ts +65 -0
- package/dist/worker-core.js +154 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +48 -0
- package/package.json +25 -19
- package/src/block-props.ts +0 -141
- package/src/dom.ts +0 -946
- package/src/element.ts +0 -400
- package/src/html-to-react.ts +0 -455
- package/src/morph.ts +0 -253
- package/src/react.tsx +0 -1020
- package/src/renderers/CodeBlock.tsx +0 -116
- package/src/renderers/Math.tsx +0 -28
- package/src/renderers/Mermaid.tsx +0 -27
- package/src/server.tsx +0 -221
- package/src/svelte.ts +0 -179
- package/src/types-core.ts +0 -398
- package/src/types.ts +0 -7
- package/src/vue.ts +0 -184
- package/src/wasm/flux_md_core_bg.wasm +0 -0
- package/src/worker-core.ts +0 -174
- package/src/worker.ts +0 -72
- /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
|
|
File without changes
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
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
|
|
10
|
-
* server-side render primitive: feed the full
|
|
11
|
-
* `finalize`, then read `allBlocks()` (no worker,
|
|
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():
|
|
14
|
-
|
|
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
|
-
|
|
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;
|