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,252 @@
## Context
yaml2pptx.py 当前是一个 1,245 行的单文件脚本,包含了从 YAML 解析到 PPTX 生成的完整流程。虽然功能完整,但代码组织存在以下问题:
- **可读性差**:单文件过大,开发者和 LLM 工具难以快速理解代码结构
- **维护困难**:修改一个功能可能需要在文件中跳转多次,容易引入错误
- **扩展性受限**添加新元素类型需要在多处添加代码验证、PPTX 渲染、HTML 渲染)
- **测试不便**:无法对单个模块进行独立测试
当前约束:
- 必须保持 `uv run yaml2pptx.py` 的单入口使用方式
- 所有依赖通过 `/// script` 头部声明
- 不能污染主机环境的 Python 配置
## Goals / Non-Goals
**Goals:**
- 将代码拆分为职责清晰的模块,每个文件控制在 150-300 行
- 引入元素抽象层,使添加新元素类型只需实现数据类和渲染方法
- 建立清晰的依赖关系,避免循环依赖
- 保持向后兼容,用户使用方式不变
- 为未来扩展(如添加 PDF 渲染器、Video 元素)奠定基础
**Non-Goals:**
- 不改变现有功能的行为(纯重构,不添加新功能)
- 不改变 YAML 文件格式或 CLI 参数
- 不迁移到标准 Python 包(保持单文件入口模式)
- 不添加单元测试(可以在后续单独添加)
## Decisions
### 1. 四层架构设计
**决策**:采用 core/loaders/renderers/preview 四层架构
```
html2pptx/
├── yaml2pptx.py # 入口层CLI + main
├── core/ # 核心层:领域模型
│ ├── elements.py
│ ├── template.py
│ └── presentation.py
├── loaders/ # 加载层:数据输入
│ └── yaml_loader.py
├── renderers/ # 渲染层:数据输出
│ ├── pptx_renderer.py
│ └── html_renderer.py
├── preview/ # 预览层:可选功能
│ └── server.py
└── utils.py # 工具层:通用函数
```
**理由**
- **职责分离**:每层有明确的职责,修改影响范围可控
- **依赖方向清晰**:入口 → 渲染 → 核心 ← 加载,避免循环依赖
- **易于扩展**:添加新渲染器(如 PDF只需在 renderers/ 下新增文件
**替代方案**
- 扁平结构(所有文件在同一目录):简单但缺乏层次,未来扩展会混乱
- 更细粒度的分层(如 domain/application/infrastructure过度设计不适合当前规模
### 2. 元素抽象层使用 dataclass
**决策**:使用 Python dataclass 定义元素数据类
```python
from dataclasses import dataclass, field
@dataclass
class TextElement:
type: str = 'text'
content: str = ''
box: list[float] = 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 个数字")
```
**理由**
- **简洁性**dataclass 自动生成 `__init__``__repr__` 等方法,减少样板代码
- **类型提示**支持类型注解IDE 和 LLM 工具可以提供更好的补全
- **验证时机**`__post_init__` 在对象创建时自动调用,尽早发现错误
- **可扩展性**:未来可以添加方法(如 `to_dict()``validate_deep()`
**替代方案**
- TypedDict运行时是字典缺少验证能力
- 普通类:需要手写 `__init__`,代码冗长
### 3. 渲染器内置在生成器中
**决策**:将渲染逻辑内置在 PptxGenerator 和 HtmlRenderer 类中
```python
class PptxGenerator:
def add_slide(self, slide_data, base_path=None):
# 渲染元素
for elem in slide_data['elements']:
self._render_element(slide, elem, base_path)
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)
# ...
```
**理由**
- **封装性**:渲染逻辑与生成器紧密相关,内置更自然
- **简单性**:不需要额外的渲染器接口或依赖注入
- **性能**:避免额外的方法调用开销
**替代方案**
- 渲染器作为参数传入:增加复杂度,当前不需要运行时切换渲染器
- 统一渲染接口过度抽象PPTX 和 HTML 渲染差异较大
### 4. 创建时验证
**决策**:在元素对象创建时进行验证(`__post_init__` 方法)
**理由**
- **尽早失败**:在数据进入系统时就发现错误,而不是等到渲染时
- **清晰的错误位置**:验证失败时,堆栈指向元素创建处,易于定位
- **避免无效状态**:确保元素对象始终处于有效状态
**替代方案**
- 加载时验证:验证逻辑分散在 yaml_loader 中,难以维护
- 渲染时验证:错误发现太晚,可能已经生成了部分幻灯片
### 5. 元素工厂函数
**决策**:提供 `create_element(elem_dict)` 工厂函数,从字典创建元素对象
```python
def create_element(elem_dict: dict):
elem_type = elem_dict.get('type')
if elem_type == 'text':
return TextElement(**elem_dict)
elif elem_type == 'image':
return ImageElement(**elem_dict)
# ...
```
**理由**
- **统一入口**:所有元素创建都通过工厂函数,便于添加通用逻辑(如日志、缓存)
- **类型安全**:工厂函数可以进行类型检查,避免创建错误的元素类型
- **易于扩展**:添加新元素类型只需在工厂函数中添加一个分支
### 6. 依赖管理策略
**决策**:所有依赖保留在 yaml2pptx.py 的 `/// script` 头部,预览依赖不再可选
```python
# /// script
# requires-python = ">=3.8"
# dependencies = [
# "python-pptx",
# "pyyaml",
# "flask",
# "watchdog",
# ]
# ///
```
**理由**
- **简化处理**:不需要条件导入或可选依赖检查
- **一致性**:所有用户都有相同的依赖环境
- **依赖轻量**flask 和 watchdog 都是轻量级库,不会显著增加安装时间
## Risks / Trade-offs
### 风险 1导入路径变化可能引入错误
**风险**:重构后内部导入路径会发生变化,可能遗漏某些导入或引入循环依赖
**缓解措施**
- 按模块逐步迁移,每迁移一个模块就测试一次
- 使用 Python 的 `import` 语句检查(运行脚本时会立即发现导入错误)
- 保持清晰的依赖方向:入口 → 渲染 → 核心 ← 加载
### 风险 2元素验证可能过于严格
**风险**:在 `__post_init__` 中进行验证可能拒绝某些边缘情况的有效输入
**缓解措施**
- 只验证关键约束(如 box 必须有 4 个元素),不验证业务逻辑
- 提供清晰的错误消息,帮助用户快速定位问题
- 如果发现验证过严,可以在后续放宽
### 风险 3文件数量增加可能影响开发体验
**权衡**:从 1 个文件变成 8+ 个文件,开发者需要在多个文件间跳转
**缓解措施**
- 每个文件职责清晰,命名直观(如 `elements.py``pptx_renderer.py`
- 文件大小适中150-300 行),易于在单个屏幕内浏览
- 提供清晰的模块文档,说明每个文件的职责
### 风险 4重构可能引入功能回归
**风险**:代码重组过程中可能无意中改变了某些行为
**缓解措施**
- 保持函数逻辑不变,只改变组织方式
- 重构后运行完整的功能测试(手动测试所有 YAML 示例)
- 对比重构前后生成的 PPTX 文件,确保输出一致
## Migration Plan
### 阶段 1基础设施utils + loaders
1. 创建目录结构
2. 提取 `utils.py`(日志函数、颜色转换)
3. 提取 `loaders/yaml_loader.py`YAML 加载和验证)
4. 更新 yaml2pptx.py 的导入,测试 YAML 加载功能
### 阶段 2核心抽象core
1. 实现 `core/elements.py`(元素数据类 + 工厂函数)
2. 提取 `core/template.py`Template 类)
3. 提取 `core/presentation.py`Presentation 类)
4. 测试模板渲染功能
### 阶段 3渲染器renderers
1. 实现 `renderers/pptx_renderer.py`PptxGenerator + PPTX 渲染)
2. 实现 `renderers/html_renderer.py`HtmlRenderer
3. 测试 PPTX 生成和 HTML 预览功能
### 阶段 4预览功能preview
1. 提取 `preview/server.py`Flask 服务器 + 文件监听)
2. 测试预览服务器功能
### 阶段 5入口整合
1. 重构 yaml2pptx.py只保留 CLI + main
2. 运行完整的端到端测试
3. 对比重构前后的输出,确保一致性
### 回滚策略
- 保留原始 yaml2pptx.py 的备份(如 yaml2pptx.py.backup
- 如果发现严重问题,可以快速回滚到原始版本
- 使用 git 分支进行重构,确保可以随时回退
## Open Questions
无待解决问题。设计方案已在 explore mode 中与用户充分讨论并确认。