1
0

feat: 添加内联模板支持

支持在 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 字段时行为不变。
This commit is contained in:
2026-03-03 15:59:55 +08:00
parent 2ba1bd7272
commit 01eacb0b97
15 changed files with 1277 additions and 25 deletions

View File

@@ -0,0 +1,185 @@
## Context
**当前状态**:
- 项目已有完整的外部模板系统core/template.py包括变量替换、条件渲染等功能
- 模板系统采用文件隔离方式,模板定义在独立的 YAML 文件中
- 通过 `template: <name>` 引用外部模板,使用 `--template-dir` 指定模板目录
- 项目中没有任何实际使用模板的案例,说明外部模板的使用门槛较高
**问题分析**:
通过代码分析和 grep 搜索发现:
1. 整个项目中没有包含 `template:` 字段的 YAML 文件
2. 所有模板测试用例都是动态生成的,没有真实的用户使用场景
3. 外部模板需要额外的文件管理工作,增加了使用复杂度
**项目约束**:
- 面向中文开发者,使用中文编写注释和文档
- 使用 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 级别错误,不允许使用该模板
**理由**:
- 显式报错比隐式选择更安全,避免用户意外使用了错误的模板
- 内联和外部模板混合使用的情况较少,同名通常是错误或命名不当
- 强制用户明确模板来源,提升代码清晰度
- 降低调试难度,错误立即暴露而不是隐式选择
**实现**:
```python
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)` vs `Template(name, dir)`
**实现**:
```python
@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 异常)
**错误场景**:
1. 模板定义错误: `templates` 不是字典、缺少 `elements` 字段、变量定义错误
2. 模板引用错误: 引用的内联模板不存在、外部模板不存在
3. 变量传递错误: 缺少必需变量、传递未定义变量
4. 循环引用错误: 内联模板自引用(虽然禁止相互引用,但需要检测自引用)
## Risks / Trade-offs
### 风险 1: 内存占用增加
**风险**: 内联模板数据保存在内存中,可能增加内存占用
**缓解措施**:
- 单个模板数据量通常很小(<1KB内存增加可忽略
- 内联模板不需要缓存(已在内存中),不会重复占用
- 未来可以添加内存监控,如果问题严重再优化
### 风险 2: YAML 文件可读性下降
**风险**: 大量的内联模板定义可能使 YAML 文件过长,降低可读性
**缓解措施**:
- 内联模板适合简单场景,复杂场景建议使用外部模板
- 在文档中提供最佳实践建议
- IDE 的折叠功能可以帮助管理长文件
### 风险 3: 模板命名冲突强制检测
**风险**: 内联和外部模板同名时,用户可能期望使用外部模板,但系统直接报错
**缓解措施**:
- 明确的错误消息,指出模板冲突的具体位置(内联 templates 字段 vs 外部模板目录)
- 在文档中详细说明命名规则,建议使用不同的命名空间或前缀
- 在示例代码中展示正确的命名方式
- 提供 migration 指南,帮助用户解决命名冲突
### 风险 4: 循环引用误判
**风险**: 虽然禁止相互引用,但可能存在复杂的循环引用路径
**缓解措施**:
- 实现简单的循环引用检测DFS 遍历)
- 在设计阶段就明确规则,降低实现复杂度
- 添加充分的测试用例覆盖边界情况
## Open Questions
无。设计阶段已明确所有关键决策。