1
0
Files
PPTX/openspec/changes/archive/2026-03-02-refactor-yaml2pptx-modular/design.md
lanyuanxiaoyao ed940f0690 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)
2026-03-02 16:43:45 +08:00

8.8 KiB
Raw Blame History

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 定义元素数据类

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 类中

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) 工厂函数,从字典创建元素对象

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 头部,预览依赖不再可选

# /// 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.pypptx_renderer.py
  • 文件大小适中150-300 行),易于在单个屏幕内浏览
  • 提供清晰的模块文档,说明每个文件的职责

风险 4重构可能引入功能回归

风险:代码重组过程中可能无意中改变了某些行为

缓解措施

  • 保持函数逻辑不变,只改变组织方式
  • 重构后运行完整的功能测试(手动测试所有 YAML 示例)
  • 对比重构前后生成的 PPTX 文件,确保输出一致

Migration Plan

阶段 1基础设施utils + loaders

  1. 创建目录结构
  2. 提取 utils.py(日志函数、颜色转换)
  3. 提取 loaders/yaml_loader.pyYAML 加载和验证)
  4. 更新 yaml2pptx.py 的导入,测试 YAML 加载功能

阶段 2核心抽象core

  1. 实现 core/elements.py(元素数据类 + 工厂函数)
  2. 提取 core/template.pyTemplate 类)
  3. 提取 core/presentation.pyPresentation 类)
  4. 测试模板渲染功能

阶段 3渲染器renderers

  1. 实现 renderers/pptx_renderer.pyPptxGenerator + PPTX 渲染)
  2. 实现 renderers/html_renderer.pyHtmlRenderer
  3. 测试 PPTX 生成和 HTML 预览功能

阶段 4预览功能preview

  1. 提取 preview/server.pyFlask 服务器 + 文件监听)
  2. 测试预览服务器功能

阶段 5入口整合

  1. 重构 yaml2pptx.py只保留 CLI + main
  2. 运行完整的端到端测试
  3. 对比重构前后的输出,确保一致性

回滚策略

  • 保留原始 yaml2pptx.py 的备份(如 yaml2pptx.py.backup
  • 如果发现严重问题,可以快速回滚到原始版本
  • 使用 git 分支进行重构,确保可以随时回退

Open Questions

无待解决问题。设计方案已在 explore mode 中与用户充分讨论并确认。