@beyondwork/docx-react-component 1.0.11 → 1.0.13
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/README.md +8 -2
- package/package.json +35 -21
- package/src/api/public-types.ts +103 -1
- package/src/core/commands/formatting-commands.ts +742 -0
- package/src/core/commands/image-commands.ts +84 -2
- package/src/core/commands/structural-helpers.ts +309 -0
- package/src/core/commands/table-structure-commands.ts +721 -0
- package/src/core/commands/text-commands.ts +166 -1
- package/src/core/state/editor-state.ts +318 -9
- package/src/formats/xlsx/io/parse-sheet.ts +177 -7
- package/src/formats/xlsx/io/parse-styles.ts +2 -0
- package/src/formats/xlsx/io/xlsx-session.ts +18 -12
- package/src/formats/xlsx/model/sheet.ts +81 -1
- package/src/formats/xlsx/model/workbook.ts +10 -6
- package/src/io/docx-session.ts +392 -22
- package/src/io/export/export-session.ts +55 -0
- package/src/io/export/serialize-footnotes.ts +5 -20
- package/src/io/export/serialize-headers-footers.ts +5 -31
- package/src/io/export/serialize-main-document.ts +78 -5
- package/src/io/normalize/normalize-text.ts +90 -1
- package/src/io/ooxml/parse-footnotes.ts +68 -5
- package/src/io/ooxml/parse-headers-footers.ts +67 -9
- package/src/io/ooxml/parse-main-document.ts +169 -6
- package/src/io/opc/package-reader.ts +3 -3
- package/src/io/source-package-provenance.ts +241 -0
- package/src/model/canonical-document.ts +450 -2
- package/src/model/cds-1.0.0.ts +5 -2
- package/src/model/snapshot.ts +190 -19
- package/src/preservation/package-preservation.ts +0 -7
- package/src/runtime/document-runtime.ts +7 -1
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -1
- package/src/runtime/surface-projection.ts +200 -17
- package/src/runtime/table-commands.ts +79 -0
- package/src/runtime/table-schema.ts +9 -0
- package/src/ui/WordReviewEditor.tsx +708 -16
- package/src/ui-tailwind/editor-surface/pm-schema.ts +121 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +73 -7
- package/src/ui-tailwind/editor-surface/search-plugin.ts +76 -16
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +162 -14
- package/src/validation/compatibility-engine.ts +208 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import type { RuntimeRenderSnapshot as PublicRuntimeRenderSnapshot } from "../../api/public-types";
|
|
2
|
+
import type {
|
|
3
|
+
DocumentRootNode,
|
|
4
|
+
ParagraphNode,
|
|
5
|
+
TableCellNode,
|
|
6
|
+
TableNode,
|
|
7
|
+
TableRowNode,
|
|
8
|
+
} from "../../model/canonical-document.ts";
|
|
9
|
+
import type { TableSelectionDescriptor } from "../../runtime/table-commands.ts";
|
|
10
|
+
import type { CanonicalDocumentEnvelope, SelectionSnapshot } from "../state/editor-state.ts";
|
|
11
|
+
import {
|
|
12
|
+
createDetachedAnchor,
|
|
13
|
+
createNodeAnchor,
|
|
14
|
+
createRangeAnchor,
|
|
15
|
+
} from "../selection/mapping.ts";
|
|
16
|
+
import {
|
|
17
|
+
createEmptyParagraph,
|
|
18
|
+
createNoopStructuralMutation,
|
|
19
|
+
findTableCellParagraphSelection,
|
|
20
|
+
findTopLevelParagraphSelectionNearBlock,
|
|
21
|
+
type StructuralMutationResult,
|
|
22
|
+
} from "./structural-helpers.ts";
|
|
23
|
+
|
|
24
|
+
type TableStructureOperation =
|
|
25
|
+
| { type: "add-row-before" }
|
|
26
|
+
| { type: "add-row-after" }
|
|
27
|
+
| { type: "add-column-before" }
|
|
28
|
+
| { type: "add-column-after" }
|
|
29
|
+
| { type: "delete-row" }
|
|
30
|
+
| { type: "delete-column" }
|
|
31
|
+
| { type: "delete-table" }
|
|
32
|
+
| { type: "merge-cells" }
|
|
33
|
+
| { type: "split-cell" }
|
|
34
|
+
| { type: "set-cell-background"; color: string };
|
|
35
|
+
|
|
36
|
+
export function applyTableStructureOperation(
|
|
37
|
+
document: CanonicalDocumentEnvelope,
|
|
38
|
+
snapshot: PublicRuntimeRenderSnapshot,
|
|
39
|
+
selectionDescriptor: TableSelectionDescriptor | null,
|
|
40
|
+
operation: TableStructureOperation,
|
|
41
|
+
): StructuralMutationResult {
|
|
42
|
+
const root = document.content;
|
|
43
|
+
const fallbackSelection = toInternalSelectionSnapshot(snapshot.selection);
|
|
44
|
+
if (!root || root.type !== "doc") {
|
|
45
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const effectiveSelection =
|
|
49
|
+
selectionDescriptor ?? resolveTableSelectionFromSnapshot(snapshot);
|
|
50
|
+
if (!effectiveSelection) {
|
|
51
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const target = root.children[effectiveSelection.tableBlockIndex];
|
|
55
|
+
if (!target || target.type !== "table") {
|
|
56
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
switch (operation.type) {
|
|
60
|
+
case "delete-table":
|
|
61
|
+
return deleteTableBlock(document, root, effectiveSelection.tableBlockIndex, fallbackSelection);
|
|
62
|
+
case "set-cell-background":
|
|
63
|
+
return setCellBackground(document, root, target, effectiveSelection, operation.color, fallbackSelection);
|
|
64
|
+
case "split-cell":
|
|
65
|
+
return splitSelectedCell(document, root, target, effectiveSelection, fallbackSelection);
|
|
66
|
+
case "merge-cells":
|
|
67
|
+
return mergeSelectedCells(document, root, target, effectiveSelection, fallbackSelection);
|
|
68
|
+
case "add-row-before":
|
|
69
|
+
return addRow(document, root, target, effectiveSelection, "before", fallbackSelection);
|
|
70
|
+
case "add-row-after":
|
|
71
|
+
return addRow(document, root, target, effectiveSelection, "after", fallbackSelection);
|
|
72
|
+
case "delete-row":
|
|
73
|
+
return deleteRow(document, root, target, effectiveSelection, fallbackSelection);
|
|
74
|
+
case "add-column-before":
|
|
75
|
+
return addColumn(document, root, target, effectiveSelection, "before", fallbackSelection);
|
|
76
|
+
case "add-column-after":
|
|
77
|
+
return addColumn(document, root, target, effectiveSelection, "after", fallbackSelection);
|
|
78
|
+
case "delete-column":
|
|
79
|
+
return deleteColumn(document, root, target, effectiveSelection, fallbackSelection);
|
|
80
|
+
default:
|
|
81
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function addRow(
|
|
86
|
+
document: CanonicalDocumentEnvelope,
|
|
87
|
+
root: DocumentRootNode,
|
|
88
|
+
table: TableNode,
|
|
89
|
+
selection: TableSelectionDescriptor,
|
|
90
|
+
position: "before" | "after",
|
|
91
|
+
fallbackSelection: SelectionSnapshot,
|
|
92
|
+
): StructuralMutationResult {
|
|
93
|
+
if (!isSimpleTable(table)) {
|
|
94
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const columnCount = getLogicalColumnCount(table);
|
|
98
|
+
const insertIndex =
|
|
99
|
+
position === "before" ? selection.anchorCell.rowIndex : selection.anchorCell.rowIndex + 1;
|
|
100
|
+
const nextRow: TableRowNode = {
|
|
101
|
+
type: "table_row",
|
|
102
|
+
cells: Array.from({ length: columnCount }, () => createEmptyCell()),
|
|
103
|
+
};
|
|
104
|
+
const nextTable: TableNode = {
|
|
105
|
+
...table,
|
|
106
|
+
rows: [
|
|
107
|
+
...table.rows.slice(0, insertIndex),
|
|
108
|
+
nextRow,
|
|
109
|
+
...table.rows.slice(insertIndex),
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return commitTableChange(
|
|
114
|
+
document,
|
|
115
|
+
root,
|
|
116
|
+
selection.tableBlockIndex,
|
|
117
|
+
nextTable,
|
|
118
|
+
fallbackSelection,
|
|
119
|
+
insertIndex,
|
|
120
|
+
selection.anchorCell.columnIndex,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function deleteRow(
|
|
125
|
+
document: CanonicalDocumentEnvelope,
|
|
126
|
+
root: DocumentRootNode,
|
|
127
|
+
table: TableNode,
|
|
128
|
+
selection: TableSelectionDescriptor,
|
|
129
|
+
fallbackSelection: SelectionSnapshot,
|
|
130
|
+
): StructuralMutationResult {
|
|
131
|
+
if (!isSimpleTable(table) || table.rows.length <= 1) {
|
|
132
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const deleteIndex = selection.anchorCell.rowIndex;
|
|
136
|
+
const nextRows = table.rows.filter((_, rowIndex) => rowIndex !== deleteIndex);
|
|
137
|
+
const focusRowIndex = Math.max(0, Math.min(deleteIndex, nextRows.length - 1));
|
|
138
|
+
const nextTable: TableNode = {
|
|
139
|
+
...table,
|
|
140
|
+
rows: nextRows,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return commitTableChange(
|
|
144
|
+
document,
|
|
145
|
+
root,
|
|
146
|
+
selection.tableBlockIndex,
|
|
147
|
+
nextTable,
|
|
148
|
+
fallbackSelection,
|
|
149
|
+
focusRowIndex,
|
|
150
|
+
selection.anchorCell.columnIndex,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function addColumn(
|
|
155
|
+
document: CanonicalDocumentEnvelope,
|
|
156
|
+
root: DocumentRootNode,
|
|
157
|
+
table: TableNode,
|
|
158
|
+
selection: TableSelectionDescriptor,
|
|
159
|
+
position: "before" | "after",
|
|
160
|
+
fallbackSelection: SelectionSnapshot,
|
|
161
|
+
): StructuralMutationResult {
|
|
162
|
+
if (!isSimpleTable(table)) {
|
|
163
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const insertIndex =
|
|
167
|
+
position === "before"
|
|
168
|
+
? selection.anchorCell.columnIndex
|
|
169
|
+
: selection.anchorCell.columnIndex + 1;
|
|
170
|
+
const nextTable: TableNode = {
|
|
171
|
+
...table,
|
|
172
|
+
gridColumns: insertGridColumn(table.gridColumns, insertIndex),
|
|
173
|
+
rows: table.rows.map((row) => ({
|
|
174
|
+
...row,
|
|
175
|
+
cells: [
|
|
176
|
+
...row.cells.slice(0, insertIndex),
|
|
177
|
+
createEmptyCell(),
|
|
178
|
+
...row.cells.slice(insertIndex),
|
|
179
|
+
],
|
|
180
|
+
})),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return commitTableChange(
|
|
184
|
+
document,
|
|
185
|
+
root,
|
|
186
|
+
selection.tableBlockIndex,
|
|
187
|
+
nextTable,
|
|
188
|
+
fallbackSelection,
|
|
189
|
+
selection.anchorCell.rowIndex,
|
|
190
|
+
insertIndex,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function deleteColumn(
|
|
195
|
+
document: CanonicalDocumentEnvelope,
|
|
196
|
+
root: DocumentRootNode,
|
|
197
|
+
table: TableNode,
|
|
198
|
+
selection: TableSelectionDescriptor,
|
|
199
|
+
fallbackSelection: SelectionSnapshot,
|
|
200
|
+
): StructuralMutationResult {
|
|
201
|
+
if (!isSimpleTable(table) || getLogicalColumnCount(table) <= 1) {
|
|
202
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const deleteIndex = selection.anchorCell.columnIndex;
|
|
206
|
+
const nextTable: TableNode = {
|
|
207
|
+
...table,
|
|
208
|
+
gridColumns: deleteGridColumn(table.gridColumns, deleteIndex),
|
|
209
|
+
rows: table.rows.map((row) => ({
|
|
210
|
+
...row,
|
|
211
|
+
cells: row.cells.filter((_, cellIndex) => cellIndex !== deleteIndex),
|
|
212
|
+
})),
|
|
213
|
+
};
|
|
214
|
+
const focusColumnIndex = Math.max(0, Math.min(deleteIndex, getLogicalColumnCount(nextTable) - 1));
|
|
215
|
+
|
|
216
|
+
return commitTableChange(
|
|
217
|
+
document,
|
|
218
|
+
root,
|
|
219
|
+
selection.tableBlockIndex,
|
|
220
|
+
nextTable,
|
|
221
|
+
fallbackSelection,
|
|
222
|
+
selection.anchorCell.rowIndex,
|
|
223
|
+
focusColumnIndex,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function mergeSelectedCells(
|
|
228
|
+
document: CanonicalDocumentEnvelope,
|
|
229
|
+
root: DocumentRootNode,
|
|
230
|
+
table: TableNode,
|
|
231
|
+
selection: TableSelectionDescriptor,
|
|
232
|
+
fallbackSelection: SelectionSnapshot,
|
|
233
|
+
): StructuralMutationResult {
|
|
234
|
+
if (
|
|
235
|
+
!isSimpleTable(table) ||
|
|
236
|
+
selection.selectionKind !== "cell" ||
|
|
237
|
+
selection.rect.bottom - selection.rect.top < 1 ||
|
|
238
|
+
selection.rect.right - selection.rect.left < 1
|
|
239
|
+
) {
|
|
240
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const height = selection.rect.bottom - selection.rect.top;
|
|
244
|
+
const width = selection.rect.right - selection.rect.left;
|
|
245
|
+
if (height === 1 && width === 1) {
|
|
246
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const nextRows = table.rows.map((row) => ({
|
|
250
|
+
...row,
|
|
251
|
+
cells: row.cells.map((cell) => cloneCell(cell)),
|
|
252
|
+
}));
|
|
253
|
+
const mergedChildren: ParagraphNode[] = [];
|
|
254
|
+
|
|
255
|
+
for (let rowIndex = selection.rect.top; rowIndex < selection.rect.bottom; rowIndex += 1) {
|
|
256
|
+
const row = nextRows[rowIndex];
|
|
257
|
+
if (!row) {
|
|
258
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (let columnIndex = selection.rect.left; columnIndex < selection.rect.right; columnIndex += 1) {
|
|
262
|
+
const cellRef = findCellAtColumn(row, columnIndex);
|
|
263
|
+
if (!cellRef) {
|
|
264
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const paragraphs = cellRef.cell.children.filter(
|
|
268
|
+
(child): child is ParagraphNode => child.type === "paragraph",
|
|
269
|
+
);
|
|
270
|
+
mergedChildren.push(...paragraphs.map((paragraph) => structuredClone(paragraph) as ParagraphNode));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const topRow = nextRows[selection.rect.top];
|
|
275
|
+
if (!topRow) {
|
|
276
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
277
|
+
}
|
|
278
|
+
const topLeftRef = findCellAtColumn(topRow, selection.rect.left);
|
|
279
|
+
if (!topLeftRef) {
|
|
280
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
topRow.cells[topLeftRef.cellIndex] = {
|
|
284
|
+
...topLeftRef.cell,
|
|
285
|
+
...(width > 1 ? { gridSpan: width } : {}),
|
|
286
|
+
...(height > 1 ? { verticalMerge: "restart" as const } : {}),
|
|
287
|
+
children: mergedChildren.length > 0 ? mergedChildren : [createEmptyParagraph()],
|
|
288
|
+
};
|
|
289
|
+
topRow.cells.splice(topLeftRef.cellIndex + 1, width - 1);
|
|
290
|
+
|
|
291
|
+
for (let rowIndex = selection.rect.top + 1; rowIndex < selection.rect.bottom; rowIndex += 1) {
|
|
292
|
+
const row = nextRows[rowIndex];
|
|
293
|
+
const leftRef = findCellAtColumn(row, selection.rect.left);
|
|
294
|
+
if (!leftRef) {
|
|
295
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
row.cells.splice(
|
|
299
|
+
leftRef.cellIndex,
|
|
300
|
+
width,
|
|
301
|
+
{
|
|
302
|
+
...createEmptyCell(),
|
|
303
|
+
...(width > 1 ? { gridSpan: width } : {}),
|
|
304
|
+
verticalMerge: "continue",
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const nextTable: TableNode = {
|
|
310
|
+
...table,
|
|
311
|
+
rows: nextRows,
|
|
312
|
+
};
|
|
313
|
+
return commitTableChange(
|
|
314
|
+
document,
|
|
315
|
+
root,
|
|
316
|
+
selection.tableBlockIndex,
|
|
317
|
+
nextTable,
|
|
318
|
+
fallbackSelection,
|
|
319
|
+
selection.rect.top,
|
|
320
|
+
selection.rect.left,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function splitSelectedCell(
|
|
325
|
+
document: CanonicalDocumentEnvelope,
|
|
326
|
+
root: DocumentRootNode,
|
|
327
|
+
table: TableNode,
|
|
328
|
+
selection: TableSelectionDescriptor,
|
|
329
|
+
fallbackSelection: SelectionSnapshot,
|
|
330
|
+
): StructuralMutationResult {
|
|
331
|
+
const row = table.rows[selection.anchorCell.rowIndex];
|
|
332
|
+
if (!row) {
|
|
333
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
334
|
+
}
|
|
335
|
+
const cellRef = findCellAtColumn(row, selection.anchorCell.columnIndex);
|
|
336
|
+
if (!cellRef) {
|
|
337
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const width = Math.max(1, cellRef.cell.gridSpan ?? 1);
|
|
341
|
+
const height = Math.max(
|
|
342
|
+
1,
|
|
343
|
+
computeRowSpan(table, selection.anchorCell.rowIndex, selection.anchorCell.columnIndex, width),
|
|
344
|
+
);
|
|
345
|
+
if (width === 1 && height === 1) {
|
|
346
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const nextRows = table.rows.map((currentRow) => ({
|
|
350
|
+
...currentRow,
|
|
351
|
+
cells: currentRow.cells.map((cell) => cloneCell(cell)),
|
|
352
|
+
}));
|
|
353
|
+
const topRow = nextRows[selection.anchorCell.rowIndex];
|
|
354
|
+
topRow.cells.splice(
|
|
355
|
+
cellRef.cellIndex,
|
|
356
|
+
1,
|
|
357
|
+
stripCellSpan(cellRef.cell),
|
|
358
|
+
...Array.from({ length: width - 1 }, () => createEmptyCell()),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
for (let rowOffset = 1; rowOffset < height; rowOffset += 1) {
|
|
362
|
+
const rowIndex = selection.anchorCell.rowIndex + rowOffset;
|
|
363
|
+
const currentRow = nextRows[rowIndex];
|
|
364
|
+
if (!currentRow) {
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const continueRef = findCellAtColumn(currentRow, selection.anchorCell.columnIndex);
|
|
369
|
+
if (!continueRef) {
|
|
370
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
currentRow.cells.splice(
|
|
374
|
+
continueRef.cellIndex,
|
|
375
|
+
1,
|
|
376
|
+
...Array.from({ length: width }, () => createEmptyCell()),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const nextTable: TableNode = {
|
|
381
|
+
...table,
|
|
382
|
+
rows: nextRows,
|
|
383
|
+
};
|
|
384
|
+
return commitTableChange(
|
|
385
|
+
document,
|
|
386
|
+
root,
|
|
387
|
+
selection.tableBlockIndex,
|
|
388
|
+
nextTable,
|
|
389
|
+
fallbackSelection,
|
|
390
|
+
selection.anchorCell.rowIndex,
|
|
391
|
+
selection.anchorCell.columnIndex,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function setCellBackground(
|
|
396
|
+
document: CanonicalDocumentEnvelope,
|
|
397
|
+
root: DocumentRootNode,
|
|
398
|
+
table: TableNode,
|
|
399
|
+
selection: TableSelectionDescriptor,
|
|
400
|
+
color: string,
|
|
401
|
+
fallbackSelection: SelectionSnapshot,
|
|
402
|
+
): StructuralMutationResult {
|
|
403
|
+
const normalized = normalizeFillColor(color);
|
|
404
|
+
if (!normalized) {
|
|
405
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const targetRect =
|
|
409
|
+
selection.selectionKind === "text"
|
|
410
|
+
? {
|
|
411
|
+
top: 0,
|
|
412
|
+
left: 0,
|
|
413
|
+
bottom: table.rows.length,
|
|
414
|
+
right: getLogicalColumnCount(table),
|
|
415
|
+
}
|
|
416
|
+
: selection.rect;
|
|
417
|
+
let changed = false;
|
|
418
|
+
const nextRows = table.rows.map((row, rowIndex) => {
|
|
419
|
+
if (rowIndex < targetRect.top || rowIndex >= targetRect.bottom) {
|
|
420
|
+
return row;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let cursor = 0;
|
|
424
|
+
const cells = row.cells.map((cell) => {
|
|
425
|
+
const start = cursor;
|
|
426
|
+
const width = cell.gridSpan ?? 1;
|
|
427
|
+
const end = start + width;
|
|
428
|
+
cursor = end;
|
|
429
|
+
|
|
430
|
+
if (end <= targetRect.left || start >= targetRect.right) {
|
|
431
|
+
return cell;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
changed = true;
|
|
435
|
+
return {
|
|
436
|
+
...cell,
|
|
437
|
+
shading: {
|
|
438
|
+
...(cell.shading ?? {}),
|
|
439
|
+
fill: normalized,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
...row,
|
|
446
|
+
cells,
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (!changed) {
|
|
451
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const nextTable: TableNode = {
|
|
455
|
+
...table,
|
|
456
|
+
rows: nextRows,
|
|
457
|
+
};
|
|
458
|
+
return commitTableChange(
|
|
459
|
+
document,
|
|
460
|
+
root,
|
|
461
|
+
selection.tableBlockIndex,
|
|
462
|
+
nextTable,
|
|
463
|
+
fallbackSelection,
|
|
464
|
+
selection.anchorCell.rowIndex,
|
|
465
|
+
selection.anchorCell.columnIndex,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function deleteTableBlock(
|
|
470
|
+
document: CanonicalDocumentEnvelope,
|
|
471
|
+
root: DocumentRootNode,
|
|
472
|
+
tableBlockIndex: number,
|
|
473
|
+
fallbackSelection: SelectionSnapshot,
|
|
474
|
+
): StructuralMutationResult {
|
|
475
|
+
const nextChildren = root.children.filter((_, index) => index !== tableBlockIndex);
|
|
476
|
+
if (nextChildren.length === 0) {
|
|
477
|
+
nextChildren.push(createEmptyParagraph());
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const nextDocument: CanonicalDocumentEnvelope = {
|
|
481
|
+
...document,
|
|
482
|
+
content: {
|
|
483
|
+
...root,
|
|
484
|
+
children: nextChildren,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
const nextSelection =
|
|
488
|
+
findTopLevelParagraphSelectionNearBlock(nextDocument, tableBlockIndex) ??
|
|
489
|
+
fallbackSelection;
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
changed: true,
|
|
493
|
+
document: nextDocument,
|
|
494
|
+
selection: nextSelection,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function commitTableChange(
|
|
499
|
+
document: CanonicalDocumentEnvelope,
|
|
500
|
+
root: DocumentRootNode,
|
|
501
|
+
tableBlockIndex: number,
|
|
502
|
+
nextTable: TableNode,
|
|
503
|
+
fallbackSelection: SelectionSnapshot,
|
|
504
|
+
focusRowIndex: number,
|
|
505
|
+
focusColumnIndex: number,
|
|
506
|
+
): StructuralMutationResult {
|
|
507
|
+
const nextDocument: CanonicalDocumentEnvelope = {
|
|
508
|
+
...document,
|
|
509
|
+
content: {
|
|
510
|
+
...root,
|
|
511
|
+
children: root.children.map((child, index) =>
|
|
512
|
+
index === tableBlockIndex ? nextTable : child,
|
|
513
|
+
),
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
const nextSelection =
|
|
517
|
+
findTableCellParagraphSelection(
|
|
518
|
+
nextDocument,
|
|
519
|
+
tableBlockIndex,
|
|
520
|
+
focusRowIndex,
|
|
521
|
+
focusColumnIndex,
|
|
522
|
+
) ?? fallbackSelection;
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
changed: true,
|
|
526
|
+
document: nextDocument,
|
|
527
|
+
selection: nextSelection,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function resolveTableSelectionFromSnapshot(
|
|
532
|
+
snapshot: Pick<PublicRuntimeRenderSnapshot, "selection" | "surface">,
|
|
533
|
+
): TableSelectionDescriptor | null {
|
|
534
|
+
const selectionFrom = Math.min(snapshot.selection.anchor, snapshot.selection.head);
|
|
535
|
+
const selectionTo = Math.max(snapshot.selection.anchor, snapshot.selection.head);
|
|
536
|
+
const surface = snapshot.surface;
|
|
537
|
+
if (!surface) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
for (let tableBlockIndex = 0; tableBlockIndex < surface.blocks.length; tableBlockIndex += 1) {
|
|
542
|
+
const block = surface.blocks[tableBlockIndex];
|
|
543
|
+
if (block.kind !== "table") {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex += 1) {
|
|
548
|
+
const row = block.rows[rowIndex];
|
|
549
|
+
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
|
|
550
|
+
const cell = row.cells[cellIndex];
|
|
551
|
+
const hit = cell.content.find(
|
|
552
|
+
(child) =>
|
|
553
|
+
child.kind === "paragraph" &&
|
|
554
|
+
selectionFrom >= child.from &&
|
|
555
|
+
selectionTo <= child.to,
|
|
556
|
+
);
|
|
557
|
+
if (!hit) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let logicalColumn = 0;
|
|
562
|
+
for (let currentIndex = 0; currentIndex < cellIndex; currentIndex += 1) {
|
|
563
|
+
logicalColumn += row.cells[currentIndex]?.gridSpan ?? 1;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
tableBlockIndex,
|
|
568
|
+
selectionKind: "text",
|
|
569
|
+
anchorCell: {
|
|
570
|
+
rowIndex,
|
|
571
|
+
columnIndex: logicalColumn,
|
|
572
|
+
},
|
|
573
|
+
headCell: {
|
|
574
|
+
rowIndex,
|
|
575
|
+
columnIndex: logicalColumn,
|
|
576
|
+
},
|
|
577
|
+
rect: {
|
|
578
|
+
top: rowIndex,
|
|
579
|
+
left: logicalColumn,
|
|
580
|
+
bottom: rowIndex + 1,
|
|
581
|
+
right: logicalColumn + (cell.gridSpan ?? 1),
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function toInternalSelectionSnapshot(
|
|
592
|
+
selection: PublicRuntimeRenderSnapshot["selection"],
|
|
593
|
+
): SelectionSnapshot {
|
|
594
|
+
return {
|
|
595
|
+
anchor: selection.anchor,
|
|
596
|
+
head: selection.head,
|
|
597
|
+
isCollapsed: selection.isCollapsed,
|
|
598
|
+
activeRange:
|
|
599
|
+
selection.activeRange.kind === "range"
|
|
600
|
+
? createRangeAnchor(
|
|
601
|
+
selection.activeRange.from,
|
|
602
|
+
selection.activeRange.to,
|
|
603
|
+
selection.activeRange.assoc,
|
|
604
|
+
)
|
|
605
|
+
: selection.activeRange.kind === "node"
|
|
606
|
+
? createNodeAnchor(selection.activeRange.at, selection.activeRange.assoc)
|
|
607
|
+
: createDetachedAnchor(
|
|
608
|
+
selection.activeRange.lastKnownRange,
|
|
609
|
+
selection.activeRange.reason,
|
|
610
|
+
),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function isSimpleTable(table: TableNode): boolean {
|
|
615
|
+
const width = getLogicalColumnCount(table);
|
|
616
|
+
return table.rows.every((row) => {
|
|
617
|
+
let rowWidth = 0;
|
|
618
|
+
for (const cell of row.cells) {
|
|
619
|
+
if ((cell.gridSpan ?? 1) !== 1 || cell.verticalMerge) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
rowWidth += 1;
|
|
623
|
+
}
|
|
624
|
+
return rowWidth === width;
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function getLogicalColumnCount(table: TableNode): number {
|
|
629
|
+
if (table.gridColumns.length > 0) {
|
|
630
|
+
return table.gridColumns.length;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return table.rows.reduce((max, row) => {
|
|
634
|
+
const width = row.cells.reduce((sum, cell) => sum + (cell.gridSpan ?? 1), 0);
|
|
635
|
+
return Math.max(max, width);
|
|
636
|
+
}, 0);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function computeRowSpan(
|
|
640
|
+
table: TableNode,
|
|
641
|
+
rowIndex: number,
|
|
642
|
+
columnIndex: number,
|
|
643
|
+
width: number,
|
|
644
|
+
): number {
|
|
645
|
+
let rowspan = 1;
|
|
646
|
+
|
|
647
|
+
for (let currentRowIndex = rowIndex + 1; currentRowIndex < table.rows.length; currentRowIndex += 1) {
|
|
648
|
+
const cellRef = findCellAtColumn(table.rows[currentRowIndex], columnIndex);
|
|
649
|
+
if (
|
|
650
|
+
!cellRef ||
|
|
651
|
+
cellRef.cell.verticalMerge !== "continue" ||
|
|
652
|
+
(cellRef.cell.gridSpan ?? 1) !== width
|
|
653
|
+
) {
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
rowspan += 1;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return rowspan;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function findCellAtColumn(
|
|
663
|
+
row: TableRowNode,
|
|
664
|
+
logicalColumnIndex: number,
|
|
665
|
+
): { cellIndex: number; cell: TableCellNode } | null {
|
|
666
|
+
let cursor = 0;
|
|
667
|
+
|
|
668
|
+
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
|
|
669
|
+
const cell = row.cells[cellIndex];
|
|
670
|
+
const width = cell.gridSpan ?? 1;
|
|
671
|
+
if (logicalColumnIndex >= cursor && logicalColumnIndex < cursor + width) {
|
|
672
|
+
return {
|
|
673
|
+
cellIndex,
|
|
674
|
+
cell,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
cursor += width;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function createEmptyCell(): TableCellNode {
|
|
684
|
+
return {
|
|
685
|
+
type: "table_cell",
|
|
686
|
+
children: [createEmptyParagraph()],
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function cloneCell(cell: TableCellNode): TableCellNode {
|
|
691
|
+
return structuredClone(cell) as TableCellNode;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function stripCellSpan(cell: TableCellNode): TableCellNode {
|
|
695
|
+
return {
|
|
696
|
+
...cell,
|
|
697
|
+
gridSpan: undefined,
|
|
698
|
+
verticalMerge: undefined,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function insertGridColumn(columns: number[], index: number): number[] {
|
|
703
|
+
if (columns.length === 0) {
|
|
704
|
+
return columns;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const current = columns[index] ?? columns[index - 1] ?? columns[columns.length - 1] ?? 2400;
|
|
708
|
+
return [...columns.slice(0, index), current, ...columns.slice(index)];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function deleteGridColumn(columns: number[], index: number): number[] {
|
|
712
|
+
if (columns.length <= 1) {
|
|
713
|
+
return columns;
|
|
714
|
+
}
|
|
715
|
+
return columns.filter((_, columnIndex) => columnIndex !== index);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function normalizeFillColor(color: string): string | null {
|
|
719
|
+
const normalized = color.trim().replace(/^#/, "");
|
|
720
|
+
return /^[0-9A-Fa-f]{3,8}$/.test(normalized) ? normalized.toUpperCase() : null;
|
|
721
|
+
}
|