docxmlater 10.1.4 → 10.1.6

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