@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 +10 -2
- package/src/api/public-types.ts +6 -0
- package/src/compare/diff-engine.ts +84 -7
- package/src/compare/index.ts +25 -0
- package/src/compare/snapshot.ts +31 -0
- package/src/formats/xlsx/io/serialize-shared-strings.ts +72 -0
- package/src/formats/xlsx/io/serialize-sheet.ts +333 -0
- package/src/formats/xlsx/io/serialize-styles.ts +98 -0
- package/src/formats/xlsx/io/serialize-workbook.ts +429 -0
- package/src/formats/xlsx/runtime/cell-commands.ts +567 -0
- package/src/formats/xlsx/runtime/sheet-commands.ts +206 -0
- package/src/formats/xlsx/runtime/workbook-runtime.ts +177 -0
- package/src/formats/xlsx/runtime/workbook-transaction.ts +822 -0
- package/src/io/ooxml/parse-main-document.ts +6 -6
- package/src/io/ooxml/parse-revisions.ts +18 -24
- package/src/legal/bookmarks.ts +35 -0
- package/src/legal/index.ts +32 -0
- package/src/legal/signature-blocks.ts +259 -0
- package/src/runtime/numbering-prefix.ts +195 -0
- package/src/runtime/surface-projection.ts +287 -8
- package/src/ui/WordReviewEditor.tsx +105 -4
- package/src/ui-tailwind/editor-surface/pm-schema.ts +148 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +15 -29
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.
|
|
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.
|
|
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",
|
package/src/api/public-types.ts
CHANGED
|
@@ -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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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";
|
package/src/compare/snapshot.ts
CHANGED
|
@@ -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, "&")
|
|
68
|
+
.replace(/"/g, """)
|
|
69
|
+
.replace(/</g, "<")
|
|
70
|
+
.replace(/>/g, ">")
|
|
71
|
+
.replace(/'/g, "'");
|
|
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, "&")
|
|
329
|
+
.replace(/"/g, """)
|
|
330
|
+
.replace(/</g, "<")
|
|
331
|
+
.replace(/>/g, ">")
|
|
332
|
+
.replace(/'/g, "'");
|
|
333
|
+
}
|