Auto-commit 2026-04-29 16:31
This commit is contained in:
21
node_modules/path-expression-matcher/LICENSE
generated
vendored
Normal file
21
node_modules/path-expression-matcher/LICENSE
generated
vendored
Normal 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
872
node_modules/path-expression-matcher/README.md
generated
vendored
Normal 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
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
634
node_modules/path-expression-matcher/lib/pem.d.cts
generated
vendored
Normal 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
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
1
node_modules/path-expression-matcher/lib/pem.min.js.map
generated
vendored
Normal file
1
node_modules/path-expression-matcher/lib/pem.min.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
78
node_modules/path-expression-matcher/package.json
generated
vendored
Normal file
78
node_modules/path-expression-matcher/package.json
generated
vendored
Normal 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
232
node_modules/path-expression-matcher/src/Expression.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
209
node_modules/path-expression-matcher/src/ExpressionSet.js
generated
vendored
Normal file
209
node_modules/path-expression-matcher/src/ExpressionSet.js
generated
vendored
Normal 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 0–2 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
570
node_modules/path-expression-matcher/src/Matcher.js
generated
vendored
Normal 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
523
node_modules/path-expression-matcher/src/index.d.ts
generated
vendored
Normal 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
29
node_modules/path-expression-matcher/src/index.js
generated
vendored
Normal 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 };
|
||||
Reference in New Issue
Block a user