@ai-react-markdown/engine 2.4.3 → 2.5.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/dist/index.d.cts CHANGED
@@ -360,6 +360,16 @@ interface FreezeScanCheckpoint {
360
360
  piOpen: boolean;
361
361
  declOpen: boolean;
362
362
  cdataOpen: boolean;
363
+ /** parse5 BOGUS COMMENT open inside a real html-flow run: `<!` not
364
+ * followed by `--` / letter / `[CDATA[`, or `</` not followed by a
365
+ * letter (`<!\n`, `<!-`, `</\n`, `<//`) — eaten up to the next `>`;
366
+ * a `</div>` inside is comment text, not a close (2026-08-19 review r2
367
+ * P1-3: the balance zeroed past an open div). micromark has no such
368
+ * construct: those bytes are just html-block content. */
369
+ bogusOpen: boolean;
370
+ /** The RAW_TEXT_ELEMENTS element currently open (its content is text to
371
+ * parse5): tags and comment tokens are ignored until `</name>`. */
372
+ rawTextOpen: string | null;
363
373
  inFence: boolean;
364
374
  fenceChar: string;
365
375
  fenceLen: number;
@@ -425,7 +435,61 @@ interface FreezeScanCheckpoint {
425
435
  * tagBalance but not yet confirmed by a later `>` — reverted at the next
426
436
  * blank line (a tag cannot span one). See TRUNCATED_TAG_RE. */
427
437
  pendingTruncatedTags: string[];
438
+ /** Line-truncated CLOSING tags (`</div` + EOL, no `>`) inside an html-flow
439
+ * run, waiting for a `>` on a later line of the same run. Unlike opens
440
+ * they are NOT counted up front: a close tag cannot carry attributes, so
441
+ * `para </style` (prose to micromark, still-open element to parse5 —
442
+ * RAWTEXT waits for the `>`) must not zero the balance (2026-08-19
443
+ * review P1 — the boundary crossed an open `<style>`). Confirmed and
444
+ * applied only when a later line of the run brings the `>` (parse5
445
+ * completes the end tag; micromark's block ends at the blank anyway);
446
+ * dropped unapplied at the blank (element stays counted — over-block).
447
+ * Paragraph-context truncated closes are never pended: a `>` at block
448
+ * indent on the next line is a blockquote to micromark, and the one
449
+ * shape that would complete the inline close (`</b\n >`, a 4+-space
450
+ * lazy continuation) is not modelled — the element stays counted,
451
+ * over-block. */
452
+ pendingTruncatedCloses: string[];
453
+ /** An html-flow line ended inside a tag (`<div`, `</div`, `<br` — open,
454
+ * close or void, with or without attributes): parse5's tokenizer is
455
+ * still in that tag, so on the following lines everything up to the
456
+ * FIRST `>` is attribute garbage — `</div>` there does NOT close
457
+ * anything (oracle review of 2.4.4: `<div>\n<div>\n</div\n</div>` froze
458
+ * past the still-open outer div; pre-existing, 1-char slices). While
459
+ * set: a line without `>` gets no tag scan at all; the line with the
460
+ * `>` completes the pending close (if any), then only its text after
461
+ * that `>` is scanned. Cleared there and at the blank line. */
462
+ tagAcrossLines: boolean;
463
+ /** Indent of the line that set `tagAcrossLines`. A following line that
464
+ * DE-INDENTS below it may have left the container (a list item's html
465
+ * block ends there; hast-util-raw resets the tokenizer at the li/ul
466
+ * boundary, so a `<div>` on that line is a real start tag, not garbage)
467
+ * — or may still be the same html block (root-level ` </div\n</div>`,
468
+ * still garbage). Unknowable here → poison (oracle 3rd pass). */
469
+ tagAcrossLinesIndent: number;
470
+ /** parse5's position inside that tag's attribute area at the end of the
471
+ * last scanned line (see TagAttrState). A quoted value left open swallows
472
+ * `>` and line endings; a `>` that arrives while `outside` ends the tag. */
473
+ tagAcrossLinesState: TagAttrState;
474
+ /** The html-flow run since the last blank REALLY started as a micromark
475
+ * html block (type 6 / type 1 / a paragraph-not-interrupting type 7) —
476
+ * as opposed to `htmlFlowSinceBlank`, which any `<tag` / `</tag` line
477
+ * start sets (over-approximation, fine for its over-blocking uses). Only
478
+ * in a real run are the bytes raw to parse5 across line endings; the
479
+ * cross-line-tag garbage model (`tagAcrossLines`, pended closes) is
480
+ * gated on it — in a paragraph starting `</i` the next line's `<div>` /
481
+ * `<!--` are REAL blocks (oracle re-check of 2.4.4: gating on
482
+ * htmlFlowSinceBlank swallowed them — a new under-block). Sticky to the
483
+ * blank; a type 6/1 start on a later line of a non-real run promotes it. */
484
+ htmlFlowReal: boolean;
428
485
  }
486
+ /** parse5 tokenizer position inside a tag's attribute area, tracked across
487
+ * line endings for `tagAcrossLines` (2026-08-19 review r2 P1-2 / P2-3):
488
+ * `outside` = before/in an attribute name (a `"` here starts a NAME, not a
489
+ * value); `afterEq` = just past `=`; `unquoted` = in an unquoted value
490
+ * (whitespace ends it); `"` / `'` = inside a quoted value — `>` and line
491
+ * endings are value bytes there, only the matching quote leaves it. */
492
+ type TagAttrState = 'outside' | 'afterEq' | 'unquoted' | '"' | "'";
429
493
  declare function computeFreezeBoundary(text: string, options: FreezeBoundaryOptions, resume?: FreezeScanCheckpoint | null): FreezeScanResult;
430
494
 
431
495
  /**
@@ -649,38 +713,6 @@ interface DefLabels {
649
713
  linkLabels: Set<string>;
650
714
  }
651
715
  declare function collectDefLabels(source: string): DefLabels;
652
- /** Index just past the LAST blank line of `source`, or 0 if none.
653
- * Plain non-overlapping scan: for runs of blanks ("\n\n\n") this can land
654
- * a newline or two early, but the slack is whitespace-only and whitespace
655
- * can never satisfy DEF_LINE_START_RE, so the decision is identical.
656
- * @internal exported for tests only — the fast path is otherwise
657
- * indistinguishable from a full parse whose sets came out equal. */
658
- declare function lastRegionStart(source: string): number;
659
- /** A line that can START a definition, matched by the FULL def signature:
660
- * container prefixes (blockquote `>`, list bullets, ordered-list digits),
661
- * then `[label]` with the closing bracket IMMEDIATELY followed by `:` —
662
- * remark accepts a definition only with that adjacency (grammar-verified:
663
- * `[x]\n: url` and `[x] : url` are paragraphs, and `[a][b]: url` is a
664
- * reference because the label's first unescaped `]` isn't followed by
665
- * `:`). The label alternation admits escape pairs (`\]` stays inside the
666
- * label) and spans newlines (labels may soft-wrap; they cannot cross the
667
- * blank line that bounds the region). Both alternatives are disjoint, so
668
- * the scan is linear — no backtracking blowup on bracket-dense regions.
669
- *
670
- * Requiring the signature (not just a line-start `[`) is what keeps the
671
- * streaming-heavy shapes — bulleted link lists `- [t](u)`, task boxes
672
- * `- [x]`, reference lists `- [a][b]` — on the fast path; a bracket-only
673
- * probe made every append inside a blank-line-free link list pay a full
674
- * reparse (the measured Documents+smooth cliff). An INCOMPLETE def line
675
- * (`[x` with `]:` still in flight) correctly stays on the fast path too:
676
- * the parser sees no definition in it either, and the region re-check on
677
- * the completing append flips to the full parse exactly when the answer
678
- * can change. The `m` flag also matches at index 0, which is a true line
679
- * start (the region begins just past a blank line or at the document
680
- * start). Residual over-matching (e.g. `[x]:` inside an open code fence)
681
- * is safe: it costs a redundant full parse, never a wrong result.
682
- * @internal exported for tests only. */
683
- declare const DEF_LINE_START_RE: RegExp;
684
716
  interface DefLabelScanner {
685
717
  /** Equivalent to `collectDefLabels(source)` at every call, but cheap for
686
718
  * the streaming common case. Returns a REFERENCE-STABLE result while the
@@ -906,8 +938,18 @@ interface RegistryInternal extends Registry {
906
938
  * keeps the version monotonic-by-1-per-mount which makes debugging
907
939
  * easier). The granular `allocateSymbol` / `contributeLabels` methods
908
940
  * remain available for tests that need to exercise each step. */
909
- registerChunk(reactId: string, footnotes: Set<string>, links: Set<string>): symbol;
910
- allocateSymbol(reactId: string): symbol;
941
+ registerChunk(reactId: string, footnotes: Set<string>, links: Set<string>, documentIndex?: number): symbol;
942
+ /** `documentIndex` — the chunk's position in the DOCUMENT (not its mount
943
+ * order). Without it a chunk joins `chunkOrder` at the end, which is
944
+ * wrong for any list that unmounts and remounts chunks: a virtualized
945
+ * transcript scrolling back re-registers an earlier chunk after the ones
946
+ * still mounted, and footnote numbering plus "which chunk renders the
947
+ * aggregate footer" follow that order (2026-08-19 review r2 P2-10).
948
+ * Chunks that supply an index are kept sorted by it; chunks without one
949
+ * keep mount order after them. */
950
+ allocateSymbol(reactId: string, documentIndex?: number): symbol;
951
+ /** @internal Insert a chunk Symbol at its document position. */
952
+ _placeChunk(sym: symbol, documentIndex?: number): void;
911
953
  releaseSymbol(reactId: string): void;
912
954
  contributeLabels(symbol: symbol, footnotes: Set<string>, links: Set<string>): void;
913
955
  contributeChunkData(symbol: symbol, data: ChunkData): void;
@@ -915,6 +957,8 @@ interface RegistryInternal extends Registry {
915
957
  symbol: symbol;
916
958
  refcount: number;
917
959
  }>;
960
+ /** Symbol → the `documentIndex` its chunk supplied (see allocateSymbol). */
961
+ _chunkIndex: Map<symbol, number>;
918
962
  _subscribers: Set<() => void>;
919
963
  _notifyScheduled: boolean;
920
964
  _notify(): void;
@@ -1019,6 +1063,12 @@ type SanitizeSchema = typeof defaultSchema;
1019
1063
  * uncaught — there is no try/catch. Callers usually invoke this once at
1020
1064
  * module load, where a thrown error surfaces as a startup-time crash and
1021
1065
  * is the correct failure mode.
1066
+ * - **Widening is total.** Adding a tag to `tagNames` admits that element
1067
+ * with ALL of its semantics — `script`, `iframe`, `style`, `object`,
1068
+ * `form`, `base` re-enter the tree verbatim, and the two-gate URL story
1069
+ * in the README / SECURITY.md no longer describes your renderer. Prefer
1070
+ * allowing an attribute or a protocol over allowing an element; treat
1071
+ * any element you add as trusted input.
1022
1072
  *
1023
1073
  * @param modifier - Receives a deep clone of the library default. Mutate it
1024
1074
  * freely; either return the (possibly different) result, or return
@@ -1413,10 +1463,16 @@ declare function isFootnoteSection(node: Element): boolean;
1413
1463
 
1414
1464
  declare function normalizeId(s: string): string;
1415
1465
  /**
1416
- * Same as {@link normalizeId} plus resolution of backslash escapes.
1417
- * Used by PASS 0.5 substring pre-check against raw chunk source text:
1418
- * sources may write `[foo\]bar]` but the resulting label identifier is
1419
- * `foo]bar`, so we must unescape source before substring matching.
1466
+ * The form of raw chunk SOURCE that the PASS 0.5 substring pre-check compares
1467
+ * registry labels against. It is exactly {@link normalizeId}: micromark's
1468
+ * `identifier` (what the registry keys are built from) is the label's
1469
+ * source bytes with backslashes KEPT (`[^a\*b]` `a\*b`; only the `label`
1470
+ * field is unescaped), so the source must NOT be unescaped before matching
1471
+ * — an unescaping pass made every label holding an escaped punctuation
1472
+ * character miss the pre-check for good, and the cross-chunk reference
1473
+ * rendered as literal `[text][label]` (2026-08-19 review r2 P2-5; the
1474
+ * 2.4.5 narrowing to ASCII punctuation kept the wrong premise). Kept as a
1475
+ * named export for API stability.
1420
1476
  */
1421
1477
  declare function normalizeForMatch(s: string): string;
1422
1478
 
@@ -1604,25 +1660,6 @@ type AttributeEntry = NonNullable<NonNullable<Schema['attributes']>[string]>[num
1604
1660
  * is always tuple-form, so this branch is defensive only.
1605
1661
  */
1606
1662
  declare function mergeClassNameAllowlist(existing: ReadonlyArray<AttributeEntry> | undefined, extraClassNames: readonly string[]): AttributeEntry[];
1607
- /**
1608
- * The full sanitize schema used by the markdown renderer: extends
1609
- * `defaultSchema` to allow `<mark>`, the KaTeX math class names, and the
1610
- * three custom hast tags emitted by cross-chunk coordination handlers.
1611
- *
1612
- * **Owns its arrays and objects.** The shallow spread of `defaultSchema`
1613
- * alone would leave `attributes.a`, `attributes.img`, `protocols`,
1614
- * `ancestors`, and similar nested fields aliased to `rehype-sanitize`'s
1615
- * default singleton. A consumer who reasonably (but mistakenly) writes
1616
- * `sanitizeSchema.protocols.href.push('myapp')` would then poison
1617
- * `rehype-sanitize`'s `defaultSchema` for every other consumer in the
1618
- * process — a cross-package side-effect that's near-impossible to debug.
1619
- *
1620
- * One `cloneDeep` at module init breaks that aliasing without measurable
1621
- * cost (init-time only, single small object graph). The recommended
1622
- * extension API is still {@link extendSanitizeSchema}, which clones again
1623
- * per call; this layer just makes the exported singleton safe if someone
1624
- * skips the helper.
1625
- */
1626
1663
  declare const sanitizeSchema: Schema;
1627
1664
 
1628
1665
  /**
@@ -1902,6 +1939,20 @@ declare function preprocessLaTeX(str: string): string;
1902
1939
  */
1903
1940
  declare function createIncrementalLatexPreprocessor(options?: {
1904
1941
  freezeThreshold?: number;
1942
+ /** Failure backoff (default on). Tests that rely on `freezeThreshold: 0`
1943
+ * to attempt a freeze on EVERY call turn it off, so 1-char chunkings keep
1944
+ * exercising the cut rules instead of skipping most attempts; backoff
1945
+ * can only freeze LESS, so passing without it implies passing with it.
1946
+ * @internal */
1947
+ backoff?: boolean;
1948
+ /** Test hook: called once per freeze ATTEMPT (a scan of the active
1949
+ * region) with the region's length and how many source bytes the
1950
+ * attempt froze (0 = failed), so the backoff bound and freeze progress
1951
+ * can be pinned. @internal */
1952
+ onAttempt?: (info: {
1953
+ activeLength: number;
1954
+ frozenBytes: number;
1955
+ }) => void;
1905
1956
  }): (content: string) => string;
1906
1957
 
1907
1958
  /**
@@ -1970,4 +2021,4 @@ type RemendPreprocessorOptions = Omit<RemendOptions, 'katex' | 'inlineKatex'>;
1970
2021
  */
1971
2022
  declare function createRemendPreprocessor(options?: RemendPreprocessorOptions): AIMDContentPreprocessor;
1972
2023
 
1973
- export { type AIMDContentPreprocessor, type AIMarkdownEnginePlugin, type AIMarkdownEnginePluginName, type AdvanceOptions, type AdvanceResult, type AllowElement, type ChunkData, type Contribution, type CrossChunkHandlerOptions, DEFAULT_PAYLOAD, DEF_LINE_START_RE, type DefLabelScanner, type DefLabels, type Deprecation, type EnginePluginInternals, type EnginePluginStage, type ExtractContributionsOptions, type FootnoteDef, type FreezeBoundaryOptions, type IncrementalParseState, type IncrementalStage, type LinkDef, PIPELINE_STAGES, type ParsedMarkdown, type PhantomLabels, type PipelineOptions, type PipelineStage, type RefKind, type RefRecord, type Registry, type RegistryInternal, type RehypePlugins, type RehypeRebaseHashLinksOptions, type RemarkPlugins, type RemarkRehypeOptions, type RemendPreprocessorOptions, SENTINEL_FN_CONTENT, SENTINEL_LINK_URL, SMOOTH_STREAM_PACING_PRESETS, STAGE_MEASURE_PREFIX, type SanitizeSchema, type SmoothStreamController, type SmoothStreamOptions, type SmoothStreamPacing, type SmoothStreamPacingParams, type TransformContext, type UrlAttrKey, type UrlAttrTag, type UrlTransform, advanceIncrementalParse, attributeHastChildren, buildCoreRehypePlugins, buildCoreRemarkPlugins, buildCoreRemarkRehypeOptions, buildCrossChunkHandlers, buildPhantomSuffix, buildTransform, codePointSnapshots, collectDefLabels, computeFreezeBoundary, createDefLabelScanner, createFile, createIncrementalLatexPreprocessor, createProcessor, createRegistry, createRemendPreprocessor, createSmoothStreamController, defaultEnginePlugins, defaultUrlTransform, definitionList, extendSanitizeSchema, extractContributions, extractDefBodiesFromHast, footnoteSafeId, getEnginePluginInternals, hasLoneSurrogate, highlight, isFootnoteSection, lastRegionStart, measureStage, mergeClassNameAllowlist, normalizeForMatch, normalizeId, pangu, parseStage, phantomSuffixCloser, preprocessAIMDContent, preprocessLaTeX, rehypeFooterAdorn, rehypeRebaseHashLinks, removeComments, sanitizeCrossChunkUrl, sanitizeSchema, shortenDocumentId, smartypants, sourceIdFromFootnoteLiId, splitByProtectedRegions, subscribeStageTimings, transformStage, withDefs };
2024
+ export { type AIMDContentPreprocessor, type AIMarkdownEnginePlugin, type AIMarkdownEnginePluginName, type AdvanceOptions, type AdvanceResult, type AllowElement, type ChunkData, type Contribution, type CrossChunkHandlerOptions, DEFAULT_PAYLOAD, type DefLabelScanner, type DefLabels, type Deprecation, type EnginePluginInternals, type EnginePluginStage, type ExtractContributionsOptions, type FootnoteDef, type FreezeBoundaryOptions, type IncrementalParseState, type IncrementalStage, type LinkDef, PIPELINE_STAGES, type ParsedMarkdown, type PhantomLabels, type PipelineOptions, type PipelineStage, type RefKind, type RefRecord, type Registry, type RegistryInternal, type RehypePlugins, type RehypeRebaseHashLinksOptions, type RemarkPlugins, type RemarkRehypeOptions, type RemendPreprocessorOptions, SENTINEL_FN_CONTENT, SENTINEL_LINK_URL, SMOOTH_STREAM_PACING_PRESETS, STAGE_MEASURE_PREFIX, type SanitizeSchema, type SmoothStreamController, type SmoothStreamOptions, type SmoothStreamPacing, type SmoothStreamPacingParams, type TransformContext, type UrlAttrKey, type UrlAttrTag, type UrlTransform, advanceIncrementalParse, attributeHastChildren, buildCoreRehypePlugins, buildCoreRemarkPlugins, buildCoreRemarkRehypeOptions, buildCrossChunkHandlers, buildPhantomSuffix, buildTransform, codePointSnapshots, collectDefLabels, computeFreezeBoundary, createDefLabelScanner, createFile, createIncrementalLatexPreprocessor, createProcessor, createRegistry, createRemendPreprocessor, createSmoothStreamController, defaultEnginePlugins, defaultUrlTransform, definitionList, extendSanitizeSchema, extractContributions, extractDefBodiesFromHast, footnoteSafeId, getEnginePluginInternals, hasLoneSurrogate, highlight, isFootnoteSection, measureStage, mergeClassNameAllowlist, normalizeForMatch, normalizeId, pangu, parseStage, phantomSuffixCloser, preprocessAIMDContent, preprocessLaTeX, rehypeFooterAdorn, rehypeRebaseHashLinks, removeComments, sanitizeCrossChunkUrl, sanitizeSchema, shortenDocumentId, smartypants, sourceIdFromFootnoteLiId, splitByProtectedRegions, subscribeStageTimings, transformStage, withDefs };
package/dist/index.d.ts CHANGED
@@ -360,6 +360,16 @@ interface FreezeScanCheckpoint {
360
360
  piOpen: boolean;
361
361
  declOpen: boolean;
362
362
  cdataOpen: boolean;
363
+ /** parse5 BOGUS COMMENT open inside a real html-flow run: `<!` not
364
+ * followed by `--` / letter / `[CDATA[`, or `</` not followed by a
365
+ * letter (`<!\n`, `<!-`, `</\n`, `<//`) — eaten up to the next `>`;
366
+ * a `</div>` inside is comment text, not a close (2026-08-19 review r2
367
+ * P1-3: the balance zeroed past an open div). micromark has no such
368
+ * construct: those bytes are just html-block content. */
369
+ bogusOpen: boolean;
370
+ /** The RAW_TEXT_ELEMENTS element currently open (its content is text to
371
+ * parse5): tags and comment tokens are ignored until `</name>`. */
372
+ rawTextOpen: string | null;
363
373
  inFence: boolean;
364
374
  fenceChar: string;
365
375
  fenceLen: number;
@@ -425,7 +435,61 @@ interface FreezeScanCheckpoint {
425
435
  * tagBalance but not yet confirmed by a later `>` — reverted at the next
426
436
  * blank line (a tag cannot span one). See TRUNCATED_TAG_RE. */
427
437
  pendingTruncatedTags: string[];
438
+ /** Line-truncated CLOSING tags (`</div` + EOL, no `>`) inside an html-flow
439
+ * run, waiting for a `>` on a later line of the same run. Unlike opens
440
+ * they are NOT counted up front: a close tag cannot carry attributes, so
441
+ * `para </style` (prose to micromark, still-open element to parse5 —
442
+ * RAWTEXT waits for the `>`) must not zero the balance (2026-08-19
443
+ * review P1 — the boundary crossed an open `<style>`). Confirmed and
444
+ * applied only when a later line of the run brings the `>` (parse5
445
+ * completes the end tag; micromark's block ends at the blank anyway);
446
+ * dropped unapplied at the blank (element stays counted — over-block).
447
+ * Paragraph-context truncated closes are never pended: a `>` at block
448
+ * indent on the next line is a blockquote to micromark, and the one
449
+ * shape that would complete the inline close (`</b\n >`, a 4+-space
450
+ * lazy continuation) is not modelled — the element stays counted,
451
+ * over-block. */
452
+ pendingTruncatedCloses: string[];
453
+ /** An html-flow line ended inside a tag (`<div`, `</div`, `<br` — open,
454
+ * close or void, with or without attributes): parse5's tokenizer is
455
+ * still in that tag, so on the following lines everything up to the
456
+ * FIRST `>` is attribute garbage — `</div>` there does NOT close
457
+ * anything (oracle review of 2.4.4: `<div>\n<div>\n</div\n</div>` froze
458
+ * past the still-open outer div; pre-existing, 1-char slices). While
459
+ * set: a line without `>` gets no tag scan at all; the line with the
460
+ * `>` completes the pending close (if any), then only its text after
461
+ * that `>` is scanned. Cleared there and at the blank line. */
462
+ tagAcrossLines: boolean;
463
+ /** Indent of the line that set `tagAcrossLines`. A following line that
464
+ * DE-INDENTS below it may have left the container (a list item's html
465
+ * block ends there; hast-util-raw resets the tokenizer at the li/ul
466
+ * boundary, so a `<div>` on that line is a real start tag, not garbage)
467
+ * — or may still be the same html block (root-level ` </div\n</div>`,
468
+ * still garbage). Unknowable here → poison (oracle 3rd pass). */
469
+ tagAcrossLinesIndent: number;
470
+ /** parse5's position inside that tag's attribute area at the end of the
471
+ * last scanned line (see TagAttrState). A quoted value left open swallows
472
+ * `>` and line endings; a `>` that arrives while `outside` ends the tag. */
473
+ tagAcrossLinesState: TagAttrState;
474
+ /** The html-flow run since the last blank REALLY started as a micromark
475
+ * html block (type 6 / type 1 / a paragraph-not-interrupting type 7) —
476
+ * as opposed to `htmlFlowSinceBlank`, which any `<tag` / `</tag` line
477
+ * start sets (over-approximation, fine for its over-blocking uses). Only
478
+ * in a real run are the bytes raw to parse5 across line endings; the
479
+ * cross-line-tag garbage model (`tagAcrossLines`, pended closes) is
480
+ * gated on it — in a paragraph starting `</i` the next line's `<div>` /
481
+ * `<!--` are REAL blocks (oracle re-check of 2.4.4: gating on
482
+ * htmlFlowSinceBlank swallowed them — a new under-block). Sticky to the
483
+ * blank; a type 6/1 start on a later line of a non-real run promotes it. */
484
+ htmlFlowReal: boolean;
428
485
  }
486
+ /** parse5 tokenizer position inside a tag's attribute area, tracked across
487
+ * line endings for `tagAcrossLines` (2026-08-19 review r2 P1-2 / P2-3):
488
+ * `outside` = before/in an attribute name (a `"` here starts a NAME, not a
489
+ * value); `afterEq` = just past `=`; `unquoted` = in an unquoted value
490
+ * (whitespace ends it); `"` / `'` = inside a quoted value — `>` and line
491
+ * endings are value bytes there, only the matching quote leaves it. */
492
+ type TagAttrState = 'outside' | 'afterEq' | 'unquoted' | '"' | "'";
429
493
  declare function computeFreezeBoundary(text: string, options: FreezeBoundaryOptions, resume?: FreezeScanCheckpoint | null): FreezeScanResult;
430
494
 
431
495
  /**
@@ -649,38 +713,6 @@ interface DefLabels {
649
713
  linkLabels: Set<string>;
650
714
  }
651
715
  declare function collectDefLabels(source: string): DefLabels;
652
- /** Index just past the LAST blank line of `source`, or 0 if none.
653
- * Plain non-overlapping scan: for runs of blanks ("\n\n\n") this can land
654
- * a newline or two early, but the slack is whitespace-only and whitespace
655
- * can never satisfy DEF_LINE_START_RE, so the decision is identical.
656
- * @internal exported for tests only — the fast path is otherwise
657
- * indistinguishable from a full parse whose sets came out equal. */
658
- declare function lastRegionStart(source: string): number;
659
- /** A line that can START a definition, matched by the FULL def signature:
660
- * container prefixes (blockquote `>`, list bullets, ordered-list digits),
661
- * then `[label]` with the closing bracket IMMEDIATELY followed by `:` —
662
- * remark accepts a definition only with that adjacency (grammar-verified:
663
- * `[x]\n: url` and `[x] : url` are paragraphs, and `[a][b]: url` is a
664
- * reference because the label's first unescaped `]` isn't followed by
665
- * `:`). The label alternation admits escape pairs (`\]` stays inside the
666
- * label) and spans newlines (labels may soft-wrap; they cannot cross the
667
- * blank line that bounds the region). Both alternatives are disjoint, so
668
- * the scan is linear — no backtracking blowup on bracket-dense regions.
669
- *
670
- * Requiring the signature (not just a line-start `[`) is what keeps the
671
- * streaming-heavy shapes — bulleted link lists `- [t](u)`, task boxes
672
- * `- [x]`, reference lists `- [a][b]` — on the fast path; a bracket-only
673
- * probe made every append inside a blank-line-free link list pay a full
674
- * reparse (the measured Documents+smooth cliff). An INCOMPLETE def line
675
- * (`[x` with `]:` still in flight) correctly stays on the fast path too:
676
- * the parser sees no definition in it either, and the region re-check on
677
- * the completing append flips to the full parse exactly when the answer
678
- * can change. The `m` flag also matches at index 0, which is a true line
679
- * start (the region begins just past a blank line or at the document
680
- * start). Residual over-matching (e.g. `[x]:` inside an open code fence)
681
- * is safe: it costs a redundant full parse, never a wrong result.
682
- * @internal exported for tests only. */
683
- declare const DEF_LINE_START_RE: RegExp;
684
716
  interface DefLabelScanner {
685
717
  /** Equivalent to `collectDefLabels(source)` at every call, but cheap for
686
718
  * the streaming common case. Returns a REFERENCE-STABLE result while the
@@ -906,8 +938,18 @@ interface RegistryInternal extends Registry {
906
938
  * keeps the version monotonic-by-1-per-mount which makes debugging
907
939
  * easier). The granular `allocateSymbol` / `contributeLabels` methods
908
940
  * remain available for tests that need to exercise each step. */
909
- registerChunk(reactId: string, footnotes: Set<string>, links: Set<string>): symbol;
910
- allocateSymbol(reactId: string): symbol;
941
+ registerChunk(reactId: string, footnotes: Set<string>, links: Set<string>, documentIndex?: number): symbol;
942
+ /** `documentIndex` — the chunk's position in the DOCUMENT (not its mount
943
+ * order). Without it a chunk joins `chunkOrder` at the end, which is
944
+ * wrong for any list that unmounts and remounts chunks: a virtualized
945
+ * transcript scrolling back re-registers an earlier chunk after the ones
946
+ * still mounted, and footnote numbering plus "which chunk renders the
947
+ * aggregate footer" follow that order (2026-08-19 review r2 P2-10).
948
+ * Chunks that supply an index are kept sorted by it; chunks without one
949
+ * keep mount order after them. */
950
+ allocateSymbol(reactId: string, documentIndex?: number): symbol;
951
+ /** @internal Insert a chunk Symbol at its document position. */
952
+ _placeChunk(sym: symbol, documentIndex?: number): void;
911
953
  releaseSymbol(reactId: string): void;
912
954
  contributeLabels(symbol: symbol, footnotes: Set<string>, links: Set<string>): void;
913
955
  contributeChunkData(symbol: symbol, data: ChunkData): void;
@@ -915,6 +957,8 @@ interface RegistryInternal extends Registry {
915
957
  symbol: symbol;
916
958
  refcount: number;
917
959
  }>;
960
+ /** Symbol → the `documentIndex` its chunk supplied (see allocateSymbol). */
961
+ _chunkIndex: Map<symbol, number>;
918
962
  _subscribers: Set<() => void>;
919
963
  _notifyScheduled: boolean;
920
964
  _notify(): void;
@@ -1019,6 +1063,12 @@ type SanitizeSchema = typeof defaultSchema;
1019
1063
  * uncaught — there is no try/catch. Callers usually invoke this once at
1020
1064
  * module load, where a thrown error surfaces as a startup-time crash and
1021
1065
  * is the correct failure mode.
1066
+ * - **Widening is total.** Adding a tag to `tagNames` admits that element
1067
+ * with ALL of its semantics — `script`, `iframe`, `style`, `object`,
1068
+ * `form`, `base` re-enter the tree verbatim, and the two-gate URL story
1069
+ * in the README / SECURITY.md no longer describes your renderer. Prefer
1070
+ * allowing an attribute or a protocol over allowing an element; treat
1071
+ * any element you add as trusted input.
1022
1072
  *
1023
1073
  * @param modifier - Receives a deep clone of the library default. Mutate it
1024
1074
  * freely; either return the (possibly different) result, or return
@@ -1413,10 +1463,16 @@ declare function isFootnoteSection(node: Element): boolean;
1413
1463
 
1414
1464
  declare function normalizeId(s: string): string;
1415
1465
  /**
1416
- * Same as {@link normalizeId} plus resolution of backslash escapes.
1417
- * Used by PASS 0.5 substring pre-check against raw chunk source text:
1418
- * sources may write `[foo\]bar]` but the resulting label identifier is
1419
- * `foo]bar`, so we must unescape source before substring matching.
1466
+ * The form of raw chunk SOURCE that the PASS 0.5 substring pre-check compares
1467
+ * registry labels against. It is exactly {@link normalizeId}: micromark's
1468
+ * `identifier` (what the registry keys are built from) is the label's
1469
+ * source bytes with backslashes KEPT (`[^a\*b]` `a\*b`; only the `label`
1470
+ * field is unescaped), so the source must NOT be unescaped before matching
1471
+ * — an unescaping pass made every label holding an escaped punctuation
1472
+ * character miss the pre-check for good, and the cross-chunk reference
1473
+ * rendered as literal `[text][label]` (2026-08-19 review r2 P2-5; the
1474
+ * 2.4.5 narrowing to ASCII punctuation kept the wrong premise). Kept as a
1475
+ * named export for API stability.
1420
1476
  */
1421
1477
  declare function normalizeForMatch(s: string): string;
1422
1478
 
@@ -1604,25 +1660,6 @@ type AttributeEntry = NonNullable<NonNullable<Schema['attributes']>[string]>[num
1604
1660
  * is always tuple-form, so this branch is defensive only.
1605
1661
  */
1606
1662
  declare function mergeClassNameAllowlist(existing: ReadonlyArray<AttributeEntry> | undefined, extraClassNames: readonly string[]): AttributeEntry[];
1607
- /**
1608
- * The full sanitize schema used by the markdown renderer: extends
1609
- * `defaultSchema` to allow `<mark>`, the KaTeX math class names, and the
1610
- * three custom hast tags emitted by cross-chunk coordination handlers.
1611
- *
1612
- * **Owns its arrays and objects.** The shallow spread of `defaultSchema`
1613
- * alone would leave `attributes.a`, `attributes.img`, `protocols`,
1614
- * `ancestors`, and similar nested fields aliased to `rehype-sanitize`'s
1615
- * default singleton. A consumer who reasonably (but mistakenly) writes
1616
- * `sanitizeSchema.protocols.href.push('myapp')` would then poison
1617
- * `rehype-sanitize`'s `defaultSchema` for every other consumer in the
1618
- * process — a cross-package side-effect that's near-impossible to debug.
1619
- *
1620
- * One `cloneDeep` at module init breaks that aliasing without measurable
1621
- * cost (init-time only, single small object graph). The recommended
1622
- * extension API is still {@link extendSanitizeSchema}, which clones again
1623
- * per call; this layer just makes the exported singleton safe if someone
1624
- * skips the helper.
1625
- */
1626
1663
  declare const sanitizeSchema: Schema;
1627
1664
 
1628
1665
  /**
@@ -1902,6 +1939,20 @@ declare function preprocessLaTeX(str: string): string;
1902
1939
  */
1903
1940
  declare function createIncrementalLatexPreprocessor(options?: {
1904
1941
  freezeThreshold?: number;
1942
+ /** Failure backoff (default on). Tests that rely on `freezeThreshold: 0`
1943
+ * to attempt a freeze on EVERY call turn it off, so 1-char chunkings keep
1944
+ * exercising the cut rules instead of skipping most attempts; backoff
1945
+ * can only freeze LESS, so passing without it implies passing with it.
1946
+ * @internal */
1947
+ backoff?: boolean;
1948
+ /** Test hook: called once per freeze ATTEMPT (a scan of the active
1949
+ * region) with the region's length and how many source bytes the
1950
+ * attempt froze (0 = failed), so the backoff bound and freeze progress
1951
+ * can be pinned. @internal */
1952
+ onAttempt?: (info: {
1953
+ activeLength: number;
1954
+ frozenBytes: number;
1955
+ }) => void;
1905
1956
  }): (content: string) => string;
1906
1957
 
1907
1958
  /**
@@ -1970,4 +2021,4 @@ type RemendPreprocessorOptions = Omit<RemendOptions, 'katex' | 'inlineKatex'>;
1970
2021
  */
1971
2022
  declare function createRemendPreprocessor(options?: RemendPreprocessorOptions): AIMDContentPreprocessor;
1972
2023
 
1973
- export { type AIMDContentPreprocessor, type AIMarkdownEnginePlugin, type AIMarkdownEnginePluginName, type AdvanceOptions, type AdvanceResult, type AllowElement, type ChunkData, type Contribution, type CrossChunkHandlerOptions, DEFAULT_PAYLOAD, DEF_LINE_START_RE, type DefLabelScanner, type DefLabels, type Deprecation, type EnginePluginInternals, type EnginePluginStage, type ExtractContributionsOptions, type FootnoteDef, type FreezeBoundaryOptions, type IncrementalParseState, type IncrementalStage, type LinkDef, PIPELINE_STAGES, type ParsedMarkdown, type PhantomLabels, type PipelineOptions, type PipelineStage, type RefKind, type RefRecord, type Registry, type RegistryInternal, type RehypePlugins, type RehypeRebaseHashLinksOptions, type RemarkPlugins, type RemarkRehypeOptions, type RemendPreprocessorOptions, SENTINEL_FN_CONTENT, SENTINEL_LINK_URL, SMOOTH_STREAM_PACING_PRESETS, STAGE_MEASURE_PREFIX, type SanitizeSchema, type SmoothStreamController, type SmoothStreamOptions, type SmoothStreamPacing, type SmoothStreamPacingParams, type TransformContext, type UrlAttrKey, type UrlAttrTag, type UrlTransform, advanceIncrementalParse, attributeHastChildren, buildCoreRehypePlugins, buildCoreRemarkPlugins, buildCoreRemarkRehypeOptions, buildCrossChunkHandlers, buildPhantomSuffix, buildTransform, codePointSnapshots, collectDefLabels, computeFreezeBoundary, createDefLabelScanner, createFile, createIncrementalLatexPreprocessor, createProcessor, createRegistry, createRemendPreprocessor, createSmoothStreamController, defaultEnginePlugins, defaultUrlTransform, definitionList, extendSanitizeSchema, extractContributions, extractDefBodiesFromHast, footnoteSafeId, getEnginePluginInternals, hasLoneSurrogate, highlight, isFootnoteSection, lastRegionStart, measureStage, mergeClassNameAllowlist, normalizeForMatch, normalizeId, pangu, parseStage, phantomSuffixCloser, preprocessAIMDContent, preprocessLaTeX, rehypeFooterAdorn, rehypeRebaseHashLinks, removeComments, sanitizeCrossChunkUrl, sanitizeSchema, shortenDocumentId, smartypants, sourceIdFromFootnoteLiId, splitByProtectedRegions, subscribeStageTimings, transformStage, withDefs };
2024
+ export { type AIMDContentPreprocessor, type AIMarkdownEnginePlugin, type AIMarkdownEnginePluginName, type AdvanceOptions, type AdvanceResult, type AllowElement, type ChunkData, type Contribution, type CrossChunkHandlerOptions, DEFAULT_PAYLOAD, type DefLabelScanner, type DefLabels, type Deprecation, type EnginePluginInternals, type EnginePluginStage, type ExtractContributionsOptions, type FootnoteDef, type FreezeBoundaryOptions, type IncrementalParseState, type IncrementalStage, type LinkDef, PIPELINE_STAGES, type ParsedMarkdown, type PhantomLabels, type PipelineOptions, type PipelineStage, type RefKind, type RefRecord, type Registry, type RegistryInternal, type RehypePlugins, type RehypeRebaseHashLinksOptions, type RemarkPlugins, type RemarkRehypeOptions, type RemendPreprocessorOptions, SENTINEL_FN_CONTENT, SENTINEL_LINK_URL, SMOOTH_STREAM_PACING_PRESETS, STAGE_MEASURE_PREFIX, type SanitizeSchema, type SmoothStreamController, type SmoothStreamOptions, type SmoothStreamPacing, type SmoothStreamPacingParams, type TransformContext, type UrlAttrKey, type UrlAttrTag, type UrlTransform, advanceIncrementalParse, attributeHastChildren, buildCoreRehypePlugins, buildCoreRemarkPlugins, buildCoreRemarkRehypeOptions, buildCrossChunkHandlers, buildPhantomSuffix, buildTransform, codePointSnapshots, collectDefLabels, computeFreezeBoundary, createDefLabelScanner, createFile, createIncrementalLatexPreprocessor, createProcessor, createRegistry, createRemendPreprocessor, createSmoothStreamController, defaultEnginePlugins, defaultUrlTransform, definitionList, extendSanitizeSchema, extractContributions, extractDefBodiesFromHast, footnoteSafeId, getEnginePluginInternals, hasLoneSurrogate, highlight, isFootnoteSection, measureStage, mergeClassNameAllowlist, normalizeForMatch, normalizeId, pangu, parseStage, phantomSuffixCloser, preprocessAIMDContent, preprocessLaTeX, rehypeFooterAdorn, rehypeRebaseHashLinks, removeComments, sanitizeCrossChunkUrl, sanitizeSchema, shortenDocumentId, smartypants, sourceIdFromFootnoteLiId, splitByProtectedRegions, subscribeStageTimings, transformStage, withDefs };