初始化环境文件

This commit is contained in:
CN-JS-HuiBai
2026-04-04 12:49:09 +08:00
parent 07742d2688
commit c607af6fac
5971 changed files with 515160 additions and 18 deletions

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

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

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

@@ -0,0 +1,720 @@
# path-expression-matcher
Efficient path tracking and pattern matching for XML, JSON, YAML or any other parsers.
## 🎯 Purpose
`path-expression-matcher` provides two 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
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)
```
**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: '/' });
```
#### 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
}
```
##### `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 **live, read-only proxy** of the matcher. All query and inspection methods work normally, but any attempt to call a state-mutating method (`push`, `pop`, `reset`, `updateCurrent`, `restore`) or to write/delete a property throws a `TypeError`.
This is the recommended way to share the matcher with external consumers — plugins, callbacks, event handlers — that only need to inspect the current path without being able to corrupt parser state.
```javascript
const ro = matcher.readOnly();
```
**What works on the read-only view:**
```javascript
ro.matches(expr) // ✓ pattern matching
ro.getCurrentTag() // ✓ current tag name
ro.getCurrentNamespace() // ✓ current namespace
ro.getAttrValue("id") // ✓ attribute value
ro.hasAttr("id") // ✓ attribute presence check
ro.getPosition() // ✓ sibling position
ro.getCounter() // ✓ occurrence counter
ro.getDepth() // ✓ path depth
ro.toString() // ✓ path as string
ro.toArray() // ✓ path as array
ro.snapshot() // ✓ snapshot (can be used to restore the real matcher)
```
**What throws a `TypeError`:**
```javascript
ro.push("child", {}) // ✗ TypeError: Cannot call 'push' on a read-only Matcher
ro.pop() // ✗ TypeError: Cannot call 'pop' on a read-only Matcher
ro.reset() // ✗ TypeError: Cannot call 'reset' on a read-only Matcher
ro.updateCurrent({}) // ✗ TypeError: Cannot call 'updateCurrent' on a read-only Matcher
ro.restore(snapshot) // ✗ TypeError: Cannot call 'restore' on a read-only Matcher
ro.separator = '/' // ✗ TypeError: Cannot set property on a read-only Matcher
```
**Important:** The read-only view is **live** — it always reflects the current state of the underlying matcher. If you need a frozen-in-time copy instead, use `snapshot()`.
```javascript
const matcher = new Matcher();
const ro = matcher.readOnly();
matcher.push("root");
ro.getDepth(); // 1 — immediately reflects the push
matcher.push("users");
ro.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 Matcher to External Consumers
When passing the matcher into callbacks, plugins, or other code you don't control, use `readOnly()` to prevent accidental state corruption.
```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 a read-only view — consumer can inspect but not mutate
onTag(matcher.readOnly());
matcher.pop();
}
// Safe consumer — can only read
function myPlugin(ro) {
if (ro.matches(adminExpr)) {
console.log("Admin at path:", ro.toString());
console.log("Depth:", ro.getDepth());
console.log("ID:", ro.getAttrValue("id"));
}
}
// ro.push(...) or ro.reset() here would throw TypeError,
// so the parser's state is always safe.
parseTag("user", { id: "1", type: "admin" }, myPlugin);
```
**Combining with `snapshot()`:** A snapshot taken via the read-only view can still be used to restore the real matcher.
```javascript
const matcher = new Matcher();
matcher.push("root");
matcher.push("users");
const ro = matcher.readOnly();
const snap = ro.snapshot(); // ✓ snapshot works on read-only view
matcher.push("user"); // continue parsing...
matcher.restore(snap); // restore to "root.users" using the snapshot
```
```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
```javascript
// For multiple patterns, check all at once
const patterns = [
new Expression("..user"),
new Expression("..post"),
new Expression("..comment"),
];
function matchesAny(matcher, patterns) {
return patterns.some(expr => matcher.matches(expr));
}
```
## 🔗 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.

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

File diff suppressed because one or more lines are too long

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

@@ -0,0 +1,523 @@
/**
* 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;
/**
* 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;
/**
* 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;
}
declare namespace pathExpressionMatcher {
export {
Expression,
Matcher,
ExpressionOptions,
MatcherOptions,
Segment,
PathNode,
MatcherSnapshot,
};
}
export = pathExpressionMatcher;

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

@@ -0,0 +1,232 @@
/**
* Expression - Parses and stores a tag pattern expression
*
* Patterns are parsed once and stored in an optimized structure for fast matching.
*
* @example
* const expr = new Expression("root.users.user");
* const expr2 = new Expression("..user[id]:first");
* const expr3 = new Expression("root/users/user", { separator: '/' });
*/
export default class Expression {
/**
* Create a new Expression
* @param {string} pattern - Pattern string (e.g., "root.users.user", "..user[id]")
* @param {Object} options - Configuration options
* @param {string} options.separator - Path separator (default: '.')
*/
constructor(pattern, options = {}) {
this.pattern = pattern;
this.separator = options.separator || '.';
this.segments = this._parse(pattern);
// 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;
}
}

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

@@ -0,0 +1,512 @@
/**
* 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.
*
* @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
*/
/**
* Names of methods that mutate Matcher state.
* Any attempt to call these on a read-only view throws a TypeError.
* @type {Set<string>}
*/
const MUTATING_METHODS = new Set(['push', 'pop', 'reset', 'updateCurrent', 'restore']);
export default class Matcher {
/**
* Create a new Matcher
* @param {Object} options - Configuration options
* @param {string} options.separator - Default path separator (default: '.')
*/
constructor(options = {}) {
this.separator = options.separator || '.';
this.path = [];
this.siblingStacks = [];
// Each path node: { tag: string, values: object, position: number, counter: number }
// values only present for current (last) node
// Each siblingStacks entry: Map<tagName, count> tracking occurrences at each level
}
/**
* Push a new tag onto the path
* @param {string} tagName - Name of the tag
* @param {Object} attrValues - Attribute key-value pairs for current node (optional)
* @param {string} namespace - Namespace for the tag (optional)
*/
push(tagName, attrValues = null, namespace = null) {
this._pathStringCache = null; // invalidate
// Remove values from previous current node (now becoming ancestor)
if (this.path.length > 0) {
const prev = this.path[this.path.length - 1];
prev.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
};
// Store namespace if provided
if (namespace !== null && namespace !== undefined) {
node.namespace = namespace;
}
// Store values only for current node
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; // invalidate
const node = this.path.pop();
// Clean up sibling tracking for levels deeper than current
// After pop, path.length is the new depth
// We need to clean up siblingStacks[path.length + 1] and beyond
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 - Attribute values
*/
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 - Attribute name
* @returns {*} Attribute value or undefined
*/
getAttrValue(attrName) {
if (this.path.length === 0) return undefined;
const current = this.path[this.path.length - 1];
return current.values?.[attrName];
}
/**
* Check if current node has an attribute
* @param {string} attrName - Attribute name
* @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 for backward compatibility)
* @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 - Whether to include namespace in output (default: 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 && this._pathStringCache !== undefined) {
return this._pathStringCache;
}
const result = this.path.map(n =>
(includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
).join(sep);
this._pathStringCache = result;
return result;
}
// Non-default separator or includeNamespace=false: don't cache (rare case)
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; // invalidate
this.path = [];
this.siblingStacks = [];
}
/**
* Match current path against an Expression
* @param {Expression} expression - The expression to match against
* @returns {boolean} True if current path matches the expression
*/
matches(expression) {
const segments = expression.segments;
if (segments.length === 0) {
return false;
}
// Handle deep wildcard patterns
if (expression.hasDeepWildcard()) {
return this._matchWithDeepWildcard(segments);
}
// Simple path matching (no deep wildcards)
return this._matchSimple(segments);
}
/**
* Match simple path (no deep wildcards)
* @private
*/
_matchSimple(segments) {
// Path must be same length as segments
if (this.path.length !== segments.length) {
return false;
}
// Match each segment bottom-to-top
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const node = this.path[i];
const isCurrentNode = (i === this.path.length - 1);
if (!this._matchSegment(segment, node, isCurrentNode)) {
return false;
}
}
return true;
}
/**
* Match path with deep wildcards
* @private
*/
_matchWithDeepWildcard(segments) {
let pathIdx = this.path.length - 1; // Start from current node (bottom)
let segIdx = segments.length - 1; // Start from last segment
while (segIdx >= 0 && pathIdx >= 0) {
const segment = segments[segIdx];
if (segment.type === 'deep-wildcard') {
// ".." matches zero or more levels
segIdx--;
if (segIdx < 0) {
// Pattern ends with "..", always matches
return true;
}
// Find where next segment matches in the path
const nextSeg = segments[segIdx];
let found = false;
for (let i = pathIdx; i >= 0; i--) {
const isCurrentNode = (i === this.path.length - 1);
if (this._matchSegment(nextSeg, this.path[i], isCurrentNode)) {
pathIdx = i - 1;
segIdx--;
found = true;
break;
}
}
if (!found) {
return false;
}
} else {
// Regular segment
const isCurrentNode = (pathIdx === this.path.length - 1);
if (!this._matchSegment(segment, this.path[pathIdx], isCurrentNode)) {
return false;
}
pathIdx--;
segIdx--;
}
}
// All segments must be consumed
return segIdx < 0;
}
/**
* Match a single segment against a node
* @private
* @param {Object} segment - Segment from Expression
* @param {Object} node - Node from path
* @param {boolean} isCurrentNode - Whether this is the current (last) node
* @returns {boolean}
*/
_matchSegment(segment, node, isCurrentNode) {
// Match tag name (* is wildcard)
if (segment.tag !== '*' && segment.tag !== node.tag) {
return false;
}
// Match namespace if specified in segment
if (segment.namespace !== undefined) {
// Segment has namespace - node must match it
if (segment.namespace !== '*' && segment.namespace !== node.namespace) {
return false;
}
}
// If segment has no namespace, it matches nodes with or without namespace
// Match attribute name (check if node has this attribute)
// Can only check for current node since ancestors don't have values
if (segment.attrName !== undefined) {
if (!isCurrentNode) {
// Can't check attributes for ancestor nodes (values not stored)
return false;
}
if (!node.values || !(segment.attrName in node.values)) {
return false;
}
// Match attribute value (only possible for current node)
if (segment.attrValue !== undefined) {
const actualValue = node.values[segment.attrName];
// Both should be strings
if (String(actualValue) !== String(segment.attrValue)) {
return false;
}
}
}
// Match position (only for current node)
if (segment.position !== undefined) {
if (!isCurrentNode) {
// Can't check position for ancestor nodes
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') {
if (counter !== segment.positionValue) {
return false;
}
}
}
return true;
}
/**
* Create a snapshot of current state
* @returns {Object} State snapshot
*/
snapshot() {
return {
path: this.path.map(node => ({ ...node })),
siblingStacks: this.siblingStacks.map(map => new Map(map))
};
}
/**
* Restore state from snapshot
* @param {Object} snapshot - State snapshot
*/
restore(snapshot) {
this._pathStringCache = null; // invalidate
this.path = snapshot.path.map(node => ({ ...node }));
this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map));
}
/**
* Return a read-only view of this matcher.
*
* The returned object exposes all query/inspection methods but throws a
* TypeError if any state-mutating method is called (`push`, `pop`, `reset`,
* `updateCurrent`, `restore`). Property reads (e.g. `.path`, `.separator`)
* are allowed but the returned arrays/objects are frozen so callers cannot
* mutate internal state through them either.
*
* @returns {ReadOnlyMatcher} A proxy that forwards read operations and blocks writes.
*
* @example
* const matcher = new Matcher();
* matcher.push("root", {});
*
* const ro = matcher.readOnly();
* ro.matches(expr); // ✓ works
* ro.getCurrentTag(); // ✓ works
* ro.push("child", {}); // ✗ throws TypeError
* ro.reset(); // ✗ throws TypeError
*/
readOnly() {
const self = this;
return new Proxy(self, {
get(target, prop, receiver) {
// Block mutating methods
if (MUTATING_METHODS.has(prop)) {
return () => {
throw new TypeError(
`Cannot call '${prop}' on a read-only Matcher. ` +
`Obtain a writable instance to mutate state.`
);
};
}
const value = Reflect.get(target, prop, receiver);
// Freeze array/object properties so callers can't mutate internal
// state through direct property access (e.g. matcher.path.push(...))
if (prop === 'path' || prop === 'siblingStacks') {
return Object.freeze(
Array.isArray(value)
? value.map(item =>
item instanceof Map
? Object.freeze(new Map(item)) // freeze a copy of each Map
: Object.freeze({ ...item }) // freeze a copy of each node
)
: value
);
}
// Bind methods so `this` inside them still refers to the real Matcher
if (typeof value === 'function') {
return value.bind(target);
}
return value;
},
// Prevent any property assignment on the read-only view
set(_target, prop) {
throw new TypeError(
`Cannot set property '${String(prop)}' on a read-only Matcher.`
);
},
// Prevent property deletion
deleteProperty(_target, prop) {
throw new TypeError(
`Cannot delete property '${String(prop)}' from a read-only Matcher.`
);
}
});
}
}

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

@@ -0,0 +1,518 @@
/**
* 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>[];
}
/**
* 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
* ```typescript
* 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
* ```
*/
export 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;
/**
* 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
* ```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;
/**
* 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
* ```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;
/**
* 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
* ```typescript
* 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;
/**
* 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;
}
/**
* Default export containing both Expression and Matcher
*/
declare const _default: {
Expression: typeof Expression;
Matcher: typeof Matcher;
};
export default _default;

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

@@ -0,0 +1,28 @@
/**
* 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';
export { Expression, Matcher };
export default { Expression, Matcher };