@ai-react-markdown/engine 2.4.1 → 2.4.3

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
@@ -384,6 +384,16 @@ interface FreezeScanCheckpoint {
384
384
  prevLineWasValidDef: boolean;
385
385
  /** An earlier line of the current paragraph left an unpaired backtick
386
386
  * run — masking is disabled until the paragraph ends (safety gate). */
387
+ /** A `[` left unclosed at the end of a paragraph line (code spans
388
+ * masked). micromark lets a reference label span soft line breaks, so
389
+ * the label may close on a LATER line where the per-line REF_RE never
390
+ * sees a `[…]` pair (v2.4.1 review P1: `see [foo\nbar] end` + a late
391
+ * `[foo bar]: /u` retargeted frozen output). Cleared wherever the
392
+ * paragraph ends. */
393
+ openBracket: {
394
+ offset: number;
395
+ text: string;
396
+ } | null;
387
397
  paragraphHasUnpairedRun: boolean;
388
398
  /** Blocker-6 pending flag: a confirmed html-flow line left balanced
389
399
  * floating raw remnant, and no later content line has pinned the seam
@@ -754,34 +764,6 @@ interface ExtractContributionsOptions {
754
764
  * Defs matching these are skipped to avoid leaking sentinel rows into
755
765
  * registry.chunkData. */
756
766
  phantomFootnoteLabels?: Set<string>;
757
- /**
758
- * Caller's resolved URL transform (typically `props.urlTransform ??
759
- * defaultUrlTransform`). Applied to every emitted `linkDef.url` so the
760
- * registry stores already-sanitized URLs.
761
- *
762
- * Cross-chunk link/image references render through the registry rather
763
- * than the in-tree hast (which is where react-markdown's transform pass
764
- * normally enforces `urlTransform`). Without this, a chunk defining
765
- * `[evil]: javascript:alert(1)` could XSS a sibling chunk that uses
766
- * `[click][evil]` — the standalone path strips the protocol; the cross-
767
- * chunk path would have rendered `<a href="javascript:…">`. Sanitizing at
768
- * contribute time also benefits any future consumer that reads
769
- * `Registry.resolveLinkDef` directly.
770
- *
771
- * Invocation contract mirrors react-markdown's hast-pass call site
772
- * (`buildTransform` in `./markdown/transform.ts`): a synthetic
773
- * `<a href={url}>` element stands in for the node argument since
774
- * mdast `definition` nodes have no hast counterpart. The key is `'href'`
775
- * — link defs are far more common than image defs, and protocol-allowlist
776
- * transforms (including `defaultUrlTransform`) are key-agnostic anyway.
777
- * A `null` return collapses to the empty string, matching how
778
- * `transform.ts` would render a blocked attribute.
779
- *
780
- * Omitting this option preserves v1 behavior (URLs stored raw). Library
781
- * callers should always supply it; the option stays optional so unit-test
782
- * fixtures that don't care about URL safety can construct minimal calls.
783
- */
784
- urlTransform?: UrlTransform;
785
767
  }
786
768
  declare function extractContributions(mdast: Root, options?: ExtractContributionsOptions): Generator<Contribution>;
787
769
 
@@ -850,10 +832,12 @@ interface ChunkData {
850
832
  * interface. Driving the registry directly is reserved for internal
851
833
  * coordinators (the package's own `MarkdownContent` renderer) and tests,
852
834
  * which import the wider `RegistryInternal` type from this module.
853
- * `RegistryInternal` is exported here but NOT re-exported from the package
854
- * barrel keeping mutators off the public surface prevents a misbehaving
855
- * consumer-component from corrupting refcounts, skipping version bumps, or
856
- * otherwise breaking the invariants the renderer relies on.
835
+ * `RegistryInternal` is exported from this engine package (the core
836
+ * renderer consumes it) but NOT from `@ai-react-markdown/core`'s public
837
+ * barrel keeping mutators off the consumer-facing surface prevents a
838
+ * misbehaving consumer-component from corrupting refcounts, skipping
839
+ * version bumps, or otherwise breaking the invariants the renderer relies
840
+ * on.
857
841
  */
858
842
  interface Registry {
859
843
  /** Chunk mount-order Symbol list. **Read-only.** Direct mutation
@@ -878,18 +862,15 @@ interface Registry {
878
862
  canonicalLinkFor(label: string): symbol | null;
879
863
  globalNumber(label: string): number | null;
880
864
  /**
881
- * Resolve a cross-chunk link definition by label. The returned `url` is the
882
- * value the contributing chunk's `urlTransform` produced cross-chunk
883
- * link/image references run a second, per-attribute sanitization pass
884
- * (`urlTransform` + `sanitizeSchema.protocols`) at render time, so the
885
- * placeholder components themselves never trust this value blindly.
865
+ * Resolve a cross-chunk link definition by label. The returned `url` is
866
+ * the RAW destination from the contributing chunk's sourcethe registry
867
+ * does not sanitize. The library's cross-chunk link/image placeholders run
868
+ * the full per-attribute sanitization (`sanitizeSchema.protocols` +
869
+ * `urlTransform`, correct key) at render time.
886
870
  *
887
871
  * Consumers reading `def.url` directly (custom backlink panels, analytics,
888
- * dev tooling) receive a defense-in-depth-filtered string but should still
889
- * pipe it through their own `urlTransform` if they intend to render it as
890
- * an `href`/`src` — the contribute-time pass uses the `'href'` key and a
891
- * synthetic `<a>` node, so a key-aware policy may treat the value
892
- * differently when used as an `<img src>`.
872
+ * dev tooling) MUST run their own policy before rendering it as an
873
+ * `href`/`src` a chunk can define `[evil]: javascript:alert(1)`.
893
874
  */
894
875
  resolveLinkDef(label: string): LinkDef | null;
895
876
  getRefsForLabel(label: string): number;
@@ -906,9 +887,10 @@ interface Registry {
906
887
  * methods and implementation-private fields (reactId-keyed refcount table,
907
888
  * subscriber set, microtask-coalesce flag, `_notify` itself).
908
889
  *
909
- * Exported from this module so internal coordinators (`MarkdownContent`)
910
- * and tests can hold a strongly-typed reference, but **not** re-exported
911
- * from the package barrel a consumer flipping `_notifyScheduled = true`
890
+ * Exported from this module (and the engine barrel) so internal
891
+ * coordinators (`MarkdownContent`) and tests can hold a strongly-typed
892
+ * reference, but **not** re-exported from `@ai-react-markdown/core`'s
893
+ * barrel — a consumer flipping `_notifyScheduled = true`
912
894
  * or pushing into `chunkOrder` directly would silently break the
913
895
  * coalesce / numbering invariants. The runtime value returned by
914
896
  * {@link createRegistry} always satisfies this wider shape; public consumers
@@ -1319,11 +1301,13 @@ declare function buildCrossChunkHandlers(): Handlers;
1319
1301
  * In the standalone path, every `<a href>` and `<img src>` element passes
1320
1302
  * through TWO gates before render:
1321
1303
  *
1322
- * 1. `urlTransform(url, key, node)` — the caller's allowlist (default:
1323
- * `defaultUrlTransform`, which mirrors GitHub's protocol allowlist).
1324
- * Runs in `markdown/transform.ts` during the hast visit pass.
1325
- * 2. `rehype-sanitize` `protocols.<attr>` allowlist — drops the attribute
1304
+ * 1. `rehype-sanitize` `protocols.<attr>` allowlist drops the attribute
1326
1305
  * entirely if the URL's protocol isn't permitted for that attribute.
1306
+ * Runs in the rehype plugin chain.
1307
+ * 2. `urlTransform(url, key, node)` — the caller's allowlist (default:
1308
+ * `defaultUrlTransform`, which mirrors GitHub's protocol allowlist).
1309
+ * Runs in `markdown/transform.ts` during the hast visit pass; its
1310
+ * return value IS the attribute (`''` renders `href=""`).
1327
1311
  *
1328
1312
  * Cross-chunk references skip BOTH passes naturally: the placeholder hast
1329
1313
  * tag (`<cross-chunk-link>` / `<cross-chunk-image>`) carries only a `label`
@@ -1353,13 +1337,20 @@ declare function buildCrossChunkHandlers(): Handlers;
1353
1337
  type UrlAttrKey = 'href' | 'src';
1354
1338
  type UrlAttrTag = 'a' | 'img';
1355
1339
  /**
1356
- * Run a cross-chunk-resolved URL through both standalone gates.
1340
+ * Run a cross-chunk-resolved URL through both standalone gates, in the
1341
+ * standalone ORDER: `rehype-sanitize`'s protocol allowlist runs in the
1342
+ * plugin chain first (a blocked URL loses the attribute entirely), then
1343
+ * `urlTransform` rewrites whatever survived during the hast visit pass (its
1344
+ * return value is the attribute — `''` from `defaultUrlTransform` renders
1345
+ * `href=""`, exactly like a legal empty destination `[x]: <>`).
1357
1346
  *
1358
- * @returns Sanitized URL string. Empty string when either gate strips the
1359
- * URL — matches `defaultUrlTransform`'s and `rehype-sanitize`'s observable
1360
- * behavior of replacing a blocked URL with `''`.
1347
+ * @returns `null` when the protocol gate strips the attribute (render it
1348
+ * ABSENT); otherwise the transformed URL string possibly `''`, which
1349
+ * the caller must render as `href=""` / `src=""` to stay byte-identical
1350
+ * with standalone (v2.4.1 review: the two used to collapse into `''` and
1351
+ * the placeholder omitted the attribute for `[x]: <>` too).
1361
1352
  */
1362
- declare function sanitizeCrossChunkUrl(rawUrl: string, key: UrlAttrKey, tagName: UrlAttrTag, urlTransform: UrlTransform, schema: SanitizeSchema): string;
1353
+ declare function sanitizeCrossChunkUrl(rawUrl: string, key: UrlAttrKey, tagName: UrlAttrTag, urlTransform: UrlTransform, schema: SanitizeSchema): string | null;
1363
1354
 
1364
1355
  /**
1365
1356
  * The five shipped engine plugins and the default set.
@@ -1670,7 +1661,11 @@ declare const sanitizeSchema: Schema;
1670
1661
  * and the trailing grapheme of the source is held back until it is
1671
1662
  * confirmed — by more text arriving or by `finish()` — so a surrogate
1672
1663
  * half or a still-growing emoji ZWJ sequence is never revealed to the
1673
- * parser mid-cluster.
1664
+ * parser mid-cluster. `snap()` (a replacement, or the very first `update`)
1665
+ * is outside this promise by design: it shows the whole replacement text
1666
+ * at once, trailing half-cluster included — a hold-back there would leave
1667
+ * a controller that never drains when nothing follows (v2.4.2 review
1668
+ * P3-1). Hosts that seed a stream mid-cluster get one frame of U+FFFD.
1674
1669
  *
1675
1670
  * @module components/smoothStream/controller
1676
1671
  */
@@ -1754,7 +1749,8 @@ interface SmoothStreamController {
1754
1749
  * a later {@link update} resumes animation.
1755
1750
  */
1756
1751
  finish(): void;
1757
- /** Jumps to `source` instantly, no animation, and clears any backlog. */
1752
+ /** Jumps to `source` instantly, no animation, and clears any backlog.
1753
+ * No trailing-grapheme hold-back (see the module docs). */
1758
1754
  snap(source: string): void;
1759
1755
  /**
1760
1756
  * Reveals everything pending right now (skip-animation affordance). Keeps
@@ -1868,7 +1864,12 @@ interface Segment {
1868
1864
  * Split content into alternating text and protected segments.
1869
1865
  * Protected segments (isCode: true) are excluded from LaTeX processing:
1870
1866
  * - fenced multiline code blocks: 3+ backticks or tildes at the *start of a
1871
- * line* (≤3 space indent). Mid-line runs are never fence openers.
1867
+ * line* (any indentation container-relative limits are not modelled).
1868
+ * Mid-line runs are never fence openers. INDENTED code blocks (4+ spaces
1869
+ * after a blank line, outside any container) are NOT modelled: without a
1870
+ * container model they cannot be told from a list item's continuation
1871
+ * paragraph, and protecting them would silence math in nested lists.
1872
+ * Known limitation — `$` inside an indented code block may be rewritten.
1872
1873
  * - inline code spans: a run of N backticks closed by another run of exactly
1873
1874
  * N backticks. May span newlines. Multi-backtick forms (e.g. `` `` `x` ``)
1874
1875
  * are supported so literal backtick characters can appear inside.
package/dist/index.d.ts CHANGED
@@ -384,6 +384,16 @@ interface FreezeScanCheckpoint {
384
384
  prevLineWasValidDef: boolean;
385
385
  /** An earlier line of the current paragraph left an unpaired backtick
386
386
  * run — masking is disabled until the paragraph ends (safety gate). */
387
+ /** A `[` left unclosed at the end of a paragraph line (code spans
388
+ * masked). micromark lets a reference label span soft line breaks, so
389
+ * the label may close on a LATER line where the per-line REF_RE never
390
+ * sees a `[…]` pair (v2.4.1 review P1: `see [foo\nbar] end` + a late
391
+ * `[foo bar]: /u` retargeted frozen output). Cleared wherever the
392
+ * paragraph ends. */
393
+ openBracket: {
394
+ offset: number;
395
+ text: string;
396
+ } | null;
387
397
  paragraphHasUnpairedRun: boolean;
388
398
  /** Blocker-6 pending flag: a confirmed html-flow line left balanced
389
399
  * floating raw remnant, and no later content line has pinned the seam
@@ -754,34 +764,6 @@ interface ExtractContributionsOptions {
754
764
  * Defs matching these are skipped to avoid leaking sentinel rows into
755
765
  * registry.chunkData. */
756
766
  phantomFootnoteLabels?: Set<string>;
757
- /**
758
- * Caller's resolved URL transform (typically `props.urlTransform ??
759
- * defaultUrlTransform`). Applied to every emitted `linkDef.url` so the
760
- * registry stores already-sanitized URLs.
761
- *
762
- * Cross-chunk link/image references render through the registry rather
763
- * than the in-tree hast (which is where react-markdown's transform pass
764
- * normally enforces `urlTransform`). Without this, a chunk defining
765
- * `[evil]: javascript:alert(1)` could XSS a sibling chunk that uses
766
- * `[click][evil]` — the standalone path strips the protocol; the cross-
767
- * chunk path would have rendered `<a href="javascript:…">`. Sanitizing at
768
- * contribute time also benefits any future consumer that reads
769
- * `Registry.resolveLinkDef` directly.
770
- *
771
- * Invocation contract mirrors react-markdown's hast-pass call site
772
- * (`buildTransform` in `./markdown/transform.ts`): a synthetic
773
- * `<a href={url}>` element stands in for the node argument since
774
- * mdast `definition` nodes have no hast counterpart. The key is `'href'`
775
- * — link defs are far more common than image defs, and protocol-allowlist
776
- * transforms (including `defaultUrlTransform`) are key-agnostic anyway.
777
- * A `null` return collapses to the empty string, matching how
778
- * `transform.ts` would render a blocked attribute.
779
- *
780
- * Omitting this option preserves v1 behavior (URLs stored raw). Library
781
- * callers should always supply it; the option stays optional so unit-test
782
- * fixtures that don't care about URL safety can construct minimal calls.
783
- */
784
- urlTransform?: UrlTransform;
785
767
  }
786
768
  declare function extractContributions(mdast: Root, options?: ExtractContributionsOptions): Generator<Contribution>;
787
769
 
@@ -850,10 +832,12 @@ interface ChunkData {
850
832
  * interface. Driving the registry directly is reserved for internal
851
833
  * coordinators (the package's own `MarkdownContent` renderer) and tests,
852
834
  * which import the wider `RegistryInternal` type from this module.
853
- * `RegistryInternal` is exported here but NOT re-exported from the package
854
- * barrel keeping mutators off the public surface prevents a misbehaving
855
- * consumer-component from corrupting refcounts, skipping version bumps, or
856
- * otherwise breaking the invariants the renderer relies on.
835
+ * `RegistryInternal` is exported from this engine package (the core
836
+ * renderer consumes it) but NOT from `@ai-react-markdown/core`'s public
837
+ * barrel keeping mutators off the consumer-facing surface prevents a
838
+ * misbehaving consumer-component from corrupting refcounts, skipping
839
+ * version bumps, or otherwise breaking the invariants the renderer relies
840
+ * on.
857
841
  */
858
842
  interface Registry {
859
843
  /** Chunk mount-order Symbol list. **Read-only.** Direct mutation
@@ -878,18 +862,15 @@ interface Registry {
878
862
  canonicalLinkFor(label: string): symbol | null;
879
863
  globalNumber(label: string): number | null;
880
864
  /**
881
- * Resolve a cross-chunk link definition by label. The returned `url` is the
882
- * value the contributing chunk's `urlTransform` produced cross-chunk
883
- * link/image references run a second, per-attribute sanitization pass
884
- * (`urlTransform` + `sanitizeSchema.protocols`) at render time, so the
885
- * placeholder components themselves never trust this value blindly.
865
+ * Resolve a cross-chunk link definition by label. The returned `url` is
866
+ * the RAW destination from the contributing chunk's sourcethe registry
867
+ * does not sanitize. The library's cross-chunk link/image placeholders run
868
+ * the full per-attribute sanitization (`sanitizeSchema.protocols` +
869
+ * `urlTransform`, correct key) at render time.
886
870
  *
887
871
  * Consumers reading `def.url` directly (custom backlink panels, analytics,
888
- * dev tooling) receive a defense-in-depth-filtered string but should still
889
- * pipe it through their own `urlTransform` if they intend to render it as
890
- * an `href`/`src` — the contribute-time pass uses the `'href'` key and a
891
- * synthetic `<a>` node, so a key-aware policy may treat the value
892
- * differently when used as an `<img src>`.
872
+ * dev tooling) MUST run their own policy before rendering it as an
873
+ * `href`/`src` a chunk can define `[evil]: javascript:alert(1)`.
893
874
  */
894
875
  resolveLinkDef(label: string): LinkDef | null;
895
876
  getRefsForLabel(label: string): number;
@@ -906,9 +887,10 @@ interface Registry {
906
887
  * methods and implementation-private fields (reactId-keyed refcount table,
907
888
  * subscriber set, microtask-coalesce flag, `_notify` itself).
908
889
  *
909
- * Exported from this module so internal coordinators (`MarkdownContent`)
910
- * and tests can hold a strongly-typed reference, but **not** re-exported
911
- * from the package barrel a consumer flipping `_notifyScheduled = true`
890
+ * Exported from this module (and the engine barrel) so internal
891
+ * coordinators (`MarkdownContent`) and tests can hold a strongly-typed
892
+ * reference, but **not** re-exported from `@ai-react-markdown/core`'s
893
+ * barrel — a consumer flipping `_notifyScheduled = true`
912
894
  * or pushing into `chunkOrder` directly would silently break the
913
895
  * coalesce / numbering invariants. The runtime value returned by
914
896
  * {@link createRegistry} always satisfies this wider shape; public consumers
@@ -1319,11 +1301,13 @@ declare function buildCrossChunkHandlers(): Handlers;
1319
1301
  * In the standalone path, every `<a href>` and `<img src>` element passes
1320
1302
  * through TWO gates before render:
1321
1303
  *
1322
- * 1. `urlTransform(url, key, node)` — the caller's allowlist (default:
1323
- * `defaultUrlTransform`, which mirrors GitHub's protocol allowlist).
1324
- * Runs in `markdown/transform.ts` during the hast visit pass.
1325
- * 2. `rehype-sanitize` `protocols.<attr>` allowlist — drops the attribute
1304
+ * 1. `rehype-sanitize` `protocols.<attr>` allowlist drops the attribute
1326
1305
  * entirely if the URL's protocol isn't permitted for that attribute.
1306
+ * Runs in the rehype plugin chain.
1307
+ * 2. `urlTransform(url, key, node)` — the caller's allowlist (default:
1308
+ * `defaultUrlTransform`, which mirrors GitHub's protocol allowlist).
1309
+ * Runs in `markdown/transform.ts` during the hast visit pass; its
1310
+ * return value IS the attribute (`''` renders `href=""`).
1327
1311
  *
1328
1312
  * Cross-chunk references skip BOTH passes naturally: the placeholder hast
1329
1313
  * tag (`<cross-chunk-link>` / `<cross-chunk-image>`) carries only a `label`
@@ -1353,13 +1337,20 @@ declare function buildCrossChunkHandlers(): Handlers;
1353
1337
  type UrlAttrKey = 'href' | 'src';
1354
1338
  type UrlAttrTag = 'a' | 'img';
1355
1339
  /**
1356
- * Run a cross-chunk-resolved URL through both standalone gates.
1340
+ * Run a cross-chunk-resolved URL through both standalone gates, in the
1341
+ * standalone ORDER: `rehype-sanitize`'s protocol allowlist runs in the
1342
+ * plugin chain first (a blocked URL loses the attribute entirely), then
1343
+ * `urlTransform` rewrites whatever survived during the hast visit pass (its
1344
+ * return value is the attribute — `''` from `defaultUrlTransform` renders
1345
+ * `href=""`, exactly like a legal empty destination `[x]: <>`).
1357
1346
  *
1358
- * @returns Sanitized URL string. Empty string when either gate strips the
1359
- * URL — matches `defaultUrlTransform`'s and `rehype-sanitize`'s observable
1360
- * behavior of replacing a blocked URL with `''`.
1347
+ * @returns `null` when the protocol gate strips the attribute (render it
1348
+ * ABSENT); otherwise the transformed URL string possibly `''`, which
1349
+ * the caller must render as `href=""` / `src=""` to stay byte-identical
1350
+ * with standalone (v2.4.1 review: the two used to collapse into `''` and
1351
+ * the placeholder omitted the attribute for `[x]: <>` too).
1361
1352
  */
1362
- declare function sanitizeCrossChunkUrl(rawUrl: string, key: UrlAttrKey, tagName: UrlAttrTag, urlTransform: UrlTransform, schema: SanitizeSchema): string;
1353
+ declare function sanitizeCrossChunkUrl(rawUrl: string, key: UrlAttrKey, tagName: UrlAttrTag, urlTransform: UrlTransform, schema: SanitizeSchema): string | null;
1363
1354
 
1364
1355
  /**
1365
1356
  * The five shipped engine plugins and the default set.
@@ -1670,7 +1661,11 @@ declare const sanitizeSchema: Schema;
1670
1661
  * and the trailing grapheme of the source is held back until it is
1671
1662
  * confirmed — by more text arriving or by `finish()` — so a surrogate
1672
1663
  * half or a still-growing emoji ZWJ sequence is never revealed to the
1673
- * parser mid-cluster.
1664
+ * parser mid-cluster. `snap()` (a replacement, or the very first `update`)
1665
+ * is outside this promise by design: it shows the whole replacement text
1666
+ * at once, trailing half-cluster included — a hold-back there would leave
1667
+ * a controller that never drains when nothing follows (v2.4.2 review
1668
+ * P3-1). Hosts that seed a stream mid-cluster get one frame of U+FFFD.
1674
1669
  *
1675
1670
  * @module components/smoothStream/controller
1676
1671
  */
@@ -1754,7 +1749,8 @@ interface SmoothStreamController {
1754
1749
  * a later {@link update} resumes animation.
1755
1750
  */
1756
1751
  finish(): void;
1757
- /** Jumps to `source` instantly, no animation, and clears any backlog. */
1752
+ /** Jumps to `source` instantly, no animation, and clears any backlog.
1753
+ * No trailing-grapheme hold-back (see the module docs). */
1758
1754
  snap(source: string): void;
1759
1755
  /**
1760
1756
  * Reveals everything pending right now (skip-animation affordance). Keeps
@@ -1868,7 +1864,12 @@ interface Segment {
1868
1864
  * Split content into alternating text and protected segments.
1869
1865
  * Protected segments (isCode: true) are excluded from LaTeX processing:
1870
1866
  * - fenced multiline code blocks: 3+ backticks or tildes at the *start of a
1871
- * line* (≤3 space indent). Mid-line runs are never fence openers.
1867
+ * line* (any indentation container-relative limits are not modelled).
1868
+ * Mid-line runs are never fence openers. INDENTED code blocks (4+ spaces
1869
+ * after a blank line, outside any container) are NOT modelled: without a
1870
+ * container model they cannot be told from a list item's continuation
1871
+ * paragraph, and protecting them would silence math in nested lists.
1872
+ * Known limitation — `$` inside an indented code block may be rewritten.
1872
1873
  * - inline code spans: a run of N backticks closed by another run of exactly
1873
1874
  * N backticks. May span newlines. Multi-backtick forms (e.g. `` `` `x` ``)
1874
1875
  * are supported so literal backtick characters can appear inside.