fast-xml-parser 5.5.7 → 5.5.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-xml-parser",
3
- "version": "5.5.7",
3
+ "version": "5.5.9",
4
4
  "description": "Validate XML, Parse XML, Build XML without C/C++ based libraries",
5
5
  "main": "./lib/fxp.cjs",
6
6
  "type": "module",
@@ -88,7 +88,7 @@
88
88
  ],
89
89
  "dependencies": {
90
90
  "fast-xml-builder": "^1.1.4",
91
- "path-expression-matcher": "^1.1.3",
92
- "strnum": "^2.2.0"
91
+ "path-expression-matcher": "^1.2.0",
92
+ "strnum": "^2.2.2"
93
93
  }
94
- }
94
+ }
package/src/fxp.d.ts CHANGED
@@ -1,7 +1,149 @@
1
- //import type { Matcher, Expression } from 'path-expression-matcher';
1
+ /**
2
+ * Types copied from path-expression-matcher
3
+ * @version <version>
4
+ * @updated <date>
5
+ *
6
+ * Update this file when path-expression-matcher releases a new version.
7
+ * Source: https://github.com/NaturalIntelligence/path-expression-matcher
8
+ */
9
+
10
+ /**
11
+ * Options for creating an Expression
12
+ */
13
+ export interface ExpressionOptions {
14
+ /**
15
+ * Path separator character
16
+ * @default '.'
17
+ */
18
+ separator?: string;
19
+ }
20
+
21
+ /**
22
+ * Parsed segment from an expression pattern
23
+ */
24
+ export interface Segment {
25
+ type: 'tag' | 'deep-wildcard';
26
+ tag?: string;
27
+ namespace?: string;
28
+ attrName?: string;
29
+ attrValue?: string;
30
+ position?: 'first' | 'last' | 'odd' | 'even' | 'nth';
31
+ positionValue?: number;
32
+ }
33
+
34
+ /**
35
+ * Expression - Parses and stores a tag pattern expression.
36
+ * Patterns are parsed once and stored in an optimized structure for fast matching.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const expr = new Expression("root.users.user");
41
+ * const expr2 = new Expression("..user[id]:first");
42
+ * const expr3 = new Expression("root/users/user", { separator: '/' });
43
+ * ```
44
+ *
45
+ * Pattern Syntax:
46
+ * - `root.users.user` — Match exact path
47
+ * - `..user` — Match "user" at any depth (deep wildcard)
48
+ * - `user[id]` — Match user tag with "id" attribute
49
+ * - `user[id=123]` — Match user tag where id="123"
50
+ * - `user:first` — Match first occurrence of user tag
51
+ * - `ns::user` — Match user tag with namespace "ns"
52
+ * - `ns::user[id]:first` — Combine namespace, attribute, and position
53
+ */
54
+ export class Expression {
55
+ readonly pattern: string;
56
+ readonly separator: string;
57
+ readonly segments: Segment[];
58
+
59
+ constructor(pattern: string, options?: ExpressionOptions);
60
+
61
+ get length(): number;
62
+ hasDeepWildcard(): boolean;
63
+ hasAttributeCondition(): boolean;
64
+ hasPositionSelector(): boolean;
65
+ toString(): string;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // ReadonlyMatcher
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /**
73
+ * A live read-only view of a Matcher instance, returned by Matcher.readOnly.
74
+ *
75
+ * All query and inspection methods work normally and always reflect the current
76
+ * state of the underlying matcher. State-mutating methods (`push`, `pop`,
77
+ * `reset`, `updateCurrent`, `restore`) are not present — calling them on the
78
+ * underlying Proxy throws a `TypeError` at runtime.
79
+ *
80
+ * This is the type received by all FXP user callbacks when `jPath: false`.
81
+ */
82
+ export interface ReadonlyMatcher {
83
+ readonly separator: string;
84
+
85
+ /** Check if current path matches an Expression. */
86
+ matches(expression: Expression): boolean;
87
+
88
+ /** Get current tag name, or `undefined` if path is empty. */
89
+ getCurrentTag(): string | undefined;
90
+
91
+ /** Get current namespace, or `undefined` if not present. */
92
+ getCurrentNamespace(): string | undefined;
93
+
94
+ /** Get attribute value of the current node. */
95
+ getAttrValue(attrName: string): any;
96
+
97
+ /** Check if the current node has a given attribute. */
98
+ hasAttr(attrName: string): boolean;
99
+
100
+ /** Sibling position of the current node (child index in parent). */
101
+ getPosition(): number;
102
+
103
+ /** Occurrence counter of the current tag name at this level. */
104
+ getCounter(): number;
105
+
106
+ /** Number of nodes in the current path. */
107
+ getDepth(): number;
108
+
109
+ /** Current path as a string (e.g. `"root.users.user"`). */
110
+ toString(separator?: string, includeNamespace?: boolean): string;
111
+
112
+ /** Current path as an array of tag names. */
113
+ toArray(): string[];
114
+
115
+ /**
116
+ * Create a snapshot of the current state.
117
+ * The snapshot can be passed to the real Matcher.restore if needed.
118
+ */
119
+ snapshot(): MatcherSnapshot;
120
+ }
121
+
122
+ /** Internal node structure — exposed via snapshot only. */
123
+ export interface PathNode {
124
+ tag: string;
125
+ namespace?: string;
126
+ position: number;
127
+ counter: number;
128
+ values?: Record<string, any>;
129
+ }
130
+
131
+ /** Snapshot of matcher state returned by `snapshot()` and `readOnly().snapshot()`. */
132
+ export interface MatcherSnapshot {
133
+ path: PathNode[];
134
+ siblingStacks: Map<string, number>[];
135
+ }
136
+
137
+ /**********************************************************************
138
+ *
139
+ * END of path-expression-matcher relevant typings
140
+ *
141
+ **********************************************************************/
2
142
 
3
- type Matcher = unknown;
4
- type Expression = unknown;
143
+ // jPath: true → string
144
+ // jPath: false → ReadonlyMatcher
145
+ type JPathOrMatcher = string | ReadonlyMatcher;
146
+ type JPathOrExpression = string | Expression;
5
147
 
6
148
  export type ProcessEntitiesOptions = {
7
149
  /**
@@ -63,7 +205,7 @@ export type ProcessEntitiesOptions = {
63
205
  *
64
206
  * Defaults to `null`
65
207
  */
66
- tagFilter?: ((tagName: string, jPathOrMatcher: string | Matcher) => boolean) | null;
208
+ tagFilter?: ((tagName: string, jPathOrMatcher: JPathOrMatcher) => boolean) | null;
67
209
  };
68
210
 
69
211
  export type X2jOptions = {
@@ -108,7 +250,7 @@ export type X2jOptions = {
108
250
  *
109
251
  * Defaults to `true`
110
252
  */
111
- ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPathOrMatcher: string | Matcher) => boolean);
253
+ ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPathOrMatcher: JPathOrMatcher) => boolean);
112
254
 
113
255
  /**
114
256
  * Whether to remove namespace string from tag and attribute names
@@ -175,7 +317,7 @@ export type X2jOptions = {
175
317
  *
176
318
  * Defaults to `(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode) => val`
177
319
  */
178
- tagValueProcessor?: (tagName: string, tagValue: string, jPathOrMatcher: string | Matcher, hasAttributes: boolean, isLeafNode: boolean) => unknown;
320
+ tagValueProcessor?: (tagName: string, tagValue: string, jPathOrMatcher: JPathOrMatcher, hasAttributes: boolean, isLeafNode: boolean) => unknown;
179
321
 
180
322
  /**
181
323
  * Control how attribute value should be parsed
@@ -188,7 +330,7 @@ export type X2jOptions = {
188
330
  *
189
331
  * Defaults to `(attrName, val, jPathOrMatcher) => val`
190
332
  */
191
- attributeValueProcessor?: (attrName: string, attrValue: string, jPathOrMatcher: string | Matcher) => unknown;
333
+ attributeValueProcessor?: (attrName: string, attrValue: string, jPathOrMatcher: JPathOrMatcher) => unknown;
192
334
 
193
335
  /**
194
336
  * Options to pass to `strnum` for parsing numbers
@@ -206,7 +348,7 @@ export type X2jOptions = {
206
348
  *
207
349
  * Defaults to `[]`
208
350
  */
209
- stopNodes?: (string | Expression)[];
351
+ stopNodes?: JPathOrExpression[];
210
352
 
211
353
  /**
212
354
  * List of tags without closing tags
@@ -233,7 +375,7 @@ export type X2jOptions = {
233
375
  *
234
376
  * Defaults to `() => false`
235
377
  */
236
- isArray?: (tagName: string, jPathOrMatcher: string | Matcher, isLeafNode: boolean, isAttribute: boolean) => boolean;
378
+ isArray?: (tagName: string, jPathOrMatcher: JPathOrMatcher, isLeafNode: boolean, isAttribute: boolean) => boolean;
237
379
 
238
380
  /**
239
381
  * Whether to process default and DOCTYPE entities
@@ -295,7 +437,7 @@ export type X2jOptions = {
295
437
  *
296
438
  * Defaults to `(tagName, jPathOrMatcher, attrs) => tagName`
297
439
  */
298
- updateTag?: (tagName: string, jPathOrMatcher: string | Matcher, attrs: { [k: string]: string }) => string | boolean;
440
+ updateTag?: (tagName: string, jPathOrMatcher: JPathOrMatcher, attrs: { [k: string]: string }) => string | boolean;
299
441
 
300
442
  /**
301
443
  * If true, adds a Symbol to all object nodes, accessible by {@link XMLParser.getMetaDataSymbol} with
@@ -479,7 +621,7 @@ export type XmlBuilderOptions = {
479
621
  *
480
622
  * Defaults to `[]`
481
623
  */
482
- stopNodes?: (string | Expression)[];
624
+ stopNodes?: JPathOrExpression[];
483
625
 
484
626
  /**
485
627
  * Control how tag value should be parsed. Called only if tag value is not empty
@@ -113,6 +113,10 @@ export default class OrderedObjParser {
113
113
  // Initialize path matcher for path-expression-matcher
114
114
  this.matcher = new Matcher();
115
115
 
116
+ // Live read-only proxy of matcher — PEM creates and caches this internally.
117
+ // All user callbacks receive this instead of the mutable matcher.
118
+ this.readonlyMatcher = this.matcher.readOnly();
119
+
116
120
  // Flag to track if current node is a stop node (optimization)
117
121
  this.isCurrentNodeStopNode = false;
118
122
 
@@ -225,7 +229,7 @@ function buildAttributesMap(attrStr, jPath, tagName) {
225
229
  if (this.options.trimValues) {
226
230
  parsedVal = parsedVal.trim();
227
231
  }
228
- parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath);
232
+ parsedVal = this.replaceEntitiesValue(parsedVal, tagName, this.readonlyMatcher);
229
233
  rawAttrsForMatcher[attrName] = parsedVal;
230
234
  }
231
235
  }
@@ -240,7 +244,7 @@ function buildAttributesMap(attrStr, jPath, tagName) {
240
244
  const attrName = this.resolveNameSpace(matches[i][1]);
241
245
 
242
246
  // Convert jPath to string if needed for ignoreAttributesFn
243
- const jPathStr = this.options.jPath ? jPath.toString() : jPath;
247
+ const jPathStr = this.options.jPath ? jPath.toString() : this.readonlyMatcher;
244
248
  if (this.ignoreAttributesFn(attrName, jPathStr)) {
245
249
  continue
246
250
  }
@@ -259,10 +263,10 @@ function buildAttributesMap(attrStr, jPath, tagName) {
259
263
  if (this.options.trimValues) {
260
264
  oldVal = oldVal.trim();
261
265
  }
262
- oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath);
266
+ oldVal = this.replaceEntitiesValue(oldVal, tagName, this.readonlyMatcher);
263
267
 
264
- // Pass jPath string or matcher based on options.jPath setting
265
- const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath;
268
+ // Pass jPath string or readonlyMatcher based on options.jPath setting
269
+ const jPathOrMatcher = this.options.jPath ? jPath.toString() : this.readonlyMatcher;
266
270
  const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher);
267
271
  if (newVal === null || newVal === undefined) {
268
272
  //don't parse
@@ -329,7 +333,7 @@ const parseXml = function (xmlData) {
329
333
  tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName;
330
334
 
331
335
  if (currentNode) {
332
- textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
336
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
333
337
  }
334
338
 
335
339
  //check if last tag of nested tag was unpaired tag
@@ -354,7 +358,7 @@ const parseXml = function (xmlData) {
354
358
  let tagData = readTagExp(xmlData, i, false, "?>");
355
359
  if (!tagData) throw new Error("Pi Tag is not closed.");
356
360
 
357
- textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
361
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
358
362
  if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) {
359
363
  //do nothing
360
364
  } else {
@@ -365,7 +369,7 @@ const parseXml = function (xmlData) {
365
369
  if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) {
366
370
  childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName);
367
371
  }
368
- this.addChild(currentNode, childNode, this.matcher, i);
372
+ this.addChild(currentNode, childNode, this.readonlyMatcher, i);
369
373
  }
370
374
 
371
375
 
@@ -375,7 +379,7 @@ const parseXml = function (xmlData) {
375
379
  if (this.options.commentPropName) {
376
380
  const comment = xmlData.substring(i + 4, endIndex - 2);
377
381
 
378
- textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
382
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
379
383
 
380
384
  currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]);
381
385
  }
@@ -388,9 +392,9 @@ const parseXml = function (xmlData) {
388
392
  const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
389
393
  const tagExp = xmlData.substring(i + 9, closeIndex);
390
394
 
391
- textData = this.saveTextToParentTag(textData, currentNode, this.matcher);
395
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher);
392
396
 
393
- let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true);
397
+ let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true);
394
398
  if (val == undefined) val = "";
395
399
 
396
400
  //cdata should be set even if it is 0 length string
@@ -432,7 +436,7 @@ const parseXml = function (xmlData) {
432
436
  if (currentNode && textData) {
433
437
  if (currentNode.tagname !== '!xml') {
434
438
  //when nested tag is found
435
- textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false);
439
+ textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false);
436
440
  }
437
441
  }
438
442
 
@@ -522,7 +526,7 @@ const parseXml = function (xmlData) {
522
526
  this.matcher.pop(); // Pop the stop node tag
523
527
  this.isCurrentNodeStopNode = false; // Reset flag
524
528
 
525
- this.addChild(currentNode, childNode, this.matcher, startIndex);
529
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
526
530
  } else {
527
531
  //selfClosing tag
528
532
  if (isSelfClosing) {
@@ -532,7 +536,7 @@ const parseXml = function (xmlData) {
532
536
  if (prefixedAttrs) {
533
537
  childNode[":@"] = prefixedAttrs;
534
538
  }
535
- this.addChild(currentNode, childNode, this.matcher, startIndex);
539
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
536
540
  this.matcher.pop(); // Pop self-closing tag
537
541
  this.isCurrentNodeStopNode = false; // Reset flag
538
542
  }
@@ -541,7 +545,7 @@ const parseXml = function (xmlData) {
541
545
  if (prefixedAttrs) {
542
546
  childNode[":@"] = prefixedAttrs;
543
547
  }
544
- this.addChild(currentNode, childNode, this.matcher, startIndex);
548
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
545
549
  this.matcher.pop(); // Pop unpaired tag
546
550
  this.isCurrentNodeStopNode = false; // Reset flag
547
551
  i = result.closeIndex;
@@ -559,7 +563,7 @@ const parseXml = function (xmlData) {
559
563
  if (prefixedAttrs) {
560
564
  childNode[":@"] = prefixedAttrs;
561
565
  }
562
- this.addChild(currentNode, childNode, this.matcher, startIndex);
566
+ this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex);
563
567
  currentNode = childNode;
564
568
  }
565
569
  textData = "";
@@ -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, orderedObjParser.matcher);
38
+ else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher);
39
39
  }
40
40
 
41
41
  /**
@@ -68,4 +68,4 @@ export default class XMLParser {
68
68
  static getMetaDataSymbol() {
69
69
  return XmlNode.getMetaDataSymbol();
70
70
  }
71
- }
71
+ }
@@ -35,18 +35,17 @@ function stripAttributePrefix(attrs, prefix) {
35
35
  * @param {Matcher} matcher - Path matcher instance
36
36
  * @returns
37
37
  */
38
- export default function prettify(node, options, matcher) {
39
- return compress(node, options, matcher);
38
+ export default function prettify(node, options, matcher, readonlyMatcher) {
39
+ return compress(node, options, matcher, readonlyMatcher);
40
40
  }
41
41
 
42
42
  /**
43
- *
44
43
  * @param {array} arr
45
44
  * @param {object} options
46
45
  * @param {Matcher} matcher - Path matcher instance
47
46
  * @returns object
48
47
  */
49
- function compress(arr, options, matcher) {
48
+ function compress(arr, options, matcher, readonlyMatcher) {
50
49
  let text;
51
50
  const compressedObj = {}; //This is intended to be a plain object
52
51
  for (let i = 0; i < arr.length; i++) {
@@ -69,11 +68,11 @@ function compress(arr, options, matcher) {
69
68
  continue;
70
69
  } else if (tagObj[property]) {
71
70
 
72
- let val = compress(tagObj[property], options, matcher);
71
+ let val = compress(tagObj[property], options, matcher, readonlyMatcher);
73
72
  const isLeaf = isLeafTag(val, options);
74
73
 
75
74
  if (tagObj[":@"]) {
76
- assignAttributes(val, tagObj[":@"], matcher, options);
75
+ assignAttributes(val, tagObj[":@"], readonlyMatcher, options);
77
76
  } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) {
78
77
  val = val[options.textNodeName];
79
78
  } else if (Object.keys(val).length === 0) {
@@ -95,8 +94,8 @@ function compress(arr, options, matcher) {
95
94
  //TODO: if a node is not an array, then check if it should be an array
96
95
  //also determine if it is a leaf node
97
96
 
98
- // Pass jPath string or matcher based on options.jPath setting
99
- const jPathOrMatcher = options.jPath ? matcher.toString() : matcher;
97
+ // Pass jPath string or readonlyMatcher based on options.jPath setting
98
+ const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher;
100
99
  if (options.isArray(property, jPathOrMatcher, isLeaf)) {
101
100
  compressedObj[property] = [val];
102
101
  } else {
@@ -128,7 +127,7 @@ function propName(obj) {
128
127
  }
129
128
  }
130
129
 
131
- function assignAttributes(obj, attrMap, matcher, options) {
130
+ function assignAttributes(obj, attrMap, readonlyMatcher, options) {
132
131
  if (attrMap) {
133
132
  const keys = Object.keys(attrMap);
134
133
  const len = keys.length; //don't make it inline
@@ -143,8 +142,8 @@ function assignAttributes(obj, attrMap, matcher, options) {
143
142
  // For attributes, we need to create a temporary path
144
143
  // Pass jPath string or matcher based on options.jPath setting
145
144
  const jPathOrMatcher = options.jPath
146
- ? matcher.toString() + "." + rawAttrName
147
- : matcher;
145
+ ? readonlyMatcher.toString() + "." + rawAttrName
146
+ : readonlyMatcher;
148
147
 
149
148
  if (options.isArray(atrrName, jPathOrMatcher, true, true)) {
150
149
  obj[atrrName] = [attrMap[atrrName]];