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.
- package/CHANGELOG.md +1229 -0
- package/LICENSE +21 -0
- package/README.md +1265 -0
- package/dist/block-props.d.ts +18 -0
- package/dist/block-props.js +75 -0
- package/dist/client.d.ts +370 -0
- package/dist/client.js +754 -0
- package/dist/decorate.d.ts +24 -0
- package/dist/decorate.js +71 -0
- package/dist/dom.d.ts +130 -0
- package/dist/dom.js +627 -0
- package/dist/element.d.ts +20 -0
- package/dist/element.js +288 -0
- package/dist/hi.d.ts +12 -0
- package/dist/hi.js +215 -0
- package/dist/html-to-react.d.ts +61 -0
- package/dist/html-to-react.js +338 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +18 -0
- package/dist/morph.d.ts +28 -0
- package/dist/morph.js +166 -0
- package/dist/react.d.ts +236 -0
- package/dist/react.js +539 -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 +82 -0
- package/dist/solid.d.ts +104 -0
- package/dist/solid.js +54 -0
- package/dist/styles.css +188 -0
- package/dist/svelte.d.ts +80 -0
- package/dist/svelte.js +59 -0
- package/dist/types-core.d.ts +436 -0
- package/dist/types-core.js +0 -0
- package/dist/types-react.d.ts +13 -0
- package/dist/types-react.js +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/url-safety.d.ts +12 -0
- package/dist/url-safety.js +45 -0
- package/dist/vue.d.ts +94 -0
- package/dist/vue.js +79 -0
- package/dist/wasm/LICENSE +21 -0
- package/dist/wasm/README.md +71 -0
- package/dist/wasm/brook_md_core.d.ts +166 -0
- package/dist/wasm/brook_md_core.js +512 -0
- package/dist/wasm/brook_md_core_bg.wasm +0 -0
- package/dist/wasm/brook_md_core_bg.wasm.d.ts +26 -0
- package/dist/worker-core.d.ts +65 -0
- package/dist/worker-core.js +155 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +49 -0
- 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
|
+
};
|
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 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
|
+
};
|
package/dist/solid.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/styles.css
ADDED
|
@@ -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; }
|
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 { 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
|
+
};
|