@xlvane/compat 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,7 @@ In CommonJS, `const ExcelJS = require('exceljs');` gives the same object; call `
56
56
  The full list, with the exceljs source of each behaviour, is in [docs/exceljs-compat.md](https://github.com/DevXord/xlvane/blob/main/docs/exceljs-compat.md) (also in `node_modules/xlvane/docs/exceljs-compat.md` after installing). The ones most likely to matter:
57
57
 
58
58
  - **Not offered:** row and column operations on a streamed worksheet (`insertRow(s)`, `spliceRows`, `duplicateRow`, `spliceColumns`; exceljs's streaming worksheet does not have them either) throw an `XlvError` with code `XLV_E_NOT_SUPPORTED` and the name of the method. Deep imports of exceljs internals (`exceljs/lib/...`) do not resolve; `exceljs/dist/es5` and the `exceljs/dist/exceljs*.js` files do. Nothing is dropped silently.
59
- - **CSV:** text that starts with `=`, `+`, `-`, `@`, a tab, CR or LF is written with a leading `'`, so spreadsheet applications do not run it as a formula. `formulaInjection: "off"` writes it unchanged, as exceljs does.
59
+ - **CSV:** text that starts with `=`, `+`, `-`, `@`, a tab, CR, LF or another control character, or with a full-width or small form of these signs, is written with a leading `'`, so spreadsheet applications do not run it as a formula. `formulaInjection: "off"` writes it unchanged, as exceljs does.
60
60
  - **Sheet ids:** `worksheet.id` is written as `sheetId` and kept on loading, as in exceljs; an id outside 1 to 65534 is written as the lowest unused one, and the streaming `WorkbookReader` numbers worksheets 1, 2, 3… in file order.
61
61
  - **Views:** only the first view of a worksheet is written; a frozen view always gets the cell right of and below the panes as `topLeftCell`.
62
62
  - **Notes:** `protection.locked` and `lockText` read back as `'True'`/`'False'`, the values Excel writes.
@@ -66,6 +66,7 @@ The full list, with the exceljs source of each behaviour, is in [docs/exceljs-co
66
66
 
67
67
  ## Links
68
68
 
69
+ - Website: [xlvane.github.io](https://xlvane.github.io)
69
70
  - Changes in each version: [CHANGELOG.md](https://github.com/DevXord/xlvane/blob/main/CHANGELOG.md)
70
71
  - Bugs and questions: [GitHub issues](https://github.com/DevXord/xlvane/issues)
71
72
  - Reporting a vulnerability: [SECURITY.md](https://github.com/DevXord/xlvane/blob/main/SECURITY.md)
package/dist/index.d.ts CHANGED
@@ -2700,6 +2700,20 @@ export declare class Column {
2700
2700
  get protection(): Partial<Protection> | undefined;
2701
2701
  set protection(v: Partial<Protection> | undefined);
2702
2702
  }
2703
+ /** A merged range of a worksheet. */
2704
+ type MergeRange = {
2705
+ top: number;
2706
+ left: number;
2707
+ bottom: number;
2708
+ right: number;
2709
+ };
2710
+ /** The merged ranges of a worksheet by master address, with a counter of changes (D-H01). */
2711
+ declare class MergeMap extends Map<string, MergeRange> {
2712
+ version: number;
2713
+ override set(key: string, value: MergeRange): this;
2714
+ override delete(key: string): boolean;
2715
+ override clear(): void;
2716
+ }
2703
2717
  /**
2704
2718
  * A worksheet, as exceljs `Worksheet` (lib/doc/worksheet.js).
2705
2719
  *
@@ -2761,12 +2775,10 @@ export declare class Worksheet {
2761
2775
  /** @internal */
2762
2776
  _columns: Column[];
2763
2777
  /** @internal */
2764
- _merges: Map<string, {
2765
- top: number;
2766
- left: number;
2767
- bottom: number;
2768
- right: number;
2769
- }>;
2778
+ _merges: MergeMap;
2779
+ /** The cells covered by `_merges` (row × 16 385 + column), valid while `_mergedVersion` equals `_merges.version`. */
2780
+ private _mergedCells;
2781
+ private _mergedVersion;
2770
2782
  private readonly _keys;
2771
2783
  private _name;
2772
2784
  /** @internal Use `workbook.addWorksheet()`. */
@@ -2929,6 +2941,8 @@ export declare class Worksheet {
2929
2941
  mergeCellsWithoutStyle(...args: unknown[]): void;
2930
2942
  private _mergeKeepStyle;
2931
2943
  mergeCells(...args: unknown[]): void;
2944
+ /** The index of merged cells, rebuilt from `_merges` after any other change to it. */
2945
+ private _mergedIndex;
2932
2946
  unMergeCells(...args: unknown[]): void;
2933
2947
  get hasMerges(): boolean;
2934
2948
  /** Adds conditional formatting for a range (exceljs `addConditionalFormatting`). */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { XlvError, adjustFormula, adjustRange, createWorkbook, hashPassword, imageFormat, openWorkbookStream, readWorkbook, shiftFormula, writeWorkbook, writeWorkbookStream } from "xlvane";
1
+ import { DEFAULT_READ_LIMITS, XlvError, adjustFormula, adjustRange, createWorkbook, hashPassword, imageFormat, openWorkbookStream, readWorkbook, shiftFormula, writeWorkbook, writeWorkbookStream } from "xlvane";
2
2
  // src/media.ts
3
3
  const MAX_COLS = 16384;
4
4
  const MAX_ROWS = 1048576;
@@ -3628,6 +3628,22 @@ function parseRangeArgs(args) {
3628
3628
  right: Math.max(s.col, e.col)
3629
3629
  };
3630
3630
  }
3631
+ /** The merged ranges of a worksheet by master address, with a counter of changes (D-H01). */
3632
+ var MergeMap = class extends Map {
3633
+ version = 0;
3634
+ set(key, value) {
3635
+ this.version++;
3636
+ return super.set(key, value);
3637
+ }
3638
+ delete(key) {
3639
+ this.version++;
3640
+ return super.delete(key);
3641
+ }
3642
+ clear() {
3643
+ this.version++;
3644
+ super.clear();
3645
+ }
3646
+ };
3631
3647
  /**
3632
3648
  * A worksheet, as exceljs `Worksheet` (lib/doc/worksheet.js).
3633
3649
  *
@@ -3673,7 +3689,10 @@ var Worksheet = class {
3673
3689
  /** @internal */
3674
3690
  _columns = [];
3675
3691
  /** @internal */
3676
- _merges = /* @__PURE__ */ new Map();
3692
+ _merges = new MergeMap();
3693
+ /** The cells covered by `_merges` (row × 16 385 + column), valid while `_mergedVersion` equals `_merges.version`. */
3694
+ _mergedCells = /* @__PURE__ */ new Set();
3695
+ _mergedVersion = -1;
3677
3696
  _keys = /* @__PURE__ */ new Map();
3678
3697
  _name = "";
3679
3698
  /** @internal Use `workbook.addWorksheet()`. */
@@ -4378,7 +4397,8 @@ var Worksheet = class {
4378
4397
  _mergeKeepStyle = false;
4379
4398
  mergeCells(...args) {
4380
4399
  const range = parseRangeArgs(args);
4381
- for (const m of this._merges.values()) if (range.top <= m.bottom && m.top <= range.bottom && range.left <= m.right && m.left <= range.right) throw new Error("Cannot merge already merged cells");
4400
+ const merged = this._mergedIndex();
4401
+ for (let r = range.top; r <= range.bottom; r++) for (let c = range.left; c <= range.right; c++) if (merged.has(r * 16385 + c)) throw new Error("Cannot merge already merged cells");
4382
4402
  const master = this.getCell(range.top, range.left);
4383
4403
  for (let r = range.top; r <= range.bottom; r++) for (let c = range.left; c <= range.right; c++) {
4384
4404
  if (r === range.top && c === range.left) continue;
@@ -4388,6 +4408,17 @@ var Worksheet = class {
4388
4408
  }
4389
4409
  master._mergeCount++;
4390
4410
  this._merges.set(`${columnLetter(range.left)}${String(range.top)}`, range);
4411
+ for (let r = range.top; r <= range.bottom; r++) for (let c = range.left; c <= range.right; c++) merged.add(r * 16385 + c);
4412
+ this._mergedVersion = this._merges.version;
4413
+ }
4414
+ /** The index of merged cells, rebuilt from `_merges` after any other change to it. */
4415
+ _mergedIndex() {
4416
+ if (this._mergedVersion !== this._merges.version) {
4417
+ this._mergedCells = /* @__PURE__ */ new Set();
4418
+ for (const m of this._merges.values()) for (let r = m.top; r <= m.bottom; r++) for (let c = m.left; c <= m.right; c++) this._mergedCells.add(r * 16385 + c);
4419
+ this._mergedVersion = this._merges.version;
4420
+ }
4421
+ return this._mergedCells;
4391
4422
  }
4392
4423
  unMergeCells(...args) {
4393
4424
  const range = parseRangeArgs(args);
@@ -4575,7 +4606,11 @@ const INJECTION_START = /* @__PURE__ */ new Set([
4575
4606
  "=",
4576
4607
  "+",
4577
4608
  "-",
4578
- "@"
4609
+ "@",
4610
+ "﹦",
4611
+ "﹢",
4612
+ "﹣",
4613
+ "﹫"
4579
4614
  ]);
4580
4615
  function invalid(name, message, hint) {
4581
4616
  return new XlvError("XLV_E_INVALID_OPTION", message, {
@@ -5032,7 +5067,7 @@ function worksheetCsv(sheet, options) {
5032
5067
  const escape = (out) => {
5033
5068
  if (injection !== "escape" || typeof out !== "string") return [out, false];
5034
5069
  const first = out.replace(/\0/g, "")[0];
5035
- return first !== void 0 && INJECTION_START.has(first) ? [`'${out}`, f.quote !== ""] : [out, false];
5070
+ return first !== void 0 && (INJECTION_START.has(first) || first.charCodeAt(0) < 32) ? [`'${out}`, f.quote !== ""] : [out, false];
5036
5071
  };
5037
5072
  if (f.headers) lines.push(f.headers.map((h, i) => {
5038
5073
  const [out, force] = escape(h);
@@ -5985,6 +6020,24 @@ var Workbook = class {
5985
6020
  _fill(read, options = {}) {
5986
6021
  const ignore = new Set(options.ignoreNodes ?? []);
5987
6022
  const use = (node) => !ignore.has(node);
6023
+ let cellsRead = 0;
6024
+ for (const s of read.sheets) cellsRead += s.cellCount;
6025
+ const budget = Math.max(DEFAULT_READ_LIMITS.maxCells, 16 * cellsRead);
6026
+ let spent = 0;
6027
+ const spend = (positions, what, sheet) => {
6028
+ spent += positions;
6029
+ 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.",
6033
+ details: {
6034
+ limit: "maxCells",
6035
+ value: budget
6036
+ }
6037
+ });
6038
+ };
6039
+ const rowBudget = Math.max(67108864, 16 * cellsRead);
6040
+ let rowsLooked = 0;
5988
6041
  for (const s of read.sheets) {
5989
6042
  if (s.kind !== "worksheet" || options.maxRows === void 0 && options.maxCols === void 0 || !use("sheetData")) continue;
5990
6043
  const perRow = /* @__PURE__ */ new Map();
@@ -6055,6 +6108,7 @@ var Workbook = class {
6055
6108
  ws.orderNo = position;
6056
6109
  for (const c of use("cols") ? s.columns : []) {
6057
6110
  const last = Math.min(c.max, 16384);
6111
+ spend(Math.max(0, last - c.min + 1), "The column definitions", s.name);
6058
6112
  for (let n = c.min; n <= last; n++) {
6059
6113
  const column = ws.getColumn(n);
6060
6114
  if (c.width !== null) column.width = c.width;
@@ -6108,6 +6162,15 @@ var Workbook = class {
6108
6162
  }
6109
6163
  const dim = /(\d+)$/.exec(s.dimension ?? "");
6110
6164
  lastRow = use("sheetData") ? Math.min(Math.max(lastRow, dim ? Number(dim[1]) : 0), 1048576) : 0;
6165
+ rowsLooked += lastRow;
6166
+ if (rowsLooked > rowBudget) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `The rows of worksheet "${s.name}" up to its dimension or last cell would bring the rows looked at for their settings past ${String(rowBudget)} for ${String(cellsRead)} cells in the file.`, {
6167
+ location: { sheet: s.name },
6168
+ hint: "The sheets declare far more rows than they hold. Load the file with ignoreNodes: [\"sheetData\"] to skip cells and rows, or read it with xlvane's readWorkbook.",
6169
+ details: {
6170
+ limit: "maxCells",
6171
+ value: rowBudget
6172
+ }
6173
+ });
6111
6174
  for (let r = 1; r <= lastRow; r++) {
6112
6175
  const info = s.rowInfo(r);
6113
6176
  if (!info) continue;
@@ -6117,7 +6180,11 @@ var Workbook = class {
6117
6180
  if (info.outlineLevel) row.outlineLevel = info.outlineLevel;
6118
6181
  }
6119
6182
  for (const note of s.notes) ws.getCell(note.ref).note = noteOut(note);
6120
- for (const m of use("mergeCells") ? s.merges : []) if (m.includes(":")) ws.mergeCells(m);
6183
+ for (const m of use("mergeCells") ? s.merges : []) {
6184
+ if (!m.includes(":")) continue;
6185
+ spend(areaOf(m), "A merged range", s.name);
6186
+ ws.mergeCells(m);
6187
+ }
6121
6188
  if (use("sheetViews")) ws.views = [viewOut(s.view, s.freeze)];
6122
6189
  const tab = colorOut(s.tabColor ?? void 0);
6123
6190
  if (tab && use("sheetPr")) ws.properties["tabColor"] = tab;
@@ -6128,7 +6195,10 @@ var Workbook = class {
6128
6195
  if (use("pageSetup")) ws.pageSetup = pageSetupOut(s.pageSetup, s.printArea, s.printTitles);
6129
6196
  if (use("headerFooter")) ws.headerFooter = headerFooterOut(s.headerFooter);
6130
6197
  if (use("rowBreaks")) ws.rowBreaks = rowBreaksOut(s.pageBreaks.rows);
6131
- for (const def of use("tableParts") ? s.tables : []) ws.tables[def.name] = tableOut(ws, def);
6198
+ for (const def of use("tableParts") ? s.tables : []) {
6199
+ spend(areaOf(def.ref), "A table", s.name);
6200
+ ws.tables[def.name] = tableOut(ws, def);
6201
+ }
6132
6202
  for (const img of use("drawing") ? s.images : []) {
6133
6203
  const id = img.image === null ? void 0 : mediaIds.get(img.image);
6134
6204
  const media = id === void 0 ? null : imageOut(ws, img, id);
@@ -6166,6 +6236,18 @@ function freeIds(wanted) {
6166
6236
  return next;
6167
6237
  });
6168
6238
  }
6239
+ /** Cells in a range such as "A1:C3" (1 for a cell, 0 for anything else). */
6240
+ function areaOf(ref) {
6241
+ 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;
6243
+ const col = (letters) => {
6244
+ let n = 0;
6245
+ for (const ch of letters) n = n * 26 + ch.charCodeAt(0) - 64;
6246
+ return n;
6247
+ };
6248
+ 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);
6250
+ }
6169
6251
  // src/stream.ts
6170
6252
  /**
6171
6253
  * A cell value for the streaming writer. Rows are written as they are committed, before later
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xlvane/compat",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
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.1.0"
75
+ "xlvane": "^1.2.0"
76
76
  },
77
77
  "publishConfig": {
78
78
  "access": "public",