@ai-react-markdown/engine 2.5.4 → 2.6.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
@@ -316,202 +316,15 @@ interface FreezeScanResult {
316
316
  * re-lexing the confirmed prefix. Single-consumer; see module docs. */
317
317
  checkpoint: FreezeScanCheckpoint;
318
318
  }
319
- interface Candidate {
320
- /** Freeze boundary: start of the line after this blank line. */
321
- offset: number;
322
- /** Consecutive confirmed blank lines (outside fences/math) ending here. */
323
- blankRun: number;
324
- /** No unbalanced HTML container / comment / raw block before this point. */
325
- htmlBalanced: boolean;
326
- /** Rolling continuation-hazard verdict at emission (blocker 3). */
327
- hazard: boolean;
328
- /** Blocker-6: the run ending at this blank left balanced FLOATING raw
329
- * remnant whose hast seam is tail-dependent; reject this candidate. */
330
- seamRisk: boolean;
331
- /** Blocker-4 settle verdict, decided by the NEXT confirmed line (`null`
332
- * while that line hasn't confirmed — only the newest candidate can be
333
- * pending). Storing the verdict instead of the line lets the checkpoint
334
- * drop its lines array, which retained a full copy of the document
335
- * (round-2 review: ~2-3× doc size per mounted instance). */
336
- defListSettled: boolean | null;
337
- }
338
- interface UnresolvedRef {
339
- offset: number;
340
- label: string;
341
- footnote: boolean;
342
- }
343
- /** Mutable resume state. All fields describe the scan strictly BEFORE the
344
- * first unconfirmed character (`confirmedOffset`). */
319
+ /** Opaque resume token. Produced by one `computeFreezeBoundary` call and
320
+ * passed back on the next APPEND-ONLY call; the only supported operations
321
+ * are storing it and passing it back. The field set is an implementation
322
+ * detail of the scanner and changes between minor versions — the brand key
323
+ * (a structural string literal, safe across dual d.ts entries) is all that
324
+ * ships in the public type. */
345
325
  interface FreezeScanCheckpoint {
346
- defListEnabled: boolean;
347
- /** Grammar-profile switches baked at creation — a checkpoint is only
348
- * resumable under the exact profile that built it. */
349
- mathFlow: boolean;
350
- referenceTaint: boolean;
351
- /** Start offset of the first line NOT yet baked into this checkpoint. */
352
- confirmedOffset: number;
353
- candidates: Candidate[];
354
- defs: Map<string, number>;
355
- footnoteDefs: Map<string, number>;
356
- unresolvedRefs: UnresolvedRef[];
357
- tagBalance: Map<string, number>;
358
- openTotal: number;
359
- commentOpen: boolean;
360
- piOpen: boolean;
361
- declOpen: boolean;
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;
373
- inFence: boolean;
374
- fenceChar: string;
375
- fenceLen: number;
376
- inMath: boolean;
377
- /** Opening dollar-run length while inMath — the close run must match it. */
378
- mathFenceLen: number;
379
- /** Indent (0-3 spaces) of the line that opened the current fence/math
380
- * block. Not a blocker input — read by `phantomSuffixCloser` to emit a
381
- * closer at the SAME indent, which closes the block whether it sits at
382
- * top level (≤3 spaces are allowed there) or inside a list item whose
383
- * content indent the opener line already satisfies. */
384
- openIndent: number;
385
- blankRun: number;
386
- lastBlankStart: number;
387
- /** Rolling blocker-3 verdict ("nearest decisive block start so far"). */
388
- hazardVerdict: boolean;
389
- /** Previous confirmed line was blank (block-start detection). */
390
- prevLineBlank: boolean;
391
- /** Previous confirmed line was a plain text line (def-chain detection). */
392
- prevLineWasText: boolean;
393
- /** Previous confirmed line registered a VALID definition (def chains). */
394
- prevLineWasValidDef: boolean;
395
- /** An earlier line of the current paragraph left an unpaired backtick
396
- * run — masking is disabled until the paragraph ends (safety gate). */
397
- /** A `[` left unclosed at the end of a paragraph line (code spans
398
- * masked). micromark lets a reference label span soft line breaks, so
399
- * the label may close on a LATER line where the per-line REF_RE never
400
- * sees a `[…]` pair (v2.4.1 review P1: `see [foo\nbar] end` + a late
401
- * `[foo bar]: /u` retargeted frozen output). Cleared wherever the
402
- * paragraph ends. */
403
- openBracket: {
404
- offset: number;
405
- text: string;
406
- } | null;
407
- paragraphHasUnpairedRun: boolean;
408
- /** Blocker-6 pending flag: a confirmed html-flow line left balanced
409
- * floating raw remnant, and no later content line has pinned the seam
410
- * yet. Persists across blank lines (every candidate emitted while set
411
- * has the remnant as its last frozen child); cleared by the next
412
- * non-blank line that starts OUTSIDE an html-flow run. */
413
- htmlSeamPending: boolean;
414
- /** A line since the last blank started with `<` at block indent — an html
415
- * FLOW block is (approximately) running, and it only ends at a blank
416
- * line. micromark does no inline parsing there: backtick runs are
417
- * literal text, so code-span masking would hide REAL tags from the
418
- * balance scan (under-block — fuzz counterexample: `</details>` followed
419
- * by an unblanked `` `<div>` `` line). While set, masking is skipped —
420
- * which can only over-block (safe direction). */
421
- htmlFlowSinceBlank: boolean;
422
- /** Offset of the first fence/math OPEN suppressed by `htmlFlowSinceBlank`
423
- * (Infinity = none). Whether the run really swallowed that line depends
424
- * on container context the line scan cannot see (`<embed` inside a list
425
- * item is a lazy paragraph line, and the glued `$$` a REAL math open —
426
- * seed-20260757 under-block: the tracker's fence phase INVERTS from that
427
- * line on, every later close reads as an open, and the corruption never
428
- * resyncs). Candidates past this offset are rejected outright — sticky,
429
- * pure over-block; candidates before it are untouched (the ambiguous
430
- * region then re-parses inside the tail). The rolling hazard poison for
431
- * ambiguous tag names stays, but it decays at the next decisive block
432
- * start — this field is the phase-corruption backstop that does not. */
433
- phasePoisonedAt: number;
434
- /** Tag names of line-truncated opens (`<div` at EOL) counted into
435
- * tagBalance but not yet confirmed by a later `>` — reverted at the next
436
- * blank line (a tag cannot span one). See TRUNCATED_TAG_RE. */
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;
485
- /** The run since the last blank was OPENED by a CommonMark type-1 block
486
- * (`<script` / `<pre` / `<style` / `<textarea`). Type 1 is the one block
487
- * that ends at its CLOSER line rather than at a blank, so `htmlFlowReal`
488
- * being "sticky to the blank" over-ran it: the line after `</script>` is
489
- * a fresh PARAGRAPH to micromark, but the scanner still treated it as
490
- * raw html flow — and in a real run end tags may carry attributes, so a
491
- * `</div a="b">` there was counted as a REAL close. `p <div> x </div
492
- * a="b"> y` after a `</script>` line therefore froze past an element the
493
- * actual parse leaves OPEN, nesting the whole rest of the document
494
- * inside it (2026-08-20 soak leg 2; identical for pre/style/textarea,
495
- * and correct as-is for a type-6 run, which really does reach the blank).
496
- * Only set when the type-1 opener is the line that STARTED the run — a
497
- * `<script>` nested inside an open type-6 block must not end it. */
498
- type1FlowOpen: boolean;
499
- /** The currently open raw-text element (`rawTextOpen`) was opened INLINE —
500
- * in paragraph context rather than in a real html-flow run. */
501
- rawTextInline: boolean;
502
- /** parse5 is in "script data escaped": a `<!--` has been seen inside the
503
- * open `<script>`. Only meaningful while `rawTextOpen === 'script'`.
504
- * Deliberately NOT cleared by `-->` — staying escaped can only poison
505
- * more often, which is the over-blocking side. */
506
- scriptDataEscaped: boolean;
326
+ readonly '~freezeScanCheckpoint'?: never;
507
327
  }
508
- /** parse5 tokenizer position inside a tag's attribute area, tracked across
509
- * line endings for `tagAcrossLines` (2026-08-19 review r2 P1-2 / P2-3):
510
- * `outside` = before/in an attribute name (a `"` here starts a NAME, not a
511
- * value); `afterEq` = just past `=`; `unquoted` = in an unquoted value
512
- * (whitespace ends it); `"` / `'` = inside a quoted value — `>` and line
513
- * endings are value bytes there, only the matching quote leaves it. */
514
- type TagAttrState = 'outside' | 'afterEq' | 'unquoted' | '"' | "'";
515
328
  declare function computeFreezeBoundary(text: string, options: FreezeBoundaryOptions, resume?: FreezeScanCheckpoint | null): FreezeScanResult;
516
329
 
517
330
  /**
package/dist/index.d.ts CHANGED
@@ -316,202 +316,15 @@ interface FreezeScanResult {
316
316
  * re-lexing the confirmed prefix. Single-consumer; see module docs. */
317
317
  checkpoint: FreezeScanCheckpoint;
318
318
  }
319
- interface Candidate {
320
- /** Freeze boundary: start of the line after this blank line. */
321
- offset: number;
322
- /** Consecutive confirmed blank lines (outside fences/math) ending here. */
323
- blankRun: number;
324
- /** No unbalanced HTML container / comment / raw block before this point. */
325
- htmlBalanced: boolean;
326
- /** Rolling continuation-hazard verdict at emission (blocker 3). */
327
- hazard: boolean;
328
- /** Blocker-6: the run ending at this blank left balanced FLOATING raw
329
- * remnant whose hast seam is tail-dependent; reject this candidate. */
330
- seamRisk: boolean;
331
- /** Blocker-4 settle verdict, decided by the NEXT confirmed line (`null`
332
- * while that line hasn't confirmed — only the newest candidate can be
333
- * pending). Storing the verdict instead of the line lets the checkpoint
334
- * drop its lines array, which retained a full copy of the document
335
- * (round-2 review: ~2-3× doc size per mounted instance). */
336
- defListSettled: boolean | null;
337
- }
338
- interface UnresolvedRef {
339
- offset: number;
340
- label: string;
341
- footnote: boolean;
342
- }
343
- /** Mutable resume state. All fields describe the scan strictly BEFORE the
344
- * first unconfirmed character (`confirmedOffset`). */
319
+ /** Opaque resume token. Produced by one `computeFreezeBoundary` call and
320
+ * passed back on the next APPEND-ONLY call; the only supported operations
321
+ * are storing it and passing it back. The field set is an implementation
322
+ * detail of the scanner and changes between minor versions — the brand key
323
+ * (a structural string literal, safe across dual d.ts entries) is all that
324
+ * ships in the public type. */
345
325
  interface FreezeScanCheckpoint {
346
- defListEnabled: boolean;
347
- /** Grammar-profile switches baked at creation — a checkpoint is only
348
- * resumable under the exact profile that built it. */
349
- mathFlow: boolean;
350
- referenceTaint: boolean;
351
- /** Start offset of the first line NOT yet baked into this checkpoint. */
352
- confirmedOffset: number;
353
- candidates: Candidate[];
354
- defs: Map<string, number>;
355
- footnoteDefs: Map<string, number>;
356
- unresolvedRefs: UnresolvedRef[];
357
- tagBalance: Map<string, number>;
358
- openTotal: number;
359
- commentOpen: boolean;
360
- piOpen: boolean;
361
- declOpen: boolean;
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;
373
- inFence: boolean;
374
- fenceChar: string;
375
- fenceLen: number;
376
- inMath: boolean;
377
- /** Opening dollar-run length while inMath — the close run must match it. */
378
- mathFenceLen: number;
379
- /** Indent (0-3 spaces) of the line that opened the current fence/math
380
- * block. Not a blocker input — read by `phantomSuffixCloser` to emit a
381
- * closer at the SAME indent, which closes the block whether it sits at
382
- * top level (≤3 spaces are allowed there) or inside a list item whose
383
- * content indent the opener line already satisfies. */
384
- openIndent: number;
385
- blankRun: number;
386
- lastBlankStart: number;
387
- /** Rolling blocker-3 verdict ("nearest decisive block start so far"). */
388
- hazardVerdict: boolean;
389
- /** Previous confirmed line was blank (block-start detection). */
390
- prevLineBlank: boolean;
391
- /** Previous confirmed line was a plain text line (def-chain detection). */
392
- prevLineWasText: boolean;
393
- /** Previous confirmed line registered a VALID definition (def chains). */
394
- prevLineWasValidDef: boolean;
395
- /** An earlier line of the current paragraph left an unpaired backtick
396
- * run — masking is disabled until the paragraph ends (safety gate). */
397
- /** A `[` left unclosed at the end of a paragraph line (code spans
398
- * masked). micromark lets a reference label span soft line breaks, so
399
- * the label may close on a LATER line where the per-line REF_RE never
400
- * sees a `[…]` pair (v2.4.1 review P1: `see [foo\nbar] end` + a late
401
- * `[foo bar]: /u` retargeted frozen output). Cleared wherever the
402
- * paragraph ends. */
403
- openBracket: {
404
- offset: number;
405
- text: string;
406
- } | null;
407
- paragraphHasUnpairedRun: boolean;
408
- /** Blocker-6 pending flag: a confirmed html-flow line left balanced
409
- * floating raw remnant, and no later content line has pinned the seam
410
- * yet. Persists across blank lines (every candidate emitted while set
411
- * has the remnant as its last frozen child); cleared by the next
412
- * non-blank line that starts OUTSIDE an html-flow run. */
413
- htmlSeamPending: boolean;
414
- /** A line since the last blank started with `<` at block indent — an html
415
- * FLOW block is (approximately) running, and it only ends at a blank
416
- * line. micromark does no inline parsing there: backtick runs are
417
- * literal text, so code-span masking would hide REAL tags from the
418
- * balance scan (under-block — fuzz counterexample: `</details>` followed
419
- * by an unblanked `` `<div>` `` line). While set, masking is skipped —
420
- * which can only over-block (safe direction). */
421
- htmlFlowSinceBlank: boolean;
422
- /** Offset of the first fence/math OPEN suppressed by `htmlFlowSinceBlank`
423
- * (Infinity = none). Whether the run really swallowed that line depends
424
- * on container context the line scan cannot see (`<embed` inside a list
425
- * item is a lazy paragraph line, and the glued `$$` a REAL math open —
426
- * seed-20260757 under-block: the tracker's fence phase INVERTS from that
427
- * line on, every later close reads as an open, and the corruption never
428
- * resyncs). Candidates past this offset are rejected outright — sticky,
429
- * pure over-block; candidates before it are untouched (the ambiguous
430
- * region then re-parses inside the tail). The rolling hazard poison for
431
- * ambiguous tag names stays, but it decays at the next decisive block
432
- * start — this field is the phase-corruption backstop that does not. */
433
- phasePoisonedAt: number;
434
- /** Tag names of line-truncated opens (`<div` at EOL) counted into
435
- * tagBalance but not yet confirmed by a later `>` — reverted at the next
436
- * blank line (a tag cannot span one). See TRUNCATED_TAG_RE. */
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;
485
- /** The run since the last blank was OPENED by a CommonMark type-1 block
486
- * (`<script` / `<pre` / `<style` / `<textarea`). Type 1 is the one block
487
- * that ends at its CLOSER line rather than at a blank, so `htmlFlowReal`
488
- * being "sticky to the blank" over-ran it: the line after `</script>` is
489
- * a fresh PARAGRAPH to micromark, but the scanner still treated it as
490
- * raw html flow — and in a real run end tags may carry attributes, so a
491
- * `</div a="b">` there was counted as a REAL close. `p <div> x </div
492
- * a="b"> y` after a `</script>` line therefore froze past an element the
493
- * actual parse leaves OPEN, nesting the whole rest of the document
494
- * inside it (2026-08-20 soak leg 2; identical for pre/style/textarea,
495
- * and correct as-is for a type-6 run, which really does reach the blank).
496
- * Only set when the type-1 opener is the line that STARTED the run — a
497
- * `<script>` nested inside an open type-6 block must not end it. */
498
- type1FlowOpen: boolean;
499
- /** The currently open raw-text element (`rawTextOpen`) was opened INLINE —
500
- * in paragraph context rather than in a real html-flow run. */
501
- rawTextInline: boolean;
502
- /** parse5 is in "script data escaped": a `<!--` has been seen inside the
503
- * open `<script>`. Only meaningful while `rawTextOpen === 'script'`.
504
- * Deliberately NOT cleared by `-->` — staying escaped can only poison
505
- * more often, which is the over-blocking side. */
506
- scriptDataEscaped: boolean;
326
+ readonly '~freezeScanCheckpoint'?: never;
507
327
  }
508
- /** parse5 tokenizer position inside a tag's attribute area, tracked across
509
- * line endings for `tagAcrossLines` (2026-08-19 review r2 P1-2 / P2-3):
510
- * `outside` = before/in an attribute name (a `"` here starts a NAME, not a
511
- * value); `afterEq` = just past `=`; `unquoted` = in an unquoted value
512
- * (whitespace ends it); `"` / `'` = inside a quoted value — `>` and line
513
- * endings are value bytes there, only the matching quote leaves it. */
514
- type TagAttrState = 'outside' | 'afterEq' | 'unquoted' | '"' | "'";
515
328
  declare function computeFreezeBoundary(text: string, options: FreezeBoundaryOptions, resume?: FreezeScanCheckpoint | null): FreezeScanResult;
516
329
 
517
330
  /**
@@ -337,6 +337,26 @@ var HTML_BREAKOUT_TAGS = /* @__PURE__ */ new Set([
337
337
  "var"
338
338
  ]);
339
339
  var HTML_INTEGRATION_POINTS = ["foreignobject", "desc", "title", "mi", "mo", "mn", "ms", "mtext", "annotation-xml"];
340
+ var SCOPE_BARRIER_NAMES = /* @__PURE__ */ new Set([
341
+ "applet",
342
+ "caption",
343
+ "html",
344
+ "table",
345
+ "td",
346
+ "th",
347
+ "marquee",
348
+ "object",
349
+ "template",
350
+ "mi",
351
+ "mo",
352
+ "mn",
353
+ "ms",
354
+ "mtext",
355
+ "annotation-xml",
356
+ "foreignobject",
357
+ "desc",
358
+ "title"
359
+ ]);
340
360
  var FOREIGN_ROOT_NAMES = ["svg", "math"];
341
361
  var TYPE1_NAMES = /* @__PURE__ */ new Set(["script", "pre", "style", "textarea"]);
342
362
  var RAW_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
@@ -507,6 +527,7 @@ function freshCheckpoint(defListEnabled, mathFlow, referenceTaint) {
507
527
  piOpen: false,
508
528
  bogusOpen: false,
509
529
  rawTextOpen: null,
530
+ openStack: [],
510
531
  scriptDataEscaped: false,
511
532
  declOpen: false,
512
533
  cdataOpen: false,
@@ -630,7 +651,8 @@ function classifyBlockStart(text, indent, defListEnabled) {
630
651
  function computeFreezeBoundary(text, options, resume) {
631
652
  const mathFlow = options.mathFlow ?? true;
632
653
  const referenceTaint = options.referenceTaint ?? true;
633
- const cp = resume && resume.defListEnabled === options.defListEnabled && resume.mathFlow === mathFlow && resume.referenceTaint === referenceTaint && resume.confirmedOffset <= text.length ? resume : freshCheckpoint(options.defListEnabled, mathFlow, referenceTaint);
654
+ const prev = resume;
655
+ const cp = prev && prev.defListEnabled === options.defListEnabled && prev.mathFlow === mathFlow && prev.referenceTaint === referenceTaint && prev.confirmedOffset <= text.length ? prev : freshCheckpoint(options.defListEnabled, mathFlow, referenceTaint);
634
656
  let start = cp.confirmedOffset;
635
657
  let tailLine = null;
636
658
  while (start < text.length) {
@@ -693,6 +715,14 @@ function computeFreezeBoundary(text, options, resume) {
693
715
  }
694
716
  return { boundary, checkpoint: cp };
695
717
  }
718
+ function pendingFenceCloser(checkpoint) {
719
+ const cp = checkpoint;
720
+ if (cp.phasePoisonedAt !== Infinity) return "";
721
+ if (cp.openIndent !== 0) return "";
722
+ if (cp.inFence) return cp.fenceChar.repeat(cp.fenceLen);
723
+ if (cp.inMath) return "$".repeat(cp.mathFenceLen);
724
+ return "";
725
+ }
696
726
  function floatingResidue(text, commentOpenAtStart) {
697
727
  let out = "";
698
728
  let open = commentOpenAtStart;
@@ -768,6 +798,7 @@ function processConfirmedLine(cp, ln, text) {
768
798
  cp.openTotal -= count;
769
799
  }
770
800
  }
801
+ if (cp.openStack.length > 0) cp.openStack = cp.openStack.filter((n) => !FOREIGN_ROOT_NAMES.includes(n));
771
802
  };
772
803
  const noteBreakout = (tag, closing) => {
773
804
  if (closing || cp.rawTextOpen !== null) return;
@@ -791,13 +822,25 @@ function processConfirmedLine(cp, ln, text) {
791
822
  }
792
823
  }
793
824
  if (DOCUMENT_STRUCTURE_NAMES.has(tag)) cp.phasePoisonedAt = 0;
825
+ if (tag === "template" && !closing) cp.phasePoisonedAt = 0;
794
826
  if (closing) {
827
+ let idx = -1;
828
+ for (let i = cp.openStack.length - 1; i >= 0; i--) {
829
+ if (cp.openStack[i] === tag) {
830
+ idx = i;
831
+ break;
832
+ }
833
+ if (SCOPE_BARRIER_NAMES.has(cp.openStack[i])) break;
834
+ }
835
+ if (idx === -1) return;
836
+ cp.openStack.splice(idx, 1);
795
837
  const count = cp.tagBalance.get(tag) ?? 0;
796
838
  if (count > 0) {
797
839
  cp.tagBalance.set(tag, count - 1);
798
840
  cp.openTotal -= 1;
799
841
  }
800
842
  } else {
843
+ cp.openStack.push(tag);
801
844
  cp.tagBalance.set(tag, (cp.tagBalance.get(tag) ?? 0) + 1);
802
845
  cp.openTotal += 1;
803
846
  }
@@ -916,6 +959,9 @@ function processConfirmedLine(cp, ln, text) {
916
959
  if (!cp.type1FlowOpen) {
917
960
  cp.htmlFlowSinceBlank = false;
918
961
  cp.htmlFlowReal = false;
962
+ if (cp.rawTextOpen !== null && !cp.rawTextInline) {
963
+ cp.phasePoisonedAt = 0;
964
+ }
919
965
  }
920
966
  cp.prevLineBlank = true;
921
967
  cp.prevLineWasText = false;
@@ -1015,6 +1061,7 @@ ${cont(scanText)}` };
1015
1061
  const poisonRawDivergence = () => {
1016
1062
  cp.phasePoisonedAt = Math.min(cp.phasePoisonedAt, ln.start);
1017
1063
  };
1064
+ let inlineRawOpenerIdx = -1;
1018
1065
  while (pos < scanText.length) {
1019
1066
  if (cp.piOpen) {
1020
1067
  const c = scanText.indexOf("?>", pos);
@@ -1070,6 +1117,7 @@ ${cont(scanText)}` };
1070
1117
  } else if (first === cd) {
1071
1118
  rawSpans.push([cd, cd + 9]);
1072
1119
  cp.cdataOpen = true;
1120
+ if (!isMdBlank(scanText.slice(0, cd)) || ln.indent > 3) inlineRawOpenerIdx = cd;
1073
1121
  pos = cd + 9;
1074
1122
  } else if (first === pi) {
1075
1123
  if (scanText[pi + 2] === ">") {
@@ -1080,14 +1128,19 @@ ${cont(scanText)}` };
1080
1128
  }
1081
1129
  rawSpans.push([pi, pi + 2]);
1082
1130
  cp.piOpen = true;
1131
+ if (!isMdBlank(scanText.slice(0, pi)) || ln.indent > 3) inlineRawOpenerIdx = pi;
1083
1132
  pos = pi + 2;
1084
1133
  } else {
1085
1134
  rawSpans.push([decl, decl + 2]);
1086
1135
  if (ln.indent <= 3 && /^doctype/i.test(scanText.slice(decl + 2))) cp.phasePoisonedAt = 0;
1087
1136
  cp.declOpen = true;
1137
+ if (!isMdBlank(scanText.slice(0, decl)) || ln.indent > 3) inlineRawOpenerIdx = decl;
1088
1138
  pos = decl + 2;
1089
1139
  }
1090
1140
  }
1141
+ if (inlineRawOpenerIdx !== -1 && (cp.piOpen || cp.declOpen || cp.cdataOpen)) {
1142
+ cp.phasePoisonedAt = 0;
1143
+ }
1091
1144
  let tagText = scanText;
1092
1145
  for (const [from, to] of rawSpans) {
1093
1146
  tagText = tagText.slice(0, from) + " ".repeat(to - from) + tagText.slice(to);
@@ -1176,9 +1229,9 @@ ${cont(scanText)}` };
1176
1229
  if (VOID_TAGS.has(tag) || selfClosing && honoursSelfClosing(tag)) continue;
1177
1230
  applyTag(tag, closing);
1178
1231
  }
1179
- if (cp.commentOpen && lastCommentOpenerIdx !== -1 && !inRawText) {
1180
- if (!isMdBlank(tagText.slice(0, lastCommentOpenerIdx))) {
1181
- cp.phasePoisonedAt = Math.min(cp.phasePoisonedAt, ln.start + lastCommentOpenerIdx);
1232
+ if (cp.commentOpen && lastCommentOpenerIdx !== -1) {
1233
+ if (!isMdBlank(tagText.slice(0, lastCommentOpenerIdx)) || ln.indent > 3) {
1234
+ cp.phasePoisonedAt = 0;
1182
1235
  }
1183
1236
  }
1184
1237
  if (masked !== null && masked !== ln.text) {
@@ -1478,6 +1531,40 @@ function hasStrayTablePart(values) {
1478
1531
  }
1479
1532
  return false;
1480
1533
  }
1534
+ var HEAD_ROUTED_NAMES = /* @__PURE__ */ new Set([
1535
+ "base",
1536
+ "basefont",
1537
+ "bgsound",
1538
+ "link",
1539
+ "meta",
1540
+ "noframes",
1541
+ "script",
1542
+ "style",
1543
+ "template",
1544
+ "title"
1545
+ ]);
1546
+ var HEAD_ROUTED_RAW_TEXT_RE = /<(script|style|title|noframes)(?=[\s/>])/i;
1547
+ var ANY_START_TAG_RE = /<([a-z][a-z0-9-]*)(?=[\s/>])/gi;
1548
+ function headRoutedCaptureUnclosed(values) {
1549
+ if (values.length === 0) return false;
1550
+ const joined = values.join("\n");
1551
+ const opener = HEAD_ROUTED_RAW_TEXT_RE.exec(joined);
1552
+ if (opener === null) return false;
1553
+ const before = joined.slice(0, opener.index);
1554
+ if (/<[!?]/.test(before)) return true;
1555
+ ANY_START_TAG_RE.lastIndex = 0;
1556
+ let m;
1557
+ while ((m = ANY_START_TAG_RE.exec(before)) !== null) {
1558
+ if (!HEAD_ROUTED_NAMES.has(m[1].toLowerCase())) return false;
1559
+ }
1560
+ const name = opener[1].toLowerCase();
1561
+ const after = joined.slice(opener.index + opener[0].length);
1562
+ if (name === "script") {
1563
+ const close = after.search(/<\/script(?=[\s/>])/i);
1564
+ return close === -1 || /<!--/.test(after.slice(0, close));
1565
+ }
1566
+ return !new RegExp(`</${name}(?=[\\s/>])`, "i").test(after);
1567
+ }
1481
1568
  var STRAY_SYNTHESIZED_END_TAG_RE = /<\/(?:br|p)\b/i;
1482
1569
  function spliceTrees(input) {
1483
1570
  const { prevMdast, prevHast, tailMdast, tailHast, content, boundary, injectionPrefix, injectedSegments } = input;
@@ -1536,11 +1623,14 @@ function spliceTrees(input) {
1536
1623
  });
1537
1624
  const tailWrapVisible = tailMdastChildren.some((child) => !isWrapInvisible(child));
1538
1625
  if (hasStrayTablePart(prefixMdast.flatMap((c) => c.type === "html" ? [c.value] : []))) return null;
1626
+ const leadingHtml = [];
1539
1627
  for (const child of tailMdastChildren) {
1540
1628
  if (isWrapInvisible(child)) continue;
1541
1629
  if (child.type !== "html") break;
1542
1630
  if (STRAY_SYNTHESIZED_END_TAG_RE.test(child.value) || TABLE_PART_TAG_RE.test(child.value)) return null;
1631
+ leadingHtml.push(child.value);
1543
1632
  }
1633
+ if (headRoutedCaptureUnclosed(leadingHtml)) return null;
1544
1634
  const aligned = alignPrefixCut(prefixMdast, cutRegion, tailWrapVisible);
1545
1635
  if (aligned === null) return null;
1546
1636
  const hastChildren = aligned.children;
@@ -2184,12 +2274,10 @@ function phantomSuffixCloser(content) {
2184
2274
  const endsWithNewline = content.endsWith("\n");
2185
2275
  const confirmed = endsWithNewline ? content : content + "\n";
2186
2276
  const { checkpoint } = computeFreezeBoundary(confirmed, { defListEnabled: false, referenceTaint: false });
2187
- if (checkpoint.phasePoisonedAt !== Infinity) return "";
2188
- if (checkpoint.openIndent !== 0) return "";
2189
- const nl = endsWithNewline ? "" : "\n";
2190
- if (checkpoint.inFence) return `${nl}${checkpoint.fenceChar.repeat(checkpoint.fenceLen)}`;
2191
- if (checkpoint.inMath) return `${nl}${"$".repeat(checkpoint.mathFenceLen)}`;
2192
- return "";
2277
+ const closer = pendingFenceCloser(checkpoint);
2278
+ if (closer === "") return "";
2279
+ return endsWithNewline ? closer : `
2280
+ ${closer}`;
2193
2281
  }
2194
2282
 
2195
2283
  // src/components/extractContributions.ts
@@ -2465,7 +2553,7 @@ function createRegistry(onEmpty) {
2465
2553
 
2466
2554
  // src/components/pluginChain.ts
2467
2555
  var import_rehype_katex = __toESM(require("rehype-katex"), 1);
2468
- var import_rehype_raw = __toESM(require("rehype-raw"), 1);
2556
+ var import_rehype_raw = __toESM(require("@ai-markdown/rehype-raw"), 1);
2469
2557
  var import_rehype_unwrap_images = __toESM(require("rehype-unwrap-images"), 1);
2470
2558
 
2471
2559
  // src/components/rehypeUnwrapCrossChunkImages.ts