@stll/folio-core 0.33.0 → 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.
- package/dist/ai-edits/apply.d.ts +15 -0
- package/dist/ai-edits/apply.js +135 -24
- package/dist/ai-edits/headless.d.ts +47 -3
- package/dist/ai-edits/headless.js +53 -3
- package/dist/ai-edits/index.d.ts +1 -1
- package/dist/ai-edits/snapshot.d.ts +19 -1
- package/dist/ai-edits/snapshot.js +25 -3
- package/dist/ai-edits/table-geometry.d.ts +56 -0
- package/dist/ai-edits/table-geometry.js +217 -0
- package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
- package/dist/ai-edits/table-row-column-mutations.js +12 -4
- package/dist/ai-edits/table-template.d.ts +60 -0
- package/dist/ai-edits/table-template.js +176 -0
- package/dist/ai-edits/types.d.ts +7 -0
- package/dist/compare/__fixtures__/body-sequence.d.ts +86 -4
- package/dist/compare/__fixtures__/body-sequence.js +169 -12
- package/dist/compare/compare.d.ts +15 -8
- package/dist/compare/compare.js +138 -34
- package/dist/compare/plan.d.ts +31 -1
- package/dist/compare/plan.js +277 -28
- package/dist/compare/reproducible-package.d.ts +7 -3
- package/dist/compare/reproducible-package.js +20 -3
- package/dist/compare/types.d.ts +20 -3
- package/dist/compare/types.js +13 -1
- package/dist/compare/verification.d.ts +49 -2
- package/dist/compare/verification.js +115 -1
- package/dist/compat/eigenpal.d.ts +5 -5
- package/dist/compat/eigenpal.js +3 -3
- package/dist/document-operations.d.ts +10 -1
- package/dist/document-operations.js +3 -2
- package/dist/docx/appVersionNormalization.d.ts +49 -0
- package/dist/docx/appVersionNormalization.js +74 -0
- package/dist/docx/blockContentParser.js +3 -2
- package/dist/docx/drawingUtils.js +4 -3
- package/dist/docx/hyperlinkParser.d.ts +9 -1
- package/dist/docx/hyperlinkParser.js +19 -13
- package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
- package/dist/docx/paraIdRangeNormalization.js +64 -0
- package/dist/docx/paragraphParser.js +82 -6
- package/dist/docx/revisionIdNormalization.d.ts +15 -1
- package/dist/docx/revisionIdNormalization.js +22 -4
- package/dist/docx/rezip.d.ts +13 -1
- package/dist/docx/rezip.js +80 -21
- package/dist/docx/runParser.js +9 -8
- package/dist/docx/sdtProperties.js +4 -3
- package/dist/docx/selectiveSave.js +7 -6
- package/dist/docx/serializer/commentSerializer.d.ts +1 -1
- package/dist/docx/serializer/commentSerializer.js +43 -19
- package/dist/docx/serializer/documentSerializer.d.ts +3 -1
- package/dist/docx/serializer/documentSerializer.js +42 -78
- package/dist/docx/serializer/fontTableSerializer.js +8 -5
- package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
- package/dist/docx/serializer/headerFooterSerializer.js +34 -29
- package/dist/docx/serializer/noteSerializer.js +34 -30
- package/dist/docx/serializer/numberingSerializer.js +8 -4
- package/dist/docx/serializer/paragraphSerializer.js +45 -21
- package/dist/docx/serializer/partNamespaces.d.ts +78 -0
- package/dist/docx/serializer/partNamespaces.js +324 -0
- package/dist/docx/serializer/runSerializer.js +1 -1
- package/dist/docx/serializer/settingsSerializer.js +8 -2
- package/dist/docx/serializer/stylesSerializer.js +8 -5
- package/dist/docx/serializer/tableSerializer.js +107 -27
- package/dist/docx/serializer/themeSerializer.js +10 -2
- package/dist/docx/server/build.d.ts +1 -1
- package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
- package/dist/docx/strictValueEncodings.gen.js +275 -0
- package/dist/docx/tableParser.d.ts +0 -6
- package/dist/docx/tableParser.js +52 -7
- package/dist/docx/transitionalSpelling.d.ts +23 -0
- package/dist/docx/transitionalSpelling.js +36 -0
- package/dist/docx/universalMeasure.d.ts +20 -0
- package/dist/docx/universalMeasure.js +32 -0
- package/dist/docx/verbatimCapture.d.ts +20 -0
- package/dist/docx/verbatimCapture.js +131 -0
- package/dist/docx/vmlImageParser.js +4 -3
- package/dist/docx/watermarkParser.js +4 -3
- package/dist/docx/xmlParser.d.ts +23 -2
- package/dist/docx/xmlParser.js +43 -11
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/prosemirror/commands/comments.js +8 -2
- package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
- package/dist/prosemirror/containerFinalParagraph.js +92 -0
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +15 -4
- package/dist/prosemirror/conversion/toProseDoc.js +12 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
- package/dist/prosemirror/schema/marks.d.ts +1 -1
- package/dist/prosemirror/schema/nodes.d.ts +22 -0
- package/dist/server.d.ts +1 -1
- package/dist/types/block-id.d.ts +12 -1
- package/dist/types/block-id.js +17 -1
- package/dist/utils/canonicalJson.d.ts +14 -0
- package/dist/utils/canonicalJson.js +19 -0
- package/dist/utils/formatToStyle.d.ts +1 -1
- 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 {
|
|
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
|
-
|
|
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,
|
|
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
|
|
48
|
-
|
|
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 };
|
package/dist/ai-edits/types.d.ts
CHANGED
|
@@ -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;
|