path-expression-matcher 1.3.0 → 1.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.
- package/README.md +52 -54
- package/lib/pem.cjs +1 -1
- package/lib/pem.d.cts +18 -0
- package/lib/pem.min.js +1 -1
- package/lib/pem.min.js.map +1 -1
- package/package.json +1 -1
- package/src/Expression.js +2 -2
- package/src/ExpressionSet.js +24 -4
- package/src/Matcher.js +223 -195
- package/src/index.d.ts +81 -246
package/src/Matcher.js
CHANGED
|
@@ -1,61 +1,203 @@
|
|
|
1
1
|
import ExpressionSet from "./ExpressionSet.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MatcherView - A lightweight read-only view over a Matcher's internal state.
|
|
5
|
+
*
|
|
6
|
+
* Created once by Matcher and reused across all callbacks. Holds a direct
|
|
7
|
+
* reference to the parent Matcher so it always reflects current parser state
|
|
8
|
+
* with zero copying or freezing overhead.
|
|
9
|
+
*
|
|
10
|
+
* Users receive this via {@link Matcher#readOnly} or directly from parser
|
|
11
|
+
* callbacks. It exposes all query and matching methods but has no mutation
|
|
12
|
+
* methods — misuse is caught at the TypeScript level rather than at runtime.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const matcher = new Matcher();
|
|
16
|
+
* const view = matcher.readOnly();
|
|
17
|
+
*
|
|
18
|
+
* matcher.push("root", {});
|
|
19
|
+
* view.getCurrentTag(); // "root"
|
|
20
|
+
* view.getDepth(); // 1
|
|
21
|
+
*/
|
|
22
|
+
export class MatcherView {
|
|
23
|
+
/**
|
|
24
|
+
* @param {Matcher} matcher - The parent Matcher instance to read from.
|
|
25
|
+
*/
|
|
26
|
+
constructor(matcher) {
|
|
27
|
+
this._matcher = matcher;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the path separator used by the parent matcher.
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
get separator() {
|
|
35
|
+
return this._matcher.separator;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get current tag name.
|
|
40
|
+
* @returns {string|undefined}
|
|
41
|
+
*/
|
|
42
|
+
getCurrentTag() {
|
|
43
|
+
const path = this._matcher.path;
|
|
44
|
+
return path.length > 0 ? path[path.length - 1].tag : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get current namespace.
|
|
49
|
+
* @returns {string|undefined}
|
|
50
|
+
*/
|
|
51
|
+
getCurrentNamespace() {
|
|
52
|
+
const path = this._matcher.path;
|
|
53
|
+
return path.length > 0 ? path[path.length - 1].namespace : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get current node's attribute value.
|
|
58
|
+
* @param {string} attrName
|
|
59
|
+
* @returns {*}
|
|
60
|
+
*/
|
|
61
|
+
getAttrValue(attrName) {
|
|
62
|
+
const path = this._matcher.path;
|
|
63
|
+
if (path.length === 0) return undefined;
|
|
64
|
+
return path[path.length - 1].values?.[attrName];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if current node has an attribute.
|
|
69
|
+
* @param {string} attrName
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
hasAttr(attrName) {
|
|
73
|
+
const path = this._matcher.path;
|
|
74
|
+
if (path.length === 0) return false;
|
|
75
|
+
const current = path[path.length - 1];
|
|
76
|
+
return current.values !== undefined && attrName in current.values;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get current node's sibling position (child index in parent).
|
|
81
|
+
* @returns {number}
|
|
82
|
+
*/
|
|
83
|
+
getPosition() {
|
|
84
|
+
const path = this._matcher.path;
|
|
85
|
+
if (path.length === 0) return -1;
|
|
86
|
+
return path[path.length - 1].position ?? 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get current node's repeat counter (occurrence count of this tag name).
|
|
91
|
+
* @returns {number}
|
|
92
|
+
*/
|
|
93
|
+
getCounter() {
|
|
94
|
+
const path = this._matcher.path;
|
|
95
|
+
if (path.length === 0) return -1;
|
|
96
|
+
return path[path.length - 1].counter ?? 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get current node's sibling index (alias for getPosition).
|
|
101
|
+
* @returns {number}
|
|
102
|
+
* @deprecated Use getPosition() or getCounter() instead
|
|
103
|
+
*/
|
|
104
|
+
getIndex() {
|
|
105
|
+
return this.getPosition();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get current path depth.
|
|
110
|
+
* @returns {number}
|
|
111
|
+
*/
|
|
112
|
+
getDepth() {
|
|
113
|
+
return this._matcher.path.length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get path as string.
|
|
118
|
+
* @param {string} [separator] - Optional separator (uses default if not provided)
|
|
119
|
+
* @param {boolean} [includeNamespace=true]
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
toString(separator, includeNamespace = true) {
|
|
123
|
+
return this._matcher.toString(separator, includeNamespace);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get path as array of tag names.
|
|
128
|
+
* @returns {string[]}
|
|
129
|
+
*/
|
|
130
|
+
toArray() {
|
|
131
|
+
return this._matcher.path.map(n => n.tag);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Match current path against an Expression.
|
|
136
|
+
* @param {Expression} expression
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
matches(expression) {
|
|
140
|
+
return this._matcher.matches(expression);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Match any expression in the given set against the current path.
|
|
145
|
+
* @param {ExpressionSet} exprSet
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
matchesAny(exprSet) {
|
|
149
|
+
return exprSet.matchesAny(this._matcher);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
2
153
|
/**
|
|
3
|
-
* Matcher - Tracks current path in XML/JSON tree and matches against Expressions
|
|
4
|
-
*
|
|
154
|
+
* Matcher - Tracks current path in XML/JSON tree and matches against Expressions.
|
|
155
|
+
*
|
|
5
156
|
* The matcher maintains a stack of nodes representing the current path from root to
|
|
6
157
|
* current tag. It only stores attribute values for the current (top) node to minimize
|
|
7
158
|
* memory usage. Sibling tracking is used to auto-calculate position and counter.
|
|
8
|
-
*
|
|
159
|
+
*
|
|
160
|
+
* Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to
|
|
161
|
+
* user callbacks — it always reflects current state with no Proxy overhead.
|
|
162
|
+
*
|
|
9
163
|
* @example
|
|
10
164
|
* const matcher = new Matcher();
|
|
11
165
|
* matcher.push("root", {});
|
|
12
166
|
* matcher.push("users", {});
|
|
13
167
|
* matcher.push("user", { id: "123", type: "admin" });
|
|
14
|
-
*
|
|
168
|
+
*
|
|
15
169
|
* const expr = new Expression("root.users.user");
|
|
16
170
|
* matcher.matches(expr); // true
|
|
17
171
|
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Names of methods that mutate Matcher state.
|
|
21
|
-
* Any attempt to call these on a read-only view throws a TypeError.
|
|
22
|
-
* @type {Set<string>}
|
|
23
|
-
*/
|
|
24
|
-
const MUTATING_METHODS = new Set(['push', 'pop', 'reset', 'updateCurrent', 'restore']);
|
|
25
|
-
|
|
26
172
|
export default class Matcher {
|
|
27
173
|
/**
|
|
28
|
-
* Create a new Matcher
|
|
29
|
-
* @param {Object} options
|
|
30
|
-
* @param {string} options.separator - Default path separator
|
|
174
|
+
* Create a new Matcher.
|
|
175
|
+
* @param {Object} [options={}]
|
|
176
|
+
* @param {string} [options.separator='.'] - Default path separator
|
|
31
177
|
*/
|
|
32
178
|
constructor(options = {}) {
|
|
33
179
|
this.separator = options.separator || '.';
|
|
34
180
|
this.path = [];
|
|
35
181
|
this.siblingStacks = [];
|
|
36
|
-
// Each path node: { tag
|
|
182
|
+
// Each path node: { tag, values, position, counter, namespace? }
|
|
37
183
|
// values only present for current (last) node
|
|
38
184
|
// Each siblingStacks entry: Map<tagName, count> tracking occurrences at each level
|
|
39
185
|
this._pathStringCache = null;
|
|
40
|
-
this.
|
|
41
|
-
this._frozenSiblingsCache = null; // cache for readOnly().siblingStacks
|
|
186
|
+
this._view = new MatcherView(this);
|
|
42
187
|
}
|
|
43
188
|
|
|
44
189
|
/**
|
|
45
|
-
* Push a new tag onto the path
|
|
46
|
-
* @param {string} tagName
|
|
47
|
-
* @param {Object} attrValues
|
|
48
|
-
* @param {string} namespace
|
|
190
|
+
* Push a new tag onto the path.
|
|
191
|
+
* @param {string} tagName
|
|
192
|
+
* @param {Object|null} [attrValues=null]
|
|
193
|
+
* @param {string|null} [namespace=null]
|
|
49
194
|
*/
|
|
50
195
|
push(tagName, attrValues = null, namespace = null) {
|
|
51
|
-
//invalidate cache
|
|
52
196
|
this._pathStringCache = null;
|
|
53
|
-
|
|
54
|
-
this._frozenSiblingsCache = null;
|
|
197
|
+
|
|
55
198
|
// Remove values from previous current node (now becoming ancestor)
|
|
56
199
|
if (this.path.length > 0) {
|
|
57
|
-
|
|
58
|
-
prev.values = undefined;
|
|
200
|
+
this.path[this.path.length - 1].values = undefined;
|
|
59
201
|
}
|
|
60
202
|
|
|
61
203
|
// Get or create sibling tracking for current level
|
|
@@ -88,12 +230,10 @@ export default class Matcher {
|
|
|
88
230
|
counter: counter
|
|
89
231
|
};
|
|
90
232
|
|
|
91
|
-
// Store namespace if provided
|
|
92
233
|
if (namespace !== null && namespace !== undefined) {
|
|
93
234
|
node.namespace = namespace;
|
|
94
235
|
}
|
|
95
236
|
|
|
96
|
-
// Store values only for current node
|
|
97
237
|
if (attrValues !== null && attrValues !== undefined) {
|
|
98
238
|
node.values = attrValues;
|
|
99
239
|
}
|
|
@@ -102,20 +242,15 @@ export default class Matcher {
|
|
|
102
242
|
}
|
|
103
243
|
|
|
104
244
|
/**
|
|
105
|
-
* Pop the last tag from the path
|
|
245
|
+
* Pop the last tag from the path.
|
|
106
246
|
* @returns {Object|undefined} The popped node
|
|
107
247
|
*/
|
|
108
248
|
pop() {
|
|
109
249
|
if (this.path.length === 0) return undefined;
|
|
110
|
-
//invalidate cache
|
|
111
250
|
this._pathStringCache = null;
|
|
112
|
-
|
|
113
|
-
this._frozenSiblingsCache = null;
|
|
251
|
+
|
|
114
252
|
const node = this.path.pop();
|
|
115
253
|
|
|
116
|
-
// Clean up sibling tracking for levels deeper than current
|
|
117
|
-
// After pop, path.length is the new depth
|
|
118
|
-
// We need to clean up siblingStacks[path.length + 1] and beyond
|
|
119
254
|
if (this.siblingStacks.length > this.path.length + 1) {
|
|
120
255
|
this.siblingStacks.length = this.path.length + 1;
|
|
121
256
|
}
|
|
@@ -124,22 +259,21 @@ export default class Matcher {
|
|
|
124
259
|
}
|
|
125
260
|
|
|
126
261
|
/**
|
|
127
|
-
* Update current node's attribute values
|
|
128
|
-
* Useful when attributes are parsed after push
|
|
129
|
-
* @param {Object} attrValues
|
|
262
|
+
* Update current node's attribute values.
|
|
263
|
+
* Useful when attributes are parsed after push.
|
|
264
|
+
* @param {Object} attrValues
|
|
130
265
|
*/
|
|
131
266
|
updateCurrent(attrValues) {
|
|
132
267
|
if (this.path.length > 0) {
|
|
133
268
|
const current = this.path[this.path.length - 1];
|
|
134
269
|
if (attrValues !== null && attrValues !== undefined) {
|
|
135
270
|
current.values = attrValues;
|
|
136
|
-
this._frozenPathCache = null;
|
|
137
271
|
}
|
|
138
272
|
}
|
|
139
273
|
}
|
|
140
274
|
|
|
141
275
|
/**
|
|
142
|
-
* Get current tag name
|
|
276
|
+
* Get current tag name.
|
|
143
277
|
* @returns {string|undefined}
|
|
144
278
|
*/
|
|
145
279
|
getCurrentTag() {
|
|
@@ -147,7 +281,7 @@ export default class Matcher {
|
|
|
147
281
|
}
|
|
148
282
|
|
|
149
283
|
/**
|
|
150
|
-
* Get current namespace
|
|
284
|
+
* Get current namespace.
|
|
151
285
|
* @returns {string|undefined}
|
|
152
286
|
*/
|
|
153
287
|
getCurrentNamespace() {
|
|
@@ -155,19 +289,18 @@ export default class Matcher {
|
|
|
155
289
|
}
|
|
156
290
|
|
|
157
291
|
/**
|
|
158
|
-
* Get current node's attribute value
|
|
159
|
-
* @param {string} attrName
|
|
160
|
-
* @returns {*}
|
|
292
|
+
* Get current node's attribute value.
|
|
293
|
+
* @param {string} attrName
|
|
294
|
+
* @returns {*}
|
|
161
295
|
*/
|
|
162
296
|
getAttrValue(attrName) {
|
|
163
297
|
if (this.path.length === 0) return undefined;
|
|
164
|
-
|
|
165
|
-
return current.values?.[attrName];
|
|
298
|
+
return this.path[this.path.length - 1].values?.[attrName];
|
|
166
299
|
}
|
|
167
300
|
|
|
168
301
|
/**
|
|
169
|
-
* Check if current node has an attribute
|
|
170
|
-
* @param {string} attrName
|
|
302
|
+
* Check if current node has an attribute.
|
|
303
|
+
* @param {string} attrName
|
|
171
304
|
* @returns {boolean}
|
|
172
305
|
*/
|
|
173
306
|
hasAttr(attrName) {
|
|
@@ -177,7 +310,7 @@ export default class Matcher {
|
|
|
177
310
|
}
|
|
178
311
|
|
|
179
312
|
/**
|
|
180
|
-
* Get current node's sibling position (child index in parent)
|
|
313
|
+
* Get current node's sibling position (child index in parent).
|
|
181
314
|
* @returns {number}
|
|
182
315
|
*/
|
|
183
316
|
getPosition() {
|
|
@@ -186,7 +319,7 @@ export default class Matcher {
|
|
|
186
319
|
}
|
|
187
320
|
|
|
188
321
|
/**
|
|
189
|
-
* Get current node's repeat counter (occurrence count of this tag name)
|
|
322
|
+
* Get current node's repeat counter (occurrence count of this tag name).
|
|
190
323
|
* @returns {number}
|
|
191
324
|
*/
|
|
192
325
|
getCounter() {
|
|
@@ -195,7 +328,7 @@ export default class Matcher {
|
|
|
195
328
|
}
|
|
196
329
|
|
|
197
330
|
/**
|
|
198
|
-
* Get current node's sibling index (alias for getPosition
|
|
331
|
+
* Get current node's sibling index (alias for getPosition).
|
|
199
332
|
* @returns {number}
|
|
200
333
|
* @deprecated Use getPosition() or getCounter() instead
|
|
201
334
|
*/
|
|
@@ -204,7 +337,7 @@ export default class Matcher {
|
|
|
204
337
|
}
|
|
205
338
|
|
|
206
339
|
/**
|
|
207
|
-
* Get current path depth
|
|
340
|
+
* Get current path depth.
|
|
208
341
|
* @returns {number}
|
|
209
342
|
*/
|
|
210
343
|
getDepth() {
|
|
@@ -212,9 +345,9 @@ export default class Matcher {
|
|
|
212
345
|
}
|
|
213
346
|
|
|
214
347
|
/**
|
|
215
|
-
* Get path as string
|
|
216
|
-
* @param {string} separator - Optional separator (uses default if not provided)
|
|
217
|
-
* @param {boolean} includeNamespace
|
|
348
|
+
* Get path as string.
|
|
349
|
+
* @param {string} [separator] - Optional separator (uses default if not provided)
|
|
350
|
+
* @param {boolean} [includeNamespace=true]
|
|
218
351
|
* @returns {string}
|
|
219
352
|
*/
|
|
220
353
|
toString(separator, includeNamespace = true) {
|
|
@@ -222,24 +355,23 @@ export default class Matcher {
|
|
|
222
355
|
const isDefault = (sep === this.separator && includeNamespace === true);
|
|
223
356
|
|
|
224
357
|
if (isDefault) {
|
|
225
|
-
if (this._pathStringCache !== null
|
|
358
|
+
if (this._pathStringCache !== null) {
|
|
226
359
|
return this._pathStringCache;
|
|
227
360
|
}
|
|
228
361
|
const result = this.path.map(n =>
|
|
229
|
-
(
|
|
362
|
+
(n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
|
|
230
363
|
).join(sep);
|
|
231
364
|
this._pathStringCache = result;
|
|
232
365
|
return result;
|
|
233
366
|
}
|
|
234
367
|
|
|
235
|
-
// Non-default separator or includeNamespace=false: don't cache (rare case)
|
|
236
368
|
return this.path.map(n =>
|
|
237
369
|
(includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
|
|
238
370
|
).join(sep);
|
|
239
371
|
}
|
|
240
372
|
|
|
241
373
|
/**
|
|
242
|
-
* Get path as array of tag names
|
|
374
|
+
* Get path as array of tag names.
|
|
243
375
|
* @returns {string[]}
|
|
244
376
|
*/
|
|
245
377
|
toArray() {
|
|
@@ -247,21 +379,18 @@ export default class Matcher {
|
|
|
247
379
|
}
|
|
248
380
|
|
|
249
381
|
/**
|
|
250
|
-
* Reset the path to empty
|
|
382
|
+
* Reset the path to empty.
|
|
251
383
|
*/
|
|
252
384
|
reset() {
|
|
253
|
-
//invalidate cache
|
|
254
385
|
this._pathStringCache = null;
|
|
255
|
-
this._frozenPathCache = null;
|
|
256
|
-
this._frozenSiblingsCache = null;
|
|
257
386
|
this.path = [];
|
|
258
387
|
this.siblingStacks = [];
|
|
259
388
|
}
|
|
260
389
|
|
|
261
390
|
/**
|
|
262
|
-
* Match current path against an Expression
|
|
263
|
-
* @param {Expression} expression
|
|
264
|
-
* @returns {boolean}
|
|
391
|
+
* Match current path against an Expression.
|
|
392
|
+
* @param {Expression} expression
|
|
393
|
+
* @returns {boolean}
|
|
265
394
|
*/
|
|
266
395
|
matches(expression) {
|
|
267
396
|
const segments = expression.segments;
|
|
@@ -270,32 +399,23 @@ export default class Matcher {
|
|
|
270
399
|
return false;
|
|
271
400
|
}
|
|
272
401
|
|
|
273
|
-
// Handle deep wildcard patterns
|
|
274
402
|
if (expression.hasDeepWildcard()) {
|
|
275
403
|
return this._matchWithDeepWildcard(segments);
|
|
276
404
|
}
|
|
277
405
|
|
|
278
|
-
// Simple path matching (no deep wildcards)
|
|
279
406
|
return this._matchSimple(segments);
|
|
280
407
|
}
|
|
281
408
|
|
|
282
409
|
/**
|
|
283
|
-
* Match simple path (no deep wildcards)
|
|
284
410
|
* @private
|
|
285
411
|
*/
|
|
286
412
|
_matchSimple(segments) {
|
|
287
|
-
// Path must be same length as segments
|
|
288
413
|
if (this.path.length !== segments.length) {
|
|
289
414
|
return false;
|
|
290
415
|
}
|
|
291
416
|
|
|
292
|
-
// Match each segment bottom-to-top
|
|
293
417
|
for (let i = 0; i < segments.length; i++) {
|
|
294
|
-
|
|
295
|
-
const node = this.path[i];
|
|
296
|
-
const isCurrentNode = (i === this.path.length - 1);
|
|
297
|
-
|
|
298
|
-
if (!this._matchSegment(segment, node, isCurrentNode)) {
|
|
418
|
+
if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) {
|
|
299
419
|
return false;
|
|
300
420
|
}
|
|
301
421
|
}
|
|
@@ -304,32 +424,27 @@ export default class Matcher {
|
|
|
304
424
|
}
|
|
305
425
|
|
|
306
426
|
/**
|
|
307
|
-
* Match path with deep wildcards
|
|
308
427
|
* @private
|
|
309
428
|
*/
|
|
310
429
|
_matchWithDeepWildcard(segments) {
|
|
311
|
-
let pathIdx = this.path.length - 1;
|
|
312
|
-
let segIdx = segments.length - 1;
|
|
430
|
+
let pathIdx = this.path.length - 1;
|
|
431
|
+
let segIdx = segments.length - 1;
|
|
313
432
|
|
|
314
433
|
while (segIdx >= 0 && pathIdx >= 0) {
|
|
315
434
|
const segment = segments[segIdx];
|
|
316
435
|
|
|
317
436
|
if (segment.type === 'deep-wildcard') {
|
|
318
|
-
// ".." matches zero or more levels
|
|
319
437
|
segIdx--;
|
|
320
438
|
|
|
321
439
|
if (segIdx < 0) {
|
|
322
|
-
// Pattern ends with "..", always matches
|
|
323
440
|
return true;
|
|
324
441
|
}
|
|
325
442
|
|
|
326
|
-
// Find where next segment matches in the path
|
|
327
443
|
const nextSeg = segments[segIdx];
|
|
328
444
|
let found = false;
|
|
329
445
|
|
|
330
446
|
for (let i = pathIdx; i >= 0; i--) {
|
|
331
|
-
|
|
332
|
-
if (this._matchSegment(nextSeg, this.path[i], isCurrentNode)) {
|
|
447
|
+
if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) {
|
|
333
448
|
pathIdx = i - 1;
|
|
334
449
|
segIdx--;
|
|
335
450
|
found = true;
|
|
@@ -341,9 +456,7 @@ export default class Matcher {
|
|
|
341
456
|
return false;
|
|
342
457
|
}
|
|
343
458
|
} else {
|
|
344
|
-
|
|
345
|
-
const isCurrentNode = (pathIdx === this.path.length - 1);
|
|
346
|
-
if (!this._matchSegment(segment, this.path[pathIdx], isCurrentNode)) {
|
|
459
|
+
if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) {
|
|
347
460
|
return false;
|
|
348
461
|
}
|
|
349
462
|
pathIdx--;
|
|
@@ -351,38 +464,25 @@ export default class Matcher {
|
|
|
351
464
|
}
|
|
352
465
|
}
|
|
353
466
|
|
|
354
|
-
// All segments must be consumed
|
|
355
467
|
return segIdx < 0;
|
|
356
468
|
}
|
|
357
469
|
|
|
358
470
|
/**
|
|
359
|
-
* Match a single segment against a node
|
|
360
471
|
* @private
|
|
361
|
-
* @param {Object} segment - Segment from Expression
|
|
362
|
-
* @param {Object} node - Node from path
|
|
363
|
-
* @param {boolean} isCurrentNode - Whether this is the current (last) node
|
|
364
|
-
* @returns {boolean}
|
|
365
472
|
*/
|
|
366
473
|
_matchSegment(segment, node, isCurrentNode) {
|
|
367
|
-
// Match tag name (* is wildcard)
|
|
368
474
|
if (segment.tag !== '*' && segment.tag !== node.tag) {
|
|
369
475
|
return false;
|
|
370
476
|
}
|
|
371
477
|
|
|
372
|
-
// Match namespace if specified in segment
|
|
373
478
|
if (segment.namespace !== undefined) {
|
|
374
|
-
// Segment has namespace - node must match it
|
|
375
479
|
if (segment.namespace !== '*' && segment.namespace !== node.namespace) {
|
|
376
480
|
return false;
|
|
377
481
|
}
|
|
378
482
|
}
|
|
379
|
-
// If segment has no namespace, it matches nodes with or without namespace
|
|
380
483
|
|
|
381
|
-
// Match attribute name (check if node has this attribute)
|
|
382
|
-
// Can only check for current node since ancestors don't have values
|
|
383
484
|
if (segment.attrName !== undefined) {
|
|
384
485
|
if (!isCurrentNode) {
|
|
385
|
-
// Can't check attributes for ancestor nodes (values not stored)
|
|
386
486
|
return false;
|
|
387
487
|
}
|
|
388
488
|
|
|
@@ -390,20 +490,15 @@ export default class Matcher {
|
|
|
390
490
|
return false;
|
|
391
491
|
}
|
|
392
492
|
|
|
393
|
-
// Match attribute value (only possible for current node)
|
|
394
493
|
if (segment.attrValue !== undefined) {
|
|
395
|
-
|
|
396
|
-
// Both should be strings
|
|
397
|
-
if (String(actualValue) !== String(segment.attrValue)) {
|
|
494
|
+
if (String(node.values[segment.attrName]) !== String(segment.attrValue)) {
|
|
398
495
|
return false;
|
|
399
496
|
}
|
|
400
497
|
}
|
|
401
498
|
}
|
|
402
499
|
|
|
403
|
-
// Match position (only for current node)
|
|
404
500
|
if (segment.position !== undefined) {
|
|
405
501
|
if (!isCurrentNode) {
|
|
406
|
-
// Can't check position for ancestor nodes
|
|
407
502
|
return false;
|
|
408
503
|
}
|
|
409
504
|
|
|
@@ -415,10 +510,8 @@ export default class Matcher {
|
|
|
415
510
|
return false;
|
|
416
511
|
} else if (segment.position === 'even' && counter % 2 !== 0) {
|
|
417
512
|
return false;
|
|
418
|
-
} else if (segment.position === 'nth') {
|
|
419
|
-
|
|
420
|
-
return false;
|
|
421
|
-
}
|
|
513
|
+
} else if (segment.position === 'nth' && counter !== segment.positionValue) {
|
|
514
|
+
return false;
|
|
422
515
|
}
|
|
423
516
|
}
|
|
424
517
|
|
|
@@ -426,17 +519,17 @@ export default class Matcher {
|
|
|
426
519
|
}
|
|
427
520
|
|
|
428
521
|
/**
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
522
|
+
* Match any expression in the given set against the current path.
|
|
523
|
+
* @param {ExpressionSet} exprSet
|
|
524
|
+
* @returns {boolean}
|
|
525
|
+
*/
|
|
433
526
|
matchesAny(exprSet) {
|
|
434
527
|
return exprSet.matchesAny(this);
|
|
435
528
|
}
|
|
436
529
|
|
|
437
530
|
/**
|
|
438
|
-
* Create a snapshot of current state
|
|
439
|
-
* @returns {Object}
|
|
531
|
+
* Create a snapshot of current state.
|
|
532
|
+
* @returns {Object}
|
|
440
533
|
*/
|
|
441
534
|
snapshot() {
|
|
442
535
|
return {
|
|
@@ -446,97 +539,32 @@ export default class Matcher {
|
|
|
446
539
|
}
|
|
447
540
|
|
|
448
541
|
/**
|
|
449
|
-
* Restore state from snapshot
|
|
450
|
-
* @param {Object} snapshot
|
|
542
|
+
* Restore state from snapshot.
|
|
543
|
+
* @param {Object} snapshot
|
|
451
544
|
*/
|
|
452
545
|
restore(snapshot) {
|
|
453
|
-
//invalidate cache
|
|
454
546
|
this._pathStringCache = null;
|
|
455
|
-
this._frozenPathCache = null;
|
|
456
|
-
this._frozenSiblingsCache = null;
|
|
457
547
|
this.path = snapshot.path.map(node => ({ ...node }));
|
|
458
548
|
this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map));
|
|
459
549
|
}
|
|
460
550
|
|
|
461
551
|
/**
|
|
462
|
-
* Return
|
|
552
|
+
* Return the read-only {@link MatcherView} for this matcher.
|
|
463
553
|
*
|
|
464
|
-
* The returned
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
* are allowed but the returned arrays/objects are frozen so callers cannot
|
|
468
|
-
* mutate internal state through them either.
|
|
554
|
+
* The same instance is returned on every call — no allocation occurs.
|
|
555
|
+
* It always reflects the current parser state and is safe to pass to
|
|
556
|
+
* user callbacks without risk of accidental mutation.
|
|
469
557
|
*
|
|
470
|
-
* @returns {
|
|
558
|
+
* @returns {MatcherView}
|
|
471
559
|
*
|
|
472
560
|
* @example
|
|
473
|
-
* const
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* ro.getCurrentTag(); // ✓ works
|
|
479
|
-
* ro.push("child", {}); // ✗ throws TypeError
|
|
480
|
-
* ro.reset(); // ✗ throws TypeError
|
|
561
|
+
* const view = matcher.readOnly();
|
|
562
|
+
* // pass view to callbacks — it stays in sync automatically
|
|
563
|
+
* view.matches(expr); // ✓
|
|
564
|
+
* view.getCurrentTag(); // ✓
|
|
565
|
+
* // view.push(...) // ✗ method does not exist — caught by TypeScript
|
|
481
566
|
*/
|
|
482
567
|
readOnly() {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return new Proxy(self, {
|
|
486
|
-
get(target, prop, receiver) {
|
|
487
|
-
// Block mutating methods
|
|
488
|
-
if (MUTATING_METHODS.has(prop)) {
|
|
489
|
-
return () => {
|
|
490
|
-
throw new TypeError(
|
|
491
|
-
`Cannot call '${prop}' on a read-only Matcher. ` +
|
|
492
|
-
`Obtain a writable instance to mutate state.`
|
|
493
|
-
);
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Return cached frozen copy of path — rebuilt only after push/pop/updateCurrent/reset/restore
|
|
498
|
-
if (prop === 'path') {
|
|
499
|
-
if (target._frozenPathCache === null) {
|
|
500
|
-
target._frozenPathCache = Object.freeze(
|
|
501
|
-
target.path.map(node => Object.freeze({ ...node }))
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
return target._frozenPathCache;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Return cached frozen copy of siblingStacks — rebuilt only after push/pop/reset/restore
|
|
508
|
-
if (prop === 'siblingStacks') {
|
|
509
|
-
if (target._frozenSiblingsCache === null) {
|
|
510
|
-
target._frozenSiblingsCache = Object.freeze(
|
|
511
|
-
target.siblingStacks.map(map => Object.freeze(new Map(map)))
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
return target._frozenSiblingsCache;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const value = Reflect.get(target, prop, receiver);
|
|
518
|
-
|
|
519
|
-
// Bind methods so `this` inside them still refers to the real Matcher
|
|
520
|
-
if (typeof value === 'function') {
|
|
521
|
-
return value.bind(target);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return value;
|
|
525
|
-
},
|
|
526
|
-
|
|
527
|
-
// Prevent any property assignment on the read-only view
|
|
528
|
-
set(_target, prop) {
|
|
529
|
-
throw new TypeError(
|
|
530
|
-
`Cannot set property '${String(prop)}' on a read-only Matcher.`
|
|
531
|
-
);
|
|
532
|
-
},
|
|
533
|
-
|
|
534
|
-
// Prevent property deletion
|
|
535
|
-
deleteProperty(_target, prop) {
|
|
536
|
-
throw new TypeError(
|
|
537
|
-
`Cannot delete property '${String(prop)}' from a read-only Matcher.`
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
});
|
|
568
|
+
return this._view;
|
|
541
569
|
}
|
|
542
570
|
}
|