@stll/folio-core 0.51.0 → 0.53.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 (316) hide show
  1. package/README.md +23 -0
  2. package/dist/ai-edits/apply.d.ts +26 -1
  3. package/dist/ai-edits/apply.js +1519 -105
  4. package/dist/ai-edits/batch-claims.d.ts +130 -0
  5. package/dist/ai-edits/batch-claims.js +244 -0
  6. package/dist/ai-edits/blockRange.js +9 -3
  7. package/dist/ai-edits/character-boundaries.d.ts +43 -0
  8. package/dist/ai-edits/character-boundaries.js +95 -0
  9. package/dist/ai-edits/clean-text.d.ts +33 -3
  10. package/dist/ai-edits/clean-text.js +52 -6
  11. package/dist/ai-edits/comment-lifecycle.d.ts +18 -0
  12. package/dist/ai-edits/comment-lifecycle.js +52 -0
  13. package/dist/ai-edits/headless.d.ts +71 -7
  14. package/dist/ai-edits/headless.js +381 -41
  15. package/dist/ai-edits/index.d.ts +2 -1
  16. package/dist/ai-edits/index.js +2 -1
  17. package/dist/ai-edits/minimal-replacement.d.ts +21 -6
  18. package/dist/ai-edits/minimal-replacement.js +134 -15
  19. package/dist/ai-edits/newListNumbering.d.ts +23 -0
  20. package/dist/ai-edits/newListNumbering.js +94 -0
  21. package/dist/ai-edits/note-references.d.ts +96 -0
  22. package/dist/ai-edits/note-references.js +139 -0
  23. package/dist/ai-edits/pending-suggestions.d.ts +79 -0
  24. package/dist/ai-edits/pending-suggestions.js +306 -0
  25. package/dist/ai-edits/read.d.ts +1 -1
  26. package/dist/ai-edits/read.js +56 -43
  27. package/dist/ai-edits/result-validation.d.ts +29 -0
  28. package/dist/ai-edits/result-validation.js +164 -0
  29. package/dist/ai-edits/snapshot.d.ts +36 -3
  30. package/dist/ai-edits/snapshot.js +177 -23
  31. package/dist/ai-edits/table-cell-mutations.d.ts +1 -1
  32. package/dist/ai-edits/table-cell-mutations.js +4 -1
  33. package/dist/ai-edits/table-geometry.d.ts +1 -1
  34. package/dist/ai-edits/table-row-column-mutations.d.ts +38 -16
  35. package/dist/ai-edits/table-row-column-mutations.js +61 -26
  36. package/dist/ai-edits/types.d.ts +155 -16
  37. package/dist/compare/compare.d.ts +1 -1
  38. package/dist/compare/compare.js +65 -16
  39. package/dist/compare/content-alignment.js +13 -2
  40. package/dist/compare/content-types.d.ts +7 -0
  41. package/dist/compare/content.d.ts +4 -1
  42. package/dist/compare/content.js +6 -1
  43. package/dist/compare/formatting.js +2 -1
  44. package/dist/compare/inline-atoms.d.ts +21 -4
  45. package/dist/compare/inline-atoms.js +138 -16
  46. package/dist/compare/inline-provenance.js +18 -22
  47. package/dist/compare/plan.js +10 -5
  48. package/dist/compare/scenario.js +14 -3
  49. package/dist/compare/section-boundary-properties.js +14 -1
  50. package/dist/compare/types.d.ts +4 -2
  51. package/dist/compare/types.js +2 -1
  52. package/dist/compare/verification.d.ts +1 -1
  53. package/dist/compare/verification.js +15 -3
  54. package/dist/controller/committedLayoutDiff.d.ts +25 -0
  55. package/dist/controller/committedLayoutDiff.js +71 -0
  56. package/dist/controller/contentControlWidgetController.d.ts +1 -1
  57. package/dist/controller/folioEditor.d.ts +1 -1
  58. package/dist/controller/fontReadiness.d.ts +70 -3
  59. package/dist/controller/fontReadiness.js +100 -10
  60. package/dist/controller/headerFooterEditorManager.js +6 -1
  61. package/dist/controller/layoutPipeline.d.ts +12 -10
  62. package/dist/controller/layoutPipeline.js +62 -9
  63. package/dist/controller/layoutRunOptions.d.ts +24 -0
  64. package/dist/controller/layoutRunOptions.js +17 -0
  65. package/dist/controller/layoutScheduler.d.ts +19 -14
  66. package/dist/controller/layoutScheduler.js +15 -12
  67. package/dist/controller/layoutSession.d.ts +26 -2
  68. package/dist/controller/layoutSession.js +1 -1
  69. package/dist/controller/noteEditorManager.js +8 -3
  70. package/dist/document-operations.d.ts +11 -2
  71. package/dist/document-operations.js +230 -21
  72. package/dist/docx/altChunk.d.ts +8 -0
  73. package/dist/docx/altChunk.js +17 -0
  74. package/dist/docx/attributeRemainder.d.ts +1 -1
  75. package/dist/docx/blockContentParser.d.ts +64 -1
  76. package/dist/docx/blockContentParser.js +17 -4
  77. package/dist/docx/blockCustomXmlShell.d.ts +7 -0
  78. package/dist/docx/blockCustomXmlShell.js +14 -0
  79. package/dist/docx/blockPlainText.js +7 -0
  80. package/dist/docx/commentAnchorIndex.js +1 -1
  81. package/dist/docx/commentRangeIntegrity.js +8 -3
  82. package/dist/docx/commentReferenceNormalization.js +1 -1
  83. package/dist/docx/commentReplyMarkers.js +20 -1
  84. package/dist/docx/compatibility.d.ts +1 -1
  85. package/dist/docx/compatibility.js +34 -13
  86. package/dist/docx/containerChildren.gen.d.ts +1 -1
  87. package/dist/docx/containerChildren.gen.js +1 -0
  88. package/dist/docx/documentParser.js +12 -12
  89. package/dist/docx/drawingIdNormalization.d.ts +3 -1
  90. package/dist/docx/drawingIdNormalization.js +6 -1
  91. package/dist/docx/ensureParaIds.js +119 -97
  92. package/dist/docx/footnoteParser.js +8 -4
  93. package/dist/docx/headerFooterParser.js +3 -3
  94. package/dist/docx/headerFooterReferenceNormalization.d.ts +20 -1
  95. package/dist/docx/headerFooterReferenceNormalization.js +52 -27
  96. package/dist/docx/hyperlinkParser.d.ts +1 -1
  97. package/dist/docx/index.d.ts +1 -1
  98. package/dist/docx/listNumberingInstances.d.ts +59 -0
  99. package/dist/docx/listNumberingInstances.js +249 -0
  100. package/dist/docx/normalizeBaseDirection.js +2 -1
  101. package/dist/docx/opaqueCarrier.d.ts +7 -0
  102. package/dist/docx/opaqueCarrier.js +38 -0
  103. package/dist/docx/packageParts.js +4 -1
  104. package/dist/docx/paragraphParser.js +91 -24
  105. package/dist/docx/paragraphPropertySource.d.ts +4 -2
  106. package/dist/docx/paragraphPropertySource.js +7 -1
  107. package/dist/docx/paragraphTraversal.js +1 -0
  108. package/dist/docx/parseWarningMessage.js +4 -1
  109. package/dist/docx/parser.js +173 -20
  110. package/dist/docx/rezip.js +27 -5
  111. package/dist/docx/sdtProperties.js +1 -1
  112. package/dist/docx/sdtPropertiesPatch.js +3 -0
  113. package/dist/docx/selectiveSave.js +16 -9
  114. package/dist/docx/selectiveXmlPatch.js +106 -37
  115. package/dist/docx/serializer/blockCustomXmlSerializer.d.ts +5 -0
  116. package/dist/docx/serializer/blockCustomXmlSerializer.js +4 -0
  117. package/dist/docx/serializer/documentSerializer.js +2 -0
  118. package/dist/docx/serializer/headerFooterSerializer.js +2 -0
  119. package/dist/docx/serializer/noteSerializer.js +3 -1
  120. package/dist/docx/serializer/paragraphSerializer.js +33 -9
  121. package/dist/docx/serializer/tableSerializer.js +25 -38
  122. package/dist/docx/server/createBilingualDocument.js +3 -3
  123. package/dist/docx/tableParser.d.ts +157 -1
  124. package/dist/docx/tableParser.js +89 -10
  125. package/dist/docx/textBoxParser.js +1 -1
  126. package/dist/docx/unzip.d.ts +2 -1
  127. package/dist/docx/unzip.js +1 -1
  128. package/dist/docx/wordprocessingPrefixes.d.ts +78 -0
  129. package/dist/docx/wordprocessingPrefixes.js +286 -0
  130. package/dist/generated/text_shaper.js +26 -0
  131. package/dist/generated/text_shaper_bg.wasm +0 -0
  132. package/dist/i18n/messages/catalogs.gen.d.ts +136 -0
  133. package/dist/i18n/messages/catalogs.gen.js +153 -17
  134. package/dist/i18n/messages/messages.gen.d.ts +8 -0
  135. package/dist/internal/compare/inline-presentation.d.ts +2 -1
  136. package/dist/internal/compare/inline-presentation.js +11 -10
  137. package/dist/internal/headlessRevisionResolutionGuard.d.ts +2 -8
  138. package/dist/internal/headlessRevisionResolutionGuard.js +2 -11
  139. package/dist/internal/indexedPositionMap.d.ts +11 -0
  140. package/dist/internal/indexedPositionMap.js +42 -0
  141. package/dist/internal/pageBreakRunSourceDescendantIndex.js +2 -1
  142. package/dist/internal/revisionResolutionEdits.d.ts +13 -0
  143. package/dist/internal/revisionResolutionEdits.js +19 -0
  144. package/dist/internal/revisionResolutionInline.d.ts +24 -0
  145. package/dist/internal/{headlessRevisionResolution.js → revisionResolutionInline.js} +61 -43
  146. package/dist/internal/revisionResolutionStep.d.ts +34 -0
  147. package/dist/internal/revisionResolutionStep.js +282 -0
  148. package/dist/internal/revisionResolutionTracking.d.ts +11 -0
  149. package/dist/internal/revisionResolutionTracking.js +124 -0
  150. package/dist/internal/wholeStoryRevisionResolution.d.ts +56 -0
  151. package/dist/internal/wholeStoryRevisionResolution.js +530 -0
  152. package/dist/layout-bridge/convert/markupViewFlow.d.ts +20 -0
  153. package/dist/layout-bridge/convert/markupViewFlow.js +123 -0
  154. package/dist/layout-bridge/convert/tableConversion.js +1 -1
  155. package/dist/layout-bridge/convert/toFlowBlocks.js +10 -1
  156. package/dist/layout-engine/layoutInstrumentation.d.ts +9 -1
  157. package/dist/layout-engine/layoutInstrumentation.js +4 -1
  158. package/dist/layout-engine/measure/cache.d.ts +18 -1
  159. package/dist/layout-engine/measure/cache.js +31 -1
  160. package/dist/layout-engine/measure/font-metrics.worker.js +3 -7
  161. package/dist/layout-engine/measure/measureContainer.js +7 -47
  162. package/dist/layout-engine/measure/measureHelpers.d.ts +6 -1
  163. package/dist/layout-engine/measure/measureHelpers.js +22 -2
  164. package/dist/layout-engine/measure/measureWorker.js +2 -3
  165. package/dist/layout-engine/measure/measureWorkerProtocol.d.ts +6 -7
  166. package/dist/layout-engine/measure/measureWorkerProtocol.js +1 -2
  167. package/dist/layout-engine/types.d.ts +6 -0
  168. package/dist/layout-painter/renderPage.js +1 -0
  169. package/dist/layout-painter/renderParagraph.js +1 -1
  170. package/dist/managers/ContextMenuManager.js +5 -2
  171. package/dist/managers/types.d.ts +3 -0
  172. package/dist/markdown/fromMarkdown.js +2 -1
  173. package/dist/markdown/internals.d.ts +11 -1
  174. package/dist/markdown/internals.js +24 -3
  175. package/dist/markdown/renderBlock.js +39 -9
  176. package/dist/markdown/renderParagraph.d.ts +13 -1
  177. package/dist/markdown/renderParagraph.js +85 -38
  178. package/dist/markdown/renderRuns.js +3 -10
  179. package/dist/markdown/renderTable.js +24 -12
  180. package/dist/markdown/trailers.js +2 -2
  181. package/dist/markdown/types.d.ts +29 -7
  182. package/dist/paged-layout/incrementalMeasure.d.ts +1 -2
  183. package/dist/paged-layout/incrementalMeasure.js +1 -9
  184. package/dist/paged-layout/transactionDirtyRange.d.ts +1 -3
  185. package/dist/paged-layout/transactionDirtyRange.js +29 -41
  186. package/dist/panel-layout.d.ts +83 -0
  187. package/dist/panel-layout.js +96 -0
  188. package/dist/prosemirror/anchoredTextBoxes.d.ts +37 -0
  189. package/dist/prosemirror/anchoredTextBoxes.js +156 -0
  190. package/dist/prosemirror/attrs/index.d.ts +6 -2
  191. package/dist/prosemirror/attrs/index.js +172 -1
  192. package/dist/prosemirror/commands/comments.d.ts +7 -8
  193. package/dist/prosemirror/commands/comments.js +241 -207
  194. package/dist/prosemirror/commands/contentControls.d.ts +1 -1
  195. package/dist/prosemirror/commands/formatPainter.d.ts +1 -1
  196. package/dist/prosemirror/commands/formatting.d.ts +1 -1
  197. package/dist/prosemirror/commands/index.d.ts +2 -2
  198. package/dist/prosemirror/commands/index.js +2 -2
  199. package/dist/prosemirror/commands/paragraph.d.ts +7 -1
  200. package/dist/prosemirror/commands/paragraph.js +9 -1
  201. package/dist/prosemirror/commands/paragraphBookmarkJoin.d.ts +13 -0
  202. package/dist/prosemirror/commands/paragraphBookmarkJoin.js +22 -0
  203. package/dist/prosemirror/commands/pastePlainText.d.ts +1 -1
  204. package/dist/prosemirror/commands/resolveAllTableChanges.d.ts +40 -0
  205. package/dist/prosemirror/commands/resolveAllTableChanges.js +615 -0
  206. package/dist/prosemirror/commands/resolveNodePropertyChangeAttrs.d.ts +15 -0
  207. package/dist/prosemirror/commands/resolveNodePropertyChangeAttrs.js +46 -0
  208. package/dist/prosemirror/commands/resolveParagraphProperties.d.ts +16 -0
  209. package/dist/prosemirror/commands/resolveParagraphProperties.js +59 -0
  210. package/dist/prosemirror/commands/sectionBreak.js +2 -1
  211. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +8 -2
  212. package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
  213. package/dist/prosemirror/contentControlRevisions.d.ts +1 -1
  214. package/dist/prosemirror/conversion/fromProseDoc.js +181 -31
  215. package/dist/prosemirror/conversion/toProseDoc.js +39 -4
  216. package/dist/prosemirror/emptyFieldResultRuns.d.ts +18 -0
  217. package/dist/prosemirror/emptyFieldResultRuns.js +33 -0
  218. package/dist/prosemirror/extensions/ExtensionManager.d.ts +1 -1
  219. package/dist/prosemirror/extensions/ExtensionManager.js +3 -1
  220. package/dist/prosemirror/extensions/StarterKit.js +3 -1
  221. package/dist/prosemirror/extensions/core/DocExtension.js +1 -1
  222. package/dist/prosemirror/extensions/core/ParagraphExtension.js +12 -0
  223. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +3 -0
  224. package/dist/prosemirror/extensions/features/ListExtension.js +161 -123
  225. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +19 -2
  226. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +33 -20
  227. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +31 -2
  228. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +270 -99
  229. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +3 -2
  230. package/dist/prosemirror/extensions/features/pasteCleanup.js +2 -1
  231. package/dist/prosemirror/extensions/features/pastedHtmlLists.d.ts +12 -0
  232. package/dist/prosemirror/extensions/features/pastedHtmlLists.js +126 -0
  233. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +21 -5
  234. package/dist/prosemirror/extensions/marks/RunIdentityExtension.d.ts +21 -18
  235. package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +37 -17
  236. package/dist/prosemirror/extensions/marks/markUtils.d.ts +7 -2
  237. package/dist/prosemirror/extensions/marks/markUtils.js +8 -2
  238. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.d.ts +10 -0
  239. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +35 -0
  240. package/dist/prosemirror/extensions/nodes/BlockCustomXmlExtension.d.ts +6 -0
  241. package/dist/prosemirror/extensions/nodes/BlockCustomXmlExtension.js +52 -0
  242. package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +3 -3
  243. package/dist/prosemirror/extensions/nodes/FieldExtension.js +50 -15
  244. package/dist/prosemirror/extensions/nodes/PreservedBlockExtension.js +4 -1
  245. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  246. package/dist/prosemirror/extensions/nodes/TableExtension.js +19 -18
  247. package/dist/prosemirror/extensions/types.d.ts +4 -1
  248. package/dist/prosemirror/findReplaceSelection.js +1 -1
  249. package/dist/prosemirror/index.d.ts +3 -2
  250. package/dist/prosemirror/index.js +3 -2
  251. package/dist/prosemirror/indexedNodeLookup.d.ts +17 -0
  252. package/dist/prosemirror/indexedNodeLookup.js +103 -0
  253. package/dist/prosemirror/listAutoformatMarkers.d.ts +19 -0
  254. package/dist/prosemirror/listAutoformatMarkers.js +98 -0
  255. package/dist/prosemirror/listInstanceReferences.d.ts +12 -0
  256. package/dist/prosemirror/listInstanceReferences.js +72 -0
  257. package/dist/prosemirror/listLabels.d.ts +24 -0
  258. package/dist/prosemirror/listLabels.js +111 -0
  259. package/dist/prosemirror/listNumbering.d.ts +110 -0
  260. package/dist/prosemirror/listNumbering.js +345 -0
  261. package/dist/prosemirror/listRenderingAttrs.d.ts +11 -2
  262. package/dist/prosemirror/listRenderingAttrs.js +8 -1
  263. package/dist/prosemirror/markupViewProjection.d.ts +95 -0
  264. package/dist/prosemirror/markupViewProjection.js +83 -0
  265. package/dist/prosemirror/numberingAttr.d.ts +9 -1
  266. package/dist/prosemirror/numberingAttr.js +9 -1
  267. package/dist/prosemirror/outlineLevelAttr.js +1 -1
  268. package/dist/prosemirror/paragraphIndentation.d.ts +10 -1
  269. package/dist/prosemirror/paragraphIndentation.js +29 -7
  270. package/dist/prosemirror/paragraphMarkJoin.d.ts +19 -0
  271. package/dist/prosemirror/paragraphMarkJoin.js +24 -0
  272. package/dist/prosemirror/plugins/createDocScanPlugin.d.ts +1 -1
  273. package/dist/prosemirror/plugins/documentNumbering.d.ts +27 -1
  274. package/dist/prosemirror/plugins/documentNumbering.js +97 -11
  275. package/dist/prosemirror/plugins/documentStyles.d.ts +10 -1
  276. package/dist/prosemirror/plugins/documentStyles.js +14 -1
  277. package/dist/prosemirror/plugins/suggestionMode.d.ts +16 -2
  278. package/dist/prosemirror/plugins/suggestionMode.js +116 -52
  279. package/dist/prosemirror/plugins/templateDirectives.d.ts +1 -1
  280. package/dist/prosemirror/plugins/transactionInvariants.d.ts +32 -0
  281. package/dist/prosemirror/plugins/transactionInvariants.js +86 -0
  282. package/dist/prosemirror/positionSweep.d.ts +24 -0
  283. package/dist/prosemirror/positionSweep.js +222 -0
  284. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +1 -1
  285. package/dist/prosemirror/rejoinRunCarriers.d.ts +1 -1
  286. package/dist/prosemirror/replacedAnnotations.d.ts +33 -1
  287. package/dist/prosemirror/replacedAnnotations.js +170 -1
  288. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +1 -1
  289. package/dist/prosemirror/runStyleFormatting.d.ts +7 -1
  290. package/dist/prosemirror/runStyleFormatting.js +13 -7
  291. package/dist/prosemirror/schema/index.d.ts +2 -2
  292. package/dist/prosemirror/schema/index.js +3 -0
  293. package/dist/prosemirror/schema/nodes.d.ts +42 -11
  294. package/dist/prosemirror/sectionMarkEdits.d.ts +20 -0
  295. package/dist/prosemirror/sectionMarkEdits.js +59 -0
  296. package/dist/prosemirror/storyListNumbering.d.ts +17 -0
  297. package/dist/prosemirror/storyListNumbering.js +102 -0
  298. package/dist/prosemirror/tableGridMutation.d.ts +48 -2
  299. package/dist/prosemirror/tableGridMutation.js +106 -1
  300. package/dist/prosemirror/validation.js +5 -1
  301. package/dist/redline.js +93 -9
  302. package/dist/render-dom/BodySelectionOverlay.d.ts +4 -1
  303. package/dist/render-dom/BodySelectionOverlay.js +3 -2
  304. package/dist/shaping/shaper.d.ts +47 -4
  305. package/dist/shaping/shaper.js +46 -15
  306. package/dist/text-shaping.d.ts +4 -0
  307. package/dist/text-shaping.js +4 -0
  308. package/dist/utils/findReplace.js +2 -2
  309. package/dist/utils/mergeDocumentContent.js +1 -1
  310. package/dist/utils/noteReferenceLabels.d.ts +36 -0
  311. package/dist/utils/noteReferenceLabels.js +32 -0
  312. package/dist/utils/replaceText.js +1 -1
  313. package/dist/version-comparison.d.ts +1 -0
  314. package/dist/version-comparison.js +1 -0
  315. package/package.json +8 -2
  316. package/dist/internal/headlessRevisionResolution.d.ts +0 -23
@@ -1,38 +1,49 @@
1
1
  import { paragraphNumberingReference, paragraphNumberingReferenceId } from "../docx/numberingReference.js";
2
2
  import { formattingEquals } from "../docx/runConsolidator.js";
3
+ import { isStyleSourcedParagraphNumbering } from "../internal/paragraphFormattingSerialization.js";
4
+ import { deleteTextBoxesAnchoredAt, droppedTextBoxAnchorIds, endOfParagraphWithItsTextBoxes, gatherTextBoxesAfterParagraph, textBoxAnchorIdOf, textBoxAnchorIdsBetween } from "../prosemirror/anchoredTextBoxes.js";
3
5
  import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
4
6
  import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
5
7
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
6
8
  import { encloseWholeControls } from "../prosemirror/contentControlRevisions.js";
7
9
  import { requestDeterministicParaIds } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
8
10
  import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
11
+ import "../prosemirror/extensions/nodes/TextBoxAnchorExtension.js";
9
12
  import { CLEARED_LIST_RENDERING_ATTRS } from "../prosemirror/listMarker.js";
10
- import { paragraphNumberingAttr, readParagraphNumberingAttr } from "../prosemirror/numberingAttr.js";
13
+ import { paragraphNumberingAttr, readParagraphNumberingAttr, removedNumberingAttr } from "../prosemirror/numberingAttr.js";
11
14
  import { directParagraphAlignment } from "../prosemirror/paragraphAlignment.js";
15
+ import { styleResolvedParagraphFormatting } from "../prosemirror/paragraphFormattingProvenance.js";
12
16
  import { directParagraphIndentation, paragraphIndentationAttrPatch, paragraphIndentationEqual, paragraphIndentationFromFormatting, withDirectParagraphIndentation } from "../prosemirror/paragraphIndentation.js";
17
+ import { joinAtParagraphMark } from "../prosemirror/paragraphMarkJoin.js";
13
18
  import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
14
- import { getDocumentNumbering } from "../prosemirror/plugins/documentNumbering.js";
15
- import { getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
16
- import { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, hasReplacedAnnotations, inheritedReplacementMarks, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
19
+ import { getDocumentNumbering, getDocumentNumberingInstanceIds } from "../prosemirror/plugins/documentNumbering.js";
20
+ import { getDocumentStyleDefinitions, getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
21
+ import { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentsAcrossBlockBoundary, commentsForTrackedInsertion, hasReplacedAnnotations, inheritedReplacementMarks, placeTrackedInsertion, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
17
22
  import { applyMarksToRunFormattingRepresentation, runFormattingInlineControlNodeName, selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
23
+ import { RUN_FORMATTING_MARK_NAMES } from "../prosemirror/runFormattingMarkNames.js";
18
24
  import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
19
- import { paragraphRunStyleContext, paragraphRunStyleContextAt } from "../prosemirror/runStyleFormatting.js";
20
- import { listLevelAttrPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
25
+ import { paragraphRunStyleContext, paragraphRunStyleContextAt, resolveParagraphBodyRunFormatting } from "../prosemirror/runStyleFormatting.js";
26
+ import { sectionPropertiesOf } from "../prosemirror/sectionCarrier.js";
27
+ import { listAttrsFromNumbering, listLevelAttrPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
21
28
  import { isZeroWidthAnchor } from "../prosemirror/zeroWidthAnchors.js";
22
29
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
30
+ import { BatchClaims } from "./batch-claims.js";
23
31
  import { stripBlockIdentityAttrs } from "./block-identity.js";
32
+ import { describeCharacterSplit } from "./character-boundaries.js";
24
33
  import { buildCleanBlockText, resolveCleanTextRange } from "./clean-text.js";
25
34
  import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers } from "./inline-emphasis.js";
26
- import { applyTextChanges, changesFromSegments, planTextChanges, widenChangesToAtomicSpans } from "./minimal-replacement.js";
27
- import { hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
35
+ import { applyTextChanges, changesFromSegments, keepAtomicSpans, planTextChanges, widenChangesToAtomicSpans } from "./minimal-replacement.js";
36
+ import { concreteListReference, resolveNewListOperations } from "./newListNumbering.js";
37
+ import { cutsIntoNoteReference, noteReferenceEditsAllowed, noteReferenceOf, noteReferenceSpansWithin, planChangesAroundNoteReferences, segmentsAroundNoteReferences } from "./note-references.js";
38
+ import { collectNoteReferenceLabels, hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
28
39
  import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
29
40
  import { planTableMutations } from "./table-mutation-plan.js";
30
- import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
41
+ import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, describeUnplacedCellTexts, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
31
42
  import { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell } from "./table-targets.js";
32
43
  import { tableFromTemplate } from "./table-template.js";
33
44
  import { wordDiffSessionFromOptions } from "./word-diff.js";
34
45
  import { panic } from "better-result";
35
- import { Fragment } from "prosemirror-model";
46
+ import { Fragment, Mark } from "prosemirror-model";
36
47
  import { TableMap } from "prosemirror-tables";
37
48
  import { canJoin, canSplit } from "prosemirror-transform";
38
49
  //#region src/ai-edits/apply.ts
@@ -109,6 +120,12 @@ const SUGGESTED_SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
109
120
  "insertTableColumn",
110
121
  "deleteTableColumn"
111
122
  ]);
123
+ /** Operations that only add marks: they move no position of the document. */
124
+ const ANNOTATION_OPERATION_TYPES = /* @__PURE__ */ new Set([
125
+ "formatRange",
126
+ "commentOnRange",
127
+ "commentOnBlock"
128
+ ]);
112
129
  const REPLACE_BLOCK_IMPACT = {
113
130
  text: {
114
131
  changesStyle: false,
@@ -167,6 +184,7 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
167
184
  const styleChanged = nextStyleId !== void 0 && (attrs.styleId ?? null) !== nextStyleId;
168
185
  if (styleChanged) {
169
186
  patch["styleId"] = nextStyleId;
187
+ patch["_resolvedFormatting"] = styleResolvedParagraphFormatting(resolvedFormattingFromStyle);
170
188
  patch["alignmentFromStyle"] = resolvedFormattingFromStyle?.alignment;
171
189
  if (properties.alignment === void 0) patch["alignment"] = currentDirectAlignment ?? resolvedFormattingFromStyle?.alignment ?? null;
172
190
  if (properties.spacing === void 0) Object.assign(patch, paragraphSpacingAttrPatch({
@@ -182,25 +200,42 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
182
200
  else originalFormatting.styleId = nextStyleId;
183
201
  if (currentDirectAlignment !== void 0) originalFormatting.alignment = currentDirectAlignment;
184
202
  originalFormattingChanged = true;
203
+ patch["outlineLevel"] = attrs._originalFormatting?.outlineLevel ?? resolvedFormattingFromStyle?.outlineLevel ?? null;
204
+ Object.assign(patch, restyledListAttrs({
205
+ attrs: { ...attrs },
206
+ styleNumbering: resolvedFormattingFromStyle?.numPr,
207
+ removesNumbering: false,
208
+ numbering
209
+ }));
185
210
  }
211
+ if (properties.outlineLevel !== void 0) {
212
+ const directOutlineLevel = properties.outlineLevel ?? void 0;
213
+ originalFormatting ??= {};
214
+ if (directOutlineLevel === void 0) Reflect.deleteProperty(originalFormatting, "outlineLevel");
215
+ else originalFormatting.outlineLevel = directOutlineLevel;
216
+ patch["outlineLevel"] = directOutlineLevel ?? resolvedFormattingFromStyle?.outlineLevel ?? null;
217
+ originalFormattingChanged = true;
218
+ }
219
+ const numPrFromStyle = styleChanged ? readParagraphNumberingAttr(patch["numPrFromStyle"]) : attrs.numPrFromStyle;
186
220
  if (properties.numbering !== void 0) if (properties.numbering === null) {
187
- patch["numPr"] = null;
221
+ patch["numPr"] = removedNumberingAttr(numPrFromStyle);
188
222
  Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
189
223
  } else {
224
+ const listReference = concreteListReference(properties.numbering);
190
225
  Object.assign(patch, listLevelAttrPatch(attrs, {
191
- numId: properties.numbering.numId,
192
- ilvl: properties.numbering.level
226
+ numId: listReference.numId,
227
+ ilvl: listReference.level
193
228
  }, numbering));
194
229
  if (properties.listLevel === null) {
195
- patch["numPr"] = paragraphNumberingAttr(paragraphNumberingReference({ numId: properties.numbering.numId }));
230
+ patch["numPr"] = paragraphNumberingAttr(paragraphNumberingReference({ numId: listReference.numId }));
196
231
  patch["numPrFromStyle"] = null;
197
232
  }
198
233
  }
199
234
  else if (properties.listLevel !== void 0) if (properties.listLevel === null) {
200
- patch["numPr"] = null;
235
+ patch["numPr"] = removedNumberingAttr(numPrFromStyle);
201
236
  Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
202
237
  } else {
203
- const numId = paragraphNumberingReferenceId(readParagraphNumberingAttr(node.attrs["numPr"]) ?? void 0);
238
+ const numId = paragraphNumberingReferenceId(readParagraphNumberingAttr(styleChanged ? patch["numPr"] : node.attrs["numPr"]) ?? void 0);
204
239
  if (numId === void 0) {
205
240
  patch["numPr"] = paragraphNumberingAttr({
206
241
  kind: "levelOnly",
@@ -242,6 +277,24 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
242
277
  return Object.keys(patch).length > 0 ? patch : null;
243
278
  };
244
279
  const REVISION_MARK_NAMES = /* @__PURE__ */ new Set(["insertion", "deletion"]);
280
+ const hasOrdinaryTextRevision = (block) => {
281
+ let found = false;
282
+ block.descendants((node) => {
283
+ if (node.marks.some((mark) => REVISION_MARK_NAMES.has(mark.type.name) && mark.attrs["provenance"] !== "suggested")) {
284
+ found = true;
285
+ return false;
286
+ }
287
+ return true;
288
+ });
289
+ return found;
290
+ };
291
+ const hasOrdinaryParagraphMarkRevision = (block) => {
292
+ const mark = block.attrs["pPrMark"];
293
+ if (typeof mark !== "object" || mark === null || !("info" in mark)) return false;
294
+ const info = mark.info;
295
+ if (typeof info !== "object" || info === null) return false;
296
+ return !("provenance" in info) || info.provenance !== "suggested";
297
+ };
245
298
  /**
246
299
  * Strip insertion and deletion marks from the zero-width anchors the batch
247
300
  * touched.
@@ -251,10 +304,12 @@ const REVISION_MARK_NAMES = /* @__PURE__ */ new Set(["insertion", "deletion"]);
251
304
  * must keep its existing revision ownership. Marking is by range everywhere;
252
305
  * the guard lives here once rather than at every call site.
253
306
  */
254
- const withoutRevisionsOnZeroWidthAnchors = (tr, batchRevisionIds) => {
307
+ const withoutRevisionsOnZeroWidthAnchors = (tr, batchRevisionIds, deletedTextBoxAnchorIds) => {
255
308
  const anchors = [];
256
309
  tr.doc.descendants((node, pos) => {
257
310
  if (!isZeroWidthAnchor(node)) return true;
311
+ const textBoxAnchorId = textBoxAnchorIdOf(node);
312
+ if (textBoxAnchorId !== void 0 && deletedTextBoxAnchorIds.has(textBoxAnchorId)) return false;
258
313
  const marks = node.marks.filter(({ attrs, type }) => REVISION_MARK_NAMES.has(type.name) && batchRevisionIds.has(attrs["revisionId"]));
259
314
  if (marks.length > 0) anchors.push({
260
315
  from: pos,
@@ -272,7 +327,9 @@ const withoutRevisionsOnZeroWidthAnchors = (tr, batchRevisionIds) => {
272
327
  *
273
328
  * Skips what is already deleted, so an earlier revision's run is not marked
274
329
  * twice, and skips zero-width anchors because the operation did not create or
275
- * delete them; their existing revision ownership must remain unchanged.
330
+ * delete them; their existing revision ownership must remain unchanged. A
331
+ * text box's anchor is the exception: it stands for the box's drawing run,
332
+ * which is the paragraph's content, and the box follows it.
276
333
  */
277
334
  const undeletedContentAtomRanges = (tr, blockFrom) => {
278
335
  const position = tr.mapping.map(blockFrom);
@@ -280,7 +337,7 @@ const undeletedContentAtomRanges = (tr, blockFrom) => {
280
337
  if (!block?.isTextblock) return [];
281
338
  const ranges = [];
282
339
  block.forEach((child, offset) => {
283
- if (child.isText || isZeroWidthAnchor(child) || child.marks.some(({ type }) => type.name === "deletion")) return;
340
+ if (child.isText || isZeroWidthAnchor(child) && child.type.name !== "textBoxAnchor" || child.marks.some(({ type }) => type.name === "deletion")) return;
284
341
  const from = position + 1 + offset;
285
342
  ranges.push({
286
343
  from,
@@ -390,6 +447,12 @@ const applyBlockParagraphProperties = ({ tr, position, node, properties, styleRe
390
447
  revisionId: null,
391
448
  revisionIds: []
392
449
  };
450
+ const nextStyleId = properties.styleId === void 0 ? attrs.styleId : properties.styleId;
451
+ if ((attrs.styleId ?? null) !== (nextStyleId ?? null)) patch["defaultTextFormatting"] = resolveParagraphBodyRunFormatting({
452
+ styleId: properties.styleId ?? void 0,
453
+ tableRunFormatting: attrs._tableRunFormatting,
454
+ styleResolver
455
+ }).defaultFormatting ?? null;
393
456
  const changeInfo = revisionInfo?.();
394
457
  const change = changeInfo ? {
395
458
  type: "paragraphPropertyChange",
@@ -413,13 +476,46 @@ const applyBlockParagraphProperties = ({ tr, position, node, properties, styleRe
413
476
  tr,
414
477
  revisionIds: []
415
478
  };
479
+ const styled = bridgeResult.tr.setNodeMarkup(position, void 0, nextAttrs);
416
480
  return {
417
- tr: bridgeResult.tr.setNodeMarkup(position, void 0, nextAttrs),
481
+ tr: (attrs.styleId ?? null) === (nextStyleId ?? null) ? styled : rebaseParagraphRuns({
482
+ tr: styled,
483
+ position,
484
+ previous: node,
485
+ styleResolver
486
+ }),
418
487
  changed: true,
419
488
  revisionId: change?.info.id ?? null,
420
489
  revisionIds: change ? [change.info.id, ...bridgeResult.revisionIds] : []
421
490
  };
422
491
  };
492
+ /** Rebuild rendered marks after the paragraph's style cascade changes. */
493
+ const rebaseParagraphRuns = ({ tr, position, previous, target, styleResolver }) => {
494
+ const paragraph = tr.doc.nodeAt(position);
495
+ if (!paragraph) return tr;
496
+ const sourceContext = paragraphRunStyleContext(previous, styleResolver);
497
+ const targetContext = paragraphRunStyleContext(target ?? paragraph, styleResolver);
498
+ const representations = selectRunFormattingCarrierRepresentations({
499
+ doc: tr.doc,
500
+ from: position + 1,
501
+ to: position + paragraph.nodeSize - 1
502
+ });
503
+ for (const representation of representations) applyMarksToRunFormattingRepresentation({
504
+ tr,
505
+ representation,
506
+ marks: reconcileRunFormattingMarks({
507
+ authoredFormatting: readAuthoredRunFormatting({
508
+ context: sourceContext,
509
+ marks: representation.node.marks,
510
+ styleResolver
511
+ }),
512
+ context: targetContext,
513
+ node: representation.node,
514
+ styleResolver
515
+ })
516
+ });
517
+ return tr;
518
+ };
423
519
  const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) => {
424
520
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return {
425
521
  tr,
@@ -449,6 +545,11 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
449
545
  revisionId: null,
450
546
  revisionIds: []
451
547
  };
548
+ if ((attrs.styleId ?? null) !== item.operation.styleId) patch["defaultTextFormatting"] = resolveParagraphBodyRunFormatting({
549
+ styleId: item.operation.styleId ?? void 0,
550
+ tableRunFormatting: attrs._tableRunFormatting,
551
+ styleResolver
552
+ }).defaultFormatting ?? null;
452
553
  const existing = attrs._propertyChanges;
453
554
  const changeInfo = revisionInfo?.();
454
555
  const change = changeInfo ? {
@@ -472,8 +573,14 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
472
573
  tr,
473
574
  revisionIds: []
474
575
  };
576
+ const styled = bridgeResult.tr.setNodeMarkup(blockPosition, void 0, nextAttrs);
475
577
  return {
476
- tr: bridgeResult.tr.setNodeMarkup(blockPosition, void 0, nextAttrs),
578
+ tr: (attrs.styleId ?? null) === item.operation.styleId ? styled : rebaseParagraphRuns({
579
+ tr: styled,
580
+ position: blockPosition,
581
+ previous: block,
582
+ styleResolver
583
+ }),
477
584
  revisionId: change?.info.id ?? null,
478
585
  revisionIds: change ? [change.info.id, ...bridgeResult.revisionIds] : []
479
586
  };
@@ -747,10 +854,14 @@ let revisionIdCursor = Date.now() * 1e3;
747
854
  */
748
855
  const collectLiveBlocksByHash = (doc) => {
749
856
  const byHash = /* @__PURE__ */ new Map();
857
+ const noteReferences = collectNoteReferenceLabels(doc);
750
858
  doc.descendants((node, pos) => {
751
859
  if (isHiddenTableRow(node)) return false;
752
860
  if (!node.isTextblock) return true;
753
- const cleanText = buildCleanBlockText(node, pos).text;
861
+ const cleanText = buildCleanBlockText(node, pos, {
862
+ fieldResults: "text",
863
+ noteReferences
864
+ }).text;
754
865
  const hash = hashFolioAIBlockText(normalizeFolioAIBlockText(cleanText));
755
866
  const bucket = byHash.get(hash) ?? [];
756
867
  bucket.push({
@@ -804,6 +915,190 @@ const getTableMutationPlanTarget = (item) => {
804
915
  return { type: "none" };
805
916
  };
806
917
  /**
918
+ * `insertion`, resolved against the document as it was read, placed through
919
+ * what `tr` has already changed. The batch runs from the end backwards, so
920
+ * nothing it has applied yet sits before the new row; mapped all the same, so
921
+ * that stays true of the positions the row is written at and not only of the
922
+ * order they are visited in. A row another operation of the batch inserted
923
+ * at the same boundary is the later one in input order, so the new row goes
924
+ * before it.
925
+ */
926
+ const mapTableRowInsertion = (tr, insertion) => {
927
+ const tableStart = tr.mapping.map(insertion.tableStart, -1);
928
+ return {
929
+ ...insertion,
930
+ tableStart,
931
+ rowPosition: tr.mapping.map(insertion.rowPosition, -1),
932
+ rowspanUpdates: insertion.rowspanUpdates.map((update) => tr.mapping.map(insertion.tableStart + update, 1) - tableStart)
933
+ };
934
+ };
935
+ /** The cells a column deletion removes: every cell standing in that column alone. */
936
+ const deletedColumnCellRanges = (doc, { tablePosition, columnIndex }) => {
937
+ const table = doc.nodeAt(tablePosition);
938
+ if (!table || table.type.spec["tableRole"] !== "table") return [];
939
+ const map = TableMap.get(table);
940
+ const tableStart = tablePosition + 1;
941
+ const ranges = [];
942
+ const seen = /* @__PURE__ */ new Set();
943
+ for (let row = 0; row < map.height; row++) {
944
+ const relative = map.map[row * map.width + columnIndex];
945
+ if (relative === void 0 || seen.has(relative)) continue;
946
+ seen.add(relative);
947
+ const rectangle = map.findCell(relative);
948
+ const cell = table.nodeAt(relative);
949
+ if (!cell || rectangle.left !== columnIndex || rectangle.right !== columnIndex + 1) continue;
950
+ ranges.push({
951
+ from: tableStart + relative,
952
+ to: tableStart + relative + cell.nodeSize
953
+ });
954
+ }
955
+ return ranges;
956
+ };
957
+ /** A row deletion also changes cells whose vertical merge began above it. */
958
+ const deletedRowAffectedRanges = ({ doc, deletion, row }) => {
959
+ const ranges = [{
960
+ from: deletion.rowPosition,
961
+ to: deletion.rowPosition + row.nodeSize
962
+ }];
963
+ const table = doc.nodeAt(deletion.tablePosition);
964
+ if (table?.type.spec["tableRole"] !== "table") return panic("A resolved row deletion lost its table", { position: deletion.tablePosition });
965
+ const map = TableMap.get(table);
966
+ const seen = /* @__PURE__ */ new Set();
967
+ for (let column = 0; column < map.width; column++) {
968
+ const relative = map.map[deletion.rowIndex * map.width + column];
969
+ if (relative === void 0 || seen.has(relative)) continue;
970
+ seen.add(relative);
971
+ const position = deletion.tableStart + relative;
972
+ if (position >= deletion.rowPosition) continue;
973
+ const cell = table.nodeAt(relative);
974
+ if (cell) ranges.push({
975
+ from: position,
976
+ to: position + cell.nodeSize
977
+ });
978
+ }
979
+ return ranges;
980
+ };
981
+ /** What `item` claims of the document its batch resolved against; see `batch-claims.ts`. */
982
+ const batchClaimOf = ({ item, doc, producesTrackedChanges }) => {
983
+ const block = item.blockFrom;
984
+ switch (item.operation.type) {
985
+ case "replaceInBlock":
986
+ case "replaceRange": return {
987
+ type: "text",
988
+ block,
989
+ from: item.from,
990
+ to: item.to
991
+ };
992
+ case "formatRange":
993
+ case "commentOnRange":
994
+ case "commentOnBlock": return {
995
+ type: "annotation",
996
+ block,
997
+ from: item.from,
998
+ to: item.to
999
+ };
1000
+ case "splitBlock": return {
1001
+ type: "split",
1002
+ block,
1003
+ from: item.from,
1004
+ to: item.to
1005
+ };
1006
+ case "replaceBlock": return item.replaceBlockImpact === "style" ? {
1007
+ type: "paragraphProperties",
1008
+ block
1009
+ } : {
1010
+ type: "rewriteBlock",
1011
+ block
1012
+ };
1013
+ case "setBlockParagraphProperties": return {
1014
+ type: "paragraphProperties",
1015
+ block
1016
+ };
1017
+ case "mergeBlockWithNext": return {
1018
+ type: "merge",
1019
+ block,
1020
+ next: item.blockTo,
1021
+ joinsNow: !producesTrackedChanges
1022
+ };
1023
+ case "deleteBlock": {
1024
+ const at = doc.resolve(block);
1025
+ const endsItsContainer = paragraphEndsItsContainer(at, item.blockNode.type.name);
1026
+ const keepsParagraph = producesTrackedChanges ? endsItsContainer : endsItsContainer && at.nodeBefore?.type.name !== item.blockNode.type.name;
1027
+ return {
1028
+ type: "deleteBlock",
1029
+ block,
1030
+ end: item.blockTo,
1031
+ keepsParagraph,
1032
+ removesNode: !producesTrackedChanges && !keepsParagraph,
1033
+ direct: !producesTrackedChanges
1034
+ };
1035
+ }
1036
+ case "insertAfterBlock":
1037
+ case "insertBeforeBlock":
1038
+ case "insertSignatureTable":
1039
+ case "insertTable": return {
1040
+ type: "insertion",
1041
+ at: item.from
1042
+ };
1043
+ case "deleteTable": {
1044
+ const deleted = item.deletedTable;
1045
+ return deleted ? {
1046
+ type: "tableRemoval",
1047
+ table: deleted.position,
1048
+ wholeTable: true,
1049
+ axis: "table",
1050
+ ranges: [{
1051
+ from: deleted.position,
1052
+ to: deleted.position + deleted.node.nodeSize
1053
+ }]
1054
+ } : { type: "unclaimed" };
1055
+ }
1056
+ case "deleteTableRow": {
1057
+ const deletion = item.tableRowDeletion;
1058
+ const row = deletion ? doc.nodeAt(deletion.rowPosition) : null;
1059
+ const table = deletion ? doc.nodeAt(deletion.tablePosition) : null;
1060
+ return deletion && row && table?.type.spec["tableRole"] === "table" ? {
1061
+ type: "tableRemoval",
1062
+ table: deletion.tablePosition,
1063
+ wholeTable: table.childCount === 1,
1064
+ axis: "row",
1065
+ ranges: table.childCount === 1 ? [{
1066
+ from: deletion.tablePosition,
1067
+ to: deletion.tablePosition + table.nodeSize
1068
+ }] : deletedRowAffectedRanges({
1069
+ doc,
1070
+ deletion,
1071
+ row
1072
+ })
1073
+ } : { type: "unclaimed" };
1074
+ }
1075
+ case "deleteTableColumn": {
1076
+ const deletion = item.tableColumnDeletion;
1077
+ return deletion ? {
1078
+ type: "tableRemoval",
1079
+ table: deletion.tablePosition,
1080
+ wholeTable: false,
1081
+ axis: "column",
1082
+ ranges: deletedColumnCellRanges(doc, deletion)
1083
+ } : { type: "unclaimed" };
1084
+ }
1085
+ case "insertTableRow":
1086
+ case "insertTableColumn":
1087
+ case "mergeTableCells":
1088
+ case "splitTableCell": {
1089
+ const target = getTableMutationPlanTarget(item);
1090
+ let axis = "cell";
1091
+ if (item.operation.type === "insertTableRow") axis = "row";
1092
+ if (item.operation.type === "insertTableColumn") axis = "column";
1093
+ return target.type === "none" ? { type: "unclaimed" } : {
1094
+ type: "tableStructure",
1095
+ table: target.tablePosition,
1096
+ axis
1097
+ };
1098
+ }
1099
+ }
1100
+ };
1101
+ /**
807
1102
  * The snapshot recorded an `hashOccurrenceCount` per anchor but
808
1103
  * not which ordinal within that bucket the block was — recompute
809
1104
  * on demand from the snapshot's anchor map. Stable iteration
@@ -956,6 +1251,79 @@ const isBatchableParagraphInsertion = (item, mode) => {
956
1251
  const isEmptyInsert = insertTexts.length === 1 && insertTexts[0]?.length === 0;
957
1252
  return !(mode === "tracked-changes" && item.operation.pageBreakBefore === true && isEmptyInsert);
958
1253
  };
1254
+ const insertBlocksInsideComments = ({ tr, at, blocks, carriedComments }) => {
1255
+ const spanning = carriedComments ?? commentsAcrossBlockBoundary(tr.doc, at);
1256
+ const fragment = Fragment.from(blocks);
1257
+ let next = tr.insert(at, fragment);
1258
+ for (const comment of spanning) next = next.addMark(at, at + fragment.size, comment);
1259
+ return next;
1260
+ };
1261
+ /** A paragraph mark that is a pending insertion (not a relocation's end). */
1262
+ const isInsertedPPrMark = (value) => typeof value === "object" && value !== null && "kind" in value && value.kind === "ins";
1263
+ const isSuggestedParagraphInsert = (paragraph) => typeof paragraph.attrs["_suggestedInsert"] === "object" && paragraph.attrs["_suggestedInsert"] !== null;
1264
+ /** A paragraph mark that is a pending deletion, or a relocation's source. */
1265
+ const isDeletedPPrMark = (value) => typeof value === "object" && value !== null && "kind" in value && (value.kind === "del" || value.kind === "moveFrom");
1266
+ const hasRevisionMarker = (value) => typeof value === "object" && value !== null && "revisionId" in value;
1267
+ const isDeletedCell = (cell) => {
1268
+ const marker = cell.attrs["cellMarker"];
1269
+ return typeof marker === "object" && marker !== null && "kind" in marker && marker.kind === "del";
1270
+ };
1271
+ /**
1272
+ * Whether accepting every change removes the paragraph at `blockFrom` whole:
1273
+ * its paragraph mark and all of its text pending deletion (`cleanText`, the
1274
+ * accepted reading, is empty), or the table row or cell holding it pending
1275
+ * deletion. A reader lists such a paragraph as a blank block.
1276
+ *
1277
+ * Word keeps a paragraph whose mark is deleted joined to the next one: text
1278
+ * typed into it stays inserted there, and accepting runs it into the next
1279
+ * paragraph (or, in a deleted row, takes it away with the row). An operation
1280
+ * that asks for the paragraph to read something cannot get that without
1281
+ * rejecting the deletion, which is the reviewer's call, not a side effect.
1282
+ */
1283
+ const isPendingDeletion = (doc, blockFrom, paragraph, cleanText) => {
1284
+ if (cleanText.length === 0 && isDeletedPPrMark(paragraph.attrs["pPrMark"])) return true;
1285
+ const $block = doc.resolve(blockFrom);
1286
+ for (let depth = $block.depth; depth > 0; depth -= 1) {
1287
+ const ancestor = $block.node(depth);
1288
+ const name = ancestor.type.name;
1289
+ if (name === "tableRow" && hasRevisionMarker(ancestor.attrs["trDel"])) return true;
1290
+ if ((name === "tableCell" || name === "tableHeader") && isDeletedCell(ancestor)) return true;
1291
+ }
1292
+ return false;
1293
+ };
1294
+ /** Whether every piece of the paragraph's content is pending inserted text. */
1295
+ const holdsOnlyInsertedContent = (paragraph) => {
1296
+ let whollyInserted = true;
1297
+ paragraph.descendants((node) => {
1298
+ if (!whollyInserted) return false;
1299
+ if (!node.isInline || isZeroWidthAnchor(node)) return true;
1300
+ whollyInserted = node.marks.some((mark) => mark.type.name === "insertion" && mark.attrs["moveKind"] !== "moveTo");
1301
+ return false;
1302
+ });
1303
+ return whollyInserted;
1304
+ };
1305
+ /**
1306
+ * How to take back the paragraph at `position` when it is wholly a pending
1307
+ * insertion (the break that introduced it inserted, and everything in it
1308
+ * inserted text), or `null` when it is not. Such a paragraph goes outright:
1309
+ * accepting and rejecting both leave it out.
1310
+ *
1311
+ * The inserted break is the paragraph's own (an `ins` mark, or a suggested
1312
+ * whole-paragraph insert), except on the paragraph that ends its container:
1313
+ * that one keeps the container's original mark and the inserted break was
1314
+ * rotated onto the paragraph before it, whose `ins` mark then goes too.
1315
+ */
1316
+ const pendingParagraphRetraction = (doc, position) => {
1317
+ const paragraph = doc.nodeAt(position);
1318
+ if (paragraph?.type.name !== "paragraph" || !holdsOnlyInsertedContent(paragraph)) return null;
1319
+ const at = doc.resolve(position);
1320
+ const before = at.nodeBefore;
1321
+ const endsItsContainer = paragraphEndsItsContainer(at, paragraph.type.name);
1322
+ if (at.parent.childCount < 2 || endsItsContainer && before?.type.name !== paragraph.type.name) return null;
1323
+ if (isInsertedPPrMark(paragraph.attrs["pPrMark"]) || isSuggestedParagraphInsert(paragraph)) return { clearBreakAt: null };
1324
+ if (endsItsContainer && before && paragraph.attrs["pPrMark"] == null) return isInsertedPPrMark(before.attrs["pPrMark"]) ? { clearBreakAt: position - before.nodeSize } : null;
1325
+ return null;
1326
+ };
959
1327
  const addedBreakRevisionId = (value) => {
960
1328
  if (typeof value !== "object" || value === null || !("kind" in value) || !("info" in value)) return null;
961
1329
  if (value.kind !== "ins" && value.kind !== "moveTo") return null;
@@ -963,6 +1331,198 @@ const addedBreakRevisionId = (value) => {
963
1331
  if (typeof info !== "object" || info === null || !("id" in info)) return null;
964
1332
  return typeof info.id === "number" ? info.id : null;
965
1333
  };
1334
+ /** The revision id of a paragraph mark that says its break was added or deleted. */
1335
+ const addedOrDeletedBreakRevisionId = (value) => {
1336
+ if (typeof value !== "object" || value === null || !("kind" in value) || !("info" in value)) return null;
1337
+ if (value.kind !== "del") return addedBreakRevisionId(value);
1338
+ const info = value.info;
1339
+ if (typeof info !== "object" || info === null || !("id" in info)) return null;
1340
+ return typeof info.id === "number" ? info.id : null;
1341
+ };
1342
+ /** Whether every piece of the paragraph's content is pending deleted text (or it has none). */
1343
+ const holdsOnlyDeletedContent = (paragraph) => {
1344
+ let whollyDeleted = true;
1345
+ paragraph.descendants((node) => {
1346
+ if (!whollyDeleted) return false;
1347
+ if (!node.isInline || isZeroWidthAnchor(node)) return true;
1348
+ whollyDeleted = node.marks.some((mark) => mark.type.name === "deletion");
1349
+ return false;
1350
+ });
1351
+ return whollyDeleted;
1352
+ };
1353
+ /** A paragraph mark that is a plain pending deletion (not a relocation's source). */
1354
+ const isPlainDeletedPPrMark = (value) => typeof value === "object" && value !== null && "kind" in value && value.kind === "del";
1355
+ /** An emptied paragraph still carries deleted content, rather than being an original blank. */
1356
+ const hasDeletedContent = (paragraph) => {
1357
+ let found = false;
1358
+ paragraph.descendants((node) => {
1359
+ if (node.isInline && node.marks.some((mark) => mark.type.name === "deletion")) {
1360
+ found = true;
1361
+ return false;
1362
+ }
1363
+ return !found;
1364
+ });
1365
+ return found;
1366
+ };
1367
+ /** The final paragraph reached by a chain of wholly deleted paragraphs. */
1368
+ const deletedFinalParagraphAfter = (at, paragraphTypeName) => {
1369
+ let position = at.pos;
1370
+ for (let index = at.index(); index < at.parent.childCount; index++) {
1371
+ const paragraph = at.parent.child(index);
1372
+ if (paragraph.type.name !== paragraphTypeName || !holdsOnlyDeletedContent(paragraph)) return null;
1373
+ if (index === at.parent.childCount - 1) return paragraph.attrs["pPrMark"] == null && hasDeletedContent(paragraph) ? position : null;
1374
+ if (!isPlainDeletedPPrMark(paragraph.attrs["pPrMark"])) return null;
1375
+ position += paragraph.nodeSize;
1376
+ }
1377
+ return null;
1378
+ };
1379
+ /**
1380
+ * Where the added break of the final paragraph at `at` rotates to: the free
1381
+ * mark `addedBreakCarrierBefore` finds, or else a paragraph the batch deletes
1382
+ * whole, whose deleted break the added one cancels (`cancelsDeletedBreak`).
1383
+ * The cancelling paragraph must be followed directly by the paragraph its
1384
+ * words run into.
1385
+ */
1386
+ const finalBreakCarrierBefore = (at, paragraphTypeName, batchRevisionIds) => {
1387
+ const free = addedBreakCarrierBefore(at, paragraphTypeName);
1388
+ if (free) return {
1389
+ ...free,
1390
+ cancelsDeletedBreak: false
1391
+ };
1392
+ let position = at.pos;
1393
+ for (let index = at.index() - 1; index >= 0; index--) {
1394
+ const sibling = at.parent.child(index);
1395
+ position -= sibling.nodeSize;
1396
+ if (sibling.type.name !== paragraphTypeName) {
1397
+ if (sibling.type.spec["tableRole"] === "table") return null;
1398
+ continue;
1399
+ }
1400
+ const mark = sibling.attrs["pPrMark"];
1401
+ const revisionId = addedBreakRevisionId(mark);
1402
+ if (revisionId !== null && batchRevisionIds.has(revisionId)) continue;
1403
+ const deletedBreakId = isPlainDeletedPPrMark(mark) ? addedOrDeletedBreakRevisionId(mark) : null;
1404
+ return deletedBreakId !== null && batchRevisionIds.has(deletedBreakId) && holdsOnlyDeletedContent(sibling) && at.parent.maybeChild(index + 1)?.type.name === paragraphTypeName ? {
1405
+ position,
1406
+ node: sibling,
1407
+ cancelsDeletedBreak: true
1408
+ } : null;
1409
+ }
1410
+ return null;
1411
+ };
1412
+ /**
1413
+ * Finish the tracked deletion of every paragraph a container ends with.
1414
+ *
1415
+ * Its own mark cannot be deleted (see `paragraphEndsItsContainer`), so the
1416
+ * deletion emptied it and left the mark. Applied directly, the paragraph goes
1417
+ * whenever a paragraph precedes it, which becomes the container's last; only
1418
+ * a container that would be left empty or ending in a table keeps an empty
1419
+ * paragraph. Tracked resolves to the same: the preceding paragraph's mark is
1420
+ * deleted, and accepting runs that paragraph into the emptied one, keeping
1421
+ * its own properties since it holds the words.
1422
+ *
1423
+ * A paragraph with a break pending deletion already runs into this one. If
1424
+ * its content is also wholly deleted, look past it for the surviving carrier;
1425
+ * otherwise another final-break revision would depend on batch boundaries.
1426
+ * A preceding break that is itself a pending insertion is
1427
+ * retracted instead of deleted: the paragraph joins the emptied one now, and
1428
+ * records the emptied one's properties as `w:pPrChange` where they differ,
1429
+ * which is what rejecting reads to put them back.
1430
+ *
1431
+ * This runs once over the finished batch: which paragraph precedes the
1432
+ * emptied one is settled only then, since the batch runs backwards and an
1433
+ * insertion or deletion before it lands after it.
1434
+ */
1435
+ const withRetiredFinalParagraphs = ({ tr, deletions, appliedIds, batchRevisionIds, revisionSeed, author, date }) => {
1436
+ let nextRevisionId = revisionSeed;
1437
+ const addedRevisions = [];
1438
+ const retractedRevisionIds = [];
1439
+ const resolvedOperationIds = /* @__PURE__ */ new Set();
1440
+ const located = deletions.filter(({ operationId }) => appliedIds.has(operationId)).map((deletion) => ({
1441
+ deletion,
1442
+ at: tr.mapping.slice(deletion.mappedFrom).map(deletion.position)
1443
+ })).toSorted((left, right) => right.at - left.at);
1444
+ for (const { deletion: { operationId, revisionExtras }, at } of located) {
1445
+ const emptied = tr.doc.nodeAt(at);
1446
+ if (emptied?.type.name !== "paragraph") continue;
1447
+ const $emptied = tr.doc.resolve(at);
1448
+ if (!paragraphEndsItsContainer($emptied, emptied.type.name)) continue;
1449
+ let position = at;
1450
+ for (let index = $emptied.index() - 1; index >= 0; index--) {
1451
+ const previous = $emptied.parent.child(index);
1452
+ position -= previous.nodeSize;
1453
+ if (previous.type.name !== emptied.type.name) break;
1454
+ const mark = previous.attrs["pPrMark"];
1455
+ if (mark == null) {
1456
+ const revisionId = nextRevisionId++;
1457
+ tr.setNodeAttribute(position, "pPrMark", {
1458
+ kind: "del",
1459
+ info: {
1460
+ id: revisionId,
1461
+ author,
1462
+ date,
1463
+ ...revisionExtras
1464
+ }
1465
+ });
1466
+ resolvedOperationIds.add(operationId);
1467
+ addedRevisions.push({
1468
+ operationId,
1469
+ revisionId
1470
+ });
1471
+ break;
1472
+ }
1473
+ if (isPlainDeletedPPrMark(mark)) {
1474
+ if (holdsOnlyDeletedContent(previous)) continue;
1475
+ break;
1476
+ }
1477
+ if (isInsertedPPrMark(mark)) {
1478
+ const following = tr.doc.nodeAt(position + previous.nodeSize);
1479
+ if (following?.type.name !== emptied.type.name) break;
1480
+ const previousFormatting = paragraphPropertiesBeforeBatch(following, batchRevisionIds);
1481
+ const formattingChanges = JSON.stringify(previousFormatting) !== JSON.stringify(paragraphPropertiesSnapshot(previous));
1482
+ const existing = expectParagraphAttrs(previous)._propertyChanges;
1483
+ if (formattingChanges && hasSerializableParagraphPropertyChange(existing)) break;
1484
+ joinAtParagraphMark({
1485
+ tr,
1486
+ paragraphPos: position,
1487
+ paragraph: previous,
1488
+ next: following
1489
+ });
1490
+ resolvedOperationIds.add(operationId);
1491
+ const retracted = addedBreakRevisionId(mark);
1492
+ if (retracted !== null) retractedRevisionIds.push(retracted);
1493
+ const joined = tr.doc.nodeAt(position);
1494
+ if (formattingChanges && joined) {
1495
+ const revisionId = nextRevisionId++;
1496
+ tr.setNodeMarkup(position, void 0, {
1497
+ ...joined.attrs,
1498
+ _propertyChanges: [...Array.isArray(existing) ? existing : [], {
1499
+ type: "paragraphPropertyChange",
1500
+ info: {
1501
+ id: revisionId,
1502
+ author,
1503
+ date,
1504
+ ...revisionExtras
1505
+ },
1506
+ previousFormatting
1507
+ }]
1508
+ });
1509
+ addedRevisions.push({
1510
+ operationId,
1511
+ revisionId
1512
+ });
1513
+ }
1514
+ break;
1515
+ }
1516
+ break;
1517
+ }
1518
+ }
1519
+ return {
1520
+ nextRevisionId,
1521
+ resolvedOperationIds,
1522
+ addedRevisions,
1523
+ retractedRevisionIds
1524
+ };
1525
+ };
966
1526
  /**
967
1527
  * Move an ADDED paragraph break off every paragraph its container ends with.
968
1528
  *
@@ -985,8 +1545,50 @@ const addedBreakRevisionId = (value) => {
985
1545
  * which paragraph ends a container is only settled when the batch is, and an
986
1546
  * insertion that looked final was undone by the next operation writing a table
987
1547
  * after it.
1548
+ *
1549
+ * The paragraph the run was appended after may be one the same batch deletes:
1550
+ * the insertion landed first, so the deletion took that paragraph's mark as
1551
+ * well. The break rotating onto it then cancels the deleted one, as deleting a
1552
+ * paragraph whose break is inserted does one operation at a time: the deleted
1553
+ * paragraph's words run into the one after it, which ends with the free mark,
1554
+ * and neither break is left to resolve.
988
1555
  */
989
1556
  const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, author, date, initials }) => {
1557
+ const supersededDeletedBreaks = [];
1558
+ const collectSupersededDeletedBreaks = (parent, contentStart) => {
1559
+ let previous = null;
1560
+ parent.forEach((node, offset) => {
1561
+ const position = contentStart + offset;
1562
+ if (node.type.name !== "paragraph") {
1563
+ previous = null;
1564
+ if (!node.isTextblock && node.childCount > 0) collectSupersededDeletedBreaks(node, position + 1);
1565
+ return;
1566
+ }
1567
+ if (previous) {
1568
+ const deletedMark = previous.node.attrs["pPrMark"];
1569
+ const insertedMark = node.attrs["pPrMark"];
1570
+ const insertedRevisionId = addedBreakRevisionId(insertedMark);
1571
+ const trackedInsertion = insertedRevisionId !== null && batchRevisionIds.has(insertedRevisionId);
1572
+ const suggestedInsertion = isSuggestedParagraphInsert(node);
1573
+ const deletedBreakRevisionId = addedOrDeletedBreakRevisionId(deletedMark);
1574
+ if (isPlainDeletedPPrMark(deletedMark) && deletedBreakRevisionId !== null && !batchRevisionIds.has(deletedBreakRevisionId) && !holdsOnlyDeletedContent(previous.node) && (trackedInsertion || suggestedInsertion) && holdsOnlyInsertedContent(node)) supersededDeletedBreaks.push({
1575
+ preceding: previous.position,
1576
+ inserted: position,
1577
+ deletedMark,
1578
+ precedingMark: trackedInsertion ? insertedMark : null
1579
+ });
1580
+ }
1581
+ previous = {
1582
+ node,
1583
+ position
1584
+ };
1585
+ });
1586
+ };
1587
+ collectSupersededDeletedBreaks(tr.doc, 0);
1588
+ for (const { preceding, inserted, deletedMark, precedingMark } of supersededDeletedBreaks) {
1589
+ tr.setNodeAttribute(preceding, "pPrMark", precedingMark);
1590
+ tr.setNodeAttribute(inserted, "pPrMark", deletedMark);
1591
+ }
990
1592
  const paragraphTypeName = tr.doc.type.schema.nodes["paragraph"]?.name ?? "paragraph";
991
1593
  const rotations = finalParagraphsOf(tr.doc, paragraphTypeName).flatMap(({ node, position }) => {
992
1594
  const mark = node.attrs["pPrMark"];
@@ -997,13 +1599,14 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
997
1599
  ownerRevisionId: revisionId,
998
1600
  position
999
1601
  }] : [];
1000
- });
1602
+ }).toSorted((left, right) => right.position - left.position);
1001
1603
  let next = tr;
1002
1604
  let nextRevisionId = revisionSeed;
1003
1605
  const synthesizedRevisions = [];
1606
+ const retractedRevisionIds = [];
1004
1607
  for (const { position: finalPosition, node: final, mark: finalMark } of rotations) {
1005
1608
  const resolved = next.doc.resolve(finalPosition);
1006
- const carrier = addedBreakCarrierBefore(resolved, final.type.name);
1609
+ const carrier = finalBreakCarrierBefore(resolved, final.type.name, batchRevisionIds);
1007
1610
  if (!carrier) {
1008
1611
  next = next.setNodeAttribute(finalPosition, "pPrMark", null);
1009
1612
  continue;
@@ -1041,7 +1644,8 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1041
1644
  const ownerRevisionId = addedBreakRevisionId(carriedMark);
1042
1645
  if (ownerRevisionId === null || !batchRevisionIds.has(ownerRevisionId)) panic("A final-mark rotation crossed a break outside its operation batch", { ownerRevisionId });
1043
1646
  const displacedMark = previous.node.attrs["pPrMark"];
1044
- next = next.setNodeAttribute(previous.position, "pPrMark", carriedMark);
1647
+ const cancels = carrier.cancelsDeletedBreak && previous.position === carrier.position;
1648
+ if (!cancels) next = next.setNodeAttribute(previous.position, "pPrMark", carriedMark);
1045
1649
  const currentFormatting = paragraphPropertiesSnapshot(current.node);
1046
1650
  if (JSON.stringify(previousFormatting) !== JSON.stringify(currentFormatting)) {
1047
1651
  const liveCurrent = next.doc.nodeAt(current.position);
@@ -1067,6 +1671,22 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1067
1671
  revisionId
1068
1672
  });
1069
1673
  }
1674
+ if (cancels) {
1675
+ const deleted = next.doc.nodeAt(previous.position);
1676
+ const following = next.doc.nodeAt(current.position);
1677
+ if (!deleted || !following) panic("A cancelled final-mark rotation lost its paragraphs", { position: previous.position });
1678
+ joinAtParagraphMark({
1679
+ tr: next,
1680
+ paragraphPos: previous.position,
1681
+ paragraph: deleted,
1682
+ next: following,
1683
+ firstIsGoing: true
1684
+ });
1685
+ retractedRevisionIds.push(ownerRevisionId);
1686
+ const deletedBreakId = addedOrDeletedBreakRevisionId(displacedMark);
1687
+ if (deletedBreakId !== null) retractedRevisionIds.push(deletedBreakId);
1688
+ break;
1689
+ }
1070
1690
  if (displacedMark == null) break;
1071
1691
  carriedMark = displacedMark;
1072
1692
  }
@@ -1074,10 +1694,26 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1074
1694
  return {
1075
1695
  transaction: next,
1076
1696
  nextRevisionId,
1077
- synthesizedRevisions
1697
+ synthesizedRevisions,
1698
+ retractedRevisionIds
1699
+ };
1700
+ };
1701
+ const restyledListAttrs = ({ attrs, styleNumbering, removesNumbering, numbering }) => {
1702
+ const stated = readParagraphNumberingAttr(attrs["numPr"]);
1703
+ const statedByOldStyle = isStyleSourcedParagraphNumbering(stated, readParagraphNumberingAttr(attrs["numPrFromStyle"]));
1704
+ const fromStyle = styleNumbering === void 0 ? null : paragraphNumberingAttr(styleNumbering);
1705
+ const numPr = removesNumbering ? removedNumberingAttr(fromStyle) : stated !== null && !statedByOldStyle ? stated : fromStyle;
1706
+ return {
1707
+ ...CLEARED_LIST_RENDERING_ATTRS,
1708
+ ...numPr?.kind === "reference" && listAttrsFromNumbering({
1709
+ numId: numPr.numId,
1710
+ ilvl: numPr.ilvl ?? 0
1711
+ }, numbering),
1712
+ numPr,
1713
+ numPrFromStyle: fromStyle
1078
1714
  };
1079
1715
  };
1080
- const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove, numbering }) => {
1716
+ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove, numbering, styleResolver }) => {
1081
1717
  const operation = item.operation;
1082
1718
  if (operation.type !== "insertAfterBlock" && operation.type !== "insertBeforeBlock") panic("Only paragraph insertions can build inserted paragraphs", { type: operation.type });
1083
1719
  const insertionType = schema.marks["insertion"];
@@ -1131,11 +1767,11 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1131
1767
  }) : buildEmphasisInlineContent(schema, text, marks);
1132
1768
  const attrs = formatsParagraph ? { ...baseAttrs } : {};
1133
1769
  if (isFirstParagraph && operation.pageBreakBefore === true) attrs["pageBreakBefore"] = true;
1134
- const explicitNumbering = operation.numbering;
1770
+ const explicitNumbering = operation.numbering === void 0 || operation.numbering === null ? operation.numbering : concreteListReference(operation.numbering);
1135
1771
  const listLevel = operation.listLevel;
1136
1772
  if (formatsParagraph) {
1137
1773
  if (explicitNumbering === null) {
1138
- attrs["numPr"] = null;
1774
+ attrs["numPr"] = removedNumberingAttr(readParagraphNumberingAttr(attrs["numPrFromStyle"]));
1139
1775
  Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
1140
1776
  } else if (explicitNumbering !== void 0) {
1141
1777
  Object.assign(attrs, listLevelAttrPatch(operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode), {
@@ -1147,7 +1783,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1147
1783
  attrs["numPrFromStyle"] = null;
1148
1784
  }
1149
1785
  } else if (listLevel === null) {
1150
- attrs["numPr"] = null;
1786
+ attrs["numPr"] = removedNumberingAttr(readParagraphNumberingAttr(attrs["numPrFromStyle"]));
1151
1787
  Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
1152
1788
  } else if (typeof listLevel === "number") {
1153
1789
  const numId = paragraphNumberingReferenceId(readParagraphNumberingAttr(Reflect.get(baseAttrs, "numPr")) ?? void 0);
@@ -1165,9 +1801,24 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1165
1801
  }
1166
1802
  if (formatsParagraph && operation.styleId !== void 0) {
1167
1803
  attrs["styleId"] = operation.styleId;
1168
- if (operation.inheritFormatting !== false && operation.styleId !== null) Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
1804
+ attrs["_resolvedFormatting"] = styleResolvedParagraphFormatting(formattingFromStyle);
1805
+ attrs["defaultTextFormatting"] = resolveParagraphBodyRunFormatting({
1806
+ styleId: operation.styleId ?? void 0,
1807
+ tableRunFormatting: expectParagraphAttrs(item.blockNode)._tableRunFormatting,
1808
+ styleResolver
1809
+ }).defaultFormatting ?? null;
1810
+ if (operation.inheritFormatting !== false && operation.styleId !== null) {
1811
+ Object.assign(attrs, restyledListAttrs({
1812
+ attrs,
1813
+ styleNumbering: formattingFromStyle?.numPr,
1814
+ removesNumbering: explicitNumbering === null || explicitNumbering === void 0 && listLevel === null,
1815
+ numbering
1816
+ }));
1817
+ attrs["outlineLevel"] = attrs["_originalFormatting"]?.outlineLevel ?? formattingFromStyle?.outlineLevel ?? null;
1818
+ }
1169
1819
  }
1170
- if (formatsParagraph && (operation.styleId !== void 0 || operation.alignment !== void 0 || operation.spacing !== void 0 || operation.indentation !== void 0)) {
1820
+ if (formatsParagraph && operation.outlineLevel !== void 0) attrs["outlineLevel"] = operation.outlineLevel ?? formattingFromStyle?.outlineLevel ?? null;
1821
+ if (formatsParagraph && (operation.styleId !== void 0 || operation.outlineLevel !== void 0 || operation.alignment !== void 0 || operation.spacing !== void 0 || operation.indentation !== void 0)) {
1171
1822
  const inheritedDirectAlignment = operation.inheritFormatting === false ? void 0 : directParagraphAlignment(expectParagraphAttrs(item.blockNode));
1172
1823
  const inheritedDirectSpacing = operation.inheritFormatting === false ? void 0 : directParagraphSpacing(expectParagraphAttrs(item.blockNode));
1173
1824
  const inheritedDirectIndentation = operation.inheritFormatting === false ? void 0 : directParagraphIndentation(expectParagraphAttrs(item.blockNode));
@@ -1183,6 +1834,11 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1183
1834
  if (operation.styleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
1184
1835
  else originalFormatting.styleId = operation.styleId;
1185
1836
  }
1837
+ if (operation.outlineLevel !== void 0) {
1838
+ originalFormatting ??= {};
1839
+ if (operation.outlineLevel === null) Reflect.deleteProperty(originalFormatting, "outlineLevel");
1840
+ else originalFormatting.outlineLevel = operation.outlineLevel;
1841
+ }
1186
1842
  if (operation.alignment !== void 0 || inheritedDirectAlignment !== void 0) {
1187
1843
  originalFormatting ??= {};
1188
1844
  if (directAlignment === void 0) Reflect.deleteProperty(originalFormatting, "alignment");
@@ -1200,6 +1856,17 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1200
1856
  }));
1201
1857
  attrs["_originalFormatting"] = originalFormatting && Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
1202
1858
  }
1859
+ if (formatsParagraph && operation.styleId !== void 0 && formattingFromStyle?.numPr?.kind === "reference" && operation.numbering === void 0 && operation.listLevel === void 0) {
1860
+ const { numId, ilvl = 0 } = formattingFromStyle.numPr;
1861
+ const levelIndentation = numbering?.getLevel(numId, ilvl)?.pPr;
1862
+ if (levelIndentation && operation.indentation === void 0) {
1863
+ const direct = attrs["_originalFormatting"];
1864
+ const directIndentLeft = typeof direct === "object" && direct !== null && "indentLeft" in direct ? direct.indentLeft : void 0;
1865
+ const directIndentFirstLine = typeof direct === "object" && direct !== null && "indentFirstLine" in direct ? direct.indentFirstLine : void 0;
1866
+ if (directIndentLeft === void 0 && formattingFromStyle.indentLeft === void 0) attrs["indentLeft"] = levelIndentation.indentLeft ?? attrs["indentLeft"];
1867
+ if (directIndentFirstLine === void 0 && formattingFromStyle.indentFirstLine === void 0) attrs["indentFirstLine"] = levelIndentation.indentFirstLine ?? attrs["indentFirstLine"];
1868
+ }
1869
+ }
1203
1870
  if (isSuggested && suggestionId !== null && paragraphRevisionId !== null) attrs["_suggestedInsert"] = {
1204
1871
  suggestionId,
1205
1872
  revisionId: paragraphRevisionId,
@@ -1207,7 +1874,28 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1207
1874
  date,
1208
1875
  ...initials ? { initials } : {}
1209
1876
  };
1210
- nodes.push(item.blockNode.type.create(attrs, content));
1877
+ const paragraph = item.blockNode.type.create(attrs, content);
1878
+ const sourceContext = paragraphRunStyleContext(operation.inheritFormatting === false ? item.blockNode.type.create({}) : item.blockNode, styleResolver);
1879
+ const targetContext = paragraphRunStyleContext(paragraph, styleResolver);
1880
+ const reconciled = [];
1881
+ paragraph.forEach((child) => {
1882
+ if (!child.isInline) {
1883
+ reconciled.push(child);
1884
+ return;
1885
+ }
1886
+ const reconciledMarks = reconcileRunFormattingMarks({
1887
+ authoredFormatting: readAuthoredRunFormatting({
1888
+ context: sourceContext,
1889
+ marks: child.marks,
1890
+ styleResolver
1891
+ }),
1892
+ context: targetContext,
1893
+ node: child,
1894
+ styleResolver
1895
+ });
1896
+ reconciled.push(child.mark(reconciledMarks));
1897
+ });
1898
+ nodes.push(paragraph.copy(Fragment.fromArray(reconciled)));
1211
1899
  }
1212
1900
  if (producesTrackedChanges && !isSuggested) for (const [index, node] of nodes.entries()) {
1213
1901
  const revisionId = nextRevisionId++;
@@ -1231,7 +1919,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1231
1919
  nextRevisionId
1232
1920
  };
1233
1921
  };
1234
- const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates, replacementBackground = "clear" }) => {
1922
+ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: requestedOperations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates, replacementBackground = "clear", undefinedStyles = "refuse" }) => {
1235
1923
  const applied = [];
1236
1924
  const skipped = [];
1237
1925
  const normalizations = [];
@@ -1263,7 +1951,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1263
1951
  return id;
1264
1952
  };
1265
1953
  const styleResolver = getDocumentStyleResolver(view.state);
1266
- const numbering = getDocumentNumbering(view.state);
1954
+ const styleDefinitions = undefinedStyles === "keep" ? null : getDocumentStyleDefinitions(view.state);
1955
+ const { operations, numbering } = resolveNewListOperations(requestedOperations, getDocumentNumbering(view.state));
1267
1956
  const formattingFromStyleForInsertion = (item) => {
1268
1957
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return;
1269
1958
  const anchorAttrs = expectParagraphAttrs(item.blockNode);
@@ -1277,6 +1966,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1277
1966
  };
1278
1967
  const claimedTableRows = /* @__PURE__ */ new Set();
1279
1968
  const claimedTableColumns = /* @__PURE__ */ new Set();
1969
+ const batchClaims = new BatchClaims();
1280
1970
  const diffText = wordDiffMode === "coarse" ? coarseWordDiff : wordDiffSessionFromOptions(wordDiff).diff;
1281
1971
  const producesTrackedChanges = mode !== "direct";
1282
1972
  const isSuggested = mode === "suggested";
@@ -1290,6 +1980,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1290
1980
  };
1291
1981
  const liveBlocks = collectLiveBlocksByHash(view.state.doc);
1292
1982
  const liveBlocksByParaId = collectLiveBlocksByParaId(view.state.doc);
1983
+ const noteReferences = collectNoteReferenceLabels(view.state.doc);
1984
+ const numberingInstanceIds = getDocumentNumberingInstanceIds(view.state) === null ? null : new Set(numbering?.definitions.nums.map(({ numId }) => numId) ?? []);
1293
1985
  for (const [index, operation] of operations.entries()) {
1294
1986
  const commentText = getOperationCommentText(operation);
1295
1987
  if (commentText !== void 0 && (!commentType || createCommentId === void 0)) {
@@ -1304,12 +1996,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1304
1996
  operation,
1305
1997
  liveBlocks,
1306
1998
  liveBlocksByParaId,
1999
+ noteReferences,
1307
2000
  doc: view.state.doc
1308
2001
  });
1309
2002
  if (resolution.type === "skip") {
1310
2003
  skipped.push({
1311
2004
  id: operation.id,
1312
- reason: resolution.reason
2005
+ reason: resolution.reason,
2006
+ ...resolution.message !== void 0 && { message: resolution.message }
1313
2007
  });
1314
2008
  continue;
1315
2009
  }
@@ -1320,6 +2014,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1320
2014
  });
1321
2015
  continue;
1322
2016
  }
2017
+ const undefinedStyle = findUndefinedParagraphStyleReference(operation, styleDefinitions);
2018
+ if (undefinedStyle !== void 0) {
2019
+ skipped.push({
2020
+ id: operation.id,
2021
+ reason: "missingStyle",
2022
+ message: describeUndefinedParagraphStyle(undefinedStyle, styleDefinitions)
2023
+ });
2024
+ continue;
2025
+ }
2026
+ const undefinedNumbering = findUndefinedNumberingReference(operation, numberingInstanceIds);
2027
+ if (undefinedNumbering !== void 0) {
2028
+ skipped.push({
2029
+ id: operation.id,
2030
+ reason: "missingNumbering",
2031
+ message: describeUndefinedNumbering(undefinedNumbering, numberingInstanceIds)
2032
+ });
2033
+ continue;
2034
+ }
1323
2035
  const deletion = resolution.operation.tableRowDeletion;
1324
2036
  if (deletion) {
1325
2037
  const rowKey = `${deletion.tablePosition}:${deletion.rowIndex}`;
@@ -1330,7 +2042,6 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1330
2042
  });
1331
2043
  continue;
1332
2044
  }
1333
- claimedTableRows.add(rowKey);
1334
2045
  }
1335
2046
  const columnDeletion = resolution.operation.tableColumnDeletion;
1336
2047
  if (columnDeletion) {
@@ -1342,8 +2053,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1342
2053
  });
1343
2054
  continue;
1344
2055
  }
1345
- claimedTableColumns.add(columnKey);
1346
2056
  }
2057
+ const claim = batchClaimOf({
2058
+ item: resolution.operation,
2059
+ doc: view.state.doc,
2060
+ producesTrackedChanges
2061
+ });
2062
+ const claimedBy = batchClaims.conflictOf(claim);
2063
+ if (claimedBy !== null) {
2064
+ skipped.push({
2065
+ id: operation.id,
2066
+ reason: "overlappingOperation",
2067
+ message: `operation ${JSON.stringify(claimedBy)}, earlier in this batch, already claims its target.`
2068
+ });
2069
+ continue;
2070
+ }
2071
+ batchClaims.add(operation.id, claim);
2072
+ if (deletion) claimedTableRows.add(`${deletion.tablePosition}:${deletion.rowIndex}`);
2073
+ if (columnDeletion) claimedTableColumns.add(getTableColumnCoordinateKey(columnDeletion));
1347
2074
  if ((operation.type === "insertAfterBlock" || operation.type === "insertBeforeBlock") && operation.lineBreakMode !== "inline" && LINE_BREAK_PATTERN.test(operation.text)) normalizations.push({
1348
2075
  id: operation.id,
1349
2076
  code: "splitMultilineText",
@@ -1394,6 +2121,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1394
2121
  });
1395
2122
  }
1396
2123
  let tr = view.state.tr;
2124
+ /** Text boxes a tracked block deletion deletes, by anchor. */
2125
+ const deletedTextBoxAnchorIds = /* @__PURE__ */ new Set();
2126
+ /** Container-ending paragraphs a tracked `deleteBlock` emptied: see `withRetiredFinalParagraphs`. */
2127
+ const deletedFinalParagraphs = [];
2128
+ const deferredNoopFinalDeletions = /* @__PURE__ */ new Set();
1397
2129
  const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
1398
2130
  let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor;
1399
2131
  /**
@@ -1409,7 +2141,30 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1409
2141
  };
1410
2142
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
1411
2143
  const insertedColumnCounts = /* @__PURE__ */ new Map();
2144
+ const tableAfterDeletedSplit = /* @__PURE__ */ new Map();
2145
+ if (mode === "tracked-changes") {
2146
+ const afterTables = /* @__PURE__ */ new Map();
2147
+ for (const item of executableResolved) {
2148
+ if (item.operation.type !== "insertTable" || item.operation.position === "before") continue;
2149
+ const boundaries = afterTables.get(item.blockFrom) ?? /* @__PURE__ */ new Set();
2150
+ boundaries.add(item.from);
2151
+ afterTables.set(item.blockFrom, boundaries);
2152
+ }
2153
+ for (const item of executableResolved) {
2154
+ if (item.operation.type !== "deleteBlock") continue;
2155
+ if (!isInsertedPPrMark(item.blockNode.attrs["pPrMark"])) continue;
2156
+ const at = view.state.doc.resolve(item.blockFrom);
2157
+ if (at.parent.maybeChild(at.index() + 1)?.type !== item.blockNode.type) continue;
2158
+ if (afterTables.get(item.blockFrom)?.has(item.blockTo)) tableAfterDeletedSplit.set(item.blockFrom, commentsAcrossBlockBoundary(view.state.doc, item.blockTo));
2159
+ }
2160
+ }
1412
2161
  const executionOrder = executableResolved.toSorted((left, right) => {
2162
+ if (left.blockFrom === right.blockFrom && tableAfterDeletedSplit.has(left.blockFrom)) {
2163
+ if (left.operation.type === "deleteBlock" && right.operation.type === "insertTable" && right.from === left.blockTo) return -1;
2164
+ if (left.operation.type === "insertTable" && right.operation.type === "deleteBlock" && left.from === right.blockTo) return 1;
2165
+ }
2166
+ const leftAnnotates = ANNOTATION_OPERATION_TYPES.has(left.operation.type);
2167
+ if (leftAnnotates !== ANNOTATION_OPERATION_TYPES.has(right.operation.type)) return leftAnnotates ? -1 : 1;
1413
2168
  const leftCellShape = left.tableCellMerge ?? left.tableCellSplit;
1414
2169
  const rightCellShape = right.tableCellMerge ?? right.tableCellSplit;
1415
2170
  if (!leftCellShape && rightCellShape) return -1;
@@ -1473,7 +2228,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1473
2228
  suggestionId: insertionSuggestionId,
1474
2229
  revisionSeed,
1475
2230
  isPairedMove,
1476
- numbering
2231
+ numbering,
2232
+ styleResolver
1477
2233
  });
1478
2234
  revisionSeed = built.nextRevisionId;
1479
2235
  claimSharedRevisionIds(revisionSeed);
@@ -1499,6 +2255,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1499
2255
  const commentMark = provisionalCommentId !== void 0 && commentType ? commentType.create({ commentId: provisionalCommentId }) : null;
1500
2256
  const stepsBefore = tr.steps.length;
1501
2257
  let appliedRevisionIds = [];
2258
+ let unifiedFormatting = false;
1502
2259
  if (isSuggested && !SUGGESTED_SUPPORTED_OPERATION_TYPES.has(item.operation.type)) {
1503
2260
  skipped.push({
1504
2261
  id: item.operation.id,
@@ -1528,13 +2285,15 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1528
2285
  const minimal = applyMinimalDirectReplacement({
1529
2286
  tr,
1530
2287
  item,
2288
+ noteReferences,
1531
2289
  replacement: item.operation.replace,
1532
2290
  commentMark,
1533
2291
  replacementBackground,
1534
2292
  ...styleResolver !== void 0 ? { styleResolver } : {}
1535
2293
  });
1536
2294
  if (minimal !== null) {
1537
- tr = minimal;
2295
+ tr = minimal.transaction;
2296
+ unifiedFormatting = minimal.unifiesFormatting;
1538
2297
  break;
1539
2298
  }
1540
2299
  }
@@ -1545,6 +2304,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1545
2304
  const tracked = applyMinimalTrackedReplacement({
1546
2305
  tr,
1547
2306
  item,
2307
+ noteReferences,
1548
2308
  replacement: stripInlineEmphasisMarkers(item.operation.replace),
1549
2309
  commentMark,
1550
2310
  replacementBackground,
@@ -1568,6 +2328,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1568
2328
  if (tracked) {
1569
2329
  tr = tracked.transaction;
1570
2330
  operationRevisionSeed = tracked.nextRevisionId;
2331
+ unifiedFormatting = tracked.unifiesFormatting;
1571
2332
  appliedRevisionIds = [
1572
2333
  revisionIdDelete,
1573
2334
  revisionIdInsert,
@@ -1576,6 +2337,13 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1576
2337
  break;
1577
2338
  }
1578
2339
  }
2340
+ if (rangeHoldsNoteReference(item)) {
2341
+ skipped.push({
2342
+ id: item.operation.id,
2343
+ reason: "protectedReference"
2344
+ });
2345
+ continue;
2346
+ }
1579
2347
  const stepsBeforeBackgroundClear = tr.steps.length;
1580
2348
  const backgroundResult = clearReplacementBackground({
1581
2349
  tr,
@@ -1705,6 +2473,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1705
2473
  const minimal = changesText && mode === "direct" ? applyMinimalDirectReplacement({
1706
2474
  tr,
1707
2475
  item,
2476
+ noteReferences,
1708
2477
  replacement: item.operation.text,
1709
2478
  commentMark,
1710
2479
  replacementBackground,
@@ -1713,6 +2482,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1713
2482
  const tracked = changesText && producesTrackedChanges ? applyMinimalTrackedReplacement({
1714
2483
  tr,
1715
2484
  item,
2485
+ noteReferences,
1716
2486
  replacement: stripInlineEmphasisMarkers(item.operation.text),
1717
2487
  commentMark,
1718
2488
  replacementBackground,
@@ -1733,13 +2503,30 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1733
2503
  });
1734
2504
  continue;
1735
2505
  }
1736
- if (minimal !== null) tr = minimal;
1737
- else if (tracked !== null) {
2506
+ if (minimal !== null) {
2507
+ tr = minimal.transaction;
2508
+ unifiedFormatting = minimal.unifiesFormatting;
2509
+ } else if (tracked !== null) {
1738
2510
  tr = tracked.transaction;
1739
2511
  operationRevisionSeed = tracked.nextRevisionId;
2512
+ unifiedFormatting = tracked.unifiesFormatting;
1740
2513
  clearedBackground = tracked.backgroundRevisionIds.length > 0;
1741
2514
  backgroundRevisionIds = tracked.backgroundRevisionIds;
1742
2515
  } else if (changesText) {
2516
+ if (rangeHoldsNoteReference(item)) {
2517
+ skipped.push({
2518
+ id: item.operation.id,
2519
+ reason: "protectedReference"
2520
+ });
2521
+ continue;
2522
+ }
2523
+ if (buildCleanBlockText(item.blockNode, item.blockFrom).structuralBoundaries.length > 0) {
2524
+ skipped.push({
2525
+ id: item.operation.id,
2526
+ reason: "unsupportedBlock"
2527
+ });
2528
+ continue;
2529
+ }
1743
2530
  const stepsBeforeBackgroundClear = tr.steps.length;
1744
2531
  const backgroundResult = clearReplacementBackground({
1745
2532
  tr,
@@ -1802,6 +2589,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1802
2589
  }
1803
2590
  case "insertAfterBlock":
1804
2591
  case "insertBeforeBlock": {
2592
+ if (isSuggested && isPendingDeletion(tr.doc, item.blockFrom, item.blockNode, buildCleanBlockText(item.blockNode, item.blockFrom, {
2593
+ fieldResults: "text",
2594
+ noteReferences
2595
+ }).text)) {
2596
+ skipped.push({
2597
+ id: item.operation.id,
2598
+ reason: "pendingDeletion"
2599
+ });
2600
+ continue;
2601
+ }
1805
2602
  const built = buildInsertedParagraphs({
1806
2603
  item,
1807
2604
  schema: view.state.schema,
@@ -1814,11 +2611,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1814
2611
  suggestionId,
1815
2612
  revisionSeed: operationRevisionSeed,
1816
2613
  isPairedMove,
1817
- numbering
2614
+ numbering,
2615
+ styleResolver
1818
2616
  });
1819
2617
  operationRevisionSeed = built.nextRevisionId;
1820
2618
  if (built.revisionIds.length > 0) appliedRevisionIds = built.revisionIds;
1821
- tr = tr.insert(item.from, built.nodes);
2619
+ tr = insertBlocksInsideComments({
2620
+ tr,
2621
+ at: item.from,
2622
+ blocks: built.nodes
2623
+ });
1822
2624
  break;
1823
2625
  }
1824
2626
  case "insertSignatureTable": {
@@ -1871,7 +2673,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1871
2673
  const template = tableTemplates?.get(item.operation.id);
1872
2674
  const result = applyTableRowInsertion({
1873
2675
  tr,
1874
- insertion,
2676
+ insertion: mapTableRowInsertion(tr, insertion),
1875
2677
  cellTexts: item.operation.cellTexts,
1876
2678
  revision,
1877
2679
  ...template !== void 0 && { template }
@@ -1906,6 +2708,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1906
2708
  cellTexts: item.operation.cellTexts,
1907
2709
  revision
1908
2710
  });
2711
+ if (result.type === "payloadDoesNotFit") {
2712
+ skipped.push({
2713
+ id: item.operation.id,
2714
+ reason: "payloadDoesNotFit",
2715
+ message: result.message
2716
+ });
2717
+ continue;
2718
+ }
1909
2719
  if (result.type === "unsupported") {
1910
2720
  skipped.push({
1911
2721
  id: item.operation.id,
@@ -2061,12 +2871,32 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2061
2871
  }
2062
2872
  break;
2063
2873
  }
2064
- case "deleteBlock":
2874
+ case "deleteBlock": {
2875
+ if (isSuggested && (hasOrdinaryTextRevision(item.blockNode) || hasOrdinaryParagraphMarkRevision(item.blockNode))) {
2876
+ skipped.push({
2877
+ id: item.operation.id,
2878
+ reason: "unsupportedMode"
2879
+ });
2880
+ continue;
2881
+ }
2065
2882
  if (mode === "direct") {
2066
2883
  const at = tr.doc.resolve(item.blockFrom);
2067
2884
  tr = !paragraphEndsItsContainer(at, item.blockNode.type.name) || at.nodeBefore?.type.name === item.blockNode.type.name ? tr.delete(item.blockFrom, item.blockTo) : tr.delete(item.blockFrom + 1, item.blockTo - 1);
2068
2885
  break;
2069
2886
  }
2887
+ const retraction = isPairedMove(item.operation.moveId) ? null : pendingParagraphRetraction(tr.doc, item.blockFrom);
2888
+ if (retraction) {
2889
+ if (isSuggested && !isSuggestedParagraphInsert(item.blockNode)) {
2890
+ skipped.push({
2891
+ id: item.operation.id,
2892
+ reason: "unsupportedMode"
2893
+ });
2894
+ continue;
2895
+ }
2896
+ tr = tr.delete(item.blockFrom, item.blockTo);
2897
+ if (retraction.clearBreakAt !== null) tr = tr.setNodeAttribute(retraction.clearBreakAt, "pPrMark", null);
2898
+ break;
2899
+ }
2070
2900
  if (deletionType) {
2071
2901
  const revisionId = operationRevisionSeed++;
2072
2902
  const deletionMark = deletionType.create({
@@ -2079,6 +2909,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2079
2909
  tr = tr.addMark(item.from, item.to, deletionMark);
2080
2910
  const atomRanges = undeletedContentAtomRanges(tr, item.blockFrom);
2081
2911
  for (const { from, to } of atomRanges) tr = tr.addMark(from, to, deletionMark);
2912
+ for (const anchorId of textBoxAnchorIdsBetween(tr.doc, tr.mapping.map(item.blockFrom), tr.mapping.map(item.blockTo))) deletedTextBoxAnchorIds.add(anchorId);
2082
2913
  encloseWholeControls({
2083
2914
  tr,
2084
2915
  from: item.blockFrom,
@@ -2088,7 +2919,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2088
2919
  const inlineRevisionApplied = item.from < item.to || atomRanges.length > 0;
2089
2920
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
2090
2921
  const markPosition = tr.mapping.map(item.blockFrom);
2091
- if (!paragraphEndsItsContainer(tr.doc.resolve(markPosition), item.blockNode.type.name) && tr.doc.nodeAt(markPosition)?.attrs["pPrMark"] == null) {
2922
+ const endsItsContainer = paragraphEndsItsContainer(tr.doc.resolve(markPosition), item.blockNode.type.name);
2923
+ const deleted = tr.doc.nodeAt(markPosition);
2924
+ const following = deleted && tr.doc.nodeAt(markPosition + deleted.nodeSize);
2925
+ if (deleted && following?.type === deleted.type && !isPairedMove(item.operation.moveId) && isInsertedPPrMark(deleted.attrs["pPrMark"])) joinAtParagraphMark({
2926
+ tr,
2927
+ paragraphPos: markPosition,
2928
+ paragraph: deleted,
2929
+ next: following,
2930
+ firstIsGoing: true
2931
+ });
2932
+ else if (!endsItsContainer && deleted?.attrs["pPrMark"] == null) {
2092
2933
  const markRevisionId = operationRevisionSeed++;
2093
2934
  tr = tr.setNodeAttribute(markPosition, "pPrMark", {
2094
2935
  kind: isPairedMove(item.operation.moveId) ? "moveFrom" : "del",
@@ -2100,10 +2941,25 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2100
2941
  }
2101
2942
  });
2102
2943
  appliedRevisionIds = inlineRevisionApplied ? [revisionId, markRevisionId] : [markRevisionId];
2944
+ } else if (endsItsContainer && !isPairedMove(item.operation.moveId)) deletedFinalParagraphs.push({
2945
+ operationId: item.operation.id,
2946
+ position: markPosition,
2947
+ mappedFrom: tr.mapping.maps.length,
2948
+ revisionExtras: trackedRevisionExtras
2949
+ });
2950
+ else if (deleted && !isPairedMove(item.operation.moveId) && isPlainDeletedPPrMark(deleted.attrs["pPrMark"]) && holdsOnlyDeletedContent(deleted)) {
2951
+ const finalPosition = deletedFinalParagraphAfter(tr.doc.resolve(markPosition), deleted.type.name);
2952
+ if (finalPosition !== null) deletedFinalParagraphs.push({
2953
+ operationId: item.operation.id,
2954
+ position: finalPosition,
2955
+ mappedFrom: tr.mapping.maps.length,
2956
+ revisionExtras: trackedRevisionExtras
2957
+ });
2103
2958
  }
2104
2959
  }
2105
2960
  if (commentMark) tr = tr.addMark(item.from, item.to, commentMark);
2106
2961
  break;
2962
+ }
2107
2963
  case "insertTable": {
2108
2964
  const revision = {
2109
2965
  revisionId: operationRevisionSeed,
@@ -2129,7 +2985,55 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2129
2985
  continue;
2130
2986
  }
2131
2987
  if (producesTrackedChanges) appliedRevisionIds = [operationRevisionSeed++];
2132
- tr = tr.insert(item.from, table);
2988
+ const carriedComments = tableAfterDeletedSplit.get(item.blockFrom);
2989
+ const usesRetractedSplit = carriedComments !== void 0 && item.from === item.blockTo;
2990
+ let at = usesRetractedSplit ? tr.mapping.map(item.blockFrom, -1) : item.from;
2991
+ let insertionComments = carriedComments;
2992
+ if (!usesRetractedSplit) {
2993
+ const boundary = tr.doc.resolve(at);
2994
+ const before = boundary.nodeBefore;
2995
+ const finalDeleted = deletedFinalParagraphAfter(boundary, "paragraph");
2996
+ const mapping = tr.mapping;
2997
+ const pendingIndex = finalDeleted === null ? -1 : deletedFinalParagraphs.findIndex(({ position, mappedFrom }) => mapping.slice(mappedFrom).map(position) === finalDeleted);
2998
+ const pending = pendingIndex < 0 ? void 0 : deletedFinalParagraphs.at(pendingIndex);
2999
+ if (before?.type.name === "paragraph" && finalDeleted !== null && (pending !== void 0 || isPlainDeletedPPrMark(before.attrs["pPrMark"]) && !holdsOnlyDeletedContent(before))) {
3000
+ insertionComments = commentsAcrossBlockBoundary(tr.doc, at);
3001
+ const final = tr.doc.nodeAt(finalDeleted);
3002
+ if (!final) panic("A deleted final paragraph vanished before table insertion", { finalDeleted });
3003
+ if (pending) {
3004
+ const receiptIndex = applied.findIndex(({ id }) => id === pending.operationId);
3005
+ const receipt = receiptIndex < 0 ? void 0 : applied.at(receiptIndex);
3006
+ if (receipt === void 0) panic("A deleted final paragraph lost its operation receipt", { operationId: pending.operationId });
3007
+ const revisionId = operationRevisionSeed++;
3008
+ tr.setNodeAttribute(finalDeleted, "pPrMark", {
3009
+ kind: "del",
3010
+ info: {
3011
+ id: revisionId,
3012
+ author,
3013
+ date,
3014
+ ...pending.revisionExtras
3015
+ }
3016
+ });
3017
+ applied[receiptIndex] = {
3018
+ ...receipt,
3019
+ revisionId: receipt.revisionId ?? revisionId,
3020
+ revisionIds: [...receipt.revisionIds ?? [], revisionId]
3021
+ };
3022
+ deletedFinalParagraphs.splice(pendingIndex, 1);
3023
+ deferredNoopFinalDeletions.delete(pending.operationId);
3024
+ } else {
3025
+ tr.setNodeAttribute(at - before.nodeSize, "pPrMark", null);
3026
+ tr.setNodeAttribute(finalDeleted, "pPrMark", before.attrs["pPrMark"]);
3027
+ }
3028
+ at = finalDeleted + final.nodeSize;
3029
+ }
3030
+ }
3031
+ tr = insertBlocksInsideComments({
3032
+ tr,
3033
+ at,
3034
+ blocks: table,
3035
+ ...insertionComments !== void 0 && { carriedComments: insertionComments }
3036
+ });
2133
3037
  markStructuralChange(tr);
2134
3038
  break;
2135
3039
  }
@@ -2173,10 +3077,21 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2173
3077
  };
2174
3078
  const rowPositions = [];
2175
3079
  let rowOffset = livePosition + 1;
3080
+ let holdsPendingInsertedRow = false;
2176
3081
  liveTable.forEach((row) => {
2177
- if (row.type.spec["tableRole"] === "row" && row.attrs["trDel"] == null) rowPositions.push(rowOffset);
3082
+ if (row.type.spec["tableRole"] === "row" && row.attrs["trDel"] == null) {
3083
+ rowPositions.push(rowOffset);
3084
+ holdsPendingInsertedRow ||= row.attrs["trIns"] != null;
3085
+ }
2178
3086
  rowOffset += row.nodeSize;
2179
3087
  });
3088
+ if (holdsPendingInsertedRow) {
3089
+ skipped.push({
3090
+ id: item.operation.id,
3091
+ reason: "unsupportedBlock"
3092
+ });
3093
+ continue;
3094
+ }
2180
3095
  if (rowPositions.length === 0) {
2181
3096
  skipped.push({
2182
3097
  id: item.operation.id,
@@ -2198,8 +3113,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2198
3113
  break;
2199
3114
  }
2200
3115
  case "setBlockParagraphProperties": {
2201
- const blockPosition = tr.mapping.map(item.blockFrom);
2202
- const liveBlock = tr.doc.nodeAt(blockPosition) ?? item.blockNode;
3116
+ const mapped = tr.mapping.mapResult(item.blockFrom);
3117
+ const liveBlock = mapped.deletedAcross ? null : tr.doc.nodeAt(mapped.pos);
3118
+ const paraId = item.blockNode.attrs["paraId"];
3119
+ if (!liveBlock || liveBlock.type !== item.blockNode.type || typeof paraId === "string" && liveBlock.attrs["paraId"] !== paraId) {
3120
+ skipped.push({
3121
+ id: item.operation.id,
3122
+ reason: "missingBlock"
3123
+ });
3124
+ continue;
3125
+ }
3126
+ const blockPosition = mapped.pos;
2203
3127
  const appliedProperties = applyBlockParagraphProperties({
2204
3128
  tr,
2205
3129
  position: blockPosition,
@@ -2226,9 +3150,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2226
3150
  break;
2227
3151
  }
2228
3152
  case "splitBlock": {
3153
+ const splitsFromItsTextBoxes = textBoxAnchorIdsBetween(tr.doc, item.blockFrom, item.from).size > 0 && endOfParagraphWithItsTextBoxes(tr.doc, item.blockFrom) > item.blockTo;
2229
3154
  if (mode === "direct") {
2230
3155
  if (item.to > item.from) tr = tr.delete(item.from, item.to);
2231
3156
  tr = tr.split(item.from);
3157
+ if (splitsFromItsTextBoxes) gatherTextBoxesAfterParagraph(tr, item.blockFrom);
3158
+ } else if (splitsFromItsTextBoxes) {
3159
+ skipped.push({
3160
+ id: item.operation.id,
3161
+ reason: "unsupportedBlock"
3162
+ });
3163
+ continue;
2232
3164
  } else {
2233
3165
  const revisionIdMark = operationRevisionSeed++;
2234
3166
  const info = {
@@ -2288,15 +3220,49 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2288
3220
  case "mergeBlockWithNext": {
2289
3221
  const separator = item.operation.separator ?? "";
2290
3222
  const insertAt = item.blockTo - 1;
3223
+ const second = tr.doc.nodeAt(item.blockTo);
3224
+ const spanningComments = commentsAcrossBlockBoundary(tr.doc, item.blockTo);
3225
+ const withSeparator = (transaction) => {
3226
+ const paragraph = transaction.doc.nodeAt(item.blockFrom);
3227
+ const sourceMarks = paragraph?.lastChild?.marks.filter(({ type }) => RUN_FORMATTING_MARK_NAMES.has(type.name)) ?? [];
3228
+ const sourceNode = view.state.schema.text(separator, sourceMarks);
3229
+ const context = paragraph && paragraphRunStyleContext(paragraph, styleResolver);
3230
+ const marks = context ? reconcileRunFormattingMarks({
3231
+ authoredFormatting: readAuthoredRunFormatting({
3232
+ context,
3233
+ marks: sourceMarks,
3234
+ styleResolver
3235
+ }),
3236
+ context,
3237
+ node: sourceNode,
3238
+ styleResolver
3239
+ }) : sourceMarks;
3240
+ let next = transaction.replaceWith(insertAt, insertAt, view.state.schema.text(separator, marks));
3241
+ for (const comment of spanningComments) next = next.addMark(insertAt, insertAt + separator.length, comment);
3242
+ return next;
3243
+ };
2291
3244
  if (mode === "direct") {
2292
- if (separator.length > 0) tr = tr.insertText(separator, insertAt);
3245
+ if (separator.length > 0) tr = withSeparator(tr);
3246
+ const nextPosition = item.blockTo + separator.length;
3247
+ const next = tr.doc.nodeAt(nextPosition);
3248
+ const paragraph = tr.doc.nodeAt(item.blockFrom);
3249
+ if (next && paragraph) tr = rebaseParagraphRuns({
3250
+ tr,
3251
+ position: nextPosition,
3252
+ previous: next,
3253
+ target: paragraph,
3254
+ styleResolver
3255
+ });
2293
3256
  tr = tr.join(item.blockTo + separator.length);
3257
+ const secondSection = second ? sectionPropertiesOf(second) : null;
3258
+ const joined = tr.doc.nodeAt(item.blockFrom);
3259
+ if (joined && sectionPropertiesOf(joined) !== secondSection) tr = tr.setNodeAttribute(item.blockFrom, "_sectionProperties", secondSection);
2294
3260
  } else {
2295
3261
  const revisionIdMark = operationRevisionSeed++;
2296
3262
  appliedRevisionIds = [revisionIdMark];
2297
3263
  if (separator.length > 0 && insertionType) {
2298
3264
  const revisionIdSeparator = operationRevisionSeed++;
2299
- tr = tr.insertText(separator, insertAt);
3265
+ tr = withSeparator(tr);
2300
3266
  tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
2301
3267
  revisionId: revisionIdSeparator,
2302
3268
  author,
@@ -2305,7 +3271,31 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2305
3271
  }));
2306
3272
  appliedRevisionIds.push(revisionIdSeparator);
2307
3273
  }
2308
- tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
3274
+ const paragraph = tr.doc.nodeAt(item.blockFrom);
3275
+ const joinPos = item.blockTo + separator.length;
3276
+ const next = tr.doc.nodeAt(joinPos);
3277
+ if (!paragraph || !next) panic("A resolved merge lost its paragraphs");
3278
+ if (isInsertedPPrMark(paragraph.attrs["pPrMark"])) {
3279
+ joinAtParagraphMark({
3280
+ tr,
3281
+ paragraphPos: item.blockFrom,
3282
+ paragraph,
3283
+ next
3284
+ });
3285
+ const previousFormatting = paragraphPropertiesSnapshot(next);
3286
+ const existing = expectParagraphAttrs(paragraph)._propertyChanges;
3287
+ if (holdsOnlyInsertedContent(paragraph) && !hasSerializableParagraphPropertyChange(existing) && JSON.stringify(paragraphPropertiesSnapshot(paragraph)) !== JSON.stringify(previousFormatting)) tr = tr.setNodeAttribute(item.blockFrom, "_propertyChanges", [...Array.isArray(existing) ? existing : [], {
3288
+ type: "paragraphPropertyChange",
3289
+ info: {
3290
+ id: revisionIdMark,
3291
+ author,
3292
+ date,
3293
+ ...trackedRevisionExtras
3294
+ },
3295
+ previousFormatting
3296
+ }]);
3297
+ else appliedRevisionIds = appliedRevisionIds.filter((id) => id !== revisionIdMark);
3298
+ } else tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2309
3299
  kind: "del",
2310
3300
  info: {
2311
3301
  id: revisionIdMark,
@@ -2343,11 +3333,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2343
3333
  default: break;
2344
3334
  }
2345
3335
  if (tr.steps.length === stepsBefore) {
2346
- skipped.push({
2347
- id: item.operation.id,
2348
- reason: "noopOperation"
2349
- });
2350
- continue;
3336
+ if (!deletedFinalParagraphs.some(({ operationId }) => operationId === item.operation.id)) {
3337
+ skipped.push({
3338
+ id: item.operation.id,
3339
+ reason: "noopOperation"
3340
+ });
3341
+ continue;
3342
+ }
3343
+ deferredNoopFinalDeletions.add(item.operation.id);
2351
3344
  }
2352
3345
  let committedCommentId;
2353
3346
  if (item.commentText !== void 0) {
@@ -2366,6 +3359,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2366
3359
  }
2367
3360
  revisionSeed = operationRevisionSeed;
2368
3361
  claimSharedRevisionIds(revisionSeed);
3362
+ if (unifiedFormatting) normalizations.push({
3363
+ id: item.operation.id,
3364
+ code: "uniformReplacementFormatting"
3365
+ });
2369
3366
  applied.push({
2370
3367
  id: item.operation.id,
2371
3368
  ...committedCommentId !== void 0 && { commentId: committedCommentId },
@@ -2376,9 +3373,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2376
3373
  ...suggestionId !== null && { suggestionId }
2377
3374
  });
2378
3375
  }
2379
- if (tr.docChanged) {
3376
+ if (tr.docChanged || deferredNoopFinalDeletions.size > 0) {
2380
3377
  const batchRevisionIds = new Set(applied.flatMap(({ revisionIds }) => revisionIds ?? []));
2381
- tr = withoutRevisionsOnZeroWidthAnchors(tr, batchRevisionIds);
3378
+ tr = withoutRevisionsOnZeroWidthAnchors(tr, batchRevisionIds, deletedTextBoxAnchorIds);
3379
+ if (deleteTextBoxesAnchoredAt(tr, droppedTextBoxAnchorIds(view.state.doc, tr.doc))) markStructuralChange(tr);
2382
3380
  continueSharedRevisionIds();
2383
3381
  const rotated = withRotatedAddedFinalBreaks({
2384
3382
  tr,
@@ -2390,6 +3388,25 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2390
3388
  });
2391
3389
  tr = rotated.transaction;
2392
3390
  revisionSeed = rotated.nextRevisionId;
3391
+ const retired = withRetiredFinalParagraphs({
3392
+ tr,
3393
+ deletions: deletedFinalParagraphs,
3394
+ appliedIds: new Set(applied.map(({ id }) => id)),
3395
+ batchRevisionIds,
3396
+ revisionSeed,
3397
+ author,
3398
+ date
3399
+ });
3400
+ revisionSeed = retired.nextRevisionId;
3401
+ for (const operationId of deferredNoopFinalDeletions) {
3402
+ if (retired.resolvedOperationIds.has(operationId)) continue;
3403
+ const receiptIndex = applied.findIndex(({ id }) => id === operationId);
3404
+ if (receiptIndex >= 0) applied.splice(receiptIndex, 1);
3405
+ skipped.push({
3406
+ id: operationId,
3407
+ reason: "noopOperation"
3408
+ });
3409
+ }
2393
3410
  const receiptIndexByRevisionId = /* @__PURE__ */ new Map();
2394
3411
  for (const [receiptIndex, receipt] of applied.entries()) for (const revisionId of receipt.revisionIds ?? []) receiptIndexByRevisionId.set(revisionId, receiptIndex);
2395
3412
  for (const { ownerRevisionId, revisionId } of rotated.synthesizedRevisions) {
@@ -2404,9 +3421,35 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2404
3421
  revisionIds: [...receipt.revisionIds, revisionId]
2405
3422
  };
2406
3423
  }
3424
+ for (const { operationId, revisionId } of retired.addedRevisions) {
3425
+ const receiptIndex = applied.findIndex(({ id }) => id === operationId);
3426
+ const receipt = applied.at(receiptIndex);
3427
+ if (receiptIndex < 0 || !receipt) panic("A retired final-mark revision lost its operation receipt", {
3428
+ operationId,
3429
+ revisionId
3430
+ });
3431
+ const revisionIds = [...receipt.revisionIds ?? [], revisionId];
3432
+ applied[receiptIndex] = {
3433
+ ...receipt,
3434
+ revisionId: receipt.revisionId ?? revisionId,
3435
+ revisionIds
3436
+ };
3437
+ }
3438
+ const retractedRevisionIds = /* @__PURE__ */ new Set([...rotated.retractedRevisionIds, ...retired.retractedRevisionIds]);
3439
+ if (retractedRevisionIds.size > 0) for (const [receiptIndex, receipt] of applied.entries()) {
3440
+ const revisionIds = receipt.revisionIds?.filter((id) => !retractedRevisionIds.has(id));
3441
+ if (revisionIds === void 0) continue;
3442
+ const { revisionId: _oldRevisionId, revisionIds: _oldRevisionIds, ...withoutRevisions } = receipt;
3443
+ const firstRevisionId = revisionIds.at(0);
3444
+ applied[receiptIndex] = firstRevisionId === void 0 ? withoutRevisions : {
3445
+ ...withoutRevisions,
3446
+ revisionId: firstRevisionId,
3447
+ revisionIds
3448
+ };
3449
+ }
2407
3450
  claimSharedRevisionIds(revisionSeed);
2408
3451
  if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
2409
- view.dispatch(tr);
3452
+ if (tr.docChanged) view.dispatch(tr);
2410
3453
  }
2411
3454
  return {
2412
3455
  applied,
@@ -2416,9 +3459,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2416
3459
  };
2417
3460
  };
2418
3461
  const applyFolioAIEditOperations = (options) => applyFolioAIEditOperationsInternal(options);
2419
- const previewFolioAIEditOperations = (options) => {
3462
+ const previewFolioAIEditOperations = (options) => previewFolioAIEditOperationsWithResult(options).outcome;
3463
+ /**
3464
+ * A preview together with the document it would leave and the provisional
3465
+ * comment ids it placed, for a caller that checks the result before reporting
3466
+ * it. Internal: not re-exported from the package entry points.
3467
+ */
3468
+ const previewFolioAIEditOperationsWithResult = (options) => {
2420
3469
  const { view, createCommentId, ...applyOptions } = options;
2421
3470
  let previewCommentId = -1;
3471
+ const commentIds = [];
2422
3472
  const previewView = {
2423
3473
  state: view.state,
2424
3474
  dispatch: (transaction) => {
@@ -2428,15 +3478,23 @@ const previewFolioAIEditOperations = (options) => {
2428
3478
  const result = applyFolioAIEditOperationsInternal({
2429
3479
  ...applyOptions,
2430
3480
  view: previewView,
2431
- ...createCommentId !== void 0 && { createCommentId: () => previewCommentId-- },
3481
+ ...createCommentId !== void 0 && { createCommentId: () => {
3482
+ const id = previewCommentId--;
3483
+ commentIds.push(id);
3484
+ return id;
3485
+ } },
2432
3486
  revisionIdSeed: -1e9,
2433
3487
  wordDiffMode: "coarse"
2434
3488
  });
2435
3489
  return {
2436
- applied: result.applied.map(({ id }) => ({ id })),
2437
- skipped: result.skipped,
2438
- ...result.normalizations !== void 0 && { normalizations: result.normalizations },
2439
- nextRevisionId: options.revisionStamp?.idSeed ?? revisionIdCursor
3490
+ outcome: {
3491
+ applied: result.applied.map(({ id }) => ({ id })),
3492
+ skipped: result.skipped,
3493
+ ...result.normalizations !== void 0 && { normalizations: result.normalizations },
3494
+ nextRevisionId: options.revisionStamp?.idSeed ?? revisionIdCursor
3495
+ },
3496
+ doc: previewView.state.doc,
3497
+ commentIds
2440
3498
  };
2441
3499
  };
2442
3500
  const CLEAN_TEXT_CONTROL_PATTERN = /[\t\n]/gu;
@@ -2460,9 +3518,10 @@ const cleanTextInlineNodes = ({ schema, text, marks = [] }) => {
2460
3518
  if (trailing.length > 0) nodes.push(schema.text(trailing, marks));
2461
3519
  return nodes;
2462
3520
  };
2463
- const insertCleanText = ({ tr, from, to, text, standsInFor }) => {
3521
+ const insertCleanText = ({ tr, from, to, text, standsInFor, keepsReplacedText = false }) => {
2464
3522
  const annotations = surveyReplacedAnnotations(tr.doc, from, to);
2465
- const replaced = standsInFor ? surveyReplacedAnnotations(tr.doc, standsInFor.from, standsInFor.to).carried : [];
3523
+ const carried = standsInFor ? surveyReplacedAnnotations(tr.doc, standsInFor.from, standsInFor.to).carried : [];
3524
+ const replaced = keepsReplacedText ? commentsForTrackedInsertion(tr.doc, from, [carried])[0] ?? [] : carried;
2466
3525
  if (!hasReplacedAnnotations(annotations) && replaced.length === 0 && !hasCleanTextControls(text)) return {
2467
3526
  transaction: tr.insertText(text, from, to),
2468
3527
  start: from,
@@ -2539,7 +3598,8 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2539
3598
  standsInFor: {
2540
3599
  from: item.from,
2541
3600
  to: item.to
2542
- }
3601
+ },
3602
+ keepsReplacedText: true
2543
3603
  });
2544
3604
  nextTr = inserted.transaction;
2545
3605
  nextTr = nextTr.addMark(inserted.start, inserted.end, insertionType.create(insAttrs));
@@ -2551,6 +3611,17 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2551
3611
  }
2552
3612
  return nextTr;
2553
3613
  };
3614
+ /** Whether the resolved range covers a footnote or endnote reference. */
3615
+ const rangeHoldsNoteReference = ({ blockNode, blockFrom, from, to }) => {
3616
+ let holds = false;
3617
+ blockNode.descendants((node, pos) => {
3618
+ if (holds) return false;
3619
+ const start = blockFrom + 1 + pos;
3620
+ if (noteReferenceOf(node) !== null && start < to && start + node.nodeSize > from) holds = true;
3621
+ return !holds;
3622
+ });
3623
+ return holds;
3624
+ };
2554
3625
  /**
2555
3626
  * The clean-text span a text replacement matched: where it starts in the
2556
3627
  * block's clean text and what it says. `null` when the operation does not
@@ -2592,17 +3663,27 @@ const isCarriedMark = (mark) => NON_INCLUSIVE_MARK_DISPOSITION[mark.type.name] =
2592
3663
  * document character by character, or the changes do not produce the
2593
3664
  * replacement.
2594
3665
  */
2595
- const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2596
- const cleanBlock = buildCleanBlockText(item.blockNode, item.blockFrom);
3666
+ const planDocumentReplacement = ({ doc, item, replacement, noteReferences, planChanges, keepFieldsOf }) => {
3667
+ const cleanBlock = buildCleanBlockText(item.blockNode, item.blockFrom, {
3668
+ fieldResults: "text",
3669
+ noteReferences
3670
+ });
2597
3671
  const span = replacedCleanSpan(item, cleanBlock);
2598
3672
  if (span === null) return null;
2599
3673
  const spanStart = span.start;
2600
3674
  const spanEnd = span.start + span.text.length;
2601
- const atomicSpans = cleanBlock.structuralBoundaries.flatMap((boundary) => boundary.type === "field" ? [{
3675
+ const noteReferenceSpans = noteReferenceSpansWithin(cleanBlock, spanStart, spanEnd);
3676
+ const editsReferences = !(segmentsAroundNoteReferences(span.text, replacement, noteReferenceSpans) !== null) && noteReferenceEditsAllowed();
3677
+ const atomicSpans = cleanBlock.structuralBoundaries.flatMap((boundary) => boundary.type === "field" || editsReferences && boundary.type === "noteReference" ? [{
2602
3678
  offset: boundary.offset - spanStart,
2603
3679
  length: boundary.length
2604
3680
  }] : []);
2605
- const changes = widenChangesToAtomicSpans(span.text, planChanges(span.text), atomicSpans);
3681
+ const planPieces = (plan) => editsReferences ? [...plan(span.text, replacement)] : planChangesAroundNoteReferences(span.text, replacement, noteReferenceSpans, plan);
3682
+ const pieceChanges = planPieces(planChanges);
3683
+ if (pieceChanges === null) return null;
3684
+ const widened = widenChangesToAtomicSpans(span.text, pieceChanges, atomicSpans);
3685
+ const keptPieces = keepFieldsOf === void 0 ? null : planPieces(keepFieldsOf);
3686
+ const changes = keptPieces === null ? widened : keepAtomicSpans(span.text, replacement, widened, widenChangesToAtomicSpans(span.text, keptPieces, atomicSpans), atomicSpans);
2606
3687
  if (applyTextChanges(span.text, changes) !== replacement) return null;
2607
3688
  const unitAt = (index) => {
2608
3689
  const from = cleanBlock.offsets[index];
@@ -2610,7 +3691,7 @@ const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2610
3691
  if (from === void 0 || !node) return null;
2611
3692
  return {
2612
3693
  from,
2613
- to: node.isText ? from + 1 : from + node.nodeSize
3694
+ to: !node.isText || noteReferenceOf(node) !== null ? from + node.nodeSize : from + 1
2614
3695
  };
2615
3696
  };
2616
3697
  const matchedFrom = item.from;
@@ -2730,6 +3811,72 @@ const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2730
3811
  replacementStyleSource
2731
3812
  };
2732
3813
  };
3814
+ /** The marks that present a character: its formatting, and the links and comments over it. */
3815
+ const presentationMarks = (marks) => marks.filter((mark) => isFormattingMark(mark) || isCarriedMark(mark));
3816
+ /**
3817
+ * Whether a change of `plan` replaces characters that do not all present
3818
+ * alike. Its new text takes one formatting, the first replaced character's,
3819
+ * and every link and comment over the stretch, so the others' formatting ends
3820
+ * where the replaced text did. That is the rule for every mode, and the
3821
+ * receipt says it was applied (`uniformReplacementFormatting`).
3822
+ */
3823
+ const replacementUnifiesFormatting = (doc, plan) => plan.planned.some(({ change, pieces }) => {
3824
+ if (pieces.length === 0 || change.text.length === 0) return false;
3825
+ let first = null;
3826
+ for (let index = change.start; index < change.end; index++) {
3827
+ const unit = plan.unitAt(plan.spanStart + index);
3828
+ const marks = presentationMarks((unit ? doc.nodeAt(unit.from)?.marks : null) ?? Mark.none);
3829
+ if (first === null) first = marks;
3830
+ else if (!Mark.sameSet(first, marks)) return true;
3831
+ }
3832
+ return false;
3833
+ });
3834
+ /**
3835
+ * The marks each UTF-16 unit of `replacement` carries once a direct
3836
+ * replacement has written it (see {@link applyMinimalDirectReplacement}): a
3837
+ * character the direct plan keeps keeps its own, and one it writes takes the
3838
+ * marks that plan gives its change. Background cleared from a touched stretch
3839
+ * is left off either way.
3840
+ *
3841
+ * A tracked replacement writes its insertions with these marks, so accepting
3842
+ * it leaves exactly the text and formatting the direct edit writes, although
3843
+ * its redline cuts the text into coarser, readable changes than the direct
3844
+ * edit does.
3845
+ */
3846
+ const directReplacementMarks = ({ doc, plan, replacementBackground }) => {
3847
+ const clearsBackground = replacementBackground === "clear";
3848
+ const touched = clearsBackground ? touchedBackgroundIndices({
3849
+ doc,
3850
+ plan
3851
+ }) : /* @__PURE__ */ new Set();
3852
+ const withoutBackground = (marks) => marks.filter((mark) => !BACKGROUND_MARK_NAMES.has(mark.type.name));
3853
+ const allocated = [];
3854
+ let cursor = 0;
3855
+ const keepUntil = (end) => {
3856
+ for (; cursor < end; cursor++) {
3857
+ const index = plan.spanStart + cursor;
3858
+ const unit = plan.unitAt(index);
3859
+ const marks = (unit ? doc.nodeAt(unit.from)?.marks : null) ?? Mark.none;
3860
+ allocated.push(touched.has(index) ? withoutBackground(marks) : marks);
3861
+ }
3862
+ };
3863
+ for (const { change, pieces, insertion } of plan.planned) {
3864
+ keepUntil(change.start);
3865
+ cursor = change.end;
3866
+ let marks;
3867
+ if (insertion !== void 0) marks = insertion.marks(doc);
3868
+ else {
3869
+ const first = pieces.at(0);
3870
+ const last = pieces.at(-1);
3871
+ if (first === void 0 || last === void 0) panic("A planned replacement lost the characters it removes");
3872
+ marks = addCarriedMarks(inheritedReplacementMarks(doc, first.from, first.to), surveyReplacedAnnotations(doc, first.from, last.to).carried);
3873
+ }
3874
+ const written = clearsBackground ? withoutBackground(marks) : marks;
3875
+ for (let offset = 0; offset < change.text.length; offset++) allocated.push(written);
3876
+ }
3877
+ keepUntil(plan.spanEnd - plan.spanStart);
3878
+ return allocated;
3879
+ };
2733
3880
  /**
2734
3881
  * Apply a direct-mode text replacement as the difference between the matched
2735
3882
  * text and its replacement; `minimal-replacement.ts` states the contract.
@@ -2738,15 +3885,17 @@ const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2738
3885
  * cannot be mapped onto the document character by character. The caller then
2739
3886
  * replaces the matched span whole, as it always did.
2740
3887
  */
2741
- const applyMinimalDirectReplacement = ({ tr, item, replacement, commentMark, replacementBackground, styleResolver }) => {
3888
+ const applyMinimalDirectReplacement = ({ tr, item, noteReferences, replacement, commentMark, replacementBackground, styleResolver }) => {
2742
3889
  const plan = planDocumentReplacement({
2743
3890
  doc: tr.doc,
2744
3891
  item,
2745
3892
  replacement,
2746
- planChanges: (source) => planTextChanges(source, replacement)
3893
+ noteReferences,
3894
+ planChanges: planTextChanges
2747
3895
  });
2748
3896
  if (plan === null || plan.changes.length === 0) return null;
2749
3897
  const { planned, matchedFrom, matchedTo } = plan;
3898
+ const unifiesFormatting = replacementUnifiesFormatting(tr.doc, plan);
2750
3899
  let nextTr = tr;
2751
3900
  if (replacementBackground === "clear") nextTr = clearTouchedBackground({
2752
3901
  tr: nextTr,
@@ -2810,7 +3959,10 @@ const applyMinimalDirectReplacement = ({ tr, item, replacement, commentMark, rep
2810
3959
  }
2811
3960
  if (to > from) nextTr = nextTr.addMark(from, to, commentMark);
2812
3961
  }
2813
- return nextTr;
3962
+ return {
3963
+ transaction: nextTr,
3964
+ unifiesFormatting
3965
+ };
2814
3966
  };
2815
3967
  /**
2816
3968
  * Apply a tracked-changes or suggested text replacement as its redline: each
@@ -2825,7 +3977,7 @@ const applyMinimalDirectReplacement = ({ tr, item, replacement, commentMark, rep
2825
3977
  * cannot be mapped onto the document character by character; the caller then
2826
3978
  * marks the whole match as replaced, as it always did.
2827
3979
  */
2828
- const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, replacementBackground, styleResolver, diffText, revisionIdDelete, revisionIdInsert, revisionIdBackgroundSeed, author, date, initials, suggestionId }) => {
3980
+ const applyMinimalTrackedReplacement = ({ tr, item, noteReferences, replacement, commentMark, replacementBackground, styleResolver, diffText, revisionIdDelete, revisionIdInsert, revisionIdBackgroundSeed, author, date, initials, suggestionId }) => {
2829
3981
  const schema = tr.doc.type.schema;
2830
3982
  const insertionType = schema.marks["insertion"];
2831
3983
  const deletionType = schema.marks["deletion"];
@@ -2834,9 +3986,24 @@ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, re
2834
3986
  doc: tr.doc,
2835
3987
  item,
2836
3988
  replacement,
2837
- planChanges: (source) => changesFromSegments(diffText(source, replacement))
3989
+ noteReferences,
3990
+ planChanges: (source, target) => changesFromSegments(diffText(source, target)),
3991
+ keepFieldsOf: planTextChanges
2838
3992
  });
2839
3993
  if (plan === null) return null;
3994
+ const directPlan = planDocumentReplacement({
3995
+ doc: tr.doc,
3996
+ item,
3997
+ replacement,
3998
+ noteReferences,
3999
+ planChanges: planTextChanges
4000
+ });
4001
+ const allocatedMarks = directPlan === null ? null : directReplacementMarks({
4002
+ doc: tr.doc,
4003
+ plan: directPlan,
4004
+ replacementBackground
4005
+ });
4006
+ const unifiesFormatting = directPlan === null ? false : replacementUnifiesFormatting(tr.doc, directPlan);
2840
4007
  let nextTr = tr;
2841
4008
  let backgroundRevisionIds = [];
2842
4009
  let nextRevisionId = revisionIdBackgroundSeed;
@@ -2886,10 +4053,39 @@ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, re
2886
4053
  ...revisionAttrs
2887
4054
  });
2888
4055
  const insertedMarks = (marks) => marks.filter((mark) => !REVISION_MARK_NAMES.has(mark.type.name) && mark.type.name !== "runPropertyChange" && !(clearsBackground && BACKGROUND_MARK_NAMES.has(mark.type.name)));
2889
- for (const change of plan.planned.toReversed()) {
4056
+ const replacementStarts = [];
4057
+ let replacementShift = 0;
4058
+ for (const { change } of plan.planned) {
4059
+ replacementStarts.push(change.start + replacementShift);
4060
+ replacementShift += change.text.length - (change.end - change.start);
4061
+ }
4062
+ const insertedNodes = (text, replacementStart, fallback) => {
4063
+ if (allocatedMarks?.length !== replacement.length) return cleanTextInlineNodes({
4064
+ schema,
4065
+ text,
4066
+ marks: insertedMarks(fallback)
4067
+ });
4068
+ const marksAt = (offset) => allocatedMarks[replacementStart + offset] ?? fallback;
4069
+ const nodes = [];
4070
+ let groupStart = 0;
4071
+ for (let offset = 1; offset <= text.length; offset++) {
4072
+ if (offset < text.length && Mark.sameSet(marksAt(offset), marksAt(groupStart))) continue;
4073
+ nodes.push(...cleanTextInlineNodes({
4074
+ schema,
4075
+ text: text.slice(groupStart, offset),
4076
+ marks: insertedMarks(marksAt(groupStart))
4077
+ }));
4078
+ groupStart = offset;
4079
+ }
4080
+ return nodes;
4081
+ };
4082
+ for (let plannedIndex = plan.planned.length - 1; plannedIndex >= 0; plannedIndex--) {
4083
+ const change = plan.planned[plannedIndex];
4084
+ if (change === void 0) panic("A planned replacement change went missing", { plannedIndex });
2890
4085
  const doc = nextTr.doc;
2891
4086
  let at;
2892
4087
  let marks;
4088
+ let removedFrom = null;
2893
4089
  if (change.insertion !== void 0) {
2894
4090
  at = change.insertion.at;
2895
4091
  marks = change.insertion.marks(doc);
@@ -2899,6 +4095,7 @@ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, re
2899
4095
  const styleSource = plan.replacementStyleSource(change.change);
2900
4096
  if (first === void 0 || last === void 0 || styleSource === null) panic("A planned replacement lost the characters it removes");
2901
4097
  at = last.to;
4098
+ removedFrom = first.from;
2902
4099
  marks = addCarriedMarks(inheritedReplacementMarks(doc, styleSource.from, styleSource.to), surveyReplacedAnnotations(doc, first.from, last.to).carried);
2903
4100
  for (const piece of change.pieces) {
2904
4101
  nextTr = nextTr.addMark(piece.from, piece.to, deletion);
@@ -2906,21 +4103,36 @@ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, re
2906
4103
  }
2907
4104
  }
2908
4105
  if (change.text.length === 0) continue;
2909
- const nodes = cleanTextInlineNodes({
2910
- schema,
2911
- text: change.text,
2912
- marks: insertedMarks(marks)
2913
- });
2914
- nextTr = nextTr.insert(at, nodes);
2915
- const end = at + Fragment.fromArray(nodes).size;
2916
- nextTr = nextTr.addMark(at, end, insertion);
2917
- if (commentMark) nextTr = nextTr.addMark(at, end, commentMark);
4106
+ const pieces = insertedNodes(change.text, replacementStarts[plannedIndex] ?? 0, marks);
4107
+ const positions = removedFrom === null ? pieces.map(() => at) : placeTrackedInsertion(nextTr.doc, removedFrom, at, pieces.map((node) => node.marks));
4108
+ const runs = [];
4109
+ for (const [index, piece] of pieces.entries()) {
4110
+ const position = positions[index] ?? at;
4111
+ const run = runs.at(-1);
4112
+ if (run?.at === position) run.pieces.push(piece);
4113
+ else runs.push({
4114
+ at: position,
4115
+ pieces: [piece]
4116
+ });
4117
+ }
4118
+ for (const run of runs.toReversed()) {
4119
+ let nodes = run.pieces;
4120
+ if (removedFrom === null) {
4121
+ const pieceMarks = commentsForTrackedInsertion(nextTr.doc, run.at, run.pieces.map((node) => node.marks));
4122
+ nodes = run.pieces.map((node, index) => node.mark(pieceMarks[index] ?? node.marks));
4123
+ }
4124
+ nextTr = nextTr.insert(run.at, nodes);
4125
+ const end = run.at + Fragment.fromArray(nodes).size;
4126
+ nextTr = nextTr.addMark(run.at, end, insertion);
4127
+ if (commentMark) nextTr = nextTr.addMark(run.at, end, commentMark);
4128
+ }
2918
4129
  }
2919
4130
  return {
2920
4131
  type: "applied",
2921
4132
  transaction: nextTr,
2922
4133
  backgroundRevisionIds,
2923
- nextRevisionId
4134
+ nextRevisionId,
4135
+ unifiesFormatting
2924
4136
  };
2925
4137
  };
2926
4138
  /**
@@ -2980,7 +4192,7 @@ const clearTouchedBackground = ({ tr, ranges, styleResolver }) => {
2980
4192
  });
2981
4193
  return nextTr;
2982
4194
  };
2983
- const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId }) => {
4195
+ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId, noteReferences }) => {
2984
4196
  const anchor = snapshot.anchors[blockId];
2985
4197
  if (!anchor) return {
2986
4198
  type: "skip",
@@ -3006,7 +4218,10 @@ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId
3006
4218
  type: "skip",
3007
4219
  reason: "changedBlock"
3008
4220
  };
3009
- const cleanBlock = buildCleanBlockText(live.node, live.from);
4221
+ const cleanBlock = buildCleanBlockText(live.node, live.from, {
4222
+ fieldResults: "text",
4223
+ noteReferences
4224
+ });
3010
4225
  const currentText = cleanBlock.text;
3011
4226
  const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText));
3012
4227
  const structuralBoundaryHash = hashFolioAIBlockStructuralBoundaries(cleanBlock);
@@ -3024,13 +4239,52 @@ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId
3024
4239
  currentTextHash
3025
4240
  };
3026
4241
  };
3027
- const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, doc }) => {
4242
+ const resolveOperation = (args) => {
4243
+ const resolution = resolveOperationTarget(args);
4244
+ if (resolution.type === "skip") return resolution;
4245
+ return (noteReferenceEditsAllowed() ? null : refuseNoteReferenceEdit(resolution.operation, args.noteReferences)) ?? resolution;
4246
+ };
4247
+ /**
4248
+ * A text change may rewrite the prose around a footnote or endnote reference
4249
+ * but never the reference: its marker is structure the clean text shows, not
4250
+ * characters it holds. The replacement has to keep every marker its match
4251
+ * covers, in order, and may not write a marker-shaped string of its own; the
4252
+ * replacement's inline emphasis would rebuild the match whole, reference
4253
+ * included, so it cannot carry one either. Checked before anything is applied,
4254
+ * so direct and tracked mode refuse the same operations.
4255
+ */
4256
+ const refuseNoteReferenceEdit = (item, noteReferences) => {
4257
+ const { operation } = item;
4258
+ let replacement;
4259
+ if (operation.type === "replaceInBlock" || operation.type === "replaceRange") replacement = operation.replace;
4260
+ else if (operation.type === "replaceBlock") replacement = operation.text;
4261
+ else return null;
4262
+ const cleanBlock = buildCleanBlockText(item.blockNode, item.blockFrom, {
4263
+ fieldResults: "text",
4264
+ noteReferences
4265
+ });
4266
+ const span = replacedCleanSpan({ operation }, cleanBlock);
4267
+ if (span === null) return null;
4268
+ const spans = noteReferenceSpansWithin(cleanBlock, span.start, span.start + span.text.length);
4269
+ const rebuildsBlock = operation.type === "replaceBlock" && operation.preserveFormatting === false;
4270
+ if (spans.length > 0 && (hasInlineEmphasis(replacement) || rebuildsBlock)) return {
4271
+ type: "skip",
4272
+ reason: "protectedReference"
4273
+ };
4274
+ if (operation.type === "replaceBlock" && replacement === span.text) return null;
4275
+ return segmentsAroundNoteReferences(span.text, stripInlineEmphasisMarkers(replacement), spans) === null ? {
4276
+ type: "skip",
4277
+ reason: "protectedReference"
4278
+ } : null;
4279
+ };
4280
+ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, noteReferences, doc }) => {
3028
4281
  const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
3029
4282
  const primaryBlock = resolveStableBlock({
3030
4283
  snapshot,
3031
4284
  blockId,
3032
4285
  liveBlocks,
3033
- liveBlocksByParaId
4286
+ liveBlocksByParaId,
4287
+ noteReferences
3034
4288
  });
3035
4289
  if (primaryBlock.type === "skip") return primaryBlock;
3036
4290
  const { blockNode, blockFrom, blockTo, cleanBlock, currentText, currentTextHash } = primaryBlock;
@@ -3049,6 +4303,16 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3049
4303
  type: "skip",
3050
4304
  reason: "staleRange"
3051
4305
  };
4306
+ const characterSplit = describeCharacterSplit(currentText, [startOffset, endOffset], operation.type === "replaceRange" ? "grapheme" : "codePoint");
4307
+ if (characterSplit !== null) return {
4308
+ type: "skip",
4309
+ reason: "splitsCharacter",
4310
+ message: characterSplit
4311
+ };
4312
+ if (cutsIntoNoteReference(cleanBlock, startOffset, endOffset)) return {
4313
+ type: "skip",
4314
+ reason: "protectedReference"
4315
+ };
3052
4316
  const range = resolveCleanTextRange({
3053
4317
  cleanBlock,
3054
4318
  startOffset,
@@ -3079,7 +4343,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3079
4343
  const isInsertAfter = operation.type === "insertAfterBlock";
3080
4344
  let insertFrom;
3081
4345
  if (tableBoundary) insertFrom = isInsertAfter ? tableBoundary.after : tableBoundary.before;
3082
- else insertFrom = isInsertAfter ? blockTo : blockFrom;
4346
+ else insertFrom = isInsertAfter ? endOfParagraphWithItsTextBoxes(doc, blockFrom) : blockFrom;
3083
4347
  const insertTexts = operation.lineBreakMode !== "inline" && LINE_BREAK_PATTERN.test(operation.text) ? splitInsertParagraphTexts(operation.text) : [operation.text];
3084
4348
  return {
3085
4349
  type: "resolved",
@@ -3103,7 +4367,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3103
4367
  const tableBoundary = findOutermostTableBoundary(doc, blockFrom);
3104
4368
  let insertFrom;
3105
4369
  if (tableBoundary) insertFrom = position === "after" ? tableBoundary.after : tableBoundary.before;
3106
- else insertFrom = position === "after" ? blockTo : blockFrom;
4370
+ else insertFrom = position === "after" ? endOfParagraphWithItsTextBoxes(doc, blockFrom) : blockFrom;
3107
4371
  return {
3108
4372
  type: "resolved",
3109
4373
  operation: {
@@ -3122,10 +4386,20 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3122
4386
  blockFrom,
3123
4387
  position: operation.position ?? "after"
3124
4388
  });
3125
- if (!insertion || (operation.cellTexts?.length ?? 0) > insertion.columnCount) return {
4389
+ if (!insertion) return {
3126
4390
  type: "skip",
3127
4391
  reason: "unsupportedBlock"
3128
4392
  };
4393
+ const overflow = describeUnplacedCellTexts({
4394
+ cellTexts: operation.cellTexts,
4395
+ capacity: insertion.cells.length,
4396
+ target: "row"
4397
+ });
4398
+ if (overflow !== null) return {
4399
+ type: "skip",
4400
+ reason: "payloadDoesNotFit",
4401
+ message: overflow
4402
+ };
3129
4403
  return {
3130
4404
  type: "resolved",
3131
4405
  operation: {
@@ -3248,7 +4522,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3248
4522
  snapshot,
3249
4523
  blockId: operation.endBlockId,
3250
4524
  liveBlocks,
3251
- liveBlocksByParaId
4525
+ liveBlocksByParaId,
4526
+ noteReferences
3252
4527
  });
3253
4528
  if (endTarget.type === "skip") return endTarget;
3254
4529
  const endCell = findEnclosingTableCell(doc, endTarget.blockFrom);
@@ -3320,7 +4595,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3320
4595
  if (operation.type === "insertTable") {
3321
4596
  const boundary = findOutermostTableBoundary(doc, blockFrom) ?? {
3322
4597
  before: blockFrom,
3323
- after: blockTo
4598
+ after: endOfParagraphWithItsTextBoxes(doc, blockFrom)
3324
4599
  };
3325
4600
  const insertFrom = (operation.position ?? "after") === "after" ? boundary.after : boundary.before;
3326
4601
  return {
@@ -3382,6 +4657,12 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3382
4657
  type: "skip",
3383
4658
  reason: "staleRange"
3384
4659
  };
4660
+ const characterSplit = describeCharacterSplit(currentText, [startOffset, endOffset], "grapheme");
4661
+ if (characterSplit !== null) return {
4662
+ type: "skip",
4663
+ reason: "splitsCharacter",
4664
+ message: characterSplit
4665
+ };
3385
4666
  const range = resolveCleanTextRange({
3386
4667
  cleanBlock,
3387
4668
  startOffset,
@@ -3420,10 +4701,14 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3420
4701
  }
3421
4702
  };
3422
4703
  }
4704
+ if (operation.type === "replaceBlock" && isPendingDeletion(doc, blockFrom, blockNode, currentText)) return {
4705
+ type: "skip",
4706
+ reason: "pendingDeletion"
4707
+ };
3423
4708
  if (operation.type === "deleteBlock" || operation.type === "replaceBlock") {
3424
4709
  const replaceChangesText = operation.type === "replaceBlock" && operation.text !== currentText;
3425
4710
  const replaceChangesStyle = operation.type === "replaceBlock" && operation.styleId !== void 0 && operation.styleId !== (expectParagraphAttrs(blockNode).styleId ?? null);
3426
- if (replaceChangesText && cleanBlock.structuralBoundaries.length > 0) return {
4711
+ if (replaceChangesText && cleanBlock.structuralBoundaries.some((boundary) => boundary.type !== "field" && boundary.type !== "noteReference")) return {
3427
4712
  type: "skip",
3428
4713
  reason: "unsupportedBlock"
3429
4714
  };
@@ -3503,7 +4788,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3503
4788
  reason: "noopOperation"
3504
4789
  };
3505
4790
  const quote = getOperationQuote(operation);
3506
- const range = resolveTextInCleanBlock(cleanBlock, quote || currentText);
4791
+ const range = resolveTextInCleanBlock(cleanBlock, quote || currentText, operation.type === "replaceInBlock" ? "grapheme" : "codePoint");
3507
4792
  if (range.type !== "resolved") return range;
3508
4793
  return {
3509
4794
  type: "resolved",
@@ -3517,7 +4802,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3517
4802
  }
3518
4803
  };
3519
4804
  };
3520
- const resolveTextInCleanBlock = (cleanBlock, find) => {
4805
+ const resolveTextInCleanBlock = (cleanBlock, find, strictness) => {
3521
4806
  if (find.length === 0) return {
3522
4807
  type: "skip",
3523
4808
  reason: "emptyOperation"
@@ -3532,6 +4817,16 @@ const resolveTextInCleanBlock = (cleanBlock, find) => {
3532
4817
  type: "skip",
3533
4818
  reason: "ambiguousFind"
3534
4819
  };
4820
+ const characterSplit = describeCharacterSplit(text, [firstIndex, firstIndex + find.length], strictness);
4821
+ if (characterSplit !== null) return {
4822
+ type: "skip",
4823
+ reason: "splitsCharacter",
4824
+ message: `the match's ${characterSplit}`
4825
+ };
4826
+ if (cutsIntoNoteReference(cleanBlock, firstIndex, firstIndex + find.length)) return {
4827
+ type: "skip",
4828
+ reason: "protectedReference"
4829
+ };
3535
4830
  const range = resolveCleanTextRange({
3536
4831
  cleanBlock,
3537
4832
  startOffset: firstIndex,
@@ -3557,9 +4852,128 @@ const getTextRangeFromCleanBlock = (cleanBlock) => {
3557
4852
  };
3558
4853
  };
3559
4854
  const getOperationCommentText = (operation) => "comment" in operation ? operation.comment?.text : void 0;
4855
+ /**
4856
+ * Every paragraph style an operation names, with the field that names it.
4857
+ * `null` clears the style and names none; an omitted one keeps the style the
4858
+ * paragraph or its anchor already carries, which the document holds by
4859
+ * construction.
4860
+ */
4861
+ const operationParagraphStyleReferences = (operation) => {
4862
+ switch (operation.type) {
4863
+ case "insertAfterBlock":
4864
+ case "insertBeforeBlock":
4865
+ case "replaceBlock": return [{
4866
+ path: "styleId",
4867
+ styleId: operation.styleId
4868
+ }];
4869
+ case "setBlockParagraphProperties": return [{
4870
+ path: "properties.styleId",
4871
+ styleId: operation.properties.styleId
4872
+ }];
4873
+ case "splitBlock": return [{
4874
+ path: "firstParagraphProperties.styleId",
4875
+ styleId: operation.firstParagraphProperties?.styleId
4876
+ }, {
4877
+ path: "secondParagraphProperties.styleId",
4878
+ styleId: operation.secondParagraphProperties?.styleId
4879
+ }];
4880
+ case "mergeBlockWithNext": return [{
4881
+ path: "mergedParagraphProperties.styleId",
4882
+ styleId: operation.mergedParagraphProperties?.styleId
4883
+ }];
4884
+ default: return [];
4885
+ }
4886
+ };
4887
+ /**
4888
+ * The first paragraph style the operation names that the document does not
4889
+ * define as a paragraph style. A `w:pStyle` naming no definition, or a table
4890
+ * or character style, confers no formatting: the paragraph keeps its body
4891
+ * look while the operation reports a restyle. Refused here, before anything
4892
+ * is applied, the way an undefined block id is. `null` definitions (a state
4893
+ * without the styles plugin) cannot say what is defined, and check nothing.
4894
+ */
4895
+ const findUndefinedParagraphStyleReference = (operation, definitions) => {
4896
+ if (definitions === null) return;
4897
+ for (const { path, styleId } of operationParagraphStyleReferences(operation)) {
4898
+ if (styleId === null || styleId === void 0) continue;
4899
+ const definition = definitions.get(styleId);
4900
+ if (definition?.type !== "paragraph") return {
4901
+ path,
4902
+ styleId,
4903
+ definedAs: definition?.type
4904
+ };
4905
+ }
4906
+ };
4907
+ const MAX_LISTED_PARAGRAPH_STYLES = 12;
4908
+ const describeUndefinedParagraphStyle = ({ path, styleId, definedAs }, definitions) => {
4909
+ const defined = (definitions?.paragraphStyles() ?? []).map((style) => style.styleId);
4910
+ let available = "it defines no visible paragraph styles";
4911
+ if (defined.length > 0) available = `its paragraph styles include ${defined.slice(0, MAX_LISTED_PARAGRAPH_STYLES).join(", ")}${defined.length > MAX_LISTED_PARAGRAPH_STYLES ? ", …" : ""}`;
4912
+ return `${path} "${styleId}" ${definedAs === void 0 ? "names no style in this document" : `names a ${definedAs} style, not a paragraph style`} (${available}).`;
4913
+ };
4914
+ /** The `w:num` id a stated numbering names, when it names one. */
4915
+ const statedNumberingInstance = (numbering) => {
4916
+ if (numbering === null || numbering === void 0) return;
4917
+ const numId = Reflect.get(numbering, "numId");
4918
+ return typeof numId === "number" ? numId : void 0;
4919
+ };
4920
+ /**
4921
+ * Every numbering instance an operation names, with the field that names it.
4922
+ * `listLevel` is not among them: it keeps the instance the paragraph or its
4923
+ * anchor already references, which the document holds by construction.
4924
+ */
4925
+ const operationNumberingReferences = (operation) => {
4926
+ switch (operation.type) {
4927
+ case "insertAfterBlock":
4928
+ case "insertBeforeBlock": return [{
4929
+ path: "numbering",
4930
+ numbering: operation.numbering
4931
+ }];
4932
+ case "setBlockParagraphProperties": return [{
4933
+ path: "properties.numbering",
4934
+ numbering: operation.properties.numbering
4935
+ }];
4936
+ case "splitBlock": return [{
4937
+ path: "firstParagraphProperties.numbering",
4938
+ numbering: operation.firstParagraphProperties?.numbering
4939
+ }, {
4940
+ path: "secondParagraphProperties.numbering",
4941
+ numbering: operation.secondParagraphProperties?.numbering
4942
+ }];
4943
+ case "mergeBlockWithNext": return [{
4944
+ path: "mergedParagraphProperties.numbering",
4945
+ numbering: operation.mergedParagraphProperties?.numbering
4946
+ }];
4947
+ default: return [];
4948
+ }
4949
+ };
4950
+ /**
4951
+ * The first numbering instance the operation names that the document does not
4952
+ * define. A paragraph referencing a missing `w:num` is a model no save can
4953
+ * write, so it is refused here, before anything is applied, the way a block id
4954
+ * naming no block is. `null` instance ids (a state without the numbering
4955
+ * plugin) cannot say what is defined, and check nothing.
4956
+ */
4957
+ const findUndefinedNumberingReference = (operation, instanceIds) => {
4958
+ if (instanceIds === null) return;
4959
+ for (const { path, numbering } of operationNumberingReferences(operation)) {
4960
+ const numId = statedNumberingInstance(numbering);
4961
+ if (numId !== void 0 && !instanceIds.has(numId)) return {
4962
+ path,
4963
+ numId
4964
+ };
4965
+ }
4966
+ };
4967
+ const MAX_LISTED_NUMBERING_INSTANCES = 12;
4968
+ const describeUndefinedNumbering = ({ path, numId }, instanceIds) => {
4969
+ const defined = [...instanceIds ?? []].sort((left, right) => left - right);
4970
+ let available = "it defines none";
4971
+ if (defined.length > 0) available = `it defines ${defined.slice(0, MAX_LISTED_NUMBERING_INSTANCES).join(", ")}${defined.length > MAX_LISTED_NUMBERING_INSTANCES ? ", …" : ""}`;
4972
+ return `${path}.numId ${String(numId)} names no numbering instance in this document (${available}).`;
4973
+ };
3560
4974
  const getOperationQuote = (operation) => {
3561
4975
  if (operation.type === "replaceInBlock") return operation.find;
3562
4976
  if (operation.type === "commentOnBlock") return operation.quote;
3563
4977
  };
3564
4978
  //#endregion
3565
- export { applyFolioAIEditOperations, previewFolioAIEditOperations };
4979
+ export { applyFolioAIEditOperations, previewFolioAIEditOperations, previewFolioAIEditOperationsWithResult };