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,674 +1,687 @@
1
- /**
2
- * DocumentTrackingContext - Implementation of automatic change tracking
3
- *
4
- * Manages pending changes and creates Revision objects when flushed.
5
- * Supports consolidation of similar changes within a time window.
6
- *
7
- * @module DocumentTrackingContext
8
- */
9
-
10
- import { Revision, RevisionType } from '../elements/Revision';
11
- import { RevisionManager } from '../elements/RevisionManager';
12
- import { Run, type RunFormatting } from '../elements/Run';
13
- import { Paragraph } from '../elements/Paragraph';
14
- import { Table } from '../elements/Table';
15
- import { TableRow } from '../elements/TableRow';
16
- import { TableCell } from '../elements/TableCell';
17
- import { Section } from '../elements/Section';
18
- import type { TrackingContext, PendingChange, TrackableElement } from './TrackingContext';
19
- import { formatDateForXml } from '../utils/dateFormatting';
20
-
21
- /**
22
- * Enable options for tracking context
23
- */
24
- export interface TrackingEnableOptions {
25
- /** Author name for new revisions (default: 'DocHub') */
26
- author?: string;
27
- /** Whether to track formatting changes (default: true) */
28
- trackFormatting?: boolean;
29
- }
30
-
31
- /**
32
- * Implementation of TrackingContext for Document
33
- */
34
- export class DocumentTrackingContext implements TrackingContext {
35
- private enabled = false;
36
- private trackFormatting = true;
37
- private author = 'DocHub';
38
- private revisionManager: RevisionManager;
39
-
40
- /** Counter for assigning stable element IDs */
41
- private elementIdCounter = 0;
42
- /** Stable element identity map (WeakMap so elements can be GC'd) */
43
- private elementIdMap = new WeakMap<object, number>();
44
-
45
- /** Pending changes waiting to be flushed */
46
- private pendingChanges = new Map<string, PendingChange>();
47
-
48
- /** Properties considered "formatting" (vs structural) */
49
- private static readonly FORMATTING_PROPERTIES = new Set([
50
- 'bold',
51
- 'italic',
52
- 'underline',
53
- 'strike',
54
- 'dstrike',
55
- 'subscript',
56
- 'superscript',
57
- 'font',
58
- 'size',
59
- 'color',
60
- 'highlight',
61
- 'smallCaps',
62
- 'allCaps',
63
- 'characterSpacing',
64
- 'scaling',
65
- 'position',
66
- 'emphasis',
67
- 'shadow',
68
- 'emboss',
69
- 'imprint',
70
- 'outline',
71
- 'vanish',
72
- ]);
73
-
74
- /**
75
- * Creates a new DocumentTrackingContext
76
- * @param revisionManager - RevisionManager to register revisions with
77
- */
78
- constructor(revisionManager: RevisionManager) {
79
- this.revisionManager = revisionManager;
80
- }
81
-
82
- /**
83
- * Enable change tracking
84
- * @param options - Enable options
85
- */
86
- enable(options?: TrackingEnableOptions): void {
87
- this.enabled = true;
88
- if (options?.author) {
89
- this.author = options.author;
90
- }
91
- if (options?.trackFormatting !== undefined) {
92
- this.trackFormatting = options.trackFormatting;
93
- }
94
- }
95
-
96
- /**
97
- * Disable change tracking and flush pending changes
98
- */
99
- disable(): void {
100
- this.flushPendingChanges();
101
- this.enabled = false;
102
- }
103
-
104
- /**
105
- * Set the author for new revisions
106
- * Flushes any pending changes before switching to prevent mixed authorship
107
- * @param author - Author name
108
- */
109
- setAuthor(author: string): void {
110
- // Flush pending changes before switching authors to prevent mixed authorship
111
- if (this.enabled && this.pendingChanges.size > 0) {
112
- this.flushPendingChanges();
113
- }
114
- this.author = author;
115
- }
116
-
117
- // ═══════════════════════════════════════════════════════════════════════════
118
- // TrackingContext Interface Implementation
119
- // ═══════════════════════════════════════════════════════════════════════════
120
-
121
- isEnabled(): boolean {
122
- return this.enabled;
123
- }
124
-
125
- getAuthor(): string {
126
- return this.author;
127
- }
128
-
129
- getRevisionManager(): RevisionManager {
130
- return this.revisionManager;
131
- }
132
-
133
- isTrackFormattingEnabled(): boolean {
134
- return this.trackFormatting;
135
- }
136
-
137
- trackRunPropertyChange(
138
- run: Run,
139
- property: string,
140
- oldValue: unknown,
141
- newValue: unknown
142
- ): void {
143
- if (!this.enabled) return;
144
- if (this.valuesEqual(oldValue, newValue)) return;
145
-
146
- // Skip formatting changes if not tracking them
147
- if (
148
- !this.trackFormatting &&
149
- DocumentTrackingContext.FORMATTING_PROPERTIES.has(property)
150
- ) {
151
- return;
152
- }
153
-
154
- // Create consolidation key with element identity
155
- const key = `runProp:${property}:${this.stringifyValue(newValue)}@${this.getElementId(run)}`;
156
-
157
- this.addPendingChange(key, {
158
- type: 'runPropertiesChange',
159
- property,
160
- previousValue: oldValue,
161
- newValue,
162
- element: run,
163
- timestamp: Date.now(),
164
- });
165
- }
166
-
167
- trackParagraphPropertyChange(
168
- paragraph: TrackableElement,
169
- property: string,
170
- oldValue: unknown,
171
- newValue: unknown
172
- ): void {
173
- if (!this.enabled) return;
174
- if (this.valuesEqual(oldValue, newValue)) return;
175
-
176
- const key = `paraProp:${property}:${this.stringifyValue(newValue)}@${this.getElementId(paragraph)}`;
177
-
178
- this.addPendingChange(key, {
179
- type: 'paragraphPropertiesChange',
180
- property,
181
- previousValue: oldValue,
182
- newValue,
183
- element: paragraph,
184
- timestamp: Date.now(),
185
- });
186
- }
187
-
188
- trackHyperlinkChange(
189
- hyperlink: TrackableElement,
190
- changeType: string,
191
- oldValue: unknown,
192
- newValue: unknown
193
- ): void {
194
- if (!this.enabled) return;
195
- if (this.valuesEqual(oldValue, newValue)) return;
196
-
197
- // Hyperlink changes use dedicated type for proper categorization
198
- const key = `hyperlink:${changeType}:${this.stringifyValue(newValue)}@${this.getElementId(hyperlink)}`;
199
-
200
- this.addPendingChange(key, {
201
- type: 'hyperlinkChange',
202
- property: changeType,
203
- previousValue: oldValue,
204
- newValue,
205
- element: hyperlink,
206
- timestamp: Date.now(),
207
- });
208
- }
209
-
210
- trackTableChange(
211
- element: TrackableElement,
212
- property: string,
213
- oldValue: unknown,
214
- newValue: unknown
215
- ): void {
216
- if (!this.enabled) return;
217
- if (this.valuesEqual(oldValue, newValue)) return;
218
-
219
- // Determine revision type based on element type using instanceof (minification-safe)
220
- let type: RevisionType = 'tablePropertiesChange';
221
- let elementType = 'Table';
222
-
223
- if (element instanceof TableRow) {
224
- type = 'tableRowPropertiesChange';
225
- elementType = 'TableRow';
226
- } else if (element instanceof TableCell) {
227
- type = 'tableCellPropertiesChange';
228
- elementType = 'TableCell';
229
- }
230
-
231
- const key = `table:${elementType}:${property}:${this.stringifyValue(newValue)}@${this.getElementId(element)}`;
232
-
233
- this.addPendingChange(key, {
234
- type,
235
- property,
236
- previousValue: oldValue,
237
- newValue,
238
- element,
239
- timestamp: Date.now(),
240
- });
241
- }
242
-
243
- trackSectionChange(
244
- section: TrackableElement,
245
- property: string,
246
- oldValue: unknown,
247
- newValue: unknown
248
- ): void {
249
- if (!this.enabled) return;
250
- if (this.valuesEqual(oldValue, newValue)) return;
251
-
252
- const key = `section:${property}:${this.stringifyValue(newValue)}@${this.getElementId(section)}`;
253
-
254
- this.addPendingChange(key, {
255
- type: 'sectionPropertiesChange',
256
- property,
257
- previousValue: oldValue,
258
- newValue,
259
- element: section,
260
- timestamp: Date.now(),
261
- });
262
- }
263
-
264
- trackInsertion(element: TrackableElement, text: string): void {
265
- if (!this.enabled) return;
266
- if (!text) return;
267
-
268
- const key = `insert:${Date.now()}:${text.substring(0, 20)}`;
269
-
270
- this.addPendingChange(key, {
271
- type: 'insert',
272
- property: 'text',
273
- previousValue: undefined,
274
- newValue: text,
275
- element,
276
- timestamp: Date.now(),
277
- });
278
- }
279
-
280
- trackDeletion(element: TrackableElement, text: string): void {
281
- if (!this.enabled) return;
282
- if (!text) return;
283
-
284
- const key = `delete:${Date.now()}:${text.substring(0, 20)}`;
285
-
286
- this.addPendingChange(key, {
287
- type: 'delete',
288
- property: 'text',
289
- previousValue: text,
290
- newValue: undefined,
291
- element,
292
- timestamp: Date.now(),
293
- });
294
- }
295
-
296
- flushPendingChanges(): Revision[] {
297
- const revisions: Revision[] = [];
298
-
299
- // Group pending changes by element for consolidation
300
- const paragraphChanges = new Map<Paragraph, PendingChange[]>();
301
- const tableChanges = new Map<Table, PendingChange[]>();
302
- const rowChanges = new Map<TableRow, PendingChange[]>();
303
- const cellChanges = new Map<TableCell, PendingChange[]>();
304
- const sectionChanges = new Map<Section, PendingChange[]>();
305
- const runChanges = new Map<Run, PendingChange[]>();
306
-
307
- for (const [, pending] of this.pendingChanges) {
308
- const revision = this.createRevision(pending);
309
- this.revisionManager.register(revision);
310
- revisions.push(revision);
311
-
312
- // Collect changes by element type for *PrChange application
313
- if (pending.type === 'paragraphPropertiesChange' && pending.element instanceof Paragraph) {
314
- const changes = paragraphChanges.get(pending.element) || [];
315
- changes.push(pending);
316
- paragraphChanges.set(pending.element, changes);
317
- } else if (pending.type === 'tablePropertiesChange' && pending.element instanceof Table) {
318
- const changes = tableChanges.get(pending.element) || [];
319
- changes.push(pending);
320
- tableChanges.set(pending.element, changes);
321
- } else if (pending.type === 'tableRowPropertiesChange' && pending.element instanceof TableRow) {
322
- const changes = rowChanges.get(pending.element) || [];
323
- changes.push(pending);
324
- rowChanges.set(pending.element, changes);
325
- } else if (pending.type === 'tableCellPropertiesChange' && pending.element instanceof TableCell) {
326
- const changes = cellChanges.get(pending.element) || [];
327
- changes.push(pending);
328
- cellChanges.set(pending.element, changes);
329
- } else if (pending.type === 'sectionPropertiesChange' && pending.element instanceof Section) {
330
- const changes = sectionChanges.get(pending.element) || [];
331
- changes.push(pending);
332
- sectionChanges.set(pending.element, changes);
333
- } else if (pending.type === 'runPropertiesChange' && pending.element instanceof Run) {
334
- const changes = runChanges.get(pending.element) || [];
335
- changes.push(pending);
336
- runChanges.set(pending.element, changes);
337
- }
338
- }
339
-
340
- // Apply pPrChange to each paragraph that has property changes
341
- for (const [paragraph, changes] of paragraphChanges) {
342
- this.applyParagraphPrChange(paragraph, changes);
343
- }
344
-
345
- // Apply tblPrChange to each Table
346
- // Per ECMA-376 §17.13.5.36, tblPrChange must contain FULL previous tblPr,
347
- // not just the delta of changed properties.
348
- for (const [table, changes] of tableChanges) {
349
- // Build full snapshot: start from current formatting, roll back changed properties
350
- const currentFormatting = table.getFormatting();
351
- const fullPrevProps: Record<string, unknown> = {};
352
-
353
- for (const [key, value] of Object.entries(currentFormatting)) {
354
- if (value !== undefined) {
355
- fullPrevProps[key] = value;
356
- }
357
- }
358
-
359
- // Roll back changed properties to their previous values
360
- let latestTimestamp = 0;
361
- for (const change of changes) {
362
- if (change.previousValue !== undefined) {
363
- fullPrevProps[change.property] = change.previousValue;
364
- } else {
365
- delete fullPrevProps[change.property];
366
- }
367
- if (change.timestamp > latestTimestamp) {
368
- latestTimestamp = change.timestamp;
369
- }
370
- }
371
-
372
- const date = formatDateForXml(new Date(latestTimestamp));
373
-
374
- const existing = table.getTblPrChange();
375
- if (existing) {
376
- // Merge: existing previous state takes precedence (it's the ORIGINAL baseline)
377
- const merged = { ...fullPrevProps, ...(existing.previousProperties || {}) };
378
- table.setTblPrChange({ ...existing, previousProperties: merged });
379
- } else {
380
- table.setTblPrChange({
381
- author: this.author,
382
- date,
383
- id: String(this.revisionManager.consumeNextId()),
384
- previousProperties: fullPrevProps,
385
- });
386
- }
387
- }
388
-
389
- // Apply trPrChange to each TableRow
390
- for (const [row, changes] of rowChanges) {
391
- this.applyElementPrChange(changes, (prevProps, getNextId, date) => {
392
- const existing = row.getTrPrChange();
393
- if (existing) {
394
- const merged = { ...(existing.previousProperties || {}), ...prevProps };
395
- row.setTrPrChange({ ...existing, previousProperties: merged });
396
- } else {
397
- row.setTrPrChange({ author: this.author, date, id: String(getNextId()), previousProperties: prevProps });
398
- }
399
- });
400
- }
401
-
402
- // Apply tcPrChange to each TableCell
403
- for (const [cell, changes] of cellChanges) {
404
- this.applyElementPrChange(changes, (prevProps, getNextId, date) => {
405
- const existing = cell.getTcPrChange();
406
- if (existing) {
407
- const merged = { ...(existing.previousProperties || {}), ...prevProps };
408
- cell.setTcPrChange({ ...existing, previousProperties: merged });
409
- } else {
410
- cell.setTcPrChange({ author: this.author, date, id: String(getNextId()), previousProperties: prevProps });
411
- }
412
- });
413
- }
414
-
415
- // Apply sectPrChange to each Section
416
- for (const [section, changes] of sectionChanges) {
417
- this.applyElementPrChange(changes, (prevProps, getNextId, date) => {
418
- const existing = section.getSectPrChange();
419
- if (existing) {
420
- const merged = { ...(existing.previousProperties || {}), ...prevProps };
421
- section.setSectPrChange({ ...existing, previousProperties: merged });
422
- } else {
423
- section.setSectPrChange({ author: this.author, date, id: String(getNextId()), previousProperties: prevProps });
424
- }
425
- });
426
- }
427
-
428
- // Apply rPrChange to each Run that has property changes
429
- for (const [run, changes] of runChanges) {
430
- this.applyRunPrChange(run, changes);
431
- }
432
-
433
- this.pendingChanges.clear();
434
- return revisions;
435
- }
436
-
437
- // ═══════════════════════════════════════════════════════════════════════════
438
- // Private Methods
439
- // ═══════════════════════════════════════════════════════════════════════════
440
-
441
- /**
442
- * Apply pPrChange to a paragraph (extracted from flushPendingChanges for readability)
443
- */
444
- private applyParagraphPrChange(paragraph: Paragraph, changes: PendingChange[]): void {
445
- const newPreviousProperties: Record<string, unknown> = {};
446
- let latestTimestamp = 0;
447
-
448
- for (const change of changes) {
449
- // Record previous value even if undefined (property wasn't set before)
450
- newPreviousProperties[change.property] = change.previousValue;
451
- if (change.timestamp > latestTimestamp) {
452
- latestTimestamp = change.timestamp;
453
- }
454
- }
455
-
456
- const existingChange = paragraph.formatting.pPrChange;
457
-
458
- if (existingChange) {
459
- const mergedPreviousProperties: Record<string, unknown> = {
460
- ...(existingChange.previousProperties || {}),
461
- };
462
- for (const [prop, value] of Object.entries(newPreviousProperties)) {
463
- mergedPreviousProperties[prop] = value;
464
- }
465
- paragraph.formatting.pPrChange = {
466
- author: existingChange.author,
467
- date: existingChange.date,
468
- id: existingChange.id,
469
- previousProperties: mergedPreviousProperties,
470
- };
471
- } else {
472
- const revisionId = this.revisionManager.consumeNextId();
473
- paragraph.formatting.pPrChange = {
474
- author: this.author,
475
- date: formatDateForXml(new Date(latestTimestamp)),
476
- id: String(revisionId),
477
- previousProperties: newPreviousProperties,
478
- };
479
- }
480
- }
481
-
482
- /**
483
- * Apply rPrChange to a run (mirrors applyParagraphPrChange for runs)
484
- */
485
- private applyRunPrChange(run: Run, changes: PendingChange[]): void {
486
- const newPreviousProperties: Partial<RunFormatting> = {};
487
- let latestTimestamp = 0;
488
-
489
- for (const change of changes) {
490
- (newPreviousProperties as Record<string, unknown>)[change.property] = change.previousValue;
491
- if (change.timestamp > latestTimestamp) {
492
- latestTimestamp = change.timestamp;
493
- }
494
- }
495
-
496
- const existingChange = run.getPropertyChangeRevision();
497
-
498
- if (existingChange) {
499
- // Merge with existing rPrChange — preserve original author/date,
500
- // add new previous properties
501
- const mergedPreviousProperties: Partial<RunFormatting> = {
502
- ...(existingChange.previousProperties || {}),
503
- ...newPreviousProperties,
504
- };
505
- run.setPropertyChangeRevision({
506
- ...existingChange,
507
- previousProperties: mergedPreviousProperties,
508
- });
509
- } else {
510
- const revisionId = this.revisionManager.consumeNextId();
511
- run.setPropertyChangeRevision({
512
- id: revisionId,
513
- author: this.author,
514
- date: new Date(latestTimestamp),
515
- previousProperties: newPreviousProperties,
516
- });
517
- }
518
- }
519
-
520
- /**
521
- * Generic helper to apply *PrChange to table/row/cell/section elements
522
- */
523
- private applyElementPrChange(
524
- changes: PendingChange[],
525
- applier: (prevProps: Record<string, unknown>, getNextId: () => number, date: string) => void
526
- ): void {
527
- const prevProps: Record<string, unknown> = {};
528
- let latestTimestamp = 0;
529
-
530
- for (const change of changes) {
531
- // Record previous value even if undefined (property wasn't set before)
532
- prevProps[change.property] = change.previousValue;
533
- if (change.timestamp > latestTimestamp) {
534
- latestTimestamp = change.timestamp;
535
- }
536
- }
537
-
538
- const date = formatDateForXml(new Date(latestTimestamp));
539
- applier(prevProps, () => this.revisionManager.consumeNextId(), date);
540
- }
541
-
542
- /**
543
- * Add a pending change, consolidating with existing if same key
544
- */
545
- private addPendingChange(key: string, change: PendingChange): void {
546
- const existing = this.pendingChanges.get(key);
547
- if (existing) {
548
- existing.count = (existing.count || 1) + 1;
549
- // Keep the first previousValue for consolidated changes
550
- } else {
551
- this.pendingChanges.set(key, { ...change, count: 1 });
552
- }
553
- }
554
-
555
- /**
556
- * Create a Revision from a pending change
557
- */
558
- private createRevision(pending: PendingChange): Revision {
559
- const previousProps: Record<string, any> = {};
560
- const newProps: Record<string, any> = {};
561
-
562
- if (pending.previousValue !== undefined) {
563
- previousProps[pending.property] = pending.previousValue;
564
- }
565
- if (pending.newValue !== undefined) {
566
- newProps[pending.property] = pending.newValue;
567
- }
568
-
569
- // Get or create a Run for the revision content
570
- const run = this.getRunFromElement(pending.element);
571
-
572
- return new Revision({
573
- author: this.author,
574
- type: pending.type,
575
- content: run,
576
- previousProperties:
577
- Object.keys(previousProps).length > 0 ? previousProps : undefined,
578
- newProperties: Object.keys(newProps).length > 0 ? newProps : undefined,
579
- date: new Date(pending.timestamp),
580
- });
581
- }
582
-
583
- /**
584
- * Get or create a Run from an element for revision content
585
- */
586
- private getRunFromElement(element: TrackableElement): Run {
587
- if (element instanceof Run) {
588
- return element;
589
- }
590
-
591
- // Use instanceof for type-safe element identification (minification-safe)
592
- if (element instanceof Table) return new Run('Table');
593
- if (element instanceof TableRow) return new Run('TableRow');
594
- if (element instanceof TableCell) return new Run('TableCell');
595
- if (element instanceof Section) return new Run('Section');
596
- if (element instanceof Paragraph) return new Run('Paragraph');
597
-
598
- // Fallback for other elements (e.g., Hyperlink)
599
- const hasGetText = 'getText' in element && typeof (element as { getText?: () => string }).getText === 'function';
600
- const text = hasGetText
601
- ? (element as { getText: () => string }).getText()
602
- : element?.constructor?.name || 'Unknown element';
603
- return new Run(typeof text === 'string' ? text : String(text));
604
- }
605
-
606
- /**
607
- * Get a stable unique ID for an element (used in consolidation keys)
608
- */
609
- private getElementId(element: TrackableElement): number {
610
- let id = this.elementIdMap.get(element as object);
611
- if (id === undefined) {
612
- id = this.elementIdCounter++;
613
- this.elementIdMap.set(element as object, id);
614
- }
615
- return id;
616
- }
617
-
618
- /**
619
- * Deep equality check for tracking values (handles objects, primitives, null/undefined)
620
- */
621
- private valuesEqual(a: unknown, b: unknown): boolean {
622
- if (a === b) return true;
623
- if (a == null || b == null) return false;
624
- if (typeof a !== 'object' || typeof b !== 'object') return false;
625
- return JSON.stringify(a) === JSON.stringify(b);
626
- }
627
-
628
- /**
629
- * Stringify a value for use in consolidation key
630
- */
631
- private stringifyValue(value: unknown): string {
632
- if (value === undefined) return 'undefined';
633
- if (value === null) return 'null';
634
- if (typeof value === 'object') {
635
- return JSON.stringify(value);
636
- }
637
- return String(value);
638
- }
639
-
640
- /**
641
- * Create an insertion revision (factory to avoid circular dependency in Run)
642
- */
643
- createInsertion(content: Run, date?: Date): Revision {
644
- return Revision.createInsertion(this.author, content, date);
645
- }
646
-
647
- /**
648
- * Create a deletion revision (factory to avoid circular dependency in Run)
649
- */
650
- createDeletion(content: Run, date?: Date): Revision {
651
- return Revision.createDeletion(this.author, content, date);
652
- }
653
-
654
- /**
655
- * Get count of pending changes
656
- */
657
- getPendingCount(): number {
658
- return this.pendingChanges.size;
659
- }
660
-
661
- /**
662
- * Check if there are pending changes
663
- */
664
- hasPendingChanges(): boolean {
665
- return this.pendingChanges.size > 0;
666
- }
667
-
668
- /**
669
- * Clear all pending changes without creating revisions
670
- */
671
- clearPendingChanges(): void {
672
- this.pendingChanges.clear();
673
- }
674
- }
1
+ /**
2
+ * DocumentTrackingContext - Implementation of automatic change tracking
3
+ *
4
+ * Manages pending changes and creates Revision objects when flushed.
5
+ * Supports consolidation of similar changes within a time window.
6
+ *
7
+ * @module DocumentTrackingContext
8
+ */
9
+
10
+ import { Revision, RevisionType } from '../elements/Revision';
11
+ import { RevisionManager } from '../elements/RevisionManager';
12
+ import { Run, type RunFormatting } from '../elements/Run';
13
+ import { Paragraph } from '../elements/Paragraph';
14
+ import { Table } from '../elements/Table';
15
+ import { TableRow } from '../elements/TableRow';
16
+ import { TableCell } from '../elements/TableCell';
17
+ import { Section } from '../elements/Section';
18
+ import type { TrackingContext, PendingChange, TrackableElement } from './TrackingContext';
19
+ import { formatDateForXml } from '../utils/dateFormatting';
20
+
21
+ /**
22
+ * Enable options for tracking context
23
+ */
24
+ export interface TrackingEnableOptions {
25
+ /** Author name for new revisions (default: 'DocHub') */
26
+ author?: string;
27
+ /** Whether to track formatting changes (default: true) */
28
+ trackFormatting?: boolean;
29
+ }
30
+
31
+ /**
32
+ * Implementation of TrackingContext for Document
33
+ */
34
+ export class DocumentTrackingContext implements TrackingContext {
35
+ private enabled = false;
36
+ private trackFormatting = true;
37
+ private author = 'DocHub';
38
+ private revisionManager: RevisionManager;
39
+
40
+ /** Counter for assigning stable element IDs */
41
+ private elementIdCounter = 0;
42
+ /** Stable element identity map (WeakMap so elements can be GC'd) */
43
+ private elementIdMap = new WeakMap<object, number>();
44
+
45
+ /** Pending changes waiting to be flushed */
46
+ private pendingChanges = new Map<string, PendingChange>();
47
+
48
+ /** Properties considered "formatting" (vs structural) */
49
+ private static readonly FORMATTING_PROPERTIES = new Set([
50
+ 'bold',
51
+ 'italic',
52
+ 'underline',
53
+ 'strike',
54
+ 'dstrike',
55
+ 'subscript',
56
+ 'superscript',
57
+ 'font',
58
+ 'size',
59
+ 'color',
60
+ 'highlight',
61
+ 'smallCaps',
62
+ 'allCaps',
63
+ 'characterSpacing',
64
+ 'scaling',
65
+ 'position',
66
+ 'emphasis',
67
+ 'shadow',
68
+ 'emboss',
69
+ 'imprint',
70
+ 'outline',
71
+ 'vanish',
72
+ ]);
73
+
74
+ /**
75
+ * Creates a new DocumentTrackingContext
76
+ * @param revisionManager - RevisionManager to register revisions with
77
+ */
78
+ constructor(revisionManager: RevisionManager) {
79
+ this.revisionManager = revisionManager;
80
+ }
81
+
82
+ /**
83
+ * Enable change tracking
84
+ * @param options - Enable options
85
+ */
86
+ enable(options?: TrackingEnableOptions): void {
87
+ this.enabled = true;
88
+ if (options?.author) {
89
+ this.author = options.author;
90
+ }
91
+ if (options?.trackFormatting !== undefined) {
92
+ this.trackFormatting = options.trackFormatting;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Disable change tracking and flush pending changes
98
+ */
99
+ disable(): void {
100
+ this.flushPendingChanges();
101
+ this.enabled = false;
102
+ }
103
+
104
+ /**
105
+ * Set the author for new revisions
106
+ * Flushes any pending changes before switching to prevent mixed authorship
107
+ * @param author - Author name
108
+ */
109
+ setAuthor(author: string): void {
110
+ // Flush pending changes before switching authors to prevent mixed authorship
111
+ if (this.enabled && this.pendingChanges.size > 0) {
112
+ this.flushPendingChanges();
113
+ }
114
+ this.author = author;
115
+ }
116
+
117
+ // ═══════════════════════════════════════════════════════════════════════════
118
+ // TrackingContext Interface Implementation
119
+ // ═══════════════════════════════════════════════════════════════════════════
120
+
121
+ isEnabled(): boolean {
122
+ return this.enabled;
123
+ }
124
+
125
+ getAuthor(): string {
126
+ return this.author;
127
+ }
128
+
129
+ getRevisionManager(): RevisionManager {
130
+ return this.revisionManager;
131
+ }
132
+
133
+ isTrackFormattingEnabled(): boolean {
134
+ return this.trackFormatting;
135
+ }
136
+
137
+ trackRunPropertyChange(run: Run, property: string, oldValue: unknown, newValue: unknown): void {
138
+ if (!this.enabled) return;
139
+ if (this.valuesEqual(oldValue, newValue)) return;
140
+
141
+ // Skip formatting changes if not tracking them
142
+ if (!this.trackFormatting && DocumentTrackingContext.FORMATTING_PROPERTIES.has(property)) {
143
+ return;
144
+ }
145
+
146
+ // Create consolidation key with element identity
147
+ const key = `runProp:${property}:${this.stringifyValue(newValue)}@${this.getElementId(run)}`;
148
+
149
+ this.addPendingChange(key, {
150
+ type: 'runPropertiesChange',
151
+ property,
152
+ previousValue: oldValue,
153
+ newValue,
154
+ element: run,
155
+ timestamp: Date.now(),
156
+ });
157
+ }
158
+
159
+ trackParagraphPropertyChange(
160
+ paragraph: TrackableElement,
161
+ property: string,
162
+ oldValue: unknown,
163
+ newValue: unknown
164
+ ): void {
165
+ if (!this.enabled) return;
166
+ if (this.valuesEqual(oldValue, newValue)) return;
167
+
168
+ const key = `paraProp:${property}:${this.stringifyValue(newValue)}@${this.getElementId(paragraph)}`;
169
+
170
+ this.addPendingChange(key, {
171
+ type: 'paragraphPropertiesChange',
172
+ property,
173
+ previousValue: oldValue,
174
+ newValue,
175
+ element: paragraph,
176
+ timestamp: Date.now(),
177
+ });
178
+ }
179
+
180
+ trackHyperlinkChange(
181
+ hyperlink: TrackableElement,
182
+ changeType: string,
183
+ oldValue: unknown,
184
+ newValue: unknown
185
+ ): void {
186
+ if (!this.enabled) return;
187
+ if (this.valuesEqual(oldValue, newValue)) return;
188
+
189
+ // Hyperlink changes use dedicated type for proper categorization
190
+ const key = `hyperlink:${changeType}:${this.stringifyValue(newValue)}@${this.getElementId(hyperlink)}`;
191
+
192
+ this.addPendingChange(key, {
193
+ type: 'hyperlinkChange',
194
+ property: changeType,
195
+ previousValue: oldValue,
196
+ newValue,
197
+ element: hyperlink,
198
+ timestamp: Date.now(),
199
+ });
200
+ }
201
+
202
+ trackTableChange(
203
+ element: TrackableElement,
204
+ property: string,
205
+ oldValue: unknown,
206
+ newValue: unknown
207
+ ): void {
208
+ if (!this.enabled) return;
209
+ if (this.valuesEqual(oldValue, newValue)) return;
210
+
211
+ // Determine revision type based on element type using instanceof (minification-safe)
212
+ let type: RevisionType = 'tablePropertiesChange';
213
+ let elementType = 'Table';
214
+
215
+ if (element instanceof TableRow) {
216
+ type = 'tableRowPropertiesChange';
217
+ elementType = 'TableRow';
218
+ } else if (element instanceof TableCell) {
219
+ type = 'tableCellPropertiesChange';
220
+ elementType = 'TableCell';
221
+ }
222
+
223
+ const key = `table:${elementType}:${property}:${this.stringifyValue(newValue)}@${this.getElementId(element)}`;
224
+
225
+ this.addPendingChange(key, {
226
+ type,
227
+ property,
228
+ previousValue: oldValue,
229
+ newValue,
230
+ element,
231
+ timestamp: Date.now(),
232
+ });
233
+ }
234
+
235
+ trackSectionChange(
236
+ section: TrackableElement,
237
+ property: string,
238
+ oldValue: unknown,
239
+ newValue: unknown
240
+ ): void {
241
+ if (!this.enabled) return;
242
+ if (this.valuesEqual(oldValue, newValue)) return;
243
+
244
+ const key = `section:${property}:${this.stringifyValue(newValue)}@${this.getElementId(section)}`;
245
+
246
+ this.addPendingChange(key, {
247
+ type: 'sectionPropertiesChange',
248
+ property,
249
+ previousValue: oldValue,
250
+ newValue,
251
+ element: section,
252
+ timestamp: Date.now(),
253
+ });
254
+ }
255
+
256
+ trackInsertion(element: TrackableElement, text: string): void {
257
+ if (!this.enabled) return;
258
+ if (!text) return;
259
+
260
+ const key = `insert:${Date.now()}:${text.substring(0, 20)}`;
261
+
262
+ this.addPendingChange(key, {
263
+ type: 'insert',
264
+ property: 'text',
265
+ previousValue: undefined,
266
+ newValue: text,
267
+ element,
268
+ timestamp: Date.now(),
269
+ });
270
+ }
271
+
272
+ trackDeletion(element: TrackableElement, text: string): void {
273
+ if (!this.enabled) return;
274
+ if (!text) return;
275
+
276
+ const key = `delete:${Date.now()}:${text.substring(0, 20)}`;
277
+
278
+ this.addPendingChange(key, {
279
+ type: 'delete',
280
+ property: 'text',
281
+ previousValue: text,
282
+ newValue: undefined,
283
+ element,
284
+ timestamp: Date.now(),
285
+ });
286
+ }
287
+
288
+ flushPendingChanges(): Revision[] {
289
+ const revisions: Revision[] = [];
290
+
291
+ // Group pending changes by element for consolidation
292
+ const paragraphChanges = new Map<Paragraph, PendingChange[]>();
293
+ const tableChanges = new Map<Table, PendingChange[]>();
294
+ const rowChanges = new Map<TableRow, PendingChange[]>();
295
+ const cellChanges = new Map<TableCell, PendingChange[]>();
296
+ const sectionChanges = new Map<Section, PendingChange[]>();
297
+ const runChanges = new Map<Run, PendingChange[]>();
298
+
299
+ for (const [, pending] of this.pendingChanges) {
300
+ const revision = this.createRevision(pending);
301
+ this.revisionManager.register(revision);
302
+ revisions.push(revision);
303
+
304
+ // Collect changes by element type for *PrChange application
305
+ if (pending.type === 'paragraphPropertiesChange' && pending.element instanceof Paragraph) {
306
+ const changes = paragraphChanges.get(pending.element) || [];
307
+ changes.push(pending);
308
+ paragraphChanges.set(pending.element, changes);
309
+ } else if (pending.type === 'tablePropertiesChange' && pending.element instanceof Table) {
310
+ const changes = tableChanges.get(pending.element) || [];
311
+ changes.push(pending);
312
+ tableChanges.set(pending.element, changes);
313
+ } else if (
314
+ pending.type === 'tableRowPropertiesChange' &&
315
+ pending.element instanceof TableRow
316
+ ) {
317
+ const changes = rowChanges.get(pending.element) || [];
318
+ changes.push(pending);
319
+ rowChanges.set(pending.element, changes);
320
+ } else if (
321
+ pending.type === 'tableCellPropertiesChange' &&
322
+ pending.element instanceof TableCell
323
+ ) {
324
+ const changes = cellChanges.get(pending.element) || [];
325
+ changes.push(pending);
326
+ cellChanges.set(pending.element, changes);
327
+ } else if (pending.type === 'sectionPropertiesChange' && pending.element instanceof Section) {
328
+ const changes = sectionChanges.get(pending.element) || [];
329
+ changes.push(pending);
330
+ sectionChanges.set(pending.element, changes);
331
+ } else if (pending.type === 'runPropertiesChange' && pending.element instanceof Run) {
332
+ const changes = runChanges.get(pending.element) || [];
333
+ changes.push(pending);
334
+ runChanges.set(pending.element, changes);
335
+ }
336
+ }
337
+
338
+ // Apply pPrChange to each paragraph that has property changes
339
+ for (const [paragraph, changes] of paragraphChanges) {
340
+ this.applyParagraphPrChange(paragraph, changes);
341
+ }
342
+
343
+ // Apply tblPrChange to each Table
344
+ // Per ECMA-376 §17.13.5.36, tblPrChange must contain FULL previous tblPr,
345
+ // not just the delta of changed properties.
346
+ for (const [table, changes] of tableChanges) {
347
+ // Build full snapshot: start from current formatting, roll back changed properties
348
+ const currentFormatting = table.getFormatting();
349
+ const fullPrevProps: Record<string, unknown> = {};
350
+
351
+ for (const [key, value] of Object.entries(currentFormatting)) {
352
+ if (value !== undefined) {
353
+ fullPrevProps[key] = value;
354
+ }
355
+ }
356
+
357
+ // Roll back changed properties to their previous values
358
+ let latestTimestamp = 0;
359
+ for (const change of changes) {
360
+ if (change.previousValue !== undefined) {
361
+ fullPrevProps[change.property] = change.previousValue;
362
+ } else {
363
+ delete fullPrevProps[change.property];
364
+ }
365
+ if (change.timestamp > latestTimestamp) {
366
+ latestTimestamp = change.timestamp;
367
+ }
368
+ }
369
+
370
+ const date = formatDateForXml(new Date(latestTimestamp));
371
+
372
+ const existing = table.getTblPrChange();
373
+ if (existing) {
374
+ // Merge: existing previous state takes precedence (it's the ORIGINAL baseline)
375
+ const merged = { ...fullPrevProps, ...(existing.previousProperties || {}) };
376
+ table.setTblPrChange({ ...existing, previousProperties: merged });
377
+ } else {
378
+ table.setTblPrChange({
379
+ author: this.author,
380
+ date,
381
+ id: String(this.revisionManager.consumeNextId()),
382
+ previousProperties: fullPrevProps,
383
+ });
384
+ }
385
+ }
386
+
387
+ // Apply trPrChange to each TableRow
388
+ for (const [row, changes] of rowChanges) {
389
+ this.applyElementPrChange(changes, (prevProps, getNextId, date) => {
390
+ const existing = row.getTrPrChange();
391
+ if (existing) {
392
+ const merged = { ...(existing.previousProperties || {}), ...prevProps };
393
+ row.setTrPrChange({ ...existing, previousProperties: merged });
394
+ } else {
395
+ row.setTrPrChange({
396
+ author: this.author,
397
+ date,
398
+ id: String(getNextId()),
399
+ previousProperties: prevProps,
400
+ });
401
+ }
402
+ });
403
+ }
404
+
405
+ // Apply tcPrChange to each TableCell
406
+ for (const [cell, changes] of cellChanges) {
407
+ this.applyElementPrChange(changes, (prevProps, getNextId, date) => {
408
+ const existing = cell.getTcPrChange();
409
+ if (existing) {
410
+ const merged = { ...(existing.previousProperties || {}), ...prevProps };
411
+ cell.setTcPrChange({ ...existing, previousProperties: merged });
412
+ } else {
413
+ cell.setTcPrChange({
414
+ author: this.author,
415
+ date,
416
+ id: String(getNextId()),
417
+ previousProperties: prevProps,
418
+ });
419
+ }
420
+ });
421
+ }
422
+
423
+ // Apply sectPrChange to each Section
424
+ for (const [section, changes] of sectionChanges) {
425
+ this.applyElementPrChange(changes, (prevProps, getNextId, date) => {
426
+ const existing = section.getSectPrChange();
427
+ if (existing) {
428
+ const merged = { ...(existing.previousProperties || {}), ...prevProps };
429
+ section.setSectPrChange({ ...existing, previousProperties: merged });
430
+ } else {
431
+ section.setSectPrChange({
432
+ author: this.author,
433
+ date,
434
+ id: String(getNextId()),
435
+ previousProperties: prevProps,
436
+ });
437
+ }
438
+ });
439
+ }
440
+
441
+ // Apply rPrChange to each Run that has property changes
442
+ for (const [run, changes] of runChanges) {
443
+ this.applyRunPrChange(run, changes);
444
+ }
445
+
446
+ this.pendingChanges.clear();
447
+ return revisions;
448
+ }
449
+
450
+ // ═══════════════════════════════════════════════════════════════════════════
451
+ // Private Methods
452
+ // ═══════════════════════════════════════════════════════════════════════════
453
+
454
+ /**
455
+ * Apply pPrChange to a paragraph (extracted from flushPendingChanges for readability)
456
+ */
457
+ private applyParagraphPrChange(paragraph: Paragraph, changes: PendingChange[]): void {
458
+ const newPreviousProperties: Record<string, unknown> = {};
459
+ let latestTimestamp = 0;
460
+
461
+ for (const change of changes) {
462
+ // Record previous value even if undefined (property wasn't set before)
463
+ newPreviousProperties[change.property] = change.previousValue;
464
+ if (change.timestamp > latestTimestamp) {
465
+ latestTimestamp = change.timestamp;
466
+ }
467
+ }
468
+
469
+ const existingChange = paragraph.formatting.pPrChange;
470
+
471
+ if (existingChange) {
472
+ const mergedPreviousProperties: Record<string, unknown> = {
473
+ ...(existingChange.previousProperties || {}),
474
+ };
475
+ for (const [prop, value] of Object.entries(newPreviousProperties)) {
476
+ mergedPreviousProperties[prop] = value;
477
+ }
478
+ paragraph.formatting.pPrChange = {
479
+ author: existingChange.author,
480
+ date: existingChange.date,
481
+ id: existingChange.id,
482
+ previousProperties: mergedPreviousProperties,
483
+ };
484
+ } else {
485
+ const revisionId = this.revisionManager.consumeNextId();
486
+ paragraph.formatting.pPrChange = {
487
+ author: this.author,
488
+ date: formatDateForXml(new Date(latestTimestamp)),
489
+ id: String(revisionId),
490
+ previousProperties: newPreviousProperties,
491
+ };
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Apply rPrChange to a run (mirrors applyParagraphPrChange for runs)
497
+ */
498
+ private applyRunPrChange(run: Run, changes: PendingChange[]): void {
499
+ const newPreviousProperties: Partial<RunFormatting> = {};
500
+ let latestTimestamp = 0;
501
+
502
+ for (const change of changes) {
503
+ (newPreviousProperties as Record<string, unknown>)[change.property] = change.previousValue;
504
+ if (change.timestamp > latestTimestamp) {
505
+ latestTimestamp = change.timestamp;
506
+ }
507
+ }
508
+
509
+ const existingChange = run.getPropertyChangeRevision();
510
+
511
+ if (existingChange) {
512
+ // Merge with existing rPrChange — preserve original author/date,
513
+ // add new previous properties
514
+ const mergedPreviousProperties: Partial<RunFormatting> = {
515
+ ...(existingChange.previousProperties || {}),
516
+ ...newPreviousProperties,
517
+ };
518
+ run.setPropertyChangeRevision({
519
+ ...existingChange,
520
+ previousProperties: mergedPreviousProperties,
521
+ });
522
+ } else {
523
+ const revisionId = this.revisionManager.consumeNextId();
524
+ run.setPropertyChangeRevision({
525
+ id: revisionId,
526
+ author: this.author,
527
+ date: new Date(latestTimestamp),
528
+ previousProperties: newPreviousProperties,
529
+ });
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Generic helper to apply *PrChange to table/row/cell/section elements
535
+ */
536
+ private applyElementPrChange(
537
+ changes: PendingChange[],
538
+ applier: (prevProps: Record<string, unknown>, getNextId: () => number, date: string) => void
539
+ ): void {
540
+ const prevProps: Record<string, unknown> = {};
541
+ let latestTimestamp = 0;
542
+
543
+ for (const change of changes) {
544
+ // Record previous value even if undefined (property wasn't set before)
545
+ prevProps[change.property] = change.previousValue;
546
+ if (change.timestamp > latestTimestamp) {
547
+ latestTimestamp = change.timestamp;
548
+ }
549
+ }
550
+
551
+ const date = formatDateForXml(new Date(latestTimestamp));
552
+ applier(prevProps, () => this.revisionManager.consumeNextId(), date);
553
+ }
554
+
555
+ /**
556
+ * Add a pending change, consolidating with existing if same key
557
+ */
558
+ private addPendingChange(key: string, change: PendingChange): void {
559
+ const existing = this.pendingChanges.get(key);
560
+ if (existing) {
561
+ existing.count = (existing.count || 1) + 1;
562
+ // Keep the first previousValue for consolidated changes
563
+ } else {
564
+ this.pendingChanges.set(key, { ...change, count: 1 });
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Create a Revision from a pending change
570
+ */
571
+ private createRevision(pending: PendingChange): Revision {
572
+ const previousProps: Record<string, any> = {};
573
+ const newProps: Record<string, any> = {};
574
+
575
+ if (pending.previousValue !== undefined) {
576
+ previousProps[pending.property] = pending.previousValue;
577
+ }
578
+ if (pending.newValue !== undefined) {
579
+ newProps[pending.property] = pending.newValue;
580
+ }
581
+
582
+ // Get or create a Run for the revision content
583
+ const run = this.getRunFromElement(pending.element);
584
+
585
+ return new Revision({
586
+ author: this.author,
587
+ type: pending.type,
588
+ content: run,
589
+ previousProperties: Object.keys(previousProps).length > 0 ? previousProps : undefined,
590
+ newProperties: Object.keys(newProps).length > 0 ? newProps : undefined,
591
+ date: new Date(pending.timestamp),
592
+ });
593
+ }
594
+
595
+ /**
596
+ * Get or create a Run from an element for revision content
597
+ */
598
+ private getRunFromElement(element: TrackableElement): Run {
599
+ if (element instanceof Run) {
600
+ return element;
601
+ }
602
+
603
+ // Use instanceof for type-safe element identification (minification-safe)
604
+ if (element instanceof Table) return new Run('Table');
605
+ if (element instanceof TableRow) return new Run('TableRow');
606
+ if (element instanceof TableCell) return new Run('TableCell');
607
+ if (element instanceof Section) return new Run('Section');
608
+ if (element instanceof Paragraph) return new Run('Paragraph');
609
+
610
+ // Fallback for other elements (e.g., Hyperlink)
611
+ const hasGetText =
612
+ 'getText' in element && typeof (element as { getText?: () => string }).getText === 'function';
613
+ const text = hasGetText
614
+ ? (element as { getText: () => string }).getText()
615
+ : element?.constructor?.name || 'Unknown element';
616
+ return new Run(typeof text === 'string' ? text : String(text));
617
+ }
618
+
619
+ /**
620
+ * Get a stable unique ID for an element (used in consolidation keys)
621
+ */
622
+ private getElementId(element: TrackableElement): number {
623
+ let id = this.elementIdMap.get(element as object);
624
+ if (id === undefined) {
625
+ id = this.elementIdCounter++;
626
+ this.elementIdMap.set(element as object, id);
627
+ }
628
+ return id;
629
+ }
630
+
631
+ /**
632
+ * Deep equality check for tracking values (handles objects, primitives, null/undefined)
633
+ */
634
+ private valuesEqual(a: unknown, b: unknown): boolean {
635
+ if (a === b) return true;
636
+ if (a == null || b == null) return false;
637
+ if (typeof a !== 'object' || typeof b !== 'object') return false;
638
+ return JSON.stringify(a) === JSON.stringify(b);
639
+ }
640
+
641
+ /**
642
+ * Stringify a value for use in consolidation key
643
+ */
644
+ private stringifyValue(value: unknown): string {
645
+ if (value === undefined) return 'undefined';
646
+ if (value === null) return 'null';
647
+ if (typeof value === 'object') {
648
+ return JSON.stringify(value);
649
+ }
650
+ return String(value);
651
+ }
652
+
653
+ /**
654
+ * Create an insertion revision (factory to avoid circular dependency in Run)
655
+ */
656
+ createInsertion(content: Run, date?: Date): Revision {
657
+ return Revision.createInsertion(this.author, content, date);
658
+ }
659
+
660
+ /**
661
+ * Create a deletion revision (factory to avoid circular dependency in Run)
662
+ */
663
+ createDeletion(content: Run, date?: Date): Revision {
664
+ return Revision.createDeletion(this.author, content, date);
665
+ }
666
+
667
+ /**
668
+ * Get count of pending changes
669
+ */
670
+ getPendingCount(): number {
671
+ return this.pendingChanges.size;
672
+ }
673
+
674
+ /**
675
+ * Check if there are pending changes
676
+ */
677
+ hasPendingChanges(): boolean {
678
+ return this.pendingChanges.size > 0;
679
+ }
680
+
681
+ /**
682
+ * Clear all pending changes without creating revisions
683
+ */
684
+ clearPendingChanges(): void {
685
+ this.pendingChanges.clear();
686
+ }
687
+ }