fast-xml-parser 5.5.12 → 5.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-xml-parser",
3
- "version": "5.5.12",
3
+ "version": "5.7.0",
4
4
  "description": "Validate XML, Parse XML, Build XML without C/C++ based libraries",
5
5
  "main": "./lib/fxp.cjs",
6
6
  "type": "module",
@@ -87,7 +87,8 @@
87
87
  }
88
88
  ],
89
89
  "dependencies": {
90
- "fast-xml-builder": "^1.1.4",
90
+ "@nodable/entities": "^2.1.0",
91
+ "fast-xml-builder": "^1.1.5",
91
92
  "path-expression-matcher": "^1.5.0",
92
93
  "strnum": "^2.2.3"
93
94
  }
package/src/fxp.d.ts CHANGED
@@ -211,6 +211,14 @@ export type ProcessEntitiesOptions = {
211
211
  tagFilter?: ((tagName: string, jPathOrMatcher: JPathOrMatcher) => boolean) | null;
212
212
  };
213
213
 
214
+ export type EntityDecoderOptions = {
215
+ setExternalEntities: (entities: Record<string, string>) => void;
216
+ addInputEntities: (entities: Record<string, string>) => void;
217
+ reset: () => void;
218
+ decode: (text: string) => string;
219
+ setXmlVersion: (version: string) => void;
220
+ }
221
+
214
222
  export type X2jOptions = {
215
223
  /**
216
224
  * Preserve the order of tags in resulting JS object
@@ -397,9 +405,14 @@ export type X2jOptions = {
397
405
  * Whether to process HTML entities
398
406
  *
399
407
  * Defaults to `false`
408
+ * @deprecated Use `entityDecoder` instead
400
409
  */
401
410
  htmlEntities?: boolean;
402
411
 
412
+ /**
413
+ * Custom entity decoder
414
+ */
415
+ entityDecoder?: EntityDecoderOptions;
403
416
  /**
404
417
  * Whether to ignore the declaration tag from output
405
418
  *
@@ -707,6 +720,9 @@ export class XMLParser {
707
720
  export class XMLValidator {
708
721
  static validate(xmlData: string, options?: validationOptions): true | ValidationError;
709
722
  }
723
+ /**
724
+ * @deprecated Use npm package 'fast-xml-builder' instead
725
+ */
710
726
  export class XMLBuilder {
711
727
  constructor(options?: XmlBuilderOptions);
712
728
  build(jObj: any): string;
@@ -35,11 +35,8 @@ export default class DocTypeReader {
35
35
  );
36
36
  }
37
37
  //const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
38
- const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
- entities[entityName] = {
40
- regx: RegExp(`&${escaped};`, "g"),
41
- val: val
42
- };
38
+ //const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ entities[entityName] = val;
43
40
  entityCount++;
44
41
  }
45
42
  }
@@ -1,4 +1,5 @@
1
1
  import { DANGEROUS_PROPERTY_NAMES, criticalProperties } from "../util.js";
2
+ import { COMMON_HTML, CURRENCY } from '@nodable/entities';
2
3
 
3
4
  const defaultOnDangerousProperty = (name) => {
4
5
  if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
@@ -39,6 +40,7 @@ export const defaultOptions = {
39
40
  unpairedTags: [],
40
41
  processEntities: true,
41
42
  htmlEntities: false,
43
+ entityDecoder: null,
42
44
  ignoreDeclaration: false,
43
45
  ignorePiTags: false,
44
46
  transformTagName: false,
@@ -85,18 +87,19 @@ function validatePropertyName(propertyName, optionName) {
85
87
  * @param {boolean|object} value
86
88
  * @returns {object} Always returns normalized object
87
89
  */
88
- function normalizeProcessEntities(value) {
90
+ function normalizeProcessEntities(value, htmlEntities) {
89
91
  // Boolean backward compatibility
90
92
  if (typeof value === 'boolean') {
91
93
  return {
92
94
  enabled: value, // true or false
93
95
  maxEntitySize: 10000,
94
- maxExpansionDepth: 10,
95
- maxTotalExpansions: 1000,
96
+ maxExpansionDepth: 10000,
97
+ maxTotalExpansions: Infinity,
96
98
  maxExpandedLength: 100000,
97
- maxEntityCount: 100,
99
+ maxEntityCount: 1000,
98
100
  allowedTags: null,
99
- tagFilter: null
101
+ tagFilter: null,
102
+ appliesTo: "all",
100
103
  };
101
104
  }
102
105
 
@@ -110,7 +113,8 @@ function normalizeProcessEntities(value) {
110
113
  maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000),
111
114
  maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000),
112
115
  allowedTags: value.allowedTags ?? null,
113
- tagFilter: value.tagFilter ?? null
116
+ tagFilter: value.tagFilter ?? null,
117
+ appliesTo: value.appliesTo ?? "all",
114
118
  };
115
119
  }
116
120
 
@@ -141,7 +145,7 @@ export const buildOptions = function (options) {
141
145
  }
142
146
 
143
147
  // Always normalize processEntities for backward compatibility and validation
144
- built.processEntities = normalizeProcessEntities(built.processEntities);
148
+ built.processEntities = normalizeProcessEntities(built.processEntities, built.htmlEntities);
145
149
  built.unpairedTagsSet = new Set(built.unpairedTags);
146
150
  // Convert old-style stopNodes for backward compatibility
147
151
  if (built.stopNodes && Array.isArray(built.stopNodes)) {
@@ -8,6 +8,7 @@ import toNumber from "strnum";
8
8
  import getIgnoreAttributesFn from "../ignoreAttributes.js";
9
9
  import { Expression, Matcher } from 'path-expression-matcher';
10
10
  import { ExpressionSet } from 'path-expression-matcher';
11
+ import { EntityDecoder, XML, CURRENCY, COMMON_HTML } from '@nodable/entities';
11
12
 
12
13
  // const regx =
13
14
  // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
@@ -72,32 +73,6 @@ export default class OrderedObjParser {
72
73
  this.options = options;
73
74
  this.currentNode = null;
74
75
  this.tagsNodeStack = [];
75
- this.docTypeEntities = {};
76
- this.lastEntities = {
77
- "apos": { regex: /&(apos|#39|#x27);/g, val: "'" },
78
- "gt": { regex: /&(gt|#62|#x3E);/g, val: ">" },
79
- "lt": { regex: /&(lt|#60|#x3C);/g, val: "<" },
80
- "quot": { regex: /&(quot|#34|#x22);/g, val: "\"" },
81
- };
82
- this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" };
83
- this.htmlEntities = {
84
- "space": { regex: /&(nbsp|#160);/g, val: " " },
85
- // "lt" : { regex: /&(lt|#60);/g, val: "<" },
86
- // "gt" : { regex: /&(gt|#62);/g, val: ">" },
87
- // "amp" : { regex: /&(amp|#38);/g, val: "&" },
88
- // "quot" : { regex: /&(quot|#34);/g, val: "\"" },
89
- // "apos" : { regex: /&(apos|#39);/g, val: "'" },
90
- "cent": { regex: /&(cent|#162);/g, val: "¢" },
91
- "pound": { regex: /&(pound|#163);/g, val: "£" },
92
- "yen": { regex: /&(yen|#165);/g, val: "¥" },
93
- "euro": { regex: /&(euro|#8364);/g, val: "€" },
94
- "copyright": { regex: /&(copy|#169);/g, val: "©" },
95
- "reg": { regex: /&(reg|#174);/g, val: "®" },
96
- "inr": { regex: /&(inr|#8377);/g, val: "₹" },
97
- "num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") },
98
- "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") },
99
- };
100
- this.addExternalEntities = addExternalEntities;
101
76
  this.parseXml = parseXml;
102
77
  this.parseTextData = parseTextData;
103
78
  this.resolveNameSpace = resolveNameSpace;
@@ -110,6 +85,23 @@ export default class OrderedObjParser {
110
85
  this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
111
86
  this.entityExpansionCount = 0;
112
87
  this.currentExpandedLength = 0;
88
+ let namedEntities = { ...XML };
89
+ if (this.options.entityDecoder) {
90
+ this.entityDecoder = this.options.entityDecoder
91
+ } else {
92
+ if (typeof this.options.htmlEntities === "object") namedEntities = this.options.htmlEntities;
93
+ else if (this.options.htmlEntities === true) namedEntities = { ...COMMON_HTML, ...CURRENCY };
94
+ this.entityDecoder = new EntityDecoder({
95
+ namedEntities: namedEntities,
96
+ numericAllowed: this.options.htmlEntities,
97
+ limit: {
98
+ maxTotalExpansions: this.options.processEntities.maxTotalExpansions,
99
+ maxExpandedLength: this.options.processEntities.maxExpandedLength,
100
+ applyLimitsTo: this.options.processEntities.appliesTo,
101
+ }
102
+ //postCheck: resolved => resolved
103
+ });
104
+ }
113
105
 
114
106
  // Initialize path matcher for path-expression-matcher
115
107
  this.matcher = new Matcher();
@@ -141,17 +133,6 @@ export default class OrderedObjParser {
141
133
 
142
134
  }
143
135
 
144
- function addExternalEntities(externalEntities) {
145
- const entKeys = Object.keys(externalEntities);
146
- for (let i = 0; i < entKeys.length; i++) {
147
- const ent = entKeys[i];
148
- const escaped = ent.replace(/[.\-+*:]/g, '\\.');
149
- this.lastEntities[ent] = {
150
- regex: new RegExp("&" + escaped + ";", "g"),
151
- val: externalEntities[ent]
152
- }
153
- }
154
- }
155
136
 
156
137
  /**
157
138
  * @param {string} val
@@ -212,9 +193,9 @@ function resolveNameSpace(tagname) {
212
193
  //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm");
213
194
  const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
214
195
 
215
- function buildAttributesMap(attrStr, jPath, tagName) {
196
+ function buildAttributesMap(attrStr, jPath, tagName, force = false) {
216
197
  const options = this.options;
217
- if (options.ignoreAttributes !== true && typeof attrStr === 'string') {
198
+ if (force === true || (options.ignoreAttributes !== true && typeof attrStr === 'string')) {
218
199
  // attrStr = attrStr.replace(/\r?\n/g, ' ');
219
200
  //attrStr = attrStr || attrStr.trim();
220
201
 
@@ -304,13 +285,11 @@ const parseXml = function (xmlData) {
304
285
 
305
286
  // Reset matcher for new document
306
287
  this.matcher.reset();
288
+ this.entityDecoder.reset();
307
289
 
308
290
  // Reset entity expansion counters for this document
309
291
  this.entityExpansionCount = 0;
310
292
  this.currentExpandedLength = 0;
311
- this.docTypeEntitiesKeys = [];
312
- this.lastEntitiesKeys = Object.keys(this.lastEntities);
313
- this.htmlEntitiesKeys = this.options.htmlEntities ? Object.keys(this.htmlEntities) : [];
314
293
  const options = this.options;
315
294
  const docTypeReader = new DocTypeReader(options.processEntities);
316
295
  const xmlLen = xmlData.length;
@@ -360,6 +339,11 @@ const parseXml = function (xmlData) {
360
339
  if (!tagData) throw new Error("Pi Tag is not closed.");
361
340
 
362
341
  textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
342
+ const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true);
343
+ if (attsMap) {
344
+ const ver = attsMap[this.options.attributeNamePrefix + "version"];
345
+ this.entityDecoder.setXmlVersion(Number(ver) || 1.0);
346
+ }
363
347
  if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) {
364
348
  //do nothing
365
349
  } else {
@@ -367,8 +351,8 @@ const parseXml = function (xmlData) {
367
351
  const childNode = new xmlNode(tagData.tagName);
368
352
  childNode.add(options.textNodeName, "");
369
353
 
370
- if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
371
- childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
354
+ if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) {
355
+ childNode[":@"] = attsMap
372
356
  }
373
357
  this.addChild(currentNode, childNode, this.readonlyMatcher, i);
374
358
  }
@@ -390,8 +374,7 @@ const parseXml = function (xmlData) {
390
374
  } else if (c1 === 33
391
375
  && xmlData.charCodeAt(i + 2) === 68) { //'!D'
392
376
  const result = docTypeReader.readDocType(xmlData, i);
393
- this.docTypeEntities = result.entities;
394
- this.docTypeEntitiesKeys = Object.keys(this.docTypeEntities) || []
377
+ this.entityDecoder.addInputEntities(result.entities);
395
378
  i = result.i;
396
379
  } else if (c1 === 33
397
380
  && xmlData.charCodeAt(i + 2) === 91) { // '!['
@@ -490,6 +473,7 @@ const parseXml = function (xmlData) {
490
473
 
491
474
  if (prefixedAttrs) {
492
475
  // Extract raw attributes (without prefix) for our use
476
+ //TODO: seems a performance overhead
493
477
  rawAttrs = extractRawAttributes(prefixedAttrs, options);
494
478
  }
495
479
  }
@@ -632,78 +616,7 @@ function replaceEntitiesValue(val, tagName, jPath) {
632
616
  }
633
617
  }
634
618
 
635
- // Replace DOCTYPE entities
636
- for (const entityName of this.docTypeEntitiesKeys) {
637
- const entity = this.docTypeEntities[entityName];
638
- const matches = val.match(entity.regx);
639
-
640
- if (matches) {
641
- // Track expansions
642
- this.entityExpansionCount += matches.length;
643
-
644
- // Check expansion limit
645
- if (entityConfig.maxTotalExpansions &&
646
- this.entityExpansionCount > entityConfig.maxTotalExpansions) {
647
- throw new Error(
648
- `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
649
- );
650
- }
651
-
652
- // Store length before replacement
653
- const lengthBefore = val.length;
654
- val = val.replace(entity.regx, entity.val);
655
-
656
- // Check expanded length immediately after replacement
657
- if (entityConfig.maxExpandedLength) {
658
- this.currentExpandedLength += (val.length - lengthBefore);
659
-
660
- if (this.currentExpandedLength > entityConfig.maxExpandedLength) {
661
- throw new Error(
662
- `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}`
663
- );
664
- }
665
- }
666
- }
667
- }
668
- if (val.indexOf('&') === -1) return val;
669
- // Replace standard entities
670
- for (const entityName of this.lastEntitiesKeys) {
671
- const entity = this.lastEntities[entityName];
672
- const matches = val.match(entity.regex);
673
- if (matches) {
674
- this.entityExpansionCount += matches.length;
675
- if (entityConfig.maxTotalExpansions &&
676
- this.entityExpansionCount > entityConfig.maxTotalExpansions) {
677
- throw new Error(
678
- `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
679
- );
680
- }
681
- }
682
- val = val.replace(entity.regex, entity.val);
683
- }
684
- if (val.indexOf('&') === -1) return val;
685
-
686
- // Replace HTML entities if enabled
687
- for (const entityName of this.htmlEntitiesKeys) {
688
- const entity = this.htmlEntities[entityName];
689
- const matches = val.match(entity.regex);
690
- if (matches) {
691
- //console.log(matches);
692
- this.entityExpansionCount += matches.length;
693
- if (entityConfig.maxTotalExpansions &&
694
- this.entityExpansionCount > entityConfig.maxTotalExpansions) {
695
- throw new Error(
696
- `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
697
- );
698
- }
699
- }
700
- val = val.replace(entity.regex, entity.val);
701
- }
702
-
703
- // Replace ampersand entity last
704
- val = val.replace(this.ampEntity.regex, this.ampEntity.val);
705
-
706
- return val;
619
+ return this.entityDecoder.decode(val);
707
620
  }
708
621
 
709
622
 
@@ -32,7 +32,7 @@ export default class XMLParser {
32
32
  }
33
33
  }
34
34
  const orderedObjParser = new OrderedObjParser(this.options);
35
- orderedObjParser.addExternalEntities(this.externalEntities);
35
+ orderedObjParser.entityDecoder.setExternalEntities(this.externalEntities);
36
36
  const orderedResult = orderedObjParser.parseXml(xmlData);
37
37
  if (this.options.preserveOrder || orderedResult === undefined) return orderedResult;
38
38
  else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher);