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/dom.d.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { FluxClient } from "./client.js";
2
+ import type { Block, BlockComponentProps, RenderMetricsHook } from "./types-core.js";
3
+ /**
4
+ * Framework-neutral DOM renderer for a {@link FluxClient}. Mounts the streaming
5
+ * document into a container and keeps it in sync via direct DOM mutation,
6
+ * mirroring the JSX renderer's block model: each block is keyed by its stable
7
+ * parser-assigned id, and a committed block's node is reused untouched on every
8
+ * later patch (the parity analogue of the JSX renderer's block memo). Only the
9
+ * streaming tail is rebuilt.
10
+ *
11
+ * This is the foundation the Web Component / Vue / Svelte / Solid bindings
12
+ * build on; it imports only neutral modules and carries no framework dependency.
13
+ *
14
+ * ## Custom components
15
+ *
16
+ * Pass `components` to override a whole block kind (or a component tag). Keys
17
+ * are capitalized block-kind names (`CodeBlock`, `Table`, `Mermaid`…) or, for
18
+ * `Component` blocks, the tag name (e.g. `Thinking`) with `Component` as the
19
+ * generic fallback. A component receives {@link BlockComponentProps} and returns
20
+ * an `HTMLElement` or an HTML string. There is no tag-level override path (no
21
+ * `table`/`a`/`code` keys) — that requires an HTML→tree pass the DOM renderer
22
+ * doesn't carry.
23
+ */
24
+ export interface MountHandle {
25
+ destroy(): void;
26
+ refresh(): void;
27
+ /**
28
+ * The id of the streaming **tail** block — the one block that may re-render on
29
+ * the next patch (a committed block's node is frozen, so its id never appears
30
+ * here). Returns `null` when no block is open (idle / fully committed).
31
+ *
32
+ * Purely derived from the live snapshot; reading it renders nothing and mutates
33
+ * nothing. It exists so a fine-grained framework binding (Solid `createMemo`,
34
+ * Vue `computed`, Svelte `derived`) can narrow a reactive cell to *just the tail*
35
+ * for its own scheduling/diagnostics — the DOM is already updated by the
36
+ * renderer's own subscribe loop, so this never changes what is drawn.
37
+ */
38
+ openBlockId(): number | null;
39
+ }
40
+ export type DomBlockComponent = (props: BlockComponentProps) => HTMLElement | string;
41
+ /** Override map: capitalized block-kind / component-tag keys only. */
42
+ export type DomComponents = Record<string, DomBlockComponent>;
43
+ export interface MountOptions {
44
+ components?: DomComponents;
45
+ /**
46
+ * Optional HTML sanitizer applied to every generic block's HTML before it is
47
+ * injected via `innerHTML` — **including the streaming (open/speculative)
48
+ * tail**. The built-in code/math/mermaid renderers operate on already-escaped
49
+ * content and are not run through it (same as the JSX renderer). When omitted,
50
+ * rendering is byte-identical and zero-cost.
51
+ */
52
+ sanitize?: (html: string) => string;
53
+ /**
54
+ * Skip layout/paint for off-screen *closed* blocks via CSS
55
+ * `content-visibility: auto` (for very long documents). Off by default.
56
+ */
57
+ virtualize?: boolean;
58
+ /**
59
+ * Keep a bottom snap target so the view follows the streaming tail. CSS-only:
60
+ * emits a sentinel with `scroll-snap-align: end`; you add
61
+ * `scroll-snap-type: y proximity` to your scroll container. Off by default.
62
+ */
63
+ stickToBottom?: boolean;
64
+ /** Use the built-in code highlighter. Default true; suppressed when a
65
+ * `components.CodeBlock` override is supplied. */
66
+ highlightCode?: boolean;
67
+ /** Coalesce patches into one DOM write per animation frame. Default true. */
68
+ batch?: boolean;
69
+ /**
70
+ * Opt-in (default false). When a generic open/streaming block grows, morph its
71
+ * existing DOM subtree **in place** toward the new HTML instead of rebuilding
72
+ * the whole node with `innerHTML`. The browser then only repaints/relayouts
73
+ * the parts that changed, and focus/text-selection inside the streaming tail
74
+ * survive a token append. The default path (full rebuild) is byte-identical
75
+ * and unchanged; this only affects generic blocks rendered via the `innerHTML`
76
+ * fast path (not code/math/mermaid/component overrides). The morphed subtree is
77
+ * equivalent to the rebuilt one. */
78
+ morphOpenBlocks?: boolean;
79
+ /** Appended to the root's `className` (the `flux-md` class is always present). */
80
+ className?: string;
81
+ /** Set on the root element. */
82
+ id?: string;
83
+ /** Set on the root element (e.g. `"article"`, `"log"`). */
84
+ role?: string;
85
+ /**
86
+ * Make the root a live region so screen readers announce streamed content.
87
+ * `"polite"` coalesces rapid updates (does not read every token). Off by default.
88
+ */
89
+ ariaLive?: "off" | "polite" | "assertive";
90
+ /** Live-region atomicity; pair with `ariaLive`. Off by default. */
91
+ ariaAtomic?: boolean;
92
+ /**
93
+ * Optional render-churn probe. Fires once per ACTUAL node build/rebuild of a
94
+ * block — never for a committed block whose node is reused untouched on a
95
+ * tail-only patch. The callback gets the block id and a {@link RenderMetrics}
96
+ * sample (per-block `renderCount`/rebuild count, `speculativeToggleCount`,
97
+ * `lastRenderMs`, `kind`). Zero overhead when omitted, and advances
98
+ * `client.getMetrics().rebuildCount`.
99
+ */
100
+ onRenderMetrics?: RenderMetricsHook;
101
+ }
102
+ export declare function mountFluxMarkdown(client: FluxClient, container: HTMLElement, options?: MountOptions): MountHandle;
103
+ /**
104
+ * Derive the streaming tail's block id from an ordered snapshot: the id of the
105
+ * last block when it is open, else `null`. The open block is always the tail by
106
+ * construction (the parser only keeps the final block speculative/open), so this
107
+ * is an O(1) read of the last element — no scan. Shared so the framework
108
+ * adapters expose the same "what may re-render next" signal as the DOM handle.
109
+ */
110
+ export declare function tailOpenBlockId(snapshot: readonly Block[]): number | null;
package/dist/dom.js ADDED
@@ -0,0 +1,576 @@
1
+ import { highlight } from "./hi.js";
2
+ import { morph } from "./morph.js";
3
+ import { blockProps, extractLang } from "./block-props.js";
4
+ const INTRINSIC_PX = {
5
+ Paragraph: 80,
6
+ Heading: 44,
7
+ CodeBlock: 300,
8
+ MathBlock: 140,
9
+ Mermaid: 220,
10
+ List: 120,
11
+ Blockquote: 100,
12
+ Alert: 120,
13
+ Table: 200,
14
+ Rule: 24,
15
+ Html: 80,
16
+ Component: 120
17
+ };
18
+ function mountFluxMarkdown(client, container, options = {}) {
19
+ if (typeof document === "undefined") {
20
+ throw new Error("mountFluxMarkdown is browser-only; call it after the DOM exists.");
21
+ }
22
+ const components = options.components && Object.keys(options.components).length > 0 ? options.components : void 0;
23
+ const { sanitize, virtualize, stickToBottom, onRenderMetrics } = options;
24
+ const hasPerf = typeof performance !== "undefined";
25
+ const highlightCode = options.highlightCode !== false && !components?.CodeBlock;
26
+ const batch = options.batch !== false && typeof requestAnimationFrame === "function";
27
+ const morphOpenBlocks = options.morphOpenBlocks === true;
28
+ const root = document.createElement("div");
29
+ root.className = options.className ? `flux-md ${options.className}` : "flux-md";
30
+ if (options.id) root.id = options.id;
31
+ if (options.role) root.setAttribute("role", options.role);
32
+ if (options.ariaLive) root.setAttribute("aria-live", options.ariaLive);
33
+ if (options.ariaAtomic !== void 0) root.setAttribute("aria-atomic", String(options.ariaAtomic));
34
+ container.appendChild(root);
35
+ let anchor = null;
36
+ if (stickToBottom) {
37
+ anchor = document.createElement("div");
38
+ anchor.className = "flux-bottom-anchor";
39
+ anchor.setAttribute("aria-hidden", "true");
40
+ anchor.style.scrollSnapAlign = "end";
41
+ root.appendChild(anchor);
42
+ }
43
+ const mounted = /* @__PURE__ */ new Map();
44
+ let order = [];
45
+ let dead = false;
46
+ let frame = 0;
47
+ let lastRenderGeneric = false;
48
+ function sync() {
49
+ if (dead) return;
50
+ const snapshot = client.getSnapshot();
51
+ const nextOrder = new Array(snapshot.length);
52
+ const seen = /* @__PURE__ */ new Set();
53
+ for (let i = 0; i < snapshot.length; i++) {
54
+ const b = snapshot[i];
55
+ nextOrder[i] = b.id;
56
+ seen.add(b.id);
57
+ const existing = mounted.get(b.id);
58
+ if (!existing) {
59
+ const t02 = onRenderMetrics && hasPerf ? performance.now() : 0;
60
+ const mb = {
61
+ id: b.id,
62
+ node: void 0,
63
+ html: b.html,
64
+ open: b.open,
65
+ speculative: b.speculative,
66
+ kind: b.kind.type,
67
+ renderCount: 0,
68
+ toggleCount: 0,
69
+ generic: false
70
+ };
71
+ mb.node = renderBlock(b, mb);
72
+ mb.generic = lastRenderGeneric;
73
+ mounted.set(b.id, mb);
74
+ if (onRenderMetrics) noteRender(mb, b, t02);
75
+ continue;
76
+ }
77
+ if (existing.html === b.html && existing.open === b.open && existing.speculative === b.speculative) {
78
+ continue;
79
+ }
80
+ const t0 = onRenderMetrics && hasPerf ? performance.now() : 0;
81
+ if (existing.table && b.open && b.kind.type === "Table") {
82
+ const data = tableData(b);
83
+ if (data) {
84
+ syncTbody(existing.table, data);
85
+ if (onRenderMetrics) noteRender(existing, b, t0);
86
+ if (existing.speculative !== b.speculative) {
87
+ existing.node.classList.toggle("flux-speculative", b.speculative);
88
+ }
89
+ existing.html = b.html;
90
+ existing.open = b.open;
91
+ existing.speculative = b.speculative;
92
+ continue;
93
+ }
94
+ }
95
+ if (morphOpenBlocks && b.open && existing.open && existing.generic && existing.kind === b.kind.type && usesGenericPath(b)) {
96
+ morph(existing.node, sanitize ? sanitize(b.html) : b.html);
97
+ if (onRenderMetrics) noteRender(existing, b, t0);
98
+ existing.html = b.html;
99
+ existing.speculative = b.speculative;
100
+ existing.node.className = genericClassName(b);
101
+ existing.generic = !sanitize;
102
+ continue;
103
+ }
104
+ if (!sanitize && existing.generic && existing.kind === b.kind.type && existing.open === b.open && existing.speculative === b.speculative && b.html.length > existing.html.length && b.html.startsWith(existing.html) && isDepth0Boundary(existing.html, b.html)) {
105
+ existing.node.insertAdjacentHTML("beforeend", b.html.slice(existing.html.length));
106
+ if (onRenderMetrics) noteRender(existing, b, t0);
107
+ existing.html = b.html;
108
+ continue;
109
+ }
110
+ existing.table = void 0;
111
+ const node = renderBlock(b, existing);
112
+ existing.node.replaceWith(node);
113
+ existing.node = node;
114
+ if (onRenderMetrics) noteRender(existing, b, t0);
115
+ existing.html = b.html;
116
+ existing.open = b.open;
117
+ existing.speculative = b.speculative;
118
+ existing.kind = b.kind.type;
119
+ existing.generic = lastRenderGeneric;
120
+ }
121
+ if (mounted.size > seen.size) {
122
+ for (const [id, mb] of mounted) {
123
+ if (!seen.has(id)) {
124
+ mb.node.remove();
125
+ mounted.delete(id);
126
+ }
127
+ }
128
+ }
129
+ order = nextOrder;
130
+ reconcileChildren();
131
+ }
132
+ function noteRender(mb, b, t0) {
133
+ mb.renderCount++;
134
+ if (mb.speculative !== b.speculative) mb.toggleCount++;
135
+ client.__noteRebuild();
136
+ onRenderMetrics(b.id, {
137
+ renderCount: mb.renderCount,
138
+ speculativeToggleCount: mb.toggleCount,
139
+ lastRenderMs: hasPerf ? performance.now() - t0 : 0,
140
+ kind: b.kind.type
141
+ });
142
+ }
143
+ function reconcileChildren() {
144
+ let cursor = root.firstChild;
145
+ for (let i = 0; i < order.length; i++) {
146
+ const mb = mounted.get(order[i]);
147
+ if (!mb) continue;
148
+ const want = mb.node;
149
+ if (cursor === want) {
150
+ cursor = want.nextSibling;
151
+ continue;
152
+ }
153
+ root.insertBefore(want, cursor);
154
+ }
155
+ }
156
+ function renderBlock(b, mb) {
157
+ const content = renderBlockContent(b, mb);
158
+ if (virtualize && !b.open && !b.speculative) {
159
+ const px = INTRINSIC_PX[b.kind.type] ?? 120;
160
+ content.style.contentVisibility = "auto";
161
+ content.style.containIntrinsicSize = `auto ${px}px`;
162
+ }
163
+ return content;
164
+ }
165
+ function renderBlockContent(b, mb) {
166
+ const kind = b.kind.type;
167
+ lastRenderGeneric = false;
168
+ if (components) {
169
+ if (kind === "Component") {
170
+ const tag = b.kind.data?.tag;
171
+ const override = tag && components[tag] || components.Component;
172
+ if (override) return wrapOverrideResult(override(blockProps(b)));
173
+ }
174
+ const blockOverride = components[kind];
175
+ if (blockOverride) return wrapOverrideResult(blockOverride(blockProps(b)));
176
+ }
177
+ switch (kind) {
178
+ case "CodeBlock":
179
+ if (highlightCode) return renderCodeBlock(b);
180
+ break;
181
+ // fall through to the generic path
182
+ case "MathBlock":
183
+ return renderMathBlock(b);
184
+ case "Mermaid":
185
+ return renderMermaid(b);
186
+ }
187
+ if (kind === "Table" && b.open) {
188
+ const data = tableData(b);
189
+ if (data) return buildKeyedTable(b, data, mb);
190
+ }
191
+ if (b.open && kind === "List") {
192
+ const keyed = renderKeyedList(b);
193
+ if (keyed) return keyed;
194
+ }
195
+ const node = document.createElement("div");
196
+ node.className = genericClassName(b);
197
+ if (b.open && !sanitize && (kind === "Blockquote" || kind === "Alert")) {
198
+ const wrapper = renderKeyedContainer(b);
199
+ if (wrapper) {
200
+ node.appendChild(wrapper);
201
+ return node;
202
+ }
203
+ }
204
+ node.innerHTML = sanitize ? sanitize(b.html) : b.html;
205
+ lastRenderGeneric = !sanitize;
206
+ return node;
207
+ }
208
+ function renderKeyedList(b) {
209
+ const ld = b.kind.data;
210
+ const items = ld?.items;
211
+ if (!Array.isArray(items) || items.length === 0) return null;
212
+ const node = document.createElement("div");
213
+ node.className = "flux-block flux-block-list" + (b.open ? " flux-open" : "") + (b.speculative ? " flux-speculative" : "");
214
+ const list = document.createElement(ld?.ordered ? "ol" : "ul");
215
+ if (ld?.ordered && ld.start !== void 0 && ld.start !== 1) {
216
+ list.setAttribute("start", String(ld.start));
217
+ }
218
+ for (const it of items) {
219
+ const li = document.createElement("li");
220
+ li.innerHTML = sanitize ? sanitize(it.html) : it.html;
221
+ list.appendChild(li);
222
+ }
223
+ node.appendChild(list);
224
+ return node;
225
+ }
226
+ function renderKeyedContainer(b) {
227
+ const nested = b.kind.data?.nested;
228
+ if (!Array.isArray(nested)) return null;
229
+ const tagName = b.kind.type === "Alert" ? "div" : "blockquote";
230
+ const wrapper = document.createElement(tagName);
231
+ applyOpenTagAttrs(wrapper, b.html);
232
+ if (b.kind.type === "Alert") {
233
+ const title = alertTitleHtml(b.html);
234
+ if (title) {
235
+ const t = document.createElement("div");
236
+ t.innerHTML = title;
237
+ const titleNode = t.firstElementChild;
238
+ if (titleNode) wrapper.appendChild(titleNode);
239
+ }
240
+ }
241
+ for (let i = 0; i < nested.length; i++) {
242
+ const child = document.createElement("div");
243
+ child.innerHTML = nested[i].html;
244
+ const inner = child.firstElementChild;
245
+ wrapper.appendChild(inner ?? child);
246
+ }
247
+ return wrapper;
248
+ }
249
+ function buildKeyedTable(b, data, mb) {
250
+ const node = document.createElement("div");
251
+ node.className = "flux-block flux-block-table flux-open" + (b.speculative ? " flux-speculative" : "");
252
+ const table = document.createElement("table");
253
+ if (b.html.startsWith('<table dir="auto"')) table.setAttribute("dir", "auto");
254
+ const scope = b.html.includes('<th scope="col"');
255
+ const thead = document.createElement("thead");
256
+ const htr = document.createElement("tr");
257
+ for (let j = 0; j < data.headers.length; j++) {
258
+ htr.appendChild(makeCell("th", data.headers[j].html, data.aligns[j] ?? null, scope));
259
+ }
260
+ thead.appendChild(htr);
261
+ table.appendChild(thead);
262
+ const km = { table, tbody: null, scope, committed: 0, lastRow: null };
263
+ mb.table = km;
264
+ node.appendChild(table);
265
+ syncTbody(km, data);
266
+ return node;
267
+ }
268
+ function syncTbody(km, data) {
269
+ const n = data.rows.length;
270
+ if (n === 0) {
271
+ if (km.tbody) {
272
+ km.tbody.remove();
273
+ km.tbody = null;
274
+ }
275
+ km.committed = 0;
276
+ km.lastRow = null;
277
+ return;
278
+ }
279
+ if (!km.tbody) {
280
+ km.tbody = document.createElement("tbody");
281
+ km.table.appendChild(km.tbody);
282
+ }
283
+ const tbody = km.tbody;
284
+ if (km.lastRow) {
285
+ km.lastRow.remove();
286
+ km.lastRow = null;
287
+ }
288
+ for (let i = km.committed; i < n - 1; i++) {
289
+ tbody.appendChild(makeRow(data.rows[i], data.aligns));
290
+ }
291
+ km.committed = n - 1;
292
+ const last = makeRow(data.rows[n - 1], data.aligns);
293
+ tbody.appendChild(last);
294
+ km.lastRow = last;
295
+ }
296
+ function makeRow(cells, aligns) {
297
+ const tr = document.createElement("tr");
298
+ for (let j = 0; j < cells.length; j++) {
299
+ tr.appendChild(makeCell("td", cells[j].html, aligns[j] ?? null, false));
300
+ }
301
+ return tr;
302
+ }
303
+ function makeCell(tag, html, align, scope) {
304
+ const cell = document.createElement(tag);
305
+ if (tag === "th" && scope) cell.setAttribute("scope", "col");
306
+ if (align) cell.style.textAlign = align;
307
+ cell.innerHTML = sanitize ? sanitize(html) : html;
308
+ return cell;
309
+ }
310
+ function genericClassName(b) {
311
+ return "flux-block flux-block-" + b.kind.type.toLowerCase() + (b.open ? " flux-open" : "") + (b.speculative ? " flux-speculative" : "");
312
+ }
313
+ function usesGenericPath(b) {
314
+ const kind = b.kind.type;
315
+ if (components) {
316
+ if (kind === "Component") {
317
+ const tag = b.kind.data?.tag;
318
+ if (tag && components[tag] || components.Component) return false;
319
+ }
320
+ if (components[kind]) return false;
321
+ }
322
+ if (kind === "CodeBlock") return !highlightCode;
323
+ if (kind === "MathBlock" || kind === "Mermaid") return false;
324
+ return true;
325
+ }
326
+ function wrapOverrideResult(result) {
327
+ if (typeof result === "string") {
328
+ const node = document.createElement("div");
329
+ node.innerHTML = result;
330
+ return node;
331
+ }
332
+ return result;
333
+ }
334
+ function renderCodeBlock(b) {
335
+ const lang = extractLang(b.html) || "text";
336
+ const text = b.open ? "" : decodeCodeText(b.html);
337
+ const highlighted = text ? highlight(text, lang) : null;
338
+ const block = document.createElement("div");
339
+ block.className = "flux-code-block" + (b.open ? " flux-streaming" : "");
340
+ const header = document.createElement("div");
341
+ header.className = "flux-code-header";
342
+ const langSpan = document.createElement("span");
343
+ langSpan.className = "flux-code-lang";
344
+ langSpan.textContent = lang;
345
+ header.appendChild(langSpan);
346
+ if (b.open) {
347
+ const pill = document.createElement("span");
348
+ pill.className = "flux-code-streaming-pill";
349
+ pill.textContent = "streaming";
350
+ header.appendChild(pill);
351
+ } else {
352
+ header.appendChild(makeCopyButton(text));
353
+ }
354
+ block.appendChild(header);
355
+ const body = document.createElement("div");
356
+ body.className = "flux-code-body";
357
+ if (highlighted) {
358
+ const pre = document.createElement("pre");
359
+ pre.tabIndex = 0;
360
+ pre.setAttribute("role", "region");
361
+ pre.setAttribute("aria-label", `${lang} code`);
362
+ const code = document.createElement("code");
363
+ code.innerHTML = highlighted;
364
+ pre.appendChild(code);
365
+ body.appendChild(pre);
366
+ } else {
367
+ const div = document.createElement("div");
368
+ div.tabIndex = 0;
369
+ div.setAttribute("role", "region");
370
+ div.setAttribute("aria-label", `${lang} code`);
371
+ div.innerHTML = b.html;
372
+ body.appendChild(div);
373
+ }
374
+ block.appendChild(body);
375
+ return block;
376
+ }
377
+ function renderMathBlock(b) {
378
+ const block = document.createElement("div");
379
+ block.className = "flux-math-block" + (b.open ? " flux-streaming" : "");
380
+ const header = document.createElement("div");
381
+ header.className = "flux-math-header";
382
+ const lang = document.createElement("span");
383
+ lang.className = "flux-math-lang";
384
+ lang.textContent = "math";
385
+ header.appendChild(lang);
386
+ if (b.open) header.appendChild(streamingPill());
387
+ block.appendChild(header);
388
+ const body = document.createElement("div");
389
+ body.className = "flux-math-body";
390
+ body.innerHTML = b.html;
391
+ block.appendChild(body);
392
+ return block;
393
+ }
394
+ function renderMermaid(b) {
395
+ const block = document.createElement("div");
396
+ block.className = "flux-mermaid-block" + (b.open ? " flux-streaming" : "");
397
+ const header = document.createElement("div");
398
+ header.className = "flux-mermaid-header";
399
+ const lang = document.createElement("span");
400
+ lang.className = "flux-mermaid-lang";
401
+ lang.textContent = "mermaid";
402
+ header.appendChild(lang);
403
+ if (b.open) header.appendChild(streamingPill());
404
+ block.appendChild(header);
405
+ const body = document.createElement("div");
406
+ body.className = "flux-mermaid-body";
407
+ body.innerHTML = b.html;
408
+ block.appendChild(body);
409
+ return block;
410
+ }
411
+ function streamingPill() {
412
+ const pill = document.createElement("span");
413
+ pill.className = "flux-code-streaming-pill";
414
+ pill.textContent = "streaming";
415
+ return pill;
416
+ }
417
+ const COPY_ICON = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15V5a2 2 0 0 1 2-2h10"></path></svg><span>Copy</span>';
418
+ const COPIED_ICON = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg><span>Copied</span>';
419
+ function makeCopyButton(text) {
420
+ const btn = document.createElement("button");
421
+ btn.type = "button";
422
+ btn.className = "flux-code-copy";
423
+ btn.setAttribute("aria-label", "Copy code");
424
+ btn.setAttribute("aria-live", "polite");
425
+ btn.innerHTML = COPY_ICON;
426
+ let timer = null;
427
+ btn.addEventListener("click", () => {
428
+ const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
429
+ if (!clip || !clip.writeText || !text) return;
430
+ clip.writeText(text).then(
431
+ () => {
432
+ btn.setAttribute("aria-label", "Copied");
433
+ btn.innerHTML = COPIED_ICON;
434
+ if (timer !== null) clearTimeout(timer);
435
+ timer = setTimeout(() => {
436
+ btn.setAttribute("aria-label", "Copy code");
437
+ btn.innerHTML = COPY_ICON;
438
+ }, 1500);
439
+ },
440
+ // Permission denied / blocked: stay silent, leave button usable.
441
+ () => {
442
+ }
443
+ );
444
+ });
445
+ return btn;
446
+ }
447
+ const unsubscribe = client.subscribe(() => {
448
+ if (dead) return;
449
+ if (batch) {
450
+ if (frame === 0) frame = requestAnimationFrame(flush);
451
+ } else {
452
+ sync();
453
+ }
454
+ });
455
+ function flush() {
456
+ frame = 0;
457
+ sync();
458
+ }
459
+ sync();
460
+ return {
461
+ destroy() {
462
+ if (dead) return;
463
+ dead = true;
464
+ if (frame !== 0) {
465
+ cancelAnimationFrame(frame);
466
+ frame = 0;
467
+ }
468
+ unsubscribe();
469
+ root.remove();
470
+ },
471
+ refresh() {
472
+ if (dead) return;
473
+ sync();
474
+ },
475
+ openBlockId() {
476
+ return tailOpenBlockId(client.getSnapshot());
477
+ }
478
+ };
479
+ }
480
+ function tableData(b) {
481
+ if (b.kind.type !== "Table") return void 0;
482
+ const data = b.kind.data;
483
+ if (!data || !Array.isArray(data.rows) || !Array.isArray(data.aligns) || !Array.isArray(data.headers)) {
484
+ return void 0;
485
+ }
486
+ return data;
487
+ }
488
+ const VOID_ELEMENTS = /* @__PURE__ */ new Set([
489
+ "area",
490
+ "base",
491
+ "br",
492
+ "col",
493
+ "embed",
494
+ "hr",
495
+ "img",
496
+ "input",
497
+ "link",
498
+ "meta",
499
+ "param",
500
+ "source",
501
+ "track",
502
+ "wbr"
503
+ ]);
504
+ function isDepth0Boundary(prefix, full) {
505
+ const c0 = full.charCodeAt(prefix.length);
506
+ if (c0 !== 60) return false;
507
+ const c1 = full.charCodeAt(prefix.length + 1);
508
+ const isLetter = c1 >= 65 && c1 <= 90 || c1 >= 97 && c1 <= 122;
509
+ if (!isLetter) return false;
510
+ let depth = 0;
511
+ let i = 0;
512
+ const n = prefix.length;
513
+ while (i < n) {
514
+ const lt = prefix.indexOf("<", i);
515
+ if (lt === -1) break;
516
+ i = lt + 1;
517
+ if (i >= n) return false;
518
+ const ch = prefix.charCodeAt(i);
519
+ if (ch === 33 || ch === 63) return false;
520
+ let closing = false;
521
+ if (ch === 47) {
522
+ closing = true;
523
+ i++;
524
+ }
525
+ const nameStart = i;
526
+ while (i < n) {
527
+ const t = prefix.charCodeAt(i);
528
+ const nameChar = t >= 65 && t <= 90 || t >= 97 && t <= 122 || t >= 48 && t <= 57 || t === 45;
529
+ if (!nameChar) break;
530
+ i++;
531
+ }
532
+ if (i === nameStart) return false;
533
+ const name = prefix.slice(nameStart, i).toLowerCase();
534
+ const gt = prefix.indexOf(">", i);
535
+ if (gt === -1) return false;
536
+ const selfClosing = prefix.charCodeAt(gt - 1) === 47;
537
+ i = gt + 1;
538
+ if (closing) {
539
+ depth--;
540
+ if (depth < 0) return false;
541
+ } else if (!selfClosing && !VOID_ELEMENTS.has(name)) {
542
+ depth++;
543
+ }
544
+ }
545
+ return depth === 0;
546
+ }
547
+ function tailOpenBlockId(snapshot) {
548
+ const tail = snapshot.length > 0 ? snapshot[snapshot.length - 1] : void 0;
549
+ return tail && tail.open ? tail.id : null;
550
+ }
551
+ function decodeCodeText(html) {
552
+ const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
553
+ if (!m) return "";
554
+ return m[1].replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
555
+ }
556
+ const CONTAINER_ATTR_RE = /([a-zA-Z][a-zA-Z0-9-]*)="([^"]*)"/g;
557
+ function applyOpenTagAttrs(el, html) {
558
+ const gt = html.indexOf(">");
559
+ const open = gt < 0 ? html : html.slice(0, gt);
560
+ let m;
561
+ CONTAINER_ATTR_RE.lastIndex = 0;
562
+ while (m = CONTAINER_ATTR_RE.exec(open)) {
563
+ const name = m[1].toLowerCase();
564
+ if (name === "class" || name === "dir" || name === "role" || name.startsWith("data-")) {
565
+ el.setAttribute(name, m[2]);
566
+ }
567
+ }
568
+ }
569
+ function alertTitleHtml(html) {
570
+ const m = html.match(/<p class="markdown-alert-title"[^>]*>[\s\S]*?<\/p>/);
571
+ return m ? m[0] : "";
572
+ }
573
+ export {
574
+ mountFluxMarkdown,
575
+ tailOpenBlockId
576
+ };