@stll/folio-core 0.36.0 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +59 -1
  2. package/dist/ai-edits/apply.js +689 -367
  3. package/dist/ai-edits/block-identity.d.ts +4 -1
  4. package/dist/ai-edits/block-identity.js +4 -1
  5. package/dist/ai-edits/blockRange.js +14 -14
  6. package/dist/ai-edits/clean-text.d.ts +40 -1
  7. package/dist/ai-edits/clean-text.js +52 -2
  8. package/dist/ai-edits/headless.d.ts +3 -1
  9. package/dist/ai-edits/headless.js +133 -71
  10. package/dist/ai-edits/index.d.ts +3 -3
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.js +38 -20
  13. package/dist/ai-edits/snapshot.d.ts +14 -6
  14. package/dist/ai-edits/snapshot.js +99 -19
  15. package/dist/ai-edits/table-cell-mutations.js +1 -1
  16. package/dist/ai-edits/table-row-column-mutations.js +7 -0
  17. package/dist/ai-edits/table-template.d.ts +11 -1
  18. package/dist/ai-edits/table-template.js +49 -17
  19. package/dist/ai-edits/types.d.ts +60 -57
  20. package/dist/ai-suggestions/conflict.js +1 -4
  21. package/dist/ai-suggestions/text-positions.d.ts +8 -0
  22. package/dist/ai-suggestions/text-positions.js +27 -5
  23. package/dist/compare/__fixtures__/body-sequence.js +3 -4
  24. package/dist/compare/column-alignment.d.ts +13 -13
  25. package/dist/compare/column-alignment.js +78 -39
  26. package/dist/compare/compare.d.ts +4 -3
  27. package/dist/compare/compare.js +41 -5
  28. package/dist/compare/content-alignment.d.ts +95 -0
  29. package/dist/compare/content-alignment.js +1450 -0
  30. package/dist/compare/content-types.d.ts +100 -0
  31. package/dist/compare/content-types.js +0 -0
  32. package/dist/compare/content.d.ts +209 -0
  33. package/dist/compare/content.js +885 -0
  34. package/dist/compare/formatting.d.ts +5 -5
  35. package/dist/compare/formatting.js +6 -4
  36. package/dist/compare/plan.d.ts +6 -1
  37. package/dist/compare/plan.js +201 -569
  38. package/dist/compare/scenario.d.ts +2 -2
  39. package/dist/compare/types.d.ts +3 -3
  40. package/dist/compare/verification.d.ts +2 -2
  41. package/dist/compare/verification.js +25 -8
  42. package/dist/compat/eigenpal.d.ts +5 -3
  43. package/dist/compat/eigenpal.js +3 -2
  44. package/dist/controller/headerFooterEditorManager.js +14 -6
  45. package/dist/controller/hiddenEditorManager.d.ts +7 -2
  46. package/dist/controller/hiddenEditorManager.js +15 -6
  47. package/dist/controller/layoutPipeline.js +5 -3
  48. package/dist/controller/noteEditorManager.js +12 -7
  49. package/dist/display-list/build/headerFooterPrimitives.js +1 -0
  50. package/dist/display-list/build/storyPrimitives.d.ts +5 -2
  51. package/dist/display-list/build/storyPrimitives.js +60 -3
  52. package/dist/display-list/build/tablePrimitives.js +30 -0
  53. package/dist/display-list/primitives.d.ts +1 -1
  54. package/dist/document-operations.d.ts +8 -6
  55. package/dist/document-operations.js +121 -26
  56. package/dist/docx/blockContentParser.js +6 -27
  57. package/dist/docx/ensureParaIds.js +9 -4
  58. package/dist/docx/numberingParser.d.ts +3 -1
  59. package/dist/docx/numberingParser.js +4 -1
  60. package/dist/docx/paragraphParser.js +7 -6
  61. package/dist/docx/paragraphTraversal.js +30 -34
  62. package/dist/docx/rezip.d.ts +0 -8
  63. package/dist/docx/rezip.js +41 -16
  64. package/dist/docx/runParser.js +4 -2
  65. package/dist/docx/serializer/paragraphSerializer.js +2 -0
  66. package/dist/docx/serializer/runSerializer.js +5 -3
  67. package/dist/docx/serializer/trackedChangeAttributes.d.ts +3 -1
  68. package/dist/docx/serializer/trackedChangeAttributes.js +11 -1
  69. package/dist/docx/server/build.d.ts +1 -1
  70. package/dist/headless-layout.js +5 -4
  71. package/dist/index.d.ts +5 -3
  72. package/dist/index.js +3 -2
  73. package/dist/internal/headlessRevisionResolution.d.ts +23 -0
  74. package/dist/internal/headlessRevisionResolution.js +176 -0
  75. package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
  76. package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
  77. package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
  78. package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
  79. package/dist/internal/pageBreakRunPartition.d.ts +26 -0
  80. package/dist/internal/pageBreakRunPartition.js +35 -0
  81. package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
  82. package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
  83. package/dist/internal/sectionEndpointResolution.d.ts +32 -0
  84. package/dist/internal/sectionEndpointResolution.js +38 -0
  85. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  86. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
  87. package/dist/layout-bridge/convert/headerFooterLayout.js +48 -5
  88. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
  89. package/dist/layout-bridge/convert/toFlowBlocks.js +379 -75
  90. package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
  91. package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
  92. package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
  93. package/dist/layout-bridge/dom/textStreamDom.js +75 -0
  94. package/dist/layout-bridge/headerFooterLayout.js +16 -30
  95. package/dist/layout-engine/index.js +12 -3
  96. package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
  97. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
  98. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  99. package/dist/layout-engine/types.d.ts +7 -0
  100. package/dist/layout-painter/imageLayout.d.ts +1 -1
  101. package/dist/layout-painter/renderTable.js +20 -0
  102. package/dist/model.d.ts +1 -1
  103. package/dist/paged-layout/rangeProjection.js +6 -9
  104. package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
  105. package/dist/paged-layout/transactionDirtyRange.js +23 -1
  106. package/dist/prosemirror/attrs/index.d.ts +7 -3
  107. package/dist/prosemirror/attrs/index.js +113 -7
  108. package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
  109. package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
  110. package/dist/prosemirror/commands/comments.d.ts +10 -1
  111. package/dist/prosemirror/commands/comments.js +167 -59
  112. package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
  113. package/dist/prosemirror/commands/formatPainter.js +319 -63
  114. package/dist/prosemirror/commands/index.d.ts +2 -2
  115. package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
  116. package/dist/prosemirror/commands/pageBreak.js +18 -36
  117. package/dist/prosemirror/commands/propertyChangeScope.d.ts +7 -5
  118. package/dist/prosemirror/commands/propertyChangeScope.js +27 -18
  119. package/dist/prosemirror/conversion/fromProseDoc.d.ts +7 -2
  120. package/dist/prosemirror/conversion/fromProseDoc.js +809 -209
  121. package/dist/prosemirror/conversion/runShadingMark.js +2 -2
  122. package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
  123. package/dist/prosemirror/conversion/toProseDoc.js +416 -283
  124. package/dist/prosemirror/extensions/StarterKit.js +4 -0
  125. package/dist/prosemirror/extensions/core/ParagraphExtension.js +12 -8
  126. package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
  127. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +1 -1
  128. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
  129. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
  130. package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
  131. package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
  132. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
  133. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
  134. package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
  135. package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
  136. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
  137. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
  138. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
  139. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
  140. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
  141. package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
  142. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
  143. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  144. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
  145. package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
  146. package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
  147. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
  148. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
  149. package/dist/prosemirror/findReplaceSelection.js +8 -3
  150. package/dist/prosemirror/index.d.ts +3 -3
  151. package/dist/prosemirror/listMarker.d.ts +26 -1
  152. package/dist/prosemirror/listMarker.js +108 -14
  153. package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
  154. package/dist/prosemirror/pageBreakRunProjection.js +47 -0
  155. package/dist/prosemirror/paraText.js +6 -0
  156. package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
  157. package/dist/prosemirror/paragraphSpacing.js +125 -0
  158. package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
  159. package/dist/prosemirror/plugins/documentNumbering.js +18 -0
  160. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
  161. package/dist/prosemirror/plugins/pmTextScan.js +14 -4
  162. package/dist/prosemirror/plugins/suggestionMode.js +8 -10
  163. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
  164. package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
  165. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
  166. package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
  167. package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
  168. package/dist/prosemirror/runFormattingProvenance.js +48 -0
  169. package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
  170. package/dist/prosemirror/runFormattingReconciliation.js +54 -0
  171. package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
  172. package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
  173. package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
  174. package/dist/prosemirror/runStyleFormatting.js +105 -0
  175. package/dist/prosemirror/schema/index.d.ts +3 -3
  176. package/dist/prosemirror/schema/marks.d.ts +82 -9
  177. package/dist/prosemirror/schema/marks.js +73 -1
  178. package/dist/prosemirror/schema/nodes.d.ts +18 -5
  179. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
  180. package/dist/prosemirror/styles/resolvedStyleAttrs.js +44 -23
  181. package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
  182. package/dist/prosemirror/styles/styleResolver.js +3 -3
  183. package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
  184. package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
  185. package/dist/prosemirror/tableGridMutation.d.ts +20 -0
  186. package/dist/prosemirror/tableGridMutation.js +30 -0
  187. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
  188. package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
  189. package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
  190. package/dist/prosemirror/validation.js +23 -4
  191. package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
  192. package/dist/render-dom/RenderedDomContext.js +17 -34
  193. package/dist/server.d.ts +4 -4
  194. package/dist/server.js +2 -2
  195. package/dist/style-engine/styleEngine.d.ts +1 -1
  196. package/dist/utils/clipboard.js +1 -1
  197. package/dist/utils/findReplace.js +38 -21
  198. package/dist/version-comparison.d.ts +38 -44
  199. package/dist/version-comparison.js +270 -401
  200. package/package.json +3 -2
@@ -1,17 +1,23 @@
1
- import { expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
2
- import { paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
1
+ import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
2
+ import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
3
3
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
4
- import { marksToTextFormatting } from "../prosemirror/conversion/fromProseDoc.js";
5
4
  import { requestDeterministicParaIds } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
6
5
  import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
6
+ import { CLEARED_LIST_RENDERING_ATTRS } from "../prosemirror/listMarker.js";
7
7
  import { directParagraphAlignment } from "../prosemirror/paragraphAlignment.js";
8
+ import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
9
+ import { getDocumentNumbering } from "../prosemirror/plugins/documentNumbering.js";
8
10
  import { getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
11
+ import { selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
12
+ import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
13
+ import { paragraphRunStyleContextAt } from "../prosemirror/runStyleFormatting.js";
14
+ import { listLevelAttrPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
9
15
  import { isZeroWidthAnchor } from "../prosemirror/zeroWidthAnchors.js";
10
16
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
11
17
  import { stripBlockIdentityAttrs } from "./block-identity.js";
12
- import { buildCleanBlockText } from "./clean-text.js";
18
+ import { buildCleanBlockText, resolveCleanTextRange } from "./clean-text.js";
13
19
  import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers } from "./inline-emphasis.js";
14
- import { hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
20
+ import { hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
15
21
  import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
16
22
  import { planTableMutations } from "./table-mutation-plan.js";
17
23
  import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
@@ -19,6 +25,7 @@ import { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBounda
19
25
  import { tableFromTemplate } from "./table-template.js";
20
26
  import { wordDiffSessionFromOptions } from "./word-diff.js";
21
27
  import { panic } from "better-result";
28
+ import { Mark } from "prosemirror-model";
22
29
  import { TableMap } from "prosemirror-tables";
23
30
  import { canJoin, canSplit } from "prosemirror-transform";
24
31
  //#region src/ai-edits/apply.ts
@@ -43,6 +50,36 @@ const coarseWordDiff = (before, after) => {
43
50
  text: after
44
51
  }];
45
52
  };
53
+ const commentMarkRanges = (doc, commentType, commentId) => {
54
+ const ranges = [];
55
+ doc.descendants((node, position) => {
56
+ for (const mark of node.marks) if (mark.type === commentType && mark.attrs["commentId"] === commentId) ranges.push({
57
+ from: position,
58
+ to: position + node.nodeSize,
59
+ mark
60
+ });
61
+ });
62
+ return ranges;
63
+ };
64
+ /**
65
+ * Replace an operation-local comment id only after its edit has succeeded.
66
+ * The provisional mark lives solely in the undispatched transaction, so an
67
+ * operation that refuses or produces no anchor has no external comment side
68
+ * effect and cannot leave an orphan comments.xml entry behind.
69
+ */
70
+ const commitProvisionalComment = ({ tr, commentType, provisionalId, committedId }) => {
71
+ const ranges = commentMarkRanges(tr.doc, commentType, provisionalId);
72
+ if (ranges.length === 0) panic("A commented operation changed the document without producing a comment anchor");
73
+ if (provisionalId === committedId) return tr;
74
+ for (const { from, to, mark } of ranges) {
75
+ tr = tr.removeMark(from, to, mark);
76
+ tr = tr.addMark(from, to, commentType.create({
77
+ ...mark.attrs,
78
+ commentId: committedId
79
+ }));
80
+ }
81
+ return tr;
82
+ };
46
83
  /**
47
84
  * Operation types applied in `"suggested"` mode. Every produced revision — an
48
85
  * inline mark, a whole-node `_suggestedInsert` marker, or a suggested
@@ -65,21 +102,6 @@ const SUGGESTED_SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
65
102
  "insertTableColumn",
66
103
  "deleteTableColumn"
67
104
  ]);
68
- /**
69
- * The attrs that render a list label, cleared together whenever a paragraph
70
- * stops being a list item. `w:numPr` alone is not enough: the marker attrs the
71
- * editor caches would keep drawing a label on a paragraph that is no longer in
72
- * the list. One frozen record rather than a list repeated at each call site,
73
- * because a list repeated is a list that drifts.
74
- */
75
- const CLEARED_LIST_MARKER_ATTRS = Object.freeze({
76
- listMarker: null,
77
- listMarkerHidden: null,
78
- listLevelNumFmts: null,
79
- listLevelStarts: null,
80
- listAbstractNumId: null,
81
- listStartOverride: null
82
- });
83
105
  const REPLACE_BLOCK_IMPACT = {
84
106
  text: {
85
107
  changesStyle: false,
@@ -95,14 +117,40 @@ const REPLACE_BLOCK_IMPACT = {
95
117
  }
96
118
  };
97
119
  const isResolvedReplaceBlockOperation = (item) => item.operation.type === "replaceBlock";
120
+ const isResolvedBaseReplaceBlockOperation = (item) => item.operation.type === "replaceBlock";
121
+ const hasRepresentableCommentAnchor = (item, mode) => {
122
+ switch (item.operation.type) {
123
+ case "insertAfterBlock":
124
+ case "insertBeforeBlock": return (item.insertTexts ?? [""]).some((text) => stripInlineEmphasisMarkers(text).length > 0);
125
+ case "insertSignatureTable": return false;
126
+ case "replaceInBlock":
127
+ case "replaceRange": return stripInlineEmphasisMarkers(item.operation.replace).length > 0 || mode !== "direct" && item.from < item.to;
128
+ case "replaceBlock": {
129
+ if (!isResolvedBaseReplaceBlockOperation(item)) panic("Resolved replaceBlock operation lost its impact discriminator");
130
+ const { changesText } = REPLACE_BLOCK_IMPACT[item.replaceBlockImpact];
131
+ if (!changesText) return item.from < item.to;
132
+ return stripInlineEmphasisMarkers(item.operation.text).length > 0 || mode !== "direct" && item.from < item.to;
133
+ }
134
+ case "deleteBlock": return mode !== "direct" && item.from < item.to;
135
+ default: return item.from < item.to;
136
+ }
137
+ };
138
+ const writesParagraphPropertyChange = (item) => {
139
+ if (item.operation.type === "setBlockParagraphProperties") return true;
140
+ if (item.operation.type === "splitBlock") return item.operation.firstParagraphProperties !== void 0 || item.operation.secondParagraphProperties !== void 0;
141
+ if (item.operation.type === "mergeBlockWithNext") return item.operation.mergedParagraphProperties !== void 0;
142
+ return isResolvedReplaceBlockOperation(item) && REPLACE_BLOCK_IMPACT[item.replaceBlockImpact].changesStyle;
143
+ };
98
144
  const resolveReplaceBlockImpact = ({ changesText, changesStyle }) => {
99
145
  if (changesText) return changesStyle ? "textAndStyle" : "text";
100
146
  if (changesStyle) return "style";
101
147
  return panic("Cannot resolve a replaceBlock that changes neither text nor style");
102
148
  };
103
- const paragraphPropertiesPatch = ({ node, properties, resolvedAlignmentFromStyle }) => {
149
+ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyle, numbering = null }) => {
104
150
  const attrs = expectParagraphAttrs(node);
105
151
  const currentDirectAlignment = directParagraphAlignment(attrs);
152
+ const currentDirectSpacing = directParagraphSpacing(attrs);
153
+ const resolvedSpacingFromStyle = paragraphSpacingFromFormatting(resolvedFormattingFromStyle);
106
154
  const patch = {};
107
155
  let originalFormatting = attrs._originalFormatting === void 0 || attrs._originalFormatting === null ? void 0 : { ...attrs._originalFormatting };
108
156
  let originalFormattingChanged = false;
@@ -110,8 +158,12 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedAlignmentFromStyle
110
158
  const styleChanged = nextStyleId !== void 0 && (attrs.styleId ?? null) !== nextStyleId;
111
159
  if (styleChanged) {
112
160
  patch["styleId"] = nextStyleId;
113
- patch["alignmentFromStyle"] = resolvedAlignmentFromStyle;
114
- if (properties.alignment === void 0) patch["alignment"] = currentDirectAlignment ?? resolvedAlignmentFromStyle ?? null;
161
+ patch["alignmentFromStyle"] = resolvedFormattingFromStyle?.alignment;
162
+ if (properties.alignment === void 0) patch["alignment"] = currentDirectAlignment ?? resolvedFormattingFromStyle?.alignment ?? null;
163
+ if (properties.spacing === void 0) Object.assign(patch, paragraphSpacingAttrPatch({
164
+ direct: currentDirectSpacing,
165
+ inherited: resolvedSpacingFromStyle
166
+ }));
115
167
  originalFormatting ??= {};
116
168
  if (nextStyleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
117
169
  else originalFormatting.styleId = nextStyleId;
@@ -120,29 +172,39 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedAlignmentFromStyle
120
172
  }
121
173
  if (properties.listLevel !== void 0) {
122
174
  const numPr = node.attrs["numPr"];
123
- const current = typeof numPr === "object" && numPr !== null && "ilvl" in numPr ? numPr.ilvl : void 0;
124
175
  if (properties.listLevel === null) {
125
- if (numPr !== null && numPr !== void 0) {
126
- patch["numPr"] = null;
127
- Object.assign(patch, CLEARED_LIST_MARKER_ATTRS);
128
- }
129
- } else if (current !== properties.listLevel) {
176
+ patch["numPr"] = null;
177
+ Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
178
+ } else {
130
179
  const numId = typeof numPr === "object" && numPr !== null && "numId" in numPr ? numPr.numId : void 0;
131
- patch["numPr"] = {
132
- ...typeof numId === "number" && { numId },
180
+ if (typeof numId === "number") Object.assign(patch, listLevelAttrPatch(attrs, {
181
+ numId,
133
182
  ilvl: properties.listLevel
134
- };
183
+ }, numbering));
184
+ else {
185
+ patch["numPr"] = { ilvl: properties.listLevel };
186
+ Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
187
+ }
135
188
  }
136
189
  }
137
190
  if (properties.alignment !== void 0 && (styleChanged || (currentDirectAlignment ?? null) !== properties.alignment)) {
138
191
  originalFormatting ??= {};
139
192
  if (properties.alignment === null) Reflect.deleteProperty(originalFormatting, "alignment");
140
193
  else originalFormatting.alignment = properties.alignment;
141
- const alignmentFromStyle = properties.styleId === void 0 ? attrs.alignmentFromStyle : resolvedAlignmentFromStyle;
194
+ const alignmentFromStyle = properties.styleId === void 0 ? attrs.alignmentFromStyle : resolvedFormattingFromStyle?.alignment;
142
195
  patch["alignment"] = properties.alignment ?? alignmentFromStyle ?? null;
143
196
  originalFormattingChanged = true;
144
197
  }
145
- if (originalFormattingChanged && originalFormatting !== void 0) patch["_originalFormatting"] = Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
198
+ if (properties.spacing !== void 0 && (styleChanged || !paragraphSpacingEqual(currentDirectSpacing, properties.spacing))) {
199
+ const directSpacing = properties.spacing ?? void 0;
200
+ originalFormatting = withDirectParagraphSpacing(originalFormatting, directSpacing);
201
+ Object.assign(patch, paragraphSpacingAttrPatch({
202
+ direct: directSpacing,
203
+ inherited: resolvedSpacingFromStyle
204
+ }));
205
+ originalFormattingChanged = true;
206
+ }
207
+ if (originalFormattingChanged) patch["_originalFormatting"] = originalFormatting && Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
146
208
  return Object.keys(patch).length > 0 ? patch : null;
147
209
  };
148
210
  const REVISION_MARK_NAMES = /* @__PURE__ */ new Set(["insertion", "deletion"]);
@@ -201,7 +263,58 @@ const paragraphPropertiesBeforeBatch = (node, batchRevisionIds) => {
201
263
  if (!earliestBatchChange) return paragraphPropertiesSnapshot(node);
202
264
  return earliestBatchChange.previousFormatting ?? {};
203
265
  };
204
- const resolveAlignmentFromStyle = ({ attrs, styleId, styleResolver }) => styleResolver?.resolveParagraphStyle(styleId).paragraphFormatting?.alignment ?? ((styleId ?? void 0) === (attrs.styleId ?? void 0) ? attrs.alignmentFromStyle : void 0);
266
+ const resolveFormattingFromStyle = ({ attrs, styleId, styleResolver }) => {
267
+ const resolved = styleResolver?.resolveParagraphStyle(styleId).paragraphFormatting;
268
+ if (resolved !== void 0) return resolved;
269
+ if ((styleId ?? void 0) !== (attrs.styleId ?? void 0)) return;
270
+ const fallback = {};
271
+ if (attrs.alignmentFromStyle !== void 0) fallback.alignment = attrs.alignmentFromStyle;
272
+ return Object.keys(fallback).length > 0 ? fallback : void 0;
273
+ };
274
+ /**
275
+ * Apply one paragraph-property patch to a live node. A tracked patch stores
276
+ * the complete previous pPr: a partial snapshot cannot distinguish a property
277
+ * the change left alone from one it explicitly cleared when rejecting it.
278
+ */
279
+ const applyBlockParagraphProperties = ({ tr, position, node, properties, styleResolver, numbering = null, revisionInfo }) => {
280
+ const attrs = expectParagraphAttrs(node);
281
+ const resolvedFormattingFromStyle = properties.styleId === void 0 ? resolveFormattingFromStyle({
282
+ attrs,
283
+ styleId: attrs.styleId,
284
+ styleResolver
285
+ }) : resolveFormattingFromStyle({
286
+ attrs,
287
+ styleId: properties.styleId,
288
+ styleResolver
289
+ });
290
+ const patch = paragraphPropertiesPatch({
291
+ node,
292
+ properties,
293
+ resolvedFormattingFromStyle,
294
+ numbering
295
+ });
296
+ if (patch === null) return {
297
+ tr,
298
+ changed: false,
299
+ revisionId: null
300
+ };
301
+ const changeInfo = revisionInfo?.();
302
+ const change = changeInfo ? {
303
+ type: "paragraphPropertyChange",
304
+ info: changeInfo,
305
+ previousFormatting: paragraphPropertiesSnapshot(node)
306
+ } : null;
307
+ const existing = attrs._propertyChanges;
308
+ return {
309
+ tr: tr.setNodeMarkup(position, void 0, {
310
+ ...node.attrs,
311
+ ...patch,
312
+ ...change ? { _propertyChanges: [...Array.isArray(existing) ? existing : [], change] } : {}
313
+ }),
314
+ changed: true,
315
+ revisionId: change?.info.id ?? null
316
+ };
317
+ };
205
318
  const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) => {
206
319
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return {
207
320
  tr,
@@ -214,7 +327,7 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
214
327
  revisionId: null
215
328
  };
216
329
  const attrs = expectParagraphAttrs(block);
217
- const resolvedAlignmentFromStyle = resolveAlignmentFromStyle({
330
+ const resolvedFormattingFromStyle = resolveFormattingFromStyle({
218
331
  attrs,
219
332
  styleId: item.operation.styleId,
220
333
  styleResolver
@@ -222,7 +335,7 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
222
335
  const patch = paragraphPropertiesPatch({
223
336
  node: block,
224
337
  properties: { styleId: item.operation.styleId },
225
- resolvedAlignmentFromStyle
338
+ resolvedFormattingFromStyle
226
339
  });
227
340
  if (patch === null) return {
228
341
  tr,
@@ -247,141 +360,134 @@ const REPLACEMENT_BACKGROUND_CLEAR_FORMATTING = {
247
360
  highlight: false,
248
361
  runShading: false
249
362
  };
250
- const INLINE_FORMATTING_MARK_NAMES = {
251
- bold: "bold",
252
- italic: "italic",
253
- underline: "underline",
254
- strike: "strike",
255
- fontFamily: "fontFamily",
256
- fontSizePt: "fontSize",
257
- color: "textColor",
258
- highlight: "highlight",
259
- runShading: "runShading"
260
- };
261
- const DIRECT_FONT_PROPERTIES = [
262
- "fontFamily",
263
- "fontSize",
264
- "color"
265
- ];
266
- const formattingMarkName = (property) => {
267
- if (!Object.hasOwn(INLINE_FORMATTING_MARK_NAMES, property)) return null;
268
- const name = Reflect.get(INLINE_FORMATTING_MARK_NAMES, property);
269
- return typeof name === "string" ? name : null;
270
- };
271
- const formattingMarkAttrs = (property, value) => {
272
- switch (property) {
273
- case "underline": return { style: "single" };
274
- case "fontFamily": return typeof value === "string" ? {
275
- ascii: value,
276
- hAnsi: value
277
- } : null;
278
- case "fontSizePt": return typeof value === "number" ? { size: value * 2 } : null;
279
- case "color": return typeof value === "string" ? { rgb: value.replace(/^#/u, "").toUpperCase() } : null;
280
- default: return {};
363
+ const includedFormattingProperty = (includedProperties, property) => includedProperties === void 0 || includedProperties.includes(property);
364
+ const patchAuthoredRunFormatting = ({ authoredFormatting, formatting, includedProperties }) => {
365
+ const next = { ...authoredFormatting };
366
+ for (const property of ["bold", "italic"]) {
367
+ const value = formatting[property];
368
+ if (value === void 0 || !includedFormattingProperty(includedProperties, property)) continue;
369
+ if (value === null) Reflect.deleteProperty(next, property);
370
+ else next[property] = value;
281
371
  }
282
- };
283
- const applyInlineFormatting = ({ tr, schema, from, to, formatting, includedProperties }) => {
284
- for (const [property, value] of Object.entries(formatting)) {
285
- if (includedProperties && !includedProperties.includes(property)) continue;
286
- const markName = formattingMarkName(property);
287
- const markType = markName ? schema.marks[markName] : void 0;
288
- if (!markType) continue;
289
- if (value !== false && value !== null) {
290
- const attrs = formattingMarkAttrs(property, value);
291
- if (attrs) tr.addMark(from, to, markType.create(attrs));
292
- continue;
293
- }
294
- tr.removeMark(from, to, markType);
372
+ if (formatting.underline !== void 0 && includedFormattingProperty(includedProperties, "underline")) if (formatting.underline === null) delete next.underline;
373
+ else next.underline = { style: formatting.underline ? "single" : "none" };
374
+ if (formatting.strike !== void 0 && includedFormattingProperty(includedProperties, "strike")) {
375
+ delete next.doubleStrike;
376
+ if (formatting.strike === null) delete next.strike;
377
+ else next.strike = formatting.strike;
295
378
  }
296
- applyDirectFontProvenance({
297
- tr,
298
- schema,
379
+ if (formatting.fontFamily !== void 0 && includedFormattingProperty(includedProperties, "fontFamily")) if (formatting.fontFamily === null) delete next.fontFamily;
380
+ else next.fontFamily = {
381
+ ascii: formatting.fontFamily,
382
+ hAnsi: formatting.fontFamily
383
+ };
384
+ if (formatting.fontSizePt !== void 0 && includedFormattingProperty(includedProperties, "fontSizePt")) if (formatting.fontSizePt === null) delete next.fontSize;
385
+ else next.fontSize = formatting.fontSizePt * 2;
386
+ if (formatting.color !== void 0 && includedFormattingProperty(includedProperties, "color")) if (formatting.color === null) delete next.color;
387
+ else next.color = { rgb: formatting.color.replace(/^#/u, "").toUpperCase() };
388
+ if (formatting.highlight === false && includedFormattingProperty(includedProperties, "highlight")) delete next.highlight;
389
+ if (formatting.runShading === false && includedFormattingProperty(includedProperties, "runShading")) delete next.shading;
390
+ return next;
391
+ };
392
+ const applyInlineFormatting = ({ tr, from, to, formatting, includedProperties, styleResolver }) => {
393
+ const representations = selectRunFormattingCarrierRepresentations({
394
+ doc: tr.doc,
299
395
  from,
300
- to,
301
- formatting,
302
- ...includedProperties ? { includedProperties } : {}
396
+ to
303
397
  });
398
+ for (const representation of representations) {
399
+ const styleContext = paragraphRunStyleContextAt({
400
+ doc: tr.doc,
401
+ pos: representation.from,
402
+ ...styleResolver !== void 0 ? { styleResolver } : {}
403
+ });
404
+ const authoredFormatting = readAuthoredRunFormatting({
405
+ context: styleContext,
406
+ marks: representation.node.marks,
407
+ ...styleResolver !== void 0 ? { styleResolver } : {}
408
+ });
409
+ const marks = reconcileRunFormattingMarks({
410
+ authoredFormatting: patchAuthoredRunFormatting({
411
+ authoredFormatting,
412
+ formatting,
413
+ ...includedProperties !== void 0 ? { includedProperties } : {}
414
+ }),
415
+ context: styleContext,
416
+ node: representation.node,
417
+ ...styleResolver !== void 0 ? { styleResolver } : {}
418
+ });
419
+ applyMarksToRunFormattingRepresentation({
420
+ tr,
421
+ representation,
422
+ marks
423
+ });
424
+ }
304
425
  return tr;
305
426
  };
306
- const applyDirectFontProvenance = ({ tr, schema, from, to, formatting, includedProperties }) => {
307
- if ("highlight" in formatting) return;
308
- const changed = [
309
- [
310
- "fontFamily",
311
- "fontFamily",
312
- formatting.fontFamily
313
- ],
314
- [
315
- "fontSize",
316
- "fontSizePt",
317
- formatting.fontSizePt
318
- ],
319
- [
320
- "color",
321
- "color",
322
- formatting.color
323
- ]
324
- ].filter(([, inputProperty, value]) => value !== void 0 && (!includedProperties || includedProperties.includes(inputProperty)));
325
- const markType = schema.marks["runFormattingOverride"];
326
- if (!markType || changed.length === 0) return;
327
- tr.doc.nodesBetween(from, to, (node, pos) => {
328
- if (!node.isText) return;
329
- const segmentFrom = Math.max(from, pos);
330
- const segmentTo = Math.min(to, pos + node.nodeSize);
331
- const existing = node.marks.find((mark) => mark.type === markType);
332
- const attrs = existing ? expectRunFormattingOverrideMarkAttrs(existing) : {};
333
- const directFontProperties = new Set(attrs.directFontProperties);
334
- for (const [property, , value] of changed) if (value === null) directFontProperties.delete(property);
335
- else directFontProperties.add(property);
336
- tr.removeMark(segmentFrom, segmentTo, markType);
337
- const nextAttrs = {
338
- ...attrs,
339
- directFontProperties: DIRECT_FONT_PROPERTIES.filter((property) => directFontProperties.has(property))
340
- };
341
- if (Object.entries(nextAttrs).some(([key, value]) => key === "directFontProperties" ? Array.isArray(value) && value.length > 0 : value !== null && value !== void 0)) tr.addMark(segmentFrom, segmentTo, markType.create(nextAttrs));
342
- });
343
- };
344
- const directFontPropertyForFormattingProperty = (property) => {
345
- switch (property) {
346
- case "fontFamily": return "fontFamily";
347
- case "fontSizePt": return "fontSize";
348
- case "color": return "color";
349
- default: return null;
427
+ const applyMarksToRunFormattingRepresentation = ({ tr, representation, marks }) => {
428
+ const { node, position, from, to } = representation;
429
+ if (Mark.sameSet(node.marks, marks)) return;
430
+ if (!node.isText) {
431
+ tr.setNodeMarkup(position, void 0, node.attrs, marks);
432
+ return;
350
433
  }
434
+ for (const current of node.marks) if (!marks.some((candidate) => candidate.eq(current))) tr.removeMark(from, to, current.type);
435
+ for (const next of marks) if (!node.marks.some((candidate) => candidate.eq(next))) tr.addMark(from, to, next);
351
436
  };
352
- const formattingPropertyWouldChange = (marks, property, value) => {
353
- const markName = formattingMarkName(property);
354
- const mark = marks.find((candidate) => candidate.type.name === markName);
355
- const directFontProperty = directFontPropertyForFormattingProperty(property);
356
- if (directFontProperty) {
357
- const overrideMark = marks.find((candidate) => candidate.type.name === "runFormattingOverride");
358
- const isDirect = overrideMark ? (expectRunFormattingOverrideMarkAttrs(overrideMark).directFontProperties ?? []).includes(directFontProperty) : false;
359
- if (value === false || value === null) return isDirect;
360
- if (!isDirect) return true;
361
- } else if (value === false || value === null) return mark !== void 0;
362
- if (property === "underline") return mark?.attrs["style"] !== "single";
363
- if (property === "fontFamily") return mark?.attrs["ascii"] !== value || mark?.attrs["hAnsi"] !== value;
364
- if (property === "fontSizePt") return Number(mark?.attrs["size"]) !== Number(value) * 2;
365
- if (property === "color") {
366
- const current = mark?.attrs["rgb"];
367
- return (typeof current === "string" ? current.replace(/^#/u, "").toUpperCase() : null) !== String(value).replace(/^#/u, "").toUpperCase();
437
+ const sameDefinedFormattingValue = (left, right) => {
438
+ if (left === right) return true;
439
+ if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) return false;
440
+ if (Array.isArray(left) || Array.isArray(right)) {
441
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
442
+ return left.every((value, index) => sameDefinedFormattingValue(value, right.at(index)));
368
443
  }
369
- return mark === void 0;
444
+ const leftEntries = Object.entries(left).filter(([, value]) => value !== void 0);
445
+ const rightEntries = Object.entries(right).filter(([, value]) => value !== void 0);
446
+ if (leftEntries.length !== rightEntries.length) return false;
447
+ return leftEntries.every(([property, value]) => Object.hasOwn(right, property) && sameDefinedFormattingValue(value, Reflect.get(right, property)));
370
448
  };
371
- const formattingChangesForMarks = (marks, formatting) => {
449
+ const formattingChangesForAuthoredFormatting = (authoredFormatting, formatting) => {
372
450
  const changes = [];
373
- for (const [property, value] of Object.entries(formatting)) if (formattingPropertyWouldChange(marks, property, value)) changes.push(property);
451
+ for (const property of [
452
+ "bold",
453
+ "italic",
454
+ "underline",
455
+ "strike",
456
+ "fontFamily",
457
+ "fontSizePt",
458
+ "color",
459
+ "highlight",
460
+ "runShading"
461
+ ]) {
462
+ if (formatting[property] === void 0) continue;
463
+ const next = patchAuthoredRunFormatting({
464
+ authoredFormatting,
465
+ formatting,
466
+ includedProperties: [property]
467
+ });
468
+ if (!sameDefinedFormattingValue(authoredFormatting, next)) changes.push(property);
469
+ }
374
470
  return changes;
375
471
  };
376
- const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, author, date, initials, suggestionId = null }) => {
377
- if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return tr;
378
- if (mode === "direct") return applyInlineFormatting({
379
- tr,
380
- schema,
381
- from,
382
- to,
383
- formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING
384
- });
472
+ const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
473
+ if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return {
474
+ type: "applied",
475
+ transaction: tr,
476
+ revisionIds: [],
477
+ nextRevisionId: revisionIdSeed
478
+ };
479
+ if (mode === "direct") return {
480
+ type: "applied",
481
+ transaction: applyInlineFormatting({
482
+ tr,
483
+ from,
484
+ to,
485
+ formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
486
+ ...styleResolver !== void 0 ? { styleResolver } : {}
487
+ }),
488
+ revisionIds: [],
489
+ nextRevisionId: revisionIdSeed
490
+ };
385
491
  return applyTrackedInlineFormatting({
386
492
  tr,
387
493
  schema,
@@ -389,26 +495,85 @@ const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, au
389
495
  from,
390
496
  to,
391
497
  formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
392
- revisionId,
498
+ revisionIdSeed,
393
499
  author,
394
500
  date,
395
501
  initials,
396
- suggestionId
502
+ suggestionId,
503
+ ...styleResolver !== void 0 ? { styleResolver } : {}
397
504
  });
398
505
  };
399
- const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionId, author, date, initials, suggestionId = null }) => {
506
+ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
400
507
  const propertyChangeType = schema.marks["runPropertyChange"];
401
- if (!propertyChangeType) return tr;
508
+ if (!propertyChangeType) return {
509
+ type: "applied",
510
+ transaction: tr,
511
+ revisionIds: [],
512
+ nextRevisionId: revisionIdSeed
513
+ };
402
514
  const segments = [];
403
- doc.nodesBetween(from, to, (node, pos) => {
404
- if (!node.isText) return;
405
- const includedProperties = formattingChangesForMarks(node.marks, formatting);
406
- if (includedProperties.length === 0) return;
407
- const segmentFrom = Math.max(from, pos);
408
- const segmentTo = Math.min(to, pos + node.nodeSize);
409
- const previousFormatting = marksToTextFormatting(node.marks);
515
+ let hasPendingRunPropertyChange = false;
516
+ const representations = selectRunFormattingCarrierRepresentations({
517
+ doc,
518
+ from,
519
+ to
520
+ });
521
+ for (const representation of representations) {
522
+ const { node } = representation;
523
+ const styleContext = paragraphRunStyleContextAt({
524
+ doc,
525
+ pos: representation.from,
526
+ ...styleResolver !== void 0 ? { styleResolver } : {}
527
+ });
528
+ const previousFormatting = readAuthoredRunFormatting({
529
+ context: styleContext,
530
+ marks: node.marks,
531
+ ...styleResolver !== void 0 ? { styleResolver } : {}
532
+ });
533
+ const includedProperties = formattingChangesForAuthoredFormatting(previousFormatting, formatting);
534
+ if (includedProperties.length === 0) continue;
410
535
  const existingMark = node.marks.find((mark) => mark.type === propertyChangeType);
411
- const existingChanges = existingMark ? expectRunPropertyChangeMarkAttrs(existingMark).changes : [];
536
+ if (existingMark && expectRunPropertyChangeMarkAttrs(existingMark).changes.length > 0) {
537
+ hasPendingRunPropertyChange = true;
538
+ continue;
539
+ }
540
+ const formattingMarks = reconcileRunFormattingMarks({
541
+ authoredFormatting: patchAuthoredRunFormatting({
542
+ authoredFormatting: previousFormatting,
543
+ formatting,
544
+ includedProperties
545
+ }),
546
+ context: styleContext,
547
+ node,
548
+ ...styleResolver !== void 0 ? { styleResolver } : {}
549
+ });
550
+ segments.push({
551
+ formattingMarks,
552
+ representation,
553
+ includedProperties,
554
+ previousFormatting
555
+ });
556
+ }
557
+ if (hasPendingRunPropertyChange) return {
558
+ type: "pendingRunPropertyChange",
559
+ transaction: tr,
560
+ revisionIds: [],
561
+ nextRevisionId: revisionIdSeed
562
+ };
563
+ if (segments.length === 0) return {
564
+ type: "applied",
565
+ transaction: tr,
566
+ revisionIds: [],
567
+ nextRevisionId: revisionIdSeed
568
+ };
569
+ const suggestionAttrs = suggestionId === null ? {} : {
570
+ provenance: "suggested",
571
+ suggestionId
572
+ };
573
+ const revisionIds = [];
574
+ for (const segment of segments) {
575
+ const revisionId = revisionIdSeed + revisionIds.length;
576
+ revisionIds.push(revisionId);
412
577
  const change = {
413
578
  type: "runPropertyChange",
414
579
  info: {
@@ -417,35 +582,24 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
417
582
  date,
418
583
  ...initials ? { initials } : {}
419
584
  },
420
- ...Object.keys(previousFormatting).length > 0 ? { previousFormatting } : {}
585
+ ...Object.keys(segment.previousFormatting).length > 0 ? { previousFormatting: segment.previousFormatting } : {}
421
586
  };
422
- segments.push({
423
- from: segmentFrom,
424
- to: segmentTo,
425
- includedProperties,
426
- changes: [...existingChanges, change]
427
- });
428
- });
429
- if (segments.length === 0) return tr;
430
- const suggestionAttrs = suggestionId === null ? {} : {
431
- provenance: "suggested",
432
- suggestionId
433
- };
434
- for (const segment of segments) {
435
- applyInlineFormatting({
587
+ const marks = propertyChangeType.create({
588
+ changes: [change],
589
+ ...suggestionAttrs
590
+ }).addToSet(segment.formattingMarks);
591
+ applyMarksToRunFormattingRepresentation({
436
592
  tr,
437
- schema,
438
- from: segment.from,
439
- to: segment.to,
440
- formatting,
441
- includedProperties: segment.includedProperties
593
+ representation: segment.representation,
594
+ marks
442
595
  });
443
- tr.addMark(segment.from, segment.to, propertyChangeType.create({
444
- changes: segment.changes,
445
- ...suggestionAttrs
446
- }));
447
596
  }
448
- return tr;
597
+ return {
598
+ type: "applied",
599
+ transaction: tr,
600
+ revisionIds,
601
+ nextRevisionId: revisionIdSeed + revisionIds.length
602
+ };
449
603
  };
450
604
  /**
451
605
  * Module-scoped monotonic counter for tracked-change revision ids.
@@ -686,6 +840,7 @@ const buildEmphasisInlineContent = (schema, text, baseMarks) => {
686
840
  };
687
841
  const isBatchableParagraphInsertion = (item, mode) => {
688
842
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return false;
843
+ if (item.commentText !== void 0) return false;
689
844
  const insertTexts = item.insertTexts ?? [""];
690
845
  const isEmptyInsert = insertTexts.length === 1 && insertTexts[0]?.length === 0;
691
846
  return !(mode === "tracked-changes" && item.operation.pageBreakBefore === true && isEmptyInsert);
@@ -781,6 +936,7 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
781
936
  const liveCurrent = next.doc.nodeAt(current.position);
782
937
  if (!liveCurrent || liveCurrent.type.name !== final.type.name) panic("A final-mark rotation lost its paragraph", { position: current.position });
783
938
  const existing = expectParagraphAttrs(liveCurrent)._propertyChanges;
939
+ if (hasSerializableParagraphPropertyChange(existing)) panic("A final-mark rotation cannot append a second w:pPrChange", { position: current.position });
784
940
  const revisionId = nextRevisionId++;
785
941
  next = next.setNodeMarkup(current.position, void 0, {
786
942
  ...liveCurrent.attrs,
@@ -810,7 +966,7 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
810
966
  synthesizedRevisions
811
967
  };
812
968
  };
813
- const buildInsertedParagraphs = ({ item, schema, alignmentFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove }) => {
969
+ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove, numbering }) => {
814
970
  const operation = item.operation;
815
971
  if (operation.type !== "insertAfterBlock" && operation.type !== "insertBeforeBlock") panic("Only paragraph insertions can build inserted paragraphs", { type: operation.type });
816
972
  const insertionType = schema.marks["insertion"];
@@ -823,7 +979,12 @@ const buildInsertedParagraphs = ({ item, schema, alignmentFromStyle, mode, autho
823
979
  suggestionId
824
980
  } : {}
825
981
  };
826
- const baseAttrs = operation.inheritFormatting === false ? {} : stripBlockIdentityAttrs(item.blockNode.attrs);
982
+ const baseAttrs = operation.inheritFormatting === false ? {} : {
983
+ ...stripBlockIdentityAttrs(item.blockNode.attrs),
984
+ _propertyChanges: null,
985
+ pPrMark: null,
986
+ _suggestedInsert: null
987
+ };
827
988
  const insertTexts = item.insertTexts ?? [""];
828
989
  const revisionIds = [];
829
990
  const nodes = [];
@@ -847,33 +1008,51 @@ const buildInsertedParagraphs = ({ item, schema, alignmentFromStyle, mode, autho
847
1008
  const content = text.length > 0 ? buildEmphasisInlineContent(schema, text, marks) : null;
848
1009
  const attrs = isFirstParagraph ? { ...baseAttrs } : {};
849
1010
  if (isFirstParagraph && operation.pageBreakBefore === true) attrs["pageBreakBefore"] = true;
850
- if (isFirstParagraph && operation.listLevel === null) {
1011
+ const listLevel = operation.listLevel;
1012
+ if (isFirstParagraph && listLevel === null) {
851
1013
  attrs["numPr"] = null;
852
- Object.assign(attrs, CLEARED_LIST_MARKER_ATTRS);
853
- } else if (isFirstParagraph && operation.listLevel !== void 0) {
854
- const anchorNumPr = baseAttrs["numPr"];
1014
+ Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
1015
+ } else if (isFirstParagraph && typeof listLevel === "number") {
1016
+ const anchorNumPr = Reflect.get(baseAttrs, "numPr");
855
1017
  const numId = typeof anchorNumPr === "object" && anchorNumPr !== null && "numId" in anchorNumPr ? anchorNumPr.numId : void 0;
856
- attrs["numPr"] = {
857
- ...typeof numId === "number" && { numId },
858
- ilvl: operation.listLevel
859
- };
1018
+ if (typeof numId === "number") Object.assign(attrs, listLevelAttrPatch(operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode), {
1019
+ numId,
1020
+ ilvl: listLevel
1021
+ }, numbering));
1022
+ else {
1023
+ attrs["numPr"] = { ilvl: listLevel };
1024
+ Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
1025
+ }
860
1026
  }
861
1027
  if (isFirstParagraph && operation.styleId !== void 0) {
862
1028
  attrs["styleId"] = operation.styleId;
863
- if (operation.inheritFormatting !== false && operation.styleId !== null) Object.assign(attrs, CLEARED_LIST_MARKER_ATTRS);
1029
+ if (operation.inheritFormatting !== false && operation.styleId !== null) Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
864
1030
  }
865
- if (isFirstParagraph && (operation.styleId !== void 0 || operation.alignment !== void 0)) {
1031
+ if (isFirstParagraph && (operation.styleId !== void 0 || operation.alignment !== void 0 || operation.spacing !== void 0)) {
866
1032
  const inheritedDirectAlignment = operation.inheritFormatting === false ? void 0 : directParagraphAlignment(expectParagraphAttrs(item.blockNode));
1033
+ const inheritedDirectSpacing = operation.inheritFormatting === false ? void 0 : directParagraphSpacing(expectParagraphAttrs(item.blockNode));
867
1034
  const directAlignment = operation.alignment === void 0 ? inheritedDirectAlignment : operation.alignment ?? void 0;
868
- attrs["alignmentFromStyle"] = alignmentFromStyle;
869
- attrs["alignment"] = directAlignment ?? alignmentFromStyle ?? null;
1035
+ const directSpacing = operation.spacing === void 0 ? inheritedDirectSpacing : operation.spacing ?? void 0;
1036
+ attrs["alignmentFromStyle"] = formattingFromStyle?.alignment;
1037
+ attrs["alignment"] = directAlignment ?? formattingFromStyle?.alignment ?? null;
870
1038
  const sourceFormatting = attrs["_originalFormatting"];
871
- const originalFormatting = typeof sourceFormatting === "object" && sourceFormatting !== null ? { ...sourceFormatting } : {};
872
- if (operation.styleId !== void 0) if (operation.styleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
873
- else originalFormatting.styleId = operation.styleId;
874
- if (operation.alignment !== void 0 || inheritedDirectAlignment !== void 0) if (directAlignment === void 0) Reflect.deleteProperty(originalFormatting, "alignment");
875
- else originalFormatting.alignment = directAlignment;
876
- attrs["_originalFormatting"] = Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
1039
+ let originalFormatting = typeof sourceFormatting === "object" && sourceFormatting !== null ? { ...sourceFormatting } : void 0;
1040
+ if (operation.styleId !== void 0) {
1041
+ originalFormatting ??= {};
1042
+ if (operation.styleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
1043
+ else originalFormatting.styleId = operation.styleId;
1044
+ }
1045
+ if (operation.alignment !== void 0 || inheritedDirectAlignment !== void 0) {
1046
+ originalFormatting ??= {};
1047
+ if (directAlignment === void 0) Reflect.deleteProperty(originalFormatting, "alignment");
1048
+ else originalFormatting.alignment = directAlignment;
1049
+ }
1050
+ if (operation.spacing !== void 0) originalFormatting = withDirectParagraphSpacing(originalFormatting, directSpacing);
1051
+ if (operation.styleId !== void 0 || operation.spacing !== void 0) Object.assign(attrs, paragraphSpacingAttrPatch({
1052
+ direct: directSpacing,
1053
+ inherited: paragraphSpacingFromFormatting(formattingFromStyle)
1054
+ }));
1055
+ attrs["_originalFormatting"] = originalFormatting && Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
877
1056
  }
878
1057
  if (isSuggested && suggestionId !== null && paragraphRevisionId !== null) attrs["_suggestedInsert"] = {
879
1058
  suggestionId,
@@ -914,13 +1093,37 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
914
1093
  const insertionType = view.state.schema.marks["insertion"];
915
1094
  const deletionType = view.state.schema.marks["deletion"];
916
1095
  const commentType = view.state.schema.marks["comment"];
1096
+ const occupiedCommentIds = /* @__PURE__ */ new Set();
1097
+ if (commentType) view.state.doc.descendants((node) => {
1098
+ for (const mark of node.marks) {
1099
+ if (mark.type !== commentType) continue;
1100
+ const commentId = mark.attrs["commentId"];
1101
+ if (typeof commentId === "number") occupiedCommentIds.add(commentId);
1102
+ }
1103
+ });
1104
+ let provisionalCommentIdCursor = Number.MIN_SAFE_INTEGER;
1105
+ const reserveProvisionalCommentId = () => {
1106
+ while (occupiedCommentIds.has(provisionalCommentIdCursor)) provisionalCommentIdCursor += 1;
1107
+ const id = provisionalCommentIdCursor;
1108
+ provisionalCommentIdCursor += 1;
1109
+ occupiedCommentIds.add(id);
1110
+ return id;
1111
+ };
1112
+ const allocateCommittedCommentId = (text) => {
1113
+ if (createCommentId === void 0) panic("A resolved comment operation lost its comment-id allocator");
1114
+ const id = createCommentId(text);
1115
+ if (!Number.isSafeInteger(id) || occupiedCommentIds.has(id)) panic("A comment-id allocator returned an invalid or duplicate id", { id });
1116
+ occupiedCommentIds.add(id);
1117
+ return id;
1118
+ };
917
1119
  const styleResolver = getDocumentStyleResolver(view.state);
918
- const alignmentFromStyleForInsertion = (item) => {
1120
+ const numbering = getDocumentNumbering(view.state);
1121
+ const formattingFromStyleForInsertion = (item) => {
919
1122
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return;
920
1123
  const anchorAttrs = expectParagraphAttrs(item.blockNode);
921
1124
  const inheritedStyleId = item.operation.inheritFormatting === false ? null : anchorAttrs.styleId;
922
1125
  const styleId = item.operation.styleId !== void 0 ? item.operation.styleId : inheritedStyleId;
923
- return resolveAlignmentFromStyle({
1126
+ return resolveFormattingFromStyle({
924
1127
  attrs: anchorAttrs,
925
1128
  styleId,
926
1129
  styleResolver
@@ -964,6 +1167,13 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
964
1167
  });
965
1168
  continue;
966
1169
  }
1170
+ if (commentText !== void 0 && !hasRepresentableCommentAnchor(resolution.operation, mode)) {
1171
+ skipped.push({
1172
+ id: operation.id,
1173
+ reason: "unsupportedBlock"
1174
+ });
1175
+ continue;
1176
+ }
967
1177
  const deletion = resolution.operation.tableRowDeletion;
968
1178
  if (deletion) {
969
1179
  const rowKey = `${deletion.tablePosition}:${deletion.rowIndex}`;
@@ -993,11 +1203,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
993
1203
  code: "splitMultilineText",
994
1204
  paragraphCount: resolution.operation.insertTexts?.length ?? 1
995
1205
  });
996
- const commentId = commentText !== void 0 ? createCommentId?.(commentText) : void 0;
997
1206
  resolved.push({
998
1207
  ...resolution.operation,
999
1208
  originalIndex: index,
1000
- ...commentId !== void 0 && { commentId }
1209
+ ...commentText !== void 0 && { commentText }
1001
1210
  });
1002
1211
  }
1003
1212
  const tablePlan = planTableMutations(resolved.map((item) => ({
@@ -1040,6 +1249,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1040
1249
  }
1041
1250
  let tr = view.state.tr;
1042
1251
  const revisionIdReservation = executableResolved.reduce((total, item) => total + estimateRevisionIdReservation(item), 0);
1252
+ const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
1043
1253
  let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionSeed(revisionIdReservation);
1044
1254
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
1045
1255
  const insertedColumnCounts = /* @__PURE__ */ new Map();
@@ -1071,6 +1281,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1071
1281
  for (let executionIndex = 0; executionIndex < executionOrder.length; executionIndex++) {
1072
1282
  const item = executionOrder[executionIndex];
1073
1283
  if (!item) panic("The operation execution index exceeded the resolved plan", { executionIndex });
1284
+ if (mode === "tracked-changes" && writesParagraphPropertyChange(item)) {
1285
+ const livePosition = tr.mapping.map(item.blockFrom);
1286
+ const liveBlock = tr.doc.nodeAt(livePosition) ?? item.blockNode;
1287
+ if (hasSerializableParagraphPropertyChange(liveBlock.type.name === "paragraph" ? expectParagraphAttrs(liveBlock)._propertyChanges : void 0)) {
1288
+ skipped.push({
1289
+ id: item.operation.id,
1290
+ reason: "pendingParagraphPropertyChange"
1291
+ });
1292
+ continue;
1293
+ }
1294
+ }
1074
1295
  if (isBatchableParagraphInsertion(item, mode)) {
1075
1296
  const insertionRun = [item];
1076
1297
  for (let lookahead = executionIndex + 1; lookahead < executionOrder.length; lookahead++) {
@@ -1083,25 +1304,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1083
1304
  const runApplied = [];
1084
1305
  for (const insertion of insertionRun) {
1085
1306
  const insertionSuggestionId = isSuggested ? insertion.operation.suggestionId ?? insertion.operation.id : null;
1086
- const insertionCommentMark = insertion.commentId !== void 0 && commentType ? commentType.create({ commentId: insertion.commentId }) : null;
1087
1307
  const built = buildInsertedParagraphs({
1088
1308
  item: insertion,
1089
1309
  schema: view.state.schema,
1090
- alignmentFromStyle: alignmentFromStyleForInsertion(insertion),
1310
+ formattingFromStyle: formattingFromStyleForInsertion(insertion),
1091
1311
  mode,
1092
1312
  author,
1093
1313
  date,
1094
1314
  initials,
1095
- commentMark: insertionCommentMark,
1315
+ commentMark: null,
1096
1316
  suggestionId: insertionSuggestionId,
1097
1317
  revisionSeed,
1098
- isPairedMove
1318
+ isPairedMove,
1319
+ numbering
1099
1320
  });
1100
1321
  revisionSeed = built.nextRevisionId;
1101
1322
  nodeGroups.push(built.nodes);
1102
1323
  runApplied.push({
1103
1324
  id: insertion.operation.id,
1104
- ...insertion.commentId !== void 0 && { commentId: insertion.commentId },
1105
1325
  ...built.revisionIds[0] !== void 0 && {
1106
1326
  revisionId: built.revisionIds[0],
1107
1327
  revisionIds: built.revisionIds
@@ -1117,9 +1337,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1117
1337
  continue;
1118
1338
  }
1119
1339
  }
1120
- const commentMark = item.commentId !== void 0 && commentType ? commentType.create({ commentId: item.commentId }) : null;
1340
+ const provisionalCommentId = item.commentText !== void 0 ? reserveProvisionalCommentId() : void 0;
1341
+ const commentMark = provisionalCommentId !== void 0 && commentType ? commentType.create({ commentId: provisionalCommentId }) : null;
1121
1342
  const stepsBefore = tr.steps.length;
1122
- let appliedRevisionIds;
1343
+ let appliedRevisionIds = [];
1123
1344
  if (isSuggested && !SUGGESTED_SUPPORTED_OPERATION_TYPES.has(item.operation.type)) {
1124
1345
  skipped.push({
1125
1346
  id: item.operation.id,
@@ -1135,8 +1356,9 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1135
1356
  suggestionId
1136
1357
  } : {}
1137
1358
  };
1359
+ let operationRevisionSeed = revisionSeed;
1138
1360
  const structuralRevision = producesTrackedChanges ? {
1139
- revisionId: revisionSeed,
1361
+ revisionId: operationRevisionSeed,
1140
1362
  author,
1141
1363
  date,
1142
1364
  ...trackedRevisionExtras
@@ -1144,22 +1366,32 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1144
1366
  switch (item.operation.type) {
1145
1367
  case "replaceInBlock":
1146
1368
  case "replaceRange": {
1147
- const revisionIdDelete = revisionSeed++;
1148
- const revisionIdInsert = revisionSeed++;
1149
- const revisionIdBackground = revisionSeed++;
1369
+ const revisionIdDelete = operationRevisionSeed;
1370
+ const revisionIdInsert = producesTrackedChanges ? operationRevisionSeed + 1 : operationRevisionSeed;
1371
+ const revisionIdBackgroundSeed = producesTrackedChanges ? operationRevisionSeed + 2 : operationRevisionSeed;
1150
1372
  const stepsBeforeBackgroundClear = tr.steps.length;
1151
- tr = clearReplacementBackground({
1373
+ const backgroundResult = clearReplacementBackground({
1152
1374
  tr,
1153
1375
  schema: view.state.schema,
1154
1376
  from: item.from,
1155
1377
  to: item.to,
1156
1378
  mode,
1157
- revisionId: revisionIdBackground,
1379
+ revisionIdSeed: revisionIdBackgroundSeed,
1158
1380
  author,
1159
1381
  date,
1160
1382
  initials,
1161
- suggestionId
1383
+ suggestionId,
1384
+ styleResolver
1162
1385
  });
1386
+ if (backgroundResult.type === "pendingRunPropertyChange") {
1387
+ skipped.push({
1388
+ id: item.operation.id,
1389
+ reason: backgroundResult.type
1390
+ });
1391
+ continue;
1392
+ }
1393
+ tr = backgroundResult.transaction;
1394
+ operationRevisionSeed = backgroundResult.nextRevisionId;
1163
1395
  const clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
1164
1396
  tr = applyTextReplacement({
1165
1397
  tr,
@@ -1177,7 +1409,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1177
1409
  if (producesTrackedChanges) appliedRevisionIds = [
1178
1410
  revisionIdDelete,
1179
1411
  revisionIdInsert,
1180
- ...clearedBackground ? [revisionIdBackground] : []
1412
+ ...clearedBackground ? backgroundResult.revisionIds : []
1181
1413
  ];
1182
1414
  break;
1183
1415
  }
@@ -1186,44 +1418,52 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1186
1418
  break;
1187
1419
  case "formatRange":
1188
1420
  if (producesTrackedChanges) {
1189
- const revisionId = revisionSeed++;
1190
- tr = applyTrackedInlineFormatting({
1421
+ const formattingResult = applyTrackedInlineFormatting({
1191
1422
  tr,
1192
1423
  schema: view.state.schema,
1193
1424
  doc: tr.doc,
1194
1425
  from: item.from,
1195
1426
  to: item.to,
1196
1427
  formatting: item.operation.formatting,
1197
- revisionId,
1428
+ revisionIdSeed: operationRevisionSeed,
1198
1429
  author,
1199
1430
  date,
1200
1431
  initials,
1201
- suggestionId
1432
+ suggestionId,
1433
+ styleResolver
1202
1434
  });
1203
- appliedRevisionIds = [revisionId];
1435
+ if (formattingResult.type === "pendingRunPropertyChange") {
1436
+ skipped.push({
1437
+ id: item.operation.id,
1438
+ reason: formattingResult.type
1439
+ });
1440
+ continue;
1441
+ }
1442
+ tr = formattingResult.transaction;
1443
+ operationRevisionSeed = formattingResult.nextRevisionId;
1444
+ appliedRevisionIds = [...formattingResult.revisionIds];
1204
1445
  break;
1205
1446
  }
1206
1447
  tr = applyInlineFormatting({
1207
1448
  tr,
1208
- schema: view.state.schema,
1209
1449
  from: item.from,
1210
1450
  to: item.to,
1211
- formatting: item.operation.formatting
1451
+ formatting: item.operation.formatting,
1452
+ ...styleResolver !== void 0 ? { styleResolver } : {}
1212
1453
  });
1213
1454
  break;
1214
1455
  case "replaceBlock": {
1215
1456
  if (!isResolvedReplaceBlockOperation(item)) panic("Resolved replaceBlock operation lost its impact discriminator");
1216
1457
  const { changesStyle, changesText } = REPLACE_BLOCK_IMPACT[item.replaceBlockImpact];
1217
- const revisionIdDelete = revisionSeed;
1218
- const revisionIdInsert = changesText ? revisionSeed + 1 : revisionSeed;
1219
- const revisionIdBackground = changesText ? revisionSeed + 2 : revisionSeed;
1220
- if (changesText) revisionSeed += 3;
1221
- const revisionIdParagraph = producesTrackedChanges && changesStyle ? revisionSeed++ : null;
1458
+ const revisionIdDelete = operationRevisionSeed;
1459
+ const revisionIdInsert = changesText ? operationRevisionSeed + 1 : operationRevisionSeed;
1460
+ const revisionIdBackgroundSeed = changesText ? operationRevisionSeed + 2 : operationRevisionSeed;
1461
+ if (producesTrackedChanges && changesText) operationRevisionSeed = revisionIdBackgroundSeed;
1222
1462
  if (item.operation.preserveFormatting === false && mode === "direct") {
1223
1463
  const replacement = item.operation.text;
1224
1464
  const paragraphType = view.state.schema.nodes["paragraph"];
1225
1465
  if (paragraphType) {
1226
- const node = paragraphType.create(null, replacement.length === 0 ? null : buildEmphasisInlineContent(view.state.schema, replacement, []));
1466
+ const node = paragraphType.create(null, replacement.length === 0 ? null : buildEmphasisInlineContent(view.state.schema, replacement, commentMark ? [commentMark] : []));
1227
1467
  tr = tr.replaceWith(item.blockFrom, item.blockTo, node);
1228
1468
  tr = applyReplaceBlockStyleId({
1229
1469
  item,
@@ -1244,21 +1484,33 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1244
1484
  break;
1245
1485
  }
1246
1486
  let clearedBackground = false;
1487
+ let backgroundRevisionIds = [];
1247
1488
  if (changesText) {
1248
1489
  const stepsBeforeBackgroundClear = tr.steps.length;
1249
- tr = clearReplacementBackground({
1490
+ const backgroundResult = clearReplacementBackground({
1250
1491
  tr,
1251
1492
  schema: view.state.schema,
1252
1493
  from: item.from,
1253
1494
  to: item.to,
1254
1495
  mode,
1255
- revisionId: revisionIdBackground,
1496
+ revisionIdSeed: revisionIdBackgroundSeed,
1256
1497
  author,
1257
1498
  date,
1258
1499
  initials,
1259
- suggestionId
1500
+ suggestionId,
1501
+ styleResolver
1260
1502
  });
1503
+ if (backgroundResult.type === "pendingRunPropertyChange") {
1504
+ skipped.push({
1505
+ id: item.operation.id,
1506
+ reason: backgroundResult.type
1507
+ });
1508
+ continue;
1509
+ }
1510
+ tr = backgroundResult.transaction;
1511
+ operationRevisionSeed = backgroundResult.nextRevisionId;
1261
1512
  clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
1513
+ backgroundRevisionIds = backgroundResult.revisionIds;
1262
1514
  tr = applyTextReplacement({
1263
1515
  tr,
1264
1516
  item,
@@ -1273,6 +1525,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1273
1525
  diffText
1274
1526
  });
1275
1527
  }
1528
+ const revisionIdParagraph = producesTrackedChanges && changesStyle ? operationRevisionSeed++ : null;
1276
1529
  const styleResult = applyReplaceBlockStyleId({
1277
1530
  item,
1278
1531
  tr,
@@ -1285,9 +1538,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1285
1538
  } } : {}
1286
1539
  });
1287
1540
  tr = styleResult.tr;
1541
+ if (!changesText && commentMark) tr = tr.addMark(item.from, item.to, commentMark);
1288
1542
  if (producesTrackedChanges) appliedRevisionIds = [
1289
1543
  ...changesText ? [revisionIdDelete, revisionIdInsert] : [],
1290
- ...clearedBackground ? [revisionIdBackground] : [],
1544
+ ...clearedBackground ? backgroundRevisionIds : [],
1291
1545
  ...styleResult.revisionId === null ? [] : [styleResult.revisionId]
1292
1546
  ];
1293
1547
  break;
@@ -1306,17 +1560,18 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1306
1560
  const built = buildInsertedParagraphs({
1307
1561
  item,
1308
1562
  schema: view.state.schema,
1309
- alignmentFromStyle: alignmentFromStyleForInsertion(item),
1563
+ formattingFromStyle: formattingFromStyleForInsertion(item),
1310
1564
  mode,
1311
1565
  author,
1312
1566
  date,
1313
1567
  initials,
1314
1568
  commentMark,
1315
1569
  suggestionId,
1316
- revisionSeed,
1317
- isPairedMove
1570
+ revisionSeed: operationRevisionSeed,
1571
+ isPairedMove,
1572
+ numbering
1318
1573
  });
1319
- revisionSeed = built.nextRevisionId;
1574
+ operationRevisionSeed = built.nextRevisionId;
1320
1575
  if (built.revisionIds.length > 0) appliedRevisionIds = built.revisionIds;
1321
1576
  tr = tr.insert(item.from, built.nodes);
1322
1577
  break;
@@ -1342,7 +1597,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1342
1597
  }
1343
1598
  let node = signatureTable;
1344
1599
  if (isSuggested && suggestionId !== null) {
1345
- const revisionId = revisionSeed++;
1600
+ const revisionId = operationRevisionSeed++;
1346
1601
  node = signatureTable.type.create({
1347
1602
  ...signatureTable.attrs,
1348
1603
  _suggestedInsert: {
@@ -1385,7 +1640,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1385
1640
  }
1386
1641
  tr = result.transaction;
1387
1642
  if (result.revisionId !== null) {
1388
- revisionSeed++;
1643
+ operationRevisionSeed++;
1389
1644
  appliedRevisionIds = [result.revisionId];
1390
1645
  }
1391
1646
  break;
@@ -1415,7 +1670,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1415
1670
  }
1416
1671
  tr = result.transaction;
1417
1672
  if (result.revisionId !== null) {
1418
- revisionSeed++;
1673
+ operationRevisionSeed++;
1419
1674
  appliedRevisionIds = [result.revisionId];
1420
1675
  }
1421
1676
  const columnKey = getTableColumnCoordinateKey(insertion);
@@ -1448,7 +1703,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1448
1703
  }
1449
1704
  tr = result.transaction;
1450
1705
  if (result.revisionId !== null) {
1451
- revisionSeed++;
1706
+ operationRevisionSeed++;
1452
1707
  appliedRevisionIds = [result.revisionId];
1453
1708
  }
1454
1709
  break;
@@ -1464,7 +1719,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1464
1719
  });
1465
1720
  continue;
1466
1721
  }
1467
- const revisionId = mode === "tracked-changes" ? revisionSeed : null;
1722
+ const revisionId = mode === "tracked-changes" ? operationRevisionSeed : null;
1468
1723
  const nextTr = revisionId === null ? mergeTableRectangle({
1469
1724
  tr,
1470
1725
  tablePosition,
@@ -1488,7 +1743,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1488
1743
  }
1489
1744
  tr = nextTr;
1490
1745
  if (revisionId !== null) {
1491
- revisionSeed++;
1746
+ operationRevisionSeed++;
1492
1747
  appliedRevisionIds = [revisionId];
1493
1748
  }
1494
1749
  break;
@@ -1504,7 +1759,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1504
1759
  });
1505
1760
  continue;
1506
1761
  }
1507
- const revisionId = mode === "tracked-changes" ? revisionSeed : null;
1762
+ const revisionId = mode === "tracked-changes" ? operationRevisionSeed : null;
1508
1763
  const nextTr = revisionId === null ? splitTableRectangle({
1509
1764
  tr,
1510
1765
  tablePosition,
@@ -1528,7 +1783,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1528
1783
  }
1529
1784
  tr = nextTr;
1530
1785
  if (revisionId !== null) {
1531
- revisionSeed++;
1786
+ operationRevisionSeed++;
1532
1787
  appliedRevisionIds = [revisionId];
1533
1788
  }
1534
1789
  break;
@@ -1556,7 +1811,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1556
1811
  }
1557
1812
  tr = result.transaction;
1558
1813
  if (result.revisionId !== null) {
1559
- revisionSeed++;
1814
+ operationRevisionSeed++;
1560
1815
  appliedRevisionIds = [result.revisionId];
1561
1816
  }
1562
1817
  break;
@@ -1568,7 +1823,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1568
1823
  break;
1569
1824
  }
1570
1825
  if (deletionType) {
1571
- const revisionId = revisionSeed++;
1826
+ const revisionId = operationRevisionSeed++;
1572
1827
  const deletionMark = deletionType.create({
1573
1828
  revisionId,
1574
1829
  author,
@@ -1583,7 +1838,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1583
1838
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
1584
1839
  const markPosition = tr.mapping.map(item.blockFrom);
1585
1840
  if (!paragraphEndsItsContainer(tr.doc.resolve(markPosition), item.blockNode.type.name) && tr.doc.nodeAt(markPosition)?.attrs["pPrMark"] == null) {
1586
- const markRevisionId = revisionSeed++;
1841
+ const markRevisionId = operationRevisionSeed++;
1587
1842
  tr = tr.setNodeAttribute(markPosition, "pPrMark", {
1588
1843
  kind: isPairedMove(item.operation.moveId) ? "moveFrom" : "del",
1589
1844
  info: {
@@ -1600,7 +1855,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1600
1855
  break;
1601
1856
  case "insertTable": {
1602
1857
  const revision = {
1603
- revisionId: revisionSeed,
1858
+ revisionId: operationRevisionSeed,
1604
1859
  author,
1605
1860
  date,
1606
1861
  ...trackedRevisionExtras
@@ -1622,7 +1877,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1622
1877
  });
1623
1878
  continue;
1624
1879
  }
1625
- if (producesTrackedChanges) appliedRevisionIds = [revisionSeed++];
1880
+ if (producesTrackedChanges) appliedRevisionIds = [operationRevisionSeed++];
1626
1881
  tr = tr.insert(item.from, table);
1627
1882
  markStructuralChange(tr);
1628
1883
  break;
@@ -1660,7 +1915,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1660
1915
  continue;
1661
1916
  }
1662
1917
  const revision = {
1663
- revisionId: revisionSeed++,
1918
+ revisionId: operationRevisionSeed++,
1664
1919
  author,
1665
1920
  date,
1666
1921
  ...trackedRevisionExtras
@@ -1694,80 +1949,89 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1694
1949
  case "setBlockParagraphProperties": {
1695
1950
  const blockPosition = tr.mapping.map(item.blockFrom);
1696
1951
  const liveBlock = tr.doc.nodeAt(blockPosition) ?? item.blockNode;
1697
- const liveAttrs = expectParagraphAttrs(liveBlock);
1698
- const resolvedAlignmentFromStyle = item.operation.properties.styleId === void 0 ? void 0 : resolveAlignmentFromStyle({
1699
- attrs: liveAttrs,
1700
- styleId: item.operation.properties.styleId,
1701
- styleResolver
1702
- });
1703
- const patch = paragraphPropertiesPatch({
1952
+ const appliedProperties = applyBlockParagraphProperties({
1953
+ tr,
1954
+ position: blockPosition,
1704
1955
  node: liveBlock,
1705
1956
  properties: item.operation.properties,
1706
- resolvedAlignmentFromStyle
1957
+ styleResolver,
1958
+ numbering,
1959
+ ...mode === "direct" ? {} : { revisionInfo: () => ({
1960
+ id: operationRevisionSeed++,
1961
+ author,
1962
+ date,
1963
+ ...trackedRevisionExtras
1964
+ }) }
1707
1965
  });
1708
- if (patch === null) {
1966
+ if (!appliedProperties.changed) {
1709
1967
  skipped.push({
1710
1968
  id: item.operation.id,
1711
1969
  reason: "noopOperation"
1712
1970
  });
1713
1971
  continue;
1714
1972
  }
1715
- if (mode === "direct") {
1716
- tr = tr.setNodeMarkup(blockPosition, void 0, {
1717
- ...liveBlock.attrs,
1718
- ...patch
1719
- });
1720
- break;
1721
- }
1722
- const revisionId = revisionSeed++;
1723
- const change = {
1724
- type: "paragraphPropertyChange",
1725
- info: {
1726
- id: revisionId,
1727
- author,
1728
- date,
1729
- ...trackedRevisionExtras
1730
- },
1731
- previousFormatting: paragraphPropertiesSnapshot(liveBlock)
1732
- };
1733
- const existing = expectParagraphAttrs(liveBlock)._propertyChanges;
1734
- tr = tr.setNodeMarkup(blockPosition, void 0, {
1735
- ...liveBlock.attrs,
1736
- ...patch,
1737
- _propertyChanges: [...Array.isArray(existing) ? existing : [], change]
1738
- });
1739
- appliedRevisionIds = [revisionId];
1973
+ tr = appliedProperties.tr;
1974
+ if (appliedProperties.revisionId !== null) appliedRevisionIds = [appliedProperties.revisionId];
1740
1975
  break;
1741
1976
  }
1742
1977
  case "splitBlock": {
1743
1978
  if (mode === "direct") {
1744
1979
  if (item.to > item.from) tr = tr.delete(item.from, item.to);
1745
1980
  tr = tr.split(item.from);
1746
- break;
1747
- }
1748
- const revisionIdMark = revisionSeed++;
1749
- const info = {
1750
- id: revisionIdMark,
1751
- author,
1752
- date,
1753
- ...trackedRevisionExtras
1754
- };
1755
- appliedRevisionIds = [revisionIdMark];
1756
- if (item.to > item.from && deletionType) {
1757
- const revisionIdSeparator = revisionSeed++;
1758
- tr = tr.addMark(item.from, item.to, deletionType.create({
1759
- revisionId: revisionIdSeparator,
1981
+ } else {
1982
+ const revisionIdMark = operationRevisionSeed++;
1983
+ const info = {
1984
+ id: revisionIdMark,
1760
1985
  author,
1761
1986
  date,
1762
1987
  ...trackedRevisionExtras
1763
- }));
1764
- appliedRevisionIds = [revisionIdMark, revisionIdSeparator];
1988
+ };
1989
+ appliedRevisionIds = [revisionIdMark];
1990
+ if (item.to > item.from && deletionType) {
1991
+ const revisionIdSeparator = operationRevisionSeed++;
1992
+ tr = tr.addMark(item.from, item.to, deletionType.create({
1993
+ revisionId: revisionIdSeparator,
1994
+ author,
1995
+ date,
1996
+ ...trackedRevisionExtras
1997
+ }));
1998
+ appliedRevisionIds.push(revisionIdSeparator);
1999
+ }
2000
+ tr = tr.split(item.from);
2001
+ tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2002
+ kind: "ins",
2003
+ info
2004
+ });
1765
2005
  }
1766
- tr = tr.split(item.from);
1767
- tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
1768
- kind: "ins",
1769
- info
2006
+ const allocateSplitParagraphPropertyRevisionInfo = () => ({
2007
+ id: operationRevisionSeed++,
2008
+ author,
2009
+ date,
2010
+ ...trackedRevisionExtras
1770
2011
  });
2012
+ const paragraphPropertyTargets = [{
2013
+ position: item.blockFrom,
2014
+ properties: item.operation.firstParagraphProperties
2015
+ }, {
2016
+ position: item.from + 1,
2017
+ properties: item.operation.secondParagraphProperties
2018
+ }];
2019
+ for (const { position, properties } of paragraphPropertyTargets) {
2020
+ if (!properties) continue;
2021
+ const paragraph = tr.doc.nodeAt(position);
2022
+ if (!paragraph) panic("A resolved split did not produce two paragraphs");
2023
+ const appliedProperties = applyBlockParagraphProperties({
2024
+ tr,
2025
+ position,
2026
+ node: paragraph,
2027
+ properties,
2028
+ styleResolver,
2029
+ numbering,
2030
+ ...mode === "direct" ? {} : { revisionInfo: allocateSplitParagraphPropertyRevisionInfo }
2031
+ });
2032
+ tr = appliedProperties.tr;
2033
+ if (appliedProperties.revisionId !== null) appliedRevisionIds.push(appliedProperties.revisionId);
2034
+ }
1771
2035
  break;
1772
2036
  }
1773
2037
  case "mergeBlockWithNext": {
@@ -1776,30 +2040,50 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1776
2040
  if (mode === "direct") {
1777
2041
  if (separator.length > 0) tr = tr.insertText(separator, insertAt);
1778
2042
  tr = tr.join(item.blockTo + separator.length);
1779
- break;
2043
+ } else {
2044
+ const revisionIdMark = operationRevisionSeed++;
2045
+ appliedRevisionIds = [revisionIdMark];
2046
+ if (separator.length > 0 && insertionType) {
2047
+ const revisionIdSeparator = operationRevisionSeed++;
2048
+ tr = tr.insertText(separator, insertAt);
2049
+ tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
2050
+ revisionId: revisionIdSeparator,
2051
+ author,
2052
+ date,
2053
+ ...trackedRevisionExtras
2054
+ }));
2055
+ appliedRevisionIds.push(revisionIdSeparator);
2056
+ }
2057
+ tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2058
+ kind: "del",
2059
+ info: {
2060
+ id: revisionIdMark,
2061
+ author,
2062
+ date,
2063
+ ...trackedRevisionExtras
2064
+ }
2065
+ });
1780
2066
  }
1781
- const revisionIdMark = revisionSeed++;
1782
- appliedRevisionIds = [revisionIdMark];
1783
- if (separator.length > 0 && insertionType) {
1784
- const revisionIdSeparator = revisionSeed++;
1785
- tr = tr.insertText(separator, insertAt);
1786
- tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
1787
- revisionId: revisionIdSeparator,
1788
- author,
1789
- date,
1790
- ...trackedRevisionExtras
1791
- }));
1792
- appliedRevisionIds = [revisionIdMark, revisionIdSeparator];
2067
+ if (item.operation.mergedParagraphProperties) {
2068
+ const mergedParagraph = tr.doc.nodeAt(item.blockFrom);
2069
+ if (!mergedParagraph) panic("A resolved merge did not produce a paragraph");
2070
+ const appliedProperties = applyBlockParagraphProperties({
2071
+ tr,
2072
+ position: item.blockFrom,
2073
+ node: mergedParagraph,
2074
+ properties: item.operation.mergedParagraphProperties,
2075
+ styleResolver,
2076
+ numbering,
2077
+ ...mode === "direct" ? {} : { revisionInfo: () => ({
2078
+ id: operationRevisionSeed++,
2079
+ author,
2080
+ date,
2081
+ ...trackedRevisionExtras
2082
+ }) }
2083
+ });
2084
+ tr = appliedProperties.tr;
2085
+ if (appliedProperties.revisionId !== null) appliedRevisionIds.push(appliedProperties.revisionId);
1793
2086
  }
1794
- tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
1795
- kind: "del",
1796
- info: {
1797
- id: revisionIdMark,
1798
- author,
1799
- date,
1800
- ...trackedRevisionExtras
1801
- }
1802
- });
1803
2087
  break;
1804
2088
  }
1805
2089
  case "commentOnBlock":
@@ -1814,10 +2098,25 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1814
2098
  });
1815
2099
  continue;
1816
2100
  }
2101
+ let committedCommentId;
2102
+ if (item.commentText !== void 0) {
2103
+ if (commentType === void 0 || provisionalCommentId === void 0 || commentMarkRanges(tr.doc, commentType, provisionalCommentId).length === 0) panic("A commented operation changed the document without producing a comment anchor", {
2104
+ operationId: item.operation.id,
2105
+ operationType: item.operation.type
2106
+ });
2107
+ committedCommentId = allocateCommittedCommentId(item.commentText);
2108
+ tr = commitProvisionalComment({
2109
+ tr,
2110
+ commentType,
2111
+ provisionalId: provisionalCommentId,
2112
+ committedId: committedCommentId
2113
+ });
2114
+ }
2115
+ revisionSeed = operationRevisionSeed;
1817
2116
  applied.push({
1818
2117
  id: item.operation.id,
1819
- ...item.commentId !== void 0 && { commentId: item.commentId },
1820
- ...appliedRevisionIds !== void 0 && appliedRevisionIds[0] !== void 0 && {
2118
+ ...committedCommentId !== void 0 && { commentId: committedCommentId },
2119
+ ...appliedRevisionIds[0] !== void 0 && {
1821
2120
  revisionId: appliedRevisionIds[0],
1822
2121
  revisionIds: appliedRevisionIds
1823
2122
  },
@@ -1851,6 +2150,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1851
2150
  revisionIds: [...receipt.revisionIds, revisionId]
1852
2151
  };
1853
2152
  }
2153
+ if (ownsSharedRevisionIdCursor) revisionIdCursor = Math.max(revisionIdCursor, revisionSeed);
1854
2154
  if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
1855
2155
  view.dispatch(tr);
1856
2156
  }
@@ -2032,7 +2332,8 @@ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId
2032
2332
  const cleanBlock = buildCleanBlockText(live.node, live.from);
2033
2333
  const currentText = cleanBlock.text;
2034
2334
  const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText));
2035
- if (currentTextHash !== anchor.textHash) return {
2335
+ const structuralBoundaryHash = hashFolioAIBlockStructuralBoundaries(cleanBlock);
2336
+ if (currentTextHash !== anchor.textHash || structuralBoundaryHash !== anchor.structuralBoundaryHash) return {
2036
2337
  type: "skip",
2037
2338
  reason: "changedBlock"
2038
2339
  };
@@ -2062,9 +2363,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2062
2363
  };
2063
2364
  if (operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange") {
2064
2365
  const { startOffset, endOffset, selectedTextHash } = operation.range;
2065
- const from = cleanBlock.offsets[startOffset];
2066
- const to = cleanBlock.offsets[endOffset];
2067
- if (from === void 0 || to === void 0) return {
2366
+ if (cleanBlock.offsets[startOffset] === void 0 || cleanBlock.offsets[endOffset] === void 0) return {
2068
2367
  type: "skip",
2069
2368
  reason: "staleRange"
2070
2369
  };
@@ -2073,6 +2372,15 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2073
2372
  type: "skip",
2074
2373
  reason: "staleRange"
2075
2374
  };
2375
+ const range = resolveCleanTextRange({
2376
+ cleanBlock,
2377
+ startOffset,
2378
+ endOffset
2379
+ });
2380
+ if (range === null) return {
2381
+ type: "skip",
2382
+ reason: "unsupportedBlock"
2383
+ };
2076
2384
  if (operation.type === "replaceRange" && selectedText === operation.replace) return {
2077
2385
  type: "skip",
2078
2386
  reason: "noopOperation"
@@ -2081,8 +2389,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2081
2389
  type: "resolved",
2082
2390
  operation: {
2083
2391
  operation,
2084
- from,
2085
- to,
2392
+ from: range.from,
2393
+ to: range.to,
2086
2394
  blockFrom,
2087
2395
  blockTo,
2088
2396
  blockNode
@@ -2385,8 +2693,10 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2385
2693
  };
2386
2694
  if (operation.type === "splitBlock") {
2387
2695
  const separator = operation.separator ?? "";
2388
- const at = cleanBlock.offsets[operation.offset];
2389
- const after = cleanBlock.offsets[operation.offset + separator.length];
2696
+ const startOffset = operation.offset;
2697
+ const endOffset = operation.offset + separator.length;
2698
+ const at = cleanBlock.offsets[startOffset];
2699
+ const after = cleanBlock.offsets[endOffset];
2390
2700
  if (at === void 0 || after === void 0 || operation.offset === 0 || operation.offset + separator.length >= currentText.length) return {
2391
2701
  type: "skip",
2392
2702
  reason: "staleRange"
@@ -2395,7 +2705,12 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2395
2705
  type: "skip",
2396
2706
  reason: "staleRange"
2397
2707
  };
2398
- if (!canSplit(doc, at)) return {
2708
+ const range = resolveCleanTextRange({
2709
+ cleanBlock,
2710
+ startOffset,
2711
+ endOffset
2712
+ });
2713
+ if (range === null || !canSplit(doc, range.from)) return {
2399
2714
  type: "skip",
2400
2715
  reason: "unsupportedBlock"
2401
2716
  };
@@ -2403,8 +2718,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2403
2718
  type: "resolved",
2404
2719
  operation: {
2405
2720
  operation,
2406
- from: at,
2407
- to: after,
2721
+ from: range.from,
2722
+ to: range.to,
2408
2723
  blockFrom,
2409
2724
  blockTo,
2410
2725
  blockNode
@@ -2429,7 +2744,12 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2429
2744
  };
2430
2745
  }
2431
2746
  if (operation.type === "deleteBlock" || operation.type === "replaceBlock") {
2747
+ const replaceChangesText = operation.type === "replaceBlock" && operation.text !== currentText;
2432
2748
  const replaceChangesStyle = operation.type === "replaceBlock" && operation.styleId !== void 0 && operation.styleId !== (expectParagraphAttrs(blockNode).styleId ?? null);
2749
+ if (replaceChangesText && cleanBlock.structuralBoundaries.length > 0) return {
2750
+ type: "skip",
2751
+ reason: "unsupportedBlock"
2752
+ };
2433
2753
  const range = getTextRangeFromCleanBlock(cleanBlock);
2434
2754
  if (!range) {
2435
2755
  const insertionPoint = cleanBlock.offsets.at(0);
@@ -2451,7 +2771,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2451
2771
  blockTo,
2452
2772
  blockNode,
2453
2773
  replaceBlockImpact: resolveReplaceBlockImpact({
2454
- changesText: operation.text !== currentText,
2774
+ changesText: replaceChangesText,
2455
2775
  changesStyle: replaceChangesStyle
2456
2776
  })
2457
2777
  }
@@ -2484,7 +2804,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2484
2804
  blockTo,
2485
2805
  blockNode,
2486
2806
  replaceBlockImpact: resolveReplaceBlockImpact({
2487
- changesText: operation.text !== currentText,
2807
+ changesText: replaceChangesText,
2488
2808
  changesStyle: replaceChangesStyle
2489
2809
  })
2490
2810
  }
@@ -2525,7 +2845,7 @@ const resolveTextInCleanBlock = (cleanBlock, find) => {
2525
2845
  type: "skip",
2526
2846
  reason: "emptyOperation"
2527
2847
  };
2528
- const { text, offsets } = cleanBlock;
2848
+ const { text } = cleanBlock;
2529
2849
  const firstIndex = text.indexOf(find);
2530
2850
  if (firstIndex === -1) return {
2531
2851
  type: "skip",
@@ -2535,16 +2855,18 @@ const resolveTextInCleanBlock = (cleanBlock, find) => {
2535
2855
  type: "skip",
2536
2856
  reason: "ambiguousFind"
2537
2857
  };
2538
- const from = offsets[firstIndex];
2539
- const to = offsets[firstIndex + find.length];
2540
- if (from === void 0 || to === void 0) return {
2858
+ const range = resolveCleanTextRange({
2859
+ cleanBlock,
2860
+ startOffset: firstIndex,
2861
+ endOffset: firstIndex + find.length
2862
+ });
2863
+ if (range === null) return {
2541
2864
  type: "skip",
2542
2865
  reason: "unsupportedBlock"
2543
2866
  };
2544
2867
  return {
2545
2868
  type: "resolved",
2546
- from,
2547
- to
2869
+ ...range
2548
2870
  };
2549
2871
  };
2550
2872
  const getTextRangeFromCleanBlock = (cleanBlock) => {