@shbernal/ts-xlsx 3.0.0 → 3.1.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 (213) hide show
  1. package/README.md +4 -1
  2. package/dist/bytes.d.ts +7 -0
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +60 -2
  5. package/dist/core/address.js +40 -24
  6. package/dist/core/autofilter.d.ts +4 -3
  7. package/dist/core/autofilter.js +31 -30
  8. package/dist/core/axis-handle.d.ts +2 -0
  9. package/dist/core/axis-handle.js +5 -2
  10. package/dist/core/cell.d.ts +4 -3
  11. package/dist/core/cell.js +4 -6
  12. package/dist/core/clone.d.ts +29 -0
  13. package/dist/core/clone.js +24 -0
  14. package/dist/core/color-resolution.js +2 -4
  15. package/dist/core/column.d.ts +1 -0
  16. package/dist/core/column.js +3 -0
  17. package/dist/core/comment-thread.js +2 -1
  18. package/dist/core/conditional-formatting-overlay.d.ts +2 -1
  19. package/dist/core/conditional-formatting-overlay.js +2 -2
  20. package/dist/core/conditional-formatting.d.ts +9 -1
  21. package/dist/core/conditional-formatting.js +34 -18
  22. package/dist/core/containers.d.ts +12 -0
  23. package/dist/core/containers.js +5 -0
  24. package/dist/core/data-validation-overlay.d.ts +2 -1
  25. package/dist/core/data-validation-overlay.js +4 -4
  26. package/dist/core/data-validation.d.ts +6 -0
  27. package/dist/core/data-validation.js +15 -4
  28. package/dist/core/date-format.d.ts +20 -0
  29. package/dist/core/date-format.js +160 -0
  30. package/dist/core/date.d.ts +43 -8
  31. package/dist/core/date.js +18 -2
  32. package/dist/core/formula.d.ts +26 -0
  33. package/dist/core/formula.js +20 -4
  34. package/dist/core/grid-edits.d.ts +2 -3
  35. package/dist/core/grid-edits.js +69 -70
  36. package/dist/core/grid-shift.d.ts +58 -9
  37. package/dist/core/grid-shift.js +26 -4
  38. package/dist/core/image.d.ts +13 -9
  39. package/dist/core/image.js +7 -5
  40. package/dist/core/internal.d.ts +8 -2
  41. package/dist/core/limits.d.ts +23 -0
  42. package/dist/core/limits.js +4 -0
  43. package/dist/core/merge.d.ts +5 -4
  44. package/dist/core/merge.js +18 -19
  45. package/dist/core/pivot-table.js +63 -45
  46. package/dist/core/protection.js +2 -11
  47. package/dist/core/range.js +34 -30
  48. package/dist/core/row-input.js +2 -2
  49. package/dist/core/row.d.ts +1 -0
  50. package/dist/core/row.js +3 -0
  51. package/dist/core/style.d.ts +85 -70
  52. package/dist/core/style.js +92 -28
  53. package/dist/core/table-style.d.ts +1 -1
  54. package/dist/core/table-style.js +4 -4
  55. package/dist/core/table.d.ts +27 -9
  56. package/dist/core/table.js +51 -34
  57. package/dist/core/theme.js +6 -5
  58. package/dist/core/value.d.ts +9 -0
  59. package/dist/core/value.js +36 -34
  60. package/dist/core/workbook-media.d.ts +34 -0
  61. package/dist/core/workbook-media.js +44 -0
  62. package/dist/core/workbook-styles.d.ts +67 -1
  63. package/dist/core/workbook-vba.js +3 -2
  64. package/dist/core/workbook.d.ts +23 -2
  65. package/dist/core/workbook.js +24 -40
  66. package/dist/core/worksheet-comments.d.ts +2 -1
  67. package/dist/core/worksheet-comments.js +20 -13
  68. package/dist/core/worksheet-merges.d.ts +44 -0
  69. package/dist/core/worksheet-merges.js +59 -0
  70. package/dist/core/worksheet-model.d.ts +9 -1
  71. package/dist/core/worksheet-model.js +24 -9
  72. package/dist/core/worksheet.d.ts +55 -6
  73. package/dist/core/worksheet.js +36 -44
  74. package/dist/customui/ribbon.js +2 -1
  75. package/dist/entries/core.d.ts +18 -13
  76. package/dist/entries/core.js +12 -6
  77. package/dist/entries/xlsx.d.ts +1 -1
  78. package/dist/errors.d.ts +64 -0
  79. package/dist/errors.js +22 -0
  80. package/dist/hex.d.ts +8 -0
  81. package/dist/hex.js +3 -0
  82. package/dist/io/csv/delimiter.d.ts +9 -0
  83. package/dist/io/csv/delimiter.js +6 -0
  84. package/dist/io/csv/read.js +11 -4
  85. package/dist/io/csv/write.d.ts +9 -2
  86. package/dist/io/csv/write.js +9 -38
  87. package/dist/io/opc/inflate.js +14 -0
  88. package/dist/io/opc/namespaces.d.ts +16 -0
  89. package/dist/io/opc/namespaces.js +4 -0
  90. package/dist/io/opc/part-paths.d.ts +9 -0
  91. package/dist/io/opc/part-paths.js +1 -1
  92. package/dist/io/opc/read-opc.d.ts +54 -14
  93. package/dist/io/opc/read-opc.js +42 -20
  94. package/dist/io/opc/read-options.d.ts +5 -1
  95. package/dist/io/opc/sniff-format.d.ts +3 -3
  96. package/dist/io/opc/sniff-format.js +2 -2
  97. package/dist/io/style/xf-style.d.ts +23 -1
  98. package/dist/io/style/xf-style.js +8 -0
  99. package/dist/io/xlsb/formula.d.ts +2 -2
  100. package/dist/io/xlsb/formula.js +49 -34
  101. package/dist/io/xlsb/primitives.js +8 -14
  102. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  103. package/dist/io/xlsb/ptg-functions.js +385 -596
  104. package/dist/io/xlsb/read-styles.js +45 -81
  105. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  106. package/dist/io/xlsb/read-worksheet.js +73 -89
  107. package/dist/io/xlsb/read.d.ts +6 -3
  108. package/dist/io/xlsb/read.js +44 -23
  109. package/dist/io/xlsb/record-stream.d.ts +30 -0
  110. package/dist/io/xlsb/record-stream.js +20 -0
  111. package/dist/io/xlsb/record-types.d.ts +1 -5
  112. package/dist/io/xlsb/record-types.js +1 -5
  113. package/dist/io/xlsx/cell-accumulator.d.ts +8 -1
  114. package/dist/io/xlsx/cell-accumulator.js +32 -9
  115. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  116. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  117. package/dist/io/xlsx/cell-value.d.ts +5 -3
  118. package/dist/io/xlsx/cell-value.js +13 -15
  119. package/dist/io/xlsx/color-xml.js +3 -2
  120. package/dist/io/xlsx/column-budget.d.ts +26 -0
  121. package/dist/io/xlsx/column-budget.js +22 -0
  122. package/dist/io/xlsx/comments.d.ts +6 -2
  123. package/dist/io/xlsx/comments.js +36 -32
  124. package/dist/io/xlsx/conditional-formatting.d.ts +6 -3
  125. package/dist/io/xlsx/conditional-formatting.js +30 -26
  126. package/dist/io/xlsx/data-validation.js +29 -27
  127. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  128. package/dist/io/xlsx/edit-vba.js +14 -21
  129. package/dist/io/xlsx/font-xml.d.ts +7 -0
  130. package/dist/io/xlsx/font-xml.js +104 -0
  131. package/dist/io/xlsx/hyperlinks.d.ts +37 -5
  132. package/dist/io/xlsx/hyperlinks.js +22 -13
  133. package/dist/io/xlsx/images.js +11 -13
  134. package/dist/io/xlsx/namespaces.d.ts +16 -0
  135. package/dist/io/xlsx/namespaces.js +7 -0
  136. package/dist/io/xlsx/package-plan.d.ts +14 -2
  137. package/dist/io/xlsx/package-plan.js +39 -11
  138. package/dist/io/xlsx/pivot.js +8 -7
  139. package/dist/io/xlsx/read-parts.d.ts +59 -0
  140. package/dist/io/xlsx/read-parts.js +252 -0
  141. package/dist/io/xlsx/read-pivot.js +2 -1
  142. package/dist/io/xlsx/read-repair.d.ts +26 -0
  143. package/dist/io/xlsx/read-repair.js +39 -0
  144. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +6 -3
  145. package/dist/io/xlsx/read-rows.d.ts +18 -5
  146. package/dist/io/xlsx/read-rows.js +45 -31
  147. package/dist/io/xlsx/read-shared-strings.js +3 -2
  148. package/dist/io/xlsx/read-styles.d.ts +1 -6
  149. package/dist/io/xlsx/read-styles.js +6 -71
  150. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  151. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  152. package/dist/io/xlsx/read-worksheet.d.ts +2 -1
  153. package/dist/io/xlsx/read-worksheet.js +35 -164
  154. package/dist/io/xlsx/read.d.ts +5 -12
  155. package/dist/io/xlsx/read.js +73 -406
  156. package/dist/io/xlsx/rich-text.js +1 -1
  157. package/dist/io/xlsx/row-position.d.ts +18 -0
  158. package/dist/io/xlsx/row-position.js +11 -0
  159. package/dist/io/xlsx/row-xml.d.ts +128 -0
  160. package/dist/io/xlsx/row-xml.js +203 -0
  161. package/dist/io/xlsx/shared-formulas.js +2 -2
  162. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  163. package/dist/io/xlsx/sheet-properties.js +145 -9
  164. package/dist/io/xlsx/style-elements.d.ts +43 -0
  165. package/dist/io/xlsx/style-elements.js +156 -0
  166. package/dist/io/xlsx/styles.d.ts +12 -3
  167. package/dist/io/xlsx/styles.js +5 -207
  168. package/dist/io/xlsx/tables.js +10 -14
  169. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  170. package/dist/io/xlsx/theme-xml.js +107 -69
  171. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  172. package/dist/io/xlsx/threaded-comments.js +2 -1
  173. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  174. package/dist/io/xlsx/workbook-xml.js +60 -34
  175. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  176. package/dist/io/xlsx/worksheet-xml.js +48 -229
  177. package/dist/io/xlsx/write-stream.d.ts +65 -16
  178. package/dist/io/xlsx/write-stream.js +50 -20
  179. package/dist/io/xlsx/write.d.ts +1 -2
  180. package/dist/io/xlsx/write.js +114 -74
  181. package/dist/rel-type.d.ts +16 -0
  182. package/dist/rel-type.js +6 -0
  183. package/dist/token-set.d.ts +13 -0
  184. package/dist/token-set.js +4 -1
  185. package/dist/vba/bytes.d.ts +14 -5
  186. package/dist/vba/bytes.js +16 -6
  187. package/dist/vba/cfb-format.d.ts +8 -0
  188. package/dist/vba/cfb-format.js +1 -0
  189. package/dist/vba/cfb-writer.d.ts +9 -0
  190. package/dist/vba/cfb-writer.js +12 -9
  191. package/dist/vba/cfb.d.ts +13 -1
  192. package/dist/vba/cfb.js +73 -51
  193. package/dist/vba/codepage.js +2 -1
  194. package/dist/vba/errors.d.ts +7 -0
  195. package/dist/vba/ms-ovba.js +51 -16
  196. package/dist/vba/project-editor.js +35 -40
  197. package/dist/vba/project.d.ts +11 -1
  198. package/dist/vba/project.js +27 -8
  199. package/dist/vba/vba-encoding.js +4 -5
  200. package/dist/xml/xml-attrs.d.ts +37 -0
  201. package/dist/xml/xml-attrs.js +30 -0
  202. package/dist/xml/xml-chars.d.ts +40 -0
  203. package/dist/xml/xml-chars.js +12 -0
  204. package/dist/xml/xml-namespaces.d.ts +31 -0
  205. package/dist/xml/xml-namespaces.js +50 -0
  206. package/dist/xml/xml-read.d.ts +72 -10
  207. package/dist/xml/xml-read.js +94 -18
  208. package/dist/xml/xml-scan.d.ts +15 -37
  209. package/dist/xml/xml-scan.js +12 -40
  210. package/dist/xml/xml.d.ts +28 -27
  211. package/dist/xml/xml.js +24 -24
  212. package/package.json +12 -7
  213. /package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +0 -0
@@ -1,25 +1,17 @@
1
- import { numFmtCodeFor, NO_PRESERVED_STYLE_TABLES, resolveStyleTable, } from '../style/xf-style.js';
1
+ import { ALIGNMENT_FACETS, FILL_PATTERNS_IN_SCHEMA_ORDER, } from '../../core/style.js';
2
+ import { numFmtCodeFor, NO_PRESERVED_STYLE_TABLES, protectionFrom, resolveStyleTable, } from '../style/xf-style.js';
2
3
  import { RecordReader } from './primitives.js';
3
- import { readRecords } from './record-stream.js';
4
+ import { blockTracker, readRecords } from './record-stream.js';
4
5
  import { BRT } from './record-types.js';
5
- const COLLECTION_STARTS = new Map([
6
- [BRT.BeginFmts, 'fmts'],
7
- [BRT.BeginFonts, 'fonts'],
8
- [BRT.BeginFills, 'fills'],
9
- [BRT.BeginBorders, 'borders'],
10
- [BRT.BeginCellStyleXFs, 'cellStyleXfs'],
11
- [BRT.BeginCellXFs, 'cellXfs'],
12
- [BRT.BeginStyles, 'styles'],
13
- ]);
14
- const COLLECTION_ENDS = new Set([
15
- BRT.EndFmts,
16
- BRT.EndFonts,
17
- BRT.EndFills,
18
- BRT.EndBorders,
19
- BRT.EndCellStyleXFs,
20
- BRT.EndCellXFs,
21
- BRT.EndStyles,
22
- ]);
6
+ const COLLECTIONS = [
7
+ [BRT.BeginFmts, BRT.EndFmts, 'fmts'],
8
+ [BRT.BeginFonts, BRT.EndFonts, 'fonts'],
9
+ [BRT.BeginFills, BRT.EndFills, 'fills'],
10
+ [BRT.BeginBorders, BRT.EndBorders, 'borders'],
11
+ [BRT.BeginCellStyleXFs, BRT.EndCellStyleXFs, 'cellStyleXfs'],
12
+ [BRT.BeginCellXFs, BRT.EndCellXFs, 'cellXfs'],
13
+ [BRT.BeginStyles, BRT.EndStyles, 'styles'],
14
+ ];
23
15
  export function parseStyleTable(part) {
24
16
  if (part === undefined)
25
17
  return { cellXfs: [], namedStyles: [], preserved: NO_PRESERVED_STYLE_TABLES };
@@ -30,43 +22,36 @@ export function parseStyleTable(part) {
30
22
  const namedXfs = [];
31
23
  const directXfs = [];
32
24
  const labels = [];
33
- let collection;
25
+ const blocks = blockTracker(COLLECTIONS);
34
26
  for (const record of readRecords(part)) {
35
- if (COLLECTION_ENDS.has(record.type)) {
36
- collection = undefined;
37
- continue;
38
- }
39
- const started = COLLECTION_STARTS.get(record.type);
40
- if (started !== undefined) {
41
- collection = started;
27
+ if (blocks.boundary(record.type))
42
28
  continue;
43
- }
44
29
  const reader = new RecordReader(record.data);
45
30
  switch (record.type) {
46
31
  case BRT.Fmt:
47
- if (collection === 'fmts')
32
+ if (blocks.isOpen('fmts'))
48
33
  numFmtCodes.set(reader.u16(), reader.wideString());
49
34
  break;
50
35
  case BRT.Font:
51
- if (collection === 'fonts')
36
+ if (blocks.isOpen('fonts'))
52
37
  fonts.push(readFont(reader));
53
38
  break;
54
39
  case BRT.Fill:
55
- if (collection === 'fills')
40
+ if (blocks.isOpen('fills'))
56
41
  fills.push(readFill(reader));
57
42
  break;
58
43
  case BRT.Border:
59
- if (collection === 'borders')
44
+ if (blocks.isOpen('borders'))
60
45
  borders.push(readBorder(reader));
61
46
  break;
62
47
  case BRT.XF:
63
- if (collection === 'cellXfs' || collection === 'cellStyleXfs') {
48
+ if (blocks.isOpen('cellXfs') || blocks.isOpen('cellStyleXfs')) {
64
49
  const deps = { fonts, fills, borders, numFmtCodes };
65
- (collection === 'cellXfs' ? directXfs : namedXfs).push(readXf(reader, deps, collection === 'cellXfs'));
50
+ (blocks.isOpen('cellXfs') ? directXfs : namedXfs).push(readXf(reader, deps, blocks.isOpen('cellXfs')));
66
51
  }
67
52
  break;
68
53
  case BRT.Style:
69
- if (collection === 'styles')
54
+ if (blocks.isOpen('styles'))
70
55
  labels.push(readStyleLabel(reader));
71
56
  break;
72
57
  default:
@@ -110,34 +95,33 @@ function readXf(reader, deps, isDirect) {
110
95
  return draft;
111
96
  }
112
97
  const NOT_A_CELL_XF = 0xffff;
98
+ const BIFF12_ALIGNMENT = {
99
+ horizontal: ({ flags }) => {
100
+ const value = HORIZONTAL_ALIGNMENTS[flags & 0b111];
101
+ return value === undefined ? {} : { horizontal: value };
102
+ },
103
+ vertical: ({ flags }) => {
104
+ const value = VERTICAL_ALIGNMENTS[(flags >> 3) & 0b111];
105
+ return value === undefined ? {} : { vertical: value };
106
+ },
107
+ textRotation: ({ rotation }) => (rotation === 0 ? {} : { textRotation: rotation }),
108
+ wrapText: ({ flags }) => ((flags & 0x0040) === 0 ? {} : { wrapText: true }),
109
+ indent: ({ indent }) => (indent === 0 ? {} : { indent }),
110
+ shrinkToFit: ({ flags }) => ((flags & 0x0100) === 0 ? {} : { shrinkToFit: true }),
111
+ readingOrder: ({ flags }) => {
112
+ const value = (flags >> 10) & 0b11;
113
+ return value === 0 ? {} : { readingOrder: value };
114
+ },
115
+ };
113
116
  function readAlignment(flags, rotation, indent) {
117
+ const bits = { flags, rotation, indent };
114
118
  const out = {};
115
- const horizontal = HORIZONTAL_ALIGNMENTS[flags & 0b111];
116
- if (horizontal !== undefined)
117
- out.horizontal = horizontal;
118
- const vertical = VERTICAL_ALIGNMENTS[(flags >> 3) & 0b111];
119
- if (vertical !== undefined)
120
- out.vertical = vertical;
121
- if (rotation !== 0)
122
- out.textRotation = rotation;
123
- if ((flags & 0x0040) !== 0)
124
- out.wrapText = true;
125
- if (indent !== 0)
126
- out.indent = indent;
127
- if ((flags & 0x0100) !== 0)
128
- out.shrinkToFit = true;
129
- const readingOrder = (flags >> 10) & 0b11;
130
- if (readingOrder !== 0)
131
- out.readingOrder = readingOrder;
119
+ for (const facet of ALIGNMENT_FACETS)
120
+ Object.assign(out, BIFF12_ALIGNMENT[facet.key](bits));
132
121
  return Object.keys(out).length > 0 ? out : undefined;
133
122
  }
134
123
  function readProtection(flags) {
135
- const out = {};
136
- if ((flags & 0x1000) === 0)
137
- out.locked = false;
138
- if ((flags & 0x2000) !== 0)
139
- out.hidden = true;
140
- return Object.keys(out).length > 0 ? out : undefined;
124
+ return protectionFrom({ locked: (flags & 0x1000) !== 0, hidden: (flags & 0x2000) !== 0 });
141
125
  }
142
126
  const HORIZONTAL_ALIGNMENTS = [
143
127
  undefined,
@@ -208,7 +192,8 @@ const UNDERLINE_STYLES = new Map([
208
192
  [0x22, 'doubleAccounting'],
209
193
  ]);
210
194
  function readFill(reader) {
211
- const pattern = FILL_PATTERNS[reader.u32()];
195
+ const raw = FILL_PATTERNS_IN_SCHEMA_ORDER[reader.u32()];
196
+ const pattern = raw === 'none' ? undefined : raw;
212
197
  if (pattern === undefined)
213
198
  return undefined;
214
199
  const fgColor = notSentinel(reader.color(), AUTOMATIC_FOREGROUND);
@@ -225,27 +210,6 @@ const AUTOMATIC_BACKGROUND = 65;
225
210
  function notSentinel(color, sentinel) {
226
211
  return color?.indexed === sentinel ? undefined : color;
227
212
  }
228
- const FILL_PATTERNS = [
229
- undefined,
230
- 'solid',
231
- 'mediumGray',
232
- 'darkGray',
233
- 'lightGray',
234
- 'darkHorizontal',
235
- 'darkVertical',
236
- 'darkDown',
237
- 'darkUp',
238
- 'darkGrid',
239
- 'darkTrellis',
240
- 'lightHorizontal',
241
- 'lightVertical',
242
- 'lightDown',
243
- 'lightUp',
244
- 'lightGrid',
245
- 'lightTrellis',
246
- 'gray125',
247
- 'gray0625',
248
- ];
249
213
  function readBorder(reader) {
250
214
  const flags = reader.u8();
251
215
  const top = readEdge(reader);
@@ -1,3 +1,4 @@
1
+ import { type DateEpoch } from '../../core/date.ts';
1
2
  import type { Worksheet } from '../../core/worksheet.ts';
2
3
  import { type XfStyle } from '../style/xf-style.ts';
3
4
  import { type FormulaScope } from './formula.ts';
@@ -6,4 +7,22 @@ import { type FormulaScope } from './formula.ts';
6
7
  * non-empty cell with the style its index resolves to in `xfStyles` and, for a formula cell, the text
7
8
  * its token stream decodes to through `scope`.
8
9
  */
9
- export declare function parseWorksheet(part: Uint8Array, sheet: Worksheet, sharedStrings: readonly string[], xfStyles: ReadonlyArray<XfStyle>, scope: FormulaScope): void;
10
+ export declare function parseWorksheet(part: Uint8Array, context: WorksheetReadContext): void;
11
+ /**
12
+ * What reading a worksheet part needs around it: the sheet being filled and the three tables its
13
+ * records resolve through, plus the date system a serial under a date format counts from.
14
+ *
15
+ * One value rather than five positional arguments, two of them arrays and two of those `readonly
16
+ * string[]`/`ReadonlyArray<XfStyle>` -- a run whose order the call site cannot be read against, on a
17
+ * reader that decides what every cell in the sheet says. It is also exactly the half of
18
+ * {@link CellRecordContext} that does not change between records, which is why that one extends it
19
+ * and the record loop spreads this one into it.
20
+ */
21
+ export interface WorksheetReadContext {
22
+ readonly sheet: Worksheet;
23
+ readonly sharedStrings: readonly string[];
24
+ readonly xfStyles: ReadonlyArray<XfStyle>;
25
+ readonly scope: FormulaScope;
26
+ /** The workbook's date system, from `BrtWbProp`: what a serial under a date format counts from. */
27
+ readonly dateEpoch: DateEpoch;
28
+ }
@@ -1,5 +1,5 @@
1
- import { encodeAddress, MAX_COLUMN } from '../../core/address.js';
2
- import { isDateFormat, serialToDate } from '../../core/date.js';
1
+ import { encodeAddress, encodeRect, MAX_COLUMN_INDEX, MAX_ROW_INDEX } from '../../core/address.js';
2
+ import { coerceDateSerial } from '../../core/date.js';
3
3
  import { unmangleFunctions } from '../../core/formula.js';
4
4
  import { assignStyleFacets } from '../../core/style.js';
5
5
  import { applyXfToCell } from '../style/xf-style.js';
@@ -7,23 +7,31 @@ import { decodeFormula, formulaAnchor } from './formula.js';
7
7
  import { errorCodeFor, RecordReader } from './primitives.js';
8
8
  import { readRecords } from './record-stream.js';
9
9
  import { BRT } from './record-types.js';
10
- const CELL_RECORDS = new Set([
11
- BRT.CellBlank,
12
- BRT.CellRk,
13
- BRT.CellError,
14
- BRT.CellBool,
15
- BRT.CellReal,
16
- BRT.CellSt,
17
- BRT.CellIsst,
18
- BRT.CellRString,
10
+ const value = (read) => ({
11
+ kind: 'value',
12
+ read,
13
+ });
14
+ const formula = (read) => ({ kind: 'formula', read });
15
+ const CELL_RECORDS = new Map([
16
+ [BRT.CellBlank, value(() => null)],
17
+ [BRT.CellRk, value((r, c) => asNumberOrDate(r.rk(), c.numFmt, c.epoch))],
18
+ [BRT.CellReal, value((r, c) => asNumberOrDate(r.f64(), c.numFmt, c.epoch))],
19
+ [BRT.CellBool, value((r) => r.u8() !== 0)],
20
+ [BRT.CellError, value((r) => errorValueOrNull(r.u8()))],
21
+ [BRT.CellSt, value((r) => r.wideString())],
22
+ [BRT.CellRString, value((r) => r.richString())],
23
+ [BRT.CellIsst, value((r, c) => c.sharedStrings[r.u32()] ?? '')],
24
+ [BRT.FmlaNum, formula((r, c) => asNumberOrDate(r.f64(), c.numFmt, c.epoch))],
25
+ [BRT.FmlaBool, formula((r) => r.u8() !== 0)],
26
+ [BRT.FmlaError, formula((r) => errorValueOrNull(r.u8()) ?? undefined)],
27
+ [BRT.FmlaString, formula((r) => r.wideString())],
19
28
  ]);
20
- const FORMULA_RECORDS = new Set([
21
- BRT.FmlaString,
22
- BRT.FmlaNum,
23
- BRT.FmlaBool,
24
- BRT.FmlaError,
25
- ]);
26
- export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
29
+ function errorValueOrNull(code) {
30
+ const error = errorCodeFor(code);
31
+ return error === undefined ? null : { error };
32
+ }
33
+ export function parseWorksheet(part, context) {
34
+ const { sheet, xfStyles, scope } = context;
27
35
  let row = -1;
28
36
  let rowStyle = -1;
29
37
  const columnStyle = new Map();
@@ -31,6 +39,7 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
31
39
  const groups = new Map();
32
40
  const deferred = [];
33
41
  for (const record of readRecords(part)) {
42
+ const cellRecord = CELL_RECORDS.get(record.type);
34
43
  const reader = new RecordReader(record.data);
35
44
  if (record.type === BRT.WsFmtInfo) {
36
45
  reader.skip(6);
@@ -47,7 +56,12 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
47
56
  else if (record.type === BRT.MergeCell) {
48
57
  const { rowFirst, rowLast, colFirst, colLast } = reader.range();
49
58
  if (inGrid(colFirst, rowFirst) && inGrid(colLast, rowLast)) {
50
- sheet.mergeCells(`${encodeAddress(colFirst + 1, rowFirst + 1)}:${encodeAddress(colLast + 1, rowLast + 1)}`);
59
+ sheet.mergeCells(encodeRect({
60
+ top: rowFirst + 1,
61
+ left: colFirst + 1,
62
+ bottom: rowLast + 1,
63
+ right: colLast + 1,
64
+ }));
51
65
  }
52
66
  }
53
67
  else if (record.type === BRT.ArrFmla) {
@@ -58,36 +72,10 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
58
72
  rgcb: reader.bytes(reader.u32()),
59
73
  });
60
74
  }
61
- else if ((CELL_RECORDS.has(record.type) || FORMULA_RECORDS.has(record.type)) && row > 0) {
62
- const { column, styleIndex } = reader.cell();
63
- if (!inGrid(column, row - 1))
64
- continue;
65
- const resolved = styleIndex > 0 ? styleIndex : rowStyle >= 0 ? rowStyle : (columnStyle.get(column) ?? -1);
66
- const style = resolved >= 0 ? xfStyles[resolved] : xfStyles[0];
67
- const cell = sheet.getCell(encodeAddress(column + 1, row));
68
- applyXfToCell(cell, style);
69
- if (!FORMULA_RECORDS.has(record.type)) {
70
- cell.value = decodeCell(record.type, reader, sharedStrings, style?.numFmt);
71
- continue;
72
- }
73
- const result = cachedResult(record.type, reader, style?.numFmt);
74
- reader.skip(2);
75
- const rgce = reader.bytes(reader.u32());
76
- const rgcb = reader.bytes(reader.u32());
77
- const anchor = formulaAnchor(rgce, rgcb);
78
- if (anchor === undefined) {
79
- cell.value = formulaValue(decodeFormula(rgce, rgcb, scope), result);
80
- }
81
- else {
82
- deferred.push({
83
- cell,
84
- row: row - 1,
85
- column,
86
- anchorRow: anchor.row,
87
- anchorColumn: anchor.column,
88
- result,
89
- });
90
- }
75
+ else if (cellRecord !== undefined) {
76
+ const member = readCellRecord(cellRecord, reader, { ...context, row, rowStyle, columnStyle });
77
+ if (member !== undefined)
78
+ deferred.push(member);
91
79
  }
92
80
  }
93
81
  for (const member of deferred) {
@@ -99,6 +87,40 @@ export function parseWorksheet(part, sheet, sharedStrings, xfStyles, scope) {
99
87
  : formulaValue(decodeFormula(group.rgce, group.rgcb, scope), member.result);
100
88
  }
101
89
  }
90
+ function readCellRecord(record, reader, context) {
91
+ const { sheet, sharedStrings, xfStyles, scope, dateEpoch, row, rowStyle, columnStyle } = context;
92
+ if (row <= 0)
93
+ return undefined;
94
+ const { column, styleIndex } = reader.cell();
95
+ if (!inGrid(column, row - 1))
96
+ return undefined;
97
+ const resolved = styleIndex > 0 ? styleIndex : rowStyle >= 0 ? rowStyle : (columnStyle.get(column) ?? -1);
98
+ const style = resolved >= 0 ? xfStyles[resolved] : xfStyles[0];
99
+ const cell = sheet.getCell(encodeAddress(column + 1, row));
100
+ applyXfToCell(cell, style);
101
+ const ctx = { sharedStrings, numFmt: style?.numFmt, epoch: dateEpoch };
102
+ if (record.kind === 'value') {
103
+ cell.value = record.read(reader, ctx);
104
+ return undefined;
105
+ }
106
+ const result = record.read(reader, ctx);
107
+ reader.skip(2);
108
+ const rgce = reader.bytes(reader.u32());
109
+ const rgcb = reader.bytes(reader.u32());
110
+ const anchor = formulaAnchor(rgce, rgcb);
111
+ if (anchor === undefined) {
112
+ cell.value = formulaValue(decodeFormula(rgce, rgcb, scope), result);
113
+ return undefined;
114
+ }
115
+ return {
116
+ cell,
117
+ row: row - 1,
118
+ column,
119
+ anchorRow: anchor.row,
120
+ anchorColumn: anchor.column,
121
+ result,
122
+ };
123
+ }
102
124
  function groupKey(row, column) {
103
125
  return `${row}:${column}`;
104
126
  }
@@ -108,49 +130,11 @@ function formulaValue(formula, result) {
108
130
  const stored = unmangleFunctions(formula);
109
131
  return result === undefined ? { formula: stored } : { formula: stored, result };
110
132
  }
111
- function cachedResult(type, reader, numFmt) {
112
- switch (type) {
113
- case BRT.FmlaNum:
114
- return asNumberOrDate(reader.f64(), numFmt);
115
- case BRT.FmlaBool:
116
- return reader.u8() !== 0;
117
- case BRT.FmlaError: {
118
- const error = errorCodeFor(reader.u8());
119
- return error === undefined ? undefined : { error };
120
- }
121
- default:
122
- return reader.wideString();
123
- }
124
- }
125
- const MAX_ROW_INDEX = 1048575;
126
- const MAX_COLUMN_INDEX = MAX_COLUMN - 1;
127
133
  function inGrid(column, row) {
128
134
  return column >= 0 && column <= MAX_COLUMN_INDEX && row >= 0 && row <= MAX_ROW_INDEX;
129
135
  }
130
- function decodeCell(type, reader, sharedStrings, numFmt) {
131
- switch (type) {
132
- case BRT.CellRk:
133
- return asNumberOrDate(reader.rk(), numFmt);
134
- case BRT.CellReal:
135
- return asNumberOrDate(reader.f64(), numFmt);
136
- case BRT.CellBool:
137
- return reader.u8() !== 0;
138
- case BRT.CellError: {
139
- const error = errorCodeFor(reader.u8());
140
- return error === undefined ? null : { error };
141
- }
142
- case BRT.CellSt:
143
- return reader.wideString();
144
- case BRT.CellRString:
145
- return reader.richString();
146
- case BRT.CellIsst:
147
- return sharedStrings[reader.u32()] ?? '';
148
- default:
149
- return null;
150
- }
151
- }
152
- function asNumberOrDate(value, numFmt) {
153
- return numFmt !== undefined && isDateFormat(numFmt) ? serialToDate(value) : value;
136
+ function asNumberOrDate(value, numFmt, epoch) {
137
+ return coerceDateSerial(value, numFmt, epoch);
154
138
  }
155
139
  function applyRow(reader, sheet, defaultRowHeight) {
156
140
  const index = reader.u32();
@@ -1,5 +1,5 @@
1
1
  import { Workbook } from '../../core/workbook.ts';
2
- import type { ReadXlsxOptions } from '../opc/read-options.ts';
2
+ import type { ReadPackageOptions } from '../opc/read-options.ts';
3
3
  /** The office-document part every `.xlsb` package is entered through. */
4
4
  export declare const XLSB_WORKBOOK_PART = "xl/workbook.bin";
5
5
  /**
@@ -11,10 +11,13 @@ export declare const XLSB_WORKBOOK_PART = "xl/workbook.bin";
11
11
  * @throws {PackageReadError} if the input is a ZIP that cannot be unpacked: a corrupt or
12
12
  * truncated archive, or one exceeding the inflate bound (a probable zip bomb).
13
13
  */
14
- export declare function readXlsb(data: Uint8Array, options?: ReadXlsxOptions): Workbook;
14
+ export declare function readXlsb(data: Uint8Array, options?: ReadPackageOptions): Workbook;
15
15
  /**
16
16
  * Build the model from an already-inflated `.xlsb` package. Separate from {@link readXlsb} so the
17
17
  * `.xlsx` reader can hand over a package it has already inflated and classified, rather than
18
18
  * inflating the same bytes twice.
19
+ *
20
+ * `documentPath` is where the package's own `_rels/.rels` says its workbook lives; it defaults to the
21
+ * conventional path for a caller holding nothing but the parts.
19
22
  */
20
- export declare function readXlsbPackage(files: Record<string, Uint8Array>): Workbook;
23
+ export declare function readXlsbPackage(files: Record<string, Uint8Array>, documentPath?: string): Workbook;
@@ -1,33 +1,40 @@
1
1
  import { unmangleFunctions } from '../../core/formula.js';
2
2
  import { INTERNAL } from '../../core/internal.js';
3
3
  import { Workbook } from '../../core/workbook.js';
4
+ import { quoted } from '../../errors.js';
4
5
  import { UnsupportedFormatError } from '../opc/errors.js';
5
6
  import { openSpreadsheetPackage, packageAccessors, readPartRelationships } from '../opc/read-opc.js';
7
+ import { XlsbParseError } from './errors.js';
6
8
  import { decodeFormula } from './formula.js';
7
9
  import { RecordReader } from './primitives.js';
8
10
  import { parseSharedStrings } from './read-shared-strings.js';
9
11
  import { parseStyleTable } from './read-styles.js';
10
12
  import { parseWorksheet } from './read-worksheet.js';
11
- import { readRecords } from './record-stream.js';
13
+ import { blockTracker, readRecords } from './record-stream.js';
12
14
  import { BRT } from './record-types.js';
13
15
  export const XLSB_WORKBOOK_PART = 'xl/workbook.bin';
14
16
  export function readXlsb(data, options = {}) {
15
- return readXlsbPackage(openSpreadsheetPackage(data, options.maxUncompressedBytes).files);
17
+ const { files, documentPath, workbookXml } = openSpreadsheetPackage(data, options.maxUncompressedBytes);
18
+ if (workbookXml !== undefined) {
19
+ throw new UnsupportedFormatError('unknown', `not a valid .xlsb package: its office document ${quoted(documentPath)} is XML, not the binary ${XLSB_WORKBOOK_PART} a .xlsb carries; read it with readXlsx`);
20
+ }
21
+ return readXlsbPackage(files, documentPath);
16
22
  }
17
- export function readXlsbPackage(files) {
23
+ export function readXlsbPackage(files, documentPath = XLSB_WORKBOOK_PART) {
18
24
  const { partText, partBytes } = packageAccessors(files);
19
- const workbookPart = partBytes(XLSB_WORKBOOK_PART);
25
+ const workbookPart = partBytes(documentPath);
20
26
  if (workbookPart === undefined) {
21
- throw new UnsupportedFormatError('unknown', `not a valid .xlsb package: ${XLSB_WORKBOOK_PART} is missing`);
27
+ throw new UnsupportedFormatError('unknown', `not a valid .xlsb package: ${quoted(documentPath)} is missing`);
22
28
  }
23
- const rels = readPartRelationships(XLSB_WORKBOOK_PART, partText);
24
- const sharedStrings = parseSharedStrings(partBytes('xl/sharedStrings.bin'));
25
- const { cellXfs, namedStyles, defaultFont } = parseStyleTable(partBytes('xl/styles.bin'));
29
+ const rels = readPartRelationships(documentPath, partText, partBytes);
30
+ const sharedStrings = parseSharedStrings(rels.relatedBytes('sharedStrings') ?? partBytes('xl/sharedStrings.bin'));
31
+ const { cellXfs, namedStyles, defaultFont } = parseStyleTable(rels.relatedBytes('styles') ?? partBytes('xl/styles.bin'));
26
32
  const workbook = new Workbook();
27
33
  if (namedStyles.length > 1)
28
34
  workbook[INTERNAL].restoreNamedStyles(namedStyles);
29
35
  workbook[INTERNAL].restoreDefaultFont(defaultFont);
30
36
  const declaration = readWorkbookPart(workbookPart);
37
+ workbook.dateEpoch = declaration.dateEpoch;
31
38
  const scope = {
32
39
  sheetNames: declaration.sheets.map((sheet) => sheet.name),
33
40
  externSheets: declaration.externSheets,
@@ -39,36 +46,42 @@ export function readXlsbPackage(files) {
39
46
  const target = declared.relId === undefined ? undefined : rels.byId(declared.relId)?.target;
40
47
  const part = target === undefined ? undefined : partBytes(rels.pathOf(target));
41
48
  if (part !== undefined)
42
- parseWorksheet(part, sheet, sharedStrings, cellXfs, scope);
49
+ parseWorksheet(part, {
50
+ sheet,
51
+ sharedStrings,
52
+ xfStyles: cellXfs,
53
+ scope,
54
+ dateEpoch: declaration.dateEpoch,
55
+ });
43
56
  }
44
57
  for (const defined of definedNames(declaration, scope))
45
58
  workbook.defineName(defined);
46
59
  return workbook;
47
60
  }
61
+ const WORKBOOK_BLOCKS = [
62
+ [BRT.BeginBundleShs, BRT.EndBundleShs, 'bundle'],
63
+ [BRT.BeginExternals, BRT.EndExternals, 'externals'],
64
+ ];
48
65
  function readWorkbookPart(part) {
49
66
  const sheets = [];
50
67
  const names = [];
51
68
  let externSheets = [];
52
- let inBundle = false;
53
- let inExternals = false;
69
+ const blocks = blockTracker(WORKBOOK_BLOCKS);
54
70
  let supportingBooks = 0;
55
71
  let selfSupBook;
72
+ let dateEpoch = 1900;
56
73
  for (const record of readRecords(part)) {
57
- if (record.type === BRT.BeginBundleShs)
58
- inBundle = true;
59
- else if (record.type === BRT.EndBundleShs)
60
- inBundle = false;
61
- else if (record.type === BRT.BundleSh && inBundle)
74
+ if (blocks.boundary(record.type))
75
+ continue;
76
+ if (record.type === BRT.WbProp)
77
+ dateEpoch = readDateEpoch(record.data);
78
+ else if (record.type === BRT.BundleSh && blocks.isOpen('bundle'))
62
79
  sheets.push(readSheet(record.data));
63
- else if (record.type === BRT.BeginExternals)
64
- inExternals = true;
65
- else if (record.type === BRT.EndExternals)
66
- inExternals = false;
67
80
  else if (record.type === BRT.ExternSheet)
68
81
  externSheets = readExternSheets(record.data);
69
82
  else if (record.type === BRT.SupSelf)
70
83
  selfSupBook = supportingBooks++;
71
- else if (inExternals)
84
+ else if (blocks.isOpen('externals'))
72
85
  supportingBooks++;
73
86
  else if (record.type === BRT.Name)
74
87
  names.push(readName(record.data));
@@ -78,8 +91,14 @@ function readWorkbookPart(part) {
78
91
  names,
79
92
  externSheets,
80
93
  selfSupBook: supportingBooks === 1 ? selfSupBook : undefined,
94
+ dateEpoch,
81
95
  };
82
96
  }
97
+ function readDateEpoch(data) {
98
+ if (data.length < 4)
99
+ return 1900;
100
+ return (new RecordReader(data).u32() & 1) === 0 ? 1900 : 1904;
101
+ }
83
102
  function readSheet(data) {
84
103
  const reader = new RecordReader(data);
85
104
  const state = SHEET_STATES[reader.u32()] ?? 'visible';
@@ -90,9 +109,11 @@ function readSheet(data) {
90
109
  function readExternSheets(data) {
91
110
  const reader = new RecordReader(data);
92
111
  const count = reader.u32();
112
+ if (count * XTI_BYTES > reader.remaining) {
113
+ throw new XlsbParseError(`BIFF12 externSheet table declares ${count} entries needing ${count * XTI_BYTES} bytes but ` +
114
+ `only ${reader.remaining} remain in the record`);
115
+ }
93
116
  const entries = [];
94
- if (count * XTI_BYTES > reader.remaining)
95
- return entries;
96
117
  for (let index = 0; index < count; index++) {
97
118
  entries.push({ supBook: reader.u32(), firstSheet: reader.i32(), lastSheet: reader.i32() });
98
119
  }
@@ -12,3 +12,33 @@ export interface BiffRecord {
12
12
  * end of the part.
13
13
  */
14
14
  export declare function readRecords(part: Uint8Array): Generator<BiffRecord>;
15
+ /**
16
+ * Track which `Begin…`/`End…` blocks a BIFF12 record stream is currently inside.
17
+ *
18
+ * BIFF12 is flat: a collection is a `BeginFonts` record, its members, then `EndFonts`, and a reader
19
+ * has to carry that state itself. Two readers here did, and only one did it legibly. The style reader
20
+ * kept an explicit collection with start and end tables; the workbook reader re-solved it with loose
21
+ * booleans in an if-else chain whose *ordering was load-bearing and unstated*: move the `EndExternals`
22
+ * arm below the catch-all that counts records inside the externals block and the count silently goes
23
+ * wrong. Answering "is this record a block boundary" separately from "what does this record mean"
24
+ * removes that constraint rather than documenting it.
25
+ *
26
+ * Each end names the block it closes, so an `EndFonts` cannot close `<fills>` the way a bare set of
27
+ * end markers allowed. Blocks are tracked independently, so two that a damaged file interleaves stay
28
+ * separate rather than one clearing the other.
29
+ *
30
+ * A block is declared as one `[start, end, block]` triple rather than as an entry in a start table
31
+ * and a matching entry in an end table. The two-table form let a caller pair `BeginFills` with
32
+ * `EndFonts` in a way that compiles and reads plausibly, which is the very mistake the paragraph
33
+ * above says this exists to prevent.
34
+ */
35
+ export declare function blockTracker<T>(blocks: readonly (readonly [number, number, T])[]): BlockTracker<T>;
36
+ export interface BlockTracker<T> {
37
+ /** Whether the stream is currently inside `block`. */
38
+ isOpen(block: T): boolean;
39
+ /**
40
+ * Take a record's type as a possible block boundary, returning whether it was one. A boundary
41
+ * carries no content of its own, so a caller that gets `true` skips the record entirely.
42
+ */
43
+ boundary(type: number): boolean;
44
+ }
@@ -26,3 +26,23 @@ function byteAt(part, index) {
26
26
  throw new XlsbParseError('truncated BIFF12 record header');
27
27
  return value;
28
28
  }
29
+ export function blockTracker(blocks) {
30
+ const starts = new Map(blocks.map(([start, , block]) => [start, block]));
31
+ const ends = new Map(blocks.map(([, end, block]) => [end, block]));
32
+ const open = new Set();
33
+ return {
34
+ isOpen: (block) => open.has(block),
35
+ boundary(type) {
36
+ const ended = ends.get(type);
37
+ if (ended !== undefined) {
38
+ open.delete(ended);
39
+ return true;
40
+ }
41
+ const started = starts.get(type);
42
+ if (started === undefined)
43
+ return false;
44
+ open.add(started);
45
+ return true;
46
+ },
47
+ };
48
+ }
@@ -16,7 +16,6 @@ export declare const BRT: {
16
16
  readonly ColInfo: 60;
17
17
  readonly MergeCell: 176;
18
18
  readonly SSTItem: 19;
19
- readonly BeginSst: 159;
20
19
  readonly EndSst: 160;
21
20
  readonly Fmt: 44;
22
21
  readonly Font: 43;
@@ -38,6 +37,7 @@ export declare const BRT: {
38
37
  readonly EndCellXFs: 618;
39
38
  readonly BeginStyles: 619;
40
39
  readonly EndStyles: 620;
40
+ readonly WbProp: 153;
41
41
  readonly BundleSh: 156;
42
42
  readonly BeginBundleShs: 143;
43
43
  readonly EndBundleShs: 144;
@@ -46,10 +46,6 @@ export declare const BRT: {
46
46
  readonly EndExternals: 354;
47
47
  readonly SupSelf: 357;
48
48
  readonly ExternSheet: 362;
49
- readonly WsProp: 147;
50
- readonly WsDim: 148;
51
49
  readonly WsFmtInfo: 485;
52
- readonly BeginSheetData: 145;
53
- readonly EndSheetData: 146;
54
50
  readonly ArrFmla: 426;
55
51
  };