@stll/folio-core 0.33.1 → 0.34.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 (121) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +271 -48
  3. package/dist/ai-edits/headless.d.ts +47 -3
  4. package/dist/ai-edits/headless.js +53 -3
  5. package/dist/ai-edits/index.d.ts +1 -1
  6. package/dist/ai-edits/snapshot.d.ts +19 -1
  7. package/dist/ai-edits/snapshot.js +49 -5
  8. package/dist/ai-edits/table-geometry.d.ts +56 -0
  9. package/dist/ai-edits/table-geometry.js +217 -0
  10. package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
  11. package/dist/ai-edits/table-row-column-mutations.js +12 -4
  12. package/dist/ai-edits/table-template.d.ts +60 -0
  13. package/dist/ai-edits/table-template.js +176 -0
  14. package/dist/ai-edits/types.d.ts +13 -6
  15. package/dist/compare/__fixtures__/body-sequence.d.ts +61 -2
  16. package/dist/compare/__fixtures__/body-sequence.js +94 -6
  17. package/dist/compare/compare.d.ts +14 -7
  18. package/dist/compare/compare.js +136 -38
  19. package/dist/compare/formatting.d.ts +1 -1
  20. package/dist/compare/formatting.js +38 -9
  21. package/dist/compare/plan.d.ts +31 -1
  22. package/dist/compare/plan.js +233 -66
  23. package/dist/compare/types.d.ts +9 -7
  24. package/dist/compare/types.js +7 -5
  25. package/dist/compare/verification.d.ts +31 -12
  26. package/dist/compare/verification.js +106 -18
  27. package/dist/compat/eigenpal.d.ts +4 -4
  28. package/dist/compat/eigenpal.js +2 -2
  29. package/dist/controller/headerFooterEditorManager.js +11 -9
  30. package/dist/controller/layoutPipeline.js +24 -3
  31. package/dist/display-list/build/watermarkPrimitives.js +15 -3
  32. package/dist/display-list/primitives.d.ts +1 -1
  33. package/dist/document-operations.d.ts +10 -1
  34. package/dist/document-operations.js +33 -5
  35. package/dist/docx/appVersionNormalization.d.ts +49 -0
  36. package/dist/docx/appVersionNormalization.js +74 -0
  37. package/dist/docx/blockContentParser.js +3 -2
  38. package/dist/docx/drawingUtils.js +4 -3
  39. package/dist/docx/headerFooterParser.js +8 -14
  40. package/dist/docx/paragraphParser.js +48 -2
  41. package/dist/docx/rezip.d.ts +10 -4
  42. package/dist/docx/rezip.js +54 -18
  43. package/dist/docx/runParser.js +9 -8
  44. package/dist/docx/sdtProperties.js +4 -3
  45. package/dist/docx/selectiveSave.js +7 -6
  46. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  47. package/dist/docx/serializer/commentSerializer.js +43 -19
  48. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  49. package/dist/docx/serializer/documentSerializer.js +42 -78
  50. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  51. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  52. package/dist/docx/serializer/headerFooterSerializer.js +55 -31
  53. package/dist/docx/serializer/noteSerializer.js +34 -30
  54. package/dist/docx/serializer/numberingSerializer.js +8 -4
  55. package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
  56. package/dist/docx/serializer/paragraphSerializer.js +22 -12
  57. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  58. package/dist/docx/serializer/partNamespaces.js +324 -0
  59. package/dist/docx/serializer/runSerializer.js +1 -1
  60. package/dist/docx/serializer/settingsSerializer.js +8 -2
  61. package/dist/docx/serializer/stylesSerializer.js +8 -5
  62. package/dist/docx/serializer/tableSerializer.js +83 -18
  63. package/dist/docx/serializer/themeSerializer.js +10 -2
  64. package/dist/docx/server/build.d.ts +1 -1
  65. package/dist/docx/settingsParser.js +3 -0
  66. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  67. package/dist/docx/strictValueEncodings.gen.js +275 -0
  68. package/dist/docx/tableParser.d.ts +0 -6
  69. package/dist/docx/tableParser.js +52 -7
  70. package/dist/docx/transitionalSpelling.d.ts +23 -0
  71. package/dist/docx/transitionalSpelling.js +36 -0
  72. package/dist/docx/universalMeasure.d.ts +20 -0
  73. package/dist/docx/universalMeasure.js +32 -0
  74. package/dist/docx/verbatimCapture.d.ts +20 -0
  75. package/dist/docx/verbatimCapture.js +131 -0
  76. package/dist/docx/vmlImageParser.js +4 -3
  77. package/dist/docx/watermarkParser.d.ts +2 -4
  78. package/dist/docx/watermarkParser.js +8 -9
  79. package/dist/docx/xmlParser.d.ts +23 -2
  80. package/dist/docx/xmlParser.js +43 -11
  81. package/dist/headless-layout.js +14 -2
  82. package/dist/index.d.ts +4 -4
  83. package/dist/index.js +2 -2
  84. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
  85. package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
  86. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
  87. package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
  88. package/dist/layout-engine/index.js +11 -4
  89. package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
  90. package/dist/layout-engine/justifiedLineFit.js +4 -4
  91. package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
  92. package/dist/layout-engine/measure/measureBlocks.js +1 -1
  93. package/dist/layout-engine/measure/measureParagraph.js +29 -29
  94. package/dist/layout-painter/renderPage.js +6 -1
  95. package/dist/layout-painter/renderParagraph.js +18 -8
  96. package/dist/layout-painter/renderWatermark.js +11 -4
  97. package/dist/model.d.ts +3 -3
  98. package/dist/model.js +2 -2
  99. package/dist/prosemirror/attrs/index.js +11 -0
  100. package/dist/prosemirror/commands/comments.js +24 -3
  101. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  102. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  103. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  104. package/dist/prosemirror/conversion/fromProseDoc.js +47 -9
  105. package/dist/prosemirror/conversion/toProseDoc.js +61 -14
  106. package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
  107. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  108. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
  109. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  110. package/dist/prosemirror/schema/marks.d.ts +3 -9
  111. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  112. package/dist/server.d.ts +1 -1
  113. package/dist/types/block-id.d.ts +12 -1
  114. package/dist/types/block-id.js +17 -1
  115. package/dist/utils/canonicalJson.d.ts +14 -0
  116. package/dist/utils/canonicalJson.js +19 -0
  117. package/dist/utils/fontResolver.js +51 -0
  118. package/dist/utils/formatToStyle.d.ts +1 -1
  119. package/dist/utils/formatToStyle.js +41 -1
  120. package/dist/watermark/index.js +7 -0
  121. package/package.json +2 -2
@@ -5,8 +5,8 @@ import { DocPositionRange, clampRangeToDocSize, resolveFolioAIBlockRange, resolv
5
5
  import { buildAnnotatedBlockText } from "./clean-text.js";
6
6
  import { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationResultBase, FolioDocumentOperationStatus, FolioDocumentOperationStory, FolioDocumentOperationType, FolioDocumentOperationUndoFailureReason, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js";
7
7
  import { FolioCommentAnchor, FolioReviewChange, FolioReviewChangeKind, getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
8
- import { FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsToStoryOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioEditableDocumentStoryHandle, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, FolioReviewedStory, FolioReviewedView, UnsupportedFolioReviewedViewError, isFolioResolvedReviewedView, isFolioReviewedView } from "./headless.js";
9
8
  import { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, isFolioAIContentBlock, normalizeFolioAIBlockText } from "./snapshot.js";
9
+ import { FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsToStoryOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioEditableDocumentStoryHandle, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, FolioReviewedStory, FolioReviewedView, UnsupportedFolioReviewedViewError, isFolioResolvedReviewedView, isFolioReviewedView } from "./headless.js";
10
10
  import { getFolioDocumentOutline, readFolioDocumentSection } from "./scoped-reading.js";
11
11
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
12
12
  export { type ApplyFolioDocumentOperationsOptions, type DocPositionRange, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIBlockTableLocation, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyOutcome, type FolioAIEditApplyResult, type FolioAIEditNormalization, type FolioAIEditNormalizationCode, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAIEditView, type FolioAIInlineFormatting, type FolioAISignatureParty, type FolioAITextRangeHandle, type FolioApplyDocumentOperationsToStoryOptions, type FolioCommentAnchor, type FolioDocumentNavigationTarget, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationResultBase, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocumentOutline, type FolioDocumentOutlineEntry, type FolioDocumentSection, type FolioDocumentSectionHandle, type FolioDocumentSectionReadResult, type FolioDocumentStory, type FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, type FolioEditableDocumentStoryHandle, type FolioReadReviewedStoryOptions, type FolioResolveReviewedStoryOptions, type FolioResolvedReviewedView, type FolioReviewChange, type FolioReviewChangeKind, type FolioReviewedStory, type FolioReviewedView, type FolioRevisionStamp, type FolioWordDiffOptions, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, UnsupportedFolioReviewedViewError, WORD_DIFF_GRANULARITIES, type WordDiffGranularity, type WordDiffNormalization, type WordDiffOptions, type WordDiffSegment, applyFolioAIEditOperations, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, buildAnnotatedBlockText, clampRangeToDocSize, createFolioAIEditSnapshot, createFolioAITextRangeHandle, diffWordSegments, getCommentAnchorsFromDoc, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioDocumentOutline, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, isFolioAIContentBlock, isFolioDocumentOperationModeSupported, isFolioResolvedReviewedView, isFolioReviewedView, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, readFolioDocumentSection, resolveFolioAIBlockRange, resolveFolioAITextRange, resolvePassageRange };
@@ -50,6 +50,24 @@ declare const createFolioAITextRangeHandle: ({ blockId, text, startOffset, endOf
50
50
  * whole subtree, so a table nested inside a hidden row is hidden with it.
51
51
  */
52
52
  declare const isHiddenTableRow: (node: Node) => boolean;
53
+ /** One table of a story, numbered the way {@link createFolioAIEditSnapshot} numbers it. */
54
+ type FolioStoryTable = {
55
+ /** Document-order index over every table, nested ones included. */
56
+ index: number;
57
+ /** The table node's position in the story document. */
58
+ start: number;
59
+ node: Node;
60
+ };
61
+ /**
62
+ * Every table of one story in document order, nested tables included and
63
+ * hidden rows' subtrees excluded.
64
+ *
65
+ * The single place a story's tables are numbered. `table.tableIndex` on a
66
+ * block, the geometry a comparison copies out of the target and the geometry
67
+ * it checks its own work against all read this list, so no second walk can
68
+ * number the same document differently.
69
+ */
70
+ declare const folioStoryTables: (doc: Node) => FolioStoryTable[];
53
71
  declare const createFolioAIEditSnapshot: (doc: Node) => FolioAIEditSnapshot;
54
72
  //#endregion
55
- export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
73
+ export { FolioStoryTable, createFolioAIEditSnapshot, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
@@ -1,3 +1,4 @@
1
+ import { expectRunFormattingOverrideMarkAttrs } from "../prosemirror/attrs/index.js";
1
2
  import { deriveBlankBlockId, deriveBlockId } from "../types/block-id.js";
2
3
  import { buildCleanBlockText } from "./clean-text.js";
3
4
  import { TableMap } from "prosemirror-tables";
@@ -69,6 +70,29 @@ const TABLE_ROLE_ROW = "row";
69
70
  */
70
71
  const isHiddenTableRow = (node) => node.type.name === TABLE_ROW_NODE_NAME && node.attrs["hidden"] === true;
71
72
  /**
73
+ * Every table of one story in document order, nested tables included and
74
+ * hidden rows' subtrees excluded.
75
+ *
76
+ * The single place a story's tables are numbered. `table.tableIndex` on a
77
+ * block, the geometry a comparison copies out of the target and the geometry
78
+ * it checks its own work against all read this list, so no second walk can
79
+ * number the same document differently.
80
+ */
81
+ const folioStoryTables = (doc) => {
82
+ const tables = [];
83
+ doc.descendants((node, pos) => {
84
+ if (node.isTextblock) return false;
85
+ if (isHiddenTableRow(node)) return false;
86
+ if (node.type.spec["tableRole"] === TABLE_ROLE_TABLE) tables.push({
87
+ index: tables.length,
88
+ start: pos,
89
+ node
90
+ });
91
+ return true;
92
+ });
93
+ return tables;
94
+ };
95
+ /**
72
96
  * Locate the block inside its innermost enclosing table, or `undefined` when
73
97
  * it sits outside every table.
74
98
  */
@@ -102,7 +126,7 @@ const createFolioAIEditSnapshot = (doc) => {
102
126
  const draftBlocks = [];
103
127
  const hashCounts = /* @__PURE__ */ new Map();
104
128
  const usedBlockIds = /* @__PURE__ */ new Set();
105
- const tableIndexByStart = /* @__PURE__ */ new Map();
129
+ const tableIndexByStart = new Map(folioStoryTables(doc).map(({ start, index }) => [start, index]));
106
130
  const path = [];
107
131
  let blockIndex = 0;
108
132
  let blankIndex = 0;
@@ -110,7 +134,6 @@ const createFolioAIEditSnapshot = (doc) => {
110
134
  while (pos >= (path.at(-1)?.end ?? Number.POSITIVE_INFINITY)) path.pop();
111
135
  if (!node.isTextblock) {
112
136
  if (isHiddenTableRow(node)) return false;
113
- if (node.type.spec["tableRole"] === TABLE_ROLE_TABLE) tableIndexByStart.set(pos, tableIndexByStart.size);
114
137
  if (!node.isLeaf) path.push({
115
138
  node,
116
139
  start: pos,
@@ -230,14 +253,16 @@ const getPreviewRuns = (node) => {
230
253
  if (!child.isText || child.text === void 0) return true;
231
254
  if (child.marks.some((mark) => mark.type.name === DELETION_MARK)) return false;
232
255
  const style = getPreviewRunStyle(child.marks, defaultStyle);
256
+ const directFormatting = getDirectPreviewRunStyle(child.marks, defaultStyle);
233
257
  const previous = runs.at(-1);
234
- if (previous && samePreviewRunStyle(previous, style)) {
258
+ if (previous && samePreviewRunStyle(previous, style) && sameDirectFormatting(previous.directFormatting, directFormatting)) {
235
259
  previous.text += child.text;
236
260
  return false;
237
261
  }
238
262
  runs.push({
239
263
  text: child.text,
240
- ...style
264
+ ...style,
265
+ ...!isEmptyPreviewRunStyle(directFormatting) && { directFormatting }
241
266
  });
242
267
  return false;
243
268
  });
@@ -288,6 +313,23 @@ const getPreviewRunStyle = (marks, defaultStyle) => {
288
313
  }
289
314
  return style;
290
315
  };
316
+ const getDirectPreviewRunStyle = (marks, inheritedStyle) => {
317
+ const markedStyle = getPreviewRunStyle(marks, {});
318
+ const directStyle = {};
319
+ const overrideMark = marks.find(({ type }) => type.name === "runFormattingOverride");
320
+ const hasFormattingProvenance = overrideMark !== void 0 || marks.some(({ type }) => type.name === "characterStyle");
321
+ const directFontProperties = overrideMark ? expectRunFormattingOverrideMarkAttrs(overrideMark).directFontProperties : void 0;
322
+ for (const property of [
323
+ "bold",
324
+ "italic",
325
+ "underline",
326
+ "strike"
327
+ ]) if (Boolean(markedStyle[property]) !== Boolean(inheritedStyle[property])) directStyle[property] = Boolean(markedStyle[property]);
328
+ if (markedStyle.fontFamily !== void 0 && (hasFormattingProvenance ? directFontProperties?.includes("fontFamily") : markedStyle.fontFamily !== inheritedStyle.fontFamily)) directStyle.fontFamily = markedStyle.fontFamily;
329
+ if (markedStyle.fontSizePt !== void 0 && (hasFormattingProvenance ? directFontProperties?.includes("fontSize") : markedStyle.fontSizePt !== inheritedStyle.fontSizePt)) directStyle.fontSizePt = markedStyle.fontSizePt;
330
+ if (markedStyle.color !== void 0 && (hasFormattingProvenance ? directFontProperties?.includes("color") : markedStyle.color !== inheritedStyle.color)) directStyle.color = markedStyle.color;
331
+ return directStyle;
332
+ };
291
333
  const getBooleanTextFormatting = (formatting) => ({
292
334
  ...Reflect.get(formatting, "bold") === true && { bold: true },
293
335
  ...Reflect.get(formatting, "italic") === true && { italic: true },
@@ -330,6 +372,8 @@ const getColorFromAttrs = (attrs) => {
330
372
  return `#${rgb}`;
331
373
  };
332
374
  const samePreviewRunStyle = (run, style) => run.bold === style.bold && run.italic === style.italic && run.underline === style.underline && run.strike === style.strike && run.fontFamily === style.fontFamily && run.fontSizePt === style.fontSizePt && run.color === style.color;
375
+ const isEmptyPreviewRunStyle = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
376
+ const sameDirectFormatting = (left, right) => left === void 0 && isEmptyPreviewRunStyle(right) || left !== void 0 && left.bold === right.bold && left.italic === right.italic && left.underline === right.underline && left.strike === right.strike && left.fontFamily === right.fontFamily && left.fontSizePt === right.fontSizePt && left.color === right.color;
333
377
  const isUnstyledPreviewRun = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
334
378
  //#endregion
335
- export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
379
+ export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
@@ -0,0 +1,56 @@
1
+ import { FolioStoryTable } from "./snapshot.js";
2
+ import { Transaction } from "prosemirror-state";
3
+ import { Node } from "prosemirror-model";
4
+ //#region src/ai-edits/table-geometry.d.ts
5
+ /**
6
+ * One line per table, in the document order tables are numbered in: the
7
+ * table's own properties, then each row's and each cell's. Nested tables get
8
+ * their own lines rather than being folded into their parent's, so a
9
+ * difference names the table it is in.
10
+ */
11
+ declare const projectTableGeometry: (tables: readonly FolioStoryTable[]) => string[];
12
+ /** Where a cell sits: which table, which row of it, which cell of that row. */
13
+ type TableCellCoordinate = {
14
+ tableIndex: number;
15
+ rowIndex: number;
16
+ cellIndex: number;
17
+ };
18
+ /** One base cell and the target cell it was aligned with. */
19
+ type TableGeometryPairing = {
20
+ base: TableCellCoordinate;
21
+ target: TableCellCoordinate;
22
+ };
23
+ type MatchTableGeometryOptions = {
24
+ tr: Transaction;
25
+ /** The base story's tables, with the positions the transaction will write at. */
26
+ baseTables: readonly FolioStoryTable[];
27
+ /** The target story's tables, by the same index the pairings name. */
28
+ targetTables: ReadonlyMap<number, Node>;
29
+ pairings: readonly TableGeometryPairing[];
30
+ revision: {
31
+ author: string;
32
+ date: string;
33
+ idSeed: number;
34
+ };
35
+ };
36
+ type MatchTableGeometryResult = {
37
+ /** First revision id a following batch may allocate. */
38
+ nextRevisionId: number;
39
+ /** Tables, rows and cells whose properties the match moved. */
40
+ matched: number;
41
+ };
42
+ /**
43
+ * Copy a paired table's, row's and cell's properties from the document it was
44
+ * compared against, recording the previous set so a reject restores it.
45
+ *
46
+ * Pairings name cells, because that is what the block alignment resolves; the
47
+ * row and the table each cell sits in are matched with it. A property set that
48
+ * already agrees is left alone, so an unchanged table produces no revision.
49
+ *
50
+ * `colspan` / `rowspan` never move: they shape the table map, and changing one
51
+ * without restructuring the rows around it leaves the map inconsistent with
52
+ * its own grid. That is the rule a reject of a `w:tcPrChange` follows too.
53
+ */
54
+ declare const matchTableGeometry: ({ tr, baseTables, targetTables, pairings, revision }: MatchTableGeometryOptions) => MatchTableGeometryResult;
55
+ //#endregion
56
+ export { MatchTableGeometryResult, TableCellCoordinate, TableGeometryPairing, matchTableGeometry, projectTableGeometry };
@@ -0,0 +1,217 @@
1
+ import { tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "../prosemirror/commands/propertyChangeScope.js";
2
+ import { tableAttrsToFormatting, tableCellAttrsToFormatting, tableRowAttrsToFormatting } from "../prosemirror/conversion/fromProseDoc.js";
3
+ import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
4
+ import { canonicalJson } from "../utils/canonicalJson.js";
5
+ //#region src/ai-edits/table-geometry.ts
6
+ const ORIGINAL_FORMATTING = "_originalFormatting";
7
+ /**
8
+ * The formatting a parser stored verbatim for the save path. It is deliberately
9
+ * outside every projection below: a document saved by an editor materializes
10
+ * style-resolved properties into its own `w:tcPr`, so two packages that render
11
+ * the same table can store different formatting for it, and comparing what was
12
+ * stored would report a difference no redline can or should represent. What is
13
+ * compared is the effective set the properties resolve to.
14
+ */
15
+ const withoutOriginalFormatting = (keys) => keys.filter((key) => key !== ORIGINAL_FORMATTING);
16
+ /**
17
+ * Attrs a `w:tblPrChange` reject restores, and therefore the attrs a match
18
+ * writes and the projection reads. `columnWidths` is deliberately outside
19
+ * them: the grid is `w:tblGrid`, not `w:tblPr`, and the editable model has no
20
+ * `w:tblGridChange` to record a change of it against.
21
+ */
22
+ const TABLE_SCOPED_ATTRS = withoutOriginalFormatting(Object.keys(tableRejectAttrPatch(void 0)));
23
+ const ROW_SCOPED_ATTRS = withoutOriginalFormatting(Object.keys(tableRowRejectAttrPatch(void 0)));
24
+ const CELL_SCOPED_ATTRS = withoutOriginalFormatting(Object.keys(tableCellRejectAttrPatch(void 0, void 0)));
25
+ /**
26
+ * `false` and absent are the same thing for every property in scope here: they
27
+ * are all presence flags (`w:tblHeader`, `w:hidden`, `w:noWrap`), and a parser
28
+ * that materializes an absent one as `false` must not read as a difference
29
+ * from one that leaves it unset.
30
+ */
31
+ const scopedAttrs = (attrs, keys) => {
32
+ const scoped = {};
33
+ for (const key of keys) {
34
+ const value = attrs[key];
35
+ scoped[key] = value === false ? null : value ?? null;
36
+ }
37
+ return scoped;
38
+ };
39
+ const cellProjection = (cell) => canonicalJson({
40
+ colspan: cell.attrs["colspan"],
41
+ rowspan: cell.attrs["rowspan"],
42
+ ...scopedAttrs(cell.attrs, CELL_SCOPED_ATTRS)
43
+ });
44
+ const rowProjection = (row) => {
45
+ const cells = [];
46
+ row.forEach((cell) => {
47
+ cells.push(cellProjection(cell));
48
+ });
49
+ return `${canonicalJson(scopedAttrs(row.attrs, ROW_SCOPED_ATTRS))}|${cells.join("|")}`;
50
+ };
51
+ /**
52
+ * One line per table, in the document order tables are numbered in: the
53
+ * table's own properties, then each row's and each cell's. Nested tables get
54
+ * their own lines rather than being folded into their parent's, so a
55
+ * difference names the table it is in.
56
+ */
57
+ const projectTableGeometry = (tables) => tables.map(({ node }) => {
58
+ const rows = [];
59
+ node.forEach((row) => {
60
+ rows.push(rowProjection(row));
61
+ });
62
+ return `${canonicalJson(scopedAttrs(node.attrs, TABLE_SCOPED_ATTRS))}#${rows.join("#")}`;
63
+ });
64
+ const childPositions = (node, start) => {
65
+ const positions = [];
66
+ let offset = start + 1;
67
+ node.forEach((child) => {
68
+ positions.push(offset);
69
+ offset += child.nodeSize;
70
+ });
71
+ return positions;
72
+ };
73
+ /**
74
+ * `Node["attrs"]` is an open record, and the converters below want the node
75
+ * type's own shape. The two span counts are the only members the schema always
76
+ * carries a value for, so naming them is what turns the record into one.
77
+ */
78
+ const cellAttrsOf = (node) => ({
79
+ ...effectiveAttrs(node),
80
+ colspan: typeof node.attrs["colspan"] === "number" ? node.attrs["colspan"] : 1,
81
+ rowspan: typeof node.attrs["rowspan"] === "number" ? node.attrs["rowspan"] : 1
82
+ });
83
+ /**
84
+ * The node's attrs with the style cascade's own values cleared, so the
85
+ * converter treats every effective value as one the node states.
86
+ *
87
+ * A change element stores the COMPLETE previous property set and a reject
88
+ * rebuilds the live properties from it alone, so the record has to hold what
89
+ * the node renders with — including a border its table style supplied. The
90
+ * save path wants the opposite (write only what the node states, or the
91
+ * inherited value becomes an override), which is what the resolved companions
92
+ * are for; a record is the one place they get in the way.
93
+ */
94
+ const effectiveAttrs = (node) => ({
95
+ ...node.attrs,
96
+ _resolvedBorders: null,
97
+ _resolvedMargins: null,
98
+ _resolvedCellMargins: null
99
+ });
100
+ const TABLE_SCOPE = {
101
+ keys: TABLE_SCOPED_ATTRS,
102
+ formattingOf: (node) => tableAttrsToFormatting(effectiveAttrs(node)),
103
+ rejectPatch: (previousFormatting) => tableRejectAttrPatch(previousFormatting),
104
+ changeAttr: "tblPrChange",
105
+ changeType: "tablePropertyChange"
106
+ };
107
+ const ROW_SCOPE = {
108
+ keys: ROW_SCOPED_ATTRS,
109
+ formattingOf: (node) => tableRowAttrsToFormatting(node.attrs),
110
+ rejectPatch: (previousFormatting) => tableRowRejectAttrPatch(previousFormatting),
111
+ changeAttr: "trPrChange",
112
+ changeType: "tableRowPropertyChange"
113
+ };
114
+ const CELL_SCOPE = {
115
+ keys: CELL_SCOPED_ATTRS,
116
+ formattingOf: (node) => tableCellAttrsToFormatting(cellAttrsOf(node)),
117
+ rejectPatch: (previousFormatting, liveFormatting) => tableCellRejectAttrPatch(previousFormatting, liveFormatting),
118
+ changeAttr: "tcPrChange",
119
+ changeType: "tableCellPropertyChange"
120
+ };
121
+ const scopedValues = (attrs, keys) => canonicalJson(scopedAttrs(attrs, keys));
122
+ /**
123
+ * One node's properties, matched to the node it was paired with, or `null`
124
+ * when the difference cannot be recorded.
125
+ *
126
+ * A change element stores the COMPLETE previous property set, and rejecting it
127
+ * rebuilds the live properties from that record alone. So the record is built
128
+ * from what the node would SERIALIZE, not from what its parser stored: a cell
129
+ * inherits borders and margins from the table and from a table style, and a
130
+ * record that omitted them would reject to a third document.
131
+ *
132
+ * The check is direct, and it is what keeps the round trip exact: rebuild the
133
+ * live values from the record and see whether they come back. Where they do
134
+ * not, the difference is left alone rather than written as a revision that
135
+ * cannot be undone.
136
+ */
137
+ const propertyChangeFor = (base, target, position, scope) => {
138
+ const live = scopedValues(base.attrs, scope.keys);
139
+ if (live === scopedValues(target.attrs, scope.keys)) return null;
140
+ const previousFormatting = scope.formattingOf(base);
141
+ if (scopedValues(scope.rejectPatch(previousFormatting, previousFormatting), scope.keys) !== live) return null;
142
+ return {
143
+ position,
144
+ attrs: {
145
+ ...scopedAttrs(target.attrs, scope.keys),
146
+ [ORIGINAL_FORMATTING]: scope.formattingOf(target) ?? null
147
+ },
148
+ changeAttr: scope.changeAttr,
149
+ changeType: scope.changeType,
150
+ previousFormatting
151
+ };
152
+ };
153
+ /**
154
+ * Copy a paired table's, row's and cell's properties from the document it was
155
+ * compared against, recording the previous set so a reject restores it.
156
+ *
157
+ * Pairings name cells, because that is what the block alignment resolves; the
158
+ * row and the table each cell sits in are matched with it. A property set that
159
+ * already agrees is left alone, so an unchanged table produces no revision.
160
+ *
161
+ * `colspan` / `rowspan` never move: they shape the table map, and changing one
162
+ * without restructuring the rows around it leaves the map inconsistent with
163
+ * its own grid. That is the rule a reject of a `w:tcPrChange` follows too.
164
+ */
165
+ const matchTableGeometry = ({ tr, baseTables, targetTables, pairings, revision }) => {
166
+ const baseByIndex = new Map(baseTables.map((table) => [table.index, table]));
167
+ const targets = [];
168
+ const claimed = /* @__PURE__ */ new Set();
169
+ const consider = (base, target, position, scope) => {
170
+ if (claimed.has(position)) return;
171
+ claimed.add(position);
172
+ const change = propertyChangeFor(base, target, position, scope);
173
+ if (change) targets.push(change);
174
+ };
175
+ for (const { base, target } of pairings) {
176
+ const baseTable = baseByIndex.get(base.tableIndex);
177
+ const targetTable = targetTables.get(target.tableIndex);
178
+ if (!baseTable || !targetTable) continue;
179
+ const baseRow = baseTable.node.maybeChild(base.rowIndex);
180
+ const targetRow = targetTable.maybeChild(target.rowIndex);
181
+ const rowPosition = childPositions(baseTable.node, baseTable.start)[base.rowIndex];
182
+ if (!baseRow || !targetRow || rowPosition === void 0) continue;
183
+ const baseCell = baseRow.maybeChild(base.cellIndex);
184
+ const targetCell = targetRow.maybeChild(target.cellIndex);
185
+ const cellPosition = childPositions(baseRow, rowPosition)[base.cellIndex];
186
+ if (!baseCell || !targetCell || cellPosition === void 0) continue;
187
+ consider(baseTable.node, targetTable, baseTable.start, TABLE_SCOPE);
188
+ consider(baseRow, targetRow, rowPosition, ROW_SCOPE);
189
+ consider(baseCell, targetCell, cellPosition, CELL_SCOPE);
190
+ }
191
+ let revisionId = revision.idSeed;
192
+ for (const target of targets) {
193
+ const node = tr.doc.nodeAt(target.position);
194
+ if (!node) continue;
195
+ tr.setNodeMarkup(target.position, void 0, {
196
+ ...node.attrs,
197
+ ...target.attrs,
198
+ [target.changeAttr]: [{
199
+ type: target.changeType,
200
+ info: {
201
+ id: revisionId,
202
+ author: revision.author,
203
+ date: revision.date
204
+ },
205
+ ...target.previousFormatting != null && { previousFormatting: target.previousFormatting }
206
+ }]
207
+ });
208
+ revisionId += 1;
209
+ }
210
+ if (revisionId > revision.idSeed) markStructuralChange(tr);
211
+ return {
212
+ nextRevisionId: revisionId,
213
+ matched: targets.length
214
+ };
215
+ };
216
+ //#endregion
217
+ export { matchTableGeometry, projectTableGeometry };
@@ -1,4 +1,4 @@
1
- import { TrackedChangeProvenance } from "../prosemirror/schema/marks.js";
1
+ import { TableStructureRevision } from "./table-template.js";
2
2
  import { TableRowTarget } from "./table-targets.js";
3
3
  import { Transaction } from "prosemirror-state";
4
4
  import { Node } from "prosemirror-model";
@@ -27,19 +27,6 @@ type TableColumnInsertion = {
27
27
  columnIndex: number;
28
28
  };
29
29
  type TableColumnDeletion = TableColumnInsertion;
30
- type TableStructureRevision = {
31
- revisionId: number;
32
- author: string;
33
- date: string;
34
- /** Optional author initials (w:initials), carried for round-trip. */
35
- initials?: string;
36
- /**
37
- * `"suggested"` marks the produced `trIns`/`trDel`/`cellMarker` as an AI
38
- * proposal that is stripped from serialized DOCX until accepted.
39
- */
40
- provenance?: TrackedChangeProvenance;
41
- suggestionId?: string;
42
- };
43
30
  type TableRowColumnMutationResult = {
44
31
  type: "applied";
45
32
  transaction: Transaction;
@@ -63,8 +50,39 @@ type ApplyTableRowInsertionOptions = {
63
50
  insertion: TableRowInsertion;
64
51
  cellTexts: readonly string[] | undefined;
65
52
  revision: TableStructureRevision | null;
53
+ /**
54
+ * The row to place, when the caller has one — a comparison adding a row the
55
+ * target document already holds. Its `w:trPr` and per-cell `w:tcPr` travel
56
+ * with it; `cellTexts` builds the row from the neighbouring one when it is
57
+ * absent or does not fit this table's grid.
58
+ */
59
+ template?: Node;
60
+ };
61
+ declare const applyTableRowInsertion: ({ tr, insertion, cellTexts, revision, template }: ApplyTableRowInsertionOptions) => TableRowColumnMutationResult;
62
+ type MarkTableRowContentOptions = {
63
+ tr: Transaction;
64
+ rowPosition: number;
65
+ kind: "insertion" | "deletion";
66
+ revision: TableStructureRevision;
66
67
  };
67
- declare const applyTableRowInsertion: ({ tr, insertion, cellTexts, revision }: ApplyTableRowInsertionOptions) => TableRowColumnMutationResult;
68
+ /**
69
+ * Mark the runs inside a row whose structural revision was just written.
70
+ *
71
+ * Word records a tracked row insertion or deletion twice: on the row
72
+ * (`w:trPr/w:ins` | `w:trPr/w:del`) AND around every run in its cells
73
+ * (`w:ins` | `w:del`, the latter with `w:delText`). A consumer that reads only
74
+ * run-level revisions — which is most of them — keeps a deleted row's text on
75
+ * accept and an inserted row's text on reject when the row marker stands
76
+ * alone, so the two must always be written together.
77
+ *
78
+ * Two kinds of run stay unmarked. A run that already carries a revision keeps
79
+ * it, because OOXML nests `w:ins`/`w:del` but the editable model holds one
80
+ * wrapper per run and overwriting would drop the earlier revision. A cell that
81
+ * spans into the row below survives the deletion — `removeRow` moves it down
82
+ * and shortens its span — so marking its text would delete content the
83
+ * accepted document must still hold.
84
+ */
85
+ declare const markTableRowContent: ({ tr, rowPosition, kind, revision }: MarkTableRowContentOptions) => void;
68
86
  type ApplyTableColumnInsertionOptions = {
69
87
  tr: Transaction;
70
88
  insertion: TableColumnInsertion;
@@ -98,4 +116,4 @@ declare const applyTableRowDeletion: ({ tr, deletion, revision }: ApplyTableRowD
98
116
  */
99
117
  declare const splitCellParagraphTexts: (text: string) => string[];
100
118
  //#endregion
101
- export { TableColumnDeletion, TableColumnInsertion, TableRowDeletion, TableRowInsertion, TableStructureRevision, applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, splitCellParagraphTexts };
119
+ export { MarkTableRowContentOptions, TableColumnDeletion, TableColumnInsertion, TableRowDeletion, TableRowInsertion, applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts };
@@ -1,6 +1,7 @@
1
1
  import { markStructuralChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
2
2
  import { stripBlockIdentityAttrs } from "./block-identity.js";
3
3
  import { findEnclosingTableCell, findEnclosingTableRow } from "./table-targets.js";
4
+ import { tableRowFromTemplate } from "./table-template.js";
4
5
  import { TableMap, columnIsHeader, removeColumn, removeRow, rowIsHeader, tableNodeTypes } from "prosemirror-tables";
5
6
  //#region src/ai-edits/table-row-column-mutations.ts
6
7
  const getTableColumnCoordinateKey = ({ tablePosition, columnIndex }) => `${tablePosition}:${columnIndex}`;
@@ -25,7 +26,7 @@ const findTableRowInsertion = ({ doc, blockFrom, position }) => {
25
26
  rowIndex
26
27
  });
27
28
  };
28
- const applyTableRowInsertion = ({ tr, insertion, cellTexts, revision }) => {
29
+ const applyTableRowInsertion = ({ tr, insertion, cellTexts, revision, template }) => {
29
30
  if (revision && insertion.rowspanUpdates.length > 0) return { type: "unsupported" };
30
31
  const liveRowspanUpdates = [];
31
32
  for (const updatePosition of insertion.rowspanUpdates) {
@@ -44,8 +45,15 @@ const applyTableRowInsertion = ({ tr, insertion, cellTexts, revision }) => {
44
45
  rowspan: update.rowspan + 1
45
46
  });
46
47
  const rowAttrs = revision ? { trIns: revision } : null;
47
- const row = insertion.rowType.create(rowAttrs, insertion.cells);
48
- tr.insert(insertion.rowPosition, populateTableRow(row, cellTexts));
48
+ const templated = template === void 0 ? null : tableRowFromTemplate({
49
+ template,
50
+ columnCount: insertion.columnCount
51
+ });
52
+ const row = templated ? templated.type.create({
53
+ ...templated.attrs,
54
+ ...rowAttrs
55
+ }, templated.content) : populateTableRow(insertion.rowType.create(rowAttrs, insertion.cells), cellTexts);
56
+ tr.insert(insertion.rowPosition, row);
49
57
  if (revision) markTableRowContent({
50
58
  tr,
51
59
  rowPosition: insertion.rowPosition,
@@ -389,4 +397,4 @@ const getTableColumnDeletionCellPositions = ({ map, table, columnIndex }) => {
389
397
  return [...positions];
390
398
  };
391
399
  //#endregion
392
- export { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, splitCellParagraphTexts };
400
+ export { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, markTableRowContent, splitCellParagraphTexts };
@@ -0,0 +1,60 @@
1
+ import { TrackedChangeProvenance } from "../prosemirror/schema/marks.js";
2
+ import { Node, Schema } from "prosemirror-model";
3
+ //#region src/ai-edits/table-template.d.ts
4
+ /** The node an operation should place, by the id of the operation placing it. */
5
+ type FolioTableTemplates = ReadonlyMap<string, Node>;
6
+ /**
7
+ * A structural tracked change on a table row.
8
+ *
9
+ * A whole-table insertion has no element of its own in the format: every row
10
+ * carries `w:trPr/w:ins`, and every run inside it carries `w:ins` as well. A
11
+ * consumer that reads only run-level revisions keeps the text of a rejected
12
+ * insertion when the row marker stands alone, so the two are always written
13
+ * together.
14
+ */
15
+ type TableStructureRevision = {
16
+ revisionId: number;
17
+ author: string;
18
+ date: string;
19
+ /** Optional author initials (w:initials), carried for round-trip. */
20
+ initials?: string;
21
+ /**
22
+ * `"suggested"` marks the produced `trIns`/`trDel`/`cellMarker` as an AI
23
+ * proposal that is stripped from serialized DOCX until accepted.
24
+ */
25
+ provenance?: TrackedChangeProvenance;
26
+ suggestionId?: string;
27
+ };
28
+ type TableFromTemplateOptions = {
29
+ schema: Schema;
30
+ template: Node;
31
+ /** Present in tracked mode: the whole table is stamped as one insertion. */
32
+ revision?: TableStructureRevision;
33
+ };
34
+ /**
35
+ * The template table, ready to insert: its properties, grid, rows and cells
36
+ * carried verbatim, nested tables included, and every row plus every run
37
+ * stamped as an insertion when the caller is tracking changes.
38
+ *
39
+ * `null` when the template is not a table or holds no rows, so the caller can
40
+ * fall back to the operation's own cell texts rather than place something that
41
+ * is not one.
42
+ */
43
+ declare const tableFromTemplate: ({ schema, template, revision }: TableFromTemplateOptions) => Node | null;
44
+ type TableRowFromTemplateOptions = {
45
+ template: Node;
46
+ /** Grid columns the row has to occupy, from the table receiving it. */
47
+ columnCount: number;
48
+ };
49
+ /**
50
+ * The template row, ready to insert into an existing table.
51
+ *
52
+ * Refused — `null`, so the caller falls back to the operation's cell texts —
53
+ * when the row's cells do not span exactly the receiving table's grid: a row
54
+ * of a different width would leave the table's map inconsistent with its own
55
+ * grid. A cell that merged vertically in the document it came from is placed
56
+ * unmerged, because the rows it reached into are not this table's.
57
+ */
58
+ declare const tableRowFromTemplate: ({ template, columnCount }: TableRowFromTemplateOptions) => Node | null;
59
+ //#endregion
60
+ export { FolioTableTemplates, TableStructureRevision, tableFromTemplate, tableRowFromTemplate };