@xlvane/compat 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +56 -17
- package/dist/index.js +970 -130
- package/package.json +2 -2
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
|
}
|
|
@@ -1382,6 +1434,8 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1382
1434
|
}
|
|
1383
1435
|
return c;
|
|
1384
1436
|
};
|
|
1437
|
+
const styleFor = (cell) => useStyles ? cellStyleIn(cell, styleOf(cell.style)) : isDateCell(cell) ? { numFmt: 14 } : {};
|
|
1438
|
+
const uniform = uniformMerges(ws);
|
|
1385
1439
|
ws._rows.forEach((row) => {
|
|
1386
1440
|
if (!row) return;
|
|
1387
1441
|
if (row.height !== void 0 || row.hidden || row.outlineLevel) sheet.row(row.number, {
|
|
@@ -1391,13 +1445,31 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1391
1445
|
...row.collapsed ? { collapsed: true } : {}
|
|
1392
1446
|
});
|
|
1393
1447
|
row._cells.forEach((cell) => {
|
|
1394
|
-
if (cell)
|
|
1448
|
+
if (!cell) return;
|
|
1449
|
+
if (cell._master !== null && uniform.masters.has(cell._master)) {
|
|
1450
|
+
if (cell.note !== void 0) at(ws.name, cell.address, () => {
|
|
1451
|
+
const note = noteIn(cell);
|
|
1452
|
+
if (note) sheet.note(cell.address, note);
|
|
1453
|
+
});
|
|
1454
|
+
} else at(ws.name, cell.address, () => writeCell(cell));
|
|
1395
1455
|
});
|
|
1396
1456
|
});
|
|
1457
|
+
for (const m of uniform.ranges) {
|
|
1458
|
+
const master = ws.findCell(m.top, m.left);
|
|
1459
|
+
if (!master) continue;
|
|
1460
|
+
const style = styleFor(master);
|
|
1461
|
+
if (Object.keys(style).length === 0) continue;
|
|
1462
|
+
const width = m.right - m.left + 1;
|
|
1463
|
+
const step = Math.max(1, Math.floor(MERGE_STYLE_BLOCK / width));
|
|
1464
|
+
for (let top = m.top; top <= m.bottom; top += step) {
|
|
1465
|
+
const bottom = Math.min(m.bottom, top + step - 1);
|
|
1466
|
+
sheet.style(`${columnLetter(m.left)}${String(top)}:${columnLetter(m.right)}${String(bottom)}`, style);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1397
1469
|
function writeCell(cell) {
|
|
1398
1470
|
const note = noteIn(cell);
|
|
1399
1471
|
if (note) sheet.note(cell.address, note);
|
|
1400
|
-
const style =
|
|
1472
|
+
const style = styleFor(cell);
|
|
1401
1473
|
const value = cell.type === ValueType.Merge ? null : valueIn(cell, sharedRanges);
|
|
1402
1474
|
if (value === null && Object.keys(style).length === 0) return;
|
|
1403
1475
|
sheet.set(cell.address, value, style);
|
|
@@ -1433,6 +1505,55 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1433
1505
|
for (const d of meta.definedNames ?? []) book.defineName(d.name, d.ranges.join(","));
|
|
1434
1506
|
return book;
|
|
1435
1507
|
}
|
|
1508
|
+
/** Cells styled by one `sheet.style` call for a merged range: at most what the core styles at once. */
|
|
1509
|
+
const MERGE_STYLE_BLOCK = 1048576;
|
|
1510
|
+
/** Whether two exceljs styles have the same parts (the same objects, as a merge's copies have). */
|
|
1511
|
+
function sameStyleParts(a, b) {
|
|
1512
|
+
if (a === b) return true;
|
|
1513
|
+
let n = 0;
|
|
1514
|
+
for (const k in a) {
|
|
1515
|
+
if (a[k] !== b[k]) return false;
|
|
1516
|
+
n++;
|
|
1517
|
+
}
|
|
1518
|
+
for (const _ in b) n--;
|
|
1519
|
+
return n === 0;
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* The merged ranges of a worksheet whose other cells all exist, belong to the range's master and
|
|
1523
|
+
* carry the master's style (the copy `mergeCells` gives them, as in exceljs), and the masters all
|
|
1524
|
+
* of whose ranges are such. Their cells are written as the master's style over the range (K2),
|
|
1525
|
+
* and their notes one by one (K3).
|
|
1526
|
+
*/
|
|
1527
|
+
function uniformMerges(ws) {
|
|
1528
|
+
const ranges = [];
|
|
1529
|
+
const masters = /* @__PURE__ */ new Set();
|
|
1530
|
+
const mixed = /* @__PURE__ */ new Set();
|
|
1531
|
+
for (const m of ws._merges.values()) {
|
|
1532
|
+
const master = ws.findCell(m.top, m.left);
|
|
1533
|
+
if (!master) continue;
|
|
1534
|
+
let ok = true;
|
|
1535
|
+
for (let r = m.top; ok && r <= m.bottom; r++) {
|
|
1536
|
+
const row = ws.findRow(r);
|
|
1537
|
+
for (let c = m.left; c <= m.right; c++) {
|
|
1538
|
+
if (r === m.top && c === m.left) continue;
|
|
1539
|
+
const cell = row?.findCell(c);
|
|
1540
|
+
if (!cell || cell._master !== master || !sameStyleParts(cell.style, master.style)) {
|
|
1541
|
+
ok = false;
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (ok) {
|
|
1547
|
+
ranges.push(m);
|
|
1548
|
+
masters.add(master);
|
|
1549
|
+
} else mixed.add(master);
|
|
1550
|
+
}
|
|
1551
|
+
for (const master of mixed) masters.delete(master);
|
|
1552
|
+
return {
|
|
1553
|
+
ranges: ranges.filter((m) => masters.has(ws.findCell(m.top, m.left))),
|
|
1554
|
+
masters
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1436
1557
|
/**
|
|
1437
1558
|
* The validations, conditional formats, protection and printing settings of a worksheet in the
|
|
1438
1559
|
* form of the core API (D-C06). exceljs always writes page margins and page setup
|
|
@@ -1546,6 +1667,18 @@ function viewOut(view, freeze) {
|
|
|
1546
1667
|
};
|
|
1547
1668
|
}
|
|
1548
1669
|
/**
|
|
1670
|
+
* Whether a formula read by xlvane has text, without making the core build it: the core moves a
|
|
1671
|
+
* shared master's text to a dependent only when the dependent's `text` is read (a getter), and
|
|
1672
|
+
* gives `null` (a plain value) when it cannot move it. Reading the text of every dependent made a
|
|
1673
|
+
* small file with a huge master and many dependents exhaust memory (K2), and exceljs reads a
|
|
1674
|
+
* dependent as `{ sharedFormula }` without its text anyway.
|
|
1675
|
+
*/
|
|
1676
|
+
function hasFormulaText(f) {
|
|
1677
|
+
const d = Object.getOwnPropertyDescriptor(f, "text");
|
|
1678
|
+
if (d !== void 0 && d.get !== void 0) return true;
|
|
1679
|
+
return f.text !== null;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1549
1682
|
* A formula read by xlvane in exceljs's shape (cell-xform.js L460–470; X4-29): a shared-formula
|
|
1550
1683
|
* master `{ formula, result, ref, shareType: 'shared' }`, a dependent `{ sharedFormula: master,
|
|
1551
1684
|
* result }` (its `cell.formula` is translated), an array formula `{ formula, result, ref,
|
|
@@ -1725,11 +1858,17 @@ function rangeText(r) {
|
|
|
1725
1858
|
return r.top === r.bottom && r.left === r.right ? a : `${a}:${address(r.bottom, r.right)}`;
|
|
1726
1859
|
}
|
|
1727
1860
|
/**
|
|
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
|
|
1861
|
+
* Cells per workbook up to which validations read from a file are given to every cell they
|
|
1862
|
+
* cover, as exceljs does (data-validations-xform.js L229–239). Larger ranges, such as whole
|
|
1863
|
+
* columns, and ranges read after the budget is spent keep their range as the key; `find()` and
|
|
1864
|
+
* `cell.dataValidation` still answer for every cell. The budget is shared by all sheets of a
|
|
1865
|
+
* load, so many sheets cannot each expand 100 000 cells (security review J, C2).
|
|
1731
1866
|
*/
|
|
1732
1867
|
const MAX_EXPANDED_CELLS = 1e5;
|
|
1868
|
+
/** @internal The budget of {@link dataValidationsOut} for one workbook load. */
|
|
1869
|
+
function validationBudget() {
|
|
1870
|
+
return { left: MAX_EXPANDED_CELLS };
|
|
1871
|
+
}
|
|
1733
1872
|
/**
|
|
1734
1873
|
* The data validations of a worksheet (`worksheet.dataValidations`), keyed by cell address or
|
|
1735
1874
|
* range as in exceljs (lib/doc/data-validations.js). `remove()` deletes the entry; exceljs
|
|
@@ -1906,21 +2045,23 @@ function validationOut(d, date1904) {
|
|
|
1906
2045
|
]) if (d[k] !== null) out[k] = d[k];
|
|
1907
2046
|
return out;
|
|
1908
2047
|
}
|
|
1909
|
-
/**
|
|
1910
|
-
|
|
2048
|
+
/**
|
|
2049
|
+
* The `worksheet.dataValidations` model of validations read by xlvane. `budget` is the number of
|
|
2050
|
+
* cells still to expand; pass the same object for every sheet of a workbook.
|
|
2051
|
+
*/
|
|
2052
|
+
function dataValidationsOut(list, date1904, budget = validationBudget()) {
|
|
1911
2053
|
const model = Object.create(null);
|
|
1912
|
-
let budget = MAX_EXPANDED_CELLS;
|
|
1913
2054
|
for (const d of list) {
|
|
1914
2055
|
const v = validationOut(d, date1904);
|
|
1915
2056
|
for (const part of d.sqref.split(/\s+/)) {
|
|
1916
2057
|
const r = parseRange(part);
|
|
1917
2058
|
if (!r) continue;
|
|
1918
2059
|
const size = (r.bottom - r.top + 1) * (r.right - r.left + 1);
|
|
1919
|
-
if (size > budget) {
|
|
2060
|
+
if (size > budget.left) {
|
|
1920
2061
|
model[part] = v;
|
|
1921
2062
|
continue;
|
|
1922
2063
|
}
|
|
1923
|
-
budget -= size;
|
|
2064
|
+
budget.left -= size;
|
|
1924
2065
|
for (let row = r.top; row <= r.bottom; row++) for (let col = r.left; col <= r.right; col++) model[address(row, col)] = v;
|
|
1925
2066
|
}
|
|
1926
2067
|
}
|
|
@@ -2798,6 +2939,56 @@ function notSupported(feature) {
|
|
|
2798
2939
|
details: { feature }
|
|
2799
2940
|
});
|
|
2800
2941
|
}
|
|
2942
|
+
/**
|
|
2943
|
+
* The shared-formula text moved per workbook (K4 S1, D-K04). A move costs time in proportion to the
|
|
2944
|
+
* master's length, and the load-time budget is spent once, so a 4 KB file whose 1 000 000-character
|
|
2945
|
+
* master is shared with 250 cells took about 47 s to read every dependent's formula once. Two
|
|
2946
|
+
* budgets, counting the master's text read plus the moved text built for each `cell.formula` that
|
|
2947
|
+
* is not kept yet:
|
|
2948
|
+
* - every move: 512 Mi characters, the default `maxSharedStringChars` with which xlvane's
|
|
2949
|
+
* `readWorkbook` expands shared formulas, so a workbook the core reads is read here too;
|
|
2950
|
+
* - moves of masters longer than 8192 characters, the longest formula Office writes ([MS-OI29500]
|
|
2951
|
+
* §2.1.1085 a): 32 Mi characters, so such a master stops after a few moves.
|
|
2952
|
+
*/
|
|
2953
|
+
const SHARED_FORMULA_TEXT = 536870912;
|
|
2954
|
+
const LONG_SHARED_FORMULA_TEXT = 33554432;
|
|
2955
|
+
const LONG_FORMULA = 8192;
|
|
2956
|
+
/** Characters moved in all and for long masters, per workbook (a `Worksheet.workbook`). */
|
|
2957
|
+
const sharedFormulaSpent = /* @__PURE__ */ new WeakMap();
|
|
2958
|
+
/**
|
|
2959
|
+
* @internal Moves the text of a shared-formula master at `master` to `row`/`col` of `sheet` with
|
|
2960
|
+
* `shiftFormula`, counted against the workbook's shared-formula budgets; `undefined` when the
|
|
2961
|
+
* formula cannot be moved. Throws `XLV_E_LIMIT_EXCEEDED` before a move that would pass one.
|
|
2962
|
+
*/
|
|
2963
|
+
function moveSharedFormula(workbook, sheet, text, master, row, col) {
|
|
2964
|
+
let spent = sharedFormulaSpent.get(workbook);
|
|
2965
|
+
if (spent === void 0) sharedFormulaSpent.set(workbook, spent = {
|
|
2966
|
+
chars: 0,
|
|
2967
|
+
long: 0
|
|
2968
|
+
});
|
|
2969
|
+
const cost = 2 * text.length;
|
|
2970
|
+
const long = text.length > LONG_FORMULA;
|
|
2971
|
+
const limit = long && spent.long + cost > LONG_SHARED_FORMULA_TEXT ? LONG_SHARED_FORMULA_TEXT : spent.chars + cost > SHARED_FORMULA_TEXT ? SHARED_FORMULA_TEXT : null;
|
|
2972
|
+
if (limit !== null) {
|
|
2973
|
+
const cell = `${columnLetter(col)}${String(row)}`;
|
|
2974
|
+
throw new XlvError("XLV_E_LIMIT_EXCEEDED", `Moving the shared formula of ${columnLetter(master.col)}${String(master.row)} (${String(text.length)} characters) to ${cell} of worksheet "${sheet}" would bring the shared-formula text moved in this workbook past ${String(limit)} characters.`, {
|
|
2975
|
+
location: {
|
|
2976
|
+
sheet,
|
|
2977
|
+
cell
|
|
2978
|
+
},
|
|
2979
|
+
hint: "The workbook shares very long formulas with many cells. Read the dependents' cached result (cell.result) instead of cell.formula, or read the file with xlvane's readWorkbook, whose shared-formula text is limited by limits.maxSharedStringChars.",
|
|
2980
|
+
details: {
|
|
2981
|
+
limit: "sharedFormulaText",
|
|
2982
|
+
value: limit
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
const moved = shiftFormula(text, row - master.row, col - master.col);
|
|
2987
|
+
const used = text.length + (moved?.length ?? 0);
|
|
2988
|
+
spent.chars += used;
|
|
2989
|
+
if (long) spent.long += used;
|
|
2990
|
+
return moved ?? void 0;
|
|
2991
|
+
}
|
|
2801
2992
|
/** Column letters for a 1-based number (A … XFD). */
|
|
2802
2993
|
function columnLetter(n) {
|
|
2803
2994
|
let s = "";
|
|
@@ -2842,7 +3033,7 @@ function typeOf(value) {
|
|
|
2842
3033
|
if (value instanceof Date) return ValueType.Date;
|
|
2843
3034
|
if (isRecord(value)) {
|
|
2844
3035
|
if (value["text"] && value["hyperlink"]) return ValueType.Hyperlink;
|
|
2845
|
-
if (value["
|
|
3036
|
+
if (value["sharedFormula"] || value["formula"]) return ValueType.Formula;
|
|
2846
3037
|
if (value["richText"]) return ValueType.RichText;
|
|
2847
3038
|
if (value["sharedString"]) return ValueType.SharedString;
|
|
2848
3039
|
if (value["error"]) return ValueType.Error;
|
|
@@ -2891,6 +3082,8 @@ var Cell = class {
|
|
|
2891
3082
|
_master = null;
|
|
2892
3083
|
/** @internal Number of merged ranges this cell is the master of. */
|
|
2893
3084
|
_mergeCount = 0;
|
|
3085
|
+
/** The moved formula of a `{ sharedFormula }` value, and what it was moved from (see `formula`). */
|
|
3086
|
+
_moved = void 0;
|
|
2894
3087
|
/** @internal Use `worksheet.getCell()`. */
|
|
2895
3088
|
constructor(_row, _col) {
|
|
2896
3089
|
this._row = _row;
|
|
@@ -2976,12 +3169,39 @@ var Cell = class {
|
|
|
2976
3169
|
const v = this.value;
|
|
2977
3170
|
if (!isRecord(v)) return void 0;
|
|
2978
3171
|
if (typeof v["formula"] === "string") return v["formula"];
|
|
2979
|
-
|
|
2980
|
-
|
|
3172
|
+
const shared = v["sharedFormula"];
|
|
3173
|
+
if (typeof shared === "string") {
|
|
3174
|
+
const masterRef = shared.replace(/\$/g, "");
|
|
2981
3175
|
const master = this.worksheet.findCell(masterRef);
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
3176
|
+
let text;
|
|
3177
|
+
let at;
|
|
3178
|
+
if (!master) {
|
|
3179
|
+
text = this.worksheet._sharedMasters?.get(masterRef.toUpperCase());
|
|
3180
|
+
if (text === void 0) return void 0;
|
|
3181
|
+
at = decodeAddress(masterRef);
|
|
3182
|
+
} else {
|
|
3183
|
+
if (master === this) return void 0;
|
|
3184
|
+
text = master.formula;
|
|
3185
|
+
if (text === void 0) return void 0;
|
|
3186
|
+
at = {
|
|
3187
|
+
row: master.row,
|
|
3188
|
+
col: master.col
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
const kept = this._moved;
|
|
3192
|
+
if (kept !== void 0 && kept.value === v && kept.shared === shared && kept.master === text && kept.masterRow === at.row && kept.masterCol === at.col && kept.row === this.row && kept.col === this.col) return kept.text;
|
|
3193
|
+
const moved = moveSharedFormula(this.worksheet.workbook, this.worksheet.name, text, at, this.row, this.col);
|
|
3194
|
+
this._moved = {
|
|
3195
|
+
value: v,
|
|
3196
|
+
shared,
|
|
3197
|
+
master: text,
|
|
3198
|
+
masterRow: at.row,
|
|
3199
|
+
masterCol: at.col,
|
|
3200
|
+
row: this.row,
|
|
3201
|
+
col: this.col,
|
|
3202
|
+
text: moved
|
|
3203
|
+
};
|
|
3204
|
+
return moved;
|
|
2985
3205
|
}
|
|
2986
3206
|
}
|
|
2987
3207
|
/** `FormulaType.Shared` for a `{ sharedFormula }` value, `Master` for other formulas, otherwise `None` (lib/doc/cell.js L807). */
|
|
@@ -3628,6 +3848,30 @@ function parseRangeArgs(args) {
|
|
|
3628
3848
|
right: Math.max(s.col, e.col)
|
|
3629
3849
|
};
|
|
3630
3850
|
}
|
|
3851
|
+
/**
|
|
3852
|
+
* @internal exceljs's rules for a worksheet name (lib/doc/worksheet.js L134-173): the name to
|
|
3853
|
+
* use, cut to 31 characters, or exceljs's message for a name it refuses. `taken` tells whether
|
|
3854
|
+
* another sheet of the workbook has a name (already cut) that equals it ignoring case.
|
|
3855
|
+
*/
|
|
3856
|
+
function checkSheetName(value, taken) {
|
|
3857
|
+
const fail = (message) => ({
|
|
3858
|
+
name: typeof value === "string" ? value : "",
|
|
3859
|
+
message,
|
|
3860
|
+
duplicate: false
|
|
3861
|
+
});
|
|
3862
|
+
if (typeof value !== "string") return fail("The name has to be a string.");
|
|
3863
|
+
if (value === "") return fail("The name can't be empty.");
|
|
3864
|
+
if (value === "History") return fail("The name \"History\" is protected. Please use a different name.");
|
|
3865
|
+
if (/[*?:/\\[\]]/.test(value)) return fail(`Worksheet name ${value} cannot include any of the following characters: * ? : \\ / [ ]`);
|
|
3866
|
+
if (/(^')|('$)/.test(value)) return fail(`The first or last character of worksheet name cannot be a single quotation mark: ${value}`);
|
|
3867
|
+
const name = value.length > 31 ? value.substring(0, 31) : value;
|
|
3868
|
+
if (taken(name)) return {
|
|
3869
|
+
name,
|
|
3870
|
+
message: `Worksheet name already exists: ${name}`,
|
|
3871
|
+
duplicate: true
|
|
3872
|
+
};
|
|
3873
|
+
return { name };
|
|
3874
|
+
}
|
|
3631
3875
|
/** The merged ranges of a worksheet by master address, with a counter of changes (D-H01). */
|
|
3632
3876
|
var MergeMap = class extends Map {
|
|
3633
3877
|
version = 0;
|
|
@@ -3666,7 +3910,15 @@ var Worksheet = class {
|
|
|
3666
3910
|
* largest of the other sheets for a new sheet (lib/doc/workbook.js L81-85), the 0-based position
|
|
3667
3911
|
* in the file for a loaded one (L203). Worksheets are listed in this order.
|
|
3668
3912
|
*/
|
|
3669
|
-
orderNo
|
|
3913
|
+
get orderNo() {
|
|
3914
|
+
return this._orderNo;
|
|
3915
|
+
}
|
|
3916
|
+
set orderNo(value) {
|
|
3917
|
+
const old = this._orderNo;
|
|
3918
|
+
this._orderNo = value;
|
|
3919
|
+
this.workbook._orderNoChanged?.(this, old);
|
|
3920
|
+
}
|
|
3921
|
+
_orderNo;
|
|
3670
3922
|
state;
|
|
3671
3923
|
views;
|
|
3672
3924
|
autoFilter = null;
|
|
@@ -3690,6 +3942,11 @@ var Worksheet = class {
|
|
|
3690
3942
|
_columns = [];
|
|
3691
3943
|
/** @internal */
|
|
3692
3944
|
_merges = new MergeMap();
|
|
3945
|
+
/**
|
|
3946
|
+
* @internal Formulas of shared-formula masters by address, for the dependents of a worksheet
|
|
3947
|
+
* whose rows are not kept (stream.xlsx.WorkbookReader), so `cell.formula` can move them (K2).
|
|
3948
|
+
*/
|
|
3949
|
+
_sharedMasters = void 0;
|
|
3693
3950
|
/** The cells covered by `_merges` (row × 16 385 + column), valid while `_mergedVersion` equals `_merges.version`. */
|
|
3694
3951
|
_mergedCells = /* @__PURE__ */ new Set();
|
|
3695
3952
|
_mergedVersion = -1;
|
|
@@ -3717,16 +3974,12 @@ var Worksheet = class {
|
|
|
3717
3974
|
}
|
|
3718
3975
|
/** Sheet name rules of exceljs (lib/doc/worksheet.js L134–173), including truncation to 31 characters. */
|
|
3719
3976
|
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;
|
|
3977
|
+
if (value === this._name) return;
|
|
3978
|
+
const checked = checkSheetName(value, (name) => this.workbook._nameTaken(name, this));
|
|
3979
|
+
if (checked.message !== void 0) throw new Error(checked.message);
|
|
3980
|
+
const old = this._name;
|
|
3981
|
+
this._name = checked.name;
|
|
3982
|
+
this.workbook._renamed?.(this, old);
|
|
3730
3983
|
}
|
|
3731
3984
|
/** Deprecated in exceljs in favour of `properties.tabColor`, which it reads and sets (lib/doc/worksheet.js L827). */
|
|
3732
3985
|
get tabColor() {
|
|
@@ -4610,8 +4863,12 @@ const INJECTION_START = /* @__PURE__ */ new Set([
|
|
|
4610
4863
|
"﹦",
|
|
4611
4864
|
"﹢",
|
|
4612
4865
|
"﹣",
|
|
4613
|
-
"﹫"
|
|
4866
|
+
"﹫",
|
|
4867
|
+
"",
|
|
4868
|
+
""
|
|
4614
4869
|
]);
|
|
4870
|
+
/** Characters a field is quoted for while formulas are escaped, whatever the delimiter: other applications may split on them (K2). */
|
|
4871
|
+
const COMMON_DELIMITERS = ",; |";
|
|
4615
4872
|
function invalid(name, message, hint) {
|
|
4616
4873
|
return new XlvError("XLV_E_INVALID_OPTION", message, {
|
|
4617
4874
|
hint,
|
|
@@ -4707,7 +4964,7 @@ function formatDate(date, format, utc) {
|
|
|
4707
4964
|
});
|
|
4708
4965
|
}
|
|
4709
4966
|
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 =
|
|
4967
|
+
const WORD = /(?<!\d)\d*[^-_:/,()\s\d]+/;
|
|
4711
4968
|
const ONE_OR_TWO = /\d\d?/;
|
|
4712
4969
|
const PARSE_EXPRESSIONS = {
|
|
4713
4970
|
A: WORD,
|
|
@@ -4897,9 +5154,41 @@ function malformed(line, message) {
|
|
|
4897
5154
|
details: { line }
|
|
4898
5155
|
});
|
|
4899
5156
|
}
|
|
4900
|
-
/**
|
|
4901
|
-
|
|
4902
|
-
|
|
5157
|
+
/** Rows and columns of a worksheet (Part 1 §18.3.1.73, ST_CellRef up to XFD1048576). */
|
|
5158
|
+
const MAX_SHEET_ROWS = 1048576;
|
|
5159
|
+
const MAX_SHEET_COLUMNS = 16384;
|
|
5160
|
+
/** Fields added to the worksheet in total, as the core's `maxCells` read limit (K3). */
|
|
5161
|
+
const MAX_CSV_CELLS = DEFAULT_READ_LIMITS.maxCells;
|
|
5162
|
+
function tooMany(what, line) {
|
|
5163
|
+
if (what === "cells") return new XlvError("XLV_E_LIMIT_EXCEEDED", `CSV line ${String(line)}: the CSV has more than ${String(MAX_CSV_CELLS)} fields in the rows read, the most csv.read adds to a worksheet.`, {
|
|
5164
|
+
location: { line },
|
|
5165
|
+
hint: "Split the CSV file into smaller parts, read only part of it with parserOptions.maxRows or skipRows, or read it with the core fromCSV, whose maxCells option can be raised for a trusted file.",
|
|
5166
|
+
details: {
|
|
5167
|
+
limit: "maxCells",
|
|
5168
|
+
value: MAX_CSV_CELLS,
|
|
5169
|
+
line
|
|
5170
|
+
}
|
|
5171
|
+
});
|
|
5172
|
+
const rows = what === "rows";
|
|
5173
|
+
const max = rows ? MAX_SHEET_ROWS : MAX_SHEET_COLUMNS;
|
|
5174
|
+
return new XlvError("XLV_E_LIMIT_EXCEEDED", `CSV line ${String(line)}: ${rows ? `the CSV has more than ${String(max)} rows` : `the row has more than ${String(max)} fields`}, more than a worksheet can hold.`, {
|
|
5175
|
+
location: { line },
|
|
5176
|
+
hint: rows ? "Split the CSV file into parts of at most 1048576 rows, or read only part of it with parserOptions.maxRows." : "Split the CSV file into parts of at most 16384 columns, or check the delimiter (parserOptions.delimiter).",
|
|
5177
|
+
details: {
|
|
5178
|
+
limit: rows ? "maxRows" : "maxColumns",
|
|
5179
|
+
value: max,
|
|
5180
|
+
line
|
|
5181
|
+
}
|
|
5182
|
+
});
|
|
5183
|
+
}
|
|
5184
|
+
/**
|
|
5185
|
+
* The rows of CSV text, split as fast-csv 4.3 does with the given parser options, one at a time:
|
|
5186
|
+
* the rows that `ignoreEmpty`, the header, `skipRows` and `maxRows` drop are left out. More than
|
|
5187
|
+
* 1 048 576 rows, or a row of more than 16 384 fields, throw `XLV_E_LIMIT_EXCEEDED` as soon as
|
|
5188
|
+
* they are reached, because a worksheet cannot hold them (K2); so do more than 5 000 000 fields
|
|
5189
|
+
* in the rows given (`DEFAULT_READ_LIMITS.maxCells`, K3).
|
|
5190
|
+
*/
|
|
5191
|
+
function* csvRows(text, o) {
|
|
4903
5192
|
let at = 0;
|
|
4904
5193
|
let line = 1;
|
|
4905
5194
|
const n = text.length;
|
|
@@ -4908,6 +5197,10 @@ function parseCsv(text, o) {
|
|
|
4908
5197
|
if (text[i] === "\r" && text[i + 1] === "\n") return i + 2;
|
|
4909
5198
|
return i < n ? i + 1 : i;
|
|
4910
5199
|
};
|
|
5200
|
+
const trim = (f) => o.trim ? f.trim() : o.ltrim ? f.trimStart() : o.rtrim ? f.trimEnd() : f;
|
|
5201
|
+
let kept = 0;
|
|
5202
|
+
let emitted = 0;
|
|
5203
|
+
let cells = 0;
|
|
4911
5204
|
for (let k = 0; k < o.skipLines && at < n; k++) {
|
|
4912
5205
|
while (!atLineEnd(at)) at++;
|
|
4913
5206
|
at = skipLineEnd(at);
|
|
@@ -4920,38 +5213,50 @@ function parseCsv(text, o) {
|
|
|
4920
5213
|
line++;
|
|
4921
5214
|
continue;
|
|
4922
5215
|
}
|
|
5216
|
+
const rowLine = line;
|
|
4923
5217
|
const fields = [];
|
|
5218
|
+
let extra = 0;
|
|
5219
|
+
let extraText = false;
|
|
5220
|
+
const push = (field) => {
|
|
5221
|
+
if (fields.length < MAX_SHEET_COLUMNS) fields.push(field);
|
|
5222
|
+
else {
|
|
5223
|
+
extra++;
|
|
5224
|
+
if (field.trim() !== "") extraText = true;
|
|
5225
|
+
}
|
|
5226
|
+
};
|
|
4924
5227
|
for (;;) {
|
|
4925
5228
|
let start = at;
|
|
4926
5229
|
while (start < n && (text[start] === " " || text[start] === " ") && text[start] !== o.delimiter) start++;
|
|
4927
5230
|
if (o.quote !== "" && text[start] === o.quote) {
|
|
4928
5231
|
let value = "";
|
|
4929
5232
|
let i = start + 1;
|
|
5233
|
+
let from = i;
|
|
4930
5234
|
const opened = line;
|
|
4931
5235
|
for (;;) {
|
|
4932
5236
|
if (i >= n) throw malformed(opened, `missing closing ${o.quote} for the field that starts here.`);
|
|
4933
5237
|
const ch = text[i];
|
|
4934
5238
|
if (ch === o.escape && text[i + 1] === o.quote) {
|
|
4935
|
-
value += o.quote;
|
|
5239
|
+
value += text.slice(from, i) + o.quote;
|
|
4936
5240
|
i += 2;
|
|
5241
|
+
from = i;
|
|
4937
5242
|
continue;
|
|
4938
5243
|
}
|
|
4939
5244
|
if (ch === o.quote) {
|
|
5245
|
+
value += text.slice(from, i);
|
|
4940
5246
|
i++;
|
|
4941
5247
|
break;
|
|
4942
5248
|
}
|
|
4943
5249
|
if (ch === "\n") line++;
|
|
4944
|
-
value += ch;
|
|
4945
5250
|
i++;
|
|
4946
5251
|
}
|
|
4947
5252
|
while (i < n && (text[i] === " " || text[i] === " ") && text[i] !== o.delimiter) i++;
|
|
4948
5253
|
if (i < n && text[i] !== o.delimiter && text[i] !== "\n" && text[i] !== "\r") throw malformed(line, `unexpected text after the closing ${o.quote}.`);
|
|
4949
|
-
|
|
5254
|
+
push(value);
|
|
4950
5255
|
at = i;
|
|
4951
5256
|
} else {
|
|
4952
5257
|
let i = at;
|
|
4953
5258
|
while (i < n && text[i] !== o.delimiter && text[i] !== "\n" && text[i] !== "\r") i++;
|
|
4954
|
-
|
|
5259
|
+
push(text.slice(at, i));
|
|
4955
5260
|
at = i;
|
|
4956
5261
|
}
|
|
4957
5262
|
if (at < n && text[at] === o.delimiter) {
|
|
@@ -4963,14 +5268,30 @@ function parseCsv(text, o) {
|
|
|
4963
5268
|
at = skipLineEnd(at);
|
|
4964
5269
|
line++;
|
|
4965
5270
|
const row = fields.length === 1 && fields[0].trim() === "" ? [""] : fields;
|
|
4966
|
-
|
|
5271
|
+
if (o.ignoreEmpty && !extraText && row.every((f) => f.trim() === "")) continue;
|
|
5272
|
+
kept++;
|
|
5273
|
+
if (o.skipHeader && kept === 1) continue;
|
|
5274
|
+
if (kept - (o.skipHeader ? 1 : 0) <= o.skipRows) continue;
|
|
5275
|
+
if (o.maxRows > 0 && emitted >= o.maxRows) continue;
|
|
5276
|
+
if (extra > 0) throw tooMany("columns", rowLine);
|
|
5277
|
+
if (++emitted > MAX_SHEET_ROWS) throw tooMany("rows", rowLine);
|
|
5278
|
+
cells += row.length;
|
|
5279
|
+
if (cells > MAX_CSV_CELLS) throw tooMany("cells", rowLine);
|
|
5280
|
+
yield {
|
|
5281
|
+
fields: row.map(trim),
|
|
5282
|
+
line: rowLine
|
|
5283
|
+
};
|
|
4967
5284
|
}
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
5285
|
+
}
|
|
5286
|
+
/** The delimiters and line-break characters in `text`: at least the number of fields less one. */
|
|
5287
|
+
function fieldEnds(text, delimiter) {
|
|
5288
|
+
const d = delimiter.charCodeAt(0);
|
|
5289
|
+
let n = 0;
|
|
5290
|
+
for (let i = 0; i < text.length; i++) {
|
|
5291
|
+
const c = text.charCodeAt(i);
|
|
5292
|
+
if (c === d || c === 10 || c === 13) n++;
|
|
5293
|
+
}
|
|
5294
|
+
return n;
|
|
4974
5295
|
}
|
|
4975
5296
|
/**
|
|
4976
5297
|
* exceljs's default conversion of a CSV field (lib/csv/csv.js L57–83), with two fixes: text of
|
|
@@ -4990,7 +5311,7 @@ function csvValue(datum, dateFormats) {
|
|
|
4990
5311
|
if (special !== void 0) return typeof special === "object" ? { ...special } : special;
|
|
4991
5312
|
return datum;
|
|
4992
5313
|
}
|
|
4993
|
-
function formatterOptions(raw) {
|
|
5314
|
+
function formatterOptions(raw, escapeFormulas) {
|
|
4994
5315
|
const o = raw ?? {};
|
|
4995
5316
|
const delimiter = typeof o["delimiter"] === "string" ? o["delimiter"] : ",";
|
|
4996
5317
|
const rowDelimiter = typeof o["rowDelimiter"] === "string" ? o["rowDelimiter"] : "\n";
|
|
@@ -5007,9 +5328,27 @@ function formatterOptions(raw) {
|
|
|
5007
5328
|
headers: Array.isArray(o["headers"]) && o["writeHeaders"] !== false ? o["headers"].map(String) : null,
|
|
5008
5329
|
includeEndRowDelimiter: o["includeEndRowDelimiter"] === true,
|
|
5009
5330
|
writeBOM: o["writeBOM"] === true,
|
|
5010
|
-
escapeChars: new RegExp(`[${escapeRe(delimiter)}${escapeRe(rowDelimiter)}|\\r|\\n]`)
|
|
5331
|
+
escapeChars: new RegExp(`[${escapeRe(delimiter)}${escapeRe(rowDelimiter)}|\\r|\\n${escapeFormulas ? escapeRe(COMMON_DELIMITERS) : ""}]`),
|
|
5332
|
+
strictQuoting: escapeFormulas
|
|
5011
5333
|
};
|
|
5012
5334
|
}
|
|
5335
|
+
/**
|
|
5336
|
+
* Whether an application that splits `text` at a comma, ";", tab or "|" would get a field that
|
|
5337
|
+
* starts like a formula: one of those characters followed by a formula sign, a control character
|
|
5338
|
+
* or a byte order mark, after any spaces or tabs and an opening double quote (K2).
|
|
5339
|
+
*/
|
|
5340
|
+
function splitStartsFormula(text) {
|
|
5341
|
+
for (let i = 0; i < text.length; i++) {
|
|
5342
|
+
if (!COMMON_DELIMITERS.includes(text[i])) continue;
|
|
5343
|
+
let j = i + 1;
|
|
5344
|
+
while (text[j] === " " || text[j] === " ") j++;
|
|
5345
|
+
if (text[j] === "\"") j++;
|
|
5346
|
+
while (text[j] === " ") j++;
|
|
5347
|
+
const c = text[j];
|
|
5348
|
+
if (c !== void 0 && (INJECTION_START.has(c) || c.charCodeAt(0) < 32)) return true;
|
|
5349
|
+
}
|
|
5350
|
+
return false;
|
|
5351
|
+
}
|
|
5013
5352
|
/** One field as fast-csv FieldFormatter.format writes it. */
|
|
5014
5353
|
function formatField(value, index, f, forceQuote) {
|
|
5015
5354
|
const text = `${value === null || value === void 0 ? "" : String(value)}`.replace(/\0/g, "");
|
|
@@ -5020,6 +5359,13 @@ function formatField(value, index, f, forceQuote) {
|
|
|
5020
5359
|
field: index + 1
|
|
5021
5360
|
}
|
|
5022
5361
|
});
|
|
5362
|
+
if (f.quote === "" && f.strictQuoting && splitStartsFormula(text)) throw new XlvError("XLV_E_INVALID_OPTION", `Field ${String(index + 1)} has a comma, ";", tab or "|" followed by a formula sign, which an application splitting on that character reads as a formula; it cannot be written with quote: false.`, {
|
|
5363
|
+
hint: "Leave formatterOptions.quote at its default (\") so such fields are quoted, or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
5364
|
+
details: {
|
|
5365
|
+
option: "quote",
|
|
5366
|
+
field: index + 1
|
|
5367
|
+
}
|
|
5368
|
+
});
|
|
5023
5369
|
if (f.quote !== "" && text.includes(f.quote)) return `${f.quote}${text.split(f.quote).join(f.escapedQuote)}${f.quote}`;
|
|
5024
5370
|
const q = f.quoteColumns;
|
|
5025
5371
|
const quoteThis = forceQuote || q === true || Array.isArray(q) && q[index] === true;
|
|
@@ -5039,7 +5385,7 @@ function defaultWriteMap(dateFormat, dateUTC) {
|
|
|
5039
5385
|
if (typeof value === "object") {
|
|
5040
5386
|
const v = value;
|
|
5041
5387
|
if (v["text"] || v["hyperlink"]) return v["hyperlink"] || v["text"] || "";
|
|
5042
|
-
if (v["
|
|
5388
|
+
if (v["sharedFormula"] || v["formula"] || "result" in v) {
|
|
5043
5389
|
const result = v["result"];
|
|
5044
5390
|
if (result === void 0 || result === null) return "";
|
|
5045
5391
|
if (result instanceof Date) return date(result);
|
|
@@ -5052,26 +5398,252 @@ function defaultWriteMap(dateFormat, dateUTC) {
|
|
|
5052
5398
|
return value;
|
|
5053
5399
|
};
|
|
5054
5400
|
}
|
|
5401
|
+
/** The output encoding for the `encoding` option (an unsupported one is refused when encoding). */
|
|
5402
|
+
function outputEncoding(encoding) {
|
|
5403
|
+
const e = (encoding ?? "").toLowerCase();
|
|
5404
|
+
if (e === "latin1" || e === "binary") return "latin1";
|
|
5405
|
+
if (e === "utf16le" || e === "utf-16le" || e === "ucs2" || e === "ucs-2") return "utf16le";
|
|
5406
|
+
return "utf8";
|
|
5407
|
+
}
|
|
5408
|
+
/** Leading UTF-8 sequences of U+FF1D, U+FF0B, U+FF0D, U+FF20, U+FE66, U+FE62, U+FE63, U+FE6B. */
|
|
5409
|
+
const UTF8_FORMULA_SIGNS = [
|
|
5410
|
+
[
|
|
5411
|
+
239,
|
|
5412
|
+
188,
|
|
5413
|
+
157
|
|
5414
|
+
],
|
|
5415
|
+
[
|
|
5416
|
+
239,
|
|
5417
|
+
188,
|
|
5418
|
+
139
|
|
5419
|
+
],
|
|
5420
|
+
[
|
|
5421
|
+
239,
|
|
5422
|
+
188,
|
|
5423
|
+
141
|
|
5424
|
+
],
|
|
5425
|
+
[
|
|
5426
|
+
239,
|
|
5427
|
+
188,
|
|
5428
|
+
160
|
|
5429
|
+
],
|
|
5430
|
+
[
|
|
5431
|
+
239,
|
|
5432
|
+
185,
|
|
5433
|
+
166
|
|
5434
|
+
],
|
|
5435
|
+
[
|
|
5436
|
+
239,
|
|
5437
|
+
185,
|
|
5438
|
+
162
|
|
5439
|
+
],
|
|
5440
|
+
[
|
|
5441
|
+
239,
|
|
5442
|
+
185,
|
|
5443
|
+
163
|
|
5444
|
+
],
|
|
5445
|
+
[
|
|
5446
|
+
239,
|
|
5447
|
+
185,
|
|
5448
|
+
171
|
|
5449
|
+
]
|
|
5450
|
+
];
|
|
5055
5451
|
/**
|
|
5056
|
-
*
|
|
5452
|
+
* Whether the first bytes of a field, as latin1 or UTF-16LE writes it, are a byte order mark
|
|
5453
|
+
* (EF BB BF, FF FE, FE FF), which makes a reader decode the rest of the file in that encoding
|
|
5454
|
+
* and drop the mark, or the UTF-8 form of a full-width or small formula sign, which a reader that
|
|
5455
|
+
* guesses UTF-8 shows as one (K2). `prefix` holds bytes written before the field (latin1 writes
|
|
5456
|
+
* the `writeBOM` mark U+FEFF as the single byte FF). UTF-16LE bytes are taken without their NUL
|
|
5457
|
+
* bytes, as an 8-bit reader that drops them reads them (K3); a sequence that holds no NUL byte is
|
|
5458
|
+
* the same either way.
|
|
5459
|
+
*/
|
|
5460
|
+
function encodedStartIsUnsafe(text, encoding, prefix) {
|
|
5461
|
+
if (encoding === "utf8") return false;
|
|
5462
|
+
const bytes = [...prefix];
|
|
5463
|
+
for (let i = 0; bytes.length < 3 && i < text.length; i++) {
|
|
5464
|
+
const c = text.charCodeAt(i);
|
|
5465
|
+
if (encoding === "latin1") bytes.push(c & 255);
|
|
5466
|
+
else for (const b of [c & 255, c >> 8]) if (b !== 0) bytes.push(b);
|
|
5467
|
+
}
|
|
5468
|
+
const starts = (lead) => lead.every((b, i) => bytes[i] === b);
|
|
5469
|
+
return starts([
|
|
5470
|
+
239,
|
|
5471
|
+
187,
|
|
5472
|
+
191
|
|
5473
|
+
]) || starts([255, 254]) || starts([254, 255]) || UTF8_FORMULA_SIGNS.some(starts);
|
|
5474
|
+
}
|
|
5475
|
+
/** Bytes that end a field or a row for a reader of the 8-bit text, whatever its delimiter. */
|
|
5476
|
+
const BYTE_BREAKS = /* @__PURE__ */ new Set([
|
|
5477
|
+
44,
|
|
5478
|
+
59,
|
|
5479
|
+
9,
|
|
5480
|
+
124,
|
|
5481
|
+
13,
|
|
5482
|
+
10
|
|
5483
|
+
]);
|
|
5484
|
+
/** Whether the 8-bit text `view` has a formula start at `i`: "=", or spaces or tabs, a quote and "=". */
|
|
5485
|
+
function viewFormulaAt(view, i) {
|
|
5486
|
+
if (view[i] === "=") return true;
|
|
5487
|
+
let j = i;
|
|
5488
|
+
while (view[j] === " " || view[j] === " ") j++;
|
|
5489
|
+
return view[j] === "\"" && view[j + 1] === "=";
|
|
5490
|
+
}
|
|
5491
|
+
/**
|
|
5492
|
+
* Whether the UTF-16LE bytes of `text` without NULs start a formula (see {@link viewFormulaAt})
|
|
5493
|
+
* because of a character above U+00FF, such as U+3D00 (00 3D) or U+4E3D (3D 4E). Starts made only
|
|
5494
|
+
* of characters up to U+00FF are the same text in both readings and are checked as text.
|
|
5495
|
+
*/
|
|
5496
|
+
function utf16StartsFormula(text) {
|
|
5497
|
+
let view = "";
|
|
5498
|
+
let foreign = false;
|
|
5499
|
+
let lead = 0;
|
|
5500
|
+
let blank = true;
|
|
5501
|
+
for (let i = 0; i < text.length && view.length - lead < 2; i++) {
|
|
5502
|
+
const c = text.charCodeAt(i);
|
|
5503
|
+
if (c > 255) foreign = true;
|
|
5504
|
+
for (const b of c > 255 ? [c & 255, c >> 8] : [c]) {
|
|
5505
|
+
if (b === 0) continue;
|
|
5506
|
+
view += String.fromCharCode(b);
|
|
5507
|
+
if (blank && (b === 32 || b === 9)) lead++;
|
|
5508
|
+
else blank = false;
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
return foreign && viewFormulaAt(view, 0);
|
|
5512
|
+
}
|
|
5513
|
+
/** The UTF-16LE bytes of a written field without NULs, and whether characters above U+00FF give a break or a quote among them. */
|
|
5514
|
+
function utf16View(written, delimiter) {
|
|
5515
|
+
let view = "";
|
|
5516
|
+
let foreignBreak = false;
|
|
5517
|
+
let foreignQuote = false;
|
|
5518
|
+
for (let i = 0; i < written.length; i++) {
|
|
5519
|
+
const c = written.charCodeAt(i);
|
|
5520
|
+
for (const b of c > 255 ? [c & 255, c >> 8] : [c]) {
|
|
5521
|
+
if (b === 0) continue;
|
|
5522
|
+
if (c > 255) {
|
|
5523
|
+
if (b === 34) foreignQuote = true;
|
|
5524
|
+
else if (BYTE_BREAKS.has(b) || delimiter.includes(String.fromCharCode(b))) foreignBreak = true;
|
|
5525
|
+
}
|
|
5526
|
+
view += String.fromCharCode(b);
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
return {
|
|
5530
|
+
view,
|
|
5531
|
+
foreignBreak,
|
|
5532
|
+
foreignQuote
|
|
5533
|
+
};
|
|
5534
|
+
}
|
|
5535
|
+
/** Whether the 8-bit text `view` has a delimiter (`delimiter` or a common one) or a line break followed by a formula start. */
|
|
5536
|
+
function viewSplitStartsFormula(view, delimiter) {
|
|
5537
|
+
for (let i = 0; i < view.length; i++) {
|
|
5538
|
+
if (BYTE_BREAKS.has(view.charCodeAt(i)) && viewFormulaAt(view, i + 1)) return true;
|
|
5539
|
+
if (view.startsWith(delimiter, i) && viewFormulaAt(view, i + delimiter.length)) return true;
|
|
5540
|
+
}
|
|
5541
|
+
return false;
|
|
5542
|
+
}
|
|
5543
|
+
/** Column letters of a 1-based column number. */
|
|
5544
|
+
function columnName(n) {
|
|
5545
|
+
let s = "";
|
|
5546
|
+
for (let c = n; c > 0; c = Math.floor((c - 1) / 26)) s = String.fromCharCode(65 + (c - 1) % 26) + s;
|
|
5547
|
+
return s;
|
|
5548
|
+
}
|
|
5549
|
+
/**
|
|
5550
|
+
* The CSV text of a worksheet, as exceljs `csv.write` gives it, for output in `encoding`.
|
|
5057
5551
|
*
|
|
5058
5552
|
* @internal
|
|
5059
5553
|
*/
|
|
5060
|
-
function worksheetCsv(sheet, options) {
|
|
5061
|
-
const f = formatterOptions(options.formatterOptions);
|
|
5554
|
+
function worksheetCsv(sheet, options, encoding = "utf8") {
|
|
5062
5555
|
const injection = options.formulaInjection ?? "escape";
|
|
5063
5556
|
if (injection !== "escape" && injection !== "off") throw invalid("formulaInjection", "The option \"formulaInjection\" must be \"escape\" or \"off\".", "Use \"escape\" (default) or \"off\".");
|
|
5557
|
+
const f = formatterOptions(options.formatterOptions, injection === "escape");
|
|
5558
|
+
const latin1 = encoding === "latin1";
|
|
5559
|
+
const sheetName = sheet?.name ?? "";
|
|
5560
|
+
if (latin1) {
|
|
5561
|
+
for (const [name, value] of [
|
|
5562
|
+
["delimiter", f.delimiter],
|
|
5563
|
+
["rowDelimiter", f.rowDelimiter],
|
|
5564
|
+
["quote", f.quote],
|
|
5565
|
+
["escape", f.escapedQuote]
|
|
5566
|
+
]) 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.");
|
|
5567
|
+
}
|
|
5568
|
+
if (injection === "escape") {
|
|
5569
|
+
const off = "or pass formulaInjection: \"off\" for a trusted consumer.";
|
|
5570
|
+
if (f.delimiter === "") throw invalid("delimiter", "The delimiter is empty, so the fields of a row run together and the text of one field could complete a formula or a byte order mark begun by the one before.", `Use "," (default), ";", "|" or "\\t", ${off}`);
|
|
5571
|
+
if (encoding !== "utf8") {
|
|
5572
|
+
for (const [name, value] of [["delimiter", f.delimiter], ["rowDelimiter", f.rowDelimiter]]) if (/[^\u0000-\u007f]/.test(value)) throw invalid(name, `The option "${name}" (${JSON.stringify(value)}) has characters other than ASCII, whose ${encoding} bytes could join the bytes of the fields around them into a byte order mark or a formula sign.`, `Use ASCII characters with encoding "${encoding}" (for example "," or ";"), write UTF-8 (the default encoding), ${off}`);
|
|
5573
|
+
}
|
|
5574
|
+
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}`);
|
|
5575
|
+
if (![
|
|
5576
|
+
"\n",
|
|
5577
|
+
"\r\n",
|
|
5578
|
+
"\r"
|
|
5579
|
+
].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}`);
|
|
5580
|
+
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}`);
|
|
5581
|
+
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.");
|
|
5582
|
+
}
|
|
5583
|
+
/** The text latin1 will write: characters above U+00FF as "?". */
|
|
5584
|
+
const narrow = (out) => latin1 && typeof out === "string" ? out.replace(/[^\u0000-\u00ff]/g, "?") : out;
|
|
5585
|
+
const utf16 = encoding === "utf16le" && injection === "escape";
|
|
5586
|
+
const marked = injection === "escape" && (utf16 || latin1 && f.writeBOM);
|
|
5587
|
+
/** A quote may be taken differently by a reader of the 8-bit text from here on (K3). */
|
|
5588
|
+
let quoteShifted = false;
|
|
5589
|
+
const unsafeField = (index, row, reason, hint) => {
|
|
5590
|
+
const cell = `${columnName(index + 1)}${String(row)}`;
|
|
5591
|
+
return new XlvError("XLV_E_INVALID_OPTION", `The text of ${row === 0 ? `header ${String(index + 1)}` : `cell ${cell}`} cannot be written safely as ${encoding}: ${reason}`, {
|
|
5592
|
+
location: row === 0 ? { sheet: sheetName } : {
|
|
5593
|
+
sheet: sheetName,
|
|
5594
|
+
cell
|
|
5595
|
+
},
|
|
5596
|
+
hint,
|
|
5597
|
+
details: {
|
|
5598
|
+
option: "encoding",
|
|
5599
|
+
field: index + 1,
|
|
5600
|
+
row
|
|
5601
|
+
}
|
|
5602
|
+
});
|
|
5603
|
+
};
|
|
5604
|
+
/**
|
|
5605
|
+
* A formatted field, refused if a standard reader would take its leading quote as quoting, or
|
|
5606
|
+
* if a reader of the output as 8-bit text that drops NUL bytes (LibreOffice) would split it
|
|
5607
|
+
* before a formula start: in utf16le output, when characters above U+00FF give a delimiter,
|
|
5608
|
+
* line break or quote there, when an earlier quote of that kind may shift the quoting, or for
|
|
5609
|
+
* the first field after a mark that reader keeps (K3). `row` is the row number, 0 for the header
|
|
5610
|
+
* line; `atStart`: the field is the first one of the file.
|
|
5611
|
+
*/
|
|
5612
|
+
const field = (out, index, force, row, atStart) => {
|
|
5613
|
+
const text = formatField(out, index, f, force);
|
|
5614
|
+
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 '"'.`, {
|
|
5615
|
+
hint: "Leave formatterOptions.quote at its default (\"), or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
5616
|
+
details: {
|
|
5617
|
+
option: "quote",
|
|
5618
|
+
field: index + 1
|
|
5619
|
+
}
|
|
5620
|
+
});
|
|
5621
|
+
const first = marked && atStart;
|
|
5622
|
+
if (first || quoteShifted || utf16 && /[^\u0000-\u00ff]/.test(text)) {
|
|
5623
|
+
const { view, foreignBreak, foreignQuote } = utf16 ? utf16View(text, f.delimiter) : {
|
|
5624
|
+
view: text,
|
|
5625
|
+
foreignBreak: false,
|
|
5626
|
+
foreignQuote: false
|
|
5627
|
+
};
|
|
5628
|
+
if ((first || foreignBreak || foreignQuote || quoteShifted) && viewSplitStartsFormula(view, f.delimiter)) throw first ? unsafeField(index, row, `the file starts with ${utf16 ? "the UTF-16LE byte order mark (FF FE)" : "the byte FF (writeBOM)"}, which LibreOffice keeps when it reads the file as 8-bit text, so the opening quote of this first field is not taken as quoting and its delimiter or line break followed by "=" starts a formula.`, utf16 ? "Write UTF-8 (the default encoding), move this text out of the first cell, or pass formulaInjection: \"off\" for a trusted consumer." : "Leave formatterOptions.writeBOM off with latin1, write UTF-8, or pass formulaInjection: \"off\" for a trusted consumer.") : utf16 ? unsafeField(index, row, "read as 8-bit text without its NUL bytes, as LibreOffice reads UTF-16LE CSV files, its bytes hold a delimiter or a line break followed by \"=\", which starts a formula there.", "Write UTF-8 (the default encoding), or pass formulaInjection: \"off\" for a trusted consumer.") : unsafeField(index, row, "the first field, after the byte FF of writeBOM, is split by LibreOffice, which may then take the quotes after it differently, and this field holds a delimiter or a line break followed by \"=\".", "Leave formatterOptions.writeBOM off with latin1, write UTF-8, or pass formulaInjection: \"off\" for a trusted consumer.");
|
|
5629
|
+
if (foreignQuote || first && ([...view].some((c) => BYTE_BREAKS.has(c.charCodeAt(0))) || view.includes(f.delimiter))) quoteShifted = true;
|
|
5630
|
+
}
|
|
5631
|
+
return text;
|
|
5632
|
+
};
|
|
5064
5633
|
const map = options.map ?? defaultWriteMap(options.dateFormat, options.dateUTC === true);
|
|
5065
5634
|
const includeEmptyRows = options.includeEmptyRows === void 0 || options.includeEmptyRows;
|
|
5066
5635
|
const lines = [];
|
|
5067
|
-
const escape = (
|
|
5636
|
+
const escape = (raw, atStart) => {
|
|
5637
|
+
const out = narrow(raw === null || raw === void 0 || typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint" || typeof raw === "string" ? raw : String(raw));
|
|
5068
5638
|
if (injection !== "escape" || typeof out !== "string") return [out, false];
|
|
5069
|
-
const
|
|
5070
|
-
|
|
5639
|
+
const text = out.replace(/\0/g, "");
|
|
5640
|
+
const first = text[0];
|
|
5641
|
+
if (first === void 0) return [out, false];
|
|
5642
|
+
return INJECTION_START.has(first) || first.charCodeAt(0) < 32 || encodedStartIsUnsafe(text, encoding, atStart && f.writeBOM && latin1 ? [255] : []) || utf16 && utf16StartsFormula(text) ? [`'${out}`, f.quote !== ""] : [out, false];
|
|
5071
5643
|
};
|
|
5072
5644
|
if (f.headers) lines.push(f.headers.map((h, i) => {
|
|
5073
|
-
const [out, force] = escape(h);
|
|
5074
|
-
return
|
|
5645
|
+
const [out, force] = escape(h, i === 0);
|
|
5646
|
+
return field(out, i, force, 0, i === 0);
|
|
5075
5647
|
}).join(f.delimiter));
|
|
5076
5648
|
if (sheet) {
|
|
5077
5649
|
let lastRow = 0;
|
|
@@ -5085,8 +5657,9 @@ function worksheetCsv(sheet, options) {
|
|
|
5085
5657
|
fields.push("");
|
|
5086
5658
|
continue;
|
|
5087
5659
|
}
|
|
5088
|
-
const
|
|
5089
|
-
|
|
5660
|
+
const atStart = lines.length === 0 && i === 0;
|
|
5661
|
+
const [out, force] = escape(map(values[i], i), atStart);
|
|
5662
|
+
fields.push(field(out, i, force, rowNumber, atStart));
|
|
5090
5663
|
}
|
|
5091
5664
|
lines.push(fields.join(f.delimiter));
|
|
5092
5665
|
lastRow = rowNumber;
|
|
@@ -5094,7 +5667,7 @@ function worksheetCsv(sheet, options) {
|
|
|
5094
5667
|
}
|
|
5095
5668
|
let text = lines.join(f.rowDelimiter);
|
|
5096
5669
|
if (f.includeEndRowDelimiter) text += f.rowDelimiter;
|
|
5097
|
-
return (f.writeBOM ? "" : "") + text;
|
|
5670
|
+
return (f.writeBOM || utf16 ? "" : "") + text;
|
|
5098
5671
|
}
|
|
5099
5672
|
function encode(text, encoding) {
|
|
5100
5673
|
const e = (encoding ?? "utf8").toLowerCase();
|
|
@@ -5128,11 +5701,28 @@ function decoderFor(encoding) {
|
|
|
5128
5701
|
async function readText(stream, encoding) {
|
|
5129
5702
|
const decoder = decoderFor(encoding);
|
|
5130
5703
|
let text = "";
|
|
5704
|
+
let size = 0;
|
|
5705
|
+
const count = (n) => {
|
|
5706
|
+
size += n;
|
|
5707
|
+
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.`, {
|
|
5708
|
+
hint: "Split the CSV file, or read it with the core fromCSV in parts.",
|
|
5709
|
+
details: {
|
|
5710
|
+
limit: "maxInputBytes",
|
|
5711
|
+
value: DEFAULT_READ_LIMITS.maxInputBytes
|
|
5712
|
+
}
|
|
5713
|
+
});
|
|
5714
|
+
};
|
|
5131
5715
|
const add = (chunk) => {
|
|
5132
|
-
if (typeof chunk === "string")
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
else
|
|
5716
|
+
if (typeof chunk === "string") {
|
|
5717
|
+
count(chunk.length);
|
|
5718
|
+
text += chunk;
|
|
5719
|
+
} else if (chunk instanceof Uint8Array) {
|
|
5720
|
+
count(chunk.length);
|
|
5721
|
+
text += decoder.decode(chunk, { stream: true });
|
|
5722
|
+
} else if (chunk instanceof ArrayBuffer) {
|
|
5723
|
+
count(chunk.byteLength);
|
|
5724
|
+
text += decoder.decode(new Uint8Array(chunk), { stream: true });
|
|
5725
|
+
} 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
5726
|
};
|
|
5137
5727
|
const s = stream;
|
|
5138
5728
|
if (typeof stream === "string") add(stream);
|
|
@@ -5219,10 +5809,16 @@ var CsvIO = class {
|
|
|
5219
5809
|
async read(stream, options = {}) {
|
|
5220
5810
|
const worksheet = this.workbook.addWorksheet(options.sheetName);
|
|
5221
5811
|
const parser = parserOptions(options.parserOptions);
|
|
5222
|
-
const
|
|
5812
|
+
const text = await readText(stream, parser.encoding);
|
|
5223
5813
|
const formats = options.dateFormats ?? DEFAULT_DATE_FORMATS;
|
|
5224
5814
|
const map = options.map ?? ((datum) => csvValue(datum, formats));
|
|
5225
|
-
|
|
5815
|
+
if (text.length >= MAX_CSV_CELLS && fieldEnds(text, parser.delimiter) >= MAX_CSV_CELLS) try {
|
|
5816
|
+
const rows = csvRows(text, parser);
|
|
5817
|
+
while (rows.next().done !== true);
|
|
5818
|
+
} catch (error) {
|
|
5819
|
+
if (error instanceof XlvError && error.code === "XLV_E_LIMIT_EXCEEDED" && error.details?.limit === "maxCells") throw error;
|
|
5820
|
+
}
|
|
5821
|
+
for (const row of csvRows(text, parser)) worksheet.addRow(row.fields.map((datum, i) => map(datum, i)));
|
|
5226
5822
|
return worksheet;
|
|
5227
5823
|
}
|
|
5228
5824
|
/** Reads a CSV file (Node.js, Bun, Deno) into a new worksheet. */
|
|
@@ -5250,7 +5846,7 @@ var CsvIO = class {
|
|
|
5250
5846
|
}
|
|
5251
5847
|
/** Writes a worksheet to a CSV file (Node.js, Bun, Deno), replacing it. */
|
|
5252
5848
|
async writeFile(path, options = {}) {
|
|
5253
|
-
const bytes = encode(this.text(options), options.encoding);
|
|
5849
|
+
const bytes = encode(this.text(options, outputEncoding(options.encoding)), options.encoding);
|
|
5254
5850
|
const fs = await nodeFs("writeFile");
|
|
5255
5851
|
try {
|
|
5256
5852
|
await fs.writeFile(path, bytes);
|
|
@@ -5274,7 +5870,7 @@ var CsvIO = class {
|
|
|
5274
5870
|
createInputStream() {
|
|
5275
5871
|
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
5872
|
}
|
|
5277
|
-
text(options) {
|
|
5873
|
+
text(options, encoding = "utf8") {
|
|
5278
5874
|
const key = options.sheetName || options.sheetId;
|
|
5279
5875
|
const sheet = this.workbook.getWorksheet(key);
|
|
5280
5876
|
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 +5878,7 @@ var CsvIO = class {
|
|
|
5282
5878
|
hint: `Available worksheets: ${this.workbook.worksheets.map((w) => `"${w.name}"`).join(", ") || "none"}.`,
|
|
5283
5879
|
details: { available: this.workbook.worksheets.map((w) => w.name) }
|
|
5284
5880
|
});
|
|
5285
|
-
return worksheetCsv(sheet, options);
|
|
5881
|
+
return worksheetCsv(sheet, options, encoding);
|
|
5286
5882
|
}
|
|
5287
5883
|
};
|
|
5288
5884
|
// src/defined-names.ts
|
|
@@ -5335,6 +5931,33 @@ function decodeEx(value) {
|
|
|
5335
5931
|
...address
|
|
5336
5932
|
} : address;
|
|
5337
5933
|
}
|
|
5934
|
+
/**
|
|
5935
|
+
* Refuses a range that reaches past the sheet (row 1048576, column XFD) before its cells are
|
|
5936
|
+
* created one by one. exceljs bounds only column letters, so a range such as `$A$1:$A$99999999999`
|
|
5937
|
+
* makes it loop for hours; xlvane throws instead (security review J, C1).
|
|
5938
|
+
*/
|
|
5939
|
+
function checkGrid(location) {
|
|
5940
|
+
const { bottom = 0, right = 0 } = location;
|
|
5941
|
+
if (bottom <= 1048576 && right <= 16384) return;
|
|
5942
|
+
const where = `${location.sheetName === void 0 ? "" : `${location.sheetName}!`}rows ${String(location.top)}-${String(bottom)}, columns ${String(location.left)}-${String(right)}`;
|
|
5943
|
+
throw new XlvError("XLV_E_INVALID_CELL_REF", `The range ${where.slice(0, 80)} reaches past the sheet (row 1048576, column XFD).`, {
|
|
5944
|
+
hint: "Use a range inside A1:XFD1048576, for example \"Data!$A$1:$B$10\".",
|
|
5945
|
+
details: { ref: where.slice(0, 80) }
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
/**
|
|
5949
|
+
* @internal The number of cells the `model` setter of {@link DefinedNames} creates for one range
|
|
5950
|
+
* read from a file, or `null` for a range that reaches past the sheet (it is dropped, as invalid
|
|
5951
|
+
* ranges are; security review J, C1).
|
|
5952
|
+
*/
|
|
5953
|
+
function rangeCellCount(rangeStr) {
|
|
5954
|
+
if (!RANGE.test(rangeStr.split("!").pop() ?? "")) return 0;
|
|
5955
|
+
const location = decodeEx(rangeStr);
|
|
5956
|
+
if (!location.top) return location.row !== void 0 && location.col !== void 0 ? 1 : 0;
|
|
5957
|
+
const { top, left = 0, bottom = 0, right = 0 } = location;
|
|
5958
|
+
if (bottom > 1048576 || right > 16384) return null;
|
|
5959
|
+
return Math.max(0, bottom - top + 1) * Math.max(0, right - left + 1);
|
|
5960
|
+
}
|
|
5338
5961
|
/** The sheet prefix exceljs writes (lib/doc/range.js L153): quoted unless letters and digits only. */
|
|
5339
5962
|
function sheetPrefix(sheetName) {
|
|
5340
5963
|
if (!sheetName) return "";
|
|
@@ -5368,8 +5991,10 @@ var CellMatrix = class {
|
|
|
5368
5991
|
return this.sheets.get(sheetName)?.[rowNumber]?.[colNumber];
|
|
5369
5992
|
}
|
|
5370
5993
|
addCellEx(address) {
|
|
5371
|
-
if (address.top)
|
|
5372
|
-
|
|
5994
|
+
if (address.top) {
|
|
5995
|
+
checkGrid(address);
|
|
5996
|
+
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);
|
|
5997
|
+
} else this.findCellEx(address, true);
|
|
5373
5998
|
}
|
|
5374
5999
|
getCellEx(address) {
|
|
5375
6000
|
return this.findCellEx(address, true);
|
|
@@ -5466,6 +6091,7 @@ var DefinedNames = class {
|
|
|
5466
6091
|
this.addEx(decodeEx(locStr), name);
|
|
5467
6092
|
}
|
|
5468
6093
|
addEx(location, name) {
|
|
6094
|
+
if (location.top) checkGrid(location);
|
|
5469
6095
|
const matrix = this.getMatrix(name);
|
|
5470
6096
|
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
6097
|
else matrix.addCellEx(location);
|
|
@@ -5736,12 +6362,7 @@ var XlsxIO = class {
|
|
|
5736
6362
|
}
|
|
5737
6363
|
/** Reads XLSX data from a readable stream (Node.js stream or any async iterable of bytes). */
|
|
5738
6364
|
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
|
-
}
|
|
6365
|
+
const { chunks, size } = await collectInput(stream, "xlsx.read()");
|
|
5745
6366
|
const all = new Uint8Array(size);
|
|
5746
6367
|
let at = 0;
|
|
5747
6368
|
for (const c of chunks) {
|
|
@@ -5883,12 +6504,71 @@ var Workbook = class {
|
|
|
5883
6504
|
get _date1904() {
|
|
5884
6505
|
return this.properties.date1904 === true;
|
|
5885
6506
|
}
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
6507
|
+
_byName = /* @__PURE__ */ new Map();
|
|
6508
|
+
_firstFree = 1;
|
|
6509
|
+
_maxOrderNo = 0;
|
|
6510
|
+
_maxOrderNoStale = false;
|
|
6511
|
+
/** @internal Whether a sheet other than `exclude` has this name, ignoring case (the duplicate check of Worksheet.name). */
|
|
6512
|
+
_nameTaken(name, exclude) {
|
|
6513
|
+
const sheet = this._byName.get(name.toLowerCase());
|
|
6514
|
+
return sheet !== void 0 && sheet !== exclude;
|
|
6515
|
+
}
|
|
6516
|
+
/** @internal Keeps the name index in step with a renamed sheet. */
|
|
6517
|
+
_renamed(sheet, oldName) {
|
|
6518
|
+
if (this._sheets[sheet.id] !== sheet) return;
|
|
6519
|
+
const old = oldName.toLowerCase();
|
|
6520
|
+
if (this._byName.get(old) === sheet) this._byName.delete(old);
|
|
6521
|
+
this._byName.set(sheet.name.toLowerCase(), sheet);
|
|
6522
|
+
}
|
|
6523
|
+
/** @internal Keeps the largest orderNo in step with a changed one. */
|
|
6524
|
+
_orderNoChanged(sheet, oldOrderNo) {
|
|
6525
|
+
if (this._sheets[sheet.id] !== sheet || this._maxOrderNoStale) return;
|
|
6526
|
+
const value = sheet.orderNo;
|
|
6527
|
+
const plain = (v) => v === void 0 || typeof v === "number" && !Number.isNaN(v);
|
|
6528
|
+
if (!plain(value) || !plain(oldOrderNo)) this._maxOrderNoStale = true;
|
|
6529
|
+
else if (value !== void 0 && value > this._maxOrderNo) this._maxOrderNo = value;
|
|
6530
|
+
else if (oldOrderNo !== void 0 && oldOrderNo >= this._maxOrderNo && (value === void 0 || value < oldOrderNo)) this._maxOrderNoStale = true;
|
|
6531
|
+
}
|
|
6532
|
+
/** @internal Puts a worksheet into `_sheets` at its id and indexes it. */
|
|
6533
|
+
_putSheet(sheet) {
|
|
6534
|
+
const previous = this._sheets[sheet.id];
|
|
6535
|
+
if (previous && previous !== sheet) this._dropSheet(previous);
|
|
6536
|
+
this._sheets[sheet.id] = sheet;
|
|
6537
|
+
this._byName.set(sheet.name.toLowerCase(), sheet);
|
|
6538
|
+
this._orderNoChanged(sheet, void 0);
|
|
6539
|
+
}
|
|
6540
|
+
/** Takes a worksheet out of `_sheets` and its indexes. */
|
|
6541
|
+
_dropSheet(sheet) {
|
|
6542
|
+
this._sheets[sheet.id] = void 0;
|
|
6543
|
+
const key = sheet.name.toLowerCase();
|
|
6544
|
+
if (this._byName.get(key) === sheet) this._byName.delete(key);
|
|
6545
|
+
if (sheet.id < this._firstFree) this._firstFree = Math.max(1, sheet.id);
|
|
6546
|
+
const order = sheet.orderNo;
|
|
6547
|
+
if (order !== void 0 && !(typeof order === "number" && order < this._maxOrderNo)) this._maxOrderNoStale = true;
|
|
6548
|
+
}
|
|
6549
|
+
/** Empties `_sheets` and its indexes. */
|
|
6550
|
+
_clearSheets() {
|
|
6551
|
+
this._sheets = [];
|
|
6552
|
+
this._byName = /* @__PURE__ */ new Map();
|
|
6553
|
+
this._firstFree = 1;
|
|
6554
|
+
this._maxOrderNo = 0;
|
|
6555
|
+
this._maxOrderNoStale = false;
|
|
6556
|
+
}
|
|
6557
|
+
/** The largest orderNo of the sheets, as exceljs computes it (lib/doc/workbook.js L81-85). */
|
|
6558
|
+
_lastOrderNo() {
|
|
6559
|
+
if (this._maxOrderNoStale) {
|
|
6560
|
+
this._maxOrderNo = this._sheets.reduce((acc, ws) => ws?.orderNo !== void 0 && ws.orderNo > acc ? ws.orderNo : acc, 0);
|
|
6561
|
+
this._maxOrderNoStale = false;
|
|
6562
|
+
}
|
|
6563
|
+
return this._maxOrderNo;
|
|
6564
|
+
}
|
|
6565
|
+
/** The id the next worksheet gets (lib/doc/workbook.js `nextId`): the lowest free id. */
|
|
5889
6566
|
get nextId() {
|
|
5890
|
-
|
|
5891
|
-
|
|
6567
|
+
const sheets = this._sheets;
|
|
6568
|
+
let i = Math.max(1, this._firstFree);
|
|
6569
|
+
while (i < sheets.length && sheets[i]) i++;
|
|
6570
|
+
this._firstFree = i;
|
|
6571
|
+
return i < sheets.length ? i : sheets.length || 1;
|
|
5892
6572
|
}
|
|
5893
6573
|
/** Adds a worksheet. Names follow exceljs rules (31 characters at most; longer names are cut). */
|
|
5894
6574
|
addWorksheet(name, options) {
|
|
@@ -5897,13 +6577,34 @@ var Workbook = class {
|
|
|
5897
6577
|
/** @internal Adds a worksheet with a given id (a loaded file's sheetId, a model's id; D-F03). */
|
|
5898
6578
|
_addWorksheetWithId(id, name, options) {
|
|
5899
6579
|
const sheet = new Worksheet(this, id, name, options);
|
|
5900
|
-
|
|
5901
|
-
this.
|
|
6580
|
+
const last = this._lastOrderNo();
|
|
6581
|
+
this._putSheet(sheet);
|
|
6582
|
+
sheet.orderNo = last + 1;
|
|
5902
6583
|
return sheet;
|
|
5903
6584
|
}
|
|
6585
|
+
/**
|
|
6586
|
+
* @internal Adds a worksheet read from a file (`xlsx.load`, `stream.xlsx.WorkbookReader`). A
|
|
6587
|
+
* name exceljs refuses gives an XlvError with exceljs's message, the sheet and a hint, instead
|
|
6588
|
+
* of the plain Error of `addWorksheet`; an empty name is refused too.
|
|
6589
|
+
*/
|
|
6590
|
+
_addReadWorksheet(id, name, options) {
|
|
6591
|
+
const checked = checkSheetName(name, (n) => this._nameTaken(n));
|
|
6592
|
+
if (checked.message !== void 0) {
|
|
6593
|
+
const existing = checked.duplicate ? this._byName.get(checked.name.toLowerCase())?.name : void 0;
|
|
6594
|
+
throw new XlvError(checked.duplicate ? "XLV_E_DUPLICATE_SHEET_NAME" : "XLV_E_INVALID_SHEET_NAME", checked.message, {
|
|
6595
|
+
location: { sheet: name },
|
|
6596
|
+
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.",
|
|
6597
|
+
details: existing === void 0 ? { name } : {
|
|
6598
|
+
name,
|
|
6599
|
+
existing
|
|
6600
|
+
}
|
|
6601
|
+
});
|
|
6602
|
+
}
|
|
6603
|
+
return this._addWorksheetWithId(id, name, options);
|
|
6604
|
+
}
|
|
5904
6605
|
removeWorksheet(id) {
|
|
5905
6606
|
const sheet = this.getWorksheet(id);
|
|
5906
|
-
if (sheet) this.
|
|
6607
|
+
if (sheet) this._dropSheet(sheet);
|
|
5907
6608
|
}
|
|
5908
6609
|
/** Removes a worksheet by id or name, as `removeWorksheet` (lib/doc/workbook.js L77). */
|
|
5909
6610
|
removeWorksheetEx(worksheet) {
|
|
@@ -5979,7 +6680,11 @@ var Workbook = class {
|
|
|
5979
6680
|
getWorksheet(id) {
|
|
5980
6681
|
if (id === void 0) return this.worksheets[0];
|
|
5981
6682
|
if (typeof id === "number") return this._sheets[id];
|
|
5982
|
-
if (typeof id === "string")
|
|
6683
|
+
if (typeof id === "string") {
|
|
6684
|
+
const sheet = this._byName.get(id.toLowerCase());
|
|
6685
|
+
if (sheet?.name === id) return sheet;
|
|
6686
|
+
return id === "" ? this.worksheets.find((w) => w.name === id) : void 0;
|
|
6687
|
+
}
|
|
5983
6688
|
}
|
|
5984
6689
|
/** Worksheets in workbook order. */
|
|
5985
6690
|
get worksheets() {
|
|
@@ -6027,16 +6732,35 @@ var Workbook = class {
|
|
|
6027
6732
|
const spend = (positions, what, sheet) => {
|
|
6028
6733
|
spent += positions;
|
|
6029
6734
|
if (spent <= budget) return;
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6735
|
+
const of = sheet === void 0 ? "" : ` of worksheet "${sheet}"`;
|
|
6736
|
+
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.`, {
|
|
6737
|
+
...sheet === void 0 ? {} : { location: { sheet } },
|
|
6738
|
+
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
6739
|
details: {
|
|
6034
6740
|
limit: "maxCells",
|
|
6035
6741
|
value: budget
|
|
6036
6742
|
}
|
|
6037
6743
|
});
|
|
6038
6744
|
};
|
|
6745
|
+
const mergeBudget = Math.max(MERGED_CELLS_BUDGET, cellsRead);
|
|
6746
|
+
let merged = 0;
|
|
6747
|
+
const spendMerge = (positions, ref, sheet) => {
|
|
6748
|
+
merged += positions;
|
|
6749
|
+
if (merged <= mergeBudget) return;
|
|
6750
|
+
throw new XlvError("XLV_E_LIMIT_EXCEEDED", `The merged range ${ref} of worksheet "${sheet}" would bring the merged cells past ${String(mergeBudget)} for ${String(cellsRead)} cells in the file.`, {
|
|
6751
|
+
location: {
|
|
6752
|
+
sheet,
|
|
6753
|
+
cell: ref
|
|
6754
|
+
},
|
|
6755
|
+
hint: "The file merges far more cells than it holds. Load it with ignoreNodes: [\"mergeCells\"] to skip merged ranges, or read it with xlvane's readWorkbook, which does not create these cells.",
|
|
6756
|
+
details: {
|
|
6757
|
+
limit: "mergedCells",
|
|
6758
|
+
value: mergeBudget
|
|
6759
|
+
}
|
|
6760
|
+
});
|
|
6761
|
+
};
|
|
6039
6762
|
const rowBudget = Math.max(67108864, 16 * cellsRead);
|
|
6763
|
+
const validations = validationBudget();
|
|
6040
6764
|
let rowsLooked = 0;
|
|
6041
6765
|
for (const s of read.sheets) {
|
|
6042
6766
|
if (s.kind !== "worksheet" || options.maxRows === void 0 && options.maxCols === void 0 || !use("sheetData")) continue;
|
|
@@ -6058,7 +6782,7 @@ var Workbook = class {
|
|
|
6058
6782
|
for (const count of perRow.values()) if (count > options.maxCols) limit("maxCols", options.maxCols, count);
|
|
6059
6783
|
}
|
|
6060
6784
|
}
|
|
6061
|
-
this.
|
|
6785
|
+
this._clearSheets();
|
|
6062
6786
|
if (read.dateSystem === "1904") this.properties.date1904 = true;
|
|
6063
6787
|
else delete this.properties.date1904;
|
|
6064
6788
|
const p = read.properties;
|
|
@@ -6078,11 +6802,9 @@ var Workbook = class {
|
|
|
6078
6802
|
this.company = p.company;
|
|
6079
6803
|
this.manager = p.manager;
|
|
6080
6804
|
this._media = mediaOut(read.images);
|
|
6081
|
-
const
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
return Math.max(n, 0);
|
|
6085
|
-
};
|
|
6805
|
+
const worksheetsUpTo = [];
|
|
6806
|
+
for (const s of read.sheets) worksheetsUpTo.push((worksheetsUpTo[worksheetsUpTo.length - 1] ?? 0) + (s.kind === "worksheet" ? 1 : 0));
|
|
6807
|
+
const sheetIndex = (i) => i < 0 || worksheetsUpTo.length === 0 ? 0 : Math.max((worksheetsUpTo[Math.min(i, worksheetsUpTo.length - 1)] ?? 0) - 1, 0);
|
|
6086
6808
|
this.views = read.views.map((v) => {
|
|
6087
6809
|
const view = {
|
|
6088
6810
|
x: v.x ?? 0,
|
|
@@ -6097,14 +6819,22 @@ var Workbook = class {
|
|
|
6097
6819
|
});
|
|
6098
6820
|
this.calcProperties = read.calculation?.fullCalcOnLoad ? { fullCalcOnLoad: true } : {};
|
|
6099
6821
|
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
|
-
|
|
6822
|
+
this._definedNames.model = read.definedNames.filter((d) => !d.name.startsWith("_xlnm.")).map((d) => {
|
|
6823
|
+
const ranges = extractRanges(d.refersTo).filter((range) => {
|
|
6824
|
+
const cells = rangeCellCount(range);
|
|
6825
|
+
if (cells === null) return false;
|
|
6826
|
+
spend(cells, `The defined name "${d.name.slice(0, 255)}"`, void 0);
|
|
6827
|
+
return true;
|
|
6828
|
+
});
|
|
6829
|
+
return {
|
|
6830
|
+
name: d.name,
|
|
6831
|
+
ranges
|
|
6832
|
+
};
|
|
6833
|
+
});
|
|
6104
6834
|
const ids = freeIds(read.sheets.map((s) => s.kind === "worksheet" ? s.sheetId : void 0));
|
|
6105
6835
|
for (const [position, s] of read.sheets.entries()) {
|
|
6106
6836
|
if (s.kind !== "worksheet") continue;
|
|
6107
|
-
const ws = this.
|
|
6837
|
+
const ws = this._addReadWorksheet(ids[position] ?? this.nextId, s.name, { state: s.state });
|
|
6108
6838
|
ws.orderNo = position;
|
|
6109
6839
|
for (const c of use("cols") ? s.columns : []) {
|
|
6110
6840
|
const last = Math.min(c.max, 16384);
|
|
@@ -6141,7 +6871,7 @@ var Workbook = class {
|
|
|
6141
6871
|
const target = ws.getCell(cell.row, cell.col);
|
|
6142
6872
|
let value;
|
|
6143
6873
|
const plain = cell.type === "error" ? { error: cell.value } : cell.value;
|
|
6144
|
-
if (cell.formula && cell.formula
|
|
6874
|
+
if (cell.formula && hasFormulaText(cell.formula)) value = formulaOut(cell.formula, plain, cell.ref, masters);
|
|
6145
6875
|
else value = cell.richText ? richTextOut(cell.richText) : plain;
|
|
6146
6876
|
const h = cell.hyperlink;
|
|
6147
6877
|
if (h && use("hyperlinks") && (h.target !== null || h.location !== null && h.location !== "")) {
|
|
@@ -6183,13 +6913,28 @@ var Workbook = class {
|
|
|
6183
6913
|
for (const m of use("mergeCells") ? s.merges : []) {
|
|
6184
6914
|
if (!m.includes(":")) continue;
|
|
6185
6915
|
spend(areaOf(m), "A merged range", s.name);
|
|
6186
|
-
|
|
6916
|
+
spendMerge(areaOf(m), m, s.name);
|
|
6917
|
+
try {
|
|
6918
|
+
ws.mergeCells(m);
|
|
6919
|
+
} catch (error) {
|
|
6920
|
+
if (error instanceof XlvError || !(error instanceof Error) || error.message !== "Cannot merge already merged cells") throw error;
|
|
6921
|
+
const other = mergeOverlapping(ws, m);
|
|
6922
|
+
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"}.`, {
|
|
6923
|
+
location: {
|
|
6924
|
+
sheet: s.name,
|
|
6925
|
+
cell: m
|
|
6926
|
+
},
|
|
6927
|
+
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.",
|
|
6928
|
+
details: other === void 0 ? {} : { other },
|
|
6929
|
+
cause: error
|
|
6930
|
+
});
|
|
6931
|
+
}
|
|
6187
6932
|
}
|
|
6188
6933
|
if (use("sheetViews")) ws.views = [viewOut(s.view, s.freeze)];
|
|
6189
6934
|
const tab = colorOut(s.tabColor ?? void 0);
|
|
6190
6935
|
if (tab && use("sheetPr")) ws.properties["tabColor"] = tab;
|
|
6191
6936
|
if (s.autoFilter && use("autoFilter")) ws.autoFilter = s.autoFilter;
|
|
6192
|
-
if (use("dataValidations")) ws.dataValidations.model = dataValidationsOut(s.dataValidations, read.dateSystem === "1904");
|
|
6937
|
+
if (use("dataValidations")) ws.dataValidations.model = dataValidationsOut(s.dataValidations, read.dateSystem === "1904", validations);
|
|
6193
6938
|
if (use("conditionalFormatting")) ws.conditionalFormattings = conditionalFormatsOut(s.conditionalFormats);
|
|
6194
6939
|
if (use("sheetProtection")) ws.sheetProtection = s.protection ? protectionOut(s.protection) : null;
|
|
6195
6940
|
if (use("pageSetup")) ws.pageSetup = pageSetupOut(s.pageSetup, s.printArea, s.printTitles);
|
|
@@ -6212,6 +6957,33 @@ var Workbook = class {
|
|
|
6212
6957
|
}
|
|
6213
6958
|
}
|
|
6214
6959
|
};
|
|
6960
|
+
/** Merged cells `xlsx.load` creates at most for a file with fewer cells (one whole column; K2). */
|
|
6961
|
+
const MERGED_CELLS_BUDGET = 1048576;
|
|
6962
|
+
/**
|
|
6963
|
+
* @internal The chunks of an input stream, read until it ends. Reading stops with
|
|
6964
|
+
* XLV_E_LIMIT_EXCEEDED as soon as the chunks pass maxInputBytes, the size the core reads at most,
|
|
6965
|
+
* so an endless stream is not collected first (security review J, C8).
|
|
6966
|
+
*/
|
|
6967
|
+
async function collectInput(stream, what) {
|
|
6968
|
+
const limit = DEFAULT_READ_LIMITS.maxInputBytes;
|
|
6969
|
+
const chunks = [];
|
|
6970
|
+
let size = 0;
|
|
6971
|
+
for await (const chunk of stream) {
|
|
6972
|
+
chunks.push(chunk);
|
|
6973
|
+
size += chunk.length;
|
|
6974
|
+
if (size > limit) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `The stream given to ${what} holds more than ${String(limit)} bytes (maxInputBytes).`, {
|
|
6975
|
+
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 } }.",
|
|
6976
|
+
details: {
|
|
6977
|
+
limit: "maxInputBytes",
|
|
6978
|
+
value: limit
|
|
6979
|
+
}
|
|
6980
|
+
});
|
|
6981
|
+
}
|
|
6982
|
+
return {
|
|
6983
|
+
chunks,
|
|
6984
|
+
size
|
|
6985
|
+
};
|
|
6986
|
+
}
|
|
6215
6987
|
/**
|
|
6216
6988
|
* Worksheet ids for a list of wanted ids: each whole number from 1 to 65534 (the sheet ids Office
|
|
6217
6989
|
* uses, W2-20) that is not wanted by an earlier entry is kept; other entries (missing, invalid,
|
|
@@ -6236,17 +7008,33 @@ function freeIds(wanted) {
|
|
|
6236
7008
|
return next;
|
|
6237
7009
|
});
|
|
6238
7010
|
}
|
|
6239
|
-
/**
|
|
6240
|
-
function
|
|
7011
|
+
/** The rows and columns of a range such as "A1:C3" or a cell, or `null` for anything else. */
|
|
7012
|
+
function rangeOf(ref) {
|
|
6241
7013
|
const m = /^([A-Z]{1,3})(\d{1,7})(?::([A-Z]{1,3})(\d{1,7}))?$/.exec(ref.replace(/\$/g, ""));
|
|
6242
|
-
if (!m) return
|
|
7014
|
+
if (!m) return null;
|
|
6243
7015
|
const col = (letters) => {
|
|
6244
7016
|
let n = 0;
|
|
6245
7017
|
for (const ch of letters) n = n * 26 + ch.charCodeAt(0) - 64;
|
|
6246
7018
|
return n;
|
|
6247
7019
|
};
|
|
6248
7020
|
const [, c1 = "A", r1 = "1", c2 = c1, r2 = r1] = m;
|
|
6249
|
-
return
|
|
7021
|
+
return {
|
|
7022
|
+
top: Math.min(Number(r1), Number(r2)),
|
|
7023
|
+
left: Math.min(col(c1), col(c2)),
|
|
7024
|
+
bottom: Math.max(Number(r1), Number(r2)),
|
|
7025
|
+
right: Math.max(col(c1), col(c2))
|
|
7026
|
+
};
|
|
7027
|
+
}
|
|
7028
|
+
/** Cells in a range such as "A1:C3" (1 for a cell, 0 for anything else). */
|
|
7029
|
+
function areaOf(ref) {
|
|
7030
|
+
const r = rangeOf(ref);
|
|
7031
|
+
return r ? (r.bottom - r.top + 1) * (r.right - r.left + 1) : 0;
|
|
7032
|
+
}
|
|
7033
|
+
/** A merged range of the worksheet that shares cells with `ref`, as "A1:B2". */
|
|
7034
|
+
function mergeOverlapping(ws, ref) {
|
|
7035
|
+
const r = rangeOf(ref);
|
|
7036
|
+
if (!r) return void 0;
|
|
7037
|
+
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
7038
|
}
|
|
6251
7039
|
// src/stream.ts
|
|
6252
7040
|
/**
|
|
@@ -6259,7 +7047,7 @@ function streamValueIn(cell, masters) {
|
|
|
6259
7047
|
const v = cell.value;
|
|
6260
7048
|
if (v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
|
|
6261
7049
|
const r = v;
|
|
6262
|
-
if (typeof r["
|
|
7050
|
+
if (typeof r["ref"] === "string" && r["shareType"] !== "array" && typeof r["formula"] === "string") {
|
|
6263
7051
|
masters.set(cell.address, {
|
|
6264
7052
|
formula: r["formula"],
|
|
6265
7053
|
row: cell.row,
|
|
@@ -6641,7 +7429,7 @@ var WorkbookWriter = class {
|
|
|
6641
7429
|
/** Adds a worksheet. Sheets are written in the order their first rows are committed. */
|
|
6642
7430
|
addWorksheet(name, options) {
|
|
6643
7431
|
const sheet = new WorksheetWriter(this, this.nextId++, name, options);
|
|
6644
|
-
this._book.
|
|
7432
|
+
this._book._putSheet(sheet);
|
|
6645
7433
|
this.open.push(sheet);
|
|
6646
7434
|
return sheet;
|
|
6647
7435
|
}
|
|
@@ -6866,6 +7654,44 @@ var WorksheetReader = class extends Emitter {
|
|
|
6866
7654
|
}
|
|
6867
7655
|
};
|
|
6868
7656
|
/**
|
|
7657
|
+
* The value of a shared-formula dependent read by the streaming reader: `{ sharedFormula: master,
|
|
7658
|
+
* result }`, as exceljs's xlsx.load gives it, with a non-enumerable `formula` that moves the
|
|
7659
|
+
* master's text (`masters`, by address) to `row`/`col` when it is first read. The master's row is
|
|
7660
|
+
* not kept, so a sheet the value is copied into (stream.xlsx.WorkbookWriter, whose committed rows
|
|
7661
|
+
* are dropped) can write this formula instead of looking the master up (K3); reading the rows
|
|
7662
|
+
* alone builds no moved texts (K2). Setting `formula` replaces it with an ordinary property. A move
|
|
7663
|
+
* counts against the shared-formula budget of `workbook`, as `cell.formula` does (K4 S1).
|
|
7664
|
+
*/
|
|
7665
|
+
function sharedDependent(master, result, masters, row, col, workbook, sheet) {
|
|
7666
|
+
const value = {
|
|
7667
|
+
sharedFormula: master,
|
|
7668
|
+
...result
|
|
7669
|
+
};
|
|
7670
|
+
let moved;
|
|
7671
|
+
let built = false;
|
|
7672
|
+
Object.defineProperty(value, "formula", {
|
|
7673
|
+
configurable: true,
|
|
7674
|
+
enumerable: false,
|
|
7675
|
+
get() {
|
|
7676
|
+
if (!built) {
|
|
7677
|
+
const text = masters.get(master);
|
|
7678
|
+
moved = text === void 0 ? void 0 : moveSharedFormula(workbook, sheet, text, decodeAddress(master), row, col);
|
|
7679
|
+
built = true;
|
|
7680
|
+
}
|
|
7681
|
+
return moved;
|
|
7682
|
+
},
|
|
7683
|
+
set(formula) {
|
|
7684
|
+
Object.defineProperty(this, "formula", {
|
|
7685
|
+
value: formula,
|
|
7686
|
+
writable: true,
|
|
7687
|
+
enumerable: true,
|
|
7688
|
+
configurable: true
|
|
7689
|
+
});
|
|
7690
|
+
}
|
|
7691
|
+
});
|
|
7692
|
+
return value;
|
|
7693
|
+
}
|
|
7694
|
+
/**
|
|
6869
7695
|
* The streaming reader of exceljs (`ExcelJS.stream.xlsx.WorkbookReader`): iterate worksheets
|
|
6870
7696
|
* and their rows with `for await`, or call `read()` and listen to `worksheet`, `end`,
|
|
6871
7697
|
* `finished` and `error` events (and `entry` with `entries: 'emit'`). Rows are not kept.
|
|
@@ -6894,8 +7720,7 @@ var WorkbookReader = class extends Emitter {
|
|
|
6894
7720
|
return openWorkbookStreamFile(input);
|
|
6895
7721
|
}
|
|
6896
7722
|
if (input instanceof Uint8Array || input instanceof ArrayBuffer) return openWorkbookStream(input);
|
|
6897
|
-
const chunks =
|
|
6898
|
-
for await (const c of input) chunks.push(c);
|
|
7723
|
+
const { chunks } = await collectInput(input, "stream.xlsx.WorkbookReader");
|
|
6899
7724
|
return openWorkbookStream(new Blob(chunks));
|
|
6900
7725
|
}
|
|
6901
7726
|
/** Worksheet events in workbook order, as exceljs `parse()` yields them (`{ eventType: 'worksheet', value }`). */
|
|
@@ -6914,18 +7739,33 @@ var WorkbookReader = class extends Emitter {
|
|
|
6914
7739
|
type: "worksheet",
|
|
6915
7740
|
id: String(id)
|
|
6916
7741
|
});
|
|
6917
|
-
const sheet = host.
|
|
7742
|
+
const sheet = host._addReadWorksheet(host.nextId, info.name, { state: info.state });
|
|
6918
7743
|
const name = info.name;
|
|
7744
|
+
const masters = /* @__PURE__ */ new Map();
|
|
7745
|
+
const kept = /* @__PURE__ */ new Map();
|
|
7746
|
+
sheet._sharedMasters = kept;
|
|
6919
7747
|
const rows = async function* () {
|
|
6920
7748
|
for await (const r of book.rows(name)) {
|
|
6921
7749
|
const row = sheet.getRow(r.number);
|
|
6922
7750
|
for (const cell of r.cells) {
|
|
6923
7751
|
const plain = cell.type === "error" ? { error: cell.value } : cell.value;
|
|
6924
7752
|
const target = row.getCell(cell.col);
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
7753
|
+
const f = cell.formula;
|
|
7754
|
+
const result = plain === null ? {} : { result: plain };
|
|
7755
|
+
let value;
|
|
7756
|
+
if (f && f.kind === "shared" && f.sharedIndex !== null && f.ref === null && masters.has(f.sharedIndex) && hasFormulaText(f)) value = sharedDependent(masters.get(f.sharedIndex), result, kept, r.number, cell.col, host, name);
|
|
7757
|
+
else if (f && hasFormulaText(f)) {
|
|
7758
|
+
const text = f.text ?? "";
|
|
7759
|
+
if (f.kind === "shared" && f.sharedIndex !== null && f.ref !== null) {
|
|
7760
|
+
masters.set(f.sharedIndex, cell.ref);
|
|
7761
|
+
kept.set(cell.ref, text);
|
|
7762
|
+
}
|
|
7763
|
+
value = {
|
|
7764
|
+
formula: text,
|
|
7765
|
+
...result
|
|
7766
|
+
};
|
|
7767
|
+
} else value = cell.richText ? richTextOut(cell.richText) : plain;
|
|
7768
|
+
target.value = value;
|
|
6929
7769
|
if (styles) target.style = styleOut(cell.style, cell.numFmt);
|
|
6930
7770
|
}
|
|
6931
7771
|
for (const cell of r.styledCells) {
|