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,18 @@
|
|
|
1
|
+
import type { Block, BlockComponentProps } from "./types-core.js";
|
|
2
|
+
/** Info-string language from a code block's `data-lang="…"`. */
|
|
3
|
+
export declare function extractLang(html: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Convert sanitized HTML attribute pairs into a spreadable object, keeping the
|
|
6
|
+
* HTML-form names (`class`, `for`) verbatim. This is the deliberate divergence
|
|
7
|
+
* from the JSX renderer (which renames to `className`/`htmlFor` for a prop
|
|
8
|
+
* spread): the DOM renderer applies them via `el.setAttribute(name, value)`,
|
|
9
|
+
* which wants the literal HTML names.
|
|
10
|
+
*/
|
|
11
|
+
export declare function htmlAttrs(pairs: [string, string][]): Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Build the props a block-kind / component-tag override receives — the same
|
|
14
|
+
* shape the JSX renderer's block-kind props carry, with ONE deliberate
|
|
15
|
+
* divergence: for `Component` blocks `attrs` stay in HTML form (`class`/`for`)
|
|
16
|
+
* because DOM overrides apply them via `setAttribute` (see {@link htmlAttrs}).
|
|
17
|
+
*/
|
|
18
|
+
export declare function blockProps(block: Block): BlockComponentProps;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
function decodeEntities(s) {
|
|
2
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
3
|
+
}
|
|
4
|
+
function decodeCodeText(html) {
|
|
5
|
+
const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
|
|
6
|
+
return m ? decodeEntities(m[1]) : "";
|
|
7
|
+
}
|
|
8
|
+
function decodeMathText(html) {
|
|
9
|
+
const d = html.match(/<div class="math math-display">([\s\S]*?)<\/div>/);
|
|
10
|
+
if (d) return decodeEntities(d[1]);
|
|
11
|
+
return decodeCodeText(html);
|
|
12
|
+
}
|
|
13
|
+
function extractLang(html) {
|
|
14
|
+
const m = html.match(/data-lang="([^"]+)"/);
|
|
15
|
+
return m ? m[1] : "";
|
|
16
|
+
}
|
|
17
|
+
function componentInnerHtml(html, tag) {
|
|
18
|
+
const gt = html.indexOf(">");
|
|
19
|
+
if (gt < 0) return "";
|
|
20
|
+
let inner = html.slice(gt + 1);
|
|
21
|
+
const close = `</${tag}>`;
|
|
22
|
+
if (inner.endsWith(close)) inner = inner.slice(0, -close.length);
|
|
23
|
+
return inner.replace(/^\n/, "").replace(/\n$/, "");
|
|
24
|
+
}
|
|
25
|
+
function htmlAttrs(pairs) {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const [k, v] of pairs) out[k] = v;
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function blockProps(block) {
|
|
31
|
+
const props = {
|
|
32
|
+
block,
|
|
33
|
+
html: block.html,
|
|
34
|
+
open: block.open,
|
|
35
|
+
speculative: block.speculative
|
|
36
|
+
};
|
|
37
|
+
const data = block.kind.data;
|
|
38
|
+
if (block.kind.type === "CodeBlock") {
|
|
39
|
+
props.text = data?.code ?? decodeCodeText(block.html);
|
|
40
|
+
props.language = data?.lang ?? "";
|
|
41
|
+
if (typeof data?.code === "string") {
|
|
42
|
+
props.code = { lang: data.lang ?? null, code: data.code };
|
|
43
|
+
}
|
|
44
|
+
} else if (block.kind.type === "MathBlock") {
|
|
45
|
+
props.text = data?.latex ?? decodeMathText(block.html);
|
|
46
|
+
if (typeof data?.latex === "string") {
|
|
47
|
+
props.math = { latex: data.latex };
|
|
48
|
+
}
|
|
49
|
+
} else if (block.kind.type === "List") {
|
|
50
|
+
if (data && typeof data.start === "number") {
|
|
51
|
+
props.list = { ordered: !!data.ordered, start: data.start, items: data.items };
|
|
52
|
+
}
|
|
53
|
+
} else if (block.kind.type === "Component") {
|
|
54
|
+
props.tag = data?.tag ?? "";
|
|
55
|
+
props.attrs = htmlAttrs(data?.attrs ?? []);
|
|
56
|
+
props.html = componentInnerHtml(block.html, props.tag);
|
|
57
|
+
} else if (block.kind.type === "Table") {
|
|
58
|
+
props.table = block.kind.data;
|
|
59
|
+
} else if (block.kind.type === "Heading") {
|
|
60
|
+
if (typeof block.kind.data === "object" && block.kind.data !== null) {
|
|
61
|
+
props.heading = block.kind.data;
|
|
62
|
+
}
|
|
63
|
+
} else if (block.kind.type === "Blockquote" || block.kind.type === "Alert") {
|
|
64
|
+
const cd = block.kind.data;
|
|
65
|
+
if (cd && Array.isArray(cd.nested)) {
|
|
66
|
+
props.container = { nested: cd.nested };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return props;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
blockProps,
|
|
73
|
+
extractLang,
|
|
74
|
+
htmlAttrs
|
|
75
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type { Block, FromWorker, ParserConfig, Patch, ToWorker, WorkerLike } from "./types-core.js";
|
|
2
|
+
/**
|
|
3
|
+
* The ordered-block store backing a stream, extracted as a pure function so
|
|
4
|
+
* its reference-stability contract is testable without a Worker.
|
|
5
|
+
*
|
|
6
|
+
* **The contract that prevents extra React re-renders:** a block, once
|
|
7
|
+
* committed, is never re-sent by the parser, so `applyPatch` never replaces it
|
|
8
|
+
* in the map. Its object reference stays identical across every later patch —
|
|
9
|
+
* which is exactly what `blocksEqual` (the BlockView memo) checks, so committed
|
|
10
|
+
* blocks never re-render (and never re-parse) as the stream grows. Only the
|
|
11
|
+
* `active` tail gets fresh references each patch, and only it re-renders.
|
|
12
|
+
*/
|
|
13
|
+
export interface BlockStore {
|
|
14
|
+
committed: Map<number, Block>;
|
|
15
|
+
committedOrder: number[];
|
|
16
|
+
active: Block[];
|
|
17
|
+
snapshot: Block[];
|
|
18
|
+
}
|
|
19
|
+
export declare function emptyBlockStore(): BlockStore;
|
|
20
|
+
/** A heading entry for building a table of contents — see {@link BrookClient.outline}. */
|
|
21
|
+
export interface OutlineEntry {
|
|
22
|
+
/** Heading level 1–6. */
|
|
23
|
+
level: number;
|
|
24
|
+
/** Plain-text heading content (tags stripped, entities decoded). */
|
|
25
|
+
text: string;
|
|
26
|
+
/** Stable block id — usable as a scroll target / React key. */
|
|
27
|
+
id: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function applyPatch(store: BlockStore, patch: Patch): void;
|
|
30
|
+
interface PoolWorker {
|
|
31
|
+
worker: WorkerLike;
|
|
32
|
+
ready: boolean;
|
|
33
|
+
/** Set once WASM init fails; whenWorkerReady rejects with this thereafter. */
|
|
34
|
+
failed: Error | null;
|
|
35
|
+
streamCount: number;
|
|
36
|
+
/** Live stream ids on this worker — so a fatal failure can notify each one. */
|
|
37
|
+
streamIds: Set<number>;
|
|
38
|
+
readyWaiters: Array<{
|
|
39
|
+
resolve: () => void;
|
|
40
|
+
reject: (e: Error) => void;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A pool of Web Workers, each multiplexing many `BrookParser`s keyed by stream
|
|
45
|
+
* id. This is what lets brookmd scale past `hardwareConcurrency` concurrent
|
|
46
|
+
* streams without oversubscribing OS threads: 50 streams share (at most) the
|
|
47
|
+
* cap's worth of workers instead of spawning 50.
|
|
48
|
+
*
|
|
49
|
+
* Worker creation is **lazy and load-aware**: while under the cap, each new
|
|
50
|
+
* stream gets its own worker (so 1 stream = 1 worker, identical to the old
|
|
51
|
+
* behavior); once at the cap, new streams attach to the least-loaded worker.
|
|
52
|
+
*
|
|
53
|
+
* The constructor injects a `WorkerLike` factory so the routing and lifecycle
|
|
54
|
+
* logic is unit-testable with a fake worker — no real Worker or WASM needed.
|
|
55
|
+
*/
|
|
56
|
+
export declare class BrookPool {
|
|
57
|
+
private factory;
|
|
58
|
+
private cap;
|
|
59
|
+
private workers;
|
|
60
|
+
private handlers;
|
|
61
|
+
private nextStreamId;
|
|
62
|
+
constructor(factory: () => WorkerLike, cap: number);
|
|
63
|
+
/** Reserve a stream id and assign a worker, registering its message handler. */
|
|
64
|
+
acquire(handler: (msg: FromWorker) => void): {
|
|
65
|
+
streamId: number;
|
|
66
|
+
pw: PoolWorker;
|
|
67
|
+
};
|
|
68
|
+
/** Free a stream's parser in its worker; keep the worker warm for siblings. */
|
|
69
|
+
release(streamId: number, pw: PoolWorker): void;
|
|
70
|
+
/** Inverse of {@link release}: re-register a stream's handler so it receives
|
|
71
|
+
* patches again. For React StrictMode's dev double-mount, which destroys a
|
|
72
|
+
* client on the simulated unmount and remounts the SAME instance. The worker
|
|
73
|
+
* lazily recreates the disposed parser on the next append. */
|
|
74
|
+
reattach(streamId: number, pw: PoolWorker, handler: (msg: FromWorker) => void): void;
|
|
75
|
+
send(pw: PoolWorker, msg: ToWorker): void;
|
|
76
|
+
/** Resolves when the given worker has finished WASM init; rejects if it failed. */
|
|
77
|
+
whenWorkerReady(pw: PoolWorker): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Eagerly spin up one worker so WASM init starts BEFORE the first stream —
|
|
80
|
+
* taking the one-time init off the first-token critical path (e.g. call
|
|
81
|
+
* `getDefaultPool().warm()` on app load / route entry). Reuses a live worker
|
|
82
|
+
* if one exists; the warm worker is the one the first stream attaches to (it
|
|
83
|
+
* has spare capacity), so the work is not wasted. Resolves when that worker has
|
|
84
|
+
* finished initializing WASM; rejects if init fails fatally. Browser-only (it
|
|
85
|
+
* constructs a `Worker`).
|
|
86
|
+
*/
|
|
87
|
+
warm(): Promise<void>;
|
|
88
|
+
/** Terminate every worker (test teardown / full shutdown). */
|
|
89
|
+
disposeAll(): void;
|
|
90
|
+
get workerCount(): number;
|
|
91
|
+
private pick;
|
|
92
|
+
private create;
|
|
93
|
+
private onMessage;
|
|
94
|
+
private dispatch;
|
|
95
|
+
}
|
|
96
|
+
/** The process-wide default pool every `BrookClient` shares unless given one. */
|
|
97
|
+
export declare function getDefaultPool(): BrookPool;
|
|
98
|
+
/** TEST-ONLY: drop the process-wide default pool so the next {@link getDefaultPool}
|
|
99
|
+
* rebuilds it (lazily, with the current global `Worker`). Lets a test file that
|
|
100
|
+
* drives the default pool start from a clean, deterministic state regardless of
|
|
101
|
+
* which other file warmed it first in bun's shared test process. Not part of the
|
|
102
|
+
* public API and a no-op for normal runtime use. */
|
|
103
|
+
export declare function __resetDefaultPool(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Subscriber-driven store backing a single streaming parser. Each client owns
|
|
106
|
+
* one stream within a shared {@link BrookPool}; many clients multiplex over a
|
|
107
|
+
* small set of workers (see the pool for the scaling story).
|
|
108
|
+
*
|
|
109
|
+
* The store exposes:
|
|
110
|
+
* - subscribe(listener): for React's useSyncExternalStore
|
|
111
|
+
* - getSnapshot(): the current ordered list of blocks
|
|
112
|
+
* - getMetrics(): per-stream perf metrics
|
|
113
|
+
*
|
|
114
|
+
* Mutation methods:
|
|
115
|
+
* - append(chunk): forward to the worker
|
|
116
|
+
* - finalize(): mark the stream done
|
|
117
|
+
* - reset(): start fresh
|
|
118
|
+
*/
|
|
119
|
+
export declare class BrookClient {
|
|
120
|
+
private pool;
|
|
121
|
+
private pw;
|
|
122
|
+
private streamId;
|
|
123
|
+
private config?;
|
|
124
|
+
private configSent;
|
|
125
|
+
private listeners;
|
|
126
|
+
private store;
|
|
127
|
+
private onError?;
|
|
128
|
+
private onBlock?;
|
|
129
|
+
private attached;
|
|
130
|
+
private lastContent;
|
|
131
|
+
private contentDone;
|
|
132
|
+
private coalesce;
|
|
133
|
+
private rafHandle;
|
|
134
|
+
private finalizePending;
|
|
135
|
+
private epoch;
|
|
136
|
+
private staleSnapshot;
|
|
137
|
+
private staleTrimmed;
|
|
138
|
+
private idNamespace;
|
|
139
|
+
private mergeCache;
|
|
140
|
+
private appendedBytes;
|
|
141
|
+
private patchCount;
|
|
142
|
+
private totalParseMicros;
|
|
143
|
+
private lastPatchMs;
|
|
144
|
+
private firstAppendMs;
|
|
145
|
+
private retainedBytes;
|
|
146
|
+
private wasmMemoryBytes;
|
|
147
|
+
private renderCount;
|
|
148
|
+
private rebuildCount;
|
|
149
|
+
/**
|
|
150
|
+
* @param options.pool worker pool to join (defaults to the shared
|
|
151
|
+
* process-wide pool — pass a dedicated `BrookPool` only for isolation).
|
|
152
|
+
* @param options.config per-stream parser flags (see {@link ParserConfig});
|
|
153
|
+
* omitted fields use library defaults. Applied once, immutable thereafter.
|
|
154
|
+
* @param options.onError invoked on a worker/parse error or a fatal WASM-init
|
|
155
|
+
* failure (`fatal: true`). Without it, errors are only `console.error`d and
|
|
156
|
+
* a load failure surfaces solely as a rejected {@link BrookClient.whenReady}.
|
|
157
|
+
* @param options.onBlock invoked once per block as it commits (in document
|
|
158
|
+
* order, after the store updates) — for side effects like lazily
|
|
159
|
+
* highlighting a finished code block or analytics. A committed block never
|
|
160
|
+
* re-fires; the streaming tail does not (subscribe for live tail updates).
|
|
161
|
+
* NOTE: this is a PARSER-commit hook — the block carries the parser's raw
|
|
162
|
+
* id. During a setContent divergence swap the rendered view may show that
|
|
163
|
+
* block under a different id (an adopted old id, or a namespaced one), so
|
|
164
|
+
* correlate with rendered blocks via subscribe()+getSnapshot(), not this id.
|
|
165
|
+
* @param options.coalesce opt-in (default `false`): collapse multiple
|
|
166
|
+
* intra-frame patch notifications into ONE `requestAnimationFrame`-scheduled
|
|
167
|
+
* flush to subscribers, so a React `useSyncExternalStore` consumer renders at
|
|
168
|
+
* most once per frame instead of once per patch. Lossless — committed blocks
|
|
169
|
+
* are reference-stable, so only superseded tail-only renders are skipped. The
|
|
170
|
+
* stream-completion (finalize) patch always flushes synchronously, and a
|
|
171
|
+
* pending frame is cancelled on `reset()`/`destroy()`. No effect when
|
|
172
|
+
* `requestAnimationFrame` is unavailable (e.g. SSR) — emits stay synchronous.
|
|
173
|
+
*/
|
|
174
|
+
constructor(options?: {
|
|
175
|
+
pool?: BrookPool;
|
|
176
|
+
config?: ParserConfig;
|
|
177
|
+
onError?: (err: {
|
|
178
|
+
message: string;
|
|
179
|
+
fatal?: boolean;
|
|
180
|
+
}) => void;
|
|
181
|
+
onBlock?: (block: Block) => void;
|
|
182
|
+
coalesce?: boolean;
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Lazily reserve this client's stream id and bind it to a pool worker. The
|
|
186
|
+
* SOLE place that calls pool.acquire() — so the worker is created on the FIRST
|
|
187
|
+
* worker-bound operation (append/finalize/reset/pipeFrom/whenReady), never at
|
|
188
|
+
* construct time. This is what makes `new BrookClient()` SSR-safe: nothing here
|
|
189
|
+
* runs during an SSR render (which only subscribes + reads the snapshot).
|
|
190
|
+
*
|
|
191
|
+
* Idempotent: once this.pw is set it returns it immediately and never
|
|
192
|
+
* re-acquires — this.pw is never nulled (destroy() deliberately keeps it so
|
|
193
|
+
* StrictMode's destroy()→reattach() on the SAME instance re-registers the same
|
|
194
|
+
* slot). Note: streamId/worker assignment now follows first-worker-bound-op
|
|
195
|
+
* order, not construction order — a client constructed first no longer
|
|
196
|
+
* necessarily owns the lowest streamId. This affects neither the pool cap nor
|
|
197
|
+
* multiplexing (pick() is unchanged and remains the only path to create()).
|
|
198
|
+
*/
|
|
199
|
+
private ensureAcquired;
|
|
200
|
+
get ready(): boolean;
|
|
201
|
+
whenReady(): Promise<void>;
|
|
202
|
+
private firstConfig;
|
|
203
|
+
append(chunk: string): void;
|
|
204
|
+
finalize(): void;
|
|
205
|
+
/**
|
|
206
|
+
* Pipe a source straight in: read it to completion, `append()` each chunk,
|
|
207
|
+
* then `finalize()`. The LLM-native path — e.g.
|
|
208
|
+
* `await client.pipeFrom(await fetch("/api/chat"))`. Accepts:
|
|
209
|
+
* - a `Response` or its `ReadableStream<Uint8Array>` body (bytes; decoded
|
|
210
|
+
* with `TextDecoder({ stream: true })` so a multibyte sequence straddling
|
|
211
|
+
* a chunk boundary carries into the next read), or
|
|
212
|
+
* - an `AsyncIterable<string>` (e.g. an SSE delta generator) — string chunks
|
|
213
|
+
* appended verbatim.
|
|
214
|
+
*
|
|
215
|
+
* Pass `opts.signal` to supersede/cancel: the signal is checked on every
|
|
216
|
+
* iteration, so once aborted no further chunk is appended and **finalize is
|
|
217
|
+
* skipped** (a superseded stream must not finalize). For a byte source the
|
|
218
|
+
* reader is also `cancel()`'d to tear down the upstream. Resolves once
|
|
219
|
+
* finalized (or cleanly on abort); rejects if the source itself errors.
|
|
220
|
+
* Browser-only for byte sources (uses `TextDecoder`).
|
|
221
|
+
*/
|
|
222
|
+
pipeFrom(source: ReadableStream<Uint8Array> | Response | AsyncIterable<string>, opts?: {
|
|
223
|
+
signal?: AbortSignal;
|
|
224
|
+
}): Promise<void>;
|
|
225
|
+
/**
|
|
226
|
+
* Drive the parser from a CONTROLLED full string instead of manual appends.
|
|
227
|
+
* Pass the whole document-so-far each time; setContent diffs it against the
|
|
228
|
+
* last value and does the minimal work:
|
|
229
|
+
* - **prefix-extension** (the streaming-growth case) → append only the new
|
|
230
|
+
* suffix, so committed blocks stay put and only the active tail re-parses;
|
|
231
|
+
* - **any other change** (e.g. a finished stream swapped for a re-processed
|
|
232
|
+
* final string) → reset + reparse the whole new string, keeping the
|
|
233
|
+
* current view on screen until the reparse lands: the document never
|
|
234
|
+
* blanks, scroll never moves, and blocks whose rendered content is
|
|
235
|
+
* unchanged keep their identity (and React keys) so only genuinely
|
|
236
|
+
* changed blocks re-render. An empty new string is an explicit clear and
|
|
237
|
+
* hard-resets immediately.
|
|
238
|
+
*
|
|
239
|
+
* This is the first-class bridge for UIs that hold a streaming message as a
|
|
240
|
+
* single growing string prop (the common React shape) — no hand-rolled diff,
|
|
241
|
+
* no readiness gate (appends before WASM is ready are buffered). Pass
|
|
242
|
+
* `{ done: true }` once the content is final to `finalize()` (idempotent within
|
|
243
|
+
* a generation; a content change *after* done reopens the stream via a fresh
|
|
244
|
+
* reparse, since a finalized parser is terminal and can't be appended to).
|
|
245
|
+
* Drive a given client with `setContent` *or* manual `append()`/`finalize()`,
|
|
246
|
+
* not both — they share the internal diff baseline.
|
|
247
|
+
*
|
|
248
|
+
* v1 note: the non-prefix path is a full reparse, not a partial rewind —
|
|
249
|
+
* committed blocks are frozen, so there is no truncate-to-offset. For the
|
|
250
|
+
* common case (append-growth + one end-of-stream swap) that is optimal. A
|
|
251
|
+
* transform that rewrites *earlier* bytes on every update is an anti-pattern
|
|
252
|
+
* here (it forces a reparse each tick); do that enrichment at render time via
|
|
253
|
+
* `components` instead, keeping the source append-only.
|
|
254
|
+
*/
|
|
255
|
+
setContent(content: string, opts?: {
|
|
256
|
+
done?: boolean;
|
|
257
|
+
}): void;
|
|
258
|
+
reset(): void;
|
|
259
|
+
/**
|
|
260
|
+
* setContent's divergence reset: rebuild the parser exactly like {@link reset},
|
|
261
|
+
* but keep `preserve` (the currently displayed view) on screen while the new
|
|
262
|
+
* content reparses. No notify fires here — subscribers keep reading the same
|
|
263
|
+
* snapshot reference until the first new-generation patch merges over it, so
|
|
264
|
+
* the swap is seamless: no empty frame, no container collapse, no scroll
|
|
265
|
+
* clamp, and blocks whose content survives the reprocess never re-render.
|
|
266
|
+
*/
|
|
267
|
+
private softReset;
|
|
268
|
+
/**
|
|
269
|
+
* Finish a preserved-view divergence swap by making the merged view THE
|
|
270
|
+
* store: adopted ids become the committed keys, the merged array becomes the
|
|
271
|
+
* snapshot, and every scrap of merge state drops. From here getSnapshot() is
|
|
272
|
+
* a plain field read again (zero steady-state overhead) and the superseded
|
|
273
|
+
* generation's blocks are garbage — only one document stays in memory.
|
|
274
|
+
* Runs on the terminal (final) patch, which commits everything — if anything
|
|
275
|
+
* is somehow still open, the lazy merge simply stays live instead (the
|
|
276
|
+
* incremental reuse keeps it linear).
|
|
277
|
+
*/
|
|
278
|
+
private collapseStale;
|
|
279
|
+
private resetParser;
|
|
280
|
+
destroy(): void;
|
|
281
|
+
/**
|
|
282
|
+
* Re-register with the pool after {@link destroy} so the client receives
|
|
283
|
+
* patches again. Needed only for React StrictMode's dev double-mount, where
|
|
284
|
+
* the renderer destroys on the simulated unmount then remounts the SAME
|
|
285
|
+
* client instance; apps don't normally call this. No-op if still attached.
|
|
286
|
+
*/
|
|
287
|
+
reattach(): void;
|
|
288
|
+
subscribe: (fn: () => void) => () => boolean;
|
|
289
|
+
getSnapshot: () => Block[];
|
|
290
|
+
/**
|
|
291
|
+
* Positional merge of the preserved pre-divergence view over the rebuilding
|
|
292
|
+
* store (see {@link softReset}). Per position:
|
|
293
|
+
* - identical committed block (html + kind + open + speculative) → the OLD
|
|
294
|
+
* block object, so its id and reference survive the swap and the block
|
|
295
|
+
* never re-renders (blocksEqual / the DOM keyed reconcile hold);
|
|
296
|
+
* - changed committed block → the new block CARRYING THE OLD BLOCK'S id, so
|
|
297
|
+
* the same keyed component re-renders in place and its state (pagination,
|
|
298
|
+
* expansion…) survives the swap; only a NET-NEW position (past the old
|
|
299
|
+
* document's end) takes a namespace-offset id, where a raw parser id
|
|
300
|
+
* could genuinely collide with a retained old id;
|
|
301
|
+
* - still-open block over old content → the old block (never a shrinking
|
|
302
|
+
* partial where complete content was already on screen); past the old
|
|
303
|
+
* document's end the live tail streams in as-is;
|
|
304
|
+
* - position the reparse hasn't reached → the old block, until the terminal
|
|
305
|
+
* patch sets `staleTrimmed` and the view clamps to the new length.
|
|
306
|
+
*
|
|
307
|
+
* LINEARITY: committed blocks are reference-stable across patches (the store
|
|
308
|
+
* contract), so a base entry pointer-equal to the previous merge's reproduces
|
|
309
|
+
* its previous decision without re-running the O(html) equality compare. Each
|
|
310
|
+
* block is string-compared exactly once — when it first commits — keeping a
|
|
311
|
+
* long post-divergence stream linear instead of quadratic. Only the active
|
|
312
|
+
* tail (fresh references each patch, small by design) re-compares per patch.
|
|
313
|
+
*/
|
|
314
|
+
private mergeStale;
|
|
315
|
+
/**
|
|
316
|
+
* Internal: a renderer with an `onRenderMetrics` hook calls this once per
|
|
317
|
+
* actual React block render so `getMetrics().renderCount` aggregates churn.
|
|
318
|
+
* No-op cost when no hook is wired (it is simply never called). Not part of
|
|
319
|
+
* the public API surface — the underscore marks it renderer-internal.
|
|
320
|
+
*/
|
|
321
|
+
__noteRender(): void;
|
|
322
|
+
/**
|
|
323
|
+
* Internal: the DOM renderer calls this once per actual node rebuild (the
|
|
324
|
+
* changed-block branch) when an `onRenderMetrics` hook is wired, so
|
|
325
|
+
* `getMetrics().rebuildCount` aggregates churn. Never called without a hook.
|
|
326
|
+
*/
|
|
327
|
+
__noteRebuild(): void;
|
|
328
|
+
getMetrics(): {
|
|
329
|
+
bytes: number;
|
|
330
|
+
patches: number;
|
|
331
|
+
meanParseMicros: number;
|
|
332
|
+
totalParseMs: number;
|
|
333
|
+
throughputKBs: number;
|
|
334
|
+
committedBlocks: number;
|
|
335
|
+
activeBlocks: number;
|
|
336
|
+
lastPatchAgoMs: number;
|
|
337
|
+
retainedBytes: number;
|
|
338
|
+
wasmMemoryBytes: number;
|
|
339
|
+
renderCount: number;
|
|
340
|
+
rebuildCount: number;
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* A heading outline of the current snapshot (committed + active), in document
|
|
344
|
+
* order — for a table of contents. Works mid-stream; entries appear as their
|
|
345
|
+
* headings stream in. The `id` is stable, so a built ToC won't re-key.
|
|
346
|
+
*/
|
|
347
|
+
outline(): OutlineEntry[];
|
|
348
|
+
/**
|
|
349
|
+
* The rendered document as plain text — tags stripped, entities decoded,
|
|
350
|
+
* blocks separated by blank lines. Derived from the rendered HTML (the source
|
|
351
|
+
* markdown is parsed away in WASM and not retained client-side), so it is a
|
|
352
|
+
* readable approximation for search indexing / summaries, not a round-trip of
|
|
353
|
+
* the original source.
|
|
354
|
+
*/
|
|
355
|
+
toPlaintext(): string;
|
|
356
|
+
private onMessage;
|
|
357
|
+
/**
|
|
358
|
+
* Notify subscribers of a new snapshot.
|
|
359
|
+
*
|
|
360
|
+
* With `coalesce` off (default) this is fully synchronous, exactly as before.
|
|
361
|
+
* With it on and `requestAnimationFrame` available, a normal emit only
|
|
362
|
+
* *schedules* a single per-frame flush — repeated intra-frame emits collapse
|
|
363
|
+
* into one notify. `sync` forces an immediate flush (stream completion / reset)
|
|
364
|
+
* and cancels any frame already pending so the snapshot is delivered once.
|
|
365
|
+
*/
|
|
366
|
+
private emit;
|
|
367
|
+
private flushNow;
|
|
368
|
+
private cancelFrame;
|
|
369
|
+
}
|
|
370
|
+
export {};
|