@stll/folio-core 0.36.0 → 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 (200) hide show
  1. package/README.md +59 -1
  2. package/dist/ai-edits/apply.js +545 -296
  3. package/dist/ai-edits/block-identity.d.ts +4 -1
  4. package/dist/ai-edits/block-identity.js +4 -1
  5. package/dist/ai-edits/blockRange.js +14 -14
  6. package/dist/ai-edits/clean-text.d.ts +40 -1
  7. package/dist/ai-edits/clean-text.js +52 -2
  8. package/dist/ai-edits/headless.d.ts +3 -1
  9. package/dist/ai-edits/headless.js +133 -71
  10. package/dist/ai-edits/index.d.ts +3 -3
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.js +38 -20
  13. package/dist/ai-edits/snapshot.d.ts +14 -6
  14. package/dist/ai-edits/snapshot.js +99 -19
  15. package/dist/ai-edits/table-cell-mutations.js +1 -1
  16. package/dist/ai-edits/table-row-column-mutations.js +7 -0
  17. package/dist/ai-edits/table-template.d.ts +11 -1
  18. package/dist/ai-edits/table-template.js +49 -17
  19. package/dist/ai-edits/types.d.ts +45 -57
  20. package/dist/ai-suggestions/conflict.js +1 -4
  21. package/dist/ai-suggestions/text-positions.d.ts +8 -0
  22. package/dist/ai-suggestions/text-positions.js +27 -5
  23. package/dist/compare/__fixtures__/body-sequence.js +3 -4
  24. package/dist/compare/column-alignment.d.ts +13 -13
  25. package/dist/compare/column-alignment.js +78 -39
  26. package/dist/compare/compare.d.ts +4 -3
  27. package/dist/compare/compare.js +41 -5
  28. package/dist/compare/content-alignment.d.ts +95 -0
  29. package/dist/compare/content-alignment.js +1450 -0
  30. package/dist/compare/content-types.d.ts +100 -0
  31. package/dist/compare/content-types.js +0 -0
  32. package/dist/compare/content.d.ts +209 -0
  33. package/dist/compare/content.js +885 -0
  34. package/dist/compare/formatting.d.ts +5 -5
  35. package/dist/compare/formatting.js +6 -4
  36. package/dist/compare/plan.d.ts +6 -1
  37. package/dist/compare/plan.js +169 -567
  38. package/dist/compare/scenario.d.ts +2 -2
  39. package/dist/compare/types.d.ts +3 -3
  40. package/dist/compare/verification.d.ts +2 -2
  41. package/dist/compare/verification.js +25 -8
  42. package/dist/compat/eigenpal.d.ts +5 -3
  43. package/dist/compat/eigenpal.js +3 -2
  44. package/dist/controller/headerFooterEditorManager.js +14 -6
  45. package/dist/controller/hiddenEditorManager.d.ts +7 -2
  46. package/dist/controller/hiddenEditorManager.js +15 -6
  47. package/dist/controller/layoutPipeline.js +5 -3
  48. package/dist/controller/noteEditorManager.js +12 -7
  49. package/dist/display-list/build/headerFooterPrimitives.js +1 -0
  50. package/dist/display-list/build/storyPrimitives.d.ts +5 -2
  51. package/dist/display-list/build/storyPrimitives.js +60 -3
  52. package/dist/display-list/build/tablePrimitives.js +30 -0
  53. package/dist/display-list/primitives.d.ts +1 -1
  54. package/dist/document-operations.d.ts +6 -4
  55. package/dist/document-operations.js +77 -12
  56. package/dist/docx/blockContentParser.js +6 -27
  57. package/dist/docx/ensureParaIds.js +9 -4
  58. package/dist/docx/numberingParser.d.ts +3 -1
  59. package/dist/docx/numberingParser.js +4 -1
  60. package/dist/docx/paragraphParser.js +7 -6
  61. package/dist/docx/paragraphTraversal.js +30 -34
  62. package/dist/docx/rezip.d.ts +0 -8
  63. package/dist/docx/rezip.js +41 -16
  64. package/dist/docx/runParser.js +4 -2
  65. package/dist/docx/serializer/paragraphSerializer.js +2 -0
  66. package/dist/docx/serializer/runSerializer.js +5 -3
  67. package/dist/docx/serializer/trackedChangeAttributes.d.ts +3 -1
  68. package/dist/docx/serializer/trackedChangeAttributes.js +11 -1
  69. package/dist/docx/server/build.d.ts +1 -1
  70. package/dist/headless-layout.js +5 -4
  71. package/dist/index.d.ts +5 -3
  72. package/dist/index.js +3 -2
  73. package/dist/internal/headlessRevisionResolution.d.ts +23 -0
  74. package/dist/internal/headlessRevisionResolution.js +155 -0
  75. package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
  76. package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
  77. package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
  78. package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
  79. package/dist/internal/pageBreakRunPartition.d.ts +26 -0
  80. package/dist/internal/pageBreakRunPartition.js +35 -0
  81. package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
  82. package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
  83. package/dist/internal/sectionEndpointResolution.d.ts +32 -0
  84. package/dist/internal/sectionEndpointResolution.js +38 -0
  85. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  86. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
  87. package/dist/layout-bridge/convert/headerFooterLayout.js +48 -5
  88. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
  89. package/dist/layout-bridge/convert/toFlowBlocks.js +379 -75
  90. package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
  91. package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
  92. package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
  93. package/dist/layout-bridge/dom/textStreamDom.js +75 -0
  94. package/dist/layout-bridge/headerFooterLayout.js +16 -30
  95. package/dist/layout-engine/index.js +12 -3
  96. package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
  97. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
  98. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  99. package/dist/layout-engine/types.d.ts +7 -0
  100. package/dist/layout-painter/imageLayout.d.ts +1 -1
  101. package/dist/layout-painter/renderTable.js +20 -0
  102. package/dist/model.d.ts +1 -1
  103. package/dist/paged-layout/rangeProjection.js +6 -9
  104. package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
  105. package/dist/paged-layout/transactionDirtyRange.js +23 -1
  106. package/dist/prosemirror/attrs/index.d.ts +7 -3
  107. package/dist/prosemirror/attrs/index.js +113 -7
  108. package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
  109. package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
  110. package/dist/prosemirror/commands/comments.d.ts +10 -1
  111. package/dist/prosemirror/commands/comments.js +190 -59
  112. package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
  113. package/dist/prosemirror/commands/formatPainter.js +319 -63
  114. package/dist/prosemirror/commands/index.d.ts +2 -2
  115. package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
  116. package/dist/prosemirror/commands/pageBreak.js +18 -36
  117. package/dist/prosemirror/commands/propertyChangeScope.d.ts +7 -5
  118. package/dist/prosemirror/commands/propertyChangeScope.js +27 -18
  119. package/dist/prosemirror/conversion/fromProseDoc.d.ts +7 -2
  120. package/dist/prosemirror/conversion/fromProseDoc.js +809 -209
  121. package/dist/prosemirror/conversion/runShadingMark.js +2 -2
  122. package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
  123. package/dist/prosemirror/conversion/toProseDoc.js +416 -283
  124. package/dist/prosemirror/extensions/StarterKit.js +4 -0
  125. package/dist/prosemirror/extensions/core/ParagraphExtension.js +12 -8
  126. package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
  127. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +1 -1
  128. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
  129. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
  130. package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
  131. package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
  132. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
  133. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
  134. package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
  135. package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
  136. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
  137. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
  138. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
  139. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
  140. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
  141. package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
  142. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
  143. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  144. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
  145. package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
  146. package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
  147. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
  148. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
  149. package/dist/prosemirror/findReplaceSelection.js +8 -3
  150. package/dist/prosemirror/index.d.ts +3 -3
  151. package/dist/prosemirror/listMarker.d.ts +26 -1
  152. package/dist/prosemirror/listMarker.js +108 -14
  153. package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
  154. package/dist/prosemirror/pageBreakRunProjection.js +47 -0
  155. package/dist/prosemirror/paraText.js +6 -0
  156. package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
  157. package/dist/prosemirror/paragraphSpacing.js +125 -0
  158. package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
  159. package/dist/prosemirror/plugins/documentNumbering.js +18 -0
  160. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
  161. package/dist/prosemirror/plugins/pmTextScan.js +14 -4
  162. package/dist/prosemirror/plugins/suggestionMode.js +8 -10
  163. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
  164. package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
  165. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
  166. package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
  167. package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
  168. package/dist/prosemirror/runFormattingProvenance.js +48 -0
  169. package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
  170. package/dist/prosemirror/runFormattingReconciliation.js +54 -0
  171. package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
  172. package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
  173. package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
  174. package/dist/prosemirror/runStyleFormatting.js +105 -0
  175. package/dist/prosemirror/schema/index.d.ts +3 -3
  176. package/dist/prosemirror/schema/marks.d.ts +82 -9
  177. package/dist/prosemirror/schema/marks.js +73 -1
  178. package/dist/prosemirror/schema/nodes.d.ts +18 -5
  179. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
  180. package/dist/prosemirror/styles/resolvedStyleAttrs.js +44 -23
  181. package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
  182. package/dist/prosemirror/styles/styleResolver.js +3 -3
  183. package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
  184. package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
  185. package/dist/prosemirror/tableGridMutation.d.ts +20 -0
  186. package/dist/prosemirror/tableGridMutation.js +30 -0
  187. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
  188. package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
  189. package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
  190. package/dist/prosemirror/validation.js +23 -4
  191. package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
  192. package/dist/render-dom/RenderedDomContext.js +17 -34
  193. package/dist/server.d.ts +4 -4
  194. package/dist/server.js +2 -2
  195. package/dist/style-engine/styleEngine.d.ts +1 -1
  196. package/dist/utils/clipboard.js +1 -1
  197. package/dist/utils/findReplace.js +38 -21
  198. package/dist/version-comparison.d.ts +38 -44
  199. package/dist/version-comparison.js +270 -401
  200. package/package.json +3 -2
@@ -1,17 +1,23 @@
1
- import { expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
2
- import { paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
1
+ import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
2
+ import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
3
3
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
4
- import { marksToTextFormatting } from "../prosemirror/conversion/fromProseDoc.js";
5
4
  import { requestDeterministicParaIds } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
6
5
  import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
6
+ import { CLEARED_LIST_RENDERING_ATTRS } from "../prosemirror/listMarker.js";
7
7
  import { directParagraphAlignment } from "../prosemirror/paragraphAlignment.js";
8
+ import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
9
+ import { getDocumentNumbering } from "../prosemirror/plugins/documentNumbering.js";
8
10
  import { getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
11
+ import { selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
12
+ import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
13
+ import { paragraphRunStyleContextAt } from "../prosemirror/runStyleFormatting.js";
14
+ import { listLevelAttrPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
9
15
  import { isZeroWidthAnchor } from "../prosemirror/zeroWidthAnchors.js";
10
16
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
11
17
  import { stripBlockIdentityAttrs } from "./block-identity.js";
12
- import { buildCleanBlockText } from "./clean-text.js";
18
+ import { buildCleanBlockText, resolveCleanTextRange } from "./clean-text.js";
13
19
  import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers } from "./inline-emphasis.js";
14
- import { hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
20
+ import { hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
15
21
  import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
16
22
  import { planTableMutations } from "./table-mutation-plan.js";
17
23
  import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
@@ -19,6 +25,7 @@ import { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBounda
19
25
  import { tableFromTemplate } from "./table-template.js";
20
26
  import { wordDiffSessionFromOptions } from "./word-diff.js";
21
27
  import { panic } from "better-result";
28
+ import { Mark } from "prosemirror-model";
22
29
  import { TableMap } from "prosemirror-tables";
23
30
  import { canJoin, canSplit } from "prosemirror-transform";
24
31
  //#region src/ai-edits/apply.ts
@@ -43,6 +50,36 @@ const coarseWordDiff = (before, after) => {
43
50
  text: after
44
51
  }];
45
52
  };
53
+ const commentMarkRanges = (doc, commentType, commentId) => {
54
+ const ranges = [];
55
+ doc.descendants((node, position) => {
56
+ for (const mark of node.marks) if (mark.type === commentType && mark.attrs["commentId"] === commentId) ranges.push({
57
+ from: position,
58
+ to: position + node.nodeSize,
59
+ mark
60
+ });
61
+ });
62
+ return ranges;
63
+ };
64
+ /**
65
+ * Replace an operation-local comment id only after its edit has succeeded.
66
+ * The provisional mark lives solely in the undispatched transaction, so an
67
+ * operation that refuses or produces no anchor has no external comment side
68
+ * effect and cannot leave an orphan comments.xml entry behind.
69
+ */
70
+ const commitProvisionalComment = ({ tr, commentType, provisionalId, committedId }) => {
71
+ const ranges = commentMarkRanges(tr.doc, commentType, provisionalId);
72
+ if (ranges.length === 0) panic("A commented operation changed the document without producing a comment anchor");
73
+ if (provisionalId === committedId) return tr;
74
+ for (const { from, to, mark } of ranges) {
75
+ tr = tr.removeMark(from, to, mark);
76
+ tr = tr.addMark(from, to, commentType.create({
77
+ ...mark.attrs,
78
+ commentId: committedId
79
+ }));
80
+ }
81
+ return tr;
82
+ };
46
83
  /**
47
84
  * Operation types applied in `"suggested"` mode. Every produced revision — an
48
85
  * inline mark, a whole-node `_suggestedInsert` marker, or a suggested
@@ -65,21 +102,6 @@ const SUGGESTED_SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
65
102
  "insertTableColumn",
66
103
  "deleteTableColumn"
67
104
  ]);
68
- /**
69
- * The attrs that render a list label, cleared together whenever a paragraph
70
- * stops being a list item. `w:numPr` alone is not enough: the marker attrs the
71
- * editor caches would keep drawing a label on a paragraph that is no longer in
72
- * the list. One frozen record rather than a list repeated at each call site,
73
- * because a list repeated is a list that drifts.
74
- */
75
- const CLEARED_LIST_MARKER_ATTRS = Object.freeze({
76
- listMarker: null,
77
- listMarkerHidden: null,
78
- listLevelNumFmts: null,
79
- listLevelStarts: null,
80
- listAbstractNumId: null,
81
- listStartOverride: null
82
- });
83
105
  const REPLACE_BLOCK_IMPACT = {
84
106
  text: {
85
107
  changesStyle: false,
@@ -95,14 +117,35 @@ const REPLACE_BLOCK_IMPACT = {
95
117
  }
96
118
  };
97
119
  const isResolvedReplaceBlockOperation = (item) => item.operation.type === "replaceBlock";
120
+ const isResolvedBaseReplaceBlockOperation = (item) => item.operation.type === "replaceBlock";
121
+ const hasRepresentableCommentAnchor = (item, mode) => {
122
+ switch (item.operation.type) {
123
+ case "insertAfterBlock":
124
+ case "insertBeforeBlock": return (item.insertTexts ?? [""]).some((text) => stripInlineEmphasisMarkers(text).length > 0);
125
+ case "insertSignatureTable": return false;
126
+ case "replaceInBlock":
127
+ case "replaceRange": return stripInlineEmphasisMarkers(item.operation.replace).length > 0 || mode !== "direct" && item.from < item.to;
128
+ case "replaceBlock": {
129
+ if (!isResolvedBaseReplaceBlockOperation(item)) panic("Resolved replaceBlock operation lost its impact discriminator");
130
+ const { changesText } = REPLACE_BLOCK_IMPACT[item.replaceBlockImpact];
131
+ if (!changesText) return item.from < item.to;
132
+ return stripInlineEmphasisMarkers(item.operation.text).length > 0 || mode !== "direct" && item.from < item.to;
133
+ }
134
+ case "deleteBlock": return mode !== "direct" && item.from < item.to;
135
+ default: return item.from < item.to;
136
+ }
137
+ };
138
+ const writesParagraphPropertyChange = (item) => item.operation.type === "setBlockParagraphProperties" || isResolvedReplaceBlockOperation(item) && REPLACE_BLOCK_IMPACT[item.replaceBlockImpact].changesStyle;
98
139
  const resolveReplaceBlockImpact = ({ changesText, changesStyle }) => {
99
140
  if (changesText) return changesStyle ? "textAndStyle" : "text";
100
141
  if (changesStyle) return "style";
101
142
  return panic("Cannot resolve a replaceBlock that changes neither text nor style");
102
143
  };
103
- const paragraphPropertiesPatch = ({ node, properties, resolvedAlignmentFromStyle }) => {
144
+ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyle, numbering = null }) => {
104
145
  const attrs = expectParagraphAttrs(node);
105
146
  const currentDirectAlignment = directParagraphAlignment(attrs);
147
+ const currentDirectSpacing = directParagraphSpacing(attrs);
148
+ const resolvedSpacingFromStyle = paragraphSpacingFromFormatting(resolvedFormattingFromStyle);
106
149
  const patch = {};
107
150
  let originalFormatting = attrs._originalFormatting === void 0 || attrs._originalFormatting === null ? void 0 : { ...attrs._originalFormatting };
108
151
  let originalFormattingChanged = false;
@@ -110,8 +153,12 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedAlignmentFromStyle
110
153
  const styleChanged = nextStyleId !== void 0 && (attrs.styleId ?? null) !== nextStyleId;
111
154
  if (styleChanged) {
112
155
  patch["styleId"] = nextStyleId;
113
- patch["alignmentFromStyle"] = resolvedAlignmentFromStyle;
114
- if (properties.alignment === void 0) patch["alignment"] = currentDirectAlignment ?? resolvedAlignmentFromStyle ?? null;
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
+ }));
115
162
  originalFormatting ??= {};
116
163
  if (nextStyleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
117
164
  else originalFormatting.styleId = nextStyleId;
@@ -120,29 +167,39 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedAlignmentFromStyle
120
167
  }
121
168
  if (properties.listLevel !== void 0) {
122
169
  const numPr = node.attrs["numPr"];
123
- const current = typeof numPr === "object" && numPr !== null && "ilvl" in numPr ? numPr.ilvl : void 0;
124
170
  if (properties.listLevel === null) {
125
- if (numPr !== null && numPr !== void 0) {
126
- patch["numPr"] = null;
127
- Object.assign(patch, CLEARED_LIST_MARKER_ATTRS);
128
- }
129
- } else if (current !== properties.listLevel) {
171
+ patch["numPr"] = null;
172
+ Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
173
+ } else {
130
174
  const numId = typeof numPr === "object" && numPr !== null && "numId" in numPr ? numPr.numId : void 0;
131
- patch["numPr"] = {
132
- ...typeof numId === "number" && { numId },
175
+ if (typeof numId === "number") Object.assign(patch, listLevelAttrPatch(attrs, {
176
+ numId,
133
177
  ilvl: properties.listLevel
134
- };
178
+ }, numbering));
179
+ else {
180
+ patch["numPr"] = { ilvl: properties.listLevel };
181
+ Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
182
+ }
135
183
  }
136
184
  }
137
185
  if (properties.alignment !== void 0 && (styleChanged || (currentDirectAlignment ?? null) !== properties.alignment)) {
138
186
  originalFormatting ??= {};
139
187
  if (properties.alignment === null) Reflect.deleteProperty(originalFormatting, "alignment");
140
188
  else originalFormatting.alignment = properties.alignment;
141
- const alignmentFromStyle = properties.styleId === void 0 ? attrs.alignmentFromStyle : resolvedAlignmentFromStyle;
189
+ const alignmentFromStyle = properties.styleId === void 0 ? attrs.alignmentFromStyle : resolvedFormattingFromStyle?.alignment;
142
190
  patch["alignment"] = properties.alignment ?? alignmentFromStyle ?? null;
143
191
  originalFormattingChanged = true;
144
192
  }
145
- if (originalFormattingChanged && originalFormatting !== void 0) patch["_originalFormatting"] = Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
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;
146
203
  return Object.keys(patch).length > 0 ? patch : null;
147
204
  };
148
205
  const REVISION_MARK_NAMES = /* @__PURE__ */ new Set(["insertion", "deletion"]);
@@ -201,7 +258,14 @@ const paragraphPropertiesBeforeBatch = (node, batchRevisionIds) => {
201
258
  if (!earliestBatchChange) return paragraphPropertiesSnapshot(node);
202
259
  return earliestBatchChange.previousFormatting ?? {};
203
260
  };
204
- const resolveAlignmentFromStyle = ({ attrs, styleId, styleResolver }) => styleResolver?.resolveParagraphStyle(styleId).paragraphFormatting?.alignment ?? ((styleId ?? void 0) === (attrs.styleId ?? void 0) ? attrs.alignmentFromStyle : void 0);
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;
268
+ };
205
269
  const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) => {
206
270
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return {
207
271
  tr,
@@ -214,7 +278,7 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
214
278
  revisionId: null
215
279
  };
216
280
  const attrs = expectParagraphAttrs(block);
217
- const resolvedAlignmentFromStyle = resolveAlignmentFromStyle({
281
+ const resolvedFormattingFromStyle = resolveFormattingFromStyle({
218
282
  attrs,
219
283
  styleId: item.operation.styleId,
220
284
  styleResolver
@@ -222,7 +286,7 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
222
286
  const patch = paragraphPropertiesPatch({
223
287
  node: block,
224
288
  properties: { styleId: item.operation.styleId },
225
- resolvedAlignmentFromStyle
289
+ resolvedFormattingFromStyle
226
290
  });
227
291
  if (patch === null) return {
228
292
  tr,
@@ -247,141 +311,134 @@ const REPLACEMENT_BACKGROUND_CLEAR_FORMATTING = {
247
311
  highlight: false,
248
312
  runShading: false
249
313
  };
250
- const INLINE_FORMATTING_MARK_NAMES = {
251
- bold: "bold",
252
- italic: "italic",
253
- underline: "underline",
254
- strike: "strike",
255
- fontFamily: "fontFamily",
256
- fontSizePt: "fontSize",
257
- color: "textColor",
258
- highlight: "highlight",
259
- runShading: "runShading"
260
- };
261
- const DIRECT_FONT_PROPERTIES = [
262
- "fontFamily",
263
- "fontSize",
264
- "color"
265
- ];
266
- const formattingMarkName = (property) => {
267
- if (!Object.hasOwn(INLINE_FORMATTING_MARK_NAMES, property)) return null;
268
- const name = Reflect.get(INLINE_FORMATTING_MARK_NAMES, property);
269
- return typeof name === "string" ? name : null;
270
- };
271
- const formattingMarkAttrs = (property, value) => {
272
- switch (property) {
273
- case "underline": return { style: "single" };
274
- case "fontFamily": return typeof value === "string" ? {
275
- ascii: value,
276
- hAnsi: value
277
- } : null;
278
- case "fontSizePt": return typeof value === "number" ? { size: value * 2 } : null;
279
- case "color": return typeof value === "string" ? { rgb: value.replace(/^#/u, "").toUpperCase() } : null;
280
- default: return {};
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;
281
322
  }
282
- };
283
- const applyInlineFormatting = ({ tr, schema, from, to, formatting, includedProperties }) => {
284
- for (const [property, value] of Object.entries(formatting)) {
285
- if (includedProperties && !includedProperties.includes(property)) continue;
286
- const markName = formattingMarkName(property);
287
- const markType = markName ? schema.marks[markName] : void 0;
288
- if (!markType) continue;
289
- if (value !== false && value !== null) {
290
- const attrs = formattingMarkAttrs(property, value);
291
- if (attrs) tr.addMark(from, to, markType.create(attrs));
292
- continue;
293
- }
294
- tr.removeMark(from, to, markType);
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;
295
329
  }
296
- applyDirectFontProvenance({
297
- tr,
298
- 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,
299
346
  from,
300
- to,
301
- formatting,
302
- ...includedProperties ? { includedProperties } : {}
347
+ to
303
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
+ }
304
376
  return tr;
305
377
  };
306
- const applyDirectFontProvenance = ({ tr, schema, from, to, formatting, includedProperties }) => {
307
- if ("highlight" in formatting) return;
308
- const changed = [
309
- [
310
- "fontFamily",
311
- "fontFamily",
312
- formatting.fontFamily
313
- ],
314
- [
315
- "fontSize",
316
- "fontSizePt",
317
- formatting.fontSizePt
318
- ],
319
- [
320
- "color",
321
- "color",
322
- formatting.color
323
- ]
324
- ].filter(([, inputProperty, value]) => value !== void 0 && (!includedProperties || includedProperties.includes(inputProperty)));
325
- const markType = schema.marks["runFormattingOverride"];
326
- if (!markType || changed.length === 0) return;
327
- tr.doc.nodesBetween(from, to, (node, pos) => {
328
- if (!node.isText) return;
329
- const segmentFrom = Math.max(from, pos);
330
- const segmentTo = Math.min(to, pos + node.nodeSize);
331
- const existing = node.marks.find((mark) => mark.type === markType);
332
- const attrs = existing ? expectRunFormattingOverrideMarkAttrs(existing) : {};
333
- const directFontProperties = new Set(attrs.directFontProperties);
334
- for (const [property, , value] of changed) if (value === null) directFontProperties.delete(property);
335
- else directFontProperties.add(property);
336
- tr.removeMark(segmentFrom, segmentTo, markType);
337
- const nextAttrs = {
338
- ...attrs,
339
- directFontProperties: DIRECT_FONT_PROPERTIES.filter((property) => directFontProperties.has(property))
340
- };
341
- if (Object.entries(nextAttrs).some(([key, value]) => key === "directFontProperties" ? Array.isArray(value) && value.length > 0 : value !== null && value !== void 0)) tr.addMark(segmentFrom, segmentTo, markType.create(nextAttrs));
342
- });
343
- };
344
- const directFontPropertyForFormattingProperty = (property) => {
345
- switch (property) {
346
- case "fontFamily": return "fontFamily";
347
- case "fontSizePt": return "fontSize";
348
- case "color": return "color";
349
- default: return null;
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;
350
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);
351
387
  };
352
- const formattingPropertyWouldChange = (marks, property, value) => {
353
- const markName = formattingMarkName(property);
354
- const mark = marks.find((candidate) => candidate.type.name === markName);
355
- const directFontProperty = directFontPropertyForFormattingProperty(property);
356
- if (directFontProperty) {
357
- const overrideMark = marks.find((candidate) => candidate.type.name === "runFormattingOverride");
358
- const isDirect = overrideMark ? (expectRunFormattingOverrideMarkAttrs(overrideMark).directFontProperties ?? []).includes(directFontProperty) : false;
359
- if (value === false || value === null) return isDirect;
360
- if (!isDirect) return true;
361
- } else if (value === false || value === null) return mark !== void 0;
362
- if (property === "underline") return mark?.attrs["style"] !== "single";
363
- if (property === "fontFamily") return mark?.attrs["ascii"] !== value || mark?.attrs["hAnsi"] !== value;
364
- if (property === "fontSizePt") return Number(mark?.attrs["size"]) !== Number(value) * 2;
365
- if (property === "color") {
366
- const current = mark?.attrs["rgb"];
367
- return (typeof current === "string" ? current.replace(/^#/u, "").toUpperCase() : null) !== String(value).replace(/^#/u, "").toUpperCase();
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)));
368
394
  }
369
- 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)));
370
399
  };
371
- const formattingChangesForMarks = (marks, formatting) => {
400
+ const formattingChangesForAuthoredFormatting = (authoredFormatting, formatting) => {
372
401
  const changes = [];
373
- 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
+ }
374
421
  return changes;
375
422
  };
376
- const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, author, date, initials, suggestionId = null }) => {
377
- if (![schema.marks["highlight"], schema.marks["runShading"]].some((markType) => markType !== void 0 && tr.doc.rangeHasMark(from, to, markType))) return tr;
378
- if (mode === "direct") return applyInlineFormatting({
379
- tr,
380
- schema,
381
- from,
382
- to,
383
- formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING
384
- });
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
+ };
385
442
  return applyTrackedInlineFormatting({
386
443
  tr,
387
444
  schema,
@@ -389,26 +446,85 @@ const clearReplacementBackground = ({ tr, schema, from, to, mode, revisionId, au
389
446
  from,
390
447
  to,
391
448
  formatting: REPLACEMENT_BACKGROUND_CLEAR_FORMATTING,
392
- revisionId,
449
+ revisionIdSeed,
393
450
  author,
394
451
  date,
395
452
  initials,
396
- suggestionId
453
+ suggestionId,
454
+ ...styleResolver !== void 0 ? { styleResolver } : {}
397
455
  });
398
456
  };
399
- 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 }) => {
400
458
  const propertyChangeType = schema.marks["runPropertyChange"];
401
- if (!propertyChangeType) return tr;
459
+ if (!propertyChangeType) return {
460
+ type: "applied",
461
+ transaction: tr,
462
+ revisionIds: [],
463
+ nextRevisionId: revisionIdSeed
464
+ };
402
465
  const segments = [];
403
- doc.nodesBetween(from, to, (node, pos) => {
404
- if (!node.isText) return;
405
- const includedProperties = formattingChangesForMarks(node.marks, formatting);
406
- if (includedProperties.length === 0) return;
407
- const segmentFrom = Math.max(from, pos);
408
- const segmentTo = Math.min(to, pos + node.nodeSize);
409
- const previousFormatting = marksToTextFormatting(node.marks);
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;
410
486
  const existingMark = node.marks.find((mark) => mark.type === propertyChangeType);
411
- 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);
412
528
  const change = {
413
529
  type: "runPropertyChange",
414
530
  info: {
@@ -417,35 +533,24 @@ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, r
417
533
  date,
418
534
  ...initials ? { initials } : {}
419
535
  },
420
- ...Object.keys(previousFormatting).length > 0 ? { previousFormatting } : {}
536
+ ...Object.keys(segment.previousFormatting).length > 0 ? { previousFormatting: segment.previousFormatting } : {}
421
537
  };
422
- segments.push({
423
- from: segmentFrom,
424
- to: segmentTo,
425
- includedProperties,
426
- changes: [...existingChanges, change]
427
- });
428
- });
429
- if (segments.length === 0) return tr;
430
- const suggestionAttrs = suggestionId === null ? {} : {
431
- provenance: "suggested",
432
- suggestionId
433
- };
434
- for (const segment of segments) {
435
- applyInlineFormatting({
538
+ const marks = propertyChangeType.create({
539
+ changes: [change],
540
+ ...suggestionAttrs
541
+ }).addToSet(segment.formattingMarks);
542
+ applyMarksToRunFormattingRepresentation({
436
543
  tr,
437
- schema,
438
- from: segment.from,
439
- to: segment.to,
440
- formatting,
441
- includedProperties: segment.includedProperties
544
+ representation: segment.representation,
545
+ marks
442
546
  });
443
- tr.addMark(segment.from, segment.to, propertyChangeType.create({
444
- changes: segment.changes,
445
- ...suggestionAttrs
446
- }));
447
547
  }
448
- return tr;
548
+ return {
549
+ type: "applied",
550
+ transaction: tr,
551
+ revisionIds,
552
+ nextRevisionId: revisionIdSeed + revisionIds.length
553
+ };
449
554
  };
450
555
  /**
451
556
  * Module-scoped monotonic counter for tracked-change revision ids.
@@ -686,6 +791,7 @@ const buildEmphasisInlineContent = (schema, text, baseMarks) => {
686
791
  };
687
792
  const isBatchableParagraphInsertion = (item, mode) => {
688
793
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return false;
794
+ if (item.commentText !== void 0) return false;
689
795
  const insertTexts = item.insertTexts ?? [""];
690
796
  const isEmptyInsert = insertTexts.length === 1 && insertTexts[0]?.length === 0;
691
797
  return !(mode === "tracked-changes" && item.operation.pageBreakBefore === true && isEmptyInsert);
@@ -781,6 +887,7 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
781
887
  const liveCurrent = next.doc.nodeAt(current.position);
782
888
  if (!liveCurrent || liveCurrent.type.name !== final.type.name) panic("A final-mark rotation lost its paragraph", { position: current.position });
783
889
  const existing = expectParagraphAttrs(liveCurrent)._propertyChanges;
890
+ if (hasSerializableParagraphPropertyChange(existing)) panic("A final-mark rotation cannot append a second w:pPrChange", { position: current.position });
784
891
  const revisionId = nextRevisionId++;
785
892
  next = next.setNodeMarkup(current.position, void 0, {
786
893
  ...liveCurrent.attrs,
@@ -810,7 +917,7 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
810
917
  synthesizedRevisions
811
918
  };
812
919
  };
813
- const buildInsertedParagraphs = ({ item, schema, alignmentFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove }) => {
920
+ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove, numbering }) => {
814
921
  const operation = item.operation;
815
922
  if (operation.type !== "insertAfterBlock" && operation.type !== "insertBeforeBlock") panic("Only paragraph insertions can build inserted paragraphs", { type: operation.type });
816
923
  const insertionType = schema.marks["insertion"];
@@ -823,7 +930,12 @@ const buildInsertedParagraphs = ({ item, schema, alignmentFromStyle, mode, autho
823
930
  suggestionId
824
931
  } : {}
825
932
  };
826
- 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
+ };
827
939
  const insertTexts = item.insertTexts ?? [""];
828
940
  const revisionIds = [];
829
941
  const nodes = [];
@@ -847,33 +959,51 @@ const buildInsertedParagraphs = ({ item, schema, alignmentFromStyle, mode, autho
847
959
  const content = text.length > 0 ? buildEmphasisInlineContent(schema, text, marks) : null;
848
960
  const attrs = isFirstParagraph ? { ...baseAttrs } : {};
849
961
  if (isFirstParagraph && operation.pageBreakBefore === true) attrs["pageBreakBefore"] = true;
850
- if (isFirstParagraph && operation.listLevel === null) {
962
+ const listLevel = operation.listLevel;
963
+ if (isFirstParagraph && listLevel === null) {
851
964
  attrs["numPr"] = null;
852
- Object.assign(attrs, CLEARED_LIST_MARKER_ATTRS);
853
- } else if (isFirstParagraph && operation.listLevel !== void 0) {
854
- 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");
855
968
  const numId = typeof anchorNumPr === "object" && anchorNumPr !== null && "numId" in anchorNumPr ? anchorNumPr.numId : void 0;
856
- attrs["numPr"] = {
857
- ...typeof numId === "number" && { numId },
858
- ilvl: operation.listLevel
859
- };
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
+ }
860
977
  }
861
978
  if (isFirstParagraph && operation.styleId !== void 0) {
862
979
  attrs["styleId"] = operation.styleId;
863
- 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);
864
981
  }
865
- if (isFirstParagraph && (operation.styleId !== void 0 || operation.alignment !== void 0)) {
982
+ if (isFirstParagraph && (operation.styleId !== void 0 || operation.alignment !== void 0 || operation.spacing !== void 0)) {
866
983
  const inheritedDirectAlignment = operation.inheritFormatting === false ? void 0 : directParagraphAlignment(expectParagraphAttrs(item.blockNode));
984
+ const inheritedDirectSpacing = operation.inheritFormatting === false ? void 0 : directParagraphSpacing(expectParagraphAttrs(item.blockNode));
867
985
  const directAlignment = operation.alignment === void 0 ? inheritedDirectAlignment : operation.alignment ?? void 0;
868
- attrs["alignmentFromStyle"] = alignmentFromStyle;
869
- attrs["alignment"] = directAlignment ?? alignmentFromStyle ?? null;
986
+ const directSpacing = operation.spacing === void 0 ? inheritedDirectSpacing : operation.spacing ?? void 0;
987
+ attrs["alignmentFromStyle"] = formattingFromStyle?.alignment;
988
+ attrs["alignment"] = directAlignment ?? formattingFromStyle?.alignment ?? null;
870
989
  const sourceFormatting = attrs["_originalFormatting"];
871
- const originalFormatting = typeof sourceFormatting === "object" && sourceFormatting !== null ? { ...sourceFormatting } : {};
872
- if (operation.styleId !== void 0) if (operation.styleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
873
- else originalFormatting.styleId = operation.styleId;
874
- if (operation.alignment !== void 0 || inheritedDirectAlignment !== void 0) if (directAlignment === void 0) Reflect.deleteProperty(originalFormatting, "alignment");
875
- else originalFormatting.alignment = directAlignment;
876
- attrs["_originalFormatting"] = Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
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;
877
1007
  }
878
1008
  if (isSuggested && suggestionId !== null && paragraphRevisionId !== null) attrs["_suggestedInsert"] = {
879
1009
  suggestionId,
@@ -914,13 +1044,37 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
914
1044
  const insertionType = view.state.schema.marks["insertion"];
915
1045
  const deletionType = view.state.schema.marks["deletion"];
916
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
+ };
917
1070
  const styleResolver = getDocumentStyleResolver(view.state);
918
- const alignmentFromStyleForInsertion = (item) => {
1071
+ const numbering = getDocumentNumbering(view.state);
1072
+ const formattingFromStyleForInsertion = (item) => {
919
1073
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return;
920
1074
  const anchorAttrs = expectParagraphAttrs(item.blockNode);
921
1075
  const inheritedStyleId = item.operation.inheritFormatting === false ? null : anchorAttrs.styleId;
922
1076
  const styleId = item.operation.styleId !== void 0 ? item.operation.styleId : inheritedStyleId;
923
- return resolveAlignmentFromStyle({
1077
+ return resolveFormattingFromStyle({
924
1078
  attrs: anchorAttrs,
925
1079
  styleId,
926
1080
  styleResolver
@@ -964,6 +1118,13 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
964
1118
  });
965
1119
  continue;
966
1120
  }
1121
+ if (commentText !== void 0 && !hasRepresentableCommentAnchor(resolution.operation, mode)) {
1122
+ skipped.push({
1123
+ id: operation.id,
1124
+ reason: "unsupportedBlock"
1125
+ });
1126
+ continue;
1127
+ }
967
1128
  const deletion = resolution.operation.tableRowDeletion;
968
1129
  if (deletion) {
969
1130
  const rowKey = `${deletion.tablePosition}:${deletion.rowIndex}`;
@@ -993,11 +1154,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
993
1154
  code: "splitMultilineText",
994
1155
  paragraphCount: resolution.operation.insertTexts?.length ?? 1
995
1156
  });
996
- const commentId = commentText !== void 0 ? createCommentId?.(commentText) : void 0;
997
1157
  resolved.push({
998
1158
  ...resolution.operation,
999
1159
  originalIndex: index,
1000
- ...commentId !== void 0 && { commentId }
1160
+ ...commentText !== void 0 && { commentText }
1001
1161
  });
1002
1162
  }
1003
1163
  const tablePlan = planTableMutations(resolved.map((item) => ({
@@ -1040,6 +1200,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1040
1200
  }
1041
1201
  let tr = view.state.tr;
1042
1202
  const revisionIdReservation = executableResolved.reduce((total, item) => total + estimateRevisionIdReservation(item), 0);
1203
+ const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
1043
1204
  let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionSeed(revisionIdReservation);
1044
1205
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
1045
1206
  const insertedColumnCounts = /* @__PURE__ */ new Map();
@@ -1071,6 +1232,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1071
1232
  for (let executionIndex = 0; executionIndex < executionOrder.length; executionIndex++) {
1072
1233
  const item = executionOrder[executionIndex];
1073
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
+ }
1074
1246
  if (isBatchableParagraphInsertion(item, mode)) {
1075
1247
  const insertionRun = [item];
1076
1248
  for (let lookahead = executionIndex + 1; lookahead < executionOrder.length; lookahead++) {
@@ -1083,25 +1255,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1083
1255
  const runApplied = [];
1084
1256
  for (const insertion of insertionRun) {
1085
1257
  const insertionSuggestionId = isSuggested ? insertion.operation.suggestionId ?? insertion.operation.id : null;
1086
- const insertionCommentMark = insertion.commentId !== void 0 && commentType ? commentType.create({ commentId: insertion.commentId }) : null;
1087
1258
  const built = buildInsertedParagraphs({
1088
1259
  item: insertion,
1089
1260
  schema: view.state.schema,
1090
- alignmentFromStyle: alignmentFromStyleForInsertion(insertion),
1261
+ formattingFromStyle: formattingFromStyleForInsertion(insertion),
1091
1262
  mode,
1092
1263
  author,
1093
1264
  date,
1094
1265
  initials,
1095
- commentMark: insertionCommentMark,
1266
+ commentMark: null,
1096
1267
  suggestionId: insertionSuggestionId,
1097
1268
  revisionSeed,
1098
- isPairedMove
1269
+ isPairedMove,
1270
+ numbering
1099
1271
  });
1100
1272
  revisionSeed = built.nextRevisionId;
1101
1273
  nodeGroups.push(built.nodes);
1102
1274
  runApplied.push({
1103
1275
  id: insertion.operation.id,
1104
- ...insertion.commentId !== void 0 && { commentId: insertion.commentId },
1105
1276
  ...built.revisionIds[0] !== void 0 && {
1106
1277
  revisionId: built.revisionIds[0],
1107
1278
  revisionIds: built.revisionIds
@@ -1117,7 +1288,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1117
1288
  continue;
1118
1289
  }
1119
1290
  }
1120
- 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;
1121
1293
  const stepsBefore = tr.steps.length;
1122
1294
  let appliedRevisionIds;
1123
1295
  if (isSuggested && !SUGGESTED_SUPPORTED_OPERATION_TYPES.has(item.operation.type)) {
@@ -1135,8 +1307,9 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1135
1307
  suggestionId
1136
1308
  } : {}
1137
1309
  };
1310
+ let operationRevisionSeed = revisionSeed;
1138
1311
  const structuralRevision = producesTrackedChanges ? {
1139
- revisionId: revisionSeed,
1312
+ revisionId: operationRevisionSeed,
1140
1313
  author,
1141
1314
  date,
1142
1315
  ...trackedRevisionExtras
@@ -1144,22 +1317,32 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1144
1317
  switch (item.operation.type) {
1145
1318
  case "replaceInBlock":
1146
1319
  case "replaceRange": {
1147
- const revisionIdDelete = revisionSeed++;
1148
- const revisionIdInsert = revisionSeed++;
1149
- const revisionIdBackground = revisionSeed++;
1320
+ const revisionIdDelete = operationRevisionSeed;
1321
+ const revisionIdInsert = producesTrackedChanges ? operationRevisionSeed + 1 : operationRevisionSeed;
1322
+ const revisionIdBackgroundSeed = producesTrackedChanges ? operationRevisionSeed + 2 : operationRevisionSeed;
1150
1323
  const stepsBeforeBackgroundClear = tr.steps.length;
1151
- tr = clearReplacementBackground({
1324
+ const backgroundResult = clearReplacementBackground({
1152
1325
  tr,
1153
1326
  schema: view.state.schema,
1154
1327
  from: item.from,
1155
1328
  to: item.to,
1156
1329
  mode,
1157
- revisionId: revisionIdBackground,
1330
+ revisionIdSeed: revisionIdBackgroundSeed,
1158
1331
  author,
1159
1332
  date,
1160
1333
  initials,
1161
- suggestionId
1334
+ suggestionId,
1335
+ styleResolver
1162
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;
1163
1346
  const clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
1164
1347
  tr = applyTextReplacement({
1165
1348
  tr,
@@ -1177,7 +1360,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1177
1360
  if (producesTrackedChanges) appliedRevisionIds = [
1178
1361
  revisionIdDelete,
1179
1362
  revisionIdInsert,
1180
- ...clearedBackground ? [revisionIdBackground] : []
1363
+ ...clearedBackground ? backgroundResult.revisionIds : []
1181
1364
  ];
1182
1365
  break;
1183
1366
  }
@@ -1186,44 +1369,52 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1186
1369
  break;
1187
1370
  case "formatRange":
1188
1371
  if (producesTrackedChanges) {
1189
- const revisionId = revisionSeed++;
1190
- tr = applyTrackedInlineFormatting({
1372
+ const formattingResult = applyTrackedInlineFormatting({
1191
1373
  tr,
1192
1374
  schema: view.state.schema,
1193
1375
  doc: tr.doc,
1194
1376
  from: item.from,
1195
1377
  to: item.to,
1196
1378
  formatting: item.operation.formatting,
1197
- revisionId,
1379
+ revisionIdSeed: operationRevisionSeed,
1198
1380
  author,
1199
1381
  date,
1200
1382
  initials,
1201
- suggestionId
1383
+ suggestionId,
1384
+ styleResolver
1202
1385
  });
1203
- 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];
1204
1396
  break;
1205
1397
  }
1206
1398
  tr = applyInlineFormatting({
1207
1399
  tr,
1208
- schema: view.state.schema,
1209
1400
  from: item.from,
1210
1401
  to: item.to,
1211
- formatting: item.operation.formatting
1402
+ formatting: item.operation.formatting,
1403
+ ...styleResolver !== void 0 ? { styleResolver } : {}
1212
1404
  });
1213
1405
  break;
1214
1406
  case "replaceBlock": {
1215
1407
  if (!isResolvedReplaceBlockOperation(item)) panic("Resolved replaceBlock operation lost its impact discriminator");
1216
1408
  const { changesStyle, changesText } = REPLACE_BLOCK_IMPACT[item.replaceBlockImpact];
1217
- const revisionIdDelete = revisionSeed;
1218
- const revisionIdInsert = changesText ? revisionSeed + 1 : revisionSeed;
1219
- const revisionIdBackground = changesText ? revisionSeed + 2 : revisionSeed;
1220
- if (changesText) revisionSeed += 3;
1221
- const revisionIdParagraph = producesTrackedChanges && changesStyle ? revisionSeed++ : null;
1409
+ const revisionIdDelete = operationRevisionSeed;
1410
+ const revisionIdInsert = changesText ? operationRevisionSeed + 1 : operationRevisionSeed;
1411
+ const revisionIdBackgroundSeed = changesText ? operationRevisionSeed + 2 : operationRevisionSeed;
1412
+ if (producesTrackedChanges && changesText) operationRevisionSeed = revisionIdBackgroundSeed;
1222
1413
  if (item.operation.preserveFormatting === false && mode === "direct") {
1223
1414
  const replacement = item.operation.text;
1224
1415
  const paragraphType = view.state.schema.nodes["paragraph"];
1225
1416
  if (paragraphType) {
1226
- const node = paragraphType.create(null, 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] : []));
1227
1418
  tr = tr.replaceWith(item.blockFrom, item.blockTo, node);
1228
1419
  tr = applyReplaceBlockStyleId({
1229
1420
  item,
@@ -1244,21 +1435,33 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1244
1435
  break;
1245
1436
  }
1246
1437
  let clearedBackground = false;
1438
+ let backgroundRevisionIds = [];
1247
1439
  if (changesText) {
1248
1440
  const stepsBeforeBackgroundClear = tr.steps.length;
1249
- tr = clearReplacementBackground({
1441
+ const backgroundResult = clearReplacementBackground({
1250
1442
  tr,
1251
1443
  schema: view.state.schema,
1252
1444
  from: item.from,
1253
1445
  to: item.to,
1254
1446
  mode,
1255
- revisionId: revisionIdBackground,
1447
+ revisionIdSeed: revisionIdBackgroundSeed,
1256
1448
  author,
1257
1449
  date,
1258
1450
  initials,
1259
- suggestionId
1451
+ suggestionId,
1452
+ styleResolver
1260
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;
1261
1463
  clearedBackground = tr.steps.length > stepsBeforeBackgroundClear;
1464
+ backgroundRevisionIds = backgroundResult.revisionIds;
1262
1465
  tr = applyTextReplacement({
1263
1466
  tr,
1264
1467
  item,
@@ -1273,6 +1476,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1273
1476
  diffText
1274
1477
  });
1275
1478
  }
1479
+ const revisionIdParagraph = producesTrackedChanges && changesStyle ? operationRevisionSeed++ : null;
1276
1480
  const styleResult = applyReplaceBlockStyleId({
1277
1481
  item,
1278
1482
  tr,
@@ -1285,9 +1489,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1285
1489
  } } : {}
1286
1490
  });
1287
1491
  tr = styleResult.tr;
1492
+ if (!changesText && commentMark) tr = tr.addMark(item.from, item.to, commentMark);
1288
1493
  if (producesTrackedChanges) appliedRevisionIds = [
1289
1494
  ...changesText ? [revisionIdDelete, revisionIdInsert] : [],
1290
- ...clearedBackground ? [revisionIdBackground] : [],
1495
+ ...clearedBackground ? backgroundRevisionIds : [],
1291
1496
  ...styleResult.revisionId === null ? [] : [styleResult.revisionId]
1292
1497
  ];
1293
1498
  break;
@@ -1306,17 +1511,18 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1306
1511
  const built = buildInsertedParagraphs({
1307
1512
  item,
1308
1513
  schema: view.state.schema,
1309
- alignmentFromStyle: alignmentFromStyleForInsertion(item),
1514
+ formattingFromStyle: formattingFromStyleForInsertion(item),
1310
1515
  mode,
1311
1516
  author,
1312
1517
  date,
1313
1518
  initials,
1314
1519
  commentMark,
1315
1520
  suggestionId,
1316
- revisionSeed,
1317
- isPairedMove
1521
+ revisionSeed: operationRevisionSeed,
1522
+ isPairedMove,
1523
+ numbering
1318
1524
  });
1319
- revisionSeed = built.nextRevisionId;
1525
+ operationRevisionSeed = built.nextRevisionId;
1320
1526
  if (built.revisionIds.length > 0) appliedRevisionIds = built.revisionIds;
1321
1527
  tr = tr.insert(item.from, built.nodes);
1322
1528
  break;
@@ -1342,7 +1548,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1342
1548
  }
1343
1549
  let node = signatureTable;
1344
1550
  if (isSuggested && suggestionId !== null) {
1345
- const revisionId = revisionSeed++;
1551
+ const revisionId = operationRevisionSeed++;
1346
1552
  node = signatureTable.type.create({
1347
1553
  ...signatureTable.attrs,
1348
1554
  _suggestedInsert: {
@@ -1385,7 +1591,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1385
1591
  }
1386
1592
  tr = result.transaction;
1387
1593
  if (result.revisionId !== null) {
1388
- revisionSeed++;
1594
+ operationRevisionSeed++;
1389
1595
  appliedRevisionIds = [result.revisionId];
1390
1596
  }
1391
1597
  break;
@@ -1415,7 +1621,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1415
1621
  }
1416
1622
  tr = result.transaction;
1417
1623
  if (result.revisionId !== null) {
1418
- revisionSeed++;
1624
+ operationRevisionSeed++;
1419
1625
  appliedRevisionIds = [result.revisionId];
1420
1626
  }
1421
1627
  const columnKey = getTableColumnCoordinateKey(insertion);
@@ -1448,7 +1654,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1448
1654
  }
1449
1655
  tr = result.transaction;
1450
1656
  if (result.revisionId !== null) {
1451
- revisionSeed++;
1657
+ operationRevisionSeed++;
1452
1658
  appliedRevisionIds = [result.revisionId];
1453
1659
  }
1454
1660
  break;
@@ -1464,7 +1670,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1464
1670
  });
1465
1671
  continue;
1466
1672
  }
1467
- const revisionId = mode === "tracked-changes" ? revisionSeed : null;
1673
+ const revisionId = mode === "tracked-changes" ? operationRevisionSeed : null;
1468
1674
  const nextTr = revisionId === null ? mergeTableRectangle({
1469
1675
  tr,
1470
1676
  tablePosition,
@@ -1488,7 +1694,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1488
1694
  }
1489
1695
  tr = nextTr;
1490
1696
  if (revisionId !== null) {
1491
- revisionSeed++;
1697
+ operationRevisionSeed++;
1492
1698
  appliedRevisionIds = [revisionId];
1493
1699
  }
1494
1700
  break;
@@ -1504,7 +1710,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1504
1710
  });
1505
1711
  continue;
1506
1712
  }
1507
- const revisionId = mode === "tracked-changes" ? revisionSeed : null;
1713
+ const revisionId = mode === "tracked-changes" ? operationRevisionSeed : null;
1508
1714
  const nextTr = revisionId === null ? splitTableRectangle({
1509
1715
  tr,
1510
1716
  tablePosition,
@@ -1528,7 +1734,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1528
1734
  }
1529
1735
  tr = nextTr;
1530
1736
  if (revisionId !== null) {
1531
- revisionSeed++;
1737
+ operationRevisionSeed++;
1532
1738
  appliedRevisionIds = [revisionId];
1533
1739
  }
1534
1740
  break;
@@ -1556,7 +1762,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1556
1762
  }
1557
1763
  tr = result.transaction;
1558
1764
  if (result.revisionId !== null) {
1559
- revisionSeed++;
1765
+ operationRevisionSeed++;
1560
1766
  appliedRevisionIds = [result.revisionId];
1561
1767
  }
1562
1768
  break;
@@ -1568,7 +1774,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1568
1774
  break;
1569
1775
  }
1570
1776
  if (deletionType) {
1571
- const revisionId = revisionSeed++;
1777
+ const revisionId = operationRevisionSeed++;
1572
1778
  const deletionMark = deletionType.create({
1573
1779
  revisionId,
1574
1780
  author,
@@ -1583,7 +1789,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1583
1789
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
1584
1790
  const markPosition = tr.mapping.map(item.blockFrom);
1585
1791
  if (!paragraphEndsItsContainer(tr.doc.resolve(markPosition), item.blockNode.type.name) && tr.doc.nodeAt(markPosition)?.attrs["pPrMark"] == null) {
1586
- const markRevisionId = revisionSeed++;
1792
+ const markRevisionId = operationRevisionSeed++;
1587
1793
  tr = tr.setNodeAttribute(markPosition, "pPrMark", {
1588
1794
  kind: isPairedMove(item.operation.moveId) ? "moveFrom" : "del",
1589
1795
  info: {
@@ -1600,7 +1806,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1600
1806
  break;
1601
1807
  case "insertTable": {
1602
1808
  const revision = {
1603
- revisionId: revisionSeed,
1809
+ revisionId: operationRevisionSeed,
1604
1810
  author,
1605
1811
  date,
1606
1812
  ...trackedRevisionExtras
@@ -1622,7 +1828,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1622
1828
  });
1623
1829
  continue;
1624
1830
  }
1625
- if (producesTrackedChanges) appliedRevisionIds = [revisionSeed++];
1831
+ if (producesTrackedChanges) appliedRevisionIds = [operationRevisionSeed++];
1626
1832
  tr = tr.insert(item.from, table);
1627
1833
  markStructuralChange(tr);
1628
1834
  break;
@@ -1660,7 +1866,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1660
1866
  continue;
1661
1867
  }
1662
1868
  const revision = {
1663
- revisionId: revisionSeed++,
1869
+ revisionId: operationRevisionSeed++,
1664
1870
  author,
1665
1871
  date,
1666
1872
  ...trackedRevisionExtras
@@ -1695,7 +1901,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1695
1901
  const blockPosition = tr.mapping.map(item.blockFrom);
1696
1902
  const liveBlock = tr.doc.nodeAt(blockPosition) ?? item.blockNode;
1697
1903
  const liveAttrs = expectParagraphAttrs(liveBlock);
1698
- const resolvedAlignmentFromStyle = item.operation.properties.styleId === void 0 ? void 0 : resolveAlignmentFromStyle({
1904
+ const resolvedFormattingFromStyle = item.operation.properties.styleId === void 0 ? resolveFormattingFromStyle({
1905
+ attrs: liveAttrs,
1906
+ styleId: liveAttrs.styleId,
1907
+ styleResolver
1908
+ }) : resolveFormattingFromStyle({
1699
1909
  attrs: liveAttrs,
1700
1910
  styleId: item.operation.properties.styleId,
1701
1911
  styleResolver
@@ -1703,7 +1913,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1703
1913
  const patch = paragraphPropertiesPatch({
1704
1914
  node: liveBlock,
1705
1915
  properties: item.operation.properties,
1706
- resolvedAlignmentFromStyle
1916
+ resolvedFormattingFromStyle,
1917
+ numbering
1707
1918
  });
1708
1919
  if (patch === null) {
1709
1920
  skipped.push({
@@ -1719,7 +1930,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1719
1930
  });
1720
1931
  break;
1721
1932
  }
1722
- const revisionId = revisionSeed++;
1933
+ const revisionId = operationRevisionSeed++;
1723
1934
  const change = {
1724
1935
  type: "paragraphPropertyChange",
1725
1936
  info: {
@@ -1745,7 +1956,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1745
1956
  tr = tr.split(item.from);
1746
1957
  break;
1747
1958
  }
1748
- const revisionIdMark = revisionSeed++;
1959
+ const revisionIdMark = operationRevisionSeed++;
1749
1960
  const info = {
1750
1961
  id: revisionIdMark,
1751
1962
  author,
@@ -1754,7 +1965,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1754
1965
  };
1755
1966
  appliedRevisionIds = [revisionIdMark];
1756
1967
  if (item.to > item.from && deletionType) {
1757
- const revisionIdSeparator = revisionSeed++;
1968
+ const revisionIdSeparator = operationRevisionSeed++;
1758
1969
  tr = tr.addMark(item.from, item.to, deletionType.create({
1759
1970
  revisionId: revisionIdSeparator,
1760
1971
  author,
@@ -1778,10 +1989,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1778
1989
  tr = tr.join(item.blockTo + separator.length);
1779
1990
  break;
1780
1991
  }
1781
- const revisionIdMark = revisionSeed++;
1992
+ const revisionIdMark = operationRevisionSeed++;
1782
1993
  appliedRevisionIds = [revisionIdMark];
1783
1994
  if (separator.length > 0 && insertionType) {
1784
- const revisionIdSeparator = revisionSeed++;
1995
+ const revisionIdSeparator = operationRevisionSeed++;
1785
1996
  tr = tr.insertText(separator, insertAt);
1786
1997
  tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
1787
1998
  revisionId: revisionIdSeparator,
@@ -1814,9 +2025,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1814
2025
  });
1815
2026
  continue;
1816
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;
1817
2043
  applied.push({
1818
2044
  id: item.operation.id,
1819
- ...item.commentId !== void 0 && { commentId: item.commentId },
2045
+ ...committedCommentId !== void 0 && { commentId: committedCommentId },
1820
2046
  ...appliedRevisionIds !== void 0 && appliedRevisionIds[0] !== void 0 && {
1821
2047
  revisionId: appliedRevisionIds[0],
1822
2048
  revisionIds: appliedRevisionIds
@@ -1851,6 +2077,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1851
2077
  revisionIds: [...receipt.revisionIds, revisionId]
1852
2078
  };
1853
2079
  }
2080
+ if (ownsSharedRevisionIdCursor) revisionIdCursor = Math.max(revisionIdCursor, revisionSeed);
1854
2081
  if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
1855
2082
  view.dispatch(tr);
1856
2083
  }
@@ -2032,7 +2259,8 @@ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId
2032
2259
  const cleanBlock = buildCleanBlockText(live.node, live.from);
2033
2260
  const currentText = cleanBlock.text;
2034
2261
  const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText));
2035
- if (currentTextHash !== anchor.textHash) return {
2262
+ const structuralBoundaryHash = hashFolioAIBlockStructuralBoundaries(cleanBlock);
2263
+ if (currentTextHash !== anchor.textHash || structuralBoundaryHash !== anchor.structuralBoundaryHash) return {
2036
2264
  type: "skip",
2037
2265
  reason: "changedBlock"
2038
2266
  };
@@ -2062,9 +2290,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2062
2290
  };
2063
2291
  if (operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange") {
2064
2292
  const { startOffset, endOffset, selectedTextHash } = operation.range;
2065
- const from = cleanBlock.offsets[startOffset];
2066
- const to = cleanBlock.offsets[endOffset];
2067
- if (from === void 0 || to === void 0) return {
2293
+ if (cleanBlock.offsets[startOffset] === void 0 || cleanBlock.offsets[endOffset] === void 0) return {
2068
2294
  type: "skip",
2069
2295
  reason: "staleRange"
2070
2296
  };
@@ -2073,6 +2299,15 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2073
2299
  type: "skip",
2074
2300
  reason: "staleRange"
2075
2301
  };
2302
+ const range = resolveCleanTextRange({
2303
+ cleanBlock,
2304
+ startOffset,
2305
+ endOffset
2306
+ });
2307
+ if (range === null) return {
2308
+ type: "skip",
2309
+ reason: "unsupportedBlock"
2310
+ };
2076
2311
  if (operation.type === "replaceRange" && selectedText === operation.replace) return {
2077
2312
  type: "skip",
2078
2313
  reason: "noopOperation"
@@ -2081,8 +2316,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2081
2316
  type: "resolved",
2082
2317
  operation: {
2083
2318
  operation,
2084
- from,
2085
- to,
2319
+ from: range.from,
2320
+ to: range.to,
2086
2321
  blockFrom,
2087
2322
  blockTo,
2088
2323
  blockNode
@@ -2385,8 +2620,10 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2385
2620
  };
2386
2621
  if (operation.type === "splitBlock") {
2387
2622
  const separator = operation.separator ?? "";
2388
- const at = cleanBlock.offsets[operation.offset];
2389
- 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];
2390
2627
  if (at === void 0 || after === void 0 || operation.offset === 0 || operation.offset + separator.length >= currentText.length) return {
2391
2628
  type: "skip",
2392
2629
  reason: "staleRange"
@@ -2395,7 +2632,12 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2395
2632
  type: "skip",
2396
2633
  reason: "staleRange"
2397
2634
  };
2398
- 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 {
2399
2641
  type: "skip",
2400
2642
  reason: "unsupportedBlock"
2401
2643
  };
@@ -2403,8 +2645,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2403
2645
  type: "resolved",
2404
2646
  operation: {
2405
2647
  operation,
2406
- from: at,
2407
- to: after,
2648
+ from: range.from,
2649
+ to: range.to,
2408
2650
  blockFrom,
2409
2651
  blockTo,
2410
2652
  blockNode
@@ -2429,7 +2671,12 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2429
2671
  };
2430
2672
  }
2431
2673
  if (operation.type === "deleteBlock" || operation.type === "replaceBlock") {
2674
+ const replaceChangesText = operation.type === "replaceBlock" && operation.text !== currentText;
2432
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
+ };
2433
2680
  const range = getTextRangeFromCleanBlock(cleanBlock);
2434
2681
  if (!range) {
2435
2682
  const insertionPoint = cleanBlock.offsets.at(0);
@@ -2451,7 +2698,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2451
2698
  blockTo,
2452
2699
  blockNode,
2453
2700
  replaceBlockImpact: resolveReplaceBlockImpact({
2454
- changesText: operation.text !== currentText,
2701
+ changesText: replaceChangesText,
2455
2702
  changesStyle: replaceChangesStyle
2456
2703
  })
2457
2704
  }
@@ -2484,7 +2731,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
2484
2731
  blockTo,
2485
2732
  blockNode,
2486
2733
  replaceBlockImpact: resolveReplaceBlockImpact({
2487
- changesText: operation.text !== currentText,
2734
+ changesText: replaceChangesText,
2488
2735
  changesStyle: replaceChangesStyle
2489
2736
  })
2490
2737
  }
@@ -2525,7 +2772,7 @@ const resolveTextInCleanBlock = (cleanBlock, find) => {
2525
2772
  type: "skip",
2526
2773
  reason: "emptyOperation"
2527
2774
  };
2528
- const { text, offsets } = cleanBlock;
2775
+ const { text } = cleanBlock;
2529
2776
  const firstIndex = text.indexOf(find);
2530
2777
  if (firstIndex === -1) return {
2531
2778
  type: "skip",
@@ -2535,16 +2782,18 @@ const resolveTextInCleanBlock = (cleanBlock, find) => {
2535
2782
  type: "skip",
2536
2783
  reason: "ambiguousFind"
2537
2784
  };
2538
- const from = offsets[firstIndex];
2539
- const to = offsets[firstIndex + find.length];
2540
- 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 {
2541
2791
  type: "skip",
2542
2792
  reason: "unsupportedBlock"
2543
2793
  };
2544
2794
  return {
2545
2795
  type: "resolved",
2546
- from,
2547
- to
2796
+ ...range
2548
2797
  };
2549
2798
  };
2550
2799
  const getTextRangeFromCleanBlock = (cleanBlock) => {