path-expression-matcher 1.4.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/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 - Configuration options
30
- * @param {string} options.separator - Default path separator (default: '.')
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: string, values: object, position: number, counter: number }
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._frozenPathCache = null; // cache for readOnly().path
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 - Name of the tag
47
- * @param {Object} attrValues - Attribute key-value pairs for current node (optional)
48
- * @param {string} namespace - Namespace for the tag (optional)
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
- this._frozenPathCache = null;
54
- this._frozenSiblingsCache = null;
197
+
55
198
  // Remove values from previous current node (now becoming ancestor)
56
199
  if (this.path.length > 0) {
57
- const prev = this.path[this.path.length - 1];
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
- this._frozenPathCache = null;
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 - Attribute values
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 - Attribute name
160
- * @returns {*} Attribute value or undefined
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
- const current = this.path[this.path.length - 1];
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 - Attribute name
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 for backward compatibility)
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 - Whether to include namespace in output (default: true)
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 && this._pathStringCache !== undefined) {
358
+ if (this._pathStringCache !== null) {
226
359
  return this._pathStringCache;
227
360
  }
228
361
  const result = this.path.map(n =>
229
- (includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
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 - The expression to match against
264
- * @returns {boolean} True if current path matches the expression
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
- const segment = segments[i];
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; // Start from current node (bottom)
312
- let segIdx = segments.length - 1; // Start from last segment
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
- const isCurrentNode = (i === this.path.length - 1);
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
- // Regular segment
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
- const actualValue = node.values[segment.attrName];
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
- if (counter !== segment.positionValue) {
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
- * Match any expression in the given set against the current path.
430
- * @param {ExpressionSet} exprSet - The set of expressions to match against.
431
- * @returns {boolean} - True if any expression in the set matches the current path, false otherwise.
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} State snapshot
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 - State 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 a read-only view of this matcher.
552
+ * Return the read-only {@link MatcherView} for this matcher.
463
553
  *
464
- * The returned object exposes all query/inspection methods but throws a
465
- * TypeError if any state-mutating method is called (`push`, `pop`, `reset`,
466
- * `updateCurrent`, `restore`). Property reads (e.g. `.path`, `.separator`)
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 {ReadOnlyMatcher} A proxy that forwards read operations and blocks writes.
558
+ * @returns {MatcherView}
471
559
  *
472
560
  * @example
473
- * const matcher = new Matcher();
474
- * matcher.push("root", {});
475
- *
476
- * const ro = matcher.readOnly();
477
- * ro.matches(expr); // works
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
- const self = this;
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
  }