@justybase/spreadsheet-tasks 2.2.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.
package/CHANGELOG.md CHANGED
@@ -7,8 +7,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.1.0] - 2026-10-24
11
+
12
+ ### Changed
13
+
14
+ - XLSX output omits the `r` attribute on cells that directly follow the previous
15
+ cell (`<c><v>1</v></c>`). A cell keeps `r` after a gap, at the start of a row that
16
+ does not begin in column A, and in the header row only when needed. Excel and the
17
+ readers in this package resolve the position from the order of cells.
18
+ - XLSX and XLSB writers accept `{ compressionLevel }` (integer 0–9, default 6). The
19
+ archiver default was level 1, which compresses less.
20
+
21
+ ### Fixed
22
+
23
+ - Raw CRLF and lone CR in XML text (shared strings, inline strings, `str` cells) are
24
+ normalized to LF on read, as XML 1.0 requires. `&#13;` still reads as CR.
25
+ - `escapeXmlText` writes CR as `&#13;`, so a CR written by the library survives a
26
+ round trip instead of being normalized away. The XLSX writer now does the same in
27
+ shared strings, where a raw CR was previously written and read back as LF by Excel.
28
+ - Text containing `_xHHHH_` is escaped as `_x005F_xHHHH_` when written, and
29
+ `_xHHHH_` written by Excel (for example `_x000D_`) is decoded on read (ECMA-376
30
+ ST_Xstring). Attribute values are not affected.
31
+ - `XlsxUpdater` infers the column of a cell without `r` when it reads existing styles,
32
+ so style inheritance works for files produced by this library and by xlspy.
33
+
34
+ ## [3.0.0] - 2026-10-23
35
+
36
+ ### Breaking
37
+
38
+ - `XlsxReader` / `XlsbReader` now return an empty row (`[]`, `getValue()` → `null`)
39
+ for every missing row position, including leading gaps, based on the row `r`
40
+ attribute (XLSX) and `BrtRowHdr` (XLSB). Like xlspy, gap positions are yielded;
41
+ unlike xlspy's padded `[None] * cols`, this reader yields a zero-length row.
42
+ Code that counts rows must account for gap rows.
43
+
44
+ ### Documentation
45
+
46
+ - Date conversion is UTC-based on both write and read: a `Date` is stored by its
47
+ UTC instant (`getTime()`), so `new Date(2024, 5, 30)` in UTC+2 appears as
48
+ 2024-06-29 22:00 in Excel. Use `Date.UTC(...)` for date-only values.
49
+
10
50
  ### Added
11
51
 
52
+ - `selectSheet(nameOrIndex)` and `nextResult()` on `ExcelReaderAbstract`,
53
+ implemented by `XlsxReader` and `XlsbReader`. Default remains sheet 0; state
54
+ (row/EOF, `fieldCount`) resets on selection.
55
+ - Excel limits (`MAX_ROWS=1_048_576`, `MAX_COLS=16_384`, `MAX_CELL_CHARS=32_767`
56
+ UTF-16 units) enforced in `XlsxWriter`, `XlsbWriter`, `BrowserXlsxWriter` and
57
+ `BrowserXlsbWriter`. Overlong text, oversized dimensions and rows/headers
58
+ longer than `columnCount` throw instead of writing corrupt files (xlspy parity).
59
+ - Shared XML helpers: `parseTagAttributes()`, `resolveRelTarget()` and
60
+ single-pass `unescapeXml()` in `src/xmlUtils.ts`, exported from the package
61
+ entry point.
12
62
  - `convertXlsbToXlsx` and `convertXlsxToXlsb` — Windows/Excel COM conversions
13
63
  with protected destinations and optional explicit overwriting.
14
64
  - `replaceSheetDataStream()` and `saveStreaming()` for XLSX/XLSB updaters,
@@ -19,6 +69,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
69
 
20
70
  ### Fixed
21
71
 
72
+ - `BrowserXlsxWriter` / `BrowserXlsbWriter` skip a `FormattedCell` whose value is
73
+ `null` or `undefined`, matching the Node writers (previously a `TypeError`).
74
+ - Browser sources in `src/browser/` are type-checked under `strict` by
75
+ `tsconfig.browser.json`, which `npm run typecheck` runs. The `@ts-nocheck` pragmas
76
+ were removed.
77
+ - Column letters ≥702 (`AAA`…`XFD`): `XlsxWriter` and `BrowserXlsxWriter` now
78
+ delegate to `columnIndexToLetter()` instead of falling back to `'A'`.
79
+ - XLSB shared-string table: `BrtBeginSst` now writes `total` then `unique`
80
+ (was swapped); `BiffReaderWriter` and updaters already expected that order.
81
+ - Writer finalization is atomic: output goes to a same-directory temp file,
82
+ output/archive errors reject the promise, streams are destroyed and the temp
83
+ file removed; only a complete workbook is renamed into place (xlspy parity).
84
+ `writeAdmZipStreaming()` now observes output errors before/during the append loop.
85
+ - Workbook relationships and sheet paths: absolute `Target="/xl/…"` resolved
86
+ correctly, attribute order no longer matters (`parseTagAttributes` used in
87
+ both readers and both updaters). Verified against openpyxl output.
88
+ - `unescapeXml()` single-pass: `&amp;lt;` stays `&lt;` instead of becoming `<`.
89
+ - Shared strings: `<rPh>` phonetics skipped, `<si/>` yields `""`, rich-text
90
+ `inlineStr` concatenates all `<t>` runs.
91
+ - XLSX cell types: `t="str"` → text, `t="e"` → error text, `t="d"` → `Date`.
92
+ - Self-closing `<row …/>` no longer swallows the next row.
93
+ - 1904 date system: `workbookPr date1904` (XLSX) and `BrtWbProp` (XLSB) set the
94
+ reader/updater epoch (`1904-01-01` vs `1899-12-30`).
95
+ - XLSB dimension: updaters patch `BrtWsDim` (`0x94`, `rwLast@4`/`colLast@12`)
96
+ instead of `BrtSel` (`0x98`).
97
+ - Stale parts after `replaceSheetData`: `xl/calcChain.xml` (`.bin` for XLSB)
98
+ removed with its `Content_Types`/rels entries; XLSX `xl/tables/*.xml` refs
99
+ (`<table>` + `<autoFilter>`) updated to the new dimension when the table's
100
+ width is unchanged, without expanding over other tables. XLSB tables are not
101
+ patched (documented limitation).
22
102
  - Destination replacement during Excel conversion now preserves files created
23
103
  concurrently and rolls back safely when the final rename fails.
24
104
  - Shared-string `count` values now reflect the replaced worksheet rather than
@@ -26,6 +106,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
26
106
  committed only once.
27
107
  - Updater saves now install the destination atomically through a same-directory
28
108
  temporary file.
109
+ - 1900 date system leap-day gap: Excel counts a fictitious 1900-02-29, so
110
+ serials 1-59 denote 1900-01-01..1900-02-28. `oaDateToTime()` /
111
+ `timeToOaDate()` in `writerHelpers.ts` apply the one-day shift (both
112
+ directions, all readers/writers/updaters incl. browser builds); the 1904
113
+ system is unaffected. Previously serial 1 read as 1899-12-31, disagreeing
114
+ with Excel, xlspy and openpyxl.
115
+ - `XlsbUpdater` date fallback now recognises custom `BrtFmt` date formats
116
+ (e.g. xlspy's `yyyy-mm-dd`) via `BiffReaderWriter.readStyles()` instead of
117
+ only built-in numFmt ranges, so replacement `Date` cells keep a date style
118
+ instead of degrading to raw serials.
29
119
 
30
120
  ## [2.1.0] - 2026-08-07
31
121
 
package/README.md CHANGED
@@ -179,6 +179,17 @@ await writer.finalize();
179
179
  - Generate data on-the-fly from databases, APIs, or other sources
180
180
  - Same performance as batch mode
181
181
 
182
+ ### Compression Level
183
+
184
+ XLSX and XLSB writers compress the package with zlib. The default is level 6; pass a
185
+ level from 0 (store) to 9 (smallest, slowest) to trade size for speed:
186
+
187
+ ```typescript
188
+ const writer = new XlsxWriter('fast.xlsx', { compressionLevel: 1 });
189
+ ```
190
+
191
+ Out-of-range values throw a `RangeError`.
192
+
182
193
  ### Reading Excel Files
183
194
 
184
195
  ```typescript
@@ -181,6 +181,10 @@ var BrowserBigBuffer = class {
181
181
  var BrowserZip = class _BrowserZip {
182
182
  constructor() {
183
183
  this.files = [];
184
+ this.files = [];
185
+ }
186
+ static {
187
+ this._crc32Table = null;
184
188
  }
185
189
  /**
186
190
  * Add a file entry (string or Uint8Array).
@@ -298,7 +302,6 @@ var BrowserZip = class _BrowserZip {
298
302
  return (crc ^ 4294967295) >>> 0;
299
303
  }
300
304
  };
301
- BrowserZip._crc32Table = null;
302
305
 
303
306
  // src/Formats.ts
304
307
  var F = {
@@ -343,15 +346,64 @@ function getFormat(val) {
343
346
  return null;
344
347
  }
345
348
 
349
+ // src/xmlUtils.ts
350
+ function columnIndexToLetter(colIndex) {
351
+ let n = colIndex + 1;
352
+ let result = "";
353
+ while (n > 0) {
354
+ const rem = (n - 1) % 26;
355
+ result = String.fromCharCode(65 + rem) + result;
356
+ n = Math.floor((n - 1) / 26);
357
+ }
358
+ return result;
359
+ }
360
+
361
+ // src/writerHelpers.ts
362
+ var OA_EPOCH_1900 = Date.UTC(1899, 11, 30);
363
+ var MARCH_1_1900 = Date.UTC(1900, 2, 1);
364
+ function timeToOaDate(timeMs, epochMs) {
365
+ let serial = (timeMs - epochMs) / 864e5;
366
+ if (epochMs === OA_EPOCH_1900 && timeMs < MARCH_1_1900) {
367
+ serial -= 1;
368
+ }
369
+ return serial;
370
+ }
371
+ var MAX_ROWS = 1048576;
372
+ var MAX_COLS = 16384;
373
+ var MAX_CELL_CHARS = 32767;
374
+ function assertCellTextLength(text) {
375
+ if (text.length > MAX_CELL_CHARS) {
376
+ throw new Error(
377
+ `Excel cell text exceeds ${MAX_CELL_CHARS} UTF-16 code units (got ${text.length}).`
378
+ );
379
+ }
380
+ }
381
+ function assertColumnCount(columnCount) {
382
+ if (!Number.isInteger(columnCount) || columnCount < 1 || columnCount > MAX_COLS) {
383
+ throw new Error(
384
+ `Excel column count must be 1..${MAX_COLS} (got ${columnCount}).`
385
+ );
386
+ }
387
+ }
388
+ function assertRowFitsColumns(kind, length, columnCount) {
389
+ if (length > columnCount) {
390
+ throw new Error(
391
+ `Excel ${kind} length ${length} exceeds columnCount ${columnCount}.`
392
+ );
393
+ }
394
+ }
395
+ function assertRowCount(totalRows) {
396
+ if (totalRows > MAX_ROWS) {
397
+ throw new Error(
398
+ `Excel row count exceeds ${MAX_ROWS} (got ${totalRows}).`
399
+ );
400
+ }
401
+ }
402
+ function cellRefAttr(col, prevCol, colRef, rowNum) {
403
+ return col === prevCol + 1 ? "" : ` r="${colRef}${rowNum}"`;
404
+ }
405
+
346
406
  // src/browser/BrowserXlsxWriter.ts
347
- var COL_LETTERS = (() => {
348
- const l = [];
349
- for (let i = 65; i < 91; i++) l.push(String.fromCharCode(i));
350
- const t = [];
351
- for (const p of l) for (let i = 65; i < 91; i++) t.push(p + String.fromCharCode(i));
352
- l.push(...t);
353
- return l;
354
- })();
355
407
  var BrowserXlsxWriter = class {
356
408
  constructor() {
357
409
  this._zip = new BrowserZip();
@@ -376,7 +428,7 @@ var BrowserXlsxWriter = class {
376
428
  this._nextXf = 6;
377
429
  }
378
430
  _cl(i) {
379
- return i < COL_LETTERS.length ? COL_LETTERS[i] : "A";
431
+ return columnIndexToLetter(i);
380
432
  }
381
433
  _san(n) {
382
434
  if (!n || typeof n !== "string") return `Sheet${this._sc + 1}`;
@@ -389,17 +441,17 @@ var BrowserXlsxWriter = class {
389
441
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
390
442
  }
391
443
  _esc(s) {
392
- if (typeof s !== "string") return s;
444
+ s = s.replace(/_x([0-9A-Fa-f]{4})_/g, "_x005F_x$1_");
393
445
  let need = false;
394
446
  for (let i = 0; i < s.length; i++) {
395
447
  const c = s.charCodeAt(i);
396
- if (c === 38 || c === 60 || c === 62 || c === 34 || c === 39 || c >= 0 && c <= 8 || c === 11 || c === 12 || c >= 14 && c <= 31) {
448
+ if (c === 38 || c === 60 || c === 62 || c === 34 || c === 39 || c === 13 || c >= 0 && c <= 8 || c === 11 || c === 12 || c >= 14 && c <= 31) {
397
449
  need = true;
398
450
  break;
399
451
  }
400
452
  }
401
453
  if (!need) return s;
402
- const escaped = s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
454
+ const escaped = s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;").replace(/\r/g, "&#13;");
403
455
  let out = "";
404
456
  for (let i = 0; i < escaped.length; i++) {
405
457
  const c = escaped.charCodeAt(i);
@@ -416,7 +468,9 @@ var BrowserXlsxWriter = class {
416
468
  this._sc++;
417
469
  this._sl.push({ name: sn, path: `xl/worksheets/sheet${this._sc}.xml`, hidden, fn: `sheet${this._sc}.xml`, id: this._sc, rId: `rId${this._sc}`, fhr: null });
418
470
  }
419
- _wsc(bb, val, cr, rn, so) {
471
+ /** `ref` is the cell's `r` attribute from `cellRefAttr`, or '' when omitted. */
472
+ _wsc(bb, val, ref, so) {
473
+ assertCellTextLength(val);
420
474
  let idx = this._sstM.get(val);
421
475
  if (idx === void 0) {
422
476
  idx = this._sstA.length;
@@ -425,9 +479,10 @@ var BrowserXlsxWriter = class {
425
479
  }
426
480
  this._sstCnt++;
427
481
  const sa = so !== void 0 ? ` s="${so}"` : "";
428
- bb.writeString(`<c r="${cr}${rn}" t="s"${sa}><v>${idx}</v></c>`);
482
+ bb.writeString(`<c${ref} t="s"${sa}><v>${idx}</v></c>`);
429
483
  }
430
- _wscStyle(bb, val, cr, rn, styleId) {
484
+ _wscStyle(bb, val, ref, styleId) {
485
+ assertCellTextLength(val);
431
486
  let idx = this._sstM.get(val);
432
487
  if (idx === void 0) {
433
488
  idx = this._sstA.length;
@@ -435,35 +490,45 @@ var BrowserXlsxWriter = class {
435
490
  this._sstM.set(val, idx);
436
491
  }
437
492
  this._sstCnt++;
438
- bb.writeString(`<c r="${cr}${rn}" t="s" s="${styleId}"><v>${idx}</v></c>`);
493
+ bb.writeString(`<c${ref} t="s" s="${styleId}"><v>${idx}</v></c>`);
439
494
  }
440
495
  _oa(d) {
441
- return (d.getTime() - this._oaE) / 864e5;
496
+ return timeToOaDate(d.getTime(), this._oaE);
442
497
  }
443
498
  _regFmt(fs) {
444
- if (this._fmtXf.has(fs)) return this._fmtXf.get(fs);
499
+ const existing = this._fmtXf.get(fs);
500
+ if (existing !== void 0) return existing;
445
501
  const nid = this._nextNfmt++;
446
502
  this._fmtReg.set(fs, nid);
447
503
  const xf = this._nextXf++;
448
504
  this._fmtXf.set(fs, xf);
449
505
  return xf;
450
506
  }
451
- _wcv(bb, raw, cr, rn) {
452
- if (raw === null || raw === void 0) return;
507
+ /**
508
+ * Write one cell of row `rn` at column `c` (letter `cr`). Returns the column
509
+ * of the last cell written, which the caller passes as `prevCol` for the next
510
+ * cell; a null cell writes nothing and leaves `prevCol` unchanged.
511
+ */
512
+ _wcv(bb, raw, c, cr, rn, prevCol) {
513
+ if (raw === null || raw === void 0) return prevCol;
453
514
  const fs = getFormat(raw);
454
- const v = fs !== null ? unwrapCell(raw) : raw;
515
+ const v = unwrapCell(raw);
455
516
  const xf = fs !== null ? this._regFmt(fs) : -1;
456
517
  const sa = xf >= 0 ? ` s="${xf}"` : "";
518
+ if (v === null || v === void 0) return prevCol;
519
+ const ref = cellRefAttr(c, prevCol, cr, rn);
520
+ const so = xf >= 0 ? xf : void 0;
457
521
  if (typeof v === "number") {
458
- if (Number.isFinite(v)) bb.writeString(`<c r="${cr}${rn}"${sa}><v>${v}</v></c>`);
459
- else this._wsc(bb, v.toString(), cr, rn, xf >= 0 ? xf : void 0);
460
- } else if (typeof v === "bigint") this._wsc(bb, v.toString(), cr, rn, xf >= 0 ? xf : void 0);
461
- else if (typeof v === "boolean") bb.writeString(`<c r="${cr}${rn}" t="b"${sa}><v>${v ? 1 : 0}</v></c>`);
522
+ if (Number.isFinite(v)) bb.writeString(`<c${ref}${sa}><v>${v}</v></c>`);
523
+ else this._wsc(bb, v.toString(), ref, so);
524
+ } else if (typeof v === "bigint") this._wsc(bb, v.toString(), ref, so);
525
+ else if (typeof v === "boolean") bb.writeString(`<c${ref} t="b"${sa}><v>${v ? 1 : 0}</v></c>`);
462
526
  else if (v instanceof Date) {
463
527
  const o = this._oa(v);
464
- if (Number.isFinite(o)) bb.writeString(`<c r="${cr}${rn}" s="${xf >= 0 ? xf : 1}"><v>${o}</v></c>`);
465
- else this._wsc(bb, v.toString(), cr, rn, xf >= 0 ? xf : void 0);
466
- } else this._wsc(bb, v.toString(), cr, rn, xf >= 0 ? xf : void 0);
528
+ if (Number.isFinite(o)) bb.writeString(`<c${ref} s="${xf >= 0 ? xf : 1}"><v>${o}</v></c>`);
529
+ else this._wsc(bb, v.toString(), ref, so);
530
+ } else this._wsc(bb, v.toString(), ref, so);
531
+ return c;
467
532
  }
468
533
  _sheetHead(bb, cc, isFirst, hasAF) {
469
534
  bb.writeString('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>');
@@ -480,13 +545,19 @@ var BrowserXlsxWriter = class {
480
545
  }
481
546
  startSheet(name, colCount, headers, options = {}) {
482
547
  if (this._isS) throw new Error("Already streaming");
548
+ assertColumnCount(colCount);
549
+ if (headers !== void 0) {
550
+ assertRowFitsColumns("header", headers.length, colCount);
551
+ for (const h of headers) assertCellTextLength(h);
552
+ }
483
553
  const { hidden = false, doAutofilter = true, headerStyle = "bold" } = options;
484
554
  let hs = 3;
485
555
  if (headerStyle === "fill") hs = 4;
486
556
  else if (headerStyle === "bold+fill") hs = 5;
487
557
  this.addSheet(name, hidden);
488
558
  this._isS = true;
489
- this._csb = new BrowserBigBuffer();
559
+ const csb = new BrowserBigBuffer();
560
+ this._csb = csb;
490
561
  this._csrn = 0;
491
562
  this._csSC = 0;
492
563
  this._csEC = colCount;
@@ -500,33 +571,38 @@ var BrowserXlsxWriter = class {
500
571
  if (w > 80) w = 80;
501
572
  if (this._cw[i] < w) this._cw[i] = w;
502
573
  }
503
- this._sheetHead(this._csb, colCount, this._sc === 1, this._csAF);
574
+ this._sheetHead(csb, colCount, this._sc === 1, this._csAF);
504
575
  if (headers) {
505
576
  this._csrn++;
506
- this._csb.writeString(`<row r="${this._csrn}">`);
507
- for (let c = 0; c < headers.length; c++) this._wscStyle(this._csb, headers[c], cl[c], this._csrn, hs);
508
- this._csb.writeString("</row>");
577
+ csb.writeString(`<row r="${this._csrn}">`);
578
+ for (let c = 0; c < headers.length; c++) this._wscStyle(csb, headers[c], "", hs);
579
+ csb.writeString("</row>");
509
580
  }
510
581
  }
511
582
  writeRow(row) {
512
- if (!this._isS) throw new Error("Not streaming");
583
+ if (!this._isS || this._csb === null) throw new Error("Not streaming");
584
+ if (row.length !== this._csEC) throw new Error(`Row length mismatch. Expected ${this._csEC} columns, got ${row.length}`);
585
+ const csb = this._csb;
513
586
  this._csrn++;
514
- this._csb.writeString(`<row r="${this._csrn}">`);
515
- for (let c = 0; c < row.length; c++) this._wcv(this._csb, row[c], this._csCL[c], this._csrn);
516
- this._csb.writeString("</row>");
587
+ assertRowCount(this._csrn);
588
+ csb.writeString(`<row r="${this._csrn}">`);
589
+ let prev = -1;
590
+ for (let c = 0; c < row.length; c++) prev = this._wcv(csb, row[c], c, this._csCL[c], this._csrn, prev);
591
+ csb.writeString("</row>");
517
592
  }
518
593
  endSheet() {
519
- if (!this._isS) throw new Error("Not streaming");
520
- this._csb.writeString("</sheetData>");
594
+ if (!this._isS || this._csb === null) throw new Error("Not streaming");
595
+ const csb = this._csb;
596
+ csb.writeString("</sheetData>");
521
597
  if (this._csAF && this._csEC > 0) {
522
598
  this._afOn = true;
523
599
  const fr = `A1:${this._csCL[this._csEC - 1]}${this._csrn}`;
524
- this._csb.writeString(`<autoFilter ref="${fr}"/>`);
600
+ csb.writeString(`<autoFilter ref="${fr}"/>`);
525
601
  const sh = this._sl[this._sc - 1];
526
602
  sh.fhr = `${this._fmn(sh.name)}!$A$1:$${this._csCL[this._csEC - 1]}$${this._csrn}`;
527
603
  }
528
- this._csb.writeString("</worksheet>");
529
- this._zip.addFile(this._sl[this._sc - 1].path, this._csb.toUint8Array());
604
+ csb.writeString("</worksheet>");
605
+ this._zip.addFile(this._sl[this._sc - 1].path, csb.toUint8Array());
530
606
  this._isS = false;
531
607
  this._csb = null;
532
608
  }
@@ -538,6 +614,14 @@ var BrowserXlsxWriter = class {
538
614
  else if (headerStyle === "bold+fill") hs = 5;
539
615
  const bb = new BrowserBigBuffer();
540
616
  const cc = rows.length > 0 ? rows[0].length : headers ? headers.length : 0;
617
+ if (cc > 0) assertColumnCount(cc);
618
+ const tr = rows.length + (headers ? 1 : 0);
619
+ assertRowCount(tr);
620
+ if (headers) {
621
+ assertRowFitsColumns("header", headers.length, cc);
622
+ for (const h of headers) assertCellTextLength(h);
623
+ }
624
+ for (let r = 0; r < rows.length; r++) assertRowFitsColumns("row", rows[r].length, cc);
541
625
  this._cw = new Array(cc).fill(-1);
542
626
  const cl = new Array(cc);
543
627
  for (let i = 0; i < cc; i++) cl[i] = this._cl(i);
@@ -550,23 +634,24 @@ var BrowserXlsxWriter = class {
550
634
  const raw = rows[r][c];
551
635
  if (raw == null) continue;
552
636
  const v = isFormattedCell(raw) ? raw.value : raw;
637
+ if (v === null || v === void 0) continue;
553
638
  let w = 1.3 * (v instanceof Date ? 10 : v.toString().length) + 3;
554
639
  if (w > 80) w = 80;
555
640
  if (this._cw[c] < w) this._cw[c] = w;
556
641
  }
557
- const tr = rows.length + (headers ? 1 : 0);
558
642
  this._sheetHead(bb, cc, this._sc === 1, doAutofilter && !!headers);
559
643
  let rn = 0;
560
644
  if (headers) {
561
645
  rn++;
562
646
  bb.writeString(`<row r="${rn}">`);
563
- for (let c = 0; c < headers.length; c++) this._wscStyle(bb, headers[c], cl[c], rn, hs);
647
+ for (let c = 0; c < headers.length; c++) this._wscStyle(bb, headers[c], "", hs);
564
648
  bb.writeString("</row>");
565
649
  }
566
650
  for (let r = 0; r < rows.length; r++) {
567
651
  rn++;
568
652
  bb.writeString(`<row r="${rn}">`);
569
- for (let c = 0; c < rows[r].length; c++) this._wcv(bb, rows[r][c], cl[c], rn);
653
+ let prev = -1;
654
+ for (let c = 0; c < rows[r].length; c++) prev = this._wcv(bb, rows[r][c], c, cl[c], rn, prev);
570
655
  bb.writeString("</row>");
571
656
  }
572
657
  bb.writeString("</sheetData>");
@@ -2468,7 +2553,8 @@ var BrowserXlsbWriter = class {
2468
2553
  return s;
2469
2554
  }
2470
2555
  _registerFormat(fmtString) {
2471
- if (this._fmtMap.has(fmtString)) return this._fmtMap.get(fmtString).xf;
2556
+ const existing = this._fmtMap.get(fmtString);
2557
+ if (existing !== void 0) return existing.xf;
2472
2558
  const nid = this._nextNid++;
2473
2559
  const xf = this._nextXf++;
2474
2560
  this._fmtMap.set(fmtString, { nid, xf });
@@ -2592,6 +2678,11 @@ var BrowserXlsbWriter = class {
2592
2678
  }
2593
2679
  startSheet(sheetName, columnCount, headers, options = {}) {
2594
2680
  if (this._isStreaming) throw new Error("Already streaming");
2681
+ assertColumnCount(columnCount);
2682
+ if (headers !== void 0) {
2683
+ assertRowFitsColumns("header", headers.length, columnCount);
2684
+ for (const h of headers) assertCellTextLength(h);
2685
+ }
2595
2686
  const { hidden = false, doAutofilter = true, headerStyle = "bold" } = options;
2596
2687
  let hs = 3;
2597
2688
  if (headerStyle === "fill") hs = 4;
@@ -2620,15 +2711,19 @@ var BrowserXlsbWriter = class {
2620
2711
  }
2621
2712
  }
2622
2713
  writeRow(row) {
2623
- if (!this._isStreaming) throw new Error("Not streaming");
2624
2714
  const bb = this._currentSheetBuffer;
2715
+ if (!this._isStreaming || bb === null) throw new Error("Not streaming");
2716
+ if (row.length !== this._currentSheetEndCol - this._currentSheetStartCol) {
2717
+ throw new Error(`Row length mismatch. Expected ${this._currentSheetEndCol - this._currentSheetStartCol} columns, got ${row.length}`);
2718
+ }
2719
+ assertRowCount(this._currentSheetRowNum + 1);
2625
2720
  this.createRowHeader(bb, this._currentSheetRowNum, this._currentSheetStartCol, this._currentSheetEndCol);
2626
2721
  for (let c = 0; c < row.length; c++) this._writeCell(bb, row[c], c);
2627
2722
  this._currentSheetRowNum++;
2628
2723
  }
2629
2724
  endSheet() {
2630
- if (!this._isStreaming) throw new Error("Not streaming");
2631
2725
  const bb = this._currentSheetBuffer;
2726
+ if (!this._isStreaming || bb === null) throw new Error("Not streaming");
2632
2727
  bb.write(this._sheet1Bytes.subarray(218, 290));
2633
2728
  if (this._currentSheetDoAutofilter) {
2634
2729
  this._autofilterIsOn = true;
@@ -2664,6 +2759,14 @@ var BrowserXlsbWriter = class {
2664
2759
  else if (headerStyle === "bold+fill") hs = 5;
2665
2760
  const bb = new BrowserBigBuffer();
2666
2761
  const colCount = rows.length > 0 ? rows[0].length : headers ? headers.length : 0;
2762
+ if (colCount > 0) assertColumnCount(colCount);
2763
+ const totalBr = rows.length + (headers ? 1 : 0);
2764
+ assertRowCount(totalBr);
2765
+ if (headers) {
2766
+ assertRowFitsColumns("header", headers.length, colCount);
2767
+ for (const h of headers) assertCellTextLength(h);
2768
+ }
2769
+ for (let r = 0; r < rows.length; r++) assertRowFitsColumns("row", rows[r].length, colCount);
2667
2770
  this._colWidths = new Array(colCount).fill(-1);
2668
2771
  if (headers) {
2669
2772
  for (let i = 0; i < colCount; i++) {
@@ -2678,6 +2781,7 @@ var BrowserXlsbWriter = class {
2678
2781
  const raw = rows[r][c];
2679
2782
  if (raw == null) continue;
2680
2783
  const v = isFormattedCell(raw) ? raw.value : raw;
2784
+ if (v === null || v === void 0) continue;
2681
2785
  const len = v instanceof Date ? 10 : v.toString().length;
2682
2786
  let w = 1.3 * len + 3;
2683
2787
  if (w > 80) w = 80;
@@ -2755,7 +2859,7 @@ var BrowserXlsbWriter = class {
2755
2859
  _writeCell(bb, raw, c) {
2756
2860
  if (raw === null || raw === void 0) return;
2757
2861
  const fmtString = getFormat(raw);
2758
- const val = fmtString !== null ? unwrapCell(raw) : raw;
2862
+ const val = unwrapCell(raw);
2759
2863
  const styleNum = fmtString !== null ? this._registerFormat(fmtString) : 0;
2760
2864
  if (typeof val === "number") {
2761
2865
  if (Number.isInteger(val) && val >= this._rRkIntegerLowerLimit && val <= this._rRkIntegerUpperLimit) {
@@ -2768,9 +2872,9 @@ var BrowserXlsbWriter = class {
2768
2872
  } else if (typeof val === "boolean") {
2769
2873
  this.writeBool(bb, val, c);
2770
2874
  } else if (val instanceof Date) {
2771
- const oaDate = (val.getTime() - this._oaEpoch) / 864e5;
2875
+ const oaDate = timeToOaDate(val.getTime(), this._oaEpoch);
2772
2876
  this.writeDouble(bb, oaDate, c, fmtString !== null ? styleNum : 1);
2773
- } else {
2877
+ } else if (val !== null && val !== void 0) {
2774
2878
  this.writeString(bb, val.toString(), c, 0, fmtString !== null ? styleNum : void 0);
2775
2879
  }
2776
2880
  }
@@ -2823,13 +2927,12 @@ var BrowserXlsbWriter = class {
2823
2927
  bb.writeUnsafeByte(val ? 1 : 0);
2824
2928
  }
2825
2929
  writeDateTime(bb, date, colNum) {
2826
- this.writeDouble(bb, (date.getTime() - this._oaEpoch) / 864e5, colNum, 1);
2930
+ this.writeDouble(bb, timeToOaDate(date.getTime(), this._oaEpoch), colNum, 1);
2827
2931
  }
2828
- writeString(bb, val, colNum, styleIndex = 0, styleOverride = void 0) {
2829
- let index;
2830
- if (this._sstDic.has(val)) {
2831
- index = this._sstDic.get(val);
2832
- } else {
2932
+ writeString(bb, val, colNum, styleIndex = 0, styleOverride) {
2933
+ assertCellTextLength(val);
2934
+ let index = this._sstDic.get(val);
2935
+ if (index === void 0) {
2833
2936
  index = this._sstCntUnique++;
2834
2937
  this._sstDic.set(val, index);
2835
2938
  }
@@ -2850,8 +2953,8 @@ var BrowserXlsbWriter = class {
2850
2953
  bb.writeByte(159);
2851
2954
  bb.writeByte(1);
2852
2955
  bb.writeByte(8);
2853
- bb.writeInt32LE(this._sstCntUnique);
2854
2956
  bb.writeInt32LE(this._sstCntAll);
2957
+ bb.writeInt32LE(this._sstCntUnique);
2855
2958
  for (const [txt] of this._sstDic) {
2856
2959
  const tl = txt.length;
2857
2960
  bb.writeByte(19);
@@ -2927,7 +3030,7 @@ var BrowserXlsbWriter = class {
2927
3030
  }
2928
3031
  wbBuffers.push(this._workbookBinMiddle);
2929
3032
  if (this._autofilterIsOn) {
2930
- const filtered = this._sheetList.filter((s) => s.filterData);
3033
+ const filtered = this._sheetList.filter((s) => s.filterData !== void 0);
2931
3034
  if (filtered.length > 0) {
2932
3035
  wbBuffers.push(this._magicFilterExcel2016Fix0);
2933
3036
  const cnt = filtered.length;