支持在 YAML 源文件中直接定义模板,无需单独的模板文件。 简化单文档编写流程,降低模板系统使用门槛。 核心功能: - 在 YAML 顶层新增 templates 字段定义内联模板 - 支持变量替换、条件渲染、默认值等完整模板功能 - 内联模板优先于外部模板查找 - 同名冲突检测:禁止内联和外部模板同名 - 相互引用检测:禁止内联模板之间相互引用 - 完整的错误处理和验证机制 代码变更: - core/template.py: 新增 from_data() 类方法 - core/presentation.py: 支持内联模板查找和冲突检测 - loaders/yaml_loader.py: 新增 validate_templates_yaml() 验证 - validators/: 扩展验证器支持内联模板 测试: - 新增 9 个内联模板专项测试 - 修复 1 个变量验证测试 - 所有 333 个测试通过 文档: - README.md: 添加内联模板使用指南和最佳实践 - README_DEV.md: 说明实现细节和设计决策 完全向后兼容,不使用 templates 字段时行为不变。
231 lines
7.3 KiB
Python
231 lines
7.3 KiB
Python
"""
|
||
模板系统模块
|
||
|
||
管理可复用的幻灯片布局和变量解析。
|
||
"""
|
||
|
||
import re
|
||
from pathlib import Path
|
||
from loaders.yaml_loader import YAMLError, load_yaml_file, validate_template_yaml
|
||
|
||
|
||
class Template:
|
||
"""模板类,管理可复用的幻灯片布局"""
|
||
|
||
def __init__(self, template_file, templates_dir=None):
|
||
"""
|
||
初始化模板
|
||
|
||
Args:
|
||
template_file: 模板名称(纯文件名,不含路径)
|
||
templates_dir: 模板文件目录
|
||
"""
|
||
# 检查是否提供了 templates_dir
|
||
if templates_dir is None:
|
||
raise YAMLError(
|
||
f"未指定模板目录,无法加载模板: {template_file}\n"
|
||
f"请使用 --template-dir 参数指定模板目录"
|
||
)
|
||
|
||
# 验证模板名称(不能包含路径分隔符)
|
||
if '/' in template_file or '\\' in template_file:
|
||
raise YAMLError(
|
||
f"模板名称不能包含路径分隔符: {template_file}\n"
|
||
f"模板名称应该是纯文件名,如: 'title-slide'"
|
||
)
|
||
|
||
# 构建模板路径
|
||
template_path = Path(templates_dir) / f"{template_file}.yaml"
|
||
|
||
# 检查文件是否存在
|
||
if not template_path.exists():
|
||
raise YAMLError(
|
||
f"模板文件不存在: {template_file}\n"
|
||
f"查找位置: {templates_dir}\n"
|
||
f"期望文件: {template_path}\n"
|
||
f"提示: 请检查模板名称和模板目录是否正确"
|
||
)
|
||
|
||
# 加载并验证模板文件
|
||
self.data = load_yaml_file(template_path)
|
||
validate_template_yaml(self.data, str(template_path))
|
||
|
||
# 解析变量定义
|
||
self.vars_def = {}
|
||
for var in self.data.get('vars', []):
|
||
self.vars_def[var['name']] = var
|
||
|
||
# 元素列表
|
||
self.elements = self.data.get('elements', [])
|
||
|
||
@classmethod
|
||
def from_data(cls, template_data, template_name):
|
||
"""从字典创建模板(内联模板)
|
||
|
||
Args:
|
||
template_data: 模板数据字典
|
||
template_name: 模板名称
|
||
|
||
Returns:
|
||
Template 对象
|
||
"""
|
||
obj = cls.__new__(cls)
|
||
obj.data = template_data
|
||
|
||
# 解析变量定义
|
||
obj.vars_def = {}
|
||
for var in template_data.get('vars', []):
|
||
obj.vars_def[var['name']] = var
|
||
|
||
# 元素列表
|
||
obj.elements = template_data.get('elements', [])
|
||
|
||
return obj
|
||
|
||
def _external_template_exists(self, template_name):
|
||
"""检查外部模板文件是否存在"""
|
||
if not hasattr(self, 'templates_dir') or not self.templates_dir:
|
||
return False
|
||
template_path = Path(self.templates_dir) / f"{template_name}.yaml"
|
||
return template_path.exists()
|
||
|
||
def resolve_value(self, value, vars_values):
|
||
"""
|
||
解析单个值中的变量引用
|
||
|
||
Args:
|
||
value: 要解析的值
|
||
vars_values: 用户提供的变量值字典
|
||
|
||
Returns:
|
||
解析后的值
|
||
"""
|
||
if not isinstance(value, str):
|
||
return value
|
||
|
||
# 匹配 {xxx} 模式
|
||
pattern = r'\{([^}]+)\}'
|
||
|
||
def replacer(match):
|
||
expr = match.group(1)
|
||
|
||
# 模板变量: {title}
|
||
if expr in vars_values:
|
||
return str(vars_values[expr])
|
||
else:
|
||
raise YAMLError(f"未定义的变量: {expr}")
|
||
|
||
result = re.sub(pattern, replacer, value)
|
||
|
||
# 如果结果是纯数字字符串,转换回数字类型
|
||
try:
|
||
# 尝试转换为整数
|
||
if '.' not in result:
|
||
return int(result)
|
||
# 尝试转换为浮点数
|
||
else:
|
||
return float(result)
|
||
except ValueError:
|
||
# 不是数字,返回字符串
|
||
return result
|
||
|
||
def resolve_element(self, elem, vars_values):
|
||
"""
|
||
递归解析元素中的所有变量
|
||
|
||
Args:
|
||
elem: 元素(dict, list, 或其他类型)
|
||
vars_values: 用户提供的变量值字典
|
||
|
||
Returns:
|
||
解析后的元素
|
||
"""
|
||
if isinstance(elem, dict):
|
||
return {k: self.resolve_element(v, vars_values)
|
||
for k, v in elem.items() if k != 'visible'}
|
||
elif isinstance(elem, list):
|
||
return [self.resolve_element(item, vars_values)
|
||
for item in elem]
|
||
elif isinstance(elem, str):
|
||
return self.resolve_value(elem, vars_values)
|
||
else:
|
||
return elem
|
||
|
||
def evaluate_condition(self, condition, vars_values):
|
||
"""
|
||
评估条件表达式(简单的存在性检查)
|
||
|
||
Args:
|
||
condition: 条件字符串,如 "{subtitle != ''}"
|
||
vars_values: 变量值字典
|
||
|
||
Returns:
|
||
bool: 条件是否满足
|
||
"""
|
||
# 简单实现:检查变量是否非空
|
||
# 匹配 {var_name != ''} 或 {var_name != ""}
|
||
pattern = r'\{(\w+)\s*!=\s*[\'\"]{2}\}'
|
||
match = re.match(pattern, condition)
|
||
|
||
if match:
|
||
var_name = match.group(1)
|
||
value = vars_values.get(var_name, '')
|
||
return value != ''
|
||
|
||
# 默认返回 True
|
||
return True
|
||
|
||
def render(self, vars_values):
|
||
"""
|
||
渲染模板,返回实际的元素列表
|
||
|
||
Args:
|
||
vars_values: 用户提供的变量值字典
|
||
|
||
Returns:
|
||
list: 渲染后的元素列表
|
||
|
||
Raises:
|
||
YAMLError: 缺少必需变量,内联模板相互引用
|
||
"""
|
||
# 检测内联模板相互引用(禁止)
|
||
for elem in self.elements:
|
||
if isinstance(elem, dict) and 'template' in elem:
|
||
raise YAMLError(
|
||
f"内联模板不支持相互引用:元素中包含 'template' 字段\n"
|
||
f"提示: 内联模板只能包含元素定义,不能引用其他模板"
|
||
)
|
||
|
||
# 填充所有变量的默认值(如果用户未提供)
|
||
for var_name, var_def in self.vars_def.items():
|
||
if var_name not in vars_values:
|
||
# 检查是否是必需变量
|
||
if var_def.get('required', False):
|
||
# 必需变量必须有默认值或用户提供
|
||
if 'default' in var_def:
|
||
vars_values[var_name] = self.resolve_value(
|
||
var_def['default'], vars_values
|
||
)
|
||
else:
|
||
raise YAMLError(f"缺少必需变量: {var_name}")
|
||
else:
|
||
# 可选变量使用默认值(如果有)
|
||
if 'default' in var_def:
|
||
vars_values[var_name] = self.resolve_value(
|
||
var_def['default'], vars_values
|
||
)
|
||
|
||
# 渲染所有元素
|
||
rendered_elements = []
|
||
for elem in self.elements:
|
||
# 检查条件渲染
|
||
if 'visible' in elem:
|
||
if not self.evaluate_condition(elem['visible'], vars_values):
|
||
continue
|
||
|
||
# 深度解析元素中的所有变量引用
|
||
rendered_elem = self.resolve_element(elem, vars_values)
|
||
rendered_elements.append(rendered_elem)
|
||
|
||
return rendered_elements
|