@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.
Files changed (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /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); // dxGCol, cchDefColWidth: the default *column* width, which the model does not read.
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); // fAlwaysCalc: a recalculation hint, not part of the formula.
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); // grbitFlags: per-cell recalculation hints the model does not carry.
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); // fExtraAsc/fExtraDsc: border padding, a rendering hint the model does not carry.
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;
@@ -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); // iTabId: the sheet's stable id, which the model assigns itself.
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); // chKey: the Alt-key shortcut a macro name can carry.
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':