docxmlater 10.1.3 → 10.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +759 -754
  2. package/dist/constants/legacyCompatFlags.js +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.js.map +1 -1
  5. package/dist/core/Document.d.ts +50 -50
  6. package/dist/core/Document.d.ts.map +1 -1
  7. package/dist/core/Document.js +483 -471
  8. package/dist/core/Document.js.map +1 -1
  9. package/dist/core/DocumentContent.d.ts +9 -9
  10. package/dist/core/DocumentContent.d.ts.map +1 -1
  11. package/dist/core/DocumentContent.js +1 -1
  12. package/dist/core/DocumentContent.js.map +1 -1
  13. package/dist/core/DocumentGenerator.d.ts +11 -11
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +251 -251
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.js.map +1 -1
  18. package/dist/core/DocumentParser.d.ts +15 -15
  19. package/dist/core/DocumentParser.d.ts.map +1 -1
  20. package/dist/core/DocumentParser.js +2123 -2155
  21. package/dist/core/DocumentParser.js.map +1 -1
  22. package/dist/core/DocumentValidator.d.ts.map +1 -1
  23. package/dist/core/DocumentValidator.js +2 -5
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.js.map +1 -1
  26. package/dist/core/RelationshipManager.d.ts.map +1 -1
  27. package/dist/core/RelationshipManager.js +3 -3
  28. package/dist/core/RelationshipManager.js.map +1 -1
  29. package/dist/elements/AlternateContent.js.map +1 -1
  30. package/dist/elements/Bookmark.d.ts.map +1 -1
  31. package/dist/elements/Bookmark.js +3 -1
  32. package/dist/elements/Bookmark.js.map +1 -1
  33. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  34. package/dist/elements/BookmarkManager.js.map +1 -1
  35. package/dist/elements/Comment.d.ts.map +1 -1
  36. package/dist/elements/Comment.js +9 -6
  37. package/dist/elements/Comment.js.map +1 -1
  38. package/dist/elements/CommentManager.d.ts.map +1 -1
  39. package/dist/elements/CommentManager.js +18 -17
  40. package/dist/elements/CommentManager.js.map +1 -1
  41. package/dist/elements/CommonTypes.d.ts +21 -21
  42. package/dist/elements/CommonTypes.d.ts.map +1 -1
  43. package/dist/elements/CommonTypes.js +56 -56
  44. package/dist/elements/CommonTypes.js.map +1 -1
  45. package/dist/elements/CustomXml.js.map +1 -1
  46. package/dist/elements/Endnote.d.ts.map +1 -1
  47. package/dist/elements/Endnote.js +6 -6
  48. package/dist/elements/Endnote.js.map +1 -1
  49. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  50. package/dist/elements/EndnoteManager.js +6 -7
  51. package/dist/elements/EndnoteManager.js.map +1 -1
  52. package/dist/elements/Field.d.ts.map +1 -1
  53. package/dist/elements/Field.js +82 -25
  54. package/dist/elements/Field.js.map +1 -1
  55. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  56. package/dist/elements/FieldHelpers.js.map +1 -1
  57. package/dist/elements/FontManager.d.ts.map +1 -1
  58. package/dist/elements/FontManager.js +1 -1
  59. package/dist/elements/FontManager.js.map +1 -1
  60. package/dist/elements/Footer.js +2 -2
  61. package/dist/elements/Footer.js.map +1 -1
  62. package/dist/elements/Footnote.d.ts.map +1 -1
  63. package/dist/elements/Footnote.js +6 -6
  64. package/dist/elements/Footnote.js.map +1 -1
  65. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  66. package/dist/elements/FootnoteManager.js +6 -7
  67. package/dist/elements/FootnoteManager.js.map +1 -1
  68. package/dist/elements/Header.js +2 -2
  69. package/dist/elements/Header.js.map +1 -1
  70. package/dist/elements/HeaderFooterManager.js.map +1 -1
  71. package/dist/elements/Hyperlink.d.ts +5 -3
  72. package/dist/elements/Hyperlink.d.ts.map +1 -1
  73. package/dist/elements/Hyperlink.js +134 -76
  74. package/dist/elements/Hyperlink.js.map +1 -1
  75. package/dist/elements/Image.d.ts.map +1 -1
  76. package/dist/elements/Image.js +238 -106
  77. package/dist/elements/Image.js.map +1 -1
  78. package/dist/elements/ImageManager.d.ts.map +1 -1
  79. package/dist/elements/ImageManager.js +1 -1
  80. package/dist/elements/ImageManager.js.map +1 -1
  81. package/dist/elements/ImageRun.js +1 -1
  82. package/dist/elements/ImageRun.js.map +1 -1
  83. package/dist/elements/MathElement.js.map +1 -1
  84. package/dist/elements/Paragraph.d.ts +24 -24
  85. package/dist/elements/Paragraph.d.ts.map +1 -1
  86. package/dist/elements/Paragraph.js +181 -188
  87. package/dist/elements/Paragraph.js.map +1 -1
  88. package/dist/elements/PreservedElement.js.map +1 -1
  89. package/dist/elements/PropertyChangeTypes.d.ts.map +1 -1
  90. package/dist/elements/PropertyChangeTypes.js +6 -6
  91. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  92. package/dist/elements/RangeMarker.d.ts.map +1 -1
  93. package/dist/elements/RangeMarker.js.map +1 -1
  94. package/dist/elements/Revision.d.ts.map +1 -1
  95. package/dist/elements/Revision.js +4 -5
  96. package/dist/elements/Revision.js.map +1 -1
  97. package/dist/elements/RevisionContent.js.map +1 -1
  98. package/dist/elements/RevisionManager.d.ts.map +1 -1
  99. package/dist/elements/RevisionManager.js +40 -48
  100. package/dist/elements/RevisionManager.js.map +1 -1
  101. package/dist/elements/Run.d.ts +16 -16
  102. package/dist/elements/Run.d.ts.map +1 -1
  103. package/dist/elements/Run.js +256 -238
  104. package/dist/elements/Run.js.map +1 -1
  105. package/dist/elements/Section.d.ts.map +1 -1
  106. package/dist/elements/Section.js +36 -11
  107. package/dist/elements/Section.js.map +1 -1
  108. package/dist/elements/Shape.d.ts.map +1 -1
  109. package/dist/elements/Shape.js.map +1 -1
  110. package/dist/elements/StructuredDocumentTag.d.ts +6 -6
  111. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  112. package/dist/elements/StructuredDocumentTag.js +99 -104
  113. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  114. package/dist/elements/Table.d.ts +11 -11
  115. package/dist/elements/Table.d.ts.map +1 -1
  116. package/dist/elements/Table.js +102 -107
  117. package/dist/elements/Table.js.map +1 -1
  118. package/dist/elements/TableCell.d.ts +10 -10
  119. package/dist/elements/TableCell.d.ts.map +1 -1
  120. package/dist/elements/TableCell.js +105 -106
  121. package/dist/elements/TableCell.js.map +1 -1
  122. package/dist/elements/TableGridChange.d.ts.map +1 -1
  123. package/dist/elements/TableGridChange.js.map +1 -1
  124. package/dist/elements/TableOfContents.d.ts.map +1 -1
  125. package/dist/elements/TableOfContents.js +4 -4
  126. package/dist/elements/TableOfContents.js.map +1 -1
  127. package/dist/elements/TableOfContentsElement.js.map +1 -1
  128. package/dist/elements/TableRow.d.ts.map +1 -1
  129. package/dist/elements/TableRow.js +13 -6
  130. package/dist/elements/TableRow.js.map +1 -1
  131. package/dist/elements/TextBox.d.ts.map +1 -1
  132. package/dist/elements/TextBox.js +3 -5
  133. package/dist/elements/TextBox.js.map +1 -1
  134. package/dist/formatting/AbstractNumbering.d.ts +4 -4
  135. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  136. package/dist/formatting/AbstractNumbering.js +54 -49
  137. package/dist/formatting/AbstractNumbering.js.map +1 -1
  138. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  139. package/dist/formatting/NumberingInstance.js +1 -3
  140. package/dist/formatting/NumberingInstance.js.map +1 -1
  141. package/dist/formatting/NumberingLevel.d.ts +5 -5
  142. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  143. package/dist/formatting/NumberingLevel.js +119 -125
  144. package/dist/formatting/NumberingLevel.js.map +1 -1
  145. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  146. package/dist/formatting/NumberingManager.js +9 -9
  147. package/dist/formatting/NumberingManager.js.map +1 -1
  148. package/dist/formatting/Style.d.ts +11 -11
  149. package/dist/formatting/Style.d.ts.map +1 -1
  150. package/dist/formatting/Style.js +219 -247
  151. package/dist/formatting/Style.js.map +1 -1
  152. package/dist/formatting/StylesManager.d.ts +2 -2
  153. package/dist/formatting/StylesManager.d.ts.map +1 -1
  154. package/dist/formatting/StylesManager.js +96 -102
  155. package/dist/formatting/StylesManager.js.map +1 -1
  156. package/dist/helpers/CleanupHelper.d.ts +1 -1
  157. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  158. package/dist/helpers/CleanupHelper.js +6 -6
  159. package/dist/helpers/CleanupHelper.js.map +1 -1
  160. package/dist/images/ImageOptimizer.js +7 -7
  161. package/dist/images/ImageOptimizer.js.map +1 -1
  162. package/dist/index.d.ts +9 -9
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js.map +1 -1
  165. package/dist/managers/DrawingManager.js.map +1 -1
  166. package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
  167. package/dist/tracking/DocumentTrackingContext.js +23 -7
  168. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  169. package/dist/tracking/TrackingContext.d.ts.map +1 -1
  170. package/dist/tracking/TrackingContext.js.map +1 -1
  171. package/dist/types/compatibility-types.js.map +1 -1
  172. package/dist/types/formatting.js.map +1 -1
  173. package/dist/types/list-types.d.ts +6 -6
  174. package/dist/types/list-types.js.map +1 -1
  175. package/dist/types/settings-types.js.map +1 -1
  176. package/dist/types/styleConfig.d.ts +2 -2
  177. package/dist/types/styleConfig.js.map +1 -1
  178. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  179. package/dist/utils/ChangelogGenerator.js +97 -101
  180. package/dist/utils/ChangelogGenerator.js.map +1 -1
  181. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  182. package/dist/utils/CompatibilityUpgrader.js +1 -1
  183. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  184. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  185. package/dist/utils/InMemoryRevisionAcceptor.js +1 -6
  186. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  187. package/dist/utils/MoveOperationHelper.d.ts.map +1 -1
  188. package/dist/utils/MoveOperationHelper.js +1 -1
  189. package/dist/utils/MoveOperationHelper.js.map +1 -1
  190. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  191. package/dist/utils/RevisionAwareProcessor.js +2 -4
  192. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  193. package/dist/utils/RevisionWalker.d.ts.map +1 -1
  194. package/dist/utils/RevisionWalker.js +4 -12
  195. package/dist/utils/RevisionWalker.js.map +1 -1
  196. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  197. package/dist/utils/SelectiveRevisionAcceptor.js +2 -6
  198. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  199. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  200. package/dist/utils/ShadingResolver.js +1 -1
  201. package/dist/utils/ShadingResolver.js.map +1 -1
  202. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  203. package/dist/utils/acceptRevisions.js +23 -12
  204. package/dist/utils/acceptRevisions.js.map +1 -1
  205. package/dist/utils/cnfStyleDecoder.d.ts +1 -1
  206. package/dist/utils/cnfStyleDecoder.d.ts.map +1 -1
  207. package/dist/utils/cnfStyleDecoder.js +40 -40
  208. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  209. package/dist/utils/corruptionDetection.d.ts.map +1 -1
  210. package/dist/utils/corruptionDetection.js.map +1 -1
  211. package/dist/utils/dateFormatting.js.map +1 -1
  212. package/dist/utils/deepClone.js +1 -1
  213. package/dist/utils/deepClone.js.map +1 -1
  214. package/dist/utils/diagnostics.d.ts.map +1 -1
  215. package/dist/utils/diagnostics.js +1 -1
  216. package/dist/utils/diagnostics.js.map +1 -1
  217. package/dist/utils/errorHandling.js.map +1 -1
  218. package/dist/utils/formatting.d.ts.map +1 -1
  219. package/dist/utils/formatting.js +10 -2
  220. package/dist/utils/formatting.js.map +1 -1
  221. package/dist/utils/list-detection.d.ts +2 -2
  222. package/dist/utils/list-detection.d.ts.map +1 -1
  223. package/dist/utils/list-detection.js +21 -23
  224. package/dist/utils/list-detection.js.map +1 -1
  225. package/dist/utils/logger.d.ts.map +1 -1
  226. package/dist/utils/logger.js +12 -7
  227. package/dist/utils/logger.js.map +1 -1
  228. package/dist/utils/parsingHelpers.js.map +1 -1
  229. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  230. package/dist/utils/stripTrackedChanges.js +3 -3
  231. package/dist/utils/stripTrackedChanges.js.map +1 -1
  232. package/dist/utils/textDiff.d.ts +1 -1
  233. package/dist/utils/textDiff.js +8 -8
  234. package/dist/utils/textDiff.js.map +1 -1
  235. package/dist/utils/units.js.map +1 -1
  236. package/dist/utils/validation.d.ts.map +1 -1
  237. package/dist/utils/validation.js +24 -7
  238. package/dist/utils/validation.js.map +1 -1
  239. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  240. package/dist/utils/xmlSanitization.js +3 -3
  241. package/dist/utils/xmlSanitization.js.map +1 -1
  242. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  243. package/dist/validation/RevisionAutoFixer.js +5 -5
  244. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  245. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  246. package/dist/validation/RevisionValidator.js +7 -9
  247. package/dist/validation/RevisionValidator.js.map +1 -1
  248. package/dist/validation/ValidationRules.js +3 -3
  249. package/dist/validation/ValidationRules.js.map +1 -1
  250. package/dist/validation/index.js.map +1 -1
  251. package/dist/xml/XMLBuilder.d.ts +1 -1
  252. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  253. package/dist/xml/XMLBuilder.js +98 -100
  254. package/dist/xml/XMLBuilder.js.map +1 -1
  255. package/dist/xml/XMLParser.d.ts.map +1 -1
  256. package/dist/xml/XMLParser.js +61 -66
  257. package/dist/xml/XMLParser.js.map +1 -1
  258. package/dist/zip/ZipHandler.d.ts.map +1 -1
  259. package/dist/zip/ZipHandler.js.map +1 -1
  260. package/dist/zip/ZipReader.d.ts.map +1 -1
  261. package/dist/zip/ZipReader.js +1 -3
  262. package/dist/zip/ZipReader.js.map +1 -1
  263. package/dist/zip/ZipWriter.d.ts +1 -1
  264. package/dist/zip/ZipWriter.d.ts.map +1 -1
  265. package/dist/zip/ZipWriter.js +28 -36
  266. package/dist/zip/ZipWriter.js.map +1 -1
  267. package/dist/zip/types.js +1 -1
  268. package/dist/zip/types.js.map +1 -1
  269. package/package.json +92 -92
  270. package/src/__tests__/helper-methods.test.ts +512 -512
  271. package/src/constants/legacyCompatFlags.ts +138 -138
  272. package/src/constants/limits.ts +50 -50
  273. package/src/core/Document.ts +985 -1145
  274. package/src/core/DocumentContent.ts +461 -467
  275. package/src/core/DocumentGenerator.ts +1133 -1104
  276. package/src/core/DocumentIdManager.ts +158 -158
  277. package/src/core/DocumentParser.ts +2347 -2716
  278. package/src/core/DocumentValidator.ts +363 -372
  279. package/src/core/Relationship.ts +367 -367
  280. package/src/core/RelationshipManager.ts +429 -428
  281. package/src/elements/AlternateContent.ts +42 -42
  282. package/src/elements/Bookmark.ts +212 -210
  283. package/src/elements/BookmarkManager.ts +247 -250
  284. package/src/elements/Comment.ts +356 -359
  285. package/src/elements/CommentManager.ts +499 -502
  286. package/src/elements/CommonTypes.ts +524 -549
  287. package/src/elements/CustomXml.ts +36 -36
  288. package/src/elements/Endnote.ts +221 -217
  289. package/src/elements/EndnoteManager.ts +246 -249
  290. package/src/elements/Field.ts +1292 -1233
  291. package/src/elements/FieldHelpers.ts +329 -333
  292. package/src/elements/FontManager.ts +336 -339
  293. package/src/elements/Footer.ts +269 -269
  294. package/src/elements/Footnote.ts +221 -217
  295. package/src/elements/FootnoteManager.ts +246 -249
  296. package/src/elements/Header.ts +269 -269
  297. package/src/elements/HeaderFooterManager.ts +219 -219
  298. package/src/elements/Hyperlink.ts +1288 -1193
  299. package/src/elements/Image.ts +1982 -1756
  300. package/src/elements/ImageManager.ts +437 -432
  301. package/src/elements/ImageRun.ts +59 -59
  302. package/src/elements/MathElement.ts +65 -65
  303. package/src/elements/Paragraph.ts +4347 -4287
  304. package/src/elements/PreservedElement.ts +53 -53
  305. package/src/elements/PropertyChangeTypes.ts +458 -442
  306. package/src/elements/RangeMarker.ts +382 -400
  307. package/src/elements/Revision.ts +1198 -1217
  308. package/src/elements/RevisionContent.ts +73 -73
  309. package/src/elements/RevisionManager.ts +1070 -1070
  310. package/src/elements/Run.ts +3103 -3073
  311. package/src/elements/Section.ts +1521 -1421
  312. package/src/elements/Shape.ts +884 -873
  313. package/src/elements/StructuredDocumentTag.ts +1176 -1207
  314. package/src/elements/Table.ts +2468 -2524
  315. package/src/elements/TableCell.ts +1617 -1621
  316. package/src/elements/TableGridChange.ts +149 -151
  317. package/src/elements/TableOfContents.ts +701 -691
  318. package/src/elements/TableOfContentsElement.ts +89 -89
  319. package/src/elements/TableRow.ts +960 -929
  320. package/src/elements/TextBox.ts +766 -768
  321. package/src/formatting/AbstractNumbering.ts +580 -579
  322. package/src/formatting/NumberingInstance.ts +295 -299
  323. package/src/formatting/NumberingLevel.ts +981 -1040
  324. package/src/formatting/NumberingManager.ts +833 -827
  325. package/src/formatting/Style.ts +1785 -1879
  326. package/src/formatting/StylesManager.ts +1090 -1130
  327. package/src/helpers/CleanupHelper.ts +524 -524
  328. package/src/images/ImageOptimizer.ts +274 -274
  329. package/src/index.ts +559 -554
  330. package/src/managers/DrawingManager.ts +319 -319
  331. package/src/tracking/DocumentTrackingContext.ts +687 -674
  332. package/src/tracking/TrackingContext.ts +175 -173
  333. package/src/types/compatibility-types.ts +49 -49
  334. package/src/types/formatting.ts +210 -210
  335. package/src/types/list-types.ts +14 -14
  336. package/src/types/settings-types.ts +59 -59
  337. package/src/types/styleConfig.ts +189 -189
  338. package/src/utils/ChangelogGenerator.ts +1583 -1581
  339. package/src/utils/CompatibilityUpgrader.ts +235 -237
  340. package/src/utils/InMemoryRevisionAcceptor.ts +691 -696
  341. package/src/utils/MoveOperationHelper.ts +233 -238
  342. package/src/utils/RevisionAwareProcessor.ts +518 -526
  343. package/src/utils/RevisionWalker.ts +427 -457
  344. package/src/utils/SelectiveRevisionAcceptor.ts +662 -683
  345. package/src/utils/ShadingResolver.ts +105 -107
  346. package/src/utils/acceptRevisions.ts +723 -714
  347. package/src/utils/cnfStyleDecoder.ts +212 -217
  348. package/src/utils/corruptionDetection.ts +346 -345
  349. package/src/utils/dateFormatting.ts +20 -20
  350. package/src/utils/deepClone.ts +77 -78
  351. package/src/utils/diagnostics.ts +125 -129
  352. package/src/utils/errorHandling.ts +80 -80
  353. package/src/utils/formatting.ts +220 -213
  354. package/src/utils/list-detection.ts +32 -42
  355. package/src/utils/logger.ts +412 -404
  356. package/src/utils/parsingHelpers.ts +190 -190
  357. package/src/utils/stripTrackedChanges.ts +356 -353
  358. package/src/utils/textDiff.ts +100 -100
  359. package/src/utils/units.ts +421 -421
  360. package/src/utils/validation.ts +553 -542
  361. package/src/utils/xmlSanitization.ts +179 -182
  362. package/src/validation/RevisionAutoFixer.ts +541 -542
  363. package/src/validation/RevisionValidator.ts +470 -460
  364. package/src/validation/ValidationRules.ts +338 -338
  365. package/src/validation/index.ts +30 -30
  366. package/src/xml/XMLBuilder.ts +857 -871
  367. package/src/xml/XMLParser.ts +877 -919
  368. package/src/zip/ZipHandler.ts +629 -637
  369. package/src/zip/ZipReader.ts +295 -299
  370. package/src/zip/ZipWriter.ts +374 -390
  371. package/src/zip/types.ts +116 -116
@@ -1,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
+ }