@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.
- package/dist/browser/modules/excel/workbook.browser.js +10 -0
- package/dist/browser/modules/excel/xlsx/xform/book/sheet-xform.d.ts +12 -2
- package/dist/browser/modules/excel/xlsx/xform/book/sheet-xform.js +56 -2
- package/dist/browser/modules/excel/xlsx/xform/book/workbook-xform.d.ts +7 -0
- package/dist/browser/modules/excel/xlsx/xform/book/workbook-xform.js +48 -2
- package/dist/cjs/modules/excel/workbook.browser.js +10 -0
- package/dist/cjs/modules/excel/xlsx/xform/book/sheet-xform.js +56 -2
- package/dist/cjs/modules/excel/xlsx/xform/book/workbook-xform.js +48 -2
- package/dist/esm/modules/excel/workbook.browser.js +10 -0
- package/dist/esm/modules/excel/xlsx/xform/book/sheet-xform.js +56 -2
- package/dist/esm/modules/excel/xlsx/xform/book/workbook-xform.js +48 -2
- package/dist/iife/excelts.iife.js +88 -35
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +37 -37
- package/dist/types/modules/excel/xlsx/xform/book/sheet-xform.d.ts +12 -2
- package/dist/types/modules/excel/xlsx/xform/book/workbook-xform.d.ts +7 -0
- package/package.json +12 -12
|
@@ -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:
|
|
45
|
+
id: parseSheetId(node.attributes.sheetId),
|
|
22
46
|
state: parseWorksheetState(node.attributes.state),
|
|
23
|
-
rId: node
|
|
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:
|
|
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 =
|
|
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:
|
|
48
|
+
id: parseSheetId(node.attributes.sheetId),
|
|
25
49
|
state: parseWorksheetState(node.attributes.state),
|
|
26
|
-
rId: node
|
|
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:
|
|
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 =
|
|
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:
|
|
45
|
+
id: parseSheetId(node.attributes.sheetId),
|
|
22
46
|
state: parseWorksheetState(node.attributes.state),
|
|
23
|
-
rId: node
|
|
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:
|
|
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 =
|
|
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) {
|