import { FIELD_MAP, type FieldDef, type OperatorId } from './fields' import type { Condition, RuleBuilderState } from './types' /** Quotes a value as an expr string literal. */ export function quote(value: string): string { return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` } const REGEX_SPECIALS = /[.+?^${}()|[\]\\]/g /** Converts a `*.example.com` style pattern into an anchored regular expression. */ export function wildcardToRegex(pattern: string): string { return `^${pattern.replace(REGEX_SPECIALS, '\\$&').replace(/\*/g, '.*')}$` } export interface WildcardExplain { kind: 'exact' | 'suffix' | 'prefix' | 'contains' | 'regex' | 'any' /** The meaningful part of the pattern, e.g. ".example.com" for "*.example.com". */ value: string } /** Describes how a wildcard pattern will be matched, for the inline hint. */ export function explainWildcard(pattern: string): WildcardExplain { const p = pattern.trim() if (p === '*') return { kind: 'any', value: '' } if (!p.includes('*')) return { kind: 'exact', value: p } const starts = p.startsWith('*') const ends = p.endsWith('*') const core = p.replace(/^\*/, '').replace(/\*$/, '') if (core.includes('*')) return { kind: 'regex', value: wildcardToRegex(p) } if (starts && ends) return { kind: 'contains', value: core } if (starts) return { kind: 'suffix', value: core } if (ends) return { kind: 'prefix', value: core } return { kind: 'regex', value: wildcardToRegex(p) } } function wildcardTerm(subject: string, pattern: string): string { const info = explainWildcard(pattern) switch (info.kind) { case 'any': return `${subject} != ""` case 'exact': return `${subject} == ${quote(info.value)}` case 'suffix': return `${subject} endsWith ${quote(info.value)}` case 'prefix': return `${subject} startsWith ${quote(info.value)}` case 'contains': return `${subject} contains ${quote(info.value)}` case 'regex': return `${subject} matches ${quote(info.value)}` } } /** Builds the expression for one value of a condition. */ function term(field: FieldDef, op: OperatorId, subject: string, value: string): string { const v = value.trim() switch (op) { case 'is': return field.type === 'port' ? `${subject} == ${v}` : `${subject} == ${quote(v)}` case 'subdomain': return `(${subject} == ${quote(v)} || ${subject} endsWith ${quote('.' + v)})` case 'wildcard': return wildcardTerm(subject, v) case 'contains': case 'startsWith': case 'endsWith': return `${subject} ${op} ${quote(v)}` case 'regex': return `${subject} matches ${quote(v)}` case 'geosite': return `geosite(${subject}, ${quote(v.toLowerCase())})` case 'cidr': return `cidr(${subject}, ${quote(v)})` case 'geoip': return `geoip(${subject}, ${quote(v.toLowerCase())})` case 'detected': return `${v} != nil` case 'range': return '' } } /** Compiles a single condition into an expression, or "" when incomplete. */ export function compileCondition(condition: Condition): string { const field = FIELD_MAP[condition.field] if (!field) return '' const values = condition.values.map((v) => v.trim()).filter(Boolean) let body: string if (condition.op === 'range') { const [from, to] = condition.values if (!from?.trim() || !to?.trim()) return '' body = `(${field.expr} >= ${from.trim()} && ${field.expr} <= ${to.trim()})` } else { if (values.length === 0) return '' const subject = field.iter ? (field.item as string) : (field.expr as string) const terms = values.map((v) => term(field, condition.op, subject, v)) const joined = terms.length > 1 ? `(${terms.join(' || ')})` : terms[0] body = field.iter ? `any(${field.iter}, {${joined}})` : joined } if (!condition.negate) return body return body.startsWith('(') && body.endsWith(')') ? `!${body}` : `!(${body})` } /** Compiles the whole builder state into a rule expression. */ export function compileRule(state: RuleBuilderState): string { const parts = state.conditions.map(compileCondition).filter(Boolean) if (parts.length === 0) return '' if (parts.length === 1) return parts[0] return parts.join(state.mode === 'and' ? ' && ' : ' || ') }