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,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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/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
+ };
@@ -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 {};