@depup/fast-xml-parser 5.5.11-depup.1 → 5.10.1-depup.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.
@@ -8,6 +8,9 @@ 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, ENTITY_ACTION } from '@nodable/entities';
12
+ import { isUnsafe, HTML as HTML_CONTEXT, XML as XML_CONTEXT } from "is-unsafe"
13
+
11
14
 
12
15
  // const regx =
13
16
  // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
@@ -68,36 +71,10 @@ function extractNamespace(rawTagName) {
68
71
  }
69
72
 
70
73
  export default class OrderedObjParser {
71
- constructor(options) {
74
+ constructor(options, externalEntities) {
72
75
  this.options = options;
73
76
  this.currentNode = null;
74
77
  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
78
  this.parseXml = parseXml;
102
79
  this.parseTextData = parseTextData;
103
80
  this.resolveNameSpace = resolveNameSpace;
@@ -110,12 +87,32 @@ export default class OrderedObjParser {
110
87
  this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
111
88
  this.entityExpansionCount = 0;
112
89
  this.currentExpandedLength = 0;
90
+ this.doctypefound = false;
91
+ let namedEntities = { ...XML };
92
+ if (this.options.entityDecoder) {
93
+ this.entityDecoder = this.options.entityDecoder
94
+ } else {
95
+ if (typeof this.options.htmlEntities === "object") namedEntities = this.options.htmlEntities;
96
+ else if (this.options.htmlEntities === true) namedEntities = { ...COMMON_HTML, ...CURRENCY };
97
+ this.entityDecoder = new EntityDecoder({
98
+ namedEntities: { ...namedEntities, ...externalEntities },
99
+ numericAllowed: this.options.htmlEntities,
100
+ limit: {
101
+ maxTotalExpansions: this.options.processEntities.maxTotalExpansions,
102
+ maxExpandedLength: this.options.processEntities.maxExpandedLength,
103
+ applyLimitsTo: this.options.processEntities.appliesTo,
104
+ },
105
+ // onExternalEntity: (name, value) => isUnsafe(value) ? 'block' : 'allow',
106
+ onInputEntity: (name, value) =>
107
+ //TODO: VALID_CONTEXTS.HTML should be set only if this.options.htmlEntities
108
+ isUnsafe(value, [HTML_CONTEXT, XML_CONTEXT]) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW,
109
+
110
+ //postCheck: resolved => resolved
111
+ });
112
+ }
113
113
 
114
114
  // Initialize path matcher for path-expression-matcher
115
115
  this.matcher = new Matcher();
116
-
117
- // Live read-only proxy of matcher — PEM creates and caches this internally.
118
- // All user callbacks receive this instead of the mutable matcher.
119
116
  this.readonlyMatcher = this.matcher.readOnly();
120
117
 
121
118
  // Flag to track if current node is a stop node (optimization)
@@ -141,17 +138,6 @@ export default class OrderedObjParser {
141
138
 
142
139
  }
143
140
 
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
141
 
156
142
  /**
157
143
  * @param {string} val
@@ -212,9 +198,9 @@ function resolveNameSpace(tagname) {
212
198
  //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm");
213
199
  const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
214
200
 
215
- function buildAttributesMap(attrStr, jPath, tagName) {
201
+ function buildAttributesMap(attrStr, jPath, tagName, force = false) {
216
202
  const options = this.options;
217
- if (options.ignoreAttributes !== true && typeof attrStr === 'string') {
203
+ if (force === true || (options.ignoreAttributes !== true && typeof attrStr === 'string')) {
218
204
  // attrStr = attrStr.replace(/\r?\n/g, ' ');
219
205
  //attrStr = attrStr || attrStr.trim();
220
206
 
@@ -288,7 +274,7 @@ function buildAttributesMap(attrStr, jPath, tagName) {
288
274
 
289
275
  if (!hasAttrs) return;
290
276
 
291
- if (options.attributesGroupName) {
277
+ if (options.attributesGroupName && !options.preserveOrder) {
292
278
  const attrCollection = {};
293
279
  attrCollection[options.attributesGroupName] = attrs;
294
280
  return attrCollection;
@@ -304,13 +290,12 @@ const parseXml = function (xmlData) {
304
290
 
305
291
  // Reset matcher for new document
306
292
  this.matcher.reset();
293
+ this.entityDecoder.reset();
307
294
 
308
295
  // Reset entity expansion counters for this document
309
296
  this.entityExpansionCount = 0;
310
297
  this.currentExpandedLength = 0;
311
- this.docTypeEntitiesKeys = [];
312
- this.lastEntitiesKeys = Object.keys(this.lastEntities);
313
- this.htmlEntitiesKeys = this.options.htmlEntities ? Object.keys(this.htmlEntities) : [];
298
+ this.doctypefound = false;
314
299
  const options = this.options;
315
300
  const docTypeReader = new DocTypeReader(options.processEntities);
316
301
  const xmlLen = xmlData.length;
@@ -360,6 +345,12 @@ const parseXml = function (xmlData) {
360
345
  if (!tagData) throw new Error("Pi Tag is not closed.");
361
346
 
362
347
  textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
348
+ const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true);
349
+ if (attsMap) {
350
+ const ver = attsMap[this.options.attributeNamePrefix + "version"];
351
+ this.entityDecoder.setXmlVersion(Number(ver) || 1.0);
352
+ docTypeReader.setXmlVersion(Number(ver) || 1.0);
353
+ }
363
354
  if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) {
364
355
  //do nothing
365
356
  } else {
@@ -367,8 +358,8 @@ const parseXml = function (xmlData) {
367
358
  const childNode = new xmlNode(tagData.tagName);
368
359
  childNode.add(options.textNodeName, "");
369
360
 
370
- if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
371
- childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
361
+ if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) {
362
+ childNode[":@"] = attsMap
372
363
  }
373
364
  this.addChild(currentNode, childNode, this.readonlyMatcher, i);
374
365
  }
@@ -389,9 +380,10 @@ const parseXml = function (xmlData) {
389
380
  i = endIndex;
390
381
  } else if (c1 === 33
391
382
  && xmlData.charCodeAt(i + 2) === 68) { //'!D'
383
+ if (this.doctypefound) throw new Error("Multiple DOCTYPE declarations found.");
384
+ this.doctypefound = true;
392
385
  const result = docTypeReader.readDocType(xmlData, i);
393
- this.docTypeEntities = result.entities;
394
- this.docTypeEntitiesKeys = Object.keys(this.docTypeEntities) || []
386
+ this.entityDecoder.addInputEntities(result.entities);
395
387
  i = result.i;
396
388
  } else if (c1 === 33
397
389
  && xmlData.charCodeAt(i + 2) === 91) { // '!['
@@ -490,6 +482,7 @@ const parseXml = function (xmlData) {
490
482
 
491
483
  if (prefixedAttrs) {
492
484
  // Extract raw attributes (without prefix) for our use
485
+ //TODO: seems a performance overhead
493
486
  rawAttrs = extractRawAttributes(prefixedAttrs, options);
494
487
  }
495
488
  }
@@ -632,78 +625,7 @@ function replaceEntitiesValue(val, tagName, jPath) {
632
625
  }
633
626
  }
634
627
 
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;
628
+ return this.entityDecoder.decode(val);
707
629
  }
708
630
 
709
631
 
@@ -742,12 +664,16 @@ function isItStopNode() {
742
664
  * @returns
743
665
  */
744
666
  function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
667
+ //TODO: ignore boolean attributes in tag expression
668
+ //TODO: if ignore attributes, dont read full attribute expression but the end. But read for xml declaration
745
669
  let attrBoundary = 0;
746
- const chars = [];
747
670
  const len = xmlData.length;
748
671
  const closeCode0 = closingChar.charCodeAt(0);
749
672
  const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1;
750
673
 
674
+ let result = '';
675
+ let segmentStart = i;
676
+
751
677
  for (let index = i; index < len; index++) {
752
678
  const code = xmlData.charCodeAt(index);
753
679
 
@@ -758,17 +684,18 @@ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
758
684
  } else if (code === closeCode0) {
759
685
  if (closeCode1 !== -1) {
760
686
  if (xmlData.charCodeAt(index + 1) === closeCode1) {
761
- return { data: String.fromCharCode(...chars), index };
687
+ result += xmlData.substring(segmentStart, index);
688
+ return { data: result, index };
762
689
  }
763
690
  } else {
764
- return { data: String.fromCharCode(...chars), index };
691
+ result += xmlData.substring(segmentStart, index);
692
+ return { data: result, index };
765
693
  }
766
- } else if (code === 9) { // \t
767
- chars.push(32); // space
768
- continue;
694
+ } else if (code === 9 && !attrBoundary) { // \t - only replace with space outside attribute values
695
+ // Flush accumulated segment, add space, start new segment
696
+ result += xmlData.substring(segmentStart, index) + ' ';
697
+ segmentStart = index + 1;
769
698
  }
770
-
771
- chars.push(code);
772
699
  }
773
700
  }
774
701
 
@@ -858,7 +785,7 @@ function readStopNodeData(xmlData, tagName, i) {
858
785
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
859
786
  i = closeIndex;
860
787
  } else {
861
- const tagData = readTagExp(xmlData, i, '>')
788
+ const tagData = readTagExp(xmlData, i, false)
862
789
 
863
790
  if (tagData) {
864
791
  const openTagName = tagData && tagData.tagName;
@@ -31,8 +31,8 @@ export default class XMLParser {
31
31
  throw Error(`${result.err.msg}:${result.err.line}:${result.err.col}`)
32
32
  }
33
33
  }
34
- const orderedObjParser = new OrderedObjParser(this.options);
35
- orderedObjParser.addExternalEntities(this.externalEntities);
34
+ const orderedObjParser = new OrderedObjParser(this.options, 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);
@@ -71,6 +71,10 @@ function compress(arr, options, matcher, readonlyMatcher) {
71
71
  let val = compress(tagObj[property], options, matcher, readonlyMatcher);
72
72
  const isLeaf = isLeafTag(val, options);
73
73
 
74
+ if (Object.keys(val).length === 0 && options.alwaysCreateTextNode) {
75
+ val[options.textNodeName] = "";
76
+ }
77
+
74
78
  if (tagObj[":@"]) {
75
79
  assignAttributes(val, tagObj[":@"], readonlyMatcher, options);
76
80
  } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) {