@shbernal/ts-xlsx 1.1.0 → 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 (127) 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.js +0 -131
  5. package/dist/core/color-resolution.js +0 -60
  6. package/dist/core/column.js +0 -60
  7. package/dist/core/comment-thread.js +0 -45
  8. package/dist/core/conditional-formatting-overlay.js +0 -11
  9. package/dist/core/conditional-formatting.js +0 -10
  10. package/dist/core/containers.js +0 -15
  11. package/dist/core/data-validation-overlay.js +0 -25
  12. package/dist/core/data-validation.js +0 -7
  13. package/dist/core/date.js +0 -39
  14. package/dist/core/formula.js +0 -117
  15. package/dist/core/grid-edits.js +0 -38
  16. package/dist/core/image.js +0 -25
  17. package/dist/core/internal.js +0 -27
  18. package/dist/core/limits.js +0 -45
  19. package/dist/core/merge.d.ts +18 -0
  20. package/dist/core/merge.js +22 -7
  21. package/dist/core/modern-functions.js +0 -17
  22. package/dist/core/page-setup.js +0 -5
  23. package/dist/core/pivot-table.d.ts +1 -1
  24. package/dist/core/pivot-table.js +0 -48
  25. package/dist/core/preserved.js +0 -4
  26. package/dist/core/protection.js +0 -26
  27. package/dist/core/range.js +0 -112
  28. package/dist/core/row-input.d.ts +24 -0
  29. package/dist/core/row-input.js +29 -0
  30. package/dist/core/row.js +0 -66
  31. package/dist/core/style.js +0 -26
  32. package/dist/core/table-style.js +0 -28
  33. package/dist/core/table.js +0 -97
  34. package/dist/core/text-metrics.js +0 -40
  35. package/dist/core/theme.js +0 -85
  36. package/dist/core/value.js +0 -94
  37. package/dist/core/workbook-protection.js +0 -13
  38. package/dist/core/workbook.js +0 -380
  39. package/dist/core/worksheet-model.js +2 -23
  40. package/dist/core/worksheet.js +7 -506
  41. package/dist/customui/errors.js +0 -10
  42. package/dist/customui/index.js +0 -5
  43. package/dist/customui/ribbon.js +0 -43
  44. package/dist/entries/core.js +0 -6
  45. package/dist/entries/csv.js +0 -4
  46. package/dist/entries/customui.js +0 -2
  47. package/dist/entries/errors.js +0 -13
  48. package/dist/entries/vba.js +0 -5
  49. package/dist/entries/xlsb.js +0 -6
  50. package/dist/entries/xlsx.js +0 -13
  51. package/dist/errors.d.ts +14 -0
  52. package/dist/errors.js +5 -56
  53. package/dist/index.js +0 -12
  54. package/dist/io/csv/read.js +0 -18
  55. package/dist/io/csv/write.js +0 -18
  56. package/dist/io/opc/errors.js +0 -30
  57. package/dist/io/opc/inflate.js +0 -35
  58. package/dist/io/opc/namespaces.js +0 -15
  59. package/dist/io/opc/part-paths.js +0 -11
  60. package/dist/io/opc/read-opc.js +0 -31
  61. package/dist/io/opc/read-options.js +0 -6
  62. package/dist/io/opc/rels.js +0 -13
  63. package/dist/io/opc/sniff-format.js +0 -50
  64. package/dist/io/style/xf-style.js +0 -31
  65. package/dist/io/xlsb/errors.js +0 -10
  66. package/dist/io/xlsb/formula.js +1 -99
  67. package/dist/io/xlsb/primitives.js +1 -83
  68. package/dist/io/xlsb/ptg-functions.js +0 -34
  69. package/dist/io/xlsb/read-shared-strings.js +0 -7
  70. package/dist/io/xlsb/read-styles.js +3 -66
  71. package/dist/io/xlsb/read-worksheet.js +4 -87
  72. package/dist/io/xlsb/read.js +2 -60
  73. package/dist/io/xlsb/record-stream.js +0 -28
  74. package/dist/io/xlsb/record-types.js +0 -19
  75. package/dist/io/xlsx/cell-accumulator.js +0 -32
  76. package/dist/io/xlsx/cell-value.js +0 -29
  77. package/dist/io/xlsx/color-xml.d.ts +6 -0
  78. package/dist/io/xlsx/color-xml.js +42 -0
  79. package/dist/io/xlsx/comments.js +0 -84
  80. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  81. package/dist/io/xlsx/conditional-formatting.js +1 -97
  82. package/dist/io/xlsx/data-validation.js +0 -56
  83. package/dist/io/xlsx/edit-vba.js +0 -54
  84. package/dist/io/xlsx/errors.js +0 -12
  85. package/dist/io/xlsx/hyperlinks.js +0 -40
  86. package/dist/io/xlsx/images.js +0 -32
  87. package/dist/io/xlsx/namespaces.js +0 -42
  88. package/dist/io/xlsx/package-plan.js +0 -47
  89. package/dist/io/xlsx/pivot.js +0 -22
  90. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  91. package/dist/io/xlsx/read-rows.js +1 -83
  92. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  93. package/dist/io/xlsx/read-styles.d.ts +25 -1
  94. package/dist/io/xlsx/read-styles.js +61 -108
  95. package/dist/io/xlsx/read-worksheet.js +1 -88
  96. package/dist/io/xlsx/read.js +3 -211
  97. package/dist/io/xlsx/relationships.js +0 -11
  98. package/dist/io/xlsx/rich-runs.js +0 -16
  99. package/dist/io/xlsx/rich-text.js +0 -12
  100. package/dist/io/xlsx/shared-formulas.js +0 -7
  101. package/dist/io/xlsx/shared-strings.js +0 -21
  102. package/dist/io/xlsx/sheet-properties.js +1 -64
  103. package/dist/io/xlsx/styles.d.ts +1 -29
  104. package/dist/io/xlsx/styles.js +2 -366
  105. package/dist/io/xlsx/tables.js +5 -52
  106. package/dist/io/xlsx/threaded-comments.js +0 -103
  107. package/dist/io/xlsx/workbook-xml.js +0 -94
  108. package/dist/io/xlsx/worksheet-xml.js +0 -174
  109. package/dist/io/xlsx/write-stream.js +0 -137
  110. package/dist/io/xlsx/write.js +1 -134
  111. package/dist/io/xlsx/x14-ext.js +0 -8
  112. package/dist/vba/cfb-writer.js +12 -59
  113. package/dist/vba/cfb.js +2 -32
  114. package/dist/vba/codepage.js +1 -23
  115. package/dist/vba/errors.js +0 -15
  116. package/dist/vba/index.js +0 -10
  117. package/dist/vba/ms-ovba.js +3 -46
  118. package/dist/vba/project-editor.js +4 -89
  119. package/dist/vba/project.js +2 -34
  120. package/dist/vba/vba-encoding.js +1 -17
  121. package/dist/xml/errors.js +0 -10
  122. package/dist/xml/xml-read.js +3 -82
  123. package/dist/xml/xml.js +0 -39
  124. package/package.json +8 -9
  125. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  126. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  127. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,42 +1,11 @@
1
- // The `Ftab` built-in function table ([MS-XLS] 2.5.198.17, shared verbatim by BIFF12): the index a
2
- // `PtgFunc`/`PtgFuncVar` token carries, mapped to the function name that index means.
3
- //
4
- // A binary formula names no function — it cites one by number — so this table is the whole difference
5
- // between `SUM(A1:A5)` and an unreadable stream. It is transcribed from the normative table rather
6
- // than assembled from Excel's UI, which matters at both ends: the low indices include the XLM macro
7
- // functions (`GOTO`, `ADD.BAR`, …) that no modern sheet uses but a legacy file may still carry, and
8
- // the table simply stops at `RTD`. Everything Microsoft added after that — the whole modern library,
9
- // `TEXTJOIN` through `LAMBDA` — is *not* here and never will be: a post-2007 function is called
10
- // through index 255 ("user defined") with an `_xlfn.`-prefixed name as its first operand, which is
11
- // the same indirection the XML form spells out in the function name itself.
12
- //
13
- // The four gaps (0x00CA–CB, 0x00D9–DA, 0x00F9–FA, 0x014D) are gaps in the specification: those
14
- // indices name no function, so a formula citing one is not decodable rather than decodable as
15
- // something adjacent.
16
- /** The index [MS-XLS] reserves for a call whose name is given by the token stream's first operand. */
17
1
  export const FTAB_USER_DEFINED = 0x00ff;
18
- /** Look up a built-in function name by its `iftab` index, or `undefined` if the index names none. */
19
2
  export function functionNameFor(index) {
20
3
  return FTAB[index];
21
4
  }
22
- /**
23
- * How many arguments a fixed-arity function takes, or `undefined` for one whose argument count varies.
24
- *
25
- * A `PtgFunc` token states only *which* function is called, never with how many arguments — it is the
26
- * token Excel emits precisely because the count is implied by the function. So decoding `ROUND(A1,2)`
27
- * back from its two operands and one call token is impossible without this: the arity is the only
28
- * thing that says which operands on the stack belong to the call. (`PtgFuncVar` carries its own count
29
- * and needs none of this; the two token forms partition the table between them.)
30
- *
31
- * Transcribed from the parameter grammar each function's [MS-XLS] 2.5.198.17 entry states.
32
- */
33
5
  export function fixedArityFor(name) {
34
6
  return FIXED_ARITY.get(name);
35
7
  }
36
- // Keyed by name rather than index: an arity belongs to the function, and a name is checkable at a
37
- // glance where a hex index is not.
38
8
  const FIXED_ARITY = new Map(Object.entries({
39
- // "This function takes no parameters."
40
9
  NA: 0,
41
10
  PI: 0,
42
11
  TRUE: 0,
@@ -248,8 +217,6 @@ const FIXED_ARITY = new Map(Object.entries({
248
217
  TTEST: 4,
249
218
  WEIBULL: 4,
250
219
  }));
251
- // The table as contiguous runs, each `[first index, names in order]`. Written this way so the four
252
- // specification gaps are visible as run boundaries instead of hiding as miscounted array entries.
253
220
  const RUNS = [
254
221
  [
255
222
  0x0000,
@@ -650,7 +617,6 @@ const RUNS = [
650
617
  ],
651
618
  ],
652
619
  ];
653
- // Indexed by `iftab`; a hole is an index the specification does not define.
654
620
  const FTAB = RUNS.reduce((table, [start, names]) => {
655
621
  names.forEach((name, offset) => {
656
622
  table[start + offset] = name;
@@ -1,13 +1,6 @@
1
- // The BIFF12 shared-string table: `xl/sharedStrings.bin` in, the pool a `BrtCellIsst` cell indexes.
2
- //
3
- // The binary table is a flat run of `BrtSSTItem` records between a Begin/End pair, each holding one
4
- // `RichStr`. Only the text is taken: a pooled string's per-run formatting is not modelled in this cut,
5
- // so the runs are left unread rather than half-decoded (see `primitives.ts`). Reading stops at the
6
- // closing record, so trailing future-record blocks are never framed.
7
1
  import { RecordReader } from './primitives.js';
8
2
  import { readRecords } from './record-stream.js';
9
3
  import { BRT } from './record-types.js';
10
- /** Parse `xl/sharedStrings.bin` into the pool, in index order. An absent part is an empty pool. */
11
4
  export function parseSharedStrings(part) {
12
5
  if (part === undefined)
13
6
  return [];
@@ -1,14 +1,3 @@
1
- // The BIFF12 style-sheet reader: `xl/styles.bin` in, the same {@link StyleTable} the XML reader
2
- // produces out. Every collection (`fmts`, `fonts`, `fills`, `borders`, `cellStyleXfs`, `cellXfs`,
3
- // `styles`) is a Begin/End record pair around its entries, so the pass tracks which collection it is
4
- // inside — `BrtXF` appears in two of them and is meaningless without that context.
5
- //
6
- // The resolution *rules* are deliberately not restated here: number-format ids go through the XML
7
- // reader's `numFmtCodeFor`, and each facet is emitted with the same "only when it differs from the
8
- // default" discipline the XML path uses. That is what makes the binary and XML readings of one
9
- // workbook the same model rather than two similar ones — a bottom vertical alignment, a locked cell,
10
- // or a General number format is written explicitly in BIFF12 and omitted in XML, so the binary side
11
- // has to drop exactly what the XML side never had.
12
1
  import { assignStyleFacets } from '../../core/style.js';
13
2
  import { numFmtCodeFor } from '../style/xf-style.js';
14
3
  import { RecordReader } from './primitives.js';
@@ -32,7 +21,6 @@ const COLLECTION_ENDS = new Set([
32
21
  BRT.EndCellXFs,
33
22
  BRT.EndStyles,
34
23
  ]);
35
- /** Parse `xl/styles.bin` into the flat cell-format table a worksheet's style indices resolve against. */
36
24
  export function parseStyleTable(part) {
37
25
  if (part === undefined)
38
26
  return { cellXfs: [], namedStyles: [] };
@@ -86,8 +74,6 @@ export function parseStyleTable(part) {
86
74
  break;
87
75
  }
88
76
  }
89
- // Layer each direct format over the named style its xfId links to, exactly as the XML reader does:
90
- // a facet the cell's own xf sets wins, one it leaves unset falls through to the named base.
91
77
  const cellXfs = directXfs.map((xf) => {
92
78
  if (xf.xfId === undefined)
93
79
  return xf;
@@ -104,22 +90,15 @@ export function parseStyleTable(part) {
104
90
  style.builtinId = label.builtinId;
105
91
  return style;
106
92
  });
107
- // Font 0 is the workbook's declared default, carried out whole as well as flattened onto the xfs
108
- // that name it — see {@link StyleTable.defaultFont}. The XML reader does the same.
109
93
  const defaultFont = fonts[0];
110
94
  return defaultFont === undefined ? { cellXfs, namedStyles } : { cellXfs, namedStyles, defaultFont };
111
95
  }
112
- // `BrtXF` ([MS-XLSB] 2.4.876): five facet indices, the two inline alignment scalars, then two flag
113
- // words. `isDirect` distinguishes a cellXfs entry (whose ixfeParent links to a named style) from a
114
- // cellStyleXfs entry (whose ixfeParent is the 0xFFFF "I am the named style" marker).
115
96
  function readXf(reader, deps, isDirect) {
116
97
  const parent = reader.u16();
117
98
  const numFmt = numFmtCodeFor(reader.u16(), deps.numFmtCodes);
118
99
  const font = deps.fonts[reader.u16()];
119
100
  const fill = deps.fills[reader.u16()];
120
101
  const borderIndex = reader.u16();
121
- // Border 0 is the genuinely empty default (font 0, by contrast, is the workbook's real default
122
- // face), so only a custom index names an actual border — the same asymmetry the XML reader keeps.
123
102
  const border = borderIndex > 0 ? deps.borders[borderIndex] : undefined;
124
103
  const rotation = reader.u8();
125
104
  const indent = reader.u8();
@@ -139,20 +118,13 @@ function readXf(reader, deps, isDirect) {
139
118
  const protection = readProtection(flags);
140
119
  if (protection !== undefined)
141
120
  draft.protection = protection;
142
- // f123Prefix is the binary spelling of the `quotePrefix` attribute: the cell's text is displayed
143
- // with a leading apostrophe stripped.
144
121
  if ((flags & 0x8000) !== 0)
145
122
  draft.quotePrefix = true;
146
- // Only a link beyond the Normal default (0) carries information, and only a direct format has one.
147
123
  if (isDirect && parent !== NOT_A_CELL_XF && parent > 0)
148
124
  draft.xfId = parent;
149
125
  return draft;
150
126
  }
151
- // The `ixfeParent` value a cell *style* XF carries in place of a link, since it is itself the base.
152
127
  const NOT_A_CELL_XF = 0xffff;
153
- // BIFF12 states every alignment field on every xf, where XML omits the ones at their default. Keep
154
- // only what the XML reader would have seen, so the two readings of one workbook agree: `general`
155
- // horizontal, `bottom` vertical, and zero rotation/indent/reading-order are absences, not values.
156
128
  function readAlignment(flags, rotation, indent) {
157
129
  const out = {};
158
130
  const horizontal = HORIZONTAL_ALIGNMENTS[flags & 0b111];
@@ -174,8 +146,6 @@ function readAlignment(flags, rotation, indent) {
174
146
  out.readingOrder = readingOrder;
175
147
  return Object.keys(out).length > 0 ? out : undefined;
176
148
  }
177
- // `locked` defaults to TRUE in OOXML, so an *unlocked* cell is the state that carries information;
178
- // `hidden` defaults to false, so only a set flag does. A default xf yields no protection at all.
179
149
  function readProtection(flags) {
180
150
  const out = {};
181
151
  if ((flags & 0x1000) === 0)
@@ -184,8 +154,6 @@ function readProtection(flags) {
184
154
  out.hidden = true;
185
155
  return Object.keys(out).length > 0 ? out : undefined;
186
156
  }
187
- // `alc` ([MS-XLSB] 2.4.876), indexed by its stored value. `general` is index 0 and is left out
188
- // deliberately: it is the type-dependent default, which the model spells as no horizontal alignment.
189
157
  const HORIZONTAL_ALIGNMENTS = [
190
158
  undefined,
191
159
  'left',
@@ -196,7 +164,6 @@ const HORIZONTAL_ALIGNMENTS = [
196
164
  'centerContinuous',
197
165
  'distributed',
198
166
  ];
199
- // `alcv`, likewise — with `bottom` (index 2) left out as the default.
200
167
  const VERTICAL_ALIGNMENTS = [
201
168
  'top',
202
169
  'center',
@@ -204,8 +171,6 @@ const VERTICAL_ALIGNMENTS = [
204
171
  'justify',
205
172
  'distributed',
206
173
  ];
207
- // `BrtFont` ([MS-XLSB] 2.4.690). Weight is a numeric scale (400 normal, 700 bold) rather than a flag,
208
- // and the italic/strike/outline bits live in a separate word from it.
209
174
  function readFont(reader) {
210
175
  const height = reader.u16();
211
176
  const flags = reader.u16();
@@ -214,14 +179,13 @@ function readFont(reader) {
214
179
  const underline = reader.u8();
215
180
  const family = reader.u8();
216
181
  const charset = reader.u8();
217
- reader.skip(1); // unused
182
+ reader.skip(1);
218
183
  const color = reader.color();
219
184
  const scheme = reader.u8();
220
185
  const name = reader.wideString();
221
186
  const font = {};
222
187
  if (name !== '')
223
188
  font.name = name;
224
- // Stored in twips; the model (like the XML) carries points.
225
189
  if (height > 0)
226
190
  font.size = height / 20;
227
191
  if (family !== 0)
@@ -234,8 +198,6 @@ function readFont(reader) {
234
198
  font.scheme = 'minor';
235
199
  if (color !== undefined)
236
200
  font.color = color;
237
- // Each boolean facet is recorded only when on, mirroring XML's present-or-absent `<b/>`/`<i/>`:
238
- // a non-bold font must not read back as `bold: false`.
239
201
  if (weight >= BOLD_WEIGHT)
240
202
  font.bold = true;
241
203
  if ((flags & 0b0000_0010) !== 0)
@@ -254,27 +216,16 @@ function readFont(reader) {
254
216
  return font;
255
217
  }
256
218
  const BOLD_WEIGHT = 700;
257
- // `uls` ([MS-XLSB] 2.4.690). A single underline is `true`, not `'single'`, because that is what XML's
258
- // bare `<u/>` — which is what Excel writes for it — reads back as.
259
219
  const UNDERLINE_STYLES = new Map([
260
220
  [0x01, true],
261
221
  [0x02, 'double'],
262
222
  [0x21, 'singleAccounting'],
263
223
  [0x22, 'doubleAccounting'],
264
224
  ]);
265
- // `BrtFill` ([MS-XLSB] 2.4.681). The pattern code and OOXML's `ST_PatternType` enumerate the same
266
- // patterns in the same order, so the code indexes the name list directly.
267
225
  function readFill(reader) {
268
226
  const pattern = FILL_PATTERNS[reader.u32()];
269
- // `none` is the absence of a fill, and an unmodelled pattern (a gradient — see below) is dropped
270
- // rather than guessed, so an unfilled cell reads back unfilled either way.
271
227
  if (pattern === undefined)
272
228
  return undefined;
273
- // BIFF12 always states both colours; XML states only the ones the fill actually has, using the two
274
- // legacy-palette sentinels for the rest — 64 is "automatic foreground", 65 "automatic background".
275
- // Dropping each in its own slot reproduces exactly what the XML reader sees: an untouched hatch
276
- // pattern carries no colours at all, while a solid fill keeps the explicit `bgColor indexed="64"`
277
- // Excel writes beside its foreground.
278
229
  const fgColor = notSentinel(reader.color(), AUTOMATIC_FOREGROUND);
279
230
  const bgColor = notSentinel(reader.color(), AUTOMATIC_BACKGROUND);
280
231
  return {
@@ -289,10 +240,6 @@ const AUTOMATIC_BACKGROUND = 65;
289
240
  function notSentinel(color, sentinel) {
290
241
  return color?.indexed === sentinel ? undefined : color;
291
242
  }
292
- // Indexed by the stored `fls` value. Index 0 (`none`) is deliberately absent: an unfilled cell
293
- // carries no fill. Gradient fills (`fls` 0x28) are not decoded in this cut — the stop array's layout
294
- // is the one piece of BrtFill this reader has no Excel-authored sample to check against, and a
295
- // silently wrong gradient is worse than none.
296
243
  const FILL_PATTERNS = [
297
244
  undefined,
298
245
  'solid',
@@ -314,9 +261,6 @@ const FILL_PATTERNS = [
314
261
  'gray125',
315
262
  'gray0625',
316
263
  ];
317
- // `BrtBorder` ([MS-XLSB] 2.4.314): the two diagonal-direction bits, then five `Blxf` edges in the
318
- // order top, bottom, left, right, diagonal — which is *not* the model's or the schema's order, so
319
- // the edges are read positionally and named here.
320
264
  function readBorder(reader) {
321
265
  const flags = reader.u8();
322
266
  const top = readEdge(reader);
@@ -339,21 +283,16 @@ function readBorder(reader) {
339
283
  border.diagonalDown = true;
340
284
  if ((flags & 0b10) !== 0)
341
285
  border.diagonalUp = true;
342
- // An all-default border is the empty one every unbordered cell shares; it carries nothing.
343
286
  return Object.keys(border).length > 0 ? border : undefined;
344
287
  }
345
- // A `Blxf` ([MS-XLSB] 2.5.5): a line style, a reserved byte, and a colour. Style 0 is "no edge",
346
- // which the model spells by omitting the edge rather than by a `none` value.
347
288
  function readEdge(reader) {
348
289
  const style = BORDER_STYLES[reader.u8()];
349
- reader.skip(1); // reserved
290
+ reader.skip(1);
350
291
  const color = reader.color();
351
292
  if (style === undefined)
352
293
  return undefined;
353
294
  return color === undefined ? { style } : { style, color };
354
295
  }
355
- // Indexed by the stored `dg` value; index 0 (`none`) is absent, as above. The order is the binary
356
- // format's own and differs from `ST_BorderStyle`'s declaration order, so it cannot be shared.
357
296
  const BORDER_STYLES = [
358
297
  undefined,
359
298
  'thin',
@@ -370,13 +309,11 @@ const BORDER_STYLES = [
370
309
  'mediumDashDotDot',
371
310
  'slantDashDot',
372
311
  ];
373
- // `BrtStyle` ([MS-XLSB] 2.4.809): which cellStyleXfs entry this names, and how it is labelled. The
374
- // gallery index is only meaningful for a built-in style, which the flag word declares.
375
312
  function readStyleLabel(reader) {
376
313
  const xfId = reader.u32();
377
314
  const flags = reader.u16();
378
315
  const builtinId = reader.u8();
379
- reader.skip(1); // iLevel: the outline depth of a built-in RowLevel/ColLevel style.
316
+ reader.skip(1);
380
317
  const name = reader.wideString();
381
318
  return {
382
319
  xfId,
@@ -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;