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,1104 +1,1133 @@
1
- /**
2
- * DocumentGenerator - Handles XML generation for DOCX files
3
- * Converts structured data to OpenXML format
4
- */
5
-
6
- import { CommentManager } from "../elements/CommentManager";
7
- import { EndnoteManager } from "../elements/EndnoteManager";
8
- import { FontManager } from "../elements/FontManager";
9
- import { FootnoteManager } from "../elements/FootnoteManager";
10
- import { HeaderFooterManager } from "../elements/HeaderFooterManager";
11
- import { Hyperlink } from "../elements/Hyperlink";
12
- import { ImageManager } from "../elements/ImageManager";
13
- import { Paragraph } from "../elements/Paragraph";
14
- import { Revision } from "../elements/Revision";
15
- import { isHyperlinkContent } from "../elements/RevisionContent";
16
- import { Section } from "../elements/Section";
17
- import { StructuredDocumentTag } from "../elements/StructuredDocumentTag";
18
- import { Table } from "../elements/Table";
19
- import { TableOfContentsElement } from "../elements/TableOfContentsElement";
20
- import { AlternateContent } from "../elements/AlternateContent";
21
- import { MathParagraph } from "../elements/MathElement";
22
- import { CustomXmlBlock } from "../elements/CustomXml";
23
- import { PreservedElement } from "../elements/PreservedElement";
24
- import { formatDateForXml } from "../utils/dateFormatting";
25
- import { getGlobalLogger, createScopedLogger, ILogger } from "../utils/logger";
26
- import { XMLBuilder, XMLElement } from "../xml/XMLBuilder";
27
- import { DocumentProperties } from "./Document";
28
- import { BodyElement } from "./DocumentContent";
29
- import { RelationshipManager } from "./RelationshipManager";
30
- import { TrackChangesSettings } from "../types/settings-types";
31
-
32
- // Create scoped logger for DocumentGenerator operations
33
- function getLogger(): ILogger {
34
- return createScopedLogger(getGlobalLogger(), 'DocumentGenerator');
35
- }
36
-
37
- /**
38
- * Interface for ZipHandler methods used in content type generation
39
- * This provides type safety for the ZipHandler parameter without creating
40
- * a circular dependency with the zip module.
41
- */
42
- export interface IZipHandlerReader {
43
- /** Get list of file paths in the archive */
44
- getFilePaths?(): string[];
45
- /** Check if a file exists in the archive */
46
- hasFile?(path: string): boolean;
47
- }
48
-
49
- /**
50
- * Normalizes toXML() output to always return an array.
51
- * Some elements (e.g., TableOfContentsElement) return XMLElement[], while others return XMLElement.
52
- * This helper provides consistent array handling.
53
- *
54
- * @param xml - XMLElement or XMLElement[] from toXML()
55
- * @returns XMLElement array
56
- */
57
- function normalizeXmlOutput(xml: XMLElement | XMLElement[]): XMLElement[] {
58
- return Array.isArray(xml) ? xml : [xml];
59
- }
60
-
61
- /**
62
- * DocumentGenerator handles all XML generation logic
63
- */
64
- export class DocumentGenerator {
65
- /**
66
- * Generates [Content_Types].xml
67
- */
68
- generateContentTypes(): string {
69
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
70
- <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
71
- <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
72
- <Default Extension="xml" ContentType="application/xml"/>
73
- <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
74
- <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
75
- <Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
76
- <Override PartName="/word/fontTable.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"/>
77
- <Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/>
78
- <Override PartName="/word/webSettings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"/>
79
- <Override PartName="/word/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
80
- <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
81
- <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
82
- </Types>`;
83
- }
84
-
85
- /**
86
- * Generates _rels/.rels
87
- */
88
- generateRels(): string {
89
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
90
- <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
91
- <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
92
- <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
93
- <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
94
- </Relationships>`;
95
- }
96
-
97
- /**
98
- * Generates word/document.xml with current body elements
99
- */
100
- generateDocumentXml(
101
- bodyElements: BodyElement[],
102
- section: Section,
103
- namespaces: Record<string, string>,
104
- documentBackground?: { color?: string; themeColor?: string; themeTint?: string; themeShade?: string }
105
- ): string {
106
- const logger = getLogger();
107
- logger.info('Generating document.xml', { elementCount: bodyElements.length });
108
-
109
- const bodyXmls: XMLElement[] = [];
110
-
111
- // Generate XML for each body element
112
- // Uses normalizeXmlOutput() to handle both single XMLElement and XMLElement[] returns
113
- for (const element of bodyElements) {
114
- const xmlElements = normalizeXmlOutput(element.toXML());
115
- bodyXmls.push(...xmlElements);
116
- }
117
-
118
- // Add section properties at the end
119
- bodyXmls.push(section.toXML());
120
-
121
- // Build pre-body content (w:background) per ECMA-376 Part 1 §17.2.1
122
- let preBodyContent: XMLElement[] | undefined;
123
- if (documentBackground) {
124
- const bgAttrs: Record<string, string> = {};
125
- if (documentBackground.color) bgAttrs["w:color"] = documentBackground.color;
126
- if (documentBackground.themeColor) bgAttrs["w:themeColor"] = documentBackground.themeColor;
127
- if (documentBackground.themeTint) bgAttrs["w:themeTint"] = documentBackground.themeTint;
128
- if (documentBackground.themeShade) bgAttrs["w:themeShade"] = documentBackground.themeShade;
129
- preBodyContent = [XMLBuilder.wSelf("background", bgAttrs)];
130
- }
131
-
132
- const result = XMLBuilder.createDocument(bodyXmls, namespaces, preBodyContent);
133
- logger.info('Document.xml generated', { xmlSize: result.length });
134
- return result;
135
- }
136
-
137
- /**
138
- * Generates docProps/core.xml with extended properties
139
- */
140
- generateCoreProps(properties: DocumentProperties): string {
141
- const now = new Date();
142
- const created = properties.created || now;
143
- const modified = properties.modified || now;
144
-
145
- const formatDate = (date: Date): string => {
146
- return formatDateForXml(date);
147
- };
148
-
149
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
150
- <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
151
- xmlns:dc="http://purl.org/dc/elements/1.1/"
152
- xmlns:dcterms="http://purl.org/dc/terms/"
153
- xmlns:dcmitype="http://purl.org/dc/dcmitype/"
154
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
155
- <dc:title>${XMLBuilder.sanitizeXmlContent(properties.title || "")}</dc:title>
156
- <dc:subject>${XMLBuilder.sanitizeXmlContent(
157
- properties.subject || ""
158
- )}</dc:subject>
159
- <dc:creator>${XMLBuilder.sanitizeXmlContent(
160
- properties.creator || "DocXML"
161
- )}</dc:creator>
162
- <cp:keywords>${XMLBuilder.sanitizeXmlContent(
163
- properties.keywords || ""
164
- )}</cp:keywords>
165
- <dc:description>${XMLBuilder.sanitizeXmlContent(
166
- properties.description || ""
167
- )}</dc:description>
168
- <cp:lastModifiedBy>${XMLBuilder.sanitizeXmlContent(
169
- properties.lastModifiedBy || properties.creator || "DocXML"
170
- )}</cp:lastModifiedBy>
171
- <cp:revision>${properties.revision || 1}</cp:revision>${
172
- properties.category
173
- ? `\n <cp:category>${XMLBuilder.sanitizeXmlContent(
174
- properties.category
175
- )}</cp:category>`
176
- : ""
177
- }${
178
- properties.contentStatus
179
- ? `\n <cp:contentStatus>${XMLBuilder.sanitizeXmlContent(
180
- properties.contentStatus
181
- )}</cp:contentStatus>`
182
- : ""
183
- }${
184
- properties.language
185
- ? `\n <dc:language>${XMLBuilder.sanitizeXmlContent(
186
- properties.language
187
- )}</dc:language>`
188
- : ""
189
- }
190
- <dcterms:created xsi:type="dcterms:W3CDTF">${formatDate(
191
- created
192
- )}</dcterms:created>
193
- <dcterms:modified xsi:type="dcterms:W3CDTF">${formatDate(
194
- modified
195
- )}</dcterms:modified>
196
- </cp:coreProperties>`;
197
- }
198
-
199
- /**
200
- * Generates docProps/app.xml with extended properties
201
- */
202
- generateAppProps(properties: DocumentProperties = {}): string {
203
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
204
- <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
205
- xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
206
- <Application>${XMLBuilder.sanitizeXmlContent(
207
- properties.application || "docxmlater"
208
- )}</Application>
209
- <DocSecurity>0</DocSecurity>
210
- <ScaleCrop>false</ScaleCrop>
211
- <Company>${XMLBuilder.sanitizeXmlContent(properties.company || "")}</Company>${
212
- properties.manager
213
- ? `\n <Manager>${XMLBuilder.sanitizeXmlContent(
214
- properties.manager
215
- )}</Manager>`
216
- : ""
217
- }
218
- <LinksUpToDate>false</LinksUpToDate>
219
- <SharedDoc>false</SharedDoc>
220
- <HyperlinksChanged>false</HyperlinksChanged>
221
- <AppVersion>${XMLBuilder.sanitizeXmlContent(
222
- properties.appVersion || properties.version || "1.0.0"
223
- )}</AppVersion>
224
- </Properties>`;
225
- }
226
-
227
- /**
228
- * Generates docProps/custom.xml with custom properties
229
- */
230
- generateCustomProps(
231
- customProps: Record<string, string | number | boolean | Date>
232
- ): string {
233
- if (!customProps || Object.keys(customProps).length === 0) {
234
- return "";
235
- }
236
-
237
- const formatCustomValue = (
238
- key: string,
239
- value: string | number | boolean | Date,
240
- pid: number
241
- ): string => {
242
- if (typeof value === "string") {
243
- return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${XMLBuilder.sanitizeXmlContent(
244
- key
245
- )}">
246
- <vt:lpwstr>${XMLBuilder.sanitizeXmlContent(value)}</vt:lpwstr>
247
- </property>`;
248
- } else if (typeof value === "number") {
249
- return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${XMLBuilder.sanitizeXmlContent(
250
- key
251
- )}">
252
- <vt:r8>${value}</vt:r8>
253
- </property>`;
254
- } else if (typeof value === "boolean") {
255
- return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${XMLBuilder.sanitizeXmlContent(
256
- key
257
- )}">
258
- <vt:bool>${value ? "true" : "false"}</vt:bool>
259
- </property>`;
260
- } else if (value instanceof Date) {
261
- return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${XMLBuilder.sanitizeXmlContent(
262
- key
263
- )}">
264
- <vt:filetime>${value.toISOString()}</vt:filetime>
265
- </property>`;
266
- }
267
- return "";
268
- };
269
-
270
- const properties = Object.entries(customProps)
271
- .map(([key, value], index) => formatCustomValue(key, value, index + 2))
272
- .filter((prop) => prop !== "")
273
- .join("\n");
274
-
275
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
276
- <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
277
- xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
278
- ${properties}
279
- </Properties>`;
280
- }
281
-
282
- /**
283
- * Generates [Content_Types].xml with image extensions, headers/footers, comments, and fonts
284
- * Preserves entries for files that exist in the loaded document (customXML, etc.)
285
- * Merges framework-generated entries with original entries for round-trip fidelity
286
- */
287
- generateContentTypesWithImagesHeadersFootersAndComments(
288
- imageManager: ImageManager,
289
- headerFooterManager: HeaderFooterManager,
290
- commentManager: CommentManager,
291
- zipHandler: IZipHandlerReader,
292
- fontManager?: FontManager,
293
- hasCustomProperties = false,
294
- originalContentTypes?: { defaults: Set<string>; overrides: Set<string> },
295
- footnoteManager?: FootnoteManager,
296
- endnoteManager?: EndnoteManager
297
- ): string {
298
- const images = imageManager.getAllImages();
299
- const headers = headerFooterManager.getAllHeaders();
300
- const footers = headerFooterManager.getAllFooters();
301
- const hasComments = commentManager.getCount() > 0;
302
-
303
- // Build sets for framework-generated entries
304
- const generatedDefaults = new Set<string>();
305
- const generatedOverrides = new Set<string>();
306
-
307
- // Default types - always needed
308
- generatedDefaults.add('rels|application/vnd.openxmlformats-package.relationships+xml');
309
- generatedDefaults.add('xml|application/xml');
310
-
311
- // Image extensions from ImageManager
312
- for (const entry of images) {
313
- const ext = entry.image.getExtension();
314
- const mimeType = ImageManager.getMimeType(ext);
315
- generatedDefaults.add(`${ext}|${mimeType}`);
316
- }
317
-
318
- // Also detect image files in the archive not tracked by ImageManager
319
- // (e.g., numPicBullet images referenced by numbering.xml.rels)
320
- const mediaExtensions = new Map<string, string>([
321
- ['png', 'image/png'], ['jpeg', 'image/jpeg'], ['jpg', 'image/jpeg'],
322
- ['gif', 'image/gif'], ['bmp', 'image/bmp'], ['tiff', 'image/tiff'],
323
- ['emf', 'image/x-emf'], ['wmf', 'image/x-wmf'],
324
- ]);
325
- for (const file of (zipHandler.getFilePaths?.() || [])) {
326
- if (file.startsWith('word/media/')) {
327
- const ext = file.split('.').pop()?.toLowerCase();
328
- if (ext && mediaExtensions.has(ext)) {
329
- generatedDefaults.add(`${ext}|${mediaExtensions.get(ext)}`);
330
- }
331
- }
332
- }
333
-
334
- // Font extensions (if FontManager provided)
335
- if (fontManager && fontManager.getCount() > 0) {
336
- const fontEntries = fontManager.generateContentTypeEntries();
337
- for (const entry of fontEntries) {
338
- // Parse each entry and add to set (entries are XML strings)
339
- const extMatch = /Extension="([^"]+)"/.exec(entry);
340
- const typeMatch = /ContentType="([^"]+)"/.exec(entry);
341
- if (extMatch && typeMatch) {
342
- generatedDefaults.add(`${extMatch[1]}|${typeMatch[1]}`);
343
- }
344
- }
345
- }
346
-
347
- // Check for embedded .ttf fonts from original document
348
- // Also create a Set for efficient file existence checks
349
- const files = zipHandler.getFilePaths?.() || [];
350
- const filesInArchive = new Set(files);
351
- const hasTtfFonts = files.some((f: string) => f.endsWith(".ttf"));
352
- if (hasTtfFonts) {
353
- generatedDefaults.add('ttf|application/x-font-ttf');
354
- }
355
-
356
- // Override types - only add if file exists in archive
357
- generatedOverrides.add('/word/document.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml');
358
- if (filesInArchive.has('word/styles.xml')) {
359
- generatedOverrides.add('/word/styles.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml');
360
- }
361
- if (filesInArchive.has('word/numbering.xml')) {
362
- generatedOverrides.add('/word/numbering.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml');
363
- }
364
- if (filesInArchive.has('word/fontTable.xml')) {
365
- generatedOverrides.add('/word/fontTable.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml');
366
- }
367
- if (filesInArchive.has('word/settings.xml')) {
368
- generatedOverrides.add('/word/settings.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml');
369
- }
370
- if (filesInArchive.has('word/webSettings.xml')) {
371
- generatedOverrides.add('/word/webSettings.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml');
372
- }
373
- if (filesInArchive.has('word/theme/theme1.xml')) {
374
- generatedOverrides.add('/word/theme/theme1.xml|application/vnd.openxmlformats-officedocument.theme+xml');
375
- }
376
-
377
- // Headers - only add if file actually exists in archive
378
- // This prevents corruption when HeaderFooterManager has stale entries for removed headers
379
- for (const entry of headers) {
380
- const filePath = `word/${entry.filename}`;
381
- if (filesInArchive.has(filePath)) {
382
- generatedOverrides.add(`/word/${entry.filename}|application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml`);
383
- }
384
- }
385
-
386
- // Footers - only add if file actually exists in archive
387
- // This prevents corruption when HeaderFooterManager has stale entries for removed footers
388
- for (const entry of footers) {
389
- const filePath = `word/${entry.filename}`;
390
- if (filesInArchive.has(filePath)) {
391
- generatedOverrides.add(`/word/${entry.filename}|application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml`);
392
- }
393
- }
394
-
395
- // Comments
396
- if (hasComments) {
397
- generatedOverrides.add('/word/comments.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml');
398
- }
399
-
400
- // Footnotes
401
- const hasFootnotes = (footnoteManager && footnoteManager.getCount() > 0) || filesInArchive.has('word/footnotes.xml');
402
- if (hasFootnotes) {
403
- generatedOverrides.add('/word/footnotes.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml');
404
- }
405
-
406
- // Endnotes
407
- const hasEndnotes = (endnoteManager && endnoteManager.getCount() > 0) || filesInArchive.has('word/endnotes.xml');
408
- if (hasEndnotes) {
409
- generatedOverrides.add('/word/endnotes.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml');
410
- }
411
-
412
- // People (track changes authors)
413
- if (filesInArchive.has('word/people.xml')) {
414
- generatedOverrides.add('/word/people.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml');
415
- }
416
-
417
- // Comment companion files (passthrough)
418
- if (filesInArchive.has('word/commentsExtended.xml')) {
419
- generatedOverrides.add('/word/commentsExtended.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml');
420
- }
421
- if (filesInArchive.has('word/commentsIds.xml')) {
422
- generatedOverrides.add('/word/commentsIds.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml');
423
- }
424
- if (filesInArchive.has('word/commentsExtensible.xml')) {
425
- generatedOverrides.add('/word/commentsExtensible.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml');
426
- }
427
-
428
- // Core properties (only add if file exists)
429
- if (zipHandler.hasFile?.("docProps/core.xml") || filesInArchive.has('docProps/core.xml')) {
430
- generatedOverrides.add('/docProps/core.xml|application/vnd.openxmlformats-package.core-properties+xml');
431
- }
432
-
433
- // App.xml if it exists
434
- if (zipHandler.hasFile?.("docProps/app.xml")) {
435
- generatedOverrides.add('/docProps/app.xml|application/vnd.openxmlformats-officedocument.extended-properties+xml');
436
- }
437
-
438
- // Custom properties if exists or will be created
439
- if (zipHandler.hasFile?.("docProps/custom.xml") || hasCustomProperties) {
440
- generatedOverrides.add('/docProps/custom.xml|application/vnd.openxmlformats-officedocument.custom-properties+xml');
441
- }
442
-
443
- // CustomXML entries if they exist
444
- if (zipHandler.hasFile?.("customXML/item1.xml")) {
445
- generatedOverrides.add('/customXML/item1.xml|application/xml');
446
- }
447
- if (zipHandler.hasFile?.("customXML/itemProps1.xml")) {
448
- generatedOverrides.add('/customXML/itemProps1.xml|application/vnd.openxmlformats-officedocument.customXmlProperties+xml');
449
- }
450
-
451
- // Merge with original entries, but ONLY keep overrides for files that actually exist
452
- // This prevents corruption when headers/footers are removed but their Content_Types entries
453
- // from the original document would otherwise be preserved
454
- const allDefaults = new Set([
455
- ...generatedDefaults,
456
- ...(originalContentTypes?.defaults || [])
457
- ]);
458
-
459
- // filesInArchive was created earlier (line 318) for header/footer validation
460
- // Reuse it here to filter original overrides as well
461
-
462
- // Filter original overrides to only include files that exist in the archive
463
- const filteredOriginalOverrides: string[] = [];
464
- for (const entry of (originalContentTypes?.overrides || [])) {
465
- const parts = entry.split('|');
466
- const partName = parts[0] || '';
467
- // Convert /word/footer1.xml to word/footer1.xml for comparison
468
- const normalizedPath = partName.startsWith('/') ? partName.slice(1) : partName;
469
- if (filesInArchive.has(normalizedPath)) {
470
- filteredOriginalOverrides.push(entry);
471
- }
472
- }
473
-
474
- const allOverrides = new Set([
475
- ...generatedOverrides,
476
- ...filteredOriginalOverrides
477
- ]);
478
-
479
- // Build XML from merged sets
480
- let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
481
- xml += '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">\n';
482
-
483
- // Add all default entries (escape attribute values for security)
484
- for (const entry of allDefaults) {
485
- const parts = entry.split('|');
486
- const ext = parts[0] || '';
487
- const contentType = parts[1] || '';
488
- const escapedExt = XMLBuilder.escapeXmlAttribute(ext);
489
- const escapedContentType = XMLBuilder.escapeXmlAttribute(contentType);
490
- xml += ` <Default Extension="${escapedExt}" ContentType="${escapedContentType}"/>\n`;
491
- }
492
-
493
- // Add all override entries (escape attribute values for security)
494
- for (const entry of allOverrides) {
495
- const parts = entry.split('|');
496
- const partName = parts[0] || '';
497
- const contentType = parts[1] || '';
498
- const escapedPartName = XMLBuilder.escapeXmlAttribute(partName);
499
- const escapedContentType = XMLBuilder.escapeXmlAttribute(contentType);
500
- xml += ` <Override PartName="${escapedPartName}" ContentType="${escapedContentType}"/>\n`;
501
- }
502
-
503
- xml += '</Types>';
504
-
505
- return xml;
506
- }
507
-
508
- /**
509
- * Clears ORPHANED hyperlink relationships from the RelationshipManager
510
- * Only removes relationships that don't have corresponding hyperlinks in the document
511
- *
512
- * This prevents corruption when paragraphs with hyperlinks are removed but
513
- * their relationships remain, causing Word's "unreadable content" error.
514
- * Preserves relationships for existing hyperlinks to maintain round-trip integrity.
515
- */
516
- private clearOrphanedHyperlinkRelationships(
517
- bodyElements: BodyElement[],
518
- headerFooterManager: HeaderFooterManager,
519
- relationshipManager: RelationshipManager,
520
- footnoteManager?: FootnoteManager,
521
- endnoteManager?: EndnoteManager
522
- ): void {
523
- // Step 1: Collect all relationship IDs currently used by hyperlinks
524
- const usedRelIds = new Set<string>();
525
-
526
- // Helper to scan paragraphs for hyperlink relationship IDs
527
- const scanParagraph = (para: Paragraph) => {
528
- for (const item of para.getContent()) {
529
- // Direct hyperlinks in paragraph
530
- if (item instanceof Hyperlink && item.isExternal()) {
531
- const relId = item.getRelationshipId();
532
- if (relId) {
533
- usedRelIds.add(relId);
534
- }
535
- }
536
- // Hyperlinks inside Revision objects (tracked changes)
537
- if (item instanceof Revision) {
538
- for (const revContent of item.getContent()) {
539
- if (isHyperlinkContent(revContent)) {
540
- const hyperlink = revContent;
541
- if (hyperlink.isExternal()) {
542
- const relId = hyperlink.getRelationshipId();
543
- if (relId) {
544
- usedRelIds.add(relId);
545
- }
546
- }
547
- }
548
- }
549
- }
550
- }
551
- };
552
-
553
- // Helper to recursively scan any element type for hyperlinks
554
- const scanElement = (element: BodyElement | Paragraph | Table | StructuredDocumentTag): void => {
555
- if (element instanceof Paragraph) {
556
- // Scan paragraph content for hyperlinks
557
- scanParagraph(element);
558
- }
559
- else if (element instanceof Table) {
560
- // Scan all cells in the table
561
- for (let row = 0; row < element.getRowCount(); row++) {
562
- for (let col = 0; col < element.getColumnCount(); col++) {
563
- const cell = element.getCell(row, col);
564
- if (cell) {
565
- // Scan each paragraph in the cell
566
- const paragraphs = cell.getParagraphs();
567
- for (const para of paragraphs) {
568
- scanParagraph(para);
569
- }
570
- // Scan raw nested content (nested tables, SDTs stored as raw XML)
571
- // Extract any relationship IDs referenced in the raw XML to prevent orphan removal
572
- for (const nested of cell.getRawNestedContent()) {
573
- const rIdPattern = /r:id="(rId\d+)"/g;
574
- let rIdMatch: RegExpExecArray | null;
575
- while ((rIdMatch = rIdPattern.exec(nested.xml)) !== null) {
576
- usedRelIds.add(rIdMatch[1]!);
577
- }
578
- }
579
- }
580
- }
581
- }
582
- }
583
- else if (element instanceof StructuredDocumentTag) {
584
- // Recursively scan SDT content (can contain Paragraphs, Tables, or nested SDTs)
585
- const content = element.getContent();
586
- for (const item of content) {
587
- scanElement(item); // Recursive call handles nested structures
588
- }
589
- }
590
- // TableOfContentsElement is for programmatic TOCs - real TOCs come as SDTs
591
- };
592
-
593
- // Scan body elements (handles all nested structures)
594
- for (const element of bodyElements) {
595
- scanElement(element);
596
- }
597
-
598
- // Scan headers (including tables and SDTs in headers)
599
- const headers = headerFooterManager.getAllHeaders();
600
- for (const header of headers) {
601
- for (const element of header.header.getElements()) {
602
- scanElement(element);
603
- }
604
- }
605
-
606
- // Scan footers (including tables and SDTs in footers)
607
- const footers = headerFooterManager.getAllFooters();
608
- for (const footer of footers) {
609
- for (const element of footer.footer.getElements()) {
610
- scanElement(element);
611
- }
612
- }
613
-
614
- // Scan footnotes for hyperlink relationship IDs
615
- if (footnoteManager) {
616
- for (const footnote of footnoteManager.getAllFootnotes()) {
617
- for (const para of footnote.getParagraphs()) {
618
- scanParagraph(para);
619
- }
620
- }
621
- }
622
-
623
- // Scan endnotes for hyperlink relationship IDs
624
- if (endnoteManager) {
625
- for (const endnote of endnoteManager.getAllEndnotes()) {
626
- for (const para of endnote.getParagraphs()) {
627
- scanParagraph(para);
628
- }
629
- }
630
- }
631
-
632
- // Step 2: Remove ONLY orphaned relationships (not used by any hyperlink)
633
- const allHyperlinkRels = relationshipManager.getRelationshipsByType(
634
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
635
- );
636
-
637
- for (const rel of allHyperlinkRels) {
638
- if (!usedRelIds.has(rel.getId())) {
639
- // This relationship is orphaned - remove it
640
- relationshipManager.removeRelationship(rel.getId());
641
- }
642
- }
643
- }
644
-
645
- /**
646
- * Processes all hyperlinks in paragraphs and registers them with RelationshipManager
647
- * Clears orphaned hyperlink relationships to prevent corruption while preserving valid ones
648
- *
649
- * **IMPORTANT:** This method recursively processes ALL element types including:
650
- * - Top-level paragraphs
651
- * - Tables (all cells)
652
- * - StructuredDocumentTags (SDTs / content controls)
653
- * - Headers and footers
654
- *
655
- * This ensures hyperlinks with URL+anchor combinations (like theSource links)
656
- * that have their relationshipId cleared during parsing get new relationships
657
- * registered before XML generation.
658
- */
659
- processHyperlinks(
660
- bodyElements: BodyElement[],
661
- headerFooterManager: HeaderFooterManager,
662
- relationshipManager: RelationshipManager,
663
- footnoteManager?: FootnoteManager,
664
- endnoteManager?: EndnoteManager
665
- ): void {
666
- const logger = getLogger();
667
- logger.info('Processing hyperlinks');
668
-
669
- // Clear ORPHANED hyperlink relationships to prevent corruption
670
- // This is critical when paragraphs are removed (e.g., via clearParagraphs())
671
- // but preserves relationships for existing hyperlinks (round-trip integrity)
672
- this.clearOrphanedHyperlinkRelationships(
673
- bodyElements,
674
- headerFooterManager,
675
- relationshipManager,
676
- footnoteManager,
677
- endnoteManager
678
- );
679
-
680
- // Helper to recursively process any element type for hyperlinks
681
- // Mirrors the pattern in clearOrphanedHyperlinkRelationships() for consistency
682
- const processElement = (element: BodyElement | Paragraph | Table | StructuredDocumentTag): void => {
683
- if (element instanceof Paragraph) {
684
- this.processHyperlinksInParagraph(element, relationshipManager);
685
- }
686
- else if (element instanceof Table) {
687
- // Process all cells in the table
688
- for (let row = 0; row < element.getRowCount(); row++) {
689
- for (let col = 0; col < element.getColumnCount(); col++) {
690
- const cell = element.getCell(row, col);
691
- if (cell) {
692
- // Scan each paragraph in the cell
693
- const paragraphs = cell.getParagraphs();
694
- for (const para of paragraphs) {
695
- this.processHyperlinksInParagraph(para, relationshipManager);
696
- }
697
- }
698
- }
699
- }
700
- }
701
- else if (element instanceof StructuredDocumentTag) {
702
- // Recursively process SDT content (can contain Paragraphs, Tables, or nested SDTs)
703
- const content = element.getContent();
704
- for (const item of content) {
705
- processElement(item); // Recursive call handles nested structures
706
- }
707
- }
708
- // TableOfContentsElement is for programmatic TOCs - real TOCs come as SDTs
709
- };
710
-
711
- // Process body elements (handles all nested structures)
712
- for (const element of bodyElements) {
713
- processElement(element);
714
- }
715
-
716
- // Process headers (including tables and SDTs in headers)
717
- const headers = headerFooterManager.getAllHeaders();
718
- for (const header of headers) {
719
- for (const element of header.header.getElements()) {
720
- processElement(element);
721
- }
722
- }
723
-
724
- // Process footers (including tables and SDTs in footers)
725
- const footers = headerFooterManager.getAllFooters();
726
- for (const footer of footers) {
727
- for (const element of footer.footer.getElements()) {
728
- processElement(element);
729
- }
730
- }
731
-
732
- // Process footnotes for hyperlinks that need relationship registration
733
- if (footnoteManager) {
734
- for (const footnote of footnoteManager.getAllFootnotes()) {
735
- for (const para of footnote.getParagraphs()) {
736
- this.processHyperlinksInParagraph(para, relationshipManager);
737
- }
738
- }
739
- }
740
-
741
- // Process endnotes for hyperlinks that need relationship registration
742
- if (endnoteManager) {
743
- for (const endnote of endnoteManager.getAllEndnotes()) {
744
- for (const para of endnote.getParagraphs()) {
745
- this.processHyperlinksInParagraph(para, relationshipManager);
746
- }
747
- }
748
- }
749
-
750
- logger.info('Hyperlinks processed');
751
- }
752
-
753
- /**
754
- * Processes hyperlinks in a single paragraph
755
- *
756
- * **Validation:** Throws error if external hyperlink has no URL to prevent
757
- * document corruption per ECMA-376 §17.16.22.
758
- *
759
- * Also processes hyperlinks inside Revision objects (tracked changes).
760
- *
761
- * @throws {Error} If external hyperlink has undefined/empty URL
762
- */
763
- private processHyperlinksInParagraph(
764
- paragraph: Paragraph,
765
- relationshipManager: RelationshipManager
766
- ): void {
767
- const content = paragraph.getContent();
768
-
769
- for (const item of content) {
770
- // Direct hyperlink in paragraph
771
- if (
772
- item instanceof Hyperlink &&
773
- item.isExternal() &&
774
- !item.getRelationshipId()
775
- ) {
776
- this.registerHyperlinkRelationship(item, relationshipManager);
777
- }
778
-
779
- // Hyperlinks inside Revision objects (tracked changes)
780
- if (item instanceof Revision) {
781
- for (const revContent of item.getContent()) {
782
- if (isHyperlinkContent(revContent)) {
783
- const hyperlink = revContent;
784
- if (hyperlink.isExternal() && !hyperlink.getRelationshipId()) {
785
- this.registerHyperlinkRelationship(hyperlink, relationshipManager);
786
- }
787
- }
788
- }
789
- }
790
- }
791
- }
792
-
793
- /**
794
- * Registers a hyperlink with the relationship manager
795
- *
796
- * @throws {Error} If hyperlink has no URL
797
- */
798
- private registerHyperlinkRelationship(
799
- hyperlink: Hyperlink,
800
- relationshipManager: RelationshipManager
801
- ): void {
802
- const url = hyperlink.getUrl();
803
-
804
- // Validate that external hyperlink has a URL
805
- // This prevents invalid document generation and fails early with clear error
806
- if (!url) {
807
- throw new Error(
808
- `Invalid hyperlink in paragraph: External hyperlink "${hyperlink.getText()}" has no URL. ` +
809
- `This would create a corrupted document per ECMA-376 §17.16.22. ` +
810
- `Fix the hyperlink by providing a valid URL before saving.`
811
- );
812
- }
813
-
814
- const relationship = relationshipManager.addHyperlink(url);
815
- hyperlink.setRelationshipId(relationship.getId());
816
- }
817
-
818
- /**
819
- * Generates word/fontTable.xml
820
- * Required for DOCX compliance - defines fonts used in the document
821
- */
822
- generateFontTable(): string {
823
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
824
- <w:fonts xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
825
- xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
826
- <w:font w:name="Calibri">
827
- <w:panose1 w:val="020F0502020204030204"/>
828
- <w:charset w:val="00"/>
829
- <w:family w:val="swiss"/>
830
- <w:pitch w:val="variable"/>
831
- <w:sig w:usb0="E10002FF" w:usb1="4000ACFF" w:usb2="00000009" w:usb3="00000000" w:csb0="0000019F" w:csb1="00000000"/>
832
- </w:font>
833
- <w:font w:name="Times New Roman">
834
- <w:panose1 w:val="02020603050405020304"/>
835
- <w:charset w:val="00"/>
836
- <w:family w:val="roman"/>
837
- <w:pitch w:val="variable"/>
838
- <w:sig w:usb0="E0002AFF" w:usb1="C000785B" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/>
839
- </w:font>
840
- <w:font w:name="Arial">
841
- <w:panose1 w:val="020B0604020202020204"/>
842
- <w:charset w:val="00"/>
843
- <w:family w:val="swiss"/>
844
- <w:pitch w:val="variable"/>
845
- <w:sig w:usb0="E0002AFF" w:usb1="C000247B" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/>
846
- </w:font>
847
- <w:font w:name="Courier New">
848
- <w:panose1 w:val="02070309020205020404"/>
849
- <w:charset w:val="00"/>
850
- <w:family w:val="modern"/>
851
- <w:pitch w:val="fixed"/>
852
- <w:sig w:usb0="E0002AFF" w:usb1="C0007843" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/>
853
- </w:font>
854
- <w:font w:name="Calibri Light">
855
- <w:panose1 w:val="020F0302020204030204"/>
856
- <w:charset w:val="00"/>
857
- <w:family w:val="swiss"/>
858
- <w:pitch w:val="variable"/>
859
- <w:sig w:usb0="E10002FF" w:usb1="4000ACFF" w:usb2="00000009" w:usb3="00000000" w:csb0="0000019F" w:csb1="00000000"/>
860
- </w:font>
861
- <w:font w:name="Georgia">
862
- <w:panose1 w:val="02040502050204030303"/>
863
- <w:charset w:val="00"/>
864
- <w:family w:val="roman"/>
865
- <w:pitch w:val="variable"/>
866
- <w:sig w:usb0="E0002AFF" w:usb1="00000000" w:usb2="00000000" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/>
867
- </w:font>
868
- </w:fonts>`;
869
- }
870
-
871
- /**
872
- * Generates word/webSettings.xml
873
- * When called with no argument, produces the minimal static template.
874
- * When called with settings, generates XML reflecting in-memory state.
875
- */
876
- generateWebSettings(settings?: {
877
- optimizeForBrowser?: boolean;
878
- allowPNG?: boolean;
879
- relyOnVML?: boolean;
880
- doNotRelyOnCSS?: boolean;
881
- doNotSaveAsSingleFile?: boolean;
882
- doNotOrganizeInFolder?: boolean;
883
- doNotUseLongFileNames?: boolean;
884
- pixelsPerInch?: number;
885
- targetScreenSz?: string;
886
- encoding?: string;
887
- }): string {
888
- if (!settings) {
889
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
890
- <w:webSettings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
891
- xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
892
- <w:optimizeForBrowser/>
893
- <w:allowPNG/>
894
- </w:webSettings>`;
895
- }
896
-
897
- let children = '';
898
- if (settings.optimizeForBrowser) children += '\n <w:optimizeForBrowser/>';
899
- if (settings.allowPNG) children += '\n <w:allowPNG/>';
900
- if (settings.relyOnVML) children += '\n <w:relyOnVML/>';
901
- if (settings.doNotRelyOnCSS) children += '\n <w:doNotRelyOnCSS/>';
902
- if (settings.doNotSaveAsSingleFile) children += '\n <w:doNotSaveAsSingleFile/>';
903
- if (settings.doNotOrganizeInFolder) children += '\n <w:doNotOrganizeInFolder/>';
904
- if (settings.doNotUseLongFileNames) children += '\n <w:doNotUseLongFileNames/>';
905
- if (settings.pixelsPerInch !== undefined) children += `\n <w:pixelsPerInch w:val="${settings.pixelsPerInch}"/>`;
906
- if (settings.targetScreenSz) children += `\n <w:targetScreenSz w:val="${settings.targetScreenSz}"/>`;
907
- if (settings.encoding) children += `\n <w:encoding w:val="${settings.encoding}"/>`;
908
-
909
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
910
- <w:webSettings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
911
- xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${children}
912
- </w:webSettings>`;
913
- }
914
-
915
- /**
916
- * Generates word/settings.xml
917
- * Required for DOCX compliance - defines document settings
918
- * @param trackChangesSettings - Optional track changes settings
919
- */
920
- generateSettings(trackChangesSettings?: TrackChangesSettings): string {
921
- let xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
922
- <w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
923
- xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
924
- <w:zoom w:percent="100"/>`;
925
-
926
- // Track changes settings ordered per CT_Settings:
927
- // revisionView (#30) trackRevisions (#31) → doNotTrackFormatting (#33)
928
- if (trackChangesSettings?.revisionView) {
929
- const view = trackChangesSettings.revisionView;
930
- // Only emit revisionView if it differs from defaults (all true)
931
- if (!view.showInsertionsAndDeletions || !view.showFormatting || view.showInkAnnotations === false) {
932
- xml += `\n <w:revisionView w:insDel="${view.showInsertionsAndDeletions ? '1' : '0'}" w:formatting="${view.showFormatting ? '1' : '0'}"`;
933
- if (view.showInkAnnotations !== undefined) {
934
- xml += ` w:inkAnnotations="${view.showInkAnnotations ? '1' : '0'}"`;
935
- }
936
- xml += '/>';
937
- }
938
- }
939
-
940
- if (trackChangesSettings?.trackChangesEnabled) {
941
- xml += '\n <w:trackRevisions/>';
942
- }
943
-
944
- if (trackChangesSettings?.trackFormatting !== undefined) {
945
- if (!trackChangesSettings.trackFormatting) {
946
- xml += '\n <w:doNotTrackFormatting/>';
947
- }
948
- // trackFormatting=true: emit nothing (default when w:trackRevisions is present)
949
- }
950
-
951
- // Document protection
952
- if (trackChangesSettings?.documentProtection) {
953
- const prot = trackChangesSettings.documentProtection;
954
- xml += `\n <w:documentProtection w:edit="${prot.edit}" w:enforcement="${prot.enforcement ? '1' : '0'}"`;
955
- if (prot.cryptProviderType) {
956
- xml += ` w:cryptProviderType="${prot.cryptProviderType}"`;
957
- }
958
- if (prot.cryptAlgorithmClass) {
959
- xml += ` w:cryptAlgorithmClass="${prot.cryptAlgorithmClass}"`;
960
- }
961
- if (prot.cryptAlgorithmType) {
962
- xml += ` w:cryptAlgorithmType="${prot.cryptAlgorithmType}"`;
963
- }
964
- if (prot.cryptAlgorithmSid) {
965
- xml += ` w:cryptAlgorithmSid="${prot.cryptAlgorithmSid}"`;
966
- }
967
- if (prot.cryptSpinCount) {
968
- xml += ` w:cryptSpinCount="${prot.cryptSpinCount}"`;
969
- }
970
- if (prot.hash) {
971
- xml += ` w:hash="${prot.hash}"`;
972
- }
973
- if (prot.salt) {
974
- xml += ` w:salt="${prot.salt}"`;
975
- }
976
- xml += '/>';
977
- }
978
-
979
- xml += `
980
- <w:defaultTabStop w:val="720"/>
981
- <w:characterSpacingControl w:val="doNotCompress"/>
982
- <w:updateFields w:val="true"/>`;
983
-
984
- // RSIDs (Revision Save IDs)
985
- if (trackChangesSettings?.rsids && trackChangesSettings.rsids.length > 0) {
986
- xml += '\n <w:rsids>';
987
- if (trackChangesSettings.rsidRoot) {
988
- xml += `\n <w:rsidRoot w:val="${trackChangesSettings.rsidRoot}"/>`;
989
- }
990
- for (const rsid of trackChangesSettings.rsids) {
991
- xml += `\n <w:rsid w:val="${rsid}"/>`;
992
- }
993
- xml += '\n </w:rsids>';
994
- }
995
-
996
- xml += `
997
- <w:compat>
998
- <w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/>
999
- <w:compatSetting w:name="overrideTableStyleFontSizeAndJustification" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
1000
- <w:compatSetting w:name="enableOpenTypeFeatures" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
1001
- <w:compatSetting w:name="doNotFlipMirrorIndents" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
1002
- <w:compatSetting w:name="differentiateMultirowTableHeaders" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
1003
- </w:compat>
1004
- <w:themeFontLang w:val="en-US"/>
1005
- <w:clrSchemeMapping w:bg1="light1" w:t1="dark1" w:bg2="light2" w:t2="dark2" w:accent1="accent1" w:accent2="accent2" w:accent3="accent3" w:accent4="accent4" w:accent5="accent5" w:accent6="accent6" w:hyperlink="hyperlink" w:followedHyperlink="followedHyperlink"/>
1006
- </w:settings>`;
1007
-
1008
- return xml;
1009
- }
1010
-
1011
- /**
1012
- * Generates word/theme/theme1.xml
1013
- * Required for DOCX compliance - defines color and font theme
1014
- */
1015
- generateTheme(): string {
1016
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1017
- <a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
1018
- <a:themeElements>
1019
- <a:clrScheme name="Office">
1020
- <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
1021
- <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
1022
- <a:dk2><a:srgbClr val="44546A"/></a:dk2>
1023
- <a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
1024
- <a:accent1><a:srgbClr val="5B9BD5"/></a:accent1>
1025
- <a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
1026
- <a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
1027
- <a:accent4><a:srgbClr val="FFC000"/></a:accent4>
1028
- <a:accent5><a:srgbClr val="4472C4"/></a:accent5>
1029
- <a:accent6><a:srgbClr val="70AD47"/></a:accent6>
1030
- <a:hlink><a:srgbClr val="0563C1"/></a:hlink>
1031
- <a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
1032
- </a:clrScheme>
1033
- <a:fontScheme name="Office">
1034
- <a:majorFont>
1035
- <a:latin typeface="Calibri Light" panose="020F0302020204030204"/>
1036
- <a:ea typeface=""/>
1037
- <a:cs typeface=""/>
1038
- </a:majorFont>
1039
- <a:minorFont>
1040
- <a:latin typeface="Calibri" panose="020F0502020204030204"/>
1041
- <a:ea typeface=""/>
1042
- <a:cs typeface=""/>
1043
- </a:minorFont>
1044
- </a:fontScheme>
1045
- <a:fmtScheme name="Office">
1046
- <a:fillStyleLst>
1047
- <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
1048
- <a:gradFill rotWithShape="1">
1049
- <a:gsLst>
1050
- <a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs>
1051
- <a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs>
1052
- <a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs>
1053
- </a:gsLst>
1054
- <a:lin ang="5400000" scaled="0"/>
1055
- </a:gradFill>
1056
- <a:gradFill rotWithShape="1">
1057
- <a:gsLst>
1058
- <a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs>
1059
- <a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs>
1060
- <a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs>
1061
- </a:gsLst>
1062
- <a:lin ang="5400000" scaled="0"/>
1063
- </a:gradFill>
1064
- </a:fillStyleLst>
1065
- <a:lnStyleLst>
1066
- <a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
1067
- <a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
1068
- <a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
1069
- </a:lnStyleLst>
1070
- <a:effectStyleLst>
1071
- <a:effectStyle><a:effectLst/></a:effectStyle>
1072
- <a:effectStyle><a:effectLst/></a:effectStyle>
1073
- <a:effectStyle>
1074
- <a:effectLst>
1075
- <a:outerShdw blurRad="57150" dist="19050" dir="5400000" algn="ctr" rotWithShape="0">
1076
- <a:srgbClr val="000000"><a:alpha val="63000"/></a:srgbClr>
1077
- </a:outerShdw>
1078
- </a:effectLst>
1079
- </a:effectStyle>
1080
- </a:effectStyleLst>
1081
- <a:bgFillStyleLst>
1082
- <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
1083
- <a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill>
1084
- <a:gradFill rotWithShape="1">
1085
- <a:gsLst>
1086
- <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs>
1087
- <a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs>
1088
- <a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs>
1089
- </a:gsLst>
1090
- <a:lin ang="5400000" scaled="0"/>
1091
- </a:gradFill>
1092
- </a:bgFillStyleLst>
1093
- </a:fmtScheme>
1094
- </a:themeElements>
1095
- <a:objectDefaults/>
1096
- <a:extraClrSchemeLst/>
1097
- <a:extLst>
1098
- <a:ext uri="{05A4C25C-085E-4340-85A3-A5531E510DB2}">
1099
- <thm15:themeFamily xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" name="Office Theme" id="{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}" vid="{4A3C46E8-61CC-4603-A589-7422A47A8E4A}"/>
1100
- </a:ext>
1101
- </a:extLst>
1102
- </a:theme>`;
1103
- }
1104
- }
1
+ /**
2
+ * DocumentGenerator - Handles XML generation for DOCX files
3
+ * Converts structured data to OpenXML format
4
+ */
5
+
6
+ import { CommentManager } from '../elements/CommentManager';
7
+ import { EndnoteManager } from '../elements/EndnoteManager';
8
+ import { FontManager } from '../elements/FontManager';
9
+ import { FootnoteManager } from '../elements/FootnoteManager';
10
+ import { HeaderFooterManager } from '../elements/HeaderFooterManager';
11
+ import { Hyperlink } from '../elements/Hyperlink';
12
+ import { ImageManager } from '../elements/ImageManager';
13
+ import { Paragraph } from '../elements/Paragraph';
14
+ import { Revision } from '../elements/Revision';
15
+ import { isHyperlinkContent } from '../elements/RevisionContent';
16
+ import { Section } from '../elements/Section';
17
+ import { StructuredDocumentTag } from '../elements/StructuredDocumentTag';
18
+ import { Table } from '../elements/Table';
19
+ import { TableOfContentsElement } from '../elements/TableOfContentsElement';
20
+ import { AlternateContent } from '../elements/AlternateContent';
21
+ import { MathParagraph } from '../elements/MathElement';
22
+ import { CustomXmlBlock } from '../elements/CustomXml';
23
+ import { PreservedElement } from '../elements/PreservedElement';
24
+ import { formatDateForXml } from '../utils/dateFormatting';
25
+ import { getGlobalLogger, createScopedLogger, ILogger } from '../utils/logger';
26
+ import { XMLBuilder, XMLElement } from '../xml/XMLBuilder';
27
+ import { DocumentProperties } from './Document';
28
+ import { BodyElement } from './DocumentContent';
29
+ import { RelationshipManager } from './RelationshipManager';
30
+ import { TrackChangesSettings } from '../types/settings-types';
31
+
32
+ // Create scoped logger for DocumentGenerator operations
33
+ function getLogger(): ILogger {
34
+ return createScopedLogger(getGlobalLogger(), 'DocumentGenerator');
35
+ }
36
+
37
+ /**
38
+ * Interface for ZipHandler methods used in content type generation
39
+ * This provides type safety for the ZipHandler parameter without creating
40
+ * a circular dependency with the zip module.
41
+ */
42
+ export interface IZipHandlerReader {
43
+ /** Get list of file paths in the archive */
44
+ getFilePaths?(): string[];
45
+ /** Check if a file exists in the archive */
46
+ hasFile?(path: string): boolean;
47
+ }
48
+
49
+ /**
50
+ * Normalizes toXML() output to always return an array.
51
+ * Some elements (e.g., TableOfContentsElement) return XMLElement[], while others return XMLElement.
52
+ * This helper provides consistent array handling.
53
+ *
54
+ * @param xml - XMLElement or XMLElement[] from toXML()
55
+ * @returns XMLElement array
56
+ */
57
+ function normalizeXmlOutput(xml: XMLElement | XMLElement[]): XMLElement[] {
58
+ return Array.isArray(xml) ? xml : [xml];
59
+ }
60
+
61
+ /**
62
+ * DocumentGenerator handles all XML generation logic
63
+ */
64
+ export class DocumentGenerator {
65
+ /**
66
+ * Generates [Content_Types].xml
67
+ */
68
+ generateContentTypes(): string {
69
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
70
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
71
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
72
+ <Default Extension="xml" ContentType="application/xml"/>
73
+ <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
74
+ <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
75
+ <Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
76
+ <Override PartName="/word/fontTable.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"/>
77
+ <Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/>
78
+ <Override PartName="/word/webSettings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"/>
79
+ <Override PartName="/word/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
80
+ <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
81
+ <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
82
+ </Types>`;
83
+ }
84
+
85
+ /**
86
+ * Generates _rels/.rels
87
+ */
88
+ generateRels(): string {
89
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
90
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
91
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
92
+ <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
93
+ <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
94
+ </Relationships>`;
95
+ }
96
+
97
+ /**
98
+ * Generates word/document.xml with current body elements
99
+ */
100
+ generateDocumentXml(
101
+ bodyElements: BodyElement[],
102
+ section: Section,
103
+ namespaces: Record<string, string>,
104
+ documentBackground?: {
105
+ color?: string;
106
+ themeColor?: string;
107
+ themeTint?: string;
108
+ themeShade?: string;
109
+ }
110
+ ): string {
111
+ const logger = getLogger();
112
+ logger.info('Generating document.xml', { elementCount: bodyElements.length });
113
+
114
+ const bodyXmls: XMLElement[] = [];
115
+
116
+ // Generate XML for each body element
117
+ // Uses normalizeXmlOutput() to handle both single XMLElement and XMLElement[] returns
118
+ for (const element of bodyElements) {
119
+ const xmlElements = normalizeXmlOutput(element.toXML());
120
+ bodyXmls.push(...xmlElements);
121
+ }
122
+
123
+ // Add section properties at the end
124
+ bodyXmls.push(section.toXML());
125
+
126
+ // Build pre-body content (w:background) per ECMA-376 Part 1 §17.2.1
127
+ let preBodyContent: XMLElement[] | undefined;
128
+ if (documentBackground) {
129
+ const bgAttrs: Record<string, string> = {};
130
+ if (documentBackground.color) bgAttrs['w:color'] = documentBackground.color;
131
+ if (documentBackground.themeColor) bgAttrs['w:themeColor'] = documentBackground.themeColor;
132
+ if (documentBackground.themeTint) bgAttrs['w:themeTint'] = documentBackground.themeTint;
133
+ if (documentBackground.themeShade) bgAttrs['w:themeShade'] = documentBackground.themeShade;
134
+ preBodyContent = [XMLBuilder.wSelf('background', bgAttrs)];
135
+ }
136
+
137
+ const result = XMLBuilder.createDocument(bodyXmls, namespaces, preBodyContent);
138
+ logger.info('Document.xml generated', { xmlSize: result.length });
139
+ return result;
140
+ }
141
+
142
+ /**
143
+ * Generates docProps/core.xml with extended properties
144
+ */
145
+ generateCoreProps(properties: DocumentProperties): string {
146
+ const now = new Date();
147
+ const created = properties.created || now;
148
+ const modified = properties.modified || now;
149
+
150
+ const formatDate = (date: Date): string => {
151
+ return formatDateForXml(date);
152
+ };
153
+
154
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
155
+ <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
156
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
157
+ xmlns:dcterms="http://purl.org/dc/terms/"
158
+ xmlns:dcmitype="http://purl.org/dc/dcmitype/"
159
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
160
+ <dc:title>${XMLBuilder.sanitizeXmlContent(properties.title || '')}</dc:title>
161
+ <dc:subject>${XMLBuilder.sanitizeXmlContent(properties.subject || '')}</dc:subject>
162
+ <dc:creator>${XMLBuilder.sanitizeXmlContent(properties.creator || 'DocXML')}</dc:creator>
163
+ <cp:keywords>${XMLBuilder.sanitizeXmlContent(properties.keywords || '')}</cp:keywords>
164
+ <dc:description>${XMLBuilder.sanitizeXmlContent(properties.description || '')}</dc:description>
165
+ <cp:lastModifiedBy>${XMLBuilder.sanitizeXmlContent(
166
+ properties.lastModifiedBy || properties.creator || 'DocXML'
167
+ )}</cp:lastModifiedBy>
168
+ <cp:revision>${properties.revision || 1}</cp:revision>${
169
+ properties.category
170
+ ? `\n <cp:category>${XMLBuilder.sanitizeXmlContent(properties.category)}</cp:category>`
171
+ : ''
172
+ }${
173
+ properties.contentStatus
174
+ ? `\n <cp:contentStatus>${XMLBuilder.sanitizeXmlContent(
175
+ properties.contentStatus
176
+ )}</cp:contentStatus>`
177
+ : ''
178
+ }${
179
+ properties.language
180
+ ? `\n <dc:language>${XMLBuilder.sanitizeXmlContent(properties.language)}</dc:language>`
181
+ : ''
182
+ }
183
+ <dcterms:created xsi:type="dcterms:W3CDTF">${formatDate(created)}</dcterms:created>
184
+ <dcterms:modified xsi:type="dcterms:W3CDTF">${formatDate(modified)}</dcterms:modified>
185
+ </cp:coreProperties>`;
186
+ }
187
+
188
+ /**
189
+ * Generates docProps/app.xml with extended properties
190
+ */
191
+ generateAppProps(properties: DocumentProperties = {}): string {
192
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
193
+ <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
194
+ xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
195
+ <Application>${XMLBuilder.sanitizeXmlContent(
196
+ properties.application || 'docxmlater'
197
+ )}</Application>
198
+ <DocSecurity>0</DocSecurity>
199
+ <ScaleCrop>false</ScaleCrop>
200
+ <Company>${XMLBuilder.sanitizeXmlContent(properties.company || '')}</Company>${
201
+ properties.manager
202
+ ? `\n <Manager>${XMLBuilder.sanitizeXmlContent(properties.manager)}</Manager>`
203
+ : ''
204
+ }
205
+ <LinksUpToDate>false</LinksUpToDate>
206
+ <SharedDoc>false</SharedDoc>
207
+ <HyperlinksChanged>false</HyperlinksChanged>
208
+ <AppVersion>${XMLBuilder.sanitizeXmlContent(
209
+ properties.appVersion || properties.version || '1.0.0'
210
+ )}</AppVersion>
211
+ </Properties>`;
212
+ }
213
+
214
+ /**
215
+ * Generates docProps/custom.xml with custom properties
216
+ */
217
+ generateCustomProps(customProps: Record<string, string | number | boolean | Date>): string {
218
+ if (!customProps || Object.keys(customProps).length === 0) {
219
+ return '';
220
+ }
221
+
222
+ const formatCustomValue = (
223
+ key: string,
224
+ value: string | number | boolean | Date,
225
+ pid: number
226
+ ): string => {
227
+ if (typeof value === 'string') {
228
+ return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${XMLBuilder.sanitizeXmlContent(
229
+ key
230
+ )}">
231
+ <vt:lpwstr>${XMLBuilder.sanitizeXmlContent(value)}</vt:lpwstr>
232
+ </property>`;
233
+ } else if (typeof value === 'number') {
234
+ return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${XMLBuilder.sanitizeXmlContent(
235
+ key
236
+ )}">
237
+ <vt:r8>${value}</vt:r8>
238
+ </property>`;
239
+ } else if (typeof value === 'boolean') {
240
+ return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${XMLBuilder.sanitizeXmlContent(
241
+ key
242
+ )}">
243
+ <vt:bool>${value ? 'true' : 'false'}</vt:bool>
244
+ </property>`;
245
+ } else if (value instanceof Date) {
246
+ return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${XMLBuilder.sanitizeXmlContent(
247
+ key
248
+ )}">
249
+ <vt:filetime>${value.toISOString()}</vt:filetime>
250
+ </property>`;
251
+ }
252
+ return '';
253
+ };
254
+
255
+ const properties = Object.entries(customProps)
256
+ .map(([key, value], index) => formatCustomValue(key, value, index + 2))
257
+ .filter((prop) => prop !== '')
258
+ .join('\n');
259
+
260
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
261
+ <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
262
+ xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
263
+ ${properties}
264
+ </Properties>`;
265
+ }
266
+
267
+ /**
268
+ * Generates [Content_Types].xml with image extensions, headers/footers, comments, and fonts
269
+ * Preserves entries for files that exist in the loaded document (customXML, etc.)
270
+ * Merges framework-generated entries with original entries for round-trip fidelity
271
+ */
272
+ generateContentTypesWithImagesHeadersFootersAndComments(
273
+ imageManager: ImageManager,
274
+ headerFooterManager: HeaderFooterManager,
275
+ commentManager: CommentManager,
276
+ zipHandler: IZipHandlerReader,
277
+ fontManager?: FontManager,
278
+ hasCustomProperties = false,
279
+ originalContentTypes?: { defaults: Set<string>; overrides: Set<string> },
280
+ footnoteManager?: FootnoteManager,
281
+ endnoteManager?: EndnoteManager
282
+ ): string {
283
+ const images = imageManager.getAllImages();
284
+ const headers = headerFooterManager.getAllHeaders();
285
+ const footers = headerFooterManager.getAllFooters();
286
+ const hasComments = commentManager.getCount() > 0;
287
+
288
+ // Build sets for framework-generated entries
289
+ const generatedDefaults = new Set<string>();
290
+ const generatedOverrides = new Set<string>();
291
+
292
+ // Default types - always needed
293
+ generatedDefaults.add('rels|application/vnd.openxmlformats-package.relationships+xml');
294
+ generatedDefaults.add('xml|application/xml');
295
+
296
+ // Image extensions from ImageManager
297
+ for (const entry of images) {
298
+ const ext = entry.image.getExtension();
299
+ const mimeType = ImageManager.getMimeType(ext);
300
+ generatedDefaults.add(`${ext}|${mimeType}`);
301
+ }
302
+
303
+ // Also detect image files in the archive not tracked by ImageManager
304
+ // (e.g., numPicBullet images referenced by numbering.xml.rels)
305
+ const mediaExtensions = new Map<string, string>([
306
+ ['png', 'image/png'],
307
+ ['jpeg', 'image/jpeg'],
308
+ ['jpg', 'image/jpeg'],
309
+ ['gif', 'image/gif'],
310
+ ['bmp', 'image/bmp'],
311
+ ['tiff', 'image/tiff'],
312
+ ['emf', 'image/x-emf'],
313
+ ['wmf', 'image/x-wmf'],
314
+ ]);
315
+ for (const file of zipHandler.getFilePaths?.() || []) {
316
+ if (file.startsWith('word/media/')) {
317
+ const ext = file.split('.').pop()?.toLowerCase();
318
+ if (ext && mediaExtensions.has(ext)) {
319
+ generatedDefaults.add(`${ext}|${mediaExtensions.get(ext)}`);
320
+ }
321
+ }
322
+ }
323
+
324
+ // Font extensions (if FontManager provided)
325
+ if (fontManager && fontManager.getCount() > 0) {
326
+ const fontEntries = fontManager.generateContentTypeEntries();
327
+ for (const entry of fontEntries) {
328
+ // Parse each entry and add to set (entries are XML strings)
329
+ const extMatch = /Extension="([^"]+)"/.exec(entry);
330
+ const typeMatch = /ContentType="([^"]+)"/.exec(entry);
331
+ if (extMatch && typeMatch) {
332
+ generatedDefaults.add(`${extMatch[1]}|${typeMatch[1]}`);
333
+ }
334
+ }
335
+ }
336
+
337
+ // Check for embedded .ttf fonts from original document
338
+ // Also create a Set for efficient file existence checks
339
+ const files = zipHandler.getFilePaths?.() || [];
340
+ const filesInArchive = new Set(files);
341
+ const hasTtfFonts = files.some((f: string) => f.endsWith('.ttf'));
342
+ if (hasTtfFonts) {
343
+ generatedDefaults.add('ttf|application/x-font-ttf');
344
+ }
345
+
346
+ // Override types - only add if file exists in archive
347
+ generatedOverrides.add(
348
+ '/word/document.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'
349
+ );
350
+ if (filesInArchive.has('word/styles.xml')) {
351
+ generatedOverrides.add(
352
+ '/word/styles.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml'
353
+ );
354
+ }
355
+ if (filesInArchive.has('word/numbering.xml')) {
356
+ generatedOverrides.add(
357
+ '/word/numbering.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml'
358
+ );
359
+ }
360
+ if (filesInArchive.has('word/fontTable.xml')) {
361
+ generatedOverrides.add(
362
+ '/word/fontTable.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml'
363
+ );
364
+ }
365
+ if (filesInArchive.has('word/settings.xml')) {
366
+ generatedOverrides.add(
367
+ '/word/settings.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml'
368
+ );
369
+ }
370
+ if (filesInArchive.has('word/webSettings.xml')) {
371
+ generatedOverrides.add(
372
+ '/word/webSettings.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml'
373
+ );
374
+ }
375
+ if (filesInArchive.has('word/theme/theme1.xml')) {
376
+ generatedOverrides.add(
377
+ '/word/theme/theme1.xml|application/vnd.openxmlformats-officedocument.theme+xml'
378
+ );
379
+ }
380
+
381
+ // Headers - only add if file actually exists in archive
382
+ // This prevents corruption when HeaderFooterManager has stale entries for removed headers
383
+ for (const entry of headers) {
384
+ const filePath = `word/${entry.filename}`;
385
+ if (filesInArchive.has(filePath)) {
386
+ generatedOverrides.add(
387
+ `/word/${entry.filename}|application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml`
388
+ );
389
+ }
390
+ }
391
+
392
+ // Footers - only add if file actually exists in archive
393
+ // This prevents corruption when HeaderFooterManager has stale entries for removed footers
394
+ for (const entry of footers) {
395
+ const filePath = `word/${entry.filename}`;
396
+ if (filesInArchive.has(filePath)) {
397
+ generatedOverrides.add(
398
+ `/word/${entry.filename}|application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml`
399
+ );
400
+ }
401
+ }
402
+
403
+ // Comments
404
+ if (hasComments) {
405
+ generatedOverrides.add(
406
+ '/word/comments.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml'
407
+ );
408
+ }
409
+
410
+ // Footnotes
411
+ const hasFootnotes =
412
+ (footnoteManager && footnoteManager.getCount() > 0) ||
413
+ filesInArchive.has('word/footnotes.xml');
414
+ if (hasFootnotes) {
415
+ generatedOverrides.add(
416
+ '/word/footnotes.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml'
417
+ );
418
+ }
419
+
420
+ // Endnotes
421
+ const hasEndnotes =
422
+ (endnoteManager && endnoteManager.getCount() > 0) || filesInArchive.has('word/endnotes.xml');
423
+ if (hasEndnotes) {
424
+ generatedOverrides.add(
425
+ '/word/endnotes.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml'
426
+ );
427
+ }
428
+
429
+ // People (track changes authors)
430
+ if (filesInArchive.has('word/people.xml')) {
431
+ generatedOverrides.add(
432
+ '/word/people.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml'
433
+ );
434
+ }
435
+
436
+ // Comment companion files (passthrough)
437
+ if (filesInArchive.has('word/commentsExtended.xml')) {
438
+ generatedOverrides.add(
439
+ '/word/commentsExtended.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml'
440
+ );
441
+ }
442
+ if (filesInArchive.has('word/commentsIds.xml')) {
443
+ generatedOverrides.add(
444
+ '/word/commentsIds.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml'
445
+ );
446
+ }
447
+ if (filesInArchive.has('word/commentsExtensible.xml')) {
448
+ generatedOverrides.add(
449
+ '/word/commentsExtensible.xml|application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml'
450
+ );
451
+ }
452
+
453
+ // Core properties (only add if file exists)
454
+ if (zipHandler.hasFile?.('docProps/core.xml') || filesInArchive.has('docProps/core.xml')) {
455
+ generatedOverrides.add(
456
+ '/docProps/core.xml|application/vnd.openxmlformats-package.core-properties+xml'
457
+ );
458
+ }
459
+
460
+ // App.xml if it exists
461
+ if (zipHandler.hasFile?.('docProps/app.xml')) {
462
+ generatedOverrides.add(
463
+ '/docProps/app.xml|application/vnd.openxmlformats-officedocument.extended-properties+xml'
464
+ );
465
+ }
466
+
467
+ // Custom properties if exists or will be created
468
+ if (zipHandler.hasFile?.('docProps/custom.xml') || hasCustomProperties) {
469
+ generatedOverrides.add(
470
+ '/docProps/custom.xml|application/vnd.openxmlformats-officedocument.custom-properties+xml'
471
+ );
472
+ }
473
+
474
+ // CustomXML entries if they exist
475
+ if (zipHandler.hasFile?.('customXML/item1.xml')) {
476
+ generatedOverrides.add('/customXML/item1.xml|application/xml');
477
+ }
478
+ if (zipHandler.hasFile?.('customXML/itemProps1.xml')) {
479
+ generatedOverrides.add(
480
+ '/customXML/itemProps1.xml|application/vnd.openxmlformats-officedocument.customXmlProperties+xml'
481
+ );
482
+ }
483
+
484
+ // Merge with original entries, but ONLY keep overrides for files that actually exist
485
+ // This prevents corruption when headers/footers are removed but their Content_Types entries
486
+ // from the original document would otherwise be preserved
487
+ const allDefaults = new Set([...generatedDefaults, ...(originalContentTypes?.defaults || [])]);
488
+
489
+ // filesInArchive was created earlier (line 318) for header/footer validation
490
+ // Reuse it here to filter original overrides as well
491
+
492
+ // Filter original overrides to only include files that exist in the archive
493
+ const filteredOriginalOverrides: string[] = [];
494
+ for (const entry of originalContentTypes?.overrides || []) {
495
+ const parts = entry.split('|');
496
+ const partName = parts[0] || '';
497
+ // Convert /word/footer1.xml to word/footer1.xml for comparison
498
+ const normalizedPath = partName.startsWith('/') ? partName.slice(1) : partName;
499
+ if (filesInArchive.has(normalizedPath)) {
500
+ filteredOriginalOverrides.push(entry);
501
+ }
502
+ }
503
+
504
+ const allOverrides = new Set([...generatedOverrides, ...filteredOriginalOverrides]);
505
+
506
+ // Build XML from merged sets
507
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
508
+ xml += '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">\n';
509
+
510
+ // Add all default entries (escape attribute values for security)
511
+ for (const entry of allDefaults) {
512
+ const parts = entry.split('|');
513
+ const ext = parts[0] || '';
514
+ const contentType = parts[1] || '';
515
+ const escapedExt = XMLBuilder.escapeXmlAttribute(ext);
516
+ const escapedContentType = XMLBuilder.escapeXmlAttribute(contentType);
517
+ xml += ` <Default Extension="${escapedExt}" ContentType="${escapedContentType}"/>\n`;
518
+ }
519
+
520
+ // Add all override entries (escape attribute values for security)
521
+ for (const entry of allOverrides) {
522
+ const parts = entry.split('|');
523
+ const partName = parts[0] || '';
524
+ const contentType = parts[1] || '';
525
+ const escapedPartName = XMLBuilder.escapeXmlAttribute(partName);
526
+ const escapedContentType = XMLBuilder.escapeXmlAttribute(contentType);
527
+ xml += ` <Override PartName="${escapedPartName}" ContentType="${escapedContentType}"/>\n`;
528
+ }
529
+
530
+ xml += '</Types>';
531
+
532
+ return xml;
533
+ }
534
+
535
+ /**
536
+ * Clears ORPHANED hyperlink relationships from the RelationshipManager
537
+ * Only removes relationships that don't have corresponding hyperlinks in the document
538
+ *
539
+ * This prevents corruption when paragraphs with hyperlinks are removed but
540
+ * their relationships remain, causing Word's "unreadable content" error.
541
+ * Preserves relationships for existing hyperlinks to maintain round-trip integrity.
542
+ */
543
+ private clearOrphanedHyperlinkRelationships(
544
+ bodyElements: BodyElement[],
545
+ headerFooterManager: HeaderFooterManager,
546
+ relationshipManager: RelationshipManager,
547
+ footnoteManager?: FootnoteManager,
548
+ endnoteManager?: EndnoteManager
549
+ ): void {
550
+ // Step 1: Collect all relationship IDs currently used by hyperlinks
551
+ const usedRelIds = new Set<string>();
552
+
553
+ // Helper to scan paragraphs for hyperlink relationship IDs
554
+ const scanParagraph = (para: Paragraph) => {
555
+ for (const item of para.getContent()) {
556
+ // Direct hyperlinks in paragraph
557
+ if (item instanceof Hyperlink && item.isExternal()) {
558
+ const relId = item.getRelationshipId();
559
+ if (relId) {
560
+ usedRelIds.add(relId);
561
+ }
562
+ }
563
+ // Hyperlinks inside Revision objects (tracked changes)
564
+ if (item instanceof Revision) {
565
+ for (const revContent of item.getContent()) {
566
+ if (isHyperlinkContent(revContent)) {
567
+ const hyperlink = revContent;
568
+ if (hyperlink.isExternal()) {
569
+ const relId = hyperlink.getRelationshipId();
570
+ if (relId) {
571
+ usedRelIds.add(relId);
572
+ }
573
+ }
574
+ }
575
+ }
576
+ }
577
+ }
578
+ };
579
+
580
+ // Helper to recursively scan any element type for hyperlinks
581
+ const scanElement = (
582
+ element: BodyElement | Paragraph | Table | StructuredDocumentTag
583
+ ): void => {
584
+ if (element instanceof Paragraph) {
585
+ // Scan paragraph content for hyperlinks
586
+ scanParagraph(element);
587
+ } else if (element instanceof Table) {
588
+ // Scan all cells in the table
589
+ for (let row = 0; row < element.getRowCount(); row++) {
590
+ for (let col = 0; col < element.getColumnCount(); col++) {
591
+ const cell = element.getCell(row, col);
592
+ if (cell) {
593
+ // Scan each paragraph in the cell
594
+ const paragraphs = cell.getParagraphs();
595
+ for (const para of paragraphs) {
596
+ scanParagraph(para);
597
+ }
598
+ // Scan raw nested content (nested tables, SDTs stored as raw XML)
599
+ // Extract any relationship IDs referenced in the raw XML to prevent orphan removal
600
+ for (const nested of cell.getRawNestedContent()) {
601
+ const rIdPattern = /r:id="(rId\d+)"/g;
602
+ let rIdMatch: RegExpExecArray | null;
603
+ while ((rIdMatch = rIdPattern.exec(nested.xml)) !== null) {
604
+ usedRelIds.add(rIdMatch[1]!);
605
+ }
606
+ }
607
+ }
608
+ }
609
+ }
610
+ } else if (element instanceof StructuredDocumentTag) {
611
+ // Recursively scan SDT content (can contain Paragraphs, Tables, or nested SDTs)
612
+ const content = element.getContent();
613
+ for (const item of content) {
614
+ scanElement(item); // Recursive call handles nested structures
615
+ }
616
+ }
617
+ // TableOfContentsElement is for programmatic TOCs - real TOCs come as SDTs
618
+ };
619
+
620
+ // Scan body elements (handles all nested structures)
621
+ for (const element of bodyElements) {
622
+ scanElement(element);
623
+ }
624
+
625
+ // Scan headers (including tables and SDTs in headers)
626
+ const headers = headerFooterManager.getAllHeaders();
627
+ for (const header of headers) {
628
+ for (const element of header.header.getElements()) {
629
+ scanElement(element);
630
+ }
631
+ }
632
+
633
+ // Scan footers (including tables and SDTs in footers)
634
+ const footers = headerFooterManager.getAllFooters();
635
+ for (const footer of footers) {
636
+ for (const element of footer.footer.getElements()) {
637
+ scanElement(element);
638
+ }
639
+ }
640
+
641
+ // Scan footnotes for hyperlink relationship IDs
642
+ if (footnoteManager) {
643
+ for (const footnote of footnoteManager.getAllFootnotes()) {
644
+ for (const para of footnote.getParagraphs()) {
645
+ scanParagraph(para);
646
+ }
647
+ }
648
+ }
649
+
650
+ // Scan endnotes for hyperlink relationship IDs
651
+ if (endnoteManager) {
652
+ for (const endnote of endnoteManager.getAllEndnotes()) {
653
+ for (const para of endnote.getParagraphs()) {
654
+ scanParagraph(para);
655
+ }
656
+ }
657
+ }
658
+
659
+ // Step 2: Remove ONLY orphaned relationships (not used by any hyperlink)
660
+ const allHyperlinkRels = relationshipManager.getRelationshipsByType(
661
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink'
662
+ );
663
+
664
+ for (const rel of allHyperlinkRels) {
665
+ if (!usedRelIds.has(rel.getId())) {
666
+ // This relationship is orphaned - remove it
667
+ relationshipManager.removeRelationship(rel.getId());
668
+ }
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Processes all hyperlinks in paragraphs and registers them with RelationshipManager
674
+ * Clears orphaned hyperlink relationships to prevent corruption while preserving valid ones
675
+ *
676
+ * **IMPORTANT:** This method recursively processes ALL element types including:
677
+ * - Top-level paragraphs
678
+ * - Tables (all cells)
679
+ * - StructuredDocumentTags (SDTs / content controls)
680
+ * - Headers and footers
681
+ *
682
+ * This ensures hyperlinks with URL+anchor combinations (like theSource links)
683
+ * that have their relationshipId cleared during parsing get new relationships
684
+ * registered before XML generation.
685
+ */
686
+ processHyperlinks(
687
+ bodyElements: BodyElement[],
688
+ headerFooterManager: HeaderFooterManager,
689
+ relationshipManager: RelationshipManager,
690
+ footnoteManager?: FootnoteManager,
691
+ endnoteManager?: EndnoteManager
692
+ ): void {
693
+ const logger = getLogger();
694
+ logger.info('Processing hyperlinks');
695
+
696
+ // Clear ORPHANED hyperlink relationships to prevent corruption
697
+ // This is critical when paragraphs are removed (e.g., via clearParagraphs())
698
+ // but preserves relationships for existing hyperlinks (round-trip integrity)
699
+ this.clearOrphanedHyperlinkRelationships(
700
+ bodyElements,
701
+ headerFooterManager,
702
+ relationshipManager,
703
+ footnoteManager,
704
+ endnoteManager
705
+ );
706
+
707
+ // Helper to recursively process any element type for hyperlinks
708
+ // Mirrors the pattern in clearOrphanedHyperlinkRelationships() for consistency
709
+ const processElement = (
710
+ element: BodyElement | Paragraph | Table | StructuredDocumentTag
711
+ ): void => {
712
+ if (element instanceof Paragraph) {
713
+ this.processHyperlinksInParagraph(element, relationshipManager);
714
+ } else if (element instanceof Table) {
715
+ // Process all cells in the table
716
+ for (let row = 0; row < element.getRowCount(); row++) {
717
+ for (let col = 0; col < element.getColumnCount(); col++) {
718
+ const cell = element.getCell(row, col);
719
+ if (cell) {
720
+ // Scan each paragraph in the cell
721
+ const paragraphs = cell.getParagraphs();
722
+ for (const para of paragraphs) {
723
+ this.processHyperlinksInParagraph(para, relationshipManager);
724
+ }
725
+ }
726
+ }
727
+ }
728
+ } else if (element instanceof StructuredDocumentTag) {
729
+ // Recursively process SDT content (can contain Paragraphs, Tables, or nested SDTs)
730
+ const content = element.getContent();
731
+ for (const item of content) {
732
+ processElement(item); // Recursive call handles nested structures
733
+ }
734
+ }
735
+ // TableOfContentsElement is for programmatic TOCs - real TOCs come as SDTs
736
+ };
737
+
738
+ // Process body elements (handles all nested structures)
739
+ for (const element of bodyElements) {
740
+ processElement(element);
741
+ }
742
+
743
+ // Process headers (including tables and SDTs in headers)
744
+ const headers = headerFooterManager.getAllHeaders();
745
+ for (const header of headers) {
746
+ for (const element of header.header.getElements()) {
747
+ processElement(element);
748
+ }
749
+ }
750
+
751
+ // Process footers (including tables and SDTs in footers)
752
+ const footers = headerFooterManager.getAllFooters();
753
+ for (const footer of footers) {
754
+ for (const element of footer.footer.getElements()) {
755
+ processElement(element);
756
+ }
757
+ }
758
+
759
+ // Process footnotes for hyperlinks that need relationship registration
760
+ if (footnoteManager) {
761
+ for (const footnote of footnoteManager.getAllFootnotes()) {
762
+ for (const para of footnote.getParagraphs()) {
763
+ this.processHyperlinksInParagraph(para, relationshipManager);
764
+ }
765
+ }
766
+ }
767
+
768
+ // Process endnotes for hyperlinks that need relationship registration
769
+ if (endnoteManager) {
770
+ for (const endnote of endnoteManager.getAllEndnotes()) {
771
+ for (const para of endnote.getParagraphs()) {
772
+ this.processHyperlinksInParagraph(para, relationshipManager);
773
+ }
774
+ }
775
+ }
776
+
777
+ logger.info('Hyperlinks processed');
778
+ }
779
+
780
+ /**
781
+ * Processes hyperlinks in a single paragraph
782
+ *
783
+ * **Validation:** Throws error if external hyperlink has no URL to prevent
784
+ * document corruption per ECMA-376 §17.16.22.
785
+ *
786
+ * Also processes hyperlinks inside Revision objects (tracked changes).
787
+ *
788
+ * @throws {Error} If external hyperlink has undefined/empty URL
789
+ */
790
+ private processHyperlinksInParagraph(
791
+ paragraph: Paragraph,
792
+ relationshipManager: RelationshipManager
793
+ ): void {
794
+ const content = paragraph.getContent();
795
+
796
+ for (const item of content) {
797
+ // Direct hyperlink in paragraph
798
+ if (item instanceof Hyperlink && item.isExternal() && !item.getRelationshipId()) {
799
+ this.registerHyperlinkRelationship(item, relationshipManager);
800
+ }
801
+
802
+ // Hyperlinks inside Revision objects (tracked changes)
803
+ if (item instanceof Revision) {
804
+ for (const revContent of item.getContent()) {
805
+ if (isHyperlinkContent(revContent)) {
806
+ const hyperlink = revContent;
807
+ if (hyperlink.isExternal() && !hyperlink.getRelationshipId()) {
808
+ this.registerHyperlinkRelationship(hyperlink, relationshipManager);
809
+ }
810
+ }
811
+ }
812
+ }
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Registers a hyperlink with the relationship manager
818
+ *
819
+ * @throws {Error} If hyperlink has no URL
820
+ */
821
+ private registerHyperlinkRelationship(
822
+ hyperlink: Hyperlink,
823
+ relationshipManager: RelationshipManager
824
+ ): void {
825
+ const url = hyperlink.getUrl();
826
+
827
+ // Validate that external hyperlink has a URL
828
+ // This prevents invalid document generation and fails early with clear error
829
+ if (!url) {
830
+ throw new Error(
831
+ `Invalid hyperlink in paragraph: External hyperlink "${hyperlink.getText()}" has no URL. ` +
832
+ `This would create a corrupted document per ECMA-376 §17.16.22. ` +
833
+ `Fix the hyperlink by providing a valid URL before saving.`
834
+ );
835
+ }
836
+
837
+ const relationship = relationshipManager.addHyperlink(url);
838
+ hyperlink.setRelationshipId(relationship.getId());
839
+ }
840
+
841
+ /**
842
+ * Generates word/fontTable.xml
843
+ * Required for DOCX compliance - defines fonts used in the document
844
+ */
845
+ generateFontTable(): string {
846
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
847
+ <w:fonts xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
848
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
849
+ <w:font w:name="Calibri">
850
+ <w:panose1 w:val="020F0502020204030204"/>
851
+ <w:charset w:val="00"/>
852
+ <w:family w:val="swiss"/>
853
+ <w:pitch w:val="variable"/>
854
+ <w:sig w:usb0="E10002FF" w:usb1="4000ACFF" w:usb2="00000009" w:usb3="00000000" w:csb0="0000019F" w:csb1="00000000"/>
855
+ </w:font>
856
+ <w:font w:name="Times New Roman">
857
+ <w:panose1 w:val="02020603050405020304"/>
858
+ <w:charset w:val="00"/>
859
+ <w:family w:val="roman"/>
860
+ <w:pitch w:val="variable"/>
861
+ <w:sig w:usb0="E0002AFF" w:usb1="C000785B" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/>
862
+ </w:font>
863
+ <w:font w:name="Arial">
864
+ <w:panose1 w:val="020B0604020202020204"/>
865
+ <w:charset w:val="00"/>
866
+ <w:family w:val="swiss"/>
867
+ <w:pitch w:val="variable"/>
868
+ <w:sig w:usb0="E0002AFF" w:usb1="C000247B" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/>
869
+ </w:font>
870
+ <w:font w:name="Courier New">
871
+ <w:panose1 w:val="02070309020205020404"/>
872
+ <w:charset w:val="00"/>
873
+ <w:family w:val="modern"/>
874
+ <w:pitch w:val="fixed"/>
875
+ <w:sig w:usb0="E0002AFF" w:usb1="C0007843" w:usb2="00000009" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/>
876
+ </w:font>
877
+ <w:font w:name="Calibri Light">
878
+ <w:panose1 w:val="020F0302020204030204"/>
879
+ <w:charset w:val="00"/>
880
+ <w:family w:val="swiss"/>
881
+ <w:pitch w:val="variable"/>
882
+ <w:sig w:usb0="E10002FF" w:usb1="4000ACFF" w:usb2="00000009" w:usb3="00000000" w:csb0="0000019F" w:csb1="00000000"/>
883
+ </w:font>
884
+ <w:font w:name="Georgia">
885
+ <w:panose1 w:val="02040502050204030303"/>
886
+ <w:charset w:val="00"/>
887
+ <w:family w:val="roman"/>
888
+ <w:pitch w:val="variable"/>
889
+ <w:sig w:usb0="E0002AFF" w:usb1="00000000" w:usb2="00000000" w:usb3="00000000" w:csb0="000001FF" w:csb1="00000000"/>
890
+ </w:font>
891
+ </w:fonts>`;
892
+ }
893
+
894
+ /**
895
+ * Generates word/webSettings.xml
896
+ * When called with no argument, produces the minimal static template.
897
+ * When called with settings, generates XML reflecting in-memory state.
898
+ */
899
+ generateWebSettings(settings?: {
900
+ optimizeForBrowser?: boolean;
901
+ allowPNG?: boolean;
902
+ relyOnVML?: boolean;
903
+ doNotRelyOnCSS?: boolean;
904
+ doNotSaveAsSingleFile?: boolean;
905
+ doNotOrganizeInFolder?: boolean;
906
+ doNotUseLongFileNames?: boolean;
907
+ pixelsPerInch?: number;
908
+ targetScreenSz?: string;
909
+ encoding?: string;
910
+ }): string {
911
+ if (!settings) {
912
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
913
+ <w:webSettings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
914
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
915
+ <w:optimizeForBrowser/>
916
+ <w:allowPNG/>
917
+ </w:webSettings>`;
918
+ }
919
+
920
+ let children = '';
921
+ if (settings.optimizeForBrowser) children += '\n <w:optimizeForBrowser/>';
922
+ if (settings.allowPNG) children += '\n <w:allowPNG/>';
923
+ if (settings.relyOnVML) children += '\n <w:relyOnVML/>';
924
+ if (settings.doNotRelyOnCSS) children += '\n <w:doNotRelyOnCSS/>';
925
+ if (settings.doNotSaveAsSingleFile) children += '\n <w:doNotSaveAsSingleFile/>';
926
+ if (settings.doNotOrganizeInFolder) children += '\n <w:doNotOrganizeInFolder/>';
927
+ if (settings.doNotUseLongFileNames) children += '\n <w:doNotUseLongFileNames/>';
928
+ if (settings.pixelsPerInch !== undefined)
929
+ children += `\n <w:pixelsPerInch w:val="${settings.pixelsPerInch}"/>`;
930
+ if (settings.targetScreenSz)
931
+ children += `\n <w:targetScreenSz w:val="${settings.targetScreenSz}"/>`;
932
+ if (settings.encoding) children += `\n <w:encoding w:val="${settings.encoding}"/>`;
933
+
934
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
935
+ <w:webSettings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
936
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${children}
937
+ </w:webSettings>`;
938
+ }
939
+
940
+ /**
941
+ * Generates word/settings.xml
942
+ * Required for DOCX compliance - defines document settings
943
+ * @param trackChangesSettings - Optional track changes settings
944
+ */
945
+ generateSettings(trackChangesSettings?: TrackChangesSettings): string {
946
+ let xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
947
+ <w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
948
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
949
+ <w:zoom w:percent="100"/>`;
950
+
951
+ // Track changes settings — ordered per CT_Settings:
952
+ // revisionView (#30) → trackRevisions (#31) → doNotTrackFormatting (#33)
953
+ if (trackChangesSettings?.revisionView) {
954
+ const view = trackChangesSettings.revisionView;
955
+ // Only emit revisionView if it differs from defaults (all true)
956
+ if (
957
+ !view.showInsertionsAndDeletions ||
958
+ !view.showFormatting ||
959
+ view.showInkAnnotations === false
960
+ ) {
961
+ xml += `\n <w:revisionView w:insDel="${view.showInsertionsAndDeletions ? '1' : '0'}" w:formatting="${view.showFormatting ? '1' : '0'}"`;
962
+ if (view.showInkAnnotations !== undefined) {
963
+ xml += ` w:inkAnnotations="${view.showInkAnnotations ? '1' : '0'}"`;
964
+ }
965
+ xml += '/>';
966
+ }
967
+ }
968
+
969
+ if (trackChangesSettings?.trackChangesEnabled) {
970
+ xml += '\n <w:trackRevisions/>';
971
+ }
972
+
973
+ if (trackChangesSettings?.trackFormatting !== undefined) {
974
+ if (!trackChangesSettings.trackFormatting) {
975
+ xml += '\n <w:doNotTrackFormatting/>';
976
+ }
977
+ // trackFormatting=true: emit nothing (default when w:trackRevisions is present)
978
+ }
979
+
980
+ // Document protection
981
+ if (trackChangesSettings?.documentProtection) {
982
+ const prot = trackChangesSettings.documentProtection;
983
+ xml += `\n <w:documentProtection w:edit="${prot.edit}" w:enforcement="${prot.enforcement ? '1' : '0'}"`;
984
+ if (prot.cryptProviderType) {
985
+ xml += ` w:cryptProviderType="${prot.cryptProviderType}"`;
986
+ }
987
+ if (prot.cryptAlgorithmClass) {
988
+ xml += ` w:cryptAlgorithmClass="${prot.cryptAlgorithmClass}"`;
989
+ }
990
+ if (prot.cryptAlgorithmType) {
991
+ xml += ` w:cryptAlgorithmType="${prot.cryptAlgorithmType}"`;
992
+ }
993
+ if (prot.cryptAlgorithmSid) {
994
+ xml += ` w:cryptAlgorithmSid="${prot.cryptAlgorithmSid}"`;
995
+ }
996
+ if (prot.cryptSpinCount) {
997
+ xml += ` w:cryptSpinCount="${prot.cryptSpinCount}"`;
998
+ }
999
+ if (prot.hash) {
1000
+ xml += ` w:hash="${prot.hash}"`;
1001
+ }
1002
+ if (prot.salt) {
1003
+ xml += ` w:salt="${prot.salt}"`;
1004
+ }
1005
+ xml += '/>';
1006
+ }
1007
+
1008
+ xml += `
1009
+ <w:defaultTabStop w:val="720"/>
1010
+ <w:characterSpacingControl w:val="doNotCompress"/>
1011
+ <w:updateFields w:val="true"/>`;
1012
+
1013
+ // RSIDs (Revision Save IDs)
1014
+ if (trackChangesSettings?.rsids && trackChangesSettings.rsids.length > 0) {
1015
+ xml += '\n <w:rsids>';
1016
+ if (trackChangesSettings.rsidRoot) {
1017
+ xml += `\n <w:rsidRoot w:val="${trackChangesSettings.rsidRoot}"/>`;
1018
+ }
1019
+ for (const rsid of trackChangesSettings.rsids) {
1020
+ xml += `\n <w:rsid w:val="${rsid}"/>`;
1021
+ }
1022
+ xml += '\n </w:rsids>';
1023
+ }
1024
+
1025
+ xml += `
1026
+ <w:compat>
1027
+ <w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/>
1028
+ <w:compatSetting w:name="overrideTableStyleFontSizeAndJustification" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
1029
+ <w:compatSetting w:name="enableOpenTypeFeatures" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
1030
+ <w:compatSetting w:name="doNotFlipMirrorIndents" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
1031
+ <w:compatSetting w:name="differentiateMultirowTableHeaders" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
1032
+ </w:compat>
1033
+ <w:themeFontLang w:val="en-US"/>
1034
+ <w:clrSchemeMapping w:bg1="light1" w:t1="dark1" w:bg2="light2" w:t2="dark2" w:accent1="accent1" w:accent2="accent2" w:accent3="accent3" w:accent4="accent4" w:accent5="accent5" w:accent6="accent6" w:hyperlink="hyperlink" w:followedHyperlink="followedHyperlink"/>
1035
+ </w:settings>`;
1036
+
1037
+ return xml;
1038
+ }
1039
+
1040
+ /**
1041
+ * Generates word/theme/theme1.xml
1042
+ * Required for DOCX compliance - defines color and font theme
1043
+ */
1044
+ generateTheme(): string {
1045
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1046
+ <a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
1047
+ <a:themeElements>
1048
+ <a:clrScheme name="Office">
1049
+ <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
1050
+ <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
1051
+ <a:dk2><a:srgbClr val="44546A"/></a:dk2>
1052
+ <a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
1053
+ <a:accent1><a:srgbClr val="5B9BD5"/></a:accent1>
1054
+ <a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
1055
+ <a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
1056
+ <a:accent4><a:srgbClr val="FFC000"/></a:accent4>
1057
+ <a:accent5><a:srgbClr val="4472C4"/></a:accent5>
1058
+ <a:accent6><a:srgbClr val="70AD47"/></a:accent6>
1059
+ <a:hlink><a:srgbClr val="0563C1"/></a:hlink>
1060
+ <a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
1061
+ </a:clrScheme>
1062
+ <a:fontScheme name="Office">
1063
+ <a:majorFont>
1064
+ <a:latin typeface="Calibri Light" panose="020F0302020204030204"/>
1065
+ <a:ea typeface=""/>
1066
+ <a:cs typeface=""/>
1067
+ </a:majorFont>
1068
+ <a:minorFont>
1069
+ <a:latin typeface="Calibri" panose="020F0502020204030204"/>
1070
+ <a:ea typeface=""/>
1071
+ <a:cs typeface=""/>
1072
+ </a:minorFont>
1073
+ </a:fontScheme>
1074
+ <a:fmtScheme name="Office">
1075
+ <a:fillStyleLst>
1076
+ <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
1077
+ <a:gradFill rotWithShape="1">
1078
+ <a:gsLst>
1079
+ <a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs>
1080
+ <a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs>
1081
+ <a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs>
1082
+ </a:gsLst>
1083
+ <a:lin ang="5400000" scaled="0"/>
1084
+ </a:gradFill>
1085
+ <a:gradFill rotWithShape="1">
1086
+ <a:gsLst>
1087
+ <a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs>
1088
+ <a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs>
1089
+ <a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs>
1090
+ </a:gsLst>
1091
+ <a:lin ang="5400000" scaled="0"/>
1092
+ </a:gradFill>
1093
+ </a:fillStyleLst>
1094
+ <a:lnStyleLst>
1095
+ <a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
1096
+ <a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
1097
+ <a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>
1098
+ </a:lnStyleLst>
1099
+ <a:effectStyleLst>
1100
+ <a:effectStyle><a:effectLst/></a:effectStyle>
1101
+ <a:effectStyle><a:effectLst/></a:effectStyle>
1102
+ <a:effectStyle>
1103
+ <a:effectLst>
1104
+ <a:outerShdw blurRad="57150" dist="19050" dir="5400000" algn="ctr" rotWithShape="0">
1105
+ <a:srgbClr val="000000"><a:alpha val="63000"/></a:srgbClr>
1106
+ </a:outerShdw>
1107
+ </a:effectLst>
1108
+ </a:effectStyle>
1109
+ </a:effectStyleLst>
1110
+ <a:bgFillStyleLst>
1111
+ <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
1112
+ <a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill>
1113
+ <a:gradFill rotWithShape="1">
1114
+ <a:gsLst>
1115
+ <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs>
1116
+ <a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs>
1117
+ <a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs>
1118
+ </a:gsLst>
1119
+ <a:lin ang="5400000" scaled="0"/>
1120
+ </a:gradFill>
1121
+ </a:bgFillStyleLst>
1122
+ </a:fmtScheme>
1123
+ </a:themeElements>
1124
+ <a:objectDefaults/>
1125
+ <a:extraClrSchemeLst/>
1126
+ <a:extLst>
1127
+ <a:ext uri="{05A4C25C-085E-4340-85A3-A5531E510DB2}">
1128
+ <thm15:themeFamily xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" name="Office Theme" id="{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}" vid="{4A3C46E8-61CC-4603-A589-7422A47A8E4A}"/>
1129
+ </a:ext>
1130
+ </a:extLst>
1131
+ </a:theme>`;
1132
+ }
1133
+ }