@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,21 +1,3 @@
|
|
|
1
|
-
// The BIFF12 worksheet-body reader: one `xl/worksheets/sheetN.bin` in, one populated {@link Worksheet}
|
|
2
|
-
// out — the binary counterpart of `../xlsx/read-worksheet.ts`, filling the very same model.
|
|
3
|
-
//
|
|
4
|
-
// The cell table is a flat, row-major run of records: a `BrtRowHdr` opens a row and every cell record
|
|
5
|
-
// until the next one belongs to it, so the open row is a single variable rather than nested state.
|
|
6
|
-
// Each cell record is a `Cell` header followed by a value shaped by the record's own type — which is
|
|
7
|
-
// what makes the binary form quick to parse: there is no `t=` attribute to interpret, the record
|
|
8
|
-
// number *is* the type.
|
|
9
|
-
//
|
|
10
|
-
// A formula cell carries both halves of what the XML form spells in `<f>` and `<v>`: a `Ptg` token
|
|
11
|
-
// stream, decoded back to text by `./formula.ts`, and the result Excel last computed. The two are
|
|
12
|
-
// filled independently — a formula whose stream uses a token this reader does not decode still
|
|
13
|
-
// surfaces its cached value, which is exactly what the reader could see before the decoder existed.
|
|
14
|
-
//
|
|
15
|
-
// One shape needs a second look at the sheet. An array formula's member cells hold a `PtgExp`
|
|
16
|
-
// pointing at the group's top-left, and the `BrtArrFmla` record carrying the group's actual formula
|
|
17
|
-
// comes *after* those cells in the stream. Those cells are therefore parked and resolved once the
|
|
18
|
-
// whole part has been read.
|
|
19
1
|
import { encodeAddress, MAX_COLUMN } from '../../core/address.js';
|
|
20
2
|
import { isDateFormat, serialToDate } from '../../core/date.js';
|
|
21
3
|
import { unmangleFunctions } from '../../core/formula.js';
|
|
@@ -25,9 +7,6 @@ import { decodeFormula, formulaAnchor } from './formula.js';
|
|
|
25
7
|
import { errorCodeFor, RecordReader } from './primitives.js';
|
|
26
8
|
import { readRecords } from './record-stream.js';
|
|
27
9
|
import { BRT } from './record-types.js';
|
|
28
|
-
// Every record that carries a plain cell — one whose payload is a value and nothing else. Membership
|
|
29
|
-
// drives the dispatch below, so a record type absent from both this set and {@link FORMULA_RECORDS}
|
|
30
|
-
// is skipped whole rather than being mistaken for a cell and consuming the reader.
|
|
31
10
|
const CELL_RECORDS = new Set([
|
|
32
11
|
BRT.CellBlank,
|
|
33
12
|
BRT.CellRk,
|
|
@@ -38,40 +17,23 @@ const CELL_RECORDS = new Set([
|
|
|
38
17
|
BRT.CellIsst,
|
|
39
18
|
BRT.CellRString,
|
|
40
19
|
]);
|
|
41
|
-
// Every record that carries a formula: a cached result of the record's own kind, then the token
|
|
42
|
-
// stream that produced it.
|
|
43
20
|
const FORMULA_RECORDS = new Set([
|
|
44
21
|
BRT.FmlaString,
|
|
45
22
|
BRT.FmlaNum,
|
|
46
23
|
BRT.FmlaBool,
|
|
47
24
|
BRT.FmlaError,
|
|
48
25
|
]);
|
|
49
|
-
/**
|
|
50
|
-
* Read a worksheet part into `sheet`: its column and row geometry, its merged ranges, and every
|
|
51
|
-
* non-empty cell with the style its index resolves to in `xfStyles` and, for a formula cell, the text
|
|
52
|
-
* its token stream decodes to through `scope`.
|
|
53
|
-
*/
|
|
54
26
|
export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
|
|
55
|
-
// The open row, one-based as the model counts them. -1 means none is open, which a cell record
|
|
56
|
-
// arriving before any row header (a malformed sheet) is dropped against rather than guessed at.
|
|
57
27
|
let row = -1;
|
|
58
|
-
// A row that declares a format supplies the default for its cells that carry none, as a column
|
|
59
|
-
// does; the next row header replaces it.
|
|
60
28
|
let rowStyle = -1;
|
|
61
|
-
// A column's format is the last fallback. Column records always precede the cell table.
|
|
62
29
|
const columnStyle = new Map();
|
|
63
|
-
// The sheet's default row height, in twips. Every row header restates its height whether or not the
|
|
64
|
-
// row has one of its own, so the default is what tells the two apart — see {@link applyRow}.
|
|
65
|
-
// `BrtWsFmtInfo` precedes the cell table, so it is always known by the time a row is read.
|
|
66
30
|
let defaultRowHeight = -1;
|
|
67
|
-
// The formula of each array-formula group, keyed by the group's top-left cell, and the member cells
|
|
68
|
-
// waiting on one. Both are needed because `BrtArrFmla` follows the cells it speaks for.
|
|
69
31
|
const groups = new Map();
|
|
70
32
|
const deferred = [];
|
|
71
33
|
for (const record of readRecords(part)) {
|
|
72
34
|
const reader = new RecordReader(record.data);
|
|
73
35
|
if (record.type === BRT.WsFmtInfo) {
|
|
74
|
-
reader.skip(6);
|
|
36
|
+
reader.skip(6);
|
|
75
37
|
defaultRowHeight = reader.u16();
|
|
76
38
|
}
|
|
77
39
|
else if (record.type === BRT.ColInfo) {
|
|
@@ -90,7 +52,7 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
|
|
|
90
52
|
}
|
|
91
53
|
else if (record.type === BRT.ArrFmla) {
|
|
92
54
|
const { rowFirst, colFirst } = reader.range();
|
|
93
|
-
reader.skip(1);
|
|
55
|
+
reader.skip(1);
|
|
94
56
|
groups.set(groupKey(rowFirst, colFirst), {
|
|
95
57
|
rgce: reader.bytes(reader.u32()),
|
|
96
58
|
rgcb: reader.bytes(reader.u32()),
|
|
@@ -100,9 +62,6 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
|
|
|
100
62
|
const { column, styleIndex } = reader.cell();
|
|
101
63
|
if (!inGrid(column, row - 1))
|
|
102
64
|
continue;
|
|
103
|
-
// A cell's own format wins, then its row's, then its column's — the order Excel applies.
|
|
104
|
-
// Index 0 is the default xf, which BIFF12 writes where XML simply omits `s`, so it means
|
|
105
|
-
// "no format of my own" and lets the row/column default through.
|
|
106
65
|
const resolved = styleIndex > 0 ? styleIndex : rowStyle >= 0 ? rowStyle : (columnStyle.get(column) ?? -1);
|
|
107
66
|
const style = resolved >= 0 ? xfStyles[resolved] : xfStyles[0];
|
|
108
67
|
const cell = sheet.getCell(encodeAddress(column + 1, row));
|
|
@@ -112,7 +71,7 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
|
|
|
112
71
|
continue;
|
|
113
72
|
}
|
|
114
73
|
const result = cachedResult(record.type, reader, style?.numFmt);
|
|
115
|
-
reader.skip(2);
|
|
74
|
+
reader.skip(2);
|
|
116
75
|
const rgce = reader.bytes(reader.u32());
|
|
117
76
|
const rgcb = reader.bytes(reader.u32());
|
|
118
77
|
const anchor = formulaAnchor(rgce, rgcb);
|
|
@@ -132,8 +91,6 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
|
|
|
132
91
|
}
|
|
133
92
|
}
|
|
134
93
|
for (const member of deferred) {
|
|
135
|
-
// Only the group's top-left cell states the formula; the rest carry the value it produced, which
|
|
136
|
-
// is exactly what the XML form writes for them.
|
|
137
94
|
const group = groups.get(groupKey(member.anchorRow, member.anchorColumn));
|
|
138
95
|
const own = member.row === member.anchorRow && member.column === member.anchorColumn;
|
|
139
96
|
member.cell.value =
|
|
@@ -145,23 +102,15 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
|
|
|
145
102
|
function groupKey(row, column) {
|
|
146
103
|
return `${row}:${column}`;
|
|
147
104
|
}
|
|
148
|
-
// Pair a decoded formula with its cached result, in the shape the XML reader produces for the same
|
|
149
|
-
// cell. A formula the decoder could not read leaves the value alone: the cached result is still true,
|
|
150
|
-
// and is what this reader surfaced before formulas were decoded at all.
|
|
151
105
|
function formulaValue(formula, result) {
|
|
152
106
|
if (formula === undefined)
|
|
153
107
|
return result ?? null;
|
|
154
|
-
// Strip the `_xlfn.`/`_xlpm.` on-disk mangling, as the XML reader does, so the model never holds it.
|
|
155
108
|
const stored = unmangleFunctions(formula);
|
|
156
109
|
return result === undefined ? { formula: stored } : { formula: stored, result };
|
|
157
110
|
}
|
|
158
|
-
// The result a formula record cached, decoded by the record's own kind — the binary counterpart of
|
|
159
|
-
// reading `<v>` under the `t` attribute.
|
|
160
111
|
function cachedResult(type, reader, numFmt) {
|
|
161
112
|
switch (type) {
|
|
162
113
|
case BRT.FmlaNum:
|
|
163
|
-
// A formula's cached numeric result honours the cell's date format exactly as a bare number
|
|
164
|
-
// does, so a date-valued formula reads back as a Date rather than a serial.
|
|
165
114
|
return asNumberOrDate(reader.f64(), numFmt);
|
|
166
115
|
case BRT.FmlaBool:
|
|
167
116
|
return reader.u8() !== 0;
|
|
@@ -173,19 +122,11 @@ function cachedResult(type, reader, numFmt) {
|
|
|
173
122
|
return reader.wideString();
|
|
174
123
|
}
|
|
175
124
|
}
|
|
176
|
-
// Excel's grid bounds, zero-based as the binary format counts. [MS-XLSB] states them as MUST
|
|
177
|
-
// constraints, which is exactly why a reader has to check them: a damaged or hostile file states
|
|
178
|
-
// whatever it likes, and an address beyond the grid has nowhere to go. Everything positional funnels
|
|
179
|
-
// through here before it reaches the model, so an out-of-grid record is dropped rather than turned
|
|
180
|
-
// into an unrepresentable address (which the address encoder would reject) or, worse, a column loop
|
|
181
|
-
// four billion iterations long.
|
|
182
125
|
const MAX_ROW_INDEX = 1048575;
|
|
183
126
|
const MAX_COLUMN_INDEX = MAX_COLUMN - 1;
|
|
184
127
|
function inGrid(column, row) {
|
|
185
128
|
return column >= 0 && column <= MAX_COLUMN_INDEX && row >= 0 && row <= MAX_ROW_INDEX;
|
|
186
129
|
}
|
|
187
|
-
// Decode a cell record's payload — the reader is positioned just past the shared `Cell` header, so
|
|
188
|
-
// what remains is exactly the value this record type carries.
|
|
189
130
|
function decodeCell(type, reader, sharedStrings, numFmt) {
|
|
190
131
|
switch (type) {
|
|
191
132
|
case BRT.CellRk:
|
|
@@ -195,45 +136,32 @@ function decodeCell(type, reader, sharedStrings, numFmt) {
|
|
|
195
136
|
case BRT.CellBool:
|
|
196
137
|
return reader.u8() !== 0;
|
|
197
138
|
case BRT.CellError: {
|
|
198
|
-
// An unrecognised error byte keeps the cell non-empty without inventing an error the model
|
|
199
|
-
// does not define; there is no text form to fall back to as there is in XML.
|
|
200
139
|
const error = errorCodeFor(reader.u8());
|
|
201
140
|
return error === undefined ? null : { error };
|
|
202
141
|
}
|
|
203
142
|
case BRT.CellSt:
|
|
204
143
|
return reader.wideString();
|
|
205
144
|
case BRT.CellRString:
|
|
206
|
-
// Rich runs are not modelled in this cut; the flattened text is what a consumer sees.
|
|
207
145
|
return reader.richString();
|
|
208
146
|
case BRT.CellIsst:
|
|
209
147
|
return sharedStrings[reader.u32()] ?? '';
|
|
210
148
|
default:
|
|
211
|
-
// BrtCellBlank: formatted but empty. The style is already applied; the value is genuinely none.
|
|
212
149
|
return null;
|
|
213
150
|
}
|
|
214
151
|
}
|
|
215
|
-
// A number stored under a date format is a date serial — surface it as a Date so a date read from an
|
|
216
|
-
// `.xlsb` is the same value the `.xlsx` twin yields, not a bare number.
|
|
217
152
|
function asNumberOrDate(value, numFmt) {
|
|
218
153
|
return numFmt !== undefined && isDateFormat(numFmt) ? serialToDate(value) : value;
|
|
219
154
|
}
|
|
220
|
-
// `BrtRowHdr` ([MS-XLSB] 2.4.770): the row index, its default format, its height, and a byte of
|
|
221
|
-
// layout flags. Returns the open row (one-based) and the style index its cells inherit.
|
|
222
155
|
function applyRow(reader, sheet, defaultRowHeight) {
|
|
223
156
|
const index = reader.u32();
|
|
224
157
|
const styleIndex = reader.u32();
|
|
225
158
|
const height = reader.u16();
|
|
226
|
-
reader.skip(1);
|
|
159
|
+
reader.skip(1);
|
|
227
160
|
const flags = reader.u8();
|
|
228
|
-
// A row beyond the grid closes the open row without opening another, so its cells are dropped too.
|
|
229
161
|
if (index > MAX_ROW_INDEX)
|
|
230
162
|
return { row: -1, styleIndex: -1 };
|
|
231
163
|
const row = index + 1;
|
|
232
164
|
const handle = sheet.getRow(row);
|
|
233
|
-
// Every row header restates a height; only a row whose height is its *own* has one to record. That
|
|
234
|
-
// is a row the user sized by hand, or one Excel auto-fitted to a taller font or wrapped text — both
|
|
235
|
-
// differ from the sheet default, which is exactly when XML emits `ht`. A row merely restating the
|
|
236
|
-
// default carries no height, so it must not read back with one.
|
|
237
165
|
if ((flags & ROW_CUSTOM_HEIGHT) !== 0 || height !== defaultRowHeight) {
|
|
238
166
|
handle.height = height / TWIPS_PER_POINT;
|
|
239
167
|
}
|
|
@@ -244,7 +172,6 @@ function applyRow(reader, sheet, defaultRowHeight) {
|
|
|
244
172
|
handle.outlineLevel = outlineLevel;
|
|
245
173
|
if ((flags & ROW_COLLAPSED) !== 0)
|
|
246
174
|
handle.collapsed = true;
|
|
247
|
-
// The row's format applies only when it says so, mirroring XML's `customFormat="1"` gate.
|
|
248
175
|
return { row, styleIndex: (flags & ROW_CUSTOM_FORMAT) !== 0 ? styleIndex : -1 };
|
|
249
176
|
}
|
|
250
177
|
const TWIPS_PER_POINT = 20;
|
|
@@ -253,7 +180,6 @@ const ROW_COLLAPSED = 0b0000_1000;
|
|
|
253
180
|
const ROW_HIDDEN = 0b0001_0000;
|
|
254
181
|
const ROW_CUSTOM_HEIGHT = 0b0010_0000;
|
|
255
182
|
const ROW_CUSTOM_FORMAT = 0b0100_0000;
|
|
256
|
-
// `BrtColInfo` ([MS-XLSB] 2.4.319): one record per run of identically-sized columns.
|
|
257
183
|
function applyColumn(reader, sheet, xfStyles, columnStyle) {
|
|
258
184
|
const first = reader.u32();
|
|
259
185
|
const last = reader.u32();
|
|
@@ -262,24 +188,16 @@ function applyColumn(reader, sheet, xfStyles, columnStyle) {
|
|
|
262
188
|
const flags = reader.u16();
|
|
263
189
|
const style = styleIndex > 0 ? xfStyles[styleIndex] : undefined;
|
|
264
190
|
const outlineLevel = (flags >> COLUMN_OUTLINE_SHIFT) & 0b111;
|
|
265
|
-
// A run can name every column to the right of the data; materialising all 16 384 of them would
|
|
266
|
-
// turn a two-column sheet into a 16 384-entry model, so a run is only applied where it carries
|
|
267
|
-
// something a default column does not.
|
|
268
191
|
if (style === undefined &&
|
|
269
192
|
outlineLevel === 0 &&
|
|
270
193
|
(flags & (COLUMN_HIDDEN | COLUMN_CUSTOM_WIDTH | COLUMN_COLLAPSED)) === 0) {
|
|
271
194
|
return;
|
|
272
195
|
}
|
|
273
|
-
// The loop bound comes from the file, so it is clamped to the grid before it is one: an unclamped
|
|
274
|
-
// run declaring four billion columns is a denial of service, not a wide sheet.
|
|
275
196
|
const lastInGrid = Math.min(last, MAX_COLUMN_INDEX);
|
|
276
197
|
if (first > lastInGrid)
|
|
277
198
|
return;
|
|
278
199
|
for (let index = first; index <= lastInGrid; index++) {
|
|
279
200
|
const column = sheet.getColumn(index + 1);
|
|
280
|
-
// The stored width is taken whether or not the file marks it user-set, matching the XML reader:
|
|
281
|
-
// a `<col>`/`BrtColInfo` exists only for a column that differs from the sheet default in *some*
|
|
282
|
-
// way, and it always states the width that column actually has.
|
|
283
201
|
column.width = width / COLUMN_WIDTH_UNITS;
|
|
284
202
|
if ((flags & COLUMN_HIDDEN) !== 0)
|
|
285
203
|
column.hidden = true;
|
|
@@ -293,7 +211,6 @@ function applyColumn(reader, sheet, xfStyles, columnStyle) {
|
|
|
293
211
|
columnStyle.set(index, styleIndex);
|
|
294
212
|
}
|
|
295
213
|
}
|
|
296
|
-
// Column width is stored in 1/256ths of a character, where XML states the character count directly.
|
|
297
214
|
const COLUMN_WIDTH_UNITS = 256;
|
|
298
215
|
const COLUMN_HIDDEN = 0b0000_0001;
|
|
299
216
|
const COLUMN_CUSTOM_WIDTH = 0b0000_0010;
|
package/dist/io/xlsb/read.js
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
// The buffered `.xlsb` reader: a binary OPC package in, a {@link Workbook} model out.
|
|
2
|
-
//
|
|
3
|
-
// An `.xlsb` is the *same* OPC/ZIP container and the *same* relationship graph as an `.xlsx` — the
|
|
4
|
-
// workbook part points at its sheets, its shared strings, and its style sheet through ordinary
|
|
5
|
-
// `.rels` XML. Only the office-document parts differ: `xl/workbook.bin`, `xl/worksheets/sheetN.bin`
|
|
6
|
-
// and friends are BIFF12 record streams instead of XML. So this module reuses the container layer
|
|
7
|
-
// wholesale (`../opc/`, the bounded inflater) and swaps only the part parsers, which is
|
|
8
|
-
// exactly the "two codecs over one model" shape the format note argues for.
|
|
9
|
-
//
|
|
10
|
-
// The model produced is the one `readXlsx` produces, not a parallel one: the same `Workbook`, the
|
|
11
|
-
// same `XfStyle` table, the same cells. That is what lets a caller convert between the two forms —
|
|
12
|
-
// and what the corpus asserts, by reading a workbook Excel saved in both forms and comparing.
|
|
13
|
-
//
|
|
14
|
-
// Not yet decoded (each its own slice of work, none silently wrong): rich-text runs, tables, pivots,
|
|
15
|
-
// and conditional formatting.
|
|
16
1
|
import { unmangleFunctions } from '../../core/formula.js';
|
|
17
2
|
import { INTERNAL } from '../../core/internal.js';
|
|
18
3
|
import { Workbook } from '../../core/workbook.js';
|
|
@@ -27,26 +12,11 @@ import { parseStyleTable } from './read-styles.js';
|
|
|
27
12
|
import { parseWorksheet } from './read-worksheet.js';
|
|
28
13
|
import { readRecords } from './record-stream.js';
|
|
29
14
|
import { BRT } from './record-types.js';
|
|
30
|
-
/** The office-document part every `.xlsb` package is entered through. */
|
|
31
15
|
export const XLSB_WORKBOOK_PART = 'xl/workbook.bin';
|
|
32
|
-
/**
|
|
33
|
-
* Read an `.xlsb` (binary BIFF12) package into a {@link Workbook}.
|
|
34
|
-
*
|
|
35
|
-
* @throws {UnsupportedFormatError} if the input is not an `.xlsb` package — a legacy `.xls`
|
|
36
|
-
* (`.format === 'xls'`), an XML `.xlsx` or unrecognised blob (`'unknown'`).
|
|
37
|
-
* @throws {XlsbParseError} if a binary part is malformed.
|
|
38
|
-
* @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
39
|
-
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
40
|
-
*/
|
|
41
16
|
export function readXlsb(data, options = {}) {
|
|
42
17
|
const cap = options.maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
|
|
43
18
|
return readXlsbPackage(inflateSpreadsheetPackage(data, cap));
|
|
44
19
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Build the model from an already-inflated `.xlsb` package. Separate from {@link readXlsb} so the
|
|
47
|
-
* `.xlsx` reader can hand over a package it has already inflated and classified, rather than
|
|
48
|
-
* inflating the same bytes twice.
|
|
49
|
-
*/
|
|
50
20
|
export function readXlsbPackage(files) {
|
|
51
21
|
const { partText, partBytes } = packageAccessors(files);
|
|
52
22
|
const workbookPart = partBytes(XLSB_WORKBOOK_PART);
|
|
@@ -57,12 +27,8 @@ export function readXlsbPackage(files) {
|
|
|
57
27
|
const sharedStrings = parseSharedStrings(partBytes('xl/sharedStrings.bin'));
|
|
58
28
|
const { cellXfs, namedStyles, defaultFont } = parseStyleTable(partBytes('xl/styles.bin'));
|
|
59
29
|
const workbook = new Workbook();
|
|
60
|
-
// As in the XML reader, the named-style layer is restored only when a file declares more than the
|
|
61
|
-
// Normal default, so an ordinary workbook keeps an empty table and writes just that default back.
|
|
62
30
|
if (namedStyles.length > 1)
|
|
63
31
|
workbook[INTERNAL].restoreNamedStyles(namedStyles);
|
|
64
|
-
// As in the XML reader, font 0 is the workbook's declared default and must survive a re-write; an
|
|
65
|
-
// assumed Calibri in its place changes every empty cell and every character-unit column width.
|
|
66
32
|
workbook[INTERNAL].restoreDefaultFont(defaultFont);
|
|
67
33
|
const declaration = readWorkbookPart(workbookPart);
|
|
68
34
|
const scope = {
|
|
@@ -82,17 +48,12 @@ export function readXlsbPackage(files) {
|
|
|
82
48
|
workbook.defineName(defined);
|
|
83
49
|
return workbook;
|
|
84
50
|
}
|
|
85
|
-
// One pass over `xl/workbook.bin`, gathering everything the rest of the read depends on: the sheet
|
|
86
|
-
// bundle, the externals block a 3-D reference resolves through, and the defined names.
|
|
87
51
|
function readWorkbookPart(part) {
|
|
88
52
|
const sheets = [];
|
|
89
53
|
const names = [];
|
|
90
54
|
let externSheets = [];
|
|
91
55
|
let inBundle = false;
|
|
92
56
|
let inExternals = false;
|
|
93
|
-
// A workbook with no external links declares exactly one supporting book: itself. Rather than
|
|
94
|
-
// enumerate every record type that could open another — and risk miscounting into a *wrong* sheet
|
|
95
|
-
// name — anything else inside the externals block disqualifies the whole table.
|
|
96
57
|
let supportingBooks = 0;
|
|
97
58
|
let selfSupBook;
|
|
98
59
|
for (const record of readRecords(part)) {
|
|
@@ -122,21 +83,17 @@ function readWorkbookPart(part) {
|
|
|
122
83
|
selfSupBook: supportingBooks === 1 ? selfSupBook : undefined,
|
|
123
84
|
};
|
|
124
85
|
}
|
|
125
|
-
// `BrtBundleSh` ([MS-XLSB] 2.4.303) — the binary spelling of `<sheet name state r:id/>`.
|
|
126
86
|
function readSheet(data) {
|
|
127
87
|
const reader = new RecordReader(data);
|
|
128
88
|
const state = SHEET_STATES[reader.u32()] ?? 'visible';
|
|
129
|
-
reader.skip(4);
|
|
89
|
+
reader.skip(4);
|
|
130
90
|
const relId = reader.nullableWideString();
|
|
131
91
|
return { name: reader.wideString(), relId, state };
|
|
132
92
|
}
|
|
133
|
-
// `BrtExternSheet` ([MS-XLSB] 2.4.677): the `Xti` table every 3-D reference indexes into.
|
|
134
93
|
function readExternSheets(data) {
|
|
135
94
|
const reader = new RecordReader(data);
|
|
136
95
|
const count = reader.u32();
|
|
137
96
|
const entries = [];
|
|
138
|
-
// Each entry is three 4-byte fields; checking the count against what the record holds keeps a forged
|
|
139
|
-
// one from driving the loop rather than the record's own length.
|
|
140
97
|
if (count * XTI_BYTES > reader.remaining)
|
|
141
98
|
return entries;
|
|
142
99
|
for (let index = 0; index < count; index++) {
|
|
@@ -145,12 +102,10 @@ function readExternSheets(data) {
|
|
|
145
102
|
return entries;
|
|
146
103
|
}
|
|
147
104
|
const XTI_BYTES = 12;
|
|
148
|
-
// `BrtName` ([MS-XLSB] 2.4.673). The target is left undecoded here: it is a token stream that may cite
|
|
149
|
-
// a sheet by index, and the sheet bundle is not necessarily complete at this point in the stream.
|
|
150
105
|
function readName(data) {
|
|
151
106
|
const reader = new RecordReader(data);
|
|
152
107
|
const flags = reader.u32();
|
|
153
|
-
reader.skip(1);
|
|
108
|
+
reader.skip(1);
|
|
154
109
|
const itab = reader.u32();
|
|
155
110
|
const name = reader.wideString();
|
|
156
111
|
const rgce = reader.bytes(reader.u32());
|
|
@@ -163,19 +118,8 @@ function readName(data) {
|
|
|
163
118
|
rgcb,
|
|
164
119
|
};
|
|
165
120
|
}
|
|
166
|
-
// `itab` for a workbook-global name; any other value is a zero-based sheet index.
|
|
167
121
|
const GLOBAL_NAME_SCOPE = 0xffffffff;
|
|
168
|
-
// `fFunc`: the name registers something callable rather than a range.
|
|
169
122
|
const NAME_IS_FUNCTION = 0x00000002;
|
|
170
|
-
// The workbook's defined names, as the model holds them.
|
|
171
|
-
//
|
|
172
|
-
// Two kinds of `BrtName` are dropped, both because the XML form does not persist them either — so
|
|
173
|
-
// carrying them through would make the two readings of one workbook disagree. A *function* name is
|
|
174
|
-
// Excel's registration of a callable (every post-2007 function gets one, `_xlfn.TEXTJOIN` and
|
|
175
|
-
// friends); its target is the placeholder `#NAME?`, not a range. And `_xlnm._FilterDatabase` is the
|
|
176
|
-
// built-in Excel derives from a sheet's autofilter, which the model reconstructs from the autofilter
|
|
177
|
-
// itself. A name whose target uses a token this reader cannot decode is dropped too, rather than
|
|
178
|
-
// surfaced with a target that is a guess.
|
|
179
123
|
function definedNames(declaration, scope) {
|
|
180
124
|
const names = [];
|
|
181
125
|
for (const declared of declaration.names) {
|
|
@@ -188,12 +132,10 @@ function definedNames(declaration, scope) {
|
|
|
188
132
|
names.push({
|
|
189
133
|
name: declared.name,
|
|
190
134
|
...(sheet === undefined ? {} : { scope: sheet }),
|
|
191
|
-
// Stripped back to the readable form, the same normalisation the XML reader applies.
|
|
192
135
|
refersTo: unmangleFunctions(refersTo),
|
|
193
136
|
});
|
|
194
137
|
}
|
|
195
138
|
return names;
|
|
196
139
|
}
|
|
197
140
|
const FILTER_DATABASE_NAME = '_xlnm._FilterDatabase';
|
|
198
|
-
// `hsState` ([MS-XLSB] 2.4.303), indexed by its stored value.
|
|
199
141
|
const SHEET_STATES = ['visible', 'hidden', 'veryHidden'];
|
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
// The BIFF12 record framing every `.bin` part of an `.xlsb` package is built from ([MS-XLSB] 2.1.4).
|
|
2
|
-
//
|
|
3
|
-
// A part is a bare concatenation of records — no header, no index, no terminator — each framed as a
|
|
4
|
-
// variable-length type, a variable-length size, and that many payload bytes. Both prefixes are
|
|
5
|
-
// 7-bits-per-byte with the high bit meaning "one more byte follows": the type is 1–2 bytes (so the
|
|
6
|
-
// single-byte space is reserved for the hot records — a cell is one byte of framing), the size is 1–4.
|
|
7
|
-
//
|
|
8
|
-
// **This is the reader's hostile-input frontier.** The declared size is attacker-controlled, so it is
|
|
9
|
-
// never allowed to drive an allocation: a record's payload is handed out as a `subarray` *view* onto
|
|
10
|
-
// the part the inflate bound already materialised and capped, and a size that would run past the end
|
|
11
|
-
// of the part is rejected outright rather than clamped. A lying length therefore costs nothing and
|
|
12
|
-
// fails closed, and the whole stream's memory is exactly the part's own bytes.
|
|
13
1
|
import { XlsbParseError } from './errors.js';
|
|
14
|
-
/**
|
|
15
|
-
* Decode a `.bin` part into its record sequence, in order.
|
|
16
|
-
*
|
|
17
|
-
* Lazy: a caller that stops early (having found what it needs) never frames the rest of the part.
|
|
18
|
-
*
|
|
19
|
-
* @throws {XlsbParseError} if a record header is truncated or a record's declared size runs past the
|
|
20
|
-
* end of the part.
|
|
21
|
-
*/
|
|
22
2
|
export function* readRecords(part) {
|
|
23
3
|
let offset = 0;
|
|
24
4
|
while (offset < part.length) {
|
|
@@ -26,8 +6,6 @@ export function* readRecords(part) {
|
|
|
26
6
|
let type = lowType & 0x7f;
|
|
27
7
|
if ((lowType & 0x80) !== 0)
|
|
28
8
|
type |= (byteAt(part, offset++) & 0x7f) << 7;
|
|
29
|
-
// Up to four size bytes, least-significant seven bits first. The fourth byte's continuation bit
|
|
30
|
-
// is specified as ignored, which the fixed bound expresses: the loop simply stops there.
|
|
31
9
|
let size = 0;
|
|
32
10
|
for (let index = 0; index < 4; index++) {
|
|
33
11
|
const piece = byteAt(part, offset++);
|
|
@@ -35,9 +13,6 @@ export function* readRecords(part) {
|
|
|
35
13
|
if ((piece & 0x80) === 0)
|
|
36
14
|
break;
|
|
37
15
|
}
|
|
38
|
-
// The one check that makes a forged size harmless: compare it against what the part *actually*
|
|
39
|
-
// holds, before anything is handed out. `part.length - offset` cannot be negative here — byteAt
|
|
40
|
-
// has already proven every header byte was in range.
|
|
41
16
|
if (size > part.length - offset) {
|
|
42
17
|
throw new XlsbParseError(`BIFF12 record ${type} declares ${size} bytes but only ${part.length - offset} remain in the part`);
|
|
43
18
|
}
|
|
@@ -45,9 +20,6 @@ export function* readRecords(part) {
|
|
|
45
20
|
offset += size;
|
|
46
21
|
}
|
|
47
22
|
}
|
|
48
|
-
// Every header byte goes through here, so a part that ends mid-header fails closed rather than
|
|
49
|
-
// folding `undefined` into the arithmetic (which `noUncheckedIndexedAccess` would otherwise let
|
|
50
|
-
// through as a silent NaN-shaped type or size).
|
|
51
23
|
function byteAt(part, index) {
|
|
52
24
|
const value = part[index];
|
|
53
25
|
if (value === undefined)
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
// The BIFF12 record types this reader consumes, by their [MS-XLSB] "Record Enumeration" (section 2.3)
|
|
2
|
-
// numbers. Named constants rather than magic numbers at the switch sites: a record number carries no
|
|
3
|
-
// hint of what it is, so `BRT.CellRk` is the difference between a readable parser and a table of
|
|
4
|
-
// unexplained integers.
|
|
5
|
-
//
|
|
6
|
-
// Deliberately partial. [MS-XLSB] enumerates ~760 record types; listing the ones we do not decode
|
|
7
|
-
// would claim coverage we do not have. A record whose type is absent here is skipped by the parsers,
|
|
8
|
-
// which is the correct behaviour for a forward-compatible reader — the format grows, and an unknown
|
|
9
|
-
// record is always safely framed (its size is in the stream) even when its meaning is not known.
|
|
10
|
-
/** BIFF12 record numbers, grouped by the part whose stream they appear in. */
|
|
11
1
|
export const BRT = {
|
|
12
|
-
// Cell table (worksheet part). The single-byte record space — these are the hot path.
|
|
13
2
|
RowHdr: 0,
|
|
14
3
|
CellBlank: 1,
|
|
15
4
|
CellRk: 2,
|
|
@@ -25,13 +14,9 @@ export const BRT = {
|
|
|
25
14
|
CellRString: 62,
|
|
26
15
|
ColInfo: 60,
|
|
27
16
|
MergeCell: 176,
|
|
28
|
-
// Shared-string table.
|
|
29
17
|
SSTItem: 19,
|
|
30
18
|
BeginSst: 159,
|
|
31
19
|
EndSst: 160,
|
|
32
|
-
// Style sheet: each collection is a Begin/End pair around its entries, and `XF` appears inside two
|
|
33
|
-
// of them (the named-style layer and the direct-format layer), so the style parser tracks which
|
|
34
|
-
// collection it is in rather than keying on the record number alone.
|
|
35
20
|
Fmt: 44,
|
|
36
21
|
Font: 43,
|
|
37
22
|
Fill: 45,
|
|
@@ -52,18 +37,14 @@ export const BRT = {
|
|
|
52
37
|
EndCellXFs: 618,
|
|
53
38
|
BeginStyles: 619,
|
|
54
39
|
EndStyles: 620,
|
|
55
|
-
// Workbook part.
|
|
56
40
|
BundleSh: 156,
|
|
57
41
|
BeginBundleShs: 143,
|
|
58
42
|
EndBundleShs: 144,
|
|
59
43
|
Name: 39,
|
|
60
|
-
// The externals block: which workbooks a formula can reach, and which sheets of them each `ixti` a
|
|
61
|
-
// 3-D reference carries names. `SupSelf` declares a supporting book that is *this* workbook.
|
|
62
44
|
BeginExternals: 353,
|
|
63
45
|
EndExternals: 354,
|
|
64
46
|
SupSelf: 357,
|
|
65
47
|
ExternSheet: 362,
|
|
66
|
-
// Worksheet structure.
|
|
67
48
|
WsProp: 147,
|
|
68
49
|
WsDim: 148,
|
|
69
50
|
WsFmtInfo: 485,
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// Cell-gathering state machine for the worksheet-body reader. It owns the still-textual pieces of the
|
|
2
|
-
// one `<c>` currently being read — its address/type/style, formula, cached value, inline text, and
|
|
3
|
-
// rich-text runs — plus the shared-formula master map that spans the whole sheet. Each `<c>` resets
|
|
4
|
-
// the per-cell state on {@link beginCell} and commits it on {@link finalize}, so this class is the
|
|
5
|
-
// single owner of "what has this cell gathered so far" — to a cell what {@link RunAccumulator} is to a
|
|
6
|
-
// rich string. Value *decoding* stays in `cell-value.ts`; this class only gathers the raw pieces.
|
|
7
1
|
import { decodeAddress, encodeAddress } from '../../core/address.js';
|
|
8
2
|
import { translateFormula, unmangleFunctions } from '../../core/formula.js';
|
|
9
3
|
import { boolPresent } from '../../xml/xml-read.js';
|
|
@@ -17,8 +11,6 @@ export class CellAccumulator {
|
|
|
17
11
|
#col = -1;
|
|
18
12
|
#row = -1;
|
|
19
13
|
#formula = '';
|
|
20
|
-
// Shared-formula bookkeeping. A master `<f t="shared" ref si>TEXT</f>` seeds the group; every clone
|
|
21
|
-
// `<f t="shared" si/>` in the sheet references it by `si` and carries no text of its own.
|
|
22
14
|
#formulaShared = false;
|
|
23
15
|
#formulaSi = -1;
|
|
24
16
|
#sharedClone = false;
|
|
@@ -28,27 +20,19 @@ export class CellAccumulator {
|
|
|
28
20
|
#hasFormula = false;
|
|
29
21
|
#hasValue = false;
|
|
30
22
|
#runs = new RunAccumulator();
|
|
31
|
-
// Masters always precede their clones (Excel keeps the master top-left), so a clone resolves against
|
|
32
|
-
// a map filled as the sheet streams: the master's formula translated to the clone's position.
|
|
33
23
|
#masters = new Map();
|
|
34
|
-
/** This cell's `<c r>` address (`"B3"`), or '' when it carried none. */
|
|
35
24
|
get ref() {
|
|
36
25
|
return this.#ref;
|
|
37
26
|
}
|
|
38
|
-
/** This cell's own `<c s>` style index, or -1 when it carries none. */
|
|
39
27
|
get styleIndex() {
|
|
40
28
|
return this.#style;
|
|
41
29
|
}
|
|
42
|
-
/** This cell's 1-based column, or -1 when its address was absent or unparseable. */
|
|
43
30
|
get col() {
|
|
44
31
|
return this.#col;
|
|
45
32
|
}
|
|
46
|
-
/** The rich-text run accumulator, driven by the surrounding parser's `<r>`/`<rPr>` handling. */
|
|
47
33
|
get runs() {
|
|
48
34
|
return this.#runs;
|
|
49
35
|
}
|
|
50
|
-
// Begin a new `<c>`: record its address/type/style and clear every per-cell gathered field so the
|
|
51
|
-
// last cell's formula, value, runs, or shared/data-table declaration cannot bleed into this one.
|
|
52
36
|
beginCell(attrs) {
|
|
53
37
|
this.#ref = attrs.r ?? '';
|
|
54
38
|
this.#type = attrs.t ?? '';
|
|
@@ -66,9 +50,6 @@ export class CellAccumulator {
|
|
|
66
50
|
this.#sharedClone = false;
|
|
67
51
|
this.#dataTable = null;
|
|
68
52
|
}
|
|
69
|
-
// Begin an `<f>`: record its shared-formula grouping and any data-table declaration. A self-closing
|
|
70
|
-
// `<f t="shared" si/>` is a clone — it fires no close and carries no text — so mark it here to
|
|
71
|
-
// resolve against its master when the cell finalises.
|
|
72
53
|
beginFormula(attrs, selfClosing) {
|
|
73
54
|
this.#formulaShared = attrs.t === 'shared';
|
|
74
55
|
this.#formulaSi = attrs.si !== undefined ? Number(attrs.si) : -1;
|
|
@@ -92,22 +73,14 @@ export class CellAccumulator {
|
|
|
92
73
|
this.#valueText = text;
|
|
93
74
|
this.#hasValue = true;
|
|
94
75
|
}
|
|
95
|
-
// Begin an `<is>`: clear the inline string and its runs so a rich value built from a previous cell's
|
|
96
|
-
// runs keeps its own array.
|
|
97
76
|
beginInlineString() {
|
|
98
77
|
this.#inlineText = '';
|
|
99
78
|
this.#runs.reset();
|
|
100
79
|
}
|
|
101
|
-
// Route a `<t>`'s text: to the open run when one is active, otherwise to the inline string when the
|
|
102
|
-
// parser is inside an `<is>`. A run takes precedence — a run is also inside the inline string.
|
|
103
80
|
appendText(text, inInlineString) {
|
|
104
81
|
if (!this.#runs.appendText(text) && inInlineString)
|
|
105
82
|
this.#inlineText += text;
|
|
106
83
|
}
|
|
107
|
-
// Commit the gathered cell to the sheet with its already-resolved style (the caller applies the
|
|
108
|
-
// cell → row → column inheritance order). A data-table cell surfaces its declaration; a shared-formula
|
|
109
|
-
// master seeds the group before finalising, and a clone resolves to the master translated to its own
|
|
110
|
-
// position. Everything else decodes as an ordinary `<c>` payload. An address-less cell is a no-op.
|
|
111
84
|
finalize(sheet, sharedStrings, style) {
|
|
112
85
|
if (this.#ref === '')
|
|
113
86
|
return;
|
|
@@ -138,7 +111,6 @@ export class CellAccumulator {
|
|
|
138
111
|
const value = {
|
|
139
112
|
sharedFormula: encodeAddress(master.col, master.row),
|
|
140
113
|
formula: unmangleFunctions(translated),
|
|
141
|
-
// A clone's cached result honours the cell's date format the same way a plain formula's does.
|
|
142
114
|
...(this.#hasValue
|
|
143
115
|
? { result: decodeFormulaResult(this.#type, this.#valueText, style?.numFmt) }
|
|
144
116
|
: {}),
|
|
@@ -153,10 +125,6 @@ export class CellAccumulator {
|
|
|
153
125
|
applyXfToCell(cell, style);
|
|
154
126
|
cell.value = this.decode(sharedStrings, style);
|
|
155
127
|
}
|
|
156
|
-
// Decode the gathered pieces into a plain cell value, resolving the shared pool and date formats but
|
|
157
|
-
// NOT the shared-formula / data-table declarations {@link finalize} handles. This is what a data
|
|
158
|
-
// read (the streaming reader) wants: the cell's own value, with a shared-formula clone surfacing its
|
|
159
|
-
// cached result rather than a translated formula it will not evaluate.
|
|
160
128
|
decode(sharedStrings, style) {
|
|
161
129
|
const raw = {
|
|
162
130
|
type: this.#type,
|
|
@@ -1,34 +1,17 @@
|
|
|
1
|
-
// Decoding a worksheet cell's on-disk `<c>` payload into a model {@link CellValue}.
|
|
2
|
-
//
|
|
3
|
-
// This is the single value-decoding surface both readers share: the buffered reader
|
|
4
|
-
// (`./read.ts`) and the streaming row reader (`./read-rows.ts`). Keeping it in one place is
|
|
5
|
-
// what guarantees a cell read one row at a time decodes identically to the same cell read as
|
|
6
|
-
// part of a whole workbook — a divergence here would be a silent data bug in exactly one path.
|
|
7
1
|
import { isDateFormat, serialToDate } from '../../core/date.js';
|
|
8
2
|
import { unmangleFunctions } from '../../core/formula.js';
|
|
9
3
|
import { isErrorCode, } from '../../core/value.js';
|
|
10
4
|
import { boolStrict } from '../../xml/xml-read.js';
|
|
11
|
-
/**
|
|
12
|
-
* Decode a gathered cell into its model value. A formula cell becomes a `{formula, result?}`
|
|
13
|
-
* object (the on-disk `_xlfn.`/`_xlpm.` mangling stripped back to the readable name); a plain
|
|
14
|
-
* numeric cell under a date number format becomes a {@link Date}; everything else decodes by its
|
|
15
|
-
* `t` type. `numFmt` is the cell's resolved number-format code, used only for date detection.
|
|
16
|
-
*/
|
|
17
5
|
export function decodeCellContent(raw, sharedStrings, numFmt) {
|
|
18
6
|
if (raw.hasFormula) {
|
|
19
7
|
const stored = unmangleFunctions(raw.formula);
|
|
20
8
|
const result = raw.hasValue ? decodeFormulaResult(raw.type, raw.valueText, numFmt) : undefined;
|
|
21
9
|
return result === undefined ? { formula: stored } : { formula: stored, result };
|
|
22
10
|
}
|
|
23
|
-
// An inline string built from `<r>` runs is rich text — surface its runs rather than flattening
|
|
24
|
-
// them to the concatenated `inlineText` a plain string would decode to.
|
|
25
11
|
if (raw.type === 'inlineStr' && raw.richTextRuns !== undefined && raw.richTextRuns.length > 0) {
|
|
26
12
|
return { richText: raw.richTextRuns };
|
|
27
13
|
}
|
|
28
14
|
const value = decodeValue(raw.type, raw.valueText, raw.inlineText, raw.hasValue, sharedStrings);
|
|
29
|
-
// A number stored under a date format is a date serial — surface it as a Date so a written
|
|
30
|
-
// date round-trips as a date, not a bare number. Only plain numeric cells qualify; a string,
|
|
31
|
-
// boolean, or formula result under a date format keeps its own kind.
|
|
32
15
|
return typeof value === 'number' && numFmt !== undefined && isDateFormat(numFmt)
|
|
33
16
|
? serialToDate(value)
|
|
34
17
|
: value;
|
|
@@ -40,13 +23,8 @@ function decodeValue(type, valueText, inlineText, hasValue, sharedStrings) {
|
|
|
40
23
|
case 'str':
|
|
41
24
|
return valueText;
|
|
42
25
|
case 'd':
|
|
43
|
-
// A Strict-mode (ISO/IEC 29500 Strict) date cell stores an ISO 8601 value directly, not a
|
|
44
|
-
// serial. Parse it literally — an ISO date is UTC — so it reads as the date it states rather
|
|
45
|
-
// than a 1900-epoch serial the transitional decoder would fabricate from the text.
|
|
46
26
|
return valueText === '' ? null : new Date(valueText);
|
|
47
27
|
case 's': {
|
|
48
|
-
// A `t="s"` cell indexes the shared pool; the entry is a plain string or, when Excel pooled a
|
|
49
|
-
// rich value, a {@link RichTextValue} whose runs surface here rather than being flattened.
|
|
50
28
|
const index = Number(valueText);
|
|
51
29
|
return Number.isInteger(index) ? (sharedStrings[index] ?? '') : '';
|
|
52
30
|
}
|
|
@@ -58,19 +36,12 @@ function decodeValue(type, valueText, inlineText, hasValue, sharedStrings) {
|
|
|
58
36
|
return hasValue ? Number(valueText) : null;
|
|
59
37
|
}
|
|
60
38
|
}
|
|
61
|
-
/** Decode a formula's cached `<v>` result by its `t` type, coercing a numeric result under a date
|
|
62
|
-
* `numFmt` to a {@link Date} exactly as a bare numeric cell is — so a date-valued formula result
|
|
63
|
-
* (e.g. `TODAY()`) reads back as a Date, not a serial. Shared by the buffered reader's shared-formula
|
|
64
|
-
* clone resolution, which caches a result the same way a plain formula cell does. */
|
|
65
39
|
export function decodeFormulaResult(type, valueText, numFmt) {
|
|
66
40
|
const result = decodeResult(type, valueText);
|
|
67
41
|
return typeof result === 'number' && numFmt !== undefined && isDateFormat(numFmt)
|
|
68
42
|
? serialToDate(result)
|
|
69
43
|
: result;
|
|
70
44
|
}
|
|
71
|
-
// The formula-result subset of `decodeValue`: a cached result is only ever a string, boolean,
|
|
72
|
-
// error, or number — never a shared-string index, inline string, or Strict-mode date — so this
|
|
73
|
-
// handles just those cases rather than the full cell-value grammar.
|
|
74
45
|
function decodeResult(type, valueText) {
|
|
75
46
|
switch (type) {
|
|
76
47
|
case 'str':
|