docxmlater 11.0.10 → 12.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -13
- package/dist/core/Document.d.ts +28 -2
- package/dist/core/Document.d.ts.map +1 -1
- package/dist/core/Document.js +725 -170
- package/dist/core/Document.js.map +1 -1
- package/dist/core/DocumentGenerator.d.ts +1 -0
- package/dist/core/DocumentGenerator.d.ts.map +1 -1
- package/dist/core/DocumentGenerator.js +77 -25
- package/dist/core/DocumentGenerator.js.map +1 -1
- package/dist/core/DocumentParser.d.ts +7 -3
- package/dist/core/DocumentParser.d.ts.map +1 -1
- package/dist/core/DocumentParser.js +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 +726 -171
- package/dist/esm/core/Document.js.map +1 -1
- package/dist/esm/core/DocumentGenerator.js +77 -25
- package/dist/esm/core/DocumentGenerator.js.map +1 -1
- package/dist/esm/core/DocumentParser.js +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 +58 -8
- package/dist/esm/processors/RevisionWalker.js.map +1 -1
- package/dist/esm/processors/SelectiveRevisionAcceptor.js +116 -17
- package/dist/esm/processors/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/esm/processors/cnfStyleDecoder.js +7 -7
- package/dist/esm/processors/cnfStyleDecoder.js.map +1 -1
- package/dist/esm/processors/stripTrackedChanges.js +16 -5
- package/dist/esm/processors/stripTrackedChanges.js.map +1 -1
- package/dist/esm/utils/deepClone.js +24 -11
- package/dist/esm/utils/deepClone.js.map +1 -1
- package/dist/esm/utils/units.js +1 -1
- package/dist/esm/utils/units.js.map +1 -1
- package/dist/esm/utils/validation.js +11 -23
- package/dist/esm/utils/validation.js.map +1 -1
- package/dist/esm/utils/xmlSanitization.js +131 -7
- package/dist/esm/utils/xmlSanitization.js.map +1 -1
- package/dist/esm/validation/RevisionAutoFixer.js +69 -55
- package/dist/esm/validation/RevisionAutoFixer.js.map +1 -1
- package/dist/esm/validation/RevisionValidator.js +24 -20
- package/dist/esm/validation/RevisionValidator.js.map +1 -1
- package/dist/esm/xml/XMLBuilder.js +52 -20
- package/dist/esm/xml/XMLBuilder.js.map +1 -1
- package/dist/esm/xml/XMLParser.js +55 -13
- package/dist/esm/xml/XMLParser.js.map +1 -1
- package/dist/esm/zip/ZipHandler.js +37 -13
- package/dist/esm/zip/ZipHandler.js.map +1 -1
- package/dist/esm/zip/ZipReader.js +53 -6
- package/dist/esm/zip/ZipReader.js.map +1 -1
- package/dist/esm/zip/errors.js +7 -0
- package/dist/esm/zip/errors.js.map +1 -1
- package/dist/esm/zip/types.js +4 -0
- package/dist/esm/zip/types.js.map +1 -1
- package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
- package/dist/formatting/AbstractNumbering.js +1 -1
- package/dist/formatting/AbstractNumbering.js.map +1 -1
- package/dist/formatting/NumberingLevel.d.ts +1 -1
- package/dist/formatting/NumberingLevel.d.ts.map +1 -1
- package/dist/formatting/NumberingLevel.js +8 -6
- package/dist/formatting/NumberingLevel.js.map +1 -1
- package/dist/formatting/NumberingManager.d.ts.map +1 -1
- package/dist/formatting/NumberingManager.js +16 -1
- package/dist/formatting/NumberingManager.js.map +1 -1
- package/dist/formatting/Style.d.ts.map +1 -1
- package/dist/formatting/Style.js +14 -0
- package/dist/formatting/Style.js.map +1 -1
- package/dist/formatting/StylesManager.d.ts +2 -0
- package/dist/formatting/StylesManager.d.ts.map +1 -1
- package/dist/formatting/StylesManager.js +14 -2
- package/dist/formatting/StylesManager.js.map +1 -1
- package/dist/helpers/CleanupHelper.d.ts.map +1 -1
- package/dist/helpers/CleanupHelper.js +103 -26
- package/dist/helpers/CleanupHelper.js.map +1 -1
- package/dist/images/ImageOptimizer.d.ts.map +1 -1
- package/dist/images/ImageOptimizer.js +52 -4
- package/dist/images/ImageOptimizer.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -1
- package/dist/internal.js.map +1 -1
- package/dist/processors/CompatibilityUpgrader.d.ts.map +1 -1
- package/dist/processors/CompatibilityUpgrader.js +7 -1
- package/dist/processors/CompatibilityUpgrader.js.map +1 -1
- package/dist/processors/InMemoryRevisionAcceptor.d.ts +1 -0
- package/dist/processors/InMemoryRevisionAcceptor.d.ts.map +1 -1
- package/dist/processors/InMemoryRevisionAcceptor.js +135 -13
- package/dist/processors/InMemoryRevisionAcceptor.js.map +1 -1
- package/dist/processors/RevisionWalker.d.ts +2 -0
- package/dist/processors/RevisionWalker.d.ts.map +1 -1
- package/dist/processors/RevisionWalker.js +58 -8
- package/dist/processors/RevisionWalker.js.map +1 -1
- package/dist/processors/SelectiveRevisionAcceptor.d.ts.map +1 -1
- package/dist/processors/SelectiveRevisionAcceptor.js +116 -17
- package/dist/processors/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/processors/cnfStyleDecoder.d.ts.map +1 -1
- package/dist/processors/cnfStyleDecoder.js +7 -7
- package/dist/processors/cnfStyleDecoder.js.map +1 -1
- package/dist/processors/stripTrackedChanges.d.ts.map +1 -1
- package/dist/processors/stripTrackedChanges.js +16 -5
- package/dist/processors/stripTrackedChanges.js.map +1 -1
- package/dist/utils/deepClone.d.ts.map +1 -1
- package/dist/utils/deepClone.js +24 -11
- package/dist/utils/deepClone.js.map +1 -1
- package/dist/utils/units.d.ts.map +1 -1
- package/dist/utils/units.js +1 -1
- package/dist/utils/units.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +11 -23
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/xmlSanitization.d.ts +2 -0
- package/dist/utils/xmlSanitization.d.ts.map +1 -1
- package/dist/utils/xmlSanitization.js +133 -7
- package/dist/utils/xmlSanitization.js.map +1 -1
- package/dist/validation/RevisionAutoFixer.d.ts +6 -2
- package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
- package/dist/validation/RevisionAutoFixer.js +68 -54
- package/dist/validation/RevisionAutoFixer.js.map +1 -1
- package/dist/validation/RevisionValidator.d.ts +2 -0
- package/dist/validation/RevisionValidator.d.ts.map +1 -1
- package/dist/validation/RevisionValidator.js +26 -21
- package/dist/validation/RevisionValidator.js.map +1 -1
- package/dist/xml/XMLBuilder.d.ts +1 -0
- package/dist/xml/XMLBuilder.d.ts.map +1 -1
- package/dist/xml/XMLBuilder.js +52 -20
- package/dist/xml/XMLBuilder.js.map +1 -1
- package/dist/xml/XMLParser.d.ts +1 -0
- package/dist/xml/XMLParser.d.ts.map +1 -1
- package/dist/xml/XMLParser.js +55 -13
- package/dist/xml/XMLParser.js.map +1 -1
- package/dist/zip/ZipHandler.d.ts.map +1 -1
- package/dist/zip/ZipHandler.js +37 -13
- package/dist/zip/ZipHandler.js.map +1 -1
- package/dist/zip/ZipReader.d.ts.map +1 -1
- package/dist/zip/ZipReader.js +52 -5
- package/dist/zip/ZipReader.js.map +1 -1
- package/dist/zip/errors.d.ts +3 -0
- package/dist/zip/errors.d.ts.map +1 -1
- package/dist/zip/errors.js +9 -1
- package/dist/zip/errors.js.map +1 -1
- package/dist/zip/types.d.ts +4 -0
- package/dist/zip/types.d.ts.map +1 -1
- package/dist/zip/types.js +4 -0
- package/dist/zip/types.js.map +1 -1
- package/package.json +2 -2
- package/src/core/Document.ts +1159 -240
- package/src/core/DocumentGenerator.ts +106 -26
- package/src/core/DocumentParser.ts +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 +83 -7
- package/src/processors/SelectiveRevisionAcceptor.ts +164 -28
- package/src/processors/cnfStyleDecoder.ts +18 -12
- package/src/processors/stripTrackedChanges.ts +47 -10
- package/src/utils/deepClone.ts +48 -14
- package/src/utils/units.ts +3 -1
- package/src/utils/validation.ts +30 -32
- package/src/utils/xmlSanitization.ts +218 -22
- package/src/validation/RevisionAutoFixer.ts +109 -65
- package/src/validation/RevisionValidator.ts +48 -23
- package/src/xml/XMLBuilder.ts +82 -22
- package/src/xml/XMLParser.ts +87 -20
- package/src/zip/ZipHandler.ts +61 -21
- package/src/zip/ZipReader.ts +135 -13
- package/src/zip/errors.ts +15 -0
- package/src/zip/types.ts +44 -2
package/src/xml/XMLBuilder.ts
CHANGED
|
@@ -162,9 +162,12 @@ export class XMLBuilder {
|
|
|
162
162
|
// Add attributes
|
|
163
163
|
if (element.attributes) {
|
|
164
164
|
for (const [key, value] of Object.entries(element.attributes)) {
|
|
165
|
-
if (value !== undefined && value !== null
|
|
166
|
-
//
|
|
167
|
-
|
|
165
|
+
if (value !== undefined && value !== null) {
|
|
166
|
+
// Booleans serialize as ST_OnOff literals (ECMA-376 §22.9.2.13).
|
|
167
|
+
// Explicit false must emit "0" — dropping it would flip an explicit
|
|
168
|
+
// override to inherited/on under CT_OnOff presence semantics.
|
|
169
|
+
// Callers signal absence with undefined, never false.
|
|
170
|
+
const attrValue = value === true ? '1' : value === false ? '0' : String(value);
|
|
168
171
|
// Use escapeXmlAttribute for attribute values (Issue #8)
|
|
169
172
|
xml += ` ${key}="${XMLBuilder.escapeXmlAttribute(attrValue)}"`;
|
|
170
173
|
}
|
|
@@ -245,18 +248,47 @@ export class XMLBuilder {
|
|
|
245
248
|
.replace(/'/g, ''');
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
/** The five predefined XML 1.0 named entities */
|
|
252
|
+
private static readonly NAMED_ENTITIES: Record<string, string> = {
|
|
253
|
+
lt: '<',
|
|
254
|
+
gt: '>',
|
|
255
|
+
quot: '"',
|
|
256
|
+
apos: "'",
|
|
257
|
+
amp: '&',
|
|
258
|
+
};
|
|
259
|
+
|
|
248
260
|
/**
|
|
249
261
|
* Unescapes XML entities back to original characters
|
|
262
|
+
* Decodes the five predefined named entities plus numeric character
|
|
263
|
+
* references (’ /  ), which XML 1.0 §4.1 requires every
|
|
264
|
+
* conforming processor to expand — third-party producers emit them, and
|
|
265
|
+
* leaving them raw double-escapes the ampersand on save.
|
|
250
266
|
* @param text Text with XML entities
|
|
251
267
|
* @returns Unescaped text
|
|
252
268
|
*/
|
|
253
269
|
static unescapeXml(text: string): string {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
// Single pass so text produced by one decode is never re-decoded by a
|
|
271
|
+
// later one (e.g. '&amp;' must yield '&', not '&').
|
|
272
|
+
return text.replace(
|
|
273
|
+
/&(?:(lt|gt|quot|apos|amp)|#x([0-9A-Fa-f]+)|#(\d+));/g,
|
|
274
|
+
(match, named?: string, hex?: string, dec?: string) => {
|
|
275
|
+
if (named !== undefined) {
|
|
276
|
+
return XMLBuilder.NAMED_ENTITIES[named] ?? match;
|
|
277
|
+
}
|
|
278
|
+
const code = hex !== undefined ? parseInt(hex, 16) : Number(dec);
|
|
279
|
+
// Code points outside the XML 1.0 Char production (NUL, surrogates,
|
|
280
|
+
// beyond U+10FFFF) would make the decoded string unserializable —
|
|
281
|
+
// keep the raw reference instead of injecting an invalid character.
|
|
282
|
+
const isXmlChar =
|
|
283
|
+
code === 0x9 ||
|
|
284
|
+
code === 0xa ||
|
|
285
|
+
code === 0xd ||
|
|
286
|
+
(code >= 0x20 && code <= 0xd7ff) ||
|
|
287
|
+
(code >= 0xe000 && code <= 0xfffd) ||
|
|
288
|
+
(code >= 0x10000 && code <= 0x10ffff);
|
|
289
|
+
return isXmlChar ? String.fromCodePoint(code) : match;
|
|
290
|
+
}
|
|
291
|
+
);
|
|
260
292
|
}
|
|
261
293
|
|
|
262
294
|
/**
|
|
@@ -633,11 +665,7 @@ export class XMLBuilder {
|
|
|
633
665
|
const builder = new XMLBuilder();
|
|
634
666
|
const element = XMLBuilder.objectToElement(obj, rootName);
|
|
635
667
|
if (element) {
|
|
636
|
-
|
|
637
|
-
builder.text(element);
|
|
638
|
-
} else {
|
|
639
|
-
builder.elements.push(element);
|
|
640
|
-
}
|
|
668
|
+
builder.elements.push(element);
|
|
641
669
|
}
|
|
642
670
|
return builder.build();
|
|
643
671
|
}
|
|
@@ -649,22 +677,21 @@ export class XMLBuilder {
|
|
|
649
677
|
private static objectToElement(
|
|
650
678
|
obj: ParsedXmlObject | string | number | boolean | null | undefined,
|
|
651
679
|
name: string
|
|
652
|
-
): XMLElement |
|
|
680
|
+
): XMLElement | null {
|
|
653
681
|
if (obj === null || obj === undefined) {
|
|
654
682
|
return null;
|
|
655
683
|
}
|
|
656
684
|
|
|
657
|
-
if (typeof obj !== 'object'
|
|
658
|
-
|
|
685
|
+
if (typeof obj !== 'object') {
|
|
686
|
+
// The parser collapses text-only elements to their primitive value, so
|
|
687
|
+
// the wrapping tag must be re-emitted here — bare text inside container
|
|
688
|
+
// elements (e.g. w:r) is invalid WordprocessingML.
|
|
689
|
+
return { name, children: [String(obj)] };
|
|
659
690
|
}
|
|
660
691
|
|
|
661
692
|
const attributes: Record<string, string | number | boolean> = {};
|
|
662
693
|
const children: (XMLElement | string)[] = [];
|
|
663
694
|
|
|
664
|
-
if (obj['#text'] && Object.keys(obj).length === 1) {
|
|
665
|
-
return String(obj['#text']);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
695
|
for (const key in obj) {
|
|
669
696
|
if (key.startsWith('@_')) {
|
|
670
697
|
const attrName = key.substring(2);
|
|
@@ -674,7 +701,40 @@ export class XMLBuilder {
|
|
|
674
701
|
}
|
|
675
702
|
} else if (key === '#text') {
|
|
676
703
|
children.push(String(obj[key]));
|
|
677
|
-
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// _orderedChildren is parser metadata, never document content. When
|
|
708
|
+
// present, it restores the original interleaving of same-named siblings
|
|
709
|
+
// (e.g. w:t / w:tab / w:t in a run) that the keyed object form loses.
|
|
710
|
+
const orderedChildren = obj['_orderedChildren'] as
|
|
711
|
+
| { type: string; index: number }[]
|
|
712
|
+
| undefined;
|
|
713
|
+
|
|
714
|
+
if (orderedChildren && orderedChildren.length > 0) {
|
|
715
|
+
for (const childInfo of orderedChildren) {
|
|
716
|
+
const childValue = obj[childInfo.type];
|
|
717
|
+
if (childValue === undefined) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const item = Array.isArray(childValue)
|
|
721
|
+
? childValue[childInfo.index]
|
|
722
|
+
: childInfo.index === 0
|
|
723
|
+
? childValue
|
|
724
|
+
: undefined;
|
|
725
|
+
if (item === undefined) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const childElement = XMLBuilder.objectToElement(item, childInfo.type);
|
|
729
|
+
if (childElement) {
|
|
730
|
+
children.push(childElement);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
for (const key in obj) {
|
|
735
|
+
if (key.startsWith('@_') || key === '#text' || key === '_orderedChildren') {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
678
738
|
const childObj = obj[key];
|
|
679
739
|
if (Array.isArray(childObj)) {
|
|
680
740
|
childObj.forEach((item) => {
|
package/src/xml/XMLParser.ts
CHANGED
|
@@ -139,8 +139,9 @@ export class XMLParser {
|
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// Find the end of opening tag
|
|
143
|
-
|
|
142
|
+
// Find the end of opening tag (quote-aware: attribute values may
|
|
143
|
+
// legally contain a raw '>')
|
|
144
|
+
const openEnd = XMLParser.findTagEnd(xml, startIdx);
|
|
144
145
|
if (openEnd === -1) break;
|
|
145
146
|
|
|
146
147
|
// Check if self-closing
|
|
@@ -333,8 +334,9 @@ export class XMLParser {
|
|
|
333
334
|
const startIdx = xml.indexOf(startTag);
|
|
334
335
|
if (startIdx === -1) return undefined;
|
|
335
336
|
|
|
336
|
-
// Find the end of the opening tag
|
|
337
|
-
|
|
337
|
+
// Find the end of the opening tag (quote-aware: attribute values may
|
|
338
|
+
// legally contain a raw '>')
|
|
339
|
+
const openEnd = XMLParser.findTagEnd(xml, startIdx);
|
|
338
340
|
if (openEnd === -1) return undefined;
|
|
339
341
|
|
|
340
342
|
// Find the closing tag
|
|
@@ -344,6 +346,28 @@ export class XMLParser {
|
|
|
344
346
|
return xml.substring(openEnd + 1, endIdx);
|
|
345
347
|
}
|
|
346
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Finds the first '>' at or after startPos that is not inside a quoted
|
|
351
|
+
* attribute value. XML 1.0 §2.3 permits a raw '>' inside attribute values
|
|
352
|
+
* (only '<', '&', and the delimiting quote must be escaped), so a bare
|
|
353
|
+
* indexOf('>') can end a tag header mid-attribute.
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
private static findTagEnd(xml: string, startPos: number): number {
|
|
357
|
+
let quote: string | undefined;
|
|
358
|
+
for (let pos = startPos; pos < xml.length; pos++) {
|
|
359
|
+
const ch = xml[pos];
|
|
360
|
+
if (quote) {
|
|
361
|
+
if (ch === quote) quote = undefined;
|
|
362
|
+
} else if (ch === '"' || ch === "'") {
|
|
363
|
+
quote = ch;
|
|
364
|
+
} else if (ch === '>') {
|
|
365
|
+
return pos;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return -1;
|
|
369
|
+
}
|
|
370
|
+
|
|
347
371
|
/**
|
|
348
372
|
* Extracts a complete self-closing tag with its attributes
|
|
349
373
|
* Handles cases where multiple similar tags exist (e.g., <w:sz.../> and <w:szCs.../>)
|
|
@@ -371,19 +395,17 @@ export class XMLParser {
|
|
|
371
395
|
|
|
372
396
|
// Valid separators after tag name: space, '/', or '>'
|
|
373
397
|
if (charAfterTag === ' ' || charAfterTag === '/' || charAfterTag === '>') {
|
|
374
|
-
// Found the exact tag
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// Return attributes portion (between tag name and />)
|
|
386
|
-
return xml.substring(startIdx + startPattern.length, endIdx);
|
|
398
|
+
// Found the exact tag. Bound the search to this element's own tag
|
|
399
|
+
// header: a global indexOf('/>') can land inside a later sibling
|
|
400
|
+
// when the target is written in expanded form (<tag ...></tag>),
|
|
401
|
+
// attributing the sibling's attributes to this tag.
|
|
402
|
+
const tagEnd = XMLParser.findTagEnd(xml, startIdx + startPattern.length);
|
|
403
|
+
if (tagEnd === -1) return undefined;
|
|
404
|
+
|
|
405
|
+
// Self-closing: exclude the trailing '/'; expanded form: attributes
|
|
406
|
+
// run up to the '>' of the open tag.
|
|
407
|
+
const attrEnd = xml[tagEnd - 1] === '/' ? tagEnd - 1 : tagEnd;
|
|
408
|
+
return xml.substring(startIdx + startPattern.length, attrEnd);
|
|
387
409
|
}
|
|
388
410
|
|
|
389
411
|
// Not the exact tag (e.g., found "w:sz" when looking for "w:s")
|
|
@@ -477,6 +499,27 @@ export class XMLParser {
|
|
|
477
499
|
return { value: {}, endPos: xml.length };
|
|
478
500
|
}
|
|
479
501
|
|
|
502
|
+
// Skip processing instructions (e.g. <?mso-application ...?>); without
|
|
503
|
+
// this, '?' fails the element-name match and the PI body is mangled
|
|
504
|
+
// into text one character at a time
|
|
505
|
+
if (xml.substring(openTagStart, openTagStart + 2) === '<?') {
|
|
506
|
+
const piEnd = xml.indexOf('?>', openTagStart + 2);
|
|
507
|
+
if (piEnd !== -1) {
|
|
508
|
+
return XMLParser.parseElementToObject(xml, piEnd + 2, options, depth);
|
|
509
|
+
}
|
|
510
|
+
return { value: {}, endPos: xml.length };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Skip CDATA found where an element is expected (e.g. at the root);
|
|
514
|
+
// CDATA inside element content is preserved by the content loop below
|
|
515
|
+
if (xml.substring(openTagStart, openTagStart + 9) === '<![CDATA[') {
|
|
516
|
+
const cdataEnd = xml.indexOf(']]>', openTagStart + 9);
|
|
517
|
+
if (cdataEnd !== -1) {
|
|
518
|
+
return XMLParser.parseElementToObject(xml, cdataEnd + 3, options, depth);
|
|
519
|
+
}
|
|
520
|
+
return { value: {}, endPos: xml.length };
|
|
521
|
+
}
|
|
522
|
+
|
|
480
523
|
// Extract element name
|
|
481
524
|
const nameMatch = /^([a-zA-Z0-9:_-]+)/.exec(xml.substring(openTagStart + 1));
|
|
482
525
|
if (!nameMatch) {
|
|
@@ -485,7 +528,8 @@ export class XMLParser {
|
|
|
485
528
|
|
|
486
529
|
const originalElementName: string = nameMatch[1] || '';
|
|
487
530
|
let elementName: string = originalElementName;
|
|
488
|
-
|
|
531
|
+
// Quote-aware scan: attribute values may legally contain a raw '>'
|
|
532
|
+
const tagHeaderEnd = XMLParser.findTagEnd(xml, openTagStart);
|
|
489
533
|
if (tagHeaderEnd === -1) {
|
|
490
534
|
return { value: {}, endPos: xml.length };
|
|
491
535
|
}
|
|
@@ -558,6 +602,19 @@ export class XMLParser {
|
|
|
558
602
|
}
|
|
559
603
|
}
|
|
560
604
|
|
|
605
|
+
// CDATA: append the payload verbatim — its content is by definition
|
|
606
|
+
// already unescaped, so entity decoding must not be applied
|
|
607
|
+
if (content.startsWith('<![CDATA[', nextTag)) {
|
|
608
|
+
const cdataEnd = content.indexOf(']]>', nextTag + 9);
|
|
609
|
+
if (cdataEnd === -1) {
|
|
610
|
+
textContent += content.substring(nextTag + 9);
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
textContent += content.substring(nextTag + 9, cdataEnd);
|
|
614
|
+
pos = cdataEnd + 3;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
561
618
|
// Parse child element (increment depth for children)
|
|
562
619
|
const childResult = XMLParser.parseElementToObject(content, nextTag, options, depth + 1);
|
|
563
620
|
const childObj = childResult.value as ParsedXMLObject;
|
|
@@ -602,6 +659,11 @@ export class XMLParser {
|
|
|
602
659
|
// Add children
|
|
603
660
|
if (children.length > 0) {
|
|
604
661
|
const coalescedChildren = XMLParser.coalesceChildren(children, options);
|
|
662
|
+
// Mixed content with no attributes leaves elementValue as a bare
|
|
663
|
+
// string; fold it into object form so merging children keeps the text
|
|
664
|
+
if (typeof elementValue === 'string') {
|
|
665
|
+
elementValue = { [options.textNodeName]: elementValue };
|
|
666
|
+
}
|
|
605
667
|
if (typeof elementValue === 'object' && !Array.isArray(elementValue)) {
|
|
606
668
|
elementValue = { ...elementValue, ...coalescedChildren };
|
|
607
669
|
} else {
|
|
@@ -705,10 +767,15 @@ export class XMLParser {
|
|
|
705
767
|
// Add prefix to attribute name
|
|
706
768
|
const prefixedName = options.attributeNamePrefix + attrName;
|
|
707
769
|
|
|
770
|
+
// Unescape XML entities so stored attribute values are the actual
|
|
771
|
+
// decoded strings, matching text-node handling and extractAttribute.
|
|
772
|
+
// This prevents double-escaping when the value is later re-serialized.
|
|
773
|
+
const decodedValue = XMLBuilder.unescapeXml(attrValue);
|
|
774
|
+
|
|
708
775
|
// Parse attribute value
|
|
709
776
|
attributes[prefixedName] = options.parseAttributeValue
|
|
710
|
-
? XMLParser.parseValue(
|
|
711
|
-
:
|
|
777
|
+
? XMLParser.parseValue(decodedValue)
|
|
778
|
+
: decodedValue;
|
|
712
779
|
}
|
|
713
780
|
|
|
714
781
|
return attributes;
|
package/src/zip/ZipHandler.ts
CHANGED
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
SizeLimitOptions,
|
|
15
15
|
DEFAULT_SIZE_LIMITS,
|
|
16
16
|
} from './types.js';
|
|
17
|
+
import { ResourceLimitError } from './errors.js';
|
|
17
18
|
import { getGlobalLogger, createScopedLogger, ILogger } from '../utils/logger.js';
|
|
19
|
+
import { normalizePath } from '../utils/validation.js';
|
|
18
20
|
|
|
19
21
|
// Create scoped logger for ZipHandler operations
|
|
20
22
|
function getLogger(): ILogger {
|
|
@@ -41,16 +43,19 @@ export class ZipHandler {
|
|
|
41
43
|
* Validates document size against configured limits
|
|
42
44
|
* @param sizeMB - Size in megabytes
|
|
43
45
|
* @param limits - Size limit options (merged with defaults)
|
|
44
|
-
* @throws
|
|
46
|
+
* @throws {ResourceLimitError} If the compressed size exceeds maxSizeMB
|
|
45
47
|
*/
|
|
46
48
|
private validateDocumentSize(sizeMB: number, limits: Required<SizeLimitOptions>): void {
|
|
47
49
|
const logger = getLogger();
|
|
48
50
|
const { warningSizeMB, maxSizeMB } = limits;
|
|
49
51
|
|
|
50
|
-
// Check maximum size (if enabled)
|
|
52
|
+
// Check maximum size (if enabled). Throw the typed ResourceLimitError so this
|
|
53
|
+
// compressed-size guard is catchable by the same type as the uncompressed/ratio/
|
|
54
|
+
// entry-count guards enforced downstream in ZipReader — callers can distinguish a
|
|
55
|
+
// hostile/oversized payload from a genuinely corrupt archive.
|
|
51
56
|
if (maxSizeMB > 0 && sizeMB > maxSizeMB) {
|
|
52
57
|
logger.error('Document exceeds maximum size', { sizeMB: sizeMB.toFixed(1), maxSizeMB });
|
|
53
|
-
throw new
|
|
58
|
+
throw new ResourceLimitError(
|
|
54
59
|
`Document size (${sizeMB.toFixed(1)}MB) exceeds maximum supported size (${maxSizeMB}MB). ` +
|
|
55
60
|
`This would likely cause out-of-memory errors. Consider:\n` +
|
|
56
61
|
`- Compressing/optimizing images\n` +
|
|
@@ -98,15 +103,26 @@ export class ZipHandler {
|
|
|
98
103
|
const limits = this.getSizeLimits(options);
|
|
99
104
|
this.validateDocumentSize(sizeMB, limits);
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
try {
|
|
107
|
+
await this.reader.loadFromFile(filePath, options);
|
|
108
|
+
|
|
109
|
+
// Copy all files from reader to writer for modification
|
|
110
|
+
const files = this.reader.getAllFiles();
|
|
111
|
+
this.writer.clear();
|
|
112
|
+
this.writer.addFiles(files);
|
|
113
|
+
|
|
114
|
+
this.mode = 'modify';
|
|
115
|
+
logger.info('DOCX file loaded', { fileCount: files.size, sizeMB: sizeMB.toFixed(2) });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// A failed reload must not leave the handler serving the previously loaded
|
|
118
|
+
// archive. Reset to a deterministically empty state so getFile/save/etc.
|
|
119
|
+
// cannot silently return stale content. A fresh reader discards any
|
|
120
|
+
// partially-extracted files and the stale `loaded` flag.
|
|
121
|
+
this.writer.clear();
|
|
122
|
+
this.reader = new ZipReader();
|
|
123
|
+
this.mode = 'write';
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
/**
|
|
@@ -125,15 +141,26 @@ export class ZipHandler {
|
|
|
125
141
|
const limits = this.getSizeLimits(options);
|
|
126
142
|
this.validateDocumentSize(sizeMB, limits);
|
|
127
143
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
144
|
+
try {
|
|
145
|
+
await this.reader.loadFromBuffer(buffer, options);
|
|
146
|
+
|
|
147
|
+
// Copy all files from reader to writer for modification
|
|
148
|
+
const files = this.reader.getAllFiles();
|
|
149
|
+
this.writer.clear();
|
|
150
|
+
this.writer.addFiles(files);
|
|
151
|
+
|
|
152
|
+
this.mode = 'modify';
|
|
153
|
+
logger.info('DOCX buffer loaded', { fileCount: files.size, sizeMB: sizeMB.toFixed(2) });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// A failed reload must not leave the handler serving the previously loaded
|
|
156
|
+
// archive. Reset to a deterministically empty state so getFile/save/etc.
|
|
157
|
+
// cannot silently return stale content. A fresh reader discards any
|
|
158
|
+
// partially-extracted files and the stale `loaded` flag.
|
|
159
|
+
this.writer.clear();
|
|
160
|
+
this.reader = new ZipReader();
|
|
161
|
+
this.mode = 'write';
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
137
164
|
}
|
|
138
165
|
|
|
139
166
|
// ==================== FILE OPERATIONS ====================
|
|
@@ -300,6 +327,13 @@ export class ZipHandler {
|
|
|
300
327
|
if (!file) {
|
|
301
328
|
return false;
|
|
302
329
|
}
|
|
330
|
+
// Guard against degenerate rename where source and destination collapse to
|
|
331
|
+
// the same archive key (identical strings or backslash/forward-slash variants).
|
|
332
|
+
// Without this, addFile would overwrite the single entry and the subsequent
|
|
333
|
+
// removeFile would delete it, silently destroying the file.
|
|
334
|
+
if (normalizePath(oldPath) === normalizePath(newPath)) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
303
337
|
this.addFile(newPath, file.content, {
|
|
304
338
|
binary: file.isBinary,
|
|
305
339
|
date: file.date,
|
|
@@ -336,6 +370,12 @@ export class ZipHandler {
|
|
|
336
370
|
if (!this.copyFile(srcPath, destPath)) {
|
|
337
371
|
return false;
|
|
338
372
|
}
|
|
373
|
+
// Guard against degenerate move where source and destination collapse to the
|
|
374
|
+
// same archive key. copyFile already re-added the (identical) entry, so the
|
|
375
|
+
// removeFile below would delete it, silently destroying the file.
|
|
376
|
+
if (normalizePath(srcPath) === normalizePath(destPath)) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
339
379
|
this.removeFile(srcPath);
|
|
340
380
|
return true;
|
|
341
381
|
}
|