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/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(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/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
|
+
};
|