@xlvane/compat 1.2.2 → 1.2.3
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/index.d.ts +42 -14
- package/dist/index.js +400 -89
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -289,14 +289,36 @@ export declare class Workbook {
|
|
|
289
289
|
_meta(): WorkbookMeta;
|
|
290
290
|
/** @internal */
|
|
291
291
|
get _date1904(): boolean;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
private _byName;
|
|
293
|
+
private _firstFree;
|
|
294
|
+
private _maxOrderNo;
|
|
295
|
+
private _maxOrderNoStale;
|
|
296
|
+
/** @internal Whether a sheet other than `exclude` has this name, ignoring case (the duplicate check of Worksheet.name). */
|
|
297
|
+
_nameTaken(name: string, exclude?: Worksheet): boolean;
|
|
298
|
+
/** @internal Keeps the name index in step with a renamed sheet. */
|
|
299
|
+
_renamed(sheet: Worksheet, oldName: string): void;
|
|
300
|
+
/** @internal Keeps the largest orderNo in step with a changed one. */
|
|
301
|
+
_orderNoChanged(sheet: Worksheet, oldOrderNo: number | undefined): void;
|
|
302
|
+
/** @internal Puts a worksheet into `_sheets` at its id and indexes it. */
|
|
303
|
+
_putSheet(sheet: Worksheet): void;
|
|
304
|
+
/** Takes a worksheet out of `_sheets` and its indexes. */
|
|
305
|
+
private _dropSheet;
|
|
306
|
+
/** Empties `_sheets` and its indexes. */
|
|
307
|
+
private _clearSheets;
|
|
308
|
+
/** The largest orderNo of the sheets, as exceljs computes it (lib/doc/workbook.js L81-85). */
|
|
309
|
+
private _lastOrderNo;
|
|
310
|
+
/** The id the next worksheet gets (lib/doc/workbook.js `nextId`): the lowest free id. */
|
|
295
311
|
get nextId(): number;
|
|
296
312
|
/** Adds a worksheet. Names follow exceljs rules (31 characters at most; longer names are cut). */
|
|
297
313
|
addWorksheet(name?: string, options?: WorksheetOptions): Worksheet;
|
|
298
314
|
/** @internal Adds a worksheet with a given id (a loaded file's sheetId, a model's id; D-F03). */
|
|
299
315
|
_addWorksheetWithId(id: number, name?: string, options?: WorksheetOptions): Worksheet;
|
|
316
|
+
/**
|
|
317
|
+
* @internal Adds a worksheet read from a file (`xlsx.load`, `stream.xlsx.WorkbookReader`). A
|
|
318
|
+
* name exceljs refuses gives an XlvError with exceljs's message, the sheet and a hint, instead
|
|
319
|
+
* of the plain Error of `addWorksheet`; an empty name is refused too.
|
|
320
|
+
*/
|
|
321
|
+
_addReadWorksheet(id: number, name: string, options?: WorksheetOptions): Worksheet;
|
|
300
322
|
removeWorksheet(id: number | string): void;
|
|
301
323
|
/** Removes a worksheet by id or name, as `removeWorksheet` (lib/doc/workbook.js L77). */
|
|
302
324
|
removeWorksheetEx(worksheet: Worksheet): void;
|
|
@@ -2707,6 +2729,18 @@ type MergeRange = {
|
|
|
2707
2729
|
bottom: number;
|
|
2708
2730
|
right: number;
|
|
2709
2731
|
};
|
|
2732
|
+
/**
|
|
2733
|
+
* @internal What a worksheet needs from its workbook: the duplicate check of its name and, for a
|
|
2734
|
+
* workbook that indexes its sheets, notices of a new name or orderNo.
|
|
2735
|
+
*/
|
|
2736
|
+
interface WorksheetHost {
|
|
2737
|
+
/** Whether a sheet other than `exclude` has this name, ignoring case. */
|
|
2738
|
+
_nameTaken(name: string, exclude: Worksheet): boolean;
|
|
2739
|
+
_renamed?(sheet: Worksheet, oldName: string): void;
|
|
2740
|
+
_orderNoChanged?(sheet: Worksheet, oldOrderNo: number | undefined): void;
|
|
2741
|
+
readonly definedNames?: DefinedNames;
|
|
2742
|
+
readonly worksheets?: readonly Worksheet[];
|
|
2743
|
+
}
|
|
2710
2744
|
/** The merged ranges of a worksheet by master address, with a counter of changes (D-H01). */
|
|
2711
2745
|
declare class MergeMap extends Map<string, MergeRange> {
|
|
2712
2746
|
version: number;
|
|
@@ -2729,18 +2763,16 @@ declare class MergeMap extends Map<string, MergeRange> {
|
|
|
2729
2763
|
* ```
|
|
2730
2764
|
*/
|
|
2731
2765
|
export declare class Worksheet {
|
|
2732
|
-
readonly workbook:
|
|
2733
|
-
readonly _names: (exclude: Worksheet) => string[];
|
|
2734
|
-
readonly definedNames?: DefinedNames;
|
|
2735
|
-
readonly worksheets?: readonly Worksheet[];
|
|
2736
|
-
};
|
|
2766
|
+
readonly workbook: WorksheetHost;
|
|
2737
2767
|
readonly id: number;
|
|
2738
2768
|
/**
|
|
2739
2769
|
* The position of the sheet in `workbook.worksheets`, as exceljs `orderNo`: one more than the
|
|
2740
2770
|
* largest of the other sheets for a new sheet (lib/doc/workbook.js L81-85), the 0-based position
|
|
2741
2771
|
* in the file for a loaded one (L203). Worksheets are listed in this order.
|
|
2742
2772
|
*/
|
|
2743
|
-
orderNo: number | undefined;
|
|
2773
|
+
get orderNo(): number | undefined;
|
|
2774
|
+
set orderNo(value: number | undefined);
|
|
2775
|
+
private _orderNo;
|
|
2744
2776
|
state: "visible" | "hidden" | "veryHidden";
|
|
2745
2777
|
views: WorksheetView[];
|
|
2746
2778
|
autoFilter: string | {
|
|
@@ -2782,11 +2814,7 @@ export declare class Worksheet {
|
|
|
2782
2814
|
private readonly _keys;
|
|
2783
2815
|
private _name;
|
|
2784
2816
|
/** @internal Use `workbook.addWorksheet()`. */
|
|
2785
|
-
constructor(workbook:
|
|
2786
|
-
readonly _names: (exclude: Worksheet) => string[];
|
|
2787
|
-
readonly definedNames?: DefinedNames;
|
|
2788
|
-
readonly worksheets?: readonly Worksheet[];
|
|
2789
|
-
}, id: number, name: string | undefined, options: WorksheetOptions | undefined);
|
|
2817
|
+
constructor(workbook: WorksheetHost, id: number, name: string | undefined, options: WorksheetOptions | undefined);
|
|
2790
2818
|
get name(): string;
|
|
2791
2819
|
/** Sheet name rules of exceljs (lib/doc/worksheet.js L134–173), including truncation to 31 characters. */
|
|
2792
2820
|
set name(value: string);
|
package/dist/index.js
CHANGED
|
@@ -1040,24 +1040,60 @@ function sharedFormulaRanges(ws) {
|
|
|
1040
1040
|
const m = decodeAddress(master);
|
|
1041
1041
|
return m.row === b.top && m.col === b.left;
|
|
1042
1042
|
});
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
const
|
|
1046
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
1047
|
-
const [mi, bi] = sorted[i];
|
|
1048
|
-
for (let j = i + 1; j < sorted.length && sorted[j][1].top <= bi.bottom; j++) {
|
|
1049
|
-
const [mj, bj] = sorted[j];
|
|
1050
|
-
if (overlaps(bi, bj)) {
|
|
1051
|
-
bad.add(mi);
|
|
1052
|
-
bad.add(mj);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
if (arrays.some((a) => overlaps(a, bi))) bad.add(mi);
|
|
1056
|
-
}
|
|
1043
|
+
const boxes = groups.map(([, b]) => b);
|
|
1044
|
+
const withGroups = overlapCounts(boxes, boxes);
|
|
1045
|
+
const withArrays = overlapCounts(boxes, arrays);
|
|
1057
1046
|
const out = /* @__PURE__ */ new Map();
|
|
1058
|
-
|
|
1047
|
+
groups.forEach(([master, b], i) => {
|
|
1048
|
+
if (withGroups[i] === 1 && withArrays[i] === 0) out.set(master, `${columnLetter(b.left)}${String(b.top)}:${columnLetter(b.right)}${String(b.bottom)}`);
|
|
1049
|
+
});
|
|
1059
1050
|
return out;
|
|
1060
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* For each box of `queries`, the number of boxes of `set` that share at least one cell with it.
|
|
1054
|
+
* Two boxes share cells when their rows and their columns overlap. Among the boxes with
|
|
1055
|
+
* `top <= q.bottom`, those with `bottom < q.top` are exactly the ones whose rows miss q's rows,
|
|
1056
|
+
* so the count is (boxes with top <= q.bottom whose columns meet q's) - (boxes with
|
|
1057
|
+
* bottom < q.top whose columns meet q's). Each term is found by a sweep over rows with two
|
|
1058
|
+
* Fenwick trees of columns: the columns of a box meet [a, b] unless it ends before a or starts
|
|
1059
|
+
* after b. Time O((queries + set) log(columns)).
|
|
1060
|
+
*/
|
|
1061
|
+
function overlapCounts(queries, set) {
|
|
1062
|
+
const counts = new Array(queries.length).fill(0);
|
|
1063
|
+
if (queries.length === 0 || set.length === 0) return counts;
|
|
1064
|
+
let width = 1;
|
|
1065
|
+
for (const b of set) width = Math.max(width, b.right + 1);
|
|
1066
|
+
for (const q of queries) width = Math.max(width, q.right + 1);
|
|
1067
|
+
const sweep = (key, at, sign) => {
|
|
1068
|
+
const lefts = new Array(width + 1).fill(0);
|
|
1069
|
+
const rights = new Array(width + 1).fill(0);
|
|
1070
|
+
const add = (tree, i) => {
|
|
1071
|
+
for (let k = Math.max(1, i); k <= width; k += k & -k) tree[k] += 1;
|
|
1072
|
+
};
|
|
1073
|
+
const below = (tree, i) => {
|
|
1074
|
+
let n = 0;
|
|
1075
|
+
for (let k = Math.min(i, width); k > 0; k -= k & -k) n += tree[k];
|
|
1076
|
+
return n;
|
|
1077
|
+
};
|
|
1078
|
+
const boxes = [...set].sort((x, y) => key(x) - key(y));
|
|
1079
|
+
const order = queries.map((_, i) => i).sort((x, y) => at(queries[x]) - at(queries[y]));
|
|
1080
|
+
let added = 0;
|
|
1081
|
+
for (const i of order) {
|
|
1082
|
+
const q = queries[i];
|
|
1083
|
+
const t = at(q);
|
|
1084
|
+
while (added < boxes.length && key(boxes[added]) <= t) {
|
|
1085
|
+
const b = boxes[added++];
|
|
1086
|
+
add(lefts, b.left);
|
|
1087
|
+
add(rights, b.right);
|
|
1088
|
+
}
|
|
1089
|
+
const meet = added - below(rights, q.left - 1) - (added - below(lefts, q.right));
|
|
1090
|
+
counts[i] += sign * meet;
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
sweep((b) => b.top, (q) => q.bottom, 1);
|
|
1094
|
+
sweep((b) => b.bottom, (q) => q.top - 1, -1);
|
|
1095
|
+
return counts;
|
|
1096
|
+
}
|
|
1061
1097
|
/** Converts a cell value for the writer; a `{ text, hyperlink }` value gives its text (the link is {@link linkIn}). */
|
|
1062
1098
|
function valueIn(cell, sharedRanges) {
|
|
1063
1099
|
const v = cell.value;
|
|
@@ -1328,6 +1364,17 @@ function at(sheet, cell, run) {
|
|
|
1328
1364
|
});
|
|
1329
1365
|
}
|
|
1330
1366
|
}
|
|
1367
|
+
/** The 32-bit FNV-1a hash of bytes. */
|
|
1368
|
+
function fnv1a(bytes) {
|
|
1369
|
+
let h = 2166136261;
|
|
1370
|
+
for (let i = 0; i < bytes.length; i++) h = Math.imul(h ^ bytes[i], 16777619);
|
|
1371
|
+
return h >>> 0;
|
|
1372
|
+
}
|
|
1373
|
+
function sameBytes(a, b) {
|
|
1374
|
+
if (a.length !== b.length) return false;
|
|
1375
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1331
1378
|
/** Builds an xlvane workbook from the exceljs model (D-503). */
|
|
1332
1379
|
function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
1333
1380
|
const book = createWorkbook({
|
|
@@ -1337,6 +1384,7 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1337
1384
|
if (sheets.length === 0) notSupported("Writing a workbook without worksheets");
|
|
1338
1385
|
const coreImages = /* @__PURE__ */ new Map();
|
|
1339
1386
|
const coreBytes = [];
|
|
1387
|
+
const coreByHash = /* @__PURE__ */ new Map();
|
|
1340
1388
|
const imageId = (id) => {
|
|
1341
1389
|
let core = coreImages.get(id);
|
|
1342
1390
|
if (core === void 0) {
|
|
@@ -1349,10 +1397,14 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1349
1397
|
}
|
|
1350
1398
|
});
|
|
1351
1399
|
const bytes = imageBytes(image);
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1400
|
+
const key = `${String(bytes.length)}:${String(fnv1a(bytes))}`;
|
|
1401
|
+
const candidates = coreByHash.get(key) ?? [];
|
|
1402
|
+
const same = candidates.find((c) => sameBytes(coreBytes[c], bytes));
|
|
1403
|
+
if (same === void 0) {
|
|
1354
1404
|
core = book.addImage(bytes);
|
|
1355
1405
|
coreBytes[core] = bytes;
|
|
1406
|
+
candidates.push(core);
|
|
1407
|
+
coreByHash.set(key, candidates);
|
|
1356
1408
|
} else core = same;
|
|
1357
1409
|
coreImages.set(id, core);
|
|
1358
1410
|
}
|
|
@@ -1725,11 +1777,17 @@ function rangeText(r) {
|
|
|
1725
1777
|
return r.top === r.bottom && r.left === r.right ? a : `${a}:${address(r.bottom, r.right)}`;
|
|
1726
1778
|
}
|
|
1727
1779
|
/**
|
|
1728
|
-
* Cells per
|
|
1729
|
-
* as exceljs does (data-validations-xform.js L229–239). Larger ranges, such as whole
|
|
1730
|
-
* keep their range as the key; `find()` and
|
|
1780
|
+
* Cells per workbook up to which validations read from a file are given to every cell they
|
|
1781
|
+
* cover, as exceljs does (data-validations-xform.js L229–239). Larger ranges, such as whole
|
|
1782
|
+
* columns, and ranges read after the budget is spent keep their range as the key; `find()` and
|
|
1783
|
+
* `cell.dataValidation` still answer for every cell. The budget is shared by all sheets of a
|
|
1784
|
+
* load, so many sheets cannot each expand 100 000 cells (security review J, C2).
|
|
1731
1785
|
*/
|
|
1732
1786
|
const MAX_EXPANDED_CELLS = 1e5;
|
|
1787
|
+
/** @internal The budget of {@link dataValidationsOut} for one workbook load. */
|
|
1788
|
+
function validationBudget() {
|
|
1789
|
+
return { left: MAX_EXPANDED_CELLS };
|
|
1790
|
+
}
|
|
1733
1791
|
/**
|
|
1734
1792
|
* The data validations of a worksheet (`worksheet.dataValidations`), keyed by cell address or
|
|
1735
1793
|
* range as in exceljs (lib/doc/data-validations.js). `remove()` deletes the entry; exceljs
|
|
@@ -1906,21 +1964,23 @@ function validationOut(d, date1904) {
|
|
|
1906
1964
|
]) if (d[k] !== null) out[k] = d[k];
|
|
1907
1965
|
return out;
|
|
1908
1966
|
}
|
|
1909
|
-
/**
|
|
1910
|
-
|
|
1967
|
+
/**
|
|
1968
|
+
* The `worksheet.dataValidations` model of validations read by xlvane. `budget` is the number of
|
|
1969
|
+
* cells still to expand; pass the same object for every sheet of a workbook.
|
|
1970
|
+
*/
|
|
1971
|
+
function dataValidationsOut(list, date1904, budget = validationBudget()) {
|
|
1911
1972
|
const model = Object.create(null);
|
|
1912
|
-
let budget = MAX_EXPANDED_CELLS;
|
|
1913
1973
|
for (const d of list) {
|
|
1914
1974
|
const v = validationOut(d, date1904);
|
|
1915
1975
|
for (const part of d.sqref.split(/\s+/)) {
|
|
1916
1976
|
const r = parseRange(part);
|
|
1917
1977
|
if (!r) continue;
|
|
1918
1978
|
const size = (r.bottom - r.top + 1) * (r.right - r.left + 1);
|
|
1919
|
-
if (size > budget) {
|
|
1979
|
+
if (size > budget.left) {
|
|
1920
1980
|
model[part] = v;
|
|
1921
1981
|
continue;
|
|
1922
1982
|
}
|
|
1923
|
-
budget -= size;
|
|
1983
|
+
budget.left -= size;
|
|
1924
1984
|
for (let row = r.top; row <= r.bottom; row++) for (let col = r.left; col <= r.right; col++) model[address(row, col)] = v;
|
|
1925
1985
|
}
|
|
1926
1986
|
}
|
|
@@ -3628,6 +3688,30 @@ function parseRangeArgs(args) {
|
|
|
3628
3688
|
right: Math.max(s.col, e.col)
|
|
3629
3689
|
};
|
|
3630
3690
|
}
|
|
3691
|
+
/**
|
|
3692
|
+
* @internal exceljs's rules for a worksheet name (lib/doc/worksheet.js L134-173): the name to
|
|
3693
|
+
* use, cut to 31 characters, or exceljs's message for a name it refuses. `taken` tells whether
|
|
3694
|
+
* another sheet of the workbook has a name (already cut) that equals it ignoring case.
|
|
3695
|
+
*/
|
|
3696
|
+
function checkSheetName(value, taken) {
|
|
3697
|
+
const fail = (message) => ({
|
|
3698
|
+
name: typeof value === "string" ? value : "",
|
|
3699
|
+
message,
|
|
3700
|
+
duplicate: false
|
|
3701
|
+
});
|
|
3702
|
+
if (typeof value !== "string") return fail("The name has to be a string.");
|
|
3703
|
+
if (value === "") return fail("The name can't be empty.");
|
|
3704
|
+
if (value === "History") return fail("The name \"History\" is protected. Please use a different name.");
|
|
3705
|
+
if (/[*?:/\\[\]]/.test(value)) return fail(`Worksheet name ${value} cannot include any of the following characters: * ? : \\ / [ ]`);
|
|
3706
|
+
if (/(^')|('$)/.test(value)) return fail(`The first or last character of worksheet name cannot be a single quotation mark: ${value}`);
|
|
3707
|
+
const name = value.length > 31 ? value.substring(0, 31) : value;
|
|
3708
|
+
if (taken(name)) return {
|
|
3709
|
+
name,
|
|
3710
|
+
message: `Worksheet name already exists: ${name}`,
|
|
3711
|
+
duplicate: true
|
|
3712
|
+
};
|
|
3713
|
+
return { name };
|
|
3714
|
+
}
|
|
3631
3715
|
/** The merged ranges of a worksheet by master address, with a counter of changes (D-H01). */
|
|
3632
3716
|
var MergeMap = class extends Map {
|
|
3633
3717
|
version = 0;
|
|
@@ -3666,7 +3750,15 @@ var Worksheet = class {
|
|
|
3666
3750
|
* largest of the other sheets for a new sheet (lib/doc/workbook.js L81-85), the 0-based position
|
|
3667
3751
|
* in the file for a loaded one (L203). Worksheets are listed in this order.
|
|
3668
3752
|
*/
|
|
3669
|
-
orderNo
|
|
3753
|
+
get orderNo() {
|
|
3754
|
+
return this._orderNo;
|
|
3755
|
+
}
|
|
3756
|
+
set orderNo(value) {
|
|
3757
|
+
const old = this._orderNo;
|
|
3758
|
+
this._orderNo = value;
|
|
3759
|
+
this.workbook._orderNoChanged?.(this, old);
|
|
3760
|
+
}
|
|
3761
|
+
_orderNo;
|
|
3670
3762
|
state;
|
|
3671
3763
|
views;
|
|
3672
3764
|
autoFilter = null;
|
|
@@ -3717,16 +3809,12 @@ var Worksheet = class {
|
|
|
3717
3809
|
}
|
|
3718
3810
|
/** Sheet name rules of exceljs (lib/doc/worksheet.js L134–173), including truncation to 31 characters. */
|
|
3719
3811
|
set name(value) {
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
if (
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
if (/(^')|('$)/.test(name)) throw new Error(`The first or last character of worksheet name cannot be a single quotation mark: ${name}`);
|
|
3727
|
-
if (name.length > 31) name = name.substring(0, 31);
|
|
3728
|
-
if (this.workbook._names(this).some((n) => n.toLowerCase() === name.toLowerCase())) throw new Error(`Worksheet name already exists: ${name}`);
|
|
3729
|
-
this._name = name;
|
|
3812
|
+
if (value === this._name) return;
|
|
3813
|
+
const checked = checkSheetName(value, (name) => this.workbook._nameTaken(name, this));
|
|
3814
|
+
if (checked.message !== void 0) throw new Error(checked.message);
|
|
3815
|
+
const old = this._name;
|
|
3816
|
+
this._name = checked.name;
|
|
3817
|
+
this.workbook._renamed?.(this, old);
|
|
3730
3818
|
}
|
|
3731
3819
|
/** Deprecated in exceljs in favour of `properties.tabColor`, which it reads and sets (lib/doc/worksheet.js L827). */
|
|
3732
3820
|
get tabColor() {
|
|
@@ -4707,7 +4795,7 @@ function formatDate(date, format, utc) {
|
|
|
4707
4795
|
});
|
|
4708
4796
|
}
|
|
4709
4797
|
const PARSE_TOKENS = /(\[[^[]*\])|([-_:/.,()\s]+)|(A|a|Q|YYYY|YY?|ww?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g;
|
|
4710
|
-
const WORD =
|
|
4798
|
+
const WORD = /(?<!\d)\d*[^-_:/,()\s\d]+/;
|
|
4711
4799
|
const ONE_OR_TWO = /\d\d?/;
|
|
4712
4800
|
const PARSE_EXPRESSIONS = {
|
|
4713
4801
|
A: WORD,
|
|
@@ -5057,21 +5145,55 @@ function defaultWriteMap(dateFormat, dateUTC) {
|
|
|
5057
5145
|
*
|
|
5058
5146
|
* @internal
|
|
5059
5147
|
*/
|
|
5060
|
-
function worksheetCsv(sheet, options) {
|
|
5148
|
+
function worksheetCsv(sheet, options, latin1 = false) {
|
|
5061
5149
|
const f = formatterOptions(options.formatterOptions);
|
|
5062
5150
|
const injection = options.formulaInjection ?? "escape";
|
|
5063
5151
|
if (injection !== "escape" && injection !== "off") throw invalid("formulaInjection", "The option \"formulaInjection\" must be \"escape\" or \"off\".", "Use \"escape\" (default) or \"off\".");
|
|
5152
|
+
if (latin1) {
|
|
5153
|
+
for (const [name, value] of [
|
|
5154
|
+
["delimiter", f.delimiter],
|
|
5155
|
+
["rowDelimiter", f.rowDelimiter],
|
|
5156
|
+
["quote", f.quote],
|
|
5157
|
+
["escape", f.escapedQuote]
|
|
5158
|
+
]) if (/[^\u0000-\u00ff]/.test(value)) throw invalid(name, `The option "${name}" has a character that latin1 cannot write.`, "Use characters up to U+00FF with encoding \"latin1\", or write UTF-8.");
|
|
5159
|
+
}
|
|
5160
|
+
if (injection === "escape") {
|
|
5161
|
+
const off = "or pass formulaInjection: \"off\" for a trusted consumer.";
|
|
5162
|
+
if (f.delimiter !== " " && [...f.delimiter].some((c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32)) throw invalid("delimiter", `The delimiter ${JSON.stringify(f.delimiter)} holds a formula sign or a control character, so a line could start with a formula.`, `Use "," (default), ";", "|" or "\\t", ${off}`);
|
|
5163
|
+
if (![
|
|
5164
|
+
"\n",
|
|
5165
|
+
"\r\n",
|
|
5166
|
+
"\r"
|
|
5167
|
+
].includes(f.rowDelimiter)) throw invalid("rowDelimiter", `The row delimiter ${JSON.stringify(f.rowDelimiter)} is not a line break, so a standard CSV reader would not split rows where it is written.`, `Use "\\n" (default), "\\r\\n" or "\\r", ${off}`);
|
|
5168
|
+
if (f.quote !== "" && f.quote !== "\"") throw invalid("quote", `The quote ${JSON.stringify(f.quote)} is not '"', so a standard CSV reader would split a value that holds a line break or the delimiter.`, `Leave formatterOptions.quote at its default ("), or use quote: false, ${off}`);
|
|
5169
|
+
if (f.quote !== "" && f.escapedQuote !== `${f.quote}${f.quote}`) throw invalid("escape", "An escape character other than the quote lets a standard CSV reader end a quoted field early, so a value could start a new field with a formula.", "Leave formatterOptions.escape unset (the quote is doubled), or pass formulaInjection: \"off\" for a trusted consumer.");
|
|
5170
|
+
}
|
|
5171
|
+
/** The text latin1 will write: characters above U+00FF as "?". */
|
|
5172
|
+
const narrow = (out) => latin1 && typeof out === "string" ? out.replace(/[^\u0000-\u00ff]/g, "?") : out;
|
|
5173
|
+
/** A formatted field, refused if a standard reader would take its leading quote as quoting. */
|
|
5174
|
+
const field = (out, index, force) => {
|
|
5175
|
+
const text = formatField(out, index, f, force);
|
|
5176
|
+
if (injection === "escape" && f.quote !== "\"" && text.startsWith("\"")) throw new XlvError("XLV_E_INVALID_OPTION", `Field ${String(index + 1)} starts with a double quote, which a standard CSV reader takes as quoting when formatterOptions.quote is not '"'.`, {
|
|
5177
|
+
hint: "Leave formatterOptions.quote at its default (\"), or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
5178
|
+
details: {
|
|
5179
|
+
option: "quote",
|
|
5180
|
+
field: index + 1
|
|
5181
|
+
}
|
|
5182
|
+
});
|
|
5183
|
+
return text;
|
|
5184
|
+
};
|
|
5064
5185
|
const map = options.map ?? defaultWriteMap(options.dateFormat, options.dateUTC === true);
|
|
5065
5186
|
const includeEmptyRows = options.includeEmptyRows === void 0 || options.includeEmptyRows;
|
|
5066
5187
|
const lines = [];
|
|
5067
|
-
const escape = (
|
|
5188
|
+
const escape = (raw) => {
|
|
5189
|
+
const out = narrow(raw);
|
|
5068
5190
|
if (injection !== "escape" || typeof out !== "string") return [out, false];
|
|
5069
5191
|
const first = out.replace(/\0/g, "")[0];
|
|
5070
5192
|
return first !== void 0 && (INJECTION_START.has(first) || first.charCodeAt(0) < 32) ? [`'${out}`, f.quote !== ""] : [out, false];
|
|
5071
5193
|
};
|
|
5072
5194
|
if (f.headers) lines.push(f.headers.map((h, i) => {
|
|
5073
5195
|
const [out, force] = escape(h);
|
|
5074
|
-
return
|
|
5196
|
+
return field(out, i, force);
|
|
5075
5197
|
}).join(f.delimiter));
|
|
5076
5198
|
if (sheet) {
|
|
5077
5199
|
let lastRow = 0;
|
|
@@ -5086,7 +5208,7 @@ function worksheetCsv(sheet, options) {
|
|
|
5086
5208
|
continue;
|
|
5087
5209
|
}
|
|
5088
5210
|
const [out, force] = escape(map(values[i], i));
|
|
5089
|
-
fields.push(
|
|
5211
|
+
fields.push(field(out, i, force));
|
|
5090
5212
|
}
|
|
5091
5213
|
lines.push(fields.join(f.delimiter));
|
|
5092
5214
|
lastRow = rowNumber;
|
|
@@ -5128,11 +5250,28 @@ function decoderFor(encoding) {
|
|
|
5128
5250
|
async function readText(stream, encoding) {
|
|
5129
5251
|
const decoder = decoderFor(encoding);
|
|
5130
5252
|
let text = "";
|
|
5253
|
+
let size = 0;
|
|
5254
|
+
const count = (n) => {
|
|
5255
|
+
size += n;
|
|
5256
|
+
if (size > DEFAULT_READ_LIMITS.maxInputBytes) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `The CSV input is larger than ${String(DEFAULT_READ_LIMITS.maxInputBytes)} bytes.`, {
|
|
5257
|
+
hint: "Split the CSV file, or read it with the core fromCSV in parts.",
|
|
5258
|
+
details: {
|
|
5259
|
+
limit: "maxInputBytes",
|
|
5260
|
+
value: DEFAULT_READ_LIMITS.maxInputBytes
|
|
5261
|
+
}
|
|
5262
|
+
});
|
|
5263
|
+
};
|
|
5131
5264
|
const add = (chunk) => {
|
|
5132
|
-
if (typeof chunk === "string")
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
else
|
|
5265
|
+
if (typeof chunk === "string") {
|
|
5266
|
+
count(chunk.length);
|
|
5267
|
+
text += chunk;
|
|
5268
|
+
} else if (chunk instanceof Uint8Array) {
|
|
5269
|
+
count(chunk.length);
|
|
5270
|
+
text += decoder.decode(chunk, { stream: true });
|
|
5271
|
+
} else if (chunk instanceof ArrayBuffer) {
|
|
5272
|
+
count(chunk.byteLength);
|
|
5273
|
+
text += decoder.decode(new Uint8Array(chunk), { stream: true });
|
|
5274
|
+
} else throw new XlvError("XLV_E_INVALID_INPUT", "workbook.csv.read() got a stream chunk that is neither text nor bytes.", { hint: "Pass a stream of strings, Buffers or Uint8Arrays." });
|
|
5136
5275
|
};
|
|
5137
5276
|
const s = stream;
|
|
5138
5277
|
if (typeof stream === "string") add(stream);
|
|
@@ -5250,7 +5389,8 @@ var CsvIO = class {
|
|
|
5250
5389
|
}
|
|
5251
5390
|
/** Writes a worksheet to a CSV file (Node.js, Bun, Deno), replacing it. */
|
|
5252
5391
|
async writeFile(path, options = {}) {
|
|
5253
|
-
const
|
|
5392
|
+
const e = (options.encoding ?? "").toLowerCase();
|
|
5393
|
+
const bytes = encode(this.text(options, e === "latin1" || e === "binary"), options.encoding);
|
|
5254
5394
|
const fs = await nodeFs("writeFile");
|
|
5255
5395
|
try {
|
|
5256
5396
|
await fs.writeFile(path, bytes);
|
|
@@ -5274,7 +5414,7 @@ var CsvIO = class {
|
|
|
5274
5414
|
createInputStream() {
|
|
5275
5415
|
throw new Error("`CSV#createInputStream` is deprecated. You should use `CSV#read` instead. This method will be removed in version 5.0. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md");
|
|
5276
5416
|
}
|
|
5277
|
-
text(options) {
|
|
5417
|
+
text(options, latin1 = false) {
|
|
5278
5418
|
const key = options.sheetName || options.sheetId;
|
|
5279
5419
|
const sheet = this.workbook.getWorksheet(key);
|
|
5280
5420
|
if (key !== void 0 && !sheet) throw new XlvError("XLV_E_SHEET_NOT_FOUND", `The workbook has no worksheet ${typeof key === "string" ? `named "${key}"` : `with id ${String(key)}`}.`, {
|
|
@@ -5282,7 +5422,7 @@ var CsvIO = class {
|
|
|
5282
5422
|
hint: `Available worksheets: ${this.workbook.worksheets.map((w) => `"${w.name}"`).join(", ") || "none"}.`,
|
|
5283
5423
|
details: { available: this.workbook.worksheets.map((w) => w.name) }
|
|
5284
5424
|
});
|
|
5285
|
-
return worksheetCsv(sheet, options);
|
|
5425
|
+
return worksheetCsv(sheet, options, latin1);
|
|
5286
5426
|
}
|
|
5287
5427
|
};
|
|
5288
5428
|
// src/defined-names.ts
|
|
@@ -5335,6 +5475,33 @@ function decodeEx(value) {
|
|
|
5335
5475
|
...address
|
|
5336
5476
|
} : address;
|
|
5337
5477
|
}
|
|
5478
|
+
/**
|
|
5479
|
+
* Refuses a range that reaches past the sheet (row 1048576, column XFD) before its cells are
|
|
5480
|
+
* created one by one. exceljs bounds only column letters, so a range such as `$A$1:$A$99999999999`
|
|
5481
|
+
* makes it loop for hours; xlvane throws instead (security review J, C1).
|
|
5482
|
+
*/
|
|
5483
|
+
function checkGrid(location) {
|
|
5484
|
+
const { bottom = 0, right = 0 } = location;
|
|
5485
|
+
if (bottom <= 1048576 && right <= 16384) return;
|
|
5486
|
+
const where = `${location.sheetName === void 0 ? "" : `${location.sheetName}!`}rows ${String(location.top)}-${String(bottom)}, columns ${String(location.left)}-${String(right)}`;
|
|
5487
|
+
throw new XlvError("XLV_E_INVALID_CELL_REF", `The range ${where.slice(0, 80)} reaches past the sheet (row 1048576, column XFD).`, {
|
|
5488
|
+
hint: "Use a range inside A1:XFD1048576, for example \"Data!$A$1:$B$10\".",
|
|
5489
|
+
details: { ref: where.slice(0, 80) }
|
|
5490
|
+
});
|
|
5491
|
+
}
|
|
5492
|
+
/**
|
|
5493
|
+
* @internal The number of cells the `model` setter of {@link DefinedNames} creates for one range
|
|
5494
|
+
* read from a file, or `null` for a range that reaches past the sheet (it is dropped, as invalid
|
|
5495
|
+
* ranges are; security review J, C1).
|
|
5496
|
+
*/
|
|
5497
|
+
function rangeCellCount(rangeStr) {
|
|
5498
|
+
if (!RANGE.test(rangeStr.split("!").pop() ?? "")) return 0;
|
|
5499
|
+
const location = decodeEx(rangeStr);
|
|
5500
|
+
if (!location.top) return location.row !== void 0 && location.col !== void 0 ? 1 : 0;
|
|
5501
|
+
const { top, left = 0, bottom = 0, right = 0 } = location;
|
|
5502
|
+
if (bottom > 1048576 || right > 16384) return null;
|
|
5503
|
+
return Math.max(0, bottom - top + 1) * Math.max(0, right - left + 1);
|
|
5504
|
+
}
|
|
5338
5505
|
/** The sheet prefix exceljs writes (lib/doc/range.js L153): quoted unless letters and digits only. */
|
|
5339
5506
|
function sheetPrefix(sheetName) {
|
|
5340
5507
|
if (!sheetName) return "";
|
|
@@ -5368,8 +5535,10 @@ var CellMatrix = class {
|
|
|
5368
5535
|
return this.sheets.get(sheetName)?.[rowNumber]?.[colNumber];
|
|
5369
5536
|
}
|
|
5370
5537
|
addCellEx(address) {
|
|
5371
|
-
if (address.top)
|
|
5372
|
-
|
|
5538
|
+
if (address.top) {
|
|
5539
|
+
checkGrid(address);
|
|
5540
|
+
for (let row = address.top; row <= (address.bottom ?? 0); row++) for (let col = address.left ?? 0; col <= (address.right ?? 0); col++) this.getCellAt(address.sheetName, row, col);
|
|
5541
|
+
} else this.findCellEx(address, true);
|
|
5373
5542
|
}
|
|
5374
5543
|
getCellEx(address) {
|
|
5375
5544
|
return this.findCellEx(address, true);
|
|
@@ -5466,6 +5635,7 @@ var DefinedNames = class {
|
|
|
5466
5635
|
this.addEx(decodeEx(locStr), name);
|
|
5467
5636
|
}
|
|
5468
5637
|
addEx(location, name) {
|
|
5638
|
+
if (location.top) checkGrid(location);
|
|
5469
5639
|
const matrix = this.getMatrix(name);
|
|
5470
5640
|
if (location.top) for (let col = location.left ?? 0; col <= (location.right ?? 0); col++) for (let row = location.top; row <= (location.bottom ?? 0); row++) matrix.getCellAt(location.sheetName, row, col);
|
|
5471
5641
|
else matrix.addCellEx(location);
|
|
@@ -5736,12 +5906,7 @@ var XlsxIO = class {
|
|
|
5736
5906
|
}
|
|
5737
5907
|
/** Reads XLSX data from a readable stream (Node.js stream or any async iterable of bytes). */
|
|
5738
5908
|
async read(stream, options) {
|
|
5739
|
-
const chunks =
|
|
5740
|
-
let size = 0;
|
|
5741
|
-
for await (const chunk of stream) {
|
|
5742
|
-
chunks.push(chunk);
|
|
5743
|
-
size += chunk.length;
|
|
5744
|
-
}
|
|
5909
|
+
const { chunks, size } = await collectInput(stream, "xlsx.read()");
|
|
5745
5910
|
const all = new Uint8Array(size);
|
|
5746
5911
|
let at = 0;
|
|
5747
5912
|
for (const c of chunks) {
|
|
@@ -5883,12 +6048,71 @@ var Workbook = class {
|
|
|
5883
6048
|
get _date1904() {
|
|
5884
6049
|
return this.properties.date1904 === true;
|
|
5885
6050
|
}
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
6051
|
+
_byName = /* @__PURE__ */ new Map();
|
|
6052
|
+
_firstFree = 1;
|
|
6053
|
+
_maxOrderNo = 0;
|
|
6054
|
+
_maxOrderNoStale = false;
|
|
6055
|
+
/** @internal Whether a sheet other than `exclude` has this name, ignoring case (the duplicate check of Worksheet.name). */
|
|
6056
|
+
_nameTaken(name, exclude) {
|
|
6057
|
+
const sheet = this._byName.get(name.toLowerCase());
|
|
6058
|
+
return sheet !== void 0 && sheet !== exclude;
|
|
6059
|
+
}
|
|
6060
|
+
/** @internal Keeps the name index in step with a renamed sheet. */
|
|
6061
|
+
_renamed(sheet, oldName) {
|
|
6062
|
+
if (this._sheets[sheet.id] !== sheet) return;
|
|
6063
|
+
const old = oldName.toLowerCase();
|
|
6064
|
+
if (this._byName.get(old) === sheet) this._byName.delete(old);
|
|
6065
|
+
this._byName.set(sheet.name.toLowerCase(), sheet);
|
|
6066
|
+
}
|
|
6067
|
+
/** @internal Keeps the largest orderNo in step with a changed one. */
|
|
6068
|
+
_orderNoChanged(sheet, oldOrderNo) {
|
|
6069
|
+
if (this._sheets[sheet.id] !== sheet || this._maxOrderNoStale) return;
|
|
6070
|
+
const value = sheet.orderNo;
|
|
6071
|
+
const plain = (v) => v === void 0 || typeof v === "number" && !Number.isNaN(v);
|
|
6072
|
+
if (!plain(value) || !plain(oldOrderNo)) this._maxOrderNoStale = true;
|
|
6073
|
+
else if (value !== void 0 && value > this._maxOrderNo) this._maxOrderNo = value;
|
|
6074
|
+
else if (oldOrderNo !== void 0 && oldOrderNo >= this._maxOrderNo && (value === void 0 || value < oldOrderNo)) this._maxOrderNoStale = true;
|
|
6075
|
+
}
|
|
6076
|
+
/** @internal Puts a worksheet into `_sheets` at its id and indexes it. */
|
|
6077
|
+
_putSheet(sheet) {
|
|
6078
|
+
const previous = this._sheets[sheet.id];
|
|
6079
|
+
if (previous && previous !== sheet) this._dropSheet(previous);
|
|
6080
|
+
this._sheets[sheet.id] = sheet;
|
|
6081
|
+
this._byName.set(sheet.name.toLowerCase(), sheet);
|
|
6082
|
+
this._orderNoChanged(sheet, void 0);
|
|
6083
|
+
}
|
|
6084
|
+
/** Takes a worksheet out of `_sheets` and its indexes. */
|
|
6085
|
+
_dropSheet(sheet) {
|
|
6086
|
+
this._sheets[sheet.id] = void 0;
|
|
6087
|
+
const key = sheet.name.toLowerCase();
|
|
6088
|
+
if (this._byName.get(key) === sheet) this._byName.delete(key);
|
|
6089
|
+
if (sheet.id < this._firstFree) this._firstFree = Math.max(1, sheet.id);
|
|
6090
|
+
const order = sheet.orderNo;
|
|
6091
|
+
if (order !== void 0 && !(typeof order === "number" && order < this._maxOrderNo)) this._maxOrderNoStale = true;
|
|
6092
|
+
}
|
|
6093
|
+
/** Empties `_sheets` and its indexes. */
|
|
6094
|
+
_clearSheets() {
|
|
6095
|
+
this._sheets = [];
|
|
6096
|
+
this._byName = /* @__PURE__ */ new Map();
|
|
6097
|
+
this._firstFree = 1;
|
|
6098
|
+
this._maxOrderNo = 0;
|
|
6099
|
+
this._maxOrderNoStale = false;
|
|
6100
|
+
}
|
|
6101
|
+
/** The largest orderNo of the sheets, as exceljs computes it (lib/doc/workbook.js L81-85). */
|
|
6102
|
+
_lastOrderNo() {
|
|
6103
|
+
if (this._maxOrderNoStale) {
|
|
6104
|
+
this._maxOrderNo = this._sheets.reduce((acc, ws) => ws?.orderNo !== void 0 && ws.orderNo > acc ? ws.orderNo : acc, 0);
|
|
6105
|
+
this._maxOrderNoStale = false;
|
|
6106
|
+
}
|
|
6107
|
+
return this._maxOrderNo;
|
|
6108
|
+
}
|
|
6109
|
+
/** The id the next worksheet gets (lib/doc/workbook.js `nextId`): the lowest free id. */
|
|
5889
6110
|
get nextId() {
|
|
5890
|
-
|
|
5891
|
-
|
|
6111
|
+
const sheets = this._sheets;
|
|
6112
|
+
let i = Math.max(1, this._firstFree);
|
|
6113
|
+
while (i < sheets.length && sheets[i]) i++;
|
|
6114
|
+
this._firstFree = i;
|
|
6115
|
+
return i < sheets.length ? i : sheets.length || 1;
|
|
5892
6116
|
}
|
|
5893
6117
|
/** Adds a worksheet. Names follow exceljs rules (31 characters at most; longer names are cut). */
|
|
5894
6118
|
addWorksheet(name, options) {
|
|
@@ -5897,13 +6121,34 @@ var Workbook = class {
|
|
|
5897
6121
|
/** @internal Adds a worksheet with a given id (a loaded file's sheetId, a model's id; D-F03). */
|
|
5898
6122
|
_addWorksheetWithId(id, name, options) {
|
|
5899
6123
|
const sheet = new Worksheet(this, id, name, options);
|
|
5900
|
-
|
|
5901
|
-
this.
|
|
6124
|
+
const last = this._lastOrderNo();
|
|
6125
|
+
this._putSheet(sheet);
|
|
6126
|
+
sheet.orderNo = last + 1;
|
|
5902
6127
|
return sheet;
|
|
5903
6128
|
}
|
|
6129
|
+
/**
|
|
6130
|
+
* @internal Adds a worksheet read from a file (`xlsx.load`, `stream.xlsx.WorkbookReader`). A
|
|
6131
|
+
* name exceljs refuses gives an XlvError with exceljs's message, the sheet and a hint, instead
|
|
6132
|
+
* of the plain Error of `addWorksheet`; an empty name is refused too.
|
|
6133
|
+
*/
|
|
6134
|
+
_addReadWorksheet(id, name, options) {
|
|
6135
|
+
const checked = checkSheetName(name, (n) => this._nameTaken(n));
|
|
6136
|
+
if (checked.message !== void 0) {
|
|
6137
|
+
const existing = checked.duplicate ? this._byName.get(checked.name.toLowerCase())?.name : void 0;
|
|
6138
|
+
throw new XlvError(checked.duplicate ? "XLV_E_DUPLICATE_SHEET_NAME" : "XLV_E_INVALID_SHEET_NAME", checked.message, {
|
|
6139
|
+
location: { sheet: name },
|
|
6140
|
+
hint: checked.duplicate ? "Two worksheets of the file have the same name, ignoring case and after the first 31 characters, which exceljs does not accept. Rename one in a spreadsheet application, or read the file with xlvane's readWorkbook." : "The file names a worksheet in a way exceljs does not accept. Rename the sheet in a spreadsheet application, or read the file with xlvane's readWorkbook.",
|
|
6141
|
+
details: existing === void 0 ? { name } : {
|
|
6142
|
+
name,
|
|
6143
|
+
existing
|
|
6144
|
+
}
|
|
6145
|
+
});
|
|
6146
|
+
}
|
|
6147
|
+
return this._addWorksheetWithId(id, name, options);
|
|
6148
|
+
}
|
|
5904
6149
|
removeWorksheet(id) {
|
|
5905
6150
|
const sheet = this.getWorksheet(id);
|
|
5906
|
-
if (sheet) this.
|
|
6151
|
+
if (sheet) this._dropSheet(sheet);
|
|
5907
6152
|
}
|
|
5908
6153
|
/** Removes a worksheet by id or name, as `removeWorksheet` (lib/doc/workbook.js L77). */
|
|
5909
6154
|
removeWorksheetEx(worksheet) {
|
|
@@ -5979,7 +6224,11 @@ var Workbook = class {
|
|
|
5979
6224
|
getWorksheet(id) {
|
|
5980
6225
|
if (id === void 0) return this.worksheets[0];
|
|
5981
6226
|
if (typeof id === "number") return this._sheets[id];
|
|
5982
|
-
if (typeof id === "string")
|
|
6227
|
+
if (typeof id === "string") {
|
|
6228
|
+
const sheet = this._byName.get(id.toLowerCase());
|
|
6229
|
+
if (sheet?.name === id) return sheet;
|
|
6230
|
+
return id === "" ? this.worksheets.find((w) => w.name === id) : void 0;
|
|
6231
|
+
}
|
|
5983
6232
|
}
|
|
5984
6233
|
/** Worksheets in workbook order. */
|
|
5985
6234
|
get worksheets() {
|
|
@@ -6027,9 +6276,10 @@ var Workbook = class {
|
|
|
6027
6276
|
const spend = (positions, what, sheet) => {
|
|
6028
6277
|
spent += positions;
|
|
6029
6278
|
if (spent <= budget) return;
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6279
|
+
const of = sheet === void 0 ? "" : ` of worksheet "${sheet}"`;
|
|
6280
|
+
throw new XlvError("XLV_E_LIMIT_EXCEEDED", `${what}${of} would create more than ${String(budget)} cells or columns for ${String(cellsRead)} cells in the file.`, {
|
|
6281
|
+
...sheet === void 0 ? {} : { location: { sheet } },
|
|
6282
|
+
hint: sheet === void 0 ? "The file declares names far larger than its data. Read it with xlvane's readWorkbook, which does not create these objects." : "The file declares ranges far larger than its data. Load it with ignoreNodes for that element (for example ignoreNodes: [\"mergeCells\"]), or read it with xlvane's readWorkbook, which does not create these objects.",
|
|
6033
6283
|
details: {
|
|
6034
6284
|
limit: "maxCells",
|
|
6035
6285
|
value: budget
|
|
@@ -6037,6 +6287,7 @@ var Workbook = class {
|
|
|
6037
6287
|
});
|
|
6038
6288
|
};
|
|
6039
6289
|
const rowBudget = Math.max(67108864, 16 * cellsRead);
|
|
6290
|
+
const validations = validationBudget();
|
|
6040
6291
|
let rowsLooked = 0;
|
|
6041
6292
|
for (const s of read.sheets) {
|
|
6042
6293
|
if (s.kind !== "worksheet" || options.maxRows === void 0 && options.maxCols === void 0 || !use("sheetData")) continue;
|
|
@@ -6058,7 +6309,7 @@ var Workbook = class {
|
|
|
6058
6309
|
for (const count of perRow.values()) if (count > options.maxCols) limit("maxCols", options.maxCols, count);
|
|
6059
6310
|
}
|
|
6060
6311
|
}
|
|
6061
|
-
this.
|
|
6312
|
+
this._clearSheets();
|
|
6062
6313
|
if (read.dateSystem === "1904") this.properties.date1904 = true;
|
|
6063
6314
|
else delete this.properties.date1904;
|
|
6064
6315
|
const p = read.properties;
|
|
@@ -6078,11 +6329,9 @@ var Workbook = class {
|
|
|
6078
6329
|
this.company = p.company;
|
|
6079
6330
|
this.manager = p.manager;
|
|
6080
6331
|
this._media = mediaOut(read.images);
|
|
6081
|
-
const
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
return Math.max(n, 0);
|
|
6085
|
-
};
|
|
6332
|
+
const worksheetsUpTo = [];
|
|
6333
|
+
for (const s of read.sheets) worksheetsUpTo.push((worksheetsUpTo[worksheetsUpTo.length - 1] ?? 0) + (s.kind === "worksheet" ? 1 : 0));
|
|
6334
|
+
const sheetIndex = (i) => i < 0 || worksheetsUpTo.length === 0 ? 0 : Math.max((worksheetsUpTo[Math.min(i, worksheetsUpTo.length - 1)] ?? 0) - 1, 0);
|
|
6086
6335
|
this.views = read.views.map((v) => {
|
|
6087
6336
|
const view = {
|
|
6088
6337
|
x: v.x ?? 0,
|
|
@@ -6097,14 +6346,22 @@ var Workbook = class {
|
|
|
6097
6346
|
});
|
|
6098
6347
|
this.calcProperties = read.calculation?.fullCalcOnLoad ? { fullCalcOnLoad: true } : {};
|
|
6099
6348
|
const mediaIds = new Map(read.images.map((img, i) => [img, i]));
|
|
6100
|
-
this._definedNames.model = read.definedNames.filter((d) => !d.name.startsWith("_xlnm.")).map((d) =>
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6349
|
+
this._definedNames.model = read.definedNames.filter((d) => !d.name.startsWith("_xlnm.")).map((d) => {
|
|
6350
|
+
const ranges = extractRanges(d.refersTo).filter((range) => {
|
|
6351
|
+
const cells = rangeCellCount(range);
|
|
6352
|
+
if (cells === null) return false;
|
|
6353
|
+
spend(cells, `The defined name "${d.name.slice(0, 255)}"`, void 0);
|
|
6354
|
+
return true;
|
|
6355
|
+
});
|
|
6356
|
+
return {
|
|
6357
|
+
name: d.name,
|
|
6358
|
+
ranges
|
|
6359
|
+
};
|
|
6360
|
+
});
|
|
6104
6361
|
const ids = freeIds(read.sheets.map((s) => s.kind === "worksheet" ? s.sheetId : void 0));
|
|
6105
6362
|
for (const [position, s] of read.sheets.entries()) {
|
|
6106
6363
|
if (s.kind !== "worksheet") continue;
|
|
6107
|
-
const ws = this.
|
|
6364
|
+
const ws = this._addReadWorksheet(ids[position] ?? this.nextId, s.name, { state: s.state });
|
|
6108
6365
|
ws.orderNo = position;
|
|
6109
6366
|
for (const c of use("cols") ? s.columns : []) {
|
|
6110
6367
|
const last = Math.min(c.max, 16384);
|
|
@@ -6183,13 +6440,27 @@ var Workbook = class {
|
|
|
6183
6440
|
for (const m of use("mergeCells") ? s.merges : []) {
|
|
6184
6441
|
if (!m.includes(":")) continue;
|
|
6185
6442
|
spend(areaOf(m), "A merged range", s.name);
|
|
6186
|
-
|
|
6443
|
+
try {
|
|
6444
|
+
ws.mergeCells(m);
|
|
6445
|
+
} catch (error) {
|
|
6446
|
+
if (error instanceof XlvError || !(error instanceof Error) || error.message !== "Cannot merge already merged cells") throw error;
|
|
6447
|
+
const other = mergeOverlapping(ws, m);
|
|
6448
|
+
throw new XlvError("XLV_E_OVERLAPPING_MERGE", `Cannot merge already merged cells: the merged range ${m} of worksheet "${s.name}" shares cells with ${other ?? "an earlier merged range"}.`, {
|
|
6449
|
+
location: {
|
|
6450
|
+
sheet: s.name,
|
|
6451
|
+
cell: m
|
|
6452
|
+
},
|
|
6453
|
+
hint: "The file has merged ranges that share cells, which exceljs does not accept. Load it with ignoreNodes: [\"mergeCells\"], or read it with xlvane's readWorkbook.",
|
|
6454
|
+
details: other === void 0 ? {} : { other },
|
|
6455
|
+
cause: error
|
|
6456
|
+
});
|
|
6457
|
+
}
|
|
6187
6458
|
}
|
|
6188
6459
|
if (use("sheetViews")) ws.views = [viewOut(s.view, s.freeze)];
|
|
6189
6460
|
const tab = colorOut(s.tabColor ?? void 0);
|
|
6190
6461
|
if (tab && use("sheetPr")) ws.properties["tabColor"] = tab;
|
|
6191
6462
|
if (s.autoFilter && use("autoFilter")) ws.autoFilter = s.autoFilter;
|
|
6192
|
-
if (use("dataValidations")) ws.dataValidations.model = dataValidationsOut(s.dataValidations, read.dateSystem === "1904");
|
|
6463
|
+
if (use("dataValidations")) ws.dataValidations.model = dataValidationsOut(s.dataValidations, read.dateSystem === "1904", validations);
|
|
6193
6464
|
if (use("conditionalFormatting")) ws.conditionalFormattings = conditionalFormatsOut(s.conditionalFormats);
|
|
6194
6465
|
if (use("sheetProtection")) ws.sheetProtection = s.protection ? protectionOut(s.protection) : null;
|
|
6195
6466
|
if (use("pageSetup")) ws.pageSetup = pageSetupOut(s.pageSetup, s.printArea, s.printTitles);
|
|
@@ -6213,6 +6484,31 @@ var Workbook = class {
|
|
|
6213
6484
|
}
|
|
6214
6485
|
};
|
|
6215
6486
|
/**
|
|
6487
|
+
* @internal The chunks of an input stream, read until it ends. Reading stops with
|
|
6488
|
+
* XLV_E_LIMIT_EXCEEDED as soon as the chunks pass maxInputBytes, the size the core reads at most,
|
|
6489
|
+
* so an endless stream is not collected first (security review J, C8).
|
|
6490
|
+
*/
|
|
6491
|
+
async function collectInput(stream, what) {
|
|
6492
|
+
const limit = DEFAULT_READ_LIMITS.maxInputBytes;
|
|
6493
|
+
const chunks = [];
|
|
6494
|
+
let size = 0;
|
|
6495
|
+
for await (const chunk of stream) {
|
|
6496
|
+
chunks.push(chunk);
|
|
6497
|
+
size += chunk.length;
|
|
6498
|
+
if (size > limit) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `The stream given to ${what} holds more than ${String(limit)} bytes (maxInputBytes).`, {
|
|
6499
|
+
hint: "Check that the stream is the XLSX file you expect. A larger file from a trusted source can be read with xlvane's readWorkbook and a higher limit, for example { limits: { maxInputBytes: 1073741824 } }.",
|
|
6500
|
+
details: {
|
|
6501
|
+
limit: "maxInputBytes",
|
|
6502
|
+
value: limit
|
|
6503
|
+
}
|
|
6504
|
+
});
|
|
6505
|
+
}
|
|
6506
|
+
return {
|
|
6507
|
+
chunks,
|
|
6508
|
+
size
|
|
6509
|
+
};
|
|
6510
|
+
}
|
|
6511
|
+
/**
|
|
6216
6512
|
* Worksheet ids for a list of wanted ids: each whole number from 1 to 65534 (the sheet ids Office
|
|
6217
6513
|
* uses, W2-20) that is not wanted by an earlier entry is kept; other entries (missing, invalid,
|
|
6218
6514
|
* larger or repeated ids) get the lowest ids no entry wants, in order. The limit keeps the
|
|
@@ -6236,17 +6532,33 @@ function freeIds(wanted) {
|
|
|
6236
6532
|
return next;
|
|
6237
6533
|
});
|
|
6238
6534
|
}
|
|
6239
|
-
/**
|
|
6240
|
-
function
|
|
6535
|
+
/** The rows and columns of a range such as "A1:C3" or a cell, or `null` for anything else. */
|
|
6536
|
+
function rangeOf(ref) {
|
|
6241
6537
|
const m = /^([A-Z]{1,3})(\d{1,7})(?::([A-Z]{1,3})(\d{1,7}))?$/.exec(ref.replace(/\$/g, ""));
|
|
6242
|
-
if (!m) return
|
|
6538
|
+
if (!m) return null;
|
|
6243
6539
|
const col = (letters) => {
|
|
6244
6540
|
let n = 0;
|
|
6245
6541
|
for (const ch of letters) n = n * 26 + ch.charCodeAt(0) - 64;
|
|
6246
6542
|
return n;
|
|
6247
6543
|
};
|
|
6248
6544
|
const [, c1 = "A", r1 = "1", c2 = c1, r2 = r1] = m;
|
|
6249
|
-
return
|
|
6545
|
+
return {
|
|
6546
|
+
top: Math.min(Number(r1), Number(r2)),
|
|
6547
|
+
left: Math.min(col(c1), col(c2)),
|
|
6548
|
+
bottom: Math.max(Number(r1), Number(r2)),
|
|
6549
|
+
right: Math.max(col(c1), col(c2))
|
|
6550
|
+
};
|
|
6551
|
+
}
|
|
6552
|
+
/** Cells in a range such as "A1:C3" (1 for a cell, 0 for anything else). */
|
|
6553
|
+
function areaOf(ref) {
|
|
6554
|
+
const r = rangeOf(ref);
|
|
6555
|
+
return r ? (r.bottom - r.top + 1) * (r.right - r.left + 1) : 0;
|
|
6556
|
+
}
|
|
6557
|
+
/** A merged range of the worksheet that shares cells with `ref`, as "A1:B2". */
|
|
6558
|
+
function mergeOverlapping(ws, ref) {
|
|
6559
|
+
const r = rangeOf(ref);
|
|
6560
|
+
if (!r) return void 0;
|
|
6561
|
+
for (const o of ws._merges.values()) if (o.top <= r.bottom && r.top <= o.bottom && o.left <= r.right && r.left <= o.right) return `${columnLetter(o.left)}${String(o.top)}:${columnLetter(o.right)}${String(o.bottom)}`;
|
|
6250
6562
|
}
|
|
6251
6563
|
// src/stream.ts
|
|
6252
6564
|
/**
|
|
@@ -6641,7 +6953,7 @@ var WorkbookWriter = class {
|
|
|
6641
6953
|
/** Adds a worksheet. Sheets are written in the order their first rows are committed. */
|
|
6642
6954
|
addWorksheet(name, options) {
|
|
6643
6955
|
const sheet = new WorksheetWriter(this, this.nextId++, name, options);
|
|
6644
|
-
this._book.
|
|
6956
|
+
this._book._putSheet(sheet);
|
|
6645
6957
|
this.open.push(sheet);
|
|
6646
6958
|
return sheet;
|
|
6647
6959
|
}
|
|
@@ -6894,8 +7206,7 @@ var WorkbookReader = class extends Emitter {
|
|
|
6894
7206
|
return openWorkbookStreamFile(input);
|
|
6895
7207
|
}
|
|
6896
7208
|
if (input instanceof Uint8Array || input instanceof ArrayBuffer) return openWorkbookStream(input);
|
|
6897
|
-
const chunks =
|
|
6898
|
-
for await (const c of input) chunks.push(c);
|
|
7209
|
+
const { chunks } = await collectInput(input, "stream.xlsx.WorkbookReader");
|
|
6899
7210
|
return openWorkbookStream(new Blob(chunks));
|
|
6900
7211
|
}
|
|
6901
7212
|
/** Worksheet events in workbook order, as exceljs `parse()` yields them (`{ eventType: 'worksheet', value }`). */
|
|
@@ -6914,7 +7225,7 @@ var WorkbookReader = class extends Emitter {
|
|
|
6914
7225
|
type: "worksheet",
|
|
6915
7226
|
id: String(id)
|
|
6916
7227
|
});
|
|
6917
|
-
const sheet = host.
|
|
7228
|
+
const sheet = host._addReadWorksheet(host.nextId, info.name, { state: info.state });
|
|
6918
7229
|
const name = info.name;
|
|
6919
7230
|
const rows = async function* () {
|
|
6920
7231
|
for await (const r of book.rows(name)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xlvane/compat",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "The commonly used exceljs 4.4.0 API on top of xlvane, for switching with one line in package.json.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xlsx",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"build": "tsdown"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"xlvane": "^1.2.
|
|
75
|
+
"xlvane": "^1.2.3"
|
|
76
76
|
},
|
|
77
77
|
"publishConfig": {
|
|
78
78
|
"access": "public",
|