1
0
Files
PPTX/openspec/changes/archive/2026-03-04-template-element-composition/design.md
lanyuanxiaoyao 5d60f3c2c2 feat: 实现模板元素混合模式功能
新增混合模式,允许幻灯片同时使用 template 和 elements,实现更灵活的布局组合。

核心变更:
- core/presentation.py: 修改 render_slide() 支持三种模式(纯模板/纯自定义/混合模式)
- 自定义元素可访问模板变量,实现主题色等值的统一控制
- 元素采用简单追加策略合并(模板元素在前,自定义元素在后)
- 完全向后兼容现有用法

测试覆盖:
- 新增 TestRenderSlideHybridMode 测试类,包含 8 个测试用例
- 验证向后兼容性(纯模板模式、纯自定义模式)
- 验证混合模式功能(变量共享、空元素列表、元素顺序等)
- 所有 79 个测试通过

文档更新:
- README.md: 新增"混合模式模板"章节,包含语法示例和使用场景
- README_DEV.md: 更新开发文档,说明元素合并策略和实现细节

规范更新:
- openspec/specs/template-system/spec.md:
  - 修改"系统必须支持自定义幻灯片"需求,支持混合模式
  - 新增 4 个需求:变量共享、元素合并策略、向后兼容、内联模板支持
  - 新增 13 个场景定义

归档:
- openspec/changes/archive/2026-03-04-template-element-composition/: 完整变更记录
2026-03-04 13:12:51 +08:00

7.4 KiB
Raw Blame History

Template Element Composition - 技术设计

Context

当前状态

现有模板系统采用"全有或全无"模式:

  • Presentation.render_slide() 方法使用 if-else 结构
  • template 字段:渲染模板,使用模板元素
  • template 字段:使用自定义元素
# core/presentation.py 当前实现
def render_slide(self, slide_data):
    if "template" in slide_data:
        elements = template.render(vars_values)
    else:
        elements = slide_data.get("elements", [])

约束条件

  1. 向后兼容性:现有用法必须保持不变
  2. 变量共享:自定义元素需要访问模板变量
  3. 渲染顺序模板元素在前自定义元素在后z轴顺序

涉及模块

  • core/template.py - Template 类
  • core/presentation.py - Presentation.render_slide() 方法
  • loaders/yaml_loader.py - YAML 验证(可能需要调整)

Goals / Non-Goals

Goals:

  • 支持幻灯片同时使用 templateelements 字段
  • 自定义元素能够访问模板变量
  • 元素采用简单追加策略合并
  • 完全向后兼容现有用法

Non-Goals:

  • 不支持元素的位置感知合并(如按 key 合并或区域合并)
  • 不支持元素覆盖/替换(使用 slot 机制)
  • 不添加元素重叠检测或警告
  • 不改变模板的条件渲染逻辑

Decisions

决策1修改位置选择 - Presentation.render_slide()

选择:在 Presentation.render_slide() 中实现混合模式逻辑

理由

  • render_slide() 已经负责处理幻灯片渲染的完整流程
  • 模板渲染和元素解析都在这里进行,是天然的合并点
  • 避免在 Template 类中增加对"幻灯片级元素"的认知,保持职责单一

替代方案

  • Template.render() 中添加 extra_elements 参数
    • 缺点:让 Template 类感知"外部元素",违反单一职责原则
    • 缺点:增加 render() 方法的复杂度

决策2变量解析策略 - 复用 Template.resolve_element()

选择:自定义元素复用模板的 resolve_element() 方法进行变量解析

理由

  • resolve_element() 已经实现了深度递归的变量解析逻辑
  • 支持嵌套对象、数组中的变量替换
  • 自动处理类型转换(字符串转数字)

实现方式

# 获取模板后,使用其实例方法解析自定义元素
if "template" in slide_data:
    template = self.get_template(template_name)
    vars_values = slide_data.get("vars", {})
    template_elements = template.render(vars_values)

    # 解析自定义元素(使用模板的变量上下文)
    if "elements" in slide_data:
        custom_elements = slide_data["elements"]
        resolved_custom = [template.resolve_element(e, vars_values) for e in custom_elements]

替代方案

  • 创建独立的变量解析函数
    • 缺点:代码重复,维护两套解析逻辑

决策3元素合并策略 - 简单追加

选择:使用列表追加(template_elements + custom_elements

理由

  • 简单直观,符合用户预期
  • 保持渲染顺序 = z 轴顺序
  • 无需引入复杂的合并规则

行为

final_elements = template_elements + custom_elements

替代方案

  • 按 ID/key 合并(类似 slot 机制)
    • 缺点:需要引入元素 ID 概念,复杂度高
    • 缺点:打破向后兼容性

决策4空 elements 处理

选择elements: [] 等同于不指定 elements

理由

  • 保持 YAML 语法的自然语义
  • 避免引入特殊行为差异

实现

if slide_data.get("elements"):  # 空列表为 False
    # 处理自定义元素

决策5YAML 验证不变

选择:不修改 yaml_loader.py 的验证逻辑

理由

  • 现有验证已允许 templateelements 同时存在(验证的是各自的结构)
  • 混合模式是渲染时的行为,不是 YAML 结构的变化
  • 避免不必要的验证逻辑修改

Risks / Trade-offs

风险1元素位置重叠导致意外的视觉覆盖

场景:用户在不知道模板布局的情况下,放置自定义元素与模板元素重叠

缓解措施

  • 在预览模式中用户可以实时看到效果
  • 文档中明确说明 z 轴顺序规则
  • 未来可选:添加几何验证警告(非必需)

风险2变量作用域混淆

场景:自定义元素引用了模板中未定义的变量

缓解措施

  • 现有的 resolve_value() 已能捕获未定义变量并抛出错误
  • 错误信息清晰指出缺少的变量名

风险3条件渲染元素与自定义元素的交互不明确

场景:模板中某个元素因 visible: false 被过滤,用户期望自定义元素"知道"这个区域

缓解措施

  • 这是设计预期,不是 bug
  • 文档中说明条件渲染在合并前完成
  • 用户需要了解模板的布局结构

实现概要

核心修改Presentation.render_slide()

def render_slide(self, slide_data):
    """
    渲染单个幻灯片(支持混合模式)

    新增:支持同时使用 template 和 elements
    """
    has_template = "template" in slide_data
    has_custom_elements = slide_data.get("elements")

    elements_from_template = []
    vars_values = {}

    # 步骤1渲染模板如果有
    if has_template:
        template_name = slide_data["template"]
        template = self.get_template(template_name)
        vars_values = slide_data.get("vars", {})
        elements_from_template = template.render(vars_values)

    # 步骤2处理自定义元素如果有
    elements_from_custom = []
    if has_custom_elements:
        custom_elements = slide_data["elements"]
        if has_template:
            # 混合模式:使用模板变量解析自定义元素
            template = self.get_template(slide_data["template"])
            elements_from_custom = [
                template.resolve_element(elem, vars_values)
                for elem in custom_elements
            ]
        else:
            # 纯自定义模式(原有行为)
            elements_from_custom = custom_elements

    # 步骤3合并元素
    final_elements = elements_from_template + elements_from_custom

    # 步骤4转换为元素对象原有逻辑
    element_objects = [create_element(elem) for elem in final_elements]

    return {
        "background": slide_data.get("background"),
        "elements": element_objects,
    }

测试策略

  1. 单元测试 (tests/unit/test_presentation.py)

    • 测试纯模板模式(向后兼容)
    • 测试纯自定义模式(向后兼容)
    • 测试混合模式(新功能)
    • 测试变量共享
    • 测试空 elements 列表
  2. 集成测试 (tests/integration/)

    • 端到端测试YAML → PPTX
    • 验证 z 轴顺序
    • 验证条件渲染与元素合并的交互
  3. E2E 测试 (tests/e2e/)

    • 完整的 convert 命令测试
    • 完整的 preview 命令测试

迁移计划

部署步骤

  1. 实现 Presentation.render_slide() 的混合模式逻辑
  2. 添加单元测试
  3. 添加集成测试和 E2E 测试
  4. 更新 README.md新增混合模式使用说明
  5. 更新 README_DEV.md更新开发文档

回滚策略

  • 代码修改集中在单个方法 (render_slide())
  • 如需回滚,恢复方法到原始实现即可
  • 向后兼容,现有用法不受影响

Open Questions

无。所有设计决策已在探索阶段明确。