@cj-tech-master/excelts 9.5.7 → 9.5.8-canary.20260528224225.faabb36

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.
@@ -51,7 +51,15 @@ const colCache = {
51
51
  let l3;
52
52
  let n = 1;
53
53
  if (level >= 4) {
54
- throw new ColumnOutOfBoundsError(level, "Excel supports columns from 1 to 16384");
54
+ // Defensive invariant: Excel's column space (XFD = 16,384) caps at
55
+ // three letters, so neither `l2n` nor `n2l` should ever ask for a
56
+ // higher level. Both callers validate before reaching here; if
57
+ // this branch fires it indicates a programming error in a future
58
+ // caller, not a user input problem — surface that clearly rather
59
+ // than reusing `ColumnOutOfBoundsError` (which would lie about
60
+ // the offending column, since `level` is a letter-count, not a
61
+ // column number).
62
+ throw new Error(`colCache._fill: invariant violated — level ${level} exceeds the 3-letter cap; callers must validate before invoking _fill`);
55
63
  }
56
64
  if (this._l2nFill < 1 && level >= 1) {
57
65
  while (n <= 26) {
@@ -92,6 +100,16 @@ const colCache = {
92
100
  },
93
101
  l2n(l) {
94
102
  if (!this._l2n[l]) {
103
+ // Excel's column space stops at XFD (16,384) — three letters is
104
+ // the maximum width any valid column letter can have. Reject
105
+ // longer inputs explicitly here, BEFORE handing the length to
106
+ // `_fill`, so the thrown error carries the actual offending
107
+ // letter (`AAAA`) rather than the level integer (`4`) — matching
108
+ // what the equivalent `n2l(n > 16384)` and `decodeAddress` paths
109
+ // already report.
110
+ if (l.length > 3) {
111
+ throw new ColumnOutOfBoundsError(l, "Excel supports columns from 1 to 16384");
112
+ }
95
113
  this._fill(l.length);
96
114
  }
97
115
  if (!this._l2n[l]) {
@@ -2289,6 +2289,16 @@ class Workbook {
2289
2289
  this._tableNames.clear();
2290
2290
  value.worksheets.forEach(worksheetModel => {
2291
2291
  const { id, name, state } = worksheetModel;
2292
+ // API invariant: `_worksheets` is keyed by a positive integer
2293
+ // sheet id. A worksheet model with a missing or non-integer id
2294
+ // would be stored under a string pseudo key like `"undefined"`
2295
+ // or `"NaN"`, making it unreachable via `getWorksheet(name)`
2296
+ // (issue #166). The xlsx reconciler enforces the same invariant
2297
+ // before reaching this point; programmatic callers assigning
2298
+ // `model` directly with a malformed payload land here instead.
2299
+ if (!Number.isInteger(id) || id <= 0) {
2300
+ return;
2301
+ }
2292
2302
  const orderNo = value.sheets && value.sheets.findIndex(ws => ws.id === id);
2293
2303
  const worksheet = (this._worksheets[id] = new Worksheet({
2294
2304
  id,
@@ -1,14 +1,24 @@
1
1
  import type { WorksheetState } from "../../../types.js";
2
2
  import { BaseXform } from "../base-xform.js";
3
3
  interface SheetModel {
4
- id: number;
4
+ id: number | undefined;
5
5
  name: string;
6
6
  state: WorksheetState;
7
- rId: string;
7
+ rId: string | undefined;
8
8
  }
9
9
  declare class WorksheetXform extends BaseXform {
10
+ relationshipsPrefixes: readonly string[];
10
11
  render(xmlStream: any, model: SheetModel): void;
11
12
  parseOpen(node: any): boolean;
13
+ /**
14
+ * Locate the relationship id on a `<sheet>` element. Tries every
15
+ * prefix the workbook root bound to the relationships namespace,
16
+ * then any prefix the `<sheet>` element itself rebinds locally.
17
+ * Returns `undefined` if no relationship id is present — callers
18
+ * (the workbook reconciler) will treat such a `<sheet>` as a
19
+ * half-broken declaration that can't be bound to a worksheet part.
20
+ */
21
+ private _extractRelId;
12
22
  parseText(): void;
13
23
  parseClose(): boolean;
14
24
  }
@@ -1,10 +1,34 @@
1
1
  import { BaseXform } from "../base-xform.js";
2
2
  const VALID_STATES = new Set(["visible", "hidden", "veryHidden"]);
3
+ const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
3
4
  function parseWorksheetState(raw) {
4
5
  const state = raw || "visible";
5
6
  return VALID_STATES.has(state) ? state : "visible";
6
7
  }
8
+ function parseSheetId(raw) {
9
+ if (raw === undefined) {
10
+ return undefined;
11
+ }
12
+ const id = parseInt(raw, 10);
13
+ // OOXML constrains `sheetId` to a positive integer. Anything that
14
+ // doesn't parse to one — empty string, alphabetic, zero, negative,
15
+ // overflowing — must not propagate as `NaN`/`0`/`-1` because those
16
+ // would later seed pseudo keys like `_worksheets["NaN"]` (the same
17
+ // family of bug as issue #166's `_worksheets["undefined"]`).
18
+ return Number.isInteger(id) && id > 0 ? id : undefined;
19
+ }
7
20
  class WorksheetXform extends BaseXform {
21
+ constructor() {
22
+ super(...arguments);
23
+ // All prefixes the workbook root binds to the OOXML relationships
24
+ // namespace. Conventionally a workbook declares `xmlns:r=…`, so the
25
+ // sheet uses `r:id="rId1"`. But the prefix is only a label and a
26
+ // workbook may legally bind any prefix (or several) to that
27
+ // namespace. `WorkbookXform` populates this list from the
28
+ // `<workbook>` root; `r` is the safe fallback when the workbook
29
+ // declares no relationships binding at all.
30
+ this.relationshipsPrefixes = ["r"];
31
+ }
8
32
  render(xmlStream, model) {
9
33
  xmlStream.leafNode("sheet", {
10
34
  name: model.name,
@@ -18,14 +42,44 @@ class WorksheetXform extends BaseXform {
18
42
  if (node.name === "sheet") {
19
43
  this.model = {
20
44
  name: node.attributes.name,
21
- id: parseInt(node.attributes.sheetId, 10),
45
+ id: parseSheetId(node.attributes.sheetId),
22
46
  state: parseWorksheetState(node.attributes.state),
23
- rId: node.attributes["r:id"]
47
+ rId: this._extractRelId(node)
24
48
  };
25
49
  return true;
26
50
  }
27
51
  return false;
28
52
  }
53
+ /**
54
+ * Locate the relationship id on a `<sheet>` element. Tries every
55
+ * prefix the workbook root bound to the relationships namespace,
56
+ * then any prefix the `<sheet>` element itself rebinds locally.
57
+ * Returns `undefined` if no relationship id is present — callers
58
+ * (the workbook reconciler) will treat such a `<sheet>` as a
59
+ * half-broken declaration that can't be bound to a worksheet part.
60
+ */
61
+ _extractRelId(node) {
62
+ const attrs = node.attributes ?? {};
63
+ for (const prefix of this.relationshipsPrefixes) {
64
+ const value = attrs[`${prefix}:id`];
65
+ if (value !== undefined) {
66
+ return value;
67
+ }
68
+ }
69
+ // Local-scope fallback: a `<sheet>` element occasionally redeclares
70
+ // the relationships namespace under a fresh prefix. Scan its own
71
+ // attributes for `xmlns:X="…/relationships"` and look up `X:id`.
72
+ for (const attrName of Object.keys(attrs)) {
73
+ if (attrName.startsWith("xmlns:") && attrs[attrName] === RELATIONSHIPS_NS) {
74
+ const localPrefix = attrName.slice("xmlns:".length);
75
+ const candidate = attrs[`${localPrefix}:id`];
76
+ if (candidate !== undefined) {
77
+ return candidate;
78
+ }
79
+ }
80
+ }
81
+ return undefined;
82
+ }
29
83
  parseText() { }
30
84
  parseClose() {
31
85
  return false;
@@ -5,10 +5,17 @@ declare class WorkbookXform extends BaseXform {
5
5
  map: {
6
6
  [key: string]: any;
7
7
  };
8
+ /**
9
+ * The `<sheet>` xform shared with the `sheets` ListXform. Held as a
10
+ * field so `parseOpen` can pass workbook-level state (the prefixes
11
+ * bound to the OOXML relationships namespace) into it.
12
+ */
13
+ private readonly _sheetXform;
8
14
  constructor();
9
15
  prepare(model: any): void;
10
16
  render(xmlStream: any, model: any): void;
11
17
  parseOpen(node: any): boolean;
18
+ private static _findRelationshipsPrefixes;
12
19
  parseText(text: string): void;
13
20
  parseClose(name: string): boolean;
14
21
  reconcile(model: any): void;