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