@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
@@ -11,6 +11,7 @@ export { Range } from "./modules/excel/range.js";
11
11
  export { Image } from "./modules/excel/image.js";
12
12
  export * from "./modules/excel/anchor.js";
13
13
  export { Table } from "./modules/excel/table.js";
14
+ export { Note } from "./modules/excel/note.js";
14
15
  export { DataValidations } from "./modules/excel/data-validations.js";
15
16
  export { FormCheckbox } from "./modules/excel/form-control.js";
16
17
  export * from "./modules/excel/enums.js";
@@ -14,6 +14,7 @@ export { Range } from "./modules/excel/range.js";
14
14
  export { Image } from "./modules/excel/image.js";
15
15
  export * from "./modules/excel/anchor.js";
16
16
  export { Table } from "./modules/excel/table.js";
17
+ export { Note } from "./modules/excel/note.js";
17
18
  export { DataValidations } from "./modules/excel/data-validations.js";
18
19
  export { FormCheckbox } from "./modules/excel/form-control.js";
19
20
  // Note: the formula engine lives at the `./formula` subpath so it stays
@@ -7,6 +7,7 @@ export { Range } from "./modules/excel/range.js";
7
7
  export { Image } from "./modules/excel/image.js";
8
8
  export * from "./modules/excel/anchor.js";
9
9
  export { Table } from "./modules/excel/table.js";
10
+ export { Note } from "./modules/excel/note.js";
10
11
  export { DataValidations } from "./modules/excel/data-validations.js";
11
12
  export { FormCheckbox } from "./modules/excel/form-control.js";
12
13
  export { WorkbookWriter } from "./modules/excel/stream/workbook-writer.js";
@@ -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
@@ -47,6 +47,7 @@ export interface NoteConfig {
47
47
  export interface NoteModel {
48
48
  type: string;
49
49
  note: NoteConfig;
50
+ author?: string;
50
51
  }
51
52
  export interface CellModel {
52
53
  address: string;
@@ -1,29 +1,9 @@
1
- import type { Font } from "./types.js";
2
- interface NoteText {
3
- text: string;
4
- font?: Partial<Font>;
5
- }
6
- interface NoteConfig {
7
- margins?: {
8
- insetmode?: string;
9
- inset?: number[];
10
- };
11
- protection?: {
12
- locked?: string;
13
- lockText?: string;
14
- };
15
- editAs?: string;
16
- texts?: NoteText[];
17
- anchor?: string;
18
- }
19
- interface NoteModel {
20
- type: string;
21
- note: NoteConfig;
22
- }
1
+ import type { NoteConfig, NoteModel } from "./cell.js";
23
2
  declare class Note {
24
3
  note: string | NoteConfig | undefined;
4
+ author: string | undefined;
25
5
  static readonly DEFAULT_CONFIGS: NoteModel;
26
- constructor(note?: string | NoteConfig);
6
+ constructor(note?: string | NoteConfig, author?: string);
27
7
  get model(): NoteModel;
28
8
  set model(value: NoteModel);
29
9
  static fromModel(model: NoteModel): Note;
@@ -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();
@@ -23,7 +23,11 @@ export declare function getDrawingNameFromPath(path: string): string | undefined
23
23
  export declare function getDrawingNameFromRelsPath(path: string): string | undefined;
24
24
  export declare function getVmlDrawingNameFromPath(path: string): string | undefined;
25
25
  export declare function getVmlDrawingHFNameFromPath(path: string): string | undefined;
26
- export declare function getCommentsIndexFromPath(path: string): string | undefined;
26
+ /**
27
+ * Check if a zip entry path is a comments XML file.
28
+ * Works for both `xl/comments1.xml` and `xl/comments/comment1.xml`.
29
+ */
30
+ export declare function isCommentsPath(path: string): boolean;
27
31
  export declare function getTableNameFromPath(path: string): string | undefined;
28
32
  export declare function getPivotTableNameFromPath(path: string): string | undefined;
29
33
  export declare function getPivotTableNameFromRelsPath(path: string): string | undefined;
@@ -59,6 +63,7 @@ export declare function pivotCacheDefinitionRelsPath(n: number | string): string
59
63
  export declare function pivotCacheRecordsPath(n: number | string): string;
60
64
  export declare function pivotCacheRecordsRelTarget(n: number | string): string;
61
65
  export declare function pivotTablePath(n: number | string): string;
66
+ export declare function pivotTablePathFromName(name: string): string;
62
67
  export declare function pivotTableRelsPath(n: number | string): string;
63
68
  export declare function externalLinkPath(n: number | string): string;
64
69
  export declare function externalLinkRelsPath(n: number | string): string;
@@ -80,12 +85,19 @@ export declare function commentsRelTargetFromWorksheet(sheetId: number | string)
80
85
  export declare function vmlDrawingRelTargetFromWorksheet(sheetId: number | string): string;
81
86
  export declare function vmlDrawingHFRelTargetFromWorksheet(sheetId: number | string): string;
82
87
  export declare function drawingRelTargetFromWorksheet(drawingName: string): string;
83
- export declare function vmlDrawingRelTargetFromWorksheetName(vmlName: string): string;
84
- export declare function commentsRelTargetFromWorksheetName(commentName: string): string;
85
88
  export declare function pivotTableRelTargetFromWorksheet(n: number | string): string;
86
- export declare function pivotTableRelTargetFromWorksheetName(pivotName: string): string;
87
89
  export declare function tableRelTargetFromWorksheet(target: string): string;
88
- export declare function tableRelTargetFromWorksheetName(name: string): string;
89
90
  export declare function mediaRelTargetFromRels(filename: string): string;
90
91
  export declare function ctrlPropPath(id: number | string): string;
91
92
  export declare function ctrlPropRelTargetFromWorksheet(id: number | string): string;
93
+ /**
94
+ * Resolve a relationship Target (relative or absolute) to a normalized zip path.
95
+ *
96
+ * OOXML relationship targets may be:
97
+ * - Relative: `../comments1.xml` (resolved against `baseDir`)
98
+ * - Absolute: `/xl/comments/comment1.xml` (leading slash stripped)
99
+ *
100
+ * @param baseDir The directory containing the source part (e.g. `xl/worksheets/`)
101
+ * @param target The raw Target value from the .rels file
102
+ */
103
+ export declare function resolveRelTarget(baseDir: string, target: string): string;
@@ -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":
@@ -8,6 +8,14 @@ declare class CommentsXform extends BaseXform<CommentsModel> {
8
8
  [key: string]: CommentXform;
9
9
  };
10
10
  parser: any;
11
+ /** Authors collected while parsing the <authors> element. */
12
+ private _authors;
13
+ /** Whether we are currently inside the <authors> element. */
14
+ private _inAuthors;
15
+ /** Whether we are currently inside an <author> element (collecting text). */
16
+ private _inAuthor;
17
+ /** Accumulator for the current <author> text content. */
18
+ private _currentAuthor;
11
19
  constructor();
12
20
  render(xmlStream: any, model?: CommentsModel): void;
13
21
  parseOpen(node: any): boolean;
@@ -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);
@@ -7,6 +7,8 @@ interface ClientDataModel {
7
7
  anchor: any;
8
8
  protection: Protection;
9
9
  editAs: string;
10
+ row?: number;
11
+ col?: number;
10
12
  }
11
13
  interface RenderModel {
12
14
  note: {
@@ -23,6 +25,10 @@ declare class VmlClientDataXform extends BaseXform<ClientDataModel> {
23
25
  [key: string]: any;
24
26
  };
25
27
  parser: any;
28
+ /** Name of the current simple leaf element being parsed (e.g. "x:Row"). */
29
+ private _leafName;
30
+ /** Accumulated text for the current leaf element. */
31
+ private _leafText;
26
32
  constructor();
27
33
  get tag(): string;
28
34
  render(xmlStream: any, model: RenderModel): void;
@@ -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
  }
@@ -338,8 +338,8 @@ declare class XLSX {
338
338
  subtotal?: string;
339
339
  }>, defaultMetric: PivotTableSubtotal): PivotTableSubtotal[];
340
340
  _processWorksheetEntry(stream: IParseStream, model: any, sheetNo: number, options: XlsxOptions | undefined, path: string): Promise<void>;
341
- _processCommentEntry(stream: IParseStream, model: any, name: string): Promise<void>;
342
- _processTableEntry(stream: IParseStream, model: any, name: string): Promise<void>;
341
+ _processCommentEntry(stream: IParseStream, model: any, zipPath: string): Promise<void>;
342
+ _processTableEntry(stream: IParseStream, model: any, zipPath: string): Promise<void>;
343
343
  _processWorksheetRelsEntry(stream: IParseStream, model: any, sheetNo: number): Promise<void>;
344
344
  _processMediaEntry(stream: IParseStream, model: any, filename: string): Promise<void>;
345
345
  /**
@@ -352,7 +352,7 @@ declare class XLSX {
352
352
  */
353
353
  _processDrawingEntry(stream: IParseStream, model: any, name: string, rawData?: Uint8Array): Promise<void>;
354
354
  _processDrawingRelsEntry(entry: any, model: any, name: string): Promise<void>;
355
- _processVmlDrawingEntry(entry: any, model: any, name: string): Promise<void>;
355
+ _processVmlDrawingEntry(entry: any, model: any, zipPath: string): Promise<void>;
356
356
  _processVmlDrawingHFEntry(entry: any, model: any, _name: string): Promise<void>;
357
357
  _processThemeEntry(stream: IParseStream, model: any, name: string): Promise<void>;
358
358
  _processPivotTableEntry(stream: IParseStream, model: any, name: string): Promise<void>;