@stream-mdx/core 0.4.0 → 0.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/CHANGELOG.md CHANGED
@@ -1,10 +1,17 @@
1
1
  # @stream-mdx/core
2
2
 
3
- ## 0.1.1
3
+ ## 0.5.0
4
4
 
5
- ### Patch Changes
5
+ ### Minor Changes
6
+
7
+ - Promote the current hardening and product-surface tranche as `0.5.0`.
8
+
9
+ This release rolls up:
6
10
 
7
- - Refine streaming scheduling and list layout, add worker append batching/debug state support, and refresh docs/README examples.
11
+ - renderer/store correctness hardening
12
+ - seeded smoke and scheduler-parity release discipline
13
+ - benchmark methodology and comparison cleanup
14
+ - docs, showcase, and TUI/product surface maturation
8
15
 
9
16
  ## 0.1.0
10
17
 
package/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # `@stream-mdx/core`
2
2
 
3
- Core types + utilities shared across the StreamMDX stack.
3
+ `@stream-mdx/core` is the React-free foundation of the StreamMDX stack. It provides the shared types, snapshot utilities, sanitization primitives, inline/mixed-content helpers, and performance/backpressure utilities used by the worker and renderer layers.
4
4
 
5
- This package is intentionally React-free. It contains structured-clone-safe types and helpers used by both the worker and the renderer.
6
-
7
- Most consumers should install `stream-mdx` and follow the main docs. Use `@stream-mdx/core` directly if you’re building tooling or customizing lower-level behavior.
5
+ If you are building apps, start with [`stream-mdx`](../stream-mdx/README.md). Use `@stream-mdx/core` directly when you are building tooling, protocol consumers, performance instrumentation, or lower-level integrations.
8
6
 
9
7
  ## Install
10
8
 
@@ -12,43 +10,59 @@ Most consumers should install `stream-mdx` and follow the main docs. Use `@strea
12
10
  npm install @stream-mdx/core
13
11
  ```
14
12
 
15
- ## Entry points
13
+ ## Export Surface
14
+
15
+ | Export | Purpose |
16
+ | --- | --- |
17
+ | `@stream-mdx/core` | Main types/helpers surface |
18
+ | `@stream-mdx/core/types` | Shared types |
19
+ | `@stream-mdx/core/utils` | General utilities |
20
+ | `@stream-mdx/core/code-highlighting` | Code-highlighting helpers |
21
+ | `@stream-mdx/core/inline-parser` | Inline parsing helpers |
22
+ | `@stream-mdx/core/mixed-content` | Mixed-content parsing helpers |
23
+ | `@stream-mdx/core/worker-html-sanitizer` | Worker-side HTML sanitization helpers |
24
+ | `@stream-mdx/core/security` | Security-oriented helpers |
25
+ | `@stream-mdx/core/perf/backpressure` | Backpressure config and helpers |
26
+ | `@stream-mdx/core/perf/patch-batching` | Patch batching helpers |
27
+ | `@stream-mdx/core/perf/patch-coalescing` | Patch coalescing helpers |
28
+ | `@stream-mdx/core/streaming/custom-matcher` | Custom matcher hooks |
29
+ | `@stream-mdx/core/streaming/inline-streaming` | Inline streaming helpers |
16
30
 
17
- - `@stream-mdx/core` (root)
18
- - `@stream-mdx/core/types`
19
- - `@stream-mdx/core/utils`
20
- - `@stream-mdx/core/code-highlighting`
21
- - `@stream-mdx/core/inline-parser`
22
- - `@stream-mdx/core/mixed-content`
23
- - `@stream-mdx/core/worker-html-sanitizer`
24
- - `@stream-mdx/core/security`
25
- - `@stream-mdx/core/perf/backpressure`
26
- - `@stream-mdx/core/perf/patch-batching`
27
- - `@stream-mdx/core/perf/patch-coalescing`
28
- - `@stream-mdx/core/streaming/custom-matcher`
31
+ ## Typical Uses
29
32
 
30
- ## Example
33
+ ### Backpressure tuning
31
34
 
32
35
  ```ts
33
36
  import { DEFAULT_BACKPRESSURE_CONFIG } from "@stream-mdx/core/perf/backpressure";
34
37
 
35
- export function makeConfig(overrides?: Partial<typeof DEFAULT_BACKPRESSURE_CONFIG>) {
36
- return { ...DEFAULT_BACKPRESSURE_CONFIG, ...overrides };
37
- }
38
+ export const rendererBackpressure = {
39
+ ...DEFAULT_BACKPRESSURE_CONFIG,
40
+ maxPendingBatches: 64,
41
+ };
42
+ ```
43
+
44
+ ### Patch batching/coalescing experiments
45
+
46
+ ```ts
47
+ import { splitPatchBatch } from "@stream-mdx/core/perf/patch-batching";
38
48
  ```
39
49
 
40
- ## Docs
50
+ ### Lower-level typed integrations
51
+
52
+ Use `@stream-mdx/core` together with `@stream-mdx/protocol` when you want a typed transport or a non-React consumer of StreamMDX patch data.
53
+
54
+ ## Related Packages
41
55
 
42
- - API reference: https://github.com/kmccleary3301/stream-mdx/blob/main/docs/PUBLIC_API.md
43
- - Security model: https://github.com/kmccleary3301/stream-mdx/blob/main/docs/SECURITY_MODEL.md
44
- - Protocol spec (for typed stream events): https://github.com/kmccleary3301/stream-mdx/blob/main/docs/STREAMMDX_JSON_DIFF_SPEC.md
56
+ | Package | Role |
57
+ | --- | --- |
58
+ | [`@stream-mdx/worker`](../markdown-v2-worker/README.md) | Worker runtime and hosted worker helpers |
59
+ | [`@stream-mdx/react`](../markdown-v2-react/README.md) | React renderer and server helpers |
60
+ | [`@stream-mdx/protocol`](../markdown-v2-protocol/README.md) | Protocol envelope/types for transport |
61
+ | [`@stream-mdx/tui`](../markdown-v2-tui/README.md) | TUI utilities and snapshot store |
45
62
 
46
- ## Related packages
63
+ ## Documentation
47
64
 
48
- - `@stream-mdx/react` for the React renderer
49
- - `@stream-mdx/worker` for hosted worker bundles
50
- - `@stream-mdx/plugins` for worker-side plugin primitives (advanced)
51
- - `@stream-mdx/protocol` for stable protocol/event types (e.g. TUIs)
52
- - `@stream-mdx/tui` for NDJSON helpers + a snapshot store (terminal UIs)
53
- - `@stream-mdx/mermaid` for Mermaid diagram rendering (optional)
54
- - `@stream-mdx/theme-tailwind` for an optional Tailwind-friendly theme (optional)
65
+ - [`../../docs/PUBLIC_API.md`](../../docs/PUBLIC_API.md)
66
+ - [`../../docs/SECURITY_MODEL.md`](../../docs/SECURITY_MODEL.md)
67
+ - [`../../docs/STREAMMDX_JSON_DIFF_SPEC.md`](../../docs/STREAMMDX_JSON_DIFF_SPEC.md)
68
+ - [`../../docs/DETERMINISM.md`](../../docs/DETERMINISM.md)
@@ -35,6 +35,9 @@ function normalizeNewlines(input) {
35
35
  function isFenceLine(line) {
36
36
  return /^```/.test(line.trim());
37
37
  }
38
+ function isTrailingPartialFenceLine(line) {
39
+ return /^[\t ]*`{1,2}[\t ]*$/.test(line);
40
+ }
38
41
  function stripCodeFence(raw) {
39
42
  if (!raw) {
40
43
  return { code: "", info: "", hadFence: false };
@@ -58,6 +61,9 @@ function stripCodeFence(raw) {
58
61
  return { code: codeLines2.join("\n"), info, hadFence: true };
59
62
  }
60
63
  const codeLines = lines.slice(1);
64
+ if (!normalized.endsWith("\n") && codeLines.length > 0 && isTrailingPartialFenceLine(codeLines[codeLines.length - 1] ?? "")) {
65
+ codeLines.pop();
66
+ }
61
67
  return { code: codeLines.join("\n"), info, hadFence: true };
62
68
  }
63
69
  function getDomParser() {
@@ -5,6 +5,9 @@ function normalizeNewlines(input) {
5
5
  function isFenceLine(line) {
6
6
  return /^```/.test(line.trim());
7
7
  }
8
+ function isTrailingPartialFenceLine(line) {
9
+ return /^[\t ]*`{1,2}[\t ]*$/.test(line);
10
+ }
8
11
  function stripCodeFence(raw) {
9
12
  if (!raw) {
10
13
  return { code: "", info: "", hadFence: false };
@@ -28,6 +31,9 @@ function stripCodeFence(raw) {
28
31
  return { code: codeLines2.join("\n"), info, hadFence: true };
29
32
  }
30
33
  const codeLines = lines.slice(1);
34
+ if (!normalized.endsWith("\n") && codeLines.length > 0 && isTrailingPartialFenceLine(codeLines[codeLines.length - 1] ?? "")) {
35
+ codeLines.pop();
36
+ }
31
37
  return { code: codeLines.join("\n"), info, hadFence: true };
32
38
  }
33
39
  function getDomParser() {
package/dist/index.cjs CHANGED
@@ -115,6 +115,9 @@ function normalizeNewlines(input) {
115
115
  function isFenceLine(line) {
116
116
  return /^```/.test(line.trim());
117
117
  }
118
+ function isTrailingPartialFenceLine(line) {
119
+ return /^[\t ]*`{1,2}[\t ]*$/.test(line);
120
+ }
118
121
  function stripCodeFence(raw) {
119
122
  if (!raw) {
120
123
  return { code: "", info: "", hadFence: false };
@@ -138,6 +141,9 @@ function stripCodeFence(raw) {
138
141
  return { code: codeLines2.join("\n"), info, hadFence: true };
139
142
  }
140
143
  const codeLines = lines.slice(1);
144
+ if (!normalized.endsWith("\n") && codeLines.length > 0 && isTrailingPartialFenceLine(codeLines[codeLines.length - 1] ?? "")) {
145
+ codeLines.pop();
146
+ }
141
147
  return { code: codeLines.join("\n"), info, hadFence: true };
142
148
  }
143
149
  function getDomParser() {
@@ -520,12 +526,12 @@ var InlineParser = class {
520
526
  this.registerPlugin({
521
527
  id: "strong-emphasis",
522
528
  priority: 6,
523
- re: /\*\*\*([^*\n]+?)\*\*\*|\*\*([^*\n]+?)\*\*/g,
529
+ re: /\*\*\*([^*\n]+?)\*\*\*|\*\*([^*\n]+?)\*\*|(?<![\w\\])__([^_\n]+?)__(?!\w)/g,
524
530
  toNode: (match) => ({
525
531
  kind: "strong",
526
- children: [{ kind: "text", text: match[1] || match[2] }]
532
+ children: [{ kind: "text", text: match[1] || match[2] || match[3] }]
527
533
  }),
528
- fastCheck: (text) => text.indexOf("**") !== -1
534
+ fastCheck: (text) => text.indexOf("**") !== -1 || text.indexOf("__") !== -1
529
535
  });
530
536
  this.registerPlugin({
531
537
  id: "strikethrough",
@@ -540,12 +546,12 @@ var InlineParser = class {
540
546
  this.registerPlugin({
541
547
  id: "emphasis",
542
548
  priority: 8,
543
- re: /\*([^*\n]+?)\*/g,
549
+ re: /\*([^*\n]+?)\*|(?<![\w\\])_([^_\n]+?)_(?!\w)/g,
544
550
  toNode: (match) => ({
545
551
  kind: "em",
546
- children: [{ kind: "text", text: match[1] }]
552
+ children: [{ kind: "text", text: match[1] || match[2] }]
547
553
  }),
548
- fastCheck: (text) => text.indexOf("*") !== -1
554
+ fastCheck: (text) => text.indexOf("*") !== -1 || text.indexOf("_") !== -1
549
555
  });
550
556
  this.registerPlugin({
551
557
  id: "citations",
@@ -696,6 +702,13 @@ function prepareInlineStreamingContent(content, options) {
696
702
  };
697
703
  let mathDisplayOpen = false;
698
704
  let mathDisplayCrossedNewline = false;
705
+ const shouldOpenInlineMath = (index) => {
706
+ const next = content[index + 1] ?? "";
707
+ if (/\d/.test(next)) {
708
+ return false;
709
+ }
710
+ return true;
711
+ };
699
712
  for (let i = 0; i < content.length; i++) {
700
713
  const code = content.charCodeAt(i);
701
714
  if (code === 10 || code === 13) {
@@ -734,7 +747,10 @@ function prepareInlineStreamingContent(content, options) {
734
747
  }
735
748
  i += 1;
736
749
  } else {
737
- toggleToken("math-inline");
750
+ const mathInlineOpen = stack.includes("math-inline");
751
+ if (mathInlineOpen || shouldOpenInlineMath(i)) {
752
+ toggleToken("math-inline");
753
+ }
738
754
  }
739
755
  }
740
756
  }
@@ -746,7 +762,7 @@ function prepareInlineStreamingContent(content, options) {
746
762
  if (hasIncompleteMathInline && !enableMathInlineAnticipation) {
747
763
  return { kind: "raw", status: "raw", reason: "incomplete-math" };
748
764
  }
749
- if (hasIncompleteMathDisplay && (!enableMathBlockAnticipation || mathDisplayCrossedNewline)) {
765
+ if (hasIncompleteMathDisplay && !enableMathBlockAnticipation) {
750
766
  return { kind: "raw", status: "raw", reason: "incomplete-math" };
751
767
  }
752
768
  }
@@ -769,7 +785,10 @@ function prepareInlineStreamingContent(content, options) {
769
785
  case "math-inline":
770
786
  return "$";
771
787
  case "math-display":
772
- return "$$";
788
+ if (!mathDisplayCrossedNewline) {
789
+ return "$$";
790
+ }
791
+ return content.endsWith("\n") || content.endsWith("\r") ? "$$" : "\n$$";
773
792
  default:
774
793
  return "";
775
794
  }
@@ -987,9 +1006,25 @@ function splitByTagSegments(source, baseOffset, parseInline, options) {
987
1006
  continue;
988
1007
  }
989
1008
  }
990
- if (!isSelfClosing && !mdxAllowed) {
991
- const closingIndex = findClosingHtmlTag(lowerSource, tagName.toLowerCase(), end);
992
- if (closingIndex === -1) {
1009
+ if (!isSelfClosing) {
1010
+ const closingIndex = findClosingHtmlTag(lowerSource, tagNameLower, end);
1011
+ if (closingIndex !== -1) {
1012
+ end = closingIndex;
1013
+ } else if (mdxAllowed) {
1014
+ if (mdxAutoClose) {
1015
+ const tail = source.slice(end);
1016
+ const newlineCount = countNewlines(tail, mdxMaxNewlines + 1);
1017
+ if (newlineCount > mdxMaxNewlines) {
1018
+ tagPattern.lastIndex = start + 1;
1019
+ match = tagPattern.exec(source);
1020
+ continue;
1021
+ }
1022
+ } else {
1023
+ tagPattern.lastIndex = start + 1;
1024
+ match = tagPattern.exec(source);
1025
+ continue;
1026
+ }
1027
+ } else {
993
1028
  if (htmlAutoClose && htmlAllowTags.has(tagNameLower)) {
994
1029
  const tail = source.slice(end);
995
1030
  const newlineCount = countNewlines(tail, htmlMaxNewlines + 1);
@@ -1016,7 +1051,6 @@ function splitByTagSegments(source, baseOffset, parseInline, options) {
1016
1051
  match = tagPattern.exec(source);
1017
1052
  continue;
1018
1053
  }
1019
- end = closingIndex;
1020
1054
  }
1021
1055
  if (start > cursor) {
1022
1056
  const absoluteFrom = baseIsFinite ? baseOffset + cursor : void 0;
@@ -1040,7 +1074,9 @@ function splitByTagSegments(source, baseOffset, parseInline, options) {
1040
1074
  match = tagPattern.exec(source);
1041
1075
  continue;
1042
1076
  }
1043
- if (mdxAutoClose && !rawSegment.endsWith("/>")) {
1077
+ const closingTagPattern = new RegExp(`</\\s*${escapeRegExp(tagName)}\\s*>\\s*$`, "i");
1078
+ const hasExplicitClose = closingTagPattern.test(rawSegment);
1079
+ if (mdxAutoClose && !hasExplicitClose && !rawSegment.endsWith("/>")) {
1044
1080
  rawSegment = selfCloseTag(rawSegment);
1045
1081
  segment.value = rawSegment;
1046
1082
  }
@@ -1210,6 +1246,9 @@ function selfCloseTag(rawTag) {
1210
1246
  if (closeIndex === -1) return rawTag;
1211
1247
  return `${rawTag.slice(0, closeIndex)}/>`;
1212
1248
  }
1249
+ function escapeRegExp(value) {
1250
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1251
+ }
1213
1252
  function isLikelyMdxComponent(tagName) {
1214
1253
  const first = tagName.charAt(0);
1215
1254
  return first.toUpperCase() === first && first.toLowerCase() !== first;
@@ -1490,6 +1529,12 @@ function createBlockSnapshot(block) {
1490
1529
  }
1491
1530
  }
1492
1531
  var listInlineParser = new InlineParser();
1532
+ var LIST_STREAMING_ANTICIPATION = {
1533
+ inline: true,
1534
+ mathInline: true,
1535
+ mathBlock: true,
1536
+ regex: true
1537
+ };
1493
1538
  function enrichListSnapshot(block, snapshot) {
1494
1539
  const raw = block.payload.raw ?? "";
1495
1540
  const baseOffset = block.payload.range?.from ?? 0;
@@ -1596,7 +1641,7 @@ function buildListItemSnapshot(block, listItemNode, ordered, index, id, baseOffs
1596
1641
  const nestedId = `${id}::list:${subListIndex++}`;
1597
1642
  const nestedOrdered = name === "OrderedList";
1598
1643
  const nestedSnapshot = buildListNodeSnapshot(block, cursor.node, nestedOrdered, nestedId, baseOffset, raw);
1599
- if (Array.isArray(nestedSnapshot.children) && nestedSnapshot.children.length > 0) {
1644
+ if (nestedSnapshot && Array.isArray(nestedSnapshot.children) && nestedSnapshot.children.length > 0) {
1600
1645
  childSnapshots.push(nestedSnapshot);
1601
1646
  }
1602
1647
  } else if (name === "Blockquote") {
@@ -1635,21 +1680,20 @@ function buildListItemSnapshot(block, listItemNode, ordered, index, id, baseOffs
1635
1680
  return itemSnapshot;
1636
1681
  }
1637
1682
  function parseListInline(raw, options) {
1638
- if (!options?.streaming || !options.formatAnticipation) {
1683
+ if (!options?.streaming) {
1639
1684
  return listInlineParser.parse(raw);
1640
1685
  }
1641
- const prepared = prepareInlineStreamingContent(raw, { formatAnticipation: options.formatAnticipation, math: options.math });
1686
+ const effectiveFormatAnticipation = options.formatAnticipation ?? LIST_STREAMING_ANTICIPATION;
1687
+ const prepared = prepareInlineStreamingContent(raw, { formatAnticipation: effectiveFormatAnticipation, math: options.math ?? true });
1642
1688
  if (prepared.kind === "raw") {
1643
1689
  return [{ kind: "text", text: raw }];
1644
1690
  }
1645
1691
  let preparedContent = prepared.content;
1646
- let appended = prepared.appended;
1647
- const normalized = normalizeFormatAnticipation(options.formatAnticipation);
1692
+ const normalized = normalizeFormatAnticipation(effectiveFormatAnticipation);
1648
1693
  if (normalized.regex) {
1649
1694
  const regexAppend = listInlineParser.getRegexAnticipationAppend(raw);
1650
1695
  if (regexAppend) {
1651
1696
  preparedContent += regexAppend;
1652
- appended += regexAppend;
1653
1697
  }
1654
1698
  }
1655
1699
  return listInlineParser.parse(preparedContent, { cache: false });
@@ -1702,7 +1746,7 @@ function buildCodeBlockSnapshot(block, codeNode, id, baseOffset, raw, _isFenced)
1702
1746
  const segment = raw.slice(codeNode.from, codeNode.to);
1703
1747
  const normalized = stripListIndentation(segment);
1704
1748
  const { code, info: infoString, hadFence } = stripCodeFence(normalized);
1705
- const body = hadFence ? code : dedentIndentedCode2(normalized);
1749
+ const body = hadFence ? dedentFencedCodeByClosingIndent(normalized, code) : dedentIndentedCode2(normalized);
1706
1750
  const { lang, meta } = parseCodeFenceInfo(infoString);
1707
1751
  const codeBlock = {
1708
1752
  id,
@@ -1747,6 +1791,10 @@ function buildHeadingSnapshot(block, headingNode, id, baseOffset, raw) {
1747
1791
  return createBlockSnapshot(headingBlock);
1748
1792
  }
1749
1793
  function buildListNodeSnapshot(block, listNode, ordered, id, baseOffset, raw) {
1794
+ const children = buildListItemSnapshots(block, listNode, ordered, id, baseOffset, raw);
1795
+ if (children.length === 0) {
1796
+ return null;
1797
+ }
1750
1798
  return {
1751
1799
  id,
1752
1800
  type: "list",
@@ -1754,7 +1802,7 @@ function buildListNodeSnapshot(block, listNode, ordered, id, baseOffset, raw) {
1754
1802
  ordered
1755
1803
  },
1756
1804
  range: createRange(baseOffset + listNode.from, baseOffset + listNode.to),
1757
- children: buildListItemSnapshots(block, listNode, ordered, id, baseOffset, raw)
1805
+ children
1758
1806
  };
1759
1807
  }
1760
1808
  function enrichParagraphSnapshot(block, snapshot) {
@@ -1881,6 +1929,73 @@ function dedentIndentedCode2(input) {
1881
1929
  }
1882
1930
  return lines.map((line) => line.length >= minIndent ? line.slice(minIndent) : "").join("\n").trimEnd();
1883
1931
  }
1932
+ function dedentFencedCodeByClosingIndent(fencedRaw, extractedCode) {
1933
+ if (!fencedRaw || !extractedCode) {
1934
+ return extractedCode;
1935
+ }
1936
+ const lines = fencedRaw.replace(/\r\n?/g, "\n").split("\n");
1937
+ if (lines.length < 2) {
1938
+ return extractedCode;
1939
+ }
1940
+ let closingIndex = lines.length - 1;
1941
+ while (closingIndex > 0 && lines[closingIndex].trim().length === 0) {
1942
+ closingIndex -= 1;
1943
+ }
1944
+ if (closingIndex <= 0) {
1945
+ return extractedCode;
1946
+ }
1947
+ const closingMatch = lines[closingIndex].match(/^([ \t]*)```/);
1948
+ const closingIndent = closingMatch?.[1] ?? "";
1949
+ if (closingIndent.length === 0) {
1950
+ return extractedCode;
1951
+ }
1952
+ return extractedCode.split("\n").map((line) => stripIndentPrefix(line, closingIndent)).join("\n");
1953
+ }
1954
+ function stripIndentPrefix(line, prefix) {
1955
+ if (!line || !prefix) {
1956
+ return line;
1957
+ }
1958
+ if (line.startsWith(prefix)) {
1959
+ return line.slice(prefix.length);
1960
+ }
1961
+ const targetColumns = measureIndentColumns(prefix);
1962
+ if (targetColumns <= 0) {
1963
+ return line;
1964
+ }
1965
+ let index = 0;
1966
+ let columns = 0;
1967
+ while (index < line.length && columns < targetColumns) {
1968
+ const ch = line[index];
1969
+ if (ch === " ") {
1970
+ columns += 1;
1971
+ index += 1;
1972
+ continue;
1973
+ }
1974
+ if (ch === " ") {
1975
+ columns += 4;
1976
+ index += 1;
1977
+ continue;
1978
+ }
1979
+ break;
1980
+ }
1981
+ return columns >= targetColumns ? line.slice(index) : line;
1982
+ }
1983
+ function measureIndentColumns(value) {
1984
+ let columns = 0;
1985
+ for (let i = 0; i < value.length; i += 1) {
1986
+ const ch = value[i];
1987
+ if (ch === " ") {
1988
+ columns += 1;
1989
+ continue;
1990
+ }
1991
+ if (ch === " ") {
1992
+ columns += 4;
1993
+ continue;
1994
+ }
1995
+ break;
1996
+ }
1997
+ return columns;
1998
+ }
1884
1999
  function removeHeadingMarkers2(input) {
1885
2000
  return input.replace(/^#{1,6}\s+/, "").replace(/\s+={2,}\s*$|\s+-{2,}\s*$/m, "").trim();
1886
2001
  }
@@ -1941,8 +2056,9 @@ function enrichTableSnapshot(block, snapshot) {
1941
2056
  return snapshot;
1942
2057
  }
1943
2058
  function enrichCodeSnapshot(block, snapshot) {
1944
- const source = typeof block.payload.meta?.code === "string" ? block.payload.meta?.code : block.payload.raw ?? "";
1945
- const lines = extractCodeLines(source);
2059
+ const metaCode = typeof block.payload.meta?.code === "string" ? block.payload.meta?.code : null;
2060
+ const source = metaCode ?? (block.payload.raw ?? "");
2061
+ const lines = metaCode !== null ? source.length > 0 ? source.replace(/\r\n?/g, "\n").split("\n") : [] : extractCodeLines(source);
1946
2062
  const meta = block.payload.meta;
1947
2063
  const highlightedHtml = block.payload.highlightedHtml ?? "";
1948
2064
  const hasBlockHighlight = typeof block.payload.highlightedHtml === "string" && block.payload.highlightedHtml.length > 0;
@@ -3053,7 +3169,8 @@ function collectSetProps(window2, startIndex) {
3053
3169
  }
3054
3170
  entries.push({
3055
3171
  at: cloneNodePath(current.at),
3056
- props: mergedProps
3172
+ props: mergedProps,
3173
+ meta: current.meta ? { ...current.meta } : void 0
3057
3174
  });
3058
3175
  j = k;
3059
3176
  }
@@ -3071,7 +3188,8 @@ function collectSetProps(window2, startIndex) {
3071
3188
  }
3072
3189
  const batchPatch = {
3073
3190
  op: "setPropsBatch",
3074
- entries
3191
+ entries,
3192
+ meta: first.meta ? { ...first.meta } : void 0
3075
3193
  };
3076
3194
  return { patches: [batchPatch], nextIndex: j };
3077
3195
  }
@@ -3166,7 +3284,8 @@ function coalescePatchesQuadratic(patches, config = DEFAULT_COALESCE_CONFIG) {
3166
3284
  const batchEntries = [
3167
3285
  {
3168
3286
  at: cloneNodePath(current.at),
3169
- props: mergedProps
3287
+ props: mergedProps,
3288
+ meta: current.meta ? { ...current.meta } : void 0
3170
3289
  }
3171
3290
  ];
3172
3291
  let k = j;
@@ -3190,14 +3309,16 @@ function coalescePatchesQuadratic(patches, config = DEFAULT_COALESCE_CONFIG) {
3190
3309
  }
3191
3310
  batchEntries.push({
3192
3311
  at: cloneNodePath(candidate.at),
3193
- props: candidateMergedProps
3312
+ props: candidateMergedProps,
3313
+ meta: candidate.meta ? { ...candidate.meta } : void 0
3194
3314
  });
3195
3315
  k = m;
3196
3316
  }
3197
3317
  if (batchEntries.length > 1) {
3198
3318
  coalesced.push({
3199
3319
  op: "setPropsBatch",
3200
- entries: batchEntries
3320
+ entries: batchEntries,
3321
+ meta: current.meta ? { ...current.meta } : void 0
3201
3322
  });
3202
3323
  i = k;
3203
3324
  continue;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Block, NodeSnapshot, InlineNode, Patch } from './types.cjs';
2
- export { ASTInlinePlugin, CoalescingMetrics, CodeHighlightOutputMode, CodeHighlightingMode, CompiledMdxModule, DiffBlock, DiffKind, DiffLine, DiffLineKind, FormatAnticipationConfig, InlineHtmlDescriptor, InlinePlugin, LANGUAGE_ALIASES, LazyTokenizationPriority, MixedContentSegment, NodePath, PATCH_ROOT_ID, PatchMetrics, PerformanceMetrics, ProtectedRange, ProtectedRangeKind, RegexAnticipationPattern, RegexInlinePlugin, SetPropsBatchEntry, ThemedLine, ThemedToken, TocHeading, TokenLineV1, TokenSpan, TokenStyle, WorkerErrorPayload, WorkerIn, WorkerOut, WorkerPhase } from './types.cjs';
2
+ export { ASTInlinePlugin, CoalescingMetrics, CodeHighlightOutputMode, CodeHighlightingMode, CompiledMdxModule, DiffBlock, DiffKind, DiffLine, DiffLineKind, FormatAnticipationConfig, InlineHtmlDescriptor, InlinePlugin, LANGUAGE_ALIASES, LazyTokenizationPriority, MixedContentSegment, NodePath, PATCH_ROOT_ID, PatchKind, PatchMeta, PatchMetrics, PerformanceMetrics, ProtectedRange, ProtectedRangeKind, RegexAnticipationPattern, RegexInlinePlugin, SetPropsBatchEntry, ThemedLine, ThemedToken, TocHeading, TokenLineV1, TokenSpan, TokenStyle, WorkerErrorPayload, WorkerIn, WorkerOut, WorkerPhase } from './types.cjs';
3
3
  export { HighlightedLine, dedentIndentedCode, extractCodeLines, extractCodeWrapperAttributes, extractHighlightedLines, getDefaultCodeWrapperAttributes, normalizeHighlightedLines, stripCodeFence } from './code-highlighting.cjs';
4
4
  export { PerformanceTimer, StringBuffer, applyUpdate, debounce, detectMDX, generateBlockId, getBlockKey, normalizeBlockquoteText, normalizeLang, parseCodeFenceInfo, removeHeadingMarkers } from './utils.cjs';
5
5
  export { MixedContentAutoCloseHtmlOptions, MixedContentAutoCloseMdxOptions, MixedContentOptions, extractMixedContentSegments, findClosingHtmlTag, isLikelyMdxComponent } from './mixed-content.cjs';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Block, NodeSnapshot, InlineNode, Patch } from './types.js';
2
- export { ASTInlinePlugin, CoalescingMetrics, CodeHighlightOutputMode, CodeHighlightingMode, CompiledMdxModule, DiffBlock, DiffKind, DiffLine, DiffLineKind, FormatAnticipationConfig, InlineHtmlDescriptor, InlinePlugin, LANGUAGE_ALIASES, LazyTokenizationPriority, MixedContentSegment, NodePath, PATCH_ROOT_ID, PatchMetrics, PerformanceMetrics, ProtectedRange, ProtectedRangeKind, RegexAnticipationPattern, RegexInlinePlugin, SetPropsBatchEntry, ThemedLine, ThemedToken, TocHeading, TokenLineV1, TokenSpan, TokenStyle, WorkerErrorPayload, WorkerIn, WorkerOut, WorkerPhase } from './types.js';
2
+ export { ASTInlinePlugin, CoalescingMetrics, CodeHighlightOutputMode, CodeHighlightingMode, CompiledMdxModule, DiffBlock, DiffKind, DiffLine, DiffLineKind, FormatAnticipationConfig, InlineHtmlDescriptor, InlinePlugin, LANGUAGE_ALIASES, LazyTokenizationPriority, MixedContentSegment, NodePath, PATCH_ROOT_ID, PatchKind, PatchMeta, PatchMetrics, PerformanceMetrics, ProtectedRange, ProtectedRangeKind, RegexAnticipationPattern, RegexInlinePlugin, SetPropsBatchEntry, ThemedLine, ThemedToken, TocHeading, TokenLineV1, TokenSpan, TokenStyle, WorkerErrorPayload, WorkerIn, WorkerOut, WorkerPhase } from './types.js';
3
3
  export { HighlightedLine, dedentIndentedCode, extractCodeLines, extractCodeWrapperAttributes, extractHighlightedLines, getDefaultCodeWrapperAttributes, normalizeHighlightedLines, stripCodeFence } from './code-highlighting.js';
4
4
  export { PerformanceTimer, StringBuffer, applyUpdate, debounce, detectMDX, generateBlockId, getBlockKey, normalizeBlockquoteText, normalizeLang, parseCodeFenceInfo, removeHeadingMarkers } from './utils.js';
5
5
  export { MixedContentAutoCloseHtmlOptions, MixedContentAutoCloseMdxOptions, MixedContentOptions, extractMixedContentSegments, findClosingHtmlTag, isLikelyMdxComponent } from './mixed-content.js';