@portabletext/markdown 2.1.0 → 2.2.1

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.js CHANGED
@@ -1,11 +1,36 @@
1
+ import { cleanupEfficiency, makeDiff } from "@sanity/diff-match-patch";
1
2
  import { compileSchema, defineSchema, isSpan, isTextBlock, isTypedObject } from "@portabletext/schema";
2
3
  import { buildMarksTree, isPortableTextBlock, isPortableTextListItemBlock, isPortableTextSpan, isPortableTextToolkitSpan, isPortableTextToolkitTextNode, spanToPlainText } from "@portabletext/toolkit";
3
- import LinkifyIt from "linkify-it";
4
- import { alert } from "@mdit/plugin-alert";
4
+ import { LinkifyIt } from "linkify-it";
5
5
  import markdownit from "markdown-it";
6
+ import { alert } from "@mdit/plugin-alert";
6
7
  function defaultKeyGenerator() {
7
8
  return randomKey(12);
8
9
  }
10
+ /**
11
+ * Wraps a caller-supplied key generator so one conversion can never
12
+ * mint the same key twice. Keys attach content to its identity at
13
+ * creation time (a span's `marks` entry points at the mark definition
14
+ * minted with the same key), so a generator that repeats a key does
15
+ * not just violate sibling uniqueness, it makes ownership ambiguous in
16
+ * a way no later pass can repair: two definitions sharing a key leave
17
+ * every referencing span attributable to either. Bounded retries, then
18
+ * deterministic suffixing, mirroring how sibling-uniqueness repair
19
+ * treats a generator that keeps returning claimed keys.
20
+ */
21
+ function uniqueKeyGenerator(generator) {
22
+ let mintedKeys = /* @__PURE__ */ new Set();
23
+ return () => {
24
+ let candidate = generator();
25
+ for (let attempt = 0; attempt < 3 && mintedKeys.has(candidate); attempt++) candidate = generator();
26
+ if (mintedKeys.has(candidate)) {
27
+ let base = candidate, suffix = 2;
28
+ for (; mintedKeys.has(`${base}-${suffix}`);) suffix++;
29
+ candidate = `${base}-${suffix}`;
30
+ }
31
+ return mintedKeys.add(candidate), candidate;
32
+ };
33
+ }
9
34
  const getByteHexTable = (() => {
10
35
  let table;
11
36
  return () => {
@@ -96,7 +121,10 @@ function buildListIndexMap(blocks) {
96
121
  * The CommonMark ASCII punctuation set. Only these characters can be
97
122
  * backslash-escaped into a literal without changing the parsed text.
98
123
  */
99
- const ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/, ENTITY_REFERENCE = /&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#[xX][0-9a-fA-F]+);/g, BACKSLASH_BEFORE_PUNCTUATION = RegExp(`\\\\(?=${ASCII_PUNCTUATION.source})`, "g"), TILDE_RUN = /~{2,}/g, HTML_LIKE_ANGLE_BRACKET = /<(?=[a-zA-Z/!?])/g, BRACKET_BEFORE_LINK_OPEN = /\](?=[([])/g, UNICODE_PUNCTUATION_OR_SYMBOL = /^(?:\p{P}|\p{S})$/u, linkify = new LinkifyIt();
124
+ const ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/, ENTITY_REFERENCE = /&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#[xX][0-9a-fA-F]+);/g, BACKSLASH_BEFORE_PUNCTUATION = RegExp(`\\\\(?=${ASCII_PUNCTUATION.source})`, "g"), TILDE_RUN = /~{2,}/g, HTML_LIKE_ANGLE_BRACKET = /<(?=[a-zA-Z/!?])/g, BRACKET_BEFORE_LINK_OPEN = /\](?=[([])/g, UNICODE_PUNCTUATION_OR_SYMBOL = /^(?:\p{P}|\p{S})$/u, linkify = new LinkifyIt({
125
+ fuzzyLink: !0,
126
+ urlAuth: !0
127
+ });
100
128
  /**
101
129
  * Plans the escaped replacement for every plain-text leaf a block's children
102
130
  * will produce, in the exact left-to-right order `renderText` visits them
@@ -670,10 +698,10 @@ function isHtmlShaped(value) {
670
698
  * @public
671
699
  */
672
700
  const DefaultImageRenderer = (options) => {
673
- if (!isImageShaped(options.value)) return DefaultUnknownTypeRenderer(options);
701
+ if (!isImageShaped(options.value) || !linkValidator(options.value.src)) return DefaultUnknownTypeRenderer(options);
674
702
  let alt = escapeImageAndLinkText(options.value.alt ?? ""), title = options.value.title ? ` "${escapeImageAndLinkTitle(options.value.title)}"` : "";
675
703
  return `![${alt}](${options.value.src}${title})`;
676
- };
704
+ }, md = new markdownit(), linkValidator = (url) => md.validateLink(url);
677
705
  function isImageShaped(value) {
678
706
  let image = value;
679
707
  return typeof image?.src == "string" && (image.alt == null || typeof image.alt == "string") && (image.title == null || typeof image.title == "string");
@@ -715,12 +743,23 @@ const DefaultTableRenderer = (options) => {
715
743
  function renderTable(value, renderNode) {
716
744
  let rows = value.rows, alignment = Array.isArray(value.alignment) ? value.alignment : void 0, headerRow = rows.at(0);
717
745
  if (!headerRow) return "";
718
- let getCellText = (cellBlocks) => cellBlocks.map((block, index) => renderNode({
719
- node: block,
720
- index,
721
- isInline: !1,
722
- renderNode
723
- })).join(" ").trim(), lines = [], columnCount = rows.reduce((max, row) => Math.max(max, row.cells.length), 0), renderCells = (texts) => {
746
+ let getCellText = (cellBlocks) => cellBlocks.map((block, index) => {
747
+ let rendered = renderNode({
748
+ node: block,
749
+ index,
750
+ isInline: !1,
751
+ renderNode
752
+ }), rendererOptions = {
753
+ value: block,
754
+ isInline: !1,
755
+ index,
756
+ renderNode
757
+ };
758
+ return rendered === DefaultUnknownTypeRenderer(rendererOptions) ? DefaultUnknownTypeRenderer({
759
+ ...rendererOptions,
760
+ isInline: !0
761
+ }) : rendered;
762
+ }).join(" ").trim(), lines = [], columnCount = rows.reduce((max, row) => Math.max(max, row.cells.length), 0), renderCells = (texts) => {
724
763
  let padded = [...texts];
725
764
  for (; padded.length < columnCount;) padded.push("");
726
765
  return `| ${padded.join(" | ")} |`;
@@ -753,7 +792,7 @@ const DefaultCalloutRenderer = (options) => {
753
792
  index,
754
793
  isInline: !1,
755
794
  renderNode
756
- })).join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
795
+ })).filter((rendered) => rendered !== "").join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
757
796
  return `> [!${options.value.tone.toUpperCase()}]\n${prefixed}`;
758
797
  };
759
798
  function isCalloutShaped(value) {
@@ -780,20 +819,29 @@ const DefaultBlockquoteObjectRenderer = ({ value, renderNode }) => value.content
780
819
  index,
781
820
  isInline: !1,
782
821
  renderNode
783
- })).join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n"), DefaultListRenderer = ({ value, renderNode }) => {
784
- let itemSeparator = value.items.some((item) => item.content.filter((block) => block._type !== "list").length > 1) ? "\n\n" : "\n";
785
- return value.items.map((item, itemIndex) => {
786
- let marker = getListMarker(value.kind, itemIndex, item.checked), indentWidth = value.kind === "task" ? 2 : marker.length, indent = " ".repeat(indentWidth), [first, ...rest] = item.content.map((block, blockIndex) => (blockIndex === 0 && isPortableTextBlock(block) && markListItemFirstBlock(block), {
787
- isNestedList: block._type === "list",
788
- text: renderNode({
822
+ })).filter((rendered) => rendered !== "").join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n"), DefaultListRenderer = ({ value, renderNode }) => {
823
+ let renderedItems = value.items.map((item) => {
824
+ let markerLineSettled = !1;
825
+ return item.content.map((block, blockIndex) => {
826
+ let isNestedList = block._type === "list", isTextBlock = !isNestedList && isPortableTextBlock(block);
827
+ !markerLineSettled && isTextBlock && markListItemFirstBlock(block);
828
+ let text = renderNode({
789
829
  node: block,
790
830
  index: blockIndex,
791
831
  isInline: !1,
792
832
  renderNode
793
- })
794
- })), head = `${marker}${first?.text ?? ""}`.trimEnd();
833
+ });
834
+ return text !== "" && (markerLineSettled = !0), {
835
+ isNestedList,
836
+ isTextBlock,
837
+ text
838
+ };
839
+ });
840
+ }), itemSeparator = renderedItems.some((renderedBlocks) => renderedBlocks.filter((rendered) => !rendered.isNestedList && rendered.text !== "").length > 1) ? "\n\n" : "\n";
841
+ return value.items.map((item, itemIndex) => {
842
+ let marker = getListMarker(value.kind, itemIndex, item.checked), indentWidth = value.kind === "task" ? 2 : marker.length, indent = " ".repeat(indentWidth), indentLines = (text) => text.split("\n").map((line) => line === "" ? "" : `${indent}${line}`).join("\n"), nonEmptyBlocks = (renderedItems[itemIndex] ?? []).filter((rendered) => rendered.text !== ""), markerLineCandidate = nonEmptyBlocks[0], promoted = markerLineCandidate && !markerLineCandidate.isNestedList && (value.kind !== "task" || markerLineCandidate.isTextBlock) ? markerLineCandidate : void 0, rest = promoted ? nonEmptyBlocks.slice(1) : nonEmptyBlocks, [promotedFirstLine = "", ...promotedRestLines] = (promoted?.text ?? "").split("\n"), head = [`${marker}${promotedFirstLine}`, ...promotedRestLines.length > 0 ? [indentLines(promotedRestLines.join("\n"))] : []].join("\n").trimEnd();
795
843
  return rest.length === 0 ? head : `${head}${rest.map((rendered) => {
796
- let indented = rendered.text.split("\n").map((line) => line === "" ? "" : `${indent}${line}`).join("\n");
844
+ let indented = indentLines(rendered.text);
797
845
  return rendered.isNestedList ? `\n${indented}` : `\n\n${indented}`;
798
846
  }).join("")}`;
799
847
  }).join(itemSeparator);
@@ -853,7 +901,7 @@ function portableTextToMarkdown(blocks, options = {}) {
853
901
  ...options.marks
854
902
  },
855
903
  types: {
856
- ...defaultRenderers.types,
904
+ ...gateDefaultTypeRenderers(defaultRenderers.types, options.schema, options.unknownType ?? defaultRenderers.unknownType),
857
905
  ...options.types
858
906
  },
859
907
  hardBreak: options.hardBreak ?? defaultRenderers.hardBreak,
@@ -861,22 +909,26 @@ function portableTextToMarkdown(blocks, options = {}) {
861
909
  unknownBlockStyle: options.unknownBlockStyle ?? defaultRenderers.unknownBlockStyle,
862
910
  unknownListItem: options.unknownListItem ?? defaultRenderers.unknownListItem,
863
911
  unknownMark: options.unknownMark ?? defaultRenderers.unknownMark
864
- }, renderBlockSpacing = options.blockSpacing ?? DefaultBlockSpacingRenderer, { listIndexMap, listDepthMap } = buildListIndexMap(blocks), renderNode = createRenderNode(renderers, listIndexMap, listDepthMap);
865
- return blocks.map((node, index) => {
866
- let renderedNode = renderNode({
912
+ }, renderBlockSpacing = options.blockSpacing ?? DefaultBlockSpacingRenderer, { listIndexMap, listDepthMap } = buildListIndexMap(blocks), renderNode = createRenderNode(renderers, listIndexMap, listDepthMap), renderedBlocks = blocks.map((node, index) => ({
913
+ node,
914
+ rendered: renderNode({
867
915
  node,
868
916
  index,
869
917
  isInline: !1,
870
918
  renderNode
871
- });
872
- if (index === blocks.length - 1) return renderedNode;
873
- let nextNode = blocks.at(index + 1);
874
- return nextNode ? `${renderedNode}${renderBlockSpacing({
919
+ })
920
+ })).filter(({ rendered }) => rendered !== "");
921
+ return renderedBlocks.map(({ node, rendered }, index) => {
922
+ let nextBlock = renderedBlocks.at(index + 1);
923
+ return nextBlock ? `${rendered}${renderBlockSpacing({
875
924
  current: node,
876
- next: nextNode
877
- }) ?? "\n\n"}` : renderedNode;
925
+ next: nextBlock.node
926
+ }) ?? "\n\n"}` : rendered;
878
927
  }).join("");
879
928
  }
929
+ function gateDefaultTypeRenderers(defaultTypeRenderers, schema, resolvedUnknownType) {
930
+ return schema ? Object.fromEntries(Object.entries(defaultTypeRenderers).map(([typeName, renderer]) => [typeName, (rendererOptions) => (rendererOptions.isInline ? schema.inlineObjects : schema.blockObjects).some((item) => item.name === typeName) && renderer ? renderer(rendererOptions) : resolvedUnknownType(rendererOptions)])) : defaultTypeRenderers;
931
+ }
880
932
  /********************
881
933
  * Default style definitions
882
934
  ********************/
@@ -1145,6 +1197,17 @@ const codeBlockMatcher = ({ context, value, isInline }) => {
1145
1197
  }
1146
1198
  };
1147
1199
  /**
1200
+ * Reads a token attribute as a string. markdown-it's own `Token#attrs` type
1201
+ * also allows numeric values, for attributes like an ordered list's
1202
+ * `start`, but the attributes this file reads (`src`, `href`, `title`,
1203
+ * `style`) are always strings, set by markdown-it's own link/image/table
1204
+ * parsing.
1205
+ */
1206
+ function stringAttr(token, name) {
1207
+ let value = token.attrGet(name);
1208
+ return typeof value == "string" ? value : void 0;
1209
+ }
1210
+ /**
1148
1211
  * Reads GFM column alignment from a markdown-it cell token's `style`
1149
1212
  * attribute. Tolerates other CSS declarations sharing the value.
1150
1213
  */
@@ -1254,7 +1317,7 @@ function buildDegradationMessage(degradations) {
1254
1317
  function markdownToPortableText(markdown, options) {
1255
1318
  let consolidatedOptions = {
1256
1319
  schema: options?.schema ?? defaultSchema,
1257
- keyGenerator: options?.keyGenerator ?? defaultKeyGenerator,
1320
+ keyGenerator: uniqueKeyGenerator(options?.keyGenerator ?? defaultKeyGenerator),
1258
1321
  html: { inline: options?.html?.inline ?? "skip" },
1259
1322
  marks: {
1260
1323
  ...defaultOptions.marks,
@@ -1307,11 +1370,16 @@ function markdownToPortableText(markdown, options) {
1307
1370
  message: degradationMessage["fields-dropped"](names, dropped.construct),
1308
1371
  line
1309
1372
  });
1310
- }, tokens = markdownit({
1373
+ }, md = markdownit({
1311
1374
  html: !0,
1312
1375
  linkify: !0,
1313
1376
  typographer: !1
1314
- }).enable(["strikethrough", "table"]).use(alert).parse(markdown, {}), taskCheckedByListItemIndex = /* @__PURE__ */ new Map(), taskItemTextByListItemIndex = /* @__PURE__ */ new Map();
1377
+ }).enable(["strikethrough", "table"]).use(alert);
1378
+ md.linkify.set({
1379
+ fuzzyLink: !0,
1380
+ urlAuth: !0
1381
+ });
1382
+ let tokens = md.parse(markdown, {}), taskCheckedByListItemIndex = /* @__PURE__ */ new Map(), taskItemTextByListItemIndex = /* @__PURE__ */ new Map();
1315
1383
  for (let i = 0; i < tokens.length; i++) {
1316
1384
  if (tokens[i]?.type !== "list_item_open") continue;
1317
1385
  let inlineIndex = -1;
@@ -1830,7 +1898,7 @@ function markdownToPortableText(markdown, options) {
1830
1898
  break;
1831
1899
  case "th_open":
1832
1900
  case "td_open": {
1833
- currentTable && inTableHead && token.type === "th_open" && currentTable.alignment.push(extractAlignmentFromStyleAttr(token.attrGet("style")));
1901
+ currentTable && inTableHead && token.type === "th_open" && currentTable.alignment.push(extractAlignmentFromStyleAttr(stringAttr(token, "style") ?? null));
1834
1902
  let style = consolidatedOptions.block.normal({ context: { schema: consolidatedOptions.schema } });
1835
1903
  style ? startBlock(style) : (reportStyleFallback("normal", currentTable?.line), startBlock("normal"));
1836
1904
  break;
@@ -1857,12 +1925,15 @@ function markdownToPortableText(markdown, options) {
1857
1925
  });
1858
1926
  let demotedInlineImages = [];
1859
1927
  for (let block of cellBlocks) if (block._type === "block" && "children" in block && Array.isArray(block.children)) for (let child of block.children) typeof child == "object" && child && demotedTableImages.has(child) && demotedInlineImages.push(child);
1860
- let firstBlock = cellBlocks[0], liftedImage;
1928
+ let firstBlock = cellBlocks[0], liftedObject;
1861
1929
  if (cellBlocks.length === 1 && firstBlock && firstBlock._type === "block" && "children" in firstBlock && Array.isArray(firstBlock.children) && firstBlock.children.length === 1) {
1862
1930
  let onlyChild = firstBlock.children[0];
1863
- typeof onlyChild == "object" && onlyChild && "_type" in onlyChild && onlyChild._type !== consolidatedOptions.schema.span.name && onlyChild._type === "image" && (cellBlocks[0] = onlyChild, liftedImage = onlyChild);
1931
+ if (typeof onlyChild == "object" && onlyChild && "_type" in onlyChild && onlyChild._type !== consolidatedOptions.schema.span.name) {
1932
+ let declaredInline = consolidatedOptions.schema.inlineObjects.some((inlineObject) => inlineObject.name === onlyChild._type), declaredBlock = consolidatedOptions.schema.blockObjects.some((blockObject) => blockObject.name === onlyChild._type);
1933
+ declaredInline && !declaredBlock || (cellBlocks[0] = onlyChild, liftedObject = onlyChild);
1934
+ }
1864
1935
  }
1865
- for (let demotedImage of demotedInlineImages) if (demotedImage !== liftedImage) {
1936
+ for (let demotedImage of demotedInlineImages) if (demotedImage !== liftedObject) {
1866
1937
  let { alt, src } = demotedImage;
1867
1938
  report({
1868
1939
  type: "image-block-to-inline",
@@ -1883,7 +1954,7 @@ function markdownToPortableText(markdown, options) {
1883
1954
  if (token.children?.length === 1 && token.children[0]?.type === "image") {
1884
1955
  let imageToken = token.children[0];
1885
1956
  if (!imageToken) break;
1886
- let src = imageToken.attrs?.find(([name]) => name === "src")?.at(1) || "", alt = unescapeImageAndLinkText(imageToken.content || ""), title = imageToken.attrs?.find(([name]) => name === "title")?.at(1) || void 0, blockImageObject = consolidatedOptions.types.image({
1957
+ let src = stringAttr(imageToken, "src") || "", alt = unescapeImageAndLinkText(imageToken.content || ""), title = stringAttr(imageToken, "title"), blockImageObject = consolidatedOptions.types.image({
1887
1958
  context: {
1888
1959
  schema: consolidatedOptions.schema,
1889
1960
  keyGenerator: consolidatedOptions.keyGenerator
@@ -2056,7 +2127,7 @@ function markdownToPortableText(markdown, options) {
2056
2127
  break;
2057
2128
  }
2058
2129
  case "link_open": {
2059
- let href = childToken.attrs?.find(([name]) => name === "href")?.at(1);
2130
+ let href = stringAttr(childToken, "href");
2060
2131
  if (!href) {
2061
2132
  let missingHrefSnippet = truncateSnippet(collectInlineText(inlineChildren, childIndex, "link_open", "link_close"));
2062
2133
  report({
@@ -2067,7 +2138,7 @@ function markdownToPortableText(markdown, options) {
2067
2138
  });
2068
2139
  break;
2069
2140
  }
2070
- let title = childToken.attrs?.find(([name]) => name === "title")?.at(1), linkObject = consolidatedOptions.marks.link({
2141
+ let title = stringAttr(childToken, "title"), linkObject = consolidatedOptions.marks.link({
2071
2142
  context: {
2072
2143
  schema: consolidatedOptions.schema,
2073
2144
  keyGenerator: consolidatedOptions.keyGenerator
@@ -2103,7 +2174,7 @@ function markdownToPortableText(markdown, options) {
2103
2174
  break;
2104
2175
  }
2105
2176
  case "image": {
2106
- let src = childToken.attrs?.find(([name]) => name === "src")?.at(1) || "", alt = unescapeImageAndLinkText(childToken.content || ""), inlineImageObject = consolidatedOptions.types.image({
2177
+ let src = stringAttr(childToken, "src") || "", alt = unescapeImageAndLinkText(childToken.content || ""), inlineImageObject = consolidatedOptions.types.image({
2107
2178
  context: {
2108
2179
  schema: consolidatedOptions.schema,
2109
2180
  keyGenerator: consolidatedOptions.keyGenerator
@@ -2234,6 +2305,828 @@ function parseJsonObjectFence(code) {
2234
2305
  let objectValue = parsed;
2235
2306
  if (typeof objectValue._type == "string" && objectValue._type.length !== 0) return objectValue;
2236
2307
  }
2237
- export { DefaultBlockSpacingRenderer, DefaultBlockquoteObjectRenderer, DefaultBlockquoteRenderer, DefaultCalloutRenderer, DefaultCodeBlockRenderer, DefaultCodeRenderer, DefaultEmRenderer, DefaultH1Renderer, DefaultH2Renderer, DefaultH3Renderer, DefaultH4Renderer, DefaultH5Renderer, DefaultH6Renderer, DefaultHardBreakRenderer, DefaultHorizontalRuleRenderer, DefaultHtmlRenderer, DefaultImageRenderer, DefaultLinkRenderer, DefaultListItemRenderer, DefaultListRenderer, DefaultNormalRenderer, DefaultStrikeThroughRenderer, DefaultStrongRenderer, DefaultTableRenderer, DefaultUnderlineRenderer, markdownToPortableText, portableTextToMarkdown };
2308
+ /**
2309
+ * Deliberately high pending calibration against real agent edit
2310
+ * traces: a wrong match moves anchors onto unrelated text, a fresh
2311
+ * key resets one block.
2312
+ */
2313
+ const MIN_BLOCK_SIMILARITY = .8, MAX_SIMILARITY_PAIRS = 2500;
2314
+ /**
2315
+ * Converts edited markdown to Portable Text, restores stored keys, and
2316
+ * restores fields the markdown dialect cannot express (dropped by
2317
+ * serialization, so the edit could not have touched them); a field
2318
+ * markdown does express follows the edit. The same rule covers a
2319
+ * custom style or decorator markdown has no syntax for, coerced to a
2320
+ * built-in on the round trip. An empty or whitespace-only text block
2321
+ * is the same case taken to the whole block: markdown has no form for
2322
+ * either, so it is restored next to its surviving neighbor and
2323
+ * dropped along with that neighbor if the neighbor does not survive.
2324
+ * Keys aim for what the
2325
+ * same edit would have produced in an editor:
2326
+ * unchanged, moved, and rewritten-in-place content keeps its keys
2327
+ * (rewriting a paragraph in place keeps its identity, like typing over
2328
+ * it), a split keeps the key on its first non-empty fragment, a merge
2329
+ * keeps the first source block's key, and a `json:object` payload keeps
2330
+ * the key it carries, unless reconciliation matches it to stored
2331
+ * content, which takes the stored key even over a differing key in the
2332
+ * payload. When an insertion or deletion makes positions ambiguous,
2333
+ * only clear similarity evidence adopts a key and everything else gets
2334
+ * a new one; gathering that evidence is time-capped, so on very large
2335
+ * ambiguous edits the set of adopted keys can differ across machine
2336
+ * speeds, degrading toward fresh keys.
2337
+ * Output keys are unique among siblings. The function does not mutate
2338
+ * `storedPortableText` and returns a value, not patches. The `schema`
2339
+ * is taken once and governs both directions; pass the same `serialize`
2340
+ * options that produced the markdown that was edited. A throwing or
2341
+ * stateful custom matcher or renderer propagates or degrades matching
2342
+ * respectively.
2343
+ * Reconciliation never merges concurrent edits: compare the stored
2344
+ * field against the live document before writing the result back.
2345
+ * The trades in one line: same-position replacement inherits identity,
2346
+ * a count-preserving rewrite pairs positionally (block-level and
2347
+ * sibling-level alike), and evidence gathering is capped, degrading to
2348
+ * fresh keys.
2349
+ *
2350
+ * @public
2351
+ */
2352
+ function applyMarkdownEdit(storedPortableText, editedMarkdown, options) {
2353
+ let onReconciliation = options?.onReconciliation, recorder = onReconciliation ? {
2354
+ preservationBasis: /* @__PURE__ */ new WeakMap(),
2355
+ renamePreviousKey: /* @__PURE__ */ new WeakMap(),
2356
+ annotationKeyConflicts: [],
2357
+ ambiguousRegionGroups: [],
2358
+ skipReason: void 0
2359
+ } : void 0, result = structuredClone(markdownToPortableText(editedMarkdown, {
2360
+ ...options?.deserialize,
2361
+ schema: options?.schema
2362
+ })), canonical = canonicalizeStored(storedPortableText, options), originOf = traceOrigins(nonEmptyBlocks(storedPortableText, options), canonical, recorder), adoptedNodes = /* @__PURE__ */ new WeakSet();
2363
+ if (originOf) {
2364
+ let alignment = alignBlocks(canonical, result, recorder);
2365
+ if (alignment) {
2366
+ adoptAnchors(alignment.anchors, originOf, result, adoptedNodes, recorder);
2367
+ let gaps = adoptMoves(alignment.gaps, canonical, result, originOf, adoptedNodes, recorder);
2368
+ for (let gap of gaps) resolveGap(gap.storedIndexes, gap.editedIndexes, canonical, result, originOf, adoptedNodes, recorder);
2369
+ reinsertEmptyRuns(storedPortableText, result, adoptedNodes, options, recorder);
2370
+ }
2371
+ }
2372
+ return enforceSiblingKeyUniqueness(result, options?.deserialize?.keyGenerator ?? defaultKeyGenerator, adoptedNodes, recorder), recorder && onReconciliation && onReconciliation(buildReconciliationReport(result, recorder)), result;
2373
+ }
2374
+ /**
2375
+ * Re-expresses the stored value in the parser's dialect by serializing
2376
+ * it and parsing it right back: the parser merges same-mark sibling
2377
+ * spans, reorders marks, collapses list levels, and fills defaults, so
2378
+ * content the edit never touched only deep-equals its parsed
2379
+ * counterpart after both sides have been through the same round trip.
2380
+ * The replacement keys are positional (`__canonical_<n>`), so a match
2381
+ * against canonical node `n` can be traded back for stored node `n`'s
2382
+ * real `_key`.
2383
+ */
2384
+ function canonicalizeStored(stored, options) {
2385
+ let storedMarkdown = portableTextToMarkdown(structuredClone(stored), {
2386
+ ...options?.serialize,
2387
+ schema: options?.schema
2388
+ }), { onDegradation, ...canonicalDeserializeOptions } = options?.deserialize ?? {}, canonicalKeyCounter = 0;
2389
+ return markdownToPortableText(storedMarkdown, {
2390
+ ...canonicalDeserializeOptions,
2391
+ schema: options?.schema,
2392
+ keyGenerator: () => `__canonical_${canonicalKeyCounter++}`
2393
+ });
2394
+ }
2395
+ /**
2396
+ * Positional pairing between `stored` and `canonical` is only
2397
+ * trustworthy when serialization preserved the node count and the
2398
+ * type sequence; when it did not (heading hard-break splits, lossy
2399
+ * table normalization), no key can be traced back to its owner, so
2400
+ * nothing adopts. `stored` is already the non-empty subsequence: empty
2401
+ * text blocks have no markdown form, so `canonical` never carries them
2402
+ * either, and `reinsertEmptyRuns` restores them afterward.
2403
+ */
2404
+ function traceOrigins(stored, canonical, recorder) {
2405
+ if (canonical.length !== stored.length) {
2406
+ recorder && (recorder.skipReason = "round-trip-mismatch");
2407
+ return;
2408
+ }
2409
+ for (let index = 0; index < stored.length; index++) if (stored[index]._type !== canonical[index]?._type) {
2410
+ recorder && (recorder.skipReason = "round-trip-mismatch");
2411
+ return;
2412
+ }
2413
+ for (let index = 0; index < stored.length; index++) {
2414
+ let storedNode = stored[index], canonicalNode = canonical[index];
2415
+ if (isTextBlock$1(storedNode) && isTextBlock$1(canonicalNode) && blockText(storedNode).trim() !== blockText(canonicalNode).trim()) {
2416
+ recorder && (recorder.skipReason = "round-trip-mismatch");
2417
+ return;
2418
+ }
2419
+ }
2420
+ return (canonicalIndex) => stored[canonicalIndex];
2421
+ }
2422
+ /**
2423
+ * Equal runs become anchor pairs (so repeated content pairs
2424
+ * first-to-first when adopted), and the delete/insert runs between
2425
+ * them form the gaps.
2426
+ */
2427
+ function alignBlocks(canonical, result, recorder) {
2428
+ let tokenByNeutral = /* @__PURE__ */ new Map(), tokenOf = (node) => {
2429
+ let neutral = neutralForm(node), token = tokenByNeutral.get(neutral);
2430
+ return token === void 0 && (token = String.fromCharCode(tokenByNeutral.size + 1), tokenByNeutral.set(neutral, token)), token;
2431
+ }, canonicalTokens = canonical.map(tokenOf).join(""), resultTokens = result.map(tokenOf).join("");
2432
+ if (tokenByNeutral.size > 55e3) {
2433
+ recorder && (recorder.skipReason = "document-too-large");
2434
+ return;
2435
+ }
2436
+ let diffs = makeDiff(canonicalTokens, resultTokens, { checkLines: !1 }), anchors = [], gaps = [], gap = {
2437
+ storedIndexes: [],
2438
+ editedIndexes: []
2439
+ }, flushGap = () => {
2440
+ (gap.storedIndexes.length > 0 || gap.editedIndexes.length > 0) && (gaps.push(gap), gap = {
2441
+ storedIndexes: [],
2442
+ editedIndexes: []
2443
+ });
2444
+ }, canonicalIndex = 0, resultIndex = 0;
2445
+ for (let [operation, text] of diffs) if (operation === 0) {
2446
+ flushGap();
2447
+ for (let offset = 0; offset < text.length; offset++) anchors.push({
2448
+ canonicalIndex,
2449
+ resultIndex
2450
+ }), canonicalIndex++, resultIndex++;
2451
+ } else if (operation === -1) for (let offset = 0; offset < text.length; offset++) gap.storedIndexes.push(canonicalIndex++);
2452
+ else for (let offset = 0; offset < text.length; offset++) gap.editedIndexes.push(resultIndex++);
2453
+ return flushGap(), {
2454
+ anchors,
2455
+ gaps
2456
+ };
2457
+ }
2458
+ function adoptAnchors(anchors, originOf, result, adoptedNodes, recorder) {
2459
+ for (let anchor of anchors) result[anchor.resultIndex] = adoptVerbatim(originOf(anchor.canonicalIndex), result[anchor.resultIndex], adoptedNodes, "content-unchanged", recorder);
2460
+ }
2461
+ /**
2462
+ * An anchor or a unique exact leftover pairs a canonical block against
2463
+ * a parsed one that share the same neutral form (that is what put them
2464
+ * in the same equal-diff run or the same neutral-form bucket), so
2465
+ * everything the dialect can express is untouched and everything it
2466
+ * cannot express was invisible to the edit. The stored subtree is the
2467
+ * truth at every depth, spans, marks, markDefs, and any field the
2468
+ * dialect drops, so adoption replaces the whole node rather than
2469
+ * reconciling into the parsed shape (which would otherwise, for
2470
+ * instance, keep a parser-side span merge that collapsed an
2471
+ * unmappable mark boundary the edit never touched). `restoreFields`
2472
+ * and per-child reconciliation are skipped entirely: a verbatim clone
2473
+ * of the stored node is already complete.
2474
+ */
2475
+ function adoptVerbatim(original, target, adoptedNodes, basis, recorder) {
2476
+ let clone = structuredClone(original);
2477
+ return fillMissingKeysFromTarget(clone, target), markSubtreeAdopted(clone, adoptedNodes), recorder && (tagSubtreePreserved(clone, recorder), basis !== "content-unchanged" && typeof clone._key == "string" && recorder.preservationBasis.set(clone, basis)), clone;
2478
+ }
2479
+ /**
2480
+ * A stored node practically always carries its own `_key`; when it
2481
+ * genuinely does not, there is nothing to adopt, so the clone keeps
2482
+ * whatever key the plain parse already minted at the corresponding
2483
+ * position, the same key adoption would have left in place. The walk
2484
+ * follows both trees positionally (not by content matching, which is
2485
+ * exactly what an exact-signature match already guarantees agrees at
2486
+ * every position the two sides both still have).
2487
+ */
2488
+ function fillMissingKeysFromTarget(clone, target) {
2489
+ typeof clone._key != "string" && typeof target._key == "string" && (clone._key = target._key);
2490
+ for (let field of Object.keys(clone)) {
2491
+ let cloneValue = clone[field], targetValue = target[field];
2492
+ if (isTypedObjectArray(cloneValue) && isTypedObjectArray(targetValue)) {
2493
+ let length = Math.min(cloneValue.length, targetValue.length);
2494
+ for (let index = 0; index < length; index++) fillMissingKeysFromTarget(cloneValue[index], targetValue[index]);
2495
+ } else typeof cloneValue == "object" && cloneValue && !Array.isArray(cloneValue) && typeof targetValue == "object" && targetValue && !Array.isArray(targetValue) && fillMissingKeysFromTarget(cloneValue, targetValue);
2496
+ }
2497
+ }
2498
+ /**
2499
+ * Every keyed node in a verbatim clone is adopted, not only its root:
2500
+ * the sibling-key-uniqueness pass recurses into every nested keyed
2501
+ * array, and an adopted-first ordering there only favors a clone's
2502
+ * descendants if they are themselves marked adopted.
2503
+ */
2504
+ function markSubtreeAdopted(node, adoptedNodes) {
2505
+ adoptedNodes.add(node);
2506
+ for (let value of Object.values(node)) if (Array.isArray(value) && value.every((item) => typeof item == "object" && !!item) && value.length > 0) for (let child of value) markSubtreeAdopted(child, adoptedNodes);
2507
+ else typeof value == "object" && value && markSubtreeAdopted(value, adoptedNodes);
2508
+ }
2509
+ /**
2510
+ * Moves: content that left one gap and reappeared in another. Unique
2511
+ * exact pairs across all gaps adopt before any gap-local pairing can
2512
+ * consume the keys they need.
2513
+ */
2514
+ function adoptMoves(gaps, canonical, result, originOf, adoptedNodes, recorder) {
2515
+ let consumedStored = /* @__PURE__ */ new Set(), consumedEdited = /* @__PURE__ */ new Set(), storedLeftovers = gaps.flatMap((g) => g.storedIndexes), editedLeftovers = gaps.flatMap((g) => g.editedIndexes), storedByNeutral = /* @__PURE__ */ new Map();
2516
+ for (let index of storedLeftovers) {
2517
+ let neutral = neutralForm(canonical[index]);
2518
+ storedByNeutral.set(neutral, [...storedByNeutral.get(neutral) ?? [], index]);
2519
+ }
2520
+ let editedByNeutral = /* @__PURE__ */ new Map();
2521
+ for (let index of editedLeftovers) {
2522
+ let neutral = neutralForm(result[index]);
2523
+ editedByNeutral.set(neutral, [...editedByNeutral.get(neutral) ?? [], index]);
2524
+ }
2525
+ for (let [neutral, storedIndexes] of storedByNeutral) {
2526
+ let editedIndexes = editedByNeutral.get(neutral);
2527
+ if (storedIndexes.length !== 1 || !editedIndexes || editedIndexes.length !== 1) continue;
2528
+ consumedStored.add(storedIndexes[0]), consumedEdited.add(editedIndexes[0]);
2529
+ let clone = adoptVerbatim(originOf(storedIndexes[0]), result[editedIndexes[0]], adoptedNodes, "content-moved", recorder);
2530
+ result[editedIndexes[0]] = clone;
2531
+ }
2532
+ return gaps.map((currentGap) => ({
2533
+ storedIndexes: currentGap.storedIndexes.filter((index) => !consumedStored.has(index)),
2534
+ editedIndexes: currentGap.editedIndexes.filter((index) => !consumedEdited.has(index))
2535
+ }));
2536
+ }
2537
+ /**
2538
+ * Gap policy, in order: split/merge survivor (the first fragment or
2539
+ * first source block keeps the key, matching what pressing enter or
2540
+ * backspace does in the editor), positional zip for equal counts
2541
+ * (typing over a paragraph keeps its identity), similarity for
2542
+ * unequal counts (an insertion or deletion shifted positions, so
2543
+ * position lies and only mutual unique best evidence adopts).
2544
+ */
2545
+ function resolveGap(storedIndexes, editedIndexes, canonical, result, originOf, adoptedNodes, recorder) {
2546
+ let remainingStored = new Set(storedIndexes), remainingEdited = new Set(editedIndexes), withinConcatenationCap = storedIndexes.length * editedIndexes.length <= MAX_SIMILARITY_PAIRS;
2547
+ if (withinConcatenationCap) for (let storedIndex of storedIndexes) {
2548
+ if (!remainingStored.has(storedIndex)) continue;
2549
+ let storedBlock = canonical[storedIndex];
2550
+ if (!isTextBlock$1(storedBlock)) continue;
2551
+ let fragments = findConcatenation(blockText(storedBlock), editedIndexes.filter((index) => remainingEdited.has(index)), result);
2552
+ if (fragments) {
2553
+ remainingStored.delete(storedIndex);
2554
+ for (let fragment of fragments) remainingEdited.delete(fragment);
2555
+ let survivor = fragments.find((fragment) => blockText(result[fragment]).length > 0) ?? fragments[0];
2556
+ adoptNode(originOf(storedIndex), canonical[storedIndex], result[survivor], adoptedNodes, "content-split", recorder);
2557
+ }
2558
+ }
2559
+ if (withinConcatenationCap) for (let editedIndex of editedIndexes) {
2560
+ if (!remainingEdited.has(editedIndex)) continue;
2561
+ let editedBlock = result[editedIndex];
2562
+ if (!isTextBlock$1(editedBlock)) continue;
2563
+ let sources = findConcatenation(blockText(editedBlock), storedIndexes.filter((index) => remainingStored.has(index)), canonical);
2564
+ if (sources) {
2565
+ remainingEdited.delete(editedIndex);
2566
+ for (let source of sources) remainingStored.delete(source);
2567
+ adoptNode(originOf(sources[0]), canonical[sources[0]], result[editedIndex], adoptedNodes, "content-merged", recorder);
2568
+ }
2569
+ }
2570
+ let storedRest = [...remainingStored], editedRest = [...remainingEdited];
2571
+ if (storedRest.length === editedRest.length) {
2572
+ for (let offset = 0; offset < storedRest.length; offset++) {
2573
+ let storedBlock = canonical[storedRest[offset]], editedBlock = result[editedRest[offset]];
2574
+ storedBlock._type === editedBlock._type && adoptNode(originOf(storedRest[offset]), canonical[storedRest[offset]], editedBlock, adoptedNodes, "same-position", recorder);
2575
+ }
2576
+ return;
2577
+ }
2578
+ if (storedRest.length * editedRest.length > MAX_SIMILARITY_PAIRS) {
2579
+ recorder && recorder.ambiguousRegionGroups.push(editedRest.map((editedIndex) => result[editedIndex]));
2580
+ return;
2581
+ }
2582
+ let scores = /* @__PURE__ */ new Map();
2583
+ for (let storedIndex of storedRest) for (let editedIndex of editedRest) {
2584
+ let score = blockSimilarity(canonical[storedIndex], result[editedIndex]);
2585
+ score >= MIN_BLOCK_SIMILARITY && scores.set(`${storedIndex}:${editedIndex}`, score);
2586
+ }
2587
+ for (let storedIndex of storedRest) {
2588
+ let best = uniqueBest(editedRest, (editedIndex) => scores.get(`${storedIndex}:${editedIndex}`));
2589
+ best !== void 0 && uniqueBest(storedRest, (otherStoredIndex) => scores.get(`${otherStoredIndex}:${best}`)) === storedIndex && adoptNode(originOf(storedIndex), canonical[storedIndex], result[best], adoptedNodes, "similar-content", recorder);
2590
+ }
2591
+ }
2592
+ /**
2593
+ * Fragments join with nothing or a single space, since a markdown
2594
+ * merge is often a soft-wrap join that inserts one ("alpha\nbeta"
2595
+ * parses to "alpha beta").
2596
+ */
2597
+ function findConcatenation(wholeText, candidateIndexes, nodes) {
2598
+ if (wholeText.length !== 0) for (let joiner of ["", " "]) for (let start = 0; start < candidateIndexes.length; start++) {
2599
+ let concatenated = "", used = [];
2600
+ for (let position = start; position < candidateIndexes.length; position++) {
2601
+ let index = candidateIndexes[position];
2602
+ if (position > start && candidateIndexes[position - 1] !== index - 1 || !isTextBlock$1(nodes[index]) || (concatenated = used.length === 0 ? blockText(nodes[index]) : concatenated + joiner + blockText(nodes[index]), used.push(index), concatenated.length > wholeText.length)) break;
2603
+ if (concatenated === wholeText && used.length > 1) return used;
2604
+ }
2605
+ }
2606
+ }
2607
+ /**
2608
+ * Adopts the original node's `_key`, its markdown-inexpressible
2609
+ * fields, then its `markDefs` before its other keyed children, since
2610
+ * `span.marks` references need the adopted `markDefs` keys already in
2611
+ * place.
2612
+ */
2613
+ function adoptNode(original, canonicalCounterpart, target, adoptedNodes, basis, recorder) {
2614
+ adoptedNodes.add(target), typeof original._key == "string" && (target._key = original._key, recorder?.preservationBasis.set(target, basis)), restoreFields(original, canonicalCounterpart, target), rewriteMarkReferences(target, adoptMarkDefs(original, canonicalCounterpart, target, recorder));
2615
+ for (let field of Object.keys(target)) {
2616
+ if (field === "markDefs") continue;
2617
+ let originalChildren = original[field], targetChildren = target[field];
2618
+ if (!isTypedObjectArray(originalChildren) || !isTypedObjectArray(targetChildren)) continue;
2619
+ let canonicalChildren = canonicalChildArray(canonicalCounterpart, field, originalChildren), matchedOriginal = /* @__PURE__ */ new Set(), matchedTarget = /* @__PURE__ */ new Set(), originalGroups = groupByNeutralForm(originalChildren, matchedOriginal, buildAliasMap(original)), targetGroups = groupByNeutralForm(targetChildren, matchedTarget, buildAliasMap(target));
2620
+ for (let [neutral, originalIndexes] of originalGroups) {
2621
+ let targetIndexes = targetGroups.get(neutral);
2622
+ originalIndexes.length !== 1 || !targetIndexes || targetIndexes.length !== 1 || (matchedOriginal.add(originalIndexes[0]), matchedTarget.add(targetIndexes[0]), adoptNode(originalChildren[originalIndexes[0]], canonicalChildren?.[originalIndexes[0]], targetChildren[targetIndexes[0]], adoptedNodes, "content-unchanged", recorder));
2623
+ }
2624
+ field === "children" && originalChildren.length * targetChildren.length <= MAX_SIMILARITY_PAIRS && adoptMergedSpans(originalChildren, canonicalChildren, matchedOriginal, targetChildren, matchedTarget, adoptedNodes, recorder), adoptResidualZip(originalChildren, canonicalChildren, matchedOriginal, targetChildren, matchedTarget, adoptedNodes, recorder);
2625
+ }
2626
+ }
2627
+ /**
2628
+ * The container-level counterpart of `traceOrigins`'s guard: a
2629
+ * child's canonical form is only trustworthy when the canonical
2630
+ * container holds the same field as the same typed object array,
2631
+ * equal in length and `_type` sequence to the original's, so pairing
2632
+ * by index (original child `i` to canonical child `i`) means the same
2633
+ * content on both sides.
2634
+ */
2635
+ function canonicalChildArray(canonicalCounterpart, field, originalChildren) {
2636
+ if (!canonicalCounterpart) return;
2637
+ let candidate = canonicalCounterpart[field];
2638
+ if (!(!isTypedObjectArray(candidate) || candidate.length !== originalChildren.length)) {
2639
+ for (let index = 0; index < originalChildren.length; index++) if (originalChildren[index]._type !== candidate[index]._type) return;
2640
+ return candidate;
2641
+ }
2642
+ }
2643
+ /**
2644
+ * Restores fields the markdown dialect dropped or altered: present on
2645
+ * the original, unchanged by the edit (the target still agrees with
2646
+ * the original's own canonical round trip), and different on that
2647
+ * round trip from the original (so the parse, left to itself, could
2648
+ * never have produced the original's value). Absence counts as a
2649
+ * value under both comparisons, which is what folds a wholly dropped
2650
+ * field (no markdown form at all) and a coerced one (a custom style
2651
+ * or decorator markdown silently maps to its closest built-in) into
2652
+ * one rule. Structural child arrays (`children`, `markDefs`, and
2653
+ * typed object arrays generally) are excluded: their elements adopt
2654
+ * individually through the recursive per-child walk instead, except
2655
+ * when the target has no such element to walk at all: a typed-object
2656
+ * array field the dialect drops entirely leaves nothing on the target
2657
+ * side for that walk to reconcile, so it falls through to the same
2658
+ * absent-on-target, absent-on-canonical oracle as every scalar field,
2659
+ * restored verbatim rather than left missing. Restored values are
2660
+ * cloned, since the sibling-key-uniqueness pass may rewrite `_key`s
2661
+ * inside a restored array of objects, and the original must stay
2662
+ * untouched.
2663
+ */
2664
+ function restoreFields(original, canonicalCounterpart, target) {
2665
+ if (canonicalCounterpart) for (let field of Object.keys(original)) {
2666
+ if (field === "_key" || field === "_type" || field === "markDefs" || field === "children") continue;
2667
+ if (isTypedObjectArray(original[field])) {
2668
+ target[field] === void 0 && canonicalCounterpart[field] === void 0 && (target[field] = structuredClone(original[field]));
2669
+ continue;
2670
+ }
2671
+ let canonicalValue = canonicalCounterpart[field];
2672
+ valuesEqual(target[field], canonicalValue) && (valuesEqual(canonicalValue, original[field]) || (target[field] = structuredClone(original[field])));
2673
+ }
2674
+ }
2675
+ /**
2676
+ * Deep equality for restoration's before/after comparison: the
2677
+ * `encodeNeutral` encoding without alias rewriting, so it compares
2678
+ * `marks` arrays (and any other array or nested object) by value
2679
+ * rather than by identity. Absent (`undefined`) encodes to the same
2680
+ * string on both sides, so two absent fields count as equal.
2681
+ */
2682
+ function valuesEqual(a, b) {
2683
+ return encodeNeutral(a, void 0) === encodeNeutral(b, void 0);
2684
+ }
2685
+ /**
2686
+ * A span in the edited output can be the merge of several stored
2687
+ * spans: the parser merges adjacent same-mark spans, and an edit that
2688
+ * removes formatting merges across the old mark boundary too. Matching
2689
+ * is by text alone, and the first contributor's key survives, matching
2690
+ * the editor's own span-merge normalization.
2691
+ */
2692
+ function adoptMergedSpans(originalChildren, canonicalChildren, matchedOriginal, targetChildren, matchedTarget, adoptedNodes, recorder) {
2693
+ for (let targetIndex = 0; targetIndex < targetChildren.length; targetIndex++) {
2694
+ if (matchedTarget.has(targetIndex)) continue;
2695
+ let targetSpan = targetChildren[targetIndex], targetText = targetSpan.text;
2696
+ if (typeof targetText == "string") for (let start = 0; start < originalChildren.length; start++) {
2697
+ if (matchedOriginal.has(start)) continue;
2698
+ let concatenated = "", used = [];
2699
+ for (let index = start; index < originalChildren.length && !matchedOriginal.has(index); index++) {
2700
+ let originalSpan = originalChildren[index];
2701
+ if (typeof originalSpan.text != "string" || (concatenated += originalSpan.text, used.push(index), concatenated.length > targetText.length)) break;
2702
+ if (concatenated === targetText && used.length > 1) {
2703
+ matchedTarget.add(targetIndex);
2704
+ for (let usedIndex of used) matchedOriginal.add(usedIndex);
2705
+ adoptNode(originalChildren[used[0]], canonicalChildren?.[used[0]], targetSpan, adoptedNodes, "content-merged", recorder);
2706
+ break;
2707
+ }
2708
+ }
2709
+ if (matchedTarget.has(targetIndex)) break;
2710
+ }
2711
+ }
2712
+ }
2713
+ /**
2714
+ * The equal-count residual rule, mirroring `resolveGap`'s positional
2715
+ * zip: elements left unmatched after neutral-form (and, for
2716
+ * `children`, span-merge) matching are treated as in-place edits when
2717
+ * both sides leave the same count, position being the same evidence
2718
+ * the block-level zip already trusts, and the trade is the same too:
2719
+ * a reorder-plus-edit with balanced counts mispairs. Unequal counts
2720
+ * adopt nothing, since position no longer lines up.
2721
+ */
2722
+ function adoptResidualZip(originalChildren, canonicalChildren, matchedOriginal, targetChildren, matchedTarget, adoptedNodes, recorder) {
2723
+ let originalRest = originalChildren.map((node, index) => ({
2724
+ node,
2725
+ index
2726
+ })).filter(({ index }) => !matchedOriginal.has(index)), targetRest = targetChildren.filter((_, index) => !matchedTarget.has(index));
2727
+ if (originalRest.length === targetRest.length) for (let offset = 0; offset < originalRest.length; offset++) {
2728
+ let originalNode = originalRest[offset].node, targetNode = targetRest[offset];
2729
+ originalNode._type === targetNode._type && adoptNode(originalNode, canonicalChildren?.[originalRest[offset].index], targetNode, adoptedNodes, "same-position", recorder);
2730
+ }
2731
+ }
2732
+ /**
2733
+ * Matches `markDefs` by definition content, `_key` excluded. Returns
2734
+ * the mapping from the target's fresh keys to the adopted stored
2735
+ * keys, for rewriting `span.marks` references.
2736
+ */
2737
+ function adoptMarkDefs(original, canonicalCounterpart, target, recorder) {
2738
+ let keyMap = /* @__PURE__ */ new Map(), originalDefs = original.markDefs, targetDefs = target.markDefs;
2739
+ if (!isTypedObjectArray(originalDefs) || !isTypedObjectArray(targetDefs)) return keyMap;
2740
+ let canonicalDefs = canonicalChildArray(canonicalCounterpart, "markDefs", originalDefs), matchedOriginal = /* @__PURE__ */ new Set(), matchedTarget = /* @__PURE__ */ new Set(), originalGroups = groupByNeutralForm(originalDefs.map((def, index) => canonicalDefs?.[index] ?? def), matchedOriginal), targetGroups = groupByNeutralForm(targetDefs, matchedTarget), adoptDef = (originalDef, canonicalDef, targetDef, basis) => {
2741
+ if (restoreFields(originalDef, canonicalDef, targetDef), typeof originalDef._key == "string" && typeof targetDef._key == "string") {
2742
+ let adoptedKey = originalDef._key;
2743
+ if (targetDefs.some((def) => def !== targetDef && def._key === adoptedKey)) {
2744
+ recorder?.annotationKeyConflicts.push(targetDef);
2745
+ return;
2746
+ }
2747
+ keyMap.set(targetDef._key, adoptedKey), targetDef._key = adoptedKey, recorder?.preservationBasis.set(targetDef, basis);
2748
+ }
2749
+ };
2750
+ for (let [neutral, originalIndexes] of originalGroups) {
2751
+ let targetIndexes = targetGroups.get(neutral);
2752
+ if (!(!targetIndexes || targetIndexes.length !== originalIndexes.length)) for (let offset = 0; offset < originalIndexes.length; offset++) matchedOriginal.add(originalIndexes[offset]), matchedTarget.add(targetIndexes[offset]), adoptDef(originalDefs[originalIndexes[offset]], canonicalDefs?.[originalIndexes[offset]], targetDefs[targetIndexes[offset]], originalIndexes.length === 1 ? "content-unchanged" : "same-position");
2753
+ }
2754
+ let originalRest = originalDefs.map((node, index) => ({
2755
+ node,
2756
+ index
2757
+ })).filter(({ index }) => !matchedOriginal.has(index)), targetRest = targetDefs.filter((_, index) => !matchedTarget.has(index));
2758
+ return originalRest.length === 1 && targetRest.length === 1 && originalRest[0].node._type === targetRest[0]._type && adoptDef(originalRest[0].node, canonicalDefs?.[originalRest[0].index], targetRest[0], "same-position"), keyMap;
2759
+ }
2760
+ function rewriteMarkReferences(block, keyMap) {
2761
+ if (keyMap.size === 0) return;
2762
+ let children = block.children;
2763
+ if (isTypedObjectArray(children)) for (let child of children) {
2764
+ let marks = child.marks;
2765
+ Array.isArray(marks) && (child.marks = marks.map((mark) => typeof mark == "string" && keyMap.has(mark) ? keyMap.get(mark) : mark));
2766
+ }
2767
+ }
2768
+ function groupByNeutralForm(nodes, exclude, aliasByKey) {
2769
+ let groups = /* @__PURE__ */ new Map();
2770
+ for (let index = 0; index < nodes.length; index++) {
2771
+ if (exclude.has(index)) continue;
2772
+ let neutral = aliasByKey ? encodeNeutral(nodes[index], aliasByKey) : neutralForm(nodes[index]), group = groups.get(neutral);
2773
+ group ? group.push(index) : groups.set(neutral, [index]);
2774
+ }
2775
+ return groups;
2776
+ }
2777
+ /**
2778
+ * A canonical JSON encoding that erases identity: `_key` properties
2779
+ * are dropped, object properties are sorted, and annotation `_key`
2780
+ * references inside `span.marks` are rewritten to the definition's
2781
+ * position in `markDefs` (dropping `_key` alone would compare the
2782
+ * stored annotation key against the fresh one and reject an unchanged
2783
+ * link).
2784
+ */
2785
+ function neutralForm(node) {
2786
+ return encodeNeutral(node, buildAliasMap(node));
2787
+ }
2788
+ /**
2789
+ * Aliases each `markDefs` key to a key-independent spelling of the
2790
+ * definition itself, so `span.marks` references compare by what the
2791
+ * annotation is rather than which key it carries. The spelling is the
2792
+ * definition's own neutral form, not its array position: aliasing by
2793
+ * position made every annotated sibling span's neutral form shift when
2794
+ * a definition was inserted or removed before its own, so adding one
2795
+ * link re-keyed unrelated annotated spans. Identical definitions (the
2796
+ * same link twice) are disambiguated by occurrence order among
2797
+ * identical forms only, which no unrelated insertion can shift.
2798
+ */
2799
+ function buildAliasMap(node) {
2800
+ let markDefs = node.markDefs;
2801
+ if (!isTypedObjectArray(markDefs)) return;
2802
+ let aliasByKey = /* @__PURE__ */ new Map(), occurrenceByForm = /* @__PURE__ */ new Map();
2803
+ for (let definition of markDefs) {
2804
+ let key = definition._key;
2805
+ if (typeof key != "string") continue;
2806
+ let form = encodeNeutral(definition, void 0), occurrence = occurrenceByForm.get(form) ?? 0;
2807
+ occurrenceByForm.set(form, occurrence + 1), aliasByKey.set(key, `@annotation:${occurrence}:${form}`);
2808
+ }
2809
+ return aliasByKey;
2810
+ }
2811
+ function encodeNeutral(value, aliasByKey) {
2812
+ return Array.isArray(value) ? `[${value.map((item) => encodeNeutral(item, aliasByKey)).join(",")}]` : typeof value == "object" && value ? `{${Object.entries(value).filter(([field]) => field !== "_key").sort(([a], [b]) => a < b ? -1 : +(a > b)).map(([field, fieldValue]) => {
2813
+ if (field === "marks" && aliasByKey && Array.isArray(fieldValue)) {
2814
+ let aliased = fieldValue.map((mark) => typeof mark == "string" && aliasByKey.has(mark) ? aliasByKey.get(mark) : mark);
2815
+ return `${JSON.stringify(field)}:${JSON.stringify(aliased)}`;
2816
+ }
2817
+ return `${JSON.stringify(field)}:${encodeNeutral(fieldValue, aliasByKey)}`;
2818
+ }).join(",")}}` : JSON.stringify(value) ?? "undefined";
2819
+ }
2820
+ /**
2821
+ * The block's comparison text: concatenated span text with inline
2822
+ * objects as sentinels. Marks are ignored, since a formatting-only
2823
+ * edit does not change textual identity.
2824
+ */
2825
+ function blockText(block) {
2826
+ let children = block.children;
2827
+ return isTypedObjectArray(children) ? children.map((child) => typeof child.text == "string" ? child.text : "").join("") : "";
2828
+ }
2829
+ /**
2830
+ * Text similarity in `[0, 1]`, gated to `0` unless the block shells
2831
+ * agree and the inline objects are compatible: however alike the
2832
+ * prose, blocks that disagree on structure are not the same block.
2833
+ * The length prescreen skips the diff when the size difference alone
2834
+ * puts the score under `MIN_BLOCK_SIMILARITY`.
2835
+ */
2836
+ function blockSimilarity(canonicalBlock, resultBlock) {
2837
+ if (!shellEquals(canonicalBlock, resultBlock) || !inlineObjectsCompatible(canonicalBlock, resultBlock)) return 0;
2838
+ let canonicalText = blockText(canonicalBlock), resultText = blockText(resultBlock);
2839
+ if (canonicalText.length === 0 || resultText.length === 0) return 0;
2840
+ let longer = Math.max(canonicalText.length, resultText.length);
2841
+ return 1 - Math.abs(canonicalText.length - resultText.length) / longer < MIN_BLOCK_SIMILARITY ? 0 : 1 - levenshteinFromDiffs(cleanupEfficiency(makeDiff(canonicalText, resultText, { timeout: .05 }))) / longer;
2842
+ }
2843
+ /**
2844
+ * The distance derivation from diff runs: insertions and deletions
2845
+ * between equality runs accumulate as the larger of the two, so a
2846
+ * delete-plus-insert counts as one substitution.
2847
+ */
2848
+ function levenshteinFromDiffs(diffs) {
2849
+ let distance = 0, insertions = 0, deletions = 0;
2850
+ for (let [operation, text] of diffs) operation === 1 ? insertions += text.length : operation === -1 ? deletions += text.length : (distance += Math.max(insertions, deletions), insertions = 0, deletions = 0);
2851
+ return distance + Math.max(insertions, deletions);
2852
+ }
2853
+ /**
2854
+ * The block minus its content: `_type`, `style`, `listItem`, `level`,
2855
+ * and any custom fields must agree before text similarity means
2856
+ * anything.
2857
+ */
2858
+ function shellEquals(a, b) {
2859
+ let shellOf = (node) => encodeNeutral(Object.fromEntries(Object.entries(node).filter(([field]) => field !== "children" && field !== "markDefs")), void 0);
2860
+ return shellOf(a) === shellOf(b);
2861
+ }
2862
+ /**
2863
+ * Inline objects are opaque content: two blocks whose objects differ
2864
+ * are different blocks no matter how similar their prose is.
2865
+ */
2866
+ function inlineObjectsCompatible(a, b) {
2867
+ let objectsOf = (node) => {
2868
+ let children = node.children;
2869
+ return isTypedObjectArray(children) ? children.filter((child) => typeof child.text != "string") : [];
2870
+ }, aObjects = objectsOf(a), bObjects = objectsOf(b);
2871
+ return aObjects.length === bObjects.length && aObjects.every((aObject, index) => {
2872
+ let bObject = bObjects[index];
2873
+ return aObject._type === bObject._type ? typeof aObject._key == "string" && aObject._key === bObject._key || neutralForm(aObject) === neutralForm(bObject) : !1;
2874
+ });
2875
+ }
2876
+ /**
2877
+ * The highest-scoring candidate, or `undefined` on a tie: a tie is
2878
+ * ambiguity, and ambiguity refuses adoption rather than guessing.
2879
+ */
2880
+ function uniqueBest(candidates, scoreOf) {
2881
+ let best, bestScore = 0, tied = !1;
2882
+ for (let candidate of candidates) {
2883
+ let score = scoreOf(candidate);
2884
+ score !== void 0 && (score > bestScore ? (best = candidate, bestScore = score, tied = !1) : score === bestScore && best !== void 0 && (tied = !0));
2885
+ }
2886
+ return tied ? void 0 : best;
2887
+ }
2888
+ /**
2889
+ * Deliberately loose: any node with a `children` array reconciles like
2890
+ * a text block, custom block types included.
2891
+ */
2892
+ function isTextBlock$1(node) {
2893
+ return Array.isArray(node.children);
2894
+ }
2895
+ /**
2896
+ * A text block the round trip drops. Whether a whitespace-only block
2897
+ * survives serialize→parse depends on the converters, not on
2898
+ * structure: a heading renders `## ` and survives, a custom style
2899
+ * falls back to a plain paragraph and vanishes, and a non-breaking
2900
+ * space renders "blank-looking" output the parser still keeps (JS
2901
+ * `trim` folds NBSP, CommonMark does not), so any string check here is
2902
+ * a hand-written mirror of one converter or the other that drifts. The
2903
+ * block's own render-then-reparse is the authority, the same round
2904
+ * trip `canonicalizeStored` performs, so this predicate cannot
2905
+ * disagree with the canonical node count. The JS-trim pre-check only
2906
+ * keeps the per-block round trip off paths that cannot qualify: it
2907
+ * over-admits candidates (NBSP text passes it), and the reparse then
2908
+ * decides.
2909
+ */
2910
+ function isEmptyTextBlock(node, options) {
2911
+ if (!(isTextBlock$1(node) && blockText(node).trim() === "")) return !1;
2912
+ let rendered = portableTextToMarkdown([structuredClone(node)], {
2913
+ ...options?.serialize,
2914
+ schema: options?.schema
2915
+ });
2916
+ if (rendered === "") return !0;
2917
+ let { onDegradation: _onDegradation, ...deserializeOptions } = options?.deserialize ?? {}, probeKeyCounter = 0;
2918
+ return markdownToPortableText(rendered, {
2919
+ ...deserializeOptions,
2920
+ schema: options?.schema,
2921
+ keyGenerator: () => `empty-probe-${probeKeyCounter++}`
2922
+ }).length === 0;
2923
+ }
2924
+ /**
2925
+ * Blank lines are markdown's block separator, so an empty text block
2926
+ * has no serialized form: `canonicalizeStored`'s round trip drops it,
2927
+ * the same way `markdownToPortableText` would if it were parsed back
2928
+ * from `editedMarkdown`. Tracing origins and aligning blocks over this
2929
+ * subsequence keeps both sides the same length; `reinsertEmptyRuns`
2930
+ * restores the dropped blocks afterward.
2931
+ */
2932
+ function nonEmptyBlocks(stored, options) {
2933
+ return stored.filter((node) => !isEmptyTextBlock(node, options));
2934
+ }
2935
+ function isTypedObjectArray(value) {
2936
+ return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item == "object" && !!item && typeof item._type == "string");
2937
+ }
2938
+ /**
2939
+ * Maximal runs of empty text blocks, each paired with the surviving
2940
+ * neighbor its restoration hangs off: a run with a preceding block
2941
+ * anchors to it (insert after); a run at the document's start, with
2942
+ * none, anchors to the block that follows it (insert before). A run
2943
+ * with neither (the whole document is empty blocks) has nothing to
2944
+ * anchor to and is dropped.
2945
+ */
2946
+ function findEmptyRuns(stored, options) {
2947
+ let runs = [], index = 0;
2948
+ for (; index < stored.length;) {
2949
+ if (!isEmptyTextBlock(stored[index], options)) {
2950
+ index++;
2951
+ continue;
2952
+ }
2953
+ let runStart = index;
2954
+ for (; index < stored.length && isEmptyTextBlock(stored[index], options);) index++;
2955
+ let precedingBlock = runStart > 0 ? stored[runStart - 1] : void 0, followingBlock = index < stored.length ? stored[index] : void 0, anchor = precedingBlock ?? followingBlock;
2956
+ anchor && typeof anchor._key == "string" && runs.push({
2957
+ anchorKey: anchor._key,
2958
+ insertAfter: precedingBlock !== void 0,
2959
+ blocks: stored.slice(runStart, index)
2960
+ });
2961
+ }
2962
+ return runs;
2963
+ }
2964
+ /**
2965
+ * Restores each empty run next to the result node that adopted its
2966
+ * anchor's key, found by `_key` since positions have already shifted
2967
+ * under insertion, deletion, and move. An anchor whose key did not
2968
+ * survive into `result` (its region was rewritten or deleted) drops
2969
+ * the run with it, consistent with rewrite semantics elsewhere in this
2970
+ * module. Runs are cloned and marked adopted, the same authoritative
2971
+ * status as every other restored key, so a collision resolves in
2972
+ * their favor like `enforceSiblingKeyUniqueness` already does for
2973
+ * `json:object` duplicates.
2974
+ */
2975
+ function reinsertEmptyRuns(stored, result, adoptedNodes, options, recorder) {
2976
+ for (let run of findEmptyRuns(stored, options)) {
2977
+ let adoptedAnchorIndex = result.findIndex((node) => node._key === run.anchorKey && adoptedNodes.has(node)), anchorIndex = adoptedAnchorIndex === -1 ? result.findIndex((node) => node._key === run.anchorKey) : adoptedAnchorIndex;
2978
+ if (anchorIndex === -1) continue;
2979
+ let clones = run.blocks.map((block) => structuredClone(block));
2980
+ for (let clone of clones) adoptedNodes.add(clone), recorder && tagSubtreePreserved(clone, recorder);
2981
+ result.splice(run.insertAfter ? anchorIndex + 1 : anchorIndex, 0, ...clones);
2982
+ }
2983
+ }
2984
+ /**
2985
+ * Every keyed node in a reinserted empty run is the stored value
2986
+ * verbatim, at every depth, so key resolution reporting tags the
2987
+ * whole subtree `content-unchanged` rather than only the run's top
2988
+ * block.
2989
+ */
2990
+ function tagSubtreePreserved(node, recorder) {
2991
+ typeof node._key == "string" && recorder.preservationBasis.set(node, "content-unchanged");
2992
+ for (let value of Object.values(node)) if (Array.isArray(value) && value.every((item) => typeof item == "object" && !!item) && value.length > 0) for (let child of value) tagSubtreePreserved(child, recorder);
2993
+ else typeof value == "object" && value && tagSubtreePreserved(value, recorder);
2994
+ }
2995
+ /**
2996
+ * `json:object` payloads transport their `_key` verbatim, so a
2997
+ * copy-pasted fence puts the same key on two siblings, and everything
2998
+ * downstream (patches, anchors, editor normalization) assumes sibling
2999
+ * keys are unique. Adopted keys are authoritative, so a duplicate that
3000
+ * was adopted from the stored value wins and the other occurrences are
3001
+ * regenerated.
3002
+ */
3003
+ function enforceSiblingKeyUniqueness(nodes, keyGenerator, adoptedNodes, recorder, onKeyRewritten) {
3004
+ let usedKeys = /* @__PURE__ */ new Set(), claim = (node) => {
3005
+ let key = node._key;
3006
+ if (typeof key == "string") {
3007
+ if (usedKeys.has(key)) {
3008
+ let freshKey;
3009
+ for (let attempt = 0; attempt < 3; attempt++) {
3010
+ let candidate = keyGenerator();
3011
+ if (!usedKeys.has(candidate)) {
3012
+ freshKey = candidate;
3013
+ break;
3014
+ }
3015
+ }
3016
+ if (freshKey === void 0) {
3017
+ let suffix = 1, candidate = `${key}_${suffix}`;
3018
+ for (; usedKeys.has(candidate);) suffix++, candidate = `${key}_${suffix}`;
3019
+ freshKey = candidate;
3020
+ }
3021
+ node._key = freshKey, usedKeys.add(freshKey), onKeyRewritten?.(key, freshKey), recorder?.renamePreviousKey.set(node, key);
3022
+ return;
3023
+ }
3024
+ usedKeys.add(key);
3025
+ }
3026
+ }, adopted = nodes.filter((node) => adoptedNodes.has(node)), rest = nodes.filter((node) => !adoptedNodes.has(node));
3027
+ for (let node of [...adopted, ...rest]) claim(node);
3028
+ for (let node of nodes) enforceNestedSiblingKeyUniqueness(node, keyGenerator, adoptedNodes, recorder);
3029
+ }
3030
+ function enforceNestedSiblingKeyUniqueness(node, keyGenerator, adoptedNodes, recorder) {
3031
+ for (let [field, value] of Object.entries(node)) Array.isArray(value) && value.every((item) => typeof item == "object" && !!item) && value.length > 0 ? enforceSiblingKeyUniqueness(value, keyGenerator, adoptedNodes, recorder, field === "markDefs" ? buildMarkDefKeyRewriter(node) : void 0) : typeof value == "object" && value && enforceNestedSiblingKeyUniqueness(value, keyGenerator, adoptedNodes, recorder);
3032
+ }
3033
+ /**
3034
+ * A colliding `keyGenerator` can mint the identical string for two
3035
+ * `markDefs` entries, which means the block's spans already reference
3036
+ * that shared string ambiguously before any rewrite happens: a
3037
+ * blanket find-and-replace of the old key would move every span's
3038
+ * reference, including the one that was never renamed. The
3039
+ * `markDefs` array and each span's `marks` are walked in the same
3040
+ * fixed document order, so the Nth occurrence of a given key in one
3041
+ * lines up with the Nth occurrence in the other; the first occurrence
3042
+ * is always the survivor (`enforceSiblingKeyUniqueness` only renames
3043
+ * on collision, never the first sighting of a key), so each rename
3044
+ * event retargets the next occurrence in that shared order instead of
3045
+ * every occurrence.
3046
+ */
3047
+ function buildMarkDefKeyRewriter(block) {
3048
+ let children = block.children;
3049
+ if (!isTypedObjectArray(children)) return () => {};
3050
+ let occurrencesByKey = /* @__PURE__ */ new Map();
3051
+ for (let child of children) {
3052
+ let marks = child.marks;
3053
+ if (Array.isArray(marks)) for (let markIndex = 0; markIndex < marks.length; markIndex++) {
3054
+ let mark = marks[markIndex];
3055
+ if (typeof mark != "string") continue;
3056
+ let occurrences = occurrencesByKey.get(mark) ?? [];
3057
+ occurrences.push({
3058
+ child,
3059
+ markIndex
3060
+ }), occurrencesByKey.set(mark, occurrences);
3061
+ }
3062
+ }
3063
+ let consumedByKey = /* @__PURE__ */ new Map();
3064
+ return (oldKey, newKey) => {
3065
+ let occurrences = occurrencesByKey.get(oldKey), index = consumedByKey.get(oldKey) ?? 1;
3066
+ consumedByKey.set(oldKey, index + 1);
3067
+ let target = occurrences?.[index];
3068
+ if (!target) return;
3069
+ let marks = target.child.marks;
3070
+ Array.isArray(marks) && (marks[target.markIndex] = newKey);
3071
+ };
3072
+ }
3073
+ /**
3074
+ * Materializes the public report from the recorder's node-identity
3075
+ * decisions, walking the settled result tree once so every `key` and
3076
+ * `path` matches the returned value exactly: key resolution records
3077
+ * decisions before the sibling-uniqueness pass can still rewrite a
3078
+ * key, so keys and paths are only trustworthy read back from the
3079
+ * final tree, not from the moment a decision was made.
3080
+ */
3081
+ function buildReconciliationReport(result, recorder) {
3082
+ let preservedKeys = [], renamedKeys = [], pathByNode = /* @__PURE__ */ new WeakMap(), walk = (node, path) => {
3083
+ pathByNode.set(node, path);
3084
+ let key = node._key;
3085
+ if (typeof key == "string") {
3086
+ let basis = recorder.preservationBasis.get(node), previousKey = recorder.renamePreviousKey.get(node);
3087
+ basis && previousKey === void 0 && preservedKeys.push({
3088
+ basis,
3089
+ key,
3090
+ path
3091
+ }), previousKey !== void 0 && renamedKeys.push({
3092
+ previousKey,
3093
+ key,
3094
+ path
3095
+ });
3096
+ }
3097
+ for (let [field, value] of Object.entries(node)) Array.isArray(value) && value.every((item) => typeof item == "object" && !!item) && value.length > 0 ? value.forEach((child, index) => {
3098
+ let childKey = child._key;
3099
+ walk(child, [
3100
+ ...path,
3101
+ field,
3102
+ typeof childKey == "string" ? { _key: childKey } : index
3103
+ ]);
3104
+ }) : typeof value == "object" && value && walk(value, [...path, field]);
3105
+ };
3106
+ if (result.forEach((block, index) => {
3107
+ let key = block._key;
3108
+ walk(block, [typeof key == "string" ? { _key: key } : index]);
3109
+ }), recorder.skipReason) return {
3110
+ keyMatching: "skipped",
3111
+ reason: recorder.skipReason,
3112
+ renamedKeys
3113
+ };
3114
+ let keyFallbacks = [];
3115
+ for (let group of recorder.ambiguousRegionGroups) keyFallbacks.push({
3116
+ type: "ambiguous-region-too-large",
3117
+ keys: group.map((node) => node._key).filter((key) => typeof key == "string")
3118
+ });
3119
+ for (let node of recorder.annotationKeyConflicts) keyFallbacks.push({
3120
+ type: "annotation-key-conflict",
3121
+ path: pathByNode.get(node)
3122
+ });
3123
+ return {
3124
+ keyMatching: "performed",
3125
+ preservedKeys,
3126
+ keyFallbacks,
3127
+ renamedKeys
3128
+ };
3129
+ }
3130
+ export { DefaultBlockSpacingRenderer, DefaultBlockquoteObjectRenderer, DefaultBlockquoteRenderer, DefaultCalloutRenderer, DefaultCodeBlockRenderer, DefaultCodeRenderer, DefaultEmRenderer, DefaultH1Renderer, DefaultH2Renderer, DefaultH3Renderer, DefaultH4Renderer, DefaultH5Renderer, DefaultH6Renderer, DefaultHardBreakRenderer, DefaultHorizontalRuleRenderer, DefaultHtmlRenderer, DefaultImageRenderer, DefaultLinkRenderer, DefaultListItemRenderer, DefaultListRenderer, DefaultNormalRenderer, DefaultStrikeThroughRenderer, DefaultStrongRenderer, DefaultTableRenderer, DefaultUnderlineRenderer, applyMarkdownEdit, markdownToPortableText, portableTextToMarkdown };
2238
3131
 
2239
3132
  //# sourceMappingURL=index.js.map