brookmd 0.22.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 (59) hide show
  1. package/CHANGELOG.md +1229 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1265 -0
  4. package/dist/block-props.d.ts +18 -0
  5. package/dist/block-props.js +75 -0
  6. package/dist/client.d.ts +370 -0
  7. package/dist/client.js +754 -0
  8. package/dist/decorate.d.ts +24 -0
  9. package/dist/decorate.js +71 -0
  10. package/dist/dom.d.ts +130 -0
  11. package/dist/dom.js +627 -0
  12. package/dist/element.d.ts +20 -0
  13. package/dist/element.js +288 -0
  14. package/dist/hi.d.ts +12 -0
  15. package/dist/hi.js +215 -0
  16. package/dist/html-to-react.d.ts +61 -0
  17. package/dist/html-to-react.js +338 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.js +18 -0
  20. package/dist/morph.d.ts +28 -0
  21. package/dist/morph.js +166 -0
  22. package/dist/react.d.ts +236 -0
  23. package/dist/react.js +539 -0
  24. package/dist/renderers/CodeBlock.d.ts +7 -0
  25. package/dist/renderers/CodeBlock.js +75 -0
  26. package/dist/renderers/Math.d.ts +14 -0
  27. package/dist/renderers/Math.js +15 -0
  28. package/dist/renderers/Mermaid.d.ts +13 -0
  29. package/dist/renderers/Mermaid.js +15 -0
  30. package/dist/server-react.d.ts +32 -0
  31. package/dist/server-react.js +48 -0
  32. package/dist/server.d.ts +31 -0
  33. package/dist/server.js +82 -0
  34. package/dist/solid.d.ts +104 -0
  35. package/dist/solid.js +54 -0
  36. package/dist/styles.css +188 -0
  37. package/dist/svelte.d.ts +80 -0
  38. package/dist/svelte.js +59 -0
  39. package/dist/types-core.d.ts +436 -0
  40. package/dist/types-core.js +0 -0
  41. package/dist/types-react.d.ts +13 -0
  42. package/dist/types-react.js +0 -0
  43. package/dist/types.d.ts +2 -0
  44. package/dist/types.js +2 -0
  45. package/dist/url-safety.d.ts +12 -0
  46. package/dist/url-safety.js +45 -0
  47. package/dist/vue.d.ts +94 -0
  48. package/dist/vue.js +79 -0
  49. package/dist/wasm/LICENSE +21 -0
  50. package/dist/wasm/README.md +71 -0
  51. package/dist/wasm/brook_md_core.d.ts +166 -0
  52. package/dist/wasm/brook_md_core.js +512 -0
  53. package/dist/wasm/brook_md_core_bg.wasm +0 -0
  54. package/dist/wasm/brook_md_core_bg.wasm.d.ts +26 -0
  55. package/dist/worker-core.d.ts +65 -0
  56. package/dist/worker-core.js +155 -0
  57. package/dist/worker.d.ts +1 -0
  58. package/dist/worker.js +49 -0
  59. package/package.json +87 -0
@@ -0,0 +1,32 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Components, ParserConfig } from "./types.js";
3
+ interface BrookMarkdownStaticProps {
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 `brook-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 `<BrookMarkdown>` 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 `brook-md` root +
24
+ * per-block structure with the same `components` overrides (inline/block
25
+ * component tags dispatch here too). Requires `initBrook` (or `initBrookSync`)
26
+ * from `brookmd/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 `<BrookMarkdown>` instead (and if you SSR-then-hydrate, render the
29
+ * *same* component on both sides).
30
+ */
31
+ export declare function BrookMarkdownStatic({ content, config, components, className, id, role, "aria-live": ariaLive, "aria-atomic": ariaAtomic, }: BrookMarkdownStaticProps): 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 = "brook-block brook-block-" + kind.toLowerCase() + (block.open ? " brook-open" : "") + (block.speculative ? " brook-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 BrookMarkdownStatic({
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 ? `brook-md ${className}` : "brook-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
+ BrookMarkdownStatic
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 isBrookReady(): 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 initBrook} auto-loads the co-located WASM. */
7
+ export declare function initBrookSync(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 initBrook(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 initBrook} 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 `<BrookMarkdown>`-matching React tree, use
26
+ * `BrookMarkdownStatic` from `brookmd/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,82 @@
1
+ import initWasmAsync, { BrookParser, initSync } from "./wasm/brook_md_core.js";
2
+ let ready = false;
3
+ function isBrookReady() {
4
+ return ready;
5
+ }
6
+ function initBrookSync(wasm) {
7
+ if (ready) return;
8
+ initSync({ module: wasm });
9
+ ready = true;
10
+ }
11
+ let initPromise = null;
12
+ function initBrook(opts) {
13
+ if (ready) return Promise.resolve();
14
+ if (opts?.wasm) {
15
+ initBrookSync(opts.wasm);
16
+ return Promise.resolve();
17
+ }
18
+ if (!initPromise) {
19
+ initPromise = (async () => {
20
+ const wasmUrl = new URL("./wasm/brook_md_core_bg.wasm", import.meta.url);
21
+ if (wasmUrl.protocol === "file:") {
22
+ const { readFile } = await import("node:fs/promises");
23
+ initBrookSync(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 BrookParser();
37
+ p.setGfmAutolinks(config?.gfmAutolinks ?? true);
38
+ p.setGfmAlerts(config?.gfmAlerts ?? true);
39
+ p.setGfmTagfilter(config?.gfmTagfilter ?? false);
40
+ p.setGfmFootnotes(config?.gfmFootnotes ?? false);
41
+ p.setGfmMath(config?.gfmMath ?? false);
42
+ p.setDirAuto(config?.dirAuto ?? false);
43
+ p.setA11y(config?.a11y ?? false);
44
+ p.setUnsafeHtml(config?.unsafeHtml ?? false);
45
+ p.setComponentTags(config?.componentTags ?? []);
46
+ p.setInlineComponentTags(config?.inlineComponentTags ?? []);
47
+ p.setHtmlSanitize(
48
+ config?.htmlAllowlist !== void 0 || config?.dropHtmlTags !== void 0,
49
+ config?.htmlAllowlist ?? [],
50
+ config?.dropHtmlTags ?? []
51
+ );
52
+ p.setBlockData(config?.blockData ?? false);
53
+ return p;
54
+ }
55
+ function requireReady() {
56
+ if (!ready) {
57
+ throw new Error(
58
+ "brookmd/server: WASM not initialized. Call `await initBrook()` (or `initBrookSync(bytes)`) once before rendering."
59
+ );
60
+ }
61
+ }
62
+ function parseToBlocks(markdown, opts) {
63
+ requireReady();
64
+ const p = makeParser(opts?.config);
65
+ try {
66
+ p.append(markdown);
67
+ p.finalize();
68
+ return JSON.parse(p.allBlocks());
69
+ } finally {
70
+ p.free();
71
+ }
72
+ }
73
+ function renderToString(markdown, opts) {
74
+ return parseToBlocks(markdown, opts).map((b) => b.html).join("");
75
+ }
76
+ export {
77
+ initBrook,
78
+ initBrookSync,
79
+ isBrookReady,
80
+ parseToBlocks,
81
+ renderToString
82
+ };
@@ -0,0 +1,104 @@
1
+ import { type Accessor, type JSX } from "solid-js";
2
+ import { BrookClient } from "./client.js";
3
+ import type { ParserConfig } from "./types-core.js";
4
+ import { type MountHandle, type MountOptions } from "./dom.js";
5
+ /**
6
+ * Solid binding for the framework-neutral DOM renderer ({@link mountBrookMarkdown}).
7
+ *
8
+ * Deliberately thin lifecycle glue: it mounts the renderer once on `onMount` and
9
+ * tears it down on `onCleanup`. There is **no** `createEffect` — the DOM renderer
10
+ * owns its own `client.subscribe` loop and patches the container directly, so
11
+ * re-running mount on signal changes would thrash (double-subscribe, rebuild the
12
+ * tree). Props are read once as a non-reactive snapshot at mount time.
13
+ *
14
+ * Ownership: unmount calls `handle.destroy()` (unsubscribe + remove the renderer
15
+ * root) and never `client.destroy()`. The caller owns the worker/stream.
16
+ */
17
+ export interface BrookMarkdownProps extends MountOptions {
18
+ client: BrookClient;
19
+ class?: string;
20
+ style?: JSX.CSSProperties | string;
21
+ }
22
+ /**
23
+ * Mount the DOM renderer and register its teardown — the testable core, free of
24
+ * JSX so it runs under any toolchain. `getProps` is read once (snapshot), the
25
+ * handle is returned so callers/tests can observe `destroy`, and the teardown is
26
+ * handed to `registerCleanup` (Solid's `onCleanup` at the call site).
27
+ */
28
+ export declare function mountSolid(getProps: () => BrookMarkdownProps, container: HTMLElement, registerCleanup: (fn: () => void) => void): MountHandle;
29
+ /**
30
+ * A fine-grained accessor for the streaming **tail** block id — the one block
31
+ * that may still re-render — driven by Solid's own reactivity. Subscribes to the
32
+ * client once and updates a `createSignal` only when the tail id changes, so a
33
+ * downstream `createMemo`/effect that reads it re-evaluates *only* for the tail,
34
+ * never for the committed body. Reading it renders nothing: the DOM is owned by
35
+ * {@link mountBrookMarkdown}; this is a scheduling/diagnostic signal that mirrors
36
+ * {@link MountHandle.openBlockId} through Solid's primitive.
37
+ *
38
+ * Registrars are injected (like {@link mountSolid}) so the testable core runs
39
+ * under any toolchain; the public {@link createTailBlockId} wires Solid's
40
+ * `onCleanup`.
41
+ */
42
+ export declare function setupTailBlockId(client: BrookClient, registerCleanup: (fn: () => void) => void): Accessor<number | null>;
43
+ /**
44
+ * Own a fine-grained tail-block-id accessor for `client`, wired to Solid's
45
+ * `onCleanup`. Pair it with `<BrookMarkdown client={client} />`: the component
46
+ * draws the document, this accessor narrows any extra reactive work you key off
47
+ * the live tail (e.g. a "streaming…" affordance) to just the open block.
48
+ */
49
+ export declare function createTailBlockId(client: BrookClient): Accessor<number | null>;
50
+ /**
51
+ * The container `<div>` the DOM renderer mounts into. We do not set
52
+ * `class="brook-md"`: the renderer appends its own `.brook-md` root inside it.
53
+ *
54
+ * Authored imperatively rather than with a JSX literal: a JSX literal makes
55
+ * bun's transform inject an automatic-runtime import (`jsxDEV` from
56
+ * `solid-js/jsx-dev-runtime`) that Solid does not provide (Solid compiles JSX
57
+ * via dom-expressions, not a runtime), which breaks importing this module under
58
+ * bun. A real DOM node is a valid Solid `JSX.Element`; under a Solid build this
59
+ * is equivalent to `<div ref={container} class={props.class} style={props.style} />`.
60
+ */
61
+ export declare function BrookMarkdown(props: BrookMarkdownProps): JSX.Element;
62
+ /**
63
+ * Wire a controlled string to a freshly-constructed {@link BrookClient}, free of
64
+ * Solid's reactive runtime so it runs (and is tested) under any toolchain. The
65
+ * registrars are injected: the public {@link createBrookMarkdownString} passes
66
+ * Solid's real `createEffect` / `onCleanup`; tests pass hand-rolled stand-ins
67
+ * (mirroring how {@link mountSolid} takes `registerCleanup`).
68
+ *
69
+ * Ownership DIFFERS from {@link mountSolid}: this constructs the client and so
70
+ * `registerCleanup`s `client.destroy()` — it OWNS the worker/stream. `config` is
71
+ * read ONCE here (the constructor treats it as immutable); `getContent()` and
72
+ * `streaming` are read INSIDE the effect so the effect tracks them reactively.
73
+ */
74
+ export declare function setupBrookMarkdownString(getContent: () => string, getOptions: (() => {
75
+ config?: ParserConfig;
76
+ streaming?: boolean;
77
+ }) | undefined, registerEffect: (fn: () => void) => void, registerCleanup: (fn: () => void) => void): BrookClient;
78
+ /**
79
+ * Own a {@link BrookClient} driven by a CONTROLLED full string — the Solid
80
+ * analogue of React's `useBrookMarkdownString`, for UIs that hold a streaming
81
+ * message as a single growing string (a signal/memo) rather than as a stream.
82
+ * Pass an accessor for the whole document-so-far; on every change
83
+ * {@link BrookClient.setContent} diffs it and does the minimal work (a
84
+ * prefix-extension appends only the delta; any divergence resets and reparses).
85
+ *
86
+ * Pass `streaming: false` (via `getOptions`) once the content is final to
87
+ * finalize the stream and commit its last block (only then does a finished code
88
+ * fence highlight + show its copy button). If `streaming` is omitted or `true`
89
+ * the stream is left OPEN. `config` is read once at construction and is
90
+ * immutable, so it is not a change trigger.
91
+ *
92
+ * **Returns the owned client** — pass it to `<BrookMarkdown client={client} />`
93
+ * (and read `outline()` / `getMetrics()` off it). The client is constructed in
94
+ * the body (constructor is worker-free → SSR-safe) and destroyed on cleanup.
95
+ *
96
+ * SSR-safety: `setContent` is what spawns a Worker (via `append`), so it runs
97
+ * ONLY inside a `createEffect` — Solid does not run user effects during
98
+ * `renderToString`, so nothing touches a Worker on the server render path (the
99
+ * body only constructs the worker-free client).
100
+ */
101
+ export declare function createBrookMarkdownString(getContent: () => string, getOptions?: () => {
102
+ config?: ParserConfig;
103
+ streaming?: boolean;
104
+ }): BrookClient;
package/dist/solid.js ADDED
@@ -0,0 +1,54 @@
1
+ import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
2
+ import { BrookClient } from "./client.js";
3
+ import { mountBrookMarkdown, tailOpenBlockId } from "./dom.js";
4
+ function mountSolid(getProps, container, registerCleanup) {
5
+ const p = getProps();
6
+ const handle = mountBrookMarkdown(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 BrookMarkdown(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 setupBrookMarkdownString(getContent, getOptions, registerEffect, registerCleanup) {
37
+ const client = new BrookClient({ config: getOptions?.()?.config });
38
+ registerEffect(() => {
39
+ client.setContent(getContent(), { done: getOptions?.()?.streaming === false });
40
+ });
41
+ registerCleanup(() => client.destroy());
42
+ return client;
43
+ }
44
+ function createBrookMarkdownString(getContent, getOptions) {
45
+ return setupBrookMarkdownString(getContent, getOptions, createEffect, onCleanup);
46
+ }
47
+ export {
48
+ BrookMarkdown,
49
+ createBrookMarkdownString,
50
+ createTailBlockId,
51
+ mountSolid,
52
+ setupBrookMarkdownString,
53
+ setupTailBlockId
54
+ };
@@ -0,0 +1,188 @@
1
+ /*
2
+ * brookmd — optional default theme.
3
+ *
4
+ * import "brookmd/styles.css";
5
+ *
6
+ * Opt-in: import it for good-looking output out of the box (including the
7
+ * built-in syntax highlighter's colors); skip it to bring your own CSS — the
8
+ * rendered HTML is identical either way. Everything is scoped to `.brook-md`
9
+ * (the renderer's root) and driven by CSS custom properties, so you re-theme by
10
+ * overriding a few `--brook-*` vars rather than rewriting selectors.
11
+ *
12
+ * Light by default; dark automatically via `prefers-color-scheme`. Force a mode
13
+ * with `<div class="brook-md brook-dark">` or `brook-light`.
14
+ */
15
+
16
+ .brook-md {
17
+ /* surfaces + text (light) */
18
+ --brook-fg: #1f2328;
19
+ --brook-fg-muted: #59636e;
20
+ --brook-fg-faint: #818b98;
21
+ --brook-border: #d1d9e0;
22
+ --brook-bg-code: #f6f8fa;
23
+ --brook-bg-inline: rgba(129, 139, 152, 0.16);
24
+ --brook-bg-quote: rgba(129, 139, 152, 0.08);
25
+ --brook-accent: #0969da;
26
+ /* syntax tokens (light) */
27
+ --brook-t-kw: #cf222e;
28
+ --brook-t-str: #0a3069;
29
+ --brook-t-num: #0550ae;
30
+ --brook-t-com: #59636e;
31
+ --brook-t-fn: #6639ba;
32
+ --brook-t-ty: #953800;
33
+ --brook-t-mac: #1f6feb;
34
+ --brook-t-attr: #116329;
35
+ --brook-t-tag: #116329;
36
+ --brook-t-var: #953800;
37
+ --brook-t-pun: var(--brook-fg);
38
+ /* sizing */
39
+ --brook-radius: 6px;
40
+ --brook-gap: 16px;
41
+
42
+ color: var(--brook-fg);
43
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
44
+ line-height: 1.6;
45
+ font-size: 1rem;
46
+ word-wrap: break-word;
47
+ overflow-wrap: anywhere;
48
+ }
49
+
50
+ /* Dark — automatic, and as an explicit `.brook-dark` escape hatch. */
51
+ @media (prefers-color-scheme: dark) {
52
+ .brook-md:not(.brook-light) {
53
+ --brook-fg: #e6edf3;
54
+ --brook-fg-muted: #9198a1;
55
+ --brook-fg-faint: #6e7681;
56
+ --brook-border: #3d444d;
57
+ --brook-bg-code: #151b23;
58
+ --brook-bg-inline: rgba(101, 108, 118, 0.2);
59
+ --brook-bg-quote: rgba(101, 108, 118, 0.1);
60
+ --brook-accent: #4493f8;
61
+ --brook-t-kw: #ff7b72;
62
+ --brook-t-str: #a5d6ff;
63
+ --brook-t-num: #79c0ff;
64
+ --brook-t-com: #8b949e;
65
+ --brook-t-fn: #d2a8ff;
66
+ --brook-t-ty: #ffa657;
67
+ --brook-t-mac: #79c0ff;
68
+ --brook-t-attr: #7ee787;
69
+ --brook-t-tag: #7ee787;
70
+ --brook-t-var: #ffa657;
71
+ }
72
+ }
73
+ .brook-md.brook-dark {
74
+ --brook-fg: #e6edf3;
75
+ --brook-fg-muted: #9198a1;
76
+ --brook-fg-faint: #6e7681;
77
+ --brook-border: #3d444d;
78
+ --brook-bg-code: #151b23;
79
+ --brook-bg-inline: rgba(101, 108, 118, 0.2);
80
+ --brook-bg-quote: rgba(101, 108, 118, 0.1);
81
+ --brook-accent: #4493f8;
82
+ --brook-t-kw: #ff7b72;
83
+ --brook-t-str: #a5d6ff;
84
+ --brook-t-num: #79c0ff;
85
+ --brook-t-com: #8b949e;
86
+ --brook-t-fn: #d2a8ff;
87
+ --brook-t-ty: #ffa657;
88
+ --brook-t-mac: #79c0ff;
89
+ --brook-t-attr: #7ee787;
90
+ --brook-t-tag: #7ee787;
91
+ --brook-t-var: #ffa657;
92
+ }
93
+
94
+ /* ---- block rhythm ---------------------------------------------------------- */
95
+ .brook-md > * { margin: 0 0 var(--brook-gap) 0; }
96
+ .brook-md > *:last-child { margin-bottom: 0; }
97
+
98
+ /* ---- headings -------------------------------------------------------------- */
99
+ .brook-md h1, .brook-md h2, .brook-md h3,
100
+ .brook-md h4, .brook-md h5, .brook-md h6 {
101
+ font-weight: 600;
102
+ line-height: 1.25;
103
+ margin: 24px 0 var(--brook-gap);
104
+ }
105
+ .brook-md h1 { font-size: 2em; padding-bottom: 0.3em; border-bottom: 1px solid var(--brook-border); }
106
+ .brook-md h2 { font-size: 1.5em; padding-bottom: 0.3em; border-bottom: 1px solid var(--brook-border); }
107
+ .brook-md h3 { font-size: 1.25em; }
108
+ .brook-md h4 { font-size: 1em; }
109
+ .brook-md h5 { font-size: 0.875em; }
110
+ .brook-md h6 { font-size: 0.85em; color: var(--brook-fg-muted); }
111
+ .brook-md > h1:first-child, .brook-md > h2:first-child, .brook-md > h3:first-child { margin-top: 0; }
112
+
113
+ /* ---- inline ---------------------------------------------------------------- */
114
+ .brook-md a { color: var(--brook-accent); text-decoration: none; }
115
+ .brook-md a:hover { text-decoration: underline; }
116
+ /* A streaming link whose URL hasn't finished arriving renders as an inert
117
+ `<a data-brook-pending>` with no href — the UA gives an href-less anchor no
118
+ link styling, so restate the settled resting style here (identical color and
119
+ decoration, so completion changes nothing visually). Cursor stays default:
120
+ not clickable yet. */
121
+ .brook-md a[data-brook-pending] { color: var(--brook-accent); text-decoration: none; cursor: default; }
122
+ .brook-md strong { font-weight: 600; }
123
+ .brook-md em { font-style: italic; }
124
+ .brook-md del { color: var(--brook-fg-muted); }
125
+ .brook-md img { max-width: 100%; height: auto; }
126
+
127
+ /* ---- lists ----------------------------------------------------------------- */
128
+ .brook-md ul, .brook-md ol { padding-left: 2em; }
129
+ .brook-md li { margin: 0.25em 0; }
130
+ .brook-md li > ul, .brook-md li > ol { margin: 0.25em 0; }
131
+ .brook-md li::marker { color: var(--brook-fg-faint); }
132
+ .brook-md input[type="checkbox"] { margin: 0 0.4em 0 0; }
133
+
134
+ /* ---- blockquote + alerts --------------------------------------------------- */
135
+ .brook-md blockquote {
136
+ margin: 0 0 var(--brook-gap);
137
+ padding: 0.4em 1em;
138
+ color: var(--brook-fg-muted);
139
+ border-left: 0.25em solid var(--brook-border);
140
+ background: var(--brook-bg-quote);
141
+ }
142
+ .brook-md blockquote > :last-child { margin-bottom: 0; }
143
+
144
+ /* ---- tables ---------------------------------------------------------------- */
145
+ .brook-md table { border-collapse: collapse; width: 100%; display: block; overflow-x: auto; }
146
+ .brook-md th, .brook-md td { padding: 6px 13px; border: 1px solid var(--brook-border); }
147
+ .brook-md th { font-weight: 600; background: var(--brook-bg-code); }
148
+ .brook-md tr:nth-child(2n) td { background: var(--brook-bg-quote); }
149
+
150
+ /* ---- code ------------------------------------------------------------------ */
151
+ .brook-md code, .brook-md pre {
152
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
153
+ font-size: 0.9em;
154
+ }
155
+ .brook-md :not(pre) > code {
156
+ padding: 0.2em 0.4em;
157
+ border-radius: var(--brook-radius);
158
+ background: var(--brook-bg-inline);
159
+ }
160
+ .brook-md pre {
161
+ padding: 14px 16px;
162
+ overflow-x: auto;
163
+ border-radius: var(--brook-radius);
164
+ background: var(--brook-bg-code);
165
+ line-height: 1.5;
166
+ }
167
+ .brook-md pre code { padding: 0; background: none; border: 0; }
168
+
169
+ /* ---- rule ------------------------------------------------------------------ */
170
+ .brook-md hr { height: 1px; border: 0; background: var(--brook-border); margin: 24px 0; }
171
+
172
+ /* ---- syntax highlighter (the built-in `highlight()` token spans) ----------- */
173
+ .brook-md .t-kw { color: var(--brook-t-kw); }
174
+ .brook-md .t-str,
175
+ .brook-md .t-rx { color: var(--brook-t-str); }
176
+ .brook-md .t-num,
177
+ .brook-md .t-lt { color: var(--brook-t-num); }
178
+ .brook-md .t-com { color: var(--brook-t-com); font-style: italic; }
179
+ .brook-md .t-fn { color: var(--brook-t-fn); }
180
+ .brook-md .t-ty { color: var(--brook-t-ty); }
181
+ .brook-md .t-mac,
182
+ .brook-md .t-dec { color: var(--brook-t-mac); }
183
+ .brook-md .t-attr,
184
+ .brook-md .t-sel { color: var(--brook-t-attr); }
185
+ .brook-md .t-tag { color: var(--brook-t-tag); }
186
+ .brook-md .t-var { color: var(--brook-t-var); }
187
+ .brook-md .t-pun { color: var(--brook-t-pun); }
188
+ .brook-md .t-txt { color: inherit; }
@@ -0,0 +1,80 @@
1
+ import type { ActionReturn } from "svelte/action";
2
+ import { type Readable } from "svelte/store";
3
+ import { BrookClient } 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 BrookClient} 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:brookMarkdown={{ client, stickToBottom: true }} />
15
+ * ```
16
+ */
17
+ export interface BrookMarkdownParams {
18
+ client: BrookClient;
19
+ components?: DomComponents;
20
+ sanitize?: (h: string) => string;
21
+ virtualize?: boolean;
22
+ stickToBottom?: boolean;
23
+ }
24
+ export declare function brookMarkdown(node: HTMLElement, params: BrookMarkdownParams): ActionReturn<BrookMarkdownParams>;
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 brookMarkdown} 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: BrookClient): Readable<number | null>;
40
+ /**
41
+ * Controlled-string sibling of {@link brookMarkdown}: instead of taking a
42
+ * caller-owned client, this action OWNS a single {@link BrookClient} (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 BrookClient.setContent} diffs it: a prefix-extension appends only the
47
+ * delta; any divergence resets and reparses.
48
+ *
49
+ * ```svelte
50
+ * <div use:brookMarkdownString={{ 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
+ * `BrookClient` 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 brookMarkdown}: 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 `brookMarkdown`,
69
+ * but the remount reuses the SAME client so its `setContent` diff baseline
70
+ * survives.
71
+ */
72
+ export interface BrookMarkdownStringParams extends Omit<BrookMarkdownParams, "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 brookMarkdownString(node: HTMLElement, params: BrookMarkdownStringParams): ActionReturn<BrookMarkdownStringParams>;
package/dist/svelte.js ADDED
@@ -0,0 +1,59 @@
1
+ import { readable } from "svelte/store";
2
+ import { BrookClient } from "./client.js";
3
+ import { mountBrookMarkdown, tailOpenBlockId } from "./dom.js";
4
+ function brookMarkdown(node, params) {
5
+ let { client, ...options } = params;
6
+ let handle = mountBrookMarkdown(client, node, options);
7
+ return {
8
+ update(next) {
9
+ if (next.client === client && next.components === options.components && next.sanitize === options.sanitize && next.virtualize === options.virtualize && next.stickToBottom === options.stickToBottom) {
10
+ return;
11
+ }
12
+ handle.destroy();
13
+ ({ client, ...options } = next);
14
+ handle = mountBrookMarkdown(client, node, options);
15
+ },
16
+ destroy() {
17
+ handle.destroy();
18
+ }
19
+ };
20
+ }
21
+ function tailBlockId(client) {
22
+ return readable(tailOpenBlockId(client.getSnapshot()), (set) => {
23
+ const unsubscribe = client.subscribe(() => set(tailOpenBlockId(client.getSnapshot())));
24
+ return unsubscribe;
25
+ });
26
+ }
27
+ function mountOptionsOf(p) {
28
+ const { content: _c, streaming: _s, config: _cfg, ...rest } = p;
29
+ void _c;
30
+ void _s;
31
+ void _cfg;
32
+ return rest;
33
+ }
34
+ function brookMarkdownString(node, params) {
35
+ let options = mountOptionsOf(params);
36
+ const client = new BrookClient({ config: params.config });
37
+ let handle = mountBrookMarkdown(client, node, options);
38
+ client.setContent(params.content, { done: params.streaming === false });
39
+ return {
40
+ update(next) {
41
+ client.setContent(next.content, { done: next.streaming === false });
42
+ if (next.components === options.components && next.sanitize === options.sanitize && next.virtualize === options.virtualize && next.stickToBottom === options.stickToBottom) {
43
+ return;
44
+ }
45
+ handle.destroy();
46
+ options = mountOptionsOf(next);
47
+ handle = mountBrookMarkdown(client, node, options);
48
+ },
49
+ destroy() {
50
+ handle.destroy();
51
+ client.destroy();
52
+ }
53
+ };
54
+ }
55
+ export {
56
+ brookMarkdown,
57
+ brookMarkdownString,
58
+ tailBlockId
59
+ };