新增混合模式,允许幻灯片同时使用 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/: 完整变更记录
330 lines
14 KiB
Markdown
330 lines
14 KiB
Markdown
# Template System
|
||
|
||
## Purpose
|
||
|
||
Template system 提供可复用的幻灯片布局和样式定义。模板包含变量定义、元素列表,支持变量替换、条件渲染,以及从目录加载。颜色和样式直接在模板中定义,无需额外的主题抽象层。
|
||
|
||
## Requirements
|
||
|
||
### Requirement: 模板必须定义变量列表
|
||
|
||
模板 SHALL 包含 vars 字段,定义该模板需要的变量,包括变量名、是否必需、默认值等。
|
||
|
||
#### Scenario: 加载模板变量定义
|
||
|
||
- **WHEN** 模板文件定义了 `vars` 列表,包含 `name`、`required`、`default` 等字段
|
||
- **THEN** 系统成功加载变量定义,可通过模板对象访问
|
||
|
||
#### Scenario: 验证必需变量
|
||
|
||
- **WHEN** 模板定义了 `{name: title, required: true}` 的变量
|
||
- **THEN** 渲染时如果未提供该变量,系统抛出错误
|
||
|
||
#### Scenario: 使用变量默认值
|
||
|
||
- **WHEN** 模板定义了 `{name: subtitle, required: false, default: ""}` 的变量
|
||
- **THEN** 渲染时如果未提供该变量,系统使用空字符串作为默认值
|
||
|
||
### Requirement: 模板必须定义元素列表
|
||
|
||
模板 SHALL 包含 elements 字段,定义该模板的幻灯片布局和元素。
|
||
|
||
#### Scenario: 加载模板元素定义
|
||
|
||
- **WHEN** 模板文件定义了 `elements` 列表,包含文本、图片、形状等元素
|
||
- **THEN** 系统成功加载元素列表,准备渲染
|
||
|
||
#### Scenario: 元素包含模板变量引用
|
||
|
||
- **WHEN** 模板元素中包含 `{title}` 等模板变量引用
|
||
- **THEN** 系统在渲染时用用户提供的值替换变量
|
||
|
||
#### Scenario: 元素直接指定样式值
|
||
|
||
- **WHEN** 模板元素中直接指定 `color: "#4a90e2"` 等样式值
|
||
- **THEN** 系统正确应用该样式值
|
||
|
||
### Requirement: 系统必须支持模板渲染
|
||
|
||
系统 SHALL 能够根据用户提供的变量值渲染模板,生成实际的元素列表。
|
||
|
||
#### Scenario: 渲染包含变量的模板
|
||
|
||
- **WHEN** 用户提供 `{title: "Hello", subtitle: "World"}` 渲染模板
|
||
- **THEN** 系统将模板中的 `{title}` 替换为 "Hello",`{subtitle}` 替换为 "World"
|
||
|
||
#### Scenario: 数值类型自动转换
|
||
|
||
- **WHEN** 模板定义 `size: "{font_size}"` 且用户提供 `{font_size: "44"}`
|
||
- **THEN** 系统自动将字符串 "44" 转换为整数 44
|
||
|
||
#### Scenario: 检测未定义的模板变量
|
||
|
||
- **WHEN** 模板中引用了 `{undefined_var}`,但该变量未在 vars 中定义,也未由用户提供
|
||
- **THEN** 系统抛出错误,指出未定义的变量
|
||
|
||
### Requirement: 系统必须支持条件渲染
|
||
|
||
系统 SHALL 支持基于变量值的条件渲染,通过 `visible` 字段控制元素是否显示。条件表达式使用 simpleeval 引擎评估,支持复杂的逻辑判断、比较运算、成员测试和数学计算。
|
||
|
||
#### Scenario: 显示满足条件的元素
|
||
|
||
- **WHEN** 元素定义了 `visible: "{count > 0}"`,且用户提供的 count 大于 0
|
||
- **THEN** 系统渲染该元素
|
||
|
||
#### Scenario: 隐藏不满足条件的元素
|
||
|
||
- **WHEN** 元素定义了 `visible: "{count > 0}"`,但用户提供的 count 等于 0
|
||
- **THEN** 系统跳过该元素,不渲染到幻灯片中
|
||
|
||
#### Scenario: 复杂逻辑条件
|
||
|
||
- **WHEN** 元素定义了 `visible: "{count > 0 and status == 'active'}"`,且两个条件都满足
|
||
- **THEN** 系统渲染该元素
|
||
|
||
#### Scenario: 成员测试条件
|
||
|
||
- **WHEN** 元素定义了 `visible: "{status in ['draft', 'review']}"`,且 status 为 "draft"
|
||
- **THEN** 系统渲染该元素
|
||
|
||
#### Scenario: 数学运算条件
|
||
|
||
- **WHEN** 元素定义了 `visible: "{(price * discount) > 50}"`,且计算结果大于 50
|
||
- **THEN** 系统渲染该元素
|
||
|
||
#### Scenario: 条件表达式语法错误
|
||
|
||
- **WHEN** visible 字段包含无效的条件表达式(如 `{count > }`)
|
||
- **THEN** 系统抛出错误,提示"条件表达式语法错误",并显示具体的语法问题
|
||
|
||
#### Scenario: 条件表达式中的变量未定义
|
||
|
||
- **WHEN** visible 字段引用了未定义的变量(如 `{undefined_var > 0}`)
|
||
- **THEN** 系统抛出错误,提示"条件表达式中的变量未定义: undefined_var",并列出可用变量
|
||
|
||
#### Scenario: 条件表达式使用不支持的函数
|
||
|
||
- **WHEN** visible 字段使用了不在白名单中的函数(如 `{eval(code)}`)
|
||
- **THEN** 系统抛出错误,提示"条件表达式中使用了不支持的函数: eval"
|
||
|
||
#### Scenario: 向后兼容的简单表达式
|
||
|
||
- **WHEN** 元素定义了 `visible: "{subtitle != ''}"`(旧语法格式)
|
||
- **THEN** 系统使用新的 simpleeval 引擎正确评估该表达式
|
||
|
||
|
||
### Requirement: 模板文件必须可从指定目录加载
|
||
|
||
系统 SHALL 从用户通过 `--template-dir` 参数指定的目录加载模板文件,支持通过模板名称引用。模板名称必须是纯文件名,不能包含路径分隔符。
|
||
|
||
#### Scenario: 通过名称加载模板
|
||
|
||
- **WHEN** 幻灯片指定 `template: title-slide`,且用户提供 `--template-dir /path/to/templates`
|
||
- **THEN** 系统从 `/path/to/templates/title-slide.yaml` 加载模板文件
|
||
|
||
#### Scenario: 模板文件不存在时报错
|
||
|
||
- **WHEN** 幻灯片引用不存在的模板名称
|
||
- **THEN** 系统抛出错误,提示"模板文件不存在: <模板名>",并显示查找位置和期望文件路径
|
||
|
||
#### Scenario: 缓存已加载的模板
|
||
|
||
- **WHEN** 多个幻灯片使用同一个模板
|
||
- **THEN** 系统仅加载一次模板文件,后续使用缓存
|
||
|
||
#### Scenario: 错误信息包含详细的查找信息
|
||
|
||
- **WHEN** 模板文件未找到
|
||
- **THEN** 错误信息包含:模板名称、查找位置(template_dir)、期望文件的完整路径、解决建议
|
||
|
||
### Requirement: 系统必须支持自定义幻灯片
|
||
|
||
系统 SHALL 支持不使用模板的自定义幻灯片,以及同时使用模板和自定义元素的混合模式幻灯片。
|
||
|
||
#### Scenario: 渲染自定义幻灯片
|
||
|
||
- **WHEN** 幻灯片未指定 `template` 字段,直接包含 `elements` 列表
|
||
- **THEN** 系统跳过模板渲染,直接处理元素列表
|
||
|
||
#### Scenario: 自定义幻灯片中直接指定样式
|
||
|
||
- **WHEN** 自定义幻灯片的元素直接指定 `color: "#4a90e2"`
|
||
- **THEN** 系统正确应用该颜色值
|
||
|
||
#### Scenario: 自定义幻灯片和模板混合使用
|
||
|
||
- **WHEN** 演示文稿中部分幻灯片使用模板,部分为自定义
|
||
- **THEN** 系统正确处理两种类型的幻灯片
|
||
|
||
#### Scenario: 混合模式幻灯片同时使用模板和自定义元素
|
||
|
||
- **WHEN** 幻灯片同时指定了 `template` 字段和 `elements` 列表
|
||
- **THEN** 系统先渲染模板获取模板元素列表,再追加自定义元素列表,生成最终的元素列表
|
||
|
||
#### Scenario: 混合模式中模板元素在前
|
||
|
||
- **WHEN** 幻灯片使用混合模式,模板元素和自定义元素位置重叠
|
||
- **THEN** 自定义元素在 z 轴上覆盖模板元素(后渲染在上层)
|
||
|
||
### Requirement: 模板变量解析必须深度递归
|
||
|
||
系统 SHALL 递归解析模板元素的所有嵌套字段中的变量引用。
|
||
|
||
#### Scenario: 解析嵌套对象中的变量
|
||
|
||
- **WHEN** 模板元素定义了 `font: {size: "{font_size}", color: "{text_color}"}`
|
||
- **THEN** 系统正确解析嵌套对象中的所有变量引用
|
||
|
||
#### Scenario: 解析数组中的变量
|
||
|
||
- **WHEN** 模板元素定义了 `box: ["{left}", 2, 8, 3]`
|
||
- **THEN** 系统正确解析数组中的变量引用
|
||
|
||
#### Scenario: 解析多层嵌套的变量
|
||
|
||
- **WHEN** 模板包含复杂的嵌套结构,多层使用变量引用
|
||
- **THEN** 系统递归解析所有层级的变量,直到无变量引用为止
|
||
|
||
### Requirement: 模板名称必须是纯文件名
|
||
|
||
系统 SHALL 验证模板名称不包含路径分隔符,确保模板只能从指定目录的一层加载。
|
||
|
||
#### Scenario: 拒绝包含正斜杠的模板名称
|
||
|
||
- **WHEN** 幻灯片指定 `template: subdir/title-slide`
|
||
- **THEN** 系统抛出错误,提示"模板名称不能包含路径分隔符: subdir/title-slide"
|
||
|
||
#### Scenario: 拒绝包含反斜杠的模板名称
|
||
|
||
- **WHEN** 幻灯片指定 `template: subdir\title-slide`
|
||
- **THEN** 系统抛出错误,提示"模板名称不能包含路径分隔符: subdir\title-slide"
|
||
|
||
#### Scenario: 拒绝路径遍历尝试
|
||
|
||
- **WHEN** 幻灯片指定 `template: ../other-templates/slide`
|
||
- **THEN** 系统抛出错误,提示模板名称不能包含路径分隔符
|
||
|
||
#### Scenario: 接受纯文件名
|
||
|
||
- **WHEN** 幻灯片指定 `template: title-slide`(不包含路径分隔符)
|
||
- **THEN** 系统正常处理,从指定的 template_dir 加载模板
|
||
|
||
#### Scenario: 错误信息提供正确格式示例
|
||
|
||
- **WHEN** 系统因模板名称包含路径分隔符而报错
|
||
- **THEN** 错误信息中包含"模板名称应该是纯文件名,如: 'title-slide'"的提示
|
||
|
||
### Requirement: 未指定模板目录时必须报错
|
||
|
||
系统 SHALL 在用户未提供 `--template-dir` 参数但 YAML 文件中使用了模板时,立即报错。
|
||
|
||
#### Scenario: 使用模板但未指定目录
|
||
|
||
- **WHEN** YAML 文件中包含 `template: title-slide`,但 `templates_dir` 参数为 `None`
|
||
- **THEN** 系统在尝试加载模板时抛出错误,提示"未指定模板目录,无法加载模板"
|
||
|
||
#### Scenario: 不使用模板时不检查目录
|
||
|
||
- **WHEN** YAML 文件中所有幻灯片都是自定义幻灯片(不包含 `template` 字段)
|
||
- **THEN** 系统不检查 `templates_dir` 是否为 `None`,正常处理
|
||
|
||
### Requirement: 幻灯片定义必须支持 enabled 字段
|
||
|
||
幻灯片定义 SHALL 支持可选的 `enabled` 布尔字段,用于控制该幻灯片是否渲染。该字段与模板系统的其他字段(template、vars、elements、background)独立工作。
|
||
|
||
#### Scenario: 幻灯片包含 enabled 字段
|
||
|
||
- **WHEN** 幻灯片定义包含 `enabled: false` 或 `enabled: true`
|
||
- **THEN** 系统正常加载幻灯片定义,并在渲染时检查该字段
|
||
|
||
#### Scenario: enabled 字段可选
|
||
|
||
- **WHEN** 幻灯片定义未包含 `enabled` 字段
|
||
- **THEN** 系统默认该幻灯片为启用状态
|
||
|
||
#### Scenario: enabled 与 template 共存
|
||
|
||
- **WHEN** 幻灯片同时定义了 `enabled: false` 和 `template: title-slide`
|
||
- **THEN** 系统跳过该幻灯片,不加载模板
|
||
|
||
#### Scenario: enabled 与自定义幻灯片共存
|
||
|
||
- **WHEN** 自定义幻灯片(不使用模板)定义了 `enabled: false`
|
||
- **THEN** 系统跳过该幻灯片,不渲染元素列表
|
||
|
||
### Requirement: 模板与自定义元素必须支持变量共享
|
||
|
||
系统 SHALL 允许自定义元素访问模板中定义的变量,实现主题色、布局参数等值的统一控制。
|
||
|
||
#### Scenario: 自定义元素使用模板变量
|
||
|
||
- **WHEN** 幻灯片使用 `template: content-slide`,提供 `vars: {theme_color: "#3949ab"}`,且自定义元素中定义 `fill: "{theme_color}"`
|
||
- **THEN** 系统将自定义元素中的 `{theme_color}` 替换为 `"#3949ab"`
|
||
|
||
#### Scenario: 自定义元素使用模板默认变量
|
||
|
||
- **WHEN** 模板定义了 `default: "#3949ab"` 的 `theme_color` 变量,幻灯片未提供该变量值,且自定义元素引用 `{theme_color}`
|
||
- **THEN** 系统使用模板的默认值 `"#3949ab"` 进行替换
|
||
|
||
#### Scenario: 自定义元素引用未定义变量时报错
|
||
|
||
- **WHEN** 自定义元素引用了 `{undefined_var}`,且该变量未在模板 vars 中定义,也未由幻灯片提供
|
||
- **THEN** 系统抛出错误,指出未定义的变量
|
||
|
||
### Requirement: 元素合并必须采用追加策略
|
||
|
||
系统 SHALL 使用简单追加策略合并模板元素和自定义元素,保持渲染顺序和 z 轴层级。
|
||
|
||
#### Scenario: 模板元素和自定义元素合并顺序
|
||
|
||
- **WHEN** 模板渲染后产生 2 个元素,幻灯片自定义元素列表包含 3 个元素
|
||
- **THEN** 最终元素列表包含 5 个元素,顺序为:模板元素1、模板元素2、自定义元素1、自定义元素2、自定义元素3
|
||
|
||
#### Scenario: 空自定义元素列表
|
||
|
||
- **WHEN** 幻灯片指定 `template` 和 `elements: []`(空数组)
|
||
- **THEN** 最终元素列表仅包含模板元素,等同于不指定 `elements` 字段
|
||
|
||
#### Scenario: 模板条件渲染后的元素合并
|
||
|
||
- **WHEN** 模板包含 3 个元素,其中 1 个因 `visible` 条件为假被过滤,幻灯片包含 2 个自定义元素
|
||
- **THEN** 最终元素列表包含 4 个元素:模板的 2 个可见元素,加上幻灯片的 2 个自定义元素
|
||
|
||
### Requirement: 混合模式必须保持向后兼容
|
||
|
||
系统 SHALL 在不使用混合模式时,保持与现有版本完全一致的行为。
|
||
|
||
#### Scenario: 仅使用模板时不指定 elements
|
||
|
||
- **WHEN** 幻灯片仅指定 `template` 字段,不包含 `elements` 字段
|
||
- **THEN** 系统表现与现有版本完全一致,仅渲染模板元素
|
||
|
||
#### Scenario: 仅使用自定义元素时不指定 template
|
||
|
||
- **WHEN** 幻灯片仅指定 `elements` 字段,不包含 `template` 字段
|
||
- **THEN** 系统表现与现有版本完全一致,仅渲染自定义元素
|
||
|
||
#### Scenario: 既不使用模板也不使用自定义元素
|
||
|
||
- **WHEN** 幻灯片既不指定 `template` 也不指定 `elements`
|
||
- **THEN** 系统生成空幻灯片(仅包含背景设置)
|
||
|
||
### Requirement: 混合模式必须支持内联模板
|
||
|
||
系统 SHALL 在混合模式中支持内联模板与外部模板,功能保持一致。
|
||
|
||
#### Scenario: 内联模板与自定义元素混合使用
|
||
|
||
- **WHEN** 幻灯片引用内联模板(在 YAML 文件的 `templates` 字段中定义),同时包含 `elements` 列表
|
||
- **THEN** 系统正确渲染内联模板元素,并追加自定义元素
|
||
|
||
#### Scenario: 外部模板与自定义元素混合使用
|
||
|
||
- **WHEN** 幻灯片引用外部模板(从 `--template-dir` 目录加载),同时包含 `elements` 列表
|
||
- **THEN** 系统正确加载外部模板,渲染模板元素,并追加自定义元素
|
||
|
||
#### Scenario: 内联和外部模板在同一演示文稿中混合使用
|
||
|
||
- **WHEN** 演示文稿同时定义了内联模板和使用外部模板,且部分幻灯片使用混合模式
|
||
- **THEN** 系统正确处理所有组合情况
|