@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
|
@@ -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
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/browser/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,29 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -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.
|
|
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
|
}
|
|
@@ -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,
|
|
342
|
-
_processTableEntry(stream: IParseStream, model: any,
|
|
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,
|
|
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>;
|