@xlvane/compat 1.0.1 → 1.1.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
@@ -57,8 +57,8 @@ The full list, with the exceljs source of each behaviour, is in [docs/exceljs-co
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
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.
60
- - **Sheet ids:** the file gets `sheetId` 1, 2, 3… in sheet order, and a loaded workbook numbers its sheets 1, 2, 3… in file order. After removing a sheet, `getWorksheet(id)` can give another sheet than with exceljs; look sheets up by name.
61
- - **Views:** only the first view of a worksheet is written; a split view is written without its `topLeftCell`.
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
+ - **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.
63
63
  - **Module object:** it is frozen, so adding or replacing its properties (monkey-patching) has no effect.
64
64
  - **Streaming writer speed:** `stream.xlsx.WorkbookWriter` compresses in JavaScript (fflate) and took about twice as long as exceljs, which uses Node.js zlib (300 000 rows: 6.1 s against 2.8 s).
package/dist/index.d.ts CHANGED
@@ -295,6 +295,8 @@ export declare class Workbook {
295
295
  get nextId(): number;
296
296
  /** Adds a worksheet. Names follow exceljs rules (31 characters at most; longer names are cut). */
297
297
  addWorksheet(name?: string, options?: WorksheetOptions): Worksheet;
298
+ /** @internal Adds a worksheet with a given id (a loaded file's sheetId, a model's id; D-F03). */
299
+ _addWorksheetWithId(id: number, name?: string, options?: WorksheetOptions): Worksheet;
298
300
  removeWorksheet(id: number | string): void;
299
301
  /** Removes a worksheet by id or name, as `removeWorksheet` (lib/doc/workbook.js L77). */
300
302
  removeWorksheetEx(worksheet: Worksheet): void;
package/dist/index.js CHANGED
@@ -1228,9 +1228,10 @@ function noteBoxOut(box) {
1228
1228
  * The first exceljs view and the tab colour (X4-11, X4-12). xlvane writes one view: frozen panes
1229
1229
  * (`xSplit`/`ySplit` in columns and rows) or a split window (`xSplit`/`ySplit` in 1/20 point, as
1230
1230
  * exceljs), plus `zoomScale`, `showGridLines`, `showRowColHeaders`, `rightToLeft` and `style`.
1231
- * `activeCell` is written for every view; `topLeftCell` only for a normal view (a split view's
1232
- * exceljs `topLeftCell` is `pane@topLeftCell`, which the core does not write); `activePane`
1233
- * follows from the panes.
1231
+ * `activeCell` is written for every view; `topLeftCell` for a normal view as
1232
+ * `sheetView@topLeftCell` and for a split view as `pane@topLeftCell`, as exceljs writes them
1233
+ * (sheet-view-xform.js L72-85; D-F02); a frozen view's `topLeftCell` follows from the panes;
1234
+ * `activePane` follows from the panes.
1234
1235
  */
1235
1236
  function layoutIn(ws) {
1236
1237
  const v = ws.views[0];
@@ -1251,10 +1252,14 @@ function layoutIn(ws) {
1251
1252
  rows: v.ySplit ?? 0,
1252
1253
  cols: v.xSplit ?? 0
1253
1254
  };
1254
- else if (v.state === "split" && ((v.xSplit ?? 0) > 0 || (v.ySplit ?? 0) > 0)) view.split = {
1255
- x: (v.xSplit ?? 0) / 20,
1256
- y: (v.ySplit ?? 0) / 20
1257
- };
1255
+ else if (v.state === "split" && ((v.xSplit ?? 0) > 0 || (v.ySplit ?? 0) > 0)) {
1256
+ const corner = typeof v["topLeftCell"] === "string" && v["topLeftCell"] !== "" ? { topLeftCell: v["topLeftCell"] } : {};
1257
+ view.split = {
1258
+ x: (v.xSplit ?? 0) / 20,
1259
+ y: (v.ySplit ?? 0) / 20,
1260
+ ...corner
1261
+ };
1262
+ }
1258
1263
  }
1259
1264
  let tab = ws.properties["tabColor"];
1260
1265
  if (tab && typeof tab.argb === "string" && /^[0-9A-Fa-f]{7}$/.test(tab.argb)) tab = {
@@ -1356,7 +1361,10 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
1356
1361
  if (meta.views && meta.views.length > 0) book.views(bookViewsIn(meta.views));
1357
1362
  if (meta.calcProperties?.["fullCalcOnLoad"]) book.calculation({ fullCalcOnLoad: true });
1358
1363
  for (const ws of sheets) at(ws.name, void 0, () => {
1359
- const sheet = book.addSheet(ws.name, { state: ws.state });
1364
+ const sheet = book.addSheet(ws.name, {
1365
+ state: ws.state,
1366
+ ...sheetIdOf(ws.id)
1367
+ });
1360
1368
  const useStyles = meta.useStyles !== false;
1361
1369
  const sharedRanges = sharedFormulaRanges(ws);
1362
1370
  const props = sheetPropertiesIn(ws.properties);
@@ -1487,8 +1495,9 @@ function fontOut(f) {
1487
1495
  /**
1488
1496
  * An exceljs view from the view read by xlvane, shaped as exceljs reads one (sheet-view-xform.js
1489
1497
  * L145–192; X4-11): a missing `zoomScaleNormal` reads as 100; a frozen view gets the default
1490
- * `topLeftCell`; a split view gets `xSplit`/`ySplit` in 1/20 point and the active pane xlvane
1491
- * writes; a normal view has no `topLeftCell`, as in exceljs.
1498
+ * `topLeftCell`; a split view gets `xSplit`/`ySplit` in 1/20 point, `pane@topLeftCell` as
1499
+ * `topLeftCell` (sheet-view-xform.js L118-124) and the active pane xlvane writes; a normal view
1500
+ * has no `topLeftCell`, as in exceljs.
1492
1501
  */
1493
1502
  function viewOut(view, freeze) {
1494
1503
  const common = {
@@ -1516,14 +1525,14 @@ function viewOut(view, freeze) {
1516
1525
  }
1517
1526
  const active = view.activeCell !== null ? { activeCell: view.activeCell } : {};
1518
1527
  if (view.split) {
1519
- const { x, y } = view.split;
1528
+ const { x, y, topLeftCell } = view.split;
1520
1529
  const activePane = x > 0 && y > 0 ? "bottomRight" : y > 0 ? "bottomLeft" : "topRight";
1521
1530
  return {
1522
1531
  ...common,
1523
1532
  state: "split",
1524
1533
  xSplit: Math.round(x * 20),
1525
1534
  ySplit: Math.round(y * 20),
1526
- topLeftCell: void 0,
1535
+ topLeftCell,
1527
1536
  activePane,
1528
1537
  ...active,
1529
1538
  ...style
@@ -1665,6 +1674,10 @@ function styleOut(s, numFmt) {
1665
1674
  function readBytes(bytes) {
1666
1675
  return readWorkbook(bytes);
1667
1676
  }
1677
+ /** `{ sheetId }` for a worksheet id that Office accepts (1 to 65534, W2-20), otherwise nothing (D-F03). */
1678
+ function sheetIdOf(id) {
1679
+ return Number.isInteger(id) && id >= 1 && id <= 65534 ? { sheetId: id } : {};
1680
+ }
1668
1681
  // src/sheet-rules.ts
1669
1682
  const MAX_ROW = 1048576;
1670
1683
  const MAX_COL = 16384;
@@ -5844,7 +5857,10 @@ var Workbook = class {
5844
5857
  }
5845
5858
  /** Adds a worksheet. Names follow exceljs rules (31 characters at most; longer names are cut). */
5846
5859
  addWorksheet(name, options) {
5847
- const id = this.nextId;
5860
+ return this._addWorksheetWithId(this.nextId, name, options);
5861
+ }
5862
+ /** @internal Adds a worksheet with a given id (a loaded file's sheetId, a model's id; D-F03). */
5863
+ _addWorksheetWithId(id, name, options) {
5848
5864
  const sheet = new Worksheet(this, id, name, options);
5849
5865
  sheet.orderNo = this._sheets.reduce((acc, ws) => ws?.orderNo !== void 0 && ws.orderNo > acc ? ws.orderNo : acc, 0) + 1;
5850
5866
  this._sheets[id] = sheet;
@@ -5916,8 +5932,10 @@ var Workbook = class {
5916
5932
  this.views = Array.isArray(value["views"]) ? [...value["views"]] : [];
5917
5933
  this._media = Array.isArray(value["media"]) ? [...value["media"]] : [];
5918
5934
  for (const ws of this.worksheets) this.removeWorksheet(ws.id);
5919
- for (const m of Array.isArray(value["worksheets"]) ? value["worksheets"] : []) {
5920
- const ws = this.addWorksheet(String(m["name"] ?? ""), { state: m["state"] ?? "visible" });
5935
+ const models = Array.isArray(value["worksheets"]) ? value["worksheets"] : [];
5936
+ const ids = freeIds(models.map((m) => m["id"]));
5937
+ for (const [i, m] of models.entries()) {
5938
+ const ws = this._addWorksheetWithId(ids[i] ?? this.nextId, String(m["name"] ?? ""), { state: m["state"] ?? "visible" });
5921
5939
  ws.model = m;
5922
5940
  }
5923
5941
  this._definedNames.model = Array.isArray(value["definedNames"]) ? value["definedNames"] : [];
@@ -6030,9 +6048,10 @@ var Workbook = class {
6030
6048
  name: d.name,
6031
6049
  ranges: extractRanges(d.refersTo)
6032
6050
  }));
6051
+ const ids = freeIds(read.sheets.map((s) => s.kind === "worksheet" ? s.sheetId : void 0));
6033
6052
  for (const [position, s] of read.sheets.entries()) {
6034
6053
  if (s.kind !== "worksheet") continue;
6035
- const ws = this.addWorksheet(s.name, { state: s.state });
6054
+ const ws = this._addWorksheetWithId(ids[position] ?? this.nextId, s.name, { state: s.state });
6036
6055
  ws.orderNo = position;
6037
6056
  for (const c of use("cols") ? s.columns : []) {
6038
6057
  const last = Math.min(c.max, 16384);
@@ -6123,6 +6142,30 @@ var Workbook = class {
6123
6142
  }
6124
6143
  }
6125
6144
  };
6145
+ /**
6146
+ * Worksheet ids for a list of wanted ids: each whole number from 1 to 65534 (the sheet ids Office
6147
+ * uses, W2-20) that is not wanted by an earlier entry is kept; other entries (missing, invalid,
6148
+ * larger or repeated ids) get the lowest ids no entry wants, in order. The limit keeps the
6149
+ * `_sheets` array small: an id from an untrusted file such as 4294967295 is not an array index and
6150
+ * 20000000 makes every scan of the array slow (found by review, D-F03). `undefined` entries
6151
+ * (sheets that are not worksheets) get no id.
6152
+ */
6153
+ function freeIds(wanted) {
6154
+ const taken = /* @__PURE__ */ new Set();
6155
+ const kept = wanted.map((w) => {
6156
+ if (typeof w !== "number" || !Number.isInteger(w) || w < 1 || w > 65534 || taken.has(w)) return null;
6157
+ taken.add(w);
6158
+ return w;
6159
+ });
6160
+ let next = 1;
6161
+ return kept.map((k, i) => {
6162
+ if (wanted[i] === void 0) return void 0;
6163
+ if (k !== null) return k;
6164
+ while (taken.has(next)) next++;
6165
+ taken.add(next);
6166
+ return next;
6167
+ });
6168
+ }
6126
6169
  // src/stream.ts
6127
6170
  /**
6128
6171
  * A cell value for the streaming writer. Rows are written as they are committed, before later
@@ -6284,6 +6327,7 @@ var WorksheetWriter = class extends Worksheet {
6284
6327
  return {
6285
6328
  name: this.name,
6286
6329
  state: this.state,
6330
+ ...sheetIdOf(this.id),
6287
6331
  columns: trimTrailing(this.columns.map((c) => columnIn(c, this.writer._useStyles))),
6288
6332
  ...layout.freeze ? { freeze: layout.freeze } : {},
6289
6333
  ...Object.keys(layout.view).length > 0 ? { view: layout.view } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xlvane/compat",
3
- "version": "1.0.1",
3
+ "version": "1.1.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.0.1"
75
+ "xlvane": "^1.1.0"
76
76
  },
77
77
  "publishConfig": {
78
78
  "access": "public",