docxmlater 11.0.10 → 12.1.0
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 +53 -13
- package/dist/core/Document.d.ts +29 -3
- package/dist/core/Document.d.ts.map +1 -1
- package/dist/core/Document.js +733 -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 +427 -81
- 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 +735 -172
- 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 +427 -81
- 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 +223 -39
- 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/acceptRevisions.js +20 -11
- package/dist/esm/processors/acceptRevisions.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 +9 -3
- package/dist/processors/RevisionWalker.d.ts.map +1 -1
- package/dist/processors/RevisionWalker.js +223 -39
- 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/acceptRevisions.d.ts +1 -0
- package/dist/processors/acceptRevisions.d.ts.map +1 -1
- package/dist/processors/acceptRevisions.js +21 -11
- package/dist/processors/acceptRevisions.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 +1184 -242
- package/src/core/DocumentGenerator.ts +106 -26
- package/src/core/DocumentParser.ts +681 -114
- 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 +390 -46
- package/src/processors/SelectiveRevisionAcceptor.ts +164 -28
- package/src/processors/acceptRevisions.ts +67 -17
- 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
package/src/core/Document.ts
CHANGED
|
@@ -81,17 +81,22 @@ function getLogger(): ILogger {
|
|
|
81
81
|
}
|
|
82
82
|
// Raw XML revision acceptance - used at load time BEFORE parsing
|
|
83
83
|
// cleanupRevisionMetadata - cleanup metadata files after in-memory acceptance
|
|
84
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
acceptAllRevisions,
|
|
86
|
+
rejectAllRevisions,
|
|
87
|
+
cleanupRevisionMetadata,
|
|
88
|
+
} from '../processors/acceptRevisions.js';
|
|
85
89
|
// In-memory revision acceptance - used AFTER parsing, allows subsequent modifications
|
|
86
90
|
import { acceptRevisionsInMemory } from '../processors/InMemoryRevisionAcceptor.js';
|
|
87
91
|
import { stripTrackedChanges } from '../processors/stripTrackedChanges.js';
|
|
88
92
|
import { diffText, diffHasUnchangedParts } from '../utils/textDiff.js';
|
|
93
|
+
import { reorderRunPropertyChildren } from '../utils/xmlSanitization.js';
|
|
89
94
|
import { XMLBuilder } from '../xml/XMLBuilder.js';
|
|
90
95
|
import { XMLParser } from '../xml/XMLParser.js';
|
|
91
96
|
import { DocumentTrackingContext } from '../tracking/DocumentTrackingContext.js';
|
|
92
97
|
import type { TrackingContext } from '../tracking/TrackingContext.js';
|
|
93
98
|
import { ZipHandler } from '../zip/ZipHandler.js';
|
|
94
|
-
import { DOCX_PATHS } from '../zip/types.js';
|
|
99
|
+
import { DOCX_PATHS, SizeLimitOptions } from '../zip/types.js';
|
|
95
100
|
import { DocumentGenerator } from './DocumentGenerator.js';
|
|
96
101
|
import { DocumentIdManager } from './DocumentIdManager.js';
|
|
97
102
|
import { DocumentParser } from './DocumentParser.js';
|
|
@@ -102,7 +107,7 @@ import {
|
|
|
102
107
|
type DocumentEventType,
|
|
103
108
|
} from './DocumentEvents.js';
|
|
104
109
|
import { RelationshipManager } from './RelationshipManager.js';
|
|
105
|
-
import { RelationshipType } from './Relationship.js';
|
|
110
|
+
import { Relationship, RelationshipType } from './Relationship.js';
|
|
106
111
|
import { BodyElement } from './DocumentContent.js';
|
|
107
112
|
import { optimizeImage, ImageOptimizationResult } from '../images/ImageOptimizer.js';
|
|
108
113
|
|
|
@@ -156,7 +161,8 @@ export interface DocumentLoadOptions extends DocumentOptions {
|
|
|
156
161
|
*
|
|
157
162
|
* - 'preserve': Keep tracked changes as-is (may cause corruption if IDs conflict)
|
|
158
163
|
* - 'accept': Accept all changes - removes revision markup, keeps inserted content, removes deleted content (default)
|
|
159
|
-
* - '
|
|
164
|
+
* - 'reject': Reject all changes - reverts to the original pre-edit document: removes inserted content, restores deleted content, undoes moves, and restores previous formatting from property-change markers (the exact inverse of 'accept')
|
|
165
|
+
* - 'strip': Remove all revision markup, keeping inserted content and removing deleted content (same resulting text as 'accept')
|
|
160
166
|
*
|
|
161
167
|
* Default: 'accept' (prevents corruption from revision ID conflicts)
|
|
162
168
|
*
|
|
@@ -170,6 +176,9 @@ export interface DocumentLoadOptions extends DocumentOptions {
|
|
|
170
176
|
* // Explicit accept
|
|
171
177
|
* const doc = await Document.load('file.docx', { revisionHandling: 'accept' });
|
|
172
178
|
*
|
|
179
|
+
* // Reject all changes - revert to the original pre-edit document
|
|
180
|
+
* const doc = await Document.load('file.docx', { revisionHandling: 'reject' });
|
|
181
|
+
*
|
|
173
182
|
* // Strip all tracked changes
|
|
174
183
|
* const doc = await Document.load('file.docx', { revisionHandling: 'strip' });
|
|
175
184
|
*
|
|
@@ -177,7 +186,7 @@ export interface DocumentLoadOptions extends DocumentOptions {
|
|
|
177
186
|
* const doc = await Document.load('file.docx', { revisionHandling: 'preserve' });
|
|
178
187
|
* ```
|
|
179
188
|
*/
|
|
180
|
-
revisionHandling?: 'preserve' | 'accept' | 'strip';
|
|
189
|
+
revisionHandling?: 'preserve' | 'accept' | 'reject' | 'strip';
|
|
181
190
|
|
|
182
191
|
/**
|
|
183
192
|
* Accept all tracked changes after parsing using in-memory transformation.
|
|
@@ -209,6 +218,68 @@ export interface DocumentLoadOptions extends DocumentOptions {
|
|
|
209
218
|
* ```
|
|
210
219
|
*/
|
|
211
220
|
acceptRevisions?: boolean;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resource limits applied while reading the DOCX archive.
|
|
224
|
+
*
|
|
225
|
+
* Forwarded to the underlying ZIP reader to guard against availability
|
|
226
|
+
* attacks (zip bombs, entry-count amplification) when loading untrusted
|
|
227
|
+
* input. Defaults are intentionally generous so legitimate documents load
|
|
228
|
+
* unchanged; tighten the fields on {@link SizeLimitOptions} for untrusted
|
|
229
|
+
* sources.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* const doc = await Document.load('untrusted.docx', {
|
|
234
|
+
* sizeLimits: { maxTotalUncompressedMB: 100, maxEntryCount: 500 },
|
|
235
|
+
* });
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
sizeLimits?: SizeLimitOptions;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Options controlling {@link Document.toMarkdown} conversion fidelity.
|
|
243
|
+
*
|
|
244
|
+
* Markdown cannot natively express every Word construct, so the converter
|
|
245
|
+
* falls back to inline HTML (when {@link htmlFallback} is enabled) for things
|
|
246
|
+
* like underline, super/subscript, highlight and color, and collects
|
|
247
|
+
* footnotes/endnotes as GFM-style references. These options let callers tune
|
|
248
|
+
* how aggressively that extra information is preserved.
|
|
249
|
+
*/
|
|
250
|
+
export interface MarkdownConversionOptions {
|
|
251
|
+
/**
|
|
252
|
+
* Emit inline HTML for character formatting Markdown cannot represent:
|
|
253
|
+
* underline (`<u>`), superscript (`<sup>`), subscript (`<sub>`), highlight
|
|
254
|
+
* (`<mark>`) and text color (`<span style="color:#…">`). Tracked deletions
|
|
255
|
+
* (in `'preserve'` mode) are wrapped in `<del>`. When `false`, that
|
|
256
|
+
* formatting is dropped but the text itself is still emitted. Default: `true`.
|
|
257
|
+
*/
|
|
258
|
+
htmlFallback?: boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Collect footnote/endnote references as GFM footnote markers (`[^fn1]`,
|
|
261
|
+
* `[^en1]`) and append their definitions at the end of the output.
|
|
262
|
+
* Default: `true`.
|
|
263
|
+
*/
|
|
264
|
+
footnotes?: boolean;
|
|
265
|
+
/**
|
|
266
|
+
* Emit inline images as `` where `src` is the image's
|
|
267
|
+
* relationship id (or a placeholder for unsaved documents). When `false`,
|
|
268
|
+
* images are skipped. Default: `true`.
|
|
269
|
+
*/
|
|
270
|
+
images?: boolean;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Internal state threaded through a single Markdown conversion pass.
|
|
275
|
+
* @internal
|
|
276
|
+
*/
|
|
277
|
+
interface MarkdownContext {
|
|
278
|
+
opts: Required<MarkdownConversionOptions>;
|
|
279
|
+
/** Ordered, de-duplicated footnote/endnote definitions to append at the end. */
|
|
280
|
+
notes: { marker: string; text: string }[];
|
|
281
|
+
/** Markers already emitted, to avoid duplicate definitions. */
|
|
282
|
+
noteMarkers: Set<string>;
|
|
212
283
|
}
|
|
213
284
|
|
|
214
285
|
/**
|
|
@@ -249,6 +320,12 @@ export class Document {
|
|
|
249
320
|
() => this.documentIdManager.getNextId(),
|
|
250
321
|
(existingId) => this.documentIdManager.ensureNextIdAbove(existingId)
|
|
251
322
|
);
|
|
323
|
+
// Instance-level comment edits (resolve, setAuthor, addRun, ...) must
|
|
324
|
+
// flip the dirty flag, otherwise saveComments() takes the passthrough
|
|
325
|
+
// branch and writes the original comments.xml, silently dropping them
|
|
326
|
+
this.commentManager.setModifiedNotifier(() => {
|
|
327
|
+
this._commentsModified = true;
|
|
328
|
+
});
|
|
252
329
|
|
|
253
330
|
this.trackingContext = new DocumentTrackingContext(this.revisionManager);
|
|
254
331
|
this.parser = new DocumentParser();
|
|
@@ -324,6 +401,7 @@ export class Document {
|
|
|
324
401
|
private _originalNumberingXml?: string;
|
|
325
402
|
private _originalSettingsXml?: string;
|
|
326
403
|
private _originalAppPropsXml?: string;
|
|
404
|
+
private _originalCorePropsXml?: string;
|
|
327
405
|
private _originalFootnotesXml?: string;
|
|
328
406
|
private _originalEndnotesXml?: string;
|
|
329
407
|
private _originalCommentsXml?: string;
|
|
@@ -352,6 +430,9 @@ export class Document {
|
|
|
352
430
|
// Track whether app properties have been programmatically modified since load
|
|
353
431
|
private _appPropsModified = false;
|
|
354
432
|
|
|
433
|
+
// Track whether core properties have been programmatically modified since load
|
|
434
|
+
private _corePropsModified = false;
|
|
435
|
+
|
|
355
436
|
// Track whether footnotes/endnotes have been programmatically modified since load
|
|
356
437
|
private _footnotesModified = false;
|
|
357
438
|
private _endnotesModified = false;
|
|
@@ -785,7 +866,7 @@ export class Document {
|
|
|
785
866
|
logger.info('Loading document from file', { path: filePath });
|
|
786
867
|
|
|
787
868
|
const zipHandler = new ZipHandler();
|
|
788
|
-
await zipHandler.load(filePath);
|
|
869
|
+
await zipHandler.load(filePath, { sizeLimits: options?.sizeLimits });
|
|
789
870
|
|
|
790
871
|
const doc = await Document.initializeFromZip(zipHandler, options);
|
|
791
872
|
doc._events.emit('afterLoad', { source: 'file', path: filePath });
|
|
@@ -825,7 +906,7 @@ export class Document {
|
|
|
825
906
|
logger.info('Loading document from buffer', { bufferSize: buffer.length });
|
|
826
907
|
|
|
827
908
|
const zipHandler = new ZipHandler();
|
|
828
|
-
await zipHandler.loadFromBuffer(buffer);
|
|
909
|
+
await zipHandler.loadFromBuffer(buffer, { sizeLimits: options?.sizeLimits });
|
|
829
910
|
|
|
830
911
|
const doc = await Document.initializeFromZip(zipHandler, options);
|
|
831
912
|
doc._events.emit('afterLoad', { source: 'buffer' });
|
|
@@ -846,6 +927,17 @@ export class Document {
|
|
|
846
927
|
// If acceptRevisions is true, we need to preserve revisions during parsing
|
|
847
928
|
// so they can be accepted using in-memory transformation after parsing
|
|
848
929
|
const useInMemoryAccept = options?.acceptRevisions === true;
|
|
930
|
+
// `acceptRevisions: true` performs an in-memory ACCEPT, which is the exact
|
|
931
|
+
// opposite of `revisionHandling: 'reject'`. Honoring the in-memory accept
|
|
932
|
+
// gate would silently produce the accepted document when the caller asked
|
|
933
|
+
// to revert — surface the conflict instead of doing the opposite.
|
|
934
|
+
if (useInMemoryAccept && options?.revisionHandling === 'reject') {
|
|
935
|
+
throw new Error(
|
|
936
|
+
"Conflicting load options: 'acceptRevisions: true' (in-memory accept) cannot be combined " +
|
|
937
|
+
'with \'revisionHandling: "reject"\'. Use \'revisionHandling: "reject"\' on its own to ' +
|
|
938
|
+
'revert the document to its original pre-edit state.'
|
|
939
|
+
);
|
|
940
|
+
}
|
|
849
941
|
const revisionHandling = useInMemoryAccept
|
|
850
942
|
? 'preserve' // Force preserve so revisions are parsed into model
|
|
851
943
|
: (options?.revisionHandling ?? 'accept'); // Default to accept
|
|
@@ -854,6 +946,10 @@ export class Document {
|
|
|
854
946
|
if (revisionHandling === 'accept') {
|
|
855
947
|
// Accept all tracked changes to prevent corruption (raw XML approach)
|
|
856
948
|
await acceptAllRevisions(zipHandler);
|
|
949
|
+
} else if (revisionHandling === 'reject') {
|
|
950
|
+
// Reject all tracked changes - revert to the original pre-edit document
|
|
951
|
+
// (inverse of accept: drop insertions, restore deletions/moves/formatting)
|
|
952
|
+
await rejectAllRevisions(zipHandler);
|
|
857
953
|
} else if (revisionHandling === 'strip') {
|
|
858
954
|
// Strip all tracked changes completely
|
|
859
955
|
await stripTrackedChanges(zipHandler);
|
|
@@ -890,7 +986,10 @@ export class Document {
|
|
|
890
986
|
|
|
891
987
|
const numberingXml = zipHandler.getFileAsString(DOCX_PATHS.NUMBERING);
|
|
892
988
|
if (numberingXml) {
|
|
893
|
-
|
|
989
|
+
// Numbering definitions are written back verbatim for fidelity, so repair
|
|
990
|
+
// any out-of-order w:rPr children here (e.g. w:b/w:bCs appended after
|
|
991
|
+
// w:color/w:sz by some producers) — Word flags such ordering as corrupt.
|
|
992
|
+
doc._originalNumberingXml = reorderRunPropertyChildren(numberingXml);
|
|
894
993
|
}
|
|
895
994
|
|
|
896
995
|
// Preserve original settings.xml and parse managed settings into in-memory state
|
|
@@ -907,6 +1006,15 @@ export class Document {
|
|
|
907
1006
|
doc._originalAppPropsXml = appPropsXml;
|
|
908
1007
|
}
|
|
909
1008
|
|
|
1009
|
+
// Preserve original core.xml for round-trip fidelity. parseProperties only
|
|
1010
|
+
// extracts a fixed subset of OPC core properties; optional elements like
|
|
1011
|
+
// cp:lastPrinted, dc:identifier, and cp:version would otherwise be dropped
|
|
1012
|
+
// on a plain load→save round-trip.
|
|
1013
|
+
const corePropsXml = zipHandler.getFileAsString(DOCX_PATHS.CORE_PROPS);
|
|
1014
|
+
if (corePropsXml) {
|
|
1015
|
+
doc._originalCorePropsXml = corePropsXml;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
910
1018
|
// Preserve original webSettings.xml for round-trip fidelity (w:divs, flags, etc.)
|
|
911
1019
|
const webSettingsXml = zipHandler.getFileAsString(DOCX_PATHS.WEB_SETTINGS);
|
|
912
1020
|
if (webSettingsXml) {
|
|
@@ -1306,18 +1414,20 @@ export class Document {
|
|
|
1306
1414
|
|
|
1307
1415
|
// Register headers with HeaderFooterManager (deduplicate by rId)
|
|
1308
1416
|
// Multiple section property types may reference the same rId
|
|
1417
|
+
// The parsed filename (relationship target) is preserved so saveHeaders()
|
|
1418
|
+
// writes content back to the part the relationship actually points at
|
|
1309
1419
|
const registeredHeaderRIds = new Set<string>();
|
|
1310
|
-
for (const { header, relationshipId } of headersFooters.headers) {
|
|
1420
|
+
for (const { header, relationshipId, filename } of headersFooters.headers) {
|
|
1311
1421
|
if (registeredHeaderRIds.has(relationshipId)) continue;
|
|
1312
|
-
this.headerFooterManager.registerHeader(header, relationshipId);
|
|
1422
|
+
this.headerFooterManager.registerHeader(header, relationshipId, filename);
|
|
1313
1423
|
registeredHeaderRIds.add(relationshipId);
|
|
1314
1424
|
}
|
|
1315
1425
|
|
|
1316
1426
|
// Register footers with HeaderFooterManager (deduplicate by rId)
|
|
1317
1427
|
const registeredFooterRIds = new Set<string>();
|
|
1318
|
-
for (const { footer, relationshipId } of headersFooters.footers) {
|
|
1428
|
+
for (const { footer, relationshipId, filename } of headersFooters.footers) {
|
|
1319
1429
|
if (registeredFooterRIds.has(relationshipId)) continue;
|
|
1320
|
-
this.headerFooterManager.registerFooter(footer, relationshipId);
|
|
1430
|
+
this.headerFooterManager.registerFooter(footer, relationshipId, filename);
|
|
1321
1431
|
registeredFooterRIds.add(relationshipId);
|
|
1322
1432
|
}
|
|
1323
1433
|
} catch (headerFooterError) {
|
|
@@ -1413,7 +1523,13 @@ export class Document {
|
|
|
1413
1523
|
}
|
|
1414
1524
|
|
|
1415
1525
|
const parser = new DocumentParser();
|
|
1416
|
-
|
|
1526
|
+
// commentsExtended.xml carries the resolved state (w15:done) keyed by
|
|
1527
|
+
// each comment's last-paragraph paraId — CT_Comment itself declares no
|
|
1528
|
+
// done attribute, so isResolved() needs the companion part
|
|
1529
|
+
const comments = parser.parseCommentsXml(
|
|
1530
|
+
commentsXml,
|
|
1531
|
+
this._originalCommentCompanionFiles.get(DOCX_PATHS.COMMENTS_EXTENDED)
|
|
1532
|
+
);
|
|
1417
1533
|
|
|
1418
1534
|
// Register each comment with its existing ID
|
|
1419
1535
|
for (const comment of comments) {
|
|
@@ -1435,7 +1551,12 @@ export class Document {
|
|
|
1435
1551
|
|
|
1436
1552
|
try {
|
|
1437
1553
|
const parser = new DocumentParser();
|
|
1438
|
-
|
|
1554
|
+
// Hyperlink r:id targets live in the part-scoped rels file per OPC;
|
|
1555
|
+
// resolving them at parse time lets a real footnote edit regenerate
|
|
1556
|
+
// working links instead of dangling r:id references
|
|
1557
|
+
const relsXml = this.zipHandler.getFileAsString('word/_rels/footnotes.xml.rels');
|
|
1558
|
+
const partRels = relsXml ? RelationshipManager.fromXml(relsXml) : undefined;
|
|
1559
|
+
const footnotes = parser.parseFootnotesXml(footnotesXml, partRels);
|
|
1439
1560
|
|
|
1440
1561
|
for (const footnote of footnotes) {
|
|
1441
1562
|
const id = footnote.getId();
|
|
@@ -1461,7 +1582,9 @@ export class Document {
|
|
|
1461
1582
|
|
|
1462
1583
|
try {
|
|
1463
1584
|
const parser = new DocumentParser();
|
|
1464
|
-
const
|
|
1585
|
+
const relsXml = this.zipHandler.getFileAsString('word/_rels/endnotes.xml.rels');
|
|
1586
|
+
const partRels = relsXml ? RelationshipManager.fromXml(relsXml) : undefined;
|
|
1587
|
+
const endnotes = parser.parseEndnotesXml(endnotesXml, partRels);
|
|
1465
1588
|
|
|
1466
1589
|
for (const endnote of endnotes) {
|
|
1467
1590
|
const id = endnote.getId();
|
|
@@ -1647,12 +1770,18 @@ export class Document {
|
|
|
1647
1770
|
// When tracking enabled, bind context and wrap existing content in w:ins revisions
|
|
1648
1771
|
if (this.trackChangesEnabled && this.trackingContext.isEnabled()) {
|
|
1649
1772
|
this.bindTrackingToElement(paragraph);
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1773
|
+
const author = this.trackingContext.getAuthor();
|
|
1774
|
+
// Each top-level Run/Hyperlink is REPLACED by its insert revision in
|
|
1775
|
+
// place. Wrapping paragraph.getRuns() and appending the revision would
|
|
1776
|
+
// leave the originals live alongside the w:ins copy (text serialized
|
|
1777
|
+
// twice, unremovable by accept/reject) and would re-wrap runs already
|
|
1778
|
+
// inside existing revisions or hyperlinks.
|
|
1779
|
+
for (const item of paragraph.getContent()) {
|
|
1780
|
+
if (item instanceof Run || item instanceof Hyperlink) {
|
|
1781
|
+
const insertion = Revision.createInsertion(author, item);
|
|
1782
|
+
this.trackingContext.getRevisionManager().register(insertion);
|
|
1783
|
+
paragraph.replaceContent(item, [insertion]);
|
|
1784
|
+
}
|
|
1656
1785
|
}
|
|
1657
1786
|
}
|
|
1658
1787
|
|
|
@@ -2307,6 +2436,7 @@ export class Document {
|
|
|
2307
2436
|
*/
|
|
2308
2437
|
setTitle(title: string): this {
|
|
2309
2438
|
this.properties.title = title;
|
|
2439
|
+
this._corePropsModified = true;
|
|
2310
2440
|
return this;
|
|
2311
2441
|
}
|
|
2312
2442
|
|
|
@@ -2317,6 +2447,7 @@ export class Document {
|
|
|
2317
2447
|
*/
|
|
2318
2448
|
setSubject(subject: string): this {
|
|
2319
2449
|
this.properties.subject = subject;
|
|
2450
|
+
this._corePropsModified = true;
|
|
2320
2451
|
return this;
|
|
2321
2452
|
}
|
|
2322
2453
|
|
|
@@ -2327,6 +2458,7 @@ export class Document {
|
|
|
2327
2458
|
*/
|
|
2328
2459
|
setCreator(creator: string): this {
|
|
2329
2460
|
this.properties.creator = creator;
|
|
2461
|
+
this._corePropsModified = true;
|
|
2330
2462
|
return this;
|
|
2331
2463
|
}
|
|
2332
2464
|
|
|
@@ -2351,6 +2483,7 @@ export class Document {
|
|
|
2351
2483
|
*/
|
|
2352
2484
|
setKeywords(keywords: string): this {
|
|
2353
2485
|
this.properties.keywords = keywords;
|
|
2486
|
+
this._corePropsModified = true;
|
|
2354
2487
|
return this;
|
|
2355
2488
|
}
|
|
2356
2489
|
|
|
@@ -2361,6 +2494,7 @@ export class Document {
|
|
|
2361
2494
|
*/
|
|
2362
2495
|
setDescription(description: string): this {
|
|
2363
2496
|
this.properties.description = description;
|
|
2497
|
+
this._corePropsModified = true;
|
|
2364
2498
|
return this;
|
|
2365
2499
|
}
|
|
2366
2500
|
|
|
@@ -2371,6 +2505,7 @@ export class Document {
|
|
|
2371
2505
|
*/
|
|
2372
2506
|
setCategory(category: string): this {
|
|
2373
2507
|
this.properties.category = category;
|
|
2508
|
+
this._corePropsModified = true;
|
|
2374
2509
|
return this;
|
|
2375
2510
|
}
|
|
2376
2511
|
|
|
@@ -2381,6 +2516,7 @@ export class Document {
|
|
|
2381
2516
|
*/
|
|
2382
2517
|
setContentStatus(status: string): this {
|
|
2383
2518
|
this.properties.contentStatus = status;
|
|
2519
|
+
this._corePropsModified = true;
|
|
2384
2520
|
return this;
|
|
2385
2521
|
}
|
|
2386
2522
|
|
|
@@ -2845,7 +2981,7 @@ export class Document {
|
|
|
2845
2981
|
* console.log(doc.toPlainText());
|
|
2846
2982
|
* ```
|
|
2847
2983
|
*/
|
|
2848
|
-
static async loadFromBase64(base64: string, options?:
|
|
2984
|
+
static async loadFromBase64(base64: string, options?: DocumentLoadOptions): Promise<Document> {
|
|
2849
2985
|
const buffer = Buffer.from(base64, 'base64');
|
|
2850
2986
|
return Document.loadFromBuffer(buffer, options);
|
|
2851
2987
|
}
|
|
@@ -3327,6 +3463,7 @@ export class Document {
|
|
|
3327
3463
|
|
|
3328
3464
|
/**
|
|
3329
3465
|
* Updates the core properties with current values
|
|
3466
|
+
* Uses preservation strategy to maintain original metadata when unmodified
|
|
3330
3467
|
*/
|
|
3331
3468
|
private updateCoreProps(): void {
|
|
3332
3469
|
if (
|
|
@@ -3334,8 +3471,65 @@ export class Document {
|
|
|
3334
3471
|
this._removedParts.has('docProps/core.xml')
|
|
3335
3472
|
)
|
|
3336
3473
|
return;
|
|
3337
|
-
|
|
3338
|
-
|
|
3474
|
+
if (this._originalCorePropsXml && !this._corePropsModified) {
|
|
3475
|
+
// Preserve original as-is — no changes to core properties. Keeps
|
|
3476
|
+
// unparsed elements (cp:lastPrinted, dc:identifier, cp:version) intact.
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
if (this._originalCorePropsXml && this._corePropsModified) {
|
|
3480
|
+
// Merge: update only changed fields in original XML
|
|
3481
|
+
const mergedXml = this.mergeCorePropsWithOriginal();
|
|
3482
|
+
this.zipHandler.updateFile(DOCX_PATHS.CORE_PROPS, mergedXml);
|
|
3483
|
+
} else {
|
|
3484
|
+
// New document — generate from scratch
|
|
3485
|
+
const xml = this.generator.generateCoreProps(this.properties);
|
|
3486
|
+
this.zipHandler.updateFile(DOCX_PATHS.CORE_PROPS, xml);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
/**
|
|
3491
|
+
* Merges modified core properties with the original core.xml.
|
|
3492
|
+
* Updates only fields that have been programmatically changed, preserving
|
|
3493
|
+
* unparsed OPC core properties (cp:lastPrinted, dc:identifier, cp:version,
|
|
3494
|
+
* dcterms:created/modified, etc.) that the parser never extracts.
|
|
3495
|
+
*/
|
|
3496
|
+
private mergeCorePropsWithOriginal(): string {
|
|
3497
|
+
if (!this._originalCorePropsXml) {
|
|
3498
|
+
return this.generator.generateCoreProps(this.properties);
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
let xml = this._originalCorePropsXml;
|
|
3502
|
+
|
|
3503
|
+
// Each parsed core property maps to a single OOXML element. Replace the
|
|
3504
|
+
// existing element's text in place; if the element is absent, insert it
|
|
3505
|
+
// before the closing tag so the new value is not silently dropped.
|
|
3506
|
+
const mergeField = (tag: string, value: string | undefined): void => {
|
|
3507
|
+
if (value === undefined) return;
|
|
3508
|
+
const escaped = XMLBuilder.sanitizeXmlContent(value);
|
|
3509
|
+
const openClose = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?</${tag}>`);
|
|
3510
|
+
const selfClosing = new RegExp(`<${tag}\\b[^>]*/>`);
|
|
3511
|
+
if (openClose.test(xml)) {
|
|
3512
|
+
xml = xml.replace(openClose, `<${tag}>${escaped}</${tag}>`);
|
|
3513
|
+
} else if (selfClosing.test(xml)) {
|
|
3514
|
+
xml = xml.replace(selfClosing, `<${tag}>${escaped}</${tag}>`);
|
|
3515
|
+
} else {
|
|
3516
|
+
xml = xml.replace(
|
|
3517
|
+
/<\/cp:coreProperties>/,
|
|
3518
|
+
` <${tag}>${escaped}</${tag}>\n</cp:coreProperties>`
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
};
|
|
3522
|
+
|
|
3523
|
+
mergeField('dc:title', this.properties.title);
|
|
3524
|
+
mergeField('dc:subject', this.properties.subject);
|
|
3525
|
+
mergeField('dc:creator', this.properties.creator);
|
|
3526
|
+
mergeField('cp:keywords', this.properties.keywords);
|
|
3527
|
+
mergeField('dc:description', this.properties.description);
|
|
3528
|
+
mergeField('cp:category', this.properties.category);
|
|
3529
|
+
mergeField('cp:contentStatus', this.properties.contentStatus);
|
|
3530
|
+
mergeField('dc:language', this.properties.language);
|
|
3531
|
+
|
|
3532
|
+
return xml;
|
|
3339
3533
|
}
|
|
3340
3534
|
|
|
3341
3535
|
/**
|
|
@@ -3379,6 +3573,9 @@ export class Document {
|
|
|
3379
3573
|
xml = xml.replace(/<Company>[^<]*<\/Company>/, `<Company>${escaped}</Company>`);
|
|
3380
3574
|
} else if (xml.includes('<Company/>')) {
|
|
3381
3575
|
xml = xml.replace(/<Company\/>/, `<Company>${escaped}</Company>`);
|
|
3576
|
+
} else {
|
|
3577
|
+
// Tag fully absent: insert so the value is not silently dropped on save
|
|
3578
|
+
xml = xml.replace(/<\/Properties>/, ` <Company>${escaped}</Company>\n</Properties>`);
|
|
3382
3579
|
}
|
|
3383
3580
|
}
|
|
3384
3581
|
|
|
@@ -3390,6 +3587,12 @@ export class Document {
|
|
|
3390
3587
|
/<Application>[^<]*<\/Application>/,
|
|
3391
3588
|
`<Application>${escaped}</Application>`
|
|
3392
3589
|
);
|
|
3590
|
+
} else {
|
|
3591
|
+
// Tag fully absent: insert so the value is not silently dropped on save
|
|
3592
|
+
xml = xml.replace(
|
|
3593
|
+
/<\/Properties>/,
|
|
3594
|
+
` <Application>${escaped}</Application>\n</Properties>`
|
|
3595
|
+
);
|
|
3393
3596
|
}
|
|
3394
3597
|
}
|
|
3395
3598
|
|
|
@@ -3398,6 +3601,9 @@ export class Document {
|
|
|
3398
3601
|
const escaped = XMLBuilder.sanitizeXmlContent(this.properties.appVersion);
|
|
3399
3602
|
if (xml.includes('<AppVersion>')) {
|
|
3400
3603
|
xml = xml.replace(/<AppVersion>[^<]*<\/AppVersion>/, `<AppVersion>${escaped}</AppVersion>`);
|
|
3604
|
+
} else {
|
|
3605
|
+
// Tag fully absent: insert so the value is not silently dropped on save
|
|
3606
|
+
xml = xml.replace(/<\/Properties>/, ` <AppVersion>${escaped}</AppVersion>\n</Properties>`);
|
|
3401
3607
|
}
|
|
3402
3608
|
}
|
|
3403
3609
|
|
|
@@ -3406,6 +3612,9 @@ export class Document {
|
|
|
3406
3612
|
const escaped = XMLBuilder.sanitizeXmlContent(this.properties.manager);
|
|
3407
3613
|
if (xml.includes('<Manager>')) {
|
|
3408
3614
|
xml = xml.replace(/<Manager>[^<]*<\/Manager>/, `<Manager>${escaped}</Manager>`);
|
|
3615
|
+
} else {
|
|
3616
|
+
// Tag fully absent: insert so the value is not silently dropped on save
|
|
3617
|
+
xml = xml.replace(/<\/Properties>/, ` <Manager>${escaped}</Manager>\n</Properties>`);
|
|
3409
3618
|
}
|
|
3410
3619
|
}
|
|
3411
3620
|
|
|
@@ -3592,9 +3801,10 @@ export class Document {
|
|
|
3592
3801
|
* Merges modified styles with the original styles.xml
|
|
3593
3802
|
*
|
|
3594
3803
|
* This preserves all styles from the original document while only updating
|
|
3595
|
-
* styles that have been explicitly modified via addStyle()
|
|
3804
|
+
* styles that have been explicitly modified via addStyle() and stripping
|
|
3805
|
+
* styles that have been removed via removeStyle().
|
|
3596
3806
|
*
|
|
3597
|
-
* @returns Merged XML string with original styles + modified styles
|
|
3807
|
+
* @returns Merged XML string with original styles + modified styles - removed styles
|
|
3598
3808
|
* @private
|
|
3599
3809
|
*/
|
|
3600
3810
|
private mergeStylesWithOriginal(): string {
|
|
@@ -3603,9 +3813,10 @@ export class Document {
|
|
|
3603
3813
|
}
|
|
3604
3814
|
|
|
3605
3815
|
const modifiedStyleIds = this.stylesManager.getModifiedStyleIds();
|
|
3816
|
+
const removedStyleIds = this.stylesManager.getRemovedStyleIds();
|
|
3606
3817
|
|
|
3607
|
-
// If nothing was modified, return original as-is
|
|
3608
|
-
if (modifiedStyleIds.size === 0) {
|
|
3818
|
+
// If nothing was modified or removed, return original as-is
|
|
3819
|
+
if (modifiedStyleIds.size === 0 && removedStyleIds.size === 0) {
|
|
3609
3820
|
return this._originalStylesXml;
|
|
3610
3821
|
}
|
|
3611
3822
|
|
|
@@ -3646,6 +3857,16 @@ export class Document {
|
|
|
3646
3857
|
}
|
|
3647
3858
|
}
|
|
3648
3859
|
|
|
3860
|
+
// Strip removed style definitions — without this, removals never reach
|
|
3861
|
+
// the saved styles.xml (mirrors the numbering merge's removed-ID handling)
|
|
3862
|
+
for (const styleId of removedStyleIds) {
|
|
3863
|
+
const escapedStyleId = styleId.replace(/[.*+?^${}()|[\]\\"]/g, '\\$&');
|
|
3864
|
+
const removedPattern = new RegExp(
|
|
3865
|
+
`\\s*<w:style[^>]*\\sw:styleId="${escapedStyleId}"[^>]*>[\\s\\S]*?</w:style>`
|
|
3866
|
+
);
|
|
3867
|
+
resultXml = resultXml.replace(removedPattern, '');
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3649
3870
|
return resultXml;
|
|
3650
3871
|
}
|
|
3651
3872
|
|
|
@@ -4003,9 +4224,22 @@ export class Document {
|
|
|
4003
4224
|
|
|
4004
4225
|
// w:revisionView (#31 in CT_Settings)
|
|
4005
4226
|
const view = this.revisionViewSettings;
|
|
4006
|
-
// Only emit revisionView if it differs from defaults (all true)
|
|
4007
|
-
|
|
4008
|
-
|
|
4227
|
+
// Only emit revisionView if it differs from defaults (all true). showMarkup
|
|
4228
|
+
// and showComments default true when absent, so a parsed w:markup="0" /
|
|
4229
|
+
// w:comments="0" must also trigger emission or it would be dropped here.
|
|
4230
|
+
if (
|
|
4231
|
+
!view.showInsertionsAndDeletions ||
|
|
4232
|
+
!view.showFormatting ||
|
|
4233
|
+
!view.showInkAnnotations ||
|
|
4234
|
+
view.showMarkup === false ||
|
|
4235
|
+
view.showComments === false
|
|
4236
|
+
) {
|
|
4237
|
+
// CT_TrackChangesView §17.15.1.77: append w:markup / w:comments only when
|
|
4238
|
+
// explicitly false (absent = default true) so the reviewer's default
|
|
4239
|
+
// markup view survives round-trip, mirroring DocumentGenerator.
|
|
4240
|
+
const markupAttr = view.showMarkup === false ? ' w:markup="0"' : '';
|
|
4241
|
+
const commentsAttr = view.showComments === false ? ' w:comments="0"' : '';
|
|
4242
|
+
trackBlock += `\n <w:revisionView w:insDel="${view.showInsertionsAndDeletions ? '1' : '0'}" w:formatting="${view.showFormatting ? '1' : '0'}" w:inkAnnotations="${view.showInkAnnotations ? '1' : '0'}"${markupAttr}${commentsAttr}/>`;
|
|
4009
4243
|
}
|
|
4010
4244
|
|
|
4011
4245
|
// w:trackRevisions (#32)
|
|
@@ -4062,6 +4296,8 @@ export class Document {
|
|
|
4062
4296
|
const prot = this.documentProtection;
|
|
4063
4297
|
const esc = XMLBuilder.escapeXmlAttribute;
|
|
4064
4298
|
let protXml = `\n <w:documentProtection w:edit="${esc(prot.edit)}" w:enforcement="${prot.enforcement ? '1' : '0'}"`;
|
|
4299
|
+
// w:formatting (CT_DocProtect §17.15.1.29) — tri-state, emit explicit 0/1
|
|
4300
|
+
if (prot.formatting !== undefined) protXml += ` w:formatting="${prot.formatting ? '1' : '0'}"`;
|
|
4065
4301
|
if (prot.cryptProviderType) protXml += ` w:cryptProviderType="${esc(prot.cryptProviderType)}"`;
|
|
4066
4302
|
if (prot.cryptAlgorithmClass)
|
|
4067
4303
|
protXml += ` w:cryptAlgorithmClass="${esc(prot.cryptAlgorithmClass)}"`;
|
|
@@ -4072,6 +4308,12 @@ export class Document {
|
|
|
4072
4308
|
if (prot.cryptSpinCount) protXml += ` w:cryptSpinCount="${esc(String(prot.cryptSpinCount))}"`;
|
|
4073
4309
|
if (prot.hash) protXml += ` w:hash="${esc(prot.hash)}"`;
|
|
4074
4310
|
if (prot.salt) protXml += ` w:salt="${esc(prot.salt)}"`;
|
|
4311
|
+
// Modern Word 2013+ crypto attributes (ISO/IEC 29500-4 §13) — without
|
|
4312
|
+
// these a modern password-protected document loses its password material
|
|
4313
|
+
// whenever any settings-modifying API runs before save.
|
|
4314
|
+
if (prot.algorithmName) protXml += ` w:algorithmName="${esc(prot.algorithmName)}"`;
|
|
4315
|
+
if (prot.hashValue) protXml += ` w:hashValue="${esc(prot.hashValue)}"`;
|
|
4316
|
+
if (prot.saltValue) protXml += ` w:saltValue="${esc(prot.saltValue)}"`;
|
|
4075
4317
|
protXml += '/>';
|
|
4076
4318
|
|
|
4077
4319
|
// Insert before w:autoFormatOverride (#36) if exists,
|
|
@@ -5597,7 +5839,10 @@ export class Document {
|
|
|
5597
5839
|
* - numId references inside body-level tracked deletions (w:del blocks
|
|
5598
5840
|
* that were skipped during parsing)
|
|
5599
5841
|
* - Header, footer, footnote, and endnote XML files
|
|
5600
|
-
* -
|
|
5842
|
+
* - Style definitions (w:numPr in a style's pPr), since styles attach
|
|
5843
|
+
* list numbering without any paragraph carrying a direct numId
|
|
5844
|
+
* - Raw XML safety net scan of all relevant ZIP entries, including
|
|
5845
|
+
* styles.xml and comments.xml
|
|
5601
5846
|
*
|
|
5602
5847
|
* A definition is only removed if it appears in NONE of these sources.
|
|
5603
5848
|
*
|
|
@@ -5663,6 +5908,25 @@ export class Document {
|
|
|
5663
5908
|
}
|
|
5664
5909
|
}
|
|
5665
5910
|
|
|
5911
|
+
// 7. Scan style definitions for numPr references — Word attaches multilevel
|
|
5912
|
+
// list numbering to paragraph styles (e.g., Headings, list styles), so a
|
|
5913
|
+
// numId can be in use while no paragraph carries a direct w:numPr
|
|
5914
|
+
for (const style of this.stylesManager.getAllStyles()) {
|
|
5915
|
+
const styleNumId = style.getProperties().numPr?.numId;
|
|
5916
|
+
if (styleNumId !== undefined) {
|
|
5917
|
+
usedNumIds.add(styleNumId);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
|
|
5921
|
+
// 8. Raw safety net for styles.xml and comments.xml — preserved XML can
|
|
5922
|
+
// carry numId references not represented in the in-memory model
|
|
5923
|
+
for (const filePath of [DOCX_PATHS.STYLES, 'word/comments.xml']) {
|
|
5924
|
+
const xml = this.zipHandler.getFileAsString(filePath);
|
|
5925
|
+
if (xml) {
|
|
5926
|
+
this.collectNumIdsFromXml(xml, usedNumIds);
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
|
|
5666
5930
|
// Clean up numbering definitions not found in any of the above scans
|
|
5667
5931
|
this.numberingManager.cleanupUnusedNumbering(usedNumIds);
|
|
5668
5932
|
}
|
|
@@ -5868,7 +6132,69 @@ export class Document {
|
|
|
5868
6132
|
}
|
|
5869
6133
|
const registeredSet = new Set(existingRevisions);
|
|
5870
6134
|
|
|
5871
|
-
|
|
6135
|
+
// Preserved raw XML (revision-nested image runs, w:pict/chart passthrough run
|
|
6136
|
+
// content, preserved elements, nested-table passthrough) is emitted verbatim
|
|
6137
|
+
// with its original wp:docPr id, outside the renumbering below. Reassigned ids
|
|
6138
|
+
// must start above every id that survives serialization unchanged, otherwise a
|
|
6139
|
+
// renumbered drawing can duplicate a preserved one (wp:docPr/@id must be
|
|
6140
|
+
// unique per ECMA-376 Part 1 §20.4.2.5).
|
|
6141
|
+
let maxPreservedDocPrId = 0;
|
|
6142
|
+
const collectPreservedDocPrIds = (rawXml: string | undefined): void => {
|
|
6143
|
+
if (!rawXml || !rawXml.includes('wp:docPr')) return;
|
|
6144
|
+
const docPrPattern = /<wp:docPr\b[^>]*\bid="(\d+)"/g;
|
|
6145
|
+
let match: RegExpExecArray | null;
|
|
6146
|
+
while ((match = docPrPattern.exec(rawXml)) !== null) {
|
|
6147
|
+
const id = parseInt(match[1]!, 10);
|
|
6148
|
+
if (id > maxPreservedDocPrId) maxPreservedDocPrId = id;
|
|
6149
|
+
}
|
|
6150
|
+
};
|
|
6151
|
+
const collectFromRun = (run: Run): void => {
|
|
6152
|
+
if (run instanceof ImageRun) {
|
|
6153
|
+
collectPreservedDocPrIds(run.getRawRunXml());
|
|
6154
|
+
return;
|
|
6155
|
+
}
|
|
6156
|
+
for (const contentItem of run.getContent()) {
|
|
6157
|
+
collectPreservedDocPrIds(contentItem.rawXml);
|
|
6158
|
+
}
|
|
6159
|
+
};
|
|
6160
|
+
const collectFromParagraph = (para: Paragraph): void => {
|
|
6161
|
+
for (const item of para.getContent()) {
|
|
6162
|
+
if (item instanceof Revision) {
|
|
6163
|
+
for (const revItem of item.getContent()) {
|
|
6164
|
+
if (revItem instanceof Run) collectFromRun(revItem);
|
|
6165
|
+
}
|
|
6166
|
+
} else if (item instanceof Run) {
|
|
6167
|
+
collectFromRun(item);
|
|
6168
|
+
} else if (item instanceof PreservedElement) {
|
|
6169
|
+
collectPreservedDocPrIds(item.getRawXml());
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
};
|
|
6173
|
+
for (const element of this.bodyElements) {
|
|
6174
|
+
if (element instanceof Paragraph) {
|
|
6175
|
+
collectFromParagraph(element);
|
|
6176
|
+
} else if (element instanceof Table) {
|
|
6177
|
+
for (const row of element.getRows()) {
|
|
6178
|
+
for (const cell of row.getCells()) {
|
|
6179
|
+
for (const para of cell.getParagraphs()) {
|
|
6180
|
+
collectFromParagraph(para);
|
|
6181
|
+
}
|
|
6182
|
+
for (const nested of cell.getRawNestedContent()) {
|
|
6183
|
+
collectPreservedDocPrIds(nested.xml);
|
|
6184
|
+
}
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
} else {
|
|
6188
|
+
// Raw-XML passthrough body elements (AlternateContent, MathParagraph,
|
|
6189
|
+
// CustomXmlBlock, PreservedElement, registered elements) expose getRawXml().
|
|
6190
|
+
const passthrough = element as { getRawXml?: () => string };
|
|
6191
|
+
if (typeof passthrough.getRawXml === 'function') {
|
|
6192
|
+
collectPreservedDocPrIds(passthrough.getRawXml());
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
}
|
|
6196
|
+
|
|
6197
|
+
let docPrId = maxPreservedDocPrId + 1;
|
|
5872
6198
|
|
|
5873
6199
|
// Collect existing paraIds to avoid collisions when generating new ones
|
|
5874
6200
|
const existingParaIds = new Set<string>();
|
|
@@ -5906,6 +6232,16 @@ export class Document {
|
|
|
5906
6232
|
item.setDocPrId(docPrId++);
|
|
5907
6233
|
} else if (item instanceof ImageRun) {
|
|
5908
6234
|
item.getImageElement().setDocPrId(docPrId++);
|
|
6235
|
+
} else if (item instanceof Revision) {
|
|
6236
|
+
// Revision-nested images serialize from the model when no raw run XML
|
|
6237
|
+
// was captured (programmatic) or after a mutation splices a regenerated
|
|
6238
|
+
// w:drawing — give them fresh ids so they cannot collide with the
|
|
6239
|
+
// renumbered top-level drawings. Unmutated raw runs ignore the model id.
|
|
6240
|
+
for (const revItem of item.getContent()) {
|
|
6241
|
+
if (revItem instanceof ImageRun) {
|
|
6242
|
+
revItem.getImageElement().setDocPrId(docPrId++);
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
5909
6245
|
}
|
|
5910
6246
|
}
|
|
5911
6247
|
|
|
@@ -6631,6 +6967,10 @@ export class Document {
|
|
|
6631
6967
|
numLevel.setHangingIndent(360);
|
|
6632
6968
|
}
|
|
6633
6969
|
|
|
6970
|
+
// Level setters don't propagate dirty state; without this the save
|
|
6971
|
+
// pipeline writes the original numbering.xml verbatim for loaded docs
|
|
6972
|
+
this.numberingManager.markAbstractNumberingModified(abstractNumId);
|
|
6973
|
+
|
|
6634
6974
|
// Apply paragraph formatting to all paragraphs using this list
|
|
6635
6975
|
this.applyFormattingToListParagraphs(instance.getNumId());
|
|
6636
6976
|
count++;
|
|
@@ -6697,6 +7037,10 @@ export class Document {
|
|
|
6697
7037
|
numLevel.setAlignment('left');
|
|
6698
7038
|
}
|
|
6699
7039
|
|
|
7040
|
+
// Level setters don't propagate dirty state; without this the save
|
|
7041
|
+
// pipeline writes the original numbering.xml verbatim for loaded docs
|
|
7042
|
+
this.numberingManager.markAbstractNumberingModified(abstractNumId);
|
|
7043
|
+
|
|
6700
7044
|
// Apply paragraph formatting to all paragraphs using this list
|
|
6701
7045
|
this.applyFormattingToListParagraphs(instance.getNumId());
|
|
6702
7046
|
count++;
|
|
@@ -9778,8 +10122,11 @@ export class Document {
|
|
|
9778
10122
|
* @returns This document for chaining
|
|
9779
10123
|
*/
|
|
9780
10124
|
setHeader(header: Header): this {
|
|
9781
|
-
//
|
|
9782
|
-
|
|
10125
|
+
// The relationship must target the part name the manager assigns,
|
|
10126
|
+
// otherwise saveHeaders() writes content to a part nothing references
|
|
10127
|
+
const relationship = this.relationshipManager.addHeader(
|
|
10128
|
+
this.headerFooterManager.peekHeaderFilename(header)
|
|
10129
|
+
);
|
|
9783
10130
|
|
|
9784
10131
|
// Register with manager
|
|
9785
10132
|
this.headerFooterManager.registerHeader(header, relationship.getId());
|
|
@@ -9799,9 +10146,9 @@ export class Document {
|
|
|
9799
10146
|
// Enable title page
|
|
9800
10147
|
this.section.setTitlePage(true);
|
|
9801
10148
|
|
|
9802
|
-
//
|
|
10149
|
+
// Target the part name the manager will assign on registration
|
|
9803
10150
|
const relationship = this.relationshipManager.addHeader(
|
|
9804
|
-
|
|
10151
|
+
this.headerFooterManager.peekHeaderFilename(header)
|
|
9805
10152
|
);
|
|
9806
10153
|
|
|
9807
10154
|
// Register with manager
|
|
@@ -9819,9 +10166,9 @@ export class Document {
|
|
|
9819
10166
|
* @returns This document for chaining
|
|
9820
10167
|
*/
|
|
9821
10168
|
setEvenPageHeader(header: Header): this {
|
|
9822
|
-
//
|
|
10169
|
+
// Target the part name the manager will assign on registration
|
|
9823
10170
|
const relationship = this.relationshipManager.addHeader(
|
|
9824
|
-
|
|
10171
|
+
this.headerFooterManager.peekHeaderFilename(header)
|
|
9825
10172
|
);
|
|
9826
10173
|
|
|
9827
10174
|
// Register with manager
|
|
@@ -9839,8 +10186,11 @@ export class Document {
|
|
|
9839
10186
|
* @returns This document for chaining
|
|
9840
10187
|
*/
|
|
9841
10188
|
setFooter(footer: Footer): this {
|
|
9842
|
-
//
|
|
9843
|
-
|
|
10189
|
+
// The relationship must target the part name the manager assigns,
|
|
10190
|
+
// otherwise saveFooters() writes content to a part nothing references
|
|
10191
|
+
const relationship = this.relationshipManager.addFooter(
|
|
10192
|
+
this.headerFooterManager.peekFooterFilename(footer)
|
|
10193
|
+
);
|
|
9844
10194
|
|
|
9845
10195
|
// Register with manager
|
|
9846
10196
|
this.headerFooterManager.registerFooter(footer, relationship.getId());
|
|
@@ -9860,9 +10210,9 @@ export class Document {
|
|
|
9860
10210
|
// Enable title page
|
|
9861
10211
|
this.section.setTitlePage(true);
|
|
9862
10212
|
|
|
9863
|
-
//
|
|
10213
|
+
// Target the part name the manager will assign on registration
|
|
9864
10214
|
const relationship = this.relationshipManager.addFooter(
|
|
9865
|
-
|
|
10215
|
+
this.headerFooterManager.peekFooterFilename(footer)
|
|
9866
10216
|
);
|
|
9867
10217
|
|
|
9868
10218
|
// Register with manager
|
|
@@ -9880,9 +10230,9 @@ export class Document {
|
|
|
9880
10230
|
* @returns This document for chaining
|
|
9881
10231
|
*/
|
|
9882
10232
|
setEvenPageFooter(footer: Footer): this {
|
|
9883
|
-
//
|
|
10233
|
+
// Target the part name the manager will assign on registration
|
|
9884
10234
|
const relationship = this.relationshipManager.addFooter(
|
|
9885
|
-
|
|
10235
|
+
this.headerFooterManager.peekFooterFilename(footer)
|
|
9886
10236
|
);
|
|
9887
10237
|
|
|
9888
10238
|
// Register with manager
|
|
@@ -10188,6 +10538,26 @@ export class Document {
|
|
|
10188
10538
|
}
|
|
10189
10539
|
// Remove companion files since regenerated comments lack w14:paraId
|
|
10190
10540
|
this.removeCommentCompanionFiles();
|
|
10541
|
+
|
|
10542
|
+
// Resolved state lives in commentsExtended.xml (w15:commentEx
|
|
10543
|
+
// w15:done) — CT_Comment declares no done attribute, so the part is
|
|
10544
|
+
// regenerated alongside comments.xml whenever a comment is resolved
|
|
10545
|
+
const extendedXml = this.commentManager.generateCommentsExtendedXml();
|
|
10546
|
+
if (extendedXml) {
|
|
10547
|
+
this.zipHandler.addFile(DOCX_PATHS.COMMENTS_EXTENDED, extendedXml);
|
|
10548
|
+
const hasExtendedRel = this.relationshipManager
|
|
10549
|
+
.getAllRelationships()
|
|
10550
|
+
.some((rel) => rel.getTarget() === 'commentsExtended.xml');
|
|
10551
|
+
if (!hasExtendedRel) {
|
|
10552
|
+
this.relationshipManager.addRelationship(
|
|
10553
|
+
Relationship.create({
|
|
10554
|
+
id: this.relationshipManager.generateId(),
|
|
10555
|
+
type: 'http://schemas.microsoft.com/office/2011/relationships/commentsExtended',
|
|
10556
|
+
target: 'commentsExtended.xml',
|
|
10557
|
+
})
|
|
10558
|
+
);
|
|
10559
|
+
}
|
|
10560
|
+
}
|
|
10191
10561
|
} else if (this._originalCommentsXml) {
|
|
10192
10562
|
// Passthrough — preserve original comments.xml exactly
|
|
10193
10563
|
this.zipHandler.addFile('word/comments.xml', this._originalCommentsXml);
|
|
@@ -10236,7 +10606,14 @@ export class Document {
|
|
|
10236
10606
|
}
|
|
10237
10607
|
|
|
10238
10608
|
private saveFootnotes(): void {
|
|
10239
|
-
|
|
10609
|
+
// Dirty flag gates regeneration (mirrors saveComments) so an unmodified
|
|
10610
|
+
// loaded footnotes.xml round-trips via passthrough; the count check only
|
|
10611
|
+
// covers footnotes registered directly on the manager for documents that
|
|
10612
|
+
// never had a footnotes part.
|
|
10613
|
+
if (
|
|
10614
|
+
this._footnotesModified ||
|
|
10615
|
+
(!this._originalFootnotesXml && this.footnoteManager.getCount() > 0)
|
|
10616
|
+
) {
|
|
10240
10617
|
const xml = this.footnoteManager.generateFootnotesXml();
|
|
10241
10618
|
this.zipHandler.addFile(DOCX_PATHS.FOOTNOTES, xml);
|
|
10242
10619
|
|
|
@@ -10267,7 +10644,11 @@ export class Document {
|
|
|
10267
10644
|
}
|
|
10268
10645
|
|
|
10269
10646
|
private saveEndnotes(): void {
|
|
10270
|
-
|
|
10647
|
+
// Same passthrough gating as saveFootnotes
|
|
10648
|
+
if (
|
|
10649
|
+
this._endnotesModified ||
|
|
10650
|
+
(!this._originalEndnotesXml && this.endnoteManager.getCount() > 0)
|
|
10651
|
+
) {
|
|
10271
10652
|
const xml = this.endnoteManager.generateEndnotesXml();
|
|
10272
10653
|
this.zipHandler.addFile(DOCX_PATHS.ENDNOTES, xml);
|
|
10273
10654
|
|
|
@@ -10364,10 +10745,21 @@ export class Document {
|
|
|
10364
10745
|
|
|
10365
10746
|
this.zipHandler.addFile(relsPath, xml);
|
|
10366
10747
|
|
|
10367
|
-
// Remove these relationships from the main RelationshipManager
|
|
10368
|
-
//
|
|
10748
|
+
// Remove these relationships from the main RelationshipManager so they
|
|
10749
|
+
// don't appear in document.xml.rels. Per OPC, relationship IDs are
|
|
10750
|
+
// part-scoped: a loaded header/footnote hyperlink keeps an ID from its
|
|
10751
|
+
// own .rels file that may collide with an unrelated document.xml.rels
|
|
10752
|
+
// entry (styles, settings, an image). Only remove when the main entry
|
|
10753
|
+
// is genuinely the same hyperlink (type and target match).
|
|
10369
10754
|
for (const rel of hyperlinkRels) {
|
|
10370
|
-
this.relationshipManager.
|
|
10755
|
+
const mainRel = this.relationshipManager.getRelationship(rel.id);
|
|
10756
|
+
if (
|
|
10757
|
+
mainRel &&
|
|
10758
|
+
mainRel.getType() === RelationshipType.HYPERLINK &&
|
|
10759
|
+
mainRel.getTarget() === rel.url
|
|
10760
|
+
) {
|
|
10761
|
+
this.relationshipManager.removeRelationship(rel.id);
|
|
10762
|
+
}
|
|
10371
10763
|
}
|
|
10372
10764
|
}
|
|
10373
10765
|
}
|
|
@@ -10531,20 +10923,32 @@ export class Document {
|
|
|
10531
10923
|
const defaults = new Set<string>();
|
|
10532
10924
|
const overrides = new Set<string>();
|
|
10533
10925
|
|
|
10534
|
-
//
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10926
|
+
// Per XML 1.0, attribute order is insignificant, values may use single or
|
|
10927
|
+
// double quotes, and empty elements may be expanded (<Override ...></Override>).
|
|
10928
|
+
// Match each opening tag and pull attributes out individually so spec-valid
|
|
10929
|
+
// packages from non-Word producers keep their entries on save (the generator
|
|
10930
|
+
// regenerates [Content_Types].xml; unmatched entries would be silently lost).
|
|
10931
|
+
const readAttr = (tag: string, name: string): string | undefined => {
|
|
10932
|
+
const match = tag.match(new RegExp(`(?:^|\\s)${name}\\s*=\\s*("([^"]*)"|'([^']*)')`));
|
|
10933
|
+
if (!match) return undefined;
|
|
10934
|
+
// Unescape so re-serialization (which escapes) doesn't double-escape
|
|
10935
|
+
return XMLBuilder.unescapeXml(match[2] ?? match[3] ?? '');
|
|
10936
|
+
};
|
|
10937
|
+
|
|
10938
|
+
for (const tagMatch of xml.matchAll(/<Default\b[^>]*>/g)) {
|
|
10939
|
+
const ext = readAttr(tagMatch[0], 'Extension');
|
|
10940
|
+
const contentType = readAttr(tagMatch[0], 'ContentType');
|
|
10941
|
+
if (ext && contentType) {
|
|
10942
|
+
defaults.add(`${ext}|${contentType}`); // Store as "ext|mimetype"
|
|
10943
|
+
}
|
|
10540
10944
|
}
|
|
10541
10945
|
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10946
|
+
for (const tagMatch of xml.matchAll(/<Override\b[^>]*>/g)) {
|
|
10947
|
+
const partName = readAttr(tagMatch[0], 'PartName');
|
|
10948
|
+
const contentType = readAttr(tagMatch[0], 'ContentType');
|
|
10949
|
+
if (partName && contentType) {
|
|
10950
|
+
overrides.add(`${partName}|${contentType}`); // Store as "path|mimetype"
|
|
10951
|
+
}
|
|
10548
10952
|
}
|
|
10549
10953
|
|
|
10550
10954
|
return { defaults, overrides };
|
|
@@ -11509,6 +11913,12 @@ export class Document {
|
|
|
11509
11913
|
*/
|
|
11510
11914
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11511
11915
|
private bindTrackingToElement(element: any): void {
|
|
11916
|
+
// Two-arm coverage: the getContent() arm below binds any paragraph-content
|
|
11917
|
+
// item exposing _setTrackingContext (Hyperlink, ComplexField, etc.) so their
|
|
11918
|
+
// own setText() emits tracked markup; the getRuns() arm binds the inner Runs
|
|
11919
|
+
// that actually carry the text. Both arms are required — neither subsumes the
|
|
11920
|
+
// other — to fully wire a paragraph's tracking context.
|
|
11921
|
+
|
|
11512
11922
|
// Set tracking context on element if it supports it
|
|
11513
11923
|
if (element && typeof element._setTrackingContext === 'function') {
|
|
11514
11924
|
element._setTrackingContext(this.trackingContext);
|
|
@@ -11528,14 +11938,13 @@ export class Document {
|
|
|
11528
11938
|
}
|
|
11529
11939
|
}
|
|
11530
11940
|
|
|
11531
|
-
// Bind to
|
|
11941
|
+
// Bind to content items that accept a tracking context (Hyperlink,
|
|
11942
|
+
// ComplexField, etc.). Hyperlink.setText() consults its own
|
|
11943
|
+
// trackingContext, so it must be bound here too; re-binding the inner
|
|
11944
|
+
// Runs handled above is idempotent.
|
|
11532
11945
|
if (element && typeof element.getContent === 'function') {
|
|
11533
11946
|
for (const item of element.getContent()) {
|
|
11534
|
-
if (
|
|
11535
|
-
item &&
|
|
11536
|
-
typeof item._setTrackingContext === 'function' &&
|
|
11537
|
-
typeof item.getInstruction === 'function'
|
|
11538
|
-
) {
|
|
11947
|
+
if (item && typeof item._setTrackingContext === 'function') {
|
|
11539
11948
|
item._setTrackingContext(this.trackingContext);
|
|
11540
11949
|
}
|
|
11541
11950
|
}
|
|
@@ -11876,6 +12285,10 @@ export class Document {
|
|
|
11876
12285
|
updated.size = sizeInPoints;
|
|
11877
12286
|
}
|
|
11878
12287
|
normalStyle.setRunFormatting(updated);
|
|
12288
|
+
// Re-register so the style lands in _modifiedStyleIds — otherwise
|
|
12289
|
+
// mergeStylesWithOriginal() returns the original styles.xml verbatim
|
|
12290
|
+
// for loaded documents and the change is silently dropped
|
|
12291
|
+
this.stylesManager.addStyle(normalStyle);
|
|
11879
12292
|
return this;
|
|
11880
12293
|
}
|
|
11881
12294
|
|
|
@@ -11904,6 +12317,10 @@ export class Document {
|
|
|
11904
12317
|
|
|
11905
12318
|
const existing = normalStyle.getRunFormatting() ?? {};
|
|
11906
12319
|
normalStyle.setRunFormatting({ ...existing, size: sizeInPoints });
|
|
12320
|
+
// Re-register so the style lands in _modifiedStyleIds — otherwise
|
|
12321
|
+
// mergeStylesWithOriginal() returns the original styles.xml verbatim
|
|
12322
|
+
// for loaded documents and the change is silently dropped
|
|
12323
|
+
this.stylesManager.addStyle(normalStyle);
|
|
11907
12324
|
return this;
|
|
11908
12325
|
}
|
|
11909
12326
|
|
|
@@ -12438,6 +12855,70 @@ export class Document {
|
|
|
12438
12855
|
this._endnotesModified = true;
|
|
12439
12856
|
}
|
|
12440
12857
|
|
|
12858
|
+
/**
|
|
12859
|
+
* Removes a footnote definition and every w:footnoteReference run pointing
|
|
12860
|
+
* at it in the body. Per ECMA-376 §17.11.14 each reference must resolve to
|
|
12861
|
+
* a footnote definition, so the references are stripped together with the
|
|
12862
|
+
* definition; the dirty flag ensures footnotes.xml is regenerated instead
|
|
12863
|
+
* of being restored from the original passthrough XML.
|
|
12864
|
+
* @param id - Footnote ID
|
|
12865
|
+
* @returns True if removed, false if not found or a special separator type
|
|
12866
|
+
*/
|
|
12867
|
+
removeFootnote(id: number): boolean {
|
|
12868
|
+
const removed = this.footnoteManager.removeFootnote(id);
|
|
12869
|
+
if (removed) {
|
|
12870
|
+
this._footnotesModified = true;
|
|
12871
|
+
this.removeNoteReferenceRuns('footnoteReference', id);
|
|
12872
|
+
}
|
|
12873
|
+
return removed;
|
|
12874
|
+
}
|
|
12875
|
+
|
|
12876
|
+
/**
|
|
12877
|
+
* Removes an endnote definition and every w:endnoteReference run pointing
|
|
12878
|
+
* at it in the body. Same reference-consistency contract as
|
|
12879
|
+
* {@link removeFootnote} (ECMA-376 §17.11.3).
|
|
12880
|
+
* @param id - Endnote ID
|
|
12881
|
+
* @returns True if removed, false if not found or a special separator type
|
|
12882
|
+
*/
|
|
12883
|
+
removeEndnote(id: number): boolean {
|
|
12884
|
+
const removed = this.endnoteManager.removeEndnote(id);
|
|
12885
|
+
if (removed) {
|
|
12886
|
+
this._endnotesModified = true;
|
|
12887
|
+
this.removeNoteReferenceRuns('endnoteReference', id);
|
|
12888
|
+
}
|
|
12889
|
+
return removed;
|
|
12890
|
+
}
|
|
12891
|
+
|
|
12892
|
+
/**
|
|
12893
|
+
* Strips body runs containing a footnote/endnote reference to the given
|
|
12894
|
+
* note ID so the saved document.xml never references a deleted definition.
|
|
12895
|
+
*/
|
|
12896
|
+
private removeNoteReferenceRuns(
|
|
12897
|
+
refType: 'footnoteReference' | 'endnoteReference',
|
|
12898
|
+
id: number
|
|
12899
|
+
): void {
|
|
12900
|
+
for (const para of this.getAllParagraphs()) {
|
|
12901
|
+
const content = para.getContent();
|
|
12902
|
+
// Iterate backwards so removals don't shift pending indices
|
|
12903
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
12904
|
+
const item = content[i];
|
|
12905
|
+
if (!(item instanceof Run)) {
|
|
12906
|
+
continue;
|
|
12907
|
+
}
|
|
12908
|
+
const matches = item
|
|
12909
|
+
.getContent()
|
|
12910
|
+
.some(
|
|
12911
|
+
(c) =>
|
|
12912
|
+
c.type === refType &&
|
|
12913
|
+
(refType === 'footnoteReference' ? c.footnoteId === id : c.endnoteId === id)
|
|
12914
|
+
);
|
|
12915
|
+
if (matches) {
|
|
12916
|
+
para.removeContentAt(i);
|
|
12917
|
+
}
|
|
12918
|
+
}
|
|
12919
|
+
}
|
|
12920
|
+
}
|
|
12921
|
+
|
|
12441
12922
|
/**
|
|
12442
12923
|
* Adds a comment to a paragraph (wraps the entire paragraph)
|
|
12443
12924
|
* Creates the comment if text is provided, or uses an existing comment object
|
|
@@ -12594,13 +13075,70 @@ export class Document {
|
|
|
12594
13075
|
* @returns True if the comment was removed
|
|
12595
13076
|
*/
|
|
12596
13077
|
removeComment(id: number): boolean {
|
|
13078
|
+
// Capture reply IDs before removal — CommentManager.removeComment deletes
|
|
13079
|
+
// them too, and their body anchors must be scrubbed along with the parent's
|
|
13080
|
+
const replyIds = this.commentManager.getReplies(id).map((reply) => reply.getId());
|
|
12597
13081
|
const removed = this.commentManager.removeComment(id);
|
|
12598
13082
|
if (removed) {
|
|
13083
|
+
this.removeCommentAnchors([id, ...replyIds]);
|
|
12599
13084
|
this._commentsModified = true;
|
|
12600
13085
|
}
|
|
12601
13086
|
return removed;
|
|
12602
13087
|
}
|
|
12603
13088
|
|
|
13089
|
+
/**
|
|
13090
|
+
* Strips comment anchors (range markers and reference runs) for the given
|
|
13091
|
+
* comment IDs from every paragraph in the body, headers, footers, footnotes,
|
|
13092
|
+
* and endnotes. Without this, deleting a comment definition leaves
|
|
13093
|
+
* w:commentRangeStart/End and w:commentReference pointing at a comment that
|
|
13094
|
+
* no longer exists in comments.xml — Word reports such documents as
|
|
13095
|
+
* containing unreadable content.
|
|
13096
|
+
*/
|
|
13097
|
+
private removeCommentAnchors(commentIds: number[]): void {
|
|
13098
|
+
const strip = (paragraphs: Paragraph[]): boolean => {
|
|
13099
|
+
let removed = false;
|
|
13100
|
+
for (const paragraph of paragraphs) {
|
|
13101
|
+
for (const commentId of commentIds) {
|
|
13102
|
+
if (paragraph.removeCommentAnchor(commentId)) {
|
|
13103
|
+
removed = true;
|
|
13104
|
+
}
|
|
13105
|
+
}
|
|
13106
|
+
}
|
|
13107
|
+
return removed;
|
|
13108
|
+
};
|
|
13109
|
+
|
|
13110
|
+
strip(this.getAllParagraphs());
|
|
13111
|
+
|
|
13112
|
+
for (const entry of this.headerFooterManager.getAllHeaders()) {
|
|
13113
|
+
strip(this.extractParagraphsFromElements(entry.header.getElements()));
|
|
13114
|
+
}
|
|
13115
|
+
for (const entry of this.headerFooterManager.getAllFooters()) {
|
|
13116
|
+
strip(this.extractParagraphsFromElements(entry.footer.getElements()));
|
|
13117
|
+
}
|
|
13118
|
+
|
|
13119
|
+
// Footnote/endnote parts are raw-XML passthrough unless flagged modified,
|
|
13120
|
+
// so anchor removal must mark them dirty to take effect on save
|
|
13121
|
+
let footnotesChanged = false;
|
|
13122
|
+
for (const footnote of this.footnoteManager.getAllFootnotes()) {
|
|
13123
|
+
if (strip(footnote.getParagraphs())) {
|
|
13124
|
+
footnotesChanged = true;
|
|
13125
|
+
}
|
|
13126
|
+
}
|
|
13127
|
+
if (footnotesChanged) {
|
|
13128
|
+
this._footnotesModified = true;
|
|
13129
|
+
}
|
|
13130
|
+
|
|
13131
|
+
let endnotesChanged = false;
|
|
13132
|
+
for (const endnote of this.endnoteManager.getAllEndnotes()) {
|
|
13133
|
+
if (strip(endnote.getParagraphs())) {
|
|
13134
|
+
endnotesChanged = true;
|
|
13135
|
+
}
|
|
13136
|
+
}
|
|
13137
|
+
if (endnotesChanged) {
|
|
13138
|
+
this._endnotesModified = true;
|
|
13139
|
+
}
|
|
13140
|
+
}
|
|
13141
|
+
|
|
12604
13142
|
/**
|
|
12605
13143
|
* Checks if there are any revisions in the document
|
|
12606
13144
|
* @returns True if there are no revisions
|
|
@@ -13342,8 +13880,10 @@ export class Document {
|
|
|
13342
13880
|
// Clear all managers using their clear() methods
|
|
13343
13881
|
this.stylesManager.clear();
|
|
13344
13882
|
this.numberingManager.clear();
|
|
13345
|
-
|
|
13883
|
+
// Release buffers before clear() empties the images map; the reverse order
|
|
13884
|
+
// would leave releaseAllImageData() iterating zero entries.
|
|
13346
13885
|
this.imageManager.releaseAllImageData();
|
|
13886
|
+
this.imageManager.clear();
|
|
13347
13887
|
this.relationshipManager.clear();
|
|
13348
13888
|
this.headerFooterManager.clear();
|
|
13349
13889
|
this.bookmarkManager.clear();
|
|
@@ -13364,6 +13904,7 @@ export class Document {
|
|
|
13364
13904
|
this._originalNumberingXml = undefined;
|
|
13365
13905
|
this._originalSettingsXml = undefined;
|
|
13366
13906
|
this._originalAppPropsXml = undefined;
|
|
13907
|
+
this._originalCorePropsXml = undefined;
|
|
13367
13908
|
this._originalFootnotesXml = undefined;
|
|
13368
13909
|
this._originalEndnotesXml = undefined;
|
|
13369
13910
|
this._originalCommentsXml = undefined;
|
|
@@ -13372,6 +13913,7 @@ export class Document {
|
|
|
13372
13913
|
this._originalContentTypes = undefined;
|
|
13373
13914
|
this._settingsModified = false;
|
|
13374
13915
|
this._appPropsModified = false;
|
|
13916
|
+
this._corePropsModified = false;
|
|
13375
13917
|
this._footnotesModified = false;
|
|
13376
13918
|
this._endnotesModified = false;
|
|
13377
13919
|
this._originalWebSettingsXml = undefined;
|
|
@@ -14614,7 +15156,9 @@ export class Document {
|
|
|
14614
15156
|
const matches = originalText.match(wordPattern);
|
|
14615
15157
|
if (matches) {
|
|
14616
15158
|
replacementCount += matches.length;
|
|
14617
|
-
|
|
15159
|
+
// Replacer function inserts `replace` literally; passing the raw
|
|
15160
|
+
// string would expand $-substitution tokens ($&, $', $`, $$).
|
|
15161
|
+
newText = originalText.replace(wordPattern, () => replace);
|
|
14618
15162
|
}
|
|
14619
15163
|
} else {
|
|
14620
15164
|
// Simple substring replacement
|
|
@@ -14625,7 +15169,9 @@ export class Document {
|
|
|
14625
15169
|
const matches = originalText.match(searchPattern);
|
|
14626
15170
|
if (matches) {
|
|
14627
15171
|
replacementCount += matches.length;
|
|
14628
|
-
|
|
15172
|
+
// Replacer function inserts `replace` literally; passing the raw
|
|
15173
|
+
// string would expand $-substitution tokens ($&, $', $`, $$).
|
|
15174
|
+
newText = originalText.replace(searchPattern, () => replace);
|
|
14629
15175
|
}
|
|
14630
15176
|
}
|
|
14631
15177
|
|
|
@@ -15253,6 +15799,13 @@ export class Document {
|
|
|
15253
15799
|
* @param separator - String to insert between paragraphs (default: '\n')
|
|
15254
15800
|
* @returns Plain text content of the entire document
|
|
15255
15801
|
*
|
|
15802
|
+
* @remarks
|
|
15803
|
+
* The output depends on the `revisionHandling` mode the document was loaded with, and reflects any tracked changes applied to the document after loading.
|
|
15804
|
+
* In `'preserve'` mode the result now includes BOTH tracked-inserted (`w:ins`) and
|
|
15805
|
+
* tracked-deleted (`w:delText`) text, interleaved in document order. In
|
|
15806
|
+
* `'accept'`/`'strip'` modes (the default) the output is the post-acceptance plain
|
|
15807
|
+
* text — inserted content kept, deleted content dropped.
|
|
15808
|
+
*
|
|
15256
15809
|
* @example
|
|
15257
15810
|
* ```typescript
|
|
15258
15811
|
* const text = doc.toPlainText();
|
|
@@ -15264,23 +15817,37 @@ export class Document {
|
|
|
15264
15817
|
*/
|
|
15265
15818
|
toPlainText(separator = '\n'): string {
|
|
15266
15819
|
const paragraphs = this.getAllParagraphs();
|
|
15267
|
-
|
|
15820
|
+
// Use the revision-aware path so preserve-mode documents retain the text
|
|
15821
|
+
// carried by tracked changes (inserted w:t and deleted w:delText) instead
|
|
15822
|
+
// of silently dropping it.
|
|
15823
|
+
return paragraphs.map((p) => p.getTextIncludingRevisions()).join(separator);
|
|
15268
15824
|
}
|
|
15269
15825
|
|
|
15270
15826
|
/**
|
|
15271
15827
|
* Converts the document to Markdown format
|
|
15272
15828
|
*
|
|
15273
|
-
*
|
|
15274
|
-
*
|
|
15275
|
-
*
|
|
15276
|
-
* -
|
|
15277
|
-
* -
|
|
15278
|
-
* -
|
|
15279
|
-
*
|
|
15280
|
-
*
|
|
15281
|
-
*
|
|
15829
|
+
* Walks the body in order and renders every information-bearing construct,
|
|
15830
|
+
* falling back to inline HTML where Markdown has no native equivalent so
|
|
15831
|
+
* that no content is silently dropped:
|
|
15832
|
+
* - Headings → `#` … `######`
|
|
15833
|
+
* - Bold/italic/strikethrough → `**` / `*` / `~~`; monospace fonts → `` `code` ``
|
|
15834
|
+
* - Underline/super/subscript/highlight/color → inline HTML (`<u>`, `<sup>`,
|
|
15835
|
+
* `<sub>`, `<mark>`, `<span style="color:#…">`) when {@link MarkdownConversionOptions.htmlFallback}
|
|
15836
|
+
* - Hyperlinks → `[text](url)` (internal links resolve to `#anchor`)
|
|
15837
|
+
* - Inline images → ``
|
|
15838
|
+
* - Footnote/endnote references → `[^fn1]` / `[^en1]` with definitions appended
|
|
15839
|
+
* - Line breaks → `<br>` (or newline), tabs preserved
|
|
15840
|
+
* - Numbered/bulleted lists → `1.` / `-` with two-space indentation per nesting level
|
|
15841
|
+
* - Block quotes (Quote styles) → `>` prefixes
|
|
15842
|
+
* - Tables → GFM pipe tables; tables with merged or nested cells → inline HTML
|
|
15843
|
+
* - Field results, shapes, text boxes, structured document tags and preserved
|
|
15844
|
+
* elements → their textual content
|
|
15845
|
+
* - Tracked insertions (preserve mode) render inline; deletions wrap in `~~`/`<del>`
|
|
15846
|
+
*
|
|
15847
|
+
* Useful for document processing pipelines, content migration, documentation
|
|
15282
15848
|
* generation, and plain-text extraction with structure preserved.
|
|
15283
15849
|
*
|
|
15850
|
+
* @param options - Conversion fidelity options (see {@link MarkdownConversionOptions})
|
|
15284
15851
|
* @returns Markdown string representation of the document
|
|
15285
15852
|
*
|
|
15286
15853
|
* @example
|
|
@@ -15298,139 +15865,495 @@ export class Document {
|
|
|
15298
15865
|
* // | Alice | 30 |
|
|
15299
15866
|
* ```
|
|
15300
15867
|
*/
|
|
15301
|
-
toMarkdown(): string {
|
|
15302
|
-
const
|
|
15868
|
+
toMarkdown(options?: MarkdownConversionOptions): string {
|
|
15869
|
+
const ctx: MarkdownContext = {
|
|
15870
|
+
opts: {
|
|
15871
|
+
htmlFallback: options?.htmlFallback ?? true,
|
|
15872
|
+
footnotes: options?.footnotes ?? true,
|
|
15873
|
+
images: options?.images ?? true,
|
|
15874
|
+
},
|
|
15875
|
+
notes: [],
|
|
15876
|
+
noteMarkers: new Set<string>(),
|
|
15877
|
+
};
|
|
15303
15878
|
|
|
15879
|
+
const lines: string[] = [];
|
|
15304
15880
|
for (const element of this.bodyElements) {
|
|
15305
|
-
|
|
15306
|
-
const mdLine = this.paragraphToMarkdown(element);
|
|
15307
|
-
if (mdLine !== null) {
|
|
15308
|
-
lines.push(mdLine);
|
|
15309
|
-
lines.push('');
|
|
15310
|
-
}
|
|
15311
|
-
} else if (element instanceof Table) {
|
|
15312
|
-
lines.push(...this.tableToMarkdown(element));
|
|
15313
|
-
lines.push('');
|
|
15314
|
-
}
|
|
15315
|
-
// Other element types (SDT, AlternateContent, etc.) are skipped
|
|
15881
|
+
this.bodyElementToMarkdown(element, ctx, lines);
|
|
15316
15882
|
}
|
|
15317
15883
|
|
|
15318
|
-
// Remove trailing blank line
|
|
15884
|
+
// Remove trailing blank line(s)
|
|
15319
15885
|
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
15320
15886
|
lines.pop();
|
|
15321
15887
|
}
|
|
15322
15888
|
|
|
15323
|
-
|
|
15889
|
+
let out = lines.join('\n');
|
|
15890
|
+
|
|
15891
|
+
// Append footnote/endnote definitions in first-reference order.
|
|
15892
|
+
if (ctx.opts.footnotes && ctx.notes.length > 0) {
|
|
15893
|
+
const defs = ctx.notes.map((n) => `[^${n.marker}]: ${n.text}`).join('\n');
|
|
15894
|
+
out = out ? `${out}\n\n${defs}` : defs;
|
|
15895
|
+
}
|
|
15896
|
+
|
|
15897
|
+
return out;
|
|
15898
|
+
}
|
|
15899
|
+
|
|
15900
|
+
/**
|
|
15901
|
+
* Renders a single body element into the running Markdown line buffer.
|
|
15902
|
+
* Recurses into block-level structured document tags.
|
|
15903
|
+
* @internal
|
|
15904
|
+
*/
|
|
15905
|
+
private bodyElementToMarkdown(element: BodyElement, ctx: MarkdownContext, lines: string[]): void {
|
|
15906
|
+
if (element instanceof Paragraph) {
|
|
15907
|
+
const md = this.paragraphToMarkdown(element, ctx);
|
|
15908
|
+
if (md !== null) {
|
|
15909
|
+
lines.push(md);
|
|
15910
|
+
lines.push('');
|
|
15911
|
+
}
|
|
15912
|
+
} else if (element instanceof Table) {
|
|
15913
|
+
const tableLines = this.tableToMarkdown(element, ctx);
|
|
15914
|
+
if (tableLines.length > 0) {
|
|
15915
|
+
lines.push(...tableLines);
|
|
15916
|
+
lines.push('');
|
|
15917
|
+
}
|
|
15918
|
+
} else if (element instanceof StructuredDocumentTag) {
|
|
15919
|
+
// Block SDT: render its inner content (paragraphs/tables/nested SDTs).
|
|
15920
|
+
for (const child of element.getContent()) {
|
|
15921
|
+
this.bodyElementToMarkdown(child, ctx, lines);
|
|
15922
|
+
}
|
|
15923
|
+
} else if (element instanceof PreservedElement) {
|
|
15924
|
+
// Best-effort: surface any text so round-trip-preserved blocks are not lost.
|
|
15925
|
+
const text = this.extractTextFromRawXml(element.getRawXml());
|
|
15926
|
+
if (text) {
|
|
15927
|
+
lines.push(this.escapeMarkdown(text));
|
|
15928
|
+
lines.push('');
|
|
15929
|
+
}
|
|
15930
|
+
}
|
|
15931
|
+
// TableOfContentsElement is intentionally skipped: its content is generated
|
|
15932
|
+
// from the headings, which are already represented in the Markdown output.
|
|
15324
15933
|
}
|
|
15325
15934
|
|
|
15326
15935
|
/**
|
|
15327
|
-
* Converts a paragraph to a Markdown
|
|
15936
|
+
* Converts a paragraph to a Markdown block (heading, list item, block quote,
|
|
15937
|
+
* or plain paragraph). Returns null for empty, non-list paragraphs.
|
|
15328
15938
|
* @internal
|
|
15329
15939
|
*/
|
|
15330
|
-
private paragraphToMarkdown(para: Paragraph): string | null {
|
|
15331
|
-
const
|
|
15332
|
-
|
|
15940
|
+
private paragraphToMarkdown(para: Paragraph, ctx: MarkdownContext): string | null {
|
|
15941
|
+
const inline = this.paragraphContentToMarkdown(para, ctx);
|
|
15942
|
+
const hasNumbering = para.hasNumbering();
|
|
15943
|
+
if (!inline && !hasNumbering) return null;
|
|
15333
15944
|
|
|
15334
|
-
// Headings
|
|
15945
|
+
// Headings take precedence over every other block style.
|
|
15335
15946
|
const headingLevel = para.detectHeadingLevel();
|
|
15336
15947
|
if (headingLevel !== null && headingLevel >= 1 && headingLevel <= 6) {
|
|
15337
|
-
return '#'.repeat(headingLevel) + ' ' +
|
|
15948
|
+
return '#'.repeat(headingLevel) + ' ' + this.toSingleLine(inline);
|
|
15338
15949
|
}
|
|
15339
15950
|
|
|
15340
|
-
|
|
15341
|
-
|
|
15342
|
-
|
|
15343
|
-
|
|
15344
|
-
|
|
15345
|
-
|
|
15951
|
+
const style = (para.getStyle() ?? '').toLowerCase();
|
|
15952
|
+
const isListStyle =
|
|
15953
|
+
style.includes('listbullet') ||
|
|
15954
|
+
style.includes('list bullet') ||
|
|
15955
|
+
style.includes('listnumber') ||
|
|
15956
|
+
style.includes('list number');
|
|
15957
|
+
|
|
15958
|
+
if (hasNumbering || isListStyle) {
|
|
15959
|
+
const level = para.getNumbering()?.level ?? 0;
|
|
15960
|
+
const indent = ' '.repeat(Math.max(0, level));
|
|
15961
|
+
const marker = this.isOrderedList(para) ? '1.' : '-';
|
|
15962
|
+
return `${indent}${marker} ${this.toSingleLine(inline)}`;
|
|
15963
|
+
}
|
|
15964
|
+
|
|
15965
|
+
// Block quote (Quote / IntenseQuote styles).
|
|
15966
|
+
if (style.includes('quote')) {
|
|
15967
|
+
return this.toSingleLine(inline)
|
|
15968
|
+
.split('\n')
|
|
15969
|
+
.map((l) => `> ${l}`)
|
|
15970
|
+
.join('\n');
|
|
15346
15971
|
}
|
|
15347
15972
|
|
|
15348
|
-
return
|
|
15973
|
+
return inline;
|
|
15974
|
+
}
|
|
15975
|
+
|
|
15976
|
+
/**
|
|
15977
|
+
* Determines whether a numbered paragraph uses an ordered (decimal/letter/
|
|
15978
|
+
* roman) format rather than a bullet. Resolves the numbering definition when
|
|
15979
|
+
* available, falling back to the paragraph's style name.
|
|
15980
|
+
* @internal
|
|
15981
|
+
*/
|
|
15982
|
+
private isOrderedList(para: Paragraph): boolean {
|
|
15983
|
+
const numbering = para.getNumbering();
|
|
15984
|
+
if (numbering) {
|
|
15985
|
+
const instance = this.numberingManager.getInstance(numbering.numId);
|
|
15986
|
+
if (instance) {
|
|
15987
|
+
const abstract = this.numberingManager.getAbstractNumbering(instance.getAbstractNumId());
|
|
15988
|
+
const levelDef = abstract?.getLevel(numbering.level);
|
|
15989
|
+
const format = levelDef?.getFormat();
|
|
15990
|
+
if (format) return format !== 'bullet';
|
|
15991
|
+
}
|
|
15992
|
+
}
|
|
15993
|
+
const style = (para.getStyle() ?? '').toLowerCase();
|
|
15994
|
+
if (style.includes('bullet')) return false;
|
|
15995
|
+
if (style.includes('number')) return true;
|
|
15996
|
+
return true;
|
|
15349
15997
|
}
|
|
15350
15998
|
|
|
15351
15999
|
/**
|
|
15352
16000
|
* Converts paragraph inline content to Markdown with formatting.
|
|
15353
16001
|
* @internal
|
|
15354
16002
|
*/
|
|
15355
|
-
private paragraphContentToMarkdown(para: Paragraph): string {
|
|
15356
|
-
|
|
16003
|
+
private paragraphContentToMarkdown(para: Paragraph, ctx: MarkdownContext): string {
|
|
16004
|
+
return this.inlineContentToMarkdown(para.getContent(), ctx);
|
|
16005
|
+
}
|
|
15357
16006
|
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
|
|
16007
|
+
/**
|
|
16008
|
+
* Converts a sequence of inline content items (runs, hyperlinks, fields,
|
|
16009
|
+
* revisions, shapes, text boxes, preserved elements) to Markdown.
|
|
16010
|
+
* @internal
|
|
16011
|
+
*/
|
|
16012
|
+
private inlineContentToMarkdown(items: ParagraphContent[], ctx: MarkdownContext): string {
|
|
16013
|
+
const parts: string[] = [];
|
|
15362
16014
|
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
15367
|
-
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
} else if (fmt.italic) {
|
|
15372
|
-
md = `*${md}*`;
|
|
16015
|
+
for (const item of items) {
|
|
16016
|
+
// ImageRun extends Run, so it must be checked first.
|
|
16017
|
+
if (item instanceof ImageRun) {
|
|
16018
|
+
if (ctx.opts.images) {
|
|
16019
|
+
const image = item.getImageElement();
|
|
16020
|
+
const alt = this.escapeMarkdown(image.getAltText() || 'image');
|
|
16021
|
+
const src = image.getRelationshipId() ?? 'image';
|
|
16022
|
+
parts.push(``);
|
|
15373
16023
|
}
|
|
15374
|
-
|
|
15375
|
-
|
|
15376
|
-
|
|
16024
|
+
} else if (item instanceof Run) {
|
|
16025
|
+
parts.push(this.runToMarkdown(item, ctx));
|
|
16026
|
+
} else if (item instanceof Hyperlink) {
|
|
16027
|
+
const runs = item.getRuns();
|
|
16028
|
+
let label =
|
|
16029
|
+
runs.length > 0
|
|
16030
|
+
? runs.map((r) => this.runToMarkdown(r, ctx, true)).join('')
|
|
16031
|
+
: this.escapeMarkdown(item.getText());
|
|
16032
|
+
if (!label) label = this.escapeMarkdown(item.getText());
|
|
16033
|
+
let url = item.getUrl() ?? '';
|
|
16034
|
+
const anchor = item.getAnchor();
|
|
16035
|
+
if (anchor) url = url ? `${url}#${anchor}` : `#${anchor}`;
|
|
16036
|
+
parts.push(`[${label}](${url})`);
|
|
16037
|
+
} else if (item instanceof Revision) {
|
|
16038
|
+
const inner = this.inlineContentToMarkdown(item.getContent(), ctx);
|
|
16039
|
+
if (!inner) continue;
|
|
16040
|
+
const type = item.getType();
|
|
16041
|
+
if (type === 'delete' || type === 'moveFrom') {
|
|
16042
|
+
// Deleted/moved-away content (only present in 'preserve' mode): mark it
|
|
16043
|
+
// so the information survives without claiming it as final body text.
|
|
16044
|
+
parts.push(ctx.opts.htmlFallback ? `<del>${inner}</del>` : `~~${inner}~~`);
|
|
16045
|
+
} else {
|
|
16046
|
+
parts.push(inner);
|
|
15377
16047
|
}
|
|
15378
|
-
|
|
15379
|
-
|
|
15380
|
-
if (
|
|
15381
|
-
|
|
15382
|
-
|
|
15383
|
-
)
|
|
15384
|
-
|
|
16048
|
+
} else if (item instanceof ComplexField) {
|
|
16049
|
+
const result = item.getResult();
|
|
16050
|
+
if (result) parts.push(this.escapeMarkdown(result));
|
|
16051
|
+
} else if (item instanceof Field) {
|
|
16052
|
+
const result = item.getCachedResult();
|
|
16053
|
+
if (result) parts.push(this.escapeMarkdown(result));
|
|
16054
|
+
} else if (item instanceof TextBox) {
|
|
16055
|
+
const text = item
|
|
16056
|
+
.getParagraphs()
|
|
16057
|
+
.map((p) => this.paragraphContentToMarkdown(p, ctx))
|
|
16058
|
+
.filter((s) => s !== '')
|
|
16059
|
+
.join(' ');
|
|
16060
|
+
if (text) parts.push(text);
|
|
16061
|
+
} else if (item instanceof Shape) {
|
|
16062
|
+
const text = item.getText();
|
|
16063
|
+
if (text) parts.push(this.escapeMarkdown(text));
|
|
16064
|
+
} else if (item instanceof PreservedElement) {
|
|
16065
|
+
const type = item.getElementType();
|
|
16066
|
+
// Comment range/reference markers carry no inline body text.
|
|
16067
|
+
if (!type.startsWith('w:comment')) {
|
|
16068
|
+
const text = this.extractTextFromRawXml(item.getRawXml());
|
|
16069
|
+
if (text) parts.push(this.escapeMarkdown(text));
|
|
15385
16070
|
}
|
|
15386
|
-
|
|
15387
|
-
parts.push(md);
|
|
15388
|
-
} else if (item instanceof Hyperlink) {
|
|
15389
|
-
const url = item.getUrl() || '';
|
|
15390
|
-
const linkText = item.getText() || url;
|
|
15391
|
-
parts.push(`[${linkText}](${url})`);
|
|
15392
16071
|
}
|
|
15393
|
-
//
|
|
16072
|
+
// RangeMarker (bookmarks) carry no display text and are skipped.
|
|
15394
16073
|
}
|
|
15395
16074
|
|
|
15396
16075
|
return parts.join('');
|
|
15397
16076
|
}
|
|
15398
16077
|
|
|
15399
16078
|
/**
|
|
15400
|
-
* Converts a
|
|
16079
|
+
* Converts a single run to Markdown, applying inline formatting and emitting
|
|
16080
|
+
* footnote/endnote markers. Tabs, breaks and symbols are preserved.
|
|
16081
|
+
* @internal
|
|
16082
|
+
*/
|
|
16083
|
+
private runToMarkdown(run: Run, ctx: MarkdownContext, inLink = false): string {
|
|
16084
|
+
const fmt = run.getFormatting();
|
|
16085
|
+
const isMono = !!(
|
|
16086
|
+
fmt.font &&
|
|
16087
|
+
/^(courier|consolas|monaco|menlo|source code|fira code|jetbrains mono)/i.test(fmt.font)
|
|
16088
|
+
);
|
|
16089
|
+
const escape = (s: string): string => (isMono ? s : this.escapeMarkdown(s));
|
|
16090
|
+
|
|
16091
|
+
const segments: string[] = [];
|
|
16092
|
+
const markers: string[] = [];
|
|
16093
|
+
|
|
16094
|
+
for (const content of run.getContent()) {
|
|
16095
|
+
switch (content.type) {
|
|
16096
|
+
case 'text':
|
|
16097
|
+
case 'instructionText':
|
|
16098
|
+
segments.push(escape(content.value ?? ''));
|
|
16099
|
+
break;
|
|
16100
|
+
case 'tab':
|
|
16101
|
+
segments.push('\t');
|
|
16102
|
+
break;
|
|
16103
|
+
case 'carriageReturn':
|
|
16104
|
+
segments.push('\n');
|
|
16105
|
+
break;
|
|
16106
|
+
case 'break':
|
|
16107
|
+
// Page/column breaks are layout-only; line breaks become hard breaks.
|
|
16108
|
+
if (content.breakType !== 'page' && content.breakType !== 'column') {
|
|
16109
|
+
segments.push(ctx.opts.htmlFallback ? '<br>' : '\n');
|
|
16110
|
+
}
|
|
16111
|
+
break;
|
|
16112
|
+
case 'noBreakHyphen':
|
|
16113
|
+
segments.push('‑');
|
|
16114
|
+
break;
|
|
16115
|
+
case 'symbol':
|
|
16116
|
+
if (content.symbolChar) {
|
|
16117
|
+
const code = parseInt(content.symbolChar, 16);
|
|
16118
|
+
if (!Number.isNaN(code)) segments.push(escape(String.fromCharCode(code)));
|
|
16119
|
+
}
|
|
16120
|
+
break;
|
|
16121
|
+
case 'footnoteReference':
|
|
16122
|
+
if (ctx.opts.footnotes && content.footnoteId !== undefined) {
|
|
16123
|
+
markers.push(this.noteMarker(content.footnoteId, 'fn', ctx));
|
|
16124
|
+
}
|
|
16125
|
+
break;
|
|
16126
|
+
case 'endnoteReference':
|
|
16127
|
+
if (ctx.opts.footnotes && content.endnoteId !== undefined) {
|
|
16128
|
+
markers.push(this.noteMarker(content.endnoteId, 'en', ctx));
|
|
16129
|
+
}
|
|
16130
|
+
break;
|
|
16131
|
+
// Field chars, VML, soft hyphens and other layout markers contribute no text.
|
|
16132
|
+
default:
|
|
16133
|
+
break;
|
|
16134
|
+
}
|
|
16135
|
+
}
|
|
16136
|
+
|
|
16137
|
+
let text = segments.join('');
|
|
16138
|
+
if (text) {
|
|
16139
|
+
if (isMono) {
|
|
16140
|
+
text = `\`${text}\``;
|
|
16141
|
+
}
|
|
16142
|
+
// Emphasis (bold/italic innermost, strikethrough outermost).
|
|
16143
|
+
if (fmt.bold && fmt.italic) text = `***${text}***`;
|
|
16144
|
+
else if (fmt.bold) text = `**${text}**`;
|
|
16145
|
+
else if (fmt.italic) text = `*${text}*`;
|
|
16146
|
+
if (fmt.strike || fmt.dstrike) text = `~~${text}~~`;
|
|
16147
|
+
|
|
16148
|
+
if (ctx.opts.htmlFallback) {
|
|
16149
|
+
if (fmt.superscript) text = `<sup>${text}</sup>`;
|
|
16150
|
+
else if (fmt.subscript) text = `<sub>${text}</sub>`;
|
|
16151
|
+
if (fmt.highlight && fmt.highlight !== 'none') text = `<mark>${text}</mark>`;
|
|
16152
|
+
// Underline and color are the conventional appearance of a hyperlink, so
|
|
16153
|
+
// suppress them inside link labels to avoid redundant markup; elsewhere
|
|
16154
|
+
// they carry real information and are preserved as inline HTML.
|
|
16155
|
+
if (!inLink) {
|
|
16156
|
+
if (fmt.underline && fmt.underline !== 'none') text = `<u>${text}</u>`;
|
|
16157
|
+
if (fmt.color && fmt.color !== 'auto' && /^[0-9a-fA-F]{6}$/.test(fmt.color)) {
|
|
16158
|
+
text = `<span style="color:#${fmt.color}">${text}</span>`;
|
|
16159
|
+
}
|
|
16160
|
+
}
|
|
16161
|
+
}
|
|
16162
|
+
}
|
|
16163
|
+
|
|
16164
|
+
// Footnote/endnote markers follow the text, unaffected by run formatting.
|
|
16165
|
+
return text + markers.join('');
|
|
16166
|
+
}
|
|
16167
|
+
|
|
16168
|
+
/**
|
|
16169
|
+
* Resolves a footnote/endnote reference to a GFM marker and records its
|
|
16170
|
+
* definition (once) for later emission.
|
|
16171
|
+
* @internal
|
|
16172
|
+
*/
|
|
16173
|
+
private noteMarker(id: number, kind: 'fn' | 'en', ctx: MarkdownContext): string {
|
|
16174
|
+
const marker = `${kind}${id}`;
|
|
16175
|
+
if (!ctx.noteMarkers.has(marker)) {
|
|
16176
|
+
ctx.noteMarkers.add(marker);
|
|
16177
|
+
const note =
|
|
16178
|
+
kind === 'fn' ? this.footnoteManager.getFootnote(id) : this.endnoteManager.getEndnote(id);
|
|
16179
|
+
let text = '';
|
|
16180
|
+
if (note) {
|
|
16181
|
+
text = note
|
|
16182
|
+
.getParagraphs()
|
|
16183
|
+
.map((p) => this.paragraphContentToMarkdown(p, ctx))
|
|
16184
|
+
.filter((s) => s !== '')
|
|
16185
|
+
.join(' ')
|
|
16186
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
16187
|
+
.trim();
|
|
16188
|
+
}
|
|
16189
|
+
ctx.notes.push({ marker, text });
|
|
16190
|
+
}
|
|
16191
|
+
return `[^${marker}]`;
|
|
16192
|
+
}
|
|
16193
|
+
|
|
16194
|
+
/**
|
|
16195
|
+
* Converts a table to Markdown. Simple grid tables become GFM pipe tables;
|
|
16196
|
+
* tables with merged or nested cells fall back to inline HTML (when enabled)
|
|
16197
|
+
* so structural information is preserved.
|
|
15401
16198
|
* @internal
|
|
15402
16199
|
*/
|
|
15403
|
-
private tableToMarkdown(table: Table): string[] {
|
|
15404
|
-
const
|
|
15405
|
-
if (
|
|
16200
|
+
private tableToMarkdown(table: Table, ctx: MarkdownContext): string[] {
|
|
16201
|
+
const rows = table.getRows();
|
|
16202
|
+
if (rows.length === 0) return [];
|
|
16203
|
+
|
|
16204
|
+
const isComplex = rows.some((row) =>
|
|
16205
|
+
row
|
|
16206
|
+
.getCells()
|
|
16207
|
+
.some(
|
|
16208
|
+
(cell) =>
|
|
16209
|
+
(cell.getColumnSpan() ?? 1) > 1 ||
|
|
16210
|
+
cell.getVerticalMerge() !== undefined ||
|
|
16211
|
+
cell.hasNestedTables()
|
|
16212
|
+
)
|
|
16213
|
+
);
|
|
15406
16214
|
|
|
15407
|
-
|
|
16215
|
+
if (isComplex && ctx.opts.htmlFallback) {
|
|
16216
|
+
return this.tableToHtmlBlock(table);
|
|
16217
|
+
}
|
|
16218
|
+
|
|
16219
|
+
const matrix = rows.map((row) => row.getCells().map((cell) => this.cellToMarkdown(cell, ctx)));
|
|
16220
|
+
const colCount = Math.max(...matrix.map((row) => row.length));
|
|
15408
16221
|
if (colCount === 0) return [];
|
|
15409
16222
|
|
|
15410
|
-
|
|
15411
|
-
const normalized = data.map((row) => {
|
|
16223
|
+
const normalized = matrix.map((row) => {
|
|
15412
16224
|
const padded = [...row];
|
|
15413
16225
|
while (padded.length < colCount) padded.push('');
|
|
15414
|
-
|
|
15415
|
-
return padded.map((cell) => cell.replace(/\|/g, '\\|').replace(/\n/g, ' ').trim());
|
|
16226
|
+
return padded;
|
|
15416
16227
|
});
|
|
15417
16228
|
|
|
15418
16229
|
const lines: string[] = [];
|
|
15419
|
-
|
|
15420
|
-
// Header row
|
|
15421
16230
|
lines.push('| ' + normalized[0]!.join(' | ') + ' |');
|
|
15422
|
-
|
|
15423
|
-
// Separator row
|
|
15424
16231
|
lines.push('| ' + normalized[0]!.map(() => '---').join(' | ') + ' |');
|
|
15425
|
-
|
|
15426
|
-
// Data rows
|
|
15427
16232
|
for (let i = 1; i < normalized.length; i++) {
|
|
15428
16233
|
lines.push('| ' + normalized[i]!.join(' | ') + ' |');
|
|
15429
16234
|
}
|
|
15430
|
-
|
|
15431
16235
|
return lines;
|
|
15432
16236
|
}
|
|
15433
16237
|
|
|
16238
|
+
/**
|
|
16239
|
+
* Renders a single table cell as inline Markdown text (formatting preserved),
|
|
16240
|
+
* with pipes escaped and line breaks collapsed for pipe-table compatibility.
|
|
16241
|
+
* @internal
|
|
16242
|
+
*/
|
|
16243
|
+
private cellToMarkdown(cell: TableCell, ctx: MarkdownContext): string {
|
|
16244
|
+
const text = cell
|
|
16245
|
+
.getParagraphs()
|
|
16246
|
+
.map((p) => this.paragraphContentToMarkdown(p, ctx))
|
|
16247
|
+
.filter((s) => s !== '')
|
|
16248
|
+
.join(' ');
|
|
16249
|
+
return text.replace(/\|/g, '\\|').replace(/<br>/g, ' ').replace(/\n/g, ' ').trim();
|
|
16250
|
+
}
|
|
16251
|
+
|
|
16252
|
+
/**
|
|
16253
|
+
* Renders a table with merged or nested cells as an inline HTML table,
|
|
16254
|
+
* honoring `colspan`/`rowspan` from grid and vertical-merge information.
|
|
16255
|
+
* @internal
|
|
16256
|
+
*/
|
|
16257
|
+
private tableToHtmlBlock(table: Table): string[] {
|
|
16258
|
+
const rows = table.getRows();
|
|
16259
|
+
|
|
16260
|
+
// Precompute each cell's starting grid column (accounting for column spans).
|
|
16261
|
+
const rowCells = rows.map((row) => {
|
|
16262
|
+
let gridStart = 0;
|
|
16263
|
+
return row.getCells().map((cell) => {
|
|
16264
|
+
const span = cell.getColumnSpan() ?? 1;
|
|
16265
|
+
const entry = { cell, gridStart, span };
|
|
16266
|
+
gridStart += span;
|
|
16267
|
+
return entry;
|
|
16268
|
+
});
|
|
16269
|
+
});
|
|
16270
|
+
|
|
16271
|
+
const html: string[] = ['<table>'];
|
|
16272
|
+
for (let ri = 0; ri < rowCells.length; ri++) {
|
|
16273
|
+
html.push('<tr>');
|
|
16274
|
+
for (const { cell, gridStart, span } of rowCells[ri]!) {
|
|
16275
|
+
// Cells continuing a vertical merge are covered by the rowspan above.
|
|
16276
|
+
if (cell.getVerticalMerge() === 'continue') continue;
|
|
16277
|
+
|
|
16278
|
+
let rowspan = 1;
|
|
16279
|
+
if (cell.getVerticalMerge() === 'restart') {
|
|
16280
|
+
for (let rj = ri + 1; rj < rowCells.length; rj++) {
|
|
16281
|
+
const below = rowCells[rj]!.find((e) => e.gridStart === gridStart);
|
|
16282
|
+
if (below?.cell.getVerticalMerge() === 'continue') rowspan++;
|
|
16283
|
+
else break;
|
|
16284
|
+
}
|
|
16285
|
+
}
|
|
16286
|
+
|
|
16287
|
+
const tag = ri === 0 ? 'th' : 'td';
|
|
16288
|
+
const attrs =
|
|
16289
|
+
(span > 1 ? ` colspan="${span}"` : '') + (rowspan > 1 ? ` rowspan="${rowspan}"` : '');
|
|
16290
|
+
|
|
16291
|
+
let content =
|
|
16292
|
+
cell
|
|
16293
|
+
.getParagraphs()
|
|
16294
|
+
.map((p) => this.paragraphContentToHTML(p))
|
|
16295
|
+
.filter((s) => s !== '')
|
|
16296
|
+
.join('<br>') || this.escapeHTML(cell.getText());
|
|
16297
|
+
|
|
16298
|
+
if (cell.hasNestedTables()) {
|
|
16299
|
+
const nestedText = cell
|
|
16300
|
+
.getRawNestedContent()
|
|
16301
|
+
.map((n) => this.extractTextFromRawXml(n.xml))
|
|
16302
|
+
.filter((s) => s)
|
|
16303
|
+
.join(' ');
|
|
16304
|
+
if (nestedText) content += `<br>${this.escapeHTML(nestedText)}`;
|
|
16305
|
+
}
|
|
16306
|
+
|
|
16307
|
+
html.push(`<${tag}${attrs}>${content}</${tag}>`);
|
|
16308
|
+
}
|
|
16309
|
+
html.push('</tr>');
|
|
16310
|
+
}
|
|
16311
|
+
html.push('</table>');
|
|
16312
|
+
return html;
|
|
16313
|
+
}
|
|
16314
|
+
|
|
16315
|
+
/**
|
|
16316
|
+
* Escapes Markdown-significant characters in literal text so they render
|
|
16317
|
+
* verbatim rather than as formatting.
|
|
16318
|
+
* @internal
|
|
16319
|
+
*/
|
|
16320
|
+
private escapeMarkdown(text: string): string {
|
|
16321
|
+
return text.replace(/([\\`*_[\]<>])/g, '\\$1');
|
|
16322
|
+
}
|
|
16323
|
+
|
|
16324
|
+
/**
|
|
16325
|
+
* Collapses hard/soft line breaks to single spaces for contexts that must
|
|
16326
|
+
* stay on one line (headings, list items, table cells).
|
|
16327
|
+
* @internal
|
|
16328
|
+
*/
|
|
16329
|
+
private toSingleLine(text: string): string {
|
|
16330
|
+
return text.replace(/<br>/g, ' ').replace(/\s*\n\s*/g, ' ');
|
|
16331
|
+
}
|
|
16332
|
+
|
|
16333
|
+
/**
|
|
16334
|
+
* Extracts visible text from a raw OOXML fragment by concatenating the
|
|
16335
|
+
* contents of its `w:t` elements and decoding XML entities. Used as a
|
|
16336
|
+
* last-resort so preserved/round-trip elements never silently lose text.
|
|
16337
|
+
* @internal
|
|
16338
|
+
*/
|
|
16339
|
+
private extractTextFromRawXml(xml: string): string {
|
|
16340
|
+
if (!xml.includes('<w:t')) return '';
|
|
16341
|
+
const parts: string[] = [];
|
|
16342
|
+
const regex = /<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g;
|
|
16343
|
+
let match: RegExpExecArray | null;
|
|
16344
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
16345
|
+
parts.push(match[1] ?? '');
|
|
16346
|
+
}
|
|
16347
|
+
return parts
|
|
16348
|
+
.join('')
|
|
16349
|
+
.replace(/</g, '<')
|
|
16350
|
+
.replace(/>/g, '>')
|
|
16351
|
+
.replace(/"/g, '"')
|
|
16352
|
+
.replace(/'/g, "'")
|
|
16353
|
+
.replace(/&/g, '&')
|
|
16354
|
+
.trim();
|
|
16355
|
+
}
|
|
16356
|
+
|
|
15434
16357
|
/**
|
|
15435
16358
|
* Converts the document to an HTML string
|
|
15436
16359
|
*
|
|
@@ -15567,8 +16490,13 @@ export class Document {
|
|
|
15567
16490
|
|
|
15568
16491
|
parts.push(html);
|
|
15569
16492
|
} else if (item instanceof Hyperlink) {
|
|
15570
|
-
|
|
15571
|
-
|
|
16493
|
+
// Word stores the fragment/anchor (e.g. "!/view?docid=…") separately
|
|
16494
|
+
// from the base URL; recombine them so deep links are not truncated.
|
|
16495
|
+
let rawUrl = item.getUrl() ?? '';
|
|
16496
|
+
const anchor = item.getAnchor();
|
|
16497
|
+
if (anchor) rawUrl = rawUrl ? `${rawUrl}#${anchor}` : `#${anchor}`;
|
|
16498
|
+
const url = this.escapeHTML(rawUrl);
|
|
16499
|
+
const linkText = this.escapeHTML(item.getText() || rawUrl);
|
|
15572
16500
|
parts.push(`<a href="${url}">${linkText}</a>`);
|
|
15573
16501
|
}
|
|
15574
16502
|
}
|
|
@@ -15871,7 +16799,12 @@ export class Document {
|
|
|
15871
16799
|
let index: number;
|
|
15872
16800
|
|
|
15873
16801
|
if (typeof paragraphOrIndex === 'number') {
|
|
15874
|
-
index
|
|
16802
|
+
// Numeric index is paragraph-ordinal (matching getParagraphAt /
|
|
16803
|
+
// getParagraphIndex), not a bodyElements index — tables or SDTs before
|
|
16804
|
+
// the target would otherwise shift it onto the wrong paragraph.
|
|
16805
|
+
const para = this.getParagraphAt(paragraphOrIndex);
|
|
16806
|
+
if (!para) return false;
|
|
16807
|
+
index = this.bodyElements.indexOf(para);
|
|
15875
16808
|
} else {
|
|
15876
16809
|
// Find the index of the paragraph
|
|
15877
16810
|
index = this.bodyElements.indexOf(paragraphOrIndex);
|
|
@@ -15884,13 +16817,12 @@ export class Document {
|
|
|
15884
16817
|
// No paragraphRemoved event in this branch — the paragraph is still
|
|
15885
16818
|
// structurally present, just marked deleted via revision.
|
|
15886
16819
|
if (this.trackChangesEnabled && this.trackingContext.isEnabled()) {
|
|
15887
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
}
|
|
16820
|
+
// clearContent with tracking bound REPLACES each Run/Hyperlink with
|
|
16821
|
+
// its delete revision in place. Appending a revision built from
|
|
16822
|
+
// getRuns() would leave the originals live alongside the w:del copy,
|
|
16823
|
+
// so the text would serialize twice and survive an accept-all.
|
|
16824
|
+
this.bindTrackingToElement(element);
|
|
16825
|
+
element.clearContent();
|
|
15894
16826
|
return true;
|
|
15895
16827
|
}
|
|
15896
16828
|
this.bodyElements.splice(index, 1);
|
|
@@ -16511,23 +17443,12 @@ export class Document {
|
|
|
16511
17443
|
}
|
|
16512
17444
|
};
|
|
16513
17445
|
|
|
17446
|
+
// getAllParagraphs() already walks table-cell paragraphs, so a separate
|
|
17447
|
+
// table pass would report each table hyperlink twice.
|
|
16514
17448
|
for (const paragraph of this.getAllParagraphs()) {
|
|
16515
17449
|
extractHyperlinksFromParagraph(paragraph);
|
|
16516
17450
|
}
|
|
16517
17451
|
|
|
16518
|
-
// Also check in tables
|
|
16519
|
-
for (const table of this.getTables()) {
|
|
16520
|
-
for (const row of table.getRows()) {
|
|
16521
|
-
for (const cell of row.getCells()) {
|
|
16522
|
-
// TableCell has getParagraphs method
|
|
16523
|
-
const cellParagraphs = cell instanceof TableCell ? cell.getParagraphs() : [];
|
|
16524
|
-
for (const para of cellParagraphs) {
|
|
16525
|
-
extractHyperlinksFromParagraph(para);
|
|
16526
|
-
}
|
|
16527
|
-
}
|
|
16528
|
-
}
|
|
16529
|
-
}
|
|
16530
|
-
|
|
16531
17452
|
return hyperlinks;
|
|
16532
17453
|
}
|
|
16533
17454
|
|
|
@@ -16721,6 +17642,8 @@ export class Document {
|
|
|
16721
17642
|
getBookmarks(): { bookmark: Bookmark; paragraph: Paragraph }[] {
|
|
16722
17643
|
const bookmarks: { bookmark: Bookmark; paragraph: Paragraph }[] = [];
|
|
16723
17644
|
|
|
17645
|
+
// getAllParagraphs() already walks table-cell paragraphs, so a separate
|
|
17646
|
+
// table pass would report each table bookmark twice.
|
|
16724
17647
|
for (const paragraph of this.getAllParagraphs()) {
|
|
16725
17648
|
// Get bookmarks that start in this paragraph
|
|
16726
17649
|
for (const bookmark of paragraph.getBookmarksStart()) {
|
|
@@ -16728,19 +17651,6 @@ export class Document {
|
|
|
16728
17651
|
}
|
|
16729
17652
|
}
|
|
16730
17653
|
|
|
16731
|
-
// Also check in tables
|
|
16732
|
-
for (const table of this.getTables()) {
|
|
16733
|
-
for (const row of table.getRows()) {
|
|
16734
|
-
for (const cell of row.getCells()) {
|
|
16735
|
-
for (const para of cell.getParagraphs()) {
|
|
16736
|
-
for (const bookmark of para.getBookmarksStart()) {
|
|
16737
|
-
bookmarks.push({ bookmark, paragraph: para });
|
|
16738
|
-
}
|
|
16739
|
-
}
|
|
16740
|
-
}
|
|
16741
|
-
}
|
|
16742
|
-
}
|
|
16743
|
-
|
|
16744
17654
|
return bookmarks;
|
|
16745
17655
|
}
|
|
16746
17656
|
|
|
@@ -16780,27 +17690,14 @@ export class Document {
|
|
|
16780
17690
|
getFields(): { field: FieldLike; paragraph: Paragraph; table?: Table }[] {
|
|
16781
17691
|
const results: { field: FieldLike; paragraph: Paragraph; table?: Table }[] = [];
|
|
16782
17692
|
|
|
16783
|
-
//
|
|
17693
|
+
// getAllParagraphs() already walks table-cell paragraphs, so a separate
|
|
17694
|
+
// table pass would report each table field twice.
|
|
16784
17695
|
for (const paragraph of this.getAllParagraphs()) {
|
|
16785
17696
|
for (const field of paragraph.getFields()) {
|
|
16786
17697
|
results.push({ field, paragraph });
|
|
16787
17698
|
}
|
|
16788
17699
|
}
|
|
16789
17700
|
|
|
16790
|
-
// Get fields from paragraphs inside table cells
|
|
16791
|
-
for (const table of this.getTables()) {
|
|
16792
|
-
for (const row of table.getRows()) {
|
|
16793
|
-
for (const cell of row.getCells()) {
|
|
16794
|
-
const cellParagraphs = cell instanceof TableCell ? cell.getParagraphs() : [];
|
|
16795
|
-
for (const para of cellParagraphs) {
|
|
16796
|
-
for (const field of para.getFields()) {
|
|
16797
|
-
results.push({ field, paragraph: para, table });
|
|
16798
|
-
}
|
|
16799
|
-
}
|
|
16800
|
-
}
|
|
16801
|
-
}
|
|
16802
|
-
}
|
|
16803
|
-
|
|
16804
17701
|
return results;
|
|
16805
17702
|
}
|
|
16806
17703
|
|
|
@@ -16892,7 +17789,14 @@ export class Document {
|
|
|
16892
17789
|
|
|
16893
17790
|
// 5. If format changed (e.g., bmp → png), update filename + relationships
|
|
16894
17791
|
if (result.newExtension !== extension) {
|
|
16895
|
-
|
|
17792
|
+
// Swapping the extension can collide with a distinct existing media
|
|
17793
|
+
// part (e.g. image1.bmp → image1.png when image1.png already exists;
|
|
17794
|
+
// non-Word producers don't share Word's global numbering). Resolve to
|
|
17795
|
+
// a fresh, unused name before renaming so neither image is clobbered.
|
|
17796
|
+
const newFilename = this.resolveUniqueMediaFilename(
|
|
17797
|
+
filename.replace(/\.[^.]+$/, `.${result.newExtension}`),
|
|
17798
|
+
filename
|
|
17799
|
+
);
|
|
16896
17800
|
this.imageManager.updateEntryFilename(image, newFilename);
|
|
16897
17801
|
|
|
16898
17802
|
// Update relationship targets across all relationship managers
|
|
@@ -16908,6 +17812,33 @@ export class Document {
|
|
|
16908
17812
|
return { optimizedCount, totalSavedBytes: totalSaved };
|
|
16909
17813
|
}
|
|
16910
17814
|
|
|
17815
|
+
/**
|
|
17816
|
+
* Resolves a media filename that does not collide with an existing ZIP media
|
|
17817
|
+
* part or another registered image entry. If the candidate is free, it is
|
|
17818
|
+
* returned unchanged; otherwise a numeric suffix is appended to the base name
|
|
17819
|
+
* (image1.png → image1-1.png → image1-2.png …) until a unique name is found.
|
|
17820
|
+
* @param candidate The desired filename (base name + extension, no directory)
|
|
17821
|
+
* @param currentFilename The filename being renamed away from (excluded from collision check)
|
|
17822
|
+
* @private
|
|
17823
|
+
*/
|
|
17824
|
+
private resolveUniqueMediaFilename(candidate: string, currentFilename: string): string {
|
|
17825
|
+
const dotIndex = candidate.lastIndexOf('.');
|
|
17826
|
+
const base = dotIndex === -1 ? candidate : candidate.slice(0, dotIndex);
|
|
17827
|
+
const ext = dotIndex === -1 ? '' : candidate.slice(dotIndex);
|
|
17828
|
+
|
|
17829
|
+
const isTaken = (name: string): boolean =>
|
|
17830
|
+
this.zipHandler.hasFile(`word/media/${name}`) ||
|
|
17831
|
+
this.imageManager.isFilenameOwnedByOther(name, currentFilename);
|
|
17832
|
+
|
|
17833
|
+
let name = candidate;
|
|
17834
|
+
let counter = 1;
|
|
17835
|
+
while (isTaken(name)) {
|
|
17836
|
+
name = `${base}-${counter}${ext}`;
|
|
17837
|
+
counter++;
|
|
17838
|
+
}
|
|
17839
|
+
return name;
|
|
17840
|
+
}
|
|
17841
|
+
|
|
16911
17842
|
/**
|
|
16912
17843
|
* Updates an image relationship target when the image format changes.
|
|
16913
17844
|
* Handles both document body and header/footer relationships.
|
|
@@ -17296,7 +18227,7 @@ export class Document {
|
|
|
17296
18227
|
|
|
17297
18228
|
this.bodyElements.forEach((element, index) => {
|
|
17298
18229
|
if (element instanceof Paragraph) {
|
|
17299
|
-
const isEmpty =
|
|
18230
|
+
const isEmpty = this.isParagraphRemovableBlank(element);
|
|
17300
18231
|
if (isEmpty && lastWasEmpty) {
|
|
17301
18232
|
toRemove.push(index);
|
|
17302
18233
|
}
|
|
@@ -17392,6 +18323,44 @@ export class Document {
|
|
|
17392
18323
|
return { removed, normalized };
|
|
17393
18324
|
}
|
|
17394
18325
|
|
|
18326
|
+
/**
|
|
18327
|
+
* Determines whether a body paragraph is safely removable as a duplicate
|
|
18328
|
+
* empty paragraph. Text emptiness alone is insufficient: a paragraph with no
|
|
18329
|
+
* visible text can still carry a bookmark/comment anchor, an inline image or
|
|
18330
|
+
* shape, a range marker, or a section break (sectPr) in its formatting.
|
|
18331
|
+
* Removing such a paragraph silently destroys that content. Mirrors the
|
|
18332
|
+
* richer blankness standard used by TableCell.isParaBlank.
|
|
18333
|
+
* @private
|
|
18334
|
+
*/
|
|
18335
|
+
private isParagraphRemovableBlank(para: Paragraph): boolean {
|
|
18336
|
+
if (para.getText().trim() !== '') return false;
|
|
18337
|
+
|
|
18338
|
+
// Inline section break — removing it merges sections and drops that
|
|
18339
|
+
// section's page setup / headers / margins.
|
|
18340
|
+
if (para.formatting.sectPr !== undefined) return false;
|
|
18341
|
+
|
|
18342
|
+
// Bookmark anchors — destroying them breaks REF fields / internal hyperlinks
|
|
18343
|
+
// targeting them ("Error! Bookmark not defined.").
|
|
18344
|
+
if (para.getBookmarksStart().length > 0 || para.getBookmarksEnd().length > 0) {
|
|
18345
|
+
return false;
|
|
18346
|
+
}
|
|
18347
|
+
|
|
18348
|
+
// Comment anchors.
|
|
18349
|
+
if (para.getCommentsStart().length > 0 || para.getCommentsEnd().length > 0) {
|
|
18350
|
+
return false;
|
|
18351
|
+
}
|
|
18352
|
+
|
|
18353
|
+
// Inline images, shapes, and range markers (bookmark/comment range markers)
|
|
18354
|
+
// are invisible to getText() but are real content.
|
|
18355
|
+
for (const item of para.getContent()) {
|
|
18356
|
+
if (item instanceof ImageRun || item instanceof Shape || item instanceof RangeMarker) {
|
|
18357
|
+
return false;
|
|
18358
|
+
}
|
|
18359
|
+
}
|
|
18360
|
+
|
|
18361
|
+
return true;
|
|
18362
|
+
}
|
|
18363
|
+
|
|
17395
18364
|
/**
|
|
17396
18365
|
* Sets the document language
|
|
17397
18366
|
* @param language - Language code (e.g., 'en-US', 'es-ES', 'fr-FR')
|
|
@@ -17403,6 +18372,7 @@ export class Document {
|
|
|
17403
18372
|
this.properties = {};
|
|
17404
18373
|
}
|
|
17405
18374
|
this.properties.language = language;
|
|
18375
|
+
this._corePropsModified = true;
|
|
17406
18376
|
|
|
17407
18377
|
return this;
|
|
17408
18378
|
}
|
|
@@ -17701,7 +18671,9 @@ export class Document {
|
|
|
17701
18671
|
? new RegExp(find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
|
|
17702
18672
|
: new RegExp(find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
17703
18673
|
|
|
17704
|
-
//
|
|
18674
|
+
// getAllParagraphs() walks recursively and already includes every
|
|
18675
|
+
// table-cell paragraph, so a separate table loop would re-apply the
|
|
18676
|
+
// replacement to the same Run instances (e.g. 'cat'->'cats' => 'catss')
|
|
17705
18677
|
for (const para of this.getAllParagraphs()) {
|
|
17706
18678
|
for (const run of para.getRuns()) {
|
|
17707
18679
|
const text = run.getText();
|
|
@@ -17725,36 +18697,6 @@ export class Document {
|
|
|
17725
18697
|
}
|
|
17726
18698
|
}
|
|
17727
18699
|
|
|
17728
|
-
// Process paragraphs inside table cells
|
|
17729
|
-
for (const table of this.getTables()) {
|
|
17730
|
-
for (const row of table.getRows()) {
|
|
17731
|
-
for (const cell of row.getCells()) {
|
|
17732
|
-
for (const para of cell.getParagraphs()) {
|
|
17733
|
-
for (const run of para.getRuns()) {
|
|
17734
|
-
const text = run.getText();
|
|
17735
|
-
if (!text) continue;
|
|
17736
|
-
|
|
17737
|
-
// Check formatting constraints
|
|
17738
|
-
const runFormatting = run.getFormatting();
|
|
17739
|
-
if (matchBold && !runFormatting.bold) continue;
|
|
17740
|
-
if (matchItalic && !runFormatting.italic) continue;
|
|
17741
|
-
|
|
17742
|
-
// Perform replacement
|
|
17743
|
-
if (pattern.test(text)) {
|
|
17744
|
-
const newText = text.replace(pattern, replace);
|
|
17745
|
-
if (newText !== text) {
|
|
17746
|
-
run.setText(newText);
|
|
17747
|
-
replacedCount++;
|
|
17748
|
-
}
|
|
17749
|
-
}
|
|
17750
|
-
// Reset regex lastIndex for next iteration
|
|
17751
|
-
pattern.lastIndex = 0;
|
|
17752
|
-
}
|
|
17753
|
-
}
|
|
17754
|
-
}
|
|
17755
|
-
}
|
|
17756
|
-
}
|
|
17757
|
-
|
|
17758
18700
|
return replacedCount;
|
|
17759
18701
|
}
|
|
17760
18702
|
|