@stll/folio-core 0.11.0 → 0.12.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 (194) hide show
  1. package/dist/ai-edits/apply.js +602 -50
  2. package/dist/ai-edits/block-identity.d.ts +4 -0
  3. package/dist/ai-edits/block-identity.js +9 -0
  4. package/dist/ai-edits/blockRange.d.ts +28 -1
  5. package/dist/ai-edits/blockRange.js +67 -1
  6. package/dist/ai-edits/headless.d.ts +4 -4
  7. package/dist/ai-edits/headless.js +9 -9
  8. package/dist/ai-edits/index.d.ts +2 -2
  9. package/dist/ai-edits/index.js +2 -2
  10. package/dist/ai-edits/read.d.ts +7 -7
  11. package/dist/ai-edits/read.js +110 -3
  12. package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
  13. package/dist/ai-edits/table-cell-mutations.js +237 -0
  14. package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
  15. package/dist/ai-edits/table-mutation-plan.js +90 -0
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +100 -0
  17. package/dist/ai-edits/table-row-column-mutations.js +300 -0
  18. package/dist/ai-edits/table-targets.d.ts +32 -0
  19. package/dist/ai-edits/table-targets.js +70 -0
  20. package/dist/ai-edits/types.d.ts +34 -0
  21. package/dist/compat/eigenpal.js +1 -1
  22. package/dist/content-controls/mutateContentControls.js +1 -1
  23. package/dist/controller/activeEditorStory.d.ts +36 -0
  24. package/dist/controller/activeEditorStory.js +30 -0
  25. package/dist/controller/contentControlWidgetController.d.ts +55 -0
  26. package/dist/controller/contentControlWidgetController.js +141 -0
  27. package/dist/controller/fontReadiness.js +2 -0
  28. package/dist/controller/headerFooterEditorManager.js +1 -1
  29. package/dist/controller/hiddenEditorApi.js +2 -2
  30. package/dist/controller/hiddenEditorManager.d.ts +12 -1
  31. package/dist/controller/hiddenEditorManager.js +32 -11
  32. package/dist/controller/layoutPipeline.d.ts +2 -0
  33. package/dist/controller/layoutPipeline.js +16 -1
  34. package/dist/controller/noteEditorManager.d.ts +36 -0
  35. package/dist/controller/noteEditorManager.js +210 -0
  36. package/dist/document-operations.d.ts +35 -3
  37. package/dist/document-operations.js +161 -3
  38. package/dist/docx/blockContentParser.js +2 -2
  39. package/dist/docx/commentParser.js +6 -1
  40. package/dist/docx/conformance.js +8 -1
  41. package/dist/docx/documentGrid.d.ts +8 -0
  42. package/dist/docx/documentGrid.js +8 -0
  43. package/dist/docx/drawingUtils.js +1 -1
  44. package/dist/docx/ensureParaIds.js +1 -1
  45. package/dist/docx/fieldParser.js +1 -1
  46. package/dist/docx/notePropertiesParser.js +1 -1
  47. package/dist/docx/numberingParser.js +1 -1
  48. package/dist/docx/paragraphParser.js +63 -58
  49. package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
  50. package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
  51. package/dist/docx/paragraphTraversal.js +1 -1
  52. package/dist/docx/parser.js +3 -3
  53. package/dist/docx/rezip.js +1 -1
  54. package/dist/docx/runParser.js +4 -2
  55. package/dist/docx/sdtProperties.js +1 -1
  56. package/dist/docx/sectionParser.js +1 -1
  57. package/dist/docx/selectiveSave.js +1 -1
  58. package/dist/docx/serializer/commentSerializer.js +1 -2
  59. package/dist/docx/serializer/documentSerializer.js +2 -2
  60. package/dist/docx/serializer/headerFooterSerializer.js +2 -2
  61. package/dist/docx/serializer/noteSerializer.js +2 -2
  62. package/dist/docx/serializer/paragraphSerializer.js +7 -1
  63. package/dist/docx/serializer/runSerializer.js +4 -2
  64. package/dist/docx/serializer/stylesSerializer.js +1 -1
  65. package/dist/docx/serializer/tableSerializer.d.ts +5 -4
  66. package/dist/docx/serializer/tableSerializer.js +17 -13
  67. package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
  68. package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
  69. package/dist/docx/server/boundedArchive.d.ts +14 -3
  70. package/dist/docx/server/boundedArchive.js +56 -11
  71. package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
  72. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
  73. package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
  74. package/dist/docx/server/inspectDocxPackage.js +217 -0
  75. package/dist/docx/server/validateDocxConformance.d.ts +41 -0
  76. package/dist/docx/server/validateDocxConformance.js +336 -0
  77. package/dist/docx/settingsParser.js +11 -1
  78. package/dist/docx/shapeParser.js +1 -1
  79. package/dist/docx/styleParser.js +4 -2
  80. package/dist/docx/tableParser.js +18 -7
  81. package/dist/docx/textBoxParser.d.ts +3 -3
  82. package/dist/docx/textBoxParser.js +16 -12
  83. package/dist/docx/xmlSafety.d.ts +5 -0
  84. package/dist/docx/xmlSafety.js +9 -0
  85. package/dist/fields/resolveFieldValues.js +1 -3
  86. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  87. package/dist/i18n/messages/catalogs.gen.js +17 -0
  88. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  89. package/dist/index.js +1 -1
  90. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  91. package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
  92. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  93. package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
  94. package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
  95. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  96. package/dist/layout-bridge/convert/toFlowBlocks.js +136 -20
  97. package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
  98. package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
  99. package/dist/layout-bridge/engine/clickToPosition.js +2 -2
  100. package/dist/layout-engine/index.d.ts +2 -2
  101. package/dist/layout-engine/index.js +43 -28
  102. package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
  103. package/dist/layout-engine/justificationCompatibility.js +8 -0
  104. package/dist/layout-engine/keep-together.js +11 -1
  105. package/dist/layout-engine/measure/cache.js +21 -5
  106. package/dist/layout-engine/measure/lineBreakProvider.js +53 -34
  107. package/dist/layout-engine/measure/measureBlocks.js +6 -3
  108. package/dist/layout-engine/measure/measureContainer.js +1 -1
  109. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  110. package/dist/layout-engine/measure/measureHelpers.js +3 -3
  111. package/dist/layout-engine/measure/measureParagraph.js +69 -25
  112. package/dist/layout-engine/measure/measureProvider.js +4 -3
  113. package/dist/layout-engine/measure/tableCellFlow.js +5 -2
  114. package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
  115. package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
  116. package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
  117. package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
  118. package/dist/layout-engine/paginator.js +5 -1
  119. package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
  120. package/dist/layout-engine/paragraphSpacing.js +107 -13
  121. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  122. package/dist/layout-engine/types.d.ts +26 -13
  123. package/dist/layout-painter/registry/modules/textBox.js +5 -1
  124. package/dist/layout-painter/renderPage.d.ts +5 -2
  125. package/dist/layout-painter/renderPage.js +56 -12
  126. package/dist/layout-painter/renderParagraph.js +22 -6
  127. package/dist/layout-painter/renderTable.d.ts +5 -1
  128. package/dist/layout-painter/renderTable.js +5 -2
  129. package/dist/layout-painter/renderTextBox.d.ts +2 -1
  130. package/dist/layout-painter/renderTextBox.js +25 -14
  131. package/dist/managers/DocumentLoaderManager.js +1 -1
  132. package/dist/model.js +1 -1
  133. package/dist/prosemirror/attrs/index.d.ts +4 -2
  134. package/dist/prosemirror/attrs/index.js +154 -18
  135. package/dist/prosemirror/commands/comments.d.ts +3 -3
  136. package/dist/prosemirror/commands/comments.js +248 -12
  137. package/dist/prosemirror/commands/contentControls.js +1 -1
  138. package/dist/prosemirror/commands/index.js +1 -1
  139. package/dist/prosemirror/commands/pageBreak.js +1 -1
  140. package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
  141. package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
  142. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
  143. package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
  144. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
  145. package/dist/prosemirror/conversion/fromProseDoc.js +286 -61
  146. package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
  147. package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
  148. package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
  149. package/dist/prosemirror/conversion/toProseDoc.js +347 -98
  150. package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
  151. package/dist/prosemirror/extensions/StarterKit.js +6 -1
  152. package/dist/prosemirror/extensions/core/ParagraphExtension.js +4 -1
  153. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
  154. package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
  155. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
  156. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
  157. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
  158. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
  159. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
  160. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
  161. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +24 -1
  162. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  163. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
  164. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
  165. package/dist/prosemirror/extensions/nodes/TableExtension.js +9 -3
  166. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
  167. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
  168. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
  169. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
  170. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +34 -8
  171. package/dist/prosemirror/plugins/contentControlWidgets.js +125 -91
  172. package/dist/prosemirror/plugins/selectionTracker.js +1 -0
  173. package/dist/prosemirror/plugins/suggestionMode.js +1 -1
  174. package/dist/prosemirror/schema/index.d.ts +2 -2
  175. package/dist/prosemirror/schema/marks.d.ts +5 -1
  176. package/dist/prosemirror/schema/nodes.d.ts +64 -7
  177. package/dist/prosemirror/selectionState.js +1 -0
  178. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  179. package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
  180. package/dist/prosemirror/tableCellMergeRevision.js +22 -0
  181. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
  182. package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
  183. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
  184. package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
  185. package/dist/prosemirror/validation.js +9 -1
  186. package/dist/server.d.ts +6 -2
  187. package/dist/server.js +5 -1
  188. package/dist/utils/fontFamilyMerge.js +9 -11
  189. package/dist/utils/fontLoader.js +3 -2
  190. package/dist/utils/fontResolver.js +22 -4
  191. package/dist/utils/fontWeights.d.ts +1 -1
  192. package/dist/utils/fontWeights.js +1 -1
  193. package/dist/utils/paragraphFormattingMerge.js +1 -0
  194. package/package.json +1 -1
@@ -1,27 +1,17 @@
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
- /**
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.
12
- */
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
- };
25
15
  const applyReplaceBlockStyleId = ({ item, tr }) => {
26
16
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return tr;
27
17
  const block = tr.doc.nodeAt(item.blockFrom);
@@ -31,6 +21,61 @@ const applyReplaceBlockStyleId = ({ item, tr }) => {
31
21
  styleId: item.operation.styleId
32
22
  });
33
23
  };
24
+ const applyInlineFormatting = ({ tr, schema, from, to, formatting }) => {
25
+ for (const [name, enabled] of Object.entries(formatting)) {
26
+ const markType = schema.marks[name];
27
+ if (!markType) continue;
28
+ if (enabled) {
29
+ tr.addMark(from, to, name === "underline" ? markType.create({ style: "single" }) : markType.create());
30
+ continue;
31
+ }
32
+ tr.removeMark(from, to, markType);
33
+ }
34
+ return tr;
35
+ };
36
+ const formattingWouldChange = (marks, formatting) => Object.entries(formatting).some(([name, enabled]) => {
37
+ const mark = marks.find((candidate) => candidate.type.name === name);
38
+ if (!enabled) return mark !== void 0;
39
+ if (name === "underline") return mark?.attrs["style"] !== "single";
40
+ return mark === void 0;
41
+ });
42
+ const applyTrackedInlineFormatting = ({ tr, schema, doc, from, to, formatting, revisionId, author, date }) => {
43
+ const propertyChangeType = schema.marks["runPropertyChange"];
44
+ if (!propertyChangeType) return tr;
45
+ const segments = [];
46
+ doc.nodesBetween(from, to, (node, pos) => {
47
+ if (!node.isText || !formattingWouldChange(node.marks, formatting)) return;
48
+ const segmentFrom = Math.max(from, pos);
49
+ const segmentTo = Math.min(to, pos + node.nodeSize);
50
+ const previousFormatting = marksToTextFormatting(node.marks);
51
+ const existingMark = node.marks.find((mark) => mark.type === propertyChangeType);
52
+ const existingChanges = existingMark ? expectRunPropertyChangeMarkAttrs(existingMark).changes : [];
53
+ const change = {
54
+ type: "runPropertyChange",
55
+ info: {
56
+ id: revisionId,
57
+ author,
58
+ date
59
+ },
60
+ ...Object.keys(previousFormatting).length > 0 ? { previousFormatting } : {}
61
+ };
62
+ segments.push({
63
+ from: segmentFrom,
64
+ to: segmentTo,
65
+ changes: [...existingChanges, change]
66
+ });
67
+ });
68
+ if (segments.length === 0) return tr;
69
+ applyInlineFormatting({
70
+ tr,
71
+ schema,
72
+ from,
73
+ to,
74
+ formatting
75
+ });
76
+ for (const segment of segments) tr.addMark(segment.from, segment.to, propertyChangeType.create({ changes: segment.changes }));
77
+ return tr;
78
+ };
34
79
  /**
35
80
  * Module-scoped monotonic counter for tracked-change revision ids.
36
81
  * Seeded once from `Date.now()` so ids are roughly time-ordered for
@@ -93,13 +138,21 @@ const collectLiveBlocksByParaId = (doc) => {
93
138
  });
94
139
  return byParaId;
95
140
  };
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)
141
+ const getTableMutationPlanTarget = (item) => {
142
+ if (item.tableCellMerge) return {
143
+ type: "mergeCells",
144
+ ...item.tableCellMerge
145
+ };
146
+ if (item.tableCellSplit) return {
147
+ type: "splitCell",
148
+ ...item.tableCellSplit
149
+ };
150
+ const tablePosition = item.tableRowInsertion?.tableStart !== void 0 ? item.tableRowInsertion.tableStart - 1 : item.tableRowDeletion?.tablePosition ?? item.tableColumnInsertion?.tablePosition ?? item.tableColumnDeletion?.tablePosition;
151
+ if (tablePosition !== void 0) return {
152
+ type: "tableStructure",
153
+ tablePosition
101
154
  };
102
- return null;
155
+ return { type: "none" };
103
156
  };
104
157
  /**
105
158
  * The snapshot recorded an `hashOccurrenceCount` per anchor but
@@ -200,6 +253,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
200
253
  const insertionType = view.state.schema.marks["insertion"];
201
254
  const deletionType = view.state.schema.marks["deletion"];
202
255
  const commentType = view.state.schema.marks["comment"];
256
+ const claimedTableRows = /* @__PURE__ */ new Set();
257
+ const claimedTableColumns = /* @__PURE__ */ new Set();
203
258
  if (mode === "tracked-changes" && (!insertionType || !deletionType)) return {
204
259
  applied,
205
260
  skipped: operations.map((operation) => ({
@@ -210,13 +265,6 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
210
265
  const liveBlocks = collectLiveBlocksByHash(view.state.doc);
211
266
  const liveBlocksByParaId = collectLiveBlocksByParaId(view.state.doc);
212
267
  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
268
  const commentText = getOperationCommentText(operation);
221
269
  if (commentText !== void 0 && (!commentType || createCommentId === void 0)) {
222
270
  skipped.push({
@@ -239,6 +287,30 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
239
287
  });
240
288
  continue;
241
289
  }
290
+ const deletion = resolution.operation.tableRowDeletion;
291
+ if (deletion) {
292
+ const rowKey = `${deletion.tablePosition}:${deletion.rowIndex}`;
293
+ if (claimedTableRows.has(rowKey)) {
294
+ skipped.push({
295
+ id: operation.id,
296
+ reason: "noopOperation"
297
+ });
298
+ continue;
299
+ }
300
+ claimedTableRows.add(rowKey);
301
+ }
302
+ const columnDeletion = resolution.operation.tableColumnDeletion;
303
+ if (columnDeletion) {
304
+ const columnKey = getTableColumnCoordinateKey(columnDeletion);
305
+ if (claimedTableColumns.has(columnKey)) {
306
+ skipped.push({
307
+ id: operation.id,
308
+ reason: "noopOperation"
309
+ });
310
+ continue;
311
+ }
312
+ claimedTableColumns.add(columnKey);
313
+ }
242
314
  const commentId = commentText !== void 0 ? createCommentId?.(commentText) : void 0;
243
315
  resolved.push({
244
316
  ...resolution.operation,
@@ -246,14 +318,43 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
246
318
  ...commentId !== void 0 && { commentId }
247
319
  });
248
320
  }
249
- if (resolved.length === 0) return {
321
+ const tablePlan = planTableMutations(resolved.map((item) => ({
322
+ item,
323
+ operationId: item.operation.id,
324
+ target: getTableMutationPlanTarget(item)
325
+ })));
326
+ skipped.push(...tablePlan.skipped);
327
+ const executableResolved = tablePlan.executable;
328
+ if (executableResolved.length === 0) return {
250
329
  applied,
251
330
  skipped
252
331
  };
253
332
  let tr = view.state.tr;
254
- let revisionSeed = revisionIdSeed ?? nextRevisionSeed(resolved.length);
333
+ let revisionSeed = revisionIdSeed ?? nextRevisionSeed(executableResolved.length);
255
334
  const date = (/* @__PURE__ */ new Date()).toISOString();
256
- for (const item of resolved.toSorted((left, right) => {
335
+ const insertedColumnCounts = /* @__PURE__ */ new Map();
336
+ for (const item of executableResolved.toSorted((left, right) => {
337
+ const leftCellShape = left.tableCellMerge ?? left.tableCellSplit;
338
+ const rightCellShape = right.tableCellMerge ?? right.tableCellSplit;
339
+ if (!leftCellShape && rightCellShape) return -1;
340
+ if (leftCellShape && !rightCellShape) return 1;
341
+ if (leftCellShape && rightCellShape) {
342
+ if (leftCellShape.tablePosition !== rightCellShape.tablePosition) return rightCellShape.tablePosition - leftCellShape.tablePosition;
343
+ if (leftCellShape.rectangle.bottom !== rightCellShape.rectangle.bottom) return rightCellShape.rectangle.bottom - leftCellShape.rectangle.bottom;
344
+ if (leftCellShape.rectangle.right !== rightCellShape.rectangle.right) return rightCellShape.rectangle.right - leftCellShape.rectangle.right;
345
+ return right.originalIndex - left.originalIndex;
346
+ }
347
+ const leftColumn = left.tableColumnInsertion ?? left.tableColumnDeletion;
348
+ const rightColumn = right.tableColumnInsertion ?? right.tableColumnDeletion;
349
+ if (!leftColumn && rightColumn) return -1;
350
+ if (leftColumn && !rightColumn) return 1;
351
+ if (leftColumn && rightColumn) {
352
+ if (leftColumn.tablePosition !== rightColumn.tablePosition) return rightColumn.tablePosition - leftColumn.tablePosition;
353
+ if (leftColumn.columnIndex !== rightColumn.columnIndex) return rightColumn.columnIndex - leftColumn.columnIndex;
354
+ const leftIsInsertion = left.tableColumnInsertion !== void 0;
355
+ if (leftIsInsertion !== (right.tableColumnInsertion !== void 0)) return leftIsInsertion ? -1 : 1;
356
+ return right.originalIndex - left.originalIndex;
357
+ }
257
358
  if (left.from !== right.from) return right.from - left.from;
258
359
  return right.originalIndex - left.originalIndex;
259
360
  })) {
@@ -282,12 +383,29 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
282
383
  if (commentMark) tr = tr.addMark(item.from, item.to, commentMark);
283
384
  break;
284
385
  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);
386
+ if (mode === "tracked-changes") {
387
+ const revisionId = revisionSeed++;
388
+ tr = applyTrackedInlineFormatting({
389
+ tr,
390
+ schema: view.state.schema,
391
+ doc: tr.doc,
392
+ from: item.from,
393
+ to: item.to,
394
+ formatting: item.operation.formatting,
395
+ revisionId,
396
+ author,
397
+ date
398
+ });
399
+ appliedRevisionIds = [revisionId];
400
+ break;
290
401
  }
402
+ tr = applyInlineFormatting({
403
+ tr,
404
+ schema: view.state.schema,
405
+ from: item.from,
406
+ to: item.to,
407
+ formatting: item.operation.formatting
408
+ });
291
409
  break;
292
410
  case "replaceBlock": {
293
411
  const revisionIdDelete = revisionSeed++;
@@ -349,7 +467,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
349
467
  }
350
468
  if (commentMark) marks.push(commentMark);
351
469
  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) };
470
+ const attrs = { ...item.operation.inheritFormatting === false ? {} : stripBlockIdentityAttrs(item.blockNode.attrs) };
353
471
  if (item.operation.pageBreakBefore === true) attrs["pageBreakBefore"] = true;
354
472
  if (item.operation.styleId !== void 0) {
355
473
  attrs["styleId"] = item.operation.styleId;
@@ -388,6 +506,223 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
388
506
  tr = tr.insert(item.from, node);
389
507
  break;
390
508
  }
509
+ case "insertTableRow": {
510
+ const insertion = item.tableRowInsertion;
511
+ if (!insertion) {
512
+ skipped.push({
513
+ id: item.operation.id,
514
+ reason: "unsupportedBlock"
515
+ });
516
+ continue;
517
+ }
518
+ const revision = mode === "tracked-changes" ? {
519
+ revisionId: revisionSeed,
520
+ author,
521
+ date
522
+ } : null;
523
+ const result = applyTableRowInsertion({
524
+ tr,
525
+ insertion,
526
+ cellTexts: item.operation.cellTexts,
527
+ revision
528
+ });
529
+ if (result.type === "unsupported") {
530
+ skipped.push({
531
+ id: item.operation.id,
532
+ reason: "unsupportedBlock"
533
+ });
534
+ continue;
535
+ }
536
+ tr = result.transaction;
537
+ if (result.revisionId !== null) {
538
+ revisionSeed++;
539
+ appliedRevisionIds = [result.revisionId];
540
+ }
541
+ break;
542
+ }
543
+ case "insertTableColumn": {
544
+ const insertion = item.tableColumnInsertion;
545
+ if (!insertion) {
546
+ skipped.push({
547
+ id: item.operation.id,
548
+ reason: "unsupportedBlock"
549
+ });
550
+ continue;
551
+ }
552
+ const revision = mode === "tracked-changes" ? {
553
+ revisionId: revisionSeed,
554
+ author,
555
+ date
556
+ } : null;
557
+ const result = applyTableColumnInsertion({
558
+ tr,
559
+ insertion,
560
+ cellTexts: item.operation.cellTexts,
561
+ revision
562
+ });
563
+ if (result.type === "unsupported") {
564
+ skipped.push({
565
+ id: item.operation.id,
566
+ reason: "unsupportedBlock"
567
+ });
568
+ continue;
569
+ }
570
+ tr = result.transaction;
571
+ if (result.revisionId !== null) {
572
+ revisionSeed++;
573
+ appliedRevisionIds = [result.revisionId];
574
+ }
575
+ const columnKey = getTableColumnCoordinateKey(insertion);
576
+ insertedColumnCounts.set(columnKey, (insertedColumnCounts.get(columnKey) ?? 0) + 1);
577
+ break;
578
+ }
579
+ case "deleteTableColumn": {
580
+ const deletion = item.tableColumnDeletion;
581
+ if (!deletion) {
582
+ skipped.push({
583
+ id: item.operation.id,
584
+ reason: "unsupportedBlock"
585
+ });
586
+ continue;
587
+ }
588
+ const columnKey = getTableColumnCoordinateKey(deletion);
589
+ const revision = mode === "tracked-changes" ? {
590
+ revisionId: revisionSeed,
591
+ author,
592
+ date
593
+ } : null;
594
+ const result = applyTableColumnDeletion({
595
+ tr,
596
+ deletion,
597
+ insertedColumnCount: insertedColumnCounts.get(columnKey) ?? 0,
598
+ revision
599
+ });
600
+ if (result.type === "unsupported") {
601
+ skipped.push({
602
+ id: item.operation.id,
603
+ reason: "unsupportedBlock"
604
+ });
605
+ continue;
606
+ }
607
+ tr = result.transaction;
608
+ if (result.revisionId !== null) {
609
+ revisionSeed++;
610
+ appliedRevisionIds = [result.revisionId];
611
+ }
612
+ break;
613
+ }
614
+ case "mergeTableCells": {
615
+ const merge = item.tableCellMerge;
616
+ const tablePosition = merge ? tr.mapping.map(merge.tablePosition, 1) : null;
617
+ const table = tablePosition === null ? null : tr.doc.nodeAt(tablePosition);
618
+ if (!merge || tablePosition === null || !table || table.type.spec["tableRole"] !== "table") {
619
+ skipped.push({
620
+ id: item.operation.id,
621
+ reason: "unsupportedBlock"
622
+ });
623
+ continue;
624
+ }
625
+ const revisionId = mode === "tracked-changes" ? revisionSeed : null;
626
+ const nextTr = revisionId === null ? mergeTableRectangle({
627
+ tr,
628
+ tablePosition,
629
+ table,
630
+ rectangle: merge.rectangle
631
+ }) : mergeTrackedVerticalTableCells({
632
+ tr,
633
+ tablePosition,
634
+ table,
635
+ rectangle: merge.rectangle,
636
+ revisionId,
637
+ author,
638
+ date
639
+ });
640
+ if (!nextTr) {
641
+ skipped.push({
642
+ id: item.operation.id,
643
+ reason: "unsupportedBlock"
644
+ });
645
+ continue;
646
+ }
647
+ tr = nextTr;
648
+ if (revisionId !== null) {
649
+ revisionSeed++;
650
+ appliedRevisionIds = [revisionId];
651
+ }
652
+ break;
653
+ }
654
+ case "splitTableCell": {
655
+ const split = item.tableCellSplit;
656
+ const tablePosition = split ? tr.mapping.map(split.tablePosition, 1) : null;
657
+ const table = tablePosition === null ? null : tr.doc.nodeAt(tablePosition);
658
+ if (!split || tablePosition === null || !table || table.type.spec["tableRole"] !== "table") {
659
+ skipped.push({
660
+ id: item.operation.id,
661
+ reason: "unsupportedBlock"
662
+ });
663
+ continue;
664
+ }
665
+ const revisionId = mode === "tracked-changes" ? revisionSeed : null;
666
+ const nextTr = revisionId === null ? splitTableRectangle({
667
+ tr,
668
+ tablePosition,
669
+ table,
670
+ rectangle: split.rectangle
671
+ }) : splitTrackedVerticalTableCell({
672
+ tr,
673
+ tablePosition,
674
+ table,
675
+ rectangle: split.rectangle,
676
+ revisionId,
677
+ author,
678
+ date
679
+ });
680
+ if (!nextTr) {
681
+ skipped.push({
682
+ id: item.operation.id,
683
+ reason: "unsupportedBlock"
684
+ });
685
+ continue;
686
+ }
687
+ tr = nextTr;
688
+ if (revisionId !== null) {
689
+ revisionSeed++;
690
+ appliedRevisionIds = [revisionId];
691
+ }
692
+ break;
693
+ }
694
+ case "deleteTableRow": {
695
+ const deletion = item.tableRowDeletion;
696
+ if (!deletion) {
697
+ skipped.push({
698
+ id: item.operation.id,
699
+ reason: "unsupportedBlock"
700
+ });
701
+ continue;
702
+ }
703
+ const result = applyTableRowDeletion({
704
+ tr,
705
+ deletion,
706
+ revision: mode === "tracked-changes" ? {
707
+ revisionId: revisionSeed,
708
+ author,
709
+ date
710
+ } : null
711
+ });
712
+ if (result.type === "unsupported") {
713
+ skipped.push({
714
+ id: item.operation.id,
715
+ reason: "unsupportedBlock"
716
+ });
717
+ continue;
718
+ }
719
+ tr = result.transaction;
720
+ if (result.revisionId !== null) {
721
+ revisionSeed++;
722
+ appliedRevisionIds = [result.revisionId];
723
+ }
724
+ break;
725
+ }
391
726
  case "deleteBlock":
392
727
  if (mode === "direct") {
393
728
  tr = tr.delete(item.blockFrom, item.blockTo);
@@ -561,8 +896,7 @@ const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete,
561
896
  }
562
897
  return nextTr;
563
898
  };
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;
899
+ const resolveStableBlock = ({ snapshot, blockId, liveBlocks, liveBlocksByParaId }) => {
566
900
  const anchor = snapshot.anchors[blockId];
567
901
  if (!anchor) return {
568
902
  type: "skip",
@@ -588,20 +922,37 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
588
922
  type: "skip",
589
923
  reason: "changedBlock"
590
924
  };
591
- const blockNode = live.node;
592
- const blockFrom = live.from;
593
- const blockTo = live.to;
594
- const cleanBlock = buildCleanBlockText(blockNode, blockFrom);
925
+ const cleanBlock = buildCleanBlockText(live.node, live.from);
595
926
  const currentText = cleanBlock.text;
596
927
  const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText));
597
- if (operation.precondition !== void 0 && currentTextHash !== operation.precondition.blockTextHash) return {
598
- type: "skip",
599
- reason: "preconditionFailed"
600
- };
601
928
  if (currentTextHash !== anchor.textHash) return {
602
929
  type: "skip",
603
930
  reason: "changedBlock"
604
931
  };
932
+ return {
933
+ type: "resolved",
934
+ blockNode: live.node,
935
+ blockFrom: live.from,
936
+ blockTo: live.to,
937
+ cleanBlock,
938
+ currentText,
939
+ currentTextHash
940
+ };
941
+ };
942
+ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, doc }) => {
943
+ const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
944
+ const primaryBlock = resolveStableBlock({
945
+ snapshot,
946
+ blockId,
947
+ liveBlocks,
948
+ liveBlocksByParaId
949
+ });
950
+ if (primaryBlock.type === "skip") return primaryBlock;
951
+ const { blockNode, blockFrom, blockTo, cleanBlock, currentText, currentTextHash } = primaryBlock;
952
+ if (operation.precondition !== void 0 && currentTextHash !== operation.precondition.blockTextHash) return {
953
+ type: "skip",
954
+ reason: "preconditionFailed"
955
+ };
605
956
  if (operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange") {
606
957
  const { startOffset, endOffset, selectedTextHash } = operation.range;
607
958
  const from = cleanBlock.offsets[startOffset];
@@ -676,6 +1027,207 @@ const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId,
676
1027
  }
677
1028
  };
678
1029
  }
1030
+ if (operation.type === "insertTableRow") {
1031
+ const insertion = findTableRowInsertion({
1032
+ doc,
1033
+ blockFrom,
1034
+ position: operation.position ?? "after"
1035
+ });
1036
+ if (!insertion || (operation.cellTexts?.length ?? 0) > insertion.cells.length) return {
1037
+ type: "skip",
1038
+ reason: "unsupportedBlock"
1039
+ };
1040
+ return {
1041
+ type: "resolved",
1042
+ operation: {
1043
+ operation,
1044
+ from: insertion.rowPosition,
1045
+ to: insertion.rowPosition,
1046
+ blockFrom,
1047
+ blockTo,
1048
+ blockNode,
1049
+ tableRowInsertion: insertion
1050
+ }
1051
+ };
1052
+ }
1053
+ if (operation.type === "deleteTableRow") {
1054
+ const target = findEnclosingTableRow(doc, blockFrom);
1055
+ if (!target) return {
1056
+ type: "skip",
1057
+ reason: "unsupportedBlock"
1058
+ };
1059
+ return {
1060
+ type: "resolved",
1061
+ operation: {
1062
+ operation,
1063
+ from: target.rowPosition,
1064
+ to: target.rowPosition,
1065
+ blockFrom,
1066
+ blockTo,
1067
+ blockNode,
1068
+ tableRowDeletion: {
1069
+ tableStart: target.tableStart,
1070
+ tablePosition: target.tablePosition,
1071
+ rowIndex: target.rowIndex,
1072
+ rowPosition: target.rowPosition
1073
+ }
1074
+ }
1075
+ };
1076
+ }
1077
+ if (operation.type === "insertTableColumn") {
1078
+ const insertion = findTableColumnInsertion({
1079
+ doc,
1080
+ blockFrom,
1081
+ position: operation.position ?? "after"
1082
+ });
1083
+ if (!insertion) return {
1084
+ type: "skip",
1085
+ reason: "unsupportedBlock"
1086
+ };
1087
+ const { boundaryPosition, ...tableColumnInsertion } = insertion;
1088
+ return {
1089
+ type: "resolved",
1090
+ operation: {
1091
+ operation,
1092
+ from: boundaryPosition,
1093
+ to: boundaryPosition,
1094
+ blockFrom,
1095
+ blockTo,
1096
+ blockNode,
1097
+ tableColumnInsertion
1098
+ }
1099
+ };
1100
+ }
1101
+ if (operation.type === "deleteTableColumn") {
1102
+ const target = findEnclosingTableCell(doc, blockFrom);
1103
+ if (!target) return {
1104
+ type: "skip",
1105
+ reason: "unsupportedBlock"
1106
+ };
1107
+ return {
1108
+ type: "resolved",
1109
+ operation: {
1110
+ operation,
1111
+ from: target.cellPosition,
1112
+ to: target.cellPosition,
1113
+ blockFrom,
1114
+ blockTo,
1115
+ blockNode,
1116
+ tableColumnDeletion: {
1117
+ tablePosition: target.tablePosition,
1118
+ columnIndex: target.leftColumnIndex
1119
+ }
1120
+ }
1121
+ };
1122
+ }
1123
+ if (operation.type === "mergeTableCells") {
1124
+ const startCell = findEnclosingTableCell(doc, blockFrom);
1125
+ if (!startCell) return {
1126
+ type: "skip",
1127
+ reason: "unsupportedBlock"
1128
+ };
1129
+ const table = doc.nodeAt(startCell.tablePosition);
1130
+ if (!table || table.type.spec["tableRole"] !== "table") return {
1131
+ type: "skip",
1132
+ reason: "unsupportedBlock"
1133
+ };
1134
+ const map = TableMap.get(table);
1135
+ let rectangle;
1136
+ let endCellPosition;
1137
+ let endCellEndPosition;
1138
+ if (operation.rowCount !== void 0) {
1139
+ rectangle = {
1140
+ left: startCell.leftColumnIndex,
1141
+ top: startCell.topRowIndex,
1142
+ right: startCell.rightColumnIndex,
1143
+ bottom: startCell.topRowIndex + operation.rowCount
1144
+ };
1145
+ if (rectangle.right - rectangle.left !== 1 || rectangle.bottom > map.height || operation.rowCount < 2) return {
1146
+ type: "skip",
1147
+ reason: "unsupportedBlock"
1148
+ };
1149
+ const endRelativePosition = map.map[(rectangle.bottom - 1) * map.width + rectangle.left];
1150
+ const endCell = endRelativePosition === void 0 ? null : table.nodeAt(endRelativePosition);
1151
+ if (!endCell || endRelativePosition === void 0) return {
1152
+ type: "skip",
1153
+ reason: "unsupportedBlock"
1154
+ };
1155
+ endCellPosition = startCell.tablePosition + 1 + endRelativePosition;
1156
+ endCellEndPosition = endCellPosition + endCell.nodeSize;
1157
+ } else {
1158
+ const endTarget = resolveStableBlock({
1159
+ snapshot,
1160
+ blockId: operation.endBlockId,
1161
+ liveBlocks,
1162
+ liveBlocksByParaId
1163
+ });
1164
+ if (endTarget.type === "skip") return endTarget;
1165
+ const endCell = findEnclosingTableCell(doc, endTarget.blockFrom);
1166
+ if (!endCell || startCell.tablePosition !== endCell.tablePosition) return {
1167
+ type: "skip",
1168
+ reason: "unsupportedBlock"
1169
+ };
1170
+ rectangle = {
1171
+ left: Math.min(startCell.leftColumnIndex, endCell.leftColumnIndex),
1172
+ top: Math.min(startCell.topRowIndex, endCell.topRowIndex),
1173
+ right: Math.max(startCell.rightColumnIndex, endCell.rightColumnIndex),
1174
+ bottom: Math.max(startCell.bottomRowIndex, endCell.bottomRowIndex)
1175
+ };
1176
+ endCellPosition = endCell.cellPosition;
1177
+ endCellEndPosition = endCell.cellEndPosition;
1178
+ }
1179
+ if (rectangle.right - rectangle.left === 1 && rectangle.bottom - rectangle.top === 1 || tableRectangleCutsMergedCell(map, rectangle)) return {
1180
+ type: "skip",
1181
+ reason: rectangle.right - rectangle.left === 1 && rectangle.bottom - rectangle.top === 1 ? "noopOperation" : "unsupportedBlock"
1182
+ };
1183
+ return {
1184
+ type: "resolved",
1185
+ operation: {
1186
+ operation,
1187
+ from: Math.min(startCell.cellPosition, endCellPosition),
1188
+ to: Math.max(startCell.cellEndPosition, endCellEndPosition),
1189
+ blockFrom,
1190
+ blockTo,
1191
+ blockNode,
1192
+ tableCellMerge: {
1193
+ tablePosition: startCell.tablePosition,
1194
+ rectangle
1195
+ }
1196
+ }
1197
+ };
1198
+ }
1199
+ if (operation.type === "splitTableCell") {
1200
+ const cell = findEnclosingTableCell(doc, blockFrom);
1201
+ if (!cell) return {
1202
+ type: "skip",
1203
+ reason: "unsupportedBlock"
1204
+ };
1205
+ const rectangle = {
1206
+ left: cell.leftColumnIndex,
1207
+ top: cell.topRowIndex,
1208
+ right: cell.rightColumnIndex,
1209
+ bottom: cell.bottomRowIndex
1210
+ };
1211
+ if (rectangle.right - rectangle.left === 1 && rectangle.bottom - rectangle.top === 1) return {
1212
+ type: "skip",
1213
+ reason: "noopOperation"
1214
+ };
1215
+ return {
1216
+ type: "resolved",
1217
+ operation: {
1218
+ operation,
1219
+ from: cell.cellPosition,
1220
+ to: cell.cellEndPosition,
1221
+ blockFrom,
1222
+ blockTo,
1223
+ blockNode,
1224
+ tableCellSplit: {
1225
+ tablePosition: cell.tablePosition,
1226
+ rectangle
1227
+ }
1228
+ }
1229
+ };
1230
+ }
679
1231
  if (operation.type === "deleteBlock" || operation.type === "replaceBlock") {
680
1232
  const range = getTextRangeFromCleanBlock(cleanBlock);
681
1233
  if (!range) {