@justybase/spreadsheet-tasks 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +154 -40
- package/browser/browser-spreadsheet.js +10 -3
- package/browser/justybase-spreadsheet.min.js +11 -11
- package/dist/cjs/index.d.ts +173 -3
- package/dist/cjs/index.js +1330 -55
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +1326 -55
- package/dist/esm/index.js.map +1 -1
- package/docs/API.md +209 -0
- package/examples/basic-read.ts +1 -1
- package/examples/basic-write.ts +1 -1
- package/examples/datetime-widths.ts +1 -1
- package/examples/large-dataset.ts +1 -1
- package/examples/multiple-sheets.ts +1 -1
- package/examples/streaming-example.ts +3 -1
- package/examples/update-existing.ts +53 -0
- package/package.json +6 -5
package/dist/esm/index.js
CHANGED
|
@@ -402,8 +402,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
|
|
|
402
402
|
this.xfIdToNumFmtId = [];
|
|
403
403
|
this.customDateFormats = null;
|
|
404
404
|
}
|
|
405
|
-
async open(
|
|
406
|
-
this.zip = new AdmZip(
|
|
405
|
+
async open(path3, readSharedStrings = true) {
|
|
406
|
+
this.zip = new AdmZip(path3);
|
|
407
407
|
const wbEntry = this.zip.getEntry("xl/workbook.bin");
|
|
408
408
|
if (wbEntry) {
|
|
409
409
|
const buf = wbEntry.getData();
|
|
@@ -448,8 +448,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
|
|
|
448
448
|
this.resultsCount = this.sheets.length;
|
|
449
449
|
this._currentSheetIndex = -1;
|
|
450
450
|
}
|
|
451
|
-
_readSharedStrings(
|
|
452
|
-
const entry = this.zip.getEntry(
|
|
451
|
+
_readSharedStrings(path3) {
|
|
452
|
+
const entry = this.zip.getEntry(path3);
|
|
453
453
|
if (!entry) return;
|
|
454
454
|
const reader = new BiffReaderWriter(entry.getData());
|
|
455
455
|
while (reader.readSharedStrings()) {
|
|
@@ -458,8 +458,8 @@ var XlsbReader = class extends ExcelReaderAbstract {
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
|
-
_readStyles(
|
|
462
|
-
const entry = this.zip.getEntry(
|
|
461
|
+
_readStyles(path3) {
|
|
462
|
+
const entry = this.zip.getEntry(path3);
|
|
463
463
|
if (!entry) return;
|
|
464
464
|
const reader = new BiffReaderWriter(entry.getData());
|
|
465
465
|
while (reader.readStyles()) {
|
|
@@ -549,6 +549,93 @@ var XlsbReader = class extends ExcelReaderAbstract {
|
|
|
549
549
|
|
|
550
550
|
// src/XlsxReader.ts
|
|
551
551
|
import yauzl from "yauzl";
|
|
552
|
+
|
|
553
|
+
// src/xmlUtils.ts
|
|
554
|
+
function hasInvalidControlChars(str) {
|
|
555
|
+
for (let i = 0; i < str.length; i++) {
|
|
556
|
+
const c = str.charCodeAt(i);
|
|
557
|
+
if (c < 32 && c !== 9 && c !== 10 && c !== 13) {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
function stripInvalidControlChars(str) {
|
|
564
|
+
let out = "";
|
|
565
|
+
for (let i = 0; i < str.length; i++) {
|
|
566
|
+
const c = str.charCodeAt(i);
|
|
567
|
+
if (c === 9 || c === 10 || c === 13 || c >= 32 && c <= 55295 || c >= 57344 && c <= 65533) {
|
|
568
|
+
out += str[i];
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
function escapeXmlText(str) {
|
|
574
|
+
if (!/&|<|>|"|'/.test(str) && !hasInvalidControlChars(str)) {
|
|
575
|
+
return str;
|
|
576
|
+
}
|
|
577
|
+
let out = str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
578
|
+
if (hasInvalidControlChars(out)) {
|
|
579
|
+
out = stripInvalidControlChars(out);
|
|
580
|
+
}
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
function unescapeXml(str) {
|
|
584
|
+
if (!str || str.indexOf("&") === -1) return str;
|
|
585
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
586
|
+
}
|
|
587
|
+
function columnIndexToLetter(colIndex) {
|
|
588
|
+
let n = colIndex + 1;
|
|
589
|
+
let result = "";
|
|
590
|
+
while (n > 0) {
|
|
591
|
+
const rem = (n - 1) % 26;
|
|
592
|
+
result = String.fromCharCode(65 + rem) + result;
|
|
593
|
+
n = Math.floor((n - 1) / 26);
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
function columnLetterToIndex(letter) {
|
|
598
|
+
let column = 0;
|
|
599
|
+
const length = letter.length;
|
|
600
|
+
for (let i = 0; i < length; i++) {
|
|
601
|
+
column += (letter.charCodeAt(i) - 64) * Math.pow(26, length - i - 1);
|
|
602
|
+
}
|
|
603
|
+
return column - 1;
|
|
604
|
+
}
|
|
605
|
+
function trimTrailingEmptyRows(rows) {
|
|
606
|
+
let end = rows.length;
|
|
607
|
+
while (end > 0 && rows[end - 1].every((v) => v === null || v === void 0)) {
|
|
608
|
+
end--;
|
|
609
|
+
}
|
|
610
|
+
return end === rows.length ? rows : rows.slice(0, end);
|
|
611
|
+
}
|
|
612
|
+
function parseSharedStringsXml(xml) {
|
|
613
|
+
const result = [];
|
|
614
|
+
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;
|
|
631
|
+
}
|
|
632
|
+
result.push(unescapeXml(val));
|
|
633
|
+
pos = siEnd + 5;
|
|
634
|
+
}
|
|
635
|
+
return result;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/XlsxReader.ts
|
|
552
639
|
var XlsxReader = class extends ExcelReaderAbstract {
|
|
553
640
|
constructor() {
|
|
554
641
|
super();
|
|
@@ -564,9 +651,9 @@ var XlsxReader = class extends ExcelReaderAbstract {
|
|
|
564
651
|
this.xfIdToNumFmtId = [];
|
|
565
652
|
this.customDateFormats = /* @__PURE__ */ new Set();
|
|
566
653
|
}
|
|
567
|
-
async open(
|
|
568
|
-
return new Promise((
|
|
569
|
-
yauzl.open(
|
|
654
|
+
async open(path3, readSharedStrings = true) {
|
|
655
|
+
return new Promise((resolve2, reject) => {
|
|
656
|
+
yauzl.open(path3, { lazyEntries: true, autoClose: false }, async (err, zipfile) => {
|
|
570
657
|
if (err) return reject(err);
|
|
571
658
|
this.zipfile = zipfile;
|
|
572
659
|
this.entries = /* @__PURE__ */ new Map();
|
|
@@ -590,7 +677,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
|
|
|
590
677
|
const sheetRegex = /<sheet[^>]*name="([^"]*)"[^>]*sheetId="([^"]*)"[^>]*r:id="([^"]*)"/g;
|
|
591
678
|
let match;
|
|
592
679
|
while ((match = sheetRegex.exec(wbContent)) !== null) {
|
|
593
|
-
const name =
|
|
680
|
+
const name = unescapeXml(match[1]);
|
|
594
681
|
const sheetId = match[2];
|
|
595
682
|
const rId = match[3];
|
|
596
683
|
const target = rIdToTarget[rId];
|
|
@@ -614,7 +701,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
|
|
|
614
701
|
}
|
|
615
702
|
this.resultsCount = this.sheets.length;
|
|
616
703
|
this._currentSheetIndex = -1;
|
|
617
|
-
|
|
704
|
+
resolve2();
|
|
618
705
|
} catch (e) {
|
|
619
706
|
reject(e);
|
|
620
707
|
}
|
|
@@ -628,48 +715,23 @@ var XlsxReader = class extends ExcelReaderAbstract {
|
|
|
628
715
|
this.zipfile.close();
|
|
629
716
|
}
|
|
630
717
|
}
|
|
631
|
-
async _readZipEntryContent(
|
|
632
|
-
const entry = this.entries.get(
|
|
718
|
+
async _readZipEntryContent(path3) {
|
|
719
|
+
const entry = this.entries.get(path3);
|
|
633
720
|
if (!entry) return null;
|
|
634
|
-
return new Promise((
|
|
721
|
+
return new Promise((resolve2, reject) => {
|
|
635
722
|
this.zipfile.openReadStream(entry, (err, readStream) => {
|
|
636
723
|
if (err) return reject(err);
|
|
637
724
|
const chunks = [];
|
|
638
725
|
readStream.on("data", (chunk) => chunks.push(chunk));
|
|
639
726
|
readStream.on("end", () => {
|
|
640
|
-
|
|
727
|
+
resolve2(Buffer.concat(chunks).toString("utf8"));
|
|
641
728
|
});
|
|
642
729
|
readStream.on("error", reject);
|
|
643
730
|
});
|
|
644
731
|
});
|
|
645
732
|
}
|
|
646
|
-
_unescapeXml(str) {
|
|
647
|
-
if (!str) return "";
|
|
648
|
-
if (str.indexOf("&") === -1) return str;
|
|
649
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
650
|
-
}
|
|
651
733
|
_parseSharedStrings(xml) {
|
|
652
|
-
|
|
653
|
-
while (true) {
|
|
654
|
-
const siStart = xml.indexOf("<si>", pos);
|
|
655
|
-
if (siStart === -1) break;
|
|
656
|
-
const siEnd = xml.indexOf("</si>", siStart);
|
|
657
|
-
if (siEnd === -1) break;
|
|
658
|
-
const content = xml.substring(siStart, siEnd);
|
|
659
|
-
let val = "";
|
|
660
|
-
let tPos = 0;
|
|
661
|
-
while (true) {
|
|
662
|
-
const tStart = content.indexOf("<t", tPos);
|
|
663
|
-
if (tStart === -1) break;
|
|
664
|
-
const tagEnd = content.indexOf(">", tStart);
|
|
665
|
-
const tEnd = content.indexOf("</t>", tagEnd);
|
|
666
|
-
if (tEnd === -1) break;
|
|
667
|
-
val += content.substring(tagEnd + 1, tEnd);
|
|
668
|
-
tPos = tEnd + 4;
|
|
669
|
-
}
|
|
670
|
-
this.sharedStrings.push(this._unescapeXml(val));
|
|
671
|
-
pos = siEnd + 5;
|
|
672
|
-
}
|
|
734
|
+
this.sharedStrings = parseSharedStringsXml(xml);
|
|
673
735
|
}
|
|
674
736
|
_parseStyles(xml) {
|
|
675
737
|
const numFmtRegex = /<numFmt\s+[^>]*numFmtId="(\d+)"[^>]*formatCode="([^"]*)"/g;
|
|
@@ -760,7 +822,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
|
|
|
760
822
|
if (key === "r") {
|
|
761
823
|
let letterLen = 0;
|
|
762
824
|
while (letterLen < val2.length && val2.charCodeAt(letterLen) >= 65) letterLen++;
|
|
763
|
-
colIndex =
|
|
825
|
+
colIndex = columnLetterToIndex(val2.substring(0, letterLen));
|
|
764
826
|
} else if (key === "t") {
|
|
765
827
|
type = val2;
|
|
766
828
|
} else if (key === "s") {
|
|
@@ -803,7 +865,7 @@ var XlsxReader = class extends ExcelReaderAbstract {
|
|
|
803
865
|
} else if (type === "b") {
|
|
804
866
|
finalVal = val === "1" || val === "true";
|
|
805
867
|
} else if (type === "inlineStr") {
|
|
806
|
-
finalVal =
|
|
868
|
+
finalVal = unescapeXml(val);
|
|
807
869
|
} else if (type === "n" || type === "") {
|
|
808
870
|
finalVal = parseFloat(val);
|
|
809
871
|
if (styleIndex > 0 && this.xfIdToNumFmtId) {
|
|
@@ -827,14 +889,6 @@ var XlsxReader = class extends ExcelReaderAbstract {
|
|
|
827
889
|
}
|
|
828
890
|
}
|
|
829
891
|
}
|
|
830
|
-
_columnLetterToIndex(letter) {
|
|
831
|
-
let column = 0;
|
|
832
|
-
const length = letter.length;
|
|
833
|
-
for (let i = 0; i < length; i++) {
|
|
834
|
-
column += (letter.charCodeAt(i) - 64) * Math.pow(26, length - i - 1);
|
|
835
|
-
}
|
|
836
|
-
return column - 1;
|
|
837
|
-
}
|
|
838
892
|
getValue(i) {
|
|
839
893
|
if (i < 0 || i >= this._currentRow.length) return null;
|
|
840
894
|
return this._currentRow[i];
|
|
@@ -3276,7 +3330,7 @@ var XlsbWriter = class _XlsbWriter {
|
|
|
3276
3330
|
* Must be awaited before the file is fully written.
|
|
3277
3331
|
*/
|
|
3278
3332
|
finalize() {
|
|
3279
|
-
return new Promise((
|
|
3333
|
+
return new Promise((resolve2, reject) => {
|
|
3280
3334
|
try {
|
|
3281
3335
|
this.saveSst();
|
|
3282
3336
|
this.archive.append(this._buildStylesBin(), { name: "xl/styles.bin" });
|
|
@@ -3374,7 +3428,7 @@ var XlsbWriter = class _XlsbWriter {
|
|
|
3374
3428
|
</Relationships>`;
|
|
3375
3429
|
this.archive.append(wsRels, { name: `xl/worksheets/_rels/${sheet.nameInArchive}.rels` });
|
|
3376
3430
|
}
|
|
3377
|
-
this.output.on("close", () =>
|
|
3431
|
+
this.output.on("close", () => resolve2());
|
|
3378
3432
|
this.archive.on("error", (err) => reject(err));
|
|
3379
3433
|
this.archive.finalize();
|
|
3380
3434
|
} catch (err) {
|
|
@@ -3722,7 +3776,7 @@ var XlsxWriter = class {
|
|
|
3722
3776
|
}
|
|
3723
3777
|
/** Finalize the ZIP package and close the output stream. */
|
|
3724
3778
|
finalize() {
|
|
3725
|
-
return new Promise((
|
|
3779
|
+
return new Promise((resolve2, reject) => {
|
|
3726
3780
|
try {
|
|
3727
3781
|
this._writeSharedStrings();
|
|
3728
3782
|
this._writeStyles();
|
|
@@ -3730,7 +3784,7 @@ var XlsxWriter = class {
|
|
|
3730
3784
|
this._writeContentTypes();
|
|
3731
3785
|
this._writeRels();
|
|
3732
3786
|
this.output.on("close", () => {
|
|
3733
|
-
|
|
3787
|
+
resolve2();
|
|
3734
3788
|
});
|
|
3735
3789
|
this.archive.on("error", (err) => {
|
|
3736
3790
|
reject(err);
|
|
@@ -3865,6 +3919,1219 @@ ${xfEntries.join("\n")}
|
|
|
3865
3919
|
this.archive.append(wbRels, { name: "xl/_rels/workbook.xml.rels" });
|
|
3866
3920
|
}
|
|
3867
3921
|
};
|
|
3922
|
+
|
|
3923
|
+
// src/XlsxUpdater.ts
|
|
3924
|
+
import * as fs3 from "fs";
|
|
3925
|
+
import AdmZip2 from "adm-zip";
|
|
3926
|
+
var XlsxUpdater = class {
|
|
3927
|
+
/**
|
|
3928
|
+
* @param path Path to an existing `.xlsx` file.
|
|
3929
|
+
*/
|
|
3930
|
+
constructor(path3) {
|
|
3931
|
+
this.sheetNameToPath = /* @__PURE__ */ new Map();
|
|
3932
|
+
this.pivotCacheDefs = [];
|
|
3933
|
+
this.pivotTableDefPaths = [];
|
|
3934
|
+
this.sharedStringsXml = null;
|
|
3935
|
+
this.sharedStringsValues = [];
|
|
3936
|
+
this.sharedStringsOriginalLength = 0;
|
|
3937
|
+
this.sharedStringsCount = 0;
|
|
3938
|
+
this.stringIndexMap = /* @__PURE__ */ new Map();
|
|
3939
|
+
this.sourcePath = path3;
|
|
3940
|
+
this._oaEpoch = Date.UTC(1899, 11, 30);
|
|
3941
|
+
if (!fs3.existsSync(path3)) {
|
|
3942
|
+
throw new Error(`XlsxUpdater: file not found: ${path3}`);
|
|
3943
|
+
}
|
|
3944
|
+
this.zip = new AdmZip2(path3);
|
|
3945
|
+
if (this.zip.getEntry("xl/workbook.bin")) {
|
|
3946
|
+
throw new Error(
|
|
3947
|
+
"XlsxUpdater: XLSB files are not supported yet. Convert the workbook to XLSX first."
|
|
3948
|
+
);
|
|
3949
|
+
}
|
|
3950
|
+
if (!this.zip.getEntry("xl/workbook.xml")) {
|
|
3951
|
+
throw new Error("XlsxUpdater: not a valid XLSX workbook (xl/workbook.xml missing).");
|
|
3952
|
+
}
|
|
3953
|
+
this._loadWorkbookStructure();
|
|
3954
|
+
}
|
|
3955
|
+
/** Worksheet names in workbook order. */
|
|
3956
|
+
getSheetNames() {
|
|
3957
|
+
return [...this.sheetNameToPath.keys()];
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* Clear the entire cell data of the target sheet and write new rows in its place.
|
|
3961
|
+
* All other parts of the workbook (pivot tables, other sheets, styles, …) are preserved.
|
|
3962
|
+
*
|
|
3963
|
+
* @param sheetName Name of the worksheet to replace.
|
|
3964
|
+
* @param rows New data rows (`CellValue` cells; null/undefined = blank).
|
|
3965
|
+
* @param options See {@link ReplaceSheetDataOptions}.
|
|
3966
|
+
*/
|
|
3967
|
+
replaceSheetData(sheetName, rows, options = {}) {
|
|
3968
|
+
const { headers, styleFallback = "inherit" } = options;
|
|
3969
|
+
rows = trimTrailingEmptyRows(rows);
|
|
3970
|
+
const sheetPath = this.sheetNameToPath.get(sheetName);
|
|
3971
|
+
if (!sheetPath) {
|
|
3972
|
+
throw new Error(`XlsxUpdater: sheet "${sheetName}" not found in the workbook.`);
|
|
3973
|
+
}
|
|
3974
|
+
const entry = this.zip.getEntry(sheetPath);
|
|
3975
|
+
if (!entry) {
|
|
3976
|
+
throw new Error(`XlsxUpdater: worksheet part missing for sheet "${sheetName}".`);
|
|
3977
|
+
}
|
|
3978
|
+
const sheetXml = entry.getData().toString("utf8");
|
|
3979
|
+
this._ensureSharedStringsLoaded();
|
|
3980
|
+
let colStyles = /* @__PURE__ */ new Map();
|
|
3981
|
+
let headerStyles = /* @__PURE__ */ new Map();
|
|
3982
|
+
let dateStyle = null;
|
|
3983
|
+
if (styleFallback !== "general") {
|
|
3984
|
+
const collected = this._collectExistingStyles(sheetXml);
|
|
3985
|
+
colStyles = collected.dataStyles;
|
|
3986
|
+
headerStyles = collected.headerStyles;
|
|
3987
|
+
dateStyle = this._findDateStyleIndex();
|
|
3988
|
+
}
|
|
3989
|
+
const lastCol = Math.max(rows.reduce((m, r) => Math.max(m, r.length), 0), (headers == null ? void 0 : headers.length) ?? 0) - 1;
|
|
3990
|
+
const totalRows = rows.length + (headers ? 1 : 0);
|
|
3991
|
+
const newSheetData = this._buildSheetData(rows, headers, colStyles, headerStyles, dateStyle);
|
|
3992
|
+
const newXml = this._patchSheetXml(sheetXml, newSheetData, totalRows, lastCol);
|
|
3993
|
+
this.zip.updateFile(sheetPath, Buffer.from(newXml, "utf8"));
|
|
3994
|
+
if (this.sharedStringsXml !== null) {
|
|
3995
|
+
this._commitSharedStrings();
|
|
3996
|
+
}
|
|
3997
|
+
const dimRef = totalRows > 0 && lastCol >= 0 ? `A1:${columnIndexToLetter(lastCol)}${totalRows}` : "A1";
|
|
3998
|
+
this._updatePivotCaches(sheetName, dimRef, rows.length);
|
|
3999
|
+
this._addRefreshOnLoadToPivotTables();
|
|
4000
|
+
}
|
|
4001
|
+
/**
|
|
4002
|
+
* Write the updated workbook to disk.
|
|
4003
|
+
* @param outputPath Destination path; defaults to the source file (overwrites in place).
|
|
4004
|
+
*/
|
|
4005
|
+
save(outputPath) {
|
|
4006
|
+
const target = outputPath ?? this.sourcePath;
|
|
4007
|
+
fs3.writeFileSync(target, this.zip.toBuffer());
|
|
4008
|
+
}
|
|
4009
|
+
/** The updated workbook as an in-memory ZIP buffer. */
|
|
4010
|
+
toBuffer() {
|
|
4011
|
+
return this.zip.toBuffer();
|
|
4012
|
+
}
|
|
4013
|
+
// ------------------------------------------------------------------
|
|
4014
|
+
// Workbook structure
|
|
4015
|
+
// ------------------------------------------------------------------
|
|
4016
|
+
_loadWorkbookStructure() {
|
|
4017
|
+
const relsEntry = this.zip.getEntry("xl/_rels/workbook.xml.rels");
|
|
4018
|
+
const rIdToTarget = {};
|
|
4019
|
+
if (relsEntry) {
|
|
4020
|
+
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];
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
const wbEntry = this.zip.getEntry("xl/workbook.xml");
|
|
4028
|
+
const wbXml = wbEntry ? wbEntry.getData().toString("utf8") : "";
|
|
4029
|
+
const sheetRe = /<sheet\b[^>]*>/g;
|
|
4030
|
+
let match;
|
|
4031
|
+
while ((match = sheetRe.exec(wbXml)) !== null) {
|
|
4032
|
+
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/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
|
+
}
|
|
4044
|
+
if (fullPath) {
|
|
4045
|
+
this.sheetNameToPath.set(name, fullPath);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
for (const entry of this.zip.getEntries()) {
|
|
4049
|
+
const name = entry.entryName;
|
|
4050
|
+
if (/^xl\/pivotCache\/pivotCacheDefinition\d*\.xml$/.test(name)) {
|
|
4051
|
+
this.pivotCacheDefs.push({ path: name, xml: entry.getData().toString("utf8") });
|
|
4052
|
+
} else if (/^xl\/pivotTables\/pivotTable\d*\.xml$/.test(name)) {
|
|
4053
|
+
this.pivotTableDefPaths.push(name);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
// ------------------------------------------------------------------
|
|
4058
|
+
// Shared strings
|
|
4059
|
+
// ------------------------------------------------------------------
|
|
4060
|
+
_ensureSharedStringsLoaded() {
|
|
4061
|
+
if (this.stringIndexMap.size > 0 || this.sharedStringsXml !== null) {
|
|
4062
|
+
return;
|
|
4063
|
+
}
|
|
4064
|
+
const entry = this.zip.getEntry("xl/sharedStrings.xml");
|
|
4065
|
+
if (!entry) {
|
|
4066
|
+
this.sharedStringsXml = null;
|
|
4067
|
+
return;
|
|
4068
|
+
}
|
|
4069
|
+
this.sharedStringsXml = entry.getData().toString("utf8");
|
|
4070
|
+
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;
|
|
4074
|
+
this.sharedStringsValues.forEach((value, index) => {
|
|
4075
|
+
if (!this.stringIndexMap.has(value)) {
|
|
4076
|
+
this.stringIndexMap.set(value, index);
|
|
4077
|
+
}
|
|
4078
|
+
});
|
|
4079
|
+
this.sharedStringsOriginalLength = this.sharedStringsValues.length;
|
|
4080
|
+
}
|
|
4081
|
+
_commitSharedStrings() {
|
|
4082
|
+
if (this.sharedStringsXml === null || this.sharedStringsValues.length === this.sharedStringsOriginalLength) {
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
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;
|
|
4096
|
+
}
|
|
4097
|
+
let appended = "";
|
|
4098
|
+
for (let i = this.sharedStringsOriginalLength; i < this.sharedStringsValues.length; i++) {
|
|
4099
|
+
const txt = this.sharedStringsValues[i];
|
|
4100
|
+
const clean = escapeXmlText(txt);
|
|
4101
|
+
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);
|
|
4105
|
+
this.zip.updateFile("xl/sharedStrings.xml", Buffer.from(xml, "utf8"));
|
|
4106
|
+
}
|
|
4107
|
+
// ------------------------------------------------------------------
|
|
4108
|
+
// Styles
|
|
4109
|
+
// ------------------------------------------------------------------
|
|
4110
|
+
_collectExistingStyles(sheetXml) {
|
|
4111
|
+
const dataCounts = /* @__PURE__ */ new Map();
|
|
4112
|
+
const headerStyles = /* @__PURE__ */ new Map();
|
|
4113
|
+
const sdStart = sheetXml.indexOf("<sheetData>");
|
|
4114
|
+
if (sdStart === -1) return { dataStyles: /* @__PURE__ */ new Map(), headerStyles };
|
|
4115
|
+
const sdEnd = sheetXml.indexOf("</sheetData>", sdStart);
|
|
4116
|
+
if (sdEnd === -1) return { dataStyles: /* @__PURE__ */ new Map(), headerStyles };
|
|
4117
|
+
const region = sheetXml.substring(sdStart, sdEnd);
|
|
4118
|
+
const cellRe = /<c\b[^>]*>/g;
|
|
4119
|
+
let m;
|
|
4120
|
+
while ((m = cellRe.exec(region)) !== null) {
|
|
4121
|
+
const tag = m[0];
|
|
4122
|
+
const rMatch = /r="([A-Z]+)(\d+)"/.exec(tag);
|
|
4123
|
+
if (!rMatch) continue;
|
|
4124
|
+
const col = columnLetterToIndex(rMatch[1]);
|
|
4125
|
+
const row = parseInt(rMatch[2], 10);
|
|
4126
|
+
const sMatch = /s="(\d+)"/.exec(tag);
|
|
4127
|
+
if (!sMatch) continue;
|
|
4128
|
+
const s = parseInt(sMatch[1], 10);
|
|
4129
|
+
if (s <= 0) continue;
|
|
4130
|
+
if (row === 1) {
|
|
4131
|
+
if (!headerStyles.has(col)) headerStyles.set(col, s);
|
|
4132
|
+
continue;
|
|
4133
|
+
}
|
|
4134
|
+
let colCounts = dataCounts.get(col);
|
|
4135
|
+
if (!colCounts) {
|
|
4136
|
+
colCounts = /* @__PURE__ */ new Map();
|
|
4137
|
+
dataCounts.set(col, colCounts);
|
|
4138
|
+
}
|
|
4139
|
+
colCounts.set(s, (colCounts.get(s) ?? 0) + 1);
|
|
4140
|
+
}
|
|
4141
|
+
const dataStyles = /* @__PURE__ */ new Map();
|
|
4142
|
+
for (const [col, colCounts] of dataCounts) {
|
|
4143
|
+
let best = 0;
|
|
4144
|
+
let bestCount = -1;
|
|
4145
|
+
for (const [s, count] of colCounts) {
|
|
4146
|
+
if (count > bestCount) {
|
|
4147
|
+
best = s;
|
|
4148
|
+
bestCount = count;
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
if (bestCount > 0) dataStyles.set(col, best);
|
|
4152
|
+
}
|
|
4153
|
+
return { dataStyles, headerStyles };
|
|
4154
|
+
}
|
|
4155
|
+
_findDateStyleIndex() {
|
|
4156
|
+
const entry = this.zip.getEntry("xl/styles.xml");
|
|
4157
|
+
if (!entry) return null;
|
|
4158
|
+
const xml = entry.getData().toString("utf8");
|
|
4159
|
+
const customDateFormats = /* @__PURE__ */ new Set();
|
|
4160
|
+
const numFmtRe = /<numFmt\s+[^>]*numFmtId="(\d+)"[^>]*formatCode="([^"]*)"/g;
|
|
4161
|
+
let m;
|
|
4162
|
+
while ((m = numFmtRe.exec(xml)) !== null) {
|
|
4163
|
+
const id = parseInt(m[1], 10);
|
|
4164
|
+
const code = m[2].toLowerCase();
|
|
4165
|
+
if (code.includes("yy") || code.includes("mm") || code.includes("dd") || code.includes("h:mm")) {
|
|
4166
|
+
customDateFormats.add(id);
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
const cellXfsStart = xml.indexOf("<cellXfs");
|
|
4170
|
+
if (cellXfsStart === -1) return null;
|
|
4171
|
+
const cellXfsEnd = xml.indexOf("</cellXfs>", cellXfsStart);
|
|
4172
|
+
if (cellXfsEnd === -1) return null;
|
|
4173
|
+
const cellXfsContent = xml.substring(cellXfsStart, cellXfsEnd);
|
|
4174
|
+
const xfRe = /<xf\b[^>]*>/g;
|
|
4175
|
+
let index = 0;
|
|
4176
|
+
while ((m = xfRe.exec(cellXfsContent)) !== null) {
|
|
4177
|
+
const nfMatch = /numFmtId="(\d+)"/.exec(m[0]);
|
|
4178
|
+
const nfId = nfMatch ? parseInt(nfMatch[1], 10) : 0;
|
|
4179
|
+
const isDate = nfId >= 14 && nfId <= 22 || nfId >= 45 && nfId <= 47 || customDateFormats.has(nfId);
|
|
4180
|
+
if (isDate) return index;
|
|
4181
|
+
index++;
|
|
4182
|
+
}
|
|
4183
|
+
return null;
|
|
4184
|
+
}
|
|
4185
|
+
// ------------------------------------------------------------------
|
|
4186
|
+
// Cell / row serialization
|
|
4187
|
+
// ------------------------------------------------------------------
|
|
4188
|
+
_buildSheetData(rows, headers, colStyles, headerStyles, dateStyle) {
|
|
4189
|
+
let out = "";
|
|
4190
|
+
let rowNum = 0;
|
|
4191
|
+
if (headers) {
|
|
4192
|
+
rowNum = 1;
|
|
4193
|
+
out += '<row r="1">';
|
|
4194
|
+
for (let c = 0; c < headers.length; c++) {
|
|
4195
|
+
const style = headerStyles.get(c) ?? 0;
|
|
4196
|
+
out += this._stringCell(headers[c], c, rowNum, style);
|
|
4197
|
+
}
|
|
4198
|
+
out += "</row>";
|
|
4199
|
+
}
|
|
4200
|
+
for (const row of rows) {
|
|
4201
|
+
rowNum++;
|
|
4202
|
+
out += `<row r="${rowNum}">`;
|
|
4203
|
+
for (let c = 0; c < row.length; c++) {
|
|
4204
|
+
const raw = row[c];
|
|
4205
|
+
if (raw === null || raw === void 0) continue;
|
|
4206
|
+
out += this._valueCell(raw, c, rowNum, colStyles, dateStyle);
|
|
4207
|
+
}
|
|
4208
|
+
out += "</row>";
|
|
4209
|
+
}
|
|
4210
|
+
return out;
|
|
4211
|
+
}
|
|
4212
|
+
_valueCell(raw, col, rowNum, colStyles, dateStyle) {
|
|
4213
|
+
const colRef = columnIndexToLetter(col);
|
|
4214
|
+
const fmtString = getFormat(raw);
|
|
4215
|
+
const val = fmtString !== null ? unwrapCell(raw) : raw;
|
|
4216
|
+
const colStyle = colStyles.get(col) ?? 0;
|
|
4217
|
+
if (typeof val === "number") {
|
|
4218
|
+
if (Number.isFinite(val)) {
|
|
4219
|
+
return `<c r="${colRef}${rowNum}"${colStyle > 0 ? ` s="${colStyle}"` : ""}><v>${val}</v></c>`;
|
|
4220
|
+
}
|
|
4221
|
+
return this._stringCell(val.toString(), col, rowNum, colStyle);
|
|
4222
|
+
}
|
|
4223
|
+
if (typeof val === "bigint") {
|
|
4224
|
+
return this._stringCell(val.toString(), col, rowNum, colStyle);
|
|
4225
|
+
}
|
|
4226
|
+
if (typeof val === "boolean") {
|
|
4227
|
+
return `<c r="${colRef}${rowNum}" t="b"${colStyle > 0 ? ` s="${colStyle}"` : ""}><v>${val ? 1 : 0}</v></c>`;
|
|
4228
|
+
}
|
|
4229
|
+
if (val instanceof Date) {
|
|
4230
|
+
const oaDate = (val.getTime() - this._oaEpoch) / 864e5;
|
|
4231
|
+
if (Number.isFinite(oaDate)) {
|
|
4232
|
+
const style = colStyle > 0 ? colStyle : dateStyle !== null ? dateStyle : 0;
|
|
4233
|
+
return `<c r="${colRef}${rowNum}"${style > 0 ? ` s="${style}"` : ""}><v>${oaDate}</v></c>`;
|
|
4234
|
+
}
|
|
4235
|
+
return this._stringCell(val.toString(), col, rowNum, colStyle);
|
|
4236
|
+
}
|
|
4237
|
+
if (val !== null && val !== void 0) {
|
|
4238
|
+
return this._stringCell(String(val), col, rowNum, colStyle);
|
|
4239
|
+
}
|
|
4240
|
+
return "";
|
|
4241
|
+
}
|
|
4242
|
+
_stringCell(text, col, rowNum, style) {
|
|
4243
|
+
const colRef = columnIndexToLetter(col);
|
|
4244
|
+
const styleAttr = style > 0 ? ` s="${style}"` : "";
|
|
4245
|
+
if (this.sharedStringsXml === null) {
|
|
4246
|
+
const clean = escapeXmlText(text);
|
|
4247
|
+
const preserve = clean.length > 0 && (clean[0] === " " || clean[clean.length - 1] === " " || /[\t\n\r]/.test(clean));
|
|
4248
|
+
const tAttr = preserve ? ' xml:space="preserve"' : "";
|
|
4249
|
+
return `<c r="${colRef}${rowNum}" t="inlineStr"${styleAttr}><is><t${tAttr}>${clean}</t></is></c>`;
|
|
4250
|
+
}
|
|
4251
|
+
let index = this.stringIndexMap.get(text);
|
|
4252
|
+
if (index === void 0) {
|
|
4253
|
+
index = this.sharedStringsValues.length;
|
|
4254
|
+
this.sharedStringsValues.push(text);
|
|
4255
|
+
this.stringIndexMap.set(text, index);
|
|
4256
|
+
}
|
|
4257
|
+
this.sharedStringsCount++;
|
|
4258
|
+
return `<c r="${colRef}${rowNum}" t="s"${styleAttr}><v>${index}</v></c>`;
|
|
4259
|
+
}
|
|
4260
|
+
// ------------------------------------------------------------------
|
|
4261
|
+
// Worksheet XML patching
|
|
4262
|
+
// ------------------------------------------------------------------
|
|
4263
|
+
_patchSheetXml(sheetXml, newSheetData, totalRows, lastCol) {
|
|
4264
|
+
let xml = sheetXml;
|
|
4265
|
+
const dimRef = totalRows > 0 && lastCol >= 0 ? `A1:${columnIndexToLetter(lastCol)}${totalRows}` : "A1";
|
|
4266
|
+
const dimRe = /<dimension\b[^>]*>/;
|
|
4267
|
+
const dimMatch = dimRe.exec(xml);
|
|
4268
|
+
if (dimMatch) {
|
|
4269
|
+
xml = xml.substring(0, dimMatch.index) + dimMatch[0].replace(/ref="[^"]*"/, `ref="${dimRef}"`) + xml.substring(dimMatch.index + dimMatch[0].length);
|
|
4270
|
+
} else {
|
|
4271
|
+
const rootRe = /<worksheet\b[^>]*>/;
|
|
4272
|
+
const rootMatch = rootRe.exec(xml);
|
|
4273
|
+
if (rootMatch) {
|
|
4274
|
+
const insertAt = rootMatch.index + rootMatch[0].length;
|
|
4275
|
+
xml = xml.substring(0, insertAt) + `<dimension ref="${dimRef}"/>` + xml.substring(insertAt);
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
const sdOpen = "<sheetData>";
|
|
4279
|
+
const sdStart = xml.indexOf(sdOpen);
|
|
4280
|
+
if (sdStart === -1) {
|
|
4281
|
+
const selfClosing = /<sheetData\s*\/\s*>/;
|
|
4282
|
+
const scMatch = selfClosing.exec(xml);
|
|
4283
|
+
if (scMatch) {
|
|
4284
|
+
xml = xml.substring(0, scMatch.index) + "<sheetData>" + newSheetData + "</sheetData>" + xml.substring(scMatch.index + scMatch[0].length);
|
|
4285
|
+
return xml;
|
|
4286
|
+
}
|
|
4287
|
+
throw new Error("XlsxUpdater: <sheetData> element not found in worksheet XML.");
|
|
4288
|
+
}
|
|
4289
|
+
const sdEnd = xml.indexOf("</sheetData>", sdStart);
|
|
4290
|
+
if (sdEnd === -1) {
|
|
4291
|
+
throw new Error("XlsxUpdater: <sheetData> element not closed in worksheet XML.");
|
|
4292
|
+
}
|
|
4293
|
+
xml = xml.substring(0, sdStart + sdOpen.length) + newSheetData + xml.substring(sdEnd);
|
|
4294
|
+
const afRe = /<autoFilter\b[^>]*>/;
|
|
4295
|
+
const afMatch = afRe.exec(xml);
|
|
4296
|
+
if (afMatch && totalRows > 0 && lastCol >= 0 && afMatch[0].includes("ref=")) {
|
|
4297
|
+
xml = xml.substring(0, afMatch.index) + afMatch[0].replace(/ref="[^"]*"/, `ref="${dimRef}"`) + xml.substring(afMatch.index + afMatch[0].length);
|
|
4298
|
+
}
|
|
4299
|
+
return xml;
|
|
4300
|
+
}
|
|
4301
|
+
// ------------------------------------------------------------------
|
|
4302
|
+
// Pivot table integration
|
|
4303
|
+
// ------------------------------------------------------------------
|
|
4304
|
+
_updatePivotCaches(sheetName, dimRef, recordCount) {
|
|
4305
|
+
for (const cache of this.pivotCacheDefs) {
|
|
4306
|
+
const wsRe = /<worksheetSource\b[^>]*>/;
|
|
4307
|
+
const wsMatch = wsRe.exec(cache.xml);
|
|
4308
|
+
if (!wsMatch) continue;
|
|
4309
|
+
const wsTag = wsMatch[0];
|
|
4310
|
+
const sheetAttr = /sheet="([^"]*)"/.exec(wsTag);
|
|
4311
|
+
if (!sheetAttr || sheetAttr[1] !== sheetName) continue;
|
|
4312
|
+
let xml = cache.xml;
|
|
4313
|
+
xml = xml.substring(0, wsMatch.index) + wsTag.replace(/ref="[^"]*"/, `ref="${dimRef}"`) + xml.substring(wsMatch.index + wsTag.length);
|
|
4314
|
+
const rootRe = /<pivotCacheDefinition\b[^>]*>/;
|
|
4315
|
+
const rootMatch = rootRe.exec(xml);
|
|
4316
|
+
if (rootMatch) {
|
|
4317
|
+
let rootTag = rootMatch[0];
|
|
4318
|
+
if (/recordCount="\d+"/.test(rootTag)) {
|
|
4319
|
+
rootTag = rootTag.replace(/recordCount="\d+"/, `recordCount="${recordCount}"`);
|
|
4320
|
+
} else if (!/\/>$/.test(rootTag)) {
|
|
4321
|
+
rootTag = rootTag.replace(/>$/, ` recordCount="${recordCount}">`);
|
|
4322
|
+
}
|
|
4323
|
+
xml = xml.substring(0, rootMatch.index) + rootTag + xml.substring(rootMatch.index + rootMatch[0].length);
|
|
4324
|
+
}
|
|
4325
|
+
this.zip.updateFile(cache.path, Buffer.from(xml, "utf8"));
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
_addRefreshOnLoadToPivotTables() {
|
|
4329
|
+
for (const path3 of this.pivotTableDefPaths) {
|
|
4330
|
+
const entry = this.zip.getEntry(path3);
|
|
4331
|
+
if (!entry) continue;
|
|
4332
|
+
let xml = entry.getData().toString("utf8");
|
|
4333
|
+
const rootRe = /<pivotTableDefinition\b[^>]*>/;
|
|
4334
|
+
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;
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
return { values, total, unique, endSstOffset };
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
// src/XlsbUpdater.ts
|
|
4416
|
+
var CELL_RECORDS = /* @__PURE__ */ new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
|
4417
|
+
var RK_INT_LOWER = -1 << 29;
|
|
4418
|
+
var RK_INT_UPPER = (1 << 29) - 1;
|
|
4419
|
+
var XlsbUpdater = class {
|
|
4420
|
+
/**
|
|
4421
|
+
* @param path Path to an existing `.xlsb` file.
|
|
4422
|
+
*/
|
|
4423
|
+
constructor(path3) {
|
|
4424
|
+
this.sheetNameToPath = /* @__PURE__ */ new Map();
|
|
4425
|
+
this.pivotCacheDefPaths = [];
|
|
4426
|
+
this.sharedStringsBuf = null;
|
|
4427
|
+
this.sharedStringsValues = [];
|
|
4428
|
+
this.sharedStringsTotal = 0;
|
|
4429
|
+
this.sharedStringsUnique = 0;
|
|
4430
|
+
this.sharedStringsEndSst = 0;
|
|
4431
|
+
this.stringIndexMap = /* @__PURE__ */ new Map();
|
|
4432
|
+
// ------------------------------------------------------------------
|
|
4433
|
+
// Shared strings
|
|
4434
|
+
// ------------------------------------------------------------------
|
|
4435
|
+
this._appendCount = 0;
|
|
4436
|
+
this.sourcePath = path3;
|
|
4437
|
+
this._oaEpoch = Date.UTC(1899, 11, 30);
|
|
4438
|
+
if (!fs4.existsSync(path3)) {
|
|
4439
|
+
throw new Error(`XlsbUpdater: file not found: ${path3}`);
|
|
4440
|
+
}
|
|
4441
|
+
this.zip = new AdmZip3(path3);
|
|
4442
|
+
if (!this.zip.getEntry("xl/workbook.bin")) {
|
|
4443
|
+
throw new Error(
|
|
4444
|
+
"XlsbUpdater: not an XLSB workbook (xl/workbook.bin missing). Convert the file to XLSB first."
|
|
4445
|
+
);
|
|
4446
|
+
}
|
|
4447
|
+
this._loadWorkbookStructure();
|
|
4448
|
+
}
|
|
4449
|
+
/** Worksheet names in workbook order. */
|
|
4450
|
+
getSheetNames() {
|
|
4451
|
+
return [...this.sheetNameToPath.keys()];
|
|
4452
|
+
}
|
|
4453
|
+
/**
|
|
4454
|
+
* Clear the entire cell data of the target sheet and write new rows in its place.
|
|
4455
|
+
* All other parts of the workbook (pivot tables, other sheets, styles, …) are preserved.
|
|
4456
|
+
*
|
|
4457
|
+
* @param sheetName Name of the worksheet to replace.
|
|
4458
|
+
* @param rows New data rows (`CellValue` cells; null/undefined = blank).
|
|
4459
|
+
* @param options See {@link ReplaceSheetDataOptions}.
|
|
4460
|
+
*/
|
|
4461
|
+
replaceSheetData(sheetName, rows, options = {}) {
|
|
4462
|
+
const { headers, styleFallback = "inherit" } = options;
|
|
4463
|
+
rows = trimTrailingEmptyRows(rows);
|
|
4464
|
+
const sheetPath = this.sheetNameToPath.get(sheetName);
|
|
4465
|
+
if (!sheetPath) {
|
|
4466
|
+
throw new Error(`XlsbUpdater: sheet "${sheetName}" not found in the workbook.`);
|
|
4467
|
+
}
|
|
4468
|
+
const entry = this.zip.getEntry(sheetPath);
|
|
4469
|
+
if (!entry) {
|
|
4470
|
+
throw new Error(`XlsbUpdater: worksheet part missing for sheet "${sheetName}".`);
|
|
4471
|
+
}
|
|
4472
|
+
const sheetBuf = entry.getData();
|
|
4473
|
+
this._ensureSharedStringsLoaded();
|
|
4474
|
+
let dataStyles = /* @__PURE__ */ new Map();
|
|
4475
|
+
let headerStyles = /* @__PURE__ */ new Map();
|
|
4476
|
+
let dateXf = null;
|
|
4477
|
+
if (styleFallback !== "general") {
|
|
4478
|
+
const collected = this._collectStyles(sheetBuf);
|
|
4479
|
+
dataStyles = collected.dataStyles;
|
|
4480
|
+
headerStyles = collected.headerStyles;
|
|
4481
|
+
dateXf = this._findDateXf();
|
|
4482
|
+
}
|
|
4483
|
+
const lastCol = Math.max(0, Math.max(
|
|
4484
|
+
rows.reduce((m, r) => Math.max(m, r.length), 0),
|
|
4485
|
+
(headers == null ? void 0 : headers.length) ?? 0
|
|
4486
|
+
) - 1);
|
|
4487
|
+
const newRows = this._buildRows(rows, headers, dataStyles, headerStyles, dateXf);
|
|
4488
|
+
const { rowsStart, rowsEnd, autoFilterRanges } = this._findRowsRegion(sheetBuf);
|
|
4489
|
+
const newSheet = Buffer.alloc(sheetBuf.length - (rowsEnd - rowsStart) + newRows.length);
|
|
4490
|
+
sheetBuf.copy(newSheet, 0, 0, rowsStart);
|
|
4491
|
+
newRows.copy(newSheet, rowsStart);
|
|
4492
|
+
sheetBuf.copy(newSheet, rowsStart + newRows.length, rowsEnd);
|
|
4493
|
+
const lastRowIdx = rows.length + (headers ? 1 : 0) - 1;
|
|
4494
|
+
const delta = newRows.length - (rowsEnd - rowsStart);
|
|
4495
|
+
for (const af of autoFilterRanges) {
|
|
4496
|
+
const newAf = af + delta;
|
|
4497
|
+
newSheet.writeInt32LE(lastCol, newAf + 12);
|
|
4498
|
+
newSheet.writeInt32LE(lastRowIdx, newAf + 4);
|
|
4499
|
+
}
|
|
4500
|
+
this._patchDimension(newSheet, lastRowIdx, lastCol);
|
|
4501
|
+
this.zip.updateFile(sheetPath, newSheet);
|
|
4502
|
+
if (this.sharedStringsBuf !== null && this._appendCount > 0) {
|
|
4503
|
+
this._commitSharedStrings();
|
|
4504
|
+
}
|
|
4505
|
+
const rwLast = Math.max(0, rows.length + (headers ? 1 : 0) - 1);
|
|
4506
|
+
const colLast = lastCol;
|
|
4507
|
+
this._patchPivotCaches(sheetName, rwLast, colLast);
|
|
4508
|
+
}
|
|
4509
|
+
/**
|
|
4510
|
+
* Write the updated workbook to disk.
|
|
4511
|
+
* @param outputPath Destination path; defaults to the source file (overwrites in place).
|
|
4512
|
+
*/
|
|
4513
|
+
save(outputPath) {
|
|
4514
|
+
const target = outputPath ?? this.sourcePath;
|
|
4515
|
+
fs4.writeFileSync(target, this.zip.toBuffer());
|
|
4516
|
+
}
|
|
4517
|
+
/** The updated workbook as an in-memory ZIP buffer. */
|
|
4518
|
+
toBuffer() {
|
|
4519
|
+
return this.zip.toBuffer();
|
|
4520
|
+
}
|
|
4521
|
+
// ------------------------------------------------------------------
|
|
4522
|
+
// Workbook structure
|
|
4523
|
+
// ------------------------------------------------------------------
|
|
4524
|
+
_loadWorkbookStructure() {
|
|
4525
|
+
const relsEntry = this.zip.getEntry("xl/_rels/workbook.bin.rels");
|
|
4526
|
+
const rIdToTarget = {};
|
|
4527
|
+
if (relsEntry) {
|
|
4528
|
+
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];
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
const wbEntry = this.zip.getEntry("xl/workbook.bin");
|
|
4536
|
+
if (wbEntry) {
|
|
4537
|
+
const wbBuf = wbEntry.getData();
|
|
4538
|
+
const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
|
|
4539
|
+
let pos = 0;
|
|
4540
|
+
while (readRecord(wbBuf, pos, rec)) {
|
|
4541
|
+
pos = rec.dataEnd;
|
|
4542
|
+
if (rec.id !== 156 || rec.len < 8) continue;
|
|
4543
|
+
let o = rec.dataStart + 8;
|
|
4544
|
+
const rIdLen = wbBuf.readUInt32LE(o);
|
|
4545
|
+
o += 4;
|
|
4546
|
+
const rId = readUtf16(wbBuf, o, rIdLen);
|
|
4547
|
+
o += rIdLen * 2;
|
|
4548
|
+
const nameLen = wbBuf.readUInt32LE(o);
|
|
4549
|
+
o += 4;
|
|
4550
|
+
const name = readUtf16(wbBuf, o, nameLen);
|
|
4551
|
+
let target = rIdToTarget[rId] ?? "";
|
|
4552
|
+
if (target.startsWith("/")) target = target.substring(1);
|
|
4553
|
+
else if (!target.startsWith("xl/")) target = "xl/" + target;
|
|
4554
|
+
if (target) {
|
|
4555
|
+
this.sheetNameToPath.set(name, target);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
for (const e of this.zip.getEntries()) {
|
|
4560
|
+
if (/^xl\/pivotCache\/pivotCacheDefinition\d*\.bin$/.test(e.entryName)) {
|
|
4561
|
+
this.pivotCacheDefPaths.push(e.entryName);
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
_ensureSharedStringsLoaded() {
|
|
4566
|
+
if (this.stringIndexMap.size > 0 || this.sharedStringsBuf !== null) {
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
const entry = this.zip.getEntry("xl/sharedStrings.bin");
|
|
4570
|
+
if (!entry) {
|
|
4571
|
+
this.sharedStringsBuf = null;
|
|
4572
|
+
return;
|
|
4573
|
+
}
|
|
4574
|
+
this.sharedStringsBuf = entry.getData();
|
|
4575
|
+
const parsed = parseSharedStringsBin(this.sharedStringsBuf);
|
|
4576
|
+
this.sharedStringsValues = parsed.values;
|
|
4577
|
+
this.sharedStringsTotal = parsed.total;
|
|
4578
|
+
this.sharedStringsUnique = parsed.unique;
|
|
4579
|
+
this.sharedStringsEndSst = parsed.endSstOffset;
|
|
4580
|
+
this.sharedStringsValues.forEach((value, index) => {
|
|
4581
|
+
if (!this.stringIndexMap.has(value)) {
|
|
4582
|
+
this.stringIndexMap.set(value, index);
|
|
4583
|
+
}
|
|
4584
|
+
});
|
|
4585
|
+
this._appendCount = 0;
|
|
4586
|
+
}
|
|
4587
|
+
_commitSharedStrings() {
|
|
4588
|
+
if (this.sharedStringsBuf === null || this._appendCount === 0) return;
|
|
4589
|
+
const originalValuesLength = this.sharedStringsValues.length - this._appendCount;
|
|
4590
|
+
let newItems = Buffer.alloc(0);
|
|
4591
|
+
for (let i = originalValuesLength; i < this.sharedStringsValues.length; i++) {
|
|
4592
|
+
const text = this.sharedStringsValues[i];
|
|
4593
|
+
const payload = Buffer.alloc(1 + 4 + text.length * 2);
|
|
4594
|
+
payload[0] = 0;
|
|
4595
|
+
payload.writeUInt32LE(text.length, 1);
|
|
4596
|
+
payload.write(text, 5, text.length * 2, "utf16le");
|
|
4597
|
+
newItems = Buffer.concat([newItems, buildRecord(19, payload)]);
|
|
4598
|
+
}
|
|
4599
|
+
const out = Buffer.alloc(this.sharedStringsBuf.length + newItems.length);
|
|
4600
|
+
this.sharedStringsBuf.copy(out, 0, 0, this.sharedStringsEndSst);
|
|
4601
|
+
newItems.copy(out, this.sharedStringsEndSst);
|
|
4602
|
+
this.sharedStringsBuf.copy(out, this.sharedStringsEndSst + newItems.length, this.sharedStringsEndSst);
|
|
4603
|
+
const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
|
|
4604
|
+
let pos = 0;
|
|
4605
|
+
while (readRecord(out, pos, rec)) {
|
|
4606
|
+
if (rec.id === 159) {
|
|
4607
|
+
out.writeUInt32LE(this.sharedStringsTotal, rec.dataStart);
|
|
4608
|
+
out.writeUInt32LE(this.sharedStringsUnique, rec.dataStart + 4);
|
|
4609
|
+
break;
|
|
4610
|
+
}
|
|
4611
|
+
pos = rec.dataEnd;
|
|
4612
|
+
}
|
|
4613
|
+
this.zip.updateFile("xl/sharedStrings.bin", out);
|
|
4614
|
+
}
|
|
4615
|
+
// ------------------------------------------------------------------
|
|
4616
|
+
// Styles
|
|
4617
|
+
// ------------------------------------------------------------------
|
|
4618
|
+
_collectStyles(sheetBuf) {
|
|
4619
|
+
const dataCounts = /* @__PURE__ */ new Map();
|
|
4620
|
+
const headerStyles = /* @__PURE__ */ new Map();
|
|
4621
|
+
const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
|
|
4622
|
+
let pos = 0;
|
|
4623
|
+
let row = -1;
|
|
4624
|
+
while (readRecord(sheetBuf, pos, rec)) {
|
|
4625
|
+
pos = rec.dataEnd;
|
|
4626
|
+
if (rec.id === 0) {
|
|
4627
|
+
row = sheetBuf.readInt32LE(rec.dataStart);
|
|
4628
|
+
continue;
|
|
4629
|
+
}
|
|
4630
|
+
if (!CELL_RECORDS.has(rec.id) || rec.len < 8) continue;
|
|
4631
|
+
const col = sheetBuf.readUInt32LE(rec.dataStart);
|
|
4632
|
+
const xf = sheetBuf.readUInt32LE(rec.dataStart + 4) & 16777215;
|
|
4633
|
+
if (xf <= 0) continue;
|
|
4634
|
+
if (row === 0) {
|
|
4635
|
+
if (!headerStyles.has(col)) headerStyles.set(col, xf);
|
|
4636
|
+
continue;
|
|
4637
|
+
}
|
|
4638
|
+
let colCounts = dataCounts.get(col);
|
|
4639
|
+
if (!colCounts) {
|
|
4640
|
+
colCounts = /* @__PURE__ */ new Map();
|
|
4641
|
+
dataCounts.set(col, colCounts);
|
|
4642
|
+
}
|
|
4643
|
+
colCounts.set(xf, (colCounts.get(xf) ?? 0) + 1);
|
|
4644
|
+
}
|
|
4645
|
+
const dataStyles = /* @__PURE__ */ new Map();
|
|
4646
|
+
for (const [col, colCounts] of dataCounts) {
|
|
4647
|
+
let best = 0;
|
|
4648
|
+
let bestCount = -1;
|
|
4649
|
+
for (const [xf, count] of colCounts) {
|
|
4650
|
+
if (count > bestCount) {
|
|
4651
|
+
best = xf;
|
|
4652
|
+
bestCount = count;
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
if (bestCount > 0) dataStyles.set(col, best);
|
|
4656
|
+
}
|
|
4657
|
+
return { dataStyles, headerStyles };
|
|
4658
|
+
}
|
|
4659
|
+
_findDateXf() {
|
|
4660
|
+
const entry = this.zip.getEntry("xl/styles.bin");
|
|
4661
|
+
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++;
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
return null;
|
|
4686
|
+
}
|
|
4687
|
+
// ------------------------------------------------------------------
|
|
4688
|
+
// Row / cell serialization
|
|
4689
|
+
// ------------------------------------------------------------------
|
|
4690
|
+
_buildRows(rows, headers, dataStyles, headerStyles, dateXf) {
|
|
4691
|
+
const chunks = [];
|
|
4692
|
+
const maxCol = Math.max(rows.reduce((m, r) => Math.max(m, r.length), 0), (headers == null ? void 0 : headers.length) ?? 0, 0);
|
|
4693
|
+
const colLast = Math.max(0, maxCol - 1);
|
|
4694
|
+
const pushRow = (rowNum2, cellBytes) => {
|
|
4695
|
+
const header = Buffer.alloc(27);
|
|
4696
|
+
header[0] = 0;
|
|
4697
|
+
header[1] = 25;
|
|
4698
|
+
header.writeInt32LE(rowNum2, 2);
|
|
4699
|
+
header[10] = 44;
|
|
4700
|
+
header[11] = 1;
|
|
4701
|
+
header[15] = 1;
|
|
4702
|
+
header.writeInt32LE(0, 19);
|
|
4703
|
+
header.writeInt32LE(colLast, 23);
|
|
4704
|
+
chunks.push(header);
|
|
4705
|
+
for (const cell of cellBytes) {
|
|
4706
|
+
chunks.push(cell);
|
|
4707
|
+
}
|
|
4708
|
+
};
|
|
4709
|
+
if (headers) {
|
|
4710
|
+
const cells = [];
|
|
4711
|
+
for (let c = 0; c < headers.length; c++) {
|
|
4712
|
+
const style = headerStyles.get(c) ?? 0;
|
|
4713
|
+
cells.push(this._stringCellBytes(c, style, headers[c]));
|
|
4714
|
+
}
|
|
4715
|
+
pushRow(0, cells);
|
|
4716
|
+
}
|
|
4717
|
+
let rowNum = headers ? 1 : 0;
|
|
4718
|
+
for (const row of rows) {
|
|
4719
|
+
const cells = [];
|
|
4720
|
+
for (let c = 0; c < row.length; c++) {
|
|
4721
|
+
const raw = row[c];
|
|
4722
|
+
if (raw === null || raw === void 0) continue;
|
|
4723
|
+
cells.push(this._valueCellBytes(raw, c, dataStyles, dateXf));
|
|
4724
|
+
}
|
|
4725
|
+
pushRow(rowNum, cells);
|
|
4726
|
+
rowNum++;
|
|
4727
|
+
}
|
|
4728
|
+
return Buffer.concat(chunks);
|
|
4729
|
+
}
|
|
4730
|
+
_valueCellBytes(raw, col, dataStyles, dateXf) {
|
|
4731
|
+
const fmtString = getFormat(raw);
|
|
4732
|
+
const val = fmtString !== null ? unwrapCell(raw) : raw;
|
|
4733
|
+
const colStyle = dataStyles.get(col) ?? 0;
|
|
4734
|
+
if (typeof val === "number") {
|
|
4735
|
+
if (Number.isFinite(val) && Number.isInteger(val) && val >= RK_INT_LOWER && val <= RK_INT_UPPER) {
|
|
4736
|
+
const payload = Buffer.alloc(12);
|
|
4737
|
+
payload.writeUInt32LE(col, 0);
|
|
4738
|
+
payload.writeUInt32LE(colStyle, 4);
|
|
4739
|
+
payload.writeInt32LE(val << 2 | 2, 8);
|
|
4740
|
+
return buildRecord(2, payload);
|
|
4741
|
+
}
|
|
4742
|
+
if (Number.isFinite(val)) {
|
|
4743
|
+
const payload = Buffer.alloc(16);
|
|
4744
|
+
payload.writeUInt32LE(col, 0);
|
|
4745
|
+
payload.writeUInt32LE(colStyle, 4);
|
|
4746
|
+
payload.writeDoubleLE(val, 8);
|
|
4747
|
+
return buildRecord(5, payload);
|
|
4748
|
+
}
|
|
4749
|
+
return this._stringCellBytes(col, colStyle, val.toString());
|
|
4750
|
+
}
|
|
4751
|
+
if (typeof val === "bigint") {
|
|
4752
|
+
return this._stringCellBytes(col, colStyle, val.toString());
|
|
4753
|
+
}
|
|
4754
|
+
if (typeof val === "boolean") {
|
|
4755
|
+
const payload = Buffer.alloc(9);
|
|
4756
|
+
payload.writeUInt32LE(col, 0);
|
|
4757
|
+
payload.writeUInt32LE(colStyle, 4);
|
|
4758
|
+
payload[8] = val ? 1 : 0;
|
|
4759
|
+
return buildRecord(4, payload);
|
|
4760
|
+
}
|
|
4761
|
+
if (val instanceof Date) {
|
|
4762
|
+
const oaDate = (val.getTime() - this._oaEpoch) / 864e5;
|
|
4763
|
+
if (Number.isFinite(oaDate)) {
|
|
4764
|
+
const style = colStyle > 0 ? colStyle : dateXf !== null ? dateXf : 0;
|
|
4765
|
+
const payload = Buffer.alloc(16);
|
|
4766
|
+
payload.writeUInt32LE(col, 0);
|
|
4767
|
+
payload.writeUInt32LE(style, 4);
|
|
4768
|
+
payload.writeDoubleLE(oaDate, 8);
|
|
4769
|
+
return buildRecord(5, payload);
|
|
4770
|
+
}
|
|
4771
|
+
return this._stringCellBytes(col, colStyle, val.toString());
|
|
4772
|
+
}
|
|
4773
|
+
if (val !== null && val !== void 0) {
|
|
4774
|
+
return this._stringCellBytes(col, colStyle, String(val));
|
|
4775
|
+
}
|
|
4776
|
+
return this._stringCellBytes(col, colStyle, "");
|
|
4777
|
+
}
|
|
4778
|
+
_stringCellBytes(col, style, text) {
|
|
4779
|
+
if (this.sharedStringsBuf === null) {
|
|
4780
|
+
const payload2 = Buffer.alloc(12 + text.length * 2);
|
|
4781
|
+
payload2.writeUInt32LE(col, 0);
|
|
4782
|
+
payload2.writeUInt32LE(style, 4);
|
|
4783
|
+
payload2.writeUInt32LE(text.length, 8);
|
|
4784
|
+
payload2.write(text, 12, text.length * 2, "utf16le");
|
|
4785
|
+
return buildRecord(6, payload2);
|
|
4786
|
+
}
|
|
4787
|
+
let index = this.stringIndexMap.get(text);
|
|
4788
|
+
if (index === void 0) {
|
|
4789
|
+
index = this.sharedStringsValues.length;
|
|
4790
|
+
this.sharedStringsValues.push(text);
|
|
4791
|
+
this.stringIndexMap.set(text, index);
|
|
4792
|
+
this.sharedStringsUnique++;
|
|
4793
|
+
this._appendCount++;
|
|
4794
|
+
}
|
|
4795
|
+
this.sharedStringsTotal++;
|
|
4796
|
+
const payload = Buffer.alloc(12);
|
|
4797
|
+
payload.writeUInt32LE(col, 0);
|
|
4798
|
+
payload.writeUInt32LE(style, 4);
|
|
4799
|
+
payload.writeUInt32LE(index, 8);
|
|
4800
|
+
return buildRecord(7, payload);
|
|
4801
|
+
}
|
|
4802
|
+
// ------------------------------------------------------------------
|
|
4803
|
+
// Rows region detection
|
|
4804
|
+
// ------------------------------------------------------------------
|
|
4805
|
+
_findRowsRegion(sheetBuf) {
|
|
4806
|
+
const autoFilterRanges = [];
|
|
4807
|
+
let rowsStart = -1;
|
|
4808
|
+
let lastCellEnd = -1;
|
|
4809
|
+
let lastW25 = -1;
|
|
4810
|
+
const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
|
|
4811
|
+
let pos = 0;
|
|
4812
|
+
while (readRecord(sheetBuf, pos, rec)) {
|
|
4813
|
+
if (rec.id === 0) {
|
|
4814
|
+
if (rowsStart === -1) rowsStart = rec.headerStart;
|
|
4815
|
+
} else if (rec.id >= 1 && rec.id <= 11) {
|
|
4816
|
+
lastCellEnd = rec.dataEnd;
|
|
4817
|
+
} else if (rec.id === 37 && rec.len === 6) {
|
|
4818
|
+
lastW25 = rec.headerStart;
|
|
4819
|
+
} else if (rec.id === 161 && rec.len >= 16) {
|
|
4820
|
+
autoFilterRanges.push(rec.dataStart);
|
|
4821
|
+
}
|
|
4822
|
+
pos = rec.dataEnd;
|
|
4823
|
+
}
|
|
4824
|
+
if (rowsStart === -1) {
|
|
4825
|
+
const insertAt = lastW25 >= 0 ? lastW25 : sheetBuf.length;
|
|
4826
|
+
return { rowsStart: insertAt, rowsEnd: insertAt, autoFilterRanges };
|
|
4827
|
+
}
|
|
4828
|
+
const rowsEnd = lastCellEnd >= 0 ? lastCellEnd : lastW25 >= 0 ? lastW25 : sheetBuf.length;
|
|
4829
|
+
return { rowsStart, rowsEnd, autoFilterRanges };
|
|
4830
|
+
}
|
|
4831
|
+
/**
|
|
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)
|
|
4834
|
+
* fields to read the full row range; stale values make the last written row
|
|
4835
|
+
* invisible to Excel (and drop it from pivot refreshes).
|
|
4836
|
+
*/
|
|
4837
|
+
_patchDimension(sheetBuf, lastRowIdx, lastCol) {
|
|
4838
|
+
const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
|
|
4839
|
+
let pos = 0;
|
|
4840
|
+
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);
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4846
|
+
pos = rec.dataEnd;
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
// ------------------------------------------------------------------
|
|
4850
|
+
// Pivot cache integration
|
|
4851
|
+
// ------------------------------------------------------------------
|
|
4852
|
+
_patchPivotCaches(sheetName, rwLast, colLast) {
|
|
4853
|
+
for (const path3 of this.pivotCacheDefPaths) {
|
|
4854
|
+
const entry = this.zip.getEntry(path3);
|
|
4855
|
+
if (!entry) continue;
|
|
4856
|
+
const buf = Buffer.from(entry.getData());
|
|
4857
|
+
const rec = { headerStart: 0, dataStart: 0, dataEnd: 0, id: 0, len: 0 };
|
|
4858
|
+
let pos = 0;
|
|
4859
|
+
let patched = false;
|
|
4860
|
+
let refreshXfPatched = false;
|
|
4861
|
+
while (readRecord(buf, pos, rec)) {
|
|
4862
|
+
pos = rec.dataEnd;
|
|
4863
|
+
if (rec.id === 179 && rec.len >= 4 && !refreshXfPatched) {
|
|
4864
|
+
if (rec.dataStart + 4 <= buf.length) {
|
|
4865
|
+
const v = buf[rec.dataStart + 3];
|
|
4866
|
+
if ((v & 4) === 0) {
|
|
4867
|
+
buf[rec.dataStart + 3] = v | 4;
|
|
4868
|
+
patched = true;
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
refreshXfPatched = true;
|
|
4872
|
+
}
|
|
4873
|
+
if (rec.id === 187 && rec.len >= 23) {
|
|
4874
|
+
const cch = buf.readUInt32LE(rec.dataStart + 3);
|
|
4875
|
+
const nameStart = rec.dataStart + 7;
|
|
4876
|
+
const refStart = nameStart + cch * 2;
|
|
4877
|
+
const name = readUtf16(buf, nameStart, cch);
|
|
4878
|
+
if (name === sheetName && refStart + 16 <= buf.length) {
|
|
4879
|
+
buf.writeInt32LE(rwLast, refStart + 4);
|
|
4880
|
+
buf.writeInt32LE(colLast, refStart + 12);
|
|
4881
|
+
patched = true;
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
if (patched) {
|
|
4886
|
+
this.zip.updateFile(path3, buf);
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
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 {
|
|
4926
|
+
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;
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
if (backupCreated) {
|
|
4940
|
+
operations.remove(backupPath);
|
|
4941
|
+
}
|
|
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
|
+
);
|
|
4952
|
+
}
|
|
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
|
+
);
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
throw error;
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
// src/ExcelConverter.ts
|
|
4964
|
+
var POWERSHELL_CONVERSION_SCRIPT = `
|
|
4965
|
+
$ErrorActionPreference = 'Stop'
|
|
4966
|
+
$inputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_INPUT')
|
|
4967
|
+
$outputPath = [Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_OUTPUT')
|
|
4968
|
+
$fileFormat = [int][Environment]::GetEnvironmentVariable('JUSTYBASE_EXCEL_FORMAT')
|
|
4969
|
+
$excel = $null
|
|
4970
|
+
$workbook = $null
|
|
4971
|
+
$exitCode = 0
|
|
4972
|
+
|
|
4973
|
+
try {
|
|
4974
|
+
$excel = New-Object -ComObject Excel.Application
|
|
4975
|
+
$excel.Visible = $false
|
|
4976
|
+
$excel.DisplayAlerts = $false
|
|
4977
|
+
$excel.ScreenUpdating = $false
|
|
4978
|
+
|
|
4979
|
+
# Open read-only so the source workbook is never changed by the conversion.
|
|
4980
|
+
$workbook = $excel.Workbooks.Open($inputPath, 0, $true)
|
|
4981
|
+
if ($null -eq $workbook) {
|
|
4982
|
+
throw 'Excel did not return a workbook when opening the source file.'
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
$workbook.SaveAs($outputPath, $fileFormat)
|
|
4986
|
+
} catch {
|
|
4987
|
+
$exitCode = 1
|
|
4988
|
+
[Console]::Error.WriteLine($_.Exception.ToString())
|
|
4989
|
+
} finally {
|
|
4990
|
+
if ($null -ne $workbook) {
|
|
4991
|
+
try { $workbook.Close($false) } catch { }
|
|
4992
|
+
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($workbook) | Out-Null } catch { }
|
|
4993
|
+
}
|
|
4994
|
+
if ($null -ne $excel) {
|
|
4995
|
+
try { $excel.Quit() } catch { }
|
|
4996
|
+
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null } catch { }
|
|
4997
|
+
}
|
|
4998
|
+
[GC]::Collect()
|
|
4999
|
+
[GC]::WaitForPendingFinalizers()
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
exit $exitCode
|
|
5003
|
+
`;
|
|
5004
|
+
function errorMessage2(error) {
|
|
5005
|
+
return error instanceof Error ? error.message : String(error);
|
|
5006
|
+
}
|
|
5007
|
+
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) {
|
|
5011
|
+
throw new Error(
|
|
5012
|
+
`Excel conversion: source must have the ${inputExtension} extension: ${inputPath}`
|
|
5013
|
+
);
|
|
5014
|
+
}
|
|
5015
|
+
if (path2.extname(output).toLowerCase() !== outputExtension) {
|
|
5016
|
+
throw new Error(
|
|
5017
|
+
`Excel conversion: destination must have the ${outputExtension} extension: ${outputPath}`
|
|
5018
|
+
);
|
|
5019
|
+
}
|
|
5020
|
+
if (!fs6.existsSync(input)) {
|
|
5021
|
+
throw new Error(`Excel conversion: source file not found: ${inputPath}`);
|
|
5022
|
+
}
|
|
5023
|
+
if (!fs6.statSync(input).isFile()) {
|
|
5024
|
+
throw new Error(`Excel conversion: source path is not a file: ${inputPath}`);
|
|
5025
|
+
}
|
|
5026
|
+
if (!fs6.existsSync(path2.dirname(output))) {
|
|
5027
|
+
throw new Error(`Excel conversion: destination directory not found: ${path2.dirname(output)}`);
|
|
5028
|
+
}
|
|
5029
|
+
if (fs6.existsSync(output)) {
|
|
5030
|
+
if (fs6.statSync(output).isDirectory()) {
|
|
5031
|
+
throw new Error(`Excel conversion: destination path is a directory: ${outputPath}`);
|
|
5032
|
+
}
|
|
5033
|
+
if (!overwrite) {
|
|
5034
|
+
throw new Error(
|
|
5035
|
+
`Excel conversion: destination already exists (pass { overwrite: true } to replace it): ${outputPath}`
|
|
5036
|
+
);
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
if (process.platform === "win32" && input.toLowerCase() === output.toLowerCase()) {
|
|
5040
|
+
throw new Error("Excel conversion: source and destination must be different files.");
|
|
5041
|
+
}
|
|
5042
|
+
return { input, output };
|
|
5043
|
+
}
|
|
5044
|
+
function runPowerShellConversion(inputPath, outputPath, fileFormat) {
|
|
5045
|
+
const encodedCommand = Buffer.from(POWERSHELL_CONVERSION_SCRIPT, "utf16le").toString("base64");
|
|
5046
|
+
const environment = {
|
|
5047
|
+
...process.env,
|
|
5048
|
+
JUSTYBASE_EXCEL_INPUT: inputPath,
|
|
5049
|
+
JUSTYBASE_EXCEL_OUTPUT: outputPath,
|
|
5050
|
+
JUSTYBASE_EXCEL_FORMAT: String(fileFormat)
|
|
5051
|
+
};
|
|
5052
|
+
return new Promise((resolve2, reject) => {
|
|
5053
|
+
var _a, _b, _c, _d;
|
|
5054
|
+
let stdout = "";
|
|
5055
|
+
let stderr = "";
|
|
5056
|
+
let settled = false;
|
|
5057
|
+
const child = spawn(
|
|
5058
|
+
"powershell.exe",
|
|
5059
|
+
[
|
|
5060
|
+
"-NoLogo",
|
|
5061
|
+
"-NoProfile",
|
|
5062
|
+
"-NonInteractive",
|
|
5063
|
+
"-ExecutionPolicy",
|
|
5064
|
+
"Bypass",
|
|
5065
|
+
"-EncodedCommand",
|
|
5066
|
+
encodedCommand
|
|
5067
|
+
],
|
|
5068
|
+
{ env: environment, windowsHide: true }
|
|
5069
|
+
);
|
|
5070
|
+
(_a = child.stdout) == null ? void 0 : _a.setEncoding("utf8");
|
|
5071
|
+
(_b = child.stderr) == null ? void 0 : _b.setEncoding("utf8");
|
|
5072
|
+
(_c = child.stdout) == null ? void 0 : _c.on("data", (chunk) => {
|
|
5073
|
+
stdout += chunk;
|
|
5074
|
+
});
|
|
5075
|
+
(_d = child.stderr) == null ? void 0 : _d.on("data", (chunk) => {
|
|
5076
|
+
stderr += chunk;
|
|
5077
|
+
});
|
|
5078
|
+
child.once("error", (error) => {
|
|
5079
|
+
if (settled) return;
|
|
5080
|
+
settled = true;
|
|
5081
|
+
reject(new Error(`Excel conversion: unable to start PowerShell/COM: ${error.message}`));
|
|
5082
|
+
});
|
|
5083
|
+
child.once("close", (code) => {
|
|
5084
|
+
if (settled) return;
|
|
5085
|
+
settled = true;
|
|
5086
|
+
if (code === 0) {
|
|
5087
|
+
resolve2();
|
|
5088
|
+
return;
|
|
5089
|
+
}
|
|
5090
|
+
const details = (stderr || stdout).replace(/\s+/g, " ").trim();
|
|
5091
|
+
reject(new Error(
|
|
5092
|
+
`Excel conversion failed (PowerShell exit code ${code ?? "unknown"})${details ? `: ${details}` : "."}`
|
|
5093
|
+
));
|
|
5094
|
+
});
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
async function convertExcelFile(inputPath, outputPath, inputExtension, outputExtension, fileFormat, options = {}) {
|
|
5098
|
+
const { input, output } = validateConversionPaths(
|
|
5099
|
+
inputPath,
|
|
5100
|
+
outputPath,
|
|
5101
|
+
inputExtension,
|
|
5102
|
+
outputExtension,
|
|
5103
|
+
options.overwrite === true
|
|
5104
|
+
);
|
|
5105
|
+
if (process.platform !== "win32") {
|
|
5106
|
+
throw new Error("Excel conversion requires Windows with desktop Microsoft Excel and COM automation.");
|
|
5107
|
+
}
|
|
5108
|
+
const temporaryOutput = path2.join(
|
|
5109
|
+
path2.dirname(output),
|
|
5110
|
+
`.${path2.basename(output, outputExtension)}.${randomUUID2()}.conversion${outputExtension}`
|
|
5111
|
+
);
|
|
5112
|
+
try {
|
|
5113
|
+
await runPowerShellConversion(input, temporaryOutput, fileFormat);
|
|
5114
|
+
if (!fs6.existsSync(temporaryOutput)) {
|
|
5115
|
+
throw new Error("Excel conversion completed without creating the destination file.");
|
|
5116
|
+
}
|
|
5117
|
+
installTemporaryOutput(temporaryOutput, output, options.overwrite === true);
|
|
5118
|
+
} catch (error) {
|
|
5119
|
+
throw new Error(`Excel conversion ${inputExtension} -> ${outputExtension}: ${errorMessage2(error)}`);
|
|
5120
|
+
} finally {
|
|
5121
|
+
if (fs6.existsSync(temporaryOutput)) {
|
|
5122
|
+
try {
|
|
5123
|
+
fs6.rmSync(temporaryOutput);
|
|
5124
|
+
} catch {
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
function convertXlsbToXlsx(inputPath, outputPath, options) {
|
|
5130
|
+
return convertExcelFile(inputPath, outputPath, ".xlsb", ".xlsx", 51, options);
|
|
5131
|
+
}
|
|
5132
|
+
function convertXlsxToXlsb(inputPath, outputPath, options) {
|
|
5133
|
+
return convertExcelFile(inputPath, outputPath, ".xlsx", ".xlsb", 50, options);
|
|
5134
|
+
}
|
|
3868
5135
|
export {
|
|
3869
5136
|
BiffReaderWriter,
|
|
3870
5137
|
BigBuffer,
|
|
@@ -3874,10 +5141,14 @@ export {
|
|
|
3874
5141
|
ReaderFactory,
|
|
3875
5142
|
StreamingSheetState,
|
|
3876
5143
|
XlsbReader,
|
|
5144
|
+
XlsbUpdater,
|
|
3877
5145
|
XlsbWriter,
|
|
3878
5146
|
XlsxReader,
|
|
5147
|
+
XlsxUpdater,
|
|
3879
5148
|
XlsxWriter,
|
|
3880
5149
|
applyHeaderWidths,
|
|
5150
|
+
convertXlsbToXlsx,
|
|
5151
|
+
convertXlsxToXlsb,
|
|
3881
5152
|
defaultColWidth,
|
|
3882
5153
|
getFormat,
|
|
3883
5154
|
initColWidths,
|