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

7.1 KiB
Raw Blame History

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 结构清晰,与 metadataslides 字段同级
  • 字典结构便于查找和管理
  • 不与现有字段冲突,向后兼容

替代方案考虑:

  • 方案 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) vs Template(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 异常)

错误场景:

  1. 模板定义错误: templates 不是字典、缺少 elements 字段、变量定义错误
  2. 模板引用错误: 引用的内联模板不存在、外部模板不存在
  3. 变量传递错误: 缺少必需变量、传递未定义变量
  4. 循环引用错误: 内联模板自引用(虽然禁止相互引用,但需要检测自引用)

Risks / Trade-offs

风险 1: 内存占用增加

风险: 内联模板数据保存在内存中,可能增加内存占用

缓解措施:

  • 单个模板数据量通常很小(<1KB内存增加可忽略
  • 内联模板不需要缓存(已在内存中),不会重复占用
  • 未来可以添加内存监控,如果问题严重再优化

风险 2: YAML 文件可读性下降

风险: 大量的内联模板定义可能使 YAML 文件过长,降低可读性

缓解措施:

  • 内联模板适合简单场景,复杂场景建议使用外部模板
  • 在文档中提供最佳实践建议
  • IDE 的折叠功能可以帮助管理长文件

风险 3: 模板命名冲突强制检测

风险: 内联和外部模板同名时,用户可能期望使用外部模板,但系统直接报错

缓解措施:

  • 明确的错误消息,指出模板冲突的具体位置(内联 templates 字段 vs 外部模板目录)
  • 在文档中详细说明命名规则,建议使用不同的命名空间或前缀
  • 在示例代码中展示正确的命名方式
  • 提供 migration 指南,帮助用户解决命名冲突

风险 4: 循环引用误判

风险: 虽然禁止相互引用,但可能存在复杂的循环引用路径

缓解措施:

  • 实现简单的循环引用检测DFS 遍历)
  • 在设计阶段就明确规则,降低实现复杂度
  • 添加充分的测试用例覆盖边界情况

Open Questions

无。设计阶段已明确所有关键决策。