1
0
Files
PPTX/openspec/changes/archive/2026-03-03-inline-templates/design.md
lanyuanxiaoyao 01eacb0b97 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 字段时行为不变。
2026-03-03 15:59:55 +08:00

186 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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
无。设计阶段已明确所有关键决策。