@cdk8s/awscdk-resolver 0.0.509 โ 0.0.511
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/.jsii +3 -3
- package/lib/resolve.js +1 -1
- package/node_modules/@aws-sdk/client-cloudformation/package.json +2 -2
- package/node_modules/@smithy/util-waiter/dist-cjs/index.js +1 -1
- package/node_modules/@smithy/util-waiter/dist-es/poller.js +1 -1
- package/node_modules/@smithy/util-waiter/package.json +1 -1
- package/node_modules/fast-xml-builder/CHANGELOG.md +13 -0
- package/node_modules/fast-xml-builder/README.md +1 -1
- package/node_modules/fast-xml-builder/lib/fxb.cjs +1 -0
- package/node_modules/fast-xml-builder/lib/fxb.d.cts +13 -9
- package/node_modules/fast-xml-builder/lib/fxb.min.js +2 -0
- package/node_modules/fast-xml-builder/lib/fxb.min.js.map +1 -0
- package/node_modules/fast-xml-builder/package.json +7 -5
- package/node_modules/fast-xml-builder/src/fxb.d.ts +17 -3
- package/node_modules/fast-xml-builder/src/fxb.js +262 -21
- package/node_modules/fast-xml-builder/src/orderedJs2Xml.js +161 -18
- package/node_modules/path-expression-matcher/LICENSE +21 -0
- package/node_modules/path-expression-matcher/README.md +635 -0
- package/node_modules/path-expression-matcher/lib/pem.cjs +1 -0
- package/node_modules/path-expression-matcher/lib/pem.d.cts +335 -0
- package/node_modules/path-expression-matcher/lib/pem.min.js +2 -0
- package/node_modules/path-expression-matcher/lib/pem.min.js.map +1 -0
- package/node_modules/path-expression-matcher/package.json +78 -0
- package/node_modules/path-expression-matcher/src/Expression.js +232 -0
- package/node_modules/path-expression-matcher/src/Matcher.js +414 -0
- package/node_modules/path-expression-matcher/src/index.d.ts +366 -0
- package/node_modules/path-expression-matcher/src/index.js +28 -0
- package/package.json +2 -2
- package/node_modules/fast-xml-builder/lib/builder.cjs +0 -1
- package/node_modules/fast-xml-builder/lib/builder.min.js +0 -2
- package/node_modules/fast-xml-builder/lib/builder.min.js.map +0 -1
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
# path-expression-matcher
|
|
2
|
+
|
|
3
|
+
Efficient path tracking and pattern matching for XML, JSON, YAML or any other parsers.
|
|
4
|
+
|
|
5
|
+
## ๐ฏ Purpose
|
|
6
|
+
|
|
7
|
+
`path-expression-matcher` provides two core classes for tracking and matching paths:
|
|
8
|
+
|
|
9
|
+
- **`Expression`**: Parses and stores pattern expressions (e.g., `"root.users.user[id]"`)
|
|
10
|
+
- **`Matcher`**: Tracks current path during parsing and matches against expressions
|
|
11
|
+
|
|
12
|
+
Compatible with [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) and similar tools.
|
|
13
|
+
|
|
14
|
+
## ๐ฆ Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install path-expression-matcher
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## ๐ Quick Start
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
import { Expression, Matcher } from 'path-expression-matcher';
|
|
24
|
+
|
|
25
|
+
// Create expression (parse once, reuse many times)
|
|
26
|
+
const expr = new Expression("root.users.user");
|
|
27
|
+
|
|
28
|
+
// Create matcher (tracks current path)
|
|
29
|
+
const matcher = new Matcher();
|
|
30
|
+
|
|
31
|
+
matcher.push("root");
|
|
32
|
+
matcher.push("users");
|
|
33
|
+
matcher.push("user", { id: "123" });
|
|
34
|
+
|
|
35
|
+
// Match current path against expression
|
|
36
|
+
if (matcher.matches(expr)) {
|
|
37
|
+
console.log("Match found!");
|
|
38
|
+
console.log("Current path:", matcher.toString()); // "root.users.user"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Namespace support
|
|
42
|
+
const nsExpr = new Expression("soap::Envelope.soap::Body..ns::UserId");
|
|
43
|
+
matcher.push("Envelope", null, "soap");
|
|
44
|
+
matcher.push("Body", null, "soap");
|
|
45
|
+
matcher.push("UserId", null, "ns");
|
|
46
|
+
console.log(matcher.toString()); // "soap:Envelope.soap:Body.ns:UserId"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## ๐ Pattern Syntax
|
|
50
|
+
|
|
51
|
+
### Basic Paths
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
"root.users.user" // Exact path match
|
|
55
|
+
"*.users.user" // Wildcard: any parent
|
|
56
|
+
"root.*.user" // Wildcard: any middle
|
|
57
|
+
"root.users.*" // Wildcard: any child
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Deep Wildcard
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
"..user" // user anywhere in tree
|
|
64
|
+
"root..user" // user anywhere under root
|
|
65
|
+
"..users..user" // users somewhere, then user below it
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Attribute Matching
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
"user[id]" // user with "id" attribute
|
|
72
|
+
"user[type=admin]" // user with type="admin" (current node only)
|
|
73
|
+
"root[lang]..user" // user under root that has "lang" attribute
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Position Selectors
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
"user:first" // First user (counter=0)
|
|
80
|
+
"user:nth(2)" // Third user (counter=2, zero-based)
|
|
81
|
+
"user:odd" // Odd-numbered users (counter=1,3,5...)
|
|
82
|
+
"user:even" // Even-numbered users (counter=0,2,4...)
|
|
83
|
+
"root.users.user:first" // First user under users
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Note:** Position selectors use the **counter** (occurrence count of the tag name), not the position (child index). For example, in `<root><a/><b/><a/></root>`, the second `<a/>` has position=2 but counter=1.
|
|
87
|
+
|
|
88
|
+
### Namespaces
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
"ns::user" // user with namespace "ns"
|
|
92
|
+
"soap::Envelope" // Envelope with namespace "soap"
|
|
93
|
+
"ns::user[id]" // user with namespace "ns" and "id" attribute
|
|
94
|
+
"ns::user:first" // First user with namespace "ns"
|
|
95
|
+
"*::user" // user with any namespace
|
|
96
|
+
"..ns::item" // item with namespace "ns" anywhere in tree
|
|
97
|
+
"soap::Envelope.soap::Body" // Nested namespaced elements
|
|
98
|
+
"ns::first" // Tag named "first" with namespace "ns" (NO ambiguity!)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Namespace syntax:**
|
|
102
|
+
- Use **double colon (::)** for namespace: `ns::tag`
|
|
103
|
+
- Use **single colon (:)** for position: `tag:first`
|
|
104
|
+
- Combined: `ns::tag:first` (namespace + tag + position)
|
|
105
|
+
|
|
106
|
+
**Namespace matching rules:**
|
|
107
|
+
- Pattern `ns::user` matches only nodes with namespace "ns" and tag "user"
|
|
108
|
+
- Pattern `user` (no namespace) matches nodes with tag "user" regardless of namespace
|
|
109
|
+
- Pattern `*::user` matches tag "user" with any namespace (wildcard namespace)
|
|
110
|
+
- Namespaces are tracked separately for counter/position (e.g., `ns1::item` and `ns2::item` have independent counters)
|
|
111
|
+
|
|
112
|
+
### Wildcard Differences
|
|
113
|
+
|
|
114
|
+
**Single wildcard (`*`)** - Matches exactly ONE level:
|
|
115
|
+
- `"*.fix1"` matches `root.fix1` (2 levels) โ
|
|
116
|
+
- `"*.fix1"` does NOT match `root.another.fix1` (3 levels) โ
|
|
117
|
+
- Path depth MUST equal pattern depth
|
|
118
|
+
|
|
119
|
+
**Deep wildcard (`..`)** - Matches ZERO or MORE levels:
|
|
120
|
+
- `"..fix1"` matches `root.fix1` โ
|
|
121
|
+
- `"..fix1"` matches `root.another.fix1` โ
|
|
122
|
+
- `"..fix1"` matches `a.b.c.d.fix1` โ
|
|
123
|
+
- Works at any depth
|
|
124
|
+
|
|
125
|
+
### Combined Patterns
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
"..user[id]:first" // First user with id, anywhere
|
|
129
|
+
"root..user[type=admin]" // Admin user under root
|
|
130
|
+
"ns::user[id]:first" // First namespaced user with id
|
|
131
|
+
"soap::Envelope..ns::UserId" // UserId with namespace ns under SOAP envelope
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## ๐ง API Reference
|
|
135
|
+
|
|
136
|
+
### Expression
|
|
137
|
+
|
|
138
|
+
#### Constructor
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
new Expression(pattern, options)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Parameters:**
|
|
145
|
+
- `pattern` (string): Pattern to parse
|
|
146
|
+
- `options.separator` (string): Path separator (default: `'.'`)
|
|
147
|
+
|
|
148
|
+
**Example:**
|
|
149
|
+
```javascript
|
|
150
|
+
const expr1 = new Expression("root.users.user");
|
|
151
|
+
const expr2 = new Expression("root/users/user", { separator: '/' });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Methods
|
|
155
|
+
|
|
156
|
+
- `hasDeepWildcard()` โ boolean
|
|
157
|
+
- `hasAttributeCondition()` โ boolean
|
|
158
|
+
- `hasPositionSelector()` โ boolean
|
|
159
|
+
- `toString()` โ string
|
|
160
|
+
|
|
161
|
+
### Matcher
|
|
162
|
+
|
|
163
|
+
#### Constructor
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
new Matcher(options)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Parameters:**
|
|
170
|
+
- `options.separator` (string): Default path separator (default: `'.'`)
|
|
171
|
+
|
|
172
|
+
#### Path Tracking Methods
|
|
173
|
+
|
|
174
|
+
##### `push(tagName, attrValues, namespace)`
|
|
175
|
+
|
|
176
|
+
Add a tag to the current path. Position and counter are automatically calculated.
|
|
177
|
+
|
|
178
|
+
**Parameters:**
|
|
179
|
+
- `tagName` (string): Tag name
|
|
180
|
+
- `attrValues` (object, optional): Attribute key-value pairs (current node only)
|
|
181
|
+
- `namespace` (string, optional): Namespace for the tag
|
|
182
|
+
|
|
183
|
+
**Example:**
|
|
184
|
+
```javascript
|
|
185
|
+
matcher.push("user", { id: "123", type: "admin" });
|
|
186
|
+
matcher.push("item"); // No attributes
|
|
187
|
+
matcher.push("Envelope", null, "soap"); // With namespace
|
|
188
|
+
matcher.push("Body", { version: "1.1" }, "soap"); // With both
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Position vs Counter:**
|
|
192
|
+
- **Position**: The child index in the parent (0, 1, 2, 3...)
|
|
193
|
+
- **Counter**: How many times this tag name appeared at this level (0, 1, 2...)
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
```xml
|
|
197
|
+
<root>
|
|
198
|
+
<a/> <!-- position=0, counter=0 -->
|
|
199
|
+
<b/> <!-- position=1, counter=0 -->
|
|
200
|
+
<a/> <!-- position=2, counter=1 -->
|
|
201
|
+
</root>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
##### `pop()`
|
|
205
|
+
|
|
206
|
+
Remove the last tag from the path.
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
matcher.pop();
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
##### `updateCurrent(attrValues)`
|
|
213
|
+
|
|
214
|
+
Update current node's attributes (useful when attributes are parsed after push).
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
matcher.push("user"); // Don't know values yet
|
|
218
|
+
// ... parse attributes ...
|
|
219
|
+
matcher.updateCurrent({ id: "123" });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
##### `reset()`
|
|
223
|
+
|
|
224
|
+
Clear the entire path.
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
matcher.reset();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### Query Methods
|
|
231
|
+
|
|
232
|
+
##### `matches(expression)`
|
|
233
|
+
|
|
234
|
+
Check if current path matches an Expression.
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
const expr = new Expression("root.users.user");
|
|
238
|
+
if (matcher.matches(expr)) {
|
|
239
|
+
// Current path matches
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
##### `getCurrentTag()`
|
|
244
|
+
|
|
245
|
+
Get current tag name.
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
const tag = matcher.getCurrentTag(); // "user"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
##### `getCurrentNamespace()`
|
|
252
|
+
|
|
253
|
+
Get current namespace.
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
const ns = matcher.getCurrentNamespace(); // "soap" or undefined
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
##### `getAttrValue(attrName)`
|
|
260
|
+
|
|
261
|
+
Get attribute value of current node.
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
const id = matcher.getAttrValue("id"); // "123"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
##### `hasAttr(attrName)`
|
|
268
|
+
|
|
269
|
+
Check if current node has an attribute.
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
if (matcher.hasAttr("id")) {
|
|
273
|
+
// Current node has "id" attribute
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
##### `getPosition()`
|
|
278
|
+
|
|
279
|
+
Get sibling position of current node (child index in parent).
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
const position = matcher.getPosition(); // 0, 1, 2, ...
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
##### `getCounter()`
|
|
286
|
+
|
|
287
|
+
Get repeat counter of current node (occurrence count of this tag name).
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
const counter = matcher.getCounter(); // 0, 1, 2, ...
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
##### `getIndex()` (deprecated)
|
|
294
|
+
|
|
295
|
+
Alias for `getPosition()`. Use `getPosition()` or `getCounter()` instead for clarity.
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
const index = matcher.getIndex(); // Same as getPosition()
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
##### `getDepth()`
|
|
302
|
+
|
|
303
|
+
Get current path depth.
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
const depth = matcher.getDepth(); // 3 for "root.users.user"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
##### `toString(separator?, includeNamespace?)`
|
|
310
|
+
|
|
311
|
+
Get path as string.
|
|
312
|
+
|
|
313
|
+
**Parameters:**
|
|
314
|
+
- `separator` (string, optional): Path separator (uses default if not provided)
|
|
315
|
+
- `includeNamespace` (boolean, optional): Whether to include namespaces (default: true)
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
const path = matcher.toString(); // "root.ns:user.item"
|
|
319
|
+
const path2 = matcher.toString('/'); // "root/ns:user/item"
|
|
320
|
+
const path3 = matcher.toString('.', false); // "root.user.item" (no namespaces)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
##### `toArray()`
|
|
324
|
+
|
|
325
|
+
Get path as array.
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
const arr = matcher.toArray(); // ["root", "users", "user"]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### State Management
|
|
332
|
+
|
|
333
|
+
##### `snapshot()`
|
|
334
|
+
|
|
335
|
+
Create a snapshot of current state.
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
const snapshot = matcher.snapshot();
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
##### `restore(snapshot)`
|
|
342
|
+
|
|
343
|
+
Restore from a snapshot.
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
matcher.restore(snapshot);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## ๐ก Usage Examples
|
|
350
|
+
|
|
351
|
+
### Example 1: XML Parser with stopNodes
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
355
|
+
import { Expression, Matcher } from 'path-expression-matcher';
|
|
356
|
+
|
|
357
|
+
class MyParser {
|
|
358
|
+
constructor() {
|
|
359
|
+
this.matcher = new Matcher();
|
|
360
|
+
|
|
361
|
+
// Pre-compile stop node patterns
|
|
362
|
+
this.stopNodeExpressions = [
|
|
363
|
+
new Expression("html.body.script"),
|
|
364
|
+
new Expression("html.body.style"),
|
|
365
|
+
new Expression("..svg"),
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
parseTag(tagName, attrs) {
|
|
370
|
+
this.matcher.push(tagName, attrs);
|
|
371
|
+
|
|
372
|
+
// Check if this is a stop node
|
|
373
|
+
for (const expr of this.stopNodeExpressions) {
|
|
374
|
+
if (this.matcher.matches(expr)) {
|
|
375
|
+
// Don't parse children, read as raw text
|
|
376
|
+
return this.readRawContent();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Continue normal parsing
|
|
381
|
+
this.parseChildren();
|
|
382
|
+
|
|
383
|
+
this.matcher.pop();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Example 2: Conditional Processing
|
|
389
|
+
|
|
390
|
+
```javascript
|
|
391
|
+
const matcher = new Matcher();
|
|
392
|
+
const userExpr = new Expression("..user[type=admin]");
|
|
393
|
+
const firstItemExpr = new Expression("..item:first");
|
|
394
|
+
|
|
395
|
+
function processTag(tagName, value, attrs) {
|
|
396
|
+
matcher.push(tagName, attrs);
|
|
397
|
+
|
|
398
|
+
if (matcher.matches(userExpr)) {
|
|
399
|
+
value = enhanceAdminUser(value);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (matcher.matches(firstItemExpr)) {
|
|
403
|
+
value = markAsFirst(value);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
matcher.pop();
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Example 3: Path-based Filtering
|
|
412
|
+
|
|
413
|
+
```javascript
|
|
414
|
+
const patterns = [
|
|
415
|
+
new Expression("data.users.user"),
|
|
416
|
+
new Expression("data.posts.post"),
|
|
417
|
+
new Expression("..comment[approved=true]"),
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
function shouldInclude(matcher) {
|
|
421
|
+
return patterns.some(expr => matcher.matches(expr));
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Example 4: Custom Separator
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
const matcher = new Matcher({ separator: '/' });
|
|
429
|
+
const expr = new Expression("root/config/database", { separator: '/' });
|
|
430
|
+
|
|
431
|
+
matcher.push("root");
|
|
432
|
+
matcher.push("config");
|
|
433
|
+
matcher.push("database");
|
|
434
|
+
|
|
435
|
+
console.log(matcher.toString()); // "root/config/database"
|
|
436
|
+
console.log(matcher.matches(expr)); // true
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Example 5: Attribute Checking
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
const matcher = new Matcher();
|
|
443
|
+
matcher.push("root");
|
|
444
|
+
matcher.push("user", { id: "123", type: "admin", status: "active" });
|
|
445
|
+
|
|
446
|
+
// Check attribute existence (current node only)
|
|
447
|
+
console.log(matcher.hasAttr("id")); // true
|
|
448
|
+
console.log(matcher.hasAttr("email")); // false
|
|
449
|
+
|
|
450
|
+
// Get attribute value (current node only)
|
|
451
|
+
console.log(matcher.getAttrValue("type")); // "admin"
|
|
452
|
+
|
|
453
|
+
// Match by attribute
|
|
454
|
+
const expr1 = new Expression("user[id]");
|
|
455
|
+
console.log(matcher.matches(expr1)); // true
|
|
456
|
+
|
|
457
|
+
const expr2 = new Expression("user[type=admin]");
|
|
458
|
+
console.log(matcher.matches(expr2)); // true
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Example 6: Position vs Counter
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
const matcher = new Matcher();
|
|
465
|
+
matcher.push("root");
|
|
466
|
+
|
|
467
|
+
// Mixed tags at same level
|
|
468
|
+
matcher.push("item"); // position=0, counter=0 (first item)
|
|
469
|
+
matcher.pop();
|
|
470
|
+
|
|
471
|
+
matcher.push("div"); // position=1, counter=0 (first div)
|
|
472
|
+
matcher.pop();
|
|
473
|
+
|
|
474
|
+
matcher.push("item"); // position=2, counter=1 (second item)
|
|
475
|
+
|
|
476
|
+
console.log(matcher.getPosition()); // 2 (third child overall)
|
|
477
|
+
console.log(matcher.getCounter()); // 1 (second "item" specifically)
|
|
478
|
+
|
|
479
|
+
// :first uses counter, not position
|
|
480
|
+
const expr = new Expression("root.item:first");
|
|
481
|
+
console.log(matcher.matches(expr)); // false (counter=1, not 0)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Example 7: Namespace Support (XML/SOAP)
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
const matcher = new Matcher();
|
|
488
|
+
const soapExpr = new Expression("soap::Envelope.soap::Body..ns::UserId");
|
|
489
|
+
|
|
490
|
+
// Parse SOAP document
|
|
491
|
+
matcher.push("Envelope", { xmlns: "..." }, "soap");
|
|
492
|
+
matcher.push("Body", null, "soap");
|
|
493
|
+
matcher.push("GetUserRequest", null, "ns");
|
|
494
|
+
matcher.push("UserId", null, "ns");
|
|
495
|
+
|
|
496
|
+
// Match namespaced pattern
|
|
497
|
+
if (matcher.matches(soapExpr)) {
|
|
498
|
+
console.log("Found UserId in SOAP body");
|
|
499
|
+
console.log(matcher.toString()); // "soap:Envelope.soap:Body.ns:GetUserRequest.ns:UserId"
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Namespace-specific counters
|
|
503
|
+
matcher.reset();
|
|
504
|
+
matcher.push("root");
|
|
505
|
+
matcher.push("item", null, "ns1"); // ns1::item counter=0
|
|
506
|
+
matcher.pop();
|
|
507
|
+
matcher.push("item", null, "ns2"); // ns2::item counter=0 (different namespace)
|
|
508
|
+
matcher.pop();
|
|
509
|
+
matcher.push("item", null, "ns1"); // ns1::item counter=1
|
|
510
|
+
|
|
511
|
+
const firstNs1Item = new Expression("root.ns1::item:first");
|
|
512
|
+
console.log(matcher.matches(firstNs1Item)); // false (counter=1)
|
|
513
|
+
|
|
514
|
+
const secondNs1Item = new Expression("root.ns1::item:nth(1)");
|
|
515
|
+
console.log(matcher.matches(secondNs1Item)); // true
|
|
516
|
+
|
|
517
|
+
// NO AMBIGUITY: Tags named after position keywords
|
|
518
|
+
matcher.reset();
|
|
519
|
+
matcher.push("root");
|
|
520
|
+
matcher.push("first", null, "ns"); // Tag named "first" with namespace
|
|
521
|
+
|
|
522
|
+
const expr = new Expression("root.ns::first");
|
|
523
|
+
console.log(matcher.matches(expr)); // true - matches namespace "ns", tag "first"
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## ๐๏ธ Architecture
|
|
527
|
+
|
|
528
|
+
### Data Storage Strategy
|
|
529
|
+
|
|
530
|
+
**Ancestor nodes:** Store only tag name, position, and counter (minimal memory)
|
|
531
|
+
**Current node:** Store tag name, position, counter, and attribute values
|
|
532
|
+
|
|
533
|
+
This design minimizes memory usage:
|
|
534
|
+
- No attribute names stored (derived from values object when needed)
|
|
535
|
+
- Attribute values only for current node, not ancestors
|
|
536
|
+
- Attribute checking for ancestors is not supported (acceptable trade-off)
|
|
537
|
+
- For 1M nodes with 3 attributes each, saves ~50MB vs storing attribute names
|
|
538
|
+
|
|
539
|
+
### Matching Strategy
|
|
540
|
+
|
|
541
|
+
Matching is performed **bottom-to-top** (from current node toward root):
|
|
542
|
+
1. Start at current node
|
|
543
|
+
2. Match segments from pattern end to start
|
|
544
|
+
3. Attribute checking only works for current node (ancestors have no attribute data)
|
|
545
|
+
4. Position selectors use **counter** (occurrence count), not position (child index)
|
|
546
|
+
|
|
547
|
+
### Performance
|
|
548
|
+
|
|
549
|
+
- **Expression parsing:** One-time cost when Expression is created
|
|
550
|
+
- **Expression analysis:** Cached (hasDeepWildcard, hasAttributeCondition, hasPositionSelector)
|
|
551
|
+
- **Path tracking:** O(1) for push/pop operations
|
|
552
|
+
- **Pattern matching:** O(n*m) where n = path depth, m = pattern segments
|
|
553
|
+
- **Memory per ancestor node:** ~40-60 bytes (tag, position, counter only)
|
|
554
|
+
- **Memory per current node:** ~80-120 bytes (adds attribute values)
|
|
555
|
+
|
|
556
|
+
## ๐ Design Patterns
|
|
557
|
+
|
|
558
|
+
### Pre-compile Patterns (Recommended)
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
// โ
GOOD: Parse once, reuse many times
|
|
562
|
+
const expr = new Expression("..user[id]");
|
|
563
|
+
|
|
564
|
+
for (let i = 0; i < 1000; i++) {
|
|
565
|
+
if (matcher.matches(expr)) {
|
|
566
|
+
// ...
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
```javascript
|
|
572
|
+
// โ BAD: Parse on every iteration
|
|
573
|
+
for (let i = 0; i < 1000; i++) {
|
|
574
|
+
if (matcher.matches(new Expression("..user[id]"))) {
|
|
575
|
+
// ...
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Batch Pattern Checking
|
|
581
|
+
|
|
582
|
+
```javascript
|
|
583
|
+
// For multiple patterns, check all at once
|
|
584
|
+
const patterns = [
|
|
585
|
+
new Expression("..user"),
|
|
586
|
+
new Expression("..post"),
|
|
587
|
+
new Expression("..comment"),
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
function matchesAny(matcher, patterns) {
|
|
591
|
+
return patterns.some(expr => matcher.matches(expr));
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
## ๐ Integration with fast-xml-parser
|
|
596
|
+
|
|
597
|
+
**Basic integration:**
|
|
598
|
+
|
|
599
|
+
```javascript
|
|
600
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
601
|
+
import { Expression, Matcher } from 'path-expression-matcher';
|
|
602
|
+
|
|
603
|
+
const parser = new XMLParser({
|
|
604
|
+
// Custom options using path-expression-matcher
|
|
605
|
+
stopNodes: ["script", "style"].map(tag => new Expression(`..${tag}`)),
|
|
606
|
+
|
|
607
|
+
tagValueProcessor: (tagName, value, jPath, hasAttrs, isLeaf, matcher) => {
|
|
608
|
+
// matcher is available in callbacks
|
|
609
|
+
if (matcher.matches(new Expression("..user[type=admin]"))) {
|
|
610
|
+
return enhanceValue(value);
|
|
611
|
+
}
|
|
612
|
+
return value;
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
## ๐งช Testing
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
npm test
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
All 77 tests covering:
|
|
624
|
+
- Pattern parsing (exact, wildcards, attributes, position)
|
|
625
|
+
- Path tracking (push, pop, update)
|
|
626
|
+
- Pattern matching (all combinations)
|
|
627
|
+
- Edge cases and error conditions
|
|
628
|
+
|
|
629
|
+
## ๐ License
|
|
630
|
+
|
|
631
|
+
MIT
|
|
632
|
+
|
|
633
|
+
## ๐ค Contributing
|
|
634
|
+
|
|
635
|
+
Issues and PRs welcome! This package is designed to be used by XML/JSON parsers like fast-xml-parser.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{"use strict";var t={d:(e,s)=>{for(var i in s)t.o(s,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:s[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{Expression:()=>s,Matcher:()=>i,default:()=>n});class s{constructor(t,e={}){this.pattern=t,this.separator=e.separator||".",this.segments=this._parse(t),this._hasDeepWildcard=this.segments.some(t=>"deep-wildcard"===t.type),this._hasAttributeCondition=this.segments.some(t=>void 0!==t.attrName),this._hasPositionSelector=this.segments.some(t=>void 0!==t.position)}_parse(t){const e=[];let s=0,i="";for(;s<t.length;)t[s]===this.separator?s+1<t.length&&t[s+1]===this.separator?(i.trim()&&(e.push(this._parseSegment(i.trim())),i=""),e.push({type:"deep-wildcard"}),s+=2):(i.trim()&&e.push(this._parseSegment(i.trim())),i="",s++):(i+=t[s],s++);return i.trim()&&e.push(this._parseSegment(i.trim())),e}_parseSegment(t){const e={type:"tag"};let s=null,i=t;const n=t.match(/^([^\[]+)(\[[^\]]*\])(.*)$/);if(n&&(i=n[1]+n[3],n[2])){const t=n[2].slice(1,-1);t&&(s=t)}let r,a,h=i;if(i.includes("::")){const e=i.indexOf("::");if(r=i.substring(0,e).trim(),h=i.substring(e+2).trim(),!r)throw new Error(`Invalid namespace in pattern: ${t}`)}let o=null;if(h.includes(":")){const t=h.lastIndexOf(":"),e=h.substring(0,t).trim(),s=h.substring(t+1).trim();["first","last","odd","even"].includes(s)||/^nth\(\d+\)$/.test(s)?(a=e,o=s):a=h}else a=h;if(!a)throw new Error(`Invalid segment pattern: ${t}`);if(e.tag=a,r&&(e.namespace=r),s)if(s.includes("=")){const t=s.indexOf("=");e.attrName=s.substring(0,t).trim(),e.attrValue=s.substring(t+1).trim()}else e.attrName=s.trim();if(o){const t=o.match(/^nth\((\d+)\)$/);t?(e.position="nth",e.positionValue=parseInt(t[1],10)):e.position=o}return e}get length(){return this.segments.length}hasDeepWildcard(){return this._hasDeepWildcard}hasAttributeCondition(){return this._hasAttributeCondition}hasPositionSelector(){return this._hasPositionSelector}toString(){return this.pattern}}class i{constructor(t={}){this.separator=t.separator||".",this.path=[],this.siblingStacks=[]}push(t,e=null,s=null){this.path.length>0&&(this.path[this.path.length-1].values=void 0);const i=this.path.length;this.siblingStacks[i]||(this.siblingStacks[i]=new Map);const n=this.siblingStacks[i],r=s?`${s}:${t}`:t,a=n.get(r)||0;let h=0;for(const t of n.values())h+=t;n.set(r,a+1);const o={tag:t,position:h,counter:a};null!=s&&(o.namespace=s),null!=e&&(o.values=e),this.path.push(o)}pop(){if(0===this.path.length)return;const t=this.path.pop();return this.siblingStacks.length>this.path.length+1&&(this.siblingStacks.length=this.path.length+1),t}updateCurrent(t){if(this.path.length>0){const e=this.path[this.path.length-1];null!=t&&(e.values=t)}}getCurrentTag(){return this.path.length>0?this.path[this.path.length-1].tag:void 0}getCurrentNamespace(){return this.path.length>0?this.path[this.path.length-1].namespace:void 0}getAttrValue(t){if(0===this.path.length)return;const e=this.path[this.path.length-1];return e.values?.[t]}hasAttr(t){if(0===this.path.length)return!1;const e=this.path[this.path.length-1];return void 0!==e.values&&t in e.values}getPosition(){return 0===this.path.length?-1:this.path[this.path.length-1].position??0}getCounter(){return 0===this.path.length?-1:this.path[this.path.length-1].counter??0}getIndex(){return this.getPosition()}getDepth(){return this.path.length}toString(t,e=!0){const s=t||this.separator;return this.path.map(t=>e&&t.namespace?`${t.namespace}:${t.tag}`:t.tag).join(s)}toArray(){return this.path.map(t=>t.tag)}reset(){this.path=[],this.siblingStacks=[]}matches(t){const e=t.segments;return 0!==e.length&&(t.hasDeepWildcard()?this._matchWithDeepWildcard(e):this._matchSimple(e))}_matchSimple(t){if(this.path.length!==t.length)return!1;for(let e=0;e<t.length;e++){const s=t[e],i=this.path[e],n=e===this.path.length-1;if(!this._matchSegment(s,i,n))return!1}return!0}_matchWithDeepWildcard(t){let e=this.path.length-1,s=t.length-1;for(;s>=0&&e>=0;){const i=t[s];if("deep-wildcard"===i.type){if(s--,s<0)return!0;const i=t[s];let n=!1;for(let t=e;t>=0;t--){const r=t===this.path.length-1;if(this._matchSegment(i,this.path[t],r)){e=t-1,s--,n=!0;break}}if(!n)return!1}else{const t=e===this.path.length-1;if(!this._matchSegment(i,this.path[e],t))return!1;e--,s--}}return s<0}_matchSegment(t,e,s){if("*"!==t.tag&&t.tag!==e.tag)return!1;if(void 0!==t.namespace&&"*"!==t.namespace&&t.namespace!==e.namespace)return!1;if(void 0!==t.attrName){if(!s)return!1;if(!e.values||!(t.attrName in e.values))return!1;if(void 0!==t.attrValue){const s=e.values[t.attrName];if(String(s)!==String(t.attrValue))return!1}}if(void 0!==t.position){if(!s)return!1;const i=e.counter??0;if("first"===t.position&&0!==i)return!1;if("odd"===t.position&&i%2!=1)return!1;if("even"===t.position&&i%2!=0)return!1;if("nth"===t.position&&i!==t.positionValue)return!1}return!0}snapshot(){return{path:this.path.map(t=>({...t})),siblingStacks:this.siblingStacks.map(t=>new Map(t))}}restore(t){this.path=t.path.map(t=>({...t})),this.siblingStacks=t.siblingStacks.map(t=>new Map(t))}}const n={Expression:s,Matcher:i};module.exports=e})();
|