docxmlater 10.1.4 → 10.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/README.md +759 -754
  2. package/dist/constants/legacyCompatFlags.js +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.js.map +1 -1
  5. package/dist/core/Document.d.ts +51 -50
  6. package/dist/core/Document.d.ts.map +1 -1
  7. package/dist/core/Document.js +486 -471
  8. package/dist/core/Document.js.map +1 -1
  9. package/dist/core/DocumentContent.d.ts +9 -9
  10. package/dist/core/DocumentContent.d.ts.map +1 -1
  11. package/dist/core/DocumentContent.js +1 -1
  12. package/dist/core/DocumentContent.js.map +1 -1
  13. package/dist/core/DocumentGenerator.d.ts +11 -11
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +251 -251
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.js.map +1 -1
  18. package/dist/core/DocumentParser.d.ts +15 -15
  19. package/dist/core/DocumentParser.d.ts.map +1 -1
  20. package/dist/core/DocumentParser.js +2123 -2155
  21. package/dist/core/DocumentParser.js.map +1 -1
  22. package/dist/core/DocumentValidator.d.ts.map +1 -1
  23. package/dist/core/DocumentValidator.js +2 -5
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.js.map +1 -1
  26. package/dist/core/RelationshipManager.d.ts.map +1 -1
  27. package/dist/core/RelationshipManager.js +3 -3
  28. package/dist/core/RelationshipManager.js.map +1 -1
  29. package/dist/elements/AlternateContent.js.map +1 -1
  30. package/dist/elements/Bookmark.d.ts.map +1 -1
  31. package/dist/elements/Bookmark.js +3 -1
  32. package/dist/elements/Bookmark.js.map +1 -1
  33. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  34. package/dist/elements/BookmarkManager.js.map +1 -1
  35. package/dist/elements/Comment.d.ts.map +1 -1
  36. package/dist/elements/Comment.js +9 -6
  37. package/dist/elements/Comment.js.map +1 -1
  38. package/dist/elements/CommentManager.d.ts.map +1 -1
  39. package/dist/elements/CommentManager.js +18 -17
  40. package/dist/elements/CommentManager.js.map +1 -1
  41. package/dist/elements/CommonTypes.d.ts +21 -21
  42. package/dist/elements/CommonTypes.d.ts.map +1 -1
  43. package/dist/elements/CommonTypes.js +56 -56
  44. package/dist/elements/CommonTypes.js.map +1 -1
  45. package/dist/elements/CustomXml.js.map +1 -1
  46. package/dist/elements/Endnote.d.ts.map +1 -1
  47. package/dist/elements/Endnote.js +6 -6
  48. package/dist/elements/Endnote.js.map +1 -1
  49. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  50. package/dist/elements/EndnoteManager.js +6 -7
  51. package/dist/elements/EndnoteManager.js.map +1 -1
  52. package/dist/elements/Field.d.ts.map +1 -1
  53. package/dist/elements/Field.js +82 -25
  54. package/dist/elements/Field.js.map +1 -1
  55. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  56. package/dist/elements/FieldHelpers.js.map +1 -1
  57. package/dist/elements/FontManager.d.ts.map +1 -1
  58. package/dist/elements/FontManager.js +1 -1
  59. package/dist/elements/FontManager.js.map +1 -1
  60. package/dist/elements/Footer.js +2 -2
  61. package/dist/elements/Footer.js.map +1 -1
  62. package/dist/elements/Footnote.d.ts.map +1 -1
  63. package/dist/elements/Footnote.js +6 -6
  64. package/dist/elements/Footnote.js.map +1 -1
  65. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  66. package/dist/elements/FootnoteManager.js +6 -7
  67. package/dist/elements/FootnoteManager.js.map +1 -1
  68. package/dist/elements/Header.js +2 -2
  69. package/dist/elements/Header.js.map +1 -1
  70. package/dist/elements/HeaderFooterManager.js.map +1 -1
  71. package/dist/elements/Hyperlink.d.ts +5 -3
  72. package/dist/elements/Hyperlink.d.ts.map +1 -1
  73. package/dist/elements/Hyperlink.js +134 -76
  74. package/dist/elements/Hyperlink.js.map +1 -1
  75. package/dist/elements/Image.d.ts.map +1 -1
  76. package/dist/elements/Image.js +238 -106
  77. package/dist/elements/Image.js.map +1 -1
  78. package/dist/elements/ImageManager.d.ts.map +1 -1
  79. package/dist/elements/ImageManager.js +1 -1
  80. package/dist/elements/ImageManager.js.map +1 -1
  81. package/dist/elements/ImageRun.js +1 -1
  82. package/dist/elements/ImageRun.js.map +1 -1
  83. package/dist/elements/MathElement.js.map +1 -1
  84. package/dist/elements/Paragraph.d.ts +24 -24
  85. package/dist/elements/Paragraph.d.ts.map +1 -1
  86. package/dist/elements/Paragraph.js +181 -188
  87. package/dist/elements/Paragraph.js.map +1 -1
  88. package/dist/elements/PreservedElement.js.map +1 -1
  89. package/dist/elements/PropertyChangeTypes.d.ts.map +1 -1
  90. package/dist/elements/PropertyChangeTypes.js +6 -6
  91. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  92. package/dist/elements/RangeMarker.d.ts.map +1 -1
  93. package/dist/elements/RangeMarker.js.map +1 -1
  94. package/dist/elements/Revision.d.ts.map +1 -1
  95. package/dist/elements/Revision.js +4 -5
  96. package/dist/elements/Revision.js.map +1 -1
  97. package/dist/elements/RevisionContent.js.map +1 -1
  98. package/dist/elements/RevisionManager.d.ts.map +1 -1
  99. package/dist/elements/RevisionManager.js +40 -48
  100. package/dist/elements/RevisionManager.js.map +1 -1
  101. package/dist/elements/Run.d.ts +16 -16
  102. package/dist/elements/Run.d.ts.map +1 -1
  103. package/dist/elements/Run.js +256 -238
  104. package/dist/elements/Run.js.map +1 -1
  105. package/dist/elements/Section.d.ts.map +1 -1
  106. package/dist/elements/Section.js +36 -11
  107. package/dist/elements/Section.js.map +1 -1
  108. package/dist/elements/Shape.d.ts.map +1 -1
  109. package/dist/elements/Shape.js.map +1 -1
  110. package/dist/elements/StructuredDocumentTag.d.ts +6 -6
  111. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  112. package/dist/elements/StructuredDocumentTag.js +99 -104
  113. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  114. package/dist/elements/Table.d.ts +11 -11
  115. package/dist/elements/Table.d.ts.map +1 -1
  116. package/dist/elements/Table.js +102 -107
  117. package/dist/elements/Table.js.map +1 -1
  118. package/dist/elements/TableCell.d.ts +10 -10
  119. package/dist/elements/TableCell.d.ts.map +1 -1
  120. package/dist/elements/TableCell.js +105 -106
  121. package/dist/elements/TableCell.js.map +1 -1
  122. package/dist/elements/TableGridChange.d.ts.map +1 -1
  123. package/dist/elements/TableGridChange.js.map +1 -1
  124. package/dist/elements/TableOfContents.d.ts.map +1 -1
  125. package/dist/elements/TableOfContents.js +4 -4
  126. package/dist/elements/TableOfContents.js.map +1 -1
  127. package/dist/elements/TableOfContentsElement.js.map +1 -1
  128. package/dist/elements/TableRow.d.ts.map +1 -1
  129. package/dist/elements/TableRow.js +13 -6
  130. package/dist/elements/TableRow.js.map +1 -1
  131. package/dist/elements/TextBox.d.ts.map +1 -1
  132. package/dist/elements/TextBox.js +3 -5
  133. package/dist/elements/TextBox.js.map +1 -1
  134. package/dist/formatting/AbstractNumbering.d.ts +4 -4
  135. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  136. package/dist/formatting/AbstractNumbering.js +54 -49
  137. package/dist/formatting/AbstractNumbering.js.map +1 -1
  138. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  139. package/dist/formatting/NumberingInstance.js +1 -3
  140. package/dist/formatting/NumberingInstance.js.map +1 -1
  141. package/dist/formatting/NumberingLevel.d.ts +5 -5
  142. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  143. package/dist/formatting/NumberingLevel.js +119 -125
  144. package/dist/formatting/NumberingLevel.js.map +1 -1
  145. package/dist/formatting/NumberingManager.d.ts +1 -0
  146. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  147. package/dist/formatting/NumberingManager.js +27 -9
  148. package/dist/formatting/NumberingManager.js.map +1 -1
  149. package/dist/formatting/Style.d.ts +11 -11
  150. package/dist/formatting/Style.d.ts.map +1 -1
  151. package/dist/formatting/Style.js +219 -247
  152. package/dist/formatting/Style.js.map +1 -1
  153. package/dist/formatting/StylesManager.d.ts +2 -2
  154. package/dist/formatting/StylesManager.d.ts.map +1 -1
  155. package/dist/formatting/StylesManager.js +96 -102
  156. package/dist/formatting/StylesManager.js.map +1 -1
  157. package/dist/helpers/CleanupHelper.d.ts +1 -1
  158. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  159. package/dist/helpers/CleanupHelper.js +6 -6
  160. package/dist/helpers/CleanupHelper.js.map +1 -1
  161. package/dist/images/ImageOptimizer.js +7 -7
  162. package/dist/images/ImageOptimizer.js.map +1 -1
  163. package/dist/index.d.ts +9 -9
  164. package/dist/index.d.ts.map +1 -1
  165. package/dist/index.js.map +1 -1
  166. package/dist/managers/DrawingManager.js.map +1 -1
  167. package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
  168. package/dist/tracking/DocumentTrackingContext.js +23 -7
  169. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  170. package/dist/tracking/TrackingContext.d.ts.map +1 -1
  171. package/dist/tracking/TrackingContext.js.map +1 -1
  172. package/dist/types/compatibility-types.js.map +1 -1
  173. package/dist/types/formatting.js.map +1 -1
  174. package/dist/types/list-types.d.ts +6 -6
  175. package/dist/types/list-types.js.map +1 -1
  176. package/dist/types/settings-types.js.map +1 -1
  177. package/dist/types/styleConfig.d.ts +2 -2
  178. package/dist/types/styleConfig.js.map +1 -1
  179. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  180. package/dist/utils/ChangelogGenerator.js +97 -101
  181. package/dist/utils/ChangelogGenerator.js.map +1 -1
  182. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  183. package/dist/utils/CompatibilityUpgrader.js +1 -1
  184. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  185. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  186. package/dist/utils/InMemoryRevisionAcceptor.js +1 -6
  187. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  188. package/dist/utils/MoveOperationHelper.d.ts.map +1 -1
  189. package/dist/utils/MoveOperationHelper.js +1 -1
  190. package/dist/utils/MoveOperationHelper.js.map +1 -1
  191. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  192. package/dist/utils/RevisionAwareProcessor.js +2 -4
  193. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  194. package/dist/utils/RevisionWalker.d.ts.map +1 -1
  195. package/dist/utils/RevisionWalker.js +4 -12
  196. package/dist/utils/RevisionWalker.js.map +1 -1
  197. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  198. package/dist/utils/SelectiveRevisionAcceptor.js +2 -6
  199. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  200. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  201. package/dist/utils/ShadingResolver.js +1 -1
  202. package/dist/utils/ShadingResolver.js.map +1 -1
  203. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  204. package/dist/utils/acceptRevisions.js +23 -12
  205. package/dist/utils/acceptRevisions.js.map +1 -1
  206. package/dist/utils/cnfStyleDecoder.d.ts +1 -1
  207. package/dist/utils/cnfStyleDecoder.d.ts.map +1 -1
  208. package/dist/utils/cnfStyleDecoder.js +40 -40
  209. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  210. package/dist/utils/corruptionDetection.d.ts.map +1 -1
  211. package/dist/utils/corruptionDetection.js.map +1 -1
  212. package/dist/utils/dateFormatting.js.map +1 -1
  213. package/dist/utils/deepClone.js +1 -1
  214. package/dist/utils/deepClone.js.map +1 -1
  215. package/dist/utils/diagnostics.d.ts.map +1 -1
  216. package/dist/utils/diagnostics.js +1 -1
  217. package/dist/utils/diagnostics.js.map +1 -1
  218. package/dist/utils/errorHandling.js.map +1 -1
  219. package/dist/utils/formatting.d.ts.map +1 -1
  220. package/dist/utils/formatting.js +10 -2
  221. package/dist/utils/formatting.js.map +1 -1
  222. package/dist/utils/list-detection.d.ts +2 -2
  223. package/dist/utils/list-detection.d.ts.map +1 -1
  224. package/dist/utils/list-detection.js +21 -23
  225. package/dist/utils/list-detection.js.map +1 -1
  226. package/dist/utils/logger.d.ts.map +1 -1
  227. package/dist/utils/logger.js +12 -7
  228. package/dist/utils/logger.js.map +1 -1
  229. package/dist/utils/parsingHelpers.js.map +1 -1
  230. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  231. package/dist/utils/stripTrackedChanges.js +3 -3
  232. package/dist/utils/stripTrackedChanges.js.map +1 -1
  233. package/dist/utils/textDiff.d.ts +1 -1
  234. package/dist/utils/textDiff.js +8 -8
  235. package/dist/utils/textDiff.js.map +1 -1
  236. package/dist/utils/units.js.map +1 -1
  237. package/dist/utils/validation.d.ts.map +1 -1
  238. package/dist/utils/validation.js +24 -7
  239. package/dist/utils/validation.js.map +1 -1
  240. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  241. package/dist/utils/xmlSanitization.js +3 -3
  242. package/dist/utils/xmlSanitization.js.map +1 -1
  243. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  244. package/dist/validation/RevisionAutoFixer.js +5 -5
  245. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  246. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  247. package/dist/validation/RevisionValidator.js +7 -9
  248. package/dist/validation/RevisionValidator.js.map +1 -1
  249. package/dist/validation/ValidationRules.js +3 -3
  250. package/dist/validation/ValidationRules.js.map +1 -1
  251. package/dist/validation/index.js.map +1 -1
  252. package/dist/xml/XMLBuilder.d.ts +1 -1
  253. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  254. package/dist/xml/XMLBuilder.js +98 -100
  255. package/dist/xml/XMLBuilder.js.map +1 -1
  256. package/dist/xml/XMLParser.d.ts.map +1 -1
  257. package/dist/xml/XMLParser.js +61 -66
  258. package/dist/xml/XMLParser.js.map +1 -1
  259. package/dist/zip/ZipHandler.d.ts.map +1 -1
  260. package/dist/zip/ZipHandler.js.map +1 -1
  261. package/dist/zip/ZipReader.d.ts.map +1 -1
  262. package/dist/zip/ZipReader.js +1 -3
  263. package/dist/zip/ZipReader.js.map +1 -1
  264. package/dist/zip/ZipWriter.d.ts +1 -1
  265. package/dist/zip/ZipWriter.d.ts.map +1 -1
  266. package/dist/zip/ZipWriter.js +28 -36
  267. package/dist/zip/ZipWriter.js.map +1 -1
  268. package/dist/zip/types.js +1 -1
  269. package/dist/zip/types.js.map +1 -1
  270. package/package.json +92 -92
  271. package/src/__tests__/helper-methods.test.ts +512 -512
  272. package/src/constants/legacyCompatFlags.ts +138 -138
  273. package/src/constants/limits.ts +50 -50
  274. package/src/core/Document.ts +1010 -1145
  275. package/src/core/DocumentContent.ts +461 -467
  276. package/src/core/DocumentGenerator.ts +1133 -1104
  277. package/src/core/DocumentIdManager.ts +158 -158
  278. package/src/core/DocumentParser.ts +2347 -2716
  279. package/src/core/DocumentValidator.ts +363 -372
  280. package/src/core/Relationship.ts +367 -367
  281. package/src/core/RelationshipManager.ts +429 -428
  282. package/src/elements/AlternateContent.ts +42 -42
  283. package/src/elements/Bookmark.ts +212 -210
  284. package/src/elements/BookmarkManager.ts +247 -250
  285. package/src/elements/Comment.ts +356 -359
  286. package/src/elements/CommentManager.ts +499 -502
  287. package/src/elements/CommonTypes.ts +524 -549
  288. package/src/elements/CustomXml.ts +36 -36
  289. package/src/elements/Endnote.ts +221 -217
  290. package/src/elements/EndnoteManager.ts +246 -249
  291. package/src/elements/Field.ts +1292 -1233
  292. package/src/elements/FieldHelpers.ts +329 -333
  293. package/src/elements/FontManager.ts +336 -339
  294. package/src/elements/Footer.ts +269 -269
  295. package/src/elements/Footnote.ts +221 -217
  296. package/src/elements/FootnoteManager.ts +246 -249
  297. package/src/elements/Header.ts +269 -269
  298. package/src/elements/HeaderFooterManager.ts +219 -219
  299. package/src/elements/Hyperlink.ts +1288 -1193
  300. package/src/elements/Image.ts +1982 -1756
  301. package/src/elements/ImageManager.ts +437 -432
  302. package/src/elements/ImageRun.ts +59 -59
  303. package/src/elements/MathElement.ts +65 -65
  304. package/src/elements/Paragraph.ts +4347 -4287
  305. package/src/elements/PreservedElement.ts +53 -53
  306. package/src/elements/PropertyChangeTypes.ts +458 -442
  307. package/src/elements/RangeMarker.ts +382 -400
  308. package/src/elements/Revision.ts +1198 -1217
  309. package/src/elements/RevisionContent.ts +73 -73
  310. package/src/elements/RevisionManager.ts +1070 -1070
  311. package/src/elements/Run.ts +3103 -3073
  312. package/src/elements/Section.ts +1521 -1421
  313. package/src/elements/Shape.ts +884 -873
  314. package/src/elements/StructuredDocumentTag.ts +1176 -1207
  315. package/src/elements/Table.ts +2468 -2524
  316. package/src/elements/TableCell.ts +1617 -1621
  317. package/src/elements/TableGridChange.ts +149 -151
  318. package/src/elements/TableOfContents.ts +701 -691
  319. package/src/elements/TableOfContentsElement.ts +89 -89
  320. package/src/elements/TableRow.ts +960 -929
  321. package/src/elements/TextBox.ts +766 -768
  322. package/src/formatting/AbstractNumbering.ts +580 -579
  323. package/src/formatting/NumberingInstance.ts +295 -299
  324. package/src/formatting/NumberingLevel.ts +981 -1040
  325. package/src/formatting/NumberingManager.ts +875 -827
  326. package/src/formatting/Style.ts +1785 -1879
  327. package/src/formatting/StylesManager.ts +1090 -1130
  328. package/src/helpers/CleanupHelper.ts +524 -524
  329. package/src/images/ImageOptimizer.ts +274 -274
  330. package/src/index.ts +559 -554
  331. package/src/managers/DrawingManager.ts +319 -319
  332. package/src/tracking/DocumentTrackingContext.ts +687 -674
  333. package/src/tracking/TrackingContext.ts +175 -173
  334. package/src/types/compatibility-types.ts +49 -49
  335. package/src/types/formatting.ts +210 -210
  336. package/src/types/list-types.ts +14 -14
  337. package/src/types/settings-types.ts +59 -59
  338. package/src/types/styleConfig.ts +189 -189
  339. package/src/utils/ChangelogGenerator.ts +1583 -1581
  340. package/src/utils/CompatibilityUpgrader.ts +235 -237
  341. package/src/utils/InMemoryRevisionAcceptor.ts +691 -696
  342. package/src/utils/MoveOperationHelper.ts +233 -238
  343. package/src/utils/RevisionAwareProcessor.ts +518 -526
  344. package/src/utils/RevisionWalker.ts +427 -457
  345. package/src/utils/SelectiveRevisionAcceptor.ts +662 -683
  346. package/src/utils/ShadingResolver.ts +105 -107
  347. package/src/utils/acceptRevisions.ts +723 -714
  348. package/src/utils/cnfStyleDecoder.ts +212 -217
  349. package/src/utils/corruptionDetection.ts +346 -345
  350. package/src/utils/dateFormatting.ts +20 -20
  351. package/src/utils/deepClone.ts +77 -78
  352. package/src/utils/diagnostics.ts +125 -129
  353. package/src/utils/errorHandling.ts +80 -80
  354. package/src/utils/formatting.ts +220 -213
  355. package/src/utils/list-detection.ts +32 -42
  356. package/src/utils/logger.ts +412 -404
  357. package/src/utils/parsingHelpers.ts +190 -190
  358. package/src/utils/stripTrackedChanges.ts +356 -353
  359. package/src/utils/textDiff.ts +100 -100
  360. package/src/utils/units.ts +421 -421
  361. package/src/utils/validation.ts +553 -542
  362. package/src/utils/xmlSanitization.ts +179 -182
  363. package/src/validation/RevisionAutoFixer.ts +541 -542
  364. package/src/validation/RevisionValidator.ts +470 -460
  365. package/src/validation/ValidationRules.ts +338 -338
  366. package/src/validation/index.ts +30 -30
  367. package/src/xml/XMLBuilder.ts +857 -871
  368. package/src/xml/XMLParser.ts +877 -919
  369. package/src/zip/ZipHandler.ts +629 -637
  370. package/src/zip/ZipReader.ts +295 -299
  371. package/src/zip/ZipWriter.ts +374 -390
  372. package/src/zip/types.ts +116 -116
@@ -1,1070 +1,1070 @@
1
- /**
2
- * RevisionManager - Manages tracked changes (revisions) in a document
3
- *
4
- * Tracks all revisions, assigns unique IDs, and provides statistics.
5
- */
6
-
7
- import { Revision, RevisionType } from './Revision';
8
- import type { RevisionLocation } from './PropertyChangeTypes';
9
- import { getGlobalLogger, createScopedLogger, ILogger } from '../utils/logger';
10
-
11
- // Scoped logger for RevisionManager
12
- function getLogger(): ILogger {
13
- return createScopedLogger(getGlobalLogger(), 'RevisionManager');
14
- }
15
-
16
- /**
17
- * Type for the centralized ID provider callback.
18
- * Returns the next available annotation ID from a shared counter.
19
- */
20
- export type IdProviderCallback = () => number;
21
-
22
- /**
23
- * Type for callback to notify of existing IDs (for synchronization).
24
- * Called when registering existing revisions to keep the central counter in sync.
25
- */
26
- export type IdExistsCallback = (existingId: number) => void;
27
-
28
- /**
29
- * Semantic category for grouping revisions.
30
- */
31
- export type RevisionCategory =
32
- | 'content' // Text insertions, deletions
33
- | 'formatting' // Run/paragraph property changes
34
- | 'structural' // Moves, section changes
35
- | 'table'; // Table structure changes
36
-
37
- /**
38
- * Summary statistics for revisions.
39
- */
40
- export interface RevisionSummary {
41
- total: number;
42
- byType: {
43
- insertions: number;
44
- deletions: number;
45
- moves: number;
46
- propertyChanges: number;
47
- tableChanges: number;
48
- };
49
- byCategory: Record<RevisionCategory, number>;
50
- authors: string[];
51
- dateRange: { earliest: Date; latest: Date } | null;
52
- }
53
-
54
- /**
55
- * Manages document revisions (track changes)
56
- *
57
- * Per ECMA-376, revision IDs must be unique across ALL annotation types
58
- * in a document. Use setIdProvider() to connect to a centralized ID allocator.
59
- */
60
- export class RevisionManager {
61
- private revisions: Revision[] = [];
62
- private nextId = 0;
63
- private idProvider: IdProviderCallback | null = null;
64
- private idExistsNotifier: IdExistsCallback | null = null;
65
-
66
- // Performance caching for frequently accessed filtered results
67
- private revisionsByTypeCache = new Map<RevisionType, Revision[]>();
68
- private revisionsByAuthorCache = new Map<string, Revision[]>();
69
- private revisionsByCategoryCache = new Map<RevisionCategory, Revision[]>();
70
- private cacheValid = true;
71
-
72
- /**
73
- * Invalidates all caches. Called when revisions are added/removed.
74
- * @private
75
- */
76
- private invalidateCache(): void {
77
- this.revisionsByTypeCache.clear();
78
- this.revisionsByAuthorCache.clear();
79
- this.revisionsByCategoryCache.clear();
80
- this.cacheValid = false;
81
- }
82
-
83
- /**
84
- * Sets the centralized ID provider callback.
85
- * When set, IDs will be allocated from the centralized DocumentIdManager
86
- * instead of the local nextId counter.
87
- *
88
- * @param provider - Callback that returns the next available ID
89
- * @param existsNotifier - Optional callback to notify when existing IDs are found
90
- */
91
- setIdProvider(provider: IdProviderCallback, existsNotifier?: IdExistsCallback): void {
92
- this.idProvider = provider;
93
- this.idExistsNotifier = existsNotifier || null;
94
- }
95
-
96
- /**
97
- * Registers a revision with the manager
98
- * Assigns a unique ID
99
- * @param revision - Revision to register
100
- * @returns The registered revision (same instance)
101
- */
102
- register(revision: Revision): Revision {
103
- const logger = getLogger();
104
- // Assign unique ID - use centralized provider if available
105
- const id = this.idProvider ? this.idProvider() : this.nextId++;
106
- revision.setId(id);
107
-
108
- // Store revision
109
- this.revisions.push(revision);
110
- this.invalidateCache();
111
-
112
- logger.debug('Revision registered', {
113
- id: revision.getId(),
114
- type: revision.getType(),
115
- author: revision.getAuthor()
116
- });
117
-
118
- return revision;
119
- }
120
-
121
- /**
122
- * Registers an existing revision (from parsing) with its pre-assigned ID.
123
- * Unlike register(), this does NOT assign a new ID - preserves the original
124
- * ID from parsed XML. Used when loading documents to avoid overwriting
125
- * revision IDs that are already correct.
126
- *
127
- * @param revision - Revision with ID already set from XML parsing
128
- * @returns The registered revision (same instance)
129
- */
130
- registerExisting(revision: Revision): Revision {
131
- const logger = getLogger();
132
- const existingId = revision.getId();
133
-
134
- // Notify centralized ID manager about this existing ID
135
- // This ensures the shared counter stays above all existing IDs
136
- if (this.idExistsNotifier) {
137
- this.idExistsNotifier(existingId);
138
- }
139
-
140
- // Also update local nextId to avoid collisions (fallback when no provider)
141
- if (existingId >= this.nextId) {
142
- this.nextId = existingId + 1;
143
- }
144
-
145
- // Store revision (keep its existing ID - do NOT overwrite)
146
- this.revisions.push(revision);
147
- this.invalidateCache();
148
-
149
- logger.debug('Existing revision registered', {
150
- id: existingId,
151
- type: revision.getType(),
152
- author: revision.getAuthor()
153
- });
154
-
155
- return revision;
156
- }
157
-
158
- /**
159
- * Gets all revisions
160
- * @returns Array of all revisions
161
- */
162
- getAllRevisions(): Revision[] {
163
- return [...this.revisions];
164
- }
165
-
166
- /**
167
- * Gets revisions by type
168
- * Uses caching for improved performance on repeated calls
169
- * @param type - Revision type to filter by
170
- * @returns Array of revisions of the specified type
171
- */
172
- getRevisionsByType(type: RevisionType): Revision[] {
173
- // Check cache first
174
- if (this.revisionsByTypeCache.has(type)) {
175
- return [...this.revisionsByTypeCache.get(type)!];
176
- }
177
-
178
- // Compute and cache
179
- const result = this.revisions.filter(rev => rev.getType() === type);
180
- this.revisionsByTypeCache.set(type, result);
181
- return [...result];
182
- }
183
-
184
- /**
185
- * Gets revisions by author
186
- * Uses caching for improved performance on repeated calls
187
- * @param author - Author name to filter by
188
- * @returns Array of revisions by the specified author
189
- */
190
- getRevisionsByAuthor(author: string): Revision[] {
191
- // Check cache first
192
- if (this.revisionsByAuthorCache.has(author)) {
193
- return [...this.revisionsByAuthorCache.get(author)!];
194
- }
195
-
196
- // Compute and cache
197
- const result = this.revisions.filter(rev => rev.getAuthor() === author);
198
- this.revisionsByAuthorCache.set(author, result);
199
- return [...result];
200
- }
201
-
202
- /**
203
- * Gets the number of revisions
204
- * @returns Number of revisions
205
- */
206
- getCount(): number {
207
- return this.revisions.length;
208
- }
209
-
210
- /**
211
- * Gets the number of insertions
212
- * @returns Number of insertion revisions
213
- */
214
- getInsertionCount(): number {
215
- return this.getRevisionsByType('insert').length;
216
- }
217
-
218
- /**
219
- * Gets the number of deletions
220
- * @returns Number of deletion revisions
221
- */
222
- getDeletionCount(): number {
223
- return this.getRevisionsByType('delete').length;
224
- }
225
-
226
- /**
227
- * Gets all unique authors who have made changes
228
- * @returns Array of unique author names
229
- */
230
- getAuthors(): string[] {
231
- const authorsSet = new Set<string>();
232
- for (const revision of this.revisions) {
233
- authorsSet.add(revision.getAuthor());
234
- }
235
- return Array.from(authorsSet);
236
- }
237
-
238
- /**
239
- * Clears all revisions
240
- */
241
- clear(): void {
242
- const count = this.revisions.length;
243
- this.revisions = [];
244
- this.nextId = 0;
245
- this.invalidateCache();
246
- if (count > 0) {
247
- getLogger().info('Revisions cleared', { previousCount: count });
248
- }
249
- }
250
-
251
- /**
252
- * Checks if there are no revisions
253
- * @returns True if there are no tracked changes
254
- */
255
- isEmpty(): boolean {
256
- return this.revisions.length === 0;
257
- }
258
-
259
- /**
260
- * Gets the most recent N revisions
261
- * @param count - Number of recent revisions to return
262
- * @returns Array of most recent revisions
263
- */
264
- getRecentRevisions(count: number): Revision[] {
265
- return [...this.revisions]
266
- .sort((a, b) => b.getDate().getTime() - a.getDate().getTime())
267
- .slice(0, count);
268
- }
269
-
270
- /**
271
- * Searches revisions by text content
272
- * @param searchText - Text to search for (case-insensitive)
273
- * @returns Array of revisions containing the search text
274
- */
275
- findRevisionsByText(searchText: string): Revision[] {
276
- const lowerSearch = searchText.toLowerCase();
277
- return this.revisions.filter(revision => {
278
- const text = revision.getRuns()
279
- .map(run => run.getText())
280
- .join('')
281
- .toLowerCase();
282
- return text.includes(lowerSearch);
283
- });
284
- }
285
-
286
- /**
287
- * Gets all insertions (added text)
288
- * @returns Array of insertion revisions
289
- */
290
- getAllInsertions(): Revision[] {
291
- return this.getRevisionsByType('insert');
292
- }
293
-
294
- /**
295
- * Gets all deletions (removed text)
296
- * @returns Array of deletion revisions
297
- */
298
- getAllDeletions(): Revision[] {
299
- return this.getRevisionsByType('delete');
300
- }
301
-
302
- /**
303
- * Gets all run properties changes (formatting changes)
304
- * @returns Array of run property change revisions
305
- */
306
- getAllRunPropertiesChanges(): Revision[] {
307
- return this.getRevisionsByType('runPropertiesChange');
308
- }
309
-
310
- /**
311
- * Gets all paragraph properties changes
312
- * @returns Array of paragraph property change revisions
313
- */
314
- getAllParagraphPropertiesChanges(): Revision[] {
315
- return this.getRevisionsByType('paragraphPropertiesChange');
316
- }
317
-
318
- /**
319
- * Gets all table properties changes
320
- * @returns Array of table property change revisions
321
- */
322
- getAllTablePropertiesChanges(): Revision[] {
323
- return this.getRevisionsByType('tablePropertiesChange');
324
- }
325
-
326
- /**
327
- * Gets all move operations (both moveFrom and moveTo)
328
- * @returns Array of move-related revisions
329
- */
330
- getAllMoves(): Revision[] {
331
- return this.revisions.filter(rev =>
332
- rev.getType() === 'moveFrom' || rev.getType() === 'moveTo'
333
- );
334
- }
335
-
336
- /**
337
- * Gets all moveFrom revisions (source of moves)
338
- * @returns Array of moveFrom revisions
339
- */
340
- getAllMoveFrom(): Revision[] {
341
- return this.getRevisionsByType('moveFrom');
342
- }
343
-
344
- /**
345
- * Gets all moveTo revisions (destination of moves)
346
- * @returns Array of moveTo revisions
347
- */
348
- getAllMoveTo(): Revision[] {
349
- return this.getRevisionsByType('moveTo');
350
- }
351
-
352
- /**
353
- * Gets all table cell changes (insert, delete, merge)
354
- * @returns Array of table cell change revisions
355
- */
356
- getAllTableCellChanges(): Revision[] {
357
- return this.revisions.filter(rev =>
358
- rev.getType() === 'tableCellInsert' ||
359
- rev.getType() === 'tableCellDelete' ||
360
- rev.getType() === 'tableCellMerge'
361
- );
362
- }
363
-
364
- /**
365
- * Gets all numbering changes
366
- * @returns Array of numbering change revisions
367
- */
368
- getAllNumberingChanges(): Revision[] {
369
- return this.getRevisionsByType('numberingChange');
370
- }
371
-
372
- /**
373
- * Gets all property change revisions (run, paragraph, table, etc.)
374
- * @returns Array of all property change revisions
375
- */
376
- getAllPropertyChanges(): Revision[] {
377
- return this.revisions.filter(rev =>
378
- rev.getType() === 'runPropertiesChange' ||
379
- rev.getType() === 'paragraphPropertiesChange' ||
380
- rev.getType() === 'tablePropertiesChange' ||
381
- rev.getType() === 'tableRowPropertiesChange' ||
382
- rev.getType() === 'tableCellPropertiesChange' ||
383
- rev.getType() === 'sectionPropertiesChange' ||
384
- rev.getType() === 'numberingChange'
385
- );
386
- }
387
-
388
- /**
389
- * Gets move pair by move ID
390
- * @param moveId - Move operation ID
391
- * @returns Object with moveFrom and moveTo revisions (if found)
392
- */
393
- getMovePair(moveId: string): { moveFrom?: Revision; moveTo?: Revision } {
394
- const moveFrom = this.revisions.find(
395
- rev => rev.getType() === 'moveFrom' && rev.getMoveId() === moveId
396
- );
397
- const moveTo = this.revisions.find(
398
- rev => rev.getType() === 'moveTo' && rev.getMoveId() === moveId
399
- );
400
- return { moveFrom, moveTo };
401
- }
402
-
403
- /**
404
- * Gets statistics about revisions
405
- * @returns Object with revision statistics
406
- */
407
- getStats(): {
408
- total: number;
409
- insertions: number;
410
- deletions: number;
411
- propertyChanges: number;
412
- moves: number;
413
- tableCellChanges: number;
414
- authors: string[];
415
- nextId: number;
416
- } {
417
- return {
418
- total: this.revisions.length,
419
- insertions: this.getInsertionCount(),
420
- deletions: this.getDeletionCount(),
421
- propertyChanges: this.getAllPropertyChanges().length,
422
- moves: this.getAllMoves().length,
423
- tableCellChanges: this.getAllTableCellChanges().length,
424
- authors: this.getAuthors(),
425
- nextId: this.nextId,
426
- };
427
- }
428
-
429
- /**
430
- * Checks if track changes is enabled (has any revisions)
431
- * @returns True if there are revisions
432
- */
433
- isTrackingChanges(): boolean {
434
- return this.revisions.length > 0;
435
- }
436
-
437
- /**
438
- * Gets the most recent revision
439
- * @returns The most recent revision, or undefined if no revisions
440
- */
441
- getLatestRevision(): Revision | undefined {
442
- if (this.revisions.length === 0) {
443
- return undefined;
444
- }
445
- return this.revisions[this.revisions.length - 1];
446
- }
447
-
448
- /**
449
- * Gets revisions within a date range
450
- * @param startDate - Start of date range
451
- * @param endDate - End of date range
452
- * @returns Array of revisions within the date range
453
- */
454
- getRevisionsByDateRange(startDate: Date, endDate: Date): Revision[] {
455
- return this.revisions.filter(rev => {
456
- const revDate = rev.getDate();
457
- return revDate >= startDate && revDate <= endDate;
458
- });
459
- }
460
-
461
- /**
462
- * Gets the next available revision ID without consuming it.
463
- *
464
- * This is an alias for peekNextId() for backward compatibility.
465
- * Use consumeNextId() if you need to reserve an ID for manual use.
466
- *
467
- * @returns Next available revision ID (without consuming it)
468
- * @see consumeNextId for reserving IDs
469
- * @see register for automatic ID assignment
470
- */
471
- getNextId(): number {
472
- return this.nextId;
473
- }
474
-
475
- /**
476
- * Peeks at the next revision ID without incrementing
477
- * @returns Next available revision ID (without consuming it)
478
- */
479
- peekNextId(): number {
480
- return this.nextId;
481
- }
482
-
483
- /**
484
- * Consumes and returns the next revision ID.
485
- *
486
- * Use this when you need to manually assign an ID to a revision
487
- * that won't be registered through register(). The ID is reserved
488
- * and won't be reused by subsequent register() calls.
489
- *
490
- * When a centralized ID provider is set, IDs come from the shared counter.
491
- *
492
- * @returns The consumed revision ID
493
- *
494
- * @example
495
- * ```typescript
496
- * // Reserve an ID for manual assignment
497
- * const id = revisionManager.consumeNextId();
498
- * revision.setId(id);
499
- * // Don't call register() - the ID is already consumed
500
- * ```
501
- */
502
- consumeNextId(): number {
503
- // Use centralized provider if available
504
- return this.idProvider ? this.idProvider() : this.nextId++;
505
- }
506
-
507
- /**
508
- * Sets the next ID to be assigned.
509
- * Used when loading documents to avoid ID collisions with existing revisions.
510
- * @param id - The next ID value to use
511
- */
512
- setNextId(id: number): void {
513
- this.nextId = id;
514
- }
515
-
516
- /**
517
- * Creates a new RevisionManager
518
- * @returns New RevisionManager instance
519
- */
520
- static create(): RevisionManager {
521
- return new RevisionManager();
522
- }
523
-
524
- // ═══════════════════════════════════════════════════════════════════════════
525
- // NEW METHODS - Added for ChangelogGenerator and RevisionAwareProcessor
526
- // ═══════════════════════════════════════════════════════════════════════════
527
-
528
- /**
529
- * Check if any revisions exist in the manager.
530
- * @returns True if there are any revisions
531
- */
532
- hasRevisions(): boolean {
533
- return this.revisions.length > 0;
534
- }
535
-
536
- /**
537
- * Get revisions by semantic category.
538
- *
539
- * Categories:
540
- * - content: insert, delete, imageChange, fieldChange, commentChange, contentControlChange, hyperlinkChange
541
- * - formatting: runPropertiesChange, paragraphPropertiesChange, numberingChange
542
- * - structural: moveFrom, moveTo, sectionPropertiesChange, bookmarkChange
543
- * - table: tablePropertiesChange, tableCellInsert, tableCellDelete, tableCellMerge, etc.
544
- *
545
- * @param category - Semantic category to filter by
546
- * @returns Array of revisions in the specified category
547
- */
548
- getByCategory(category: RevisionCategory): Revision[] {
549
- // Check cache first
550
- if (this.revisionsByCategoryCache.has(category)) {
551
- return [...this.revisionsByCategoryCache.get(category)!];
552
- }
553
-
554
- // Compute and cache
555
- const result = this.revisions.filter(rev => {
556
- const type = rev.getType();
557
- switch (category) {
558
- case 'content':
559
- return type === 'insert' ||
560
- type === 'delete' ||
561
- // Internal tracking types for rich content changes
562
- type === 'imageChange' ||
563
- type === 'fieldChange' ||
564
- type === 'commentChange' ||
565
- type === 'contentControlChange' ||
566
- type === 'hyperlinkChange';
567
-
568
- case 'formatting':
569
- return type === 'runPropertiesChange' ||
570
- type === 'paragraphPropertiesChange' ||
571
- type === 'numberingChange';
572
-
573
- case 'structural':
574
- return type === 'moveFrom' ||
575
- type === 'moveTo' ||
576
- type === 'sectionPropertiesChange' ||
577
- // Bookmarks are structural markers
578
- type === 'bookmarkChange';
579
-
580
- case 'table':
581
- return type === 'tablePropertiesChange' ||
582
- type === 'tableExceptionPropertiesChange' ||
583
- type === 'tableRowPropertiesChange' ||
584
- type === 'tableCellPropertiesChange' ||
585
- type === 'tableCellInsert' ||
586
- type === 'tableCellDelete' ||
587
- type === 'tableCellMerge';
588
-
589
- default:
590
- return false;
591
- }
592
- });
593
- this.revisionsByCategoryCache.set(category, result);
594
- return [...result];
595
- }
596
-
597
- /**
598
- * Get revisions affecting a specific paragraph.
599
- *
600
- * Uses the revision's location data if available. Returns revisions
601
- * where location.paragraphIndex matches the specified index.
602
- *
603
- * Note: Revisions must have location data set (via setLocation()) for
604
- * accurate filtering. Revisions without location data are excluded.
605
- *
606
- * @param paragraphIndex - Index of the paragraph (0-based)
607
- * @returns Array of revisions affecting the specified paragraph
608
- *
609
- * @example
610
- * ```typescript
611
- * const revisions = revisionManager.getRevisionsForParagraph(3);
612
- * console.log(`${revisions.length} revisions affect paragraph 3`);
613
- * ```
614
- */
615
- getRevisionsForParagraph(paragraphIndex: number): Revision[] {
616
- if (paragraphIndex < 0) {
617
- return [];
618
- }
619
- return this.revisions.filter(rev => {
620
- const loc = rev.getLocation();
621
- if (!loc) return false;
622
- return loc.paragraphIndex === paragraphIndex;
623
- });
624
- }
625
-
626
- /**
627
- * Get summary statistics for all revisions.
628
- * Provides comprehensive breakdown by type, category, and author.
629
- *
630
- * @returns Summary statistics object
631
- */
632
- getSummary(): RevisionSummary {
633
- const byCategory: Record<RevisionCategory, number> = {
634
- content: 0,
635
- formatting: 0,
636
- structural: 0,
637
- table: 0,
638
- };
639
-
640
- let earliest: Date | null = null;
641
- let latest: Date | null = null;
642
-
643
- // Count by category and track date range
644
- for (const rev of this.revisions) {
645
- const type = rev.getType();
646
- const date = rev.getDate();
647
-
648
- // Update date range
649
- if (!earliest || date < earliest) earliest = date;
650
- if (!latest || date > latest) latest = date;
651
-
652
- // Categorize
653
- if (type === 'insert' || type === 'delete') {
654
- byCategory.content++;
655
- } else if (
656
- type === 'runPropertiesChange' ||
657
- type === 'paragraphPropertiesChange' ||
658
- type === 'numberingChange'
659
- ) {
660
- byCategory.formatting++;
661
- } else if (
662
- type === 'moveFrom' ||
663
- type === 'moveTo' ||
664
- type === 'sectionPropertiesChange'
665
- ) {
666
- byCategory.structural++;
667
- } else if (
668
- type === 'tablePropertiesChange' ||
669
- type === 'tableExceptionPropertiesChange' ||
670
- type === 'tableRowPropertiesChange' ||
671
- type === 'tableCellPropertiesChange' ||
672
- type === 'tableCellInsert' ||
673
- type === 'tableCellDelete' ||
674
- type === 'tableCellMerge'
675
- ) {
676
- byCategory.table++;
677
- }
678
- }
679
-
680
- const summary = {
681
- total: this.revisions.length,
682
- byType: {
683
- insertions: this.getInsertionCount(),
684
- deletions: this.getDeletionCount(),
685
- moves: this.getAllMoves().length,
686
- propertyChanges: this.getAllPropertyChanges().length,
687
- tableChanges: this.getAllTableCellChanges().length,
688
- },
689
- byCategory,
690
- authors: this.getAuthors(),
691
- dateRange: earliest && latest ? { earliest, latest } : null,
692
- };
693
-
694
- if (summary.total > 0) {
695
- getLogger().info('Revision summary', {
696
- total: summary.total,
697
- ins: summary.byType.insertions,
698
- del: summary.byType.deletions,
699
- fmt: summary.byType.propertyChanges,
700
- authors: summary.authors.length
701
- });
702
- }
703
-
704
- return summary;
705
- }
706
-
707
- /**
708
- * Get a revision by its ID.
709
- *
710
- * @param id - Revision ID to find
711
- * @returns Revision with the specified ID, or undefined
712
- */
713
- getById(id: number): Revision | undefined {
714
- return this.revisions.find(rev => rev.getId() === id);
715
- }
716
-
717
- /**
718
- * Remove a revision by its ID.
719
- *
720
- * @param id - ID of the revision to remove
721
- * @returns True if revision was found and removed
722
- */
723
- removeById(id: number): boolean {
724
- const index = this.revisions.findIndex(rev => rev.getId() === id);
725
- if (index === -1) return false;
726
-
727
- this.revisions.splice(index, 1);
728
- this.invalidateCache();
729
- return true;
730
- }
731
-
732
- /**
733
- * Get revisions matching multiple criteria.
734
- *
735
- * @param criteria - Filter criteria
736
- * @returns Array of matching revisions
737
- */
738
- getMatching(criteria: {
739
- types?: RevisionType[];
740
- authors?: string[];
741
- categories?: RevisionCategory[];
742
- dateRange?: { start: Date; end: Date };
743
- }): Revision[] {
744
- return this.revisions.filter(rev => {
745
- // Filter by types
746
- if (criteria.types && !criteria.types.includes(rev.getType())) {
747
- return false;
748
- }
749
-
750
- // Filter by authors
751
- if (criteria.authors && !criteria.authors.includes(rev.getAuthor())) {
752
- return false;
753
- }
754
-
755
- // Filter by categories
756
- if (criteria.categories) {
757
- const revCategory = this.getRevisionCategory(rev);
758
- if (!criteria.categories.includes(revCategory)) {
759
- return false;
760
- }
761
- }
762
-
763
- // Filter by date range
764
- if (criteria.dateRange) {
765
- const date = rev.getDate();
766
- if (date < criteria.dateRange.start || date > criteria.dateRange.end) {
767
- return false;
768
- }
769
- }
770
-
771
- return true;
772
- });
773
- }
774
-
775
- /**
776
- * Get the semantic category of a revision.
777
- * @internal
778
- */
779
- private getRevisionCategory(revision: Revision): RevisionCategory {
780
- const type = revision.getType();
781
-
782
- if (type === 'insert' || type === 'delete') {
783
- return 'content';
784
- }
785
- if (
786
- type === 'runPropertiesChange' ||
787
- type === 'paragraphPropertiesChange' ||
788
- type === 'numberingChange'
789
- ) {
790
- return 'formatting';
791
- }
792
- if (
793
- type === 'moveFrom' ||
794
- type === 'moveTo' ||
795
- type === 'sectionPropertiesChange'
796
- ) {
797
- return 'structural';
798
- }
799
- if (
800
- type === 'tablePropertiesChange' ||
801
- type === 'tableExceptionPropertiesChange' ||
802
- type === 'tableRowPropertiesChange' ||
803
- type === 'tableCellPropertiesChange' ||
804
- type === 'tableCellInsert' ||
805
- type === 'tableCellDelete' ||
806
- type === 'tableCellMerge'
807
- ) {
808
- return 'table';
809
- }
810
-
811
- // Default
812
- return 'content';
813
- }
814
-
815
- // ============================================================
816
- // Location-Aware Methods
817
- // ============================================================
818
-
819
- /**
820
- * Gets revisions affecting a specific run within a paragraph.
821
- *
822
- * Uses the revision's location data if available.
823
- *
824
- * @param paragraphIndex - Index of the paragraph (0-based)
825
- * @param runIndex - Index of the run within the paragraph (0-based)
826
- * @returns Array of revisions affecting the specified run
827
- *
828
- * @example
829
- * ```typescript
830
- * const revisions = revisionManager.getRevisionsForRun(0, 2);
831
- * console.log(`${revisions.length} revisions affect run 2 in paragraph 0`);
832
- * ```
833
- */
834
- getRevisionsForRun(paragraphIndex: number, runIndex: number): Revision[] {
835
- return this.revisions.filter(rev => {
836
- const loc = rev.getLocation();
837
- if (!loc) return false;
838
- return loc.paragraphIndex === paragraphIndex && loc.runIndex === runIndex;
839
- });
840
- }
841
-
842
- /**
843
- * Gets revisions by location criteria.
844
- *
845
- * Filters revisions based on their location within the document structure.
846
- * All specified criteria must match (AND logic).
847
- *
848
- * @param criteria - Location filter criteria
849
- * @returns Array of revisions matching the criteria
850
- *
851
- * @example
852
- * ```typescript
853
- * // Get all revisions in paragraph 5
854
- * const paraRevisions = revisionManager.getRevisionsByLocation({
855
- * paragraphIndex: 5
856
- * });
857
- *
858
- * // Get all revisions in table row 2, cell 1
859
- * const cellRevisions = revisionManager.getRevisionsByLocation({
860
- * tableRow: 2,
861
- * tableCell: 1
862
- * });
863
- * ```
864
- */
865
- getRevisionsByLocation(criteria: Partial<RevisionLocation>): Revision[] {
866
- return this.revisions.filter(rev => {
867
- const loc = rev.getLocation();
868
- if (!loc) return false;
869
-
870
- // Check each criteria if specified
871
- if (criteria.paragraphIndex !== undefined &&
872
- loc.paragraphIndex !== criteria.paragraphIndex) {
873
- return false;
874
- }
875
- if (criteria.runIndex !== undefined &&
876
- loc.runIndex !== criteria.runIndex) {
877
- return false;
878
- }
879
- if (criteria.tableRow !== undefined &&
880
- loc.tableRow !== criteria.tableRow) {
881
- return false;
882
- }
883
- if (criteria.tableCell !== undefined &&
884
- loc.tableCell !== criteria.tableCell) {
885
- return false;
886
- }
887
- if (criteria.sectionIndex !== undefined &&
888
- loc.sectionIndex !== criteria.sectionIndex) {
889
- return false;
890
- }
891
- if (criteria.headerFooterType !== undefined &&
892
- loc.headerFooterType !== criteria.headerFooterType) {
893
- return false;
894
- }
895
-
896
- return true;
897
- });
898
- }
899
-
900
- /**
901
- * Gets revisions that have location data.
902
- *
903
- * @returns Array of revisions with location information
904
- */
905
- getRevisionsWithLocation(): Revision[] {
906
- return this.revisions.filter(rev => rev.getLocation() !== undefined);
907
- }
908
-
909
- /**
910
- * Gets revisions that do NOT have location data.
911
- *
912
- * @returns Array of revisions without location information
913
- */
914
- getRevisionsWithoutLocation(): Revision[] {
915
- return this.revisions.filter(rev => rev.getLocation() === undefined);
916
- }
917
-
918
- // ============================================================
919
- // Validation Methods
920
- // ============================================================
921
-
922
- /**
923
- * Validates that all revision IDs are unique.
924
- *
925
- * Per ECMA-376, revision IDs must be unique within a document.
926
- * Duplicate IDs can cause Word to reject the document or
927
- * produce unexpected behavior.
928
- *
929
- * @returns Validation result with any duplicate IDs found
930
- *
931
- * @example
932
- * ```typescript
933
- * const result = revisionManager.validateRevisionIds();
934
- * if (!result.valid) {
935
- * console.error('Duplicate IDs found:', result.duplicates);
936
- * }
937
- * ```
938
- */
939
- validateRevisionIds(): { valid: boolean; duplicates: number[] } {
940
- const seen = new Set<number>();
941
- const duplicates: number[] = [];
942
-
943
- for (const rev of this.revisions) {
944
- const id = rev.getId();
945
- if (seen.has(id)) {
946
- if (!duplicates.includes(id)) {
947
- duplicates.push(id);
948
- }
949
- }
950
- seen.add(id);
951
- }
952
-
953
- return {
954
- valid: duplicates.length === 0,
955
- duplicates,
956
- };
957
- }
958
-
959
- /**
960
- * Reassigns all revision IDs to ensure uniqueness.
961
- *
962
- * This is useful after merging documents or when duplicate
963
- * IDs are detected. IDs are reassigned sequentially starting
964
- * from the specified value.
965
- *
966
- * @param startId - Starting ID value (default: 0)
967
- * @returns Number of IDs reassigned
968
- *
969
- * @example
970
- * ```typescript
971
- * const count = revisionManager.reassignRevisionIds();
972
- * console.log(`Reassigned ${count} revision IDs`);
973
- * ```
974
- */
975
- reassignRevisionIds(startId = 0): number {
976
- let currentId = startId;
977
-
978
- for (const rev of this.revisions) {
979
- rev.setId(currentId++);
980
- }
981
-
982
- // Update nextId to continue from where we left off
983
- this.nextId = currentId;
984
-
985
- return this.revisions.length;
986
- }
987
-
988
- /**
989
- * Validates move operation pairs (moveFrom/moveTo).
990
- *
991
- * Each moveFrom must have a matching moveTo with the same moveId,
992
- * and vice versa. Orphaned move markers can cause document corruption.
993
- *
994
- * @returns Validation result with orphaned move IDs
995
- *
996
- * @example
997
- * ```typescript
998
- * const result = revisionManager.validateMovePairs();
999
- * if (!result.valid) {
1000
- * console.error('Orphaned moveFrom IDs:', result.orphanedMoveFrom);
1001
- * console.error('Orphaned moveTo IDs:', result.orphanedMoveTo);
1002
- * }
1003
- * ```
1004
- */
1005
- validateMovePairs(): {
1006
- valid: boolean;
1007
- orphanedMoveFrom: string[];
1008
- orphanedMoveTo: string[];
1009
- } {
1010
- const moveFromIds = new Map<string, Revision>();
1011
- const moveToIds = new Map<string, Revision>();
1012
-
1013
- for (const rev of this.revisions) {
1014
- const moveId = rev.getMoveId();
1015
- if (!moveId) continue;
1016
-
1017
- if (rev.getType() === 'moveFrom') {
1018
- moveFromIds.set(moveId, rev);
1019
- } else if (rev.getType() === 'moveTo') {
1020
- moveToIds.set(moveId, rev);
1021
- }
1022
- }
1023
-
1024
- const orphanedMoveFrom: string[] = [];
1025
- const orphanedMoveTo: string[] = [];
1026
-
1027
- // Find moveFrom without matching moveTo
1028
- for (const moveId of moveFromIds.keys()) {
1029
- if (!moveToIds.has(moveId)) {
1030
- orphanedMoveFrom.push(moveId);
1031
- }
1032
- }
1033
-
1034
- // Find moveTo without matching moveFrom
1035
- for (const moveId of moveToIds.keys()) {
1036
- if (!moveFromIds.has(moveId)) {
1037
- orphanedMoveTo.push(moveId);
1038
- }
1039
- }
1040
-
1041
- return {
1042
- valid: orphanedMoveFrom.length === 0 && orphanedMoveTo.length === 0,
1043
- orphanedMoveFrom,
1044
- orphanedMoveTo,
1045
- };
1046
- }
1047
-
1048
- /**
1049
- * Gets the highest revision ID currently in use.
1050
- *
1051
- * @returns Highest ID, or -1 if no revisions exist
1052
- */
1053
- getHighestId(): number {
1054
- if (this.revisions.length === 0) return -1;
1055
- return Math.max(...this.revisions.map(r => r.getId()));
1056
- }
1057
-
1058
- /**
1059
- * Ensures the next ID is higher than all existing IDs.
1060
- *
1061
- * Useful after loading a document with existing revisions
1062
- * to prevent ID conflicts with new revisions.
1063
- */
1064
- syncNextId(): void {
1065
- const highestId = this.getHighestId();
1066
- if (highestId >= this.nextId) {
1067
- this.nextId = highestId + 1;
1068
- }
1069
- }
1070
- }
1
+ /**
2
+ * RevisionManager - Manages tracked changes (revisions) in a document
3
+ *
4
+ * Tracks all revisions, assigns unique IDs, and provides statistics.
5
+ */
6
+
7
+ import { Revision, RevisionType } from './Revision';
8
+ import type { RevisionLocation } from './PropertyChangeTypes';
9
+ import { getGlobalLogger, createScopedLogger, ILogger } from '../utils/logger';
10
+
11
+ // Scoped logger for RevisionManager
12
+ function getLogger(): ILogger {
13
+ return createScopedLogger(getGlobalLogger(), 'RevisionManager');
14
+ }
15
+
16
+ /**
17
+ * Type for the centralized ID provider callback.
18
+ * Returns the next available annotation ID from a shared counter.
19
+ */
20
+ export type IdProviderCallback = () => number;
21
+
22
+ /**
23
+ * Type for callback to notify of existing IDs (for synchronization).
24
+ * Called when registering existing revisions to keep the central counter in sync.
25
+ */
26
+ export type IdExistsCallback = (existingId: number) => void;
27
+
28
+ /**
29
+ * Semantic category for grouping revisions.
30
+ */
31
+ export type RevisionCategory =
32
+ | 'content' // Text insertions, deletions
33
+ | 'formatting' // Run/paragraph property changes
34
+ | 'structural' // Moves, section changes
35
+ | 'table'; // Table structure changes
36
+
37
+ /**
38
+ * Summary statistics for revisions.
39
+ */
40
+ export interface RevisionSummary {
41
+ total: number;
42
+ byType: {
43
+ insertions: number;
44
+ deletions: number;
45
+ moves: number;
46
+ propertyChanges: number;
47
+ tableChanges: number;
48
+ };
49
+ byCategory: Record<RevisionCategory, number>;
50
+ authors: string[];
51
+ dateRange: { earliest: Date; latest: Date } | null;
52
+ }
53
+
54
+ /**
55
+ * Manages document revisions (track changes)
56
+ *
57
+ * Per ECMA-376, revision IDs must be unique across ALL annotation types
58
+ * in a document. Use setIdProvider() to connect to a centralized ID allocator.
59
+ */
60
+ export class RevisionManager {
61
+ private revisions: Revision[] = [];
62
+ private nextId = 0;
63
+ private idProvider: IdProviderCallback | null = null;
64
+ private idExistsNotifier: IdExistsCallback | null = null;
65
+
66
+ // Performance caching for frequently accessed filtered results
67
+ private revisionsByTypeCache = new Map<RevisionType, Revision[]>();
68
+ private revisionsByAuthorCache = new Map<string, Revision[]>();
69
+ private revisionsByCategoryCache = new Map<RevisionCategory, Revision[]>();
70
+ private cacheValid = true;
71
+
72
+ /**
73
+ * Invalidates all caches. Called when revisions are added/removed.
74
+ * @private
75
+ */
76
+ private invalidateCache(): void {
77
+ this.revisionsByTypeCache.clear();
78
+ this.revisionsByAuthorCache.clear();
79
+ this.revisionsByCategoryCache.clear();
80
+ this.cacheValid = false;
81
+ }
82
+
83
+ /**
84
+ * Sets the centralized ID provider callback.
85
+ * When set, IDs will be allocated from the centralized DocumentIdManager
86
+ * instead of the local nextId counter.
87
+ *
88
+ * @param provider - Callback that returns the next available ID
89
+ * @param existsNotifier - Optional callback to notify when existing IDs are found
90
+ */
91
+ setIdProvider(provider: IdProviderCallback, existsNotifier?: IdExistsCallback): void {
92
+ this.idProvider = provider;
93
+ this.idExistsNotifier = existsNotifier || null;
94
+ }
95
+
96
+ /**
97
+ * Registers a revision with the manager
98
+ * Assigns a unique ID
99
+ * @param revision - Revision to register
100
+ * @returns The registered revision (same instance)
101
+ */
102
+ register(revision: Revision): Revision {
103
+ const logger = getLogger();
104
+ // Assign unique ID - use centralized provider if available
105
+ const id = this.idProvider ? this.idProvider() : this.nextId++;
106
+ revision.setId(id);
107
+
108
+ // Store revision
109
+ this.revisions.push(revision);
110
+ this.invalidateCache();
111
+
112
+ logger.debug('Revision registered', {
113
+ id: revision.getId(),
114
+ type: revision.getType(),
115
+ author: revision.getAuthor(),
116
+ });
117
+
118
+ return revision;
119
+ }
120
+
121
+ /**
122
+ * Registers an existing revision (from parsing) with its pre-assigned ID.
123
+ * Unlike register(), this does NOT assign a new ID - preserves the original
124
+ * ID from parsed XML. Used when loading documents to avoid overwriting
125
+ * revision IDs that are already correct.
126
+ *
127
+ * @param revision - Revision with ID already set from XML parsing
128
+ * @returns The registered revision (same instance)
129
+ */
130
+ registerExisting(revision: Revision): Revision {
131
+ const logger = getLogger();
132
+ const existingId = revision.getId();
133
+
134
+ // Notify centralized ID manager about this existing ID
135
+ // This ensures the shared counter stays above all existing IDs
136
+ if (this.idExistsNotifier) {
137
+ this.idExistsNotifier(existingId);
138
+ }
139
+
140
+ // Also update local nextId to avoid collisions (fallback when no provider)
141
+ if (existingId >= this.nextId) {
142
+ this.nextId = existingId + 1;
143
+ }
144
+
145
+ // Store revision (keep its existing ID - do NOT overwrite)
146
+ this.revisions.push(revision);
147
+ this.invalidateCache();
148
+
149
+ logger.debug('Existing revision registered', {
150
+ id: existingId,
151
+ type: revision.getType(),
152
+ author: revision.getAuthor(),
153
+ });
154
+
155
+ return revision;
156
+ }
157
+
158
+ /**
159
+ * Gets all revisions
160
+ * @returns Array of all revisions
161
+ */
162
+ getAllRevisions(): Revision[] {
163
+ return [...this.revisions];
164
+ }
165
+
166
+ /**
167
+ * Gets revisions by type
168
+ * Uses caching for improved performance on repeated calls
169
+ * @param type - Revision type to filter by
170
+ * @returns Array of revisions of the specified type
171
+ */
172
+ getRevisionsByType(type: RevisionType): Revision[] {
173
+ // Check cache first
174
+ if (this.revisionsByTypeCache.has(type)) {
175
+ return [...this.revisionsByTypeCache.get(type)!];
176
+ }
177
+
178
+ // Compute and cache
179
+ const result = this.revisions.filter((rev) => rev.getType() === type);
180
+ this.revisionsByTypeCache.set(type, result);
181
+ return [...result];
182
+ }
183
+
184
+ /**
185
+ * Gets revisions by author
186
+ * Uses caching for improved performance on repeated calls
187
+ * @param author - Author name to filter by
188
+ * @returns Array of revisions by the specified author
189
+ */
190
+ getRevisionsByAuthor(author: string): Revision[] {
191
+ // Check cache first
192
+ if (this.revisionsByAuthorCache.has(author)) {
193
+ return [...this.revisionsByAuthorCache.get(author)!];
194
+ }
195
+
196
+ // Compute and cache
197
+ const result = this.revisions.filter((rev) => rev.getAuthor() === author);
198
+ this.revisionsByAuthorCache.set(author, result);
199
+ return [...result];
200
+ }
201
+
202
+ /**
203
+ * Gets the number of revisions
204
+ * @returns Number of revisions
205
+ */
206
+ getCount(): number {
207
+ return this.revisions.length;
208
+ }
209
+
210
+ /**
211
+ * Gets the number of insertions
212
+ * @returns Number of insertion revisions
213
+ */
214
+ getInsertionCount(): number {
215
+ return this.getRevisionsByType('insert').length;
216
+ }
217
+
218
+ /**
219
+ * Gets the number of deletions
220
+ * @returns Number of deletion revisions
221
+ */
222
+ getDeletionCount(): number {
223
+ return this.getRevisionsByType('delete').length;
224
+ }
225
+
226
+ /**
227
+ * Gets all unique authors who have made changes
228
+ * @returns Array of unique author names
229
+ */
230
+ getAuthors(): string[] {
231
+ const authorsSet = new Set<string>();
232
+ for (const revision of this.revisions) {
233
+ authorsSet.add(revision.getAuthor());
234
+ }
235
+ return Array.from(authorsSet);
236
+ }
237
+
238
+ /**
239
+ * Clears all revisions
240
+ */
241
+ clear(): void {
242
+ const count = this.revisions.length;
243
+ this.revisions = [];
244
+ this.nextId = 0;
245
+ this.invalidateCache();
246
+ if (count > 0) {
247
+ getLogger().info('Revisions cleared', { previousCount: count });
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Checks if there are no revisions
253
+ * @returns True if there are no tracked changes
254
+ */
255
+ isEmpty(): boolean {
256
+ return this.revisions.length === 0;
257
+ }
258
+
259
+ /**
260
+ * Gets the most recent N revisions
261
+ * @param count - Number of recent revisions to return
262
+ * @returns Array of most recent revisions
263
+ */
264
+ getRecentRevisions(count: number): Revision[] {
265
+ return [...this.revisions]
266
+ .sort((a, b) => b.getDate().getTime() - a.getDate().getTime())
267
+ .slice(0, count);
268
+ }
269
+
270
+ /**
271
+ * Searches revisions by text content
272
+ * @param searchText - Text to search for (case-insensitive)
273
+ * @returns Array of revisions containing the search text
274
+ */
275
+ findRevisionsByText(searchText: string): Revision[] {
276
+ const lowerSearch = searchText.toLowerCase();
277
+ return this.revisions.filter((revision) => {
278
+ const text = revision
279
+ .getRuns()
280
+ .map((run) => run.getText())
281
+ .join('')
282
+ .toLowerCase();
283
+ return text.includes(lowerSearch);
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Gets all insertions (added text)
289
+ * @returns Array of insertion revisions
290
+ */
291
+ getAllInsertions(): Revision[] {
292
+ return this.getRevisionsByType('insert');
293
+ }
294
+
295
+ /**
296
+ * Gets all deletions (removed text)
297
+ * @returns Array of deletion revisions
298
+ */
299
+ getAllDeletions(): Revision[] {
300
+ return this.getRevisionsByType('delete');
301
+ }
302
+
303
+ /**
304
+ * Gets all run properties changes (formatting changes)
305
+ * @returns Array of run property change revisions
306
+ */
307
+ getAllRunPropertiesChanges(): Revision[] {
308
+ return this.getRevisionsByType('runPropertiesChange');
309
+ }
310
+
311
+ /**
312
+ * Gets all paragraph properties changes
313
+ * @returns Array of paragraph property change revisions
314
+ */
315
+ getAllParagraphPropertiesChanges(): Revision[] {
316
+ return this.getRevisionsByType('paragraphPropertiesChange');
317
+ }
318
+
319
+ /**
320
+ * Gets all table properties changes
321
+ * @returns Array of table property change revisions
322
+ */
323
+ getAllTablePropertiesChanges(): Revision[] {
324
+ return this.getRevisionsByType('tablePropertiesChange');
325
+ }
326
+
327
+ /**
328
+ * Gets all move operations (both moveFrom and moveTo)
329
+ * @returns Array of move-related revisions
330
+ */
331
+ getAllMoves(): Revision[] {
332
+ return this.revisions.filter(
333
+ (rev) => rev.getType() === 'moveFrom' || rev.getType() === 'moveTo'
334
+ );
335
+ }
336
+
337
+ /**
338
+ * Gets all moveFrom revisions (source of moves)
339
+ * @returns Array of moveFrom revisions
340
+ */
341
+ getAllMoveFrom(): Revision[] {
342
+ return this.getRevisionsByType('moveFrom');
343
+ }
344
+
345
+ /**
346
+ * Gets all moveTo revisions (destination of moves)
347
+ * @returns Array of moveTo revisions
348
+ */
349
+ getAllMoveTo(): Revision[] {
350
+ return this.getRevisionsByType('moveTo');
351
+ }
352
+
353
+ /**
354
+ * Gets all table cell changes (insert, delete, merge)
355
+ * @returns Array of table cell change revisions
356
+ */
357
+ getAllTableCellChanges(): Revision[] {
358
+ return this.revisions.filter(
359
+ (rev) =>
360
+ rev.getType() === 'tableCellInsert' ||
361
+ rev.getType() === 'tableCellDelete' ||
362
+ rev.getType() === 'tableCellMerge'
363
+ );
364
+ }
365
+
366
+ /**
367
+ * Gets all numbering changes
368
+ * @returns Array of numbering change revisions
369
+ */
370
+ getAllNumberingChanges(): Revision[] {
371
+ return this.getRevisionsByType('numberingChange');
372
+ }
373
+
374
+ /**
375
+ * Gets all property change revisions (run, paragraph, table, etc.)
376
+ * @returns Array of all property change revisions
377
+ */
378
+ getAllPropertyChanges(): Revision[] {
379
+ return this.revisions.filter(
380
+ (rev) =>
381
+ rev.getType() === 'runPropertiesChange' ||
382
+ rev.getType() === 'paragraphPropertiesChange' ||
383
+ rev.getType() === 'tablePropertiesChange' ||
384
+ rev.getType() === 'tableRowPropertiesChange' ||
385
+ rev.getType() === 'tableCellPropertiesChange' ||
386
+ rev.getType() === 'sectionPropertiesChange' ||
387
+ rev.getType() === 'numberingChange'
388
+ );
389
+ }
390
+
391
+ /**
392
+ * Gets move pair by move ID
393
+ * @param moveId - Move operation ID
394
+ * @returns Object with moveFrom and moveTo revisions (if found)
395
+ */
396
+ getMovePair(moveId: string): { moveFrom?: Revision; moveTo?: Revision } {
397
+ const moveFrom = this.revisions.find(
398
+ (rev) => rev.getType() === 'moveFrom' && rev.getMoveId() === moveId
399
+ );
400
+ const moveTo = this.revisions.find(
401
+ (rev) => rev.getType() === 'moveTo' && rev.getMoveId() === moveId
402
+ );
403
+ return { moveFrom, moveTo };
404
+ }
405
+
406
+ /**
407
+ * Gets statistics about revisions
408
+ * @returns Object with revision statistics
409
+ */
410
+ getStats(): {
411
+ total: number;
412
+ insertions: number;
413
+ deletions: number;
414
+ propertyChanges: number;
415
+ moves: number;
416
+ tableCellChanges: number;
417
+ authors: string[];
418
+ nextId: number;
419
+ } {
420
+ return {
421
+ total: this.revisions.length,
422
+ insertions: this.getInsertionCount(),
423
+ deletions: this.getDeletionCount(),
424
+ propertyChanges: this.getAllPropertyChanges().length,
425
+ moves: this.getAllMoves().length,
426
+ tableCellChanges: this.getAllTableCellChanges().length,
427
+ authors: this.getAuthors(),
428
+ nextId: this.nextId,
429
+ };
430
+ }
431
+
432
+ /**
433
+ * Checks if track changes is enabled (has any revisions)
434
+ * @returns True if there are revisions
435
+ */
436
+ isTrackingChanges(): boolean {
437
+ return this.revisions.length > 0;
438
+ }
439
+
440
+ /**
441
+ * Gets the most recent revision
442
+ * @returns The most recent revision, or undefined if no revisions
443
+ */
444
+ getLatestRevision(): Revision | undefined {
445
+ if (this.revisions.length === 0) {
446
+ return undefined;
447
+ }
448
+ return this.revisions[this.revisions.length - 1];
449
+ }
450
+
451
+ /**
452
+ * Gets revisions within a date range
453
+ * @param startDate - Start of date range
454
+ * @param endDate - End of date range
455
+ * @returns Array of revisions within the date range
456
+ */
457
+ getRevisionsByDateRange(startDate: Date, endDate: Date): Revision[] {
458
+ return this.revisions.filter((rev) => {
459
+ const revDate = rev.getDate();
460
+ return revDate >= startDate && revDate <= endDate;
461
+ });
462
+ }
463
+
464
+ /**
465
+ * Gets the next available revision ID without consuming it.
466
+ *
467
+ * This is an alias for peekNextId() for backward compatibility.
468
+ * Use consumeNextId() if you need to reserve an ID for manual use.
469
+ *
470
+ * @returns Next available revision ID (without consuming it)
471
+ * @see consumeNextId for reserving IDs
472
+ * @see register for automatic ID assignment
473
+ */
474
+ getNextId(): number {
475
+ return this.nextId;
476
+ }
477
+
478
+ /**
479
+ * Peeks at the next revision ID without incrementing
480
+ * @returns Next available revision ID (without consuming it)
481
+ */
482
+ peekNextId(): number {
483
+ return this.nextId;
484
+ }
485
+
486
+ /**
487
+ * Consumes and returns the next revision ID.
488
+ *
489
+ * Use this when you need to manually assign an ID to a revision
490
+ * that won't be registered through register(). The ID is reserved
491
+ * and won't be reused by subsequent register() calls.
492
+ *
493
+ * When a centralized ID provider is set, IDs come from the shared counter.
494
+ *
495
+ * @returns The consumed revision ID
496
+ *
497
+ * @example
498
+ * ```typescript
499
+ * // Reserve an ID for manual assignment
500
+ * const id = revisionManager.consumeNextId();
501
+ * revision.setId(id);
502
+ * // Don't call register() - the ID is already consumed
503
+ * ```
504
+ */
505
+ consumeNextId(): number {
506
+ // Use centralized provider if available
507
+ return this.idProvider ? this.idProvider() : this.nextId++;
508
+ }
509
+
510
+ /**
511
+ * Sets the next ID to be assigned.
512
+ * Used when loading documents to avoid ID collisions with existing revisions.
513
+ * @param id - The next ID value to use
514
+ */
515
+ setNextId(id: number): void {
516
+ this.nextId = id;
517
+ }
518
+
519
+ /**
520
+ * Creates a new RevisionManager
521
+ * @returns New RevisionManager instance
522
+ */
523
+ static create(): RevisionManager {
524
+ return new RevisionManager();
525
+ }
526
+
527
+ // ═══════════════════════════════════════════════════════════════════════════
528
+ // NEW METHODS - Added for ChangelogGenerator and RevisionAwareProcessor
529
+ // ═══════════════════════════════════════════════════════════════════════════
530
+
531
+ /**
532
+ * Check if any revisions exist in the manager.
533
+ * @returns True if there are any revisions
534
+ */
535
+ hasRevisions(): boolean {
536
+ return this.revisions.length > 0;
537
+ }
538
+
539
+ /**
540
+ * Get revisions by semantic category.
541
+ *
542
+ * Categories:
543
+ * - content: insert, delete, imageChange, fieldChange, commentChange, contentControlChange, hyperlinkChange
544
+ * - formatting: runPropertiesChange, paragraphPropertiesChange, numberingChange
545
+ * - structural: moveFrom, moveTo, sectionPropertiesChange, bookmarkChange
546
+ * - table: tablePropertiesChange, tableCellInsert, tableCellDelete, tableCellMerge, etc.
547
+ *
548
+ * @param category - Semantic category to filter by
549
+ * @returns Array of revisions in the specified category
550
+ */
551
+ getByCategory(category: RevisionCategory): Revision[] {
552
+ // Check cache first
553
+ if (this.revisionsByCategoryCache.has(category)) {
554
+ return [...this.revisionsByCategoryCache.get(category)!];
555
+ }
556
+
557
+ // Compute and cache
558
+ const result = this.revisions.filter((rev) => {
559
+ const type = rev.getType();
560
+ switch (category) {
561
+ case 'content':
562
+ return (
563
+ type === 'insert' ||
564
+ type === 'delete' ||
565
+ // Internal tracking types for rich content changes
566
+ type === 'imageChange' ||
567
+ type === 'fieldChange' ||
568
+ type === 'commentChange' ||
569
+ type === 'contentControlChange' ||
570
+ type === 'hyperlinkChange'
571
+ );
572
+
573
+ case 'formatting':
574
+ return (
575
+ type === 'runPropertiesChange' ||
576
+ type === 'paragraphPropertiesChange' ||
577
+ type === 'numberingChange'
578
+ );
579
+
580
+ case 'structural':
581
+ return (
582
+ type === 'moveFrom' ||
583
+ type === 'moveTo' ||
584
+ type === 'sectionPropertiesChange' ||
585
+ // Bookmarks are structural markers
586
+ type === 'bookmarkChange'
587
+ );
588
+
589
+ case 'table':
590
+ return (
591
+ type === 'tablePropertiesChange' ||
592
+ type === 'tableExceptionPropertiesChange' ||
593
+ type === 'tableRowPropertiesChange' ||
594
+ type === 'tableCellPropertiesChange' ||
595
+ type === 'tableCellInsert' ||
596
+ type === 'tableCellDelete' ||
597
+ type === 'tableCellMerge'
598
+ );
599
+
600
+ default:
601
+ return false;
602
+ }
603
+ });
604
+ this.revisionsByCategoryCache.set(category, result);
605
+ return [...result];
606
+ }
607
+
608
+ /**
609
+ * Get revisions affecting a specific paragraph.
610
+ *
611
+ * Uses the revision's location data if available. Returns revisions
612
+ * where location.paragraphIndex matches the specified index.
613
+ *
614
+ * Note: Revisions must have location data set (via setLocation()) for
615
+ * accurate filtering. Revisions without location data are excluded.
616
+ *
617
+ * @param paragraphIndex - Index of the paragraph (0-based)
618
+ * @returns Array of revisions affecting the specified paragraph
619
+ *
620
+ * @example
621
+ * ```typescript
622
+ * const revisions = revisionManager.getRevisionsForParagraph(3);
623
+ * console.log(`${revisions.length} revisions affect paragraph 3`);
624
+ * ```
625
+ */
626
+ getRevisionsForParagraph(paragraphIndex: number): Revision[] {
627
+ if (paragraphIndex < 0) {
628
+ return [];
629
+ }
630
+ return this.revisions.filter((rev) => {
631
+ const loc = rev.getLocation();
632
+ if (!loc) return false;
633
+ return loc.paragraphIndex === paragraphIndex;
634
+ });
635
+ }
636
+
637
+ /**
638
+ * Get summary statistics for all revisions.
639
+ * Provides comprehensive breakdown by type, category, and author.
640
+ *
641
+ * @returns Summary statistics object
642
+ */
643
+ getSummary(): RevisionSummary {
644
+ const byCategory: Record<RevisionCategory, number> = {
645
+ content: 0,
646
+ formatting: 0,
647
+ structural: 0,
648
+ table: 0,
649
+ };
650
+
651
+ let earliest: Date | null = null;
652
+ let latest: Date | null = null;
653
+
654
+ // Count by category and track date range
655
+ for (const rev of this.revisions) {
656
+ const type = rev.getType();
657
+ const date = rev.getDate();
658
+
659
+ // Update date range
660
+ if (!earliest || date < earliest) earliest = date;
661
+ if (!latest || date > latest) latest = date;
662
+
663
+ // Categorize
664
+ if (type === 'insert' || type === 'delete') {
665
+ byCategory.content++;
666
+ } else if (
667
+ type === 'runPropertiesChange' ||
668
+ type === 'paragraphPropertiesChange' ||
669
+ type === 'numberingChange'
670
+ ) {
671
+ byCategory.formatting++;
672
+ } else if (type === 'moveFrom' || type === 'moveTo' || type === 'sectionPropertiesChange') {
673
+ byCategory.structural++;
674
+ } else if (
675
+ type === 'tablePropertiesChange' ||
676
+ type === 'tableExceptionPropertiesChange' ||
677
+ type === 'tableRowPropertiesChange' ||
678
+ type === 'tableCellPropertiesChange' ||
679
+ type === 'tableCellInsert' ||
680
+ type === 'tableCellDelete' ||
681
+ type === 'tableCellMerge'
682
+ ) {
683
+ byCategory.table++;
684
+ }
685
+ }
686
+
687
+ const summary = {
688
+ total: this.revisions.length,
689
+ byType: {
690
+ insertions: this.getInsertionCount(),
691
+ deletions: this.getDeletionCount(),
692
+ moves: this.getAllMoves().length,
693
+ propertyChanges: this.getAllPropertyChanges().length,
694
+ tableChanges: this.getAllTableCellChanges().length,
695
+ },
696
+ byCategory,
697
+ authors: this.getAuthors(),
698
+ dateRange: earliest && latest ? { earliest, latest } : null,
699
+ };
700
+
701
+ if (summary.total > 0) {
702
+ getLogger().info('Revision summary', {
703
+ total: summary.total,
704
+ ins: summary.byType.insertions,
705
+ del: summary.byType.deletions,
706
+ fmt: summary.byType.propertyChanges,
707
+ authors: summary.authors.length,
708
+ });
709
+ }
710
+
711
+ return summary;
712
+ }
713
+
714
+ /**
715
+ * Get a revision by its ID.
716
+ *
717
+ * @param id - Revision ID to find
718
+ * @returns Revision with the specified ID, or undefined
719
+ */
720
+ getById(id: number): Revision | undefined {
721
+ return this.revisions.find((rev) => rev.getId() === id);
722
+ }
723
+
724
+ /**
725
+ * Remove a revision by its ID.
726
+ *
727
+ * @param id - ID of the revision to remove
728
+ * @returns True if revision was found and removed
729
+ */
730
+ removeById(id: number): boolean {
731
+ const index = this.revisions.findIndex((rev) => rev.getId() === id);
732
+ if (index === -1) return false;
733
+
734
+ this.revisions.splice(index, 1);
735
+ this.invalidateCache();
736
+ return true;
737
+ }
738
+
739
+ /**
740
+ * Get revisions matching multiple criteria.
741
+ *
742
+ * @param criteria - Filter criteria
743
+ * @returns Array of matching revisions
744
+ */
745
+ getMatching(criteria: {
746
+ types?: RevisionType[];
747
+ authors?: string[];
748
+ categories?: RevisionCategory[];
749
+ dateRange?: { start: Date; end: Date };
750
+ }): Revision[] {
751
+ return this.revisions.filter((rev) => {
752
+ // Filter by types
753
+ if (criteria.types && !criteria.types.includes(rev.getType())) {
754
+ return false;
755
+ }
756
+
757
+ // Filter by authors
758
+ if (criteria.authors && !criteria.authors.includes(rev.getAuthor())) {
759
+ return false;
760
+ }
761
+
762
+ // Filter by categories
763
+ if (criteria.categories) {
764
+ const revCategory = this.getRevisionCategory(rev);
765
+ if (!criteria.categories.includes(revCategory)) {
766
+ return false;
767
+ }
768
+ }
769
+
770
+ // Filter by date range
771
+ if (criteria.dateRange) {
772
+ const date = rev.getDate();
773
+ if (date < criteria.dateRange.start || date > criteria.dateRange.end) {
774
+ return false;
775
+ }
776
+ }
777
+
778
+ return true;
779
+ });
780
+ }
781
+
782
+ /**
783
+ * Get the semantic category of a revision.
784
+ * @internal
785
+ */
786
+ private getRevisionCategory(revision: Revision): RevisionCategory {
787
+ const type = revision.getType();
788
+
789
+ if (type === 'insert' || type === 'delete') {
790
+ return 'content';
791
+ }
792
+ if (
793
+ type === 'runPropertiesChange' ||
794
+ type === 'paragraphPropertiesChange' ||
795
+ type === 'numberingChange'
796
+ ) {
797
+ return 'formatting';
798
+ }
799
+ if (type === 'moveFrom' || type === 'moveTo' || type === 'sectionPropertiesChange') {
800
+ return 'structural';
801
+ }
802
+ if (
803
+ type === 'tablePropertiesChange' ||
804
+ type === 'tableExceptionPropertiesChange' ||
805
+ type === 'tableRowPropertiesChange' ||
806
+ type === 'tableCellPropertiesChange' ||
807
+ type === 'tableCellInsert' ||
808
+ type === 'tableCellDelete' ||
809
+ type === 'tableCellMerge'
810
+ ) {
811
+ return 'table';
812
+ }
813
+
814
+ // Default
815
+ return 'content';
816
+ }
817
+
818
+ // ============================================================
819
+ // Location-Aware Methods
820
+ // ============================================================
821
+
822
+ /**
823
+ * Gets revisions affecting a specific run within a paragraph.
824
+ *
825
+ * Uses the revision's location data if available.
826
+ *
827
+ * @param paragraphIndex - Index of the paragraph (0-based)
828
+ * @param runIndex - Index of the run within the paragraph (0-based)
829
+ * @returns Array of revisions affecting the specified run
830
+ *
831
+ * @example
832
+ * ```typescript
833
+ * const revisions = revisionManager.getRevisionsForRun(0, 2);
834
+ * console.log(`${revisions.length} revisions affect run 2 in paragraph 0`);
835
+ * ```
836
+ */
837
+ getRevisionsForRun(paragraphIndex: number, runIndex: number): Revision[] {
838
+ return this.revisions.filter((rev) => {
839
+ const loc = rev.getLocation();
840
+ if (!loc) return false;
841
+ return loc.paragraphIndex === paragraphIndex && loc.runIndex === runIndex;
842
+ });
843
+ }
844
+
845
+ /**
846
+ * Gets revisions by location criteria.
847
+ *
848
+ * Filters revisions based on their location within the document structure.
849
+ * All specified criteria must match (AND logic).
850
+ *
851
+ * @param criteria - Location filter criteria
852
+ * @returns Array of revisions matching the criteria
853
+ *
854
+ * @example
855
+ * ```typescript
856
+ * // Get all revisions in paragraph 5
857
+ * const paraRevisions = revisionManager.getRevisionsByLocation({
858
+ * paragraphIndex: 5
859
+ * });
860
+ *
861
+ * // Get all revisions in table row 2, cell 1
862
+ * const cellRevisions = revisionManager.getRevisionsByLocation({
863
+ * tableRow: 2,
864
+ * tableCell: 1
865
+ * });
866
+ * ```
867
+ */
868
+ getRevisionsByLocation(criteria: Partial<RevisionLocation>): Revision[] {
869
+ return this.revisions.filter((rev) => {
870
+ const loc = rev.getLocation();
871
+ if (!loc) return false;
872
+
873
+ // Check each criteria if specified
874
+ if (criteria.paragraphIndex !== undefined && loc.paragraphIndex !== criteria.paragraphIndex) {
875
+ return false;
876
+ }
877
+ if (criteria.runIndex !== undefined && loc.runIndex !== criteria.runIndex) {
878
+ return false;
879
+ }
880
+ if (criteria.tableRow !== undefined && loc.tableRow !== criteria.tableRow) {
881
+ return false;
882
+ }
883
+ if (criteria.tableCell !== undefined && loc.tableCell !== criteria.tableCell) {
884
+ return false;
885
+ }
886
+ if (criteria.sectionIndex !== undefined && loc.sectionIndex !== criteria.sectionIndex) {
887
+ return false;
888
+ }
889
+ if (
890
+ criteria.headerFooterType !== undefined &&
891
+ loc.headerFooterType !== criteria.headerFooterType
892
+ ) {
893
+ return false;
894
+ }
895
+
896
+ return true;
897
+ });
898
+ }
899
+
900
+ /**
901
+ * Gets revisions that have location data.
902
+ *
903
+ * @returns Array of revisions with location information
904
+ */
905
+ getRevisionsWithLocation(): Revision[] {
906
+ return this.revisions.filter((rev) => rev.getLocation() !== undefined);
907
+ }
908
+
909
+ /**
910
+ * Gets revisions that do NOT have location data.
911
+ *
912
+ * @returns Array of revisions without location information
913
+ */
914
+ getRevisionsWithoutLocation(): Revision[] {
915
+ return this.revisions.filter((rev) => rev.getLocation() === undefined);
916
+ }
917
+
918
+ // ============================================================
919
+ // Validation Methods
920
+ // ============================================================
921
+
922
+ /**
923
+ * Validates that all revision IDs are unique.
924
+ *
925
+ * Per ECMA-376, revision IDs must be unique within a document.
926
+ * Duplicate IDs can cause Word to reject the document or
927
+ * produce unexpected behavior.
928
+ *
929
+ * @returns Validation result with any duplicate IDs found
930
+ *
931
+ * @example
932
+ * ```typescript
933
+ * const result = revisionManager.validateRevisionIds();
934
+ * if (!result.valid) {
935
+ * console.error('Duplicate IDs found:', result.duplicates);
936
+ * }
937
+ * ```
938
+ */
939
+ validateRevisionIds(): { valid: boolean; duplicates: number[] } {
940
+ const seen = new Set<number>();
941
+ const duplicates: number[] = [];
942
+
943
+ for (const rev of this.revisions) {
944
+ const id = rev.getId();
945
+ if (seen.has(id)) {
946
+ if (!duplicates.includes(id)) {
947
+ duplicates.push(id);
948
+ }
949
+ }
950
+ seen.add(id);
951
+ }
952
+
953
+ return {
954
+ valid: duplicates.length === 0,
955
+ duplicates,
956
+ };
957
+ }
958
+
959
+ /**
960
+ * Reassigns all revision IDs to ensure uniqueness.
961
+ *
962
+ * This is useful after merging documents or when duplicate
963
+ * IDs are detected. IDs are reassigned sequentially starting
964
+ * from the specified value.
965
+ *
966
+ * @param startId - Starting ID value (default: 0)
967
+ * @returns Number of IDs reassigned
968
+ *
969
+ * @example
970
+ * ```typescript
971
+ * const count = revisionManager.reassignRevisionIds();
972
+ * console.log(`Reassigned ${count} revision IDs`);
973
+ * ```
974
+ */
975
+ reassignRevisionIds(startId = 0): number {
976
+ let currentId = startId;
977
+
978
+ for (const rev of this.revisions) {
979
+ rev.setId(currentId++);
980
+ }
981
+
982
+ // Update nextId to continue from where we left off
983
+ this.nextId = currentId;
984
+
985
+ return this.revisions.length;
986
+ }
987
+
988
+ /**
989
+ * Validates move operation pairs (moveFrom/moveTo).
990
+ *
991
+ * Each moveFrom must have a matching moveTo with the same moveId,
992
+ * and vice versa. Orphaned move markers can cause document corruption.
993
+ *
994
+ * @returns Validation result with orphaned move IDs
995
+ *
996
+ * @example
997
+ * ```typescript
998
+ * const result = revisionManager.validateMovePairs();
999
+ * if (!result.valid) {
1000
+ * console.error('Orphaned moveFrom IDs:', result.orphanedMoveFrom);
1001
+ * console.error('Orphaned moveTo IDs:', result.orphanedMoveTo);
1002
+ * }
1003
+ * ```
1004
+ */
1005
+ validateMovePairs(): {
1006
+ valid: boolean;
1007
+ orphanedMoveFrom: string[];
1008
+ orphanedMoveTo: string[];
1009
+ } {
1010
+ const moveFromIds = new Map<string, Revision>();
1011
+ const moveToIds = new Map<string, Revision>();
1012
+
1013
+ for (const rev of this.revisions) {
1014
+ const moveId = rev.getMoveId();
1015
+ if (!moveId) continue;
1016
+
1017
+ if (rev.getType() === 'moveFrom') {
1018
+ moveFromIds.set(moveId, rev);
1019
+ } else if (rev.getType() === 'moveTo') {
1020
+ moveToIds.set(moveId, rev);
1021
+ }
1022
+ }
1023
+
1024
+ const orphanedMoveFrom: string[] = [];
1025
+ const orphanedMoveTo: string[] = [];
1026
+
1027
+ // Find moveFrom without matching moveTo
1028
+ for (const moveId of moveFromIds.keys()) {
1029
+ if (!moveToIds.has(moveId)) {
1030
+ orphanedMoveFrom.push(moveId);
1031
+ }
1032
+ }
1033
+
1034
+ // Find moveTo without matching moveFrom
1035
+ for (const moveId of moveToIds.keys()) {
1036
+ if (!moveFromIds.has(moveId)) {
1037
+ orphanedMoveTo.push(moveId);
1038
+ }
1039
+ }
1040
+
1041
+ return {
1042
+ valid: orphanedMoveFrom.length === 0 && orphanedMoveTo.length === 0,
1043
+ orphanedMoveFrom,
1044
+ orphanedMoveTo,
1045
+ };
1046
+ }
1047
+
1048
+ /**
1049
+ * Gets the highest revision ID currently in use.
1050
+ *
1051
+ * @returns Highest ID, or -1 if no revisions exist
1052
+ */
1053
+ getHighestId(): number {
1054
+ if (this.revisions.length === 0) return -1;
1055
+ return Math.max(...this.revisions.map((r) => r.getId()));
1056
+ }
1057
+
1058
+ /**
1059
+ * Ensures the next ID is higher than all existing IDs.
1060
+ *
1061
+ * Useful after loading a document with existing revisions
1062
+ * to prevent ID conflicts with new revisions.
1063
+ */
1064
+ syncNextId(): void {
1065
+ const highestId = this.getHighestId();
1066
+ if (highestId >= this.nextId) {
1067
+ this.nextId = highestId + 1;
1068
+ }
1069
+ }
1070
+ }