1
0

refactor: modularize yaml2pptx into layered architecture

Refactor yaml2pptx.py from a 1,245-line monolithic script into a modular
architecture with clear separation of concerns. The entry point is now
127 lines, with business logic distributed across focused modules.

Architecture:
- core/: Domain models (elements, template, presentation)
- loaders/: YAML loading and validation
- renderers/: PPTX and HTML rendering
- preview/: Flask preview server
- utils.py: Shared utilities

Key improvements:
- Element abstraction layer using dataclass with validation
- Renderer logic built into generator classes
- Single-direction dependencies (no circular imports)
- Each module 150-300 lines for better readability
- Backward compatible CLI interface

Documentation:
- README.md: User-facing usage guide
- README_DEV.md: Developer documentation

OpenSpec:
- Archive refactor-yaml2pptx-modular change (63/70 tasks complete)
- Sync 5 delta specs to main specs (2 new + 3 updated)
This commit is contained in:
2026-03-02 16:43:45 +08:00
parent b2132dc06b
commit ed940f0690
31 changed files with 3142 additions and 1307 deletions

View File

@@ -0,0 +1,114 @@
## ADDED Requirements
### Requirement: 元素必须使用 dataclass 定义
系统必须使用 Python dataclass 定义所有元素类型TextElement, ImageElement, ShapeElement, TableElement提供类型安全和自动生成的方法。
#### Scenario: 文本元素使用 dataclass 定义
- **WHEN** 开发者查看 core/elements.py 中的 TextElement 类
- **THEN** 应使用 `@dataclass` 装饰器定义,包含 type、content、box、font 字段
#### Scenario: 图片元素使用 dataclass 定义
- **WHEN** 开发者查看 core/elements.py 中的 ImageElement 类
- **THEN** 应使用 `@dataclass` 装饰器定义,包含 type、src、box 字段
#### Scenario: 形状元素使用 dataclass 定义
- **WHEN** 开发者查看 core/elements.py 中的 ShapeElement 类
- **THEN** 应使用 `@dataclass` 装饰器定义,包含 type、shape、box、fill、line 字段
#### Scenario: 表格元素使用 dataclass 定义
- **WHEN** 开发者查看 core/elements.py 中的 TableElement 类
- **THEN** 应使用 `@dataclass` 装饰器定义,包含 type、data、position、col_widths、style 字段
### Requirement: 元素必须在创建时进行验证
系统必须在元素对象创建时(`__post_init__` 方法)进行数据验证,确保元素数据的有效性,尽早发现错误。
#### Scenario: 文本元素验证 box 字段
- **WHEN** 创建 TextElement 对象时 box 字段不是包含 4 个数字的列表
- **THEN** 系统应抛出 ValueError 异常,提示 "box 必须包含 4 个数字"
#### Scenario: 图片元素验证 src 字段
- **WHEN** 创建 ImageElement 对象时 src 字段为空
- **THEN** 系统应抛出 ValueError 异常,提示 "图片元素必须指定 src"
#### Scenario: 形状元素验证 box 字段
- **WHEN** 创建 ShapeElement 对象时 box 字段不是包含 4 个数字的列表
- **THEN** 系统应抛出 ValueError 异常,提示 "box 必须包含 4 个数字"
#### Scenario: 表格元素验证 data 字段
- **WHEN** 创建 TableElement 对象时 data 字段为空列表
- **THEN** 系统应抛出 ValueError 异常,提示 "表格数据不能为空"
### Requirement: 必须提供元素工厂函数
系统必须提供 `create_element(elem_dict)` 工厂函数,从字典创建对应类型的元素对象,统一元素创建入口。
#### Scenario: 从字典创建文本元素
- **WHEN** 调用 `create_element({'type': 'text', 'content': 'Hello', 'box': [1, 1, 8, 1]})`
- **THEN** 系统应返回 TextElement 对象,包含相应的字段值
#### Scenario: 从字典创建图片元素
- **WHEN** 调用 `create_element({'type': 'image', 'src': 'image.png', 'box': [1, 1, 4, 3]})`
- **THEN** 系统应返回 ImageElement 对象,包含相应的字段值
#### Scenario: 从字典创建形状元素
- **WHEN** 调用 `create_element({'type': 'shape', 'shape': 'rectangle', 'box': [1, 1, 2, 1]})`
- **THEN** 系统应返回 ShapeElement 对象,包含相应的字段值
#### Scenario: 从字典创建表格元素
- **WHEN** 调用 `create_element({'type': 'table', 'data': [['A', 'B'], ['1', '2']], 'position': [1, 1]})`
- **THEN** 系统应返回 TableElement 对象,包含相应的字段值
#### Scenario: 不支持的元素类型
- **WHEN** 调用 `create_element({'type': 'unknown'})`
- **THEN** 系统应抛出异常,提示不支持的元素类型
### Requirement: 元素类型必须支持未来扩展
系统的元素抽象层设计必须支持未来添加新元素类型(如 VideoElement只需定义新的 dataclass 和在工厂函数中添加分支。
#### Scenario: 添加新元素类型的步骤清晰
- **WHEN** 开发者需要添加新元素类型(如 Video
- **THEN** 应只需要:
1. 在 core/elements.py 中定义新的 dataclass如 VideoElement
2.`create_element()` 工厂函数中添加对应的分支
3. 在各个渲染器中实现渲染方法
#### Scenario: 新元素类型不影响现有元素
- **WHEN** 添加新元素类型
- **THEN** 现有元素类型Text, Image, Shape, Table的行为不应受到影响
### Requirement: 元素数据类必须提供默认值
系统必须为元素数据类的可选字段提供合理的默认值,简化元素创建。
#### Scenario: 文本元素的默认值
- **WHEN** 创建 TextElement 对象时不提供 box 和 font 字段
- **THEN** 系统应使用默认值box=[1, 1, 8, 1]font={}
#### Scenario: 图片元素的默认值
- **WHEN** 创建 ImageElement 对象时不提供 box 字段
- **THEN** 系统应使用默认值box=[1, 1, 4, 3]
#### Scenario: 形状元素的默认值
- **WHEN** 创建 ShapeElement 对象时不提供 fill 和 line 字段
- **THEN** 系统应使用默认值或 None