@depup/fast-xml-parser 5.5.11-depup.1 → 5.11.0-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;
@@ -351,7 +336,12 @@ const parseXml = function (xmlData) {
351
336
  this.matcher.pop();
352
337
  this.isCurrentNodeStopNode = false; // Reset flag when closing tag
353
338
 
354
- currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
339
+ //a closing tag with no matching opening tag leaves the stack empty
340
+ currentNode = this.tagsNodeStack.pop() || xmlObj;//avoid recursion, set the parent tag scope
341
+
342
+ if (options.captureMetaData && currentNode) {
343
+ currentNode.addEndIndex(closeIndex + 1);
344
+ }
355
345
  textData = "";
356
346
  i = closeIndex;
357
347
  } else if (c1 === 63) { //'?'
@@ -360,6 +350,12 @@ const parseXml = function (xmlData) {
360
350
  if (!tagData) throw new Error("Pi Tag is not closed.");
361
351
 
362
352
  textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
353
+ const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true);
354
+ if (attsMap) {
355
+ const ver = attsMap[this.options.attributeNamePrefix + "version"];
356
+ this.entityDecoder.setXmlVersion(Number(ver) || 1.0);
357
+ docTypeReader.setXmlVersion(Number(ver) || 1.0);
358
+ }
363
359
  if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) {
364
360
  //do nothing
365
361
  } else {
@@ -367,10 +363,15 @@ const parseXml = function (xmlData) {
367
363
  const childNode = new xmlNode(tagData.tagName);
368
364
  childNode.add(options.textNodeName, "");
369
365
 
370
- if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
371
- childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
366
+ if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) {
367
+ childNode[":@"] = attsMap
372
368
  }
373
369
  this.addChild(currentNode, childNode, this.readonlyMatcher, i);
370
+
371
+ if (options.captureMetaData) {
372
+ // closeIndex points at '?' of the closing '?>'
373
+ currentNode.addEndIndex(tagData.closeIndex + 2);
374
+ }
374
375
  }
375
376
 
376
377
 
@@ -389,9 +390,10 @@ const parseXml = function (xmlData) {
389
390
  i = endIndex;
390
391
  } else if (c1 === 33
391
392
  && xmlData.charCodeAt(i + 2) === 68) { //'!D'
393
+ if (this.doctypefound) throw new Error("Multiple DOCTYPE declarations found.");
394
+ this.doctypefound = true;
392
395
  const result = docTypeReader.readDocType(xmlData, i);
393
- this.docTypeEntities = result.entities;
394
- this.docTypeEntitiesKeys = Object.keys(this.docTypeEntities) || []
396
+ this.entityDecoder.addInputEntities(result.entities);
395
397
  i = result.i;
396
398
  } else if (c1 === 33
397
399
  && xmlData.charCodeAt(i + 2) === 91) { // '!['
@@ -490,6 +492,7 @@ const parseXml = function (xmlData) {
490
492
 
491
493
  if (prefixedAttrs) {
492
494
  // Extract raw attributes (without prefix) for our use
495
+ //TODO: seems a performance overhead
493
496
  rawAttrs = extractRawAttributes(prefixedAttrs, options);
494
497
  }
495
498
  }
@@ -533,6 +536,10 @@ const parseXml = function (xmlData) {
533
536
  this.isCurrentNodeStopNode = false; // Reset flag
534
537
 
535
538
  this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
539
+
540
+ if (options.captureMetaData) {
541
+ currentNode.addEndIndex(i + 1);
542
+ }
536
543
  } else {
537
544
  //selfClosing tag
538
545
  if (isSelfClosing) {
@@ -543,6 +550,10 @@ const parseXml = function (xmlData) {
543
550
  childNode[":@"] = prefixedAttrs;
544
551
  }
545
552
  this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
553
+
554
+ if (options.captureMetaData) {
555
+ currentNode.addEndIndex(closeIndex + 1);
556
+ }
546
557
  this.matcher.pop(); // Pop self-closing tag
547
558
  this.isCurrentNodeStopNode = false; // Reset flag
548
559
  }
@@ -552,6 +563,10 @@ const parseXml = function (xmlData) {
552
563
  childNode[":@"] = prefixedAttrs;
553
564
  }
554
565
  this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
566
+
567
+ if (options.captureMetaData) {
568
+ currentNode.addEndIndex(result.closeIndex + 1);
569
+ }
555
570
  this.matcher.pop(); // Pop unpaired tag
556
571
  this.isCurrentNodeStopNode = false; // Reset flag
557
572
  i = result.closeIndex;
@@ -632,78 +647,7 @@ function replaceEntitiesValue(val, tagName, jPath) {
632
647
  }
633
648
  }
634
649
 
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;
650
+ return this.entityDecoder.decode(val);
707
651
  }
708
652
 
709
653
 
@@ -742,12 +686,16 @@ function isItStopNode() {
742
686
  * @returns
743
687
  */
744
688
  function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
689
+ //TODO: ignore boolean attributes in tag expression
690
+ //TODO: if ignore attributes, dont read full attribute expression but the end. But read for xml declaration
745
691
  let attrBoundary = 0;
746
- const chars = [];
747
692
  const len = xmlData.length;
748
693
  const closeCode0 = closingChar.charCodeAt(0);
749
694
  const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1;
750
695
 
696
+ let result = '';
697
+ let segmentStart = i;
698
+
751
699
  for (let index = i; index < len; index++) {
752
700
  const code = xmlData.charCodeAt(index);
753
701
 
@@ -758,17 +706,18 @@ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
758
706
  } else if (code === closeCode0) {
759
707
  if (closeCode1 !== -1) {
760
708
  if (xmlData.charCodeAt(index + 1) === closeCode1) {
761
- return { data: String.fromCharCode(...chars), index };
709
+ result += xmlData.substring(segmentStart, index);
710
+ return { data: result, index };
762
711
  }
763
712
  } else {
764
- return { data: String.fromCharCode(...chars), index };
713
+ result += xmlData.substring(segmentStart, index);
714
+ return { data: result, index };
765
715
  }
766
- } else if (code === 9) { // \t
767
- chars.push(32); // space
768
- continue;
716
+ } else if (code === 9 && !attrBoundary) { // \t - only replace with space outside attribute values
717
+ // Flush accumulated segment, add space, start new segment
718
+ result += xmlData.substring(segmentStart, index) + ' ';
719
+ segmentStart = index + 1;
769
720
  }
770
-
771
- chars.push(code);
772
721
  }
773
722
  }
774
723
 
@@ -858,7 +807,7 @@ function readStopNodeData(xmlData, tagName, i) {
858
807
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
859
808
  i = closeIndex;
860
809
  } else {
861
- const tagData = readTagExp(xmlData, i, '>')
810
+ const tagData = readTagExp(xmlData, i, false)
862
811
 
863
812
  if (tagData) {
864
813
  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) {
@@ -27,12 +27,26 @@ export default class XmlNode {
27
27
  this.child.push({ [node.tagname]: node.child });
28
28
  }
29
29
  // if requested, add the startIndex
30
+ this.addStartIndex(startIndex);
31
+ }
32
+
33
+ addStartIndex(startIndex) {
30
34
  if (startIndex !== undefined) {
31
35
  // Note: for now we just overwrite the metadata. If we had more complex metadata,
32
36
  // we might need to do an object append here: metadata = { ...metadata, startIndex }
33
37
  this.child[this.child.length - 1][METADATA_SYMBOL] = { startIndex };
34
38
  }
35
39
  }
40
+
41
+ addEndIndex(endIndex) {
42
+ const lastChild = this.child[this.child.length - 1];
43
+ // endIndex is write-once: when updateTag drops a node, the last child is a
44
+ // previously completed sibling whose endIndex must not be overwritten
45
+ if (lastChild !== undefined && lastChild[METADATA_SYMBOL] !== undefined
46
+ && lastChild[METADATA_SYMBOL].endIndex === undefined) {
47
+ lastChild[METADATA_SYMBOL].endIndex = endIndex;
48
+ }
49
+ }
36
50
  /** symbol used for metadata */
37
51
  static getMetaDataSymbol() {
38
52
  return METADATA_SYMBOL;