@lucianpacurar/iso20022.js 0.2.17 → 0.2.18

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/dist/index.js CHANGED
@@ -32,6 +32,24 @@ function isExist(v) {
32
32
  return typeof v !== 'undefined';
33
33
  }
34
34
 
35
+ /**
36
+ * Dangerous property names that could lead to prototype pollution or security issues
37
+ */
38
+ const DANGEROUS_PROPERTY_NAMES = [
39
+ // '__proto__',
40
+ // 'constructor',
41
+ // 'prototype',
42
+ 'hasOwnProperty',
43
+ 'toString',
44
+ 'valueOf',
45
+ '__defineGetter__',
46
+ '__defineSetter__',
47
+ '__lookupGetter__',
48
+ '__lookupSetter__'
49
+ ];
50
+
51
+ const criticalProperties = ["__proto__", "constructor", "prototype"];
52
+
35
53
  const defaultOptions$2 = {
36
54
  allowBooleanAttributes: false, //A tag can have attributes without any value
37
55
  unpairedTags: []
@@ -54,19 +72,19 @@ function validate(xmlData, options) {
54
72
  // check for byte order mark (BOM)
55
73
  xmlData = xmlData.substr(1);
56
74
  }
57
-
75
+
58
76
  for (let i = 0; i < xmlData.length; i++) {
59
77
 
60
- if (xmlData[i] === '<' && xmlData[i+1] === '?') {
61
- i+=2;
62
- i = readPI(xmlData,i);
78
+ if (xmlData[i] === '<' && xmlData[i + 1] === '?') {
79
+ i += 2;
80
+ i = readPI(xmlData, i);
63
81
  if (i.err) return i;
64
- }else if (xmlData[i] === '<') {
82
+ } else if (xmlData[i] === '<') {
65
83
  //starting of tag
66
84
  //read until you reach to '>' avoiding any '>' in attribute value
67
85
  let tagStartPos = i;
68
86
  i++;
69
-
87
+
70
88
  if (xmlData[i] === '!') {
71
89
  i = readCommentAndCDATA(xmlData, i);
72
90
  continue;
@@ -102,14 +120,14 @@ function validate(xmlData, options) {
102
120
  if (tagName.trim().length === 0) {
103
121
  msg = "Invalid space after '<'.";
104
122
  } else {
105
- msg = "Tag '"+tagName+"' is an invalid name.";
123
+ msg = "Tag '" + tagName + "' is an invalid name.";
106
124
  }
107
125
  return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i));
108
126
  }
109
127
 
110
128
  const result = readAttributeStr(xmlData, i);
111
129
  if (result === false) {
112
- return getErrorObject('InvalidAttr', "Attributes for '"+tagName+"' have open quote.", getLineNumberForPosition(xmlData, i));
130
+ return getErrorObject('InvalidAttr', "Attributes for '" + tagName + "' have open quote.", getLineNumberForPosition(xmlData, i));
113
131
  }
114
132
  let attrStr = result.value;
115
133
  i = result.index;
@@ -130,17 +148,17 @@ function validate(xmlData, options) {
130
148
  }
131
149
  } else if (closingTag) {
132
150
  if (!result.tagClosed) {
133
- return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
151
+ return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' doesn't have proper closing.", getLineNumberForPosition(xmlData, i));
134
152
  } else if (attrStr.trim().length > 0) {
135
- return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
153
+ return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos));
136
154
  } else if (tags.length === 0) {
137
- return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
155
+ return getErrorObject('InvalidTag', "Closing tag '" + tagName + "' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos));
138
156
  } else {
139
157
  const otg = tags.pop();
140
158
  if (tagName !== otg.tagName) {
141
159
  let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos);
142
160
  return getErrorObject('InvalidTag',
143
- "Expected closing tag '"+otg.tagName+"' (opened in line "+openPos.line+", col "+openPos.col+") instead of closing tag '"+tagName+"'.",
161
+ "Expected closing tag '" + otg.tagName + "' (opened in line " + openPos.line + ", col " + openPos.col + ") instead of closing tag '" + tagName + "'.",
144
162
  getLineNumberForPosition(xmlData, tagStartPos));
145
163
  }
146
164
 
@@ -161,8 +179,8 @@ function validate(xmlData, options) {
161
179
  //if the root level has been reached before ...
162
180
  if (reachedRoot === true) {
163
181
  return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
164
- } else if(options.unpairedTags.indexOf(tagName) !== -1); else {
165
- tags.push({tagName, tagStartPos});
182
+ } else if (options.unpairedTags.indexOf(tagName) !== -1) ; else {
183
+ tags.push({ tagName, tagStartPos });
166
184
  }
167
185
  tagFound = true;
168
186
  }
@@ -176,7 +194,7 @@ function validate(xmlData, options) {
176
194
  i++;
177
195
  i = readCommentAndCDATA(xmlData, i);
178
196
  continue;
179
- } else if (xmlData[i+1] === '?') {
197
+ } else if (xmlData[i + 1] === '?') {
180
198
  i = readPI(xmlData, ++i);
181
199
  if (i.err) return i;
182
200
  } else {
@@ -187,7 +205,7 @@ function validate(xmlData, options) {
187
205
  if (afterAmp == -1)
188
206
  return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i));
189
207
  i = afterAmp;
190
- }else {
208
+ } else {
191
209
  if (reachedRoot === true && !isWhiteSpace(xmlData[i])) {
192
210
  return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i));
193
211
  }
@@ -198,27 +216,27 @@ function validate(xmlData, options) {
198
216
  }
199
217
  }
200
218
  } else {
201
- if ( isWhiteSpace(xmlData[i])) {
219
+ if (isWhiteSpace(xmlData[i])) {
202
220
  continue;
203
221
  }
204
- return getErrorObject('InvalidChar', "char '"+xmlData[i]+"' is not expected.", getLineNumberForPosition(xmlData, i));
222
+ return getErrorObject('InvalidChar', "char '" + xmlData[i] + "' is not expected.", getLineNumberForPosition(xmlData, i));
205
223
  }
206
224
  }
207
225
 
208
226
  if (!tagFound) {
209
227
  return getErrorObject('InvalidXml', 'Start tag expected.', 1);
210
- }else if (tags.length == 1) {
211
- return getErrorObject('InvalidTag', "Unclosed tag '"+tags[0].tagName+"'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
212
- }else if (tags.length > 0) {
213
- return getErrorObject('InvalidXml', "Invalid '"+
214
- JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '')+
215
- "' found.", {line: 1, col: 1});
228
+ } else if (tags.length == 1) {
229
+ return getErrorObject('InvalidTag', "Unclosed tag '" + tags[0].tagName + "'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos));
230
+ } else if (tags.length > 0) {
231
+ return getErrorObject('InvalidXml', "Invalid '" +
232
+ JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '') +
233
+ "' found.", { line: 1, col: 1 });
216
234
  }
217
235
 
218
236
  return true;
219
237
  }
220
- function isWhiteSpace(char){
221
- return char === ' ' || char === '\t' || char === '\n' || char === '\r';
238
+ function isWhiteSpace(char) {
239
+ return char === ' ' || char === '\t' || char === '\n' || char === '\r';
222
240
  }
223
241
  /**
224
242
  * Read Processing insstructions and skip
@@ -352,25 +370,25 @@ function validateAttributeString(attrStr, options) {
352
370
  for (let i = 0; i < matches.length; i++) {
353
371
  if (matches[i][1].length === 0) {
354
372
  //nospace before attribute name: a="sd"b="saf"
355
- return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(matches[i]))
373
+ return getErrorObject('InvalidAttr', "Attribute '" + matches[i][2] + "' has no space in starting.", getPositionFromMatch(matches[i]))
356
374
  } else if (matches[i][3] !== undefined && matches[i][4] === undefined) {
357
- return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' is without value.", getPositionFromMatch(matches[i]));
375
+ return getErrorObject('InvalidAttr', "Attribute '" + matches[i][2] + "' is without value.", getPositionFromMatch(matches[i]));
358
376
  } else if (matches[i][3] === undefined && !options.allowBooleanAttributes) {
359
377
  //independent attribute: ab
360
- return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(matches[i]));
378
+ return getErrorObject('InvalidAttr', "boolean attribute '" + matches[i][2] + "' is not allowed.", getPositionFromMatch(matches[i]));
361
379
  }
362
380
  /* else if(matches[i][6] === undefined){//attribute without value: ab=
363
381
  return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}};
364
382
  } */
365
383
  const attrName = matches[i][2];
366
384
  if (!validateAttrName(attrName)) {
367
- return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(matches[i]));
385
+ return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is an invalid name.", getPositionFromMatch(matches[i]));
368
386
  }
369
- if (!attrNames.hasOwnProperty(attrName)) {
387
+ if (!Object.prototype.hasOwnProperty.call(attrNames, attrName)) {
370
388
  //check for duplicate attribute.
371
389
  attrNames[attrName] = 1;
372
390
  } else {
373
- return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(matches[i]));
391
+ return getErrorObject('InvalidAttr', "Attribute '" + attrName + "' is repeated.", getPositionFromMatch(matches[i]));
374
392
  }
375
393
  }
376
394
 
@@ -449,6 +467,14 @@ function getPositionFromMatch(match) {
449
467
  return match.startIndex + match[1].length;
450
468
  }
451
469
 
470
+ const defaultOnDangerousProperty = (name) => {
471
+ if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
472
+ return "__" + name;
473
+ }
474
+ return name;
475
+ };
476
+
477
+
452
478
  const defaultOptions$1 = {
453
479
  preserveOrder: false,
454
480
  attributeNamePrefix: '@_',
@@ -489,8 +515,38 @@ const defaultOptions$1 = {
489
515
  },
490
516
  // skipEmptyListItem: false
491
517
  captureMetaData: false,
518
+ maxNestedTags: 100,
519
+ strictReservedNames: true,
520
+ jPath: true, // if true, pass jPath string to callbacks; if false, pass matcher instance
521
+ onDangerousProperty: defaultOnDangerousProperty
492
522
  };
493
523
 
524
+
525
+ /**
526
+ * Validates that a property name is safe to use
527
+ * @param {string} propertyName - The property name to validate
528
+ * @param {string} optionName - The option field name (for error message)
529
+ * @throws {Error} If property name is dangerous
530
+ */
531
+ function validatePropertyName(propertyName, optionName) {
532
+ if (typeof propertyName !== 'string') {
533
+ return; // Only validate string property names
534
+ }
535
+
536
+ const normalized = propertyName.toLowerCase();
537
+ if (DANGEROUS_PROPERTY_NAMES.some(dangerous => normalized === dangerous.toLowerCase())) {
538
+ throw new Error(
539
+ `[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
540
+ );
541
+ }
542
+
543
+ if (criticalProperties.some(dangerous => normalized === dangerous.toLowerCase())) {
544
+ throw new Error(
545
+ `[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
546
+ );
547
+ }
548
+ }
549
+
494
550
  /**
495
551
  * Normalizes processEntities option for backward compatibility
496
552
  * @param {boolean|object} value
@@ -505,6 +561,7 @@ function normalizeProcessEntities(value) {
505
561
  maxExpansionDepth: 10,
506
562
  maxTotalExpansions: 1000,
507
563
  maxExpandedLength: 100000,
564
+ maxEntityCount: 100,
508
565
  allowedTags: null,
509
566
  tagFilter: null
510
567
  };
@@ -513,11 +570,12 @@ function normalizeProcessEntities(value) {
513
570
  // Object config - merge with defaults
514
571
  if (typeof value === 'object' && value !== null) {
515
572
  return {
516
- enabled: value.enabled !== false, // default true if not specified
517
- maxEntitySize: value.maxEntitySize ?? 10000,
518
- maxExpansionDepth: value.maxExpansionDepth ?? 10,
519
- maxTotalExpansions: value.maxTotalExpansions ?? 1000,
520
- maxExpandedLength: value.maxExpandedLength ?? 100000,
573
+ enabled: value.enabled !== false,
574
+ maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000),
575
+ maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10000),
576
+ maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity),
577
+ maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000),
578
+ maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000),
521
579
  allowedTags: value.allowedTags ?? null,
522
580
  tagFilter: value.tagFilter ?? null
523
581
  };
@@ -530,8 +588,39 @@ function normalizeProcessEntities(value) {
530
588
  const buildOptions = function (options) {
531
589
  const built = Object.assign({}, defaultOptions$1, options);
532
590
 
591
+ // Validate property names to prevent prototype pollution
592
+ const propertyNameOptions = [
593
+ { value: built.attributeNamePrefix, name: 'attributeNamePrefix' },
594
+ { value: built.attributesGroupName, name: 'attributesGroupName' },
595
+ { value: built.textNodeName, name: 'textNodeName' },
596
+ { value: built.cdataPropName, name: 'cdataPropName' },
597
+ { value: built.commentPropName, name: 'commentPropName' }
598
+ ];
599
+
600
+ for (const { value, name } of propertyNameOptions) {
601
+ if (value) {
602
+ validatePropertyName(value, name);
603
+ }
604
+ }
605
+
606
+ if (built.onDangerousProperty === null) {
607
+ built.onDangerousProperty = defaultOnDangerousProperty;
608
+ }
609
+
533
610
  // Always normalize processEntities for backward compatibility and validation
534
611
  built.processEntities = normalizeProcessEntities(built.processEntities);
612
+ built.unpairedTagsSet = new Set(built.unpairedTags);
613
+ // Convert old-style stopNodes for backward compatibility
614
+ if (built.stopNodes && Array.isArray(built.stopNodes)) {
615
+ built.stopNodes = built.stopNodes.map(node => {
616
+ if (typeof node === 'string' && node.startsWith('*.')) {
617
+ // Old syntax: *.tagname meant "tagname anywhere"
618
+ // Convert to new syntax: ..tagname
619
+ return '..' + node.substring(2);
620
+ }
621
+ return node;
622
+ });
623
+ }
535
624
  //console.debug(built.processEntities)
536
625
  return built;
537
626
  };
@@ -544,23 +633,23 @@ if (typeof Symbol !== "function") {
544
633
  METADATA_SYMBOL$1 = Symbol("XML Node Metadata");
545
634
  }
546
635
 
547
- class XmlNode{
636
+ class XmlNode {
548
637
  constructor(tagname) {
549
638
  this.tagname = tagname;
550
639
  this.child = []; //nested tags, text, cdata, comments in order
551
- this[":@"] = {}; //attributes map
640
+ this[":@"] = Object.create(null); //attributes map
552
641
  }
553
- add(key,val){
642
+ add(key, val) {
554
643
  // this.child.push( {name : key, val: val, isCdata: isCdata });
555
- if(key === "__proto__") key = "#__proto__";
556
- this.child.push( {[key]: val });
644
+ if (key === "__proto__") key = "#__proto__";
645
+ this.child.push({ [key]: val });
557
646
  }
558
647
  addChild(node, startIndex) {
559
- if(node.tagname === "__proto__") node.tagname = "#__proto__";
560
- if(node[":@"] && Object.keys(node[":@"]).length > 0){
561
- this.child.push( { [node.tagname]: node.child, [":@"]: node[":@"] });
562
- }else {
563
- this.child.push( { [node.tagname]: node.child });
648
+ if (node.tagname === "__proto__") node.tagname = "#__proto__";
649
+ if (node[":@"] && Object.keys(node[":@"]).length > 0) {
650
+ this.child.push({ [node.tagname]: node.child, [":@"]: node[":@"] });
651
+ } else {
652
+ this.child.push({ [node.tagname]: node.child });
564
653
  }
565
654
  // if requested, add the startIndex
566
655
  if (startIndex !== undefined) {
@@ -582,8 +671,9 @@ class DocTypeReader {
582
671
  }
583
672
 
584
673
  readDocType(xmlData, i) {
674
+ const entities = Object.create(null);
675
+ let entityCount = 0;
585
676
 
586
- const entities = {};
587
677
  if (xmlData[i + 3] === 'O' &&
588
678
  xmlData[i + 4] === 'C' &&
589
679
  xmlData[i + 5] === 'T' &&
@@ -601,11 +691,20 @@ class DocTypeReader {
601
691
  let entityName, val;
602
692
  [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
603
693
  if (val.indexOf("&") === -1) { //Parameter entities are not supported
604
- const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
694
+ if (this.options.enabled !== false &&
695
+ this.options.maxEntityCount != null &&
696
+ entityCount >= this.options.maxEntityCount) {
697
+ throw new Error(
698
+ `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})`
699
+ );
700
+ }
701
+ //const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
702
+ const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
605
703
  entities[entityName] = {
606
704
  regx: RegExp(`&${escaped};`, "g"),
607
705
  val: val
608
706
  };
707
+ entityCount++;
609
708
  }
610
709
  }
611
710
  else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
@@ -665,11 +764,12 @@ class DocTypeReader {
665
764
  i = skipWhitespace(xmlData, i);
666
765
 
667
766
  // Read entity name
668
- let entityName = "";
767
+ const startIndex = i;
669
768
  while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
670
- entityName += xmlData[i];
671
769
  i++;
672
770
  }
771
+ let entityName = xmlData.substring(startIndex, i);
772
+
673
773
  validateEntityName(entityName);
674
774
 
675
775
  // Skip whitespace after entity name
@@ -690,7 +790,7 @@ class DocTypeReader {
690
790
 
691
791
  // Validate entity size
692
792
  if (this.options.enabled !== false &&
693
- this.options.maxEntitySize &&
793
+ this.options.maxEntitySize != null &&
694
794
  entityValue.length > this.options.maxEntitySize) {
695
795
  throw new Error(
696
796
  `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})`
@@ -706,11 +806,13 @@ class DocTypeReader {
706
806
  i = skipWhitespace(xmlData, i);
707
807
 
708
808
  // Read notation name
709
- let notationName = "";
809
+
810
+ const startIndex = i;
710
811
  while (i < xmlData.length && !/\s/.test(xmlData[i])) {
711
- notationName += xmlData[i];
712
812
  i++;
713
813
  }
814
+ let notationName = xmlData.substring(startIndex, i);
815
+
714
816
  !this.suppressValidationErr && validateEntityName(notationName);
715
817
 
716
818
  // Skip whitespace after notation name
@@ -760,10 +862,11 @@ class DocTypeReader {
760
862
  }
761
863
  i++;
762
864
 
865
+ const startIndex = i;
763
866
  while (i < xmlData.length && xmlData[i] !== startChar) {
764
- identifierVal += xmlData[i];
765
867
  i++;
766
868
  }
869
+ identifierVal = xmlData.substring(startIndex, i);
767
870
 
768
871
  if (xmlData[i] !== startChar) {
769
872
  throw new Error(`Unterminated ${type} value`);
@@ -783,11 +886,11 @@ class DocTypeReader {
783
886
  i = skipWhitespace(xmlData, i);
784
887
 
785
888
  // Read element name
786
- let elementName = "";
889
+ const startIndex = i;
787
890
  while (i < xmlData.length && !/\s/.test(xmlData[i])) {
788
- elementName += xmlData[i];
789
891
  i++;
790
892
  }
893
+ let elementName = xmlData.substring(startIndex, i);
791
894
 
792
895
  // Validate element name
793
896
  if (!this.suppressValidationErr && !isName(elementName)) {
@@ -804,10 +907,12 @@ class DocTypeReader {
804
907
  i++; // Move past '('
805
908
 
806
909
  // Read content model
910
+ const startIndex = i;
807
911
  while (i < xmlData.length && xmlData[i] !== ")") {
808
- contentModel += xmlData[i];
809
912
  i++;
810
913
  }
914
+ contentModel = xmlData.substring(startIndex, i);
915
+
811
916
  if (xmlData[i] !== ")") {
812
917
  throw new Error("Unterminated content model");
813
918
  }
@@ -828,11 +933,11 @@ class DocTypeReader {
828
933
  i = skipWhitespace(xmlData, i);
829
934
 
830
935
  // Read element name
831
- let elementName = "";
936
+ let startIndex = i;
832
937
  while (i < xmlData.length && !/\s/.test(xmlData[i])) {
833
- elementName += xmlData[i];
834
938
  i++;
835
939
  }
940
+ let elementName = xmlData.substring(startIndex, i);
836
941
 
837
942
  // Validate element name
838
943
  validateEntityName(elementName);
@@ -841,11 +946,11 @@ class DocTypeReader {
841
946
  i = skipWhitespace(xmlData, i);
842
947
 
843
948
  // Read attribute name
844
- let attributeName = "";
949
+ startIndex = i;
845
950
  while (i < xmlData.length && !/\s/.test(xmlData[i])) {
846
- attributeName += xmlData[i];
847
951
  i++;
848
952
  }
953
+ let attributeName = xmlData.substring(startIndex, i);
849
954
 
850
955
  // Validate attribute name
851
956
  if (!validateEntityName(attributeName)) {
@@ -873,11 +978,13 @@ class DocTypeReader {
873
978
  // Read the list of allowed notations
874
979
  let allowedNotations = [];
875
980
  while (i < xmlData.length && xmlData[i] !== ")") {
876
- let notation = "";
981
+
982
+
983
+ const startIndex = i;
877
984
  while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") {
878
- notation += xmlData[i];
879
985
  i++;
880
986
  }
987
+ let notation = xmlData.substring(startIndex, i);
881
988
 
882
989
  // Validate notation name
883
990
  notation = notation.trim();
@@ -903,10 +1010,11 @@ class DocTypeReader {
903
1010
  attributeType += " (" + allowedNotations.join("|") + ")";
904
1011
  } else {
905
1012
  // Handle simple types (e.g., CDATA, ID, IDREF, etc.)
1013
+ const startIndex = i;
906
1014
  while (i < xmlData.length && !/\s/.test(xmlData[i])) {
907
- attributeType += xmlData[i];
908
1015
  i++;
909
1016
  }
1017
+ attributeType += xmlData.substring(startIndex, i);
910
1018
 
911
1019
  // Validate simple attribute type
912
1020
  const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"];
@@ -970,48 +1078,52 @@ const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/;
970
1078
  // const octRegex = /^0x[a-z0-9]+/;
971
1079
  // const binRegex = /0x[a-z0-9]+/;
972
1080
 
973
-
1081
+
974
1082
  const consider = {
975
- hex : true,
1083
+ hex: true,
976
1084
  // oct: false,
977
1085
  leadingZeros: true,
978
1086
  decimalPoint: "\.",
979
1087
  eNotation: true,
980
- //skipLike: /regex/
1088
+ //skipLike: /regex/,
1089
+ infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal))
981
1090
  };
982
1091
 
983
- function toNumber(str, options = {}){
984
- options = Object.assign({}, consider, options );
985
- if(!str || typeof str !== "string" ) return str;
986
-
987
- let trimmedStr = str.trim();
988
-
989
- if(options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str;
990
- else if(str==="0") return 0;
1092
+ function toNumber(str, options = {}) {
1093
+ options = Object.assign({}, consider, options);
1094
+ if (!str || typeof str !== "string") return str;
1095
+
1096
+ let trimmedStr = str.trim();
1097
+
1098
+ if (trimmedStr.length === 0) return str;
1099
+ else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str;
1100
+ else if (trimmedStr === "0") return 0;
991
1101
  else if (options.hex && hexRegex.test(trimmedStr)) {
992
1102
  return parse_int(trimmedStr, 16);
993
- // }else if (options.oct && octRegex.test(str)) {
994
- // return Number.parseInt(val, 8);
995
- }else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation
996
- return resolveEnotation(str,trimmedStr,options);
997
- // }else if (options.parseBin && binRegex.test(str)) {
998
- // return Number.parseInt(val, 2);
999
- }else {
1103
+ // }else if (options.oct && octRegex.test(str)) {
1104
+ // return Number.parseInt(val, 8);
1105
+ } else if (!isFinite(trimmedStr)) { //Infinity
1106
+ return handleInfinity(str, Number(trimmedStr), options);
1107
+ } else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation
1108
+ return resolveEnotation(str, trimmedStr, options);
1109
+ // }else if (options.parseBin && binRegex.test(str)) {
1110
+ // return Number.parseInt(val, 2);
1111
+ } else {
1000
1112
  //separate negative sign, leading zeros, and rest number
1001
1113
  const match = numRegex.exec(trimmedStr);
1002
1114
  // +00.123 => [ , '+', '00', '.123', ..
1003
- if(match){
1115
+ if (match) {
1004
1116
  const sign = match[1] || "";
1005
1117
  const leadingZeros = match[2];
1006
1118
  let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros
1007
1119
  const decimalAdjacentToLeadingZeros = sign ? // 0., -00., 000.
1008
- str[leadingZeros.length+1] === "."
1120
+ str[leadingZeros.length + 1] === "."
1009
1121
  : str[leadingZeros.length] === ".";
1010
1122
 
1011
1123
  //trim ending zeros for floating number
1012
- if(!options.leadingZeros //leading zeros are not allowed
1013
- && (leadingZeros.length > 1
1014
- || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))){
1124
+ if (!options.leadingZeros //leading zeros are not allowed
1125
+ && (leadingZeros.length > 1
1126
+ || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))) {
1015
1127
  // 00, 00.3, +03.24, 03, 03.24
1016
1128
  return str;
1017
1129
  }
@@ -1019,54 +1131,59 @@ function toNumber(str, options = {}){
1019
1131
  const num = Number(trimmedStr);
1020
1132
  const parsedStr = String(num);
1021
1133
 
1022
- if( num === 0) return num;
1023
- if(parsedStr.search(/[eE]/) !== -1){ //given number is long and parsed to eNotation
1024
- if(options.eNotation) return num;
1134
+ if (num === 0) return num;
1135
+ if (parsedStr.search(/[eE]/) !== -1) { //given number is long and parsed to eNotation
1136
+ if (options.eNotation) return num;
1025
1137
  else return str;
1026
- }else if(trimmedStr.indexOf(".") !== -1){ //floating number
1027
- if(parsedStr === "0") return num; //0.0
1028
- else if(parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000
1029
- else if( parsedStr === `${sign}${numTrimmedByZeros}`) return num;
1138
+ } else if (trimmedStr.indexOf(".") !== -1) { //floating number
1139
+ if (parsedStr === "0") return num; //0.0
1140
+ else if (parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000
1141
+ else if (parsedStr === `${sign}${numTrimmedByZeros}`) return num;
1030
1142
  else return str;
1031
1143
  }
1032
-
1033
- let n = leadingZeros? numTrimmedByZeros : trimmedStr;
1034
- if(leadingZeros){
1144
+
1145
+ let n = leadingZeros ? numTrimmedByZeros : trimmedStr;
1146
+ if (leadingZeros) {
1035
1147
  // -009 => -9
1036
- return (n === parsedStr) || (sign+n === parsedStr) ? num : str
1037
- }else {
1148
+ return (n === parsedStr) || (sign + n === parsedStr) ? num : str
1149
+ } else {
1038
1150
  // +9
1039
- return (n === parsedStr) || (n === sign+parsedStr) ? num : str
1151
+ return (n === parsedStr) || (n === sign + parsedStr) ? num : str
1040
1152
  }
1041
1153
  }
1042
- }else { //non-numeric string
1154
+ } else { //non-numeric string
1043
1155
  return str;
1044
1156
  }
1045
1157
  }
1046
1158
  }
1047
1159
 
1048
1160
  const eNotationRegx = /^([-+])?(0*)(\d*(\.\d*)?[eE][-\+]?\d+)$/;
1049
- function resolveEnotation(str,trimmedStr,options){
1050
- if(!options.eNotation) return str;
1051
- const notation = trimmedStr.match(eNotationRegx);
1052
- if(notation){
1161
+ function resolveEnotation(str, trimmedStr, options) {
1162
+ if (!options.eNotation) return str;
1163
+ const notation = trimmedStr.match(eNotationRegx);
1164
+ if (notation) {
1053
1165
  let sign = notation[1] || "";
1054
1166
  const eChar = notation[3].indexOf("e") === -1 ? "E" : "e";
1055
1167
  const leadingZeros = notation[2];
1056
1168
  const eAdjacentToLeadingZeros = sign ? // 0E.
1057
- str[leadingZeros.length+1] === eChar
1169
+ str[leadingZeros.length + 1] === eChar
1058
1170
  : str[leadingZeros.length] === eChar;
1059
1171
 
1060
- if(leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str;
1061
- else if(leadingZeros.length === 1
1062
- && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)){
1172
+ if (leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str;
1173
+ else if (leadingZeros.length === 1
1174
+ && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) {
1175
+ return Number(trimmedStr);
1176
+ } else if (leadingZeros.length > 0) {
1177
+ // Has leading zeros — only accept if leadingZeros option allows it
1178
+ if (options.leadingZeros && !eAdjacentToLeadingZeros) {
1179
+ trimmedStr = (notation[1] || "") + notation[3];
1063
1180
  return Number(trimmedStr);
1064
- }else if(options.leadingZeros && !eAdjacentToLeadingZeros){ //accept with leading zeros
1065
- //remove leading 0s
1066
- trimmedStr = (notation[1] || "") + notation[3];
1181
+ } else return str;
1182
+ } else {
1183
+ // No leading zeros always valid e-notation, parse it
1067
1184
  return Number(trimmedStr);
1068
- }else return str;
1069
- }else {
1185
+ }
1186
+ } else {
1070
1187
  return str;
1071
1188
  }
1072
1189
  }
@@ -1076,26 +1193,49 @@ function resolveEnotation(str,trimmedStr,options){
1076
1193
  * @param {string} numStr without leading zeros
1077
1194
  * @returns
1078
1195
  */
1079
- function trimZeros(numStr){
1080
- if(numStr && numStr.indexOf(".") !== -1){//float
1196
+ function trimZeros(numStr) {
1197
+ if (numStr && numStr.indexOf(".") !== -1) {//float
1081
1198
  numStr = numStr.replace(/0+$/, ""); //remove ending zeros
1082
- if(numStr === ".") numStr = "0";
1083
- else if(numStr[0] === ".") numStr = "0"+numStr;
1084
- else if(numStr[numStr.length-1] === ".") numStr = numStr.substring(0,numStr.length-1);
1199
+ if (numStr === ".") numStr = "0";
1200
+ else if (numStr[0] === ".") numStr = "0" + numStr;
1201
+ else if (numStr[numStr.length - 1] === ".") numStr = numStr.substring(0, numStr.length - 1);
1085
1202
  return numStr;
1086
1203
  }
1087
1204
  return numStr;
1088
1205
  }
1089
1206
 
1090
- function parse_int(numStr, base){
1207
+ function parse_int(numStr, base) {
1091
1208
  //polyfill
1092
- if(parseInt) return parseInt(numStr, base);
1093
- else if(Number.parseInt) return Number.parseInt(numStr, base);
1094
- else if(window && window.parseInt) return window.parseInt(numStr, base);
1209
+ if (parseInt) return parseInt(numStr, base);
1210
+ else if (Number.parseInt) return Number.parseInt(numStr, base);
1211
+ else if (window && window.parseInt) return window.parseInt(numStr, base);
1095
1212
  else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported")
1096
1213
  }
1097
1214
 
1098
- function getIgnoreAttributesFn(ignoreAttributes) {
1215
+ /**
1216
+ * Handle infinite values based on user option
1217
+ * @param {string} str - original input string
1218
+ * @param {number} num - parsed number (Infinity or -Infinity)
1219
+ * @param {object} options - user options
1220
+ * @returns {string|number|null} based on infinity option
1221
+ */
1222
+ function handleInfinity(str, num, options) {
1223
+ const isPositive = num === Infinity;
1224
+
1225
+ switch (options.infinity.toLowerCase()) {
1226
+ case "null":
1227
+ return null;
1228
+ case "infinity":
1229
+ return num; // Return Infinity or -Infinity
1230
+ case "string":
1231
+ return isPositive ? "Infinity" : "-Infinity";
1232
+ case "original":
1233
+ default:
1234
+ return str; // Return original string like "1e1000"
1235
+ }
1236
+ }
1237
+
1238
+ function getIgnoreAttributesFn$1(ignoreAttributes) {
1099
1239
  if (typeof ignoreAttributes === 'function') {
1100
1240
  return ignoreAttributes
1101
1241
  }
@@ -1114,6 +1254,1018 @@ function getIgnoreAttributesFn(ignoreAttributes) {
1114
1254
  return () => false
1115
1255
  }
1116
1256
 
1257
+ /**
1258
+ * Expression - Parses and stores a tag pattern expression
1259
+ *
1260
+ * Patterns are parsed once and stored in an optimized structure for fast matching.
1261
+ *
1262
+ * @example
1263
+ * const expr = new Expression("root.users.user");
1264
+ * const expr2 = new Expression("..user[id]:first");
1265
+ * const expr3 = new Expression("root/users/user", { separator: '/' });
1266
+ */
1267
+ class Expression {
1268
+ /**
1269
+ * Create a new Expression
1270
+ * @param {string} pattern - Pattern string (e.g., "root.users.user", "..user[id]")
1271
+ * @param {Object} options - Configuration options
1272
+ * @param {string} options.separator - Path separator (default: '.')
1273
+ */
1274
+ constructor(pattern, options = {}, data) {
1275
+ this.pattern = pattern;
1276
+ this.separator = options.separator || '.';
1277
+ this.segments = this._parse(pattern);
1278
+ this.data = data;
1279
+ // Cache expensive checks for performance (O(1) instead of O(n))
1280
+ this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard');
1281
+ this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined);
1282
+ this._hasPositionSelector = this.segments.some(seg => seg.position !== undefined);
1283
+ }
1284
+
1285
+ /**
1286
+ * Parse pattern string into segments
1287
+ * @private
1288
+ * @param {string} pattern - Pattern to parse
1289
+ * @returns {Array} Array of segment objects
1290
+ */
1291
+ _parse(pattern) {
1292
+ const segments = [];
1293
+
1294
+ // Split by separator but handle ".." specially
1295
+ let i = 0;
1296
+ let currentPart = '';
1297
+
1298
+ while (i < pattern.length) {
1299
+ if (pattern[i] === this.separator) {
1300
+ // Check if next char is also separator (deep wildcard)
1301
+ if (i + 1 < pattern.length && pattern[i + 1] === this.separator) {
1302
+ // Flush current part if any
1303
+ if (currentPart.trim()) {
1304
+ segments.push(this._parseSegment(currentPart.trim()));
1305
+ currentPart = '';
1306
+ }
1307
+ // Add deep wildcard
1308
+ segments.push({ type: 'deep-wildcard' });
1309
+ i += 2; // Skip both separators
1310
+ } else {
1311
+ // Regular separator
1312
+ if (currentPart.trim()) {
1313
+ segments.push(this._parseSegment(currentPart.trim()));
1314
+ }
1315
+ currentPart = '';
1316
+ i++;
1317
+ }
1318
+ } else {
1319
+ currentPart += pattern[i];
1320
+ i++;
1321
+ }
1322
+ }
1323
+
1324
+ // Flush remaining part
1325
+ if (currentPart.trim()) {
1326
+ segments.push(this._parseSegment(currentPart.trim()));
1327
+ }
1328
+
1329
+ return segments;
1330
+ }
1331
+
1332
+ /**
1333
+ * Parse a single segment
1334
+ * @private
1335
+ * @param {string} part - Segment string (e.g., "user", "ns::user", "user[id]", "ns::user:first")
1336
+ * @returns {Object} Segment object
1337
+ */
1338
+ _parseSegment(part) {
1339
+ const segment = { type: 'tag' };
1340
+
1341
+ // NEW NAMESPACE SYNTAX (v2.0):
1342
+ // ============================
1343
+ // Namespace uses DOUBLE colon (::)
1344
+ // Position uses SINGLE colon (:)
1345
+ //
1346
+ // Examples:
1347
+ // "user" → tag
1348
+ // "user:first" → tag + position
1349
+ // "user[id]" → tag + attribute
1350
+ // "user[id]:first" → tag + attribute + position
1351
+ // "ns::user" → namespace + tag
1352
+ // "ns::user:first" → namespace + tag + position
1353
+ // "ns::user[id]" → namespace + tag + attribute
1354
+ // "ns::user[id]:first" → namespace + tag + attribute + position
1355
+ // "ns::first" → namespace + tag named "first" (NO ambiguity!)
1356
+ //
1357
+ // This eliminates all ambiguity:
1358
+ // :: = namespace separator
1359
+ // : = position selector
1360
+ // [] = attributes
1361
+
1362
+ // Step 1: Extract brackets [attr] or [attr=value]
1363
+ let bracketContent = null;
1364
+ let withoutBrackets = part;
1365
+
1366
+ const bracketMatch = part.match(/^([^\[]+)(\[[^\]]*\])(.*)$/);
1367
+ if (bracketMatch) {
1368
+ withoutBrackets = bracketMatch[1] + bracketMatch[3];
1369
+ if (bracketMatch[2]) {
1370
+ const content = bracketMatch[2].slice(1, -1);
1371
+ if (content) {
1372
+ bracketContent = content;
1373
+ }
1374
+ }
1375
+ }
1376
+
1377
+ // Step 2: Check for namespace (double colon ::)
1378
+ let namespace = undefined;
1379
+ let tagAndPosition = withoutBrackets;
1380
+
1381
+ if (withoutBrackets.includes('::')) {
1382
+ const nsIndex = withoutBrackets.indexOf('::');
1383
+ namespace = withoutBrackets.substring(0, nsIndex).trim();
1384
+ tagAndPosition = withoutBrackets.substring(nsIndex + 2).trim(); // Skip ::
1385
+
1386
+ if (!namespace) {
1387
+ throw new Error(`Invalid namespace in pattern: ${part}`);
1388
+ }
1389
+ }
1390
+
1391
+ // Step 3: Parse tag and position (single colon :)
1392
+ let tag = undefined;
1393
+ let positionMatch = null;
1394
+
1395
+ if (tagAndPosition.includes(':')) {
1396
+ const colonIndex = tagAndPosition.lastIndexOf(':'); // Use last colon for position
1397
+ const tagPart = tagAndPosition.substring(0, colonIndex).trim();
1398
+ const posPart = tagAndPosition.substring(colonIndex + 1).trim();
1399
+
1400
+ // Verify position is a valid keyword
1401
+ const isPositionKeyword = ['first', 'last', 'odd', 'even'].includes(posPart) ||
1402
+ /^nth\(\d+\)$/.test(posPart);
1403
+
1404
+ if (isPositionKeyword) {
1405
+ tag = tagPart;
1406
+ positionMatch = posPart;
1407
+ } else {
1408
+ // Not a valid position keyword, treat whole thing as tag
1409
+ tag = tagAndPosition;
1410
+ }
1411
+ } else {
1412
+ tag = tagAndPosition;
1413
+ }
1414
+
1415
+ if (!tag) {
1416
+ throw new Error(`Invalid segment pattern: ${part}`);
1417
+ }
1418
+
1419
+ segment.tag = tag;
1420
+ if (namespace) {
1421
+ segment.namespace = namespace;
1422
+ }
1423
+
1424
+ // Step 4: Parse attributes
1425
+ if (bracketContent) {
1426
+ if (bracketContent.includes('=')) {
1427
+ const eqIndex = bracketContent.indexOf('=');
1428
+ segment.attrName = bracketContent.substring(0, eqIndex).trim();
1429
+ segment.attrValue = bracketContent.substring(eqIndex + 1).trim();
1430
+ } else {
1431
+ segment.attrName = bracketContent.trim();
1432
+ }
1433
+ }
1434
+
1435
+ // Step 5: Parse position selector
1436
+ if (positionMatch) {
1437
+ const nthMatch = positionMatch.match(/^nth\((\d+)\)$/);
1438
+ if (nthMatch) {
1439
+ segment.position = 'nth';
1440
+ segment.positionValue = parseInt(nthMatch[1], 10);
1441
+ } else {
1442
+ segment.position = positionMatch;
1443
+ }
1444
+ }
1445
+
1446
+ return segment;
1447
+ }
1448
+
1449
+ /**
1450
+ * Get the number of segments
1451
+ * @returns {number}
1452
+ */
1453
+ get length() {
1454
+ return this.segments.length;
1455
+ }
1456
+
1457
+ /**
1458
+ * Check if expression contains deep wildcard
1459
+ * @returns {boolean}
1460
+ */
1461
+ hasDeepWildcard() {
1462
+ return this._hasDeepWildcard;
1463
+ }
1464
+
1465
+ /**
1466
+ * Check if expression has attribute conditions
1467
+ * @returns {boolean}
1468
+ */
1469
+ hasAttributeCondition() {
1470
+ return this._hasAttributeCondition;
1471
+ }
1472
+
1473
+ /**
1474
+ * Check if expression has position selectors
1475
+ * @returns {boolean}
1476
+ */
1477
+ hasPositionSelector() {
1478
+ return this._hasPositionSelector;
1479
+ }
1480
+
1481
+ /**
1482
+ * Get string representation
1483
+ * @returns {string}
1484
+ */
1485
+ toString() {
1486
+ return this.pattern;
1487
+ }
1488
+ }
1489
+
1490
+ /**
1491
+ * ExpressionSet - An indexed collection of Expressions for efficient bulk matching
1492
+ *
1493
+ * Instead of iterating all expressions on every tag, ExpressionSet pre-indexes
1494
+ * them at insertion time by depth and terminal tag name. At match time, only
1495
+ * the relevant bucket is evaluated — typically reducing checks from O(E) to O(1)
1496
+ * lookup plus O(small bucket) matches.
1497
+ *
1498
+ * Three buckets are maintained:
1499
+ * - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first)
1500
+ * - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only)
1501
+ * - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed)
1502
+ *
1503
+ * @example
1504
+ * import { Expression, ExpressionSet } from 'fast-xml-tagger';
1505
+ *
1506
+ * // Build once at config time
1507
+ * const stopNodes = new ExpressionSet();
1508
+ * stopNodes.add(new Expression('root.users.user'));
1509
+ * stopNodes.add(new Expression('root.config.setting'));
1510
+ * stopNodes.add(new Expression('..script'));
1511
+ *
1512
+ * // Query on every tag — hot path
1513
+ * if (stopNodes.matchesAny(matcher)) { ... }
1514
+ */
1515
+ class ExpressionSet {
1516
+ constructor() {
1517
+ /** @type {Map<string, import('./Expression.js').default[]>} depth:tag → expressions */
1518
+ this._byDepthAndTag = new Map();
1519
+
1520
+ /** @type {Map<number, import('./Expression.js').default[]>} depth → wildcard-tag expressions */
1521
+ this._wildcardByDepth = new Map();
1522
+
1523
+ /** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */
1524
+ this._deepWildcards = [];
1525
+
1526
+ /** @type {Set<string>} pattern strings already added — used for deduplication */
1527
+ this._patterns = new Set();
1528
+
1529
+ /** @type {boolean} whether the set is sealed against further additions */
1530
+ this._sealed = false;
1531
+ }
1532
+
1533
+ /**
1534
+ * Add an Expression to the set.
1535
+ * Duplicate patterns (same pattern string) are silently ignored.
1536
+ *
1537
+ * @param {import('./Expression.js').default} expression - A pre-constructed Expression instance
1538
+ * @returns {this} for chaining
1539
+ * @throws {TypeError} if called after seal()
1540
+ *
1541
+ * @example
1542
+ * set.add(new Expression('root.users.user'));
1543
+ * set.add(new Expression('..script'));
1544
+ */
1545
+ add(expression) {
1546
+ if (this._sealed) {
1547
+ throw new TypeError(
1548
+ 'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.'
1549
+ );
1550
+ }
1551
+
1552
+ // Deduplicate by pattern string
1553
+ if (this._patterns.has(expression.pattern)) return this;
1554
+ this._patterns.add(expression.pattern);
1555
+
1556
+ if (expression.hasDeepWildcard()) {
1557
+ this._deepWildcards.push(expression);
1558
+ return this;
1559
+ }
1560
+
1561
+ const depth = expression.length;
1562
+ const lastSeg = expression.segments[expression.segments.length - 1];
1563
+ const tag = lastSeg?.tag;
1564
+
1565
+ if (!tag || tag === '*') {
1566
+ // Can index by depth but not by tag
1567
+ if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []);
1568
+ this._wildcardByDepth.get(depth).push(expression);
1569
+ } else {
1570
+ // Tightest bucket: depth + tag
1571
+ const key = `${depth}:${tag}`;
1572
+ if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []);
1573
+ this._byDepthAndTag.get(key).push(expression);
1574
+ }
1575
+
1576
+ return this;
1577
+ }
1578
+
1579
+ /**
1580
+ * Add multiple expressions at once.
1581
+ *
1582
+ * @param {import('./Expression.js').default[]} expressions - Array of Expression instances
1583
+ * @returns {this} for chaining
1584
+ *
1585
+ * @example
1586
+ * set.addAll([
1587
+ * new Expression('root.users.user'),
1588
+ * new Expression('root.config.setting'),
1589
+ * ]);
1590
+ */
1591
+ addAll(expressions) {
1592
+ for (const expr of expressions) this.add(expr);
1593
+ return this;
1594
+ }
1595
+
1596
+ /**
1597
+ * Check whether a pattern string is already present in the set.
1598
+ *
1599
+ * @param {import('./Expression.js').default} expression
1600
+ * @returns {boolean}
1601
+ */
1602
+ has(expression) {
1603
+ return this._patterns.has(expression.pattern);
1604
+ }
1605
+
1606
+ /**
1607
+ * Number of expressions in the set.
1608
+ * @type {number}
1609
+ */
1610
+ get size() {
1611
+ return this._patterns.size;
1612
+ }
1613
+
1614
+ /**
1615
+ * Seal the set against further modifications.
1616
+ * Useful to prevent accidental mutations after config is built.
1617
+ * Calling add() or addAll() on a sealed set throws a TypeError.
1618
+ *
1619
+ * @returns {this}
1620
+ */
1621
+ seal() {
1622
+ this._sealed = true;
1623
+ return this;
1624
+ }
1625
+
1626
+ /**
1627
+ * Whether the set has been sealed.
1628
+ * @type {boolean}
1629
+ */
1630
+ get isSealed() {
1631
+ return this._sealed;
1632
+ }
1633
+
1634
+ /**
1635
+ * Test whether the matcher's current path matches any expression in the set.
1636
+ *
1637
+ * Evaluation order (cheapest → most expensive):
1638
+ * 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions
1639
+ * 2. Depth-only wildcard bucket — O(1) lookup, rare
1640
+ * 3. Deep-wildcard list — always checked, but usually small
1641
+ *
1642
+ * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
1643
+ * @returns {boolean} true if any expression matches the current path
1644
+ *
1645
+ * @example
1646
+ * if (stopNodes.matchesAny(matcher)) {
1647
+ * // handle stop node
1648
+ * }
1649
+ */
1650
+ matchesAny(matcher) {
1651
+ return this.findMatch(matcher) !== null;
1652
+ }
1653
+ /**
1654
+ * Find and return the first Expression that matches the matcher's current path.
1655
+ *
1656
+ * Uses the same evaluation order as matchesAny (cheapest → most expensive):
1657
+ * 1. Exact depth + tag bucket
1658
+ * 2. Depth-only wildcard bucket
1659
+ * 3. Deep-wildcard list
1660
+ *
1661
+ * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
1662
+ * @returns {import('./Expression.js').default | null} the first matching Expression, or null
1663
+ *
1664
+ * @example
1665
+ * const expr = stopNodes.findMatch(matcher);
1666
+ * if (expr) {
1667
+ * // access expr.config, expr.pattern, etc.
1668
+ * }
1669
+ */
1670
+ findMatch(matcher) {
1671
+ const depth = matcher.getDepth();
1672
+ const tag = matcher.getCurrentTag();
1673
+
1674
+ // 1. Tightest bucket — most expressions live here
1675
+ const exactKey = `${depth}:${tag}`;
1676
+ const exactBucket = this._byDepthAndTag.get(exactKey);
1677
+ if (exactBucket) {
1678
+ for (let i = 0; i < exactBucket.length; i++) {
1679
+ if (matcher.matches(exactBucket[i])) return exactBucket[i];
1680
+ }
1681
+ }
1682
+
1683
+ // 2. Depth-matched wildcard-tag expressions
1684
+ const wildcardBucket = this._wildcardByDepth.get(depth);
1685
+ if (wildcardBucket) {
1686
+ for (let i = 0; i < wildcardBucket.length; i++) {
1687
+ if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i];
1688
+ }
1689
+ }
1690
+
1691
+ // 3. Deep wildcards — cannot be pre-filtered by depth or tag
1692
+ for (let i = 0; i < this._deepWildcards.length; i++) {
1693
+ if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i];
1694
+ }
1695
+
1696
+ return null;
1697
+ }
1698
+ }
1699
+
1700
+ /**
1701
+ * MatcherView - A lightweight read-only view over a Matcher's internal state.
1702
+ *
1703
+ * Created once by Matcher and reused across all callbacks. Holds a direct
1704
+ * reference to the parent Matcher so it always reflects current parser state
1705
+ * with zero copying or freezing overhead.
1706
+ *
1707
+ * Users receive this via {@link Matcher#readOnly} or directly from parser
1708
+ * callbacks. It exposes all query and matching methods but has no mutation
1709
+ * methods — misuse is caught at the TypeScript level rather than at runtime.
1710
+ *
1711
+ * @example
1712
+ * const matcher = new Matcher();
1713
+ * const view = matcher.readOnly();
1714
+ *
1715
+ * matcher.push("root", {});
1716
+ * view.getCurrentTag(); // "root"
1717
+ * view.getDepth(); // 1
1718
+ */
1719
+ class MatcherView {
1720
+ /**
1721
+ * @param {Matcher} matcher - The parent Matcher instance to read from.
1722
+ */
1723
+ constructor(matcher) {
1724
+ this._matcher = matcher;
1725
+ }
1726
+
1727
+ /**
1728
+ * Get the path separator used by the parent matcher.
1729
+ * @returns {string}
1730
+ */
1731
+ get separator() {
1732
+ return this._matcher.separator;
1733
+ }
1734
+
1735
+ /**
1736
+ * Get current tag name.
1737
+ * @returns {string|undefined}
1738
+ */
1739
+ getCurrentTag() {
1740
+ const path = this._matcher.path;
1741
+ return path.length > 0 ? path[path.length - 1].tag : undefined;
1742
+ }
1743
+
1744
+ /**
1745
+ * Get current namespace.
1746
+ * @returns {string|undefined}
1747
+ */
1748
+ getCurrentNamespace() {
1749
+ const path = this._matcher.path;
1750
+ return path.length > 0 ? path[path.length - 1].namespace : undefined;
1751
+ }
1752
+
1753
+ /**
1754
+ * Get current node's attribute value.
1755
+ * @param {string} attrName
1756
+ * @returns {*}
1757
+ */
1758
+ getAttrValue(attrName) {
1759
+ const path = this._matcher.path;
1760
+ if (path.length === 0) return undefined;
1761
+ return path[path.length - 1].values?.[attrName];
1762
+ }
1763
+
1764
+ /**
1765
+ * Check if current node has an attribute.
1766
+ * @param {string} attrName
1767
+ * @returns {boolean}
1768
+ */
1769
+ hasAttr(attrName) {
1770
+ const path = this._matcher.path;
1771
+ if (path.length === 0) return false;
1772
+ const current = path[path.length - 1];
1773
+ return current.values !== undefined && attrName in current.values;
1774
+ }
1775
+
1776
+ /**
1777
+ * Get current node's sibling position (child index in parent).
1778
+ * @returns {number}
1779
+ */
1780
+ getPosition() {
1781
+ const path = this._matcher.path;
1782
+ if (path.length === 0) return -1;
1783
+ return path[path.length - 1].position ?? 0;
1784
+ }
1785
+
1786
+ /**
1787
+ * Get current node's repeat counter (occurrence count of this tag name).
1788
+ * @returns {number}
1789
+ */
1790
+ getCounter() {
1791
+ const path = this._matcher.path;
1792
+ if (path.length === 0) return -1;
1793
+ return path[path.length - 1].counter ?? 0;
1794
+ }
1795
+
1796
+ /**
1797
+ * Get current node's sibling index (alias for getPosition).
1798
+ * @returns {number}
1799
+ * @deprecated Use getPosition() or getCounter() instead
1800
+ */
1801
+ getIndex() {
1802
+ return this.getPosition();
1803
+ }
1804
+
1805
+ /**
1806
+ * Get current path depth.
1807
+ * @returns {number}
1808
+ */
1809
+ getDepth() {
1810
+ return this._matcher.path.length;
1811
+ }
1812
+
1813
+ /**
1814
+ * Get path as string.
1815
+ * @param {string} [separator] - Optional separator (uses default if not provided)
1816
+ * @param {boolean} [includeNamespace=true]
1817
+ * @returns {string}
1818
+ */
1819
+ toString(separator, includeNamespace = true) {
1820
+ return this._matcher.toString(separator, includeNamespace);
1821
+ }
1822
+
1823
+ /**
1824
+ * Get path as array of tag names.
1825
+ * @returns {string[]}
1826
+ */
1827
+ toArray() {
1828
+ return this._matcher.path.map(n => n.tag);
1829
+ }
1830
+
1831
+ /**
1832
+ * Match current path against an Expression.
1833
+ * @param {Expression} expression
1834
+ * @returns {boolean}
1835
+ */
1836
+ matches(expression) {
1837
+ return this._matcher.matches(expression);
1838
+ }
1839
+
1840
+ /**
1841
+ * Match any expression in the given set against the current path.
1842
+ * @param {ExpressionSet} exprSet
1843
+ * @returns {boolean}
1844
+ */
1845
+ matchesAny(exprSet) {
1846
+ return exprSet.matchesAny(this._matcher);
1847
+ }
1848
+ }
1849
+
1850
+ /**
1851
+ * Matcher - Tracks current path in XML/JSON tree and matches against Expressions.
1852
+ *
1853
+ * The matcher maintains a stack of nodes representing the current path from root to
1854
+ * current tag. It only stores attribute values for the current (top) node to minimize
1855
+ * memory usage. Sibling tracking is used to auto-calculate position and counter.
1856
+ *
1857
+ * Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to
1858
+ * user callbacks — it always reflects current state with no Proxy overhead.
1859
+ *
1860
+ * @example
1861
+ * const matcher = new Matcher();
1862
+ * matcher.push("root", {});
1863
+ * matcher.push("users", {});
1864
+ * matcher.push("user", { id: "123", type: "admin" });
1865
+ *
1866
+ * const expr = new Expression("root.users.user");
1867
+ * matcher.matches(expr); // true
1868
+ */
1869
+ class Matcher {
1870
+ /**
1871
+ * Create a new Matcher.
1872
+ * @param {Object} [options={}]
1873
+ * @param {string} [options.separator='.'] - Default path separator
1874
+ */
1875
+ constructor(options = {}) {
1876
+ this.separator = options.separator || '.';
1877
+ this.path = [];
1878
+ this.siblingStacks = [];
1879
+ // Each path node: { tag, values, position, counter, namespace? }
1880
+ // values only present for current (last) node
1881
+ // Each siblingStacks entry: Map<tagName, count> tracking occurrences at each level
1882
+ this._pathStringCache = null;
1883
+ this._view = new MatcherView(this);
1884
+ }
1885
+
1886
+ /**
1887
+ * Push a new tag onto the path.
1888
+ * @param {string} tagName
1889
+ * @param {Object|null} [attrValues=null]
1890
+ * @param {string|null} [namespace=null]
1891
+ */
1892
+ push(tagName, attrValues = null, namespace = null) {
1893
+ this._pathStringCache = null;
1894
+
1895
+ // Remove values from previous current node (now becoming ancestor)
1896
+ if (this.path.length > 0) {
1897
+ this.path[this.path.length - 1].values = undefined;
1898
+ }
1899
+
1900
+ // Get or create sibling tracking for current level
1901
+ const currentLevel = this.path.length;
1902
+ if (!this.siblingStacks[currentLevel]) {
1903
+ this.siblingStacks[currentLevel] = new Map();
1904
+ }
1905
+
1906
+ const siblings = this.siblingStacks[currentLevel];
1907
+
1908
+ // Create a unique key for sibling tracking that includes namespace
1909
+ const siblingKey = namespace ? `${namespace}:${tagName}` : tagName;
1910
+
1911
+ // Calculate counter (how many times this tag appeared at this level)
1912
+ const counter = siblings.get(siblingKey) || 0;
1913
+
1914
+ // Calculate position (total children at this level so far)
1915
+ let position = 0;
1916
+ for (const count of siblings.values()) {
1917
+ position += count;
1918
+ }
1919
+
1920
+ // Update sibling count for this tag
1921
+ siblings.set(siblingKey, counter + 1);
1922
+
1923
+ // Create new node
1924
+ const node = {
1925
+ tag: tagName,
1926
+ position: position,
1927
+ counter: counter
1928
+ };
1929
+
1930
+ if (namespace !== null && namespace !== undefined) {
1931
+ node.namespace = namespace;
1932
+ }
1933
+
1934
+ if (attrValues !== null && attrValues !== undefined) {
1935
+ node.values = attrValues;
1936
+ }
1937
+
1938
+ this.path.push(node);
1939
+ }
1940
+
1941
+ /**
1942
+ * Pop the last tag from the path.
1943
+ * @returns {Object|undefined} The popped node
1944
+ */
1945
+ pop() {
1946
+ if (this.path.length === 0) return undefined;
1947
+ this._pathStringCache = null;
1948
+
1949
+ const node = this.path.pop();
1950
+
1951
+ if (this.siblingStacks.length > this.path.length + 1) {
1952
+ this.siblingStacks.length = this.path.length + 1;
1953
+ }
1954
+
1955
+ return node;
1956
+ }
1957
+
1958
+ /**
1959
+ * Update current node's attribute values.
1960
+ * Useful when attributes are parsed after push.
1961
+ * @param {Object} attrValues
1962
+ */
1963
+ updateCurrent(attrValues) {
1964
+ if (this.path.length > 0) {
1965
+ const current = this.path[this.path.length - 1];
1966
+ if (attrValues !== null && attrValues !== undefined) {
1967
+ current.values = attrValues;
1968
+ }
1969
+ }
1970
+ }
1971
+
1972
+ /**
1973
+ * Get current tag name.
1974
+ * @returns {string|undefined}
1975
+ */
1976
+ getCurrentTag() {
1977
+ return this.path.length > 0 ? this.path[this.path.length - 1].tag : undefined;
1978
+ }
1979
+
1980
+ /**
1981
+ * Get current namespace.
1982
+ * @returns {string|undefined}
1983
+ */
1984
+ getCurrentNamespace() {
1985
+ return this.path.length > 0 ? this.path[this.path.length - 1].namespace : undefined;
1986
+ }
1987
+
1988
+ /**
1989
+ * Get current node's attribute value.
1990
+ * @param {string} attrName
1991
+ * @returns {*}
1992
+ */
1993
+ getAttrValue(attrName) {
1994
+ if (this.path.length === 0) return undefined;
1995
+ return this.path[this.path.length - 1].values?.[attrName];
1996
+ }
1997
+
1998
+ /**
1999
+ * Check if current node has an attribute.
2000
+ * @param {string} attrName
2001
+ * @returns {boolean}
2002
+ */
2003
+ hasAttr(attrName) {
2004
+ if (this.path.length === 0) return false;
2005
+ const current = this.path[this.path.length - 1];
2006
+ return current.values !== undefined && attrName in current.values;
2007
+ }
2008
+
2009
+ /**
2010
+ * Get current node's sibling position (child index in parent).
2011
+ * @returns {number}
2012
+ */
2013
+ getPosition() {
2014
+ if (this.path.length === 0) return -1;
2015
+ return this.path[this.path.length - 1].position ?? 0;
2016
+ }
2017
+
2018
+ /**
2019
+ * Get current node's repeat counter (occurrence count of this tag name).
2020
+ * @returns {number}
2021
+ */
2022
+ getCounter() {
2023
+ if (this.path.length === 0) return -1;
2024
+ return this.path[this.path.length - 1].counter ?? 0;
2025
+ }
2026
+
2027
+ /**
2028
+ * Get current node's sibling index (alias for getPosition).
2029
+ * @returns {number}
2030
+ * @deprecated Use getPosition() or getCounter() instead
2031
+ */
2032
+ getIndex() {
2033
+ return this.getPosition();
2034
+ }
2035
+
2036
+ /**
2037
+ * Get current path depth.
2038
+ * @returns {number}
2039
+ */
2040
+ getDepth() {
2041
+ return this.path.length;
2042
+ }
2043
+
2044
+ /**
2045
+ * Get path as string.
2046
+ * @param {string} [separator] - Optional separator (uses default if not provided)
2047
+ * @param {boolean} [includeNamespace=true]
2048
+ * @returns {string}
2049
+ */
2050
+ toString(separator, includeNamespace = true) {
2051
+ const sep = separator || this.separator;
2052
+ const isDefault = (sep === this.separator && includeNamespace === true);
2053
+
2054
+ if (isDefault) {
2055
+ if (this._pathStringCache !== null) {
2056
+ return this._pathStringCache;
2057
+ }
2058
+ const result = this.path.map(n =>
2059
+ (n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
2060
+ ).join(sep);
2061
+ this._pathStringCache = result;
2062
+ return result;
2063
+ }
2064
+
2065
+ return this.path.map(n =>
2066
+ (includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
2067
+ ).join(sep);
2068
+ }
2069
+
2070
+ /**
2071
+ * Get path as array of tag names.
2072
+ * @returns {string[]}
2073
+ */
2074
+ toArray() {
2075
+ return this.path.map(n => n.tag);
2076
+ }
2077
+
2078
+ /**
2079
+ * Reset the path to empty.
2080
+ */
2081
+ reset() {
2082
+ this._pathStringCache = null;
2083
+ this.path = [];
2084
+ this.siblingStacks = [];
2085
+ }
2086
+
2087
+ /**
2088
+ * Match current path against an Expression.
2089
+ * @param {Expression} expression
2090
+ * @returns {boolean}
2091
+ */
2092
+ matches(expression) {
2093
+ const segments = expression.segments;
2094
+
2095
+ if (segments.length === 0) {
2096
+ return false;
2097
+ }
2098
+
2099
+ if (expression.hasDeepWildcard()) {
2100
+ return this._matchWithDeepWildcard(segments);
2101
+ }
2102
+
2103
+ return this._matchSimple(segments);
2104
+ }
2105
+
2106
+ /**
2107
+ * @private
2108
+ */
2109
+ _matchSimple(segments) {
2110
+ if (this.path.length !== segments.length) {
2111
+ return false;
2112
+ }
2113
+
2114
+ for (let i = 0; i < segments.length; i++) {
2115
+ if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) {
2116
+ return false;
2117
+ }
2118
+ }
2119
+
2120
+ return true;
2121
+ }
2122
+
2123
+ /**
2124
+ * @private
2125
+ */
2126
+ _matchWithDeepWildcard(segments) {
2127
+ let pathIdx = this.path.length - 1;
2128
+ let segIdx = segments.length - 1;
2129
+
2130
+ while (segIdx >= 0 && pathIdx >= 0) {
2131
+ const segment = segments[segIdx];
2132
+
2133
+ if (segment.type === 'deep-wildcard') {
2134
+ segIdx--;
2135
+
2136
+ if (segIdx < 0) {
2137
+ return true;
2138
+ }
2139
+
2140
+ const nextSeg = segments[segIdx];
2141
+ let found = false;
2142
+
2143
+ for (let i = pathIdx; i >= 0; i--) {
2144
+ if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) {
2145
+ pathIdx = i - 1;
2146
+ segIdx--;
2147
+ found = true;
2148
+ break;
2149
+ }
2150
+ }
2151
+
2152
+ if (!found) {
2153
+ return false;
2154
+ }
2155
+ } else {
2156
+ if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) {
2157
+ return false;
2158
+ }
2159
+ pathIdx--;
2160
+ segIdx--;
2161
+ }
2162
+ }
2163
+
2164
+ return segIdx < 0;
2165
+ }
2166
+
2167
+ /**
2168
+ * @private
2169
+ */
2170
+ _matchSegment(segment, node, isCurrentNode) {
2171
+ if (segment.tag !== '*' && segment.tag !== node.tag) {
2172
+ return false;
2173
+ }
2174
+
2175
+ if (segment.namespace !== undefined) {
2176
+ if (segment.namespace !== '*' && segment.namespace !== node.namespace) {
2177
+ return false;
2178
+ }
2179
+ }
2180
+
2181
+ if (segment.attrName !== undefined) {
2182
+ if (!isCurrentNode) {
2183
+ return false;
2184
+ }
2185
+
2186
+ if (!node.values || !(segment.attrName in node.values)) {
2187
+ return false;
2188
+ }
2189
+
2190
+ if (segment.attrValue !== undefined) {
2191
+ if (String(node.values[segment.attrName]) !== String(segment.attrValue)) {
2192
+ return false;
2193
+ }
2194
+ }
2195
+ }
2196
+
2197
+ if (segment.position !== undefined) {
2198
+ if (!isCurrentNode) {
2199
+ return false;
2200
+ }
2201
+
2202
+ const counter = node.counter ?? 0;
2203
+
2204
+ if (segment.position === 'first' && counter !== 0) {
2205
+ return false;
2206
+ } else if (segment.position === 'odd' && counter % 2 !== 1) {
2207
+ return false;
2208
+ } else if (segment.position === 'even' && counter % 2 !== 0) {
2209
+ return false;
2210
+ } else if (segment.position === 'nth' && counter !== segment.positionValue) {
2211
+ return false;
2212
+ }
2213
+ }
2214
+
2215
+ return true;
2216
+ }
2217
+
2218
+ /**
2219
+ * Match any expression in the given set against the current path.
2220
+ * @param {ExpressionSet} exprSet
2221
+ * @returns {boolean}
2222
+ */
2223
+ matchesAny(exprSet) {
2224
+ return exprSet.matchesAny(this);
2225
+ }
2226
+
2227
+ /**
2228
+ * Create a snapshot of current state.
2229
+ * @returns {Object}
2230
+ */
2231
+ snapshot() {
2232
+ return {
2233
+ path: this.path.map(node => ({ ...node })),
2234
+ siblingStacks: this.siblingStacks.map(map => new Map(map))
2235
+ };
2236
+ }
2237
+
2238
+ /**
2239
+ * Restore state from snapshot.
2240
+ * @param {Object} snapshot
2241
+ */
2242
+ restore(snapshot) {
2243
+ this._pathStringCache = null;
2244
+ this.path = snapshot.path.map(node => ({ ...node }));
2245
+ this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map));
2246
+ }
2247
+
2248
+ /**
2249
+ * Return the read-only {@link MatcherView} for this matcher.
2250
+ *
2251
+ * The same instance is returned on every call — no allocation occurs.
2252
+ * It always reflects the current parser state and is safe to pass to
2253
+ * user callbacks without risk of accidental mutation.
2254
+ *
2255
+ * @returns {MatcherView}
2256
+ *
2257
+ * @example
2258
+ * const view = matcher.readOnly();
2259
+ * // pass view to callbacks — it stays in sync automatically
2260
+ * view.matches(expr); // ✓
2261
+ * view.getCurrentTag(); // ✓
2262
+ * // view.push(...) // ✗ method does not exist — caught by TypeScript
2263
+ */
2264
+ readOnly() {
2265
+ return this._view;
2266
+ }
2267
+ }
2268
+
1117
2269
  // const regx =
1118
2270
  // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
1119
2271
  // .replace(/NAME/g, util.nameRegexp);
@@ -1121,6 +2273,57 @@ function getIgnoreAttributesFn(ignoreAttributes) {
1121
2273
  //const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
1122
2274
  //const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
1123
2275
 
2276
+ // Helper functions for attribute and namespace handling
2277
+
2278
+ /**
2279
+ * Extract raw attributes (without prefix) from prefixed attribute map
2280
+ * @param {object} prefixedAttrs - Attributes with prefix from buildAttributesMap
2281
+ * @param {object} options - Parser options containing attributeNamePrefix
2282
+ * @returns {object} Raw attributes for matcher
2283
+ */
2284
+ function extractRawAttributes(prefixedAttrs, options) {
2285
+ if (!prefixedAttrs) return {};
2286
+
2287
+ // Handle attributesGroupName option
2288
+ const attrs = options.attributesGroupName
2289
+ ? prefixedAttrs[options.attributesGroupName]
2290
+ : prefixedAttrs;
2291
+
2292
+ if (!attrs) return {};
2293
+
2294
+ const rawAttrs = {};
2295
+ for (const key in attrs) {
2296
+ // Remove the attribute prefix to get raw name
2297
+ if (key.startsWith(options.attributeNamePrefix)) {
2298
+ const rawName = key.substring(options.attributeNamePrefix.length);
2299
+ rawAttrs[rawName] = attrs[key];
2300
+ } else {
2301
+ // Attribute without prefix (shouldn't normally happen, but be safe)
2302
+ rawAttrs[key] = attrs[key];
2303
+ }
2304
+ }
2305
+ return rawAttrs;
2306
+ }
2307
+
2308
+ /**
2309
+ * Extract namespace from raw tag name
2310
+ * @param {string} rawTagName - Tag name possibly with namespace (e.g., "soap:Envelope")
2311
+ * @returns {string|undefined} Namespace or undefined
2312
+ */
2313
+ function extractNamespace(rawTagName) {
2314
+ if (!rawTagName || typeof rawTagName !== 'string') return undefined;
2315
+
2316
+ const colonIndex = rawTagName.indexOf(':');
2317
+ if (colonIndex !== -1 && colonIndex > 0) {
2318
+ const ns = rawTagName.substring(0, colonIndex);
2319
+ // Don't treat xmlns as a namespace
2320
+ if (ns !== 'xmlns') {
2321
+ return ns;
2322
+ }
2323
+ }
2324
+ return undefined;
2325
+ }
2326
+
1124
2327
  class OrderedObjParser {
1125
2328
  constructor(options) {
1126
2329
  this.options = options;
@@ -1161,22 +2364,35 @@ class OrderedObjParser {
1161
2364
  this.readStopNodeData = readStopNodeData;
1162
2365
  this.saveTextToParentTag = saveTextToParentTag;
1163
2366
  this.addChild = addChild;
1164
- this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes);
2367
+ this.ignoreAttributesFn = getIgnoreAttributesFn$1(this.options.ignoreAttributes);
1165
2368
  this.entityExpansionCount = 0;
1166
2369
  this.currentExpandedLength = 0;
1167
2370
 
1168
- if (this.options.stopNodes && this.options.stopNodes.length > 0) {
1169
- this.stopNodesExact = new Set();
1170
- this.stopNodesWildcard = new Set();
1171
- for (let i = 0; i < this.options.stopNodes.length; i++) {
1172
- const stopNodeExp = this.options.stopNodes[i];
1173
- if (typeof stopNodeExp !== 'string') continue;
1174
- if (stopNodeExp.startsWith("*.")) {
1175
- this.stopNodesWildcard.add(stopNodeExp.substring(2));
1176
- } else {
1177
- this.stopNodesExact.add(stopNodeExp);
2371
+ // Initialize path matcher for path-expression-matcher
2372
+ this.matcher = new Matcher();
2373
+
2374
+ // Live read-only proxy of matcher PEM creates and caches this internally.
2375
+ // All user callbacks receive this instead of the mutable matcher.
2376
+ this.readonlyMatcher = this.matcher.readOnly();
2377
+
2378
+ // Flag to track if current node is a stop node (optimization)
2379
+ this.isCurrentNodeStopNode = false;
2380
+
2381
+ // Pre-compile stopNodes expressions
2382
+ this.stopNodeExpressionsSet = new ExpressionSet();
2383
+ const stopNodesOpts = this.options.stopNodes;
2384
+ if (stopNodesOpts && stopNodesOpts.length > 0) {
2385
+ for (let i = 0; i < stopNodesOpts.length; i++) {
2386
+ const stopNodeExp = stopNodesOpts[i];
2387
+ if (typeof stopNodeExp === 'string') {
2388
+ // Convert string to Expression object
2389
+ this.stopNodeExpressionsSet.add(new Expression(stopNodeExp));
2390
+ } else if (stopNodeExp instanceof Expression) {
2391
+ // Already an Expression object
2392
+ this.stopNodeExpressionsSet.add(stopNodeExp);
1178
2393
  }
1179
2394
  }
2395
+ this.stopNodeExpressionsSet.seal();
1180
2396
  }
1181
2397
  }
1182
2398
 
@@ -1197,33 +2413,36 @@ function addExternalEntities(externalEntities) {
1197
2413
  /**
1198
2414
  * @param {string} val
1199
2415
  * @param {string} tagName
1200
- * @param {string} jPath
2416
+ * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
1201
2417
  * @param {boolean} dontTrim
1202
2418
  * @param {boolean} hasAttributes
1203
2419
  * @param {boolean} isLeafNode
1204
2420
  * @param {boolean} escapeEntities
1205
2421
  */
1206
2422
  function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) {
2423
+ const options = this.options;
1207
2424
  if (val !== undefined) {
1208
- if (this.options.trimValues && !dontTrim) {
2425
+ if (options.trimValues && !dontTrim) {
1209
2426
  val = val.trim();
1210
2427
  }
1211
2428
  if (val.length > 0) {
1212
2429
  if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
1213
2430
 
1214
- const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
2431
+ // Pass jPath string or matcher based on options.jPath setting
2432
+ const jPathOrMatcher = options.jPath ? jPath.toString() : jPath;
2433
+ const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode);
1215
2434
  if (newval === null || newval === undefined) {
1216
2435
  //don't parse
1217
2436
  return val;
1218
2437
  } else if (typeof newval !== typeof val || newval !== val) {
1219
2438
  //overwrite
1220
2439
  return newval;
1221
- } else if (this.options.trimValues) {
1222
- return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
2440
+ } else if (options.trimValues) {
2441
+ return parseValue(val, options.parseTagValue, options.numberParseOptions);
1223
2442
  } else {
1224
2443
  const trimmedVal = val.trim();
1225
2444
  if (trimmedVal === val) {
1226
- return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions);
2445
+ return parseValue(val, options.parseTagValue, options.numberParseOptions);
1227
2446
  } else {
1228
2447
  return val;
1229
2448
  }
@@ -1251,216 +2470,299 @@ function resolveNameSpace(tagname) {
1251
2470
  const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm');
1252
2471
 
1253
2472
  function buildAttributesMap(attrStr, jPath, tagName) {
1254
- if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') {
2473
+ const options = this.options;
2474
+ if (options.ignoreAttributes !== true && typeof attrStr === 'string') {
1255
2475
  // attrStr = attrStr.replace(/\r?\n/g, ' ');
1256
2476
  //attrStr = attrStr || attrStr.trim();
1257
2477
 
1258
2478
  const matches = getAllMatches(attrStr, attrsRegx);
1259
2479
  const len = matches.length; //don't make it inline
1260
2480
  const attrs = {};
2481
+
2482
+ // Pre-process values once: trim + entity replacement
2483
+ // Reused in both matcher update and second pass
2484
+ const processedVals = new Array(len);
2485
+ let hasRawAttrs = false;
2486
+ const rawAttrsForMatcher = {};
2487
+
1261
2488
  for (let i = 0; i < len; i++) {
1262
2489
  const attrName = this.resolveNameSpace(matches[i][1]);
1263
- if (this.ignoreAttributesFn(attrName, jPath)) {
1264
- continue
2490
+ const oldVal = matches[i][4];
2491
+
2492
+ if (attrName.length && oldVal !== undefined) {
2493
+ let val = oldVal;
2494
+ if (options.trimValues) val = val.trim();
2495
+ val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher);
2496
+ processedVals[i] = val;
2497
+
2498
+ rawAttrsForMatcher[attrName] = val;
2499
+ hasRawAttrs = true;
1265
2500
  }
1266
- let oldVal = matches[i][4];
1267
- let aName = this.options.attributeNamePrefix + attrName;
2501
+ }
2502
+
2503
+ // Update matcher ONCE before second pass, if applicable
2504
+ if (hasRawAttrs && typeof jPath === 'object' && jPath.updateCurrent) {
2505
+ jPath.updateCurrent(rawAttrsForMatcher);
2506
+ }
2507
+
2508
+ // Hoist toString() once — path doesn't change during attribute processing
2509
+ const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher;
2510
+
2511
+ // Second pass: apply processors, build final attrs
2512
+ let hasAttrs = false;
2513
+ for (let i = 0; i < len; i++) {
2514
+ const attrName = this.resolveNameSpace(matches[i][1]);
2515
+
2516
+ if (this.ignoreAttributesFn(attrName, jPathStr)) continue;
2517
+
2518
+ let aName = options.attributeNamePrefix + attrName;
2519
+
1268
2520
  if (attrName.length) {
1269
- if (this.options.transformAttributeName) {
1270
- aName = this.options.transformAttributeName(aName);
2521
+ if (options.transformAttributeName) {
2522
+ aName = options.transformAttributeName(aName);
1271
2523
  }
1272
- if (aName === "__proto__") aName = "#__proto__";
1273
- if (oldVal !== undefined) {
1274
- if (this.options.trimValues) {
1275
- oldVal = oldVal.trim();
1276
- }
1277
- oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);
1278
- const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);
2524
+ aName = sanitizeName(aName, options);
2525
+
2526
+ if (matches[i][4] !== undefined) {
2527
+ // Reuse already-processed value — no double entity replacement
2528
+ const oldVal = processedVals[i];
2529
+
2530
+ const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr);
1279
2531
  if (newVal === null || newVal === undefined) {
1280
- //don't parse
1281
2532
  attrs[aName] = oldVal;
1282
2533
  } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) {
1283
- //overwrite
1284
2534
  attrs[aName] = newVal;
1285
2535
  } else {
1286
- //parse
1287
- attrs[aName] = parseValue(
1288
- oldVal,
1289
- this.options.parseAttributeValue,
1290
- this.options.numberParseOptions
1291
- );
2536
+ attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions);
1292
2537
  }
1293
- } else if (this.options.allowBooleanAttributes) {
2538
+ hasAttrs = true;
2539
+ } else if (options.allowBooleanAttributes) {
1294
2540
  attrs[aName] = true;
2541
+ hasAttrs = true;
1295
2542
  }
1296
2543
  }
1297
2544
  }
1298
- if (!Object.keys(attrs).length) {
1299
- return;
1300
- }
1301
- if (this.options.attributesGroupName) {
2545
+
2546
+ if (!hasAttrs) return;
2547
+
2548
+ if (options.attributesGroupName) {
1302
2549
  const attrCollection = {};
1303
- attrCollection[this.options.attributesGroupName] = attrs;
2550
+ attrCollection[options.attributesGroupName] = attrs;
1304
2551
  return attrCollection;
1305
2552
  }
1306
- return attrs
2553
+ return attrs;
1307
2554
  }
1308
2555
  }
1309
-
1310
2556
  const parseXml = function (xmlData) {
1311
2557
  xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line
1312
2558
  const xmlObj = new XmlNode('!xml');
1313
2559
  let currentNode = xmlObj;
1314
2560
  let textData = "";
1315
- let jPath = "";
2561
+
2562
+ // Reset matcher for new document
2563
+ this.matcher.reset();
1316
2564
 
1317
2565
  // Reset entity expansion counters for this document
1318
2566
  this.entityExpansionCount = 0;
1319
2567
  this.currentExpandedLength = 0;
1320
-
1321
- const docTypeReader = new DocTypeReader(this.options.processEntities);
1322
- for (let i = 0; i < xmlData.length; i++) {//for each char in XML data
2568
+ this.docTypeEntitiesKeys = [];
2569
+ this.lastEntitiesKeys = Object.keys(this.lastEntities);
2570
+ this.htmlEntitiesKeys = this.options.htmlEntities ? Object.keys(this.htmlEntities) : [];
2571
+ const options = this.options;
2572
+ const docTypeReader = new DocTypeReader(options.processEntities);
2573
+ const xmlLen = xmlData.length;
2574
+ for (let i = 0; i < xmlLen; i++) {//for each char in XML data
1323
2575
  const ch = xmlData[i];
1324
2576
  if (ch === '<') {
1325
2577
  // const nextIndex = i+1;
1326
2578
  // const _2ndChar = xmlData[nextIndex];
1327
- if (xmlData[i + 1] === '/') {//Closing Tag
2579
+ const c1 = xmlData.charCodeAt(i + 1);
2580
+ if (c1 === 47) {//Closing Tag '/'
1328
2581
  const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.");
1329
2582
  let tagName = xmlData.substring(i + 2, closeIndex).trim();
1330
2583
 
1331
- if (this.options.removeNSPrefix) {
2584
+ if (options.removeNSPrefix) {
1332
2585
  const colonIndex = tagName.indexOf(":");
1333
2586
  if (colonIndex !== -1) {
1334
2587
  tagName = tagName.substr(colonIndex + 1);
1335
2588
  }
1336
2589
  }
1337
2590
 
1338
- if (this.options.transformTagName) {
1339
- tagName = this.options.transformTagName(tagName);
1340
- }
2591
+ tagName = transformTagName(options.transformTagName, tagName, "", options).tagName;
1341
2592
 
1342
2593
  if (currentNode) {
1343
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
2594
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
1344
2595
  }
1345
2596
 
1346
2597
  //check if last tag of nested tag was unpaired tag
1347
- const lastTagName = jPath.substring(jPath.lastIndexOf(".") + 1);
1348
- if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {
2598
+ const lastTagName = this.matcher.getCurrentTag();
2599
+ if (tagName && options.unpairedTagsSet.has(tagName)) {
1349
2600
  throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
1350
2601
  }
1351
- let propIndex = 0;
1352
- if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
1353
- propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.') - 1);
2602
+ if (lastTagName && options.unpairedTagsSet.has(lastTagName)) {
2603
+ // Pop the unpaired tag
2604
+ this.matcher.pop();
1354
2605
  this.tagsNodeStack.pop();
1355
- } else {
1356
- propIndex = jPath.lastIndexOf(".");
1357
2606
  }
1358
- jPath = jPath.substring(0, propIndex);
2607
+ // Pop the closing tag
2608
+ this.matcher.pop();
2609
+ this.isCurrentNodeStopNode = false; // Reset flag when closing tag
1359
2610
 
1360
2611
  currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
1361
2612
  textData = "";
1362
2613
  i = closeIndex;
1363
- } else if (xmlData[i + 1] === '?') {
2614
+ } else if (c1 === 63) { //'?'
1364
2615
 
1365
2616
  let tagData = readTagExp(xmlData, i, false, "?>");
1366
2617
  if (!tagData) throw new Error("Pi Tag is not closed.");
1367
2618
 
1368
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
1369
- if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) ; else {
2619
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
2620
+ if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) ; else {
1370
2621
 
1371
2622
  const childNode = new XmlNode(tagData.tagName);
1372
- childNode.add(this.options.textNodeName, "");
2623
+ childNode.add(options.textNodeName, "");
1373
2624
 
1374
2625
  if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
1375
- childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);
2626
+ childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
1376
2627
  }
1377
- this.addChild(currentNode, childNode, jPath, i);
2628
+ this.addChild(currentNode, childNode, this.readonlyMatcher, i);
1378
2629
  }
1379
2630
 
1380
2631
 
1381
2632
  i = tagData.closeIndex + 1;
1382
- } else if (xmlData.substr(i + 1, 3) === '!--') {
2633
+ } else if (c1 === 33
2634
+ && xmlData.charCodeAt(i + 2) === 45
2635
+ && xmlData.charCodeAt(i + 3) === 45) { //'!--'
1383
2636
  const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.");
1384
- if (this.options.commentPropName) {
2637
+ if (options.commentPropName) {
1385
2638
  const comment = xmlData.substring(i + 4, endIndex - 2);
1386
2639
 
1387
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
2640
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
1388
2641
 
1389
- currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
2642
+ currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]);
1390
2643
  }
1391
2644
  i = endIndex;
1392
- } else if (xmlData.substr(i + 1, 2) === '!D') {
2645
+ } else if (c1 === 33
2646
+ && xmlData.charCodeAt(i + 2) === 68) { //'!D'
1393
2647
  const result = docTypeReader.readDocType(xmlData, i);
1394
2648
  this.docTypeEntities = result.entities;
2649
+ this.docTypeEntitiesKeys = Object.keys(this.docTypeEntities) || [];
1395
2650
  i = result.i;
1396
- } else if (xmlData.substr(i + 1, 2) === '![') {
2651
+ } else if (c1 === 33
2652
+ && xmlData.charCodeAt(i + 2) === 91) { // '!['
1397
2653
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
1398
2654
  const tagExp = xmlData.substring(i + 9, closeIndex);
1399
2655
 
1400
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
2656
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
1401
2657
 
1402
- let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);
2658
+ let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true);
1403
2659
  if (val == undefined) val = "";
1404
2660
 
1405
2661
  //cdata should be set even if it is 0 length string
1406
- if (this.options.cdataPropName) {
1407
- currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]);
2662
+ if (options.cdataPropName) {
2663
+ currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]);
1408
2664
  } else {
1409
- currentNode.add(this.options.textNodeName, val);
2665
+ currentNode.add(options.textNodeName, val);
1410
2666
  }
1411
2667
 
1412
2668
  i = closeIndex + 2;
1413
2669
  } else {//Opening tag
1414
- let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
2670
+ let result = readTagExp(xmlData, i, options.removeNSPrefix);
2671
+
2672
+ // Safety check: readTagExp can return undefined
2673
+ if (!result) {
2674
+ // Log context for debugging
2675
+ const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50));
2676
+ throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`);
2677
+ }
2678
+
1415
2679
  let tagName = result.tagName;
1416
2680
  const rawTagName = result.rawTagName;
1417
2681
  let tagExp = result.tagExp;
1418
2682
  let attrExpPresent = result.attrExpPresent;
1419
2683
  let closeIndex = result.closeIndex;
1420
2684
 
1421
- if (this.options.transformTagName) {
1422
- //console.log(tagExp, tagName)
1423
- const newTagName = this.options.transformTagName(tagName);
1424
- if (tagExp === tagName) {
1425
- tagExp = newTagName;
1426
- }
1427
- tagName = newTagName;
2685
+ ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options));
2686
+
2687
+ if (options.strictReservedNames &&
2688
+ (tagName === options.commentPropName
2689
+ || tagName === options.cdataPropName
2690
+ || tagName === options.textNodeName
2691
+ || tagName === options.attributesGroupName
2692
+ )) {
2693
+ throw new Error(`Invalid tag name: ${tagName}`);
1428
2694
  }
1429
2695
 
1430
2696
  //save text as child node
1431
2697
  if (currentNode && textData) {
1432
2698
  if (currentNode.tagname !== '!xml') {
1433
2699
  //when nested tag is found
1434
- textData = this.saveTextToParentTag(textData, currentNode, jPath, false);
2700
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false);
1435
2701
  }
1436
2702
  }
1437
2703
 
1438
2704
  //check if last tag was unpaired tag
1439
2705
  const lastTag = currentNode;
1440
- if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
2706
+ if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) {
1441
2707
  currentNode = this.tagsNodeStack.pop();
1442
- jPath = jPath.substring(0, jPath.lastIndexOf("."));
2708
+ this.matcher.pop();
1443
2709
  }
2710
+
2711
+ // Clean up self-closing syntax BEFORE processing attributes
2712
+ // This is where tagExp gets the trailing / removed
2713
+ let isSelfClosing = false;
2714
+ if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
2715
+ isSelfClosing = true;
2716
+ if (tagName[tagName.length - 1] === "/") {
2717
+ tagName = tagName.substr(0, tagName.length - 1);
2718
+ tagExp = tagName;
2719
+ } else {
2720
+ tagExp = tagExp.substr(0, tagExp.length - 1);
2721
+ }
2722
+
2723
+ // Re-check attrExpPresent after cleaning
2724
+ attrExpPresent = (tagName !== tagExp);
2725
+ }
2726
+
2727
+ // Now process attributes with CLEAN tagExp (no trailing /)
2728
+ let prefixedAttrs = null;
2729
+ let namespace = undefined;
2730
+
2731
+ // Extract namespace from rawTagName
2732
+ namespace = extractNamespace(rawTagName);
2733
+
2734
+ // Push tag to matcher FIRST (with empty attrs for now) so callbacks see correct path
1444
2735
  if (tagName !== xmlObj.tagname) {
1445
- jPath += jPath ? "." + tagName : tagName;
2736
+ this.matcher.push(tagName, {}, namespace);
1446
2737
  }
2738
+
2739
+ // Now build attributes - callbacks will see correct matcher state
2740
+ if (tagName !== tagExp && attrExpPresent) {
2741
+ // Build attributes (returns prefixed attributes for the tree)
2742
+ // Note: buildAttributesMap now internally updates the matcher with raw attributes
2743
+ prefixedAttrs = this.buildAttributesMap(tagExp, this.matcher, tagName);
2744
+
2745
+ if (prefixedAttrs) {
2746
+ // Extract raw attributes (without prefix) for our use
2747
+ extractRawAttributes(prefixedAttrs, options);
2748
+ }
2749
+ }
2750
+
2751
+ // Now check if this is a stop node (after attributes are set)
2752
+ if (tagName !== xmlObj.tagname) {
2753
+ this.isCurrentNodeStopNode = this.isItStopNode();
2754
+ }
2755
+
1447
2756
  const startIndex = i;
1448
- if (this.isItStopNode(this.stopNodesExact, this.stopNodesWildcard, jPath, tagName)) {
2757
+ if (this.isCurrentNodeStopNode) {
1449
2758
  let tagContent = "";
1450
- //self-closing tag
1451
- if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
1452
- if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
1453
- tagName = tagName.substr(0, tagName.length - 1);
1454
- jPath = jPath.substr(0, jPath.length - 1);
1455
- tagExp = tagName;
1456
- } else {
1457
- tagExp = tagExp.substr(0, tagExp.length - 1);
1458
- }
2759
+
2760
+ // For self-closing tags, content is empty
2761
+ if (isSelfClosing) {
1459
2762
  i = result.closeIndex;
1460
2763
  }
1461
2764
  //unpaired tag
1462
- else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
1463
-
2765
+ else if (options.unpairedTagsSet.has(tagName)) {
1464
2766
  i = result.closeIndex;
1465
2767
  }
1466
2768
  //normal tag
@@ -1474,52 +2776,54 @@ const parseXml = function (xmlData) {
1474
2776
 
1475
2777
  const childNode = new XmlNode(tagName);
1476
2778
 
1477
- if (tagName !== tagExp && attrExpPresent) {
1478
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
1479
- }
1480
- if (tagContent) {
1481
- tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
2779
+ if (prefixedAttrs) {
2780
+ childNode[":@"] = prefixedAttrs;
1482
2781
  }
1483
2782
 
1484
- jPath = jPath.substr(0, jPath.lastIndexOf("."));
1485
- childNode.add(this.options.textNodeName, tagContent);
2783
+ // For stop nodes, store raw content as-is without any processing
2784
+ childNode.add(options.textNodeName, tagContent);
2785
+
2786
+ this.matcher.pop(); // Pop the stop node tag
2787
+ this.isCurrentNodeStopNode = false; // Reset flag
1486
2788
 
1487
- this.addChild(currentNode, childNode, jPath, startIndex);
2789
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
1488
2790
  } else {
1489
2791
  //selfClosing tag
1490
- if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
1491
- if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
1492
- tagName = tagName.substr(0, tagName.length - 1);
1493
- jPath = jPath.substr(0, jPath.length - 1);
1494
- tagExp = tagName;
1495
- } else {
1496
- tagExp = tagExp.substr(0, tagExp.length - 1);
1497
- }
2792
+ if (isSelfClosing) {
2793
+ ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options));
1498
2794
 
1499
- if (this.options.transformTagName) {
1500
- const newTagName = this.options.transformTagName(tagName);
1501
- if (tagExp === tagName) {
1502
- tagExp = newTagName;
1503
- }
1504
- tagName = newTagName;
2795
+ const childNode = new XmlNode(tagName);
2796
+ if (prefixedAttrs) {
2797
+ childNode[":@"] = prefixedAttrs;
1505
2798
  }
1506
-
2799
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
2800
+ this.matcher.pop(); // Pop self-closing tag
2801
+ this.isCurrentNodeStopNode = false; // Reset flag
2802
+ }
2803
+ else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag
1507
2804
  const childNode = new XmlNode(tagName);
1508
- if (tagName !== tagExp && attrExpPresent) {
1509
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
2805
+ if (prefixedAttrs) {
2806
+ childNode[":@"] = prefixedAttrs;
1510
2807
  }
1511
- this.addChild(currentNode, childNode, jPath, startIndex);
1512
- jPath = jPath.substr(0, jPath.lastIndexOf("."));
2808
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
2809
+ this.matcher.pop(); // Pop unpaired tag
2810
+ this.isCurrentNodeStopNode = false; // Reset flag
2811
+ i = result.closeIndex;
2812
+ // Continue to next iteration without changing currentNode
2813
+ continue;
1513
2814
  }
1514
2815
  //opening tag
1515
2816
  else {
1516
2817
  const childNode = new XmlNode(tagName);
2818
+ if (this.tagsNodeStack.length > options.maxNestedTags) {
2819
+ throw new Error("Maximum nested tags exceeded");
2820
+ }
1517
2821
  this.tagsNodeStack.push(currentNode);
1518
2822
 
1519
- if (tagName !== tagExp && attrExpPresent) {
1520
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
2823
+ if (prefixedAttrs) {
2824
+ childNode[":@"] = prefixedAttrs;
1521
2825
  }
1522
- this.addChild(currentNode, childNode, jPath, startIndex);
2826
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
1523
2827
  currentNode = childNode;
1524
2828
  }
1525
2829
  textData = "";
@@ -1533,10 +2837,13 @@ const parseXml = function (xmlData) {
1533
2837
  return xmlObj.child;
1534
2838
  };
1535
2839
 
1536
- function addChild(currentNode, childNode, jPath, startIndex) {
2840
+ function addChild(currentNode, childNode, matcher, startIndex) {
1537
2841
  // unset startIndex if not requested
1538
2842
  if (!this.options.captureMetaData) startIndex = undefined;
1539
- const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"]);
2843
+
2844
+ // Pass jPath string or matcher based on options.jPath setting
2845
+ const jPathOrMatcher = this.options.jPath ? matcher.toString() : matcher;
2846
+ const result = this.options.updateTag(childNode.tagname, jPathOrMatcher, childNode[":@"]);
1540
2847
  if (result === false) ; else if (typeof result === "string") {
1541
2848
  childNode.tagname = result;
1542
2849
  currentNode.addChild(childNode, startIndex);
@@ -1545,33 +2852,40 @@ function addChild(currentNode, childNode, jPath, startIndex) {
1545
2852
  }
1546
2853
  }
1547
2854
 
1548
- const replaceEntitiesValue$1 = function (val, tagName, jPath) {
1549
- // Performance optimization: Early return if no entities to replace
1550
- if (val.indexOf('&') === -1) {
1551
- return val;
1552
- }
1553
-
2855
+ /**
2856
+ * @param {object} val - Entity object with regex and val properties
2857
+ * @param {string} tagName - Tag name
2858
+ * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
2859
+ */
2860
+ function replaceEntitiesValue$1(val, tagName, jPath) {
1554
2861
  const entityConfig = this.options.processEntities;
1555
2862
 
1556
- if (!entityConfig.enabled) {
2863
+ if (!entityConfig || !entityConfig.enabled) {
1557
2864
  return val;
1558
2865
  }
1559
2866
 
1560
- // Check tag-specific filtering
2867
+ // Check if tag is allowed to contain entities
1561
2868
  if (entityConfig.allowedTags) {
1562
- if (!entityConfig.allowedTags.includes(tagName)) {
1563
- return val; // Skip entity replacement for current tag as not set
2869
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
2870
+ const allowed = Array.isArray(entityConfig.allowedTags)
2871
+ ? entityConfig.allowedTags.includes(tagName)
2872
+ : entityConfig.allowedTags(tagName, jPathOrMatcher);
2873
+
2874
+ if (!allowed) {
2875
+ return val;
1564
2876
  }
1565
2877
  }
1566
2878
 
2879
+ // Apply custom tag filter if provided
1567
2880
  if (entityConfig.tagFilter) {
1568
- if (!entityConfig.tagFilter(tagName, jPath)) {
2881
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
2882
+ if (!entityConfig.tagFilter(tagName, jPathOrMatcher)) {
1569
2883
  return val; // Skip based on custom filter
1570
2884
  }
1571
2885
  }
1572
2886
 
1573
2887
  // Replace DOCTYPE entities
1574
- for (let entityName in this.docTypeEntities) {
2888
+ for (const entityName of this.docTypeEntitiesKeys) {
1575
2889
  const entity = this.docTypeEntities[entityName];
1576
2890
  const matches = val.match(entity.regx);
1577
2891
 
@@ -1602,60 +2916,75 @@ const replaceEntitiesValue$1 = function (val, tagName, jPath) {
1602
2916
  }
1603
2917
  }
1604
2918
  }
1605
- }
1606
- if (val.indexOf('&') === -1) return val; // Early exit
1607
-
1608
- // Replace standard entities
1609
- for (let entityName in this.lastEntities) {
1610
- const entity = this.lastEntities[entityName];
2919
+ }
2920
+ if (val.indexOf('&') === -1) return val;
2921
+ // Replace standard entities
2922
+ for (const entityName of this.lastEntitiesKeys) {
2923
+ const entity = this.lastEntities[entityName];
2924
+ const matches = val.match(entity.regex);
2925
+ if (matches) {
2926
+ this.entityExpansionCount += matches.length;
2927
+ if (entityConfig.maxTotalExpansions &&
2928
+ this.entityExpansionCount > entityConfig.maxTotalExpansions) {
2929
+ throw new Error(
2930
+ `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
2931
+ );
2932
+ }
2933
+ }
1611
2934
  val = val.replace(entity.regex, entity.val);
1612
2935
  }
1613
- if (val.indexOf('&') === -1) return val; // Early exit
2936
+ if (val.indexOf('&') === -1) return val;
1614
2937
 
1615
2938
  // Replace HTML entities if enabled
1616
- if (this.options.htmlEntities) {
1617
- for (let entityName in this.htmlEntities) {
1618
- const entity = this.htmlEntities[entityName];
1619
- val = val.replace(entity.regex, entity.val);
2939
+ for (const entityName of this.htmlEntitiesKeys) {
2940
+ const entity = this.htmlEntities[entityName];
2941
+ const matches = val.match(entity.regex);
2942
+ if (matches) {
2943
+ //console.log(matches);
2944
+ this.entityExpansionCount += matches.length;
2945
+ if (entityConfig.maxTotalExpansions &&
2946
+ this.entityExpansionCount > entityConfig.maxTotalExpansions) {
2947
+ throw new Error(
2948
+ `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
2949
+ );
2950
+ }
1620
2951
  }
2952
+ val = val.replace(entity.regex, entity.val);
1621
2953
  }
1622
2954
 
1623
2955
  // Replace ampersand entity last
1624
2956
  val = val.replace(this.ampEntity.regex, this.ampEntity.val);
1625
2957
 
1626
2958
  return val;
1627
- };
2959
+ }
1628
2960
 
1629
2961
 
1630
- function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) {
2962
+ function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) {
1631
2963
  if (textData) { //store previously collected data as textNode
1632
- if (isLeafNode === undefined) isLeafNode = currentNode.child.length === 0;
2964
+ if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0;
1633
2965
 
1634
2966
  textData = this.parseTextData(textData,
1635
- currentNode.tagname,
1636
- jPath,
2967
+ parentNode.tagname,
2968
+ matcher,
1637
2969
  false,
1638
- currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false,
2970
+ parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
1639
2971
  isLeafNode);
1640
2972
 
1641
2973
  if (textData !== undefined && textData !== "")
1642
- currentNode.add(this.options.textNodeName, textData);
2974
+ parentNode.add(this.options.textNodeName, textData);
1643
2975
  textData = "";
1644
2976
  }
1645
2977
  return textData;
1646
2978
  }
1647
2979
 
1648
- //TODO: use jPath to simplify the logic
1649
2980
  /**
1650
- * @param {Set} stopNodesExact
1651
- * @param {Set} stopNodesWildcard
1652
- * @param {string} jPath
1653
- * @param {string} currentTagName
2981
+ * @param {Array<Expression>} stopNodeExpressions - Array of compiled Expression objects
2982
+ * @param {Matcher} matcher - Current path matcher
1654
2983
  */
1655
- function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName) {
1656
- if (stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;
1657
- if (stopNodesExact && stopNodesExact.has(jPath)) return true;
1658
- return false;
2984
+ function isItStopNode() {
2985
+ if (this.stopNodeExpressionsSet.size === 0) return false;
2986
+
2987
+ return this.matcher.matchesAny(this.stopNodeExpressionsSet);
1659
2988
  }
1660
2989
 
1661
2990
  /**
@@ -1665,32 +2994,33 @@ function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName)
1665
2994
  * @returns
1666
2995
  */
1667
2996
  function tagExpWithClosingIndex(xmlData, i, closingChar = ">") {
1668
- let attrBoundary;
1669
- let tagExp = "";
1670
- for (let index = i; index < xmlData.length; index++) {
1671
- let ch = xmlData[index];
2997
+ let attrBoundary = 0;
2998
+ const chars = [];
2999
+ const len = xmlData.length;
3000
+ const closeCode0 = closingChar.charCodeAt(0);
3001
+ const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1;
3002
+
3003
+ for (let index = i; index < len; index++) {
3004
+ const code = xmlData.charCodeAt(index);
3005
+
1672
3006
  if (attrBoundary) {
1673
- if (ch === attrBoundary) attrBoundary = "";//reset
1674
- } else if (ch === '"' || ch === "'") {
1675
- attrBoundary = ch;
1676
- } else if (ch === closingChar[0]) {
1677
- if (closingChar[1]) {
1678
- if (xmlData[index + 1] === closingChar[1]) {
1679
- return {
1680
- data: tagExp,
1681
- index: index
1682
- }
3007
+ if (code === attrBoundary) attrBoundary = 0;
3008
+ } else if (code === 34 || code === 39) { // " or '
3009
+ attrBoundary = code;
3010
+ } else if (code === closeCode0) {
3011
+ if (closeCode1 !== -1) {
3012
+ if (xmlData.charCodeAt(index + 1) === closeCode1) {
3013
+ return { data: String.fromCharCode(...chars), index };
1683
3014
  }
1684
3015
  } else {
1685
- return {
1686
- data: tagExp,
1687
- index: index
1688
- }
3016
+ return { data: String.fromCharCode(...chars), index };
1689
3017
  }
1690
- } else if (ch === '\t') {
1691
- ch = " ";
3018
+ } else if (code === 9) { // \t
3019
+ chars.push(32); // space
3020
+ continue;
1692
3021
  }
1693
- tagExp += ch;
3022
+
3023
+ chars.push(code);
1694
3024
  }
1695
3025
  }
1696
3026
 
@@ -1703,6 +3033,12 @@ function findClosingIndex(xmlData, str, i, errMsg) {
1703
3033
  }
1704
3034
  }
1705
3035
 
3036
+ function findClosingChar(xmlData, char, i, errMsg) {
3037
+ const closingIndex = xmlData.indexOf(char, i);
3038
+ if (closingIndex === -1) throw new Error(errMsg);
3039
+ return closingIndex; // no offset needed
3040
+ }
3041
+
1706
3042
  function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") {
1707
3043
  const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar);
1708
3044
  if (!result) return;
@@ -1744,10 +3080,12 @@ function readStopNodeData(xmlData, tagName, i) {
1744
3080
  // Starting at 1 since we already have an open tag
1745
3081
  let openTagCount = 1;
1746
3082
 
1747
- for (; i < xmlData.length; i++) {
3083
+ const xmllen = xmlData.length;
3084
+ for (; i < xmllen; i++) {
1748
3085
  if (xmlData[i] === "<") {
1749
- if (xmlData[i + 1] === "/") {//close tag
1750
- const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`);
3086
+ const c1 = xmlData.charCodeAt(i + 1);
3087
+ if (c1 === 47) {//close tag '/'
3088
+ const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`);
1751
3089
  let closeTagName = xmlData.substring(i + 2, closeIndex).trim();
1752
3090
  if (closeTagName === tagName) {
1753
3091
  openTagCount--;
@@ -1759,13 +3097,16 @@ function readStopNodeData(xmlData, tagName, i) {
1759
3097
  }
1760
3098
  }
1761
3099
  i = closeIndex;
1762
- } else if (xmlData[i + 1] === '?') {
3100
+ } else if (c1 === 63) { //?
1763
3101
  const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.");
1764
3102
  i = closeIndex;
1765
- } else if (xmlData.substr(i + 1, 3) === '!--') {
3103
+ } else if (c1 === 33
3104
+ && xmlData.charCodeAt(i + 2) === 45
3105
+ && xmlData.charCodeAt(i + 3) === 45) { // '!--'
1766
3106
  const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.");
1767
3107
  i = closeIndex;
1768
- } else if (xmlData.substr(i + 1, 2) === '![') {
3108
+ } else if (c1 === 33
3109
+ && xmlData.charCodeAt(i + 2) === 91) { // '!['
1769
3110
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2;
1770
3111
  i = closeIndex;
1771
3112
  } else {
@@ -1809,97 +3150,173 @@ function fromCodePoint(str, base, prefix) {
1809
3150
  }
1810
3151
  }
1811
3152
 
3153
+ function transformTagName(fn, tagName, tagExp, options) {
3154
+ if (fn) {
3155
+ const newTagName = fn(tagName);
3156
+ if (tagExp === tagName) {
3157
+ tagExp = newTagName;
3158
+ }
3159
+ tagName = newTagName;
3160
+ }
3161
+ tagName = sanitizeName(tagName, options);
3162
+ return { tagName, tagExp };
3163
+ }
3164
+
3165
+
3166
+
3167
+ function sanitizeName(name, options) {
3168
+ if (criticalProperties.includes(name)) {
3169
+ throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`);
3170
+ } else if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
3171
+ return options.onDangerousProperty(name);
3172
+ }
3173
+ return name;
3174
+ }
3175
+
1812
3176
  const METADATA_SYMBOL = XmlNode.getMetaDataSymbol();
1813
3177
 
3178
+ /**
3179
+ * Helper function to strip attribute prefix from attribute map
3180
+ * @param {object} attrs - Attributes with prefix (e.g., {"@_class": "code"})
3181
+ * @param {string} prefix - Attribute prefix to remove (e.g., "@_")
3182
+ * @returns {object} Attributes without prefix (e.g., {"class": "code"})
3183
+ */
3184
+ function stripAttributePrefix(attrs, prefix) {
3185
+ if (!attrs || typeof attrs !== 'object') return {};
3186
+ if (!prefix) return attrs;
3187
+
3188
+ const rawAttrs = {};
3189
+ for (const key in attrs) {
3190
+ if (key.startsWith(prefix)) {
3191
+ const rawName = key.substring(prefix.length);
3192
+ rawAttrs[rawName] = attrs[key];
3193
+ } else {
3194
+ // Attribute without prefix (shouldn't normally happen, but be safe)
3195
+ rawAttrs[key] = attrs[key];
3196
+ }
3197
+ }
3198
+ return rawAttrs;
3199
+ }
3200
+
1814
3201
  /**
1815
3202
  *
1816
3203
  * @param {array} node
1817
3204
  * @param {any} options
3205
+ * @param {Matcher} matcher - Path matcher instance
1818
3206
  * @returns
1819
3207
  */
1820
- function prettify(node, options){
1821
- return compress( node, options);
3208
+ function prettify(node, options, matcher, readonlyMatcher) {
3209
+ return compress(node, options, matcher, readonlyMatcher);
1822
3210
  }
1823
3211
 
1824
3212
  /**
1825
- *
1826
3213
  * @param {array} arr
1827
3214
  * @param {object} options
1828
- * @param {string} jPath
3215
+ * @param {Matcher} matcher - Path matcher instance
1829
3216
  * @returns object
1830
3217
  */
1831
- function compress(arr, options, jPath){
3218
+ function compress(arr, options, matcher, readonlyMatcher) {
1832
3219
  let text;
1833
- const compressedObj = {};
3220
+ const compressedObj = {}; //This is intended to be a plain object
1834
3221
  for (let i = 0; i < arr.length; i++) {
1835
3222
  const tagObj = arr[i];
1836
3223
  const property = propName$1(tagObj);
1837
- let newJpath = "";
1838
- if(jPath === undefined) newJpath = property;
1839
- else newJpath = jPath + "." + property;
1840
3224
 
1841
- if(property === options.textNodeName){
1842
- if(text === undefined) text = tagObj[property];
3225
+ // Push current property to matcher WITH RAW ATTRIBUTES (no prefix)
3226
+ if (property !== undefined && property !== options.textNodeName) {
3227
+ const rawAttrs = stripAttributePrefix(
3228
+ tagObj[":@"] || {},
3229
+ options.attributeNamePrefix
3230
+ );
3231
+ matcher.push(property, rawAttrs);
3232
+ }
3233
+
3234
+ if (property === options.textNodeName) {
3235
+ if (text === undefined) text = tagObj[property];
1843
3236
  else text += "" + tagObj[property];
1844
- }else if(property === undefined){
3237
+ } else if (property === undefined) {
1845
3238
  continue;
1846
- }else if(tagObj[property]){
1847
-
1848
- let val = compress(tagObj[property], options, newJpath);
3239
+ } else if (tagObj[property]) {
3240
+
3241
+ let val = compress(tagObj[property], options, matcher, readonlyMatcher);
1849
3242
  const isLeaf = isLeafTag(val, options);
1850
- if (tagObj[METADATA_SYMBOL] !== undefined) {
1851
- val[METADATA_SYMBOL] = tagObj[METADATA_SYMBOL]; // copy over metadata
1852
- }
1853
3243
 
1854
- if(tagObj[":@"]){
1855
- assignAttributes( val, tagObj[":@"], newJpath, options);
1856
- }else if(Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode){
3244
+ if (tagObj[":@"]) {
3245
+ assignAttributes(val, tagObj[":@"], readonlyMatcher, options);
3246
+ } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) {
1857
3247
  val = val[options.textNodeName];
1858
- }else if(Object.keys(val).length === 0){
1859
- if(options.alwaysCreateTextNode) val[options.textNodeName] = "";
3248
+ } else if (Object.keys(val).length === 0) {
3249
+ if (options.alwaysCreateTextNode) val[options.textNodeName] = "";
1860
3250
  else val = "";
1861
3251
  }
1862
3252
 
1863
- if(compressedObj[property] !== undefined && compressedObj.hasOwnProperty(property)) {
1864
- if(!Array.isArray(compressedObj[property])) {
1865
- compressedObj[property] = [ compressedObj[property] ];
3253
+ if (tagObj[METADATA_SYMBOL] !== undefined && typeof val === "object" && val !== null) {
3254
+ val[METADATA_SYMBOL] = tagObj[METADATA_SYMBOL]; // copy over metadata
3255
+ }
3256
+
3257
+
3258
+ if (compressedObj[property] !== undefined && Object.prototype.hasOwnProperty.call(compressedObj, property)) {
3259
+ if (!Array.isArray(compressedObj[property])) {
3260
+ compressedObj[property] = [compressedObj[property]];
1866
3261
  }
1867
3262
  compressedObj[property].push(val);
1868
- }else {
3263
+ } else {
1869
3264
  //TODO: if a node is not an array, then check if it should be an array
1870
3265
  //also determine if it is a leaf node
1871
- if (options.isArray(property, newJpath, isLeaf )) {
3266
+
3267
+ // Pass jPath string or readonlyMatcher based on options.jPath setting
3268
+ const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher;
3269
+ if (options.isArray(property, jPathOrMatcher, isLeaf)) {
1872
3270
  compressedObj[property] = [val];
1873
- }else {
3271
+ } else {
1874
3272
  compressedObj[property] = val;
1875
3273
  }
1876
3274
  }
3275
+
3276
+ // Pop property from matcher after processing
3277
+ if (property !== undefined && property !== options.textNodeName) {
3278
+ matcher.pop();
3279
+ }
1877
3280
  }
1878
-
3281
+
1879
3282
  }
1880
3283
  // if(text && text.length > 0) compressedObj[options.textNodeName] = text;
1881
- if(typeof text === "string"){
1882
- if(text.length > 0) compressedObj[options.textNodeName] = text;
1883
- }else if(text !== undefined) compressedObj[options.textNodeName] = text;
3284
+ if (typeof text === "string") {
3285
+ if (text.length > 0) compressedObj[options.textNodeName] = text;
3286
+ } else if (text !== undefined) compressedObj[options.textNodeName] = text;
3287
+
3288
+
1884
3289
  return compressedObj;
1885
3290
  }
1886
3291
 
1887
- function propName$1(obj){
3292
+ function propName$1(obj) {
1888
3293
  const keys = Object.keys(obj);
1889
3294
  for (let i = 0; i < keys.length; i++) {
1890
3295
  const key = keys[i];
1891
- if(key !== ":@") return key;
3296
+ if (key !== ":@") return key;
1892
3297
  }
1893
3298
  }
1894
3299
 
1895
- function assignAttributes(obj, attrMap, jpath, options){
3300
+ function assignAttributes(obj, attrMap, readonlyMatcher, options) {
1896
3301
  if (attrMap) {
1897
3302
  const keys = Object.keys(attrMap);
1898
3303
  const len = keys.length; //don't make it inline
1899
3304
  for (let i = 0; i < len; i++) {
1900
- const atrrName = keys[i];
1901
- if (options.isArray(atrrName, jpath + "." + atrrName, true, true)) {
1902
- obj[atrrName] = [ attrMap[atrrName] ];
3305
+ const atrrName = keys[i]; // This is the PREFIXED name (e.g., "@_class")
3306
+
3307
+ // Strip prefix for matcher path (for isArray callback)
3308
+ const rawAttrName = atrrName.startsWith(options.attributeNamePrefix)
3309
+ ? atrrName.substring(options.attributeNamePrefix.length)
3310
+ : atrrName;
3311
+
3312
+ // For attributes, we need to create a temporary path
3313
+ // Pass jPath string or matcher based on options.jPath setting
3314
+ const jPathOrMatcher = options.jPath
3315
+ ? readonlyMatcher.toString() + "." + rawAttrName
3316
+ : readonlyMatcher;
3317
+
3318
+ if (options.isArray(atrrName, jPathOrMatcher, true, true)) {
3319
+ obj[atrrName] = [attrMap[atrrName]];
1903
3320
  } else {
1904
3321
  obj[atrrName] = attrMap[atrrName];
1905
3322
  }
@@ -1907,10 +3324,10 @@ function assignAttributes(obj, attrMap, jpath, options){
1907
3324
  }
1908
3325
  }
1909
3326
 
1910
- function isLeafTag(obj, options){
3327
+ function isLeafTag(obj, options) {
1911
3328
  const { textNodeName } = options;
1912
3329
  const propCount = Object.keys(obj).length;
1913
-
3330
+
1914
3331
  if (propCount === 0) {
1915
3332
  return true;
1916
3333
  }
@@ -1925,38 +3342,38 @@ function isLeafTag(obj, options){
1925
3342
  return false;
1926
3343
  }
1927
3344
 
1928
- class XMLParser{
1929
-
1930
- constructor(options){
3345
+ class XMLParser {
3346
+
3347
+ constructor(options) {
1931
3348
  this.externalEntities = {};
1932
3349
  this.options = buildOptions(options);
1933
-
3350
+
1934
3351
  }
1935
3352
  /**
1936
3353
  * Parse XML dats to JS object
1937
3354
  * @param {string|Uint8Array} xmlData
1938
3355
  * @param {boolean|Object} validationOption
1939
3356
  */
1940
- parse(xmlData,validationOption){
1941
- if(typeof xmlData !== "string" && xmlData.toString){
3357
+ parse(xmlData, validationOption) {
3358
+ if (typeof xmlData !== "string" && xmlData.toString) {
1942
3359
  xmlData = xmlData.toString();
1943
- }else if(typeof xmlData !== "string"){
3360
+ } else if (typeof xmlData !== "string") {
1944
3361
  throw new Error("XML data is accepted in String or Bytes[] form.")
1945
3362
  }
1946
-
1947
- if( validationOption){
1948
- if(validationOption === true) validationOption = {}; //validate with default options
1949
-
3363
+
3364
+ if (validationOption) {
3365
+ if (validationOption === true) validationOption = {}; //validate with default options
3366
+
1950
3367
  const result = validate(xmlData, validationOption);
1951
3368
  if (result !== true) {
1952
- throw Error( `${result.err.msg}:${result.err.line}:${result.err.col}` )
3369
+ throw Error(`${result.err.msg}:${result.err.line}:${result.err.col}`)
1953
3370
  }
1954
- }
3371
+ }
1955
3372
  const orderedObjParser = new OrderedObjParser(this.options);
1956
3373
  orderedObjParser.addExternalEntities(this.externalEntities);
1957
3374
  const orderedResult = orderedObjParser.parseXml(xmlData);
1958
- if(this.options.preserveOrder || orderedResult === undefined) return orderedResult;
1959
- else return prettify(orderedResult, this.options);
3375
+ if (this.options.preserveOrder || orderedResult === undefined) return orderedResult;
3376
+ else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher);
1960
3377
  }
1961
3378
 
1962
3379
  /**
@@ -1964,14 +3381,14 @@ class XMLParser{
1964
3381
  * @param {string} key
1965
3382
  * @param {string} value
1966
3383
  */
1967
- addEntity(key, value){
1968
- if(value.indexOf("&") !== -1){
3384
+ addEntity(key, value) {
3385
+ if (value.indexOf("&") !== -1) {
1969
3386
  throw new Error("Entity value can't have '&'")
1970
- }else if(key.indexOf("&") !== -1 || key.indexOf(";") !== -1){
3387
+ } else if (key.indexOf("&") !== -1 || key.indexOf(";") !== -1) {
1971
3388
  throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for '&#xD;'")
1972
- }else if(value === "&"){
3389
+ } else if (value === "&") {
1973
3390
  throw new Error("An entity with value '&' is not permitted");
1974
- }else {
3391
+ } else {
1975
3392
  this.externalEntities[key] = value;
1976
3393
  }
1977
3394
  }
@@ -2004,25 +3421,61 @@ function toXml(jArray, options) {
2004
3421
  if (options.format && options.indentBy.length > 0) {
2005
3422
  indentation = EOL;
2006
3423
  }
2007
- return arrToStr(jArray, options, "", indentation);
3424
+
3425
+ // Pre-compile stopNode expressions for pattern matching
3426
+ const stopNodeExpressions = [];
3427
+ if (options.stopNodes && Array.isArray(options.stopNodes)) {
3428
+ for (let i = 0; i < options.stopNodes.length; i++) {
3429
+ const node = options.stopNodes[i];
3430
+ if (typeof node === 'string') {
3431
+ stopNodeExpressions.push(new Expression(node));
3432
+ } else if (node instanceof Expression) {
3433
+ stopNodeExpressions.push(node);
3434
+ }
3435
+ }
3436
+ }
3437
+
3438
+ // Initialize matcher for path tracking
3439
+ const matcher = new Matcher();
3440
+
3441
+ return arrToStr(jArray, options, indentation, matcher, stopNodeExpressions);
2008
3442
  }
2009
3443
 
2010
- function arrToStr(arr, options, jPath, indentation) {
3444
+ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) {
2011
3445
  let xmlStr = "";
2012
3446
  let isPreviousElementTag = false;
2013
3447
 
3448
+ if (options.maxNestedTags && matcher.getDepth() > options.maxNestedTags) {
3449
+ throw new Error("Maximum nested tags exceeded");
3450
+ }
3451
+
3452
+ if (!Array.isArray(arr)) {
3453
+ // Non-array values (e.g. string tag values) should be treated as text content
3454
+ if (arr !== undefined && arr !== null) {
3455
+ let text = arr.toString();
3456
+ text = replaceEntitiesValue(text, options);
3457
+ return text;
3458
+ }
3459
+ return "";
3460
+ }
3461
+
2014
3462
  for (let i = 0; i < arr.length; i++) {
2015
3463
  const tagObj = arr[i];
2016
3464
  const tagName = propName(tagObj);
2017
- if(tagName === undefined) continue;
3465
+ if (tagName === undefined) continue;
2018
3466
 
2019
- let newJPath = "";
2020
- if (jPath.length === 0) newJPath = tagName;
2021
- else newJPath = `${jPath}.${tagName}`;
3467
+ // Extract attributes from ":@" property
3468
+ const attrValues = extractAttributeValues(tagObj[":@"], options);
3469
+
3470
+ // Push tag to matcher WITH attributes
3471
+ matcher.push(tagName, attrValues);
3472
+
3473
+ // Check if this is a stop node using Expression matching
3474
+ const isStopNode = checkStopNode(matcher, stopNodeExpressions);
2022
3475
 
2023
3476
  if (tagName === options.textNodeName) {
2024
3477
  let tagText = tagObj[tagName];
2025
- if (!isStopNode(newJPath, options)) {
3478
+ if (!isStopNode) {
2026
3479
  tagText = options.tagValueProcessor(tagName, tagText);
2027
3480
  tagText = replaceEntitiesValue(tagText, options);
2028
3481
  }
@@ -2031,6 +3484,7 @@ function arrToStr(arr, options, jPath, indentation) {
2031
3484
  }
2032
3485
  xmlStr += tagText;
2033
3486
  isPreviousElementTag = false;
3487
+ matcher.pop();
2034
3488
  continue;
2035
3489
  } else if (tagName === options.cdataPropName) {
2036
3490
  if (isPreviousElementTag) {
@@ -2038,27 +3492,42 @@ function arrToStr(arr, options, jPath, indentation) {
2038
3492
  }
2039
3493
  xmlStr += `<![CDATA[${tagObj[tagName][0][options.textNodeName]}]]>`;
2040
3494
  isPreviousElementTag = false;
3495
+ matcher.pop();
2041
3496
  continue;
2042
3497
  } else if (tagName === options.commentPropName) {
2043
3498
  xmlStr += indentation + `<!--${tagObj[tagName][0][options.textNodeName]}-->`;
2044
3499
  isPreviousElementTag = true;
3500
+ matcher.pop();
2045
3501
  continue;
2046
3502
  } else if (tagName[0] === "?") {
2047
- const attStr = attr_to_str(tagObj[":@"], options);
3503
+ const attStr = attr_to_str(tagObj[":@"], options, isStopNode);
2048
3504
  const tempInd = tagName === "?xml" ? "" : indentation;
2049
3505
  let piTextNodeName = tagObj[tagName][0][options.textNodeName];
2050
3506
  piTextNodeName = piTextNodeName.length !== 0 ? " " + piTextNodeName : ""; //remove extra spacing
2051
3507
  xmlStr += tempInd + `<${tagName}${piTextNodeName}${attStr}?>`;
2052
3508
  isPreviousElementTag = true;
3509
+ matcher.pop();
2053
3510
  continue;
2054
3511
  }
3512
+
2055
3513
  let newIdentation = indentation;
2056
3514
  if (newIdentation !== "") {
2057
3515
  newIdentation += options.indentBy;
2058
3516
  }
2059
- const attStr = attr_to_str(tagObj[":@"], options);
3517
+
3518
+ // Pass isStopNode to attr_to_str so attributes are also not processed for stopNodes
3519
+ const attStr = attr_to_str(tagObj[":@"], options, isStopNode);
2060
3520
  const tagStart = indentation + `<${tagName}${attStr}`;
2061
- const tagValue = arrToStr(tagObj[tagName], options, newJPath, newIdentation);
3521
+
3522
+ // If this is a stopNode, get raw content without processing
3523
+ let tagValue;
3524
+ if (isStopNode) {
3525
+ tagValue = getRawContent(tagObj[tagName], options);
3526
+ } else {
3527
+
3528
+ tagValue = arrToStr(tagObj[tagName], options, newIdentation, matcher, stopNodeExpressions);
3529
+ }
3530
+
2062
3531
  if (options.unpairedTags.indexOf(tagName) !== -1) {
2063
3532
  if (options.suppressUnpairedNode) xmlStr += tagStart + ">";
2064
3533
  else xmlStr += tagStart + "/>";
@@ -2076,27 +3545,129 @@ function arrToStr(arr, options, jPath, indentation) {
2076
3545
  xmlStr += `</${tagName}>`;
2077
3546
  }
2078
3547
  isPreviousElementTag = true;
3548
+
3549
+ // Pop tag from matcher
3550
+ matcher.pop();
2079
3551
  }
2080
3552
 
2081
3553
  return xmlStr;
2082
3554
  }
2083
3555
 
3556
+ /**
3557
+ * Extract attribute values from the ":@" object and return as plain object
3558
+ * for passing to matcher.push()
3559
+ */
3560
+ function extractAttributeValues(attrMap, options) {
3561
+ if (!attrMap || options.ignoreAttributes) return null;
3562
+
3563
+ const attrValues = {};
3564
+ let hasAttrs = false;
3565
+
3566
+ for (let attr in attrMap) {
3567
+ if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue;
3568
+ // Remove the attribute prefix to get clean attribute name
3569
+ const cleanAttrName = attr.startsWith(options.attributeNamePrefix)
3570
+ ? attr.substr(options.attributeNamePrefix.length)
3571
+ : attr;
3572
+ attrValues[cleanAttrName] = attrMap[attr];
3573
+ hasAttrs = true;
3574
+ }
3575
+
3576
+ return hasAttrs ? attrValues : null;
3577
+ }
3578
+
3579
+ /**
3580
+ * Extract raw content from a stopNode without any processing
3581
+ * This preserves the content exactly as-is, including special characters
3582
+ */
3583
+ function getRawContent(arr, options) {
3584
+ if (!Array.isArray(arr)) {
3585
+ // Non-array values return as-is
3586
+ if (arr !== undefined && arr !== null) {
3587
+ return arr.toString();
3588
+ }
3589
+ return "";
3590
+ }
3591
+
3592
+ let content = "";
3593
+ for (let i = 0; i < arr.length; i++) {
3594
+ const item = arr[i];
3595
+ const tagName = propName(item);
3596
+
3597
+ if (tagName === options.textNodeName) {
3598
+ // Raw text content - NO processing, NO entity replacement
3599
+ content += item[tagName];
3600
+ } else if (tagName === options.cdataPropName) {
3601
+ // CDATA content
3602
+ content += item[tagName][0][options.textNodeName];
3603
+ } else if (tagName === options.commentPropName) {
3604
+ // Comment content
3605
+ content += item[tagName][0][options.textNodeName];
3606
+ } else if (tagName && tagName[0] === "?") {
3607
+ // Processing instruction - skip for stopNodes
3608
+ continue;
3609
+ } else if (tagName) {
3610
+ // Nested tags within stopNode
3611
+ // Recursively get raw content and reconstruct the tag
3612
+ // For stopNodes, we don't process attributes either
3613
+ const attStr = attr_to_str_raw(item[":@"], options);
3614
+ const nestedContent = getRawContent(item[tagName], options);
3615
+
3616
+ if (!nestedContent || nestedContent.length === 0) {
3617
+ content += `<${tagName}${attStr}/>`;
3618
+ } else {
3619
+ content += `<${tagName}${attStr}>${nestedContent}</${tagName}>`;
3620
+ }
3621
+ }
3622
+ }
3623
+ return content;
3624
+ }
3625
+
3626
+ /**
3627
+ * Build attribute string for stopNodes - NO entity replacement
3628
+ */
3629
+ function attr_to_str_raw(attrMap, options) {
3630
+ let attrStr = "";
3631
+ if (attrMap && !options.ignoreAttributes) {
3632
+ for (let attr in attrMap) {
3633
+ if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue;
3634
+ // For stopNodes, use raw value without processing
3635
+ let attrVal = attrMap[attr];
3636
+ if (attrVal === true && options.suppressBooleanAttributes) {
3637
+ attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}`;
3638
+ } else {
3639
+ attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`;
3640
+ }
3641
+ }
3642
+ }
3643
+ return attrStr;
3644
+ }
3645
+
2084
3646
  function propName(obj) {
2085
3647
  const keys = Object.keys(obj);
2086
3648
  for (let i = 0; i < keys.length; i++) {
2087
3649
  const key = keys[i];
2088
- if(!obj.hasOwnProperty(key)) continue;
3650
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
2089
3651
  if (key !== ":@") return key;
2090
3652
  }
2091
3653
  }
2092
3654
 
2093
- function attr_to_str(attrMap, options) {
3655
+ function attr_to_str(attrMap, options, isStopNode) {
2094
3656
  let attrStr = "";
2095
3657
  if (attrMap && !options.ignoreAttributes) {
2096
3658
  for (let attr in attrMap) {
2097
- if(!attrMap.hasOwnProperty(attr)) continue;
2098
- let attrVal = options.attributeValueProcessor(attr, attrMap[attr]);
2099
- attrVal = replaceEntitiesValue(attrVal, options);
3659
+ if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue;
3660
+ let attrVal;
3661
+
3662
+ if (isStopNode) {
3663
+ // For stopNodes, use raw value without any processing
3664
+ attrVal = attrMap[attr];
3665
+ } else {
3666
+ // Normal processing: apply attributeValueProcessor and entity replacement
3667
+ attrVal = options.attributeValueProcessor(attr, attrMap[attr]);
3668
+ attrVal = replaceEntitiesValue(attrVal, options);
3669
+ }
3670
+
2100
3671
  if (attrVal === true && options.suppressBooleanAttributes) {
2101
3672
  attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}`;
2102
3673
  } else {
@@ -2107,11 +3678,13 @@ function attr_to_str(attrMap, options) {
2107
3678
  return attrStr;
2108
3679
  }
2109
3680
 
2110
- function isStopNode(jPath, options) {
2111
- jPath = jPath.substr(0, jPath.length - options.textNodeName.length - 1);
2112
- let tagName = jPath.substr(jPath.lastIndexOf(".") + 1);
2113
- for (let index in options.stopNodes) {
2114
- if (options.stopNodes[index] === jPath || options.stopNodes[index] === "*." + tagName) return true;
3681
+ function checkStopNode(matcher, stopNodeExpressions) {
3682
+ if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false;
3683
+
3684
+ for (let i = 0; i < stopNodeExpressions.length; i++) {
3685
+ if (matcher.matches(stopNodeExpressions[i])) {
3686
+ return true;
3687
+ }
2115
3688
  }
2116
3689
  return false;
2117
3690
  }
@@ -2126,6 +3699,25 @@ function replaceEntitiesValue(textValue, options) {
2126
3699
  return textValue;
2127
3700
  }
2128
3701
 
3702
+ function getIgnoreAttributesFn(ignoreAttributes) {
3703
+ if (typeof ignoreAttributes === 'function') {
3704
+ return ignoreAttributes
3705
+ }
3706
+ if (Array.isArray(ignoreAttributes)) {
3707
+ return (attrName) => {
3708
+ for (const pattern of ignoreAttributes) {
3709
+ if (typeof pattern === 'string' && attrName === pattern) {
3710
+ return true
3711
+ }
3712
+ if (pattern instanceof RegExp && pattern.test(attrName)) {
3713
+ return true
3714
+ }
3715
+ }
3716
+ }
3717
+ }
3718
+ return () => false
3719
+ }
3720
+
2129
3721
  const defaultOptions = {
2130
3722
  attributeNamePrefix: '@_',
2131
3723
  attributesGroupName: false,
@@ -2137,10 +3729,10 @@ const defaultOptions = {
2137
3729
  suppressEmptyNode: false,
2138
3730
  suppressUnpairedNode: true,
2139
3731
  suppressBooleanAttributes: true,
2140
- tagValueProcessor: function(key, a) {
3732
+ tagValueProcessor: function (key, a) {
2141
3733
  return a;
2142
3734
  },
2143
- attributeValueProcessor: function(attrName, a) {
3735
+ attributeValueProcessor: function (attrName, a) {
2144
3736
  return a;
2145
3737
  },
2146
3738
  preserveOrder: false,
@@ -2157,13 +3749,42 @@ const defaultOptions = {
2157
3749
  stopNodes: [],
2158
3750
  // transformTagName: false,
2159
3751
  // transformAttributeName: false,
2160
- oneListGroup: false
3752
+ oneListGroup: false,
3753
+ maxNestedTags: 100,
3754
+ jPath: true // When true, callbacks receive string jPath; when false, receive Matcher instance
2161
3755
  };
2162
3756
 
2163
3757
  function Builder(options) {
2164
3758
  this.options = Object.assign({}, defaultOptions, options);
3759
+
3760
+ // Convert old-style stopNodes for backward compatibility
3761
+ // Old syntax: "*.tag" meant "tag anywhere in tree"
3762
+ // New syntax: "..tag" means "tag anywhere in tree"
3763
+ if (this.options.stopNodes && Array.isArray(this.options.stopNodes)) {
3764
+ this.options.stopNodes = this.options.stopNodes.map(node => {
3765
+ if (typeof node === 'string' && node.startsWith('*.')) {
3766
+ // Convert old wildcard syntax to deep wildcard
3767
+ return '..' + node.substring(2);
3768
+ }
3769
+ return node;
3770
+ });
3771
+ }
3772
+
3773
+ // Pre-compile stopNode expressions for pattern matching
3774
+ this.stopNodeExpressions = [];
3775
+ if (this.options.stopNodes && Array.isArray(this.options.stopNodes)) {
3776
+ for (let i = 0; i < this.options.stopNodes.length; i++) {
3777
+ const node = this.options.stopNodes[i];
3778
+ if (typeof node === 'string') {
3779
+ this.stopNodeExpressions.push(new Expression(node));
3780
+ } else if (node instanceof Expression) {
3781
+ this.stopNodeExpressions.push(node);
3782
+ }
3783
+ }
3784
+ }
3785
+
2165
3786
  if (this.options.ignoreAttributes === true || this.options.attributesGroupName) {
2166
- this.isAttribute = function(/*a*/) {
3787
+ this.isAttribute = function (/*a*/) {
2167
3788
  return false;
2168
3789
  };
2169
3790
  } else {
@@ -2179,7 +3800,7 @@ function Builder(options) {
2179
3800
  this.tagEndChar = '>\n';
2180
3801
  this.newLine = '\n';
2181
3802
  } else {
2182
- this.indentate = function() {
3803
+ this.indentate = function () {
2183
3804
  return '';
2184
3805
  };
2185
3806
  this.tagEndChar = '>';
@@ -2187,25 +3808,35 @@ function Builder(options) {
2187
3808
  }
2188
3809
  }
2189
3810
 
2190
- Builder.prototype.build = function(jObj) {
2191
- if(this.options.preserveOrder){
3811
+ Builder.prototype.build = function (jObj) {
3812
+ if (this.options.preserveOrder) {
2192
3813
  return toXml(jObj, this.options);
2193
- }else {
2194
- if(Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1){
3814
+ } else {
3815
+ if (Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1) {
2195
3816
  jObj = {
2196
- [this.options.arrayNodeName] : jObj
3817
+ [this.options.arrayNodeName]: jObj
2197
3818
  };
2198
3819
  }
2199
- return this.j2x(jObj, 0, []).val;
3820
+ // Initialize matcher for path tracking
3821
+ const matcher = new Matcher();
3822
+ return this.j2x(jObj, 0, matcher).val;
2200
3823
  }
2201
3824
  };
2202
3825
 
2203
- Builder.prototype.j2x = function(jObj, level, ajPath) {
3826
+ Builder.prototype.j2x = function (jObj, level, matcher) {
2204
3827
  let attrStr = '';
2205
3828
  let val = '';
2206
- const jPath = ajPath.join('.');
3829
+ if (this.options.maxNestedTags && matcher.getDepth() >= this.options.maxNestedTags) {
3830
+ throw new Error("Maximum nested tags exceeded");
3831
+ }
3832
+ // Get jPath based on option: string for backward compatibility, or Matcher for new features
3833
+ const jPath = this.options.jPath ? matcher.toString() : matcher;
3834
+
3835
+ // Check if current node is a stopNode (will be used for attribute encoding)
3836
+ const isCurrentStopNode = this.checkStopNode(matcher);
3837
+
2207
3838
  for (let key in jObj) {
2208
- if(!Object.prototype.hasOwnProperty.call(jObj, key)) continue;
3839
+ if (!Object.prototype.hasOwnProperty.call(jObj, key)) continue;
2209
3840
  if (typeof jObj[key] === 'undefined') {
2210
3841
  // supress undefined node only if it is not an attribute
2211
3842
  if (this.isAttribute(key)) {
@@ -2224,19 +3855,34 @@ Builder.prototype.j2x = function(jObj, level, ajPath) {
2224
3855
  }
2225
3856
  // val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
2226
3857
  } else if (jObj[key] instanceof Date) {
2227
- val += this.buildTextValNode(jObj[key], key, '', level);
3858
+ val += this.buildTextValNode(jObj[key], key, '', level, matcher);
2228
3859
  } else if (typeof jObj[key] !== 'object') {
2229
3860
  //premitive type
2230
3861
  const attr = this.isAttribute(key);
2231
3862
  if (attr && !this.ignoreAttributesFn(attr, jPath)) {
2232
- attrStr += this.buildAttrPairStr(attr, '' + jObj[key]);
3863
+ attrStr += this.buildAttrPairStr(attr, '' + jObj[key], isCurrentStopNode);
2233
3864
  } else if (!attr) {
2234
3865
  //tag value
2235
3866
  if (key === this.options.textNodeName) {
2236
3867
  let newval = this.options.tagValueProcessor(key, '' + jObj[key]);
2237
3868
  val += this.replaceEntitiesValue(newval);
2238
3869
  } else {
2239
- val += this.buildTextValNode(jObj[key], key, '', level);
3870
+ // Check if this is a stopNode before building
3871
+ matcher.push(key);
3872
+ const isStopNode = this.checkStopNode(matcher);
3873
+ matcher.pop();
3874
+
3875
+ if (isStopNode) {
3876
+ // Build as raw content without encoding
3877
+ const textValue = '' + jObj[key];
3878
+ if (textValue === '') {
3879
+ val += this.indentate(level) + '<' + key + this.closeTag(key) + this.tagEndChar;
3880
+ } else {
3881
+ val += this.indentate(level) + '<' + key + '>' + textValue + '</' + key + this.tagEndChar;
3882
+ }
3883
+ } else {
3884
+ val += this.buildTextValNode(jObj[key], key, '', level, matcher);
3885
+ }
2240
3886
  }
2241
3887
  }
2242
3888
  } else if (Array.isArray(jObj[key])) {
@@ -2247,18 +3893,23 @@ Builder.prototype.j2x = function(jObj, level, ajPath) {
2247
3893
  for (let j = 0; j < arrLen; j++) {
2248
3894
  const item = jObj[key][j];
2249
3895
  if (typeof item === 'undefined') ; else if (item === null) {
2250
- if(key[0] === "?") val += this.indentate(level) + '<' + key + '?' + this.tagEndChar;
3896
+ if (key[0] === "?") val += this.indentate(level) + '<' + key + '?' + this.tagEndChar;
2251
3897
  else val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
2252
3898
  // val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
2253
3899
  } else if (typeof item === 'object') {
2254
- if(this.options.oneListGroup){
2255
- const result = this.j2x(item, level + 1, ajPath.concat(key));
3900
+ if (this.options.oneListGroup) {
3901
+ // Push tag to matcher before recursive call
3902
+ matcher.push(key);
3903
+ const result = this.j2x(item, level + 1, matcher);
3904
+ // Pop tag from matcher after recursive call
3905
+ matcher.pop();
3906
+
2256
3907
  listTagVal += result.val;
2257
3908
  if (this.options.attributesGroupName && item.hasOwnProperty(this.options.attributesGroupName)) {
2258
3909
  listTagAttr += result.attrStr;
2259
3910
  }
2260
- }else {
2261
- listTagVal += this.processTextOrObjNode(item, key, level, ajPath);
3911
+ } else {
3912
+ listTagVal += this.processTextOrObjNode(item, key, level, matcher);
2262
3913
  }
2263
3914
  } else {
2264
3915
  if (this.options.oneListGroup) {
@@ -2266,11 +3917,26 @@ Builder.prototype.j2x = function(jObj, level, ajPath) {
2266
3917
  textValue = this.replaceEntitiesValue(textValue);
2267
3918
  listTagVal += textValue;
2268
3919
  } else {
2269
- listTagVal += this.buildTextValNode(item, key, '', level);
3920
+ // Check if this is a stopNode before building
3921
+ matcher.push(key);
3922
+ const isStopNode = this.checkStopNode(matcher);
3923
+ matcher.pop();
3924
+
3925
+ if (isStopNode) {
3926
+ // Build as raw content without encoding
3927
+ const textValue = '' + item;
3928
+ if (textValue === '') {
3929
+ listTagVal += this.indentate(level) + '<' + key + this.closeTag(key) + this.tagEndChar;
3930
+ } else {
3931
+ listTagVal += this.indentate(level) + '<' + key + '>' + textValue + '</' + key + this.tagEndChar;
3932
+ }
3933
+ } else {
3934
+ listTagVal += this.buildTextValNode(item, key, '', level, matcher);
3935
+ }
2270
3936
  }
2271
3937
  }
2272
3938
  }
2273
- if(this.options.oneListGroup){
3939
+ if (this.options.oneListGroup) {
2274
3940
  listTagVal = this.buildObjectNode(listTagVal, key, listTagAttr, level);
2275
3941
  }
2276
3942
  val += listTagVal;
@@ -2280,99 +3946,269 @@ Builder.prototype.j2x = function(jObj, level, ajPath) {
2280
3946
  const Ks = Object.keys(jObj[key]);
2281
3947
  const L = Ks.length;
2282
3948
  for (let j = 0; j < L; j++) {
2283
- attrStr += this.buildAttrPairStr(Ks[j], '' + jObj[key][Ks[j]]);
3949
+ attrStr += this.buildAttrPairStr(Ks[j], '' + jObj[key][Ks[j]], isCurrentStopNode);
2284
3950
  }
2285
3951
  } else {
2286
- val += this.processTextOrObjNode(jObj[key], key, level, ajPath);
3952
+ val += this.processTextOrObjNode(jObj[key], key, level, matcher);
2287
3953
  }
2288
3954
  }
2289
3955
  }
2290
- return {attrStr: attrStr, val: val};
3956
+ return { attrStr: attrStr, val: val };
2291
3957
  };
2292
3958
 
2293
- Builder.prototype.buildAttrPairStr = function(attrName, val){
2294
- val = this.options.attributeValueProcessor(attrName, '' + val);
2295
- val = this.replaceEntitiesValue(val);
3959
+ Builder.prototype.buildAttrPairStr = function (attrName, val, isStopNode) {
3960
+ if (!isStopNode) {
3961
+ val = this.options.attributeValueProcessor(attrName, '' + val);
3962
+ val = this.replaceEntitiesValue(val);
3963
+ }
2296
3964
  if (this.options.suppressBooleanAttributes && val === "true") {
2297
3965
  return ' ' + attrName;
2298
3966
  } else return ' ' + attrName + '="' + val + '"';
2299
3967
  };
2300
3968
 
2301
- function processTextOrObjNode (object, key, level, ajPath) {
2302
- const result = this.j2x(object, level + 1, ajPath.concat(key));
3969
+ function processTextOrObjNode(object, key, level, matcher) {
3970
+ // Extract attributes to pass to matcher
3971
+ const attrValues = this.extractAttributes(object);
3972
+
3973
+ // Push tag to matcher before recursion WITH attributes
3974
+ matcher.push(key, attrValues);
3975
+
3976
+ // Check if this entire node is a stopNode
3977
+ const isStopNode = this.checkStopNode(matcher);
3978
+
3979
+ if (isStopNode) {
3980
+ // For stopNodes, build raw content without entity encoding
3981
+ const rawContent = this.buildRawContent(object);
3982
+ const attrStr = this.buildAttributesForStopNode(object);
3983
+ matcher.pop();
3984
+ return this.buildObjectNode(rawContent, key, attrStr, level);
3985
+ }
3986
+
3987
+ const result = this.j2x(object, level + 1, matcher);
3988
+ // Pop tag from matcher after recursion
3989
+ matcher.pop();
3990
+
2303
3991
  if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) {
2304
- return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level);
3992
+ return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level, matcher);
2305
3993
  } else {
2306
3994
  return this.buildObjectNode(result.val, key, result.attrStr, level);
2307
3995
  }
2308
3996
  }
2309
3997
 
2310
- Builder.prototype.buildObjectNode = function(val, key, attrStr, level) {
2311
- if(val === ""){
2312
- if(key[0] === "?") return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar;
3998
+ // Helper method to extract attributes from an object
3999
+ Builder.prototype.extractAttributes = function (obj) {
4000
+ if (!obj || typeof obj !== 'object') return null;
4001
+
4002
+ const attrValues = {};
4003
+ let hasAttrs = false;
4004
+
4005
+ // Check for attributesGroupName (when attributes are grouped)
4006
+ if (this.options.attributesGroupName && obj[this.options.attributesGroupName]) {
4007
+ const attrGroup = obj[this.options.attributesGroupName];
4008
+ for (let attrKey in attrGroup) {
4009
+ if (!Object.prototype.hasOwnProperty.call(attrGroup, attrKey)) continue;
4010
+ // Remove attribute prefix if present
4011
+ const cleanKey = attrKey.startsWith(this.options.attributeNamePrefix)
4012
+ ? attrKey.substring(this.options.attributeNamePrefix.length)
4013
+ : attrKey;
4014
+ attrValues[cleanKey] = attrGroup[attrKey];
4015
+ hasAttrs = true;
4016
+ }
4017
+ } else {
4018
+ // Look for individual attributes (prefixed with attributeNamePrefix)
4019
+ for (let key in obj) {
4020
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
4021
+ const attr = this.isAttribute(key);
4022
+ if (attr) {
4023
+ attrValues[attr] = obj[key];
4024
+ hasAttrs = true;
4025
+ }
4026
+ }
4027
+ }
4028
+
4029
+ return hasAttrs ? attrValues : null;
4030
+ };
4031
+
4032
+ // Build raw content for stopNode without entity encoding
4033
+ Builder.prototype.buildRawContent = function (obj) {
4034
+ if (typeof obj === 'string') {
4035
+ return obj; // Already a string, return as-is
4036
+ }
4037
+
4038
+ if (typeof obj !== 'object' || obj === null) {
4039
+ return String(obj);
4040
+ }
4041
+
4042
+ // Check if this is a stopNode data from parser: { "#text": "raw xml", "@_attr": "val" }
4043
+ if (obj[this.options.textNodeName] !== undefined) {
4044
+ return obj[this.options.textNodeName]; // Return raw text without encoding
4045
+ }
4046
+
4047
+ // Build raw XML from nested structure
4048
+ let content = '';
4049
+
4050
+ for (let key in obj) {
4051
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
4052
+
4053
+ // Skip attributes
4054
+ if (this.isAttribute(key)) continue;
4055
+ if (this.options.attributesGroupName && key === this.options.attributesGroupName) continue;
4056
+
4057
+ const value = obj[key];
4058
+
4059
+ if (key === this.options.textNodeName) {
4060
+ content += value; // Raw text
4061
+ } else if (Array.isArray(value)) {
4062
+ // Array of same tag
4063
+ for (let item of value) {
4064
+ if (typeof item === 'string' || typeof item === 'number') {
4065
+ content += `<${key}>${item}</${key}>`;
4066
+ } else if (typeof item === 'object' && item !== null) {
4067
+ const nestedContent = this.buildRawContent(item);
4068
+ const nestedAttrs = this.buildAttributesForStopNode(item);
4069
+ if (nestedContent === '') {
4070
+ content += `<${key}${nestedAttrs}/>`;
4071
+ } else {
4072
+ content += `<${key}${nestedAttrs}>${nestedContent}</${key}>`;
4073
+ }
4074
+ }
4075
+ }
4076
+ } else if (typeof value === 'object' && value !== null) {
4077
+ // Nested object
4078
+ const nestedContent = this.buildRawContent(value);
4079
+ const nestedAttrs = this.buildAttributesForStopNode(value);
4080
+ if (nestedContent === '') {
4081
+ content += `<${key}${nestedAttrs}/>`;
4082
+ } else {
4083
+ content += `<${key}${nestedAttrs}>${nestedContent}</${key}>`;
4084
+ }
4085
+ } else {
4086
+ // Primitive value
4087
+ content += `<${key}>${value}</${key}>`;
4088
+ }
4089
+ }
4090
+
4091
+ return content;
4092
+ };
4093
+
4094
+ // Build attribute string for stopNode (no entity encoding)
4095
+ Builder.prototype.buildAttributesForStopNode = function (obj) {
4096
+ if (!obj || typeof obj !== 'object') return '';
4097
+
4098
+ let attrStr = '';
4099
+
4100
+ // Check for attributesGroupName (when attributes are grouped)
4101
+ if (this.options.attributesGroupName && obj[this.options.attributesGroupName]) {
4102
+ const attrGroup = obj[this.options.attributesGroupName];
4103
+ for (let attrKey in attrGroup) {
4104
+ if (!Object.prototype.hasOwnProperty.call(attrGroup, attrKey)) continue;
4105
+ const cleanKey = attrKey.startsWith(this.options.attributeNamePrefix)
4106
+ ? attrKey.substring(this.options.attributeNamePrefix.length)
4107
+ : attrKey;
4108
+ const val = attrGroup[attrKey];
4109
+ if (val === true && this.options.suppressBooleanAttributes) {
4110
+ attrStr += ' ' + cleanKey;
4111
+ } else {
4112
+ attrStr += ' ' + cleanKey + '="' + val + '"'; // No encoding for stopNode
4113
+ }
4114
+ }
4115
+ } else {
4116
+ // Look for individual attributes
4117
+ for (let key in obj) {
4118
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
4119
+ const attr = this.isAttribute(key);
4120
+ if (attr) {
4121
+ const val = obj[key];
4122
+ if (val === true && this.options.suppressBooleanAttributes) {
4123
+ attrStr += ' ' + attr;
4124
+ } else {
4125
+ attrStr += ' ' + attr + '="' + val + '"'; // No encoding for stopNode
4126
+ }
4127
+ }
4128
+ }
4129
+ }
4130
+
4131
+ return attrStr;
4132
+ };
4133
+
4134
+ Builder.prototype.buildObjectNode = function (val, key, attrStr, level) {
4135
+ if (val === "") {
4136
+ if (key[0] === "?") return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar;
2313
4137
  else {
2314
4138
  return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;
2315
4139
  }
2316
- }else {
4140
+ } else {
2317
4141
 
2318
4142
  let tagEndExp = '</' + key + this.tagEndChar;
2319
4143
  let piClosingChar = "";
2320
-
2321
- if(key[0] === "?") {
4144
+
4145
+ if (key[0] === "?") {
2322
4146
  piClosingChar = "?";
2323
4147
  tagEndExp = "";
2324
4148
  }
2325
-
4149
+
2326
4150
  // attrStr is an empty string in case the attribute came as undefined or null
2327
4151
  if ((attrStr || attrStr === '') && val.indexOf('<') === -1) {
2328
- return ( this.indentate(level) + '<' + key + attrStr + piClosingChar + '>' + val + tagEndExp );
4152
+ return (this.indentate(level) + '<' + key + attrStr + piClosingChar + '>' + val + tagEndExp);
2329
4153
  } else if (this.options.commentPropName !== false && key === this.options.commentPropName && piClosingChar.length === 0) {
2330
4154
  return this.indentate(level) + `<!--${val}-->` + this.newLine;
2331
- }else {
4155
+ } else {
2332
4156
  return (
2333
4157
  this.indentate(level) + '<' + key + attrStr + piClosingChar + this.tagEndChar +
2334
4158
  val +
2335
- this.indentate(level) + tagEndExp );
4159
+ this.indentate(level) + tagEndExp);
2336
4160
  }
2337
4161
  }
2338
4162
  };
2339
4163
 
2340
- Builder.prototype.closeTag = function(key){
4164
+ Builder.prototype.closeTag = function (key) {
2341
4165
  let closeTag = "";
2342
- if(this.options.unpairedTags.indexOf(key) !== -1){ //unpaired
2343
- if(!this.options.suppressUnpairedNode) closeTag = "/";
2344
- }else if(this.options.suppressEmptyNode){ //empty
4166
+ if (this.options.unpairedTags.indexOf(key) !== -1) { //unpaired
4167
+ if (!this.options.suppressUnpairedNode) closeTag = "/";
4168
+ } else if (this.options.suppressEmptyNode) { //empty
2345
4169
  closeTag = "/";
2346
- }else {
4170
+ } else {
2347
4171
  closeTag = `></${key}`;
2348
4172
  }
2349
4173
  return closeTag;
2350
4174
  };
2351
4175
 
2352
- Builder.prototype.buildTextValNode = function(val, key, attrStr, level) {
4176
+ Builder.prototype.checkStopNode = function (matcher) {
4177
+ if (!this.stopNodeExpressions || this.stopNodeExpressions.length === 0) return false;
4178
+
4179
+ for (let i = 0; i < this.stopNodeExpressions.length; i++) {
4180
+ if (matcher.matches(this.stopNodeExpressions[i])) {
4181
+ return true;
4182
+ }
4183
+ }
4184
+ return false;
4185
+ };
4186
+
4187
+ Builder.prototype.buildTextValNode = function (val, key, attrStr, level, matcher) {
2353
4188
  if (this.options.cdataPropName !== false && key === this.options.cdataPropName) {
2354
- return this.indentate(level) + `<![CDATA[${val}]]>` + this.newLine;
2355
- }else if (this.options.commentPropName !== false && key === this.options.commentPropName) {
2356
- return this.indentate(level) + `<!--${val}-->` + this.newLine;
2357
- }else if(key[0] === "?") {//PI tag
2358
- return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar;
2359
- }else {
4189
+ return this.indentate(level) + `<![CDATA[${val}]]>` + this.newLine;
4190
+ } else if (this.options.commentPropName !== false && key === this.options.commentPropName) {
4191
+ return this.indentate(level) + `<!--${val}-->` + this.newLine;
4192
+ } else if (key[0] === "?") {//PI tag
4193
+ return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar;
4194
+ } else {
4195
+ // Normal processing: apply tagValueProcessor and entity replacement
2360
4196
  let textValue = this.options.tagValueProcessor(key, val);
2361
4197
  textValue = this.replaceEntitiesValue(textValue);
2362
-
2363
- if( textValue === ''){
4198
+
4199
+ if (textValue === '') {
2364
4200
  return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;
2365
- }else {
4201
+ } else {
2366
4202
  return this.indentate(level) + '<' + key + attrStr + '>' +
2367
- textValue +
4203
+ textValue +
2368
4204
  '</' + key + this.tagEndChar;
2369
4205
  }
2370
4206
  }
2371
4207
  };
2372
4208
 
2373
- Builder.prototype.replaceEntitiesValue = function(textValue){
2374
- if(textValue && textValue.length > 0 && this.options.processEntities){
2375
- for (let i=0; i<this.options.entities.length; i++) {
4209
+ Builder.prototype.replaceEntitiesValue = function (textValue) {
4210
+ if (textValue && textValue.length > 0 && this.options.processEntities) {
4211
+ for (let i = 0; i < this.options.entities.length; i++) {
2376
4212
  const entity = this.options.entities[i];
2377
4213
  textValue = textValue.replace(entity.regex, entity.val);
2378
4214
  }
@@ -2398,10 +4234,7 @@ const ISO20022Messages = {
2398
4234
  CAMT_005: 'CAMT.005',
2399
4235
  CAMT_006: 'CAMT.006',
2400
4236
  CAMT_052: 'CAMT.052',
2401
- CAMT_053: 'CAMT.053',
2402
- PAIN_001: 'PAIN.001',
2403
- PAIN_002: 'PAIN.002',
2404
- };
4237
+ CAMT_053: 'CAMT.053'};
2405
4238
  const ISO20022Implementations = new Map();
2406
4239
  function registerISO20022Implementation(cl) {
2407
4240
  cl.supportedMessages().forEach(msg => {
@@ -7494,15 +9327,15 @@ const exportAccountIdentification = (accountId) => {
7494
9327
  };
7495
9328
  // TODO: Add both BIC and ABA routing numbers at the same time
7496
9329
  const parseAgent = (agent) => {
7497
- // Get BIC if it exists first
7498
- if (agent.FinInstnId.BIC) {
7499
- return {
7500
- bic: agent.FinInstnId.BIC,
7501
- };
9330
+ const bic = agent.FinInstnId.BICFI || agent.FinInstnId.BIC;
9331
+ if (bic) {
9332
+ return { bic };
7502
9333
  }
7503
- return {
7504
- abaRoutingNumber: (agent.FinInstnId.Othr?.Id || agent.FinInstnId.ClrSysMmbId.MmbId).toString(),
7505
- };
9334
+ const aba = agent.FinInstnId.Othr?.Id || agent.FinInstnId.ClrSysMmbId?.MmbId;
9335
+ if (aba != null) {
9336
+ return { abaRoutingNumber: String(aba) };
9337
+ }
9338
+ throw new Error('Unable to parse agent: no BIC, BICFI, Othr.Id, or ClrSysMmbId.MmbId present');
7506
9339
  };
7507
9340
  const exportAgent = (agent) => {
7508
9341
  const obj = {
@@ -7645,7 +9478,7 @@ var native = {
7645
9478
  };
7646
9479
 
7647
9480
  function v4(options, buf, offset) {
7648
- if (native.randomUUID && !buf && !options) {
9481
+ if (native.randomUUID && true && !options) {
7649
9482
  return native.randomUUID();
7650
9483
  }
7651
9484
  options = options || {};