docxmlater 10.1.3 → 10.1.5

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 (371) 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 +50 -50
  6. package/dist/core/Document.d.ts.map +1 -1
  7. package/dist/core/Document.js +483 -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.map +1 -1
  146. package/dist/formatting/NumberingManager.js +9 -9
  147. package/dist/formatting/NumberingManager.js.map +1 -1
  148. package/dist/formatting/Style.d.ts +11 -11
  149. package/dist/formatting/Style.d.ts.map +1 -1
  150. package/dist/formatting/Style.js +219 -247
  151. package/dist/formatting/Style.js.map +1 -1
  152. package/dist/formatting/StylesManager.d.ts +2 -2
  153. package/dist/formatting/StylesManager.d.ts.map +1 -1
  154. package/dist/formatting/StylesManager.js +96 -102
  155. package/dist/formatting/StylesManager.js.map +1 -1
  156. package/dist/helpers/CleanupHelper.d.ts +1 -1
  157. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  158. package/dist/helpers/CleanupHelper.js +6 -6
  159. package/dist/helpers/CleanupHelper.js.map +1 -1
  160. package/dist/images/ImageOptimizer.js +7 -7
  161. package/dist/images/ImageOptimizer.js.map +1 -1
  162. package/dist/index.d.ts +9 -9
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js.map +1 -1
  165. package/dist/managers/DrawingManager.js.map +1 -1
  166. package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
  167. package/dist/tracking/DocumentTrackingContext.js +23 -7
  168. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  169. package/dist/tracking/TrackingContext.d.ts.map +1 -1
  170. package/dist/tracking/TrackingContext.js.map +1 -1
  171. package/dist/types/compatibility-types.js.map +1 -1
  172. package/dist/types/formatting.js.map +1 -1
  173. package/dist/types/list-types.d.ts +6 -6
  174. package/dist/types/list-types.js.map +1 -1
  175. package/dist/types/settings-types.js.map +1 -1
  176. package/dist/types/styleConfig.d.ts +2 -2
  177. package/dist/types/styleConfig.js.map +1 -1
  178. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  179. package/dist/utils/ChangelogGenerator.js +97 -101
  180. package/dist/utils/ChangelogGenerator.js.map +1 -1
  181. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  182. package/dist/utils/CompatibilityUpgrader.js +1 -1
  183. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  184. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  185. package/dist/utils/InMemoryRevisionAcceptor.js +1 -6
  186. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  187. package/dist/utils/MoveOperationHelper.d.ts.map +1 -1
  188. package/dist/utils/MoveOperationHelper.js +1 -1
  189. package/dist/utils/MoveOperationHelper.js.map +1 -1
  190. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  191. package/dist/utils/RevisionAwareProcessor.js +2 -4
  192. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  193. package/dist/utils/RevisionWalker.d.ts.map +1 -1
  194. package/dist/utils/RevisionWalker.js +4 -12
  195. package/dist/utils/RevisionWalker.js.map +1 -1
  196. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  197. package/dist/utils/SelectiveRevisionAcceptor.js +2 -6
  198. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  199. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  200. package/dist/utils/ShadingResolver.js +1 -1
  201. package/dist/utils/ShadingResolver.js.map +1 -1
  202. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  203. package/dist/utils/acceptRevisions.js +23 -12
  204. package/dist/utils/acceptRevisions.js.map +1 -1
  205. package/dist/utils/cnfStyleDecoder.d.ts +1 -1
  206. package/dist/utils/cnfStyleDecoder.d.ts.map +1 -1
  207. package/dist/utils/cnfStyleDecoder.js +40 -40
  208. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  209. package/dist/utils/corruptionDetection.d.ts.map +1 -1
  210. package/dist/utils/corruptionDetection.js.map +1 -1
  211. package/dist/utils/dateFormatting.js.map +1 -1
  212. package/dist/utils/deepClone.js +1 -1
  213. package/dist/utils/deepClone.js.map +1 -1
  214. package/dist/utils/diagnostics.d.ts.map +1 -1
  215. package/dist/utils/diagnostics.js +1 -1
  216. package/dist/utils/diagnostics.js.map +1 -1
  217. package/dist/utils/errorHandling.js.map +1 -1
  218. package/dist/utils/formatting.d.ts.map +1 -1
  219. package/dist/utils/formatting.js +10 -2
  220. package/dist/utils/formatting.js.map +1 -1
  221. package/dist/utils/list-detection.d.ts +2 -2
  222. package/dist/utils/list-detection.d.ts.map +1 -1
  223. package/dist/utils/list-detection.js +21 -23
  224. package/dist/utils/list-detection.js.map +1 -1
  225. package/dist/utils/logger.d.ts.map +1 -1
  226. package/dist/utils/logger.js +12 -7
  227. package/dist/utils/logger.js.map +1 -1
  228. package/dist/utils/parsingHelpers.js.map +1 -1
  229. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  230. package/dist/utils/stripTrackedChanges.js +3 -3
  231. package/dist/utils/stripTrackedChanges.js.map +1 -1
  232. package/dist/utils/textDiff.d.ts +1 -1
  233. package/dist/utils/textDiff.js +8 -8
  234. package/dist/utils/textDiff.js.map +1 -1
  235. package/dist/utils/units.js.map +1 -1
  236. package/dist/utils/validation.d.ts.map +1 -1
  237. package/dist/utils/validation.js +24 -7
  238. package/dist/utils/validation.js.map +1 -1
  239. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  240. package/dist/utils/xmlSanitization.js +3 -3
  241. package/dist/utils/xmlSanitization.js.map +1 -1
  242. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  243. package/dist/validation/RevisionAutoFixer.js +5 -5
  244. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  245. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  246. package/dist/validation/RevisionValidator.js +7 -9
  247. package/dist/validation/RevisionValidator.js.map +1 -1
  248. package/dist/validation/ValidationRules.js +3 -3
  249. package/dist/validation/ValidationRules.js.map +1 -1
  250. package/dist/validation/index.js.map +1 -1
  251. package/dist/xml/XMLBuilder.d.ts +1 -1
  252. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  253. package/dist/xml/XMLBuilder.js +98 -100
  254. package/dist/xml/XMLBuilder.js.map +1 -1
  255. package/dist/xml/XMLParser.d.ts.map +1 -1
  256. package/dist/xml/XMLParser.js +61 -66
  257. package/dist/xml/XMLParser.js.map +1 -1
  258. package/dist/zip/ZipHandler.d.ts.map +1 -1
  259. package/dist/zip/ZipHandler.js.map +1 -1
  260. package/dist/zip/ZipReader.d.ts.map +1 -1
  261. package/dist/zip/ZipReader.js +1 -3
  262. package/dist/zip/ZipReader.js.map +1 -1
  263. package/dist/zip/ZipWriter.d.ts +1 -1
  264. package/dist/zip/ZipWriter.d.ts.map +1 -1
  265. package/dist/zip/ZipWriter.js +28 -36
  266. package/dist/zip/ZipWriter.js.map +1 -1
  267. package/dist/zip/types.js +1 -1
  268. package/dist/zip/types.js.map +1 -1
  269. package/package.json +92 -92
  270. package/src/__tests__/helper-methods.test.ts +512 -512
  271. package/src/constants/legacyCompatFlags.ts +138 -138
  272. package/src/constants/limits.ts +50 -50
  273. package/src/core/Document.ts +985 -1145
  274. package/src/core/DocumentContent.ts +461 -467
  275. package/src/core/DocumentGenerator.ts +1133 -1104
  276. package/src/core/DocumentIdManager.ts +158 -158
  277. package/src/core/DocumentParser.ts +2347 -2716
  278. package/src/core/DocumentValidator.ts +363 -372
  279. package/src/core/Relationship.ts +367 -367
  280. package/src/core/RelationshipManager.ts +429 -428
  281. package/src/elements/AlternateContent.ts +42 -42
  282. package/src/elements/Bookmark.ts +212 -210
  283. package/src/elements/BookmarkManager.ts +247 -250
  284. package/src/elements/Comment.ts +356 -359
  285. package/src/elements/CommentManager.ts +499 -502
  286. package/src/elements/CommonTypes.ts +524 -549
  287. package/src/elements/CustomXml.ts +36 -36
  288. package/src/elements/Endnote.ts +221 -217
  289. package/src/elements/EndnoteManager.ts +246 -249
  290. package/src/elements/Field.ts +1292 -1233
  291. package/src/elements/FieldHelpers.ts +329 -333
  292. package/src/elements/FontManager.ts +336 -339
  293. package/src/elements/Footer.ts +269 -269
  294. package/src/elements/Footnote.ts +221 -217
  295. package/src/elements/FootnoteManager.ts +246 -249
  296. package/src/elements/Header.ts +269 -269
  297. package/src/elements/HeaderFooterManager.ts +219 -219
  298. package/src/elements/Hyperlink.ts +1288 -1193
  299. package/src/elements/Image.ts +1982 -1756
  300. package/src/elements/ImageManager.ts +437 -432
  301. package/src/elements/ImageRun.ts +59 -59
  302. package/src/elements/MathElement.ts +65 -65
  303. package/src/elements/Paragraph.ts +4347 -4287
  304. package/src/elements/PreservedElement.ts +53 -53
  305. package/src/elements/PropertyChangeTypes.ts +458 -442
  306. package/src/elements/RangeMarker.ts +382 -400
  307. package/src/elements/Revision.ts +1198 -1217
  308. package/src/elements/RevisionContent.ts +73 -73
  309. package/src/elements/RevisionManager.ts +1070 -1070
  310. package/src/elements/Run.ts +3103 -3073
  311. package/src/elements/Section.ts +1521 -1421
  312. package/src/elements/Shape.ts +884 -873
  313. package/src/elements/StructuredDocumentTag.ts +1176 -1207
  314. package/src/elements/Table.ts +2468 -2524
  315. package/src/elements/TableCell.ts +1617 -1621
  316. package/src/elements/TableGridChange.ts +149 -151
  317. package/src/elements/TableOfContents.ts +701 -691
  318. package/src/elements/TableOfContentsElement.ts +89 -89
  319. package/src/elements/TableRow.ts +960 -929
  320. package/src/elements/TextBox.ts +766 -768
  321. package/src/formatting/AbstractNumbering.ts +580 -579
  322. package/src/formatting/NumberingInstance.ts +295 -299
  323. package/src/formatting/NumberingLevel.ts +981 -1040
  324. package/src/formatting/NumberingManager.ts +833 -827
  325. package/src/formatting/Style.ts +1785 -1879
  326. package/src/formatting/StylesManager.ts +1090 -1130
  327. package/src/helpers/CleanupHelper.ts +524 -524
  328. package/src/images/ImageOptimizer.ts +274 -274
  329. package/src/index.ts +559 -554
  330. package/src/managers/DrawingManager.ts +319 -319
  331. package/src/tracking/DocumentTrackingContext.ts +687 -674
  332. package/src/tracking/TrackingContext.ts +175 -173
  333. package/src/types/compatibility-types.ts +49 -49
  334. package/src/types/formatting.ts +210 -210
  335. package/src/types/list-types.ts +14 -14
  336. package/src/types/settings-types.ts +59 -59
  337. package/src/types/styleConfig.ts +189 -189
  338. package/src/utils/ChangelogGenerator.ts +1583 -1581
  339. package/src/utils/CompatibilityUpgrader.ts +235 -237
  340. package/src/utils/InMemoryRevisionAcceptor.ts +691 -696
  341. package/src/utils/MoveOperationHelper.ts +233 -238
  342. package/src/utils/RevisionAwareProcessor.ts +518 -526
  343. package/src/utils/RevisionWalker.ts +427 -457
  344. package/src/utils/SelectiveRevisionAcceptor.ts +662 -683
  345. package/src/utils/ShadingResolver.ts +105 -107
  346. package/src/utils/acceptRevisions.ts +723 -714
  347. package/src/utils/cnfStyleDecoder.ts +212 -217
  348. package/src/utils/corruptionDetection.ts +346 -345
  349. package/src/utils/dateFormatting.ts +20 -20
  350. package/src/utils/deepClone.ts +77 -78
  351. package/src/utils/diagnostics.ts +125 -129
  352. package/src/utils/errorHandling.ts +80 -80
  353. package/src/utils/formatting.ts +220 -213
  354. package/src/utils/list-detection.ts +32 -42
  355. package/src/utils/logger.ts +412 -404
  356. package/src/utils/parsingHelpers.ts +190 -190
  357. package/src/utils/stripTrackedChanges.ts +356 -353
  358. package/src/utils/textDiff.ts +100 -100
  359. package/src/utils/units.ts +421 -421
  360. package/src/utils/validation.ts +553 -542
  361. package/src/utils/xmlSanitization.ts +179 -182
  362. package/src/validation/RevisionAutoFixer.ts +541 -542
  363. package/src/validation/RevisionValidator.ts +470 -460
  364. package/src/validation/ValidationRules.ts +338 -338
  365. package/src/validation/index.ts +30 -30
  366. package/src/xml/XMLBuilder.ts +857 -871
  367. package/src/xml/XMLParser.ts +877 -919
  368. package/src/zip/ZipHandler.ts +629 -637
  369. package/src/zip/ZipReader.ts +295 -299
  370. package/src/zip/ZipWriter.ts +374 -390
  371. package/src/zip/types.ts +116 -116
@@ -1,871 +1,857 @@
1
- /**
2
- * XMLBuilder - Utility for building XML content
3
- * Provides a simple fluent API for generating WordprocessingML XML
4
- */
5
-
6
- import { removeInvalidXmlChars } from "../utils/xmlSanitization";
7
- import type { ShadingConfig } from "../elements/CommonTypes";
8
- import { buildShadingAttributes } from "../elements/CommonTypes";
9
-
10
- /** Represents a parsed XML object from XMLParser.parseToObject() */
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- parsed XML has dynamic keys and recursive structure
12
- type ParsedXmlObject = Record<string, any>;
13
-
14
- /**
15
- * Represents an XML element with attributes and children
16
- */
17
- export interface XMLElement {
18
- name: string;
19
- attributes?: Record<string, string | number | boolean | undefined>;
20
- children?: (XMLElement | string)[];
21
- selfClosing?: boolean;
22
- /** Raw XML content to include without escaping (used for VML passthrough) */
23
- rawXml?: string;
24
- }
25
-
26
- /**
27
- * XML Builder for creating WordprocessingML XML
28
- */
29
- export class XMLBuilder {
30
- private elements: (XMLElement | string)[] = [];
31
-
32
- /**
33
- * Elements that must NEVER be self-closing in Word XML per ECMA-376.
34
- * Self-closing these elements causes Word to not parse correctly or lose content.
35
- */
36
- private static readonly CANNOT_SELF_CLOSE = [
37
- "w:t",
38
- "w:r",
39
- "w:p",
40
- "w:tbl",
41
- "w:tr",
42
- "w:tc",
43
- "w:body",
44
- "w:document",
45
- "w:hyperlink",
46
- "w:sdt",
47
- "w:sdtContent",
48
- "w:sdtPr",
49
- "w:pPr",
50
- "w:rPr",
51
- "w:sectPr",
52
- "w:del", // Deletion revisions - container element, must have closing tag
53
- "w:ins", // Insertion revisions - container element, must have closing tag
54
- "w:moveFrom", // Move source markers - container element
55
- "w:moveTo", // Move destination markers - container element
56
- // Note: w:bookmarkStart and w:bookmarkEnd MUST be self-closing per ECMA-376
57
- ];
58
-
59
- /**
60
- * Adds an element to the builder
61
- * @param name - Element name (with namespace prefix if needed)
62
- * @param attributes - Element attributes
63
- * @param children - Child elements or text
64
- * @returns This builder for chaining
65
- */
66
- element(
67
- name: string,
68
- attributes?: Record<string, string | number | boolean | undefined>,
69
- children?: (XMLElement | string)[]
70
- ): XMLBuilder {
71
- this.elements.push({
72
- name,
73
- attributes,
74
- children,
75
- });
76
- return this;
77
- }
78
-
79
- /**
80
- * Adds a self-closing element
81
- * @param name - Element name
82
- * @param attributes - Element attributes
83
- * @returns This builder for chaining
84
- * @throws {Error} If attempting to create self-closing w:t element (not allowed per ECMA-376)
85
- */
86
- selfClosingElement(
87
- name: string,
88
- attributes?: Record<string, string | number | boolean | undefined>
89
- ): XMLBuilder {
90
- // Validation: Text elements (<w:t>) cannot be self-closing per ECMA-376
91
- // Self-closing <w:t/> elements cause Word to fail opening the document
92
- if (name === 'w:t' || name === 't') {
93
- throw new Error(
94
- 'Text elements (<w:t>) cannot be self-closing per ECMA-376. ' +
95
- 'Use element() with empty text content instead: XMLBuilder.w("t", attrs, [""])'
96
- );
97
- }
98
-
99
- this.elements.push({
100
- name,
101
- attributes,
102
- selfClosing: true,
103
- });
104
- return this;
105
- }
106
-
107
- /**
108
- * Adds text content
109
- * @param text - Text to add
110
- * @returns This builder for chaining
111
- */
112
- text(text: string): XMLBuilder {
113
- this.elements.push(text);
114
- return this;
115
- }
116
-
117
- /**
118
- * Builds the XML string
119
- * @param includeDeclaration - Whether to include XML declaration
120
- * @returns Generated XML string
121
- */
122
- build(includeDeclaration = false): string {
123
- let xml = "";
124
-
125
- if (includeDeclaration) {
126
- xml += '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
127
- }
128
-
129
- xml += this.elementsToString(this.elements);
130
- return xml;
131
- }
132
-
133
- /**
134
- * Converts elements to XML string
135
- */
136
- private elementsToString(elements: (XMLElement | string)[]): string {
137
- let xml = "";
138
-
139
- for (const element of elements) {
140
- if (typeof element === "string") {
141
- xml += this.escapeXml(element);
142
- } else {
143
- xml += this.elementToString(element);
144
- }
145
- }
146
-
147
- return xml;
148
- }
149
-
150
- /**
151
- * Converts a single element to XML string
152
- */
153
- private elementToString(element: XMLElement): string {
154
- // Special case: raw XML passthrough (no wrapper element)
155
- // Used for VML and other legacy content that must be preserved exactly
156
- if (element.name === "__rawXml" && element.rawXml) {
157
- return element.rawXml;
158
- }
159
-
160
- let xml = `<${element.name}`;
161
-
162
- // Add attributes
163
- if (element.attributes) {
164
- for (const [key, value] of Object.entries(element.attributes)) {
165
- if (value !== undefined && value !== null && value !== false) {
166
- // Handle boolean attributes
167
- const attrValue = value === true ? key : String(value);
168
- // Use escapeXmlAttribute for attribute values (Issue #8)
169
- xml += ` ${key}="${XMLBuilder.escapeXmlAttribute(attrValue)}"`;
170
- }
171
- }
172
- }
173
-
174
- // Self-closing element validation
175
- if (element.selfClosing) {
176
- if (XMLBuilder.CANNOT_SELF_CLOSE.includes(element.name)) {
177
- // Instead of throwing, force open/close tags for safety
178
- xml += "></" + element.name + ">";
179
- return xml;
180
- }
181
- xml += "/>";
182
- return xml;
183
- }
184
-
185
- xml += ">";
186
-
187
- // Add raw XML content if present (for VML passthrough)
188
- if (element.rawXml) {
189
- xml += element.rawXml;
190
- }
191
-
192
- // Add children
193
- if (element.children && element.children.length > 0) {
194
- xml += this.elementsToString(element.children);
195
- }
196
-
197
- xml += `</${element.name}>`;
198
- return xml;
199
- }
200
-
201
- /**
202
- * Escapes special XML characters for text content
203
- * (Issue #8 fix: Use escapeXmlText for element text, escapeXmlAttribute called directly for attrs)
204
- */
205
- private escapeXml(text: string): string {
206
- // This method is now only used for text content in elementsToString()
207
- // Attributes call escapeXmlAttribute() directly in elementToString()
208
- // Text content should NOT escape quotes (only & < >)
209
- return XMLBuilder.escapeXmlText(text);
210
- }
211
-
212
- /**
213
- * Escapes XML text content (element text nodes)
214
- * Removes invalid XML 1.0 control characters and escapes: & < >
215
- *
216
- * Per XML 1.0 spec, control chars 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F are invalid.
217
- * Tab (0x09), newline (0x0A), and CR (0x0D) are preserved.
218
- *
219
- * @param text Text to escape
220
- * @returns Escaped text safe for XML content
221
- */
222
- static escapeXmlText(text: string): string {
223
- return removeInvalidXmlChars(text)
224
- .replace(/&/g, "&amp;")
225
- .replace(/</g, "&lt;")
226
- .replace(/>/g, "&gt;");
227
- }
228
-
229
- /**
230
- * Escapes XML attribute values
231
- * Removes invalid XML 1.0 control characters and escapes: & < > " '
232
- *
233
- * Per XML 1.0 spec, control chars 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F are invalid.
234
- * Tab (0x09), newline (0x0A), and CR (0x0D) are preserved.
235
- *
236
- * @param value Attribute value to escape
237
- * @returns Escaped value safe for XML attributes
238
- */
239
- static escapeXmlAttribute(value: string): string {
240
- return removeInvalidXmlChars(value)
241
- .replace(/&/g, "&amp;")
242
- .replace(/</g, "&lt;")
243
- .replace(/>/g, "&gt;")
244
- .replace(/"/g, "&quot;")
245
- .replace(/'/g, "&apos;");
246
- }
247
-
248
- /**
249
- * Unescapes XML entities back to original characters
250
- * @param text Text with XML entities
251
- * @returns Unescaped text
252
- */
253
- static unescapeXml(text: string): string {
254
- return text
255
- .replace(/&lt;/g, "<")
256
- .replace(/&gt;/g, ">")
257
- .replace(/&quot;/g, '"')
258
- .replace(/&apos;/g, "'")
259
- .replace(/&amp;/g, "&"); // Must be last to avoid double-unescaping
260
- }
261
-
262
- /**
263
- * Sanitizes and escapes XML content for safe inclusion in XML documents
264
- * Removes control characters, null bytes, and escapes special XML characters
265
- * Use this for user-provided content that may contain unsafe characters
266
- *
267
- * Per XML 1.0 spec, control chars 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F are invalid.
268
- * Tab (0x09), newline (0x0A), and CR (0x0D) are preserved.
269
- *
270
- * @param text Text to sanitize and escape
271
- * @returns Sanitized text safe for XML content
272
- *
273
- * **Issue #11 fix:** Prevents malformed XML from CDATA markers, control chars, etc.
274
- */
275
- static sanitizeXmlContent(text: string): string {
276
- return (
277
- removeInvalidXmlChars(text)
278
- // Escape CDATA end marker to prevent CDATA injection
279
- .replace(/\]\]>/g, "]]&gt;")
280
- // Standard XML escaping (& must be first to avoid double-escaping)
281
- .replace(/&/g, "&amp;")
282
- .replace(/</g, "&lt;")
283
- .replace(/>/g, "&gt;")
284
- );
285
- }
286
-
287
- /**
288
- * Creates a WordprocessingML namespace attribute object
289
- */
290
- static createNamespaces(): Record<string, string> {
291
- return {
292
- "xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
293
- "xmlns:r":
294
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
295
- "xmlns:wp":
296
- "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
297
- "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main",
298
- "xmlns:pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
299
- "xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml",
300
- "xmlns:wpc":
301
- "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
302
- "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
303
- "xmlns:o": "urn:schemas-microsoft-com:office:office",
304
- "xmlns:v": "urn:schemas-microsoft-com:vml",
305
- "xmlns:wp14":
306
- "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
307
- "xmlns:w10": "urn:schemas-microsoft-com:office:word",
308
- "xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml",
309
- "xmlns:wpg":
310
- "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
311
- "xmlns:wpi":
312
- "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
313
- "xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml",
314
- "xmlns:wps":
315
- "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
316
- "xmlns:asvg":
317
- "http://schemas.microsoft.com/office/drawing/2016/SVG/main",
318
- };
319
- }
320
-
321
- /**
322
- * Converts a single XMLElement to its XML string representation
323
- * Useful for merging elements into existing XML
324
- *
325
- * @param element - The XMLElement to convert
326
- * @returns XML string representation of the element
327
- */
328
- static elementToString(element: XMLElement): string {
329
- const builder = new XMLBuilder();
330
- // Use the private elementToString via the elements array + build
331
- (builder as any).elements.push(element);
332
- return builder.build();
333
- }
334
-
335
- /**
336
- * Helper method to create a WordprocessingML element
337
- * @param name - Element name (without 'w:' prefix)
338
- * @param attributes - Element attributes
339
- * @param children - Child elements
340
- * @returns XMLElement
341
- */
342
- static w(
343
- name: string,
344
- attributes?: Record<string, string | number | boolean | undefined>,
345
- children?: (XMLElement | string)[]
346
- ): XMLElement {
347
- return {
348
- name: `w:${name}`,
349
- attributes,
350
- children,
351
- };
352
- }
353
-
354
- /**
355
- * Helper method to create a self-closing WordprocessingML element
356
- * @param name - Element name (without 'w:' prefix)
357
- * @param attributes - Element attributes
358
- * @returns XMLElement
359
- */
360
- static wSelf(
361
- name: string,
362
- attributes?: Record<string, string | number | boolean | undefined>
363
- ): XMLElement {
364
- return {
365
- name: `w:${name}`,
366
- attributes,
367
- selfClosing: true,
368
- };
369
- }
370
-
371
- /**
372
- * Helper method to create a w14 element (Word 2010+ features)
373
- * @param name - Element name (without 'w14:' prefix)
374
- * @param attributes - Element attributes
375
- * @param children - Child elements
376
- * @returns XMLElement
377
- */
378
- static w14(
379
- name: string,
380
- attributes?: Record<string, string | number | boolean | undefined>,
381
- children?: (XMLElement | string)[]
382
- ): XMLElement {
383
- return {
384
- name: `w14:${name}`,
385
- attributes,
386
- children,
387
- };
388
- }
389
-
390
- /**
391
- * Helper method to create a self-closing w14 element
392
- * @param name - Element name (without 'w14:' prefix)
393
- * @param attributes - Element attributes
394
- * @returns XMLElement
395
- */
396
- static w14Self(
397
- name: string,
398
- attributes?: Record<string, string | number | boolean | undefined>
399
- ): XMLElement {
400
- return {
401
- name: `w14:${name}`,
402
- attributes,
403
- selfClosing: true,
404
- };
405
- }
406
-
407
- /**
408
- * Helper method to create a DrawingML element (a: namespace)
409
- * @param name - Element name (without 'a:' prefix)
410
- * @param attributes - Element attributes
411
- * @param children - Child elements
412
- * @returns XMLElement
413
- */
414
- static a(
415
- name: string,
416
- attributes?: Record<string, string | number | boolean | undefined>,
417
- children?: (XMLElement | string)[]
418
- ): XMLElement {
419
- return {
420
- name: `a:${name}`,
421
- attributes,
422
- children,
423
- };
424
- }
425
-
426
- /**
427
- * Helper method to create a self-closing DrawingML element
428
- * @param name - Element name (without 'a:' prefix)
429
- * @param attributes - Element attributes
430
- * @returns XMLElement
431
- */
432
- static aSelf(
433
- name: string,
434
- attributes?: Record<string, string | number | boolean | undefined>
435
- ): XMLElement {
436
- return {
437
- name: `a:${name}`,
438
- attributes,
439
- selfClosing: true,
440
- };
441
- }
442
-
443
- /**
444
- * Helper method to create a Picture element (pic: namespace)
445
- * @param name - Element name (without 'pic:' prefix)
446
- * @param attributes - Element attributes
447
- * @param children - Child elements
448
- * @returns XMLElement
449
- */
450
- static pic(
451
- name: string,
452
- attributes?: Record<string, string | number | boolean | undefined>,
453
- children?: (XMLElement | string)[]
454
- ): XMLElement {
455
- return {
456
- name: `pic:${name}`,
457
- attributes,
458
- children,
459
- };
460
- }
461
-
462
- /**
463
- * Helper method to create a self-closing Picture element
464
- * @param name - Element name (without 'pic:' prefix)
465
- * @param attributes - Element attributes
466
- * @returns XMLElement
467
- */
468
- static picSelf(
469
- name: string,
470
- attributes?: Record<string, string | number | boolean | undefined>
471
- ): XMLElement {
472
- return {
473
- name: `pic:${name}`,
474
- attributes,
475
- selfClosing: true,
476
- };
477
- }
478
-
479
- /**
480
- * Helper method to create a Wordprocessing Drawing element (wp: namespace)
481
- * @param name - Element name (without 'wp:' prefix)
482
- * @param attributes - Element attributes
483
- * @param children - Child elements
484
- * @returns XMLElement
485
- */
486
- static wp(
487
- name: string,
488
- attributes?: Record<string, string | number | boolean | undefined>,
489
- children?: (XMLElement | string)[]
490
- ): XMLElement {
491
- return {
492
- name: `wp:${name}`,
493
- attributes,
494
- children,
495
- };
496
- }
497
-
498
- /**
499
- * Helper method to create a self-closing Wordprocessing Drawing element
500
- * @param name - Element name (without 'wp:' prefix)
501
- * @param attributes - Element attributes
502
- * @returns XMLElement
503
- */
504
- static wpSelf(
505
- name: string,
506
- attributes?: Record<string, string | number | boolean | undefined>
507
- ): XMLElement {
508
- return {
509
- name: `wp:${name}`,
510
- attributes,
511
- selfClosing: true,
512
- };
513
- }
514
-
515
- /**
516
- * Helper to create cx/cy extent attributes (for a:ext, wp:extent, etc.)
517
- * @param name - Element name (e.g., 'ext')
518
- * @param cx - Width in EMUs
519
- * @param cy - Height in EMUs
520
- * @returns Self-closing XMLElement
521
- */
522
- static cxCy(
523
- name: string,
524
- cx: number,
525
- cy: number
526
- ): XMLElement {
527
- return {
528
- name,
529
- attributes: { cx, cy },
530
- selfClosing: true
531
- };
532
- }
533
-
534
- /**
535
- * Creates an SDT (Structured Document Tag) wrapper for content
536
- * @param content - Content to wrap (paragraphs, tables, etc.)
537
- * @param options - SDT options
538
- * @returns XMLElement representing the SDT wrapper
539
- */
540
- static createSDT(
541
- content: XMLElement[],
542
- options?: {
543
- id?: number;
544
- docPartGallery?: string;
545
- docPartUnique?: boolean;
546
- }
547
- ): XMLElement {
548
- const sdtId = options?.id ?? Math.floor(Math.random() * 2000000000) - 1000000000;
549
-
550
- // Build SDT properties
551
- const sdtPrChildren: XMLElement[] = [
552
- XMLBuilder.wSelf('id', { 'w:val': sdtId })
553
- ];
554
-
555
- // Add docPartObj if docPartGallery is specified
556
- if (options?.docPartGallery) {
557
- sdtPrChildren.push(
558
- XMLBuilder.w('docPartObj', undefined, [
559
- XMLBuilder.wSelf('docPartGallery', { 'w:val': options.docPartGallery }),
560
- XMLBuilder.wSelf('docPartUnique', {
561
- 'w:val': options?.docPartUnique !== false ? '1' : '0'
562
- })
563
- ])
564
- );
565
- }
566
-
567
- // Create complete SDT structure
568
- return XMLBuilder.w('sdt', undefined, [
569
- XMLBuilder.w('sdtPr', undefined, sdtPrChildren),
570
- XMLBuilder.w('sdtContent', undefined, content)
571
- ]);
572
- }
573
-
574
- /**
575
- * Creates a complete WordprocessingML document structure
576
- * @param bodyContent - Content for the document body
577
- * @returns XML string for word/document.xml
578
- */
579
- static createDocument(
580
- bodyContent: XMLElement[],
581
- namespaces: Record<string, string> = {},
582
- preBodyContent?: XMLElement[]
583
- ): string {
584
- const builder = new XMLBuilder();
585
-
586
- // Preserve document's original namespace order, then fill in framework defaults
587
- const allNamespaces: Record<string, string> = {};
588
- for (const [key, value] of Object.entries(namespaces)) {
589
- allNamespaces[key] = value;
590
- }
591
- for (const [key, value] of Object.entries(XMLBuilder.createNamespaces())) {
592
- if (!(key in allNamespaces)) {
593
- allNamespaces[key] = value;
594
- }
595
- }
596
-
597
- // Ensure mc:Ignorable is present when extended namespaces are declared.
598
- // Per ECMA-376, mc:Ignorable tells Word which namespace prefixes can be
599
- // safely ignored if the processor doesn't support them. Without it,
600
- // attributes like w14:paraId in raw XML passthrough zones cause corruption.
601
- if (!allNamespaces["mc:Ignorable"]) {
602
- const ignorable: string[] = [];
603
- if (allNamespaces["xmlns:w14"]) ignorable.push("w14");
604
- if (allNamespaces["xmlns:w15"]) ignorable.push("w15");
605
- if (allNamespaces["xmlns:wp14"]) ignorable.push("wp14");
606
- if (allNamespaces["xmlns:w16se"]) ignorable.push("w16se");
607
- if (allNamespaces["xmlns:w16cid"]) ignorable.push("w16cid");
608
- if (allNamespaces["xmlns:w16"]) ignorable.push("w16");
609
- if (allNamespaces["xmlns:w16cex"]) ignorable.push("w16cex");
610
- if (allNamespaces["xmlns:w16sdtdh"]) ignorable.push("w16sdtdh");
611
- if (allNamespaces["xmlns:w16sdtfl"]) ignorable.push("w16sdtfl");
612
- if (allNamespaces["xmlns:w16du"]) ignorable.push("w16du");
613
- if (allNamespaces["xmlns:asvg"]) ignorable.push("asvg");
614
- if (ignorable.length > 0) {
615
- allNamespaces["mc:Ignorable"] = ignorable.join(" ");
616
- }
617
- } else {
618
- // mc:Ignorable was loaded from the original document — ensure every
619
- // prefix referenced in mc:Ignorable has a matching xmlns declaration.
620
- // Without this, the validator rejects prefixes that appear in
621
- // mc:Ignorable but lack namespace declarations in the root element.
622
- const defaults = XMLBuilder.createNamespaces();
623
- const prefixes = allNamespaces["mc:Ignorable"].split(/\s+/);
624
- for (const prefix of prefixes) {
625
- const nsKey = `xmlns:${prefix}`;
626
- if (!allNamespaces[nsKey] && defaults[nsKey]) {
627
- allNamespaces[nsKey] = defaults[nsKey];
628
- }
629
- }
630
- }
631
-
632
- const documentChildren: XMLElement[] = [];
633
- if (preBodyContent) {
634
- documentChildren.push(...preBodyContent);
635
- }
636
- documentChildren.push(XMLBuilder.w("body", undefined, bodyContent));
637
- builder.element("w:document", allNamespaces, documentChildren);
638
-
639
- return builder.build(true);
640
- }
641
-
642
- /**
643
- * Builds an XML string from a JavaScript object.
644
- * This is the reverse of XMLParser.parseToObject
645
- */
646
- static buildObject(obj: ParsedXmlObject, rootName: string): string {
647
- const builder = new XMLBuilder();
648
- const element = XMLBuilder.objectToElement(obj, rootName);
649
- if (element) {
650
- if (typeof element === "string") {
651
- builder.text(element);
652
- } else {
653
- builder.elements.push(element);
654
- }
655
- }
656
- return builder.build();
657
- }
658
-
659
- /**
660
- * Converts a JavaScript object to an XMLElement.
661
- * @private
662
- */
663
- private static objectToElement(
664
- obj: ParsedXmlObject | string | number | boolean | null | undefined,
665
- name: string
666
- ): XMLElement | string | null {
667
- if (obj === null || obj === undefined) {
668
- return null;
669
- }
670
-
671
- if (typeof obj !== "object" || obj === null) {
672
- return String(obj);
673
- }
674
-
675
- const attributes: Record<string, string | number | boolean> = {};
676
- const children: (XMLElement | string)[] = [];
677
-
678
- if (obj["#text"] && Object.keys(obj).length === 1) {
679
- return String(obj["#text"]);
680
- }
681
-
682
- for (const key in obj) {
683
- if (key.startsWith("@_")) {
684
- const attrName = key.substring(2);
685
- // Validate attribute name is not empty after prefix removal
686
- if (attrName.length > 0) {
687
- attributes[attrName] = obj[key];
688
- }
689
- } else if (key === "#text") {
690
- children.push(String(obj[key]));
691
- } else {
692
- const childObj = obj[key];
693
- if (Array.isArray(childObj)) {
694
- childObj.forEach((item) => {
695
- const childElement = XMLBuilder.objectToElement(item, key);
696
- if (childElement) {
697
- children.push(childElement);
698
- }
699
- });
700
- } else {
701
- const childElement = XMLBuilder.objectToElement(childObj, key);
702
- if (childElement) {
703
- children.push(childElement);
704
- }
705
- }
706
- }
707
- }
708
-
709
- const element: XMLElement = {
710
- name,
711
- attributes,
712
- children: children.length > 0 ? children : undefined,
713
- };
714
-
715
- if (!element.children || element.children.length === 0) {
716
- if (!XMLBuilder.CANNOT_SELF_CLOSE.includes(name)) {
717
- element.selfClosing = true;
718
- }
719
- }
720
-
721
- return element;
722
- }
723
-
724
- /**
725
- * Helper method to build attributes object, filtering out undefined/null values
726
- * This simplifies the common pattern of conditionally adding attributes
727
- *
728
- * @param mapping - Map of attribute names to values
729
- * @returns Filtered attributes object with only defined values
730
- *
731
- * @example
732
- * ```typescript
733
- * const attrs = XMLBuilder.buildAttributes({
734
- * 'w:before': spacing?.before,
735
- * 'w:after': spacing?.after,
736
- * 'w:line': spacing?.line
737
- * });
738
- * // Returns only attributes with defined values
739
- * ```
740
- */
741
- static buildAttributes(mapping: Record<string, any>): Record<string, string | number> {
742
- const attrs: Record<string, string | number> = {};
743
- for (const [key, value] of Object.entries(mapping)) {
744
- if (value !== undefined && value !== null) {
745
- attrs[key] = value;
746
- }
747
- }
748
- return attrs;
749
- }
750
-
751
- /**
752
- * Creates a border element for WordprocessingML
753
- * Used for table borders, cell borders, and paragraph borders
754
- *
755
- * @param side - Border side (e.g., 'top', 'left', 'bottom', 'right', 'insideH', 'insideV')
756
- * @param border - Border definition
757
- * @returns XML element for border
758
- *
759
- * @example
760
- * ```typescript
761
- * const border = XMLBuilder.createBorder('top', {
762
- * style: 'single',
763
- * size: 4,
764
- * color: 'FF0000',
765
- * space: 0
766
- * });
767
- * ```
768
- */
769
- static createBorder(
770
- side: string,
771
- border: {
772
- style?: string;
773
- size?: number;
774
- color?: string;
775
- space?: number;
776
- }
777
- ): XMLElement {
778
- const attrs = XMLBuilder.buildAttributes({
779
- 'w:val': border.style || 'single',
780
- 'w:sz': border.size,
781
- 'w:color': border.color,
782
- 'w:space': border.space
783
- });
784
-
785
- return XMLBuilder.wSelf(side, attrs);
786
- }
787
-
788
- /**
789
- * Creates a shading element for WordprocessingML
790
- * Used for paragraph shading, table shading, and cell shading
791
- *
792
- * @param shading - Shading definition (ShadingConfig with theme support)
793
- * @returns XML element for shading, or null if no shading properties
794
- *
795
- * @example
796
- * ```typescript
797
- * const shading = XMLBuilder.createShading({
798
- * fill: 'FFFF00',
799
- * pattern: 'clear',
800
- * color: '000000'
801
- * });
802
- * ```
803
- */
804
- static createShading(shading: ShadingConfig): XMLElement | null {
805
- const attrs = buildShadingAttributes(shading);
806
- // Default w:val to "clear" if not specified but other attrs exist
807
- if (!attrs['w:val'] && Object.keys(attrs).length > 0) {
808
- attrs['w:val'] = 'clear';
809
- }
810
- if (Object.keys(attrs).length > 0) {
811
- return XMLBuilder.wSelf('shd', attrs);
812
- }
813
- return null;
814
- }
815
-
816
- /**
817
- * Creates a margins element (tcMar, pgMar, etc.)
818
- * Used for cell margins, page margins, etc.
819
- *
820
- * @param type - Margin type element name (e.g., 'tcMar', 'pgMar')
821
- * @param margins - Margin values in twips
822
- * @returns XML element for margins, or null if no margins defined
823
- *
824
- * @example
825
- * ```typescript
826
- * const margins = XMLBuilder.createMargins('tcMar', {
827
- * top: 100,
828
- * bottom: 100,
829
- * left: 100,
830
- * right: 100
831
- * });
832
- * ```
833
- */
834
- static createMargins(
835
- type: string,
836
- margins: {
837
- top?: number;
838
- bottom?: number;
839
- left?: number;
840
- right?: number;
841
- start?: number;
842
- end?: number;
843
- }
844
- ): XMLElement | null {
845
- const children: XMLElement[] = [];
846
-
847
- if (margins.top !== undefined) {
848
- children.push(XMLBuilder.wSelf('top', { 'w:w': margins.top, 'w:type': 'dxa' }));
849
- }
850
- if (margins.bottom !== undefined) {
851
- children.push(XMLBuilder.wSelf('bottom', { 'w:w': margins.bottom, 'w:type': 'dxa' }));
852
- }
853
- if (margins.left !== undefined) {
854
- children.push(XMLBuilder.wSelf('left', { 'w:w': margins.left, 'w:type': 'dxa' }));
855
- }
856
- if (margins.right !== undefined) {
857
- children.push(XMLBuilder.wSelf('right', { 'w:w': margins.right, 'w:type': 'dxa' }));
858
- }
859
- if (margins.start !== undefined) {
860
- children.push(XMLBuilder.wSelf('start', { 'w:w': margins.start, 'w:type': 'dxa' }));
861
- }
862
- if (margins.end !== undefined) {
863
- children.push(XMLBuilder.wSelf('end', { 'w:w': margins.end, 'w:type': 'dxa' }));
864
- }
865
-
866
- if (children.length > 0) {
867
- return XMLBuilder.w(type, undefined, children);
868
- }
869
- return null;
870
- }
871
- }
1
+ /**
2
+ * XMLBuilder - Utility for building XML content
3
+ * Provides a simple fluent API for generating WordprocessingML XML
4
+ */
5
+
6
+ import { removeInvalidXmlChars } from '../utils/xmlSanitization';
7
+ import type { ShadingConfig } from '../elements/CommonTypes';
8
+ import { buildShadingAttributes } from '../elements/CommonTypes';
9
+
10
+ /** Represents a parsed XML object from XMLParser.parseToObject() */
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- parsed XML has dynamic keys and recursive structure
12
+ type ParsedXmlObject = Record<string, any>;
13
+
14
+ /**
15
+ * Represents an XML element with attributes and children
16
+ */
17
+ export interface XMLElement {
18
+ name: string;
19
+ attributes?: Record<string, string | number | boolean | undefined>;
20
+ children?: (XMLElement | string)[];
21
+ selfClosing?: boolean;
22
+ /** Raw XML content to include without escaping (used for VML passthrough) */
23
+ rawXml?: string;
24
+ }
25
+
26
+ /**
27
+ * XML Builder for creating WordprocessingML XML
28
+ */
29
+ export class XMLBuilder {
30
+ private elements: (XMLElement | string)[] = [];
31
+
32
+ /**
33
+ * Elements that must NEVER be self-closing in Word XML per ECMA-376.
34
+ * Self-closing these elements causes Word to not parse correctly or lose content.
35
+ */
36
+ private static readonly CANNOT_SELF_CLOSE = [
37
+ 'w:t',
38
+ 'w:r',
39
+ 'w:p',
40
+ 'w:tbl',
41
+ 'w:tr',
42
+ 'w:tc',
43
+ 'w:body',
44
+ 'w:document',
45
+ 'w:hyperlink',
46
+ 'w:sdt',
47
+ 'w:sdtContent',
48
+ 'w:sdtPr',
49
+ 'w:pPr',
50
+ 'w:rPr',
51
+ 'w:sectPr',
52
+ 'w:del', // Deletion revisions - container element, must have closing tag
53
+ 'w:ins', // Insertion revisions - container element, must have closing tag
54
+ 'w:moveFrom', // Move source markers - container element
55
+ 'w:moveTo', // Move destination markers - container element
56
+ // Note: w:bookmarkStart and w:bookmarkEnd MUST be self-closing per ECMA-376
57
+ ];
58
+
59
+ /**
60
+ * Adds an element to the builder
61
+ * @param name - Element name (with namespace prefix if needed)
62
+ * @param attributes - Element attributes
63
+ * @param children - Child elements or text
64
+ * @returns This builder for chaining
65
+ */
66
+ element(
67
+ name: string,
68
+ attributes?: Record<string, string | number | boolean | undefined>,
69
+ children?: (XMLElement | string)[]
70
+ ): XMLBuilder {
71
+ this.elements.push({
72
+ name,
73
+ attributes,
74
+ children,
75
+ });
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Adds a self-closing element
81
+ * @param name - Element name
82
+ * @param attributes - Element attributes
83
+ * @returns This builder for chaining
84
+ * @throws {Error} If attempting to create self-closing w:t element (not allowed per ECMA-376)
85
+ */
86
+ selfClosingElement(
87
+ name: string,
88
+ attributes?: Record<string, string | number | boolean | undefined>
89
+ ): XMLBuilder {
90
+ // Validation: Text elements (<w:t>) cannot be self-closing per ECMA-376
91
+ // Self-closing <w:t/> elements cause Word to fail opening the document
92
+ if (name === 'w:t' || name === 't') {
93
+ throw new Error(
94
+ 'Text elements (<w:t>) cannot be self-closing per ECMA-376. ' +
95
+ 'Use element() with empty text content instead: XMLBuilder.w("t", attrs, [""])'
96
+ );
97
+ }
98
+
99
+ this.elements.push({
100
+ name,
101
+ attributes,
102
+ selfClosing: true,
103
+ });
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * Adds text content
109
+ * @param text - Text to add
110
+ * @returns This builder for chaining
111
+ */
112
+ text(text: string): XMLBuilder {
113
+ this.elements.push(text);
114
+ return this;
115
+ }
116
+
117
+ /**
118
+ * Builds the XML string
119
+ * @param includeDeclaration - Whether to include XML declaration
120
+ * @returns Generated XML string
121
+ */
122
+ build(includeDeclaration = false): string {
123
+ let xml = '';
124
+
125
+ if (includeDeclaration) {
126
+ xml += '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
127
+ }
128
+
129
+ xml += this.elementsToString(this.elements);
130
+ return xml;
131
+ }
132
+
133
+ /**
134
+ * Converts elements to XML string
135
+ */
136
+ private elementsToString(elements: (XMLElement | string)[]): string {
137
+ let xml = '';
138
+
139
+ for (const element of elements) {
140
+ if (typeof element === 'string') {
141
+ xml += this.escapeXml(element);
142
+ } else {
143
+ xml += this.elementToString(element);
144
+ }
145
+ }
146
+
147
+ return xml;
148
+ }
149
+
150
+ /**
151
+ * Converts a single element to XML string
152
+ */
153
+ private elementToString(element: XMLElement): string {
154
+ // Special case: raw XML passthrough (no wrapper element)
155
+ // Used for VML and other legacy content that must be preserved exactly
156
+ if (element.name === '__rawXml' && element.rawXml) {
157
+ return element.rawXml;
158
+ }
159
+
160
+ let xml = `<${element.name}`;
161
+
162
+ // Add attributes
163
+ if (element.attributes) {
164
+ for (const [key, value] of Object.entries(element.attributes)) {
165
+ if (value !== undefined && value !== null && value !== false) {
166
+ // Handle boolean attributes
167
+ const attrValue = value === true ? key : String(value);
168
+ // Use escapeXmlAttribute for attribute values (Issue #8)
169
+ xml += ` ${key}="${XMLBuilder.escapeXmlAttribute(attrValue)}"`;
170
+ }
171
+ }
172
+ }
173
+
174
+ // Self-closing element validation
175
+ if (element.selfClosing) {
176
+ if (XMLBuilder.CANNOT_SELF_CLOSE.includes(element.name)) {
177
+ // Instead of throwing, force open/close tags for safety
178
+ xml += '></' + element.name + '>';
179
+ return xml;
180
+ }
181
+ xml += '/>';
182
+ return xml;
183
+ }
184
+
185
+ xml += '>';
186
+
187
+ // Add raw XML content if present (for VML passthrough)
188
+ if (element.rawXml) {
189
+ xml += element.rawXml;
190
+ }
191
+
192
+ // Add children
193
+ if (element.children && element.children.length > 0) {
194
+ xml += this.elementsToString(element.children);
195
+ }
196
+
197
+ xml += `</${element.name}>`;
198
+ return xml;
199
+ }
200
+
201
+ /**
202
+ * Escapes special XML characters for text content
203
+ * (Issue #8 fix: Use escapeXmlText for element text, escapeXmlAttribute called directly for attrs)
204
+ */
205
+ private escapeXml(text: string): string {
206
+ // This method is now only used for text content in elementsToString()
207
+ // Attributes call escapeXmlAttribute() directly in elementToString()
208
+ // Text content should NOT escape quotes (only & < >)
209
+ return XMLBuilder.escapeXmlText(text);
210
+ }
211
+
212
+ /**
213
+ * Escapes XML text content (element text nodes)
214
+ * Removes invalid XML 1.0 control characters and escapes: & < >
215
+ *
216
+ * Per XML 1.0 spec, control chars 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F are invalid.
217
+ * Tab (0x09), newline (0x0A), and CR (0x0D) are preserved.
218
+ *
219
+ * @param text Text to escape
220
+ * @returns Escaped text safe for XML content
221
+ */
222
+ static escapeXmlText(text: string): string {
223
+ return removeInvalidXmlChars(text)
224
+ .replace(/&/g, '&amp;')
225
+ .replace(/</g, '&lt;')
226
+ .replace(/>/g, '&gt;');
227
+ }
228
+
229
+ /**
230
+ * Escapes XML attribute values
231
+ * Removes invalid XML 1.0 control characters and escapes: & < > " '
232
+ *
233
+ * Per XML 1.0 spec, control chars 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F are invalid.
234
+ * Tab (0x09), newline (0x0A), and CR (0x0D) are preserved.
235
+ *
236
+ * @param value Attribute value to escape
237
+ * @returns Escaped value safe for XML attributes
238
+ */
239
+ static escapeXmlAttribute(value: string): string {
240
+ return removeInvalidXmlChars(value)
241
+ .replace(/&/g, '&amp;')
242
+ .replace(/</g, '&lt;')
243
+ .replace(/>/g, '&gt;')
244
+ .replace(/"/g, '&quot;')
245
+ .replace(/'/g, '&apos;');
246
+ }
247
+
248
+ /**
249
+ * Unescapes XML entities back to original characters
250
+ * @param text Text with XML entities
251
+ * @returns Unescaped text
252
+ */
253
+ static unescapeXml(text: string): string {
254
+ return text
255
+ .replace(/&lt;/g, '<')
256
+ .replace(/&gt;/g, '>')
257
+ .replace(/&quot;/g, '"')
258
+ .replace(/&apos;/g, "'")
259
+ .replace(/&amp;/g, '&'); // Must be last to avoid double-unescaping
260
+ }
261
+
262
+ /**
263
+ * Sanitizes and escapes XML content for safe inclusion in XML documents
264
+ * Removes control characters, null bytes, and escapes special XML characters
265
+ * Use this for user-provided content that may contain unsafe characters
266
+ *
267
+ * Per XML 1.0 spec, control chars 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F are invalid.
268
+ * Tab (0x09), newline (0x0A), and CR (0x0D) are preserved.
269
+ *
270
+ * @param text Text to sanitize and escape
271
+ * @returns Sanitized text safe for XML content
272
+ *
273
+ * **Issue #11 fix:** Prevents malformed XML from CDATA markers, control chars, etc.
274
+ */
275
+ static sanitizeXmlContent(text: string): string {
276
+ return (
277
+ removeInvalidXmlChars(text)
278
+ // Escape CDATA end marker to prevent CDATA injection
279
+ .replace(/\]\]>/g, ']]&gt;')
280
+ // Standard XML escaping (& must be first to avoid double-escaping)
281
+ .replace(/&/g, '&amp;')
282
+ .replace(/</g, '&lt;')
283
+ .replace(/>/g, '&gt;')
284
+ );
285
+ }
286
+
287
+ /**
288
+ * Creates a WordprocessingML namespace attribute object
289
+ */
290
+ static createNamespaces(): Record<string, string> {
291
+ return {
292
+ 'xmlns:w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
293
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
294
+ 'xmlns:wp': 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing',
295
+ 'xmlns:a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
296
+ 'xmlns:pic': 'http://schemas.openxmlformats.org/drawingml/2006/picture',
297
+ 'xmlns:w14': 'http://schemas.microsoft.com/office/word/2010/wordml',
298
+ 'xmlns:wpc': 'http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas',
299
+ 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
300
+ 'xmlns:o': 'urn:schemas-microsoft-com:office:office',
301
+ 'xmlns:v': 'urn:schemas-microsoft-com:vml',
302
+ 'xmlns:wp14': 'http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing',
303
+ 'xmlns:w10': 'urn:schemas-microsoft-com:office:word',
304
+ 'xmlns:w15': 'http://schemas.microsoft.com/office/word/2012/wordml',
305
+ 'xmlns:wpg': 'http://schemas.microsoft.com/office/word/2010/wordprocessingGroup',
306
+ 'xmlns:wpi': 'http://schemas.microsoft.com/office/word/2010/wordprocessingInk',
307
+ 'xmlns:wne': 'http://schemas.microsoft.com/office/word/2006/wordml',
308
+ 'xmlns:wps': 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape',
309
+ 'xmlns:asvg': 'http://schemas.microsoft.com/office/drawing/2016/SVG/main',
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Converts a single XMLElement to its XML string representation
315
+ * Useful for merging elements into existing XML
316
+ *
317
+ * @param element - The XMLElement to convert
318
+ * @returns XML string representation of the element
319
+ */
320
+ static elementToString(element: XMLElement): string {
321
+ const builder = new XMLBuilder();
322
+ // Use the private elementToString via the elements array + build
323
+ (builder as any).elements.push(element);
324
+ return builder.build();
325
+ }
326
+
327
+ /**
328
+ * Helper method to create a WordprocessingML element
329
+ * @param name - Element name (without 'w:' prefix)
330
+ * @param attributes - Element attributes
331
+ * @param children - Child elements
332
+ * @returns XMLElement
333
+ */
334
+ static w(
335
+ name: string,
336
+ attributes?: Record<string, string | number | boolean | undefined>,
337
+ children?: (XMLElement | string)[]
338
+ ): XMLElement {
339
+ return {
340
+ name: `w:${name}`,
341
+ attributes,
342
+ children,
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Helper method to create a self-closing WordprocessingML element
348
+ * @param name - Element name (without 'w:' prefix)
349
+ * @param attributes - Element attributes
350
+ * @returns XMLElement
351
+ */
352
+ static wSelf(
353
+ name: string,
354
+ attributes?: Record<string, string | number | boolean | undefined>
355
+ ): XMLElement {
356
+ return {
357
+ name: `w:${name}`,
358
+ attributes,
359
+ selfClosing: true,
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Helper method to create a w14 element (Word 2010+ features)
365
+ * @param name - Element name (without 'w14:' prefix)
366
+ * @param attributes - Element attributes
367
+ * @param children - Child elements
368
+ * @returns XMLElement
369
+ */
370
+ static w14(
371
+ name: string,
372
+ attributes?: Record<string, string | number | boolean | undefined>,
373
+ children?: (XMLElement | string)[]
374
+ ): XMLElement {
375
+ return {
376
+ name: `w14:${name}`,
377
+ attributes,
378
+ children,
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Helper method to create a self-closing w14 element
384
+ * @param name - Element name (without 'w14:' prefix)
385
+ * @param attributes - Element attributes
386
+ * @returns XMLElement
387
+ */
388
+ static w14Self(
389
+ name: string,
390
+ attributes?: Record<string, string | number | boolean | undefined>
391
+ ): XMLElement {
392
+ return {
393
+ name: `w14:${name}`,
394
+ attributes,
395
+ selfClosing: true,
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Helper method to create a DrawingML element (a: namespace)
401
+ * @param name - Element name (without 'a:' prefix)
402
+ * @param attributes - Element attributes
403
+ * @param children - Child elements
404
+ * @returns XMLElement
405
+ */
406
+ static a(
407
+ name: string,
408
+ attributes?: Record<string, string | number | boolean | undefined>,
409
+ children?: (XMLElement | string)[]
410
+ ): XMLElement {
411
+ return {
412
+ name: `a:${name}`,
413
+ attributes,
414
+ children,
415
+ };
416
+ }
417
+
418
+ /**
419
+ * Helper method to create a self-closing DrawingML element
420
+ * @param name - Element name (without 'a:' prefix)
421
+ * @param attributes - Element attributes
422
+ * @returns XMLElement
423
+ */
424
+ static aSelf(
425
+ name: string,
426
+ attributes?: Record<string, string | number | boolean | undefined>
427
+ ): XMLElement {
428
+ return {
429
+ name: `a:${name}`,
430
+ attributes,
431
+ selfClosing: true,
432
+ };
433
+ }
434
+
435
+ /**
436
+ * Helper method to create a Picture element (pic: namespace)
437
+ * @param name - Element name (without 'pic:' prefix)
438
+ * @param attributes - Element attributes
439
+ * @param children - Child elements
440
+ * @returns XMLElement
441
+ */
442
+ static pic(
443
+ name: string,
444
+ attributes?: Record<string, string | number | boolean | undefined>,
445
+ children?: (XMLElement | string)[]
446
+ ): XMLElement {
447
+ return {
448
+ name: `pic:${name}`,
449
+ attributes,
450
+ children,
451
+ };
452
+ }
453
+
454
+ /**
455
+ * Helper method to create a self-closing Picture element
456
+ * @param name - Element name (without 'pic:' prefix)
457
+ * @param attributes - Element attributes
458
+ * @returns XMLElement
459
+ */
460
+ static picSelf(
461
+ name: string,
462
+ attributes?: Record<string, string | number | boolean | undefined>
463
+ ): XMLElement {
464
+ return {
465
+ name: `pic:${name}`,
466
+ attributes,
467
+ selfClosing: true,
468
+ };
469
+ }
470
+
471
+ /**
472
+ * Helper method to create a Wordprocessing Drawing element (wp: namespace)
473
+ * @param name - Element name (without 'wp:' prefix)
474
+ * @param attributes - Element attributes
475
+ * @param children - Child elements
476
+ * @returns XMLElement
477
+ */
478
+ static wp(
479
+ name: string,
480
+ attributes?: Record<string, string | number | boolean | undefined>,
481
+ children?: (XMLElement | string)[]
482
+ ): XMLElement {
483
+ return {
484
+ name: `wp:${name}`,
485
+ attributes,
486
+ children,
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Helper method to create a self-closing Wordprocessing Drawing element
492
+ * @param name - Element name (without 'wp:' prefix)
493
+ * @param attributes - Element attributes
494
+ * @returns XMLElement
495
+ */
496
+ static wpSelf(
497
+ name: string,
498
+ attributes?: Record<string, string | number | boolean | undefined>
499
+ ): XMLElement {
500
+ return {
501
+ name: `wp:${name}`,
502
+ attributes,
503
+ selfClosing: true,
504
+ };
505
+ }
506
+
507
+ /**
508
+ * Helper to create cx/cy extent attributes (for a:ext, wp:extent, etc.)
509
+ * @param name - Element name (e.g., 'ext')
510
+ * @param cx - Width in EMUs
511
+ * @param cy - Height in EMUs
512
+ * @returns Self-closing XMLElement
513
+ */
514
+ static cxCy(name: string, cx: number, cy: number): XMLElement {
515
+ return {
516
+ name,
517
+ attributes: { cx, cy },
518
+ selfClosing: true,
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Creates an SDT (Structured Document Tag) wrapper for content
524
+ * @param content - Content to wrap (paragraphs, tables, etc.)
525
+ * @param options - SDT options
526
+ * @returns XMLElement representing the SDT wrapper
527
+ */
528
+ static createSDT(
529
+ content: XMLElement[],
530
+ options?: {
531
+ id?: number;
532
+ docPartGallery?: string;
533
+ docPartUnique?: boolean;
534
+ }
535
+ ): XMLElement {
536
+ const sdtId = options?.id ?? Math.floor(Math.random() * 2000000000) - 1000000000;
537
+
538
+ // Build SDT properties
539
+ const sdtPrChildren: XMLElement[] = [XMLBuilder.wSelf('id', { 'w:val': sdtId })];
540
+
541
+ // Add docPartObj if docPartGallery is specified
542
+ if (options?.docPartGallery) {
543
+ sdtPrChildren.push(
544
+ XMLBuilder.w('docPartObj', undefined, [
545
+ XMLBuilder.wSelf('docPartGallery', { 'w:val': options.docPartGallery }),
546
+ XMLBuilder.wSelf('docPartUnique', {
547
+ 'w:val': options?.docPartUnique !== false ? '1' : '0',
548
+ }),
549
+ ])
550
+ );
551
+ }
552
+
553
+ // Create complete SDT structure
554
+ return XMLBuilder.w('sdt', undefined, [
555
+ XMLBuilder.w('sdtPr', undefined, sdtPrChildren),
556
+ XMLBuilder.w('sdtContent', undefined, content),
557
+ ]);
558
+ }
559
+
560
+ /**
561
+ * Creates a complete WordprocessingML document structure
562
+ * @param bodyContent - Content for the document body
563
+ * @returns XML string for word/document.xml
564
+ */
565
+ static createDocument(
566
+ bodyContent: XMLElement[],
567
+ namespaces: Record<string, string> = {},
568
+ preBodyContent?: XMLElement[]
569
+ ): string {
570
+ const builder = new XMLBuilder();
571
+
572
+ // Preserve document's original namespace order, then fill in framework defaults
573
+ const allNamespaces: Record<string, string> = {};
574
+ for (const [key, value] of Object.entries(namespaces)) {
575
+ allNamespaces[key] = value;
576
+ }
577
+ for (const [key, value] of Object.entries(XMLBuilder.createNamespaces())) {
578
+ if (!(key in allNamespaces)) {
579
+ allNamespaces[key] = value;
580
+ }
581
+ }
582
+
583
+ // Ensure mc:Ignorable is present when extended namespaces are declared.
584
+ // Per ECMA-376, mc:Ignorable tells Word which namespace prefixes can be
585
+ // safely ignored if the processor doesn't support them. Without it,
586
+ // attributes like w14:paraId in raw XML passthrough zones cause corruption.
587
+ if (!allNamespaces['mc:Ignorable']) {
588
+ const ignorable: string[] = [];
589
+ if (allNamespaces['xmlns:w14']) ignorable.push('w14');
590
+ if (allNamespaces['xmlns:w15']) ignorable.push('w15');
591
+ if (allNamespaces['xmlns:wp14']) ignorable.push('wp14');
592
+ if (allNamespaces['xmlns:w16se']) ignorable.push('w16se');
593
+ if (allNamespaces['xmlns:w16cid']) ignorable.push('w16cid');
594
+ if (allNamespaces['xmlns:w16']) ignorable.push('w16');
595
+ if (allNamespaces['xmlns:w16cex']) ignorable.push('w16cex');
596
+ if (allNamespaces['xmlns:w16sdtdh']) ignorable.push('w16sdtdh');
597
+ if (allNamespaces['xmlns:w16sdtfl']) ignorable.push('w16sdtfl');
598
+ if (allNamespaces['xmlns:w16du']) ignorable.push('w16du');
599
+ if (allNamespaces['xmlns:asvg']) ignorable.push('asvg');
600
+ if (ignorable.length > 0) {
601
+ allNamespaces['mc:Ignorable'] = ignorable.join(' ');
602
+ }
603
+ } else {
604
+ // mc:Ignorable was loaded from the original document — ensure every
605
+ // prefix referenced in mc:Ignorable has a matching xmlns declaration.
606
+ // Without this, the validator rejects prefixes that appear in
607
+ // mc:Ignorable but lack namespace declarations in the root element.
608
+ const defaults = XMLBuilder.createNamespaces();
609
+ const prefixes = allNamespaces['mc:Ignorable'].split(/\s+/);
610
+ for (const prefix of prefixes) {
611
+ const nsKey = `xmlns:${prefix}`;
612
+ if (!allNamespaces[nsKey] && defaults[nsKey]) {
613
+ allNamespaces[nsKey] = defaults[nsKey];
614
+ }
615
+ }
616
+ }
617
+
618
+ const documentChildren: XMLElement[] = [];
619
+ if (preBodyContent) {
620
+ documentChildren.push(...preBodyContent);
621
+ }
622
+ documentChildren.push(XMLBuilder.w('body', undefined, bodyContent));
623
+ builder.element('w:document', allNamespaces, documentChildren);
624
+
625
+ return builder.build(true);
626
+ }
627
+
628
+ /**
629
+ * Builds an XML string from a JavaScript object.
630
+ * This is the reverse of XMLParser.parseToObject
631
+ */
632
+ static buildObject(obj: ParsedXmlObject, rootName: string): string {
633
+ const builder = new XMLBuilder();
634
+ const element = XMLBuilder.objectToElement(obj, rootName);
635
+ if (element) {
636
+ if (typeof element === 'string') {
637
+ builder.text(element);
638
+ } else {
639
+ builder.elements.push(element);
640
+ }
641
+ }
642
+ return builder.build();
643
+ }
644
+
645
+ /**
646
+ * Converts a JavaScript object to an XMLElement.
647
+ * @private
648
+ */
649
+ private static objectToElement(
650
+ obj: ParsedXmlObject | string | number | boolean | null | undefined,
651
+ name: string
652
+ ): XMLElement | string | null {
653
+ if (obj === null || obj === undefined) {
654
+ return null;
655
+ }
656
+
657
+ if (typeof obj !== 'object' || obj === null) {
658
+ return String(obj);
659
+ }
660
+
661
+ const attributes: Record<string, string | number | boolean> = {};
662
+ const children: (XMLElement | string)[] = [];
663
+
664
+ if (obj['#text'] && Object.keys(obj).length === 1) {
665
+ return String(obj['#text']);
666
+ }
667
+
668
+ for (const key in obj) {
669
+ if (key.startsWith('@_')) {
670
+ const attrName = key.substring(2);
671
+ // Validate attribute name is not empty after prefix removal
672
+ if (attrName.length > 0) {
673
+ attributes[attrName] = obj[key];
674
+ }
675
+ } else if (key === '#text') {
676
+ children.push(String(obj[key]));
677
+ } else {
678
+ const childObj = obj[key];
679
+ if (Array.isArray(childObj)) {
680
+ childObj.forEach((item) => {
681
+ const childElement = XMLBuilder.objectToElement(item, key);
682
+ if (childElement) {
683
+ children.push(childElement);
684
+ }
685
+ });
686
+ } else {
687
+ const childElement = XMLBuilder.objectToElement(childObj, key);
688
+ if (childElement) {
689
+ children.push(childElement);
690
+ }
691
+ }
692
+ }
693
+ }
694
+
695
+ const element: XMLElement = {
696
+ name,
697
+ attributes,
698
+ children: children.length > 0 ? children : undefined,
699
+ };
700
+
701
+ if (!element.children || element.children.length === 0) {
702
+ if (!XMLBuilder.CANNOT_SELF_CLOSE.includes(name)) {
703
+ element.selfClosing = true;
704
+ }
705
+ }
706
+
707
+ return element;
708
+ }
709
+
710
+ /**
711
+ * Helper method to build attributes object, filtering out undefined/null values
712
+ * This simplifies the common pattern of conditionally adding attributes
713
+ *
714
+ * @param mapping - Map of attribute names to values
715
+ * @returns Filtered attributes object with only defined values
716
+ *
717
+ * @example
718
+ * ```typescript
719
+ * const attrs = XMLBuilder.buildAttributes({
720
+ * 'w:before': spacing?.before,
721
+ * 'w:after': spacing?.after,
722
+ * 'w:line': spacing?.line
723
+ * });
724
+ * // Returns only attributes with defined values
725
+ * ```
726
+ */
727
+ static buildAttributes(mapping: Record<string, any>): Record<string, string | number> {
728
+ const attrs: Record<string, string | number> = {};
729
+ for (const [key, value] of Object.entries(mapping)) {
730
+ if (value !== undefined && value !== null) {
731
+ attrs[key] = value;
732
+ }
733
+ }
734
+ return attrs;
735
+ }
736
+
737
+ /**
738
+ * Creates a border element for WordprocessingML
739
+ * Used for table borders, cell borders, and paragraph borders
740
+ *
741
+ * @param side - Border side (e.g., 'top', 'left', 'bottom', 'right', 'insideH', 'insideV')
742
+ * @param border - Border definition
743
+ * @returns XML element for border
744
+ *
745
+ * @example
746
+ * ```typescript
747
+ * const border = XMLBuilder.createBorder('top', {
748
+ * style: 'single',
749
+ * size: 4,
750
+ * color: 'FF0000',
751
+ * space: 0
752
+ * });
753
+ * ```
754
+ */
755
+ static createBorder(
756
+ side: string,
757
+ border: {
758
+ style?: string;
759
+ size?: number;
760
+ color?: string;
761
+ space?: number;
762
+ }
763
+ ): XMLElement {
764
+ const attrs = XMLBuilder.buildAttributes({
765
+ 'w:val': border.style || 'single',
766
+ 'w:sz': border.size,
767
+ 'w:color': border.color,
768
+ 'w:space': border.space,
769
+ });
770
+
771
+ return XMLBuilder.wSelf(side, attrs);
772
+ }
773
+
774
+ /**
775
+ * Creates a shading element for WordprocessingML
776
+ * Used for paragraph shading, table shading, and cell shading
777
+ *
778
+ * @param shading - Shading definition (ShadingConfig with theme support)
779
+ * @returns XML element for shading, or null if no shading properties
780
+ *
781
+ * @example
782
+ * ```typescript
783
+ * const shading = XMLBuilder.createShading({
784
+ * fill: 'FFFF00',
785
+ * pattern: 'clear',
786
+ * color: '000000'
787
+ * });
788
+ * ```
789
+ */
790
+ static createShading(shading: ShadingConfig): XMLElement | null {
791
+ const attrs = buildShadingAttributes(shading);
792
+ // Default w:val to "clear" if not specified but other attrs exist
793
+ if (!attrs['w:val'] && Object.keys(attrs).length > 0) {
794
+ attrs['w:val'] = 'clear';
795
+ }
796
+ if (Object.keys(attrs).length > 0) {
797
+ return XMLBuilder.wSelf('shd', attrs);
798
+ }
799
+ return null;
800
+ }
801
+
802
+ /**
803
+ * Creates a margins element (tcMar, pgMar, etc.)
804
+ * Used for cell margins, page margins, etc.
805
+ *
806
+ * @param type - Margin type element name (e.g., 'tcMar', 'pgMar')
807
+ * @param margins - Margin values in twips
808
+ * @returns XML element for margins, or null if no margins defined
809
+ *
810
+ * @example
811
+ * ```typescript
812
+ * const margins = XMLBuilder.createMargins('tcMar', {
813
+ * top: 100,
814
+ * bottom: 100,
815
+ * left: 100,
816
+ * right: 100
817
+ * });
818
+ * ```
819
+ */
820
+ static createMargins(
821
+ type: string,
822
+ margins: {
823
+ top?: number;
824
+ bottom?: number;
825
+ left?: number;
826
+ right?: number;
827
+ start?: number;
828
+ end?: number;
829
+ }
830
+ ): XMLElement | null {
831
+ const children: XMLElement[] = [];
832
+
833
+ if (margins.top !== undefined) {
834
+ children.push(XMLBuilder.wSelf('top', { 'w:w': margins.top, 'w:type': 'dxa' }));
835
+ }
836
+ if (margins.bottom !== undefined) {
837
+ children.push(XMLBuilder.wSelf('bottom', { 'w:w': margins.bottom, 'w:type': 'dxa' }));
838
+ }
839
+ if (margins.left !== undefined) {
840
+ children.push(XMLBuilder.wSelf('left', { 'w:w': margins.left, 'w:type': 'dxa' }));
841
+ }
842
+ if (margins.right !== undefined) {
843
+ children.push(XMLBuilder.wSelf('right', { 'w:w': margins.right, 'w:type': 'dxa' }));
844
+ }
845
+ if (margins.start !== undefined) {
846
+ children.push(XMLBuilder.wSelf('start', { 'w:w': margins.start, 'w:type': 'dxa' }));
847
+ }
848
+ if (margins.end !== undefined) {
849
+ children.push(XMLBuilder.wSelf('end', { 'w:w': margins.end, 'w:type': 'dxa' }));
850
+ }
851
+
852
+ if (children.length > 0) {
853
+ return XMLBuilder.w(type, undefined, children);
854
+ }
855
+ return null;
856
+ }
857
+ }