@stll/folio-core 0.33.1 → 0.33.2

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 (91) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +134 -25
  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 +25 -3
  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 +7 -0
  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/plan.d.ts +31 -1
  20. package/dist/compare/plan.js +233 -66
  21. package/dist/compare/types.d.ts +9 -7
  22. package/dist/compare/types.js +7 -5
  23. package/dist/compare/verification.d.ts +31 -12
  24. package/dist/compare/verification.js +88 -17
  25. package/dist/compat/eigenpal.d.ts +4 -4
  26. package/dist/compat/eigenpal.js +2 -2
  27. package/dist/display-list/primitives.d.ts +1 -1
  28. package/dist/document-operations.d.ts +10 -1
  29. package/dist/document-operations.js +3 -2
  30. package/dist/docx/appVersionNormalization.d.ts +49 -0
  31. package/dist/docx/appVersionNormalization.js +74 -0
  32. package/dist/docx/blockContentParser.js +3 -2
  33. package/dist/docx/drawingUtils.js +4 -3
  34. package/dist/docx/paragraphParser.js +3 -2
  35. package/dist/docx/rezip.d.ts +10 -4
  36. package/dist/docx/rezip.js +54 -18
  37. package/dist/docx/runParser.js +9 -8
  38. package/dist/docx/sdtProperties.js +4 -3
  39. package/dist/docx/selectiveSave.js +7 -6
  40. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  41. package/dist/docx/serializer/commentSerializer.js +43 -19
  42. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  43. package/dist/docx/serializer/documentSerializer.js +42 -78
  44. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  45. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  46. package/dist/docx/serializer/headerFooterSerializer.js +34 -29
  47. package/dist/docx/serializer/noteSerializer.js +34 -30
  48. package/dist/docx/serializer/numberingSerializer.js +8 -4
  49. package/dist/docx/serializer/paragraphSerializer.js +3 -3
  50. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  51. package/dist/docx/serializer/partNamespaces.js +324 -0
  52. package/dist/docx/serializer/runSerializer.js +1 -1
  53. package/dist/docx/serializer/settingsSerializer.js +8 -2
  54. package/dist/docx/serializer/stylesSerializer.js +8 -5
  55. package/dist/docx/serializer/tableSerializer.js +83 -18
  56. package/dist/docx/serializer/themeSerializer.js +10 -2
  57. package/dist/docx/server/build.d.ts +1 -1
  58. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  59. package/dist/docx/strictValueEncodings.gen.js +275 -0
  60. package/dist/docx/tableParser.d.ts +0 -6
  61. package/dist/docx/tableParser.js +52 -7
  62. package/dist/docx/transitionalSpelling.d.ts +23 -0
  63. package/dist/docx/transitionalSpelling.js +36 -0
  64. package/dist/docx/universalMeasure.d.ts +20 -0
  65. package/dist/docx/universalMeasure.js +32 -0
  66. package/dist/docx/verbatimCapture.d.ts +20 -0
  67. package/dist/docx/verbatimCapture.js +131 -0
  68. package/dist/docx/vmlImageParser.js +4 -3
  69. package/dist/docx/watermarkParser.js +4 -3
  70. package/dist/docx/xmlParser.d.ts +23 -2
  71. package/dist/docx/xmlParser.js +43 -11
  72. package/dist/index.d.ts +4 -4
  73. package/dist/index.js +2 -2
  74. package/dist/model.d.ts +3 -3
  75. package/dist/model.js +2 -2
  76. package/dist/prosemirror/commands/comments.js +2 -1
  77. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  78. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  79. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  80. package/dist/prosemirror/conversion/fromProseDoc.js +15 -4
  81. package/dist/prosemirror/conversion/toProseDoc.js +12 -3
  82. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  83. package/dist/prosemirror/schema/marks.d.ts +1 -1
  84. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  85. package/dist/server.d.ts +1 -1
  86. package/dist/types/block-id.d.ts +12 -1
  87. package/dist/types/block-id.js +17 -1
  88. package/dist/utils/canonicalJson.d.ts +14 -0
  89. package/dist/utils/canonicalJson.js +19 -0
  90. package/dist/utils/formatToStyle.d.ts +1 -1
  91. package/package.json +2 -2
@@ -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 };
@@ -0,0 +1,176 @@
1
+ import { stripBlockIdentityAttrs } from "./block-identity.js";
2
+ import { Fragment } from "prosemirror-model";
3
+ //#region src/ai-edits/table-template.ts
4
+ /**
5
+ * Building a table from another document's table rather than from a grid of
6
+ * strings.
7
+ *
8
+ * `insertTable` and `insertTableRow` describe their content as cell texts,
9
+ * which is the right shape for a caller that writes a table from nothing. A
10
+ * comparison is not that caller: the table it adds already exists in the
11
+ * document it is comparing against, complete with its `w:tblPr`, its
12
+ * `w:tblGrid` widths, per-row `w:trPr`, per-cell `w:tcPr` — spans, vertical
13
+ * merges, shading, borders, margins, alignment — and cell paragraphs carrying
14
+ * their own properties. Rebuilding that from text loses every one of them, and
15
+ * a consumer accepting the redline then gets a table that is not the one it
16
+ * was compared to.
17
+ *
18
+ * So the operation may be handed the node it should place. The copy is
19
+ * structural and keeps everything by default; what it cannot bring is what
20
+ * only the package it came from can resolve — a relationship id (a drawing, a
21
+ * hyperlink), a note or comment id, a bookmark, a paragraph identity, or a
22
+ * tracked change belonging to the other document's history. Everything that
23
+ * describes the table itself travels.
24
+ */
25
+ /**
26
+ * Nodes that name something the other package owns: a drawing's relationship,
27
+ * an anchored frame's, a bookmark's id. They resolve to nothing once the table
28
+ * has crossed, so they are dropped rather than copied into a dangling
29
+ * reference.
30
+ */
31
+ const PACKAGE_BOUND_NODE_NAMES = /* @__PURE__ */ new Set([
32
+ "image",
33
+ "textBox",
34
+ "textBoxAnchor",
35
+ "shape",
36
+ "bookmarkBoundary"
37
+ ]);
38
+ /**
39
+ * Marks that name something the other package owns. Run properties are the
40
+ * point of copying the runs at all, so the denial is narrow: a relationship, a
41
+ * note, a comment, and the other document's revisions.
42
+ */
43
+ const PACKAGE_BOUND_MARK_NAMES = /* @__PURE__ */ new Set([
44
+ "hyperlink",
45
+ "footnoteRef",
46
+ "comment",
47
+ "insertion",
48
+ "deletion",
49
+ "runPropertyChange"
50
+ ]);
51
+ /**
52
+ * Paragraph attrs cleared on a copied paragraph: its identity, the bookmarks
53
+ * and empty hyperlinks that name package-scoped ids, the section it ended, and
54
+ * the tracked-change history of the document it came from. Its formatting —
55
+ * style, alignment, spacing, indentation, borders, shading, tabs — is what the
56
+ * copy exists to carry, and stays.
57
+ */
58
+ const CLEARED_PARAGRAPH_ATTRS = [
59
+ "bookmarks",
60
+ "_emptyHyperlinks",
61
+ "_propertyChanges",
62
+ "pPrMark",
63
+ "_suggestedInsert",
64
+ "_sectionProperties",
65
+ "sectionBreakType",
66
+ "renderedPageBreakBefore"
67
+ ];
68
+ /** Table, row and cell attrs that record the other document's revisions. */
69
+ const CLEARED_TABLE_ATTRS = ["tblPrChange", "_suggestedInsert"];
70
+ const CLEARED_ROW_ATTRS = [
71
+ "trIns",
72
+ "trDel",
73
+ "trPrChange"
74
+ ];
75
+ const CLEARED_CELL_ATTRS = ["cellMarker", "tcPrChange"];
76
+ const withoutAttrs = (attrs, cleared) => {
77
+ const next = { ...attrs };
78
+ for (const key of cleared) next[key] = null;
79
+ return next;
80
+ };
81
+ const insertionMarkOf = (schema, revision) => {
82
+ const markType = schema.marks["insertion"];
83
+ if (!revision || !markType) return null;
84
+ const { revisionId, author, date, initials, provenance, suggestionId } = revision;
85
+ return markType.create({
86
+ revisionId,
87
+ author,
88
+ date,
89
+ ...initials !== void 0 && { initials },
90
+ ...provenance !== void 0 && { provenance },
91
+ ...suggestionId !== void 0 && { suggestionId }
92
+ });
93
+ };
94
+ const copiedAttrs = (node, context) => {
95
+ switch (node.type.spec["tableRole"]) {
96
+ case "table": return withoutAttrs(node.attrs, CLEARED_TABLE_ATTRS);
97
+ case "row": {
98
+ const attrs = withoutAttrs(node.attrs, CLEARED_ROW_ATTRS);
99
+ return context.revision ? {
100
+ ...attrs,
101
+ trIns: context.revision
102
+ } : attrs;
103
+ }
104
+ case "cell":
105
+ case "header_cell": {
106
+ const attrs = withoutAttrs(node.attrs, CLEARED_CELL_ATTRS);
107
+ return context.clampRowSpan ? {
108
+ ...attrs,
109
+ rowspan: 1
110
+ } : attrs;
111
+ }
112
+ default: return node.isTextblock ? withoutAttrs(stripBlockIdentityAttrs(node.attrs), CLEARED_PARAGRAPH_ATTRS) : node.attrs;
113
+ }
114
+ };
115
+ /**
116
+ * One node of the template, or `null` when it names something the package it
117
+ * came from owns. Inline content carries the insertion mark; a nested table is
118
+ * copied by the same rules as the one holding it.
119
+ */
120
+ const copyNode = (node, context) => {
121
+ if (PACKAGE_BOUND_NODE_NAMES.has(node.type.name)) return null;
122
+ const marks = node.marks.filter(({ type }) => !PACKAGE_BOUND_MARK_NAMES.has(type.name));
123
+ if (node.isLeaf) return node.mark(node.isInline && context.insertion ? [...marks, context.insertion] : marks);
124
+ const inner = node.type.spec["tableRole"] === "table" ? {
125
+ ...context,
126
+ clampRowSpan: false
127
+ } : context;
128
+ const content = [];
129
+ node.forEach((child) => {
130
+ const copied = copyNode(child, inner);
131
+ if (copied) content.push(copied);
132
+ });
133
+ return node.type.create(copiedAttrs(node, context), Fragment.fromArray(content), marks);
134
+ };
135
+ /**
136
+ * The template table, ready to insert: its properties, grid, rows and cells
137
+ * carried verbatim, nested tables included, and every row plus every run
138
+ * stamped as an insertion when the caller is tracking changes.
139
+ *
140
+ * `null` when the template is not a table or holds no rows, so the caller can
141
+ * fall back to the operation's own cell texts rather than place something that
142
+ * is not one.
143
+ */
144
+ const tableFromTemplate = ({ schema, template, revision }) => {
145
+ if (template.type.spec["tableRole"] !== "table" || template.childCount === 0) return null;
146
+ return copyNode(template, {
147
+ revision: revision ?? null,
148
+ insertion: insertionMarkOf(schema, revision ?? null),
149
+ clampRowSpan: false
150
+ });
151
+ };
152
+ /**
153
+ * The template row, ready to insert into an existing table.
154
+ *
155
+ * Refused — `null`, so the caller falls back to the operation's cell texts —
156
+ * when the row's cells do not span exactly the receiving table's grid: a row
157
+ * of a different width would leave the table's map inconsistent with its own
158
+ * grid. A cell that merged vertically in the document it came from is placed
159
+ * unmerged, because the rows it reached into are not this table's.
160
+ */
161
+ const tableRowFromTemplate = ({ template, columnCount }) => {
162
+ if (template.type.spec["tableRole"] !== "row" || template.childCount === 0) return null;
163
+ let spanned = 0;
164
+ template.forEach((cell) => {
165
+ const colspan = cell.attrs["colspan"];
166
+ spanned += typeof colspan === "number" ? colspan : 1;
167
+ });
168
+ if (spanned !== columnCount) return null;
169
+ return copyNode(template, {
170
+ revision: null,
171
+ insertion: null,
172
+ clampRowSpan: true
173
+ });
174
+ };
175
+ //#endregion
176
+ export { tableFromTemplate, tableRowFromTemplate };
@@ -276,6 +276,13 @@ type FolioAIEditOperation = FolioAIEditReviewMeta & {
276
276
  * Delete the whole block. A block with words loses them and its paragraph
277
277
  * mark; a BLANK block has only a paragraph mark to lose, and loses it, so
278
278
  * the empty line goes away rather than the operation doing nothing.
279
+ *
280
+ * The block that ENDS its container is the exception, in both modes: a
281
+ * body, a cell, a header or footer, a note and a text box each end with a
282
+ * paragraph, and a deleted mark there would say "join with the paragraph
283
+ * after this one" where there is none. It loses its words and keeps its
284
+ * place. To remove it, delete the mark of the block BEFORE it, which
285
+ * merges forward into it.
279
286
  */
280
287
  {
281
288
  id: string;
@@ -13,6 +13,61 @@
13
13
  * gets written.
14
14
  */
15
15
  type CellContent = string | readonly BodyItem[];
16
+ /** `w:tcPr` children a fixture can author, each named after the element. */
17
+ type CellProperties = {
18
+ /** `w:gridSpan`: grid columns this cell occupies. */
19
+ gridSpan?: number;
20
+ /** `w:vMerge`: the cell starts a vertical merge, or continues one. */
21
+ verticalMerge?: "restart" | "continue";
22
+ /** `w:tcW` in twips. */
23
+ width?: number;
24
+ /** `w:shd` fill colour, as six hex digits. */
25
+ shadingFill?: string;
26
+ /** `w:vAlign`. */
27
+ verticalAlign?: "top" | "center" | "bottom";
28
+ /** `w:tcBorders`, single style on all four sides, in eighths of a point. */
29
+ borderSize?: number;
30
+ /** `w:tcMar`, the same margin on all four sides, in twips. */
31
+ margin?: number;
32
+ };
33
+ /** A cell: its content alone, or its content and its own `w:tcPr`. */
34
+ type Cell = CellContent | ({
35
+ content: CellContent;
36
+ } & CellProperties);
37
+ /** A row: its cells alone, or its cells and its own `w:trPr`. */
38
+ type TableRow = readonly Cell[] | {
39
+ cells: readonly Cell[];
40
+ /** `w:trHeight` in twips. */
41
+ height?: number;
42
+ /** `w:tblHeader`: the row repeats at the top of every page. */
43
+ header?: boolean;
44
+ /** `w:jc` on the row. */
45
+ justification?: "left" | "center" | "right";
46
+ };
47
+ /** `w:tblPr` children a fixture can author, each named after the element. */
48
+ type TableProperties = {
49
+ /** `w:tblStyle`. */
50
+ styleId?: string;
51
+ /** `w:tblW`. */
52
+ width?: {
53
+ value: number;
54
+ type: "auto" | "dxa" | "pct";
55
+ };
56
+ /** `w:jc` on the table. */
57
+ justification?: "left" | "center" | "right";
58
+ /** `w:tblInd` in twips. */
59
+ indent?: number;
60
+ /** `w:tblBorders`, single style on every side, in eighths of a point. */
61
+ borderSize?: number;
62
+ /** `w:shd` fill colour on the table, as six hex digits. */
63
+ shadingFill?: string;
64
+ /** `w:tblLayout`. */
65
+ layout?: "fixed" | "autofit";
66
+ /** `w:tblCellMar`, the same margin on all four sides, in twips. */
67
+ cellMargin?: number;
68
+ /** `w:tblLook` value, as four hex digits. */
69
+ look?: string;
70
+ };
16
71
  /**
17
72
  * One inline of a paragraph: plain text, or text carrying an external
18
73
  * hyperlink. A link is the case where a revision wrapper and the linked runs
@@ -35,7 +90,11 @@ type BodyItem = {
35
90
  paraId?: string;
36
91
  } | {
37
92
  kind: "table";
38
- rows: readonly (readonly CellContent[])[];
93
+ rows: readonly TableRow[];
94
+ /** `w:tblPr` children, written in the order the schema requires. */
95
+ properties?: TableProperties;
96
+ /** `w:tblGrid` column widths, in twips. One per grid column. */
97
+ columnWidths?: readonly number[];
39
98
  /**
40
99
  * Rows a package hides with `w:hidden`. The snapshot skips their whole
41
100
  * subtree, so a document that has one is the case where the snapshot
@@ -53,4 +112,4 @@ type BodySequenceOptions = {
53
112
  };
54
113
  declare const buildBodySequenceDocx: (items: readonly BodyItem[], { header }?: BodySequenceOptions) => Promise<ArrayBuffer>;
55
114
  //#endregion
56
- export { BodyItem, BodySequenceOptions, CellContent, ParagraphInline, buildBodySequenceDocx };
115
+ export { BodyItem, BodySequenceOptions, Cell, CellContent, CellProperties, ParagraphInline, TableProperties, TableRow, buildBodySequenceDocx };