@stll/folio-core 0.35.1 → 0.37.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 (236) hide show
  1. package/README.md +59 -1
  2. package/dist/ai-edits/apply.js +878 -377
  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 +141 -73
  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 +105 -20
  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 +62 -61
  20. package/dist/ai-edits/word-diff.d.ts +18 -3
  21. package/dist/ai-edits/word-diff.js +556 -67
  22. package/dist/ai-suggestions/conflict.js +1 -4
  23. package/dist/ai-suggestions/text-positions.d.ts +8 -0
  24. package/dist/ai-suggestions/text-positions.js +27 -5
  25. package/dist/compare/__fixtures__/body-sequence.js +3 -4
  26. package/dist/compare/column-alignment.d.ts +13 -13
  27. package/dist/compare/column-alignment.js +78 -39
  28. package/dist/compare/compare.d.ts +4 -3
  29. package/dist/compare/compare.js +48 -21
  30. package/dist/compare/content-alignment.d.ts +95 -0
  31. package/dist/compare/content-alignment.js +1450 -0
  32. package/dist/compare/content-types.d.ts +100 -0
  33. package/dist/compare/content-types.js +0 -0
  34. package/dist/compare/content.d.ts +209 -0
  35. package/dist/compare/content.js +885 -0
  36. package/dist/compare/formatting.d.ts +5 -5
  37. package/dist/compare/formatting.js +6 -4
  38. package/dist/compare/plan.d.ts +6 -1
  39. package/dist/compare/plan.js +171 -566
  40. package/dist/compare/scenario.d.ts +2 -2
  41. package/dist/compare/types.d.ts +3 -3
  42. package/dist/compare/verification.d.ts +8 -9
  43. package/dist/compare/verification.js +72 -50
  44. package/dist/compat/eigenpal.d.ts +5 -3
  45. package/dist/compat/eigenpal.js +3 -2
  46. package/dist/controller/headerFooterEditorManager.js +18 -10
  47. package/dist/controller/hiddenEditorApi.js +1 -1
  48. package/dist/controller/hiddenEditorManager.d.ts +7 -2
  49. package/dist/controller/hiddenEditorManager.js +15 -6
  50. package/dist/controller/layoutPipeline.js +5 -3
  51. package/dist/controller/noteEditorManager.js +12 -7
  52. package/dist/display-list/build/headerFooterPrimitives.js +1 -0
  53. package/dist/display-list/build/storyPrimitives.d.ts +5 -2
  54. package/dist/display-list/build/storyPrimitives.js +60 -3
  55. package/dist/display-list/build/tablePrimitives.js +30 -0
  56. package/dist/display-list/primitives.d.ts +1 -1
  57. package/dist/document-operations.d.ts +8 -4
  58. package/dist/document-operations.js +104 -12
  59. package/dist/docx/blockContentParser.js +6 -27
  60. package/dist/docx/commentParser.js +2 -4
  61. package/dist/docx/commentRangeIntegrity.js +2 -4
  62. package/dist/docx/documentClone.d.ts +2 -0
  63. package/dist/docx/documentClone.js +2 -0
  64. package/dist/docx/ensureParaIds.js +9 -4
  65. package/dist/docx/headerFooterParser.js +2 -4
  66. package/dist/docx/normalizeBaseDirection.js +5 -7
  67. package/dist/docx/numberingParser.d.ts +3 -1
  68. package/dist/docx/numberingParser.js +4 -1
  69. package/dist/docx/paragraphParser.js +34 -35
  70. package/dist/docx/paragraphPropertySource.d.ts +49 -0
  71. package/dist/docx/paragraphPropertySource.js +113 -0
  72. package/dist/docx/paragraphTraversal.js +30 -34
  73. package/dist/docx/rezip.d.ts +0 -8
  74. package/dist/docx/rezip.js +41 -16
  75. package/dist/docx/runConsolidator.js +2 -4
  76. package/dist/docx/runParser.js +6 -17
  77. package/dist/docx/sectionParser.js +1 -15
  78. package/dist/docx/serializer/paragraphSerializer.js +289 -30
  79. package/dist/docx/serializer/runSerializer.js +6 -12
  80. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -10
  81. package/dist/docx/serializer/tableSerializer.js +4 -23
  82. package/dist/docx/serializer/trackedChangeAttributes.d.ts +11 -0
  83. package/dist/docx/serializer/trackedChangeAttributes.js +31 -0
  84. package/dist/docx/server/build.d.ts +1 -1
  85. package/dist/docx/server/createBilingualDocument.js +8 -10
  86. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +22 -13
  87. package/dist/docx/server/validateDocxConformance.js +17 -13
  88. package/dist/docx/tableParser.js +1 -37
  89. package/dist/docx/trackedChangeInfo.d.ts +11 -0
  90. package/dist/docx/trackedChangeInfo.js +38 -0
  91. package/dist/docx/verbatimCapture.d.ts +20 -9
  92. package/dist/docx/verbatimCapture.js +208 -8
  93. package/dist/docx/xmlParser.d.ts +3 -1
  94. package/dist/docx/xmlParser.js +6 -1
  95. package/dist/docx/xmlSafety.d.ts +1 -1
  96. package/dist/docx/xmlSafety.js +157 -0
  97. package/dist/headless-layout.js +5 -4
  98. package/dist/index.d.ts +5 -3
  99. package/dist/index.js +3 -2
  100. package/dist/internal/headlessRevisionResolution.d.ts +23 -0
  101. package/dist/internal/headlessRevisionResolution.js +155 -0
  102. package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
  103. package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
  104. package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
  105. package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
  106. package/dist/internal/pageBreakRunPartition.d.ts +26 -0
  107. package/dist/internal/pageBreakRunPartition.js +35 -0
  108. package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
  109. package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
  110. package/dist/internal/sectionEndpointResolution.d.ts +32 -0
  111. package/dist/internal/sectionEndpointResolution.js +38 -0
  112. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  113. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
  114. package/dist/layout-bridge/convert/headerFooterLayout.js +52 -9
  115. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
  116. package/dist/layout-bridge/convert/toFlowBlocks.js +391 -82
  117. package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
  118. package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
  119. package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
  120. package/dist/layout-bridge/dom/textStreamDom.js +75 -0
  121. package/dist/layout-bridge/headerFooterLayout.js +16 -30
  122. package/dist/layout-engine/index.js +12 -3
  123. package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
  124. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
  125. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  126. package/dist/layout-engine/types.d.ts +7 -0
  127. package/dist/layout-painter/imageLayout.d.ts +1 -1
  128. package/dist/layout-painter/renderTable.js +20 -0
  129. package/dist/markdown/renderBlock.js +7 -10
  130. package/dist/model.d.ts +1 -1
  131. package/dist/paged-layout/rangeProjection.js +6 -9
  132. package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
  133. package/dist/paged-layout/transactionDirtyRange.js +23 -1
  134. package/dist/prosemirror/attrs/index.d.ts +7 -3
  135. package/dist/prosemirror/attrs/index.js +128 -7
  136. package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
  137. package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
  138. package/dist/prosemirror/commands/comments.d.ts +10 -1
  139. package/dist/prosemirror/commands/comments.js +551 -87
  140. package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
  141. package/dist/prosemirror/commands/formatPainter.js +319 -63
  142. package/dist/prosemirror/commands/index.d.ts +2 -2
  143. package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
  144. package/dist/prosemirror/commands/pageBreak.js +18 -36
  145. package/dist/prosemirror/commands/propertyChangeScope.d.ts +27 -3
  146. package/dist/prosemirror/commands/propertyChangeScope.js +85 -14
  147. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -5
  148. package/dist/prosemirror/conversion/fromProseDoc.js +935 -223
  149. package/dist/prosemirror/conversion/runShadingMark.js +2 -2
  150. package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
  151. package/dist/prosemirror/conversion/toProseDoc.js +439 -291
  152. package/dist/prosemirror/extensions/StarterKit.js +4 -0
  153. package/dist/prosemirror/extensions/core/ParagraphExtension.js +54 -6
  154. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -1
  155. package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
  156. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +19 -6
  157. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
  158. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
  159. package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
  160. package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
  161. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
  162. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
  163. package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
  164. package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
  165. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
  166. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
  167. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
  168. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
  169. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  170. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
  171. package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
  172. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
  173. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  174. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
  175. package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
  176. package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
  177. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
  178. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
  179. package/dist/prosemirror/findReplaceSelection.js +8 -3
  180. package/dist/prosemirror/index.d.ts +3 -3
  181. package/dist/prosemirror/listMarker.d.ts +26 -1
  182. package/dist/prosemirror/listMarker.js +108 -14
  183. package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
  184. package/dist/prosemirror/pageBreakRunProjection.js +47 -0
  185. package/dist/prosemirror/paraText.js +6 -0
  186. package/dist/prosemirror/paragraphAlignment.d.ts +12 -0
  187. package/dist/prosemirror/paragraphAlignment.js +13 -0
  188. package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
  189. package/dist/prosemirror/paragraphSpacing.js +125 -0
  190. package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
  191. package/dist/prosemirror/plugins/documentNumbering.js +18 -0
  192. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
  193. package/dist/prosemirror/plugins/pmTextScan.js +14 -4
  194. package/dist/prosemirror/plugins/suggestionMode.js +9 -11
  195. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
  196. package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
  197. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
  198. package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
  199. package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
  200. package/dist/prosemirror/runFormattingProvenance.js +48 -0
  201. package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
  202. package/dist/prosemirror/runFormattingReconciliation.js +54 -0
  203. package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
  204. package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
  205. package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
  206. package/dist/prosemirror/runStyleFormatting.js +105 -0
  207. package/dist/prosemirror/schema/index.d.ts +3 -3
  208. package/dist/prosemirror/schema/marks.d.ts +84 -9
  209. package/dist/prosemirror/schema/marks.js +73 -1
  210. package/dist/prosemirror/schema/nodes.d.ts +30 -6
  211. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
  212. package/dist/prosemirror/styles/resolvedStyleAttrs.js +45 -23
  213. package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
  214. package/dist/prosemirror/styles/styleResolver.js +3 -3
  215. package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
  216. package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
  217. package/dist/prosemirror/tableGridMutation.d.ts +20 -0
  218. package/dist/prosemirror/tableGridMutation.js +30 -0
  219. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
  220. package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
  221. package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
  222. package/dist/prosemirror/validation.js +23 -4
  223. package/dist/redline.js +4 -1
  224. package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
  225. package/dist/render-dom/RenderedDomContext.js +17 -34
  226. package/dist/server.d.ts +4 -4
  227. package/dist/server.js +2 -2
  228. package/dist/style-engine/styleEngine.d.ts +1 -1
  229. package/dist/utils/clipboard.js +1 -1
  230. package/dist/utils/findReplace.js +38 -21
  231. package/dist/utils/mergeDocumentContent.js +3 -3
  232. package/dist/utils/replaceText.js +2 -1
  233. package/dist/version-comparison.d.ts +38 -44
  234. package/dist/version-comparison.js +270 -399
  235. package/dist/watermark/index.js +2 -4
  236. package/package.json +8 -2
@@ -1,25 +1,85 @@
1
- import { expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
2
- import { PPR_CHANGE_SCOPED_ATTR_KEYS } 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
+ 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";
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";
7
15
  import { isZeroWidthAnchor } from "../prosemirror/zeroWidthAnchors.js";
8
16
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
9
17
  import { stripBlockIdentityAttrs } from "./block-identity.js";
10
- import { buildCleanBlockText } from "./clean-text.js";
18
+ import { buildCleanBlockText, resolveCleanTextRange } from "./clean-text.js";
11
19
  import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers } from "./inline-emphasis.js";
12
- import { hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
20
+ import { hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
13
21
  import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
14
22
  import { planTableMutations } from "./table-mutation-plan.js";
15
23
  import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
16
24
  import { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell } from "./table-targets.js";
17
25
  import { tableFromTemplate } from "./table-template.js";
18
- import { diffWordSegments } from "./word-diff.js";
26
+ import { wordDiffSessionFromOptions } from "./word-diff.js";
19
27
  import { panic } from "better-result";
28
+ import { Mark } from "prosemirror-model";
20
29
  import { TableMap } from "prosemirror-tables";
21
30
  import { canJoin, canSplit } from "prosemirror-transform";
22
31
  //#region src/ai-edits/apply.ts
32
+ const coarseWordDiff = (before, after) => {
33
+ if (before === after) return before.length === 0 ? [] : [{
34
+ type: "equal",
35
+ text: before
36
+ }];
37
+ if (before.length === 0) return [{
38
+ type: "ins",
39
+ text: after
40
+ }];
41
+ if (after.length === 0) return [{
42
+ type: "del",
43
+ text: before
44
+ }];
45
+ return [{
46
+ type: "del",
47
+ text: before
48
+ }, {
49
+ type: "ins",
50
+ text: after
51
+ }];
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
+ };
23
83
  /**
24
84
  * Operation types applied in `"suggested"` mode. Every produced revision — an
25
85
  * inline mark, a whole-node `_suggestedInsert` marker, or a suggested
@@ -31,6 +91,7 @@ const SUGGESTED_SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
31
91
  "replaceInBlock",
32
92
  "replaceRange",
33
93
  "formatRange",
94
+ "setBlockParagraphProperties",
34
95
  "replaceBlock",
35
96
  "deleteBlock",
36
97
  "insertAfterBlock",
@@ -41,47 +102,104 @@ const SUGGESTED_SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
41
102
  "insertTableColumn",
42
103
  "deleteTableColumn"
43
104
  ]);
44
- /**
45
- * The attrs that render a list label, cleared together whenever a paragraph
46
- * stops being a list item. `w:numPr` alone is not enough: the marker attrs the
47
- * editor caches would keep drawing a label on a paragraph that is no longer in
48
- * the list. One frozen record rather than a list repeated at each call site,
49
- * because a list repeated is a list that drifts.
50
- */
51
- const CLEARED_LIST_MARKER_ATTRS = Object.freeze({
52
- listMarker: null,
53
- listMarkerHidden: null,
54
- listLevelNumFmts: null,
55
- listLevelStarts: null,
56
- listAbstractNumId: null,
57
- listStartOverride: null
58
- });
59
- /**
60
- * The attrs one `setBlockParagraphProperties` writes, or `null` when the
61
- * block already holds them. `styleId: null` clears the style; `listLevel`
62
- * moves `w:numPr/w:ilvl` and leaves `w:numId` alone, because a demoted item
63
- * stays in the same list; `listLevel: null` drops `w:numPr` entirely, which
64
- * is a paragraph that stopped being a list item.
65
- */
66
- const paragraphPropertiesPatch = (node, properties) => {
105
+ const REPLACE_BLOCK_IMPACT = {
106
+ text: {
107
+ changesStyle: false,
108
+ changesText: true
109
+ },
110
+ style: {
111
+ changesStyle: true,
112
+ changesText: false
113
+ },
114
+ textAndStyle: {
115
+ changesStyle: true,
116
+ changesText: true
117
+ }
118
+ };
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) => item.operation.type === "setBlockParagraphProperties" || isResolvedReplaceBlockOperation(item) && REPLACE_BLOCK_IMPACT[item.replaceBlockImpact].changesStyle;
139
+ const resolveReplaceBlockImpact = ({ changesText, changesStyle }) => {
140
+ if (changesText) return changesStyle ? "textAndStyle" : "text";
141
+ if (changesStyle) return "style";
142
+ return panic("Cannot resolve a replaceBlock that changes neither text nor style");
143
+ };
144
+ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyle, numbering = null }) => {
145
+ const attrs = expectParagraphAttrs(node);
146
+ const currentDirectAlignment = directParagraphAlignment(attrs);
147
+ const currentDirectSpacing = directParagraphSpacing(attrs);
148
+ const resolvedSpacingFromStyle = paragraphSpacingFromFormatting(resolvedFormattingFromStyle);
67
149
  const patch = {};
68
- if (properties.styleId !== void 0 && (node.attrs["styleId"] ?? null) !== properties.styleId) patch["styleId"] = properties.styleId;
150
+ let originalFormatting = attrs._originalFormatting === void 0 || attrs._originalFormatting === null ? void 0 : { ...attrs._originalFormatting };
151
+ let originalFormattingChanged = false;
152
+ const nextStyleId = properties.styleId;
153
+ const styleChanged = nextStyleId !== void 0 && (attrs.styleId ?? null) !== nextStyleId;
154
+ if (styleChanged) {
155
+ patch["styleId"] = nextStyleId;
156
+ patch["alignmentFromStyle"] = resolvedFormattingFromStyle?.alignment;
157
+ if (properties.alignment === void 0) patch["alignment"] = currentDirectAlignment ?? resolvedFormattingFromStyle?.alignment ?? null;
158
+ if (properties.spacing === void 0) Object.assign(patch, paragraphSpacingAttrPatch({
159
+ direct: currentDirectSpacing,
160
+ inherited: resolvedSpacingFromStyle
161
+ }));
162
+ originalFormatting ??= {};
163
+ if (nextStyleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
164
+ else originalFormatting.styleId = nextStyleId;
165
+ if (currentDirectAlignment !== void 0) originalFormatting.alignment = currentDirectAlignment;
166
+ originalFormattingChanged = true;
167
+ }
69
168
  if (properties.listLevel !== void 0) {
70
169
  const numPr = node.attrs["numPr"];
71
- const current = typeof numPr === "object" && numPr !== null && "ilvl" in numPr ? numPr.ilvl : void 0;
72
170
  if (properties.listLevel === null) {
73
- if (numPr !== null && numPr !== void 0) {
74
- patch["numPr"] = null;
75
- Object.assign(patch, CLEARED_LIST_MARKER_ATTRS);
76
- }
77
- } else if (current !== properties.listLevel) {
171
+ patch["numPr"] = null;
172
+ Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
173
+ } else {
78
174
  const numId = typeof numPr === "object" && numPr !== null && "numId" in numPr ? numPr.numId : void 0;
79
- patch["numPr"] = {
80
- ...typeof numId === "number" && { numId },
175
+ if (typeof numId === "number") Object.assign(patch, listLevelAttrPatch(attrs, {
176
+ numId,
81
177
  ilvl: properties.listLevel
82
- };
178
+ }, numbering));
179
+ else {
180
+ patch["numPr"] = { ilvl: properties.listLevel };
181
+ Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
182
+ }
83
183
  }
84
184
  }
185
+ if (properties.alignment !== void 0 && (styleChanged || (currentDirectAlignment ?? null) !== properties.alignment)) {
186
+ originalFormatting ??= {};
187
+ if (properties.alignment === null) Reflect.deleteProperty(originalFormatting, "alignment");
188
+ else originalFormatting.alignment = properties.alignment;
189
+ const alignmentFromStyle = properties.styleId === void 0 ? attrs.alignmentFromStyle : resolvedFormattingFromStyle?.alignment;
190
+ patch["alignment"] = properties.alignment ?? alignmentFromStyle ?? null;
191
+ originalFormattingChanged = true;
192
+ }
193
+ if (properties.spacing !== void 0 && (styleChanged || !paragraphSpacingEqual(currentDirectSpacing, properties.spacing))) {
194
+ const directSpacing = properties.spacing ?? void 0;
195
+ originalFormatting = withDirectParagraphSpacing(originalFormatting, directSpacing);
196
+ Object.assign(patch, paragraphSpacingAttrPatch({
197
+ direct: directSpacing,
198
+ inherited: resolvedSpacingFromStyle
199
+ }));
200
+ originalFormattingChanged = true;
201
+ }
202
+ if (originalFormattingChanged) patch["_originalFormatting"] = originalFormatting && Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
85
203
  return Object.keys(patch).length > 0 ? patch : null;
86
204
  };
87
205
  const REVISION_MARK_NAMES = /* @__PURE__ */ new Set(["insertion", "deletion"]);
@@ -132,163 +250,195 @@ const undeletedContentAtomRanges = (tr, blockFrom) => {
132
250
  });
133
251
  return ranges;
134
252
  };
135
- /** The in-scope paragraph properties as they stand, for a `w:pPrChange` record. */
136
- const paragraphPropertiesSnapshot = (node) => {
137
- const snapshot = {};
138
- for (const key of PPR_CHANGE_SCOPED_ATTR_KEYS) {
139
- const value = node.attrs[key];
140
- if (value !== null && value !== void 0) snapshot[key] = value;
141
- }
142
- return snapshot;
253
+ /** The carrier pPr before this batch changed it, for final-mark rotation. */
254
+ const paragraphPropertiesBeforeBatch = (node, batchRevisionIds) => {
255
+ const propertyChanges = expectParagraphAttrs(node)._propertyChanges;
256
+ if (!Array.isArray(propertyChanges)) return paragraphPropertiesSnapshot(node);
257
+ const earliestBatchChange = propertyChanges.find(({ info }) => batchRevisionIds.has(info.id));
258
+ if (!earliestBatchChange) return paragraphPropertiesSnapshot(node);
259
+ return earliestBatchChange.previousFormatting ?? {};
260
+ };
261
+ const resolveFormattingFromStyle = ({ attrs, styleId, styleResolver }) => {
262
+ const resolved = styleResolver?.resolveParagraphStyle(styleId).paragraphFormatting;
263
+ if (resolved !== void 0) return resolved;
264
+ if ((styleId ?? void 0) !== (attrs.styleId ?? void 0)) return;
265
+ const fallback = {};
266
+ if (attrs.alignmentFromStyle !== void 0) fallback.alignment = attrs.alignmentFromStyle;
267
+ return Object.keys(fallback).length > 0 ? fallback : void 0;
143
268
  };
144
- const applyReplaceBlockStyleId = ({ item, tr }) => {
145
- if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return tr;
146
- const block = tr.doc.nodeAt(item.blockFrom);
147
- if (!block) return tr;
148
- return tr.setNodeMarkup(item.blockFrom, void 0, {
149
- ...block.attrs,
150
- styleId: item.operation.styleId
269
+ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) => {
270
+ if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return {
271
+ tr,
272
+ revisionId: null
273
+ };
274
+ const blockPosition = tr.mapping.map(item.blockFrom, -1);
275
+ const block = tr.doc.nodeAt(blockPosition);
276
+ if (!block) return {
277
+ tr,
278
+ revisionId: null
279
+ };
280
+ const attrs = expectParagraphAttrs(block);
281
+ const resolvedFormattingFromStyle = resolveFormattingFromStyle({
282
+ attrs,
283
+ styleId: item.operation.styleId,
284
+ styleResolver
285
+ });
286
+ const patch = paragraphPropertiesPatch({
287
+ node: block,
288
+ properties: { styleId: item.operation.styleId },
289
+ resolvedFormattingFromStyle
151
290
  });
291
+ if (patch === null) return {
292
+ tr,
293
+ revisionId: null
294
+ };
295
+ const existing = attrs._propertyChanges;
296
+ const change = revisionInfo ? {
297
+ type: "paragraphPropertyChange",
298
+ info: revisionInfo,
299
+ previousFormatting: paragraphPropertiesSnapshot(block)
300
+ } : null;
301
+ return {
302
+ tr: tr.setNodeMarkup(blockPosition, void 0, {
303
+ ...block.attrs,
304
+ ...patch,
305
+ ...change ? { _propertyChanges: [...Array.isArray(existing) ? existing : [], change] } : {}
306
+ }),
307
+ revisionId: change?.info.id ?? null
308
+ };
152
309
  };
153
310
  const REPLACEMENT_BACKGROUND_CLEAR_FORMATTING = {
154
311
  highlight: false,
155
312
  runShading: false
156
313
  };
157
- const INLINE_FORMATTING_MARK_NAMES = {
158
- bold: "bold",
159
- italic: "italic",
160
- underline: "underline",
161
- strike: "strike",
162
- fontFamily: "fontFamily",
163
- fontSizePt: "fontSize",
164
- color: "textColor",
165
- highlight: "highlight",
166
- runShading: "runShading"
167
- };
168
- const DIRECT_FONT_PROPERTIES = [
169
- "fontFamily",
170
- "fontSize",
171
- "color"
172
- ];
173
- const formattingMarkName = (property) => {
174
- if (!Object.hasOwn(INLINE_FORMATTING_MARK_NAMES, property)) return null;
175
- const name = Reflect.get(INLINE_FORMATTING_MARK_NAMES, property);
176
- return typeof name === "string" ? name : null;
177
- };
178
- const formattingMarkAttrs = (property, value) => {
179
- switch (property) {
180
- case "underline": return { style: "single" };
181
- case "fontFamily": return typeof value === "string" ? {
182
- ascii: value,
183
- hAnsi: value
184
- } : null;
185
- case "fontSizePt": return typeof value === "number" ? { size: value * 2 } : null;
186
- case "color": return typeof value === "string" ? { rgb: value.replace(/^#/u, "").toUpperCase() } : null;
187
- default: return {};
314
+ const includedFormattingProperty = (includedProperties, property) => includedProperties === void 0 || includedProperties.includes(property);
315
+ const patchAuthoredRunFormatting = ({ authoredFormatting, formatting, includedProperties }) => {
316
+ const next = { ...authoredFormatting };
317
+ for (const property of ["bold", "italic"]) {
318
+ const value = formatting[property];
319
+ if (value === void 0 || !includedFormattingProperty(includedProperties, property)) continue;
320
+ if (value === null) Reflect.deleteProperty(next, property);
321
+ else next[property] = value;
188
322
  }
189
- };
190
- const applyInlineFormatting = ({ tr, schema, from, to, formatting, includedProperties }) => {
191
- for (const [property, value] of Object.entries(formatting)) {
192
- if (includedProperties && !includedProperties.includes(property)) continue;
193
- const markName = formattingMarkName(property);
194
- const markType = markName ? schema.marks[markName] : void 0;
195
- if (!markType) continue;
196
- if (value !== false && value !== null) {
197
- const attrs = formattingMarkAttrs(property, value);
198
- if (attrs) tr.addMark(from, to, markType.create(attrs));
199
- continue;
200
- }
201
- tr.removeMark(from, to, markType);
323
+ if (formatting.underline !== void 0 && includedFormattingProperty(includedProperties, "underline")) if (formatting.underline === null) delete next.underline;
324
+ else next.underline = { style: formatting.underline ? "single" : "none" };
325
+ if (formatting.strike !== void 0 && includedFormattingProperty(includedProperties, "strike")) {
326
+ delete next.doubleStrike;
327
+ if (formatting.strike === null) delete next.strike;
328
+ else next.strike = formatting.strike;
202
329
  }
203
- applyDirectFontProvenance({
204
- tr,
205
- schema,
330
+ if (formatting.fontFamily !== void 0 && includedFormattingProperty(includedProperties, "fontFamily")) if (formatting.fontFamily === null) delete next.fontFamily;
331
+ else next.fontFamily = {
332
+ ascii: formatting.fontFamily,
333
+ hAnsi: formatting.fontFamily
334
+ };
335
+ if (formatting.fontSizePt !== void 0 && includedFormattingProperty(includedProperties, "fontSizePt")) if (formatting.fontSizePt === null) delete next.fontSize;
336
+ else next.fontSize = formatting.fontSizePt * 2;
337
+ if (formatting.color !== void 0 && includedFormattingProperty(includedProperties, "color")) if (formatting.color === null) delete next.color;
338
+ else next.color = { rgb: formatting.color.replace(/^#/u, "").toUpperCase() };
339
+ if (formatting.highlight === false && includedFormattingProperty(includedProperties, "highlight")) delete next.highlight;
340
+ if (formatting.runShading === false && includedFormattingProperty(includedProperties, "runShading")) delete next.shading;
341
+ return next;
342
+ };
343
+ const applyInlineFormatting = ({ tr, from, to, formatting, includedProperties, styleResolver }) => {
344
+ const representations = selectRunFormattingCarrierRepresentations({
345
+ doc: tr.doc,
206
346
  from,
207
- to,
208
- formatting,
209
- ...includedProperties ? { includedProperties } : {}
347
+ to
210
348
  });
349
+ for (const representation of representations) {
350
+ const styleContext = paragraphRunStyleContextAt({
351
+ doc: tr.doc,
352
+ pos: representation.from,
353
+ ...styleResolver !== void 0 ? { styleResolver } : {}
354
+ });
355
+ const authoredFormatting = readAuthoredRunFormatting({
356
+ context: styleContext,
357
+ marks: representation.node.marks,
358
+ ...styleResolver !== void 0 ? { styleResolver } : {}
359
+ });
360
+ const marks = reconcileRunFormattingMarks({
361
+ authoredFormatting: patchAuthoredRunFormatting({
362
+ authoredFormatting,
363
+ formatting,
364
+ ...includedProperties !== void 0 ? { includedProperties } : {}
365
+ }),
366
+ context: styleContext,
367
+ node: representation.node,
368
+ ...styleResolver !== void 0 ? { styleResolver } : {}
369
+ });
370
+ applyMarksToRunFormattingRepresentation({
371
+ tr,
372
+ representation,
373
+ marks
374
+ });
375
+ }
211
376
  return tr;
212
377
  };
213
- const applyDirectFontProvenance = ({ tr, schema, from, to, formatting, includedProperties }) => {
214
- if ("highlight" in formatting) return;
215
- const changed = [
216
- [
217
- "fontFamily",
218
- "fontFamily",
219
- formatting.fontFamily
220
- ],
221
- [
222
- "fontSize",
223
- "fontSizePt",
224
- formatting.fontSizePt
225
- ],
226
- [
227
- "color",
228
- "color",
229
- formatting.color
230
- ]
231
- ].filter(([, inputProperty, value]) => value !== void 0 && (!includedProperties || includedProperties.includes(inputProperty)));
232
- const markType = schema.marks["runFormattingOverride"];
233
- if (!markType || changed.length === 0) return;
234
- tr.doc.nodesBetween(from, to, (node, pos) => {
235
- if (!node.isText) return;
236
- const segmentFrom = Math.max(from, pos);
237
- const segmentTo = Math.min(to, pos + node.nodeSize);
238
- const existing = node.marks.find((mark) => mark.type === markType);
239
- const attrs = existing ? expectRunFormattingOverrideMarkAttrs(existing) : {};
240
- const directFontProperties = new Set(attrs.directFontProperties);
241
- for (const [property, , value] of changed) if (value === null) directFontProperties.delete(property);
242
- else directFontProperties.add(property);
243
- tr.removeMark(segmentFrom, segmentTo, markType);
244
- const nextAttrs = {
245
- ...attrs,
246
- directFontProperties: DIRECT_FONT_PROPERTIES.filter((property) => directFontProperties.has(property))
247
- };
248
- 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));
249
- });
250
- };
251
- const directFontPropertyForFormattingProperty = (property) => {
252
- switch (property) {
253
- case "fontFamily": return "fontFamily";
254
- case "fontSizePt": return "fontSize";
255
- case "color": return "color";
256
- default: return null;
378
+ const applyMarksToRunFormattingRepresentation = ({ tr, representation, marks }) => {
379
+ const { node, position, from, to } = representation;
380
+ if (Mark.sameSet(node.marks, marks)) return;
381
+ if (!node.isText) {
382
+ tr.setNodeMarkup(position, void 0, node.attrs, marks);
383
+ return;
257
384
  }
385
+ for (const current of node.marks) if (!marks.some((candidate) => candidate.eq(current))) tr.removeMark(from, to, current.type);
386
+ for (const next of marks) if (!node.marks.some((candidate) => candidate.eq(next))) tr.addMark(from, to, next);
258
387
  };
259
- const formattingPropertyWouldChange = (marks, property, value) => {
260
- const markName = formattingMarkName(property);
261
- const mark = marks.find((candidate) => candidate.type.name === markName);
262
- const directFontProperty = directFontPropertyForFormattingProperty(property);
263
- if (directFontProperty) {
264
- const overrideMark = marks.find((candidate) => candidate.type.name === "runFormattingOverride");
265
- const isDirect = overrideMark ? (expectRunFormattingOverrideMarkAttrs(overrideMark).directFontProperties ?? []).includes(directFontProperty) : false;
266
- if (value === false || value === null) return isDirect;
267
- if (!isDirect) return true;
268
- } else if (value === false || value === null) return mark !== void 0;
269
- if (property === "underline") return mark?.attrs["style"] !== "single";
270
- if (property === "fontFamily") return mark?.attrs["ascii"] !== value || mark?.attrs["hAnsi"] !== value;
271
- if (property === "fontSizePt") return Number(mark?.attrs["size"]) !== Number(value) * 2;
272
- if (property === "color") {
273
- const current = mark?.attrs["rgb"];
274
- return (typeof current === "string" ? current.replace(/^#/u, "").toUpperCase() : null) !== String(value).replace(/^#/u, "").toUpperCase();
388
+ const sameDefinedFormattingValue = (left, right) => {
389
+ if (left === right) return true;
390
+ if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) return false;
391
+ if (Array.isArray(left) || Array.isArray(right)) {
392
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
393
+ return left.every((value, index) => sameDefinedFormattingValue(value, right.at(index)));
275
394
  }
276
- return mark === void 0;
395
+ const leftEntries = Object.entries(left).filter(([, value]) => value !== void 0);
396
+ const rightEntries = Object.entries(right).filter(([, value]) => value !== void 0);
397
+ if (leftEntries.length !== rightEntries.length) return false;
398
+ return leftEntries.every(([property, value]) => Object.hasOwn(right, property) && sameDefinedFormattingValue(value, Reflect.get(right, property)));
277
399
  };
278
- const formattingChangesForMarks = (marks, formatting) => {
400
+ const formattingChangesForAuthoredFormatting = (authoredFormatting, formatting) => {
279
401
  const changes = [];
280
- for (const [property, value] of Object.entries(formatting)) if (formattingPropertyWouldChange(marks, property, value)) changes.push(property);
402
+ for (const property of [
403
+ "bold",
404
+ "italic",
405
+ "underline",
406
+ "strike",
407
+ "fontFamily",
408
+ "fontSizePt",
409
+ "color",
410
+ "highlight",
411
+ "runShading"
412
+ ]) {
413
+ if (formatting[property] === void 0) continue;
414
+ const next = patchAuthoredRunFormatting({
415
+ authoredFormatting,
416
+ formatting,
417
+ includedProperties: [property]
418
+ });
419
+ if (!sameDefinedFormattingValue(authoredFormatting, next)) changes.push(property);
420
+ }
281
421
  return changes;
282
422
  };
283
- const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, author, date, initials, suggestionId = null }) => {
284
- if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return tr;
285
- if (mode === "direct") return applyInlineFormatting({
286
- tr,
287
- schema,
288
- from,
289
- to,
290
- formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING
291
- });
423
+ const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
424
+ if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return {
425
+ type: "applied",
426
+ transaction: tr,
427
+ revisionIds: [],
428
+ nextRevisionId: revisionIdSeed
429
+ };
430
+ if (mode === "direct") return {
431
+ type: "applied",
432
+ transaction: applyInlineFormatting({
433
+ tr,
434
+ from,
435
+ to,
436
+ formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
437
+ ...styleResolver !== void 0 ? { styleResolver } : {}
438
+ }),
439
+ revisionIds: [],
440
+ nextRevisionId: revisionIdSeed
441
+ };
292
442
  return applyTrackedInlineFormatting({
293
443
  tr,
294
444
  schema,
@@ -296,26 +446,85 @@ const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, au
296
446
  from,
297
447
  to,
298
448
  formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
299
- revisionId,
449
+ revisionIdSeed,
300
450
  author,
301
451
  date,
302
452
  initials,
303
- suggestionId
453
+ suggestionId,
454
+ ...styleResolver !== void 0 ? { styleResolver } : {}
304
455
  });
305
456
  };
306
- const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionId, author, date, initials, suggestionId = null }) => {
457
+ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionIdSeed, author, date, initials, suggestionId = null, styleResolver }) => {
307
458
  const propertyChangeType = schema.marks["runPropertyChange"];
308
- if (!propertyChangeType) return tr;
459
+ if (!propertyChangeType) return {
460
+ type: "applied",
461
+ transaction: tr,
462
+ revisionIds: [],
463
+ nextRevisionId: revisionIdSeed
464
+ };
309
465
  const segments = [];
310
- doc.nodesBetween(from, to, (node, pos) => {
311
- if (!node.isText) return;
312
- const includedProperties = formattingChangesForMarks(node.marks, formatting);
313
- if (includedProperties.length === 0) return;
314
- const segmentFrom = Math.max(from, pos);
315
- const segmentTo = Math.min(to, pos + node.nodeSize);
316
- const previousFormatting = marksToTextFormatting(node.marks);
466
+ let hasPendingRunPropertyChange = false;
467
+ const representations = selectRunFormattingCarrierRepresentations({
468
+ doc,
469
+ from,
470
+ to
471
+ });
472
+ for (const representation of representations) {
473
+ const { node } = representation;
474
+ const styleContext = paragraphRunStyleContextAt({
475
+ doc,
476
+ pos: representation.from,
477
+ ...styleResolver !== void 0 ? { styleResolver } : {}
478
+ });
479
+ const previousFormatting = readAuthoredRunFormatting({
480
+ context: styleContext,
481
+ marks: node.marks,
482
+ ...styleResolver !== void 0 ? { styleResolver } : {}
483
+ });
484
+ const includedProperties = formattingChangesForAuthoredFormatting(previousFormatting, formatting);
485
+ if (includedProperties.length === 0) continue;
317
486
  const existingMark = node.marks.find((mark) => mark.type === propertyChangeType);
318
- const existingChanges = existingMark ? expectRunPropertyChangeMarkAttrs(existingMark).changes : [];
487
+ if (existingMark && expectRunPropertyChangeMarkAttrs(existingMark).changes.length > 0) {
488
+ hasPendingRunPropertyChange = true;
489
+ continue;
490
+ }
491
+ const formattingMarks = reconcileRunFormattingMarks({
492
+ authoredFormatting: patchAuthoredRunFormatting({
493
+ authoredFormatting: previousFormatting,
494
+ formatting,
495
+ includedProperties
496
+ }),
497
+ context: styleContext,
498
+ node,
499
+ ...styleResolver !== void 0 ? { styleResolver } : {}
500
+ });
501
+ segments.push({
502
+ formattingMarks,
503
+ representation,
504
+ includedProperties,
505
+ previousFormatting
506
+ });
507
+ }
508
+ if (hasPendingRunPropertyChange) return {
509
+ type: "pendingRunPropertyChange",
510
+ transaction: tr,
511
+ revisionIds: [],
512
+ nextRevisionId: revisionIdSeed
513
+ };
514
+ if (segments.length === 0) return {
515
+ type: "applied",
516
+ transaction: tr,
517
+ revisionIds: [],
518
+ nextRevisionId: revisionIdSeed
519
+ };
520
+ const suggestionAttrs = suggestionId === null ? {} : {
521
+ provenance: "suggested",
522
+ suggestionId
523
+ };
524
+ const revisionIds = [];
525
+ for (const segment of segments) {
526
+ const revisionId = revisionIdSeed + revisionIds.length;
527
+ revisionIds.push(revisionId);
319
528
  const change = {
320
529
  type: "runPropertyChange",
321
530
  info: {
@@ -324,35 +533,24 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
324
533
  date,
325
534
  ...initials ? { initials } : {}
326
535
  },
327
- ...Object.keys(previousFormatting).length > 0 ? { previousFormatting } : {}
536
+ ...Object.keys(segment.previousFormatting).length > 0 ? { previousFormatting: segment.previousFormatting } : {}
328
537
  };
329
- segments.push({
330
- from: segmentFrom,
331
- to: segmentTo,
332
- includedProperties,
333
- changes: [...existingChanges, change]
334
- });
335
- });
336
- if (segments.length === 0) return tr;
337
- const suggestionAttrs = suggestionId === null ? {} : {
338
- provenance: "suggested",
339
- suggestionId
340
- };
341
- for (const segment of segments) {
342
- applyInlineFormatting({
538
+ const marks = propertyChangeType.create({
539
+ changes: [change],
540
+ ...suggestionAttrs
541
+ }).addToSet(segment.formattingMarks);
542
+ applyMarksToRunFormattingRepresentation({
343
543
  tr,
344
- schema,
345
- from: segment.from,
346
- to: segment.to,
347
- formatting,
348
- includedProperties: segment.includedProperties
544
+ representation: segment.representation,
545
+ marks
349
546
  });
350
- tr.addMark(segment.from, segment.to, propertyChangeType.create({
351
- changes: segment.changes,
352
- ...suggestionAttrs
353
- }));
354
547
  }
355
- return tr;
548
+ return {
549
+ type: "applied",
550
+ transaction: tr,
551
+ revisionIds,
552
+ nextRevisionId: revisionIdSeed + revisionIds.length
553
+ };
356
554
  };
357
555
  /**
358
556
  * Module-scoped monotonic counter for tracked-change revision ids.
@@ -378,15 +576,17 @@ const nextRevisionSeed = (revisionIdCount) => {
378
576
  * `insertAfterBlock` / `insertBeforeBlock` (`text` split on line breaks,
379
577
  * see `splitInsertParagraphTexts`) allocates one id per paragraph in
380
578
  * tracked-changes mode plus one for each paragraph MARK it brings, so it
381
- * needs more than four once split into more than two paragraphs reserving
382
- * less than that would let a later `nextRevisionSeed` call reuse an id this
383
- * operation already stamped on the document.
579
+ * needs more than four once split into more than two paragraphs. Rotating a
580
+ * terminal run can add one paragraph-property revision per inserted paragraph;
581
+ * reserving less than that would let a later `nextRevisionSeed` call reuse an
582
+ * id this operation already stamped on the document.
384
583
  */
385
584
  const REVISION_IDS_PER_OPERATION = 4;
386
- /** Ids one inserted paragraph allocates: its runs, and its paragraph mark. */
387
- const REVISION_IDS_PER_INSERTED_PARAGRAPH = 2;
388
585
  const estimateRevisionIdReservation = (item) => {
389
- if (item.operation.type === "insertAfterBlock" || item.operation.type === "insertBeforeBlock") return Math.max((item.insertTexts?.length ?? 1) * REVISION_IDS_PER_INSERTED_PARAGRAPH, REVISION_IDS_PER_OPERATION);
586
+ if (item.operation.type === "insertAfterBlock" || item.operation.type === "insertBeforeBlock") {
587
+ const paragraphCount = item.insertTexts?.length ?? 1;
588
+ return Math.max(paragraphCount * 3, REVISION_IDS_PER_OPERATION);
589
+ }
390
590
  return REVISION_IDS_PER_OPERATION;
391
591
  };
392
592
  /**
@@ -591,10 +791,18 @@ const buildEmphasisInlineContent = (schema, text, baseMarks) => {
591
791
  };
592
792
  const isBatchableParagraphInsertion = (item, mode) => {
593
793
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return false;
794
+ if (item.commentText !== void 0) return false;
594
795
  const insertTexts = item.insertTexts ?? [""];
595
796
  const isEmptyInsert = insertTexts.length === 1 && insertTexts[0]?.length === 0;
596
797
  return !(mode === "tracked-changes" && item.operation.pageBreakBefore === true && isEmptyInsert);
597
798
  };
799
+ const addedBreakRevisionId = (value) => {
800
+ if (typeof value !== "object" || value === null || !("kind" in value) || !("info" in value)) return null;
801
+ if (value.kind !== "ins" && value.kind !== "moveTo") return null;
802
+ const info = value.info;
803
+ if (typeof info !== "object" || info === null || !("id" in info)) return null;
804
+ return typeof info.id === "number" ? info.id : null;
805
+ };
598
806
  /**
599
807
  * Move an ADDED paragraph break off every paragraph its container ends with.
600
808
  *
@@ -604,11 +812,10 @@ const isBatchableParagraphInsertion = (item, mode) => {
604
812
  * revision neither accepting nor rejecting everything can clear.
605
813
  *
606
814
  * Appending at a container's end therefore rotates exactly as removing from it
607
- * does. The break that was added sits between the paragraph the run was
608
- * appended after and the first appended one, so THAT paragraph's mark is the
609
- * inserted one, each appended paragraph but the last keeps an inserted mark of
610
- * its own, and the paragraph the container now ends with takes the free mark
611
- * the run was appended after. Only which paragraph is left markless changes.
815
+ * does. The first inserted break moves onto the paragraph the run was appended
816
+ * after, every later break moves one paragraph left, and the paragraph the
817
+ * container now ends with takes the free mark. Only which paragraph is left
818
+ * markless changes; the order and ownership of the breaks do not.
612
819
  *
613
820
  * A paragraph's properties live on its mark, so the paragraph that inherits
614
821
  * the free one records the other's as `w:pPrChange` — what a rejection reads
@@ -621,50 +828,96 @@ const isBatchableParagraphInsertion = (item, mode) => {
621
828
  */
622
829
  const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, author, date, initials }) => {
623
830
  const paragraphTypeName = tr.doc.type.schema.nodes["paragraph"]?.name ?? "paragraph";
624
- const rotations = finalParagraphsOf(tr.doc, paragraphTypeName).filter(({ node }) => {
831
+ const rotations = finalParagraphsOf(tr.doc, paragraphTypeName).flatMap(({ node, position }) => {
625
832
  const mark = node.attrs["pPrMark"];
626
- if (typeof mark !== "object" || mark === null || !("kind" in mark)) return false;
627
- if (mark.kind !== "ins" && mark.kind !== "moveTo") return false;
628
- const info = "info" in mark ? mark.info : void 0;
629
- const revisionId = typeof info === "object" && info !== null && "id" in info ? info.id : void 0;
630
- return typeof revisionId === "number" && batchRevisionIds.has(revisionId);
833
+ const revisionId = addedBreakRevisionId(mark);
834
+ return revisionId !== null && batchRevisionIds.has(revisionId) ? [{
835
+ mark,
836
+ node,
837
+ ownerRevisionId: revisionId,
838
+ position
839
+ }] : [];
631
840
  });
632
841
  let next = tr;
633
842
  let nextRevisionId = revisionSeed;
634
- for (const { position: finalPosition, node: final } of rotations) {
635
- const carrier = addedBreakCarrierBefore(next.doc.resolve(finalPosition), final.type.name);
843
+ const synthesizedRevisions = [];
844
+ for (const { position: finalPosition, node: final, mark: finalMark } of rotations) {
845
+ const resolved = next.doc.resolve(finalPosition);
846
+ const carrier = addedBreakCarrierBefore(resolved, final.type.name);
636
847
  if (!carrier) {
637
848
  next = next.setNodeAttribute(finalPosition, "pPrMark", null);
638
849
  continue;
639
850
  }
640
- next = next.setNodeAttribute(carrier.position, "pPrMark", final.attrs["pPrMark"]);
641
- const previousFormatting = paragraphPropertiesSnapshot(carrier.node);
642
- if (JSON.stringify(previousFormatting) === JSON.stringify(paragraphPropertiesSnapshot(final))) {
851
+ const path = [{
852
+ node: final,
853
+ position: finalPosition
854
+ }];
855
+ let position = finalPosition;
856
+ for (let index = resolved.index() - 1; index >= 0; index--) {
857
+ const sibling = resolved.parent.child(index);
858
+ position -= sibling.nodeSize;
859
+ if (sibling.type.name !== final.type.name) continue;
860
+ path.push({
861
+ node: sibling,
862
+ position
863
+ });
864
+ if (position === carrier.position) break;
865
+ }
866
+ if (path.at(-1)?.position !== carrier.position) {
643
867
  next = next.setNodeAttribute(finalPosition, "pPrMark", null);
644
868
  continue;
645
869
  }
646
- const existing = expectParagraphAttrs(final)._propertyChanges;
647
- next = next.setNodeMarkup(finalPosition, void 0, {
648
- ...final.attrs,
649
- pPrMark: null,
650
- _propertyChanges: [...Array.isArray(existing) ? existing : [], {
651
- type: "paragraphPropertyChange",
652
- info: {
653
- id: nextRevisionId++,
654
- author,
655
- date,
656
- ...initials ? { initials } : {}
657
- },
658
- previousFormatting
659
- }]
660
- });
870
+ if (path.slice(0, -1).some(({ node }) => {
871
+ const revisionId = addedBreakRevisionId(node.attrs["pPrMark"]);
872
+ return revisionId === null || !batchRevisionIds.has(revisionId);
873
+ })) continue;
874
+ const previousFormatting = paragraphPropertiesBeforeBatch(carrier.node, batchRevisionIds);
875
+ next = next.setNodeAttribute(finalPosition, "pPrMark", null);
876
+ let carriedMark = finalMark;
877
+ for (let index = 1; index < path.length; index++) {
878
+ const current = path[index - 1];
879
+ const previous = path[index];
880
+ if (!current || !previous) panic("A final-mark rotation exceeded its paragraph path", { index });
881
+ const ownerRevisionId = addedBreakRevisionId(carriedMark);
882
+ if (ownerRevisionId === null || !batchRevisionIds.has(ownerRevisionId)) panic("A final-mark rotation crossed a break outside its operation batch", { ownerRevisionId });
883
+ const displacedMark = previous.node.attrs["pPrMark"];
884
+ next = next.setNodeAttribute(previous.position, "pPrMark", carriedMark);
885
+ const currentFormatting = paragraphPropertiesSnapshot(current.node);
886
+ if (JSON.stringify(previousFormatting) !== JSON.stringify(currentFormatting)) {
887
+ const liveCurrent = next.doc.nodeAt(current.position);
888
+ if (!liveCurrent || liveCurrent.type.name !== final.type.name) panic("A final-mark rotation lost its paragraph", { position: current.position });
889
+ const existing = expectParagraphAttrs(liveCurrent)._propertyChanges;
890
+ if (hasSerializableParagraphPropertyChange(existing)) panic("A final-mark rotation cannot append a second w:pPrChange", { position: current.position });
891
+ const revisionId = nextRevisionId++;
892
+ next = next.setNodeMarkup(current.position, void 0, {
893
+ ...liveCurrent.attrs,
894
+ _propertyChanges: [...Array.isArray(existing) ? existing : [], {
895
+ type: "paragraphPropertyChange",
896
+ info: {
897
+ id: revisionId,
898
+ author,
899
+ date,
900
+ ...initials ? { initials } : {}
901
+ },
902
+ previousFormatting
903
+ }]
904
+ });
905
+ synthesizedRevisions.push({
906
+ ownerRevisionId,
907
+ revisionId
908
+ });
909
+ }
910
+ if (displacedMark == null) break;
911
+ carriedMark = displacedMark;
912
+ }
661
913
  }
662
914
  return {
663
915
  transaction: next,
664
- nextRevisionId
916
+ nextRevisionId,
917
+ synthesizedRevisions
665
918
  };
666
919
  };
667
- const buildInsertedParagraphs = ({ item, schema, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove }) => {
920
+ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove, numbering }) => {
668
921
  const operation = item.operation;
669
922
  if (operation.type !== "insertAfterBlock" && operation.type !== "insertBeforeBlock") panic("Only paragraph insertions can build inserted paragraphs", { type: operation.type });
670
923
  const insertionType = schema.marks["insertion"];
@@ -677,7 +930,12 @@ const buildInsertedParagraphs = ({ item, schema, mode, author, date, initials, c
677
930
  suggestionId
678
931
  } : {}
679
932
  };
680
- const baseAttrs = operation.inheritFormatting === false ? {} : stripBlockIdentityAttrs(item.blockNode.attrs);
933
+ const baseAttrs = operation.inheritFormatting === false ? {} : {
934
+ ...stripBlockIdentityAttrs(item.blockNode.attrs),
935
+ _propertyChanges: null,
936
+ pPrMark: null,
937
+ _suggestedInsert: null
938
+ };
681
939
  const insertTexts = item.insertTexts ?? [""];
682
940
  const revisionIds = [];
683
941
  const nodes = [];
@@ -701,20 +959,51 @@ const buildInsertedParagraphs = ({ item, schema, mode, author, date, initials, c
701
959
  const content = text.length > 0 ? buildEmphasisInlineContent(schema, text, marks) : null;
702
960
  const attrs = isFirstParagraph ? { ...baseAttrs } : {};
703
961
  if (isFirstParagraph && operation.pageBreakBefore === true) attrs["pageBreakBefore"] = true;
704
- if (isFirstParagraph && operation.listLevel === null) {
962
+ const listLevel = operation.listLevel;
963
+ if (isFirstParagraph && listLevel === null) {
705
964
  attrs["numPr"] = null;
706
- Object.assign(attrs, CLEARED_LIST_MARKER_ATTRS);
707
- } else if (isFirstParagraph && operation.listLevel !== void 0) {
708
- const anchorNumPr = baseAttrs["numPr"];
965
+ Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
966
+ } else if (isFirstParagraph && typeof listLevel === "number") {
967
+ const anchorNumPr = Reflect.get(baseAttrs, "numPr");
709
968
  const numId = typeof anchorNumPr === "object" && anchorNumPr !== null && "numId" in anchorNumPr ? anchorNumPr.numId : void 0;
710
- attrs["numPr"] = {
711
- ...typeof numId === "number" && { numId },
712
- ilvl: operation.listLevel
713
- };
969
+ if (typeof numId === "number") Object.assign(attrs, listLevelAttrPatch(operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode), {
970
+ numId,
971
+ ilvl: listLevel
972
+ }, numbering));
973
+ else {
974
+ attrs["numPr"] = { ilvl: listLevel };
975
+ Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
976
+ }
714
977
  }
715
978
  if (isFirstParagraph && operation.styleId !== void 0) {
716
979
  attrs["styleId"] = operation.styleId;
717
- if (operation.inheritFormatting !== false && operation.styleId !== null) Object.assign(attrs, CLEARED_LIST_MARKER_ATTRS);
980
+ if (operation.inheritFormatting !== false && operation.styleId !== null) Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
981
+ }
982
+ if (isFirstParagraph && (operation.styleId !== void 0 || operation.alignment !== void 0 || operation.spacing !== void 0)) {
983
+ const inheritedDirectAlignment = operation.inheritFormatting === false ? void 0 : directParagraphAlignment(expectParagraphAttrs(item.blockNode));
984
+ const inheritedDirectSpacing = operation.inheritFormatting === false ? void 0 : directParagraphSpacing(expectParagraphAttrs(item.blockNode));
985
+ const directAlignment = operation.alignment === void 0 ? inheritedDirectAlignment : operation.alignment ?? void 0;
986
+ const directSpacing = operation.spacing === void 0 ? inheritedDirectSpacing : operation.spacing ?? void 0;
987
+ attrs["alignmentFromStyle"] = formattingFromStyle?.alignment;
988
+ attrs["alignment"] = directAlignment ?? formattingFromStyle?.alignment ?? null;
989
+ const sourceFormatting = attrs["_originalFormatting"];
990
+ let originalFormatting = typeof sourceFormatting === "object" && sourceFormatting !== null ? { ...sourceFormatting } : void 0;
991
+ if (operation.styleId !== void 0) {
992
+ originalFormatting ??= {};
993
+ if (operation.styleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
994
+ else originalFormatting.styleId = operation.styleId;
995
+ }
996
+ if (operation.alignment !== void 0 || inheritedDirectAlignment !== void 0) {
997
+ originalFormatting ??= {};
998
+ if (directAlignment === void 0) Reflect.deleteProperty(originalFormatting, "alignment");
999
+ else originalFormatting.alignment = directAlignment;
1000
+ }
1001
+ if (operation.spacing !== void 0) originalFormatting = withDirectParagraphSpacing(originalFormatting, directSpacing);
1002
+ if (operation.styleId !== void 0 || operation.spacing !== void 0) Object.assign(attrs, paragraphSpacingAttrPatch({
1003
+ direct: directSpacing,
1004
+ inherited: paragraphSpacingFromFormatting(formattingFromStyle)
1005
+ }));
1006
+ attrs["_originalFormatting"] = originalFormatting && Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
718
1007
  }
719
1008
  if (isSuggested && suggestionId !== null && paragraphRevisionId !== null) attrs["_suggestedInsert"] = {
720
1009
  suggestionId,
@@ -747,7 +1036,7 @@ const buildInsertedParagraphs = ({ item, schema, mode, author, date, initials, c
747
1036
  nextRevisionId
748
1037
  };
749
1038
  };
750
- const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, tableTemplates }) => {
1039
+ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates }) => {
751
1040
  const applied = [];
752
1041
  const skipped = [];
753
1042
  const normalizations = [];
@@ -755,8 +1044,45 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
755
1044
  const insertionType = view.state.schema.marks["insertion"];
756
1045
  const deletionType = view.state.schema.marks["deletion"];
757
1046
  const commentType = view.state.schema.marks["comment"];
1047
+ const occupiedCommentIds = /* @__PURE__ */ new Set();
1048
+ if (commentType) view.state.doc.descendants((node) => {
1049
+ for (const mark of node.marks) {
1050
+ if (mark.type !== commentType) continue;
1051
+ const commentId = mark.attrs["commentId"];
1052
+ if (typeof commentId === "number") occupiedCommentIds.add(commentId);
1053
+ }
1054
+ });
1055
+ let provisionalCommentIdCursor = Number.MIN_SAFE_INTEGER;
1056
+ const reserveProvisionalCommentId = () => {
1057
+ while (occupiedCommentIds.has(provisionalCommentIdCursor)) provisionalCommentIdCursor += 1;
1058
+ const id = provisionalCommentIdCursor;
1059
+ provisionalCommentIdCursor += 1;
1060
+ occupiedCommentIds.add(id);
1061
+ return id;
1062
+ };
1063
+ const allocateCommittedCommentId = (text) => {
1064
+ if (createCommentId === void 0) panic("A resolved comment operation lost its comment-id allocator");
1065
+ const id = createCommentId(text);
1066
+ if (!Number.isSafeInteger(id) || occupiedCommentIds.has(id)) panic("A comment-id allocator returned an invalid or duplicate id", { id });
1067
+ occupiedCommentIds.add(id);
1068
+ return id;
1069
+ };
1070
+ const styleResolver = getDocumentStyleResolver(view.state);
1071
+ const numbering = getDocumentNumbering(view.state);
1072
+ const formattingFromStyleForInsertion = (item) => {
1073
+ if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return;
1074
+ const anchorAttrs = expectParagraphAttrs(item.blockNode);
1075
+ const inheritedStyleId = item.operation.inheritFormatting === false ? null : anchorAttrs.styleId;
1076
+ const styleId = item.operation.styleId !== void 0 ? item.operation.styleId : inheritedStyleId;
1077
+ return resolveFormattingFromStyle({
1078
+ attrs: anchorAttrs,
1079
+ styleId,
1080
+ styleResolver
1081
+ });
1082
+ };
758
1083
  const claimedTableRows = /* @__PURE__ */ new Set();
759
1084
  const claimedTableColumns = /* @__PURE__ */ new Set();
1085
+ const diffText = wordDiffMode === "coarse" ? coarseWordDiff : wordDiffSessionFromOptions(wordDiff).diff;
760
1086
  const producesTrackedChanges = mode !== "direct";
761
1087
  const isSuggested = mode === "suggested";
762
1088
  if (producesTrackedChanges && (!insertionType || !deletionType)) return {
@@ -792,6 +1118,13 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
792
1118
  });
793
1119
  continue;
794
1120
  }
1121
+ if (commentText !== void 0 && !hasRepresentableCommentAnchor(resolution.operation, mode)) {
1122
+ skipped.push({
1123
+ id: operation.id,
1124
+ reason: "unsupportedBlock"
1125
+ });
1126
+ continue;
1127
+ }
795
1128
  const deletion = resolution.operation.tableRowDeletion;
796
1129
  if (deletion) {
797
1130
  const rowKey = `${deletion.tablePosition}:${deletion.rowIndex}`;
@@ -821,11 +1154,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
821
1154
  code: "splitMultilineText",
822
1155
  paragraphCount: resolution.operation.insertTexts?.length ?? 1
823
1156
  });
824
- const commentId = commentText !== void 0 ? createCommentId?.(commentText) : void 0;
825
1157
  resolved.push({
826
1158
  ...resolution.operation,
827
1159
  originalIndex: index,
828
- ...commentId !== void 0 && { commentId }
1160
+ ...commentText !== void 0 && { commentText }
829
1161
  });
830
1162
  }
831
1163
  const tablePlan = planTableMutations(resolved.map((item) => ({
@@ -868,6 +1200,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
868
1200
  }
869
1201
  let tr = view.state.tr;
870
1202
  const revisionIdReservation = executableResolved.reduce((total, item) => total + estimateRevisionIdReservation(item), 0);
1203
+ const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
871
1204
  let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionSeed(revisionIdReservation);
872
1205
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
873
1206
  const insertedColumnCounts = /* @__PURE__ */ new Map();
@@ -899,6 +1232,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
899
1232
  for (let executionIndex = 0; executionIndex < executionOrder.length; executionIndex++) {
900
1233
  const item = executionOrder[executionIndex];
901
1234
  if (!item) panic("The operation execution index exceeded the resolved plan", { executionIndex });
1235
+ if (mode === "tracked-changes" && writesParagraphPropertyChange(item)) {
1236
+ const livePosition = tr.mapping.map(item.blockFrom);
1237
+ const liveBlock = tr.doc.nodeAt(livePosition) ?? item.blockNode;
1238
+ if (hasSerializableParagraphPropertyChange(liveBlock.type.name === "paragraph" ? expectParagraphAttrs(liveBlock)._propertyChanges : void 0)) {
1239
+ skipped.push({
1240
+ id: item.operation.id,
1241
+ reason: "pendingParagraphPropertyChange"
1242
+ });
1243
+ continue;
1244
+ }
1245
+ }
902
1246
  if (isBatchableParagraphInsertion(item, mode)) {
903
1247
  const insertionRun = [item];
904
1248
  for (let lookahead = executionIndex + 1; lookahead < executionOrder.length; lookahead++) {
@@ -911,24 +1255,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
911
1255
  const runApplied = [];
912
1256
  for (const insertion of insertionRun) {
913
1257
  const insertionSuggestionId = isSuggested ? insertion.operation.suggestionId ?? insertion.operation.id : null;
914
- const insertionCommentMark = insertion.commentId !== void 0 && commentType ? commentType.create({ commentId: insertion.commentId }) : null;
915
1258
  const built = buildInsertedParagraphs({
916
1259
  item: insertion,
917
1260
  schema: view.state.schema,
1261
+ formattingFromStyle: formattingFromStyleForInsertion(insertion),
918
1262
  mode,
919
1263
  author,
920
1264
  date,
921
1265
  initials,
922
- commentMark: insertionCommentMark,
1266
+ commentMark: null,
923
1267
  suggestionId: insertionSuggestionId,
924
1268
  revisionSeed,
925
- isPairedMove
1269
+ isPairedMove,
1270
+ numbering
926
1271
  });
927
1272
  revisionSeed = built.nextRevisionId;
928
1273
  nodeGroups.push(built.nodes);
929
1274
  runApplied.push({
930
1275
  id: insertion.operation.id,
931
- ...insertion.commentId !== void 0 && { commentId: insertion.commentId },
932
1276
  ...built.revisionIds[0] !== void 0 && {
933
1277
  revisionId: built.revisionIds[0],
934
1278
  revisionIds: built.revisionIds
@@ -944,7 +1288,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
944
1288
  continue;
945
1289
  }
946
1290
  }
947
- const commentMark = item.commentId !== void 0 && commentType ? commentType.create({ commentId: item.commentId }) : null;
1291
+ const provisionalCommentId = item.commentText !== void 0 ? reserveProvisionalCommentId() : void 0;
1292
+ const commentMark = provisionalCommentId !== void 0 && commentType ? commentType.create({ commentId: provisionalCommentId }) : null;
948
1293
  const stepsBefore = tr.steps.length;
949
1294
  let appliedRevisionIds;
950
1295
  if (isSuggested && !SUGGESTED_SUPPORTED_OPERATION_TYPES.has(item.operation.type)) {
@@ -962,8 +1307,9 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
962
1307
  suggestionId
963
1308
  } : {}
964
1309
  };
1310
+ let operationRevisionSeed = revisionSeed;
965
1311
  const structuralRevision = producesTrackedChanges ? {
966
- revisionId: revisionSeed,
1312
+ revisionId: operationRevisionSeed,
967
1313
  author,
968
1314
  date,
969
1315
  ...trackedRevisionExtras
@@ -971,22 +1317,32 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
971
1317
  switch (item.operation.type) {
972
1318
  case "replaceInBlock":
973
1319
  case "replaceRange": {
974
- const revisionIdDelete = revisionSeed++;
975
- const revisionIdInsert = revisionSeed++;
976
- const revisionIdBackground = revisionSeed++;
1320
+ const revisionIdDelete = operationRevisionSeed;
1321
+ const revisionIdInsert = producesTrackedChanges ? operationRevisionSeed + 1 : operationRevisionSeed;
1322
+ const revisionIdBackgroundSeed = producesTrackedChanges ? operationRevisionSeed + 2 : operationRevisionSeed;
977
1323
  const stepsBeforeBackgroundClear = tr.steps.length;
978
- tr = clearReplacementBackground({
1324
+ const backgroundResult = clearReplacementBackground({
979
1325
  tr,
980
1326
  schema: view.state.schema,
981
1327
  from: item.from,
982
1328
  to: item.to,
983
1329
  mode,
984
- revisionId: revisionIdBackground,
1330
+ revisionIdSeed: revisionIdBackgroundSeed,
985
1331
  author,
986
1332
  date,
987
1333
  initials,
988
- suggestionId
1334
+ suggestionId,
1335
+ styleResolver
989
1336
  });
1337
+ if (backgroundResult.type === "pendingRunPropertyChange") {
1338
+ skipped.push({
1339
+ id: item.operation.id,
1340
+ reason: backgroundResult.type
1341
+ });
1342
+ continue;
1343
+ }
1344
+ tr = backgroundResult.transaction;
1345
+ operationRevisionSeed = backgroundResult.nextRevisionId;
990
1346
  const clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
991
1347
  tr = applyTextReplacement({
992
1348
  tr,
@@ -999,12 +1355,12 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
999
1355
  commentMark,
1000
1356
  suggestionId,
1001
1357
  initials,
1002
- granularity: wordDiff?.granularity ?? "word"
1358
+ diffText
1003
1359
  });
1004
1360
  if (producesTrackedChanges) appliedRevisionIds = [
1005
1361
  revisionIdDelete,
1006
1362
  revisionIdInsert,
1007
- ...clearedBackground ? [revisionIdBackground] : []
1363
+ ...clearedBackground ? backgroundResult.revisionIds : []
1008
1364
  ];
1009
1365
  break;
1010
1366
  }
@@ -1013,89 +1369,131 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1013
1369
  break;
1014
1370
  case "formatRange":
1015
1371
  if (producesTrackedChanges) {
1016
- const revisionId = revisionSeed++;
1017
- tr = applyTrackedInlineFormatting({
1372
+ const formattingResult = applyTrackedInlineFormatting({
1018
1373
  tr,
1019
1374
  schema: view.state.schema,
1020
1375
  doc: tr.doc,
1021
1376
  from: item.from,
1022
1377
  to: item.to,
1023
1378
  formatting: item.operation.formatting,
1024
- revisionId,
1379
+ revisionIdSeed: operationRevisionSeed,
1025
1380
  author,
1026
1381
  date,
1027
1382
  initials,
1028
- suggestionId
1383
+ suggestionId,
1384
+ styleResolver
1029
1385
  });
1030
- appliedRevisionIds = [revisionId];
1386
+ if (formattingResult.type === "pendingRunPropertyChange") {
1387
+ skipped.push({
1388
+ id: item.operation.id,
1389
+ reason: formattingResult.type
1390
+ });
1391
+ continue;
1392
+ }
1393
+ tr = formattingResult.transaction;
1394
+ operationRevisionSeed = formattingResult.nextRevisionId;
1395
+ appliedRevisionIds = [...formattingResult.revisionIds];
1031
1396
  break;
1032
1397
  }
1033
1398
  tr = applyInlineFormatting({
1034
1399
  tr,
1035
- schema: view.state.schema,
1036
1400
  from: item.from,
1037
1401
  to: item.to,
1038
- formatting: item.operation.formatting
1402
+ formatting: item.operation.formatting,
1403
+ ...styleResolver !== void 0 ? { styleResolver } : {}
1039
1404
  });
1040
1405
  break;
1041
1406
  case "replaceBlock": {
1042
- const revisionIdDelete = revisionSeed++;
1043
- const revisionIdInsert = revisionSeed++;
1044
- const revisionIdBackground = revisionSeed++;
1407
+ if (!isResolvedReplaceBlockOperation(item)) panic("Resolved replaceBlock operation lost its impact discriminator");
1408
+ const { changesStyle, changesText } = REPLACE_BLOCK_IMPACT[item.replaceBlockImpact];
1409
+ const revisionIdDelete = operationRevisionSeed;
1410
+ const revisionIdInsert = changesText ? operationRevisionSeed + 1 : operationRevisionSeed;
1411
+ const revisionIdBackgroundSeed = changesText ? operationRevisionSeed + 2 : operationRevisionSeed;
1412
+ if (producesTrackedChanges && changesText) operationRevisionSeed = revisionIdBackgroundSeed;
1045
1413
  if (item.operation.preserveFormatting === false && mode === "direct") {
1046
1414
  const replacement = item.operation.text;
1047
1415
  const paragraphType = view.state.schema.nodes["paragraph"];
1048
1416
  if (paragraphType) {
1049
- const replaceAttrs = item.operation.styleId !== void 0 ? { styleId: item.operation.styleId } : null;
1050
- const node = paragraphType.create(replaceAttrs, replacement.length === 0 ? null : buildEmphasisInlineContent(view.state.schema, replacement, []));
1417
+ const node = paragraphType.create(null, replacement.length === 0 ? null : buildEmphasisInlineContent(view.state.schema, replacement, commentMark ? [commentMark] : []));
1051
1418
  tr = tr.replaceWith(item.blockFrom, item.blockTo, node);
1419
+ tr = applyReplaceBlockStyleId({
1420
+ item,
1421
+ tr,
1422
+ styleResolver
1423
+ }).tr;
1052
1424
  break;
1053
1425
  }
1054
1426
  }
1055
1427
  if (mode === "direct" && hasInlineEmphasis(item.operation.text)) {
1056
- const attrs = item.operation.styleId !== void 0 ? {
1057
- ...item.blockNode.attrs,
1058
- styleId: item.operation.styleId
1059
- } : { ...item.blockNode.attrs };
1060
- const node = item.blockNode.type.create(attrs, buildEmphasisInlineContent(view.state.schema, item.operation.text, commentMark ? [commentMark] : []));
1428
+ const node = item.blockNode.type.create({ ...item.blockNode.attrs }, buildEmphasisInlineContent(view.state.schema, item.operation.text, commentMark ? [commentMark] : []));
1061
1429
  tr = tr.replaceWith(item.blockFrom, item.blockTo, node);
1430
+ tr = applyReplaceBlockStyleId({
1431
+ item,
1432
+ tr,
1433
+ styleResolver
1434
+ }).tr;
1062
1435
  break;
1063
1436
  }
1064
- const stepsBeforeBackgroundClear = tr.steps.length;
1065
- tr = clearReplacementBackground({
1066
- tr,
1067
- schema: view.state.schema,
1068
- from: item.from,
1069
- to: item.to,
1070
- mode,
1071
- revisionId: revisionIdBackground,
1072
- author,
1073
- date,
1074
- initials,
1075
- suggestionId
1076
- });
1077
- const clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
1078
- tr = applyTextReplacement({
1079
- tr,
1080
- item,
1081
- mode,
1082
- author,
1083
- date,
1084
- revisionIdDelete,
1085
- revisionIdInsert,
1086
- commentMark,
1087
- suggestionId,
1088
- initials,
1089
- granularity: wordDiff?.granularity ?? "word"
1090
- });
1091
- tr = applyReplaceBlockStyleId({
1437
+ let clearedBackground = false;
1438
+ let backgroundRevisionIds = [];
1439
+ if (changesText) {
1440
+ const stepsBeforeBackgroundClear = tr.steps.length;
1441
+ const backgroundResult = clearReplacementBackground({
1442
+ tr,
1443
+ schema: view.state.schema,
1444
+ from: item.from,
1445
+ to: item.to,
1446
+ mode,
1447
+ revisionIdSeed: revisionIdBackgroundSeed,
1448
+ author,
1449
+ date,
1450
+ initials,
1451
+ suggestionId,
1452
+ styleResolver
1453
+ });
1454
+ if (backgroundResult.type === "pendingRunPropertyChange") {
1455
+ skipped.push({
1456
+ id: item.operation.id,
1457
+ reason: backgroundResult.type
1458
+ });
1459
+ continue;
1460
+ }
1461
+ tr = backgroundResult.transaction;
1462
+ operationRevisionSeed = backgroundResult.nextRevisionId;
1463
+ clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
1464
+ backgroundRevisionIds = backgroundResult.revisionIds;
1465
+ tr = applyTextReplacement({
1466
+ tr,
1467
+ item,
1468
+ mode,
1469
+ author,
1470
+ date,
1471
+ revisionIdDelete,
1472
+ revisionIdInsert,
1473
+ commentMark,
1474
+ suggestionId,
1475
+ initials,
1476
+ diffText
1477
+ });
1478
+ }
1479
+ const revisionIdParagraph = producesTrackedChanges && changesStyle ? operationRevisionSeed++ : null;
1480
+ const styleResult = applyReplaceBlockStyleId({
1092
1481
  item,
1093
- tr
1482
+ tr,
1483
+ styleResolver,
1484
+ ...revisionIdParagraph !== null ? { revisionInfo: {
1485
+ id: revisionIdParagraph,
1486
+ author,
1487
+ date,
1488
+ ...trackedRevisionExtras
1489
+ } } : {}
1094
1490
  });
1491
+ tr = styleResult.tr;
1492
+ if (!changesText && commentMark) tr = tr.addMark(item.from, item.to, commentMark);
1095
1493
  if (producesTrackedChanges) appliedRevisionIds = [
1096
- revisionIdDelete,
1097
- revisionIdInsert,
1098
- ...clearedBackground ? [revisionIdBackground] : []
1494
+ ...changesText ? [revisionIdDelete, revisionIdInsert] : [],
1495
+ ...clearedBackground ? backgroundRevisionIds : [],
1496
+ ...styleResult.revisionId === null ? [] : [styleResult.revisionId]
1099
1497
  ];
1100
1498
  break;
1101
1499
  }
@@ -1113,16 +1511,18 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1113
1511
  const built = buildInsertedParagraphs({
1114
1512
  item,
1115
1513
  schema: view.state.schema,
1514
+ formattingFromStyle: formattingFromStyleForInsertion(item),
1116
1515
  mode,
1117
1516
  author,
1118
1517
  date,
1119
1518
  initials,
1120
1519
  commentMark,
1121
1520
  suggestionId,
1122
- revisionSeed,
1123
- isPairedMove
1521
+ revisionSeed: operationRevisionSeed,
1522
+ isPairedMove,
1523
+ numbering
1124
1524
  });
1125
- revisionSeed = built.nextRevisionId;
1525
+ operationRevisionSeed = built.nextRevisionId;
1126
1526
  if (built.revisionIds.length > 0) appliedRevisionIds = built.revisionIds;
1127
1527
  tr = tr.insert(item.from, built.nodes);
1128
1528
  break;
@@ -1148,7 +1548,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1148
1548
  }
1149
1549
  let node = signatureTable;
1150
1550
  if (isSuggested && suggestionId !== null) {
1151
- const revisionId = revisionSeed++;
1551
+ const revisionId = operationRevisionSeed++;
1152
1552
  node = signatureTable.type.create({
1153
1553
  ...signatureTable.attrs,
1154
1554
  _suggestedInsert: {
@@ -1191,7 +1591,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1191
1591
  }
1192
1592
  tr = result.transaction;
1193
1593
  if (result.revisionId !== null) {
1194
- revisionSeed++;
1594
+ operationRevisionSeed++;
1195
1595
  appliedRevisionIds = [result.revisionId];
1196
1596
  }
1197
1597
  break;
@@ -1221,7 +1621,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1221
1621
  }
1222
1622
  tr = result.transaction;
1223
1623
  if (result.revisionId !== null) {
1224
- revisionSeed++;
1624
+ operationRevisionSeed++;
1225
1625
  appliedRevisionIds = [result.revisionId];
1226
1626
  }
1227
1627
  const columnKey = getTableColumnCoordinateKey(insertion);
@@ -1254,7 +1654,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1254
1654
  }
1255
1655
  tr = result.transaction;
1256
1656
  if (result.revisionId !== null) {
1257
- revisionSeed++;
1657
+ operationRevisionSeed++;
1258
1658
  appliedRevisionIds = [result.revisionId];
1259
1659
  }
1260
1660
  break;
@@ -1270,7 +1670,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1270
1670
  });
1271
1671
  continue;
1272
1672
  }
1273
- const revisionId = mode === "tracked-changes" ? revisionSeed : null;
1673
+ const revisionId = mode === "tracked-changes" ? operationRevisionSeed : null;
1274
1674
  const nextTr = revisionId === null ? mergeTableRectangle({
1275
1675
  tr,
1276
1676
  tablePosition,
@@ -1294,7 +1694,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1294
1694
  }
1295
1695
  tr = nextTr;
1296
1696
  if (revisionId !== null) {
1297
- revisionSeed++;
1697
+ operationRevisionSeed++;
1298
1698
  appliedRevisionIds = [revisionId];
1299
1699
  }
1300
1700
  break;
@@ -1310,7 +1710,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1310
1710
  });
1311
1711
  continue;
1312
1712
  }
1313
- const revisionId = mode === "tracked-changes" ? revisionSeed : null;
1713
+ const revisionId = mode === "tracked-changes" ? operationRevisionSeed : null;
1314
1714
  const nextTr = revisionId === null ? splitTableRectangle({
1315
1715
  tr,
1316
1716
  tablePosition,
@@ -1334,7 +1734,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1334
1734
  }
1335
1735
  tr = nextTr;
1336
1736
  if (revisionId !== null) {
1337
- revisionSeed++;
1737
+ operationRevisionSeed++;
1338
1738
  appliedRevisionIds = [revisionId];
1339
1739
  }
1340
1740
  break;
@@ -1362,7 +1762,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1362
1762
  }
1363
1763
  tr = result.transaction;
1364
1764
  if (result.revisionId !== null) {
1365
- revisionSeed++;
1765
+ operationRevisionSeed++;
1366
1766
  appliedRevisionIds = [result.revisionId];
1367
1767
  }
1368
1768
  break;
@@ -1374,7 +1774,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1374
1774
  break;
1375
1775
  }
1376
1776
  if (deletionType) {
1377
- const revisionId = revisionSeed++;
1777
+ const revisionId = operationRevisionSeed++;
1378
1778
  const deletionMark = deletionType.create({
1379
1779
  revisionId,
1380
1780
  author,
@@ -1389,7 +1789,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1389
1789
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
1390
1790
  const markPosition = tr.mapping.map(item.blockFrom);
1391
1791
  if (!paragraphEndsItsContainer(tr.doc.resolve(markPosition), item.blockNode.type.name) && tr.doc.nodeAt(markPosition)?.attrs["pPrMark"] == null) {
1392
- const markRevisionId = revisionSeed++;
1792
+ const markRevisionId = operationRevisionSeed++;
1393
1793
  tr = tr.setNodeAttribute(markPosition, "pPrMark", {
1394
1794
  kind: isPairedMove(item.operation.moveId) ? "moveFrom" : "del",
1395
1795
  info: {
@@ -1406,7 +1806,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1406
1806
  break;
1407
1807
  case "insertTable": {
1408
1808
  const revision = {
1409
- revisionId: revisionSeed,
1809
+ revisionId: operationRevisionSeed,
1410
1810
  author,
1411
1811
  date,
1412
1812
  ...trackedRevisionExtras
@@ -1428,7 +1828,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1428
1828
  });
1429
1829
  continue;
1430
1830
  }
1431
- if (producesTrackedChanges) appliedRevisionIds = [revisionSeed++];
1831
+ if (producesTrackedChanges) appliedRevisionIds = [operationRevisionSeed++];
1432
1832
  tr = tr.insert(item.from, table);
1433
1833
  markStructuralChange(tr);
1434
1834
  break;
@@ -1466,7 +1866,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1466
1866
  continue;
1467
1867
  }
1468
1868
  const revision = {
1469
- revisionId: revisionSeed++,
1869
+ revisionId: operationRevisionSeed++,
1470
1870
  author,
1471
1871
  date,
1472
1872
  ...trackedRevisionExtras
@@ -1500,7 +1900,22 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1500
1900
  case "setBlockParagraphProperties": {
1501
1901
  const blockPosition = tr.mapping.map(item.blockFrom);
1502
1902
  const liveBlock = tr.doc.nodeAt(blockPosition) ?? item.blockNode;
1503
- const patch = paragraphPropertiesPatch(liveBlock, item.operation.properties);
1903
+ const liveAttrs = expectParagraphAttrs(liveBlock);
1904
+ const resolvedFormattingFromStyle = item.operation.properties.styleId === void 0 ? resolveFormattingFromStyle({
1905
+ attrs: liveAttrs,
1906
+ styleId: liveAttrs.styleId,
1907
+ styleResolver
1908
+ }) : resolveFormattingFromStyle({
1909
+ attrs: liveAttrs,
1910
+ styleId: item.operation.properties.styleId,
1911
+ styleResolver
1912
+ });
1913
+ const patch = paragraphPropertiesPatch({
1914
+ node: liveBlock,
1915
+ properties: item.operation.properties,
1916
+ resolvedFormattingFromStyle,
1917
+ numbering
1918
+ });
1504
1919
  if (patch === null) {
1505
1920
  skipped.push({
1506
1921
  id: item.operation.id,
@@ -1515,14 +1930,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1515
1930
  });
1516
1931
  break;
1517
1932
  }
1518
- const revisionId = revisionSeed++;
1933
+ const revisionId = operationRevisionSeed++;
1519
1934
  const change = {
1520
1935
  type: "paragraphPropertyChange",
1521
1936
  info: {
1522
1937
  id: revisionId,
1523
1938
  author,
1524
1939
  date,
1525
- ...initials ? { initials } : {}
1940
+ ...trackedRevisionExtras
1526
1941
  },
1527
1942
  previousFormatting: paragraphPropertiesSnapshot(liveBlock)
1528
1943
  };
@@ -1541,7 +1956,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1541
1956
  tr = tr.split(item.from);
1542
1957
  break;
1543
1958
  }
1544
- const revisionIdMark = revisionSeed++;
1959
+ const revisionIdMark = operationRevisionSeed++;
1545
1960
  const info = {
1546
1961
  id: revisionIdMark,
1547
1962
  author,
@@ -1550,7 +1965,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1550
1965
  };
1551
1966
  appliedRevisionIds = [revisionIdMark];
1552
1967
  if (item.to > item.from && deletionType) {
1553
- const revisionIdSeparator = revisionSeed++;
1968
+ const revisionIdSeparator = operationRevisionSeed++;
1554
1969
  tr = tr.addMark(item.from, item.to, deletionType.create({
1555
1970
  revisionId: revisionIdSeparator,
1556
1971
  author,
@@ -1574,10 +1989,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1574
1989
  tr = tr.join(item.blockTo + separator.length);
1575
1990
  break;
1576
1991
  }
1577
- const revisionIdMark = revisionSeed++;
1992
+ const revisionIdMark = operationRevisionSeed++;
1578
1993
  appliedRevisionIds = [revisionIdMark];
1579
1994
  if (separator.length > 0 && insertionType) {
1580
- const revisionIdSeparator = revisionSeed++;
1995
+ const revisionIdSeparator = operationRevisionSeed++;
1581
1996
  tr = tr.insertText(separator, insertAt);
1582
1997
  tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
1583
1998
  revisionId: revisionIdSeparator,
@@ -1610,9 +2025,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1610
2025
  });
1611
2026
  continue;
1612
2027
  }
2028
+ let committedCommentId;
2029
+ if (item.commentText !== void 0) {
2030
+ 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", {
2031
+ operationId: item.operation.id,
2032
+ operationType: item.operation.type
2033
+ });
2034
+ committedCommentId = allocateCommittedCommentId(item.commentText);
2035
+ tr = commitProvisionalComment({
2036
+ tr,
2037
+ commentType,
2038
+ provisionalId: provisionalCommentId,
2039
+ committedId: committedCommentId
2040
+ });
2041
+ }
2042
+ revisionSeed = operationRevisionSeed;
1613
2043
  applied.push({
1614
2044
  id: item.operation.id,
1615
- ...item.commentId !== void 0 && { commentId: item.commentId },
2045
+ ...committedCommentId !== void 0 && { commentId: committedCommentId },
1616
2046
  ...appliedRevisionIds !== void 0 && appliedRevisionIds[0] !== void 0 && {
1617
2047
  revisionId: appliedRevisionIds[0],
1618
2048
  revisionIds: appliedRevisionIds
@@ -1633,6 +2063,21 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1633
2063
  });
1634
2064
  tr = rotated.transaction;
1635
2065
  revisionSeed = rotated.nextRevisionId;
2066
+ const receiptIndexByRevisionId = /* @__PURE__ */ new Map();
2067
+ for (const [receiptIndex, receipt] of applied.entries()) for (const revisionId of receipt.revisionIds ?? []) receiptIndexByRevisionId.set(revisionId, receiptIndex);
2068
+ for (const { ownerRevisionId, revisionId } of rotated.synthesizedRevisions) {
2069
+ const receiptIndex = receiptIndexByRevisionId.get(ownerRevisionId);
2070
+ const receipt = receiptIndex === void 0 ? void 0 : applied.at(receiptIndex);
2071
+ if (receiptIndex === void 0 || !receipt?.revisionIds) panic("A synthesized final-mark revision lost its operation receipt", {
2072
+ ownerRevisionId,
2073
+ revisionId
2074
+ });
2075
+ applied[receiptIndex] = {
2076
+ ...receipt,
2077
+ revisionIds: [...receipt.revisionIds, revisionId]
2078
+ };
2079
+ }
2080
+ if (ownsSharedRevisionIdCursor) revisionIdCursor = Math.max(revisionIdCursor, revisionSeed);
1636
2081
  if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
1637
2082
  view.dispatch(tr);
1638
2083
  }
@@ -1657,7 +2102,8 @@ const previewFolioAIEditOperations = (options) => {
1657
2102
  ...applyOptions,
1658
2103
  view: previewView,
1659
2104
  ...createCommentId !== void 0 && { createCommentId: () => previewCommentId-- },
1660
- revisionIdSeed: -1e9
2105
+ revisionIdSeed: -1e9,
2106
+ wordDiffMode: "coarse"
1661
2107
  });
1662
2108
  return {
1663
2109
  applied: result.applied.map(({ id }) => ({ id })),
@@ -1666,7 +2112,7 @@ const previewFolioAIEditOperations = (options) => {
1666
2112
  nextRevisionId: options.revisionStamp?.idSeed ?? revisionIdCursor
1667
2113
  };
1668
2114
  };
1669
- const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark, suggestionId = null, initials, granularity }) => {
2115
+ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark, suggestionId = null, initials, diffText }) => {
1670
2116
  let nextTr = tr;
1671
2117
  const replacement = stripInlineEmphasisMarkers((() => {
1672
2118
  if (item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") return item.operation.replace;
@@ -1720,7 +2166,7 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
1720
2166
  }
1721
2167
  }
1722
2168
  if (sourceText !== null && cleanBlock !== null) {
1723
- const segments = diffWordSegments(sourceText, replacement, { granularity });
2169
+ const segments = diffText(sourceText, replacement);
1724
2170
  const offsets = cleanBlock.offsets;
1725
2171
  const offsetAt = (cleanOffset) => offsets[cleanOffset] ?? null;
1726
2172
  const steps = [];
@@ -1813,7 +2259,8 @@ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId
1813
2259
  const cleanBlock = buildCleanBlockText(live.node, live.from);
1814
2260
  const currentText = cleanBlock.text;
1815
2261
  const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText));
1816
- if (currentTextHash !== anchor.textHash) return {
2262
+ const structuralBoundaryHash = hashFolioAIBlockStructuralBoundaries(cleanBlock);
2263
+ if (currentTextHash !== anchor.textHash || structuralBoundaryHash !== anchor.structuralBoundaryHash) return {
1817
2264
  type: "skip",
1818
2265
  reason: "changedBlock"
1819
2266
  };
@@ -1843,9 +2290,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
1843
2290
  };
1844
2291
  if (operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange") {
1845
2292
  const { startOffset, endOffset, selectedTextHash } = operation.range;
1846
- const from = cleanBlock.offsets[startOffset];
1847
- const to = cleanBlock.offsets[endOffset];
1848
- if (from === void 0 || to === void 0) return {
2293
+ if (cleanBlock.offsets[startOffset] === void 0 || cleanBlock.offsets[endOffset] === void 0) return {
1849
2294
  type: "skip",
1850
2295
  reason: "staleRange"
1851
2296
  };
@@ -1854,6 +2299,15 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
1854
2299
  type: "skip",
1855
2300
  reason: "staleRange"
1856
2301
  };
2302
+ const range = resolveCleanTextRange({
2303
+ cleanBlock,
2304
+ startOffset,
2305
+ endOffset
2306
+ });
2307
+ if (range === null) return {
2308
+ type: "skip",
2309
+ reason: "unsupportedBlock"
2310
+ };
1857
2311
  if (operation.type === "replaceRange" && selectedText === operation.replace) return {
1858
2312
  type: "skip",
1859
2313
  reason: "noopOperation"
@@ -1862,8 +2316,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
1862
2316
  type: "resolved",
1863
2317
  operation: {
1864
2318
  operation,
1865
- from,
1866
- to,
2319
+ from: range.from,
2320
+ to: range.to,
1867
2321
  blockFrom,
1868
2322
  blockTo,
1869
2323
  blockNode
@@ -2166,8 +2620,10 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2166
2620
  };
2167
2621
  if (operation.type === "splitBlock") {
2168
2622
  const separator = operation.separator ?? "";
2169
- const at = cleanBlock.offsets[operation.offset];
2170
- const after = cleanBlock.offsets[operation.offset + separator.length];
2623
+ const startOffset = operation.offset;
2624
+ const endOffset = operation.offset + separator.length;
2625
+ const at = cleanBlock.offsets[startOffset];
2626
+ const after = cleanBlock.offsets[endOffset];
2171
2627
  if (at === void 0 || after === void 0 || operation.offset === 0 || operation.offset + separator.length >= currentText.length) return {
2172
2628
  type: "skip",
2173
2629
  reason: "staleRange"
@@ -2176,7 +2632,12 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2176
2632
  type: "skip",
2177
2633
  reason: "staleRange"
2178
2634
  };
2179
- if (!canSplit(doc, at)) return {
2635
+ const range = resolveCleanTextRange({
2636
+ cleanBlock,
2637
+ startOffset,
2638
+ endOffset
2639
+ });
2640
+ if (range === null || !canSplit(doc, range.from)) return {
2180
2641
  type: "skip",
2181
2642
  reason: "unsupportedBlock"
2182
2643
  };
@@ -2184,8 +2645,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2184
2645
  type: "resolved",
2185
2646
  operation: {
2186
2647
  operation,
2187
- from: at,
2188
- to: after,
2648
+ from: range.from,
2649
+ to: range.to,
2189
2650
  blockFrom,
2190
2651
  blockTo,
2191
2652
  blockNode
@@ -2210,6 +2671,12 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2210
2671
  };
2211
2672
  }
2212
2673
  if (operation.type === "deleteBlock" || operation.type === "replaceBlock") {
2674
+ const replaceChangesText = operation.type === "replaceBlock" && operation.text !== currentText;
2675
+ const replaceChangesStyle = operation.type === "replaceBlock" && operation.styleId !== void 0 && operation.styleId !== (expectParagraphAttrs(blockNode).styleId ?? null);
2676
+ if (replaceChangesText && cleanBlock.structuralBoundaries.length > 0) return {
2677
+ type: "skip",
2678
+ reason: "unsupportedBlock"
2679
+ };
2213
2680
  const range = getTextRangeFromCleanBlock(cleanBlock);
2214
2681
  if (!range) {
2215
2682
  const insertionPoint = cleanBlock.offsets.at(0);
@@ -2217,10 +2684,25 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2217
2684
  type: "skip",
2218
2685
  reason: "unsupportedBlock"
2219
2686
  };
2220
- if (!(operation.type === "deleteBlock" ? true : currentText.length === 0 && operation.text.length > 0)) return {
2687
+ if (!(operation.type === "deleteBlock" ? true : currentText.length === 0 && (operation.text.length > 0 || replaceChangesStyle))) return {
2221
2688
  type: "skip",
2222
2689
  reason: "unsupportedBlock"
2223
2690
  };
2691
+ if (operation.type === "replaceBlock") return {
2692
+ type: "resolved",
2693
+ operation: {
2694
+ operation,
2695
+ from: insertionPoint,
2696
+ to: insertionPoint,
2697
+ blockFrom,
2698
+ blockTo,
2699
+ blockNode,
2700
+ replaceBlockImpact: resolveReplaceBlockImpact({
2701
+ changesText: replaceChangesText,
2702
+ changesStyle: replaceChangesStyle
2703
+ })
2704
+ }
2705
+ };
2224
2706
  return {
2225
2707
  type: "resolved",
2226
2708
  operation: {
@@ -2233,9 +2715,26 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2233
2715
  }
2234
2716
  };
2235
2717
  }
2236
- if (operation.type === "replaceBlock" && operation.text === currentText) return {
2237
- type: "skip",
2238
- reason: "noopOperation"
2718
+ if (operation.type === "replaceBlock" && operation.text === currentText) {
2719
+ if (!replaceChangesStyle) return {
2720
+ type: "skip",
2721
+ reason: "noopOperation"
2722
+ };
2723
+ }
2724
+ if (operation.type === "replaceBlock") return {
2725
+ type: "resolved",
2726
+ operation: {
2727
+ operation,
2728
+ from: range.from,
2729
+ to: range.to,
2730
+ blockFrom,
2731
+ blockTo,
2732
+ blockNode,
2733
+ replaceBlockImpact: resolveReplaceBlockImpact({
2734
+ changesText: replaceChangesText,
2735
+ changesStyle: replaceChangesStyle
2736
+ })
2737
+ }
2239
2738
  };
2240
2739
  return {
2241
2740
  type: "resolved",
@@ -2273,7 +2772,7 @@ const resolveTextInCleanBlock = (cleanBlock, find) => {
2273
2772
  type: "skip",
2274
2773
  reason: "emptyOperation"
2275
2774
  };
2276
- const { text, offsets } = cleanBlock;
2775
+ const { text } = cleanBlock;
2277
2776
  const firstIndex = text.indexOf(find);
2278
2777
  if (firstIndex === -1) return {
2279
2778
  type: "skip",
@@ -2283,16 +2782,18 @@ const resolveTextInCleanBlock = (cleanBlock, find) => {
2283
2782
  type: "skip",
2284
2783
  reason: "ambiguousFind"
2285
2784
  };
2286
- const from = offsets[firstIndex];
2287
- const to = offsets[firstIndex + find.length];
2288
- if (from === void 0 || to === void 0) return {
2785
+ const range = resolveCleanTextRange({
2786
+ cleanBlock,
2787
+ startOffset: firstIndex,
2788
+ endOffset: firstIndex + find.length
2789
+ });
2790
+ if (range === null) return {
2289
2791
  type: "skip",
2290
2792
  reason: "unsupportedBlock"
2291
2793
  };
2292
2794
  return {
2293
2795
  type: "resolved",
2294
- from,
2295
- to
2796
+ ...range
2296
2797
  };
2297
2798
  };
2298
2799
  const getTextRangeFromCleanBlock = (cleanBlock) => {