初始化环境文件
This commit is contained in:
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 = {}) {
|
||||
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
512
node_modules/path-expression-matcher/src/Matcher.js
generated
vendored
Normal 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
518
node_modules/path-expression-matcher/src/index.d.ts
generated
vendored
Normal 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
28
node_modules/path-expression-matcher/src/index.js
generated
vendored
Normal 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 };
|
||||
Reference in New Issue
Block a user