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