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/react.d.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { Block, BlockComponentProps, Components } from "./types.js";
|
|
2
|
+
import { FluxClient } from "./client.js";
|
|
3
|
+
import type { ParserConfig, RenderMetricsHook } from "./types-core.js";
|
|
4
|
+
/**
|
|
5
|
+
* Render a streaming markdown document from a FluxClient. Each block is its
|
|
6
|
+
* own memoized React node keyed by its stable parser-assigned ID, so React
|
|
7
|
+
* only reconciles the blocks whose HTML actually changed since the last
|
|
8
|
+
* patch. Heavy renderers (Shiki, KaTeX, Mermaid) defer work until a block
|
|
9
|
+
* is closed.
|
|
10
|
+
*
|
|
11
|
+
* ## Custom components
|
|
12
|
+
*
|
|
13
|
+
* Pass `components` to override rendering (see {@link Components}):
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <FluxMarkdown
|
|
17
|
+
* client={client}
|
|
18
|
+
* components={{
|
|
19
|
+
* table: (p) => <table className="my-table" {...p} />, // tag-level
|
|
20
|
+
* a: (p) => <a target="_blank" rel="noreferrer" {...p} />,
|
|
21
|
+
* CodeBlock: (p) => <MyCodeBlock {...p} />, // block-kind
|
|
22
|
+
* }}
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Rules:
|
|
27
|
+
* - **Tag-level** keys (`table`, `a`, `code`, `h1`…) replace that element
|
|
28
|
+
* wherever it appears inside a block. Applied by converting the block's
|
|
29
|
+
* trusted HTML to a React tree.
|
|
30
|
+
* - **Block-kind** keys ({@link BlockKindTag}: `CodeBlock`, `Mermaid`,
|
|
31
|
+
* `Table`…) replace the whole block; the component gets
|
|
32
|
+
* {@link BlockComponentProps}.
|
|
33
|
+
* - **Open / speculative** blocks always render via `innerHTML` (their HTML
|
|
34
|
+
* is partial); a tag-level override takes effect once the block commits.
|
|
35
|
+
* - With no `components` prop the renderer takes the original fast
|
|
36
|
+
* `innerHTML` path — output is byte-identical to before.
|
|
37
|
+
* - **Memoize `components`** (or hoist it) if you define it inside a
|
|
38
|
+
* component — a fresh object identity each render busts the block memo and
|
|
39
|
+
* forces every block to re-parse on every patch.
|
|
40
|
+
* - For code blocks the built-in highlighter is the default; it is bypassed
|
|
41
|
+
* (so your override wins) when you provide `components.CodeBlock`,
|
|
42
|
+
* `components.pre`, or `components.code`.
|
|
43
|
+
*/
|
|
44
|
+
interface FluxMarkdownProps {
|
|
45
|
+
/**
|
|
46
|
+
* A caller-owned client (you drive `append`/`finalize` and own its lifecycle —
|
|
47
|
+
* the component never destroys it). Mutually exclusive with `stream`; if both
|
|
48
|
+
* are given, `client` wins (a dev warning fires).
|
|
49
|
+
*/
|
|
50
|
+
client?: FluxClient;
|
|
51
|
+
/**
|
|
52
|
+
* A stream to render directly — the 1-line common case. Pass a `Response`, a
|
|
53
|
+
* `ReadableStream<Uint8Array>`, or an `AsyncIterable<string>` (e.g. SSE
|
|
54
|
+
* deltas) and the component owns an internal client, pipes the stream, and
|
|
55
|
+
* destroys it on unmount. A new `stream` identity supersedes the old.
|
|
56
|
+
*/
|
|
57
|
+
stream?: AsyncIterable<string> | ReadableStream<Uint8Array> | Response;
|
|
58
|
+
/** Parser config for the internally-created client (stream mode only). */
|
|
59
|
+
streamConfig?: ParserConfig;
|
|
60
|
+
/** Called if piping the `stream` rejects (the source errored). Not the worker error channel. */
|
|
61
|
+
onStreamError?: (err: Error) => void;
|
|
62
|
+
components?: Components;
|
|
63
|
+
/**
|
|
64
|
+
* Skip layout/paint for off-screen blocks via CSS `content-visibility: auto`
|
|
65
|
+
* — for very long documents (hundreds+ of blocks). Off by default. Applies
|
|
66
|
+
* only to *closed* blocks (the streaming tail always renders fully). Keeps
|
|
67
|
+
* nodes in the DOM; it cuts rendering cost, not node count.
|
|
68
|
+
*/
|
|
69
|
+
virtualize?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Render a bottom snap target so the view follows the streaming tail. This is
|
|
72
|
+
* CSS-only: it emits a sentinel with `scroll-snap-align: end`; **you** add
|
|
73
|
+
* `scroll-snap-type: y proximity` to your scroll container. The view then
|
|
74
|
+
* follows the bottom as content streams in and releases when the user scrolls
|
|
75
|
+
* up (and re-locks when they scroll back near the bottom). Off by default.
|
|
76
|
+
*/
|
|
77
|
+
stickToBottom?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Optional HTML sanitizer applied to every block's HTML before it is injected
|
|
80
|
+
* via `innerHTML` — **including the streaming (open/speculative) tail**, the
|
|
81
|
+
* path that raw `innerHTML` would otherwise expose. Pass a real sanitizer
|
|
82
|
+
* (e.g. DOMPurify's `sanitize`) when rendering untrusted / LLM HTML with
|
|
83
|
+
* `unsafeHtml` on. flux-md stays zero-dep — you bring the sanitizer. The
|
|
84
|
+
* built-in code/math renderers operate on already-escaped content and are not
|
|
85
|
+
* run through it. When omitted, rendering is byte-identical and zero-cost.
|
|
86
|
+
*
|
|
87
|
+
* **Memoize / hoist this** (same trap as `components`): a fresh closure each
|
|
88
|
+
* render busts the per-block memo, so every block re-sanitizes and re-parses
|
|
89
|
+
* on every patch instead of only the streaming tail.
|
|
90
|
+
*/
|
|
91
|
+
sanitize?: (html: string) => string;
|
|
92
|
+
/**
|
|
93
|
+
* Opt-in: when an OPEN (streaming) block re-renders each patch, reuse the
|
|
94
|
+
* React nodes of its top-level children whose HTML is unchanged and re-parse
|
|
95
|
+
* only the new trailing content, instead of re-parsing the block's whole HTML
|
|
96
|
+
* every tick. Only takes effect when `components` (or `sanitize`) routes a
|
|
97
|
+
* block through the parser; the no-`components` fast path (`innerHTML`) is
|
|
98
|
+
* untouched. Off by default and byte-identical to default rendering — enable
|
|
99
|
+
* it for documents with a long, slowly-growing streamed block under a custom
|
|
100
|
+
* `components` map. Closed blocks are already wholesale memo-skipped, so this
|
|
101
|
+
* applies only to the streaming tail.
|
|
102
|
+
*/
|
|
103
|
+
childMemo?: boolean;
|
|
104
|
+
/** Appended to the root's `className` (the `flux-md` class is always present). */
|
|
105
|
+
className?: string;
|
|
106
|
+
/** Set on the root element. */
|
|
107
|
+
id?: string;
|
|
108
|
+
/** Set on the root element (e.g. `"article"`, `"log"`). */
|
|
109
|
+
role?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Make the root a live region so screen readers announce streamed content.
|
|
112
|
+
* `"polite"` (recommended) coalesces rapid updates and announces when the
|
|
113
|
+
* reader is idle — it does **not** read every token. Off by default.
|
|
114
|
+
*/
|
|
115
|
+
"aria-live"?: "off" | "polite" | "assertive";
|
|
116
|
+
/** Live-region atomicity; pair with `aria-live`. Off by default. */
|
|
117
|
+
"aria-atomic"?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Optional render-churn probe. Fires once per ACTUAL render of a block —
|
|
120
|
+
* never for a committed block that memo-skips on a tail-only patch. The
|
|
121
|
+
* callback gets the block id and a {@link RenderMetrics} sample (per-block
|
|
122
|
+
* `renderCount`, `speculativeToggleCount`, `lastRenderMs`, `kind`). Zero
|
|
123
|
+
* overhead when omitted. **Memoize / hoist this** (same trap as `components`):
|
|
124
|
+
* a fresh closure each render busts the per-block memo, forcing every block to
|
|
125
|
+
* re-render on every patch.
|
|
126
|
+
*/
|
|
127
|
+
onRenderMetrics?: RenderMetricsHook;
|
|
128
|
+
/**
|
|
129
|
+
* **Opt-in, off by default.** Render the block list through React's
|
|
130
|
+
* `useDeferredValue`, so a burst of patches can yield to higher-priority
|
|
131
|
+
* updates and the streaming tail commits at a lower priority. When a deferred
|
|
132
|
+
* render is in flight the root carries an extra `flux-deferred` class (style it
|
|
133
|
+
* however you like). This is a *no-op on a single patch*, has no effect during
|
|
134
|
+
* SSR (`useDeferredValue` is a client-only concern), and **does not change
|
|
135
|
+
* output** — only commit timing. Leaving it unset is recommended; rAF
|
|
136
|
+
* coalescing (the DOM adapter's batched path) is the preferred way to absorb
|
|
137
|
+
* high-frequency patches. Provided for callers who specifically want React's
|
|
138
|
+
* concurrent deferral of the visible tail.
|
|
139
|
+
*/
|
|
140
|
+
deferTail?: boolean;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Own a {@link FluxClient} for the lifetime of a component and drive it from a
|
|
144
|
+
* `stream` (a `Response`, `ReadableStream<Uint8Array>`, or
|
|
145
|
+
* `AsyncIterable<string>`). Returns the client (read `outline()` / `getMetrics()`
|
|
146
|
+
* off it, or pass it to `<FluxMarkdown client={…} />`). The client is created
|
|
147
|
+
* once and destroyed on unmount; a new `stream` identity supersedes the old
|
|
148
|
+
* (the prior pipe is aborted, the parser is reset, the new stream is piped).
|
|
149
|
+
*
|
|
150
|
+
* Caveat (matches the manual `useEffect` form): a single-use stream — a
|
|
151
|
+
* `Response`/`ReadableStream`, or an async generator — can only be consumed
|
|
152
|
+
* once, so React **StrictMode**'s dev-only double-mount may truncate it in
|
|
153
|
+
* development. Production mounts once and is unaffected. If you need dev-exact
|
|
154
|
+
* streaming, drive a caller-owned client manually.
|
|
155
|
+
*/
|
|
156
|
+
export declare function useFluxStream(stream: AsyncIterable<string> | ReadableStream<Uint8Array> | Response | null | undefined, options?: {
|
|
157
|
+
config?: ParserConfig;
|
|
158
|
+
onError?: (err: Error) => void;
|
|
159
|
+
}): FluxClient;
|
|
160
|
+
/**
|
|
161
|
+
* Own a {@link FluxClient} driven by a CONTROLLED full string — the bridge for
|
|
162
|
+
* UIs that hold a streaming message as a single growing string prop (the common
|
|
163
|
+
* React shape) rather than as a stream. Pass the whole document-so-far on each
|
|
164
|
+
* render and {@link FluxClient.setContent} diffs it: a prefix-extension appends
|
|
165
|
+
* only the delta; any divergence (e.g. the finished text swapped for a
|
|
166
|
+
* re-processed final string) resets and reparses. Returns the owned client —
|
|
167
|
+
* pass it to `<FluxMarkdown client={…} />` (and read `outline()` etc.).
|
|
168
|
+
*
|
|
169
|
+
* Pass `streaming: false` once the content is final to finalize the stream and
|
|
170
|
+
* commit its last block (only then does a finished code fence highlight + show
|
|
171
|
+
* its copy button). If `streaming` is omitted or `true` the stream is left OPEN
|
|
172
|
+
* — right for a still-growing string, but a *complete static* string rendered as
|
|
173
|
+
* `useFluxMarkdownString(md)` keeps its last block in the streaming state until
|
|
174
|
+
* you pass `{ streaming: false }`. (Inferring "done" from an absent flag is
|
|
175
|
+
* deliberately avoided: it would re-finalize on every token for callers that
|
|
176
|
+
* grow the string without the flag — an O(n²) reparse trap.) The client is
|
|
177
|
+
* created once and destroyed on unmount; StrictMode's dev double-mount is handled
|
|
178
|
+
* (reattach re-feeds the document). For a true stream source
|
|
179
|
+
* (`Response` / `ReadableStream` / SSE generator) use {@link useFluxStream}
|
|
180
|
+
* instead — it avoids buffering the whole document as a string.
|
|
181
|
+
*/
|
|
182
|
+
export declare function useFluxMarkdownString(content: string, options?: {
|
|
183
|
+
config?: ParserConfig;
|
|
184
|
+
streaming?: boolean;
|
|
185
|
+
}): FluxClient;
|
|
186
|
+
declare function FluxMarkdownImpl(props: FluxMarkdownProps): import("react/jsx-runtime").JSX.Element;
|
|
187
|
+
export declare const FluxMarkdown: import("react").MemoExoticComponent<typeof FluxMarkdownImpl>;
|
|
188
|
+
export declare function blockKindProps(block: Block, components?: Components): BlockComponentProps;
|
|
189
|
+
export declare function blocksEqual(prev: {
|
|
190
|
+
block: Block;
|
|
191
|
+
components?: Components;
|
|
192
|
+
virtualize?: boolean;
|
|
193
|
+
sanitize?: (html: string) => string;
|
|
194
|
+
childMemo?: boolean;
|
|
195
|
+
onRenderMetrics?: RenderMetricsHook;
|
|
196
|
+
}, next: {
|
|
197
|
+
block: Block;
|
|
198
|
+
components?: Components;
|
|
199
|
+
virtualize?: boolean;
|
|
200
|
+
sanitize?: (html: string) => string;
|
|
201
|
+
childMemo?: boolean;
|
|
202
|
+
onRenderMetrics?: RenderMetricsHook;
|
|
203
|
+
}): boolean;
|
|
204
|
+
export {};
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import {
|
|
3
|
+
createElement,
|
|
4
|
+
memo,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
useDeferredValue
|
|
11
|
+
} from "react";
|
|
12
|
+
import { FluxClient } from "./client.js";
|
|
13
|
+
import { CodeBlock } from "./renderers/CodeBlock.js";
|
|
14
|
+
import { MathBlock } from "./renderers/Math.js";
|
|
15
|
+
import { Mermaid } from "./renderers/Mermaid.js";
|
|
16
|
+
import { htmlToReact } from "./html-to-react.js";
|
|
17
|
+
function FluxMarkdownFromClient({
|
|
18
|
+
client,
|
|
19
|
+
components,
|
|
20
|
+
virtualize,
|
|
21
|
+
stickToBottom,
|
|
22
|
+
sanitize,
|
|
23
|
+
childMemo,
|
|
24
|
+
className,
|
|
25
|
+
id,
|
|
26
|
+
role,
|
|
27
|
+
"aria-live": ariaLive,
|
|
28
|
+
"aria-atomic": ariaAtomic,
|
|
29
|
+
onRenderMetrics,
|
|
30
|
+
deferTail
|
|
31
|
+
}) {
|
|
32
|
+
const blocks = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot);
|
|
33
|
+
const deferred = useDeferredValue(blocks);
|
|
34
|
+
const rendered = deferTail ? deferred : blocks;
|
|
35
|
+
const isDeferring = deferTail ? rendered !== blocks : false;
|
|
36
|
+
const comps = useMemo(
|
|
37
|
+
() => components && Object.keys(components).length > 0 ? components : void 0,
|
|
38
|
+
[components]
|
|
39
|
+
);
|
|
40
|
+
const onMetrics = useMemo(
|
|
41
|
+
() => onRenderMetrics ? (id2, m) => {
|
|
42
|
+
client.__noteRender();
|
|
43
|
+
onRenderMetrics(id2, m);
|
|
44
|
+
} : void 0,
|
|
45
|
+
[client, onRenderMetrics]
|
|
46
|
+
);
|
|
47
|
+
const rootClass = isDeferring ? className ? `flux-md flux-deferred ${className}` : "flux-md flux-deferred" : className ? `flux-md ${className}` : "flux-md";
|
|
48
|
+
return /* @__PURE__ */ jsxs(
|
|
49
|
+
"div",
|
|
50
|
+
{
|
|
51
|
+
className: rootClass,
|
|
52
|
+
id,
|
|
53
|
+
role,
|
|
54
|
+
"aria-live": ariaLive,
|
|
55
|
+
"aria-atomic": ariaAtomic,
|
|
56
|
+
children: [
|
|
57
|
+
rendered.map((b) => /* @__PURE__ */ jsx(
|
|
58
|
+
BlockView,
|
|
59
|
+
{
|
|
60
|
+
block: b,
|
|
61
|
+
components: comps,
|
|
62
|
+
virtualize,
|
|
63
|
+
sanitize,
|
|
64
|
+
childMemo,
|
|
65
|
+
onRenderMetrics: onMetrics
|
|
66
|
+
},
|
|
67
|
+
b.id
|
|
68
|
+
)),
|
|
69
|
+
stickToBottom && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { scrollSnapAlign: "end" }, className: "flux-bottom-anchor" })
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
function useFluxStream(stream, options) {
|
|
75
|
+
const [client] = useState(() => new FluxClient({ config: options?.config, coalesce: true }));
|
|
76
|
+
const onErrorRef = useRef(options?.onError);
|
|
77
|
+
onErrorRef.current = options?.onError;
|
|
78
|
+
const prevStream = useRef(void 0);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
client.reattach();
|
|
81
|
+
return () => client.destroy();
|
|
82
|
+
}, [client]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (stream == null) return;
|
|
85
|
+
const ac = new AbortController();
|
|
86
|
+
if (prevStream.current !== void 0 && prevStream.current !== stream) {
|
|
87
|
+
client.reset();
|
|
88
|
+
}
|
|
89
|
+
prevStream.current = stream;
|
|
90
|
+
client.pipeFrom(stream, { signal: ac.signal }).catch((e) => {
|
|
91
|
+
if (!ac.signal.aborted) {
|
|
92
|
+
onErrorRef.current?.(e instanceof Error ? e : new Error(String(e)));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return () => ac.abort();
|
|
96
|
+
}, [stream, client]);
|
|
97
|
+
return client;
|
|
98
|
+
}
|
|
99
|
+
function useFluxMarkdownString(content, options) {
|
|
100
|
+
const [client] = useState(() => new FluxClient({ config: options?.config, coalesce: true }));
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
client.reattach();
|
|
103
|
+
return () => client.destroy();
|
|
104
|
+
}, [client]);
|
|
105
|
+
const streaming = options?.streaming;
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
client.setContent(content, { done: streaming === false });
|
|
108
|
+
}, [client, content, streaming]);
|
|
109
|
+
return client;
|
|
110
|
+
}
|
|
111
|
+
function FluxMarkdownFromStream(props) {
|
|
112
|
+
const client = useFluxStream(props.stream, {
|
|
113
|
+
config: props.streamConfig,
|
|
114
|
+
onError: props.onStreamError
|
|
115
|
+
});
|
|
116
|
+
return /* @__PURE__ */ jsx(FluxMarkdownFromClient, { ...props, client });
|
|
117
|
+
}
|
|
118
|
+
function FluxMarkdownImpl(props) {
|
|
119
|
+
if (props.stream != null && props.client == null) {
|
|
120
|
+
return /* @__PURE__ */ jsx(FluxMarkdownFromStream, { ...props });
|
|
121
|
+
}
|
|
122
|
+
if (props.client == null) {
|
|
123
|
+
throw new Error("<FluxMarkdown>: pass either a `client` or a `stream` prop.");
|
|
124
|
+
}
|
|
125
|
+
return /* @__PURE__ */ jsx(FluxMarkdownFromClient, { ...props });
|
|
126
|
+
}
|
|
127
|
+
const FluxMarkdown = memo(FluxMarkdownImpl);
|
|
128
|
+
function decodeEntities(s) {
|
|
129
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
130
|
+
}
|
|
131
|
+
function decodeCodeText(html) {
|
|
132
|
+
const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
|
|
133
|
+
return m ? decodeEntities(m[1]) : "";
|
|
134
|
+
}
|
|
135
|
+
function decodeMathText(html) {
|
|
136
|
+
const d = html.match(/<div class="math math-display">([\s\S]*?)<\/div>/);
|
|
137
|
+
if (d) return decodeEntities(d[1]);
|
|
138
|
+
return decodeCodeText(html);
|
|
139
|
+
}
|
|
140
|
+
function blockKindProps(block, components) {
|
|
141
|
+
const props = {
|
|
142
|
+
block,
|
|
143
|
+
html: block.html,
|
|
144
|
+
open: block.open,
|
|
145
|
+
speculative: block.speculative
|
|
146
|
+
};
|
|
147
|
+
const data = block.kind.data;
|
|
148
|
+
if (block.kind.type === "CodeBlock") {
|
|
149
|
+
props.text = data?.code ?? decodeCodeText(block.html);
|
|
150
|
+
props.language = data?.lang ?? "";
|
|
151
|
+
if (typeof data?.code === "string") {
|
|
152
|
+
props.code = { lang: data.lang ?? null, code: data.code };
|
|
153
|
+
}
|
|
154
|
+
} else if (block.kind.type === "MathBlock") {
|
|
155
|
+
props.text = data?.latex ?? decodeMathText(block.html);
|
|
156
|
+
if (typeof data?.latex === "string") {
|
|
157
|
+
props.math = { latex: data.latex };
|
|
158
|
+
}
|
|
159
|
+
} else if (block.kind.type === "List") {
|
|
160
|
+
if (data && typeof data.start === "number") {
|
|
161
|
+
props.list = { ordered: !!data.ordered, start: data.start, items: data.items };
|
|
162
|
+
}
|
|
163
|
+
} else if (block.kind.type === "Component") {
|
|
164
|
+
props.tag = data?.tag ?? "";
|
|
165
|
+
props.attrs = reactAttrs(data?.attrs ?? []);
|
|
166
|
+
props.html = componentInnerHtml(block.html, props.tag);
|
|
167
|
+
props.children = htmlToReact(props.html, components ?? {});
|
|
168
|
+
} else if (block.kind.type === "Table") {
|
|
169
|
+
props.table = block.kind.data;
|
|
170
|
+
} else if (block.kind.type === "Heading") {
|
|
171
|
+
if (typeof block.kind.data === "object" && block.kind.data !== null) {
|
|
172
|
+
props.heading = block.kind.data;
|
|
173
|
+
}
|
|
174
|
+
} else if (block.kind.type === "Blockquote" || block.kind.type === "Alert") {
|
|
175
|
+
const cd = block.kind.data;
|
|
176
|
+
if (cd && Array.isArray(cd.nested)) {
|
|
177
|
+
props.container = { nested: cd.nested };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return props;
|
|
181
|
+
}
|
|
182
|
+
const REACT_ATTR_NAME = Object.assign(/* @__PURE__ */ Object.create(null), {
|
|
183
|
+
class: "className",
|
|
184
|
+
for: "htmlFor"
|
|
185
|
+
});
|
|
186
|
+
const ATTR_DENY = /* @__PURE__ */ new Set([
|
|
187
|
+
"dangerouslysetinnerhtml",
|
|
188
|
+
"ref",
|
|
189
|
+
"key",
|
|
190
|
+
"defaultvalue",
|
|
191
|
+
"defaultchecked",
|
|
192
|
+
"suppresshydrationwarning",
|
|
193
|
+
"suppresscontenteditablewarning"
|
|
194
|
+
]);
|
|
195
|
+
const SAFE_ATTR_NAME = /^[a-z][a-z0-9-]*$/i;
|
|
196
|
+
function reactAttrs(pairs) {
|
|
197
|
+
const out = {};
|
|
198
|
+
for (const [k, v] of pairs) {
|
|
199
|
+
const lower = k.toLowerCase();
|
|
200
|
+
if (lower.startsWith("on")) continue;
|
|
201
|
+
if (ATTR_DENY.has(lower)) continue;
|
|
202
|
+
if (!(lower in REACT_ATTR_NAME) && !SAFE_ATTR_NAME.test(k)) continue;
|
|
203
|
+
out[REACT_ATTR_NAME[lower] ?? k] = v;
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function componentInnerHtml(html, tag) {
|
|
208
|
+
const gt = html.indexOf(">");
|
|
209
|
+
if (gt < 0) return "";
|
|
210
|
+
let inner = html.slice(gt + 1);
|
|
211
|
+
const close = `</${tag}>`;
|
|
212
|
+
if (inner.endsWith(close)) inner = inner.slice(0, -close.length);
|
|
213
|
+
return inner.replace(/^\n/, "").replace(/\n$/, "");
|
|
214
|
+
}
|
|
215
|
+
const CHILD_MEMO_CAP = 4096;
|
|
216
|
+
function SafeHtml({
|
|
217
|
+
html,
|
|
218
|
+
components,
|
|
219
|
+
childMemo
|
|
220
|
+
}) {
|
|
221
|
+
const memoRef = useRef(null);
|
|
222
|
+
const compRef = useRef(null);
|
|
223
|
+
return useMemo(() => {
|
|
224
|
+
if (!childMemo) {
|
|
225
|
+
memoRef.current = null;
|
|
226
|
+
return htmlToReact(html, components);
|
|
227
|
+
}
|
|
228
|
+
if (memoRef.current === null || compRef.current !== components) {
|
|
229
|
+
memoRef.current = /* @__PURE__ */ new Map();
|
|
230
|
+
compRef.current = components;
|
|
231
|
+
}
|
|
232
|
+
const map = memoRef.current;
|
|
233
|
+
if (map.size > CHILD_MEMO_CAP) map.clear();
|
|
234
|
+
return htmlToReact(html, components, map);
|
|
235
|
+
}, [html, components, childMemo]);
|
|
236
|
+
}
|
|
237
|
+
function KeyedListItemImpl({
|
|
238
|
+
html,
|
|
239
|
+
components,
|
|
240
|
+
sanitize
|
|
241
|
+
}) {
|
|
242
|
+
const safe = sanitize ? sanitize(html) : html;
|
|
243
|
+
if (components) {
|
|
244
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(SafeHtml, { html: safe, components }) });
|
|
245
|
+
}
|
|
246
|
+
return /* @__PURE__ */ jsx("li", { dangerouslySetInnerHTML: { __html: safe } });
|
|
247
|
+
}
|
|
248
|
+
const KeyedListItem = memo(KeyedListItemImpl);
|
|
249
|
+
function KeyedList({
|
|
250
|
+
className,
|
|
251
|
+
ordered,
|
|
252
|
+
start,
|
|
253
|
+
items,
|
|
254
|
+
components,
|
|
255
|
+
sanitize
|
|
256
|
+
}) {
|
|
257
|
+
const children = items.map((it, i) => /* @__PURE__ */ jsx(KeyedListItem, { html: it.html, components, sanitize }, i));
|
|
258
|
+
const inner = ordered ? createElement("ol", start !== void 0 && start !== 1 ? { start } : null, children) : createElement("ul", null, children);
|
|
259
|
+
return /* @__PURE__ */ jsx("div", { className, children: inner });
|
|
260
|
+
}
|
|
261
|
+
function KeyedContainer({
|
|
262
|
+
block,
|
|
263
|
+
nested,
|
|
264
|
+
components
|
|
265
|
+
}) {
|
|
266
|
+
const tagName = block.kind.type === "Alert" ? "div" : "blockquote";
|
|
267
|
+
const attrs = useMemo(() => parseOpenTagAttrs(block.html), [block.html]);
|
|
268
|
+
const children = [];
|
|
269
|
+
if (block.kind.type === "Alert") {
|
|
270
|
+
const title = alertTitleHtml(block.html);
|
|
271
|
+
if (title) {
|
|
272
|
+
children.push(/* @__PURE__ */ jsx(SafeHtml, { html: title, components }, "title"));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (let i = 0; i < nested.length; i++) {
|
|
276
|
+
children.push(/* @__PURE__ */ jsx(SafeHtml, { html: nested[i].html, components }, i));
|
|
277
|
+
}
|
|
278
|
+
return createElement(tagName, attrs, children);
|
|
279
|
+
}
|
|
280
|
+
const CONTAINER_ATTR_RE = /([a-zA-Z][a-zA-Z0-9-]*)="([^"]*)"/g;
|
|
281
|
+
function parseOpenTagAttrs(html) {
|
|
282
|
+
const gt = html.indexOf(">");
|
|
283
|
+
const open = gt < 0 ? html : html.slice(0, gt);
|
|
284
|
+
const out = {};
|
|
285
|
+
let m;
|
|
286
|
+
CONTAINER_ATTR_RE.lastIndex = 0;
|
|
287
|
+
while (m = CONTAINER_ATTR_RE.exec(open)) {
|
|
288
|
+
const name = m[1].toLowerCase();
|
|
289
|
+
if (name === "class") out.className = m[2];
|
|
290
|
+
else if (name === "dir" || name === "role" || name.startsWith("data-")) out[name] = m[2];
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
function alertTitleHtml(html) {
|
|
295
|
+
const m = html.match(/<p class="markdown-alert-title"[^>]*>.*?<\/p>/s);
|
|
296
|
+
return m ? m[0] : "";
|
|
297
|
+
}
|
|
298
|
+
const NO_COMPONENTS = Object.freeze(/* @__PURE__ */ Object.create(null));
|
|
299
|
+
const TableCellView = memo(function TableCellView2({
|
|
300
|
+
tag,
|
|
301
|
+
html,
|
|
302
|
+
align,
|
|
303
|
+
scope,
|
|
304
|
+
components,
|
|
305
|
+
sanitize
|
|
306
|
+
}) {
|
|
307
|
+
const tree = useMemo(
|
|
308
|
+
() => htmlToReact(sanitize ? sanitize(html) : html, components),
|
|
309
|
+
[html, components, sanitize]
|
|
310
|
+
);
|
|
311
|
+
return createElement(
|
|
312
|
+
tag,
|
|
313
|
+
{
|
|
314
|
+
scope: tag === "th" && scope ? "col" : void 0,
|
|
315
|
+
style: align ? { textAlign: align } : void 0
|
|
316
|
+
},
|
|
317
|
+
tree
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
function KeyedTable({
|
|
321
|
+
data,
|
|
322
|
+
html,
|
|
323
|
+
components,
|
|
324
|
+
sanitize
|
|
325
|
+
}) {
|
|
326
|
+
const comps = components ?? NO_COMPONENTS;
|
|
327
|
+
const headerPrefix = useMemo(() => {
|
|
328
|
+
const i = html.indexOf("</thead>");
|
|
329
|
+
return i === -1 ? html : html.slice(0, i);
|
|
330
|
+
}, [html]);
|
|
331
|
+
const dir = useMemo(
|
|
332
|
+
() => headerPrefix.startsWith('<table dir="auto"') ? "auto" : void 0,
|
|
333
|
+
[headerPrefix]
|
|
334
|
+
);
|
|
335
|
+
const scope = useMemo(() => headerPrefix.includes('<th scope="col"'), [headerPrefix]);
|
|
336
|
+
const aligns = data.aligns;
|
|
337
|
+
return /* @__PURE__ */ jsxs("table", { dir, children: [
|
|
338
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: data.headers.map((c, j) => /* @__PURE__ */ jsx(
|
|
339
|
+
TableCellView,
|
|
340
|
+
{
|
|
341
|
+
tag: "th",
|
|
342
|
+
html: c.html,
|
|
343
|
+
align: aligns[j] ?? null,
|
|
344
|
+
scope,
|
|
345
|
+
components: comps,
|
|
346
|
+
sanitize
|
|
347
|
+
},
|
|
348
|
+
j
|
|
349
|
+
)) }) }),
|
|
350
|
+
data.rows.length > 0 && /* @__PURE__ */ jsx("tbody", { children: data.rows.map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.map((c, j) => /* @__PURE__ */ jsx(
|
|
351
|
+
TableCellView,
|
|
352
|
+
{
|
|
353
|
+
tag: "td",
|
|
354
|
+
html: c.html,
|
|
355
|
+
align: aligns[j] ?? null,
|
|
356
|
+
scope,
|
|
357
|
+
components: comps,
|
|
358
|
+
sanitize
|
|
359
|
+
},
|
|
360
|
+
j
|
|
361
|
+
)) }, i)) })
|
|
362
|
+
] });
|
|
363
|
+
}
|
|
364
|
+
const INTRINSIC_PX = {
|
|
365
|
+
Paragraph: 80,
|
|
366
|
+
Heading: 44,
|
|
367
|
+
CodeBlock: 300,
|
|
368
|
+
MathBlock: 140,
|
|
369
|
+
Mermaid: 220,
|
|
370
|
+
List: 120,
|
|
371
|
+
Blockquote: 100,
|
|
372
|
+
Alert: 120,
|
|
373
|
+
Table: 200,
|
|
374
|
+
Rule: 24,
|
|
375
|
+
Html: 80,
|
|
376
|
+
Component: 120
|
|
377
|
+
};
|
|
378
|
+
function BlockViewImpl(props) {
|
|
379
|
+
const { block, virtualize, onRenderMetrics } = props;
|
|
380
|
+
const metricsRef = useRef(
|
|
381
|
+
onRenderMetrics ? { renderCount: 0, toggle: 0, speculative: block.speculative } : null
|
|
382
|
+
);
|
|
383
|
+
const hasPerf = typeof performance !== "undefined";
|
|
384
|
+
const t0 = onRenderMetrics && hasPerf ? performance.now() : 0;
|
|
385
|
+
const content = renderBlockContent(props);
|
|
386
|
+
if (onRenderMetrics) {
|
|
387
|
+
const m = metricsRef.current ??= { renderCount: 0, toggle: 0, speculative: block.speculative };
|
|
388
|
+
m.renderCount++;
|
|
389
|
+
if (m.speculative !== block.speculative) {
|
|
390
|
+
m.toggle++;
|
|
391
|
+
m.speculative = block.speculative;
|
|
392
|
+
}
|
|
393
|
+
onRenderMetrics(block.id, {
|
|
394
|
+
renderCount: m.renderCount,
|
|
395
|
+
speculativeToggleCount: m.toggle,
|
|
396
|
+
lastRenderMs: hasPerf ? performance.now() - t0 : 0,
|
|
397
|
+
kind: block.kind.type
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (virtualize && !block.open && !block.speculative) {
|
|
401
|
+
const px = INTRINSIC_PX[block.kind.type] ?? 120;
|
|
402
|
+
return /* @__PURE__ */ jsx("div", { style: { contentVisibility: "auto", containIntrinsicSize: `auto ${px}px` }, children: content });
|
|
403
|
+
}
|
|
404
|
+
return content;
|
|
405
|
+
}
|
|
406
|
+
function renderBlockContent({
|
|
407
|
+
block,
|
|
408
|
+
components,
|
|
409
|
+
sanitize,
|
|
410
|
+
childMemo
|
|
411
|
+
}) {
|
|
412
|
+
const kind = block.kind.type;
|
|
413
|
+
if (components) {
|
|
414
|
+
if (kind === "Component") {
|
|
415
|
+
const tag = block.kind.data?.tag;
|
|
416
|
+
const override = tag && components[tag] || components.Component;
|
|
417
|
+
if (override) {
|
|
418
|
+
return createElement(override, blockKindProps(block, components));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const blockOverride = components[kind];
|
|
422
|
+
if (blockOverride) {
|
|
423
|
+
return createElement(blockOverride, blockKindProps(block, components));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
switch (kind) {
|
|
427
|
+
case "CodeBlock": {
|
|
428
|
+
const wantsCodeOverride = !!components && (!!components.pre || !!components.code);
|
|
429
|
+
if (!wantsCodeOverride) return /* @__PURE__ */ jsx(CodeBlock, { html: block.html, open: block.open });
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case "MathBlock":
|
|
433
|
+
return /* @__PURE__ */ jsx(MathBlock, { html: block.html, open: block.open });
|
|
434
|
+
case "Mermaid":
|
|
435
|
+
return /* @__PURE__ */ jsx(Mermaid, { html: block.html, open: block.open });
|
|
436
|
+
}
|
|
437
|
+
const className = "flux-block flux-block-" + kind.toLowerCase() + (block.open ? " flux-open" : "") + (block.speculative ? " flux-speculative" : "");
|
|
438
|
+
if (kind === "Table" && block.open) {
|
|
439
|
+
const data = block.kind.data;
|
|
440
|
+
if (data && Array.isArray(data.rows)) {
|
|
441
|
+
return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(KeyedTable, { data, html: block.html, components, sanitize }) });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (block.open && kind === "List") {
|
|
445
|
+
const ld = block.kind.data;
|
|
446
|
+
const items = ld?.items;
|
|
447
|
+
const tagOverride = !!components && (!!components.ul || !!components.ol || !!components.li);
|
|
448
|
+
if (Array.isArray(items) && items.length > 0 && !tagOverride) {
|
|
449
|
+
return /* @__PURE__ */ jsx(
|
|
450
|
+
KeyedList,
|
|
451
|
+
{
|
|
452
|
+
className,
|
|
453
|
+
ordered: !!ld?.ordered,
|
|
454
|
+
start: ld?.start,
|
|
455
|
+
items,
|
|
456
|
+
components,
|
|
457
|
+
sanitize
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (components) {
|
|
463
|
+
if (block.open && !sanitize && (kind === "Blockquote" || kind === "Alert")) {
|
|
464
|
+
const nested = block.kind.data?.nested;
|
|
465
|
+
if (Array.isArray(nested)) {
|
|
466
|
+
return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(KeyedContainer, { block, nested, components }) });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const safe = sanitize ? sanitize(block.html) : block.html;
|
|
470
|
+
return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(SafeHtml, { html: safe, components, childMemo: childMemo && block.open }) });
|
|
471
|
+
}
|
|
472
|
+
return /* @__PURE__ */ jsx(
|
|
473
|
+
"div",
|
|
474
|
+
{
|
|
475
|
+
className,
|
|
476
|
+
dangerouslySetInnerHTML: { __html: sanitize ? sanitize(block.html) : block.html }
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
function blocksEqual(prev, next) {
|
|
481
|
+
return prev.block.id === next.block.id && prev.block.html === next.block.html && prev.block.open === next.block.open && prev.block.speculative === next.block.speculative && prev.components === next.components && prev.virtualize === next.virtualize && prev.sanitize === next.sanitize && prev.childMemo === next.childMemo && prev.onRenderMetrics === next.onRenderMetrics;
|
|
482
|
+
}
|
|
483
|
+
const BlockView = memo(BlockViewImpl, blocksEqual);
|
|
484
|
+
export {
|
|
485
|
+
FluxMarkdown,
|
|
486
|
+
blockKindProps,
|
|
487
|
+
blocksEqual,
|
|
488
|
+
useFluxMarkdownString,
|
|
489
|
+
useFluxStream
|
|
490
|
+
};
|