@usejunior/docx-core 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +86 -28
- package/dist/.tsbuildinfo +1 -0
- package/dist/atomizer.d.ts +218 -0
- package/dist/atomizer.d.ts.map +1 -0
- package/dist/atomizer.js +856 -0
- package/dist/atomizer.js.map +1 -0
- package/dist/baselines/atomizer/atomLcs.d.ts +96 -0
- package/dist/baselines/atomizer/atomLcs.d.ts.map +1 -0
- package/dist/baselines/atomizer/atomLcs.js +347 -0
- package/dist/baselines/atomizer/atomLcs.js.map +1 -0
- package/dist/baselines/atomizer/debug.d.ts +41 -0
- package/dist/baselines/atomizer/debug.d.ts.map +1 -0
- package/dist/baselines/atomizer/debug.js +85 -0
- package/dist/baselines/atomizer/debug.js.map +1 -0
- package/dist/baselines/atomizer/documentReconstructor.d.ts +64 -0
- package/dist/baselines/atomizer/documentReconstructor.d.ts.map +1 -0
- package/dist/baselines/atomizer/documentReconstructor.js +939 -0
- package/dist/baselines/atomizer/documentReconstructor.js.map +1 -0
- package/dist/baselines/atomizer/hierarchicalLcs.d.ts +111 -0
- package/dist/baselines/atomizer/hierarchicalLcs.d.ts.map +1 -0
- package/dist/baselines/atomizer/hierarchicalLcs.js +469 -0
- package/dist/baselines/atomizer/hierarchicalLcs.js.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier.d.ts +183 -0
- package/dist/baselines/atomizer/inPlaceModifier.d.ts.map +1 -0
- package/dist/baselines/atomizer/inPlaceModifier.js +1600 -0
- package/dist/baselines/atomizer/inPlaceModifier.js.map +1 -0
- package/dist/baselines/atomizer/numberingIntegration.d.ts +59 -0
- package/dist/baselines/atomizer/numberingIntegration.d.ts.map +1 -0
- package/dist/baselines/atomizer/numberingIntegration.js +209 -0
- package/dist/baselines/atomizer/numberingIntegration.js.map +1 -0
- package/dist/baselines/atomizer/pipeline.d.ts +65 -0
- package/dist/baselines/atomizer/pipeline.d.ts.map +1 -0
- package/dist/baselines/atomizer/pipeline.js +510 -0
- package/dist/baselines/atomizer/pipeline.js.map +1 -0
- package/dist/baselines/atomizer/premergeRuns.d.ts +26 -0
- package/dist/baselines/atomizer/premergeRuns.d.ts.map +1 -0
- package/dist/baselines/atomizer/premergeRuns.js +150 -0
- package/dist/baselines/atomizer/premergeRuns.js.map +1 -0
- package/dist/baselines/atomizer/trackChangesAcceptor.d.ts +63 -0
- package/dist/baselines/atomizer/trackChangesAcceptor.d.ts.map +1 -0
- package/dist/baselines/atomizer/trackChangesAcceptor.js +254 -0
- package/dist/baselines/atomizer/trackChangesAcceptor.js.map +1 -0
- package/dist/baselines/atomizer/trackChangesAcceptorAst.d.ts +64 -0
- package/dist/baselines/atomizer/trackChangesAcceptorAst.d.ts.map +1 -0
- package/dist/baselines/atomizer/trackChangesAcceptorAst.js +586 -0
- package/dist/baselines/atomizer/trackChangesAcceptorAst.js.map +1 -0
- package/dist/baselines/atomizer/xmlToWmlElement.d.ts +65 -0
- package/dist/baselines/atomizer/xmlToWmlElement.d.ts.map +1 -0
- package/dist/baselines/atomizer/xmlToWmlElement.js +95 -0
- package/dist/baselines/atomizer/xmlToWmlElement.js.map +1 -0
- package/dist/baselines/diffmatch/documentBuilder.d.ts +44 -0
- package/dist/baselines/diffmatch/documentBuilder.d.ts.map +1 -0
- package/dist/baselines/diffmatch/documentBuilder.js +227 -0
- package/dist/baselines/diffmatch/documentBuilder.js.map +1 -0
- package/dist/baselines/diffmatch/paragraphAlignment.d.ts +75 -0
- package/dist/baselines/diffmatch/paragraphAlignment.d.ts.map +1 -0
- package/dist/baselines/diffmatch/paragraphAlignment.js +206 -0
- package/dist/baselines/diffmatch/paragraphAlignment.js.map +1 -0
- package/dist/baselines/diffmatch/pipeline.d.ts +33 -0
- package/dist/baselines/diffmatch/pipeline.d.ts.map +1 -0
- package/dist/baselines/diffmatch/pipeline.js +84 -0
- package/dist/baselines/diffmatch/pipeline.js.map +1 -0
- package/dist/baselines/diffmatch/runDiff.d.ts +53 -0
- package/dist/baselines/diffmatch/runDiff.d.ts.map +1 -0
- package/dist/baselines/diffmatch/runDiff.js +253 -0
- package/dist/baselines/diffmatch/runDiff.js.map +1 -0
- package/dist/baselines/diffmatch/trackChangesRenderer.d.ts +64 -0
- package/dist/baselines/diffmatch/trackChangesRenderer.d.ts.map +1 -0
- package/dist/baselines/diffmatch/trackChangesRenderer.js +178 -0
- package/dist/baselines/diffmatch/trackChangesRenderer.js.map +1 -0
- package/dist/baselines/diffmatch/xmlParser.d.ts +45 -0
- package/dist/baselines/diffmatch/xmlParser.d.ts.map +1 -0
- package/dist/baselines/diffmatch/xmlParser.js +344 -0
- package/dist/baselines/diffmatch/xmlParser.js.map +1 -0
- package/dist/baselines/wmlcomparer/DocxodusWasm.d.ts +51 -0
- package/dist/baselines/wmlcomparer/DocxodusWasm.d.ts.map +1 -0
- package/dist/baselines/wmlcomparer/DocxodusWasm.js +83 -0
- package/dist/baselines/wmlcomparer/DocxodusWasm.js.map +1 -0
- package/dist/baselines/wmlcomparer/DotnetCli.d.ts +40 -0
- package/dist/baselines/wmlcomparer/DotnetCli.d.ts.map +1 -0
- package/dist/baselines/wmlcomparer/DotnetCli.js +135 -0
- package/dist/baselines/wmlcomparer/DotnetCli.js.map +1 -0
- package/dist/benchmark/metrics.d.ts +72 -0
- package/dist/benchmark/metrics.d.ts.map +1 -0
- package/dist/benchmark/metrics.js +45 -0
- package/dist/benchmark/metrics.js.map +1 -0
- package/dist/benchmark/reporter.d.ts +23 -0
- package/dist/benchmark/reporter.d.ts.map +1 -0
- package/dist/benchmark/reporter.js +147 -0
- package/dist/benchmark/reporter.js.map +1 -0
- package/dist/benchmark/runner.d.ts +30 -0
- package/dist/benchmark/runner.d.ts.map +1 -0
- package/dist/benchmark/runner.js +233 -0
- package/dist/benchmark/runner.js.map +1 -0
- package/dist/cli/compare-two.d.ts +28 -0
- package/dist/cli/compare-two.d.ts.map +1 -0
- package/dist/cli/compare-two.js +110 -0
- package/dist/cli/compare-two.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core-types.d.ts +296 -0
- package/dist/core-types.d.ts.map +1 -0
- package/dist/core-types.js +122 -0
- package/dist/core-types.js.map +1 -0
- package/dist/footnotes.d.ts +144 -0
- package/dist/footnotes.d.ts.map +1 -0
- package/dist/footnotes.js +291 -0
- package/dist/footnotes.js.map +1 -0
- package/dist/format-detection.d.ts +120 -0
- package/dist/format-detection.d.ts.map +1 -0
- package/dist/format-detection.js +338 -0
- package/dist/format-detection.js.map +1 -0
- package/dist/index.d.ts +177 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/output-artifacts.d.ts +6 -0
- package/dist/integration/output-artifacts.d.ts.map +1 -0
- package/dist/integration/output-artifacts.js +30 -0
- package/dist/integration/output-artifacts.js.map +1 -0
- package/dist/move-detection.d.ts +211 -0
- package/dist/move-detection.d.ts.map +1 -0
- package/dist/move-detection.js +391 -0
- package/dist/move-detection.js.map +1 -0
- package/dist/numbering.d.ts +136 -0
- package/dist/numbering.d.ts.map +1 -0
- package/dist/numbering.js +446 -0
- package/dist/numbering.js.map +1 -0
- package/dist/primitives/accept_changes.d.ts +30 -0
- package/dist/primitives/accept_changes.d.ts.map +1 -0
- package/dist/primitives/accept_changes.js +241 -0
- package/dist/primitives/accept_changes.js.map +1 -0
- package/dist/primitives/bookmarks.d.ts +12 -0
- package/dist/primitives/bookmarks.d.ts.map +1 -0
- package/dist/primitives/bookmarks.js +248 -0
- package/dist/primitives/bookmarks.js.map +1 -0
- package/dist/primitives/comments.d.ts +88 -0
- package/dist/primitives/comments.d.ts.map +1 -0
- package/dist/primitives/comments.js +703 -0
- package/dist/primitives/comments.js.map +1 -0
- package/dist/primitives/document.d.ts +168 -0
- package/dist/primitives/document.d.ts.map +1 -0
- package/dist/primitives/document.js +532 -0
- package/dist/primitives/document.js.map +1 -0
- package/dist/primitives/document_view.d.ts +93 -0
- package/dist/primitives/document_view.d.ts.map +1 -0
- package/dist/primitives/document_view.js +722 -0
- package/dist/primitives/document_view.js.map +1 -0
- package/dist/primitives/dom-helpers.d.ts +94 -0
- package/dist/primitives/dom-helpers.d.ts.map +1 -0
- package/dist/primitives/dom-helpers.js +219 -0
- package/dist/primitives/dom-helpers.js.map +1 -0
- package/dist/primitives/errors.d.ts +7 -0
- package/dist/primitives/errors.d.ts.map +1 -0
- package/dist/primitives/errors.js +10 -0
- package/dist/primitives/errors.js.map +1 -0
- package/dist/primitives/extract_revisions.d.ts +50 -0
- package/dist/primitives/extract_revisions.d.ts.map +1 -0
- package/dist/primitives/extract_revisions.js +340 -0
- package/dist/primitives/extract_revisions.js.map +1 -0
- package/dist/primitives/footnotes.d.ts +37 -0
- package/dist/primitives/footnotes.d.ts.map +1 -0
- package/dist/primitives/footnotes.js +552 -0
- package/dist/primitives/footnotes.js.map +1 -0
- package/dist/primitives/formatting_tags.d.ts +30 -0
- package/dist/primitives/formatting_tags.d.ts.map +1 -0
- package/dist/primitives/formatting_tags.js +217 -0
- package/dist/primitives/formatting_tags.js.map +1 -0
- package/dist/primitives/index.d.ts +26 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +26 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/primitives/layout.d.ts +53 -0
- package/dist/primitives/layout.d.ts.map +1 -0
- package/dist/primitives/layout.js +178 -0
- package/dist/primitives/layout.js.map +1 -0
- package/dist/primitives/list_labels.d.ts +19 -0
- package/dist/primitives/list_labels.d.ts.map +1 -0
- package/dist/primitives/list_labels.js +57 -0
- package/dist/primitives/list_labels.js.map +1 -0
- package/dist/primitives/matching.d.ts +17 -0
- package/dist/primitives/matching.d.ts.map +1 -0
- package/dist/primitives/matching.js +144 -0
- package/dist/primitives/matching.js.map +1 -0
- package/dist/primitives/merge_runs.d.ts +23 -0
- package/dist/primitives/merge_runs.d.ts.map +1 -0
- package/dist/primitives/merge_runs.js +195 -0
- package/dist/primitives/merge_runs.js.map +1 -0
- package/dist/primitives/namespaces.d.ts +90 -0
- package/dist/primitives/namespaces.d.ts.map +1 -0
- package/dist/primitives/namespaces.js +107 -0
- package/dist/primitives/namespaces.js.map +1 -0
- package/dist/primitives/numbering.d.ts +27 -0
- package/dist/primitives/numbering.d.ts.map +1 -0
- package/dist/primitives/numbering.js +182 -0
- package/dist/primitives/numbering.js.map +1 -0
- package/dist/primitives/prevent_double_elevation.d.ts +18 -0
- package/dist/primitives/prevent_double_elevation.d.ts.map +1 -0
- package/dist/primitives/prevent_double_elevation.js +190 -0
- package/dist/primitives/prevent_double_elevation.js.map +1 -0
- package/dist/primitives/reject_changes.d.ts +27 -0
- package/dist/primitives/reject_changes.d.ts.map +1 -0
- package/dist/primitives/reject_changes.js +371 -0
- package/dist/primitives/reject_changes.js.map +1 -0
- package/dist/primitives/relationships.d.ts +7 -0
- package/dist/primitives/relationships.d.ts.map +1 -0
- package/dist/primitives/relationships.js +24 -0
- package/dist/primitives/relationships.js.map +1 -0
- package/dist/primitives/semantic_tags.d.ts +32 -0
- package/dist/primitives/semantic_tags.d.ts.map +1 -0
- package/dist/primitives/semantic_tags.js +139 -0
- package/dist/primitives/semantic_tags.js.map +1 -0
- package/dist/primitives/simplify_redlines.d.ts +19 -0
- package/dist/primitives/simplify_redlines.d.ts.map +1 -0
- package/dist/primitives/simplify_redlines.js +94 -0
- package/dist/primitives/simplify_redlines.js.map +1 -0
- package/dist/primitives/styles.d.ts +36 -0
- package/dist/primitives/styles.d.ts.map +1 -0
- package/dist/primitives/styles.js +190 -0
- package/dist/primitives/styles.js.map +1 -0
- package/dist/primitives/text.d.ts +27 -0
- package/dist/primitives/text.d.ts.map +1 -0
- package/dist/primitives/text.js +416 -0
- package/dist/primitives/text.js.map +1 -0
- package/dist/primitives/validate_document.d.ts +24 -0
- package/dist/primitives/validate_document.d.ts.map +1 -0
- package/dist/primitives/validate_document.js +147 -0
- package/dist/primitives/validate_document.js.map +1 -0
- package/dist/primitives/xml.d.ts +5 -0
- package/dist/primitives/xml.d.ts.map +1 -0
- package/dist/primitives/xml.js +19 -0
- package/dist/primitives/xml.js.map +1 -0
- package/dist/primitives/zip.d.ts +25 -0
- package/dist/primitives/zip.d.ts.map +1 -0
- package/dist/primitives/zip.js +78 -0
- package/dist/primitives/zip.js.map +1 -0
- package/dist/shared/docx/DocxArchive.d.ts +94 -0
- package/dist/shared/docx/DocxArchive.d.ts.map +1 -0
- package/dist/shared/docx/DocxArchive.js +169 -0
- package/dist/shared/docx/DocxArchive.js.map +1 -0
- package/dist/shared/ooxml/namespaces.d.ts +149 -0
- package/dist/shared/ooxml/namespaces.d.ts.map +1 -0
- package/dist/shared/ooxml/namespaces.js +224 -0
- package/dist/shared/ooxml/namespaces.js.map +1 -0
- package/dist/shared/ooxml/types.d.ts +136 -0
- package/dist/shared/ooxml/types.d.ts.map +1 -0
- package/dist/shared/ooxml/types.js +7 -0
- package/dist/shared/ooxml/types.js.map +1 -0
- package/package.json +63 -6
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* comments — OOXML comment insertion, threaded replies, and part bootstrapping.
|
|
3
|
+
*
|
|
4
|
+
* Creates comment XML parts when missing, inserts comment range markers,
|
|
5
|
+
* and supports threaded replies via commentsExtended.xml.
|
|
6
|
+
*/
|
|
7
|
+
import { OOXML, W } from './namespaces.js';
|
|
8
|
+
import { parseXml, serializeXml } from './xml.js';
|
|
9
|
+
import { getParagraphRuns } from './text.js';
|
|
10
|
+
import { getParagraphBookmarkId } from './bookmarks.js';
|
|
11
|
+
// ── Relationship types ──────────────────────────────────────────────────
|
|
12
|
+
const REL_TYPE_COMMENTS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments';
|
|
13
|
+
const REL_TYPE_COMMENTS_EXTENDED = 'http://schemas.microsoft.com/office/2011/relationships/commentsExtended';
|
|
14
|
+
const REL_TYPE_PEOPLE = 'http://schemas.microsoft.com/office/2011/relationships/people';
|
|
15
|
+
// ── Content types ───────────────────────────────────────────────────────
|
|
16
|
+
const CT_COMMENTS = 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml';
|
|
17
|
+
const CT_COMMENTS_EXTENDED = 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml';
|
|
18
|
+
const CT_PEOPLE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml';
|
|
19
|
+
// ── Minimal XML templates ───────────────────────────────────────────────
|
|
20
|
+
const COMMENTS_XML_TEMPLATE = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
21
|
+
`<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"` +
|
|
22
|
+
` xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"` +
|
|
23
|
+
` xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"/>`;
|
|
24
|
+
const COMMENTS_EXTENDED_XML_TEMPLATE = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
25
|
+
`<w15:commentsEx xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"` +
|
|
26
|
+
` xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"/>`;
|
|
27
|
+
const PEOPLE_XML_TEMPLATE = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
28
|
+
`<w15:people xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"/>`;
|
|
29
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
30
|
+
function generateParaId() {
|
|
31
|
+
// 8-hex-digit random ID used for w14:paraId / w15:paraId
|
|
32
|
+
const val = Math.floor(Math.random() * 0xFFFFFFFF);
|
|
33
|
+
return val.toString(16).toUpperCase().padStart(8, '0');
|
|
34
|
+
}
|
|
35
|
+
function isoNow() {
|
|
36
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create missing comment XML parts when a DOCX has no comment infrastructure.
|
|
40
|
+
* Idempotent — skips parts that already exist.
|
|
41
|
+
*/
|
|
42
|
+
export async function bootstrapCommentParts(zip) {
|
|
43
|
+
const created = [];
|
|
44
|
+
// 1. Ensure comment parts exist
|
|
45
|
+
if (!zip.hasFile('word/comments.xml')) {
|
|
46
|
+
zip.writeText('word/comments.xml', COMMENTS_XML_TEMPLATE);
|
|
47
|
+
created.push('word/comments.xml');
|
|
48
|
+
}
|
|
49
|
+
if (!zip.hasFile('word/commentsExtended.xml')) {
|
|
50
|
+
zip.writeText('word/commentsExtended.xml', COMMENTS_EXTENDED_XML_TEMPLATE);
|
|
51
|
+
created.push('word/commentsExtended.xml');
|
|
52
|
+
}
|
|
53
|
+
if (!zip.hasFile('word/people.xml')) {
|
|
54
|
+
zip.writeText('word/people.xml', PEOPLE_XML_TEMPLATE);
|
|
55
|
+
created.push('word/people.xml');
|
|
56
|
+
}
|
|
57
|
+
if (created.length === 0)
|
|
58
|
+
return { partsCreated: [] };
|
|
59
|
+
// 2. Update [Content_Types].xml
|
|
60
|
+
await ensureContentTypes(zip, created);
|
|
61
|
+
// 3. Update word/_rels/document.xml.rels
|
|
62
|
+
await ensureRelationships(zip, created);
|
|
63
|
+
return { partsCreated: created };
|
|
64
|
+
}
|
|
65
|
+
async function ensureContentTypes(zip, newParts) {
|
|
66
|
+
const ctPath = '[Content_Types].xml';
|
|
67
|
+
let ctXml;
|
|
68
|
+
try {
|
|
69
|
+
ctXml = await zip.readText(ctPath);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Minimal [Content_Types].xml if missing
|
|
73
|
+
ctXml =
|
|
74
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
75
|
+
`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>`;
|
|
76
|
+
}
|
|
77
|
+
const ctDoc = parseXml(ctXml);
|
|
78
|
+
const typesEl = ctDoc.documentElement;
|
|
79
|
+
const ctNs = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
|
80
|
+
const partToCt = {
|
|
81
|
+
'word/comments.xml': CT_COMMENTS,
|
|
82
|
+
'word/commentsExtended.xml': CT_COMMENTS_EXTENDED,
|
|
83
|
+
'word/people.xml': CT_PEOPLE,
|
|
84
|
+
};
|
|
85
|
+
// Check existing overrides
|
|
86
|
+
const overrides = Array.from(typesEl.getElementsByTagNameNS(ctNs, 'Override'));
|
|
87
|
+
const existingPartNames = new Set(overrides.map((o) => o.getAttribute('PartName')));
|
|
88
|
+
for (const part of newParts) {
|
|
89
|
+
const partName = `/${part}`;
|
|
90
|
+
const contentType = partToCt[part];
|
|
91
|
+
if (!contentType || existingPartNames.has(partName))
|
|
92
|
+
continue;
|
|
93
|
+
const override = ctDoc.createElementNS(ctNs, 'Override');
|
|
94
|
+
override.setAttribute('PartName', partName);
|
|
95
|
+
override.setAttribute('ContentType', contentType);
|
|
96
|
+
typesEl.appendChild(override);
|
|
97
|
+
}
|
|
98
|
+
zip.writeText(ctPath, serializeXml(ctDoc));
|
|
99
|
+
}
|
|
100
|
+
async function ensureRelationships(zip, newParts) {
|
|
101
|
+
const relsPath = 'word/_rels/document.xml.rels';
|
|
102
|
+
let relsXml;
|
|
103
|
+
try {
|
|
104
|
+
relsXml = await zip.readText(relsPath);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
relsXml =
|
|
108
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
109
|
+
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>`;
|
|
110
|
+
}
|
|
111
|
+
const relsDoc = parseXml(relsXml);
|
|
112
|
+
const relsEl = relsDoc.documentElement;
|
|
113
|
+
const relNs = OOXML.REL_NS;
|
|
114
|
+
const partToRelType = {
|
|
115
|
+
'word/comments.xml': REL_TYPE_COMMENTS,
|
|
116
|
+
'word/commentsExtended.xml': REL_TYPE_COMMENTS_EXTENDED,
|
|
117
|
+
'word/people.xml': REL_TYPE_PEOPLE,
|
|
118
|
+
};
|
|
119
|
+
// Check existing relationships
|
|
120
|
+
const existingRels = Array.from(relsEl.getElementsByTagNameNS(relNs, 'Relationship'));
|
|
121
|
+
const existingTypes = new Set(existingRels.map((r) => r.getAttribute('Type')));
|
|
122
|
+
// Allocate next rId
|
|
123
|
+
let maxId = 0;
|
|
124
|
+
for (const rel of existingRels) {
|
|
125
|
+
const id = rel.getAttribute('Id') ?? '';
|
|
126
|
+
const match = /^rId(\d+)$/.exec(id);
|
|
127
|
+
if (match)
|
|
128
|
+
maxId = Math.max(maxId, parseInt(match[1], 10));
|
|
129
|
+
}
|
|
130
|
+
for (const part of newParts) {
|
|
131
|
+
const relType = partToRelType[part];
|
|
132
|
+
if (!relType || existingTypes.has(relType))
|
|
133
|
+
continue;
|
|
134
|
+
maxId++;
|
|
135
|
+
const rel = relsDoc.createElementNS(relNs, 'Relationship');
|
|
136
|
+
rel.setAttribute('Id', `rId${maxId}`);
|
|
137
|
+
rel.setAttribute('Type', relType);
|
|
138
|
+
// Target is relative to word/
|
|
139
|
+
rel.setAttribute('Target', part.replace('word/', ''));
|
|
140
|
+
relsEl.appendChild(rel);
|
|
141
|
+
}
|
|
142
|
+
zip.writeText(relsPath, serializeXml(relsDoc));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Insert a root comment anchored to a text range within a paragraph.
|
|
146
|
+
*
|
|
147
|
+
* - Allocates next comment ID from existing comments.xml
|
|
148
|
+
* - Inserts commentRangeStart/commentRangeEnd markers in document body
|
|
149
|
+
* - Inserts commentReference run after range end
|
|
150
|
+
* - Adds comment entry to comments.xml
|
|
151
|
+
* - Adds author to people.xml if not present
|
|
152
|
+
*/
|
|
153
|
+
export async function addComment(documentXml, zip, params) {
|
|
154
|
+
const { paragraphEl, start, end, author, text, initials } = params;
|
|
155
|
+
// Load comments.xml
|
|
156
|
+
const commentsXml = await zip.readText('word/comments.xml');
|
|
157
|
+
const commentsDoc = parseXml(commentsXml);
|
|
158
|
+
// Allocate next comment ID
|
|
159
|
+
const commentId = allocateNextCommentId(commentsDoc);
|
|
160
|
+
// Insert range markers and reference in document body
|
|
161
|
+
insertCommentMarkers(documentXml, paragraphEl, commentId, start, end);
|
|
162
|
+
// Add comment element to comments.xml
|
|
163
|
+
const paraId = generateParaId();
|
|
164
|
+
addCommentElement(commentsDoc, {
|
|
165
|
+
id: commentId,
|
|
166
|
+
author,
|
|
167
|
+
initials: initials ?? author.charAt(0).toUpperCase(),
|
|
168
|
+
text,
|
|
169
|
+
paraId,
|
|
170
|
+
});
|
|
171
|
+
zip.writeText('word/comments.xml', serializeXml(commentsDoc));
|
|
172
|
+
// Add author to people.xml
|
|
173
|
+
await ensureAuthorInPeople(zip, author);
|
|
174
|
+
return { commentId };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Add a threaded reply to an existing comment.
|
|
178
|
+
*
|
|
179
|
+
* Replies don't have range markers in the document body.
|
|
180
|
+
* Thread linkage is stored in commentsExtended.xml via paraIdParent.
|
|
181
|
+
*/
|
|
182
|
+
export async function addCommentReply(_documentXml, zip, params) {
|
|
183
|
+
const { parentCommentId, author, text, initials } = params;
|
|
184
|
+
// Load comments.xml
|
|
185
|
+
const commentsXml = await zip.readText('word/comments.xml');
|
|
186
|
+
const commentsDoc = parseXml(commentsXml);
|
|
187
|
+
// Find parent comment's paraId
|
|
188
|
+
const parentParaId = findCommentParaId(commentsDoc, parentCommentId);
|
|
189
|
+
if (!parentParaId) {
|
|
190
|
+
throw new Error(`Parent comment ID ${parentCommentId} not found in comments.xml`);
|
|
191
|
+
}
|
|
192
|
+
// Allocate ID and add reply comment
|
|
193
|
+
const commentId = allocateNextCommentId(commentsDoc);
|
|
194
|
+
const replyParaId = generateParaId();
|
|
195
|
+
addCommentElement(commentsDoc, {
|
|
196
|
+
id: commentId,
|
|
197
|
+
author,
|
|
198
|
+
initials: initials ?? author.charAt(0).toUpperCase(),
|
|
199
|
+
text,
|
|
200
|
+
paraId: replyParaId,
|
|
201
|
+
});
|
|
202
|
+
zip.writeText('word/comments.xml', serializeXml(commentsDoc));
|
|
203
|
+
// Link reply in commentsExtended.xml
|
|
204
|
+
await linkReplyInCommentsExtended(zip, replyParaId, parentParaId);
|
|
205
|
+
// Ensure parent also has an entry in commentsExtended.xml
|
|
206
|
+
await ensureCommentExEntry(zip, parentParaId);
|
|
207
|
+
// Add author to people.xml
|
|
208
|
+
await ensureAuthorInPeople(zip, author);
|
|
209
|
+
return { commentId, parentCommentId };
|
|
210
|
+
}
|
|
211
|
+
// ── Internal helpers ────────────────────────────────────────────────────
|
|
212
|
+
function allocateNextCommentId(commentsDoc) {
|
|
213
|
+
const commentEls = commentsDoc.getElementsByTagNameNS(OOXML.W_NS, W.comment);
|
|
214
|
+
let maxId = -1;
|
|
215
|
+
for (let i = 0; i < commentEls.length; i++) {
|
|
216
|
+
const el = commentEls.item(i);
|
|
217
|
+
const idStr = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
218
|
+
if (idStr) {
|
|
219
|
+
const id = parseInt(idStr, 10);
|
|
220
|
+
if (id > maxId)
|
|
221
|
+
maxId = id;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return maxId + 1;
|
|
225
|
+
}
|
|
226
|
+
function findCommentParaId(commentsDoc, commentId) {
|
|
227
|
+
const commentEls = commentsDoc.getElementsByTagNameNS(OOXML.W_NS, W.comment);
|
|
228
|
+
for (let i = 0; i < commentEls.length; i++) {
|
|
229
|
+
const el = commentEls.item(i);
|
|
230
|
+
const idStr = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
231
|
+
if (idStr && parseInt(idStr, 10) === commentId) {
|
|
232
|
+
// paraId is on the w:p child inside the comment
|
|
233
|
+
const paras = el.getElementsByTagNameNS(OOXML.W_NS, W.p);
|
|
234
|
+
if (paras.length > 0) {
|
|
235
|
+
const p = paras.item(0);
|
|
236
|
+
return p.getAttributeNS(OOXML.W14_NS, 'paraId') ?? p.getAttribute('w14:paraId') ?? null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
function addCommentElement(commentsDoc, params) {
|
|
243
|
+
const root = commentsDoc.documentElement;
|
|
244
|
+
const commentEl = commentsDoc.createElementNS(OOXML.W_NS, 'w:comment');
|
|
245
|
+
commentEl.setAttribute('w:id', String(params.id));
|
|
246
|
+
commentEl.setAttribute('w:author', params.author);
|
|
247
|
+
commentEl.setAttribute('w:date', isoNow());
|
|
248
|
+
commentEl.setAttribute('w:initials', params.initials);
|
|
249
|
+
// Comment body: <w:p w14:paraId="..."><w:pPr><w:pStyle w:val="CommentText"/></w:pPr><w:r><w:annotationRef/></w:r><w:r><w:t>text</w:t></w:r></w:p>
|
|
250
|
+
const p = commentsDoc.createElementNS(OOXML.W_NS, 'w:p');
|
|
251
|
+
p.setAttribute('w14:paraId', params.paraId);
|
|
252
|
+
// Annotation reference run
|
|
253
|
+
const refRun = commentsDoc.createElementNS(OOXML.W_NS, 'w:r');
|
|
254
|
+
const annotRef = commentsDoc.createElementNS(OOXML.W_NS, 'w:annotationRef');
|
|
255
|
+
refRun.appendChild(annotRef);
|
|
256
|
+
p.appendChild(refRun);
|
|
257
|
+
// Text run
|
|
258
|
+
const textRun = commentsDoc.createElementNS(OOXML.W_NS, 'w:r');
|
|
259
|
+
const t = commentsDoc.createElementNS(OOXML.W_NS, 'w:t');
|
|
260
|
+
if (params.text.startsWith(' ') || params.text.endsWith(' ')) {
|
|
261
|
+
t.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
|
262
|
+
}
|
|
263
|
+
t.appendChild(commentsDoc.createTextNode(params.text));
|
|
264
|
+
textRun.appendChild(t);
|
|
265
|
+
p.appendChild(textRun);
|
|
266
|
+
commentEl.appendChild(p);
|
|
267
|
+
root.appendChild(commentEl);
|
|
268
|
+
}
|
|
269
|
+
function insertCommentMarkers(documentXml, paragraphEl, commentId, start, end) {
|
|
270
|
+
// Find the runs in the paragraph and map string offsets to DOM positions
|
|
271
|
+
const runs = getParagraphRuns(paragraphEl);
|
|
272
|
+
// Create marker elements
|
|
273
|
+
const rangeStart = documentXml.createElementNS(OOXML.W_NS, 'w:commentRangeStart');
|
|
274
|
+
rangeStart.setAttribute('w:id', String(commentId));
|
|
275
|
+
const rangeEnd = documentXml.createElementNS(OOXML.W_NS, 'w:commentRangeEnd');
|
|
276
|
+
rangeEnd.setAttribute('w:id', String(commentId));
|
|
277
|
+
const refRun = documentXml.createElementNS(OOXML.W_NS, 'w:r');
|
|
278
|
+
const rPr = documentXml.createElementNS(OOXML.W_NS, 'w:rPr');
|
|
279
|
+
const rStyle = documentXml.createElementNS(OOXML.W_NS, 'w:rStyle');
|
|
280
|
+
rStyle.setAttribute('w:val', 'CommentReference');
|
|
281
|
+
rPr.appendChild(rStyle);
|
|
282
|
+
refRun.appendChild(rPr);
|
|
283
|
+
const commentRef = documentXml.createElementNS(OOXML.W_NS, 'w:commentReference');
|
|
284
|
+
commentRef.setAttribute('w:id', String(commentId));
|
|
285
|
+
refRun.appendChild(commentRef);
|
|
286
|
+
// Map string offsets to run positions
|
|
287
|
+
let pos = 0;
|
|
288
|
+
let startRunIdx = -1;
|
|
289
|
+
let endRunIdx = -1;
|
|
290
|
+
for (let i = 0; i < runs.length; i++) {
|
|
291
|
+
const runEnd = pos + runs[i].text.length;
|
|
292
|
+
if (startRunIdx < 0 && start < runEnd)
|
|
293
|
+
startRunIdx = i;
|
|
294
|
+
if (endRunIdx < 0 && end <= runEnd)
|
|
295
|
+
endRunIdx = i;
|
|
296
|
+
pos = runEnd;
|
|
297
|
+
}
|
|
298
|
+
// Fallback: if offsets don't map cleanly, wrap the whole paragraph
|
|
299
|
+
if (startRunIdx < 0)
|
|
300
|
+
startRunIdx = 0;
|
|
301
|
+
if (endRunIdx < 0)
|
|
302
|
+
endRunIdx = runs.length - 1;
|
|
303
|
+
// Insert commentRangeStart before the start run
|
|
304
|
+
if (runs.length > 0 && startRunIdx < runs.length) {
|
|
305
|
+
paragraphEl.insertBefore(rangeStart, runs[startRunIdx].r);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// No runs — insert at end of paragraph
|
|
309
|
+
paragraphEl.appendChild(rangeStart);
|
|
310
|
+
}
|
|
311
|
+
// Insert commentRangeEnd and reference run after the end run
|
|
312
|
+
if (runs.length > 0 && endRunIdx < runs.length) {
|
|
313
|
+
const afterEndRun = runs[endRunIdx].r.nextSibling;
|
|
314
|
+
paragraphEl.insertBefore(rangeEnd, afterEndRun);
|
|
315
|
+
paragraphEl.insertBefore(refRun, afterEndRun);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
paragraphEl.appendChild(rangeEnd);
|
|
319
|
+
paragraphEl.appendChild(refRun);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function linkReplyInCommentsExtended(zip, replyParaId, parentParaId) {
|
|
323
|
+
const extXml = await zip.readText('word/commentsExtended.xml');
|
|
324
|
+
const extDoc = parseXml(extXml);
|
|
325
|
+
const root = extDoc.documentElement;
|
|
326
|
+
const exEl = extDoc.createElementNS(OOXML.W15_NS, 'w15:commentEx');
|
|
327
|
+
exEl.setAttribute('w15:paraId', replyParaId);
|
|
328
|
+
exEl.setAttribute('w15:paraIdParent', parentParaId);
|
|
329
|
+
exEl.setAttribute('w15:done', '0');
|
|
330
|
+
root.appendChild(exEl);
|
|
331
|
+
zip.writeText('word/commentsExtended.xml', serializeXml(extDoc));
|
|
332
|
+
}
|
|
333
|
+
async function ensureCommentExEntry(zip, paraId) {
|
|
334
|
+
const extXml = await zip.readText('word/commentsExtended.xml');
|
|
335
|
+
const extDoc = parseXml(extXml);
|
|
336
|
+
const root = extDoc.documentElement;
|
|
337
|
+
// Check if entry already exists
|
|
338
|
+
const existing = root.getElementsByTagNameNS(OOXML.W15_NS, 'commentEx');
|
|
339
|
+
for (let i = 0; i < existing.length; i++) {
|
|
340
|
+
const el = existing.item(i);
|
|
341
|
+
const pid = el.getAttributeNS(OOXML.W15_NS, 'paraId') ?? el.getAttribute('w15:paraId');
|
|
342
|
+
if (pid === paraId)
|
|
343
|
+
return; // Already present
|
|
344
|
+
}
|
|
345
|
+
const exEl = extDoc.createElementNS(OOXML.W15_NS, 'w15:commentEx');
|
|
346
|
+
exEl.setAttribute('w15:paraId', paraId);
|
|
347
|
+
exEl.setAttribute('w15:done', '0');
|
|
348
|
+
root.appendChild(exEl);
|
|
349
|
+
zip.writeText('word/commentsExtended.xml', serializeXml(extDoc));
|
|
350
|
+
}
|
|
351
|
+
async function ensureAuthorInPeople(zip, author) {
|
|
352
|
+
const peopleXml = await zip.readText('word/people.xml');
|
|
353
|
+
const peopleDoc = parseXml(peopleXml);
|
|
354
|
+
const root = peopleDoc.documentElement;
|
|
355
|
+
// Check if author already exists
|
|
356
|
+
const persons = root.getElementsByTagNameNS(OOXML.W15_NS, 'person');
|
|
357
|
+
for (let i = 0; i < persons.length; i++) {
|
|
358
|
+
const el = persons.item(i);
|
|
359
|
+
const name = el.getAttributeNS(OOXML.W15_NS, 'author') ?? el.getAttribute('w15:author');
|
|
360
|
+
if (name === author)
|
|
361
|
+
return; // Already present
|
|
362
|
+
}
|
|
363
|
+
const personEl = peopleDoc.createElementNS(OOXML.W15_NS, 'w15:person');
|
|
364
|
+
personEl.setAttribute('w15:author', author);
|
|
365
|
+
// Add a presenceInfo child (required by Word)
|
|
366
|
+
const presenceInfo = peopleDoc.createElementNS(OOXML.W15_NS, 'w15:presenceInfo');
|
|
367
|
+
presenceInfo.setAttribute('w15:providerId', 'None');
|
|
368
|
+
presenceInfo.setAttribute('w15:userId', author);
|
|
369
|
+
personEl.appendChild(presenceInfo);
|
|
370
|
+
root.appendChild(personEl);
|
|
371
|
+
zip.writeText('word/people.xml', serializeXml(peopleDoc));
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Read all comments from a document, building a threaded tree.
|
|
375
|
+
*
|
|
376
|
+
* Root comments are returned at the top level; replies are nested under
|
|
377
|
+
* their parent's `replies` array. Thread linkage is resolved via
|
|
378
|
+
* commentsExtended.xml paraIdParent relationships.
|
|
379
|
+
*/
|
|
380
|
+
export async function getComments(zip, documentXml) {
|
|
381
|
+
const commentsText = await zip.readTextOrNull('word/comments.xml');
|
|
382
|
+
if (!commentsText)
|
|
383
|
+
return [];
|
|
384
|
+
const commentsDoc = parseXml(commentsText);
|
|
385
|
+
const commentEls = commentsDoc.getElementsByTagNameNS(OOXML.W_NS, W.comment);
|
|
386
|
+
if (commentEls.length === 0)
|
|
387
|
+
return [];
|
|
388
|
+
// Build a map of commentId → { paraId, Comment }
|
|
389
|
+
const byParaId = new Map();
|
|
390
|
+
const byId = new Map();
|
|
391
|
+
for (let i = 0; i < commentEls.length; i++) {
|
|
392
|
+
const el = commentEls.item(i);
|
|
393
|
+
const idStr = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
394
|
+
const id = idStr ? parseInt(idStr, 10) : -1;
|
|
395
|
+
if (id < 0)
|
|
396
|
+
continue;
|
|
397
|
+
const author = el.getAttributeNS(OOXML.W_NS, 'author') ?? el.getAttribute('w:author') ?? '';
|
|
398
|
+
const date = el.getAttributeNS(OOXML.W_NS, 'date') ?? el.getAttribute('w:date') ?? '';
|
|
399
|
+
const initials = el.getAttributeNS(OOXML.W_NS, 'initials') ?? el.getAttribute('w:initials') ?? '';
|
|
400
|
+
// Extract text from <w:t> elements, skipping annotationRef runs
|
|
401
|
+
const text = extractCommentText(el);
|
|
402
|
+
// Get paraId from first <w:p> child
|
|
403
|
+
const paras = el.getElementsByTagNameNS(OOXML.W_NS, W.p);
|
|
404
|
+
let paragraphId = null;
|
|
405
|
+
if (paras.length > 0) {
|
|
406
|
+
const p = paras.item(0);
|
|
407
|
+
paragraphId = p.getAttributeNS(OOXML.W14_NS, 'paraId') ?? p.getAttribute('w14:paraId') ?? null;
|
|
408
|
+
}
|
|
409
|
+
const comment = {
|
|
410
|
+
id,
|
|
411
|
+
author,
|
|
412
|
+
date,
|
|
413
|
+
initials,
|
|
414
|
+
text,
|
|
415
|
+
paragraphId,
|
|
416
|
+
anchoredParagraphId: null,
|
|
417
|
+
replies: [],
|
|
418
|
+
};
|
|
419
|
+
byId.set(id, comment);
|
|
420
|
+
if (paragraphId)
|
|
421
|
+
byParaId.set(paragraphId, comment);
|
|
422
|
+
}
|
|
423
|
+
// Resolve anchoredParagraphId by scanning documentXml for commentRangeStart elements
|
|
424
|
+
const rangeStarts = documentXml.getElementsByTagNameNS(OOXML.W_NS, W.commentRangeStart);
|
|
425
|
+
for (let i = 0; i < rangeStarts.length; i++) {
|
|
426
|
+
const rs = rangeStarts.item(i);
|
|
427
|
+
const cidStr = rs.getAttributeNS(OOXML.W_NS, 'id') ?? rs.getAttribute('w:id');
|
|
428
|
+
if (!cidStr)
|
|
429
|
+
continue;
|
|
430
|
+
const cid = parseInt(cidStr, 10);
|
|
431
|
+
const comment = byId.get(cid);
|
|
432
|
+
if (!comment)
|
|
433
|
+
continue;
|
|
434
|
+
// Walk up to find enclosing <w:p>
|
|
435
|
+
let parent = rs.parentNode;
|
|
436
|
+
while (parent && parent.nodeType === 1) {
|
|
437
|
+
const pel = parent;
|
|
438
|
+
if (pel.localName === W.p && pel.namespaceURI === OOXML.W_NS) {
|
|
439
|
+
comment.anchoredParagraphId = getParagraphBookmarkId(pel);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
parent = parent.parentNode;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Build thread tree from commentsExtended.xml
|
|
446
|
+
const extText = await zip.readTextOrNull('word/commentsExtended.xml');
|
|
447
|
+
if (extText) {
|
|
448
|
+
const extDoc = parseXml(extText);
|
|
449
|
+
const exEls = extDoc.getElementsByTagNameNS(OOXML.W15_NS, 'commentEx');
|
|
450
|
+
for (let i = 0; i < exEls.length; i++) {
|
|
451
|
+
const ex = exEls.item(i);
|
|
452
|
+
const childParaId = ex.getAttributeNS(OOXML.W15_NS, 'paraId') ?? ex.getAttribute('w15:paraId');
|
|
453
|
+
const parentParaId = ex.getAttributeNS(OOXML.W15_NS, 'paraIdParent') ?? ex.getAttribute('w15:paraIdParent');
|
|
454
|
+
if (!childParaId || !parentParaId)
|
|
455
|
+
continue;
|
|
456
|
+
const child = byParaId.get(childParaId);
|
|
457
|
+
const parentComment = byParaId.get(parentParaId);
|
|
458
|
+
if (child && parentComment) {
|
|
459
|
+
parentComment.replies.push(child);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Collect root-level comments (those not appearing as anyone's reply)
|
|
464
|
+
const replyParaIds = new Set();
|
|
465
|
+
if (extText) {
|
|
466
|
+
const extDoc = parseXml(extText);
|
|
467
|
+
const exEls = extDoc.getElementsByTagNameNS(OOXML.W15_NS, 'commentEx');
|
|
468
|
+
for (let i = 0; i < exEls.length; i++) {
|
|
469
|
+
const ex = exEls.item(i);
|
|
470
|
+
const childParaId = ex.getAttributeNS(OOXML.W15_NS, 'paraId') ?? ex.getAttribute('w15:paraId');
|
|
471
|
+
const parentParaId = ex.getAttributeNS(OOXML.W15_NS, 'paraIdParent') ?? ex.getAttribute('w15:paraIdParent');
|
|
472
|
+
if (childParaId && parentParaId) {
|
|
473
|
+
replyParaIds.add(childParaId);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const roots = [];
|
|
478
|
+
for (const comment of byId.values()) {
|
|
479
|
+
if (!comment.paragraphId || !replyParaIds.has(comment.paragraphId)) {
|
|
480
|
+
roots.push(comment);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return roots;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Get a single comment by ID, searching the full tree including replies.
|
|
487
|
+
*/
|
|
488
|
+
export async function getComment(zip, documentXml, commentId) {
|
|
489
|
+
const all = await getComments(zip, documentXml);
|
|
490
|
+
return findCommentById(all, commentId);
|
|
491
|
+
}
|
|
492
|
+
function findCommentById(comments, id) {
|
|
493
|
+
for (const c of comments) {
|
|
494
|
+
if (c.id === id)
|
|
495
|
+
return c;
|
|
496
|
+
const found = findCommentById(c.replies, id);
|
|
497
|
+
if (found)
|
|
498
|
+
return found;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
// ── Comment deletion ─────────────────────────────────────────────────
|
|
503
|
+
/**
|
|
504
|
+
* Delete a comment and all its descendants from the document.
|
|
505
|
+
*
|
|
506
|
+
* - Removes comment elements from comments.xml
|
|
507
|
+
* - Removes commentEx entries from commentsExtended.xml (if present)
|
|
508
|
+
* - For root comments: removes commentRangeStart, commentRangeEnd, and
|
|
509
|
+
* commentReference from document.xml (element-level; run removed only if empty)
|
|
510
|
+
* - Transitive cascade: deleting any node also deletes all descendants
|
|
511
|
+
*/
|
|
512
|
+
export async function deleteComment(documentXml, zip, params) {
|
|
513
|
+
const { commentId } = params;
|
|
514
|
+
const commentsText = await zip.readTextOrNull('word/comments.xml');
|
|
515
|
+
if (!commentsText)
|
|
516
|
+
throw new Error(`Comment ID ${commentId} not found`);
|
|
517
|
+
const commentsDoc = parseXml(commentsText);
|
|
518
|
+
// Find the target comment element and its paraId
|
|
519
|
+
const targetEl = findCommentElementById(commentsDoc, commentId);
|
|
520
|
+
if (!targetEl)
|
|
521
|
+
throw new Error(`Comment ID ${commentId} not found`);
|
|
522
|
+
const targetParaId = getCommentElParaId(targetEl);
|
|
523
|
+
// Collect all IDs to delete: the target + all transitive descendants
|
|
524
|
+
const idsToDelete = new Set([commentId]);
|
|
525
|
+
const paraIdsToDelete = new Set();
|
|
526
|
+
if (targetParaId)
|
|
527
|
+
paraIdsToDelete.add(targetParaId);
|
|
528
|
+
// Build paraId→commentId and paraId→commentEl maps for all comments
|
|
529
|
+
const paraIdToId = new Map();
|
|
530
|
+
const allCommentEls = commentsDoc.getElementsByTagNameNS(OOXML.W_NS, W.comment);
|
|
531
|
+
for (let i = 0; i < allCommentEls.length; i++) {
|
|
532
|
+
const el = allCommentEls.item(i);
|
|
533
|
+
const idStr = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
534
|
+
const id = idStr ? parseInt(idStr, 10) : -1;
|
|
535
|
+
if (id < 0)
|
|
536
|
+
continue;
|
|
537
|
+
const pid = getCommentElParaId(el);
|
|
538
|
+
if (pid)
|
|
539
|
+
paraIdToId.set(pid, id);
|
|
540
|
+
}
|
|
541
|
+
// Read commentsExtended.xml to find descendants via paraIdParent graph
|
|
542
|
+
const extText = await zip.readTextOrNull('word/commentsExtended.xml');
|
|
543
|
+
if (extText) {
|
|
544
|
+
const extDoc = parseXml(extText);
|
|
545
|
+
const exEls = extDoc.getElementsByTagNameNS(OOXML.W15_NS, 'commentEx');
|
|
546
|
+
// Build parent→children map
|
|
547
|
+
const childrenOf = new Map();
|
|
548
|
+
for (let i = 0; i < exEls.length; i++) {
|
|
549
|
+
const ex = exEls.item(i);
|
|
550
|
+
const childPid = ex.getAttributeNS(OOXML.W15_NS, 'paraId') ?? ex.getAttribute('w15:paraId');
|
|
551
|
+
const parentPid = ex.getAttributeNS(OOXML.W15_NS, 'paraIdParent') ?? ex.getAttribute('w15:paraIdParent');
|
|
552
|
+
if (childPid && parentPid) {
|
|
553
|
+
const arr = childrenOf.get(parentPid);
|
|
554
|
+
if (arr)
|
|
555
|
+
arr.push(childPid);
|
|
556
|
+
else
|
|
557
|
+
childrenOf.set(parentPid, [childPid]);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// BFS from target paraId to collect all descendant paraIds
|
|
561
|
+
const queue = targetParaId ? [targetParaId] : [];
|
|
562
|
+
while (queue.length > 0) {
|
|
563
|
+
const pid = queue.shift();
|
|
564
|
+
const children = childrenOf.get(pid);
|
|
565
|
+
if (!children)
|
|
566
|
+
continue;
|
|
567
|
+
for (const childPid of children) {
|
|
568
|
+
if (!paraIdsToDelete.has(childPid)) {
|
|
569
|
+
paraIdsToDelete.add(childPid);
|
|
570
|
+
const childId = paraIdToId.get(childPid);
|
|
571
|
+
if (childId != null)
|
|
572
|
+
idsToDelete.add(childId);
|
|
573
|
+
queue.push(childPid);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// 1. Remove comment elements from comments.xml
|
|
579
|
+
const elsToRemove = [];
|
|
580
|
+
for (let i = 0; i < allCommentEls.length; i++) {
|
|
581
|
+
const el = allCommentEls.item(i);
|
|
582
|
+
const idStr = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
583
|
+
const id = idStr ? parseInt(idStr, 10) : -1;
|
|
584
|
+
if (idsToDelete.has(id))
|
|
585
|
+
elsToRemove.push(el);
|
|
586
|
+
}
|
|
587
|
+
for (const el of elsToRemove) {
|
|
588
|
+
el.parentNode?.removeChild(el);
|
|
589
|
+
}
|
|
590
|
+
zip.writeText('word/comments.xml', serializeXml(commentsDoc));
|
|
591
|
+
// 2. Remove commentEx entries from commentsExtended.xml (if present)
|
|
592
|
+
if (extText) {
|
|
593
|
+
const extDoc = parseXml(extText);
|
|
594
|
+
const exEls = extDoc.getElementsByTagNameNS(OOXML.W15_NS, 'commentEx');
|
|
595
|
+
const exToRemove = [];
|
|
596
|
+
for (let i = 0; i < exEls.length; i++) {
|
|
597
|
+
const ex = exEls.item(i);
|
|
598
|
+
const pid = ex.getAttributeNS(OOXML.W15_NS, 'paraId') ?? ex.getAttribute('w15:paraId');
|
|
599
|
+
if (pid && paraIdsToDelete.has(pid))
|
|
600
|
+
exToRemove.push(ex);
|
|
601
|
+
}
|
|
602
|
+
for (const ex of exToRemove) {
|
|
603
|
+
ex.parentNode?.removeChild(ex);
|
|
604
|
+
}
|
|
605
|
+
zip.writeText('word/commentsExtended.xml', serializeXml(extDoc));
|
|
606
|
+
}
|
|
607
|
+
// 3. Remove range markers and commentReference from document.xml (for root comments)
|
|
608
|
+
for (const cid of idsToDelete) {
|
|
609
|
+
removeCommentMarkersFromDocument(documentXml, cid);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function findCommentElementById(commentsDoc, commentId) {
|
|
613
|
+
const commentEls = commentsDoc.getElementsByTagNameNS(OOXML.W_NS, W.comment);
|
|
614
|
+
for (let i = 0; i < commentEls.length; i++) {
|
|
615
|
+
const el = commentEls.item(i);
|
|
616
|
+
const idStr = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
617
|
+
if (idStr && parseInt(idStr, 10) === commentId)
|
|
618
|
+
return el;
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
function getCommentElParaId(commentEl) {
|
|
623
|
+
const paras = commentEl.getElementsByTagNameNS(OOXML.W_NS, W.p);
|
|
624
|
+
if (paras.length === 0)
|
|
625
|
+
return null;
|
|
626
|
+
const p = paras.item(0);
|
|
627
|
+
return p.getAttributeNS(OOXML.W14_NS, 'paraId') ?? p.getAttribute('w14:paraId') ?? null;
|
|
628
|
+
}
|
|
629
|
+
function removeCommentMarkersFromDocument(documentXml, commentId) {
|
|
630
|
+
const cidStr = String(commentId);
|
|
631
|
+
// Remove commentRangeStart elements
|
|
632
|
+
const rangeStarts = documentXml.getElementsByTagNameNS(OOXML.W_NS, W.commentRangeStart);
|
|
633
|
+
const startsToRemove = [];
|
|
634
|
+
for (let i = 0; i < rangeStarts.length; i++) {
|
|
635
|
+
const el = rangeStarts.item(i);
|
|
636
|
+
const id = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
637
|
+
if (id === cidStr)
|
|
638
|
+
startsToRemove.push(el);
|
|
639
|
+
}
|
|
640
|
+
for (const el of startsToRemove)
|
|
641
|
+
el.parentNode?.removeChild(el);
|
|
642
|
+
// Remove commentRangeEnd elements
|
|
643
|
+
const rangeEnds = documentXml.getElementsByTagNameNS(OOXML.W_NS, W.commentRangeEnd);
|
|
644
|
+
const endsToRemove = [];
|
|
645
|
+
for (let i = 0; i < rangeEnds.length; i++) {
|
|
646
|
+
const el = rangeEnds.item(i);
|
|
647
|
+
const id = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
648
|
+
if (id === cidStr)
|
|
649
|
+
endsToRemove.push(el);
|
|
650
|
+
}
|
|
651
|
+
for (const el of endsToRemove)
|
|
652
|
+
el.parentNode?.removeChild(el);
|
|
653
|
+
// Remove commentReference elements (safe: remove element, then run only if empty)
|
|
654
|
+
const refs = documentXml.getElementsByTagNameNS(OOXML.W_NS, W.commentReference);
|
|
655
|
+
const refsToRemove = [];
|
|
656
|
+
for (let i = 0; i < refs.length; i++) {
|
|
657
|
+
const el = refs.item(i);
|
|
658
|
+
const id = el.getAttributeNS(OOXML.W_NS, 'id') ?? el.getAttribute('w:id');
|
|
659
|
+
if (id === cidStr)
|
|
660
|
+
refsToRemove.push(el);
|
|
661
|
+
}
|
|
662
|
+
for (const ref of refsToRemove) {
|
|
663
|
+
const run = ref.parentNode;
|
|
664
|
+
if (!run)
|
|
665
|
+
continue;
|
|
666
|
+
run.removeChild(ref);
|
|
667
|
+
// Remove run only if it has no visible content after removing the reference
|
|
668
|
+
if (!hasVisibleRunContent(run)) {
|
|
669
|
+
run.parentNode?.removeChild(run);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function hasVisibleRunContent(run) {
|
|
674
|
+
for (const child of Array.from(run.childNodes)) {
|
|
675
|
+
if (child.nodeType !== 1)
|
|
676
|
+
continue;
|
|
677
|
+
const el = child;
|
|
678
|
+
if (el.namespaceURI !== OOXML.W_NS)
|
|
679
|
+
continue;
|
|
680
|
+
if (el.localName === W.rPr)
|
|
681
|
+
continue;
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
function extractCommentText(commentEl) {
|
|
687
|
+
const parts = [];
|
|
688
|
+
const runs = commentEl.getElementsByTagNameNS(OOXML.W_NS, W.r);
|
|
689
|
+
for (let i = 0; i < runs.length; i++) {
|
|
690
|
+
const run = runs.item(i);
|
|
691
|
+
// Skip runs that contain annotationRef (they're metadata, not user text)
|
|
692
|
+
const annotRefs = run.getElementsByTagNameNS(OOXML.W_NS, W.annotationRef);
|
|
693
|
+
if (annotRefs.length > 0)
|
|
694
|
+
continue;
|
|
695
|
+
const ts = run.getElementsByTagNameNS(OOXML.W_NS, W.t);
|
|
696
|
+
for (let j = 0; j < ts.length; j++) {
|
|
697
|
+
const t = ts.item(j);
|
|
698
|
+
parts.push(t.textContent ?? '');
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return parts.join('');
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=comments.js.map
|