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,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
+ }