fast-xml-parser 5.4.2 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,8 @@ import xmlNode from './xmlNode.js';
6
6
  import DocTypeReader from './DocTypeReader.js';
7
7
  import toNumber from "strnum";
8
8
  import getIgnoreAttributesFn from "../ignoreAttributes.js";
9
+ import { Expression, Matcher } from 'path-expression-matcher';
10
+
9
11
 
10
12
  // const regx =
11
13
  // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
@@ -14,6 +16,57 @@ import getIgnoreAttributesFn from "../ignoreAttributes.js";
14
16
  //const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
15
17
  //const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");
16
18
 
19
+ // Helper functions for attribute and namespace handling
20
+
21
+ /**
22
+ * Extract raw attributes (without prefix) from prefixed attribute map
23
+ * @param {object} prefixedAttrs - Attributes with prefix from buildAttributesMap
24
+ * @param {object} options - Parser options containing attributeNamePrefix
25
+ * @returns {object} Raw attributes for matcher
26
+ */
27
+ function extractRawAttributes(prefixedAttrs, options) {
28
+ if (!prefixedAttrs) return {};
29
+
30
+ // Handle attributesGroupName option
31
+ const attrs = options.attributesGroupName
32
+ ? prefixedAttrs[options.attributesGroupName]
33
+ : prefixedAttrs;
34
+
35
+ if (!attrs) return {};
36
+
37
+ const rawAttrs = {};
38
+ for (const key in attrs) {
39
+ // Remove the attribute prefix to get raw name
40
+ if (key.startsWith(options.attributeNamePrefix)) {
41
+ const rawName = key.substring(options.attributeNamePrefix.length);
42
+ rawAttrs[rawName] = attrs[key];
43
+ } else {
44
+ // Attribute without prefix (shouldn't normally happen, but be safe)
45
+ rawAttrs[key] = attrs[key];
46
+ }
47
+ }
48
+ return rawAttrs;
49
+ }
50
+
51
+ /**
52
+ * Extract namespace from raw tag name
53
+ * @param {string} rawTagName - Tag name possibly with namespace (e.g., "soap:Envelope")
54
+ * @returns {string|undefined} Namespace or undefined
55
+ */
56
+ function extractNamespace(rawTagName) {
57
+ if (!rawTagName || typeof rawTagName !== 'string') return undefined;
58
+
59
+ const colonIndex = rawTagName.indexOf(':');
60
+ if (colonIndex !== -1 && colonIndex > 0) {
61
+ const ns = rawTagName.substring(0, colonIndex);
62
+ // Don't treat xmlns as a namespace
63
+ if (ns !== 'xmlns') {
64
+ return ns;
65
+ }
66
+ }
67
+ return undefined;
68
+ }
69
+
17
70
  export default class OrderedObjParser {
18
71
  constructor(options) {
19
72
  this.options = options;
@@ -58,16 +111,23 @@ export default class OrderedObjParser {
58
111
  this.entityExpansionCount = 0;
59
112
  this.currentExpandedLength = 0;
60
113
 
114
+ // Initialize path matcher for path-expression-matcher
115
+ this.matcher = new Matcher();
116
+
117
+ // Flag to track if current node is a stop node (optimization)
118
+ this.isCurrentNodeStopNode = false;
119
+
120
+ // Pre-compile stopNodes expressions
61
121
  if (this.options.stopNodes && this.options.stopNodes.length > 0) {
62
- this.stopNodesExact = new Set();
63
- this.stopNodesWildcard = new Set();
122
+ this.stopNodeExpressions = [];
64
123
  for (let i = 0; i < this.options.stopNodes.length; i++) {
65
124
  const stopNodeExp = this.options.stopNodes[i];
66
- if (typeof stopNodeExp !== 'string') continue;
67
- if (stopNodeExp.startsWith("*.")) {
68
- this.stopNodesWildcard.add(stopNodeExp.substring(2));
69
- } else {
70
- this.stopNodesExact.add(stopNodeExp);
125
+ if (typeof stopNodeExp === 'string') {
126
+ // Convert string to Expression object
127
+ this.stopNodeExpressions.push(new Expression(stopNodeExp));
128
+ } else if (stopNodeExp instanceof Expression) {
129
+ // Already an Expression object
130
+ this.stopNodeExpressions.push(stopNodeExp);
71
131
  }
72
132
  }
73
133
  }
@@ -90,7 +150,7 @@ function addExternalEntities(externalEntities) {
90
150
  /**
91
151
  * @param {string} val
92
152
  * @param {string} tagName
93
- * @param {string} jPath
153
+ * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
94
154
  * @param {boolean} dontTrim
95
155
  * @param {boolean} hasAttributes
96
156
  * @param {boolean} isLeafNode
@@ -104,7 +164,9 @@ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode,
104
164
  if (val.length > 0) {
105
165
  if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath);
106
166
 
107
- const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode);
167
+ // Pass jPath string or matcher based on options.jPath setting
168
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
169
+ const newval = this.options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode);
108
170
  if (newval === null || newval === undefined) {
109
171
  //don't parse
110
172
  return val;
@@ -151,13 +213,42 @@ function buildAttributesMap(attrStr, jPath, tagName) {
151
213
  const matches = getAllMatches(attrStr, attrsRegx);
152
214
  const len = matches.length; //don't make it inline
153
215
  const attrs = {};
216
+
217
+ // First pass: parse all attributes and update matcher with raw values
218
+ // This ensures the matcher has all attribute values when processors run
219
+ const rawAttrsForMatcher = {};
220
+ for (let i = 0; i < len; i++) {
221
+ const attrName = this.resolveNameSpace(matches[i][1]);
222
+ const oldVal = matches[i][4];
223
+
224
+ if (attrName.length && oldVal !== undefined) {
225
+ let parsedVal = oldVal;
226
+ if (this.options.trimValues) {
227
+ parsedVal = parsedVal.trim();
228
+ }
229
+ parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath);
230
+ rawAttrsForMatcher[attrName] = parsedVal;
231
+ }
232
+ }
233
+
234
+ // Update matcher with raw attribute values BEFORE running processors
235
+ if (Object.keys(rawAttrsForMatcher).length > 0 && typeof jPath === 'object' && jPath.updateCurrent) {
236
+ jPath.updateCurrent(rawAttrsForMatcher);
237
+ }
238
+
239
+ // Second pass: now process attributes with matcher having full attribute context
154
240
  for (let i = 0; i < len; i++) {
155
241
  const attrName = this.resolveNameSpace(matches[i][1]);
156
- if (this.ignoreAttributesFn(attrName, jPath)) {
242
+
243
+ // Convert jPath to string if needed for ignoreAttributesFn
244
+ const jPathStr = this.options.jPath ? jPath.toString() : jPath;
245
+ if (this.ignoreAttributesFn(attrName, jPathStr)) {
157
246
  continue
158
247
  }
248
+
159
249
  let oldVal = matches[i][4];
160
250
  let aName = this.options.attributeNamePrefix + attrName;
251
+
161
252
  if (attrName.length) {
162
253
  if (this.options.transformAttributeName) {
163
254
  aName = this.options.transformAttributeName(aName);
@@ -169,7 +260,10 @@ function buildAttributesMap(attrStr, jPath, tagName) {
169
260
  oldVal = oldVal.trim();
170
261
  }
171
262
  oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);
172
- const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath);
263
+
264
+ // Pass jPath string or matcher based on options.jPath setting
265
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
266
+ const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher);
173
267
  if (newVal === null || newVal === undefined) {
174
268
  //don't parse
175
269
  attrs[aName] = oldVal;
@@ -189,6 +283,7 @@ function buildAttributesMap(attrStr, jPath, tagName) {
189
283
  }
190
284
  }
191
285
  }
286
+
192
287
  if (!Object.keys(attrs).length) {
193
288
  return;
194
289
  }
@@ -206,7 +301,9 @@ const parseXml = function (xmlData) {
206
301
  const xmlObj = new xmlNode('!xml');
207
302
  let currentNode = xmlObj;
208
303
  let textData = "";
209
- let jPath = "";
304
+
305
+ // Reset matcher for new document
306
+ this.matcher.reset();
210
307
 
211
308
  // Reset entity expansion counters for this document
212
309
  this.entityExpansionCount = 0;
@@ -234,22 +331,22 @@ const parseXml = function (xmlData) {
234
331
  }
235
332
 
236
333
  if (currentNode) {
237
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
334
+ textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
238
335
  }
239
336
 
240
337
  //check if last tag of nested tag was unpaired tag
241
- const lastTagName = jPath.substring(jPath.lastIndexOf(".") + 1);
338
+ const lastTagName = this.matcher.getCurrentTag();
242
339
  if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) {
243
340
  throw new Error(`Unpaired tag can not be used as closing tag: </${tagName}>`);
244
341
  }
245
- let propIndex = 0
246
342
  if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) {
247
- propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.') - 1)
343
+ // Pop the unpaired tag
344
+ this.matcher.pop();
248
345
  this.tagsNodeStack.pop();
249
- } else {
250
- propIndex = jPath.lastIndexOf(".");
251
346
  }
252
- jPath = jPath.substring(0, propIndex);
347
+ // Pop the closing tag
348
+ this.matcher.pop();
349
+ this.isCurrentNodeStopNode = false; // Reset flag when closing tag
253
350
 
254
351
  currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope
255
352
  textData = "";
@@ -259,7 +356,7 @@ const parseXml = function (xmlData) {
259
356
  let tagData = readTagExp(xmlData, i, false, "?>");
260
357
  if (!tagData) throw new Error("Pi Tag is not closed.");
261
358
 
262
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
359
+ textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
263
360
  if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
264
361
  //do nothing
265
362
  } else {
@@ -268,9 +365,9 @@ const parseXml = function (xmlData) {
268
365
  childNode.add(this.options.textNodeName, "");
269
366
 
270
367
  if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
271
- childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);
368
+ childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
272
369
  }
273
- this.addChild(currentNode, childNode, jPath, i);
370
+ this.addChild(currentNode, childNode, this.matcher, i);
274
371
  }
275
372
 
276
373
 
@@ -280,7 +377,7 @@ const parseXml = function (xmlData) {
280
377
  if (this.options.commentPropName) {
281
378
  const comment = xmlData.substring(i + 4, endIndex - 2);
282
379
 
283
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
380
+ textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
284
381
 
285
382
  currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
286
383
  }
@@ -293,9 +390,9 @@ const parseXml = function (xmlData) {
293
390
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
294
391
  const tagExp = xmlData.substring(i + 9, closeIndex);
295
392
 
296
- textData = this.saveTextToParentTag(textData, currentNode, jPath);
393
+ textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
297
394
 
298
- let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true);
395
+ let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true);
299
396
  if (val == undefined) val = "";
300
397
 
301
398
  //cdata should be set even if it is 0 length string
@@ -308,6 +405,14 @@ const parseXml = function (xmlData) {
308
405
  i = closeIndex + 2;
309
406
  } else {//Opening tag
310
407
  let result = readTagExp(xmlData, i, this.options.removeNSPrefix);
408
+
409
+ // Safety check: readTagExp can return undefined
410
+ if (!result) {
411
+ // Log context for debugging
412
+ const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlData.length, i + 50));
413
+ throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`);
414
+ }
415
+
311
416
  let tagName = result.tagName;
312
417
  const rawTagName = result.rawTagName;
313
418
  let tagExp = result.tagExp;
@@ -334,7 +439,7 @@ const parseXml = function (xmlData) {
334
439
  if (currentNode && textData) {
335
440
  if (currentNode.tagname !== '!xml') {
336
441
  //when nested tag is found
337
- textData = this.saveTextToParentTag(textData, currentNode, jPath, false);
442
+ textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false);
338
443
  }
339
444
  }
340
445
 
@@ -342,28 +447,65 @@ const parseXml = function (xmlData) {
342
447
  const lastTag = currentNode;
343
448
  if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) {
344
449
  currentNode = this.tagsNodeStack.pop();
345
- jPath = jPath.substring(0, jPath.lastIndexOf("."));
450
+ this.matcher.pop();
451
+ }
452
+
453
+ // Clean up self-closing syntax BEFORE processing attributes
454
+ // This is where tagExp gets the trailing / removed
455
+ let isSelfClosing = false;
456
+ if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
457
+ isSelfClosing = true;
458
+ if (tagName[tagName.length - 1] === "/") {
459
+ tagName = tagName.substr(0, tagName.length - 1);
460
+ tagExp = tagName;
461
+ } else {
462
+ tagExp = tagExp.substr(0, tagExp.length - 1);
463
+ }
464
+
465
+ // Re-check attrExpPresent after cleaning
466
+ attrExpPresent = (tagName !== tagExp);
467
+ }
468
+
469
+ // Now process attributes with CLEAN tagExp (no trailing /)
470
+ let prefixedAttrs = null;
471
+ let rawAttrs = {};
472
+ let namespace = undefined;
473
+
474
+ // Extract namespace from rawTagName
475
+ namespace = extractNamespace(rawTagName);
476
+
477
+ // Push tag to matcher FIRST (with empty attrs for now) so callbacks see correct path
478
+ if (tagName !== xmlObj.tagname) {
479
+ this.matcher.push(tagName, {}, namespace);
346
480
  }
481
+
482
+ // Now build attributes - callbacks will see correct matcher state
483
+ if (tagName !== tagExp && attrExpPresent) {
484
+ // Build attributes (returns prefixed attributes for the tree)
485
+ // Note: buildAttributesMap now internally updates the matcher with raw attributes
486
+ prefixedAttrs = this.buildAttributesMap(tagExp, this.matcher, tagName);
487
+
488
+ if (prefixedAttrs) {
489
+ // Extract raw attributes (without prefix) for our use
490
+ rawAttrs = extractRawAttributes(prefixedAttrs, this.options);
491
+ }
492
+ }
493
+
494
+ // Now check if this is a stop node (after attributes are set)
347
495
  if (tagName !== xmlObj.tagname) {
348
- jPath += jPath ? "." + tagName : tagName;
496
+ this.isCurrentNodeStopNode = this.isItStopNode(this.stopNodeExpressions, this.matcher);
349
497
  }
498
+
350
499
  const startIndex = i;
351
- if (this.isItStopNode(this.stopNodesExact, this.stopNodesWildcard, jPath, tagName)) {
500
+ if (this.isCurrentNodeStopNode) {
352
501
  let tagContent = "";
353
- //self-closing tag
354
- if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
355
- if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
356
- tagName = tagName.substr(0, tagName.length - 1);
357
- jPath = jPath.substr(0, jPath.length - 1);
358
- tagExp = tagName;
359
- } else {
360
- tagExp = tagExp.substr(0, tagExp.length - 1);
361
- }
502
+
503
+ // For self-closing tags, content is empty
504
+ if (isSelfClosing) {
362
505
  i = result.closeIndex;
363
506
  }
364
507
  //unpaired tag
365
508
  else if (this.options.unpairedTags.indexOf(tagName) !== -1) {
366
-
367
509
  i = result.closeIndex;
368
510
  }
369
511
  //normal tag
@@ -377,28 +519,20 @@ const parseXml = function (xmlData) {
377
519
 
378
520
  const childNode = new xmlNode(tagName);
379
521
 
380
- if (tagName !== tagExp && attrExpPresent) {
381
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
382
- }
383
- if (tagContent) {
384
- tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true);
522
+ if (prefixedAttrs) {
523
+ childNode[":@"] = prefixedAttrs;
385
524
  }
386
525
 
387
- jPath = jPath.substr(0, jPath.lastIndexOf("."));
526
+ // For stop nodes, store raw content as-is without any processing
388
527
  childNode.add(this.options.textNodeName, tagContent);
389
528
 
390
- this.addChild(currentNode, childNode, jPath, startIndex);
529
+ this.matcher.pop(); // Pop the stop node tag
530
+ this.isCurrentNodeStopNode = false; // Reset flag
531
+
532
+ this.addChild(currentNode, childNode, this.matcher, startIndex);
391
533
  } else {
392
534
  //selfClosing tag
393
- if (tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1) {
394
- if (tagName[tagName.length - 1] === "/") { //remove trailing '/'
395
- tagName = tagName.substr(0, tagName.length - 1);
396
- jPath = jPath.substr(0, jPath.length - 1);
397
- tagExp = tagName;
398
- } else {
399
- tagExp = tagExp.substr(0, tagExp.length - 1);
400
- }
401
-
535
+ if (isSelfClosing) {
402
536
  if (this.options.transformTagName) {
403
537
  const newTagName = this.options.transformTagName(tagName);
404
538
  if (tagExp === tagName) {
@@ -408,19 +542,21 @@ const parseXml = function (xmlData) {
408
542
  }
409
543
 
410
544
  const childNode = new xmlNode(tagName);
411
- if (tagName !== tagExp && attrExpPresent) {
412
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
545
+ if (prefixedAttrs) {
546
+ childNode[":@"] = prefixedAttrs;
413
547
  }
414
- this.addChild(currentNode, childNode, jPath, startIndex);
415
- jPath = jPath.substr(0, jPath.lastIndexOf("."));
548
+ this.addChild(currentNode, childNode, this.matcher, startIndex);
549
+ this.matcher.pop(); // Pop self-closing tag
550
+ this.isCurrentNodeStopNode = false; // Reset flag
416
551
  }
417
- else if(this.options.unpairedTags.indexOf(tagName) !== -1){//unpaired tag
552
+ else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag
418
553
  const childNode = new xmlNode(tagName);
419
- if(tagName !== tagExp && attrExpPresent){
420
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath);
554
+ if (prefixedAttrs) {
555
+ childNode[":@"] = prefixedAttrs;
421
556
  }
422
- this.addChild(currentNode, childNode, jPath, startIndex);
423
- jPath = jPath.substr(0, jPath.lastIndexOf("."));
557
+ this.addChild(currentNode, childNode, this.matcher, startIndex);
558
+ this.matcher.pop(); // Pop unpaired tag
559
+ this.isCurrentNodeStopNode = false; // Reset flag
424
560
  i = result.closeIndex;
425
561
  // Continue to next iteration without changing currentNode
426
562
  continue;
@@ -433,10 +569,10 @@ const parseXml = function (xmlData) {
433
569
  }
434
570
  this.tagsNodeStack.push(currentNode);
435
571
 
436
- if (tagName !== tagExp && attrExpPresent) {
437
- childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
572
+ if (prefixedAttrs) {
573
+ childNode[":@"] = prefixedAttrs;
438
574
  }
439
- this.addChild(currentNode, childNode, jPath, startIndex);
575
+ this.addChild(currentNode, childNode, this.matcher, startIndex);
440
576
  currentNode = childNode;
441
577
  }
442
578
  textData = "";
@@ -450,10 +586,13 @@ const parseXml = function (xmlData) {
450
586
  return xmlObj.child;
451
587
  }
452
588
 
453
- function addChild(currentNode, childNode, jPath, startIndex) {
589
+ function addChild(currentNode, childNode, matcher, startIndex) {
454
590
  // unset startIndex if not requested
455
591
  if (!this.options.captureMetaData) startIndex = undefined;
456
- const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"])
592
+
593
+ // Pass jPath string or matcher based on options.jPath setting
594
+ const jPathOrMatcher = this.options.jPath ? matcher.toString() : matcher;
595
+ const result = this.options.updateTag(childNode.tagname, jPathOrMatcher, childNode[":@"])
457
596
  if (result === false) {
458
597
  //do nothing
459
598
  } else if (typeof result === "string") {
@@ -464,27 +603,34 @@ function addChild(currentNode, childNode, jPath, startIndex) {
464
603
  }
465
604
  }
466
605
 
467
- const replaceEntitiesValue = function (val, tagName, jPath) {
468
- // Performance optimization: Early return if no entities to replace
469
- if (val.indexOf('&') === -1) {
470
- return val;
471
- }
472
-
606
+ /**
607
+ * @param {object} val - Entity object with regex and val properties
608
+ * @param {string} tagName - Tag name
609
+ * @param {string|Matcher} jPath - jPath string or Matcher instance based on options.jPath
610
+ */
611
+ function replaceEntitiesValue(val, tagName, jPath) {
473
612
  const entityConfig = this.options.processEntities;
474
613
 
475
- if (!entityConfig.enabled) {
614
+ if (!entityConfig || !entityConfig.enabled) {
476
615
  return val;
477
616
  }
478
617
 
479
- // Check tag-specific filtering
618
+ // Check if tag is allowed to contain entities
480
619
  if (entityConfig.allowedTags) {
481
- if (!entityConfig.allowedTags.includes(tagName)) {
482
- return val; // Skip entity replacement for current tag as not set
620
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
621
+ const allowed = Array.isArray(entityConfig.allowedTags)
622
+ ? entityConfig.allowedTags.includes(tagName)
623
+ : entityConfig.allowedTags(tagName, jPathOrMatcher);
624
+
625
+ if (!allowed) {
626
+ return val;
483
627
  }
484
628
  }
485
629
 
630
+ // Apply custom tag filter if provided
486
631
  if (entityConfig.tagFilter) {
487
- if (!entityConfig.tagFilter(tagName, jPath)) {
632
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
633
+ if (!entityConfig.tagFilter(tagName, jPathOrMatcher)) {
488
634
  return val; // Skip based on custom filter
489
635
  }
490
636
  }
@@ -546,13 +692,13 @@ const replaceEntitiesValue = function (val, tagName, jPath) {
546
692
  }
547
693
 
548
694
 
549
- function saveTextToParentTag(textData, parentNode, jPath, isLeafNode) {
695
+ function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) {
550
696
  if (textData) { //store previously collected data as textNode
551
697
  if (isLeafNode === undefined) isLeafNode = parentNode.child.length === 0
552
698
 
553
699
  textData = this.parseTextData(textData,
554
700
  parentNode.tagname,
555
- jPath,
701
+ matcher,
556
702
  false,
557
703
  parentNode[":@"] ? Object.keys(parentNode[":@"]).length !== 0 : false,
558
704
  isLeafNode);
@@ -566,14 +712,17 @@ function saveTextToParentTag(textData, parentNode, jPath, isLeafNode) {
566
712
 
567
713
  //TODO: use jPath to simplify the logic
568
714
  /**
569
- * @param {Set} stopNodesExact
570
- * @param {Set} stopNodesWildcard
571
- * @param {string} jPath
572
- * @param {string} currentTagName
715
+ * @param {Array<Expression>} stopNodeExpressions - Array of compiled Expression objects
716
+ * @param {Matcher} matcher - Current path matcher
573
717
  */
574
- function isItStopNode(stopNodesExact, stopNodesWildcard, jPath, currentTagName) {
575
- if (stopNodesWildcard && stopNodesWildcard.has(currentTagName)) return true;
576
- if (stopNodesExact && stopNodesExact.has(jPath)) return true;
718
+ function isItStopNode(stopNodeExpressions, matcher) {
719
+ if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false;
720
+
721
+ for (let i = 0; i < stopNodeExpressions.length; i++) {
722
+ if (matcher.matches(stopNodeExpressions[i])) {
723
+ return true;
724
+ }
725
+ }
577
726
  return false;
578
727
  }
579
728
 
@@ -35,7 +35,7 @@ export default class XMLParser {
35
35
  orderedObjParser.addExternalEntities(this.externalEntities);
36
36
  const orderedResult = orderedObjParser.parseXml(xmlData);
37
37
  if (this.options.preserveOrder || orderedResult === undefined) return orderedResult;
38
- else return prettify(orderedResult, this.options);
38
+ else return prettify(orderedResult, this.options, orderedObjParser.matcher);
39
39
  }
40
40
 
41
41
  /**