新增混合模式,允许幻灯片同时使用 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/: 完整变更记录
248 lines
7.4 KiB
Markdown
248 lines
7.4 KiB
Markdown
# 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
|
||
|
||
无。所有设计决策已在探索阶段明确。
|