支持在 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 字段时行为不变。
7.1 KiB
7.1 KiB
Context
当前状态:
- 项目已有完整的外部模板系统(core/template.py),包括变量替换、条件渲染等功能
- 模板系统采用文件隔离方式,模板定义在独立的 YAML 文件中
- 通过
template: <name>引用外部模板,使用--template-dir指定模板目录 - 项目中没有任何实际使用模板的案例,说明外部模板的使用门槛较高
问题分析: 通过代码分析和 grep 搜索发现:
- 整个项目中没有包含
template:字段的 YAML 文件 - 所有模板测试用例都是动态生成的,没有真实的用户使用场景
- 外部模板需要额外的文件管理工作,增加了使用复杂度
项目约束:
- 面向中文开发者,使用中文编写注释和文档
- 使用 uv 运行 Python 脚本,禁止直接使用主机环境 Python
- 测试文件必须放在 temp/ 目录
- 每次功能迭代需要更新 README.md 和 README_DEV.md
- 所有需求必须设计全面、合理的测试内容
Goals / Non-Goals
Goals:
- 支持在 YAML 源文件中定义和使用内联模板
- 保持与外部模板系统的兼容性,允许混合使用
- 禁止内联和外部模板同名,要求模板名称必须唯一
- 提供完整的错误处理和验证机制
- 保持代码向后兼容,不破坏现有功能
Non-Goals:
- 不支持内联模板之间的相互引用
- 不支持内联模板引用外部模板
- 不实现模板继承或组合功能
- 不修改外部模板的现有功能
Decisions
决策 1: 使用 templates 字段定义内联模板
选择: 在 YAML 文件顶层新增 templates 字段,字典类型,键为模板名称,值为模板定义
理由:
- 保持 YAML 结构清晰,与
metadata、slides字段同级 - 字典结构便于查找和管理
- 不与现有字段冲突,向后兼容
替代方案考虑:
- 方案 A: 在
metadata中定义 templates - 会造成 metadata 职责混乱 - 方案 B: 使用 YAML 锚点语法 - 无法支持变量替换和条件渲染
- 方案 C: 在每个 slide 中定义内联模板 - 无法实现模板复用
决策 2: 禁止内联和外部模板同名
选择: 当同名模板同时存在于内联和外部时,直接抛出 ERROR 级别错误,不允许使用该模板
理由:
- 显式报错比隐式选择更安全,避免用户意外使用了错误的模板
- 内联和外部模板混合使用的情况较少,同名通常是错误或命名不当
- 强制用户明确模板来源,提升代码清晰度
- 降低调试难度,错误立即暴露而不是隐式选择
实现:
def get_template(self, template_name):
# 1. 先检查内联模板
if hasattr(self, 'inline_templates') and template_name in self.inline_templates:
# 2. 检查外部模板是否也存在同名
if self._external_template_exists(template_name):
raise YAMLError(
f"模板名称冲突: '{template_name}' 同时存在于内联模板和外部模板目录\n"
f"请使用不同的模板名称以避免冲突"
)
inline_data = self.inline_templates[template_name]
return Template.from_data(inline_data, template_name)
# 3. 回退到外部模板
if template_name not in self.template_cache:
self.template_cache[template_name] = Template(
template_name, self.templates_dir
)
return self.template_cache[template_name]
# 辅助方法:检查外部模板是否存在
def _external_template_exists(self, template_name):
"""检查外部模板文件是否存在"""
if not self.templates_dir:
return False
template_path = Path(self.templates_dir) / f"{template_name}.yaml"
return template_path.exists()
决策 3: 新增 Template.from_data() 类方法
选择: 添加类方法从字典创建模板实例,而不是修改现有的 __init__
理由:
- 保持现有
Template.__init__的不变性,专注于外部模板加载 - 区分内联和外部模板的创建路径,降低耦合度
- 类方法语义清晰:
Template.from_data(data, name)vsTemplate(name, dir)
实现:
@classmethod
def from_data(cls, template_data, template_name):
"""从字典创建模板(内联模板)"""
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
决策 4: 禁止内联模板相互引用
选择: 在设计阶段明确禁止内联模板相互引用,不在实现中处理此场景
理由:
- 降低实现复杂度,避免循环引用检测逻辑
- 内联模板主要用于单文档简单场景,不需要复杂引用
- 外部模板系统已经支持跨文档复用,复杂场景应使用外部模板
错误处理:
- 如果检测到内联模板引用其他内联模板,抛出 ERROR 级别错误
- 明确在文档中说明此限制
决策 5: 完整的错误处理机制
选择: 实现四层错误处理:模板定义错误、模板引用错误、变量传递错误、循环引用错误
理由:
- 确保用户能够快速定位和修复问题
- 提前发现潜在的错误,避免在渲染时才暴露
- 符合项目现有的错误处理模式(YAMLError 异常)
错误场景:
- 模板定义错误:
templates不是字典、缺少elements字段、变量定义错误 - 模板引用错误: 引用的内联模板不存在、外部模板不存在
- 变量传递错误: 缺少必需变量、传递未定义变量
- 循环引用错误: 内联模板自引用(虽然禁止相互引用,但需要检测自引用)
Risks / Trade-offs
风险 1: 内存占用增加
风险: 内联模板数据保存在内存中,可能增加内存占用
缓解措施:
- 单个模板数据量通常很小(<1KB),内存增加可忽略
- 内联模板不需要缓存(已在内存中),不会重复占用
- 未来可以添加内存监控,如果问题严重再优化
风险 2: YAML 文件可读性下降
风险: 大量的内联模板定义可能使 YAML 文件过长,降低可读性
缓解措施:
- 内联模板适合简单场景,复杂场景建议使用外部模板
- 在文档中提供最佳实践建议
- IDE 的折叠功能可以帮助管理长文件
风险 3: 模板命名冲突强制检测
风险: 内联和外部模板同名时,用户可能期望使用外部模板,但系统直接报错
缓解措施:
- 明确的错误消息,指出模板冲突的具体位置(内联 templates 字段 vs 外部模板目录)
- 在文档中详细说明命名规则,建议使用不同的命名空间或前缀
- 在示例代码中展示正确的命名方式
- 提供 migration 指南,帮助用户解决命名冲突
风险 4: 循环引用误判
风险: 虽然禁止相互引用,但可能存在复杂的循环引用路径
缓解措施:
- 实现简单的循环引用检测(DFS 遍历)
- 在设计阶段就明确规则,降低实现复杂度
- 添加充分的测试用例覆盖边界情况
Open Questions
无。设计阶段已明确所有关键决策。