docxmlater 11.0.10 → 12.1.0

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 (361) hide show
  1. package/README.md +53 -13
  2. package/dist/core/Document.d.ts +29 -3
  3. package/dist/core/Document.d.ts.map +1 -1
  4. package/dist/core/Document.js +733 -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 +427 -81
  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 +735 -172
  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 +427 -81
  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 +223 -39
  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/acceptRevisions.js +20 -11
  179. package/dist/esm/processors/acceptRevisions.js.map +1 -1
  180. package/dist/esm/processors/cnfStyleDecoder.js +7 -7
  181. package/dist/esm/processors/cnfStyleDecoder.js.map +1 -1
  182. package/dist/esm/processors/stripTrackedChanges.js +16 -5
  183. package/dist/esm/processors/stripTrackedChanges.js.map +1 -1
  184. package/dist/esm/utils/deepClone.js +24 -11
  185. package/dist/esm/utils/deepClone.js.map +1 -1
  186. package/dist/esm/utils/units.js +1 -1
  187. package/dist/esm/utils/units.js.map +1 -1
  188. package/dist/esm/utils/validation.js +11 -23
  189. package/dist/esm/utils/validation.js.map +1 -1
  190. package/dist/esm/utils/xmlSanitization.js +131 -7
  191. package/dist/esm/utils/xmlSanitization.js.map +1 -1
  192. package/dist/esm/validation/RevisionAutoFixer.js +69 -55
  193. package/dist/esm/validation/RevisionAutoFixer.js.map +1 -1
  194. package/dist/esm/validation/RevisionValidator.js +24 -20
  195. package/dist/esm/validation/RevisionValidator.js.map +1 -1
  196. package/dist/esm/xml/XMLBuilder.js +52 -20
  197. package/dist/esm/xml/XMLBuilder.js.map +1 -1
  198. package/dist/esm/xml/XMLParser.js +55 -13
  199. package/dist/esm/xml/XMLParser.js.map +1 -1
  200. package/dist/esm/zip/ZipHandler.js +37 -13
  201. package/dist/esm/zip/ZipHandler.js.map +1 -1
  202. package/dist/esm/zip/ZipReader.js +53 -6
  203. package/dist/esm/zip/ZipReader.js.map +1 -1
  204. package/dist/esm/zip/errors.js +7 -0
  205. package/dist/esm/zip/errors.js.map +1 -1
  206. package/dist/esm/zip/types.js +4 -0
  207. package/dist/esm/zip/types.js.map +1 -1
  208. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  209. package/dist/formatting/AbstractNumbering.js +1 -1
  210. package/dist/formatting/AbstractNumbering.js.map +1 -1
  211. package/dist/formatting/NumberingLevel.d.ts +1 -1
  212. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  213. package/dist/formatting/NumberingLevel.js +8 -6
  214. package/dist/formatting/NumberingLevel.js.map +1 -1
  215. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  216. package/dist/formatting/NumberingManager.js +16 -1
  217. package/dist/formatting/NumberingManager.js.map +1 -1
  218. package/dist/formatting/Style.d.ts.map +1 -1
  219. package/dist/formatting/Style.js +14 -0
  220. package/dist/formatting/Style.js.map +1 -1
  221. package/dist/formatting/StylesManager.d.ts +2 -0
  222. package/dist/formatting/StylesManager.d.ts.map +1 -1
  223. package/dist/formatting/StylesManager.js +14 -2
  224. package/dist/formatting/StylesManager.js.map +1 -1
  225. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  226. package/dist/helpers/CleanupHelper.js +103 -26
  227. package/dist/helpers/CleanupHelper.js.map +1 -1
  228. package/dist/images/ImageOptimizer.d.ts.map +1 -1
  229. package/dist/images/ImageOptimizer.js +52 -4
  230. package/dist/images/ImageOptimizer.js.map +1 -1
  231. package/dist/index.d.ts +3 -1
  232. package/dist/index.d.ts.map +1 -1
  233. package/dist/index.js +4 -2
  234. package/dist/index.js.map +1 -1
  235. package/dist/internal.d.ts +1 -1
  236. package/dist/internal.d.ts.map +1 -1
  237. package/dist/internal.js +2 -1
  238. package/dist/internal.js.map +1 -1
  239. package/dist/processors/CompatibilityUpgrader.d.ts.map +1 -1
  240. package/dist/processors/CompatibilityUpgrader.js +7 -1
  241. package/dist/processors/CompatibilityUpgrader.js.map +1 -1
  242. package/dist/processors/InMemoryRevisionAcceptor.d.ts +1 -0
  243. package/dist/processors/InMemoryRevisionAcceptor.d.ts.map +1 -1
  244. package/dist/processors/InMemoryRevisionAcceptor.js +135 -13
  245. package/dist/processors/InMemoryRevisionAcceptor.js.map +1 -1
  246. package/dist/processors/RevisionWalker.d.ts +9 -3
  247. package/dist/processors/RevisionWalker.d.ts.map +1 -1
  248. package/dist/processors/RevisionWalker.js +223 -39
  249. package/dist/processors/RevisionWalker.js.map +1 -1
  250. package/dist/processors/SelectiveRevisionAcceptor.d.ts.map +1 -1
  251. package/dist/processors/SelectiveRevisionAcceptor.js +116 -17
  252. package/dist/processors/SelectiveRevisionAcceptor.js.map +1 -1
  253. package/dist/processors/acceptRevisions.d.ts +1 -0
  254. package/dist/processors/acceptRevisions.d.ts.map +1 -1
  255. package/dist/processors/acceptRevisions.js +21 -11
  256. package/dist/processors/acceptRevisions.js.map +1 -1
  257. package/dist/processors/cnfStyleDecoder.d.ts.map +1 -1
  258. package/dist/processors/cnfStyleDecoder.js +7 -7
  259. package/dist/processors/cnfStyleDecoder.js.map +1 -1
  260. package/dist/processors/stripTrackedChanges.d.ts.map +1 -1
  261. package/dist/processors/stripTrackedChanges.js +16 -5
  262. package/dist/processors/stripTrackedChanges.js.map +1 -1
  263. package/dist/utils/deepClone.d.ts.map +1 -1
  264. package/dist/utils/deepClone.js +24 -11
  265. package/dist/utils/deepClone.js.map +1 -1
  266. package/dist/utils/units.d.ts.map +1 -1
  267. package/dist/utils/units.js +1 -1
  268. package/dist/utils/units.js.map +1 -1
  269. package/dist/utils/validation.d.ts.map +1 -1
  270. package/dist/utils/validation.js +11 -23
  271. package/dist/utils/validation.js.map +1 -1
  272. package/dist/utils/xmlSanitization.d.ts +2 -0
  273. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  274. package/dist/utils/xmlSanitization.js +133 -7
  275. package/dist/utils/xmlSanitization.js.map +1 -1
  276. package/dist/validation/RevisionAutoFixer.d.ts +6 -2
  277. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  278. package/dist/validation/RevisionAutoFixer.js +68 -54
  279. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  280. package/dist/validation/RevisionValidator.d.ts +2 -0
  281. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  282. package/dist/validation/RevisionValidator.js +26 -21
  283. package/dist/validation/RevisionValidator.js.map +1 -1
  284. package/dist/xml/XMLBuilder.d.ts +1 -0
  285. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  286. package/dist/xml/XMLBuilder.js +52 -20
  287. package/dist/xml/XMLBuilder.js.map +1 -1
  288. package/dist/xml/XMLParser.d.ts +1 -0
  289. package/dist/xml/XMLParser.d.ts.map +1 -1
  290. package/dist/xml/XMLParser.js +55 -13
  291. package/dist/xml/XMLParser.js.map +1 -1
  292. package/dist/zip/ZipHandler.d.ts.map +1 -1
  293. package/dist/zip/ZipHandler.js +37 -13
  294. package/dist/zip/ZipHandler.js.map +1 -1
  295. package/dist/zip/ZipReader.d.ts.map +1 -1
  296. package/dist/zip/ZipReader.js +52 -5
  297. package/dist/zip/ZipReader.js.map +1 -1
  298. package/dist/zip/errors.d.ts +3 -0
  299. package/dist/zip/errors.d.ts.map +1 -1
  300. package/dist/zip/errors.js +9 -1
  301. package/dist/zip/errors.js.map +1 -1
  302. package/dist/zip/types.d.ts +4 -0
  303. package/dist/zip/types.d.ts.map +1 -1
  304. package/dist/zip/types.js +4 -0
  305. package/dist/zip/types.js.map +1 -1
  306. package/package.json +2 -2
  307. package/src/core/Document.ts +1184 -242
  308. package/src/core/DocumentGenerator.ts +106 -26
  309. package/src/core/DocumentParser.ts +681 -114
  310. package/src/core/RelationshipManager.ts +446 -442
  311. package/src/elements/Comment.ts +123 -18
  312. package/src/elements/CommentManager.ts +121 -22
  313. package/src/elements/EndnoteManager.ts +4 -1
  314. package/src/elements/Field.ts +134 -43
  315. package/src/elements/FieldHelpers.ts +20 -4
  316. package/src/elements/Footer.ts +62 -67
  317. package/src/elements/FootnoteManager.ts +4 -1
  318. package/src/elements/Header.ts +62 -67
  319. package/src/elements/HeaderFooterManager.ts +56 -8
  320. package/src/elements/Hyperlink.ts +150 -55
  321. package/src/elements/Image.ts +51 -7
  322. package/src/elements/ImageManager.ts +24 -0
  323. package/src/elements/Paragraph.ts +161 -40
  324. package/src/elements/Revision.ts +446 -47
  325. package/src/elements/Run.ts +44 -6
  326. package/src/elements/Section.ts +25 -9
  327. package/src/elements/Shape.ts +52 -44
  328. package/src/elements/StructuredDocumentTag.ts +83 -8
  329. package/src/elements/Table.ts +151 -50
  330. package/src/elements/TableCell.ts +57 -17
  331. package/src/elements/TableOfContents.ts +10 -9
  332. package/src/elements/TableRow.ts +35 -3
  333. package/src/elements/TextBox.ts +58 -43
  334. package/src/formatting/AbstractNumbering.ts +4 -2
  335. package/src/formatting/NumberingLevel.ts +50 -12
  336. package/src/formatting/NumberingManager.ts +32 -3
  337. package/src/formatting/Style.ts +14 -0
  338. package/src/formatting/StylesManager.ts +29 -2
  339. package/src/helpers/CleanupHelper.ts +122 -30
  340. package/src/images/ImageOptimizer.ts +64 -5
  341. package/src/index.ts +7 -0
  342. package/src/internal.ts +1 -0
  343. package/src/processors/CompatibilityUpgrader.ts +15 -2
  344. package/src/processors/InMemoryRevisionAcceptor.ts +276 -17
  345. package/src/processors/RevisionWalker.ts +390 -46
  346. package/src/processors/SelectiveRevisionAcceptor.ts +164 -28
  347. package/src/processors/acceptRevisions.ts +67 -17
  348. package/src/processors/cnfStyleDecoder.ts +18 -12
  349. package/src/processors/stripTrackedChanges.ts +47 -10
  350. package/src/utils/deepClone.ts +48 -14
  351. package/src/utils/units.ts +3 -1
  352. package/src/utils/validation.ts +30 -32
  353. package/src/utils/xmlSanitization.ts +218 -22
  354. package/src/validation/RevisionAutoFixer.ts +109 -65
  355. package/src/validation/RevisionValidator.ts +48 -23
  356. package/src/xml/XMLBuilder.ts +82 -22
  357. package/src/xml/XMLParser.ts +87 -20
  358. package/src/zip/ZipHandler.ts +61 -21
  359. package/src/zip/ZipReader.ts +135 -13
  360. package/src/zip/errors.ts +15 -0
  361. package/src/zip/types.ts +44 -2
@@ -17,7 +17,11 @@ import {
17
17
  FixAction,
18
18
  ValidationIssue,
19
19
  } from './ValidationRules.js';
20
- import { RevisionValidator } from './RevisionValidator.js';
20
+ import {
21
+ CONTENTLESS_REVISION_TYPES,
22
+ RevisionValidator,
23
+ revisionHasContent,
24
+ } from './RevisionValidator.js';
21
25
 
22
26
  /**
23
27
  * Automatically fixes revision validation issues.
@@ -81,9 +85,16 @@ export class RevisionAutoFixer {
81
85
  actions.push(...this.fixMissingAuthors(revisions, defaultAuthor, options?.dryRun));
82
86
  }
83
87
 
84
- // Fix orphaned move markers (REV003, REV004)
88
+ // Fix orphaned move markers (REV003, REV004) — one pairing pass
89
+ // produces both rules, so gate each side individually to honor
90
+ // onlyRules/skipRules without mutating revisions the caller excluded.
85
91
  if (shouldProcess('REV003') || shouldProcess('REV004')) {
86
- actions.push(...this.fixOrphanedMoveMarkers(revisionManager, revisions, options?.dryRun));
92
+ actions.push(
93
+ ...this.fixOrphanedMoveMarkers(revisionManager, revisions, options?.dryRun, doc, {
94
+ moveFrom: shouldProcess('REV003'),
95
+ moveTo: shouldProcess('REV004'),
96
+ })
97
+ );
87
98
  }
88
99
 
89
100
  // Fix missing dates (REV101)
@@ -98,7 +109,7 @@ export class RevisionAutoFixer {
98
109
 
99
110
  // Fix empty revisions (REV103)
100
111
  if (shouldProcess('REV103')) {
101
- actions.push(...this.fixEmptyRevisions(revisionManager, revisions, options?.dryRun));
112
+ actions.push(...this.fixEmptyRevisions(revisionManager, revisions, options?.dryRun, doc));
102
113
  }
103
114
 
104
115
  // Fix non-sequential IDs (REV104)
@@ -297,20 +308,51 @@ export class RevisionAutoFixer {
297
308
  return actions;
298
309
  }
299
310
 
311
+ /**
312
+ * Removes a revision from the paragraph content that owns it.
313
+ *
314
+ * RevisionManager.removeById() only deregisters the revision from the
315
+ * manager registry, but serialization reads Paragraph.content directly —
316
+ * without this step the removed markup would still be written on save.
317
+ *
318
+ * @param doc - Document whose paragraphs are searched
319
+ * @param revision - Revision instance to remove
320
+ * @returns True if the revision was found and removed from a paragraph
321
+ */
322
+ private static removeRevisionFromContent(doc: Document, revision: Revision): boolean {
323
+ for (const paragraph of doc.getAllParagraphs()) {
324
+ // replaceContent matches by identity and splices without tracking side effects
325
+ if (paragraph.replaceContent(revision, [])) {
326
+ return true;
327
+ }
328
+ }
329
+ return false;
330
+ }
331
+
300
332
  /**
301
333
  * Fix orphaned move markers by removing them.
302
334
  *
303
335
  * @param revisionManager - RevisionManager instance
304
336
  * @param revisions - Array of revisions
305
337
  * @param dryRun - If true, only report changes without applying
338
+ * @param doc - Document to remove the revision content from; without it
339
+ * only the manager registry is pruned and saved XML keeps the markup
340
+ * @param ruleFilter - Which sides of the pairing to fix; REV003 (moveFrom)
341
+ * and REV004 (moveTo) are separate rules, so callers honoring
342
+ * onlyRules/skipRules must be able to fix one without the other.
343
+ * Defaults to both.
306
344
  * @returns Array of fix actions
307
345
  */
308
346
  static fixOrphanedMoveMarkers(
309
347
  revisionManager: { removeById(id: number): boolean },
310
348
  revisions: Revision[],
311
- dryRun?: boolean
349
+ dryRun?: boolean,
350
+ doc?: Document,
351
+ ruleFilter?: { moveFrom?: boolean; moveTo?: boolean }
312
352
  ): FixAction[] {
313
353
  const actions: FixAction[] = [];
354
+ const fixMoveFrom = ruleFilter?.moveFrom ?? true;
355
+ const fixMoveTo = ruleFilter?.moveTo ?? true;
314
356
 
315
357
  const moveFromIds = new Map<string, Revision>();
316
358
  const moveToIds = new Map<string, Revision>();
@@ -326,52 +368,62 @@ export class RevisionAutoFixer {
326
368
  }
327
369
  }
328
370
 
329
- // Remove orphaned moveFrom
330
- for (const [moveId, rev] of moveFromIds) {
331
- if (!moveToIds.has(moveId)) {
332
- const issue: ValidationIssue = {
333
- code: REVISION_RULES.ORPHANED_MOVE_FROM.code,
334
- severity: 'error',
335
- message: `Orphaned moveFrom with moveId="${moveId}"`,
336
- location: { revisionId: rev.getId() },
337
- autoFixable: true,
338
- };
339
-
340
- actions.push({
341
- issue,
342
- action: `Removed orphaned moveFrom (ID: ${rev.getId()}, moveId: ${moveId})`,
343
- before: { type: 'moveFrom', moveId },
344
- after: null,
345
- success: true,
346
- });
347
-
348
- if (!dryRun) {
349
- revisionManager.removeById(rev.getId());
371
+ // Remove orphaned moveFrom (REV003)
372
+ if (fixMoveFrom) {
373
+ for (const [moveId, rev] of moveFromIds) {
374
+ if (!moveToIds.has(moveId)) {
375
+ const issue: ValidationIssue = {
376
+ code: REVISION_RULES.ORPHANED_MOVE_FROM.code,
377
+ severity: 'error',
378
+ message: `Orphaned moveFrom with moveId="${moveId}"`,
379
+ location: { revisionId: rev.getId() },
380
+ autoFixable: true,
381
+ };
382
+
383
+ actions.push({
384
+ issue,
385
+ action: `Removed orphaned moveFrom (ID: ${rev.getId()}, moveId: ${moveId})`,
386
+ before: { type: 'moveFrom', moveId },
387
+ after: null,
388
+ success: true,
389
+ });
390
+
391
+ if (!dryRun) {
392
+ revisionManager.removeById(rev.getId());
393
+ if (doc) {
394
+ this.removeRevisionFromContent(doc, rev);
395
+ }
396
+ }
350
397
  }
351
398
  }
352
399
  }
353
400
 
354
- // Remove orphaned moveTo
355
- for (const [moveId, rev] of moveToIds) {
356
- if (!moveFromIds.has(moveId)) {
357
- const issue: ValidationIssue = {
358
- code: REVISION_RULES.ORPHANED_MOVE_TO.code,
359
- severity: 'error',
360
- message: `Orphaned moveTo with moveId="${moveId}"`,
361
- location: { revisionId: rev.getId() },
362
- autoFixable: true,
363
- };
364
-
365
- actions.push({
366
- issue,
367
- action: `Removed orphaned moveTo (ID: ${rev.getId()}, moveId: ${moveId})`,
368
- before: { type: 'moveTo', moveId },
369
- after: null,
370
- success: true,
371
- });
372
-
373
- if (!dryRun) {
374
- revisionManager.removeById(rev.getId());
401
+ // Remove orphaned moveTo (REV004)
402
+ if (fixMoveTo) {
403
+ for (const [moveId, rev] of moveToIds) {
404
+ if (!moveFromIds.has(moveId)) {
405
+ const issue: ValidationIssue = {
406
+ code: REVISION_RULES.ORPHANED_MOVE_TO.code,
407
+ severity: 'error',
408
+ message: `Orphaned moveTo with moveId="${moveId}"`,
409
+ location: { revisionId: rev.getId() },
410
+ autoFixable: true,
411
+ };
412
+
413
+ actions.push({
414
+ issue,
415
+ action: `Removed orphaned moveTo (ID: ${rev.getId()}, moveId: ${moveId})`,
416
+ before: { type: 'moveTo', moveId },
417
+ after: null,
418
+ success: true,
419
+ });
420
+
421
+ if (!dryRun) {
422
+ revisionManager.removeById(rev.getId());
423
+ if (doc) {
424
+ this.removeRevisionFromContent(doc, rev);
425
+ }
426
+ }
375
427
  }
376
428
  }
377
429
  }
@@ -385,38 +437,27 @@ export class RevisionAutoFixer {
385
437
  * @param revisionManager - RevisionManager instance
386
438
  * @param revisions - Array of revisions
387
439
  * @param dryRun - If true, only report changes without applying
440
+ * @param doc - Document to remove the revision content from; without it
441
+ * only the manager registry is pruned and saved XML keeps the markup
388
442
  * @returns Array of fix actions
389
443
  */
390
444
  static fixEmptyRevisions(
391
445
  revisionManager: RevisionManager,
392
446
  revisions: Revision[],
393
- dryRun?: boolean
447
+ dryRun?: boolean,
448
+ doc?: Document
394
449
  ): FixAction[] {
395
450
  const actions: FixAction[] = [];
396
451
 
397
- const propertyChangeTypes = [
398
- 'runPropertiesChange',
399
- 'paragraphPropertiesChange',
400
- 'tablePropertiesChange',
401
- 'tableExceptionPropertiesChange',
402
- 'tableRowPropertiesChange',
403
- 'tableCellPropertiesChange',
404
- 'sectionPropertiesChange',
405
- 'numberingChange',
406
- ];
407
-
408
452
  for (const rev of revisions) {
409
453
  const type = rev.getType();
410
454
 
411
- // Skip property changes
412
- if (propertyChangeTypes.includes(type)) {
455
+ // Property changes and cell markers are contentless by design
456
+ if (CONTENTLESS_REVISION_TYPES.has(type)) {
413
457
  continue;
414
458
  }
415
459
 
416
- const runs = rev.getRuns();
417
- const hasContent = runs.length > 0 && runs.some((r) => r.getText().length > 0);
418
-
419
- if (!hasContent) {
460
+ if (!revisionHasContent(rev)) {
420
461
  const issue: ValidationIssue = {
421
462
  code: REVISION_RULES.EMPTY_REVISION.code,
422
463
  severity: 'warning',
@@ -435,6 +476,9 @@ export class RevisionAutoFixer {
435
476
 
436
477
  if (!dryRun) {
437
478
  revisionManager.removeById(rev.getId());
479
+ if (doc) {
480
+ this.removeRevisionFromContent(doc, rev);
481
+ }
438
482
  }
439
483
  }
440
484
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type { Document } from '../core/Document.js';
11
11
  import type { Revision } from '../elements/Revision.js';
12
+ import { isHyperlinkContent, isImageRunContent } from '../elements/RevisionContent.js';
12
13
  import {
13
14
  REVISION_RULES,
14
15
  ValidationIssue,
@@ -18,6 +19,44 @@ import {
18
19
  } from './ValidationRules.js';
19
20
  import { ValidationRuleRegistry } from './ValidationRuleRegistry.js';
20
21
 
22
+ /**
23
+ * Revision types that are structurally contentless per ECMA-376: property
24
+ * change markers carry before/after property snapshots rather than runs, and
25
+ * table cell markers (w:cellIns/w:cellDel/w:cellMerge) annotate the cell
26
+ * itself. The REV103 empty-content rule must never flag these.
27
+ *
28
+ * Shared with RevisionAutoFixer so the validator and fixer cannot drift
29
+ * apart on which revision types are allowed to be empty.
30
+ */
31
+ export const CONTENTLESS_REVISION_TYPES: ReadonlySet<string> = new Set([
32
+ 'runPropertiesChange',
33
+ 'paragraphPropertiesChange',
34
+ 'tablePropertiesChange',
35
+ 'tableExceptionPropertiesChange',
36
+ 'tableRowPropertiesChange',
37
+ 'tableCellPropertiesChange',
38
+ 'sectionPropertiesChange',
39
+ 'numberingChange',
40
+ 'tableCellInsert',
41
+ 'tableCellDelete',
42
+ 'tableCellMerge',
43
+ ]);
44
+
45
+ /**
46
+ * True when a revision carries real content. Per ECMA-376, w:ins/w:del may
47
+ * contain w:hyperlink and image runs (w:r with w:drawing) in addition to
48
+ * text runs, so a runs-only text check would falsely flag hyperlink-only
49
+ * and image-only tracked changes as empty. Shared with RevisionAutoFixer.
50
+ */
51
+ export function revisionHasContent(rev: Revision): boolean {
52
+ // Covers text runs and hyperlink display text
53
+ if (rev.getText().length > 0) {
54
+ return true;
55
+ }
56
+ // Hyperlinks count even without display text; ImageRuns have no text at all
57
+ return rev.getContent().some((item) => isHyperlinkContent(item) || isImageRunContent(item));
58
+ }
59
+
21
60
  /**
22
61
  * Validates document revisions for ECMA-376 compliance.
23
62
  *
@@ -67,15 +106,15 @@ export class RevisionValidator {
67
106
  if (!skipRules.has('REV001')) {
68
107
  allIssues.push(...this.validateRevisionIds(revisions));
69
108
  }
70
- if (!skipRules.has('REV003') && !skipRules.has('REV004')) {
71
- allIssues.push(...this.validateMovePairs(revisions));
72
- }
109
+ // validateMovePairs emits both REV003 and REV004 from one pairing pass;
110
+ // filter per issue code so skipping one rule does not silently disable
111
+ // the other (REV004 is error-severity and corruption-relevant).
112
+ allIssues.push(...this.validateMovePairs(revisions).filter((i) => !skipRules.has(i.code)));
73
113
  if (!skipRules.has('REV002')) {
74
114
  allIssues.push(...this.validateAuthors(revisions));
75
115
  }
76
- if (!skipRules.has('REV101') && !skipRules.has('REV102')) {
77
- allIssues.push(...this.validateDates(revisions));
78
- }
116
+ // validateDates emits both REV101 and REV102 — same per-code filtering.
117
+ allIssues.push(...this.validateDates(revisions).filter((i) => !skipRules.has(i.code)));
79
118
  if (!skipRules.has('REV103')) {
80
119
  allIssues.push(...this.validateContent(revisions));
81
120
  }
@@ -302,29 +341,15 @@ export class RevisionValidator {
302
341
  static validateContent(revisions: Revision[]): ValidationIssue[] {
303
342
  const issues: ValidationIssue[] = [];
304
343
 
305
- const propertyChangeTypes = [
306
- 'runPropertiesChange',
307
- 'paragraphPropertiesChange',
308
- 'tablePropertiesChange',
309
- 'tableExceptionPropertiesChange',
310
- 'tableRowPropertiesChange',
311
- 'tableCellPropertiesChange',
312
- 'sectionPropertiesChange',
313
- 'numberingChange',
314
- ];
315
-
316
344
  for (const rev of revisions) {
317
345
  const type = rev.getType();
318
346
 
319
- // Property changes don't need text content
320
- if (propertyChangeTypes.includes(type)) {
347
+ // Property changes and cell markers are contentless by design
348
+ if (CONTENTLESS_REVISION_TYPES.has(type)) {
321
349
  continue;
322
350
  }
323
351
 
324
- const runs = rev.getRuns();
325
- const hasContent = runs.length > 0 && runs.some((r) => r.getText().length > 0);
326
-
327
- if (!hasContent) {
352
+ if (!revisionHasContent(rev)) {
328
353
  issues.push(
329
354
  createIssueFromRule(
330
355
  REVISION_RULES.EMPTY_REVISION,
@@ -162,9 +162,12 @@ export class XMLBuilder {
162
162
  // Add attributes
163
163
  if (element.attributes) {
164
164
  for (const [key, value] of Object.entries(element.attributes)) {
165
- if (value !== undefined && value !== null && value !== false) {
166
- // Handle boolean attributes
167
- const attrValue = value === true ? key : String(value);
165
+ if (value !== undefined && value !== null) {
166
+ // Booleans serialize as ST_OnOff literals (ECMA-376 §22.9.2.13).
167
+ // Explicit false must emit "0" dropping it would flip an explicit
168
+ // override to inherited/on under CT_OnOff presence semantics.
169
+ // Callers signal absence with undefined, never false.
170
+ const attrValue = value === true ? '1' : value === false ? '0' : String(value);
168
171
  // Use escapeXmlAttribute for attribute values (Issue #8)
169
172
  xml += ` ${key}="${XMLBuilder.escapeXmlAttribute(attrValue)}"`;
170
173
  }
@@ -245,18 +248,47 @@ export class XMLBuilder {
245
248
  .replace(/'/g, '&apos;');
246
249
  }
247
250
 
251
+ /** The five predefined XML 1.0 named entities */
252
+ private static readonly NAMED_ENTITIES: Record<string, string> = {
253
+ lt: '<',
254
+ gt: '>',
255
+ quot: '"',
256
+ apos: "'",
257
+ amp: '&',
258
+ };
259
+
248
260
  /**
249
261
  * Unescapes XML entities back to original characters
262
+ * Decodes the five predefined named entities plus numeric character
263
+ * references (&#8217; / &#xA0;), which XML 1.0 §4.1 requires every
264
+ * conforming processor to expand — third-party producers emit them, and
265
+ * leaving them raw double-escapes the ampersand on save.
250
266
  * @param text Text with XML entities
251
267
  * @returns Unescaped text
252
268
  */
253
269
  static unescapeXml(text: string): string {
254
- return text
255
- .replace(/&lt;/g, '<')
256
- .replace(/&gt;/g, '>')
257
- .replace(/&quot;/g, '"')
258
- .replace(/&apos;/g, "'")
259
- .replace(/&amp;/g, '&'); // Must be last to avoid double-unescaping
270
+ // Single pass so text produced by one decode is never re-decoded by a
271
+ // later one (e.g. '&#38;amp;' must yield '&amp;', not '&').
272
+ return text.replace(
273
+ /&(?:(lt|gt|quot|apos|amp)|#x([0-9A-Fa-f]+)|#(\d+));/g,
274
+ (match, named?: string, hex?: string, dec?: string) => {
275
+ if (named !== undefined) {
276
+ return XMLBuilder.NAMED_ENTITIES[named] ?? match;
277
+ }
278
+ const code = hex !== undefined ? parseInt(hex, 16) : Number(dec);
279
+ // Code points outside the XML 1.0 Char production (NUL, surrogates,
280
+ // beyond U+10FFFF) would make the decoded string unserializable —
281
+ // keep the raw reference instead of injecting an invalid character.
282
+ const isXmlChar =
283
+ code === 0x9 ||
284
+ code === 0xa ||
285
+ code === 0xd ||
286
+ (code >= 0x20 && code <= 0xd7ff) ||
287
+ (code >= 0xe000 && code <= 0xfffd) ||
288
+ (code >= 0x10000 && code <= 0x10ffff);
289
+ return isXmlChar ? String.fromCodePoint(code) : match;
290
+ }
291
+ );
260
292
  }
261
293
 
262
294
  /**
@@ -633,11 +665,7 @@ export class XMLBuilder {
633
665
  const builder = new XMLBuilder();
634
666
  const element = XMLBuilder.objectToElement(obj, rootName);
635
667
  if (element) {
636
- if (typeof element === 'string') {
637
- builder.text(element);
638
- } else {
639
- builder.elements.push(element);
640
- }
668
+ builder.elements.push(element);
641
669
  }
642
670
  return builder.build();
643
671
  }
@@ -649,22 +677,21 @@ export class XMLBuilder {
649
677
  private static objectToElement(
650
678
  obj: ParsedXmlObject | string | number | boolean | null | undefined,
651
679
  name: string
652
- ): XMLElement | string | null {
680
+ ): XMLElement | null {
653
681
  if (obj === null || obj === undefined) {
654
682
  return null;
655
683
  }
656
684
 
657
- if (typeof obj !== 'object' || obj === null) {
658
- return String(obj);
685
+ if (typeof obj !== 'object') {
686
+ // The parser collapses text-only elements to their primitive value, so
687
+ // the wrapping tag must be re-emitted here — bare text inside container
688
+ // elements (e.g. w:r) is invalid WordprocessingML.
689
+ return { name, children: [String(obj)] };
659
690
  }
660
691
 
661
692
  const attributes: Record<string, string | number | boolean> = {};
662
693
  const children: (XMLElement | string)[] = [];
663
694
 
664
- if (obj['#text'] && Object.keys(obj).length === 1) {
665
- return String(obj['#text']);
666
- }
667
-
668
695
  for (const key in obj) {
669
696
  if (key.startsWith('@_')) {
670
697
  const attrName = key.substring(2);
@@ -674,7 +701,40 @@ export class XMLBuilder {
674
701
  }
675
702
  } else if (key === '#text') {
676
703
  children.push(String(obj[key]));
677
- } else {
704
+ }
705
+ }
706
+
707
+ // _orderedChildren is parser metadata, never document content. When
708
+ // present, it restores the original interleaving of same-named siblings
709
+ // (e.g. w:t / w:tab / w:t in a run) that the keyed object form loses.
710
+ const orderedChildren = obj['_orderedChildren'] as
711
+ | { type: string; index: number }[]
712
+ | undefined;
713
+
714
+ if (orderedChildren && orderedChildren.length > 0) {
715
+ for (const childInfo of orderedChildren) {
716
+ const childValue = obj[childInfo.type];
717
+ if (childValue === undefined) {
718
+ continue;
719
+ }
720
+ const item = Array.isArray(childValue)
721
+ ? childValue[childInfo.index]
722
+ : childInfo.index === 0
723
+ ? childValue
724
+ : undefined;
725
+ if (item === undefined) {
726
+ continue;
727
+ }
728
+ const childElement = XMLBuilder.objectToElement(item, childInfo.type);
729
+ if (childElement) {
730
+ children.push(childElement);
731
+ }
732
+ }
733
+ } else {
734
+ for (const key in obj) {
735
+ if (key.startsWith('@_') || key === '#text' || key === '_orderedChildren') {
736
+ continue;
737
+ }
678
738
  const childObj = obj[key];
679
739
  if (Array.isArray(childObj)) {
680
740
  childObj.forEach((item) => {