@shbernal/ts-xlsx 1.0.0
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/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
// Worksheet serialisation: a Worksheet model into its `xl/worksheets/sheetN.xml` part and the sheet's
|
|
2
|
+
// rels part. Owns the row/cell renderer the streaming writer also drives, and orchestrates the whole
|
|
3
|
+
// sheet body — the print/page/view/protection blocks live in `sheet-properties.ts`, shared-formula
|
|
4
|
+
// planning in `shared-formulas.ts`, each imported here rather than duplicated. Table *parts*
|
|
5
|
+
// (`xl/tables/tableN.xml`) are `tables.ts`'s concern, alongside their reader — this module only wires
|
|
6
|
+
// the sheet's `<tableParts>` back-references to them.
|
|
7
|
+
import { decodeRange, encodeAddress, MAX_COLUMN } from "../../core/address.js";
|
|
8
|
+
import { DEFAULT_DATE_NUMFMT, dateToSerial } from "../../core/date.js";
|
|
9
|
+
import { mangleFormula } from "../../core/formula.js";
|
|
10
|
+
import { NAMED_STYLE_ID } from "../../core/internal.js";
|
|
11
|
+
import { detectValueType, isDataTableFormulaValue, isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, } from "../../core/value.js";
|
|
12
|
+
import { AuthoringError, InternalError } from "../../errors.js";
|
|
13
|
+
import { escapeAttr, escapeText, numberText, textElement, XML_DECLARATION } from "../../xml/xml.js";
|
|
14
|
+
import { relativePartPath } from "../opc/part-paths.js";
|
|
15
|
+
import { relationship, relationshipsPart } from "../opc/rels.js";
|
|
16
|
+
import { conditionalFormattingsExtXml, conditionalFormattingsXml } from "./conditional-formatting.js";
|
|
17
|
+
import { dataValidationsExtXml, dataValidationsXml } from "./data-validation.js";
|
|
18
|
+
import { hyperlinksXml } from "./hyperlinks.js";
|
|
19
|
+
import { SLICER_LIST_EXT_URI } from "./namespaces.js";
|
|
20
|
+
import { NS, REL } from "./relationships.js";
|
|
21
|
+
import { richTextRunsXml } from "./rich-text.js";
|
|
22
|
+
import { planSharedFormulas } from "./shared-formulas.js";
|
|
23
|
+
import { autoFilterXml, headerFooterXml, pageBreaksXml, pageMarginsXml, pageSetupXml, printOptionsXml, sheetProtectionXml, sheetPrXml, sheetViewsXml, } from "./sheet-properties.js";
|
|
24
|
+
import { x14Ext } from "./x14-ext.js";
|
|
25
|
+
/**
|
|
26
|
+
* The used-cell extent of a sheet — the top-left/bottom-right grid bounds that fold into the
|
|
27
|
+
* `<dimension>`. Rows carrying only formatting (a row height, an outline level) do not extend the
|
|
28
|
+
* used range, matching how Excel records `<dimension>`, so {@link add} ignores them. A fresh extent
|
|
29
|
+
* holds the `Infinity`/`-Infinity` sentinels; {@link isEmpty} reports that no used cell has been seen.
|
|
30
|
+
*/
|
|
31
|
+
export class Extent {
|
|
32
|
+
top = Infinity;
|
|
33
|
+
left = Infinity;
|
|
34
|
+
bottom = -Infinity;
|
|
35
|
+
right = -Infinity;
|
|
36
|
+
// Seed from a prior extent (the rows a streaming writer already flushed and evicted) so the buffered
|
|
37
|
+
// pass folds its live rows onto the same bounds; unseeded, it starts empty.
|
|
38
|
+
constructor(seed) {
|
|
39
|
+
if (seed) {
|
|
40
|
+
this.top = seed.top;
|
|
41
|
+
this.left = seed.left;
|
|
42
|
+
this.bottom = seed.bottom;
|
|
43
|
+
this.right = seed.right;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Whether no used cell has been folded in yet — the sheet's dimension is then the lone cell `A1`. */
|
|
47
|
+
get isEmpty() {
|
|
48
|
+
return this.bottom === -Infinity;
|
|
49
|
+
}
|
|
50
|
+
/** Fold a rendered row's used-column span into the extent. `minCol` is `Infinity` when the row
|
|
51
|
+
* carried no cells (only formatting), which extends nothing. */
|
|
52
|
+
add(row, minCol, maxCol) {
|
|
53
|
+
if (minCol === Infinity)
|
|
54
|
+
return;
|
|
55
|
+
if (row < this.top)
|
|
56
|
+
this.top = row;
|
|
57
|
+
if (row > this.bottom)
|
|
58
|
+
this.bottom = row;
|
|
59
|
+
if (minCol < this.left)
|
|
60
|
+
this.left = minCol;
|
|
61
|
+
if (maxCol > this.right)
|
|
62
|
+
this.right = maxCol;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function worksheetXml(sheet, tables, styles, references, hyperlinks, sharedStrings, active, flushed) {
|
|
66
|
+
// A merge overlapping a table is Excel-invalid geometry; reject it before serialising
|
|
67
|
+
// rather than emit a package a consumer repairs on open.
|
|
68
|
+
validateMerges(sheet);
|
|
69
|
+
const columnDefaults = buildColumnDefaults(sheet);
|
|
70
|
+
// A cell filled from a shared formula is written as a master (seeding the group) or a clone
|
|
71
|
+
// (referencing it by shared index); resolve every such role before the row loop so each cell knows
|
|
72
|
+
// how to serialise its `<f>`. This also validates the master/clone geometry, throwing if a clone
|
|
73
|
+
// precedes its master or its master carries no formula.
|
|
74
|
+
const sharedRoles = planSharedFormulas(sheet);
|
|
75
|
+
// A fully-hidden outline group's collapse toggle belongs on its summary row; derive that set once
|
|
76
|
+
// so the row loop can stamp it even onto a summary row that carries no properties of its own. The
|
|
77
|
+
// same pass yields the sheet's deepest row outline level for `<sheetFormatPr>`.
|
|
78
|
+
const rowOutline = scanRowOutline(sheet);
|
|
79
|
+
const collapsedSummaries = rowOutline.collapsedSummaries;
|
|
80
|
+
const context = {
|
|
81
|
+
columnDefaults,
|
|
82
|
+
styles,
|
|
83
|
+
sharedStrings,
|
|
84
|
+
sharedRoles,
|
|
85
|
+
collapsedSummaries,
|
|
86
|
+
};
|
|
87
|
+
const liveRows = [];
|
|
88
|
+
// Seed the used-cell extent with any rows the streaming writer already serialised and evicted, so
|
|
89
|
+
// the dimension spans both them and the live rows below.
|
|
90
|
+
const extent = new Extent(flushed?.extent);
|
|
91
|
+
for (const entry of sheet.rows()) {
|
|
92
|
+
const { xml, minCol, maxCol } = renderRow(entry, context);
|
|
93
|
+
if (xml === '')
|
|
94
|
+
continue;
|
|
95
|
+
liveRows.push({ number: entry.number, xml });
|
|
96
|
+
extent.add(entry.number, minCol, maxCol);
|
|
97
|
+
}
|
|
98
|
+
const dimensionRef = extent.isEmpty
|
|
99
|
+
? 'A1'
|
|
100
|
+
: `${encodeAddress(extent.left, extent.top)}:${encodeAddress(extent.right, extent.bottom)}`;
|
|
101
|
+
// Merge the streaming writer's pre-rendered rows with the live ones into ascending row order — a
|
|
102
|
+
// flushed row can carry any number, and rows may be committed out of order. The buffered path has no
|
|
103
|
+
// flushed rows, so it skips the merge and its sort entirely.
|
|
104
|
+
const orderedRows = flushed
|
|
105
|
+
? [...flushed.rows, ...liveRows].sort((a, b) => a.number - b.number)
|
|
106
|
+
: liveRows;
|
|
107
|
+
const bodyXml = orderedRows.map((row) => row.xml).join('');
|
|
108
|
+
const sheetData = bodyXml === '' ? '<sheetData/>' : `<sheetData>${bodyXml}</sheetData>`;
|
|
109
|
+
return (XML_DECLARATION +
|
|
110
|
+
`<worksheet xmlns="${NS.main}" xmlns:r="${NS.docRels}">` +
|
|
111
|
+
sheetPrXml(sheet) +
|
|
112
|
+
`<dimension ref="${dimensionRef}"/>` +
|
|
113
|
+
sheetViewsXml(sheet.view, active) +
|
|
114
|
+
sheetFormatPr(sheet.properties, {
|
|
115
|
+
col: maxColumnOutlineLevel(sheet),
|
|
116
|
+
// A streamed sheet's flushed rows are gone from the model; their deepest level rides along on
|
|
117
|
+
// the flush record so the header still reports the whole sheet's outline.
|
|
118
|
+
row: Math.max(rowOutline.maxLevel, flushed?.maxRowOutlineLevel ?? 0),
|
|
119
|
+
}) +
|
|
120
|
+
colsXml(sheet, styles) +
|
|
121
|
+
sheetData +
|
|
122
|
+
sheetProtectionXml(sheet.protection) +
|
|
123
|
+
// CT_Worksheet order: <autoFilter> follows <sheetProtection> (and the scenarios block) and
|
|
124
|
+
// precedes <mergeCells>. Its `_FilterDatabase` companion is emitted in the workbook part.
|
|
125
|
+
autoFilterXml(sheet.autoFilter) +
|
|
126
|
+
mergeCellsXml(sheet.merges) +
|
|
127
|
+
// CT_Worksheet order: <conditionalFormatting> blocks follow <mergeCells>, then <dataValidations>,
|
|
128
|
+
// then <hyperlinks> — all precede the print settings.
|
|
129
|
+
conditionalFormattingsXml(sheet.conditionalFormattings, styles) +
|
|
130
|
+
dataValidationsXml(sheet.dataValidations) +
|
|
131
|
+
hyperlinksXml(hyperlinks) +
|
|
132
|
+
// CT_Worksheet order: <printOptions> precedes <pageMargins>, which precedes <pageSetup>.
|
|
133
|
+
printOptionsXml(sheet.printOptions) +
|
|
134
|
+
pageMarginsXml(sheet.pageMargins) +
|
|
135
|
+
pageSetupXml(sheet.pageSetup, references.printerSettingsRelId) +
|
|
136
|
+
headerFooterXml(sheet.headerFooter) +
|
|
137
|
+
// CT_Worksheet order: <rowBreaks> follows <headerFooter>, <colBreaks> follows <rowBreaks>, and
|
|
138
|
+
// both precede the drawing block.
|
|
139
|
+
pageBreaksXml(sheet.rowBreaks, 'rowBreaks') +
|
|
140
|
+
pageBreaksXml(sheet.columnBreaks, 'colBreaks') +
|
|
141
|
+
// Schema order near the tail: <drawing> (the images), then <legacyDrawing> (the VML holding the
|
|
142
|
+
// note boxes), then <legacyDrawingHF> (a preserved header/footer image's VML), then <picture>
|
|
143
|
+
// (the sheet background), then <tableParts>.
|
|
144
|
+
refElement('drawing', references.drawingRelId) +
|
|
145
|
+
refElement('legacyDrawing', references.legacyDrawingRelId) +
|
|
146
|
+
refElement('legacyDrawingHF', references.legacyDrawingHFRelId) +
|
|
147
|
+
refElement('picture', references.backgroundRelId) +
|
|
148
|
+
tablePartsXml(tables) +
|
|
149
|
+
// `<extLst>` is the final child of CT_Worksheet and a worksheet may carry at most one. Both the
|
|
150
|
+
// x14 conditional-formatting extensions (data-bar gradient/negative-fill/axis) and the extended
|
|
151
|
+
// (x14) data validations ride inside it as sibling `<ext>` blocks — so they are gathered here into
|
|
152
|
+
// a single `<extLst>` rather than each emitting its own.
|
|
153
|
+
worksheetExtLstXml(sheet, references.slicerRelIds) +
|
|
154
|
+
'</worksheet>');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A column's style facets are defaults its cells inherit unless they override them; the writer
|
|
158
|
+
* composes each cell's full style up front (cell over row over column, per facet) so a cell that
|
|
159
|
+
* overrides one facet still carries the column's others, rather than silently dropping them. Frozen
|
|
160
|
+
* once by the streaming writer at its first flush so every eagerly-rendered row sees the same defaults.
|
|
161
|
+
*/
|
|
162
|
+
export function buildColumnDefaults(sheet) {
|
|
163
|
+
const columnDefaults = new Map();
|
|
164
|
+
// `columns()` yields only columns that carry a format record, so the fallback is unreachable —
|
|
165
|
+
// it is here because the handle's `properties` is honestly optional, not because a defined
|
|
166
|
+
// column can lack one.
|
|
167
|
+
for (const { index, properties } of sheet.columns())
|
|
168
|
+
columnDefaults.set(index, properties ?? {});
|
|
169
|
+
return columnDefaults;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Serialise one row to its `<row>` element, or '' when the row has neither data nor its own
|
|
173
|
+
* formatting. Returns the used-column bounds (`Infinity`/`-Infinity` when nothing was rendered) so a
|
|
174
|
+
* caller can fold them into the sheet dimension. Shared by the buffered sheet pass and the streaming
|
|
175
|
+
* writer's eager flush, so both emit byte-identical rows.
|
|
176
|
+
*/
|
|
177
|
+
export function renderRow(entry, ctx) {
|
|
178
|
+
const { number, cells, properties } = entry;
|
|
179
|
+
// A cell earns a <c> element if it holds a value OR carries its own style: a formatted-but-empty
|
|
180
|
+
// cell (a fill/border on a null value) is a real cell to Excel, and dropping it would lose the
|
|
181
|
+
// formatting. A cell with neither is inherited from its row/column and needs no element of its own.
|
|
182
|
+
const rendered = cells.filter((cell) => cell.value !== null || hasOwnStyle(cell));
|
|
183
|
+
const attrs = rowAttrs(properties, ctx.styles, ctx.collapsedSummaries.has(number));
|
|
184
|
+
// A row with neither data nor its own formatting has nothing to serialise.
|
|
185
|
+
if (rendered.length === 0 && attrs === '')
|
|
186
|
+
return { xml: '', minCol: Infinity, maxCol: -Infinity };
|
|
187
|
+
const rowFill = properties?.fill;
|
|
188
|
+
const cellsXml = rendered
|
|
189
|
+
.map((cell) => {
|
|
190
|
+
const style = ctx.styles.styleId(composeCellStyle(cell, rowFill, ctx.columnDefaults.get(cell.col)));
|
|
191
|
+
return cellXml(cell, style, ctx.sharedRoles.get(cell.address), ctx.sharedStrings);
|
|
192
|
+
})
|
|
193
|
+
.join('');
|
|
194
|
+
let minCol = Infinity;
|
|
195
|
+
let maxCol = -Infinity;
|
|
196
|
+
for (const cell of rendered) {
|
|
197
|
+
if (cell.col < minCol)
|
|
198
|
+
minCol = cell.col;
|
|
199
|
+
if (cell.col > maxCol)
|
|
200
|
+
maxCol = cell.col;
|
|
201
|
+
}
|
|
202
|
+
return { xml: `<row r="${number}"${attrs}>${cellsXml}</row>`, minCol, maxCol };
|
|
203
|
+
}
|
|
204
|
+
// Compose a cell's full style by resolving each facet cell-over-row-over-column, so a cell that
|
|
205
|
+
// overrides one facet still carries the row's fill and the column's other facets rather than silently
|
|
206
|
+
// dropping them — the per-facet precedence Excel applies. The row contributes only a fill today;
|
|
207
|
+
// quote-prefix and the named-style link are cell-only, with no row/column default to inherit.
|
|
208
|
+
function composeCellStyle(cell, rowFill, colDef) {
|
|
209
|
+
return {
|
|
210
|
+
fill: cell.fill ?? rowFill ?? colDef?.fill,
|
|
211
|
+
// A bare Date carries no format of its own, so it renders as a raw serial and reads back as a
|
|
212
|
+
// number unless we apply a date format. An explicit cell/column format wins.
|
|
213
|
+
numFmt: cell.numFmt ?? colDef?.numFmt ?? dateDefaultNumFmt(cell.value),
|
|
214
|
+
font: cell.font ?? colDef?.font,
|
|
215
|
+
border: cell.border ?? colDef?.border,
|
|
216
|
+
alignment: cell.alignment ?? colDef?.alignment,
|
|
217
|
+
protection: cell.protection ?? colDef?.protection,
|
|
218
|
+
quotePrefix: cell.quotePrefix,
|
|
219
|
+
// Preserved so a round-trip keeps the cell tied to its named style rather than flattening it into
|
|
220
|
+
// a purely-direct format.
|
|
221
|
+
xfId: cell[NAMED_STYLE_ID],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// Assemble the worksheet's single `<extLst>` from every x14 extension the sheet carries, or '' when it
|
|
225
|
+
// carries none. Each producer returns a bare `<ext>` so they compose without nesting an `<extLst>`.
|
|
226
|
+
function worksheetExtLstXml(sheet, slicerRelIds) {
|
|
227
|
+
const exts = [
|
|
228
|
+
conditionalFormattingsExtXml(sheet.conditionalFormattings),
|
|
229
|
+
dataValidationsExtXml(sheet.dataValidations),
|
|
230
|
+
slicerListExtXml(slicerRelIds),
|
|
231
|
+
].filter((ext) => ext !== '');
|
|
232
|
+
return exts.length === 0 ? '' : `<extLst>${exts.join('')}</extLst>`;
|
|
233
|
+
}
|
|
234
|
+
// The worksheet-body `<x14:slicerList>` extension that reconnects a sheet to its preserved slicer
|
|
235
|
+
// parts. Each `<x14:slicer>` names the sheet-local relationship id its slicer rel was re-emitted under,
|
|
236
|
+
// so the wiring stays consistent even though the id is reassigned on write. '' when the sheet has none.
|
|
237
|
+
function slicerListExtXml(slicerRelIds) {
|
|
238
|
+
if (slicerRelIds.length === 0)
|
|
239
|
+
return '';
|
|
240
|
+
const slicers = slicerRelIds.map((relId) => `<x14:slicer r:id="${relId}"/>`).join('');
|
|
241
|
+
return x14Ext(SLICER_LIST_EXT_URI, `<x14:slicerList>${slicers}</x14:slicerList>`);
|
|
242
|
+
}
|
|
243
|
+
// Excel forbids a merged range from intersecting a formatted table; such a file opens as
|
|
244
|
+
// corrupt. The writer is the OOXML gatekeeper for this cross-feature geometry conflict.
|
|
245
|
+
function validateMerges(sheet) {
|
|
246
|
+
if (sheet.merges.length === 0 || sheet.tables.length === 0)
|
|
247
|
+
return;
|
|
248
|
+
for (const merge of sheet.merges) {
|
|
249
|
+
const { left, right, top, bottom } = decodeRange(merge);
|
|
250
|
+
if (left === undefined || right === undefined || top === undefined || bottom === undefined)
|
|
251
|
+
continue;
|
|
252
|
+
for (const table of sheet.tables) {
|
|
253
|
+
const region = table.region;
|
|
254
|
+
const overlaps = left <= region.right &&
|
|
255
|
+
right >= region.left &&
|
|
256
|
+
top <= region.bottom &&
|
|
257
|
+
bottom >= region.top;
|
|
258
|
+
if (overlaps) {
|
|
259
|
+
throw new AuthoringError(`merged range ${merge} overlaps table "${table.name}" (${table.range}) — Excel forbids a merge inside a table`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function mergeCellsXml(merges) {
|
|
265
|
+
if (merges.length === 0)
|
|
266
|
+
return '';
|
|
267
|
+
const cells = merges
|
|
268
|
+
.map((range) => `<mergeCell ref="${escapeAttr(decodeRange(range).dimensions)}"/>`)
|
|
269
|
+
.join('');
|
|
270
|
+
return `<mergeCells count="${merges.length}">${cells}</mergeCells>`;
|
|
271
|
+
}
|
|
272
|
+
// A tail reference element (`<drawing r:id="…"/>` and its `<legacyDrawing>`/`<legacyDrawingHF>`/
|
|
273
|
+
// `<picture>` siblings) wiring the sheet to a part by relationship id, or '' when the sheet carries no
|
|
274
|
+
// part of that kind — each such id is null then.
|
|
275
|
+
function refElement(tag, relId) {
|
|
276
|
+
return relId === null ? '' : `<${tag} r:id="${relId}"/>`;
|
|
277
|
+
}
|
|
278
|
+
function tablePartsXml(tables) {
|
|
279
|
+
if (tables.length === 0)
|
|
280
|
+
return '';
|
|
281
|
+
const parts = tables.map(({ relId }) => `<tablePart r:id="${relId}"/>`).join('');
|
|
282
|
+
return `<tableParts count="${tables.length}">${parts}</tableParts>`;
|
|
283
|
+
}
|
|
284
|
+
export function worksheetRelsXml(tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedReferences, pivots) {
|
|
285
|
+
const rels = [
|
|
286
|
+
...tables.map(({ relId, number }) => relationship(relId, REL.table, `../tables/table${number}.xml`)),
|
|
287
|
+
// A pivot table hosted on this sheet is reached by a relationship of type pivotTable; Excel
|
|
288
|
+
// discovers the pivot from the rels part, so the sheet body itself carries no reference to it.
|
|
289
|
+
...pivots.map((pivot) => relationship(pivot.sheetRelId, REL.pivotTable, `../pivotTables/pivotTable${pivot.number}.xml`)),
|
|
290
|
+
...(drawing === null
|
|
291
|
+
? []
|
|
292
|
+
: [relationship(drawing.relId, REL.drawing, `../drawings/drawing${drawing.number}.xml`)]),
|
|
293
|
+
...(comments === null
|
|
294
|
+
? []
|
|
295
|
+
: [
|
|
296
|
+
relationship(comments.vmlRelId, REL.vmlDrawing, `../drawings/vmlDrawing${comments.number}.vml`),
|
|
297
|
+
relationship(comments.commentsRelId, REL.comments, `../comments${comments.number}.xml`),
|
|
298
|
+
]),
|
|
299
|
+
// A threaded-comment part, like a pivot table, is reached by relationship alone — no worksheet element
|
|
300
|
+
// names it, so this relationship is the only thing that makes Excel look for the conversation.
|
|
301
|
+
...(threadedComments === null
|
|
302
|
+
? []
|
|
303
|
+
: [
|
|
304
|
+
relationship(threadedComments.relId, REL.threadedComment, `../threadedComments/threadedComment${threadedComments.number}.xml`),
|
|
305
|
+
]),
|
|
306
|
+
...(printerSettings === null
|
|
307
|
+
? []
|
|
308
|
+
: [
|
|
309
|
+
relationship(printerSettings.relId, REL.printerSettings, `../printerSettings/printerSettings${printerSettings.number}.bin`),
|
|
310
|
+
]),
|
|
311
|
+
...(background === null
|
|
312
|
+
? []
|
|
313
|
+
: [
|
|
314
|
+
relationship(background.relId, REL.image, `../media/image${background.mediaNumber}.${background.extension}`),
|
|
315
|
+
]),
|
|
316
|
+
// A preserved reference targets its entry part's new (package-absolute) path; a worksheet always
|
|
317
|
+
// lives under `xl/worksheets/`, so the target is that path made relative to that directory.
|
|
318
|
+
...preservedReferences.map((reference) => relationship(reference.relId, reference.relType, escapeAttr(relativePartPath('xl/worksheets/sheet1.xml', reference.entryPath)))),
|
|
319
|
+
// An external hyperlink's target is a URL outside the package, so its relationship carries
|
|
320
|
+
// TargetMode="External". Internal links have no relId and contribute nothing here.
|
|
321
|
+
...hyperlinks
|
|
322
|
+
.filter((link) => link.relId !== undefined && link.target !== undefined)
|
|
323
|
+
.map((link) => relationship(link.relId, REL.hyperlink, escapeAttr(link.target), {
|
|
324
|
+
external: true,
|
|
325
|
+
})),
|
|
326
|
+
];
|
|
327
|
+
return relationshipsPart(rels);
|
|
328
|
+
}
|
|
329
|
+
// Excel's standard row height in points, emitted as the `defaultRowHeight` when the sheet does not
|
|
330
|
+
// override it so a reader sees the same baseline Excel would write.
|
|
331
|
+
const DEFAULT_ROW_HEIGHT = 15;
|
|
332
|
+
// `<sheetFormatPr>` carries the sheet's grid defaults and, when the sheet groups anything, the depth
|
|
333
|
+
// of its deepest outline. A consumer sizes the outline bars from those depths — the strips that sit
|
|
334
|
+
// above the column headers and left of the row headers — so a grouped sheet that omits them lays its
|
|
335
|
+
// grid out with no room reserved for a bar it then has to draw. Both are omitted at zero, as Excel
|
|
336
|
+
// does, so an ungrouped sheet stays byte-clean.
|
|
337
|
+
function sheetFormatPr(properties, outlineLevel) {
|
|
338
|
+
const rowHeight = properties.defaultRowHeight ?? DEFAULT_ROW_HEIGHT;
|
|
339
|
+
let attrs = ` defaultRowHeight="${numberText(rowHeight)}"`;
|
|
340
|
+
if (properties.defaultColWidth !== undefined) {
|
|
341
|
+
attrs += ` defaultColWidth="${numberText(properties.defaultColWidth)}"`;
|
|
342
|
+
}
|
|
343
|
+
// A non-standard default row height is only honoured by Excel when customHeight is set.
|
|
344
|
+
if (properties.defaultRowHeight !== undefined)
|
|
345
|
+
attrs += ' customHeight="1"';
|
|
346
|
+
if (outlineLevel.col > 0)
|
|
347
|
+
attrs += ` outlineLevelCol="${outlineLevel.col}"`;
|
|
348
|
+
if (outlineLevel.row > 0)
|
|
349
|
+
attrs += ` outlineLevelRow="${outlineLevel.row}"`;
|
|
350
|
+
return `<sheetFormatPr${attrs}/>`;
|
|
351
|
+
}
|
|
352
|
+
// The deepest column outline level the sheet declares — the `outlineLevelCol` its `<sheetFormatPr>`
|
|
353
|
+
// reports. A column past XFD contributes nothing: {@link colsXml} drops it as out-of-range, so its
|
|
354
|
+
// group would have no `<col>` to sit on.
|
|
355
|
+
function maxColumnOutlineLevel(sheet) {
|
|
356
|
+
let max = 0;
|
|
357
|
+
for (const column of sheet.columns()) {
|
|
358
|
+
if (column.index > MAX_COLUMN)
|
|
359
|
+
continue;
|
|
360
|
+
max = Math.max(max, column.outlineLevel ?? 0);
|
|
361
|
+
}
|
|
362
|
+
return max;
|
|
363
|
+
}
|
|
364
|
+
function colsXml(sheet, styles) {
|
|
365
|
+
// Runs of adjacent columns that carry identical definitions are coalesced into a single
|
|
366
|
+
// `<col min max>` span — Excel writes columns this way, and it keeps the part compact for a
|
|
367
|
+
// sheet whose columns share a width or outline level. A gap in the indices or any difference
|
|
368
|
+
// in the emitted attributes breaks the run.
|
|
369
|
+
const runs = [];
|
|
370
|
+
for (const { index, properties } of sheet.columns()) {
|
|
371
|
+
// OOXML has no column past XFD (16384); a definition beyond it is corrupt to Excel,
|
|
372
|
+
// so drop it rather than emit an out-of-range <col> range.
|
|
373
|
+
if (index > MAX_COLUMN)
|
|
374
|
+
continue;
|
|
375
|
+
const body = colBody(properties ?? {}, styles);
|
|
376
|
+
// A <col> with no width, visibility, or style says nothing; omit it entirely. That also covers
|
|
377
|
+
// the column with no format record at all, which `columns()` does not in fact yield.
|
|
378
|
+
if (body === null)
|
|
379
|
+
continue;
|
|
380
|
+
const last = runs[runs.length - 1];
|
|
381
|
+
if (last !== undefined && last.max === index - 1 && last.body === body) {
|
|
382
|
+
last.max = index;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
runs.push({ min: index, max: index, body });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (runs.length === 0)
|
|
389
|
+
return '';
|
|
390
|
+
const cols = runs.map((run) => `<col min="${run.min}" max="${run.max}"${run.body}/>`).join('');
|
|
391
|
+
return `<cols>${cols}</cols>`;
|
|
392
|
+
}
|
|
393
|
+
// The attributes of a `<col>` sans its `min`/`max` span (each with a leading space), or `null` when
|
|
394
|
+
// the column declares nothing worth emitting. Two columns with the same body are interchangeable, so
|
|
395
|
+
// the body doubles as the equivalence key that {@link colsXml} coalesces adjacent runs by.
|
|
396
|
+
function colBody(properties, styles) {
|
|
397
|
+
let attrs = '';
|
|
398
|
+
let meaningful = false;
|
|
399
|
+
if (properties.width !== undefined) {
|
|
400
|
+
attrs += ` width="${numberText(properties.width)}" customWidth="1"`;
|
|
401
|
+
meaningful = true;
|
|
402
|
+
}
|
|
403
|
+
if (properties.hidden) {
|
|
404
|
+
attrs += ' hidden="1"';
|
|
405
|
+
meaningful = true;
|
|
406
|
+
}
|
|
407
|
+
if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
|
|
408
|
+
attrs += ` outlineLevel="${properties.outlineLevel}"`;
|
|
409
|
+
meaningful = true;
|
|
410
|
+
}
|
|
411
|
+
if (properties.collapsed) {
|
|
412
|
+
attrs += ' collapsed="1"';
|
|
413
|
+
meaningful = true;
|
|
414
|
+
}
|
|
415
|
+
// The column's style facets are carried as its own `<col>` style; its populated cells inherit
|
|
416
|
+
// them via the composition above, and this `style` makes Excel apply them to the column's empty
|
|
417
|
+
// cells too.
|
|
418
|
+
const style = styles.styleId({
|
|
419
|
+
fill: properties.fill,
|
|
420
|
+
numFmt: properties.numFmt,
|
|
421
|
+
font: properties.font,
|
|
422
|
+
border: properties.border,
|
|
423
|
+
alignment: properties.alignment,
|
|
424
|
+
protection: properties.protection,
|
|
425
|
+
});
|
|
426
|
+
if (style !== 0) {
|
|
427
|
+
attrs += ` style="${style}"`;
|
|
428
|
+
meaningful = true;
|
|
429
|
+
}
|
|
430
|
+
return meaningful ? attrs : null;
|
|
431
|
+
}
|
|
432
|
+
function rowAttrs(properties, styles, collapsedSummary) {
|
|
433
|
+
if (properties === undefined)
|
|
434
|
+
return collapsedSummary ? ' collapsed="1"' : '';
|
|
435
|
+
let attrs = '';
|
|
436
|
+
if (properties.height !== undefined)
|
|
437
|
+
attrs += ` ht="${numberText(properties.height)}" customHeight="1"`;
|
|
438
|
+
if (properties.hidden)
|
|
439
|
+
attrs += ' hidden="1"';
|
|
440
|
+
if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
|
|
441
|
+
attrs += ` outlineLevel="${properties.outlineLevel}"`;
|
|
442
|
+
}
|
|
443
|
+
// The collapse toggle is set explicitly by the author, or derived onto a summary row whose whole
|
|
444
|
+
// detail group is hidden (see {@link collapsedSummaryRows}). It rides the summary row, never the
|
|
445
|
+
// detail rows.
|
|
446
|
+
if (properties.collapsed || collapsedSummary)
|
|
447
|
+
attrs += ' collapsed="1"';
|
|
448
|
+
// A row-level fill is a default format for the row's cells; customFormat="1" is what makes
|
|
449
|
+
// Excel honour the row's `s`, and a cell without its own `s` then inherits it.
|
|
450
|
+
const style = styles.styleId({ fill: properties.fill });
|
|
451
|
+
if (style !== 0)
|
|
452
|
+
attrs += ` s="${style}" customFormat="1"`;
|
|
453
|
+
return attrs;
|
|
454
|
+
}
|
|
455
|
+
// A collapsed outline group is two coordinated facts: its detail rows carry outlineLevel and are
|
|
456
|
+
// hidden, AND the summary row that terminates the group carries `collapsed`. Authors typically set
|
|
457
|
+
// only outlineLevel + hidden on the detail rows, so the summary flag is derived here rather than
|
|
458
|
+
// demanded of the caller: a row is a collapsed summary iff its adjacent detail run — the contiguous
|
|
459
|
+
// higher-outline-level rows on the summary side — is non-empty and every row in it is hidden.
|
|
460
|
+
// Placement follows the sheet's summaryBelow flag (Excel's default is summary below the detail); the
|
|
461
|
+
// walk stops at the first row of level <= the summary's own, so a gap or a boundary ends the group.
|
|
462
|
+
function scanRowOutline(sheet) {
|
|
463
|
+
const level = new Map();
|
|
464
|
+
const hidden = new Map();
|
|
465
|
+
let maxLevel = 0;
|
|
466
|
+
for (const { number, properties } of sheet.rows()) {
|
|
467
|
+
const rowLevel = properties?.outlineLevel ?? 0;
|
|
468
|
+
level.set(number, rowLevel);
|
|
469
|
+
hidden.set(number, properties?.hidden ?? false);
|
|
470
|
+
if (rowLevel > maxLevel)
|
|
471
|
+
maxLevel = rowLevel;
|
|
472
|
+
}
|
|
473
|
+
const levelOf = (row) => level.get(row) ?? 0;
|
|
474
|
+
const step = sheet.outline.summaryBelow === false ? 1 : -1;
|
|
475
|
+
const collapsedSummaries = new Set();
|
|
476
|
+
for (const [summary, summaryLevel] of level) {
|
|
477
|
+
let detail = summary + step;
|
|
478
|
+
let sawDetail = false;
|
|
479
|
+
let allHidden = true;
|
|
480
|
+
while (levelOf(detail) > summaryLevel) {
|
|
481
|
+
sawDetail = true;
|
|
482
|
+
if (!hidden.get(detail))
|
|
483
|
+
allHidden = false;
|
|
484
|
+
detail += step;
|
|
485
|
+
}
|
|
486
|
+
if (sawDetail && allHidden)
|
|
487
|
+
collapsedSummaries.add(summary);
|
|
488
|
+
}
|
|
489
|
+
return { collapsedSummaries, maxLevel };
|
|
490
|
+
}
|
|
491
|
+
// A valid Date — whether the cell's own value or a formula's cached result — with no format of its
|
|
492
|
+
// own gets the default date format so it renders and reads back as a date rather than a bare serial.
|
|
493
|
+
// An Invalid Date and every non-date value contribute nothing here.
|
|
494
|
+
function dateDefaultNumFmt(value) {
|
|
495
|
+
const date = value instanceof Date
|
|
496
|
+
? value
|
|
497
|
+
: (isFormulaValue(value) || isSharedFormulaValue(value)) && value.result instanceof Date
|
|
498
|
+
? value.result
|
|
499
|
+
: undefined;
|
|
500
|
+
return date !== undefined && !Number.isNaN(date.getTime()) ? DEFAULT_DATE_NUMFMT : undefined;
|
|
501
|
+
}
|
|
502
|
+
function cellXml(cell, style, shared, sharedStrings) {
|
|
503
|
+
const ref = cell.address;
|
|
504
|
+
const value = cell.value;
|
|
505
|
+
const s = style !== 0 ? ` s="${style}"` : '';
|
|
506
|
+
const formula = cellFormulaXml(ref, s, value, shared);
|
|
507
|
+
if (formula !== undefined)
|
|
508
|
+
return formula;
|
|
509
|
+
if (value instanceof Date) {
|
|
510
|
+
// An Invalid Date (new Date(NaN)) has no serial; keep the cell (and its style) but emit no
|
|
511
|
+
// value rather than throwing, so one bad date never takes down the whole sheet's export.
|
|
512
|
+
if (Number.isNaN(value.getTime()))
|
|
513
|
+
return `<c r="${ref}"${s}/>`;
|
|
514
|
+
return `<c r="${ref}"${s}><v>${numberText(dateToSerial(value))}</v></c>`;
|
|
515
|
+
}
|
|
516
|
+
if (typeof value === 'number') {
|
|
517
|
+
// A non-finite number (NaN, ±Infinity) has no OOXML representation; keep the cell and its style
|
|
518
|
+
// but emit no value rather than a bare "NaN"/"Infinity" token — the same graceful degradation an
|
|
519
|
+
// Invalid Date gets, so one bad value never corrupts the sheet or takes down the whole export.
|
|
520
|
+
if (!Number.isFinite(value))
|
|
521
|
+
return `<c r="${ref}"${s}/>`;
|
|
522
|
+
return `<c r="${ref}"${s}><v>${numberText(value)}</v></c>`;
|
|
523
|
+
}
|
|
524
|
+
if (typeof value === 'boolean') {
|
|
525
|
+
return `<c r="${ref}"${s} t="b"><v>${value ? 1 : 0}</v></c>`;
|
|
526
|
+
}
|
|
527
|
+
if (typeof value === 'string') {
|
|
528
|
+
// With shared strings on, the cell holds only the pool index (`t="s"`); otherwise the text
|
|
529
|
+
// lives inline in the cell. Both decode to the same string on read.
|
|
530
|
+
if (sharedStrings !== null) {
|
|
531
|
+
return `<c r="${ref}"${s} t="s"><v>${sharedStrings.intern(value)}</v></c>`;
|
|
532
|
+
}
|
|
533
|
+
return `<c r="${ref}"${s} t="inlineStr"><is>${textElement(value)}</is></c>`;
|
|
534
|
+
}
|
|
535
|
+
if (isRichTextValue(value)) {
|
|
536
|
+
// With shared strings on, rich text is pooled as a rich `<si>` (the cell holds only its index);
|
|
537
|
+
// otherwise the runs live inline. Both decode back to the same runs on read.
|
|
538
|
+
if (sharedStrings !== null) {
|
|
539
|
+
return `<c r="${ref}"${s} t="s"><v>${sharedStrings.intern(value)}</v></c>`;
|
|
540
|
+
}
|
|
541
|
+
return `<c r="${ref}"${s} t="inlineStr"><is>${richTextRunsXml(value.richText)}</is></c>`;
|
|
542
|
+
}
|
|
543
|
+
if (isHyperlinkValue(value)) {
|
|
544
|
+
// The cell holds only the visible label; the link itself rides in the sheet's <hyperlinks>.
|
|
545
|
+
// The label is either a plain string or rich text, serialised the same way a cell value of
|
|
546
|
+
// that kind would be.
|
|
547
|
+
const label = typeof value.text === 'string'
|
|
548
|
+
? textElement(value.text)
|
|
549
|
+
: richTextRunsXml(value.text.richText);
|
|
550
|
+
return `<c r="${ref}"${s} t="inlineStr"><is>${label}</is></c>`;
|
|
551
|
+
}
|
|
552
|
+
if (isErrorValue(value)) {
|
|
553
|
+
// An error literal serialises under t="e" with its code as the value. The codes are a closed
|
|
554
|
+
// set of canonical spellings (see ERROR_CODES) with no XML-special characters, so no escaping.
|
|
555
|
+
return `<c r="${ref}"${s} t="e"><v>${value.error}</v></c>`;
|
|
556
|
+
}
|
|
557
|
+
// A null value only reaches here for a formatted-but-empty cell (the row loop keeps it for its
|
|
558
|
+
// style); emit the styled cell with no <v>, exactly how Excel stores a formatted blank.
|
|
559
|
+
if (value === null)
|
|
560
|
+
return `<c r="${ref}"${s}/>`;
|
|
561
|
+
// Every ValueType kind is served by an arm above (a formula routes through its own writer), so
|
|
562
|
+
// this is unreachable — it exists because the union is not exhaustively narrowed here.
|
|
563
|
+
throw new InternalError(`writing a ${detectValueType(value)} cell value has no arm — every CellValue kind is handled above`);
|
|
564
|
+
}
|
|
565
|
+
// Whether a cell carries any style facet of its own — the reason to serialise it even when empty.
|
|
566
|
+
// A note is not a style: it lives in the comments part, not the cell's <c> element, so it does not
|
|
567
|
+
// count here. Row/column-inherited formatting is likewise excluded; only the cell's own facets do.
|
|
568
|
+
function hasOwnStyle(cell) {
|
|
569
|
+
return (cell.fill !== undefined ||
|
|
570
|
+
cell.numFmt !== undefined ||
|
|
571
|
+
cell.font !== undefined ||
|
|
572
|
+
cell.border !== undefined ||
|
|
573
|
+
cell.alignment !== undefined ||
|
|
574
|
+
cell.protection !== undefined ||
|
|
575
|
+
cell.quotePrefix === true ||
|
|
576
|
+
cell[NAMED_STYLE_ID] !== undefined);
|
|
577
|
+
}
|
|
578
|
+
// Serialise a formula cell — a shared-formula master or clone, a What-If data table, or a plain
|
|
579
|
+
// formula — into its `<c>` element, or return undefined when the value is not a formula so `cellXml`
|
|
580
|
+
// falls through to its value dispatch.
|
|
581
|
+
function cellFormulaXml(ref, s, value, shared) {
|
|
582
|
+
// A shared-formula master seeds the group with its formula text under `t="shared" ref si`; a clone
|
|
583
|
+
// carries no text of its own, only a back-reference to the master's `si`. Its cached result still
|
|
584
|
+
// travels with the cell.
|
|
585
|
+
if (shared !== undefined) {
|
|
586
|
+
if (shared.ref !== undefined && isFormulaValue(value)) {
|
|
587
|
+
const f = `<f t="shared" ref="${shared.ref}" si="${shared.si}">${escapeText(mangleFormula(value.formula))}</f>`;
|
|
588
|
+
return formulaBodyXml(ref, s, f, value.result);
|
|
589
|
+
}
|
|
590
|
+
const result = isSharedFormulaValue(value) ? value.result : undefined;
|
|
591
|
+
return formulaBodyXml(ref, s, `<f t="shared" si="${shared.si}"/>`, result);
|
|
592
|
+
}
|
|
593
|
+
if (isDataTableFormulaValue(value)) {
|
|
594
|
+
// A data-table formula carries no expression text — only its declaration attributes — which we
|
|
595
|
+
// re-emit verbatim so a read-modify-write cycle preserves the What-If kind the library never
|
|
596
|
+
// evaluates. The cached result travels as any formula result does.
|
|
597
|
+
const attrs = `ref="${escapeAttr(value.ref)}"` +
|
|
598
|
+
` dt2D="${value.dataTable2D ? 1 : 0}"` +
|
|
599
|
+
` dtr="${value.dataTableRow ? 1 : 0}"` +
|
|
600
|
+
(value.r1 !== undefined ? ` r1="${escapeAttr(value.r1)}"` : '') +
|
|
601
|
+
(value.r2 !== undefined ? ` r2="${escapeAttr(value.r2)}"` : '');
|
|
602
|
+
return formulaBodyXml(ref, s, `<f t="dataTable" ${attrs}/>`, value.result);
|
|
603
|
+
}
|
|
604
|
+
if (isFormulaValue(value)) {
|
|
605
|
+
return formulaBodyXml(ref, s, `<f>${escapeText(mangleFormula(value.formula))}</f>`, value.result);
|
|
606
|
+
}
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
// Wrap a prepared `<f>` element (a plain formula, or a shared master/slave `<f>`) with the cell
|
|
610
|
+
// element and its cached result, typing the cell by the result's kind exactly as a bare value of that
|
|
611
|
+
// kind would be.
|
|
612
|
+
function formulaBodyXml(ref, s, f, result) {
|
|
613
|
+
// A non-finite cached result (a `1/0` that reached the model as Infinity/NaN) has no OOXML
|
|
614
|
+
// representation; keep the formula but cache no value rather than emit a bare "NaN" — the same
|
|
615
|
+
// graceful degradation a bare non-finite cell and an Invalid Date result get.
|
|
616
|
+
if (result === undefined || (typeof result === 'number' && !Number.isFinite(result))) {
|
|
617
|
+
return `<c r="${ref}"${s}>${f}</c>`;
|
|
618
|
+
}
|
|
619
|
+
if (typeof result === 'number') {
|
|
620
|
+
return `<c r="${ref}"${s}>${f}<v>${numberText(result)}</v></c>`;
|
|
621
|
+
}
|
|
622
|
+
if (typeof result === 'boolean') {
|
|
623
|
+
return `<c r="${ref}"${s} t="b">${f}<v>${result ? 1 : 0}</v></c>`;
|
|
624
|
+
}
|
|
625
|
+
if (typeof result === 'string') {
|
|
626
|
+
return `<c r="${ref}"${s} t="str">${f}<v>${escapeText(result)}</v></c>`;
|
|
627
|
+
}
|
|
628
|
+
if (isErrorValue(result)) {
|
|
629
|
+
// A formula that evaluated to an error caches its code under t="e", exactly as a bare error
|
|
630
|
+
// cell does — the reader's decodeResult mirrors decodeValue for this case.
|
|
631
|
+
return `<c r="${ref}"${s} t="e">${f}<v>${result.error}</v></c>`;
|
|
632
|
+
}
|
|
633
|
+
if (result instanceof Date) {
|
|
634
|
+
// A date-valued result caches its serial exactly as a bare date cell stores its value; the
|
|
635
|
+
// cell's date number format (applied when its style is composed) is what makes both read back as
|
|
636
|
+
// a Date. An Invalid Date has no serial, so cache no result rather than emit NaN.
|
|
637
|
+
if (Number.isNaN(result.getTime()))
|
|
638
|
+
return `<c r="${ref}"${s}>${f}</c>`;
|
|
639
|
+
return `<c r="${ref}"${s}>${f}<v>${numberText(dateToSerial(result))}</v></c>`;
|
|
640
|
+
}
|
|
641
|
+
// Every FormulaResult kind is handled above; this guards a value that reached here past the model.
|
|
642
|
+
throw new InternalError('writing a non-primitive formula result has no arm — every FormulaResult kind is handled above');
|
|
643
|
+
}
|