97 lines
3.0 KiB
Python
97 lines
3.0 KiB
Python
"""符文引擎核心:解析 ``{{...}}`` 占位符并替换。
|
|
|
|
纯逻辑,无 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()
|