@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.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -28
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/atomizer.d.ts +218 -0
  5. package/dist/atomizer.d.ts.map +1 -0
  6. package/dist/atomizer.js +856 -0
  7. package/dist/atomizer.js.map +1 -0
  8. package/dist/baselines/atomizer/atomLcs.d.ts +96 -0
  9. package/dist/baselines/atomizer/atomLcs.d.ts.map +1 -0
  10. package/dist/baselines/atomizer/atomLcs.js +347 -0
  11. package/dist/baselines/atomizer/atomLcs.js.map +1 -0
  12. package/dist/baselines/atomizer/debug.d.ts +41 -0
  13. package/dist/baselines/atomizer/debug.d.ts.map +1 -0
  14. package/dist/baselines/atomizer/debug.js +85 -0
  15. package/dist/baselines/atomizer/debug.js.map +1 -0
  16. package/dist/baselines/atomizer/documentReconstructor.d.ts +64 -0
  17. package/dist/baselines/atomizer/documentReconstructor.d.ts.map +1 -0
  18. package/dist/baselines/atomizer/documentReconstructor.js +939 -0
  19. package/dist/baselines/atomizer/documentReconstructor.js.map +1 -0
  20. package/dist/baselines/atomizer/hierarchicalLcs.d.ts +111 -0
  21. package/dist/baselines/atomizer/hierarchicalLcs.d.ts.map +1 -0
  22. package/dist/baselines/atomizer/hierarchicalLcs.js +469 -0
  23. package/dist/baselines/atomizer/hierarchicalLcs.js.map +1 -0
  24. package/dist/baselines/atomizer/inPlaceModifier.d.ts +183 -0
  25. package/dist/baselines/atomizer/inPlaceModifier.d.ts.map +1 -0
  26. package/dist/baselines/atomizer/inPlaceModifier.js +1600 -0
  27. package/dist/baselines/atomizer/inPlaceModifier.js.map +1 -0
  28. package/dist/baselines/atomizer/numberingIntegration.d.ts +59 -0
  29. package/dist/baselines/atomizer/numberingIntegration.d.ts.map +1 -0
  30. package/dist/baselines/atomizer/numberingIntegration.js +209 -0
  31. package/dist/baselines/atomizer/numberingIntegration.js.map +1 -0
  32. package/dist/baselines/atomizer/pipeline.d.ts +65 -0
  33. package/dist/baselines/atomizer/pipeline.d.ts.map +1 -0
  34. package/dist/baselines/atomizer/pipeline.js +510 -0
  35. package/dist/baselines/atomizer/pipeline.js.map +1 -0
  36. package/dist/baselines/atomizer/premergeRuns.d.ts +26 -0
  37. package/dist/baselines/atomizer/premergeRuns.d.ts.map +1 -0
  38. package/dist/baselines/atomizer/premergeRuns.js +150 -0
  39. package/dist/baselines/atomizer/premergeRuns.js.map +1 -0
  40. package/dist/baselines/atomizer/trackChangesAcceptor.d.ts +63 -0
  41. package/dist/baselines/atomizer/trackChangesAcceptor.d.ts.map +1 -0
  42. package/dist/baselines/atomizer/trackChangesAcceptor.js +254 -0
  43. package/dist/baselines/atomizer/trackChangesAcceptor.js.map +1 -0
  44. package/dist/baselines/atomizer/trackChangesAcceptorAst.d.ts +64 -0
  45. package/dist/baselines/atomizer/trackChangesAcceptorAst.d.ts.map +1 -0
  46. package/dist/baselines/atomizer/trackChangesAcceptorAst.js +586 -0
  47. package/dist/baselines/atomizer/trackChangesAcceptorAst.js.map +1 -0
  48. package/dist/baselines/atomizer/xmlToWmlElement.d.ts +65 -0
  49. package/dist/baselines/atomizer/xmlToWmlElement.d.ts.map +1 -0
  50. package/dist/baselines/atomizer/xmlToWmlElement.js +95 -0
  51. package/dist/baselines/atomizer/xmlToWmlElement.js.map +1 -0
  52. package/dist/baselines/diffmatch/documentBuilder.d.ts +44 -0
  53. package/dist/baselines/diffmatch/documentBuilder.d.ts.map +1 -0
  54. package/dist/baselines/diffmatch/documentBuilder.js +227 -0
  55. package/dist/baselines/diffmatch/documentBuilder.js.map +1 -0
  56. package/dist/baselines/diffmatch/paragraphAlignment.d.ts +75 -0
  57. package/dist/baselines/diffmatch/paragraphAlignment.d.ts.map +1 -0
  58. package/dist/baselines/diffmatch/paragraphAlignment.js +206 -0
  59. package/dist/baselines/diffmatch/paragraphAlignment.js.map +1 -0
  60. package/dist/baselines/diffmatch/pipeline.d.ts +33 -0
  61. package/dist/baselines/diffmatch/pipeline.d.ts.map +1 -0
  62. package/dist/baselines/diffmatch/pipeline.js +84 -0
  63. package/dist/baselines/diffmatch/pipeline.js.map +1 -0
  64. package/dist/baselines/diffmatch/runDiff.d.ts +53 -0
  65. package/dist/baselines/diffmatch/runDiff.d.ts.map +1 -0
  66. package/dist/baselines/diffmatch/runDiff.js +253 -0
  67. package/dist/baselines/diffmatch/runDiff.js.map +1 -0
  68. package/dist/baselines/diffmatch/trackChangesRenderer.d.ts +64 -0
  69. package/dist/baselines/diffmatch/trackChangesRenderer.d.ts.map +1 -0
  70. package/dist/baselines/diffmatch/trackChangesRenderer.js +178 -0
  71. package/dist/baselines/diffmatch/trackChangesRenderer.js.map +1 -0
  72. package/dist/baselines/diffmatch/xmlParser.d.ts +45 -0
  73. package/dist/baselines/diffmatch/xmlParser.d.ts.map +1 -0
  74. package/dist/baselines/diffmatch/xmlParser.js +344 -0
  75. package/dist/baselines/diffmatch/xmlParser.js.map +1 -0
  76. package/dist/baselines/wmlcomparer/DocxodusWasm.d.ts +51 -0
  77. package/dist/baselines/wmlcomparer/DocxodusWasm.d.ts.map +1 -0
  78. package/dist/baselines/wmlcomparer/DocxodusWasm.js +83 -0
  79. package/dist/baselines/wmlcomparer/DocxodusWasm.js.map +1 -0
  80. package/dist/baselines/wmlcomparer/DotnetCli.d.ts +40 -0
  81. package/dist/baselines/wmlcomparer/DotnetCli.d.ts.map +1 -0
  82. package/dist/baselines/wmlcomparer/DotnetCli.js +135 -0
  83. package/dist/baselines/wmlcomparer/DotnetCli.js.map +1 -0
  84. package/dist/benchmark/metrics.d.ts +72 -0
  85. package/dist/benchmark/metrics.d.ts.map +1 -0
  86. package/dist/benchmark/metrics.js +45 -0
  87. package/dist/benchmark/metrics.js.map +1 -0
  88. package/dist/benchmark/reporter.d.ts +23 -0
  89. package/dist/benchmark/reporter.d.ts.map +1 -0
  90. package/dist/benchmark/reporter.js +147 -0
  91. package/dist/benchmark/reporter.js.map +1 -0
  92. package/dist/benchmark/runner.d.ts +30 -0
  93. package/dist/benchmark/runner.d.ts.map +1 -0
  94. package/dist/benchmark/runner.js +233 -0
  95. package/dist/benchmark/runner.js.map +1 -0
  96. package/dist/cli/compare-two.d.ts +28 -0
  97. package/dist/cli/compare-two.d.ts.map +1 -0
  98. package/dist/cli/compare-two.js +110 -0
  99. package/dist/cli/compare-two.js.map +1 -0
  100. package/dist/cli/index.d.ts +3 -0
  101. package/dist/cli/index.d.ts.map +1 -0
  102. package/dist/cli/index.js +21 -0
  103. package/dist/cli/index.js.map +1 -0
  104. package/dist/core-types.d.ts +296 -0
  105. package/dist/core-types.d.ts.map +1 -0
  106. package/dist/core-types.js +122 -0
  107. package/dist/core-types.js.map +1 -0
  108. package/dist/footnotes.d.ts +144 -0
  109. package/dist/footnotes.d.ts.map +1 -0
  110. package/dist/footnotes.js +291 -0
  111. package/dist/footnotes.js.map +1 -0
  112. package/dist/format-detection.d.ts +120 -0
  113. package/dist/format-detection.d.ts.map +1 -0
  114. package/dist/format-detection.js +338 -0
  115. package/dist/format-detection.js.map +1 -0
  116. package/dist/index.d.ts +177 -0
  117. package/dist/index.d.ts.map +1 -0
  118. package/dist/index.js +55 -0
  119. package/dist/index.js.map +1 -0
  120. package/dist/integration/output-artifacts.d.ts +6 -0
  121. package/dist/integration/output-artifacts.d.ts.map +1 -0
  122. package/dist/integration/output-artifacts.js +30 -0
  123. package/dist/integration/output-artifacts.js.map +1 -0
  124. package/dist/move-detection.d.ts +211 -0
  125. package/dist/move-detection.d.ts.map +1 -0
  126. package/dist/move-detection.js +391 -0
  127. package/dist/move-detection.js.map +1 -0
  128. package/dist/numbering.d.ts +136 -0
  129. package/dist/numbering.d.ts.map +1 -0
  130. package/dist/numbering.js +446 -0
  131. package/dist/numbering.js.map +1 -0
  132. package/dist/primitives/accept_changes.d.ts +30 -0
  133. package/dist/primitives/accept_changes.d.ts.map +1 -0
  134. package/dist/primitives/accept_changes.js +241 -0
  135. package/dist/primitives/accept_changes.js.map +1 -0
  136. package/dist/primitives/bookmarks.d.ts +12 -0
  137. package/dist/primitives/bookmarks.d.ts.map +1 -0
  138. package/dist/primitives/bookmarks.js +248 -0
  139. package/dist/primitives/bookmarks.js.map +1 -0
  140. package/dist/primitives/comments.d.ts +88 -0
  141. package/dist/primitives/comments.d.ts.map +1 -0
  142. package/dist/primitives/comments.js +703 -0
  143. package/dist/primitives/comments.js.map +1 -0
  144. package/dist/primitives/document.d.ts +168 -0
  145. package/dist/primitives/document.d.ts.map +1 -0
  146. package/dist/primitives/document.js +532 -0
  147. package/dist/primitives/document.js.map +1 -0
  148. package/dist/primitives/document_view.d.ts +93 -0
  149. package/dist/primitives/document_view.d.ts.map +1 -0
  150. package/dist/primitives/document_view.js +722 -0
  151. package/dist/primitives/document_view.js.map +1 -0
  152. package/dist/primitives/dom-helpers.d.ts +94 -0
  153. package/dist/primitives/dom-helpers.d.ts.map +1 -0
  154. package/dist/primitives/dom-helpers.js +219 -0
  155. package/dist/primitives/dom-helpers.js.map +1 -0
  156. package/dist/primitives/errors.d.ts +7 -0
  157. package/dist/primitives/errors.d.ts.map +1 -0
  158. package/dist/primitives/errors.js +10 -0
  159. package/dist/primitives/errors.js.map +1 -0
  160. package/dist/primitives/extract_revisions.d.ts +50 -0
  161. package/dist/primitives/extract_revisions.d.ts.map +1 -0
  162. package/dist/primitives/extract_revisions.js +340 -0
  163. package/dist/primitives/extract_revisions.js.map +1 -0
  164. package/dist/primitives/footnotes.d.ts +37 -0
  165. package/dist/primitives/footnotes.d.ts.map +1 -0
  166. package/dist/primitives/footnotes.js +552 -0
  167. package/dist/primitives/footnotes.js.map +1 -0
  168. package/dist/primitives/formatting_tags.d.ts +30 -0
  169. package/dist/primitives/formatting_tags.d.ts.map +1 -0
  170. package/dist/primitives/formatting_tags.js +217 -0
  171. package/dist/primitives/formatting_tags.js.map +1 -0
  172. package/dist/primitives/index.d.ts +26 -0
  173. package/dist/primitives/index.d.ts.map +1 -0
  174. package/dist/primitives/index.js +26 -0
  175. package/dist/primitives/index.js.map +1 -0
  176. package/dist/primitives/layout.d.ts +53 -0
  177. package/dist/primitives/layout.d.ts.map +1 -0
  178. package/dist/primitives/layout.js +178 -0
  179. package/dist/primitives/layout.js.map +1 -0
  180. package/dist/primitives/list_labels.d.ts +19 -0
  181. package/dist/primitives/list_labels.d.ts.map +1 -0
  182. package/dist/primitives/list_labels.js +57 -0
  183. package/dist/primitives/list_labels.js.map +1 -0
  184. package/dist/primitives/matching.d.ts +17 -0
  185. package/dist/primitives/matching.d.ts.map +1 -0
  186. package/dist/primitives/matching.js +144 -0
  187. package/dist/primitives/matching.js.map +1 -0
  188. package/dist/primitives/merge_runs.d.ts +23 -0
  189. package/dist/primitives/merge_runs.d.ts.map +1 -0
  190. package/dist/primitives/merge_runs.js +195 -0
  191. package/dist/primitives/merge_runs.js.map +1 -0
  192. package/dist/primitives/namespaces.d.ts +90 -0
  193. package/dist/primitives/namespaces.d.ts.map +1 -0
  194. package/dist/primitives/namespaces.js +107 -0
  195. package/dist/primitives/namespaces.js.map +1 -0
  196. package/dist/primitives/numbering.d.ts +27 -0
  197. package/dist/primitives/numbering.d.ts.map +1 -0
  198. package/dist/primitives/numbering.js +182 -0
  199. package/dist/primitives/numbering.js.map +1 -0
  200. package/dist/primitives/prevent_double_elevation.d.ts +18 -0
  201. package/dist/primitives/prevent_double_elevation.d.ts.map +1 -0
  202. package/dist/primitives/prevent_double_elevation.js +190 -0
  203. package/dist/primitives/prevent_double_elevation.js.map +1 -0
  204. package/dist/primitives/reject_changes.d.ts +27 -0
  205. package/dist/primitives/reject_changes.d.ts.map +1 -0
  206. package/dist/primitives/reject_changes.js +371 -0
  207. package/dist/primitives/reject_changes.js.map +1 -0
  208. package/dist/primitives/relationships.d.ts +7 -0
  209. package/dist/primitives/relationships.d.ts.map +1 -0
  210. package/dist/primitives/relationships.js +24 -0
  211. package/dist/primitives/relationships.js.map +1 -0
  212. package/dist/primitives/semantic_tags.d.ts +32 -0
  213. package/dist/primitives/semantic_tags.d.ts.map +1 -0
  214. package/dist/primitives/semantic_tags.js +139 -0
  215. package/dist/primitives/semantic_tags.js.map +1 -0
  216. package/dist/primitives/simplify_redlines.d.ts +19 -0
  217. package/dist/primitives/simplify_redlines.d.ts.map +1 -0
  218. package/dist/primitives/simplify_redlines.js +94 -0
  219. package/dist/primitives/simplify_redlines.js.map +1 -0
  220. package/dist/primitives/styles.d.ts +36 -0
  221. package/dist/primitives/styles.d.ts.map +1 -0
  222. package/dist/primitives/styles.js +190 -0
  223. package/dist/primitives/styles.js.map +1 -0
  224. package/dist/primitives/text.d.ts +27 -0
  225. package/dist/primitives/text.d.ts.map +1 -0
  226. package/dist/primitives/text.js +416 -0
  227. package/dist/primitives/text.js.map +1 -0
  228. package/dist/primitives/validate_document.d.ts +24 -0
  229. package/dist/primitives/validate_document.d.ts.map +1 -0
  230. package/dist/primitives/validate_document.js +147 -0
  231. package/dist/primitives/validate_document.js.map +1 -0
  232. package/dist/primitives/xml.d.ts +5 -0
  233. package/dist/primitives/xml.d.ts.map +1 -0
  234. package/dist/primitives/xml.js +19 -0
  235. package/dist/primitives/xml.js.map +1 -0
  236. package/dist/primitives/zip.d.ts +25 -0
  237. package/dist/primitives/zip.d.ts.map +1 -0
  238. package/dist/primitives/zip.js +78 -0
  239. package/dist/primitives/zip.js.map +1 -0
  240. package/dist/shared/docx/DocxArchive.d.ts +94 -0
  241. package/dist/shared/docx/DocxArchive.d.ts.map +1 -0
  242. package/dist/shared/docx/DocxArchive.js +169 -0
  243. package/dist/shared/docx/DocxArchive.js.map +1 -0
  244. package/dist/shared/ooxml/namespaces.d.ts +149 -0
  245. package/dist/shared/ooxml/namespaces.d.ts.map +1 -0
  246. package/dist/shared/ooxml/namespaces.js +224 -0
  247. package/dist/shared/ooxml/namespaces.js.map +1 -0
  248. package/dist/shared/ooxml/types.d.ts +136 -0
  249. package/dist/shared/ooxml/types.d.ts.map +1 -0
  250. package/dist/shared/ooxml/types.js +7 -0
  251. package/dist/shared/ooxml/types.js.map +1 -0
  252. 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