@portabletext/markdown 2.1.0 → 2.2.0

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