@stll/folio-core 0.11.0 → 0.13.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 (263) hide show
  1. package/dist/ai-edits/apply.d.ts +2 -1
  2. package/dist/ai-edits/apply.js +693 -63
  3. package/dist/ai-edits/block-identity.d.ts +4 -0
  4. package/dist/ai-edits/block-identity.js +9 -0
  5. package/dist/ai-edits/blockRange.d.ts +28 -1
  6. package/dist/ai-edits/blockRange.js +68 -2
  7. package/dist/ai-edits/clean-text.js +2 -1
  8. package/dist/ai-edits/headless.d.ts +4 -4
  9. package/dist/ai-edits/headless.js +9 -9
  10. package/dist/ai-edits/index.d.ts +2 -2
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.d.ts +7 -7
  13. package/dist/ai-edits/read.js +110 -3
  14. package/dist/ai-edits/snapshot.js +21 -3
  15. package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
  16. package/dist/ai-edits/table-cell-mutations.js +238 -0
  17. package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
  18. package/dist/ai-edits/table-mutation-plan.js +90 -0
  19. package/dist/ai-edits/table-row-column-mutations.d.ts +108 -0
  20. package/dist/ai-edits/table-row-column-mutations.js +300 -0
  21. package/dist/ai-edits/table-targets.d.ts +32 -0
  22. package/dist/ai-edits/table-targets.js +70 -0
  23. package/dist/ai-edits/types.d.ts +62 -1
  24. package/dist/ai-edits/word-diff.js +21 -0
  25. package/dist/compat/eigenpal.d.ts +3 -3
  26. package/dist/compat/eigenpal.js +3 -3
  27. package/dist/content-controls/mutateContentControls.js +1 -1
  28. package/dist/controller/activeEditorStory.d.ts +36 -0
  29. package/dist/controller/activeEditorStory.js +30 -0
  30. package/dist/controller/contentControlWidgetController.d.ts +55 -0
  31. package/dist/controller/contentControlWidgetController.js +141 -0
  32. package/dist/controller/fontReadiness.js +2 -0
  33. package/dist/controller/headerFooterEditorManager.js +2 -2
  34. package/dist/controller/hiddenEditorApi.js +2 -2
  35. package/dist/controller/hiddenEditorManager.d.ts +12 -1
  36. package/dist/controller/hiddenEditorManager.js +44 -12
  37. package/dist/controller/layoutPipeline.d.ts +2 -0
  38. package/dist/controller/layoutPipeline.js +17 -2
  39. package/dist/controller/noteEditorManager.d.ts +36 -0
  40. package/dist/controller/noteEditorManager.js +210 -0
  41. package/dist/document-operations.d.ts +43 -11
  42. package/dist/document-operations.js +186 -18
  43. package/dist/docx/blockContentParser.js +21 -7
  44. package/dist/docx/commentParser.js +6 -1
  45. package/dist/docx/commentRangeIntegrity.d.ts +6 -0
  46. package/dist/docx/commentRangeIntegrity.js +186 -0
  47. package/dist/docx/conformance.js +62 -1
  48. package/dist/docx/documentGrid.d.ts +8 -0
  49. package/dist/docx/documentGrid.js +8 -0
  50. package/dist/docx/drawingUtils.js +1 -1
  51. package/dist/docx/encryption/agileDecryption.js +12 -2
  52. package/dist/docx/encryption/compoundFile.js +6 -1
  53. package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
  54. package/dist/docx/encryption/encryptionInfo.js +18 -2
  55. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  56. package/dist/docx/ensureParaIds.js +8 -7
  57. package/dist/docx/fieldParser.js +1 -1
  58. package/dist/docx/groupDrawingParser.js +10 -5
  59. package/dist/docx/hyperlinkParser.js +1 -1
  60. package/dist/docx/imageParser.js +10 -6
  61. package/dist/docx/metadataPrivacy.js +17 -3
  62. package/dist/docx/notePropertiesParser.js +1 -1
  63. package/dist/docx/numberingParser.js +1 -1
  64. package/dist/docx/paragraphParser.js +70 -61
  65. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  66. package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
  67. package/dist/docx/paragraphTraversal.js +1 -1
  68. package/dist/docx/parser.js +3 -3
  69. package/dist/docx/rezip.js +20 -9
  70. package/dist/docx/runParser.js +17 -6
  71. package/dist/docx/sdtProperties.js +1 -1
  72. package/dist/docx/sectionParser.js +18 -5
  73. package/dist/docx/selectiveSave.js +5 -1
  74. package/dist/docx/selectiveXmlPatch.d.ts +22 -1
  75. package/dist/docx/selectiveXmlPatch.js +61 -1
  76. package/dist/docx/serializer/blockSdtSerializer.js +3 -2
  77. package/dist/docx/serializer/commentSerializer.js +5 -6
  78. package/dist/docx/serializer/documentSerializer.js +2 -2
  79. package/dist/docx/serializer/headerFooterSerializer.js +2 -2
  80. package/dist/docx/serializer/noteSerializer.js +2 -2
  81. package/dist/docx/serializer/paragraphSerializer.js +23 -15
  82. package/dist/docx/serializer/runSerializer.js +25 -21
  83. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
  84. package/dist/docx/serializer/stylesSerializer.js +1 -1
  85. package/dist/docx/serializer/tableSerializer.d.ts +5 -4
  86. package/dist/docx/serializer/tableSerializer.js +26 -20
  87. package/dist/docx/serializer/xmlUtils.d.ts +13 -1
  88. package/dist/docx/serializer/xmlUtils.js +27 -1
  89. package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
  90. package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
  91. package/dist/docx/server/boundedArchive.d.ts +14 -3
  92. package/dist/docx/server/boundedArchive.js +56 -11
  93. package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
  94. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
  95. package/dist/docx/server/extractDocxText.js +55 -8
  96. package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
  97. package/dist/docx/server/inspectDocxPackage.js +217 -0
  98. package/dist/docx/server/validateDocxConformance.d.ts +41 -0
  99. package/dist/docx/server/validateDocxConformance.js +336 -0
  100. package/dist/docx/settingsParser.js +26 -3
  101. package/dist/docx/shapeParser.js +1 -1
  102. package/dist/docx/styleParser.js +4 -2
  103. package/dist/docx/tableParser.d.ts +8 -1
  104. package/dist/docx/tableParser.js +31 -10
  105. package/dist/docx/textBoxParser.d.ts +3 -3
  106. package/dist/docx/textBoxParser.js +16 -12
  107. package/dist/docx/unzip.js +1 -0
  108. package/dist/docx/vmlImageParser.js +11 -3
  109. package/dist/docx/xmlParser.js +11 -1
  110. package/dist/docx/xmlSafety.d.ts +5 -0
  111. package/dist/docx/xmlSafety.js +9 -0
  112. package/dist/fields/resolveFieldValues.js +1 -3
  113. package/dist/fonts/embeddedFonts.d.ts +33 -5
  114. package/dist/fonts/embeddedFonts.js +37 -8
  115. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  116. package/dist/i18n/messages/catalogs.gen.js +17 -0
  117. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  118. package/dist/index.d.ts +3 -3
  119. package/dist/index.js +3 -3
  120. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  121. package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
  122. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  123. package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
  124. package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
  125. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  126. package/dist/layout-bridge/convert/toFlowBlocks.js +151 -20
  127. package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
  128. package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
  129. package/dist/layout-bridge/engine/clickToPosition.js +2 -2
  130. package/dist/layout-engine/index.d.ts +2 -2
  131. package/dist/layout-engine/index.js +58 -30
  132. package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
  133. package/dist/layout-engine/justificationCompatibility.js +8 -0
  134. package/dist/layout-engine/keep-together.js +11 -1
  135. package/dist/layout-engine/measure/cache.js +23 -5
  136. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  137. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  138. package/dist/layout-engine/measure/lineBreakProvider.js +56 -37
  139. package/dist/layout-engine/measure/measureBlocks.js +15 -4
  140. package/dist/layout-engine/measure/measureContainer.js +1 -1
  141. package/dist/layout-engine/measure/measureHelpers.d.ts +11 -2
  142. package/dist/layout-engine/measure/measureHelpers.js +15 -4
  143. package/dist/layout-engine/measure/measureParagraph.js +85 -25
  144. package/dist/layout-engine/measure/measureProvider.js +4 -3
  145. package/dist/layout-engine/measure/tableCellFlow.js +5 -2
  146. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
  147. package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
  148. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
  149. package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
  150. package/dist/layout-engine/paginator.js +5 -1
  151. package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
  152. package/dist/layout-engine/paragraphSpacing.js +107 -13
  153. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  154. package/dist/layout-engine/types.d.ts +48 -15
  155. package/dist/layout-painter/registry/modules/textBox.js +5 -1
  156. package/dist/layout-painter/renderImage.d.ts +12 -1
  157. package/dist/layout-painter/renderImage.js +21 -4
  158. package/dist/layout-painter/renderPage.d.ts +5 -2
  159. package/dist/layout-painter/renderPage.js +59 -14
  160. package/dist/layout-painter/renderParagraph.js +55 -15
  161. package/dist/layout-painter/renderTable.d.ts +5 -1
  162. package/dist/layout-painter/renderTable.js +7 -3
  163. package/dist/layout-painter/renderTextBox.d.ts +2 -1
  164. package/dist/layout-painter/renderTextBox.js +25 -14
  165. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  166. package/dist/layout-painter/renderWatermark.js +2 -1
  167. package/dist/managers/DocumentLoaderManager.js +1 -1
  168. package/dist/markdown/fromMarkdown.js +1 -1
  169. package/dist/model.js +1 -1
  170. package/dist/prosemirror/attrs/index.d.ts +4 -2
  171. package/dist/prosemirror/attrs/index.js +175 -18
  172. package/dist/prosemirror/commands/comments.d.ts +79 -4
  173. package/dist/prosemirror/commands/comments.js +622 -13
  174. package/dist/prosemirror/commands/contentControls.js +1 -1
  175. package/dist/prosemirror/commands/hyperlink.js +15 -1
  176. package/dist/prosemirror/commands/image.js +7 -1
  177. package/dist/prosemirror/commands/index.d.ts +2 -2
  178. package/dist/prosemirror/commands/index.js +2 -2
  179. package/dist/prosemirror/commands/pageBreak.js +1 -1
  180. package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
  181. package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
  182. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
  183. package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
  184. package/dist/prosemirror/commentIdAllocator.js +13 -3
  185. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
  186. package/dist/prosemirror/conversion/fromProseDoc.js +404 -62
  187. package/dist/prosemirror/conversion/index.js +1 -1
  188. package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
  189. package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
  190. package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
  191. package/dist/prosemirror/conversion/toProseDoc.js +364 -103
  192. package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
  193. package/dist/prosemirror/extensions/StarterKit.js +6 -1
  194. package/dist/prosemirror/extensions/core/ParagraphExtension.js +6 -2
  195. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
  196. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  197. package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
  198. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
  199. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
  200. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
  201. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
  202. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  203. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
  204. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  205. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
  206. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +70 -11
  207. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  208. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
  209. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
  210. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  211. package/dist/prosemirror/extensions/nodes/TableExtension.js +14 -6
  212. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
  213. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
  214. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
  215. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
  216. package/dist/prosemirror/index.d.ts +1 -1
  217. package/dist/prosemirror/index.js +1 -1
  218. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +36 -10
  219. package/dist/prosemirror/plugins/contentControlWidgets.js +128 -94
  220. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  221. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  222. package/dist/prosemirror/plugins/selectionTracker.js +1 -0
  223. package/dist/prosemirror/plugins/suggestionMode.js +5 -4
  224. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  225. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  226. package/dist/prosemirror/schema/index.d.ts +2 -2
  227. package/dist/prosemirror/schema/marks.d.ts +30 -3
  228. package/dist/prosemirror/schema/marks.js +15 -0
  229. package/dist/prosemirror/schema/nodes.d.ts +111 -6
  230. package/dist/prosemirror/selectionState.js +1 -0
  231. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  232. package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
  233. package/dist/prosemirror/tableCellMergeRevision.js +22 -0
  234. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
  235. package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
  236. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
  237. package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
  238. package/dist/prosemirror/validation.js +9 -1
  239. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  240. package/dist/server.d.ts +7 -3
  241. package/dist/server.js +7 -3
  242. package/dist/utils/clipboard.d.ts +10 -1
  243. package/dist/utils/clipboard.js +33 -6
  244. package/dist/utils/colorResolver.d.ts +8 -1
  245. package/dist/utils/colorResolver.js +35 -5
  246. package/dist/utils/fontFamilyMerge.js +9 -11
  247. package/dist/utils/fontLoader.js +3 -2
  248. package/dist/utils/fontResolver.d.ts +2 -1
  249. package/dist/utils/fontResolver.js +31 -7
  250. package/dist/utils/fontWeights.d.ts +1 -1
  251. package/dist/utils/fontWeights.js +1 -1
  252. package/dist/utils/hexId.d.ts +11 -1
  253. package/dist/utils/hexId.js +11 -1
  254. package/dist/utils/paragraphFormattingMerge.js +1 -0
  255. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  256. package/dist/utils/sanitizeImageSrc.js +30 -0
  257. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  258. package/dist/utils/stripXmlDeclarations.js +35 -0
  259. package/dist/version-comparison.d.ts +22 -2
  260. package/dist/version-comparison.js +41 -12
  261. package/dist/watermark/index.d.ts +11 -1
  262. package/dist/watermark/index.js +22 -1
  263. package/package.json +3 -2
@@ -1,27 +1,38 @@
1
+ import { expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js";
2
+ import { marksToTextFormatting } from "../prosemirror/conversion/fromProseDoc.js";
1
3
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
4
+ import { stripBlockIdentityAttrs } from "./block-identity.js";
2
5
  import { buildCleanBlockText } from "./clean-text.js";
3
6
  import { hasInlineEmphasis, parseInlineEmphasisRuns, stripInlineEmphasisMarkers } from "./inline-emphasis.js";
4
7
  import { hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js";
8
+ import { planTableMutations } from "./table-mutation-plan.js";
9
+ import { findEnclosingTableBoundary, findEnclosingTableCell, findEnclosingTableRow, tableRectangleCutsMergedCell } from "./table-targets.js";
10
+ import { mergeTableRectangle, mergeTrackedVerticalTableCells, splitTableRectangle, splitTrackedVerticalTableCell } from "./table-cell-mutations.js";
11
+ import { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey } from "./table-row-column-mutations.js";
5
12
  import { diffWordSegments } from "./word-diff.js";
13
+ import { TableMap } from "prosemirror-tables";
6
14
  //#region src/ai-edits/apply.ts
7
15
  /**
8
- * Block attrs that identify a block instance (Word's
9
- * `w14:paraId` / `w14:textId`, future tracked-change identifiers).
10
- * Reuse during inheritFormatting would create duplicate IDs, so
11
- * we strip them when synthesising a sibling.
16
+ * Operation types applied in `"suggested"` mode. Every produced revision — an
17
+ * inline mark, a whole-node `_suggestedInsert` marker, or a suggested
18
+ * `trIns`/`trDel`/`cellMarker` is stripped from serialized DOCX until
19
+ * accepted. Cell merge/split (not row/column ops) and comment ops stay
20
+ * `unsupportedMode`.
12
21
  */
13
- const IDENTITY_BLOCK_ATTRS = /* @__PURE__ */ new Set(["paraId", "textId"]);
14
- const stripIdentityAttrs = (attrs) => {
15
- const next = {};
16
- for (const [key, value] of Object.entries(attrs)) {
17
- if (IDENTITY_BLOCK_ATTRS.has(key)) {
18
- next[key] = null;
19
- continue;
20
- }
21
- next[key] = value;
22
- }
23
- return next;
24
- };
22
+ const SUGGESTED_SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
23
+ "replaceInBlock",
24
+ "replaceRange",
25
+ "formatRange",
26
+ "replaceBlock",
27
+ "deleteBlock",
28
+ "insertAfterBlock",
29
+ "insertBeforeBlock",
30
+ "insertSignatureTable",
31
+ "insertTableRow",
32
+ "deleteTableRow",
33
+ "insertTableColumn",
34
+ "deleteTableColumn"
35
+ ]);
25
36
  const applyReplaceBlockStyleId = ({ item, tr }) => {
26
37
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return tr;
27
38
  const block = tr.doc.nodeAt(item.blockFrom);
@@ -31,6 +42,69 @@ const applyReplaceBlockStyleId = ({ item, tr }) => {
31
42
  styleId: item.operation.styleId
32
43
  });
33
44
  };
45
+ const applyInlineFormatting = ({ tr, schema, from, to, formatting }) => {
46
+ for (const [name, enabled] of Object.entries(formatting)) {
47
+ const markType = schema.marks[name];
48
+ if (!markType) continue;
49
+ if (enabled) {
50
+ tr.addMark(from, to, name === "underline" ? markType.create({ style: "single" }) : markType.create());
51
+ continue;
52
+ }
53
+ tr.removeMark(from, to, markType);
54
+ }
55
+ return tr;
56
+ };
57
+ const formattingWouldChange = (marks, formatting) => Object.entries(formatting).some(([name, enabled]) => {
58
+ const mark = marks.find((candidate) => candidate.type.name === name);
59
+ if (!enabled) return mark !== void 0;
60
+ if (name === "underline") return mark?.attrs["style"] !== "single";
61
+ return mark === void 0;
62
+ });
63
+ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionId, author, date, initials, suggestionId = null }) => {
64
+ const propertyChangeType = schema.marks["runPropertyChange"];
65
+ if (!propertyChangeType) return tr;
66
+ const segments = [];
67
+ doc.nodesBetween(from, to, (node, pos) => {
68
+ if (!node.isText || !formattingWouldChange(node.marks, formatting)) return;
69
+ const segmentFrom = Math.max(from, pos);
70
+ const segmentTo = Math.min(to, pos + node.nodeSize);
71
+ const previousFormatting = marksToTextFormatting(node.marks);
72
+ const existingMark = node.marks.find((mark) => mark.type === propertyChangeType);
73
+ const existingChanges = existingMark ? expectRunPropertyChangeMarkAttrs(existingMark).changes : [];
74
+ const change = {
75
+ type: "runPropertyChange",
76
+ info: {
77
+ id: revisionId,
78
+ author,
79
+ date,
80
+ ...initials ? { initials } : {}
81
+ },
82
+ ...Object.keys(previousFormatting).length > 0 ? { previousFormatting } : {}
83
+ };
84
+ segments.push({
85
+ from: segmentFrom,
86
+ to: segmentTo,
87
+ changes: [...existingChanges, change]
88
+ });
89
+ });
90
+ if (segments.length === 0) return tr;
91
+ const suggestionAttrs = suggestionId === null ? {} : {
92
+ provenance: "suggested",
93
+ suggestionId
94
+ };
95
+ applyInlineFormatting({
96
+ tr,
97
+ schema,
98
+ from,
99
+ to,
100
+ formatting
101
+ });
102
+ for (const segment of segments) tr.addMark(segment.from, segment.to, propertyChangeType.create({
103
+ changes: segment.changes,
104
+ ...suggestionAttrs
105
+ }));
106
+ return tr;
107
+ };
34
108
  /**
35
109
  * Module-scoped monotonic counter for tracked-change revision ids.
36
110
  * Seeded once from `Date.now()` so ids are roughly time-ordered for
@@ -93,13 +167,21 @@ const collectLiveBlocksByParaId = (doc) => {
93
167
  });
94
168
  return byParaId;
95
169
  };
96
- const findEnclosingTableBoundary = (doc, blockFrom) => {
97
- const resolved = doc.resolve(blockFrom);
98
- for (let depth = resolved.depth; depth > 0; depth--) if (resolved.node(depth).type.name === "table") return {
99
- before: resolved.before(depth),
100
- after: resolved.after(depth)
170
+ const getTableMutationPlanTarget = (item) => {
171
+ if (item.tableCellMerge) return {
172
+ type: "mergeCells",
173
+ ...item.tableCellMerge
101
174
  };
102
- return null;
175
+ if (item.tableCellSplit) return {
176
+ type: "splitCell",
177
+ ...item.tableCellSplit
178
+ };
179
+ const tablePosition = item.tableRowInsertion?.tableStart !== void 0 ? item.tableRowInsertion.tableStart - 1 : item.tableRowDeletion?.tablePosition ?? item.tableColumnInsertion?.tablePosition ?? item.tableColumnDeletion?.tablePosition;
180
+ if (tablePosition !== void 0) return {
181
+ type: "tableStructure",
182
+ tablePosition
183
+ };
184
+ return { type: "none" };
103
185
  };
104
186
  /**
105
187
  * The snapshot recorded an `hashOccurrenceCount` per anchor but
@@ -193,14 +275,18 @@ const buildEmphasisInlineContent = (schema, text, baseMarks) => {
193
275
  }
194
276
  return nodes.length > 0 ? nodes : [schema.text(text, [...baseMarks])];
195
277
  };
196
- const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", createCommentId, revisionIdSeed }) => {
278
+ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionIdSeed }) => {
197
279
  const applied = [];
198
280
  const skipped = [];
199
281
  const resolved = [];
200
282
  const insertionType = view.state.schema.marks["insertion"];
201
283
  const deletionType = view.state.schema.marks["deletion"];
202
284
  const commentType = view.state.schema.marks["comment"];
203
- if (mode === "tracked-changes" && (!insertionType || !deletionType)) return {
285
+ const claimedTableRows = /* @__PURE__ */ new Set();
286
+ const claimedTableColumns = /* @__PURE__ */ new Set();
287
+ const producesTrackedChanges = mode !== "direct";
288
+ const isSuggested = mode === "suggested";
289
+ if (producesTrackedChanges && (!insertionType || !deletionType)) return {
204
290
  applied,
205
291
  skipped: operations.map((operation) => ({
206
292
  id: operation.id,
@@ -210,13 +296,6 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
210
296
  const liveBlocks = collectLiveBlocksByHash(view.state.doc);
211
297
  const liveBlocksByParaId = collectLiveBlocksByParaId(view.state.doc);
212
298
  for (const [index, operation] of operations.entries()) {
213
- if (mode === "tracked-changes" && operation.type === "formatRange") {
214
- skipped.push({
215
- id: operation.id,
216
- reason: "unsupportedMode"
217
- });
218
- continue;
219
- }
220
299
  const commentText = getOperationCommentText(operation);
221
300
  if (commentText !== void 0 && (!commentType || createCommentId === void 0)) {
222
301
  skipped.push({
@@ -239,6 +318,30 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
239
318
  });
240
319
  continue;
241
320
  }
321
+ const deletion = resolution.operation.tableRowDeletion;
322
+ if (deletion) {
323
+ const rowKey = `${deletion.tablePosition}:${deletion.rowIndex}`;
324
+ if (claimedTableRows.has(rowKey)) {
325
+ skipped.push({
326
+ id: operation.id,
327
+ reason: "noopOperation"
328
+ });
329
+ continue;
330
+ }
331
+ claimedTableRows.add(rowKey);
332
+ }
333
+ const columnDeletion = resolution.operation.tableColumnDeletion;
334
+ if (columnDeletion) {
335
+ const columnKey = getTableColumnCoordinateKey(columnDeletion);
336
+ if (claimedTableColumns.has(columnKey)) {
337
+ skipped.push({
338
+ id: operation.id,
339
+ reason: "noopOperation"
340
+ });
341
+ continue;
342
+ }
343
+ claimedTableColumns.add(columnKey);
344
+ }
242
345
  const commentId = commentText !== void 0 ? createCommentId?.(commentText) : void 0;
243
346
  resolved.push({
244
347
  ...resolution.operation,
@@ -246,20 +349,70 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
246
349
  ...commentId !== void 0 && { commentId }
247
350
  });
248
351
  }
249
- if (resolved.length === 0) return {
352
+ const tablePlan = planTableMutations(resolved.map((item) => ({
353
+ item,
354
+ operationId: item.operation.id,
355
+ target: getTableMutationPlanTarget(item)
356
+ })));
357
+ skipped.push(...tablePlan.skipped);
358
+ const executableResolved = tablePlan.executable;
359
+ if (executableResolved.length === 0) return {
250
360
  applied,
251
361
  skipped
252
362
  };
253
363
  let tr = view.state.tr;
254
- let revisionSeed = revisionIdSeed ?? nextRevisionSeed(resolved.length);
364
+ let revisionSeed = revisionIdSeed ?? nextRevisionSeed(executableResolved.length);
255
365
  const date = (/* @__PURE__ */ new Date()).toISOString();
256
- for (const item of resolved.toSorted((left, right) => {
366
+ const insertedColumnCounts = /* @__PURE__ */ new Map();
367
+ for (const item of executableResolved.toSorted((left, right) => {
368
+ const leftCellShape = left.tableCellMerge ?? left.tableCellSplit;
369
+ const rightCellShape = right.tableCellMerge ?? right.tableCellSplit;
370
+ if (!leftCellShape && rightCellShape) return -1;
371
+ if (leftCellShape && !rightCellShape) return 1;
372
+ if (leftCellShape && rightCellShape) {
373
+ if (leftCellShape.tablePosition !== rightCellShape.tablePosition) return rightCellShape.tablePosition - leftCellShape.tablePosition;
374
+ if (leftCellShape.rectangle.bottom !== rightCellShape.rectangle.bottom) return rightCellShape.rectangle.bottom - leftCellShape.rectangle.bottom;
375
+ if (leftCellShape.rectangle.right !== rightCellShape.rectangle.right) return rightCellShape.rectangle.right - leftCellShape.rectangle.right;
376
+ return right.originalIndex - left.originalIndex;
377
+ }
378
+ const leftColumn = left.tableColumnInsertion ?? left.tableColumnDeletion;
379
+ const rightColumn = right.tableColumnInsertion ?? right.tableColumnDeletion;
380
+ if (!leftColumn && rightColumn) return -1;
381
+ if (leftColumn && !rightColumn) return 1;
382
+ if (leftColumn && rightColumn) {
383
+ if (leftColumn.tablePosition !== rightColumn.tablePosition) return rightColumn.tablePosition - leftColumn.tablePosition;
384
+ if (leftColumn.columnIndex !== rightColumn.columnIndex) return rightColumn.columnIndex - leftColumn.columnIndex;
385
+ const leftIsInsertion = left.tableColumnInsertion !== void 0;
386
+ if (leftIsInsertion !== (right.tableColumnInsertion !== void 0)) return leftIsInsertion ? -1 : 1;
387
+ return right.originalIndex - left.originalIndex;
388
+ }
257
389
  if (left.from !== right.from) return right.from - left.from;
258
390
  return right.originalIndex - left.originalIndex;
259
391
  })) {
260
392
  const commentMark = item.commentId !== void 0 && commentType ? commentType.create({ commentId: item.commentId }) : null;
261
393
  const stepsBefore = tr.steps.length;
262
394
  let appliedRevisionIds;
395
+ if (isSuggested && !SUGGESTED_SUPPORTED_OPERATION_TYPES.has(item.operation.type)) {
396
+ skipped.push({
397
+ id: item.operation.id,
398
+ reason: "unsupportedMode"
399
+ });
400
+ continue;
401
+ }
402
+ const suggestionId = isSuggested ? item.operation.suggestionId ?? item.operation.id : null;
403
+ const trackedRevisionExtras = {
404
+ ...initials ? { initials } : {},
405
+ ...suggestionId !== null ? {
406
+ provenance: "suggested",
407
+ suggestionId
408
+ } : {}
409
+ };
410
+ const structuralRevision = producesTrackedChanges ? {
411
+ revisionId: revisionSeed,
412
+ author,
413
+ date,
414
+ ...trackedRevisionExtras
415
+ } : null;
263
416
  switch (item.operation.type) {
264
417
  case "replaceInBlock":
265
418
  case "replaceRange": {
@@ -273,21 +426,42 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
273
426
  date,
274
427
  revisionIdDelete,
275
428
  revisionIdInsert,
276
- commentMark
429
+ commentMark,
430
+ suggestionId,
431
+ initials
277
432
  });
278
- if (mode === "tracked-changes") appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
433
+ if (producesTrackedChanges) appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
279
434
  break;
280
435
  }
281
436
  case "commentOnRange":
282
437
  if (commentMark) tr = tr.addMark(item.from, item.to, commentMark);
283
438
  break;
284
439
  case "formatRange":
285
- for (const [name, enabled] of Object.entries(item.operation.formatting)) {
286
- const markType = view.state.schema.marks[name];
287
- if (!markType) continue;
288
- if (enabled) tr = tr.addMark(item.from, item.to, name === "underline" ? markType.create({ style: "single" }) : markType.create());
289
- else tr = tr.removeMark(item.from, item.to, markType);
440
+ if (producesTrackedChanges) {
441
+ const revisionId = revisionSeed++;
442
+ tr = applyTrackedInlineFormatting({
443
+ tr,
444
+ schema: view.state.schema,
445
+ doc: tr.doc,
446
+ from: item.from,
447
+ to: item.to,
448
+ formatting: item.operation.formatting,
449
+ revisionId,
450
+ author,
451
+ date,
452
+ initials,
453
+ suggestionId
454
+ });
455
+ appliedRevisionIds = [revisionId];
456
+ break;
290
457
  }
458
+ tr = applyInlineFormatting({
459
+ tr,
460
+ schema: view.state.schema,
461
+ from: item.from,
462
+ to: item.to,
463
+ formatting: item.operation.formatting
464
+ });
291
465
  break;
292
466
  case "replaceBlock": {
293
467
  const revisionIdDelete = revisionSeed++;
@@ -319,13 +493,15 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
319
493
  date,
320
494
  revisionIdDelete,
321
495
  revisionIdInsert,
322
- commentMark
496
+ commentMark,
497
+ suggestionId,
498
+ initials
323
499
  });
324
500
  tr = applyReplaceBlockStyleId({
325
501
  item,
326
502
  tr
327
503
  });
328
- if (mode === "tracked-changes") appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
504
+ if (producesTrackedChanges) appliedRevisionIds = [revisionIdDelete, revisionIdInsert];
329
505
  break;
330
506
  }
331
507
  case "insertAfterBlock":
@@ -338,18 +514,21 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
338
514
  continue;
339
515
  }
340
516
  const marks = [];
341
- if (mode === "tracked-changes" && insertionType) {
517
+ let insertedBlockRevisionId = null;
518
+ if (producesTrackedChanges && insertionType) {
342
519
  const revisionId = revisionSeed++;
520
+ insertedBlockRevisionId = revisionId;
343
521
  marks.push(insertionType.create({
344
522
  revisionId,
345
523
  author,
346
- date
524
+ date,
525
+ ...trackedRevisionExtras
347
526
  }));
348
527
  appliedRevisionIds = [revisionId];
349
528
  }
350
529
  if (commentMark) marks.push(commentMark);
351
530
  const content = item.insertText && item.insertText.length > 0 ? buildEmphasisInlineContent(view.state.schema, item.insertText, marks) : null;
352
- const attrs = { ...item.operation.inheritFormatting === false ? {} : stripIdentityAttrs(item.blockNode.attrs) };
531
+ const attrs = { ...item.operation.inheritFormatting === false ? {} : stripBlockIdentityAttrs(item.blockNode.attrs) };
353
532
  if (item.operation.pageBreakBefore === true) attrs["pageBreakBefore"] = true;
354
533
  if (item.operation.styleId !== void 0) {
355
534
  attrs["styleId"] = item.operation.styleId;
@@ -362,6 +541,13 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
362
541
  attrs["listStartOverride"] = null;
363
542
  }
364
543
  }
544
+ if (isSuggested && suggestionId !== null && insertedBlockRevisionId !== null) attrs["_suggestedInsert"] = {
545
+ suggestionId,
546
+ revisionId: insertedBlockRevisionId,
547
+ author,
548
+ date,
549
+ ...initials ? { initials } : {}
550
+ };
365
551
  const node = item.blockNode.type.create(attrs, content);
366
552
  tr = tr.insert(item.from, node);
367
553
  break;
@@ -374,20 +560,236 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
374
560
  });
375
561
  continue;
376
562
  }
377
- const node = buildSignatureTableNode({
563
+ const signatureTable = buildSignatureTableNode({
378
564
  schema: view.state.schema,
379
565
  parties: item.operation.parties
380
566
  });
381
- if (!node) {
567
+ if (!signatureTable) {
382
568
  skipped.push({
383
569
  id: item.operation.id,
384
570
  reason: "unsupportedBlock"
385
571
  });
386
572
  continue;
387
573
  }
574
+ let node = signatureTable;
575
+ if (isSuggested && suggestionId !== null) {
576
+ const revisionId = revisionSeed++;
577
+ node = signatureTable.type.create({
578
+ ...signatureTable.attrs,
579
+ _suggestedInsert: {
580
+ suggestionId,
581
+ revisionId,
582
+ author,
583
+ date,
584
+ ...initials ? { initials } : {}
585
+ }
586
+ }, signatureTable.content, signatureTable.marks);
587
+ appliedRevisionIds = [revisionId];
588
+ }
388
589
  tr = tr.insert(item.from, node);
389
590
  break;
390
591
  }
592
+ case "insertTableRow": {
593
+ const insertion = item.tableRowInsertion;
594
+ if (!insertion) {
595
+ skipped.push({
596
+ id: item.operation.id,
597
+ reason: "unsupportedBlock"
598
+ });
599
+ continue;
600
+ }
601
+ const revision = structuralRevision;
602
+ const result = applyTableRowInsertion({
603
+ tr,
604
+ insertion,
605
+ cellTexts: item.operation.cellTexts,
606
+ revision
607
+ });
608
+ if (result.type === "unsupported") {
609
+ skipped.push({
610
+ id: item.operation.id,
611
+ reason: "unsupportedBlock"
612
+ });
613
+ continue;
614
+ }
615
+ tr = result.transaction;
616
+ if (result.revisionId !== null) {
617
+ revisionSeed++;
618
+ appliedRevisionIds = [result.revisionId];
619
+ }
620
+ break;
621
+ }
622
+ case "insertTableColumn": {
623
+ const insertion = item.tableColumnInsertion;
624
+ if (!insertion) {
625
+ skipped.push({
626
+ id: item.operation.id,
627
+ reason: "unsupportedBlock"
628
+ });
629
+ continue;
630
+ }
631
+ const revision = structuralRevision;
632
+ const result = applyTableColumnInsertion({
633
+ tr,
634
+ insertion,
635
+ cellTexts: item.operation.cellTexts,
636
+ revision
637
+ });
638
+ if (result.type === "unsupported") {
639
+ skipped.push({
640
+ id: item.operation.id,
641
+ reason: "unsupportedBlock"
642
+ });
643
+ continue;
644
+ }
645
+ tr = result.transaction;
646
+ if (result.revisionId !== null) {
647
+ revisionSeed++;
648
+ appliedRevisionIds = [result.revisionId];
649
+ }
650
+ const columnKey = getTableColumnCoordinateKey(insertion);
651
+ insertedColumnCounts.set(columnKey, (insertedColumnCounts.get(columnKey) ?? 0) + 1);
652
+ break;
653
+ }
654
+ case "deleteTableColumn": {
655
+ const deletion = item.tableColumnDeletion;
656
+ if (!deletion) {
657
+ skipped.push({
658
+ id: item.operation.id,
659
+ reason: "unsupportedBlock"
660
+ });
661
+ continue;
662
+ }
663
+ const columnKey = getTableColumnCoordinateKey(deletion);
664
+ const revision = structuralRevision;
665
+ const result = applyTableColumnDeletion({
666
+ tr,
667
+ deletion,
668
+ insertedColumnCount: insertedColumnCounts.get(columnKey) ?? 0,
669
+ revision
670
+ });
671
+ if (result.type === "unsupported") {
672
+ skipped.push({
673
+ id: item.operation.id,
674
+ reason: "unsupportedBlock"
675
+ });
676
+ continue;
677
+ }
678
+ tr = result.transaction;
679
+ if (result.revisionId !== null) {
680
+ revisionSeed++;
681
+ appliedRevisionIds = [result.revisionId];
682
+ }
683
+ break;
684
+ }
685
+ case "mergeTableCells": {
686
+ const merge = item.tableCellMerge;
687
+ const tablePosition = merge ? tr.mapping.map(merge.tablePosition, 1) : null;
688
+ const table = tablePosition === null ? null : tr.doc.nodeAt(tablePosition);
689
+ if (!merge || tablePosition === null || !table || table.type.spec["tableRole"] !== "table") {
690
+ skipped.push({
691
+ id: item.operation.id,
692
+ reason: "unsupportedBlock"
693
+ });
694
+ continue;
695
+ }
696
+ const revisionId = mode === "tracked-changes" ? revisionSeed : null;
697
+ const nextTr = revisionId === null ? mergeTableRectangle({
698
+ tr,
699
+ tablePosition,
700
+ table,
701
+ rectangle: merge.rectangle
702
+ }) : mergeTrackedVerticalTableCells({
703
+ tr,
704
+ tablePosition,
705
+ table,
706
+ rectangle: merge.rectangle,
707
+ revisionId,
708
+ author,
709
+ date
710
+ });
711
+ if (!nextTr) {
712
+ skipped.push({
713
+ id: item.operation.id,
714
+ reason: "unsupportedBlock"
715
+ });
716
+ continue;
717
+ }
718
+ tr = nextTr;
719
+ if (revisionId !== null) {
720
+ revisionSeed++;
721
+ appliedRevisionIds = [revisionId];
722
+ }
723
+ break;
724
+ }
725
+ case "splitTableCell": {
726
+ const split = item.tableCellSplit;
727
+ const tablePosition = split ? tr.mapping.map(split.tablePosition, 1) : null;
728
+ const table = tablePosition === null ? null : tr.doc.nodeAt(tablePosition);
729
+ if (!split || tablePosition === null || !table || table.type.spec["tableRole"] !== "table") {
730
+ skipped.push({
731
+ id: item.operation.id,
732
+ reason: "unsupportedBlock"
733
+ });
734
+ continue;
735
+ }
736
+ const revisionId = mode === "tracked-changes" ? revisionSeed : null;
737
+ const nextTr = revisionId === null ? splitTableRectangle({
738
+ tr,
739
+ tablePosition,
740
+ table,
741
+ rectangle: split.rectangle
742
+ }) : splitTrackedVerticalTableCell({
743
+ tr,
744
+ tablePosition,
745
+ table,
746
+ rectangle: split.rectangle,
747
+ revisionId,
748
+ author,
749
+ date
750
+ });
751
+ if (!nextTr) {
752
+ skipped.push({
753
+ id: item.operation.id,
754
+ reason: "unsupportedBlock"
755
+ });
756
+ continue;
757
+ }
758
+ tr = nextTr;
759
+ if (revisionId !== null) {
760
+ revisionSeed++;
761
+ appliedRevisionIds = [revisionId];
762
+ }
763
+ break;
764
+ }
765
+ case "deleteTableRow": {
766
+ const deletion = item.tableRowDeletion;
767
+ if (!deletion) {
768
+ skipped.push({
769
+ id: item.operation.id,
770
+ reason: "unsupportedBlock"
771
+ });
772
+ continue;
773
+ }
774
+ const result = applyTableRowDeletion({
775
+ tr,
776
+ deletion,
777
+ revision: structuralRevision
778
+ });
779
+ if (result.type === "unsupported") {
780
+ skipped.push({
781
+ id: item.operation.id,
782
+ reason: "unsupportedBlock"
783
+ });
784
+ continue;
785
+ }
786
+ tr = result.transaction;
787
+ if (result.revisionId !== null) {
788
+ revisionSeed++;
789
+ appliedRevisionIds = [result.revisionId];
790
+ }
791
+ break;
792
+ }
391
793
  case "deleteBlock":
392
794
  if (mode === "direct") {
393
795
  tr = tr.delete(item.blockFrom, item.blockTo);
@@ -398,7 +800,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
398
800
  tr = tr.addMark(item.from, item.to, deletionType.create({
399
801
  revisionId,
400
802
  author,
401
- date
803
+ date,
804
+ ...trackedRevisionExtras
402
805
  }));
403
806
  appliedRevisionIds = [revisionId];
404
807
  }
@@ -422,7 +825,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
422
825
  ...appliedRevisionIds !== void 0 && appliedRevisionIds[0] !== void 0 && {
423
826
  revisionId: appliedRevisionIds[0],
424
827
  revisionIds: appliedRevisionIds
425
- }
828
+ },
829
+ ...suggestionId !== null && { suggestionId }
426
830
  });
427
831
  }
428
832
  if (tr.docChanged) view.dispatch(tr);
@@ -452,7 +856,7 @@ const previewFolioAIEditOperations = (options) => {
452
856
  skipped: result.skipped
453
857
  };
454
858
  };
455
- const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark }) => {
859
+ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark, suggestionId = null, initials }) => {
456
860
  let nextTr = tr;
457
861
  const replacement = stripInlineEmphasisMarkers((() => {
458
862
  if (item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") return item.operation.replace;
@@ -470,15 +874,24 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
470
874
  }
471
875
  const insertionType = nextTr.doc.type.schema.marks["insertion"];
472
876
  const deletionType = nextTr.doc.type.schema.marks["deletion"];
877
+ const suggestionAttrs = suggestionId === null ? {} : {
878
+ provenance: "suggested",
879
+ suggestionId
880
+ };
881
+ const initialsAttr = initials ? { initials } : {};
473
882
  const delAttrs = {
474
883
  revisionId: revisionIdDelete,
475
884
  author,
476
- date
885
+ date,
886
+ ...initialsAttr,
887
+ ...suggestionAttrs
477
888
  };
478
889
  const insAttrs = {
479
890
  revisionId: revisionIdInsert,
480
891
  author,
481
- date
892
+ date,
893
+ ...initialsAttr,
894
+ ...suggestionAttrs
482
895
  };
483
896
  const cleanBlock = item.blockNode.content.size === item.blockNode.textContent.length ? buildCleanBlockText(item.blockNode, item.blockFrom) : null;
484
897
  let sourceText = null;
@@ -561,8 +974,7 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
561
974
  }
562
975
  return nextTr;
563
976
  };
564
- const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, doc }) => {
565
- const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
977
+ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId }) => {
566
978
  const anchor = snapshot.anchors[blockId];
567
979
  if (!anchor) return {
568
980
  type: "skip",
@@ -588,20 +1000,37 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
588
1000
  type: "skip",
589
1001
  reason: "changedBlock"
590
1002
  };
591
- const blockNode = live.node;
592
- const blockFrom = live.from;
593
- const blockTo = live.to;
594
- const cleanBlock = buildCleanBlockText(blockNode, blockFrom);
1003
+ const cleanBlock = buildCleanBlockText(live.node, live.from);
595
1004
  const currentText = cleanBlock.text;
596
1005
  const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText));
597
- if (operation.precondition !== void 0 && currentTextHash !== operation.precondition.blockTextHash) return {
598
- type: "skip",
599
- reason: "preconditionFailed"
600
- };
601
1006
  if (currentTextHash !== anchor.textHash) return {
602
1007
  type: "skip",
603
1008
  reason: "changedBlock"
604
1009
  };
1010
+ return {
1011
+ type: "resolved",
1012
+ blockNode: live.node,
1013
+ blockFrom: live.from,
1014
+ blockTo: live.to,
1015
+ cleanBlock,
1016
+ currentText,
1017
+ currentTextHash
1018
+ };
1019
+ };
1020
+ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, doc }) => {
1021
+ const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
1022
+ const primaryBlock = resolveStableBlock({
1023
+ snapshot,
1024
+ blockId,
1025
+ liveBlocks,
1026
+ liveBlocksByParaId
1027
+ });
1028
+ if (primaryBlock.type === "skip") return primaryBlock;
1029
+ const { blockNode, blockFrom, blockTo, cleanBlock, currentText, currentTextHash } = primaryBlock;
1030
+ if (operation.precondition !== void 0 && currentTextHash !== operation.precondition.blockTextHash) return {
1031
+ type: "skip",
1032
+ reason: "preconditionFailed"
1033
+ };
605
1034
  if (operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange") {
606
1035
  const { startOffset, endOffset, selectedTextHash } = operation.range;
607
1036
  const from = cleanBlock.offsets[startOffset];
@@ -676,6 +1105,207 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
676
1105
  }
677
1106
  };
678
1107
  }
1108
+ if (operation.type === "insertTableRow") {
1109
+ const insertion = findTableRowInsertion({
1110
+ doc,
1111
+ blockFrom,
1112
+ position: operation.position ?? "after"
1113
+ });
1114
+ if (!insertion || (operation.cellTexts?.length ?? 0) > insertion.cells.length) return {
1115
+ type: "skip",
1116
+ reason: "unsupportedBlock"
1117
+ };
1118
+ return {
1119
+ type: "resolved",
1120
+ operation: {
1121
+ operation,
1122
+ from: insertion.rowPosition,
1123
+ to: insertion.rowPosition,
1124
+ blockFrom,
1125
+ blockTo,
1126
+ blockNode,
1127
+ tableRowInsertion: insertion
1128
+ }
1129
+ };
1130
+ }
1131
+ if (operation.type === "deleteTableRow") {
1132
+ const target = findEnclosingTableRow(doc, blockFrom);
1133
+ if (!target) return {
1134
+ type: "skip",
1135
+ reason: "unsupportedBlock"
1136
+ };
1137
+ return {
1138
+ type: "resolved",
1139
+ operation: {
1140
+ operation,
1141
+ from: target.rowPosition,
1142
+ to: target.rowPosition,
1143
+ blockFrom,
1144
+ blockTo,
1145
+ blockNode,
1146
+ tableRowDeletion: {
1147
+ tableStart: target.tableStart,
1148
+ tablePosition: target.tablePosition,
1149
+ rowIndex: target.rowIndex,
1150
+ rowPosition: target.rowPosition
1151
+ }
1152
+ }
1153
+ };
1154
+ }
1155
+ if (operation.type === "insertTableColumn") {
1156
+ const insertion = findTableColumnInsertion({
1157
+ doc,
1158
+ blockFrom,
1159
+ position: operation.position ?? "after"
1160
+ });
1161
+ if (!insertion) return {
1162
+ type: "skip",
1163
+ reason: "unsupportedBlock"
1164
+ };
1165
+ const { boundaryPosition, ...tableColumnInsertion } = insertion;
1166
+ return {
1167
+ type: "resolved",
1168
+ operation: {
1169
+ operation,
1170
+ from: boundaryPosition,
1171
+ to: boundaryPosition,
1172
+ blockFrom,
1173
+ blockTo,
1174
+ blockNode,
1175
+ tableColumnInsertion
1176
+ }
1177
+ };
1178
+ }
1179
+ if (operation.type === "deleteTableColumn") {
1180
+ const target = findEnclosingTableCell(doc, blockFrom);
1181
+ if (!target) return {
1182
+ type: "skip",
1183
+ reason: "unsupportedBlock"
1184
+ };
1185
+ return {
1186
+ type: "resolved",
1187
+ operation: {
1188
+ operation,
1189
+ from: target.cellPosition,
1190
+ to: target.cellPosition,
1191
+ blockFrom,
1192
+ blockTo,
1193
+ blockNode,
1194
+ tableColumnDeletion: {
1195
+ tablePosition: target.tablePosition,
1196
+ columnIndex: target.leftColumnIndex
1197
+ }
1198
+ }
1199
+ };
1200
+ }
1201
+ if (operation.type === "mergeTableCells") {
1202
+ const startCell = findEnclosingTableCell(doc, blockFrom);
1203
+ if (!startCell) return {
1204
+ type: "skip",
1205
+ reason: "unsupportedBlock"
1206
+ };
1207
+ const table = doc.nodeAt(startCell.tablePosition);
1208
+ if (!table || table.type.spec["tableRole"] !== "table") return {
1209
+ type: "skip",
1210
+ reason: "unsupportedBlock"
1211
+ };
1212
+ const map = TableMap.get(table);
1213
+ let rectangle;
1214
+ let endCellPosition;
1215
+ let endCellEndPosition;
1216
+ if (operation.rowCount !== void 0) {
1217
+ rectangle = {
1218
+ left: startCell.leftColumnIndex,
1219
+ top: startCell.topRowIndex,
1220
+ right: startCell.rightColumnIndex,
1221
+ bottom: startCell.topRowIndex + operation.rowCount
1222
+ };
1223
+ if (rectangle.right - rectangle.left !== 1 || rectangle.bottom > map.height || operation.rowCount < 2) return {
1224
+ type: "skip",
1225
+ reason: "unsupportedBlock"
1226
+ };
1227
+ const endRelativePosition = map.map[(rectangle.bottom - 1) * map.width + rectangle.left];
1228
+ const endCell = endRelativePosition === void 0 ? null : table.nodeAt(endRelativePosition);
1229
+ if (!endCell || endRelativePosition === void 0) return {
1230
+ type: "skip",
1231
+ reason: "unsupportedBlock"
1232
+ };
1233
+ endCellPosition = startCell.tablePosition + 1 + endRelativePosition;
1234
+ endCellEndPosition = endCellPosition + endCell.nodeSize;
1235
+ } else {
1236
+ const endTarget = resolveStableBlock({
1237
+ snapshot,
1238
+ blockId: operation.endBlockId,
1239
+ liveBlocks,
1240
+ liveBlocksByParaId
1241
+ });
1242
+ if (endTarget.type === "skip") return endTarget;
1243
+ const endCell = findEnclosingTableCell(doc, endTarget.blockFrom);
1244
+ if (!endCell || startCell.tablePosition !== endCell.tablePosition) return {
1245
+ type: "skip",
1246
+ reason: "unsupportedBlock"
1247
+ };
1248
+ rectangle = {
1249
+ left: Math.min(startCell.leftColumnIndex, endCell.leftColumnIndex),
1250
+ top: Math.min(startCell.topRowIndex, endCell.topRowIndex),
1251
+ right: Math.max(startCell.rightColumnIndex, endCell.rightColumnIndex),
1252
+ bottom: Math.max(startCell.bottomRowIndex, endCell.bottomRowIndex)
1253
+ };
1254
+ endCellPosition = endCell.cellPosition;
1255
+ endCellEndPosition = endCell.cellEndPosition;
1256
+ }
1257
+ if (rectangle.right - rectangle.left === 1 && rectangle.bottom - rectangle.top === 1 || tableRectangleCutsMergedCell(map, rectangle)) return {
1258
+ type: "skip",
1259
+ reason: rectangle.right - rectangle.left === 1 && rectangle.bottom - rectangle.top === 1 ? "noopOperation" : "unsupportedBlock"
1260
+ };
1261
+ return {
1262
+ type: "resolved",
1263
+ operation: {
1264
+ operation,
1265
+ from: Math.min(startCell.cellPosition, endCellPosition),
1266
+ to: Math.max(startCell.cellEndPosition, endCellEndPosition),
1267
+ blockFrom,
1268
+ blockTo,
1269
+ blockNode,
1270
+ tableCellMerge: {
1271
+ tablePosition: startCell.tablePosition,
1272
+ rectangle
1273
+ }
1274
+ }
1275
+ };
1276
+ }
1277
+ if (operation.type === "splitTableCell") {
1278
+ const cell = findEnclosingTableCell(doc, blockFrom);
1279
+ if (!cell) return {
1280
+ type: "skip",
1281
+ reason: "unsupportedBlock"
1282
+ };
1283
+ const rectangle = {
1284
+ left: cell.leftColumnIndex,
1285
+ top: cell.topRowIndex,
1286
+ right: cell.rightColumnIndex,
1287
+ bottom: cell.bottomRowIndex
1288
+ };
1289
+ if (rectangle.right - rectangle.left === 1 && rectangle.bottom - rectangle.top === 1) return {
1290
+ type: "skip",
1291
+ reason: "noopOperation"
1292
+ };
1293
+ return {
1294
+ type: "resolved",
1295
+ operation: {
1296
+ operation,
1297
+ from: cell.cellPosition,
1298
+ to: cell.cellEndPosition,
1299
+ blockFrom,
1300
+ blockTo,
1301
+ blockNode,
1302
+ tableCellSplit: {
1303
+ tablePosition: cell.tablePosition,
1304
+ rectangle
1305
+ }
1306
+ }
1307
+ };
1308
+ }
679
1309
  if (operation.type === "deleteBlock" || operation.type === "replaceBlock") {
680
1310
  const range = getTextRangeFromCleanBlock(cleanBlock);
681
1311
  if (!range) {