@stream-mdx/react 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +14 -4
  2. package/README.md +65 -15
  3. package/dist/components/bottom-stick-scroll-area.cjs +344 -0
  4. package/dist/components/bottom-stick-scroll-area.d.cts +36 -0
  5. package/dist/components/bottom-stick-scroll-area.d.ts +36 -0
  6. package/dist/components/bottom-stick-scroll-area.mjs +320 -0
  7. package/dist/components/index.cjs +503 -172
  8. package/dist/components/index.d.cts +3 -1
  9. package/dist/components/index.d.ts +3 -1
  10. package/dist/components/index.mjs +350 -22
  11. package/dist/{index-D2MYusAM.d.cts → index-BVhp_pcF.d.ts} +3 -0
  12. package/dist/{index-D2MYusAM.d.ts → index-EaeouYp2.d.cts} +3 -0
  13. package/dist/index.cjs +1472 -486
  14. package/dist/index.d.cts +6 -4
  15. package/dist/index.d.ts +6 -4
  16. package/dist/index.mjs +1256 -272
  17. package/dist/mdx-client.cjs +1 -1
  18. package/dist/mdx-client.mjs +1 -1
  19. package/dist/mdx-coordinator.cjs +28 -7
  20. package/dist/mdx-coordinator.d.cts +2 -2
  21. package/dist/mdx-coordinator.d.ts +2 -2
  22. package/dist/mdx-coordinator.mjs +28 -7
  23. package/dist/renderer/node-views.cjs +271 -26
  24. package/dist/renderer/node-views.d.cts +3 -1
  25. package/dist/renderer/node-views.d.ts +3 -1
  26. package/dist/renderer/node-views.mjs +272 -27
  27. package/dist/renderer/patch-batching.cjs +2 -0
  28. package/dist/renderer/patch-batching.d.cts +1 -1
  29. package/dist/renderer/patch-batching.d.ts +1 -1
  30. package/dist/renderer/patch-batching.mjs +2 -1
  31. package/dist/renderer/patch-commit-scheduler.cjs +82 -30
  32. package/dist/renderer/patch-commit-scheduler.d.cts +15 -3
  33. package/dist/renderer/patch-commit-scheduler.d.ts +15 -3
  34. package/dist/renderer/patch-commit-scheduler.mjs +84 -30
  35. package/dist/renderer/store.cjs +382 -36
  36. package/dist/renderer/store.d.cts +3 -0
  37. package/dist/renderer/store.d.ts +3 -0
  38. package/dist/renderer/store.mjs +382 -36
  39. package/dist/renderer.cjs +758 -114
  40. package/dist/renderer.d.cts +3 -1
  41. package/dist/renderer.d.ts +3 -1
  42. package/dist/renderer.mjs +760 -116
  43. package/dist/server.cjs +669 -71
  44. package/dist/server.d.cts +5 -0
  45. package/dist/server.d.ts +5 -0
  46. package/dist/server.mjs +670 -72
  47. package/dist/{streaming-markdown-DOc2uLiP.d.ts → streaming-markdown-CEI9eDZm.d.ts} +3 -1
  48. package/dist/{streaming-markdown-D-JcByd9.d.cts → streaming-markdown-DRisClXb.d.cts} +3 -1
  49. package/dist/streaming-markdown.cjs +788 -121
  50. package/dist/streaming-markdown.d.cts +4 -2
  51. package/dist/streaming-markdown.d.ts +4 -2
  52. package/dist/streaming-markdown.mjs +790 -123
  53. package/dist/utils/inline-html.d.cts +3 -1
  54. package/dist/utils/inline-html.d.ts +3 -1
  55. package/package.json +14 -3
  56. package/shadcn/README.md +28 -0
  57. package/shadcn/bottom-stick-scroll-area.tsx +358 -0
package/dist/server.cjs CHANGED
@@ -102,7 +102,7 @@ var init_mdx_client = __esm({
102
102
  if (block.type !== "mdx") {
103
103
  throw new Error("Block is not MDX type");
104
104
  }
105
- const cacheKey = block.id;
105
+ const cacheKey = `${block.id}\0${block.payload.raw ?? ""}`;
106
106
  const cachedPromise = this.cache.get(cacheKey);
107
107
  if (cachedPromise) {
108
108
  return cachedPromise;
@@ -972,7 +972,11 @@ var defaultInlineComponents = {
972
972
  const n = number ?? "?";
973
973
  const href = number ? `#fn:${n}` : void 0;
974
974
  const id = number ? `fnref:${n}` : void 0;
975
- return import_react6.default.createElement("sup", { className: "footnote-ref" }, import_react6.default.createElement("a", { href, id, "data-label": label }, String(n)));
975
+ return import_react6.default.createElement(
976
+ "sup",
977
+ { className: "footnote-ref" },
978
+ import_react6.default.createElement("a", { href, id, "data-label": label, style: { color: "var(--foreground)", textDecoration: "underline" } }, String(n))
979
+ );
976
980
  }
977
981
  };
978
982
  var defaultBlockComponents = {
@@ -1115,7 +1119,7 @@ var defaultBlockComponents = {
1115
1119
  dangerouslySetInnerHTML: { __html: htmlString }
1116
1120
  });
1117
1121
  },
1118
- mdx: ({ compiledRef, compiledModule, status, errorMessage, raw }) => {
1122
+ mdx: ({ compiledRef, compiledModule, status, errorMessage }) => {
1119
1123
  const [Comp, setComp] = import_react6.default.useState(null);
1120
1124
  const [internalError, setInternalError] = import_react6.default.useState(null);
1121
1125
  const [renderError, setRenderError] = import_react6.default.useState(null);
@@ -1216,7 +1220,6 @@ var defaultBlockComponents = {
1216
1220
  const waitingForVisibility = deferHydration && !shouldHydrate && Boolean(resolvedId);
1217
1221
  const pendingLabel = waitingForVisibility ? "MDX ready (hydrate on view)\u2026" : compileMode === "worker" ? "Compiling MDX (client)\u2026" : "Compiling MDX (server)\u2026";
1218
1222
  const message = failureMessage ?? (compileMode === "worker" ? "MDX compilation failed in client mode. Try switching back to server rendering or restarting the stream." : "MDX compilation failed. Try restarting the stream.");
1219
- const rawFallback = typeof raw === "string" && raw.trim().length > 0 ? import_react6.default.createElement("pre", { className: "markdown-mdx-fallback" }, raw) : null;
1220
1223
  const allowRetry = Boolean(internalError || renderError);
1221
1224
  const retryButton = allowRetry ? import_react6.default.createElement(
1222
1225
  "button",
@@ -1228,9 +1231,10 @@ var defaultBlockComponents = {
1228
1231
  "Retry hydration"
1229
1232
  ) : null;
1230
1233
  const errorPanel = import_react6.default.createElement(
1231
- "div",
1232
- { className: "markdown-mdx-error" },
1233
- import_react6.default.createElement(import_react6.default.Fragment, null, import_react6.default.createElement("strong", null, "MDX failed"), import_react6.default.createElement("div", null, message))
1234
+ import_react6.default.Fragment,
1235
+ null,
1236
+ import_react6.default.createElement("strong", null, "MDX failed"),
1237
+ import_react6.default.createElement("div", null, message)
1234
1238
  );
1235
1239
  if (Comp && !failureMessage) {
1236
1240
  return import_react6.default.createElement(
@@ -1253,14 +1257,13 @@ var defaultBlockComponents = {
1253
1257
  return import_react6.default.createElement(
1254
1258
  "div",
1255
1259
  {
1256
- className: "markdown-mdx",
1260
+ className: "markdown-mdx error",
1257
1261
  ref: containerRef,
1258
1262
  "data-mdx-ref": pendingId,
1259
1263
  "data-mdx-status": "error",
1260
1264
  "data-mdx-mode": compileMode
1261
1265
  },
1262
1266
  errorPanel,
1263
- rawFallback,
1264
1267
  retryButton
1265
1268
  );
1266
1269
  }
@@ -1287,7 +1290,16 @@ var defaultBlockComponents = {
1287
1290
  { key: item.number, id: `fn:${item.number}` },
1288
1291
  renderInlineNodes(item.inlines, defaultInlineComponents),
1289
1292
  " ",
1290
- import_react6.default.createElement("a", { href: `#fnref:${item.number}`, className: "footnote-backref", "aria-label": "Back to content" }, "\u21A9")
1293
+ import_react6.default.createElement(
1294
+ "a",
1295
+ {
1296
+ href: `#fnref:${item.number}`,
1297
+ className: "footnote-backref",
1298
+ "aria-label": "Back to content",
1299
+ style: { color: "var(--foreground)", textDecoration: "underline" }
1300
+ },
1301
+ "\u21A9"
1302
+ )
1291
1303
  )
1292
1304
  );
1293
1305
  return import_react6.default.createElement("section", { className: "footnotes" }, import_react6.default.createElement("hr", {}), import_react6.default.createElement("ol", {}, listItems));
@@ -1835,6 +1847,7 @@ var DeferredRenderContext = import_react8.default.createContext(null);
1835
1847
 
1836
1848
  // src/renderer/store.ts
1837
1849
  var import_core4 = require("@stream-mdx/core");
1850
+ var import_patch_batching = require("@stream-mdx/core/perf/patch-batching");
1838
1851
 
1839
1852
  // src/renderer/list-utils.ts
1840
1853
  function updateNodeDepth(node, depth) {
@@ -1933,12 +1946,28 @@ function snapshotToBlock(node) {
1933
1946
  }
1934
1947
  return null;
1935
1948
  }
1936
- var nodeSnapshotCache = /* @__PURE__ */ new Map();
1937
- var childrenSnapshotCache = /* @__PURE__ */ new Map();
1949
+ var nodeSnapshotCacheByMap = /* @__PURE__ */ new WeakMap();
1950
+ var childrenSnapshotCacheByMap = /* @__PURE__ */ new WeakMap();
1938
1951
  var NODE_SNAPSHOT_CACHE_LIMIT = 5e4;
1939
1952
  var CHILDREN_SNAPSHOT_CACHE_LIMIT = 5e4;
1940
1953
  var NODE_SNAPSHOT_CACHE_BUFFER = Math.max(200, Math.floor(NODE_SNAPSHOT_CACHE_LIMIT * 0.1));
1941
1954
  var CHILDREN_SNAPSHOT_CACHE_BUFFER = Math.max(200, Math.floor(CHILDREN_SNAPSHOT_CACHE_LIMIT * 0.1));
1955
+ function getNodeSnapshotCacheForMap(map) {
1956
+ let cache = nodeSnapshotCacheByMap.get(map);
1957
+ if (!cache) {
1958
+ cache = /* @__PURE__ */ new Map();
1959
+ nodeSnapshotCacheByMap.set(map, cache);
1960
+ }
1961
+ return cache;
1962
+ }
1963
+ function getChildrenSnapshotCacheForMap(map) {
1964
+ let cache = childrenSnapshotCacheByMap.get(map);
1965
+ if (!cache) {
1966
+ cache = /* @__PURE__ */ new Map();
1967
+ childrenSnapshotCacheByMap.set(map, cache);
1968
+ }
1969
+ return cache;
1970
+ }
1942
1971
  function pruneCache(cache, max, buffer) {
1943
1972
  if (cache.size <= max + buffer) return;
1944
1973
  let remaining = cache.size - max;
@@ -1948,6 +1977,14 @@ function pruneCache(cache, max, buffer) {
1948
1977
  if (remaining <= 0) break;
1949
1978
  }
1950
1979
  }
1980
+ function deleteSnapshotCacheEntry(map, id) {
1981
+ getNodeSnapshotCacheForMap(map).delete(id);
1982
+ getChildrenSnapshotCacheForMap(map).delete(id);
1983
+ }
1984
+ function clearSnapshotCaches(map) {
1985
+ getNodeSnapshotCacheForMap(map).clear();
1986
+ getChildrenSnapshotCacheForMap(map).clear();
1987
+ }
1951
1988
  var EMPTY_CHILDREN = Object.freeze([]);
1952
1989
  var EMPTY_NODE_SNAPSHOT = Object.freeze({ version: -1, node: void 0 });
1953
1990
  var EMPTY_CHILDREN_SNAPSHOT = Object.freeze({
@@ -2115,9 +2152,57 @@ function createRootRecord() {
2115
2152
  parentId: null,
2116
2153
  children: [],
2117
2154
  props: {},
2118
- version: 0
2155
+ version: 0,
2156
+ blockEpoch: 0
2119
2157
  };
2120
2158
  }
2159
+ function readNumericEpoch(value) {
2160
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
2161
+ return void 0;
2162
+ }
2163
+ return value;
2164
+ }
2165
+ function readSnapshotBlockEpoch(snapshot) {
2166
+ return readNumericEpoch(snapshot.meta?.blockEpoch);
2167
+ }
2168
+ function readPatchBlockEpoch(patch) {
2169
+ if (patch.op === "setHTML") {
2170
+ return readNumericEpoch(patch.patchMeta?.blockEpoch);
2171
+ }
2172
+ return readNumericEpoch(patch.meta?.blockEpoch);
2173
+ }
2174
+ function readPatchParseEpoch(patch) {
2175
+ if (patch.op === "setHTML") {
2176
+ return readNumericEpoch(patch.patchMeta?.parseEpoch);
2177
+ }
2178
+ return readNumericEpoch(patch.meta?.parseEpoch);
2179
+ }
2180
+ function readPatchTx(patch) {
2181
+ if (patch.op === "setHTML") {
2182
+ return readNumericEpoch(patch.patchMeta?.tx);
2183
+ }
2184
+ return readNumericEpoch(patch.meta?.tx);
2185
+ }
2186
+ function assignSubtreeBlockEpoch(map, rootId, blockEpoch, touched) {
2187
+ const root = map.get(rootId);
2188
+ if (!root) return;
2189
+ const stack = [root];
2190
+ while (stack.length > 0) {
2191
+ const node = stack.pop();
2192
+ if (!node) continue;
2193
+ if (node.blockEpoch !== blockEpoch) {
2194
+ node.blockEpoch = blockEpoch;
2195
+ node.version += 1;
2196
+ touched?.add(node.id);
2197
+ }
2198
+ for (const childId of node.children) {
2199
+ const child = map.get(childId);
2200
+ if (child) {
2201
+ stack.push(child);
2202
+ }
2203
+ }
2204
+ }
2205
+ }
2121
2206
  function ensureArray(value) {
2122
2207
  return Array.isArray(value) ? value : [];
2123
2208
  }
@@ -2177,7 +2262,7 @@ function escapeHtml2(value) {
2177
2262
  }
2178
2263
  function stripOuterLineSpan(html) {
2179
2264
  if (!html) return null;
2180
- const openTagMatch = html.match(/<span[^>]*>/i);
2265
+ const openTagMatch = html.match(/<span[^>]*class="[^"]*\bline\b[^"]*"[^>]*>/i);
2181
2266
  if (!openTagMatch) {
2182
2267
  return null;
2183
2268
  }
@@ -2226,8 +2311,7 @@ function normalizeCodeBlockChildren(nodes, parent, touched) {
2226
2311
  if (node.type !== "code-line") {
2227
2312
  anomalies.push(`type-mismatch:${node.id}:${node.type}`);
2228
2313
  nodes.delete(node.id);
2229
- nodeSnapshotCache.delete(node.id);
2230
- childrenSnapshotCache.delete(node.id);
2314
+ deleteSnapshotCacheEntry(nodes, node.id);
2231
2315
  touched.add(node.id);
2232
2316
  continue;
2233
2317
  }
@@ -2271,16 +2355,14 @@ function normalizeCodeBlockChildren(nodes, parent, touched) {
2271
2355
  const existing = nodes.get(desiredId);
2272
2356
  if (existing && existing !== node) {
2273
2357
  nodes.delete(desiredId);
2274
- nodeSnapshotCache.delete(desiredId);
2275
- childrenSnapshotCache.delete(desiredId);
2358
+ deleteSnapshotCacheEntry(nodes, desiredId);
2276
2359
  touched.add(desiredId);
2277
2360
  mutated = true;
2278
2361
  }
2279
2362
  if (node.id !== desiredId) {
2280
2363
  const previousId = node.id;
2281
2364
  nodes.delete(node.id);
2282
- nodeSnapshotCache.delete(previousId);
2283
- childrenSnapshotCache.delete(previousId);
2365
+ deleteSnapshotCacheEntry(nodes, previousId);
2284
2366
  node.id = desiredId;
2285
2367
  nodes.set(desiredId, node);
2286
2368
  touched.add(desiredId);
@@ -2311,8 +2393,7 @@ function normalizeCodeBlockChildren(nodes, parent, touched) {
2311
2393
  for (const [id, record] of nodes) {
2312
2394
  if (record.parentId === parent.id && record.type === "code-line" && !allowed.has(id)) {
2313
2395
  nodes.delete(id);
2314
- nodeSnapshotCache.delete(id);
2315
- childrenSnapshotCache.delete(id);
2396
+ deleteSnapshotCacheEntry(nodes, id);
2316
2397
  touched.add(id);
2317
2398
  mutated = true;
2318
2399
  }
@@ -2396,8 +2477,10 @@ function normalizeCodeLineProps(incoming, previous) {
2396
2477
  }
2397
2478
  function createCodeLineProps(index, text, highlight, tokens) {
2398
2479
  const safeText = typeof text === "string" ? text : "";
2399
- const sanitized = sanitizeLineInnerHtml(highlight, safeText);
2400
- const html = sanitized && sanitized.length >= safeText.length ? sanitized : escapeHtml2(safeText);
2480
+ const hasIncomingHighlight = typeof highlight === "string" && highlight.trim().length > 0;
2481
+ const sanitized = hasIncomingHighlight ? sanitizeLineInnerHtml(highlight, safeText) : null;
2482
+ const hasRenderableTokens = tokens !== void 0 && tokens !== null;
2483
+ const html = sanitized && sanitized.length >= safeText.length ? sanitized : hasRenderableTokens ? null : escapeHtml2(safeText);
2401
2484
  const props = {
2402
2485
  index,
2403
2486
  text: safeText,
@@ -2508,6 +2591,18 @@ function validateCodeBlockChildren(nodes, parent) {
2508
2591
  issues.push({ kind: "dangling-node", id });
2509
2592
  }
2510
2593
  }
2594
+ if (parent.block?.isFinalized) {
2595
+ const snapshot = (0, import_core4.createBlockSnapshot)((0, import_core4.cloneBlock)(parent.block));
2596
+ const expectedChildren = ensureArray(snapshot.children).filter((child) => child.type === "code-line");
2597
+ if (expectedChildren.length !== parent.children.length) {
2598
+ issues.push({
2599
+ kind: "line-count-mismatch",
2600
+ id: parent.id,
2601
+ expected: expectedChildren.length,
2602
+ actual: parent.children.length
2603
+ });
2604
+ }
2605
+ }
2511
2606
  return { ok: issues.length === 0, issues };
2512
2607
  }
2513
2608
  function rebuildCodeBlockFromSnapshot(nodes, parent, touched, issues) {
@@ -2524,7 +2619,7 @@ function rebuildCodeBlockFromSnapshot(nodes, parent, touched, issues) {
2524
2619
  }
2525
2620
  const snapshot = (0, import_core4.createBlockSnapshot)((0, import_core4.cloneBlock)(parent.block));
2526
2621
  removeSubtree(nodes, parent.id, touched);
2527
- const inserted = insertSnapshotAt(nodes, ancestor, index, snapshot);
2622
+ const inserted = insertSnapshotAt(nodes, ancestor, index, snapshot, touched, parent.blockEpoch);
2528
2623
  if (!inserted) {
2529
2624
  return false;
2530
2625
  }
@@ -2547,7 +2642,55 @@ function arraysEqual(a, b) {
2547
2642
  }
2548
2643
  return true;
2549
2644
  }
2550
- function buildNodeFromSnapshot(map, snapshot, parentId) {
2645
+ function expectedCodeLineId(parentId, index) {
2646
+ return `${parentId}::line:${index}`;
2647
+ }
2648
+ function collectStoreInvariantViolations(nodes) {
2649
+ const violations = [];
2650
+ for (const [id, node] of nodes) {
2651
+ if (node.type === "__root__") continue;
2652
+ if (node.children.length > 1) {
2653
+ const seen = /* @__PURE__ */ new Set();
2654
+ for (const childId of node.children) {
2655
+ if (seen.has(childId)) {
2656
+ violations.push(`duplicate child id:${id}:${childId}`);
2657
+ } else {
2658
+ seen.add(childId);
2659
+ }
2660
+ }
2661
+ }
2662
+ if (node.type === "list") {
2663
+ const parent = node.parentId ? nodes.get(node.parentId) : void 0;
2664
+ if (parent?.type === "list-item" && node.children.length === 0) {
2665
+ violations.push(`empty nested list:${id}`);
2666
+ }
2667
+ }
2668
+ if (node.type === "code") {
2669
+ for (let index = 0; index < node.children.length; index += 1) {
2670
+ const childId = node.children[index];
2671
+ const child = nodes.get(childId);
2672
+ if (!child) {
2673
+ violations.push(`missing code-line node:${node.id}:${childId}`);
2674
+ continue;
2675
+ }
2676
+ if (child.type !== "code-line") {
2677
+ violations.push(`non-code child in code block:${node.id}:${child.id}:${child.type}`);
2678
+ continue;
2679
+ }
2680
+ const expectedId = expectedCodeLineId(node.id, index);
2681
+ if (child.id !== expectedId) {
2682
+ violations.push(`code-line id mismatch:${node.id}:${child.id}:${expectedId}`);
2683
+ }
2684
+ const indexProp = typeof child.props?.index === "number" ? child.props.index : null;
2685
+ if (indexProp !== index) {
2686
+ violations.push(`code-line index mismatch:${node.id}:${child.id}:${indexProp}:${index}`);
2687
+ }
2688
+ }
2689
+ }
2690
+ }
2691
+ return violations;
2692
+ }
2693
+ function buildNodeFromSnapshot(map, snapshot, parentId, blockEpoch) {
2551
2694
  const record = {
2552
2695
  id: snapshot.id,
2553
2696
  type: snapshot.type,
@@ -2557,6 +2700,7 @@ function buildNodeFromSnapshot(map, snapshot, parentId) {
2557
2700
  meta: snapshot.meta ? { ...snapshot.meta } : void 0,
2558
2701
  range: snapshot.range ? { ...snapshot.range } : void 0,
2559
2702
  version: 0,
2703
+ blockEpoch,
2560
2704
  block: snapshotToBlock(snapshot) ?? void 0
2561
2705
  };
2562
2706
  if (record.type === "code") {
@@ -2565,7 +2709,7 @@ function buildNodeFromSnapshot(map, snapshot, parentId) {
2565
2709
  map.set(record.id, record);
2566
2710
  const childSnapshots = ensureArray(snapshot.children);
2567
2711
  for (const child of childSnapshots) {
2568
- const childRecord = buildNodeFromSnapshot(map, child, record.id);
2712
+ const childRecord = buildNodeFromSnapshot(map, child, record.id, blockEpoch);
2569
2713
  record.children.push(childRecord.id);
2570
2714
  }
2571
2715
  return record;
@@ -2593,8 +2737,7 @@ function removeSubtree(map, id, touched) {
2593
2737
  }
2594
2738
  }
2595
2739
  map.delete(node.id);
2596
- nodeSnapshotCache.delete(node.id);
2597
- childrenSnapshotCache.delete(node.id);
2740
+ deleteSnapshotCacheEntry(map, node.id);
2598
2741
  touched.add(node.id);
2599
2742
  for (const childId of node.children) {
2600
2743
  const child = map.get(childId);
@@ -2626,13 +2769,13 @@ function resolveParent(map, path) {
2626
2769
  }
2627
2770
  return map.get(path.blockId);
2628
2771
  }
2629
- function insertSnapshotAt(map, parent, index, snapshot, touched) {
2772
+ function insertSnapshotAt(map, parent, index, snapshot, touched, blockEpoch) {
2630
2773
  if (!parent) return null;
2631
2774
  const existingIndex = parent.children.indexOf(snapshot.id);
2632
2775
  if (existingIndex !== -1) {
2633
2776
  removeSubtree(map, parent.children[existingIndex], touched ?? /* @__PURE__ */ new Set());
2634
2777
  }
2635
- const record = buildNodeFromSnapshot(map, snapshot, parent.id);
2778
+ const record = buildNodeFromSnapshot(map, snapshot, parent.id, blockEpoch ?? readSnapshotBlockEpoch(snapshot) ?? parent.blockEpoch);
2636
2779
  const insertedIds = [];
2637
2780
  const queue = [record];
2638
2781
  while (queue.length > 0) {
@@ -2650,13 +2793,13 @@ function insertSnapshotAt(map, parent, index, snapshot, touched) {
2650
2793
  ensureUniqueChildren(parent, touched);
2651
2794
  return { record, insertedIds };
2652
2795
  }
2653
- function replaceSnapshotAt(map, parent, index, snapshot, touched) {
2796
+ function replaceSnapshotAt(map, parent, index, snapshot, touched, blockEpoch) {
2654
2797
  if (!parent) return;
2655
2798
  if (index < 0 || index >= parent.children.length) return;
2656
2799
  const targetId = parent.children[index];
2657
2800
  parent.children.splice(index, 1);
2658
2801
  removeSubtree(map, targetId, touched);
2659
- const inserted = insertSnapshotAt(map, parent, index, snapshot, touched);
2802
+ const inserted = insertSnapshotAt(map, parent, index, snapshot, touched, blockEpoch);
2660
2803
  if (inserted) {
2661
2804
  for (const id of inserted.insertedIds) {
2662
2805
  touched.add(id);
@@ -2676,6 +2819,24 @@ function createRendererStore(initialBlocks = []) {
2676
2819
  const listeners = /* @__PURE__ */ new Set();
2677
2820
  let notifyScheduled = false;
2678
2821
  let lastCoalescingMetrics = null;
2822
+ let invariantViolations = [];
2823
+ let transientDiagnostics = [];
2824
+ let nextBlockEpoch = 1;
2825
+ const debugCounters = {
2826
+ appendLineGuardRejected: 0,
2827
+ staleEpochRejected: 0,
2828
+ storeInvariantViolationCount: 0
2829
+ };
2830
+ const allocateBlockEpoch = (preferred) => {
2831
+ const resolved = readNumericEpoch(preferred);
2832
+ if (resolved !== void 0) {
2833
+ nextBlockEpoch = Math.max(nextBlockEpoch, resolved + 1);
2834
+ return resolved;
2835
+ }
2836
+ const epoch = nextBlockEpoch;
2837
+ nextBlockEpoch += 1;
2838
+ return epoch;
2839
+ };
2679
2840
  const getRaf = () => {
2680
2841
  if (typeof globalThis !== "undefined" && typeof globalThis.requestAnimationFrame === "function") {
2681
2842
  return globalThis.requestAnimationFrame;
@@ -2707,9 +2868,12 @@ function createRendererStore(initialBlocks = []) {
2707
2868
  }
2708
2869
  }
2709
2870
  }
2871
+ function appendDiagnostic(message) {
2872
+ transientDiagnostics = [...transientDiagnostics, message];
2873
+ invariantViolations = [...invariantViolations, message];
2874
+ }
2710
2875
  function rebuildFromBlocks(blocks) {
2711
- nodeSnapshotCache.clear();
2712
- childrenSnapshotCache.clear();
2876
+ clearSnapshotCaches(nodes);
2713
2877
  nodes = /* @__PURE__ */ new Map();
2714
2878
  const root = createRootRecord();
2715
2879
  nodes.set(root.id, root);
@@ -2718,9 +2882,12 @@ function createRendererStore(initialBlocks = []) {
2718
2882
  for (let i = 0; i < blocks.length; i++) {
2719
2883
  const block = (0, import_core4.cloneBlock)(blocks[i]);
2720
2884
  const snapshot = (0, import_core4.createBlockSnapshot)(block);
2721
- insertSnapshotAt(nodes, root, root.children.length, snapshot);
2885
+ insertSnapshotAt(nodes, root, root.children.length, snapshot, void 0, allocateBlockEpoch(readSnapshotBlockEpoch(snapshot)));
2722
2886
  }
2723
2887
  normalizeAllListDepths(nodes, /* @__PURE__ */ new Set(), import_core4.PATCH_ROOT_ID);
2888
+ transientDiagnostics = [];
2889
+ invariantViolations = collectStoreInvariantViolations(nodes);
2890
+ debugCounters.storeInvariantViolationCount += invariantViolations.length;
2724
2891
  version++;
2725
2892
  blocksDirty = true;
2726
2893
  scheduleNotify();
@@ -2785,11 +2952,57 @@ function createRendererStore(initialBlocks = []) {
2785
2952
  listDepthDirty = true;
2786
2953
  }
2787
2954
  };
2788
- const applyPropsUpdate = (at, props) => {
2955
+ const semanticTxEpochs = /* @__PURE__ */ new Map();
2956
+ const resolveBlockEpochTarget = (patch, at) => {
2957
+ if ((patch.op === "insertChild" || patch.op === "replaceChild") && at.blockId === import_core4.PATCH_ROOT_ID) {
2958
+ const nodeId = patch.node?.id;
2959
+ return nodeId ? nodes.get(nodeId) : void 0;
2960
+ }
2961
+ return nodes.get(at.blockId) ?? (at.nodeId ? nodes.get(at.nodeId) : void 0);
2962
+ };
2963
+ const rejectStaleEpochPatch = (patch, at) => {
2964
+ const expectedBlockEpoch = readPatchBlockEpoch(patch);
2965
+ const patchTx = readPatchTx(patch);
2966
+ if (expectedBlockEpoch === void 0) {
2967
+ return false;
2968
+ }
2969
+ if ((0, import_patch_batching.getPatchKind)(patch) !== "semantic" || !at) {
2970
+ return false;
2971
+ }
2972
+ const target = resolveBlockEpochTarget(patch, at);
2973
+ if (!target || target.blockEpoch === expectedBlockEpoch) {
2974
+ return false;
2975
+ }
2976
+ const txEpoch = at.blockId && patchTx !== void 0 ? semanticTxEpochs.get(at.blockId) : void 0;
2977
+ if (txEpoch && txEpoch.tx === patchTx && txEpoch.blockEpoch === target.blockEpoch) {
2978
+ return false;
2979
+ }
2980
+ debugCounters.staleEpochRejected += 1;
2981
+ appendDiagnostic(
2982
+ `stale epoch rejected:${patch.op}:${target.id}:expected=${expectedBlockEpoch}:actual=${target.blockEpoch}`
2983
+ );
2984
+ return true;
2985
+ };
2986
+ const advanceBlockEpoch = (blockId, touchedSet, preferredEpoch, tx) => {
2987
+ if (!blockId || blockId === import_core4.PATCH_ROOT_ID) {
2988
+ return null;
2989
+ }
2990
+ const blockRoot = nodes.get(blockId);
2991
+ if (!blockRoot) {
2992
+ return null;
2993
+ }
2994
+ const nextEpoch = allocateBlockEpoch(preferredEpoch);
2995
+ assignSubtreeBlockEpoch(nodes, blockRoot.id, nextEpoch, touchedSet);
2996
+ if (tx !== void 0) {
2997
+ semanticTxEpochs.set(blockRoot.id, { tx, blockEpoch: nextEpoch });
2998
+ }
2999
+ return nextEpoch;
3000
+ };
3001
+ const applyPropsUpdate = (at, props, preferredEpoch, tx, semanticUpdate = false) => {
2789
3002
  const perfStartLocal = perfEnabled ? getPerfNow() : 0;
2790
3003
  const targetId = at.nodeId ?? at.blockId;
2791
3004
  if (!targetId) return;
2792
- const target = nodes.get(targetId);
3005
+ let target = nodes.get(targetId);
2793
3006
  if (!target) return;
2794
3007
  const parentType = target.parentId ? nodes.get(target.parentId)?.type : void 0;
2795
3008
  if (debugListPatches && (isListRelated(target.type) || isListRelated(parentType))) {
@@ -2816,6 +3029,9 @@ function createRendererStore(initialBlocks = []) {
2816
3029
  propsChanged = true;
2817
3030
  }
2818
3031
  if (propsChanged) {
3032
+ if (semanticUpdate) {
3033
+ advanceBlockEpoch(at.blockId, touched, preferredEpoch, tx);
3034
+ }
2819
3035
  target.version++;
2820
3036
  touched.add(target.id);
2821
3037
  const parent = target.parentId ? nodes.get(target.parentId) : void 0;
@@ -2879,16 +3095,56 @@ function createRendererStore(initialBlocks = []) {
2879
3095
  }
2880
3096
  }
2881
3097
  let blockChanged = false;
3098
+ let typeChanged = false;
3099
+ let rebuiltFromFinalizedBlock = false;
3100
+ let blockEpochAdvanced = false;
2882
3101
  const incomingBlock = props?.block;
2883
3102
  if (incomingBlock && typeof incomingBlock === "object") {
2884
3103
  target.block = (0, import_core4.cloneBlock)(incomingBlock);
3104
+ const previousType = target.type;
3105
+ const incomingType = typeof target.block.type === "string" ? target.block.type : target.type;
3106
+ if (incomingType !== target.type) {
3107
+ target.type = incomingType;
3108
+ typeChanged = true;
3109
+ if (isListRelated(previousType) || isListRelated(target.type)) {
3110
+ markListDepthDirty(target.id);
3111
+ }
3112
+ }
2885
3113
  if (target.type === "code") {
2886
3114
  applyCodeBlockMetadata(target);
2887
3115
  }
3116
+ if (semanticUpdate) {
3117
+ advanceBlockEpoch(at.blockId, touched, preferredEpoch, tx);
3118
+ blockEpochAdvanced = true;
3119
+ }
3120
+ const shouldRebuildFinalizedTree = !at.nodeId && target.id === at.blockId && Boolean(target.block?.isFinalized) && target.type !== "code" && Boolean(target.parentId);
3121
+ if (shouldRebuildFinalizedTree) {
3122
+ const parent = target.parentId ? nodes.get(target.parentId) : void 0;
3123
+ const index = parent ? parent.children.indexOf(target.id) : -1;
3124
+ if (parent && index !== -1) {
3125
+ const snapshot = (0, import_core4.createBlockSnapshot)((0, import_core4.cloneBlock)(target.block));
3126
+ replaceSnapshotAt(nodes, parent, index, snapshot, touched, target.blockEpoch);
3127
+ mutatedBlocks = true;
3128
+ blockChanged = true;
3129
+ rebuiltFromFinalizedBlock = true;
3130
+ const rebuilt = nodes.get(target.id);
3131
+ if (isListRelated(previousType) || isListRelated(rebuilt?.type)) {
3132
+ markListDepthDirty(target.id);
3133
+ }
3134
+ target = rebuilt ?? target;
3135
+ }
3136
+ }
2888
3137
  mutatedBlocks = true;
2889
3138
  blockChanged = true;
2890
3139
  }
2891
- if (propsChanged || blockChanged) {
3140
+ if (rebuiltFromFinalizedBlock) {
3141
+ return;
3142
+ }
3143
+ if (propsChanged || blockChanged || typeChanged) {
3144
+ if (semanticUpdate && !blockEpochAdvanced) {
3145
+ advanceBlockEpoch(at.blockId, touched, preferredEpoch, tx);
3146
+ blockEpochAdvanced = true;
3147
+ }
2892
3148
  target.version++;
2893
3149
  touched.add(target.id);
2894
3150
  let parentForNormalization;
@@ -2931,13 +3187,22 @@ function createRendererStore(initialBlocks = []) {
2931
3187
  const applyPatch = (patch) => {
2932
3188
  switch (patch.op) {
2933
3189
  case "insertChild": {
3190
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
2934
3191
  const parent = resolveParent(nodes, patch.at);
2935
- const inserted = insertSnapshotAt(nodes, parent, patch.index, patch.node, touched);
3192
+ const inserted = insertSnapshotAt(
3193
+ nodes,
3194
+ parent,
3195
+ patch.index,
3196
+ patch.node,
3197
+ touched,
3198
+ allocateBlockEpoch(readPatchParseEpoch(patch) ?? readSnapshotBlockEpoch(patch.node))
3199
+ );
2936
3200
  if (inserted) {
2937
3201
  for (const id of inserted.insertedIds) {
2938
3202
  touched.add(id);
2939
3203
  }
2940
3204
  mutatedBlocks = true;
3205
+ advanceBlockEpoch(patch.at.blockId, touched, readPatchParseEpoch(patch), readPatchTx(patch));
2941
3206
  if (isListRelated(inserted.record.type) || isListRelated(parent?.type)) {
2942
3207
  markListDepthDirty(inserted.record.id);
2943
3208
  if (parent) {
@@ -2957,6 +3222,7 @@ function createRendererStore(initialBlocks = []) {
2957
3222
  break;
2958
3223
  }
2959
3224
  case "deleteChild": {
3225
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
2960
3226
  const parent = resolveParent(nodes, patch.at);
2961
3227
  if (!parent) break;
2962
3228
  if (patch.index < 0 || patch.index >= parent.children.length) break;
@@ -2968,6 +3234,7 @@ function createRendererStore(initialBlocks = []) {
2968
3234
  touched.add(parent.id);
2969
3235
  ensureUniqueChildren(parent, touched);
2970
3236
  mutatedBlocks = true;
3237
+ advanceBlockEpoch(patch.at.blockId, touched, readPatchParseEpoch(patch), readPatchTx(patch));
2971
3238
  if (isListRelated(parent.type) || isListRelated(removedNode?.type)) {
2972
3239
  markListDepthDirty(parent.id);
2973
3240
  }
@@ -2984,11 +3251,20 @@ function createRendererStore(initialBlocks = []) {
2984
3251
  break;
2985
3252
  }
2986
3253
  case "replaceChild": {
3254
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
2987
3255
  const parent = resolveParent(nodes, patch.at);
2988
3256
  if (!parent) break;
2989
- replaceSnapshotAt(nodes, parent, patch.index, patch.node, touched);
3257
+ replaceSnapshotAt(
3258
+ nodes,
3259
+ parent,
3260
+ patch.index,
3261
+ patch.node,
3262
+ touched,
3263
+ allocateBlockEpoch(readPatchParseEpoch(patch) ?? readSnapshotBlockEpoch(patch.node))
3264
+ );
2990
3265
  touched.add(parent.id);
2991
3266
  mutatedBlocks = true;
3267
+ advanceBlockEpoch(patch.at.blockId, touched, readPatchParseEpoch(patch), readPatchTx(patch));
2992
3268
  if (isListRelated(parent.type)) {
2993
3269
  markListDepthDirty(parent.id);
2994
3270
  }
@@ -3002,18 +3278,33 @@ function createRendererStore(initialBlocks = []) {
3002
3278
  break;
3003
3279
  }
3004
3280
  case "setProps": {
3005
- applyPropsUpdate(patch.at, patch.props);
3281
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
3282
+ applyPropsUpdate(patch.at, patch.props, readPatchParseEpoch(patch), readPatchTx(patch), (0, import_patch_batching.getPatchKind)(patch) === "semantic");
3006
3283
  break;
3007
3284
  }
3008
3285
  case "setPropsBatch": {
3009
3286
  const entries2 = Array.isArray(patch.entries) ? patch.entries : [];
3010
3287
  for (const entry of entries2) {
3011
3288
  if (!entry || !entry.at) continue;
3012
- applyPropsUpdate(entry.at, entry.props);
3289
+ const entryPatch = {
3290
+ op: "setProps",
3291
+ at: entry.at,
3292
+ props: entry.props,
3293
+ meta: entry.meta ?? patch.meta
3294
+ };
3295
+ if (rejectStaleEpochPatch(entryPatch, entry.at)) continue;
3296
+ applyPropsUpdate(
3297
+ entry.at,
3298
+ entry.props,
3299
+ readPatchParseEpoch(entryPatch),
3300
+ readPatchTx(entryPatch),
3301
+ (0, import_patch_batching.getPatchKind)(entryPatch) === "semantic"
3302
+ );
3013
3303
  }
3014
3304
  break;
3015
3305
  }
3016
3306
  case "finalize": {
3307
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
3017
3308
  const target = nodes.get(patch.at.nodeId ?? patch.at.blockId);
3018
3309
  if (!target || !target.block) break;
3019
3310
  if (!target.block.isFinalized) {
@@ -3022,6 +3313,17 @@ function createRendererStore(initialBlocks = []) {
3022
3313
  target.version++;
3023
3314
  touched.add(target.id);
3024
3315
  mutatedBlocks = true;
3316
+ if (!patch.at.nodeId && target.id === patch.at.blockId && target.type !== "code" && target.parentId) {
3317
+ const parent = nodes.get(target.parentId);
3318
+ const index = parent ? parent.children.indexOf(target.id) : -1;
3319
+ if (parent && index !== -1) {
3320
+ const snapshot = (0, import_core4.createBlockSnapshot)((0, import_core4.cloneBlock)(target.block));
3321
+ replaceSnapshotAt(nodes, parent, index, snapshot, touched, target.blockEpoch);
3322
+ if (isListRelated(target.type) || isListRelated(snapshot.type)) {
3323
+ markListDepthDirty(target.id);
3324
+ }
3325
+ }
3326
+ }
3025
3327
  }
3026
3328
  if (isListRelated(target.type)) {
3027
3329
  markListDepthDirty(target.id);
@@ -3035,6 +3337,7 @@ function createRendererStore(initialBlocks = []) {
3035
3337
  break;
3036
3338
  }
3037
3339
  case "reorder": {
3340
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
3038
3341
  const parent = resolveParent(nodes, patch.at);
3039
3342
  if (!parent) break;
3040
3343
  const length = parent.children.length;
@@ -3049,6 +3352,9 @@ function createRendererStore(initialBlocks = []) {
3049
3352
  parent.children.splice(to, 0, ...moved);
3050
3353
  parent.version++;
3051
3354
  touched.add(parent.id);
3355
+ if ((0, import_patch_batching.getPatchKind)(patch) === "semantic") {
3356
+ advanceBlockEpoch(patch.at.blockId, touched, readPatchParseEpoch(patch), readPatchTx(patch));
3357
+ }
3052
3358
  if (isListRelated(parent.type)) {
3053
3359
  markListDepthDirty(parent.id);
3054
3360
  }
@@ -3064,15 +3370,31 @@ function createRendererStore(initialBlocks = []) {
3064
3370
  break;
3065
3371
  }
3066
3372
  case "appendLines": {
3373
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
3067
3374
  const parent = resolveParent(nodes, patch.at);
3068
3375
  if (!parent) break;
3069
- const startIndex = Math.max(0, Math.min(patch.startIndex, parent.children.length));
3376
+ if (parent.type !== "code") {
3377
+ debugCounters.appendLineGuardRejected += 1;
3378
+ appendDiagnostic(`appendLines target not code:${parent.id}:${parent.type}`);
3379
+ break;
3380
+ }
3381
+ const requestedStartIndex = Math.max(0, patch.startIndex);
3382
+ const startIndex = Math.max(0, Math.min(requestedStartIndex, parent.children.length));
3070
3383
  const lineCount = patch.lines?.length ?? 0;
3071
3384
  if (lineCount === 0) break;
3385
+ const prefixMatches = parent.children.every((childId, index) => childId === expectedCodeLineId(parent.id, index));
3386
+ const isStrictTailAppend = requestedStartIndex === parent.children.length;
3387
+ if (!isStrictTailAppend || !prefixMatches) {
3388
+ debugCounters.appendLineGuardRejected += 1;
3389
+ appendDiagnostic(
3390
+ `appendLines guard rejected:${parent.id}:requestedStart=${requestedStartIndex}:childCount=${parent.children.length}:prefixMatches=${prefixMatches}`
3391
+ );
3392
+ break;
3393
+ }
3072
3394
  const originalChildren = parent.children.slice();
3073
3395
  const insertedIds = [];
3074
3396
  const insertedSet = /* @__PURE__ */ new Set();
3075
- const isTrailingAppend = startIndex >= originalChildren.length;
3397
+ const isTrailingAppend = true;
3076
3398
  let parentChildrenChanged = false;
3077
3399
  let codeLinesMutated = false;
3078
3400
  for (let offset = 0; offset < lineCount; offset++) {
@@ -3114,7 +3436,8 @@ function createRendererStore(initialBlocks = []) {
3114
3436
  parentId: parent.id,
3115
3437
  children: [],
3116
3438
  props: nextLineProps,
3117
- version: 0
3439
+ version: 0,
3440
+ blockEpoch: parent.blockEpoch
3118
3441
  };
3119
3442
  nodes.set(lineId, record);
3120
3443
  touched.add(lineId);
@@ -3164,6 +3487,7 @@ function createRendererStore(initialBlocks = []) {
3164
3487
  break;
3165
3488
  }
3166
3489
  case "setHTML": {
3490
+ if (rejectStaleEpochPatch(patch, patch.at)) break;
3167
3491
  const target = nodes.get(patch.at.nodeId ?? patch.at.blockId);
3168
3492
  if (!target) break;
3169
3493
  const sanitized = patch.sanitized ? patch.html : (0, import_core4.sanitizeHTML)(patch.html);
@@ -3206,6 +3530,30 @@ function createRendererStore(initialBlocks = []) {
3206
3530
  applyPatch(patch);
3207
3531
  }
3208
3532
  }
3533
+ const finalizedTableRoots = /* @__PURE__ */ new Set();
3534
+ for (const touchedId of touched) {
3535
+ let cursor = nodes.get(touchedId);
3536
+ while (cursor && cursor.id !== import_core4.PATCH_ROOT_ID) {
3537
+ if (cursor.type === "table" && cursor.block?.isFinalized && cursor.parentId) {
3538
+ finalizedTableRoots.add(cursor.id);
3539
+ break;
3540
+ }
3541
+ cursor = cursor.parentId ? nodes.get(cursor.parentId) : void 0;
3542
+ }
3543
+ }
3544
+ if (finalizedTableRoots.size > 0) {
3545
+ for (const tableId of finalizedTableRoots) {
3546
+ const tableNode = nodes.get(tableId);
3547
+ if (!tableNode || !tableNode.block || !tableNode.parentId) continue;
3548
+ const parent = nodes.get(tableNode.parentId);
3549
+ if (!parent) continue;
3550
+ const index = parent.children.indexOf(tableNode.id);
3551
+ if (index === -1) continue;
3552
+ const snapshot = (0, import_core4.createBlockSnapshot)((0, import_core4.cloneBlock)(tableNode.block));
3553
+ replaceSnapshotAt(nodes, parent, index, snapshot, touched, tableNode.blockEpoch);
3554
+ mutatedBlocks = true;
3555
+ }
3556
+ }
3209
3557
  if (mutatedBlocks) {
3210
3558
  blocksDirty = true;
3211
3559
  }
@@ -3226,6 +3574,8 @@ function createRendererStore(initialBlocks = []) {
3226
3574
  normalizeListDepthsForIds(nodes, touched, listDepthDirtyNodes, import_core4.PATCH_ROOT_ID);
3227
3575
  }
3228
3576
  }
3577
+ invariantViolations = [...collectStoreInvariantViolations(nodes), ...transientDiagnostics];
3578
+ debugCounters.storeInvariantViolationCount += invariantViolations.length;
3229
3579
  if (touched.size > 0 || mutatedBlocks) {
3230
3580
  version++;
3231
3581
  scheduleNotify();
@@ -3274,6 +3624,7 @@ function createRendererStore(initialBlocks = []) {
3274
3624
  if (!record) {
3275
3625
  return EMPTY_NODE_SNAPSHOT;
3276
3626
  }
3627
+ const nodeSnapshotCache = getNodeSnapshotCacheForMap(nodes);
3277
3628
  const cached = nodeSnapshotCache.get(id);
3278
3629
  if (cached && cached.version === record.version && cached.node === record) {
3279
3630
  return cached;
@@ -3293,6 +3644,7 @@ function createRendererStore(initialBlocks = []) {
3293
3644
  if (!record) {
3294
3645
  return EMPTY_CHILDREN_SNAPSHOT;
3295
3646
  }
3647
+ const childrenSnapshotCache = getChildrenSnapshotCacheForMap(nodes);
3296
3648
  const cached = childrenSnapshotCache.get(id);
3297
3649
  if (cached && cached.version === record.version) {
3298
3650
  return cached;
@@ -3302,6 +3654,12 @@ function createRendererStore(initialBlocks = []) {
3302
3654
  pruneCache(childrenSnapshotCache, CHILDREN_SNAPSHOT_CACHE_LIMIT, CHILDREN_SNAPSHOT_CACHE_BUFFER);
3303
3655
  return snapshot;
3304
3656
  },
3657
+ getInvariantViolations() {
3658
+ return invariantViolations.slice();
3659
+ },
3660
+ getDebugCounters() {
3661
+ return { ...debugCounters };
3662
+ },
3305
3663
  getLastCoalescingMetrics() {
3306
3664
  return lastCoalescingMetrics;
3307
3665
  },
@@ -3750,8 +4108,10 @@ var ListBlockView = import_react10.default.memo(
3750
4108
  isFinalized: block?.isFinalized ?? false
3751
4109
  });
3752
4110
  }, [listDebugEnabled, blockId, depth, ordered, childIds, listItemIds, store, block?.isFinalized]);
3753
- const listStyle = import_react10.default.useMemo(() => {
3754
- if (!ordered || listItemIds.length === 0) return void 0;
4111
+ const { listStyle, markerDigits } = import_react10.default.useMemo(() => {
4112
+ if (!ordered || listItemIds.length === 0) {
4113
+ return { listStyle: void 0, markerDigits: 1 };
4114
+ }
3755
4115
  let maxDigits = 1;
3756
4116
  listItemIds.forEach((childId, index) => {
3757
4117
  const childNode = store.getNode(childId);
@@ -3763,9 +4123,14 @@ var ListBlockView = import_react10.default.memo(
3763
4123
  const digits = digitMatch ? digitMatch.length : text.length;
3764
4124
  if (digits > maxDigits) maxDigits = digits;
3765
4125
  });
3766
- const baseIndent = depth === 0 ? "2.5rem" : depth === 1 ? "2rem" : "1.5rem";
4126
+ const baseIndent = depth === 0 ? "2rem" : depth === 1 ? "1.75rem" : "1.5rem";
4127
+ const extraDigits = Math.max(0, maxDigits - 1);
3767
4128
  return {
3768
- ["--list-indent"]: `calc(${baseIndent} + ${maxDigits}ch)`
4129
+ markerDigits: maxDigits,
4130
+ listStyle: {
4131
+ ["--list-indent"]: extraDigits > 0 ? `calc(${baseIndent} + ${extraDigits}ch)` : baseIndent,
4132
+ ["--list-marker-digits"]: String(maxDigits)
4133
+ }
3769
4134
  };
3770
4135
  }, [ordered, listItemIds, store, depth]);
3771
4136
  if (listItemIds.length === 0) return null;
@@ -3774,6 +4139,7 @@ var ListBlockView = import_react10.default.memo(
3774
4139
  {
3775
4140
  className: `markdown-list ${ordered ? "ordered" : "unordered"}`,
3776
4141
  "data-list-depth": depth,
4142
+ "data-marker-digits": ordered ? markerDigits : void 0,
3777
4143
  "data-list-id": listDebugEnabled ? blockId : void 0,
3778
4144
  style: listStyle,
3779
4145
  children: listItemIds.map((childId, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -3955,6 +4321,7 @@ var CodeBlockView = import_react10.default.memo(({ store, blockId, registry }) =
3955
4321
  deferredConfig ? { ...deferredConfig, enabled: true } : { enabled: false }
3956
4322
  );
3957
4323
  if (!node || !node.block) return null;
4324
+ const blockIsFinalized = node.block.isFinalized;
3958
4325
  const nodeVersion = node.version;
3959
4326
  const lines = import_react10.default.useMemo(() => {
3960
4327
  const seenIds = /* @__PURE__ */ new Set();
@@ -3985,13 +4352,25 @@ var CodeBlockView = import_react10.default.memo(({ store, blockId, registry }) =
3985
4352
  const lang = typeof node.props?.lang === "string" ? node.props?.lang : node.block.payload.meta?.lang;
3986
4353
  const preAttrs = node.props?.preAttrs ?? void 0;
3987
4354
  const codeAttrs = node.props?.codeAttrs ?? void 0;
4355
+ const blockMeta = node.block.payload.meta ?? void 0;
4356
+ const lazyEnabled = Boolean(blockMeta?.lazyTokenization);
4357
+ const lazyTokenizedUntil = typeof blockMeta?.lazyTokenizedUntil === "number" ? blockMeta.lazyTokenizedUntil : 0;
4358
+ const lazyHighlightedLines = Array.isArray(blockMeta?.highlightedLines) ? blockMeta.highlightedLines.length : 0;
4359
+ const lazyTokenLines = Array.isArray(blockMeta?.tokenLines) ? blockMeta.tokenLines.length : 0;
4360
+ const lazySourceTextLineCount = import_react10.default.useMemo(() => {
4361
+ const raw = typeof node.block?.payload.raw === "string" ? node.block.payload.raw : "";
4362
+ const metaCode = typeof blockMeta?.code === "string" ? blockMeta.code : null;
4363
+ const sourceText = resolveCodeSourceText(raw, metaCode);
4364
+ if (sourceText.length === 0) return 0;
4365
+ return sourceText.split("\n").length;
4366
+ }, [blockMeta, node.block?.payload.raw]);
4367
+ const totalLazyLineCount = Math.max(lines.length, lazyHighlightedLines, lazyTokenLines, lazySourceTextLineCount);
3988
4368
  const virtualizationConfig = DEFAULT_VIRTUALIZED_CODE_CONFIG;
3989
4369
  const virtualizationDisabled = typeof process !== "undefined" && typeof process.env === "object" && process.env.STREAM_MDX_DISABLE_VIRTUALIZED_CODE === "true";
3990
- const shouldVirtualize = !virtualizationDisabled && virtualizationConfig.enabled && lines.length >= virtualizationConfig.virtualizeThreshold;
4370
+ const canVirtualize = node.block.isFinalized && !lazyEnabled;
4371
+ const shouldVirtualize = canVirtualize && !virtualizationDisabled && virtualizationConfig.enabled && lines.length >= virtualizationConfig.virtualizeThreshold;
3991
4372
  const virtualization = useVirtualizedCode(lines, shouldVirtualize ? virtualizationConfig : { ...virtualizationConfig, enabled: false });
3992
4373
  const highlightRequester = useCodeHighlightRequester();
3993
- const lazyEnabled = Boolean(node.block.payload.meta?.lazyTokenization);
3994
- const lazyTokenizedUntil = typeof node.block.payload.meta?.lazyTokenizedUntil === "number" ? node.block.payload.meta.lazyTokenizedUntil : 0;
3995
4374
  const lastRangeRef = import_react10.default.useRef(null);
3996
4375
  const rafRef = import_react10.default.useRef(null);
3997
4376
  const idleRef = import_react10.default.useRef(null);
@@ -4072,29 +4451,111 @@ var CodeBlockView = import_react10.default.memo(({ store, blockId, registry }) =
4072
4451
  virtualization.window.startIndex,
4073
4452
  virtualization.window.endIndex
4074
4453
  ]);
4075
- const highlightedHtml = import_react10.default.useMemo(() => {
4076
- if (!shouldVirtualize && node.block?.payload.highlightedHtml) {
4077
- return node.block.payload.highlightedHtml;
4454
+ import_react10.default.useEffect(() => {
4455
+ if (shouldVirtualize || !blockIsFinalized || !lazyEnabled || !highlightRequester) return;
4456
+ if (totalLazyLineCount <= 0 || lazyTokenizedUntil >= totalLazyLineCount) return;
4457
+ const nextRange = {
4458
+ visibleStart: 0,
4459
+ visibleEnd: totalLazyLineCount,
4460
+ prefetchStart: 0,
4461
+ prefetchEnd: totalLazyLineCount,
4462
+ tokenizedUntil: lazyTokenizedUntil
4463
+ };
4464
+ const last = lastRangeRef.current;
4465
+ if (last && last.visibleStart === nextRange.visibleStart && last.visibleEnd === nextRange.visibleEnd && last.prefetchStart === nextRange.prefetchStart && last.prefetchEnd === nextRange.prefetchEnd && last.tokenizedUntil === nextRange.tokenizedUntil) {
4466
+ return;
4078
4467
  }
4468
+ lastRangeRef.current = nextRange;
4469
+ if (rafRef.current !== null && typeof cancelAnimationFrame === "function") {
4470
+ cancelAnimationFrame(rafRef.current);
4471
+ }
4472
+ const requestFullHighlight = () => {
4473
+ highlightRequester({
4474
+ blockId,
4475
+ startLine: 0,
4476
+ endLine: totalLazyLineCount,
4477
+ priority: "visible",
4478
+ reason: "finalize-full"
4479
+ });
4480
+ rafRef.current = null;
4481
+ };
4482
+ if (typeof requestAnimationFrame === "function") {
4483
+ rafRef.current = requestAnimationFrame(requestFullHighlight);
4484
+ } else {
4485
+ requestFullHighlight();
4486
+ }
4487
+ }, [blockId, blockIsFinalized, highlightRequester, lazyEnabled, lazyTokenizedUntil, shouldVirtualize, totalLazyLineCount]);
4488
+ const highlightedHtml = import_react10.default.useMemo(() => {
4489
+ const blockHtml = node.block?.payload.highlightedHtml ?? "";
4490
+ const raw = typeof node.block?.payload.raw === "string" ? node.block.payload.raw : "";
4491
+ const metaCode = typeof node.block?.payload.meta?.code === "string" ? (node.block?.payload.meta).code : null;
4492
+ const sourceText = resolveCodeSourceText(raw, metaCode);
4493
+ const sourceEndsWithNewline = sourceText.endsWith("\n");
4494
+ const sourceLines = sourceText.length > 0 ? sourceText.split("\n") : [];
4495
+ const rawFallbackLines = buildCodeFallbackLines(raw);
4496
+ const deterministicWrapperAttrs = (0, import_core5.getDefaultCodeWrapperAttributes)(lang);
4497
+ const composedPreAttrs = deterministicWrapperAttrs.preAttrs;
4498
+ const composedCodeAttrs = deterministicWrapperAttrs.codeAttrs;
4499
+ const shouldAppendTerminalNewline = (renderedLines) => {
4500
+ if (!sourceEndsWithNewline || renderedLines.length === 0 || lines.length === 0) {
4501
+ return false;
4502
+ }
4503
+ const lastRendered = renderedLines[renderedLines.length - 1];
4504
+ const lastKnown = lines[lines.length - 1];
4505
+ if (!lastRendered || !lastKnown || lastRendered.index !== lastKnown.index || lastRendered.text === "") {
4506
+ return false;
4507
+ }
4508
+ const expectedRenderedLineCount = Math.max(0, sourceLines.length - 1);
4509
+ const expectedLastRenderedLine = sourceLines.length >= 2 ? sourceLines[sourceLines.length - 2] ?? "" : "";
4510
+ return renderedLines.length === expectedRenderedLineCount && lastRendered.text === expectedLastRenderedLine;
4511
+ };
4079
4512
  if (lines.length > 0) {
4080
4513
  const hasLineHtml = lines.some((line) => typeof line.html === "string" && line.html.length > 0);
4081
- if (!hasLineHtml && node.block?.payload.highlightedHtml) {
4082
- return node.block.payload.highlightedHtml;
4514
+ if (!hasLineHtml && blockHtml) {
4515
+ return blockHtml;
4083
4516
  }
4084
4517
  if (shouldVirtualize) {
4085
- return composeHighlightedHtml(virtualization.window.visibleLines, preAttrs, codeAttrs);
4518
+ return composeHighlightedHtml(
4519
+ virtualization.window.visibleLines,
4520
+ composedPreAttrs,
4521
+ composedCodeAttrs,
4522
+ shouldAppendTerminalNewline(virtualization.window.visibleLines)
4523
+ );
4086
4524
  }
4087
- return composeHighlightedHtml(lines, preAttrs, codeAttrs);
4525
+ return composeHighlightedHtml(lines, composedPreAttrs, composedCodeAttrs, shouldAppendTerminalNewline(lines));
4526
+ }
4527
+ if (!shouldVirtualize && blockHtml) {
4528
+ const hasHighlightedLines = blockHtml.includes('class="line"');
4529
+ if (!hasHighlightedLines && rawFallbackLines.length > 0) {
4530
+ return composeHighlightedHtml(
4531
+ rawFallbackLines,
4532
+ composedPreAttrs,
4533
+ composedCodeAttrs,
4534
+ sourceEndsWithNewline && rawFallbackLines[rawFallbackLines.length - 1]?.text !== ""
4535
+ );
4536
+ }
4537
+ return blockHtml;
4088
4538
  }
4089
- const blockHtml = node.block?.payload.highlightedHtml ?? "";
4090
4539
  if (blockHtml?.trim().match(/^```[\w-]*\s*$/)) {
4091
- const raw = node.block?.payload.raw ?? "";
4092
- if (raw.trim().length > 0) {
4093
- return composeHighlightedHtml([{ index: 0, text: raw, html: null, id: "temp" }], preAttrs, codeAttrs);
4540
+ if (rawFallbackLines.length > 0) {
4541
+ return composeHighlightedHtml(
4542
+ rawFallbackLines,
4543
+ composedPreAttrs,
4544
+ composedCodeAttrs,
4545
+ sourceEndsWithNewline && rawFallbackLines[rawFallbackLines.length - 1]?.text !== ""
4546
+ );
4094
4547
  }
4095
4548
  }
4549
+ if (!blockHtml && rawFallbackLines.length > 0) {
4550
+ return composeHighlightedHtml(
4551
+ rawFallbackLines,
4552
+ composedPreAttrs,
4553
+ composedCodeAttrs,
4554
+ sourceEndsWithNewline && rawFallbackLines[rawFallbackLines.length - 1]?.text !== ""
4555
+ );
4556
+ }
4096
4557
  return blockHtml;
4097
- }, [shouldVirtualize, node.block?.payload.highlightedHtml, lines, preAttrs, codeAttrs, virtualization.window.visibleLines, node.block?.payload.raw]);
4558
+ }, [blockIsFinalized, shouldVirtualize, node.block?.payload.highlightedHtml, node.block?.payload.meta, lines, lang, virtualization.window.visibleLines, node.block?.payload.raw]);
4098
4559
  const rendered = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
4099
4560
  CodeComponent,
4100
4561
  {
@@ -4106,13 +4567,19 @@ var CodeBlockView = import_react10.default.memo(({ store, blockId, registry }) =
4106
4567
  codeAttrs
4107
4568
  }
4108
4569
  );
4570
+ const effectiveLineCount = import_react10.default.useMemo(() => {
4571
+ if (lines.length > 0) {
4572
+ return lines.length;
4573
+ }
4574
+ return 1;
4575
+ }, [lines.length]);
4109
4576
  const codeFrameClass = "not-prose flex flex-col rounded-lg border border-input pt-1 font-mono text-sm";
4110
4577
  const codeMetricsAttrs = {
4111
4578
  "data-code-block": "true",
4112
4579
  "data-block-id": blockId,
4113
4580
  "data-code-virtualized": shouldVirtualize ? "true" : "false",
4114
- "data-code-total-lines": String(lines.length),
4115
- "data-code-mounted-lines": String(shouldVirtualize ? virtualization.window.mountedLines : lines.length),
4581
+ "data-code-total-lines": String(effectiveLineCount),
4582
+ "data-code-mounted-lines": String(shouldVirtualize ? virtualization.window.mountedLines : effectiveLineCount),
4116
4583
  "data-code-window-size": String(virtualization.config.windowSize)
4117
4584
  };
4118
4585
  const codeView = shouldVirtualize ? (() => {
@@ -4158,21 +4625,152 @@ var CodeBlockView = import_react10.default.memo(({ store, blockId, registry }) =
4158
4625
  return codeView;
4159
4626
  });
4160
4627
  CodeBlockView.displayName = "CodeBlockView";
4161
- function composeHighlightedHtml(lines, preAttrs, codeAttrs) {
4628
+ function composeHighlightedHtml(lines, preAttrs, codeAttrs, appendTerminalNewline = false) {
4162
4629
  const lineMarkup = lines.map((line) => {
4163
- const content = line.html ?? escapeHtml3(line.text);
4630
+ const content = line.html ?? renderTokenLineHtml(line.tokens) ?? escapeHtml3(line.text);
4164
4631
  const dataLine = Number.isFinite(line.index) ? ` data-line="${line.index + 1}"` : "";
4165
4632
  return `<span class="line"${dataLine}>${content}</span>`;
4166
4633
  }).join("\n");
4167
4634
  const preAttr = attrsToString(preAttrs);
4168
4635
  const codeAttr = attrsToString(codeAttrs);
4169
- return `<pre${preAttr}><code${codeAttr}>${lineMarkup}
4170
- </code></pre>`;
4636
+ return `<pre${preAttr}><code${codeAttr}>${lineMarkup}${appendTerminalNewline ? "\n" : ""}</code></pre>`;
4637
+ }
4638
+ var TOKEN_FONT_STYLE_ITALIC = 1;
4639
+ var TOKEN_FONT_STYLE_BOLD = 2;
4640
+ var TOKEN_FONT_STYLE_UNDERLINE = 4;
4641
+ var TOKEN_FONT_STYLE_STRIKETHROUGH = 8;
4642
+ function renderTokenLineHtml(tokens) {
4643
+ if (!tokens || !Array.isArray(tokens.spans)) {
4644
+ return null;
4645
+ }
4646
+ return mergeRenderableTokenSpans(tokens.spans).map((span) => renderTokenSpanHtml(span)).join("");
4647
+ }
4648
+ function renderTokenSpanHtml(span) {
4649
+ const dark = span.v?.dark;
4650
+ const light = span.v?.light;
4651
+ const single = span.s;
4652
+ const styles = [];
4653
+ if (dark?.fg) {
4654
+ styles.push(`--shiki-dark:${dark.fg}`);
4655
+ }
4656
+ if (light?.fg) {
4657
+ styles.push(`--shiki-light:${light.fg}`);
4658
+ }
4659
+ if (!light?.fg && !dark?.fg && single?.fg) {
4660
+ styles.push(`color:${single.fg}`);
4661
+ }
4662
+ const fontStyle = light?.fs ?? dark?.fs ?? single?.fs;
4663
+ if (typeof fontStyle === "number" && fontStyle > 0) {
4664
+ if (fontStyle & TOKEN_FONT_STYLE_ITALIC) {
4665
+ styles.push("font-style: italic");
4666
+ }
4667
+ if (fontStyle & TOKEN_FONT_STYLE_BOLD) {
4668
+ styles.push("font-weight: bold");
4669
+ }
4670
+ const decorations = [];
4671
+ if (fontStyle & TOKEN_FONT_STYLE_UNDERLINE) {
4672
+ decorations.push("underline");
4673
+ }
4674
+ if (fontStyle & TOKEN_FONT_STYLE_STRIKETHROUGH) {
4675
+ decorations.push("line-through");
4676
+ }
4677
+ if (decorations.length > 0) {
4678
+ styles.push(`text-decoration: ${decorations.join(" ")}`);
4679
+ }
4680
+ }
4681
+ const content = escapeHtml3(span.t ?? "");
4682
+ if (styles.length === 0) {
4683
+ return content;
4684
+ }
4685
+ return `<span style="${styles.join(";")}">${content}</span>`;
4686
+ }
4687
+ function mergeRenderableTokenSpans(spans) {
4688
+ if (spans.length <= 1) {
4689
+ return spans.slice();
4690
+ }
4691
+ const merged = [];
4692
+ let pendingWhitespace = "";
4693
+ for (const span of spans) {
4694
+ const text = typeof span.t === "string" ? span.t : "";
4695
+ if (/^[\t ]+$/.test(text)) {
4696
+ pendingWhitespace += text;
4697
+ continue;
4698
+ }
4699
+ const nextSpan = pendingWhitespace ? { ...span, t: `${pendingWhitespace}${text}` } : span;
4700
+ pendingWhitespace = "";
4701
+ const previous = merged[merged.length - 1];
4702
+ if (previous && sameTokenSpanStyle(previous, nextSpan)) {
4703
+ previous.t += nextSpan.t;
4704
+ } else {
4705
+ merged.push({ ...nextSpan });
4706
+ }
4707
+ }
4708
+ if (pendingWhitespace) {
4709
+ const previous = merged[merged.length - 1];
4710
+ if (previous) {
4711
+ previous.t += pendingWhitespace;
4712
+ } else {
4713
+ merged.push({ t: pendingWhitespace });
4714
+ }
4715
+ }
4716
+ return merged;
4717
+ }
4718
+ function sameTokenSpanStyle(a, b) {
4719
+ const variantKeys = /* @__PURE__ */ new Set([
4720
+ ...Object.keys(a.v ?? {}),
4721
+ ...Object.keys(b.v ?? {})
4722
+ ]);
4723
+ for (const key of variantKeys) {
4724
+ const aStyle = a.v?.[key];
4725
+ const bStyle = b.v?.[key];
4726
+ if ((aStyle?.fg ?? null) !== (bStyle?.fg ?? null)) return false;
4727
+ if ((aStyle?.bg ?? null) !== (bStyle?.bg ?? null)) return false;
4728
+ if ((aStyle?.fs ?? null) !== (bStyle?.fs ?? null)) return false;
4729
+ }
4730
+ if ((a.s?.fg ?? null) !== (b.s?.fg ?? null)) return false;
4731
+ if ((a.s?.bg ?? null) !== (b.s?.bg ?? null)) return false;
4732
+ if ((a.s?.fs ?? null) !== (b.s?.fs ?? null)) return false;
4733
+ return true;
4734
+ }
4735
+ function resolveCodeSourceText(raw, metaCode) {
4736
+ if (metaCode !== null) {
4737
+ return metaCode.replace(/\r\n?/g, "\n");
4738
+ }
4739
+ if (!raw) return "";
4740
+ const normalized = raw.replace(/\r\n?/g, "\n");
4741
+ const fenced = (0, import_core5.stripCodeFence)(normalized);
4742
+ return fenced.hadFence ? fenced.code : normalized;
4171
4743
  }
4172
4744
  function attrsToString(attrs) {
4173
4745
  if (!attrs) return "";
4174
4746
  return Object.entries(attrs).map(([key, value]) => ` ${key}="${escapeAttribute(value)}"`).join("");
4175
4747
  }
4748
+ function buildCodeFallbackLines(raw) {
4749
+ const normalized = normalizeCodeFallbackRaw(raw);
4750
+ if (normalized.length === 0) {
4751
+ if (/^\s*```[^\n]*\n?\s*$/.test(raw)) {
4752
+ return [{ id: "temp-0", index: 0, text: "", html: null }];
4753
+ }
4754
+ return [];
4755
+ }
4756
+ return normalized.split("\n").map((line, index) => ({
4757
+ id: `temp-${index}`,
4758
+ index,
4759
+ text: line,
4760
+ html: null
4761
+ }));
4762
+ }
4763
+ function normalizeCodeFallbackRaw(raw) {
4764
+ if (!raw) return "";
4765
+ const normalized = raw.replace(/\r\n?/g, "\n");
4766
+ const fenced = (0, import_core5.stripCodeFence)(normalized);
4767
+ if (!fenced.hadFence) {
4768
+ return normalized.replace(/\n+$/, "");
4769
+ }
4770
+ let code = fenced.code;
4771
+ code = code.replace(/\n+$/, "");
4772
+ return code;
4773
+ }
4176
4774
  function escapeHtml3(value) {
4177
4775
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4178
4776
  }