@ai-markdown/core 3.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/index.cjs +942 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +497 -0
- package/dist/index.d.ts +497 -0
- package/dist/index.dev.cjs +942 -0
- package/dist/index.dev.cjs.map +1 -0
- package/dist/index.dev.js +905 -0
- package/dist/index.dev.js.map +1 -0
- package/dist/index.js +905 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import * as hast from 'hast';
|
|
2
|
+
import { Element, RootContent, Root as Root$1, Nodes } from 'hast';
|
|
3
|
+
import * as mdast from 'mdast';
|
|
4
|
+
import { Root } from 'mdast';
|
|
5
|
+
import { Registry, ChunkData, PipelineOptions, AdvanceOptions } from '@ai-markdown/engine';
|
|
6
|
+
export { isFootnoteSection } from '@ai-markdown/engine';
|
|
7
|
+
|
|
8
|
+
/** Framework-neutral block planning and dependency fingerprints.
|
|
9
|
+
* Cache ownership and conversion to framework nodes belong to adapters.
|
|
10
|
+
* See docs/architecture.md and packages/core/README.md for the contract.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Source-level identity of one renderable hast block. */
|
|
14
|
+
interface BlockInfo {
|
|
15
|
+
raw: string;
|
|
16
|
+
startOffset: number;
|
|
17
|
+
endOffset: number;
|
|
18
|
+
/**
|
|
19
|
+
* Line and column at the block's start. Tracked alongside `startOffset`
|
|
20
|
+
* because two different source documents can produce the same byte offset
|
|
21
|
+
* for the "same" content (`A\n\nB\n\nTarget` vs `ABCD\n\nTarget` — both
|
|
22
|
+
* have `Target` starting at offset 6 but on different lines). Custom
|
|
23
|
+
* components that read `node.position.start.line` would otherwise see
|
|
24
|
+
* stale data from a false-positive cache hit.
|
|
25
|
+
*/
|
|
26
|
+
startLine: number;
|
|
27
|
+
startColumn: number;
|
|
28
|
+
/** True if the block contains any TAINT_TYPES node — invalidate on globalCtx change. */
|
|
29
|
+
hasReference: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Structural digest of the block's hast subtree, computed ONLY for blocks
|
|
32
|
+
* that originate from raw HTML (mdast `html` nodes, or blocks resolved via
|
|
33
|
+
* the range-containment fallback). Undefined for markdown-native blocks.
|
|
34
|
+
*
|
|
35
|
+
* Why it exists: rehype-raw applies the HTML parsing algorithm, so an
|
|
36
|
+
* UNCLOSED container tag (`<details>` mid-stream, before its `</details>`
|
|
37
|
+
* arrives) swallows every FOLLOWING top-level sibling into itself —
|
|
38
|
+
* including the synthetic `<section data-footnotes>`. The block's mdast
|
|
39
|
+
* identity (raw, position, ctx) is byte-identical across all those frames,
|
|
40
|
+
* so without this digest the first swallowed snapshot would be a permanent
|
|
41
|
+
* cache hit: content rendered inside the container freezes, and when the
|
|
42
|
+
* close tag finally arrives the real nodes re-render at top level while the
|
|
43
|
+
* stale copies stay trapped in the cached container (the "duplicate
|
|
44
|
+
* footnote section inside <details>" bug).
|
|
45
|
+
*
|
|
46
|
+
* Digest composition — `maxEnd:descendantCount:hasFootnoteSection:swallowedHash`:
|
|
47
|
+
* - max end offset over the subtree catches swallowed POSITIONED siblings
|
|
48
|
+
* (their offsets lie beyond the mdast node's own range);
|
|
49
|
+
* - descendant count catches position-less synthetic content moving in or
|
|
50
|
+
* out (the footnote section's own element carries no position, and its
|
|
51
|
+
* li contents point BACK at the definitions' offsets, which can be
|
|
52
|
+
* smaller than the container's end — maxEnd alone misses that case);
|
|
53
|
+
* - the explicit dataFootnotes bit closes the degenerate corner where a
|
|
54
|
+
* swallowed section and later real children tie on both numbers;
|
|
55
|
+
* - a hash of the swallowed extent's SOURCE (`[ownEnd, maxEnd)`) catches
|
|
56
|
+
* an equal-length, equal-shape single-frame replacement of the
|
|
57
|
+
* swallowed siblings, which leaves every count unchanged.
|
|
58
|
+
*
|
|
59
|
+
* Markdown-native blocks (paragraph/list/math/code/…) never receive
|
|
60
|
+
* reparented content — every hast descendant derives from their own mdast
|
|
61
|
+
* source range — so they skip the subtree walk entirely. This keeps the
|
|
62
|
+
* per-frame cost away from huge deterministic subtrees like KaTeX output.
|
|
63
|
+
*/
|
|
64
|
+
hastDigest?: string;
|
|
65
|
+
/** A raw-HTML container that swallowed the synthesized
|
|
66
|
+
* `<section data-footnotes>` (rehype-raw reparents the following siblings
|
|
67
|
+
* into an unclosed `<details>` / `<div>` mid-stream). The footer is then
|
|
68
|
+
* NOT a top-level plan item, so coordinated mode's "skip the local
|
|
69
|
+
* footer, the aggregate renders it" rule never fired and the document
|
|
70
|
+
* showed two `<section data-footnotes>` with colliding li ids (2026-08-19
|
|
71
|
+
* review r2 P2-9). Coordinated renders strip it from this block's subtree;
|
|
72
|
+
* standalone renders keep it exactly where the full parse puts it. */
|
|
73
|
+
containsFootnoteSection?: boolean;
|
|
74
|
+
/** TAINT-block 专属:按节点类型分桶的 label set。Normalized 形态(uppercase)。
|
|
75
|
+
* Undefined when hasReference === false. */
|
|
76
|
+
taintLabels?: {
|
|
77
|
+
footnoteRefLabels: string[];
|
|
78
|
+
linkRefLabels: string[];
|
|
79
|
+
imageRefLabels: string[];
|
|
80
|
+
footnoteDefLabels: string[];
|
|
81
|
+
/** Chunk-local footnote state the engine baked into this block's
|
|
82
|
+
* `footnote-sup` placeholders (`localOccurrence` / `localNumber` come
|
|
83
|
+
* from a per-chunk counter over all PRECEDING refs, customMdastHandlers):
|
|
84
|
+
* per ref label, refs-before-this-block count and first-appearance
|
|
85
|
+
* rank. Same raw + same registry answers but a different prefix (an
|
|
86
|
+
* in-place `[^x]`→`[^w]` edit upstream) must miss the cache, or the
|
|
87
|
+
* stale occurrence resolves to a null global occurrence and the mark
|
|
88
|
+
* vanishes (2026-08-19 review P2-4). Absent when the block has no refs. */
|
|
89
|
+
footnoteRefLocalCtx?: string;
|
|
90
|
+
/** Baked `localUrl` / `localTitle` of swallowed `cross-chunk-link` /
|
|
91
|
+
* `-image` placeholders, as `[[label, url, title|null]…]` (JSON). The
|
|
92
|
+
* render falls back to these when the registry has no canonical def,
|
|
93
|
+
* where the fingerprint's `lr:`/`ir:` part is the constant `null` —
|
|
94
|
+
* see scanSwallowedSubtree. Absent when nothing was swallowed. */
|
|
95
|
+
swallowedDefCtx?: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Dev invariant: every block hast child must retain its mdast `position`. */
|
|
100
|
+
declare function hasMdastSource(node: Element): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Compute {@link BlockInfo.hastDigest} for a raw-HTML block: iterative walk
|
|
103
|
+
* over the hast subtree collecting (max end offset, descendant count,
|
|
104
|
+
* contains-footnote-section). See the field's JSDoc for why each component
|
|
105
|
+
* is load-bearing. Exported for tests.
|
|
106
|
+
*/
|
|
107
|
+
declare function computeHtmlBlockDigest(el: Element, source?: string, ownEnd?: number): string;
|
|
108
|
+
/** {@link computeHtmlBlockDigest} plus the subtree's max end offset — the end
|
|
109
|
+
* of the SWALLOWED extent, which callers need to ask what fell inside.
|
|
110
|
+
*
|
|
111
|
+
* Both position sources here are HAST offsets, and every use of the extent
|
|
112
|
+
* is unsafe in the SHORT direction (too small and a swallowed reference
|
|
113
|
+
* escapes the taint test, so an edit to a definition outside the container
|
|
114
|
+
* leaves a stale href in the cache). They are redundant on purpose: the
|
|
115
|
+
* container's own end already reaches past the swallowed blocks, because
|
|
116
|
+
* `rehype-raw` reparses the raw string and closes the element wherever
|
|
117
|
+
* parse5 does — NOT where the mdast `html` node ends, which is just the
|
|
118
|
+
* opening tag. The descendant walk covers it a second time. Verified in
|
|
119
|
+
* 2026-08-20 A1 across 162 container/tail shapes: destroying either source
|
|
120
|
+
* alone changes nothing, destroying both under-taints 130 of them. */
|
|
121
|
+
declare function computeHtmlBlockDigestWithExtent(el: Element, source?: string, ownEnd?: number): {
|
|
122
|
+
digest: string;
|
|
123
|
+
maxEnd: number;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* One step of the document render plan, in source order.
|
|
127
|
+
*
|
|
128
|
+
* The plan covers EVERY top-level hast child so that the adapter’s block cache
|
|
129
|
+
* can preserve react-markdown's byte-equivalent output — including the whitespace
|
|
130
|
+
* `text` nodes that `mdast-util-to-hast` inserts between block elements.
|
|
131
|
+
*
|
|
132
|
+
* - `block`: a hast element with an mdast counterpart, eligible for caching by
|
|
133
|
+
* `(raw, occurrence index, ctx, startOffset)`.
|
|
134
|
+
* - `inline`: a non-element top-level child (whitespace text, comment, …) or
|
|
135
|
+
* an element that has no mdast counterpart but is still safe to render.
|
|
136
|
+
* These are rendered every frame without caching — they are typically
|
|
137
|
+
* single whitespace characters.
|
|
138
|
+
* - `synthetic`: the synthesized footnote `<section data-footnotes>`, cached
|
|
139
|
+
* in a single slot keyed by `globalCtx`.
|
|
140
|
+
*/
|
|
141
|
+
type RenderItem = {
|
|
142
|
+
kind: 'block';
|
|
143
|
+
el: Element;
|
|
144
|
+
key: string;
|
|
145
|
+
info: BlockInfo;
|
|
146
|
+
} | {
|
|
147
|
+
kind: 'inline';
|
|
148
|
+
el: RootContent;
|
|
149
|
+
key: string;
|
|
150
|
+
} | {
|
|
151
|
+
kind: 'synthetic';
|
|
152
|
+
el: Element;
|
|
153
|
+
key: string;
|
|
154
|
+
};
|
|
155
|
+
/** Extra render-time facts {@link buildBlocks} cannot read off the trees. */
|
|
156
|
+
interface BuildBlocksOptions {
|
|
157
|
+
/** Full document when planning only a tail. Its ordered reference and
|
|
158
|
+
* definition context seeds numbering and invalidation for tail blocks. */
|
|
159
|
+
contextMdast?: Root;
|
|
160
|
+
/**
|
|
161
|
+
* Labels this chunk phantom-injected because another chunk defines them
|
|
162
|
+
* (already normalized). The coordinated handlers keep these out of
|
|
163
|
+
* `state.footnoteOrder`, so the cache key's rank must keep them out too —
|
|
164
|
+
* see the note in the rank pass. Empty / omitted in standalone mode, where
|
|
165
|
+
* no phantom injection happened.
|
|
166
|
+
*/
|
|
167
|
+
phantomFootnoteLabels?: ReadonlySet<string>;
|
|
168
|
+
}
|
|
169
|
+
/** Result of {@link buildBlocks}. */
|
|
170
|
+
interface BuildBlocksResult {
|
|
171
|
+
/** Render plan in document order — drives the adapter’s block cache. */
|
|
172
|
+
plan: RenderItem[];
|
|
173
|
+
/** JSON-stringified ordered list of TAINT-typed nodes in document order. */
|
|
174
|
+
globalCtx: string;
|
|
175
|
+
/** Flat list of `BlockInfo` for cacheable blocks, in document order. */
|
|
176
|
+
blocks: BlockInfo[];
|
|
177
|
+
/** 1:1 with `blocks` — the hast Element each BlockInfo refers to. */
|
|
178
|
+
blockHasts: Element[];
|
|
179
|
+
/** Synthesized footnote section, if present. */
|
|
180
|
+
synthetic?: Element;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Build the document render plan + ctx digest from a parsed mdast and its
|
|
184
|
+
* rendered hast.
|
|
185
|
+
*
|
|
186
|
+
* Plan construction is driven by `hast.children` (NOT `mdast.children`):
|
|
187
|
+
* pipeline transformers like `remarkSqueezeParagraphs` and
|
|
188
|
+
* `remarkRemoveComments` drop blocks, so the rendered hast may have fewer
|
|
189
|
+
* top-level children than the parsed mdast. Driving by hast guarantees the
|
|
190
|
+
* plan covers exactly what will be rendered.
|
|
191
|
+
*
|
|
192
|
+
* Each hast top-level child becomes one plan item:
|
|
193
|
+
* - `<section data-footnotes>` → `synthetic` (cached in a single slot)
|
|
194
|
+
* - element with mdast counterpart → `block` (cacheable by raw + occurrence + ctx + offset)
|
|
195
|
+
* - text / comment / element-without-counterpart → `inline` (rendered fresh every frame)
|
|
196
|
+
*
|
|
197
|
+
* The mdast counterpart of a block element is found by source offset using
|
|
198
|
+
* a two-tier lookup: an exact-offset Map (mdast-util-to-hast's default 1:1
|
|
199
|
+
* propagation), then range-containment `findLast` fallback for cases like
|
|
200
|
+
* `rehype-raw` splitting one mdast `html` node into multiple hast siblings
|
|
201
|
+
* (e.g. ` <div>Hi</div>` — leading spaces shift the `<div>` offset
|
|
202
|
+
* inside the parent html node's source range).
|
|
203
|
+
*
|
|
204
|
+
* In dev, a hast block whose offset matches no mdast counterpart at all
|
|
205
|
+
* throws — that means a rehype plugin synthesized positions outside the
|
|
206
|
+
* source range, which is a bug worth surfacing. In production it falls
|
|
207
|
+
* through to an `inline` plan item so user content is never silently lost.
|
|
208
|
+
*
|
|
209
|
+
* `globalCtx` walks the full mdast for footnote/link/image refs and
|
|
210
|
+
* definitions in document order (no dedupe — order matters for footnote
|
|
211
|
+
* numbering), then JSON-stringifies the collected tuples. That string is the
|
|
212
|
+
* invalidation key for tainted blocks and the synthetic footnote section.
|
|
213
|
+
*/
|
|
214
|
+
declare function buildBlocks(mdast: Root, hast: Root$1, source: string, options?: BuildBlocksOptions): BuildBlocksResult;
|
|
215
|
+
/**
|
|
216
|
+
* Compute a per-block cache fingerprint from the registry slice this block
|
|
217
|
+
* actually depends on (footnote/link/image refs and footnote defs). Two blocks
|
|
218
|
+
* with the same fingerprint render byte-equal output; if any encoded value
|
|
219
|
+
* differs, the block must re-render.
|
|
220
|
+
*
|
|
221
|
+
* Encoding format (deterministic, stable across versions):
|
|
222
|
+
* `<clobberPrefix>|fn:<L>=<globalNumber>|fl:<[[L,prior,rank]…]>|lr:<L>=<url>|<title>|ir:<L>=<url>|fd:<L>=<canonical>/<refCount>`
|
|
223
|
+
*
|
|
224
|
+
* @param taintLabels - Per-block label dependency footprint (from BlockInfo.taintLabels).
|
|
225
|
+
* @param registry - Shared cross-chunk registry.
|
|
226
|
+
* @param thisChunkSym- The Symbol of the chunk this rendering belongs to (for canonical-vs-this comparison).
|
|
227
|
+
* @param clobberPrefix - The documentId-derived id prefix; included so href/id changes invalidate.
|
|
228
|
+
*/
|
|
229
|
+
declare function computeBlockFingerprint(taintLabels: NonNullable<BlockInfo['taintLabels']>, registry: Registry, thisChunkSym: symbol, clobberPrefix: string): string;
|
|
230
|
+
|
|
231
|
+
/** Reuse the engine's retained prefix plans, including reference blocks.
|
|
232
|
+
* Reference prefixes require full-document context when planning the tail.
|
|
233
|
+
* Raw HTML and definition regions retain complete planning: their ownership
|
|
234
|
+
* need not be one-to-one. Identity checks, not source-text
|
|
235
|
+
* guesses, establish which trees the incremental engine actually retained.
|
|
236
|
+
* Top-level traversal remains O(blocks); reference context still visits the
|
|
237
|
+
* full mdast when needed, while per-block HAST planning is tail-only. */
|
|
238
|
+
declare function createBlockPlanner(): (mdast: Root, hast: Root$1, source: string, options?: BuildBlocksOptions) => BuildBlocksResult;
|
|
239
|
+
|
|
240
|
+
/** Minimal write capability; registration and cleanup remain with the adapter. */
|
|
241
|
+
interface ContributionRegistry {
|
|
242
|
+
contributeChunkData(symbol: symbol, data: ChunkData): void;
|
|
243
|
+
}
|
|
244
|
+
interface ContributionOptions {
|
|
245
|
+
pipeline: {
|
|
246
|
+
mdast: Root;
|
|
247
|
+
hast: Root$1;
|
|
248
|
+
};
|
|
249
|
+
ownLabels: {
|
|
250
|
+
footnoteLabels: Set<string>;
|
|
251
|
+
linkLabels: Set<string>;
|
|
252
|
+
};
|
|
253
|
+
registry: ContributionRegistry | null;
|
|
254
|
+
targetPhantoms: {
|
|
255
|
+
missingFootnotes: Set<string>;
|
|
256
|
+
missingLinks: Set<string>;
|
|
257
|
+
};
|
|
258
|
+
sym: symbol | null;
|
|
259
|
+
clobberPrefix: string;
|
|
260
|
+
/** Stable identity tuple of parse inputs. Source equality alone cannot
|
|
261
|
+
* validate definition bodies after a plugin or schema change. */
|
|
262
|
+
chain: readonly unknown[];
|
|
263
|
+
}
|
|
264
|
+
/** Create one contribution publisher per mounted chunk.
|
|
265
|
+
* Call commit only in the host's commit/effect phase, after registration.
|
|
266
|
+
* Constructing a session does not register or publish anything. A discarded
|
|
267
|
+
* render must never call commit. The host owns registration and cleanup.
|
|
268
|
+
*/
|
|
269
|
+
declare function createContributionSession(): {
|
|
270
|
+
commit({ pipeline, ownLabels, registry, targetPhantoms, sym, clobberPrefix, chain }: ContributionOptions): void;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Turn-taking coordinator for smooth-streaming chunks that share a
|
|
275
|
+
* `documentId` under `<AIMarkdownDocuments>`.
|
|
276
|
+
*
|
|
277
|
+
* Each `useDocumentSmoothStream` chunk registers here in mount order and
|
|
278
|
+
* reports a sticky `done` (source stopped AND reveal drained). A chunk that
|
|
279
|
+
* mounted with empty content stays gated until every REGISTERED chunk
|
|
280
|
+
* earlier in the order is done — so the chunks read as one typewriter:
|
|
281
|
+
* chunk N reveals completely, then chunk N+1 starts.
|
|
282
|
+
*
|
|
283
|
+
* The structure deliberately mirrors `documentRegistry`'s proven patterns
|
|
284
|
+
* (chunkId-keyed refcount with microtask-deferred reclaim for Strict Mode,
|
|
285
|
+
* monotonic version + microtask-coalesced subscriber fanout, `onEmpty`
|
|
286
|
+
* container eviction) without touching the registry itself: the def
|
|
287
|
+
* registry and this coordinator share only the `documentId` scope. Chunks
|
|
288
|
+
* that never register (non-smooth chunks, opted-out chunks) simply do not
|
|
289
|
+
* exist here and can never block a successor; an unmounting chunk leaves
|
|
290
|
+
* the order, so a virtualized-away predecessor releases its successors by
|
|
291
|
+
* construction.
|
|
292
|
+
*
|
|
293
|
+
* `lastProgressAt` is the reveal-progress heartbeat behind the dev-only
|
|
294
|
+
* stuck-flag warning. Writes to it intentionally do NOT bump `version` and
|
|
295
|
+
* do NOT fan out — stamping happens once per reveal frame, and a fanout to
|
|
296
|
+
* every sibling chunk per frame would be a performance bug. Nothing
|
|
297
|
+
* subscribes to it; the pending chunk's warning timer reads it on demand.
|
|
298
|
+
*
|
|
299
|
+
* @module components/smoothStream/coordinator
|
|
300
|
+
*/
|
|
301
|
+
interface SmoothCoordinator {
|
|
302
|
+
/** Chunk mount-order chunkId list. **Read-only** — mutation corrupts the
|
|
303
|
+
* release predicate for every successor. */
|
|
304
|
+
readonly order: readonly string[];
|
|
305
|
+
/** Chunk IDs that have reported done. Sticky: entries are only removed
|
|
306
|
+
* when the chunk unregisters. **Read-only.** */
|
|
307
|
+
readonly done: ReadonlySet<string>;
|
|
308
|
+
/** Reveal-progress heartbeat, ms timestamps from the reporting chunk's
|
|
309
|
+
* clock. Written WITHOUT a version bump (see module doc). */
|
|
310
|
+
readonly lastProgressAt: ReadonlyMap<string, number>;
|
|
311
|
+
/** Monotonic counter bumped by every gating-relevant mutation
|
|
312
|
+
* (register / unregister / done transition). */
|
|
313
|
+
readonly version: number;
|
|
314
|
+
subscribe(cb: () => void): () => void;
|
|
315
|
+
/** Allocate (or refcount-reuse, for Strict Mode remounts) this chunk's
|
|
316
|
+
* slot at the current end of the mount order. */
|
|
317
|
+
register(chunkId: string): void;
|
|
318
|
+
/** Refcount decrement with microtask-deferred reclaim; the last release
|
|
319
|
+
* removes the chunk from the order (waking successors) and, when the
|
|
320
|
+
* coordinator empties, fires the container's `onEmpty`. */
|
|
321
|
+
release(chunkId: string): void;
|
|
322
|
+
/** Sticky, idempotent: the first call per chunk bumps the version; the
|
|
323
|
+
* transition is never retracted while the chunk stays mounted (a
|
|
324
|
+
* predecessor that un-finishes must not re-gate successors). */
|
|
325
|
+
markDone(chunkId: string): void;
|
|
326
|
+
/** Heartbeat stamp — no version bump, no fanout. */
|
|
327
|
+
stampProgress(chunkId: string, at: number): void;
|
|
328
|
+
/** True when every registered chunk BEFORE `chunkId` in the order has
|
|
329
|
+
* reported done. Unregistered predecessors don't exist here, so they
|
|
330
|
+
* never block. A `chunkId` that is not (yet) registered itself is
|
|
331
|
+
* treated as sitting AFTER every registered chunk — a pre-registration
|
|
332
|
+
* query must not release a chunk whose predecessors are still
|
|
333
|
+
* revealing. */
|
|
334
|
+
isReleased(chunkId: string): boolean;
|
|
335
|
+
/** The earliest not-done registered chunk before `chunkId`, or null.
|
|
336
|
+
* Read by the dev-only stuck-flag warning timer. */
|
|
337
|
+
earliestBlockerOf(chunkId: string): string | null;
|
|
338
|
+
}
|
|
339
|
+
/** Outcome of one stuck-flag warning timer firing (dev-only diagnostics). */
|
|
340
|
+
type GateWarnVerdict = 'warn' | 'rearm' | 'clear';
|
|
341
|
+
/**
|
|
342
|
+
* The warn / re-arm / clear decision behind the dev-only stuck-flag
|
|
343
|
+
* warning, extracted as a pure function so the judgment is unit-testable
|
|
344
|
+
* in node — the `setTimeout` wiring in `useDocumentSmoothStream` reduces
|
|
345
|
+
* to "call this when the timer fires".
|
|
346
|
+
*
|
|
347
|
+
* - `'clear'`: no blocker left — the release is propagating; stop.
|
|
348
|
+
* - `'rearm'`: the earliest blocker showed reveal progress within the
|
|
349
|
+
* threshold — a slow model still emitting, not a stuck flag; check
|
|
350
|
+
* again later.
|
|
351
|
+
* - `'warn'`: the blocker has made no visible progress for a full
|
|
352
|
+
* threshold while this chunk stayed gated — the predecessor's
|
|
353
|
+
* `streaming` flag is likely stuck.
|
|
354
|
+
*/
|
|
355
|
+
declare function evaluateGateWarn(coordinator: SmoothCoordinator, chunkId: string, nowMs: number, thresholdMs: number): GateWarnVerdict;
|
|
356
|
+
|
|
357
|
+
/** Create one turn-taking scope.
|
|
358
|
+
* Why: sibling chunks coordinate reveal without a framework or DOM dependency.
|
|
359
|
+
* Recommended pattern: register after mount, report sticky completion, release on unmount.
|
|
360
|
+
* Footguns: registration order controls reveal; completion cannot be retracted.
|
|
361
|
+
*/
|
|
362
|
+
declare function createSmoothCoordinator(onEmpty?: () => void): SmoothCoordinator;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Source-tail classification for the streaming cursor.
|
|
366
|
+
*
|
|
367
|
+
* The cursor's DOM walk cannot know whether the source tail is in the body
|
|
368
|
+
* or inside a footnote definition — mdast-util-to-hast relocates every
|
|
369
|
+
* definition into the synthetic end-of-document footer, decoupling the
|
|
370
|
+
* footer's DOM position from source position. This module answers the
|
|
371
|
+
* question at the mdast level, where micromark has already made every
|
|
372
|
+
* grammar decision (lazy continuation lines, container nesting, blank-line
|
|
373
|
+
* attachment, label escapes): the renderer derives a signal per frame from
|
|
374
|
+
* the tree it is about to render and stamps it on a DOM marker the cursor
|
|
375
|
+
* shell reads in the same commit.
|
|
376
|
+
*
|
|
377
|
+
* Deliberately NOT a text scan of the trailing lines: a hand-rolled
|
|
378
|
+
* back-walk would be a third parallel grammar in the repo and provably
|
|
379
|
+
* mis-classifies lazy continuations (`[^1]: line\nline2` keeps line2 in
|
|
380
|
+
* the definition with zero indentation), resurfacing the exact bug this
|
|
381
|
+
* exists to fix.
|
|
382
|
+
*
|
|
383
|
+
* @module components/streamingCursor/tailSignal
|
|
384
|
+
*/
|
|
385
|
+
|
|
386
|
+
type TailSignal =
|
|
387
|
+
/** The source tail is inside a footnote definition — its text renders in
|
|
388
|
+
* the footnote footer's `<li>` for `identifier` (mdast's case-folded
|
|
389
|
+
* form), not at the body tail. */
|
|
390
|
+
{
|
|
391
|
+
kind: 'footnote-def';
|
|
392
|
+
identifier: string;
|
|
393
|
+
}
|
|
394
|
+
/** The source tail is inside a link-reference definition — it renders
|
|
395
|
+
* nothing at all; the cursor should hide rather than point at stale
|
|
396
|
+
* body text. */
|
|
397
|
+
| {
|
|
398
|
+
kind: 'invisible-def';
|
|
399
|
+
}
|
|
400
|
+
/** The source tail is ordinary body content — the DOM-tail heuristic is
|
|
401
|
+
* correct as-is. */
|
|
402
|
+
| null;
|
|
403
|
+
/**
|
|
404
|
+
* Classify the source tail of `mdast`.
|
|
405
|
+
*
|
|
406
|
+
* @param mdast - The tree the renderer is about to render (post-pipeline
|
|
407
|
+
* parse output; the incremental path's spliced tree carries correct
|
|
408
|
+
* positions on real nodes).
|
|
409
|
+
* @param contentLength - Length of the PREPROCESSED content string the
|
|
410
|
+
* renderer parsed (not the caller's raw prop). Phantom definitions are
|
|
411
|
+
* injected as an appended suffix before parsing, so every node whose
|
|
412
|
+
* start offset sits at or past the real content's end is a phantom — the
|
|
413
|
+
* scan walks backwards past ALL of them (a real trailing def plus
|
|
414
|
+
* several phantoms is a normal shape) and classifies the first real
|
|
415
|
+
* node.
|
|
416
|
+
*/
|
|
417
|
+
declare function deriveTailSignal(mdast: Root, contentLength: number): TailSignal;
|
|
418
|
+
|
|
419
|
+
/** Assemble document footnotes as HAST without framework nodes or DOM access. */
|
|
420
|
+
|
|
421
|
+
declare function buildAggregateTree(registry: Registry, clobberPrefix: string, preserveOrphanReferences?: boolean): Element | null;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Structural clone of a hast subtree for render-time use.
|
|
425
|
+
*
|
|
426
|
+
* Copies exactly what the render-time mutators touch — every node object,
|
|
427
|
+
* its `children` array (the allow/disallow filters splice it), and its
|
|
428
|
+
* `properties` object (`urlTransform` overwrites `href`/`src`) — while
|
|
429
|
+
* SHARING `position`, `data`, string values, and individual property values
|
|
430
|
+
* by reference. This is both cheaper than a JSON round-trip (no string
|
|
431
|
+
* intermediate; `position` objects, typically over half of a serialized
|
|
432
|
+
* hast, are never materialized) and exact for any tree the pipeline
|
|
433
|
+
* produces (no lossy JSON semantics to reason about).
|
|
434
|
+
*
|
|
435
|
+
* @module components/cloneHastForRender
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
/** See module docs. Safe for any hast node union member. */
|
|
439
|
+
declare function cloneHastForRender<T extends Nodes>(node: T): T;
|
|
440
|
+
|
|
441
|
+
type RemarkRehypeOptions = NonNullable<PipelineOptions['remarkRehypeOptions']>;
|
|
442
|
+
interface PipelineFrameOptions {
|
|
443
|
+
content: string;
|
|
444
|
+
targetPhantoms: {
|
|
445
|
+
missingFootnotes: Set<string>;
|
|
446
|
+
missingLinks: Set<string>;
|
|
447
|
+
};
|
|
448
|
+
remarkPlugins: PipelineOptions['remarkPlugins'];
|
|
449
|
+
rehypePlugins: PipelineOptions['rehypePlugins'];
|
|
450
|
+
remarkRehypeOptions: PipelineOptions['remarkRehypeOptions'];
|
|
451
|
+
handlers?: RemarkRehypeOptions['handlers'];
|
|
452
|
+
preserveForBodyHarvest: boolean;
|
|
453
|
+
documentId: string;
|
|
454
|
+
provenance: string;
|
|
455
|
+
/** False for a one-shot server render; no browser-global probe is needed. */
|
|
456
|
+
incrementalParse: boolean;
|
|
457
|
+
defListEnabled: boolean;
|
|
458
|
+
measure?: AdvanceOptions['measure'];
|
|
459
|
+
}
|
|
460
|
+
declare function createPipelineSession(): {
|
|
461
|
+
reset(): void;
|
|
462
|
+
parse({ content, targetPhantoms, remarkPlugins, rehypePlugins, remarkRehypeOptions, handlers, preserveForBodyHarvest, documentId, provenance, incrementalParse, defListEnabled, measure: measureHere, }: PipelineFrameOptions): {
|
|
463
|
+
mdast: mdast.Root;
|
|
464
|
+
hast: hast.Root;
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
interface DefinitionLabels {
|
|
469
|
+
footnoteLabels: ReadonlySet<string>;
|
|
470
|
+
linkLabels: ReadonlySet<string>;
|
|
471
|
+
}
|
|
472
|
+
type PhantomTargets = PipelineFrameOptions['targetPhantoms'];
|
|
473
|
+
declare function derivePhantomTargets({ content, ownLabels, labels }: {
|
|
474
|
+
content: string;
|
|
475
|
+
ownLabels: DefinitionLabels;
|
|
476
|
+
labels: DefinitionLabels | null;
|
|
477
|
+
}, previous?: PhantomTargets): PhantomTargets;
|
|
478
|
+
interface CoordinationPolicy {
|
|
479
|
+
mode: 'coordinated' | 'orphan' | 'standalone';
|
|
480
|
+
handlers: PipelineFrameOptions['handlers'];
|
|
481
|
+
preserveForBodyHarvest: boolean;
|
|
482
|
+
}
|
|
483
|
+
declare function deriveCoordinationPolicy({ coordinated, registered, preserveOrphanReferences, }: {
|
|
484
|
+
coordinated: boolean;
|
|
485
|
+
registered: boolean;
|
|
486
|
+
/** The host has already resolved any wrapper-level override. */
|
|
487
|
+
preserveOrphanReferences: boolean;
|
|
488
|
+
}, previous?: CoordinationPolicy): CoordinationPolicy;
|
|
489
|
+
type ContributionPolicyInputs = Pick<PipelineFrameOptions, 'remarkPlugins' | 'rehypePlugins' | 'remarkRehypeOptions' | 'handlers' | 'preserveForBodyHarvest' | 'documentId' | 'provenance'> & {
|
|
490
|
+
clobberPrefix: string;
|
|
491
|
+
};
|
|
492
|
+
/** Source/phantom/registry/symbol changes are compared by the publisher itself.
|
|
493
|
+
* This tuple covers the remaining inputs that can alter harvested bodies.
|
|
494
|
+
*/
|
|
495
|
+
declare function buildContributionChain(inputs: ContributionPolicyInputs): readonly unknown[];
|
|
496
|
+
|
|
497
|
+
export { type BlockInfo, type BuildBlocksOptions, type BuildBlocksResult, type ContributionOptions, type ContributionPolicyInputs, type ContributionRegistry, type CoordinationPolicy, type DefinitionLabels, type GateWarnVerdict, type PhantomTargets, type PipelineFrameOptions, type RenderItem, type SmoothCoordinator, type TailSignal, buildAggregateTree, buildBlocks, buildContributionChain, cloneHastForRender, computeBlockFingerprint, computeHtmlBlockDigest, computeHtmlBlockDigestWithExtent, createBlockPlanner, createContributionSession, createPipelineSession, createSmoothCoordinator, deriveCoordinationPolicy, derivePhantomTargets, deriveTailSignal, evaluateGateWarn, hasMdastSource };
|