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

435
README_DEV.md Normal file
View File

@@ -0,0 +1,435 @@
# 开发文档
本文档说明 yaml2pptx 项目的代码结构、开发规范和技术决策。
## 项目概述
yaml2pptx 是一个将 YAML 格式的演示文稿源文件转换为 PPTX 文件的工具,支持模板系统和浏览器预览功能。
## 代码结构
项目采用模块化架构,按功能职责组织代码:
```
html2pptx/
├── yaml2pptx.py (127 行) # 入口脚本CLI + main 函数
├── utils.py (74 行) # 工具函数(日志、颜色转换)
├── core/ # 核心领域模型
│ ├── elements.py (96 行) # 元素抽象层dataclass
│ ├── template.py (191 行) # 模板系统
│ └── presentation.py (91 行) # 演示文稿类
├── loaders/ # 数据加载层
│ └── yaml_loader.py (113 行) # YAML 加载和验证
├── renderers/ # 渲染层
│ ├── pptx_renderer.py (292 行) # PPTX 渲染器
│ └── html_renderer.py (172 行) # HTML 渲染器(预览)
└── preview/ # 预览功能
└── server.py (244 行) # Flask 服务器 + 文件监听
```
### 依赖关系
```
yaml2pptx.py (入口)
├─→ utils (工具函数)
├─→ loaders.yaml_loader (YAML 加载)
├─→ core.presentation (演示文稿)
│ ↓
│ ├─→ core.template (模板)
│ └─→ core.elements (元素)
├─→ renderers.pptx_renderer (PPTX 生成)
│ ↓
│ └─→ core.elements
└─→ preview.server (预览服务)
└─→ renderers.html_renderer
└─→ core.elements
```
**依赖原则**
- 单向依赖:入口 → 渲染 → 核心 ← 加载
- 无循环依赖
- 核心层不依赖其他业务模块
## 模块职责
### 1. yaml2pptx.py入口层
- **职责**CLI 参数解析、流程编排
- **行数**:约 100-150 行
- **包含**
- `/// script` 依赖声明
- `parse_args()` - 命令行参数解析
- `main()` - 主流程编排
- **不包含**:业务逻辑、数据处理
### 2. utils.py工具层
- **职责**:通用工具函数
- **包含**
- 日志函数:`log_info()`, `log_success()`, `log_error()`, `log_progress()`
- 颜色转换:`hex_to_rgb()`, `validate_color()`
### 3. loaders/yaml_loader.py加载层
- **职责**YAML 文件加载和验证
- **包含**
- `YAMLError` - 自定义异常
- `load_yaml_file()` - 加载 YAML 文件
- `validate_presentation_yaml()` - 验证演示文稿结构
- `validate_template_yaml()` - 验证模板结构
### 4. core/elements.py核心层 - 元素抽象)
- **职责**:定义元素数据类和工厂函数
- **包含**
- `TextElement` - 文本元素dataclass
- `ImageElement` - 图片元素dataclass
- `ShapeElement` - 形状元素dataclass
- `TableElement` - 表格元素dataclass
- `create_element()` - 元素工厂函数
- **特点**
- 使用 `@dataclass` 装饰器
-`__post_init__` 中进行验证
- 创建时验证,尽早发现错误
### 5. core/template.py核心层 - 模板)
- **职责**:模板加载和变量解析
- **包含**
- `Template`
- 变量解析:`resolve_value()`, `resolve_element()`
- 条件渲染:`evaluate_condition()`
- 模板渲染:`render()`
### 6. core/presentation.py核心层 - 演示文稿)
- **职责**:演示文稿管理和幻灯片渲染
- **包含**
- `Presentation`
- 模板缓存:`get_template()`
- 幻灯片渲染:`render_slide()`
- **特点**
- 将元素字典转换为元素对象
- 使用 `create_element()` 工厂函数
### 7. renderers/pptx_renderer.py渲染层 - PPTX
- **职责**PPTX 文件生成
- **包含**
- `PptxGenerator`
- 渲染方法:`_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()`
- 元素分发:`_render_element()`
- **特点**
- 渲染器内置在生成器中
- 使用 `isinstance()` 检查元素类型
- 通过元素对象的属性访问数据
### 8. renderers/html_renderer.py渲染层 - HTML
- **职责**HTML 预览渲染
- **包含**
- `HtmlRenderer`
- 渲染方法:`render_text()`, `render_image()`, `render_shape()`, `render_table()`
- **特点**
- 与 PptxRenderer 共享元素抽象层
- 使用固定 DPI (96) 进行单位转换
### 9. preview/server.py预览层
- **职责**:浏览器预览和热重载
- **包含**
- Flask 应用:`create_flask_app()`
- 文件监听:`YAMLChangeHandler`
- 预览服务器:`start_preview_server()`
- HTML 模板:`HTML_TEMPLATE`, `ERROR_TEMPLATE`
## 开发规范
### 1. Python 环境
**必须使用 uv 运行脚本**
```bash
# 正确
uv run yaml2pptx.py input.yaml output.pptx
# 错误 - 严禁直接使用主机环境的 python
python yaml2pptx.py input.yaml output.pptx
```
**依赖管理**
- 所有依赖在 `yaml2pptx.py``/// script` 头部声明
- uv 会自动安装依赖,无需手动 `pip install`
### 2. 文件组织
**代码文件**
- 每个模块文件控制在 150-300 行
- 入口脚本约 100 行
- 使用有意义的文件名和目录结构
**测试文件**
- 所有测试文件、临时文件必须放在 `temp/` 目录下
- 不污染项目根目录
### 3. 代码风格
**导入顺序**
```python
# 1. 标准库
import sys
from pathlib import Path
# 2. 第三方库
import yaml
from pptx import Presentation
# 3. 本地模块
from utils import log_info
from core.elements import TextElement
```
**文档字符串**
- 每个模块必须有模块级文档字符串
- 每个类和函数必须有文档字符串
- 使用中文编写注释和文档
**命名规范**
- 模块名:小写 + 下划线(如 `yaml_loader.py`
- 类名:大驼峰(如 `TextElement`
- 函数名:小写 + 下划线(如 `load_yaml_file()`
- 常量:大写 + 下划线(如 `HTML_TEMPLATE`
## 技术决策
### 1. 元素抽象层使用 dataclass
**决策**:使用 Python dataclass 定义元素数据类
**理由**
- 简洁性:自动生成 `__init__``__repr__` 等方法
- 类型提示支持类型注解IDE 友好
- 验证时机:`__post_init__` 在创建时自动调用
- 可扩展性:未来可以添加方法
**示例**
```python
@dataclass
class TextElement:
type: str = 'text'
content: str = ''
box: list = field(default_factory=lambda: [1, 1, 8, 1])
font: dict = field(default_factory=dict)
def __post_init__(self):
if len(self.box) != 4:
raise ValueError("box 必须包含 4 个数字")
```
### 2. 渲染器内置在生成器中
**决策**:将渲染逻辑内置在 PptxGenerator 和 HtmlRenderer 类中
**理由**
- 封装性:渲染逻辑与生成器紧密相关
- 简单性:不需要额外的渲染器接口
- 性能:避免额外的方法调用开销
**示例**
```python
class PptxGenerator:
def _render_element(self, slide, elem, base_path):
if isinstance(elem, TextElement):
self._render_text(slide, elem)
elif isinstance(elem, ImageElement):
self._render_image(slide, elem, base_path)
```
### 3. 创建时验证
**决策**:在元素对象创建时进行验证(`__post_init__` 方法)
**理由**
- 尽早失败:在数据进入系统时就发现错误
- 清晰的错误位置:堆栈指向元素创建处
- 避免无效状态:确保元素对象始终有效
### 4. 元素工厂函数
**决策**:提供 `create_element(elem_dict)` 工厂函数
**理由**
- 统一入口:所有元素创建都通过工厂函数
- 类型安全:进行类型检查
- 易于扩展:添加新元素类型只需添加一个分支
## 扩展指南
### 添加新元素类型
假设要添加 `VideoElement`
**1. 在 core/elements.py 中定义数据类**
```python
@dataclass
class VideoElement:
type: str = 'video'
src: str = ''
box: list = field(default_factory=lambda: [1, 1, 4, 3])
autoplay: bool = False
def __post_init__(self):
if not self.src:
raise ValueError("视频元素必须指定 src")
if len(self.box) != 4:
raise ValueError("box 必须包含 4 个数字")
```
**2. 在工厂函数中添加分支**
```python
def create_element(elem_dict: dict):
elem_type = elem_dict.get('type')
# ... 其他类型 ...
elif elem_type == 'video':
return VideoElement(**elem_dict)
```
**3. 在 PptxGenerator 中实现渲染方法**
```python
def _render_element(self, slide, elem, base_path):
# ... 其他类型 ...
elif isinstance(elem, VideoElement):
self._render_video(slide, elem, base_path)
def _render_video(self, slide, elem: VideoElement, base_path):
# 实现视频渲染逻辑
pass
```
**4. 在 HtmlRenderer 中实现渲染方法**
```python
def render_slide(self, slide_data, index, base_path):
# ... 其他类型 ...
elif isinstance(elem, VideoElement):
elements_html += self.render_video(elem, base_path)
def render_video(self, elem: VideoElement, base_path):
# 实现 HTML 视频渲染
return f'<video src="{elem.src}" ...></video>'
```
### 添加新渲染器
假设要添加 PDF 渲染器:
**1. 创建 renderers/pdf_renderer.py**
```python
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
class PdfRenderer:
def __init__(self):
# 初始化 PDF 库
pass
def add_slide(self, slide_data, base_path=None):
# 添加页面
pass
def _render_element(self, page, elem, base_path):
if isinstance(elem, TextElement):
self._render_text(page, elem)
# ... 其他元素类型
```
**2. 在 yaml2pptx.py 中添加 PDF 模式**
```python
from renderers.pdf_renderer import PdfRenderer
def main():
# ... 解析参数 ...
if args.pdf:
# PDF 生成模式
generator = PdfRenderer()
# ... 渲染逻辑
```
## 测试规范
### 运行测试
```bash
# 基本 PPTX 生成
uv run yaml2pptx.py temp/test.yaml temp/output.pptx
# 使用模板
uv run yaml2pptx.py temp/demo.yaml temp/output.pptx --template-dir temp/templates
# 预览模式
uv run yaml2pptx.py temp/test.yaml --preview
# 指定端口
uv run yaml2pptx.py temp/test.yaml --preview --port 8080
```
### 测试文件位置
所有测试文件必须放在 `temp/` 目录下:
- `temp/*.yaml` - 测试用的 YAML 文件
- `temp/*.pptx` - 生成的 PPTX 文件
- `temp/templates/` - 测试用的模板文件
## 常见问题
### Q: 为什么不能直接使用 python 运行脚本?
A: 项目使用 uv 的 Inline script metadata 来管理依赖。直接使用 python 会导致依赖缺失。必须使用 `uv run yaml2pptx.py`
### Q: 如何添加新的依赖?
A: 在 `yaml2pptx.py``/// script` 头部添加:
```python
# /// script
# requires-python = ">=3.8"
# dependencies = [
# "python-pptx",
# "pyyaml",
# "flask",
# "watchdog",
# "new-dependency", # 添加新依赖
# ]
# ///
```
### Q: 为什么元素使用 dataclass 而不是普通字典?
A: dataclass 提供:
1. 类型安全和 IDE 支持
2. 自动生成的方法(`__init__`, `__repr__`
3. 创建时验证(`__post_init__`
4. 更好的可维护性和可扩展性
### Q: 如何调试渲染问题?
A: 使用预览模式:
```bash
uv run yaml2pptx.py temp/test.yaml --preview
```
在浏览器中查看渲染结果,支持热重载。
## 项目约束
1. **面向中文开发者**:注释、文档、错误消息使用中文
2. **使用 uv 运行**:严禁直接使用主机环境的 python
3. **测试文件隔离**:所有测试文件放在 `temp/` 目录
4. **不污染主机环境**:不修改主机的 Python 配置
## 维护指南
### 代码审查要点
- [ ] 模块文件大小合理150-300 行)
- [ ] 无循环依赖
- [ ] 所有类和函数有文档字符串
- [ ] 使用中文注释
- [ ] 元素验证在 `__post_init__` 中完成
- [ ] 导入语句按标准库、第三方库、本地模块排序
- [ ] 测试文件在 `temp/` 目录下
### 性能优化建议
1. **模板缓存**Presentation 类已实现模板缓存
2. **元素验证**:只在创建时验证一次,渲染时不再验证
3. **文件监听**:预览模式使用 watchdog 高效监听文件变化