(function() { 'use strict'; // Escape a string for use in a RegExp (literal match) function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Build regex pattern at parse time based on anchors function compilePattern(raw, anchorStart, anchorEnd) { var src = (anchorStart ? '^' : '') + raw + (anchorEnd ? '$' : ''); try { return new RegExp(src, 'i'); } catch (e) { // Invalid regex — escape and retry (always succeeds) var safe = (anchorStart ? '^' : '') + escapeRegex(raw) + (anchorEnd ? '$' : ''); return new RegExp(safe, 'i'); } } // Parse a single token string into a node function parseToken(token) { var s = token; var negate = false; var anchorStart = false; var anchorEnd = false; if (s.charAt(0) === '!') { negate = true; s = s.slice(1); } if (s.charAt(0) === '^') { anchorStart = true; s = s.slice(1); } if (s.length > 0 && s.charAt(s.length - 1) === '$') { anchorEnd = true; s = s.slice(0, -1); } if (s === '') return null; // bare * (possibly after stripping !) → wildcard-all or wildcard-none if (s === '*' && !anchorStart && !anchorEnd) { return negate ? null : { type: 'wildcard-all' }; } var re = compilePattern(s, anchorStart, anchorEnd); return { type: negate ? 'no-match' : 'match', re: re }; } // Parse expression string into AST array function parse(expression) { if (!expression || typeof expression !== 'string') return []; var trimmed = expression.trim(); if (trimmed === '') return []; if (trimmed === '*') return [{ type: 'wildcard-all' }]; var ast = []; var i = 0; var len = trimmed.length; while (i < len) { var ch = trimmed.charAt(i); if (ch === '(') { var depth = 1; var j = i + 1; while (j < len && depth > 0) { if (trimmed.charAt(j) === '(') depth++; else if (trimmed.charAt(j) === ')') depth--; j++; } var innerAst = parse(trimmed.slice(i + 1, j - 1)); if (innerAst.length === 1) { ast.push(innerAst[0]); } else if (innerAst.length > 1) { for (var k = 0; k < innerAst.length; k++) ast.push(innerAst[k]); } i = j; } else if (ch === '|') { ast.push({ type: 'pipe' }); i++; } else if (ch === ' ') { i++; } else { var j = i; while (j < len) { var c = trimmed.charAt(j); if (c === ' ' || c === '(' || c === '|' || c === ')') break; j++; } var token = trimmed.slice(i, j); if (token.length > 0) { var node = parseToken(token); if (node !== null) ast.push(node); } i = j; } } // Group pipes into OR nodes var hasPipe = false; var branches = [[]]; for (var l = 0; l < ast.length; l++) { if (ast[l].type === 'pipe') { hasPipe = true; branches.push([]); } else { branches[branches.length - 1].push(ast[l]); } } branches = branches.filter(function(b) { return b.length > 0; }); if (!hasPipe) { return ast.filter(function(n) { return n.type !== 'pipe'; }); } var orNodes = branches.map(function(branch) { if (branch.length === 1) return branch[0]; return { type: 'and', nodes: branch }; }); return [{ type: 'or', nodes: orNodes }]; } // Check if a single node matches the value function nodeMatches(node, value) { switch (node.type) { case 'wildcard-all': return true; case 'match': return node.re.test(value); case 'no-match': return !node.re.test(value); case 'or': for (var i = 0; i < node.nodes.length; i++) { if (nodeMatches(node.nodes[i], value)) return true; } return false; case 'and': for (var i = 0; i < node.nodes.length; i++) { if (!nodeMatches(node.nodes[i], value)) return false; } return true; default: return false; } } // Evaluate AST against value function matches(value, ast) { if (!ast || ast.length === 0) return true; var v = String(value); // no forced lowercase — regex has 'i' flag for (var i = 0; i < ast.length; i++) { if (!nodeMatches(ast[i], v)) return false; } return true; } if (!window.zddc) { throw new Error('shared/zddc-filter.js: window.zddc must be loaded first'); } window.zddc.filter = { parse: parse, matches: matches }; })();