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
|
@@ -9,12 +9,14 @@ import { Endnote, EndnoteType } from '../elements/Endnote.js';
|
|
|
9
9
|
import { Footnote, FootnoteType } from '../elements/Footnote.js';
|
|
10
10
|
import { BookmarkManager } from '../elements/BookmarkManager.js';
|
|
11
11
|
import { Comment } from '../elements/Comment.js';
|
|
12
|
+
import type { CommentContentItem, CommentParagraph } from '../elements/Comment.js';
|
|
12
13
|
import { CustomXmlBlock } from '../elements/CustomXml.js';
|
|
13
14
|
import { PreservedElement } from '../elements/PreservedElement.js';
|
|
14
15
|
import { RegisteredBodyElement } from '../elements/RegisteredBodyElement.js';
|
|
15
16
|
import { ElementRegistry } from './ElementRegistry.js';
|
|
16
17
|
import { MathParagraph } from '../elements/MathElement.js';
|
|
17
18
|
import { ComplexField, Field } from '../elements/Field.js';
|
|
19
|
+
import type { CachedFieldResultRun } from '../elements/Field.js';
|
|
18
20
|
import { isHyperlinkInstruction, parseHyperlinkInstruction } from '../elements/FieldHelpers.js';
|
|
19
21
|
import { Footer } from '../elements/Footer.js';
|
|
20
22
|
import { Header } from '../elements/Header.js';
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
parseOnOffAttribute,
|
|
54
56
|
} from '../utils/parsingHelpers.js';
|
|
55
57
|
import { halfPointsToPoints } from '../utils/units.js';
|
|
58
|
+
import { normalizeAnchorSizeRel } from '../utils/xmlSanitization.js';
|
|
56
59
|
import type { ShadingConfig } from '../elements/CommonTypes.js';
|
|
57
60
|
|
|
58
61
|
// Create scoped logger for DocumentParser operations
|
|
@@ -133,6 +136,7 @@ export class DocumentParser {
|
|
|
133
136
|
themeColor?: string;
|
|
134
137
|
themeTint?: string;
|
|
135
138
|
themeShade?: string;
|
|
139
|
+
rawInnerXml?: string;
|
|
136
140
|
};
|
|
137
141
|
}> {
|
|
138
142
|
const logger = getLogger();
|
|
@@ -368,9 +372,18 @@ export class DocumentParser {
|
|
|
368
372
|
parsed['w:sdt'],
|
|
369
373
|
relationshipManager,
|
|
370
374
|
zipHandler,
|
|
371
|
-
imageManager
|
|
375
|
+
imageManager,
|
|
376
|
+
elementXml
|
|
372
377
|
);
|
|
373
|
-
if (sdt)
|
|
378
|
+
if (sdt) {
|
|
379
|
+
bodyElements.push(sdt);
|
|
380
|
+
} else {
|
|
381
|
+
// A null result means the content control could not be modeled.
|
|
382
|
+
// Preserve the raw block so the SDT and everything inside it
|
|
383
|
+
// survives the save, mirroring the failed registered-element
|
|
384
|
+
// path below — otherwise the whole control would be deleted.
|
|
385
|
+
bodyElements.push(new PreservedElement(elementXml, 'sdt', 'block'));
|
|
386
|
+
}
|
|
374
387
|
pos = next.pos + elementXml.length;
|
|
375
388
|
} else {
|
|
376
389
|
pos = next.pos + 1;
|
|
@@ -1072,6 +1085,7 @@ export class DocumentParser {
|
|
|
1072
1085
|
| 'w:permEnd'
|
|
1073
1086
|
| 'm:oMath'
|
|
1074
1087
|
| 'w:ruby'
|
|
1088
|
+
| 'w:sdt'
|
|
1075
1089
|
| 'w:commentRangeStart'
|
|
1076
1090
|
| 'w:commentRangeEnd';
|
|
1077
1091
|
pos: number;
|
|
@@ -1093,6 +1107,7 @@ export class DocumentParser {
|
|
|
1093
1107
|
let permEndIndex = 0;
|
|
1094
1108
|
let oMathIndex = 0;
|
|
1095
1109
|
let rubyIndex = 0;
|
|
1110
|
+
let sdtIndex = 0;
|
|
1096
1111
|
let commentRangeStartIndex = 0;
|
|
1097
1112
|
let commentRangeEndIndex = 0;
|
|
1098
1113
|
|
|
@@ -1252,6 +1267,18 @@ export class DocumentParser {
|
|
|
1252
1267
|
index: rubyIndex++,
|
|
1253
1268
|
});
|
|
1254
1269
|
searchPos = selfClosing ? tagEnd + 1 : findClosingTagEnd(paraContent, 'w:ruby', tagEnd);
|
|
1270
|
+
} else if (tagName === 'w:sdt') {
|
|
1271
|
+
// Inline structured document tag (CT_SdtRun, ECMA-376 §17.5.2.31) —
|
|
1272
|
+
// date pickers, dropdowns, citations inline in a sentence. Preserve
|
|
1273
|
+
// as raw XML; skipping past the closing tag also keeps run indices
|
|
1274
|
+
// aligned with pElement['w:r'] (sdt-nested runs are not direct
|
|
1275
|
+
// paragraph children in the parsed object).
|
|
1276
|
+
children.push({
|
|
1277
|
+
type: 'w:sdt',
|
|
1278
|
+
pos: tagStart,
|
|
1279
|
+
index: sdtIndex++,
|
|
1280
|
+
});
|
|
1281
|
+
searchPos = selfClosing ? tagEnd + 1 : findClosingTagEnd(paraContent, 'w:sdt', tagEnd);
|
|
1255
1282
|
} else {
|
|
1256
1283
|
searchPos = tagEnd + 1;
|
|
1257
1284
|
}
|
|
@@ -1340,21 +1367,57 @@ export class DocumentParser {
|
|
|
1340
1367
|
if (runXml) {
|
|
1341
1368
|
paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
|
|
1342
1369
|
}
|
|
1370
|
+
} else if (runObj['mc:AlternateContent']) {
|
|
1371
|
+
// Word 2010+ emits every text box, WordArt, and wps shape as
|
|
1372
|
+
// <w:r><mc:AlternateContent><mc:Choice>… with a VML fallback
|
|
1373
|
+
// (ECMA-376 Part 3 §10). There is no editing model for these,
|
|
1374
|
+
// so preserve the whole run verbatim — otherwise the shape and
|
|
1375
|
+
// all its w:txbxContent text are deleted on save.
|
|
1376
|
+
const runXml = extractRunXmlAtPosition(child.pos);
|
|
1377
|
+
if (runXml) {
|
|
1378
|
+
paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
|
|
1379
|
+
}
|
|
1343
1380
|
} else if (runObj['w:drawing']) {
|
|
1381
|
+
let imageRun: ImageRun | null = null;
|
|
1344
1382
|
if (zipHandler && imageManager) {
|
|
1345
|
-
|
|
1383
|
+
imageRun = await this.parseDrawingFromObject(
|
|
1346
1384
|
runObj['w:drawing'],
|
|
1347
1385
|
zipHandler,
|
|
1348
1386
|
relationshipManager,
|
|
1349
1387
|
imageManager
|
|
1350
1388
|
);
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1389
|
+
}
|
|
1390
|
+
if (imageRun) {
|
|
1391
|
+
// Preserve the parent run's w:rPr (rFonts, noProof, b, etc.)
|
|
1392
|
+
// — without this, ImageRun.toXML() emits <w:r><w:drawing/></w:r>
|
|
1393
|
+
// and Word recalculates line height with the default font,
|
|
1394
|
+
// shifting the image and clipping it into adjacent cells.
|
|
1395
|
+
this.parseRunPropertiesFromObject(runObj['w:rPr'], imageRun);
|
|
1396
|
+
paragraph.addRun(imageRun);
|
|
1397
|
+
// EG_RunInnerContent (ECMA-376 §17.3.3) lets a single run mix a
|
|
1398
|
+
// drawing with text/tab/break siblings. Word isolates drawings,
|
|
1399
|
+
// but other generators do not — parse any sibling content as a
|
|
1400
|
+
// separate Run so its text is not dropped alongside the image.
|
|
1401
|
+
if (
|
|
1402
|
+
runObj['w:t'] ||
|
|
1403
|
+
runObj['w:tab'] ||
|
|
1404
|
+
runObj['w:br'] ||
|
|
1405
|
+
runObj['w:cr'] ||
|
|
1406
|
+
runObj['w:noBreakHyphen']
|
|
1407
|
+
) {
|
|
1408
|
+
const siblingRun = this.parseRunFromObject(runObj);
|
|
1409
|
+
if (siblingRun) {
|
|
1410
|
+
paragraph.addRun(siblingRun);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
} else {
|
|
1414
|
+
// Charts (c:chart), SmartArt (dgm:relIds), and other
|
|
1415
|
+
// non-picture graphicData have no editing model — preserve
|
|
1416
|
+
// the whole run verbatim so the drawing reference survives
|
|
1417
|
+
// round-trip instead of leaving an orphaned part.
|
|
1418
|
+
const runXml = extractRunXmlAtPosition(child.pos);
|
|
1419
|
+
if (runXml) {
|
|
1420
|
+
paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
|
|
1358
1421
|
}
|
|
1359
1422
|
}
|
|
1360
1423
|
} else if (runObj['w:pict']) {
|
|
@@ -1401,16 +1464,22 @@ export class DocumentParser {
|
|
|
1401
1464
|
if (child.index < hyperlinkArray.length) {
|
|
1402
1465
|
const hyperlinkObj = hyperlinkArray[child.index];
|
|
1403
1466
|
|
|
1404
|
-
// Hyperlinks containing tracked changes (w:del/w:ins
|
|
1405
|
-
//
|
|
1467
|
+
// Hyperlinks containing tracked changes (w:del/w:ins/w:moveFrom/
|
|
1468
|
+
// w:moveTo inside w:hyperlink) are always flattened to an editable
|
|
1469
|
+
// Hyperlink object, regardless of the revisionHandling mode (including
|
|
1470
|
+
// 'preserve'). This is a deliberate exception to 'preserve' — see the
|
|
1471
|
+
// flattening note below and the core module documentation.
|
|
1406
1472
|
const hasRevisionChildren =
|
|
1407
1473
|
hyperlinkObj['w:del'] ||
|
|
1408
1474
|
hyperlinkObj['w:ins'] ||
|
|
1409
1475
|
hyperlinkObj['w:moveFrom'] ||
|
|
1410
1476
|
hyperlinkObj['w:moveTo'];
|
|
1411
1477
|
if (hasRevisionChildren) {
|
|
1412
|
-
// Flatten revisions to make hyperlink editable (setUrl/setText)
|
|
1413
|
-
//
|
|
1478
|
+
// Flatten revisions to make the hyperlink editable (setUrl/setText):
|
|
1479
|
+
// unwrap w:ins/w:moveTo runs (kept), drop w:del/w:moveFrom content.
|
|
1480
|
+
// Applied in every load mode — 'preserve' does not retain revision
|
|
1481
|
+
// markup inside hyperlinks. Trades in-hyperlink revision fidelity
|
|
1482
|
+
// for editability.
|
|
1414
1483
|
const flattenedObj = { ...hyperlinkObj };
|
|
1415
1484
|
const allRuns: any[] = [];
|
|
1416
1485
|
|
|
@@ -1566,6 +1635,14 @@ export class DocumentParser {
|
|
|
1566
1635
|
if (elementXml) {
|
|
1567
1636
|
paragraph.addContent(new PreservedElement(elementXml, child.type, 'inline'));
|
|
1568
1637
|
}
|
|
1638
|
+
} else if (child.type === 'w:sdt') {
|
|
1639
|
+
// Inline content control — no run-level editing model, so preserve
|
|
1640
|
+
// the whole w:sdt verbatim; dropping it deletes the user-visible
|
|
1641
|
+
// text inside the control on every save.
|
|
1642
|
+
const sdtXml = extractElementXmlAtPosition(child.pos, 'w:sdt');
|
|
1643
|
+
if (sdtXml) {
|
|
1644
|
+
paragraph.addContent(new PreservedElement(sdtXml, 'w:sdt', 'inline'));
|
|
1645
|
+
}
|
|
1569
1646
|
}
|
|
1570
1647
|
}
|
|
1571
1648
|
}
|
|
@@ -1634,39 +1711,98 @@ export class DocumentParser {
|
|
|
1634
1711
|
// that is NOT inside hyperlinks to avoid duplicate content
|
|
1635
1712
|
const hyperlinkXmls = XMLParser.extractElements(revisionXml, 'w:hyperlink');
|
|
1636
1713
|
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1714
|
+
// Blank out hyperlinks with same-length padding (not removal) so that
|
|
1715
|
+
// standalone-run positions still line up with revisionXml — needed below
|
|
1716
|
+
// to restore the original document order of interleaved runs/hyperlinks.
|
|
1717
|
+
// Use split().join() instead of replace() to blank ALL occurrences of
|
|
1718
|
+
// identical hyperlinks (replace() only hits the first match, causing
|
|
1719
|
+
// duplicate content)
|
|
1640
1720
|
let xmlWithoutHyperlinks = revisionXml;
|
|
1641
1721
|
for (const hyperlinkXml of hyperlinkXmls) {
|
|
1642
|
-
xmlWithoutHyperlinks = xmlWithoutHyperlinks
|
|
1722
|
+
xmlWithoutHyperlinks = xmlWithoutHyperlinks
|
|
1723
|
+
.split(hyperlinkXml)
|
|
1724
|
+
.join(' '.repeat(hyperlinkXml.length));
|
|
1643
1725
|
}
|
|
1644
1726
|
|
|
1645
1727
|
// Extract runs from the XML without hyperlinks (these are standalone runs)
|
|
1646
1728
|
const runXmls = XMLParser.extractElements(xmlWithoutHyperlinks, 'w:r');
|
|
1647
1729
|
|
|
1730
|
+
// Interleave runs and hyperlinks by source position. Two type-grouped
|
|
1731
|
+
// passes (all runs, then all hyperlinks) would emit every hyperlink
|
|
1732
|
+
// after the last run, reordering revision text on round-trip under
|
|
1733
|
+
// preserve mode.
|
|
1734
|
+
const orderedChildren: { pos: number; type: 'w:r' | 'w:hyperlink'; xml: string }[] = [];
|
|
1735
|
+
let runCursor = 0;
|
|
1736
|
+
for (const runXml of runXmls) {
|
|
1737
|
+
const pos = xmlWithoutHyperlinks.indexOf(runXml, runCursor);
|
|
1738
|
+
orderedChildren.push({ pos: pos === -1 ? runCursor : pos, type: 'w:r', xml: runXml });
|
|
1739
|
+
if (pos !== -1) {
|
|
1740
|
+
runCursor = pos + runXml.length;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
let hyperlinkCursor = 0;
|
|
1744
|
+
for (const hyperlinkXml of hyperlinkXmls) {
|
|
1745
|
+
const pos = revisionXml.indexOf(hyperlinkXml, hyperlinkCursor);
|
|
1746
|
+
orderedChildren.push({
|
|
1747
|
+
pos: pos === -1 ? hyperlinkCursor : pos,
|
|
1748
|
+
type: 'w:hyperlink',
|
|
1749
|
+
xml: hyperlinkXml,
|
|
1750
|
+
});
|
|
1751
|
+
if (pos !== -1) {
|
|
1752
|
+
hyperlinkCursor = pos + hyperlinkXml.length;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
orderedChildren.sort((a, b) => a.pos - b.pos);
|
|
1756
|
+
|
|
1648
1757
|
// Use RevisionContent to hold both Run and Hyperlink objects
|
|
1649
1758
|
const content: import('../elements/RevisionContent.js').RevisionContent[] = [];
|
|
1650
1759
|
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1760
|
+
for (const child of orderedChildren) {
|
|
1761
|
+
if (child.type === 'w:hyperlink') {
|
|
1762
|
+
// Parse hyperlink inside revision (for tracked hyperlink changes)
|
|
1763
|
+
const hyperlinkObj = XMLParser.parseToObject(child.xml, { trimValues: false });
|
|
1764
|
+
const hyperlinkResult = this.parseHyperlinkFromObject(
|
|
1765
|
+
hyperlinkObj['w:hyperlink'],
|
|
1766
|
+
relationshipManager
|
|
1767
|
+
);
|
|
1768
|
+
if (hyperlinkResult.hyperlink) {
|
|
1769
|
+
content.push(hyperlinkResult.hyperlink);
|
|
1770
|
+
}
|
|
1771
|
+
// Collect bookmarks from hyperlinks inside revisions
|
|
1772
|
+
result.bookmarkStarts.push(...hyperlinkResult.bookmarkStarts);
|
|
1773
|
+
result.bookmarkEnds.push(...hyperlinkResult.bookmarkEnds);
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Parse standalone run (not inside hyperlinks)
|
|
1778
|
+
const runXml = child.xml;
|
|
1654
1779
|
const runObj = XMLParser.parseToObject(runXml, { trimValues: false });
|
|
1655
1780
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1656
1781
|
const runElement = runObj['w:r'] as any;
|
|
1657
1782
|
|
|
1658
1783
|
// Check if this run contains a drawing (image)
|
|
1659
1784
|
if (runElement?.['w:drawing']) {
|
|
1785
|
+
let imageRun: ImageRun | null = null;
|
|
1660
1786
|
if (zipHandler && imageManager) {
|
|
1661
|
-
|
|
1787
|
+
imageRun = await this.parseDrawingFromObject(
|
|
1662
1788
|
runElement['w:drawing'],
|
|
1663
1789
|
zipHandler,
|
|
1664
1790
|
relationshipManager,
|
|
1665
1791
|
imageManager
|
|
1666
1792
|
);
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1793
|
+
}
|
|
1794
|
+
if (imageRun) {
|
|
1795
|
+
imageRun.setRawRunXml(runXml);
|
|
1796
|
+
content.push(imageRun);
|
|
1797
|
+
} else {
|
|
1798
|
+
// Non-picture graphicData (charts, SmartArt) — RevisionContent
|
|
1799
|
+
// cannot hold a PreservedElement, so carry the w:drawing through
|
|
1800
|
+
// as raw XML inside a passthrough run to keep the reference.
|
|
1801
|
+
const drawingXmls = XMLParser.extractElements(runXml, 'w:drawing');
|
|
1802
|
+
if (drawingXmls.length > 0 && drawingXmls[0]) {
|
|
1803
|
+
const run = Run.createFromContent([{ type: 'vml', rawXml: drawingXmls[0] }]);
|
|
1804
|
+
this.parseRunPropertiesFromObject(runElement['w:rPr'], run);
|
|
1805
|
+
content.push(run);
|
|
1670
1806
|
}
|
|
1671
1807
|
}
|
|
1672
1808
|
} else {
|
|
@@ -1678,21 +1814,6 @@ export class DocumentParser {
|
|
|
1678
1814
|
}
|
|
1679
1815
|
}
|
|
1680
1816
|
|
|
1681
|
-
// Parse hyperlinks inside revision (for tracked hyperlink changes)
|
|
1682
|
-
for (const hyperlinkXml of hyperlinkXmls) {
|
|
1683
|
-
const hyperlinkObj = XMLParser.parseToObject(hyperlinkXml, { trimValues: false });
|
|
1684
|
-
const hyperlinkResult = this.parseHyperlinkFromObject(
|
|
1685
|
-
hyperlinkObj['w:hyperlink'],
|
|
1686
|
-
relationshipManager
|
|
1687
|
-
);
|
|
1688
|
-
if (hyperlinkResult.hyperlink) {
|
|
1689
|
-
content.push(hyperlinkResult.hyperlink);
|
|
1690
|
-
}
|
|
1691
|
-
// Collect bookmarks from hyperlinks inside revisions
|
|
1692
|
-
result.bookmarkStarts.push(...hyperlinkResult.bookmarkStarts);
|
|
1693
|
-
result.bookmarkEnds.push(...hyperlinkResult.bookmarkEnds);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
1817
|
// Extract bookmarks directly inside the revision (not nested in hyperlinks)
|
|
1697
1818
|
const bookmarkStartXmls = XMLParser.extractElements(xmlWithoutHyperlinks, 'w:bookmarkStart');
|
|
1698
1819
|
const bookmarkEndXmls = XMLParser.extractElements(xmlWithoutHyperlinks, 'w:bookmarkEnd');
|
|
@@ -1748,16 +1869,23 @@ export class DocumentParser {
|
|
|
1748
1869
|
/**
|
|
1749
1870
|
* Parses comments from word/comments.xml
|
|
1750
1871
|
* @param commentsXml - Raw XML content of comments.xml
|
|
1872
|
+
* @param commentsExtendedXml - Optional word/commentsExtended.xml content;
|
|
1873
|
+
* its w15:commentEx entries carry the resolved state (w15:done) keyed by
|
|
1874
|
+
* the w14:paraId of each comment's last paragraph
|
|
1751
1875
|
* @returns Array of parsed Comment objects
|
|
1752
1876
|
*/
|
|
1753
|
-
parseCommentsXml(commentsXml: string): Comment[] {
|
|
1877
|
+
parseCommentsXml(commentsXml: string, commentsExtendedXml?: string): Comment[] {
|
|
1754
1878
|
const comments: Comment[] = [];
|
|
1755
1879
|
|
|
1880
|
+
const doneByParaId = commentsExtendedXml
|
|
1881
|
+
? this.parseCommentsExtendedDone(commentsExtendedXml)
|
|
1882
|
+
: undefined;
|
|
1883
|
+
|
|
1756
1884
|
// Extract all w:comment elements
|
|
1757
1885
|
const commentXmls = XMLParser.extractElements(commentsXml, 'w:comment');
|
|
1758
1886
|
|
|
1759
1887
|
for (const commentXml of commentXmls) {
|
|
1760
|
-
const comment = this.parseCommentFromXml(commentXml);
|
|
1888
|
+
const comment = this.parseCommentFromXml(commentXml, doneByParaId);
|
|
1761
1889
|
if (comment) {
|
|
1762
1890
|
comments.push(comment);
|
|
1763
1891
|
}
|
|
@@ -1766,12 +1894,35 @@ export class DocumentParser {
|
|
|
1766
1894
|
return comments;
|
|
1767
1895
|
}
|
|
1768
1896
|
|
|
1897
|
+
/**
|
|
1898
|
+
* Builds the paraId -> done map from commentsExtended.xml. Word stores
|
|
1899
|
+
* comment resolution there (w15:commentEx w15:done) rather than on
|
|
1900
|
+
* w:comment, which declares no done attribute.
|
|
1901
|
+
*/
|
|
1902
|
+
private parseCommentsExtendedDone(commentsExtendedXml: string): Map<string, boolean> {
|
|
1903
|
+
const doneByParaId = new Map<string, boolean>();
|
|
1904
|
+
for (const commentExXml of XMLParser.extractElements(commentsExtendedXml, 'w15:commentEx')) {
|
|
1905
|
+
const paraId = XMLParser.extractAttribute(commentExXml, 'w15:paraId');
|
|
1906
|
+
if (!paraId) {
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
const done = parseOnOffAttribute(XMLParser.extractAttribute(commentExXml, 'w15:done'));
|
|
1910
|
+
doneByParaId.set(paraId.toUpperCase(), done);
|
|
1911
|
+
}
|
|
1912
|
+
return doneByParaId;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1769
1915
|
/**
|
|
1770
1916
|
* Parses a single comment element from XML
|
|
1771
1917
|
* @param commentXml - XML string for one w:comment element
|
|
1918
|
+
* @param doneByParaId - Resolved state from commentsExtended.xml, keyed by
|
|
1919
|
+
* uppercased last-paragraph w14:paraId
|
|
1772
1920
|
* @returns Parsed Comment or null
|
|
1773
1921
|
*/
|
|
1774
|
-
private parseCommentFromXml(
|
|
1922
|
+
private parseCommentFromXml(
|
|
1923
|
+
commentXml: string,
|
|
1924
|
+
doneByParaId?: Map<string, boolean>
|
|
1925
|
+
): Comment | null {
|
|
1775
1926
|
try {
|
|
1776
1927
|
// Extract attributes
|
|
1777
1928
|
const idAttr = XMLParser.extractAttribute(commentXml, 'w:id');
|
|
@@ -1792,20 +1943,45 @@ export class DocumentParser {
|
|
|
1792
1943
|
const id = parseInt(idAttr, 10);
|
|
1793
1944
|
const date = dateAttr ? new Date(dateAttr) : new Date();
|
|
1794
1945
|
const parentId = parentIdAttr ? parseInt(parentIdAttr, 10) : undefined;
|
|
1795
|
-
//
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1946
|
+
// Accept legacy done attributes as ST_OnOff (1/0/true/false/on/off);
|
|
1947
|
+
// commentsExtended.xml (checked below) is the authoritative source
|
|
1948
|
+
let done = parseOnOffAttribute(doneAttr);
|
|
1949
|
+
|
|
1950
|
+
// Parse content per source paragraph so regeneration can restore the
|
|
1951
|
+
// original <w:p> boundaries, paragraph properties, and hyperlink
|
|
1952
|
+
// wrappers — multi-paragraph comments are legal per ECMA-376
|
|
1953
|
+
// §17.13.4.2 and flattening them concatenates words across the lost
|
|
1954
|
+
// paragraph breaks
|
|
1799
1955
|
const runs: Run[] = [];
|
|
1800
|
-
const
|
|
1956
|
+
const paragraphs: CommentParagraph[] = [];
|
|
1957
|
+
const paraXmls = XMLParser.extractElements(commentXml, 'w:p');
|
|
1958
|
+
const runSources = paraXmls.length > 0 ? paraXmls : [commentXml];
|
|
1801
1959
|
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1960
|
+
// commentsExtended.xml references a comment via the w14:paraId of its
|
|
1961
|
+
// last paragraph; when an entry exists it overrides the legacy attr
|
|
1962
|
+
if (doneByParaId && paraXmls.length > 0) {
|
|
1963
|
+
const lastParaXml = paraXmls[paraXmls.length - 1]!;
|
|
1964
|
+
const lastParaId = XMLParser.extractAttribute(lastParaXml, 'w14:paraId');
|
|
1965
|
+
if (lastParaId) {
|
|
1966
|
+
const extendedDone = doneByParaId.get(lastParaId.toUpperCase());
|
|
1967
|
+
if (extendedDone !== undefined) {
|
|
1968
|
+
done = extendedDone;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
for (const sourceXml of runSources) {
|
|
1974
|
+
const content = this.parseCommentParagraphContent(sourceXml);
|
|
1975
|
+
for (const item of content) {
|
|
1976
|
+
if (item instanceof Run) {
|
|
1977
|
+
runs.push(item);
|
|
1978
|
+
} else {
|
|
1979
|
+
runs.push(...item.runs);
|
|
1980
|
+
}
|
|
1808
1981
|
}
|
|
1982
|
+
const pPr =
|
|
1983
|
+
paraXmls.length > 0 ? XMLParser.extractElements(sourceXml, 'w:pPr')[0] : undefined;
|
|
1984
|
+
paragraphs.push({ pPr, content });
|
|
1809
1985
|
}
|
|
1810
1986
|
|
|
1811
1987
|
// Create comment with parsed data
|
|
@@ -1819,6 +1995,10 @@ export class DocumentParser {
|
|
|
1819
1995
|
done,
|
|
1820
1996
|
});
|
|
1821
1997
|
|
|
1998
|
+
if (paraXmls.length > 0) {
|
|
1999
|
+
comment.setParagraphs(paragraphs);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1822
2002
|
return comment;
|
|
1823
2003
|
} catch (error: unknown) {
|
|
1824
2004
|
defaultLogger.warn(
|
|
@@ -1829,15 +2009,76 @@ export class DocumentParser {
|
|
|
1829
2009
|
}
|
|
1830
2010
|
}
|
|
1831
2011
|
|
|
2012
|
+
/**
|
|
2013
|
+
* Parses one comment paragraph's inline content in document order.
|
|
2014
|
+
* Top-level runs become Run instances; w:hyperlink wrappers pass through
|
|
2015
|
+
* as raw XML (their r:id targets live in word/_rels/comments.xml.rels,
|
|
2016
|
+
* which is preserved verbatim) with their inner runs parsed alongside so
|
|
2017
|
+
* text extraction still sees the link text.
|
|
2018
|
+
* @param sourceXml - XML of one w:p element (or the whole w:comment when
|
|
2019
|
+
* it has no paragraph children)
|
|
2020
|
+
* @returns Ordered paragraph content
|
|
2021
|
+
*/
|
|
2022
|
+
private parseCommentParagraphContent(sourceXml: string): CommentContentItem[] {
|
|
2023
|
+
const positioned: Array<{ pos: number; item: CommentContentItem }> = [];
|
|
2024
|
+
|
|
2025
|
+
const hyperlinkRanges: Array<{ start: number; end: number }> = [];
|
|
2026
|
+
let hyperlinkCursor = 0;
|
|
2027
|
+
for (const hyperlinkXml of XMLParser.extractElements(sourceXml, 'w:hyperlink')) {
|
|
2028
|
+
const start = sourceXml.indexOf(hyperlinkXml, hyperlinkCursor);
|
|
2029
|
+
if (start === -1) {
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
hyperlinkCursor = start + hyperlinkXml.length;
|
|
2033
|
+
hyperlinkRanges.push({ start, end: hyperlinkCursor });
|
|
2034
|
+
|
|
2035
|
+
const innerRuns: Run[] = [];
|
|
2036
|
+
for (const runXml of XMLParser.extractElements(hyperlinkXml, 'w:r')) {
|
|
2037
|
+
const run = this.parseCommentRun(runXml);
|
|
2038
|
+
if (run) {
|
|
2039
|
+
innerRuns.push(run);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
positioned.push({ pos: start, item: { rawXml: hyperlinkXml, runs: innerRuns } });
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
let runCursor = 0;
|
|
2046
|
+
for (const runXml of XMLParser.extractElements(sourceXml, 'w:r')) {
|
|
2047
|
+
const start = sourceXml.indexOf(runXml, runCursor);
|
|
2048
|
+
if (start === -1) {
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
runCursor = start + runXml.length;
|
|
2052
|
+
// Runs inside a hyperlink are already covered by its raw passthrough
|
|
2053
|
+
if (hyperlinkRanges.some((range) => start >= range.start && start < range.end)) {
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
const run = this.parseCommentRun(runXml);
|
|
2057
|
+
if (run) {
|
|
2058
|
+
positioned.push({ pos: start, item: run });
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
return positioned.sort((a, b) => a.pos - b.pos).map((entry) => entry.item);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
private parseCommentRun(runXml: string): Run | null {
|
|
2066
|
+
const runObj = XMLParser.parseToObject(runXml, { trimValues: false });
|
|
2067
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2068
|
+
return this.parseRunFromObject(runObj['w:r'] as any);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
1832
2071
|
/**
|
|
1833
2072
|
* Parses footnotes.xml into Footnote array
|
|
2073
|
+
* @param partRels - Part-scoped relationships (word/_rels/footnotes.xml.rels)
|
|
2074
|
+
* used to resolve hyperlink r:id targets inside footnote content
|
|
1834
2075
|
*/
|
|
1835
|
-
parseFootnotesXml(footnotesXml: string): Footnote[] {
|
|
2076
|
+
parseFootnotesXml(footnotesXml: string, partRels?: RelationshipManager): Footnote[] {
|
|
1836
2077
|
const footnotes: Footnote[] = [];
|
|
1837
2078
|
const footnoteXmls = XMLParser.extractElements(footnotesXml, 'w:footnote');
|
|
1838
2079
|
|
|
1839
2080
|
for (const footnoteXml of footnoteXmls) {
|
|
1840
|
-
const footnote = this.parseFootnoteFromXml(footnoteXml);
|
|
2081
|
+
const footnote = this.parseFootnoteFromXml(footnoteXml, partRels);
|
|
1841
2082
|
if (footnote) {
|
|
1842
2083
|
footnotes.push(footnote);
|
|
1843
2084
|
}
|
|
@@ -1846,7 +2087,10 @@ export class DocumentParser {
|
|
|
1846
2087
|
return footnotes;
|
|
1847
2088
|
}
|
|
1848
2089
|
|
|
1849
|
-
private parseFootnoteFromXml(
|
|
2090
|
+
private parseFootnoteFromXml(
|
|
2091
|
+
footnoteXml: string,
|
|
2092
|
+
partRels?: RelationshipManager
|
|
2093
|
+
): Footnote | null {
|
|
1850
2094
|
try {
|
|
1851
2095
|
const idAttr = XMLParser.extractAttribute(footnoteXml, 'w:id');
|
|
1852
2096
|
const typeAttr = XMLParser.extractAttribute(footnoteXml, 'w:type');
|
|
@@ -1870,7 +2114,7 @@ export class DocumentParser {
|
|
|
1870
2114
|
|
|
1871
2115
|
const paraXmls = XMLParser.extractElements(footnoteXml, 'w:p');
|
|
1872
2116
|
for (const paraXml of paraXmls) {
|
|
1873
|
-
const para = this.parseNoteParaFromXml(paraXml);
|
|
2117
|
+
const para = this.parseNoteParaFromXml(paraXml, partRels);
|
|
1874
2118
|
if (para) {
|
|
1875
2119
|
footnote.addParagraph(para);
|
|
1876
2120
|
}
|
|
@@ -1888,13 +2132,15 @@ export class DocumentParser {
|
|
|
1888
2132
|
|
|
1889
2133
|
/**
|
|
1890
2134
|
* Parses endnotes.xml into Endnote array
|
|
2135
|
+
* @param partRels - Part-scoped relationships (word/_rels/endnotes.xml.rels)
|
|
2136
|
+
* used to resolve hyperlink r:id targets inside endnote content
|
|
1891
2137
|
*/
|
|
1892
|
-
parseEndnotesXml(endnotesXml: string): Endnote[] {
|
|
2138
|
+
parseEndnotesXml(endnotesXml: string, partRels?: RelationshipManager): Endnote[] {
|
|
1893
2139
|
const endnotes: Endnote[] = [];
|
|
1894
2140
|
const endnoteXmls = XMLParser.extractElements(endnotesXml, 'w:endnote');
|
|
1895
2141
|
|
|
1896
2142
|
for (const endnoteXml of endnoteXmls) {
|
|
1897
|
-
const endnote = this.parseEndnoteFromXml(endnoteXml);
|
|
2143
|
+
const endnote = this.parseEndnoteFromXml(endnoteXml, partRels);
|
|
1898
2144
|
if (endnote) {
|
|
1899
2145
|
endnotes.push(endnote);
|
|
1900
2146
|
}
|
|
@@ -1903,7 +2149,7 @@ export class DocumentParser {
|
|
|
1903
2149
|
return endnotes;
|
|
1904
2150
|
}
|
|
1905
2151
|
|
|
1906
|
-
private parseEndnoteFromXml(endnoteXml: string): Endnote | null {
|
|
2152
|
+
private parseEndnoteFromXml(endnoteXml: string, partRels?: RelationshipManager): Endnote | null {
|
|
1907
2153
|
try {
|
|
1908
2154
|
const idAttr = XMLParser.extractAttribute(endnoteXml, 'w:id');
|
|
1909
2155
|
const typeAttr = XMLParser.extractAttribute(endnoteXml, 'w:type');
|
|
@@ -1927,7 +2173,7 @@ export class DocumentParser {
|
|
|
1927
2173
|
|
|
1928
2174
|
const paraXmls = XMLParser.extractElements(endnoteXml, 'w:p');
|
|
1929
2175
|
for (const paraXml of paraXmls) {
|
|
1930
|
-
const para = this.parseNoteParaFromXml(paraXml);
|
|
2176
|
+
const para = this.parseNoteParaFromXml(paraXml, partRels);
|
|
1931
2177
|
if (para) {
|
|
1932
2178
|
endnote.addParagraph(para);
|
|
1933
2179
|
}
|
|
@@ -1945,20 +2191,80 @@ export class DocumentParser {
|
|
|
1945
2191
|
|
|
1946
2192
|
/**
|
|
1947
2193
|
* Parses a paragraph from a footnote or endnote element.
|
|
1948
|
-
* Uses run-level parsing (like comments) to avoid async dependency on
|
|
2194
|
+
* Uses run-level parsing (like comments) to avoid async dependency on the
|
|
2195
|
+
* document relationship manager. Paragraph properties (pStyle FootnoteText
|
|
2196
|
+
* per ECMA-376 §17.7.4) and w:hyperlink wrappers are parsed so regeneration
|
|
2197
|
+
* after a real edit keeps note styling and working links instead of
|
|
2198
|
+
* flattening hyperlinks to plain runs; r:id targets resolve against the
|
|
2199
|
+
* part-scoped rels when provided.
|
|
1949
2200
|
*/
|
|
1950
|
-
private parseNoteParaFromXml(paraXml: string): Paragraph | null {
|
|
2201
|
+
private parseNoteParaFromXml(paraXml: string, partRels?: RelationshipManager): Paragraph | null {
|
|
1951
2202
|
try {
|
|
1952
2203
|
const para = new Paragraph();
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2204
|
+
|
|
2205
|
+
const paraObj = XMLParser.parseToObject(paraXml, { trimValues: false });
|
|
2206
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2207
|
+
const pElement = paraObj['w:p'] as any;
|
|
2208
|
+
if (pElement?.['w:pPr']) {
|
|
2209
|
+
this.parseParagraphPropertiesFromObject(pElement['w:pPr'], para);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// Scan children in document order, consuming w:r/w:hyperlink subtrees
|
|
2213
|
+
// whole so hyperlink inner runs are not double-counted as paragraph
|
|
2214
|
+
// runs; unhandled containers (w:ins, w:smartTag, ...) are descended
|
|
2215
|
+
// into so their nested runs still contribute text as before
|
|
2216
|
+
const pPrEnd = paraXml.indexOf('</w:pPr>');
|
|
2217
|
+
const contentStart = pPrEnd !== -1 ? pPrEnd + '</w:pPr>'.length : paraXml.indexOf('>') + 1;
|
|
2218
|
+
const contentEnd = paraXml.lastIndexOf('</w:p>');
|
|
2219
|
+
if (contentEnd <= contentStart) {
|
|
2220
|
+
return para;
|
|
2221
|
+
}
|
|
2222
|
+
const content = paraXml.substring(contentStart, contentEnd);
|
|
2223
|
+
|
|
2224
|
+
let searchPos = 0;
|
|
2225
|
+
while (searchPos < content.length) {
|
|
2226
|
+
const tagStart = content.indexOf('<', searchPos);
|
|
2227
|
+
if (tagStart === -1) break;
|
|
2228
|
+
const tagEnd = content.indexOf('>', tagStart);
|
|
2229
|
+
if (tagEnd === -1) break;
|
|
2230
|
+
const tagBody = content.substring(tagStart + 1, tagEnd);
|
|
2231
|
+
const tagName = tagBody.split(/[\s/>]/)[0];
|
|
2232
|
+
const selfClosing = tagBody.endsWith('/');
|
|
2233
|
+
|
|
2234
|
+
if ((tagName === 'w:r' || tagName === 'w:hyperlink') && !selfClosing) {
|
|
2235
|
+
const closingTag = `</${tagName}>`;
|
|
2236
|
+
const closingPos = content.indexOf(closingTag, tagEnd);
|
|
2237
|
+
const childEnd = closingPos === -1 ? tagEnd + 1 : closingPos + closingTag.length;
|
|
2238
|
+
const childXml = content.substring(tagStart, childEnd);
|
|
2239
|
+
const childObj = XMLParser.parseToObject(childXml, { trimValues: false });
|
|
2240
|
+
|
|
2241
|
+
if (tagName === 'w:r') {
|
|
2242
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2243
|
+
const run = this.parseRunFromObject(childObj['w:r'] as any);
|
|
2244
|
+
if (run) {
|
|
2245
|
+
para.addRun(run);
|
|
2246
|
+
}
|
|
2247
|
+
} else {
|
|
2248
|
+
const result = this.parseHyperlinkFromObject(
|
|
2249
|
+
childObj['w:hyperlink'],
|
|
2250
|
+
partRels ?? RelationshipManager.create()
|
|
2251
|
+
);
|
|
2252
|
+
if (result.hyperlink) {
|
|
2253
|
+
para.addHyperlink(result.hyperlink);
|
|
2254
|
+
}
|
|
2255
|
+
for (const bookmark of result.bookmarkStarts) {
|
|
2256
|
+
para.addBookmarkStart(bookmark);
|
|
2257
|
+
}
|
|
2258
|
+
for (const bookmark of result.bookmarkEnds) {
|
|
2259
|
+
para.addBookmarkEnd(bookmark);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
searchPos = childEnd;
|
|
2263
|
+
} else {
|
|
2264
|
+
searchPos = tagEnd + 1;
|
|
1960
2265
|
}
|
|
1961
2266
|
}
|
|
2267
|
+
|
|
1962
2268
|
return para;
|
|
1963
2269
|
} catch {
|
|
1964
2270
|
return null;
|
|
@@ -2120,16 +2426,40 @@ export class DocumentParser {
|
|
|
2120
2426
|
if (elementIndex < runArray.length) {
|
|
2121
2427
|
const child = runArray[elementIndex];
|
|
2122
2428
|
if (child['w:drawing']) {
|
|
2429
|
+
let imageRun: ImageRun | null = null;
|
|
2123
2430
|
if (zipHandler && imageManager) {
|
|
2124
|
-
|
|
2431
|
+
imageRun = await this.parseDrawingFromObject(
|
|
2125
2432
|
child['w:drawing'],
|
|
2126
2433
|
zipHandler,
|
|
2127
2434
|
relationshipManager,
|
|
2128
2435
|
imageManager
|
|
2129
2436
|
);
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2437
|
+
}
|
|
2438
|
+
if (imageRun) {
|
|
2439
|
+
this.parseRunPropertiesFromObject(child['w:rPr'], imageRun);
|
|
2440
|
+
paragraph.addRun(imageRun);
|
|
2441
|
+
// A run may legally mix a drawing with text/tab/break
|
|
2442
|
+
// siblings (ECMA-376 §17.3.3) — parse those as a separate
|
|
2443
|
+
// Run so the text is not dropped alongside the image.
|
|
2444
|
+
if (
|
|
2445
|
+
child['w:t'] ||
|
|
2446
|
+
child['w:tab'] ||
|
|
2447
|
+
child['w:br'] ||
|
|
2448
|
+
child['w:cr'] ||
|
|
2449
|
+
child['w:noBreakHyphen']
|
|
2450
|
+
) {
|
|
2451
|
+
const siblingRun = this.parseRunFromObject(child);
|
|
2452
|
+
if (siblingRun) {
|
|
2453
|
+
paragraph.addRun(siblingRun);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
} else {
|
|
2457
|
+
// No raw paragraph XML on this path — rebuild the run from
|
|
2458
|
+
// the parsed object so non-picture drawings (charts,
|
|
2459
|
+
// SmartArt) survive round-trip instead of being dropped.
|
|
2460
|
+
const runXml = this.objectToXml({ 'w:r': child });
|
|
2461
|
+
if (runXml) {
|
|
2462
|
+
paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
|
|
2133
2463
|
}
|
|
2134
2464
|
}
|
|
2135
2465
|
} else {
|
|
@@ -2181,17 +2511,41 @@ export class DocumentParser {
|
|
|
2181
2511
|
|
|
2182
2512
|
for (const child of runChildren) {
|
|
2183
2513
|
if (child['w:drawing']) {
|
|
2514
|
+
let imageRun: ImageRun | null = null;
|
|
2184
2515
|
if (zipHandler && imageManager) {
|
|
2185
2516
|
// Parse as image run
|
|
2186
|
-
|
|
2517
|
+
imageRun = await this.parseDrawingFromObject(
|
|
2187
2518
|
child['w:drawing'],
|
|
2188
2519
|
zipHandler,
|
|
2189
2520
|
relationshipManager,
|
|
2190
2521
|
imageManager
|
|
2191
2522
|
);
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2523
|
+
}
|
|
2524
|
+
if (imageRun) {
|
|
2525
|
+
this.parseRunPropertiesFromObject(child['w:rPr'], imageRun);
|
|
2526
|
+
paragraph.addRun(imageRun);
|
|
2527
|
+
// A run may legally mix a drawing with text/tab/break siblings
|
|
2528
|
+
// (ECMA-376 §17.3.3) — parse those as a separate Run so the text
|
|
2529
|
+
// is not dropped alongside the image.
|
|
2530
|
+
if (
|
|
2531
|
+
child['w:t'] ||
|
|
2532
|
+
child['w:tab'] ||
|
|
2533
|
+
child['w:br'] ||
|
|
2534
|
+
child['w:cr'] ||
|
|
2535
|
+
child['w:noBreakHyphen']
|
|
2536
|
+
) {
|
|
2537
|
+
const siblingRun = this.parseRunFromObject(child);
|
|
2538
|
+
if (siblingRun) {
|
|
2539
|
+
paragraph.addRun(siblingRun);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
} else {
|
|
2543
|
+
// No raw paragraph XML on this path — rebuild the run from the
|
|
2544
|
+
// parsed object so non-picture drawings (charts, SmartArt)
|
|
2545
|
+
// survive round-trip instead of being dropped.
|
|
2546
|
+
const runXml = this.objectToXml({ 'w:r': child });
|
|
2547
|
+
if (runXml) {
|
|
2548
|
+
paragraph.addContent(new PreservedElement(runXml, 'w:r', 'inline'));
|
|
2195
2549
|
}
|
|
2196
2550
|
}
|
|
2197
2551
|
} else {
|
|
@@ -4154,10 +4508,15 @@ export class DocumentParser {
|
|
|
4154
4508
|
if (node === undefined || node === null) {
|
|
4155
4509
|
return '';
|
|
4156
4510
|
}
|
|
4511
|
+
// XMLParser.parseElementToObject already unescaped entities while
|
|
4512
|
+
// building #text, so the value here is plain text. A second
|
|
4513
|
+
// unescapeXml would corrupt literal entity sequences (e.g. text that
|
|
4514
|
+
// was authored as "<" — stored as "&lt;" — would collapse to
|
|
4515
|
+
// "<"), breaking load→save fidelity.
|
|
4157
4516
|
if (typeof node === 'object') {
|
|
4158
|
-
return
|
|
4517
|
+
return node['#text'] != null ? String(node['#text']) : '';
|
|
4159
4518
|
}
|
|
4160
|
-
return
|
|
4519
|
+
return String(node);
|
|
4161
4520
|
};
|
|
4162
4521
|
|
|
4163
4522
|
// Field-character attributes (w:dirty, w:fldLock, w:lock on w:fldChar) are
|
|
@@ -4989,6 +5348,11 @@ export class DocumentParser {
|
|
|
4989
5348
|
anchor: finalAnchor,
|
|
4990
5349
|
text: displayText,
|
|
4991
5350
|
formatting,
|
|
5351
|
+
// Word-authored hyperlinks usually carry only <w:rStyle w:val="Hyperlink"/>
|
|
5352
|
+
// (or no rPr at all) — merging the constructor's default styling
|
|
5353
|
+
// underneath would inject direct formatting that never existed in the
|
|
5354
|
+
// source and override the document's Hyperlink character style
|
|
5355
|
+
preserveFormatting: true,
|
|
4992
5356
|
tooltip,
|
|
4993
5357
|
relationshipId: finalRelationshipId,
|
|
4994
5358
|
tgtFrame,
|
|
@@ -4996,9 +5360,20 @@ export class DocumentParser {
|
|
|
4996
5360
|
docLocation,
|
|
4997
5361
|
});
|
|
4998
5362
|
|
|
4999
|
-
//
|
|
5000
|
-
//
|
|
5001
|
-
|
|
5363
|
+
// Replace the constructor-built run with the parsed run(s). Word splits
|
|
5364
|
+
// a hyperlink's display text into one run per formatting change, so
|
|
5365
|
+
// multi-run sources must keep every run with its own rPr — collapsing
|
|
5366
|
+
// to the first run's formatting silently drops bold/italic/color on
|
|
5367
|
+
// the later runs. Single runs are preserved verbatim when they carry
|
|
5368
|
+
// text or structure like TOC tabs/breaks that a plain text run cannot
|
|
5369
|
+
// represent. Empty-text runs keep the constructor run so the
|
|
5370
|
+
// display-text fallback ('[Link]'/URL) survives.
|
|
5371
|
+
if (parsedRuns.length > 1 && (text || parsedRuns.some((r) => r.getContent().length > 1))) {
|
|
5372
|
+
hyperlink.setRuns(parsedRuns);
|
|
5373
|
+
} else if (
|
|
5374
|
+
parsedRun &&
|
|
5375
|
+
((parsedRuns.length === 1 && text) || parsedRun.getContent().length > 1)
|
|
5376
|
+
) {
|
|
5002
5377
|
hyperlink.setRun(parsedRun);
|
|
5003
5378
|
}
|
|
5004
5379
|
|
|
@@ -5220,6 +5595,32 @@ export class DocumentParser {
|
|
|
5220
5595
|
dirty,
|
|
5221
5596
|
});
|
|
5222
5597
|
|
|
5598
|
+
// Per ECMA-376 §17.16.16 CT_SimpleField, the child runs hold the
|
|
5599
|
+
// field's current (cached) result. Word does not refresh fields on
|
|
5600
|
+
// open by default, so the cached text + per-run formatting must
|
|
5601
|
+
// survive round-trip — otherwise the visible content is silently
|
|
5602
|
+
// replaced by a synthetic placeholder on save.
|
|
5603
|
+
const resultRuns = fieldObj['w:r'];
|
|
5604
|
+
const resultRunArray = Array.isArray(resultRuns)
|
|
5605
|
+
? resultRuns
|
|
5606
|
+
: resultRuns
|
|
5607
|
+
? [resultRuns]
|
|
5608
|
+
: [];
|
|
5609
|
+
const cachedRuns: CachedFieldResultRun[] = [];
|
|
5610
|
+
for (const runObj of resultRunArray) {
|
|
5611
|
+
const run = this.parseRunFromObject(runObj);
|
|
5612
|
+
if (run) {
|
|
5613
|
+
const runFormatting = run.getFormatting();
|
|
5614
|
+
cachedRuns.push({
|
|
5615
|
+
text: run.getText(),
|
|
5616
|
+
formatting: Object.keys(runFormatting).length > 0 ? runFormatting : undefined,
|
|
5617
|
+
});
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
if (cachedRuns.length > 0) {
|
|
5621
|
+
field.setCachedResultRuns(cachedRuns);
|
|
5622
|
+
}
|
|
5623
|
+
|
|
5223
5624
|
return field;
|
|
5224
5625
|
} catch (error: unknown) {
|
|
5225
5626
|
defaultLogger.warn(
|
|
@@ -5464,7 +5865,11 @@ export class DocumentParser {
|
|
|
5464
5865
|
// coerces purely-numeric font names ("2010", etc.) to JS
|
|
5465
5866
|
// numbers; cast through String() so RunFormatting's
|
|
5466
5867
|
// declared-string font fields keep their type contract.
|
|
5467
|
-
|
|
5868
|
+
// Use setFontAscii (not setFont) so only the declared ascii slot is set;
|
|
5869
|
+
// setFont mirrors into hAnsi/cs for programmatic convenience, which would
|
|
5870
|
+
// fabricate those slots on round-trip and override style/theme-inherited
|
|
5871
|
+
// fonts.
|
|
5872
|
+
if (rFonts['@_w:ascii'] !== undefined) run.setFontAscii(String(rFonts['@_w:ascii']));
|
|
5468
5873
|
// Parse additional font variants per ECMA-376 Part 1 §17.3.2.26
|
|
5469
5874
|
if (rFonts['@_w:hAnsi'] !== undefined) run.setFontHAnsi(String(rFonts['@_w:hAnsi']));
|
|
5470
5875
|
if (rFonts['@_w:eastAsia'] !== undefined) run.setFontEastAsia(String(rFonts['@_w:eastAsia']));
|
|
@@ -6431,7 +6836,11 @@ export class DocumentParser {
|
|
|
6431
6836
|
'wp:cNvGraphicFramePr',
|
|
6432
6837
|
'a:graphic',
|
|
6433
6838
|
]);
|
|
6434
|
-
if (anchorExtras)
|
|
6839
|
+
if (anchorExtras) {
|
|
6840
|
+
// Repair malformed wp14:sizeRelH/sizeRelV (bare-text percentage) so the
|
|
6841
|
+
// preserved passthrough stays schema-valid on save.
|
|
6842
|
+
image._setRawPassthrough('anchor-extra', normalizeAnchorSizeRel(anchorExtras));
|
|
6843
|
+
}
|
|
6435
6844
|
}
|
|
6436
6845
|
|
|
6437
6846
|
// DocPr extras (a:hlinkClick, a:hlinkHover, a:extLst)
|
|
@@ -6496,7 +6905,13 @@ export class DocumentParser {
|
|
|
6496
6905
|
|
|
6497
6906
|
return {
|
|
6498
6907
|
type,
|
|
6499
|
-
|
|
6908
|
+
// wrapText only exists on CT_WrapSquare/Tight/Through. For wrapNone and
|
|
6909
|
+
// wrapTopAndBottom it must stay undefined so serialization never emits an
|
|
6910
|
+
// (invalid) wrapText attribute on those elements.
|
|
6911
|
+
side:
|
|
6912
|
+
type === 'none' || type === 'topAndBottom'
|
|
6913
|
+
? wrapObj['@_wrapText']
|
|
6914
|
+
: wrapObj['@_wrapText'] || 'bothSides',
|
|
6500
6915
|
// Distance attributes are on the wrap element, not the anchor
|
|
6501
6916
|
distanceTop: wrapObj['@_distT'] ? parseInt(wrapObj['@_distT'], 10) : undefined,
|
|
6502
6917
|
distanceBottom: wrapObj['@_distB'] ? parseInt(wrapObj['@_distB'], 10) : undefined,
|
|
@@ -7931,7 +8346,8 @@ export class DocumentParser {
|
|
|
7931
8346
|
sdtObj: any,
|
|
7932
8347
|
relationshipManager: RelationshipManager,
|
|
7933
8348
|
zipHandler: ZipHandler,
|
|
7934
|
-
imageManager: ImageManager
|
|
8349
|
+
imageManager: ImageManager,
|
|
8350
|
+
rawSdtXml?: string
|
|
7935
8351
|
): Promise<StructuredDocumentTag | TableOfContentsElement | null> {
|
|
7936
8352
|
try {
|
|
7937
8353
|
if (!sdtObj) return null;
|
|
@@ -8025,10 +8441,18 @@ export class DocumentParser {
|
|
|
8025
8441
|
// <w14:checked> is CT_OnOff in the Word 2010+ extension namespace.
|
|
8026
8442
|
// Honour every ST_OnOff literal ("1"/"0"/"true"/"false"/"on"/"off")
|
|
8027
8443
|
// and treat a bare self-closing `<w14:checked/>` as true.
|
|
8444
|
+
// CT_SdtCheckboxSymbol pairs the character code with a glyph font;
|
|
8445
|
+
// capture w14:font (String-cast — XMLParser coerces numeric-looking
|
|
8446
|
+
// values) so a custom font like Wingdings survives re-serialization
|
|
8447
|
+
// instead of being rewritten to the MS Gothic default.
|
|
8448
|
+
const rawCheckedFont = checkboxElement?.['w14:checkedState']?.['@_w14:font'];
|
|
8449
|
+
const rawUncheckedFont = checkboxElement?.['w14:uncheckedState']?.['@_w14:font'];
|
|
8028
8450
|
properties.checkbox = {
|
|
8029
8451
|
checked: parseOoxmlBoolean(checkboxElement?.['w14:checked'], '@_w14:val'),
|
|
8030
8452
|
checkedState: String(checkboxElement?.['w14:checkedState']?.['@_w14:val'] ?? ''),
|
|
8031
8453
|
uncheckedState: String(checkboxElement?.['w14:uncheckedState']?.['@_w14:val'] ?? ''),
|
|
8454
|
+
checkedFont: rawCheckedFont === undefined ? undefined : String(rawCheckedFont),
|
|
8455
|
+
uncheckedFont: rawUncheckedFont === undefined ? undefined : String(rawUncheckedFont),
|
|
8032
8456
|
};
|
|
8033
8457
|
} else if (sdtPr['w:picture']) {
|
|
8034
8458
|
properties.controlType = 'picture';
|
|
@@ -8083,6 +8507,43 @@ export class DocumentParser {
|
|
|
8083
8507
|
}
|
|
8084
8508
|
}
|
|
8085
8509
|
|
|
8510
|
+
// Capture the original w:sdtPr / w:sdtEndPr markup. The modeled
|
|
8511
|
+
// properties above are a subset of CT_SdtPr (ECMA-376 §17.5.2.38);
|
|
8512
|
+
// rebuilding sdtPr from them on save would drop unmodeled children
|
|
8513
|
+
// (w:rPr, w15:appearance, w15:color, w:temporary, repeatingSection,
|
|
8514
|
+
// ...), and w:sdtEndPr has no model at all. Prefer the verbatim
|
|
8515
|
+
// substring when the caller supplied the raw element XML; nested
|
|
8516
|
+
// SDTs only have the parsed object, so reconstruct from it. Both
|
|
8517
|
+
// outer parts precede w:sdtContent per CT_SdtBlock, so when present
|
|
8518
|
+
// the first occurrence in the raw XML belongs to this SDT, never to
|
|
8519
|
+
// a nested one.
|
|
8520
|
+
let rawSdtPrXml: string | undefined;
|
|
8521
|
+
if (sdtPr !== undefined) {
|
|
8522
|
+
if (rawSdtXml) {
|
|
8523
|
+
const prPos = rawSdtXml.indexOf('<w:sdtPr');
|
|
8524
|
+
if (prPos !== -1) {
|
|
8525
|
+
rawSdtPrXml = this.extractSingleElement(rawSdtXml, 'w:sdtPr', prPos) || undefined;
|
|
8526
|
+
}
|
|
8527
|
+
}
|
|
8528
|
+
if (rawSdtPrXml === undefined) {
|
|
8529
|
+
rawSdtPrXml = this.objectToXml({ 'w:sdtPr': sdtPr }) || undefined;
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
let rawSdtEndPrXml: string | undefined;
|
|
8533
|
+
const sdtEndPr = sdtObj['w:sdtEndPr'];
|
|
8534
|
+
if (sdtEndPr !== undefined) {
|
|
8535
|
+
if (rawSdtXml) {
|
|
8536
|
+
const endPrPos = rawSdtXml.indexOf('<w:sdtEndPr');
|
|
8537
|
+
if (endPrPos !== -1) {
|
|
8538
|
+
rawSdtEndPrXml =
|
|
8539
|
+
this.extractSingleElement(rawSdtXml, 'w:sdtEndPr', endPrPos) || undefined;
|
|
8540
|
+
}
|
|
8541
|
+
}
|
|
8542
|
+
if (rawSdtEndPrXml === undefined) {
|
|
8543
|
+
rawSdtEndPrXml = this.objectToXml({ 'w:sdtEndPr': sdtEndPr }) || undefined;
|
|
8544
|
+
}
|
|
8545
|
+
}
|
|
8546
|
+
|
|
8086
8547
|
// Parse SDT content (sdtContent)
|
|
8087
8548
|
const content: any[] = [];
|
|
8088
8549
|
const sdtContent = sdtObj['w:sdtContent'];
|
|
@@ -8143,6 +8604,23 @@ export class DocumentParser {
|
|
|
8143
8604
|
);
|
|
8144
8605
|
if (nestedSdt) content.push(nestedSdt);
|
|
8145
8606
|
}
|
|
8607
|
+
} else if (!elementType.startsWith('@_') && elementType !== '#text') {
|
|
8608
|
+
// CT_SdtContentBlock also admits w:customXml plus
|
|
8609
|
+
// EG_RunLevelElts (bookmark/comment/perm range markers,
|
|
8610
|
+
// w:proofErr, w:ins/w:del, math, move-range markers).
|
|
8611
|
+
// Regenerating document.xml without them unbalances range
|
|
8612
|
+
// pairs and drops content, so pass them through as raw XML —
|
|
8613
|
+
// mirrors the body-level preservation of these same types.
|
|
8614
|
+
const others = sdtContent[elementType];
|
|
8615
|
+
const otherArray = Array.isArray(others)
|
|
8616
|
+
? others
|
|
8617
|
+
: others !== undefined
|
|
8618
|
+
? [others]
|
|
8619
|
+
: [];
|
|
8620
|
+
if (elementIndex < otherArray.length) {
|
|
8621
|
+
const rawXml = this.objectToXml({ [elementType]: otherArray[elementIndex] });
|
|
8622
|
+
if (rawXml) content.push(new PreservedElement(rawXml, elementType, 'block'));
|
|
8623
|
+
}
|
|
8146
8624
|
}
|
|
8147
8625
|
}
|
|
8148
8626
|
} else {
|
|
@@ -8186,6 +8664,33 @@ export class DocumentParser {
|
|
|
8186
8664
|
);
|
|
8187
8665
|
if (nestedSdt) content.push(nestedSdt);
|
|
8188
8666
|
}
|
|
8667
|
+
|
|
8668
|
+
// Preserve remaining CT_SdtContentBlock children (range markers,
|
|
8669
|
+
// w:customXml, math, tracked-change wrappers) that the typed
|
|
8670
|
+
// branches above do not model — same raw passthrough as the
|
|
8671
|
+
// ordered path, original order unknown without _orderedChildren.
|
|
8672
|
+
for (const key of Object.keys(sdtContent)) {
|
|
8673
|
+
if (
|
|
8674
|
+
key === 'w:p' ||
|
|
8675
|
+
key === 'w:tbl' ||
|
|
8676
|
+
key === 'w:sdt' ||
|
|
8677
|
+
key === '#text' ||
|
|
8678
|
+
key === '_orderedChildren' ||
|
|
8679
|
+
key.startsWith('@_')
|
|
8680
|
+
) {
|
|
8681
|
+
continue;
|
|
8682
|
+
}
|
|
8683
|
+
const others = sdtContent[key];
|
|
8684
|
+
const otherArray = Array.isArray(others)
|
|
8685
|
+
? others
|
|
8686
|
+
: others !== undefined
|
|
8687
|
+
? [others]
|
|
8688
|
+
: [];
|
|
8689
|
+
for (const otherObj of otherArray) {
|
|
8690
|
+
const rawXml = this.objectToXml({ [key]: otherObj });
|
|
8691
|
+
if (rawXml) content.push(new PreservedElement(rawXml, key, 'block'));
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8189
8694
|
}
|
|
8190
8695
|
}
|
|
8191
8696
|
|
|
@@ -8198,7 +8703,10 @@ export class DocumentParser {
|
|
|
8198
8703
|
}
|
|
8199
8704
|
}
|
|
8200
8705
|
|
|
8201
|
-
|
|
8706
|
+
const sdt = new StructuredDocumentTag(properties, content);
|
|
8707
|
+
if (rawSdtPrXml !== undefined) sdt._setRawSdtPrXml(rawSdtPrXml);
|
|
8708
|
+
if (rawSdtEndPrXml !== undefined) sdt._setRawSdtEndPrXml(rawSdtEndPrXml);
|
|
8709
|
+
return sdt;
|
|
8202
8710
|
} catch (error: unknown) {
|
|
8203
8711
|
defaultLogger.warn(
|
|
8204
8712
|
'[DocumentParser] Failed to parse SDT:',
|
|
@@ -8668,8 +9176,13 @@ export class DocumentParser {
|
|
|
8668
9176
|
// FIX (v1.3.1): Use _orderedChildren to maintain document order of elements
|
|
8669
9177
|
// This fixes TOC tab preservation - tabs must be in correct position
|
|
8670
9178
|
const buildXml = (o: any, name?: string): string => {
|
|
8671
|
-
|
|
8672
|
-
|
|
9179
|
+
// Text-only elements parse to a bare primitive (e.g. <w:t>Hi</w:t>
|
|
9180
|
+
// becomes 'Hi'); when a tag name is known the wrapper must be
|
|
9181
|
+
// re-emitted or the reconstructed XML silently loses the element.
|
|
9182
|
+
if (typeof o !== 'object' || o === null) {
|
|
9183
|
+
const text = String(o ?? '');
|
|
9184
|
+
return name ? `<${name}>${text}</${name}>` : text;
|
|
9185
|
+
}
|
|
8673
9186
|
|
|
8674
9187
|
const keys = Object.keys(o);
|
|
8675
9188
|
|
|
@@ -9058,6 +9571,16 @@ export class DocumentParser {
|
|
|
9058
9571
|
}
|
|
9059
9572
|
}
|
|
9060
9573
|
|
|
9574
|
+
// The nested previous <w:sectPr> inside <w:sectPrChange> holds the
|
|
9575
|
+
// section's pre-revision state. Because extractElements is a flat scan,
|
|
9576
|
+
// any per-property call below would otherwise match that nested previous
|
|
9577
|
+
// element when the live sectPr omits the property — resurrecting a
|
|
9578
|
+
// tracked-removed property (e.g. titlePg/bidi) as a live one. Strip the
|
|
9579
|
+
// change subtree so only current properties are read here; the original
|
|
9580
|
+
// string is retained for the sectPrChange extraction further down.
|
|
9581
|
+
const sectPrWithChange = sectPr;
|
|
9582
|
+
sectPr = sectPr.replace(/<w:sectPrChange[\s\S]*?<\/w:sectPrChange>/, '');
|
|
9583
|
+
|
|
9061
9584
|
const sectionProps: SectionProperties = {};
|
|
9062
9585
|
|
|
9063
9586
|
// Parse page size
|
|
@@ -9464,8 +9987,10 @@ export class DocumentParser {
|
|
|
9464
9987
|
|
|
9465
9988
|
const section = new Section(sectionProps);
|
|
9466
9989
|
|
|
9467
|
-
// Parse section property change (w:sectPrChange) per ECMA-376 Part 1 §17.13.5.32
|
|
9468
|
-
|
|
9990
|
+
// Parse section property change (w:sectPrChange) per ECMA-376 Part 1 §17.13.5.32.
|
|
9991
|
+
// Use the unstripped string: the change subtree was removed from `sectPr`
|
|
9992
|
+
// above so it could not leak into the live property reads.
|
|
9993
|
+
const sectPrChangeElements = XMLParser.extractElements(sectPrWithChange, 'w:sectPrChange');
|
|
9469
9994
|
if (sectPrChangeElements.length > 0 && sectPrChangeElements[0]) {
|
|
9470
9995
|
const changeXml = sectPrChangeElements[0];
|
|
9471
9996
|
const id = XMLParser.extractAttribute(changeXml, 'w:id') || '0';
|
|
@@ -9530,22 +10055,36 @@ export class DocumentParser {
|
|
|
9530
10055
|
}
|
|
9531
10056
|
}
|
|
9532
10057
|
|
|
9533
|
-
// Extract basedOn
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
10058
|
+
// Extract basedOn (CT_String is empty-content per ECMA-376 §17.7.4.3, so Word
|
|
10059
|
+
// serializes <w:basedOn w:val="..."/> self-closing; extractBetweenTags only
|
|
10060
|
+
// matches an explicit closing tag and would drop the inheritance chain)
|
|
10061
|
+
let basedOn: string | undefined;
|
|
10062
|
+
const basedOnAttrs = XMLParser.extractSelfClosingTag(styleXml, 'w:basedOn');
|
|
10063
|
+
if (basedOnAttrs) {
|
|
10064
|
+
basedOn = XMLParser.extractAttribute(`<w:basedOn${basedOnAttrs}/>`, 'w:val');
|
|
10065
|
+
}
|
|
9538
10066
|
|
|
9539
|
-
// Extract next
|
|
9540
|
-
|
|
9541
|
-
const
|
|
9542
|
-
|
|
9543
|
-
:
|
|
10067
|
+
// Extract next (same empty-content serialization as basedOn)
|
|
10068
|
+
let next: string | undefined;
|
|
10069
|
+
const nextAttrs = XMLParser.extractSelfClosingTag(styleXml, 'w:next');
|
|
10070
|
+
if (nextAttrs) {
|
|
10071
|
+
next = XMLParser.extractAttribute(`<w:next${nextAttrs}/>`, 'w:val');
|
|
10072
|
+
}
|
|
10073
|
+
|
|
10074
|
+
// Conditional table-style blocks (w:tblStylePr) carry their own pPr/rPr.
|
|
10075
|
+
// Per CT_Style (ECMA-376 §17.7.4.17) the root pPr/rPr precede those
|
|
10076
|
+
// blocks, so a first-match search over the full element would misattribute
|
|
10077
|
+
// a conditional block's formatting (e.g. firstRow bold/centered in banded
|
|
10078
|
+
// table styles) as the style's unconditional base formatting whenever the
|
|
10079
|
+
// root pPr/rPr are absent. Search a copy with the conditional blocks
|
|
10080
|
+
// stripped; parseConditionalFormattingFromXml still receives the full
|
|
10081
|
+
// styleXml and parses those blocks separately.
|
|
10082
|
+
const styleBase = styleXml.replace(/<w:tblStylePr[\s\S]*?<\/w:tblStylePr>/g, '');
|
|
9544
10083
|
|
|
9545
10084
|
// Parse paragraph formatting (w:pPr)
|
|
9546
10085
|
let paragraphFormatting: ParagraphFormatting | undefined;
|
|
9547
10086
|
let styleNumPr: { numId?: number; ilvl?: number } | undefined;
|
|
9548
|
-
const pPrXml = XMLParser.extractBetweenTags(
|
|
10087
|
+
const pPrXml = XMLParser.extractBetweenTags(styleBase, '<w:pPr>', '</w:pPr>');
|
|
9549
10088
|
if (pPrXml) {
|
|
9550
10089
|
paragraphFormatting = this.parseParagraphFormattingFromXml(pPrXml);
|
|
9551
10090
|
|
|
@@ -9567,7 +10106,7 @@ export class DocumentParser {
|
|
|
9567
10106
|
|
|
9568
10107
|
// Parse run formatting (w:rPr)
|
|
9569
10108
|
let runFormatting: RunFormatting | undefined;
|
|
9570
|
-
const rPrXml = XMLParser.extractBetweenTags(
|
|
10109
|
+
const rPrXml = XMLParser.extractBetweenTags(styleBase, '<w:rPr>', '</w:rPr>');
|
|
9571
10110
|
if (rPrXml) {
|
|
9572
10111
|
runFormatting = this.parseRunFormattingFromXml(rPrXml);
|
|
9573
10112
|
}
|
|
@@ -11181,15 +11720,43 @@ export class DocumentParser {
|
|
|
11181
11720
|
* Parses document background (w:background) per ECMA-376 Part 1 §17.2.1
|
|
11182
11721
|
* The w:background element appears as a child of w:document, before w:body
|
|
11183
11722
|
*/
|
|
11184
|
-
private parseDocumentBackground(
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11723
|
+
private parseDocumentBackground(docXml: string):
|
|
11724
|
+
| {
|
|
11725
|
+
color?: string;
|
|
11726
|
+
themeColor?: string;
|
|
11727
|
+
themeTint?: string;
|
|
11728
|
+
themeShade?: string;
|
|
11729
|
+
rawInnerXml?: string;
|
|
11730
|
+
}
|
|
11731
|
+
| undefined {
|
|
11732
|
+
// Match both the self-closing form (flat-color backgrounds) and the
|
|
11733
|
+
// expanded form: Word emits <w:background ...><v:background>...</v:background>
|
|
11734
|
+
// </w:background> for picture/gradient/texture page backgrounds per
|
|
11735
|
+
// ECMA-376 §17.2.1, and that child fill must survive the round-trip.
|
|
11736
|
+
const bgMatch = /<w:background([^>]*?)(\/>|>)/.exec(docXml);
|
|
11737
|
+
if (!bgMatch) return undefined;
|
|
11738
|
+
|
|
11739
|
+
const attrStr = bgMatch[1] ?? '';
|
|
11740
|
+
const result: {
|
|
11741
|
+
color?: string;
|
|
11742
|
+
themeColor?: string;
|
|
11743
|
+
themeTint?: string;
|
|
11744
|
+
themeShade?: string;
|
|
11745
|
+
rawInnerXml?: string;
|
|
11746
|
+
} = {};
|
|
11747
|
+
|
|
11748
|
+
// Expanded form: capture inner content verbatim as a passthrough slot
|
|
11749
|
+
// (VML has no object model here; re-emitted untouched by the generator)
|
|
11750
|
+
if (bgMatch[2] === '>') {
|
|
11751
|
+
const innerStart = bgMatch.index + bgMatch[0].length;
|
|
11752
|
+
const closeIndex = docXml.indexOf('</w:background>', innerStart);
|
|
11753
|
+
if (closeIndex !== -1) {
|
|
11754
|
+
const inner = docXml.substring(innerStart, closeIndex);
|
|
11755
|
+
if (inner.length > 0) {
|
|
11756
|
+
result.rawInnerXml = inner;
|
|
11757
|
+
}
|
|
11758
|
+
}
|
|
11759
|
+
}
|
|
11193
11760
|
|
|
11194
11761
|
const colorMatch = /w:color="([^"]+)"/.exec(attrStr);
|
|
11195
11762
|
if (colorMatch?.[1]) result.color = colorMatch[1];
|