@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +56 -17
  2. package/dist/index.js +970 -130
  3. 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 overlaps = (a, b) => a.top <= b.bottom && b.top <= a.bottom && a.left <= b.right && b.left <= a.right;
1044
- const bad = /* @__PURE__ */ new Set();
1045
- const sorted = [...groups].sort((x, y) => x[1].top - y[1].top);
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
- for (const [master, b] of groups) if (!bad.has(master)) out.set(master, `${columnLetter(b.left)}${String(b.top)}:${columnLetter(b.right)}${String(b.bottom)}`);
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 same = coreBytes.findIndex((b) => b.length === bytes.length && b.every((x, i) => x === bytes[i]));
1353
- if (same === -1) {
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) at(ws.name, cell.address, () => writeCell(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 = useStyles ? cellStyleIn(cell, styleOf(cell.style)) : isDateCell(cell) ? { numFmt: 14 } : {};
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 sheet up to which validations read from a file are given to every cell they cover,
1729
- * as exceljs does (data-validations-xform.js L229–239). Larger ranges, such as whole columns,
1730
- * keep their range as the key; `find()` and `cell.dataValidation` still answer for every cell.
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
- /** The `worksheet.dataValidations` model of validations read by xlvane. */
1910
- function dataValidationsOut(list, date1904) {
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["formula"] || value["sharedFormula"]) return ValueType.Formula;
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
- if (typeof v["sharedFormula"] === "string") {
2980
- const masterRef = v["sharedFormula"].replace(/\$/g, "");
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
- const text = master?.formula;
2983
- if (!master || text === void 0 || master === this) return void 0;
2984
- return shiftFormula(text, this.row - master.row, this.col - master.col) ?? void 0;
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
- let name = value;
3721
- if (name === this._name) return;
3722
- if (typeof name !== "string") throw new Error("The name has to be a string.");
3723
- if (name === "") throw new Error("The name can't be empty.");
3724
- if (name === "History") throw new Error("The name \"History\" is protected. Please use a different name.");
3725
- if (/[*?:/\\[\]]/.test(name)) throw new Error(`Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]`);
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 = /\d*[^-_:/,()\s\d]+/;
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
- /** Splits CSV text into rows of fields as fast-csv 4.3 does with the given parser options. */
4901
- function parseCsv(text, o) {
4902
- const rows = [];
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
- fields.push(value);
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
- fields.push(text.slice(at, i));
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
- rows.push(row.map((f) => o.trim ? f.trim() : o.ltrim ? f.trimStart() : o.rtrim ? f.trimEnd() : f));
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
- let out = rows;
4969
- if (o.ignoreEmpty) out = out.filter((r) => r.some((f) => f.trim() !== ""));
4970
- if (o.skipHeader) out = out.slice(1);
4971
- if (o.skipRows > 0) out = out.slice(o.skipRows);
4972
- if (o.maxRows > 0) out = out.slice(0, o.maxRows);
4973
- return out;
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["formula"] || v["sharedFormula"] || "result" in 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
- * The CSV text of a worksheet, as exceljs `csv.write` gives it.
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 = (out) => {
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 first = out.replace(/\0/g, "")[0];
5070
- return first !== void 0 && (INJECTION_START.has(first) || first.charCodeAt(0) < 32) ? [`'${out}`, f.quote !== ""] : [out, false];
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 formatField(out, i, f, force);
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 [out, force] = escape(map(values[i], i));
5089
- fields.push(formatField(out, i, f, force));
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") text += chunk;
5133
- else if (chunk instanceof Uint8Array) text += decoder.decode(chunk, { stream: true });
5134
- else if (chunk instanceof ArrayBuffer) text += decoder.decode(new Uint8Array(chunk), { stream: true });
5135
- 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." });
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 rows = parseCsv(await readText(stream, parser.encoding), parser);
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
- for (const row of rows) worksheet.addRow(row.map((datum, i) => map(datum, i)));
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) 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);
5372
- else this.findCellEx(address, true);
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
- /** @internal Names of the other sheets, for the duplicate check of Worksheet.name. */
5887
- _names = (exclude) => this.worksheets.filter((w) => w !== exclude).map((w) => w.name);
5888
- /** The id the next worksheet gets (lib/doc/workbook.js `nextId`). */
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
- for (let i = 1; i < this._sheets.length; i++) if (!this._sheets[i]) return i;
5891
- return this._sheets.length || 1;
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
- sheet.orderNo = this._sheets.reduce((acc, ws) => ws?.orderNo !== void 0 && ws.orderNo > acc ? ws.orderNo : acc, 0) + 1;
5901
- this._sheets[id] = sheet;
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._sheets[sheet.id] = void 0;
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") return this.worksheets.find((w) => w.name === id);
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
- throw new XlvError("XLV_E_LIMIT_EXCEEDED", `${what} of worksheet "${sheet}" would create more than ${String(budget)} cells or columns for ${String(cellsRead)} cells in the file.`, {
6031
- location: { sheet },
6032
- hint: "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.",
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._sheets = [];
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 sheetIndex = (i) => {
6082
- let n = -1;
6083
- for (let k = 0; k <= i && k < read.sheets.length; k++) if (read.sheets[k].kind === "worksheet") n++;
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
- name: d.name,
6102
- ranges: extractRanges(d.refersTo)
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._addWorksheetWithId(ids[position] ?? this.nextId, s.name, { state: s.state });
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.text !== null) value = formulaOut(cell.formula, plain, cell.ref, masters);
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
- ws.mergeCells(m);
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
- /** Cells in a range such as "A1:C3" (1 for a cell, 0 for anything else). */
6240
- function areaOf(ref) {
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 0;
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 (Math.abs(Number(r2) - Number(r1)) + 1) * (Math.abs(col(c2) - col(c1)) + 1);
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["formula"] === "string" && r["shareType"] !== "array" && typeof r["ref"] === "string") {
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._sheets[sheet.id] = sheet;
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.addWorksheet(info.name, { state: info.state });
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
- target.value = cell.formula && cell.formula.text !== null ? plain === null ? { formula: cell.formula.text } : {
6926
- formula: cell.formula.text,
6927
- result: plain
6928
- } : cell.richText ? richTextOut(cell.richText) : plain;
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) {