"""符文引擎核心:解析 ``{{...}}`` 占位符并替换。 纯逻辑,无 IO、无代码执行——与酒馆可执行脚本的 macro 引擎划清边界: 魂镜的符文只做文本替换,宏的解析器是受信任的 Python 函数,绝不 eval 用户输入。 规则: - 未知宏保留原 ``{{...}}`` 文本(不静默清空,避免误删用户文本)。 - 解析器抛异常时保留原 ``{{...}}`` 文本,不阻断文本流。 - 递归展开受限深度(``max_depth``),防止 ``{{a}}→{{b}}→{{a}}`` 振荡死循环。 """ from __future__ import annotations import re from collections.abc import Callable from app.modules.macros.types import MacroContext MacroResolver = Callable[[list[str], MacroContext], str] _MACRO_PATTERN = re.compile(r"\{\{(.*?)\}\}") _DEFAULT_MAX_DEPTH = 5 class MacroRegistry: """符文注册表:名称(大小写不敏感)→ 解析器。""" def __init__(self) -> None: self._macros: dict[str, MacroResolver] = {} def register(self, name: str, resolver: MacroResolver) -> None: self._macros[name.casefold()] = resolver def get(self, name: str) -> MacroResolver | None: return self._macros.get(name.casefold()) def names(self) -> tuple[str, ...]: return tuple(self._macros) def render( text: str, ctx: MacroContext, *, registry: MacroRegistry | None = None, max_depth: int = _DEFAULT_MAX_DEPTH, ) -> str: """把 ``text`` 中的 ``{{...}}`` 占位符按 ``registry`` 替换。 递归展开(宏的返回值可含新的 ``{{...}}``),直到无占位符或达 ``max_depth``。 未提供 ``registry`` 时使用内置默认符文集。 """ reg = registry if registry is not None else _default_registry_lazy() current = text for _ in range(max_depth): if "{{" not in current: break nxt = _render_once(current, reg, ctx) if nxt == current: break current = nxt return current def _render_once(text: str, reg: MacroRegistry, ctx: MacroContext) -> str: def _replace(match: re.Match[str]) -> str: body = match.group(1).strip() if not body: return match.group(0) name, args = _split_macro(body) resolver = reg.get(name) if resolver is None: return match.group(0) try: return resolver(args, ctx) except Exception: return match.group(0) return _MACRO_PATTERN.sub(_replace, text) def _split_macro(body: str) -> tuple[str, list[str]]: """``name:arg1,arg2`` → ``("name", ["arg1", "arg2"])``;无参数时 args 为空。""" if ":" in body: name, rest = body.split(":", 1) args = [a.strip() for a in rest.split(",") if a.strip()] if rest.strip() else [] else: name, args = body, [] return name.strip(), args def _default_registry_lazy() -> MacroRegistry: """延迟构造内置符文集,避免与 builtins 循环导入。""" from app.modules.macros.builtins import build_default_registry return build_default_registry()