@shbernal/ts-xlsx 1.0.3 → 1.2.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/README.md +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.d.ts +10 -0
- package/dist/core/cell.js +4 -123
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.d.ts +9 -1
- package/dist/core/column.js +0 -52
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.d.ts +30 -0
- package/dist/core/limits.js +2 -0
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -44
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.d.ts +9 -1
- package/dist/core/row.js +0 -58
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.d.ts +20 -0
- package/dist/core/text-metrics.js +10 -0
- package/dist/core/theme.js +0 -85
- package/dist/core/value.d.ts +40 -0
- package/dist/core/value.js +30 -48
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -0
- package/dist/core/workbook.js +11 -368
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +22 -0
- package/dist/core/worksheet.js +14 -485
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.d.ts +3 -1
- package/dist/entries/core.js +3 -7
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +5 -35
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +9 -10
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
|
@@ -1,9 +1,3 @@
|
|
|
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
1
|
import { decodeRange, encodeAddress, MAX_COLUMN } from '../../core/address.js';
|
|
8
2
|
import { DEFAULT_DATE_NUMFMT, dateToSerial } from '../../core/date.js';
|
|
9
3
|
import { mangleFormula } from '../../core/formula.js';
|
|
@@ -22,19 +16,11 @@ import { richTextRunsXml } from './rich-text.js';
|
|
|
22
16
|
import { planSharedFormulas } from './shared-formulas.js';
|
|
23
17
|
import { autoFilterXml, headerFooterXml, pageBreaksXml, pageMarginsXml, pageSetupXml, printOptionsXml, sheetProtectionXml, sheetPrXml, sheetViewsXml, } from './sheet-properties.js';
|
|
24
18
|
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
19
|
export class Extent {
|
|
32
20
|
top = Infinity;
|
|
33
21
|
left = Infinity;
|
|
34
22
|
bottom = -Infinity;
|
|
35
23
|
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
24
|
constructor(seed) {
|
|
39
25
|
if (seed) {
|
|
40
26
|
this.top = seed.top;
|
|
@@ -43,12 +29,9 @@ export class Extent {
|
|
|
43
29
|
this.right = seed.right;
|
|
44
30
|
}
|
|
45
31
|
}
|
|
46
|
-
/** Whether no used cell has been folded in yet — the sheet's dimension is then the lone cell `A1`. */
|
|
47
32
|
get isEmpty() {
|
|
48
33
|
return this.bottom === -Infinity;
|
|
49
34
|
}
|
|
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
35
|
add(row, minCol, maxCol) {
|
|
53
36
|
if (minCol === Infinity)
|
|
54
37
|
return;
|
|
@@ -63,18 +46,9 @@ export class Extent {
|
|
|
63
46
|
}
|
|
64
47
|
}
|
|
65
48
|
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
49
|
validateMerges(sheet);
|
|
69
50
|
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
51
|
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
52
|
const rowOutline = scanRowOutline(sheet);
|
|
79
53
|
const collapsedSummaries = rowOutline.collapsedSummaries;
|
|
80
54
|
const context = {
|
|
@@ -85,8 +59,6 @@ export function worksheetXml(sheet, tables, styles, references, hyperlinks, shar
|
|
|
85
59
|
collapsedSummaries,
|
|
86
60
|
};
|
|
87
61
|
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
62
|
const extent = new Extent(flushed?.extent);
|
|
91
63
|
for (const entry of sheet.rows()) {
|
|
92
64
|
const { xml, minCol, maxCol } = renderRow(entry, context);
|
|
@@ -98,9 +70,6 @@ export function worksheetXml(sheet, tables, styles, references, hyperlinks, shar
|
|
|
98
70
|
const dimensionRef = extent.isEmpty
|
|
99
71
|
? 'A1'
|
|
100
72
|
: `${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
73
|
const orderedRows = flushed
|
|
105
74
|
? [...flushed.rows, ...liveRows].sort((a, b) => a.number - b.number)
|
|
106
75
|
: liveRows;
|
|
@@ -113,75 +82,40 @@ export function worksheetXml(sheet, tables, styles, references, hyperlinks, shar
|
|
|
113
82
|
sheetViewsXml(sheet.view, active) +
|
|
114
83
|
sheetFormatPr(sheet.properties, {
|
|
115
84
|
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
85
|
row: Math.max(rowOutline.maxLevel, flushed?.maxRowOutlineLevel ?? 0),
|
|
119
86
|
}) +
|
|
120
87
|
colsXml(sheet, styles) +
|
|
121
88
|
sheetData +
|
|
122
89
|
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
90
|
autoFilterXml(sheet.autoFilter) +
|
|
126
91
|
mergeCellsXml(sheet.merges) +
|
|
127
|
-
// CT_Worksheet order: <conditionalFormatting> blocks follow <mergeCells>, then <dataValidations>,
|
|
128
|
-
// then <hyperlinks> — all precede the print settings.
|
|
129
92
|
conditionalFormattingsXml(sheet.conditionalFormattings, styles) +
|
|
130
93
|
dataValidationsXml(sheet.dataValidations) +
|
|
131
94
|
hyperlinksXml(hyperlinks) +
|
|
132
|
-
// CT_Worksheet order: <printOptions> precedes <pageMargins>, which precedes <pageSetup>.
|
|
133
95
|
printOptionsXml(sheet.printOptions) +
|
|
134
96
|
pageMarginsXml(sheet.pageMargins) +
|
|
135
97
|
pageSetupXml(sheet.pageSetup, references.printerSettingsRelId) +
|
|
136
98
|
headerFooterXml(sheet.headerFooter) +
|
|
137
|
-
// CT_Worksheet order: <rowBreaks> follows <headerFooter>, <colBreaks> follows <rowBreaks>, and
|
|
138
|
-
// both precede the drawing block.
|
|
139
99
|
pageBreaksXml(sheet.rowBreaks, 'rowBreaks') +
|
|
140
100
|
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
101
|
refElement('drawing', references.drawingRelId) +
|
|
145
102
|
refElement('legacyDrawing', references.legacyDrawingRelId) +
|
|
146
103
|
refElement('legacyDrawingHF', references.legacyDrawingHFRelId) +
|
|
147
104
|
refElement('picture', references.backgroundRelId) +
|
|
148
105
|
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
106
|
worksheetExtLstXml(sheet, references.slicerRelIds) +
|
|
154
107
|
'</worksheet>');
|
|
155
108
|
}
|
|
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
109
|
export function buildColumnDefaults(sheet) {
|
|
163
110
|
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
111
|
for (const { index, properties } of sheet.columns())
|
|
168
112
|
columnDefaults.set(index, properties ?? {});
|
|
169
113
|
return columnDefaults;
|
|
170
114
|
}
|
|
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
115
|
export function renderRow(entry, ctx) {
|
|
178
116
|
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
117
|
const rendered = cells.filter((cell) => cell.value !== null || hasOwnStyle(cell));
|
|
183
118
|
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
119
|
if (rendered.length === 0 && attrs === '')
|
|
186
120
|
return { xml: '', minCol: Infinity, maxCol: -Infinity };
|
|
187
121
|
const rowFill = properties?.fill;
|
|
@@ -201,28 +135,18 @@ export function renderRow(entry, ctx) {
|
|
|
201
135
|
}
|
|
202
136
|
return { xml: `<row r="${number}"${attrs}>${cellsXml}</row>`, minCol, maxCol };
|
|
203
137
|
}
|
|
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
138
|
function composeCellStyle(cell, rowFill, colDef) {
|
|
209
139
|
return {
|
|
210
140
|
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
141
|
numFmt: cell.numFmt ?? colDef?.numFmt ?? dateDefaultNumFmt(cell.value),
|
|
214
142
|
font: cell.font ?? colDef?.font,
|
|
215
143
|
border: cell.border ?? colDef?.border,
|
|
216
144
|
alignment: cell.alignment ?? colDef?.alignment,
|
|
217
145
|
protection: cell.protection ?? colDef?.protection,
|
|
218
146
|
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
147
|
xfId: cell[NAMED_STYLE_ID],
|
|
222
148
|
};
|
|
223
149
|
}
|
|
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
150
|
function worksheetExtLstXml(sheet, slicerRelIds) {
|
|
227
151
|
const exts = [
|
|
228
152
|
conditionalFormattingsExtXml(sheet.conditionalFormattings),
|
|
@@ -231,17 +155,12 @@ function worksheetExtLstXml(sheet, slicerRelIds) {
|
|
|
231
155
|
].filter((ext) => ext !== '');
|
|
232
156
|
return exts.length === 0 ? '' : `<extLst>${exts.join('')}</extLst>`;
|
|
233
157
|
}
|
|
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
158
|
function slicerListExtXml(slicerRelIds) {
|
|
238
159
|
if (slicerRelIds.length === 0)
|
|
239
160
|
return '';
|
|
240
161
|
const slicers = slicerRelIds.map((relId) => `<x14:slicer r:id="${relId}"/>`).join('');
|
|
241
162
|
return x14Ext(SLICER_LIST_EXT_URI, `<x14:slicerList>${slicers}</x14:slicerList>`);
|
|
242
163
|
}
|
|
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
164
|
function validateMerges(sheet) {
|
|
246
165
|
if (sheet.merges.length === 0 || sheet.tables.length === 0)
|
|
247
166
|
return;
|
|
@@ -269,9 +188,6 @@ function mergeCellsXml(merges) {
|
|
|
269
188
|
.join('');
|
|
270
189
|
return `<mergeCells count="${merges.length}">${cells}</mergeCells>`;
|
|
271
190
|
}
|
|
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
191
|
function refElement(tag, relId) {
|
|
276
192
|
return relId === null ? '' : `<${tag} r:id="${relId}"/>`;
|
|
277
193
|
}
|
|
@@ -284,8 +200,6 @@ function tablePartsXml(tables) {
|
|
|
284
200
|
export function worksheetRelsXml(tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedReferences, pivots) {
|
|
285
201
|
const rels = [
|
|
286
202
|
...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
203
|
...pivots.map((pivot) => relationship(pivot.sheetRelId, REL.pivotTable, `../pivotTables/pivotTable${pivot.number}.xml`)),
|
|
290
204
|
...(drawing === null
|
|
291
205
|
? []
|
|
@@ -296,8 +210,6 @@ export function worksheetRelsXml(tables, drawing, comments, threadedComments, pr
|
|
|
296
210
|
relationship(comments.vmlRelId, REL.vmlDrawing, `../drawings/vmlDrawing${comments.number}.vml`),
|
|
297
211
|
relationship(comments.commentsRelId, REL.comments, `../comments${comments.number}.xml`),
|
|
298
212
|
]),
|
|
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
213
|
...(threadedComments === null
|
|
302
214
|
? []
|
|
303
215
|
: [
|
|
@@ -313,11 +225,7 @@ export function worksheetRelsXml(tables, drawing, comments, threadedComments, pr
|
|
|
313
225
|
: [
|
|
314
226
|
relationship(background.relId, REL.image, `../media/image${background.mediaNumber}.${background.extension}`),
|
|
315
227
|
]),
|
|
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
228
|
...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
229
|
...hyperlinks
|
|
322
230
|
.filter((link) => link.relId !== undefined && link.target !== undefined)
|
|
323
231
|
.map((link) => relationship(link.relId, REL.hyperlink, escapeAttr(link.target), {
|
|
@@ -326,21 +234,13 @@ export function worksheetRelsXml(tables, drawing, comments, threadedComments, pr
|
|
|
326
234
|
];
|
|
327
235
|
return relationshipsPart(rels);
|
|
328
236
|
}
|
|
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
237
|
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
238
|
function sheetFormatPr(properties, outlineLevel) {
|
|
338
239
|
const rowHeight = properties.defaultRowHeight ?? DEFAULT_ROW_HEIGHT;
|
|
339
240
|
let attrs = ` defaultRowHeight="${numberText(rowHeight)}"`;
|
|
340
241
|
if (properties.defaultColWidth !== undefined) {
|
|
341
242
|
attrs += ` defaultColWidth="${numberText(properties.defaultColWidth)}"`;
|
|
342
243
|
}
|
|
343
|
-
// A non-standard default row height is only honoured by Excel when customHeight is set.
|
|
344
244
|
if (properties.defaultRowHeight !== undefined)
|
|
345
245
|
attrs += ' customHeight="1"';
|
|
346
246
|
if (outlineLevel.col > 0)
|
|
@@ -349,9 +249,6 @@ function sheetFormatPr(properties, outlineLevel) {
|
|
|
349
249
|
attrs += ` outlineLevelRow="${outlineLevel.row}"`;
|
|
350
250
|
return `<sheetFormatPr${attrs}/>`;
|
|
351
251
|
}
|
|
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
252
|
function maxColumnOutlineLevel(sheet) {
|
|
356
253
|
let max = 0;
|
|
357
254
|
for (const column of sheet.columns()) {
|
|
@@ -362,19 +259,11 @@ function maxColumnOutlineLevel(sheet) {
|
|
|
362
259
|
return max;
|
|
363
260
|
}
|
|
364
261
|
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
262
|
const runs = [];
|
|
370
263
|
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
264
|
if (index > MAX_COLUMN)
|
|
374
265
|
continue;
|
|
375
266
|
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
267
|
if (body === null)
|
|
379
268
|
continue;
|
|
380
269
|
const last = runs[runs.length - 1];
|
|
@@ -390,9 +279,6 @@ function colsXml(sheet, styles) {
|
|
|
390
279
|
const cols = runs.map((run) => `<col min="${run.min}" max="${run.max}"${run.body}/>`).join('');
|
|
391
280
|
return `<cols>${cols}</cols>`;
|
|
392
281
|
}
|
|
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
282
|
function colBody(properties, styles) {
|
|
397
283
|
let attrs = '';
|
|
398
284
|
let meaningful = false;
|
|
@@ -412,9 +298,6 @@ function colBody(properties, styles) {
|
|
|
412
298
|
attrs += ' collapsed="1"';
|
|
413
299
|
meaningful = true;
|
|
414
300
|
}
|
|
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
301
|
const style = styles.styleId({
|
|
419
302
|
fill: properties.fill,
|
|
420
303
|
numFmt: properties.numFmt,
|
|
@@ -440,25 +323,13 @@ function rowAttrs(properties, styles, collapsedSummary) {
|
|
|
440
323
|
if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
|
|
441
324
|
attrs += ` outlineLevel="${properties.outlineLevel}"`;
|
|
442
325
|
}
|
|
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
326
|
if (properties.collapsed || collapsedSummary)
|
|
447
327
|
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
328
|
const style = styles.styleId({ fill: properties.fill });
|
|
451
329
|
if (style !== 0)
|
|
452
330
|
attrs += ` s="${style}" customFormat="1"`;
|
|
453
331
|
return attrs;
|
|
454
332
|
}
|
|
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
333
|
function scanRowOutline(sheet) {
|
|
463
334
|
const level = new Map();
|
|
464
335
|
const hidden = new Map();
|
|
@@ -488,9 +359,6 @@ function scanRowOutline(sheet) {
|
|
|
488
359
|
}
|
|
489
360
|
return { collapsedSummaries, maxLevel };
|
|
490
361
|
}
|
|
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
362
|
function dateDefaultNumFmt(value) {
|
|
495
363
|
const date = value instanceof Date
|
|
496
364
|
? value
|
|
@@ -507,16 +375,11 @@ function cellXml(cell, style, shared, sharedStrings) {
|
|
|
507
375
|
if (formula !== undefined)
|
|
508
376
|
return formula;
|
|
509
377
|
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
378
|
if (Number.isNaN(value.getTime()))
|
|
513
379
|
return `<c r="${ref}"${s}/>`;
|
|
514
380
|
return `<c r="${ref}"${s}><v>${numberText(dateToSerial(value))}</v></c>`;
|
|
515
381
|
}
|
|
516
382
|
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
383
|
if (!Number.isFinite(value))
|
|
521
384
|
return `<c r="${ref}"${s}/>`;
|
|
522
385
|
return `<c r="${ref}"${s}><v>${numberText(value)}</v></c>`;
|
|
@@ -525,46 +388,30 @@ function cellXml(cell, style, shared, sharedStrings) {
|
|
|
525
388
|
return `<c r="${ref}"${s} t="b"><v>${value ? 1 : 0}</v></c>`;
|
|
526
389
|
}
|
|
527
390
|
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
391
|
if (sharedStrings !== null) {
|
|
531
392
|
return `<c r="${ref}"${s} t="s"><v>${sharedStrings.intern(value)}</v></c>`;
|
|
532
393
|
}
|
|
533
394
|
return `<c r="${ref}"${s} t="inlineStr"><is>${textElement(value)}</is></c>`;
|
|
534
395
|
}
|
|
535
396
|
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
397
|
if (sharedStrings !== null) {
|
|
539
398
|
return `<c r="${ref}"${s} t="s"><v>${sharedStrings.intern(value)}</v></c>`;
|
|
540
399
|
}
|
|
541
400
|
return `<c r="${ref}"${s} t="inlineStr"><is>${richTextRunsXml(value.richText)}</is></c>`;
|
|
542
401
|
}
|
|
543
402
|
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
403
|
const label = typeof value.text === 'string'
|
|
548
404
|
? textElement(value.text)
|
|
549
405
|
: richTextRunsXml(value.text.richText);
|
|
550
406
|
return `<c r="${ref}"${s} t="inlineStr"><is>${label}</is></c>`;
|
|
551
407
|
}
|
|
552
408
|
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
409
|
return `<c r="${ref}"${s} t="e"><v>${value.error}</v></c>`;
|
|
556
410
|
}
|
|
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
411
|
if (value === null)
|
|
560
412
|
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
413
|
throw new InternalError(`writing a ${detectValueType(value)} cell value has no arm — every CellValue kind is handled above`);
|
|
564
414
|
}
|
|
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
415
|
function hasOwnStyle(cell) {
|
|
569
416
|
return (cell.fill !== undefined ||
|
|
570
417
|
cell.numFmt !== undefined ||
|
|
@@ -575,13 +422,7 @@ function hasOwnStyle(cell) {
|
|
|
575
422
|
cell.quotePrefix === true ||
|
|
576
423
|
cell[NAMED_STYLE_ID] !== undefined);
|
|
577
424
|
}
|
|
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
425
|
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
426
|
if (shared !== undefined) {
|
|
586
427
|
if (shared.ref !== undefined && isFormulaValue(value)) {
|
|
587
428
|
const f = `<f t="shared" ref="${shared.ref}" si="${shared.si}">${escapeText(mangleFormula(value.formula))}</f>`;
|
|
@@ -591,9 +432,6 @@ function cellFormulaXml(ref, s, value, shared) {
|
|
|
591
432
|
return formulaBodyXml(ref, s, `<f t="shared" si="${shared.si}"/>`, result);
|
|
592
433
|
}
|
|
593
434
|
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
435
|
const attrs = `ref="${escapeAttr(value.ref)}"` +
|
|
598
436
|
` dt2D="${value.dataTable2D ? 1 : 0}"` +
|
|
599
437
|
` dtr="${value.dataTableRow ? 1 : 0}"` +
|
|
@@ -606,13 +444,7 @@ function cellFormulaXml(ref, s, value, shared) {
|
|
|
606
444
|
}
|
|
607
445
|
return undefined;
|
|
608
446
|
}
|
|
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
447
|
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
448
|
if (result === undefined || (typeof result === 'number' && !Number.isFinite(result))) {
|
|
617
449
|
return `<c r="${ref}"${s}>${f}</c>`;
|
|
618
450
|
}
|
|
@@ -626,18 +458,12 @@ function formulaBodyXml(ref, s, f, result) {
|
|
|
626
458
|
return `<c r="${ref}"${s} t="str">${f}<v>${escapeText(result)}</v></c>`;
|
|
627
459
|
}
|
|
628
460
|
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
461
|
return `<c r="${ref}"${s} t="e">${f}<v>${result.error}</v></c>`;
|
|
632
462
|
}
|
|
633
463
|
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
464
|
if (Number.isNaN(result.getTime()))
|
|
638
465
|
return `<c r="${ref}"${s}>${f}</c>`;
|
|
639
466
|
return `<c r="${ref}"${s}>${f}<v>${numberText(dateToSerial(result))}</v></c>`;
|
|
640
467
|
}
|
|
641
|
-
// Every FormulaResult kind is handled above; this guards a value that reached here past the model.
|
|
642
468
|
throw new InternalError('writing a non-primitive formula result has no arm — every FormulaResult kind is handled above');
|
|
643
469
|
}
|