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.
- package/README.md +3 -2
- package/dist/constants/legacyCompatFlags.d.ts.map +1 -1
- package/dist/constants/legacyCompatFlags.js.map +1 -1
- package/dist/constants/limits.d.ts +0 -27
- package/dist/constants/limits.d.ts.map +1 -1
- package/dist/constants/limits.js +13 -13
- package/dist/constants/limits.js.map +1 -1
- package/dist/core/Document.d.ts +23 -19
- package/dist/core/Document.d.ts.map +1 -1
- package/dist/core/Document.js +197 -63
- package/dist/core/Document.js.map +1 -1
- package/dist/core/DocumentContent.d.ts.map +1 -1
- package/dist/core/DocumentContent.js.map +1 -1
- package/dist/core/DocumentGenerator.d.ts.map +1 -1
- package/dist/core/DocumentGenerator.js +59 -24
- package/dist/core/DocumentGenerator.js.map +1 -1
- package/dist/core/DocumentIdManager.d.ts.map +1 -1
- package/dist/core/DocumentIdManager.js.map +1 -1
- package/dist/core/DocumentParser.d.ts +6 -6
- package/dist/core/DocumentParser.d.ts.map +1 -1
- package/dist/core/DocumentParser.js +86 -55
- package/dist/core/DocumentParser.js.map +1 -1
- package/dist/core/DocumentValidator.d.ts.map +1 -1
- package/dist/core/DocumentValidator.js.map +1 -1
- package/dist/core/Relationship.d.ts.map +1 -1
- package/dist/core/Relationship.js +1 -1
- package/dist/core/Relationship.js.map +1 -1
- package/dist/core/RelationshipManager.js +3 -3
- package/dist/core/RelationshipManager.js.map +1 -1
- package/dist/elements/AlternateContent.js.map +1 -1
- package/dist/elements/Bookmark.d.ts.map +1 -1
- package/dist/elements/Bookmark.js.map +1 -1
- package/dist/elements/BookmarkManager.d.ts.map +1 -1
- package/dist/elements/BookmarkManager.js.map +1 -1
- package/dist/elements/Comment.js +1 -1
- package/dist/elements/Comment.js.map +1 -1
- package/dist/elements/CommentManager.d.ts.map +1 -1
- package/dist/elements/CommentManager.js +8 -2
- package/dist/elements/CommentManager.js.map +1 -1
- package/dist/elements/CommonTypes.d.ts.map +1 -1
- package/dist/elements/CommonTypes.js +1 -2
- package/dist/elements/CommonTypes.js.map +1 -1
- package/dist/elements/CustomXml.js.map +1 -1
- package/dist/elements/Endnote.d.ts.map +1 -1
- package/dist/elements/Endnote.js.map +1 -1
- package/dist/elements/EndnoteManager.d.ts.map +1 -1
- package/dist/elements/EndnoteManager.js.map +1 -1
- package/dist/elements/Field.d.ts.map +1 -1
- package/dist/elements/Field.js +31 -28
- package/dist/elements/Field.js.map +1 -1
- package/dist/elements/FieldHelpers.d.ts.map +1 -1
- package/dist/elements/FieldHelpers.js +6 -6
- package/dist/elements/FieldHelpers.js.map +1 -1
- package/dist/elements/FontManager.d.ts.map +1 -1
- package/dist/elements/FontManager.js.map +1 -1
- package/dist/elements/Footer.js.map +1 -1
- package/dist/elements/Footnote.d.ts.map +1 -1
- package/dist/elements/Footnote.js.map +1 -1
- package/dist/elements/FootnoteManager.d.ts.map +1 -1
- package/dist/elements/FootnoteManager.js.map +1 -1
- package/dist/elements/Header.js.map +1 -1
- package/dist/elements/HeaderFooterManager.js.map +1 -1
- package/dist/elements/Hyperlink.d.ts.map +1 -1
- package/dist/elements/Hyperlink.js +5 -5
- package/dist/elements/Hyperlink.js.map +1 -1
- package/dist/elements/Image.d.ts +2 -2
- package/dist/elements/Image.d.ts.map +1 -1
- package/dist/elements/Image.js +21 -5
- package/dist/elements/Image.js.map +1 -1
- package/dist/elements/ImageManager.d.ts.map +1 -1
- package/dist/elements/ImageManager.js +2 -2
- package/dist/elements/ImageManager.js.map +1 -1
- package/dist/elements/ImageRun.js.map +1 -1
- package/dist/elements/MathElement.js.map +1 -1
- package/dist/elements/Paragraph.d.ts +8 -0
- package/dist/elements/Paragraph.d.ts.map +1 -1
- package/dist/elements/Paragraph.js +153 -118
- package/dist/elements/Paragraph.js.map +1 -1
- package/dist/elements/PreservedElement.js.map +1 -1
- package/dist/elements/PropertyChangeTypes.js.map +1 -1
- package/dist/elements/RangeMarker.js.map +1 -1
- package/dist/elements/Revision.d.ts +1 -0
- package/dist/elements/Revision.d.ts.map +1 -1
- package/dist/elements/Revision.js +44 -5
- package/dist/elements/Revision.js.map +1 -1
- package/dist/elements/RevisionContent.js.map +1 -1
- package/dist/elements/RevisionManager.d.ts.map +1 -1
- package/dist/elements/RevisionManager.js.map +1 -1
- package/dist/elements/Run.d.ts.map +1 -1
- package/dist/elements/Run.js +1 -3
- package/dist/elements/Run.js.map +1 -1
- package/dist/elements/Section.d.ts.map +1 -1
- package/dist/elements/Section.js +127 -118
- package/dist/elements/Section.js.map +1 -1
- package/dist/elements/Shape.d.ts.map +1 -1
- package/dist/elements/Shape.js +21 -0
- package/dist/elements/Shape.js.map +1 -1
- package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
- package/dist/elements/StructuredDocumentTag.js +20 -8
- package/dist/elements/StructuredDocumentTag.js.map +1 -1
- package/dist/elements/Table.d.ts +2 -2
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +29 -35
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableCell.d.ts +2 -2
- package/dist/elements/TableCell.d.ts.map +1 -1
- package/dist/elements/TableCell.js +63 -67
- package/dist/elements/TableCell.js.map +1 -1
- package/dist/elements/TableGridChange.js.map +1 -1
- package/dist/elements/TableOfContents.d.ts +6 -6
- package/dist/elements/TableOfContents.d.ts.map +1 -1
- package/dist/elements/TableOfContents.js.map +1 -1
- package/dist/elements/TableOfContentsElement.js.map +1 -1
- package/dist/elements/TableRow.d.ts.map +1 -1
- package/dist/elements/TableRow.js +65 -47
- package/dist/elements/TableRow.js.map +1 -1
- package/dist/elements/TextBox.d.ts.map +1 -1
- package/dist/elements/TextBox.js +1 -1
- package/dist/elements/TextBox.js.map +1 -1
- package/dist/formatting/AbstractNumbering.d.ts +1 -1
- package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
- package/dist/formatting/AbstractNumbering.js +11 -11
- package/dist/formatting/AbstractNumbering.js.map +1 -1
- package/dist/formatting/NumberingInstance.d.ts.map +1 -1
- package/dist/formatting/NumberingInstance.js +4 -4
- package/dist/formatting/NumberingInstance.js.map +1 -1
- package/dist/formatting/NumberingLevel.d.ts.map +1 -1
- package/dist/formatting/NumberingLevel.js +26 -26
- package/dist/formatting/NumberingLevel.js.map +1 -1
- package/dist/formatting/NumberingManager.d.ts +1 -1
- package/dist/formatting/NumberingManager.d.ts.map +1 -1
- package/dist/formatting/NumberingManager.js.map +1 -1
- package/dist/formatting/Style.d.ts.map +1 -1
- package/dist/formatting/Style.js +87 -95
- package/dist/formatting/Style.js.map +1 -1
- package/dist/formatting/StylesManager.d.ts +3 -3
- package/dist/formatting/StylesManager.d.ts.map +1 -1
- package/dist/formatting/StylesManager.js.map +1 -1
- package/dist/helpers/CleanupHelper.js.map +1 -1
- package/dist/images/ImageOptimizer.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/DrawingManager.d.ts.map +1 -1
- package/dist/managers/DrawingManager.js.map +1 -1
- package/dist/tracking/DocumentTrackingContext.js.map +1 -1
- package/dist/tracking/TrackingContext.js.map +1 -1
- package/dist/types/compatibility-types.js.map +1 -1
- package/dist/types/formatting.js.map +1 -1
- package/dist/types/list-types.d.ts +4 -4
- package/dist/types/list-types.d.ts.map +1 -1
- package/dist/types/list-types.js.map +1 -1
- package/dist/types/settings-types.js.map +1 -1
- package/dist/types/styleConfig.js.map +1 -1
- package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
- package/dist/utils/ChangelogGenerator.js.map +1 -1
- package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
- package/dist/utils/CompatibilityUpgrader.js +7 -7
- package/dist/utils/CompatibilityUpgrader.js.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.js +23 -2
- package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
- package/dist/utils/MoveOperationHelper.js.map +1 -1
- package/dist/utils/RevisionAwareProcessor.js.map +1 -1
- package/dist/utils/RevisionWalker.js.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.d.ts +1 -0
- package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.js +46 -0
- package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/utils/ShadingResolver.js +1 -1
- package/dist/utils/ShadingResolver.js.map +1 -1
- package/dist/utils/acceptRevisions.d.ts +0 -28
- package/dist/utils/acceptRevisions.d.ts.map +1 -1
- package/dist/utils/acceptRevisions.js +5 -7
- package/dist/utils/acceptRevisions.js.map +1 -1
- package/dist/utils/cnfStyleDecoder.js +1 -1
- package/dist/utils/cnfStyleDecoder.js.map +1 -1
- package/dist/utils/corruptionDetection.js.map +1 -1
- package/dist/utils/dateFormatting.js.map +1 -1
- package/dist/utils/deepClone.d.ts +0 -1
- package/dist/utils/deepClone.d.ts.map +1 -1
- package/dist/utils/deepClone.js +0 -7
- package/dist/utils/deepClone.js.map +1 -1
- package/dist/utils/diagnostics.d.ts +2 -2
- package/dist/utils/diagnostics.d.ts.map +1 -1
- package/dist/utils/diagnostics.js.map +1 -1
- package/dist/utils/errorHandling.js.map +1 -1
- package/dist/utils/formatting.js.map +1 -1
- package/dist/utils/list-detection.d.ts +2 -2
- package/dist/utils/list-detection.d.ts.map +1 -1
- package/dist/utils/list-detection.js +3 -3
- package/dist/utils/list-detection.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -4
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +0 -2
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/parsingHelpers.js.map +1 -1
- package/dist/utils/stripTrackedChanges.d.ts +0 -19
- package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
- package/dist/utils/stripTrackedChanges.js +0 -2
- package/dist/utils/stripTrackedChanges.js.map +1 -1
- package/dist/utils/textDiff.js.map +1 -1
- package/dist/utils/units.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/xmlSanitization.js.map +1 -1
- package/dist/validation/RevisionAutoFixer.js.map +1 -1
- package/dist/validation/RevisionValidator.js.map +1 -1
- package/dist/validation/ValidationRules.js.map +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/xml/XMLBuilder.d.ts.map +1 -1
- package/dist/xml/XMLBuilder.js +10 -0
- package/dist/xml/XMLBuilder.js.map +1 -1
- package/dist/xml/XMLParser.d.ts.map +1 -1
- package/dist/xml/XMLParser.js +4 -5
- package/dist/xml/XMLParser.js.map +1 -1
- package/dist/zip/ZipHandler.js.map +1 -1
- package/dist/zip/ZipReader.js.map +1 -1
- package/dist/zip/ZipWriter.js.map +1 -1
- package/dist/zip/errors.js.map +1 -1
- package/dist/zip/types.js.map +1 -1
- package/package.json +34 -4
- package/src/__tests__/helper-methods.test.ts +512 -0
- package/src/constants/legacyCompatFlags.ts +138 -0
- package/src/constants/limits.ts +50 -0
- package/src/core/CLAUDE.md +109 -0
- package/src/core/Document.ts +15569 -0
- package/src/core/DocumentContent.ts +467 -0
- package/src/core/DocumentGenerator.ts +1104 -0
- package/src/core/DocumentIdManager.ts +158 -0
- package/src/core/DocumentParser.ts +10140 -0
- package/src/core/DocumentValidator.ts +372 -0
- package/src/core/Relationship.ts +367 -0
- package/src/core/RelationshipManager.ts +428 -0
- package/src/elements/AlternateContent.ts +42 -0
- package/src/elements/Bookmark.ts +210 -0
- package/src/elements/BookmarkManager.ts +250 -0
- package/src/elements/CLAUDE.md +126 -0
- package/src/elements/Comment.ts +359 -0
- package/src/elements/CommentManager.ts +502 -0
- package/src/elements/CommonTypes.ts +549 -0
- package/src/elements/CustomXml.ts +36 -0
- package/src/elements/Endnote.ts +217 -0
- package/src/elements/EndnoteManager.ts +249 -0
- package/src/elements/Field.ts +1233 -0
- package/src/elements/FieldHelpers.ts +333 -0
- package/src/elements/FontManager.ts +339 -0
- package/src/elements/Footer.ts +269 -0
- package/src/elements/Footnote.ts +217 -0
- package/src/elements/FootnoteManager.ts +249 -0
- package/src/elements/Header.ts +269 -0
- package/src/elements/HeaderFooterManager.ts +219 -0
- package/src/elements/Hyperlink.ts +1146 -0
- package/src/elements/Image.ts +1756 -0
- package/src/elements/ImageManager.ts +432 -0
- package/src/elements/ImageRun.ts +59 -0
- package/src/elements/MathElement.ts +65 -0
- package/src/elements/Paragraph.ts +4287 -0
- package/src/elements/PreservedElement.ts +53 -0
- package/src/elements/PropertyChangeTypes.ts +442 -0
- package/src/elements/RangeMarker.ts +400 -0
- package/src/elements/Revision.ts +1217 -0
- package/src/elements/RevisionContent.ts +73 -0
- package/src/elements/RevisionManager.ts +1070 -0
- package/src/elements/Run.ts +3068 -0
- package/src/elements/Section.ts +1421 -0
- package/src/elements/Shape.ts +873 -0
- package/src/elements/StructuredDocumentTag.ts +978 -0
- package/src/elements/Table.ts +2524 -0
- package/src/elements/TableCell.ts +1586 -0
- package/src/elements/TableGridChange.ts +151 -0
- package/src/elements/TableOfContents.ts +691 -0
- package/src/elements/TableOfContentsElement.ts +89 -0
- package/src/elements/TableRow.ts +906 -0
- package/src/elements/TextBox.ts +768 -0
- package/src/formatting/AbstractNumbering.ts +548 -0
- package/src/formatting/CLAUDE.md +74 -0
- package/src/formatting/NumberingInstance.ts +212 -0
- package/src/formatting/NumberingLevel.ts +1006 -0
- package/src/formatting/NumberingManager.ts +827 -0
- package/src/formatting/Style.ts +1833 -0
- package/src/formatting/StylesManager.ts +1005 -0
- package/src/helpers/CleanupHelper.ts +524 -0
- package/src/images/ImageOptimizer.ts +274 -0
- package/src/index.ts +554 -0
- package/src/managers/CLAUDE.md +47 -0
- package/src/managers/DrawingManager.ts +319 -0
- package/src/tracking/DocumentTrackingContext.ts +643 -0
- package/src/tracking/TrackingContext.ts +173 -0
- package/src/types/compatibility-types.ts +49 -0
- package/src/types/formatting.ts +210 -0
- package/src/types/list-types.ts +152 -0
- package/src/types/settings-types.ts +59 -0
- package/src/types/styleConfig.ts +189 -0
- package/src/utils/CLAUDE.md +153 -0
- package/src/utils/ChangelogGenerator.ts +1581 -0
- package/src/utils/CompatibilityUpgrader.ts +237 -0
- package/src/utils/InMemoryRevisionAcceptor.ts +696 -0
- package/src/utils/MoveOperationHelper.ts +238 -0
- package/src/utils/RevisionAwareProcessor.ts +526 -0
- package/src/utils/RevisionWalker.ts +457 -0
- package/src/utils/SelectiveRevisionAcceptor.ts +683 -0
- package/src/utils/ShadingResolver.ts +107 -0
- package/src/utils/acceptRevisions.ts +714 -0
- package/src/utils/cnfStyleDecoder.ts +217 -0
- package/src/utils/corruptionDetection.ts +345 -0
- package/src/utils/dateFormatting.ts +20 -0
- package/src/utils/deepClone.ts +78 -0
- package/src/utils/diagnostics.ts +129 -0
- package/src/utils/errorHandling.ts +80 -0
- package/src/utils/formatting.ts +213 -0
- package/src/utils/list-detection.ts +274 -0
- package/src/utils/logger.ts +404 -0
- package/src/utils/parsingHelpers.ts +190 -0
- package/src/utils/stripTrackedChanges.ts +353 -0
- package/src/utils/textDiff.ts +100 -0
- package/src/utils/units.ts +421 -0
- package/src/utils/validation.ts +542 -0
- package/src/utils/xmlSanitization.ts +182 -0
- package/src/validation/RevisionAutoFixer.ts +542 -0
- package/src/validation/RevisionValidator.ts +460 -0
- package/src/validation/ValidationRules.ts +338 -0
- package/src/validation/index.ts +30 -0
- package/src/xml/CLAUDE.md +65 -0
- package/src/xml/XMLBuilder.ts +871 -0
- package/src/xml/XMLParser.ts +919 -0
- package/src/zip/CLAUDE.md +55 -0
- package/src/zip/ZipHandler.ts +637 -0
- package/src/zip/ZipReader.ts +299 -0
- package/src/zip/ZipWriter.ts +390 -0
- package/src/zip/errors.ts +69 -0
- package/src/zip/types.ts +116 -0
- package/dist/core/ListNormalizer.d.ts +0 -23
- package/dist/core/ListNormalizer.d.ts.map +0 -1
- package/dist/core/ListNormalizer.js +0 -624
- package/dist/core/ListNormalizer.js.map +0 -1
- package/dist/images/index.d.ts +0 -2
- package/dist/images/index.d.ts.map +0 -1
- package/dist/images/index.js +0 -8
- package/dist/images/index.js.map +0 -1
- package/dist/ms-doc/cfb/CFBReader.d.ts +0 -35
- package/dist/ms-doc/cfb/CFBReader.d.ts.map +0 -1
- package/dist/ms-doc/cfb/CFBReader.js +0 -360
- package/dist/ms-doc/cfb/CFBReader.js.map +0 -1
- package/dist/ms-doc/converter/DocToDocxConverter.d.ts +0 -55
- package/dist/ms-doc/converter/DocToDocxConverter.d.ts.map +0 -1
- package/dist/ms-doc/converter/DocToDocxConverter.js +0 -324
- package/dist/ms-doc/converter/DocToDocxConverter.js.map +0 -1
- package/dist/ms-doc/fib/FIB.d.ts +0 -18
- package/dist/ms-doc/fib/FIB.d.ts.map +0 -1
- package/dist/ms-doc/fib/FIB.js +0 -342
- package/dist/ms-doc/fib/FIB.js.map +0 -1
- package/dist/ms-doc/fields/FieldParser.d.ts +0 -31
- package/dist/ms-doc/fields/FieldParser.d.ts.map +0 -1
- package/dist/ms-doc/fields/FieldParser.js +0 -266
- package/dist/ms-doc/fields/FieldParser.js.map +0 -1
- package/dist/ms-doc/images/PictureExtractor.d.ts +0 -22
- package/dist/ms-doc/images/PictureExtractor.d.ts.map +0 -1
- package/dist/ms-doc/images/PictureExtractor.js +0 -233
- package/dist/ms-doc/images/PictureExtractor.js.map +0 -1
- package/dist/ms-doc/index.d.ts +0 -20
- package/dist/ms-doc/index.d.ts.map +0 -1
- package/dist/ms-doc/index.js +0 -59
- package/dist/ms-doc/index.js.map +0 -1
- package/dist/ms-doc/properties/SPRM.d.ts +0 -210
- package/dist/ms-doc/properties/SPRM.d.ts.map +0 -1
- package/dist/ms-doc/properties/SPRM.js +0 -633
- package/dist/ms-doc/properties/SPRM.js.map +0 -1
- package/dist/ms-doc/sections/SectionParser.d.ts +0 -25
- package/dist/ms-doc/sections/SectionParser.d.ts.map +0 -1
- package/dist/ms-doc/sections/SectionParser.js +0 -214
- package/dist/ms-doc/sections/SectionParser.js.map +0 -1
- package/dist/ms-doc/styles/StyleSheet.d.ts +0 -23
- package/dist/ms-doc/styles/StyleSheet.d.ts.map +0 -1
- package/dist/ms-doc/styles/StyleSheet.js +0 -268
- package/dist/ms-doc/styles/StyleSheet.js.map +0 -1
- package/dist/ms-doc/subdocuments/SubdocumentParser.d.ts +0 -61
- package/dist/ms-doc/subdocuments/SubdocumentParser.d.ts.map +0 -1
- package/dist/ms-doc/subdocuments/SubdocumentParser.js +0 -208
- package/dist/ms-doc/subdocuments/SubdocumentParser.js.map +0 -1
- package/dist/ms-doc/tables/TableParser.d.ts +0 -29
- package/dist/ms-doc/tables/TableParser.d.ts.map +0 -1
- package/dist/ms-doc/tables/TableParser.js +0 -176
- package/dist/ms-doc/tables/TableParser.js.map +0 -1
- package/dist/ms-doc/text/PieceTable.d.ts +0 -21
- package/dist/ms-doc/text/PieceTable.d.ts.map +0 -1
- package/dist/ms-doc/text/PieceTable.js +0 -171
- package/dist/ms-doc/text/PieceTable.js.map +0 -1
- package/dist/ms-doc/types/Constants.d.ts +0 -99
- package/dist/ms-doc/types/Constants.d.ts.map +0 -1
- package/dist/ms-doc/types/Constants.js +0 -102
- package/dist/ms-doc/types/Constants.js.map +0 -1
- package/dist/ms-doc/types/DocTypes.d.ts +0 -368
- package/dist/ms-doc/types/DocTypes.d.ts.map +0 -1
- package/dist/ms-doc/types/DocTypes.js +0 -3
- package/dist/ms-doc/types/DocTypes.js.map +0 -1
- package/dist/tracking/index.d.ts +0 -3
- package/dist/tracking/index.d.ts.map +0 -1
- package/dist/tracking/index.js +0 -6
- 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, '&')
|
|
1564
|
+
.replace(/</g, '<')
|
|
1565
|
+
.replace(/>/g, '>')
|
|
1566
|
+
.replace(/"/g, '"')
|
|
1567
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|