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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { highlight } from "../hi.js";
|
|
4
|
+
import { extractLang } from "../block-props.js";
|
|
5
|
+
function decodeText(html) {
|
|
6
|
+
const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
|
|
7
|
+
if (!m) return "";
|
|
8
|
+
return m[1].replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
9
|
+
}
|
|
10
|
+
function CodeBlockImpl({ html, open }) {
|
|
11
|
+
const lang = extractLang(html) || "text";
|
|
12
|
+
const text = useMemo(() => open ? "" : decodeText(html), [html, open]);
|
|
13
|
+
const highlighted = useMemo(() => {
|
|
14
|
+
if (!text) return null;
|
|
15
|
+
return highlight(text, lang);
|
|
16
|
+
}, [text, lang]);
|
|
17
|
+
const [copied, setCopied] = useState(false);
|
|
18
|
+
const timerRef = useRef(null);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (open) setCopied(false);
|
|
21
|
+
}, [open, html]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
return () => {
|
|
24
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
const onCopy = useCallback(() => {
|
|
28
|
+
const write = typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText ? navigator.clipboard.writeText.bind(navigator.clipboard) : null;
|
|
29
|
+
if (!write || !text) return;
|
|
30
|
+
write(text).then(
|
|
31
|
+
() => {
|
|
32
|
+
setCopied(true);
|
|
33
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
34
|
+
timerRef.current = setTimeout(() => setCopied(false), 1500);
|
|
35
|
+
},
|
|
36
|
+
// Permission denied / blocked: stay silent, leave button usable.
|
|
37
|
+
() => {
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
}, [text]);
|
|
41
|
+
return /* @__PURE__ */ jsxs("div", { className: "flux-code-block" + (open ? " flux-streaming" : ""), children: [
|
|
42
|
+
/* @__PURE__ */ jsxs("div", { className: "flux-code-header", children: [
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: "flux-code-lang", children: lang }),
|
|
44
|
+
open ? /* @__PURE__ */ jsx("span", { className: "flux-code-streaming-pill", children: "streaming" }) : /* @__PURE__ */ jsx(
|
|
45
|
+
"button",
|
|
46
|
+
{
|
|
47
|
+
type: "button",
|
|
48
|
+
className: "flux-code-copy",
|
|
49
|
+
onClick: onCopy,
|
|
50
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
51
|
+
"aria-live": "polite",
|
|
52
|
+
children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
53
|
+
/* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) }),
|
|
54
|
+
/* @__PURE__ */ jsx("span", { children: "Copied" })
|
|
55
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
56
|
+
/* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
57
|
+
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "11", height: "11", rx: "2" }),
|
|
58
|
+
/* @__PURE__ */ jsx("path", { d: "M5 15V5a2 2 0 0 1 2-2h10" })
|
|
59
|
+
] }),
|
|
60
|
+
/* @__PURE__ */ jsx("span", { children: "Copy" })
|
|
61
|
+
] })
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
] }),
|
|
65
|
+
/* @__PURE__ */ jsx("div", { className: "flux-code-body", children: highlighted ? (
|
|
66
|
+
// tabIndex=0 + role/label so keyboard users can scroll long code and
|
|
67
|
+
// screen readers announce the region with its language.
|
|
68
|
+
/* @__PURE__ */ jsx("pre", { tabIndex: 0, role: "region", "aria-label": `${lang} code`, children: /* @__PURE__ */ jsx("code", { dangerouslySetInnerHTML: { __html: highlighted } }) })
|
|
69
|
+
) : /* @__PURE__ */ jsx("div", { tabIndex: 0, role: "region", "aria-label": `${lang} code`, dangerouslySetInnerHTML: { __html: html } }) })
|
|
70
|
+
] });
|
|
71
|
+
}
|
|
72
|
+
const CodeBlock = memo(CodeBlockImpl);
|
|
73
|
+
export {
|
|
74
|
+
CodeBlock
|
|
75
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default math block — emits the LaTeX inside a `<div class="math
|
|
3
|
+
* math-display">` (or `<span class="math math-inline">` for inline). flux-md
|
|
4
|
+
* stays zero-dep, so it does not ship KaTeX/MathJax: bring your own typesetter
|
|
5
|
+
* (run it over the rendered `.math` nodes once a block closes), or override
|
|
6
|
+
* this slot via `components.MathBlock` to render the LaTeX yourself.
|
|
7
|
+
*/
|
|
8
|
+
interface Props {
|
|
9
|
+
html: string;
|
|
10
|
+
open: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare function MathImpl({ html, open }: Props): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare const MathBlock: import("react").MemoExoticComponent<typeof MathImpl>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
function MathImpl({ html, open }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs("div", { className: "flux-math-block" + (open ? " flux-streaming" : ""), children: [
|
|
5
|
+
/* @__PURE__ */ jsxs("div", { className: "flux-math-header", children: [
|
|
6
|
+
/* @__PURE__ */ jsx("span", { className: "flux-math-lang", children: "math" }),
|
|
7
|
+
open && /* @__PURE__ */ jsx("span", { className: "flux-code-streaming-pill", children: "streaming" })
|
|
8
|
+
] }),
|
|
9
|
+
/* @__PURE__ */ jsx("div", { className: "flux-math-body", dangerouslySetInnerHTML: { __html: html } })
|
|
10
|
+
] });
|
|
11
|
+
}
|
|
12
|
+
const MathBlock = memo(MathImpl);
|
|
13
|
+
export {
|
|
14
|
+
MathBlock
|
|
15
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default mermaid block — renders the diagram source verbatim in a code-like
|
|
3
|
+
* container. flux-md stays zero-dep and does not ship the Mermaid runtime:
|
|
4
|
+
* override this slot via `components.Mermaid` to render to SVG with your own
|
|
5
|
+
* Mermaid build (typically `mermaid.run` over the closed-block source text).
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
html: string;
|
|
9
|
+
open: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare function MermaidImpl({ html, open }: Props): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare const Mermaid: import("react").MemoExoticComponent<typeof MermaidImpl>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
function MermaidImpl({ html, open }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs("div", { className: "flux-mermaid-block" + (open ? " flux-streaming" : ""), children: [
|
|
5
|
+
/* @__PURE__ */ jsxs("div", { className: "flux-mermaid-header", children: [
|
|
6
|
+
/* @__PURE__ */ jsx("span", { className: "flux-mermaid-lang", children: "mermaid" }),
|
|
7
|
+
open && /* @__PURE__ */ jsx("span", { className: "flux-code-streaming-pill", children: "streaming" })
|
|
8
|
+
] }),
|
|
9
|
+
/* @__PURE__ */ jsx("div", { className: "flux-mermaid-body", dangerouslySetInnerHTML: { __html: html } })
|
|
10
|
+
] });
|
|
11
|
+
}
|
|
12
|
+
const Mermaid = memo(MermaidImpl);
|
|
13
|
+
export {
|
|
14
|
+
Mermaid
|
|
15
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Components, ParserConfig } from "./types.js";
|
|
3
|
+
interface FluxMarkdownStaticProps {
|
|
4
|
+
/** The complete markdown to render (server/static use is for finished content). */
|
|
5
|
+
content: string;
|
|
6
|
+
/** Parser config (same shape as the streaming client's). */
|
|
7
|
+
config?: ParserConfig;
|
|
8
|
+
/** Tag-level / block-kind / component-tag overrides (see {@link Components}). */
|
|
9
|
+
components?: Components;
|
|
10
|
+
/** Appended to the root's `className` (the `flux-md` class is always present). */
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Set on the root element. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Set on the root element (e.g. `"article"`). */
|
|
15
|
+
role?: string;
|
|
16
|
+
/** Make the root a live region (parity with `<FluxMarkdown>` if you hydrate). */
|
|
17
|
+
"aria-live"?: "off" | "polite" | "assertive";
|
|
18
|
+
/** Live-region atomicity; pair with `aria-live`. */
|
|
19
|
+
"aria-atomic"?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Synchronous, worker-free React rendering of finished markdown — a React Server
|
|
23
|
+
* Component, or any one-shot SSR / static render. Emits the `flux-md` root +
|
|
24
|
+
* per-block structure with the same `components` overrides (inline/block
|
|
25
|
+
* component tags dispatch here too). Requires `initFlux` (or `initFluxSync`)
|
|
26
|
+
* from `flux-md/server` to have run. Uses no hooks (RSC-safe). A **render-once**
|
|
27
|
+
* component: for live streaming, client-side code highlighting, or Mermaid use
|
|
28
|
+
* the client `<FluxMarkdown>` instead (and if you SSR-then-hydrate, render the
|
|
29
|
+
* *same* component on both sides).
|
|
30
|
+
*/
|
|
31
|
+
export declare function FluxMarkdownStatic({ content, config, components, className, id, role, "aria-live": ariaLive, "aria-atomic": ariaAtomic, }: FluxMarkdownStaticProps): ReactNode;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { htmlToReact } from "./html-to-react.js";
|
|
3
|
+
import { blockKindProps } from "./react.js";
|
|
4
|
+
import { parseToBlocks } from "./server.js";
|
|
5
|
+
function renderStaticBlock(block, components) {
|
|
6
|
+
const kind = block.kind.type;
|
|
7
|
+
if (components) {
|
|
8
|
+
if (kind === "Component") {
|
|
9
|
+
const tag = block.kind.data?.tag;
|
|
10
|
+
const override = tag && components[tag] || components.Component;
|
|
11
|
+
if (override) return createElement(override, { key: block.id, ...blockKindProps(block, components) });
|
|
12
|
+
}
|
|
13
|
+
const blockOverride = components[kind];
|
|
14
|
+
if (blockOverride) return createElement(blockOverride, { key: block.id, ...blockKindProps(block, components) });
|
|
15
|
+
}
|
|
16
|
+
const className = "flux-block flux-block-" + kind.toLowerCase() + (block.open ? " flux-open" : "") + (block.speculative ? " flux-speculative" : "");
|
|
17
|
+
if (components) {
|
|
18
|
+
return createElement("div", { key: block.id, className }, htmlToReact(block.html, components));
|
|
19
|
+
}
|
|
20
|
+
return createElement("div", { key: block.id, className, dangerouslySetInnerHTML: { __html: block.html } });
|
|
21
|
+
}
|
|
22
|
+
function FluxMarkdownStatic({
|
|
23
|
+
content,
|
|
24
|
+
config,
|
|
25
|
+
components,
|
|
26
|
+
className,
|
|
27
|
+
id,
|
|
28
|
+
role,
|
|
29
|
+
"aria-live": ariaLive,
|
|
30
|
+
"aria-atomic": ariaAtomic
|
|
31
|
+
}) {
|
|
32
|
+
const blocks = parseToBlocks(content, { config });
|
|
33
|
+
const comps = components && Object.keys(components).length > 0 ? components : void 0;
|
|
34
|
+
return createElement(
|
|
35
|
+
"div",
|
|
36
|
+
{
|
|
37
|
+
className: className ? `flux-md ${className}` : "flux-md",
|
|
38
|
+
id,
|
|
39
|
+
role,
|
|
40
|
+
"aria-live": ariaLive,
|
|
41
|
+
"aria-atomic": ariaAtomic
|
|
42
|
+
},
|
|
43
|
+
blocks.map((b) => renderStaticBlock(b, comps))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
FluxMarkdownStatic
|
|
48
|
+
};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Block, ParserConfig } from "./types.js";
|
|
2
|
+
/** Has the sync WASM core been initialized in this process? */
|
|
3
|
+
export declare function isFluxReady(): boolean;
|
|
4
|
+
/** Initialize the sync core from compiled WASM bytes (or a `WebAssembly.Module`).
|
|
5
|
+
* Idempotent. Use on runtimes without a filesystem (edge) or to control exactly
|
|
6
|
+
* when init happens; otherwise {@link initFlux} auto-loads the co-located WASM. */
|
|
7
|
+
export declare function initFluxSync(wasm: BufferSource | WebAssembly.Module): void;
|
|
8
|
+
/** Initialize the sync core once. In Node it reads the package's co-located
|
|
9
|
+
* `.wasm` off disk (Node's `fetch` can't load `file://`); on the web it fetches
|
|
10
|
+
* the bundler-resolved asset URL. Pass `{ wasm }` to supply bytes yourself
|
|
11
|
+
* (edge runtimes). Safe to call repeatedly / concurrently. */
|
|
12
|
+
export declare function initFlux(opts?: {
|
|
13
|
+
wasm?: BufferSource | WebAssembly.Module;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a complete markdown string to its block array synchronously (committed +
|
|
17
|
+
* any trailing block, in document order). Requires {@link initFlux} to have run.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseToBlocks(markdown: string, opts?: {
|
|
20
|
+
config?: ParserConfig;
|
|
21
|
+
}): Block[];
|
|
22
|
+
/**
|
|
23
|
+
* Render a complete markdown string to an HTML string synchronously — no worker,
|
|
24
|
+
* no React. The concatenated per-block HTML (XSS-safe with `unsafeHtml` off).
|
|
25
|
+
* For component dispatch / a `<FluxMarkdown>`-matching React tree, use
|
|
26
|
+
* `FluxMarkdownStatic` from `flux-md/server/react` with your framework's server
|
|
27
|
+
* renderer instead.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderToString(markdown: string, opts?: {
|
|
30
|
+
config?: ParserConfig;
|
|
31
|
+
}): string;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import initWasmAsync, { FluxParser, initSync } from "./wasm/flux_md_core.js";
|
|
2
|
+
let ready = false;
|
|
3
|
+
function isFluxReady() {
|
|
4
|
+
return ready;
|
|
5
|
+
}
|
|
6
|
+
function initFluxSync(wasm) {
|
|
7
|
+
if (ready) return;
|
|
8
|
+
initSync({ module: wasm });
|
|
9
|
+
ready = true;
|
|
10
|
+
}
|
|
11
|
+
let initPromise = null;
|
|
12
|
+
function initFlux(opts) {
|
|
13
|
+
if (ready) return Promise.resolve();
|
|
14
|
+
if (opts?.wasm) {
|
|
15
|
+
initFluxSync(opts.wasm);
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
if (!initPromise) {
|
|
19
|
+
initPromise = (async () => {
|
|
20
|
+
const wasmUrl = new URL("./wasm/flux_md_core_bg.wasm", import.meta.url);
|
|
21
|
+
if (wasmUrl.protocol === "file:") {
|
|
22
|
+
const { readFile } = await import("node:fs/promises");
|
|
23
|
+
initFluxSync(await readFile(wasmUrl));
|
|
24
|
+
} else {
|
|
25
|
+
await initWasmAsync({ module_or_path: wasmUrl });
|
|
26
|
+
ready = true;
|
|
27
|
+
}
|
|
28
|
+
})().catch((err) => {
|
|
29
|
+
initPromise = null;
|
|
30
|
+
throw err;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return initPromise;
|
|
34
|
+
}
|
|
35
|
+
function makeParser(config) {
|
|
36
|
+
const p = new FluxParser();
|
|
37
|
+
p.setGfmAutolinks(config?.gfmAutolinks ?? true);
|
|
38
|
+
p.setGfmAlerts(config?.gfmAlerts ?? true);
|
|
39
|
+
p.setGfmFootnotes(config?.gfmFootnotes ?? false);
|
|
40
|
+
p.setGfmMath(config?.gfmMath ?? false);
|
|
41
|
+
p.setDirAuto(config?.dirAuto ?? false);
|
|
42
|
+
p.setA11y(config?.a11y ?? false);
|
|
43
|
+
p.setUnsafeHtml(config?.unsafeHtml ?? false);
|
|
44
|
+
p.setComponentTags(config?.componentTags ?? []);
|
|
45
|
+
p.setInlineComponentTags(config?.inlineComponentTags ?? []);
|
|
46
|
+
p.setHtmlSanitize(
|
|
47
|
+
config?.htmlAllowlist !== void 0 || config?.dropHtmlTags !== void 0,
|
|
48
|
+
config?.htmlAllowlist ?? [],
|
|
49
|
+
config?.dropHtmlTags ?? []
|
|
50
|
+
);
|
|
51
|
+
p.setBlockData(config?.blockData ?? false);
|
|
52
|
+
return p;
|
|
53
|
+
}
|
|
54
|
+
function requireReady() {
|
|
55
|
+
if (!ready) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"flux-md/server: WASM not initialized. Call `await initFlux()` (or `initFluxSync(bytes)`) once before rendering."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function parseToBlocks(markdown, opts) {
|
|
62
|
+
requireReady();
|
|
63
|
+
const p = makeParser(opts?.config);
|
|
64
|
+
try {
|
|
65
|
+
p.append(markdown);
|
|
66
|
+
p.finalize();
|
|
67
|
+
return JSON.parse(p.allBlocks());
|
|
68
|
+
} finally {
|
|
69
|
+
p.free();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function renderToString(markdown, opts) {
|
|
73
|
+
return parseToBlocks(markdown, opts).map((b) => b.html).join("");
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
initFlux,
|
|
77
|
+
initFluxSync,
|
|
78
|
+
isFluxReady,
|
|
79
|
+
parseToBlocks,
|
|
80
|
+
renderToString
|
|
81
|
+
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FluxClient } from "./client";
|
|
3
|
-
import type { ParserConfig } from "./types-core";
|
|
4
|
-
import {
|
|
5
|
-
|
|
1
|
+
import { type Accessor, type JSX } from "solid-js";
|
|
2
|
+
import { FluxClient } from "./client.js";
|
|
3
|
+
import type { ParserConfig } from "./types-core.js";
|
|
4
|
+
import { type MountHandle, type MountOptions } from "./dom.js";
|
|
6
5
|
/**
|
|
7
6
|
* Solid binding for the framework-neutral DOM renderer ({@link mountFluxMarkdown}).
|
|
8
7
|
*
|
|
@@ -15,39 +14,18 @@ import { mountFluxMarkdown, tailOpenBlockId, type MountHandle, type MountOptions
|
|
|
15
14
|
* Ownership: unmount calls `handle.destroy()` (unsubscribe + remove the renderer
|
|
16
15
|
* root) and never `client.destroy()`. The caller owns the worker/stream.
|
|
17
16
|
*/
|
|
18
|
-
|
|
19
17
|
export interface FluxMarkdownProps extends MountOptions {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
client: FluxClient;
|
|
19
|
+
class?: string;
|
|
20
|
+
style?: JSX.CSSProperties | string;
|
|
23
21
|
}
|
|
24
|
-
|
|
25
22
|
/**
|
|
26
23
|
* Mount the DOM renderer and register its teardown — the testable core, free of
|
|
27
24
|
* JSX so it runs under any toolchain. `getProps` is read once (snapshot), the
|
|
28
25
|
* handle is returned so callers/tests can observe `destroy`, and the teardown is
|
|
29
26
|
* handed to `registerCleanup` (Solid's `onCleanup` at the call site).
|
|
30
27
|
*/
|
|
31
|
-
export function mountSolid(
|
|
32
|
-
getProps: () => FluxMarkdownProps,
|
|
33
|
-
container: HTMLElement,
|
|
34
|
-
registerCleanup: (fn: () => void) => void,
|
|
35
|
-
): MountHandle {
|
|
36
|
-
const p = getProps();
|
|
37
|
-
// Explicit field copy (not rest-spread): keeps `client`/`class`/`style` out of
|
|
38
|
-
// MountOptions and threads `batch`/`highlightCode` straight through.
|
|
39
|
-
const handle = mountFluxMarkdown(p.client, container, {
|
|
40
|
-
components: p.components,
|
|
41
|
-
sanitize: p.sanitize,
|
|
42
|
-
virtualize: p.virtualize,
|
|
43
|
-
stickToBottom: p.stickToBottom,
|
|
44
|
-
highlightCode: p.highlightCode,
|
|
45
|
-
batch: p.batch,
|
|
46
|
-
});
|
|
47
|
-
registerCleanup(() => handle.destroy());
|
|
48
|
-
return handle;
|
|
49
|
-
}
|
|
50
|
-
|
|
28
|
+
export declare function mountSolid(getProps: () => FluxMarkdownProps, container: HTMLElement, registerCleanup: (fn: () => void) => void): MountHandle;
|
|
51
29
|
/**
|
|
52
30
|
* A fine-grained accessor for the streaming **tail** block id — the one block
|
|
53
31
|
* that may still re-render — driven by Solid's own reactivity. Subscribes to the
|
|
@@ -61,28 +39,14 @@ export function mountSolid(
|
|
|
61
39
|
* under any toolchain; the public {@link createTailBlockId} wires Solid's
|
|
62
40
|
* `onCleanup`.
|
|
63
41
|
*/
|
|
64
|
-
export function setupTailBlockId(
|
|
65
|
-
client: FluxClient,
|
|
66
|
-
registerCleanup: (fn: () => void) => void,
|
|
67
|
-
): Accessor<number | null> {
|
|
68
|
-
const [tail, setTail] = createSignal<number | null>(tailOpenBlockId(client.getSnapshot()));
|
|
69
|
-
// setTail no-ops when the value is unchanged (Solid's default equality), so
|
|
70
|
-
// pure tail-html growth that keeps the same open id never re-fires downstream.
|
|
71
|
-
const unsubscribe = client.subscribe(() => setTail(tailOpenBlockId(client.getSnapshot())));
|
|
72
|
-
registerCleanup(unsubscribe);
|
|
73
|
-
return tail;
|
|
74
|
-
}
|
|
75
|
-
|
|
42
|
+
export declare function setupTailBlockId(client: FluxClient, registerCleanup: (fn: () => void) => void): Accessor<number | null>;
|
|
76
43
|
/**
|
|
77
44
|
* Own a fine-grained tail-block-id accessor for `client`, wired to Solid's
|
|
78
45
|
* `onCleanup`. Pair it with `<FluxMarkdown client={client} />`: the component
|
|
79
46
|
* draws the document, this accessor narrows any extra reactive work you key off
|
|
80
47
|
* the live tail (e.g. a "streaming…" affordance) to just the open block.
|
|
81
48
|
*/
|
|
82
|
-
export function createTailBlockId(client: FluxClient): Accessor<number | null
|
|
83
|
-
return setupTailBlockId(client, onCleanup);
|
|
84
|
-
}
|
|
85
|
-
|
|
49
|
+
export declare function createTailBlockId(client: FluxClient): Accessor<number | null>;
|
|
86
50
|
/**
|
|
87
51
|
* The container `<div>` the DOM renderer mounts into. We do not set
|
|
88
52
|
* `class="flux-md"`: the renderer appends its own `.flux-md` root inside it.
|
|
@@ -94,22 +58,7 @@ export function createTailBlockId(client: FluxClient): Accessor<number | null> {
|
|
|
94
58
|
* bun. A real DOM node is a valid Solid `JSX.Element`; under a Solid build this
|
|
95
59
|
* is equivalent to `<div ref={container} class={props.class} style={props.style} />`.
|
|
96
60
|
*/
|
|
97
|
-
export function FluxMarkdown(props: FluxMarkdownProps): JSX.Element
|
|
98
|
-
// SSR: this renderer is client-only and imperative (creates/owns its own DOM
|
|
99
|
-
// node, no hydration expected). Solid runs component bodies on the server, so
|
|
100
|
-
// guard the document access; the browser path is byte-identical (document is
|
|
101
|
-
// always defined there). onMount never fires on the server anyway.
|
|
102
|
-
if (typeof document === "undefined") return undefined as unknown as JSX.Element;
|
|
103
|
-
const container = document.createElement("div");
|
|
104
|
-
if (props.class) container.className = props.class;
|
|
105
|
-
if (typeof props.style === "string") container.setAttribute("style", props.style);
|
|
106
|
-
else if (props.style)
|
|
107
|
-
for (const [k, v] of Object.entries(props.style)) container.style.setProperty(k, String(v));
|
|
108
|
-
// Snapshot props once on mount; the renderer drives itself from here on.
|
|
109
|
-
onMount(() => mountSolid(() => props, container, onCleanup));
|
|
110
|
-
return container;
|
|
111
|
-
}
|
|
112
|
-
|
|
61
|
+
export declare function FluxMarkdown(props: FluxMarkdownProps): JSX.Element;
|
|
113
62
|
/**
|
|
114
63
|
* Wire a controlled string to a freshly-constructed {@link FluxClient}, free of
|
|
115
64
|
* Solid's reactive runtime so it runs (and is tested) under any toolchain. The
|
|
@@ -122,33 +71,10 @@ export function FluxMarkdown(props: FluxMarkdownProps): JSX.Element {
|
|
|
122
71
|
* read ONCE here (the constructor treats it as immutable); `getContent()` and
|
|
123
72
|
* `streaming` are read INSIDE the effect so the effect tracks them reactively.
|
|
124
73
|
*/
|
|
125
|
-
export function setupFluxMarkdownString(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
registerCleanup: (fn: () => void) => void,
|
|
130
|
-
): FluxClient {
|
|
131
|
-
// One client per helper instance. Constructor is worker-free → SSR-safe; the
|
|
132
|
-
// worker is spawned lazily by the first setContent → append, which only runs
|
|
133
|
-
// inside the effect below. config is read once and is immutable thereafter.
|
|
134
|
-
const client = new FluxClient({ config: getOptions?.()?.config });
|
|
135
|
-
|
|
136
|
-
// Reconcile the parser to the controlled string. setContent diffs internally,
|
|
137
|
-
// so this is correct whether `content` grows by a token or is swapped wholesale.
|
|
138
|
-
// `streaming === false` (never `!streaming`) → only an explicit false finalizes;
|
|
139
|
-
// an absent/true flag leaves the stream open (inferring "done" from an absent
|
|
140
|
-
// flag would re-finalize on every token — an O(n²) reparse trap).
|
|
141
|
-
registerEffect(() => {
|
|
142
|
-
client.setContent(getContent(), { done: getOptions?.()?.streaming === false });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// This helper OWNS the client (unlike the client-based bindings above), so it
|
|
146
|
-
// destroys it on cleanup — freeing its pool slot.
|
|
147
|
-
registerCleanup(() => client.destroy());
|
|
148
|
-
|
|
149
|
-
return client;
|
|
150
|
-
}
|
|
151
|
-
|
|
74
|
+
export declare function setupFluxMarkdownString(getContent: () => string, getOptions: (() => {
|
|
75
|
+
config?: ParserConfig;
|
|
76
|
+
streaming?: boolean;
|
|
77
|
+
}) | undefined, registerEffect: (fn: () => void) => void, registerCleanup: (fn: () => void) => void): FluxClient;
|
|
152
78
|
/**
|
|
153
79
|
* Own a {@link FluxClient} driven by a CONTROLLED full string — the Solid
|
|
154
80
|
* analogue of React's `useFluxMarkdownString`, for UIs that hold a streaming
|
|
@@ -172,9 +98,7 @@ export function setupFluxMarkdownString(
|
|
|
172
98
|
* `renderToString`, so nothing touches a Worker on the server render path (the
|
|
173
99
|
* body only constructs the worker-free client).
|
|
174
100
|
*/
|
|
175
|
-
export function createFluxMarkdownString(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
): FluxClient
|
|
179
|
-
return setupFluxMarkdownString(getContent, getOptions, createEffect, onCleanup);
|
|
180
|
-
}
|
|
101
|
+
export declare function createFluxMarkdownString(getContent: () => string, getOptions?: () => {
|
|
102
|
+
config?: ParserConfig;
|
|
103
|
+
streaming?: boolean;
|
|
104
|
+
}): FluxClient;
|
package/dist/solid.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
|
|
2
|
+
import { FluxClient } from "./client.js";
|
|
3
|
+
import { mountFluxMarkdown, tailOpenBlockId } from "./dom.js";
|
|
4
|
+
function mountSolid(getProps, container, registerCleanup) {
|
|
5
|
+
const p = getProps();
|
|
6
|
+
const handle = mountFluxMarkdown(p.client, container, {
|
|
7
|
+
components: p.components,
|
|
8
|
+
sanitize: p.sanitize,
|
|
9
|
+
virtualize: p.virtualize,
|
|
10
|
+
stickToBottom: p.stickToBottom,
|
|
11
|
+
highlightCode: p.highlightCode,
|
|
12
|
+
batch: p.batch
|
|
13
|
+
});
|
|
14
|
+
registerCleanup(() => handle.destroy());
|
|
15
|
+
return handle;
|
|
16
|
+
}
|
|
17
|
+
function setupTailBlockId(client, registerCleanup) {
|
|
18
|
+
const [tail, setTail] = createSignal(tailOpenBlockId(client.getSnapshot()));
|
|
19
|
+
const unsubscribe = client.subscribe(() => setTail(tailOpenBlockId(client.getSnapshot())));
|
|
20
|
+
registerCleanup(unsubscribe);
|
|
21
|
+
return tail;
|
|
22
|
+
}
|
|
23
|
+
function createTailBlockId(client) {
|
|
24
|
+
return setupTailBlockId(client, onCleanup);
|
|
25
|
+
}
|
|
26
|
+
function FluxMarkdown(props) {
|
|
27
|
+
if (typeof document === "undefined") return void 0;
|
|
28
|
+
const container = document.createElement("div");
|
|
29
|
+
if (props.class) container.className = props.class;
|
|
30
|
+
if (typeof props.style === "string") container.setAttribute("style", props.style);
|
|
31
|
+
else if (props.style)
|
|
32
|
+
for (const [k, v] of Object.entries(props.style)) container.style.setProperty(k, String(v));
|
|
33
|
+
onMount(() => mountSolid(() => props, container, onCleanup));
|
|
34
|
+
return container;
|
|
35
|
+
}
|
|
36
|
+
function setupFluxMarkdownString(getContent, getOptions, registerEffect, registerCleanup) {
|
|
37
|
+
const client = new FluxClient({ config: getOptions?.()?.config });
|
|
38
|
+
registerEffect(() => {
|
|
39
|
+
client.setContent(getContent(), { done: getOptions?.()?.streaming === false });
|
|
40
|
+
});
|
|
41
|
+
registerCleanup(() => client.destroy());
|
|
42
|
+
return client;
|
|
43
|
+
}
|
|
44
|
+
function createFluxMarkdownString(getContent, getOptions) {
|
|
45
|
+
return setupFluxMarkdownString(getContent, getOptions, createEffect, onCleanup);
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
FluxMarkdown,
|
|
49
|
+
createFluxMarkdownString,
|
|
50
|
+
createTailBlockId,
|
|
51
|
+
mountSolid,
|
|
52
|
+
setupFluxMarkdownString,
|
|
53
|
+
setupTailBlockId
|
|
54
|
+
};
|
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ActionReturn } from "svelte/action";
|
|
2
|
+
import { type Readable } from "svelte/store";
|
|
3
|
+
import { FluxClient } from "./client.js";
|
|
4
|
+
import type { ParserConfig } from "./types-core.js";
|
|
5
|
+
import { type DomComponents } from "./dom.js";
|
|
6
|
+
/**
|
|
7
|
+
* Svelte action that mounts a streaming {@link FluxClient} into the host node.
|
|
8
|
+
* Plain `.ts` — no `.svelte` compile step — so `use:` works unchanged in
|
|
9
|
+
* Svelte 4 and 5. The action owns only lifecycle: it mounts on creation and
|
|
10
|
+
* tears the mount down on destroy. The caller keeps ownership of the client
|
|
11
|
+
* (the worker/stream); the action never calls `client.destroy()`.
|
|
12
|
+
*
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <div use:fluxMarkdown={{ client, stickToBottom: true }} />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export interface FluxMarkdownParams {
|
|
18
|
+
client: FluxClient;
|
|
19
|
+
components?: DomComponents;
|
|
20
|
+
sanitize?: (h: string) => string;
|
|
21
|
+
virtualize?: boolean;
|
|
22
|
+
stickToBottom?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function fluxMarkdown(node: HTMLElement, params: FluxMarkdownParams): ActionReturn<FluxMarkdownParams>;
|
|
25
|
+
/**
|
|
26
|
+
* A fine-grained Svelte `Readable` store of the streaming **tail** block id — the
|
|
27
|
+
* one block that may still re-render — driven by the client's own subscribe loop.
|
|
28
|
+
* The store sets a new value only when the tail id changes, so a `$tail`
|
|
29
|
+
* subscription or `derived(tail, …)` re-evaluates *only* for the tail, never for
|
|
30
|
+
* the committed body. Subscribing renders nothing: {@link fluxMarkdown} draws the
|
|
31
|
+
* document; this mirrors `MountHandle.openBlockId` through Svelte's primitive for
|
|
32
|
+
* any extra tail-scoped work. The client subscription is owned by the store and
|
|
33
|
+
* torn down when the last subscriber leaves (Svelte's `readable` stop fn).
|
|
34
|
+
*
|
|
35
|
+
* ```svelte
|
|
36
|
+
* const tail = tailBlockId(client); // $tail is the open block id, or null
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function tailBlockId(client: FluxClient): Readable<number | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Controlled-string sibling of {@link fluxMarkdown}: instead of taking a
|
|
42
|
+
* caller-owned client, this action OWNS a single {@link FluxClient} (constructed
|
|
43
|
+
* from `config`) and drives it from a CONTROLLED full string — the bridge for
|
|
44
|
+
* Svelte UIs that hold a streaming message as one growing `content` prop rather
|
|
45
|
+
* than feeding the client by hand. Each update passes the whole document-so-far
|
|
46
|
+
* and {@link FluxClient.setContent} diffs it: a prefix-extension appends only the
|
|
47
|
+
* delta; any divergence resets and reparses.
|
|
48
|
+
*
|
|
49
|
+
* ```svelte
|
|
50
|
+
* <div use:fluxMarkdownString={{ content, streaming: !done }} />
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* Pass `streaming: false` once the content is final to finalize the stream and
|
|
54
|
+
* commit its last block (only then does a finished code fence highlight + show
|
|
55
|
+
* its copy button). When `streaming` is omitted or `true` the stream is left
|
|
56
|
+
* OPEN — right for a still-growing string, but a *complete static* string keeps
|
|
57
|
+
* its last block in the streaming state until you pass `{ streaming: false }`.
|
|
58
|
+
* (Inferring "done" from an absent flag is deliberately avoided — it would
|
|
59
|
+
* re-finalize on every token and trip an O(n²) reparse.)
|
|
60
|
+
*
|
|
61
|
+
* SSR-safe by construction: a Svelte action runs ONLY in the browser, and the
|
|
62
|
+
* `FluxClient` constructor is worker-free — the first worker is spawned lazily by
|
|
63
|
+
* `setContent`, which only runs here (never during a server render).
|
|
64
|
+
*
|
|
65
|
+
* Lifecycle differs from {@link fluxMarkdown}: this action constructs the client
|
|
66
|
+
* once (a later `config` change is ignored, like a created-once instance) and
|
|
67
|
+
* `destroy()`s it on teardown — it OWNS the client. The mount-option reconcile
|
|
68
|
+
* (`components`/`sanitize`/`virtualize`/`stickToBottom`) matches `fluxMarkdown`,
|
|
69
|
+
* but the remount reuses the SAME client so its `setContent` diff baseline
|
|
70
|
+
* survives.
|
|
71
|
+
*/
|
|
72
|
+
export interface FluxMarkdownStringParams extends Omit<FluxMarkdownParams, "client"> {
|
|
73
|
+
/** The full document-so-far. Diffed against the prior value on every update. */
|
|
74
|
+
content: string;
|
|
75
|
+
/** Leave the stream open while true/omitted; `false` finalizes (commits the tail). */
|
|
76
|
+
streaming?: boolean;
|
|
77
|
+
/** Per-stream parser flags. Applied once at construction; later changes are ignored. */
|
|
78
|
+
config?: ParserConfig;
|
|
79
|
+
}
|
|
80
|
+
export declare function fluxMarkdownString(node: HTMLElement, params: FluxMarkdownStringParams): ActionReturn<FluxMarkdownStringParams>;
|