@beyondwork/docx-react-component 1.0.14 → 1.0.15

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.14",
4
+ "version": "1.0.15",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -33,10 +33,18 @@
33
33
  "types": "./src/api/public-types.ts",
34
34
  "import": "./src/api/public-types.ts"
35
35
  },
36
+ "./compare": {
37
+ "types": "./src/compare/index.ts",
38
+ "import": "./src/compare/index.ts"
39
+ },
36
40
  "./io/docx-session": {
37
41
  "types": "./src/io/docx-session.ts",
38
42
  "import": "./src/io/docx-session.ts"
39
43
  },
44
+ "./legal": {
45
+ "types": "./src/legal/index.ts",
46
+ "import": "./src/legal/index.ts"
47
+ },
40
48
  "./runtime/document-runtime": {
41
49
  "types": "./src/runtime/document-runtime.ts",
42
50
  "import": "./src/runtime/document-runtime.ts"
@@ -90,7 +98,7 @@
90
98
  "tailwindcss": "^4.2.2"
91
99
  },
92
100
  "devDependencies": {
93
- "@chllming/wave-orchestration": "^0.9.13",
101
+ "@chllming/wave-orchestration": "^0.9.15",
94
102
  "@types/react": "19.2.14",
95
103
  "@types/react-dom": "19.2.3",
96
104
  "@typescript/native-preview": "7.0.0-dev.20260409.1",
@@ -571,16 +571,21 @@ export interface WordReviewEditorRef {
571
571
  reopenComment(commentId: string): void;
572
572
  addCommentReply(commentId: string, body: string): void;
573
573
  editCommentBody(commentId: string, body: string): void;
574
+ deleteComment(commentId: string): void;
574
575
  acceptChange(changeId: string): void;
575
576
  rejectChange(changeId: string): void;
576
577
  acceptAllChanges(): void;
577
578
  rejectAllChanges(): void;
578
579
  exportDocx(options?: ExportDocxOptions): Promise<ExportResult>;
579
580
  getSnapshot(): PersistedEditorSnapshot;
581
+ getRenderSnapshot(): RuntimeRenderSnapshot;
580
582
  getCompatibilityReport(): CompatibilityReport;
581
583
  getWarnings(): EditorWarning[];
584
+ getCommentSidebarSnapshot(): CommentSidebarSnapshot;
585
+ getTrackedChangesSnapshot(): TrackedChangesSnapshot;
582
586
  getComments(): CommentSidebarSnapshot;
583
587
  getTrackedChanges(): TrackedChangesSnapshot;
588
+ isDirty(): boolean;
584
589
  getFormattingState(): FormattingStateSnapshot;
585
590
  toggleBold(): void;
586
591
  toggleItalic(): void;
@@ -610,6 +615,7 @@ export interface WordReviewEditorRef {
610
615
  setCellBackground(color: string): void;
611
616
  search(query: string, options?: SearchOptions): SearchResultSnapshot[];
612
617
  clearSearch(): void;
618
+ setSelection(selection: SelectionSnapshot | null): void;
613
619
  scrollToRevision(revisionId: string): void;
614
620
  scrollToComment(commentId: string): void;
615
621
  }
@@ -20,6 +20,7 @@ export interface CompareVersionRef {
20
20
  export type CompareChangeKind =
21
21
  | "paragraph-insertion"
22
22
  | "paragraph-deletion"
23
+ | "paragraph-modification"
23
24
  | "structural-insertion"
24
25
  | "structural-deletion";
25
26
 
@@ -96,6 +97,22 @@ export function compareDocumentSnapshots(
96
97
  const insertBlock = nextTargetIndex < match.targetIndex ? targetBlocks[nextTargetIndex] : undefined;
97
98
 
98
99
  if (deleteBlock && insertBlock) {
100
+ if (deleteBlock.type === "paragraph" && insertBlock.type === "paragraph") {
101
+ const modificationId = `change-${nextChangeNumber}`;
102
+ nextChangeNumber += 1;
103
+ addParagraphModificationEntry(
104
+ buildEntries,
105
+ changes,
106
+ modificationId,
107
+ deleteBlock,
108
+ insertBlock,
109
+ nextTargetIndex,
110
+ );
111
+ nextBaseIndex += 1;
112
+ nextTargetIndex += 1;
113
+ continue;
114
+ }
115
+
99
116
  const deletionId = `change-${nextChangeNumber}`;
100
117
  nextChangeNumber += 1;
101
118
  addTrackedOrStructuralEntry(
@@ -153,13 +170,34 @@ export function compareDocumentSnapshots(
153
170
  }
154
171
 
155
172
  if (match.baseIndex < baseBlocks.length && match.targetIndex < targetBlocks.length) {
156
- buildEntries.push({
157
- block: cloneBlock(targetBlocks[match.targetIndex]),
158
- tracked: false,
159
- blockType: targetBlocks[match.targetIndex].type,
160
- beforeText: getBlockDisplayText(baseBlocks[match.baseIndex]),
161
- afterText: getBlockDisplayText(targetBlocks[match.targetIndex]),
162
- });
173
+ const baseText = getBlockDisplayText(baseBlocks[match.baseIndex]);
174
+ const targetText = getBlockDisplayText(targetBlocks[match.targetIndex]);
175
+ const isModified =
176
+ baseBlocks[match.baseIndex].type === "paragraph" &&
177
+ targetBlocks[match.targetIndex].type === "paragraph" &&
178
+ baseText !== targetText;
179
+
180
+ if (isModified) {
181
+ const modChangeId = `change-${nextChangeNumber}`;
182
+ nextChangeNumber += 1;
183
+ addParagraphModificationEntry(
184
+ buildEntries,
185
+ changes,
186
+ modChangeId,
187
+ baseBlocks[match.baseIndex] as ParagraphNode,
188
+ targetBlocks[match.targetIndex] as ParagraphNode,
189
+ match.targetIndex,
190
+ );
191
+ } else {
192
+ buildEntries.push({
193
+ block: cloneBlock(targetBlocks[match.targetIndex]),
194
+ tracked: false,
195
+ blockType: targetBlocks[match.targetIndex].type,
196
+ beforeText: baseText,
197
+ afterText: targetText,
198
+ });
199
+ }
200
+
163
201
  nextBaseIndex = match.baseIndex + 1;
164
202
  nextTargetIndex = match.targetIndex + 1;
165
203
  }
@@ -222,6 +260,45 @@ function addTrackedOrStructuralEntry(
222
260
  });
223
261
  }
224
262
 
263
+ function addParagraphModificationEntry(
264
+ buildEntries: BuildEntry[],
265
+ changes: CompareChange[],
266
+ changeId: string,
267
+ baseParagraph: ParagraphNode,
268
+ targetParagraph: ParagraphNode,
269
+ targetIndex: number,
270
+ ): void {
271
+ const beforeText = getBlockDisplayText(baseParagraph);
272
+ const afterText = getBlockDisplayText(targetParagraph);
273
+
274
+ buildEntries.push({
275
+ changeId: `${changeId}-before`,
276
+ block: cloneBlock(baseParagraph),
277
+ trackChange: "deletion",
278
+ tracked: true,
279
+ blockType: "paragraph",
280
+ beforeText,
281
+ });
282
+ buildEntries.push({
283
+ changeId: `${changeId}-after`,
284
+ block: cloneBlock(targetParagraph),
285
+ trackChange: "insertion",
286
+ tracked: true,
287
+ blockType: "paragraph",
288
+ afterText,
289
+ });
290
+
291
+ changes.push({
292
+ changeId,
293
+ kind: "paragraph-modification",
294
+ tracked: true,
295
+ blockType: "paragraph",
296
+ beforeText,
297
+ afterText,
298
+ targetIndex,
299
+ });
300
+ }
301
+
225
302
  function buildComparedDocument(
226
303
  base: CanonicalDocument,
227
304
  target: CanonicalDocument,
@@ -0,0 +1,25 @@
1
+ export {
2
+ compareDocumentSnapshots,
3
+ type CompareChange,
4
+ type CompareChangeKind,
5
+ type CompareDocumentVersionsOptions,
6
+ type CompareVersionRef,
7
+ type VersionCompareResult,
8
+ } from "./diff-engine.ts";
9
+
10
+ export {
11
+ exportComparedDocumentRedlines,
12
+ type ExportComparedDocumentOptions,
13
+ type ExportComparedDocumentResult,
14
+ } from "./export-redlines.ts";
15
+
16
+ export {
17
+ buildVersionAuditTrail,
18
+ createDocumentVersionSnapshot,
19
+ createDocumentVersionSnapshotId,
20
+ lockDocumentVersionSnapshot,
21
+ saveDocumentVersionSnapshot,
22
+ type CreateDocumentVersionSnapshotOptions,
23
+ type DocumentVersionSnapshot,
24
+ type VersionAuditEntry,
25
+ } from "./snapshot.ts";
@@ -53,6 +53,37 @@ export function saveDocumentVersionSnapshot(
53
53
  return next.sort(compareDocumentVersionSnapshots);
54
54
  }
55
55
 
56
+ export interface VersionAuditEntry {
57
+ versionId: string;
58
+ name: string;
59
+ createdAt: string;
60
+ documentSignature: string;
61
+ action: "created" | "compared" | "locked";
62
+ }
63
+
64
+ export function buildVersionAuditTrail(
65
+ snapshots: readonly DocumentVersionSnapshot[],
66
+ ): VersionAuditEntry[] {
67
+ return snapshots.map((snapshot) => ({
68
+ versionId: snapshot.versionId,
69
+ name: snapshot.name,
70
+ createdAt: snapshot.createdAt,
71
+ documentSignature: snapshot.documentSignature,
72
+ action: "created",
73
+ }));
74
+ }
75
+
76
+ export function lockDocumentVersionSnapshot(
77
+ snapshot: DocumentVersionSnapshot,
78
+ ): DocumentVersionSnapshot {
79
+ return {
80
+ ...snapshot,
81
+ name: snapshot.name.startsWith("[locked] ")
82
+ ? snapshot.name
83
+ : `[locked] ${snapshot.name}`,
84
+ };
85
+ }
86
+
56
87
  function compareDocumentVersionSnapshots(
57
88
  left: DocumentVersionSnapshot,
58
89
  right: DocumentVersionSnapshot,
@@ -0,0 +1,72 @@
1
+ import type { CanonicalWorkbook } from "../model/workbook.ts";
2
+ import { listSheets } from "../model/workbook.ts";
3
+
4
+ const SHARED_STRINGS_NAMESPACE = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
5
+
6
+ export interface SharedStringTableSerialization {
7
+ xml: string;
8
+ strings: string[];
9
+ indexByValue: Map<string, number>;
10
+ totalCount: number;
11
+ }
12
+
13
+ export function buildSharedStringsTable(
14
+ workbook: CanonicalWorkbook,
15
+ ): SharedStringTableSerialization {
16
+ const strings: string[] = [];
17
+ const indexByValue = new Map<string, number>();
18
+ let totalCount = 0;
19
+
20
+ for (const sheet of listSheets(workbook)) {
21
+ for (const cell of sheet.cells.values()) {
22
+ if (cell.kind !== "text") {
23
+ continue;
24
+ }
25
+
26
+ totalCount += 1;
27
+
28
+ if (!indexByValue.has(cell.value)) {
29
+ indexByValue.set(cell.value, strings.length);
30
+ strings.push(cell.value);
31
+ }
32
+ }
33
+ }
34
+
35
+ return {
36
+ xml: serializeSharedStringsXml(strings, totalCount),
37
+ strings,
38
+ indexByValue,
39
+ totalCount,
40
+ };
41
+ }
42
+
43
+ export function serializeSharedStringsXml(
44
+ strings: readonly string[],
45
+ totalCount: number = strings.length,
46
+ ): string {
47
+ const items = strings.map(serializeSharedStringItem);
48
+ const body = items.length > 0 ? `\n${items.join("\n")}\n` : "";
49
+
50
+ return [
51
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
52
+ `<sst xmlns="${SHARED_STRINGS_NAMESPACE}" count="${totalCount}" uniqueCount="${strings.length}">${body}</sst>`,
53
+ ].join("\n");
54
+ }
55
+
56
+ function serializeSharedStringItem(value: string): string {
57
+ const preserveSpace = requiresPreservedSpace(value) ? ` xml:space="preserve"` : "";
58
+ return ` <si><t${preserveSpace}>${escapeXml(value)}</t></si>`;
59
+ }
60
+
61
+ function requiresPreservedSpace(value: string): boolean {
62
+ return /^\s/.test(value) || /\s$/.test(value);
63
+ }
64
+
65
+ function escapeXml(value: string): string {
66
+ return value
67
+ .replace(/&/g, "&amp;")
68
+ .replace(/"/g, "&quot;")
69
+ .replace(/</g, "&lt;")
70
+ .replace(/>/g, "&gt;")
71
+ .replace(/'/g, "&apos;");
72
+ }
@@ -0,0 +1,333 @@
1
+ import type { CellKey, CellValue, FormulaCell } from "../model/cell.ts";
2
+ import { parseCellKey } from "../model/cell.ts";
3
+ import type { CanonicalSheet, MergeRange, SheetPaneState } from "../model/sheet.ts";
4
+
5
+ const WORKSHEET_NAMESPACE = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
6
+
7
+ export interface SheetSerializationOptions {
8
+ sharedStringIndexByValue?: ReadonlyMap<string, number>;
9
+ }
10
+
11
+ export function serializeSheetXml(
12
+ sheet: CanonicalSheet,
13
+ options: SheetSerializationOptions = {},
14
+ ): string {
15
+ const rows = groupCellsByRow(sheet);
16
+ const rowIndices = new Set<number>([
17
+ ...rows.keys(),
18
+ ...sheet.rowProps.keys(),
19
+ ]);
20
+ const sortedRowIndices = [...rowIndices].sort((left, right) => left - right);
21
+ const lines = [
22
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
23
+ `<worksheet xmlns="${WORKSHEET_NAMESPACE}">`,
24
+ ` <dimension ref="${computeDimensionRef(sheet)}"/>`,
25
+ ...serializeSheetViews(sheet.paneState),
26
+ ...serializeColumns(sheet),
27
+ ];
28
+
29
+ if (sortedRowIndices.length === 0) {
30
+ lines.push(` <sheetData/>`);
31
+ } else {
32
+ lines.push(` <sheetData>`);
33
+ for (const rowIndex of sortedRowIndices) {
34
+ const rowProps = sheet.rowProps.get(rowIndex);
35
+ const rowCells = rows.get(rowIndex) ?? [];
36
+ const rowAttributes = [
37
+ `r="${rowIndex + 1}"`,
38
+ ];
39
+
40
+ if (rowProps?.heightPt !== undefined) {
41
+ rowAttributes.push(`ht="${formatNumber(rowProps.heightPt)}"`);
42
+ }
43
+ if (rowProps?.hidden) {
44
+ rowAttributes.push(`hidden="1"`);
45
+ }
46
+ if (rowProps?.customHeight) {
47
+ rowAttributes.push(`customHeight="1"`);
48
+ }
49
+ if (rowProps?.styleRef) {
50
+ rowAttributes.push(`s="${rowProps.styleRef.xfIndex}"`, `customFormat="1"`);
51
+ }
52
+
53
+ if (rowCells.length === 0) {
54
+ lines.push(` <row ${rowAttributes.join(" ")}></row>`);
55
+ continue;
56
+ }
57
+
58
+ lines.push(` <row ${rowAttributes.join(" ")}>`);
59
+ for (const cellEntry of rowCells) {
60
+ lines.push(` ${serializeCellXml(cellEntry, options.sharedStringIndexByValue)}`);
61
+ }
62
+ lines.push(` </row>`);
63
+ }
64
+ lines.push(` </sheetData>`);
65
+ }
66
+
67
+ if (sheet.merges.length > 0) {
68
+ lines.push(` <mergeCells count="${sheet.merges.length}">`);
69
+ for (const merge of [...sheet.merges].sort(compareMergeRanges)) {
70
+ lines.push(` <mergeCell ref="${formatMergeRef(merge)}"/>`);
71
+ }
72
+ lines.push(` </mergeCells>`);
73
+ }
74
+
75
+ lines.push(`</worksheet>`);
76
+ return lines.join("\n");
77
+ }
78
+
79
+ function groupCellsByRow(
80
+ sheet: CanonicalSheet,
81
+ ): Map<number, Array<[CellKey, CellValue]>> {
82
+ const rows = new Map<number, Array<[CellKey, CellValue]>>();
83
+ const sortedCells = [...sheet.cells.entries()].sort(
84
+ ([leftKey], [rightKey]) => compareCellKeys(leftKey, rightKey),
85
+ );
86
+
87
+ for (const entry of sortedCells) {
88
+ const { row } = parseCellKey(entry[0]);
89
+ const rowEntries = rows.get(row) ?? [];
90
+ rowEntries.push(entry);
91
+ rows.set(row, rowEntries);
92
+ }
93
+
94
+ return rows;
95
+ }
96
+
97
+ function serializeSheetViews(paneState: SheetPaneState | undefined): string[] {
98
+ if (!paneState || (paneState.frozenRows <= 0 && paneState.frozenCols <= 0)) {
99
+ return [];
100
+ }
101
+
102
+ const paneAttributes = [
103
+ `state="frozen"`,
104
+ `topLeftCell="${formatCellRef(paneState.frozenRows, paneState.frozenCols)}"`,
105
+ `activePane="${resolveActivePane(paneState)}"`,
106
+ ];
107
+
108
+ if (paneState.frozenCols > 0) {
109
+ paneAttributes.push(`xSplit="${paneState.frozenCols}"`);
110
+ }
111
+ if (paneState.frozenRows > 0) {
112
+ paneAttributes.push(`ySplit="${paneState.frozenRows}"`);
113
+ }
114
+
115
+ return [
116
+ ` <sheetViews>`,
117
+ ` <sheetView workbookViewId="0">`,
118
+ ` <pane ${paneAttributes.join(" ")}/>`,
119
+ ` </sheetView>`,
120
+ ` </sheetViews>`,
121
+ ];
122
+ }
123
+
124
+ function resolveActivePane(paneState: SheetPaneState): string {
125
+ if (paneState.frozenRows > 0 && paneState.frozenCols > 0) {
126
+ return "bottomRight";
127
+ }
128
+ if (paneState.frozenRows > 0) {
129
+ return "bottomLeft";
130
+ }
131
+ return "topRight";
132
+ }
133
+
134
+ function serializeColumns(sheet: CanonicalSheet): string[] {
135
+ const columns = [...sheet.colProps.values()].sort(
136
+ (left, right) => left.colIndex - right.colIndex,
137
+ );
138
+ if (columns.length === 0) {
139
+ return [];
140
+ }
141
+
142
+ const lines = [` <cols>`];
143
+ for (const column of columns) {
144
+ const attributes = [
145
+ `min="${column.colIndex + 1}"`,
146
+ `max="${column.colIndex + 1}"`,
147
+ ];
148
+
149
+ if (column.widthChars !== undefined) {
150
+ attributes.push(`width="${formatNumber(column.widthChars)}"`);
151
+ }
152
+ if (column.hidden) {
153
+ attributes.push(`hidden="1"`);
154
+ }
155
+ if (column.customWidth) {
156
+ attributes.push(`customWidth="1"`);
157
+ }
158
+ if (column.styleRef) {
159
+ attributes.push(`style="${column.styleRef.xfIndex}"`);
160
+ }
161
+
162
+ lines.push(` <col ${attributes.join(" ")}/>`);
163
+ }
164
+ lines.push(` </cols>`);
165
+ return lines;
166
+ }
167
+
168
+ function serializeCellXml(
169
+ [key, cell]: [CellKey, CellValue],
170
+ sharedStringIndexByValue: ReadonlyMap<string, number> | undefined,
171
+ ): string {
172
+ const { row, col } = parseCellKey(key);
173
+ const ref = formatCellRef(row, col);
174
+ const styleAttr = cell.styleRef ? ` s="${cell.styleRef.xfIndex}"` : "";
175
+
176
+ switch (cell.kind) {
177
+ case "blank":
178
+ return `<c r="${ref}"${styleAttr}/>`;
179
+
180
+ case "text": {
181
+ if (sharedStringIndexByValue && sharedStringIndexByValue.has(cell.value)) {
182
+ return `<c r="${ref}"${styleAttr} t="s"><v>${sharedStringIndexByValue.get(cell.value)}</v></c>`;
183
+ }
184
+
185
+ const preserveSpace = requiresPreservedSpace(cell.value) ? ` xml:space="preserve"` : "";
186
+ return `<c r="${ref}"${styleAttr} t="inlineStr"><is><t${preserveSpace}>${escapeXml(cell.value)}</t></is></c>`;
187
+ }
188
+
189
+ case "number":
190
+ return `<c r="${ref}"${styleAttr}><v>${formatNumber(cell.value)}</v></c>`;
191
+
192
+ case "boolean":
193
+ return `<c r="${ref}"${styleAttr} t="b"><v>${cell.value ? "1" : "0"}</v></c>`;
194
+
195
+ case "error":
196
+ return `<c r="${ref}"${styleAttr} t="e"><v>${escapeXml(cell.errorCode)}</v></c>`;
197
+
198
+ case "formula":
199
+ return serializeFormulaCellXml(ref, styleAttr, cell);
200
+ }
201
+ }
202
+
203
+ function serializeFormulaCellXml(
204
+ ref: string,
205
+ styleAttr: string,
206
+ cell: FormulaCell,
207
+ ): string {
208
+ const cachedValue = serializeFormulaCachedValue(cell);
209
+ const typeAttr = cachedValue.typeCode ? ` t="${cachedValue.typeCode}"` : "";
210
+ const valueXml = cachedValue.valueXml ? `<v>${cachedValue.valueXml}</v>` : "";
211
+ return `<c r="${ref}"${styleAttr}${typeAttr}><f>${escapeXml(cell.formula)}</f>${valueXml}</c>`;
212
+ }
213
+
214
+ function serializeFormulaCachedValue(
215
+ cell: FormulaCell,
216
+ ): { typeCode?: string; valueXml?: string } {
217
+ if (!cell.cachedValue) {
218
+ return {};
219
+ }
220
+
221
+ switch (cell.cachedValue.type) {
222
+ case "blank":
223
+ return {};
224
+
225
+ case "number":
226
+ return { valueXml: formatNumber(cell.cachedValue.value) };
227
+
228
+ case "text":
229
+ return { typeCode: "str", valueXml: escapeXml(cell.cachedValue.value) };
230
+
231
+ case "boolean":
232
+ return { typeCode: "b", valueXml: cell.cachedValue.value ? "1" : "0" };
233
+
234
+ case "error":
235
+ return { typeCode: "e", valueXml: escapeXml(cell.cachedValue.errorCode) };
236
+ }
237
+ }
238
+
239
+ function computeDimensionRef(sheet: CanonicalSheet): string {
240
+ let minRow = Infinity;
241
+ let minCol = Infinity;
242
+ let maxRow = -Infinity;
243
+ let maxCol = -Infinity;
244
+
245
+ for (const key of sheet.cells.keys()) {
246
+ const { row, col } = parseCellKey(key);
247
+ minRow = Math.min(minRow, row);
248
+ minCol = Math.min(minCol, col);
249
+ maxRow = Math.max(maxRow, row);
250
+ maxCol = Math.max(maxCol, col);
251
+ }
252
+
253
+ for (const merge of sheet.merges) {
254
+ minRow = Math.min(minRow, merge.startRow);
255
+ minCol = Math.min(minCol, merge.startCol);
256
+ maxRow = Math.max(maxRow, merge.endRow);
257
+ maxCol = Math.max(maxCol, merge.endCol);
258
+ }
259
+
260
+ if (!Number.isFinite(minRow) || !Number.isFinite(minCol)) {
261
+ return "A1";
262
+ }
263
+
264
+ const start = formatCellRef(minRow, minCol);
265
+ const end = formatCellRef(maxRow, maxCol);
266
+ return start === end ? start : `${start}:${end}`;
267
+ }
268
+
269
+ function formatMergeRef(merge: MergeRange): string {
270
+ return `${formatCellRef(merge.startRow, merge.startCol)}:${formatCellRef(merge.endRow, merge.endCol)}`;
271
+ }
272
+
273
+ function formatCellRef(row: number, col: number): string {
274
+ return `${columnNumberToName(col)}${row + 1}`;
275
+ }
276
+
277
+ function columnNumberToName(col: number): string {
278
+ let index = col + 1;
279
+ let result = "";
280
+
281
+ while (index > 0) {
282
+ const remainder = (index - 1) % 26;
283
+ result = String.fromCharCode(65 + remainder) + result;
284
+ index = Math.floor((index - 1) / 26);
285
+ }
286
+
287
+ return result;
288
+ }
289
+
290
+ function compareCellKeys(left: CellKey, right: CellKey): number {
291
+ const leftAddress = parseCellKey(left);
292
+ const rightAddress = parseCellKey(right);
293
+
294
+ if (leftAddress.row !== rightAddress.row) {
295
+ return leftAddress.row - rightAddress.row;
296
+ }
297
+
298
+ return leftAddress.col - rightAddress.col;
299
+ }
300
+
301
+ function compareMergeRanges(left: MergeRange, right: MergeRange): number {
302
+ if (left.startRow !== right.startRow) {
303
+ return left.startRow - right.startRow;
304
+ }
305
+ if (left.startCol !== right.startCol) {
306
+ return left.startCol - right.startCol;
307
+ }
308
+ if (left.endRow !== right.endRow) {
309
+ return left.endRow - right.endRow;
310
+ }
311
+ return left.endCol - right.endCol;
312
+ }
313
+
314
+ function formatNumber(value: number): string {
315
+ if (!Number.isFinite(value)) {
316
+ throw new Error(`Cannot serialize non-finite numeric cell value ${value}.`);
317
+ }
318
+
319
+ return String(value);
320
+ }
321
+
322
+ function requiresPreservedSpace(value: string): boolean {
323
+ return /^\s/.test(value) || /\s$/.test(value);
324
+ }
325
+
326
+ function escapeXml(value: string): string {
327
+ return value
328
+ .replace(/&/g, "&amp;")
329
+ .replace(/"/g, "&quot;")
330
+ .replace(/</g, "&lt;")
331
+ .replace(/>/g, "&gt;")
332
+ .replace(/'/g, "&apos;");
333
+ }