docxmlater 10.1.4 → 10.1.6

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 (372) hide show
  1. package/README.md +759 -754
  2. package/dist/constants/legacyCompatFlags.js +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.js.map +1 -1
  5. package/dist/core/Document.d.ts +51 -50
  6. package/dist/core/Document.d.ts.map +1 -1
  7. package/dist/core/Document.js +486 -471
  8. package/dist/core/Document.js.map +1 -1
  9. package/dist/core/DocumentContent.d.ts +9 -9
  10. package/dist/core/DocumentContent.d.ts.map +1 -1
  11. package/dist/core/DocumentContent.js +1 -1
  12. package/dist/core/DocumentContent.js.map +1 -1
  13. package/dist/core/DocumentGenerator.d.ts +11 -11
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +251 -251
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.js.map +1 -1
  18. package/dist/core/DocumentParser.d.ts +15 -15
  19. package/dist/core/DocumentParser.d.ts.map +1 -1
  20. package/dist/core/DocumentParser.js +2123 -2155
  21. package/dist/core/DocumentParser.js.map +1 -1
  22. package/dist/core/DocumentValidator.d.ts.map +1 -1
  23. package/dist/core/DocumentValidator.js +2 -5
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.js.map +1 -1
  26. package/dist/core/RelationshipManager.d.ts.map +1 -1
  27. package/dist/core/RelationshipManager.js +3 -3
  28. package/dist/core/RelationshipManager.js.map +1 -1
  29. package/dist/elements/AlternateContent.js.map +1 -1
  30. package/dist/elements/Bookmark.d.ts.map +1 -1
  31. package/dist/elements/Bookmark.js +3 -1
  32. package/dist/elements/Bookmark.js.map +1 -1
  33. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  34. package/dist/elements/BookmarkManager.js.map +1 -1
  35. package/dist/elements/Comment.d.ts.map +1 -1
  36. package/dist/elements/Comment.js +9 -6
  37. package/dist/elements/Comment.js.map +1 -1
  38. package/dist/elements/CommentManager.d.ts.map +1 -1
  39. package/dist/elements/CommentManager.js +18 -17
  40. package/dist/elements/CommentManager.js.map +1 -1
  41. package/dist/elements/CommonTypes.d.ts +21 -21
  42. package/dist/elements/CommonTypes.d.ts.map +1 -1
  43. package/dist/elements/CommonTypes.js +56 -56
  44. package/dist/elements/CommonTypes.js.map +1 -1
  45. package/dist/elements/CustomXml.js.map +1 -1
  46. package/dist/elements/Endnote.d.ts.map +1 -1
  47. package/dist/elements/Endnote.js +6 -6
  48. package/dist/elements/Endnote.js.map +1 -1
  49. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  50. package/dist/elements/EndnoteManager.js +6 -7
  51. package/dist/elements/EndnoteManager.js.map +1 -1
  52. package/dist/elements/Field.d.ts.map +1 -1
  53. package/dist/elements/Field.js +82 -25
  54. package/dist/elements/Field.js.map +1 -1
  55. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  56. package/dist/elements/FieldHelpers.js.map +1 -1
  57. package/dist/elements/FontManager.d.ts.map +1 -1
  58. package/dist/elements/FontManager.js +1 -1
  59. package/dist/elements/FontManager.js.map +1 -1
  60. package/dist/elements/Footer.js +2 -2
  61. package/dist/elements/Footer.js.map +1 -1
  62. package/dist/elements/Footnote.d.ts.map +1 -1
  63. package/dist/elements/Footnote.js +6 -6
  64. package/dist/elements/Footnote.js.map +1 -1
  65. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  66. package/dist/elements/FootnoteManager.js +6 -7
  67. package/dist/elements/FootnoteManager.js.map +1 -1
  68. package/dist/elements/Header.js +2 -2
  69. package/dist/elements/Header.js.map +1 -1
  70. package/dist/elements/HeaderFooterManager.js.map +1 -1
  71. package/dist/elements/Hyperlink.d.ts +5 -3
  72. package/dist/elements/Hyperlink.d.ts.map +1 -1
  73. package/dist/elements/Hyperlink.js +134 -76
  74. package/dist/elements/Hyperlink.js.map +1 -1
  75. package/dist/elements/Image.d.ts.map +1 -1
  76. package/dist/elements/Image.js +238 -106
  77. package/dist/elements/Image.js.map +1 -1
  78. package/dist/elements/ImageManager.d.ts.map +1 -1
  79. package/dist/elements/ImageManager.js +1 -1
  80. package/dist/elements/ImageManager.js.map +1 -1
  81. package/dist/elements/ImageRun.js +1 -1
  82. package/dist/elements/ImageRun.js.map +1 -1
  83. package/dist/elements/MathElement.js.map +1 -1
  84. package/dist/elements/Paragraph.d.ts +24 -24
  85. package/dist/elements/Paragraph.d.ts.map +1 -1
  86. package/dist/elements/Paragraph.js +181 -188
  87. package/dist/elements/Paragraph.js.map +1 -1
  88. package/dist/elements/PreservedElement.js.map +1 -1
  89. package/dist/elements/PropertyChangeTypes.d.ts.map +1 -1
  90. package/dist/elements/PropertyChangeTypes.js +6 -6
  91. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  92. package/dist/elements/RangeMarker.d.ts.map +1 -1
  93. package/dist/elements/RangeMarker.js.map +1 -1
  94. package/dist/elements/Revision.d.ts.map +1 -1
  95. package/dist/elements/Revision.js +4 -5
  96. package/dist/elements/Revision.js.map +1 -1
  97. package/dist/elements/RevisionContent.js.map +1 -1
  98. package/dist/elements/RevisionManager.d.ts.map +1 -1
  99. package/dist/elements/RevisionManager.js +40 -48
  100. package/dist/elements/RevisionManager.js.map +1 -1
  101. package/dist/elements/Run.d.ts +16 -16
  102. package/dist/elements/Run.d.ts.map +1 -1
  103. package/dist/elements/Run.js +256 -238
  104. package/dist/elements/Run.js.map +1 -1
  105. package/dist/elements/Section.d.ts.map +1 -1
  106. package/dist/elements/Section.js +36 -11
  107. package/dist/elements/Section.js.map +1 -1
  108. package/dist/elements/Shape.d.ts.map +1 -1
  109. package/dist/elements/Shape.js.map +1 -1
  110. package/dist/elements/StructuredDocumentTag.d.ts +6 -6
  111. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  112. package/dist/elements/StructuredDocumentTag.js +99 -104
  113. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  114. package/dist/elements/Table.d.ts +11 -11
  115. package/dist/elements/Table.d.ts.map +1 -1
  116. package/dist/elements/Table.js +102 -107
  117. package/dist/elements/Table.js.map +1 -1
  118. package/dist/elements/TableCell.d.ts +10 -10
  119. package/dist/elements/TableCell.d.ts.map +1 -1
  120. package/dist/elements/TableCell.js +105 -106
  121. package/dist/elements/TableCell.js.map +1 -1
  122. package/dist/elements/TableGridChange.d.ts.map +1 -1
  123. package/dist/elements/TableGridChange.js.map +1 -1
  124. package/dist/elements/TableOfContents.d.ts.map +1 -1
  125. package/dist/elements/TableOfContents.js +4 -4
  126. package/dist/elements/TableOfContents.js.map +1 -1
  127. package/dist/elements/TableOfContentsElement.js.map +1 -1
  128. package/dist/elements/TableRow.d.ts.map +1 -1
  129. package/dist/elements/TableRow.js +13 -6
  130. package/dist/elements/TableRow.js.map +1 -1
  131. package/dist/elements/TextBox.d.ts.map +1 -1
  132. package/dist/elements/TextBox.js +3 -5
  133. package/dist/elements/TextBox.js.map +1 -1
  134. package/dist/formatting/AbstractNumbering.d.ts +4 -4
  135. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  136. package/dist/formatting/AbstractNumbering.js +54 -49
  137. package/dist/formatting/AbstractNumbering.js.map +1 -1
  138. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  139. package/dist/formatting/NumberingInstance.js +1 -3
  140. package/dist/formatting/NumberingInstance.js.map +1 -1
  141. package/dist/formatting/NumberingLevel.d.ts +5 -5
  142. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  143. package/dist/formatting/NumberingLevel.js +119 -125
  144. package/dist/formatting/NumberingLevel.js.map +1 -1
  145. package/dist/formatting/NumberingManager.d.ts +1 -0
  146. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  147. package/dist/formatting/NumberingManager.js +27 -9
  148. package/dist/formatting/NumberingManager.js.map +1 -1
  149. package/dist/formatting/Style.d.ts +11 -11
  150. package/dist/formatting/Style.d.ts.map +1 -1
  151. package/dist/formatting/Style.js +219 -247
  152. package/dist/formatting/Style.js.map +1 -1
  153. package/dist/formatting/StylesManager.d.ts +2 -2
  154. package/dist/formatting/StylesManager.d.ts.map +1 -1
  155. package/dist/formatting/StylesManager.js +96 -102
  156. package/dist/formatting/StylesManager.js.map +1 -1
  157. package/dist/helpers/CleanupHelper.d.ts +1 -1
  158. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  159. package/dist/helpers/CleanupHelper.js +6 -6
  160. package/dist/helpers/CleanupHelper.js.map +1 -1
  161. package/dist/images/ImageOptimizer.js +7 -7
  162. package/dist/images/ImageOptimizer.js.map +1 -1
  163. package/dist/index.d.ts +9 -9
  164. package/dist/index.d.ts.map +1 -1
  165. package/dist/index.js.map +1 -1
  166. package/dist/managers/DrawingManager.js.map +1 -1
  167. package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
  168. package/dist/tracking/DocumentTrackingContext.js +23 -7
  169. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  170. package/dist/tracking/TrackingContext.d.ts.map +1 -1
  171. package/dist/tracking/TrackingContext.js.map +1 -1
  172. package/dist/types/compatibility-types.js.map +1 -1
  173. package/dist/types/formatting.js.map +1 -1
  174. package/dist/types/list-types.d.ts +6 -6
  175. package/dist/types/list-types.js.map +1 -1
  176. package/dist/types/settings-types.js.map +1 -1
  177. package/dist/types/styleConfig.d.ts +2 -2
  178. package/dist/types/styleConfig.js.map +1 -1
  179. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  180. package/dist/utils/ChangelogGenerator.js +97 -101
  181. package/dist/utils/ChangelogGenerator.js.map +1 -1
  182. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  183. package/dist/utils/CompatibilityUpgrader.js +1 -1
  184. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  185. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  186. package/dist/utils/InMemoryRevisionAcceptor.js +1 -6
  187. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  188. package/dist/utils/MoveOperationHelper.d.ts.map +1 -1
  189. package/dist/utils/MoveOperationHelper.js +1 -1
  190. package/dist/utils/MoveOperationHelper.js.map +1 -1
  191. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  192. package/dist/utils/RevisionAwareProcessor.js +2 -4
  193. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  194. package/dist/utils/RevisionWalker.d.ts.map +1 -1
  195. package/dist/utils/RevisionWalker.js +4 -12
  196. package/dist/utils/RevisionWalker.js.map +1 -1
  197. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  198. package/dist/utils/SelectiveRevisionAcceptor.js +2 -6
  199. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  200. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  201. package/dist/utils/ShadingResolver.js +1 -1
  202. package/dist/utils/ShadingResolver.js.map +1 -1
  203. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  204. package/dist/utils/acceptRevisions.js +23 -12
  205. package/dist/utils/acceptRevisions.js.map +1 -1
  206. package/dist/utils/cnfStyleDecoder.d.ts +1 -1
  207. package/dist/utils/cnfStyleDecoder.d.ts.map +1 -1
  208. package/dist/utils/cnfStyleDecoder.js +40 -40
  209. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  210. package/dist/utils/corruptionDetection.d.ts.map +1 -1
  211. package/dist/utils/corruptionDetection.js.map +1 -1
  212. package/dist/utils/dateFormatting.js.map +1 -1
  213. package/dist/utils/deepClone.js +1 -1
  214. package/dist/utils/deepClone.js.map +1 -1
  215. package/dist/utils/diagnostics.d.ts.map +1 -1
  216. package/dist/utils/diagnostics.js +1 -1
  217. package/dist/utils/diagnostics.js.map +1 -1
  218. package/dist/utils/errorHandling.js.map +1 -1
  219. package/dist/utils/formatting.d.ts.map +1 -1
  220. package/dist/utils/formatting.js +10 -2
  221. package/dist/utils/formatting.js.map +1 -1
  222. package/dist/utils/list-detection.d.ts +2 -2
  223. package/dist/utils/list-detection.d.ts.map +1 -1
  224. package/dist/utils/list-detection.js +21 -23
  225. package/dist/utils/list-detection.js.map +1 -1
  226. package/dist/utils/logger.d.ts.map +1 -1
  227. package/dist/utils/logger.js +12 -7
  228. package/dist/utils/logger.js.map +1 -1
  229. package/dist/utils/parsingHelpers.js.map +1 -1
  230. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  231. package/dist/utils/stripTrackedChanges.js +3 -3
  232. package/dist/utils/stripTrackedChanges.js.map +1 -1
  233. package/dist/utils/textDiff.d.ts +1 -1
  234. package/dist/utils/textDiff.js +8 -8
  235. package/dist/utils/textDiff.js.map +1 -1
  236. package/dist/utils/units.js.map +1 -1
  237. package/dist/utils/validation.d.ts.map +1 -1
  238. package/dist/utils/validation.js +24 -7
  239. package/dist/utils/validation.js.map +1 -1
  240. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  241. package/dist/utils/xmlSanitization.js +3 -3
  242. package/dist/utils/xmlSanitization.js.map +1 -1
  243. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  244. package/dist/validation/RevisionAutoFixer.js +5 -5
  245. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  246. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  247. package/dist/validation/RevisionValidator.js +7 -9
  248. package/dist/validation/RevisionValidator.js.map +1 -1
  249. package/dist/validation/ValidationRules.js +3 -3
  250. package/dist/validation/ValidationRules.js.map +1 -1
  251. package/dist/validation/index.js.map +1 -1
  252. package/dist/xml/XMLBuilder.d.ts +1 -1
  253. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  254. package/dist/xml/XMLBuilder.js +98 -100
  255. package/dist/xml/XMLBuilder.js.map +1 -1
  256. package/dist/xml/XMLParser.d.ts.map +1 -1
  257. package/dist/xml/XMLParser.js +61 -66
  258. package/dist/xml/XMLParser.js.map +1 -1
  259. package/dist/zip/ZipHandler.d.ts.map +1 -1
  260. package/dist/zip/ZipHandler.js.map +1 -1
  261. package/dist/zip/ZipReader.d.ts.map +1 -1
  262. package/dist/zip/ZipReader.js +1 -3
  263. package/dist/zip/ZipReader.js.map +1 -1
  264. package/dist/zip/ZipWriter.d.ts +1 -1
  265. package/dist/zip/ZipWriter.d.ts.map +1 -1
  266. package/dist/zip/ZipWriter.js +28 -36
  267. package/dist/zip/ZipWriter.js.map +1 -1
  268. package/dist/zip/types.js +1 -1
  269. package/dist/zip/types.js.map +1 -1
  270. package/package.json +92 -92
  271. package/src/__tests__/helper-methods.test.ts +512 -512
  272. package/src/constants/legacyCompatFlags.ts +138 -138
  273. package/src/constants/limits.ts +50 -50
  274. package/src/core/Document.ts +1010 -1145
  275. package/src/core/DocumentContent.ts +461 -467
  276. package/src/core/DocumentGenerator.ts +1133 -1104
  277. package/src/core/DocumentIdManager.ts +158 -158
  278. package/src/core/DocumentParser.ts +2347 -2716
  279. package/src/core/DocumentValidator.ts +363 -372
  280. package/src/core/Relationship.ts +367 -367
  281. package/src/core/RelationshipManager.ts +429 -428
  282. package/src/elements/AlternateContent.ts +42 -42
  283. package/src/elements/Bookmark.ts +212 -210
  284. package/src/elements/BookmarkManager.ts +247 -250
  285. package/src/elements/Comment.ts +356 -359
  286. package/src/elements/CommentManager.ts +499 -502
  287. package/src/elements/CommonTypes.ts +524 -549
  288. package/src/elements/CustomXml.ts +36 -36
  289. package/src/elements/Endnote.ts +221 -217
  290. package/src/elements/EndnoteManager.ts +246 -249
  291. package/src/elements/Field.ts +1292 -1233
  292. package/src/elements/FieldHelpers.ts +329 -333
  293. package/src/elements/FontManager.ts +336 -339
  294. package/src/elements/Footer.ts +269 -269
  295. package/src/elements/Footnote.ts +221 -217
  296. package/src/elements/FootnoteManager.ts +246 -249
  297. package/src/elements/Header.ts +269 -269
  298. package/src/elements/HeaderFooterManager.ts +219 -219
  299. package/src/elements/Hyperlink.ts +1288 -1193
  300. package/src/elements/Image.ts +1982 -1756
  301. package/src/elements/ImageManager.ts +437 -432
  302. package/src/elements/ImageRun.ts +59 -59
  303. package/src/elements/MathElement.ts +65 -65
  304. package/src/elements/Paragraph.ts +4347 -4287
  305. package/src/elements/PreservedElement.ts +53 -53
  306. package/src/elements/PropertyChangeTypes.ts +458 -442
  307. package/src/elements/RangeMarker.ts +382 -400
  308. package/src/elements/Revision.ts +1198 -1217
  309. package/src/elements/RevisionContent.ts +73 -73
  310. package/src/elements/RevisionManager.ts +1070 -1070
  311. package/src/elements/Run.ts +3103 -3073
  312. package/src/elements/Section.ts +1521 -1421
  313. package/src/elements/Shape.ts +884 -873
  314. package/src/elements/StructuredDocumentTag.ts +1176 -1207
  315. package/src/elements/Table.ts +2468 -2524
  316. package/src/elements/TableCell.ts +1617 -1621
  317. package/src/elements/TableGridChange.ts +149 -151
  318. package/src/elements/TableOfContents.ts +701 -691
  319. package/src/elements/TableOfContentsElement.ts +89 -89
  320. package/src/elements/TableRow.ts +960 -929
  321. package/src/elements/TextBox.ts +766 -768
  322. package/src/formatting/AbstractNumbering.ts +580 -579
  323. package/src/formatting/NumberingInstance.ts +295 -299
  324. package/src/formatting/NumberingLevel.ts +981 -1040
  325. package/src/formatting/NumberingManager.ts +875 -827
  326. package/src/formatting/Style.ts +1785 -1879
  327. package/src/formatting/StylesManager.ts +1090 -1130
  328. package/src/helpers/CleanupHelper.ts +524 -524
  329. package/src/images/ImageOptimizer.ts +274 -274
  330. package/src/index.ts +559 -554
  331. package/src/managers/DrawingManager.ts +319 -319
  332. package/src/tracking/DocumentTrackingContext.ts +687 -674
  333. package/src/tracking/TrackingContext.ts +175 -173
  334. package/src/types/compatibility-types.ts +49 -49
  335. package/src/types/formatting.ts +210 -210
  336. package/src/types/list-types.ts +14 -14
  337. package/src/types/settings-types.ts +59 -59
  338. package/src/types/styleConfig.ts +189 -189
  339. package/src/utils/ChangelogGenerator.ts +1583 -1581
  340. package/src/utils/CompatibilityUpgrader.ts +235 -237
  341. package/src/utils/InMemoryRevisionAcceptor.ts +691 -696
  342. package/src/utils/MoveOperationHelper.ts +233 -238
  343. package/src/utils/RevisionAwareProcessor.ts +518 -526
  344. package/src/utils/RevisionWalker.ts +427 -457
  345. package/src/utils/SelectiveRevisionAcceptor.ts +662 -683
  346. package/src/utils/ShadingResolver.ts +105 -107
  347. package/src/utils/acceptRevisions.ts +723 -714
  348. package/src/utils/cnfStyleDecoder.ts +212 -217
  349. package/src/utils/corruptionDetection.ts +346 -345
  350. package/src/utils/dateFormatting.ts +20 -20
  351. package/src/utils/deepClone.ts +77 -78
  352. package/src/utils/diagnostics.ts +125 -129
  353. package/src/utils/errorHandling.ts +80 -80
  354. package/src/utils/formatting.ts +220 -213
  355. package/src/utils/list-detection.ts +32 -42
  356. package/src/utils/logger.ts +412 -404
  357. package/src/utils/parsingHelpers.ts +190 -190
  358. package/src/utils/stripTrackedChanges.ts +356 -353
  359. package/src/utils/textDiff.ts +100 -100
  360. package/src/utils/units.ts +421 -421
  361. package/src/utils/validation.ts +553 -542
  362. package/src/utils/xmlSanitization.ts +179 -182
  363. package/src/validation/RevisionAutoFixer.ts +541 -542
  364. package/src/validation/RevisionValidator.ts +470 -460
  365. package/src/validation/ValidationRules.ts +338 -338
  366. package/src/validation/index.ts +30 -30
  367. package/src/xml/XMLBuilder.ts +857 -871
  368. package/src/xml/XMLParser.ts +877 -919
  369. package/src/zip/ZipHandler.ts +629 -637
  370. package/src/zip/ZipReader.ts +295 -299
  371. package/src/zip/ZipWriter.ts +374 -390
  372. package/src/zip/types.ts +116 -116
@@ -1,714 +1,723 @@
1
- import { ZipHandler } from '../zip/ZipHandler';
2
- import { XMLParser } from '../xml/XMLParser';
3
- import { RevisionWalker } from './RevisionWalker';
4
-
5
- /**
6
- * Accepts all tracked changes in a Word document per Microsoft's OpenXML SDK pattern
7
- *
8
- * This implementation uses DOM-based tree walking for reliability:
9
- * 1. Insertions (<w:ins>): Keep content, remove wrapper tags
10
- * 2. Deletions (<w:del>): Remove entirely (content and tags)
11
- * 3. Move From (<w:moveFrom>): Remove entirely (source of move)
12
- * 4. Move To (<w:moveTo>): Keep content, remove wrapper (destination of move)
13
- * 5. Property changes: Remove all *Change elements
14
- * 6. Range markers: Remove all boundary markers
15
- *
16
- * Also cleans up metadata in people.xml, settings.xml, and core.xml
17
- *
18
- * @see https://learn.microsoft.com/en-us/office/open-xml/how-to-accept-all-revisions
19
- */
20
- class RevisionAcceptor {
21
- private zipHandler: ZipHandler;
22
- /** Feature flag for DOM-based processing (default: true) */
23
- private useDomBasedProcessing = true;
24
-
25
- constructor(zipHandler: ZipHandler) {
26
- this.zipHandler = zipHandler;
27
- }
28
-
29
- /**
30
- * Enable or disable DOM-based processing (for testing/migration)
31
- */
32
- setUseDomBasedProcessing(enabled: boolean): void {
33
- this.useDomBasedProcessing = enabled;
34
- }
35
-
36
- /**
37
- * Main method to accept all revisions in the document
38
- */
39
- public async acceptAllRevisions(): Promise<void> {
40
- // Process document.xml
41
- await this.processDocumentPart('word/document.xml');
42
-
43
- // Process headers
44
- const files = this.zipHandler.getFilePaths();
45
- for (const file of files) {
46
- if (/^word\/header\d+\.xml$/.exec(file)) {
47
- await this.processDocumentPart(file);
48
- }
49
- if (/^word\/footer\d+\.xml$/.exec(file)) {
50
- await this.processDocumentPart(file);
51
- }
52
- }
53
-
54
- // Clean up metadata files
55
- this.cleanupPeopleXml();
56
- this.cleanupSettingsXml();
57
- this.cleanupCorePropsXml();
58
- }
59
-
60
- /**
61
- * Process a document part (document.xml, header, footer) to accept revisions
62
- */
63
- private async processDocumentPart(partPath: string): Promise<void> {
64
- if (this.useDomBasedProcessing) {
65
- return this.processDocumentPartDOM(partPath);
66
- }
67
- return this.processDocumentPartRegex(partPath);
68
- }
69
-
70
- /**
71
- * DOM-based implementation of revision acceptance
72
- * Uses XMLParser and RevisionWalker for reliable processing
73
- */
74
- private processDocumentPartDOM(partPath: string): void {
75
- const xml = this.zipHandler.getFileAsString(partPath);
76
- if (!xml) {
77
- return;
78
- }
79
-
80
- // Step 1: Parse XML to object tree
81
- // IMPORTANT: trimValues: false preserves whitespace from xml:space="preserve" attributes
82
- const parsed = XMLParser.parseToObject(xml, { trimValues: false });
83
-
84
- // Step 2: Process revisions using DOM walker
85
- const processed = RevisionWalker.processTree(parsed, {
86
- acceptInsertions: true,
87
- acceptDeletions: true,
88
- acceptMoves: true,
89
- acceptPropertyChanges: true,
90
- });
91
-
92
- // Step 3: Handle image relationship ID remapping
93
- this.remapImageRelationshipsInTree(processed);
94
-
95
- // Step 4: Convert back to XML
96
- const outputXml =
97
- '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
98
- this.objectToXml(processed);
99
-
100
- // Step 5: Update file
101
- this.zipHandler.updateFile(partPath, outputXml);
102
- }
103
-
104
- /**
105
- * Legacy RegEx-based implementation (kept as fallback)
106
- */
107
- private processDocumentPartRegex(partPath: string): void {
108
- const xml = this.zipHandler.getFileAsString(partPath);
109
- if (!xml) {
110
- return;
111
- }
112
-
113
- let content = xml;
114
-
115
- // Step 1: Remove all range markers FIRST (before processing revisions)
116
- // This prevents orphaned references when revision content is modified
117
- content = this.removeAllRangeMarkers(content);
118
-
119
- // Step 2: Remove all property change elements
120
- // These track formatting changes and must be removed before other processing
121
- content = this.removeAllPropertyChanges(content);
122
-
123
- // Step 3: Process deletions - remove entire element INCLUDING content
124
- // Must be done before insertions to handle nested scenarios
125
- content = this.acceptDeletions(content);
126
-
127
- // Step 4: Process move operations
128
- // Remove moveFrom entirely (source), unwrap moveTo (destination)
129
- content = this.acceptMoveFrom(content);
130
- content = this.acceptMoveTo(content);
131
-
132
- // Step 5: Process insertions - keep content, remove wrapper
133
- content = this.acceptInsertions(content);
134
-
135
- // Step 6: Final cleanup - remove any remaining orphaned tags
136
- content = this.cleanupOrphanedTags(content);
137
-
138
- // Update the file
139
- this.zipHandler.updateFile(partPath, content);
140
- }
141
-
142
- /**
143
- * Remove all range marker elements
144
- * These are boundary markers for tracked changes and moves
145
- */
146
- private removeAllRangeMarkers(xml: string): string {
147
- const patterns = [
148
- // Move range markers
149
- /<w:moveFromRangeStart[^>]*(?:\/>|>.*?<\/w:moveFromRangeStart>)/gs,
150
- /<w:moveFromRangeEnd[^>]*(?:\/>|>.*?<\/w:moveFromRangeEnd>)/gs,
151
- /<w:moveToRangeStart[^>]*(?:\/>|>.*?<\/w:moveToRangeStart>)/gs,
152
- /<w:moveToRangeEnd[^>]*(?:\/>|>.*?<\/w:moveToRangeEnd>)/gs,
153
- // Custom XML range markers
154
- /<w:customXmlInsRangeStart[^>]*(?:\/>|>.*?<\/w:customXmlInsRangeStart>)/gs,
155
- /<w:customXmlInsRangeEnd[^>]*(?:\/>|>.*?<\/w:customXmlInsRangeEnd>)/gs,
156
- /<w:customXmlDelRangeStart[^>]*(?:\/>|>.*?<\/w:customXmlDelRangeStart>)/gs,
157
- /<w:customXmlDelRangeEnd[^>]*(?:\/>|>.*?<\/w:customXmlDelRangeEnd>)/gs,
158
- /<w:customXmlMoveFromRangeStart[^>]*(?:\/>|>.*?<\/w:customXmlMoveFromRangeStart>)/gs,
159
- /<w:customXmlMoveFromRangeEnd[^>]*(?:\/>|>.*?<\/w:customXmlMoveFromRangeEnd>)/gs,
160
- /<w:customXmlMoveToRangeStart[^>]*(?:\/>|>.*?<\/w:customXmlMoveToRangeStart>)/gs,
161
- /<w:customXmlMoveToRangeEnd[^>]*(?:\/>|>.*?<\/w:customXmlMoveToRangeEnd>)/gs,
162
- ];
163
-
164
- let result = xml;
165
- for (const pattern of patterns) {
166
- result = result.replace(pattern, '');
167
- }
168
- return result;
169
- }
170
-
171
- /**
172
- * Remove all property change tracking elements
173
- * Per ECMA-376, these track previous state of formatting
174
- */
175
- private removeAllPropertyChanges(xml: string): string {
176
- const patterns = [
177
- // Run property changes
178
- /<w:rPrChange[^>]*>[\s\S]*?<\/w:rPrChange>/g,
179
- // Paragraph property changes
180
- /<w:pPrChange[^>]*>[\s\S]*?<\/w:pPrChange>/g,
181
- // Table property changes
182
- /<w:tblPrChange[^>]*>[\s\S]*?<\/w:tblPrChange>/g,
183
- /<w:tblPrExChange[^>]*>[\s\S]*?<\/w:tblPrExChange>/g,
184
- // Table cell property changes
185
- /<w:tcPrChange[^>]*>[\s\S]*?<\/w:tcPrChange>/g,
186
- // Table row property changes
187
- /<w:trPrChange[^>]*>[\s\S]*?<\/w:trPrChange>/g,
188
- // Section property changes
189
- /<w:sectPrChange[^>]*>[\s\S]*?<\/w:sectPrChange>/g,
190
- // Table grid changes
191
- /<w:tblGridChange[^>]*>[\s\S]*?<\/w:tblGridChange>/g,
192
- // Numbering changes
193
- /<w:numberingChange[^>]*>[\s\S]*?<\/w:numberingChange>/g,
194
- ];
195
-
196
- let result = xml;
197
- for (const pattern of patterns) {
198
- result = result.replace(pattern, '');
199
- }
200
- return result;
201
- }
202
-
203
- /**
204
- * Accept deletions - remove the entire <w:del> element including its content
205
- *
206
- * Per Microsoft SDK: "DeletedRun elements should be removed along with their content"
207
- */
208
- private acceptDeletions(xml: string): string {
209
- let result = xml;
210
- let previousLength = 0;
211
-
212
- // Iterate until no more deletions (handles nested cases)
213
- while (result.length !== previousLength) {
214
- previousLength = result.length;
215
-
216
- // Match complete <w:del ...>...</w:del> elements and remove entirely
217
- result = result.replace(/<w:del\b[^>]*>[\s\S]*?<\/w:del>/g, '');
218
- }
219
-
220
- // Also remove self-closing deletion tags
221
- result = result.replace(/<w:del\b[^>]*\/>/g, '');
222
-
223
- return result;
224
- }
225
-
226
- /**
227
- * Accept moveFrom - remove the entire element (source of moved content)
228
- *
229
- * The content exists at the moveTo destination, so we discard the source
230
- */
231
- private acceptMoveFrom(xml: string): string {
232
- let result = xml;
233
- let previousLength = 0;
234
-
235
- while (result.length !== previousLength) {
236
- previousLength = result.length;
237
- result = result.replace(/<w:moveFrom\b[^>]*>[\s\S]*?<\/w:moveFrom>/g, '');
238
- }
239
-
240
- // Also remove self-closing tags
241
- result = result.replace(/<w:moveFrom\b[^>]*\/>/g, '');
242
-
243
- return result;
244
- }
245
-
246
- /**
247
- * Accept moveTo - keep the content, remove the wrapper tags
248
- *
249
- * The moveTo location is where the content should remain
250
- */
251
- private acceptMoveTo(xml: string): string {
252
- let result = xml;
253
-
254
- // Remove closing tags first (prevents issues with regex matching)
255
- result = result.replace(/<\/w:moveTo>/g, '');
256
-
257
- // Remove opening tags (keeps content that was inside)
258
- result = result.replace(/<w:moveTo\b[^>]*>/g, '');
259
-
260
- return result;
261
- }
262
-
263
- /**
264
- * Accept insertions - keep the content, remove the wrapper tags
265
- *
266
- * Per Microsoft SDK: "InsertedRun elements should be unwrapped, keeping their content"
267
- *
268
- * IMPORTANT: This method now handles relationship ID remapping for images inside insertions.
269
- * When Word tracks changes with images, it can reuse relationship IDs (like rId5) because
270
- * they're in separate tracked change contexts. But when we unwrap them, duplicate IDs
271
- * cause corruption. This method assigns new unique IDs to images inside insertions.
272
- */
273
- private acceptInsertions(xml: string): string {
274
- let result = xml;
275
-
276
- // Parse existing relationships
277
- const relationships = this.parseRelationships();
278
- const existingIds = new Set(relationships.keys());
279
-
280
- // Process each w:ins element and remap images one by one
281
- const insRegex = /<w:ins\b[^>]*>[\s\S]*?<\/w:ins>/g;
282
-
283
- result = result.replace(insRegex, (insMatch) => {
284
- // For each image reference inside this insertion, generate a unique new ID
285
- return insMatch.replace(/r:embed="(rId\d+)"/g, (embedMatch, oldId) => {
286
- // Generate new unique ID for THIS occurrence
287
- const newId = this.getNextRelationshipId(existingIds);
288
- existingIds.add(newId);
289
-
290
- // Add relationship with same target as original
291
- const target = relationships.get(oldId);
292
- if (target) {
293
- this.addRelationship(
294
- newId,
295
- target,
296
- 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'
297
- );
298
- }
299
-
300
- return `r:embed="${newId}"`;
301
- });
302
- });
303
-
304
- // Now unwrap the w:ins tags (content has unique remapped IDs)
305
- result = result.replace(/<\/w:ins>/g, '');
306
- result = result.replace(/<w:ins\b[^>]*>/g, '');
307
-
308
- return result;
309
- }
310
-
311
- /**
312
- * Final cleanup to remove any orphaned or malformed revision-related tags
313
- */
314
- private cleanupOrphanedTags(xml: string): string {
315
- let result = xml;
316
-
317
- // Remove any remaining self-closing revision tags
318
- result = result.replace(/<w:ins\b[^>]*\/>/g, '');
319
- result = result.replace(/<w:del\b[^>]*\/>/g, '');
320
- result = result.replace(/<w:moveFrom\b[^>]*\/>/g, '');
321
- result = result.replace(/<w:moveTo\b[^>]*\/>/g, '');
322
-
323
- // Remove empty w:r elements that might be left after removing deletions
324
- result = result.replace(/<w:r\b[^>]*>\s*<\/w:r>/g, '');
325
-
326
- // Remove empty w:p elements (but preserve those with properties)
327
- // We keep <w:p><w:pPr>...</w:pPr></w:p> as those are intentional empty paragraphs with styling
328
- result = result.replace(/<w:p>\s*<\/w:p>/g, '');
329
-
330
- return result;
331
- }
332
-
333
- /**
334
- * Parse relationship IDs from word/_rels/document.xml.rels
335
- * Returns a map of relationship ID to target path
336
- */
337
- private parseRelationships(): Map<string, string> {
338
- const relsXml = this.zipHandler.getFileAsString('word/_rels/document.xml.rels');
339
- if (!relsXml) return new Map();
340
-
341
- const map = new Map<string, string>();
342
- const relationshipRegex = /<Relationship[^>]*Id="([^"]+)"[^>]*Target="([^"]+)"[^>]*\/>/g;
343
- let match;
344
-
345
- while ((match = relationshipRegex.exec(relsXml)) !== null) {
346
- if (match[1] && match[2]) {
347
- map.set(match[1], match[2]); // rId -> target path
348
- }
349
- }
350
-
351
- return map;
352
- }
353
-
354
- /**
355
- * Get the next available relationship ID
356
- * Finds the highest numeric ID and increments it
357
- */
358
- private getNextRelationshipId(existingIds: Set<string>): string {
359
- let maxId = 0;
360
- for (const id of existingIds) {
361
- const num = parseInt(id.replace('rId', ''));
362
- if (!isNaN(num) && num > maxId) {
363
- maxId = num;
364
- }
365
- }
366
- return `rId${maxId + 1}`;
367
- }
368
-
369
- /**
370
- * Add a new relationship to word/_rels/document.xml.rels
371
- */
372
- private addRelationship(rId: string, target: string, type: string): void {
373
- const relsXml = this.zipHandler.getFileAsString('word/_rels/document.xml.rels');
374
- if (!relsXml) return;
375
-
376
- // Insert new relationship before closing tag
377
- const newRel = `<Relationship Id="${rId}" Type="${type}" Target="${target}"/>`;
378
- const updated = relsXml.replace('</Relationships>', `${newRel}\n</Relationships>`);
379
-
380
- this.zipHandler.updateFile('word/_rels/document.xml.rels', updated);
381
- }
382
-
383
- /**
384
- * Clean up all revision metadata files (people.xml, settings.xml, core.xml).
385
- *
386
- * This removes:
387
- * - All revision authors from people.xml
388
- * - Track changes settings from settings.xml
389
- * - Resets revision count in core.xml
390
- *
391
- * Called as part of acceptAllRevisions() but can also be called separately
392
- * when using in-memory revision acceptance.
393
- */
394
- public cleanupMetadata(): void {
395
- this.cleanupPeopleXml();
396
- this.cleanupSettingsXml();
397
- this.cleanupCorePropsXml();
398
- }
399
-
400
- /**
401
- * Clean up word/people.xml - remove all revision authors
402
- *
403
- * Handles both w: and w15: namespace variants
404
- */
405
- private cleanupPeopleXml(): void {
406
- const peopleXml = this.zipHandler.getFileAsString('word/people.xml');
407
- if (!peopleXml) {
408
- return;
409
- }
410
-
411
- let content = peopleXml;
412
-
413
- // Remove all person elements in any namespace variant
414
- content = content.replace(/<w:person\b[^>]*>[\s\S]*?<\/w:person>/g, '');
415
- content = content.replace(/<w15:person\b[^>]*>[\s\S]*?<\/w15:person>/g, '');
416
-
417
- // Handle any namespace-prefixed variants (w1:, w2:, etc.)
418
- content = content.replace(/<w\d+:person\b[^>]*>[\s\S]*?<\/w\d+:person>/g, '');
419
-
420
- // Also remove self-closing person elements
421
- content = content.replace(/<w:person\b[^>]*\/>/g, '');
422
- content = content.replace(/<w15:person\b[^>]*\/>/g, '');
423
- content = content.replace(/<w\d+:person\b[^>]*\/>/g, '');
424
-
425
- this.zipHandler.updateFile('word/people.xml', content);
426
- }
427
-
428
- /**
429
- * Clean up word/settings.xml - disable track changes
430
- */
431
- private cleanupSettingsXml(): void {
432
- const settingsXml = this.zipHandler.getFileAsString('word/settings.xml');
433
- if (!settingsXml) {
434
- return;
435
- }
436
-
437
- let content = settingsXml;
438
-
439
- // Remove trackRevisions element (enables tracking)
440
- content = content.replace(/<w:trackRevisions\b[^>]*\/>/g, '');
441
- content = content.replace(/<w:trackRevisions\b[^>]*>[\s\S]*?<\/w:trackRevisions>/g, '');
442
-
443
- // Remove revisionView element (controls which revisions are visible)
444
- content = content.replace(/<w:revisionView\b[^>]*\/>/g, '');
445
- content = content.replace(/<w:revisionView\b[^>]*>[\s\S]*?<\/w:revisionView>/g, '');
446
-
447
- // Remove doNotTrackMoves (prevents move tracking)
448
- content = content.replace(/<w:doNotTrackMoves\b[^>]*\/>/g, '');
449
- content = content.replace(/<w:doNotTrackMoves\b[^>]*>[\s\S]*?<\/w:doNotTrackMoves>/g, '');
450
-
451
- // Remove doNotTrackFormatting
452
- content = content.replace(/<w:doNotTrackFormatting\b[^>]*\/>/g, '');
453
- content = content.replace(/<w:doNotTrackFormatting\b[^>]*>[\s\S]*?<\/w:doNotTrackFormatting>/g, '');
454
-
455
- this.zipHandler.updateFile('word/settings.xml', content);
456
- }
457
-
458
- /**
459
- * Clean up docProps/core.xml - reset revision count
460
- */
461
- private cleanupCorePropsXml(): void {
462
- const coreXml = this.zipHandler.getFileAsString('docProps/core.xml');
463
- if (!coreXml) {
464
- return;
465
- }
466
-
467
- // Reset revision count to 1
468
- const content = coreXml.replace(
469
- /<cp:revision>\d+<\/cp:revision>/g,
470
- '<cp:revision>1</cp:revision>'
471
- );
472
-
473
- this.zipHandler.updateFile('docProps/core.xml', content);
474
- }
475
-
476
- // =========================================================================
477
- // DOM-based processing helper methods
478
- // =========================================================================
479
-
480
- /**
481
- * Convert parsed XML object back to XML string
482
- * Preserves element order using _orderedChildren metadata
483
- *
484
- * Based on DocumentParser.objectToXml() implementation
485
- */
486
- private objectToXml(obj: any): string {
487
- const buildXml = (o: any, name?: string): string => {
488
- // Handle simple string/number with a tag name: <tagName>value</tagName>
489
- if (name && (typeof o === 'string' || typeof o === 'number')) {
490
- return `<${name}>${this.escapeXml(String(o))}</${name}>`;
491
- }
492
- if (typeof o === 'string') return this.escapeXml(o);
493
- if (typeof o !== 'object' || o === null) return String(o ?? '');
494
-
495
- const keys = Object.keys(o);
496
-
497
- // If a name is provided, we're building a specific element
498
- // Don't return empty string for empty objects with a name - they become self-closing tags
499
- if (keys.length === 0 && !name) return '';
500
-
501
- const tagName = name || keys[0]!;
502
- const element = name ? o : o[tagName];
503
-
504
- let xml = `<${tagName}`;
505
-
506
- // Add attributes (keys starting with @_)
507
- if (element && typeof element === 'object') {
508
- for (const key of Object.keys(element)) {
509
- if (key.startsWith('@_')) {
510
- const attrName = key.substring(2);
511
- xml += ` ${attrName}="${this.escapeXml(String(element[key]))}"`;
512
- }
513
- }
514
- }
515
-
516
- // Check for children (non-attribute, non-text, non-metadata keys)
517
- const hasChildren =
518
- element &&
519
- typeof element === 'object' &&
520
- Object.keys(element).some(
521
- (k) =>
522
- !k.startsWith('@_') && k !== '#text' && k !== '_orderedChildren'
523
- );
524
-
525
- // Per ECMA-376, certain elements MUST NOT be self-closing (e.g., <w:p/> is invalid).
526
- // This mirrors the CANNOT_SELF_CLOSE list in XMLBuilder.ts.
527
- const CANNOT_SELF_CLOSE = [
528
- 'w:t', 'w:r', 'w:p', 'w:tbl', 'w:tr', 'w:tc', 'w:body',
529
- 'w:document', 'w:hyperlink', 'w:sdt', 'w:sdtContent', 'w:sdtPr',
530
- 'w:pPr', 'w:rPr', 'w:sectPr', 'w:del', 'w:ins', 'w:moveFrom', 'w:moveTo',
531
- ];
532
-
533
- if (!hasChildren && (!element?.['#text'])) {
534
- if (CANNOT_SELF_CLOSE.includes(tagName)) {
535
- xml += `></${tagName}>`;
536
- } else {
537
- xml += '/>';
538
- }
539
- } else {
540
- xml += '>';
541
-
542
- // Add text content
543
- if (element?.['#text']) {
544
- xml += this.escapeXml(String(element['#text']));
545
- }
546
-
547
- // Add child elements using _orderedChildren if available
548
- if (element && typeof element === 'object') {
549
- const orderedChildren = element._orderedChildren as
550
- | { type: string; index: number }[]
551
- | undefined;
552
-
553
- if (orderedChildren && orderedChildren.length > 0) {
554
- // Use _orderedChildren to preserve element order
555
- for (const childInfo of orderedChildren) {
556
- const childType = childInfo.type;
557
- const childIndex = childInfo.index;
558
-
559
- if (element[childType] !== undefined) {
560
- const children = element[childType];
561
-
562
- if (Array.isArray(children)) {
563
- if (childIndex < children.length) {
564
- const childXml = buildXml(children[childIndex], childType);
565
- xml += childXml;
566
- }
567
- } else {
568
- // Single child element
569
- if (childIndex === 0) {
570
- const childXml = buildXml(children, childType);
571
- xml += childXml;
572
- }
573
- }
574
- }
575
- }
576
- } else {
577
- // Fallback: iterate through keys if no _orderedChildren
578
- for (const key of Object.keys(element)) {
579
- if (
580
- !key.startsWith('@_') &&
581
- key !== '#text' &&
582
- key !== '_orderedChildren'
583
- ) {
584
- const children = element[key];
585
- if (Array.isArray(children)) {
586
- for (const child of children) {
587
- xml += buildXml(child, key);
588
- }
589
- } else {
590
- xml += buildXml(children, key);
591
- }
592
- }
593
- }
594
- }
595
- }
596
-
597
- xml += `</${tagName}>`;
598
- }
599
-
600
- return xml;
601
- };
602
-
603
- return buildXml(obj);
604
- }
605
-
606
- /**
607
- * Escape special XML characters
608
- */
609
- private escapeXml(str: string): string {
610
- return str
611
- .replace(/&/g, '&amp;')
612
- .replace(/</g, '&lt;')
613
- .replace(/>/g, '&gt;')
614
- .replace(/"/g, '&quot;')
615
- .replace(/'/g, '&apos;');
616
- }
617
-
618
- /**
619
- * Remap image relationship IDs in the parsed tree to prevent duplicates
620
- * Walks the tree looking for r:embed attributes and assigns unique IDs
621
- */
622
- private remapImageRelationshipsInTree(obj: any): void {
623
- const relationships = this.parseRelationships();
624
- const existingIds = new Set(relationships.keys());
625
- const remappedIds = new Map<string, string>();
626
-
627
- // Walk the tree and find all r:embed attributes
628
- this.walkTreeForEmbeds(obj, (embedId: string, parent: any, key: string) => {
629
- // Check if this ID has already been remapped
630
- if (remappedIds.has(embedId)) {
631
- parent[key] = remappedIds.get(embedId);
632
- return;
633
- }
634
-
635
- // Check if this ID needs remapping (duplicate)
636
- // For DOM-based processing, we check if we've seen this ID before in this pass
637
- const target = relationships.get(embedId);
638
- if (target) {
639
- // Generate new unique ID
640
- const newId = this.getNextRelationshipId(existingIds);
641
- existingIds.add(newId);
642
- remappedIds.set(embedId, newId);
643
-
644
- // Add relationship with same target
645
- this.addRelationship(
646
- newId,
647
- target,
648
- 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'
649
- );
650
-
651
- // Update the attribute
652
- parent[key] = newId;
653
- }
654
- });
655
- }
656
-
657
- /**
658
- * Walk the tree looking for r:embed attributes
659
- */
660
- private walkTreeForEmbeds(
661
- obj: any,
662
- callback: (embedId: string, parent: any, key: string) => void
663
- ): void {
664
- if (!obj || typeof obj !== 'object') {
665
- return;
666
- }
667
-
668
- for (const key of Object.keys(obj)) {
669
- // Check for r:embed attribute
670
- if (key === '@_r:embed') {
671
- callback(obj[key], obj, key);
672
- } else if (
673
- !key.startsWith('@_') &&
674
- key !== '#text' &&
675
- key !== '_orderedChildren'
676
- ) {
677
- const value = obj[key];
678
- if (Array.isArray(value)) {
679
- for (const item of value) {
680
- this.walkTreeForEmbeds(item, callback);
681
- }
682
- } else if (typeof value === 'object') {
683
- this.walkTreeForEmbeds(value, callback);
684
- }
685
- }
686
- }
687
- }
688
- }
689
-
690
- /**
691
- * Convenience function to accept all revisions in a document
692
- */
693
- export async function acceptAllRevisions(zipHandler: ZipHandler): Promise<void> {
694
- const acceptor = new RevisionAcceptor(zipHandler);
695
- await acceptor.acceptAllRevisions();
696
- }
697
-
698
- /**
699
- * Convenience function to clean up revision metadata files.
700
- *
701
- * This removes:
702
- * - All revision authors from people.xml
703
- * - Track changes settings from settings.xml
704
- * - Resets revision count in core.xml
705
- *
706
- * Use this after in-memory revision acceptance to ensure metadata is also cleaned.
707
- * The raw XML acceptAllRevisions() function calls this automatically.
708
- *
709
- * @param zipHandler - The ZipHandler containing the DOCX package
710
- */
711
- export function cleanupRevisionMetadata(zipHandler: ZipHandler): void {
712
- const acceptor = new RevisionAcceptor(zipHandler);
713
- acceptor.cleanupMetadata();
714
- }
1
+ import { ZipHandler } from '../zip/ZipHandler';
2
+ import { XMLParser } from '../xml/XMLParser';
3
+ import { RevisionWalker } from './RevisionWalker';
4
+
5
+ /**
6
+ * Accepts all tracked changes in a Word document per Microsoft's OpenXML SDK pattern
7
+ *
8
+ * This implementation uses DOM-based tree walking for reliability:
9
+ * 1. Insertions (<w:ins>): Keep content, remove wrapper tags
10
+ * 2. Deletions (<w:del>): Remove entirely (content and tags)
11
+ * 3. Move From (<w:moveFrom>): Remove entirely (source of move)
12
+ * 4. Move To (<w:moveTo>): Keep content, remove wrapper (destination of move)
13
+ * 5. Property changes: Remove all *Change elements
14
+ * 6. Range markers: Remove all boundary markers
15
+ *
16
+ * Also cleans up metadata in people.xml, settings.xml, and core.xml
17
+ *
18
+ * @see https://learn.microsoft.com/en-us/office/open-xml/how-to-accept-all-revisions
19
+ */
20
+ class RevisionAcceptor {
21
+ private zipHandler: ZipHandler;
22
+ /** Feature flag for DOM-based processing (default: true) */
23
+ private useDomBasedProcessing = true;
24
+
25
+ constructor(zipHandler: ZipHandler) {
26
+ this.zipHandler = zipHandler;
27
+ }
28
+
29
+ /**
30
+ * Enable or disable DOM-based processing (for testing/migration)
31
+ */
32
+ setUseDomBasedProcessing(enabled: boolean): void {
33
+ this.useDomBasedProcessing = enabled;
34
+ }
35
+
36
+ /**
37
+ * Main method to accept all revisions in the document
38
+ */
39
+ public async acceptAllRevisions(): Promise<void> {
40
+ // Process document.xml
41
+ await this.processDocumentPart('word/document.xml');
42
+
43
+ // Process headers
44
+ const files = this.zipHandler.getFilePaths();
45
+ for (const file of files) {
46
+ if (/^word\/header\d+\.xml$/.exec(file)) {
47
+ await this.processDocumentPart(file);
48
+ }
49
+ if (/^word\/footer\d+\.xml$/.exec(file)) {
50
+ await this.processDocumentPart(file);
51
+ }
52
+ }
53
+
54
+ // Clean up metadata files
55
+ this.cleanupPeopleXml();
56
+ this.cleanupSettingsXml();
57
+ this.cleanupCorePropsXml();
58
+ }
59
+
60
+ /**
61
+ * Process a document part (document.xml, header, footer) to accept revisions
62
+ */
63
+ private async processDocumentPart(partPath: string): Promise<void> {
64
+ if (this.useDomBasedProcessing) {
65
+ return this.processDocumentPartDOM(partPath);
66
+ }
67
+ return this.processDocumentPartRegex(partPath);
68
+ }
69
+
70
+ /**
71
+ * DOM-based implementation of revision acceptance
72
+ * Uses XMLParser and RevisionWalker for reliable processing
73
+ */
74
+ private processDocumentPartDOM(partPath: string): void {
75
+ const xml = this.zipHandler.getFileAsString(partPath);
76
+ if (!xml) {
77
+ return;
78
+ }
79
+
80
+ // Step 1: Parse XML to object tree
81
+ // IMPORTANT: trimValues: false preserves whitespace from xml:space="preserve" attributes
82
+ const parsed = XMLParser.parseToObject(xml, { trimValues: false });
83
+
84
+ // Step 2: Process revisions using DOM walker
85
+ const processed = RevisionWalker.processTree(parsed, {
86
+ acceptInsertions: true,
87
+ acceptDeletions: true,
88
+ acceptMoves: true,
89
+ acceptPropertyChanges: true,
90
+ });
91
+
92
+ // Step 3: Handle image relationship ID remapping
93
+ this.remapImageRelationshipsInTree(processed);
94
+
95
+ // Step 4: Convert back to XML
96
+ const outputXml =
97
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + this.objectToXml(processed);
98
+
99
+ // Step 5: Update file
100
+ this.zipHandler.updateFile(partPath, outputXml);
101
+ }
102
+
103
+ /**
104
+ * Legacy RegEx-based implementation (kept as fallback)
105
+ */
106
+ private processDocumentPartRegex(partPath: string): void {
107
+ const xml = this.zipHandler.getFileAsString(partPath);
108
+ if (!xml) {
109
+ return;
110
+ }
111
+
112
+ let content = xml;
113
+
114
+ // Step 1: Remove all range markers FIRST (before processing revisions)
115
+ // This prevents orphaned references when revision content is modified
116
+ content = this.removeAllRangeMarkers(content);
117
+
118
+ // Step 2: Remove all property change elements
119
+ // These track formatting changes and must be removed before other processing
120
+ content = this.removeAllPropertyChanges(content);
121
+
122
+ // Step 3: Process deletions - remove entire element INCLUDING content
123
+ // Must be done before insertions to handle nested scenarios
124
+ content = this.acceptDeletions(content);
125
+
126
+ // Step 4: Process move operations
127
+ // Remove moveFrom entirely (source), unwrap moveTo (destination)
128
+ content = this.acceptMoveFrom(content);
129
+ content = this.acceptMoveTo(content);
130
+
131
+ // Step 5: Process insertions - keep content, remove wrapper
132
+ content = this.acceptInsertions(content);
133
+
134
+ // Step 6: Final cleanup - remove any remaining orphaned tags
135
+ content = this.cleanupOrphanedTags(content);
136
+
137
+ // Update the file
138
+ this.zipHandler.updateFile(partPath, content);
139
+ }
140
+
141
+ /**
142
+ * Remove all range marker elements
143
+ * These are boundary markers for tracked changes and moves
144
+ */
145
+ private removeAllRangeMarkers(xml: string): string {
146
+ const patterns = [
147
+ // Move range markers
148
+ /<w:moveFromRangeStart[^>]*(?:\/>|>.*?<\/w:moveFromRangeStart>)/gs,
149
+ /<w:moveFromRangeEnd[^>]*(?:\/>|>.*?<\/w:moveFromRangeEnd>)/gs,
150
+ /<w:moveToRangeStart[^>]*(?:\/>|>.*?<\/w:moveToRangeStart>)/gs,
151
+ /<w:moveToRangeEnd[^>]*(?:\/>|>.*?<\/w:moveToRangeEnd>)/gs,
152
+ // Custom XML range markers
153
+ /<w:customXmlInsRangeStart[^>]*(?:\/>|>.*?<\/w:customXmlInsRangeStart>)/gs,
154
+ /<w:customXmlInsRangeEnd[^>]*(?:\/>|>.*?<\/w:customXmlInsRangeEnd>)/gs,
155
+ /<w:customXmlDelRangeStart[^>]*(?:\/>|>.*?<\/w:customXmlDelRangeStart>)/gs,
156
+ /<w:customXmlDelRangeEnd[^>]*(?:\/>|>.*?<\/w:customXmlDelRangeEnd>)/gs,
157
+ /<w:customXmlMoveFromRangeStart[^>]*(?:\/>|>.*?<\/w:customXmlMoveFromRangeStart>)/gs,
158
+ /<w:customXmlMoveFromRangeEnd[^>]*(?:\/>|>.*?<\/w:customXmlMoveFromRangeEnd>)/gs,
159
+ /<w:customXmlMoveToRangeStart[^>]*(?:\/>|>.*?<\/w:customXmlMoveToRangeStart>)/gs,
160
+ /<w:customXmlMoveToRangeEnd[^>]*(?:\/>|>.*?<\/w:customXmlMoveToRangeEnd>)/gs,
161
+ ];
162
+
163
+ let result = xml;
164
+ for (const pattern of patterns) {
165
+ result = result.replace(pattern, '');
166
+ }
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Remove all property change tracking elements
172
+ * Per ECMA-376, these track previous state of formatting
173
+ */
174
+ private removeAllPropertyChanges(xml: string): string {
175
+ const patterns = [
176
+ // Run property changes
177
+ /<w:rPrChange[^>]*>[\s\S]*?<\/w:rPrChange>/g,
178
+ // Paragraph property changes
179
+ /<w:pPrChange[^>]*>[\s\S]*?<\/w:pPrChange>/g,
180
+ // Table property changes
181
+ /<w:tblPrChange[^>]*>[\s\S]*?<\/w:tblPrChange>/g,
182
+ /<w:tblPrExChange[^>]*>[\s\S]*?<\/w:tblPrExChange>/g,
183
+ // Table cell property changes
184
+ /<w:tcPrChange[^>]*>[\s\S]*?<\/w:tcPrChange>/g,
185
+ // Table row property changes
186
+ /<w:trPrChange[^>]*>[\s\S]*?<\/w:trPrChange>/g,
187
+ // Section property changes
188
+ /<w:sectPrChange[^>]*>[\s\S]*?<\/w:sectPrChange>/g,
189
+ // Table grid changes
190
+ /<w:tblGridChange[^>]*>[\s\S]*?<\/w:tblGridChange>/g,
191
+ // Numbering changes
192
+ /<w:numberingChange[^>]*>[\s\S]*?<\/w:numberingChange>/g,
193
+ ];
194
+
195
+ let result = xml;
196
+ for (const pattern of patterns) {
197
+ result = result.replace(pattern, '');
198
+ }
199
+ return result;
200
+ }
201
+
202
+ /**
203
+ * Accept deletions - remove the entire <w:del> element including its content
204
+ *
205
+ * Per Microsoft SDK: "DeletedRun elements should be removed along with their content"
206
+ */
207
+ private acceptDeletions(xml: string): string {
208
+ let result = xml;
209
+ let previousLength = 0;
210
+
211
+ // Iterate until no more deletions (handles nested cases)
212
+ while (result.length !== previousLength) {
213
+ previousLength = result.length;
214
+
215
+ // Match complete <w:del ...>...</w:del> elements and remove entirely
216
+ result = result.replace(/<w:del\b[^>]*>[\s\S]*?<\/w:del>/g, '');
217
+ }
218
+
219
+ // Also remove self-closing deletion tags
220
+ result = result.replace(/<w:del\b[^>]*\/>/g, '');
221
+
222
+ return result;
223
+ }
224
+
225
+ /**
226
+ * Accept moveFrom - remove the entire element (source of moved content)
227
+ *
228
+ * The content exists at the moveTo destination, so we discard the source
229
+ */
230
+ private acceptMoveFrom(xml: string): string {
231
+ let result = xml;
232
+ let previousLength = 0;
233
+
234
+ while (result.length !== previousLength) {
235
+ previousLength = result.length;
236
+ result = result.replace(/<w:moveFrom\b[^>]*>[\s\S]*?<\/w:moveFrom>/g, '');
237
+ }
238
+
239
+ // Also remove self-closing tags
240
+ result = result.replace(/<w:moveFrom\b[^>]*\/>/g, '');
241
+
242
+ return result;
243
+ }
244
+
245
+ /**
246
+ * Accept moveTo - keep the content, remove the wrapper tags
247
+ *
248
+ * The moveTo location is where the content should remain
249
+ */
250
+ private acceptMoveTo(xml: string): string {
251
+ let result = xml;
252
+
253
+ // Remove closing tags first (prevents issues with regex matching)
254
+ result = result.replace(/<\/w:moveTo>/g, '');
255
+
256
+ // Remove opening tags (keeps content that was inside)
257
+ result = result.replace(/<w:moveTo\b[^>]*>/g, '');
258
+
259
+ return result;
260
+ }
261
+
262
+ /**
263
+ * Accept insertions - keep the content, remove the wrapper tags
264
+ *
265
+ * Per Microsoft SDK: "InsertedRun elements should be unwrapped, keeping their content"
266
+ *
267
+ * IMPORTANT: This method now handles relationship ID remapping for images inside insertions.
268
+ * When Word tracks changes with images, it can reuse relationship IDs (like rId5) because
269
+ * they're in separate tracked change contexts. But when we unwrap them, duplicate IDs
270
+ * cause corruption. This method assigns new unique IDs to images inside insertions.
271
+ */
272
+ private acceptInsertions(xml: string): string {
273
+ let result = xml;
274
+
275
+ // Parse existing relationships
276
+ const relationships = this.parseRelationships();
277
+ const existingIds = new Set(relationships.keys());
278
+
279
+ // Process each w:ins element and remap images one by one
280
+ const insRegex = /<w:ins\b[^>]*>[\s\S]*?<\/w:ins>/g;
281
+
282
+ result = result.replace(insRegex, (insMatch) => {
283
+ // For each image reference inside this insertion, generate a unique new ID
284
+ return insMatch.replace(/r:embed="(rId\d+)"/g, (embedMatch, oldId) => {
285
+ // Generate new unique ID for THIS occurrence
286
+ const newId = this.getNextRelationshipId(existingIds);
287
+ existingIds.add(newId);
288
+
289
+ // Add relationship with same target as original
290
+ const target = relationships.get(oldId);
291
+ if (target) {
292
+ this.addRelationship(
293
+ newId,
294
+ target,
295
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'
296
+ );
297
+ }
298
+
299
+ return `r:embed="${newId}"`;
300
+ });
301
+ });
302
+
303
+ // Now unwrap the w:ins tags (content has unique remapped IDs)
304
+ result = result.replace(/<\/w:ins>/g, '');
305
+ result = result.replace(/<w:ins\b[^>]*>/g, '');
306
+
307
+ return result;
308
+ }
309
+
310
+ /**
311
+ * Final cleanup to remove any orphaned or malformed revision-related tags
312
+ */
313
+ private cleanupOrphanedTags(xml: string): string {
314
+ let result = xml;
315
+
316
+ // Remove any remaining self-closing revision tags
317
+ result = result.replace(/<w:ins\b[^>]*\/>/g, '');
318
+ result = result.replace(/<w:del\b[^>]*\/>/g, '');
319
+ result = result.replace(/<w:moveFrom\b[^>]*\/>/g, '');
320
+ result = result.replace(/<w:moveTo\b[^>]*\/>/g, '');
321
+
322
+ // Remove empty w:r elements that might be left after removing deletions
323
+ result = result.replace(/<w:r\b[^>]*>\s*<\/w:r>/g, '');
324
+
325
+ // Remove empty w:p elements (but preserve those with properties)
326
+ // We keep <w:p><w:pPr>...</w:pPr></w:p> as those are intentional empty paragraphs with styling
327
+ result = result.replace(/<w:p>\s*<\/w:p>/g, '');
328
+
329
+ return result;
330
+ }
331
+
332
+ /**
333
+ * Parse relationship IDs from word/_rels/document.xml.rels
334
+ * Returns a map of relationship ID to target path
335
+ */
336
+ private parseRelationships(): Map<string, string> {
337
+ const relsXml = this.zipHandler.getFileAsString('word/_rels/document.xml.rels');
338
+ if (!relsXml) return new Map();
339
+
340
+ const map = new Map<string, string>();
341
+ const relationshipRegex = /<Relationship[^>]*Id="([^"]+)"[^>]*Target="([^"]+)"[^>]*\/>/g;
342
+ let match;
343
+
344
+ while ((match = relationshipRegex.exec(relsXml)) !== null) {
345
+ if (match[1] && match[2]) {
346
+ map.set(match[1], match[2]); // rId -> target path
347
+ }
348
+ }
349
+
350
+ return map;
351
+ }
352
+
353
+ /**
354
+ * Get the next available relationship ID
355
+ * Finds the highest numeric ID and increments it
356
+ */
357
+ private getNextRelationshipId(existingIds: Set<string>): string {
358
+ let maxId = 0;
359
+ for (const id of existingIds) {
360
+ const num = parseInt(id.replace('rId', ''));
361
+ if (!isNaN(num) && num > maxId) {
362
+ maxId = num;
363
+ }
364
+ }
365
+ return `rId${maxId + 1}`;
366
+ }
367
+
368
+ /**
369
+ * Add a new relationship to word/_rels/document.xml.rels
370
+ */
371
+ private addRelationship(rId: string, target: string, type: string): void {
372
+ const relsXml = this.zipHandler.getFileAsString('word/_rels/document.xml.rels');
373
+ if (!relsXml) return;
374
+
375
+ // Insert new relationship before closing tag
376
+ const newRel = `<Relationship Id="${rId}" Type="${type}" Target="${target}"/>`;
377
+ const updated = relsXml.replace('</Relationships>', `${newRel}\n</Relationships>`);
378
+
379
+ this.zipHandler.updateFile('word/_rels/document.xml.rels', updated);
380
+ }
381
+
382
+ /**
383
+ * Clean up all revision metadata files (people.xml, settings.xml, core.xml).
384
+ *
385
+ * This removes:
386
+ * - All revision authors from people.xml
387
+ * - Track changes settings from settings.xml
388
+ * - Resets revision count in core.xml
389
+ *
390
+ * Called as part of acceptAllRevisions() but can also be called separately
391
+ * when using in-memory revision acceptance.
392
+ */
393
+ public cleanupMetadata(): void {
394
+ this.cleanupPeopleXml();
395
+ this.cleanupSettingsXml();
396
+ this.cleanupCorePropsXml();
397
+ }
398
+
399
+ /**
400
+ * Clean up word/people.xml - remove all revision authors
401
+ *
402
+ * Handles both w: and w15: namespace variants
403
+ */
404
+ private cleanupPeopleXml(): void {
405
+ const peopleXml = this.zipHandler.getFileAsString('word/people.xml');
406
+ if (!peopleXml) {
407
+ return;
408
+ }
409
+
410
+ let content = peopleXml;
411
+
412
+ // Remove all person elements in any namespace variant
413
+ content = content.replace(/<w:person\b[^>]*>[\s\S]*?<\/w:person>/g, '');
414
+ content = content.replace(/<w15:person\b[^>]*>[\s\S]*?<\/w15:person>/g, '');
415
+
416
+ // Handle any namespace-prefixed variants (w1:, w2:, etc.)
417
+ content = content.replace(/<w\d+:person\b[^>]*>[\s\S]*?<\/w\d+:person>/g, '');
418
+
419
+ // Also remove self-closing person elements
420
+ content = content.replace(/<w:person\b[^>]*\/>/g, '');
421
+ content = content.replace(/<w15:person\b[^>]*\/>/g, '');
422
+ content = content.replace(/<w\d+:person\b[^>]*\/>/g, '');
423
+
424
+ this.zipHandler.updateFile('word/people.xml', content);
425
+ }
426
+
427
+ /**
428
+ * Clean up word/settings.xml - disable track changes
429
+ */
430
+ private cleanupSettingsXml(): void {
431
+ const settingsXml = this.zipHandler.getFileAsString('word/settings.xml');
432
+ if (!settingsXml) {
433
+ return;
434
+ }
435
+
436
+ let content = settingsXml;
437
+
438
+ // Remove trackRevisions element (enables tracking)
439
+ content = content.replace(/<w:trackRevisions\b[^>]*\/>/g, '');
440
+ content = content.replace(/<w:trackRevisions\b[^>]*>[\s\S]*?<\/w:trackRevisions>/g, '');
441
+
442
+ // Remove revisionView element (controls which revisions are visible)
443
+ content = content.replace(/<w:revisionView\b[^>]*\/>/g, '');
444
+ content = content.replace(/<w:revisionView\b[^>]*>[\s\S]*?<\/w:revisionView>/g, '');
445
+
446
+ // Remove doNotTrackMoves (prevents move tracking)
447
+ content = content.replace(/<w:doNotTrackMoves\b[^>]*\/>/g, '');
448
+ content = content.replace(/<w:doNotTrackMoves\b[^>]*>[\s\S]*?<\/w:doNotTrackMoves>/g, '');
449
+
450
+ // Remove doNotTrackFormatting
451
+ content = content.replace(/<w:doNotTrackFormatting\b[^>]*\/>/g, '');
452
+ content = content.replace(
453
+ /<w:doNotTrackFormatting\b[^>]*>[\s\S]*?<\/w:doNotTrackFormatting>/g,
454
+ ''
455
+ );
456
+
457
+ this.zipHandler.updateFile('word/settings.xml', content);
458
+ }
459
+
460
+ /**
461
+ * Clean up docProps/core.xml - reset revision count
462
+ */
463
+ private cleanupCorePropsXml(): void {
464
+ const coreXml = this.zipHandler.getFileAsString('docProps/core.xml');
465
+ if (!coreXml) {
466
+ return;
467
+ }
468
+
469
+ // Reset revision count to 1
470
+ const content = coreXml.replace(
471
+ /<cp:revision>\d+<\/cp:revision>/g,
472
+ '<cp:revision>1</cp:revision>'
473
+ );
474
+
475
+ this.zipHandler.updateFile('docProps/core.xml', content);
476
+ }
477
+
478
+ // =========================================================================
479
+ // DOM-based processing helper methods
480
+ // =========================================================================
481
+
482
+ /**
483
+ * Convert parsed XML object back to XML string
484
+ * Preserves element order using _orderedChildren metadata
485
+ *
486
+ * Based on DocumentParser.objectToXml() implementation
487
+ */
488
+ private objectToXml(obj: any): string {
489
+ const buildXml = (o: any, name?: string): string => {
490
+ // Handle simple string/number with a tag name: <tagName>value</tagName>
491
+ if (name && (typeof o === 'string' || typeof o === 'number')) {
492
+ return `<${name}>${this.escapeXml(String(o))}</${name}>`;
493
+ }
494
+ if (typeof o === 'string') return this.escapeXml(o);
495
+ if (typeof o !== 'object' || o === null) return String(o ?? '');
496
+
497
+ const keys = Object.keys(o);
498
+
499
+ // If a name is provided, we're building a specific element
500
+ // Don't return empty string for empty objects with a name - they become self-closing tags
501
+ if (keys.length === 0 && !name) return '';
502
+
503
+ const tagName = name || keys[0]!;
504
+ const element = name ? o : o[tagName];
505
+
506
+ let xml = `<${tagName}`;
507
+
508
+ // Add attributes (keys starting with @_)
509
+ if (element && typeof element === 'object') {
510
+ for (const key of Object.keys(element)) {
511
+ if (key.startsWith('@_')) {
512
+ const attrName = key.substring(2);
513
+ xml += ` ${attrName}="${this.escapeXml(String(element[key]))}"`;
514
+ }
515
+ }
516
+ }
517
+
518
+ // Check for children (non-attribute, non-text, non-metadata keys)
519
+ const hasChildren =
520
+ element &&
521
+ typeof element === 'object' &&
522
+ Object.keys(element).some(
523
+ (k) => !k.startsWith('@_') && k !== '#text' && k !== '_orderedChildren'
524
+ );
525
+
526
+ // Per ECMA-376, certain elements MUST NOT be self-closing (e.g., <w:p/> is invalid).
527
+ // This mirrors the CANNOT_SELF_CLOSE list in XMLBuilder.ts.
528
+ const CANNOT_SELF_CLOSE = [
529
+ 'w:t',
530
+ 'w:r',
531
+ 'w:p',
532
+ 'w:tbl',
533
+ 'w:tr',
534
+ 'w:tc',
535
+ 'w:body',
536
+ 'w:document',
537
+ 'w:hyperlink',
538
+ 'w:sdt',
539
+ 'w:sdtContent',
540
+ 'w:sdtPr',
541
+ 'w:pPr',
542
+ 'w:rPr',
543
+ 'w:sectPr',
544
+ 'w:del',
545
+ 'w:ins',
546
+ 'w:moveFrom',
547
+ 'w:moveTo',
548
+ ];
549
+
550
+ if (!hasChildren && !element?.['#text']) {
551
+ if (CANNOT_SELF_CLOSE.includes(tagName)) {
552
+ xml += `></${tagName}>`;
553
+ } else {
554
+ xml += '/>';
555
+ }
556
+ } else {
557
+ xml += '>';
558
+
559
+ // Add text content
560
+ if (element?.['#text']) {
561
+ xml += this.escapeXml(String(element['#text']));
562
+ }
563
+
564
+ // Add child elements using _orderedChildren if available
565
+ if (element && typeof element === 'object') {
566
+ const orderedChildren = element._orderedChildren as
567
+ | { type: string; index: number }[]
568
+ | undefined;
569
+
570
+ if (orderedChildren && orderedChildren.length > 0) {
571
+ // Use _orderedChildren to preserve element order
572
+ for (const childInfo of orderedChildren) {
573
+ const childType = childInfo.type;
574
+ const childIndex = childInfo.index;
575
+
576
+ if (element[childType] !== undefined) {
577
+ const children = element[childType];
578
+
579
+ if (Array.isArray(children)) {
580
+ if (childIndex < children.length) {
581
+ const childXml = buildXml(children[childIndex], childType);
582
+ xml += childXml;
583
+ }
584
+ } else {
585
+ // Single child element
586
+ if (childIndex === 0) {
587
+ const childXml = buildXml(children, childType);
588
+ xml += childXml;
589
+ }
590
+ }
591
+ }
592
+ }
593
+ } else {
594
+ // Fallback: iterate through keys if no _orderedChildren
595
+ for (const key of Object.keys(element)) {
596
+ if (!key.startsWith('@_') && key !== '#text' && key !== '_orderedChildren') {
597
+ const children = element[key];
598
+ if (Array.isArray(children)) {
599
+ for (const child of children) {
600
+ xml += buildXml(child, key);
601
+ }
602
+ } else {
603
+ xml += buildXml(children, key);
604
+ }
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ xml += `</${tagName}>`;
611
+ }
612
+
613
+ return xml;
614
+ };
615
+
616
+ return buildXml(obj);
617
+ }
618
+
619
+ /**
620
+ * Escape special XML characters
621
+ */
622
+ private escapeXml(str: string): string {
623
+ return str
624
+ .replace(/&/g, '&amp;')
625
+ .replace(/</g, '&lt;')
626
+ .replace(/>/g, '&gt;')
627
+ .replace(/"/g, '&quot;')
628
+ .replace(/'/g, '&apos;');
629
+ }
630
+
631
+ /**
632
+ * Remap image relationship IDs in the parsed tree to prevent duplicates
633
+ * Walks the tree looking for r:embed attributes and assigns unique IDs
634
+ */
635
+ private remapImageRelationshipsInTree(obj: any): void {
636
+ const relationships = this.parseRelationships();
637
+ const existingIds = new Set(relationships.keys());
638
+ const remappedIds = new Map<string, string>();
639
+
640
+ // Walk the tree and find all r:embed attributes
641
+ this.walkTreeForEmbeds(obj, (embedId: string, parent: any, key: string) => {
642
+ // Check if this ID has already been remapped
643
+ if (remappedIds.has(embedId)) {
644
+ parent[key] = remappedIds.get(embedId);
645
+ return;
646
+ }
647
+
648
+ // Check if this ID needs remapping (duplicate)
649
+ // For DOM-based processing, we check if we've seen this ID before in this pass
650
+ const target = relationships.get(embedId);
651
+ if (target) {
652
+ // Generate new unique ID
653
+ const newId = this.getNextRelationshipId(existingIds);
654
+ existingIds.add(newId);
655
+ remappedIds.set(embedId, newId);
656
+
657
+ // Add relationship with same target
658
+ this.addRelationship(
659
+ newId,
660
+ target,
661
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image'
662
+ );
663
+
664
+ // Update the attribute
665
+ parent[key] = newId;
666
+ }
667
+ });
668
+ }
669
+
670
+ /**
671
+ * Walk the tree looking for r:embed attributes
672
+ */
673
+ private walkTreeForEmbeds(
674
+ obj: any,
675
+ callback: (embedId: string, parent: any, key: string) => void
676
+ ): void {
677
+ if (!obj || typeof obj !== 'object') {
678
+ return;
679
+ }
680
+
681
+ for (const key of Object.keys(obj)) {
682
+ // Check for r:embed attribute
683
+ if (key === '@_r:embed') {
684
+ callback(obj[key], obj, key);
685
+ } else if (!key.startsWith('@_') && key !== '#text' && key !== '_orderedChildren') {
686
+ const value = obj[key];
687
+ if (Array.isArray(value)) {
688
+ for (const item of value) {
689
+ this.walkTreeForEmbeds(item, callback);
690
+ }
691
+ } else if (typeof value === 'object') {
692
+ this.walkTreeForEmbeds(value, callback);
693
+ }
694
+ }
695
+ }
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Convenience function to accept all revisions in a document
701
+ */
702
+ export async function acceptAllRevisions(zipHandler: ZipHandler): Promise<void> {
703
+ const acceptor = new RevisionAcceptor(zipHandler);
704
+ await acceptor.acceptAllRevisions();
705
+ }
706
+
707
+ /**
708
+ * Convenience function to clean up revision metadata files.
709
+ *
710
+ * This removes:
711
+ * - All revision authors from people.xml
712
+ * - Track changes settings from settings.xml
713
+ * - Resets revision count in core.xml
714
+ *
715
+ * Use this after in-memory revision acceptance to ensure metadata is also cleaned.
716
+ * The raw XML acceptAllRevisions() function calls this automatically.
717
+ *
718
+ * @param zipHandler - The ZipHandler containing the DOCX package
719
+ */
720
+ export function cleanupRevisionMetadata(zipHandler: ZipHandler): void {
721
+ const acceptor = new RevisionAcceptor(zipHandler);
722
+ acceptor.cleanupMetadata();
723
+ }