@ai-react-markdown/engine 2.4.2 → 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
@@ -764,34 +764,6 @@ interface ExtractContributionsOptions {
764
764
  * Defs matching these are skipped to avoid leaking sentinel rows into
765
765
  * registry.chunkData. */
766
766
  phantomFootnoteLabels?: Set<string>;
767
- /**
768
- * Caller's resolved URL transform (typically `props.urlTransform ??
769
- * defaultUrlTransform`). Applied to every emitted `linkDef.url` so the
770
- * registry stores already-sanitized URLs.
771
- *
772
- * Cross-chunk link/image references render through the registry rather
773
- * than the in-tree hast (which is where react-markdown's transform pass
774
- * normally enforces `urlTransform`). Without this, a chunk defining
775
- * `[evil]: javascript:alert(1)` could XSS a sibling chunk that uses
776
- * `[click][evil]` — the standalone path strips the protocol; the cross-
777
- * chunk path would have rendered `<a href="javascript:…">`. Sanitizing at
778
- * contribute time also benefits any future consumer that reads
779
- * `Registry.resolveLinkDef` directly.
780
- *
781
- * Invocation contract mirrors react-markdown's hast-pass call site
782
- * (`buildTransform` in `./markdown/transform.ts`): a synthetic
783
- * `<a href={url}>` element stands in for the node argument since
784
- * mdast `definition` nodes have no hast counterpart. The key is `'href'`
785
- * — link defs are far more common than image defs, and protocol-allowlist
786
- * transforms (including `defaultUrlTransform`) are key-agnostic anyway.
787
- * A `null` return collapses to the empty string, matching how
788
- * `transform.ts` would render a blocked attribute.
789
- *
790
- * Omitting this option preserves v1 behavior (URLs stored raw). Library
791
- * callers should always supply it; the option stays optional so unit-test
792
- * fixtures that don't care about URL safety can construct minimal calls.
793
- */
794
- urlTransform?: UrlTransform;
795
767
  }
796
768
  declare function extractContributions(mdast: Root, options?: ExtractContributionsOptions): Generator<Contribution>;
797
769
 
@@ -890,18 +862,15 @@ interface Registry {
890
862
  canonicalLinkFor(label: string): symbol | null;
891
863
  globalNumber(label: string): number | null;
892
864
  /**
893
- * Resolve a cross-chunk link definition by label. The returned `url` is the
894
- * value the contributing chunk's `urlTransform` produced cross-chunk
895
- * link/image references run a second, per-attribute sanitization pass
896
- * (`urlTransform` + `sanitizeSchema.protocols`) at render time, so the
897
- * 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.
898
870
  *
899
871
  * Consumers reading `def.url` directly (custom backlink panels, analytics,
900
- * dev tooling) receive a defense-in-depth-filtered string but should still
901
- * pipe it through their own `urlTransform` if they intend to render it as
902
- * an `href`/`src` — the contribute-time pass uses the `'href'` key and a
903
- * synthetic `<a>` node, so a key-aware policy may treat the value
904
- * 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)`.
905
874
  */
906
875
  resolveLinkDef(label: string): LinkDef | null;
907
876
  getRefsForLabel(label: string): number;
@@ -1692,7 +1661,11 @@ declare const sanitizeSchema: Schema;
1692
1661
  * and the trailing grapheme of the source is held back until it is
1693
1662
  * confirmed — by more text arriving or by `finish()` — so a surrogate
1694
1663
  * half or a still-growing emoji ZWJ sequence is never revealed to the
1695
- * 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.
1696
1669
  *
1697
1670
  * @module components/smoothStream/controller
1698
1671
  */
@@ -1776,7 +1749,8 @@ interface SmoothStreamController {
1776
1749
  * a later {@link update} resumes animation.
1777
1750
  */
1778
1751
  finish(): void;
1779
- /** 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). */
1780
1754
  snap(source: string): void;
1781
1755
  /**
1782
1756
  * Reveals everything pending right now (skip-animation affordance). Keeps
@@ -1890,7 +1864,12 @@ interface Segment {
1890
1864
  * Split content into alternating text and protected segments.
1891
1865
  * Protected segments (isCode: true) are excluded from LaTeX processing:
1892
1866
  * - fenced multiline code blocks: 3+ backticks or tildes at the *start of a
1893
- * 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.
1894
1873
  * - inline code spans: a run of N backticks closed by another run of exactly
1895
1874
  * N backticks. May span newlines. Multi-backtick forms (e.g. `` `` `x` ``)
1896
1875
  * are supported so literal backtick characters can appear inside.
package/dist/index.d.ts CHANGED
@@ -764,34 +764,6 @@ interface ExtractContributionsOptions {
764
764
  * Defs matching these are skipped to avoid leaking sentinel rows into
765
765
  * registry.chunkData. */
766
766
  phantomFootnoteLabels?: Set<string>;
767
- /**
768
- * Caller's resolved URL transform (typically `props.urlTransform ??
769
- * defaultUrlTransform`). Applied to every emitted `linkDef.url` so the
770
- * registry stores already-sanitized URLs.
771
- *
772
- * Cross-chunk link/image references render through the registry rather
773
- * than the in-tree hast (which is where react-markdown's transform pass
774
- * normally enforces `urlTransform`). Without this, a chunk defining
775
- * `[evil]: javascript:alert(1)` could XSS a sibling chunk that uses
776
- * `[click][evil]` — the standalone path strips the protocol; the cross-
777
- * chunk path would have rendered `<a href="javascript:…">`. Sanitizing at
778
- * contribute time also benefits any future consumer that reads
779
- * `Registry.resolveLinkDef` directly.
780
- *
781
- * Invocation contract mirrors react-markdown's hast-pass call site
782
- * (`buildTransform` in `./markdown/transform.ts`): a synthetic
783
- * `<a href={url}>` element stands in for the node argument since
784
- * mdast `definition` nodes have no hast counterpart. The key is `'href'`
785
- * — link defs are far more common than image defs, and protocol-allowlist
786
- * transforms (including `defaultUrlTransform`) are key-agnostic anyway.
787
- * A `null` return collapses to the empty string, matching how
788
- * `transform.ts` would render a blocked attribute.
789
- *
790
- * Omitting this option preserves v1 behavior (URLs stored raw). Library
791
- * callers should always supply it; the option stays optional so unit-test
792
- * fixtures that don't care about URL safety can construct minimal calls.
793
- */
794
- urlTransform?: UrlTransform;
795
767
  }
796
768
  declare function extractContributions(mdast: Root, options?: ExtractContributionsOptions): Generator<Contribution>;
797
769
 
@@ -890,18 +862,15 @@ interface Registry {
890
862
  canonicalLinkFor(label: string): symbol | null;
891
863
  globalNumber(label: string): number | null;
892
864
  /**
893
- * Resolve a cross-chunk link definition by label. The returned `url` is the
894
- * value the contributing chunk's `urlTransform` produced cross-chunk
895
- * link/image references run a second, per-attribute sanitization pass
896
- * (`urlTransform` + `sanitizeSchema.protocols`) at render time, so the
897
- * 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.
898
870
  *
899
871
  * Consumers reading `def.url` directly (custom backlink panels, analytics,
900
- * dev tooling) receive a defense-in-depth-filtered string but should still
901
- * pipe it through their own `urlTransform` if they intend to render it as
902
- * an `href`/`src` — the contribute-time pass uses the `'href'` key and a
903
- * synthetic `<a>` node, so a key-aware policy may treat the value
904
- * 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)`.
905
874
  */
906
875
  resolveLinkDef(label: string): LinkDef | null;
907
876
  getRefsForLabel(label: string): number;
@@ -1692,7 +1661,11 @@ declare const sanitizeSchema: Schema;
1692
1661
  * and the trailing grapheme of the source is held back until it is
1693
1662
  * confirmed — by more text arriving or by `finish()` — so a surrogate
1694
1663
  * half or a still-growing emoji ZWJ sequence is never revealed to the
1695
- * 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.
1696
1669
  *
1697
1670
  * @module components/smoothStream/controller
1698
1671
  */
@@ -1776,7 +1749,8 @@ interface SmoothStreamController {
1776
1749
  * a later {@link update} resumes animation.
1777
1750
  */
1778
1751
  finish(): void;
1779
- /** 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). */
1780
1754
  snap(source: string): void;
1781
1755
  /**
1782
1756
  * Reveals everything pending right now (skip-animation affordance). Keeps
@@ -1890,7 +1864,12 @@ interface Segment {
1890
1864
  * Split content into alternating text and protected segments.
1891
1865
  * Protected segments (isCode: true) are excluded from LaTeX processing:
1892
1866
  * - fenced multiline code blocks: 3+ backticks or tildes at the *start of a
1893
- * 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.
1894
1873
  * - inline code spans: a run of N backticks closed by another run of exactly
1895
1874
  * N backticks. May span newlines. Multi-backtick forms (e.g. `` `` `x` ``)
1896
1875
  * are supported so literal backtick characters can appear inside.
@@ -1149,6 +1149,8 @@ function rebaseDualWalk(node, segments, maxEnd, offsetDelta, lineDelta) {
1149
1149
  for (const child of children) rebaseDualWalk(child, segments, maxEnd, offsetDelta, lineDelta);
1150
1150
  }
1151
1151
  }
1152
+ var TABLE_PART_TAG_RE = /<(?:td|th|tr|tbody|thead|tfoot|caption|col|colgroup)\b/i;
1153
+ var STRAY_SYNTHESIZED_END_TAG_RE = /<\/(?:br|p)\b/i;
1152
1154
  function spliceTrees(input) {
1153
1155
  const { prevMdast, prevHast, tailMdast, tailHast, content, boundary, injectionPrefix, injectedSegments } = input;
1154
1156
  const injectedLen = injectionPrefix.length;
@@ -1205,6 +1207,12 @@ function spliceTrees(input) {
1205
1207
  return !(start !== void 0 && start < injectedLen);
1206
1208
  });
1207
1209
  const tailWrapVisible = tailMdastChildren.some((child) => !isWrapInvisible(child));
1210
+ if (prefixMdast.some((c) => c.type === "html" && TABLE_PART_TAG_RE.test(c.value))) return null;
1211
+ for (const child of tailMdastChildren) {
1212
+ if (isWrapInvisible(child)) continue;
1213
+ if (child.type !== "html") break;
1214
+ if (STRAY_SYNTHESIZED_END_TAG_RE.test(child.value) || TABLE_PART_TAG_RE.test(child.value)) return null;
1215
+ }
1208
1216
  const aligned = alignPrefixCut(prefixMdast, cutRegion, tailWrapVisible);
1209
1217
  if (aligned === null) return null;
1210
1218
  const hastChildren = aligned.children;
@@ -1330,6 +1338,9 @@ function alignPrefixCut(prefixMdast, cutRegion, tailWrapVisible) {
1330
1338
  return null;
1331
1339
  }
1332
1340
  const lastIsLiteral = last !== void 0 && last.type === "text" && last.value.trim() !== "";
1341
+ if (lastIsLiteral && pairIdx >= 0 && visibles[pairIdx].type !== "html") {
1342
+ return null;
1343
+ }
1333
1344
  const litOwnerEnd = pairIdx >= 0 ? visibles[pairIdx].position?.end?.offset : void 0;
1334
1345
  const litEnd = lastIsLiteral ? last.position?.end?.offset : void 0;
1335
1346
  if (lastIsLiteral && last.position !== void 0 && (litEnd === void 0 || litOwnerEnd === void 0)) {
@@ -1418,8 +1429,9 @@ function stripInjectedHast(tailMdast, tailHast, injectedLen, tailWrapVisible) {
1418
1429
  }
1419
1430
  function tailLeadingTextIsHoist(tailMdastChildren, tailHastChildren) {
1420
1431
  const firstText = tailHastChildren[0];
1421
- if (!firstText) return false;
1422
1432
  const firstVisible = tailMdastChildren.find((c) => !isWrapInvisible(c));
1433
+ if (firstVisible?.type === "html" && STRAY_SYNTHESIZED_END_TAG_RE.test(firstVisible.value)) return null;
1434
+ if (!firstText) return false;
1423
1435
  if (!isSeparatorText(firstText)) {
1424
1436
  if (firstText.type === "text" && firstText.position === void 0 && firstVisible?.type === "html") {
1425
1437
  if (/^\s*<\/[A-Za-z][A-Za-z0-9-]*\s*>/.test(firstVisible.value)) return true;
@@ -1860,17 +1872,8 @@ function phantomSuffixCloser(content) {
1860
1872
  }
1861
1873
 
1862
1874
  // src/components/extractContributions.ts
1863
- function fakeAnchorElement(url) {
1864
- return { type: "element", tagName: "a", properties: { href: url }, children: [] };
1865
- }
1866
- function sanitizeDefUrl(url, urlTransform) {
1867
- if (!urlTransform) return url;
1868
- const result = urlTransform(url, "href", fakeAnchorElement(url));
1869
- return result == null ? "" : String(result);
1870
- }
1871
1875
  function* extractContributions(mdast, options = {}) {
1872
1876
  const phantomFn = options.phantomFootnoteLabels;
1873
- const urlTransform = options.urlTransform;
1874
1877
  const out = [];
1875
1878
  (0, import_unist_util_visit3.visit)(mdast, (n) => {
1876
1879
  if (n.type === "footnoteReference") {
@@ -1898,7 +1901,7 @@ function* extractContributions(mdast, options = {}) {
1898
1901
  out.push({
1899
1902
  kind: "linkDef",
1900
1903
  label: normalizeId(d.identifier),
1901
- url: sanitizeDefUrl(d.url, urlTransform),
1904
+ url: d.url,
1902
1905
  title: d.title
1903
1906
  });
1904
1907
  }
@@ -3612,8 +3615,7 @@ function isProtocolAllowed(url, allowed) {
3612
3615
  }
3613
3616
  function sanitizeCrossChunkUrl(rawUrl, key, tagName, urlTransform, schema) {
3614
3617
  rawUrl = (0, import_micromark_util_sanitize_uri2.normalizeUri)(rawUrl);
3615
- const callerProtocols = schema.protocols;
3616
- const allowed = callerProtocols === void 0 || callerProtocols === null ? sanitizeSchema.protocols?.[key] : callerProtocols[key];
3618
+ const allowed = Object.hasOwn(schema, "protocols") ? schema.protocols?.[key] : sanitizeSchema.protocols?.[key];
3617
3619
  if (allowed && allowed.length > 0 && !isProtocolAllowed(rawUrl, allowed)) return null;
3618
3620
  const transformed = urlTransform(rawUrl, key, fakeElement(tagName, key, rawUrl));
3619
3621
  if (transformed == null) return null;
@@ -4023,15 +4025,14 @@ function lineHasBacktick(content, pos) {
4023
4025
  }
4024
4026
  return false;
4025
4027
  }
4026
- function isAtLineStart(content, pos) {
4028
+ function lineIndentBefore(content, pos) {
4027
4029
  let i = pos - 1;
4028
- let spaces = 0;
4029
- while (i >= 0 && content[i] === " ") {
4030
- spaces++;
4031
- if (spaces > 3) return false;
4030
+ let indent = 0;
4031
+ while (i >= 0 && (content[i] === " " || content[i] === " ")) {
4032
+ indent += content[i] === " " ? 4 : 1;
4032
4033
  i--;
4033
4034
  }
4034
- return i < 0 || content[i] === "\n" || content[i] === "\r";
4035
+ return i < 0 || content[i] === "\n" || content[i] === "\r" ? indent : -1;
4035
4036
  }
4036
4037
  function findClosingBacktickRun(content, start, n) {
4037
4038
  let i = start;
@@ -4052,6 +4053,7 @@ function splitByProtectedRegions(content) {
4052
4053
  let multilineStart = -1;
4053
4054
  let multilineFenceMarker = null;
4054
4055
  let multilineFenceLength = 0;
4056
+ let multilineFenceIndent = 0;
4055
4057
  function pushProtected(start, end) {
4056
4058
  if (start > lastIndex) {
4057
4059
  segments.push({ text: content.substring(lastIndex, start), isCode: false });
@@ -4065,7 +4067,8 @@ function splitByProtectedRegions(content) {
4065
4067
  if (multilineStart !== -1) {
4066
4068
  if (char === multilineFenceMarker) {
4067
4069
  const runLen = getRepeatedMarkerLength(content, i, multilineFenceMarker);
4068
- if (runLen >= multilineFenceLength && isAtLineStart(content, i) && restOfLineIsBlank(content, i + runLen)) {
4070
+ const closerIndent = lineIndentBefore(content, i);
4071
+ if (runLen >= multilineFenceLength && closerIndent !== -1 && closerIndent <= multilineFenceIndent + 3 && restOfLineIsBlank(content, i + runLen)) {
4069
4072
  pushProtected(multilineStart, i + runLen);
4070
4073
  multilineStart = -1;
4071
4074
  multilineFenceMarker = null;
@@ -4081,10 +4084,12 @@ function splitByProtectedRegions(content) {
4081
4084
  }
4082
4085
  if (char === "`" || char === "~") {
4083
4086
  const runLen = getRepeatedMarkerLength(content, i, char);
4084
- if (runLen >= 3 && isAtLineStart(content, i) && !(char === "`" && lineHasBacktick(content, i + runLen))) {
4087
+ const openerIndent = lineIndentBefore(content, i);
4088
+ if (runLen >= 3 && openerIndent !== -1 && !(char === "`" && lineHasBacktick(content, i + runLen))) {
4085
4089
  multilineStart = i;
4086
4090
  multilineFenceMarker = char;
4087
4091
  multilineFenceLength = runLen;
4092
+ multilineFenceIndent = openerIndent;
4088
4093
  i += runLen;
4089
4094
  continue;
4090
4095
  }