@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.
Files changed (49) hide show
  1. package/dist/browser/index.browser.d.ts +1 -0
  2. package/dist/browser/index.browser.js +1 -0
  3. package/dist/browser/index.d.ts +1 -0
  4. package/dist/browser/index.js +1 -0
  5. package/dist/browser/modules/excel/cell.d.ts +1 -0
  6. package/dist/browser/modules/excel/note.d.ts +3 -23
  7. package/dist/browser/modules/excel/note.js +8 -2
  8. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +17 -5
  9. package/dist/browser/modules/excel/utils/ooxml-paths.js +46 -19
  10. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
  11. package/dist/browser/modules/excel/xlsx/xform/comment/comments-xform.d.ts +8 -0
  12. package/dist/browser/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
  13. package/dist/browser/modules/excel/xlsx/xform/comment/vml-client-data-xform.d.ts +6 -0
  14. package/dist/browser/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
  15. package/dist/browser/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
  16. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +54 -14
  17. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +3 -3
  18. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +16 -15
  19. package/dist/cjs/index.js +4 -2
  20. package/dist/cjs/modules/excel/note.js +8 -2
  21. package/dist/cjs/modules/excel/utils/ooxml-paths.js +49 -24
  22. package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
  23. package/dist/cjs/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
  24. package/dist/cjs/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
  25. package/dist/cjs/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
  26. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +53 -13
  27. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +15 -14
  28. package/dist/esm/index.browser.js +1 -0
  29. package/dist/esm/index.js +1 -0
  30. package/dist/esm/modules/excel/note.js +8 -2
  31. package/dist/esm/modules/excel/utils/ooxml-paths.js +46 -19
  32. package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +3 -2
  33. package/dist/esm/modules/excel/xlsx/xform/comment/comments-xform.js +52 -5
  34. package/dist/esm/modules/excel/xlsx/xform/comment/vml-client-data-xform.js +27 -1
  35. package/dist/esm/modules/excel/xlsx/xform/comment/vml-shape-xform.js +4 -0
  36. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +54 -14
  37. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +16 -15
  38. package/dist/iife/excelts.iife.js +149 -56
  39. package/dist/iife/excelts.iife.js.map +1 -1
  40. package/dist/iife/excelts.iife.min.js +30 -30
  41. package/dist/types/index.browser.d.ts +1 -0
  42. package/dist/types/index.d.ts +1 -0
  43. package/dist/types/modules/excel/cell.d.ts +1 -0
  44. package/dist/types/modules/excel/note.d.ts +3 -23
  45. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +17 -5
  46. package/dist/types/modules/excel/xlsx/xform/comment/comments-xform.d.ts +8 -0
  47. package/dist/types/modules/excel/xlsx/xform/comment/vml-client-data-xform.d.ts +6 -0
  48. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +3 -3
  49. 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
- return deepMerge({}, Note.DEFAULT_CONFIGS, value);
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
- const commentsXmlRegex = /^xl\/comments(\d+)[.]xml$/;
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
- export function getCommentsIndexFromPath(path) {
85
- const match = commentsXmlRegex.exec(path);
86
- return match ? match[1] : undefined;
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
- ...node.attributes
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
- // TODO: support authors properly
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
- xmlStream.leafNode("author", null, "Author");
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
- this.map.comment.render(xmlStream, comment);
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.parser) {
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.parser) {
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
- const rels = (model.relationships ?? []).reduce((h, rel) => {
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 commentEntry = options.comments?.[rel.Target];
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
- if (rel.Type === RelType.VmlDrawing && model.comments && model.comments.length) {
712
- const vmlEntry = options.vmlDrawings?.[rel.Target];
713
- if (vmlEntry) {
714
- const vmlComment = vmlEntry.comments;
715
- model.comments.forEach((comment, index) => {
716
- comment.note = Object.assign({}, comment.note, vmlComment[index]);
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
- return h;
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 table = options.tables[rel.Target];
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 pivotTable = options.pivotTables[rel.Target];
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, commentsRelTargetFromWorksheetName, ctrlPropPath, drawingPath, drawingRelsPath, externalLinkPath, externalLinkRelsPath, externalLinkRelTargetFromWorkbook, OOXML_REL_TARGETS, pivotTableRelTargetFromWorksheetName, pivotCacheDefinitionRelTargetFromWorkbook, getCommentsIndexFromPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getExternalLinkIndexFromPath, getExternalLinkIndexFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, tableRelTargetFromWorksheetName, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getVmlDrawingHFNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingRelTargetFromWorksheetName, vmlDrawingPath, vmlDrawingHFPath, vmlDrawingHFRelsPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
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 format (e.g., "../pivotTables/pivotTable1.xml") matches worksheet .rels Target values,
1011
- // allowing worksheet reconciliation to look up pivot tables by their relationship target path.
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, name) {
1071
+ async _processCommentEntry(stream, model, zipPath) {
1073
1072
  const xform = new CommentsXform();
1074
1073
  const comments = await xform.parseStream(stream);
1075
- model.comments[commentsRelTargetFromWorksheetName(name)] = comments;
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, name) {
1077
+ async _processTableEntry(stream, model, zipPath) {
1078
1078
  const xform = new TableXform();
1079
1079
  const table = await xform.parseStream(stream);
1080
- model.tables[tableRelTargetFromWorksheetName(name)] = table;
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, name) {
1150
+ async _processVmlDrawingEntry(entry, model, zipPath) {
1150
1151
  const xform = new VmlDrawingXform();
1151
1152
  const vmlDrawing = await xform.parseStream(entry);
1152
- model.vmlDrawings[vmlDrawingRelTargetFromWorksheetName(name)] = vmlDrawing;
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, vmlDrawingName);
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
- const commentsIndex = getCommentsIndexFromPath(entryName);
1310
- if (commentsIndex) {
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, tableName);
1317
+ await this._processTableEntry(stream, model, entryName);
1317
1318
  return true;
1318
1319
  }
1319
1320
  const themeName = getThemeNameFromPath(entryName);