@beyondwork/docx-react-component 1.0.13 → 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 +32 -35
- 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/core/selection/review-anchors.ts +89 -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/document-runtime.ts +13 -0
- package/src/runtime/numbering-prefix.ts +195 -0
- package/src/runtime/session-capabilities.ts +22 -1
- package/src/runtime/surface-projection.ts +287 -8
- package/src/ui/WordReviewEditor.tsx +120 -10
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +8 -2
- 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/src/ui-tailwind/tw-review-workspace.tsx +3 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { WorkbookStyleRegistry, XfRecord } from "../model/styles.ts";
|
|
2
|
+
|
|
3
|
+
const STYLES_NAMESPACE = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
4
|
+
|
|
5
|
+
export function serializeStylesXml(registry: WorkbookStyleRegistry): string {
|
|
6
|
+
const numFormats = [...registry.numFormats.values()].sort(
|
|
7
|
+
(left, right) => left.numFmtId - right.numFmtId,
|
|
8
|
+
);
|
|
9
|
+
const cellFormats = registry.cellFormats;
|
|
10
|
+
const fontCount = Math.max(1, ...cellFormats.map((format) => format.fontId + 1));
|
|
11
|
+
const fillCount = Math.max(2, ...cellFormats.map((format) => format.fillId + 1));
|
|
12
|
+
const borderCount = Math.max(1, ...cellFormats.map((format) => format.borderId + 1));
|
|
13
|
+
|
|
14
|
+
const lines = [
|
|
15
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
16
|
+
`<styleSheet xmlns="${STYLES_NAMESPACE}">`,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
if (numFormats.length > 0) {
|
|
20
|
+
lines.push(` <numFmts count="${numFormats.length}">`);
|
|
21
|
+
for (const format of numFormats) {
|
|
22
|
+
lines.push(
|
|
23
|
+
` <numFmt numFmtId="${format.numFmtId}" formatCode="${escapeXml(format.formatCode)}"/>`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
lines.push(` </numFmts>`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
lines.push(` <fonts count="${fontCount}">`);
|
|
30
|
+
for (let index = 0; index < fontCount; index += 1) {
|
|
31
|
+
lines.push(` <font/>`);
|
|
32
|
+
}
|
|
33
|
+
lines.push(` </fonts>`);
|
|
34
|
+
|
|
35
|
+
lines.push(` <fills count="${fillCount}">`);
|
|
36
|
+
for (let index = 0; index < fillCount; index += 1) {
|
|
37
|
+
if (index === 0) {
|
|
38
|
+
lines.push(` <fill><patternFill patternType="none"/></fill>`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (index === 1) {
|
|
42
|
+
lines.push(` <fill><patternFill patternType="gray125"/></fill>`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
lines.push(` <fill><patternFill patternType="none"/></fill>`);
|
|
46
|
+
}
|
|
47
|
+
lines.push(` </fills>`);
|
|
48
|
+
|
|
49
|
+
lines.push(` <borders count="${borderCount}">`);
|
|
50
|
+
for (let index = 0; index < borderCount; index += 1) {
|
|
51
|
+
lines.push(` <border><left/><right/><top/><bottom/><diagonal/></border>`);
|
|
52
|
+
}
|
|
53
|
+
lines.push(` </borders>`);
|
|
54
|
+
|
|
55
|
+
lines.push(` <cellStyleXfs count="1">`);
|
|
56
|
+
lines.push(` <xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>`);
|
|
57
|
+
lines.push(` </cellStyleXfs>`);
|
|
58
|
+
|
|
59
|
+
lines.push(` <cellXfs count="${cellFormats.length}">`);
|
|
60
|
+
for (const format of cellFormats) {
|
|
61
|
+
lines.push(` <xf ${serializeXfAttributes(format)}/>`);
|
|
62
|
+
}
|
|
63
|
+
lines.push(` </cellXfs>`);
|
|
64
|
+
|
|
65
|
+
lines.push(` <cellStyles count="1">`);
|
|
66
|
+
lines.push(` <cellStyle name="Normal" xfId="0" builtinId="0"/>`);
|
|
67
|
+
lines.push(` </cellStyles>`);
|
|
68
|
+
lines.push(`</styleSheet>`);
|
|
69
|
+
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function serializeXfAttributes(format: XfRecord): string {
|
|
74
|
+
const attributes = new Map<string, string>(Object.entries(format.rawAttributes));
|
|
75
|
+
attributes.set("numFmtId", attributes.get("numFmtId") ?? String(format.numFmtId));
|
|
76
|
+
attributes.set("fontId", attributes.get("fontId") ?? String(format.fontId));
|
|
77
|
+
attributes.set("fillId", attributes.get("fillId") ?? String(format.fillId));
|
|
78
|
+
attributes.set("borderId", attributes.get("borderId") ?? String(format.borderId));
|
|
79
|
+
|
|
80
|
+
const priority = ["numFmtId", "fontId", "fillId", "borderId"];
|
|
81
|
+
const orderedKeys = [
|
|
82
|
+
...priority.filter((key) => attributes.has(key)),
|
|
83
|
+
...[...attributes.keys()].filter((key) => !priority.includes(key)),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return orderedKeys
|
|
87
|
+
.map((key) => `${key}="${escapeXml(attributes.get(key) ?? "")}"`)
|
|
88
|
+
.join(" ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function escapeXml(value: string): string {
|
|
92
|
+
return value
|
|
93
|
+
.replace(/&/g, "&")
|
|
94
|
+
.replace(/"/g, """)
|
|
95
|
+
.replace(/</g, "<")
|
|
96
|
+
.replace(/>/g, ">")
|
|
97
|
+
.replace(/'/g, "'");
|
|
98
|
+
}
|