@shbernal/ts-xlsx 2.1.0 → 3.0.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 (100) hide show
  1. package/dist/core/autofilter.d.ts +1 -1
  2. package/dist/core/autofilter.js +3 -5
  3. package/dist/core/axis-handle.d.ts +8 -0
  4. package/dist/core/axis-handle.js +14 -0
  5. package/dist/core/cell.d.ts +24 -0
  6. package/dist/core/cell.js +9 -1
  7. package/dist/core/column.d.ts +4 -1
  8. package/dist/core/column.js +31 -35
  9. package/dist/core/conditional-formatting.d.ts +5 -5
  10. package/dist/core/conditional-formatting.js +11 -25
  11. package/dist/core/data-validation.d.ts +3 -3
  12. package/dist/core/data-validation.js +7 -15
  13. package/dist/core/image.d.ts +1 -1
  14. package/dist/core/image.js +2 -4
  15. package/dist/core/merge-index.d.ts +31 -0
  16. package/dist/core/merge-index.js +60 -0
  17. package/dist/core/merge.d.ts +0 -10
  18. package/dist/core/merge.js +0 -8
  19. package/dist/core/page-setup.d.ts +72 -2
  20. package/dist/core/page-setup.js +34 -8
  21. package/dist/core/pivot-table.d.ts +1 -1
  22. package/dist/core/pivot-table.js +4 -8
  23. package/dist/core/row.d.ts +4 -1
  24. package/dist/core/row.js +19 -23
  25. package/dist/core/style.d.ts +51 -7
  26. package/dist/core/style.js +29 -34
  27. package/dist/core/table.d.ts +1 -1
  28. package/dist/core/table.js +3 -5
  29. package/dist/core/used-extent.d.ts +44 -0
  30. package/dist/core/used-extent.js +102 -0
  31. package/dist/core/worksheet.d.ts +17 -6
  32. package/dist/core/worksheet.js +48 -42
  33. package/dist/customui/ribbon.js +4 -6
  34. package/dist/io/opc/inflate.js +1 -1
  35. package/dist/io/opc/part-paths.d.ts +1 -0
  36. package/dist/io/opc/part-paths.js +15 -0
  37. package/dist/io/opc/read-opc.d.ts +22 -3
  38. package/dist/io/opc/read-opc.js +11 -39
  39. package/dist/io/opc/rels.d.ts +1 -6
  40. package/dist/io/opc/rels.js +1 -4
  41. package/dist/io/style/xf-style.d.ts +26 -2
  42. package/dist/io/style/xf-style.js +6 -0
  43. package/dist/io/xlsb/read-styles.js +6 -3
  44. package/dist/io/xlsb/read.d.ts +1 -1
  45. package/dist/io/xlsb/read.js +5 -8
  46. package/dist/io/xlsx/cell-accumulator.d.ts +1 -1
  47. package/dist/io/xlsx/cell-accumulator.js +1 -1
  48. package/dist/io/xlsx/cell-value.js +1 -1
  49. package/dist/io/xlsx/color-xml.js +1 -1
  50. package/dist/io/xlsx/comments.js +2 -1
  51. package/dist/io/xlsx/conditional-formatting.d.ts +3 -2
  52. package/dist/io/xlsx/conditional-formatting.js +22 -21
  53. package/dist/io/xlsx/data-validation.d.ts +9 -7
  54. package/dist/io/xlsx/data-validation.js +10 -9
  55. package/dist/io/xlsx/edit-vba.js +4 -11
  56. package/dist/io/xlsx/hyperlinks.d.ts +4 -2
  57. package/dist/io/xlsx/hyperlinks.js +20 -17
  58. package/dist/io/xlsx/images.js +15 -14
  59. package/dist/io/xlsx/package-plan.d.ts +9 -1
  60. package/dist/io/xlsx/package-plan.js +29 -16
  61. package/dist/io/xlsx/part-names.d.ts +41 -0
  62. package/dist/io/xlsx/part-names.js +46 -0
  63. package/dist/io/xlsx/read-pivot.js +2 -1
  64. package/dist/io/xlsx/read-rows.js +12 -13
  65. package/dist/io/xlsx/read-shared-strings.js +2 -1
  66. package/dist/io/xlsx/read-styles.d.ts +1 -1
  67. package/dist/io/xlsx/read-styles.js +63 -42
  68. package/dist/io/xlsx/read-worksheet.d.ts +10 -1
  69. package/dist/io/xlsx/read-worksheet.js +43 -56
  70. package/dist/io/xlsx/read.d.ts +2 -3
  71. package/dist/io/xlsx/read.js +81 -73
  72. package/dist/io/xlsx/rich-runs.d.ts +1 -1
  73. package/dist/io/xlsx/rich-runs.js +2 -1
  74. package/dist/io/xlsx/sheet-properties.js +21 -28
  75. package/dist/io/xlsx/styles.js +23 -20
  76. package/dist/io/xlsx/tables.js +2 -1
  77. package/dist/io/xlsx/theme-xml.js +1 -1
  78. package/dist/io/xlsx/threaded-comments.js +2 -1
  79. package/dist/io/xlsx/workbook-xml.js +29 -28
  80. package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
  81. package/dist/io/xlsx/worksheet-xml.js +14 -16
  82. package/dist/io/xlsx/write-stream.d.ts +6 -0
  83. package/dist/io/xlsx/write-stream.js +30 -22
  84. package/dist/io/xlsx/write.d.ts +5 -0
  85. package/dist/io/xlsx/write.js +155 -131
  86. package/dist/token-set.d.ts +15 -0
  87. package/dist/token-set.js +4 -0
  88. package/dist/vba/cfb-format.d.ts +24 -0
  89. package/dist/vba/cfb-format.js +12 -0
  90. package/dist/vba/cfb-writer.js +17 -17
  91. package/dist/vba/cfb.js +1 -7
  92. package/dist/vba/ms-ovba.js +32 -11
  93. package/dist/vba/project.js +13 -13
  94. package/dist/vba/vba-encoding.js +1 -1
  95. package/dist/xml/xml-read.d.ts +59 -74
  96. package/dist/xml/xml-read.js +83 -176
  97. package/dist/xml/xml-scan.d.ts +100 -0
  98. package/dist/xml/xml-scan.js +208 -0
  99. package/dist/xml/xml.d.ts +1 -1
  100. package/package.json +2 -3
@@ -0,0 +1,102 @@
1
+ import { cellCarriesContent } from './cell.js';
2
+ function rowIsUsed(cols) {
3
+ for (const cell of cols.values()) {
4
+ if (cellCarriesContent(cell))
5
+ return true;
6
+ }
7
+ return false;
8
+ }
9
+ export class UsedExtent {
10
+ #storage;
11
+ #topRowKey = 0;
12
+ #topColumnKey = 0;
13
+ #topDeclaredRow = 0;
14
+ #topDeclaredColumn = 0;
15
+ #stale = false;
16
+ constructor(storage) {
17
+ this.#storage = storage;
18
+ }
19
+ noteCell(row, col) {
20
+ if (row > this.#topRowKey)
21
+ this.#topRowKey = row;
22
+ if (col > this.#topColumnKey)
23
+ this.#topColumnKey = col;
24
+ }
25
+ noteDeclaredRow(row) {
26
+ if (row > this.#topDeclaredRow)
27
+ this.#topDeclaredRow = row;
28
+ }
29
+ noteDeclaredColumn(col) {
30
+ if (col > this.#topDeclaredColumn)
31
+ this.#topDeclaredColumn = col;
32
+ }
33
+ noteMerge(rect) {
34
+ this.noteDeclaredRow(rect.bottom);
35
+ this.noteDeclaredColumn(rect.right);
36
+ }
37
+ invalidate() {
38
+ this.#stale = true;
39
+ }
40
+ reset() {
41
+ this.#topRowKey = 0;
42
+ this.#topColumnKey = 0;
43
+ this.#topDeclaredRow = 0;
44
+ this.#topDeclaredColumn = 0;
45
+ this.#stale = false;
46
+ }
47
+ get lastRow() {
48
+ this.#refresh();
49
+ if (this.#topRowKey <= this.#topDeclaredRow)
50
+ return this.#topDeclaredRow;
51
+ const top = this.#storage.rows.get(this.#topRowKey);
52
+ if (top !== undefined && rowIsUsed(top))
53
+ return this.#topRowKey;
54
+ let last = this.#topDeclaredRow;
55
+ for (const [number, cols] of this.#storage.rows) {
56
+ if (number > last && rowIsUsed(cols))
57
+ last = number;
58
+ }
59
+ return last;
60
+ }
61
+ get lastColumn() {
62
+ this.#refresh();
63
+ if (this.#topColumnKey <= this.#topDeclaredColumn)
64
+ return this.#topDeclaredColumn;
65
+ for (const cols of this.#storage.rows.values()) {
66
+ const cell = cols.get(this.#topColumnKey);
67
+ if (cell !== undefined && cellCarriesContent(cell))
68
+ return this.#topColumnKey;
69
+ }
70
+ let last = this.#topDeclaredColumn;
71
+ for (const cols of this.#storage.rows.values()) {
72
+ for (const [col, cell] of cols) {
73
+ if (col > last && cellCarriesContent(cell))
74
+ last = col;
75
+ }
76
+ }
77
+ return last;
78
+ }
79
+ #refresh() {
80
+ if (!this.#stale)
81
+ return;
82
+ this.#stale = false;
83
+ this.#topRowKey = 0;
84
+ this.#topColumnKey = 0;
85
+ this.#topDeclaredRow = 0;
86
+ this.#topDeclaredColumn = 0;
87
+ for (const [number, cols] of this.#storage.rows) {
88
+ if (number > this.#topRowKey)
89
+ this.#topRowKey = number;
90
+ for (const col of cols.keys()) {
91
+ if (col > this.#topColumnKey)
92
+ this.#topColumnKey = col;
93
+ }
94
+ }
95
+ for (const number of this.#storage.rowProperties.keys())
96
+ this.noteDeclaredRow(number);
97
+ for (const index of this.#storage.columns.keys())
98
+ this.noteDeclaredColumn(index);
99
+ for (const rect of this.#storage.mergeRects)
100
+ this.noteMerge(rect);
101
+ }
102
+ }
@@ -24,7 +24,7 @@ import type { CellValue } from './value.ts';
24
24
  */
25
25
  export type Visibility = 'visible' | 'hidden' | 'veryHidden';
26
26
  /** Narrow a raw `<sheet state>` or `<workbookView visibility>` token to a known {@link Visibility}. */
27
- export declare function isVisibility(value: string): value is Visibility;
27
+ export declare const isVisibility: (value: string) => value is Visibility;
28
28
  export interface WorksheetState {
29
29
  /** Sheet visibility, as Excel models it. Defaults to `visible`. */
30
30
  readonly state: Visibility;
@@ -276,14 +276,21 @@ export declare class Worksheet {
276
276
  * or 0 for an empty sheet. Spans gaps: a value in row 5 makes this 5 even if rows 2–4
277
277
  * are empty. This is the used-range extent, not a populated-row tally (see
278
278
  * {@link actualRowCount}).
279
+ *
280
+ * *Carrying anything* is deliberately wide: a value, a cell's own style facet, quote-prefix flag
281
+ * or named-style link, a note, a row height or outline level, or a merge reaching down. Styling a
282
+ * band of empty rows is how a template is laid out, so those rows are used and {@link addRow}
283
+ * appends past them. A cell merely materialised by {@link getCell} and left untouched carries
284
+ * nothing, so reading a far address never grows the sheet.
279
285
  */
280
286
  get rowCount(): number;
281
287
  /** The number of rows that hold at least one non-empty cell, ignoring gaps and formatting-only rows. */
282
288
  get actualRowCount(): number;
283
289
  /**
284
- * The 1-based index of the last column carrying anything (a non-empty cell or its own format
290
+ * The 1-based index of the last column carrying anything (a used cell or the column's own format
285
291
  * properties), or 0 for an empty sheet. The used-range width, mirroring {@link rowCount} for the
286
- * other axis: a value in column E makes this 5 even if columns B–D are empty.
292
+ * other axis, down to what *carrying anything* means: a value in column E makes this 5 even if
293
+ * columns B–D are empty, and a column holding nothing but a styled empty cell is still used.
287
294
  */
288
295
  get columnCount(): number;
289
296
  /**
@@ -548,9 +555,13 @@ export declare class Worksheet {
548
555
  /**
549
556
  * Copy the row at the 1-based `start`, `options.count` times (default 1). With `options.insert`
550
557
  * (the default) the copies are inserted directly after the source, shifting the rows below, and
551
- * any merged range there, down by `count`; otherwise the copies overwrite the rows immediately
552
- * below without shifting. Each copy is a faithful duplicate of the source's values and per-cell
553
- * styles, and carries no merge of its own, so a range can be merged onto a duplicated row afterwards.
558
+ * any merged range there, down by `count`; otherwise the copies *replace* the rows immediately
559
+ * below without shifting. A destination row is not overlaid but wholly re-made, so a cell it held
560
+ * in a column the source leaves empty is dropped, exactly as it would be with a shifting insert.
561
+ *
562
+ * Each copy is a faithful duplicate of the source: its cell values, its per-cell styles, and its
563
+ * row properties (height, hidden, outline level, row fill). It carries no merge of its own, so a
564
+ * range can be merged onto a duplicated row afterwards.
554
565
  *
555
566
  * @throws {RangeError} if `start` is not a positive integer or `count` is negative.
556
567
  */
@@ -1,5 +1,6 @@
1
1
  import { AuthoringError } from '../errors.js';
2
- import { decodeCellRef, decodeRange, encodeAddress, rectsOverlap, tryDecodeCellRef, } from './address.js';
2
+ import { tokenSet } from '../token-set.js';
3
+ import { decodeCellRef, decodeRange, encodeAddress, tryDecodeCellRef } from './address.js';
3
4
  import { canonicalizeAutoFilter } from './autofilter.js';
4
5
  import { applyCellStyle, Cell, copyCellContent } from './cell.js';
5
6
  import { Column } from './column.js';
@@ -7,20 +8,19 @@ import { ConditionalFormattingOverlay } from './conditional-formatting-overlay.j
7
8
  import { DataValidationOverlay } from './data-validation-overlay.js';
8
9
  import { GridEdits } from './grid-edits.js';
9
10
  import { INTERNAL } from './internal.js';
10
- import { clearCoveredValues, masterOf } from './merge.js';
11
+ import { MergeIndex } from './merge-index.js';
12
+ import { clearCoveredValues } from './merge.js';
11
13
  import { PivotTable } from './pivot-table.js';
12
14
  import { deriveCredential, } from './protection.js';
13
15
  import { Range, rangeFrom } from './range.js';
14
16
  import { buildRowCells, positionalPlacements, rowPlacements } from './row-input.js';
15
17
  import { Row } from './row.js';
16
18
  import { Table } from './table.js';
19
+ import { UsedExtent } from './used-extent.js';
17
20
  import { WorksheetComments } from './worksheet-comments.js';
18
21
  import { WORKSHEET_MODEL_FACETS } from './worksheet-model.js';
19
22
  import { WorksheetPictures } from './worksheet-pictures.js';
20
- const VISIBILITIES = { visible: true, hidden: true, veryHidden: true };
21
- export function isVisibility(value) {
22
- return Object.hasOwn(VISIBILITIES, value);
23
- }
23
+ export const isVisibility = tokenSet({ visible: true, hidden: true, veryHidden: true });
24
24
  export class Worksheet {
25
25
  name;
26
26
  id;
@@ -54,10 +54,19 @@ export class Worksheet {
54
54
  #protection;
55
55
  #autoFilter;
56
56
  #edits;
57
+ #extent;
58
+ #mergeIndex;
57
59
  constructor(name, id, state = 'visible') {
58
60
  this.name = name;
59
61
  this.id = id;
60
62
  this.state = state;
63
+ this.#mergeIndex = new MergeIndex(this.#mergeRects);
64
+ this.#extent = new UsedExtent({
65
+ rows: this.#rows,
66
+ rowProperties: this.#rowProperties,
67
+ columns: this.#columns,
68
+ mergeRects: this.#mergeRects,
69
+ });
61
70
  this.#edits = new GridEdits({
62
71
  rows: this.#rows,
63
72
  rowProperties: this.#rowProperties,
@@ -79,7 +88,7 @@ export class Worksheet {
79
88
  }
80
89
  getCell(reference) {
81
90
  const { col, row } = decodeCellRef(reference);
82
- const master = masterOf(this.#mergeRects, row, col);
91
+ const master = this.#mergeIndex.masterOf(row, col);
83
92
  return this.#cellAt(master.row, master.col);
84
93
  }
85
94
  hasCell(row, col) {
@@ -97,30 +106,17 @@ export class Worksheet {
97
106
  return new Range(this, referenceOrTop, left ?? 0, bottom ?? 0, right ?? 0);
98
107
  }
99
108
  get rowCount() {
100
- let last = 0;
101
- for (const [number, cols] of this.#rows) {
102
- if (number > last && this.#rowHasContent(cols))
103
- last = number;
104
- }
105
- for (const number of this.#rowProperties.keys()) {
106
- if (number > last)
107
- last = number;
108
- }
109
- for (const rect of this.#mergeRects) {
110
- if (rect.bottom > last)
111
- last = rect.bottom;
112
- }
113
- return last;
109
+ return this.#extent.lastRow;
114
110
  }
115
111
  get actualRowCount() {
116
112
  let count = 0;
117
113
  for (const cols of this.#rows.values()) {
118
- if (this.#rowHasContent(cols))
114
+ if (this.#rowIsPopulated(cols))
119
115
  count++;
120
116
  }
121
117
  return count;
122
118
  }
123
- #rowHasContent(cols) {
119
+ #rowIsPopulated(cols) {
124
120
  for (const cell of cols.values()) {
125
121
  if (cell.value !== null)
126
122
  return true;
@@ -128,22 +124,7 @@ export class Worksheet {
128
124
  return false;
129
125
  }
130
126
  get columnCount() {
131
- let last = 0;
132
- for (const cols of this.#rows.values()) {
133
- for (const [col, cell] of cols) {
134
- if (cell.value !== null && col > last)
135
- last = col;
136
- }
137
- }
138
- for (const index of this.#columns.keys()) {
139
- if (index > last)
140
- last = index;
141
- }
142
- for (const rect of this.#mergeRects) {
143
- if (rect.right > last)
144
- last = rect.right;
145
- }
146
- return last;
127
+ return this.#extent.lastColumn;
147
128
  }
148
129
  get usedRange() {
149
130
  const bottom = this.rowCount;
@@ -231,11 +212,12 @@ export class Worksheet {
231
212
  const { top, left, bottom, right } = decodeRange(range);
232
213
  if (top !== undefined && left !== undefined && bottom !== undefined && right !== undefined) {
233
214
  const rect = { top, left, bottom, right };
234
- const clash = this.#mergeRects.find((existing) => rectsOverlap(existing, rect));
235
- if (clash) {
215
+ if (this.#mergeIndex.overlapping(rect) !== undefined) {
236
216
  throw new AuthoringError(`merged range "${range}" overlaps an existing merged region`);
237
217
  }
238
218
  this.#mergeRects.push(rect);
219
+ this.#mergeIndex.note(rect);
220
+ this.#extent.noteMerge(rect);
239
221
  clearCoveredValues(this.#rows, rect);
240
222
  }
241
223
  this.#merges.push(range);
@@ -257,8 +239,11 @@ export class Worksheet {
257
239
  const { top, left, bottom, right } = decodeRange(range);
258
240
  if (top !== undefined && left !== undefined && bottom !== undefined && right !== undefined) {
259
241
  const rectIndex = this.#mergeRects.findIndex((r) => r.top === top && r.left === left && r.bottom === bottom && r.right === right);
260
- if (rectIndex !== -1)
242
+ if (rectIndex !== -1) {
261
243
  this.#mergeRects.splice(rectIndex, 1);
244
+ this.#mergeIndex.invalidate();
245
+ this.#extent.invalidate();
246
+ }
262
247
  }
263
248
  return true;
264
249
  }
@@ -282,6 +267,8 @@ export class Worksheet {
282
267
  assertStartAndCount('splice', 'row', start, count);
283
268
  const inserted = inserts.map((values, i) => buildRowCells(start + i, values, this.#columns));
284
269
  this.#edits.spliceRows(start, count, inserted);
270
+ this.#mergeIndex.invalidate();
271
+ this.#extent.invalidate();
285
272
  }
286
273
  insertRow(pos, values) {
287
274
  this.spliceRows(pos, 0, values);
@@ -323,6 +310,7 @@ export class Worksheet {
323
310
  const { count = 1, insert = true } = options;
324
311
  assertStartAndCount('duplicate', 'row', start, count);
325
312
  const source = this.#rows.get(start);
313
+ const sourceProperties = this.#rowProperties.get(start);
326
314
  const snapshot = (destRow) => {
327
315
  const row = new Map();
328
316
  if (source) {
@@ -334,18 +322,30 @@ export class Worksheet {
334
322
  }
335
323
  return row;
336
324
  };
325
+ const copyProperties = (destRow) => {
326
+ if (sourceProperties === undefined)
327
+ this.#rowProperties.delete(destRow);
328
+ else
329
+ this.#rowProperties.set(destRow, { ...sourceProperties });
330
+ };
337
331
  if (insert) {
338
332
  const copies = Array.from({ length: count }, () => snapshot(start));
339
333
  this.#edits.spliceRows(start + 1, 0, copies);
334
+ this.#mergeIndex.invalidate();
340
335
  }
341
336
  else {
342
337
  for (let i = 1; i <= count; i++)
343
338
  this.#rows.set(start + i, snapshot(start + i));
344
339
  }
340
+ for (let i = 1; i <= count; i++)
341
+ copyProperties(start + i);
342
+ this.#extent.invalidate();
345
343
  }
346
344
  spliceColumns(start, count, ...inserts) {
347
345
  assertStartAndCount('splice', 'column', start, count);
348
346
  this.#edits.spliceColumns(start, count, inserts);
347
+ this.#mergeIndex.invalidate();
348
+ this.#extent.invalidate();
349
349
  }
350
350
  insertColumn(pos, values) {
351
351
  this.spliceColumns(pos, 0, values);
@@ -379,6 +379,8 @@ export class Worksheet {
379
379
  this.#dataValidations.clear();
380
380
  this.#conditionalFormattings.clear();
381
381
  this.#tables.length = 0;
382
+ this.#mergeIndex.invalidate();
383
+ this.#extent.reset();
382
384
  }
383
385
  set model(model) {
384
386
  this.#resetContent();
@@ -412,12 +414,14 @@ export class Worksheet {
412
414
  cell = new Cell(row, col);
413
415
  cols.set(col, cell);
414
416
  }
417
+ this.#extent.noteCell(row, col);
415
418
  return cell;
416
419
  }
417
420
  [INTERNAL] = {
418
421
  evictRow: (number) => {
419
422
  this.#rows.delete(number);
420
423
  this.#rowProperties.delete(number);
424
+ this.#extent.invalidate();
421
425
  },
422
426
  addLoadedPivotTable: (pivot) => {
423
427
  this.#loadedPivotTables.push(pivot);
@@ -439,6 +443,7 @@ export class Worksheet {
439
443
  properties = {};
440
444
  this.#rowProperties.set(number, properties);
441
445
  }
446
+ this.#extent.noteDeclaredRow(number);
442
447
  return properties;
443
448
  },
444
449
  rowCells: (number) => {
@@ -452,6 +457,7 @@ export class Worksheet {
452
457
  properties = {};
453
458
  this.#columns.set(index, properties);
454
459
  }
460
+ this.#extent.noteDeclaredColumn(index);
455
461
  return properties;
456
462
  },
457
463
  columnCells: (index) => {
@@ -1,5 +1,6 @@
1
1
  import { strFromU8 } from 'fflate';
2
- import { boolStrict, localName, xmlEvents } from '../xml/xml-read.js';
2
+ import { tokenSet } from '../token-set.js';
3
+ import { boolStrict, localName, xmlEvents } from '../xml/xml-scan.js';
3
4
  import { CustomUiParseError } from './errors.js';
4
5
  export const CUSTOMUI_2006_NAMESPACE = 'http://schemas.microsoft.com/office/2006/01/customui';
5
6
  export const CUSTOMUI_2009_NAMESPACE = 'http://schemas.microsoft.com/office/2009/07/customui';
@@ -9,7 +10,7 @@ export function isCustomUiRelType(type) {
9
10
  return type.endsWith('/ui/extensibility');
10
11
  }
11
12
  const MAX_DEPTH = 256;
12
- const KNOWN_KINDS = {
13
+ const isKnownControlKind = tokenSet({
13
14
  button: true,
14
15
  toggleButton: true,
15
16
  checkBox: true,
@@ -28,10 +29,7 @@ const KNOWN_KINDS = {
28
29
  dialogBoxLauncher: true,
29
30
  control: true,
30
31
  item: true,
31
- };
32
- function isKnownControlKind(local) {
33
- return Object.hasOwn(KNOWN_KINDS, local);
34
- }
32
+ });
35
33
  export function parseCustomUi(input) {
36
34
  const xml = typeof input === 'string' ? input : strFromU8(input);
37
35
  let root;
@@ -3,7 +3,7 @@ import { concat } from '../../bytes.js';
3
3
  import { PackageReadError } from './errors.js';
4
4
  const INPUT_SLICE = 1 << 14;
5
5
  export function inflatePackage(data, cap) {
6
- const files = {};
6
+ const files = Object.create(null);
7
7
  let total = 0;
8
8
  let failure;
9
9
  const unzip = new Unzip((file) => {
@@ -2,3 +2,4 @@ export declare const THEME_PART_PATH = "xl/theme/theme1.xml";
2
2
  export declare function extensionOf(partPath: string): string;
3
3
  export declare function relsPathFor(partPath: string): string;
4
4
  export declare function relativePartPath(fromPath: string, toPath: string): string;
5
+ export declare function resolveRelativePart(basePart: string, target: string): string;
@@ -22,3 +22,18 @@ export function relativePartPath(fromPath, toPath) {
22
22
  const up = fromDir.length - common;
23
23
  return [...Array(up).fill('..'), ...toSegments.slice(common)].join('/');
24
24
  }
25
+ export function resolveRelativePart(basePart, target) {
26
+ if (target.startsWith('/'))
27
+ return target.slice(1);
28
+ const baseDir = basePart.slice(0, basePart.lastIndexOf('/') + 1);
29
+ const out = [];
30
+ for (const segment of `${baseDir}${target}`.split('/')) {
31
+ if (segment === '' || segment === '.')
32
+ continue;
33
+ if (segment === '..')
34
+ out.pop();
35
+ else
36
+ out.push(segment);
37
+ }
38
+ return out.join('/');
39
+ }
@@ -6,12 +6,31 @@ export interface PackageAccessors {
6
6
  /** A part's raw bytes, or undefined when the package holds no such part. */
7
7
  partBytes: (path: string) => Uint8Array | undefined;
8
8
  }
9
+ /** A spreadsheet package opened for reading: inflated under the read bound, its parts bound to
10
+ * accessors, and its XML office document read if it has one. */
11
+ export interface OpenedSpreadsheet {
12
+ /** The inflated parts, for a reader that hands the whole package on to another codec. */
13
+ readonly files: Record<string, Uint8Array>;
14
+ readonly pkg: PackageAccessors;
15
+ /** `xl/workbook.xml`, or undefined when the package carries no XML office document (a `.xlsb`
16
+ * carries `xl/workbook.bin` instead). Each entry point answers that case for itself: they
17
+ * genuinely want different answers, and the difference is documented where they diverge. */
18
+ readonly workbookXml: string | undefined;
19
+ }
20
+ /**
21
+ * Open a spreadsheet package: the six-step preamble every reader shares, and in particular the two
22
+ * decisions worth having exactly one of. The inflate bound is a security decision (an unbounded
23
+ * inflate is a zip bomb) and "does this package carry an XML office document" is the dispatch
24
+ * decision between the two codecs, so neither should be restated once per entry point.
25
+ *
26
+ * It deliberately stops short of the shared strings and the style table: those are codec-specific
27
+ * (`xl/sharedStrings.xml` against BIFF12's own record stream), and lifting them here would put
28
+ * SpreadsheetML knowledge into the container layer.
29
+ */
30
+ export declare function openSpreadsheetPackage(data: Uint8Array, maxUncompressedBytes: number | undefined): OpenedSpreadsheet;
9
31
  export declare function packageAccessors(files: Record<string, Uint8Array>): PackageAccessors;
10
32
  export declare function relationshipTargetByType(xml: string, suffix: string): string | undefined;
11
33
  export declare function relationshipTargetsByType(xml: string, suffix: string): string[];
12
- export declare function resolveRelativePart(basePart: string, target: string): string;
13
- export declare function resolveWorkbookPart(target: string): string;
14
- export declare function parseRelationships(xml: string): Map<string, string>;
15
34
  export interface RelationshipRecord {
16
35
  readonly id: string;
17
36
  readonly type: string;
@@ -1,6 +1,13 @@
1
1
  import { strFromU8 } from 'fflate';
2
2
  import { openElements } from '../../xml/xml-read.js';
3
- import { extensionOf, relsPathFor } from './part-paths.js';
3
+ import { extensionOf, relsPathFor, resolveRelativePart } from './part-paths.js';
4
+ import { DEFAULT_MAX_UNCOMPRESSED } from './read-options.js';
5
+ import { inflateSpreadsheetPackage } from './sniff-format.js';
6
+ export function openSpreadsheetPackage(data, maxUncompressedBytes) {
7
+ const files = inflateSpreadsheetPackage(data, maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED);
8
+ const pkg = packageAccessors(files);
9
+ return { files, pkg, workbookXml: pkg.partText('xl/workbook.xml') };
10
+ }
4
11
  export function packageAccessors(files) {
5
12
  return {
6
13
  partText: (path) => {
@@ -10,48 +17,13 @@ export function packageAccessors(files) {
10
17
  partBytes: (path) => files[path],
11
18
  };
12
19
  }
13
- function matchesType(attrs, suffix) {
14
- return (attrs.Type !== undefined && attrs.Target !== undefined && attrs.Type.endsWith(`/${suffix}`));
15
- }
16
20
  export function relationshipTargetByType(xml, suffix) {
17
21
  return relationshipTargetsByType(xml, suffix)[0];
18
22
  }
19
23
  export function relationshipTargetsByType(xml, suffix) {
20
- const targets = [];
21
- for (const { attrs } of openElements(xml, 'Relationship')) {
22
- if (matchesType(attrs, suffix))
23
- targets.push(attrs.Target);
24
- }
25
- return targets;
26
- }
27
- export function resolveRelativePart(basePart, target) {
28
- if (target.startsWith('/'))
29
- return target.slice(1);
30
- const baseDir = basePart.slice(0, basePart.lastIndexOf('/') + 1);
31
- const out = [];
32
- for (const segment of `${baseDir}${target}`.split('/')) {
33
- if (segment === '' || segment === '.')
34
- continue;
35
- if (segment === '..')
36
- out.pop();
37
- else
38
- out.push(segment);
39
- }
40
- return out.join('/');
41
- }
42
- export function resolveWorkbookPart(target) {
43
- if (target.startsWith('/'))
44
- return target.slice(1);
45
- return `xl/${target.replace(/^\.\//, '')}`;
46
- }
47
- export function parseRelationships(xml) {
48
- const rels = new Map();
49
- for (const { attrs } of openElements(xml, 'Relationship')) {
50
- if (attrs.Id !== undefined && attrs.Target !== undefined) {
51
- rels.set(attrs.Id, attrs.Target);
52
- }
53
- }
54
- return rels;
24
+ return parseRelationshipRecords(xml)
25
+ .filter((record) => record.type.endsWith(`/${suffix}`))
26
+ .map((record) => record.target);
55
27
  }
56
28
  export function parseRelationshipRecords(xml) {
57
29
  const records = [];
@@ -13,14 +13,9 @@ export declare function relationship(id: string, type: string, target: string, o
13
13
  external?: boolean;
14
14
  }): string;
15
15
  export declare function relationshipsPart(relationships: readonly string[]): string;
16
- export declare function preservedRelsXml(rels: readonly {
17
- id: string;
18
- type: string;
19
- target: string;
20
- external?: boolean;
21
- }[]): string;
22
16
  export declare function relsPartXml(rels: readonly {
23
17
  id: string;
24
18
  type: string;
25
19
  target: string;
20
+ external?: boolean;
26
21
  }[]): string;
@@ -8,9 +8,6 @@ export function relationship(id, type, target, options) {
8
8
  export function relationshipsPart(relationships) {
9
9
  return `${XML_DECLARATION}<Relationships xmlns="${PKG_RELS_NS}">${relationships.join('')}</Relationships>`;
10
10
  }
11
- export function preservedRelsXml(rels) {
12
- return relationshipsPart(rels.map((rel) => relationship(rel.id, rel.type, rel.target, rel.external ? { external: true } : {})));
13
- }
14
11
  export function relsPartXml(rels) {
15
- return relationshipsPart(rels.map((rel) => relationship(rel.id, rel.type, rel.target)));
12
+ return relationshipsPart(rels.map((rel) => relationship(rel.id, rel.type, rel.target, rel.external ? { external: true } : {})));
16
13
  }
@@ -1,5 +1,5 @@
1
1
  import { type Cell } from '../../core/cell.ts';
2
- import { type Border, type CellStyle, type Fill, type Font, type NamedCellStyle } from '../../core/style.ts';
2
+ import { type Border, type CellStyle, type Fill, type Font, type NamedCellStyle, type TableStyleTable } from '../../core/style.ts';
3
3
  /**
4
4
  * What an xf resolves to: the {@link CellStyle} facet tuple, plus the two flags an xf carries that
5
5
  * are not facets. Absent facets stay undefined, matching the contract that an unset facet is simply
@@ -33,7 +33,31 @@ export interface StyleTable {
33
33
  * file declares no font table.
34
34
  */
35
35
  readonly defaultFont?: Font;
36
+ /**
37
+ * The stylesheet's preserved sub-tables, captured verbatim in the same scan that read the xfs: the
38
+ * differential styles a conditional format's `dxfId` indexes, a custom indexed palette, the
39
+ * author's recent-colour swatches, and the custom table-style definitions.
40
+ *
41
+ * They ride here rather than being extracted separately because they come out of the same part,
42
+ * and the reader that wanted them four more times over that part is what this replaced. Each is
43
+ * raw source text: re-parsing and re-serialising is exactly what would cost a foreign `<dxf>` its
44
+ * number format on a round trip.
45
+ */
46
+ readonly preserved: PreservedStyleTables;
47
+ }
48
+ /** The stylesheet content the model does not interpret and re-emits byte for byte. */
49
+ export interface PreservedStyleTables {
50
+ readonly dxfs: readonly string[];
51
+ readonly indexedColors: readonly string[];
52
+ readonly mruColors: readonly string[];
53
+ readonly tableStyles: TableStyleTable;
36
54
  }
55
+ /** The preserved sub-tables of a stylesheet that carries none: a package with no `styles.xml`, and
56
+ * the BIFF12 stylesheet, whose binary records hold no verbatim XML there would be anything to keep. */
57
+ export declare const NO_PRESERVED_STYLE_TABLES: PreservedStyleTables;
58
+ /** The xf layer of a stylesheet: what {@link resolveStyleTable} assembles out of the format tables,
59
+ * before the verbatim sub-tables that ride alongside it in a {@link StyleTable}. */
60
+ export type ResolvedXfTables = Omit<StyleTable, 'preserved'>;
37
61
  /**
38
62
  * The format code a number-format id denotes: the file's own `<numFmt>`/`BrtFmt` declaration if it
39
63
  * has one, else the built-in Excel defines for that id. Id 0 is General, the absence of a format,
@@ -81,4 +105,4 @@ export declare function resolveStyleTable(tables: {
81
105
  readonly namedXfs: ReadonlyArray<XfStyle>;
82
106
  readonly labels: ReadonlyArray<StyleLabel>;
83
107
  readonly fonts: ReadonlyArray<Font | undefined>;
84
- }): StyleTable;
108
+ }): ResolvedXfTables;
@@ -1,6 +1,12 @@
1
1
  import { applyCellStyle } from '../../core/cell.js';
2
2
  import { NAMED_STYLE_ID } from '../../core/internal.js';
3
3
  import { assignStyleFacets, } from '../../core/style.js';
4
+ export const NO_PRESERVED_STYLE_TABLES = {
5
+ dxfs: [],
6
+ indexedColors: [],
7
+ mruColors: [],
8
+ tableStyles: { styles: [] },
9
+ };
4
10
  const BUILTIN_NUMFMTS = new Map([
5
11
  [1, '0'],
6
12
  [2, '0.00'],
@@ -1,4 +1,4 @@
1
- import { numFmtCodeFor, resolveStyleTable, } from '../style/xf-style.js';
1
+ import { numFmtCodeFor, NO_PRESERVED_STYLE_TABLES, resolveStyleTable, } from '../style/xf-style.js';
2
2
  import { RecordReader } from './primitives.js';
3
3
  import { readRecords } from './record-stream.js';
4
4
  import { BRT } from './record-types.js';
@@ -22,7 +22,7 @@ const COLLECTION_ENDS = new Set([
22
22
  ]);
23
23
  export function parseStyleTable(part) {
24
24
  if (part === undefined)
25
- return { cellXfs: [], namedStyles: [] };
25
+ return { cellXfs: [], namedStyles: [], preserved: NO_PRESERVED_STYLE_TABLES };
26
26
  const numFmtCodes = new Map();
27
27
  const fonts = [];
28
28
  const fills = [];
@@ -73,7 +73,10 @@ export function parseStyleTable(part) {
73
73
  break;
74
74
  }
75
75
  }
76
- return resolveStyleTable({ directXfs, namedXfs, labels, fonts });
76
+ return {
77
+ ...resolveStyleTable({ directXfs, namedXfs, labels, fonts }),
78
+ preserved: NO_PRESERVED_STYLE_TABLES,
79
+ };
77
80
  }
78
81
  function readXf(reader, deps, isDirect) {
79
82
  const parent = reader.u16();