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/src/index.d.ts CHANGED
@@ -189,177 +189,81 @@ export interface MatcherSnapshot {
189
189
  }
190
190
 
191
191
  /**
192
- * ReadOnlyMatcher - A safe, read-only view over a {@link Matcher} instance.
192
+ * MatcherView - A lightweight read-only view over a {@link Matcher} instance.
193
193
  *
194
- * Returned by {@link Matcher.readOnly}. Exposes all query and inspection
195
- * methods but **throws a `TypeError`** if any state-mutating method is called
196
- * (`push`, `pop`, `reset`, `updateCurrent`, `restore`). Direct property
197
- * writes are also blocked.
194
+ * Created once by {@link Matcher} and reused across all callbacks no allocation
195
+ * on every invocation. Holds a direct reference to the parent Matcher's internal
196
+ * state so it always reflects the current parser position with zero copying or
197
+ * freezing overhead.
198
198
  *
199
- * Pass this to consumers that only need to inspect or match the current path
200
- * so they cannot accidentally corrupt the parser state.
199
+ * Mutation methods (`push`, `pop`, `reset`, `updateCurrent`, `restore`) are simply
200
+ * absent from this class, so misuse is caught at compile time by TypeScript rather
201
+ * than at runtime.
202
+ *
203
+ * Obtain via {@link Matcher#readOnly} — the same instance is returned every time.
201
204
  *
202
205
  * @example
203
206
  * ```typescript
204
207
  * const matcher = new Matcher();
208
+ * const view: MatcherView = matcher.readOnly();
209
+ *
205
210
  * matcher.push("root", {});
206
211
  * matcher.push("users", {});
207
212
  * matcher.push("user", { id: "123" });
208
213
  *
209
- * const ro: ReadOnlyMatcher = matcher.readOnly();
210
- *
211
- * ro.matches(expr); // ✓ works
212
- * ro.getCurrentTag(); // "user"
213
- * ro.getDepth(); // ✓ 3
214
- * ro.push("child", {}); // ✗ TypeError: Cannot call 'push' on a read-only Matcher
215
- * ro.reset(); // ✗ TypeError: Cannot call 'reset' on a read-only Matcher
214
+ * view.matches(expr); // ✓ true
215
+ * view.getCurrentTag(); // ✓ "user"
216
+ * view.getDepth(); // ✓ 3
217
+ * // view.push(...) // Property 'push' does not exist on type 'MatcherView'
216
218
  * ```
217
219
  */
218
- export interface ReadOnlyMatcher {
220
+ export class MatcherView {
219
221
  /**
220
- * Default path separator (read-only)
222
+ * Default path separator (read-only, delegates to parent Matcher)
221
223
  */
222
224
  readonly separator: string;
223
225
 
224
- /**
225
- * Current path stack (each node is a frozen copy)
226
- */
227
- readonly path: ReadonlyArray<Readonly<PathNode>>;
228
-
229
- // ── Query methods ───────────────────────────────────────────────────────────
230
-
231
- /**
232
- * Get current tag name
233
- * @returns Current tag name or undefined if path is empty
234
- */
235
226
  getCurrentTag(): string | undefined;
236
-
237
- /**
238
- * Get current namespace
239
- * @returns Current namespace or undefined if not present or path is empty
240
- */
241
227
  getCurrentNamespace(): string | undefined;
242
-
243
- /**
244
- * Get current node's attribute value
245
- * @param attrName - Attribute name
246
- * @returns Attribute value or undefined
247
- */
248
228
  getAttrValue(attrName: string): any;
249
-
250
- /**
251
- * Check if current node has an attribute
252
- * @param attrName - Attribute name
253
- */
254
229
  hasAttr(attrName: string): boolean;
255
-
256
- /**
257
- * Get current node's sibling position (child index in parent)
258
- * @returns Position index or -1 if path is empty
259
- */
260
230
  getPosition(): number;
261
-
262
- /**
263
- * Get current node's repeat counter (occurrence count of this tag name)
264
- * @returns Counter value or -1 if path is empty
265
- */
266
231
  getCounter(): number;
267
-
268
- /**
269
- * Get current node's sibling index (alias for getPosition for backward compatibility)
270
- * @returns Index or -1 if path is empty
271
- * @deprecated Use getPosition() or getCounter() instead
272
- */
232
+ /** @deprecated Use getPosition() or getCounter() instead */
273
233
  getIndex(): number;
274
-
275
- /**
276
- * Get current path depth
277
- * @returns Number of nodes in the path
278
- */
279
234
  getDepth(): number;
280
-
281
- /**
282
- * Get path as string
283
- * @param separator - Optional separator (uses default if not provided)
284
- * @param includeNamespace - Whether to include namespace in output
285
- * @returns Path string (e.g., "root.users.user" or "ns:root.ns:users.user")
286
- */
287
235
  toString(separator?: string, includeNamespace?: boolean): string;
288
-
289
- /**
290
- * Get path as array of tag names
291
- * @returns Array of tag names
292
- */
293
236
  toArray(): string[];
294
-
295
- /**
296
- * Match current path against an Expression
297
- * @param expression - The expression to match against
298
- * @returns True if current path matches the expression
299
- */
300
237
  matches(expression: Expression): boolean;
301
-
302
- /**
303
- * Test whether the matcher's current path matches **any** expression in the set.
304
- *
305
- * @param exprSet - A `ExpressionSet` instance
306
- * @returns `true` if at least one expression matches the current path
307
- */
308
238
  matchesAny(exprSet: ExpressionSet): boolean;
309
-
310
- /**
311
- * Create a snapshot of current state
312
- * @returns State snapshot that can be restored later
313
- */
314
- snapshot(): MatcherSnapshot;
315
-
316
- // ── Blocked mutating methods ────────────────────────────────────────────────
317
- // These are present in the type so callers get a compile-time error with a
318
- // helpful message instead of a silent "property does not exist" error.
319
-
320
- /**
321
- * @throws {TypeError} Always – mutation is not allowed on a read-only view.
322
- */
323
- push(tagName: string, attrValues?: Record<string, any> | null, namespace?: string | null): never;
324
-
325
- /**
326
- * @throws {TypeError} Always – mutation is not allowed on a read-only view.
327
- */
328
- pop(): never;
329
-
330
- /**
331
- * @throws {TypeError} Always – mutation is not allowed on a read-only view.
332
- */
333
- updateCurrent(attrValues: Record<string, any>): never;
334
-
335
- /**
336
- * @throws {TypeError} Always – mutation is not allowed on a read-only view.
337
- */
338
- reset(): never;
339
-
340
- /**
341
- * @throws {TypeError} Always – mutation is not allowed on a read-only view.
342
- */
343
- restore(snapshot: MatcherSnapshot): never;
344
239
  }
345
240
 
346
241
  /**
347
- * Matcher - Tracks current path in XML/JSON tree and matches against Expressions
348
- *
242
+ * @deprecated Use {@link MatcherView} instead.
243
+ * Alias kept for backward compatibility with code that references `ReadOnlyMatcher`.
244
+ */
245
+ export type ReadOnlyMatcher = MatcherView;
246
+
247
+ /**
248
+ * Matcher - Tracks current path in XML/JSON tree and matches against Expressions.
249
+ *
349
250
  * The matcher maintains a stack of nodes representing the current path from root to
350
251
  * current tag. It only stores attribute values for the current (top) node to minimize
351
252
  * memory usage.
352
- *
253
+ *
254
+ * Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to
255
+ * user callbacks — the same instance is reused on every call with no allocation overhead.
256
+ *
353
257
  * @example
354
258
  * ```typescript
355
259
  * const matcher = new Matcher();
356
260
  * matcher.push("root", {});
357
261
  * matcher.push("users", {});
358
262
  * matcher.push("user", { id: "123", type: "admin" });
359
- *
263
+ *
360
264
  * const expr = new Expression("root.users.user");
361
265
  * matcher.matches(expr); // true
362
- *
266
+ *
363
267
  * matcher.pop();
364
268
  * matcher.matches(expr); // false
365
269
  * ```
@@ -370,11 +274,6 @@ export class Matcher {
370
274
  */
371
275
  readonly separator: string;
372
276
 
373
- /**
374
- * Current path stack
375
- */
376
- readonly path: PathNode[];
377
-
378
277
  /**
379
278
  * Create a new Matcher
380
279
  * @param options - Configuration options
@@ -382,11 +281,11 @@ export class Matcher {
382
281
  constructor(options?: MatcherOptions);
383
282
 
384
283
  /**
385
- * Push a new tag onto the path
284
+ * Push a new tag onto the path.
386
285
  * @param tagName - Name of the tag
387
286
  * @param attrValues - Attribute key-value pairs for current node (optional)
388
287
  * @param namespace - Namespace for the tag (optional)
389
- *
288
+ *
390
289
  * @example
391
290
  * ```typescript
392
291
  * matcher.push("user", { id: "123", type: "admin" });
@@ -397,146 +296,63 @@ export class Matcher {
397
296
  push(tagName: string, attrValues?: Record<string, any> | null, namespace?: string | null): void;
398
297
 
399
298
  /**
400
- * Pop the last tag from the path
299
+ * Pop the last tag from the path.
401
300
  * @returns The popped node or undefined if path is empty
402
301
  */
403
302
  pop(): PathNode | undefined;
404
303
 
405
304
  /**
406
- * Update current node's attribute values
407
- * Useful when attributes are parsed after push
305
+ * Update current node's attribute values.
306
+ * Useful when attributes are parsed after push.
408
307
  * @param attrValues - Attribute values
409
308
  */
410
309
  updateCurrent(attrValues: Record<string, any>): void;
411
310
 
412
311
  /**
413
- * Get current tag name
414
- * @returns Current tag name or undefined if path is empty
312
+ * Reset the path to empty.
415
313
  */
416
- getCurrentTag(): string | undefined;
314
+ reset(): void;
417
315
 
418
316
  /**
419
- * Get current namespace
420
- * @returns Current namespace or undefined if not present or path is empty
317
+ * Create a snapshot of current state.
318
+ * @returns State snapshot that can be restored later
421
319
  */
422
- getCurrentNamespace(): string | undefined;
320
+ snapshot(): MatcherSnapshot;
423
321
 
424
322
  /**
425
- * Get current node's attribute value
426
- * @param attrName - Attribute name
427
- * @returns Attribute value or undefined
323
+ * Restore state from snapshot.
324
+ * @param snapshot - State snapshot from previous snapshot() call
428
325
  */
429
- getAttrValue(attrName: string): any;
326
+ restore(snapshot: MatcherSnapshot): void;
430
327
 
431
- /**
432
- * Check if current node has an attribute
433
- * @param attrName - Attribute name
434
- */
328
+ getCurrentTag(): string | undefined;
329
+ getCurrentNamespace(): string | undefined;
330
+ getAttrValue(attrName: string): any;
435
331
  hasAttr(attrName: string): boolean;
436
-
437
- /**
438
- * Get current node's sibling position (child index in parent)
439
- * @returns Position index or -1 if path is empty
440
- */
441
332
  getPosition(): number;
442
-
443
- /**
444
- * Get current node's repeat counter (occurrence count of this tag name)
445
- * @returns Counter value or -1 if path is empty
446
- */
447
333
  getCounter(): number;
448
-
449
- /**
450
- * Get current node's sibling index (alias for getPosition for backward compatibility)
451
- * @returns Index or -1 if path is empty
452
- * @deprecated Use getPosition() or getCounter() instead
453
- */
334
+ /** @deprecated Use getPosition() or getCounter() instead */
454
335
  getIndex(): number;
455
-
456
- /**
457
- * Get current path depth
458
- * @returns Number of nodes in the path
459
- */
460
336
  getDepth(): number;
461
-
462
- /**
463
- * Get path as string
464
- * @param separator - Optional separator (uses default if not provided)
465
- * @param includeNamespace - Whether to include namespace in output
466
- * @returns Path string (e.g., "root.users.user" or "ns:root.ns:users.user")
467
- */
468
337
  toString(separator?: string, includeNamespace?: boolean): string;
469
-
470
- /**
471
- * Get path as array of tag names
472
- * @returns Array of tag names
473
- */
474
338
  toArray(): string[];
339
+ matches(expression: Expression): boolean;
340
+ matchesAny(exprSet: ExpressionSet): boolean;
475
341
 
476
342
  /**
477
- * Reset the path to empty
478
- */
479
- reset(): void;
480
-
481
- /**
482
- * Match current path against an Expression
483
- * @param expression - The expression to match against
484
- * @returns True if current path matches the expression
485
- *
343
+ * Return the read-only {@link MatcherView} for this matcher.
344
+ *
345
+ * The same instance is returned on every call — no allocation occurs.
346
+ * Pass this to user callbacks; it always reflects current parser state.
347
+ *
486
348
  * @example
487
349
  * ```typescript
488
- * const expr = new Expression("root.users.user[id]");
489
- * const matcher = new Matcher();
490
- *
491
- * matcher.push("root");
492
- * matcher.push("users");
493
- * matcher.push("user", { id: "123" });
494
- *
495
- * matcher.matches(expr); // true
350
+ * const view = matcher.readOnly();
351
+ * // same reference every time — safe to cache
352
+ * view === matcher.readOnly(); // true
496
353
  * ```
497
354
  */
498
- matches(expression: Expression): boolean;
499
-
500
- /**
501
- * Test whether the matcher's current path matches **any** expression in the set.
502
- *
503
- * Uses the pre-built index to evaluate only the relevant bucket(s):
504
- * 1. Exact depth + tag — O(1) lookup
505
- * 2. Depth-matched wildcard tag — O(1) lookup
506
- * 3. Deep-wildcard expressions — always scanned (typically a small list)
507
- *
508
- * @param exprSet - A `ExpressionSet` instance
509
- * @returns `true` if at least one expression matches the current path
510
- *
511
- * @example
512
- * ```typescript
513
- * // Replaces:
514
- * // for (const expr of stopNodeExpressions) {
515
- * // if (matcher.matches(expr)) return true;
516
- * // }
517
- *
518
- * if (matcher.matchesAny(stopNodes)) {
519
- * // current tag is a stop node
520
- * }
521
- * ```
522
- */
523
- matchesAny(exprSet: ExpressionSet): boolean;
524
- /**
525
- * Create a snapshot of current state
526
- * @returns State snapshot that can be restored later
527
- */
528
- snapshot(): MatcherSnapshot;
529
-
530
- /**
531
- * Restore state from snapshot
532
- * @param snapshot - State snapshot from previous snapshot() call
533
- */
534
- restore(snapshot: MatcherSnapshot): void;
535
-
536
- /**
537
- * Return a read-only view of this matcher.
538
- */
539
- readOnly(): ReadOnlyMatcher;
355
+ readOnly(): MatcherView;
540
356
  }
541
357
 
542
358
  /**
@@ -673,7 +489,25 @@ export class ExpressionSet {
673
489
  * }
674
490
  * ```
675
491
  */
676
- matchesAny(matcher: Matcher | ReadOnlyMatcher): boolean;
492
+ matchesAny(matcher: Matcher | MatcherView): boolean;
493
+
494
+ /**
495
+ * Find the first expression in the set that matches the matcher's current path.
496
+ *
497
+ * Uses the pre-built index to evaluate only the relevant bucket(s):
498
+ * 1. Exact depth + tag — O(1) lookup
499
+ * 2. Depth-matched wildcard tag — O(1) lookup
500
+ * 3. Deep-wildcard expressions — always scanned (typically a small list)
501
+ *
502
+ * @param matcher - A `Matcher` instance or a `ReadOnlyMatcher` view
503
+ * @returns Expression if at least one expression matches the current path
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * const node = stopNodes.findMatch(matcher);
508
+ * ```
509
+ */
510
+ findMatch(matcher: Matcher | MatcherView): Expression;
677
511
  }
678
512
 
679
513
  /**
@@ -682,6 +516,7 @@ export class ExpressionSet {
682
516
  declare const _default: {
683
517
  Expression: typeof Expression;
684
518
  Matcher: typeof Matcher;
519
+ MatcherView: typeof MatcherView;
685
520
  ExpressionSet: typeof ExpressionSet;
686
521
  };
687
522