docxmlater 11.0.9 → 12.0.1
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 +51 -13
- package/dist/core/Document.d.ts +28 -2
- package/dist/core/Document.d.ts.map +1 -1
- package/dist/core/Document.js +725 -170
- package/dist/core/Document.js.map +1 -1
- package/dist/core/DocumentGenerator.d.ts +1 -0
- package/dist/core/DocumentGenerator.d.ts.map +1 -1
- package/dist/core/DocumentGenerator.js +77 -25
- package/dist/core/DocumentGenerator.js.map +1 -1
- package/dist/core/DocumentParser.d.ts +7 -3
- package/dist/core/DocumentParser.d.ts.map +1 -1
- package/dist/core/DocumentParser.js +431 -85
- package/dist/core/DocumentParser.js.map +1 -1
- package/dist/core/RelationshipManager.d.ts.map +1 -1
- package/dist/core/RelationshipManager.js +2 -2
- package/dist/core/RelationshipManager.js.map +1 -1
- package/dist/elements/Comment.d.ts +15 -1
- package/dist/elements/Comment.d.ts.map +1 -1
- package/dist/elements/Comment.js +67 -12
- package/dist/elements/Comment.js.map +1 -1
- package/dist/elements/CommentManager.d.ts +4 -0
- package/dist/elements/CommentManager.d.ts.map +1 -1
- package/dist/elements/CommentManager.js +74 -16
- package/dist/elements/CommentManager.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 +10 -1
- package/dist/elements/Field.d.ts.map +1 -1
- package/dist/elements/Field.js +76 -34
- package/dist/elements/Field.js.map +1 -1
- package/dist/elements/FieldHelpers.d.ts +1 -0
- package/dist/elements/FieldHelpers.d.ts.map +1 -1
- package/dist/elements/FieldHelpers.js +8 -4
- package/dist/elements/FieldHelpers.js.map +1 -1
- package/dist/elements/Footer.d.ts +3 -1
- package/dist/elements/Footer.d.ts.map +1 -1
- package/dist/elements/Footer.js +39 -53
- package/dist/elements/Footer.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.d.ts +3 -1
- package/dist/elements/Header.d.ts.map +1 -1
- package/dist/elements/Header.js +39 -53
- package/dist/elements/Header.js.map +1 -1
- package/dist/elements/HeaderFooterManager.d.ts +4 -2
- package/dist/elements/HeaderFooterManager.d.ts.map +1 -1
- package/dist/elements/HeaderFooterManager.js +36 -6
- package/dist/elements/HeaderFooterManager.js.map +1 -1
- package/dist/elements/Hyperlink.d.ts +6 -1
- package/dist/elements/Hyperlink.d.ts.map +1 -1
- package/dist/elements/Hyperlink.js +69 -44
- package/dist/elements/Hyperlink.js.map +1 -1
- package/dist/elements/Image.d.ts.map +1 -1
- package/dist/elements/Image.js +33 -6
- package/dist/elements/Image.js.map +1 -1
- package/dist/elements/ImageManager.d.ts +1 -0
- package/dist/elements/ImageManager.d.ts.map +1 -1
- package/dist/elements/ImageManager.js +11 -0
- package/dist/elements/ImageManager.js.map +1 -1
- package/dist/elements/Paragraph.d.ts +3 -0
- package/dist/elements/Paragraph.d.ts.map +1 -1
- package/dist/elements/Paragraph.js +91 -26
- package/dist/elements/Paragraph.js.map +1 -1
- package/dist/elements/Revision.d.ts +4 -0
- package/dist/elements/Revision.d.ts.map +1 -1
- package/dist/elements/Revision.js +369 -29
- package/dist/elements/Revision.js.map +1 -1
- package/dist/elements/Run.d.ts +1 -0
- package/dist/elements/Run.d.ts.map +1 -1
- package/dist/elements/Run.js +24 -4
- package/dist/elements/Run.js.map +1 -1
- package/dist/elements/Section.d.ts.map +1 -1
- package/dist/elements/Section.js +17 -7
- package/dist/elements/Section.js.map +1 -1
- package/dist/elements/Shape.d.ts.map +1 -1
- package/dist/elements/Shape.js +50 -42
- package/dist/elements/Shape.js.map +1 -1
- package/dist/elements/StructuredDocumentTag.d.ts +9 -1
- package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
- package/dist/elements/StructuredDocumentTag.js +46 -6
- package/dist/elements/StructuredDocumentTag.js.map +1 -1
- package/dist/elements/Table.d.ts +2 -0
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +90 -42
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableCell.d.ts +1 -0
- package/dist/elements/TableCell.d.ts.map +1 -1
- package/dist/elements/TableCell.js +32 -13
- package/dist/elements/TableCell.js.map +1 -1
- package/dist/elements/TableOfContents.d.ts.map +1 -1
- package/dist/elements/TableOfContents.js +5 -7
- package/dist/elements/TableOfContents.js.map +1 -1
- package/dist/elements/TableRow.d.ts +1 -0
- package/dist/elements/TableRow.d.ts.map +1 -1
- package/dist/elements/TableRow.js +16 -1
- package/dist/elements/TableRow.js.map +1 -1
- package/dist/elements/TextBox.d.ts.map +1 -1
- package/dist/elements/TextBox.js +55 -42
- package/dist/elements/TextBox.js.map +1 -1
- package/dist/esm/core/Document.js +726 -171
- package/dist/esm/core/Document.js.map +1 -1
- package/dist/esm/core/DocumentGenerator.js +77 -25
- package/dist/esm/core/DocumentGenerator.js.map +1 -1
- package/dist/esm/core/DocumentParser.js +431 -85
- package/dist/esm/core/DocumentParser.js.map +1 -1
- package/dist/esm/core/RelationshipManager.js +2 -2
- package/dist/esm/core/RelationshipManager.js.map +1 -1
- package/dist/esm/elements/Comment.js +67 -12
- package/dist/esm/elements/Comment.js.map +1 -1
- package/dist/esm/elements/CommentManager.js +74 -16
- package/dist/esm/elements/CommentManager.js.map +1 -1
- package/dist/esm/elements/EndnoteManager.js.map +1 -1
- package/dist/esm/elements/Field.js +77 -35
- package/dist/esm/elements/Field.js.map +1 -1
- package/dist/esm/elements/FieldHelpers.js +7 -4
- package/dist/esm/elements/FieldHelpers.js.map +1 -1
- package/dist/esm/elements/Footer.js +39 -53
- package/dist/esm/elements/Footer.js.map +1 -1
- package/dist/esm/elements/FootnoteManager.js.map +1 -1
- package/dist/esm/elements/Header.js +39 -53
- package/dist/esm/elements/Header.js.map +1 -1
- package/dist/esm/elements/HeaderFooterManager.js +36 -6
- package/dist/esm/elements/HeaderFooterManager.js.map +1 -1
- package/dist/esm/elements/Hyperlink.js +69 -44
- package/dist/esm/elements/Hyperlink.js.map +1 -1
- package/dist/esm/elements/Image.js +33 -6
- package/dist/esm/elements/Image.js.map +1 -1
- package/dist/esm/elements/ImageManager.js +11 -0
- package/dist/esm/elements/ImageManager.js.map +1 -1
- package/dist/esm/elements/Paragraph.js +91 -26
- package/dist/esm/elements/Paragraph.js.map +1 -1
- package/dist/esm/elements/Revision.js +369 -29
- package/dist/esm/elements/Revision.js.map +1 -1
- package/dist/esm/elements/Run.js +24 -4
- package/dist/esm/elements/Run.js.map +1 -1
- package/dist/esm/elements/Section.js +17 -7
- package/dist/esm/elements/Section.js.map +1 -1
- package/dist/esm/elements/Shape.js +50 -42
- package/dist/esm/elements/Shape.js.map +1 -1
- package/dist/esm/elements/StructuredDocumentTag.js +46 -6
- package/dist/esm/elements/StructuredDocumentTag.js.map +1 -1
- package/dist/esm/elements/Table.js +90 -42
- package/dist/esm/elements/Table.js.map +1 -1
- package/dist/esm/elements/TableCell.js +32 -13
- package/dist/esm/elements/TableCell.js.map +1 -1
- package/dist/esm/elements/TableOfContents.js +5 -7
- package/dist/esm/elements/TableOfContents.js.map +1 -1
- package/dist/esm/elements/TableRow.js +16 -1
- package/dist/esm/elements/TableRow.js.map +1 -1
- package/dist/esm/elements/TextBox.js +55 -42
- package/dist/esm/elements/TextBox.js.map +1 -1
- package/dist/esm/formatting/AbstractNumbering.js +1 -1
- package/dist/esm/formatting/AbstractNumbering.js.map +1 -1
- package/dist/esm/formatting/NumberingLevel.js +8 -6
- package/dist/esm/formatting/NumberingLevel.js.map +1 -1
- package/dist/esm/formatting/NumberingManager.js +16 -1
- package/dist/esm/formatting/NumberingManager.js.map +1 -1
- package/dist/esm/formatting/Style.js +14 -0
- package/dist/esm/formatting/Style.js.map +1 -1
- package/dist/esm/formatting/StylesManager.js +14 -2
- package/dist/esm/formatting/StylesManager.js.map +1 -1
- package/dist/esm/helpers/CleanupHelper.js +103 -26
- package/dist/esm/helpers/CleanupHelper.js.map +1 -1
- package/dist/esm/images/ImageOptimizer.js +52 -4
- package/dist/esm/images/ImageOptimizer.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal.js +1 -1
- package/dist/esm/internal.js.map +1 -1
- package/dist/esm/processors/CompatibilityUpgrader.js +7 -1
- package/dist/esm/processors/CompatibilityUpgrader.js.map +1 -1
- package/dist/esm/processors/InMemoryRevisionAcceptor.js +134 -13
- package/dist/esm/processors/InMemoryRevisionAcceptor.js.map +1 -1
- package/dist/esm/processors/RevisionWalker.js +58 -8
- package/dist/esm/processors/RevisionWalker.js.map +1 -1
- package/dist/esm/processors/SelectiveRevisionAcceptor.js +116 -17
- package/dist/esm/processors/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/esm/processors/cnfStyleDecoder.js +7 -7
- package/dist/esm/processors/cnfStyleDecoder.js.map +1 -1
- package/dist/esm/processors/stripTrackedChanges.js +16 -5
- package/dist/esm/processors/stripTrackedChanges.js.map +1 -1
- package/dist/esm/utils/deepClone.js +24 -11
- package/dist/esm/utils/deepClone.js.map +1 -1
- package/dist/esm/utils/units.js +1 -1
- package/dist/esm/utils/units.js.map +1 -1
- package/dist/esm/utils/validation.js +11 -23
- package/dist/esm/utils/validation.js.map +1 -1
- package/dist/esm/utils/xmlSanitization.js +131 -7
- package/dist/esm/utils/xmlSanitization.js.map +1 -1
- package/dist/esm/validation/RevisionAutoFixer.js +69 -55
- package/dist/esm/validation/RevisionAutoFixer.js.map +1 -1
- package/dist/esm/validation/RevisionValidator.js +24 -20
- package/dist/esm/validation/RevisionValidator.js.map +1 -1
- package/dist/esm/xml/XMLBuilder.js +52 -20
- package/dist/esm/xml/XMLBuilder.js.map +1 -1
- package/dist/esm/xml/XMLParser.js +55 -13
- package/dist/esm/xml/XMLParser.js.map +1 -1
- package/dist/esm/zip/ZipHandler.js +37 -13
- package/dist/esm/zip/ZipHandler.js.map +1 -1
- package/dist/esm/zip/ZipReader.js +53 -6
- package/dist/esm/zip/ZipReader.js.map +1 -1
- package/dist/esm/zip/errors.js +7 -0
- package/dist/esm/zip/errors.js.map +1 -1
- package/dist/esm/zip/types.js +4 -0
- package/dist/esm/zip/types.js.map +1 -1
- package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
- package/dist/formatting/AbstractNumbering.js +1 -1
- package/dist/formatting/AbstractNumbering.js.map +1 -1
- package/dist/formatting/NumberingLevel.d.ts +1 -1
- package/dist/formatting/NumberingLevel.d.ts.map +1 -1
- package/dist/formatting/NumberingLevel.js +8 -6
- package/dist/formatting/NumberingLevel.js.map +1 -1
- package/dist/formatting/NumberingManager.d.ts.map +1 -1
- package/dist/formatting/NumberingManager.js +16 -1
- package/dist/formatting/NumberingManager.js.map +1 -1
- package/dist/formatting/Style.d.ts.map +1 -1
- package/dist/formatting/Style.js +14 -0
- package/dist/formatting/Style.js.map +1 -1
- package/dist/formatting/StylesManager.d.ts +2 -0
- package/dist/formatting/StylesManager.d.ts.map +1 -1
- package/dist/formatting/StylesManager.js +14 -2
- package/dist/formatting/StylesManager.js.map +1 -1
- package/dist/helpers/CleanupHelper.d.ts.map +1 -1
- package/dist/helpers/CleanupHelper.js +103 -26
- package/dist/helpers/CleanupHelper.js.map +1 -1
- package/dist/images/ImageOptimizer.d.ts.map +1 -1
- package/dist/images/ImageOptimizer.js +52 -4
- package/dist/images/ImageOptimizer.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -1
- package/dist/internal.js.map +1 -1
- package/dist/processors/CompatibilityUpgrader.d.ts.map +1 -1
- package/dist/processors/CompatibilityUpgrader.js +7 -1
- package/dist/processors/CompatibilityUpgrader.js.map +1 -1
- package/dist/processors/InMemoryRevisionAcceptor.d.ts +1 -0
- package/dist/processors/InMemoryRevisionAcceptor.d.ts.map +1 -1
- package/dist/processors/InMemoryRevisionAcceptor.js +135 -13
- package/dist/processors/InMemoryRevisionAcceptor.js.map +1 -1
- package/dist/processors/RevisionWalker.d.ts +2 -0
- package/dist/processors/RevisionWalker.d.ts.map +1 -1
- package/dist/processors/RevisionWalker.js +58 -8
- package/dist/processors/RevisionWalker.js.map +1 -1
- package/dist/processors/SelectiveRevisionAcceptor.d.ts.map +1 -1
- package/dist/processors/SelectiveRevisionAcceptor.js +116 -17
- package/dist/processors/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/processors/cnfStyleDecoder.d.ts.map +1 -1
- package/dist/processors/cnfStyleDecoder.js +7 -7
- package/dist/processors/cnfStyleDecoder.js.map +1 -1
- package/dist/processors/stripTrackedChanges.d.ts.map +1 -1
- package/dist/processors/stripTrackedChanges.js +16 -5
- package/dist/processors/stripTrackedChanges.js.map +1 -1
- package/dist/utils/deepClone.d.ts.map +1 -1
- package/dist/utils/deepClone.js +24 -11
- package/dist/utils/deepClone.js.map +1 -1
- package/dist/utils/units.d.ts.map +1 -1
- package/dist/utils/units.js +1 -1
- package/dist/utils/units.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +11 -23
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/xmlSanitization.d.ts +2 -0
- package/dist/utils/xmlSanitization.d.ts.map +1 -1
- package/dist/utils/xmlSanitization.js +133 -7
- package/dist/utils/xmlSanitization.js.map +1 -1
- package/dist/validation/RevisionAutoFixer.d.ts +6 -2
- package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
- package/dist/validation/RevisionAutoFixer.js +68 -54
- package/dist/validation/RevisionAutoFixer.js.map +1 -1
- package/dist/validation/RevisionValidator.d.ts +2 -0
- package/dist/validation/RevisionValidator.d.ts.map +1 -1
- package/dist/validation/RevisionValidator.js +26 -21
- package/dist/validation/RevisionValidator.js.map +1 -1
- package/dist/xml/XMLBuilder.d.ts +1 -0
- package/dist/xml/XMLBuilder.d.ts.map +1 -1
- package/dist/xml/XMLBuilder.js +52 -20
- package/dist/xml/XMLBuilder.js.map +1 -1
- package/dist/xml/XMLParser.d.ts +1 -0
- package/dist/xml/XMLParser.d.ts.map +1 -1
- package/dist/xml/XMLParser.js +55 -13
- package/dist/xml/XMLParser.js.map +1 -1
- package/dist/zip/ZipHandler.d.ts.map +1 -1
- package/dist/zip/ZipHandler.js +37 -13
- package/dist/zip/ZipHandler.js.map +1 -1
- package/dist/zip/ZipReader.d.ts.map +1 -1
- package/dist/zip/ZipReader.js +52 -5
- package/dist/zip/ZipReader.js.map +1 -1
- package/dist/zip/errors.d.ts +3 -0
- package/dist/zip/errors.d.ts.map +1 -1
- package/dist/zip/errors.js +9 -1
- package/dist/zip/errors.js.map +1 -1
- package/dist/zip/types.d.ts +4 -0
- package/dist/zip/types.d.ts.map +1 -1
- package/dist/zip/types.js +4 -0
- package/dist/zip/types.js.map +1 -1
- package/package.json +2 -2
- package/src/core/Document.ts +1159 -240
- package/src/core/DocumentGenerator.ts +106 -26
- package/src/core/DocumentParser.ts +692 -120
- package/src/core/RelationshipManager.ts +446 -442
- package/src/elements/Comment.ts +123 -18
- package/src/elements/CommentManager.ts +121 -22
- package/src/elements/EndnoteManager.ts +4 -1
- package/src/elements/Field.ts +134 -43
- package/src/elements/FieldHelpers.ts +20 -4
- package/src/elements/Footer.ts +62 -67
- package/src/elements/FootnoteManager.ts +4 -1
- package/src/elements/Header.ts +62 -67
- package/src/elements/HeaderFooterManager.ts +56 -8
- package/src/elements/Hyperlink.ts +150 -55
- package/src/elements/Image.ts +51 -7
- package/src/elements/ImageManager.ts +24 -0
- package/src/elements/Paragraph.ts +161 -40
- package/src/elements/Revision.ts +446 -47
- package/src/elements/Run.ts +44 -6
- package/src/elements/Section.ts +25 -9
- package/src/elements/Shape.ts +52 -44
- package/src/elements/StructuredDocumentTag.ts +83 -8
- package/src/elements/Table.ts +151 -50
- package/src/elements/TableCell.ts +57 -17
- package/src/elements/TableOfContents.ts +10 -9
- package/src/elements/TableRow.ts +35 -3
- package/src/elements/TextBox.ts +58 -43
- package/src/formatting/AbstractNumbering.ts +4 -2
- package/src/formatting/NumberingLevel.ts +50 -12
- package/src/formatting/NumberingManager.ts +32 -3
- package/src/formatting/Style.ts +14 -0
- package/src/formatting/StylesManager.ts +29 -2
- package/src/helpers/CleanupHelper.ts +122 -30
- package/src/images/ImageOptimizer.ts +64 -5
- package/src/index.ts +7 -0
- package/src/internal.ts +1 -0
- package/src/processors/CompatibilityUpgrader.ts +15 -2
- package/src/processors/InMemoryRevisionAcceptor.ts +276 -17
- package/src/processors/RevisionWalker.ts +83 -7
- package/src/processors/SelectiveRevisionAcceptor.ts +164 -28
- package/src/processors/cnfStyleDecoder.ts +18 -12
- package/src/processors/stripTrackedChanges.ts +47 -10
- package/src/utils/deepClone.ts +48 -14
- package/src/utils/units.ts +3 -1
- package/src/utils/validation.ts +30 -32
- package/src/utils/xmlSanitization.ts +218 -22
- package/src/validation/RevisionAutoFixer.ts +109 -65
- package/src/validation/RevisionValidator.ts +48 -23
- package/src/xml/XMLBuilder.ts +82 -22
- package/src/xml/XMLParser.ts +87 -20
- package/src/zip/ZipHandler.ts +61 -21
- package/src/zip/ZipReader.ts +135 -13
- package/src/zip/errors.ts +15 -0
- package/src/zip/types.ts +44 -2
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
* - 0x09 (tab), 0x0A (newline), 0x0D (carriage return)
|
|
10
10
|
* - 0x20-0xD7FF, 0xE000-0xFFFD, 0x10000-0x10FFFF
|
|
11
11
|
*
|
|
12
|
-
* Invalid characters
|
|
12
|
+
* Invalid characters that must be removed:
|
|
13
13
|
* - 0x00-0x08 (NULL through BACKSPACE)
|
|
14
14
|
* - 0x0B-0x0C (VERTICAL TAB and FORM FEED)
|
|
15
15
|
* - 0x0E-0x1F (SHIFT OUT through UNIT SEPARATOR)
|
|
16
16
|
* - 0x7F (DELETE)
|
|
17
|
+
* - 0xFFFE-0xFFFF (Unicode noncharacters excluded by the Char production)
|
|
18
|
+
* - Unpaired surrogates (0xD800-0xDFFF outside a valid high/low pair)
|
|
17
19
|
*
|
|
18
20
|
* @module xmlSanitization
|
|
19
21
|
*/
|
|
@@ -21,26 +23,40 @@
|
|
|
21
23
|
import { getGlobalLogger } from './logger.js';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
* Regular expression matching invalid XML 1.0
|
|
25
|
-
* Matches: 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F
|
|
26
|
+
* Regular expression matching invalid XML 1.0 characters.
|
|
27
|
+
* Matches: 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F, 0xFFFE, 0xFFFF
|
|
26
28
|
* Does NOT match valid chars: 0x09 (tab), 0x0A (newline), 0x0D (CR)
|
|
27
29
|
*/
|
|
28
|
-
const INVALID_XML_CHAR_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
30
|
+
const INVALID_XML_CHAR_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
33
|
+
* Matches surrogate code units that do not form a valid high/low pair.
|
|
34
|
+
* XML 1.0 Char excludes 0xD800-0xDFFF entirely; only properly paired
|
|
35
|
+
* surrogates (which decode to a single 0x10000-0x10FFFF character) are
|
|
36
|
+
* legal, so lone halves must be stripped or they corrupt the part at
|
|
37
|
+
* UTF-8 encode time.
|
|
38
|
+
*/
|
|
39
|
+
const UNPAIRED_SURROGATE_REGEX =
|
|
40
|
+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Removes invalid XML 1.0 characters from text.
|
|
32
44
|
*
|
|
33
|
-
* Per XML 1.0 spec, characters 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F
|
|
34
|
-
*
|
|
45
|
+
* Per XML 1.0 spec, characters 0x00-0x08, 0x0B-0x0C, 0x0E-0x1F, 0x7F,
|
|
46
|
+
* 0xFFFE, 0xFFFF, and unpaired surrogates are invalid and cannot appear
|
|
47
|
+
* in XML documents. This function removes them.
|
|
35
48
|
*
|
|
36
49
|
* Valid control characters are preserved:
|
|
37
50
|
* - Tab (0x09)
|
|
38
51
|
* - Line Feed / Newline (0x0A)
|
|
39
52
|
* - Carriage Return (0x0D)
|
|
40
53
|
*
|
|
54
|
+
* Properly paired surrogates (astral-plane characters such as emoji)
|
|
55
|
+
* are preserved.
|
|
56
|
+
*
|
|
41
57
|
* @param text - Input text to sanitize
|
|
42
58
|
* @param logWarning - If true, logs a warning when invalid chars are found (default: true)
|
|
43
|
-
* @returns Sanitized text with invalid
|
|
59
|
+
* @returns Sanitized text with invalid characters removed
|
|
44
60
|
*
|
|
45
61
|
* @example
|
|
46
62
|
* ```typescript
|
|
@@ -54,31 +70,33 @@ const INVALID_XML_CHAR_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
|
54
70
|
* ```
|
|
55
71
|
*/
|
|
56
72
|
export function removeInvalidXmlChars(text: string, logWarning = true): string {
|
|
57
|
-
// Reset regex lastIndex for global
|
|
73
|
+
// Reset regex lastIndex for global regexes
|
|
58
74
|
INVALID_XML_CHAR_REGEX.lastIndex = 0;
|
|
75
|
+
UNPAIRED_SURROGATE_REGEX.lastIndex = 0;
|
|
59
76
|
|
|
60
|
-
if (logWarning && INVALID_XML_CHAR_REGEX.test(text)) {
|
|
77
|
+
if (logWarning && (INVALID_XML_CHAR_REGEX.test(text) || UNPAIRED_SURROGATE_REGEX.test(text))) {
|
|
61
78
|
// Reset regex lastIndex after test
|
|
62
79
|
INVALID_XML_CHAR_REGEX.lastIndex = 0;
|
|
80
|
+
UNPAIRED_SURROGATE_REGEX.lastIndex = 0;
|
|
63
81
|
|
|
64
82
|
const invalidChars = findInvalidXmlChars(text);
|
|
65
83
|
const hexCodes = invalidChars
|
|
66
84
|
.map((c) => `0x${c.toString(16).toUpperCase().padStart(2, '0')}`)
|
|
67
85
|
.join(', ');
|
|
68
|
-
getGlobalLogger().warn(
|
|
69
|
-
`[XMLSanitization] Removing invalid XML control characters: ${hexCodes}`
|
|
70
|
-
);
|
|
86
|
+
getGlobalLogger().warn(`[XMLSanitization] Removing invalid XML characters: ${hexCodes}`);
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
// Reset regex lastIndex before replace
|
|
74
90
|
INVALID_XML_CHAR_REGEX.lastIndex = 0;
|
|
75
|
-
|
|
91
|
+
UNPAIRED_SURROGATE_REGEX.lastIndex = 0;
|
|
92
|
+
return text.replace(INVALID_XML_CHAR_REGEX, '').replace(UNPAIRED_SURROGATE_REGEX, '');
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
/**
|
|
79
|
-
* Finds all invalid XML 1.0
|
|
96
|
+
* Finds all invalid XML 1.0 characters in text.
|
|
80
97
|
*
|
|
81
|
-
* Returns an array of unique character codes that are invalid per XML 1.0 spec
|
|
98
|
+
* Returns an array of unique character codes that are invalid per XML 1.0 spec,
|
|
99
|
+
* including 0xFFFE/0xFFFF noncharacters and unpaired surrogate code units.
|
|
82
100
|
* This is useful for diagnostics and error reporting.
|
|
83
101
|
*
|
|
84
102
|
* @param text - Text to scan for invalid characters
|
|
@@ -100,12 +118,25 @@ export function findInvalidXmlChars(text: string): number[] {
|
|
|
100
118
|
const code = text.charCodeAt(i);
|
|
101
119
|
|
|
102
120
|
// Check if character is in invalid ranges
|
|
103
|
-
|
|
121
|
+
let isInvalid =
|
|
104
122
|
(code >= 0x00 && code <= 0x08) || // NULL through BACKSPACE
|
|
105
123
|
(code >= 0x0b && code <= 0x0c) || // VERTICAL TAB and FORM FEED
|
|
106
124
|
(code >= 0x0e && code <= 0x1f) || // SHIFT OUT through UNIT SEPARATOR
|
|
107
|
-
code === 0x7f // DELETE
|
|
108
|
-
|
|
125
|
+
code === 0x7f || // DELETE
|
|
126
|
+
code === 0xfffe || // Noncharacter excluded by XML 1.0 Char
|
|
127
|
+
code === 0xffff; // Noncharacter excluded by XML 1.0 Char
|
|
128
|
+
|
|
129
|
+
if (!isInvalid && code >= 0xd800 && code <= 0xdbff) {
|
|
130
|
+
// High surrogate is only valid when followed by a low surrogate
|
|
131
|
+
const next = text.charCodeAt(i + 1); // NaN at end of string fails the range check
|
|
132
|
+
isInvalid = !(next >= 0xdc00 && next <= 0xdfff);
|
|
133
|
+
} else if (!isInvalid && code >= 0xdc00 && code <= 0xdfff) {
|
|
134
|
+
// Low surrogate is only valid when preceded by a high surrogate
|
|
135
|
+
const prev = text.charCodeAt(i - 1); // NaN at start of string fails the range check
|
|
136
|
+
isInvalid = !(prev >= 0xd800 && prev <= 0xdbff);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isInvalid) {
|
|
109
140
|
// Only add unique codes
|
|
110
141
|
if (!invalid.includes(code)) {
|
|
111
142
|
invalid.push(code);
|
|
@@ -117,7 +148,7 @@ export function findInvalidXmlChars(text: string): number[] {
|
|
|
117
148
|
}
|
|
118
149
|
|
|
119
150
|
/**
|
|
120
|
-
* Checks if text contains any invalid XML 1.0
|
|
151
|
+
* Checks if text contains any invalid XML 1.0 characters.
|
|
121
152
|
*
|
|
122
153
|
* This is a fast check that returns true/false without identifying specific characters.
|
|
123
154
|
* Use `findInvalidXmlChars()` if you need to know which characters are invalid.
|
|
@@ -133,9 +164,174 @@ export function findInvalidXmlChars(text: string): number[] {
|
|
|
133
164
|
* ```
|
|
134
165
|
*/
|
|
135
166
|
export function hasInvalidXmlChars(text: string): boolean {
|
|
136
|
-
// Reset regex lastIndex for global
|
|
167
|
+
// Reset regex lastIndex for global regexes
|
|
137
168
|
INVALID_XML_CHAR_REGEX.lastIndex = 0;
|
|
138
|
-
|
|
169
|
+
UNPAIRED_SURROGATE_REGEX.lastIndex = 0;
|
|
170
|
+
return INVALID_XML_CHAR_REGEX.test(text) || UNPAIRED_SURROGATE_REGEX.test(text);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* CT_RPr child element order per ECMA-376 Part 1 §17.3.2.28 (local names,
|
|
175
|
+
* `w:` namespace). Used to repair raw-preserved run-property blocks whose
|
|
176
|
+
* children are out of schema order — Word rejects, for example, a `w:b`
|
|
177
|
+
* that appears after `w:color`/`w:sz` and reports the document as corrupt.
|
|
178
|
+
*/
|
|
179
|
+
const CT_RPR_CHILD_ORDER: readonly string[] = [
|
|
180
|
+
'rStyle',
|
|
181
|
+
'rFonts',
|
|
182
|
+
'b',
|
|
183
|
+
'bCs',
|
|
184
|
+
'i',
|
|
185
|
+
'iCs',
|
|
186
|
+
'caps',
|
|
187
|
+
'smallCaps',
|
|
188
|
+
'strike',
|
|
189
|
+
'dstrike',
|
|
190
|
+
'outline',
|
|
191
|
+
'shadow',
|
|
192
|
+
'emboss',
|
|
193
|
+
'imprint',
|
|
194
|
+
'noProof',
|
|
195
|
+
'snapToGrid',
|
|
196
|
+
'vanish',
|
|
197
|
+
'webHidden',
|
|
198
|
+
'color',
|
|
199
|
+
'spacing',
|
|
200
|
+
'w',
|
|
201
|
+
'kern',
|
|
202
|
+
'position',
|
|
203
|
+
'sz',
|
|
204
|
+
'szCs',
|
|
205
|
+
'highlight',
|
|
206
|
+
'u',
|
|
207
|
+
'effect',
|
|
208
|
+
'bdr',
|
|
209
|
+
'shd',
|
|
210
|
+
'fitText',
|
|
211
|
+
'vertAlign',
|
|
212
|
+
'rtl',
|
|
213
|
+
'cs',
|
|
214
|
+
'em',
|
|
215
|
+
'lang',
|
|
216
|
+
'eastAsianLayout',
|
|
217
|
+
'specVanish',
|
|
218
|
+
'oMath',
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Splits the inner content of an element into its top-level child elements,
|
|
223
|
+
* respecting nesting so that a child's descendants are kept with it. Returns
|
|
224
|
+
* the raw XML fragment for each direct child in document order. Non-element
|
|
225
|
+
* text between children (whitespace) is dropped, which is safe for the
|
|
226
|
+
* element-only content models this helper targets (e.g. `w:rPr`).
|
|
227
|
+
*/
|
|
228
|
+
function splitTopLevelElements(inner: string): string[] {
|
|
229
|
+
const children: string[] = [];
|
|
230
|
+
const tagRegex = /<([A-Za-z][\w:.-]*)([^>]*?)(\/?)>|<\/([A-Za-z][\w:.-]*)\s*>/g;
|
|
231
|
+
let depth = 0;
|
|
232
|
+
let start = -1;
|
|
233
|
+
let match: RegExpExecArray | null;
|
|
234
|
+
while ((match = tagRegex.exec(inner)) !== null) {
|
|
235
|
+
const isClosing = match[4] !== undefined;
|
|
236
|
+
const isSelfClosing = match[3] === '/';
|
|
237
|
+
if (isClosing) {
|
|
238
|
+
if (depth > 0) {
|
|
239
|
+
depth--;
|
|
240
|
+
if (depth === 0 && start !== -1) {
|
|
241
|
+
children.push(inner.slice(start, tagRegex.lastIndex));
|
|
242
|
+
start = -1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (isSelfClosing) {
|
|
248
|
+
if (depth === 0) {
|
|
249
|
+
children.push(inner.slice(match.index, tagRegex.lastIndex));
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Opening tag of a paired element
|
|
254
|
+
if (depth === 0) start = match.index;
|
|
255
|
+
depth++;
|
|
256
|
+
}
|
|
257
|
+
return children;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Reorders the direct children of every `<w:rPr>` block in a raw XML part to
|
|
262
|
+
* the ECMA-376 CT_RPr sequence. Unknown/unmodeled children retain their
|
|
263
|
+
* relative position at the end of the block. The transform is idempotent:
|
|
264
|
+
* already-ordered run properties are returned unchanged.
|
|
265
|
+
*
|
|
266
|
+
* Some producers (and older versions of this framework) appended boolean
|
|
267
|
+
* toggles such as `w:b`/`w:bCs` to the end of an existing `w:rPr`, leaving
|
|
268
|
+
* them after `w:color`/`w:sz`. Word treats the out-of-order child as an
|
|
269
|
+
* unexpected element and flags the whole document as corrupt. Numbering
|
|
270
|
+
* definitions are written back verbatim for fidelity, so this repair runs on
|
|
271
|
+
* the preserved part before it is saved.
|
|
272
|
+
*
|
|
273
|
+
* @param xml - Raw XML for a part containing `w:rPr` blocks (e.g. numbering.xml)
|
|
274
|
+
* @returns XML with every `w:rPr` child sequence normalized to schema order
|
|
275
|
+
*/
|
|
276
|
+
export function reorderRunPropertyChildren(xml: string): string {
|
|
277
|
+
if (!xml || !xml.includes('<w:rPr')) return xml;
|
|
278
|
+
|
|
279
|
+
const orderIndex = (fragment: string): number => {
|
|
280
|
+
const nameMatch = /^<w:(\w+)/.exec(fragment);
|
|
281
|
+
const name = nameMatch?.[1];
|
|
282
|
+
if (!name) return CT_RPR_CHILD_ORDER.length;
|
|
283
|
+
const idx = CT_RPR_CHILD_ORDER.indexOf(name);
|
|
284
|
+
return idx === -1 ? CT_RPR_CHILD_ORDER.length : idx;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Only match non-empty <w:rPr>...</w:rPr> blocks; self-closing <w:rPr/> has
|
|
288
|
+
// no children to reorder.
|
|
289
|
+
return xml.replace(/<w:rPr(\s[^>]*)?>([\s\S]*?)<\/w:rPr>/g, (full, attrs, inner) => {
|
|
290
|
+
if (!inner || !(inner as string).includes('<')) return full;
|
|
291
|
+
const children = splitTopLevelElements(inner);
|
|
292
|
+
if (children.length < 2) return full;
|
|
293
|
+
|
|
294
|
+
const ordered = children
|
|
295
|
+
.map((frag, i) => ({ frag, i, order: orderIndex(frag) }))
|
|
296
|
+
// Stable sort: ties (including unknown children) keep original order.
|
|
297
|
+
.sort((a, b) => a.order - b.order || a.i - b.i);
|
|
298
|
+
|
|
299
|
+
if (ordered.every((c, i) => c.i === i)) return full; // already in order
|
|
300
|
+
|
|
301
|
+
return `<w:rPr${attrs ?? ''}>${ordered.map((c) => c.frag).join('')}</w:rPr>`;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Normalizes malformed floating-drawing relative-size elements in a raw anchor
|
|
307
|
+
* fragment. Per ECMA-376 (DrawingML wordprocessing 2010 extensions),
|
|
308
|
+
* `wp14:sizeRelH` must contain a `wp14:pctWidth` child and `wp14:sizeRelV` a
|
|
309
|
+
* `wp14:pctHeight` child. Some producers emit the percentage as bare text
|
|
310
|
+
* content (`<wp14:sizeRelH relativeFrom="margin">0</wp14:sizeRelH>`), which
|
|
311
|
+
* Word rejects as incomplete content. This wraps such text in the required
|
|
312
|
+
* child element. Already-valid nested forms are left untouched because the
|
|
313
|
+
* text-content pattern (`[^<]`) cannot match an element child.
|
|
314
|
+
*
|
|
315
|
+
* @param xml - Raw anchor-extras fragment captured during parsing
|
|
316
|
+
* @returns Fragment with sizeRelH/sizeRelV normalized to schema-valid form
|
|
317
|
+
*/
|
|
318
|
+
export function normalizeAnchorSizeRel(xml: string): string {
|
|
319
|
+
if (!xml) return xml;
|
|
320
|
+
return xml
|
|
321
|
+
.replace(
|
|
322
|
+
/(<wp14:sizeRelH\b[^>]*>)([^<]+)(<\/wp14:sizeRelH>)/g,
|
|
323
|
+
(_m, open: string, text: string, close: string) =>
|
|
324
|
+
text.trim() === ''
|
|
325
|
+
? `${open}${text}${close}`
|
|
326
|
+
: `${open}<wp14:pctWidth>${text.trim()}</wp14:pctWidth>${close}`
|
|
327
|
+
)
|
|
328
|
+
.replace(
|
|
329
|
+
/(<wp14:sizeRelV\b[^>]*>)([^<]+)(<\/wp14:sizeRelV>)/g,
|
|
330
|
+
(_m, open: string, text: string, close: string) =>
|
|
331
|
+
text.trim() === ''
|
|
332
|
+
? `${open}${text}${close}`
|
|
333
|
+
: `${open}<wp14:pctHeight>${text.trim()}</wp14:pctHeight>${close}`
|
|
334
|
+
);
|
|
139
335
|
}
|
|
140
336
|
|
|
141
337
|
/**
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
FixAction,
|
|
18
18
|
ValidationIssue,
|
|
19
19
|
} from './ValidationRules.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
CONTENTLESS_REVISION_TYPES,
|
|
22
|
+
RevisionValidator,
|
|
23
|
+
revisionHasContent,
|
|
24
|
+
} from './RevisionValidator.js';
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Automatically fixes revision validation issues.
|
|
@@ -81,9 +85,16 @@ export class RevisionAutoFixer {
|
|
|
81
85
|
actions.push(...this.fixMissingAuthors(revisions, defaultAuthor, options?.dryRun));
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
// Fix orphaned move markers (REV003, REV004)
|
|
88
|
+
// Fix orphaned move markers (REV003, REV004) — one pairing pass
|
|
89
|
+
// produces both rules, so gate each side individually to honor
|
|
90
|
+
// onlyRules/skipRules without mutating revisions the caller excluded.
|
|
85
91
|
if (shouldProcess('REV003') || shouldProcess('REV004')) {
|
|
86
|
-
actions.push(
|
|
92
|
+
actions.push(
|
|
93
|
+
...this.fixOrphanedMoveMarkers(revisionManager, revisions, options?.dryRun, doc, {
|
|
94
|
+
moveFrom: shouldProcess('REV003'),
|
|
95
|
+
moveTo: shouldProcess('REV004'),
|
|
96
|
+
})
|
|
97
|
+
);
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
// Fix missing dates (REV101)
|
|
@@ -98,7 +109,7 @@ export class RevisionAutoFixer {
|
|
|
98
109
|
|
|
99
110
|
// Fix empty revisions (REV103)
|
|
100
111
|
if (shouldProcess('REV103')) {
|
|
101
|
-
actions.push(...this.fixEmptyRevisions(revisionManager, revisions, options?.dryRun));
|
|
112
|
+
actions.push(...this.fixEmptyRevisions(revisionManager, revisions, options?.dryRun, doc));
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
// Fix non-sequential IDs (REV104)
|
|
@@ -297,20 +308,51 @@ export class RevisionAutoFixer {
|
|
|
297
308
|
return actions;
|
|
298
309
|
}
|
|
299
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Removes a revision from the paragraph content that owns it.
|
|
313
|
+
*
|
|
314
|
+
* RevisionManager.removeById() only deregisters the revision from the
|
|
315
|
+
* manager registry, but serialization reads Paragraph.content directly —
|
|
316
|
+
* without this step the removed markup would still be written on save.
|
|
317
|
+
*
|
|
318
|
+
* @param doc - Document whose paragraphs are searched
|
|
319
|
+
* @param revision - Revision instance to remove
|
|
320
|
+
* @returns True if the revision was found and removed from a paragraph
|
|
321
|
+
*/
|
|
322
|
+
private static removeRevisionFromContent(doc: Document, revision: Revision): boolean {
|
|
323
|
+
for (const paragraph of doc.getAllParagraphs()) {
|
|
324
|
+
// replaceContent matches by identity and splices without tracking side effects
|
|
325
|
+
if (paragraph.replaceContent(revision, [])) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
300
332
|
/**
|
|
301
333
|
* Fix orphaned move markers by removing them.
|
|
302
334
|
*
|
|
303
335
|
* @param revisionManager - RevisionManager instance
|
|
304
336
|
* @param revisions - Array of revisions
|
|
305
337
|
* @param dryRun - If true, only report changes without applying
|
|
338
|
+
* @param doc - Document to remove the revision content from; without it
|
|
339
|
+
* only the manager registry is pruned and saved XML keeps the markup
|
|
340
|
+
* @param ruleFilter - Which sides of the pairing to fix; REV003 (moveFrom)
|
|
341
|
+
* and REV004 (moveTo) are separate rules, so callers honoring
|
|
342
|
+
* onlyRules/skipRules must be able to fix one without the other.
|
|
343
|
+
* Defaults to both.
|
|
306
344
|
* @returns Array of fix actions
|
|
307
345
|
*/
|
|
308
346
|
static fixOrphanedMoveMarkers(
|
|
309
347
|
revisionManager: { removeById(id: number): boolean },
|
|
310
348
|
revisions: Revision[],
|
|
311
|
-
dryRun?: boolean
|
|
349
|
+
dryRun?: boolean,
|
|
350
|
+
doc?: Document,
|
|
351
|
+
ruleFilter?: { moveFrom?: boolean; moveTo?: boolean }
|
|
312
352
|
): FixAction[] {
|
|
313
353
|
const actions: FixAction[] = [];
|
|
354
|
+
const fixMoveFrom = ruleFilter?.moveFrom ?? true;
|
|
355
|
+
const fixMoveTo = ruleFilter?.moveTo ?? true;
|
|
314
356
|
|
|
315
357
|
const moveFromIds = new Map<string, Revision>();
|
|
316
358
|
const moveToIds = new Map<string, Revision>();
|
|
@@ -326,52 +368,62 @@ export class RevisionAutoFixer {
|
|
|
326
368
|
}
|
|
327
369
|
}
|
|
328
370
|
|
|
329
|
-
// Remove orphaned moveFrom
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
371
|
+
// Remove orphaned moveFrom (REV003)
|
|
372
|
+
if (fixMoveFrom) {
|
|
373
|
+
for (const [moveId, rev] of moveFromIds) {
|
|
374
|
+
if (!moveToIds.has(moveId)) {
|
|
375
|
+
const issue: ValidationIssue = {
|
|
376
|
+
code: REVISION_RULES.ORPHANED_MOVE_FROM.code,
|
|
377
|
+
severity: 'error',
|
|
378
|
+
message: `Orphaned moveFrom with moveId="${moveId}"`,
|
|
379
|
+
location: { revisionId: rev.getId() },
|
|
380
|
+
autoFixable: true,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
actions.push({
|
|
384
|
+
issue,
|
|
385
|
+
action: `Removed orphaned moveFrom (ID: ${rev.getId()}, moveId: ${moveId})`,
|
|
386
|
+
before: { type: 'moveFrom', moveId },
|
|
387
|
+
after: null,
|
|
388
|
+
success: true,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (!dryRun) {
|
|
392
|
+
revisionManager.removeById(rev.getId());
|
|
393
|
+
if (doc) {
|
|
394
|
+
this.removeRevisionFromContent(doc, rev);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
350
397
|
}
|
|
351
398
|
}
|
|
352
399
|
}
|
|
353
400
|
|
|
354
|
-
// Remove orphaned moveTo
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
401
|
+
// Remove orphaned moveTo (REV004)
|
|
402
|
+
if (fixMoveTo) {
|
|
403
|
+
for (const [moveId, rev] of moveToIds) {
|
|
404
|
+
if (!moveFromIds.has(moveId)) {
|
|
405
|
+
const issue: ValidationIssue = {
|
|
406
|
+
code: REVISION_RULES.ORPHANED_MOVE_TO.code,
|
|
407
|
+
severity: 'error',
|
|
408
|
+
message: `Orphaned moveTo with moveId="${moveId}"`,
|
|
409
|
+
location: { revisionId: rev.getId() },
|
|
410
|
+
autoFixable: true,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
actions.push({
|
|
414
|
+
issue,
|
|
415
|
+
action: `Removed orphaned moveTo (ID: ${rev.getId()}, moveId: ${moveId})`,
|
|
416
|
+
before: { type: 'moveTo', moveId },
|
|
417
|
+
after: null,
|
|
418
|
+
success: true,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (!dryRun) {
|
|
422
|
+
revisionManager.removeById(rev.getId());
|
|
423
|
+
if (doc) {
|
|
424
|
+
this.removeRevisionFromContent(doc, rev);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
375
427
|
}
|
|
376
428
|
}
|
|
377
429
|
}
|
|
@@ -385,38 +437,27 @@ export class RevisionAutoFixer {
|
|
|
385
437
|
* @param revisionManager - RevisionManager instance
|
|
386
438
|
* @param revisions - Array of revisions
|
|
387
439
|
* @param dryRun - If true, only report changes without applying
|
|
440
|
+
* @param doc - Document to remove the revision content from; without it
|
|
441
|
+
* only the manager registry is pruned and saved XML keeps the markup
|
|
388
442
|
* @returns Array of fix actions
|
|
389
443
|
*/
|
|
390
444
|
static fixEmptyRevisions(
|
|
391
445
|
revisionManager: RevisionManager,
|
|
392
446
|
revisions: Revision[],
|
|
393
|
-
dryRun?: boolean
|
|
447
|
+
dryRun?: boolean,
|
|
448
|
+
doc?: Document
|
|
394
449
|
): FixAction[] {
|
|
395
450
|
const actions: FixAction[] = [];
|
|
396
451
|
|
|
397
|
-
const propertyChangeTypes = [
|
|
398
|
-
'runPropertiesChange',
|
|
399
|
-
'paragraphPropertiesChange',
|
|
400
|
-
'tablePropertiesChange',
|
|
401
|
-
'tableExceptionPropertiesChange',
|
|
402
|
-
'tableRowPropertiesChange',
|
|
403
|
-
'tableCellPropertiesChange',
|
|
404
|
-
'sectionPropertiesChange',
|
|
405
|
-
'numberingChange',
|
|
406
|
-
];
|
|
407
|
-
|
|
408
452
|
for (const rev of revisions) {
|
|
409
453
|
const type = rev.getType();
|
|
410
454
|
|
|
411
|
-
//
|
|
412
|
-
if (
|
|
455
|
+
// Property changes and cell markers are contentless by design
|
|
456
|
+
if (CONTENTLESS_REVISION_TYPES.has(type)) {
|
|
413
457
|
continue;
|
|
414
458
|
}
|
|
415
459
|
|
|
416
|
-
|
|
417
|
-
const hasContent = runs.length > 0 && runs.some((r) => r.getText().length > 0);
|
|
418
|
-
|
|
419
|
-
if (!hasContent) {
|
|
460
|
+
if (!revisionHasContent(rev)) {
|
|
420
461
|
const issue: ValidationIssue = {
|
|
421
462
|
code: REVISION_RULES.EMPTY_REVISION.code,
|
|
422
463
|
severity: 'warning',
|
|
@@ -435,6 +476,9 @@ export class RevisionAutoFixer {
|
|
|
435
476
|
|
|
436
477
|
if (!dryRun) {
|
|
437
478
|
revisionManager.removeById(rev.getId());
|
|
479
|
+
if (doc) {
|
|
480
|
+
this.removeRevisionFromContent(doc, rev);
|
|
481
|
+
}
|
|
438
482
|
}
|
|
439
483
|
}
|
|
440
484
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Document } from '../core/Document.js';
|
|
11
11
|
import type { Revision } from '../elements/Revision.js';
|
|
12
|
+
import { isHyperlinkContent, isImageRunContent } from '../elements/RevisionContent.js';
|
|
12
13
|
import {
|
|
13
14
|
REVISION_RULES,
|
|
14
15
|
ValidationIssue,
|
|
@@ -18,6 +19,44 @@ import {
|
|
|
18
19
|
} from './ValidationRules.js';
|
|
19
20
|
import { ValidationRuleRegistry } from './ValidationRuleRegistry.js';
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Revision types that are structurally contentless per ECMA-376: property
|
|
24
|
+
* change markers carry before/after property snapshots rather than runs, and
|
|
25
|
+
* table cell markers (w:cellIns/w:cellDel/w:cellMerge) annotate the cell
|
|
26
|
+
* itself. The REV103 empty-content rule must never flag these.
|
|
27
|
+
*
|
|
28
|
+
* Shared with RevisionAutoFixer so the validator and fixer cannot drift
|
|
29
|
+
* apart on which revision types are allowed to be empty.
|
|
30
|
+
*/
|
|
31
|
+
export const CONTENTLESS_REVISION_TYPES: ReadonlySet<string> = new Set([
|
|
32
|
+
'runPropertiesChange',
|
|
33
|
+
'paragraphPropertiesChange',
|
|
34
|
+
'tablePropertiesChange',
|
|
35
|
+
'tableExceptionPropertiesChange',
|
|
36
|
+
'tableRowPropertiesChange',
|
|
37
|
+
'tableCellPropertiesChange',
|
|
38
|
+
'sectionPropertiesChange',
|
|
39
|
+
'numberingChange',
|
|
40
|
+
'tableCellInsert',
|
|
41
|
+
'tableCellDelete',
|
|
42
|
+
'tableCellMerge',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* True when a revision carries real content. Per ECMA-376, w:ins/w:del may
|
|
47
|
+
* contain w:hyperlink and image runs (w:r with w:drawing) in addition to
|
|
48
|
+
* text runs, so a runs-only text check would falsely flag hyperlink-only
|
|
49
|
+
* and image-only tracked changes as empty. Shared with RevisionAutoFixer.
|
|
50
|
+
*/
|
|
51
|
+
export function revisionHasContent(rev: Revision): boolean {
|
|
52
|
+
// Covers text runs and hyperlink display text
|
|
53
|
+
if (rev.getText().length > 0) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// Hyperlinks count even without display text; ImageRuns have no text at all
|
|
57
|
+
return rev.getContent().some((item) => isHyperlinkContent(item) || isImageRunContent(item));
|
|
58
|
+
}
|
|
59
|
+
|
|
21
60
|
/**
|
|
22
61
|
* Validates document revisions for ECMA-376 compliance.
|
|
23
62
|
*
|
|
@@ -67,15 +106,15 @@ export class RevisionValidator {
|
|
|
67
106
|
if (!skipRules.has('REV001')) {
|
|
68
107
|
allIssues.push(...this.validateRevisionIds(revisions));
|
|
69
108
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
109
|
+
// validateMovePairs emits both REV003 and REV004 from one pairing pass;
|
|
110
|
+
// filter per issue code so skipping one rule does not silently disable
|
|
111
|
+
// the other (REV004 is error-severity and corruption-relevant).
|
|
112
|
+
allIssues.push(...this.validateMovePairs(revisions).filter((i) => !skipRules.has(i.code)));
|
|
73
113
|
if (!skipRules.has('REV002')) {
|
|
74
114
|
allIssues.push(...this.validateAuthors(revisions));
|
|
75
115
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
116
|
+
// validateDates emits both REV101 and REV102 — same per-code filtering.
|
|
117
|
+
allIssues.push(...this.validateDates(revisions).filter((i) => !skipRules.has(i.code)));
|
|
79
118
|
if (!skipRules.has('REV103')) {
|
|
80
119
|
allIssues.push(...this.validateContent(revisions));
|
|
81
120
|
}
|
|
@@ -302,29 +341,15 @@ export class RevisionValidator {
|
|
|
302
341
|
static validateContent(revisions: Revision[]): ValidationIssue[] {
|
|
303
342
|
const issues: ValidationIssue[] = [];
|
|
304
343
|
|
|
305
|
-
const propertyChangeTypes = [
|
|
306
|
-
'runPropertiesChange',
|
|
307
|
-
'paragraphPropertiesChange',
|
|
308
|
-
'tablePropertiesChange',
|
|
309
|
-
'tableExceptionPropertiesChange',
|
|
310
|
-
'tableRowPropertiesChange',
|
|
311
|
-
'tableCellPropertiesChange',
|
|
312
|
-
'sectionPropertiesChange',
|
|
313
|
-
'numberingChange',
|
|
314
|
-
];
|
|
315
|
-
|
|
316
344
|
for (const rev of revisions) {
|
|
317
345
|
const type = rev.getType();
|
|
318
346
|
|
|
319
|
-
// Property changes
|
|
320
|
-
if (
|
|
347
|
+
// Property changes and cell markers are contentless by design
|
|
348
|
+
if (CONTENTLESS_REVISION_TYPES.has(type)) {
|
|
321
349
|
continue;
|
|
322
350
|
}
|
|
323
351
|
|
|
324
|
-
|
|
325
|
-
const hasContent = runs.length > 0 && runs.some((r) => r.getText().length > 0);
|
|
326
|
-
|
|
327
|
-
if (!hasContent) {
|
|
352
|
+
if (!revisionHasContent(rev)) {
|
|
328
353
|
issues.push(
|
|
329
354
|
createIssueFromRule(
|
|
330
355
|
REVISION_RULES.EMPTY_REVISION,
|