@stll/folio-core 0.52.0 → 0.54.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 (389) 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 +1779 -141
  4. package/dist/ai-edits/batch-claims.d.ts +145 -0
  5. package/dist/ai-edits/batch-claims.js +275 -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 +91 -7
  14. package/dist/ai-edits/headless.js +520 -51
  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 +6 -4
  26. package/dist/ai-edits/read.js +113 -50
  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/revisionStretches.d.ts +17 -0
  30. package/dist/ai-edits/revisionStretches.js +80 -0
  31. package/dist/ai-edits/snapshot.d.ts +36 -3
  32. package/dist/ai-edits/snapshot.js +180 -25
  33. package/dist/ai-edits/table-cell-mutations.d.ts +4 -2
  34. package/dist/ai-edits/table-cell-mutations.js +6 -2
  35. package/dist/ai-edits/table-geometry.d.ts +1 -1
  36. package/dist/ai-edits/table-row-column-mutations.d.ts +44 -17
  37. package/dist/ai-edits/table-row-column-mutations.js +86 -34
  38. package/dist/ai-edits/types.d.ts +155 -16
  39. package/dist/compare/compare.d.ts +1 -1
  40. package/dist/compare/compare.js +65 -16
  41. package/dist/compare/content-alignment.d.ts +5 -1
  42. package/dist/compare/content-alignment.js +52 -34
  43. package/dist/compare/content-types.d.ts +7 -0
  44. package/dist/compare/content.d.ts +4 -1
  45. package/dist/compare/content.js +6 -1
  46. package/dist/compare/formatting.js +2 -1
  47. package/dist/compare/inline-atoms.d.ts +21 -4
  48. package/dist/compare/inline-atoms.js +138 -16
  49. package/dist/compare/inline-provenance.js +1 -1
  50. package/dist/compare/plan.js +10 -5
  51. package/dist/compare/scenario.js +14 -3
  52. package/dist/compare/section-boundary-properties.js +1 -1
  53. package/dist/compare/types.d.ts +4 -2
  54. package/dist/compare/types.js +2 -1
  55. package/dist/compare/verification.d.ts +1 -1
  56. package/dist/compare/verification.js +15 -3
  57. package/dist/controller/committedLayoutDiff.d.ts +25 -0
  58. package/dist/controller/committedLayoutDiff.js +71 -0
  59. package/dist/controller/contentControlWidgetController.d.ts +1 -1
  60. package/dist/controller/folioEditor.d.ts +1 -1
  61. package/dist/controller/fontReadiness.d.ts +70 -3
  62. package/dist/controller/fontReadiness.js +100 -10
  63. package/dist/controller/headerFooterEditorManager.js +6 -1
  64. package/dist/controller/layoutPipeline.d.ts +12 -10
  65. package/dist/controller/layoutPipeline.js +65 -11
  66. package/dist/controller/layoutRunOptions.d.ts +24 -0
  67. package/dist/controller/layoutRunOptions.js +17 -0
  68. package/dist/controller/layoutScheduler.d.ts +19 -14
  69. package/dist/controller/layoutScheduler.js +15 -12
  70. package/dist/controller/layoutSession.d.ts +26 -2
  71. package/dist/controller/layoutSession.js +1 -1
  72. package/dist/controller/noteEditorManager.js +8 -3
  73. package/dist/document-operations.d.ts +11 -2
  74. package/dist/document-operations.js +230 -21
  75. package/dist/docx/altChunk.d.ts +8 -0
  76. package/dist/docx/altChunk.js +17 -0
  77. package/dist/docx/archiveInflation.d.ts +127 -0
  78. package/dist/docx/archiveInflation.js +181 -0
  79. package/dist/docx/attributeRemainder.d.ts +1 -1
  80. package/dist/docx/blockContentParser.d.ts +64 -1
  81. package/dist/docx/blockContentParser.js +17 -4
  82. package/dist/docx/blockCustomXmlShell.d.ts +7 -0
  83. package/dist/docx/blockCustomXmlShell.js +14 -0
  84. package/dist/docx/blockPlainText.js +7 -0
  85. package/dist/docx/commentAnchorIndex.js +1 -1
  86. package/dist/docx/commentRangeIntegrity.js +8 -3
  87. package/dist/docx/commentReferenceNormalization.js +1 -1
  88. package/dist/docx/commentReplyMarkers.js +20 -1
  89. package/dist/docx/compatibility.d.ts +1 -1
  90. package/dist/docx/compatibility.js +34 -13
  91. package/dist/docx/containerChildren.gen.d.ts +1 -1
  92. package/dist/docx/containerChildren.gen.js +1 -0
  93. package/dist/docx/documentParser.js +12 -12
  94. package/dist/docx/drawingIdNormalization.d.ts +3 -1
  95. package/dist/docx/drawingIdNormalization.js +6 -1
  96. package/dist/docx/ensureParaIds.js +119 -97
  97. package/dist/docx/footnoteParser.js +8 -4
  98. package/dist/docx/headerFooterParser.js +3 -3
  99. package/dist/docx/headerFooterReferenceNormalization.d.ts +20 -1
  100. package/dist/docx/headerFooterReferenceNormalization.js +52 -27
  101. package/dist/docx/hyperlinkParser.d.ts +1 -1
  102. package/dist/docx/index.d.ts +1 -1
  103. package/dist/docx/listNumberingInstances.d.ts +59 -0
  104. package/dist/docx/listNumberingInstances.js +249 -0
  105. package/dist/docx/metadataPrivacy.js +24 -1
  106. package/dist/docx/normalizeBaseDirection.js +2 -1
  107. package/dist/docx/noteReferenceMark.d.ts +15 -0
  108. package/dist/docx/noteReferenceMark.js +48 -0
  109. package/dist/docx/opaqueCarrier.d.ts +7 -0
  110. package/dist/docx/opaqueCarrier.js +38 -0
  111. package/dist/docx/packageParts.js +4 -1
  112. package/dist/docx/paragraphParser.js +96 -27
  113. package/dist/docx/paragraphPropertySource.d.ts +4 -2
  114. package/dist/docx/paragraphPropertySource.js +10 -2
  115. package/dist/docx/paragraphTraversal.js +1 -0
  116. package/dist/docx/parseWarningMessage.js +4 -1
  117. package/dist/docx/parser.js +175 -22
  118. package/dist/docx/rezip.js +31 -7
  119. package/dist/docx/sdtProperties.js +1 -1
  120. package/dist/docx/sdtPropertiesPatch.js +3 -0
  121. package/dist/docx/selectiveSave.js +16 -9
  122. package/dist/docx/selectiveXmlPatch.d.ts +3 -1
  123. package/dist/docx/selectiveXmlPatch.js +186 -39
  124. package/dist/docx/serializer/blockCustomXmlSerializer.d.ts +5 -0
  125. package/dist/docx/serializer/blockCustomXmlSerializer.js +4 -0
  126. package/dist/docx/serializer/documentSerializer.js +2 -0
  127. package/dist/docx/serializer/headerFooterSerializer.js +2 -0
  128. package/dist/docx/serializer/noteSerializer.js +3 -1
  129. package/dist/docx/serializer/paragraphSerializer.js +33 -9
  130. package/dist/docx/serializer/tableSerializer.js +25 -38
  131. package/dist/docx/server/boundedArchive.d.ts +8 -13
  132. package/dist/docx/server/boundedArchive.js +70 -59
  133. package/dist/docx/server/createBilingualDocument.js +3 -3
  134. package/dist/docx/server/extractDocxText.d.ts +8 -2
  135. package/dist/docx/server/extractDocxText.js +2 -2
  136. package/dist/docx/server/validateDocxConformance.d.ts +1 -1
  137. package/dist/docx/server/validateDocxConformance.js +1 -0
  138. package/dist/docx/tableParser.d.ts +157 -1
  139. package/dist/docx/tableParser.js +89 -10
  140. package/dist/docx/textBoxParser.js +1 -1
  141. package/dist/docx/unzip.d.ts +20 -2
  142. package/dist/docx/unzip.js +136 -25
  143. package/dist/docx/wordprocessingPrefixes.d.ts +78 -0
  144. package/dist/docx/wordprocessingPrefixes.js +286 -0
  145. package/dist/fonts/embeddedFonts.js +1 -1
  146. package/dist/i18n/messages/catalogs.gen.d.ts +170 -34
  147. package/dist/i18n/messages/catalogs.gen.js +187 -51
  148. package/dist/i18n/messages/messages.gen.d.ts +10 -2
  149. package/dist/internal/compare/inline-presentation.d.ts +2 -1
  150. package/dist/internal/compare/inline-presentation.js +11 -10
  151. package/dist/internal/headlessRevisionResolutionGuard.d.ts +2 -8
  152. package/dist/internal/headlessRevisionResolutionGuard.js +2 -11
  153. package/dist/internal/indexedPositionMap.d.ts +11 -0
  154. package/dist/internal/indexedPositionMap.js +42 -0
  155. package/dist/internal/pageBreakRunSourceDescendantIndex.js +2 -1
  156. package/dist/internal/revisionResolutionEdits.d.ts +13 -0
  157. package/dist/internal/revisionResolutionEdits.js +19 -0
  158. package/dist/internal/revisionResolutionInline.d.ts +24 -0
  159. package/dist/internal/{headlessRevisionResolution.js → revisionResolutionInline.js} +61 -43
  160. package/dist/internal/revisionResolutionStep.d.ts +34 -0
  161. package/dist/internal/revisionResolutionStep.js +282 -0
  162. package/dist/internal/revisionResolutionTracking.d.ts +11 -0
  163. package/dist/internal/revisionResolutionTracking.js +124 -0
  164. package/dist/internal/wholeStoryRevisionResolution.d.ts +56 -0
  165. package/dist/internal/wholeStoryRevisionResolution.js +600 -0
  166. package/dist/layout-bridge/convert/markupViewFlow.d.ts +20 -0
  167. package/dist/layout-bridge/convert/markupViewFlow.js +123 -0
  168. package/dist/layout-bridge/convert/tableConversion.js +1 -1
  169. package/dist/layout-bridge/convert/toFlowBlocks.js +10 -1
  170. package/dist/layout-engine/layoutInstrumentation.d.ts +9 -1
  171. package/dist/layout-engine/layoutInstrumentation.js +4 -1
  172. package/dist/layout-engine/measure/cache.d.ts +18 -1
  173. package/dist/layout-engine/measure/cache.js +31 -1
  174. package/dist/layout-engine/measure/font-metrics.worker.js +3 -7
  175. package/dist/layout-engine/measure/measureContainer.js +7 -47
  176. package/dist/layout-engine/measure/measureHelpers.d.ts +6 -1
  177. package/dist/layout-engine/measure/measureHelpers.js +22 -2
  178. package/dist/layout-engine/measure/measureParagraph.js +2 -2
  179. package/dist/layout-engine/measure/measureWorker.js +2 -3
  180. package/dist/layout-engine/measure/measureWorkerProtocol.d.ts +6 -7
  181. package/dist/layout-engine/measure/measureWorkerProtocol.js +1 -2
  182. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +12 -1
  183. package/dist/layout-engine/measure/paragraphMeasureShared.js +14 -1
  184. package/dist/layout-engine/types.d.ts +6 -0
  185. package/dist/layout-painter/renderPage.js +1 -0
  186. package/dist/layout-painter/renderParagraph.js +1 -1
  187. package/dist/managers/ContextMenuManager.js +5 -2
  188. package/dist/managers/types.d.ts +3 -0
  189. package/dist/markdown/escape.d.ts +2 -2
  190. package/dist/markdown/escape.js +4 -5
  191. package/dist/markdown/fromMarkdown.js +2 -1
  192. package/dist/markdown/internals.d.ts +11 -1
  193. package/dist/markdown/internals.js +24 -3
  194. package/dist/markdown/renderBlock.js +121 -25
  195. package/dist/markdown/renderParagraph.d.ts +13 -1
  196. package/dist/markdown/renderParagraph.js +85 -38
  197. package/dist/markdown/renderRuns.d.ts +10 -1
  198. package/dist/markdown/renderRuns.js +323 -69
  199. package/dist/markdown/renderTable.js +24 -12
  200. package/dist/markdown/trailers.js +2 -2
  201. package/dist/markdown/types.d.ts +29 -7
  202. package/dist/paged-layout/incrementalMeasure.d.ts +1 -2
  203. package/dist/paged-layout/incrementalMeasure.js +1 -9
  204. package/dist/paged-layout/transactionDirtyRange.d.ts +1 -3
  205. package/dist/paged-layout/transactionDirtyRange.js +29 -41
  206. package/dist/panel-layout.d.ts +83 -0
  207. package/dist/panel-layout.js +96 -0
  208. package/dist/prosemirror/anchoredTextBoxes.d.ts +37 -0
  209. package/dist/prosemirror/anchoredTextBoxes.js +156 -0
  210. package/dist/prosemirror/attrs/index.d.ts +6 -2
  211. package/dist/prosemirror/attrs/index.js +172 -1
  212. package/dist/prosemirror/clearRunColor.d.ts +6 -0
  213. package/dist/prosemirror/clearRunColor.js +46 -0
  214. package/dist/prosemirror/commands/comments.d.ts +7 -8
  215. package/dist/prosemirror/commands/comments.js +281 -212
  216. package/dist/prosemirror/commands/contentControls.d.ts +1 -1
  217. package/dist/prosemirror/commands/formatPainter.d.ts +1 -1
  218. package/dist/prosemirror/commands/formatPainter.js +1 -1
  219. package/dist/prosemirror/commands/formatting.d.ts +1 -1
  220. package/dist/prosemirror/commands/hyperlink.js +2 -1
  221. package/dist/prosemirror/commands/index.d.ts +2 -2
  222. package/dist/prosemirror/commands/index.js +2 -2
  223. package/dist/prosemirror/commands/paragraph.d.ts +7 -1
  224. package/dist/prosemirror/commands/paragraph.js +9 -1
  225. package/dist/prosemirror/commands/paragraphBookmarkJoin.d.ts +13 -0
  226. package/dist/prosemirror/commands/paragraphBookmarkJoin.js +22 -0
  227. package/dist/prosemirror/commands/pastePlainText.d.ts +1 -1
  228. package/dist/prosemirror/commands/propertyChangeScope.d.ts +3 -2
  229. package/dist/prosemirror/commands/propertyChangeScope.js +24 -3
  230. package/dist/prosemirror/commands/resolveAllTableChanges.d.ts +40 -0
  231. package/dist/prosemirror/commands/resolveAllTableChanges.js +620 -0
  232. package/dist/prosemirror/commands/resolveNodePropertyChangeAttrs.d.ts +15 -0
  233. package/dist/prosemirror/commands/resolveNodePropertyChangeAttrs.js +46 -0
  234. package/dist/prosemirror/commands/resolveParagraphProperties.d.ts +16 -0
  235. package/dist/prosemirror/commands/resolveParagraphProperties.js +73 -0
  236. package/dist/prosemirror/commands/sectionBreak.js +2 -1
  237. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +15 -2
  238. package/dist/prosemirror/commands/tableCellMergeResolution.js +23 -7
  239. package/dist/prosemirror/containerFinalParagraph.d.ts +11 -5
  240. package/dist/prosemirror/containerFinalParagraph.js +7 -6
  241. package/dist/prosemirror/contentControlRevisions.d.ts +1 -1
  242. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -13
  243. package/dist/prosemirror/conversion/fromProseDoc.js +177 -605
  244. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -11
  245. package/dist/prosemirror/conversion/toProseDoc.js +70 -154
  246. package/dist/prosemirror/documentSchema.d.ts +8 -0
  247. package/dist/prosemirror/documentSchema.js +18 -0
  248. package/dist/prosemirror/emptyFieldResultRuns.d.ts +18 -0
  249. package/dist/prosemirror/emptyFieldResultRuns.js +33 -0
  250. package/dist/prosemirror/extensions/ExtensionManager.d.ts +1 -1
  251. package/dist/prosemirror/extensions/ExtensionManager.js +3 -1
  252. package/dist/prosemirror/extensions/StarterKit.js +5 -1
  253. package/dist/prosemirror/extensions/core/DocExtension.js +1 -1
  254. package/dist/prosemirror/extensions/core/ParagraphExtension.js +116 -63
  255. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +4 -1
  256. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +24 -3
  257. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.d.ts +20 -0
  258. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.js +43 -0
  259. package/dist/prosemirror/extensions/features/ListExtension.js +167 -128
  260. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +19 -2
  261. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +33 -20
  262. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +31 -2
  263. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +270 -99
  264. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +3 -2
  265. package/dist/prosemirror/extensions/features/pasteCleanup.js +2 -1
  266. package/dist/prosemirror/extensions/features/pastedHeadingStyles.js +1 -1
  267. package/dist/prosemirror/extensions/features/pastedHtmlLists.d.ts +12 -0
  268. package/dist/prosemirror/extensions/features/pastedHtmlLists.js +124 -0
  269. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +33 -11
  270. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +8 -4
  271. package/dist/prosemirror/extensions/marks/RunIdentityExtension.d.ts +21 -18
  272. package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +37 -17
  273. package/dist/prosemirror/extensions/marks/StrikeExtension.js +2 -2
  274. package/dist/prosemirror/extensions/marks/SubscriptExtension.js +2 -2
  275. package/dist/prosemirror/extensions/marks/SuperscriptExtension.js +2 -2
  276. package/dist/prosemirror/extensions/marks/TextColorExtension.js +4 -3
  277. package/dist/prosemirror/extensions/marks/markUtils.d.ts +15 -8
  278. package/dist/prosemirror/extensions/marks/markUtils.js +25 -10
  279. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.d.ts +12 -0
  280. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +46 -0
  281. package/dist/prosemirror/extensions/nodes/BlockCustomXmlExtension.d.ts +6 -0
  282. package/dist/prosemirror/extensions/nodes/BlockCustomXmlExtension.js +52 -0
  283. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +2 -2
  284. package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +3 -3
  285. package/dist/prosemirror/extensions/nodes/FieldExtension.js +50 -15
  286. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +5 -3
  287. package/dist/prosemirror/extensions/nodes/PreservedBlockExtension.js +4 -1
  288. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  289. package/dist/prosemirror/extensions/nodes/TableExtension.js +121 -226
  290. package/dist/prosemirror/extensions/types.d.ts +11 -1
  291. package/dist/prosemirror/findReplaceSelection.js +1 -1
  292. package/dist/prosemirror/hyperlinkRemoval.d.ts +9 -0
  293. package/dist/prosemirror/hyperlinkRemoval.js +129 -0
  294. package/dist/prosemirror/index.d.ts +5 -3
  295. package/dist/prosemirror/index.js +5 -3
  296. package/dist/prosemirror/indexedNodeLookup.d.ts +17 -0
  297. package/dist/prosemirror/indexedNodeLookup.js +103 -0
  298. package/dist/prosemirror/listAutoformatMarkers.d.ts +19 -0
  299. package/dist/prosemirror/listAutoformatMarkers.js +98 -0
  300. package/dist/prosemirror/listInstanceReferences.d.ts +12 -0
  301. package/dist/prosemirror/listInstanceReferences.js +72 -0
  302. package/dist/prosemirror/listLabels.d.ts +24 -0
  303. package/dist/prosemirror/listLabels.js +111 -0
  304. package/dist/prosemirror/listNumbering.d.ts +92 -0
  305. package/dist/prosemirror/listNumbering.js +330 -0
  306. package/dist/prosemirror/listRendering.d.ts +27 -0
  307. package/dist/prosemirror/listRendering.js +27 -0
  308. package/dist/prosemirror/listRenderingAttrs.d.ts +11 -2
  309. package/dist/prosemirror/listRenderingAttrs.js +8 -1
  310. package/dist/prosemirror/markupViewNotes.d.ts +8 -0
  311. package/dist/prosemirror/markupViewNotes.js +49 -0
  312. package/dist/prosemirror/markupViewProjection.d.ts +97 -0
  313. package/dist/prosemirror/markupViewProjection.js +88 -0
  314. package/dist/prosemirror/noteReferenceReview.d.ts +77 -0
  315. package/dist/prosemirror/noteReferenceReview.js +332 -0
  316. package/dist/prosemirror/numberingAttr.d.ts +9 -1
  317. package/dist/prosemirror/numberingAttr.js +9 -1
  318. package/dist/prosemirror/outlineLevelAttr.js +1 -1
  319. package/dist/prosemirror/paragraphIndentation.d.ts +10 -1
  320. package/dist/prosemirror/paragraphIndentation.js +32 -7
  321. package/dist/prosemirror/paragraphMarkJoin.d.ts +21 -0
  322. package/dist/prosemirror/paragraphMarkJoin.js +26 -0
  323. package/dist/prosemirror/paragraphPropertyCarry.d.ts +64 -0
  324. package/dist/prosemirror/paragraphPropertyCarry.js +158 -0
  325. package/dist/prosemirror/plugins/createDocScanPlugin.d.ts +1 -1
  326. package/dist/prosemirror/plugins/documentNumbering.d.ts +27 -1
  327. package/dist/prosemirror/plugins/documentNumbering.js +97 -11
  328. package/dist/prosemirror/plugins/documentStyleState.d.ts +26 -0
  329. package/dist/prosemirror/plugins/documentStyleState.js +32 -0
  330. package/dist/prosemirror/plugins/documentStyles.d.ts +3 -13
  331. package/dist/prosemirror/plugins/documentStyles.js +67 -17
  332. package/dist/prosemirror/plugins/index.d.ts +2 -1
  333. package/dist/prosemirror/plugins/index.js +2 -1
  334. package/dist/prosemirror/plugins/paragraphStyleResolution.d.ts +15 -0
  335. package/dist/prosemirror/plugins/paragraphStyleResolution.js +230 -0
  336. package/dist/prosemirror/plugins/suggestionMode.d.ts +27 -3
  337. package/dist/prosemirror/plugins/suggestionMode.js +490 -63
  338. package/dist/prosemirror/plugins/templateDirectives.d.ts +1 -1
  339. package/dist/prosemirror/plugins/transactionInvariants.d.ts +32 -0
  340. package/dist/prosemirror/plugins/transactionInvariants.js +86 -0
  341. package/dist/prosemirror/positionSweep.d.ts +24 -0
  342. package/dist/prosemirror/positionSweep.js +222 -0
  343. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +23 -3
  344. package/dist/prosemirror/rebaseParagraphRunFormatting.js +39 -13
  345. package/dist/prosemirror/rebaseParagraphRuns.d.ts +36 -0
  346. package/dist/prosemirror/rebaseParagraphRuns.js +99 -0
  347. package/dist/prosemirror/rejoinRunCarriers.d.ts +1 -1
  348. package/dist/prosemirror/replacedAnnotations.d.ts +33 -1
  349. package/dist/prosemirror/replacedAnnotations.js +170 -1
  350. package/dist/prosemirror/runFormattingFromMarks.d.ts +18 -0
  351. package/dist/prosemirror/runFormattingFromMarks.js +567 -0
  352. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +1 -1
  353. package/dist/prosemirror/runFormattingReconciliation.js +10 -9
  354. package/dist/prosemirror/schema/index.d.ts +2 -2
  355. package/dist/prosemirror/schema/index.js +3 -0
  356. package/dist/prosemirror/schema/nodes.d.ts +44 -11
  357. package/dist/prosemirror/sectionMarkEdits.d.ts +20 -0
  358. package/dist/prosemirror/sectionMarkEdits.js +59 -0
  359. package/dist/prosemirror/storyListNumbering.d.ts +17 -0
  360. package/dist/prosemirror/storyListNumbering.js +102 -0
  361. package/dist/prosemirror/styles/paragraphStyleCascade.d.ts +59 -0
  362. package/dist/prosemirror/styles/paragraphStyleCascade.js +145 -0
  363. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +21 -2
  364. package/dist/prosemirror/styles/resolvedStyleAttrs.js +38 -1
  365. package/dist/prosemirror/styles/tableStyleRegions.d.ts +31 -0
  366. package/dist/prosemirror/styles/tableStyleRegions.js +55 -0
  367. package/dist/prosemirror/tableCellPaste.d.ts +74 -0
  368. package/dist/prosemirror/tableCellPaste.js +525 -0
  369. package/dist/prosemirror/tableGridMutation.d.ts +87 -2
  370. package/dist/prosemirror/tableGridMutation.js +255 -2
  371. package/dist/prosemirror/tableRunIn.d.ts +51 -0
  372. package/dist/prosemirror/tableRunIn.js +165 -0
  373. package/dist/prosemirror/textInput.js +11 -2
  374. package/dist/prosemirror/trackedRevisionPath.d.ts +17 -0
  375. package/dist/prosemirror/trackedRevisionPath.js +32 -0
  376. package/dist/prosemirror/validation.js +5 -1
  377. package/dist/redline.js +93 -9
  378. package/dist/render-dom/BodySelectionOverlay.d.ts +4 -1
  379. package/dist/render-dom/BodySelectionOverlay.js +3 -2
  380. package/dist/server.d.ts +2 -2
  381. package/dist/utils/findReplace.js +2 -2
  382. package/dist/utils/mergeDocumentContent.js +1 -1
  383. package/dist/utils/noteReferenceLabels.d.ts +36 -0
  384. package/dist/utils/noteReferenceLabels.js +32 -0
  385. package/dist/utils/replaceText.js +1 -1
  386. package/dist/version-comparison.d.ts +1 -0
  387. package/dist/version-comparison.js +1 -0
  388. package/package.json +6 -5
  389. package/dist/internal/headlessRevisionResolution.d.ts +0 -23
@@ -1,38 +1,55 @@
1
1
  import { paragraphNumberingReference, paragraphNumberingReferenceId } from "../docx/numberingReference.js";
2
2
  import { formattingEquals } from "../docx/runConsolidator.js";
3
- import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
3
+ import { isStyleSourcedParagraphNumbering } from "../internal/paragraphFormattingSerialization.js";
4
+ import { deleteTextBoxesAnchoredAt, droppedTextBoxAnchorIds, endOfParagraphWithItsTextBoxes, gatherTextBoxesAfterParagraph, textBoxAnchorIdOf, textBoxAnchorIdsBetween } from "../prosemirror/anchoredTextBoxes.js";
5
+ import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs, expectTableRowAttrs } from "../prosemirror/attrs/index.js";
4
6
  import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
7
+ import { resolveParagraphChangeAttrs } from "../prosemirror/commands/resolveParagraphProperties.js";
5
8
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
6
9
  import { encloseWholeControls } from "../prosemirror/contentControlRevisions.js";
10
+ import { JOINED_RUNS_RESTYLED_META } from "../prosemirror/extensions/features/JoinedRunStyleExtension.js";
7
11
  import { requestDeterministicParaIds } from "../prosemirror/extensions/features/ParaIdAllocatorExtension.js";
8
12
  import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
13
+ import "../prosemirror/extensions/nodes/TextBoxAnchorExtension.js";
9
14
  import { CLEARED_LIST_RENDERING_ATTRS } from "../prosemirror/listMarker.js";
10
- import { paragraphNumberingAttr, readParagraphNumberingAttr } from "../prosemirror/numberingAttr.js";
15
+ import { paragraphNumberingAttr, readParagraphNumberingAttr, removedNumberingAttr } from "../prosemirror/numberingAttr.js";
11
16
  import { directParagraphAlignment } from "../prosemirror/paragraphAlignment.js";
17
+ import { styleResolvedParagraphFormatting } from "../prosemirror/paragraphFormattingProvenance.js";
12
18
  import { directParagraphIndentation, paragraphIndentationAttrPatch, paragraphIndentationEqual, paragraphIndentationFromFormatting, withDirectParagraphIndentation } from "../prosemirror/paragraphIndentation.js";
19
+ import { joinAtParagraphMark } from "../prosemirror/paragraphMarkJoin.js";
20
+ import { carryParagraphProperties, paragraphLeftAfter, propertiesSetInBatch, recordReplacedParagraphProperties } from "../prosemirror/paragraphPropertyCarry.js";
13
21
  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";
22
+ import { getDocumentNumbering, getDocumentNumberingInstanceIds } from "../prosemirror/plugins/documentNumbering.js";
23
+ import { getDocumentStyleDefinitions, getDocumentStyleResolver } from "../prosemirror/plugins/documentStyleState.js";
24
+ import { rebaseParagraphRuns } from "../prosemirror/rebaseParagraphRuns.js";
25
+ import { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentsAcrossBlockBoundary, commentsForTrackedInsertion, hasReplacedAnnotations, inheritedReplacementMarks, placeTrackedInsertion, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
17
26
  import { applyMarksToRunFormattingRepresentation, runFormattingInlineControlNodeName, selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
27
+ import { RUN_FORMATTING_MARK_NAMES } from "../prosemirror/runFormattingMarkNames.js";
18
28
  import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
19
- import { paragraphRunStyleContext, paragraphRunStyleContextAt } from "../prosemirror/runStyleFormatting.js";
20
- import { listLevelAttrPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
29
+ import { paragraphRunStyleContext, paragraphRunStyleContextAt, resolveParagraphBodyRunFormatting } from "../prosemirror/runStyleFormatting.js";
30
+ import { sectionPropertiesOf } from "../prosemirror/sectionCarrier.js";
31
+ import { listAttrsFromNumbering, listLevelAttrPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
32
+ import { removeRowsWithoutCellsAfterBatch } from "../prosemirror/tableGridMutation.js";
21
33
  import { isZeroWidthAnchor } from "../prosemirror/zeroWidthAnchors.js";
22
34
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
35
+ import { BatchClaims } from "./batch-claims.js";
23
36
  import { stripBlockIdentityAttrs } from "./block-identity.js";
37
+ import { describeCharacterSplit } from "./character-boundaries.js";
24
38
  import { buildCleanBlockText, resolveCleanTextRange } from "./clean-text.js";
25
39
  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";
28
- import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
40
+ import { applyTextChanges, changesFromSegments, keepAtomicSpans, planTextChanges, widenChangesToAtomicSpans } from "./minimal-replacement.js";
41
+ import { concreteListReference, resolveNewListOperations } from "./newListNumbering.js";
42
+ import { cutsIntoNoteReference, noteReferenceEditsAllowed, noteReferenceOf, noteReferenceSpansWithin, planChangesAroundNoteReferences, segmentsAroundNoteReferences } from "./note-references.js";
43
+ import { separateRevisionStretches } from "./revisionStretches.js";
44
+ import { collectNoteReferenceLabels, hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isHiddenTableRow, normalizeFolioAIBlockText } from "./snapshot.js";
45
+ import { isEmptyTableCell, mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
29
46
  import { planTableMutations } from "./table-mutation-plan.js";
30
- import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
47
+ import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, describeUnplacedCellTexts, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts } from "./table-row-column-mutations.js";
31
48
  import { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell } from "./table-targets.js";
32
49
  import { tableFromTemplate } from "./table-template.js";
33
50
  import { wordDiffSessionFromOptions } from "./word-diff.js";
34
51
  import { panic } from "better-result";
35
- import { Fragment } from "prosemirror-model";
52
+ import { Fragment, Mark } from "prosemirror-model";
36
53
  import { TableMap } from "prosemirror-tables";
37
54
  import { canJoin, canSplit } from "prosemirror-transform";
38
55
  //#region src/ai-edits/apply.ts
@@ -109,6 +126,12 @@ const SUGGESTED_SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
109
126
  "insertTableColumn",
110
127
  "deleteTableColumn"
111
128
  ]);
129
+ /** Operations that only add marks: they move no position of the document. */
130
+ const ANNOTATION_OPERATION_TYPES = /* @__PURE__ */ new Set([
131
+ "formatRange",
132
+ "commentOnRange",
133
+ "commentOnBlock"
134
+ ]);
112
135
  const REPLACE_BLOCK_IMPACT = {
113
136
  text: {
114
137
  changesStyle: false,
@@ -167,6 +190,7 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
167
190
  const styleChanged = nextStyleId !== void 0 && (attrs.styleId ?? null) !== nextStyleId;
168
191
  if (styleChanged) {
169
192
  patch["styleId"] = nextStyleId;
193
+ patch["_resolvedFormatting"] = styleResolvedParagraphFormatting(resolvedFormattingFromStyle);
170
194
  patch["alignmentFromStyle"] = resolvedFormattingFromStyle?.alignment;
171
195
  if (properties.alignment === void 0) patch["alignment"] = currentDirectAlignment ?? resolvedFormattingFromStyle?.alignment ?? null;
172
196
  if (properties.spacing === void 0) Object.assign(patch, paragraphSpacingAttrPatch({
@@ -182,25 +206,42 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
182
206
  else originalFormatting.styleId = nextStyleId;
183
207
  if (currentDirectAlignment !== void 0) originalFormatting.alignment = currentDirectAlignment;
184
208
  originalFormattingChanged = true;
209
+ patch["outlineLevel"] = attrs._originalFormatting?.outlineLevel ?? resolvedFormattingFromStyle?.outlineLevel ?? null;
210
+ Object.assign(patch, restyledListAttrs({
211
+ attrs: { ...attrs },
212
+ styleNumbering: resolvedFormattingFromStyle?.numPr,
213
+ removesNumbering: false,
214
+ numbering
215
+ }));
216
+ }
217
+ if (properties.outlineLevel !== void 0) {
218
+ const directOutlineLevel = properties.outlineLevel ?? void 0;
219
+ originalFormatting ??= {};
220
+ if (directOutlineLevel === void 0) Reflect.deleteProperty(originalFormatting, "outlineLevel");
221
+ else originalFormatting.outlineLevel = directOutlineLevel;
222
+ patch["outlineLevel"] = directOutlineLevel ?? resolvedFormattingFromStyle?.outlineLevel ?? null;
223
+ originalFormattingChanged = true;
185
224
  }
225
+ const numPrFromStyle = styleChanged ? readParagraphNumberingAttr(patch["numPrFromStyle"]) : attrs.numPrFromStyle;
186
226
  if (properties.numbering !== void 0) if (properties.numbering === null) {
187
- patch["numPr"] = null;
227
+ patch["numPr"] = removedNumberingAttr(numPrFromStyle);
188
228
  Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
189
229
  } else {
230
+ const listReference = concreteListReference(properties.numbering);
190
231
  Object.assign(patch, listLevelAttrPatch(attrs, {
191
- numId: properties.numbering.numId,
192
- ilvl: properties.numbering.level
232
+ numId: listReference.numId,
233
+ ilvl: listReference.level
193
234
  }, numbering));
194
235
  if (properties.listLevel === null) {
195
- patch["numPr"] = paragraphNumberingAttr(paragraphNumberingReference({ numId: properties.numbering.numId }));
236
+ patch["numPr"] = paragraphNumberingAttr(paragraphNumberingReference({ numId: listReference.numId }));
196
237
  patch["numPrFromStyle"] = null;
197
238
  }
198
239
  }
199
240
  else if (properties.listLevel !== void 0) if (properties.listLevel === null) {
200
- patch["numPr"] = null;
241
+ patch["numPr"] = removedNumberingAttr(numPrFromStyle);
201
242
  Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
202
243
  } else {
203
- const numId = paragraphNumberingReferenceId(readParagraphNumberingAttr(node.attrs["numPr"]) ?? void 0);
244
+ const numId = paragraphNumberingReferenceId(readParagraphNumberingAttr(styleChanged ? patch["numPr"] : node.attrs["numPr"]) ?? void 0);
204
245
  if (numId === void 0) {
205
246
  patch["numPr"] = paragraphNumberingAttr({
206
247
  kind: "levelOnly",
@@ -242,6 +283,24 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
242
283
  return Object.keys(patch).length > 0 ? patch : null;
243
284
  };
244
285
  const REVISION_MARK_NAMES = /* @__PURE__ */ new Set(["insertion", "deletion"]);
286
+ const hasOrdinaryTextRevision = (block) => {
287
+ let found = false;
288
+ block.descendants((node) => {
289
+ if (node.marks.some((mark) => REVISION_MARK_NAMES.has(mark.type.name) && mark.attrs["provenance"] !== "suggested")) {
290
+ found = true;
291
+ return false;
292
+ }
293
+ return true;
294
+ });
295
+ return found;
296
+ };
297
+ const hasOrdinaryParagraphMarkRevision = (block) => {
298
+ const mark = block.attrs["pPrMark"];
299
+ if (typeof mark !== "object" || mark === null || !("info" in mark)) return false;
300
+ const info = mark.info;
301
+ if (typeof info !== "object" || info === null) return false;
302
+ return !("provenance" in info) || info.provenance !== "suggested";
303
+ };
245
304
  /**
246
305
  * Strip insertion and deletion marks from the zero-width anchors the batch
247
306
  * touched.
@@ -251,10 +310,12 @@ const REVISION_MARK_NAMES = /* @__PURE__ */ new Set(["insertion", "deletion"]);
251
310
  * must keep its existing revision ownership. Marking is by range everywhere;
252
311
  * the guard lives here once rather than at every call site.
253
312
  */
254
- const withoutRevisionsOnZeroWidthAnchors = (tr, batchRevisionIds) => {
313
+ const withoutRevisionsOnZeroWidthAnchors = (tr, batchRevisionIds, deletedTextBoxAnchorIds) => {
255
314
  const anchors = [];
256
315
  tr.doc.descendants((node, pos) => {
257
316
  if (!isZeroWidthAnchor(node)) return true;
317
+ const textBoxAnchorId = textBoxAnchorIdOf(node);
318
+ if (textBoxAnchorId !== void 0 && deletedTextBoxAnchorIds.has(textBoxAnchorId)) return false;
258
319
  const marks = node.marks.filter(({ attrs, type }) => REVISION_MARK_NAMES.has(type.name) && batchRevisionIds.has(attrs["revisionId"]));
259
320
  if (marks.length > 0) anchors.push({
260
321
  from: pos,
@@ -272,7 +333,9 @@ const withoutRevisionsOnZeroWidthAnchors = (tr, batchRevisionIds) => {
272
333
  *
273
334
  * Skips what is already deleted, so an earlier revision's run is not marked
274
335
  * twice, and skips zero-width anchors because the operation did not create or
275
- * delete them; their existing revision ownership must remain unchanged.
336
+ * delete them; their existing revision ownership must remain unchanged. A
337
+ * text box's anchor is the exception: it stands for the box's drawing run,
338
+ * which is the paragraph's content, and the box follows it.
276
339
  */
277
340
  const undeletedContentAtomRanges = (tr, blockFrom) => {
278
341
  const position = tr.mapping.map(blockFrom);
@@ -280,7 +343,7 @@ const undeletedContentAtomRanges = (tr, blockFrom) => {
280
343
  if (!block?.isTextblock) return [];
281
344
  const ranges = [];
282
345
  block.forEach((child, offset) => {
283
- if (child.isText || isZeroWidthAnchor(child) || child.marks.some(({ type }) => type.name === "deletion")) return;
346
+ if (child.isText || isZeroWidthAnchor(child) && child.type.name !== "textBoxAnchor" || child.marks.some(({ type }) => type.name === "deletion")) return;
284
347
  const from = position + 1 + offset;
285
348
  ranges.push({
286
349
  from,
@@ -289,13 +352,23 @@ const undeletedContentAtomRanges = (tr, blockFrom) => {
289
352
  });
290
353
  return ranges;
291
354
  };
292
- /** The carrier pPr before this batch changed it, for final-mark rotation. */
293
- const paragraphPropertiesBeforeBatch = (node, batchRevisionIds) => {
355
+ /**
356
+ * The carrier pPr before any pending change to it, for final-mark rotation.
357
+ *
358
+ * A paragraph added after the carrier copies its pending property change, as
359
+ * a break typed at a paragraph's end does: rejecting the addition leaves the
360
+ * added paragraph (its mark is the one that stays), so that change is what
361
+ * gives the carrier's words back the properties they had.
362
+ */
363
+ const paragraphPropertiesBeforePendingChanges = (node) => {
294
364
  const propertyChanges = expectParagraphAttrs(node)._propertyChanges;
295
- if (!Array.isArray(propertyChanges)) return paragraphPropertiesSnapshot(node);
296
- const earliestBatchChange = propertyChanges.find(({ info }) => batchRevisionIds.has(info.id));
297
- if (!earliestBatchChange) return paragraphPropertiesSnapshot(node);
298
- return earliestBatchChange.previousFormatting ?? {};
365
+ const earliest = Array.isArray(propertyChanges) ? propertyChanges[0] : void 0;
366
+ return earliest === void 0 ? paragraphPropertiesSnapshot(node) : earliest.previousFormatting ?? {};
367
+ };
368
+ /** The pPr rejecting every pending change of a paragraph's properties leaves. */
369
+ const paragraphPropertiesOnReject = (node) => {
370
+ const [earliest] = expectParagraphAttrs(node)._propertyChanges ?? [];
371
+ return earliest ? earliest.previousFormatting ?? {} : paragraphPropertiesSnapshot(node);
299
372
  };
300
373
  const resolveFormattingFromStyle = ({ attrs, styleId, styleResolver }) => {
301
374
  const resolved = styleResolver?.resolveParagraphStyle(styleId).paragraphFormatting;
@@ -368,6 +441,7 @@ const preserveRunFormattingAcrossTrackedStyleChange = ({ allocateRevisionInfo, n
368
441
  * the change left alone from one it explicitly cleared when rejecting it.
369
442
  */
370
443
  const applyBlockParagraphProperties = ({ tr, position, node, properties, styleResolver, numbering = null, revisionInfo }) => {
444
+ const storedMarks = tr.storedMarks ?? tr.selection.$from.marks();
371
445
  const attrs = expectParagraphAttrs(node);
372
446
  const resolvedFormattingFromStyle = properties.styleId === void 0 ? resolveFormattingFromStyle({
373
447
  attrs,
@@ -390,6 +464,12 @@ const applyBlockParagraphProperties = ({ tr, position, node, properties, styleRe
390
464
  revisionId: null,
391
465
  revisionIds: []
392
466
  };
467
+ const nextStyleId = properties.styleId === void 0 ? attrs.styleId : properties.styleId;
468
+ if ((attrs.styleId ?? null) !== (nextStyleId ?? null)) patch["defaultTextFormatting"] = resolveParagraphBodyRunFormatting({
469
+ styleId: properties.styleId ?? void 0,
470
+ tableRunFormatting: attrs._tableRunFormatting,
471
+ styleResolver
472
+ }).defaultFormatting ?? null;
393
473
  const changeInfo = revisionInfo?.();
394
474
  const change = changeInfo ? {
395
475
  type: "paragraphPropertyChange",
@@ -414,12 +494,25 @@ const applyBlockParagraphProperties = ({ tr, position, node, properties, styleRe
414
494
  revisionIds: []
415
495
  };
416
496
  return {
417
- tr: bridgeResult.tr.setNodeMarkup(position, void 0, nextAttrs),
497
+ tr: rebaseParagraphRuns({
498
+ tr: bridgeResult.tr.setNodeMarkup(position, void 0, nextAttrs),
499
+ position,
500
+ previous: node,
501
+ styleResolver,
502
+ storedMarks
503
+ }),
418
504
  changed: true,
419
505
  revisionId: change?.info.id ?? null,
420
506
  revisionIds: change ? [change.info.id, ...bridgeResult.revisionIds] : []
421
507
  };
422
508
  };
509
+ /** No inherited formatting: every run-formatting mark reads as authored. */
510
+ const OPERATION_TEXT_CONTEXT = {
511
+ baseParagraphFormatting: void 0,
512
+ paragraphFormatting: void 0,
513
+ paragraphMarkFormatting: void 0,
514
+ paragraphMarkPrecedesStyle: false
515
+ };
423
516
  const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) => {
424
517
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return {
425
518
  tr,
@@ -433,6 +526,7 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
433
526
  revisionId: null,
434
527
  revisionIds: []
435
528
  };
529
+ const storedMarks = tr.storedMarks ?? tr.selection.$from.marks();
436
530
  const attrs = expectParagraphAttrs(block);
437
531
  const resolvedFormattingFromStyle = resolveFormattingFromStyle({
438
532
  attrs,
@@ -449,6 +543,11 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
449
543
  revisionId: null,
450
544
  revisionIds: []
451
545
  };
546
+ if ((attrs.styleId ?? null) !== item.operation.styleId) patch["defaultTextFormatting"] = resolveParagraphBodyRunFormatting({
547
+ styleId: item.operation.styleId ?? void 0,
548
+ tableRunFormatting: attrs._tableRunFormatting,
549
+ styleResolver
550
+ }).defaultFormatting ?? null;
452
551
  const existing = attrs._propertyChanges;
453
552
  const changeInfo = revisionInfo?.();
454
553
  const change = changeInfo ? {
@@ -473,7 +572,13 @@ const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) =>
473
572
  revisionIds: []
474
573
  };
475
574
  return {
476
- tr: bridgeResult.tr.setNodeMarkup(blockPosition, void 0, nextAttrs),
575
+ tr: rebaseParagraphRuns({
576
+ tr: bridgeResult.tr.setNodeMarkup(blockPosition, void 0, nextAttrs),
577
+ position: blockPosition,
578
+ previous: block,
579
+ styleResolver,
580
+ storedMarks
581
+ }),
477
582
  revisionId: change?.info.id ?? null,
478
583
  revisionIds: change ? [change.info.id, ...bridgeResult.revisionIds] : []
479
584
  };
@@ -747,10 +852,14 @@ let revisionIdCursor = Date.now() * 1e3;
747
852
  */
748
853
  const collectLiveBlocksByHash = (doc) => {
749
854
  const byHash = /* @__PURE__ */ new Map();
855
+ const noteReferences = collectNoteReferenceLabels(doc);
750
856
  doc.descendants((node, pos) => {
751
857
  if (isHiddenTableRow(node)) return false;
752
858
  if (!node.isTextblock) return true;
753
- const cleanText = buildCleanBlockText(node, pos).text;
859
+ const cleanText = buildCleanBlockText(node, pos, {
860
+ fieldResults: "text",
861
+ noteReferences
862
+ }).text;
754
863
  const hash = hashFolioAIBlockText(normalizeFolioAIBlockText(cleanText));
755
864
  const bucket = byHash.get(hash) ?? [];
756
865
  bucket.push({
@@ -804,6 +913,211 @@ const getTableMutationPlanTarget = (item) => {
804
913
  return { type: "none" };
805
914
  };
806
915
  /**
916
+ * `insertion`, resolved against the document as it was read, placed through
917
+ * what `tr` has already changed. The batch runs from the end backwards, so
918
+ * nothing it has applied yet sits before the new row; mapped all the same, so
919
+ * that stays true of the positions the row is written at and not only of the
920
+ * order they are visited in. A row another operation of the batch inserted
921
+ * at the same boundary is the later one in input order, so the new row goes
922
+ * before it.
923
+ */
924
+ const mapTableRowInsertion = (tr, insertion) => {
925
+ const tableStart = tr.mapping.map(insertion.tableStart, -1);
926
+ return {
927
+ ...insertion,
928
+ tableStart,
929
+ rowPosition: tr.mapping.map(insertion.rowPosition, -1),
930
+ rowspanUpdates: insertion.rowspanUpdates.map((update) => tr.mapping.map(insertion.tableStart + update, 1) - tableStart)
931
+ };
932
+ };
933
+ /** The cells a column deletion removes: every cell standing in that column alone. */
934
+ const deletedColumnCellRanges = (doc, { tablePosition, columnIndex }) => {
935
+ const table = doc.nodeAt(tablePosition);
936
+ if (!table || table.type.spec["tableRole"] !== "table") return [];
937
+ const map = TableMap.get(table);
938
+ const tableStart = tablePosition + 1;
939
+ const ranges = [];
940
+ const seen = /* @__PURE__ */ new Set();
941
+ for (let row = 0; row < map.height; row++) {
942
+ const relative = map.map[row * map.width + columnIndex];
943
+ if (relative === void 0 || seen.has(relative)) continue;
944
+ seen.add(relative);
945
+ const rectangle = map.findCell(relative);
946
+ const cell = table.nodeAt(relative);
947
+ if (!cell || rectangle.left !== columnIndex || rectangle.right !== columnIndex + 1) continue;
948
+ ranges.push({
949
+ from: tableStart + relative,
950
+ to: tableStart + relative + cell.nodeSize
951
+ });
952
+ }
953
+ return ranges;
954
+ };
955
+ /** A row deletion also changes cells whose vertical merge began above it. */
956
+ const deletedRowAffectedRanges = ({ doc, deletion, row }) => {
957
+ const ranges = [{
958
+ from: deletion.rowPosition,
959
+ to: deletion.rowPosition + row.nodeSize
960
+ }];
961
+ const table = doc.nodeAt(deletion.tablePosition);
962
+ if (table?.type.spec["tableRole"] !== "table") return panic("A resolved row deletion lost its table", { position: deletion.tablePosition });
963
+ const map = TableMap.get(table);
964
+ const seen = /* @__PURE__ */ new Set();
965
+ for (let column = 0; column < map.width; column++) {
966
+ const relative = map.map[deletion.rowIndex * map.width + column];
967
+ if (relative === void 0 || seen.has(relative)) continue;
968
+ seen.add(relative);
969
+ const position = deletion.tableStart + relative;
970
+ if (position >= deletion.rowPosition) continue;
971
+ const cell = table.nodeAt(relative);
972
+ if (cell) ranges.push({
973
+ from: position,
974
+ to: position + cell.nodeSize
975
+ });
976
+ }
977
+ return ranges;
978
+ };
979
+ /**
980
+ * The blank cells a merge folds into the first cell of its rectangle. Only a
981
+ * blank cell can fold away tracked, so an edit that fills one decides whether
982
+ * the merge applies in one mode and not the other; see `batch-claims.ts`. A
983
+ * cell with content folds directly and is refused tracked whatever the batch
984
+ * does inside it.
985
+ */
986
+ const foldedBlankCellRanges = (doc, { tablePosition, rectangle }) => {
987
+ const table = doc.nodeAt(tablePosition);
988
+ if (table?.type.spec["tableRole"] !== "table") return panic("A resolved cell merge lost its table", { position: tablePosition });
989
+ const map = TableMap.get(table);
990
+ const tableStart = tablePosition + 1;
991
+ const kept = map.map[rectangle.top * map.width + rectangle.left];
992
+ return map.cellsInRect(rectangle).flatMap((relative) => {
993
+ const cell = relative === kept ? null : table.nodeAt(relative);
994
+ return cell && isEmptyTableCell(cell) ? [{
995
+ from: tableStart + relative,
996
+ to: tableStart + relative + cell.nodeSize
997
+ }] : [];
998
+ });
999
+ };
1000
+ /** What `item` claims of the document its batch resolved against; see `batch-claims.ts`. */
1001
+ const batchClaimOf = ({ item, doc, producesTrackedChanges }) => {
1002
+ const block = item.blockFrom;
1003
+ switch (item.operation.type) {
1004
+ case "replaceInBlock":
1005
+ case "replaceRange": return {
1006
+ type: "text",
1007
+ block,
1008
+ from: item.from,
1009
+ to: item.to
1010
+ };
1011
+ case "formatRange":
1012
+ case "commentOnRange":
1013
+ case "commentOnBlock": return {
1014
+ type: "annotation",
1015
+ block,
1016
+ from: item.from,
1017
+ to: item.to
1018
+ };
1019
+ case "splitBlock": return {
1020
+ type: "split",
1021
+ block,
1022
+ from: item.from,
1023
+ to: item.to
1024
+ };
1025
+ case "replaceBlock": return item.replaceBlockImpact === "style" ? {
1026
+ type: "paragraphProperties",
1027
+ block
1028
+ } : {
1029
+ type: "rewriteBlock",
1030
+ block
1031
+ };
1032
+ case "setBlockParagraphProperties": return {
1033
+ type: "paragraphProperties",
1034
+ block
1035
+ };
1036
+ case "mergeBlockWithNext": return {
1037
+ type: "merge",
1038
+ block,
1039
+ next: item.blockTo,
1040
+ joinsNow: !producesTrackedChanges
1041
+ };
1042
+ case "deleteBlock": {
1043
+ const at = doc.resolve(block);
1044
+ const endsItsContainer = paragraphEndsItsContainer(at, item.blockNode.type.name);
1045
+ const keepsParagraph = producesTrackedChanges ? endsItsContainer : endsItsContainer && at.nodeBefore?.type.name !== item.blockNode.type.name;
1046
+ return {
1047
+ type: "deleteBlock",
1048
+ block,
1049
+ end: item.blockTo,
1050
+ keepsParagraph
1051
+ };
1052
+ }
1053
+ case "insertAfterBlock":
1054
+ case "insertBeforeBlock":
1055
+ case "insertSignatureTable":
1056
+ case "insertTable": return {
1057
+ type: "insertion",
1058
+ at: item.from
1059
+ };
1060
+ case "deleteTable": {
1061
+ const deleted = item.deletedTable;
1062
+ return deleted ? {
1063
+ type: "tableRemoval",
1064
+ table: deleted.position,
1065
+ wholeTable: true,
1066
+ axis: "table",
1067
+ ranges: [{
1068
+ from: deleted.position,
1069
+ to: deleted.position + deleted.node.nodeSize
1070
+ }]
1071
+ } : { type: "unclaimed" };
1072
+ }
1073
+ case "deleteTableRow": {
1074
+ const deletion = item.tableRowDeletion;
1075
+ const row = deletion ? doc.nodeAt(deletion.rowPosition) : null;
1076
+ const table = deletion ? doc.nodeAt(deletion.tablePosition) : null;
1077
+ return deletion && row && table?.type.spec["tableRole"] === "table" ? {
1078
+ type: "tableRemoval",
1079
+ table: deletion.tablePosition,
1080
+ wholeTable: table.childCount === 1,
1081
+ axis: "row",
1082
+ ranges: table.childCount === 1 ? [{
1083
+ from: deletion.tablePosition,
1084
+ to: deletion.tablePosition + table.nodeSize
1085
+ }] : deletedRowAffectedRanges({
1086
+ doc,
1087
+ deletion,
1088
+ row
1089
+ })
1090
+ } : { type: "unclaimed" };
1091
+ }
1092
+ case "deleteTableColumn": {
1093
+ const deletion = item.tableColumnDeletion;
1094
+ return deletion ? {
1095
+ type: "tableRemoval",
1096
+ table: deletion.tablePosition,
1097
+ wholeTable: false,
1098
+ axis: "column",
1099
+ ranges: deletedColumnCellRanges(doc, deletion)
1100
+ } : { type: "unclaimed" };
1101
+ }
1102
+ case "insertTableRow":
1103
+ case "insertTableColumn":
1104
+ case "mergeTableCells":
1105
+ case "splitTableCell": {
1106
+ const target = getTableMutationPlanTarget(item);
1107
+ let axis = "cell";
1108
+ if (item.operation.type === "insertTableRow") axis = "row";
1109
+ if (item.operation.type === "insertTableColumn") axis = "column";
1110
+ if (target.type === "none") return { type: "unclaimed" };
1111
+ return {
1112
+ type: "tableStructure",
1113
+ table: target.tablePosition,
1114
+ axis,
1115
+ folded: target.type === "mergeCells" ? foldedBlankCellRanges(doc, target) : []
1116
+ };
1117
+ }
1118
+ }
1119
+ };
1120
+ /**
807
1121
  * The snapshot recorded an `hashOccurrenceCount` per anchor but
808
1122
  * not which ordinal within that bucket the block was — recompute
809
1123
  * on demand from the snapshot's anchor map. Stable iteration
@@ -956,6 +1270,94 @@ const isBatchableParagraphInsertion = (item, mode) => {
956
1270
  const isEmptyInsert = insertTexts.length === 1 && insertTexts[0]?.length === 0;
957
1271
  return !(mode === "tracked-changes" && item.operation.pageBreakBefore === true && isEmptyInsert);
958
1272
  };
1273
+ /** The comments running across both edges of the table at `tablePosition`: over all of it. */
1274
+ const commentsAcrossTable = (doc, tablePosition) => {
1275
+ const table = doc.nodeAt(tablePosition);
1276
+ if (!table) return [];
1277
+ const atEnd = commentsAcrossBlockBoundary(doc, tablePosition + table.nodeSize);
1278
+ return commentsAcrossBlockBoundary(doc, tablePosition).filter((comment) => comment.isInSet(atEnd));
1279
+ };
1280
+ /** `comments` over the inline content of the node at `position`, which an insertion just placed. */
1281
+ const addCommentsOver = (tr, position, comments) => {
1282
+ const node = tr.doc.nodeAt(position);
1283
+ if (!node || !comments) return tr;
1284
+ let next = tr;
1285
+ for (const comment of comments) next = next.addMark(position + 1, position + node.nodeSize - 1, comment);
1286
+ return next;
1287
+ };
1288
+ const insertBlocksInsideComments = ({ tr, at, blocks, carriedComments }) => {
1289
+ const spanning = carriedComments ?? commentsAcrossBlockBoundary(tr.doc, at);
1290
+ const fragment = Fragment.from(blocks);
1291
+ let next = tr.insert(at, fragment);
1292
+ for (const comment of spanning) next = next.addMark(at, at + fragment.size, comment);
1293
+ return next;
1294
+ };
1295
+ /** A paragraph mark that is a pending insertion (not a relocation's end). */
1296
+ const isInsertedPPrMark = (value) => typeof value === "object" && value !== null && "kind" in value && value.kind === "ins";
1297
+ const isSuggestedParagraphInsert = (paragraph) => typeof paragraph.attrs["_suggestedInsert"] === "object" && paragraph.attrs["_suggestedInsert"] !== null;
1298
+ /** A paragraph mark that is a pending deletion, or a relocation's source. */
1299
+ const isDeletedPPrMark = (value) => typeof value === "object" && value !== null && "kind" in value && (value.kind === "del" || value.kind === "moveFrom");
1300
+ const hasRevisionMarker = (value) => typeof value === "object" && value !== null && "revisionId" in value;
1301
+ const isDeletedCell = (cell) => {
1302
+ const marker = cell.attrs["cellMarker"];
1303
+ return typeof marker === "object" && marker !== null && "kind" in marker && marker.kind === "del";
1304
+ };
1305
+ /**
1306
+ * Whether accepting every change removes the paragraph at `blockFrom` whole:
1307
+ * its paragraph mark and all of its text pending deletion (`cleanText`, the
1308
+ * accepted reading, is empty), or the table row or cell holding it pending
1309
+ * deletion. A reader lists such a paragraph as a blank block.
1310
+ *
1311
+ * Word keeps a paragraph whose mark is deleted joined to the next one: text
1312
+ * typed into it stays inserted there, and accepting runs it into the next
1313
+ * paragraph (or, in a deleted row, takes it away with the row). An operation
1314
+ * that asks for the paragraph to read something cannot get that without
1315
+ * rejecting the deletion, which is the reviewer's call, not a side effect.
1316
+ */
1317
+ const isPendingDeletion = (doc, blockFrom, paragraph, cleanText) => {
1318
+ if (cleanText.length === 0 && isDeletedPPrMark(paragraph.attrs["pPrMark"])) return true;
1319
+ const $block = doc.resolve(blockFrom);
1320
+ for (let depth = $block.depth; depth > 0; depth -= 1) {
1321
+ const ancestor = $block.node(depth);
1322
+ const name = ancestor.type.name;
1323
+ if (name === "tableRow" && hasRevisionMarker(ancestor.attrs["trDel"])) return true;
1324
+ if ((name === "tableCell" || name === "tableHeader") && isDeletedCell(ancestor)) return true;
1325
+ }
1326
+ return false;
1327
+ };
1328
+ /** Whether every piece of the paragraph's content is pending inserted text. */
1329
+ const holdsOnlyInsertedContent = (paragraph) => {
1330
+ let whollyInserted = true;
1331
+ paragraph.descendants((node) => {
1332
+ if (!whollyInserted) return false;
1333
+ if (!node.isInline || isZeroWidthAnchor(node)) return true;
1334
+ whollyInserted = node.marks.some((mark) => mark.type.name === "insertion" && mark.attrs["moveKind"] !== "moveTo");
1335
+ return false;
1336
+ });
1337
+ return whollyInserted;
1338
+ };
1339
+ /**
1340
+ * How to take back the paragraph at `position` when it is wholly a pending
1341
+ * insertion (the break that introduced it inserted, and everything in it
1342
+ * inserted text), or `null` when it is not. Such a paragraph goes outright:
1343
+ * accepting and rejecting both leave it out.
1344
+ *
1345
+ * The inserted break is the paragraph's own (an `ins` mark, or a suggested
1346
+ * whole-paragraph insert), except on the paragraph that ends its container:
1347
+ * that one keeps the container's original mark and the inserted break was
1348
+ * rotated onto the paragraph before it, whose `ins` mark then goes too.
1349
+ */
1350
+ const pendingParagraphRetraction = (doc, position) => {
1351
+ const paragraph = doc.nodeAt(position);
1352
+ if (paragraph?.type.name !== "paragraph" || !holdsOnlyInsertedContent(paragraph)) return null;
1353
+ const at = doc.resolve(position);
1354
+ const before = at.nodeBefore;
1355
+ const endsItsContainer = paragraphEndsItsContainer(at, paragraph.type.name);
1356
+ if (at.parent.childCount < 2 || endsItsContainer && before?.type.name !== paragraph.type.name) return null;
1357
+ if (isInsertedPPrMark(paragraph.attrs["pPrMark"]) || isSuggestedParagraphInsert(paragraph)) return { clearBreakAt: null };
1358
+ if (endsItsContainer && before && paragraph.attrs["pPrMark"] == null) return isInsertedPPrMark(before.attrs["pPrMark"]) ? { clearBreakAt: position - before.nodeSize } : null;
1359
+ return null;
1360
+ };
959
1361
  const addedBreakRevisionId = (value) => {
960
1362
  if (typeof value !== "object" || value === null || !("kind" in value) || !("info" in value)) return null;
961
1363
  if (value.kind !== "ins" && value.kind !== "moveTo") return null;
@@ -963,6 +1365,277 @@ const addedBreakRevisionId = (value) => {
963
1365
  if (typeof info !== "object" || info === null || !("id" in info)) return null;
964
1366
  return typeof info.id === "number" ? info.id : null;
965
1367
  };
1368
+ /** The revision id of a paragraph mark that says its break was added or deleted. */
1369
+ const addedOrDeletedBreakRevisionId = (value) => {
1370
+ if (typeof value !== "object" || value === null || !("kind" in value) || !("info" in value)) return null;
1371
+ if (value.kind !== "del") return addedBreakRevisionId(value);
1372
+ const info = value.info;
1373
+ if (typeof info !== "object" || info === null || !("id" in info)) return null;
1374
+ return typeof info.id === "number" ? info.id : null;
1375
+ };
1376
+ /** Whether every piece of the paragraph's content is pending deleted text (or it has none). */
1377
+ const holdsOnlyDeletedContent = (paragraph) => {
1378
+ let whollyDeleted = true;
1379
+ paragraph.descendants((node) => {
1380
+ if (!whollyDeleted) return false;
1381
+ if (!node.isInline || isZeroWidthAnchor(node)) return true;
1382
+ whollyDeleted = node.marks.some((mark) => mark.type.name === "deletion");
1383
+ return false;
1384
+ });
1385
+ return whollyDeleted;
1386
+ };
1387
+ /**
1388
+ * Whether everything after `position` in its container is paragraphs pending
1389
+ * deletion: a join there, once accepted, has no words after it.
1390
+ */
1391
+ const followedOnlyByDeletedParagraphs = (doc, position) => {
1392
+ const at = doc.resolve(position);
1393
+ let deletes = false;
1394
+ for (let index = at.index(); index < at.parent.childCount; index++) {
1395
+ const node = at.parent.child(index);
1396
+ if (node.type.name !== "paragraph" || !holdsOnlyDeletedContent(node)) return false;
1397
+ deletes ||= node.content.size > 0 || isPlainDeletedPPrMark(node.attrs["pPrMark"]);
1398
+ }
1399
+ return deletes;
1400
+ };
1401
+ /** A paragraph mark that is a plain pending deletion (not a relocation's source). */
1402
+ const isPlainDeletedPPrMark = (value) => typeof value === "object" && value !== null && "kind" in value && value.kind === "del";
1403
+ /** An emptied paragraph still carries deleted content, rather than being an original blank. */
1404
+ const hasDeletedContent = (paragraph) => {
1405
+ let found = false;
1406
+ paragraph.descendants((node) => {
1407
+ if (node.isInline && node.marks.some((mark) => mark.type.name === "deletion")) {
1408
+ found = true;
1409
+ return false;
1410
+ }
1411
+ return !found;
1412
+ });
1413
+ return found;
1414
+ };
1415
+ /** The final paragraph reached by a chain of wholly deleted paragraphs. */
1416
+ const deletedFinalParagraphAfter = (at, paragraphTypeName) => {
1417
+ let position = at.pos;
1418
+ for (let index = at.index(); index < at.parent.childCount; index++) {
1419
+ const paragraph = at.parent.child(index);
1420
+ if (paragraph.type.name !== paragraphTypeName || !holdsOnlyDeletedContent(paragraph)) return null;
1421
+ if (index === at.parent.childCount - 1) return paragraph.attrs["pPrMark"] == null && hasDeletedContent(paragraph) ? position : null;
1422
+ if (!isPlainDeletedPPrMark(paragraph.attrs["pPrMark"])) return null;
1423
+ position += paragraph.nodeSize;
1424
+ }
1425
+ return null;
1426
+ };
1427
+ /**
1428
+ * Where the added break of the final paragraph at `at` rotates to: the free
1429
+ * mark `addedBreakCarrierBefore` finds, or else a paragraph the batch deletes
1430
+ * whole, whose deleted break the added one cancels (`cancelsDeletedBreak`).
1431
+ * A table whose rows all share one insertion revision from this batch is not
1432
+ * a barrier: rejecting that table removes it before the paragraphs join.
1433
+ * Existing and partially inserted tables remain barriers.
1434
+ * The cancelling paragraph must be followed directly by the paragraph its
1435
+ * words run into.
1436
+ */
1437
+ const finalBreakCarrierBefore = (at, paragraphTypeName, batchRevisionIds) => {
1438
+ const free = addedBreakCarrierBefore({
1439
+ at,
1440
+ paragraphTypeName,
1441
+ canCrossTable: (table) => {
1442
+ if (table.childCount === 0) return false;
1443
+ let revisionId;
1444
+ for (let index = 0; index < table.childCount; index++) {
1445
+ const row = table.child(index);
1446
+ if (row.type.spec["tableRole"] !== "row") return false;
1447
+ const { trIns, trDel } = expectTableRowAttrs(row);
1448
+ if (!trIns || trDel || !batchRevisionIds.has(trIns.revisionId)) return false;
1449
+ revisionId ??= trIns.revisionId;
1450
+ if (trIns.revisionId !== revisionId) return false;
1451
+ }
1452
+ return true;
1453
+ }
1454
+ });
1455
+ if (free) return {
1456
+ ...free,
1457
+ cancelsDeletedBreak: false
1458
+ };
1459
+ let position = at.pos;
1460
+ for (let index = at.index() - 1; index >= 0; index--) {
1461
+ const sibling = at.parent.child(index);
1462
+ position -= sibling.nodeSize;
1463
+ if (sibling.type.name !== paragraphTypeName) {
1464
+ if (sibling.type.spec["tableRole"] === "table") return null;
1465
+ continue;
1466
+ }
1467
+ const mark = sibling.attrs["pPrMark"];
1468
+ const revisionId = addedBreakRevisionId(mark);
1469
+ if (revisionId !== null && batchRevisionIds.has(revisionId)) continue;
1470
+ const deletedBreakId = isPlainDeletedPPrMark(mark) ? addedOrDeletedBreakRevisionId(mark) : null;
1471
+ return deletedBreakId !== null && batchRevisionIds.has(deletedBreakId) && holdsOnlyDeletedContent(sibling) && at.parent.maybeChild(index + 1)?.type.name === paragraphTypeName ? {
1472
+ position,
1473
+ node: sibling,
1474
+ cancelsDeletedBreak: true
1475
+ } : null;
1476
+ }
1477
+ return null;
1478
+ };
1479
+ /** The preceding paragraph whose break a final deletion can retire. */
1480
+ const finalParagraphPredecessor = (doc, at) => {
1481
+ const emptied = doc.nodeAt(at);
1482
+ if (emptied?.type.name !== "paragraph") return null;
1483
+ const $emptied = doc.resolve(at);
1484
+ if (!paragraphEndsItsContainer($emptied, emptied.type.name)) return null;
1485
+ let position = at;
1486
+ for (let index = $emptied.index() - 1; index >= 0; index--) {
1487
+ const node = $emptied.parent.child(index);
1488
+ position -= node.nodeSize;
1489
+ if (node.type !== emptied.type) return null;
1490
+ if (isPlainDeletedPPrMark(node.attrs["pPrMark"]) && holdsOnlyDeletedContent(node)) continue;
1491
+ return {
1492
+ node,
1493
+ position
1494
+ };
1495
+ }
1496
+ return null;
1497
+ };
1498
+ /** Pending retirements, read after the merge has mapped any inserted separator. */
1499
+ const pendingRemovedFinalBreakPositions = (tr, deletions) => {
1500
+ const positions = /* @__PURE__ */ new Set();
1501
+ for (const deletion of deletions) {
1502
+ const at = tr.mapping.slice(deletion.mappedFrom).map(deletion.position);
1503
+ const predecessor = finalParagraphPredecessor(tr.doc, at);
1504
+ if (!predecessor || predecessor.node.attrs["pPrMark"] != null) continue;
1505
+ positions.add(predecessor.position);
1506
+ }
1507
+ return positions;
1508
+ };
1509
+ /**
1510
+ * Finish the tracked deletion of every paragraph a container ends with.
1511
+ *
1512
+ * Its own mark cannot be deleted (see `paragraphEndsItsContainer`), so the
1513
+ * deletion emptied it and left the mark. Applied directly, the paragraph goes
1514
+ * whenever a paragraph precedes it, which becomes the container's last; only
1515
+ * a container that would be left empty or ending in a table keeps an empty
1516
+ * paragraph. Tracked resolves to the same: the preceding paragraph's mark is
1517
+ * deleted, and accepting runs that paragraph into the emptied one, keeping
1518
+ * its own properties since it holds the words.
1519
+ *
1520
+ * A paragraph with a break pending deletion already runs into this one. If
1521
+ * its content is also wholly deleted, look past it for the surviving carrier;
1522
+ * otherwise another final-break revision would depend on batch boundaries.
1523
+ * A preceding break that is itself a pending insertion is
1524
+ * retracted instead of deleted: the paragraph joins the emptied one now, and
1525
+ * records the emptied one's properties as `w:pPrChange` where they differ,
1526
+ * which is what rejecting reads to put them back.
1527
+ *
1528
+ * This runs once over the finished batch: which paragraph precedes the
1529
+ * emptied one is settled only then, since the batch runs backwards and an
1530
+ * insertion or deletion before it lands after it.
1531
+ */
1532
+ const withRetiredFinalParagraphs = ({ tr, deletions, mergedPropertyTargets, appliedIds, batchRevisionIds, revisionSeed, author, date, styleResolver, numbering }) => {
1533
+ let nextRevisionId = revisionSeed;
1534
+ const addedRevisions = [];
1535
+ const retractedRevisionIds = [];
1536
+ const resolvedOperationIds = /* @__PURE__ */ new Set();
1537
+ const located = deletions.filter(({ operationId }) => appliedIds.has(operationId)).map((deletion) => ({
1538
+ deletion,
1539
+ at: tr.mapping.slice(deletion.mappedFrom).map(deletion.position)
1540
+ })).toSorted((left, right) => right.at - left.at);
1541
+ for (const { deletion: { operationId, revisionExtras }, at } of located) {
1542
+ const emptied = tr.doc.nodeAt(at);
1543
+ if (emptied?.type.name !== "paragraph") continue;
1544
+ const predecessor = finalParagraphPredecessor(tr.doc, at);
1545
+ if (!predecessor) continue;
1546
+ const { node: previous, position } = predecessor;
1547
+ const mark = previous.attrs["pPrMark"];
1548
+ if (mark == null) {
1549
+ const revisionId = nextRevisionId++;
1550
+ tr.setNodeAttribute(position, "pPrMark", {
1551
+ kind: "del",
1552
+ info: {
1553
+ id: revisionId,
1554
+ author,
1555
+ date,
1556
+ ...revisionExtras
1557
+ }
1558
+ });
1559
+ const beforeBatch = resolveParagraphChangeAttrs({
1560
+ node: previous,
1561
+ mode: "reject",
1562
+ boundaryCovered: true,
1563
+ revisionSet: batchRevisionIds,
1564
+ styleResolver,
1565
+ numbering
1566
+ });
1567
+ if (!mergedPropertyTargets.some(({ position: target, mappedFrom }) => {
1568
+ const mapped = tr.mapping.slice(mappedFrom).mapResult(target);
1569
+ return !mapped.deleted && mapped.pos === at;
1570
+ })) carryParagraphProperties({
1571
+ tr,
1572
+ position: at,
1573
+ source: beforeBatch ? previous.type.create(beforeBatch, previous.content, previous.marks) : previous,
1574
+ styleResolver,
1575
+ numbering,
1576
+ revision: {
1577
+ id: revisionId,
1578
+ author,
1579
+ date,
1580
+ ...revisionExtras
1581
+ },
1582
+ keep: propertiesSetInBatch(emptied, batchRevisionIds)
1583
+ });
1584
+ resolvedOperationIds.add(operationId);
1585
+ addedRevisions.push({
1586
+ operationId,
1587
+ revisionId
1588
+ });
1589
+ continue;
1590
+ }
1591
+ if (isInsertedPPrMark(mark)) {
1592
+ const following = tr.doc.nodeAt(position + previous.nodeSize);
1593
+ if (following?.type.name !== emptied.type.name) continue;
1594
+ const previousFormatting = paragraphPropertiesBeforePendingChanges(following);
1595
+ const formattingChanges = JSON.stringify(previousFormatting) !== JSON.stringify(paragraphPropertiesSnapshot(previous));
1596
+ const existing = expectParagraphAttrs(previous)._propertyChanges;
1597
+ if (formattingChanges && hasSerializableParagraphPropertyChange(existing)) continue;
1598
+ joinAtParagraphMark({
1599
+ tr,
1600
+ paragraphPos: position,
1601
+ paragraph: previous,
1602
+ next: following,
1603
+ styleResolver
1604
+ });
1605
+ resolvedOperationIds.add(operationId);
1606
+ const retracted = addedBreakRevisionId(mark);
1607
+ if (retracted !== null) retractedRevisionIds.push(retracted);
1608
+ const revisionId = nextRevisionId;
1609
+ if (carryParagraphProperties({
1610
+ tr,
1611
+ position,
1612
+ source: previous,
1613
+ styleResolver,
1614
+ numbering,
1615
+ revision: {
1616
+ id: revisionId,
1617
+ author,
1618
+ date,
1619
+ ...revisionExtras
1620
+ },
1621
+ keep: propertiesSetInBatch(following, batchRevisionIds)
1622
+ }).tracked) {
1623
+ nextRevisionId++;
1624
+ addedRevisions.push({
1625
+ operationId,
1626
+ revisionId
1627
+ });
1628
+ }
1629
+ continue;
1630
+ }
1631
+ }
1632
+ return {
1633
+ nextRevisionId,
1634
+ resolvedOperationIds,
1635
+ addedRevisions,
1636
+ retractedRevisionIds
1637
+ };
1638
+ };
966
1639
  /**
967
1640
  * Move an ADDED paragraph break off every paragraph its container ends with.
968
1641
  *
@@ -985,8 +1658,52 @@ const addedBreakRevisionId = (value) => {
985
1658
  * which paragraph ends a container is only settled when the batch is, and an
986
1659
  * insertion that looked final was undone by the next operation writing a table
987
1660
  * after it.
1661
+ * A wholly inserted table from this batch can be crossed to reach the carrier;
1662
+ * its removal on reject exposes that same paragraph boundary again.
1663
+ *
1664
+ * The paragraph the run was appended after may be one the same batch deletes:
1665
+ * the insertion landed first, so the deletion took that paragraph's mark as
1666
+ * well. The break rotating onto it then cancels the deleted one, as deleting a
1667
+ * paragraph whose break is inserted does one operation at a time: the deleted
1668
+ * paragraph's words run into the one after it, which ends with the free mark,
1669
+ * and neither break is left to resolve.
988
1670
  */
989
- const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, author, date, initials }) => {
1671
+ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, author, date, initials, styleResolver }) => {
1672
+ const supersededDeletedBreaks = [];
1673
+ const collectSupersededDeletedBreaks = (parent, contentStart) => {
1674
+ let previous = null;
1675
+ parent.forEach((node, offset) => {
1676
+ const position = contentStart + offset;
1677
+ if (node.type.name !== "paragraph") {
1678
+ previous = null;
1679
+ if (!node.isTextblock && node.childCount > 0) collectSupersededDeletedBreaks(node, position + 1);
1680
+ return;
1681
+ }
1682
+ if (previous) {
1683
+ const deletedMark = previous.node.attrs["pPrMark"];
1684
+ const insertedMark = node.attrs["pPrMark"];
1685
+ const insertedRevisionId = addedBreakRevisionId(insertedMark);
1686
+ const trackedInsertion = insertedRevisionId !== null && batchRevisionIds.has(insertedRevisionId);
1687
+ const suggestedInsertion = isSuggestedParagraphInsert(node);
1688
+ const deletedBreakRevisionId = addedOrDeletedBreakRevisionId(deletedMark);
1689
+ if (isPlainDeletedPPrMark(deletedMark) && deletedBreakRevisionId !== null && !batchRevisionIds.has(deletedBreakRevisionId) && !holdsOnlyDeletedContent(previous.node) && (trackedInsertion || suggestedInsertion) && holdsOnlyInsertedContent(node)) supersededDeletedBreaks.push({
1690
+ preceding: previous.position,
1691
+ inserted: position,
1692
+ deletedMark,
1693
+ precedingMark: trackedInsertion ? insertedMark : null
1694
+ });
1695
+ }
1696
+ previous = {
1697
+ node,
1698
+ position
1699
+ };
1700
+ });
1701
+ };
1702
+ collectSupersededDeletedBreaks(tr.doc, 0);
1703
+ for (const { preceding, inserted, deletedMark, precedingMark } of supersededDeletedBreaks) {
1704
+ tr.setNodeAttribute(preceding, "pPrMark", precedingMark);
1705
+ tr.setNodeAttribute(inserted, "pPrMark", deletedMark);
1706
+ }
990
1707
  const paragraphTypeName = tr.doc.type.schema.nodes["paragraph"]?.name ?? "paragraph";
991
1708
  const rotations = finalParagraphsOf(tr.doc, paragraphTypeName).flatMap(({ node, position }) => {
992
1709
  const mark = node.attrs["pPrMark"];
@@ -997,13 +1714,14 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
997
1714
  ownerRevisionId: revisionId,
998
1715
  position
999
1716
  }] : [];
1000
- });
1717
+ }).toSorted((left, right) => right.position - left.position);
1001
1718
  let next = tr;
1002
1719
  let nextRevisionId = revisionSeed;
1003
1720
  const synthesizedRevisions = [];
1721
+ const retractedRevisionIds = [];
1004
1722
  for (const { position: finalPosition, node: final, mark: finalMark } of rotations) {
1005
1723
  const resolved = next.doc.resolve(finalPosition);
1006
- const carrier = addedBreakCarrierBefore(resolved, final.type.name);
1724
+ const carrier = finalBreakCarrierBefore(resolved, final.type.name, batchRevisionIds);
1007
1725
  if (!carrier) {
1008
1726
  next = next.setNodeAttribute(finalPosition, "pPrMark", null);
1009
1727
  continue;
@@ -1031,7 +1749,7 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1031
1749
  const revisionId = addedBreakRevisionId(node.attrs["pPrMark"]);
1032
1750
  return revisionId === null || !batchRevisionIds.has(revisionId);
1033
1751
  })) continue;
1034
- const previousFormatting = paragraphPropertiesBeforeBatch(carrier.node, batchRevisionIds);
1752
+ const previousFormatting = paragraphPropertiesBeforePendingChanges(carrier.node);
1035
1753
  next = next.setNodeAttribute(finalPosition, "pPrMark", null);
1036
1754
  let carriedMark = finalMark;
1037
1755
  for (let index = 1; index < path.length; index++) {
@@ -1041,7 +1759,8 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1041
1759
  const ownerRevisionId = addedBreakRevisionId(carriedMark);
1042
1760
  if (ownerRevisionId === null || !batchRevisionIds.has(ownerRevisionId)) panic("A final-mark rotation crossed a break outside its operation batch", { ownerRevisionId });
1043
1761
  const displacedMark = previous.node.attrs["pPrMark"];
1044
- next = next.setNodeAttribute(previous.position, "pPrMark", carriedMark);
1762
+ const cancels = carrier.cancelsDeletedBreak && previous.position === carrier.position;
1763
+ if (!cancels) next = next.setNodeAttribute(previous.position, "pPrMark", carriedMark);
1045
1764
  const currentFormatting = paragraphPropertiesSnapshot(current.node);
1046
1765
  if (JSON.stringify(previousFormatting) !== JSON.stringify(currentFormatting)) {
1047
1766
  const liveCurrent = next.doc.nodeAt(current.position);
@@ -1067,6 +1786,22 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1067
1786
  revisionId
1068
1787
  });
1069
1788
  }
1789
+ if (cancels) {
1790
+ const deleted = next.doc.nodeAt(previous.position);
1791
+ const following = next.doc.nodeAt(current.position);
1792
+ if (!deleted || !following) panic("A cancelled final-mark rotation lost its paragraphs", { position: previous.position });
1793
+ joinAtParagraphMark({
1794
+ tr: next,
1795
+ paragraphPos: previous.position,
1796
+ paragraph: deleted,
1797
+ next: following,
1798
+ styleResolver
1799
+ });
1800
+ retractedRevisionIds.push(ownerRevisionId);
1801
+ const deletedBreakId = addedOrDeletedBreakRevisionId(displacedMark);
1802
+ if (deletedBreakId !== null) retractedRevisionIds.push(deletedBreakId);
1803
+ break;
1804
+ }
1070
1805
  if (displacedMark == null) break;
1071
1806
  carriedMark = displacedMark;
1072
1807
  }
@@ -1074,10 +1809,26 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1074
1809
  return {
1075
1810
  transaction: next,
1076
1811
  nextRevisionId,
1077
- synthesizedRevisions
1812
+ synthesizedRevisions,
1813
+ retractedRevisionIds
1078
1814
  };
1079
1815
  };
1080
- const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove, numbering }) => {
1816
+ const restyledListAttrs = ({ attrs, styleNumbering, removesNumbering, numbering }) => {
1817
+ const stated = readParagraphNumberingAttr(attrs["numPr"]);
1818
+ const statedByOldStyle = isStyleSourcedParagraphNumbering(stated, readParagraphNumberingAttr(attrs["numPrFromStyle"]));
1819
+ const fromStyle = styleNumbering === void 0 ? null : paragraphNumberingAttr(styleNumbering);
1820
+ const numPr = removesNumbering ? removedNumberingAttr(fromStyle) : stated !== null && !statedByOldStyle ? stated : fromStyle;
1821
+ return {
1822
+ ...CLEARED_LIST_RENDERING_ATTRS,
1823
+ ...numPr?.kind === "reference" && listAttrsFromNumbering({
1824
+ numId: numPr.numId,
1825
+ ilvl: numPr.ilvl ?? 0
1826
+ }, numbering),
1827
+ numPr,
1828
+ numPrFromStyle: fromStyle
1829
+ };
1830
+ };
1831
+ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, author, date, initials, commentMark, suggestionId, revisionSeed, isPairedMove, numbering, styleResolver }) => {
1081
1832
  const operation = item.operation;
1082
1833
  if (operation.type !== "insertAfterBlock" && operation.type !== "insertBeforeBlock") panic("Only paragraph insertions can build inserted paragraphs", { type: operation.type });
1083
1834
  const insertionType = schema.marks["insertion"];
@@ -1131,11 +1882,11 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1131
1882
  }) : buildEmphasisInlineContent(schema, text, marks);
1132
1883
  const attrs = formatsParagraph ? { ...baseAttrs } : {};
1133
1884
  if (isFirstParagraph && operation.pageBreakBefore === true) attrs["pageBreakBefore"] = true;
1134
- const explicitNumbering = operation.numbering;
1885
+ const explicitNumbering = operation.numbering === void 0 || operation.numbering === null ? operation.numbering : concreteListReference(operation.numbering);
1135
1886
  const listLevel = operation.listLevel;
1136
1887
  if (formatsParagraph) {
1137
1888
  if (explicitNumbering === null) {
1138
- attrs["numPr"] = null;
1889
+ attrs["numPr"] = removedNumberingAttr(readParagraphNumberingAttr(attrs["numPrFromStyle"]));
1139
1890
  Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
1140
1891
  } else if (explicitNumbering !== void 0) {
1141
1892
  Object.assign(attrs, listLevelAttrPatch(operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode), {
@@ -1147,7 +1898,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1147
1898
  attrs["numPrFromStyle"] = null;
1148
1899
  }
1149
1900
  } else if (listLevel === null) {
1150
- attrs["numPr"] = null;
1901
+ attrs["numPr"] = removedNumberingAttr(readParagraphNumberingAttr(attrs["numPrFromStyle"]));
1151
1902
  Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
1152
1903
  } else if (typeof listLevel === "number") {
1153
1904
  const numId = paragraphNumberingReferenceId(readParagraphNumberingAttr(Reflect.get(baseAttrs, "numPr")) ?? void 0);
@@ -1165,9 +1916,24 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1165
1916
  }
1166
1917
  if (formatsParagraph && operation.styleId !== void 0) {
1167
1918
  attrs["styleId"] = operation.styleId;
1168
- if (operation.inheritFormatting !== false && operation.styleId !== null) Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
1919
+ attrs["_resolvedFormatting"] = styleResolvedParagraphFormatting(formattingFromStyle);
1920
+ attrs["defaultTextFormatting"] = resolveParagraphBodyRunFormatting({
1921
+ styleId: operation.styleId ?? void 0,
1922
+ tableRunFormatting: expectParagraphAttrs(item.blockNode)._tableRunFormatting,
1923
+ styleResolver
1924
+ }).defaultFormatting ?? null;
1925
+ if (operation.inheritFormatting !== false && operation.styleId !== null) {
1926
+ Object.assign(attrs, restyledListAttrs({
1927
+ attrs,
1928
+ styleNumbering: formattingFromStyle?.numPr,
1929
+ removesNumbering: explicitNumbering === null || explicitNumbering === void 0 && listLevel === null,
1930
+ numbering
1931
+ }));
1932
+ attrs["outlineLevel"] = attrs["_originalFormatting"]?.outlineLevel ?? formattingFromStyle?.outlineLevel ?? null;
1933
+ }
1169
1934
  }
1170
- if (formatsParagraph && (operation.styleId !== void 0 || operation.alignment !== void 0 || operation.spacing !== void 0 || operation.indentation !== void 0)) {
1935
+ if (formatsParagraph && operation.outlineLevel !== void 0) attrs["outlineLevel"] = operation.outlineLevel ?? formattingFromStyle?.outlineLevel ?? null;
1936
+ if (formatsParagraph && (operation.styleId !== void 0 || operation.outlineLevel !== void 0 || operation.alignment !== void 0 || operation.spacing !== void 0 || operation.indentation !== void 0)) {
1171
1937
  const inheritedDirectAlignment = operation.inheritFormatting === false ? void 0 : directParagraphAlignment(expectParagraphAttrs(item.blockNode));
1172
1938
  const inheritedDirectSpacing = operation.inheritFormatting === false ? void 0 : directParagraphSpacing(expectParagraphAttrs(item.blockNode));
1173
1939
  const inheritedDirectIndentation = operation.inheritFormatting === false ? void 0 : directParagraphIndentation(expectParagraphAttrs(item.blockNode));
@@ -1183,6 +1949,11 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1183
1949
  if (operation.styleId === null) Reflect.deleteProperty(originalFormatting, "styleId");
1184
1950
  else originalFormatting.styleId = operation.styleId;
1185
1951
  }
1952
+ if (operation.outlineLevel !== void 0) {
1953
+ originalFormatting ??= {};
1954
+ if (operation.outlineLevel === null) Reflect.deleteProperty(originalFormatting, "outlineLevel");
1955
+ else originalFormatting.outlineLevel = operation.outlineLevel;
1956
+ }
1186
1957
  if (operation.alignment !== void 0 || inheritedDirectAlignment !== void 0) {
1187
1958
  originalFormatting ??= {};
1188
1959
  if (directAlignment === void 0) Reflect.deleteProperty(originalFormatting, "alignment");
@@ -1200,6 +1971,17 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1200
1971
  }));
1201
1972
  attrs["_originalFormatting"] = originalFormatting && Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
1202
1973
  }
1974
+ if (formatsParagraph && operation.styleId !== void 0 && formattingFromStyle?.numPr?.kind === "reference" && operation.numbering === void 0 && operation.listLevel === void 0) {
1975
+ const { numId, ilvl = 0 } = formattingFromStyle.numPr;
1976
+ const levelIndentation = numbering?.getLevel(numId, ilvl)?.pPr;
1977
+ if (levelIndentation && operation.indentation === void 0) {
1978
+ const direct = attrs["_originalFormatting"];
1979
+ const directIndentLeft = typeof direct === "object" && direct !== null && "indentLeft" in direct ? direct.indentLeft : void 0;
1980
+ const directIndentFirstLine = typeof direct === "object" && direct !== null && "indentFirstLine" in direct ? direct.indentFirstLine : void 0;
1981
+ if (directIndentLeft === void 0 && formattingFromStyle.indentLeft === void 0) attrs["indentLeft"] = levelIndentation.indentLeft ?? attrs["indentLeft"];
1982
+ if (directIndentFirstLine === void 0 && formattingFromStyle.indentFirstLine === void 0) attrs["indentFirstLine"] = levelIndentation.indentFirstLine ?? attrs["indentFirstLine"];
1983
+ }
1984
+ }
1203
1985
  if (isSuggested && suggestionId !== null && paragraphRevisionId !== null) attrs["_suggestedInsert"] = {
1204
1986
  suggestionId,
1205
1987
  revisionId: paragraphRevisionId,
@@ -1207,7 +1989,28 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1207
1989
  date,
1208
1990
  ...initials ? { initials } : {}
1209
1991
  };
1210
- nodes.push(item.blockNode.type.create(attrs, content));
1992
+ const paragraph = item.blockNode.type.create(attrs, content);
1993
+ const sourceContext = OPERATION_TEXT_CONTEXT;
1994
+ const targetContext = paragraphRunStyleContext(paragraph, styleResolver);
1995
+ const reconciled = [];
1996
+ paragraph.forEach((child) => {
1997
+ if (!child.isInline) {
1998
+ reconciled.push(child);
1999
+ return;
2000
+ }
2001
+ const reconciledMarks = reconcileRunFormattingMarks({
2002
+ authoredFormatting: readAuthoredRunFormatting({
2003
+ context: sourceContext,
2004
+ marks: child.marks,
2005
+ styleResolver
2006
+ }),
2007
+ context: targetContext,
2008
+ node: child,
2009
+ styleResolver
2010
+ });
2011
+ reconciled.push(child.mark(reconciledMarks));
2012
+ });
2013
+ nodes.push(paragraph.copy(Fragment.fromArray(reconciled)));
1211
2014
  }
1212
2015
  if (producesTrackedChanges && !isSuggested) for (const [index, node] of nodes.entries()) {
1213
2016
  const revisionId = nextRevisionId++;
@@ -1231,7 +2034,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
1231
2034
  nextRevisionId
1232
2035
  };
1233
2036
  };
1234
- const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates, replacementBackground = "clear" }) => {
2037
+ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: requestedOperations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates, replacementBackground = "clear", undefinedStyles = "refuse" }) => {
1235
2038
  const applied = [];
1236
2039
  const skipped = [];
1237
2040
  const normalizations = [];
@@ -1263,7 +2066,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1263
2066
  return id;
1264
2067
  };
1265
2068
  const styleResolver = getDocumentStyleResolver(view.state);
1266
- const numbering = getDocumentNumbering(view.state);
2069
+ const styleDefinitions = undefinedStyles === "keep" ? null : getDocumentStyleDefinitions(view.state);
2070
+ const { operations, numbering } = resolveNewListOperations(requestedOperations, getDocumentNumbering(view.state));
1267
2071
  const formattingFromStyleForInsertion = (item) => {
1268
2072
  if (item.operation.type !== "insertAfterBlock" && item.operation.type !== "insertBeforeBlock") return;
1269
2073
  const anchorAttrs = expectParagraphAttrs(item.blockNode);
@@ -1277,6 +2081,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1277
2081
  };
1278
2082
  const claimedTableRows = /* @__PURE__ */ new Set();
1279
2083
  const claimedTableColumns = /* @__PURE__ */ new Set();
2084
+ const batchClaims = new BatchClaims();
1280
2085
  const diffText = wordDiffMode === "coarse" ? coarseWordDiff : wordDiffSessionFromOptions(wordDiff).diff;
1281
2086
  const producesTrackedChanges = mode !== "direct";
1282
2087
  const isSuggested = mode === "suggested";
@@ -1290,6 +2095,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1290
2095
  };
1291
2096
  const liveBlocks = collectLiveBlocksByHash(view.state.doc);
1292
2097
  const liveBlocksByParaId = collectLiveBlocksByParaId(view.state.doc);
2098
+ const noteReferences = collectNoteReferenceLabels(view.state.doc);
2099
+ const numberingInstanceIds = getDocumentNumberingInstanceIds(view.state) === null ? null : new Set(numbering?.definitions.nums.map(({ numId }) => numId) ?? []);
1293
2100
  for (const [index, operation] of operations.entries()) {
1294
2101
  const commentText = getOperationCommentText(operation);
1295
2102
  if (commentText !== void 0 && (!commentType || createCommentId === void 0)) {
@@ -1304,12 +2111,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1304
2111
  operation,
1305
2112
  liveBlocks,
1306
2113
  liveBlocksByParaId,
2114
+ noteReferences,
1307
2115
  doc: view.state.doc
1308
2116
  });
1309
2117
  if (resolution.type === "skip") {
1310
2118
  skipped.push({
1311
2119
  id: operation.id,
1312
- reason: resolution.reason
2120
+ reason: resolution.reason,
2121
+ ...resolution.message !== void 0 && { message: resolution.message }
1313
2122
  });
1314
2123
  continue;
1315
2124
  }
@@ -1320,6 +2129,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1320
2129
  });
1321
2130
  continue;
1322
2131
  }
2132
+ const undefinedStyle = findUndefinedParagraphStyleReference(operation, styleDefinitions);
2133
+ if (undefinedStyle !== void 0) {
2134
+ skipped.push({
2135
+ id: operation.id,
2136
+ reason: "missingStyle",
2137
+ message: describeUndefinedParagraphStyle(undefinedStyle, styleDefinitions)
2138
+ });
2139
+ continue;
2140
+ }
2141
+ const undefinedNumbering = findUndefinedNumberingReference(operation, numberingInstanceIds);
2142
+ if (undefinedNumbering !== void 0) {
2143
+ skipped.push({
2144
+ id: operation.id,
2145
+ reason: "missingNumbering",
2146
+ message: describeUndefinedNumbering(undefinedNumbering, numberingInstanceIds)
2147
+ });
2148
+ continue;
2149
+ }
1323
2150
  const deletion = resolution.operation.tableRowDeletion;
1324
2151
  if (deletion) {
1325
2152
  const rowKey = `${deletion.tablePosition}:${deletion.rowIndex}`;
@@ -1330,7 +2157,6 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1330
2157
  });
1331
2158
  continue;
1332
2159
  }
1333
- claimedTableRows.add(rowKey);
1334
2160
  }
1335
2161
  const columnDeletion = resolution.operation.tableColumnDeletion;
1336
2162
  if (columnDeletion) {
@@ -1342,8 +2168,24 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1342
2168
  });
1343
2169
  continue;
1344
2170
  }
1345
- claimedTableColumns.add(columnKey);
1346
2171
  }
2172
+ const claim = batchClaimOf({
2173
+ item: resolution.operation,
2174
+ doc: view.state.doc,
2175
+ producesTrackedChanges
2176
+ });
2177
+ const claimedBy = batchClaims.conflictOf(claim);
2178
+ if (claimedBy !== null) {
2179
+ skipped.push({
2180
+ id: operation.id,
2181
+ reason: "overlappingOperation",
2182
+ message: `operation ${JSON.stringify(claimedBy)}, earlier in this batch, already claims its target.`
2183
+ });
2184
+ continue;
2185
+ }
2186
+ batchClaims.add(operation.id, claim);
2187
+ if (deletion) claimedTableRows.add(`${deletion.tablePosition}:${deletion.rowIndex}`);
2188
+ if (columnDeletion) claimedTableColumns.add(getTableColumnCoordinateKey(columnDeletion));
1347
2189
  if ((operation.type === "insertAfterBlock" || operation.type === "insertBeforeBlock") && operation.lineBreakMode !== "inline" && LINE_BREAK_PATTERN.test(operation.text)) normalizations.push({
1348
2190
  id: operation.id,
1349
2191
  code: "splitMultilineText",
@@ -1394,6 +2236,12 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1394
2236
  });
1395
2237
  }
1396
2238
  let tr = view.state.tr;
2239
+ /** Text boxes a tracked block deletion deletes, by anchor. */
2240
+ const deletedTextBoxAnchorIds = /* @__PURE__ */ new Set();
2241
+ /** Container-ending paragraphs a tracked `deleteBlock` emptied: see `withRetiredFinalParagraphs`. */
2242
+ const deletedFinalParagraphs = [];
2243
+ const mergedPropertyTargets = [];
2244
+ const deferredNoopFinalDeletions = /* @__PURE__ */ new Set();
1397
2245
  const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
1398
2246
  let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor;
1399
2247
  /**
@@ -1409,7 +2257,31 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1409
2257
  };
1410
2258
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
1411
2259
  const insertedColumnCounts = /* @__PURE__ */ new Map();
2260
+ const emptiedRows = { pending: false };
2261
+ const tableAfterDeletedSplit = /* @__PURE__ */ new Map();
2262
+ if (mode === "tracked-changes") {
2263
+ const afterTables = /* @__PURE__ */ new Map();
2264
+ for (const item of executableResolved) {
2265
+ if (item.operation.type !== "insertTable" || item.operation.position === "before") continue;
2266
+ const boundaries = afterTables.get(item.blockFrom) ?? /* @__PURE__ */ new Set();
2267
+ boundaries.add(item.from);
2268
+ afterTables.set(item.blockFrom, boundaries);
2269
+ }
2270
+ for (const item of executableResolved) {
2271
+ if (item.operation.type !== "deleteBlock") continue;
2272
+ if (!isInsertedPPrMark(item.blockNode.attrs["pPrMark"])) continue;
2273
+ const at = view.state.doc.resolve(item.blockFrom);
2274
+ if (at.parent.maybeChild(at.index() + 1)?.type !== item.blockNode.type) continue;
2275
+ if (afterTables.get(item.blockFrom)?.has(item.blockTo)) tableAfterDeletedSplit.set(item.blockFrom, commentsAcrossBlockBoundary(view.state.doc, item.blockTo));
2276
+ }
2277
+ }
1412
2278
  const executionOrder = executableResolved.toSorted((left, right) => {
2279
+ if (left.blockFrom === right.blockFrom && tableAfterDeletedSplit.has(left.blockFrom)) {
2280
+ if (left.operation.type === "deleteBlock" && right.operation.type === "insertTable" && right.from === left.blockTo) return -1;
2281
+ if (left.operation.type === "insertTable" && right.operation.type === "deleteBlock" && left.from === right.blockTo) return 1;
2282
+ }
2283
+ const leftAnnotates = ANNOTATION_OPERATION_TYPES.has(left.operation.type);
2284
+ if (leftAnnotates !== ANNOTATION_OPERATION_TYPES.has(right.operation.type)) return leftAnnotates ? -1 : 1;
1413
2285
  const leftCellShape = left.tableCellMerge ?? left.tableCellSplit;
1414
2286
  const rightCellShape = right.tableCellMerge ?? right.tableCellSplit;
1415
2287
  if (!leftCellShape && rightCellShape) return -1;
@@ -1473,7 +2345,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1473
2345
  suggestionId: insertionSuggestionId,
1474
2346
  revisionSeed,
1475
2347
  isPairedMove,
1476
- numbering
2348
+ numbering,
2349
+ styleResolver
1477
2350
  });
1478
2351
  revisionSeed = built.nextRevisionId;
1479
2352
  claimSharedRevisionIds(revisionSeed);
@@ -1499,6 +2372,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1499
2372
  const commentMark = provisionalCommentId !== void 0 && commentType ? commentType.create({ commentId: provisionalCommentId }) : null;
1500
2373
  const stepsBefore = tr.steps.length;
1501
2374
  let appliedRevisionIds = [];
2375
+ let unifiedFormatting = false;
1502
2376
  if (isSuggested && !SUGGESTED_SUPPORTED_OPERATION_TYPES.has(item.operation.type)) {
1503
2377
  skipped.push({
1504
2378
  id: item.operation.id,
@@ -1528,13 +2402,15 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1528
2402
  const minimal = applyMinimalDirectReplacement({
1529
2403
  tr,
1530
2404
  item,
2405
+ noteReferences,
1531
2406
  replacement: item.operation.replace,
1532
2407
  commentMark,
1533
2408
  replacementBackground,
1534
2409
  ...styleResolver !== void 0 ? { styleResolver } : {}
1535
2410
  });
1536
2411
  if (minimal !== null) {
1537
- tr = minimal;
2412
+ tr = minimal.transaction;
2413
+ unifiedFormatting = minimal.unifiesFormatting;
1538
2414
  break;
1539
2415
  }
1540
2416
  }
@@ -1545,6 +2421,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1545
2421
  const tracked = applyMinimalTrackedReplacement({
1546
2422
  tr,
1547
2423
  item,
2424
+ noteReferences,
1548
2425
  replacement: stripInlineEmphasisMarkers(item.operation.replace),
1549
2426
  commentMark,
1550
2427
  replacementBackground,
@@ -1568,6 +2445,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1568
2445
  if (tracked) {
1569
2446
  tr = tracked.transaction;
1570
2447
  operationRevisionSeed = tracked.nextRevisionId;
2448
+ unifiedFormatting = tracked.unifiesFormatting;
1571
2449
  appliedRevisionIds = [
1572
2450
  revisionIdDelete,
1573
2451
  revisionIdInsert,
@@ -1576,6 +2454,13 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1576
2454
  break;
1577
2455
  }
1578
2456
  }
2457
+ if (rangeHoldsNoteReference(item)) {
2458
+ skipped.push({
2459
+ id: item.operation.id,
2460
+ reason: "protectedReference"
2461
+ });
2462
+ continue;
2463
+ }
1579
2464
  const stepsBeforeBackgroundClear = tr.steps.length;
1580
2465
  const backgroundResult = clearReplacementBackground({
1581
2466
  tr,
@@ -1705,6 +2590,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1705
2590
  const minimal = changesText && mode === "direct" ? applyMinimalDirectReplacement({
1706
2591
  tr,
1707
2592
  item,
2593
+ noteReferences,
1708
2594
  replacement: item.operation.text,
1709
2595
  commentMark,
1710
2596
  replacementBackground,
@@ -1713,6 +2599,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1713
2599
  const tracked = changesText && producesTrackedChanges ? applyMinimalTrackedReplacement({
1714
2600
  tr,
1715
2601
  item,
2602
+ noteReferences,
1716
2603
  replacement: stripInlineEmphasisMarkers(item.operation.text),
1717
2604
  commentMark,
1718
2605
  replacementBackground,
@@ -1733,13 +2620,30 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1733
2620
  });
1734
2621
  continue;
1735
2622
  }
1736
- if (minimal !== null) tr = minimal;
1737
- else if (tracked !== null) {
2623
+ if (minimal !== null) {
2624
+ tr = minimal.transaction;
2625
+ unifiedFormatting = minimal.unifiesFormatting;
2626
+ } else if (tracked !== null) {
1738
2627
  tr = tracked.transaction;
1739
2628
  operationRevisionSeed = tracked.nextRevisionId;
2629
+ unifiedFormatting = tracked.unifiesFormatting;
1740
2630
  clearedBackground = tracked.backgroundRevisionIds.length > 0;
1741
2631
  backgroundRevisionIds = tracked.backgroundRevisionIds;
1742
2632
  } else if (changesText) {
2633
+ if (rangeHoldsNoteReference(item)) {
2634
+ skipped.push({
2635
+ id: item.operation.id,
2636
+ reason: "protectedReference"
2637
+ });
2638
+ continue;
2639
+ }
2640
+ if (buildCleanBlockText(item.blockNode, item.blockFrom).structuralBoundaries.length > 0) {
2641
+ skipped.push({
2642
+ id: item.operation.id,
2643
+ reason: "unsupportedBlock"
2644
+ });
2645
+ continue;
2646
+ }
1743
2647
  const stepsBeforeBackgroundClear = tr.steps.length;
1744
2648
  const backgroundResult = clearReplacementBackground({
1745
2649
  tr,
@@ -1802,6 +2706,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1802
2706
  }
1803
2707
  case "insertAfterBlock":
1804
2708
  case "insertBeforeBlock": {
2709
+ if (isSuggested && isPendingDeletion(tr.doc, item.blockFrom, item.blockNode, buildCleanBlockText(item.blockNode, item.blockFrom, {
2710
+ fieldResults: "text",
2711
+ noteReferences
2712
+ }).text)) {
2713
+ skipped.push({
2714
+ id: item.operation.id,
2715
+ reason: "pendingDeletion"
2716
+ });
2717
+ continue;
2718
+ }
1805
2719
  const built = buildInsertedParagraphs({
1806
2720
  item,
1807
2721
  schema: view.state.schema,
@@ -1814,11 +2728,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1814
2728
  suggestionId,
1815
2729
  revisionSeed: operationRevisionSeed,
1816
2730
  isPairedMove,
1817
- numbering
2731
+ numbering,
2732
+ styleResolver
1818
2733
  });
1819
2734
  operationRevisionSeed = built.nextRevisionId;
1820
2735
  if (built.revisionIds.length > 0) appliedRevisionIds = built.revisionIds;
1821
- tr = tr.insert(item.from, built.nodes);
2736
+ tr = insertBlocksInsideComments({
2737
+ tr,
2738
+ at: item.from,
2739
+ blocks: built.nodes
2740
+ });
1822
2741
  break;
1823
2742
  }
1824
2743
  case "insertSignatureTable": {
@@ -1869,9 +2788,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1869
2788
  }
1870
2789
  const revision = structuralRevision;
1871
2790
  const template = tableTemplates?.get(item.operation.id);
2791
+ const mappedInsertion = mapTableRowInsertion(tr, insertion);
1872
2792
  const result = applyTableRowInsertion({
1873
2793
  tr,
1874
- insertion,
2794
+ insertion: mappedInsertion,
1875
2795
  cellTexts: item.operation.cellTexts,
1876
2796
  revision,
1877
2797
  ...template !== void 0 && { template }
@@ -1883,7 +2803,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1883
2803
  });
1884
2804
  continue;
1885
2805
  }
1886
- tr = result.transaction;
2806
+ tr = addCommentsOver(result.transaction, mappedInsertion.rowPosition, item.tableRowComments);
1887
2807
  if (result.revisionId !== null) {
1888
2808
  operationRevisionSeed++;
1889
2809
  appliedRevisionIds = [result.revisionId];
@@ -1906,6 +2826,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1906
2826
  cellTexts: item.operation.cellTexts,
1907
2827
  revision
1908
2828
  });
2829
+ if (result.type === "payloadDoesNotFit") {
2830
+ skipped.push({
2831
+ id: item.operation.id,
2832
+ reason: "payloadDoesNotFit",
2833
+ message: result.message
2834
+ });
2835
+ continue;
2836
+ }
1909
2837
  if (result.type === "unsupported") {
1910
2838
  skipped.push({
1911
2839
  id: item.operation.id,
@@ -1913,11 +2841,9 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1913
2841
  });
1914
2842
  continue;
1915
2843
  }
1916
- tr = result.transaction;
1917
- if (result.revisionId !== null) {
1918
- operationRevisionSeed++;
1919
- appliedRevisionIds = [result.revisionId];
1920
- }
2844
+ tr = addCommentsOver(result.transaction, tr.mapping.map(insertion.tablePosition, 1), item.tableColumnComments);
2845
+ operationRevisionSeed += result.revisionIds.length;
2846
+ appliedRevisionIds = [...result.revisionIds];
1921
2847
  const columnKey = getTableColumnCoordinateKey(insertion);
1922
2848
  insertedColumnCounts.set(columnKey, (insertedColumnCounts.get(columnKey) ?? 0) + 1);
1923
2849
  break;
@@ -1937,7 +2863,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1937
2863
  tr,
1938
2864
  deletion,
1939
2865
  insertedColumnCount: insertedColumnCounts.get(columnKey) ?? 0,
1940
- revision
2866
+ revision,
2867
+ emptiedRows
1941
2868
  });
1942
2869
  if (result.type === "unsupported") {
1943
2870
  skipped.push({
@@ -1947,10 +2874,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1947
2874
  continue;
1948
2875
  }
1949
2876
  tr = result.transaction;
1950
- if (result.revisionId !== null) {
1951
- operationRevisionSeed++;
1952
- appliedRevisionIds = [result.revisionId];
1953
- }
2877
+ operationRevisionSeed += result.revisionIds.length;
2878
+ appliedRevisionIds = [...result.revisionIds];
1954
2879
  break;
1955
2880
  }
1956
2881
  case "mergeTableCells": {
@@ -2061,12 +2986,50 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2061
2986
  }
2062
2987
  break;
2063
2988
  }
2064
- case "deleteBlock":
2989
+ case "deleteBlock": {
2990
+ if (isSuggested && (hasOrdinaryTextRevision(item.blockNode) || hasOrdinaryParagraphMarkRevision(item.blockNode))) {
2991
+ skipped.push({
2992
+ id: item.operation.id,
2993
+ reason: "unsupportedMode"
2994
+ });
2995
+ continue;
2996
+ }
2065
2997
  if (mode === "direct") {
2066
2998
  const at = tr.doc.resolve(item.blockFrom);
2067
2999
  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
3000
  break;
2069
3001
  }
3002
+ const retraction = isPairedMove(item.operation.moveId) ? null : pendingParagraphRetraction(tr.doc, item.blockFrom);
3003
+ if (retraction) {
3004
+ if (isSuggested && !isSuggestedParagraphInsert(item.blockNode)) {
3005
+ skipped.push({
3006
+ id: item.operation.id,
3007
+ reason: "unsupportedMode"
3008
+ });
3009
+ continue;
3010
+ }
3011
+ const retracted = tr.doc.nodeAt(item.blockFrom);
3012
+ tr = tr.delete(item.blockFrom, item.blockTo);
3013
+ if (retraction.clearBreakAt !== null) {
3014
+ tr = tr.setNodeAttribute(retraction.clearBreakAt, "pPrMark", null);
3015
+ const revisionIdProperties = operationRevisionSeed;
3016
+ if (retracted && recordReplacedParagraphProperties({
3017
+ tr,
3018
+ position: retraction.clearBreakAt,
3019
+ replaced: retracted,
3020
+ revision: {
3021
+ id: revisionIdProperties,
3022
+ author,
3023
+ date,
3024
+ ...trackedRevisionExtras
3025
+ }
3026
+ })) {
3027
+ operationRevisionSeed++;
3028
+ appliedRevisionIds = [revisionIdProperties];
3029
+ }
3030
+ }
3031
+ break;
3032
+ }
2070
3033
  if (deletionType) {
2071
3034
  const revisionId = operationRevisionSeed++;
2072
3035
  const deletionMark = deletionType.create({
@@ -2079,6 +3042,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2079
3042
  tr = tr.addMark(item.from, item.to, deletionMark);
2080
3043
  const atomRanges = undeletedContentAtomRanges(tr, item.blockFrom);
2081
3044
  for (const { from, to } of atomRanges) tr = tr.addMark(from, to, deletionMark);
3045
+ for (const anchorId of textBoxAnchorIdsBetween(tr.doc, tr.mapping.map(item.blockFrom), tr.mapping.map(item.blockTo))) deletedTextBoxAnchorIds.add(anchorId);
2082
3046
  encloseWholeControls({
2083
3047
  tr,
2084
3048
  from: item.blockFrom,
@@ -2088,7 +3052,39 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2088
3052
  const inlineRevisionApplied = item.from < item.to || atomRanges.length > 0;
2089
3053
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
2090
3054
  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) {
3055
+ const endsItsContainer = paragraphEndsItsContainer(tr.doc.resolve(markPosition), item.blockNode.type.name);
3056
+ const deleted = tr.doc.nodeAt(markPosition);
3057
+ const following = deleted && tr.doc.nodeAt(markPosition + deleted.nodeSize);
3058
+ if (deleted && following?.type === deleted.type && !isPairedMove(item.operation.moveId) && isInsertedPPrMark(deleted.attrs["pPrMark"])) {
3059
+ const restored = paragraphPropertiesOnReject(deleted);
3060
+ const followingChanges = expectParagraphAttrs(following)._propertyChanges;
3061
+ const recordsRestored = isInsertedPPrMark(following.attrs["pPrMark"]) && !hasSerializableParagraphPropertyChange(followingChanges) && JSON.stringify(restored) !== JSON.stringify(paragraphPropertiesSnapshot(following));
3062
+ joinAtParagraphMark({
3063
+ tr,
3064
+ paragraphPos: markPosition,
3065
+ paragraph: deleted,
3066
+ next: following,
3067
+ styleResolver: styleResolver ?? null
3068
+ });
3069
+ const joined = tr.doc.nodeAt(markPosition);
3070
+ if (recordsRestored && joined) {
3071
+ const propertyRevisionId = operationRevisionSeed++;
3072
+ tr = tr.setNodeMarkup(markPosition, void 0, {
3073
+ ...joined.attrs,
3074
+ _propertyChanges: [...Array.isArray(followingChanges) ? followingChanges : [], {
3075
+ type: "paragraphPropertyChange",
3076
+ info: {
3077
+ id: propertyRevisionId,
3078
+ author,
3079
+ date,
3080
+ ...trackedRevisionExtras
3081
+ },
3082
+ previousFormatting: restored
3083
+ }]
3084
+ });
3085
+ appliedRevisionIds = [...appliedRevisionIds, propertyRevisionId];
3086
+ }
3087
+ } else if (!endsItsContainer && deleted?.attrs["pPrMark"] == null) {
2092
3088
  const markRevisionId = operationRevisionSeed++;
2093
3089
  tr = tr.setNodeAttribute(markPosition, "pPrMark", {
2094
3090
  kind: isPairedMove(item.operation.moveId) ? "moveFrom" : "del",
@@ -2100,10 +3096,25 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2100
3096
  }
2101
3097
  });
2102
3098
  appliedRevisionIds = inlineRevisionApplied ? [revisionId, markRevisionId] : [markRevisionId];
3099
+ } else if (endsItsContainer && !isPairedMove(item.operation.moveId)) deletedFinalParagraphs.push({
3100
+ operationId: item.operation.id,
3101
+ position: markPosition,
3102
+ mappedFrom: tr.mapping.maps.length,
3103
+ revisionExtras: trackedRevisionExtras
3104
+ });
3105
+ else if (deleted && !isPairedMove(item.operation.moveId) && isPlainDeletedPPrMark(deleted.attrs["pPrMark"]) && holdsOnlyDeletedContent(deleted)) {
3106
+ const finalPosition = deletedFinalParagraphAfter(tr.doc.resolve(markPosition), deleted.type.name);
3107
+ if (finalPosition !== null) deletedFinalParagraphs.push({
3108
+ operationId: item.operation.id,
3109
+ position: finalPosition,
3110
+ mappedFrom: tr.mapping.maps.length,
3111
+ revisionExtras: trackedRevisionExtras
3112
+ });
2103
3113
  }
2104
3114
  }
2105
3115
  if (commentMark) tr = tr.addMark(item.from, item.to, commentMark);
2106
3116
  break;
3117
+ }
2107
3118
  case "insertTable": {
2108
3119
  const revision = {
2109
3120
  revisionId: operationRevisionSeed,
@@ -2129,7 +3140,55 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2129
3140
  continue;
2130
3141
  }
2131
3142
  if (producesTrackedChanges) appliedRevisionIds = [operationRevisionSeed++];
2132
- tr = tr.insert(item.from, table);
3143
+ const carriedComments = tableAfterDeletedSplit.get(item.blockFrom);
3144
+ const usesRetractedSplit = carriedComments !== void 0 && item.from === item.blockTo;
3145
+ let at = usesRetractedSplit ? tr.mapping.map(item.blockFrom, -1) : item.from;
3146
+ let insertionComments = carriedComments;
3147
+ if (!usesRetractedSplit) {
3148
+ const boundary = tr.doc.resolve(at);
3149
+ const before = boundary.nodeBefore;
3150
+ const finalDeleted = deletedFinalParagraphAfter(boundary, "paragraph");
3151
+ const mapping = tr.mapping;
3152
+ const pendingIndex = finalDeleted === null ? -1 : deletedFinalParagraphs.findIndex(({ position, mappedFrom }) => mapping.slice(mappedFrom).map(position) === finalDeleted);
3153
+ const pending = pendingIndex < 0 ? void 0 : deletedFinalParagraphs.at(pendingIndex);
3154
+ if (before?.type.name === "paragraph" && finalDeleted !== null && (pending !== void 0 || isPlainDeletedPPrMark(before.attrs["pPrMark"]) && !holdsOnlyDeletedContent(before))) {
3155
+ insertionComments = commentsAcrossBlockBoundary(tr.doc, at);
3156
+ const final = tr.doc.nodeAt(finalDeleted);
3157
+ if (!final) panic("A deleted final paragraph vanished before table insertion", { finalDeleted });
3158
+ if (pending) {
3159
+ const receiptIndex = applied.findIndex(({ id }) => id === pending.operationId);
3160
+ const receipt = receiptIndex < 0 ? void 0 : applied.at(receiptIndex);
3161
+ if (receipt === void 0) panic("A deleted final paragraph lost its operation receipt", { operationId: pending.operationId });
3162
+ const revisionId = operationRevisionSeed++;
3163
+ tr.setNodeAttribute(finalDeleted, "pPrMark", {
3164
+ kind: "del",
3165
+ info: {
3166
+ id: revisionId,
3167
+ author,
3168
+ date,
3169
+ ...pending.revisionExtras
3170
+ }
3171
+ });
3172
+ applied[receiptIndex] = {
3173
+ ...receipt,
3174
+ revisionId: receipt.revisionId ?? revisionId,
3175
+ revisionIds: [...receipt.revisionIds ?? [], revisionId]
3176
+ };
3177
+ deletedFinalParagraphs.splice(pendingIndex, 1);
3178
+ deferredNoopFinalDeletions.delete(pending.operationId);
3179
+ } else {
3180
+ tr.setNodeAttribute(at - before.nodeSize, "pPrMark", null);
3181
+ tr.setNodeAttribute(finalDeleted, "pPrMark", before.attrs["pPrMark"]);
3182
+ }
3183
+ at = finalDeleted + final.nodeSize;
3184
+ }
3185
+ }
3186
+ tr = insertBlocksInsideComments({
3187
+ tr,
3188
+ at,
3189
+ blocks: table,
3190
+ ...insertionComments !== void 0 && { carriedComments: insertionComments }
3191
+ });
2133
3192
  markStructuralChange(tr);
2134
3193
  break;
2135
3194
  }
@@ -2173,10 +3232,21 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2173
3232
  };
2174
3233
  const rowPositions = [];
2175
3234
  let rowOffset = livePosition + 1;
3235
+ let holdsPendingInsertedRow = false;
2176
3236
  liveTable.forEach((row) => {
2177
- if (row.type.spec["tableRole"] === "row" && row.attrs["trDel"] == null) rowPositions.push(rowOffset);
3237
+ if (row.type.spec["tableRole"] === "row" && row.attrs["trDel"] == null) {
3238
+ rowPositions.push(rowOffset);
3239
+ holdsPendingInsertedRow ||= row.attrs["trIns"] != null;
3240
+ }
2178
3241
  rowOffset += row.nodeSize;
2179
3242
  });
3243
+ if (holdsPendingInsertedRow) {
3244
+ skipped.push({
3245
+ id: item.operation.id,
3246
+ reason: "unsupportedBlock"
3247
+ });
3248
+ continue;
3249
+ }
2180
3250
  if (rowPositions.length === 0) {
2181
3251
  skipped.push({
2182
3252
  id: item.operation.id,
@@ -2198,8 +3268,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2198
3268
  break;
2199
3269
  }
2200
3270
  case "setBlockParagraphProperties": {
2201
- const blockPosition = tr.mapping.map(item.blockFrom);
2202
- const liveBlock = tr.doc.nodeAt(blockPosition) ?? item.blockNode;
3271
+ const mapped = tr.mapping.mapResult(item.blockFrom);
3272
+ const liveBlock = mapped.deletedAcross ? null : tr.doc.nodeAt(mapped.pos);
3273
+ const paraId = item.blockNode.attrs["paraId"];
3274
+ if (!liveBlock || liveBlock.type !== item.blockNode.type || typeof paraId === "string" && liveBlock.attrs["paraId"] !== paraId) {
3275
+ skipped.push({
3276
+ id: item.operation.id,
3277
+ reason: "missingBlock"
3278
+ });
3279
+ continue;
3280
+ }
3281
+ const blockPosition = mapped.pos;
2203
3282
  const appliedProperties = applyBlockParagraphProperties({
2204
3283
  tr,
2205
3284
  position: blockPosition,
@@ -2226,9 +3305,17 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2226
3305
  break;
2227
3306
  }
2228
3307
  case "splitBlock": {
3308
+ const splitsFromItsTextBoxes = textBoxAnchorIdsBetween(tr.doc, item.blockFrom, item.from).size > 0 && endOfParagraphWithItsTextBoxes(tr.doc, item.blockFrom) > item.blockTo;
2229
3309
  if (mode === "direct") {
2230
3310
  if (item.to > item.from) tr = tr.delete(item.from, item.to);
2231
3311
  tr = tr.split(item.from);
3312
+ if (splitsFromItsTextBoxes) gatherTextBoxesAfterParagraph(tr, item.blockFrom);
3313
+ } else if (splitsFromItsTextBoxes) {
3314
+ skipped.push({
3315
+ id: item.operation.id,
3316
+ reason: "unsupportedBlock"
3317
+ });
3318
+ continue;
2232
3319
  } else {
2233
3320
  const revisionIdMark = operationRevisionSeed++;
2234
3321
  const info = {
@@ -2286,17 +3373,61 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2286
3373
  break;
2287
3374
  }
2288
3375
  case "mergeBlockWithNext": {
2289
- const separator = item.operation.separator ?? "";
3376
+ if (mode === "direct" && !canJoin(tr.doc, item.blockTo)) {
3377
+ skipped.push({
3378
+ id: item.operation.id,
3379
+ reason: "unsupportedBlock"
3380
+ });
3381
+ continue;
3382
+ }
3383
+ const separator = followedOnlyByDeletedParagraphs(tr.doc, item.blockTo) ? "" : item.operation.separator ?? "";
2290
3384
  const insertAt = item.blockTo - 1;
3385
+ let mergedPropertiesAt = item.blockFrom;
3386
+ let mergedPropertiesRecorded = false;
3387
+ const second = tr.doc.nodeAt(item.blockTo);
3388
+ const spanningComments = commentsAcrossBlockBoundary(tr.doc, item.blockTo);
3389
+ const withSeparator = (transaction) => {
3390
+ const paragraph = transaction.doc.nodeAt(item.blockFrom);
3391
+ const sourceMarks = paragraph?.lastChild?.marks.filter(({ type }) => RUN_FORMATTING_MARK_NAMES.has(type.name)) ?? [];
3392
+ const sourceNode = view.state.schema.text(separator, sourceMarks);
3393
+ const context = paragraph && paragraphRunStyleContext(paragraph, styleResolver);
3394
+ const marks = context ? reconcileRunFormattingMarks({
3395
+ authoredFormatting: readAuthoredRunFormatting({
3396
+ context,
3397
+ marks: sourceMarks,
3398
+ styleResolver
3399
+ }),
3400
+ context,
3401
+ node: sourceNode,
3402
+ styleResolver
3403
+ }) : sourceMarks;
3404
+ let next = transaction.replaceWith(insertAt, insertAt, view.state.schema.text(separator, marks));
3405
+ for (const comment of spanningComments) next = next.addMark(insertAt, insertAt + separator.length, comment);
3406
+ return next;
3407
+ };
2291
3408
  if (mode === "direct") {
2292
- if (separator.length > 0) tr = tr.insertText(separator, insertAt);
3409
+ if (separator.length > 0) tr = withSeparator(tr);
3410
+ const nextPosition = item.blockTo + separator.length;
3411
+ const next = tr.doc.nodeAt(nextPosition);
3412
+ const paragraph = tr.doc.nodeAt(item.blockFrom);
3413
+ if (next && paragraph) tr = rebaseParagraphRuns({
3414
+ tr,
3415
+ position: nextPosition,
3416
+ previous: next,
3417
+ target: paragraph,
3418
+ styleResolver
3419
+ });
3420
+ tr.setMeta(JOINED_RUNS_RESTYLED_META, true);
2293
3421
  tr = tr.join(item.blockTo + separator.length);
3422
+ const secondSection = second ? sectionPropertiesOf(second) : null;
3423
+ const joined = tr.doc.nodeAt(item.blockFrom);
3424
+ if (joined && sectionPropertiesOf(joined) !== secondSection) tr = tr.setNodeAttribute(item.blockFrom, "_sectionProperties", secondSection);
2294
3425
  } else {
2295
3426
  const revisionIdMark = operationRevisionSeed++;
2296
3427
  appliedRevisionIds = [revisionIdMark];
2297
3428
  if (separator.length > 0 && insertionType) {
2298
3429
  const revisionIdSeparator = operationRevisionSeed++;
2299
- tr = tr.insertText(separator, insertAt);
3430
+ tr = withSeparator(tr);
2300
3431
  tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
2301
3432
  revisionId: revisionIdSeparator,
2302
3433
  author,
@@ -2305,27 +3436,86 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2305
3436
  }));
2306
3437
  appliedRevisionIds.push(revisionIdSeparator);
2307
3438
  }
2308
- tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2309
- kind: "del",
2310
- info: {
2311
- id: revisionIdMark,
2312
- author,
2313
- date,
2314
- ...trackedRevisionExtras
2315
- }
2316
- });
3439
+ const paragraph = tr.doc.nodeAt(item.blockFrom);
3440
+ const joinPos = item.blockTo + separator.length;
3441
+ const next = tr.doc.nodeAt(joinPos);
3442
+ if (!paragraph || !next) panic("A resolved merge lost its paragraphs");
3443
+ if (isInsertedPPrMark(paragraph.attrs["pPrMark"])) {
3444
+ joinAtParagraphMark({
3445
+ tr,
3446
+ paragraphPos: item.blockFrom,
3447
+ paragraph,
3448
+ next,
3449
+ styleResolver
3450
+ });
3451
+ const joinedMark = tr.doc.nodeAt(item.blockFrom)?.attrs["pPrMark"];
3452
+ const removedBreakPositions = pendingRemovedFinalBreakPositions(tr, deletedFinalParagraphs);
3453
+ const carryPos = isDeletedPPrMark(joinedMark) || removedBreakPositions.has(item.blockFrom) ? paragraphLeftAfter({
3454
+ doc: tr.doc,
3455
+ paragraphPos: item.blockFrom,
3456
+ removedBreakPositions
3457
+ }) ?? item.blockFrom : item.blockFrom;
3458
+ const carried = carryParagraphProperties({
3459
+ tr,
3460
+ position: carryPos,
3461
+ source: paragraph,
3462
+ styleResolver,
3463
+ numbering,
3464
+ revision: {
3465
+ id: revisionIdMark,
3466
+ author,
3467
+ date,
3468
+ ...trackedRevisionExtras
3469
+ }
3470
+ });
3471
+ mergedPropertiesAt = carryPos;
3472
+ tr = carried.tr;
3473
+ mergedPropertiesRecorded = carried.tracked;
3474
+ if (!carried.tracked) appliedRevisionIds = appliedRevisionIds.filter((id) => id !== revisionIdMark);
3475
+ } else {
3476
+ tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
3477
+ kind: "del",
3478
+ info: {
3479
+ id: revisionIdMark,
3480
+ author,
3481
+ date,
3482
+ ...trackedRevisionExtras
3483
+ }
3484
+ });
3485
+ const survivorPos = paragraphLeftAfter({
3486
+ doc: tr.doc,
3487
+ paragraphPos: item.blockFrom,
3488
+ removedBreakPositions: pendingRemovedFinalBreakPositions(tr, deletedFinalParagraphs)
3489
+ }) ?? joinPos;
3490
+ const carried = carryParagraphProperties({
3491
+ tr,
3492
+ position: survivorPos,
3493
+ source: paragraph,
3494
+ styleResolver,
3495
+ numbering,
3496
+ revision: {
3497
+ id: revisionIdMark,
3498
+ author,
3499
+ date,
3500
+ ...trackedRevisionExtras
3501
+ }
3502
+ });
3503
+ tr = carried.tr;
3504
+ mergedPropertiesRecorded = carried.tracked;
3505
+ mergedPropertiesAt = survivorPos;
3506
+ }
2317
3507
  }
2318
3508
  if (item.operation.mergedParagraphProperties) {
2319
- const mergedParagraph = tr.doc.nodeAt(item.blockFrom);
3509
+ const mergedParagraph = tr.doc.nodeAt(mergedPropertiesAt);
2320
3510
  if (!mergedParagraph) panic("A resolved merge did not produce a paragraph");
2321
3511
  const appliedProperties = applyBlockParagraphProperties({
2322
3512
  tr,
2323
- position: item.blockFrom,
3513
+ position: mergedPropertiesAt,
2324
3514
  node: mergedParagraph,
2325
3515
  properties: item.operation.mergedParagraphProperties,
2326
3516
  styleResolver,
2327
3517
  numbering,
2328
- ...mode === "direct" ? {} : { revisionInfo: () => ({
3518
+ ...mode === "direct" || mergedPropertiesRecorded ? {} : { revisionInfo: () => ({
2329
3519
  id: operationRevisionSeed++,
2330
3520
  author,
2331
3521
  date,
@@ -2335,6 +3525,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2335
3525
  tr = appliedProperties.tr;
2336
3526
  if (appliedProperties.revisionIds.length > 0) appliedRevisionIds.push(...appliedProperties.revisionIds);
2337
3527
  }
3528
+ if (mode !== "direct") mergedPropertyTargets.push({
3529
+ position: mergedPropertiesAt,
3530
+ mappedFrom: tr.mapping.maps.length
3531
+ });
2338
3532
  break;
2339
3533
  }
2340
3534
  case "commentOnBlock":
@@ -2343,11 +3537,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2343
3537
  default: break;
2344
3538
  }
2345
3539
  if (tr.steps.length === stepsBefore) {
2346
- skipped.push({
2347
- id: item.operation.id,
2348
- reason: "noopOperation"
2349
- });
2350
- continue;
3540
+ if (!deletedFinalParagraphs.some(({ operationId }) => operationId === item.operation.id)) {
3541
+ skipped.push({
3542
+ id: item.operation.id,
3543
+ reason: "noopOperation"
3544
+ });
3545
+ continue;
3546
+ }
3547
+ deferredNoopFinalDeletions.add(item.operation.id);
2351
3548
  }
2352
3549
  let committedCommentId;
2353
3550
  if (item.commentText !== void 0) {
@@ -2366,6 +3563,10 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2366
3563
  }
2367
3564
  revisionSeed = operationRevisionSeed;
2368
3565
  claimSharedRevisionIds(revisionSeed);
3566
+ if (unifiedFormatting) normalizations.push({
3567
+ id: item.operation.id,
3568
+ code: "uniformReplacementFormatting"
3569
+ });
2369
3570
  applied.push({
2370
3571
  id: item.operation.id,
2371
3572
  ...committedCommentId !== void 0 && { commentId: committedCommentId },
@@ -2376,9 +3577,11 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2376
3577
  ...suggestionId !== null && { suggestionId }
2377
3578
  });
2378
3579
  }
2379
- if (tr.docChanged) {
3580
+ removeRowsWithoutCellsAfterBatch(tr, emptiedRows);
3581
+ if (tr.docChanged || deferredNoopFinalDeletions.size > 0) {
2380
3582
  const batchRevisionIds = new Set(applied.flatMap(({ revisionIds }) => revisionIds ?? []));
2381
- tr = withoutRevisionsOnZeroWidthAnchors(tr, batchRevisionIds);
3583
+ tr = withoutRevisionsOnZeroWidthAnchors(tr, batchRevisionIds, deletedTextBoxAnchorIds);
3584
+ if (deleteTextBoxesAnchoredAt(tr, droppedTextBoxAnchorIds(view.state.doc, tr.doc))) markStructuralChange(tr);
2382
3585
  continueSharedRevisionIds();
2383
3586
  const rotated = withRotatedAddedFinalBreaks({
2384
3587
  tr,
@@ -2386,10 +3589,33 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2386
3589
  revisionSeed,
2387
3590
  author,
2388
3591
  date,
2389
- initials
3592
+ initials,
3593
+ styleResolver: styleResolver ?? null
2390
3594
  });
2391
3595
  tr = rotated.transaction;
2392
3596
  revisionSeed = rotated.nextRevisionId;
3597
+ const retired = withRetiredFinalParagraphs({
3598
+ tr,
3599
+ deletions: deletedFinalParagraphs,
3600
+ mergedPropertyTargets,
3601
+ appliedIds: new Set(applied.map(({ id }) => id)),
3602
+ batchRevisionIds,
3603
+ revisionSeed,
3604
+ author,
3605
+ date,
3606
+ styleResolver,
3607
+ numbering
3608
+ });
3609
+ revisionSeed = retired.nextRevisionId;
3610
+ for (const operationId of deferredNoopFinalDeletions) {
3611
+ if (retired.resolvedOperationIds.has(operationId)) continue;
3612
+ const receiptIndex = applied.findIndex(({ id }) => id === operationId);
3613
+ if (receiptIndex >= 0) applied.splice(receiptIndex, 1);
3614
+ skipped.push({
3615
+ id: operationId,
3616
+ reason: "noopOperation"
3617
+ });
3618
+ }
2393
3619
  const receiptIndexByRevisionId = /* @__PURE__ */ new Map();
2394
3620
  for (const [receiptIndex, receipt] of applied.entries()) for (const revisionId of receipt.revisionIds ?? []) receiptIndexByRevisionId.set(revisionId, receiptIndex);
2395
3621
  for (const { ownerRevisionId, revisionId } of rotated.synthesizedRevisions) {
@@ -2404,9 +3630,48 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2404
3630
  revisionIds: [...receipt.revisionIds, revisionId]
2405
3631
  };
2406
3632
  }
3633
+ for (const { operationId, revisionId } of retired.addedRevisions) {
3634
+ const receiptIndex = applied.findIndex(({ id }) => id === operationId);
3635
+ const receipt = applied.at(receiptIndex);
3636
+ if (receiptIndex < 0 || !receipt) panic("A retired final-mark revision lost its operation receipt", {
3637
+ operationId,
3638
+ revisionId
3639
+ });
3640
+ const revisionIds = [...receipt.revisionIds ?? [], revisionId];
3641
+ applied[receiptIndex] = {
3642
+ ...receipt,
3643
+ revisionId: receipt.revisionId ?? revisionId,
3644
+ revisionIds
3645
+ };
3646
+ }
3647
+ const retractedRevisionIds = /* @__PURE__ */ new Set([...rotated.retractedRevisionIds, ...retired.retractedRevisionIds]);
3648
+ if (retractedRevisionIds.size > 0) for (const [receiptIndex, receipt] of applied.entries()) {
3649
+ const revisionIds = receipt.revisionIds?.filter((id) => !retractedRevisionIds.has(id));
3650
+ if (revisionIds === void 0) continue;
3651
+ const { revisionId: _oldRevisionId, revisionIds: _oldRevisionIds, ...withoutRevisions } = receipt;
3652
+ const firstRevisionId = revisionIds.at(0);
3653
+ applied[receiptIndex] = firstRevisionId === void 0 ? withoutRevisions : {
3654
+ ...withoutRevisions,
3655
+ revisionId: firstRevisionId,
3656
+ revisionIds
3657
+ };
3658
+ }
3659
+ const separated = separateRevisionStretches({
3660
+ tr,
3661
+ revisionSeed
3662
+ });
3663
+ revisionSeed = separated.nextRevisionId;
3664
+ for (const { revisionId, from } of separated.minted) {
3665
+ const receiptIndex = applied.findIndex(({ revisionIds }) => revisionIds?.includes(from));
3666
+ const receipt = applied.at(receiptIndex);
3667
+ if (receiptIndex >= 0 && receipt?.revisionIds) applied[receiptIndex] = {
3668
+ ...receipt,
3669
+ revisionIds: [...receipt.revisionIds, revisionId]
3670
+ };
3671
+ }
2407
3672
  claimSharedRevisionIds(revisionSeed);
2408
3673
  if (revisionStamp) requestDeterministicParaIds(tr, `${revisionStamp.date}:${String(revisionStamp.idSeed)}`);
2409
- view.dispatch(tr);
3674
+ if (tr.docChanged) view.dispatch(tr);
2410
3675
  }
2411
3676
  return {
2412
3677
  applied,
@@ -2416,9 +3681,16 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2416
3681
  };
2417
3682
  };
2418
3683
  const applyFolioAIEditOperations = (options) => applyFolioAIEditOperationsInternal(options);
2419
- const previewFolioAIEditOperations = (options) => {
3684
+ const previewFolioAIEditOperations = (options) => previewFolioAIEditOperationsWithResult(options).outcome;
3685
+ /**
3686
+ * A preview together with the document it would leave and the provisional
3687
+ * comment ids it placed, for a caller that checks the result before reporting
3688
+ * it. Internal: not re-exported from the package entry points.
3689
+ */
3690
+ const previewFolioAIEditOperationsWithResult = (options) => {
2420
3691
  const { view, createCommentId, ...applyOptions } = options;
2421
3692
  let previewCommentId = -1;
3693
+ const commentIds = [];
2422
3694
  const previewView = {
2423
3695
  state: view.state,
2424
3696
  dispatch: (transaction) => {
@@ -2428,15 +3700,23 @@ const previewFolioAIEditOperations = (options) => {
2428
3700
  const result = applyFolioAIEditOperationsInternal({
2429
3701
  ...applyOptions,
2430
3702
  view: previewView,
2431
- ...createCommentId !== void 0 && { createCommentId: () => previewCommentId-- },
3703
+ ...createCommentId !== void 0 && { createCommentId: () => {
3704
+ const id = previewCommentId--;
3705
+ commentIds.push(id);
3706
+ return id;
3707
+ } },
2432
3708
  revisionIdSeed: -1e9,
2433
3709
  wordDiffMode: "coarse"
2434
3710
  });
2435
3711
  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
3712
+ outcome: {
3713
+ applied: result.applied.map(({ id }) => ({ id })),
3714
+ skipped: result.skipped,
3715
+ ...result.normalizations !== void 0 && { normalizations: result.normalizations },
3716
+ nextRevisionId: options.revisionStamp?.idSeed ?? revisionIdCursor
3717
+ },
3718
+ doc: previewView.state.doc,
3719
+ commentIds
2440
3720
  };
2441
3721
  };
2442
3722
  const CLEAN_TEXT_CONTROL_PATTERN = /[\t\n]/gu;
@@ -2460,9 +3740,10 @@ const cleanTextInlineNodes = ({ schema, text, marks = [] }) => {
2460
3740
  if (trailing.length > 0) nodes.push(schema.text(trailing, marks));
2461
3741
  return nodes;
2462
3742
  };
2463
- const insertCleanText = ({ tr, from, to, text, standsInFor }) => {
3743
+ const insertCleanText = ({ tr, from, to, text, standsInFor, keepsReplacedText = false }) => {
2464
3744
  const annotations = surveyReplacedAnnotations(tr.doc, from, to);
2465
- const replaced = standsInFor ? surveyReplacedAnnotations(tr.doc, standsInFor.from, standsInFor.to).carried : [];
3745
+ const carried = standsInFor ? surveyReplacedAnnotations(tr.doc, standsInFor.from, standsInFor.to).carried : [];
3746
+ const replaced = keepsReplacedText ? commentsForTrackedInsertion(tr.doc, from, [carried])[0] ?? [] : carried;
2466
3747
  if (!hasReplacedAnnotations(annotations) && replaced.length === 0 && !hasCleanTextControls(text)) return {
2467
3748
  transaction: tr.insertText(text, from, to),
2468
3749
  start: from,
@@ -2539,7 +3820,8 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2539
3820
  standsInFor: {
2540
3821
  from: item.from,
2541
3822
  to: item.to
2542
- }
3823
+ },
3824
+ keepsReplacedText: true
2543
3825
  });
2544
3826
  nextTr = inserted.transaction;
2545
3827
  nextTr = nextTr.addMark(inserted.start, inserted.end, insertionType.create(insAttrs));
@@ -2551,6 +3833,17 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
2551
3833
  }
2552
3834
  return nextTr;
2553
3835
  };
3836
+ /** Whether the resolved range covers a footnote or endnote reference. */
3837
+ const rangeHoldsNoteReference = ({ blockNode, blockFrom, from, to }) => {
3838
+ let holds = false;
3839
+ blockNode.descendants((node, pos) => {
3840
+ if (holds) return false;
3841
+ const start = blockFrom + 1 + pos;
3842
+ if (noteReferenceOf(node) !== null && start < to && start + node.nodeSize > from) holds = true;
3843
+ return !holds;
3844
+ });
3845
+ return holds;
3846
+ };
2554
3847
  /**
2555
3848
  * The clean-text span a text replacement matched: where it starts in the
2556
3849
  * block's clean text and what it says. `null` when the operation does not
@@ -2592,17 +3885,27 @@ const isCarriedMark = (mark) => NON_INCLUSIVE_MARK_DISPOSITION[mark.type.name] =
2592
3885
  * document character by character, or the changes do not produce the
2593
3886
  * replacement.
2594
3887
  */
2595
- const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2596
- const cleanBlock = buildCleanBlockText(item.blockNode, item.blockFrom);
3888
+ const planDocumentReplacement = ({ doc, item, replacement, noteReferences, planChanges, keepFieldsOf }) => {
3889
+ const cleanBlock = buildCleanBlockText(item.blockNode, item.blockFrom, {
3890
+ fieldResults: "text",
3891
+ noteReferences
3892
+ });
2597
3893
  const span = replacedCleanSpan(item, cleanBlock);
2598
3894
  if (span === null) return null;
2599
3895
  const spanStart = span.start;
2600
3896
  const spanEnd = span.start + span.text.length;
2601
- const atomicSpans = cleanBlock.structuralBoundaries.flatMap((boundary) => boundary.type === "field" ? [{
3897
+ const noteReferenceSpans = noteReferenceSpansWithin(cleanBlock, spanStart, spanEnd);
3898
+ const editsReferences = !(segmentsAroundNoteReferences(span.text, replacement, noteReferenceSpans) !== null) && noteReferenceEditsAllowed();
3899
+ const atomicSpans = cleanBlock.structuralBoundaries.flatMap((boundary) => boundary.type === "field" || editsReferences && boundary.type === "noteReference" ? [{
2602
3900
  offset: boundary.offset - spanStart,
2603
3901
  length: boundary.length
2604
3902
  }] : []);
2605
- const changes = widenChangesToAtomicSpans(span.text, planChanges(span.text), atomicSpans);
3903
+ const planPieces = (plan) => editsReferences ? [...plan(span.text, replacement)] : planChangesAroundNoteReferences(span.text, replacement, noteReferenceSpans, plan);
3904
+ const pieceChanges = planPieces(planChanges);
3905
+ if (pieceChanges === null) return null;
3906
+ const widened = widenChangesToAtomicSpans(span.text, pieceChanges, atomicSpans);
3907
+ const keptPieces = keepFieldsOf === void 0 ? null : planPieces(keepFieldsOf);
3908
+ const changes = keptPieces === null ? widened : keepAtomicSpans(span.text, replacement, widened, widenChangesToAtomicSpans(span.text, keptPieces, atomicSpans), atomicSpans);
2606
3909
  if (applyTextChanges(span.text, changes) !== replacement) return null;
2607
3910
  const unitAt = (index) => {
2608
3911
  const from = cleanBlock.offsets[index];
@@ -2610,7 +3913,7 @@ const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2610
3913
  if (from === void 0 || !node) return null;
2611
3914
  return {
2612
3915
  from,
2613
- to: node.isText ? from + 1 : from + node.nodeSize
3916
+ to: !node.isText || noteReferenceOf(node) !== null ? from + node.nodeSize : from + 1
2614
3917
  };
2615
3918
  };
2616
3919
  const matchedFrom = item.from;
@@ -2730,6 +4033,72 @@ const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2730
4033
  replacementStyleSource
2731
4034
  };
2732
4035
  };
4036
+ /** The marks that present a character: its formatting, and the links and comments over it. */
4037
+ const presentationMarks = (marks) => marks.filter((mark) => isFormattingMark(mark) || isCarriedMark(mark));
4038
+ /**
4039
+ * Whether a change of `plan` replaces characters that do not all present
4040
+ * alike. Its new text takes one formatting, the first replaced character's,
4041
+ * and every link and comment over the stretch, so the others' formatting ends
4042
+ * where the replaced text did. That is the rule for every mode, and the
4043
+ * receipt says it was applied (`uniformReplacementFormatting`).
4044
+ */
4045
+ const replacementUnifiesFormatting = (doc, plan) => plan.planned.some(({ change, pieces }) => {
4046
+ if (pieces.length === 0 || change.text.length === 0) return false;
4047
+ let first = null;
4048
+ for (let index = change.start; index < change.end; index++) {
4049
+ const unit = plan.unitAt(plan.spanStart + index);
4050
+ const marks = presentationMarks((unit ? doc.nodeAt(unit.from)?.marks : null) ?? Mark.none);
4051
+ if (first === null) first = marks;
4052
+ else if (!Mark.sameSet(first, marks)) return true;
4053
+ }
4054
+ return false;
4055
+ });
4056
+ /**
4057
+ * The marks each UTF-16 unit of `replacement` carries once a direct
4058
+ * replacement has written it (see {@link applyMinimalDirectReplacement}): a
4059
+ * character the direct plan keeps keeps its own, and one it writes takes the
4060
+ * marks that plan gives its change. Background cleared from a touched stretch
4061
+ * is left off either way.
4062
+ *
4063
+ * A tracked replacement writes its insertions with these marks, so accepting
4064
+ * it leaves exactly the text and formatting the direct edit writes, although
4065
+ * its redline cuts the text into coarser, readable changes than the direct
4066
+ * edit does.
4067
+ */
4068
+ const directReplacementMarks = ({ doc, plan, replacementBackground }) => {
4069
+ const clearsBackground = replacementBackground === "clear";
4070
+ const touched = clearsBackground ? touchedBackgroundIndices({
4071
+ doc,
4072
+ plan
4073
+ }) : /* @__PURE__ */ new Set();
4074
+ const withoutBackground = (marks) => marks.filter((mark) => !BACKGROUND_MARK_NAMES.has(mark.type.name));
4075
+ const allocated = [];
4076
+ let cursor = 0;
4077
+ const keepUntil = (end) => {
4078
+ for (; cursor < end; cursor++) {
4079
+ const index = plan.spanStart + cursor;
4080
+ const unit = plan.unitAt(index);
4081
+ const marks = (unit ? doc.nodeAt(unit.from)?.marks : null) ?? Mark.none;
4082
+ allocated.push(touched.has(index) ? withoutBackground(marks) : marks);
4083
+ }
4084
+ };
4085
+ for (const { change, pieces, insertion } of plan.planned) {
4086
+ keepUntil(change.start);
4087
+ cursor = change.end;
4088
+ let marks;
4089
+ if (insertion !== void 0) marks = insertion.marks(doc);
4090
+ else {
4091
+ const first = pieces.at(0);
4092
+ const last = pieces.at(-1);
4093
+ if (first === void 0 || last === void 0) panic("A planned replacement lost the characters it removes");
4094
+ marks = addCarriedMarks(inheritedReplacementMarks(doc, first.from, first.to), surveyReplacedAnnotations(doc, first.from, last.to).carried);
4095
+ }
4096
+ const written = clearsBackground ? withoutBackground(marks) : marks;
4097
+ for (let offset = 0; offset < change.text.length; offset++) allocated.push(written);
4098
+ }
4099
+ keepUntil(plan.spanEnd - plan.spanStart);
4100
+ return allocated;
4101
+ };
2733
4102
  /**
2734
4103
  * Apply a direct-mode text replacement as the difference between the matched
2735
4104
  * text and its replacement; `minimal-replacement.ts` states the contract.
@@ -2738,15 +4107,17 @@ const planDocumentReplacement = ({ doc, item, replacement, planChanges }) => {
2738
4107
  * cannot be mapped onto the document character by character. The caller then
2739
4108
  * replaces the matched span whole, as it always did.
2740
4109
  */
2741
- const applyMinimalDirectReplacement = ({ tr, item, replacement, commentMark, replacementBackground, styleResolver }) => {
4110
+ const applyMinimalDirectReplacement = ({ tr, item, noteReferences, replacement, commentMark, replacementBackground, styleResolver }) => {
2742
4111
  const plan = planDocumentReplacement({
2743
4112
  doc: tr.doc,
2744
4113
  item,
2745
4114
  replacement,
2746
- planChanges: (source) => planTextChanges(source, replacement)
4115
+ noteReferences,
4116
+ planChanges: planTextChanges
2747
4117
  });
2748
4118
  if (plan === null || plan.changes.length === 0) return null;
2749
4119
  const { planned, matchedFrom, matchedTo } = plan;
4120
+ const unifiesFormatting = replacementUnifiesFormatting(tr.doc, plan);
2750
4121
  let nextTr = tr;
2751
4122
  if (replacementBackground === "clear") nextTr = clearTouchedBackground({
2752
4123
  tr: nextTr,
@@ -2810,7 +4181,10 @@ const applyMinimalDirectReplacement = ({ tr, item, replacement, commentMark, rep
2810
4181
  }
2811
4182
  if (to > from) nextTr = nextTr.addMark(from, to, commentMark);
2812
4183
  }
2813
- return nextTr;
4184
+ return {
4185
+ transaction: nextTr,
4186
+ unifiesFormatting
4187
+ };
2814
4188
  };
2815
4189
  /**
2816
4190
  * Apply a tracked-changes or suggested text replacement as its redline: each
@@ -2825,7 +4199,7 @@ const applyMinimalDirectReplacement = ({ tr, item, replacement, commentMark, rep
2825
4199
  * cannot be mapped onto the document character by character; the caller then
2826
4200
  * marks the whole match as replaced, as it always did.
2827
4201
  */
2828
- const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, replacementBackground, styleResolver, diffText, revisionIdDelete, revisionIdInsert, revisionIdBackgroundSeed, author, date, initials, suggestionId }) => {
4202
+ const applyMinimalTrackedReplacement = ({ tr, item, noteReferences, replacement, commentMark, replacementBackground, styleResolver, diffText, revisionIdDelete, revisionIdInsert, revisionIdBackgroundSeed, author, date, initials, suggestionId }) => {
2829
4203
  const schema = tr.doc.type.schema;
2830
4204
  const insertionType = schema.marks["insertion"];
2831
4205
  const deletionType = schema.marks["deletion"];
@@ -2834,9 +4208,24 @@ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, re
2834
4208
  doc: tr.doc,
2835
4209
  item,
2836
4210
  replacement,
2837
- planChanges: (source) => changesFromSegments(diffText(source, replacement))
4211
+ noteReferences,
4212
+ planChanges: (source, target) => changesFromSegments(diffText(source, target)),
4213
+ keepFieldsOf: planTextChanges
2838
4214
  });
2839
4215
  if (plan === null) return null;
4216
+ const directPlan = planDocumentReplacement({
4217
+ doc: tr.doc,
4218
+ item,
4219
+ replacement,
4220
+ noteReferences,
4221
+ planChanges: planTextChanges
4222
+ });
4223
+ const allocatedMarks = directPlan === null ? null : directReplacementMarks({
4224
+ doc: tr.doc,
4225
+ plan: directPlan,
4226
+ replacementBackground
4227
+ });
4228
+ const unifiesFormatting = directPlan === null ? false : replacementUnifiesFormatting(tr.doc, directPlan);
2840
4229
  let nextTr = tr;
2841
4230
  let backgroundRevisionIds = [];
2842
4231
  let nextRevisionId = revisionIdBackgroundSeed;
@@ -2886,10 +4275,39 @@ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, re
2886
4275
  ...revisionAttrs
2887
4276
  });
2888
4277
  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()) {
4278
+ const replacementStarts = [];
4279
+ let replacementShift = 0;
4280
+ for (const { change } of plan.planned) {
4281
+ replacementStarts.push(change.start + replacementShift);
4282
+ replacementShift += change.text.length - (change.end - change.start);
4283
+ }
4284
+ const insertedNodes = (text, replacementStart, fallback) => {
4285
+ if (allocatedMarks?.length !== replacement.length) return cleanTextInlineNodes({
4286
+ schema,
4287
+ text,
4288
+ marks: insertedMarks(fallback)
4289
+ });
4290
+ const marksAt = (offset) => allocatedMarks[replacementStart + offset] ?? fallback;
4291
+ const nodes = [];
4292
+ let groupStart = 0;
4293
+ for (let offset = 1; offset <= text.length; offset++) {
4294
+ if (offset < text.length && Mark.sameSet(marksAt(offset), marksAt(groupStart))) continue;
4295
+ nodes.push(...cleanTextInlineNodes({
4296
+ schema,
4297
+ text: text.slice(groupStart, offset),
4298
+ marks: insertedMarks(marksAt(groupStart))
4299
+ }));
4300
+ groupStart = offset;
4301
+ }
4302
+ return nodes;
4303
+ };
4304
+ for (let plannedIndex = plan.planned.length - 1; plannedIndex >= 0; plannedIndex--) {
4305
+ const change = plan.planned[plannedIndex];
4306
+ if (change === void 0) panic("A planned replacement change went missing", { plannedIndex });
2890
4307
  const doc = nextTr.doc;
2891
4308
  let at;
2892
4309
  let marks;
4310
+ let removedFrom = null;
2893
4311
  if (change.insertion !== void 0) {
2894
4312
  at = change.insertion.at;
2895
4313
  marks = change.insertion.marks(doc);
@@ -2899,6 +4317,7 @@ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, re
2899
4317
  const styleSource = plan.replacementStyleSource(change.change);
2900
4318
  if (first === void 0 || last === void 0 || styleSource === null) panic("A planned replacement lost the characters it removes");
2901
4319
  at = last.to;
4320
+ removedFrom = first.from;
2902
4321
  marks = addCarriedMarks(inheritedReplacementMarks(doc, styleSource.from, styleSource.to), surveyReplacedAnnotations(doc, first.from, last.to).carried);
2903
4322
  for (const piece of change.pieces) {
2904
4323
  nextTr = nextTr.addMark(piece.from, piece.to, deletion);
@@ -2906,21 +4325,36 @@ const applyMinimalTrackedReplacement = ({ tr, item, replacement, commentMark, re
2906
4325
  }
2907
4326
  }
2908
4327
  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);
4328
+ const pieces = insertedNodes(change.text, replacementStarts[plannedIndex] ?? 0, marks);
4329
+ const positions = removedFrom === null ? pieces.map(() => at) : placeTrackedInsertion(nextTr.doc, removedFrom, at, pieces.map((node) => node.marks));
4330
+ const runs = [];
4331
+ for (const [index, piece] of pieces.entries()) {
4332
+ const position = positions[index] ?? at;
4333
+ const run = runs.at(-1);
4334
+ if (run?.at === position) run.pieces.push(piece);
4335
+ else runs.push({
4336
+ at: position,
4337
+ pieces: [piece]
4338
+ });
4339
+ }
4340
+ for (const run of runs.toReversed()) {
4341
+ let nodes = run.pieces;
4342
+ if (removedFrom === null) {
4343
+ const pieceMarks = commentsForTrackedInsertion(nextTr.doc, run.at, run.pieces.map((node) => node.marks));
4344
+ nodes = run.pieces.map((node, index) => node.mark(pieceMarks[index] ?? node.marks));
4345
+ }
4346
+ nextTr = nextTr.insert(run.at, nodes);
4347
+ const end = run.at + Fragment.fromArray(nodes).size;
4348
+ nextTr = nextTr.addMark(run.at, end, insertion);
4349
+ if (commentMark) nextTr = nextTr.addMark(run.at, end, commentMark);
4350
+ }
2918
4351
  }
2919
4352
  return {
2920
4353
  type: "applied",
2921
4354
  transaction: nextTr,
2922
4355
  backgroundRevisionIds,
2923
- nextRevisionId
4356
+ nextRevisionId,
4357
+ unifiesFormatting
2924
4358
  };
2925
4359
  };
2926
4360
  /**
@@ -2980,7 +4414,7 @@ const clearTouchedBackground = ({ tr, ranges, styleResolver }) => {
2980
4414
  });
2981
4415
  return nextTr;
2982
4416
  };
2983
- const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId }) => {
4417
+ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId, noteReferences }) => {
2984
4418
  const anchor = snapshot.anchors[blockId];
2985
4419
  if (!anchor) return {
2986
4420
  type: "skip",
@@ -3006,7 +4440,10 @@ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId
3006
4440
  type: "skip",
3007
4441
  reason: "changedBlock"
3008
4442
  };
3009
- const cleanBlock = buildCleanBlockText(live.node, live.from);
4443
+ const cleanBlock = buildCleanBlockText(live.node, live.from, {
4444
+ fieldResults: "text",
4445
+ noteReferences
4446
+ });
3010
4447
  const currentText = cleanBlock.text;
3011
4448
  const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText));
3012
4449
  const structuralBoundaryHash = hashFolioAIBlockStructuralBoundaries(cleanBlock);
@@ -3024,13 +4461,52 @@ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId
3024
4461
  currentTextHash
3025
4462
  };
3026
4463
  };
3027
- const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, doc }) => {
4464
+ const resolveOperation = (args) => {
4465
+ const resolution = resolveOperationTarget(args);
4466
+ if (resolution.type === "skip") return resolution;
4467
+ return (noteReferenceEditsAllowed() ? null : refuseNoteReferenceEdit(resolution.operation, args.noteReferences)) ?? resolution;
4468
+ };
4469
+ /**
4470
+ * A text change may rewrite the prose around a footnote or endnote reference
4471
+ * but never the reference: its marker is structure the clean text shows, not
4472
+ * characters it holds. The replacement has to keep every marker its match
4473
+ * covers, in order, and may not write a marker-shaped string of its own; the
4474
+ * replacement's inline emphasis would rebuild the match whole, reference
4475
+ * included, so it cannot carry one either. Checked before anything is applied,
4476
+ * so direct and tracked mode refuse the same operations.
4477
+ */
4478
+ const refuseNoteReferenceEdit = (item, noteReferences) => {
4479
+ const { operation } = item;
4480
+ let replacement;
4481
+ if (operation.type === "replaceInBlock" || operation.type === "replaceRange") replacement = operation.replace;
4482
+ else if (operation.type === "replaceBlock") replacement = operation.text;
4483
+ else return null;
4484
+ const cleanBlock = buildCleanBlockText(item.blockNode, item.blockFrom, {
4485
+ fieldResults: "text",
4486
+ noteReferences
4487
+ });
4488
+ const span = replacedCleanSpan({ operation }, cleanBlock);
4489
+ if (span === null) return null;
4490
+ const spans = noteReferenceSpansWithin(cleanBlock, span.start, span.start + span.text.length);
4491
+ const rebuildsBlock = operation.type === "replaceBlock" && operation.preserveFormatting === false;
4492
+ if (spans.length > 0 && (hasInlineEmphasis(replacement) || rebuildsBlock)) return {
4493
+ type: "skip",
4494
+ reason: "protectedReference"
4495
+ };
4496
+ if (operation.type === "replaceBlock" && replacement === span.text) return null;
4497
+ return segmentsAroundNoteReferences(span.text, stripInlineEmphasisMarkers(replacement), spans) === null ? {
4498
+ type: "skip",
4499
+ reason: "protectedReference"
4500
+ } : null;
4501
+ };
4502
+ const resolveOperationTarget = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, noteReferences, doc }) => {
3028
4503
  const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
3029
4504
  const primaryBlock = resolveStableBlock({
3030
4505
  snapshot,
3031
4506
  blockId,
3032
4507
  liveBlocks,
3033
- liveBlocksByParaId
4508
+ liveBlocksByParaId,
4509
+ noteReferences
3034
4510
  });
3035
4511
  if (primaryBlock.type === "skip") return primaryBlock;
3036
4512
  const { blockNode, blockFrom, blockTo, cleanBlock, currentText, currentTextHash } = primaryBlock;
@@ -3049,6 +4525,16 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3049
4525
  type: "skip",
3050
4526
  reason: "staleRange"
3051
4527
  };
4528
+ const characterSplit = describeCharacterSplit(currentText, [startOffset, endOffset], operation.type === "replaceRange" ? "grapheme" : "codePoint");
4529
+ if (characterSplit !== null) return {
4530
+ type: "skip",
4531
+ reason: "splitsCharacter",
4532
+ message: characterSplit
4533
+ };
4534
+ if (cutsIntoNoteReference(cleanBlock, startOffset, endOffset)) return {
4535
+ type: "skip",
4536
+ reason: "protectedReference"
4537
+ };
3052
4538
  const range = resolveCleanTextRange({
3053
4539
  cleanBlock,
3054
4540
  startOffset,
@@ -3079,7 +4565,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3079
4565
  const isInsertAfter = operation.type === "insertAfterBlock";
3080
4566
  let insertFrom;
3081
4567
  if (tableBoundary) insertFrom = isInsertAfter ? tableBoundary.after : tableBoundary.before;
3082
- else insertFrom = isInsertAfter ? blockTo : blockFrom;
4568
+ else insertFrom = isInsertAfter ? endOfParagraphWithItsTextBoxes(doc, blockFrom) : blockFrom;
3083
4569
  const insertTexts = operation.lineBreakMode !== "inline" && LINE_BREAK_PATTERN.test(operation.text) ? splitInsertParagraphTexts(operation.text) : [operation.text];
3084
4570
  return {
3085
4571
  type: "resolved",
@@ -3103,7 +4589,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3103
4589
  const tableBoundary = findOutermostTableBoundary(doc, blockFrom);
3104
4590
  let insertFrom;
3105
4591
  if (tableBoundary) insertFrom = position === "after" ? tableBoundary.after : tableBoundary.before;
3106
- else insertFrom = position === "after" ? blockTo : blockFrom;
4592
+ else insertFrom = position === "after" ? endOfParagraphWithItsTextBoxes(doc, blockFrom) : blockFrom;
3107
4593
  return {
3108
4594
  type: "resolved",
3109
4595
  operation: {
@@ -3122,10 +4608,20 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3122
4608
  blockFrom,
3123
4609
  position: operation.position ?? "after"
3124
4610
  });
3125
- if (!insertion || (operation.cellTexts?.length ?? 0) > insertion.columnCount) return {
4611
+ if (!insertion) return {
3126
4612
  type: "skip",
3127
4613
  reason: "unsupportedBlock"
3128
4614
  };
4615
+ const overflow = describeUnplacedCellTexts({
4616
+ cellTexts: operation.cellTexts,
4617
+ capacity: insertion.cells.length,
4618
+ target: "row"
4619
+ });
4620
+ if (overflow !== null) return {
4621
+ type: "skip",
4622
+ reason: "payloadDoesNotFit",
4623
+ message: overflow
4624
+ };
3129
4625
  return {
3130
4626
  type: "resolved",
3131
4627
  operation: {
@@ -3135,7 +4631,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3135
4631
  blockFrom,
3136
4632
  blockTo,
3137
4633
  blockNode,
3138
- tableRowInsertion: insertion
4634
+ tableRowInsertion: insertion,
4635
+ tableRowComments: commentsAcrossBlockBoundary(doc, insertion.rowPosition)
3139
4636
  }
3140
4637
  };
3141
4638
  }
@@ -3183,7 +4680,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3183
4680
  blockFrom,
3184
4681
  blockTo,
3185
4682
  blockNode,
3186
- tableColumnInsertion
4683
+ tableColumnInsertion,
4684
+ tableColumnComments: commentsAcrossTable(doc, tableColumnInsertion.tablePosition)
3187
4685
  }
3188
4686
  };
3189
4687
  }
@@ -3248,7 +4746,8 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3248
4746
  snapshot,
3249
4747
  blockId: operation.endBlockId,
3250
4748
  liveBlocks,
3251
- liveBlocksByParaId
4749
+ liveBlocksByParaId,
4750
+ noteReferences
3252
4751
  });
3253
4752
  if (endTarget.type === "skip") return endTarget;
3254
4753
  const endCell = findEnclosingTableCell(doc, endTarget.blockFrom);
@@ -3320,7 +4819,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3320
4819
  if (operation.type === "insertTable") {
3321
4820
  const boundary = findOutermostTableBoundary(doc, blockFrom) ?? {
3322
4821
  before: blockFrom,
3323
- after: blockTo
4822
+ after: endOfParagraphWithItsTextBoxes(doc, blockFrom)
3324
4823
  };
3325
4824
  const insertFrom = (operation.position ?? "after") === "after" ? boundary.after : boundary.before;
3326
4825
  return {
@@ -3382,6 +4881,12 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3382
4881
  type: "skip",
3383
4882
  reason: "staleRange"
3384
4883
  };
4884
+ const characterSplit = describeCharacterSplit(currentText, [startOffset, endOffset], "grapheme");
4885
+ if (characterSplit !== null) return {
4886
+ type: "skip",
4887
+ reason: "splitsCharacter",
4888
+ message: characterSplit
4889
+ };
3385
4890
  const range = resolveCleanTextRange({
3386
4891
  cleanBlock,
3387
4892
  startOffset,
@@ -3420,10 +4925,14 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3420
4925
  }
3421
4926
  };
3422
4927
  }
4928
+ if (operation.type === "replaceBlock" && isPendingDeletion(doc, blockFrom, blockNode, currentText)) return {
4929
+ type: "skip",
4930
+ reason: "pendingDeletion"
4931
+ };
3423
4932
  if (operation.type === "deleteBlock" || operation.type === "replaceBlock") {
3424
4933
  const replaceChangesText = operation.type === "replaceBlock" && operation.text !== currentText;
3425
4934
  const replaceChangesStyle = operation.type === "replaceBlock" && operation.styleId !== void 0 && operation.styleId !== (expectParagraphAttrs(blockNode).styleId ?? null);
3426
- if (replaceChangesText && cleanBlock.structuralBoundaries.length > 0) return {
4935
+ if (replaceChangesText && cleanBlock.structuralBoundaries.some((boundary) => boundary.type !== "field" && boundary.type !== "noteReference")) return {
3427
4936
  type: "skip",
3428
4937
  reason: "unsupportedBlock"
3429
4938
  };
@@ -3503,7 +5012,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3503
5012
  reason: "noopOperation"
3504
5013
  };
3505
5014
  const quote = getOperationQuote(operation);
3506
- const range = resolveTextInCleanBlock(cleanBlock, quote || currentText);
5015
+ const range = resolveTextInCleanBlock(cleanBlock, quote || currentText, operation.type === "replaceInBlock" ? "grapheme" : "codePoint");
3507
5016
  if (range.type !== "resolved") return range;
3508
5017
  return {
3509
5018
  type: "resolved",
@@ -3517,7 +5026,7 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
3517
5026
  }
3518
5027
  };
3519
5028
  };
3520
- const resolveTextInCleanBlock = (cleanBlock, find) => {
5029
+ const resolveTextInCleanBlock = (cleanBlock, find, strictness) => {
3521
5030
  if (find.length === 0) return {
3522
5031
  type: "skip",
3523
5032
  reason: "emptyOperation"
@@ -3532,6 +5041,16 @@ const resolveTextInCleanBlock = (cleanBlock, find) => {
3532
5041
  type: "skip",
3533
5042
  reason: "ambiguousFind"
3534
5043
  };
5044
+ const characterSplit = describeCharacterSplit(text, [firstIndex, firstIndex + find.length], strictness);
5045
+ if (characterSplit !== null) return {
5046
+ type: "skip",
5047
+ reason: "splitsCharacter",
5048
+ message: `the match's ${characterSplit}`
5049
+ };
5050
+ if (cutsIntoNoteReference(cleanBlock, firstIndex, firstIndex + find.length)) return {
5051
+ type: "skip",
5052
+ reason: "protectedReference"
5053
+ };
3535
5054
  const range = resolveCleanTextRange({
3536
5055
  cleanBlock,
3537
5056
  startOffset: firstIndex,
@@ -3557,9 +5076,128 @@ const getTextRangeFromCleanBlock = (cleanBlock) => {
3557
5076
  };
3558
5077
  };
3559
5078
  const getOperationCommentText = (operation) => "comment" in operation ? operation.comment?.text : void 0;
5079
+ /**
5080
+ * Every paragraph style an operation names, with the field that names it.
5081
+ * `null` clears the style and names none; an omitted one keeps the style the
5082
+ * paragraph or its anchor already carries, which the document holds by
5083
+ * construction.
5084
+ */
5085
+ const operationParagraphStyleReferences = (operation) => {
5086
+ switch (operation.type) {
5087
+ case "insertAfterBlock":
5088
+ case "insertBeforeBlock":
5089
+ case "replaceBlock": return [{
5090
+ path: "styleId",
5091
+ styleId: operation.styleId
5092
+ }];
5093
+ case "setBlockParagraphProperties": return [{
5094
+ path: "properties.styleId",
5095
+ styleId: operation.properties.styleId
5096
+ }];
5097
+ case "splitBlock": return [{
5098
+ path: "firstParagraphProperties.styleId",
5099
+ styleId: operation.firstParagraphProperties?.styleId
5100
+ }, {
5101
+ path: "secondParagraphProperties.styleId",
5102
+ styleId: operation.secondParagraphProperties?.styleId
5103
+ }];
5104
+ case "mergeBlockWithNext": return [{
5105
+ path: "mergedParagraphProperties.styleId",
5106
+ styleId: operation.mergedParagraphProperties?.styleId
5107
+ }];
5108
+ default: return [];
5109
+ }
5110
+ };
5111
+ /**
5112
+ * The first paragraph style the operation names that the document does not
5113
+ * define as a paragraph style. A `w:pStyle` naming no definition, or a table
5114
+ * or character style, confers no formatting: the paragraph keeps its body
5115
+ * look while the operation reports a restyle. Refused here, before anything
5116
+ * is applied, the way an undefined block id is. `null` definitions (a state
5117
+ * without the styles plugin) cannot say what is defined, and check nothing.
5118
+ */
5119
+ const findUndefinedParagraphStyleReference = (operation, definitions) => {
5120
+ if (definitions === null) return;
5121
+ for (const { path, styleId } of operationParagraphStyleReferences(operation)) {
5122
+ if (styleId === null || styleId === void 0) continue;
5123
+ const definition = definitions.get(styleId);
5124
+ if (definition?.type !== "paragraph") return {
5125
+ path,
5126
+ styleId,
5127
+ definedAs: definition?.type
5128
+ };
5129
+ }
5130
+ };
5131
+ const MAX_LISTED_PARAGRAPH_STYLES = 12;
5132
+ const describeUndefinedParagraphStyle = ({ path, styleId, definedAs }, definitions) => {
5133
+ const defined = (definitions?.paragraphStyles() ?? []).map((style) => style.styleId);
5134
+ let available = "it defines no visible paragraph styles";
5135
+ if (defined.length > 0) available = `its paragraph styles include ${defined.slice(0, MAX_LISTED_PARAGRAPH_STYLES).join(", ")}${defined.length > MAX_LISTED_PARAGRAPH_STYLES ? ", …" : ""}`;
5136
+ return `${path} "${styleId}" ${definedAs === void 0 ? "names no style in this document" : `names a ${definedAs} style, not a paragraph style`} (${available}).`;
5137
+ };
5138
+ /** The `w:num` id a stated numbering names, when it names one. */
5139
+ const statedNumberingInstance = (numbering) => {
5140
+ if (numbering === null || numbering === void 0) return;
5141
+ const numId = Reflect.get(numbering, "numId");
5142
+ return typeof numId === "number" ? numId : void 0;
5143
+ };
5144
+ /**
5145
+ * Every numbering instance an operation names, with the field that names it.
5146
+ * `listLevel` is not among them: it keeps the instance the paragraph or its
5147
+ * anchor already references, which the document holds by construction.
5148
+ */
5149
+ const operationNumberingReferences = (operation) => {
5150
+ switch (operation.type) {
5151
+ case "insertAfterBlock":
5152
+ case "insertBeforeBlock": return [{
5153
+ path: "numbering",
5154
+ numbering: operation.numbering
5155
+ }];
5156
+ case "setBlockParagraphProperties": return [{
5157
+ path: "properties.numbering",
5158
+ numbering: operation.properties.numbering
5159
+ }];
5160
+ case "splitBlock": return [{
5161
+ path: "firstParagraphProperties.numbering",
5162
+ numbering: operation.firstParagraphProperties?.numbering
5163
+ }, {
5164
+ path: "secondParagraphProperties.numbering",
5165
+ numbering: operation.secondParagraphProperties?.numbering
5166
+ }];
5167
+ case "mergeBlockWithNext": return [{
5168
+ path: "mergedParagraphProperties.numbering",
5169
+ numbering: operation.mergedParagraphProperties?.numbering
5170
+ }];
5171
+ default: return [];
5172
+ }
5173
+ };
5174
+ /**
5175
+ * The first numbering instance the operation names that the document does not
5176
+ * define. A paragraph referencing a missing `w:num` is a model no save can
5177
+ * write, so it is refused here, before anything is applied, the way a block id
5178
+ * naming no block is. `null` instance ids (a state without the numbering
5179
+ * plugin) cannot say what is defined, and check nothing.
5180
+ */
5181
+ const findUndefinedNumberingReference = (operation, instanceIds) => {
5182
+ if (instanceIds === null) return;
5183
+ for (const { path, numbering } of operationNumberingReferences(operation)) {
5184
+ const numId = statedNumberingInstance(numbering);
5185
+ if (numId !== void 0 && !instanceIds.has(numId)) return {
5186
+ path,
5187
+ numId
5188
+ };
5189
+ }
5190
+ };
5191
+ const MAX_LISTED_NUMBERING_INSTANCES = 12;
5192
+ const describeUndefinedNumbering = ({ path, numId }, instanceIds) => {
5193
+ const defined = [...instanceIds ?? []].sort((left, right) => left - right);
5194
+ let available = "it defines none";
5195
+ if (defined.length > 0) available = `it defines ${defined.slice(0, MAX_LISTED_NUMBERING_INSTANCES).join(", ")}${defined.length > MAX_LISTED_NUMBERING_INSTANCES ? ", …" : ""}`;
5196
+ return `${path}.numId ${String(numId)} names no numbering instance in this document (${available}).`;
5197
+ };
3560
5198
  const getOperationQuote = (operation) => {
3561
5199
  if (operation.type === "replaceInBlock") return operation.find;
3562
5200
  if (operation.type === "commentOnBlock") return operation.quote;
3563
5201
  };
3564
5202
  //#endregion
3565
- export { applyFolioAIEditOperations, previewFolioAIEditOperations };
5203
+ export { applyFolioAIEditOperations, previewFolioAIEditOperations, previewFolioAIEditOperationsWithResult };