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

248 lines
7.4 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.
# 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
# 处理自定义元素
```
### 决策5YAML 验证不变
**选择**:不修改 `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
无。所有设计决策已在探索阶段明确。