import { BUILTIN_ATOM_MAP } from "./atoms.ts"; import type { RuleExpr, RuleLayer, RuleParseError } from "./types.ts"; // MP 规则串 ↔ 结构化层。语法对齐 MP RuleParser(pyparsing infix_notation): // 规则串 = 层 ('>' 层)*;层内优先级 ! > & > |,可加括号;原子 = 字母开头的字母数字串,或数字+字母(4K、60FPS)。 // 偏离:MP 对未知原子视为不匹配、对多余 token 静默截断;我们在解析时报错并给位置(allowUnknown 时保留未知原子)。 export interface RuleParseOptions { /** 自定义条件 id(大写);不在内置表也不在这里的原子报 unknown_atom */ customAtoms?: Iterable; /** 保留未知原子(求值时视为不匹配,对齐 MP);缺省 false 报错 */ allowUnknown?: boolean; } export interface RuleParseResult { layers: RuleLayer[]; errors: RuleParseError[]; } type Tok = { t: "atom" | "(" | ")" | "!" | "&" | "|" | "bad"; v: string; pos: number }; function lex(s: string, base: number): Tok[] { const out: Tok[] = []; let i = 0; while (i < s.length) { const ch = s[i]!; if (/\s/.test(ch)) { i++; continue; } if ("()!&|".includes(ch)) { out.push({ t: ch as Tok["t"], v: ch, pos: base + i }); i++; continue; } const m = /^[A-Za-z0-9]+/.exec(s.slice(i)); if (m) { out.push({ t: "atom", v: m[0], pos: base + i }); i += m[0].length; continue; } out.push({ t: "bad", v: ch, pos: base + i }); i++; } return out; } function levenshtein(a: string, b: string): number { const dp = Array.from({ length: b.length + 1 }, (_, j) => j); for (let i = 1; i <= a.length; i++) { let prev = dp[0]!; dp[0] = i; for (let j = 1; j <= b.length; j++) { const tmp = dp[j]!; dp[j] = Math.min(dp[j]! + 1, dp[j - 1]! + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1)); prev = tmp; } } return dp[b.length]!; } /** 相近原子名(给 unknown_atom 提示) */ export function suggestAtoms(id: string, custom: Iterable = []): string[] { const all = [...[...BUILTIN_ATOM_MAP.values()].filter((a) => a.supported).map((a) => a.id), ...custom]; return all .map((x) => ({ x, d: levenshtein(id, x) })) .filter((e) => e.d <= Math.max(1, Math.floor(id.length / 2)) || e.x.startsWith(id) || id.startsWith(e.x)) .sort((a, b) => a.d - b.d) .slice(0, 3) .map((e) => e.x); } const ATOM_SHAPE = /^(?:[A-Za-z][A-Za-z0-9]*|[0-9]+[A-Za-z][A-Za-z0-9]*)$/; class LayerParser { i = 0; constructor( readonly toks: Tok[], readonly layer: number, readonly errors: RuleParseError[], readonly custom: Set, readonly opts: RuleParseOptions, ) {} peek() { return this.toks[this.i]; } err(code: RuleParseError["code"], message: string, pos: number, extra: Partial = {}) { this.errors.push({ code, message, position: pos, layer: this.layer, ...extra }); } or(): RuleExpr | null { const first = this.and(); if (!first) return null; const args = [first]; while (this.peek()?.t === "|") { this.i++; const n = this.and(); if (!n) return null; args.push(n); } return args.length === 1 ? first : flatten({ op: "or", args }); } and(): RuleExpr | null { const first = this.unary(); if (!first) return null; const args = [first]; while (this.peek()?.t === "&") { this.i++; const n = this.unary(); if (!n) return null; args.push(n); } return args.length === 1 ? first : flatten({ op: "and", args }); } unary(): RuleExpr | null { const t = this.peek(); if (t?.t === "!") { this.i++; const a = this.unary(); return a ? { op: "not", arg: a } : null; } return this.primary(); } primary(): RuleExpr | null { const t = this.peek(); if (!t) { const last = this.toks[this.toks.length - 1]; this.err("unexpected_token", "表达式不完整,缺少条件", last ? last.pos + last.v.length : 0); return null; } if (t.t === "(") { this.i++; const e = this.or(); if (!e) return null; if (this.peek()?.t !== ")") { this.err("unbalanced_paren", "括号不配对:缺少 )", t.pos); return null; } this.i++; return e; } if (t.t === "atom") { this.i++; if (!ATOM_SHAPE.test(t.v)) { this.err("unexpected_token", `「${t.v}」不是合法条件名(字母开头,或数字加字母如 4K)`, t.pos); return null; } const id = t.v.toUpperCase(); const b = BUILTIN_ATOM_MAP.get(id); if (b && !b.supported) this.err("unsupported_atom", `${id}(${b.name})不支持:音乐资源条件没有迁移`, t.pos, { atom: id }); else if (!b && !this.custom.has(id) && !this.opts.allowUnknown) { const suggestions = suggestAtoms(id, this.custom); this.err( "unknown_atom", `未知条件 ${id}${suggestions.length ? `,是不是 ${suggestions.join(" / ")}` : ""}`, t.pos, { atom: id, suggestions }, ); } return { op: "atom", id }; } if (t.t === ")") this.err("unbalanced_paren", "括号不配对:多余的 )", t.pos); else if (t.t === "bad") this.err("invalid_char", `不认识的字符「${t.v}」`, t.pos); else this.err("unexpected_token", `「${t.v}」前缺少条件`, t.pos); return null; } } function flatten(e: RuleExpr): RuleExpr { if (e.op !== "and" && e.op !== "or") return e; const args: RuleExpr[] = []; for (const a of e.args) { const f = flatten(a); if (f.op === e.op) args.push(...f.args); else args.push(f); } return { op: e.op, args }; } /** 规范化:同类 AND/OR 展平、原子大写(结构等价比较与指纹用) */ export function normalizeExpr(e: RuleExpr): RuleExpr { switch (e.op) { case "atom": return { op: "atom", id: e.id.toUpperCase() }; case "not": return { op: "not", arg: normalizeExpr(e.arg) }; default: return flatten({ op: e.op, args: e.args.map(normalizeExpr) }); } } /** 解析一层表达式(不含 `>`);offset 用于报错位置 */ export function parseExpr( s: string, opts: RuleParseOptions = {}, layer = 0, offset = 0, ): { expr: RuleExpr | null; errors: RuleParseError[] } { const errors: RuleParseError[] = []; const toks = lex(s, offset); if (toks.length === 0) { errors.push({ code: "empty_layer", message: `第 ${layer + 1} 层是空的`, position: offset, layer }); return { expr: null, errors }; } const p = new LayerParser( toks, layer, errors, new Set([...(opts.customAtoms ?? [])].map((x) => x.toUpperCase())), opts, ); const expr = p.or(); if (expr && p.i < toks.length && errors.length === 0) { const t = toks[p.i]!; if (t.t === ")") p.err("unbalanced_paren", "括号不配对:多余的 )", t.pos); else if (t.t === "bad") p.err("invalid_char", `不认识的字符「${t.v}」`, t.pos); else p.err("unexpected_token", `「${t.v}」前缺少 & 或 |`, t.pos); } return { expr: errors.length ? null : expr, errors }; } /** 规则串 → 结构化层(`>` 分层,第 1 层 pri=100) */ export function parseRuleString(s: string, opts: RuleParseOptions = {}): RuleParseResult { const errors: RuleParseError[] = []; const layers: RuleLayer[] = []; if (!s.trim()) return { layers, errors: [{ code: "empty", message: "规则串是空的", position: 0, layer: 0 }] }; let offset = 0; s.split(">").forEach((part, k) => { const r = parseExpr(part, opts, k, offset); errors.push(...r.errors); if (r.expr) layers.push({ expr: r.expr }); offset += part.length + 1; }); return { layers: errors.length ? [] : layers, errors }; } const PREC = { or: 1, and: 2, not: 3, atom: 4 } as const; /** 表达式 → 规则串(原子大写,只加必要的括号) */ export function serializeExpr(e: RuleExpr, parent = 0): string { switch (e.op) { case "atom": return e.id.toUpperCase(); case "not": return `!${serializeExpr(e.arg, PREC.not)}`; case "and": { const s = e.args.map((a) => serializeExpr(a, PREC.and)).join(" & "); return parent > PREC.and ? `(${s})` : s; } case "or": { const s = e.args.map((a) => serializeExpr(a, PREC.or)).join(" | "); return parent > PREC.or ? `(${s})` : s; } } } /** 结构化层 → 规则串 */ export function serializeLayers(layers: readonly RuleLayer[]): string { return layers.map((l) => serializeExpr(l.expr)).join(" > "); } /** 表达式引用到的原子 id(去重) */ export function atomsOf(e: RuleExpr, out = new Set()): Set { if (e.op === "atom") out.add(e.id.toUpperCase()); else if (e.op === "not") atomsOf(e.arg, out); else for (const a of e.args) atomsOf(a, out); return out; } /** MP pyparsing as_list() 形状 → RuleExpr(差分测试用):"A" | ["not", X] | [X, "and", Y, "and", Z] */ export function fromMpParseList(v: unknown): RuleExpr { if (typeof v === "string") return { op: "atom", id: v.toUpperCase() }; const a = v as unknown[]; if (a.length === 1) return fromMpParseList(a[0]); if (a[0] === "not") return { op: "not", arg: fromMpParseList(a.length === 2 ? a[1] : a.slice(1)) }; const op = a[1] as "and" | "or"; return flatten({ op, args: a.filter((_, i) => i % 2 === 0).map(fromMpParseList) }); }