Auto-commit 2026-04-29 16:31

This commit is contained in:
2026-04-29 16:31:27 -04:00
parent e8687bb6b2
commit 0495ee5bd2
19691 changed files with 3272886 additions and 138 deletions

21
node_modules/path-expression-matcher/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

872
node_modules/path-expression-matcher/README.md generated vendored Normal file
View File

@@ -0,0 +1,872 @@
# path-expression-matcher
Efficient path tracking and pattern matching for XML, JSON, YAML or any other parsers.
## 🎯 Purpose
`path-expression-matcher` provides three core classes for tracking and matching paths:
- **`Expression`**: Parses and stores pattern expressions (e.g., `"root.users.user[id]"`)
- **`Matcher`**: Tracks current path during parsing and matches against expressions
- **`MatcherView`**: A lightweight read-only view of a `Matcher`, safe to pass to callbacks
Compatible with [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) and similar tools.
## 📦 Installation
```bash
npm install path-expression-matcher
```
## 🚀 Quick Start
```javascript
import { Expression, Matcher } from 'path-expression-matcher';
// Create expression (parse once, reuse many times)
const expr = new Expression("root.users.user");
// Create matcher (tracks current path)
const matcher = new Matcher();
matcher.push("root");
matcher.push("users");
matcher.push("user", { id: "123" });
// Match current path against expression
if (matcher.matches(expr)) {
console.log("Match found!");
console.log("Current path:", matcher.toString()); // "root.users.user"
}
// Namespace support
const nsExpr = new Expression("soap::Envelope.soap::Body..ns::UserId");
matcher.push("Envelope", null, "soap");
matcher.push("Body", null, "soap");
matcher.push("UserId", null, "ns");
console.log(matcher.toString()); // "soap:Envelope.soap:Body.ns:UserId"
```
## 📖 Pattern Syntax
### Basic Paths
```javascript
"root.users.user" // Exact path match
"*.users.user" // Wildcard: any parent
"root.*.user" // Wildcard: any middle
"root.users.*" // Wildcard: any child
```
### Deep Wildcard
```javascript
"..user" // user anywhere in tree
"root..user" // user anywhere under root
"..users..user" // users somewhere, then user below it
```
### Attribute Matching
```javascript
"user[id]" // user with "id" attribute
"user[type=admin]" // user with type="admin" (current node only)
"root[lang]..user" // user under root that has "lang" attribute
```
### Position Selectors
```javascript
"user:first" // First user (counter=0)
"user:nth(2)" // Third user (counter=2, zero-based)
"user:odd" // Odd-numbered users (counter=1,3,5...)
"user:even" // Even-numbered users (counter=0,2,4...)
"root.users.user:first" // First user under users
```
**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.
### Namespaces
```javascript
"ns::user" // user with namespace "ns"
"soap::Envelope" // Envelope with namespace "soap"
"ns::user[id]" // user with namespace "ns" and "id" attribute
"ns::user:first" // First user with namespace "ns"
"*::user" // user with any namespace
"..ns::item" // item with namespace "ns" anywhere in tree
"soap::Envelope.soap::Body" // Nested namespaced elements
"ns::first" // Tag named "first" with namespace "ns" (NO ambiguity!)
```
**Namespace syntax:**
- Use **double colon (::)** for namespace: `ns::tag`
- Use **single colon (:)** for position: `tag:first`
- Combined: `ns::tag:first` (namespace + tag + position)
**Namespace matching rules:**
- Pattern `ns::user` matches only nodes with namespace "ns" and tag "user"
- Pattern `user` (no namespace) matches nodes with tag "user" regardless of namespace
- Pattern `*::user` matches tag "user" with any namespace (wildcard namespace)
- Namespaces are tracked separately for counter/position (e.g., `ns1::item` and `ns2::item` have independent counters)
### Wildcard Differences
**Single wildcard (`*`)** - Matches exactly ONE level:
- `"*.fix1"` matches `root.fix1` (2 levels) ✅
- `"*.fix1"` does NOT match `root.another.fix1` (3 levels) ❌
- Path depth MUST equal pattern depth
**Deep wildcard (`..`)** - Matches ZERO or MORE levels:
- `"..fix1"` matches `root.fix1`
- `"..fix1"` matches `root.another.fix1`
- `"..fix1"` matches `a.b.c.d.fix1`
- Works at any depth
### Combined Patterns
```javascript
"..user[id]:first" // First user with id, anywhere
"root..user[type=admin]" // Admin user under root
"ns::user[id]:first" // First namespaced user with id
"soap::Envelope..ns::UserId" // UserId with namespace ns under SOAP envelope
```
## 🔧 API Reference
### Expression
#### Constructor
```javascript
new Expression(pattern, options = {}, data)
```
**Parameters:**
- `pattern` (string): Pattern to parse
- `options.separator` (string): Path separator (default: `'.'`)
**Example:**
```javascript
const expr1 = new Expression("root.users.user");
const expr2 = new Expression("root/users/user", { separator: '/' });
const expr3 = new Expression("root/users/user", { separator: '/' }, { extra: "data"});
console.log(expr3.data) // { extra: "data" }
```
#### Methods
- `hasDeepWildcard()` → boolean
- `hasAttributeCondition()` → boolean
- `hasPositionSelector()` → boolean
- `toString()` → string
### Matcher
#### Constructor
```javascript
new Matcher(options)
```
**Parameters:**
- `options.separator` (string): Default path separator (default: `'.'`)
#### Path Tracking Methods
##### `push(tagName, attrValues, namespace)`
Add a tag to the current path. Position and counter are automatically calculated.
**Parameters:**
- `tagName` (string): Tag name
- `attrValues` (object, optional): Attribute key-value pairs (current node only)
- `namespace` (string, optional): Namespace for the tag
**Example:**
```javascript
matcher.push("user", { id: "123", type: "admin" });
matcher.push("item"); // No attributes
matcher.push("Envelope", null, "soap"); // With namespace
matcher.push("Body", { version: "1.1" }, "soap"); // With both
```
**Position vs Counter:**
- **Position**: The child index in the parent (0, 1, 2, 3...)
- **Counter**: How many times this tag name appeared at this level (0, 1, 2...)
Example:
```xml
<root>
<a/> <!-- position=0, counter=0 -->
<b/> <!-- position=1, counter=0 -->
<a/> <!-- position=2, counter=1 -->
</root>
```
##### `pop()`
Remove the last tag from the path.
```javascript
matcher.pop();
```
##### `updateCurrent(attrValues)`
Update current node's attributes (useful when attributes are parsed after push).
```javascript
matcher.push("user"); // Don't know values yet
// ... parse attributes ...
matcher.updateCurrent({ id: "123" });
```
##### `reset()`
Clear the entire path.
```javascript
matcher.reset();
```
#### Query Methods
##### `matches(expression)`
Check if current path matches an Expression.
```javascript
const expr = new Expression("root.users.user");
if (matcher.matches(expr)) {
// Current path matches
}
```
#### `matchesAny(exprSet)` → `boolean`
Please check `ExpressionSet` class for more details.
```javascript
const matcher = new Matcher();
const exprSet = new ExpressionSet();
exprSet.add(new Expression("root.users.user"));
exprSet.add(new Expression("root.config.*"));
exprSet.seal();
if (matcher.matchesAny(exprSet)) {
// Current path matches any expression in the set
}
```
##### `getCurrentTag()`
Get current tag name.
```javascript
const tag = matcher.getCurrentTag(); // "user"
```
##### `getCurrentNamespace()`
Get current namespace.
```javascript
const ns = matcher.getCurrentNamespace(); // "soap" or undefined
```
##### `getAttrValue(attrName)`
Get attribute value of current node.
```javascript
const id = matcher.getAttrValue("id"); // "123"
```
##### `hasAttr(attrName)`
Check if current node has an attribute.
```javascript
if (matcher.hasAttr("id")) {
// Current node has "id" attribute
}
```
##### `getPosition()`
Get sibling position of current node (child index in parent).
```javascript
const position = matcher.getPosition(); // 0, 1, 2, ...
```
##### `getCounter()`
Get repeat counter of current node (occurrence count of this tag name).
```javascript
const counter = matcher.getCounter(); // 0, 1, 2, ...
```
##### `getIndex()` (deprecated)
Alias for `getPosition()`. Use `getPosition()` or `getCounter()` instead for clarity.
```javascript
const index = matcher.getIndex(); // Same as getPosition()
```
##### `getDepth()`
Get current path depth.
```javascript
const depth = matcher.getDepth(); // 3 for "root.users.user"
```
##### `toString(separator?, includeNamespace?)`
Get path as string.
**Parameters:**
- `separator` (string, optional): Path separator (uses default if not provided)
- `includeNamespace` (boolean, optional): Whether to include namespaces (default: true)
```javascript
const path = matcher.toString(); // "root.ns:user.item"
const path2 = matcher.toString('/'); // "root/ns:user/item"
const path3 = matcher.toString('.', false); // "root.user.item" (no namespaces)
```
##### `toArray()`
Get path as array.
```javascript
const arr = matcher.toArray(); // ["root", "users", "user"]
```
#### State Management
##### `snapshot()`
Create a snapshot of current state.
```javascript
const snapshot = matcher.snapshot();
```
##### `restore(snapshot)`
Restore from a snapshot.
```javascript
matcher.restore(snapshot);
```
#### Read-Only Access
##### `readOnly()`
Returns a **`MatcherView`** — a lightweight, live read-only view of the matcher. All query and inspection methods work normally and always reflect the current state of the underlying matcher. Mutation methods (`push`, `pop`, `reset`, `updateCurrent`, `restore`) simply don't exist on `MatcherView`, so misuse is caught at **compile time** by TypeScript rather than at runtime.
The **same instance** is returned on every call — no allocation occurs per invocation. This is the recommended way to share the matcher with callbacks, plugins, or any external code that only needs to inspect the current path.
```javascript
const view = matcher.readOnly();
// Same reference every time — safe to cache
view === matcher.readOnly(); // true
```
**What works on the view:**
```javascript
view.matches(expr) // ✓ pattern matching
view.getCurrentTag() // ✓ current tag name
view.getCurrentNamespace() // ✓ current namespace
view.getAttrValue("id") // ✓ attribute value
view.hasAttr("id") // ✓ attribute presence check
view.getPosition() // ✓ sibling position
view.getCounter() // ✓ occurrence counter
view.getDepth() // ✓ path depth
view.toString() // ✓ path as string
view.toArray() // ✓ path as array
```
**What doesn't exist (compile-time error in TypeScript):**
```javascript
view.push("child", {}) // ✗ Property 'push' does not exist on type 'MatcherView'
view.pop() // ✗ Property 'pop' does not exist on type 'MatcherView'
view.reset() // ✗ Property 'reset' does not exist on type 'MatcherView'
view.updateCurrent({}) // ✗ Property 'updateCurrent' does not exist on type 'MatcherView'
view.restore(snapshot) // ✗ Property 'restore' does not exist on type 'MatcherView'
```
**The view is live** — it always reflects the current state of the underlying matcher.
```javascript
const matcher = new Matcher();
const view = matcher.readOnly();
matcher.push("root");
view.getDepth(); // 1 — immediately reflects the push
matcher.push("users");
view.getDepth(); // 2 — still live
```
## 💡 Usage Examples
### Example 1: XML Parser with stopNodes
```javascript
import { XMLParser } from 'fast-xml-parser';
import { Expression, Matcher } from 'path-expression-matcher';
class MyParser {
constructor() {
this.matcher = new Matcher();
// Pre-compile stop node patterns
this.stopNodeExpressions = [
new Expression("html.body.script"),
new Expression("html.body.style"),
new Expression("..svg"),
];
}
parseTag(tagName, attrs) {
this.matcher.push(tagName, attrs);
// Check if this is a stop node
for (const expr of this.stopNodeExpressions) {
if (this.matcher.matches(expr)) {
// Don't parse children, read as raw text
return this.readRawContent();
}
}
// Continue normal parsing
this.parseChildren();
this.matcher.pop();
}
}
```
### Example 2: Conditional Processing
```javascript
const matcher = new Matcher();
const userExpr = new Expression("..user[type=admin]");
const firstItemExpr = new Expression("..item:first");
function processTag(tagName, value, attrs) {
matcher.push(tagName, attrs);
if (matcher.matches(userExpr)) {
value = enhanceAdminUser(value);
}
if (matcher.matches(firstItemExpr)) {
value = markAsFirst(value);
}
matcher.pop();
return value;
}
```
### Example 3: Path-based Filtering
```javascript
const patterns = [
new Expression("data.users.user"),
new Expression("data.posts.post"),
new Expression("..comment[approved=true]"),
];
function shouldInclude(matcher) {
return patterns.some(expr => matcher.matches(expr));
}
```
### Example 4: Custom Separator
```javascript
const matcher = new Matcher({ separator: '/' });
const expr = new Expression("root/config/database", { separator: '/' });
matcher.push("root");
matcher.push("config");
matcher.push("database");
console.log(matcher.toString()); // "root/config/database"
console.log(matcher.matches(expr)); // true
```
### Example 5: Attribute Checking
```javascript
const matcher = new Matcher();
matcher.push("root");
matcher.push("user", { id: "123", type: "admin", status: "active" });
// Check attribute existence (current node only)
console.log(matcher.hasAttr("id")); // true
console.log(matcher.hasAttr("email")); // false
// Get attribute value (current node only)
console.log(matcher.getAttrValue("type")); // "admin"
// Match by attribute
const expr1 = new Expression("user[id]");
console.log(matcher.matches(expr1)); // true
const expr2 = new Expression("user[type=admin]");
console.log(matcher.matches(expr2)); // true
```
### Example 6: Position vs Counter
```javascript
const matcher = new Matcher();
matcher.push("root");
// Mixed tags at same level
matcher.push("item"); // position=0, counter=0 (first item)
matcher.pop();
matcher.push("div"); // position=1, counter=0 (first div)
matcher.pop();
matcher.push("item"); // position=2, counter=1 (second item)
console.log(matcher.getPosition()); // 2 (third child overall)
console.log(matcher.getCounter()); // 1 (second "item" specifically)
// :first uses counter, not position
const expr = new Expression("root.item:first");
console.log(matcher.matches(expr)); // false (counter=1, not 0)
```
### Example 8: Passing a Read-Only View to External Consumers
When passing the matcher into callbacks, plugins, or other code you don't control, use `readOnly()` to get a `MatcherView` — it can inspect but never mutate parser state.
```javascript
import { Expression, Matcher } from 'path-expression-matcher';
const matcher = new Matcher();
const adminExpr = new Expression("..user[type=admin]");
function parseTag(tagName, attrs, onTag) {
matcher.push(tagName, attrs);
// Pass MatcherView — consumer can inspect but not mutate
onTag(matcher.readOnly());
matcher.pop();
}
// Safe consumer — can only read
function myPlugin(view) {
if (view.matches(adminExpr)) {
console.log("Admin at path:", view.toString());
console.log("Depth:", view.getDepth());
console.log("ID:", view.getAttrValue("id"));
}
}
// view.push(...) or view.reset() don't exist on MatcherView —
// TypeScript catches misuse at compile time.
parseTag("user", { id: "1", type: "admin" }, myPlugin);
```
```javascript
const matcher = new Matcher();
const soapExpr = new Expression("soap::Envelope.soap::Body..ns::UserId");
// Parse SOAP document
matcher.push("Envelope", { xmlns: "..." }, "soap");
matcher.push("Body", null, "soap");
matcher.push("GetUserRequest", null, "ns");
matcher.push("UserId", null, "ns");
// Match namespaced pattern
if (matcher.matches(soapExpr)) {
console.log("Found UserId in SOAP body");
console.log(matcher.toString()); // "soap:Envelope.soap:Body.ns:GetUserRequest.ns:UserId"
}
// Namespace-specific counters
matcher.reset();
matcher.push("root");
matcher.push("item", null, "ns1"); // ns1::item counter=0
matcher.pop();
matcher.push("item", null, "ns2"); // ns2::item counter=0 (different namespace)
matcher.pop();
matcher.push("item", null, "ns1"); // ns1::item counter=1
const firstNs1Item = new Expression("root.ns1::item:first");
console.log(matcher.matches(firstNs1Item)); // false (counter=1)
const secondNs1Item = new Expression("root.ns1::item:nth(1)");
console.log(matcher.matches(secondNs1Item)); // true
// NO AMBIGUITY: Tags named after position keywords
matcher.reset();
matcher.push("root");
matcher.push("first", null, "ns"); // Tag named "first" with namespace
const expr = new Expression("root.ns::first");
console.log(matcher.matches(expr)); // true - matches namespace "ns", tag "first"
```
## 🏗️ Architecture
### Data Storage Strategy
**Ancestor nodes:** Store only tag name, position, and counter (minimal memory)
**Current node:** Store tag name, position, counter, and attribute values
This design minimizes memory usage:
- No attribute names stored (derived from values object when needed)
- Attribute values only for current node, not ancestors
- Attribute checking for ancestors is not supported (acceptable trade-off)
- For 1M nodes with 3 attributes each, saves ~50MB vs storing attribute names
### Matching Strategy
Matching is performed **bottom-to-top** (from current node toward root):
1. Start at current node
2. Match segments from pattern end to start
3. Attribute checking only works for current node (ancestors have no attribute data)
4. Position selectors use **counter** (occurrence count), not position (child index)
### Performance
- **Expression parsing:** One-time cost when Expression is created
- **Expression analysis:** Cached (hasDeepWildcard, hasAttributeCondition, hasPositionSelector)
- **Path tracking:** O(1) for push/pop operations
- **Pattern matching:** O(n*m) where n = path depth, m = pattern segments
- **Memory per ancestor node:** ~40-60 bytes (tag, position, counter only)
- **Memory per current node:** ~80-120 bytes (adds attribute values)
## 🎓 Design Patterns
### Pre-compile Patterns (Recommended)
```javascript
// ✅ GOOD: Parse once, reuse many times
const expr = new Expression("..user[id]");
for (let i = 0; i < 1000; i++) {
if (matcher.matches(expr)) {
// ...
}
}
```
```javascript
// ❌ BAD: Parse on every iteration
for (let i = 0; i < 1000; i++) {
if (matcher.matches(new Expression("..user[id]"))) {
// ...
}
}
```
### Batch Pattern Checking with ExpressionSet (Recommended)
For checking multiple patterns on every tag, use `ExpressionSet` instead of a manual loop.
It pre-indexes expressions at build time so each call to `matchesAny()` does an O(1) bucket
lookup rather than a full O(N) scan:
```javascript
import { Expression, ExpressionSet, Matcher } from 'path-expression-matcher';
// Build once at config/startup time
const stopNodes = new ExpressionSet();
stopNodes
.add(new Expression('root.users.user'))
.add(new Expression('root.config.*'))
.add(new Expression('..script'))
.seal(); // prevent accidental mutation during parsing
// Per-tag — hot path
if (stopNodes.matchesAny(matcher)) {
// handle stop node
}
```
This replaces the manual loop pattern:
```javascript
// ❌ Before — O(N) per tag
function isStopNode(expressions, matcher) {
for (let i = 0; i < expressions.length; i++) {
if (matcher.matches(expressions[i])) return true;
}
return false;
}
// ✅ After — O(1) lookup per tag
const stopNodes = new ExpressionSet();
stopNodes.addAll(expressions);
stopNodes.matchesAny(matcher);
//or matcher.matchesAny(stopNodes)
```
---
## 📦 ExpressionSet API
`ExpressionSet` is an indexed collection of `Expression` objects designed for efficient
bulk matching. Build it once from your config, then call `matchesAny()` on every tag.
### Constructor
```javascript
const set = new ExpressionSet();
```
### `add(expression)` → `this`
Add a single `Expression`. Duplicate patterns (same pattern string) are silently ignored.
Returns `this` for chaining. Throws `TypeError` if the set is sealed.
```javascript
set.add(new Expression('root.users.user'));
set.add(new Expression('..script'));
```
### `addAll(expressions)` → `this`
Add an array of `Expression` objects at once. Returns `this` for chaining.
```javascript
set.addAll(config.stopNodes.map(p => new Expression(p)));
```
### `has(expression)` → `boolean`
Check whether an expression with the same pattern is already present.
```javascript
set.has(new Expression('root.users.user')); // true / false
```
### `seal()` → `this`
Prevent further additions. Any subsequent call to `add()` or `addAll()` throws a `TypeError`.
Useful to guard against accidental mutation once parsing has started.
```javascript
const stopNodes = new ExpressionSet();
stopNodes.addAll(patterns).seal();
stopNodes.add(new Expression('root.extra')); // ❌ TypeError: ExpressionSet is sealed
```
### `size` → `number`
Number of distinct expressions in the set.
```javascript
set.size; // 3
```
### `isSealed` → `boolean`
Whether `seal()` has been called.
### `matchesAny(matcher)` → `boolean`
Returns `true` if the matcher's current path matches **any** expression in the set.
Accepts both a `Matcher` instance and a `MatcherView`.
```javascript
if (stopNodes.matchesAny(matcher)) { /* ... */ }
if (stopNodes.matchesAny(matcher.readOnly())) { /* ... */ } // also works
```
**How indexing works:** expressions are bucketed at `add()` time, not at match time.
| Expression type | Bucket | Lookup cost |
|---|---|---|
| Fixed path, concrete tag (`root.users.user`) | `depth:tag` map | O(1) |
| Fixed path, wildcard tag (`root.config.*`) | `depth` map | O(1) |
| Deep wildcard (`..script`) | flat list | O(D) — always scanned |
In practice, deep-wildcard expressions are rare in configs, so the list stays small.
### `findMatch(matcher)` → `Expression`
Returns the Expression instance that matched the current path. Accepts both a `Matcher` instance and a `MatcherView`.
```javascript
const node = stopNodes.findMatch(matcher);
```
### Example 7: ExpressionSet in a real parser loop
```javascript
import { XMLParser } from 'fast-xml-parser';
import { Expression, ExpressionSet, Matcher } from 'path-expression-matcher';
// Config-time setup
const stopNodes = new ExpressionSet();
stopNodes
.addAll(['script', 'style'].map(t => new Expression(`..${t}`)))
.seal();
const matcher = new Matcher();
const parser = new XMLParser({
onOpenTag(tagName, attrs) {
matcher.push(tagName, attrs);
if (stopNodes.matchesAny(matcher)) {
// treat as stop node
}
},
onCloseTag() {
matcher.pop();
},
});
```
## 🔗 Integration with fast-xml-parser
**Basic integration:**
```javascript
import { XMLParser } from 'fast-xml-parser';
import { Expression, Matcher } from 'path-expression-matcher';
const parser = new XMLParser({
// Custom options using path-expression-matcher
stopNodes: ["script", "style"].map(tag => new Expression(`..${tag}`)),
tagValueProcessor: (tagName, value, jPath, hasAttrs, isLeaf, matcher) => {
// matcher is available in callbacks
if (matcher.matches(new Expression("..user[type=admin]"))) {
return enhanceValue(value);
}
return value;
}
});
```
## 📄 License
MIT
## 🤝 Contributing
Issues and PRs welcome! This package is designed to be used by XML/JSON parsers like fast-xml-parser. But can be used with any formar parser.

1
node_modules/path-expression-matcher/lib/pem.cjs generated vendored Normal file

File diff suppressed because one or more lines are too long

634
node_modules/path-expression-matcher/lib/pem.d.cts generated vendored Normal file
View File

@@ -0,0 +1,634 @@
/**
* TypeScript definitions for path-expression-matcher (CommonJS)
*/
/**
* Options for creating an Expression
*/
declare interface ExpressionOptions {
/**
* Path separator character
* @default '.'
*/
separator?: string;
}
/**
* Parsed segment from an expression pattern
*/
declare interface Segment {
/**
* Type of segment
*/
type: 'tag' | 'deep-wildcard';
/**
* Tag name (e.g., "user", "*" for wildcard)
* Only present when type is 'tag'
*/
tag?: string;
/**
* Namespace prefix (e.g., "ns" in "ns::user")
* Only present when namespace is specified
*/
namespace?: string;
/**
* Attribute name to match (e.g., "id" in "user[id]")
* Only present when attribute condition exists
*/
attrName?: string;
/**
* Attribute value to match (e.g., "123" in "user[id=123]")
* Only present when attribute value is specified
*/
attrValue?: string;
/**
* Position selector type
* Only present when position selector exists
*/
position?: 'first' | 'last' | 'odd' | 'even' | 'nth';
/**
* Numeric value for nth() selector
* Only present when position is 'nth'
*/
positionValue?: number;
}
/**
* Expression - Parses and stores a tag pattern expression
*
* Patterns are parsed once and stored in an optimized structure for fast matching.
*
* @example
* ```javascript
* const { Expression } = require('path-expression-matcher');
* const expr = new Expression("root.users.user");
* const expr2 = new Expression("..user[id]:first");
* const expr3 = new Expression("root/users/user", { separator: '/' });
* ```
*
* Pattern Syntax:
* - `root.users.user` - Match exact path
* - `..user` - Match "user" at any depth (deep wildcard)
* - `user[id]` - Match user tag with "id" attribute
* - `user[id=123]` - Match user tag where id="123"
* - `user:first` - Match first occurrence of user tag
* - `ns::user` - Match user tag with namespace "ns"
* - `ns::user[id]:first` - Combine namespace, attribute, and position
* ```
*/
declare class Expression {
/**
* Original pattern string
*/
readonly pattern: string;
/**
* Path separator character
*/
readonly separator: string;
/**
* Parsed segments
*/
readonly segments: Segment[];
/**
* Create a new Expression
* @param pattern - Pattern string (e.g., "root.users.user", "..user[id]")
* @param options - Configuration options
*/
constructor(pattern: string, options?: ExpressionOptions);
/**
* Get the number of segments
*/
get length(): number;
/**
* Check if expression contains deep wildcard (..)
*/
hasDeepWildcard(): boolean;
/**
* Check if expression has attribute conditions
*/
hasAttributeCondition(): boolean;
/**
* Check if expression has position selectors
*/
hasPositionSelector(): boolean;
/**
* Get string representation
*/
toString(): string;
}
/**
* Options for creating a Matcher
*/
declare interface MatcherOptions {
/**
* Default path separator
* @default '.'
*/
separator?: string;
}
/**
* Internal node structure in the path stack
*/
declare interface PathNode {
/**
* Tag name
*/
tag: string;
/**
* Namespace (if present)
*/
namespace?: string;
/**
* Position in sibling list (child index in parent)
*/
position: number;
/**
* Counter (occurrence count of this tag name)
*/
counter: number;
/**
* Attribute key-value pairs
* Only present for the current (last) node in path
*/
values?: Record<string, any>;
}
/**
* Snapshot of matcher state
*/
declare interface MatcherSnapshot {
/**
* Copy of the path stack
*/
path: PathNode[];
/**
* Copy of sibling tracking maps
*/
siblingStacks: Map<string, number>[];
}
/**
* ReadOnlyMatcher - A safe, read-only view over a {@link Matcher} instance.
*
* Returned by {@link Matcher.readOnly}. Exposes all query and inspection
* methods but **throws a `TypeError`** if any state-mutating method is called
* (`push`, `pop`, `reset`, `updateCurrent`, `restore`). Direct property
* writes are also blocked.
*
* Pass this to consumers that only need to inspect or match the current path
* so they cannot accidentally corrupt the parser state.
*
* @example
* ```javascript
* const matcher = new Matcher();
* matcher.push("root", {});
* matcher.push("users", {});
* matcher.push("user", { id: "123" });
*
* const ro: ReadOnlyMatcher = matcher.readOnly();
*
* ro.matches(expr); // ✓ works
* ro.getCurrentTag(); // ✓ "user"
* ro.getDepth(); // ✓ 3
* ro.push("child", {}); // ✗ TypeError: Cannot call 'push' on a read-only Matcher
* ro.reset(); // ✗ TypeError: Cannot call 'reset' on a read-only Matcher
* ```
*/
declare interface ReadOnlyMatcher {
/**
* Default path separator (read-only)
*/
readonly separator: string;
/**
* Current path stack (each node is a frozen copy)
*/
readonly path: ReadonlyArray<Readonly<PathNode>>;
// ── Query methods ───────────────────────────────────────────────────────────
/**
* Get current tag name
* @returns Current tag name or undefined if path is empty
*/
getCurrentTag(): string | undefined;
/**
* Get current namespace
* @returns Current namespace or undefined if not present or path is empty
*/
getCurrentNamespace(): string | undefined;
/**
* Get current node's attribute value
* @param attrName - Attribute name
* @returns Attribute value or undefined
*/
getAttrValue(attrName: string): any;
/**
* Check if current node has an attribute
* @param attrName - Attribute name
*/
hasAttr(attrName: string): boolean;
/**
* Get current node's sibling position (child index in parent)
* @returns Position index or -1 if path is empty
*/
getPosition(): number;
/**
* Get current node's repeat counter (occurrence count of this tag name)
* @returns Counter value or -1 if path is empty
*/
getCounter(): number;
/**
* Get current node's sibling index (alias for getPosition for backward compatibility)
* @returns Index or -1 if path is empty
* @deprecated Use getPosition() or getCounter() instead
*/
getIndex(): number;
/**
* Get current path depth
* @returns Number of nodes in the path
*/
getDepth(): number;
/**
* Get path as string
* @param separator - Optional separator (uses default if not provided)
* @param includeNamespace - Whether to include namespace in output
* @returns Path string (e.g., "root.users.user" or "ns:root.ns:users.user")
*/
toString(separator?: string, includeNamespace?: boolean): string;
/**
* Get path as array of tag names
* @returns Array of tag names
*/
toArray(): string[];
/**
* Match current path against an Expression
* @param expression - The expression to match against
* @returns True if current path matches the expression
*/
matches(expression: Expression): boolean;
/**
* Test whether the matcher's current path matches **any** expression in the set.
*
* @param exprSet - A `ExpressionSet` instance
* @returns `true` if at least one expression matches the current path
*/
matchesAny(exprSet: ExpressionSet): boolean;
/**
* Create a snapshot of current state
* @returns State snapshot that can be restored later
*/
snapshot(): MatcherSnapshot;
// ── Blocked mutating methods ────────────────────────────────────────────────
// These are present in the type so callers get a compile-time error with a
// helpful message instead of a silent "property does not exist" error.
/**
* @throws {TypeError} Always mutation is not allowed on a read-only view.
*/
push(tagName: string, attrValues?: Record<string, any> | null, namespace?: string | null): never;
/**
* @throws {TypeError} Always mutation is not allowed on a read-only view.
*/
pop(): never;
/**
* @throws {TypeError} Always mutation is not allowed on a read-only view.
*/
updateCurrent(attrValues: Record<string, any>): never;
/**
* @throws {TypeError} Always mutation is not allowed on a read-only view.
*/
reset(): never;
/**
* @throws {TypeError} Always mutation is not allowed on a read-only view.
*/
restore(snapshot: MatcherSnapshot): never;
}
/**
* Matcher - Tracks current path in XML/JSON tree and matches against Expressions
*
* The matcher maintains a stack of nodes representing the current path from root to
* current tag. It only stores attribute values for the current (top) node to minimize
* memory usage.
*
* @example
* ```javascript
* const { Matcher } = require('path-expression-matcher');
* const matcher = new Matcher();
* matcher.push("root", {});
* matcher.push("users", {});
* matcher.push("user", { id: "123", type: "admin" });
*
* const expr = new Expression("root.users.user");
* matcher.matches(expr); // true
*
* matcher.pop();
* matcher.matches(expr); // false
* ```
*/
declare class Matcher {
/**
* Default path separator
*/
readonly separator: string;
/**
* Current path stack
*/
readonly path: PathNode[];
/**
* Create a new Matcher
* @param options - Configuration options
*/
constructor(options?: MatcherOptions);
/**
* Push a new tag onto the path
* @param tagName - Name of the tag
* @param attrValues - Attribute key-value pairs for current node (optional)
* @param namespace - Namespace for the tag (optional)
*
* @example
* ```javascript
* matcher.push("user", { id: "123", type: "admin" });
* matcher.push("user", { id: "456" }, "ns");
* matcher.push("container", null);
* ```
*/
push(tagName: string, attrValues?: Record<string, any> | null, namespace?: string | null): void;
/**
* Pop the last tag from the path
* @returns The popped node or undefined if path is empty
*/
pop(): PathNode | undefined;
/**
* Update current node's attribute values
* Useful when attributes are parsed after push
* @param attrValues - Attribute values
*/
updateCurrent(attrValues: Record<string, any>): void;
/**
* Get current tag name
* @returns Current tag name or undefined if path is empty
*/
getCurrentTag(): string | undefined;
/**
* Get current namespace
* @returns Current namespace or undefined if not present or path is empty
*/
getCurrentNamespace(): string | undefined;
/**
* Get current node's attribute value
* @param attrName - Attribute name
* @returns Attribute value or undefined
*/
getAttrValue(attrName: string): any;
/**
* Check if current node has an attribute
* @param attrName - Attribute name
*/
hasAttr(attrName: string): boolean;
/**
* Get current node's sibling position (child index in parent)
* @returns Position index or -1 if path is empty
*/
getPosition(): number;
/**
* Get current node's repeat counter (occurrence count of this tag name)
* @returns Counter value or -1 if path is empty
*/
getCounter(): number;
/**
* Get current node's sibling index (alias for getPosition for backward compatibility)
* @returns Index or -1 if path is empty
* @deprecated Use getPosition() or getCounter() instead
*/
getIndex(): number;
/**
* Get current path depth
* @returns Number of nodes in the path
*/
getDepth(): number;
/**
* Get path as string
* @param separator - Optional separator (uses default if not provided)
* @param includeNamespace - Whether to include namespace in output
* @returns Path string (e.g., "root.users.user" or "ns:root.ns:users.user")
*/
toString(separator?: string, includeNamespace?: boolean): string;
/**
* Get path as array of tag names
* @returns Array of tag names
*/
toArray(): string[];
/**
* Reset the path to empty
*/
reset(): void;
/**
* Match current path against an Expression
* @param expression - The expression to match against
* @returns True if current path matches the expression
*
* @example
* ```javascript
* const expr = new Expression("root.users.user[id]");
* const matcher = new Matcher();
*
* matcher.push("root");
* matcher.push("users");
* matcher.push("user", { id: "123" });
*
* matcher.matches(expr); // true
* ```
*/
matches(expression: Expression): boolean;
/**
* Test whether the matcher's current path matches **any** expression in the set.
*
* Uses the pre-built index to evaluate only the relevant bucket(s):
* 1. Exact depth + tag — O(1) lookup
* 2. Depth-matched wildcard tag — O(1) lookup
* 3. Deep-wildcard expressions — always scanned (typically a small list)
*
* @param exprSet - A `ExpressionSet` instance
* @returns `true` if at least one expression matches the current path
*
* @example
* ```typescript
* // Replaces:
* // for (const expr of stopNodeExpressions) {
* // if (matcher.matches(expr)) return true;
* // }
*
* if (matcher.matchesAny(stopNodes)) {
* // current tag is a stop node
* }
* ```
*/
matchesAny(exprSet: ExpressionSet): boolean;
/**
* Create a snapshot of current state
* @returns State snapshot that can be restored later
*/
snapshot(): MatcherSnapshot;
/**
* Restore state from snapshot
* @param snapshot - State snapshot from previous snapshot() call
*/
restore(snapshot: MatcherSnapshot): void;
/**
* Return a read-only view of this matcher.
*/
readOnly(): ReadOnlyMatcher;
}
/**
* ExpressionSet - An indexed collection of Expressions for efficient bulk matching
*
* Pre-indexes expressions at insertion time by depth and terminal tag name so
* that `matchesAny()` performs an O(1) bucket lookup rather than a full O(E)
* linear scan on every tag.
*
* @example
* ```javascript
* const { Expression, ExpressionSet, Matcher } = require('path-expression-matcher');
*
* // Build once at config time
* const stopNodes = new ExpressionSet();
* stopNodes
* .add(new Expression('root.users.user'))
* .add(new Expression('root.config.*'))
* .add(new Expression('..script'))
* .seal();
*
* // Per-tag — hot path
* if (stopNodes.matchesAny(matcher)) { ... }
* ```
*/
declare class ExpressionSet {
constructor();
/** Number of expressions currently in the set. */
readonly size: number;
/** Whether the set has been sealed against further modifications. */
readonly isSealed: boolean;
/**
* Add a single Expression. Duplicate patterns are silently ignored.
* @throws {TypeError} if the set has been sealed
*/
add(expression: Expression): this;
/**
* Add multiple expressions at once.
* @throws {TypeError} if the set has been sealed
*/
addAll(expressions: Expression[]): this;
/** Check whether an expression with the same pattern is already present. */
has(expression: Expression): boolean;
/**
* Seal the set against further modifications.
* Any subsequent call to add() or addAll() will throw a TypeError.
*/
seal(): this;
/**
* Test whether the matcher's current path matches any expression in the set.
* Accepts both a Matcher instance and a ReadOnlyMatcher view.
*
*
* @param matcher - A `Matcher` instance or a `ReadOnlyMatcher` view
* @returns Expression if at least one expression matches the current path
*/
matchesAny(matcher: Matcher | ReadOnlyMatcher): boolean;
/**
* Find the first expression in the set that matches the matcher's current path.
*
* @param matcher - A `Matcher` instance or a `ReadOnlyMatcher` view
* @returns Expression if at least one expression matches the current path
*
* @example
* ```typescript
* const node = stopNodes.findMatch(matcher);
* ```
*/
findMatch(matcher: Matcher | ReadOnlyMatcher): Expression;
}
declare namespace pathExpressionMatcher {
export {
Expression,
Matcher,
ExpressionSet,
ExpressionOptions,
MatcherOptions,
Segment,
PathNode,
MatcherSnapshot,
};
}
export = pathExpressionMatcher;

2
node_modules/path-expression-matcher/lib/pem.min.js generated vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

78
node_modules/path-expression-matcher/package.json generated vendored Normal file
View File

@@ -0,0 +1,78 @@
{
"name": "path-expression-matcher",
"version": "1.5.0",
"description": "Efficient path tracking and pattern matching for XML/JSON parsers",
"main": "./lib/pem.cjs",
"type": "module",
"sideEffects": false,
"module": "./src/index.js",
"types": "./src/index.d.ts",
"exports": {
".": {
"import": {
"types": "./src/index.d.ts",
"default": "./src/index.js"
},
"require": {
"types": "./lib/pem.d.cts",
"default": "./lib/pem.cjs"
}
}
},
"scripts": {
"test": "c8 --reporter=lcov --reporter=text node test/*test.js",
"bundle": "webpack --config webpack.cjs.config.js"
},
"keywords": [
"xml",
"json",
"yaml",
"path",
"matcher",
"pattern",
"xpath",
"selector",
"parser",
"fast-xml-parser",
"fast-xml-builder"
],
"author": "Amit Gupta (https://solothought.com)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/NaturalIntelligence/path-expression-matcher"
},
"bugs": {
"url": "https://github.com/NaturalIntelligence/path-expression-matcher/issues"
},
"homepage": "https://github.com/NaturalIntelligence/path-expression-matcher#readme",
"engines": {
"node": ">=14.0.0"
},
"files": [
"lib",
"src/",
"README.md",
"LICENSE"
],
"devDependencies": {
"@babel/core": "^7.13.10",
"@babel/plugin-transform-runtime": "^7.13.10",
"@babel/preset-env": "^7.13.10",
"@babel/register": "^7.13.8",
"@types/node": "20",
"babel-loader": "^8.2.2",
"c8": "^10.1.3",
"eslint": "^8.3.0",
"prettier": "^3.5.1",
"typescript": "5",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
]
}

232
node_modules/path-expression-matcher/src/Expression.js generated vendored Normal file
View File

@@ -0,0 +1,232 @@
/**
* Expression - Parses and stores a tag pattern expression
*
* Patterns are parsed once and stored in an optimized structure for fast matching.
*
* @example
* const expr = new Expression("root.users.user");
* const expr2 = new Expression("..user[id]:first");
* const expr3 = new Expression("root/users/user", { separator: '/' });
*/
export default class Expression {
/**
* Create a new Expression
* @param {string} pattern - Pattern string (e.g., "root.users.user", "..user[id]")
* @param {Object} options - Configuration options
* @param {string} options.separator - Path separator (default: '.')
*/
constructor(pattern, options = {}, data) {
this.pattern = pattern;
this.separator = options.separator || '.';
this.segments = this._parse(pattern);
this.data = data;
// Cache expensive checks for performance (O(1) instead of O(n))
this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard');
this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined);
this._hasPositionSelector = this.segments.some(seg => seg.position !== undefined);
}
/**
* Parse pattern string into segments
* @private
* @param {string} pattern - Pattern to parse
* @returns {Array} Array of segment objects
*/
_parse(pattern) {
const segments = [];
// Split by separator but handle ".." specially
let i = 0;
let currentPart = '';
while (i < pattern.length) {
if (pattern[i] === this.separator) {
// Check if next char is also separator (deep wildcard)
if (i + 1 < pattern.length && pattern[i + 1] === this.separator) {
// Flush current part if any
if (currentPart.trim()) {
segments.push(this._parseSegment(currentPart.trim()));
currentPart = '';
}
// Add deep wildcard
segments.push({ type: 'deep-wildcard' });
i += 2; // Skip both separators
} else {
// Regular separator
if (currentPart.trim()) {
segments.push(this._parseSegment(currentPart.trim()));
}
currentPart = '';
i++;
}
} else {
currentPart += pattern[i];
i++;
}
}
// Flush remaining part
if (currentPart.trim()) {
segments.push(this._parseSegment(currentPart.trim()));
}
return segments;
}
/**
* Parse a single segment
* @private
* @param {string} part - Segment string (e.g., "user", "ns::user", "user[id]", "ns::user:first")
* @returns {Object} Segment object
*/
_parseSegment(part) {
const segment = { type: 'tag' };
// NEW NAMESPACE SYNTAX (v2.0):
// ============================
// Namespace uses DOUBLE colon (::)
// Position uses SINGLE colon (:)
//
// Examples:
// "user" → tag
// "user:first" → tag + position
// "user[id]" → tag + attribute
// "user[id]:first" → tag + attribute + position
// "ns::user" → namespace + tag
// "ns::user:first" → namespace + tag + position
// "ns::user[id]" → namespace + tag + attribute
// "ns::user[id]:first" → namespace + tag + attribute + position
// "ns::first" → namespace + tag named "first" (NO ambiguity!)
//
// This eliminates all ambiguity:
// :: = namespace separator
// : = position selector
// [] = attributes
// Step 1: Extract brackets [attr] or [attr=value]
let bracketContent = null;
let withoutBrackets = part;
const bracketMatch = part.match(/^([^\[]+)(\[[^\]]*\])(.*)$/);
if (bracketMatch) {
withoutBrackets = bracketMatch[1] + bracketMatch[3];
if (bracketMatch[2]) {
const content = bracketMatch[2].slice(1, -1);
if (content) {
bracketContent = content;
}
}
}
// Step 2: Check for namespace (double colon ::)
let namespace = undefined;
let tagAndPosition = withoutBrackets;
if (withoutBrackets.includes('::')) {
const nsIndex = withoutBrackets.indexOf('::');
namespace = withoutBrackets.substring(0, nsIndex).trim();
tagAndPosition = withoutBrackets.substring(nsIndex + 2).trim(); // Skip ::
if (!namespace) {
throw new Error(`Invalid namespace in pattern: ${part}`);
}
}
// Step 3: Parse tag and position (single colon :)
let tag = undefined;
let positionMatch = null;
if (tagAndPosition.includes(':')) {
const colonIndex = tagAndPosition.lastIndexOf(':'); // Use last colon for position
const tagPart = tagAndPosition.substring(0, colonIndex).trim();
const posPart = tagAndPosition.substring(colonIndex + 1).trim();
// Verify position is a valid keyword
const isPositionKeyword = ['first', 'last', 'odd', 'even'].includes(posPart) ||
/^nth\(\d+\)$/.test(posPart);
if (isPositionKeyword) {
tag = tagPart;
positionMatch = posPart;
} else {
// Not a valid position keyword, treat whole thing as tag
tag = tagAndPosition;
}
} else {
tag = tagAndPosition;
}
if (!tag) {
throw new Error(`Invalid segment pattern: ${part}`);
}
segment.tag = tag;
if (namespace) {
segment.namespace = namespace;
}
// Step 4: Parse attributes
if (bracketContent) {
if (bracketContent.includes('=')) {
const eqIndex = bracketContent.indexOf('=');
segment.attrName = bracketContent.substring(0, eqIndex).trim();
segment.attrValue = bracketContent.substring(eqIndex + 1).trim();
} else {
segment.attrName = bracketContent.trim();
}
}
// Step 5: Parse position selector
if (positionMatch) {
const nthMatch = positionMatch.match(/^nth\((\d+)\)$/);
if (nthMatch) {
segment.position = 'nth';
segment.positionValue = parseInt(nthMatch[1], 10);
} else {
segment.position = positionMatch;
}
}
return segment;
}
/**
* Get the number of segments
* @returns {number}
*/
get length() {
return this.segments.length;
}
/**
* Check if expression contains deep wildcard
* @returns {boolean}
*/
hasDeepWildcard() {
return this._hasDeepWildcard;
}
/**
* Check if expression has attribute conditions
* @returns {boolean}
*/
hasAttributeCondition() {
return this._hasAttributeCondition;
}
/**
* Check if expression has position selectors
* @returns {boolean}
*/
hasPositionSelector() {
return this._hasPositionSelector;
}
/**
* Get string representation
* @returns {string}
*/
toString() {
return this.pattern;
}
}

View File

@@ -0,0 +1,209 @@
/**
* ExpressionSet - An indexed collection of Expressions for efficient bulk matching
*
* Instead of iterating all expressions on every tag, ExpressionSet pre-indexes
* them at insertion time by depth and terminal tag name. At match time, only
* the relevant bucket is evaluated — typically reducing checks from O(E) to O(1)
* lookup plus O(small bucket) matches.
*
* Three buckets are maintained:
* - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first)
* - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only)
* - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed)
*
* @example
* import { Expression, ExpressionSet } from 'fast-xml-tagger';
*
* // Build once at config time
* const stopNodes = new ExpressionSet();
* stopNodes.add(new Expression('root.users.user'));
* stopNodes.add(new Expression('root.config.setting'));
* stopNodes.add(new Expression('..script'));
*
* // Query on every tag — hot path
* if (stopNodes.matchesAny(matcher)) { ... }
*/
export default class ExpressionSet {
constructor() {
/** @type {Map<string, import('./Expression.js').default[]>} depth:tag → expressions */
this._byDepthAndTag = new Map();
/** @type {Map<number, import('./Expression.js').default[]>} depth → wildcard-tag expressions */
this._wildcardByDepth = new Map();
/** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */
this._deepWildcards = [];
/** @type {Set<string>} pattern strings already added — used for deduplication */
this._patterns = new Set();
/** @type {boolean} whether the set is sealed against further additions */
this._sealed = false;
}
/**
* Add an Expression to the set.
* Duplicate patterns (same pattern string) are silently ignored.
*
* @param {import('./Expression.js').default} expression - A pre-constructed Expression instance
* @returns {this} for chaining
* @throws {TypeError} if called after seal()
*
* @example
* set.add(new Expression('root.users.user'));
* set.add(new Expression('..script'));
*/
add(expression) {
if (this._sealed) {
throw new TypeError(
'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.'
);
}
// Deduplicate by pattern string
if (this._patterns.has(expression.pattern)) return this;
this._patterns.add(expression.pattern);
if (expression.hasDeepWildcard()) {
this._deepWildcards.push(expression);
return this;
}
const depth = expression.length;
const lastSeg = expression.segments[expression.segments.length - 1];
const tag = lastSeg?.tag;
if (!tag || tag === '*') {
// Can index by depth but not by tag
if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []);
this._wildcardByDepth.get(depth).push(expression);
} else {
// Tightest bucket: depth + tag
const key = `${depth}:${tag}`;
if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []);
this._byDepthAndTag.get(key).push(expression);
}
return this;
}
/**
* Add multiple expressions at once.
*
* @param {import('./Expression.js').default[]} expressions - Array of Expression instances
* @returns {this} for chaining
*
* @example
* set.addAll([
* new Expression('root.users.user'),
* new Expression('root.config.setting'),
* ]);
*/
addAll(expressions) {
for (const expr of expressions) this.add(expr);
return this;
}
/**
* Check whether a pattern string is already present in the set.
*
* @param {import('./Expression.js').default} expression
* @returns {boolean}
*/
has(expression) {
return this._patterns.has(expression.pattern);
}
/**
* Number of expressions in the set.
* @type {number}
*/
get size() {
return this._patterns.size;
}
/**
* Seal the set against further modifications.
* Useful to prevent accidental mutations after config is built.
* Calling add() or addAll() on a sealed set throws a TypeError.
*
* @returns {this}
*/
seal() {
this._sealed = true;
return this;
}
/**
* Whether the set has been sealed.
* @type {boolean}
*/
get isSealed() {
return this._sealed;
}
/**
* Test whether the matcher's current path matches any expression in the set.
*
* Evaluation order (cheapest → most expensive):
* 1. Exact depth + tag bucket — O(1) lookup, typically 02 expressions
* 2. Depth-only wildcard bucket — O(1) lookup, rare
* 3. Deep-wildcard list — always checked, but usually small
*
* @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
* @returns {boolean} true if any expression matches the current path
*
* @example
* if (stopNodes.matchesAny(matcher)) {
* // handle stop node
* }
*/
matchesAny(matcher) {
return this.findMatch(matcher) !== null;
}
/**
* Find and return the first Expression that matches the matcher's current path.
*
* Uses the same evaluation order as matchesAny (cheapest → most expensive):
* 1. Exact depth + tag bucket
* 2. Depth-only wildcard bucket
* 3. Deep-wildcard list
*
* @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
* @returns {import('./Expression.js').default | null} the first matching Expression, or null
*
* @example
* const expr = stopNodes.findMatch(matcher);
* if (expr) {
* // access expr.config, expr.pattern, etc.
* }
*/
findMatch(matcher) {
const depth = matcher.getDepth();
const tag = matcher.getCurrentTag();
// 1. Tightest bucket — most expressions live here
const exactKey = `${depth}:${tag}`;
const exactBucket = this._byDepthAndTag.get(exactKey);
if (exactBucket) {
for (let i = 0; i < exactBucket.length; i++) {
if (matcher.matches(exactBucket[i])) return exactBucket[i];
}
}
// 2. Depth-matched wildcard-tag expressions
const wildcardBucket = this._wildcardByDepth.get(depth);
if (wildcardBucket) {
for (let i = 0; i < wildcardBucket.length; i++) {
if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i];
}
}
// 3. Deep wildcards — cannot be pre-filtered by depth or tag
for (let i = 0; i < this._deepWildcards.length; i++) {
if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i];
}
return null;
}
}

570
node_modules/path-expression-matcher/src/Matcher.js generated vendored Normal file
View File

@@ -0,0 +1,570 @@
import ExpressionSet from "./ExpressionSet.js";
/**
* MatcherView - A lightweight read-only view over a Matcher's internal state.
*
* Created once by Matcher and reused across all callbacks. Holds a direct
* reference to the parent Matcher so it always reflects current parser state
* with zero copying or freezing overhead.
*
* Users receive this via {@link Matcher#readOnly} or directly from parser
* callbacks. It exposes all query and matching methods but has no mutation
* methods — misuse is caught at the TypeScript level rather than at runtime.
*
* @example
* const matcher = new Matcher();
* const view = matcher.readOnly();
*
* matcher.push("root", {});
* view.getCurrentTag(); // "root"
* view.getDepth(); // 1
*/
export class MatcherView {
/**
* @param {Matcher} matcher - The parent Matcher instance to read from.
*/
constructor(matcher) {
this._matcher = matcher;
}
/**
* Get the path separator used by the parent matcher.
* @returns {string}
*/
get separator() {
return this._matcher.separator;
}
/**
* Get current tag name.
* @returns {string|undefined}
*/
getCurrentTag() {
const path = this._matcher.path;
return path.length > 0 ? path[path.length - 1].tag : undefined;
}
/**
* Get current namespace.
* @returns {string|undefined}
*/
getCurrentNamespace() {
const path = this._matcher.path;
return path.length > 0 ? path[path.length - 1].namespace : undefined;
}
/**
* Get current node's attribute value.
* @param {string} attrName
* @returns {*}
*/
getAttrValue(attrName) {
const path = this._matcher.path;
if (path.length === 0) return undefined;
return path[path.length - 1].values?.[attrName];
}
/**
* Check if current node has an attribute.
* @param {string} attrName
* @returns {boolean}
*/
hasAttr(attrName) {
const path = this._matcher.path;
if (path.length === 0) return false;
const current = path[path.length - 1];
return current.values !== undefined && attrName in current.values;
}
/**
* Get current node's sibling position (child index in parent).
* @returns {number}
*/
getPosition() {
const path = this._matcher.path;
if (path.length === 0) return -1;
return path[path.length - 1].position ?? 0;
}
/**
* Get current node's repeat counter (occurrence count of this tag name).
* @returns {number}
*/
getCounter() {
const path = this._matcher.path;
if (path.length === 0) return -1;
return path[path.length - 1].counter ?? 0;
}
/**
* Get current node's sibling index (alias for getPosition).
* @returns {number}
* @deprecated Use getPosition() or getCounter() instead
*/
getIndex() {
return this.getPosition();
}
/**
* Get current path depth.
* @returns {number}
*/
getDepth() {
return this._matcher.path.length;
}
/**
* Get path as string.
* @param {string} [separator] - Optional separator (uses default if not provided)
* @param {boolean} [includeNamespace=true]
* @returns {string}
*/
toString(separator, includeNamespace = true) {
return this._matcher.toString(separator, includeNamespace);
}
/**
* Get path as array of tag names.
* @returns {string[]}
*/
toArray() {
return this._matcher.path.map(n => n.tag);
}
/**
* Match current path against an Expression.
* @param {Expression} expression
* @returns {boolean}
*/
matches(expression) {
return this._matcher.matches(expression);
}
/**
* Match any expression in the given set against the current path.
* @param {ExpressionSet} exprSet
* @returns {boolean}
*/
matchesAny(exprSet) {
return exprSet.matchesAny(this._matcher);
}
}
/**
* Matcher - Tracks current path in XML/JSON tree and matches against Expressions.
*
* The matcher maintains a stack of nodes representing the current path from root to
* current tag. It only stores attribute values for the current (top) node to minimize
* memory usage. Sibling tracking is used to auto-calculate position and counter.
*
* Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to
* user callbacks — it always reflects current state with no Proxy overhead.
*
* @example
* const matcher = new Matcher();
* matcher.push("root", {});
* matcher.push("users", {});
* matcher.push("user", { id: "123", type: "admin" });
*
* const expr = new Expression("root.users.user");
* matcher.matches(expr); // true
*/
export default class Matcher {
/**
* Create a new Matcher.
* @param {Object} [options={}]
* @param {string} [options.separator='.'] - Default path separator
*/
constructor(options = {}) {
this.separator = options.separator || '.';
this.path = [];
this.siblingStacks = [];
// Each path node: { tag, values, position, counter, namespace? }
// values only present for current (last) node
// Each siblingStacks entry: Map<tagName, count> tracking occurrences at each level
this._pathStringCache = null;
this._view = new MatcherView(this);
}
/**
* Push a new tag onto the path.
* @param {string} tagName
* @param {Object|null} [attrValues=null]
* @param {string|null} [namespace=null]
*/
push(tagName, attrValues = null, namespace = null) {
this._pathStringCache = null;
// Remove values from previous current node (now becoming ancestor)
if (this.path.length > 0) {
this.path[this.path.length - 1].values = undefined;
}
// Get or create sibling tracking for current level
const currentLevel = this.path.length;
if (!this.siblingStacks[currentLevel]) {
this.siblingStacks[currentLevel] = new Map();
}
const siblings = this.siblingStacks[currentLevel];
// Create a unique key for sibling tracking that includes namespace
const siblingKey = namespace ? `${namespace}:${tagName}` : tagName;
// Calculate counter (how many times this tag appeared at this level)
const counter = siblings.get(siblingKey) || 0;
// Calculate position (total children at this level so far)
let position = 0;
for (const count of siblings.values()) {
position += count;
}
// Update sibling count for this tag
siblings.set(siblingKey, counter + 1);
// Create new node
const node = {
tag: tagName,
position: position,
counter: counter
};
if (namespace !== null && namespace !== undefined) {
node.namespace = namespace;
}
if (attrValues !== null && attrValues !== undefined) {
node.values = attrValues;
}
this.path.push(node);
}
/**
* Pop the last tag from the path.
* @returns {Object|undefined} The popped node
*/
pop() {
if (this.path.length === 0) return undefined;
this._pathStringCache = null;
const node = this.path.pop();
if (this.siblingStacks.length > this.path.length + 1) {
this.siblingStacks.length = this.path.length + 1;
}
return node;
}
/**
* Update current node's attribute values.
* Useful when attributes are parsed after push.
* @param {Object} attrValues
*/
updateCurrent(attrValues) {
if (this.path.length > 0) {
const current = this.path[this.path.length - 1];
if (attrValues !== null && attrValues !== undefined) {
current.values = attrValues;
}
}
}
/**
* Get current tag name.
* @returns {string|undefined}
*/
getCurrentTag() {
return this.path.length > 0 ? this.path[this.path.length - 1].tag : undefined;
}
/**
* Get current namespace.
* @returns {string|undefined}
*/
getCurrentNamespace() {
return this.path.length > 0 ? this.path[this.path.length - 1].namespace : undefined;
}
/**
* Get current node's attribute value.
* @param {string} attrName
* @returns {*}
*/
getAttrValue(attrName) {
if (this.path.length === 0) return undefined;
return this.path[this.path.length - 1].values?.[attrName];
}
/**
* Check if current node has an attribute.
* @param {string} attrName
* @returns {boolean}
*/
hasAttr(attrName) {
if (this.path.length === 0) return false;
const current = this.path[this.path.length - 1];
return current.values !== undefined && attrName in current.values;
}
/**
* Get current node's sibling position (child index in parent).
* @returns {number}
*/
getPosition() {
if (this.path.length === 0) return -1;
return this.path[this.path.length - 1].position ?? 0;
}
/**
* Get current node's repeat counter (occurrence count of this tag name).
* @returns {number}
*/
getCounter() {
if (this.path.length === 0) return -1;
return this.path[this.path.length - 1].counter ?? 0;
}
/**
* Get current node's sibling index (alias for getPosition).
* @returns {number}
* @deprecated Use getPosition() or getCounter() instead
*/
getIndex() {
return this.getPosition();
}
/**
* Get current path depth.
* @returns {number}
*/
getDepth() {
return this.path.length;
}
/**
* Get path as string.
* @param {string} [separator] - Optional separator (uses default if not provided)
* @param {boolean} [includeNamespace=true]
* @returns {string}
*/
toString(separator, includeNamespace = true) {
const sep = separator || this.separator;
const isDefault = (sep === this.separator && includeNamespace === true);
if (isDefault) {
if (this._pathStringCache !== null) {
return this._pathStringCache;
}
const result = this.path.map(n =>
(n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
).join(sep);
this._pathStringCache = result;
return result;
}
return this.path.map(n =>
(includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
).join(sep);
}
/**
* Get path as array of tag names.
* @returns {string[]}
*/
toArray() {
return this.path.map(n => n.tag);
}
/**
* Reset the path to empty.
*/
reset() {
this._pathStringCache = null;
this.path = [];
this.siblingStacks = [];
}
/**
* Match current path against an Expression.
* @param {Expression} expression
* @returns {boolean}
*/
matches(expression) {
const segments = expression.segments;
if (segments.length === 0) {
return false;
}
if (expression.hasDeepWildcard()) {
return this._matchWithDeepWildcard(segments);
}
return this._matchSimple(segments);
}
/**
* @private
*/
_matchSimple(segments) {
if (this.path.length !== segments.length) {
return false;
}
for (let i = 0; i < segments.length; i++) {
if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) {
return false;
}
}
return true;
}
/**
* @private
*/
_matchWithDeepWildcard(segments) {
let pathIdx = this.path.length - 1;
let segIdx = segments.length - 1;
while (segIdx >= 0 && pathIdx >= 0) {
const segment = segments[segIdx];
if (segment.type === 'deep-wildcard') {
segIdx--;
if (segIdx < 0) {
return true;
}
const nextSeg = segments[segIdx];
let found = false;
for (let i = pathIdx; i >= 0; i--) {
if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) {
pathIdx = i - 1;
segIdx--;
found = true;
break;
}
}
if (!found) {
return false;
}
} else {
if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) {
return false;
}
pathIdx--;
segIdx--;
}
}
return segIdx < 0;
}
/**
* @private
*/
_matchSegment(segment, node, isCurrentNode) {
if (segment.tag !== '*' && segment.tag !== node.tag) {
return false;
}
if (segment.namespace !== undefined) {
if (segment.namespace !== '*' && segment.namespace !== node.namespace) {
return false;
}
}
if (segment.attrName !== undefined) {
if (!isCurrentNode) {
return false;
}
if (!node.values || !(segment.attrName in node.values)) {
return false;
}
if (segment.attrValue !== undefined) {
if (String(node.values[segment.attrName]) !== String(segment.attrValue)) {
return false;
}
}
}
if (segment.position !== undefined) {
if (!isCurrentNode) {
return false;
}
const counter = node.counter ?? 0;
if (segment.position === 'first' && counter !== 0) {
return false;
} else if (segment.position === 'odd' && counter % 2 !== 1) {
return false;
} else if (segment.position === 'even' && counter % 2 !== 0) {
return false;
} else if (segment.position === 'nth' && counter !== segment.positionValue) {
return false;
}
}
return true;
}
/**
* Match any expression in the given set against the current path.
* @param {ExpressionSet} exprSet
* @returns {boolean}
*/
matchesAny(exprSet) {
return exprSet.matchesAny(this);
}
/**
* Create a snapshot of current state.
* @returns {Object}
*/
snapshot() {
return {
path: this.path.map(node => ({ ...node })),
siblingStacks: this.siblingStacks.map(map => new Map(map))
};
}
/**
* Restore state from snapshot.
* @param {Object} snapshot
*/
restore(snapshot) {
this._pathStringCache = null;
this.path = snapshot.path.map(node => ({ ...node }));
this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map));
}
/**
* Return the read-only {@link MatcherView} for this matcher.
*
* The same instance is returned on every call — no allocation occurs.
* It always reflects the current parser state and is safe to pass to
* user callbacks without risk of accidental mutation.
*
* @returns {MatcherView}
*
* @example
* const view = matcher.readOnly();
* // pass view to callbacks — it stays in sync automatically
* view.matches(expr); // ✓
* view.getCurrentTag(); // ✓
* // view.push(...) // ✗ method does not exist — caught by TypeScript
*/
readOnly() {
return this._view;
}
}

523
node_modules/path-expression-matcher/src/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,523 @@
/**
* TypeScript definitions for path-expression-matcher
*
* Provides efficient path tracking and pattern matching for XML/JSON parsers.
*/
/**
* Options for creating an Expression
*/
export interface ExpressionOptions {
/**
* Path separator character
* @default '.'
*/
separator?: string;
}
/**
* Parsed segment from an expression pattern
*/
export interface Segment {
/**
* Type of segment
*/
type: 'tag' | 'deep-wildcard';
/**
* Tag name (e.g., "user", "*" for wildcard)
* Only present when type is 'tag'
*/
tag?: string;
/**
* Namespace prefix (e.g., "ns" in "ns::user")
* Only present when namespace is specified
*/
namespace?: string;
/**
* Attribute name to match (e.g., "id" in "user[id]")
* Only present when attribute condition exists
*/
attrName?: string;
/**
* Attribute value to match (e.g., "123" in "user[id=123]")
* Only present when attribute value is specified
*/
attrValue?: string;
/**
* Position selector type
* Only present when position selector exists
*/
position?: 'first' | 'last' | 'odd' | 'even' | 'nth';
/**
* Numeric value for nth() selector
* Only present when position is 'nth'
*/
positionValue?: number;
}
/**
* Expression - Parses and stores a tag pattern expression
*
* Patterns are parsed once and stored in an optimized structure for fast matching.
*
* @example
* ```typescript
* const expr = new Expression("root.users.user");
* const expr2 = new Expression("..user[id]:first");
* const expr3 = new Expression("root/users/user", { separator: '/' });
* ```
*
* Pattern Syntax:
* - `root.users.user` - Match exact path
* - `..user` - Match "user" at any depth (deep wildcard)
* - `user[id]` - Match user tag with "id" attribute
* - `user[id=123]` - Match user tag where id="123"
* - `user:first` - Match first occurrence of user tag
* - `ns::user` - Match user tag with namespace "ns"
* - `ns::user[id]:first` - Combine namespace, attribute, and position
*/
export class Expression {
/**
* Original pattern string
*/
readonly pattern: string;
/**
* Path separator character
*/
readonly separator: string;
/**
* Parsed segments
*/
readonly segments: Segment[];
/**
* Create a new Expression
* @param pattern - Pattern string (e.g., "root.users.user", "..user[id]")
* @param options - Configuration options
*/
constructor(pattern: string, options?: ExpressionOptions);
/**
* Get the number of segments
*/
get length(): number;
/**
* Check if expression contains deep wildcard (..)
*/
hasDeepWildcard(): boolean;
/**
* Check if expression has attribute conditions
*/
hasAttributeCondition(): boolean;
/**
* Check if expression has position selectors
*/
hasPositionSelector(): boolean;
/**
* Get string representation
*/
toString(): string;
}
/**
* Options for creating a Matcher
*/
export interface MatcherOptions {
/**
* Default path separator
* @default '.'
*/
separator?: string;
}
/**
* Internal node structure in the path stack
*/
export interface PathNode {
/**
* Tag name
*/
tag: string;
/**
* Namespace (if present)
*/
namespace?: string;
/**
* Position in sibling list (child index in parent)
*/
position: number;
/**
* Counter (occurrence count of this tag name)
*/
counter: number;
/**
* Attribute key-value pairs
* Only present for the current (last) node in path
*/
values?: Record<string, any>;
}
/**
* Snapshot of matcher state
*/
export interface MatcherSnapshot {
/**
* Copy of the path stack
*/
path: PathNode[];
/**
* Copy of sibling tracking maps
*/
siblingStacks: Map<string, number>[];
}
/**
* MatcherView - A lightweight read-only view over a {@link Matcher} instance.
*
* Created once by {@link Matcher} and reused across all callbacks — no allocation
* on every invocation. Holds a direct reference to the parent Matcher's internal
* state so it always reflects the current parser position with zero copying or
* freezing overhead.
*
* Mutation methods (`push`, `pop`, `reset`, `updateCurrent`, `restore`) are simply
* absent from this class, so misuse is caught at compile time by TypeScript rather
* than at runtime.
*
* Obtain via {@link Matcher#readOnly} — the same instance is returned every time.
*
* @example
* ```typescript
* const matcher = new Matcher();
* const view: MatcherView = matcher.readOnly();
*
* matcher.push("root", {});
* matcher.push("users", {});
* matcher.push("user", { id: "123" });
*
* view.matches(expr); // ✓ true
* view.getCurrentTag(); // ✓ "user"
* view.getDepth(); // ✓ 3
* // view.push(...) // ✗ Property 'push' does not exist on type 'MatcherView'
* ```
*/
export class MatcherView {
/**
* Default path separator (read-only, delegates to parent Matcher)
*/
readonly separator: string;
getCurrentTag(): string | undefined;
getCurrentNamespace(): string | undefined;
getAttrValue(attrName: string): any;
hasAttr(attrName: string): boolean;
getPosition(): number;
getCounter(): number;
/** @deprecated Use getPosition() or getCounter() instead */
getIndex(): number;
getDepth(): number;
toString(separator?: string, includeNamespace?: boolean): string;
toArray(): string[];
matches(expression: Expression): boolean;
matchesAny(exprSet: ExpressionSet): boolean;
}
/**
* @deprecated Use {@link MatcherView} instead.
* Alias kept for backward compatibility with code that references `ReadOnlyMatcher`.
*/
export type ReadOnlyMatcher = MatcherView;
/**
* Matcher - Tracks current path in XML/JSON tree and matches against Expressions.
*
* The matcher maintains a stack of nodes representing the current path from root to
* current tag. It only stores attribute values for the current (top) node to minimize
* memory usage.
*
* Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to
* user callbacks — the same instance is reused on every call with no allocation overhead.
*
* @example
* ```typescript
* const matcher = new Matcher();
* matcher.push("root", {});
* matcher.push("users", {});
* matcher.push("user", { id: "123", type: "admin" });
*
* const expr = new Expression("root.users.user");
* matcher.matches(expr); // true
*
* matcher.pop();
* matcher.matches(expr); // false
* ```
*/
export class Matcher {
/**
* Default path separator
*/
readonly separator: string;
/**
* Create a new Matcher
* @param options - Configuration options
*/
constructor(options?: MatcherOptions);
/**
* Push a new tag onto the path.
* @param tagName - Name of the tag
* @param attrValues - Attribute key-value pairs for current node (optional)
* @param namespace - Namespace for the tag (optional)
*
* @example
* ```typescript
* matcher.push("user", { id: "123", type: "admin" });
* matcher.push("user", { id: "456" }, "ns");
* matcher.push("container", null);
* ```
*/
push(tagName: string, attrValues?: Record<string, any> | null, namespace?: string | null): void;
/**
* Pop the last tag from the path.
* @returns The popped node or undefined if path is empty
*/
pop(): PathNode | undefined;
/**
* Update current node's attribute values.
* Useful when attributes are parsed after push.
* @param attrValues - Attribute values
*/
updateCurrent(attrValues: Record<string, any>): void;
/**
* Reset the path to empty.
*/
reset(): void;
/**
* Create a snapshot of current state.
* @returns State snapshot that can be restored later
*/
snapshot(): MatcherSnapshot;
/**
* Restore state from snapshot.
* @param snapshot - State snapshot from previous snapshot() call
*/
restore(snapshot: MatcherSnapshot): void;
getCurrentTag(): string | undefined;
getCurrentNamespace(): string | undefined;
getAttrValue(attrName: string): any;
hasAttr(attrName: string): boolean;
getPosition(): number;
getCounter(): number;
/** @deprecated Use getPosition() or getCounter() instead */
getIndex(): number;
getDepth(): number;
toString(separator?: string, includeNamespace?: boolean): string;
toArray(): string[];
matches(expression: Expression): boolean;
matchesAny(exprSet: ExpressionSet): boolean;
/**
* Return the read-only {@link MatcherView} for this matcher.
*
* The same instance is returned on every call — no allocation occurs.
* Pass this to user callbacks; it always reflects current parser state.
*
* @example
* ```typescript
* const view = matcher.readOnly();
* // same reference every time — safe to cache
* view === matcher.readOnly(); // true
* ```
*/
readOnly(): MatcherView;
}
/**
* ExpressionSet - An indexed collection of Expressions for efficient bulk matching
*
* Pre-indexes expressions at insertion time by depth and terminal tag name so
* that `matchesAny()` performs an O(1) bucket lookup rather than a full O(E)
* linear scan on every tag.
*
* Three internal buckets are maintained automatically:
* - **exact** — expressions with a fixed depth and a concrete terminal tag
* - **depth-wildcard** — fixed depth but terminal tag is `*`
* - **deep-wildcard** — expressions containing `..` (cannot be depth-indexed)
*
* @example
* ```typescript
* import { Expression, ExpressionSet, Matcher } from 'fast-xml-tagger';
*
* // Build once at config time
* const stopNodes = new ExpressionSet();
* stopNodes
* .add(new Expression('root.users.user'))
* .add(new Expression('root.config.*'))
* .add(new Expression('..script'))
* .seal(); // prevent accidental mutation during parsing
*
* // Query on every tag — hot path
* if (stopNodes.matchesAny(matcher)) {
* // handle stop node
* }
* ```
*/
export class ExpressionSet {
/**
* Create an empty ExpressionSet.
*/
constructor();
/**
* Number of expressions currently in the set.
*/
readonly size: number;
/**
* Whether the set has been sealed against further modifications.
*/
readonly isSealed: boolean;
/**
* Add a single Expression to the set.
*
* Duplicate patterns (same `expression.pattern` string) are silently ignored.
*
* @param expression - A pre-constructed Expression instance
* @returns `this` — for chaining
* @throws {TypeError} if the set has been sealed
*
* @example
* ```typescript
* set.add(new Expression('root.users.user'));
* set.add(new Expression('..script'));
* ```
*/
add(expression: Expression): this;
/**
* Add multiple expressions at once.
*
* @param expressions - Array of Expression instances
* @returns `this` — for chaining
* @throws {TypeError} if the set has been sealed
*
* @example
* ```typescript
* set.addAll([
* new Expression('root.users.user'),
* new Expression('root.config.setting'),
* ]);
* ```
*/
addAll(expressions: Expression[]): this;
/**
* Check whether an Expression with the same pattern is already present.
*
* @param expression - Expression to look up
* @returns `true` if the pattern was already added
*/
has(expression: Expression): boolean;
/**
* Seal the set against further modifications.
*
* After calling `seal()`, any call to `add()` or `addAll()` will throw a
* `TypeError`. This is useful to prevent accidental mutation once the config
* has been fully built and parsing has started.
*
* @returns `this` — for chaining
*
* @example
* ```typescript
* const stopNodes = new ExpressionSet();
* stopNodes.addAll(patterns.map(p => new Expression(p))).seal();
*
* // Later — safe: reads are still allowed
* stopNodes.matchesAny(matcher);
*
* // Later — throws TypeError: ExpressionSet is sealed
* stopNodes.add(new Expression('root.extra'));
* ```
*/
seal(): this;
/**
* Test whether the matcher's current path matches **any** expression in the set.
*
* Uses the pre-built index to evaluate only the relevant bucket(s):
* 1. Exact depth + tag — O(1) lookup
* 2. Depth-matched wildcard tag — O(1) lookup
* 3. Deep-wildcard expressions — always scanned (typically a small list)
*
* @param matcher - A `Matcher` instance or a `ReadOnlyMatcher` view
* @returns `true` if at least one expression matches the current path
*
* @example
* ```typescript
* // Replaces:
* // for (const expr of stopNodeExpressions) {
* // if (matcher.matches(expr)) return true;
* // }
*
* if (stopNodes.matchesAny(matcher)) {
* // current tag is a stop node
* }
* ```
*/
matchesAny(matcher: Matcher | MatcherView): boolean;
/**
* Find the first expression in the set that matches the matcher's current path.
*
* Uses the pre-built index to evaluate only the relevant bucket(s):
* 1. Exact depth + tag — O(1) lookup
* 2. Depth-matched wildcard tag — O(1) lookup
* 3. Deep-wildcard expressions — always scanned (typically a small list)
*
* @param matcher - A `Matcher` instance or a `ReadOnlyMatcher` view
* @returns Expression if at least one expression matches the current path
*
* @example
* ```typescript
* const node = stopNodes.findMatch(matcher);
* ```
*/
findMatch(matcher: Matcher | MatcherView): Expression;
}
/**
* Default export containing Expression, Matcher, and ExpressionSet
*/
declare const _default: {
Expression: typeof Expression;
Matcher: typeof Matcher;
MatcherView: typeof MatcherView;
ExpressionSet: typeof ExpressionSet;
};
export default _default;

29
node_modules/path-expression-matcher/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,29 @@
/**
* fast-xml-tagger - XML/JSON path matching library
*
* Provides efficient path tracking and pattern matching for XML/JSON parsers.
*
* @example
* import { Expression, Matcher } from 'fast-xml-tagger';
*
* // Create expression (parse once)
* const expr = new Expression("root.users.user[id]");
*
* // Create matcher (track path)
* const matcher = new Matcher();
* matcher.push("root", [], {}, 0);
* matcher.push("users", [], {}, 0);
* matcher.push("user", ["id", "type"], { id: "123", type: "admin" }, 0);
*
* // Match
* if (matcher.matches(expr)) {
* console.log("Match found!");
* }
*/
import Expression from './Expression.js';
import Matcher from './Matcher.js';
import ExpressionSet from './ExpressionSet.js';
export { Expression, Matcher, ExpressionSet };
export default { Expression, Matcher, ExpressionSet };