@cj-tech-master/excelts 9.5.7 → 9.5.8

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.
@@ -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;
@@ -1,4 +1,5 @@
1
1
  import { colCache } from "../../../utils/col-cache.js";
2
+ import { resolveRelTarget } from "../../../utils/ooxml-paths.js";
2
3
  import { BaseXform } from "../base-xform.js";
3
4
  import { DefinedNamesXform } from "./defined-name-xform.js";
4
5
  import { ExternalReferenceXform } from "./external-reference-xform.js";
@@ -14,6 +15,12 @@ import { StdDocAttributes } from "../../../../xml/writer.js";
14
15
  class WorkbookXform extends BaseXform {
15
16
  constructor() {
16
17
  super();
18
+ /**
19
+ * The `<sheet>` xform shared with the `sheets` ListXform. Held as a
20
+ * field so `parseOpen` can pass workbook-level state (the prefixes
21
+ * bound to the OOXML relationships namespace) into it.
22
+ */
23
+ this._sheetXform = new WorksheetXform();
17
24
  this.map = {
18
25
  fileVersion: WorkbookXform.STATIC_XFORMS.fileVersion,
19
26
  workbookPr: new WorkbookPropertiesXform(),
@@ -23,7 +30,7 @@ class WorkbookXform extends BaseXform {
23
30
  count: false,
24
31
  childXform: new WorkbookViewXform()
25
32
  }),
26
- sheets: new ListXform({ tag: "sheets", count: false, childXform: new WorksheetXform() }),
33
+ sheets: new ListXform({ tag: "sheets", count: false, childXform: this._sheetXform }),
27
34
  definedNames: new ListXform({
28
35
  tag: "definedNames",
29
36
  count: false,
@@ -171,6 +178,11 @@ class WorkbookXform extends BaseXform {
171
178
  }
172
179
  switch (node.name) {
173
180
  case "workbook":
181
+ // Capture every prefix the workbook root binds to the OOXML
182
+ // relationships namespace so nested `<sheet>` parsing can
183
+ // read the relationship id under any of them. Falls back to
184
+ // the conventional `r` if the workbook declares no binding.
185
+ this._sheetXform.relationshipsPrefixes = WorkbookXform._findRelationshipsPrefixes(node);
174
186
  return true;
175
187
  default:
176
188
  this.parser = this.map[node.name];
@@ -180,6 +192,17 @@ class WorkbookXform extends BaseXform {
180
192
  return true;
181
193
  }
182
194
  }
195
+ static _findRelationshipsPrefixes(node) {
196
+ const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
197
+ const attrs = node.attributes ?? {};
198
+ const prefixes = [];
199
+ for (const attrName of Object.keys(attrs)) {
200
+ if (attrName.startsWith("xmlns:") && attrs[attrName] === RELATIONSHIPS_NS) {
201
+ prefixes.push(attrName.slice("xmlns:".length));
202
+ }
203
+ }
204
+ return prefixes.length > 0 ? prefixes : ["r"];
205
+ }
183
206
  parseText(text) {
184
207
  if (this.parser) {
185
208
  this.parser.parseText(text);
@@ -247,7 +270,7 @@ class WorkbookXform extends BaseXform {
247
270
  sheetPosition += 1;
248
271
  return;
249
272
  }
250
- const target = `xl/${rel.Target.replace(/^(\s|\/xl\/)+/, "")}`;
273
+ const target = resolveRelTarget("xl", rel.Target);
251
274
  // Check if this is a chartsheet
252
275
  const chartsheetMatch = /xl\/chartsheets\/sheet(\d+)\.xml/.exec(target);
253
276
  if (chartsheetMatch) {
@@ -279,6 +302,29 @@ class WorkbookXform extends BaseXform {
279
302
  });
280
303
  // Store reconciled chartsheets on the model
281
304
  model.chartsheetsList = chartsheetsList;
305
+ // Drop unbound worksheet parts. The reader (xlsx.browser.ts)
306
+ // collects every `xl/worksheets/sheetN.xml` it sees in the zip,
307
+ // because zip entries arrive in arbitrary order relative to
308
+ // workbook.xml. The authoritative `<sheets>` list is only
309
+ // available here, post-parse — so this is the first point at
310
+ // which we can prune worksheet parts that no `<sheet>` element
311
+ // claims through a working rel binding.
312
+ //
313
+ // Without pruning, such worksheets propagate downstream with
314
+ // `id`/`name`/`state` all `undefined`, landing under the literal
315
+ // string key `"undefined"` in `Workbook._worksheets` and becoming
316
+ // unreachable via `getWorksheet(name)` (issue #166). OOXML treats
317
+ // the workbook's `<sheets>` element as the single source of truth
318
+ // for which parts belong to the workbook; we follow that contract
319
+ // strictly. Genuinely-cursed workbooks reach this branch only when
320
+ // their `<sheet>` declarations are themselves missing or broken;
321
+ // namespace-prefix and Target-path quirks are handled upstream
322
+ // (the relationships-prefix lookup in sheet-xform and
323
+ // resolveRelTarget above), so a normal Excel-authored file never
324
+ // loses sheets here.
325
+ if (Array.isArray(model.worksheets)) {
326
+ model.worksheets = model.worksheets.filter((ws) => ws && Number.isInteger(ws.id) && ws.id > 0);
327
+ }
282
328
  // reconcile print areas
283
329
  const definedNames = [];
284
330
  if (model.definedNames) {
@@ -2292,6 +2292,16 @@ class Workbook {
2292
2292
  this._tableNames.clear();
2293
2293
  value.worksheets.forEach(worksheetModel => {
2294
2294
  const { id, name, state } = worksheetModel;
2295
+ // API invariant: `_worksheets` is keyed by a positive integer
2296
+ // sheet id. A worksheet model with a missing or non-integer id
2297
+ // would be stored under a string pseudo key like `"undefined"`
2298
+ // or `"NaN"`, making it unreachable via `getWorksheet(name)`
2299
+ // (issue #166). The xlsx reconciler enforces the same invariant
2300
+ // before reaching this point; programmatic callers assigning
2301
+ // `model` directly with a malformed payload land here instead.
2302
+ if (!Number.isInteger(id) || id <= 0) {
2303
+ return;
2304
+ }
2295
2305
  const orderNo = value.sheets && value.sheets.findIndex(ws => ws.id === id);
2296
2306
  const worksheet = (this._worksheets[id] = new worksheet_1.Worksheet({
2297
2307
  id,
@@ -3,11 +3,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorksheetXform = void 0;
4
4
  const base_xform_1 = require("../base-xform.js");
5
5
  const VALID_STATES = new Set(["visible", "hidden", "veryHidden"]);
6
+ const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
6
7
  function parseWorksheetState(raw) {
7
8
  const state = raw || "visible";
8
9
  return VALID_STATES.has(state) ? state : "visible";
9
10
  }
11
+ function parseSheetId(raw) {
12
+ if (raw === undefined) {
13
+ return undefined;
14
+ }
15
+ const id = parseInt(raw, 10);
16
+ // OOXML constrains `sheetId` to a positive integer. Anything that
17
+ // doesn't parse to one — empty string, alphabetic, zero, negative,
18
+ // overflowing — must not propagate as `NaN`/`0`/`-1` because those
19
+ // would later seed pseudo keys like `_worksheets["NaN"]` (the same
20
+ // family of bug as issue #166's `_worksheets["undefined"]`).
21
+ return Number.isInteger(id) && id > 0 ? id : undefined;
22
+ }
10
23
  class WorksheetXform extends base_xform_1.BaseXform {
24
+ constructor() {
25
+ super(...arguments);
26
+ // All prefixes the workbook root binds to the OOXML relationships
27
+ // namespace. Conventionally a workbook declares `xmlns:r=…`, so the
28
+ // sheet uses `r:id="rId1"`. But the prefix is only a label and a
29
+ // workbook may legally bind any prefix (or several) to that
30
+ // namespace. `WorkbookXform` populates this list from the
31
+ // `<workbook>` root; `r` is the safe fallback when the workbook
32
+ // declares no relationships binding at all.
33
+ this.relationshipsPrefixes = ["r"];
34
+ }
11
35
  render(xmlStream, model) {
12
36
  xmlStream.leafNode("sheet", {
13
37
  name: model.name,
@@ -21,14 +45,44 @@ class WorksheetXform extends base_xform_1.BaseXform {
21
45
  if (node.name === "sheet") {
22
46
  this.model = {
23
47
  name: node.attributes.name,
24
- id: parseInt(node.attributes.sheetId, 10),
48
+ id: parseSheetId(node.attributes.sheetId),
25
49
  state: parseWorksheetState(node.attributes.state),
26
- rId: node.attributes["r:id"]
50
+ rId: this._extractRelId(node)
27
51
  };
28
52
  return true;
29
53
  }
30
54
  return false;
31
55
  }
56
+ /**
57
+ * Locate the relationship id on a `<sheet>` element. Tries every
58
+ * prefix the workbook root bound to the relationships namespace,
59
+ * then any prefix the `<sheet>` element itself rebinds locally.
60
+ * Returns `undefined` if no relationship id is present — callers
61
+ * (the workbook reconciler) will treat such a `<sheet>` as a
62
+ * half-broken declaration that can't be bound to a worksheet part.
63
+ */
64
+ _extractRelId(node) {
65
+ const attrs = node.attributes ?? {};
66
+ for (const prefix of this.relationshipsPrefixes) {
67
+ const value = attrs[`${prefix}:id`];
68
+ if (value !== undefined) {
69
+ return value;
70
+ }
71
+ }
72
+ // Local-scope fallback: a `<sheet>` element occasionally redeclares
73
+ // the relationships namespace under a fresh prefix. Scan its own
74
+ // attributes for `xmlns:X="…/relationships"` and look up `X:id`.
75
+ for (const attrName of Object.keys(attrs)) {
76
+ if (attrName.startsWith("xmlns:") && attrs[attrName] === RELATIONSHIPS_NS) {
77
+ const localPrefix = attrName.slice("xmlns:".length);
78
+ const candidate = attrs[`${localPrefix}:id`];
79
+ if (candidate !== undefined) {
80
+ return candidate;
81
+ }
82
+ }
83
+ }
84
+ return undefined;
85
+ }
32
86
  parseText() { }
33
87
  parseClose() {
34
88
  return false;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkbookXform = void 0;
4
4
  const col_cache_1 = require("../../../utils/col-cache.js");
5
+ const ooxml_paths_1 = require("../../../utils/ooxml-paths.js");
5
6
  const base_xform_1 = require("../base-xform.js");
6
7
  const defined_name_xform_1 = require("./defined-name-xform.js");
7
8
  const external_reference_xform_1 = require("./external-reference-xform.js");
@@ -17,6 +18,12 @@ const writer_1 = require("../../../../xml/writer.js");
17
18
  class WorkbookXform extends base_xform_1.BaseXform {
18
19
  constructor() {
19
20
  super();
21
+ /**
22
+ * The `<sheet>` xform shared with the `sheets` ListXform. Held as a
23
+ * field so `parseOpen` can pass workbook-level state (the prefixes
24
+ * bound to the OOXML relationships namespace) into it.
25
+ */
26
+ this._sheetXform = new sheet_xform_1.WorksheetXform();
20
27
  this.map = {
21
28
  fileVersion: WorkbookXform.STATIC_XFORMS.fileVersion,
22
29
  workbookPr: new workbook_properties_xform_1.WorkbookPropertiesXform(),
@@ -26,7 +33,7 @@ class WorkbookXform extends base_xform_1.BaseXform {
26
33
  count: false,
27
34
  childXform: new workbook_view_xform_1.WorkbookViewXform()
28
35
  }),
29
- sheets: new list_xform_1.ListXform({ tag: "sheets", count: false, childXform: new sheet_xform_1.WorksheetXform() }),
36
+ sheets: new list_xform_1.ListXform({ tag: "sheets", count: false, childXform: this._sheetXform }),
30
37
  definedNames: new list_xform_1.ListXform({
31
38
  tag: "definedNames",
32
39
  count: false,
@@ -174,6 +181,11 @@ class WorkbookXform extends base_xform_1.BaseXform {
174
181
  }
175
182
  switch (node.name) {
176
183
  case "workbook":
184
+ // Capture every prefix the workbook root binds to the OOXML
185
+ // relationships namespace so nested `<sheet>` parsing can
186
+ // read the relationship id under any of them. Falls back to
187
+ // the conventional `r` if the workbook declares no binding.
188
+ this._sheetXform.relationshipsPrefixes = WorkbookXform._findRelationshipsPrefixes(node);
177
189
  return true;
178
190
  default:
179
191
  this.parser = this.map[node.name];
@@ -183,6 +195,17 @@ class WorkbookXform extends base_xform_1.BaseXform {
183
195
  return true;
184
196
  }
185
197
  }
198
+ static _findRelationshipsPrefixes(node) {
199
+ const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
200
+ const attrs = node.attributes ?? {};
201
+ const prefixes = [];
202
+ for (const attrName of Object.keys(attrs)) {
203
+ if (attrName.startsWith("xmlns:") && attrs[attrName] === RELATIONSHIPS_NS) {
204
+ prefixes.push(attrName.slice("xmlns:".length));
205
+ }
206
+ }
207
+ return prefixes.length > 0 ? prefixes : ["r"];
208
+ }
186
209
  parseText(text) {
187
210
  if (this.parser) {
188
211
  this.parser.parseText(text);
@@ -250,7 +273,7 @@ class WorkbookXform extends base_xform_1.BaseXform {
250
273
  sheetPosition += 1;
251
274
  return;
252
275
  }
253
- const target = `xl/${rel.Target.replace(/^(\s|\/xl\/)+/, "")}`;
276
+ const target = (0, ooxml_paths_1.resolveRelTarget)("xl", rel.Target);
254
277
  // Check if this is a chartsheet
255
278
  const chartsheetMatch = /xl\/chartsheets\/sheet(\d+)\.xml/.exec(target);
256
279
  if (chartsheetMatch) {
@@ -282,6 +305,29 @@ class WorkbookXform extends base_xform_1.BaseXform {
282
305
  });
283
306
  // Store reconciled chartsheets on the model
284
307
  model.chartsheetsList = chartsheetsList;
308
+ // Drop unbound worksheet parts. The reader (xlsx.browser.ts)
309
+ // collects every `xl/worksheets/sheetN.xml` it sees in the zip,
310
+ // because zip entries arrive in arbitrary order relative to
311
+ // workbook.xml. The authoritative `<sheets>` list is only
312
+ // available here, post-parse — so this is the first point at
313
+ // which we can prune worksheet parts that no `<sheet>` element
314
+ // claims through a working rel binding.
315
+ //
316
+ // Without pruning, such worksheets propagate downstream with
317
+ // `id`/`name`/`state` all `undefined`, landing under the literal
318
+ // string key `"undefined"` in `Workbook._worksheets` and becoming
319
+ // unreachable via `getWorksheet(name)` (issue #166). OOXML treats
320
+ // the workbook's `<sheets>` element as the single source of truth
321
+ // for which parts belong to the workbook; we follow that contract
322
+ // strictly. Genuinely-cursed workbooks reach this branch only when
323
+ // their `<sheet>` declarations are themselves missing or broken;
324
+ // namespace-prefix and Target-path quirks are handled upstream
325
+ // (the relationships-prefix lookup in sheet-xform and
326
+ // resolveRelTarget above), so a normal Excel-authored file never
327
+ // loses sheets here.
328
+ if (Array.isArray(model.worksheets)) {
329
+ model.worksheets = model.worksheets.filter((ws) => ws && Number.isInteger(ws.id) && ws.id > 0);
330
+ }
285
331
  // reconcile print areas
286
332
  const definedNames = [];
287
333
  if (model.definedNames) {
@@ -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,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;
@@ -1,4 +1,5 @@
1
1
  import { colCache } from "../../../utils/col-cache.js";
2
+ import { resolveRelTarget } from "../../../utils/ooxml-paths.js";
2
3
  import { BaseXform } from "../base-xform.js";
3
4
  import { DefinedNamesXform } from "./defined-name-xform.js";
4
5
  import { ExternalReferenceXform } from "./external-reference-xform.js";
@@ -14,6 +15,12 @@ import { StdDocAttributes } from "../../../../xml/writer.js";
14
15
  class WorkbookXform extends BaseXform {
15
16
  constructor() {
16
17
  super();
18
+ /**
19
+ * The `<sheet>` xform shared with the `sheets` ListXform. Held as a
20
+ * field so `parseOpen` can pass workbook-level state (the prefixes
21
+ * bound to the OOXML relationships namespace) into it.
22
+ */
23
+ this._sheetXform = new WorksheetXform();
17
24
  this.map = {
18
25
  fileVersion: WorkbookXform.STATIC_XFORMS.fileVersion,
19
26
  workbookPr: new WorkbookPropertiesXform(),
@@ -23,7 +30,7 @@ class WorkbookXform extends BaseXform {
23
30
  count: false,
24
31
  childXform: new WorkbookViewXform()
25
32
  }),
26
- sheets: new ListXform({ tag: "sheets", count: false, childXform: new WorksheetXform() }),
33
+ sheets: new ListXform({ tag: "sheets", count: false, childXform: this._sheetXform }),
27
34
  definedNames: new ListXform({
28
35
  tag: "definedNames",
29
36
  count: false,
@@ -171,6 +178,11 @@ class WorkbookXform extends BaseXform {
171
178
  }
172
179
  switch (node.name) {
173
180
  case "workbook":
181
+ // Capture every prefix the workbook root binds to the OOXML
182
+ // relationships namespace so nested `<sheet>` parsing can
183
+ // read the relationship id under any of them. Falls back to
184
+ // the conventional `r` if the workbook declares no binding.
185
+ this._sheetXform.relationshipsPrefixes = WorkbookXform._findRelationshipsPrefixes(node);
174
186
  return true;
175
187
  default:
176
188
  this.parser = this.map[node.name];
@@ -180,6 +192,17 @@ class WorkbookXform extends BaseXform {
180
192
  return true;
181
193
  }
182
194
  }
195
+ static _findRelationshipsPrefixes(node) {
196
+ const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
197
+ const attrs = node.attributes ?? {};
198
+ const prefixes = [];
199
+ for (const attrName of Object.keys(attrs)) {
200
+ if (attrName.startsWith("xmlns:") && attrs[attrName] === RELATIONSHIPS_NS) {
201
+ prefixes.push(attrName.slice("xmlns:".length));
202
+ }
203
+ }
204
+ return prefixes.length > 0 ? prefixes : ["r"];
205
+ }
183
206
  parseText(text) {
184
207
  if (this.parser) {
185
208
  this.parser.parseText(text);
@@ -247,7 +270,7 @@ class WorkbookXform extends BaseXform {
247
270
  sheetPosition += 1;
248
271
  return;
249
272
  }
250
- const target = `xl/${rel.Target.replace(/^(\s|\/xl\/)+/, "")}`;
273
+ const target = resolveRelTarget("xl", rel.Target);
251
274
  // Check if this is a chartsheet
252
275
  const chartsheetMatch = /xl\/chartsheets\/sheet(\d+)\.xml/.exec(target);
253
276
  if (chartsheetMatch) {
@@ -279,6 +302,29 @@ class WorkbookXform extends BaseXform {
279
302
  });
280
303
  // Store reconciled chartsheets on the model
281
304
  model.chartsheetsList = chartsheetsList;
305
+ // Drop unbound worksheet parts. The reader (xlsx.browser.ts)
306
+ // collects every `xl/worksheets/sheetN.xml` it sees in the zip,
307
+ // because zip entries arrive in arbitrary order relative to
308
+ // workbook.xml. The authoritative `<sheets>` list is only
309
+ // available here, post-parse — so this is the first point at
310
+ // which we can prune worksheet parts that no `<sheet>` element
311
+ // claims through a working rel binding.
312
+ //
313
+ // Without pruning, such worksheets propagate downstream with
314
+ // `id`/`name`/`state` all `undefined`, landing under the literal
315
+ // string key `"undefined"` in `Workbook._worksheets` and becoming
316
+ // unreachable via `getWorksheet(name)` (issue #166). OOXML treats
317
+ // the workbook's `<sheets>` element as the single source of truth
318
+ // for which parts belong to the workbook; we follow that contract
319
+ // strictly. Genuinely-cursed workbooks reach this branch only when
320
+ // their `<sheet>` declarations are themselves missing or broken;
321
+ // namespace-prefix and Target-path quirks are handled upstream
322
+ // (the relationships-prefix lookup in sheet-xform and
323
+ // resolveRelTarget above), so a normal Excel-authored file never
324
+ // loses sheets here.
325
+ if (Array.isArray(model.worksheets)) {
326
+ model.worksheets = model.worksheets.filter((ws) => ws && Number.isInteger(ws.id) && ws.id > 0);
327
+ }
282
328
  // reconcile print areas
283
329
  const definedNames = [];
284
330
  if (model.definedNames) {