支持在 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 字段时行为不变。
186 lines
7.1 KiB
Markdown
186 lines
7.1 KiB
Markdown
## 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
|
||
|
||
无。设计阶段已明确所有关键决策。
|