@adeu/core 1.6.2 → 1.6.5
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/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +38 -38
- package/src/comments.test.ts +37 -37
- package/src/comments.ts +450 -450
- package/src/diff.test.ts +61 -61
- package/src/diff.ts +250 -250
- package/src/docx/bridge.ts +188 -188
- package/src/docx/dom.ts +53 -53
- package/src/docx/primitives.ts +64 -64
- package/src/domain.ts +10 -10
- package/src/engine.atomic.test.ts +57 -57
- package/src/engine.batch.test.ts +92 -92
- package/src/engine.safety.test.ts +41 -41
- package/src/engine.tables.test.ts +165 -165
- package/src/engine.ts +734 -734
- package/src/index.test.ts +7 -7
- package/src/index.ts +13 -13
- package/src/ingest.test.ts +43 -43
- package/src/ingest.ts +399 -399
- package/src/mapper.test.ts +65 -65
- package/src/mapper.ts +834 -834
- package/src/markup.test.ts +149 -149
- package/src/markup.ts +322 -322
- package/src/models.ts +50 -50
- package/src/outline.ts +376 -376
- package/src/pagination.ts +238 -238
- package/src/test-utils.ts +141 -141
- package/src/utils/docx.ts +477 -477
- package/tsconfig.json +21 -21
- package/tsup.config.ts +9 -9
- package/vitest.config.ts +11 -11
package/src/comments.ts
CHANGED
|
@@ -1,451 +1,451 @@
|
|
|
1
|
-
import { DocxPackage, Part, DocumentObject } from './docx/bridge.js';
|
|
2
|
-
import { findAllDescendants, findChild, parseXml } from './docx/dom.js';
|
|
3
|
-
|
|
4
|
-
const NS = {
|
|
5
|
-
w: 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
|
|
6
|
-
w14: 'http://schemas.microsoft.com/office/word/2010/wordml',
|
|
7
|
-
w15: 'http://schemas.microsoft.com/office/word/2012/wordml',
|
|
8
|
-
w16cid: 'http://schemas.microsoft.com/office/word/2016/wordml/cid',
|
|
9
|
-
w16cex: 'http://schemas.microsoft.com/office/word/2018/wordml/cex',
|
|
10
|
-
mc: 'http://schemas.openxmlformats.org/markup-compatibility/2006'
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const CT = {
|
|
14
|
-
COMMENTS: 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml',
|
|
15
|
-
EXTENDED: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml',
|
|
16
|
-
IDS: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml',
|
|
17
|
-
EXTENSIBLE: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml'
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const RT = {
|
|
21
|
-
COMMENTS: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
|
|
22
|
-
EXTENDED: 'http://schemas.microsoft.com/office/2011/relationships/commentsExtended',
|
|
23
|
-
IDS: 'http://schemas.microsoft.com/office/2016/09/relationships/commentsIds',
|
|
24
|
-
EXTENSIBLE: 'http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible'
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export class CommentsManager {
|
|
28
|
-
private _commentsPart: Part | null = null;
|
|
29
|
-
private _extendedPart: Part | null = null;
|
|
30
|
-
private _idsPart: Part | null = null;
|
|
31
|
-
private _extensiblePart: Part | null = null;
|
|
32
|
-
private _nextId: number | null = null;
|
|
33
|
-
|
|
34
|
-
constructor(public doc: DocumentObject) {}
|
|
35
|
-
|
|
36
|
-
public get commentsPart() {
|
|
37
|
-
if (!this._commentsPart) {
|
|
38
|
-
this._commentsPart = this._getOrCreateCommentsPart();
|
|
39
|
-
this._ensureNamespaces();
|
|
40
|
-
}
|
|
41
|
-
return this._commentsPart;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
public get extendedPart() {
|
|
45
|
-
if (!this._extendedPart) this._extendedPart = this._getOrCreateExtendedPart();
|
|
46
|
-
return this._extendedPart;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
public get idsPart() {
|
|
50
|
-
if (!this._idsPart) this._idsPart = this._getOrCreateIdsPart();
|
|
51
|
-
return this._idsPart;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
public get extensiblePart() {
|
|
55
|
-
if (!this._extensiblePart) this._extensiblePart = this._getOrCreateExtensiblePart();
|
|
56
|
-
return this._extensiblePart;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
public get nextId(): number {
|
|
60
|
-
if (this._nextId === null) this._nextId = this._getNextCommentId();
|
|
61
|
-
return this._nextId;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public set nextId(value: number) {
|
|
65
|
-
this._nextId = value;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private _getExistingPartByType(contentType: string): Part | null {
|
|
69
|
-
return this.doc.pkg.parts.find(p => p.contentType === contentType) || null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private _linkPart(part: Part, relType: string): Part {
|
|
73
|
-
for (const rel of this.doc.part.rels.values()) {
|
|
74
|
-
if (!rel.isExternal && rel.target === part.partname.split('/').pop()) {
|
|
75
|
-
return part;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
this.doc.relateTo(part, relType);
|
|
79
|
-
return part;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private _getOrCreateCommentsPart(): Part {
|
|
83
|
-
let part = this._getExistingPartByType(CT.COMMENTS);
|
|
84
|
-
if (part) return this._linkPart(part, RT.COMMENTS);
|
|
85
|
-
|
|
86
|
-
const partname = this.doc.pkg.nextPartname('/word/comments%d.xml');
|
|
87
|
-
const xml = `<w:comments xmlns:w="${NS.w}" xmlns:w14="${NS.w14}" xmlns:w15="${NS.w15}" xmlns:w16cid="${NS.w16cid}" xmlns:w16cex="${NS.w16cex}" xmlns:mc="${NS.mc}" mc:Ignorable="w14 w15 w16cid w16cex"></w:comments>`;
|
|
88
|
-
part = this.doc.pkg.addPart(partname, CT.COMMENTS, xml);
|
|
89
|
-
this.doc.relateTo(part, RT.COMMENTS);
|
|
90
|
-
return part;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private _getOrCreateExtendedPart(): Part {
|
|
94
|
-
let part = this._getExistingPartByType(CT.EXTENDED);
|
|
95
|
-
if (part) return this._linkPart(part, RT.EXTENDED);
|
|
96
|
-
|
|
97
|
-
const partname = this.doc.pkg.nextPartname('/word/commentsExtended%d.xml');
|
|
98
|
-
const xml = `<w15:commentsEx xmlns:w15="${NS.w15}"></w15:commentsEx>`;
|
|
99
|
-
part = this.doc.pkg.addPart(partname, CT.EXTENDED, xml);
|
|
100
|
-
this.doc.relateTo(part, RT.EXTENDED);
|
|
101
|
-
return part;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private _getOrCreateIdsPart(): Part {
|
|
105
|
-
let part = this._getExistingPartByType(CT.IDS);
|
|
106
|
-
if (part) return this._linkPart(part, RT.IDS);
|
|
107
|
-
|
|
108
|
-
const partname = this.doc.pkg.nextPartname('/word/commentsIds%d.xml');
|
|
109
|
-
const xml = `<w16cid:commentsIds xmlns:w16cid="${NS.w16cid}"></w16cid:commentsIds>`;
|
|
110
|
-
part = this.doc.pkg.addPart(partname, CT.IDS, xml);
|
|
111
|
-
this.doc.relateTo(part, RT.IDS);
|
|
112
|
-
return part;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private _getOrCreateExtensiblePart(): Part {
|
|
116
|
-
let part = this._getExistingPartByType(CT.EXTENSIBLE);
|
|
117
|
-
if (part) return this._linkPart(part, RT.EXTENSIBLE);
|
|
118
|
-
|
|
119
|
-
const partname = this.doc.pkg.nextPartname('/word/commentsExtensible%d.xml');
|
|
120
|
-
const xml = `<w16cex:commentsExtensible xmlns:w16cex="${NS.w16cex}"></w16cex:commentsExtensible>`;
|
|
121
|
-
part = this.doc.pkg.addPart(partname, CT.EXTENSIBLE, xml);
|
|
122
|
-
this.doc.relateTo(part, RT.EXTENSIBLE);
|
|
123
|
-
return part;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private _ensureNamespaces() {
|
|
127
|
-
// In TS we use full xml reconstruction if attributes are missing, but xmldom generally tolerates
|
|
128
|
-
// runtime attributes if the namespace is declared. For absolute safety, if it's completely missing,
|
|
129
|
-
// we would rebuild. Assuming the parser caught them if they existed.
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private _getNextCommentId(): number {
|
|
133
|
-
const ids = [0];
|
|
134
|
-
const part = this._getExistingPartByType(CT.COMMENTS);
|
|
135
|
-
if (part) {
|
|
136
|
-
const comments = findAllDescendants(part._element, 'w:comment');
|
|
137
|
-
for (const c of comments) {
|
|
138
|
-
const idStr = c.getAttribute('w:id');
|
|
139
|
-
if (idStr) ids.push(parseInt(idStr, 10) || 0);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return Math.max(...ids) + 1;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private _generateHexId(): string {
|
|
146
|
-
return Math.floor(Math.random() * 0xFFFFFFFF).toString(16).toUpperCase().padStart(8, '0');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private _getInitials(author: string): string {
|
|
150
|
-
if (!author) return '';
|
|
151
|
-
return author.split(' ').filter(Boolean).map(p => p[0]).join('').toUpperCase();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private _findParaIdForComment(commentId: string): string | null {
|
|
155
|
-
if (!this._commentsPart) return null;
|
|
156
|
-
for (const c of findAllDescendants(this._commentsPart._element, 'w:comment')) {
|
|
157
|
-
if (c.getAttribute('w:id') === commentId) {
|
|
158
|
-
for (const p of findAllDescendants(c, 'w:p')) {
|
|
159
|
-
const pid = p.getAttribute('w14:paraId');
|
|
160
|
-
if (pid) return pid;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private _findThreadRootParaId(commentId: string): string | null {
|
|
168
|
-
const directParaId = this._findParaIdForComment(commentId);
|
|
169
|
-
const extPart = this._getExistingPartByType(CT.EXTENDED);
|
|
170
|
-
if (!directParaId || !extPart) return directParaId;
|
|
171
|
-
|
|
172
|
-
for (let i = 0; i < extPart._element.childNodes.length; i++) {
|
|
173
|
-
const child = extPart._element.childNodes[i] as Element;
|
|
174
|
-
if (child.nodeType !== 1) continue;
|
|
175
|
-
if (child.getAttribute('w15:paraId') === directParaId) {
|
|
176
|
-
const parent = child.getAttribute('w15:paraIdParent');
|
|
177
|
-
if (parent) return parent;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return directParaId;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
public addComment(author: string, text: string, parentId: string | null = null): string {
|
|
184
|
-
const commentId = this.nextId.toString();
|
|
185
|
-
this.nextId++;
|
|
186
|
-
const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
187
|
-
|
|
188
|
-
const doc = this.commentsPart._element.ownerDocument!;
|
|
189
|
-
const comment = doc.createElement('w:comment');
|
|
190
|
-
comment.setAttribute('w:id', commentId);
|
|
191
|
-
comment.setAttribute('w:author', author);
|
|
192
|
-
comment.setAttribute('w:date', now);
|
|
193
|
-
|
|
194
|
-
const initials = this._getInitials(author);
|
|
195
|
-
if (initials) comment.setAttribute('w:initials', initials);
|
|
196
|
-
|
|
197
|
-
const extPart = this._getExistingPartByType(CT.EXTENDED);
|
|
198
|
-
if (parentId && !extPart) {
|
|
199
|
-
comment.setAttribute('w15:p', parentId);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const paraId = this._generateHexId();
|
|
203
|
-
const rsid = this._generateHexId();
|
|
204
|
-
|
|
205
|
-
const p = doc.createElement('w:p');
|
|
206
|
-
p.setAttribute('w14:paraId', paraId);
|
|
207
|
-
p.setAttribute('w14:textId', '77777777');
|
|
208
|
-
p.setAttribute('w:rsidR', rsid);
|
|
209
|
-
p.setAttribute('w:rsidRDefault', rsid);
|
|
210
|
-
p.setAttribute('w:rsidP', rsid);
|
|
211
|
-
|
|
212
|
-
const pPr = doc.createElement('w:pPr');
|
|
213
|
-
const pStyle = doc.createElement('w:pStyle');
|
|
214
|
-
pStyle.setAttribute('w:val', 'CommentText');
|
|
215
|
-
pPr.appendChild(pStyle);
|
|
216
|
-
p.appendChild(pPr);
|
|
217
|
-
|
|
218
|
-
const rRef = doc.createElement('w:r');
|
|
219
|
-
const rPrRef = doc.createElement('w:rPr');
|
|
220
|
-
const rStyleRef = doc.createElement('w:rStyle');
|
|
221
|
-
rStyleRef.setAttribute('w:val', 'CommentReference');
|
|
222
|
-
rPrRef.appendChild(rStyleRef);
|
|
223
|
-
rRef.appendChild(rPrRef);
|
|
224
|
-
rRef.appendChild(doc.createElement('w:annotationRef'));
|
|
225
|
-
p.appendChild(rRef);
|
|
226
|
-
|
|
227
|
-
const r = doc.createElement('w:r');
|
|
228
|
-
const t = doc.createElement('w:t');
|
|
229
|
-
t.textContent = text;
|
|
230
|
-
r.appendChild(t);
|
|
231
|
-
p.appendChild(r);
|
|
232
|
-
comment.appendChild(p);
|
|
233
|
-
|
|
234
|
-
this.commentsPart._element.appendChild(comment);
|
|
235
|
-
|
|
236
|
-
if (this.extendedPart) {
|
|
237
|
-
const parentParaId = parentId ? this._findThreadRootParaId(parentId) : null;
|
|
238
|
-
const exDoc = this.extendedPart._element.ownerDocument!;
|
|
239
|
-
const commentEx = exDoc.createElement('w15:commentEx');
|
|
240
|
-
commentEx.setAttribute('w15:paraId', paraId);
|
|
241
|
-
if (parentParaId) commentEx.setAttribute('w15:paraIdParent', parentParaId);
|
|
242
|
-
commentEx.setAttribute('w15:done', '0');
|
|
243
|
-
this.extendedPart._element.appendChild(commentEx);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (this.idsPart) {
|
|
247
|
-
const idsDoc = this.idsPart._element.ownerDocument!;
|
|
248
|
-
const commentIdEl = idsDoc.createElement('w16cid:commentId');
|
|
249
|
-
commentIdEl.setAttribute('w16cid:paraId', paraId);
|
|
250
|
-
commentIdEl.setAttribute('w16cid:durableId', this._generateHexId());
|
|
251
|
-
this.idsPart._element.appendChild(commentIdEl);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (this.extensiblePart) {
|
|
255
|
-
let durableId: string | null = null;
|
|
256
|
-
for (let i = 0; i < this.idsPart._element.childNodes.length; i++) {
|
|
257
|
-
const child = this.idsPart._element.childNodes[i] as Element;
|
|
258
|
-
if (child.nodeType === 1 && child.getAttribute('w16cid:paraId') === paraId) {
|
|
259
|
-
durableId = child.getAttribute('w16cid:durableId');
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (durableId) {
|
|
264
|
-
const cexDoc = this.extensiblePart._element.ownerDocument!;
|
|
265
|
-
const extEl = cexDoc.createElement('w16cex:commentExtensible');
|
|
266
|
-
extEl.setAttribute('w16cex:durableId', durableId);
|
|
267
|
-
extEl.setAttribute('w16cex:dateUtc', now);
|
|
268
|
-
this.extensiblePart._element.appendChild(extEl);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return commentId;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
public deleteComment(commentId: string) {
|
|
276
|
-
if (!this._commentsPart) return;
|
|
277
|
-
|
|
278
|
-
let commentEl: Element | null = null;
|
|
279
|
-
for (const c of findAllDescendants(this._commentsPart._element, 'w:comment')) {
|
|
280
|
-
if (c.getAttribute('w:id') === commentId) {
|
|
281
|
-
commentEl = c;
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (!commentEl) return;
|
|
287
|
-
|
|
288
|
-
let paraId: string | null = null;
|
|
289
|
-
for (const p of findAllDescendants(commentEl, 'w:p')) {
|
|
290
|
-
const pid = p.getAttribute('w14:paraId');
|
|
291
|
-
if (pid) {
|
|
292
|
-
paraId = pid;
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (paraId) {
|
|
298
|
-
const repliesToDelete: string[] = [];
|
|
299
|
-
if (this.extendedPart) {
|
|
300
|
-
for (let i = 0; i < this.extendedPart._element.childNodes.length; i++) {
|
|
301
|
-
const child = this.extendedPart._element.childNodes[i] as Element;
|
|
302
|
-
if (child.nodeType !== 1) continue;
|
|
303
|
-
|
|
304
|
-
if (child.getAttribute('w15:paraIdParent') === paraId) {
|
|
305
|
-
const childParaId = child.getAttribute('w15:paraId');
|
|
306
|
-
if (childParaId) {
|
|
307
|
-
for (const c of findAllDescendants(this._commentsPart._element, 'w:comment')) {
|
|
308
|
-
for (const p of findAllDescendants(c, 'w:p')) {
|
|
309
|
-
if (p.getAttribute('w14:paraId') === childParaId) {
|
|
310
|
-
const cid = c.getAttribute('w:id');
|
|
311
|
-
if (cid) repliesToDelete.push(cid);
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
for (const repId of repliesToDelete) {
|
|
322
|
-
this.deleteComment(repId);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
let durableId: string | null = null;
|
|
326
|
-
|
|
327
|
-
if (this.idsPart) {
|
|
328
|
-
const toRemove: Element[] = [];
|
|
329
|
-
for (let i = 0; i < this.idsPart._element.childNodes.length; i++) {
|
|
330
|
-
const child = this.idsPart._element.childNodes[i] as Element;
|
|
331
|
-
if (child.nodeType === 1 && child.getAttribute('w16cid:paraId') === paraId) {
|
|
332
|
-
durableId = child.getAttribute('w16cid:durableId');
|
|
333
|
-
toRemove.push(child);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
toRemove.forEach(c => this.idsPart!._element.removeChild(c));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (this.extendedPart) {
|
|
340
|
-
const toRemove: Element[] = [];
|
|
341
|
-
for (let i = 0; i < this.extendedPart._element.childNodes.length; i++) {
|
|
342
|
-
const child = this.extendedPart._element.childNodes[i] as Element;
|
|
343
|
-
if (child.nodeType === 1 && child.getAttribute('w15:paraId') === paraId) {
|
|
344
|
-
toRemove.push(child);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
toRemove.forEach(c => this.extendedPart!._element.removeChild(c));
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (durableId && this.extensiblePart) {
|
|
351
|
-
const toRemove: Element[] = [];
|
|
352
|
-
for (let i = 0; i < this.extensiblePart._element.childNodes.length; i++) {
|
|
353
|
-
const child = this.extensiblePart._element.childNodes[i] as Element;
|
|
354
|
-
if (child.nodeType === 1 && child.getAttribute('w16cex:durableId') === durableId) {
|
|
355
|
-
toRemove.push(child);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
toRemove.forEach(c => this.extensiblePart!._element.removeChild(c));
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (commentEl.parentNode) {
|
|
363
|
-
commentEl.parentNode.removeChild(commentEl);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Keep the global extraction function matching Python behavior
|
|
369
|
-
export function extract_comments_data(pkg: DocxPackage): Record<string, any> {
|
|
370
|
-
// Temporary bridge to use the new class
|
|
371
|
-
const docObj = {
|
|
372
|
-
pkg,
|
|
373
|
-
part: pkg.mainDocumentPart,
|
|
374
|
-
relateTo: () => {} // Mock since extraction is read-only
|
|
375
|
-
} as unknown as DocumentObject;
|
|
376
|
-
|
|
377
|
-
const mgr = new CommentsManager(docObj);
|
|
378
|
-
const data: Record<string, any> = {};
|
|
379
|
-
|
|
380
|
-
const part = pkg.parts.find(p => p.contentType === CT.COMMENTS);
|
|
381
|
-
if (!part) return data;
|
|
382
|
-
|
|
383
|
-
const para_id_to_cid: Record<string, string> = {};
|
|
384
|
-
const comments = findAllDescendants(part._element, 'w:comment');
|
|
385
|
-
|
|
386
|
-
for (const c of comments) {
|
|
387
|
-
const c_id = c.getAttribute('w:id');
|
|
388
|
-
if (!c_id) continue;
|
|
389
|
-
|
|
390
|
-
const c_author = c.getAttribute('w:author') || 'Unknown';
|
|
391
|
-
const c_date = c.getAttribute('w:date') || '';
|
|
392
|
-
|
|
393
|
-
let is_resolved = false;
|
|
394
|
-
const val = c.getAttribute('w15:done');
|
|
395
|
-
if (val === '1' || val === 'true' || val === 'on') is_resolved = true;
|
|
396
|
-
|
|
397
|
-
let parent_id = c.getAttribute('w15:p') || null;
|
|
398
|
-
|
|
399
|
-
const p_elems = findAllDescendants(c, 'w:p');
|
|
400
|
-
for (const p of p_elems) {
|
|
401
|
-
const pid = p.getAttribute('w14:paraId');
|
|
402
|
-
if (pid) para_id_to_cid[pid] = c_id;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const text_parts: string[] = [];
|
|
406
|
-
for (const p of p_elems) {
|
|
407
|
-
const t_elems = findAllDescendants(p, 'w:t');
|
|
408
|
-
for (const t of t_elems) {
|
|
409
|
-
if (t.textContent) text_parts.push(t.textContent);
|
|
410
|
-
}
|
|
411
|
-
text_parts.push('\n');
|
|
412
|
-
}
|
|
413
|
-
const full_text = text_parts.join('').trim();
|
|
414
|
-
|
|
415
|
-
data[c_id] = {
|
|
416
|
-
author: c_author,
|
|
417
|
-
text: full_text,
|
|
418
|
-
date: c_date,
|
|
419
|
-
resolved: is_resolved,
|
|
420
|
-
parent_id: parent_id,
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const extPart = pkg.parts.find(p => p.contentType === CT.EXTENDED);
|
|
425
|
-
if (extPart) {
|
|
426
|
-
const children = extPart._element.childNodes;
|
|
427
|
-
for (let i = 0; i < children.length; i++) {
|
|
428
|
-
const child = children[i] as Element;
|
|
429
|
-
if (child.nodeType !== 1) continue;
|
|
430
|
-
|
|
431
|
-
const para_id = child.getAttribute('w15:paraId');
|
|
432
|
-
const parent_para_id = child.getAttribute('w15:paraIdParent');
|
|
433
|
-
const done_val = child.getAttribute('w15:done');
|
|
434
|
-
|
|
435
|
-
if (para_id) {
|
|
436
|
-
const c_id = para_id_to_cid[para_id];
|
|
437
|
-
if (c_id && data[c_id]) {
|
|
438
|
-
if (parent_para_id) {
|
|
439
|
-
const p_id = para_id_to_cid[parent_para_id];
|
|
440
|
-
if (p_id) data[c_id].parent_id = p_id;
|
|
441
|
-
}
|
|
442
|
-
if (done_val === '1' || done_val === 'true' || done_val === 'on') {
|
|
443
|
-
data[c_id].resolved = true;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return data;
|
|
1
|
+
import { DocxPackage, Part, DocumentObject } from './docx/bridge.js';
|
|
2
|
+
import { findAllDescendants, findChild, parseXml } from './docx/dom.js';
|
|
3
|
+
|
|
4
|
+
const NS = {
|
|
5
|
+
w: 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
|
|
6
|
+
w14: 'http://schemas.microsoft.com/office/word/2010/wordml',
|
|
7
|
+
w15: 'http://schemas.microsoft.com/office/word/2012/wordml',
|
|
8
|
+
w16cid: 'http://schemas.microsoft.com/office/word/2016/wordml/cid',
|
|
9
|
+
w16cex: 'http://schemas.microsoft.com/office/word/2018/wordml/cex',
|
|
10
|
+
mc: 'http://schemas.openxmlformats.org/markup-compatibility/2006'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const CT = {
|
|
14
|
+
COMMENTS: 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml',
|
|
15
|
+
EXTENDED: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml',
|
|
16
|
+
IDS: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml',
|
|
17
|
+
EXTENSIBLE: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const RT = {
|
|
21
|
+
COMMENTS: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
|
|
22
|
+
EXTENDED: 'http://schemas.microsoft.com/office/2011/relationships/commentsExtended',
|
|
23
|
+
IDS: 'http://schemas.microsoft.com/office/2016/09/relationships/commentsIds',
|
|
24
|
+
EXTENSIBLE: 'http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class CommentsManager {
|
|
28
|
+
private _commentsPart: Part | null = null;
|
|
29
|
+
private _extendedPart: Part | null = null;
|
|
30
|
+
private _idsPart: Part | null = null;
|
|
31
|
+
private _extensiblePart: Part | null = null;
|
|
32
|
+
private _nextId: number | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(public doc: DocumentObject) {}
|
|
35
|
+
|
|
36
|
+
public get commentsPart() {
|
|
37
|
+
if (!this._commentsPart) {
|
|
38
|
+
this._commentsPart = this._getOrCreateCommentsPart();
|
|
39
|
+
this._ensureNamespaces();
|
|
40
|
+
}
|
|
41
|
+
return this._commentsPart;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public get extendedPart() {
|
|
45
|
+
if (!this._extendedPart) this._extendedPart = this._getOrCreateExtendedPart();
|
|
46
|
+
return this._extendedPart;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public get idsPart() {
|
|
50
|
+
if (!this._idsPart) this._idsPart = this._getOrCreateIdsPart();
|
|
51
|
+
return this._idsPart;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public get extensiblePart() {
|
|
55
|
+
if (!this._extensiblePart) this._extensiblePart = this._getOrCreateExtensiblePart();
|
|
56
|
+
return this._extensiblePart;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public get nextId(): number {
|
|
60
|
+
if (this._nextId === null) this._nextId = this._getNextCommentId();
|
|
61
|
+
return this._nextId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public set nextId(value: number) {
|
|
65
|
+
this._nextId = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _getExistingPartByType(contentType: string): Part | null {
|
|
69
|
+
return this.doc.pkg.parts.find(p => p.contentType === contentType) || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private _linkPart(part: Part, relType: string): Part {
|
|
73
|
+
for (const rel of this.doc.part.rels.values()) {
|
|
74
|
+
if (!rel.isExternal && rel.target === part.partname.split('/').pop()) {
|
|
75
|
+
return part;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
this.doc.relateTo(part, relType);
|
|
79
|
+
return part;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private _getOrCreateCommentsPart(): Part {
|
|
83
|
+
let part = this._getExistingPartByType(CT.COMMENTS);
|
|
84
|
+
if (part) return this._linkPart(part, RT.COMMENTS);
|
|
85
|
+
|
|
86
|
+
const partname = this.doc.pkg.nextPartname('/word/comments%d.xml');
|
|
87
|
+
const xml = `<w:comments xmlns:w="${NS.w}" xmlns:w14="${NS.w14}" xmlns:w15="${NS.w15}" xmlns:w16cid="${NS.w16cid}" xmlns:w16cex="${NS.w16cex}" xmlns:mc="${NS.mc}" mc:Ignorable="w14 w15 w16cid w16cex"></w:comments>`;
|
|
88
|
+
part = this.doc.pkg.addPart(partname, CT.COMMENTS, xml);
|
|
89
|
+
this.doc.relateTo(part, RT.COMMENTS);
|
|
90
|
+
return part;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private _getOrCreateExtendedPart(): Part {
|
|
94
|
+
let part = this._getExistingPartByType(CT.EXTENDED);
|
|
95
|
+
if (part) return this._linkPart(part, RT.EXTENDED);
|
|
96
|
+
|
|
97
|
+
const partname = this.doc.pkg.nextPartname('/word/commentsExtended%d.xml');
|
|
98
|
+
const xml = `<w15:commentsEx xmlns:w15="${NS.w15}"></w15:commentsEx>`;
|
|
99
|
+
part = this.doc.pkg.addPart(partname, CT.EXTENDED, xml);
|
|
100
|
+
this.doc.relateTo(part, RT.EXTENDED);
|
|
101
|
+
return part;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private _getOrCreateIdsPart(): Part {
|
|
105
|
+
let part = this._getExistingPartByType(CT.IDS);
|
|
106
|
+
if (part) return this._linkPart(part, RT.IDS);
|
|
107
|
+
|
|
108
|
+
const partname = this.doc.pkg.nextPartname('/word/commentsIds%d.xml');
|
|
109
|
+
const xml = `<w16cid:commentsIds xmlns:w16cid="${NS.w16cid}"></w16cid:commentsIds>`;
|
|
110
|
+
part = this.doc.pkg.addPart(partname, CT.IDS, xml);
|
|
111
|
+
this.doc.relateTo(part, RT.IDS);
|
|
112
|
+
return part;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private _getOrCreateExtensiblePart(): Part {
|
|
116
|
+
let part = this._getExistingPartByType(CT.EXTENSIBLE);
|
|
117
|
+
if (part) return this._linkPart(part, RT.EXTENSIBLE);
|
|
118
|
+
|
|
119
|
+
const partname = this.doc.pkg.nextPartname('/word/commentsExtensible%d.xml');
|
|
120
|
+
const xml = `<w16cex:commentsExtensible xmlns:w16cex="${NS.w16cex}"></w16cex:commentsExtensible>`;
|
|
121
|
+
part = this.doc.pkg.addPart(partname, CT.EXTENSIBLE, xml);
|
|
122
|
+
this.doc.relateTo(part, RT.EXTENSIBLE);
|
|
123
|
+
return part;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private _ensureNamespaces() {
|
|
127
|
+
// In TS we use full xml reconstruction if attributes are missing, but xmldom generally tolerates
|
|
128
|
+
// runtime attributes if the namespace is declared. For absolute safety, if it's completely missing,
|
|
129
|
+
// we would rebuild. Assuming the parser caught them if they existed.
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _getNextCommentId(): number {
|
|
133
|
+
const ids = [0];
|
|
134
|
+
const part = this._getExistingPartByType(CT.COMMENTS);
|
|
135
|
+
if (part) {
|
|
136
|
+
const comments = findAllDescendants(part._element, 'w:comment');
|
|
137
|
+
for (const c of comments) {
|
|
138
|
+
const idStr = c.getAttribute('w:id');
|
|
139
|
+
if (idStr) ids.push(parseInt(idStr, 10) || 0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return Math.max(...ids) + 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private _generateHexId(): string {
|
|
146
|
+
return Math.floor(Math.random() * 0xFFFFFFFF).toString(16).toUpperCase().padStart(8, '0');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private _getInitials(author: string): string {
|
|
150
|
+
if (!author) return '';
|
|
151
|
+
return author.split(' ').filter(Boolean).map(p => p[0]).join('').toUpperCase();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private _findParaIdForComment(commentId: string): string | null {
|
|
155
|
+
if (!this._commentsPart) return null;
|
|
156
|
+
for (const c of findAllDescendants(this._commentsPart._element, 'w:comment')) {
|
|
157
|
+
if (c.getAttribute('w:id') === commentId) {
|
|
158
|
+
for (const p of findAllDescendants(c, 'w:p')) {
|
|
159
|
+
const pid = p.getAttribute('w14:paraId');
|
|
160
|
+
if (pid) return pid;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private _findThreadRootParaId(commentId: string): string | null {
|
|
168
|
+
const directParaId = this._findParaIdForComment(commentId);
|
|
169
|
+
const extPart = this._getExistingPartByType(CT.EXTENDED);
|
|
170
|
+
if (!directParaId || !extPart) return directParaId;
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < extPart._element.childNodes.length; i++) {
|
|
173
|
+
const child = extPart._element.childNodes[i] as Element;
|
|
174
|
+
if (child.nodeType !== 1) continue;
|
|
175
|
+
if (child.getAttribute('w15:paraId') === directParaId) {
|
|
176
|
+
const parent = child.getAttribute('w15:paraIdParent');
|
|
177
|
+
if (parent) return parent;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return directParaId;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public addComment(author: string, text: string, parentId: string | null = null): string {
|
|
184
|
+
const commentId = this.nextId.toString();
|
|
185
|
+
this.nextId++;
|
|
186
|
+
const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
187
|
+
|
|
188
|
+
const doc = this.commentsPart._element.ownerDocument!;
|
|
189
|
+
const comment = doc.createElement('w:comment');
|
|
190
|
+
comment.setAttribute('w:id', commentId);
|
|
191
|
+
comment.setAttribute('w:author', author);
|
|
192
|
+
comment.setAttribute('w:date', now);
|
|
193
|
+
|
|
194
|
+
const initials = this._getInitials(author);
|
|
195
|
+
if (initials) comment.setAttribute('w:initials', initials);
|
|
196
|
+
|
|
197
|
+
const extPart = this._getExistingPartByType(CT.EXTENDED);
|
|
198
|
+
if (parentId && !extPart) {
|
|
199
|
+
comment.setAttribute('w15:p', parentId);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const paraId = this._generateHexId();
|
|
203
|
+
const rsid = this._generateHexId();
|
|
204
|
+
|
|
205
|
+
const p = doc.createElement('w:p');
|
|
206
|
+
p.setAttribute('w14:paraId', paraId);
|
|
207
|
+
p.setAttribute('w14:textId', '77777777');
|
|
208
|
+
p.setAttribute('w:rsidR', rsid);
|
|
209
|
+
p.setAttribute('w:rsidRDefault', rsid);
|
|
210
|
+
p.setAttribute('w:rsidP', rsid);
|
|
211
|
+
|
|
212
|
+
const pPr = doc.createElement('w:pPr');
|
|
213
|
+
const pStyle = doc.createElement('w:pStyle');
|
|
214
|
+
pStyle.setAttribute('w:val', 'CommentText');
|
|
215
|
+
pPr.appendChild(pStyle);
|
|
216
|
+
p.appendChild(pPr);
|
|
217
|
+
|
|
218
|
+
const rRef = doc.createElement('w:r');
|
|
219
|
+
const rPrRef = doc.createElement('w:rPr');
|
|
220
|
+
const rStyleRef = doc.createElement('w:rStyle');
|
|
221
|
+
rStyleRef.setAttribute('w:val', 'CommentReference');
|
|
222
|
+
rPrRef.appendChild(rStyleRef);
|
|
223
|
+
rRef.appendChild(rPrRef);
|
|
224
|
+
rRef.appendChild(doc.createElement('w:annotationRef'));
|
|
225
|
+
p.appendChild(rRef);
|
|
226
|
+
|
|
227
|
+
const r = doc.createElement('w:r');
|
|
228
|
+
const t = doc.createElement('w:t');
|
|
229
|
+
t.textContent = text;
|
|
230
|
+
r.appendChild(t);
|
|
231
|
+
p.appendChild(r);
|
|
232
|
+
comment.appendChild(p);
|
|
233
|
+
|
|
234
|
+
this.commentsPart._element.appendChild(comment);
|
|
235
|
+
|
|
236
|
+
if (this.extendedPart) {
|
|
237
|
+
const parentParaId = parentId ? this._findThreadRootParaId(parentId) : null;
|
|
238
|
+
const exDoc = this.extendedPart._element.ownerDocument!;
|
|
239
|
+
const commentEx = exDoc.createElement('w15:commentEx');
|
|
240
|
+
commentEx.setAttribute('w15:paraId', paraId);
|
|
241
|
+
if (parentParaId) commentEx.setAttribute('w15:paraIdParent', parentParaId);
|
|
242
|
+
commentEx.setAttribute('w15:done', '0');
|
|
243
|
+
this.extendedPart._element.appendChild(commentEx);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (this.idsPart) {
|
|
247
|
+
const idsDoc = this.idsPart._element.ownerDocument!;
|
|
248
|
+
const commentIdEl = idsDoc.createElement('w16cid:commentId');
|
|
249
|
+
commentIdEl.setAttribute('w16cid:paraId', paraId);
|
|
250
|
+
commentIdEl.setAttribute('w16cid:durableId', this._generateHexId());
|
|
251
|
+
this.idsPart._element.appendChild(commentIdEl);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (this.extensiblePart) {
|
|
255
|
+
let durableId: string | null = null;
|
|
256
|
+
for (let i = 0; i < this.idsPart._element.childNodes.length; i++) {
|
|
257
|
+
const child = this.idsPart._element.childNodes[i] as Element;
|
|
258
|
+
if (child.nodeType === 1 && child.getAttribute('w16cid:paraId') === paraId) {
|
|
259
|
+
durableId = child.getAttribute('w16cid:durableId');
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (durableId) {
|
|
264
|
+
const cexDoc = this.extensiblePart._element.ownerDocument!;
|
|
265
|
+
const extEl = cexDoc.createElement('w16cex:commentExtensible');
|
|
266
|
+
extEl.setAttribute('w16cex:durableId', durableId);
|
|
267
|
+
extEl.setAttribute('w16cex:dateUtc', now);
|
|
268
|
+
this.extensiblePart._element.appendChild(extEl);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return commentId;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public deleteComment(commentId: string) {
|
|
276
|
+
if (!this._commentsPart) return;
|
|
277
|
+
|
|
278
|
+
let commentEl: Element | null = null;
|
|
279
|
+
for (const c of findAllDescendants(this._commentsPart._element, 'w:comment')) {
|
|
280
|
+
if (c.getAttribute('w:id') === commentId) {
|
|
281
|
+
commentEl = c;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!commentEl) return;
|
|
287
|
+
|
|
288
|
+
let paraId: string | null = null;
|
|
289
|
+
for (const p of findAllDescendants(commentEl, 'w:p')) {
|
|
290
|
+
const pid = p.getAttribute('w14:paraId');
|
|
291
|
+
if (pid) {
|
|
292
|
+
paraId = pid;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (paraId) {
|
|
298
|
+
const repliesToDelete: string[] = [];
|
|
299
|
+
if (this.extendedPart) {
|
|
300
|
+
for (let i = 0; i < this.extendedPart._element.childNodes.length; i++) {
|
|
301
|
+
const child = this.extendedPart._element.childNodes[i] as Element;
|
|
302
|
+
if (child.nodeType !== 1) continue;
|
|
303
|
+
|
|
304
|
+
if (child.getAttribute('w15:paraIdParent') === paraId) {
|
|
305
|
+
const childParaId = child.getAttribute('w15:paraId');
|
|
306
|
+
if (childParaId) {
|
|
307
|
+
for (const c of findAllDescendants(this._commentsPart._element, 'w:comment')) {
|
|
308
|
+
for (const p of findAllDescendants(c, 'w:p')) {
|
|
309
|
+
if (p.getAttribute('w14:paraId') === childParaId) {
|
|
310
|
+
const cid = c.getAttribute('w:id');
|
|
311
|
+
if (cid) repliesToDelete.push(cid);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const repId of repliesToDelete) {
|
|
322
|
+
this.deleteComment(repId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let durableId: string | null = null;
|
|
326
|
+
|
|
327
|
+
if (this.idsPart) {
|
|
328
|
+
const toRemove: Element[] = [];
|
|
329
|
+
for (let i = 0; i < this.idsPart._element.childNodes.length; i++) {
|
|
330
|
+
const child = this.idsPart._element.childNodes[i] as Element;
|
|
331
|
+
if (child.nodeType === 1 && child.getAttribute('w16cid:paraId') === paraId) {
|
|
332
|
+
durableId = child.getAttribute('w16cid:durableId');
|
|
333
|
+
toRemove.push(child);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
toRemove.forEach(c => this.idsPart!._element.removeChild(c));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (this.extendedPart) {
|
|
340
|
+
const toRemove: Element[] = [];
|
|
341
|
+
for (let i = 0; i < this.extendedPart._element.childNodes.length; i++) {
|
|
342
|
+
const child = this.extendedPart._element.childNodes[i] as Element;
|
|
343
|
+
if (child.nodeType === 1 && child.getAttribute('w15:paraId') === paraId) {
|
|
344
|
+
toRemove.push(child);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
toRemove.forEach(c => this.extendedPart!._element.removeChild(c));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (durableId && this.extensiblePart) {
|
|
351
|
+
const toRemove: Element[] = [];
|
|
352
|
+
for (let i = 0; i < this.extensiblePart._element.childNodes.length; i++) {
|
|
353
|
+
const child = this.extensiblePart._element.childNodes[i] as Element;
|
|
354
|
+
if (child.nodeType === 1 && child.getAttribute('w16cex:durableId') === durableId) {
|
|
355
|
+
toRemove.push(child);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
toRemove.forEach(c => this.extensiblePart!._element.removeChild(c));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (commentEl.parentNode) {
|
|
363
|
+
commentEl.parentNode.removeChild(commentEl);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Keep the global extraction function matching Python behavior
|
|
369
|
+
export function extract_comments_data(pkg: DocxPackage): Record<string, any> {
|
|
370
|
+
// Temporary bridge to use the new class
|
|
371
|
+
const docObj = {
|
|
372
|
+
pkg,
|
|
373
|
+
part: pkg.mainDocumentPart,
|
|
374
|
+
relateTo: () => {} // Mock since extraction is read-only
|
|
375
|
+
} as unknown as DocumentObject;
|
|
376
|
+
|
|
377
|
+
const mgr = new CommentsManager(docObj);
|
|
378
|
+
const data: Record<string, any> = {};
|
|
379
|
+
|
|
380
|
+
const part = pkg.parts.find(p => p.contentType === CT.COMMENTS);
|
|
381
|
+
if (!part) return data;
|
|
382
|
+
|
|
383
|
+
const para_id_to_cid: Record<string, string> = {};
|
|
384
|
+
const comments = findAllDescendants(part._element, 'w:comment');
|
|
385
|
+
|
|
386
|
+
for (const c of comments) {
|
|
387
|
+
const c_id = c.getAttribute('w:id');
|
|
388
|
+
if (!c_id) continue;
|
|
389
|
+
|
|
390
|
+
const c_author = c.getAttribute('w:author') || 'Unknown';
|
|
391
|
+
const c_date = c.getAttribute('w:date') || '';
|
|
392
|
+
|
|
393
|
+
let is_resolved = false;
|
|
394
|
+
const val = c.getAttribute('w15:done');
|
|
395
|
+
if (val === '1' || val === 'true' || val === 'on') is_resolved = true;
|
|
396
|
+
|
|
397
|
+
let parent_id = c.getAttribute('w15:p') || null;
|
|
398
|
+
|
|
399
|
+
const p_elems = findAllDescendants(c, 'w:p');
|
|
400
|
+
for (const p of p_elems) {
|
|
401
|
+
const pid = p.getAttribute('w14:paraId');
|
|
402
|
+
if (pid) para_id_to_cid[pid] = c_id;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const text_parts: string[] = [];
|
|
406
|
+
for (const p of p_elems) {
|
|
407
|
+
const t_elems = findAllDescendants(p, 'w:t');
|
|
408
|
+
for (const t of t_elems) {
|
|
409
|
+
if (t.textContent) text_parts.push(t.textContent);
|
|
410
|
+
}
|
|
411
|
+
text_parts.push('\n');
|
|
412
|
+
}
|
|
413
|
+
const full_text = text_parts.join('').trim();
|
|
414
|
+
|
|
415
|
+
data[c_id] = {
|
|
416
|
+
author: c_author,
|
|
417
|
+
text: full_text,
|
|
418
|
+
date: c_date,
|
|
419
|
+
resolved: is_resolved,
|
|
420
|
+
parent_id: parent_id,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const extPart = pkg.parts.find(p => p.contentType === CT.EXTENDED);
|
|
425
|
+
if (extPart) {
|
|
426
|
+
const children = extPart._element.childNodes;
|
|
427
|
+
for (let i = 0; i < children.length; i++) {
|
|
428
|
+
const child = children[i] as Element;
|
|
429
|
+
if (child.nodeType !== 1) continue;
|
|
430
|
+
|
|
431
|
+
const para_id = child.getAttribute('w15:paraId');
|
|
432
|
+
const parent_para_id = child.getAttribute('w15:paraIdParent');
|
|
433
|
+
const done_val = child.getAttribute('w15:done');
|
|
434
|
+
|
|
435
|
+
if (para_id) {
|
|
436
|
+
const c_id = para_id_to_cid[para_id];
|
|
437
|
+
if (c_id && data[c_id]) {
|
|
438
|
+
if (parent_para_id) {
|
|
439
|
+
const p_id = para_id_to_cid[parent_para_id];
|
|
440
|
+
if (p_id) data[c_id].parent_id = p_id;
|
|
441
|
+
}
|
|
442
|
+
if (done_val === '1' || done_val === 'true' || done_val === 'on') {
|
|
443
|
+
data[c_id].resolved = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return data;
|
|
451
451
|
}
|