edumanagerpro2/node_modules/path-expression-matcher/src/ExpressionSet.js

210 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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