# Template Element Composition - 技术设计 ## Context ### 当前状态 现有模板系统采用"全有或全无"模式: - `Presentation.render_slide()` 方法使用 if-else 结构 - 有 `template` 字段:渲染模板,使用模板元素 - 无 `template` 字段:使用自定义元素 ```python # 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:** - 支持幻灯片同时使用 `template` 和 `elements` 字段 - 自定义元素能够访问模板变量 - 元素采用简单追加策略合并 - 完全向后兼容现有用法 **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()` 已经实现了深度递归的变量解析逻辑 - 支持嵌套对象、数组中的变量替换 - 自动处理类型转换(字符串转数字) **实现方式**: ```python # 获取模板后,使用其实例方法解析自定义元素 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 轴顺序 - 无需引入复杂的合并规则 **行为**: ```python final_elements = template_elements + custom_elements ``` **替代方案**: - 按 ID/key 合并(类似 slot 机制) - 缺点:需要引入元素 ID 概念,复杂度高 - 缺点:打破向后兼容性 ### 决策4:空 elements 处理 **选择**:`elements: []` 等同于不指定 `elements` **理由**: - 保持 YAML 语法的自然语义 - 避免引入特殊行为差异 **实现**: ```python if slide_data.get("elements"): # 空列表为 False # 处理自定义元素 ``` ### 决策5:YAML 验证不变 **选择**:不修改 `yaml_loader.py` 的验证逻辑 **理由**: - 现有验证已允许 `template` 和 `elements` 同时存在(验证的是各自的结构) - 混合模式是渲染时的行为,不是 YAML 结构的变化 - 避免不必要的验证逻辑修改 ## Risks / Trade-offs ### 风险1:元素位置重叠导致意外的视觉覆盖 **场景**:用户在不知道模板布局的情况下,放置自定义元素与模板元素重叠 **缓解措施**: - 在预览模式中用户可以实时看到效果 - 文档中明确说明 z 轴顺序规则 - 未来可选:添加几何验证警告(非必需) ### 风险2:变量作用域混淆 **场景**:自定义元素引用了模板中未定义的变量 **缓解措施**: - 现有的 `resolve_value()` 已能捕获未定义变量并抛出错误 - 错误信息清晰指出缺少的变量名 ### 风险3:条件渲染元素与自定义元素的交互不明确 **场景**:模板中某个元素因 `visible: false` 被过滤,用户期望自定义元素"知道"这个区域 **缓解措施**: - 这是设计预期,不是 bug - 文档中说明条件渲染在合并前完成 - 用户需要了解模板的布局结构 ## 实现概要 ### 核心修改:Presentation.render_slide() ```python 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 无。所有设计决策已在探索阶段明确。