docxmlater 10.0.2 → 10.0.4

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 (398) hide show
  1. package/README.md +3 -2
  2. package/dist/constants/legacyCompatFlags.d.ts.map +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.d.ts +0 -27
  5. package/dist/constants/limits.d.ts.map +1 -1
  6. package/dist/constants/limits.js +13 -13
  7. package/dist/constants/limits.js.map +1 -1
  8. package/dist/core/Document.d.ts +23 -19
  9. package/dist/core/Document.d.ts.map +1 -1
  10. package/dist/core/Document.js +197 -63
  11. package/dist/core/Document.js.map +1 -1
  12. package/dist/core/DocumentContent.d.ts.map +1 -1
  13. package/dist/core/DocumentContent.js.map +1 -1
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +59 -24
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.d.ts.map +1 -1
  18. package/dist/core/DocumentIdManager.js.map +1 -1
  19. package/dist/core/DocumentParser.d.ts +6 -6
  20. package/dist/core/DocumentParser.d.ts.map +1 -1
  21. package/dist/core/DocumentParser.js +86 -55
  22. package/dist/core/DocumentParser.js.map +1 -1
  23. package/dist/core/DocumentValidator.d.ts.map +1 -1
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.d.ts.map +1 -1
  26. package/dist/core/Relationship.js +1 -1
  27. package/dist/core/Relationship.js.map +1 -1
  28. package/dist/core/RelationshipManager.js +3 -3
  29. package/dist/core/RelationshipManager.js.map +1 -1
  30. package/dist/elements/AlternateContent.js.map +1 -1
  31. package/dist/elements/Bookmark.d.ts.map +1 -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.js +1 -1
  36. package/dist/elements/Comment.js.map +1 -1
  37. package/dist/elements/CommentManager.d.ts.map +1 -1
  38. package/dist/elements/CommentManager.js +8 -2
  39. package/dist/elements/CommentManager.js.map +1 -1
  40. package/dist/elements/CommonTypes.d.ts.map +1 -1
  41. package/dist/elements/CommonTypes.js +1 -2
  42. package/dist/elements/CommonTypes.js.map +1 -1
  43. package/dist/elements/CustomXml.js.map +1 -1
  44. package/dist/elements/Endnote.d.ts.map +1 -1
  45. package/dist/elements/Endnote.js.map +1 -1
  46. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  47. package/dist/elements/EndnoteManager.js.map +1 -1
  48. package/dist/elements/Field.d.ts.map +1 -1
  49. package/dist/elements/Field.js +31 -28
  50. package/dist/elements/Field.js.map +1 -1
  51. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  52. package/dist/elements/FieldHelpers.js +6 -6
  53. package/dist/elements/FieldHelpers.js.map +1 -1
  54. package/dist/elements/FontManager.d.ts.map +1 -1
  55. package/dist/elements/FontManager.js.map +1 -1
  56. package/dist/elements/Footer.js.map +1 -1
  57. package/dist/elements/Footnote.d.ts.map +1 -1
  58. package/dist/elements/Footnote.js.map +1 -1
  59. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  60. package/dist/elements/FootnoteManager.js.map +1 -1
  61. package/dist/elements/Header.js.map +1 -1
  62. package/dist/elements/HeaderFooterManager.js.map +1 -1
  63. package/dist/elements/Hyperlink.d.ts.map +1 -1
  64. package/dist/elements/Hyperlink.js +5 -5
  65. package/dist/elements/Hyperlink.js.map +1 -1
  66. package/dist/elements/Image.d.ts +2 -2
  67. package/dist/elements/Image.d.ts.map +1 -1
  68. package/dist/elements/Image.js +21 -5
  69. package/dist/elements/Image.js.map +1 -1
  70. package/dist/elements/ImageManager.d.ts.map +1 -1
  71. package/dist/elements/ImageManager.js +2 -2
  72. package/dist/elements/ImageManager.js.map +1 -1
  73. package/dist/elements/ImageRun.js.map +1 -1
  74. package/dist/elements/MathElement.js.map +1 -1
  75. package/dist/elements/Paragraph.d.ts +8 -0
  76. package/dist/elements/Paragraph.d.ts.map +1 -1
  77. package/dist/elements/Paragraph.js +153 -118
  78. package/dist/elements/Paragraph.js.map +1 -1
  79. package/dist/elements/PreservedElement.js.map +1 -1
  80. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  81. package/dist/elements/RangeMarker.js.map +1 -1
  82. package/dist/elements/Revision.d.ts +1 -0
  83. package/dist/elements/Revision.d.ts.map +1 -1
  84. package/dist/elements/Revision.js +44 -5
  85. package/dist/elements/Revision.js.map +1 -1
  86. package/dist/elements/RevisionContent.js.map +1 -1
  87. package/dist/elements/RevisionManager.d.ts.map +1 -1
  88. package/dist/elements/RevisionManager.js.map +1 -1
  89. package/dist/elements/Run.d.ts.map +1 -1
  90. package/dist/elements/Run.js +1 -3
  91. package/dist/elements/Run.js.map +1 -1
  92. package/dist/elements/Section.d.ts.map +1 -1
  93. package/dist/elements/Section.js +127 -118
  94. package/dist/elements/Section.js.map +1 -1
  95. package/dist/elements/Shape.d.ts.map +1 -1
  96. package/dist/elements/Shape.js +21 -0
  97. package/dist/elements/Shape.js.map +1 -1
  98. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  99. package/dist/elements/StructuredDocumentTag.js +20 -8
  100. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  101. package/dist/elements/Table.d.ts +2 -2
  102. package/dist/elements/Table.d.ts.map +1 -1
  103. package/dist/elements/Table.js +29 -35
  104. package/dist/elements/Table.js.map +1 -1
  105. package/dist/elements/TableCell.d.ts +2 -2
  106. package/dist/elements/TableCell.d.ts.map +1 -1
  107. package/dist/elements/TableCell.js +63 -67
  108. package/dist/elements/TableCell.js.map +1 -1
  109. package/dist/elements/TableGridChange.js.map +1 -1
  110. package/dist/elements/TableOfContents.d.ts +6 -6
  111. package/dist/elements/TableOfContents.d.ts.map +1 -1
  112. package/dist/elements/TableOfContents.js.map +1 -1
  113. package/dist/elements/TableOfContentsElement.js.map +1 -1
  114. package/dist/elements/TableRow.d.ts.map +1 -1
  115. package/dist/elements/TableRow.js +65 -47
  116. package/dist/elements/TableRow.js.map +1 -1
  117. package/dist/elements/TextBox.d.ts.map +1 -1
  118. package/dist/elements/TextBox.js +1 -1
  119. package/dist/elements/TextBox.js.map +1 -1
  120. package/dist/formatting/AbstractNumbering.d.ts +1 -1
  121. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  122. package/dist/formatting/AbstractNumbering.js +11 -11
  123. package/dist/formatting/AbstractNumbering.js.map +1 -1
  124. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  125. package/dist/formatting/NumberingInstance.js +4 -4
  126. package/dist/formatting/NumberingInstance.js.map +1 -1
  127. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  128. package/dist/formatting/NumberingLevel.js +26 -26
  129. package/dist/formatting/NumberingLevel.js.map +1 -1
  130. package/dist/formatting/NumberingManager.d.ts +1 -1
  131. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  132. package/dist/formatting/NumberingManager.js.map +1 -1
  133. package/dist/formatting/Style.d.ts.map +1 -1
  134. package/dist/formatting/Style.js +87 -95
  135. package/dist/formatting/Style.js.map +1 -1
  136. package/dist/formatting/StylesManager.d.ts +3 -3
  137. package/dist/formatting/StylesManager.d.ts.map +1 -1
  138. package/dist/formatting/StylesManager.js.map +1 -1
  139. package/dist/helpers/CleanupHelper.js.map +1 -1
  140. package/dist/images/ImageOptimizer.js.map +1 -1
  141. package/dist/index.js.map +1 -1
  142. package/dist/managers/DrawingManager.d.ts.map +1 -1
  143. package/dist/managers/DrawingManager.js.map +1 -1
  144. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  145. package/dist/tracking/TrackingContext.js.map +1 -1
  146. package/dist/types/compatibility-types.js.map +1 -1
  147. package/dist/types/formatting.js.map +1 -1
  148. package/dist/types/list-types.d.ts +4 -4
  149. package/dist/types/list-types.d.ts.map +1 -1
  150. package/dist/types/list-types.js.map +1 -1
  151. package/dist/types/settings-types.js.map +1 -1
  152. package/dist/types/styleConfig.js.map +1 -1
  153. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  154. package/dist/utils/ChangelogGenerator.js.map +1 -1
  155. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  156. package/dist/utils/CompatibilityUpgrader.js +7 -7
  157. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  158. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  159. package/dist/utils/InMemoryRevisionAcceptor.js +23 -2
  160. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  161. package/dist/utils/MoveOperationHelper.js.map +1 -1
  162. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  163. package/dist/utils/RevisionWalker.js.map +1 -1
  164. package/dist/utils/SelectiveRevisionAcceptor.d.ts +1 -0
  165. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  166. package/dist/utils/SelectiveRevisionAcceptor.js +46 -0
  167. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  168. package/dist/utils/ShadingResolver.js +1 -1
  169. package/dist/utils/ShadingResolver.js.map +1 -1
  170. package/dist/utils/acceptRevisions.d.ts +0 -28
  171. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  172. package/dist/utils/acceptRevisions.js +5 -7
  173. package/dist/utils/acceptRevisions.js.map +1 -1
  174. package/dist/utils/cnfStyleDecoder.js +1 -1
  175. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  176. package/dist/utils/corruptionDetection.js.map +1 -1
  177. package/dist/utils/dateFormatting.js.map +1 -1
  178. package/dist/utils/deepClone.d.ts +0 -1
  179. package/dist/utils/deepClone.d.ts.map +1 -1
  180. package/dist/utils/deepClone.js +0 -7
  181. package/dist/utils/deepClone.js.map +1 -1
  182. package/dist/utils/diagnostics.d.ts +2 -2
  183. package/dist/utils/diagnostics.d.ts.map +1 -1
  184. package/dist/utils/diagnostics.js.map +1 -1
  185. package/dist/utils/errorHandling.js.map +1 -1
  186. package/dist/utils/formatting.js.map +1 -1
  187. package/dist/utils/list-detection.d.ts +2 -2
  188. package/dist/utils/list-detection.d.ts.map +1 -1
  189. package/dist/utils/list-detection.js +3 -3
  190. package/dist/utils/list-detection.js.map +1 -1
  191. package/dist/utils/logger.d.ts +2 -4
  192. package/dist/utils/logger.d.ts.map +1 -1
  193. package/dist/utils/logger.js +0 -2
  194. package/dist/utils/logger.js.map +1 -1
  195. package/dist/utils/parsingHelpers.js.map +1 -1
  196. package/dist/utils/stripTrackedChanges.d.ts +0 -19
  197. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  198. package/dist/utils/stripTrackedChanges.js +0 -2
  199. package/dist/utils/stripTrackedChanges.js.map +1 -1
  200. package/dist/utils/textDiff.js.map +1 -1
  201. package/dist/utils/units.js.map +1 -1
  202. package/dist/utils/validation.d.ts.map +1 -1
  203. package/dist/utils/validation.js.map +1 -1
  204. package/dist/utils/xmlSanitization.js.map +1 -1
  205. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  206. package/dist/validation/RevisionValidator.js.map +1 -1
  207. package/dist/validation/ValidationRules.js.map +1 -1
  208. package/dist/validation/index.js.map +1 -1
  209. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  210. package/dist/xml/XMLBuilder.js +10 -0
  211. package/dist/xml/XMLBuilder.js.map +1 -1
  212. package/dist/xml/XMLParser.d.ts.map +1 -1
  213. package/dist/xml/XMLParser.js +4 -5
  214. package/dist/xml/XMLParser.js.map +1 -1
  215. package/dist/zip/ZipHandler.js.map +1 -1
  216. package/dist/zip/ZipReader.js.map +1 -1
  217. package/dist/zip/ZipWriter.js.map +1 -1
  218. package/dist/zip/errors.js.map +1 -1
  219. package/dist/zip/types.js.map +1 -1
  220. package/package.json +34 -4
  221. package/src/__tests__/helper-methods.test.ts +512 -0
  222. package/src/constants/legacyCompatFlags.ts +138 -0
  223. package/src/constants/limits.ts +50 -0
  224. package/src/core/CLAUDE.md +109 -0
  225. package/src/core/Document.ts +15569 -0
  226. package/src/core/DocumentContent.ts +467 -0
  227. package/src/core/DocumentGenerator.ts +1104 -0
  228. package/src/core/DocumentIdManager.ts +158 -0
  229. package/src/core/DocumentParser.ts +10140 -0
  230. package/src/core/DocumentValidator.ts +372 -0
  231. package/src/core/Relationship.ts +367 -0
  232. package/src/core/RelationshipManager.ts +428 -0
  233. package/src/elements/AlternateContent.ts +42 -0
  234. package/src/elements/Bookmark.ts +210 -0
  235. package/src/elements/BookmarkManager.ts +250 -0
  236. package/src/elements/CLAUDE.md +126 -0
  237. package/src/elements/Comment.ts +359 -0
  238. package/src/elements/CommentManager.ts +502 -0
  239. package/src/elements/CommonTypes.ts +549 -0
  240. package/src/elements/CustomXml.ts +36 -0
  241. package/src/elements/Endnote.ts +217 -0
  242. package/src/elements/EndnoteManager.ts +249 -0
  243. package/src/elements/Field.ts +1233 -0
  244. package/src/elements/FieldHelpers.ts +333 -0
  245. package/src/elements/FontManager.ts +339 -0
  246. package/src/elements/Footer.ts +269 -0
  247. package/src/elements/Footnote.ts +217 -0
  248. package/src/elements/FootnoteManager.ts +249 -0
  249. package/src/elements/Header.ts +269 -0
  250. package/src/elements/HeaderFooterManager.ts +219 -0
  251. package/src/elements/Hyperlink.ts +1146 -0
  252. package/src/elements/Image.ts +1756 -0
  253. package/src/elements/ImageManager.ts +432 -0
  254. package/src/elements/ImageRun.ts +59 -0
  255. package/src/elements/MathElement.ts +65 -0
  256. package/src/elements/Paragraph.ts +4287 -0
  257. package/src/elements/PreservedElement.ts +53 -0
  258. package/src/elements/PropertyChangeTypes.ts +442 -0
  259. package/src/elements/RangeMarker.ts +400 -0
  260. package/src/elements/Revision.ts +1217 -0
  261. package/src/elements/RevisionContent.ts +73 -0
  262. package/src/elements/RevisionManager.ts +1070 -0
  263. package/src/elements/Run.ts +3068 -0
  264. package/src/elements/Section.ts +1421 -0
  265. package/src/elements/Shape.ts +873 -0
  266. package/src/elements/StructuredDocumentTag.ts +978 -0
  267. package/src/elements/Table.ts +2524 -0
  268. package/src/elements/TableCell.ts +1586 -0
  269. package/src/elements/TableGridChange.ts +151 -0
  270. package/src/elements/TableOfContents.ts +691 -0
  271. package/src/elements/TableOfContentsElement.ts +89 -0
  272. package/src/elements/TableRow.ts +906 -0
  273. package/src/elements/TextBox.ts +768 -0
  274. package/src/formatting/AbstractNumbering.ts +548 -0
  275. package/src/formatting/CLAUDE.md +74 -0
  276. package/src/formatting/NumberingInstance.ts +212 -0
  277. package/src/formatting/NumberingLevel.ts +1006 -0
  278. package/src/formatting/NumberingManager.ts +827 -0
  279. package/src/formatting/Style.ts +1833 -0
  280. package/src/formatting/StylesManager.ts +1005 -0
  281. package/src/helpers/CleanupHelper.ts +524 -0
  282. package/src/images/ImageOptimizer.ts +274 -0
  283. package/src/index.ts +554 -0
  284. package/src/managers/CLAUDE.md +47 -0
  285. package/src/managers/DrawingManager.ts +319 -0
  286. package/src/tracking/DocumentTrackingContext.ts +643 -0
  287. package/src/tracking/TrackingContext.ts +173 -0
  288. package/src/types/compatibility-types.ts +49 -0
  289. package/src/types/formatting.ts +210 -0
  290. package/src/types/list-types.ts +152 -0
  291. package/src/types/settings-types.ts +59 -0
  292. package/src/types/styleConfig.ts +189 -0
  293. package/src/utils/CLAUDE.md +153 -0
  294. package/src/utils/ChangelogGenerator.ts +1581 -0
  295. package/src/utils/CompatibilityUpgrader.ts +237 -0
  296. package/src/utils/InMemoryRevisionAcceptor.ts +696 -0
  297. package/src/utils/MoveOperationHelper.ts +238 -0
  298. package/src/utils/RevisionAwareProcessor.ts +526 -0
  299. package/src/utils/RevisionWalker.ts +457 -0
  300. package/src/utils/SelectiveRevisionAcceptor.ts +683 -0
  301. package/src/utils/ShadingResolver.ts +107 -0
  302. package/src/utils/acceptRevisions.ts +714 -0
  303. package/src/utils/cnfStyleDecoder.ts +217 -0
  304. package/src/utils/corruptionDetection.ts +345 -0
  305. package/src/utils/dateFormatting.ts +20 -0
  306. package/src/utils/deepClone.ts +78 -0
  307. package/src/utils/diagnostics.ts +129 -0
  308. package/src/utils/errorHandling.ts +80 -0
  309. package/src/utils/formatting.ts +213 -0
  310. package/src/utils/list-detection.ts +274 -0
  311. package/src/utils/logger.ts +404 -0
  312. package/src/utils/parsingHelpers.ts +190 -0
  313. package/src/utils/stripTrackedChanges.ts +353 -0
  314. package/src/utils/textDiff.ts +100 -0
  315. package/src/utils/units.ts +421 -0
  316. package/src/utils/validation.ts +542 -0
  317. package/src/utils/xmlSanitization.ts +182 -0
  318. package/src/validation/RevisionAutoFixer.ts +542 -0
  319. package/src/validation/RevisionValidator.ts +460 -0
  320. package/src/validation/ValidationRules.ts +338 -0
  321. package/src/validation/index.ts +30 -0
  322. package/src/xml/CLAUDE.md +65 -0
  323. package/src/xml/XMLBuilder.ts +871 -0
  324. package/src/xml/XMLParser.ts +919 -0
  325. package/src/zip/CLAUDE.md +55 -0
  326. package/src/zip/ZipHandler.ts +637 -0
  327. package/src/zip/ZipReader.ts +299 -0
  328. package/src/zip/ZipWriter.ts +390 -0
  329. package/src/zip/errors.ts +69 -0
  330. package/src/zip/types.ts +116 -0
  331. package/dist/core/ListNormalizer.d.ts +0 -23
  332. package/dist/core/ListNormalizer.d.ts.map +0 -1
  333. package/dist/core/ListNormalizer.js +0 -624
  334. package/dist/core/ListNormalizer.js.map +0 -1
  335. package/dist/images/index.d.ts +0 -2
  336. package/dist/images/index.d.ts.map +0 -1
  337. package/dist/images/index.js +0 -8
  338. package/dist/images/index.js.map +0 -1
  339. package/dist/ms-doc/cfb/CFBReader.d.ts +0 -35
  340. package/dist/ms-doc/cfb/CFBReader.d.ts.map +0 -1
  341. package/dist/ms-doc/cfb/CFBReader.js +0 -360
  342. package/dist/ms-doc/cfb/CFBReader.js.map +0 -1
  343. package/dist/ms-doc/converter/DocToDocxConverter.d.ts +0 -55
  344. package/dist/ms-doc/converter/DocToDocxConverter.d.ts.map +0 -1
  345. package/dist/ms-doc/converter/DocToDocxConverter.js +0 -324
  346. package/dist/ms-doc/converter/DocToDocxConverter.js.map +0 -1
  347. package/dist/ms-doc/fib/FIB.d.ts +0 -18
  348. package/dist/ms-doc/fib/FIB.d.ts.map +0 -1
  349. package/dist/ms-doc/fib/FIB.js +0 -342
  350. package/dist/ms-doc/fib/FIB.js.map +0 -1
  351. package/dist/ms-doc/fields/FieldParser.d.ts +0 -31
  352. package/dist/ms-doc/fields/FieldParser.d.ts.map +0 -1
  353. package/dist/ms-doc/fields/FieldParser.js +0 -266
  354. package/dist/ms-doc/fields/FieldParser.js.map +0 -1
  355. package/dist/ms-doc/images/PictureExtractor.d.ts +0 -22
  356. package/dist/ms-doc/images/PictureExtractor.d.ts.map +0 -1
  357. package/dist/ms-doc/images/PictureExtractor.js +0 -233
  358. package/dist/ms-doc/images/PictureExtractor.js.map +0 -1
  359. package/dist/ms-doc/index.d.ts +0 -20
  360. package/dist/ms-doc/index.d.ts.map +0 -1
  361. package/dist/ms-doc/index.js +0 -59
  362. package/dist/ms-doc/index.js.map +0 -1
  363. package/dist/ms-doc/properties/SPRM.d.ts +0 -210
  364. package/dist/ms-doc/properties/SPRM.d.ts.map +0 -1
  365. package/dist/ms-doc/properties/SPRM.js +0 -633
  366. package/dist/ms-doc/properties/SPRM.js.map +0 -1
  367. package/dist/ms-doc/sections/SectionParser.d.ts +0 -25
  368. package/dist/ms-doc/sections/SectionParser.d.ts.map +0 -1
  369. package/dist/ms-doc/sections/SectionParser.js +0 -214
  370. package/dist/ms-doc/sections/SectionParser.js.map +0 -1
  371. package/dist/ms-doc/styles/StyleSheet.d.ts +0 -23
  372. package/dist/ms-doc/styles/StyleSheet.d.ts.map +0 -1
  373. package/dist/ms-doc/styles/StyleSheet.js +0 -268
  374. package/dist/ms-doc/styles/StyleSheet.js.map +0 -1
  375. package/dist/ms-doc/subdocuments/SubdocumentParser.d.ts +0 -61
  376. package/dist/ms-doc/subdocuments/SubdocumentParser.d.ts.map +0 -1
  377. package/dist/ms-doc/subdocuments/SubdocumentParser.js +0 -208
  378. package/dist/ms-doc/subdocuments/SubdocumentParser.js.map +0 -1
  379. package/dist/ms-doc/tables/TableParser.d.ts +0 -29
  380. package/dist/ms-doc/tables/TableParser.d.ts.map +0 -1
  381. package/dist/ms-doc/tables/TableParser.js +0 -176
  382. package/dist/ms-doc/tables/TableParser.js.map +0 -1
  383. package/dist/ms-doc/text/PieceTable.d.ts +0 -21
  384. package/dist/ms-doc/text/PieceTable.d.ts.map +0 -1
  385. package/dist/ms-doc/text/PieceTable.js +0 -171
  386. package/dist/ms-doc/text/PieceTable.js.map +0 -1
  387. package/dist/ms-doc/types/Constants.d.ts +0 -99
  388. package/dist/ms-doc/types/Constants.d.ts.map +0 -1
  389. package/dist/ms-doc/types/Constants.js +0 -102
  390. package/dist/ms-doc/types/Constants.js.map +0 -1
  391. package/dist/ms-doc/types/DocTypes.d.ts +0 -368
  392. package/dist/ms-doc/types/DocTypes.d.ts.map +0 -1
  393. package/dist/ms-doc/types/DocTypes.js +0 -3
  394. package/dist/ms-doc/types/DocTypes.js.map +0 -1
  395. package/dist/tracking/index.d.ts +0 -3
  396. package/dist/tracking/index.d.ts.map +0 -1
  397. package/dist/tracking/index.js +0 -6
  398. package/dist/tracking/index.js.map +0 -1
@@ -0,0 +1,1581 @@
1
+ /**
2
+ * ChangelogGenerator - Generates structured changelog from Word tracked changes
3
+ *
4
+ * Converts Word revisions (w:ins, w:del, property changes) into structured
5
+ * changelog data with support for consolidation, categorization, and
6
+ * multiple output formats.
7
+ *
8
+ * Follows ECMA-376 revision semantics.
9
+ *
10
+ * @module ChangelogGenerator
11
+ */
12
+
13
+ import type { Document } from '../core/Document';
14
+ import { Revision, RevisionType } from '../elements/Revision';
15
+ import { getGlobalLogger, createScopedLogger, ILogger } from './logger';
16
+
17
+ // Scoped logger for ChangelogGenerator
18
+ function getLogger(): ILogger {
19
+ return createScopedLogger(getGlobalLogger(), 'ChangelogGenerator');
20
+ }
21
+
22
+ /**
23
+ * Semantic category for grouping changes.
24
+ */
25
+ export type ChangeCategory =
26
+ | 'content' // Text insertions, deletions
27
+ | 'formatting' // Run/paragraph property changes
28
+ | 'structural' // Moves, section changes
29
+ | 'table' // Table structure changes
30
+ | 'hyperlink' // Hyperlink URL, text, or formatting changes
31
+ | 'image' // Image insertion, deletion, or property changes
32
+ | 'field' // Field insertion, deletion, or value changes
33
+ | 'comment' // Comment changes
34
+ | 'bookmark' // Bookmark changes
35
+ | 'contentControl'; // Content control (SDT) changes
36
+
37
+ /**
38
+ * Location of a change within the document.
39
+ */
40
+ export interface ChangeLocation {
41
+ /** Section index (0-based) */
42
+ sectionIndex?: number;
43
+ /** Paragraph index within body (0-based) */
44
+ paragraphIndex: number;
45
+ /** Run index within paragraph (0-based) */
46
+ runIndex?: number;
47
+ /** Nearest heading for context */
48
+ nearestHeading?: string;
49
+ /** Character offset within paragraph */
50
+ characterOffset?: number;
51
+ }
52
+
53
+ /**
54
+ * Represents a single change entry in the changelog.
55
+ * Follows ECMA-376 revision semantics.
56
+ */
57
+ export interface ChangeEntry {
58
+ /** Unique identifier (matches revision ID) */
59
+ id: string;
60
+
61
+ /** ECMA-376 revision type */
62
+ revisionType: RevisionType;
63
+
64
+ /** Semantic category for grouping */
65
+ category: ChangeCategory;
66
+
67
+ /** Human-readable description */
68
+ description: string;
69
+
70
+ /** Author who made the change */
71
+ author: string;
72
+
73
+ /** ISO 8601 timestamp */
74
+ date: Date;
75
+
76
+ /** Location in document */
77
+ location: ChangeLocation;
78
+
79
+ /** Content details */
80
+ content: {
81
+ /** Text before change (for deletions/modifications) */
82
+ before?: string;
83
+ /** Text after change (for insertions/modifications) */
84
+ after?: string;
85
+ /** Affected text (for property changes) */
86
+ affectedText?: string;
87
+ };
88
+
89
+ /** Property change details (for formatting changes) */
90
+ propertyChange?: {
91
+ property: string;
92
+ oldValue?: string;
93
+ newValue?: string;
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Output format for changelog generation.
99
+ */
100
+ export type ChangelogFormat = 'json' | 'markdown' | 'text' | 'html' | 'csv';
101
+
102
+ /**
103
+ * Options for changelog generation.
104
+ */
105
+ export interface ChangelogOptions {
106
+ /** Include formatting/property changes (default: true) */
107
+ includeFormattingChanges?: boolean;
108
+ /** Consolidate similar changes (default: false) */
109
+ consolidate?: boolean;
110
+ /** Maximum context length for descriptions (default: 50) */
111
+ maxContextLength?: number;
112
+ /** Filter by authors */
113
+ filterAuthors?: string[];
114
+ /** Filter by date range */
115
+ filterDateRange?: { start: Date; end: Date };
116
+ /** Filter by categories */
117
+ filterCategories?: ChangeCategory[];
118
+ /** Output format (default: 'markdown') */
119
+ format?: ChangelogFormat;
120
+ /** Sort changes by field */
121
+ sortBy?: 'date' | 'author' | 'type' | 'category';
122
+ /** Sort order (default: 'asc') */
123
+ sortOrder?: 'asc' | 'desc';
124
+ /** Group changes by element type */
125
+ groupByElement?: boolean;
126
+ /** Include document context (nearest heading, section) */
127
+ includeDocumentContext?: boolean;
128
+ }
129
+
130
+ /**
131
+ * Consolidated change grouping similar changes together.
132
+ */
133
+ export interface ConsolidatedChange {
134
+ /** Description of the consolidated change */
135
+ description: string;
136
+ /** Number of individual changes */
137
+ count: number;
138
+ /** Category of changes */
139
+ category: ChangeCategory;
140
+ /** Common attributes shared by all changes */
141
+ commonAttributes: {
142
+ author?: string;
143
+ revisionType?: RevisionType;
144
+ propertyChanged?: string;
145
+ newValue?: string;
146
+ };
147
+ /** Individual change IDs */
148
+ changeIds: string[];
149
+ }
150
+
151
+ /**
152
+ * Summary statistics for changelog entries.
153
+ */
154
+ export interface ChangelogSummary {
155
+ /** Total number of changes */
156
+ total: number;
157
+ /** Breakdown by category */
158
+ byCategory: Record<ChangeCategory, number>;
159
+ /** Breakdown by revision type */
160
+ byType: Record<string, number>;
161
+ /** Breakdown by author */
162
+ byAuthor: Record<string, number>;
163
+ /** Date range of changes */
164
+ dateRange: { earliest: Date; latest: Date } | null;
165
+ }
166
+
167
+ /**
168
+ * Generates changelog from Word tracked changes.
169
+ * Follows ECMA-376 revision semantics.
170
+ */
171
+ export class ChangelogGenerator {
172
+ /**
173
+ * Generate changelog entries from a document.
174
+ * Document must be loaded with { revisionHandling: 'preserve' }.
175
+ *
176
+ * @param doc - Document to extract revisions from
177
+ * @param options - Changelog generation options
178
+ * @returns Array of changelog entries
179
+ */
180
+ static fromDocument(doc: Document, options?: ChangelogOptions): ChangeEntry[] {
181
+ const revisionManager = doc.getRevisionManager();
182
+ if (!revisionManager) {
183
+ return [];
184
+ }
185
+
186
+ const revisions = revisionManager.getAllRevisions();
187
+ return this.fromRevisions(revisions, options, doc);
188
+ }
189
+
190
+ /**
191
+ * Generate changelog entries from specific revisions.
192
+ *
193
+ * @param revisions - Array of revisions to convert
194
+ * @param options - Changelog generation options
195
+ * @param doc - Optional document for context (paragraph indices, headings)
196
+ * @returns Array of changelog entries
197
+ */
198
+ static fromRevisions(
199
+ revisions: Revision[],
200
+ options?: ChangelogOptions,
201
+ doc?: Document
202
+ ): ChangeEntry[] {
203
+ const logger = getLogger();
204
+ const opts = {
205
+ includeFormattingChanges: true,
206
+ consolidate: false,
207
+ maxContextLength: 50,
208
+ ...options,
209
+ };
210
+
211
+ logger.debug('Processing revisions', {
212
+ total: revisions.length,
213
+ filters: {
214
+ categories: opts.filterCategories?.length ?? 0,
215
+ authors: opts.filterAuthors?.length ?? 0,
216
+ dateRange: !!opts.filterDateRange
217
+ }
218
+ });
219
+
220
+ const entries: ChangeEntry[] = [];
221
+ let filtered = 0;
222
+
223
+ for (let i = 0; i < revisions.length; i++) {
224
+ const revision = revisions[i];
225
+ if (!revision) continue;
226
+
227
+ const category = this.categorize(revision);
228
+
229
+ // Filter by category
230
+ if (opts.filterCategories && !opts.filterCategories.includes(category)) {
231
+ filtered++;
232
+ continue;
233
+ }
234
+
235
+ // Filter out formatting changes if requested
236
+ if (!opts.includeFormattingChanges && category === 'formatting') {
237
+ filtered++;
238
+ continue;
239
+ }
240
+
241
+ // Filter by author
242
+ if (opts.filterAuthors && !opts.filterAuthors.includes(revision.getAuthor())) {
243
+ filtered++;
244
+ continue;
245
+ }
246
+
247
+ // Filter by date range
248
+ if (opts.filterDateRange) {
249
+ const revDate = revision.getDate();
250
+ if (revDate < opts.filterDateRange.start || revDate > opts.filterDateRange.end) {
251
+ filtered++;
252
+ continue;
253
+ }
254
+ }
255
+
256
+ const entry = this.revisionToEntry(revision, i, opts.maxContextLength);
257
+ entries.push(entry);
258
+ }
259
+
260
+ if (filtered > 0) {
261
+ logger.debug('Revisions filtered', { included: entries.length, filtered });
262
+ }
263
+
264
+ return entries;
265
+ }
266
+
267
+ /**
268
+ * Convert a single revision to a changelog entry.
269
+ *
270
+ * @param revision - Revision to convert
271
+ * @param index - Index for paragraph location (default location)
272
+ * @param maxContextLength - Maximum length for text context
273
+ * @returns Changelog entry
274
+ */
275
+ private static revisionToEntry(
276
+ revision: Revision,
277
+ index: number,
278
+ maxContextLength: number
279
+ ): ChangeEntry {
280
+ const type = revision.getType();
281
+ const category = this.categorize(revision);
282
+ const runs = revision.getRuns();
283
+
284
+ // Extract text content from runs
285
+ const text = runs.map(r => r.getText()).join('');
286
+ const truncatedText = text.length > maxContextLength
287
+ ? text.substring(0, maxContextLength) + '...'
288
+ : text;
289
+
290
+ // Build content object based on revision type
291
+ const content: ChangeEntry['content'] = {};
292
+ if (type === 'insert' || type === 'moveTo') {
293
+ content.after = truncatedText;
294
+ } else if (type === 'delete' || type === 'moveFrom') {
295
+ content.before = truncatedText;
296
+ } else if (this.isPropertyChangeType(type)) {
297
+ content.affectedText = truncatedText;
298
+ }
299
+
300
+ // Handle hyperlink changes specially
301
+ const prevProps = revision.getPreviousProperties();
302
+ const newProps = revision.getNewProperties();
303
+ if (type === 'hyperlinkChange' && (prevProps || newProps)) {
304
+ (content as any).hyperlinkChange = {
305
+ urlBefore: prevProps?.url,
306
+ urlAfter: newProps?.url,
307
+ textBefore: prevProps?.text,
308
+ textAfter: newProps?.text,
309
+ };
310
+ // Set before/after for standard diff view
311
+ if (prevProps?.url !== newProps?.url) {
312
+ content.before = prevProps?.url;
313
+ content.after = newProps?.url;
314
+ } else if (prevProps?.text !== newProps?.text) {
315
+ content.before = prevProps?.text;
316
+ content.after = newProps?.text;
317
+ }
318
+ }
319
+
320
+ // Build property change details if applicable
321
+ let propertyChange: ChangeEntry['propertyChange'] | undefined;
322
+ if (prevProps || newProps) {
323
+ // Get the first property that changed
324
+ const allKeys = new Set([
325
+ ...Object.keys(prevProps || {}),
326
+ ...Object.keys(newProps || {}),
327
+ ]);
328
+ const firstKey = Array.from(allKeys)[0];
329
+ if (firstKey) {
330
+ propertyChange = {
331
+ property: firstKey,
332
+ oldValue: this.formatPropertyValue(prevProps?.[firstKey]),
333
+ newValue: this.formatPropertyValue(newProps?.[firstKey]),
334
+ };
335
+ }
336
+ }
337
+
338
+ // Use revision's location if available, otherwise fall back to index
339
+ const revisionLocation = revision.getLocation();
340
+ const location: ChangeLocation = {
341
+ paragraphIndex: revisionLocation?.paragraphIndex ?? index, // Use actual or fallback
342
+ sectionIndex: revisionLocation?.sectionIndex,
343
+ runIndex: revisionLocation?.runIndex,
344
+ };
345
+
346
+ return {
347
+ id: revision.getId().toString(),
348
+ revisionType: type,
349
+ category,
350
+ description: this.describeRevision(revision, maxContextLength),
351
+ author: revision.getAuthor(),
352
+ date: revision.getDate(),
353
+ location,
354
+ content,
355
+ propertyChange,
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Get summary statistics for changelog entries.
361
+ *
362
+ * @param entries - Array of changelog entries
363
+ * @returns Summary statistics
364
+ */
365
+ static getSummary(entries: ChangeEntry[]): ChangelogSummary {
366
+ const byCategory: Record<ChangeCategory, number> = {
367
+ content: 0,
368
+ formatting: 0,
369
+ structural: 0,
370
+ table: 0,
371
+ hyperlink: 0,
372
+ image: 0,
373
+ field: 0,
374
+ comment: 0,
375
+ bookmark: 0,
376
+ contentControl: 0,
377
+ };
378
+ const byType: Record<string, number> = {};
379
+ const byAuthor: Record<string, number> = {};
380
+ let earliest: Date | null = null;
381
+ let latest: Date | null = null;
382
+
383
+ for (const entry of entries) {
384
+ // Count by category
385
+ byCategory[entry.category]++;
386
+
387
+ // Count by type
388
+ byType[entry.revisionType] = (byType[entry.revisionType] || 0) + 1;
389
+
390
+ // Count by author
391
+ byAuthor[entry.author] = (byAuthor[entry.author] || 0) + 1;
392
+
393
+ // Track date range
394
+ if (!earliest || entry.date < earliest) {
395
+ earliest = entry.date;
396
+ }
397
+ if (!latest || entry.date > latest) {
398
+ latest = entry.date;
399
+ }
400
+ }
401
+
402
+ return {
403
+ total: entries.length,
404
+ byCategory,
405
+ byType,
406
+ byAuthor,
407
+ dateRange: earliest && latest ? { earliest, latest } : null,
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Consolidate similar changes into groups.
413
+ * Groups changes that share: same type, same property, same new value.
414
+ *
415
+ * @param entries - Array of changelog entries
416
+ * @returns Array of consolidated changes
417
+ */
418
+ static consolidate(entries: ChangeEntry[]): ConsolidatedChange[] {
419
+ const groups = new Map<string, ChangeEntry[]>();
420
+
421
+ for (const entry of entries) {
422
+ // Create grouping key
423
+ let key = `${entry.revisionType}_${entry.category}`;
424
+
425
+ // For property changes, include the property name and new value
426
+ if (entry.propertyChange) {
427
+ key += `_${entry.propertyChange.property}_${entry.propertyChange.newValue || ''}`;
428
+ }
429
+
430
+ // For content changes by same author, group them
431
+ key += `_${entry.author}`;
432
+
433
+ if (!groups.has(key)) {
434
+ groups.set(key, []);
435
+ }
436
+ groups.get(key)!.push(entry);
437
+ }
438
+
439
+ const consolidated: ConsolidatedChange[] = [];
440
+
441
+ for (const [_, groupEntries] of groups) {
442
+ const first = groupEntries[0];
443
+ if (!first) continue;
444
+
445
+ let description: string;
446
+
447
+ if (groupEntries.length === 1) {
448
+ description = first.description;
449
+ } else {
450
+ // Generate consolidated description
451
+ description = this.generateConsolidatedDescription(groupEntries);
452
+ }
453
+
454
+ consolidated.push({
455
+ description,
456
+ count: groupEntries.length,
457
+ category: first.category,
458
+ commonAttributes: {
459
+ author: this.allSame(groupEntries.map(e => e.author)) ? first.author : undefined,
460
+ revisionType: first.revisionType,
461
+ propertyChanged: first.propertyChange?.property,
462
+ newValue: first.propertyChange?.newValue,
463
+ },
464
+ changeIds: groupEntries.map(e => e.id),
465
+ });
466
+ }
467
+
468
+ // Sort by count descending
469
+ consolidated.sort((a, b) => b.count - a.count);
470
+
471
+ if (entries.length > 0) {
472
+ getLogger().info('Entries consolidated', {
473
+ input: entries.length,
474
+ groups: consolidated.length,
475
+ reduction: `${Math.round((1 - consolidated.length / entries.length) * 100)}%`
476
+ });
477
+ }
478
+
479
+ return consolidated;
480
+ }
481
+
482
+ /**
483
+ * Generate a consolidated description for a group of similar changes.
484
+ */
485
+ private static generateConsolidatedDescription(entries: ChangeEntry[]): string {
486
+ const first = entries[0];
487
+ if (!first) {
488
+ return `${entries.length} changes`;
489
+ }
490
+
491
+ const count = entries.length;
492
+
493
+ switch (first.revisionType) {
494
+ case 'insert':
495
+ return `Inserted text in ${count} locations`;
496
+ case 'delete':
497
+ return `Deleted text from ${count} locations`;
498
+ case 'moveFrom':
499
+ case 'moveTo':
500
+ return `Moved content (${count} operations)`;
501
+ case 'runPropertiesChange':
502
+ if (first.propertyChange) {
503
+ return `Changed ${first.propertyChange.property} to "${first.propertyChange.newValue}" (${count} times)`;
504
+ }
505
+ return `Changed run formatting (${count} times)`;
506
+ case 'paragraphPropertiesChange':
507
+ if (first.propertyChange) {
508
+ return `Changed paragraph ${first.propertyChange.property} (${count} times)`;
509
+ }
510
+ return `Changed paragraph formatting (${count} times)`;
511
+ case 'tablePropertiesChange':
512
+ case 'tableRowPropertiesChange':
513
+ case 'tableCellPropertiesChange':
514
+ return `Changed table formatting (${count} times)`;
515
+ case 'tableCellInsert':
516
+ return `Inserted ${count} table cells`;
517
+ case 'tableCellDelete':
518
+ return `Deleted ${count} table cells`;
519
+ case 'tableCellMerge':
520
+ return `Merged table cells (${count} operations)`;
521
+ case 'numberingChange':
522
+ return `Changed list numbering (${count} times)`;
523
+ case 'sectionPropertiesChange':
524
+ return `Changed section properties (${count} times)`;
525
+ default:
526
+ return `${count} changes of type ${first.revisionType}`;
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Check if all values in an array are the same.
532
+ */
533
+ private static allSame<T>(arr: T[]): boolean {
534
+ if (arr.length === 0) return true;
535
+ const first = arr[0];
536
+ return arr.every(v => v === first);
537
+ }
538
+
539
+ /**
540
+ * Categorize a revision into a semantic category.
541
+ *
542
+ * @param revision - Revision to categorize
543
+ * @returns Semantic category
544
+ */
545
+ static categorize(revision: Revision): ChangeCategory {
546
+ const type = revision.getType();
547
+
548
+ switch (type) {
549
+ // Content changes
550
+ case 'insert':
551
+ case 'delete':
552
+ return 'content';
553
+
554
+ // Structural changes
555
+ case 'moveFrom':
556
+ case 'moveTo':
557
+ case 'sectionPropertiesChange':
558
+ return 'structural';
559
+
560
+ // Formatting changes
561
+ case 'runPropertiesChange':
562
+ case 'paragraphPropertiesChange':
563
+ case 'numberingChange':
564
+ return 'formatting';
565
+
566
+ // Table changes
567
+ case 'tablePropertiesChange':
568
+ case 'tableExceptionPropertiesChange':
569
+ case 'tableRowPropertiesChange':
570
+ case 'tableCellPropertiesChange':
571
+ case 'tableCellInsert':
572
+ case 'tableCellDelete':
573
+ case 'tableCellMerge':
574
+ return 'table';
575
+
576
+ // Hyperlink changes
577
+ case 'hyperlinkChange':
578
+ return 'hyperlink';
579
+
580
+ // Rich content changes
581
+ case 'imageChange':
582
+ return 'image';
583
+
584
+ case 'fieldChange':
585
+ return 'field';
586
+
587
+ case 'commentChange':
588
+ return 'comment';
589
+
590
+ case 'bookmarkChange':
591
+ return 'bookmark';
592
+
593
+ case 'contentControlChange':
594
+ return 'contentControl';
595
+
596
+ default:
597
+ return 'content';
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Check if a revision type is a property change type.
603
+ */
604
+ private static isPropertyChangeType(type: RevisionType): boolean {
605
+ return [
606
+ 'runPropertiesChange',
607
+ 'paragraphPropertiesChange',
608
+ 'tablePropertiesChange',
609
+ 'tableExceptionPropertiesChange',
610
+ 'tableRowPropertiesChange',
611
+ 'tableCellPropertiesChange',
612
+ 'sectionPropertiesChange',
613
+ 'numberingChange',
614
+ ].includes(type);
615
+ }
616
+
617
+ /**
618
+ * Generate human-readable description for a revision.
619
+ *
620
+ * @param revision - Revision to describe
621
+ * @param maxLength - Maximum length for text excerpts
622
+ * @returns Human-readable description
623
+ */
624
+ static describeRevision(revision: Revision, maxLength = 50): string {
625
+ const type = revision.getType();
626
+ const author = revision.getAuthor();
627
+ const runs = revision.getRuns();
628
+ const text = runs.map(r => r.getText()).join('');
629
+ const excerpt = text.length > maxLength
630
+ ? `"${text.substring(0, maxLength)}..."`
631
+ : text ? `"${text}"` : '';
632
+
633
+ switch (type) {
634
+ case 'insert':
635
+ return excerpt ? `Inserted ${excerpt}` : 'Inserted content';
636
+ case 'delete':
637
+ return excerpt ? `Deleted ${excerpt}` : 'Deleted content';
638
+ case 'moveFrom':
639
+ return excerpt ? `Moved ${excerpt} from here` : 'Moved content from here';
640
+ case 'moveTo':
641
+ return excerpt ? `Moved ${excerpt} to here` : 'Moved content to here';
642
+ case 'runPropertiesChange':
643
+ return this.describePropertyChange(revision, 'run formatting');
644
+ case 'paragraphPropertiesChange':
645
+ return this.describePropertyChange(revision, 'paragraph formatting');
646
+ case 'tablePropertiesChange':
647
+ return 'Changed table properties';
648
+ case 'tableExceptionPropertiesChange':
649
+ return 'Changed table exception properties';
650
+ case 'tableRowPropertiesChange':
651
+ return 'Changed table row properties';
652
+ case 'tableCellPropertiesChange':
653
+ return 'Changed table cell properties';
654
+ case 'sectionPropertiesChange':
655
+ return 'Changed section properties';
656
+ case 'tableCellInsert':
657
+ return 'Inserted table cell';
658
+ case 'tableCellDelete':
659
+ return 'Deleted table cell';
660
+ case 'tableCellMerge':
661
+ return 'Merged table cells';
662
+ case 'numberingChange':
663
+ return 'Changed list numbering';
664
+ case 'hyperlinkChange':
665
+ return this.describeHyperlinkChange(revision, maxLength);
666
+ case 'imageChange':
667
+ return this.describeImageChange(revision);
668
+ case 'fieldChange':
669
+ return this.describeFieldChange(revision);
670
+ case 'commentChange':
671
+ return this.describeCommentChange(revision);
672
+ case 'bookmarkChange':
673
+ return this.describeBookmarkChange(revision);
674
+ case 'contentControlChange':
675
+ return this.describeContentControlChange(revision);
676
+ default:
677
+ return `Changed (${type})`;
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Generate description for a hyperlink change revision.
683
+ */
684
+ private static describeHyperlinkChange(revision: Revision, maxLength: number): string {
685
+ const prevProps = revision.getPreviousProperties() || {};
686
+ const newProps = revision.getNewProperties() || {};
687
+ const changes: string[] = [];
688
+
689
+ if (prevProps.url !== newProps.url) {
690
+ changes.push('URL');
691
+ }
692
+ if (prevProps.text !== newProps.text) {
693
+ changes.push('display text');
694
+ }
695
+ if (prevProps.formatting !== newProps.formatting) {
696
+ changes.push('formatting');
697
+ }
698
+
699
+ if (changes.length === 0) {
700
+ return 'Updated hyperlink';
701
+ }
702
+
703
+ return `Changed hyperlink ${changes.join(' and ')}`;
704
+ }
705
+
706
+ /**
707
+ * Generate description for an image change revision.
708
+ */
709
+ private static describeImageChange(revision: Revision): string {
710
+ const prevProps = revision.getPreviousProperties() || {};
711
+ const newProps = revision.getNewProperties() || {};
712
+ const changes: string[] = [];
713
+
714
+ // Detect type of change
715
+ if (!prevProps.imageId && newProps.imageId) {
716
+ return `Inserted image${newProps.filename ? ` "${newProps.filename}"` : ''}`;
717
+ }
718
+ if (prevProps.imageId && !newProps.imageId) {
719
+ return `Deleted image${prevProps.filename ? ` "${prevProps.filename}"` : ''}`;
720
+ }
721
+
722
+ // Property changes
723
+ if (prevProps.width !== newProps.width || prevProps.height !== newProps.height) {
724
+ changes.push('size');
725
+ }
726
+ if (prevProps.position !== newProps.position) {
727
+ changes.push('position');
728
+ }
729
+ if (prevProps.wrapping !== newProps.wrapping) {
730
+ changes.push('wrapping');
731
+ }
732
+ if (prevProps.altText !== newProps.altText) {
733
+ changes.push('alt text');
734
+ }
735
+
736
+ if (changes.length === 0) {
737
+ return 'Updated image';
738
+ }
739
+
740
+ return `Changed image ${changes.join(' and ')}`;
741
+ }
742
+
743
+ /**
744
+ * Generate description for a field change revision.
745
+ */
746
+ private static describeFieldChange(revision: Revision): string {
747
+ const prevProps = revision.getPreviousProperties() || {};
748
+ const newProps = revision.getNewProperties() || {};
749
+
750
+ // Detect type of change
751
+ if (!prevProps.fieldType && newProps.fieldType) {
752
+ return `Inserted ${newProps.fieldType || 'field'}`;
753
+ }
754
+ if (prevProps.fieldType && !newProps.fieldType) {
755
+ return `Deleted ${prevProps.fieldType || 'field'}`;
756
+ }
757
+
758
+ // Value/formula changes
759
+ if (prevProps.value !== newProps.value) {
760
+ return `Updated ${newProps.fieldType || 'field'} value`;
761
+ }
762
+ if (prevProps.formula !== newProps.formula) {
763
+ return `Changed ${newProps.fieldType || 'field'} formula`;
764
+ }
765
+
766
+ return `Updated ${newProps.fieldType || 'field'}`;
767
+ }
768
+
769
+ /**
770
+ * Generate description for a comment change revision.
771
+ */
772
+ private static describeCommentChange(revision: Revision): string {
773
+ const prevProps = revision.getPreviousProperties() || {};
774
+ const newProps = revision.getNewProperties() || {};
775
+
776
+ // Detect type of change
777
+ if (!prevProps.commentId && newProps.commentId) {
778
+ return `Added comment${newProps.author ? ` by ${newProps.author}` : ''}`;
779
+ }
780
+ if (prevProps.commentId && !newProps.commentId) {
781
+ return `Deleted comment${prevProps.author ? ` by ${prevProps.author}` : ''}`;
782
+ }
783
+ if (prevProps.text !== newProps.text) {
784
+ return `Edited comment${newProps.author ? ` by ${newProps.author}` : ''}`;
785
+ }
786
+
787
+ return 'Updated comment';
788
+ }
789
+
790
+ /**
791
+ * Generate description for a bookmark change revision.
792
+ */
793
+ private static describeBookmarkChange(revision: Revision): string {
794
+ const prevProps = revision.getPreviousProperties() || {};
795
+ const newProps = revision.getNewProperties() || {};
796
+
797
+ // Detect type of change
798
+ if (!prevProps.bookmarkId && newProps.bookmarkId) {
799
+ return `Created bookmark "${newProps.name || 'unnamed'}"`;
800
+ }
801
+ if (prevProps.bookmarkId && !newProps.bookmarkId) {
802
+ return `Deleted bookmark "${prevProps.name || 'unnamed'}"`;
803
+ }
804
+ if (prevProps.name !== newProps.name) {
805
+ return `Renamed bookmark from "${prevProps.name}" to "${newProps.name}"`;
806
+ }
807
+ if (prevProps.rangeStart !== newProps.rangeStart || prevProps.rangeEnd !== newProps.rangeEnd) {
808
+ return `Changed bookmark "${newProps.name || 'unnamed'}" range`;
809
+ }
810
+
811
+ return `Updated bookmark "${newProps.name || prevProps.name || 'unnamed'}"`;
812
+ }
813
+
814
+ /**
815
+ * Generate description for a content control change revision.
816
+ */
817
+ private static describeContentControlChange(revision: Revision): string {
818
+ const prevProps = revision.getPreviousProperties() || {};
819
+ const newProps = revision.getNewProperties() || {};
820
+
821
+ // Detect type of change
822
+ if (!prevProps.sdtId && newProps.sdtId) {
823
+ return `Inserted content control${newProps.title ? ` "${newProps.title}"` : ''}`;
824
+ }
825
+ if (prevProps.sdtId && !newProps.sdtId) {
826
+ return `Deleted content control${prevProps.title ? ` "${prevProps.title}"` : ''}`;
827
+ }
828
+ if (prevProps.title !== newProps.title) {
829
+ return `Renamed content control to "${newProps.title}"`;
830
+ }
831
+ if (prevProps.content !== newProps.content) {
832
+ return `Changed content control${newProps.title ? ` "${newProps.title}"` : ''} content`;
833
+ }
834
+
835
+ return `Updated content control${newProps.title || prevProps.title ? ` "${newProps.title || prevProps.title}"` : ''}`;
836
+ }
837
+
838
+ /**
839
+ * Generate description for a property change revision.
840
+ */
841
+ private static describePropertyChange(revision: Revision, context: string): string {
842
+ const prevProps = revision.getPreviousProperties();
843
+ const newProps = revision.getNewProperties();
844
+
845
+ if (!prevProps && !newProps) {
846
+ return `Changed ${context}`;
847
+ }
848
+
849
+ // Get meaningful property names
850
+ const propNames: string[] = [];
851
+ const allKeys = new Set([
852
+ ...Object.keys(prevProps || {}),
853
+ ...Object.keys(newProps || {}),
854
+ ]);
855
+
856
+ for (const key of allKeys) {
857
+ const oldVal = prevProps?.[key];
858
+ const newVal = newProps?.[key];
859
+ if (oldVal !== newVal) {
860
+ propNames.push(this.friendlyPropertyName(key));
861
+ }
862
+ }
863
+
864
+ if (propNames.length === 0) {
865
+ return `Changed ${context}`;
866
+ }
867
+
868
+ if (propNames.length === 1) {
869
+ return `Changed ${propNames[0]}`;
870
+ }
871
+
872
+ if (propNames.length <= 3) {
873
+ return `Changed ${propNames.join(', ')}`;
874
+ }
875
+
876
+ return `Changed ${propNames.slice(0, 2).join(', ')} and ${propNames.length - 2} more`;
877
+ }
878
+
879
+ /**
880
+ * Format a property value for display.
881
+ * Handles objects, arrays, and primitives properly.
882
+ */
883
+ private static formatPropertyValue(value: unknown): string | undefined {
884
+ if (value === null || value === undefined) {
885
+ return undefined;
886
+ }
887
+ if (typeof value === 'object') {
888
+ try {
889
+ // For objects, use JSON.stringify for proper representation
890
+ const json = JSON.stringify(value);
891
+ // Truncate if too long
892
+ return json.length > 100 ? json.substring(0, 97) + '...' : json;
893
+ } catch (e) {
894
+ getLogger().debug('Failed to stringify value', { error: String(e) });
895
+ return '[complex value]';
896
+ }
897
+ }
898
+ // For primitives, use String() for safe conversion
899
+ return String(value);
900
+ }
901
+
902
+ /**
903
+ * Convert property key to friendly name.
904
+ */
905
+ private static friendlyPropertyName(key: string): string {
906
+ const friendlyNames: Record<string, string> = {
907
+ // Run (character) properties
908
+ b: 'bold',
909
+ i: 'italic',
910
+ u: 'underline',
911
+ strike: 'strikethrough',
912
+ dstrike: 'double strikethrough',
913
+ sz: 'font size',
914
+ szCs: 'complex script font size',
915
+ color: 'text color',
916
+ highlight: 'highlight',
917
+ rFonts: 'font',
918
+ rStyle: 'character style',
919
+ vertAlign: 'vertical alignment',
920
+ vanish: 'hidden text',
921
+ caps: 'all capitals',
922
+ smallCaps: 'small capitals',
923
+ outline: 'outline effect',
924
+ shadow: 'shadow effect',
925
+ emboss: 'emboss effect',
926
+ imprint: 'imprint effect',
927
+ kern: 'kerning',
928
+ w: 'character width',
929
+ spacing: 'character spacing',
930
+ position: 'text position',
931
+ shd: 'shading',
932
+ bdr: 'border',
933
+ lang: 'language',
934
+ eastAsianLayout: 'East Asian layout',
935
+ specVanish: 'special vanish',
936
+ oMath: 'math mode',
937
+
938
+ // Paragraph properties
939
+ jc: 'alignment',
940
+ ind: 'indentation',
941
+ pStyle: 'paragraph style',
942
+ numPr: 'list numbering',
943
+ pBdr: 'paragraph border',
944
+ tabs: 'tab stops',
945
+ suppressAutoHyphens: 'hyphenation',
946
+ kinsoku: 'kinsoku rules',
947
+ wordWrap: 'word wrap',
948
+ overflowPunct: 'overflow punctuation',
949
+ topLinePunct: 'top line punctuation',
950
+ autoSpaceDE: 'auto space (DE)',
951
+ autoSpaceDN: 'auto space (DN)',
952
+ bidi: 'bidirectional',
953
+ adjustRightInd: 'right indent adjustment',
954
+ snapToGrid: 'snap to grid',
955
+ contextualSpacing: 'contextual spacing',
956
+ mirrorIndents: 'mirror indents',
957
+ suppressOverlap: 'suppress overlap',
958
+ outlineLvl: 'outline level',
959
+ divId: 'HTML division',
960
+ keepNext: 'keep with next',
961
+ keepLines: 'keep lines together',
962
+ pageBreakBefore: 'page break before',
963
+ widowControl: 'widow/orphan control',
964
+ suppressLineNumbers: 'suppress line numbers',
965
+ textboxTightWrap: 'textbox tight wrap',
966
+
967
+ // Table properties
968
+ tblStyle: 'table style',
969
+ tblW: 'table width',
970
+ tblInd: 'table indent',
971
+ tblBorders: 'table borders',
972
+ tblCellMar: 'table cell margins',
973
+ tblLook: 'table look',
974
+ tblLayout: 'table layout',
975
+ gridSpan: 'column span',
976
+ vMerge: 'vertical merge',
977
+ tcW: 'cell width',
978
+ tcBorders: 'cell borders',
979
+ vAlign: 'vertical alignment',
980
+ textDirection: 'text direction',
981
+ noWrap: 'no wrap',
982
+ tcMar: 'cell margins',
983
+ tcFitText: 'fit text',
984
+ hideMark: 'hide mark',
985
+
986
+ // Section properties
987
+ sectPr: 'section properties',
988
+ pgSz: 'page size',
989
+ pgMar: 'page margins',
990
+ cols: 'columns',
991
+ docGrid: 'document grid',
992
+ headerReference: 'header',
993
+ footerReference: 'footer',
994
+ pgNumType: 'page numbering',
995
+ formProt: 'form protection',
996
+ titlePg: 'different first page',
997
+ type: 'section type',
998
+ };
999
+
1000
+ return friendlyNames[key] || key;
1001
+ }
1002
+
1003
+ /**
1004
+ * Export changelog to Markdown format.
1005
+ *
1006
+ * @param entries - Array of changelog entries
1007
+ * @param options - Export options
1008
+ * @returns Markdown string
1009
+ */
1010
+ static toMarkdown(
1011
+ entries: ChangeEntry[],
1012
+ options?: { includeMetadata?: boolean }
1013
+ ): string {
1014
+ const opts = { includeMetadata: true, ...options };
1015
+ const lines: string[] = [];
1016
+
1017
+ lines.push('# Document Changes');
1018
+ lines.push('');
1019
+
1020
+ if (opts.includeMetadata) {
1021
+ const summary = this.getSummary(entries);
1022
+ lines.push(`**Total Changes:** ${summary.total}`);
1023
+ lines.push('');
1024
+
1025
+ if (summary.dateRange) {
1026
+ lines.push(`**Date Range:** ${summary.dateRange.earliest.toLocaleDateString()} - ${summary.dateRange.latest.toLocaleDateString()}`);
1027
+ lines.push('');
1028
+ }
1029
+
1030
+ const authors = Object.keys(summary.byAuthor);
1031
+ if (authors.length > 0) {
1032
+ lines.push(`**Authors:** ${authors.join(', ')}`);
1033
+ lines.push('');
1034
+ }
1035
+
1036
+ lines.push('---');
1037
+ lines.push('');
1038
+ }
1039
+
1040
+ // Group by category
1041
+ const byCategory = new Map<ChangeCategory, ChangeEntry[]>();
1042
+ for (const entry of entries) {
1043
+ if (!byCategory.has(entry.category)) {
1044
+ byCategory.set(entry.category, []);
1045
+ }
1046
+ byCategory.get(entry.category)!.push(entry);
1047
+ }
1048
+
1049
+ const categoryTitles: Record<ChangeCategory, string> = {
1050
+ content: 'Content Changes',
1051
+ formatting: 'Formatting Changes',
1052
+ structural: 'Structural Changes',
1053
+ table: 'Table Changes',
1054
+ hyperlink: 'Hyperlink Changes',
1055
+ image: 'Image Changes',
1056
+ field: 'Field Changes',
1057
+ comment: 'Comment Changes',
1058
+ bookmark: 'Bookmark Changes',
1059
+ contentControl: 'Content Control Changes',
1060
+ };
1061
+
1062
+ for (const [category, categoryEntries] of byCategory) {
1063
+ if (categoryEntries.length === 0) continue;
1064
+
1065
+ lines.push(`## ${categoryTitles[category]}`);
1066
+ lines.push('');
1067
+
1068
+ for (const entry of categoryEntries) {
1069
+ const date = entry.date.toLocaleDateString();
1070
+ lines.push(`- ${entry.description} *(${entry.author}, ${date})*`);
1071
+
1072
+ if (entry.content.before) {
1073
+ lines.push(` - Removed: "${entry.content.before}"`);
1074
+ }
1075
+ if (entry.content.after) {
1076
+ lines.push(` - Added: "${entry.content.after}"`);
1077
+ }
1078
+ }
1079
+
1080
+ lines.push('');
1081
+ }
1082
+
1083
+ return lines.join('\n');
1084
+ }
1085
+
1086
+ /**
1087
+ * Export changelog to plain text format.
1088
+ *
1089
+ * @param entries - Array of changelog entries
1090
+ * @returns Plain text string
1091
+ */
1092
+ static toPlainText(entries: ChangeEntry[]): string {
1093
+ const lines: string[] = [];
1094
+
1095
+ lines.push('DOCUMENT CHANGES');
1096
+ lines.push('================');
1097
+ lines.push('');
1098
+
1099
+ for (const entry of entries) {
1100
+ const date = entry.date.toLocaleDateString();
1101
+ lines.push(`[${date}] ${entry.author}: ${entry.description}`);
1102
+
1103
+ if (entry.content.before) {
1104
+ lines.push(` - ${entry.content.before}`);
1105
+ }
1106
+ if (entry.content.after) {
1107
+ lines.push(` + ${entry.content.after}`);
1108
+ }
1109
+ }
1110
+
1111
+ return lines.join('\n');
1112
+ }
1113
+
1114
+ /**
1115
+ * Export changelog to JSON (for programmatic consumption).
1116
+ *
1117
+ * @param entries - Array of changelog entries
1118
+ * @returns JSON string
1119
+ */
1120
+ static toJSON(entries: ChangeEntry[]): string {
1121
+ return JSON.stringify({
1122
+ generated: new Date().toISOString(),
1123
+ summary: this.getSummary(entries),
1124
+ entries,
1125
+ }, null, 2);
1126
+ }
1127
+
1128
+ // ============================================================
1129
+ // Unified API (Phase 2 Enhancement)
1130
+ // ============================================================
1131
+
1132
+ /**
1133
+ * Unified changelog generation - single entry point for all formats.
1134
+ *
1135
+ * Generates changelog from a document in the specified format.
1136
+ * This is the recommended method for most use cases.
1137
+ *
1138
+ * @param doc - Document to extract revisions from
1139
+ * @param options - Generation options including format
1140
+ * @returns Formatted changelog string
1141
+ *
1142
+ * @example
1143
+ * ```typescript
1144
+ * // Generate Markdown changelog
1145
+ * const md = ChangelogGenerator.generate(doc, { format: 'markdown' });
1146
+ *
1147
+ * // Generate HTML changelog
1148
+ * const html = ChangelogGenerator.generate(doc, { format: 'html' });
1149
+ *
1150
+ * // Generate CSV with filtering
1151
+ * const csv = ChangelogGenerator.generate(doc, {
1152
+ * format: 'csv',
1153
+ * filterAuthors: ['John Doe'],
1154
+ * sortBy: 'date',
1155
+ * sortOrder: 'desc'
1156
+ * });
1157
+ * ```
1158
+ */
1159
+ static generate(doc: Document, options?: ChangelogOptions): string {
1160
+ const logger = getLogger();
1161
+ const format = options?.format || 'markdown';
1162
+
1163
+ logger.info('Generating changelog', { format });
1164
+
1165
+ // Get entries with filtering
1166
+ let entries = this.fromDocument(doc, options);
1167
+
1168
+ // Apply sorting if specified
1169
+ if (options?.sortBy) {
1170
+ entries = this.sortEntries(entries, options.sortBy, options.sortOrder || 'asc');
1171
+ }
1172
+
1173
+ logger.info('Changelog entries processed', {
1174
+ entries: entries.length,
1175
+ format,
1176
+ sorted: !!options?.sortBy
1177
+ });
1178
+
1179
+ // Generate output in specified format
1180
+ switch (format) {
1181
+ case 'json':
1182
+ return this.toJSON(entries);
1183
+ case 'markdown':
1184
+ return this.toMarkdown(entries, { includeMetadata: true });
1185
+ case 'text':
1186
+ return this.toPlainText(entries);
1187
+ case 'html':
1188
+ return this.toHTML(entries, options);
1189
+ case 'csv':
1190
+ return this.toCSV(entries);
1191
+ default:
1192
+ return this.toMarkdown(entries);
1193
+ }
1194
+ }
1195
+
1196
+ /**
1197
+ * Export changelog to HTML format.
1198
+ *
1199
+ * Generates a complete HTML document with styling and structure.
1200
+ *
1201
+ * @param entries - Array of changelog entries
1202
+ * @param options - HTML generation options
1203
+ * @returns Complete HTML document string
1204
+ *
1205
+ * @example
1206
+ * ```typescript
1207
+ * const html = ChangelogGenerator.toHTML(entries);
1208
+ * fs.writeFileSync('changelog.html', html);
1209
+ * ```
1210
+ */
1211
+ static toHTML(entries: ChangeEntry[], options?: ChangelogOptions): string {
1212
+ const summary = this.getSummary(entries);
1213
+
1214
+ // Group entries by category
1215
+ const byCategory = new Map<ChangeCategory, ChangeEntry[]>();
1216
+ for (const entry of entries) {
1217
+ if (!byCategory.has(entry.category)) {
1218
+ byCategory.set(entry.category, []);
1219
+ }
1220
+ byCategory.get(entry.category)!.push(entry);
1221
+ }
1222
+
1223
+ const categoryTitles: Record<ChangeCategory, string> = {
1224
+ content: 'Content Changes',
1225
+ formatting: 'Formatting Changes',
1226
+ structural: 'Structural Changes',
1227
+ table: 'Table Changes',
1228
+ hyperlink: 'Hyperlink Changes',
1229
+ image: 'Image Changes',
1230
+ field: 'Field Changes',
1231
+ comment: 'Comment Changes',
1232
+ bookmark: 'Bookmark Changes',
1233
+ contentControl: 'Content Control Changes',
1234
+ };
1235
+
1236
+ // Build HTML
1237
+ let html = `<!DOCTYPE html>
1238
+ <html lang="en">
1239
+ <head>
1240
+ <meta charset="UTF-8">
1241
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1242
+ <title>Document Changes</title>
1243
+ <style>
1244
+ body {
1245
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1246
+ max-width: 900px;
1247
+ margin: 0 auto;
1248
+ padding: 20px;
1249
+ line-height: 1.6;
1250
+ color: #333;
1251
+ }
1252
+ h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
1253
+ h2 { color: #34495e; margin-top: 30px; }
1254
+ .summary { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
1255
+ .summary p { margin: 5px 0; }
1256
+ .category { margin-bottom: 30px; }
1257
+ .change-list { list-style: none; padding: 0; }
1258
+ .change-item {
1259
+ background: #fff;
1260
+ border: 1px solid #e9ecef;
1261
+ border-radius: 6px;
1262
+ padding: 12px 15px;
1263
+ margin-bottom: 10px;
1264
+ }
1265
+ .change-item:hover { box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
1266
+ .change-description { font-weight: 500; color: #2c3e50; }
1267
+ .change-meta { font-size: 0.85em; color: #7f8c8d; margin-top: 5px; }
1268
+ .change-content { margin-top: 8px; font-size: 0.9em; }
1269
+ .removed { color: #c0392b; text-decoration: line-through; }
1270
+ .added { color: #27ae60; }
1271
+ .badge {
1272
+ display: inline-block;
1273
+ padding: 2px 8px;
1274
+ border-radius: 4px;
1275
+ font-size: 0.75em;
1276
+ font-weight: 600;
1277
+ text-transform: uppercase;
1278
+ }
1279
+ .badge-insert { background: #d4edda; color: #155724; }
1280
+ .badge-delete { background: #f8d7da; color: #721c24; }
1281
+ .badge-formatting { background: #cce5ff; color: #004085; }
1282
+ .badge-structural { background: #fff3cd; color: #856404; }
1283
+ .badge-table { background: #e2e3e5; color: #383d41; }
1284
+ </style>
1285
+ </head>
1286
+ <body>
1287
+ <h1>Document Changes</h1>
1288
+
1289
+ <div class="summary">
1290
+ <p><strong>Total Changes:</strong> ${summary.total}</p>`;
1291
+
1292
+ if (summary.dateRange) {
1293
+ html += `
1294
+ <p><strong>Date Range:</strong> ${summary.dateRange.earliest.toLocaleDateString()} - ${summary.dateRange.latest.toLocaleDateString()}</p>`;
1295
+ }
1296
+
1297
+ const authors = Object.keys(summary.byAuthor);
1298
+ if (authors.length > 0) {
1299
+ html += `
1300
+ <p><strong>Authors:</strong> ${authors.join(', ')}</p>`;
1301
+ }
1302
+
1303
+ html += `
1304
+ </div>
1305
+ `;
1306
+
1307
+ // Add sections by category
1308
+ for (const [category, categoryEntries] of byCategory) {
1309
+ if (categoryEntries.length === 0) continue;
1310
+
1311
+ html += `
1312
+ <section class="category">
1313
+ <h2>${categoryTitles[category]}</h2>
1314
+ <ul class="change-list">`;
1315
+
1316
+ for (const entry of categoryEntries) {
1317
+ const badgeClass = this.getBadgeClass(entry.revisionType);
1318
+ const date = entry.date.toLocaleDateString();
1319
+
1320
+ html += `
1321
+ <li class="change-item">
1322
+ <span class="badge ${badgeClass}">${entry.revisionType}</span>
1323
+ <div class="change-description">${this.escapeHTML(entry.description)}</div>
1324
+ <div class="change-meta">${this.escapeHTML(entry.author)} - ${date}</div>`;
1325
+
1326
+ if (entry.content.before || entry.content.after) {
1327
+ html += `
1328
+ <div class="change-content">`;
1329
+ if (entry.content.before) {
1330
+ html += `<span class="removed">${this.escapeHTML(entry.content.before || '')}</span> `;
1331
+ }
1332
+ if (entry.content.after) {
1333
+ html += `<span class="added">${this.escapeHTML(entry.content.after || '')}</span>`;
1334
+ }
1335
+ html += `</div>`;
1336
+ }
1337
+
1338
+ html += `
1339
+ </li>`;
1340
+ }
1341
+
1342
+ html += `
1343
+ </ul>
1344
+ </section>`;
1345
+ }
1346
+
1347
+ html += `
1348
+ </body>
1349
+ </html>`;
1350
+
1351
+ return html;
1352
+ }
1353
+
1354
+ /**
1355
+ * Export changelog to CSV format.
1356
+ *
1357
+ * Generates CSV data suitable for spreadsheet applications.
1358
+ *
1359
+ * @param entries - Array of changelog entries
1360
+ * @param options - CSV generation options
1361
+ * @returns CSV string
1362
+ *
1363
+ * @example
1364
+ * ```typescript
1365
+ * const csv = ChangelogGenerator.toCSV(entries);
1366
+ * fs.writeFileSync('changelog.csv', csv);
1367
+ * ```
1368
+ */
1369
+ static toCSV(entries: ChangeEntry[], options?: {
1370
+ delimiter?: string;
1371
+ includeHeaders?: boolean;
1372
+ }): string {
1373
+ const delimiter = options?.delimiter || ',';
1374
+ const includeHeaders = options?.includeHeaders !== false;
1375
+
1376
+ const headers = [
1377
+ 'ID',
1378
+ 'Type',
1379
+ 'Category',
1380
+ 'Author',
1381
+ 'Date',
1382
+ 'Description',
1383
+ 'Before',
1384
+ 'After',
1385
+ 'Paragraph',
1386
+ 'Run'
1387
+ ];
1388
+
1389
+ const lines: string[] = [];
1390
+
1391
+ if (includeHeaders) {
1392
+ lines.push(headers.join(delimiter));
1393
+ }
1394
+
1395
+ for (const entry of entries) {
1396
+ const row = [
1397
+ entry.id,
1398
+ entry.revisionType,
1399
+ entry.category,
1400
+ this.escapeCSV(entry.author),
1401
+ entry.date.toISOString(),
1402
+ this.escapeCSV(entry.description),
1403
+ this.escapeCSV(entry.content.before || ''),
1404
+ this.escapeCSV(entry.content.after || ''),
1405
+ entry.location.paragraphIndex?.toString() || '',
1406
+ entry.location.runIndex?.toString() || '',
1407
+ ];
1408
+ lines.push(row.join(delimiter));
1409
+ }
1410
+
1411
+ return lines.join('\n');
1412
+ }
1413
+
1414
+ /**
1415
+ * Get timeline view - changes organized by date.
1416
+ *
1417
+ * Groups changelog entries by date (YYYY-MM-DD) for timeline visualization.
1418
+ *
1419
+ * @param entries - Array of changelog entries
1420
+ * @returns Map of date strings to entries for that date
1421
+ *
1422
+ * @example
1423
+ * ```typescript
1424
+ * const timeline = ChangelogGenerator.getTimeline(entries);
1425
+ * for (const [date, dayEntries] of timeline) {
1426
+ * console.log(`${date}: ${dayEntries.length} changes`);
1427
+ * }
1428
+ * ```
1429
+ */
1430
+ static getTimeline(entries: ChangeEntry[]): Map<string, ChangeEntry[]> {
1431
+ const timeline = new Map<string, ChangeEntry[]>();
1432
+
1433
+ for (const entry of entries) {
1434
+ const dateKey = entry.date.toISOString().split('T')[0]!; // YYYY-MM-DD
1435
+ if (!timeline.has(dateKey)) {
1436
+ timeline.set(dateKey, []);
1437
+ }
1438
+ timeline.get(dateKey)!.push(entry);
1439
+ }
1440
+
1441
+ // Sort the map by date
1442
+ const sortedTimeline = new Map<string, ChangeEntry[]>(
1443
+ [...timeline.entries()].sort((a, b) => a[0].localeCompare(b[0]))
1444
+ );
1445
+
1446
+ return sortedTimeline;
1447
+ }
1448
+
1449
+ /**
1450
+ * Get summary by element type.
1451
+ *
1452
+ * Groups entries by the type of document element they affect.
1453
+ *
1454
+ * @param entries - Array of changelog entries
1455
+ * @returns Object with entries grouped by element type
1456
+ */
1457
+ static getSummaryByElement(entries: ChangeEntry[]): {
1458
+ paragraphs: ChangeEntry[];
1459
+ tables: ChangeEntry[];
1460
+ sections: ChangeEntry[];
1461
+ runs: ChangeEntry[];
1462
+ hyperlinks: ChangeEntry[];
1463
+ } {
1464
+ return {
1465
+ paragraphs: entries.filter(e =>
1466
+ e.revisionType === 'paragraphPropertiesChange' ||
1467
+ e.revisionType === 'numberingChange' ||
1468
+ (e.revisionType === 'insert' && !e.location.runIndex) ||
1469
+ (e.revisionType === 'delete' && !e.location.runIndex)
1470
+ ),
1471
+ tables: entries.filter(e =>
1472
+ ['tablePropertiesChange', 'tableRowPropertiesChange',
1473
+ 'tableCellPropertiesChange', 'tableExceptionPropertiesChange',
1474
+ 'tableCellInsert', 'tableCellDelete', 'tableCellMerge'].includes(e.revisionType)
1475
+ ),
1476
+ sections: entries.filter(e =>
1477
+ e.revisionType === 'sectionPropertiesChange'
1478
+ ),
1479
+ runs: entries.filter(e =>
1480
+ e.revisionType === 'runPropertiesChange' ||
1481
+ (e.revisionType === 'insert' && e.location.runIndex !== undefined) ||
1482
+ (e.revisionType === 'delete' && e.location.runIndex !== undefined)
1483
+ ),
1484
+ hyperlinks: entries.filter(e =>
1485
+ e.revisionType === 'hyperlinkChange'
1486
+ ),
1487
+ };
1488
+ }
1489
+
1490
+ // ============================================================
1491
+ // Helper Methods
1492
+ // ============================================================
1493
+
1494
+ /**
1495
+ * Sort entries by specified field.
1496
+ * @internal
1497
+ */
1498
+ private static sortEntries(
1499
+ entries: ChangeEntry[],
1500
+ sortBy: 'date' | 'author' | 'type' | 'category',
1501
+ order: 'asc' | 'desc'
1502
+ ): ChangeEntry[] {
1503
+ const sorted = [...entries].sort((a, b) => {
1504
+ let comparison = 0;
1505
+
1506
+ switch (sortBy) {
1507
+ case 'date':
1508
+ comparison = a.date.getTime() - b.date.getTime();
1509
+ break;
1510
+ case 'author':
1511
+ comparison = a.author.localeCompare(b.author);
1512
+ break;
1513
+ case 'type':
1514
+ comparison = a.revisionType.localeCompare(b.revisionType);
1515
+ break;
1516
+ case 'category':
1517
+ comparison = a.category.localeCompare(b.category);
1518
+ break;
1519
+ }
1520
+
1521
+ return order === 'desc' ? -comparison : comparison;
1522
+ });
1523
+
1524
+ return sorted;
1525
+ }
1526
+
1527
+ /**
1528
+ * Get CSS badge class for revision type.
1529
+ * @internal
1530
+ */
1531
+ private static getBadgeClass(type: RevisionType): string {
1532
+ switch (type) {
1533
+ case 'insert':
1534
+ case 'moveTo':
1535
+ case 'tableCellInsert':
1536
+ return 'badge-insert';
1537
+ case 'delete':
1538
+ case 'moveFrom':
1539
+ case 'tableCellDelete':
1540
+ return 'badge-delete';
1541
+ case 'runPropertiesChange':
1542
+ case 'paragraphPropertiesChange':
1543
+ case 'numberingChange':
1544
+ return 'badge-formatting';
1545
+ case 'sectionPropertiesChange':
1546
+ return 'badge-structural';
1547
+ case 'tablePropertiesChange':
1548
+ case 'tableRowPropertiesChange':
1549
+ case 'tableCellPropertiesChange':
1550
+ case 'tableCellMerge':
1551
+ return 'badge-table';
1552
+ default:
1553
+ return 'badge-formatting';
1554
+ }
1555
+ }
1556
+
1557
+ /**
1558
+ * Escape HTML special characters.
1559
+ * @internal
1560
+ */
1561
+ private static escapeHTML(str: string): string {
1562
+ return str
1563
+ .replace(/&/g, '&amp;')
1564
+ .replace(/</g, '&lt;')
1565
+ .replace(/>/g, '&gt;')
1566
+ .replace(/"/g, '&quot;')
1567
+ .replace(/'/g, '&#039;');
1568
+ }
1569
+
1570
+ /**
1571
+ * Escape CSV special characters.
1572
+ * @internal
1573
+ */
1574
+ private static escapeCSV(str: string): string {
1575
+ // If string contains delimiter, newline, or quotes, wrap in quotes
1576
+ if (str.includes(',') || str.includes('\n') || str.includes('"')) {
1577
+ return `"${str.replace(/"/g, '""')}"`;
1578
+ }
1579
+ return str;
1580
+ }
1581
+ }