@beyondwork/docx-react-component 1.0.14 → 1.0.16

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.
@@ -0,0 +1,567 @@
1
+ import {
2
+ cellKey,
3
+ makeFormulaCell,
4
+ parseCellKey,
5
+ type CachedFormulaValue,
6
+ type CellValue,
7
+ type StyleRef,
8
+ } from "../model/cell.ts";
9
+ import { setCell } from "../model/sheet.ts";
10
+ import type { CanonicalSheet, ColProperties, MergeRange, RowProperties } from "../model/sheet.ts";
11
+ import type { CanonicalWorkbook } from "../model/workbook.ts";
12
+ import {
13
+ cloneWorkbook,
14
+ createEmptyWorkbookMapping,
15
+ createWorkbookSelection,
16
+ createWorkbookTransaction,
17
+ extractFormulaReferenceTokens,
18
+ getRequiredSheet,
19
+ mapSelectionThroughMapping,
20
+ mergeRangeToWorkbookRange,
21
+ normalizeRange,
22
+ normalizeWorkbookSelection,
23
+ rangesIntersect,
24
+ remapFormulasForMutation,
25
+ type WorkbookRange,
26
+ type WorkbookSelection,
27
+ type WorkbookTransaction,
28
+ } from "./workbook-transaction.ts";
29
+
30
+ export type WorkbookCellCommand =
31
+ | {
32
+ type: "cell.set-value";
33
+ sheetId: string;
34
+ row: number;
35
+ col: number;
36
+ value: CellValue;
37
+ }
38
+ | {
39
+ type: "cell.clear";
40
+ sheetId: string;
41
+ row: number;
42
+ col: number;
43
+ }
44
+ | {
45
+ type: "cell.set-formula";
46
+ sheetId: string;
47
+ row: number;
48
+ col: number;
49
+ formula: string;
50
+ referenceTokens?: string[];
51
+ cachedValue?: CachedFormulaValue;
52
+ styleRef?: StyleRef;
53
+ }
54
+ | {
55
+ type: "row.insert";
56
+ sheetId: string;
57
+ row: number;
58
+ count?: number;
59
+ }
60
+ | {
61
+ type: "row.delete";
62
+ sheetId: string;
63
+ row: number;
64
+ count?: number;
65
+ }
66
+ | {
67
+ type: "column.insert";
68
+ sheetId: string;
69
+ col: number;
70
+ count?: number;
71
+ }
72
+ | {
73
+ type: "column.delete";
74
+ sheetId: string;
75
+ col: number;
76
+ count?: number;
77
+ }
78
+ | {
79
+ type: "cells.merge";
80
+ sheetId: string;
81
+ range: WorkbookRange;
82
+ }
83
+ | {
84
+ type: "cells.unmerge";
85
+ sheetId: string;
86
+ range: WorkbookRange;
87
+ };
88
+
89
+ export function applyCellCommand(
90
+ workbook: CanonicalWorkbook,
91
+ selection: WorkbookSelection,
92
+ command: WorkbookCellCommand,
93
+ ): WorkbookTransaction {
94
+ const nextWorkbook = cloneWorkbook(workbook);
95
+ const mapping = createEmptyWorkbookMapping();
96
+
97
+ switch (command.type) {
98
+ case "cell.set-value": {
99
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
100
+ setCell(sheet, command.row, command.col, command.value);
101
+ mapping.operations.push({
102
+ type: "cell-set",
103
+ sheetId: command.sheetId,
104
+ row: command.row,
105
+ col: command.col,
106
+ });
107
+ return createWorkbookTransaction(
108
+ nextWorkbook,
109
+ normalizeWorkbookSelection(
110
+ nextWorkbook,
111
+ createWorkbookSelection(command.sheetId, command.row, command.col),
112
+ ),
113
+ mapping,
114
+ "cell.set-value",
115
+ );
116
+ }
117
+ case "cell.clear": {
118
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
119
+ setCell(sheet, command.row, command.col, undefined);
120
+ mapping.operations.push({
121
+ type: "cell-set",
122
+ sheetId: command.sheetId,
123
+ row: command.row,
124
+ col: command.col,
125
+ });
126
+ return createWorkbookTransaction(
127
+ nextWorkbook,
128
+ normalizeWorkbookSelection(
129
+ nextWorkbook,
130
+ createWorkbookSelection(command.sheetId, command.row, command.col),
131
+ ),
132
+ mapping,
133
+ "cell.clear",
134
+ );
135
+ }
136
+ case "cell.set-formula": {
137
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
138
+ const referenceTokens = command.referenceTokens ?? extractFormulaReferenceTokens(command.formula);
139
+ setCell(
140
+ sheet,
141
+ command.row,
142
+ command.col,
143
+ makeFormulaCell(command.formula, command.cachedValue, command.styleRef, referenceTokens),
144
+ );
145
+ mapping.operations.push({
146
+ type: "cell-set",
147
+ sheetId: command.sheetId,
148
+ row: command.row,
149
+ col: command.col,
150
+ });
151
+ return createWorkbookTransaction(
152
+ nextWorkbook,
153
+ normalizeWorkbookSelection(
154
+ nextWorkbook,
155
+ createWorkbookSelection(command.sheetId, command.row, command.col),
156
+ ),
157
+ mapping,
158
+ "cell.set-formula",
159
+ );
160
+ }
161
+ case "row.insert": {
162
+ const count = normalizeCount(command.count);
163
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
164
+ shiftRows(sheet, command.row, count, "insert");
165
+ mapping.operations.push({
166
+ type: "row-insert",
167
+ sheetId: command.sheetId,
168
+ index: command.row,
169
+ count,
170
+ });
171
+ remapFormulasForMutation(
172
+ nextWorkbook,
173
+ command.sheetId,
174
+ { axis: "row", kind: "insert", index: command.row, count },
175
+ mapping,
176
+ );
177
+ return createWorkbookTransaction(
178
+ nextWorkbook,
179
+ normalizeWorkbookSelection(
180
+ nextWorkbook,
181
+ mapSelectionThroughMapping(selection, mapping),
182
+ ),
183
+ mapping,
184
+ "row.insert",
185
+ );
186
+ }
187
+ case "row.delete": {
188
+ const count = normalizeCount(command.count);
189
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
190
+ shiftRows(sheet, command.row, count, "delete");
191
+ mapping.operations.push({
192
+ type: "row-delete",
193
+ sheetId: command.sheetId,
194
+ index: command.row,
195
+ count,
196
+ });
197
+ remapFormulasForMutation(
198
+ nextWorkbook,
199
+ command.sheetId,
200
+ { axis: "row", kind: "delete", index: command.row, count },
201
+ mapping,
202
+ );
203
+ return createWorkbookTransaction(
204
+ nextWorkbook,
205
+ normalizeWorkbookSelection(
206
+ nextWorkbook,
207
+ mapSelectionThroughMapping(selection, mapping),
208
+ ),
209
+ mapping,
210
+ "row.delete",
211
+ );
212
+ }
213
+ case "column.insert": {
214
+ const count = normalizeCount(command.count);
215
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
216
+ shiftColumns(sheet, command.col, count, "insert");
217
+ mapping.operations.push({
218
+ type: "column-insert",
219
+ sheetId: command.sheetId,
220
+ index: command.col,
221
+ count,
222
+ });
223
+ remapFormulasForMutation(
224
+ nextWorkbook,
225
+ command.sheetId,
226
+ { axis: "col", kind: "insert", index: command.col, count },
227
+ mapping,
228
+ );
229
+ return createWorkbookTransaction(
230
+ nextWorkbook,
231
+ normalizeWorkbookSelection(
232
+ nextWorkbook,
233
+ mapSelectionThroughMapping(selection, mapping),
234
+ ),
235
+ mapping,
236
+ "column.insert",
237
+ );
238
+ }
239
+ case "column.delete": {
240
+ const count = normalizeCount(command.count);
241
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
242
+ shiftColumns(sheet, command.col, count, "delete");
243
+ mapping.operations.push({
244
+ type: "column-delete",
245
+ sheetId: command.sheetId,
246
+ index: command.col,
247
+ count,
248
+ });
249
+ remapFormulasForMutation(
250
+ nextWorkbook,
251
+ command.sheetId,
252
+ { axis: "col", kind: "delete", index: command.col, count },
253
+ mapping,
254
+ );
255
+ return createWorkbookTransaction(
256
+ nextWorkbook,
257
+ normalizeWorkbookSelection(
258
+ nextWorkbook,
259
+ mapSelectionThroughMapping(selection, mapping),
260
+ ),
261
+ mapping,
262
+ "column.delete",
263
+ );
264
+ }
265
+ case "cells.merge": {
266
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
267
+ const range = normalizeRange(command.range);
268
+ assertNonIntersectingMerge(sheet, range);
269
+ clearMergedInteriorCells(sheet, range);
270
+ sheet.merges = [
271
+ ...sheet.merges,
272
+ {
273
+ startRow: range.startRow,
274
+ startCol: range.startCol,
275
+ endRow: range.endRow,
276
+ endCol: range.endCol,
277
+ },
278
+ ];
279
+ mapping.operations.push({
280
+ type: "merge-add",
281
+ sheetId: command.sheetId,
282
+ range,
283
+ });
284
+ return createWorkbookTransaction(
285
+ nextWorkbook,
286
+ normalizeWorkbookSelection(nextWorkbook, {
287
+ mode: "cell",
288
+ sheetId: command.sheetId,
289
+ activeCell: { row: range.startRow, col: range.startCol },
290
+ range,
291
+ }),
292
+ mapping,
293
+ "cells.merge",
294
+ );
295
+ }
296
+ case "cells.unmerge": {
297
+ const sheet = getRequiredSheet(nextWorkbook, command.sheetId);
298
+ const range = normalizeRange(command.range);
299
+ const removed = sheet.merges.filter((merge) => rangesIntersect(range, mergeRangeToWorkbookRange(merge)));
300
+ sheet.merges = sheet.merges.filter((merge) => !rangesIntersect(range, mergeRangeToWorkbookRange(merge)));
301
+ for (const merge of removed) {
302
+ mapping.operations.push({
303
+ type: "merge-remove",
304
+ sheetId: command.sheetId,
305
+ range: mergeRangeToWorkbookRange(merge),
306
+ });
307
+ }
308
+ return createWorkbookTransaction(
309
+ nextWorkbook,
310
+ normalizeWorkbookSelection(nextWorkbook, {
311
+ mode: "cell",
312
+ sheetId: command.sheetId,
313
+ activeCell: { row: range.startRow, col: range.startCol },
314
+ range,
315
+ }),
316
+ mapping,
317
+ "cells.unmerge",
318
+ );
319
+ }
320
+ }
321
+ }
322
+
323
+ function normalizeCount(value: number | undefined): number {
324
+ const next = Math.floor(value ?? 1);
325
+ return Number.isFinite(next) && next > 0 ? next : 1;
326
+ }
327
+
328
+ function shiftRows(
329
+ sheet: CanonicalSheet,
330
+ row: number,
331
+ count: number,
332
+ mode: "insert" | "delete",
333
+ ): void {
334
+ const nextCells = new Map<string, CellValue>();
335
+ for (const [key, value] of sheet.cells) {
336
+ const address = parseCellKey(key);
337
+ const nextRow = mode === "insert"
338
+ ? remapInsertedIndex(address.row, row, count)
339
+ : remapDeletedIndex(address.row, row, count);
340
+ if (nextRow === null) {
341
+ continue;
342
+ }
343
+ nextCells.set(cellKey(nextRow, address.col), value);
344
+ }
345
+ sheet.cells = nextCells;
346
+
347
+ const nextRowProps = new Map<number, RowProperties>();
348
+ for (const [rowIndex, props] of sheet.rowProps) {
349
+ const nextRow = mode === "insert"
350
+ ? remapInsertedIndex(rowIndex, row, count)
351
+ : remapDeletedIndex(rowIndex, row, count);
352
+ if (nextRow === null) {
353
+ continue;
354
+ }
355
+ nextRowProps.set(nextRow, {
356
+ ...props,
357
+ rowIndex: nextRow,
358
+ });
359
+ }
360
+ sheet.rowProps = nextRowProps;
361
+ sheet.merges = sheet.merges
362
+ .map((merge) => remapMergeRows(merge, row, count, mode))
363
+ .filter((merge): merge is MergeRange => merge !== null);
364
+
365
+ if (sheet.paneState && row <= sheet.paneState.frozenRows) {
366
+ sheet.paneState = {
367
+ ...sheet.paneState,
368
+ frozenRows: mode === "insert"
369
+ ? sheet.paneState.frozenRows + count
370
+ : Math.max(0, sheet.paneState.frozenRows - countWithin(sheet.paneState.frozenRows, row, count)),
371
+ };
372
+ }
373
+ }
374
+
375
+ function shiftColumns(
376
+ sheet: CanonicalSheet,
377
+ col: number,
378
+ count: number,
379
+ mode: "insert" | "delete",
380
+ ): void {
381
+ const nextCells = new Map<string, CellValue>();
382
+ for (const [key, value] of sheet.cells) {
383
+ const address = parseCellKey(key);
384
+ const nextCol = mode === "insert"
385
+ ? remapInsertedIndex(address.col, col, count)
386
+ : remapDeletedIndex(address.col, col, count);
387
+ if (nextCol === null) {
388
+ continue;
389
+ }
390
+ nextCells.set(cellKey(address.row, nextCol), value);
391
+ }
392
+ sheet.cells = nextCells;
393
+
394
+ const nextColProps = new Map<number, ColProperties>();
395
+ for (const [colIndex, props] of sheet.colProps) {
396
+ const nextCol = mode === "insert"
397
+ ? remapInsertedIndex(colIndex, col, count)
398
+ : remapDeletedIndex(colIndex, col, count);
399
+ if (nextCol === null) {
400
+ continue;
401
+ }
402
+ nextColProps.set(nextCol, {
403
+ ...props,
404
+ colIndex: nextCol,
405
+ });
406
+ }
407
+ sheet.colProps = nextColProps;
408
+ sheet.merges = sheet.merges
409
+ .map((merge) => remapMergeColumns(merge, col, count, mode))
410
+ .filter((merge): merge is MergeRange => merge !== null);
411
+
412
+ if (sheet.paneState && col <= sheet.paneState.frozenCols) {
413
+ sheet.paneState = {
414
+ ...sheet.paneState,
415
+ frozenCols: mode === "insert"
416
+ ? sheet.paneState.frozenCols + count
417
+ : Math.max(0, sheet.paneState.frozenCols - countWithin(sheet.paneState.frozenCols, col, count)),
418
+ };
419
+ }
420
+ }
421
+
422
+ function remapInsertedIndex(value: number, index: number, count: number): number {
423
+ return value >= index ? value + count : value;
424
+ }
425
+
426
+ function remapDeletedIndex(value: number, index: number, count: number): number | null {
427
+ const deleteEnd = index + count - 1;
428
+ if (value < index) {
429
+ return value;
430
+ }
431
+ if (value <= deleteEnd) {
432
+ return null;
433
+ }
434
+ return value - count;
435
+ }
436
+
437
+ function remapMergeRows(
438
+ merge: MergeRange,
439
+ row: number,
440
+ count: number,
441
+ mode: "insert" | "delete",
442
+ ): MergeRange | null {
443
+ if (mode === "insert") {
444
+ if (row <= merge.startRow) {
445
+ return {
446
+ ...merge,
447
+ startRow: merge.startRow + count,
448
+ endRow: merge.endRow + count,
449
+ };
450
+ }
451
+ if (row <= merge.endRow) {
452
+ return {
453
+ ...merge,
454
+ endRow: merge.endRow + count,
455
+ };
456
+ }
457
+ return merge;
458
+ }
459
+
460
+ const rowInterval = remapDeletedInterval(merge.startRow, merge.endRow, row, count);
461
+ if (!rowInterval) {
462
+ return null;
463
+ }
464
+ return {
465
+ ...merge,
466
+ startRow: rowInterval.start,
467
+ endRow: rowInterval.end,
468
+ };
469
+ }
470
+
471
+ function remapMergeColumns(
472
+ merge: MergeRange,
473
+ col: number,
474
+ count: number,
475
+ mode: "insert" | "delete",
476
+ ): MergeRange | null {
477
+ if (mode === "insert") {
478
+ if (col <= merge.startCol) {
479
+ return {
480
+ ...merge,
481
+ startCol: merge.startCol + count,
482
+ endCol: merge.endCol + count,
483
+ };
484
+ }
485
+ if (col <= merge.endCol) {
486
+ return {
487
+ ...merge,
488
+ endCol: merge.endCol + count,
489
+ };
490
+ }
491
+ return merge;
492
+ }
493
+
494
+ const colInterval = remapDeletedInterval(merge.startCol, merge.endCol, col, count);
495
+ if (!colInterval) {
496
+ return null;
497
+ }
498
+ return {
499
+ ...merge,
500
+ startCol: colInterval.start,
501
+ endCol: colInterval.end,
502
+ };
503
+ }
504
+
505
+ function remapDeletedInterval(
506
+ start: number,
507
+ end: number,
508
+ index: number,
509
+ count: number,
510
+ ): { start: number; end: number } | null {
511
+ const deleteEnd = index + count - 1;
512
+ const overlapStart = Math.max(start, index);
513
+ const overlapEnd = Math.min(end, deleteEnd);
514
+ if (overlapStart <= overlapEnd && overlapEnd - overlapStart + 1 >= end - start + 1) {
515
+ return null;
516
+ }
517
+
518
+ let nextStart = start;
519
+ let nextEnd = end;
520
+ if (start >= index && start <= deleteEnd) {
521
+ nextStart = index;
522
+ } else if (start > deleteEnd) {
523
+ nextStart = start - count;
524
+ }
525
+
526
+ if (end >= index && end <= deleteEnd) {
527
+ nextEnd = index - 1;
528
+ } else if (end > deleteEnd) {
529
+ nextEnd = end - count;
530
+ }
531
+
532
+ if (nextEnd < nextStart) {
533
+ nextEnd = nextStart;
534
+ }
535
+
536
+ return { start: nextStart, end: nextEnd };
537
+ }
538
+
539
+ function countWithin(value: number, index: number, count: number): number {
540
+ const deleteEnd = index + count - 1;
541
+ if (value < index) {
542
+ return 0;
543
+ }
544
+ if (value <= deleteEnd) {
545
+ return value - index + 1;
546
+ }
547
+ return count;
548
+ }
549
+
550
+ function assertNonIntersectingMerge(sheet: CanonicalSheet, range: WorkbookRange): void {
551
+ for (const merge of sheet.merges) {
552
+ if (rangesIntersect(range, mergeRangeToWorkbookRange(merge))) {
553
+ throw new Error("Cannot create an overlapping merge range.");
554
+ }
555
+ }
556
+ }
557
+
558
+ function clearMergedInteriorCells(sheet: CanonicalSheet, range: WorkbookRange): void {
559
+ for (let row = range.startRow; row <= range.endRow; row += 1) {
560
+ for (let col = range.startCol; col <= range.endCol; col += 1) {
561
+ if (row === range.startRow && col === range.startCol) {
562
+ continue;
563
+ }
564
+ setCell(sheet, row, col, undefined);
565
+ }
566
+ }
567
+ }