@xlvane/compat 1.2.1 → 1.2.3

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