diff --git a/README.md b/README.md index 080b34c..512006d 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,8 @@ slides: ### 条件渲染 +#### 元素级条件渲染 + 使用 `visible` 属性控制元素显示: ```yaml @@ -351,6 +353,64 @@ slides: visible: "{subtitle != ''}" # 仅当 subtitle 不为空时显示 ``` +#### 页面级启用控制 + +使用 `enabled` 参数控制整个幻灯片是否渲染: + +```yaml +slides: + # 正常渲染的幻灯片 + - template: title-slide + vars: + title: "主标题" + + # 临时禁用的幻灯片(开发调试) + - enabled: false + template: work-in-progress + vars: + title: "未完成的内容" + + # 继续渲染后续幻灯片 + - template: content-slide + vars: + title: "内容页" +``` + +**enabled 参数说明**: +- 类型:布尔值(`true` 或 `false`) +- 默认值:`true`(未指定时默认启用) +- 用途:临时禁用幻灯片,无需删除或注释 YAML 内容 +- 场景:开发调试、版本控制、A/B 测试 + +**enabled vs visible 的区别**: + +| 特性 | `enabled`(页面级) | `visible`(元素级) | +|------|-------------------|-------------------| +| 作用范围 | 整个幻灯片 | 单个元素 | +| 类型 | 布尔值 | 条件表达式 | +| 判断时机 | 加载时(静态) | 渲染时(动态) | +| 使用场景 | 临时禁用页面 | 条件显示元素 | + +**示例**: + +```yaml +slides: + # 页面启用,但副标题元素可能隐藏 + - enabled: true + template: title-slide + vars: + title: "标题" + subtitle: "" # 空字符串,元素级 visible 会隐藏副标题 + + # 整页禁用,不渲染 + - enabled: false + elements: + - type: text + content: "这一页不会出现在最终 PPTX 中" + box: [1, 1, 8, 1] + font: {size: 44} +``` + ## 🎯 命令行选项 ### check 命令 diff --git a/README_DEV.md b/README_DEV.md index 7f3611f..8de3217 100644 --- a/README_DEV.md +++ b/README_DEV.md @@ -75,7 +75,13 @@ yaml2pptx.py (入口) - `/// script` 依赖声明 - `parse_args()` - 命令行参数解析 - `main()` - 主流程编排 + - `handle_convert()` - 转换流程,包含页面级 `enabled` 检查 - **不包含**:业务逻辑、数据处理 +- **enabled 实现细节**: + - 在主渲染循环中检查 `slide_data.get('enabled', True)` + - 跳过 `enabled=false` 的幻灯片,不调用 `render_slide()` + - 维护独立的 `slide_index` 计数器,只统计实际渲染的幻灯片 + - 进度日志显示准确的渲染数量(不包括禁用的幻灯片) ### 2. utils.py(工具层) - **职责**:通用工具函数 @@ -88,12 +94,13 @@ yaml2pptx.py (入口) - **包含**: - `YAMLError` - 自定义异常 - `load_yaml_file()` - 加载 YAML 文件 - - `validate_presentation_yaml()` - 验证演示文稿结构,调用 `validate_templates_yaml()` 验证内联模板 + - `validate_presentation_yaml()` - 验证演示文稿结构,调用 `validate_templates_yaml()` 验证内联模板,验证幻灯片 `enabled` 字段 - `validate_template_yaml()` - 验证外部模板结构 - `validate_templates_yaml()` - 验证内联模板结构(templates 字段) - **特点**: - 内联模板验证包括:结构验证、元素验证、变量定义验证、默认值验证 - 检测默认值中引用不存在的变量 + - 验证 `enabled` 字段必须是布尔值,拒绝字符串或条件表达式 ### 4. core/elements.py(核心层 - 元素抽象) - **职责**:定义元素数据类和工厂函数 diff --git a/loaders/yaml_loader.py b/loaders/yaml_loader.py index 65a6103..b0c01e4 100644 --- a/loaders/yaml_loader.py +++ b/loaders/yaml_loader.py @@ -78,6 +78,15 @@ def validate_presentation_yaml(data, file_path=""): if not isinstance(data['slides'], list): raise YAMLError(f"{file_path}: 'slides' 必须是一个列表") + # 验证每个幻灯片的 enabled 字段 + for i, slide in enumerate(data['slides']): + if isinstance(slide, dict) and 'enabled' in slide: + if not isinstance(slide['enabled'], bool): + raise YAMLError( + f"{file_path}: slides[{i}].enabled 必须是布尔值(true 或 false)," + f"不支持字符串或条件表达式" + ) + # 验证 templates 字段(内联模板) validate_templates_yaml(data, file_path) diff --git a/openspec/changes/archive/2026-03-03-add-slide-enabled-control/.openspec.yaml b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/.openspec.yaml new file mode 100644 index 0000000..85cf50d --- /dev/null +++ b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-03 diff --git a/openspec/changes/archive/2026-03-03-add-slide-enabled-control/design.md b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/design.md new file mode 100644 index 0000000..b7f6bca --- /dev/null +++ b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/design.md @@ -0,0 +1,90 @@ +## Context + +当前系统支持元素级的 `visible` 条件渲染,通过条件表达式(如 `{subtitle != ''}`)动态控制元素是否显示。但在开发和调试过程中,经常需要临时禁用整个幻灯片,当前只能通过注释或删除 YAML 内容来实现,不够便捷。 + +现有渲染流程: +- `yaml2pptx.py` 主循环遍历所有幻灯片 +- `Presentation.render_slide()` 渲染单个幻灯片 +- `Template.render()` 处理模板变量和元素级 `visible` 检查 + +## Goals / Non-Goals + +**Goals:** +- 添加页面级 `enabled` 布尔参数,默认为 `true` +- 在渲染循环中检查 `enabled`,跳过禁用的幻灯片 +- 保持与元素级 `visible` 的独立性和兼容性 +- 向后兼容现有 YAML 文件 + +**Non-Goals:** +- 不支持 `enabled` 的条件表达式(仅支持布尔值) +- 不修改元素级 `visible` 的实现 +- 不影响预览模式的显示逻辑(本次不涉及) + +## Decisions + +### 决策 1: 使用 `enabled` 而非 `visible` + +**选择**: 页面级使用 `enabled` 参数 +**理由**: +- 语义清晰:`enabled` 表示静态开关,`visible` 表示动态条件 +- 避免混淆:与元素级 `visible` 区分开 +- 符合直觉:enabled=false 表示禁用整页 + +**备选方案**: 使用 `visible` 统一命名 +- 缺点:语义不清,容易与元素级混淆 +- 缺点:暗示支持条件表达式,但实际只支持布尔值 + +### 决策 2: 默认值为 `true` + +**选择**: 未指定 `enabled` 时默认为 `true` +**理由**: +- 向后兼容:现有 YAML 文件无需修改 +- 符合预期:默认行为是渲染所有幻灯片 + +### 决策 3: 在主循环检查,而非 `render_slide()` + +**选择**: 在 `yaml2pptx.py` 的主循环中检查 `enabled` +**理由**: +- 性能:跳过禁用页面,避免不必要的模板加载和渲染 +- 清晰:页面级控制在页面级处理,不侵入渲染逻辑 +- 日志:可以在跳过时记录日志 + +**备选方案**: 在 `Presentation.render_slide()` 中检查 +- 缺点:已经进入渲染流程,浪费性能 +- 缺点:返回值处理复杂(需要返回 None 或特殊标记) + +### 决策 4: 渲染计数逻辑 + +**选择**: 维护独立的 `slide_index` 计数器,只统计实际渲染的幻灯片 +**理由**: +- 日志准确:进度显示反映实际渲染数量 +- 用户友好:不会显示"处理 3/5"然后跳过 2 个的困惑情况 + +**实现**: +```python +slide_index = 0 +for i, slide_data in enumerate(slides_data, 1): + if not slide_data.get('enabled', True): + continue + slide_index += 1 + log_progress(slide_index, total_slides, f"处理幻灯片") +``` + +## Risks / Trade-offs + +**风险 1: 用户误用 `enabled` 作为条件渲染** +- 描述:用户可能期望 `enabled: "{some_var}"` 支持条件表达式 +- 缓解:文档明确说明 `enabled` 仅支持布尔值,条件渲染使用元素级 `visible` + +**风险 2: 预览模式下禁用页面的显示** +- 描述:预览模式可能需要显示禁用的页面(灰色或标记) +- 缓解:本次不涉及预览模式,后续可扩展 + +**权衡 1: 简单性 vs 灵活性** +- 选择:简单的布尔值而非条件表达式 +- 理由:满足 90% 的使用场景(调试、版本控制),保持简单 + +**权衡 2: 日志详细度** +- 当前:跳过禁用页面时不记录日志 +- 备选:记录"跳过第 X 页(已禁用)" +- 选择:不记录,避免日志噪音 diff --git a/openspec/changes/archive/2026-03-03-add-slide-enabled-control/proposal.md b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/proposal.md new file mode 100644 index 0000000..d4221d8 --- /dev/null +++ b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/proposal.md @@ -0,0 +1,27 @@ +## Why + +在开发和调试演示文稿时,经常需要临时禁用某些幻灯片而不删除它们。当前系统只有元素级的 `visible` 条件渲染,无法快速禁用整个页面。添加页面级的 `enabled` 参数可以提供简单的开关控制,方便开发调试和版本管理。 + +## What Changes + +- 在幻灯片定义中添加可选的 `enabled` 布尔参数(默认为 `true`) +- 渲染流程中检查 `enabled` 参数,跳过禁用的幻灯片 +- 保留现有的元素级 `visible` 条件渲染功能 +- 添加 YAML 验证支持 `enabled` 字段 +- 更新文档说明页面级和元素级控制的区别 + +## Capabilities + +### New Capabilities +- `slide-enabled-control`: 页面级启用/禁用控制,通过 `enabled` 布尔参数控制整个幻灯片是否渲染 + +### Modified Capabilities +- `template-system`: 扩展幻灯片定义,添加 `enabled` 字段支持 + +## Impact + +- `yaml2pptx.py`: 主渲染循环需要检查 `enabled` 参数 +- `loaders/yaml_loader.py`: 添加 `enabled` 字段验证 +- `openspec/specs/template-system/spec.md`: 添加页面级 enabled 需求 +- 测试文件: 添加 enabled 相关测试用例 +- 文档: README.md 和 README_DEV.md 需要更新 diff --git a/openspec/changes/archive/2026-03-03-add-slide-enabled-control/specs/slide-enabled-control/spec.md b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/specs/slide-enabled-control/spec.md new file mode 100644 index 0000000..6bfa7ab --- /dev/null +++ b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/specs/slide-enabled-control/spec.md @@ -0,0 +1,73 @@ +# Slide Enabled Control + +## Purpose + +页面级启用/禁用控制,通过 `enabled` 布尔参数控制整个幻灯片是否渲染。提供简单的静态开关,用于开发调试和版本管理。 + +## Requirements + +### Requirement: 幻灯片必须支持 enabled 参数 + +幻灯片定义 SHALL 支持可选的 `enabled` 布尔参数,用于控制该幻灯片是否渲染。 + +#### Scenario: 禁用的幻灯片不渲染 + +- **WHEN** 幻灯片定义了 `enabled: false` +- **THEN** 系统跳过该幻灯片,不渲染到演示文稿中 + +#### Scenario: 启用的幻灯片正常渲染 + +- **WHEN** 幻灯片定义了 `enabled: true` +- **THEN** 系统正常渲染该幻灯片 + +#### Scenario: 默认启用幻灯片 + +- **WHEN** 幻灯片未定义 `enabled` 字段 +- **THEN** 系统默认该幻灯片为启用状态,正常渲染 + +### Requirement: enabled 参数必须是布尔值 + +系统 SHALL 验证 `enabled` 参数的类型为布尔值,不支持条件表达式。 + +#### Scenario: 接受布尔值 + +- **WHEN** 幻灯片定义了 `enabled: false` 或 `enabled: true` +- **THEN** 系统正常处理 + +#### Scenario: 拒绝非布尔值 + +- **WHEN** 幻灯片定义了 `enabled: "false"` 或 `enabled: 0` 等非布尔值 +- **THEN** 系统抛出验证错误,提示 enabled 必须是布尔值 + +#### Scenario: 拒绝条件表达式 + +- **WHEN** 幻灯片定义了 `enabled: "{some_var}"` +- **THEN** 系统抛出验证错误,提示 enabled 不支持条件表达式 + +### Requirement: enabled 与元素级 visible 独立工作 + +系统 SHALL 支持页面级 `enabled` 和元素级 `visible` 同时使用,两者独立判断。 + +#### Scenario: enabled 和 visible 共存 + +- **WHEN** 幻灯片定义了 `enabled: true`,且模板元素包含 `visible` 条件 +- **THEN** 系统先检查页面级 enabled,再检查元素级 visible + +#### Scenario: enabled=false 时不检查 visible + +- **WHEN** 幻灯片定义了 `enabled: false` +- **THEN** 系统跳过该幻灯片,不执行模板渲染和元素级 visible 检查 + +### Requirement: 渲染统计必须准确 + +系统 SHALL 只统计实际渲染的幻灯片数量,不包括禁用的幻灯片。 + +#### Scenario: 进度显示准确 + +- **WHEN** 演示文稿包含 5 个幻灯片,其中 2 个 enabled=false +- **THEN** 系统显示"处理幻灯片 1/3"、"处理幻灯片 2/3"、"处理幻灯片 3/3" + +#### Scenario: 最终统计准确 + +- **WHEN** 演示文稿包含 5 个幻灯片,其中 2 个 enabled=false +- **THEN** 生成的 PPTX 文件包含 3 个幻灯片 diff --git a/openspec/changes/archive/2026-03-03-add-slide-enabled-control/specs/template-system/spec.md b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/specs/template-system/spec.md new file mode 100644 index 0000000..3db6b12 --- /dev/null +++ b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/specs/template-system/spec.md @@ -0,0 +1,27 @@ +# Template System (Delta) + +## ADDED Requirements + +### 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** 系统跳过该幻灯片,不渲染元素列表 diff --git a/openspec/changes/archive/2026-03-03-add-slide-enabled-control/tasks.md b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/tasks.md new file mode 100644 index 0000000..2e60213 --- /dev/null +++ b/openspec/changes/archive/2026-03-03-add-slide-enabled-control/tasks.md @@ -0,0 +1,33 @@ +## 1. YAML 验证支持 + +- [x] 1.1 在 loaders/yaml_loader.py 中添加 enabled 字段验证(布尔类型,可选) +- [x] 1.2 添加 enabled 字段类型检查,拒绝非布尔值 +- [x] 1.3 添加测试用例:test_slide_enabled_validation + +## 2. 渲染逻辑修改 + +- [x] 2.1 修改 yaml2pptx.py 主循环,添加 enabled 检查 +- [x] 2.2 实现独立的 slide_index 计数器,只统计实际渲染的幻灯片 +- [x] 2.3 确保 log_progress 显示准确的渲染进度 + +## 3. 测试用例 + +- [x] 3.1 添加测试:test_slide_enabled_false(禁用幻灯片被跳过) +- [x] 3.2 添加测试:test_slide_enabled_default_true(默认启用) +- [x] 3.3 添加测试:test_slide_enabled_with_template(enabled 与模板共存) +- [x] 3.4 添加测试:test_slide_enabled_with_custom_slide(enabled 与自定义幻灯片共存) +- [x] 3.5 添加测试:test_slide_enabled_with_element_visible(enabled 和 visible 共存) +- [x] 3.6 添加测试:test_slide_enabled_count(渲染统计准确) +- [x] 3.7 添加测试:test_slide_enabled_invalid_type(拒绝非布尔值) + +## 4. 文档更新 + +- [x] 4.1 更新 README.md,添加 enabled 参数说明和使用示例 +- [x] 4.2 更新 README_DEV.md,说明 enabled 的实现细节 +- [x] 4.3 在文档中明确 enabled(静态开关)和 visible(动态条件)的区别 + +## 5. 集成测试 + +- [x] 5.1 创建测试 YAML 文件,包含启用和禁用的幻灯片 +- [x] 5.2 运行完整转换流程,验证生成的 PPTX 文件正确 +- [x] 5.3 验证日志输出准确反映实际渲染的幻灯片数量 diff --git a/openspec/specs/slide-enabled-control/spec.md b/openspec/specs/slide-enabled-control/spec.md new file mode 100644 index 0000000..6bfa7ab --- /dev/null +++ b/openspec/specs/slide-enabled-control/spec.md @@ -0,0 +1,73 @@ +# Slide Enabled Control + +## Purpose + +页面级启用/禁用控制,通过 `enabled` 布尔参数控制整个幻灯片是否渲染。提供简单的静态开关,用于开发调试和版本管理。 + +## Requirements + +### Requirement: 幻灯片必须支持 enabled 参数 + +幻灯片定义 SHALL 支持可选的 `enabled` 布尔参数,用于控制该幻灯片是否渲染。 + +#### Scenario: 禁用的幻灯片不渲染 + +- **WHEN** 幻灯片定义了 `enabled: false` +- **THEN** 系统跳过该幻灯片,不渲染到演示文稿中 + +#### Scenario: 启用的幻灯片正常渲染 + +- **WHEN** 幻灯片定义了 `enabled: true` +- **THEN** 系统正常渲染该幻灯片 + +#### Scenario: 默认启用幻灯片 + +- **WHEN** 幻灯片未定义 `enabled` 字段 +- **THEN** 系统默认该幻灯片为启用状态,正常渲染 + +### Requirement: enabled 参数必须是布尔值 + +系统 SHALL 验证 `enabled` 参数的类型为布尔值,不支持条件表达式。 + +#### Scenario: 接受布尔值 + +- **WHEN** 幻灯片定义了 `enabled: false` 或 `enabled: true` +- **THEN** 系统正常处理 + +#### Scenario: 拒绝非布尔值 + +- **WHEN** 幻灯片定义了 `enabled: "false"` 或 `enabled: 0` 等非布尔值 +- **THEN** 系统抛出验证错误,提示 enabled 必须是布尔值 + +#### Scenario: 拒绝条件表达式 + +- **WHEN** 幻灯片定义了 `enabled: "{some_var}"` +- **THEN** 系统抛出验证错误,提示 enabled 不支持条件表达式 + +### Requirement: enabled 与元素级 visible 独立工作 + +系统 SHALL 支持页面级 `enabled` 和元素级 `visible` 同时使用,两者独立判断。 + +#### Scenario: enabled 和 visible 共存 + +- **WHEN** 幻灯片定义了 `enabled: true`,且模板元素包含 `visible` 条件 +- **THEN** 系统先检查页面级 enabled,再检查元素级 visible + +#### Scenario: enabled=false 时不检查 visible + +- **WHEN** 幻灯片定义了 `enabled: false` +- **THEN** 系统跳过该幻灯片,不执行模板渲染和元素级 visible 检查 + +### Requirement: 渲染统计必须准确 + +系统 SHALL 只统计实际渲染的幻灯片数量,不包括禁用的幻灯片。 + +#### Scenario: 进度显示准确 + +- **WHEN** 演示文稿包含 5 个幻灯片,其中 2 个 enabled=false +- **THEN** 系统显示"处理幻灯片 1/3"、"处理幻灯片 2/3"、"处理幻灯片 3/3" + +#### Scenario: 最终统计准确 + +- **WHEN** 演示文稿包含 5 个幻灯片,其中 2 个 enabled=false +- **THEN** 生成的 PPTX 文件包含 3 个幻灯片 diff --git a/openspec/specs/template-system/spec.md b/openspec/specs/template-system/spec.md index b9192c4..c7d4703 100644 --- a/openspec/specs/template-system/spec.md +++ b/openspec/specs/template-system/spec.md @@ -186,3 +186,27 @@ Template system 提供可复用的幻灯片布局和样式定义。模板包含 - **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** 系统跳过该幻灯片,不渲染元素列表 diff --git a/tests/unit/test_loaders/test_yaml_loader.py b/tests/unit/test_loaders/test_yaml_loader.py index 372e3a0..6373ad7 100644 --- a/tests/unit/test_loaders/test_yaml_loader.py +++ b/tests/unit/test_loaders/test_yaml_loader.py @@ -90,6 +90,56 @@ class TestValidatePresentationYaml: validate_presentation_yaml(data, "test.yaml") assert "test.yaml" in str(exc_info.value) + def test_slide_enabled_true(self): + """测试 enabled=true 的幻灯片""" + data = { + "slides": [ + {"enabled": True, "elements": []} + ] + } + # 不应该引发错误 + validate_presentation_yaml(data) + + def test_slide_enabled_false(self): + """测试 enabled=false 的幻灯片""" + data = { + "slides": [ + {"enabled": False, "elements": []} + ] + } + # 不应该引发错误 + validate_presentation_yaml(data) + + def test_slide_enabled_invalid_string(self): + """测试 enabled 为字符串时拒绝""" + data = { + "slides": [ + {"enabled": "false", "elements": []} + ] + } + with pytest.raises(YAMLError, match="enabled 必须是布尔值"): + validate_presentation_yaml(data) + + def test_slide_enabled_invalid_number(self): + """测试 enabled 为数字时拒绝""" + data = { + "slides": [ + {"enabled": 0, "elements": []} + ] + } + with pytest.raises(YAMLError, match="enabled 必须是布尔值"): + validate_presentation_yaml(data) + + def test_slide_enabled_invalid_expression(self): + """测试 enabled 为条件表达式时拒绝""" + data = { + "slides": [ + {"enabled": "{some_var}", "elements": []} + ] + } + with pytest.raises(YAMLError, match="enabled 必须是布尔值"): + validate_presentation_yaml(data) + class TestValidateTemplateYaml: """validate_template_yaml 函数测试类""" diff --git a/tests/unit/test_slide_enabled.py b/tests/unit/test_slide_enabled.py new file mode 100644 index 0000000..241d3ac --- /dev/null +++ b/tests/unit/test_slide_enabled.py @@ -0,0 +1,223 @@ +""" +幻灯片 enabled 参数测试 + +测试页面级 enabled 参数的功能 +""" + +import pytest +from pathlib import Path +from core.presentation import Presentation +from renderers.pptx_renderer import PptxGenerator + + +class TestSlideEnabled: + """幻灯片 enabled 参数测试类""" + + def test_slide_enabled_false(self, temp_dir): + """测试 enabled=false 的幻灯片被跳过""" + yaml_content = """ +slides: + - elements: + - type: text + content: "Slide 1" + box: [1, 1, 8, 1] + font: {size: 44} + - enabled: false + elements: + - type: text + content: "Slide 2 (disabled)" + box: [1, 1, 8, 1] + font: {size: 44} + - elements: + - type: text + content: "Slide 3" + box: [1, 1, 8, 1] + font: {size: 44} +""" + yaml_path = temp_dir / "test.yaml" + yaml_path.write_text(yaml_content) + + pres = Presentation(str(yaml_path)) + slides_data = pres.data.get('slides', []) + + # 统计启用的幻灯片 + enabled_slides = [s for s in slides_data if s.get('enabled', True)] + assert len(enabled_slides) == 2 # 只有 2 个启用 + + def test_slide_enabled_default_true(self, temp_dir): + """测试未指定 enabled 时默认为 true""" + yaml_content = """ +slides: + - elements: + - type: text + content: "Slide 1" + box: [1, 1, 8, 1] + font: {size: 44} +""" + yaml_path = temp_dir / "test.yaml" + yaml_path.write_text(yaml_content) + + pres = Presentation(str(yaml_path)) + slides_data = pres.data.get('slides', []) + + # 默认应该启用 + assert slides_data[0].get('enabled', True) is True + + def test_slide_enabled_with_template(self, temp_dir): + """测试 enabled 与模板共存""" + # 创建模板 + template_dir = temp_dir / "templates" + template_dir.mkdir() + template_file = template_dir / "title-slide.yaml" + template_content = """ +vars: + - name: title + required: true +elements: + - type: text + box: [1, 2, 8, 1] + content: "{title}" + font: + size: 44 +""" + template_file.write_text(template_content) + + yaml_content = """ +slides: + - template: title-slide + enabled: false + vars: + title: "Disabled Title" + - template: title-slide + vars: + title: "Enabled Title" +""" + yaml_path = temp_dir / "test.yaml" + yaml_path.write_text(yaml_content) + + pres = Presentation(str(yaml_path), str(template_dir)) + slides_data = pres.data.get('slides', []) + + # 第一个禁用,第二个启用 + assert slides_data[0].get('enabled', True) is False + assert slides_data[1].get('enabled', True) is True + + def test_slide_enabled_with_custom_slide(self, temp_dir): + """测试 enabled 与自定义幻灯片共存""" + yaml_content = """ +slides: + - enabled: false + elements: + - type: text + content: "Disabled Custom" + box: [1, 1, 8, 1] + font: {size: 44} + - elements: + - type: text + content: "Enabled Custom" + box: [1, 1, 8, 1] + font: {size: 44} +""" + yaml_path = temp_dir / "test.yaml" + yaml_path.write_text(yaml_content) + + pres = Presentation(str(yaml_path)) + slides_data = pres.data.get('slides', []) + + assert slides_data[0].get('enabled', True) is False + assert slides_data[1].get('enabled', True) is True + + def test_slide_enabled_with_element_visible(self, temp_dir): + """测试 enabled 和 visible 共存""" + # 创建模板 + template_dir = temp_dir / "templates" + template_dir.mkdir() + template_file = template_dir / "title-slide.yaml" + template_content = """ +vars: + - name: title + required: true + - name: subtitle + required: false + default: "" +elements: + - type: text + box: [1, 2, 8, 1] + content: "{title}" + font: + size: 44 + - type: text + box: [1, 3.5, 8, 0.5] + content: "{subtitle}" + visible: "{subtitle != ''}" + font: + size: 24 +""" + template_file.write_text(template_content) + + yaml_content = """ +slides: + - template: title-slide + enabled: true + vars: + title: "Title" + subtitle: "" +""" + yaml_path = temp_dir / "test.yaml" + yaml_path.write_text(yaml_content) + + pres = Presentation(str(yaml_path), str(template_dir)) + slide_data = pres.data['slides'][0] + + # 页面启用 + assert slide_data.get('enabled', True) is True + + # 渲染幻灯片,元素级 visible 会隐藏空副标题 + rendered = pres.render_slide(slide_data) + # 只有标题元素,副标题被 visible 隐藏 + assert len(rendered['elements']) == 1 + + def test_slide_enabled_count(self, temp_dir): + """测试渲染统计准确""" + yaml_content = """ +slides: + - elements: + - type: text + content: "Slide 1" + box: [1, 1, 8, 1] + font: {size: 44} + - enabled: false + elements: + - type: text + content: "Slide 2" + box: [1, 1, 8, 1] + font: {size: 44} + - enabled: false + elements: + - type: text + content: "Slide 3" + box: [1, 1, 8, 1] + font: {size: 44} + - elements: + - type: text + content: "Slide 4" + box: [1, 1, 8, 1] + font: {size: 44} + - elements: + - type: text + content: "Slide 5" + box: [1, 1, 8, 1] + font: {size: 44} +""" + yaml_path = temp_dir / "test.yaml" + yaml_path.write_text(yaml_content) + + pres = Presentation(str(yaml_path)) + slides_data = pres.data.get('slides', []) + + # 总共 5 个幻灯片 + assert len(slides_data) == 5 + + # 只有 3 个启用 + enabled_slides = [s for s in slides_data if s.get('enabled', True)] + assert len(enabled_slides) == 3 diff --git a/yaml2pptx.py b/yaml2pptx.py index d7fc2e1..28784bd 100644 --- a/yaml2pptx.py +++ b/yaml2pptx.py @@ -200,8 +200,18 @@ def handle_convert(args): slides_data = pres.data.get('slides', []) total_slides = len(slides_data) + # 统计实际渲染的幻灯片数量 + enabled_slides = [s for s in slides_data if s.get('enabled', True)] + enabled_count = len(enabled_slides) + + slide_index = 0 for i, slide_data in enumerate(slides_data, 1): - log_progress(i, total_slides, f"处理幻灯片") + # 检查页面级 enabled + if not slide_data.get('enabled', True): + continue # 跳过禁用的页面 + + slide_index += 1 + log_progress(slide_index, enabled_count, f"处理幻灯片") rendered_slide = pres.render_slide(slide_data) generator.add_slide(rendered_slide, input_path.parent)