@justybase/spreadsheet-tasks 2.1.1 → 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.
package/dist/esm/index.js CHANGED
@@ -112,6 +112,143 @@ var BigBuffer = class {
112
112
  }
113
113
  };
114
114
 
115
+ // src/Formats.ts
116
+ var F = {
117
+ THOUSANDS_SEP: "#,##0",
118
+ CURRENCY_PLN: '#,##0.00 "z\u0142"',
119
+ CURRENCY_EUR: "#,##0.00 \u20AC",
120
+ PERCENTAGE: "0%",
121
+ SCIENTIFIC: "0.00E+00",
122
+ TWO_DECIMALS: "#,##0.00",
123
+ TEXT: "@",
124
+ LEADING_ZEROS: "000000000",
125
+ DATE_SHORT: "dd.mm.yyyy",
126
+ DATE_LONG: "d mmmm yyyy",
127
+ DATE_DAY_MONTH_YEAR: "dd-mm-yyyy",
128
+ DATE_ISO: "yyyy-mm-dd",
129
+ DATE_MONTH_YEAR: "mmmm yyyy",
130
+ DATE_WEEKDAY: "dddd, d mmmm yyyy",
131
+ DATE_DAY_MONTH: "d mmmm",
132
+ DATE_YEAR_ONLY: "yyyy",
133
+ DATETIME_SHORT: "dd.mm.yyyy hh:mm",
134
+ DATETIME_LONG: "d mmmm yyyy hh:mm:ss",
135
+ TIME_HH_MM: "hh:mm",
136
+ TIME_HH_MM_SS: "hh:mm:ss",
137
+ TIME_12H: "h:mm AM/PM",
138
+ DATETIME_24H: "dd.mm.yyyy hh:mm:ss",
139
+ DATETIME_ISO: 'yyyy-mm-dd"T"hh:mm:ss',
140
+ TIME_MS: "hh:mm:ss.000"
141
+ };
142
+ function isFormattedCell(val) {
143
+ return val !== null && val !== void 0 && typeof val === "object" && "format" in val && typeof val.format === "string";
144
+ }
145
+ function unwrapCell(val) {
146
+ if (isFormattedCell(val)) {
147
+ return val.value;
148
+ }
149
+ return val;
150
+ }
151
+ function getFormat(val) {
152
+ if (isFormattedCell(val)) {
153
+ return val.format;
154
+ }
155
+ return null;
156
+ }
157
+
158
+ // src/writerHelpers.ts
159
+ var OA_EPOCH_1900 = Date.UTC(1899, 11, 30);
160
+ var MARCH_1_1900 = Date.UTC(1900, 2, 1);
161
+ function oaDateToTime(oaDate, epochMs) {
162
+ let serial = oaDate;
163
+ if (epochMs === OA_EPOCH_1900 && serial >= 1 && serial < 60) {
164
+ serial += 1;
165
+ }
166
+ return Math.round(serial * 864e5) + epochMs;
167
+ }
168
+ function timeToOaDate(timeMs, epochMs) {
169
+ let serial = (timeMs - epochMs) / 864e5;
170
+ if (epochMs === OA_EPOCH_1900 && timeMs < MARCH_1_1900) {
171
+ serial -= 1;
172
+ }
173
+ return serial;
174
+ }
175
+ var INVALID_SHEET_NAME_CHARS = /[\\/*?[\]:]/g;
176
+ function sanitizeSheetName(name, sheetCount) {
177
+ if (!name || typeof name !== "string") {
178
+ return `Sheet${sheetCount + 1}`;
179
+ }
180
+ let sanitized = name.replace(INVALID_SHEET_NAME_CHARS, "_");
181
+ if (sanitized.length > 31) {
182
+ sanitized = sanitized.substring(0, 31);
183
+ }
184
+ if (sanitized.trim().length === 0) {
185
+ sanitized = `Sheet${sheetCount + 1}`;
186
+ }
187
+ return sanitized;
188
+ }
189
+ var MAX_ROWS = 1048576;
190
+ var MAX_COLS = 16384;
191
+ var MAX_CELL_CHARS = 32767;
192
+ function assertCellTextLength(text) {
193
+ if (text.length > MAX_CELL_CHARS) {
194
+ throw new Error(
195
+ `Excel cell text exceeds ${MAX_CELL_CHARS} UTF-16 code units (got ${text.length}).`
196
+ );
197
+ }
198
+ }
199
+ function assertColumnCount(columnCount) {
200
+ if (!Number.isInteger(columnCount) || columnCount < 1 || columnCount > MAX_COLS) {
201
+ throw new Error(
202
+ `Excel column count must be 1..${MAX_COLS} (got ${columnCount}).`
203
+ );
204
+ }
205
+ }
206
+ function assertRowFitsColumns(kind, length, columnCount) {
207
+ if (length > columnCount) {
208
+ throw new Error(
209
+ `Excel ${kind} length ${length} exceeds columnCount ${columnCount}.`
210
+ );
211
+ }
212
+ }
213
+ function assertRowCount(totalRows) {
214
+ if (totalRows > MAX_ROWS) {
215
+ throw new Error(
216
+ `Excel row count exceeds ${MAX_ROWS} (got ${totalRows}).`
217
+ );
218
+ }
219
+ }
220
+ function initColWidths(columnCount) {
221
+ return new Array(columnCount).fill(-1);
222
+ }
223
+ function applyHeaderWidths(colWidths, headers, columnCount) {
224
+ for (let i = 0; i < columnCount; i++) {
225
+ const len = headers[i] ? headers[i].length + 1 : 0;
226
+ let width = 1.25 * len + 2;
227
+ if (width > 80) width = 80;
228
+ if (colWidths[i] < width) colWidths[i] = width;
229
+ }
230
+ }
231
+ function updateColWidthsFromRows(colWidths, rows, sampleLimit = 100) {
232
+ for (let r = 0; r < Math.min(rows.length, sampleLimit); r++) {
233
+ const row = rows[r];
234
+ for (let c = 0; c < row.length; c++) {
235
+ const raw = row[c];
236
+ if (raw === null || raw === void 0) continue;
237
+ const val = isFormattedCell(raw) ? raw.value : raw;
238
+ const len = val instanceof Date ? 20 : String(val).length + 1;
239
+ let width = 1.25 * len + 2;
240
+ if (width > 80) width = 80;
241
+ if (colWidths[c] < width) colWidths[c] = width;
242
+ }
243
+ }
244
+ }
245
+ function defaultColWidth(width, floor = false) {
246
+ if (width > 0) {
247
+ return floor ? Math.floor(width) : width;
248
+ }
249
+ return 10;
250
+ }
251
+
115
252
  // src/ExcelReaderAbstract.ts
116
253
  var ExcelReaderAbstract = class {
117
254
  constructor() {
@@ -126,8 +263,7 @@ var ExcelReaderAbstract = class {
126
263
  dispose() {
127
264
  }
128
265
  getDateTimeFromOaDate(oaDate) {
129
- const ms = oaDate * 864e5 + this._oaEpoch;
130
- return new Date(ms);
266
+ return new Date(oaDateToTime(oaDate, this._oaEpoch));
131
267
  }
132
268
  };
133
269
 
@@ -387,168 +523,6 @@ var BiffReaderWriter = class {
387
523
 
388
524
  // src/XlsbReader.ts
389
525
  import AdmZip from "adm-zip";
390
- var XlsbReader = class extends ExcelReaderAbstract {
391
- constructor() {
392
- super();
393
- this.zip = null;
394
- this.sharedStrings = [];
395
- this.sheetNames = [];
396
- this.sheets = [];
397
- this._currentSheetIndex = -1;
398
- this._reader = null;
399
- this._currentRow = [];
400
- this._pendingRowIndex = -1;
401
- this._eof = false;
402
- this.xfIdToNumFmtId = [];
403
- this.customDateFormats = null;
404
- }
405
- async open(path3, readSharedStrings = true) {
406
- this.zip = new AdmZip(path3);
407
- const wbEntry = this.zip.getEntry("xl/workbook.bin");
408
- if (wbEntry) {
409
- const buf = wbEntry.getData();
410
- const reader = new BiffReaderWriter(buf);
411
- while (reader.readWorkbook()) {
412
- if (reader._isSheet) {
413
- const name = reader._workbookName;
414
- const rId = reader._recId;
415
- this.sheetNames.push(name);
416
- this.sheets.push({ name, rId, path: null });
417
- }
418
- }
419
- }
420
- const relsEntry = this.zip.getEntry("xl/_rels/workbook.bin.rels");
421
- if (relsEntry) {
422
- const xml = relsEntry.getData().toString("utf8");
423
- const relRegex = /<Relationship[^>]*Id="([^"]*)"[^>]*Target="([^"]*)"/g;
424
- let match;
425
- const rIdToTarget = {};
426
- while ((match = relRegex.exec(xml)) !== null) {
427
- rIdToTarget[match[1]] = match[2];
428
- }
429
- for (const sheet of this.sheets) {
430
- let target = rIdToTarget[sheet.rId];
431
- if (target) {
432
- if (target.startsWith("/")) target = target.substring(1);
433
- if (!target.startsWith("xl/")) target = "xl/" + target;
434
- sheet.path = target;
435
- }
436
- }
437
- }
438
- if (readSharedStrings) {
439
- const ssPath = "xl/sharedStrings.bin";
440
- if (this.zip.getEntry(ssPath)) {
441
- this._readSharedStrings(ssPath);
442
- }
443
- }
444
- const stylesPath = "xl/styles.bin";
445
- if (this.zip.getEntry(stylesPath)) {
446
- this._readStyles(stylesPath);
447
- }
448
- this.resultsCount = this.sheets.length;
449
- this._currentSheetIndex = -1;
450
- }
451
- _readSharedStrings(path3) {
452
- const entry = this.zip.getEntry(path3);
453
- if (!entry) return;
454
- const reader = new BiffReaderWriter(entry.getData());
455
- while (reader.readSharedStrings()) {
456
- if (reader._sharedStringValue !== null) {
457
- this.sharedStrings.push(reader._sharedStringValue);
458
- }
459
- }
460
- }
461
- _readStyles(path3) {
462
- const entry = this.zip.getEntry(path3);
463
- if (!entry) return;
464
- const reader = new BiffReaderWriter(entry.getData());
465
- while (reader.readStyles()) {
466
- }
467
- this.xfIdToNumFmtId = reader._xfIndexToNumFmtId;
468
- this.customDateFormats = reader._customNumFmts;
469
- }
470
- getSheetNames() {
471
- return this.sheetNames;
472
- }
473
- async read() {
474
- if (this._currentSheetIndex === -1) {
475
- this._currentSheetIndex = 0;
476
- if (!this._initSheet(0)) return false;
477
- }
478
- if (this._eof) return false;
479
- this._currentRow = [];
480
- while (true) {
481
- const hasRecord = this._reader.readWorksheet();
482
- if (!hasRecord) {
483
- this._eof = true;
484
- return true;
485
- }
486
- if (this._reader._rowIndex !== -1 && this._reader._rowIndex !== this._pendingRowIndex) {
487
- this._pendingRowIndex = this._reader._rowIndex;
488
- return true;
489
- }
490
- if (this._reader._readCell) {
491
- const col = this._reader._columnNum;
492
- let val = null;
493
- switch (this._reader._cellType) {
494
- case 2:
495
- val = this.sharedStrings[this._reader._intValue];
496
- break;
497
- case 3: {
498
- val = this._reader._doubleVal;
499
- const xfIndex = this._reader._xfIndex;
500
- let numFmtId = 0;
501
- if (this.xfIdToNumFmtId && xfIndex < this.xfIdToNumFmtId.length) {
502
- numFmtId = this.xfIdToNumFmtId[xfIndex];
503
- }
504
- const isDate = numFmtId >= 14 && numFmtId <= 22 || numFmtId >= 45 && numFmtId <= 47 || this.customDateFormats && this.customDateFormats.has(numFmtId);
505
- if (isDate) {
506
- try {
507
- val = this.getDateTimeFromOaDate(val);
508
- } catch {
509
- }
510
- }
511
- break;
512
- }
513
- case 4:
514
- val = this._reader._boolValue;
515
- break;
516
- case 5:
517
- val = this._reader._stringValue;
518
- break;
519
- default:
520
- val = null;
521
- }
522
- this._currentRow[col] = val;
523
- if (col >= this.fieldCount) this.fieldCount = col + 1;
524
- }
525
- }
526
- }
527
- _initSheet(index) {
528
- if (index >= this.sheets.length) return false;
529
- const sheet = this.sheets[index];
530
- const entry = this.zip.getEntry(sheet.path);
531
- if (!entry) return false;
532
- this._reader = new BiffReaderWriter(entry.getData());
533
- this._eof = false;
534
- this._pendingRowIndex = -1;
535
- while (this._reader.readWorksheet()) {
536
- if (this._reader._rowIndex !== -1) {
537
- this._pendingRowIndex = this._reader._rowIndex;
538
- return true;
539
- }
540
- }
541
- this._eof = true;
542
- return false;
543
- }
544
- getValue(i) {
545
- if (i < 0 || i >= this._currentRow.length) return null;
546
- return this._currentRow[i];
547
- }
548
- };
549
-
550
- // src/XlsxReader.ts
551
- import yauzl from "yauzl";
552
526
 
553
527
  // src/xmlUtils.ts
554
528
  function hasInvalidControlChars(str) {
@@ -582,8 +556,57 @@ function escapeXmlText(str) {
582
556
  }
583
557
  function unescapeXml(str) {
584
558
  if (!str || str.indexOf("&") === -1) return str;
585
- return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
586
- }
559
+ return str.replace(/&(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/g, (entity) => {
560
+ switch (entity) {
561
+ case "&amp;":
562
+ return "&";
563
+ case "&lt;":
564
+ return "<";
565
+ case "&gt;":
566
+ return ">";
567
+ case "&quot;":
568
+ return '"';
569
+ case "&apos;":
570
+ return "'";
571
+ default: {
572
+ if (entity[1] === "#") {
573
+ const body = entity.slice(2, -1);
574
+ const code = body[0] === "x" || body[0] === "X" ? parseInt(body.slice(1), 16) : parseInt(body, 10);
575
+ if (Number.isFinite(code)) {
576
+ try {
577
+ return String.fromCodePoint(code);
578
+ } catch {
579
+ return entity;
580
+ }
581
+ }
582
+ }
583
+ return entity;
584
+ }
585
+ }
586
+ });
587
+ }
588
+ function parseTagAttributes(tag) {
589
+ const attrs = {};
590
+ const re = /([A-Za-z_][\w.:-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
591
+ let m;
592
+ while ((m = re.exec(tag)) !== null) {
593
+ const name = m[1];
594
+ const value = m[3] !== void 0 ? m[3] : m[4];
595
+ attrs[name] = value;
596
+ const colon = name.indexOf(":");
597
+ if (colon !== -1) {
598
+ const local = name.slice(colon + 1);
599
+ if (!(local in attrs)) attrs[local] = value;
600
+ }
601
+ }
602
+ return attrs;
603
+ }
604
+ function resolveRelTarget(target) {
605
+ if (!target) return target;
606
+ if (target.startsWith("/")) return target.slice(1);
607
+ if (target.startsWith("xl/")) return target;
608
+ return "xl/" + target;
609
+ }
587
610
  function columnIndexToLetter(colIndex) {
588
611
  let n = colIndex + 1;
589
612
  let result = "";
@@ -611,31 +634,341 @@ function trimTrailingEmptyRows(rows) {
611
634
  }
612
635
  function parseSharedStringsXml(xml) {
613
636
  const result = [];
637
+ const itemRegex = /<(?:(?:[A-Za-z_][\w.-]*):)?si\b[^>]*>([\s\S]*?)<\/(?:(?:[A-Za-z_][\w.-]*):)?si\s*>|<(?:(?:[A-Za-z_][\w.-]*):)?si\b[^>]*\/>/gi;
638
+ let itemMatch;
639
+ while ((itemMatch = itemRegex.exec(xml)) !== null) {
640
+ const full = itemMatch[0];
641
+ if (full.endsWith("/>") && itemMatch[1] === void 0) {
642
+ result.push("");
643
+ continue;
644
+ }
645
+ let content = itemMatch[1] ?? "";
646
+ content = content.replace(/<(?:(?:[A-Za-z_][\w.-]*):)?rPh\b[^>]*>[\s\S]*?<\/(?:(?:[A-Za-z_][\w.-]*):)?rPh\s*>/gi, "");
647
+ content = content.replace(/<(?:(?:[A-Za-z_][\w.-]*):)?rPh\b[^>]*\/>/gi, "");
648
+ const textRegex = /<(?:(?:[A-Za-z_][\w.-]*):)?t\b[^>]*>([\s\S]*?)<\/(?:(?:[A-Za-z_][\w.-]*):)?t\s*>/gi;
649
+ let value = "";
650
+ let textMatch;
651
+ while ((textMatch = textRegex.exec(content)) !== null) {
652
+ value += textMatch[1];
653
+ }
654
+ result.push(unescapeXml(value));
655
+ }
656
+ return result;
657
+ }
658
+
659
+ // src/biff12Utils.ts
660
+ function readVlq(buf, pos) {
661
+ let value = 0;
662
+ let shift = 0;
663
+ let byte;
664
+ do {
665
+ byte = buf[pos++];
666
+ value |= (byte & 127) << shift;
667
+ shift += 7;
668
+ } while ((byte & 128) !== 0);
669
+ return { value, nextPos: pos };
670
+ }
671
+ function vlqBytes(value) {
672
+ const out = [];
673
+ while (value >= 128) {
674
+ out.push(value & 127 | 128);
675
+ value >>>= 7;
676
+ }
677
+ out.push(value & 127);
678
+ return out;
679
+ }
680
+ function readRecord(buf, pos, rec) {
681
+ const idRes = readVlq(buf, pos);
682
+ if (idRes.nextPos > buf.length) return false;
683
+ const lenRes = readVlq(buf, idRes.nextPos);
684
+ if (lenRes.nextPos + lenRes.value > buf.length) return false;
685
+ rec.headerStart = pos;
686
+ rec.dataStart = lenRes.nextPos;
687
+ rec.dataEnd = lenRes.nextPos + lenRes.value;
688
+ rec.id = idRes.value;
689
+ rec.len = lenRes.value;
690
+ return true;
691
+ }
692
+ function buildRecord(id, payload) {
693
+ const idBytes = vlqBytes(id);
694
+ const lenBytes = vlqBytes(payload.length);
695
+ const header = Buffer.alloc(idBytes.length + lenBytes.length);
696
+ idBytes.forEach((b, i) => header[i] = b);
697
+ lenBytes.forEach((b, i) => header[idBytes.length + i] = b);
698
+ return Buffer.concat([header, payload]);
699
+ }
700
+ function readUtf16(buf, start, charCount) {
701
+ const end = start + charCount * 2;
702
+ if (end > buf.length) return "";
703
+ return buf.toString("utf16le", start, end);
704
+ }
705
+ function parseSharedStringsBin(buf) {
706
+ const values = [];
707
+ let total = 0;
708
+ let unique = 0;
709
+ let endSstOffset = buf.length;
710
+ const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
614
711
  let pos = 0;
615
- while (true) {
616
- const siStart = xml.indexOf("<si>", pos);
617
- if (siStart === -1) break;
618
- const siEnd = xml.indexOf("</si>", siStart);
619
- if (siEnd === -1) break;
620
- const content = xml.substring(siStart, siEnd);
621
- let val = "";
622
- let tPos = 0;
623
- while (true) {
624
- const tStart = content.indexOf("<t", tPos);
625
- if (tStart === -1) break;
626
- const tagEnd = content.indexOf(">", tStart);
627
- const tEnd = content.indexOf("</t>", tagEnd);
628
- if (tEnd === -1) break;
629
- val += content.substring(tagEnd + 1, tEnd);
630
- tPos = tEnd + 4;
712
+ while (readRecord(buf, pos, rec)) {
713
+ pos = rec.dataEnd;
714
+ if (rec.id === 159) {
715
+ total = buf.readUInt32LE(rec.dataStart);
716
+ unique = buf.readUInt32LE(rec.dataStart + 4);
717
+ } else if (rec.id === 19) {
718
+ const cch = buf.readUInt32LE(rec.dataStart + 1);
719
+ values.push(readUtf16(buf, rec.dataStart + 5, cch));
720
+ } else if (rec.id === 160) {
721
+ endSstOffset = rec.headerStart;
631
722
  }
632
- result.push(unescapeXml(val));
633
- pos = siEnd + 5;
634
723
  }
635
- return result;
724
+ return { values, total, unique, endSstOffset };
636
725
  }
637
726
 
727
+ // src/XlsbReader.ts
728
+ var XlsbReader = class extends ExcelReaderAbstract {
729
+ constructor() {
730
+ super();
731
+ this.zip = null;
732
+ this.sharedStrings = [];
733
+ this.sheetNames = [];
734
+ this.sheets = [];
735
+ this._currentSheetIndex = -1;
736
+ this._reader = null;
737
+ this._currentRow = [];
738
+ this._pendingRowIndex = -1;
739
+ this._nextExpectedRow = 0;
740
+ this._eof = false;
741
+ this.xfIdToNumFmtId = [];
742
+ this.customDateFormats = null;
743
+ }
744
+ async open(path9, readSharedStrings = true) {
745
+ await this.close();
746
+ this.sheetNames = [];
747
+ this.sheets = [];
748
+ this.sharedStrings = [];
749
+ this.xfIdToNumFmtId = [];
750
+ this.customDateFormats = /* @__PURE__ */ new Set();
751
+ this.fieldCount = 0;
752
+ this.resultsCount = 0;
753
+ this.actualSheetName = "";
754
+ this._currentSheetIndex = -1;
755
+ this._currentRow = [];
756
+ this._pendingRowIndex = -1;
757
+ this._nextExpectedRow = 0;
758
+ this._eof = false;
759
+ this._reader = null;
760
+ this._oaEpoch = Date.UTC(1899, 11, 30);
761
+ this.zip = new AdmZip(path9);
762
+ const wbEntry = this.zip.getEntry("xl/workbook.bin");
763
+ if (wbEntry) {
764
+ const buf = wbEntry.getData();
765
+ const reader = new BiffReaderWriter(buf);
766
+ while (reader.readWorkbook()) {
767
+ if (reader._isSheet) {
768
+ const name = reader._workbookName;
769
+ const rId = reader._recId;
770
+ this.sheetNames.push(name);
771
+ this.sheets.push({ name, rId, path: null });
772
+ }
773
+ }
774
+ }
775
+ const relsEntry = this.zip.getEntry("xl/_rels/workbook.bin.rels");
776
+ if (relsEntry) {
777
+ const xml = relsEntry.getData().toString("utf8");
778
+ const relTagRe = /<Relationship\b[^>]*>/g;
779
+ let tagMatch;
780
+ const rIdToTarget = {};
781
+ while ((tagMatch = relTagRe.exec(xml)) !== null) {
782
+ const attrs = parseTagAttributes(tagMatch[0]);
783
+ const id = attrs["Id"] ?? attrs["id"];
784
+ const target = attrs["Target"] ?? attrs["target"];
785
+ if (id !== void 0 && target !== void 0) {
786
+ rIdToTarget[id] = target;
787
+ }
788
+ }
789
+ for (const sheet of this.sheets) {
790
+ const target = rIdToTarget[sheet.rId];
791
+ if (target) {
792
+ sheet.path = resolveRelTarget(target);
793
+ }
794
+ }
795
+ }
796
+ try {
797
+ const raw = wbEntry ? wbEntry.getData() : null;
798
+ if (raw) {
799
+ const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
800
+ let pos = 0;
801
+ while (readRecord(raw, pos, rec)) {
802
+ if (rec.id === 153 && rec.len >= 4) {
803
+ const is1904 = (raw[rec.dataStart] & 1) !== 0;
804
+ this._oaEpoch = is1904 ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
805
+ break;
806
+ }
807
+ pos = rec.dataEnd;
808
+ }
809
+ }
810
+ } catch {
811
+ }
812
+ if (readSharedStrings) {
813
+ const ssPath = "xl/sharedStrings.bin";
814
+ if (this.zip.getEntry(ssPath)) {
815
+ this._readSharedStrings(ssPath);
816
+ }
817
+ }
818
+ const stylesPath = "xl/styles.bin";
819
+ if (this.zip.getEntry(stylesPath)) {
820
+ this._readStyles(stylesPath);
821
+ }
822
+ this.resultsCount = this.sheets.length;
823
+ this._currentSheetIndex = -1;
824
+ }
825
+ _readSharedStrings(path9) {
826
+ const entry = this.zip.getEntry(path9);
827
+ if (!entry) return;
828
+ const reader = new BiffReaderWriter(entry.getData());
829
+ while (reader.readSharedStrings()) {
830
+ if (reader._sharedStringValue !== null) {
831
+ this.sharedStrings.push(reader._sharedStringValue);
832
+ }
833
+ }
834
+ }
835
+ _readStyles(path9) {
836
+ const entry = this.zip.getEntry(path9);
837
+ if (!entry) return;
838
+ const reader = new BiffReaderWriter(entry.getData());
839
+ while (reader.readStyles()) {
840
+ }
841
+ this.xfIdToNumFmtId = reader._xfIndexToNumFmtId;
842
+ this.customDateFormats = reader._customNumFmts;
843
+ }
844
+ getSheetNames() {
845
+ return this.sheetNames;
846
+ }
847
+ async selectSheet(nameOrIndex) {
848
+ let index = -1;
849
+ if (typeof nameOrIndex === "number") {
850
+ index = nameOrIndex;
851
+ } else {
852
+ index = this.sheetNames.indexOf(nameOrIndex);
853
+ if (index === -1) throw new Error(`Sheet '${nameOrIndex}' not found`);
854
+ }
855
+ if (index < 0 || index >= this.sheets.length) {
856
+ throw new Error(`Sheet index ${String(nameOrIndex)} out of range`);
857
+ }
858
+ this._currentSheetIndex = index;
859
+ if (!this._initSheet(index)) {
860
+ throw new Error(`XlsbReader: worksheet part missing for sheet '${this.sheetNames[index]}'.`);
861
+ }
862
+ }
863
+ async nextResult() {
864
+ const next = this._currentSheetIndex + 1;
865
+ if (next >= this.sheets.length) return false;
866
+ this._currentSheetIndex = next;
867
+ if (!this._initSheet(next)) {
868
+ throw new Error(`XlsbReader: worksheet part missing for sheet '${this.sheetNames[next]}'.`);
869
+ }
870
+ return true;
871
+ }
872
+ async read() {
873
+ if (this._currentSheetIndex === -1) {
874
+ this._currentSheetIndex = 0;
875
+ if (!this._initSheet(0)) return false;
876
+ }
877
+ if (this._eof) return false;
878
+ if (this._pendingRowIndex > this._nextExpectedRow && this._pendingRowIndex < MAX_ROWS) {
879
+ this._currentRow = [];
880
+ this._nextExpectedRow++;
881
+ return true;
882
+ }
883
+ this._currentRow = [];
884
+ while (true) {
885
+ const hasRecord = this._reader.readWorksheet();
886
+ if (!hasRecord) {
887
+ this._eof = true;
888
+ this._nextExpectedRow = this._pendingRowIndex + 1;
889
+ return true;
890
+ }
891
+ if (this._reader._rowIndex !== -1 && this._reader._rowIndex !== this._pendingRowIndex) {
892
+ const completed = this._pendingRowIndex;
893
+ this._pendingRowIndex = this._reader._rowIndex;
894
+ this._nextExpectedRow = completed + 1;
895
+ return true;
896
+ }
897
+ if (this._reader._readCell) {
898
+ const col = this._reader._columnNum;
899
+ let val = null;
900
+ switch (this._reader._cellType) {
901
+ case 2:
902
+ val = this.sharedStrings[this._reader._intValue];
903
+ break;
904
+ case 3: {
905
+ val = this._reader._doubleVal;
906
+ const xfIndex = this._reader._xfIndex;
907
+ let numFmtId = 0;
908
+ if (this.xfIdToNumFmtId && xfIndex < this.xfIdToNumFmtId.length) {
909
+ numFmtId = this.xfIdToNumFmtId[xfIndex];
910
+ }
911
+ const isDate = numFmtId >= 14 && numFmtId <= 22 || numFmtId >= 45 && numFmtId <= 47 || this.customDateFormats && this.customDateFormats.has(numFmtId);
912
+ if (isDate) {
913
+ try {
914
+ val = this.getDateTimeFromOaDate(val);
915
+ } catch {
916
+ }
917
+ }
918
+ break;
919
+ }
920
+ case 4:
921
+ val = this._reader._boolValue;
922
+ break;
923
+ case 5:
924
+ val = this._reader._stringValue;
925
+ break;
926
+ default:
927
+ val = null;
928
+ }
929
+ this._currentRow[col] = val;
930
+ if (col >= this.fieldCount) this.fieldCount = col + 1;
931
+ }
932
+ }
933
+ }
934
+ _initSheet(index) {
935
+ if (index < 0 || index >= this.sheets.length) return false;
936
+ const sheet = this.sheets[index];
937
+ const entry = sheet.path ? this.zip.getEntry(sheet.path) : void 0;
938
+ if (!entry) return false;
939
+ this._reader = new BiffReaderWriter(entry.getData());
940
+ this._eof = false;
941
+ this._pendingRowIndex = -1;
942
+ this._nextExpectedRow = 0;
943
+ this._currentRow = [];
944
+ this.fieldCount = 0;
945
+ this.actualSheetName = sheet.name;
946
+ while (this._reader.readWorksheet()) {
947
+ if (this._reader._rowIndex !== -1) {
948
+ this._pendingRowIndex = this._reader._rowIndex;
949
+ return true;
950
+ }
951
+ }
952
+ this._eof = true;
953
+ return true;
954
+ }
955
+ getValue(i) {
956
+ if (i < 0 || i >= this._currentRow.length) return null;
957
+ return this._currentRow[i];
958
+ }
959
+ };
960
+
638
961
  // src/XlsxReader.ts
962
+ import yauzl from "yauzl";
963
+ function parseIsoDateUtc(value) {
964
+ let text = value.trim().replace(" ", "T");
965
+ if (!text) return null;
966
+ if (!/z$|[+-]\d{2}:?\d{2}$/i.test(text) && text.includes("T")) {
967
+ text += "Z";
968
+ }
969
+ const date = new Date(text);
970
+ return Number.isNaN(date.getTime()) ? null : date;
971
+ }
639
972
  var XlsxReader = class extends ExcelReaderAbstract {
640
973
  constructor() {
641
974
  super();
@@ -647,13 +980,34 @@ var XlsxReader = class extends ExcelReaderAbstract {
647
980
  this._currentSheetIndex = -1;
648
981
  this._sheetXml = null;
649
982
  this._xmlPos = 0;
983
+ /** End of the sheetData element; row tags must not be searched past it. */
984
+ this._xmlEnd = 0;
650
985
  this._currentRow = [];
986
+ this._expectedRowNum = 1;
987
+ this._pendingEmptyCount = 0;
651
988
  this.xfIdToNumFmtId = [];
652
989
  this.customDateFormats = /* @__PURE__ */ new Set();
653
990
  }
654
- async open(path3, readSharedStrings = true) {
655
- return new Promise((resolve2, reject) => {
656
- yauzl.open(path3, { lazyEntries: true, autoClose: false }, async (err, zipfile) => {
991
+ async open(path9, readSharedStrings = true) {
992
+ await this.close();
993
+ this.sheetNames = [];
994
+ this.sheets = [];
995
+ this.sharedStrings = [];
996
+ this.xfIdToNumFmtId = [];
997
+ this.customDateFormats = /* @__PURE__ */ new Set();
998
+ this.fieldCount = 0;
999
+ this.resultsCount = 0;
1000
+ this.actualSheetName = "";
1001
+ this._currentSheetIndex = -1;
1002
+ this._sheetXml = null;
1003
+ this._xmlPos = 0;
1004
+ this._xmlEnd = 0;
1005
+ this._currentRow = [];
1006
+ this._expectedRowNum = 1;
1007
+ this._pendingEmptyCount = 0;
1008
+ this._oaEpoch = Date.UTC(1899, 11, 30);
1009
+ return new Promise((resolve6, reject) => {
1010
+ yauzl.open(path9, { lazyEntries: true, autoClose: false }, async (err, zipfile) => {
657
1011
  if (err) return reject(err);
658
1012
  this.zipfile = zipfile;
659
1013
  this.entries = /* @__PURE__ */ new Map();
@@ -666,28 +1020,40 @@ var XlsxReader = class extends ExcelReaderAbstract {
666
1020
  const wbRelsContent = await this._readZipEntryContent("xl/_rels/workbook.xml.rels");
667
1021
  const rIdToTarget = {};
668
1022
  if (wbRelsContent) {
669
- const relRegex = /<Relationship[^>]*Id="([^"]*)"[^>]*Target="([^"]*)"/g;
670
- let match;
671
- while ((match = relRegex.exec(wbRelsContent)) !== null) {
672
- rIdToTarget[match[1]] = match[2];
1023
+ const relTagRe = /<Relationship\b[^>]*>/g;
1024
+ let tagMatch;
1025
+ while ((tagMatch = relTagRe.exec(wbRelsContent)) !== null) {
1026
+ const attrs = parseTagAttributes(tagMatch[0]);
1027
+ const id = attrs["Id"] ?? attrs["id"];
1028
+ const target = attrs["Target"] ?? attrs["target"];
1029
+ if (id !== void 0 && target !== void 0) {
1030
+ rIdToTarget[id] = target;
1031
+ }
673
1032
  }
674
1033
  }
675
1034
  const wbContent = await this._readZipEntryContent("xl/workbook.xml");
676
1035
  if (wbContent) {
677
- const sheetRegex = /<sheet[^>]*name="([^"]*)"[^>]*sheetId="([^"]*)"[^>]*r:id="([^"]*)"/g;
678
- let match;
679
- while ((match = sheetRegex.exec(wbContent)) !== null) {
680
- const name = unescapeXml(match[1]);
681
- const sheetId = match[2];
682
- const rId = match[3];
1036
+ const sheetTagRe = /<sheet\b[^>]*>/g;
1037
+ let tagMatch;
1038
+ while ((tagMatch = sheetTagRe.exec(wbContent)) !== null) {
1039
+ const attrs = parseTagAttributes(tagMatch[0]);
1040
+ const rawName = attrs["name"];
1041
+ const sheetId = attrs["sheetId"] ?? "";
1042
+ const rId = attrs["r:id"] ?? attrs["id"] ?? attrs["rId"];
1043
+ if (rawName === void 0 || rId === void 0) continue;
1044
+ const name = unescapeXml(rawName);
683
1045
  const target = rIdToTarget[rId];
684
- let fullPath = target;
685
- if (!fullPath.startsWith("xl/")) {
686
- fullPath = "xl/" + fullPath;
687
- }
1046
+ if (!target) continue;
1047
+ const fullPath = resolveRelTarget(target);
688
1048
  this.sheetNames.push(name);
689
1049
  this.sheets.push({ name, sheetId, rId, path: fullPath });
690
1050
  }
1051
+ const wbPrMatch = /<workbookPr\b[^>]*>/i.exec(wbContent);
1052
+ if (wbPrMatch) {
1053
+ const wbPrAttrs = parseTagAttributes(wbPrMatch[0]);
1054
+ const v = (wbPrAttrs["date1904"] ?? "").trim().toLowerCase();
1055
+ this._oaEpoch = v === "1" || v === "true" ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
1056
+ }
691
1057
  }
692
1058
  if (readSharedStrings) {
693
1059
  const ssContent = await this._readZipEntryContent("xl/sharedStrings.xml");
@@ -701,7 +1067,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
701
1067
  }
702
1068
  this.resultsCount = this.sheets.length;
703
1069
  this._currentSheetIndex = -1;
704
- resolve2();
1070
+ resolve6();
705
1071
  } catch (e) {
706
1072
  reject(e);
707
1073
  }
@@ -715,16 +1081,16 @@ var XlsxReader = class extends ExcelReaderAbstract {
715
1081
  this.zipfile.close();
716
1082
  }
717
1083
  }
718
- async _readZipEntryContent(path3) {
719
- const entry = this.entries.get(path3);
1084
+ async _readZipEntryContent(path9) {
1085
+ const entry = this.entries.get(path9);
720
1086
  if (!entry) return null;
721
- return new Promise((resolve2, reject) => {
1087
+ return new Promise((resolve6, reject) => {
722
1088
  this.zipfile.openReadStream(entry, (err, readStream) => {
723
1089
  if (err) return reject(err);
724
1090
  const chunks = [];
725
1091
  readStream.on("data", (chunk) => chunks.push(chunk));
726
1092
  readStream.on("end", () => {
727
- resolve2(Buffer.concat(chunks).toString("utf8"));
1093
+ resolve6(Buffer.concat(chunks).toString("utf8"));
728
1094
  });
729
1095
  readStream.on("error", reject);
730
1096
  });
@@ -772,6 +1138,27 @@ var XlsxReader = class extends ExcelReaderAbstract {
772
1138
  return false;
773
1139
  }
774
1140
  }
1141
+ async selectSheet(nameOrIndex) {
1142
+ let index = -1;
1143
+ if (typeof nameOrIndex === "number") {
1144
+ index = nameOrIndex;
1145
+ } else {
1146
+ index = this.sheetNames.indexOf(nameOrIndex);
1147
+ if (index === -1) throw new Error(`Sheet '${nameOrIndex}' not found`);
1148
+ }
1149
+ if (index < 0 || index >= this.sheets.length) {
1150
+ throw new Error(`Sheet index ${String(nameOrIndex)} out of range`);
1151
+ }
1152
+ this._currentSheetIndex = index;
1153
+ await this._initSheet(index);
1154
+ }
1155
+ async nextResult() {
1156
+ const next = this._currentSheetIndex + 1;
1157
+ if (next >= this.sheets.length) return false;
1158
+ this._currentSheetIndex = next;
1159
+ await this._initSheet(next);
1160
+ return true;
1161
+ }
775
1162
  async _initSheet(index) {
776
1163
  if (index >= this.sheets.length) return;
777
1164
  const sheet = this.sheets[index];
@@ -781,23 +1168,106 @@ var XlsxReader = class extends ExcelReaderAbstract {
781
1168
  this._sheetXml = "";
782
1169
  }
783
1170
  this._xmlPos = 0;
1171
+ this._xmlEnd = this._sheetXml.length;
1172
+ this._expectedRowNum = 1;
1173
+ this._pendingEmptyCount = 0;
1174
+ this._currentRow = [];
1175
+ this.fieldCount = 0;
784
1176
  const sd = this._sheetXml.indexOf("<sheetData>");
785
- if (sd !== -1) this._xmlPos = sd + 11;
1177
+ if (sd !== -1) {
1178
+ this._xmlPos = sd + 11;
1179
+ const se = this._sheetXml.indexOf("</sheetData>", this._xmlPos);
1180
+ if (se !== -1) this._xmlEnd = se;
1181
+ } else {
1182
+ const m = /<(?:[A-Za-z_][\w.-]*:)?sheetData\b[^>]*>/i.exec(this._sheetXml);
1183
+ if (m && m.index !== void 0) {
1184
+ this._xmlPos = m.index + m[0].length;
1185
+ if (/\/>\s*$/.test(m[0])) {
1186
+ this._xmlEnd = this._xmlPos;
1187
+ } else {
1188
+ const se = this._sheetXml.indexOf("</sheetData>", this._xmlPos);
1189
+ if (se !== -1) this._xmlEnd = se;
1190
+ }
1191
+ }
1192
+ }
786
1193
  }
787
1194
  _readNextRow() {
788
1195
  if (!this._sheetXml) return false;
789
- const rowStart = this._sheetXml.indexOf("<row", this._xmlPos);
1196
+ if (this._pendingEmptyCount > 0) {
1197
+ this._pendingEmptyCount--;
1198
+ this._currentRow = [];
1199
+ this._expectedRowNum++;
1200
+ return true;
1201
+ }
1202
+ const rowStart = this._findRowTagStart(this._xmlPos);
790
1203
  if (rowStart === -1) return false;
1204
+ const rowTagEnd = this._sheetXml.indexOf(">", rowStart);
1205
+ if (rowTagEnd === -1) return false;
1206
+ const rowTag = this._sheetXml.substring(rowStart, rowTagEnd + 1);
1207
+ const isSelfClosing = this._sheetXml.charCodeAt(rowTagEnd - 1) === 47;
1208
+ const rowAttrs = parseTagAttributes(rowTag);
1209
+ const rRaw = rowAttrs["r"];
1210
+ let rowNum = this._expectedRowNum;
1211
+ if (rRaw !== void 0) {
1212
+ const parsed = parseInt(rRaw, 10);
1213
+ if (Number.isFinite(parsed) && parsed >= 1 && parsed <= MAX_ROWS) rowNum = parsed;
1214
+ }
1215
+ if (rowNum > this._expectedRowNum) {
1216
+ const gap = rowNum - this._expectedRowNum;
1217
+ this._pendingEmptyCount = gap - 1;
1218
+ this._currentRow = [];
1219
+ this._expectedRowNum++;
1220
+ return true;
1221
+ }
1222
+ if (rowNum < this._expectedRowNum) {
1223
+ if (isSelfClosing) {
1224
+ this._xmlPos = rowTagEnd + 1;
1225
+ this._currentRow = [];
1226
+ return true;
1227
+ }
1228
+ const rowEnd2 = this._sheetXml.indexOf("</row>", rowStart);
1229
+ if (rowEnd2 === -1) return false;
1230
+ this._xmlPos = rowEnd2 + 6;
1231
+ this._parseRowByIndex(rowStart, rowEnd2);
1232
+ this._expectedRowNum = Math.max(this._expectedRowNum + 1, rowNum + 1);
1233
+ return true;
1234
+ }
1235
+ if (isSelfClosing) {
1236
+ this._xmlPos = rowTagEnd + 1;
1237
+ this._currentRow = [];
1238
+ this._expectedRowNum = rowNum + 1;
1239
+ return true;
1240
+ }
791
1241
  const rowEnd = this._sheetXml.indexOf("</row>", rowStart);
792
1242
  if (rowEnd === -1) return false;
793
1243
  this._xmlPos = rowEnd + 6;
794
1244
  this._parseRowByIndex(rowStart, rowEnd);
1245
+ this._expectedRowNum = rowNum + 1;
795
1246
  return true;
796
1247
  }
1248
+ /**
1249
+ * Find the next `<row …>` start, ignoring similarly named elements such as
1250
+ * `<rowBreaks>` that follow `</sheetData>`.
1251
+ */
1252
+ _findRowTagStart(from) {
1253
+ const xml = this._sheetXml;
1254
+ let pos = from;
1255
+ while (pos < this._xmlEnd) {
1256
+ const idx = xml.indexOf("<row", pos);
1257
+ if (idx === -1 || idx >= this._xmlEnd) return -1;
1258
+ const next = xml.charCodeAt(idx + 4);
1259
+ if (next === 62 || next === 47 || next === 32 || next === 9 || next === 10 || next === 13) {
1260
+ return idx;
1261
+ }
1262
+ pos = idx + 4;
1263
+ }
1264
+ return -1;
1265
+ }
797
1266
  _parseRowByIndex(rowStart, rowEnd) {
798
1267
  this._currentRow = [];
799
1268
  const xml = this._sheetXml;
800
1269
  let pos = xml.indexOf(">", rowStart) + 1;
1270
+ let nextCol = 0;
801
1271
  while (pos < rowEnd) {
802
1272
  const cStart = xml.indexOf("<c", pos);
803
1273
  if (cStart === -1 || cStart >= rowEnd) break;
@@ -831,6 +1301,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
831
1301
  }
832
1302
  }
833
1303
  let val = null;
1304
+ let inlineVals = null;
834
1305
  if (xml.charCodeAt(nextTagClose - 1) === 47) {
835
1306
  pos = nextTagClose + 1;
836
1307
  } else {
@@ -845,20 +1316,28 @@ var XlsxReader = class extends ExcelReaderAbstract {
845
1316
  }
846
1317
  } else {
847
1318
  if (type === "inlineStr") {
848
- const tStart = xml.indexOf("<t", nextTagClose);
849
- if (tStart !== -1 && tStart < cellContentEnd) {
850
- const tContentStart = xml.indexOf(">", tStart) + 1;
1319
+ inlineVals = [];
1320
+ let tSearch = nextTagClose;
1321
+ while (true) {
1322
+ const tStart = xml.indexOf("<t", tSearch);
1323
+ if (tStart === -1 || tStart >= cellContentEnd) break;
1324
+ const tClose = xml.indexOf(">", tStart);
1325
+ if (tClose === -1 || tClose >= cellContentEnd) break;
1326
+ const tContentStart = tClose + 1;
851
1327
  const tEnd = xml.indexOf("</t>", tContentStart);
852
- if (tEnd !== -1 && tEnd < cellContentEnd) {
853
- val = xml.substring(tContentStart, tEnd);
854
- }
1328
+ if (tEnd === -1 || tEnd > cellContentEnd) break;
1329
+ inlineVals.push(xml.substring(tContentStart, tEnd));
1330
+ tSearch = tEnd + 4;
855
1331
  }
1332
+ if (inlineVals.length === 0) inlineVals = null;
856
1333
  }
857
1334
  }
858
1335
  pos = cellContentEnd + 4;
859
1336
  }
860
1337
  let finalVal = null;
861
- if (val !== null) {
1338
+ if (inlineVals !== null) {
1339
+ finalVal = unescapeXml(inlineVals.join(""));
1340
+ } else if (val !== null) {
862
1341
  if (type === "s") {
863
1342
  const idx = parseInt(val, 10);
864
1343
  finalVal = this.sharedStrings[idx];
@@ -866,148 +1345,66 @@ var XlsxReader = class extends ExcelReaderAbstract {
866
1345
  finalVal = val === "1" || val === "true";
867
1346
  } else if (type === "inlineStr") {
868
1347
  finalVal = unescapeXml(val);
1348
+ } else if (type === "str") {
1349
+ finalVal = unescapeXml(val);
1350
+ } else if (type === "e") {
1351
+ finalVal = unescapeXml(val);
1352
+ } else if (type === "d") {
1353
+ finalVal = parseIsoDateUtc(val) ?? unescapeXml(val);
869
1354
  } else if (type === "n" || type === "") {
870
1355
  finalVal = parseFloat(val);
871
1356
  if (styleIndex > 0 && this.xfIdToNumFmtId) {
872
1357
  let numFmtId = 0;
873
- if (styleIndex < this.xfIdToNumFmtId.length) {
874
- numFmtId = this.xfIdToNumFmtId[styleIndex];
875
- }
876
- const isDate = numFmtId >= 14 && numFmtId <= 22 || numFmtId >= 45 && numFmtId <= 47 || this.customDateFormats && this.customDateFormats.has(numFmtId);
877
- if (isDate) {
878
- try {
879
- finalVal = this.getDateTimeFromOaDate(finalVal);
880
- } catch {
881
- }
882
- }
883
- }
884
- }
885
- }
886
- if (colIndex !== -1) {
887
- this._currentRow[colIndex] = finalVal;
888
- if (colIndex >= this.fieldCount) this.fieldCount = colIndex + 1;
889
- }
890
- }
891
- }
892
- getValue(i) {
893
- if (i < 0 || i >= this._currentRow.length) return null;
894
- return this._currentRow[i];
895
- }
896
- };
897
-
898
- // src/ReaderFactory.ts
899
- import * as path from "path";
900
- var ReaderFactory = class {
901
- /**
902
- * @param filePath Path ending in `.xlsb` or `.xlsx`
903
- * @throws if the extension is unsupported
904
- */
905
- static create(filePath) {
906
- const ext = path.extname(filePath).toLowerCase();
907
- if (ext === ".xlsx") {
908
- return new XlsxReader();
909
- } else if (ext === ".xlsb") {
910
- return new XlsbReader();
911
- } else {
912
- throw new Error(`Unsupported extension: ${ext}`);
913
- }
914
- }
915
- };
916
-
917
- // src/XlsbWriter.ts
918
- import * as fs from "fs";
919
- import archiver from "archiver";
920
- import { Readable } from "stream";
921
-
922
- // src/Formats.ts
923
- var F = {
924
- THOUSANDS_SEP: "#,##0",
925
- CURRENCY_PLN: '#,##0.00 "z\u0142"',
926
- CURRENCY_EUR: "#,##0.00 \u20AC",
927
- PERCENTAGE: "0%",
928
- SCIENTIFIC: "0.00E+00",
929
- TWO_DECIMALS: "#,##0.00",
930
- TEXT: "@",
931
- LEADING_ZEROS: "000000000",
932
- DATE_SHORT: "dd.mm.yyyy",
933
- DATE_LONG: "d mmmm yyyy",
934
- DATE_DAY_MONTH_YEAR: "dd-mm-yyyy",
935
- DATE_ISO: "yyyy-mm-dd",
936
- DATE_MONTH_YEAR: "mmmm yyyy",
937
- DATE_WEEKDAY: "dddd, d mmmm yyyy",
938
- DATE_DAY_MONTH: "d mmmm",
939
- DATE_YEAR_ONLY: "yyyy",
940
- DATETIME_SHORT: "dd.mm.yyyy hh:mm",
941
- DATETIME_LONG: "d mmmm yyyy hh:mm:ss",
942
- TIME_HH_MM: "hh:mm",
943
- TIME_HH_MM_SS: "hh:mm:ss",
944
- TIME_12H: "h:mm AM/PM",
945
- DATETIME_24H: "dd.mm.yyyy hh:mm:ss",
946
- DATETIME_ISO: 'yyyy-mm-dd"T"hh:mm:ss',
947
- TIME_MS: "hh:mm:ss.000"
948
- };
949
- function isFormattedCell(val) {
950
- return val !== null && val !== void 0 && typeof val === "object" && "format" in val && typeof val.format === "string";
951
- }
952
- function unwrapCell(val) {
953
- if (isFormattedCell(val)) {
954
- return val.value;
955
- }
956
- return val;
957
- }
958
- function getFormat(val) {
959
- if (isFormattedCell(val)) {
960
- return val.format;
961
- }
962
- return null;
963
- }
964
-
965
- // src/writerHelpers.ts
966
- var INVALID_SHEET_NAME_CHARS = /[\\/*?[\]:]/g;
967
- function sanitizeSheetName(name, sheetCount) {
968
- if (!name || typeof name !== "string") {
969
- return `Sheet${sheetCount + 1}`;
970
- }
971
- let sanitized = name.replace(INVALID_SHEET_NAME_CHARS, "_");
972
- if (sanitized.length > 31) {
973
- sanitized = sanitized.substring(0, 31);
974
- }
975
- if (sanitized.trim().length === 0) {
976
- sanitized = `Sheet${sheetCount + 1}`;
977
- }
978
- return sanitized;
979
- }
980
- function initColWidths(columnCount) {
981
- return new Array(columnCount).fill(-1);
982
- }
983
- function applyHeaderWidths(colWidths, headers, columnCount) {
984
- for (let i = 0; i < columnCount; i++) {
985
- const len = headers[i] ? headers[i].length + 1 : 0;
986
- let width = 1.25 * len + 2;
987
- if (width > 80) width = 80;
988
- if (colWidths[i] < width) colWidths[i] = width;
989
- }
990
- }
991
- function updateColWidthsFromRows(colWidths, rows, sampleLimit = 100) {
992
- for (let r = 0; r < Math.min(rows.length, sampleLimit); r++) {
993
- const row = rows[r];
994
- for (let c = 0; c < row.length; c++) {
995
- const raw = row[c];
996
- if (raw === null || raw === void 0) continue;
997
- const val = isFormattedCell(raw) ? raw.value : raw;
998
- const len = val instanceof Date ? 20 : String(val).length + 1;
999
- let width = 1.25 * len + 2;
1000
- if (width > 80) width = 80;
1001
- if (colWidths[c] < width) colWidths[c] = width;
1358
+ if (styleIndex < this.xfIdToNumFmtId.length) {
1359
+ numFmtId = this.xfIdToNumFmtId[styleIndex];
1360
+ }
1361
+ const isDate = numFmtId >= 14 && numFmtId <= 22 || numFmtId >= 45 && numFmtId <= 47 || this.customDateFormats && this.customDateFormats.has(numFmtId);
1362
+ if (isDate) {
1363
+ try {
1364
+ finalVal = this.getDateTimeFromOaDate(finalVal);
1365
+ } catch {
1366
+ }
1367
+ }
1368
+ }
1369
+ }
1370
+ }
1371
+ if (colIndex === -1) colIndex = nextCol;
1372
+ nextCol = colIndex + 1;
1373
+ this._currentRow[colIndex] = finalVal;
1374
+ if (colIndex >= this.fieldCount) this.fieldCount = colIndex + 1;
1002
1375
  }
1003
1376
  }
1004
- }
1005
- function defaultColWidth(width, floor = false) {
1006
- if (width > 0) {
1007
- return floor ? Math.floor(width) : width;
1377
+ getValue(i) {
1378
+ if (i < 0 || i >= this._currentRow.length) return null;
1379
+ return this._currentRow[i];
1008
1380
  }
1009
- return 10;
1010
- }
1381
+ };
1382
+
1383
+ // src/ReaderFactory.ts
1384
+ import * as path from "path";
1385
+ var ReaderFactory = class {
1386
+ /**
1387
+ * @param filePath Path ending in `.xlsb` or `.xlsx`
1388
+ * @throws if the extension is unsupported
1389
+ */
1390
+ static create(filePath) {
1391
+ const ext = path.extname(filePath).toLowerCase();
1392
+ if (ext === ".xlsx") {
1393
+ return new XlsxReader();
1394
+ } else if (ext === ".xlsb") {
1395
+ return new XlsbReader();
1396
+ } else {
1397
+ throw new Error(`Unsupported extension: ${ext}`);
1398
+ }
1399
+ }
1400
+ };
1401
+
1402
+ // src/XlsbWriter.ts
1403
+ import * as fs from "fs";
1404
+ import * as path2 from "path";
1405
+ import { randomUUID } from "crypto";
1406
+ import archiver from "archiver";
1407
+ import { Readable } from "stream";
1011
1408
 
1012
1409
  // src/StreamingSheetState.ts
1013
1410
  var StreamingSheetState = class {
@@ -1061,6 +1458,8 @@ var XlsbWriter = class _XlsbWriter {
1061
1458
  * @param filePath Destination `.xlsb` path on disk.
1062
1459
  */
1063
1460
  constructor(filePath) {
1461
+ this._outputError = null;
1462
+ this._finalized = false;
1064
1463
  this.sheetCount = 0;
1065
1464
  this.sheetList = [];
1066
1465
  this.sstDic = /* @__PURE__ */ new Map();
@@ -1071,8 +1470,20 @@ var XlsbWriter = class _XlsbWriter {
1071
1470
  this._nextXfIdx = 4;
1072
1471
  this._stFmtRecords = /* @__PURE__ */ new Map();
1073
1472
  this.stream = new StreamingSheetState();
1074
- this.output = fs.createWriteStream(filePath);
1473
+ this._targetPath = path2.resolve(filePath);
1474
+ fs.mkdirSync(path2.dirname(this._targetPath), { recursive: true });
1475
+ this._tempPath = path2.join(
1476
+ path2.dirname(this._targetPath),
1477
+ `.${path2.basename(this._targetPath)}.${randomUUID()}.tmp`
1478
+ );
1479
+ this.output = fs.createWriteStream(this._tempPath);
1480
+ this.output.on("error", (err) => {
1481
+ this._outputError = err;
1482
+ });
1075
1483
  this.archive = archiver("zip");
1484
+ this.archive.on("error", (err) => {
1485
+ this._outputError = err;
1486
+ });
1076
1487
  this.archive.pipe(this.output);
1077
1488
  this._oaEpoch = Date.UTC(1899, 11, 30);
1078
1489
  this._sheet1Bytes = Buffer.from([
@@ -2942,6 +3353,11 @@ var XlsbWriter = class _XlsbWriter {
2942
3353
  * @param options Optional settings (hidden, doAutofilter)
2943
3354
  */
2944
3355
  startSheet(sheetName, columnCount, headers, options = {}) {
3356
+ assertColumnCount(columnCount);
3357
+ if (headers !== void 0) {
3358
+ assertRowFitsColumns("header", headers.length, columnCount);
3359
+ for (const h of headers) assertCellTextLength(h);
3360
+ }
2945
3361
  const { hidden = false, doAutofilter = true, sampleRows } = options;
2946
3362
  this.addSheet(sheetName, hidden);
2947
3363
  this.stream.begin(columnCount, doAutofilter && headers !== void 0, new BigBuffer());
@@ -3007,6 +3423,7 @@ var XlsbWriter = class _XlsbWriter {
3007
3423
  `Row length mismatch. Expected ${this.stream.endCol - this.stream.startCol} columns, got ${row.length}`
3008
3424
  );
3009
3425
  }
3426
+ assertRowCount(this.stream.rowNum + 1);
3010
3427
  this.createRowHeader(bigBuf, this.stream.rowNum, this.stream.startCol, this.stream.endCol);
3011
3428
  for (let c = 0; c < row.length; c++) {
3012
3429
  const raw = row[c];
@@ -3029,7 +3446,7 @@ var XlsbWriter = class _XlsbWriter {
3029
3446
  } else if (typeof val === "boolean") {
3030
3447
  this.writeBool(bigBuf, val, c);
3031
3448
  } else if (val instanceof Date) {
3032
- const oaDate = (val.getTime() - this._oaEpoch) / 864e5;
3449
+ const oaDate = timeToOaDate(val.getTime(), this._oaEpoch);
3033
3450
  this.writeDouble(bigBuf, oaDate, c, fmtString !== null ? styleNum : 1);
3034
3451
  } else if (val !== null && val !== void 0) {
3035
3452
  this.writeString(bigBuf, val.toString(), c, false, fmtString !== null ? styleNum : void 0);
@@ -3086,6 +3503,16 @@ var XlsbWriter = class _XlsbWriter {
3086
3503
  } else if (headers) {
3087
3504
  columnCount = headers.length;
3088
3505
  }
3506
+ if (columnCount > 0) assertColumnCount(columnCount);
3507
+ const totalRowsCheck = rows.length + (headers ? 1 : 0);
3508
+ assertRowCount(totalRowsCheck);
3509
+ if (headers) {
3510
+ assertRowFitsColumns("header", headers.length, columnCount);
3511
+ for (const h of headers) assertCellTextLength(h);
3512
+ }
3513
+ for (let r = 0; r < rows.length; r++) {
3514
+ assertRowFitsColumns("row", rows[r].length, columnCount);
3515
+ }
3089
3516
  const colWidths = initColWidths(columnCount);
3090
3517
  if (headers) {
3091
3518
  applyHeaderWidths(colWidths, headers, columnCount);
@@ -3161,7 +3588,7 @@ var XlsbWriter = class _XlsbWriter {
3161
3588
  } else if (typeof val === "boolean") {
3162
3589
  this.writeBool(bigBuf, val, c);
3163
3590
  } else if (val instanceof Date) {
3164
- const oaDate = (val.getTime() - this._oaEpoch) / 864e5;
3591
+ const oaDate = timeToOaDate(val.getTime(), this._oaEpoch);
3165
3592
  this.writeDouble(bigBuf, oaDate, c, fmtString !== null ? styleNum : 1);
3166
3593
  } else if (val !== null && val !== void 0) {
3167
3594
  this.writeString(bigBuf, val.toString(), c, false, fmtString !== null ? styleNum : void 0);
@@ -3244,13 +3671,14 @@ var XlsbWriter = class _XlsbWriter {
3244
3671
  bigBuf.writeUnsafeByte(val ? 1 : 0);
3245
3672
  }
3246
3673
  writeDateTime(bigBuf, date, colNum) {
3247
- const oaDate = (date.getTime() - this._oaEpoch) / 864e5;
3674
+ const oaDate = timeToOaDate(date.getTime(), this._oaEpoch);
3248
3675
  this.writeDouble(bigBuf, oaDate, colNum, 1);
3249
3676
  }
3250
3677
  _getDateWidth(_date) {
3251
3678
  return 19;
3252
3679
  }
3253
3680
  writeString(bigBuf, val, colNum, bolded = false, styleOverride) {
3681
+ assertCellTextLength(val);
3254
3682
  let index;
3255
3683
  if (this.sstDic.has(val)) {
3256
3684
  index = this.sstDic.get(val);
@@ -3276,8 +3704,8 @@ var XlsbWriter = class _XlsbWriter {
3276
3704
  bigBuf.writeByte(159);
3277
3705
  bigBuf.writeByte(1);
3278
3706
  bigBuf.writeByte(8);
3279
- bigBuf.writeInt32LE(this.sstCntUnique);
3280
3707
  bigBuf.writeInt32LE(this.sstCntAll);
3708
+ bigBuf.writeInt32LE(this.sstCntUnique);
3281
3709
  for (const [txt] of this.sstDic) {
3282
3710
  const txtLen = txt.length;
3283
3711
  bigBuf.writeByte(19);
@@ -3330,8 +3758,61 @@ var XlsbWriter = class _XlsbWriter {
3330
3758
  * Must be awaited before the file is fully written.
3331
3759
  */
3332
3760
  finalize() {
3333
- return new Promise((resolve2, reject) => {
3761
+ return new Promise((resolve6, reject) => {
3762
+ if (this._finalized) {
3763
+ reject(new Error("XlsbWriter: already finalized"));
3764
+ return;
3765
+ }
3766
+ this._finalized = true;
3767
+ let settled = false;
3768
+ const cleanupTemp = () => {
3769
+ try {
3770
+ if (fs.existsSync(this._tempPath)) fs.rmSync(this._tempPath);
3771
+ } catch {
3772
+ }
3773
+ };
3774
+ const fail = (err) => {
3775
+ if (settled) return;
3776
+ settled = true;
3777
+ try {
3778
+ this.archive.abort();
3779
+ } catch {
3780
+ }
3781
+ try {
3782
+ this.output.destroy();
3783
+ } catch {
3784
+ }
3785
+ cleanupTemp();
3786
+ reject(err instanceof Error ? err : new Error(String(err)));
3787
+ };
3788
+ const succeed = () => {
3789
+ if (settled) return;
3790
+ if (this._outputError) {
3791
+ fail(this._outputError);
3792
+ return;
3793
+ }
3794
+ settled = true;
3795
+ try {
3796
+ const fd = fs.openSync(this._tempPath, "r+");
3797
+ try {
3798
+ fs.fsyncSync(fd);
3799
+ } finally {
3800
+ fs.closeSync(fd);
3801
+ }
3802
+ try {
3803
+ const existing = fs.statSync(this._targetPath);
3804
+ fs.chmodSync(this._tempPath, existing.mode & 511);
3805
+ } catch {
3806
+ }
3807
+ fs.renameSync(this._tempPath, this._targetPath);
3808
+ resolve6();
3809
+ } catch (err) {
3810
+ cleanupTemp();
3811
+ reject(err instanceof Error ? err : new Error(String(err)));
3812
+ }
3813
+ };
3334
3814
  try {
3815
+ if (this._outputError) throw this._outputError;
3335
3816
  this.saveSst();
3336
3817
  this.archive.append(this._buildStylesBin(), { name: "xl/styles.bin" });
3337
3818
  const wbBuffers = [];
@@ -3428,11 +3909,12 @@ var XlsbWriter = class _XlsbWriter {
3428
3909
  </Relationships>`;
3429
3910
  this.archive.append(wsRels, { name: `xl/worksheets/_rels/${sheet.nameInArchive}.rels` });
3430
3911
  }
3431
- this.output.on("close", () => resolve2());
3432
- this.archive.on("error", (err) => reject(err));
3912
+ this.output.once("close", succeed);
3913
+ this.output.once("error", fail);
3914
+ this.archive.once("error", fail);
3433
3915
  this.archive.finalize();
3434
3916
  } catch (err) {
3435
- reject(err);
3917
+ fail(err);
3436
3918
  }
3437
3919
  });
3438
3920
  }
@@ -3440,27 +3922,17 @@ var XlsbWriter = class _XlsbWriter {
3440
3922
 
3441
3923
  // src/XlsxWriter.ts
3442
3924
  import * as fs2 from "fs";
3925
+ import * as path3 from "path";
3926
+ import { randomUUID as randomUUID2 } from "crypto";
3443
3927
  import archiver2 from "archiver";
3444
3928
  import { Readable as Readable2 } from "stream";
3445
- var COLUMN_LETTERS = (() => {
3446
- const letters = [];
3447
- for (let i = 65; i < 91; i++) {
3448
- letters.push(String.fromCharCode(i));
3449
- }
3450
- const temp = [];
3451
- for (const p of letters) {
3452
- for (let i = 65; i < 91; i++) {
3453
- temp.push(p + String.fromCharCode(i));
3454
- }
3455
- }
3456
- letters.push(...temp);
3457
- return letters;
3458
- })();
3459
3929
  var XlsxWriter = class {
3460
3930
  /**
3461
3931
  * @param filePath Destination `.xlsx` path on disk.
3462
3932
  */
3463
3933
  constructor(filePath) {
3934
+ this._outputError = null;
3935
+ this._finalized = false;
3464
3936
  this.sheetCount = 0;
3465
3937
  this.sheetList = [];
3466
3938
  this.sstArray = [];
@@ -3474,13 +3946,25 @@ var XlsxWriter = class {
3474
3946
  this._nextXfIndex = 4;
3475
3947
  this.stream = new StreamingSheetState();
3476
3948
  this.currentColLetters = [];
3477
- this.output = fs2.createWriteStream(filePath);
3949
+ this._targetPath = path3.resolve(filePath);
3950
+ fs2.mkdirSync(path3.dirname(this._targetPath), { recursive: true });
3951
+ this._tempPath = path3.join(
3952
+ path3.dirname(this._targetPath),
3953
+ `.${path3.basename(this._targetPath)}.${randomUUID2()}.tmp`
3954
+ );
3955
+ this.output = fs2.createWriteStream(this._tempPath);
3956
+ this.output.on("error", (err) => {
3957
+ this._outputError = err;
3958
+ });
3478
3959
  this.archive = archiver2("zip");
3960
+ this.archive.on("error", (err) => {
3961
+ this._outputError = err;
3962
+ });
3479
3963
  this.archive.pipe(this.output);
3480
3964
  this._oaEpoch = Date.UTC(1899, 11, 30);
3481
3965
  }
3482
3966
  _getColumnLetter(colIndex) {
3483
- return colIndex < COLUMN_LETTERS.length ? COLUMN_LETTERS[colIndex] : "A";
3967
+ return columnIndexToLetter(colIndex);
3484
3968
  }
3485
3969
  /** @deprecated Internal; kept for unit-test access. Prefer sanitizeSheetName(). */
3486
3970
  _sanitizeSheetName(name) {
@@ -3532,6 +4016,11 @@ var XlsxWriter = class {
3532
4016
  * Start streaming a sheet. Call `writeRow` for each row, then `endSheet`.
3533
4017
  */
3534
4018
  startSheet(sheetName, columnCount, headers, options = {}) {
4019
+ assertColumnCount(columnCount);
4020
+ if (headers !== void 0) {
4021
+ assertRowFitsColumns("header", headers.length, columnCount);
4022
+ for (const h of headers) assertCellTextLength(h);
4023
+ }
3535
4024
  const { hidden = false, doAutofilter = true, sampleRows } = options;
3536
4025
  this.addSheet(sheetName, hidden);
3537
4026
  this.stream.begin(columnCount, doAutofilter && headers !== void 0, new BigBuffer());
@@ -3588,6 +4077,7 @@ var XlsxWriter = class {
3588
4077
  );
3589
4078
  }
3590
4079
  this.stream.rowNum++;
4080
+ assertRowCount(this.stream.rowNum);
3591
4081
  bigBuf.writeString(`<row r="${this.stream.rowNum}">`);
3592
4082
  for (let c = 0; c < row.length; c++) {
3593
4083
  const raw = row[c];
@@ -3651,6 +4141,16 @@ var XlsxWriter = class {
3651
4141
  } else if (headers) {
3652
4142
  columnCount = headers.length;
3653
4143
  }
4144
+ if (columnCount > 0) assertColumnCount(columnCount);
4145
+ const totalRows = rows.length + (headers ? 1 : 0);
4146
+ assertRowCount(totalRows);
4147
+ if (headers) {
4148
+ assertRowFitsColumns("header", headers.length, columnCount);
4149
+ for (const h of headers) assertCellTextLength(h);
4150
+ }
4151
+ for (let r = 0; r < rows.length; r++) {
4152
+ assertRowFitsColumns("row", rows[r].length, columnCount);
4153
+ }
3654
4154
  const colWidths = initColWidths(columnCount);
3655
4155
  const colLetters = new Array(columnCount);
3656
4156
  for (let i = 0; i < columnCount; i++) {
@@ -3662,7 +4162,6 @@ var XlsxWriter = class {
3662
4162
  updateColWidthsFromRows(colWidths, rows);
3663
4163
  bigBuf.writeString('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>');
3664
4164
  bigBuf.writeString('<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">');
3665
- const totalRows = rows.length + (headers ? 1 : 0);
3666
4165
  if (totalRows > 0 && columnCount > 0) {
3667
4166
  bigBuf.writeString(`<dimension ref="A1:${colLetters[columnCount - 1]}${totalRows}"/>`);
3668
4167
  } else {
@@ -3748,6 +4247,7 @@ var XlsxWriter = class {
3748
4247
  });
3749
4248
  }
3750
4249
  _writeStringCell(bigBuf, val, colRef, rowNum, styleOverride) {
4250
+ assertCellTextLength(val);
3751
4251
  let index = this.sstMap.get(val);
3752
4252
  if (index === void 0) {
3753
4253
  index = this.sstArray.length;
@@ -3769,29 +4269,79 @@ var XlsxWriter = class {
3769
4269
  return xfIndex;
3770
4270
  }
3771
4271
  _toOADate(date) {
3772
- return (date.getTime() - this._oaEpoch) / 864e5;
4272
+ return timeToOaDate(date.getTime(), this._oaEpoch);
3773
4273
  }
3774
4274
  _getDateWidth(_date) {
3775
4275
  return 19;
3776
4276
  }
3777
4277
  /** Finalize the ZIP package and close the output stream. */
3778
4278
  finalize() {
3779
- return new Promise((resolve2, reject) => {
4279
+ return new Promise((resolve6, reject) => {
4280
+ if (this._finalized) {
4281
+ reject(new Error("XlsxWriter: already finalized"));
4282
+ return;
4283
+ }
4284
+ this._finalized = true;
4285
+ let settled = false;
4286
+ const cleanupTemp = () => {
4287
+ try {
4288
+ if (fs2.existsSync(this._tempPath)) fs2.rmSync(this._tempPath);
4289
+ } catch {
4290
+ }
4291
+ };
4292
+ const fail = (err) => {
4293
+ if (settled) return;
4294
+ settled = true;
4295
+ try {
4296
+ this.archive.abort();
4297
+ } catch {
4298
+ }
4299
+ try {
4300
+ this.output.destroy();
4301
+ } catch {
4302
+ }
4303
+ cleanupTemp();
4304
+ reject(err instanceof Error ? err : new Error(String(err)));
4305
+ };
4306
+ const succeed = () => {
4307
+ if (settled) return;
4308
+ if (this._outputError) {
4309
+ fail(this._outputError);
4310
+ return;
4311
+ }
4312
+ settled = true;
4313
+ try {
4314
+ const fd = fs2.openSync(this._tempPath, "r+");
4315
+ try {
4316
+ fs2.fsyncSync(fd);
4317
+ } finally {
4318
+ fs2.closeSync(fd);
4319
+ }
4320
+ try {
4321
+ const existing = fs2.statSync(this._targetPath);
4322
+ fs2.chmodSync(this._tempPath, existing.mode & 511);
4323
+ } catch {
4324
+ }
4325
+ fs2.renameSync(this._tempPath, this._targetPath);
4326
+ resolve6();
4327
+ } catch (err) {
4328
+ cleanupTemp();
4329
+ reject(err instanceof Error ? err : new Error(String(err)));
4330
+ }
4331
+ };
3780
4332
  try {
4333
+ if (this._outputError) throw this._outputError;
3781
4334
  this._writeSharedStrings();
3782
4335
  this._writeStyles();
3783
4336
  this._writeWorkbook();
3784
4337
  this._writeContentTypes();
3785
4338
  this._writeRels();
3786
- this.output.on("close", () => {
3787
- resolve2();
3788
- });
3789
- this.archive.on("error", (err) => {
3790
- reject(err);
3791
- });
4339
+ this.output.once("close", succeed);
4340
+ this.output.once("error", fail);
4341
+ this.archive.once("error", fail);
3792
4342
  this.archive.finalize();
3793
4343
  } catch (err) {
3794
- reject(err);
4344
+ fail(err);
3795
4345
  }
3796
4346
  });
3797
4347
  }
@@ -3871,63 +4421,324 @@ ${xfEntries.join("\n")}
3871
4421
  const escapedName = this._escapeSheetNameForXml(sheet.name);
3872
4422
  xml += `<sheet name="${escapedName}" sheetId="${sheet.sheetId}"${state} r:id="${sheet.rId}"/>`;
3873
4423
  }
3874
- xml += `</sheets>`;
3875
- if (this._autofilterIsOn) {
3876
- xml += "<definedNames>";
3877
- for (const sheet of this.sheetList) {
3878
- if (sheet.filterHeaderRange) {
3879
- const localSheetId = sheet.sheetId - 1;
3880
- const escapedRange = this._escape(sheet.filterHeaderRange);
3881
- xml += `<definedName name="_xlnm._FilterDatabase" localSheetId="${localSheetId}" hidden="1">${escapedRange}</definedName>`;
3882
- }
4424
+ xml += `</sheets>`;
4425
+ if (this._autofilterIsOn) {
4426
+ xml += "<definedNames>";
4427
+ for (const sheet of this.sheetList) {
4428
+ if (sheet.filterHeaderRange) {
4429
+ const localSheetId = sheet.sheetId - 1;
4430
+ const escapedRange = this._escape(sheet.filterHeaderRange);
4431
+ xml += `<definedName name="_xlnm._FilterDatabase" localSheetId="${localSheetId}" hidden="1">${escapedRange}</definedName>`;
4432
+ }
4433
+ }
4434
+ xml += "</definedNames>";
4435
+ }
4436
+ xml += '<calcPr calcId="124519" fullCalcOnLoad="1"/>';
4437
+ xml += `</workbook>`;
4438
+ this.archive.append(xml, { name: "xl/workbook.xml" });
4439
+ }
4440
+ _writeContentTypes() {
4441
+ let xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4442
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
4443
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
4444
+ <Default Extension="xml" ContentType="application/xml"/>
4445
+ <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
4446
+ <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
4447
+ <Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>`;
4448
+ for (const sheet of this.sheetList) {
4449
+ xml += `<Override PartName="/${sheet.pathInArchive}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
4450
+ }
4451
+ xml += `</Types>`;
4452
+ this.archive.append(xml, { name: "[Content_Types].xml" });
4453
+ }
4454
+ _writeRels() {
4455
+ const globalRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4456
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
4457
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
4458
+ </Relationships>`;
4459
+ this.archive.append(globalRels, { name: "_rels/.rels" });
4460
+ let wbRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4461
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`;
4462
+ for (const sheet of this.sheetList) {
4463
+ wbRels += `<Relationship Id="${sheet.rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/${sheet.nameInArchive}"/>`;
4464
+ }
4465
+ let nextId = this.sheetList.length + 1;
4466
+ wbRels += `<Relationship Id="rId${nextId++}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`;
4467
+ wbRels += `<Relationship Id="rId${nextId++}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>`;
4468
+ wbRels += `</Relationships>`;
4469
+ this.archive.append(wbRels, { name: "xl/_rels/workbook.xml.rels" });
4470
+ }
4471
+ };
4472
+
4473
+ // src/XlsxUpdater.ts
4474
+ import * as fs5 from "fs";
4475
+ import * as os from "os";
4476
+ import * as path6 from "path";
4477
+ import AdmZip2 from "adm-zip";
4478
+
4479
+ // src/atomicFile.ts
4480
+ import * as fs3 from "fs";
4481
+ import * as path4 from "path";
4482
+ import { randomUUID as randomUUID3 } from "crypto";
4483
+ var realFileOperations = {
4484
+ exists: (filePath) => fs3.existsSync(filePath),
4485
+ rename: (sourcePath, destinationPath) => fs3.renameSync(sourcePath, destinationPath),
4486
+ remove: (filePath) => fs3.rmSync(filePath)
4487
+ };
4488
+ function errorMessage(error) {
4489
+ return error instanceof Error ? error.message : String(error);
4490
+ }
4491
+ function destinationExistsError(destinationPath) {
4492
+ return new Error(
4493
+ `Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${destinationPath}`
4494
+ );
4495
+ }
4496
+ function readDestinationMode(target) {
4497
+ try {
4498
+ if (!fs3.existsSync(target)) return void 0;
4499
+ return fs3.statSync(target).mode & 511;
4500
+ } catch {
4501
+ return void 0;
4502
+ }
4503
+ }
4504
+ function installTemporaryOutput(temporaryPath, destinationPath, overwrite, operations = realFileOperations) {
4505
+ const backupPath = `${destinationPath}.${randomUUID3()}.replacement-backup`;
4506
+ let backupCreated = false;
4507
+ let installed = false;
4508
+ if (operations.exists(destinationPath)) {
4509
+ if (!overwrite) {
4510
+ throw destinationExistsError(destinationPath);
4511
+ }
4512
+ operations.rename(destinationPath, backupPath);
4513
+ backupCreated = true;
4514
+ }
4515
+ try {
4516
+ try {
4517
+ operations.rename(temporaryPath, destinationPath);
4518
+ installed = true;
4519
+ } catch (error) {
4520
+ if (overwrite && !backupCreated && operations.exists(destinationPath)) {
4521
+ operations.rename(destinationPath, backupPath);
4522
+ backupCreated = true;
4523
+ operations.rename(temporaryPath, destinationPath);
4524
+ installed = true;
4525
+ } else {
4526
+ throw error;
4527
+ }
4528
+ }
4529
+ if (backupCreated) {
4530
+ operations.remove(backupPath);
4531
+ }
4532
+ } catch (error) {
4533
+ if (backupCreated && !installed) {
4534
+ if (!operations.exists(destinationPath)) {
4535
+ try {
4536
+ operations.rename(backupPath, destinationPath);
4537
+ backupCreated = false;
4538
+ } catch (rollbackError) {
4539
+ throw new Error(
4540
+ `Excel conversion: final destination install failed (${errorMessage(error)}), and restoring the original destination also failed (${errorMessage(rollbackError)}). Original file remains at ${backupPath}.`
4541
+ );
4542
+ }
4543
+ } else {
4544
+ throw new Error(
4545
+ `Excel conversion: final destination install failed (${errorMessage(error)}). A concurrent destination was preserved; the original file remains at ${backupPath}.`
4546
+ );
4547
+ }
4548
+ }
4549
+ throw error;
4550
+ }
4551
+ }
4552
+ function writeBufferAtomically(buffer, destinationPath) {
4553
+ const target = path4.resolve(destinationPath);
4554
+ fs3.mkdirSync(path4.dirname(target), { recursive: true });
4555
+ const temporaryPath = path4.join(
4556
+ path4.dirname(target),
4557
+ `.${path4.basename(target)}.${randomUUID3()}.tmp`
4558
+ );
4559
+ const destinationMode = readDestinationMode(target);
4560
+ try {
4561
+ const fd = fs3.openSync(temporaryPath, "w", destinationMode ?? 438);
4562
+ try {
4563
+ if (destinationMode !== void 0) {
4564
+ try {
4565
+ fs3.fchmodSync(fd, destinationMode);
4566
+ } catch {
4567
+ }
4568
+ }
4569
+ fs3.writeFileSync(fd, buffer);
4570
+ fs3.fsyncSync(fd);
4571
+ } finally {
4572
+ fs3.closeSync(fd);
4573
+ }
4574
+ if (destinationMode !== void 0) {
4575
+ try {
4576
+ fs3.chmodSync(temporaryPath, destinationMode);
4577
+ } catch {
4578
+ }
4579
+ }
4580
+ fs3.renameSync(temporaryPath, target);
4581
+ } finally {
4582
+ if (fs3.existsSync(temporaryPath)) {
4583
+ try {
4584
+ fs3.rmSync(temporaryPath);
4585
+ } catch {
4586
+ }
4587
+ }
4588
+ }
4589
+ }
4590
+
4591
+ // src/streamingZip.ts
4592
+ import * as fs4 from "fs";
4593
+ import * as path5 from "path";
4594
+ import { randomUUID as randomUUID4 } from "crypto";
4595
+ import { finished } from "stream/promises";
4596
+ import { Readable as Readable3 } from "stream";
4597
+ import archiver3 from "archiver";
4598
+ function readDestinationMode2(target) {
4599
+ try {
4600
+ if (!fs4.existsSync(target)) return void 0;
4601
+ return fs4.statSync(target).mode & 511;
4602
+ } catch {
4603
+ return void 0;
4604
+ }
4605
+ }
4606
+ function appendAndWait(archive, source, name, output) {
4607
+ return new Promise((resolve6, reject) => {
4608
+ const cleanup = () => {
4609
+ archive.off("entry", onEntry);
4610
+ archive.off("error", onError);
4611
+ const maybeEmitter2 = source;
4612
+ if (typeof maybeEmitter2.off === "function") {
4613
+ maybeEmitter2.off("error", onError);
4614
+ }
4615
+ if (output) {
4616
+ output.off("error", onError);
4617
+ }
4618
+ };
4619
+ const onEntry = () => {
4620
+ cleanup();
4621
+ resolve6();
4622
+ };
4623
+ const onError = (error) => {
4624
+ cleanup();
4625
+ reject(error);
4626
+ };
4627
+ archive.once("entry", onEntry);
4628
+ archive.once("error", onError);
4629
+ if (output) {
4630
+ output.once("error", onError);
4631
+ }
4632
+ const maybeEmitter = source;
4633
+ if (typeof maybeEmitter.once === "function") {
4634
+ maybeEmitter.once("error", onError);
4635
+ }
4636
+ try {
4637
+ archive.append(source, { name });
4638
+ } catch (error) {
4639
+ cleanup();
4640
+ reject(error);
4641
+ }
4642
+ });
4643
+ }
4644
+ async function writeAdmZipStreaming(zip, stagedParts, destinationPath) {
4645
+ const target = path5.resolve(destinationPath);
4646
+ fs4.mkdirSync(path5.dirname(target), { recursive: true });
4647
+ const temporaryPath = path5.join(
4648
+ path5.dirname(target),
4649
+ `.${path5.basename(target)}.${randomUUID4()}.tmp`
4650
+ );
4651
+ const destinationMode = readDestinationMode2(target);
4652
+ let output = null;
4653
+ let archive = null;
4654
+ try {
4655
+ output = fs4.createWriteStream(
4656
+ temporaryPath,
4657
+ destinationMode !== void 0 ? { mode: destinationMode } : {}
4658
+ );
4659
+ let outputError = null;
4660
+ output.on("error", (err) => {
4661
+ outputError = err;
4662
+ try {
4663
+ archive == null ? void 0 : archive.abort();
4664
+ } catch {
4665
+ }
4666
+ });
4667
+ archive = archiver3("zip");
4668
+ archive.on("error", (err) => {
4669
+ outputError = err;
4670
+ });
4671
+ archive.pipe(output);
4672
+ for (const entry of zip.getEntries()) {
4673
+ if (outputError) throw outputError;
4674
+ const stagedPath = stagedParts.get(entry.entryName);
4675
+ if (stagedPath) {
4676
+ await appendAndWait(archive, fs4.createReadStream(stagedPath), entry.entryName, output);
4677
+ } else {
4678
+ await appendAndWait(archive, Readable3.from([entry.getData()]), entry.entryName, output);
3883
4679
  }
3884
- xml += "</definedNames>";
3885
4680
  }
3886
- xml += '<calcPr calcId="124519" fullCalcOnLoad="1"/>';
3887
- xml += `</workbook>`;
3888
- this.archive.append(xml, { name: "xl/workbook.xml" });
3889
- }
3890
- _writeContentTypes() {
3891
- let xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3892
- <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
3893
- <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
3894
- <Default Extension="xml" ContentType="application/xml"/>
3895
- <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
3896
- <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
3897
- <Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>`;
3898
- for (const sheet of this.sheetList) {
3899
- xml += `<Override PartName="/${sheet.pathInArchive}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
4681
+ if (outputError) throw outputError;
4682
+ await archive.finalize();
4683
+ await finished(output);
4684
+ if (outputError) throw outputError;
4685
+ if (destinationMode !== void 0) {
4686
+ try {
4687
+ fs4.chmodSync(temporaryPath, destinationMode);
4688
+ } catch {
4689
+ }
3900
4690
  }
3901
- xml += `</Types>`;
3902
- this.archive.append(xml, { name: "[Content_Types].xml" });
3903
- }
3904
- _writeRels() {
3905
- const globalRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3906
- <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3907
- <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
3908
- </Relationships>`;
3909
- this.archive.append(globalRels, { name: "_rels/.rels" });
3910
- let wbRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3911
- <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`;
3912
- for (const sheet of this.sheetList) {
3913
- wbRels += `<Relationship Id="${sheet.rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/${sheet.nameInArchive}"/>`;
4691
+ const fd = fs4.openSync(temporaryPath, "r+");
4692
+ try {
4693
+ fs4.fsyncSync(fd);
4694
+ } finally {
4695
+ fs4.closeSync(fd);
4696
+ }
4697
+ fs4.renameSync(temporaryPath, target);
4698
+ } catch (error) {
4699
+ try {
4700
+ archive == null ? void 0 : archive.abort();
4701
+ } catch {
4702
+ }
4703
+ throw error;
4704
+ } finally {
4705
+ if (output && !output.closed) {
4706
+ output.destroy();
4707
+ }
4708
+ if (fs4.existsSync(temporaryPath)) {
4709
+ try {
4710
+ fs4.rmSync(temporaryPath);
4711
+ } catch {
4712
+ }
3914
4713
  }
3915
- let nextId = this.sheetList.length + 1;
3916
- wbRels += `<Relationship Id="rId${nextId++}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`;
3917
- wbRels += `<Relationship Id="rId${nextId++}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>`;
3918
- wbRels += `</Relationships>`;
3919
- this.archive.append(wbRels, { name: "xl/_rels/workbook.xml.rels" });
3920
4714
  }
3921
- };
4715
+ }
3922
4716
 
3923
4717
  // src/XlsxUpdater.ts
3924
- import * as fs3 from "fs";
3925
- import AdmZip2 from "adm-zip";
3926
- var XlsxUpdater = class {
4718
+ function parseCellRange(ref) {
4719
+ const m = /^\$?([A-Za-z]+)\$?(\d+)(?::\$?([A-Za-z]+)\$?(\d+))?$/.exec(ref.trim());
4720
+ if (!m) return null;
4721
+ const c1 = columnLetterToIndex(m[1].toUpperCase());
4722
+ const r1 = parseInt(m[2], 10);
4723
+ const c2 = m[3] ? columnLetterToIndex(m[3].toUpperCase()) : c1;
4724
+ const r2 = m[4] ? parseInt(m[4], 10) : r1;
4725
+ if (!Number.isFinite(c1) || !Number.isFinite(r1) || !Number.isFinite(c2) || !Number.isFinite(r2)) {
4726
+ return null;
4727
+ }
4728
+ return {
4729
+ c1: Math.min(c1, c2),
4730
+ r1: Math.min(r1, r2),
4731
+ c2: Math.max(c1, c2),
4732
+ r2: Math.max(r1, r2)
4733
+ };
4734
+ }
4735
+ var XlsxUpdater = class _XlsxUpdater {
3927
4736
  /**
3928
4737
  * @param path Path to an existing `.xlsx` file.
3929
4738
  */
3930
- constructor(path3) {
4739
+ constructor(filePath) {
4740
+ this.temporaryDirectory = null;
4741
+ this.stagedParts = /* @__PURE__ */ new Map();
3931
4742
  this.sheetNameToPath = /* @__PURE__ */ new Map();
3932
4743
  this.pivotCacheDefs = [];
3933
4744
  this.pivotTableDefPaths = [];
@@ -3935,13 +4746,15 @@ var XlsxUpdater = class {
3935
4746
  this.sharedStringsValues = [];
3936
4747
  this.sharedStringsOriginalLength = 0;
3937
4748
  this.sharedStringsCount = 0;
4749
+ this.sharedStringsPending = [];
4750
+ this.sharedStringsDirty = false;
3938
4751
  this.stringIndexMap = /* @__PURE__ */ new Map();
3939
- this.sourcePath = path3;
4752
+ this.sourcePath = filePath;
3940
4753
  this._oaEpoch = Date.UTC(1899, 11, 30);
3941
- if (!fs3.existsSync(path3)) {
3942
- throw new Error(`XlsxUpdater: file not found: ${path3}`);
4754
+ if (!fs5.existsSync(filePath)) {
4755
+ throw new Error(`XlsxUpdater: file not found: ${filePath}`);
3943
4756
  }
3944
- this.zip = new AdmZip2(path3);
4757
+ this.zip = new AdmZip2(filePath);
3945
4758
  if (this.zip.getEntry("xl/workbook.bin")) {
3946
4759
  throw new Error(
3947
4760
  "XlsxUpdater: XLSB files are not supported yet. Convert the workbook to XLSX first."
@@ -3975,8 +4788,15 @@ var XlsxUpdater = class {
3975
4788
  if (!entry) {
3976
4789
  throw new Error(`XlsxUpdater: worksheet part missing for sheet "${sheetName}".`);
3977
4790
  }
3978
- const sheetXml = entry.getData().toString("utf8");
4791
+ const sheetXml = this._entryData(sheetPath).toString("utf8");
3979
4792
  this._ensureSharedStringsLoaded();
4793
+ if (this.sharedStringsXml !== null) {
4794
+ this.sharedStringsCount = Math.max(
4795
+ 0,
4796
+ this.sharedStringsCount - this._sharedStringRefCount(sheetXml)
4797
+ );
4798
+ this.sharedStringsDirty = true;
4799
+ }
3980
4800
  let colStyles = /* @__PURE__ */ new Map();
3981
4801
  let headerStyles = /* @__PURE__ */ new Map();
3982
4802
  let dateStyle = null;
@@ -3991,12 +4811,177 @@ var XlsxUpdater = class {
3991
4811
  const newSheetData = this._buildSheetData(rows, headers, colStyles, headerStyles, dateStyle);
3992
4812
  const newXml = this._patchSheetXml(sheetXml, newSheetData, totalRows, lastCol);
3993
4813
  this.zip.updateFile(sheetPath, Buffer.from(newXml, "utf8"));
4814
+ this._discardStagedPart(sheetPath);
3994
4815
  if (this.sharedStringsXml !== null) {
3995
4816
  this._commitSharedStrings();
3996
4817
  }
3997
4818
  const dimRef = totalRows > 0 && lastCol >= 0 ? `A1:${columnIndexToLetter(lastCol)}${totalRows}` : "A1";
3998
4819
  this._updatePivotCaches(sheetName, dimRef, rows.length);
3999
4820
  this._addRefreshOnLoadToPivotTables();
4821
+ this._removeCalcChain();
4822
+ this._updateTableRefs(sheetName, dimRef);
4823
+ }
4824
+ /**
4825
+ * Replace worksheet rows from a one-pass source without materialising the
4826
+ * complete result set. Use {@link saveStreaming} to keep the final ZIP
4827
+ * write streaming as well.
4828
+ */
4829
+ async replaceSheetDataStream(sheetName, rows, options = {}) {
4830
+ const { headers, styleFallback = "inherit" } = options;
4831
+ if (styleFallback !== "inherit" && styleFallback !== "general") {
4832
+ throw new Error("XlsxUpdater: styleFallback must be 'inherit' or 'general'.");
4833
+ }
4834
+ const sheetPath = this.sheetNameToPath.get(sheetName);
4835
+ if (!sheetPath) {
4836
+ throw new Error(`XlsxUpdater: sheet "${sheetName}" not found in the workbook.`);
4837
+ }
4838
+ const entry = this.zip.getEntry(sheetPath);
4839
+ if (!entry) {
4840
+ throw new Error(`XlsxUpdater: worksheet part missing for sheet "${sheetName}".`);
4841
+ }
4842
+ const oldStage = this.stagedParts.get(sheetPath);
4843
+ const beforeValuesLength = this.sharedStringsValues.length;
4844
+ const beforePendingLength = this.sharedStringsPending.length;
4845
+ const beforeSharedStringsCount = this.sharedStringsCount;
4846
+ const beforeSharedStringsDirty = this.sharedStringsDirty;
4847
+ const beforeSharedStringsXml = this.sharedStringsXml;
4848
+ const beforeSharedStringsOriginalLength = this.sharedStringsOriginalLength;
4849
+ const beforeStringIndexMap = new Map(this.stringIndexMap);
4850
+ const sharedStringsEntry = this.zip.getEntry("xl/sharedStrings.xml");
4851
+ const beforeSharedStringsBytes = sharedStringsEntry || this.stagedParts.has("xl/sharedStrings.xml") ? Buffer.from(this._entryData("xl/sharedStrings.xml")) : null;
4852
+ const createdParts = [];
4853
+ const oldSheetXml = this._entryData(sheetPath).toString("utf8");
4854
+ const rowsPath = this._temporaryPath(".rows");
4855
+ const outputPath = this._temporaryPath(".xml");
4856
+ createdParts.push(rowsPath, outputPath);
4857
+ let output = null;
4858
+ try {
4859
+ this._ensureSharedStringsLoaded();
4860
+ if (this.sharedStringsXml !== null) {
4861
+ this.sharedStringsCount = Math.max(
4862
+ 0,
4863
+ this.sharedStringsCount - this._sharedStringRefCount(oldSheetXml)
4864
+ );
4865
+ this.sharedStringsDirty = true;
4866
+ }
4867
+ let colStyles = /* @__PURE__ */ new Map();
4868
+ let headerStyles = /* @__PURE__ */ new Map();
4869
+ let dateStyle = null;
4870
+ if (styleFallback !== "general") {
4871
+ const collected = this._collectExistingStyles(oldSheetXml);
4872
+ colStyles = collected.dataStyles;
4873
+ headerStyles = collected.headerStyles;
4874
+ dateStyle = this._findDateStyleIndex();
4875
+ }
4876
+ const prefixMatch = /<([A-Za-z_][\w.-]*:)?sheetData\b/.exec(oldSheetXml);
4877
+ const prefix = (prefixMatch == null ? void 0 : prefixMatch[1]) ?? "";
4878
+ const rowOutput = fs5.createWriteStream(rowsPath);
4879
+ output = rowOutput;
4880
+ let outputError = null;
4881
+ rowOutput.on("error", (error) => {
4882
+ outputError = error;
4883
+ });
4884
+ let nextRow = 1;
4885
+ let width = (headers == null ? void 0 : headers.length) ?? 0;
4886
+ let dataRowsSeen = 0;
4887
+ let keptDataRows = 0;
4888
+ let lastKeptEnd = 0;
4889
+ let pendingEmptyWidth = 0;
4890
+ let writtenBytes = 0;
4891
+ const write = async (data) => {
4892
+ if (outputError) throw outputError;
4893
+ writtenBytes += data.length;
4894
+ if (rowOutput.write(data)) return;
4895
+ await new Promise((resolve6, reject) => {
4896
+ const onDrain = () => {
4897
+ cleanup();
4898
+ resolve6();
4899
+ };
4900
+ const onError = (error) => {
4901
+ cleanup();
4902
+ reject(error);
4903
+ };
4904
+ const cleanup = () => {
4905
+ rowOutput.off("drain", onDrain);
4906
+ rowOutput.off("error", onError);
4907
+ };
4908
+ rowOutput.once("drain", onDrain);
4909
+ rowOutput.once("error", onError);
4910
+ });
4911
+ };
4912
+ if (headers !== void 0) {
4913
+ const cells = headers.map((value, column) => this._valueCell(value, column, nextRow, headerStyles, dateStyle));
4914
+ const header = `<${prefix}row r="${nextRow}">${cells.join("")}</${prefix}row>`;
4915
+ await write(Buffer.from(header, "utf8"));
4916
+ lastKeptEnd = writtenBytes;
4917
+ nextRow++;
4918
+ }
4919
+ for await (const sourceRow of rows) {
4920
+ if (typeof sourceRow === "string" || sourceRow instanceof Uint8Array) {
4921
+ throw new TypeError("Each row must be an iterable of cells, not text");
4922
+ }
4923
+ const row = Array.from(sourceRow);
4924
+ const cells = row.map((value, column) => value === null || value === void 0 ? "" : this._valueCell(value, column, nextRow, colStyles, dateStyle));
4925
+ const rowXml = `<${prefix}row r="${nextRow}">${cells.join("")}</${prefix}row>`;
4926
+ await write(Buffer.from(rowXml, "utf8"));
4927
+ dataRowsSeen++;
4928
+ nextRow++;
4929
+ if (row.every((value) => value === null || value === void 0)) {
4930
+ pendingEmptyWidth = Math.max(pendingEmptyWidth, row.length);
4931
+ } else {
4932
+ width = Math.max(width, pendingEmptyWidth, row.length);
4933
+ pendingEmptyWidth = 0;
4934
+ keptDataRows = dataRowsSeen;
4935
+ lastKeptEnd = writtenBytes;
4936
+ }
4937
+ }
4938
+ await new Promise((resolve6, reject) => {
4939
+ rowOutput.once("close", resolve6);
4940
+ rowOutput.once("error", reject);
4941
+ rowOutput.end();
4942
+ });
4943
+ if (outputError) throw outputError;
4944
+ const rowsFd = fs5.openSync(rowsPath, "r+");
4945
+ try {
4946
+ fs5.ftruncateSync(rowsFd, lastKeptEnd);
4947
+ } finally {
4948
+ fs5.closeSync(rowsFd);
4949
+ }
4950
+ const totalRows = keptDataRows + (headers !== void 0 ? 1 : 0);
4951
+ const lastCol = width - 1;
4952
+ const dimRef = totalRows > 0 && lastCol >= 0 ? `A1:${columnIndexToLetter(lastCol)}${totalRows}` : "A1";
4953
+ this._patchSheetFile(oldSheetXml, rowsPath, outputPath, dimRef);
4954
+ this._stagePart(sheetPath, outputPath);
4955
+ this._commitSharedStrings();
4956
+ this._updatePivotCaches(sheetName, dimRef, keptDataRows);
4957
+ this._addRefreshOnLoadToPivotTables();
4958
+ this._removeCalcChain();
4959
+ this._updateTableRefs(sheetName, dimRef);
4960
+ if (oldStage && oldStage !== outputPath) {
4961
+ this._removeTemporaryFile(oldStage);
4962
+ }
4963
+ createdParts.splice(createdParts.indexOf(outputPath), 1);
4964
+ } catch (error) {
4965
+ this.sharedStringsValues.length = beforeValuesLength;
4966
+ this.sharedStringsPending.length = beforePendingLength;
4967
+ this.sharedStringsCount = beforeSharedStringsCount;
4968
+ this.sharedStringsDirty = beforeSharedStringsDirty;
4969
+ this.sharedStringsXml = beforeSharedStringsXml;
4970
+ this.sharedStringsOriginalLength = beforeSharedStringsOriginalLength;
4971
+ this.stringIndexMap.clear();
4972
+ for (const [value, index] of beforeStringIndexMap) this.stringIndexMap.set(value, index);
4973
+ if (beforeSharedStringsBytes) {
4974
+ this.zip.updateFile("xl/sharedStrings.xml", beforeSharedStringsBytes);
4975
+ }
4976
+ if (oldStage) this.stagedParts.set(sheetPath, oldStage);
4977
+ else this.stagedParts.delete(sheetPath);
4978
+ throw error;
4979
+ } finally {
4980
+ if (output && !output.closed) output.destroy();
4981
+ for (const temporaryPath of createdParts) {
4982
+ this._removeTemporaryFile(temporaryPath);
4983
+ }
4984
+ }
4000
4985
  }
4001
4986
  /**
4002
4987
  * Write the updated workbook to disk.
@@ -4004,12 +4989,89 @@ var XlsxUpdater = class {
4004
4989
  */
4005
4990
  save(outputPath) {
4006
4991
  const target = outputPath ?? this.sourcePath;
4007
- fs3.writeFileSync(target, this.zip.toBuffer());
4992
+ this._materializeStagedParts();
4993
+ writeBufferAtomically(this.zip.toBuffer(), target);
4994
+ }
4995
+ /** Save staged streaming replacements without materialising the ZIP. */
4996
+ async saveStreaming(outputPath) {
4997
+ const target = outputPath ?? this.sourcePath;
4998
+ await writeAdmZipStreaming(this.zip, this.stagedParts, target);
4999
+ this._materializeStagedParts();
4008
5000
  }
4009
5001
  /** The updated workbook as an in-memory ZIP buffer. */
4010
5002
  toBuffer() {
5003
+ this._materializeStagedParts();
4011
5004
  return this.zip.toBuffer();
4012
5005
  }
5006
+ /**
5007
+ * Release the staging directory and any unsaved staged parts.
5008
+ * The updater remains usable; a new staging directory is created lazily
5009
+ * on the next streaming replacement.
5010
+ */
5011
+ dispose() {
5012
+ for (const stagedPath of this.stagedParts.values()) {
5013
+ try {
5014
+ fs5.rmSync(stagedPath, { force: true });
5015
+ } catch {
5016
+ }
5017
+ }
5018
+ this.stagedParts.clear();
5019
+ if (this.temporaryDirectory !== null) {
5020
+ try {
5021
+ fs5.rmSync(this.temporaryDirectory, { recursive: true, force: true });
5022
+ } catch {
5023
+ }
5024
+ this.temporaryDirectory = null;
5025
+ }
5026
+ }
5027
+ /** Support `using` declarations for automatic staging cleanup. */
5028
+ [Symbol.dispose]() {
5029
+ this.dispose();
5030
+ }
5031
+ _ensureTemporaryDirectory() {
5032
+ if (this.temporaryDirectory === null) {
5033
+ this.temporaryDirectory = fs5.mkdtempSync(path6.join(os.tmpdir(), "spreadsheet-updater-"));
5034
+ } else {
5035
+ try {
5036
+ fs5.mkdirSync(this.temporaryDirectory, { recursive: true });
5037
+ } catch {
5038
+ }
5039
+ }
5040
+ return this.temporaryDirectory;
5041
+ }
5042
+ _temporaryPath(suffix) {
5043
+ return path6.join(this._ensureTemporaryDirectory(), `part-${Date.now()}-${Math.random().toString(16).slice(2)}${suffix}`);
5044
+ }
5045
+ _removeTemporaryFile(filePath) {
5046
+ try {
5047
+ fs5.rmSync(filePath);
5048
+ } catch (error) {
5049
+ if (error.code !== "ENOENT") throw error;
5050
+ }
5051
+ }
5052
+ _entryData(name) {
5053
+ const stagedPath = this.stagedParts.get(name);
5054
+ if (stagedPath) return fs5.readFileSync(stagedPath);
5055
+ const entry = this.zip.getEntry(name);
5056
+ if (!entry) throw new Error(`XlsxUpdater: ZIP member missing: ${name}`);
5057
+ return entry.getData();
5058
+ }
5059
+ _stagePart(name, filePath) {
5060
+ this.stagedParts.set(name, filePath);
5061
+ }
5062
+ _discardStagedPart(name) {
5063
+ const stagedPath = this.stagedParts.get(name);
5064
+ if (!stagedPath) return;
5065
+ this._removeTemporaryFile(stagedPath);
5066
+ this.stagedParts.delete(name);
5067
+ }
5068
+ _materializeStagedParts() {
5069
+ for (const [name, stagedPath] of this.stagedParts) {
5070
+ this.zip.updateFile(name, fs5.readFileSync(stagedPath));
5071
+ this._removeTemporaryFile(stagedPath);
5072
+ }
5073
+ this.stagedParts.clear();
5074
+ }
4013
5075
  // ------------------------------------------------------------------
4014
5076
  // Workbook structure
4015
5077
  // ------------------------------------------------------------------
@@ -4018,10 +5080,15 @@ var XlsxUpdater = class {
4018
5080
  const rIdToTarget = {};
4019
5081
  if (relsEntry) {
4020
5082
  const rels = relsEntry.getData().toString("utf8");
4021
- const relRegex = /<Relationship[^>]*Id="([^"]*)"[^>]*Target="([^"]*)"/g;
4022
- let match2;
4023
- while ((match2 = relRegex.exec(rels)) !== null) {
4024
- rIdToTarget[match2[1]] = match2[2];
5083
+ const relTagRe = /<Relationship\b[^>]*>/g;
5084
+ let tagMatch;
5085
+ while ((tagMatch = relTagRe.exec(rels)) !== null) {
5086
+ const attrs = parseTagAttributes(tagMatch[0]);
5087
+ const id = attrs["Id"] ?? attrs["id"];
5088
+ const target = attrs["Target"] ?? attrs["target"];
5089
+ if (id !== void 0 && target !== void 0) {
5090
+ rIdToTarget[id] = target;
5091
+ }
4025
5092
  }
4026
5093
  }
4027
5094
  const wbEntry = this.zip.getEntry("xl/workbook.xml");
@@ -4030,21 +5097,23 @@ var XlsxUpdater = class {
4030
5097
  let match;
4031
5098
  while ((match = sheetRe.exec(wbXml)) !== null) {
4032
5099
  const tag = match[0];
4033
- const nameMatch = /name="([^"]*)"/.exec(tag);
4034
- const rIdMatch = /r:id="([^"]*)"/.exec(tag);
4035
- if (!nameMatch || !rIdMatch) continue;
4036
- const name = nameMatch[1].replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
4037
- const target = rIdToTarget[rIdMatch[1]] ?? "";
4038
- let fullPath = target;
4039
- if (fullPath.startsWith("/")) {
4040
- fullPath = fullPath.substring(1);
4041
- } else if (!fullPath.startsWith("xl/")) {
4042
- fullPath = "xl/" + fullPath;
4043
- }
5100
+ const attrs = parseTagAttributes(tag);
5101
+ const rawName = attrs["name"];
5102
+ const rId = attrs["r:id"] ?? attrs["id"] ?? attrs["rId"];
5103
+ if (rawName === void 0 || rId === void 0) continue;
5104
+ const name = unescapeXml(rawName);
5105
+ const target = rIdToTarget[rId] ?? "";
5106
+ const fullPath = resolveRelTarget(target);
4044
5107
  if (fullPath) {
4045
5108
  this.sheetNameToPath.set(name, fullPath);
4046
5109
  }
4047
5110
  }
5111
+ const wbPrMatch = /<workbookPr\b[^>]*>/i.exec(wbXml);
5112
+ if (wbPrMatch) {
5113
+ const wbPrAttrs = parseTagAttributes(wbPrMatch[0]);
5114
+ const v = (wbPrAttrs["date1904"] ?? "").trim().toLowerCase();
5115
+ this._oaEpoch = v === "1" || v === "true" ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
5116
+ }
4048
5117
  for (const entry of this.zip.getEntries()) {
4049
5118
  const name = entry.entryName;
4050
5119
  if (/^xl\/pivotCache\/pivotCacheDefinition\d*\.xml$/.test(name)) {
@@ -4062,47 +5131,59 @@ var XlsxUpdater = class {
4062
5131
  return;
4063
5132
  }
4064
5133
  const entry = this.zip.getEntry("xl/sharedStrings.xml");
4065
- if (!entry) {
5134
+ if (!entry && !this.stagedParts.has("xl/sharedStrings.xml")) {
4066
5135
  this.sharedStringsXml = null;
4067
5136
  return;
4068
5137
  }
4069
- this.sharedStringsXml = entry.getData().toString("utf8");
5138
+ this.sharedStringsXml = this._entryData("xl/sharedStrings.xml").toString("utf8");
4070
5139
  this.sharedStringsValues = parseSharedStringsXml(this.sharedStringsXml);
4071
- const countMatch = /<sst\b[^>]*>/.exec(this.sharedStringsXml);
4072
- const countAttr = countMatch ? /count="(\d+)"/.exec(countMatch[0]) : null;
4073
- this.sharedStringsCount = countAttr ? parseInt(countAttr[1], 10) : 0;
5140
+ const countMatch = /<(?:[A-Za-z_][\w.-]*:)?sst\b[^>]*>/i.exec(this.sharedStringsXml);
5141
+ const countAttr = countMatch ? /(?:^|\s)count\s*=\s*["'](\d+)["']/.exec(countMatch[0]) : null;
5142
+ this.sharedStringsCount = countAttr ? parseInt(countAttr[1], 10) : this.sharedStringsValues.length;
4074
5143
  this.sharedStringsValues.forEach((value, index) => {
4075
5144
  if (!this.stringIndexMap.has(value)) {
4076
5145
  this.stringIndexMap.set(value, index);
4077
5146
  }
4078
5147
  });
4079
5148
  this.sharedStringsOriginalLength = this.sharedStringsValues.length;
5149
+ this.sharedStringsPending = [];
5150
+ this.sharedStringsDirty = false;
4080
5151
  }
4081
5152
  _commitSharedStrings() {
4082
- if (this.sharedStringsXml === null || this.sharedStringsValues.length === this.sharedStringsOriginalLength) {
5153
+ var _a;
5154
+ if (this.sharedStringsXml === null || !this.sharedStringsDirty) {
4083
5155
  return;
4084
5156
  }
4085
5157
  let xml = this.sharedStringsXml;
4086
- const sstStart = xml.indexOf("<sst");
4087
- const sstTagEnd = xml.indexOf(">", sstStart);
4088
- if (sstStart !== -1 && sstTagEnd !== -1) {
4089
- const tag = xml.substring(sstStart, sstTagEnd + 1);
4090
- const newTag = tag.replace(/count="\d+"/, `count="${this.sharedStringsCount}"`).replace(/uniqueCount="\d+"/, `uniqueCount="${this.sharedStringsValues.length}"`);
4091
- xml = xml.substring(0, sstStart) + newTag + xml.substring(sstTagEnd + 1);
4092
- }
4093
- const sstEnd = xml.lastIndexOf("</sst>");
4094
- if (sstEnd === -1) {
4095
- return;
5158
+ const sstMatch = /<(?<prefix>[A-Za-z_][\w.-]*:)?sst\b[^>]*>/i.exec(xml);
5159
+ if (!sstMatch || sstMatch.index === void 0) {
5160
+ throw new Error("XlsxUpdater: sharedStrings.xml has no sst element.");
5161
+ }
5162
+ const tag = sstMatch[0];
5163
+ const prefix = ((_a = sstMatch.groups) == null ? void 0 : _a.prefix) ?? "";
5164
+ const newTag = _XlsxUpdater._replaceOrAddXmlAttribute(
5165
+ _XlsxUpdater._replaceOrAddXmlAttribute(tag, "count", String(this.sharedStringsCount)),
5166
+ "uniqueCount",
5167
+ String(this.sharedStringsValues.length)
5168
+ );
5169
+ xml = xml.substring(0, sstMatch.index) + newTag + xml.substring(sstMatch.index + tag.length);
5170
+ const closingRe = new RegExp(`</${prefix}sst\\s*>\\s*$`, "i");
5171
+ const closingMatch = closingRe.exec(xml);
5172
+ if (!closingMatch || closingMatch.index === void 0) {
5173
+ throw new Error("XlsxUpdater: sharedStrings.xml has no closing sst element.");
4096
5174
  }
4097
- let appended = "";
4098
- for (let i = this.sharedStringsOriginalLength; i < this.sharedStringsValues.length; i++) {
4099
- const txt = this.sharedStringsValues[i];
5175
+ const appended = this.sharedStringsPending.map((txt) => {
4100
5176
  const clean = escapeXmlText(txt);
4101
5177
  const preserve = clean.length > 0 && (clean[0] === " " || clean[clean.length - 1] === " " || /[\t\n\r]/.test(clean));
4102
- appended += preserve ? `<si><t xml:space="preserve">${clean}</t></si>` : `<si><t>${clean}</t></si>`;
4103
- }
4104
- xml = xml.substring(0, sstEnd) + appended + xml.substring(sstEnd);
5178
+ const tAttr = preserve ? ' xml:space="preserve"' : "";
5179
+ return `<${prefix}si><${prefix}t${tAttr}>${clean}</${prefix}t></${prefix}si>`;
5180
+ }).join("");
5181
+ xml = xml.substring(0, closingMatch.index) + appended + xml.substring(closingMatch.index);
4105
5182
  this.zip.updateFile("xl/sharedStrings.xml", Buffer.from(xml, "utf8"));
5183
+ this.sharedStringsXml = xml;
5184
+ this.sharedStringsOriginalLength = this.sharedStringsValues.length;
5185
+ this.sharedStringsPending = [];
5186
+ this.sharedStringsDirty = false;
4106
5187
  }
4107
5188
  // ------------------------------------------------------------------
4108
5189
  // Styles
@@ -4227,7 +5308,7 @@ var XlsxUpdater = class {
4227
5308
  return `<c r="${colRef}${rowNum}" t="b"${colStyle > 0 ? ` s="${colStyle}"` : ""}><v>${val ? 1 : 0}</v></c>`;
4228
5309
  }
4229
5310
  if (val instanceof Date) {
4230
- const oaDate = (val.getTime() - this._oaEpoch) / 864e5;
5311
+ const oaDate = timeToOaDate(val.getTime(), this._oaEpoch);
4231
5312
  if (Number.isFinite(oaDate)) {
4232
5313
  const style = colStyle > 0 ? colStyle : dateStyle !== null ? dateStyle : 0;
4233
5314
  return `<c r="${colRef}${rowNum}"${style > 0 ? ` s="${style}"` : ""}><v>${oaDate}</v></c>`;
@@ -4253,10 +5334,25 @@ var XlsxUpdater = class {
4253
5334
  index = this.sharedStringsValues.length;
4254
5335
  this.sharedStringsValues.push(text);
4255
5336
  this.stringIndexMap.set(text, index);
5337
+ this.sharedStringsPending.push(text);
4256
5338
  }
4257
5339
  this.sharedStringsCount++;
5340
+ this.sharedStringsDirty = true;
4258
5341
  return `<c r="${colRef}${rowNum}" t="s"${styleAttr}><v>${index}</v></c>`;
4259
5342
  }
5343
+ static _replaceOrAddXmlAttribute(tag, name, value) {
5344
+ const escaped = escapeXmlText(value);
5345
+ const attribute = new RegExp(`(\\s${name}\\s*=\\s*["'])[^"']*(["'])`, "i");
5346
+ if (attribute.test(tag)) {
5347
+ return tag.replace(attribute, `$1${escaped}$2`);
5348
+ }
5349
+ const insertion = ` ${name}="${escaped}"`;
5350
+ return tag.endsWith("/>") ? `${tag.slice(0, -2)}${insertion}/>` : `${tag.slice(0, -1)}${insertion}>`;
5351
+ }
5352
+ _sharedStringRefCount(sheetXml) {
5353
+ const cellTags = sheetXml.match(/<(?:(?:[A-Za-z_][\w.-]*):)?c\b[^>]*>/gi) ?? [];
5354
+ return cellTags.filter((tag) => /(?:^|\s)t\s*=\s*["']s["']/i.test(tag)).length;
5355
+ }
4260
5356
  // ------------------------------------------------------------------
4261
5357
  // Worksheet XML patching
4262
5358
  // ------------------------------------------------------------------
@@ -4298,6 +5394,109 @@ var XlsxUpdater = class {
4298
5394
  }
4299
5395
  return xml;
4300
5396
  }
5397
+ _patchSheetFile(sheetXml, rowsPath, outputPath, dimensionRef) {
5398
+ var _a, _b, _c, _d;
5399
+ const source = Buffer.from(sheetXml, "utf8");
5400
+ const text = sheetXml;
5401
+ const replacements = [];
5402
+ const emptySheetData = /<(?<prefix>[A-Za-z_][\w.-]*:)?sheetData\b(?<attrs>[^>]*)\/>/i.exec(text);
5403
+ if (emptySheetData && emptySheetData.index !== void 0) {
5404
+ const prefix = ((_a = emptySheetData.groups) == null ? void 0 : _a.prefix) ?? "";
5405
+ replacements.push({
5406
+ start: emptySheetData.index,
5407
+ end: emptySheetData.index + emptySheetData[0].length,
5408
+ data: Buffer.from(`<${prefix}sheetData${((_b = emptySheetData.groups) == null ? void 0 : _b.attrs) ?? ""}>`, "utf8"),
5409
+ rows: true,
5410
+ suffix: Buffer.from(`</${prefix}sheetData>`, "utf8")
5411
+ });
5412
+ } else {
5413
+ const opening = /<(?<prefix>[A-Za-z_][\w.-]*:)?sheetData\b[^>]*>/i.exec(text);
5414
+ if (!opening || opening.index === void 0) {
5415
+ throw new Error("XlsxUpdater: <sheetData> element not found in worksheet XML.");
5416
+ }
5417
+ const prefix = ((_c = opening.groups) == null ? void 0 : _c.prefix) ?? "";
5418
+ const closeRe = new RegExp(`</${prefix}sheetData\\s*>`, "i");
5419
+ const closing = closeRe.exec(text.slice(opening.index + opening[0].length));
5420
+ if (!closing || closing.index === void 0) {
5421
+ throw new Error("XlsxUpdater: <sheetData> element not closed in worksheet XML.");
5422
+ }
5423
+ const closingStart = opening.index + opening[0].length + closing.index;
5424
+ replacements.push({
5425
+ start: opening.index + opening[0].length,
5426
+ end: closingStart,
5427
+ rows: true
5428
+ });
5429
+ }
5430
+ const dimension = /<(?<prefix>[A-Za-z_][\w.-]*:)?dimension\b[^>]*>/i.exec(text);
5431
+ if (dimension && dimension.index !== void 0) {
5432
+ replacements.push({
5433
+ start: dimension.index,
5434
+ end: dimension.index + dimension[0].length,
5435
+ data: Buffer.from(
5436
+ _XlsxUpdater._replaceOrAddXmlAttribute(dimension[0], "ref", dimensionRef)
5437
+ )
5438
+ });
5439
+ } else {
5440
+ const root = /<(?<prefix>[A-Za-z_][\w.-]*:)?worksheet\b[^>]*>/i.exec(text);
5441
+ if (root && root.index !== void 0) {
5442
+ const prefix = ((_d = root.groups) == null ? void 0 : _d.prefix) ?? "";
5443
+ const insertAt = root.index + root[0].length;
5444
+ replacements.push({
5445
+ start: insertAt,
5446
+ end: insertAt,
5447
+ data: Buffer.from(`<${prefix}dimension ref="${dimensionRef}"/>`, "utf8")
5448
+ });
5449
+ }
5450
+ }
5451
+ const autoFilter = /<(?<prefix>[A-Za-z_][\w.-]*:)?autoFilter\b[^>]*>/i.exec(text);
5452
+ if (autoFilter && autoFilter.index !== void 0) {
5453
+ replacements.push({
5454
+ start: autoFilter.index,
5455
+ end: autoFilter.index + autoFilter[0].length,
5456
+ data: Buffer.from(
5457
+ _XlsxUpdater._replaceOrAddXmlAttribute(autoFilter[0], "ref", dimensionRef),
5458
+ "utf8"
5459
+ )
5460
+ });
5461
+ }
5462
+ replacements.sort((a, b) => a.start - b.start || a.end - b.end);
5463
+ const fd = fs5.openSync(outputPath, "w");
5464
+ let cursor = 0;
5465
+ const writeBuffer = (data) => {
5466
+ fs5.writeSync(fd, data);
5467
+ };
5468
+ const copyRows = () => {
5469
+ const rowsFd = fs5.openSync(rowsPath, "r");
5470
+ try {
5471
+ const chunk = Buffer.alloc(1024 * 1024);
5472
+ const length = fs5.statSync(rowsPath).size;
5473
+ let position = 0;
5474
+ while (position < length) {
5475
+ const count = fs5.readSync(rowsFd, chunk, 0, Math.min(chunk.length, length - position), position);
5476
+ if (count <= 0) throw new Error("XlsxUpdater: failed to read staged rows.");
5477
+ fs5.writeSync(fd, chunk, 0, count);
5478
+ position += count;
5479
+ }
5480
+ } finally {
5481
+ fs5.closeSync(rowsFd);
5482
+ }
5483
+ };
5484
+ try {
5485
+ for (const replacement of replacements) {
5486
+ const startByte = Buffer.byteLength(text.slice(0, replacement.start), "utf8");
5487
+ const endByte = Buffer.byteLength(text.slice(0, replacement.end), "utf8");
5488
+ if (startByte < cursor) throw new Error("XlsxUpdater: overlapping worksheet replacements.");
5489
+ writeBuffer(source.subarray(cursor, startByte));
5490
+ if (replacement.data) writeBuffer(replacement.data);
5491
+ if (replacement.rows) copyRows();
5492
+ if (replacement.suffix) writeBuffer(replacement.suffix);
5493
+ cursor = endByte;
5494
+ }
5495
+ writeBuffer(source.subarray(cursor));
5496
+ } finally {
5497
+ fs5.closeSync(fd);
5498
+ }
5499
+ }
4301
5500
  // ------------------------------------------------------------------
4302
5501
  // Pivot table integration
4303
5502
  // ------------------------------------------------------------------
@@ -4326,93 +5525,251 @@ var XlsxUpdater = class {
4326
5525
  }
4327
5526
  }
4328
5527
  _addRefreshOnLoadToPivotTables() {
4329
- for (const path3 of this.pivotTableDefPaths) {
4330
- const entry = this.zip.getEntry(path3);
5528
+ for (const path9 of this.pivotTableDefPaths) {
5529
+ const entry = this.zip.getEntry(path9);
4331
5530
  if (!entry) continue;
4332
5531
  let xml = entry.getData().toString("utf8");
4333
5532
  const rootRe = /<pivotTableDefinition\b[^>]*>/;
4334
5533
  const rootMatch = rootRe.exec(xml);
4335
- if (!rootMatch || rootMatch[0].includes("refreshOnLoad")) continue;
4336
- const newTag = rootMatch[0].replace(/(\/?>)$/, ' refreshOnLoad="1"$1');
4337
- xml = xml.substring(0, rootMatch.index) + newTag + xml.substring(rootMatch.index + rootMatch[0].length);
4338
- this.zip.updateFile(path3, Buffer.from(xml, "utf8"));
4339
- }
4340
- }
4341
- };
4342
-
4343
- // src/XlsbUpdater.ts
4344
- import * as fs4 from "fs";
4345
- import AdmZip3 from "adm-zip";
4346
-
4347
- // src/biff12Utils.ts
4348
- function readVlq(buf, pos) {
4349
- let value = 0;
4350
- let shift = 0;
4351
- let byte;
4352
- do {
4353
- byte = buf[pos++];
4354
- value |= (byte & 127) << shift;
4355
- shift += 7;
4356
- } while ((byte & 128) !== 0);
4357
- return { value, nextPos: pos };
4358
- }
4359
- function vlqBytes(value) {
4360
- const out = [];
4361
- while (value >= 128) {
4362
- out.push(value & 127 | 128);
4363
- value >>>= 7;
4364
- }
4365
- out.push(value & 127);
4366
- return out;
4367
- }
4368
- function readRecord(buf, pos, rec) {
4369
- const idRes = readVlq(buf, pos);
4370
- if (idRes.nextPos > buf.length) return false;
4371
- const lenRes = readVlq(buf, idRes.nextPos);
4372
- if (lenRes.nextPos + lenRes.value > buf.length) return false;
4373
- rec.headerStart = pos;
4374
- rec.dataStart = lenRes.nextPos;
4375
- rec.dataEnd = lenRes.nextPos + lenRes.value;
4376
- rec.id = idRes.value;
4377
- rec.len = lenRes.value;
4378
- return true;
4379
- }
4380
- function buildRecord(id, payload) {
4381
- const idBytes = vlqBytes(id);
4382
- const lenBytes = vlqBytes(payload.length);
4383
- const header = Buffer.alloc(idBytes.length + lenBytes.length);
4384
- idBytes.forEach((b, i) => header[i] = b);
4385
- lenBytes.forEach((b, i) => header[idBytes.length + i] = b);
4386
- return Buffer.concat([header, payload]);
4387
- }
4388
- function readUtf16(buf, start, charCount) {
4389
- const end = start + charCount * 2;
4390
- if (end > buf.length) return "";
4391
- return buf.toString("utf16le", start, end);
4392
- }
4393
- function parseSharedStringsBin(buf) {
4394
- const values = [];
4395
- let total = 0;
4396
- let unique = 0;
4397
- let endSstOffset = buf.length;
4398
- const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
4399
- let pos = 0;
4400
- while (readRecord(buf, pos, rec)) {
4401
- pos = rec.dataEnd;
4402
- if (rec.id === 159) {
4403
- total = buf.readUInt32LE(rec.dataStart);
4404
- unique = buf.readUInt32LE(rec.dataStart + 4);
4405
- } else if (rec.id === 19) {
4406
- const cch = buf.readUInt32LE(rec.dataStart + 1);
4407
- values.push(readUtf16(buf, rec.dataStart + 5, cch));
4408
- } else if (rec.id === 160) {
4409
- endSstOffset = rec.headerStart;
5534
+ if (!rootMatch || rootMatch[0].includes("refreshOnLoad")) continue;
5535
+ const newTag = rootMatch[0].replace(/(\/?>)$/, ' refreshOnLoad="1"$1');
5536
+ xml = xml.substring(0, rootMatch.index) + newTag + xml.substring(rootMatch.index + rootMatch[0].length);
5537
+ this.zip.updateFile(path9, Buffer.from(xml, "utf8"));
4410
5538
  }
4411
5539
  }
4412
- return { values, total, unique, endSstOffset };
4413
- }
5540
+ /**
5541
+ * Remove the stale calculation chain (xl/calcChain.xml). Excel rebuilds it
5542
+ * on open; keeping stale <c> entries for the replaced sheet would force a
5543
+ * repair prompt. Removes the part, its Content_Types override and its
5544
+ * workbook rels entry (xlspy parity).
5545
+ */
5546
+ _removeCalcChain() {
5547
+ const calcPath = "xl/calcChain.xml";
5548
+ if (!this.zip.getEntry(calcPath) && !this.stagedParts.has(calcPath)) {
5549
+ this._ensureCalcPrFullCalc();
5550
+ return;
5551
+ }
5552
+ try {
5553
+ this.zip.deleteFile(calcPath);
5554
+ } catch {
5555
+ }
5556
+ const stagedCalc = this.stagedParts.get(calcPath);
5557
+ if (stagedCalc) {
5558
+ this.stagedParts.delete(calcPath);
5559
+ try {
5560
+ fs5.rmSync(stagedCalc, { force: true });
5561
+ } catch {
5562
+ }
5563
+ }
5564
+ const ctEntry = this.zip.getEntry("[Content_Types].xml");
5565
+ if (ctEntry || this.stagedParts.has("[Content_Types].xml")) {
5566
+ try {
5567
+ let ct = this._entryData("[Content_Types].xml").toString("utf8");
5568
+ const before = ct;
5569
+ ct = ct.replace(/<Override\b[^>]*PartName="\/xl\/calcChain\.xml"[^>]*\/?>/gi, "");
5570
+ if (ct !== before) {
5571
+ this._writePartText("[Content_Types].xml", ct);
5572
+ }
5573
+ } catch {
5574
+ }
5575
+ }
5576
+ const relsPath = "xl/_rels/workbook.xml.rels";
5577
+ if (this.zip.getEntry(relsPath) || this.stagedParts.has(relsPath)) {
5578
+ try {
5579
+ const rels = this._entryData(relsPath).toString("utf8");
5580
+ const relTagRe = /<Relationship\b[^>]*>/g;
5581
+ let m;
5582
+ let out = "";
5583
+ let last = 0;
5584
+ let changed = false;
5585
+ while ((m = relTagRe.exec(rels)) !== null) {
5586
+ const attrs = parseTagAttributes(m[0]);
5587
+ const type = attrs["Type"] ?? attrs["type"] ?? "";
5588
+ const target = attrs["Target"] ?? attrs["target"] ?? "";
5589
+ const isCalcChain = type.endsWith("/calcChain") && resolveRelTarget(target) === "xl/calcChain.xml";
5590
+ if (isCalcChain) {
5591
+ out += rels.substring(last, m.index);
5592
+ last = m.index + m[0].length;
5593
+ changed = true;
5594
+ }
5595
+ }
5596
+ if (changed) {
5597
+ out += rels.substring(last);
5598
+ this._writePartText(relsPath, out);
5599
+ }
5600
+ } catch {
5601
+ }
5602
+ }
5603
+ this._ensureCalcPrFullCalc();
5604
+ }
5605
+ _writePartText(name, text) {
5606
+ if (this.stagedParts.has(name)) {
5607
+ fs5.writeFileSync(this.stagedParts.get(name), text, "utf8");
5608
+ } else {
5609
+ this.zip.updateFile(name, Buffer.from(text, "utf8"));
5610
+ }
5611
+ }
5612
+ _ensureCalcPrFullCalc() {
5613
+ try {
5614
+ const wbPath = "xl/workbook.xml";
5615
+ if (!this.zip.getEntry(wbPath) && !this.stagedParts.has(wbPath)) return;
5616
+ let xml = this._entryData(wbPath).toString("utf8");
5617
+ const calcPrRe = /<(?:[A-Za-z_][\w.-]*:)?calcPr\b[^>]*>/i;
5618
+ const match = calcPrRe.exec(xml);
5619
+ if (match && match.index !== void 0) {
5620
+ const newTag = _XlsxUpdater._replaceOrAddXmlAttribute(match[0], "fullCalcOnLoad", "1");
5621
+ xml = xml.substring(0, match.index) + newTag + xml.substring(match.index + match[0].length);
5622
+ } else {
5623
+ const insertAt = _XlsxUpdater._calcPrInsertPosition(xml);
5624
+ if (insertAt < 0) return;
5625
+ xml = xml.substring(0, insertAt) + '<calcPr fullCalcOnLoad="1"/>' + xml.substring(insertAt);
5626
+ }
5627
+ this._writePartText(wbPath, xml);
5628
+ } catch {
5629
+ }
5630
+ }
5631
+ /**
5632
+ * Insertion offset for a missing `<calcPr>`. The element follows
5633
+ * sheets/externalReferences/definedNames in the workbook schema, so insert
5634
+ * after the last of those that exists instead of right after `<workbook>`.
5635
+ */
5636
+ static _calcPrInsertPosition(xml) {
5637
+ const prefixed = "(?:[A-Za-z_][\\w.-]*:)?";
5638
+ const candidates = [
5639
+ new RegExp(`</${prefixed}sheets\\s*>`, "i"),
5640
+ new RegExp(`<${prefixed}externalReferences\\b[^>]*/>`, "i"),
5641
+ new RegExp(`<${prefixed}externalReferences\\b[^>]*>[\\s\\S]*?</${prefixed}externalReferences\\s*>`, "i"),
5642
+ new RegExp(`<${prefixed}definedNames\\b[^>]*/>`, "i"),
5643
+ new RegExp(`<${prefixed}definedNames\\b[^>]*>[\\s\\S]*?</${prefixed}definedNames\\s*>`, "i")
5644
+ ];
5645
+ let insertAt = -1;
5646
+ for (const re of candidates) {
5647
+ const m = re.exec(xml);
5648
+ if (m && m.index !== void 0) {
5649
+ insertAt = Math.max(insertAt, m.index + m[0].length);
5650
+ }
5651
+ }
5652
+ if (insertAt >= 0) return insertAt;
5653
+ const close = new RegExp(`</${prefixed}workbook\\s*>`, "i").exec(xml);
5654
+ if (close && close.index !== void 0) return close.index;
5655
+ const root = new RegExp(`<${prefixed}workbook\\b[^>]*>`, "i").exec(xml);
5656
+ if (!root || root.index === void 0) return -1;
5657
+ return root.index + root[0].length;
5658
+ }
5659
+ /**
5660
+ * Update ListObject table ranges (xl/tables/*.xml) for the replaced sheet.
5661
+ * Only tables anchored at A1 are updated to the new dimension; tables
5662
+ * elsewhere are left untouched (moving them would corrupt the sheet).
5663
+ * Empty replacements (dimRef "A1") do not shrink multi-column tables.
5664
+ */
5665
+ _updateTableRefs(sheetName, dimRef) {
5666
+ const sheetPath = this.sheetNameToPath.get(sheetName);
5667
+ if (!sheetPath) return;
5668
+ const slash = sheetPath.lastIndexOf("/");
5669
+ const dir = slash >= 0 ? sheetPath.substring(0, slash + 1) : "";
5670
+ const base = slash >= 0 ? sheetPath.substring(slash + 1) : sheetPath;
5671
+ const sheetRelsPath = `${dir}_rels/${base}.rels`;
5672
+ const sheetRelsEntry = this.zip.getEntry(sheetRelsPath);
5673
+ const hasStagedRels = this.stagedParts.has(sheetRelsPath);
5674
+ if (!sheetRelsEntry && !hasStagedRels) return;
5675
+ const tableTargets = [];
5676
+ try {
5677
+ const relsXml = this._entryData(sheetRelsPath).toString("utf8");
5678
+ const relTagRe = /<Relationship\b[^>]*>/g;
5679
+ let m;
5680
+ while ((m = relTagRe.exec(relsXml)) !== null) {
5681
+ const attrs = parseTagAttributes(m[0]);
5682
+ const type = attrs["Type"] ?? attrs["type"] ?? "";
5683
+ const target = attrs["Target"] ?? attrs["target"] ?? "";
5684
+ if (!type.includes("/table") || !target) continue;
5685
+ let tablePath;
5686
+ if (target.startsWith("/")) {
5687
+ tablePath = resolveRelTarget(target);
5688
+ } else {
5689
+ const combined = dir + target;
5690
+ const parts = [];
5691
+ for (const seg of combined.split("/")) {
5692
+ if (seg === "..") parts.pop();
5693
+ else if (seg !== "." && seg !== "") parts.push(seg);
5694
+ }
5695
+ tablePath = parts.join("/");
5696
+ }
5697
+ tableTargets.push(tablePath);
5698
+ }
5699
+ } catch {
5700
+ return;
5701
+ }
5702
+ if (tableTargets.length === 0) return;
5703
+ const tables = [];
5704
+ for (const tablePath of tableTargets) {
5705
+ try {
5706
+ const hasEntry = !!this.zip.getEntry(tablePath);
5707
+ const hasStaged = this.stagedParts.has(tablePath);
5708
+ if (!hasEntry && !hasStaged) continue;
5709
+ const xml = this._entryData(tablePath).toString("utf8");
5710
+ const tableTagRe = /<(?:[A-Za-z_][\w.-]*:)?table\b[^>]*>/i;
5711
+ const tableMatch = tableTagRe.exec(xml);
5712
+ if (!tableMatch || tableMatch.index === void 0) continue;
5713
+ const refMatch = /\bref\s*=\s*("([^"]*)"|'([^']*)')/i.exec(tableMatch[0]);
5714
+ const ref = refMatch ? refMatch[2] ?? refMatch[3] ?? "" : "";
5715
+ tables.push({
5716
+ path: tablePath,
5717
+ xml,
5718
+ tag: tableMatch[0],
5719
+ tagIndex: tableMatch.index,
5720
+ ref,
5721
+ range: parseCellRange(ref),
5722
+ staged: hasStaged
5723
+ });
5724
+ } catch {
5725
+ }
5726
+ }
5727
+ if (tables.length === 0) return;
5728
+ const dimRange = parseCellRange(dimRef);
5729
+ if (!dimRange) return;
5730
+ for (const table of tables) {
5731
+ try {
5732
+ const origRange = table.range;
5733
+ if (!origRange) continue;
5734
+ if (origRange.r1 !== 1 || origRange.c1 !== 0) continue;
5735
+ if (dimRef === "A1" && !/^A1$/i.test(table.ref)) continue;
5736
+ const origCols = origRange.c2 - origRange.c1 + 1;
5737
+ const newCols = dimRange.c2 - dimRange.c1 + 1;
5738
+ if (newCols !== origCols) continue;
5739
+ let lastRow = dimRange.r2;
5740
+ for (const other of tables) {
5741
+ if (other === table || !other.range) continue;
5742
+ if (other.range.r1 <= 1) continue;
5743
+ if (other.range.c1 > origRange.c2 || other.range.c2 < origRange.c1) continue;
5744
+ lastRow = Math.min(lastRow, other.range.r1 - 1);
5745
+ }
5746
+ if (lastRow < 1) continue;
5747
+ const newRef = origRange.c2 === 0 && lastRow === 1 ? "A1" : `A1:${columnIndexToLetter(origRange.c2)}${lastRow}`;
5748
+ let xml = table.xml;
5749
+ const newTag = _XlsxUpdater._replaceOrAddXmlAttribute(table.tag, "ref", newRef);
5750
+ xml = xml.substring(0, table.tagIndex) + newTag + xml.substring(table.tagIndex + table.tag.length);
5751
+ const afRe = /<(?:[A-Za-z_][\w.-]*:)?autoFilter\b[^>]*>/i;
5752
+ const afMatch = afRe.exec(xml);
5753
+ if (afMatch && afMatch.index !== void 0) {
5754
+ const newAf = _XlsxUpdater._replaceOrAddXmlAttribute(afMatch[0], "ref", newRef);
5755
+ xml = xml.substring(0, afMatch.index) + newAf + xml.substring(afMatch.index + afMatch[0].length);
5756
+ }
5757
+ if (table.staged) {
5758
+ fs5.writeFileSync(this.stagedParts.get(table.path), xml, "utf8");
5759
+ } else {
5760
+ this.zip.updateFile(table.path, Buffer.from(xml, "utf8"));
5761
+ }
5762
+ } catch {
5763
+ }
5764
+ }
5765
+ }
5766
+ };
4414
5767
 
4415
5768
  // src/XlsbUpdater.ts
5769
+ import * as fs6 from "fs";
5770
+ import * as os2 from "os";
5771
+ import * as path7 from "path";
5772
+ import AdmZip3 from "adm-zip";
4416
5773
  var CELL_RECORDS = /* @__PURE__ */ new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
4417
5774
  var RK_INT_LOWER = -1 << 29;
4418
5775
  var RK_INT_UPPER = (1 << 29) - 1;
@@ -4420,7 +5777,9 @@ var XlsbUpdater = class {
4420
5777
  /**
4421
5778
  * @param path Path to an existing `.xlsb` file.
4422
5779
  */
4423
- constructor(path3) {
5780
+ constructor(filePath) {
5781
+ this.temporaryDirectory = null;
5782
+ this.stagedParts = /* @__PURE__ */ new Map();
4424
5783
  this.sheetNameToPath = /* @__PURE__ */ new Map();
4425
5784
  this.pivotCacheDefPaths = [];
4426
5785
  this.sharedStringsBuf = null;
@@ -4429,16 +5788,17 @@ var XlsbUpdater = class {
4429
5788
  this.sharedStringsUnique = 0;
4430
5789
  this.sharedStringsEndSst = 0;
4431
5790
  this.stringIndexMap = /* @__PURE__ */ new Map();
5791
+ this.sharedStringsDirty = false;
4432
5792
  // ------------------------------------------------------------------
4433
5793
  // Shared strings
4434
5794
  // ------------------------------------------------------------------
4435
5795
  this._appendCount = 0;
4436
- this.sourcePath = path3;
5796
+ this.sourcePath = filePath;
4437
5797
  this._oaEpoch = Date.UTC(1899, 11, 30);
4438
- if (!fs4.existsSync(path3)) {
4439
- throw new Error(`XlsbUpdater: file not found: ${path3}`);
5798
+ if (!fs6.existsSync(filePath)) {
5799
+ throw new Error(`XlsbUpdater: file not found: ${filePath}`);
4440
5800
  }
4441
- this.zip = new AdmZip3(path3);
5801
+ this.zip = new AdmZip3(filePath);
4442
5802
  if (!this.zip.getEntry("xl/workbook.bin")) {
4443
5803
  throw new Error(
4444
5804
  "XlsbUpdater: not an XLSB workbook (xl/workbook.bin missing). Convert the file to XLSB first."
@@ -4469,8 +5829,15 @@ var XlsbUpdater = class {
4469
5829
  if (!entry) {
4470
5830
  throw new Error(`XlsbUpdater: worksheet part missing for sheet "${sheetName}".`);
4471
5831
  }
4472
- const sheetBuf = entry.getData();
5832
+ const sheetBuf = this._entryData(sheetPath);
4473
5833
  this._ensureSharedStringsLoaded();
5834
+ if (this.sharedStringsBuf !== null) {
5835
+ this.sharedStringsTotal = Math.max(
5836
+ 0,
5837
+ this.sharedStringsTotal - this._sharedStringRefCount(sheetBuf)
5838
+ );
5839
+ this.sharedStringsDirty = true;
5840
+ }
4474
5841
  let dataStyles = /* @__PURE__ */ new Map();
4475
5842
  let headerStyles = /* @__PURE__ */ new Map();
4476
5843
  let dateXf = null;
@@ -4499,12 +5866,222 @@ var XlsbUpdater = class {
4499
5866
  }
4500
5867
  this._patchDimension(newSheet, lastRowIdx, lastCol);
4501
5868
  this.zip.updateFile(sheetPath, newSheet);
4502
- if (this.sharedStringsBuf !== null && this._appendCount > 0) {
5869
+ this._discardStagedPart(sheetPath);
5870
+ if (this.sharedStringsBuf !== null) {
4503
5871
  this._commitSharedStrings();
4504
5872
  }
4505
5873
  const rwLast = Math.max(0, rows.length + (headers ? 1 : 0) - 1);
4506
5874
  const colLast = lastCol;
4507
5875
  this._patchPivotCaches(sheetName, rwLast, colLast);
5876
+ this._removeCalcChain();
5877
+ }
5878
+ /** Replace worksheet rows from a one-pass source. */
5879
+ async replaceSheetDataStream(sheetName, rows, options = {}) {
5880
+ const { headers, styleFallback = "inherit" } = options;
5881
+ if (styleFallback !== "inherit" && styleFallback !== "general") {
5882
+ throw new Error("XlsbUpdater: styleFallback must be 'inherit' or 'general'.");
5883
+ }
5884
+ const sheetPath = this.sheetNameToPath.get(sheetName);
5885
+ if (!sheetPath) {
5886
+ throw new Error(`XlsbUpdater: sheet "${sheetName}" not found in the workbook.`);
5887
+ }
5888
+ const entry = this.zip.getEntry(sheetPath);
5889
+ if (!entry) {
5890
+ throw new Error(`XlsbUpdater: worksheet part missing for sheet "${sheetName}".`);
5891
+ }
5892
+ const oldStage = this.stagedParts.get(sheetPath);
5893
+ const beforeValuesLength = this.sharedStringsValues.length;
5894
+ const beforeAppendCount = this._appendCount;
5895
+ const beforeSharedStringsTotal = this.sharedStringsTotal;
5896
+ const beforeSharedStringsUnique = this.sharedStringsUnique;
5897
+ const beforeSharedStringsEndSst = this.sharedStringsEndSst;
5898
+ const beforeSharedStringsDirty = this.sharedStringsDirty;
5899
+ const beforeStringIndexMap = new Map(this.stringIndexMap);
5900
+ const sharedStringsEntry = this.zip.getEntry("xl/sharedStrings.bin");
5901
+ const beforeSharedStringsBytes = sharedStringsEntry || this.stagedParts.has("xl/sharedStrings.bin") ? Buffer.from(this._entryData("xl/sharedStrings.bin")) : null;
5902
+ const beforeSharedStringsBuf = this.sharedStringsBuf ? Buffer.from(this.sharedStringsBuf) : null;
5903
+ const oldSheet = this._entryData(sheetPath);
5904
+ const rowsPath = this._temporaryPath(".rows");
5905
+ const outputPath = this._temporaryPath(".bin");
5906
+ const createdParts = [rowsPath, outputPath];
5907
+ let output = null;
5908
+ try {
5909
+ this._ensureSharedStringsLoaded();
5910
+ if (this.sharedStringsBuf !== null) {
5911
+ this.sharedStringsTotal = Math.max(
5912
+ 0,
5913
+ this.sharedStringsTotal - this._sharedStringRefCount(oldSheet)
5914
+ );
5915
+ this.sharedStringsDirty = true;
5916
+ }
5917
+ let dataStyles = /* @__PURE__ */ new Map();
5918
+ let headerStyles = /* @__PURE__ */ new Map();
5919
+ let dateXf = null;
5920
+ if (styleFallback !== "general") {
5921
+ const collected = this._collectStyles(oldSheet);
5922
+ dataStyles = collected.dataStyles;
5923
+ headerStyles = collected.headerStyles;
5924
+ dateXf = this._findDateXf();
5925
+ }
5926
+ const rowOutput = fs6.createWriteStream(rowsPath);
5927
+ output = rowOutput;
5928
+ let outputError = null;
5929
+ rowOutput.on("error", (error) => {
5930
+ outputError = error;
5931
+ });
5932
+ let writtenBytes = 0;
5933
+ let lastKeptEnd = 0;
5934
+ let width = (headers == null ? void 0 : headers.length) ?? 0;
5935
+ let dataRowsSeen = 0;
5936
+ let keptDataRows = 0;
5937
+ let pendingEmptyWidth = 0;
5938
+ let nextRow = 0;
5939
+ const rowHeaderOffsets = [];
5940
+ const write = async (data) => {
5941
+ if (outputError) throw outputError;
5942
+ writtenBytes += data.length;
5943
+ if (rowOutput.write(data)) return;
5944
+ await new Promise((resolve6, reject) => {
5945
+ const onDrain = () => {
5946
+ cleanup();
5947
+ resolve6();
5948
+ };
5949
+ const onError = (error) => {
5950
+ cleanup();
5951
+ reject(error);
5952
+ };
5953
+ const cleanup = () => {
5954
+ rowOutput.off("drain", onDrain);
5955
+ rowOutput.off("error", onError);
5956
+ };
5957
+ rowOutput.once("drain", onDrain);
5958
+ rowOutput.once("error", onError);
5959
+ });
5960
+ };
5961
+ const writeRow = async (rowNumber, cells) => {
5962
+ rowHeaderOffsets.push(writtenBytes);
5963
+ const header = Buffer.alloc(27);
5964
+ header[0] = 0;
5965
+ header[1] = 25;
5966
+ header.writeInt32LE(rowNumber, 2);
5967
+ header[10] = 44;
5968
+ header[11] = 1;
5969
+ header[15] = 1;
5970
+ header.writeInt32LE(0, 19);
5971
+ header.writeInt32LE(0, 23);
5972
+ await write(header);
5973
+ for (const cell of cells) await write(cell);
5974
+ };
5975
+ if (headers !== void 0) {
5976
+ await writeRow(0, headers.map((value, column) => this._stringCellBytes(column, styleFallback === "general" ? 0 : headerStyles.get(column) ?? 0, value)));
5977
+ lastKeptEnd = writtenBytes;
5978
+ nextRow = 1;
5979
+ }
5980
+ for await (const sourceRow of rows) {
5981
+ if (typeof sourceRow === "string" || sourceRow instanceof Uint8Array) {
5982
+ throw new TypeError("Each row must be an iterable of cells, not text");
5983
+ }
5984
+ const row = Array.from(sourceRow);
5985
+ const cells = row.map((value, column) => value === null || value === void 0 ? null : this._valueCellBytes(value, column, dataStyles, dateXf)).filter((cell) => cell !== null);
5986
+ await writeRow(nextRow, cells);
5987
+ dataRowsSeen++;
5988
+ nextRow++;
5989
+ if (row.every((value) => value === null || value === void 0)) {
5990
+ pendingEmptyWidth = Math.max(pendingEmptyWidth, row.length);
5991
+ } else {
5992
+ width = Math.max(width, pendingEmptyWidth, row.length);
5993
+ pendingEmptyWidth = 0;
5994
+ keptDataRows = dataRowsSeen;
5995
+ lastKeptEnd = writtenBytes;
5996
+ }
5997
+ }
5998
+ await new Promise((resolve6, reject) => {
5999
+ rowOutput.once("close", resolve6);
6000
+ rowOutput.once("error", reject);
6001
+ rowOutput.end();
6002
+ });
6003
+ if (outputError) throw outputError;
6004
+ const rowsFd = fs6.openSync(rowsPath, "r+");
6005
+ try {
6006
+ fs6.ftruncateSync(rowsFd, lastKeptEnd);
6007
+ } finally {
6008
+ fs6.closeSync(rowsFd);
6009
+ }
6010
+ const lastCol = Math.max(0, width - 1);
6011
+ const rowPatchFd = fs6.openSync(rowsPath, "r+");
6012
+ try {
6013
+ const lastColBuffer = Buffer.alloc(4);
6014
+ lastColBuffer.writeInt32LE(lastCol, 0);
6015
+ for (const offset of rowHeaderOffsets) {
6016
+ if (offset >= lastKeptEnd) continue;
6017
+ fs6.writeSync(rowPatchFd, lastColBuffer, 0, lastColBuffer.length, offset + 23);
6018
+ }
6019
+ } finally {
6020
+ fs6.closeSync(rowPatchFd);
6021
+ }
6022
+ const { rowsStart, rowsEnd, autoFilterRanges } = this._findRowsRegion(oldSheet);
6023
+ const oldLength = oldSheet.length;
6024
+ const newRowsLength = fs6.statSync(rowsPath).size;
6025
+ const delta = newRowsLength - (rowsEnd - rowsStart);
6026
+ const outputFd = fs6.openSync(outputPath, "w");
6027
+ try {
6028
+ fs6.writeSync(outputFd, oldSheet, 0, rowsStart, null);
6029
+ const rowsFdRead = fs6.openSync(rowsPath, "r");
6030
+ try {
6031
+ const chunk = Buffer.alloc(1024 * 1024);
6032
+ let position = 0;
6033
+ while (position < newRowsLength) {
6034
+ const count = fs6.readSync(rowsFdRead, chunk, 0, Math.min(chunk.length, newRowsLength - position), position);
6035
+ if (count <= 0) throw new Error("XlsbUpdater: failed to read staged rows.");
6036
+ fs6.writeSync(outputFd, chunk, 0, count);
6037
+ position += count;
6038
+ }
6039
+ } finally {
6040
+ fs6.closeSync(rowsFdRead);
6041
+ }
6042
+ fs6.writeSync(outputFd, oldSheet, rowsEnd, oldLength - rowsEnd, rowsStart + newRowsLength);
6043
+ } finally {
6044
+ fs6.closeSync(outputFd);
6045
+ }
6046
+ this._patchSheetMetadataFile(
6047
+ outputPath,
6048
+ oldSheet,
6049
+ rowsEnd,
6050
+ delta,
6051
+ autoFilterRanges,
6052
+ Math.max(0, keptDataRows + (headers !== void 0 ? 1 : 0) - 1),
6053
+ lastCol
6054
+ );
6055
+ this._stagePart(sheetPath, outputPath);
6056
+ this._commitSharedStrings();
6057
+ this._patchPivotCaches(
6058
+ sheetName,
6059
+ Math.max(0, keptDataRows + (headers !== void 0 ? 1 : 0) - 1),
6060
+ lastCol
6061
+ );
6062
+ this._removeCalcChain();
6063
+ if (oldStage && oldStage !== outputPath) this._removeTemporaryFile(oldStage);
6064
+ createdParts.splice(createdParts.indexOf(outputPath), 1);
6065
+ } catch (error) {
6066
+ this.sharedStringsValues.length = beforeValuesLength;
6067
+ this._appendCount = beforeAppendCount;
6068
+ this.sharedStringsTotal = beforeSharedStringsTotal;
6069
+ this.sharedStringsUnique = beforeSharedStringsUnique;
6070
+ this.sharedStringsEndSst = beforeSharedStringsEndSst;
6071
+ this.sharedStringsDirty = beforeSharedStringsDirty;
6072
+ this.sharedStringsBuf = beforeSharedStringsBuf;
6073
+ this.stringIndexMap.clear();
6074
+ for (const [value, index] of beforeStringIndexMap) this.stringIndexMap.set(value, index);
6075
+ if (beforeSharedStringsBytes) {
6076
+ this.zip.updateFile("xl/sharedStrings.bin", beforeSharedStringsBytes);
6077
+ }
6078
+ if (oldStage) this.stagedParts.set(sheetPath, oldStage);
6079
+ else this.stagedParts.delete(sheetPath);
6080
+ throw error;
6081
+ } finally {
6082
+ if (output && !output.closed) output.destroy();
6083
+ for (const temporaryPath of createdParts) this._removeTemporaryFile(temporaryPath);
6084
+ }
4508
6085
  }
4509
6086
  /**
4510
6087
  * Write the updated workbook to disk.
@@ -4512,12 +6089,89 @@ var XlsbUpdater = class {
4512
6089
  */
4513
6090
  save(outputPath) {
4514
6091
  const target = outputPath ?? this.sourcePath;
4515
- fs4.writeFileSync(target, this.zip.toBuffer());
6092
+ this._materializeStagedParts();
6093
+ writeBufferAtomically(this.zip.toBuffer(), target);
6094
+ }
6095
+ /** Save staged streaming replacements without materialising the ZIP. */
6096
+ async saveStreaming(outputPath) {
6097
+ const target = outputPath ?? this.sourcePath;
6098
+ await writeAdmZipStreaming(this.zip, this.stagedParts, target);
6099
+ this._materializeStagedParts();
4516
6100
  }
4517
6101
  /** The updated workbook as an in-memory ZIP buffer. */
4518
6102
  toBuffer() {
6103
+ this._materializeStagedParts();
4519
6104
  return this.zip.toBuffer();
4520
6105
  }
6106
+ /**
6107
+ * Release the staging directory and any unsaved staged parts.
6108
+ * The updater remains usable; a new staging directory is created lazily
6109
+ * on the next streaming replacement.
6110
+ */
6111
+ dispose() {
6112
+ for (const stagedPath of this.stagedParts.values()) {
6113
+ try {
6114
+ fs6.rmSync(stagedPath, { force: true });
6115
+ } catch {
6116
+ }
6117
+ }
6118
+ this.stagedParts.clear();
6119
+ if (this.temporaryDirectory !== null) {
6120
+ try {
6121
+ fs6.rmSync(this.temporaryDirectory, { recursive: true, force: true });
6122
+ } catch {
6123
+ }
6124
+ this.temporaryDirectory = null;
6125
+ }
6126
+ }
6127
+ /** Support `using` declarations for automatic staging cleanup. */
6128
+ [Symbol.dispose]() {
6129
+ this.dispose();
6130
+ }
6131
+ _ensureTemporaryDirectory() {
6132
+ if (this.temporaryDirectory === null) {
6133
+ this.temporaryDirectory = fs6.mkdtempSync(path7.join(os2.tmpdir(), "spreadsheet-updater-"));
6134
+ } else {
6135
+ try {
6136
+ fs6.mkdirSync(this.temporaryDirectory, { recursive: true });
6137
+ } catch {
6138
+ }
6139
+ }
6140
+ return this.temporaryDirectory;
6141
+ }
6142
+ _temporaryPath(suffix) {
6143
+ return path7.join(this._ensureTemporaryDirectory(), `part-${Date.now()}-${Math.random().toString(16).slice(2)}${suffix}`);
6144
+ }
6145
+ _removeTemporaryFile(filePath) {
6146
+ try {
6147
+ fs6.rmSync(filePath);
6148
+ } catch (error) {
6149
+ if (error.code !== "ENOENT") throw error;
6150
+ }
6151
+ }
6152
+ _entryData(name) {
6153
+ const stagedPath = this.stagedParts.get(name);
6154
+ if (stagedPath) return fs6.readFileSync(stagedPath);
6155
+ const entry = this.zip.getEntry(name);
6156
+ if (!entry) throw new Error(`XlsbUpdater: ZIP member missing: ${name}`);
6157
+ return entry.getData();
6158
+ }
6159
+ _stagePart(name, filePath) {
6160
+ this.stagedParts.set(name, filePath);
6161
+ }
6162
+ _discardStagedPart(name) {
6163
+ const stagedPath = this.stagedParts.get(name);
6164
+ if (!stagedPath) return;
6165
+ this._removeTemporaryFile(stagedPath);
6166
+ this.stagedParts.delete(name);
6167
+ }
6168
+ _materializeStagedParts() {
6169
+ for (const [name, stagedPath] of this.stagedParts) {
6170
+ this.zip.updateFile(name, fs6.readFileSync(stagedPath));
6171
+ this._removeTemporaryFile(stagedPath);
6172
+ }
6173
+ this.stagedParts.clear();
6174
+ }
4521
6175
  // ------------------------------------------------------------------
4522
6176
  // Workbook structure
4523
6177
  // ------------------------------------------------------------------
@@ -4526,10 +6180,15 @@ var XlsbUpdater = class {
4526
6180
  const rIdToTarget = {};
4527
6181
  if (relsEntry) {
4528
6182
  const rels = relsEntry.getData().toString("utf8");
4529
- const relRegex = /<Relationship[^>]*Id="([^"]*)"[^>]*Target="([^"]*)"/g;
4530
- let match;
4531
- while ((match = relRegex.exec(rels)) !== null) {
4532
- rIdToTarget[match[1]] = match[2];
6183
+ const relTagRe = /<Relationship\b[^>]*>/g;
6184
+ let tagMatch;
6185
+ while ((tagMatch = relTagRe.exec(rels)) !== null) {
6186
+ const attrs = parseTagAttributes(tagMatch[0]);
6187
+ const id = attrs["Id"] ?? attrs["id"];
6188
+ const target = attrs["Target"] ?? attrs["target"];
6189
+ if (id !== void 0 && target !== void 0) {
6190
+ rIdToTarget[id] = target;
6191
+ }
4533
6192
  }
4534
6193
  }
4535
6194
  const wbEntry = this.zip.getEntry("xl/workbook.bin");
@@ -4549,12 +6208,24 @@ var XlsbUpdater = class {
4549
6208
  o += 4;
4550
6209
  const name = readUtf16(wbBuf, o, nameLen);
4551
6210
  let target = rIdToTarget[rId] ?? "";
4552
- if (target.startsWith("/")) target = target.substring(1);
4553
- else if (!target.startsWith("xl/")) target = "xl/" + target;
6211
+ target = resolveRelTarget(target);
4554
6212
  if (target) {
4555
6213
  this.sheetNameToPath.set(name, target);
4556
6214
  }
4557
6215
  }
6216
+ try {
6217
+ const rec1904 = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
6218
+ let p1904 = 0;
6219
+ while (readRecord(wbBuf, p1904, rec1904)) {
6220
+ if (rec1904.id === 153 && rec1904.len >= 4) {
6221
+ const is1904 = (wbBuf[rec1904.dataStart] & 1) !== 0;
6222
+ this._oaEpoch = is1904 ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
6223
+ break;
6224
+ }
6225
+ p1904 = rec1904.dataEnd;
6226
+ }
6227
+ } catch {
6228
+ }
4558
6229
  }
4559
6230
  for (const e of this.zip.getEntries()) {
4560
6231
  if (/^xl\/pivotCache\/pivotCacheDefinition\d*\.bin$/.test(e.entryName)) {
@@ -4567,11 +6238,11 @@ var XlsbUpdater = class {
4567
6238
  return;
4568
6239
  }
4569
6240
  const entry = this.zip.getEntry("xl/sharedStrings.bin");
4570
- if (!entry) {
6241
+ if (!entry && !this.stagedParts.has("xl/sharedStrings.bin")) {
4571
6242
  this.sharedStringsBuf = null;
4572
6243
  return;
4573
6244
  }
4574
- this.sharedStringsBuf = entry.getData();
6245
+ this.sharedStringsBuf = this._entryData("xl/sharedStrings.bin");
4575
6246
  const parsed = parseSharedStringsBin(this.sharedStringsBuf);
4576
6247
  this.sharedStringsValues = parsed.values;
4577
6248
  this.sharedStringsTotal = parsed.total;
@@ -4583,9 +6254,10 @@ var XlsbUpdater = class {
4583
6254
  }
4584
6255
  });
4585
6256
  this._appendCount = 0;
6257
+ this.sharedStringsDirty = false;
4586
6258
  }
4587
6259
  _commitSharedStrings() {
4588
- if (this.sharedStringsBuf === null || this._appendCount === 0) return;
6260
+ if (this.sharedStringsBuf === null || !this.sharedStringsDirty) return;
4589
6261
  const originalValuesLength = this.sharedStringsValues.length - this._appendCount;
4590
6262
  let newItems = Buffer.alloc(0);
4591
6263
  for (let i = originalValuesLength; i < this.sharedStringsValues.length; i++) {
@@ -4611,6 +6283,9 @@ var XlsbUpdater = class {
4611
6283
  pos = rec.dataEnd;
4612
6284
  }
4613
6285
  this.zip.updateFile("xl/sharedStrings.bin", out);
6286
+ this.sharedStringsBuf = out;
6287
+ this._appendCount = 0;
6288
+ this.sharedStringsDirty = false;
4614
6289
  }
4615
6290
  // ------------------------------------------------------------------
4616
6291
  // Styles
@@ -4659,27 +6334,15 @@ var XlsbUpdater = class {
4659
6334
  _findDateXf() {
4660
6335
  const entry = this.zip.getEntry("xl/styles.bin");
4661
6336
  if (!entry) return null;
4662
- const stylesBuf = entry.getData();
4663
- const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
4664
- let pos = 0;
4665
- let inCellXfs = false;
4666
- let xfIndex = 0;
4667
- while (readRecord(stylesBuf, pos, rec)) {
4668
- pos = rec.dataEnd;
4669
- if (rec.id === 617) {
4670
- inCellXfs = true;
4671
- xfIndex = 0;
4672
- continue;
4673
- }
4674
- if (rec.id === 618) {
4675
- inCellXfs = false;
4676
- continue;
4677
- }
4678
- if (inCellXfs && rec.id === 47 && rec.len >= 4) {
4679
- const numFmtId = stylesBuf.readUInt16LE(rec.dataStart + 2);
4680
- const isDate = numFmtId >= 14 && numFmtId <= 22 || numFmtId >= 45 && numFmtId <= 47;
4681
- if (isDate) return xfIndex;
4682
- xfIndex++;
6337
+ const reader = new BiffReaderWriter(entry.getData());
6338
+ while (reader.readStyles()) {
6339
+ }
6340
+ const numFmtIds = reader._xfIndexToNumFmtId;
6341
+ const customDates = reader._customNumFmts;
6342
+ for (let i = 0; i < numFmtIds.length; i++) {
6343
+ const numFmtId = numFmtIds[i];
6344
+ if (numFmtId >= 14 && numFmtId <= 22 || numFmtId >= 45 && numFmtId <= 47 || customDates.has(numFmtId)) {
6345
+ return i;
4683
6346
  }
4684
6347
  }
4685
6348
  return null;
@@ -4759,7 +6422,7 @@ var XlsbUpdater = class {
4759
6422
  return buildRecord(4, payload);
4760
6423
  }
4761
6424
  if (val instanceof Date) {
4762
- const oaDate = (val.getTime() - this._oaEpoch) / 864e5;
6425
+ const oaDate = timeToOaDate(val.getTime(), this._oaEpoch);
4763
6426
  if (Number.isFinite(oaDate)) {
4764
6427
  const style = colStyle > 0 ? colStyle : dateXf !== null ? dateXf : 0;
4765
6428
  const payload = Buffer.alloc(16);
@@ -4793,12 +6456,51 @@ var XlsbUpdater = class {
4793
6456
  this._appendCount++;
4794
6457
  }
4795
6458
  this.sharedStringsTotal++;
6459
+ this.sharedStringsDirty = true;
4796
6460
  const payload = Buffer.alloc(12);
4797
6461
  payload.writeUInt32LE(col, 0);
4798
6462
  payload.writeUInt32LE(style, 4);
4799
6463
  payload.writeUInt32LE(index, 8);
4800
6464
  return buildRecord(7, payload);
4801
6465
  }
6466
+ _sharedStringRefCount(sheetBuf) {
6467
+ const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
6468
+ let pos = 0;
6469
+ let count = 0;
6470
+ while (readRecord(sheetBuf, pos, rec)) {
6471
+ if (rec.id === 7) count++;
6472
+ pos = rec.dataEnd;
6473
+ }
6474
+ return count;
6475
+ }
6476
+ _patchSheetMetadataFile(filePath, originalSheet, rowsEnd, delta, autoFilterRanges, lastRowIdx, lastCol) {
6477
+ const fd = fs6.openSync(filePath, "r+");
6478
+ try {
6479
+ const patchInt32 = (offset, value) => {
6480
+ const bytes = Buffer.alloc(4);
6481
+ bytes.writeInt32LE(value, 0);
6482
+ fs6.writeSync(fd, bytes, 0, bytes.length, offset);
6483
+ };
6484
+ for (const filterOffset of autoFilterRanges) {
6485
+ const newOffset = filterOffset >= rowsEnd ? filterOffset + delta : filterOffset;
6486
+ patchInt32(newOffset + 4, lastRowIdx);
6487
+ patchInt32(newOffset + 12, lastCol);
6488
+ }
6489
+ const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
6490
+ let pos = 0;
6491
+ while (readRecord(originalSheet, pos, rec)) {
6492
+ if (rec.id === 148 && rec.len >= 16) {
6493
+ const shift = rec.dataStart >= rowsEnd ? delta : 0;
6494
+ patchInt32(rec.dataStart + shift + 4, lastRowIdx);
6495
+ patchInt32(rec.dataStart + shift + 12, lastCol);
6496
+ break;
6497
+ }
6498
+ pos = rec.dataEnd;
6499
+ }
6500
+ } finally {
6501
+ fs6.closeSync(fd);
6502
+ }
6503
+ }
4802
6504
  // ------------------------------------------------------------------
4803
6505
  // Rows region detection
4804
6506
  // ------------------------------------------------------------------
@@ -4829,8 +6531,8 @@ var XlsbUpdater = class {
4829
6531
  return { rowsStart, rowsEnd, autoFilterRanges };
4830
6532
  }
4831
6533
  /**
4832
- * Keep the sheet dimension record (0x98) in sync with the new data.
4833
- * Excel uses its rwLast (payload offset 24) and colLast (payload offset 32)
6534
+ * Keep the sheet dimension record (0x94) in sync with the new data.
6535
+ * Excel uses its rwLast (payload offset 4) and colLast (payload offset 12)
4834
6536
  * fields to read the full row range; stale values make the last written row
4835
6537
  * invisible to Excel (and drop it from pivot refreshes).
4836
6538
  */
@@ -4838,9 +6540,9 @@ var XlsbUpdater = class {
4838
6540
  const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
4839
6541
  let pos = 0;
4840
6542
  while (readRecord(sheetBuf, pos, rec)) {
4841
- if (rec.id === 152 && rec.len >= 36) {
4842
- sheetBuf.writeInt32LE(lastRowIdx, rec.dataStart + 24);
4843
- sheetBuf.writeInt32LE(lastCol, rec.dataStart + 32);
6543
+ if (rec.id === 148 && rec.len >= 16) {
6544
+ sheetBuf.writeInt32LE(lastRowIdx, rec.dataStart + 4);
6545
+ sheetBuf.writeInt32LE(lastCol, rec.dataStart + 12);
4844
6546
  return;
4845
6547
  }
4846
6548
  pos = rec.dataEnd;
@@ -4850,8 +6552,8 @@ var XlsbUpdater = class {
4850
6552
  // Pivot cache integration
4851
6553
  // ------------------------------------------------------------------
4852
6554
  _patchPivotCaches(sheetName, rwLast, colLast) {
4853
- for (const path3 of this.pivotCacheDefPaths) {
4854
- const entry = this.zip.getEntry(path3);
6555
+ for (const path9 of this.pivotCacheDefPaths) {
6556
+ const entry = this.zip.getEntry(path9);
4855
6557
  if (!entry) continue;
4856
6558
  const buf = Buffer.from(entry.getData());
4857
6559
  const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
@@ -4883,84 +6585,85 @@ var XlsbUpdater = class {
4883
6585
  }
4884
6586
  }
4885
6587
  if (patched) {
4886
- this.zip.updateFile(path3, buf);
6588
+ this.zip.updateFile(path9, buf);
4887
6589
  }
4888
6590
  }
4889
6591
  }
4890
- };
4891
-
4892
- // src/ExcelConverter.ts
4893
- import * as fs6 from "fs";
4894
- import * as path2 from "path";
4895
- import { randomUUID as randomUUID2 } from "crypto";
4896
- import { spawn } from "child_process";
4897
-
4898
- // src/atomicFile.ts
4899
- import * as fs5 from "fs";
4900
- import { randomUUID } from "crypto";
4901
- var realFileOperations = {
4902
- exists: (filePath) => fs5.existsSync(filePath),
4903
- rename: (sourcePath, destinationPath) => fs5.renameSync(sourcePath, destinationPath),
4904
- remove: (filePath) => fs5.rmSync(filePath)
4905
- };
4906
- function errorMessage(error) {
4907
- return error instanceof Error ? error.message : String(error);
4908
- }
4909
- function destinationExistsError(destinationPath) {
4910
- return new Error(
4911
- `Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${destinationPath}`
4912
- );
4913
- }
4914
- function installTemporaryOutput(temporaryPath, destinationPath, overwrite, operations = realFileOperations) {
4915
- const backupPath = `${destinationPath}.${randomUUID()}.replacement-backup`;
4916
- let backupCreated = false;
4917
- let installed = false;
4918
- if (operations.exists(destinationPath)) {
4919
- if (!overwrite) {
4920
- throw destinationExistsError(destinationPath);
4921
- }
4922
- operations.rename(destinationPath, backupPath);
4923
- backupCreated = true;
4924
- }
4925
- try {
6592
+ /**
6593
+ * Remove the stale calculation chain (xl/calcChain.bin). Excel rebuilds it
6594
+ * on open. Removes the part, its Content_Types override and its workbook
6595
+ * rels entry. XLSB tables are not patched (documented limitation).
6596
+ */
6597
+ _removeCalcChain() {
6598
+ const calcPath = "xl/calcChain.bin";
6599
+ if (!this.zip.getEntry(calcPath) && !this.stagedParts.has(calcPath)) return;
4926
6600
  try {
4927
- operations.rename(temporaryPath, destinationPath);
4928
- installed = true;
4929
- } catch (error) {
4930
- if (overwrite && !backupCreated && operations.exists(destinationPath)) {
4931
- operations.rename(destinationPath, backupPath);
4932
- backupCreated = true;
4933
- operations.rename(temporaryPath, destinationPath);
4934
- installed = true;
4935
- } else {
4936
- throw error;
6601
+ this.zip.deleteFile(calcPath);
6602
+ } catch {
6603
+ }
6604
+ const stagedCalc = this.stagedParts.get(calcPath);
6605
+ if (stagedCalc) {
6606
+ this.stagedParts.delete(calcPath);
6607
+ try {
6608
+ fs6.rmSync(stagedCalc, { force: true });
6609
+ } catch {
4937
6610
  }
4938
6611
  }
4939
- if (backupCreated) {
4940
- operations.remove(backupPath);
6612
+ const ctPath = "[Content_Types].xml";
6613
+ if (this.zip.getEntry(ctPath) || this.stagedParts.has(ctPath)) {
6614
+ try {
6615
+ const ct = this._entryData(ctPath).toString("utf8");
6616
+ const updated = ct.replace(
6617
+ /<Override\b[^>]*PartName="\/xl\/calcChain\.bin"[^>]*\/?>/gi,
6618
+ ""
6619
+ );
6620
+ if (updated !== ct) this._writePartText(ctPath, updated);
6621
+ } catch {
6622
+ }
4941
6623
  }
4942
- } catch (error) {
4943
- if (backupCreated && !installed) {
4944
- if (!operations.exists(destinationPath)) {
4945
- try {
4946
- operations.rename(backupPath, destinationPath);
4947
- backupCreated = false;
4948
- } catch (rollbackError) {
4949
- throw new Error(
4950
- `Excel conversion: final destination install failed (${errorMessage(error)}), and restoring the original destination also failed (${errorMessage(rollbackError)}). Original file remains at ${backupPath}.`
4951
- );
6624
+ const relsPath = "xl/_rels/workbook.bin.rels";
6625
+ if (this.zip.getEntry(relsPath) || this.stagedParts.has(relsPath)) {
6626
+ try {
6627
+ const rels = this._entryData(relsPath).toString("utf8");
6628
+ const relTagRe = /<Relationship\b[^>]*>/g;
6629
+ let m;
6630
+ let out = "";
6631
+ let last = 0;
6632
+ let changed = false;
6633
+ while ((m = relTagRe.exec(rels)) !== null) {
6634
+ const attrs = parseTagAttributes(m[0]);
6635
+ const type = attrs["Type"] ?? attrs["type"] ?? "";
6636
+ const target = attrs["Target"] ?? attrs["target"] ?? "";
6637
+ if (type.endsWith("/calcChain") && resolveRelTarget(target) === "xl/calcChain.bin") {
6638
+ out += rels.substring(last, m.index);
6639
+ last = m.index + m[0].length;
6640
+ changed = true;
6641
+ }
4952
6642
  }
4953
- } else {
4954
- throw new Error(
4955
- `Excel conversion: final destination install failed (${errorMessage(error)}). A concurrent destination was preserved; the original file remains at ${backupPath}.`
4956
- );
6643
+ if (changed) {
6644
+ out += rels.substring(last);
6645
+ this._writePartText(relsPath, out);
6646
+ }
6647
+ } catch {
4957
6648
  }
4958
6649
  }
4959
- throw error;
4960
6650
  }
4961
- }
6651
+ /** Write an XML/text part to its staged file when present, else into the ZIP. */
6652
+ _writePartText(name, text) {
6653
+ const stagedPath = this.stagedParts.get(name);
6654
+ if (stagedPath) {
6655
+ fs6.writeFileSync(stagedPath, text, "utf8");
6656
+ } else {
6657
+ this.zip.updateFile(name, Buffer.from(text, "utf8"));
6658
+ }
6659
+ }
6660
+ };
4962
6661
 
4963
6662
  // src/ExcelConverter.ts
6663
+ import * as fs7 from "fs";
6664
+ import * as path8 from "path";
6665
+ import { randomUUID as randomUUID5 } from "crypto";
6666
+ import { spawn } from "child_process";
4964
6667
  var POWERSHELL_CONVERSION_SCRIPT = `
4965
6668
  $ErrorActionPreference = 'Stop'
4966
6669
  $inputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_INPUT')
@@ -5005,29 +6708,29 @@ function errorMessage2(error) {
5005
6708
  return error instanceof Error ? error.message : String(error);
5006
6709
  }
5007
6710
  function validateConversionPaths(inputPath, outputPath, inputExtension, outputExtension, overwrite) {
5008
- const input = path2.resolve(inputPath);
5009
- const output = path2.resolve(outputPath);
5010
- if (path2.extname(input).toLowerCase() !== inputExtension) {
6711
+ const input = path8.resolve(inputPath);
6712
+ const output = path8.resolve(outputPath);
6713
+ if (path8.extname(input).toLowerCase() !== inputExtension) {
5011
6714
  throw new Error(
5012
6715
  `Excel conversion: source must have the ${inputExtension} extension: ${inputPath}`
5013
6716
  );
5014
6717
  }
5015
- if (path2.extname(output).toLowerCase() !== outputExtension) {
6718
+ if (path8.extname(output).toLowerCase() !== outputExtension) {
5016
6719
  throw new Error(
5017
6720
  `Excel conversion: destination must have the ${outputExtension} extension: ${outputPath}`
5018
6721
  );
5019
6722
  }
5020
- if (!fs6.existsSync(input)) {
6723
+ if (!fs7.existsSync(input)) {
5021
6724
  throw new Error(`Excel conversion: source file not found: ${inputPath}`);
5022
6725
  }
5023
- if (!fs6.statSync(input).isFile()) {
6726
+ if (!fs7.statSync(input).isFile()) {
5024
6727
  throw new Error(`Excel conversion: source path is not a file: ${inputPath}`);
5025
6728
  }
5026
- if (!fs6.existsSync(path2.dirname(output))) {
5027
- throw new Error(`Excel conversion: destination directory not found: ${path2.dirname(output)}`);
6729
+ if (!fs7.existsSync(path8.dirname(output))) {
6730
+ throw new Error(`Excel conversion: destination directory not found: ${path8.dirname(output)}`);
5028
6731
  }
5029
- if (fs6.existsSync(output)) {
5030
- if (fs6.statSync(output).isDirectory()) {
6732
+ if (fs7.existsSync(output)) {
6733
+ if (fs7.statSync(output).isDirectory()) {
5031
6734
  throw new Error(`Excel conversion: destination path is a directory: ${outputPath}`);
5032
6735
  }
5033
6736
  if (!overwrite) {
@@ -5049,7 +6752,7 @@ function runPowerShellConversion(inputPath, outputPath, fileFormat) {
5049
6752
  JUSTYBASE_EXCEL_OUTPUT: outputPath,
5050
6753
  JUSTYBASE_EXCEL_FORMAT: String(fileFormat)
5051
6754
  };
5052
- return new Promise((resolve2, reject) => {
6755
+ return new Promise((resolve6, reject) => {
5053
6756
  var _a, _b, _c, _d;
5054
6757
  let stdout = "";
5055
6758
  let stderr = "";
@@ -5084,7 +6787,7 @@ function runPowerShellConversion(inputPath, outputPath, fileFormat) {
5084
6787
  if (settled) return;
5085
6788
  settled = true;
5086
6789
  if (code === 0) {
5087
- resolve2();
6790
+ resolve6();
5088
6791
  return;
5089
6792
  }
5090
6793
  const details = (stderr || stdout).replace(/\s+/g, " ").trim();
@@ -5105,22 +6808,22 @@ async function convertExcelFile(inputPath, outputPath, inputExtension, outputExt
5105
6808
  if (process.platform !== "win32") {
5106
6809
  throw new Error("Excel conversion requires Windows with desktop Microsoft Excel and COM automation.");
5107
6810
  }
5108
- const temporaryOutput = path2.join(
5109
- path2.dirname(output),
5110
- `.${path2.basename(output, outputExtension)}.${randomUUID2()}.conversion${outputExtension}`
6811
+ const temporaryOutput = path8.join(
6812
+ path8.dirname(output),
6813
+ `.${path8.basename(output, outputExtension)}.${randomUUID5()}.conversion${outputExtension}`
5111
6814
  );
5112
6815
  try {
5113
6816
  await runPowerShellConversion(input, temporaryOutput, fileFormat);
5114
- if (!fs6.existsSync(temporaryOutput)) {
6817
+ if (!fs7.existsSync(temporaryOutput)) {
5115
6818
  throw new Error("Excel conversion completed without creating the destination file.");
5116
6819
  }
5117
6820
  installTemporaryOutput(temporaryOutput, output, options.overwrite === true);
5118
6821
  } catch (error) {
5119
6822
  throw new Error(`Excel conversion ${inputExtension} -> ${outputExtension}: ${errorMessage2(error)}`);
5120
6823
  } finally {
5121
- if (fs6.existsSync(temporaryOutput)) {
6824
+ if (fs7.existsSync(temporaryOutput)) {
5122
6825
  try {
5123
- fs6.rmSync(temporaryOutput);
6826
+ fs7.rmSync(temporaryOutput);
5124
6827
  } catch {
5125
6828
  }
5126
6829
  }
@@ -5138,22 +6841,38 @@ export {
5138
6841
  ExcelReaderAbstract,
5139
6842
  F,
5140
6843
  INVALID_SHEET_NAME_CHARS,
6844
+ MAX_CELL_CHARS,
6845
+ MAX_COLS,
6846
+ MAX_ROWS,
5141
6847
  ReaderFactory,
5142
6848
  StreamingSheetState,
5143
6849
  XlsbReader,
5144
6850
  XlsbUpdater,
5145
6851
  XlsbWriter,
6852
+ XlsxUpdater as XlsmUpdater,
5146
6853
  XlsxReader,
5147
6854
  XlsxUpdater,
5148
6855
  XlsxWriter,
5149
6856
  applyHeaderWidths,
6857
+ assertCellTextLength,
6858
+ assertColumnCount,
6859
+ assertRowCount,
6860
+ assertRowFitsColumns,
6861
+ columnIndexToLetter,
6862
+ columnLetterToIndex,
5150
6863
  convertXlsbToXlsx,
5151
6864
  convertXlsxToXlsb,
5152
6865
  defaultColWidth,
6866
+ escapeXmlText,
5153
6867
  getFormat,
5154
6868
  initColWidths,
5155
6869
  isFormattedCell,
6870
+ parseSharedStringsXml,
6871
+ parseTagAttributes,
6872
+ resolveRelTarget,
5156
6873
  sanitizeSheetName,
6874
+ trimTrailingEmptyRows,
6875
+ unescapeXml,
5157
6876
  unwrapCell,
5158
6877
  updateColWidthsFromRows
5159
6878
  };