@cj-tech-master/excelts 9.4.0 → 9.4.1
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/index.browser.d.ts +1 -0
- package/dist/browser/index.browser.js +1 -0
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +1 -0
- package/dist/browser/modules/excel/cell.d.ts +1 -0
- package/dist/browser/modules/excel/note.d.ts +3 -23
- package/dist/browser/modules/excel/note.js +8 -2
- package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +17 -5
- package/dist/browser/modules/excel/utils/ooxml-paths.js +46 -19
- package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
- package/dist/browser/modules/excel/xlsx/xform/comment/comments-xform.d.ts +8 -0
- package/dist/browser/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
- package/dist/browser/modules/excel/xlsx/xform/comment/vml-client-data-xform.d.ts +6 -0
- package/dist/browser/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
- package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
- package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +54 -14
- package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +3 -3
- package/dist/browser/modules/excel/xlsx/xlsx.browser.js +16 -15
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/modules/excel/note.js +8 -2
- package/dist/cjs/modules/excel/utils/ooxml-paths.js +49 -24
- package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
- package/dist/cjs/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
- package/dist/cjs/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
- package/dist/cjs/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
- package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +53 -13
- package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +15 -14
- package/dist/esm/index.browser.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/modules/excel/note.js +8 -2
- package/dist/esm/modules/excel/utils/ooxml-paths.js +46 -19
- package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
- package/dist/esm/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
- package/dist/esm/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
- package/dist/esm/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
- package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +54 -14
- package/dist/esm/modules/excel/xlsx/xlsx.browser.js +16 -15
- package/dist/iife/excelts.iife.js +149 -56
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +30 -30
- package/dist/types/index.browser.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/modules/excel/cell.d.ts +1 -0
- package/dist/types/modules/excel/note.d.ts +3 -23
- package/dist/types/modules/excel/utils/ooxml-paths.d.ts +17 -5
- package/dist/types/modules/excel/xlsx/xform/comment/comments-xform.d.ts +8 -0
- package/dist/types/modules/excel/xlsx/xform/comment/vml-client-data-xform.d.ts +6 -0
- package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +3 -3
- package/package.json +4 -4
package/dist/esm/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { Range } from "./modules/excel/range.js";
|
|
|
10
10
|
export { Image } from "./modules/excel/image.js";
|
|
11
11
|
export * from "./modules/excel/anchor.js";
|
|
12
12
|
export { Table } from "./modules/excel/table.js";
|
|
13
|
+
export { Note } from "./modules/excel/note.js";
|
|
13
14
|
export { DataValidations } from "./modules/excel/data-validations.js";
|
|
14
15
|
export { FormCheckbox } from "./modules/excel/form-control.js";
|
|
15
16
|
// Note: the formula engine lives at the `./formula` subpath so it stays
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { deepMerge } from "./utils/under-dash.js";
|
|
2
2
|
class Note {
|
|
3
|
-
constructor(note) {
|
|
3
|
+
constructor(note, author) {
|
|
4
4
|
this.note = note;
|
|
5
|
+
this.author = author;
|
|
5
6
|
}
|
|
6
7
|
get model() {
|
|
7
8
|
let value;
|
|
@@ -26,7 +27,11 @@ class Note {
|
|
|
26
27
|
break;
|
|
27
28
|
}
|
|
28
29
|
// Suitable for all cell comments
|
|
29
|
-
|
|
30
|
+
const result = deepMerge({}, Note.DEFAULT_CONFIGS, value);
|
|
31
|
+
if (this.author !== undefined) {
|
|
32
|
+
result.author = this.author;
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
30
35
|
}
|
|
31
36
|
set model(value) {
|
|
32
37
|
const { note } = value;
|
|
@@ -37,6 +42,7 @@ class Note {
|
|
|
37
42
|
else {
|
|
38
43
|
this.note = note;
|
|
39
44
|
}
|
|
45
|
+
this.author = value.author;
|
|
40
46
|
}
|
|
41
47
|
static fromModel(model) {
|
|
42
48
|
const note = new Note();
|
|
@@ -19,7 +19,9 @@ const drawingXmlRegex = /^xl\/drawings\/(drawing\d+)[.]xml$/;
|
|
|
19
19
|
const drawingRelsXmlRegex = /^xl\/drawings\/_rels\/(drawing\d+)[.]xml[.]rels$/;
|
|
20
20
|
const vmlDrawingRegex = /^xl\/drawings\/(vmlDrawing\d+)[.]vml$/;
|
|
21
21
|
const vmlDrawingHFRegex = /^xl\/drawings\/(vmlDrawingHF\d+)[.]vml$/;
|
|
22
|
-
|
|
22
|
+
// Matches both flat layout (xl/comments1.xml) and subdirectory layout (xl/comments/comment1.xml).
|
|
23
|
+
// Both are valid OOXML — the actual path is determined by .rels, not by convention.
|
|
24
|
+
const commentsXmlRegex = /^xl\/(?:comments(\d+)|comments\/comment(\d+))[.]xml$/;
|
|
23
25
|
const tableXmlRegex = /^xl\/tables\/(table\d+)[.]xml$/;
|
|
24
26
|
const pivotTableXmlRegex = /^xl\/pivotTables\/(pivotTable\d+)[.]xml$/;
|
|
25
27
|
const pivotTableRelsXmlRegex = /^xl\/pivotTables\/_rels\/(pivotTable\d+)[.]xml[.]rels$/;
|
|
@@ -81,9 +83,12 @@ export function getVmlDrawingHFNameFromPath(path) {
|
|
|
81
83
|
const match = vmlDrawingHFRegex.exec(path);
|
|
82
84
|
return match ? match[1] : undefined;
|
|
83
85
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Check if a zip entry path is a comments XML file.
|
|
88
|
+
* Works for both `xl/comments1.xml` and `xl/comments/comment1.xml`.
|
|
89
|
+
*/
|
|
90
|
+
export function isCommentsPath(path) {
|
|
91
|
+
return commentsXmlRegex.test(path);
|
|
87
92
|
}
|
|
88
93
|
export function getTableNameFromPath(path) {
|
|
89
94
|
const match = tableXmlRegex.exec(path);
|
|
@@ -184,6 +189,9 @@ export function pivotCacheRecordsRelTarget(n) {
|
|
|
184
189
|
export function pivotTablePath(n) {
|
|
185
190
|
return `xl/pivotTables/pivotTable${n}.xml`;
|
|
186
191
|
}
|
|
192
|
+
export function pivotTablePathFromName(name) {
|
|
193
|
+
return `xl/pivotTables/${name}.xml`;
|
|
194
|
+
}
|
|
187
195
|
export function pivotTableRelsPath(n) {
|
|
188
196
|
return `xl/pivotTables/_rels/pivotTable${n}.xml.rels`;
|
|
189
197
|
}
|
|
@@ -232,29 +240,14 @@ export function drawingRelTargetFromWorksheet(drawingName) {
|
|
|
232
240
|
// Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
|
|
233
241
|
return `../drawings/${drawingName}.xml`;
|
|
234
242
|
}
|
|
235
|
-
export function vmlDrawingRelTargetFromWorksheetName(vmlName) {
|
|
236
|
-
// For VML drawings when the caller already has the logical name (e.g. "vmlDrawing1").
|
|
237
|
-
return `../drawings/${vmlName}.vml`;
|
|
238
|
-
}
|
|
239
|
-
export function commentsRelTargetFromWorksheetName(commentName) {
|
|
240
|
-
// For comments when the caller already has the logical name (e.g. "comments1").
|
|
241
|
-
return `../${commentName}.xml`;
|
|
242
|
-
}
|
|
243
243
|
export function pivotTableRelTargetFromWorksheet(n) {
|
|
244
244
|
// Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
|
|
245
245
|
return `../pivotTables/pivotTable${n}.xml`;
|
|
246
246
|
}
|
|
247
|
-
export function pivotTableRelTargetFromWorksheetName(pivotName) {
|
|
248
|
-
// For pivot tables when the caller already has the logical name (e.g. "pivotTable1").
|
|
249
|
-
return `../pivotTables/${pivotName}.xml`;
|
|
250
|
-
}
|
|
251
247
|
export function tableRelTargetFromWorksheet(target) {
|
|
252
248
|
// Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
|
|
253
249
|
return `../tables/${target}`;
|
|
254
250
|
}
|
|
255
|
-
export function tableRelTargetFromWorksheetName(name) {
|
|
256
|
-
return `../tables/${name}.xml`;
|
|
257
|
-
}
|
|
258
251
|
export function mediaRelTargetFromRels(filename) {
|
|
259
252
|
// Target from a rels file located under xl/*/_rels (base is one level deeper than xl/)
|
|
260
253
|
return `../media/${filename}`;
|
|
@@ -267,3 +260,37 @@ export function ctrlPropRelTargetFromWorksheet(id) {
|
|
|
267
260
|
// Target inside xl/worksheets/_rels/sheetN.xml.rels (base: xl/worksheets/)
|
|
268
261
|
return `../ctrlProps/ctrlProp${id}.xml`;
|
|
269
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Resolve a relationship Target (relative or absolute) to a normalized zip path.
|
|
265
|
+
*
|
|
266
|
+
* OOXML relationship targets may be:
|
|
267
|
+
* - Relative: `../comments1.xml` (resolved against `baseDir`)
|
|
268
|
+
* - Absolute: `/xl/comments/comment1.xml` (leading slash stripped)
|
|
269
|
+
*
|
|
270
|
+
* @param baseDir The directory containing the source part (e.g. `xl/worksheets/`)
|
|
271
|
+
* @param target The raw Target value from the .rels file
|
|
272
|
+
*/
|
|
273
|
+
export function resolveRelTarget(baseDir, target) {
|
|
274
|
+
// Absolute target — strip leading slash to get the zip path.
|
|
275
|
+
if (target.startsWith("/")) {
|
|
276
|
+
return target.slice(1);
|
|
277
|
+
}
|
|
278
|
+
// Ensure baseDir ends with "/" so the join works correctly.
|
|
279
|
+
const base = baseDir.endsWith("/") ? baseDir : baseDir + "/";
|
|
280
|
+
// Relative target — resolve against baseDir.
|
|
281
|
+
// Simple resolution: join base + target, then resolve `.` and `..` segments.
|
|
282
|
+
const parts = (base + target).split("/");
|
|
283
|
+
const resolved = [];
|
|
284
|
+
for (const part of parts) {
|
|
285
|
+
if (part === "." || part === "") {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
else if (part === "..") {
|
|
289
|
+
resolved.pop();
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
resolved.push(part);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return resolved.join("/");
|
|
296
|
+
}
|
|
@@ -18,7 +18,7 @@ class CommentXform extends BaseXform {
|
|
|
18
18
|
const renderModel = model || this.model;
|
|
19
19
|
xmlStream.openNode("comment", {
|
|
20
20
|
ref: renderModel.ref,
|
|
21
|
-
authorId: 0
|
|
21
|
+
authorId: renderModel.authorId ?? 0
|
|
22
22
|
});
|
|
23
23
|
xmlStream.openNode("text");
|
|
24
24
|
if (renderModel && renderModel.note && renderModel.note.texts) {
|
|
@@ -41,7 +41,8 @@ class CommentXform extends BaseXform {
|
|
|
41
41
|
note: {
|
|
42
42
|
texts: []
|
|
43
43
|
},
|
|
44
|
-
|
|
44
|
+
ref: node.attributes.ref,
|
|
45
|
+
authorId: node.attributes.authorId != null ? Number(node.attributes.authorId) : undefined
|
|
45
46
|
};
|
|
46
47
|
return true;
|
|
47
48
|
case "r":
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { BaseXform } from "../base-xform.js";
|
|
2
2
|
import { CommentXform } from "./comment-xform.js";
|
|
3
3
|
import { StdDocAttributes } from "../../../../xml/writer.js";
|
|
4
|
+
const DEFAULT_AUTHOR = "Author";
|
|
4
5
|
class CommentsXform extends BaseXform {
|
|
5
6
|
constructor() {
|
|
6
7
|
super();
|
|
8
|
+
/** Authors collected while parsing the <authors> element. */
|
|
9
|
+
this._authors = [];
|
|
10
|
+
/** Whether we are currently inside the <authors> element. */
|
|
11
|
+
this._inAuthors = false;
|
|
12
|
+
/** Whether we are currently inside an <author> element (collecting text). */
|
|
13
|
+
this._inAuthor = false;
|
|
14
|
+
/** Accumulator for the current <author> text content. */
|
|
15
|
+
this._currentAuthor = "";
|
|
7
16
|
this.map = {
|
|
8
17
|
comment: new CommentXform()
|
|
9
18
|
};
|
|
@@ -13,15 +22,23 @@ class CommentsXform extends BaseXform {
|
|
|
13
22
|
const renderModel = model || this.model;
|
|
14
23
|
xmlStream.openXml(StdDocAttributes);
|
|
15
24
|
xmlStream.openNode("comments", CommentsXform.COMMENTS_ATTRIBUTES);
|
|
16
|
-
// authors
|
|
17
|
-
|
|
25
|
+
// Collect unique authors from comments
|
|
26
|
+
const authorSet = new Set();
|
|
27
|
+
for (const comment of renderModel.comments) {
|
|
28
|
+
authorSet.add(comment.author ?? DEFAULT_AUTHOR);
|
|
29
|
+
}
|
|
30
|
+
const authors = [...authorSet];
|
|
18
31
|
xmlStream.openNode("authors");
|
|
19
|
-
|
|
32
|
+
for (const author of authors) {
|
|
33
|
+
xmlStream.leafNode("author", null, author);
|
|
34
|
+
}
|
|
20
35
|
xmlStream.closeNode();
|
|
21
36
|
// comments
|
|
22
37
|
xmlStream.openNode("commentList");
|
|
23
38
|
renderModel.comments.forEach(comment => {
|
|
24
|
-
|
|
39
|
+
// Set the authorId based on the authors list for rendering
|
|
40
|
+
const authorId = authors.indexOf(comment.author ?? DEFAULT_AUTHOR);
|
|
41
|
+
this.map.comment.render(xmlStream, { ...comment, authorId: authorId >= 0 ? authorId : 0 });
|
|
25
42
|
});
|
|
26
43
|
xmlStream.closeNode();
|
|
27
44
|
xmlStream.closeNode();
|
|
@@ -32,6 +49,16 @@ class CommentsXform extends BaseXform {
|
|
|
32
49
|
return true;
|
|
33
50
|
}
|
|
34
51
|
switch (node.name) {
|
|
52
|
+
case "authors":
|
|
53
|
+
this._inAuthors = true;
|
|
54
|
+
this._authors = [];
|
|
55
|
+
return true;
|
|
56
|
+
case "author":
|
|
57
|
+
if (this._inAuthors) {
|
|
58
|
+
this._inAuthor = true;
|
|
59
|
+
this._currentAuthor = "";
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
35
62
|
case "commentList":
|
|
36
63
|
this.model = {
|
|
37
64
|
comments: []
|
|
@@ -46,13 +73,33 @@ class CommentsXform extends BaseXform {
|
|
|
46
73
|
}
|
|
47
74
|
}
|
|
48
75
|
parseText(text) {
|
|
49
|
-
if (this.
|
|
76
|
+
if (this._inAuthor) {
|
|
77
|
+
this._currentAuthor += text;
|
|
78
|
+
}
|
|
79
|
+
else if (this.parser) {
|
|
50
80
|
this.parser.parseText(text);
|
|
51
81
|
}
|
|
52
82
|
}
|
|
53
83
|
parseClose(name) {
|
|
54
84
|
switch (name) {
|
|
85
|
+
case "authors":
|
|
86
|
+
this._inAuthors = false;
|
|
87
|
+
return true;
|
|
88
|
+
case "author":
|
|
89
|
+
if (this._inAuthors) {
|
|
90
|
+
this._authors.push(this._currentAuthor);
|
|
91
|
+
this._inAuthor = false;
|
|
92
|
+
this._currentAuthor = "";
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
55
95
|
case "commentList":
|
|
96
|
+
// Resolve authorId → author name on each comment
|
|
97
|
+
for (const comment of this.model.comments) {
|
|
98
|
+
const { authorId } = comment;
|
|
99
|
+
if (authorId != null && authorId >= 0 && authorId < this._authors.length) {
|
|
100
|
+
comment.author = this._authors[authorId];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
56
103
|
return false;
|
|
57
104
|
case "comment":
|
|
58
105
|
this.model.comments.push(this.parser.model);
|
|
@@ -6,6 +6,8 @@ const POSITION_TYPE = ["twoCells", "oneCells", "absolute"];
|
|
|
6
6
|
class VmlClientDataXform extends BaseXform {
|
|
7
7
|
constructor() {
|
|
8
8
|
super();
|
|
9
|
+
/** Accumulated text for the current leaf element. */
|
|
10
|
+
this._leafText = "";
|
|
9
11
|
this.map = {
|
|
10
12
|
"x:Anchor": new VmlAnchorXform(),
|
|
11
13
|
"x:Locked": new VmlProtectionXform({ tag: "x:Locked" }),
|
|
@@ -41,6 +43,11 @@ class VmlClientDataXform extends BaseXform {
|
|
|
41
43
|
editAs: ""
|
|
42
44
|
};
|
|
43
45
|
break;
|
|
46
|
+
case "x:Row":
|
|
47
|
+
case "x:Column":
|
|
48
|
+
this._leafName = node.name;
|
|
49
|
+
this._leafText = "";
|
|
50
|
+
break;
|
|
44
51
|
default:
|
|
45
52
|
this.parser = this.map[node.name];
|
|
46
53
|
if (this.parser) {
|
|
@@ -51,11 +58,30 @@ class VmlClientDataXform extends BaseXform {
|
|
|
51
58
|
return true;
|
|
52
59
|
}
|
|
53
60
|
parseText(text) {
|
|
54
|
-
if (this.
|
|
61
|
+
if (this._leafName) {
|
|
62
|
+
this._leafText += text;
|
|
63
|
+
}
|
|
64
|
+
else if (this.parser) {
|
|
55
65
|
this.parser.parseText(text);
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
parseClose(name) {
|
|
69
|
+
if (this._leafName) {
|
|
70
|
+
if (name === this._leafName) {
|
|
71
|
+
const value = parseInt(this._leafText, 10);
|
|
72
|
+
if (name === "x:Row") {
|
|
73
|
+
// VML uses 0-based row; convert to 1-based to match comment ref
|
|
74
|
+
this.model.row = value + 1;
|
|
75
|
+
}
|
|
76
|
+
else if (name === "x:Column") {
|
|
77
|
+
// VML uses 0-based col; convert to 1-based to match comment ref
|
|
78
|
+
this.model.col = value + 1;
|
|
79
|
+
}
|
|
80
|
+
this._leafName = undefined;
|
|
81
|
+
this._leafText = "";
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
59
85
|
if (this.parser) {
|
|
60
86
|
if (!this.parser.parseClose(name)) {
|
|
61
87
|
this.parser = undefined;
|
|
@@ -66,6 +66,10 @@ class VmlShapeXform extends BaseXform {
|
|
|
66
66
|
this.map["x:ClientData"].model && this.map["x:ClientData"].model.protection;
|
|
67
67
|
this.model.anchor = this.map["x:ClientData"].model && this.map["x:ClientData"].model.anchor;
|
|
68
68
|
this.model.editAs = this.map["x:ClientData"].model && this.map["x:ClientData"].model.editAs;
|
|
69
|
+
if (this.map["x:ClientData"].model) {
|
|
70
|
+
this.model.row = this.map["x:ClientData"].model.row;
|
|
71
|
+
this.model.col = this.map["x:ClientData"].model.col;
|
|
72
|
+
}
|
|
69
73
|
return false;
|
|
70
74
|
default:
|
|
71
75
|
return true;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { colCache } from "../../../utils/col-cache.js";
|
|
2
2
|
import { buildDrawingAnchorsAndRels, resolveMediaTarget } from "../../../utils/drawing-utils.js";
|
|
3
|
-
import { commentsRelTargetFromWorksheet, ctrlPropRelTargetFromWorksheet, drawingRelTargetFromWorksheet, pivotTableRelTargetFromWorksheet, tableRelTargetFromWorksheet, vmlDrawingRelTargetFromWorksheet, vmlDrawingHFRelTargetFromWorksheet } from "../../../utils/ooxml-paths.js";
|
|
3
|
+
import { commentsRelTargetFromWorksheet, ctrlPropRelTargetFromWorksheet, drawingRelTargetFromWorksheet, pivotTableRelTargetFromWorksheet, resolveRelTarget, tableRelTargetFromWorksheet, vmlDrawingRelTargetFromWorksheet, vmlDrawingHFRelTargetFromWorksheet } from "../../../utils/ooxml-paths.js";
|
|
4
4
|
import { RelType } from "../../rel-type.js";
|
|
5
5
|
import { BaseXform } from "../base-xform.js";
|
|
6
6
|
import { ListXform } from "../list-xform.js";
|
|
@@ -700,25 +700,63 @@ class WorkSheetXform extends BaseXform {
|
|
|
700
700
|
reconcile(model, options) {
|
|
701
701
|
// options.merges = new Merges();
|
|
702
702
|
// options.merges.reconcile(model.mergeCells, model.rows);
|
|
703
|
-
|
|
703
|
+
// Build rel index first, then process comments and VML in two passes so
|
|
704
|
+
// that the result is independent of the order rels appear in the file.
|
|
705
|
+
const relList = model.relationships ?? [];
|
|
706
|
+
const rels = relList.reduce((h, rel) => {
|
|
704
707
|
h[rel.Id] = rel;
|
|
708
|
+
return h;
|
|
709
|
+
}, {});
|
|
710
|
+
// Pass 1: resolve comments
|
|
711
|
+
for (const rel of relList) {
|
|
705
712
|
if (rel.Type === RelType.Comments) {
|
|
706
|
-
const
|
|
713
|
+
const resolvedPath = resolveRelTarget("xl/worksheets/", rel.Target);
|
|
714
|
+
const commentEntry = options.comments?.[resolvedPath];
|
|
707
715
|
if (commentEntry) {
|
|
708
716
|
model.comments = commentEntry.comments;
|
|
709
717
|
}
|
|
710
718
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
719
|
+
}
|
|
720
|
+
// Pass 2: merge VML note metadata (requires model.comments from pass 1)
|
|
721
|
+
if (model.comments && model.comments.length) {
|
|
722
|
+
for (const rel of relList) {
|
|
723
|
+
if (rel.Type === RelType.VmlDrawing) {
|
|
724
|
+
const resolvedVmlPath = resolveRelTarget("xl/worksheets/", rel.Target);
|
|
725
|
+
const vmlEntry = options.vmlDrawings?.[resolvedVmlPath];
|
|
726
|
+
if (vmlEntry) {
|
|
727
|
+
// Build a ref-keyed map from VML comments for order-independent merge.
|
|
728
|
+
// Fall back to index-based merge if VML entries lack row/col.
|
|
729
|
+
const vmlComments = vmlEntry.comments;
|
|
730
|
+
const vmlByRef = {};
|
|
731
|
+
let hasRefInfo = false;
|
|
732
|
+
for (const vc of vmlComments) {
|
|
733
|
+
if (vc.row != null && vc.col != null) {
|
|
734
|
+
const ref = colCache.encodeAddress(vc.row, vc.col);
|
|
735
|
+
vmlByRef[ref] = vc;
|
|
736
|
+
hasRefInfo = true;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (hasRefInfo) {
|
|
740
|
+
// Merge by cell reference (robust against order differences)
|
|
741
|
+
for (const comment of model.comments) {
|
|
742
|
+
const vml = vmlByRef[comment.ref];
|
|
743
|
+
if (vml) {
|
|
744
|
+
comment.note = Object.assign({}, comment.note, vml);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
// Fallback: index-based merge for VML files without row/col
|
|
750
|
+
model.comments.forEach((comment, index) => {
|
|
751
|
+
if (index < vmlComments.length) {
|
|
752
|
+
comment.note = Object.assign({}, comment.note, vmlComments[index]);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
718
757
|
}
|
|
719
758
|
}
|
|
720
|
-
|
|
721
|
-
}, {});
|
|
759
|
+
}
|
|
722
760
|
options.commentsMap = (model.comments ?? []).reduce((h, comment) => {
|
|
723
761
|
if (comment.ref) {
|
|
724
762
|
h[comment.ref] = comment;
|
|
@@ -830,7 +868,8 @@ class WorkSheetXform extends BaseXform {
|
|
|
830
868
|
model.tables = (model.tables ?? []).reduce((acc, tablePart) => {
|
|
831
869
|
const rel = rels[tablePart.rId];
|
|
832
870
|
if (rel) {
|
|
833
|
-
const
|
|
871
|
+
const resolvedPath = resolveRelTarget("xl/worksheets/", rel.Target);
|
|
872
|
+
const table = options.tables[resolvedPath];
|
|
834
873
|
if (table) {
|
|
835
874
|
acc.push(table);
|
|
836
875
|
}
|
|
@@ -842,7 +881,8 @@ class WorkSheetXform extends BaseXform {
|
|
|
842
881
|
model.pivotTables = [];
|
|
843
882
|
(model.relationships ?? []).forEach(rel => {
|
|
844
883
|
if (rel.Type === RelType.PivotTable && options.pivotTables) {
|
|
845
|
-
const
|
|
884
|
+
const resolvedPath = resolveRelTarget("xl/worksheets/", rel.Target);
|
|
885
|
+
const pivotTable = options.pivotTables[resolvedPath];
|
|
846
886
|
if (pivotTable) {
|
|
847
887
|
model.pivotTables.push(pivotTable);
|
|
848
888
|
}
|
|
@@ -13,7 +13,7 @@ import { StreamingZip, ZipDeflateFile } from "../../archive/zip/stream.js";
|
|
|
13
13
|
import { ExcelStreamStateError, ExcelFileError, ImageError, ExcelNotSupportedError, XmlParseError, TableError } from "../errors.js";
|
|
14
14
|
import { filterDrawingAnchors } from "../utils/drawing-utils.js";
|
|
15
15
|
import { rewriteExternalRefs } from "../utils/external-link-formula.js";
|
|
16
|
-
import { commentsPath,
|
|
16
|
+
import { commentsPath, ctrlPropPath, drawingPath, drawingRelsPath, externalLinkPath, externalLinkRelsPath, externalLinkRelTargetFromWorkbook, OOXML_REL_TARGETS, pivotCacheDefinitionRelTargetFromWorkbook, pivotTablePathFromName, isCommentsPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getExternalLinkIndexFromPath, getExternalLinkIndexFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getVmlDrawingHFNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingPath, vmlDrawingHFPath, vmlDrawingHFRelsPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
|
|
17
17
|
import { PassthroughManager } from "../utils/passthrough-manager.js";
|
|
18
18
|
import { StreamBuf } from "../utils/stream-buf.js";
|
|
19
19
|
import { RelType } from "./rel-type.js";
|
|
@@ -1007,9 +1007,8 @@ class XLSX {
|
|
|
1007
1007
|
applyWidthHeightFormats: pt.applyWidthHeightFormats === "1" ? "1" : "0"
|
|
1008
1008
|
};
|
|
1009
1009
|
loadedPivotTables.push(completePivotTable);
|
|
1010
|
-
// Key
|
|
1011
|
-
|
|
1012
|
-
pivotTablesIndexed[pivotTableRelTargetFromWorksheetName(pivotName)] = completePivotTable;
|
|
1010
|
+
// Key by absolute zip path so reconcile can match any rel target layout.
|
|
1011
|
+
pivotTablesIndexed[pivotTablePathFromName(pivotName)] = completePivotTable;
|
|
1013
1012
|
});
|
|
1014
1013
|
loadedPivotTables.sort((a, b) => a.tableNumber - b.tableNumber);
|
|
1015
1014
|
model.pivotTables = loadedPivotTables;
|
|
@@ -1069,15 +1068,17 @@ class XLSX {
|
|
|
1069
1068
|
model.worksheetHash[path] = worksheet;
|
|
1070
1069
|
model.worksheets.push(worksheet);
|
|
1071
1070
|
}
|
|
1072
|
-
async _processCommentEntry(stream, model,
|
|
1071
|
+
async _processCommentEntry(stream, model, zipPath) {
|
|
1073
1072
|
const xform = new CommentsXform();
|
|
1074
1073
|
const comments = await xform.parseStream(stream);
|
|
1075
|
-
|
|
1074
|
+
// Key by absolute zip path so reconcile can match any rel target layout.
|
|
1075
|
+
model.comments[zipPath] = comments;
|
|
1076
1076
|
}
|
|
1077
|
-
async _processTableEntry(stream, model,
|
|
1077
|
+
async _processTableEntry(stream, model, zipPath) {
|
|
1078
1078
|
const xform = new TableXform();
|
|
1079
1079
|
const table = await xform.parseStream(stream);
|
|
1080
|
-
|
|
1080
|
+
// Key by absolute zip path so reconcile can match any rel target layout.
|
|
1081
|
+
model.tables[zipPath] = table;
|
|
1081
1082
|
}
|
|
1082
1083
|
async _processWorksheetRelsEntry(stream, model, sheetNo) {
|
|
1083
1084
|
const xform = new RelationshipsXform();
|
|
@@ -1146,10 +1147,11 @@ class XLSX {
|
|
|
1146
1147
|
const relationships = await xform.parseStream(entry);
|
|
1147
1148
|
model.drawingRels[name] = relationships;
|
|
1148
1149
|
}
|
|
1149
|
-
async _processVmlDrawingEntry(entry, model,
|
|
1150
|
+
async _processVmlDrawingEntry(entry, model, zipPath) {
|
|
1150
1151
|
const xform = new VmlDrawingXform();
|
|
1151
1152
|
const vmlDrawing = await xform.parseStream(entry);
|
|
1152
|
-
|
|
1153
|
+
// Key by absolute zip path so reconcile can match any rel target layout.
|
|
1154
|
+
model.vmlDrawings[zipPath] = vmlDrawing;
|
|
1153
1155
|
}
|
|
1154
1156
|
async _processVmlDrawingHFEntry(entry, model, _name) {
|
|
1155
1157
|
const xform = new VmlDrawingXform();
|
|
@@ -1296,7 +1298,7 @@ class XLSX {
|
|
|
1296
1298
|
}
|
|
1297
1299
|
const vmlDrawingName = getVmlDrawingNameFromPath(entryName);
|
|
1298
1300
|
if (vmlDrawingName) {
|
|
1299
|
-
await this._processVmlDrawingEntry(stream, model,
|
|
1301
|
+
await this._processVmlDrawingEntry(stream, model, entryName);
|
|
1300
1302
|
return true;
|
|
1301
1303
|
}
|
|
1302
1304
|
// VML header/footer drawings (watermark in header mode).
|
|
@@ -1306,14 +1308,13 @@ class XLSX {
|
|
|
1306
1308
|
await this._processVmlDrawingHFEntry(stream, model, vmlHFName);
|
|
1307
1309
|
return true;
|
|
1308
1310
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
await this._processCommentEntry(stream, model, `comments${commentsIndex}`);
|
|
1311
|
+
if (isCommentsPath(entryName)) {
|
|
1312
|
+
await this._processCommentEntry(stream, model, entryName);
|
|
1312
1313
|
return true;
|
|
1313
1314
|
}
|
|
1314
1315
|
const tableName = getTableNameFromPath(entryName);
|
|
1315
1316
|
if (tableName) {
|
|
1316
|
-
await this._processTableEntry(stream, model,
|
|
1317
|
+
await this._processTableEntry(stream, model, entryName);
|
|
1317
1318
|
return true;
|
|
1318
1319
|
}
|
|
1319
1320
|
const themeName = getThemeNameFromPath(entryName);
|