@shbernal/ts-xlsx 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,29 +1,3 @@
1
- // The streaming `.xlsx` writer: author a workbook incrementally and emit its package through a Node
2
- // stream, rather than holding the finished bytes in one buffer as {@link writeXlsx} does.
3
- //
4
- // A producer adds worksheets, appends rows as it generates them, commits each sheet, then commits the
5
- // workbook — at which point the package is assembled and streamed out. The output goes to whichever
6
- // destination the caller chose at construction: its own pull-based `stream` to pipe, a caller-owned
7
- // `Writable` sink (an outbound upload), or a `filename` the writer opens. In every case `commit()`
8
- // settles — it resolves once a supplied sink has flushed the whole package and rejects if that sink
9
- // errors (an unopenable file), never hanging on a finish signal that will not come. The output rides a
10
- // genuinely
11
- // streamed zip container (fflate's `Zip`/`ZipDeflate`), which computes each entry's CRC-32
12
- // incrementally, so the archive is well-formed by construction — the defect the upstream "streaming
13
- // writer emits a corrupt zip" reports describe is structurally absent here. The bytes reload
14
- // identically to a whole-file write because both writers share `buildPackageParts` for every part.
15
- //
16
- // Peak memory: a row committed with `row.commit()` is serialised to its `<row>` XML immediately and
17
- // its cells evicted from the model, so an append-driven producer holds only the rows still in flight
18
- // rather than the whole sheet's cell graph. The eagerly-rendered rows intern into the workbook's live
19
- // style registry — the same one that emits `xl/styles.xml` — so their style ids stay correct; their
20
- // XML is handed to `buildPackageParts` and emitted at the head of `<sheetData>`. This eager path runs
21
- // with strings inline (a shared-strings pool is inherently whole-workbook, so it defeats bounding);
22
- // turning `useSharedStrings` on falls back to holding every row live until commit. A flushed row is a
23
- // finished row: it cannot join whole-sheet derivations, so a shared-formula clone in a committed row
24
- // is rejected, and rows reached only through `getCell` (never `row.commit()`) stay live and serialise
25
- // the ordinary way. The package bytes themselves are still assembled once at commit — a later slice
26
- // can flush each sheet's `<sheetData>` straight into its streamed zip entry to bound that half too.
27
1
  import { createWriteStream } from 'node:fs';
28
2
  import { PassThrough } from 'node:stream';
29
3
  import { Zip, ZipDeflate } from 'fflate';
@@ -33,12 +7,6 @@ import { isSharedFormulaValue } from '../../core/value.js';
33
7
  import { Workbook } from '../../core/workbook.js';
34
8
  import { AuthoringError } from '../../errors.js';
35
9
  import { buildColumnDefaults, buildPackageParts, createStyleRegistry, Extent, renderRow, } from './write.js';
36
- /**
37
- * A row appended to a {@link WorksheetStreamWriter}. Style its cells through {@link cells}, then call
38
- * {@link commit} to mark it finished. In an eager (inline-strings) writer, committing serialises the
39
- * row and frees its cells from the model, bounding peak memory; with `useSharedStrings` on it is a
40
- * no-op and the row stays live until the workbook commits.
41
- */
42
10
  export class StreamedRow {
43
11
  #cells;
44
12
  #sheet;
@@ -49,12 +17,9 @@ export class StreamedRow {
49
17
  this.#sheet = sheet;
50
18
  this.#number = number;
51
19
  }
52
- /** The cells this row materialised, for styling before it is committed. */
53
20
  get cells() {
54
21
  return this.#cells;
55
22
  }
56
- /** Finalise the row: an eager writer serialises it now and releases its cells; otherwise a no-op.
57
- * Committing twice is harmless — the second call does nothing rather than re-emitting the row. */
58
23
  commit() {
59
24
  if (this.#committed)
60
25
  return;
@@ -62,21 +27,12 @@ export class StreamedRow {
62
27
  this.#sheet?.flushRow(this.#number, this.#cells);
63
28
  }
64
29
  }
65
- /**
66
- * A worksheet being written incrementally. Append rows with {@link addRow}/{@link addRows}, style
67
- * cells through {@link getCell}, then {@link commit} to freeze it — after which any further mutation
68
- * is rejected with a legible error rather than silently accepted or crashing.
69
- */
70
30
  export class WorksheetStreamWriter {
71
31
  #sheet;
72
32
  #eager;
73
33
  #styles;
74
34
  #committed = false;
75
- // The last row number this writer appended, tracked independently of the model: an eager writer
76
- // evicts flushed rows, which lowers the model's used range, so leaning on it would reuse numbers.
77
35
  #lastRow = 0;
78
- // The column defaults an eagerly-rendered row inherits, frozen at the first flush so every flushed
79
- // row composes against the same columns even as later ones are defined.
80
36
  #columnDefaults;
81
37
  #flushedRows = [];
82
38
  #extent = new Extent();
@@ -86,16 +42,12 @@ export class WorksheetStreamWriter {
86
42
  this.#eager = eager;
87
43
  this.#styles = styles;
88
44
  }
89
- /** The sheet's name. */
90
45
  get name() {
91
46
  return this.#sheet.name;
92
47
  }
93
- /** The number of rows written so far — spans gaps and formatted-only rows, like the model, and
94
- * survives the eviction of eagerly-flushed rows. */
95
48
  get rowCount() {
96
49
  return Math.max(this.#lastRow, this.#sheet.rowCount);
97
50
  }
98
- /** Append one row of values after the last used row; the cells are returned for styling. */
99
51
  addRow(values) {
100
52
  this.#assertOpen();
101
53
  if (!this.#eager)
@@ -103,21 +55,16 @@ export class WorksheetStreamWriter {
103
55
  const number = this.#nextRowNumber();
104
56
  return new StreamedRow(this.#placeRow(number, values), this, number);
105
57
  }
106
- /** Append a batch of rows in one call, each landing directly below the previous. */
107
58
  addRows(rows) {
108
59
  this.#assertOpen();
109
60
  if (!this.#eager)
110
61
  return this.#sheet.addRows(rows).map((cells) => new StreamedRow(cells, null, 0));
111
62
  return rows.map((values) => this.addRow(values));
112
63
  }
113
- // The next append position: past both this writer's own high-water mark and any rows a `getCell`
114
- // materialised, so appends never collide with random-access edits or with already-evicted rows.
115
64
  #nextRowNumber() {
116
65
  this.#lastRow = Math.max(this.#lastRow, this.#sheet.rowCount) + 1;
117
66
  return this.#lastRow;
118
67
  }
119
- // Materialise a positional row at an explicit number (the model's own append would reuse numbers once
120
- // eviction shrinks its used range), returning the cells for styling. A hole leaves its cell absent.
121
68
  #placeRow(number, values) {
122
69
  const cells = [];
123
70
  values.forEach((value, index) => {
@@ -127,14 +74,6 @@ export class WorksheetStreamWriter {
127
74
  });
128
75
  return cells;
129
76
  }
130
- /**
131
- * Serialise an eagerly-committed row and release its cells from the model. Called by
132
- * {@link StreamedRow.commit}; the row's `<row>` XML is retained (interned into the workbook's live
133
- * style registry so its ids stay valid) and the cell graph is dropped, bounding peak memory.
134
- *
135
- * @throws {AuthoringError} if the row carries a shared-formula cell — a finished row cannot join the
136
- * whole-sheet formula planning, so shared formulas must be authored through {@link getCell}.
137
- */
138
77
  flushRow(number, cells) {
139
78
  for (const cell of cells) {
140
79
  if (isSharedFormulaValue(cell.value)) {
@@ -145,9 +84,6 @@ export class WorksheetStreamWriter {
145
84
  }
146
85
  this.#columnDefaults ??= buildColumnDefaults(this.#sheet);
147
86
  const properties = this.#sheet.getRow(number).properties;
148
- // The row's outline depth is read off here because eviction is about to take its properties with
149
- // it, and `<sheetFormatPr outlineLevelRow>` — written long after, once every row is flushed —
150
- // reports the deepest level on the whole sheet.
151
87
  this.#maxRowOutlineLevel = Math.max(this.#maxRowOutlineLevel, properties?.outlineLevel ?? 0);
152
88
  const { xml, minCol, maxCol } = renderRow({ number, cells, properties }, {
153
89
  columnDefaults: this.#columnDefaults,
@@ -162,7 +98,6 @@ export class WorksheetStreamWriter {
162
98
  }
163
99
  this.#sheet[INTERNAL].evictRow(number);
164
100
  }
165
- // The rows this writer flushed, or undefined if none — handed to buildPackageParts at commit.
166
101
  flushedSheet() {
167
102
  if (this.#flushedRows.length === 0)
168
103
  return undefined;
@@ -172,44 +107,22 @@ export class WorksheetStreamWriter {
172
107
  maxRowOutlineLevel: this.#maxRowOutlineLevel,
173
108
  };
174
109
  }
175
- /** Address a cell by its A1 reference to read or style it before the sheet is committed. */
176
110
  getCell(reference) {
177
111
  this.#assertOpen();
178
112
  return this.#sheet.getCell(reference);
179
113
  }
180
- /**
181
- * Attach a data validation to a range before the sheet is committed. Delegates to the model, so the
182
- * streamed package emits the `<dataValidations>` block in its CT_Worksheet position — before
183
- * `<hyperlinks>` — because both writers share one worksheet serializer.
184
- */
185
114
  addDataValidation(sqref, rule, options = {}) {
186
115
  this.#assertOpen();
187
116
  this.#sheet.addDataValidation(sqref, rule, options);
188
117
  }
189
- /**
190
- * Attach a conditional formatting to a range before the sheet is committed. Like every other block,
191
- * it lands in its schema-mandated slot — after `<mergeCells>`, before `<dataValidations>` and
192
- * `<hyperlinks>` — since the streamed sheet is serialized through the same path as a buffered write.
193
- */
194
118
  addConditionalFormatting(formatting) {
195
119
  this.#assertOpen();
196
120
  this.#sheet.addConditionalFormatting(formatting);
197
121
  }
198
- /**
199
- * Anchor a workbook image (the id from {@link WorkbookStreamWriter.addImage}) to this sheet,
200
- * spanning the rectangle from the top-left grid point `tl` to the bottom-right `br`. The streamed
201
- * package emits the drawing part, its media relationship, and the sheet's `<drawing>` reference
202
- * exactly as a buffered write does — both writers share `buildPackageParts`.
203
- */
204
122
  addImage(imageId, anchor) {
205
123
  this.#assertOpen();
206
124
  this.#sheet.addImage(imageId, anchor);
207
125
  }
208
- /**
209
- * Apply the sheet's autofilter before it is committed; mirrors {@link Worksheet.autoFilter}. The
210
- * streamed package emits `<autoFilter>` in its CT_Worksheet slot — after `<sheetProtection>` — and
211
- * contributes the hidden `_FilterDatabase` defined name, exactly as a buffered write does.
212
- */
213
126
  set autoFilter(filter) {
214
127
  this.#assertOpen();
215
128
  this.#sheet.autoFilter = filter;
@@ -217,24 +130,16 @@ export class WorksheetStreamWriter {
217
130
  get autoFilter() {
218
131
  return this.#sheet.autoFilter;
219
132
  }
220
- /**
221
- * Apply sheet-level protection before the sheet is committed; mirrors {@link Worksheet.protect}. The
222
- * shared serializer places `<sheetProtection>` ahead of `<autoFilter>` per CT_Worksheet, so a
223
- * streamed sheet carrying both stays valid rather than corrupt.
224
- */
225
133
  protect(password, options = {}) {
226
134
  this.#assertOpen();
227
135
  this.#sheet.protect(password, options);
228
136
  }
229
- /** Freeze the sheet: no more rows or edits may be added after this. */
230
137
  commit() {
231
138
  this.#committed = true;
232
139
  }
233
- /** Whether the sheet has been committed. */
234
140
  get committed() {
235
141
  return this.#committed;
236
142
  }
237
- // Internal: the underlying model sheet, so the workbook writer can serialise it at commit time.
238
143
  get model() {
239
144
  return this.#sheet;
240
145
  }
@@ -244,26 +149,15 @@ export class WorksheetStreamWriter {
244
149
  }
245
150
  }
246
151
  }
247
- /**
248
- * A workbook written incrementally to a Node stream. Add worksheets, append their rows, commit each
249
- * sheet, then {@link commit} the workbook to assemble and stream the package. The produced bytes are
250
- * available both as the resolved value of `commit()` and through {@link stream} (a Node `Readable`
251
- * that a caller can `pipe`).
252
- */
253
152
  export class WorkbookStreamWriter {
254
153
  #workbook = new Workbook();
255
154
  #sheets = [];
256
155
  #writeOptions;
257
156
  #sink;
258
- // The single style registry shared by the eager per-row flush and the commit-time serialisation, so a
259
- // flushed row's style ids match the styles.xml built from the same table.
260
157
  #styles;
261
- // Eager per-row flushing runs with strings inline; a shared-strings pool is inherently whole-workbook,
262
- // so it cannot bound memory — turning it on keeps every row live until commit.
263
158
  #eager;
264
159
  #stream;
265
160
  #committed = false;
266
- /** Calculation settings for the workbook; set `fullCalcOnLoad` before committing to emit it. */
267
161
  calcProperties = {};
268
162
  constructor(options = {}) {
269
163
  this.#writeOptions = { useSharedStrings: options.useSharedStrings ?? false };
@@ -275,32 +169,19 @@ export class WorkbookStreamWriter {
275
169
  this.#sink =
276
170
  options.stream ?? (options.filename ? createWriteStream(options.filename) : undefined);
277
171
  }
278
- /** Document-level metadata written to the package's core properties. */
279
172
  get properties() {
280
173
  return this.#workbook.properties;
281
174
  }
282
- /**
283
- * The output stream carrying the package bytes. A caller drives it with Node's standard idiom —
284
- * `writer.stream.pipe(out)` — which composes because `pipe` returns its destination. The stream is
285
- * created lazily on first access so a caller handing the writer its own sink is still free to
286
- * ignore this one.
287
- */
288
175
  get stream() {
289
176
  this.#stream ??= new PassThrough();
290
177
  return this.#stream;
291
178
  }
292
- /**
293
- * Register a picture's bytes on the workbook's shared media registry and return its id, to anchor
294
- * on any sheet with {@link WorksheetStreamWriter.addImage}. Mirrors {@link Workbook.addImage}: one
295
- * media part backs an image anchored on several sheets. Rejected once the workbook is committed.
296
- */
297
179
  addImage(options) {
298
180
  if (this.#committed) {
299
181
  throw new AuthoringError('the workbook is already committed — no more images can be registered');
300
182
  }
301
183
  return this.#workbook.addImage(options);
302
184
  }
303
- /** Create a worksheet and append it to the workbook. */
304
185
  addWorksheet(name, options = {}) {
305
186
  if (this.#committed) {
306
187
  throw new AuthoringError('the workbook is already committed — no more worksheets can be added');
@@ -309,11 +190,6 @@ export class WorkbookStreamWriter {
309
190
  this.#sheets.push(sheet);
310
191
  return sheet;
311
192
  }
312
- /**
313
- * Assemble the workbook into its package, stream the bytes through {@link stream}, and resolve with
314
- * the same bytes. Every sheet is frozen first, so a row added after this rejects legibly. Idempotent
315
- * only in that a second call throws rather than re-emitting.
316
- */
317
193
  async commit() {
318
194
  if (this.#committed) {
319
195
  throw new AuthoringError('the workbook is already committed');
@@ -323,9 +199,6 @@ export class WorkbookStreamWriter {
323
199
  sheet.commit();
324
200
  if (this.calcProperties.fullCalcOnLoad)
325
201
  this.#workbook.fullCalcOnLoad = true;
326
- // Hand every sheet's eagerly-flushed rows to the shared serialiser, which emits them alongside the
327
- // rows still live in the model. The style registry is the same one the flushed rows interned into,
328
- // so styles.xml stays consistent with the ids already baked into their XML.
329
202
  const flushed = new Map();
330
203
  for (const sheet of this.#sheets) {
331
204
  const sheetFlushed = sheet.flushedSheet();
@@ -339,9 +212,6 @@ export class WorkbookStreamWriter {
339
212
  });
340
213
  const owned = this.#stream;
341
214
  const sink = this.#sink;
342
- // Track the caller sink's terminal state before writing a byte, so an open failure that errors on a
343
- // later tick (a bad filename) is caught rather than lost — the whole point of the reject-not-hang
344
- // contract.
345
215
  const sinkSettled = sink ? settleOnFinish(sink) : undefined;
346
216
  const bytes = await streamZipPackage(parts, (chunk) => {
347
217
  owned?.write(chunk);
@@ -353,19 +223,12 @@ export class WorkbookStreamWriter {
353
223
  return bytes;
354
224
  }
355
225
  }
356
- // Resolve when a caller-supplied sink has flushed the whole package (`finish`), or reject if it errors
357
- // (`error`) — the commit promise must settle either way, never hang. Whichever fires first wins; the
358
- // other is ignored.
359
226
  function settleOnFinish(sink) {
360
227
  return new Promise((resolve, reject) => {
361
228
  sink.once('finish', resolve);
362
229
  sink.once('error', reject);
363
230
  });
364
231
  }
365
- // Zip the package parts through fflate's streaming container, forwarding each output chunk to `onChunk`
366
- // as it is produced and resolving with the whole archive once the final chunk arrives. `ZipDeflate`
367
- // deflates synchronously, so the callback fires inline as each part is pushed — the CRC-32 fflate
368
- // stamps into every entry's header therefore always matches the bytes it just compressed.
369
232
  function streamZipPackage(parts, onChunk) {
370
233
  return new Promise((resolve, reject) => {
371
234
  const collected = [];
@@ -1,18 +1,3 @@
1
- // The buffered `.xlsx` writer: a Workbook model in, an OPC zip package out.
2
- //
3
- // It serialises the part of the model that exists today — worksheets; cells holding a
4
- // number, string, boolean, or formula; column/row formatting; page margins and
5
- // header/footer; merged ranges; and worksheet tables — into a valid package (content
6
- // types, relationships, workbook, per-sheet XML, table parts, the default theme and
7
- // stylesheet, and core/app properties). Styles, images, and the richer value kinds land
8
- // as the model grows; until then the writer refuses a value it cannot represent
9
- // faithfully rather than emitting a lossy or corrupt package.
10
- //
11
- // This module is the orchestrator: it plans the package graph (via `package-plan.ts`) and
12
- // stitches the serialised parts (from `workbook-xml.ts` and `worksheet-xml.ts`) into the
13
- // part map. The row/cell renderer and the sheet's public render types live in
14
- // `worksheet-xml.ts` and are re-exported here so the streaming writer's import surface is
15
- // unchanged.
16
1
  import { strToU8, zip, zipSync } from 'fflate';
17
2
  import { DEFAULT_THEME_XML } from '../../core/theme.js';
18
3
  import { AuthoringError } from '../../errors.js';
@@ -31,35 +16,9 @@ import { personsXml, threadedCommentsXml } from './threaded-comments.js';
31
16
  import { appPropsXml, contentTypesXml, corePropsXml, FIXED_WORKBOOK_REL_COUNT, rootRelsXml, workbookRelsXml, workbookXml, } from './workbook-xml.js';
32
17
  import { worksheetRelsXml, worksheetXml, } from './worksheet-xml.js';
33
18
  export { buildColumnDefaults, Extent, renderRow, } from './worksheet-xml.js';
34
- /**
35
- * Serialise a workbook into an `.xlsx` package.
36
- *
37
- * @throws {AuthoringError} if the workbook has no worksheets (a zero-sheet package is corrupt),
38
- * or holds a value the writer cannot yet represent.
39
- */
40
19
  export function writeXlsx(workbook, options = {}) {
41
20
  return zipSync(buildPackageParts(workbook, options), { level: 6 });
42
21
  }
43
- /**
44
- * Serialise a workbook into an `.xlsx` package, deflating off the calling thread.
45
- *
46
- * Produces the same package {@link writeXlsx} does — every part compresses to identical bytes — and
47
- * exists for one reason: DEFLATE dominates the cost of writing a large workbook, and {@link writeXlsx}
48
- * spends all of it on the caller's thread. Here `fflate` deflates each part in a worker, so the event
49
- * loop keeps turning (stalls drop from the whole write to tens of milliseconds) and parts compress in
50
- * parallel, which on a multi-sheet workbook also finishes sooner. On a single-sheet workbook there is
51
- * only one part to deflate, so expect responsiveness rather than speed.
52
- *
53
- * Building the parts still happens on the calling thread — only compression moves. That is why there
54
- * is no `readXlsxAsync` mirroring this: reading is dominated by XML parsing and model building, which
55
- * no worker can take, and the reader's zip-bomb ceiling is enforced by counting output between
56
- * synchronous input slices. See ADR-0024.
57
- *
58
- * @throws {AuthoringError} — as a rejection — under the same conditions as {@link writeXlsx};
59
- * the part-building it shares happens before any worker is involved. A failure raised by the zip
60
- * layer itself (including an environment that cannot spawn a worker) propagates unwrapped, exactly
61
- * as it does from {@link writeXlsx}.
62
- */
63
22
  export async function writeXlsxAsync(workbook, options = {}) {
64
23
  const parts = buildPackageParts(workbook, options);
65
24
  return await new Promise((resolve, reject) => {
@@ -71,47 +30,22 @@ export async function writeXlsxAsync(workbook, options = {}) {
71
30
  });
72
31
  });
73
32
  }
74
- /**
75
- * A style registry seeded from a workbook's read-in style layers (differential styles, named cell
76
- * styles, custom indexed palette), ready to intern authored styles after them. Both the buffered
77
- * pass and the streaming writer build their registry through here so a cell's style id means the
78
- * same thing whichever writer emits it.
79
- */
80
33
  export function createStyleRegistry(workbook) {
81
- // Font id 0 is the workbook's own default face, resolved from what it declared, what was authored,
82
- // and its theme's body typeface — never an assumed Calibri, which would re-face every empty cell
83
- // and change the metric every character-unit column width is expressed in.
84
34
  const styles = new StyleRegistry({
85
35
  defaultFont: workbook.defaultFont,
86
36
  ...(workbook.declaredDefaultFont === undefined
87
37
  ? {}
88
38
  : { declaredDefaultFont: workbook.declaredDefaultFont }),
89
39
  });
90
- // Seed the differential-style table with the fragments read from a source file so conditional
91
- // formatting's dxfId references stay valid; styles authored on rules append after them.
92
40
  styles.seedDifferentialStyles(workbook.differentialStyles);
93
- // Seed the named cell-style layer (cellStyleXfs/cellStyles) so each style's facets re-intern into the
94
- // rebuilt sub-tables and a cell's xfId link stays valid; without any, the default Normal alone emits.
95
41
  styles.seedNamedStyles(workbook.namedStyles);
96
- // Seed the custom indexed-color palette so it re-emits verbatim and an `indexed="…"` colour keeps
97
- // its intended RGB; a workbook that never overrode the palette seeds nothing and writes no <colors>.
98
42
  styles.seedIndexedColors(workbook.indexedColors);
99
- // Seed the author's "Recent Colors" swatches so they re-emit unchanged rather than being reset.
100
43
  styles.seedMruColors(workbook.mruColors);
101
- // Seed the custom table-style definitions so a table's `styleName` still names a real definition and
102
- // each element's dxfId still indexes the differential-style table seeded above at its original index.
103
44
  styles.seedTableStyles(workbook.tableStyles);
104
- // Authored styles append after the preserved ones, and intern their elements' formatting after the
105
- // seeded dxfs — the ordering that keeps every preserved dxfId pointing where it did.
106
45
  for (const style of workbook.customTableStyles)
107
46
  styles.addTableStyle(style);
108
47
  return styles;
109
48
  }
110
- // Resolve one sheet's tail reference ids (the `<drawing>`/`<legacyDrawing>`/`<legacyDrawingHF>`/
111
- // `<picture>` slots and the slicer list) from its plan. A preserved `<drawing>` and a modeled one are
112
- // mutually exclusive, so the drawing slot takes whichever exists; a comment's VML rides the legacy-
113
- // drawing slot; and each preserved slicer surfaces its rel id so the `<x14:slicerList>` can reactivate
114
- // the widget rather than orphan its part.
115
49
  function resolveSheetReferences(plan) {
116
50
  const refs = plan.preservedRefs;
117
51
  const preservedDrawingRelId = refs.find((ref) => ref.element === 'drawing')?.relId ?? null;
@@ -128,39 +62,15 @@ function resolveSheetReferences(plan) {
128
62
  slicerRelIds,
129
63
  };
130
64
  }
131
- /**
132
- * Assemble a workbook into the map of OPC package parts (part name → bytes) that make up an `.xlsx`,
133
- * short of zipping them. This is the whole serialisation — content types, relationships, workbook,
134
- * per-sheet XML, styles, theme, media, tables, and props — factored out of {@link writeXlsx} so the
135
- * streaming writer can drive the identical parts through a streamed zip container rather than
136
- * `zipSync`. Neither writer duplicates a byte of serialisation.
137
- *
138
- * @throws {AuthoringError} if the workbook has no worksheets, or holds a value the writer cannot represent.
139
- */
140
65
  export function buildPackageParts(workbook, options = {}) {
141
66
  const sheets = workbook.worksheets;
142
67
  if (sheets.length === 0) {
143
68
  throw new AuthoringError('cannot write a workbook with no worksheets — a zero-sheet package is corrupt to Excel');
144
69
  }
145
- // With the option on, plain string cell values are pooled into a shared-strings table interned
146
- // during the sheet pass (like the style registry); a null table keeps every string inline.
147
70
  const sharedStrings = options.useSharedStrings ? new SharedStringTable() : null;
148
- // Anchored images share workbook-wide media: every image a sheet references becomes one media part,
149
- // addressed by a global number. Resolved before the sheet loop so a drawing's embeds can target it.
150
71
  const media = planMedia(workbook, sheets);
151
- // Content the model does not interpret — a vector-shape drawing, a header/footer image, a pivot
152
- // table and its caches, a slicer — captured on read and re-emitted verbatim onto collision-proof
153
- // paths. Preserved parts are renumbered past the parts the writer generates of the same kind
154
- // (drawings, VML, media), so resolving them needs only those generated counts; each sheet's
155
- // preserved references take their sheet-local rel ids in canonical position in the loop below.
156
72
  const generatedDrawingCount = sheets.filter((sheet) => sheet.images.length > 0).length;
157
73
  const preserved = planPreservedParts(workbook, generatedDrawingCount, media.parts.length);
158
- // Plan every sheet's parts in a single pass, drawing each sheet-local relationship id from that
159
- // sheet's allocator in canonical order: tables, drawing, comments (VML + comments part), printer
160
- // settings, external hyperlinks, background, preserved references, pivot tables. One running
161
- // allocator per sheet is what keeps the ids gapless and collision-free — no step re-derives its
162
- // offset by summing the ones before it, so none can drift into another's id. Part numbers (tables,
163
- // drawings, pivots) are global across the workbook and counted here in the same pass.
164
74
  let tableNumber = 0;
165
75
  let drawingNumber = 0;
166
76
  let pivotNumber = 0;
@@ -177,8 +87,6 @@ export function buildPackageParts(workbook, options = {}) {
177
87
  const registered = workbook.getImage(image.imageId);
178
88
  return {
179
89
  anchor: image.anchor,
180
- // The embed id is local to the drawing part's own rels, not the sheet's, so it is numbered
181
- // per image from rId1 rather than drawn from the sheet allocator.
182
90
  embedId: `rId${j + 1}`,
183
91
  mediaNumber: media.numberById.get(image.imageId),
184
92
  extension: registered.extension,
@@ -186,12 +94,6 @@ export function buildPackageParts(workbook, options = {}) {
186
94
  });
187
95
  drawing = { number: ++drawingNumber, relId: rels.next(), images };
188
96
  }
189
- // A conversation and the legacy fallback `<comment>` that binds its cell to it are two halves of one
190
- // representation, so both are derived from this single list and neither can be emitted without the
191
- // other. Verified against desktop Excel: a `tc=` fallback whose thread part is absent shows as neither
192
- // a thread nor a note — the text disappears rather than degrading — and a thread part whose fallback is
193
- // absent is ignored, leaving the cell blank. A thread with no messages is not one of them: it has
194
- // nothing to say, and no head id for its replies or its fallback to hang off.
195
97
  const threads = sheet.commentThreads.filter((thread) => thread.comments.length > 0);
196
98
  const sheetComments = collectComments(sheet, threads);
197
99
  const comments = sheetComments.length === 0
@@ -221,8 +123,6 @@ export function buildPackageParts(workbook, options = {}) {
221
123
  const preservedRefs = (preserved.perSheet[i] ?? []).map((reference) => ({ ...reference, relId: rels.next() }));
222
124
  const pivots = sheet.pivotTables.map((table) => {
223
125
  const number = ++pivotNumber;
224
- // Each pivot is numbered globally (its parts and its `cacheId` must be workbook-unique); the
225
- // workbook relationship reaching its cache is assigned once the modeled workbook rels are known.
226
126
  return { number, cacheId: String(number), table, sheetRelId: rels.next(), workbookRelId: '' };
227
127
  });
228
128
  return {
@@ -239,22 +139,11 @@ export function buildPackageParts(workbook, options = {}) {
239
139
  });
240
140
  const allTables = perSheet.flatMap((plan) => plan.tables);
241
141
  const allPivots = perSheet.flatMap((plan) => plan.pivots);
242
- // Serialise the worksheets first: interning each cell/row fill into the style table is a
243
- // side effect of that pass, so styles.xml can only be generated once every sheet is done. The
244
- // streaming writer supplies its own registry (already seeded, and already carrying its eagerly
245
- // flushed rows' styles); the buffered path seeds a fresh one here.
246
142
  const styles = options.styles ?? createStyleRegistry(workbook);
247
143
  const sheetXml = sheets.map((sheet, i) => {
248
144
  const plan = perSheet[i];
249
- return worksheetXml(sheet, plan.tables, styles, resolveSheetReferences(plan), plan.hyperlinks, sharedStrings,
250
- // Exactly one sheet is marked selected; the model resolves which, so no package can ship with
251
- // none selected (no view initialised on open) or with several (an accidental group selection,
252
- // where an edit to one sheet lands on all of them).
253
- i === workbook.activeTabIndex, options.flushed?.get(sheet));
145
+ return worksheetXml(sheet, plan.tables, styles, resolveSheetReferences(plan), plan.hyperlinks, sharedStrings, i === workbook.activeTabIndex, options.flushed?.get(sheet));
254
146
  });
255
- // The pool is filled only once every sheet is serialised. Emit the part (and its rel + content
256
- // type) solely when the option is on and at least one string was interned, so a workbook with no
257
- // string cells never fabricates an empty table.
258
147
  const hasSharedStrings = sharedStrings !== null && !sharedStrings.isEmpty;
259
148
  const commentNumbers = perSheet
260
149
  .map((plan) => plan.comments)
@@ -272,14 +161,7 @@ export function buildPackageParts(workbook, options = {}) {
272
161
  .map((plan) => plan.threadedComments)
273
162
  .filter((t) => t !== null)
274
163
  .map((t) => t.number);
275
- // The identity registry is emitted only beside the thread parts that point into it. With no conversation
276
- // in the package nothing can reference a `<person>`, so the part would be a workbook-level relationship
277
- // to dead weight — and it is the messages, not the registry, that make an identity worth carrying.
278
164
  const persons = threadedCommentNumbers.length === 0 ? [] : workbook.persons;
279
- // A preserved workbook reference's relationship id follows the modeled workbook rels — the sheets,
280
- // styles, theme, and (when emitted) shared strings and the threaded-comment person registry — so adding
281
- // one never renumbers an id already used. The workbook body and its rels part are wired from the same
282
- // assignment, so a pivot cache's `<pivotCaches>` registration and its relationship agree on the id.
283
165
  const modeledWorkbookRelCount = sheets.length + FIXED_WORKBOOK_REL_COUNT + (hasSharedStrings ? 1 : 0);
284
166
  const personsRelId = persons.length === 0 ? null : `rId${modeledWorkbookRelCount + 1}`;
285
167
  const workbookRelBase = modeledWorkbookRelCount + (personsRelId === null ? 0 : 1);
@@ -287,8 +169,6 @@ export function buildPackageParts(workbook, options = {}) {
287
169
  ...ref,
288
170
  relId: `rId${workbookRelBase + 1 + i}`,
289
171
  }));
290
- // A generated pivot cache's workbook relationship follows the preserved ones; the assignment
291
- // mutates the shared plan so the `<pivotCaches>` body and the rels part read the same id.
292
172
  const pivotWorkbookRelBase = workbookRelBase + preserved.workbook.length;
293
173
  allPivots.forEach((pivot, i) => {
294
174
  pivot.workbookRelId = `rId${pivotWorkbookRelBase + 1 + i}`;
@@ -302,17 +182,12 @@ export function buildPackageParts(workbook, options = {}) {
302
182
  'xl/_rels/workbook.xml.rels': strToU8(workbookRelsXml(sheets.length, hasSharedStrings, personsRelId, preservedWorkbookRels, allPivots)),
303
183
  'xl/styles.xml': strToU8(styles.toXml()),
304
184
  };
305
- // A theme read from a source package is emitted through the preserved-part path, closure and all —
306
- // with any authored overrides already spliced into its entry part by the planner. A workbook without
307
- // one gets its authored theme, or the library's default, which the stylesheet's `theme="1"` default
308
- // font still needs something to resolve against.
309
185
  if (!preserved.themeEmitted) {
310
186
  files[THEME_PART_PATH] = strToU8(workbook.authoredThemeXml() ?? DEFAULT_THEME_XML);
311
187
  }
312
188
  if (hasSharedStrings) {
313
189
  files['xl/sharedStrings.xml'] = strToU8(sharedStrings.toXml());
314
190
  }
315
- // Singular and unnumbered, unlike the per-sheet thread parts: one registry serves the whole workbook.
316
191
  if (persons.length > 0)
317
192
  files['xl/persons/person.xml'] = strToU8(personsXml(persons));
318
193
  for (const part of media.parts) {
@@ -326,9 +201,6 @@ export function buildPackageParts(workbook, options = {}) {
326
201
  emitPreservedParts(files, preserved.parts);
327
202
  return files;
328
203
  }
329
- // Emit each sheet's own parts: the sheet XML, its rels part (only when the sheet references something),
330
- // and the drawing/comment/printer-settings parts those relationships point at. `sheetXml[i]` is the
331
- // already-serialised body for `perSheet[i]`, indexed in lockstep.
332
204
  function emitSheetParts(files, perSheet, sheetXml) {
333
205
  perSheet.forEach((plan, i) => {
334
206
  const { tables, drawing, comments, threadedComments, printerSettings, background, hyperlinks, preservedRefs, pivots, } = plan;
@@ -363,9 +235,6 @@ function emitSheetParts(files, perSheet, sheetXml) {
363
235
  }
364
236
  });
365
237
  }
366
- // Emit every pivot table's three chained parts. A pivot spans a pivot-table part (linked from its host
367
- // sheet) that references a cache definition, which references its cache records. Each cache carries a
368
- // rels part naming the next link by `rId1` — the id the definition/table XML resolves against.
369
238
  function emitPivotParts(files, allPivots) {
370
239
  for (const pivot of allPivots) {
371
240
  const { number, cacheId, table } = pivot;
@@ -384,8 +253,6 @@ function emitPivotParts(files, allPivots) {
384
253
  files[`xl/pivotCache/pivotCacheRecords${number}.xml`] = strToU8(pivotCacheRecordsXml(table));
385
254
  }
386
255
  }
387
- // Emit the verbatim-preserved parts (and their rewired rels) last: their paths are collision-proof, so
388
- // ordering against the generated parts does not matter.
389
256
  function emitPreservedParts(files, parts) {
390
257
  for (const part of parts) {
391
258
  files[part.path] = part.bytes;
@@ -1,12 +1,4 @@
1
1
  import { X14_NS } from './namespaces.js';
2
- /**
3
- * Wrap an x14 extension body in the `<ext>` envelope every worksheet- and workbook-level x14
4
- * extension shares: a `uri` scoping the extension to its feature (a fixed GUID from
5
- * {@link namespaces.ts}) plus the `xmlns:x14` binding the extension namespace on the element itself —
6
- * Excel declares it per `<ext>`, never at the part root. Callers needing an `<extLst>` wrapper add it
7
- * around the result; the producers the worksheet serialiser gathers return this bare so several exts
8
- * compose under a single shared `<extLst>`.
9
- */
10
2
  export function x14Ext(uri, body) {
11
3
  return `<ext uri="${uri}" xmlns:x14="${X14_NS}">${body}</ext>`;
12
4
  }