flux-md 0.16.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +950 -0
  2. package/README.md +111 -82
  3. package/dist/block-props.d.ts +18 -0
  4. package/dist/block-props.js +75 -0
  5. package/dist/client.d.ts +311 -0
  6. package/{src/client.ts → dist/client.js} +143 -325
  7. package/dist/dom.d.ts +110 -0
  8. package/dist/dom.js +576 -0
  9. package/dist/element.d.ts +20 -0
  10. package/dist/element.js +287 -0
  11. package/dist/hi.d.ts +12 -0
  12. package/{src/hi.ts → dist/hi.js} +42 -74
  13. package/dist/html-to-react.d.ts +40 -0
  14. package/dist/html-to-react.js +344 -0
  15. package/{src/index.ts → dist/index.d.ts} +5 -25
  16. package/dist/index.js +16 -0
  17. package/dist/morph.d.ts +28 -0
  18. package/dist/morph.js +166 -0
  19. package/dist/react.d.ts +204 -0
  20. package/dist/react.js +490 -0
  21. package/dist/renderers/CodeBlock.d.ts +7 -0
  22. package/dist/renderers/CodeBlock.js +75 -0
  23. package/dist/renderers/Math.d.ts +14 -0
  24. package/dist/renderers/Math.js +15 -0
  25. package/dist/renderers/Mermaid.d.ts +13 -0
  26. package/dist/renderers/Mermaid.js +15 -0
  27. package/dist/server-react.d.ts +32 -0
  28. package/dist/server-react.js +48 -0
  29. package/dist/server.d.ts +31 -0
  30. package/dist/server.js +81 -0
  31. package/{src/solid.tsx → dist/solid.d.ts} +19 -95
  32. package/dist/solid.js +54 -0
  33. package/dist/svelte.d.ts +80 -0
  34. package/dist/svelte.js +59 -0
  35. package/dist/types-core.d.ts +382 -0
  36. package/dist/types-core.js +0 -0
  37. package/{src/types-react.ts → dist/types-react.d.ts} +0 -1
  38. package/dist/types-react.js +0 -0
  39. package/dist/types.d.ts +2 -0
  40. package/dist/types.js +2 -0
  41. package/dist/vue.d.ts +94 -0
  42. package/dist/vue.js +79 -0
  43. package/{src → dist}/wasm/flux_md_core.d.ts +18 -6
  44. package/{src → dist}/wasm/flux_md_core.js +50 -55
  45. package/dist/wasm/flux_md_core_bg.wasm +0 -0
  46. package/{src → dist}/wasm/flux_md_core_bg.wasm.d.ts +1 -0
  47. package/dist/worker-core.d.ts +65 -0
  48. package/dist/worker-core.js +154 -0
  49. package/dist/worker.d.ts +1 -0
  50. package/dist/worker.js +48 -0
  51. package/package.json +25 -19
  52. package/src/block-props.ts +0 -141
  53. package/src/dom.ts +0 -946
  54. package/src/element.ts +0 -400
  55. package/src/html-to-react.ts +0 -455
  56. package/src/morph.ts +0 -253
  57. package/src/react.tsx +0 -1020
  58. package/src/renderers/CodeBlock.tsx +0 -116
  59. package/src/renderers/Math.tsx +0 -28
  60. package/src/renderers/Mermaid.tsx +0 -27
  61. package/src/server.tsx +0 -221
  62. package/src/svelte.ts +0 -179
  63. package/src/types-core.ts +0 -398
  64. package/src/types.ts +0 -7
  65. package/src/vue.ts +0 -184
  66. package/src/wasm/flux_md_core_bg.wasm +0 -0
  67. package/src/worker-core.ts +0 -174
  68. package/src/worker.ts +0 -72
  69. /package/{src → dist}/styles.css +0 -0
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ html: string;
3
+ open: boolean;
4
+ }
5
+ declare function CodeBlockImpl({ html, open }: Props): import("react/jsx-runtime").JSX.Element;
6
+ export declare const CodeBlock: import("react").MemoExoticComponent<typeof CodeBlockImpl>;
7
+ export {};
@@ -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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/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
+ };
@@ -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 { createEffect, createSignal, onCleanup, onMount, type Accessor, type JSX } from "solid-js";
2
- import { FluxClient } from "./client";
3
- import type { ParserConfig } from "./types-core";
4
- import { mountFluxMarkdown, tailOpenBlockId, type MountHandle, type MountOptions } from "./dom";
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
- client: FluxClient;
21
- class?: string;
22
- style?: JSX.CSSProperties | string;
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
- getContent: () => string,
127
- getOptions: (() => { config?: ParserConfig; streaming?: boolean }) | undefined,
128
- registerEffect: (fn: () => void) => void,
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
- getContent: () => string,
177
- getOptions?: () => { config?: ParserConfig; streaming?: boolean },
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
+ };
@@ -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>;