docxmlater 11.0.9 → 12.0.1

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 (354) hide show
  1. package/README.md +51 -13
  2. package/dist/core/Document.d.ts +28 -2
  3. package/dist/core/Document.d.ts.map +1 -1
  4. package/dist/core/Document.js +725 -170
  5. package/dist/core/Document.js.map +1 -1
  6. package/dist/core/DocumentGenerator.d.ts +1 -0
  7. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  8. package/dist/core/DocumentGenerator.js +77 -25
  9. package/dist/core/DocumentGenerator.js.map +1 -1
  10. package/dist/core/DocumentParser.d.ts +7 -3
  11. package/dist/core/DocumentParser.d.ts.map +1 -1
  12. package/dist/core/DocumentParser.js +431 -85
  13. package/dist/core/DocumentParser.js.map +1 -1
  14. package/dist/core/RelationshipManager.d.ts.map +1 -1
  15. package/dist/core/RelationshipManager.js +2 -2
  16. package/dist/core/RelationshipManager.js.map +1 -1
  17. package/dist/elements/Comment.d.ts +15 -1
  18. package/dist/elements/Comment.d.ts.map +1 -1
  19. package/dist/elements/Comment.js +67 -12
  20. package/dist/elements/Comment.js.map +1 -1
  21. package/dist/elements/CommentManager.d.ts +4 -0
  22. package/dist/elements/CommentManager.d.ts.map +1 -1
  23. package/dist/elements/CommentManager.js +74 -16
  24. package/dist/elements/CommentManager.js.map +1 -1
  25. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  26. package/dist/elements/EndnoteManager.js.map +1 -1
  27. package/dist/elements/Field.d.ts +10 -1
  28. package/dist/elements/Field.d.ts.map +1 -1
  29. package/dist/elements/Field.js +76 -34
  30. package/dist/elements/Field.js.map +1 -1
  31. package/dist/elements/FieldHelpers.d.ts +1 -0
  32. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  33. package/dist/elements/FieldHelpers.js +8 -4
  34. package/dist/elements/FieldHelpers.js.map +1 -1
  35. package/dist/elements/Footer.d.ts +3 -1
  36. package/dist/elements/Footer.d.ts.map +1 -1
  37. package/dist/elements/Footer.js +39 -53
  38. package/dist/elements/Footer.js.map +1 -1
  39. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  40. package/dist/elements/FootnoteManager.js.map +1 -1
  41. package/dist/elements/Header.d.ts +3 -1
  42. package/dist/elements/Header.d.ts.map +1 -1
  43. package/dist/elements/Header.js +39 -53
  44. package/dist/elements/Header.js.map +1 -1
  45. package/dist/elements/HeaderFooterManager.d.ts +4 -2
  46. package/dist/elements/HeaderFooterManager.d.ts.map +1 -1
  47. package/dist/elements/HeaderFooterManager.js +36 -6
  48. package/dist/elements/HeaderFooterManager.js.map +1 -1
  49. package/dist/elements/Hyperlink.d.ts +6 -1
  50. package/dist/elements/Hyperlink.d.ts.map +1 -1
  51. package/dist/elements/Hyperlink.js +69 -44
  52. package/dist/elements/Hyperlink.js.map +1 -1
  53. package/dist/elements/Image.d.ts.map +1 -1
  54. package/dist/elements/Image.js +33 -6
  55. package/dist/elements/Image.js.map +1 -1
  56. package/dist/elements/ImageManager.d.ts +1 -0
  57. package/dist/elements/ImageManager.d.ts.map +1 -1
  58. package/dist/elements/ImageManager.js +11 -0
  59. package/dist/elements/ImageManager.js.map +1 -1
  60. package/dist/elements/Paragraph.d.ts +3 -0
  61. package/dist/elements/Paragraph.d.ts.map +1 -1
  62. package/dist/elements/Paragraph.js +91 -26
  63. package/dist/elements/Paragraph.js.map +1 -1
  64. package/dist/elements/Revision.d.ts +4 -0
  65. package/dist/elements/Revision.d.ts.map +1 -1
  66. package/dist/elements/Revision.js +369 -29
  67. package/dist/elements/Revision.js.map +1 -1
  68. package/dist/elements/Run.d.ts +1 -0
  69. package/dist/elements/Run.d.ts.map +1 -1
  70. package/dist/elements/Run.js +24 -4
  71. package/dist/elements/Run.js.map +1 -1
  72. package/dist/elements/Section.d.ts.map +1 -1
  73. package/dist/elements/Section.js +17 -7
  74. package/dist/elements/Section.js.map +1 -1
  75. package/dist/elements/Shape.d.ts.map +1 -1
  76. package/dist/elements/Shape.js +50 -42
  77. package/dist/elements/Shape.js.map +1 -1
  78. package/dist/elements/StructuredDocumentTag.d.ts +9 -1
  79. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  80. package/dist/elements/StructuredDocumentTag.js +46 -6
  81. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  82. package/dist/elements/Table.d.ts +2 -0
  83. package/dist/elements/Table.d.ts.map +1 -1
  84. package/dist/elements/Table.js +90 -42
  85. package/dist/elements/Table.js.map +1 -1
  86. package/dist/elements/TableCell.d.ts +1 -0
  87. package/dist/elements/TableCell.d.ts.map +1 -1
  88. package/dist/elements/TableCell.js +32 -13
  89. package/dist/elements/TableCell.js.map +1 -1
  90. package/dist/elements/TableOfContents.d.ts.map +1 -1
  91. package/dist/elements/TableOfContents.js +5 -7
  92. package/dist/elements/TableOfContents.js.map +1 -1
  93. package/dist/elements/TableRow.d.ts +1 -0
  94. package/dist/elements/TableRow.d.ts.map +1 -1
  95. package/dist/elements/TableRow.js +16 -1
  96. package/dist/elements/TableRow.js.map +1 -1
  97. package/dist/elements/TextBox.d.ts.map +1 -1
  98. package/dist/elements/TextBox.js +55 -42
  99. package/dist/elements/TextBox.js.map +1 -1
  100. package/dist/esm/core/Document.js +726 -171
  101. package/dist/esm/core/Document.js.map +1 -1
  102. package/dist/esm/core/DocumentGenerator.js +77 -25
  103. package/dist/esm/core/DocumentGenerator.js.map +1 -1
  104. package/dist/esm/core/DocumentParser.js +431 -85
  105. package/dist/esm/core/DocumentParser.js.map +1 -1
  106. package/dist/esm/core/RelationshipManager.js +2 -2
  107. package/dist/esm/core/RelationshipManager.js.map +1 -1
  108. package/dist/esm/elements/Comment.js +67 -12
  109. package/dist/esm/elements/Comment.js.map +1 -1
  110. package/dist/esm/elements/CommentManager.js +74 -16
  111. package/dist/esm/elements/CommentManager.js.map +1 -1
  112. package/dist/esm/elements/EndnoteManager.js.map +1 -1
  113. package/dist/esm/elements/Field.js +77 -35
  114. package/dist/esm/elements/Field.js.map +1 -1
  115. package/dist/esm/elements/FieldHelpers.js +7 -4
  116. package/dist/esm/elements/FieldHelpers.js.map +1 -1
  117. package/dist/esm/elements/Footer.js +39 -53
  118. package/dist/esm/elements/Footer.js.map +1 -1
  119. package/dist/esm/elements/FootnoteManager.js.map +1 -1
  120. package/dist/esm/elements/Header.js +39 -53
  121. package/dist/esm/elements/Header.js.map +1 -1
  122. package/dist/esm/elements/HeaderFooterManager.js +36 -6
  123. package/dist/esm/elements/HeaderFooterManager.js.map +1 -1
  124. package/dist/esm/elements/Hyperlink.js +69 -44
  125. package/dist/esm/elements/Hyperlink.js.map +1 -1
  126. package/dist/esm/elements/Image.js +33 -6
  127. package/dist/esm/elements/Image.js.map +1 -1
  128. package/dist/esm/elements/ImageManager.js +11 -0
  129. package/dist/esm/elements/ImageManager.js.map +1 -1
  130. package/dist/esm/elements/Paragraph.js +91 -26
  131. package/dist/esm/elements/Paragraph.js.map +1 -1
  132. package/dist/esm/elements/Revision.js +369 -29
  133. package/dist/esm/elements/Revision.js.map +1 -1
  134. package/dist/esm/elements/Run.js +24 -4
  135. package/dist/esm/elements/Run.js.map +1 -1
  136. package/dist/esm/elements/Section.js +17 -7
  137. package/dist/esm/elements/Section.js.map +1 -1
  138. package/dist/esm/elements/Shape.js +50 -42
  139. package/dist/esm/elements/Shape.js.map +1 -1
  140. package/dist/esm/elements/StructuredDocumentTag.js +46 -6
  141. package/dist/esm/elements/StructuredDocumentTag.js.map +1 -1
  142. package/dist/esm/elements/Table.js +90 -42
  143. package/dist/esm/elements/Table.js.map +1 -1
  144. package/dist/esm/elements/TableCell.js +32 -13
  145. package/dist/esm/elements/TableCell.js.map +1 -1
  146. package/dist/esm/elements/TableOfContents.js +5 -7
  147. package/dist/esm/elements/TableOfContents.js.map +1 -1
  148. package/dist/esm/elements/TableRow.js +16 -1
  149. package/dist/esm/elements/TableRow.js.map +1 -1
  150. package/dist/esm/elements/TextBox.js +55 -42
  151. package/dist/esm/elements/TextBox.js.map +1 -1
  152. package/dist/esm/formatting/AbstractNumbering.js +1 -1
  153. package/dist/esm/formatting/AbstractNumbering.js.map +1 -1
  154. package/dist/esm/formatting/NumberingLevel.js +8 -6
  155. package/dist/esm/formatting/NumberingLevel.js.map +1 -1
  156. package/dist/esm/formatting/NumberingManager.js +16 -1
  157. package/dist/esm/formatting/NumberingManager.js.map +1 -1
  158. package/dist/esm/formatting/Style.js +14 -0
  159. package/dist/esm/formatting/Style.js.map +1 -1
  160. package/dist/esm/formatting/StylesManager.js +14 -2
  161. package/dist/esm/formatting/StylesManager.js.map +1 -1
  162. package/dist/esm/helpers/CleanupHelper.js +103 -26
  163. package/dist/esm/helpers/CleanupHelper.js.map +1 -1
  164. package/dist/esm/images/ImageOptimizer.js +52 -4
  165. package/dist/esm/images/ImageOptimizer.js.map +1 -1
  166. package/dist/esm/index.js +1 -0
  167. package/dist/esm/index.js.map +1 -1
  168. package/dist/esm/internal.js +1 -1
  169. package/dist/esm/internal.js.map +1 -1
  170. package/dist/esm/processors/CompatibilityUpgrader.js +7 -1
  171. package/dist/esm/processors/CompatibilityUpgrader.js.map +1 -1
  172. package/dist/esm/processors/InMemoryRevisionAcceptor.js +134 -13
  173. package/dist/esm/processors/InMemoryRevisionAcceptor.js.map +1 -1
  174. package/dist/esm/processors/RevisionWalker.js +58 -8
  175. package/dist/esm/processors/RevisionWalker.js.map +1 -1
  176. package/dist/esm/processors/SelectiveRevisionAcceptor.js +116 -17
  177. package/dist/esm/processors/SelectiveRevisionAcceptor.js.map +1 -1
  178. package/dist/esm/processors/cnfStyleDecoder.js +7 -7
  179. package/dist/esm/processors/cnfStyleDecoder.js.map +1 -1
  180. package/dist/esm/processors/stripTrackedChanges.js +16 -5
  181. package/dist/esm/processors/stripTrackedChanges.js.map +1 -1
  182. package/dist/esm/utils/deepClone.js +24 -11
  183. package/dist/esm/utils/deepClone.js.map +1 -1
  184. package/dist/esm/utils/units.js +1 -1
  185. package/dist/esm/utils/units.js.map +1 -1
  186. package/dist/esm/utils/validation.js +11 -23
  187. package/dist/esm/utils/validation.js.map +1 -1
  188. package/dist/esm/utils/xmlSanitization.js +131 -7
  189. package/dist/esm/utils/xmlSanitization.js.map +1 -1
  190. package/dist/esm/validation/RevisionAutoFixer.js +69 -55
  191. package/dist/esm/validation/RevisionAutoFixer.js.map +1 -1
  192. package/dist/esm/validation/RevisionValidator.js +24 -20
  193. package/dist/esm/validation/RevisionValidator.js.map +1 -1
  194. package/dist/esm/xml/XMLBuilder.js +52 -20
  195. package/dist/esm/xml/XMLBuilder.js.map +1 -1
  196. package/dist/esm/xml/XMLParser.js +55 -13
  197. package/dist/esm/xml/XMLParser.js.map +1 -1
  198. package/dist/esm/zip/ZipHandler.js +37 -13
  199. package/dist/esm/zip/ZipHandler.js.map +1 -1
  200. package/dist/esm/zip/ZipReader.js +53 -6
  201. package/dist/esm/zip/ZipReader.js.map +1 -1
  202. package/dist/esm/zip/errors.js +7 -0
  203. package/dist/esm/zip/errors.js.map +1 -1
  204. package/dist/esm/zip/types.js +4 -0
  205. package/dist/esm/zip/types.js.map +1 -1
  206. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  207. package/dist/formatting/AbstractNumbering.js +1 -1
  208. package/dist/formatting/AbstractNumbering.js.map +1 -1
  209. package/dist/formatting/NumberingLevel.d.ts +1 -1
  210. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  211. package/dist/formatting/NumberingLevel.js +8 -6
  212. package/dist/formatting/NumberingLevel.js.map +1 -1
  213. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  214. package/dist/formatting/NumberingManager.js +16 -1
  215. package/dist/formatting/NumberingManager.js.map +1 -1
  216. package/dist/formatting/Style.d.ts.map +1 -1
  217. package/dist/formatting/Style.js +14 -0
  218. package/dist/formatting/Style.js.map +1 -1
  219. package/dist/formatting/StylesManager.d.ts +2 -0
  220. package/dist/formatting/StylesManager.d.ts.map +1 -1
  221. package/dist/formatting/StylesManager.js +14 -2
  222. package/dist/formatting/StylesManager.js.map +1 -1
  223. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  224. package/dist/helpers/CleanupHelper.js +103 -26
  225. package/dist/helpers/CleanupHelper.js.map +1 -1
  226. package/dist/images/ImageOptimizer.d.ts.map +1 -1
  227. package/dist/images/ImageOptimizer.js +52 -4
  228. package/dist/images/ImageOptimizer.js.map +1 -1
  229. package/dist/index.d.ts +3 -1
  230. package/dist/index.d.ts.map +1 -1
  231. package/dist/index.js +4 -2
  232. package/dist/index.js.map +1 -1
  233. package/dist/internal.d.ts +1 -1
  234. package/dist/internal.d.ts.map +1 -1
  235. package/dist/internal.js +2 -1
  236. package/dist/internal.js.map +1 -1
  237. package/dist/processors/CompatibilityUpgrader.d.ts.map +1 -1
  238. package/dist/processors/CompatibilityUpgrader.js +7 -1
  239. package/dist/processors/CompatibilityUpgrader.js.map +1 -1
  240. package/dist/processors/InMemoryRevisionAcceptor.d.ts +1 -0
  241. package/dist/processors/InMemoryRevisionAcceptor.d.ts.map +1 -1
  242. package/dist/processors/InMemoryRevisionAcceptor.js +135 -13
  243. package/dist/processors/InMemoryRevisionAcceptor.js.map +1 -1
  244. package/dist/processors/RevisionWalker.d.ts +2 -0
  245. package/dist/processors/RevisionWalker.d.ts.map +1 -1
  246. package/dist/processors/RevisionWalker.js +58 -8
  247. package/dist/processors/RevisionWalker.js.map +1 -1
  248. package/dist/processors/SelectiveRevisionAcceptor.d.ts.map +1 -1
  249. package/dist/processors/SelectiveRevisionAcceptor.js +116 -17
  250. package/dist/processors/SelectiveRevisionAcceptor.js.map +1 -1
  251. package/dist/processors/cnfStyleDecoder.d.ts.map +1 -1
  252. package/dist/processors/cnfStyleDecoder.js +7 -7
  253. package/dist/processors/cnfStyleDecoder.js.map +1 -1
  254. package/dist/processors/stripTrackedChanges.d.ts.map +1 -1
  255. package/dist/processors/stripTrackedChanges.js +16 -5
  256. package/dist/processors/stripTrackedChanges.js.map +1 -1
  257. package/dist/utils/deepClone.d.ts.map +1 -1
  258. package/dist/utils/deepClone.js +24 -11
  259. package/dist/utils/deepClone.js.map +1 -1
  260. package/dist/utils/units.d.ts.map +1 -1
  261. package/dist/utils/units.js +1 -1
  262. package/dist/utils/units.js.map +1 -1
  263. package/dist/utils/validation.d.ts.map +1 -1
  264. package/dist/utils/validation.js +11 -23
  265. package/dist/utils/validation.js.map +1 -1
  266. package/dist/utils/xmlSanitization.d.ts +2 -0
  267. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  268. package/dist/utils/xmlSanitization.js +133 -7
  269. package/dist/utils/xmlSanitization.js.map +1 -1
  270. package/dist/validation/RevisionAutoFixer.d.ts +6 -2
  271. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  272. package/dist/validation/RevisionAutoFixer.js +68 -54
  273. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  274. package/dist/validation/RevisionValidator.d.ts +2 -0
  275. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  276. package/dist/validation/RevisionValidator.js +26 -21
  277. package/dist/validation/RevisionValidator.js.map +1 -1
  278. package/dist/xml/XMLBuilder.d.ts +1 -0
  279. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  280. package/dist/xml/XMLBuilder.js +52 -20
  281. package/dist/xml/XMLBuilder.js.map +1 -1
  282. package/dist/xml/XMLParser.d.ts +1 -0
  283. package/dist/xml/XMLParser.d.ts.map +1 -1
  284. package/dist/xml/XMLParser.js +55 -13
  285. package/dist/xml/XMLParser.js.map +1 -1
  286. package/dist/zip/ZipHandler.d.ts.map +1 -1
  287. package/dist/zip/ZipHandler.js +37 -13
  288. package/dist/zip/ZipHandler.js.map +1 -1
  289. package/dist/zip/ZipReader.d.ts.map +1 -1
  290. package/dist/zip/ZipReader.js +52 -5
  291. package/dist/zip/ZipReader.js.map +1 -1
  292. package/dist/zip/errors.d.ts +3 -0
  293. package/dist/zip/errors.d.ts.map +1 -1
  294. package/dist/zip/errors.js +9 -1
  295. package/dist/zip/errors.js.map +1 -1
  296. package/dist/zip/types.d.ts +4 -0
  297. package/dist/zip/types.d.ts.map +1 -1
  298. package/dist/zip/types.js +4 -0
  299. package/dist/zip/types.js.map +1 -1
  300. package/package.json +2 -2
  301. package/src/core/Document.ts +1159 -240
  302. package/src/core/DocumentGenerator.ts +106 -26
  303. package/src/core/DocumentParser.ts +692 -120
  304. package/src/core/RelationshipManager.ts +446 -442
  305. package/src/elements/Comment.ts +123 -18
  306. package/src/elements/CommentManager.ts +121 -22
  307. package/src/elements/EndnoteManager.ts +4 -1
  308. package/src/elements/Field.ts +134 -43
  309. package/src/elements/FieldHelpers.ts +20 -4
  310. package/src/elements/Footer.ts +62 -67
  311. package/src/elements/FootnoteManager.ts +4 -1
  312. package/src/elements/Header.ts +62 -67
  313. package/src/elements/HeaderFooterManager.ts +56 -8
  314. package/src/elements/Hyperlink.ts +150 -55
  315. package/src/elements/Image.ts +51 -7
  316. package/src/elements/ImageManager.ts +24 -0
  317. package/src/elements/Paragraph.ts +161 -40
  318. package/src/elements/Revision.ts +446 -47
  319. package/src/elements/Run.ts +44 -6
  320. package/src/elements/Section.ts +25 -9
  321. package/src/elements/Shape.ts +52 -44
  322. package/src/elements/StructuredDocumentTag.ts +83 -8
  323. package/src/elements/Table.ts +151 -50
  324. package/src/elements/TableCell.ts +57 -17
  325. package/src/elements/TableOfContents.ts +10 -9
  326. package/src/elements/TableRow.ts +35 -3
  327. package/src/elements/TextBox.ts +58 -43
  328. package/src/formatting/AbstractNumbering.ts +4 -2
  329. package/src/formatting/NumberingLevel.ts +50 -12
  330. package/src/formatting/NumberingManager.ts +32 -3
  331. package/src/formatting/Style.ts +14 -0
  332. package/src/formatting/StylesManager.ts +29 -2
  333. package/src/helpers/CleanupHelper.ts +122 -30
  334. package/src/images/ImageOptimizer.ts +64 -5
  335. package/src/index.ts +7 -0
  336. package/src/internal.ts +1 -0
  337. package/src/processors/CompatibilityUpgrader.ts +15 -2
  338. package/src/processors/InMemoryRevisionAcceptor.ts +276 -17
  339. package/src/processors/RevisionWalker.ts +83 -7
  340. package/src/processors/SelectiveRevisionAcceptor.ts +164 -28
  341. package/src/processors/cnfStyleDecoder.ts +18 -12
  342. package/src/processors/stripTrackedChanges.ts +47 -10
  343. package/src/utils/deepClone.ts +48 -14
  344. package/src/utils/units.ts +3 -1
  345. package/src/utils/validation.ts +30 -32
  346. package/src/utils/xmlSanitization.ts +218 -22
  347. package/src/validation/RevisionAutoFixer.ts +109 -65
  348. package/src/validation/RevisionValidator.ts +48 -23
  349. package/src/xml/XMLBuilder.ts +82 -22
  350. package/src/xml/XMLParser.ts +87 -20
  351. package/src/zip/ZipHandler.ts +61 -21
  352. package/src/zip/ZipReader.ts +135 -13
  353. package/src/zip/errors.ts +15 -0
  354. package/src/zip/types.ts +44 -2
@@ -15,10 +15,11 @@
15
15
  import type { Document } from '../core/Document.js';
16
16
  import type { Paragraph, ParagraphContent } from '../elements/Paragraph.js';
17
17
  import { Revision, RevisionType } from '../elements/Revision.js';
18
- import type { Run } from '../elements/Run.js';
18
+ import type { Run, RunFormatting } from '../elements/Run.js';
19
19
  import { isRunContent, isHyperlinkContent } from '../elements/RevisionContent.js';
20
20
  import { ChangeCategory } from './ChangelogGenerator.js';
21
21
  import { SelectionCriteria } from './RevisionAwareProcessor.js';
22
+ import { mergeParagraphsWithDeletedMarks } from './InMemoryRevisionAcceptor.js';
22
23
 
23
24
  /**
24
25
  * Result of selective revision acceptance.
@@ -62,9 +63,23 @@ export class SelectiveRevisionAcceptor {
62
63
  const hasFullApi = typeof (doc as any).getAllParagraphs === 'function';
63
64
 
64
65
  if (hasFullApi) {
66
+ const acceptedMarkDeletions: Paragraph[] = [];
65
67
  this.walkAllParagraphs(doc, (paragraph) => {
66
- this.processSelectiveParagraph(paragraph, criteria, 'accept', accepted, remaining);
68
+ this.processSelectiveParagraph(
69
+ paragraph,
70
+ criteria,
71
+ 'accept',
72
+ accepted,
73
+ remaining,
74
+ acceptedMarkDeletions
75
+ );
67
76
  });
77
+ // Accepted paragraph-mark deletions combine the paragraph with its
78
+ // following sibling (ECMA-376 §17.13.5.15) — same merge the all-or-
79
+ // nothing acceptor performs.
80
+ if (acceptedMarkDeletions.length > 0) {
81
+ mergeParagraphsWithDeletedMarks(doc, new Set(acceptedMarkDeletions));
82
+ }
68
83
  } else {
69
84
  // Fallback: Filter revisions from RevisionManager only (for backward compatibility)
70
85
  const revisionManager = doc.getRevisionManager();
@@ -257,7 +272,8 @@ export class SelectiveRevisionAcceptor {
257
272
  criteria: SelectionCriteria,
258
273
  action: 'accept' | 'reject',
259
274
  processedIds: string[],
260
- remainingIds: string[]
275
+ remainingIds: string[],
276
+ acceptedMarkDeletions?: Paragraph[]
261
277
  ): void {
262
278
  const content = paragraph.getContent();
263
279
  const newContent: ParagraphContent[] = [];
@@ -267,15 +283,18 @@ export class SelectiveRevisionAcceptor {
267
283
  const revisionId = item.getId().toString();
268
284
 
269
285
  if (this.matchesCriteria(item, criteria)) {
270
- // This revision matches the criteria - process it
271
- processedIds.push(revisionId);
272
-
273
286
  if (action === 'accept') {
274
287
  // Accept: Transform based on revision type
288
+ processedIds.push(revisionId);
275
289
  this.acceptRevisionItem(item, newContent);
276
- } else {
290
+ } else if (this.rejectRevisionItem(item, newContent, paragraph)) {
277
291
  // Reject: Transform opposite of accept
278
- this.rejectRevisionItem(item, newContent);
292
+ processedIds.push(revisionId);
293
+ } else {
294
+ // The revision was kept in place (its previous-property
295
+ // snapshot could not be restored), so report it as remaining
296
+ // rather than rejected — it must stay in the RevisionManager.
297
+ remainingIds.push(revisionId);
279
298
  }
280
299
  } else {
281
300
  // This revision doesn't match - keep it
@@ -291,13 +310,18 @@ export class SelectiveRevisionAcceptor {
291
310
  // Replace paragraph content with the transformed content
292
311
  paragraph.setContent(newContent);
293
312
 
294
- // Handle paragraph mark revision markers (w:del/w:ins in w:pPr/w:rPr)
295
- // Both accept and reject clear the marker these are metadata-only markers (no content),
296
- // so there is no content to add or remove, only the marker itself to clear.
313
+ // Handle paragraph mark revision markers (w:del/w:ins in w:pPr/w:rPr).
314
+ // Rejecting a mark deletion just restores the mark (clear the marker);
315
+ // ACCEPTING one means the paragraph's contents combine with the following
316
+ // paragraph (ECMA-376 §17.13.5.15), so the paragraph is recorded for the
317
+ // post-walk merge pass in addition to clearing the marker.
297
318
  const formatting = paragraph.getFormatting();
298
319
  if (formatting.paragraphMarkDeletion) {
299
320
  const del = formatting.paragraphMarkDeletion;
300
321
  if (this.matchesMarkerCriteria(del, criteria)) {
322
+ if (action === 'accept') {
323
+ acceptedMarkDeletions?.push(paragraph);
324
+ }
301
325
  paragraph.clearParagraphMarkDeletion();
302
326
  processedIds.push(del.id.toString());
303
327
  } else {
@@ -415,8 +439,17 @@ export class SelectiveRevisionAcceptor {
415
439
  * Reject a single revision item (opposite of accept).
416
440
  * - Rejecting an insertion removes the content
417
441
  * - Rejecting a deletion keeps the content (unwraps it)
442
+ * - Rejecting a run/paragraph property change restores the previous
443
+ * properties snapshot (ECMA-376 §17.13.5.30/§17.13.5.31)
444
+ *
445
+ * @returns true if the revision was rejected, false if it was kept in
446
+ * place because its previous-property snapshot could not be restored
418
447
  */
419
- private static rejectRevisionItem(revision: Revision, newContent: ParagraphContent[]): void {
448
+ private static rejectRevisionItem(
449
+ revision: Revision,
450
+ newContent: ParagraphContent[],
451
+ paragraph: Paragraph
452
+ ): boolean {
420
453
  const revisionType = revision.getType();
421
454
  const childContent = revision.getContent();
422
455
 
@@ -424,7 +457,7 @@ export class SelectiveRevisionAcceptor {
424
457
  case 'insert':
425
458
  case 'moveTo':
426
459
  // Reject insertion: Remove the inserted content entirely
427
- break;
460
+ return true;
428
461
 
429
462
  case 'delete':
430
463
  case 'moveFrom':
@@ -436,19 +469,46 @@ export class SelectiveRevisionAcceptor {
436
469
  newContent.push(child);
437
470
  }
438
471
  }
439
- break;
472
+ return true;
473
+
474
+ case 'runPropertiesChange': {
475
+ const previous = revision.getPreviousProperties();
476
+ if (!previous) {
477
+ // No snapshot to restore from — keep the tracked change so the
478
+ // prior formatting is not silently lost.
479
+ newContent.push(revision);
480
+ return false;
481
+ }
482
+ for (const child of childContent) {
483
+ if (isRunContent(child)) {
484
+ const run = child as Run;
485
+ // w:rPrChange carries the complete pre-change run properties,
486
+ // so rejecting replaces (not merges) the run's formatting.
487
+ // Run.formatting is private; written directly the same way
488
+ // DocumentParser populates parsed properties.
489
+ (run as unknown as { formatting: RunFormatting }).formatting = {
490
+ ...(previous as RunFormatting),
491
+ };
492
+ newContent.push(run);
493
+ } else if (isHyperlinkContent(child)) {
494
+ newContent.push(child);
495
+ }
496
+ }
497
+ return true;
498
+ }
440
499
 
441
- case 'runPropertiesChange':
442
- case 'paragraphPropertiesChange':
443
- case 'tablePropertiesChange':
444
- case 'tableExceptionPropertiesChange':
445
- case 'tableRowPropertiesChange':
446
- case 'tableCellPropertiesChange':
447
- case 'sectionPropertiesChange':
448
- case 'numberingChange':
449
- // Rejecting property changes: Would need to restore old properties
450
- // For now, just keep content without the change metadata
451
- // (Full implementation would restore previousProperties)
500
+ case 'paragraphPropertiesChange': {
501
+ const previous = revision.getPreviousProperties();
502
+ if (!previous) {
503
+ newContent.push(revision);
504
+ return false;
505
+ }
506
+ // The snapshot uses the same key names as ParagraphFormatting
507
+ // (see Revision.PROPERTY_KEY_TO_ELEMENT), so the recorded values
508
+ // are written back onto the containing paragraph. Only recorded
509
+ // keys are touched: internal bookkeeping fields (revision markers,
510
+ // paraId) are not part of a w:pPrChange snapshot.
511
+ Object.assign(paragraph.formatting, previous);
452
512
  for (const child of childContent) {
453
513
  if (isRunContent(child)) {
454
514
  newContent.push(child as Run);
@@ -456,11 +516,26 @@ export class SelectiveRevisionAcceptor {
456
516
  newContent.push(child);
457
517
  }
458
518
  }
459
- break;
519
+ return true;
520
+ }
521
+
522
+ case 'tablePropertiesChange':
523
+ case 'tableExceptionPropertiesChange':
524
+ case 'tableRowPropertiesChange':
525
+ case 'tableCellPropertiesChange':
526
+ case 'sectionPropertiesChange':
527
+ case 'numberingChange':
528
+ // The element these snapshots belong to (table/row/cell/section/
529
+ // numbering definition) is not reachable from paragraph content,
530
+ // so the previous properties cannot be restored here. Keep the
531
+ // revision in place rather than discarding the snapshot.
532
+ newContent.push(revision);
533
+ return false;
460
534
 
461
535
  default:
462
536
  // Unknown type - keep the revision as-is for safety
463
537
  newContent.push(revision);
538
+ return true;
464
539
  }
465
540
  }
466
541
 
@@ -477,8 +552,69 @@ export class SelectiveRevisionAcceptor {
477
552
  criteria: SelectionCriteria,
478
553
  action: 'accept' | 'reject'
479
554
  ): SelectiveAcceptResult {
480
- // Preview is the same as the actual operation but without side effects
481
- return action === 'accept' ? this.accept(doc, criteria) : this.reject(doc, criteria);
555
+ const processed: string[] = [];
556
+ const remaining: string[] = [];
557
+
558
+ // Delegating to accept()/reject() would mutate paragraph content and
559
+ // splice revisions out of the RevisionManager, so preview classifies
560
+ // with the same traversal and criteria but never transforms anything.
561
+ const hasFullApi = typeof (doc as any).getAllParagraphs === 'function';
562
+
563
+ if (hasFullApi) {
564
+ this.walkAllParagraphs(doc, (paragraph) => {
565
+ for (const item of paragraph.getContent()) {
566
+ if (item instanceof Revision) {
567
+ if (this.matchesCriteria(item, criteria)) {
568
+ processed.push(item.getId().toString());
569
+ } else {
570
+ remaining.push(item.getId().toString());
571
+ }
572
+ }
573
+ }
574
+
575
+ const formatting = paragraph.getFormatting();
576
+ if (formatting.paragraphMarkDeletion) {
577
+ const del = formatting.paragraphMarkDeletion;
578
+ if (this.matchesMarkerCriteria(del, criteria)) {
579
+ processed.push(del.id.toString());
580
+ } else {
581
+ remaining.push(del.id.toString());
582
+ }
583
+ }
584
+ if (formatting.paragraphMarkInsertion) {
585
+ const ins = formatting.paragraphMarkInsertion;
586
+ if (this.matchesMarkerCriteria(ins, criteria)) {
587
+ processed.push(ins.id.toString());
588
+ } else {
589
+ remaining.push(ins.id.toString());
590
+ }
591
+ }
592
+ });
593
+ } else {
594
+ // Fallback: Filter revisions from RevisionManager only (for backward compatibility)
595
+ const revisionManager = doc.getRevisionManager();
596
+ if (revisionManager) {
597
+ for (const rev of revisionManager.getAllRevisions()) {
598
+ if (this.matchesCriteria(rev, criteria)) {
599
+ processed.push(rev.getId().toString());
600
+ } else {
601
+ remaining.push(rev.getId().toString());
602
+ }
603
+ }
604
+ }
605
+ }
606
+
607
+ return {
608
+ accepted: action === 'accept' ? processed : [],
609
+ rejected: action === 'reject' ? processed : [],
610
+ remaining,
611
+ summary: {
612
+ totalProcessed: processed.length + remaining.length,
613
+ acceptedCount: action === 'accept' ? processed.length : 0,
614
+ rejectedCount: action === 'reject' ? processed.length : 0,
615
+ remainingCount: remaining.length,
616
+ },
617
+ };
482
618
  }
483
619
 
484
620
  /**
@@ -78,25 +78,31 @@ const BIT_POSITIONS: Record<keyof CnfStyleFlags, number> = {
78
78
 
79
79
  /**
80
80
  * Priority order for conditional formatting resolution
81
- * More specific conditions (corners) override less specific (edges, banding)
82
- * Per ECMA-376, this is the order in which conditionals should be applied
81
+ *
82
+ * ECMA-376 §17.7.6.6 applies conditional formats in sequence (banding, then
83
+ * first/last row, first/last column, then corners top-left through
84
+ * bottom-right) with subsequent formats overriding earlier ones. First-match
85
+ * consumers need the opposite: the last-applied (winning) member of each
86
+ * group listed first, so when two conditionals in the same group co-apply
87
+ * (e.g. firstRow + lastRow in a single-row table) resolution picks the one
88
+ * Word actually renders.
83
89
  */
84
90
  export const CONDITIONAL_PRIORITY_ORDER: (keyof CnfStyleFlags)[] = [
85
- // Corner cells (most specific)
86
- 'nwCell',
87
- 'neCell',
88
- 'swCell',
91
+ // Corner cells (most specific; spec applies nw, ne, sw, se — se wins)
89
92
  'seCell',
90
- // Edge rows/columns
91
- 'firstRow',
93
+ 'swCell',
94
+ 'neCell',
95
+ 'nwCell',
96
+ // Edge rows/columns (within each pair, last is applied after first)
92
97
  'lastRow',
93
- 'firstCol',
98
+ 'firstRow',
94
99
  'lastCol',
95
- // Banding (least specific)
96
- 'band1Horz',
100
+ 'firstCol',
101
+ // Banding (least specific; even banding is applied after odd)
97
102
  'band2Horz',
98
- 'band1Vert',
103
+ 'band1Horz',
99
104
  'band2Vert',
105
+ 'band1Vert',
100
106
  ];
101
107
 
102
108
  /**
@@ -67,19 +67,30 @@ class TrackedChangesStripper {
67
67
  // STEP 2: Process insertions - keep content, remove wrapper
68
68
  content = this.processInsertions(content);
69
69
 
70
- // STEP 3: Process deletions - remove entirely
70
+ // STEP 3: Remove rows whose w:trPr carries a row-deletion marker
71
+ // (self-closing <w:del/> per ECMA-376 §17.13.5.14 marks the ENTIRE
72
+ // row as deleted). Must run before the marker itself is stripped in
73
+ // STEP 4, otherwise the row would silently survive as un-deleted.
74
+ content = this.removeDeletedRows(content);
75
+
76
+ // STEP 4: Remove self-closing revision markers (paragraph-mark
77
+ // deletions in w:pPr/w:rPr per ECMA-376 §17.13.5.15, etc.). Must run
78
+ // BEFORE the block-removal steps: their [^>]* also matches the
79
+ // trailing '/' of `<w:del .../>`, which would turn the marker into an
80
+ // opening tag and swallow everything up to the next closing tag,
81
+ // corrupting the part.
82
+ content = this.removeSelfClosingRevisionTags(content);
83
+
84
+ // STEP 5: Process deletions - remove entirely
71
85
  content = this.processDeletions(content);
72
86
 
73
- // STEP 4: Process move operations
87
+ // STEP 6: Process move operations
74
88
  content = this.processMoveFrom(content);
75
89
  content = this.processMoveTo(content);
76
90
 
77
- // STEP 5: Remove all property change tracking elements
91
+ // STEP 7: Remove all property change tracking elements
78
92
  content = this.removePropertyChanges(content);
79
93
 
80
- // STEP 6: Remove any remaining self-closing revision tags
81
- content = this.removeSelfClosingRevisionTags(content);
82
-
83
94
  // Update the file in the zip
84
95
  this.zipHandler.updateFile(partPath, content);
85
96
  }
@@ -160,6 +171,30 @@ class TrackedChangesStripper {
160
171
  return result;
161
172
  }
162
173
 
174
+ /**
175
+ * Remove entire <w:tr> rows marked as tracked deletions.
176
+ *
177
+ * Per ECMA-376 Part 1 §17.13.5.14, a self-closing `<w:del/>` inside
178
+ * `<w:trPr>` marks the whole row as deleted — stripping just the marker
179
+ * would resurrect the row instead of removing it. Mirrors the row-level
180
+ * pass in acceptRevisions.ts: the negative lookaheads keep the match
181
+ * anchored to a single row (it cannot slip past a cell boundary or a
182
+ * nested-table row and latch onto a later row's trPr-with-del).
183
+ */
184
+ private removeDeletedRows(xml: string): string {
185
+ const rowDelPattern =
186
+ /<w:tr\b[^>]*>(?:(?!<w:tc\b|<w:tr\b|<\/w:tr>)[\s\S])*?<w:trPr\b(?:\s[^>]*)?>(?:(?!<\/w:trPr>)[\s\S])*?<w:del\b[^>]*\/>(?:(?!<\/w:trPr>)[\s\S])*?<\/w:trPr>(?:(?!<w:tr\b|<\/w:tr>)[\s\S])*?<\/w:tr>/g;
187
+
188
+ let result = xml;
189
+ let previousLength = 0;
190
+ while (result.length !== previousLength) {
191
+ previousLength = result.length;
192
+ result = result.replace(rowDelPattern, '');
193
+ }
194
+
195
+ return result;
196
+ }
197
+
163
198
  /**
164
199
  * Process <w:del> elements - remove entire element
165
200
  * Uses iterative replacement to handle nested deletions
@@ -241,13 +276,15 @@ class TrackedChangesStripper {
241
276
 
242
277
  /**
243
278
  * Remove self-closing revision tags
279
+ * `\b` (not `\s+`) so attribute-less markers like `<w:del/>` are also
280
+ * caught while `<w:delText>` is not.
244
281
  */
245
282
  private removeSelfClosingRevisionTags(xml: string): string {
246
283
  const patterns = [
247
- /<w:ins\s+[^>]*\/>/g,
248
- /<w:del\s+[^>]*\/>/g,
249
- /<w:moveFrom\s+[^>]*\/>/g,
250
- /<w:moveTo\s+[^>]*\/>/g,
284
+ /<w:ins\b[^>]*\/>/g,
285
+ /<w:del\b[^>]*\/>/g,
286
+ /<w:moveFrom\b[^>]*\/>/g,
287
+ /<w:moveTo\b[^>]*\/>/g,
251
288
  ];
252
289
 
253
290
  let result = xml;
@@ -10,6 +10,12 @@
10
10
  * For simple objects (like formatting options), this is more efficient
11
11
  * than JSON.parse(JSON.stringify()) and doesn't lose non-serializable values.
12
12
  *
13
+ * Cyclic and shared references are handled via an internal seen-cache: an
14
+ * object encountered more than once during a single clone is cloned exactly
15
+ * once, so circular graphs cannot overflow the stack and shared sub-objects
16
+ * (including shared Date and RegExp instances) retain a single shared identity
17
+ * in the result.
18
+ *
13
19
  * @param obj - Object to clone
14
20
  * @returns Deep cloned copy of the object
15
21
  *
@@ -23,21 +29,50 @@
23
29
  * ```
24
30
  */
25
31
  export function deepClone<T>(obj: T): T {
32
+ // Map of already-cloned source objects to their clones, scoped to this call.
33
+ // Guards against infinite recursion on cyclic input and preserves the
34
+ // identity of references shared by multiple parents within the same graph.
35
+ return cloneInternal(obj, new WeakMap<object, unknown>());
36
+ }
37
+
38
+ function cloneInternal<T>(obj: T, seen: WeakMap<object, unknown>): T {
26
39
  // Handle primitive types and null
27
40
  if (obj === null || typeof obj !== 'object') {
28
41
  return obj;
29
42
  }
30
43
 
31
- // Handle Date
44
+ // Return the existing clone for any object already seen in this call. Placed
45
+ // before the Date/RegExp branches so a single Date/RegExp instance shared by
46
+ // multiple nodes resolves to one shared clone (the JSDoc shared-identity
47
+ // guarantee), not a fresh copy per reference.
48
+ const existing = seen.get(obj as object);
49
+ if (existing !== undefined) {
50
+ return existing as T;
51
+ }
52
+
53
+ // Handle Date (immutable value copy; cannot form a cycle). Registered in
54
+ // `seen` so repeat references to the same instance share this clone.
32
55
  if (obj instanceof Date) {
33
- return new Date(obj.getTime()) as T;
56
+ const dateCopy = new Date(obj.getTime());
57
+ seen.set(obj as object, dateCopy);
58
+ return dateCopy as T;
59
+ }
60
+
61
+ // Handle RegExp (immutable value copy; cannot form a cycle). Registered in
62
+ // `seen` so repeat references to the same instance share this clone.
63
+ if (obj instanceof RegExp) {
64
+ const regexpCopy = new RegExp(obj.source, obj.flags);
65
+ seen.set(obj as object, regexpCopy);
66
+ return regexpCopy as T;
34
67
  }
35
68
 
36
69
  // Handle Array
37
70
  if (Array.isArray(obj)) {
38
71
  const arrCopy: unknown[] = [];
72
+ // Register before recursing so self/back references resolve to this clone.
73
+ seen.set(obj as object, arrCopy);
39
74
  for (let i = 0; i < obj.length; i++) {
40
- arrCopy[i] = deepClone(obj[i]);
75
+ arrCopy[i] = cloneInternal(obj[i], seen);
41
76
  }
42
77
  return arrCopy as T;
43
78
  }
@@ -45,8 +80,9 @@ export function deepClone<T>(obj: T): T {
45
80
  // Handle Map
46
81
  if (obj instanceof Map) {
47
82
  const mapCopy = new Map();
83
+ seen.set(obj, mapCopy);
48
84
  obj.forEach((value, key) => {
49
- mapCopy.set(deepClone(key), deepClone(value));
85
+ mapCopy.set(cloneInternal(key, seen), cloneInternal(value, seen));
50
86
  });
51
87
  return mapCopy as T;
52
88
  }
@@ -54,23 +90,21 @@ export function deepClone<T>(obj: T): T {
54
90
  // Handle Set
55
91
  if (obj instanceof Set) {
56
92
  const setCopy = new Set();
93
+ seen.set(obj, setCopy);
57
94
  obj.forEach((value) => {
58
- setCopy.add(deepClone(value));
95
+ setCopy.add(cloneInternal(value, seen));
59
96
  });
60
97
  return setCopy as T;
61
98
  }
62
99
 
63
- // Handle RegExp
64
- if (obj instanceof RegExp) {
65
- return new RegExp(obj.source, obj.flags) as T;
66
- }
67
-
68
100
  // Handle plain objects
69
101
  const objCopy = Object.create(Object.getPrototypeOf(obj)) as Record<string, unknown>;
70
- for (const key in obj) {
71
- if (obj.hasOwnProperty(key)) {
72
- objCopy[key] = deepClone((obj as Record<string, unknown>)[key]);
73
- }
102
+ seen.set(obj as object, objCopy);
103
+ // Object.keys avoids the prototype-builtins shadowing risk of
104
+ // obj.hasOwnProperty(key) (matches sibling deepEqual.ts) and yields the same
105
+ // own-enumerable string keys the previous for..in + hasOwnProperty loop did.
106
+ for (const key of Object.keys(obj as object)) {
107
+ objCopy[key] = cloneInternal((obj as Record<string, unknown>)[key], seen);
74
108
  }
75
109
 
76
110
  return objCopy as T;
@@ -145,7 +145,9 @@ export function emusToPixels(emus: number, dpi: number = STANDARD_DPI): number {
145
145
  * @returns Value in twips
146
146
  */
147
147
  export function pointsToTwips(points: number): number {
148
- return points * UNITS.TWIPS_PER_POINT;
148
+ // Round to nearest integer — twips attributes (w:spacing, w:ind) require
149
+ // ST_DecimalNumber-based integer values
150
+ return Math.round(points * UNITS.TWIPS_PER_POINT);
149
151
  }
150
152
 
151
153
  /**
@@ -23,29 +23,28 @@ export function validateDocxStructure(filePaths: string[]): void {
23
23
 
24
24
  /**
25
25
  * Checks if a file path represents a binary file based on extension
26
+ *
27
+ * Uses a text-extension whitelist rather than a binary-extension whitelist:
28
+ * misclassifying text as binary only changes its in-memory representation
29
+ * (Buffer instead of string), but misclassifying binary as text corrupts the
30
+ * bytes through a lossy UTF-8 decode. OOXML packages can embed arbitrary
31
+ * binary parts (.xlsx/.pdf OLE packages, .odttf obfuscated fonts, .emz/.wmz
32
+ * compressed metafiles, extensionless parts), so anything not known to be
33
+ * text must be preserved byte-for-byte.
34
+ *
26
35
  * @param filePath - The file path to check
27
36
  * @returns True if the file is likely binary
28
37
  */
29
38
  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
- ];
39
+ const textExtensions = ['.xml', '.rels', '.svg', '.txt', '.html', '.css', '.js'];
46
40
 
47
- const extension = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
48
- return binaryExtensions.includes(extension);
41
+ const dotIndex = filePath.lastIndexOf('.');
42
+ if (dotIndex === -1) {
43
+ return true;
44
+ }
45
+
46
+ const extension = filePath.substring(dotIndex).toLowerCase();
47
+ return !textExtensions.includes(extension);
49
48
  }
50
49
 
51
50
  /**
@@ -55,12 +54,15 @@ export function isBinaryFile(filePath: string): boolean {
55
54
  *
56
55
  * **Security:** This function validates paths to prevent:
57
56
  * - Path traversal attacks (../, ..\, URL-encoded variants)
58
- * - Absolute paths (C:\, /etc/, etc.)
57
+ * - Absolute Windows paths (C:\, D:, etc.) — rejected
59
58
  * - Malicious DOCX files attempting directory escape
60
59
  *
60
+ * Unix absolute paths (/etc/...) are sanitized to archive-relative paths
61
+ * by stripping leading slashes, not rejected.
62
+ *
61
63
  * @param path - The path to normalize
62
64
  * @returns Normalized path
63
- * @throws {Error} If path contains path traversal sequences, absolute paths, or URL-encoded attacks
65
+ * @throws {Error} If path contains path traversal sequences, absolute Windows paths, or URL-encoded attacks
64
66
  */
65
67
  export function normalizePath(path: string): string {
66
68
  // First convert all backslashes to forward slashes for consistent checking
@@ -97,15 +99,6 @@ export function normalizePath(path: string): string {
97
99
  );
98
100
  }
99
101
 
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
102
  return normalized;
110
103
  }
111
104
 
@@ -161,6 +154,11 @@ export function validateTwips(value: number, fieldName = 'value'): void {
161
154
  throw new Error(`${fieldName} must be a finite number, got ${value}`);
162
155
  }
163
156
 
157
+ // ST_TwipsMeasure/ST_SignedTwipsMeasure are ST_DecimalNumber-based — integers only
158
+ if (!Number.isInteger(value)) {
159
+ throw new Error(`${fieldName} must be an integer (in twips), got ${value}`);
160
+ }
161
+
164
162
  // Reasonable range: ±22 inches (31680 twips)
165
163
  const MIN_TWIPS = -31680;
166
164
  const MAX_TWIPS = 31680;
@@ -304,7 +302,7 @@ export function validateAlignment(
304
302
 
305
303
  /**
306
304
  * Validates a font size (in half-points for Word)
307
- * Reasonable range: 2-1638 (1-819 points)
305
+ * Reasonable range: 2-3276 (1-1638 points)
308
306
  * @param size - The font size in half-points to validate
309
307
  * @param fieldName - Name of the field (for error messages)
310
308
  * @throws {Error} If the size is invalid
@@ -318,9 +316,9 @@ export function validateFontSize(size: number, fieldName = 'font size'): void {
318
316
  throw new Error(`${fieldName} must be an integer (in half-points), got ${size}`);
319
317
  }
320
318
 
321
- // Reasonable range: 2-1638 half-points (1-819 points)
319
+ // Word's limit is 1-1638 points, i.e. 2-3276 half-points for w:sz (ST_HpsMeasure)
322
320
  const MIN_SIZE = 2;
323
- const MAX_SIZE = 1638;
321
+ const MAX_SIZE = 3276;
324
322
 
325
323
  if (size < MIN_SIZE || size > MAX_SIZE) {
326
324
  throw new Error(