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,542 +1,553 @@
1
- /**
2
- * Validation utilities for DOCX files
3
- */
4
-
5
- import { REQUIRED_DOCX_FILES } from '../zip/types';
6
- import { MissingRequiredFileError } from '../zip/errors';
7
- import { defaultLogger } from './logger';
8
-
9
- /**
10
- * Validates that all required DOCX files are present
11
- * @param filePaths - Array of file paths in the archive
12
- * @throws {MissingRequiredFileError} If a required file is missing
13
- */
14
- export function validateDocxStructure(filePaths: string[]): void {
15
- const fileSet = new Set(filePaths);
16
-
17
- for (const requiredFile of REQUIRED_DOCX_FILES) {
18
- if (!fileSet.has(requiredFile)) {
19
- throw new MissingRequiredFileError(requiredFile);
20
- }
21
- }
22
- }
23
-
24
- /**
25
- * Checks if a file path represents a binary file based on extension
26
- * @param filePath - The file path to check
27
- * @returns True if the file is likely binary
28
- */
29
- export function isBinaryFile(filePath: string): boolean {
30
- const binaryExtensions = [
31
- '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.ico',
32
- '.emf', '.wmf', '.bin', '.dat', '.ttf', '.otf', '.woff',
33
- ];
34
-
35
- const extension = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
36
- return binaryExtensions.includes(extension);
37
- }
38
-
39
- /**
40
- * Normalizes a file path for consistent comparisons
41
- * Converts backslashes to forward slashes and removes leading slashes
42
- * Also validates against path traversal attacks
43
- *
44
- * **Security:** This function validates paths to prevent:
45
- * - Path traversal attacks (../, ..\, URL-encoded variants)
46
- * - Absolute paths (C:\, /etc/, etc.)
47
- * - Malicious DOCX files attempting directory escape
48
- *
49
- * @param path - The path to normalize
50
- * @returns Normalized path
51
- * @throws {Error} If path contains path traversal sequences, absolute paths, or URL-encoded attacks
52
- */
53
- export function normalizePath(path: string): string {
54
- // First convert all backslashes to forward slashes for consistent checking
55
- const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
56
-
57
- // Security: Reject URL-encoded path traversal attempts
58
- // Attackers might try: %2e%2e%2f (%2e = . and %2f = /)
59
- if (/%2[eE]|%2[fF]|%5[cC]/.test(path)) {
60
- throw new Error(
61
- `Invalid file path: "${path}" contains URL-encoded characters (%2E, %2F, %5C). ` +
62
- `This could be an attempt to bypass path validation. ` +
63
- `Only plain characters are allowed in DOCX file paths.`
64
- );
65
- }
66
-
67
- // Security: Prevent path traversal attacks
68
- // Check AFTER normalization when all paths use forward slashes
69
- // This catches: ../, /.., or standalone ".."
70
- if (normalized.includes('../') || normalized.includes('/..') || normalized === '..') {
71
- throw new Error(
72
- `Invalid file path: "${path}" contains path traversal sequence (..). ` +
73
- `This could be a malicious DOCX file attempting directory traversal. ` +
74
- `DOCX archives must only contain relative paths within the archive.`
75
- );
76
- }
77
-
78
- // Security: Prevent absolute paths (Windows drive letters)
79
- // Examples: C:/, C:\, D:, etc.
80
- if (/^[a-zA-Z]:/.test(normalized)) {
81
- throw new Error(
82
- `Invalid file path: "${path}" appears to be an absolute Windows path. ` +
83
- `Absolute paths are not allowed in DOCX archives. ` +
84
- `Only relative paths within the archive are permitted.`
85
- );
86
- }
87
-
88
- // Security: Prevent Unix absolute paths
89
- // After removing leading slashes, if it starts with / it's suspicious
90
- if (path.startsWith('/') && normalized.startsWith('/')) {
91
- throw new Error(
92
- `Invalid file path: "${path}" appears to be an absolute Unix path. ` +
93
- `Only relative paths are allowed in DOCX archives.`
94
- );
95
- }
96
-
97
- return normalized;
98
- }
99
-
100
- /**
101
- * Validates that a buffer contains a valid ZIP file signature
102
- * ZIP files start with the signature 'PK' (0x50 0x4B)
103
- * @param buffer - The buffer to validate
104
- * @returns True if the buffer appears to be a ZIP file
105
- */
106
- export function isValidZipBuffer(buffer: Buffer): boolean {
107
- if (buffer.length < 4) {
108
- return false;
109
- }
110
-
111
- // Check for ZIP signature: PK\x03\x04 or PK\x05\x06 (for empty archives)
112
- return (
113
- (buffer[0] === 0x50 && buffer[1] === 0x4B) &&
114
- ((buffer[2] === 0x03 && buffer[3] === 0x04) ||
115
- (buffer[2] === 0x05 && buffer[3] === 0x06))
116
- );
117
- }
118
-
119
- /**
120
- * Checks if a string is valid UTF-8 text
121
- * @param content - The content to check
122
- * @returns True if the content is valid text
123
- */
124
- export function isTextContent(content: Buffer | string): boolean {
125
- if (typeof content === 'string') {
126
- return true;
127
- }
128
-
129
- // Try to decode as UTF-8 and check for null bytes
130
- try {
131
- const text = content.toString('utf8');
132
- // Binary files often contain null bytes
133
- return !text.includes('\0');
134
- } catch {
135
- return false;
136
- }
137
- }
138
-
139
- /**
140
- * Validates a twips value (used for spacing, indentation, margins)
141
- * Twips: 1/20th of a point, 1440 twips = 1 inch
142
- * Reasonable range: -31680 to 31680 (±22 inches)
143
- * @param value - The twips value to validate
144
- * @param fieldName - Name of the field (for error messages)
145
- * @throws {Error} If the value is invalid
146
- */
147
- export function validateTwips(value: number, fieldName = 'value'): void {
148
- if (!Number.isFinite(value)) {
149
- throw new Error(`${fieldName} must be a finite number, got ${value}`);
150
- }
151
-
152
- // Reasonable range: ±22 inches (31680 twips)
153
- const MIN_TWIPS = -31680;
154
- const MAX_TWIPS = 31680;
155
-
156
- if (value < MIN_TWIPS || value > MAX_TWIPS) {
157
- throw new Error(
158
- `${fieldName} out of range: ${value} twips (allowed: ${MIN_TWIPS} to ${MAX_TWIPS}, ±22 inches)`
159
- );
160
- }
161
- }
162
-
163
- /**
164
- * Normalizes a color to uppercase 6-character hex format
165
- * Accepts 3-character or 6-character hex colors with or without '#' prefix
166
- * Follows Microsoft Word convention of uppercase hex colors
167
- *
168
- * @param color - Color to normalize (e.g., '#F00', 'FF0000', '#FF0000', 'f00')
169
- * @returns Normalized color (e.g., 'FF0000')
170
- * @throws Error if color format is invalid
171
- *
172
- * @example
173
- * ```typescript
174
- * normalizeColor('#F00') // Returns: 'FF0000'
175
- * normalizeColor('FF0000') // Returns: 'FF0000'
176
- * normalizeColor('#ff0000') // Returns: 'FF0000'
177
- * normalizeColor('f00') // Returns: 'FF0000'
178
- * ```
179
- */
180
- export function normalizeColor(color: string): string {
181
- const hex = color.replace(/^#/, '');
182
-
183
- // Validate hex format
184
- if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(hex)) {
185
- throw new Error(
186
- `Invalid color format: "${color}". Expected 3 or 6-character hex ` +
187
- `(e.g., "FF0000", "#FF0000", "F00", or "#F00")`
188
- );
189
- }
190
-
191
- // Expand 3-character to 6-character
192
- if (hex.length === 3) {
193
- return (hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2)).toUpperCase();
194
- }
195
-
196
- return hex.toUpperCase();
197
- }
198
-
199
- /**
200
- * Validates a hexadecimal color value
201
- * Must be 6 characters (RRGGBB format)
202
- * @param color - The color hex string to validate (without #)
203
- * @param fieldName - Name of the field (for error messages)
204
- * @throws {Error} If the color is invalid
205
- */
206
- export function validateColor(color: string, fieldName = 'color'): void {
207
- if (typeof color !== 'string') {
208
- throw new Error(`${fieldName} must be a string, got ${typeof color}`);
209
- }
210
-
211
- // Allow both with and without # prefix
212
- const cleanColor = color.startsWith('#') ? color.substring(1) : color;
213
-
214
- if (!/^[0-9A-Fa-f]{6}$/.test(cleanColor)) {
215
- throw new Error(
216
- `${fieldName} must be a 6-digit hex color (e.g., 'FF0000' or '#FF0000'), got '${color}'`
217
- );
218
- }
219
- }
220
-
221
- /**
222
- * Alias for validateColor for backwards compatibility
223
- */
224
- export const validateHexColor = validateColor;
225
-
226
- /**
227
- * Validates a numbering ID (must be non-negative integer)
228
- * @param numId - The numbering ID to validate
229
- * @param fieldName - Name of the field (for error messages)
230
- * @throws {Error} If the ID is invalid
231
- */
232
- export function validateNumberingId(numId: number, fieldName = 'numbering ID'): void {
233
- if (!Number.isInteger(numId)) {
234
- throw new Error(`${fieldName} must be an integer, got ${numId}`);
235
- }
236
-
237
- if (numId < 0) {
238
- throw new Error(`${fieldName} must be non-negative, got ${numId}`);
239
- }
240
-
241
- // Word supports numbering IDs up to 2147483647
242
- const MAX_NUM_ID = 2147483647;
243
- if (numId > MAX_NUM_ID) {
244
- throw new Error(`${fieldName} exceeds maximum value ${MAX_NUM_ID}, got ${numId}`);
245
- }
246
- }
247
-
248
- /**
249
- * Validates a numbering level (0-8 for Word)
250
- * @param level - The level to validate
251
- * @param fieldName - Name of the field (for error messages)
252
- * @param maxLevel - Maximum allowed level (default 8)
253
- * @throws {Error} If the level is invalid
254
- */
255
- export function validateLevel(
256
- level: number,
257
- fieldName = 'level',
258
- maxLevel = 8
259
- ): void {
260
- if (!Number.isInteger(level)) {
261
- throw new Error(`${fieldName} must be an integer, got ${level}`);
262
- }
263
-
264
- if (level < 0 || level > maxLevel) {
265
- throw new Error(`${fieldName} must be between 0 and ${maxLevel}, got ${level}`);
266
- }
267
- }
268
-
269
- /**
270
- * Validates an alignment value against allowed values
271
- * @param alignment - The alignment value to validate
272
- * @param allowed - Array of allowed alignment values
273
- * @param fieldName - Name of the field (for error messages)
274
- * @throws {Error} If the alignment is invalid
275
- */
276
- export function validateAlignment(
277
- alignment: string,
278
- allowed: readonly string[],
279
- fieldName = 'alignment'
280
- ): void {
281
- if (typeof alignment !== 'string') {
282
- throw new Error(`${fieldName} must be a string, got ${typeof alignment}`);
283
- }
284
-
285
- if (!allowed.includes(alignment)) {
286
- throw new Error(
287
- `Invalid ${fieldName}: '${alignment}' (allowed: ${allowed.join(', ')})`
288
- );
289
- }
290
- }
291
-
292
- /**
293
- * Validates a font size (in half-points for Word)
294
- * Reasonable range: 2-1638 (1-819 points)
295
- * @param size - The font size in half-points to validate
296
- * @param fieldName - Name of the field (for error messages)
297
- * @throws {Error} If the size is invalid
298
- */
299
- export function validateFontSize(size: number, fieldName = 'font size'): void {
300
- if (!Number.isFinite(size)) {
301
- throw new Error(`${fieldName} must be a finite number, got ${size}`);
302
- }
303
-
304
- if (!Number.isInteger(size)) {
305
- throw new Error(`${fieldName} must be an integer (in half-points), got ${size}`);
306
- }
307
-
308
- // Reasonable range: 2-1638 half-points (1-819 points)
309
- const MIN_SIZE = 2;
310
- const MAX_SIZE = 1638;
311
-
312
- if (size < MIN_SIZE || size > MAX_SIZE) {
313
- throw new Error(
314
- `${fieldName} out of range: ${size} half-points (allowed: ${MIN_SIZE}-${MAX_SIZE}, or ${MIN_SIZE / 2}-${MAX_SIZE / 2} points)`
315
- );
316
- }
317
- }
318
-
319
- /**
320
- * Validates that a string is not empty
321
- * @param value - The string to validate
322
- * @param fieldName - Name of the field (for error messages)
323
- * @throws {Error} If the string is empty or not a string
324
- */
325
- export function validateNonEmptyString(value: string, fieldName = 'value'): void {
326
- if (typeof value !== 'string') {
327
- throw new Error(`${fieldName} must be a string, got ${typeof value}`);
328
- }
329
-
330
- if (value.trim().length === 0) {
331
- throw new Error(`${fieldName} cannot be empty`);
332
- }
333
- }
334
-
335
- /**
336
- * Validates a percentage value (0-100)
337
- * @param value - The percentage to validate
338
- * @param fieldName - Name of the field (for error messages)
339
- * @throws {Error} If the percentage is invalid
340
- */
341
- export function validatePercentage(value: number, fieldName = 'percentage'): void {
342
- if (!Number.isFinite(value)) {
343
- throw new Error(`${fieldName} must be a finite number, got ${value}`);
344
- }
345
-
346
- if (value < 0 || value > 100) {
347
- throw new Error(`${fieldName} must be between 0 and 100, got ${value}`);
348
- }
349
- }
350
-
351
- /**
352
- * Validates EMUs (English Metric Units) value
353
- * Used for image dimensions: 914400 EMUs = 1 inch
354
- * Reasonable range: 0 to 50 million (about 55 inches)
355
- * @param value - The EMUs value to validate
356
- * @param fieldName - Name of the field (for error messages)
357
- * @throws {Error} If the value is invalid
358
- */
359
- export function validateEmus(value: number, fieldName = 'EMUs'): void {
360
- if (!Number.isFinite(value)) {
361
- throw new Error(`${fieldName} must be a finite number, got ${value}`);
362
- }
363
-
364
- if (!Number.isInteger(value)) {
365
- throw new Error(`${fieldName} must be an integer, got ${value}`);
366
- }
367
-
368
- if (value < 0) {
369
- throw new Error(`${fieldName} must be non-negative, got ${value}`);
370
- }
371
-
372
- // Reasonable maximum: 50 million EMUs (about 55 inches)
373
- const MAX_EMUS = 50000000;
374
- if (value > MAX_EMUS) {
375
- throw new Error(
376
- `${fieldName} exceeds maximum ${MAX_EMUS} (about 55 inches), got ${value}`
377
- );
378
- }
379
- }
380
-
381
- /**
382
- * Result of text validation for XML-like content
383
- */
384
- export interface TextValidationResult {
385
- isValid: boolean;
386
- hasXmlPatterns: boolean;
387
- warnings: string[];
388
- cleanedText?: string;
389
- }
390
-
391
- /**
392
- * Detects XML-like patterns in text that might cause display issues
393
- *
394
- * This function checks for patterns that look like XML markup which,
395
- * when properly escaped in XML output, will display as literal text
396
- * in Word documents rather than being interpreted as markup.
397
- *
398
- * @param text - The text to validate
399
- * @param context - Optional context for better warning messages (e.g., "hyperlink text")
400
- * @returns Validation result with warnings and optional cleaned text
401
- */
402
- export function detectXmlInText(text: string, context?: string): TextValidationResult {
403
- const warnings: string[] = [];
404
- let hasXmlPatterns = false;
405
-
406
- // Check for common XML element patterns
407
- const xmlElementPattern = /<\/?w:[^>]+>|<w:[^>]+\/>/g;
408
- const escapedXmlPattern = /&lt;.*?&gt;|&quot;|&apos;/g;
409
-
410
- // Check for specific problematic patterns we've seen
411
- const problematicPatterns = [
412
- /<w:t\s+xml:space="preserve">/,
413
- /<w:t\s+xml:space=["']preserve["']>/,
414
- /<\/w:t>/,
415
- /&lt;w:t\s+xml:space=&quot;preserve&quot;&gt;/,
416
- ];
417
-
418
- // Check for any XML-like tags
419
- if (xmlElementPattern.test(text)) {
420
- hasXmlPatterns = true;
421
- const contextStr = context ? ` in ${context}` : '';
422
- warnings.push(
423
- `Text${contextStr} contains XML-like markup: "${text.substring(0, 100)}${text.length > 100 ? '...' : ''}". ` +
424
- `This will be displayed as literal text in the document. ` +
425
- `If you intended to add formatting, use the appropriate API methods instead.`
426
- );
427
- }
428
-
429
- // Check for already-escaped XML entities
430
- if (escapedXmlPattern.test(text)) {
431
- hasXmlPatterns = true;
432
- const contextStr = context ? ` in ${context}` : '';
433
- warnings.push(
434
- `Text${contextStr} contains escaped XML entities (e.g., &lt;, &gt;, &quot;). ` +
435
- `These will appear as literal characters in the document.`
436
- );
437
- }
438
-
439
- // Check for specific known problematic patterns
440
- for (const pattern of problematicPatterns) {
441
- if (pattern.test(text)) {
442
- hasXmlPatterns = true;
443
- const contextStr = context ? ` in ${context}` : '';
444
- warnings.push(
445
- `Text${contextStr} contains a known problematic XML pattern that suggests ` +
446
- `the text may have been corrupted by previous processing.`
447
- );
448
- break;
449
- }
450
- }
451
-
452
- return {
453
- isValid: true, // Text is always "valid" - we just warn about potential issues
454
- hasXmlPatterns,
455
- warnings,
456
- };
457
- }
458
-
459
- /**
460
- * Cleans XML-like patterns from text
461
- *
462
- * This function removes or cleans various XML patterns that might
463
- * appear in text content, typically from corrupted or improperly
464
- * processed documents.
465
- *
466
- * @param text - The text to clean
467
- * @param aggressive - If true, removes all angle brackets; if false, only removes clear XML tags
468
- * @returns Cleaned text with XML patterns removed
469
- */
470
- export function cleanXmlFromText(text: string, aggressive = false): string {
471
- let cleaned = text;
472
-
473
- // First, unescape any HTML/XML entities
474
- cleaned = cleaned
475
- .replace(/&lt;/g, '<')
476
- .replace(/&gt;/g, '>')
477
- .replace(/&quot;/g, '"')
478
- .replace(/&apos;/g, "'")
479
- .replace(/&amp;/g, '&');
480
-
481
- // Remove specific Word XML patterns
482
- // This targets patterns like <w:t xml:space="preserve">
483
- cleaned = cleaned.replace(/<w:[^>]+>/g, '');
484
- cleaned = cleaned.replace(/<\/w:[^>]+>/g, '');
485
-
486
- // Remove any remaining XML-like tags if aggressive mode
487
- if (aggressive) {
488
- cleaned = cleaned.replace(/<[^>]+>/g, '');
489
- }
490
-
491
- // Clean up any double spaces left behind
492
- cleaned = cleaned.replace(/\s+/g, ' ').trim();
493
-
494
- return cleaned;
495
- }
496
-
497
- /**
498
- * Validates text for use in Run or Hyperlink elements
499
- *
500
- * This is the main validation function that should be called when
501
- * setting text content in Run or Hyperlink elements. It provides
502
- * warnings about problematic content and optionally cleans the text.
503
- *
504
- * @param text - The text to validate
505
- * @param options - Validation options
506
- * @returns Validation result with warnings and optionally cleaned text
507
- */
508
- export function validateRunText(
509
- text: string,
510
- options: {
511
- context?: string;
512
- autoClean?: boolean;
513
- aggressive?: boolean;
514
- warnToConsole?: boolean;
515
- } = {}
516
- ): TextValidationResult {
517
- const { context, autoClean = false, aggressive = false, warnToConsole = true } = options;
518
-
519
- // Detect XML patterns
520
- const result = detectXmlInText(text, context);
521
-
522
- // If auto-cleaning is enabled and XML patterns were found
523
- if (autoClean && result.hasXmlPatterns) {
524
- result.cleanedText = cleanXmlFromText(text, aggressive);
525
-
526
- // Add a note about cleaning
527
- result.warnings.push(
528
- `Text has been automatically cleaned. ` +
529
- `Original: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}" ` +
530
- `Cleaned: "${result.cleanedText.substring(0, 50)}${result.cleanedText.length > 50 ? '...' : ''}"`
531
- );
532
- }
533
-
534
- // Log warnings to console in development if requested
535
- if (warnToConsole && result.warnings.length > 0 && typeof console !== 'undefined') {
536
- const contextStr = context ? ` [${context}]` : '';
537
- defaultLogger.warn(`DocXML Text Validation Warning${contextStr}:`);
538
- result.warnings.forEach(warning => defaultLogger.warn(` - ${warning}`));
539
- }
540
-
541
- return result;
542
- }
1
+ /**
2
+ * Validation utilities for DOCX files
3
+ */
4
+
5
+ import { REQUIRED_DOCX_FILES } from '../zip/types';
6
+ import { MissingRequiredFileError } from '../zip/errors';
7
+ import { defaultLogger } from './logger';
8
+
9
+ /**
10
+ * Validates that all required DOCX files are present
11
+ * @param filePaths - Array of file paths in the archive
12
+ * @throws {MissingRequiredFileError} If a required file is missing
13
+ */
14
+ export function validateDocxStructure(filePaths: string[]): void {
15
+ const fileSet = new Set(filePaths);
16
+
17
+ for (const requiredFile of REQUIRED_DOCX_FILES) {
18
+ if (!fileSet.has(requiredFile)) {
19
+ throw new MissingRequiredFileError(requiredFile);
20
+ }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Checks if a file path represents a binary file based on extension
26
+ * @param filePath - The file path to check
27
+ * @returns True if the file is likely binary
28
+ */
29
+ export function isBinaryFile(filePath: string): boolean {
30
+ const binaryExtensions = [
31
+ '.png',
32
+ '.jpg',
33
+ '.jpeg',
34
+ '.gif',
35
+ '.bmp',
36
+ '.tiff',
37
+ '.ico',
38
+ '.emf',
39
+ '.wmf',
40
+ '.bin',
41
+ '.dat',
42
+ '.ttf',
43
+ '.otf',
44
+ '.woff',
45
+ ];
46
+
47
+ const extension = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
48
+ return binaryExtensions.includes(extension);
49
+ }
50
+
51
+ /**
52
+ * Normalizes a file path for consistent comparisons
53
+ * Converts backslashes to forward slashes and removes leading slashes
54
+ * Also validates against path traversal attacks
55
+ *
56
+ * **Security:** This function validates paths to prevent:
57
+ * - Path traversal attacks (../, ..\, URL-encoded variants)
58
+ * - Absolute paths (C:\, /etc/, etc.)
59
+ * - Malicious DOCX files attempting directory escape
60
+ *
61
+ * @param path - The path to normalize
62
+ * @returns Normalized path
63
+ * @throws {Error} If path contains path traversal sequences, absolute paths, or URL-encoded attacks
64
+ */
65
+ export function normalizePath(path: string): string {
66
+ // First convert all backslashes to forward slashes for consistent checking
67
+ const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
68
+
69
+ // Security: Reject URL-encoded path traversal attempts
70
+ // Attackers might try: %2e%2e%2f (%2e = . and %2f = /)
71
+ if (/%2[eE]|%2[fF]|%5[cC]/.test(path)) {
72
+ throw new Error(
73
+ `Invalid file path: "${path}" contains URL-encoded characters (%2E, %2F, %5C). ` +
74
+ `This could be an attempt to bypass path validation. ` +
75
+ `Only plain characters are allowed in DOCX file paths.`
76
+ );
77
+ }
78
+
79
+ // Security: Prevent path traversal attacks
80
+ // Check AFTER normalization when all paths use forward slashes
81
+ // This catches: ../, /.., or standalone ".."
82
+ if (normalized.includes('../') || normalized.includes('/..') || normalized === '..') {
83
+ throw new Error(
84
+ `Invalid file path: "${path}" contains path traversal sequence (..). ` +
85
+ `This could be a malicious DOCX file attempting directory traversal. ` +
86
+ `DOCX archives must only contain relative paths within the archive.`
87
+ );
88
+ }
89
+
90
+ // Security: Prevent absolute paths (Windows drive letters)
91
+ // Examples: C:/, C:\, D:, etc.
92
+ if (/^[a-zA-Z]:/.test(normalized)) {
93
+ throw new Error(
94
+ `Invalid file path: "${path}" appears to be an absolute Windows path. ` +
95
+ `Absolute paths are not allowed in DOCX archives. ` +
96
+ `Only relative paths within the archive are permitted.`
97
+ );
98
+ }
99
+
100
+ // Security: Prevent Unix absolute paths
101
+ // After removing leading slashes, if it starts with / it's suspicious
102
+ if (path.startsWith('/') && normalized.startsWith('/')) {
103
+ throw new Error(
104
+ `Invalid file path: "${path}" appears to be an absolute Unix path. ` +
105
+ `Only relative paths are allowed in DOCX archives.`
106
+ );
107
+ }
108
+
109
+ return normalized;
110
+ }
111
+
112
+ /**
113
+ * Validates that a buffer contains a valid ZIP file signature
114
+ * ZIP files start with the signature 'PK' (0x50 0x4B)
115
+ * @param buffer - The buffer to validate
116
+ * @returns True if the buffer appears to be a ZIP file
117
+ */
118
+ export function isValidZipBuffer(buffer: Buffer): boolean {
119
+ if (buffer.length < 4) {
120
+ return false;
121
+ }
122
+
123
+ // Check for ZIP signature: PK\x03\x04 or PK\x05\x06 (for empty archives)
124
+ return (
125
+ buffer[0] === 0x50 &&
126
+ buffer[1] === 0x4b &&
127
+ ((buffer[2] === 0x03 && buffer[3] === 0x04) || (buffer[2] === 0x05 && buffer[3] === 0x06))
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Checks if a string is valid UTF-8 text
133
+ * @param content - The content to check
134
+ * @returns True if the content is valid text
135
+ */
136
+ export function isTextContent(content: Buffer | string): boolean {
137
+ if (typeof content === 'string') {
138
+ return true;
139
+ }
140
+
141
+ // Try to decode as UTF-8 and check for null bytes
142
+ try {
143
+ const text = content.toString('utf8');
144
+ // Binary files often contain null bytes
145
+ return !text.includes('\0');
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Validates a twips value (used for spacing, indentation, margins)
153
+ * Twips: 1/20th of a point, 1440 twips = 1 inch
154
+ * Reasonable range: -31680 to 31680 (±22 inches)
155
+ * @param value - The twips value to validate
156
+ * @param fieldName - Name of the field (for error messages)
157
+ * @throws {Error} If the value is invalid
158
+ */
159
+ export function validateTwips(value: number, fieldName = 'value'): void {
160
+ if (!Number.isFinite(value)) {
161
+ throw new Error(`${fieldName} must be a finite number, got ${value}`);
162
+ }
163
+
164
+ // Reasonable range: ±22 inches (31680 twips)
165
+ const MIN_TWIPS = -31680;
166
+ const MAX_TWIPS = 31680;
167
+
168
+ if (value < MIN_TWIPS || value > MAX_TWIPS) {
169
+ throw new Error(
170
+ `${fieldName} out of range: ${value} twips (allowed: ${MIN_TWIPS} to ${MAX_TWIPS}, ±22 inches)`
171
+ );
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Normalizes a color to uppercase 6-character hex format
177
+ * Accepts 3-character or 6-character hex colors with or without '#' prefix
178
+ * Follows Microsoft Word convention of uppercase hex colors
179
+ *
180
+ * @param color - Color to normalize (e.g., '#F00', 'FF0000', '#FF0000', 'f00')
181
+ * @returns Normalized color (e.g., 'FF0000')
182
+ * @throws Error if color format is invalid
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * normalizeColor('#F00') // Returns: 'FF0000'
187
+ * normalizeColor('FF0000') // Returns: 'FF0000'
188
+ * normalizeColor('#ff0000') // Returns: 'FF0000'
189
+ * normalizeColor('f00') // Returns: 'FF0000'
190
+ * ```
191
+ */
192
+ export function normalizeColor(color: string): string {
193
+ const hex = color.replace(/^#/, '');
194
+
195
+ // Validate hex format
196
+ if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(hex)) {
197
+ throw new Error(
198
+ `Invalid color format: "${color}". Expected 3 or 6-character hex ` +
199
+ `(e.g., "FF0000", "#FF0000", "F00", or "#F00")`
200
+ );
201
+ }
202
+
203
+ // Expand 3-character to 6-character
204
+ if (hex.length === 3) {
205
+ return (
206
+ hex.charAt(0) +
207
+ hex.charAt(0) +
208
+ hex.charAt(1) +
209
+ hex.charAt(1) +
210
+ hex.charAt(2) +
211
+ hex.charAt(2)
212
+ ).toUpperCase();
213
+ }
214
+
215
+ return hex.toUpperCase();
216
+ }
217
+
218
+ /**
219
+ * Validates a hexadecimal color value
220
+ * Must be 6 characters (RRGGBB format)
221
+ * @param color - The color hex string to validate (without #)
222
+ * @param fieldName - Name of the field (for error messages)
223
+ * @throws {Error} If the color is invalid
224
+ */
225
+ export function validateColor(color: string, fieldName = 'color'): void {
226
+ if (typeof color !== 'string') {
227
+ throw new Error(`${fieldName} must be a string, got ${typeof color}`);
228
+ }
229
+
230
+ // Allow both with and without # prefix
231
+ const cleanColor = color.startsWith('#') ? color.substring(1) : color;
232
+
233
+ if (!/^[0-9A-Fa-f]{6}$/.test(cleanColor)) {
234
+ throw new Error(
235
+ `${fieldName} must be a 6-digit hex color (e.g., 'FF0000' or '#FF0000'), got '${color}'`
236
+ );
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Alias for validateColor for backwards compatibility
242
+ */
243
+ export const validateHexColor = validateColor;
244
+
245
+ /**
246
+ * Validates a numbering ID (must be non-negative integer)
247
+ * @param numId - The numbering ID to validate
248
+ * @param fieldName - Name of the field (for error messages)
249
+ * @throws {Error} If the ID is invalid
250
+ */
251
+ export function validateNumberingId(numId: number, fieldName = 'numbering ID'): void {
252
+ if (!Number.isInteger(numId)) {
253
+ throw new Error(`${fieldName} must be an integer, got ${numId}`);
254
+ }
255
+
256
+ if (numId < 0) {
257
+ throw new Error(`${fieldName} must be non-negative, got ${numId}`);
258
+ }
259
+
260
+ // Word supports numbering IDs up to 2147483647
261
+ const MAX_NUM_ID = 2147483647;
262
+ if (numId > MAX_NUM_ID) {
263
+ throw new Error(`${fieldName} exceeds maximum value ${MAX_NUM_ID}, got ${numId}`);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Validates a numbering level (0-8 for Word)
269
+ * @param level - The level to validate
270
+ * @param fieldName - Name of the field (for error messages)
271
+ * @param maxLevel - Maximum allowed level (default 8)
272
+ * @throws {Error} If the level is invalid
273
+ */
274
+ export function validateLevel(level: number, fieldName = 'level', maxLevel = 8): void {
275
+ if (!Number.isInteger(level)) {
276
+ throw new Error(`${fieldName} must be an integer, got ${level}`);
277
+ }
278
+
279
+ if (level < 0 || level > maxLevel) {
280
+ throw new Error(`${fieldName} must be between 0 and ${maxLevel}, got ${level}`);
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Validates an alignment value against allowed values
286
+ * @param alignment - The alignment value to validate
287
+ * @param allowed - Array of allowed alignment values
288
+ * @param fieldName - Name of the field (for error messages)
289
+ * @throws {Error} If the alignment is invalid
290
+ */
291
+ export function validateAlignment(
292
+ alignment: string,
293
+ allowed: readonly string[],
294
+ fieldName = 'alignment'
295
+ ): void {
296
+ if (typeof alignment !== 'string') {
297
+ throw new Error(`${fieldName} must be a string, got ${typeof alignment}`);
298
+ }
299
+
300
+ if (!allowed.includes(alignment)) {
301
+ throw new Error(`Invalid ${fieldName}: '${alignment}' (allowed: ${allowed.join(', ')})`);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Validates a font size (in half-points for Word)
307
+ * Reasonable range: 2-1638 (1-819 points)
308
+ * @param size - The font size in half-points to validate
309
+ * @param fieldName - Name of the field (for error messages)
310
+ * @throws {Error} If the size is invalid
311
+ */
312
+ export function validateFontSize(size: number, fieldName = 'font size'): void {
313
+ if (!Number.isFinite(size)) {
314
+ throw new Error(`${fieldName} must be a finite number, got ${size}`);
315
+ }
316
+
317
+ if (!Number.isInteger(size)) {
318
+ throw new Error(`${fieldName} must be an integer (in half-points), got ${size}`);
319
+ }
320
+
321
+ // Reasonable range: 2-1638 half-points (1-819 points)
322
+ const MIN_SIZE = 2;
323
+ const MAX_SIZE = 1638;
324
+
325
+ if (size < MIN_SIZE || size > MAX_SIZE) {
326
+ throw new Error(
327
+ `${fieldName} out of range: ${size} half-points (allowed: ${MIN_SIZE}-${MAX_SIZE}, or ${MIN_SIZE / 2}-${MAX_SIZE / 2} points)`
328
+ );
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Validates that a string is not empty
334
+ * @param value - The string to validate
335
+ * @param fieldName - Name of the field (for error messages)
336
+ * @throws {Error} If the string is empty or not a string
337
+ */
338
+ export function validateNonEmptyString(value: string, fieldName = 'value'): void {
339
+ if (typeof value !== 'string') {
340
+ throw new Error(`${fieldName} must be a string, got ${typeof value}`);
341
+ }
342
+
343
+ if (value.trim().length === 0) {
344
+ throw new Error(`${fieldName} cannot be empty`);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Validates a percentage value (0-100)
350
+ * @param value - The percentage to validate
351
+ * @param fieldName - Name of the field (for error messages)
352
+ * @throws {Error} If the percentage is invalid
353
+ */
354
+ export function validatePercentage(value: number, fieldName = 'percentage'): void {
355
+ if (!Number.isFinite(value)) {
356
+ throw new Error(`${fieldName} must be a finite number, got ${value}`);
357
+ }
358
+
359
+ if (value < 0 || value > 100) {
360
+ throw new Error(`${fieldName} must be between 0 and 100, got ${value}`);
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Validates EMUs (English Metric Units) value
366
+ * Used for image dimensions: 914400 EMUs = 1 inch
367
+ * Reasonable range: 0 to 50 million (about 55 inches)
368
+ * @param value - The EMUs value to validate
369
+ * @param fieldName - Name of the field (for error messages)
370
+ * @throws {Error} If the value is invalid
371
+ */
372
+ export function validateEmus(value: number, fieldName = 'EMUs'): void {
373
+ if (!Number.isFinite(value)) {
374
+ throw new Error(`${fieldName} must be a finite number, got ${value}`);
375
+ }
376
+
377
+ if (!Number.isInteger(value)) {
378
+ throw new Error(`${fieldName} must be an integer, got ${value}`);
379
+ }
380
+
381
+ if (value < 0) {
382
+ throw new Error(`${fieldName} must be non-negative, got ${value}`);
383
+ }
384
+
385
+ // Reasonable maximum: 50 million EMUs (about 55 inches)
386
+ const MAX_EMUS = 50000000;
387
+ if (value > MAX_EMUS) {
388
+ throw new Error(`${fieldName} exceeds maximum ${MAX_EMUS} (about 55 inches), got ${value}`);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Result of text validation for XML-like content
394
+ */
395
+ export interface TextValidationResult {
396
+ isValid: boolean;
397
+ hasXmlPatterns: boolean;
398
+ warnings: string[];
399
+ cleanedText?: string;
400
+ }
401
+
402
+ /**
403
+ * Detects XML-like patterns in text that might cause display issues
404
+ *
405
+ * This function checks for patterns that look like XML markup which,
406
+ * when properly escaped in XML output, will display as literal text
407
+ * in Word documents rather than being interpreted as markup.
408
+ *
409
+ * @param text - The text to validate
410
+ * @param context - Optional context for better warning messages (e.g., "hyperlink text")
411
+ * @returns Validation result with warnings and optional cleaned text
412
+ */
413
+ export function detectXmlInText(text: string, context?: string): TextValidationResult {
414
+ const warnings: string[] = [];
415
+ let hasXmlPatterns = false;
416
+
417
+ // Check for common XML element patterns
418
+ const xmlElementPattern = /<\/?w:[^>]+>|<w:[^>]+\/>/g;
419
+ const escapedXmlPattern = /&lt;.*?&gt;|&quot;|&apos;/g;
420
+
421
+ // Check for specific problematic patterns we've seen
422
+ const problematicPatterns = [
423
+ /<w:t\s+xml:space="preserve">/,
424
+ /<w:t\s+xml:space=["']preserve["']>/,
425
+ /<\/w:t>/,
426
+ /&lt;w:t\s+xml:space=&quot;preserve&quot;&gt;/,
427
+ ];
428
+
429
+ // Check for any XML-like tags
430
+ if (xmlElementPattern.test(text)) {
431
+ hasXmlPatterns = true;
432
+ const contextStr = context ? ` in ${context}` : '';
433
+ warnings.push(
434
+ `Text${contextStr} contains XML-like markup: "${text.substring(0, 100)}${text.length > 100 ? '...' : ''}". ` +
435
+ `This will be displayed as literal text in the document. ` +
436
+ `If you intended to add formatting, use the appropriate API methods instead.`
437
+ );
438
+ }
439
+
440
+ // Check for already-escaped XML entities
441
+ if (escapedXmlPattern.test(text)) {
442
+ hasXmlPatterns = true;
443
+ const contextStr = context ? ` in ${context}` : '';
444
+ warnings.push(
445
+ `Text${contextStr} contains escaped XML entities (e.g., &lt;, &gt;, &quot;). ` +
446
+ `These will appear as literal characters in the document.`
447
+ );
448
+ }
449
+
450
+ // Check for specific known problematic patterns
451
+ for (const pattern of problematicPatterns) {
452
+ if (pattern.test(text)) {
453
+ hasXmlPatterns = true;
454
+ const contextStr = context ? ` in ${context}` : '';
455
+ warnings.push(
456
+ `Text${contextStr} contains a known problematic XML pattern that suggests ` +
457
+ `the text may have been corrupted by previous processing.`
458
+ );
459
+ break;
460
+ }
461
+ }
462
+
463
+ return {
464
+ isValid: true, // Text is always "valid" - we just warn about potential issues
465
+ hasXmlPatterns,
466
+ warnings,
467
+ };
468
+ }
469
+
470
+ /**
471
+ * Cleans XML-like patterns from text
472
+ *
473
+ * This function removes or cleans various XML patterns that might
474
+ * appear in text content, typically from corrupted or improperly
475
+ * processed documents.
476
+ *
477
+ * @param text - The text to clean
478
+ * @param aggressive - If true, removes all angle brackets; if false, only removes clear XML tags
479
+ * @returns Cleaned text with XML patterns removed
480
+ */
481
+ export function cleanXmlFromText(text: string, aggressive = false): string {
482
+ let cleaned = text;
483
+
484
+ // First, unescape any HTML/XML entities
485
+ cleaned = cleaned
486
+ .replace(/&lt;/g, '<')
487
+ .replace(/&gt;/g, '>')
488
+ .replace(/&quot;/g, '"')
489
+ .replace(/&apos;/g, "'")
490
+ .replace(/&amp;/g, '&');
491
+
492
+ // Remove specific Word XML patterns
493
+ // This targets patterns like <w:t xml:space="preserve">
494
+ cleaned = cleaned.replace(/<w:[^>]+>/g, '');
495
+ cleaned = cleaned.replace(/<\/w:[^>]+>/g, '');
496
+
497
+ // Remove any remaining XML-like tags if aggressive mode
498
+ if (aggressive) {
499
+ cleaned = cleaned.replace(/<[^>]+>/g, '');
500
+ }
501
+
502
+ // Clean up any double spaces left behind
503
+ cleaned = cleaned.replace(/\s+/g, ' ').trim();
504
+
505
+ return cleaned;
506
+ }
507
+
508
+ /**
509
+ * Validates text for use in Run or Hyperlink elements
510
+ *
511
+ * This is the main validation function that should be called when
512
+ * setting text content in Run or Hyperlink elements. It provides
513
+ * warnings about problematic content and optionally cleans the text.
514
+ *
515
+ * @param text - The text to validate
516
+ * @param options - Validation options
517
+ * @returns Validation result with warnings and optionally cleaned text
518
+ */
519
+ export function validateRunText(
520
+ text: string,
521
+ options: {
522
+ context?: string;
523
+ autoClean?: boolean;
524
+ aggressive?: boolean;
525
+ warnToConsole?: boolean;
526
+ } = {}
527
+ ): TextValidationResult {
528
+ const { context, autoClean = false, aggressive = false, warnToConsole = true } = options;
529
+
530
+ // Detect XML patterns
531
+ const result = detectXmlInText(text, context);
532
+
533
+ // If auto-cleaning is enabled and XML patterns were found
534
+ if (autoClean && result.hasXmlPatterns) {
535
+ result.cleanedText = cleanXmlFromText(text, aggressive);
536
+
537
+ // Add a note about cleaning
538
+ result.warnings.push(
539
+ `Text has been automatically cleaned. ` +
540
+ `Original: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}" ` +
541
+ `Cleaned: "${result.cleanedText.substring(0, 50)}${result.cleanedText.length > 50 ? '...' : ''}"`
542
+ );
543
+ }
544
+
545
+ // Log warnings to console in development if requested
546
+ if (warnToConsole && result.warnings.length > 0 && typeof console !== 'undefined') {
547
+ const contextStr = context ? ` [${context}]` : '';
548
+ defaultLogger.warn(`DocXML Text Validation Warning${contextStr}:`);
549
+ result.warnings.forEach((warning) => defaultLogger.warn(` - ${warning}`));
550
+ }
551
+
552
+ return result;
553
+ }