@adeu/core 1.6.6 → 1.6.8
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 +1774 -957
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -8
- package/dist/index.d.ts +21 -8
- package/dist/index.js +1772 -957
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +54 -0
- package/src/docx/bridge.ts +15 -3
- package/src/domain.test.ts +280 -0
- package/src/domain.ts +264 -10
- package/src/index.ts +7 -8
- package/src/ingest.ts +8 -0
- package/src/sanitize/core.ts +104 -0
- package/src/sanitize/report.ts +125 -0
- package/src/sanitize/sanitize.test.ts +192 -0
- package/src/sanitize/transforms.ts +365 -0
- package/src/utils/docx.ts +12 -3
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { DocumentObject, Part } from '../docx/bridge.js';
|
|
2
|
+
import { findAllDescendants, findChild, findChildren } from '../docx/dom.js';
|
|
3
|
+
import { extract_comments_data, CommentsManager } from '../comments.js';
|
|
4
|
+
import { RedlineEngine } from '../engine.js';
|
|
5
|
+
|
|
6
|
+
export function findDescendantsByLocalName(element: Element, localName: string): Element[] {
|
|
7
|
+
const result: Element[] = [];
|
|
8
|
+
const all = element.getElementsByTagName('*');
|
|
9
|
+
for (let i = 0; i < all.length; i++) {
|
|
10
|
+
const tag = all[i].tagName;
|
|
11
|
+
if (tag === localName || tag.endsWith(':' + localName)) {
|
|
12
|
+
result.push(all[i]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function strip_rsid(doc: DocumentObject): string[] {
|
|
19
|
+
let count = 0;
|
|
20
|
+
const rsidAttrs = ['w:rsidR', 'w:rsidRPr', 'w:rsidRDefault', 'w:rsidP', 'w:rsidDel', 'w:rsidSect', 'w:rsidTr'];
|
|
21
|
+
|
|
22
|
+
const all = doc.element.getElementsByTagName('*');
|
|
23
|
+
for (let i = 0; i < all.length; i++) {
|
|
24
|
+
for (const attr of rsidAttrs) {
|
|
25
|
+
if (all[i].hasAttribute(attr)) {
|
|
26
|
+
all[i].removeAttribute(attr);
|
|
27
|
+
count++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rsidsElements = findAllDescendants(doc.element, 'w:rsids');
|
|
33
|
+
for (const el of rsidsElements) {
|
|
34
|
+
if (el.parentNode) {
|
|
35
|
+
el.parentNode.removeChild(el);
|
|
36
|
+
count++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return count ? [`rsid attributes: ${count} removed`] : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function strip_para_ids(doc: DocumentObject): string[] {
|
|
44
|
+
let count = 0;
|
|
45
|
+
const attrs = ['w14:paraId', 'w14:textId'];
|
|
46
|
+
const all = doc.element.getElementsByTagName('*');
|
|
47
|
+
for (let i = 0; i < all.length; i++) {
|
|
48
|
+
for (const attr of attrs) {
|
|
49
|
+
if (all[i].hasAttribute(attr)) {
|
|
50
|
+
all[i].removeAttribute(attr);
|
|
51
|
+
count++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return count ? [`Paragraph/text IDs: ${count} removed`] : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function strip_proof_errors(doc: DocumentObject): string[] {
|
|
59
|
+
const elements = findAllDescendants(doc.element, 'w:proofErr');
|
|
60
|
+
elements.forEach(el => el.parentNode?.removeChild(el));
|
|
61
|
+
return elements.length ? [`Spell check markers: ${elements.length} removed`] : [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function strip_empty_properties(doc: DocumentObject): string[] {
|
|
65
|
+
let count = 0;
|
|
66
|
+
for (const tag of ['w:rPr', 'w:pPr']) {
|
|
67
|
+
const elements = findAllDescendants(doc.element, tag);
|
|
68
|
+
for (const el of elements) {
|
|
69
|
+
if (el.childNodes.length === 0 || (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3 && !el.childNodes[0].textContent?.trim())) {
|
|
70
|
+
el.parentNode?.removeChild(el);
|
|
71
|
+
count++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return count ? [`Empty property elements: ${count} removed`] : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function strip_hidden_text(doc: DocumentObject): string[] {
|
|
79
|
+
let count = 0;
|
|
80
|
+
const elements = findAllDescendants(doc.element, 'w:rPr');
|
|
81
|
+
for (const rPr of elements) {
|
|
82
|
+
if (findChild(rPr, 'w:vanish') || findChild(rPr, 'w:webHidden')) {
|
|
83
|
+
const run = rPr.parentNode as Element;
|
|
84
|
+
if (run && run.tagName === 'w:r' && run.parentNode) {
|
|
85
|
+
run.parentNode.removeChild(run);
|
|
86
|
+
count++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return count ? [`Hidden text runs: ${count} removed`] : [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function count_tracked_changes(doc: DocumentObject): [number, number, number] {
|
|
94
|
+
const ins = findAllDescendants(doc.element, 'w:ins').length;
|
|
95
|
+
const del = findAllDescendants(doc.element, 'w:del').length;
|
|
96
|
+
const fmt = findAllDescendants(doc.element, 'w:rPrChange').length +
|
|
97
|
+
findAllDescendants(doc.element, 'w:pPrChange').length +
|
|
98
|
+
findAllDescendants(doc.element, 'w:sectPrChange').length;
|
|
99
|
+
return [ins, del, fmt];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function get_track_change_authors(doc: DocumentObject): Set<string> {
|
|
103
|
+
const authors = new Set<string>();
|
|
104
|
+
for (const tag of ['w:ins', 'w:del', 'w:rPrChange', 'w:pPrChange', 'w:sectPrChange']) {
|
|
105
|
+
for (const el of findAllDescendants(doc.element, tag)) {
|
|
106
|
+
const author = el.getAttribute('w:author');
|
|
107
|
+
if (author) authors.add(author);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return authors;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _getElementText(el: Element): string {
|
|
114
|
+
const texts: string[] = [];
|
|
115
|
+
const ts = findAllDescendants(el, 'w:t');
|
|
116
|
+
for (const t of ts) if (t.textContent) texts.push(t.textContent);
|
|
117
|
+
const dts = findAllDescendants(el, 'w:delText');
|
|
118
|
+
for (const dt of dts) if (dt.textContent) texts.push(dt.textContent);
|
|
119
|
+
return texts.join('');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function _truncate(text: string, maxLen: number = 60): string {
|
|
123
|
+
const clean = text.replace(/\n/g, ' ').trim();
|
|
124
|
+
if (clean.length <= maxLen) return clean;
|
|
125
|
+
return clean.substring(0, maxLen - 3) + "...";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function accept_all_tracked_changes(doc: DocumentObject): string[] {
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
const insEls = findAllDescendants(doc.element, 'w:ins');
|
|
131
|
+
const delEls = findAllDescendants(doc.element, 'w:del');
|
|
132
|
+
|
|
133
|
+
for (const ins of insEls) {
|
|
134
|
+
const text = _getElementText(ins).trim();
|
|
135
|
+
if (text) lines.push(` Accepted insertion: "${_truncate(text, 60)}"`);
|
|
136
|
+
}
|
|
137
|
+
for (const del of delEls) {
|
|
138
|
+
const text = _getElementText(del).trim();
|
|
139
|
+
if (text) lines.push(` Accepted deletion of: "${_truncate(text, 60)}"`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const engine = new RedlineEngine(doc);
|
|
143
|
+
engine.accept_all_revisions();
|
|
144
|
+
|
|
145
|
+
for (const tag of ['w:rPrChange', 'w:pPrChange', 'w:sectPrChange']) {
|
|
146
|
+
for (const el of findAllDescendants(doc.element, tag)) {
|
|
147
|
+
el.parentNode?.removeChild(el);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const total = insEls.length + delEls.length;
|
|
152
|
+
if (total) {
|
|
153
|
+
return [`Tracked changes auto-accepted: ${total}`].concat(lines);
|
|
154
|
+
}
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function get_comments_summary(doc: DocumentObject): any {
|
|
159
|
+
const data = extract_comments_data(doc.pkg);
|
|
160
|
+
const comments = [];
|
|
161
|
+
let openCount = 0;
|
|
162
|
+
let resolvedCount = 0;
|
|
163
|
+
|
|
164
|
+
for (const [cId, info] of Object.entries(data)) {
|
|
165
|
+
if (info.resolved) resolvedCount++;
|
|
166
|
+
else openCount++;
|
|
167
|
+
comments.push({ id: cId, ...info });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { total: comments.length, open: openCount, resolved: resolvedCount, comments };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function remove_all_comments(doc: DocumentObject): string[] {
|
|
174
|
+
const data = extract_comments_data(doc.pkg);
|
|
175
|
+
const keys = Object.keys(data);
|
|
176
|
+
if (keys.length === 0) return [];
|
|
177
|
+
|
|
178
|
+
const lines: string[] = [];
|
|
179
|
+
const cm = new CommentsManager(doc);
|
|
180
|
+
|
|
181
|
+
for (const [cId, info] of Object.entries(data)) {
|
|
182
|
+
const status = info.resolved ? "[Resolved]" : "[Open]";
|
|
183
|
+
lines.push(` ${status} "${_truncate(info.text || '', 60)}" (${info.author || 'Unknown'})`);
|
|
184
|
+
cm.deleteComment(cId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const tag of ['w:commentRangeStart', 'w:commentRangeEnd', 'w:commentReference']) {
|
|
188
|
+
for (const el of findAllDescendants(doc.element, tag)) {
|
|
189
|
+
el.parentNode?.removeChild(el);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const resolvedCount = Object.values(data).filter(c => c.resolved).length;
|
|
194
|
+
const openCount = Object.values(data).filter(c => !c.resolved).length;
|
|
195
|
+
return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function replace_comment_authors(doc: DocumentObject, newAuthor: string): string[] {
|
|
199
|
+
const cm = new CommentsManager(doc);
|
|
200
|
+
if (!cm.commentsPart) return [];
|
|
201
|
+
|
|
202
|
+
const original = new Set<string>();
|
|
203
|
+
const comments = findAllDescendants(cm.commentsPart._element, 'w:comment');
|
|
204
|
+
for (const c of comments) {
|
|
205
|
+
const author = c.getAttribute('w:author');
|
|
206
|
+
if (author) {
|
|
207
|
+
original.add(author);
|
|
208
|
+
c.setAttribute('w:author', newAuthor);
|
|
209
|
+
}
|
|
210
|
+
if (c.hasAttribute('w:initials')) {
|
|
211
|
+
const initials = newAuthor.split(' ').filter(Boolean).map(p => p[0]).join('').toUpperCase();
|
|
212
|
+
c.setAttribute('w:initials', initials);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return original.size ? [`Comment authors replaced: ${Array.from(original).sort().join(', ')} → "${newAuthor}"`] : [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function replace_change_authors(doc: DocumentObject, newAuthor: string): string[] {
|
|
219
|
+
const original = new Set<string>();
|
|
220
|
+
for (const tag of ['w:ins', 'w:del', 'w:rPrChange', 'w:pPrChange']) {
|
|
221
|
+
for (const el of findAllDescendants(doc.element, tag)) {
|
|
222
|
+
const author = el.getAttribute('w:author');
|
|
223
|
+
if (author) {
|
|
224
|
+
original.add(author);
|
|
225
|
+
el.setAttribute('w:author', newAuthor);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return original.size ? [`Track change authors replaced: ${Array.from(original).sort().join(', ')} → "${newAuthor}"`] : [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function normalize_change_dates(doc: DocumentObject): string[] {
|
|
233
|
+
let count = 0;
|
|
234
|
+
const fixed = "2025-01-01T00:00:00Z";
|
|
235
|
+
for (const tag of ['w:ins', 'w:del', 'w:rPrChange', 'w:pPrChange']) {
|
|
236
|
+
for (const el of findAllDescendants(doc.element, tag)) {
|
|
237
|
+
if (el.hasAttribute('w:date')) {
|
|
238
|
+
el.setAttribute('w:date', fixed);
|
|
239
|
+
count++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return count ? [`Track change timestamps: ${count} normalized`] : [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function scrub_doc_properties(doc: DocumentObject): string[] {
|
|
247
|
+
const lines: string[] = [];
|
|
248
|
+
const corePart = doc.pkg.getPartByPath('docProps/core.xml');
|
|
249
|
+
if (corePart) {
|
|
250
|
+
const creators = findDescendantsByLocalName(corePart._element, 'creator');
|
|
251
|
+
creators.forEach(c => { if (c.textContent) { lines.push(`Author: ${c.textContent}`); c.textContent = ""; }});
|
|
252
|
+
|
|
253
|
+
const modifiers = findDescendantsByLocalName(corePart._element, 'lastModifiedBy');
|
|
254
|
+
modifiers.forEach(c => { if (c.textContent) { lines.push(`Last modified by: ${c.textContent}`); c.textContent = ""; }});
|
|
255
|
+
|
|
256
|
+
const revisions = findDescendantsByLocalName(corePart._element, 'revision');
|
|
257
|
+
revisions.forEach(c => { if (c.textContent && parseInt(c.textContent) > 1) { lines.push(`Revision count: ${c.textContent} → 1`); c.textContent = "1"; }});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const appPart = doc.pkg.getPartByPath('docProps/app.xml');
|
|
261
|
+
if (appPart) {
|
|
262
|
+
const docEl = appPart._element;
|
|
263
|
+
const intFields = ["TotalTime", "Words", "Characters", "Paragraphs", "Lines", "CharactersWithSpaces"];
|
|
264
|
+
for (const f of intFields) {
|
|
265
|
+
findDescendantsByLocalName(docEl, f).forEach(el => {
|
|
266
|
+
if (el.textContent && el.textContent !== "0") {
|
|
267
|
+
if (f === "TotalTime") lines.push(`Total editing time: ${el.textContent} minutes`);
|
|
268
|
+
el.textContent = "0";
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const strFields = ["Template", "Manager", "Company"];
|
|
273
|
+
for (const f of strFields) {
|
|
274
|
+
findDescendantsByLocalName(docEl, f).forEach(el => {
|
|
275
|
+
if (el.textContent) {
|
|
276
|
+
lines.push(`${f}: ${el.textContent}`);
|
|
277
|
+
el.textContent = "";
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return lines.length ? ["Metadata scrubbed:", ...lines.map(l => ` ${l}`)] : [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function scrub_timestamps(doc: DocumentObject): string[] {
|
|
287
|
+
let modified = false;
|
|
288
|
+
const epoch = "1970-01-01T00:00:00Z";
|
|
289
|
+
const corePart = doc.pkg.getPartByPath('docProps/core.xml');
|
|
290
|
+
if (corePart) {
|
|
291
|
+
for (const tag of ['created', 'modified', 'lastPrinted']) {
|
|
292
|
+
findDescendantsByLocalName(corePart._element, tag).forEach(el => {
|
|
293
|
+
if (el.textContent && el.textContent !== epoch) {
|
|
294
|
+
el.textContent = epoch;
|
|
295
|
+
modified = true;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return modified ? ["Timestamps normalized to epoch"] : [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function strip_custom_xml(doc: DocumentObject): string[] {
|
|
304
|
+
const customParts = doc.pkg.parts.filter(p => p.partname.includes('/customXml'));
|
|
305
|
+
if (customParts.length === 0) return [];
|
|
306
|
+
|
|
307
|
+
const partnames = new Set(customParts.map(p => p.partname));
|
|
308
|
+
doc.pkg.parts = doc.pkg.parts.filter(p => !partnames.has(p.partname));
|
|
309
|
+
|
|
310
|
+
const removeRelationsTo = (relsPart: Part) => {
|
|
311
|
+
const toRemove: Element[] = [];
|
|
312
|
+
for (const rel of findAllDescendants(relsPart._element, 'Relationship')) {
|
|
313
|
+
const target = rel.getAttribute('Target');
|
|
314
|
+
if (target && target.includes('customXml')) toRemove.push(rel);
|
|
315
|
+
}
|
|
316
|
+
toRemove.forEach(r => r.parentNode?.removeChild(r));
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const rootRels = doc.pkg.getPartByPath('_rels/.rels');
|
|
320
|
+
if (rootRels) removeRelationsTo(rootRels);
|
|
321
|
+
|
|
322
|
+
const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
|
|
323
|
+
if (docRels) removeRelationsTo(docRels);
|
|
324
|
+
|
|
325
|
+
for (const sdtPr of findAllDescendants(doc.element, 'w:sdtPr')) {
|
|
326
|
+
findChildren(sdtPr, 'w:dataBinding').forEach(b => sdtPr.removeChild(b));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return [`Custom XML parts: ${customParts.length} removed`];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function strip_image_alt_text(doc: DocumentObject): string[] {
|
|
333
|
+
let count = 0;
|
|
334
|
+
for (const docPr of findDescendantsByLocalName(doc.element, 'docPr')) {
|
|
335
|
+
const descr = docPr.getAttribute('descr');
|
|
336
|
+
if (descr) {
|
|
337
|
+
const isShort = descr.length < 10;
|
|
338
|
+
const isFile = descr.includes('.') && descr.length < 60;
|
|
339
|
+
if (isShort || isFile) {
|
|
340
|
+
docPr.removeAttribute('descr');
|
|
341
|
+
count++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function audit_hyperlinks(doc: DocumentObject): string[] {
|
|
349
|
+
const internal = ["sharepoint.com", "onedrive.com", ".internal", "intranet", "localhost", "10.", "192.168.", "172.16."];
|
|
350
|
+
const warnings: string[] = [];
|
|
351
|
+
|
|
352
|
+
const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
|
|
353
|
+
for (const rel of findAllDescendants(docRels._element, 'Relationship')) {
|
|
354
|
+
if (rel.getAttribute('TargetMode') === 'External') {
|
|
355
|
+
const url = rel.getAttribute('Target') || '';
|
|
356
|
+
for (const pattern of internal) {
|
|
357
|
+
if (url.toLowerCase().includes(pattern.toLowerCase())) {
|
|
358
|
+
warnings.push(`Hyperlink targets internal URL: ${_truncate(url, 80)}`);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return warnings;
|
|
365
|
+
}
|
package/src/utils/docx.ts
CHANGED
|
@@ -370,6 +370,14 @@ function _is_page_instr(instr: string): boolean {
|
|
|
370
370
|
return parts.length > 0 && (parts[0] === 'PAGE' || parts[0] === 'NUMPAGES');
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
export function _get_part(parent: any): any {
|
|
374
|
+
if (!parent) return null;
|
|
375
|
+
if (parent.part) return parent.part;
|
|
376
|
+
if (parent.pkg && parent.pkg.mainDocumentPart) return parent.pkg.mainDocumentPart;
|
|
377
|
+
if (parent._parent) return _get_part(parent._parent);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
373
381
|
export function* iter_paragraph_content(paragraph: Paragraph): Generator<Run | DocxEvent> {
|
|
374
382
|
let in_complex_field = false;
|
|
375
383
|
let current_instr = '';
|
|
@@ -449,10 +457,11 @@ export function* iter_paragraph_content(paragraph: Paragraph): Generator<Run | D
|
|
|
449
457
|
} else if (tag === QN_W_COMMENTRANGESTART) yield { type: 'start', id: child.getAttribute(QN_W_ID)! };
|
|
450
458
|
else if (tag === QN_W_COMMENTRANGEEND) yield { type: 'end', id: child.getAttribute(QN_W_ID)! };
|
|
451
459
|
else if (tag === QN_W_HYPERLINK) {
|
|
452
|
-
const rId = child.getAttribute(QN_R_ID);
|
|
460
|
+
const rId = child.getAttribute(QN_R_ID) || child.getAttribute('id');
|
|
453
461
|
let url = '';
|
|
454
|
-
|
|
455
|
-
|
|
462
|
+
const part = _get_part(paragraph._parent);
|
|
463
|
+
if (rId && part) {
|
|
464
|
+
const rel = part.rels.get(rId);
|
|
456
465
|
if (rel && rel.isExternal) url = rel.target;
|
|
457
466
|
}
|
|
458
467
|
if (url) yield { type: 'hyperlink_start', id: rId!, date: url };
|