@shbernal/ts-xlsx 2.1.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 (225) 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 +5 -4
  7. package/dist/core/autofilter.js +34 -35
  8. package/dist/core/axis-handle.d.ts +10 -0
  9. package/dist/core/axis-handle.js +17 -0
  10. package/dist/core/cell.d.ts +25 -0
  11. package/dist/core/cell.js +9 -3
  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 +5 -1
  16. package/dist/core/column.js +34 -35
  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 +14 -6
  21. package/dist/core/conditional-formatting.js +45 -43
  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 +9 -3
  27. package/dist/core/data-validation.js +21 -18
  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 +14 -10
  39. package/dist/core/image.js +9 -9
  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-index.d.ts +31 -0
  44. package/dist/core/merge-index.js +60 -0
  45. package/dist/core/merge.d.ts +5 -14
  46. package/dist/core/merge.js +18 -27
  47. package/dist/core/page-setup.d.ts +72 -2
  48. package/dist/core/page-setup.js +34 -8
  49. package/dist/core/pivot-table.d.ts +1 -1
  50. package/dist/core/pivot-table.js +67 -53
  51. package/dist/core/protection.js +2 -11
  52. package/dist/core/range.js +34 -30
  53. package/dist/core/row-input.js +2 -2
  54. package/dist/core/row.d.ts +5 -1
  55. package/dist/core/row.js +22 -23
  56. package/dist/core/style.d.ts +134 -75
  57. package/dist/core/style.js +112 -53
  58. package/dist/core/table-style.d.ts +1 -1
  59. package/dist/core/table-style.js +4 -4
  60. package/dist/core/table.d.ts +28 -10
  61. package/dist/core/table.js +54 -39
  62. package/dist/core/theme.js +6 -5
  63. package/dist/core/used-extent.d.ts +44 -0
  64. package/dist/core/used-extent.js +102 -0
  65. package/dist/core/value.d.ts +9 -0
  66. package/dist/core/value.js +36 -34
  67. package/dist/core/workbook-media.d.ts +34 -0
  68. package/dist/core/workbook-media.js +44 -0
  69. package/dist/core/workbook-styles.d.ts +67 -1
  70. package/dist/core/workbook-vba.js +3 -2
  71. package/dist/core/workbook.d.ts +23 -2
  72. package/dist/core/workbook.js +24 -40
  73. package/dist/core/worksheet-comments.d.ts +2 -1
  74. package/dist/core/worksheet-comments.js +20 -13
  75. package/dist/core/worksheet-merges.d.ts +44 -0
  76. package/dist/core/worksheet-merges.js +59 -0
  77. package/dist/core/worksheet-model.d.ts +9 -1
  78. package/dist/core/worksheet-model.js +24 -9
  79. package/dist/core/worksheet.d.ts +72 -12
  80. package/dist/core/worksheet.js +68 -70
  81. package/dist/customui/ribbon.js +6 -7
  82. package/dist/entries/core.d.ts +18 -13
  83. package/dist/entries/core.js +12 -6
  84. package/dist/entries/xlsx.d.ts +1 -1
  85. package/dist/errors.d.ts +64 -0
  86. package/dist/errors.js +22 -0
  87. package/dist/hex.d.ts +8 -0
  88. package/dist/hex.js +3 -0
  89. package/dist/io/csv/delimiter.d.ts +9 -0
  90. package/dist/io/csv/delimiter.js +6 -0
  91. package/dist/io/csv/read.js +11 -4
  92. package/dist/io/csv/write.d.ts +9 -2
  93. package/dist/io/csv/write.js +9 -38
  94. package/dist/io/opc/inflate.js +15 -1
  95. package/dist/io/opc/namespaces.d.ts +16 -0
  96. package/dist/io/opc/namespaces.js +4 -0
  97. package/dist/io/opc/part-paths.d.ts +10 -0
  98. package/dist/io/opc/part-paths.js +16 -1
  99. package/dist/io/opc/read-opc.d.ts +69 -10
  100. package/dist/io/opc/read-opc.js +49 -55
  101. package/dist/io/opc/read-options.d.ts +5 -1
  102. package/dist/io/opc/rels.d.ts +1 -6
  103. package/dist/io/opc/rels.js +1 -4
  104. package/dist/io/opc/sniff-format.d.ts +3 -3
  105. package/dist/io/opc/sniff-format.js +2 -2
  106. package/dist/io/style/xf-style.d.ts +48 -2
  107. package/dist/io/style/xf-style.js +14 -0
  108. package/dist/io/xlsb/formula.d.ts +2 -2
  109. package/dist/io/xlsb/formula.js +49 -34
  110. package/dist/io/xlsb/primitives.js +8 -14
  111. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  112. package/dist/io/xlsb/ptg-functions.js +385 -596
  113. package/dist/io/xlsb/read-styles.js +50 -83
  114. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  115. package/dist/io/xlsb/read-worksheet.js +73 -89
  116. package/dist/io/xlsb/read.d.ts +6 -3
  117. package/dist/io/xlsb/read.js +47 -29
  118. package/dist/io/xlsb/record-stream.d.ts +30 -0
  119. package/dist/io/xlsb/record-stream.js +20 -0
  120. package/dist/io/xlsb/record-types.d.ts +1 -5
  121. package/dist/io/xlsb/record-types.js +1 -5
  122. package/dist/io/xlsx/cell-accumulator.d.ts +9 -2
  123. package/dist/io/xlsx/cell-accumulator.js +32 -9
  124. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  125. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  126. package/dist/io/xlsx/cell-value.d.ts +5 -3
  127. package/dist/io/xlsx/cell-value.js +13 -15
  128. package/dist/io/xlsx/color-xml.js +3 -2
  129. package/dist/io/xlsx/column-budget.d.ts +26 -0
  130. package/dist/io/xlsx/column-budget.js +22 -0
  131. package/dist/io/xlsx/comments.d.ts +6 -2
  132. package/dist/io/xlsx/comments.js +36 -31
  133. package/dist/io/xlsx/conditional-formatting.d.ts +9 -5
  134. package/dist/io/xlsx/conditional-formatting.js +51 -46
  135. package/dist/io/xlsx/data-validation.d.ts +9 -7
  136. package/dist/io/xlsx/data-validation.js +38 -35
  137. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  138. package/dist/io/xlsx/edit-vba.js +15 -29
  139. package/dist/io/xlsx/font-xml.d.ts +7 -0
  140. package/dist/io/xlsx/font-xml.js +104 -0
  141. package/dist/io/xlsx/hyperlinks.d.ts +41 -7
  142. package/dist/io/xlsx/hyperlinks.js +40 -28
  143. package/dist/io/xlsx/images.js +24 -25
  144. package/dist/io/xlsx/namespaces.d.ts +16 -0
  145. package/dist/io/xlsx/namespaces.js +7 -0
  146. package/dist/io/xlsx/package-plan.d.ts +23 -3
  147. package/dist/io/xlsx/package-plan.js +66 -25
  148. package/dist/io/xlsx/part-names.d.ts +41 -0
  149. package/dist/io/xlsx/part-names.js +46 -0
  150. package/dist/io/xlsx/pivot.js +8 -7
  151. package/dist/io/xlsx/read-parts.d.ts +59 -0
  152. package/dist/io/xlsx/read-parts.js +252 -0
  153. package/dist/io/xlsx/read-pivot.js +3 -1
  154. package/dist/io/xlsx/read-repair.d.ts +26 -0
  155. package/dist/io/xlsx/read-repair.js +39 -0
  156. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +1 -1
  157. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +7 -3
  158. package/dist/io/xlsx/read-rows.d.ts +18 -5
  159. package/dist/io/xlsx/read-rows.js +50 -37
  160. package/dist/io/xlsx/read-shared-strings.js +5 -3
  161. package/dist/io/xlsx/read-styles.d.ts +1 -6
  162. package/dist/io/xlsx/read-styles.js +65 -109
  163. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  164. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +11 -1
  166. package/dist/io/xlsx/read-worksheet.js +51 -193
  167. package/dist/io/xlsx/read.d.ts +5 -13
  168. package/dist/io/xlsx/read.js +111 -436
  169. package/dist/io/xlsx/rich-text.js +1 -1
  170. package/dist/io/xlsx/row-position.d.ts +18 -0
  171. package/dist/io/xlsx/row-position.js +11 -0
  172. package/dist/io/xlsx/row-xml.d.ts +128 -0
  173. package/dist/io/xlsx/row-xml.js +203 -0
  174. package/dist/io/xlsx/shared-formulas.js +2 -2
  175. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  176. package/dist/io/xlsx/sheet-properties.js +166 -37
  177. package/dist/io/xlsx/style-elements.d.ts +43 -0
  178. package/dist/io/xlsx/style-elements.js +156 -0
  179. package/dist/io/xlsx/styles.d.ts +12 -3
  180. package/dist/io/xlsx/styles.js +5 -204
  181. package/dist/io/xlsx/tables.js +11 -14
  182. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  183. package/dist/io/xlsx/theme-xml.js +107 -69
  184. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  185. package/dist/io/xlsx/threaded-comments.js +3 -1
  186. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  187. package/dist/io/xlsx/workbook-xml.js +84 -57
  188. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  189. package/dist/io/xlsx/worksheet-xml.js +59 -242
  190. package/dist/io/xlsx/write-stream.d.ts +71 -16
  191. package/dist/io/xlsx/write-stream.js +79 -41
  192. package/dist/io/xlsx/write.d.ts +6 -2
  193. package/dist/io/xlsx/write.js +217 -153
  194. package/dist/rel-type.d.ts +16 -0
  195. package/dist/rel-type.js +6 -0
  196. package/dist/token-set.d.ts +28 -0
  197. package/dist/token-set.js +7 -0
  198. package/dist/vba/bytes.d.ts +14 -5
  199. package/dist/vba/bytes.js +16 -6
  200. package/dist/vba/cfb-format.d.ts +32 -0
  201. package/dist/vba/cfb-format.js +13 -0
  202. package/dist/vba/cfb-writer.d.ts +9 -0
  203. package/dist/vba/cfb-writer.js +28 -25
  204. package/dist/vba/cfb.d.ts +13 -1
  205. package/dist/vba/cfb.js +73 -57
  206. package/dist/vba/codepage.js +2 -1
  207. package/dist/vba/errors.d.ts +7 -0
  208. package/dist/vba/ms-ovba.js +79 -23
  209. package/dist/vba/project-editor.js +35 -40
  210. package/dist/vba/project.d.ts +11 -1
  211. package/dist/vba/project.js +40 -21
  212. package/dist/vba/vba-encoding.js +5 -6
  213. package/dist/xml/xml-attrs.d.ts +37 -0
  214. package/dist/xml/xml-attrs.js +30 -0
  215. package/dist/xml/xml-chars.d.ts +40 -0
  216. package/dist/xml/xml-chars.js +12 -0
  217. package/dist/xml/xml-namespaces.d.ts +31 -0
  218. package/dist/xml/xml-namespaces.js +50 -0
  219. package/dist/xml/xml-read.d.ts +128 -81
  220. package/dist/xml/xml-read.js +161 -178
  221. package/dist/xml/xml-scan.d.ts +78 -0
  222. package/dist/xml/xml-scan.js +180 -0
  223. package/dist/xml/xml.d.ts +29 -28
  224. package/dist/xml/xml.js +24 -24
  225. package/package.json +13 -9
@@ -1,28 +1,20 @@
1
- import { numFmtCodeFor, 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
- return { cellXfs: [], namedStyles: [] };
17
+ return { cellXfs: [], namedStyles: [], preserved: NO_PRESERVED_STYLE_TABLES };
26
18
  const numFmtCodes = new Map();
27
19
  const fonts = [];
28
20
  const fills = [];
@@ -30,50 +22,46 @@ 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;
27
+ if (blocks.boundary(record.type))
37
28
  continue;
38
- }
39
- const started = COLLECTION_STARTS.get(record.type);
40
- if (started !== undefined) {
41
- collection = started;
42
- 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:
73
58
  break;
74
59
  }
75
60
  }
76
- return resolveStyleTable({ directXfs, namedXfs, labels, fonts });
61
+ return {
62
+ ...resolveStyleTable({ directXfs, namedXfs, labels, fonts }),
63
+ preserved: NO_PRESERVED_STYLE_TABLES,
64
+ };
77
65
  }
78
66
  function readXf(reader, deps, isDirect) {
79
67
  const parent = reader.u16();
@@ -107,34 +95,33 @@ function readXf(reader, deps, isDirect) {
107
95
  return draft;
108
96
  }
109
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
+ };
110
116
  function readAlignment(flags, rotation, indent) {
117
+ const bits = { flags, rotation, indent };
111
118
  const out = {};
112
- const horizontal = HORIZONTAL_ALIGNMENTS[flags & 0b111];
113
- if (horizontal !== undefined)
114
- out.horizontal = horizontal;
115
- const vertical = VERTICAL_ALIGNMENTS[(flags >> 3) & 0b111];
116
- if (vertical !== undefined)
117
- out.vertical = vertical;
118
- if (rotation !== 0)
119
- out.textRotation = rotation;
120
- if ((flags & 0x0040) !== 0)
121
- out.wrapText = true;
122
- if (indent !== 0)
123
- out.indent = indent;
124
- if ((flags & 0x0100) !== 0)
125
- out.shrinkToFit = true;
126
- const readingOrder = (flags >> 10) & 0b11;
127
- if (readingOrder !== 0)
128
- out.readingOrder = readingOrder;
119
+ for (const facet of ALIGNMENT_FACETS)
120
+ Object.assign(out, BIFF12_ALIGNMENT[facet.key](bits));
129
121
  return Object.keys(out).length > 0 ? out : undefined;
130
122
  }
131
123
  function readProtection(flags) {
132
- const out = {};
133
- if ((flags & 0x1000) === 0)
134
- out.locked = false;
135
- if ((flags & 0x2000) !== 0)
136
- out.hidden = true;
137
- return Object.keys(out).length > 0 ? out : undefined;
124
+ return protectionFrom({ locked: (flags & 0x1000) !== 0, hidden: (flags & 0x2000) !== 0 });
138
125
  }
139
126
  const HORIZONTAL_ALIGNMENTS = [
140
127
  undefined,
@@ -205,7 +192,8 @@ const UNDERLINE_STYLES = new Map([
205
192
  [0x22, 'doubleAccounting'],
206
193
  ]);
207
194
  function readFill(reader) {
208
- const pattern = FILL_PATTERNS[reader.u32()];
195
+ const raw = FILL_PATTERNS_IN_SCHEMA_ORDER[reader.u32()];
196
+ const pattern = raw === 'none' ? undefined : raw;
209
197
  if (pattern === undefined)
210
198
  return undefined;
211
199
  const fgColor = notSentinel(reader.color(), AUTOMATIC_FOREGROUND);
@@ -222,27 +210,6 @@ const AUTOMATIC_BACKGROUND = 65;
222
210
  function notSentinel(color, sentinel) {
223
211
  return color?.indexed === sentinel ? undefined : color;
224
212
  }
225
- const FILL_PATTERNS = [
226
- undefined,
227
- 'solid',
228
- 'mediumGray',
229
- 'darkGray',
230
- 'lightGray',
231
- 'darkHorizontal',
232
- 'darkVertical',
233
- 'darkDown',
234
- 'darkUp',
235
- 'darkGrid',
236
- 'darkTrellis',
237
- 'lightHorizontal',
238
- 'lightVertical',
239
- 'lightDown',
240
- 'lightUp',
241
- 'lightGrid',
242
- 'lightTrellis',
243
- 'gray125',
244
- 'gray0625',
245
- ];
246
213
  function readBorder(reader) {
247
214
  const flags = reader.u8();
248
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,36 +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
- import { packageAccessors, parseRelationships, resolveWorkbookPart } from '../opc/read-opc.js';
6
- import { DEFAULT_MAX_UNCOMPRESSED } from '../opc/read-options.js';
7
- import { inflateSpreadsheetPackage } from '../opc/sniff-format.js';
6
+ import { openSpreadsheetPackage, packageAccessors, readPartRelationships } from '../opc/read-opc.js';
7
+ import { XlsbParseError } from './errors.js';
8
8
  import { decodeFormula } from './formula.js';
9
9
  import { RecordReader } from './primitives.js';
10
10
  import { parseSharedStrings } from './read-shared-strings.js';
11
11
  import { parseStyleTable } from './read-styles.js';
12
12
  import { parseWorksheet } from './read-worksheet.js';
13
- import { readRecords } from './record-stream.js';
13
+ import { blockTracker, readRecords } from './record-stream.js';
14
14
  import { BRT } from './record-types.js';
15
15
  export const XLSB_WORKBOOK_PART = 'xl/workbook.bin';
16
16
  export function readXlsb(data, options = {}) {
17
- const cap = options.maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
18
- return readXlsbPackage(inflateSpreadsheetPackage(data, cap));
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);
19
22
  }
20
- export function readXlsbPackage(files) {
23
+ export function readXlsbPackage(files, documentPath = XLSB_WORKBOOK_PART) {
21
24
  const { partText, partBytes } = packageAccessors(files);
22
- const workbookPart = partBytes(XLSB_WORKBOOK_PART);
25
+ const workbookPart = partBytes(documentPath);
23
26
  if (workbookPart === undefined) {
24
- 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`);
25
28
  }
26
- const rels = parseRelationships(partText('xl/_rels/workbook.bin.rels') ?? '');
27
- const sharedStrings = parseSharedStrings(partBytes('xl/sharedStrings.bin'));
28
- 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'));
29
32
  const workbook = new Workbook();
30
33
  if (namedStyles.length > 1)
31
34
  workbook[INTERNAL].restoreNamedStyles(namedStyles);
32
35
  workbook[INTERNAL].restoreDefaultFont(defaultFont);
33
36
  const declaration = readWorkbookPart(workbookPart);
37
+ workbook.dateEpoch = declaration.dateEpoch;
34
38
  const scope = {
35
39
  sheetNames: declaration.sheets.map((sheet) => sheet.name),
36
40
  externSheets: declaration.externSheets,
@@ -39,39 +43,45 @@ export function readXlsbPackage(files) {
39
43
  };
40
44
  for (const declared of declaration.sheets) {
41
45
  const sheet = workbook.addWorksheet(declared.name, { state: declared.state });
42
- const target = declared.relId === undefined ? undefined : rels.get(declared.relId);
43
- const part = target === undefined ? undefined : partBytes(resolveWorkbookPart(target));
46
+ const target = declared.relId === undefined ? undefined : rels.byId(declared.relId)?.target;
47
+ const part = target === undefined ? undefined : partBytes(rels.pathOf(target));
44
48
  if (part !== undefined)
45
- parseWorksheet(part, sheet, sharedStrings, cellXfs, scope);
49
+ parseWorksheet(part, {
50
+ sheet,
51
+ sharedStrings,
52
+ xfStyles: cellXfs,
53
+ scope,
54
+ dateEpoch: declaration.dateEpoch,
55
+ });
46
56
  }
47
57
  for (const defined of definedNames(declaration, scope))
48
58
  workbook.defineName(defined);
49
59
  return workbook;
50
60
  }
61
+ const WORKBOOK_BLOCKS = [
62
+ [BRT.BeginBundleShs, BRT.EndBundleShs, 'bundle'],
63
+ [BRT.BeginExternals, BRT.EndExternals, 'externals'],
64
+ ];
51
65
  function readWorkbookPart(part) {
52
66
  const sheets = [];
53
67
  const names = [];
54
68
  let externSheets = [];
55
- let inBundle = false;
56
- let inExternals = false;
69
+ const blocks = blockTracker(WORKBOOK_BLOCKS);
57
70
  let supportingBooks = 0;
58
71
  let selfSupBook;
72
+ let dateEpoch = 1900;
59
73
  for (const record of readRecords(part)) {
60
- if (record.type === BRT.BeginBundleShs)
61
- inBundle = true;
62
- else if (record.type === BRT.EndBundleShs)
63
- inBundle = false;
64
- 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'))
65
79
  sheets.push(readSheet(record.data));
66
- else if (record.type === BRT.BeginExternals)
67
- inExternals = true;
68
- else if (record.type === BRT.EndExternals)
69
- inExternals = false;
70
80
  else if (record.type === BRT.ExternSheet)
71
81
  externSheets = readExternSheets(record.data);
72
82
  else if (record.type === BRT.SupSelf)
73
83
  selfSupBook = supportingBooks++;
74
- else if (inExternals)
84
+ else if (blocks.isOpen('externals'))
75
85
  supportingBooks++;
76
86
  else if (record.type === BRT.Name)
77
87
  names.push(readName(record.data));
@@ -81,8 +91,14 @@ function readWorkbookPart(part) {
81
91
  names,
82
92
  externSheets,
83
93
  selfSupBook: supportingBooks === 1 ? selfSupBook : undefined,
94
+ dateEpoch,
84
95
  };
85
96
  }
97
+ function readDateEpoch(data) {
98
+ if (data.length < 4)
99
+ return 1900;
100
+ return (new RecordReader(data).u32() & 1) === 0 ? 1900 : 1904;
101
+ }
86
102
  function readSheet(data) {
87
103
  const reader = new RecordReader(data);
88
104
  const state = SHEET_STATES[reader.u32()] ?? 'visible';
@@ -93,9 +109,11 @@ function readSheet(data) {
93
109
  function readExternSheets(data) {
94
110
  const reader = new RecordReader(data);
95
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
+ }
96
116
  const entries = [];
97
- if (count * XTI_BYTES > reader.remaining)
98
- return entries;
99
117
  for (let index = 0; index < count; index++) {
100
118
  entries.push({ supBook: reader.u32(), firstSheet: reader.i32(), lastSheet: reader.i32() });
101
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
+ }