@stll/folio-core 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/ai-edits/apply.js +565 -121
  2. package/dist/ai-edits/minimal-replacement.d.ts +64 -0
  3. package/dist/ai-edits/minimal-replacement.js +282 -0
  4. package/dist/controller/layoutPipeline.js +11 -1
  5. package/dist/display-list/build/paragraphPrimitives.js +63 -7
  6. package/dist/display-list/build/textBoxPrimitives.js +1 -1
  7. package/dist/display-list/build/unsupported.d.ts +2 -2
  8. package/dist/display-list/build/unsupported.js +0 -0
  9. package/dist/display-list/dom/renderDisplayListToDom.js +0 -1
  10. package/dist/docx/blockContentParser.js +2 -2
  11. package/dist/docx/bulletMarkers.d.ts +18 -2
  12. package/dist/docx/bulletMarkers.js +27 -2
  13. package/dist/docx/drawingGroupChildren.d.ts +60 -0
  14. package/dist/docx/drawingGroupChildren.js +176 -0
  15. package/dist/docx/drawingUtils.js +3 -0
  16. package/dist/docx/groupDrawingParser.d.ts +7 -1
  17. package/dist/docx/groupDrawingParser.js +123 -32
  18. package/dist/docx/packageParts.d.ts +45 -1
  19. package/dist/docx/packageParts.js +93 -2
  20. package/dist/docx/paraIdAttribute.d.ts +3 -2
  21. package/dist/docx/paragraphParser.js +91 -7
  22. package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
  23. package/dist/docx/runParser.js +10 -0
  24. package/dist/docx/selectiveSaveFlags.d.ts +10 -3
  25. package/dist/docx/selectiveSaveFlags.js +1 -1
  26. package/dist/docx/selectiveXmlPatch.d.ts +56 -34
  27. package/dist/docx/selectiveXmlPatch.js +222 -115
  28. package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
  29. package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
  30. package/dist/docx/serializer/paragraphSerializer.js +5 -4
  31. package/dist/docx/serializer/runSerializer.d.ts +5 -1
  32. package/dist/docx/serializer/runSerializer.js +4 -3
  33. package/dist/docx/shapeAlternateContent.d.ts +25 -0
  34. package/dist/docx/shapeAlternateContent.js +52 -0
  35. package/dist/docx/textBoxParser.d.ts +10 -2
  36. package/dist/docx/textBoxParser.js +9 -3
  37. package/dist/internal/headlessRevisionResolution.js +50 -13
  38. package/dist/layout-bridge/convert/toFlowBlocks.js +26 -4
  39. package/dist/layout-engine/index.d.ts +2 -2
  40. package/dist/layout-engine/index.js +33 -4
  41. package/dist/layout-engine/keep-together.d.ts +5 -2
  42. package/dist/layout-engine/keep-together.js +5 -1
  43. package/dist/layout-engine/measure/advanceComposition.js +49 -3
  44. package/dist/layout-engine/measure/measureBlocks.js +5 -3
  45. package/dist/layout-engine/measure/measureContainer.js +46 -3
  46. package/dist/layout-engine/measure/measureHelpers.js +13 -3
  47. package/dist/layout-engine/measure/measureParagraph.js +44 -81
  48. package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
  49. package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
  50. package/dist/layout-engine/paginator.d.ts +2 -0
  51. package/dist/layout-engine/paginator.js +5 -1
  52. package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
  53. package/dist/layout-engine/paragraphSpacing.js +2 -1
  54. package/dist/layout-engine/tableRowBreak.d.ts +8 -1
  55. package/dist/layout-engine/tableRowBreak.js +22 -1
  56. package/dist/layout-engine/types.d.ts +21 -2
  57. package/dist/layout-painter/renderParagraph.js +41 -4
  58. package/dist/layout-painter/renderTextBox.d.ts +12 -2
  59. package/dist/layout-painter/renderTextBox.js +26 -2
  60. package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
  61. package/dist/prosemirror/alternateContentAttrs.js +35 -0
  62. package/dist/prosemirror/attrs/index.js +90 -0
  63. package/dist/prosemirror/commands/comments.js +51 -41
  64. package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
  65. package/dist/prosemirror/contentControlRevisions.js +84 -0
  66. package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
  67. package/dist/prosemirror/conversion/toProseDoc.js +42 -19
  68. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
  69. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
  70. package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
  71. package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
  72. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
  73. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
  74. package/dist/prosemirror/listMarker.js +2 -2
  75. package/dist/prosemirror/paragraphDirection.d.ts +29 -2
  76. package/dist/prosemirror/paragraphDirection.js +21 -2
  77. package/dist/prosemirror/plugins/suggestionMode.js +37 -8
  78. package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
  79. package/dist/prosemirror/rejoinRunCarriers.js +66 -0
  80. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
  81. package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
  82. package/dist/prosemirror/schema/nodes.d.ts +37 -5
  83. package/dist/types/content.d.ts +2 -2
  84. package/package.json +2 -2
  85. package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
  86. package/dist/layout-engine/justifiedLineFit.js +0 -6
@@ -3,6 +3,7 @@ import { formattingEquals } from "../docx/runConsolidator.js";
3
3
  import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
4
4
  import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
5
5
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
6
+ import { encloseWholeControls } from "../prosemirror/contentControlRevisions.js";
6
7
  import { requestDeterministicParaIds } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
7
8
  import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
8
9
  import { CLEARED_LIST_RENDERING_ATTRS } from "../prosemirror/listMarker.js";
@@ -12,7 +13,7 @@ import { directParagraphIndentation, paragraphIndentationAttrPatch, paragraphInd
12
13
  import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
13
14
  import { getDocumentNumbering } from "../prosemirror/plugins/documentNumbering.js";
14
15
  import { getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
15
- import { addCarriedMarks, annotatedReplacement, hasReplacedAnnotations, inheritedReplacementMarks, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
16
+ import { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, hasReplacedAnnotations, inheritedReplacementMarks, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
16
17
  import { applyMarksToRunFormattingRepresentation, runFormattingInlineControlNodeName, selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
17
18
  import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
18
19
  import { paragraphRunStyleContext, paragraphRunStyleContextAt } from "../prosemirror/runStyleFormatting.js";
@@ -22,6 +23,7 @@ import { getFolioParaIdFromBlockId } from "../types/block-id.js";
22
23
  import { stripBlockIdentityAttrs } from "./block-identity.js";
23
24
  import { buildCleanBlockText, resolveCleanTextRange } from "./clean-text.js";
24
25
  import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers } from "./inline-emphasis.js";
26
+ import { applyTextChanges, changesFromSegments, planTextChanges, widenChangesToAtomicSpans } from "./minimal-replacement.js";
25
27
  import { hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
26
28
  import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
27
29
  import { planTableMutations } from "./table-mutation-plan.js";
@@ -612,7 +614,14 @@ const clearReplacementBackground = ({ tr, schema, from, to, mode, replacementBac
612
614
  ...styleResolver !== void 0 ? { styleResolver } : {}
613
615
  });
614
616
  };
615
- const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
617
+ const applyTrackedInlineFormatting = ({ from, to, ...options }) => applyTrackedInlineFormattingToRanges({
618
+ ...options,
619
+ ranges: [{
620
+ from,
621
+ to
622
+ }]
623
+ });
624
+ const applyTrackedInlineFormattingToRanges = ({ tr, schema, doc = tr.doc, ranges, formatting, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
616
625
  const propertyChangeType = schema.marks["runPropertyChange"];
617
626
  if (!propertyChangeType) return {
618
627
  type: "applied",
@@ -622,11 +631,11 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
622
631
  };
623
632
  const segments = [];
624
633
  let hasPendingRunPropertyChange = false;
625
- const representations = selectRunFormattingCarrierRepresentations({
634
+ const representations = ranges.flatMap(({ from, to }) => selectRunFormattingCarrierRepresentations({
626
635
  doc,
627
636
  from,
628
637
  to
629
- });
638
+ }));
630
639
  for (const representation of representations) {
631
640
  const { node } = representation;
632
641
  const styleContext = paragraphRunStyleContextAt({
@@ -717,36 +726,17 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
717
726
  * `Date.now()` seed per applyAIEditOperations call would collide
718
727
  * across batches that fire within the same millisecond (the panel's
719
728
  * Accept-all loop does exactly that — multiple calls in tight
720
- * succession). Reserving a contiguous range up front guarantees
721
- * uniqueness across overlapping calls in the same JS realm.
729
+ * succession).
730
+ *
731
+ * A batch without a revision stamp allocates from it as it writes and claims
732
+ * each operation's ids once they are written, rather than reserving a guess
733
+ * up front: how many revisions an operation records is known only once it has
734
+ * recorded them (clearing a background records one run-property change per
735
+ * carrier). The claim also comes before anything that may start another
736
+ * batch, a comment-id callback or the dispatch, so batches in one realm never
737
+ * share an id and follow one another without a gap.
722
738
  */
723
739
  let revisionIdCursor = Date.now() * 1e3;
724
- const nextRevisionSeed = (revisionIdCount) => {
725
- const start = revisionIdCursor;
726
- revisionIdCursor += Math.max(revisionIdCount, 1);
727
- return start;
728
- };
729
- /**
730
- * Ids one resolved operation may allocate from the shared revision-id range
731
- * reserved by `nextRevisionSeed`. Most operation types allocate at most
732
- * three (delete + insert + background-format clearing); reserving four per
733
- * operation is a safe cushion above that. A multi-paragraph
734
- * `insertAfterBlock` / `insertBeforeBlock` (`text` split on line breaks,
735
- * see `splitInsertParagraphTexts`) allocates one id per paragraph in
736
- * tracked-changes mode plus one for each paragraph MARK it brings, so it
737
- * needs more than four once split into more than two paragraphs. Rotating a
738
- * terminal run can add one paragraph-property revision per inserted paragraph;
739
- * reserving less than that would let a later `nextRevisionSeed` call reuse an
740
- * id this operation already stamped on the document.
741
- */
742
- const REVISION_IDS_PER_OPERATION = 4;
743
- const estimateRevisionIdReservation = (item) => {
744
- if (item.operation.type === "insertAfterBlock" || item.operation.type === "insertBeforeBlock") {
745
- const paragraphCount = item.insertTexts?.length ?? 1;
746
- return Math.max(paragraphCount * 3, REVISION_IDS_PER_OPERATION);
747
- }
748
- return REVISION_IDS_PER_OPERATION;
749
- };
750
740
  /**
751
741
  * Walk the live doc once and bucket every textblock by its
752
742
  * normalised text hash. Resolution then maps each snapshot anchor
@@ -1404,9 +1394,19 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1404
1394
  });
1405
1395
  }
1406
1396
  let tr = view.state.tr;
1407
- const revisionIdReservation = executableResolved.reduce((total, item) => total + estimateRevisionIdReservation(item), 0);
1408
1397
  const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
1409
- let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionSeed(revisionIdReservation);
1398
+ let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor;
1399
+ /**
1400
+ * Claim the shared ids below `nextRevisionId` for this batch. A seeded or
1401
+ * stamped batch allocates from its own range and claims nothing.
1402
+ */
1403
+ const claimSharedRevisionIds = (nextRevisionId) => {
1404
+ if (ownsSharedRevisionIdCursor) revisionIdCursor = Math.max(revisionIdCursor, nextRevisionId);
1405
+ };
1406
+ /** Continue past any shared id a batch the host started meanwhile claimed. */
1407
+ const continueSharedRevisionIds = () => {
1408
+ if (ownsSharedRevisionIdCursor) revisionSeed = Math.max(revisionSeed, revisionIdCursor);
1409
+ };
1410
1410
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
1411
1411
  const insertedColumnCounts = /* @__PURE__ */ new Map();
1412
1412
  const executionOrder = executableResolved.toSorted((left, right) => {
@@ -1437,6 +1437,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1437
1437
  for (let executionIndex = 0; executionIndex < executionOrder.length; executionIndex++) {
1438
1438
  const item = executionOrder[executionIndex];
1439
1439
  if (!item) panic("The operation execution index exceeded the resolved plan", { executionIndex });
1440
+ continueSharedRevisionIds();
1440
1441
  if (mode === "tracked-changes" && writesParagraphPropertyChange(item)) {
1441
1442
  const livePosition = tr.mapping.map(item.blockFrom);
1442
1443
  const liveBlock = tr.doc.nodeAt(livePosition) ?? item.blockNode;
@@ -1475,6 +1476,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1475
1476
  numbering
1476
1477
  });
1477
1478
  revisionSeed = built.nextRevisionId;
1479
+ claimSharedRevisionIds(revisionSeed);
1478
1480
  nodeGroups.push(built.nodes);
1479
1481
  runApplied.push({
1480
1482
  id: insertion.operation.id,
@@ -1522,9 +1524,58 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1522
1524
  switch (item.operation.type) {
1523
1525
  case "replaceInBlock":
1524
1526
  case "replaceRange": {
1527
+ if (mode === "direct" && !hasInlineEmphasis(item.operation.replace)) {
1528
+ const minimal = applyMinimalDirectReplacement({
1529
+ tr,
1530
+ item,
1531
+ replacement: item.operation.replace,
1532
+ commentMark,
1533
+ replacementBackground,
1534
+ ...styleResolver !== void 0 ? { styleResolver } : {}
1535
+ });
1536
+ if (minimal !== null) {
1537
+ tr = minimal;
1538
+ break;
1539
+ }
1540
+ }
1525
1541
  const revisionIdDelete = operationRevisionSeed;
1526
1542
  const revisionIdInsert = producesTrackedChanges ? operationRevisionSeed + 1 : operationRevisionSeed;
1527
1543
  const revisionIdBackgroundSeed = producesTrackedChanges ? operationRevisionSeed + 2 : operationRevisionSeed;
1544
+ if (producesTrackedChanges) {
1545
+ const tracked = applyMinimalTrackedReplacement({
1546
+ tr,
1547
+ item,
1548
+ replacement: stripInlineEmphasisMarkers(item.operation.replace),
1549
+ commentMark,
1550
+ replacementBackground,
1551
+ ...styleResolver !== void 0 ? { styleResolver } : {},
1552
+ diffText,
1553
+ revisionIdDelete,
1554
+ revisionIdInsert,
1555
+ revisionIdBackgroundSeed,
1556
+ author,
1557
+ date,
1558
+ initials,
1559
+ suggestionId
1560
+ });
1561
+ if (tracked?.type === "pendingRunPropertyChange") {
1562
+ skipped.push({
1563
+ id: item.operation.id,
1564
+ reason: tracked.type
1565
+ });
1566
+ continue;
1567
+ }
1568
+ if (tracked) {
1569
+ tr = tracked.transaction;
1570
+ operationRevisionSeed = tracked.nextRevisionId;
1571
+ appliedRevisionIds = [
1572
+ revisionIdDelete,
1573
+ revisionIdInsert,
1574
+ ...tracked.backgroundRevisionIds
1575
+ ];
1576
+ break;
1577
+ }
1578
+ }
1528
1579
  const stepsBeforeBackgroundClear = tr.steps.length;
1529
1580
  const backgroundResult = clearReplacementBackground({
1530
1581
  tr,
@@ -1560,8 +1611,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1560
1611
  revisionIdInsert,
1561
1612
  commentMark,
1562
1613
  suggestionId,
1563
- initials,
1564
- diffText
1614
+ initials
1565
1615
  });
1566
1616
  if (producesTrackedChanges) appliedRevisionIds = [
1567
1617
  revisionIdDelete,
@@ -1652,7 +1702,44 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1652
1702
  }
1653
1703
  let clearedBackground = false;
1654
1704
  let backgroundRevisionIds = [];
1655
- if (changesText) {
1705
+ const minimal = changesText && mode === "direct" ? applyMinimalDirectReplacement({
1706
+ tr,
1707
+ item,
1708
+ replacement: item.operation.text,
1709
+ commentMark,
1710
+ replacementBackground,
1711
+ ...styleResolver !== void 0 ? { styleResolver } : {}
1712
+ }) : null;
1713
+ const tracked = changesText && producesTrackedChanges ? applyMinimalTrackedReplacement({
1714
+ tr,
1715
+ item,
1716
+ replacement: stripInlineEmphasisMarkers(item.operation.text),
1717
+ commentMark,
1718
+ replacementBackground,
1719
+ ...styleResolver !== void 0 ? { styleResolver } : {},
1720
+ diffText,
1721
+ revisionIdDelete,
1722
+ revisionIdInsert,
1723
+ revisionIdBackgroundSeed,
1724
+ author,
1725
+ date,
1726
+ initials,
1727
+ suggestionId
1728
+ }) : null;
1729
+ if (tracked?.type === "pendingRunPropertyChange") {
1730
+ skipped.push({
1731
+ id: item.operation.id,
1732
+ reason: tracked.type
1733
+ });
1734
+ continue;
1735
+ }
1736
+ if (minimal !== null) tr = minimal;
1737
+ else if (tracked !== null) {
1738
+ tr = tracked.transaction;
1739
+ operationRevisionSeed = tracked.nextRevisionId;
1740
+ clearedBackground = tracked.backgroundRevisionIds.length > 0;
1741
+ backgroundRevisionIds = tracked.backgroundRevisionIds;
1742
+ } else if (changesText) {
1656
1743
  const stepsBeforeBackgroundClear = tr.steps.length;
1657
1744
  const backgroundResult = clearReplacementBackground({
1658
1745
  tr,
@@ -1689,8 +1776,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1689
1776
  revisionIdInsert,
1690
1777
  commentMark,
1691
1778
  suggestionId,
1692
- initials,
1693
- diffText
1779
+ initials
1694
1780
  });
1695
1781
  }
1696
1782
  const allocateStyleRevisionInfo = () => ({
@@ -1993,6 +2079,12 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1993
2079
  tr = tr.addMark(item.from, item.to, deletionMark);
1994
2080
  const atomRanges = undeletedContentAtomRanges(tr, item.blockFrom);
1995
2081
  for (const { from, to } of atomRanges) tr = tr.addMark(from, to, deletionMark);
2082
+ encloseWholeControls({
2083
+ tr,
2084
+ from: item.blockFrom,
2085
+ to: item.blockTo,
2086
+ revisionId
2087
+ });
1996
2088
  const inlineRevisionApplied = item.from < item.to || atomRanges.length > 0;
1997
2089
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
1998
2090
  const markPosition = tr.mapping.map(item.blockFrom);
@@ -2263,6 +2355,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2263
2355
  operationId: item.operation.id,
2264
2356
  operationType: item.operation.type
2265
2357
  });
2358
+ claimSharedRevisionIds(operationRevisionSeed);
2266
2359
  committedCommentId = allocateCommittedCommentId(item.commentText);
2267
2360
  tr = commitProvisionalComment({
2268
2361
  tr,
@@ -2272,6 +2365,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2272
2365
  });
2273
2366
  }
2274
2367
  revisionSeed = operationRevisionSeed;
2368
+ claimSharedRevisionIds(revisionSeed);
2275
2369
  applied.push({
2276
2370
  id: item.operation.id,
2277
2371
  ...committedCommentId !== void 0 && { commentId: committedCommentId },
@@ -2285,6 +2379,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2285
2379
  if (tr.docChanged) {
2286
2380
  const batchRevisionIds = new Set(applied.flatMap(({ revisionIds }) => revisionIds ?? []));
2287
2381
  tr = withoutRevisionsOnZeroWidthAnchors(tr, batchRevisionIds);
2382
+ continueSharedRevisionIds();
2288
2383
  const rotated = withRotatedAddedFinalBreaks({
2289
2384
  tr,
2290
2385
  batchRevisionIds,
@@ -2309,7 +2404,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2309
2404
  revisionIds: [...receipt.revisionIds, revisionId]
2310
2405
  };
2311
2406
  }
2312
- if (ownsSharedRevisionIdCursor) revisionIdCursor = Math.max(revisionIdCursor, revisionSeed);
2407
+ claimSharedRevisionIds(revisionSeed);
2313
2408
  if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
2314
2409
  view.dispatch(tr);
2315
2410
  }
@@ -2389,7 +2484,7 @@ const insertCleanText = ({ tr, from, to, text, standsInFor }) => {
2389
2484
  end: start + contentSize
2390
2485
  };
2391
2486
  };
2392
- const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark, suggestionId = null, initials, diffText }) => {
2487
+ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark, suggestionId = null, initials }) => {
2393
2488
  let nextTr = tr;
2394
2489
  const replacement = stripInlineEmphasisMarkers((() => {
2395
2490
  if (item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") return item.operation.replace;
@@ -2435,86 +2530,6 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2435
2530
  ...initialsAttr,
2436
2531
  ...suggestionAttrs
2437
2532
  };
2438
- const cleanBlock = item.blockNode.content.size === item.blockNode.textContent.length ? buildCleanBlockText(item.blockNode, item.blockFrom) : null;
2439
- let sourceText = null;
2440
- let sourceCleanStart = 0;
2441
- if (cleanBlock !== null) {
2442
- if (item.operation.type === "replaceInBlock") {
2443
- sourceText = item.operation.find;
2444
- sourceCleanStart = cleanBlock.text.indexOf(item.operation.find);
2445
- if (sourceCleanStart === -1) sourceText = null;
2446
- } else if (item.operation.type === "replaceRange") {
2447
- sourceCleanStart = item.operation.range.startOffset;
2448
- sourceText = cleanBlock.text.slice(sourceCleanStart, item.operation.range.endOffset);
2449
- } else if (item.operation.type === "replaceBlock") {
2450
- sourceText = cleanBlock.text;
2451
- sourceCleanStart = 0;
2452
- }
2453
- }
2454
- if (sourceText !== null && cleanBlock !== null) {
2455
- const segments = diffText(sourceText, replacement);
2456
- const offsets = cleanBlock.offsets;
2457
- const offsetAt = (cleanOffset) => offsets[cleanOffset] ?? null;
2458
- const steps = [];
2459
- let cursor = 0;
2460
- let allPositionsResolved = true;
2461
- for (const seg of segments) {
2462
- if (seg.type === "equal") {
2463
- cursor += seg.text.length;
2464
- continue;
2465
- }
2466
- if (seg.type === "del") {
2467
- const pmFrom = offsetAt(sourceCleanStart + cursor);
2468
- const pmTo = offsetAt(sourceCleanStart + cursor + seg.text.length);
2469
- if (pmFrom === null || pmTo === null) {
2470
- allPositionsResolved = false;
2471
- break;
2472
- }
2473
- steps.push({
2474
- kind: "del",
2475
- from: pmFrom,
2476
- to: pmTo
2477
- });
2478
- cursor += seg.text.length;
2479
- continue;
2480
- }
2481
- const pmAt = offsetAt(sourceCleanStart + cursor);
2482
- if (pmAt === null) {
2483
- allPositionsResolved = false;
2484
- break;
2485
- }
2486
- steps.push({
2487
- kind: "ins",
2488
- at: pmAt,
2489
- text: seg.text
2490
- });
2491
- }
2492
- if (allPositionsResolved) {
2493
- for (const step of steps.toReversed()) {
2494
- if (step.kind === "del" && deletionType) {
2495
- nextTr = nextTr.addMark(step.from, step.to, deletionType.create(delAttrs));
2496
- if (commentMark) nextTr = nextTr.addMark(step.from, step.to, commentMark);
2497
- continue;
2498
- }
2499
- if (step.kind === "ins" && insertionType) {
2500
- const inserted = insertCleanText({
2501
- tr: nextTr,
2502
- from: step.at,
2503
- to: step.at,
2504
- text: step.text,
2505
- standsInFor: {
2506
- from: item.from,
2507
- to: item.to
2508
- }
2509
- });
2510
- nextTr = inserted.transaction;
2511
- nextTr = nextTr.addMark(inserted.start, inserted.end, insertionType.create(insAttrs));
2512
- if (commentMark) nextTr = nextTr.addMark(inserted.start, inserted.end, commentMark);
2513
- }
2514
- }
2515
- return nextTr;
2516
- }
2517
- }
2518
2533
  if (replacement.length > 0 && insertionType) {
2519
2534
  const inserted = insertCleanText({
2520
2535
  tr: nextTr,
@@ -2536,6 +2551,435 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2536
2551
  }
2537
2552
  return nextTr;
2538
2553
  };
2554
+ /**
2555
+ * The clean-text span a text replacement matched: where it starts in the
2556
+ * block's clean text and what it says. `null` when the operation does not
2557
+ * replace text or its match is no longer in the block.
2558
+ */
2559
+ const replacedCleanSpan = (item, cleanBlock) => {
2560
+ switch (item.operation.type) {
2561
+ case "replaceInBlock": {
2562
+ const start = cleanBlock.text.indexOf(item.operation.find);
2563
+ return start === -1 ? null : {
2564
+ start,
2565
+ text: item.operation.find
2566
+ };
2567
+ }
2568
+ case "replaceRange": {
2569
+ const { startOffset, endOffset } = item.operation.range;
2570
+ return {
2571
+ start: startOffset,
2572
+ text: cleanBlock.text.slice(startOffset, endOffset)
2573
+ };
2574
+ }
2575
+ case "replaceBlock": return {
2576
+ start: 0,
2577
+ text: cleanBlock.text
2578
+ };
2579
+ default: return null;
2580
+ }
2581
+ };
2582
+ const BACKGROUND_MARK_NAMES = /* @__PURE__ */ new Set(["highlight", "runShading"]);
2583
+ /**
2584
+ * Formatting, as opposed to a mark naming something outside the text or
2585
+ * describing one character (see `NON_INCLUSIVE_MARK_DISPOSITION`).
2586
+ */
2587
+ const isFormattingMark = (mark) => mark.type.spec.inclusive !== false;
2588
+ const isCarriedMark = (mark) => NON_INCLUSIVE_MARK_DISPOSITION[mark.type.name] === "carry";
2589
+ /**
2590
+ * Map a text replacement's changes onto the document; `minimal-replacement.ts`
2591
+ * states the contract. `null` when the matched span cannot be mapped onto the
2592
+ * document character by character, or the changes do not produce the
2593
+ * replacement.
2594
+ */
2595
+ const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2596
+ const cleanBlock = buildCleanBlockText(item.blockNode, item.blockFrom);
2597
+ const span = replacedCleanSpan(item, cleanBlock);
2598
+ if (span === null) return null;
2599
+ const spanStart = span.start;
2600
+ const spanEnd = span.start + span.text.length;
2601
+ const atomicSpans = cleanBlock.structuralBoundaries.flatMap((boundary) => boundary.type === "field" ? [{
2602
+ offset: boundary.offset - spanStart,
2603
+ length: boundary.length
2604
+ }] : []);
2605
+ const changes = widenChangesToAtomicSpans(span.text, planChanges(span.text), atomicSpans);
2606
+ if (applyTextChanges(span.text, changes) !== replacement) return null;
2607
+ const unitAt = (index) => {
2608
+ const from = cleanBlock.offsets[index];
2609
+ const node = from === void 0 ? null : doc.nodeAt(from);
2610
+ if (from === void 0 || !node) return null;
2611
+ return {
2612
+ from,
2613
+ to: node.isText ? from + 1 : from + node.nodeSize
2614
+ };
2615
+ };
2616
+ const matchedFrom = item.from;
2617
+ const matchedTo = item.to;
2618
+ const holdsMatch = (start, end) => matchedFrom >= start && matchedTo <= end;
2619
+ const climbAfter = (position) => {
2620
+ let at = position;
2621
+ for (let $at = doc.resolve(at);; $at = doc.resolve(at)) {
2622
+ if (!$at.parent.isInline || at !== $at.end() || holdsMatch($at.start(), $at.end())) return at;
2623
+ at = $at.after();
2624
+ }
2625
+ };
2626
+ const climbBefore = (position) => {
2627
+ let at = position;
2628
+ for (let $at = doc.resolve(at);; $at = doc.resolve(at)) {
2629
+ if (!$at.parent.isInline || at !== $at.start() || holdsMatch($at.start(), $at.end())) return at;
2630
+ at = $at.before();
2631
+ }
2632
+ };
2633
+ const isNoteReference = (index) => {
2634
+ const unit = unitAt(index);
2635
+ return (unit === null ? null : doc.nodeAt(unit.from))?.marks.some((mark) => mark.type.name === "footnoteRef") ?? false;
2636
+ };
2637
+ const formattingSourceBefore = (index) => {
2638
+ let source = index;
2639
+ while (source >= 0 && isNoteReference(source)) source--;
2640
+ return source;
2641
+ };
2642
+ const formattingSourceAfter = (index) => {
2643
+ let source = index;
2644
+ while (source < spanEnd && isNoteReference(source)) source++;
2645
+ return source;
2646
+ };
2647
+ const replacementStyleSource = (change) => {
2648
+ const first = spanStart + change.start;
2649
+ const source = formattingSourceAfter(first);
2650
+ return unitAt(source < spanStart + change.end ? source : first);
2651
+ };
2652
+ const matchCarried = (() => {
2653
+ let carried = null;
2654
+ for (let index = spanStart; index < spanEnd; index++) {
2655
+ const unit = unitAt(index);
2656
+ const marks = (unit === null ? null : doc.nodeAt(unit.from))?.marks ?? [];
2657
+ carried = (carried ?? marks.filter(isCarriedMark)).filter((mark) => mark.isInSet(marks));
2658
+ if (carried.length === 0) break;
2659
+ }
2660
+ return carried ?? [];
2661
+ })();
2662
+ const planned = [];
2663
+ for (const change of changes) {
2664
+ const pieces = [];
2665
+ for (let index = spanStart + change.start; index < spanStart + change.end; index++) {
2666
+ const unit = unitAt(index);
2667
+ if (unit === null) return null;
2668
+ const last = pieces.at(-1);
2669
+ if (last !== void 0 && unit.from < last.to) continue;
2670
+ if (last !== void 0 && last.to === unit.from) last.to = unit.to;
2671
+ else pieces.push({ ...unit });
2672
+ }
2673
+ if (pieces.length > 0) {
2674
+ planned.push({
2675
+ change,
2676
+ pieces,
2677
+ text: change.text
2678
+ });
2679
+ continue;
2680
+ }
2681
+ const at = spanStart + change.start;
2682
+ if (span.text.length === 0) {
2683
+ planned.push({
2684
+ change,
2685
+ pieces,
2686
+ text: change.text,
2687
+ insertion: {
2688
+ at: matchedFrom,
2689
+ marks: (current) => current.resolve(matchedFrom).marks()
2690
+ }
2691
+ });
2692
+ continue;
2693
+ }
2694
+ if (at > spanStart) {
2695
+ const previous = unitAt(at - 1);
2696
+ if (previous === null) return null;
2697
+ const styleSource = unitAt(formattingSourceBefore(at - 1));
2698
+ planned.push({
2699
+ change,
2700
+ pieces,
2701
+ text: change.text,
2702
+ insertion: {
2703
+ at: climbAfter(previous.to),
2704
+ marks: (current) => addCarriedMarks(styleSource === null ? [] : current.resolve(styleSource.to).marks(), matchCarried)
2705
+ }
2706
+ });
2707
+ continue;
2708
+ }
2709
+ const first = unitAt(at);
2710
+ if (first === null) return null;
2711
+ const styleSource = unitAt(formattingSourceAfter(at));
2712
+ planned.push({
2713
+ change,
2714
+ pieces,
2715
+ text: change.text,
2716
+ insertion: {
2717
+ at: climbBefore(first.from),
2718
+ marks: (current) => addCarriedMarks(styleSource === null ? [] : (current.nodeAt(styleSource.from)?.marks ?? []).filter(isFormattingMark), matchCarried)
2719
+ }
2720
+ });
2721
+ }
2722
+ return {
2723
+ changes,
2724
+ planned,
2725
+ spanStart,
2726
+ spanEnd,
2727
+ matchedFrom,
2728
+ matchedTo,
2729
+ unitAt,
2730
+ replacementStyleSource
2731
+ };
2732
+ };
2733
+ /**
2734
+ * Apply a direct-mode text replacement as the difference between the matched
2735
+ * text and its replacement; `minimal-replacement.ts` states the contract.
2736
+ *
2737
+ * Returns `null`, before touching the transaction, when the matched span
2738
+ * cannot be mapped onto the document character by character. The caller then
2739
+ * replaces the matched span whole, as it always did.
2740
+ */
2741
+ const applyMinimalDirectReplacement = ({ tr, item, replacement, commentMark, replacementBackground, styleResolver }) => {
2742
+ const plan = planDocumentReplacement({
2743
+ doc: tr.doc,
2744
+ item,
2745
+ replacement,
2746
+ planChanges: (source) => planTextChanges(source, replacement)
2747
+ });
2748
+ if (plan === null || plan.changes.length === 0) return null;
2749
+ const { planned, matchedFrom, matchedTo } = plan;
2750
+ let nextTr = tr;
2751
+ if (replacementBackground === "clear") nextTr = clearTouchedBackground({
2752
+ tr: nextTr,
2753
+ ranges: unitRanges(touchedBackgroundIndices({
2754
+ doc: nextTr.doc,
2755
+ plan
2756
+ }), plan.unitAt),
2757
+ ...styleResolver !== void 0 ? { styleResolver } : {}
2758
+ });
2759
+ const stepsBefore = nextTr.steps.length;
2760
+ const written = [];
2761
+ const schema = nextTr.doc.type.schema;
2762
+ for (const change of planned.toReversed()) {
2763
+ if (change.insertion !== void 0) {
2764
+ const { at, marks } = change.insertion;
2765
+ nextTr = nextTr.insert(at, cleanTextInlineNodes({
2766
+ schema,
2767
+ text: change.text,
2768
+ marks: marks(nextTr.doc)
2769
+ }));
2770
+ written.push({
2771
+ from: at,
2772
+ to: at + change.text.length,
2773
+ step: nextTr.steps.length
2774
+ });
2775
+ continue;
2776
+ }
2777
+ const [first, ...rest] = change.pieces;
2778
+ if (first === void 0) continue;
2779
+ const stepsBeforeWrite = nextTr.steps.length;
2780
+ if (change.text.length > 0) {
2781
+ const replaced = insertCleanText({
2782
+ tr: nextTr,
2783
+ from: first.from,
2784
+ to: first.to,
2785
+ text: change.text,
2786
+ standsInFor: {
2787
+ from: first.from,
2788
+ to: rest.at(-1)?.to ?? first.to
2789
+ }
2790
+ });
2791
+ nextTr = replaced.transaction;
2792
+ written.push({
2793
+ from: replaced.start,
2794
+ to: replaced.end,
2795
+ step: nextTr.steps.length
2796
+ });
2797
+ } else nextTr = nextTr.delete(first.from, first.to);
2798
+ const afterWrite = nextTr.mapping.slice(stepsBeforeWrite);
2799
+ for (const piece of rest.toReversed()) nextTr = nextTr.delete(afterWrite.map(piece.from, 1), afterWrite.map(piece.to, -1));
2800
+ }
2801
+ if (commentMark && replacement.length > 0) {
2802
+ const { mapping } = nextTr;
2803
+ const throughEdit = mapping.slice(stepsBefore);
2804
+ let from = throughEdit.map(matchedFrom, -1);
2805
+ let to = throughEdit.map(matchedTo, 1);
2806
+ for (const range of written) {
2807
+ const afterRange = mapping.slice(range.step);
2808
+ from = Math.min(from, afterRange.map(range.from, -1));
2809
+ to = Math.max(to, afterRange.map(range.to, 1));
2810
+ }
2811
+ if (to > from) nextTr = nextTr.addMark(from, to, commentMark);
2812
+ }
2813
+ return nextTr;
2814
+ };
2815
+ /**
2816
+ * Apply a tracked-changes or suggested text replacement as its redline: each
2817
+ * change marks exactly the characters it removes as a deletion and writes
2818
+ * exactly its new characters as an insertion after them. Characters and
2819
+ * inline content outside every change carry no revision, and background
2820
+ * clearing records a property change only on the untouched characters of a
2821
+ * highlighted stretch a change touches (removed characters keep theirs, and
2822
+ * the new text never takes it). `minimal-replacement.ts` states the rest.
2823
+ *
2824
+ * Returns `null`, before touching the transaction, when the matched span
2825
+ * cannot be mapped onto the document character by character; the caller then
2826
+ * marks the whole match as replaced, as it always did.
2827
+ */
2828
+ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, replacementBackground, styleResolver, diffText, revisionIdDelete, revisionIdInsert, revisionIdBackgroundSeed, author, date, initials, suggestionId }) => {
2829
+ const schema = tr.doc.type.schema;
2830
+ const insertionType = schema.marks["insertion"];
2831
+ const deletionType = schema.marks["deletion"];
2832
+ if (!insertionType || !deletionType) return null;
2833
+ const plan = planDocumentReplacement({
2834
+ doc: tr.doc,
2835
+ item,
2836
+ replacement,
2837
+ planChanges: (source) => changesFromSegments(diffText(source, replacement))
2838
+ });
2839
+ if (plan === null) return null;
2840
+ let nextTr = tr;
2841
+ let backgroundRevisionIds = [];
2842
+ let nextRevisionId = revisionIdBackgroundSeed;
2843
+ const clearsBackground = replacementBackground === "clear";
2844
+ if (clearsBackground) {
2845
+ const removed = new Set(plan.changes.flatMap((change) => Array.from({ length: change.end - change.start }, (_, offset) => plan.spanStart + change.start + offset)));
2846
+ const kept = [...touchedBackgroundIndices({
2847
+ doc: nextTr.doc,
2848
+ plan
2849
+ })].filter((index) => !removed.has(index));
2850
+ const ranges = unitRanges(new Set(kept), plan.unitAt);
2851
+ if (ranges.length > 0) {
2852
+ const cleared = applyTrackedInlineFormattingToRanges({
2853
+ tr: nextTr,
2854
+ schema,
2855
+ ranges,
2856
+ formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
2857
+ revisionIdSeed: revisionIdBackgroundSeed,
2858
+ author,
2859
+ date,
2860
+ initials,
2861
+ suggestionId,
2862
+ ...styleResolver !== void 0 ? { styleResolver } : {}
2863
+ });
2864
+ if (cleared.type === "pendingRunPropertyChange") return { type: "pendingRunPropertyChange" };
2865
+ nextTr = cleared.transaction;
2866
+ backgroundRevisionIds = cleared.revisionIds;
2867
+ nextRevisionId = cleared.nextRevisionId;
2868
+ }
2869
+ }
2870
+ const suggestionAttrs = suggestionId === null ? {} : {
2871
+ provenance: "suggested",
2872
+ suggestionId
2873
+ };
2874
+ const revisionAttrs = {
2875
+ author,
2876
+ date,
2877
+ ...initials ? { initials } : {},
2878
+ ...suggestionAttrs
2879
+ };
2880
+ const deletion = deletionType.create({
2881
+ revisionId: revisionIdDelete,
2882
+ ...revisionAttrs
2883
+ });
2884
+ const insertion = insertionType.create({
2885
+ revisionId: revisionIdInsert,
2886
+ ...revisionAttrs
2887
+ });
2888
+ const insertedMarks = (marks) => marks.filter((mark) => !REVISION_MARK_NAMES.has(mark.type.name) && mark.type.name !== "runPropertyChange" && !(clearsBackground && BACKGROUND_MARK_NAMES.has(mark.type.name)));
2889
+ for (const change of plan.planned.toReversed()) {
2890
+ const doc = nextTr.doc;
2891
+ let at;
2892
+ let marks;
2893
+ if (change.insertion !== void 0) {
2894
+ at = change.insertion.at;
2895
+ marks = change.insertion.marks(doc);
2896
+ } else {
2897
+ const first = change.pieces.at(0);
2898
+ const last = change.pieces.at(-1);
2899
+ const styleSource = plan.replacementStyleSource(change.change);
2900
+ if (first === void 0 || last === void 0 || styleSource === null) panic("A planned replacement lost the characters it removes");
2901
+ at = last.to;
2902
+ marks = addCarriedMarks(inheritedReplacementMarks(doc, styleSource.from, styleSource.to), surveyReplacedAnnotations(doc, first.from, last.to).carried);
2903
+ for (const piece of change.pieces) {
2904
+ nextTr = nextTr.addMark(piece.from, piece.to, deletion);
2905
+ if (commentMark) nextTr = nextTr.addMark(piece.from, piece.to, commentMark);
2906
+ }
2907
+ }
2908
+ if (change.text.length === 0) continue;
2909
+ const nodes = cleanTextInlineNodes({
2910
+ schema,
2911
+ text: change.text,
2912
+ marks: insertedMarks(marks)
2913
+ });
2914
+ nextTr = nextTr.insert(at, nodes);
2915
+ const end = at + Fragment.fromArray(nodes).size;
2916
+ nextTr = nextTr.addMark(at, end, insertion);
2917
+ if (commentMark) nextTr = nextTr.addMark(at, end, commentMark);
2918
+ }
2919
+ return {
2920
+ type: "applied",
2921
+ transaction: nextTr,
2922
+ backgroundRevisionIds,
2923
+ nextRevisionId
2924
+ };
2925
+ };
2926
+ /**
2927
+ * The clean-text characters of every background stretch a change touches,
2928
+ * within the matched span.
2929
+ *
2930
+ * Text written onto a highlighted value is new text, so the marker saying
2931
+ * "fill this in" must not survive on it, and changing one digit of a
2932
+ * highlighted amount still fills the whole amount. Background elsewhere in the
2933
+ * match is left alone.
2934
+ */
2935
+ const touchedBackgroundIndices = ({ doc, plan: { changes, spanStart, spanEnd, unitAt } }) => {
2936
+ const hasBackground = (index) => {
2937
+ const unit = unitAt(index);
2938
+ return (unit === null ? null : doc.nodeAt(unit.from))?.marks.some((mark) => BACKGROUND_MARK_NAMES.has(mark.type.name)) ?? false;
2939
+ };
2940
+ const touched = /* @__PURE__ */ new Set();
2941
+ for (const change of changes) {
2942
+ const start = spanStart + change.start;
2943
+ const end = spanStart + change.end;
2944
+ const touchedFrom = start === end ? Math.max(start - 1, spanStart) : start;
2945
+ const touchedTo = start === end ? Math.min(end + 1, spanEnd) : end;
2946
+ for (let index = touchedFrom; index < touchedTo; index++) {
2947
+ if (touched.has(index) || !hasBackground(index)) continue;
2948
+ touched.add(index);
2949
+ for (let left = index - 1; left >= spanStart && hasBackground(left); left--) touched.add(left);
2950
+ for (let right = index + 1; right < spanEnd && hasBackground(right); right++) touched.add(right);
2951
+ }
2952
+ }
2953
+ return touched;
2954
+ };
2955
+ /** The document ranges owning clean characters `indices`, merged where they meet. */
2956
+ const unitRanges = (indices, unitAt) => {
2957
+ const ranges = [];
2958
+ for (const index of [...indices].toSorted((a, b) => a - b)) {
2959
+ const unit = unitAt(index);
2960
+ if (unit === null) continue;
2961
+ const last = ranges.at(-1);
2962
+ if (last !== void 0 && unit.from <= last.to) last.to = Math.max(last.to, unit.to);
2963
+ else ranges.push({ ...unit });
2964
+ }
2965
+ return ranges;
2966
+ };
2967
+ /**
2968
+ * Clear the highlight and shading of `ranges` (see
2969
+ * {@link touchedBackgroundIndices}) before a direct change is written. Only
2970
+ * marks change, so no position moves.
2971
+ */
2972
+ const clearTouchedBackground = ({ tr, ranges, styleResolver }) => {
2973
+ let nextTr = tr;
2974
+ for (const range of ranges) nextTr = applyInlineFormatting({
2975
+ tr: nextTr,
2976
+ from: range.from,
2977
+ to: range.to,
2978
+ formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
2979
+ ...styleResolver !== void 0 ? { styleResolver } : {}
2980
+ });
2981
+ return nextTr;
2982
+ };
2539
2983
  const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId }) => {
2540
2984
  const anchor = snapshot.anchors[blockId];
2541
2985
  if (!anchor) return {