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,2 @@
schema: spec-driven
created: 2026-03-02

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 中与用户充分讨论并确认。

View File

@@ -0,0 +1,52 @@
## Why
yaml2pptx.py 脚本已增长到 1,245 行,包含了 YAML 解析、模板系统、元素渲染、PPTX 生成、HTML 预览等多个功能模块。单文件结构导致代码难以阅读、维护和扩展,对开发者和 LLM 工具都不友好。需要将其重构为模块化结构,同时引入元素抽象层,为未来添加新元素类型和渲染器奠定基础。
## What Changes
- 将 yaml2pptx.py 拆分为多个功能模块文件,按 core/loaders/renderers/preview 四层架构组织
- 引入元素抽象层,使用 dataclass 定义元素数据类TextElement, ImageElement, ShapeElement, TableElement
- 在元素创建时进行验证(`__post_init__` 方法)
- 重构 PPTX 生成器,将渲染器内置在 PptxGenerator 类中
- 重构 HTML 渲染器,作为独立的 HtmlRenderer 类用于预览功能
- 保持 yaml2pptx.py 作为单一入口点,所有依赖声明保留在入口脚本的 `/// script` 头部
- 保持向后兼容:`uv run yaml2pptx.py` 的使用方式不变
## Capabilities
### New Capabilities
- `modular-architecture`: 模块化代码架构,将单文件脚本拆分为 core核心领域模型、loaders加载器、renderers渲染器、preview预览服务四层结构每个模块职责清晰文件大小控制在 150-300 行
- `element-abstraction`: 元素抽象层,定义统一的元素接口和数据类,支持元素验证和未来扩展新元素类型
### Modified Capabilities
- `element-rendering`: 从函数式渲染改为基于元素数据类的面向对象渲染,引入元素抽象层和创建时验证
- `pptx-generation`: 重构为内置渲染器的架构PptxGenerator 类内部包含 PPTX 渲染逻辑,通过元素类型分发到对应的渲染方法
- `html-rendering`: 从内联函数重构为独立的 HtmlRenderer 类,与 PptxRenderer 共享元素抽象层
## Impact
**代码结构**
- yaml2pptx.py从 1,245 行缩减为约 100 行(仅保留 CLI 和 main 函数)
- 新增文件:
- core/elements.py元素数据类
- core/template.pyTemplate 类)
- core/presentation.pyPresentation 类)
- loaders/yaml_loader.pyYAML 加载和验证)
- renderers/pptx_renderer.pyPptxGenerator
- renderers/html_renderer.pyHtmlRenderer
- preview/server.pyFlask 服务器和文件监听)
- utils.py工具函数
**导入路径**
- 内部导入路径会发生变化(如 `from core.elements import TextElement`
- 外部使用方式不变(`uv run yaml2pptx.py input.yaml output.pptx`
**依赖管理**
- 所有依赖保持在 yaml2pptx.py 的 `/// script` 头部
- 预览功能的依赖flask, watchdog不再可选统一在入口声明
**测试和验证**
- 需要验证所有现有功能在重构后仍正常工作
- 需要测试 YAML 解析、模板渲染、PPTX 生成、HTML 预览等完整流程

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

View File

@@ -0,0 +1,60 @@
## MODIFIED Requirements
### Requirement: 系统必须使用元素数据类进行渲染
系统 SHALL 使用元素数据类TextElement, ImageElement, ShapeElement, TableElement进行渲染而不是直接处理字典数据。渲染器接收元素对象通过类型检查分发到对应的渲染方法。
#### Scenario: 渲染器接收元素对象
- **WHEN** 渲染器的 `_render_element()` 方法被调用
- **THEN** 系统应接收元素对象(如 TextElement 实例),而不是字典
#### Scenario: 通过类型检查分发渲染
- **WHEN** 渲染器处理元素对象
- **THEN** 系统应使用 `isinstance()` 检查元素类型,分发到对应的渲染方法(如 `_render_text()`, `_render_image()`
#### Scenario: 元素验证在创建时完成
- **WHEN** 元素对象被创建
- **THEN** 系统应在 `__post_init__` 方法中完成验证,渲染时不再需要验证
### Requirement: 渲染器必须内置在生成器中
系统 SHALL 将渲染逻辑内置在 PptxGenerator 类中,作为私有方法(`_render_text()`, `_render_image()` 等),而不是独立的函数。
#### Scenario: 渲染方法作为生成器的私有方法
- **WHEN** 开发者查看 PptxGenerator 类
- **THEN** 应包含 `_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()` 等私有方法
#### Scenario: 渲染方法接收元素对象
- **WHEN** 调用 `_render_text(slide, elem)` 方法
- **THEN** `elem` 参数应为 TextElement 对象,包含 content、box、font 等属性
## ADDED Requirements
### Requirement: 元素渲染必须支持元素对象的属性访问
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`, `elem.font`)访问数据,而不是通过字典的 `get()` 方法。
#### Scenario: 访问文本元素的属性
- **WHEN** 渲染文本元素
- **THEN** 系统应使用 `elem.content` 而不是 `elem.get('content', '')`
#### Scenario: 访问图片元素的属性
- **WHEN** 渲染图片元素
- **THEN** 系统应使用 `elem.src``elem.box` 而不是 `elem.get('src')``elem.get('box')`
#### Scenario: 访问形状元素的属性
- **WHEN** 渲染形状元素
- **THEN** 系统应使用 `elem.shape`, `elem.fill`, `elem.line` 等属性
#### Scenario: 访问表格元素的属性
- **WHEN** 渲染表格元素
- **THEN** 系统应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性

View File

@@ -0,0 +1,89 @@
## MODIFIED Requirements
### Requirement: HTML 渲染必须使用独立的 HtmlRenderer 类
系统 SHALL 将 HTML 渲染逻辑重构为独立的 HtmlRenderer 类,而不是内联函数。
#### Scenario: HtmlRenderer 类定义在独立模块
- **WHEN** 开发者查看项目结构
- **THEN** HtmlRenderer 类应定义在 `renderers/html_renderer.py` 文件中
#### Scenario: HtmlRenderer 包含渲染方法
- **WHEN** 开发者查看 HtmlRenderer 类
- **THEN** 应包含 `render_slide()`, `render_text()`, `render_image()`, `render_shape()`, `render_table()` 等方法
#### Scenario: 预览服务器使用 HtmlRenderer
- **WHEN** 预览服务器生成 HTML
- **THEN** 应创建 HtmlRenderer 实例并调用其渲染方法
### Requirement: HTML 渲染必须接收元素对象
系统 SHALL 使 HtmlRenderer 的渲染方法接收元素对象TextElement, ImageElement 等),而不是字典。
#### Scenario: render_text 接收 TextElement 对象
- **WHEN** 调用 `renderer.render_text(elem)`
- **THEN** `elem` 参数应为 TextElement 对象
#### Scenario: render_image 接收 ImageElement 对象
- **WHEN** 调用 `renderer.render_image(elem, base_path)`
- **THEN** `elem` 参数应为 ImageElement 对象
#### Scenario: render_shape 接收 ShapeElement 对象
- **WHEN** 调用 `renderer.render_shape(elem)`
- **THEN** `elem` 参数应为 ShapeElement 对象
#### Scenario: render_table 接收 TableElement 对象
- **WHEN** 调用 `renderer.render_table(elem)`
- **THEN** `elem` 参数应为 TableElement 对象
## ADDED Requirements
### Requirement: HTML 渲染必须通过元素属性访问数据
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`)访问数据,而不是通过字典的 `get()` 方法。
#### Scenario: 访问文本元素属性
- **WHEN** 渲染文本元素
- **THEN** 应使用 `elem.content`, `elem.box`, `elem.font` 等属性
#### Scenario: 访问图片元素属性
- **WHEN** 渲染图片元素
- **THEN** 应使用 `elem.src`, `elem.box` 等属性
#### Scenario: 访问形状元素属性
- **WHEN** 渲染形状元素
- **THEN** 应使用 `elem.shape`, `elem.box`, `elem.fill`, `elem.line` 等属性
#### Scenario: 访问表格元素属性
- **WHEN** 渲染表格元素
- **THEN** 应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性
### Requirement: HtmlRenderer 必须与 PptxRenderer 共享元素抽象层
系统 SHALL 使 HtmlRenderer 和 PptxRenderer 都基于相同的元素数据类(定义在 core/elements.py确保一致性。
#### Scenario: 两个渲染器使用相同的元素类型
- **WHEN** 开发者查看 HtmlRenderer 和 PptxRenderer
- **THEN** 两者都应导入并使用 `from core.elements import TextElement, ImageElement, ShapeElement, TableElement`
#### Scenario: 元素验证在创建时完成
- **WHEN** 元素对象传递给 HtmlRenderer
- **THEN** 元素已经在创建时完成验证HtmlRenderer 不需要再次验证
#### Scenario: 添加新元素类型时两个渲染器同步更新
- **WHEN** 在 core/elements.py 中添加新元素类型(如 VideoElement
- **THEN** 需要在 HtmlRenderer 和 PptxRenderer 中都实现对应的渲染方法

View File

@@ -0,0 +1,99 @@
## ADDED Requirements
### Requirement: 代码必须按功能模块组织
系统必须将代码按照功能职责拆分为独立的模块文件,每个模块文件的代码行数应控制在 150-300 行之间,确保代码易于阅读和维护。
#### Scenario: 单文件脚本拆分为多个模块
- **WHEN** 开发者查看项目结构
- **THEN** 系统应包含以下模块文件:
- `yaml2pptx.py`(入口脚本,约 100 行)
- `core/elements.py`(元素数据类)
- `core/template.py`(模板系统)
- `core/presentation.py`(演示文稿类)
- `loaders/yaml_loader.py`YAML 加载)
- `renderers/pptx_renderer.py`PPTX 渲染器)
- `renderers/html_renderer.py`HTML 渲染器)
- `preview/server.py`(预览服务器)
- `utils.py`(工具函数)
#### Scenario: 每个模块文件大小适中
- **WHEN** 开发者打开任意模块文件
- **THEN** 文件代码行数应在 150-300 行之间(入口脚本除外,约 100 行)
### Requirement: 模块必须按层次组织
系统必须采用四层架构组织代码入口层yaml2pptx.py、核心层core/、加载层loaders/、渲染层renderers/、预览层preview/),每层职责清晰。
#### Scenario: 核心层包含领域模型
- **WHEN** 开发者查看 core/ 目录
- **THEN** 应包含元素数据类elements.py、模板系统template.py、演示文稿类presentation.py
#### Scenario: 加载层负责数据输入
- **WHEN** 开发者查看 loaders/ 目录
- **THEN** 应包含 YAML 加载和验证逻辑yaml_loader.py
#### Scenario: 渲染层负责数据输出
- **WHEN** 开发者查看 renderers/ 目录
- **THEN** 应包含 PPTX 渲染器pptx_renderer.py和 HTML 渲染器html_renderer.py
#### Scenario: 预览层提供可选功能
- **WHEN** 开发者查看 preview/ 目录
- **THEN** 应包含 Flask 服务器和文件监听逻辑server.py
### Requirement: 模块间依赖关系必须清晰
系统必须保持清晰的依赖方向:入口层 → 渲染层 → 核心层 ← 加载层,避免循环依赖。
#### Scenario: 依赖方向单向流动
- **WHEN** 开发者分析模块导入关系
- **THEN** 依赖关系应为:
- yaml2pptx.py 导入 renderers、loaders、core、preview
- renderers 导入 core
- loaders 导入 core
- core 不导入其他业务模块(只导入标准库和第三方库)
- preview 导入 renderers 和 core
#### Scenario: 不存在循环依赖
- **WHEN** 开发者运行脚本
- **THEN** 系统不应出现循环导入错误
### Requirement: 入口脚本必须保持单一职责
yaml2pptx.py 必须仅包含 CLI 参数解析和主流程编排,所有业务逻辑应委托给相应的模块。
#### Scenario: 入口脚本只负责 CLI 和流程编排
- **WHEN** 开发者查看 yaml2pptx.py
- **THEN** 文件应仅包含:
- `/// script` 依赖声明
- `parse_args()` 函数CLI 参数解析)
- `main()` 函数(流程编排)
- 必要的导入语句
#### Scenario: 入口脚本代码行数精简
- **WHEN** 开发者统计 yaml2pptx.py 的代码行数
- **THEN** 应约为 100 行(不包括注释和空行)
### Requirement: 保持向后兼容的使用方式
系统必须保持 `uv run yaml2pptx.py` 的使用方式不变,用户无需修改现有的调用方式。
#### Scenario: CLI 使用方式不变
- **WHEN** 用户运行 `uv run yaml2pptx.py input.yaml output.pptx`
- **THEN** 系统应正常生成 PPTX 文件,行为与重构前一致
#### Scenario: CLI 参数保持兼容
- **WHEN** 用户使用 `--template-dir``--preview``--port` 等参数
- **THEN** 系统应正确解析并执行相应功能

View File

@@ -0,0 +1,84 @@
## MODIFIED Requirements
### Requirement: PptxGenerator 必须内置渲染器
系统 SHALL 将 PptxGenerator 类重构为内置渲染器的架构,渲染逻辑作为类的私有方法,而不是独立的函数。
#### Scenario: PptxGenerator 包含渲染方法
- **WHEN** 开发者查看 PptxGenerator 类
- **THEN** 应包含 `_render_element()`, `_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()` 等私有方法
#### Scenario: add_slide 方法调用内置渲染器
- **WHEN** 调用 `generator.add_slide(slide_data, base_path)`
- **THEN** 系统应在方法内部调用 `_render_element()` 来渲染每个元素
#### Scenario: 渲染方法接收元素对象
- **WHEN** 渲染方法被调用
- **THEN** 应接收元素对象(如 TextElement, ImageElement而不是字典
### Requirement: PptxGenerator 必须位于 renderers 模块
系统 SHALL 将 PptxGenerator 类从主脚本提取到 `renderers/pptx_renderer.py` 模块中。
#### Scenario: PptxGenerator 在独立模块中
- **WHEN** 开发者查看项目结构
- **THEN** PptxGenerator 类应定义在 `renderers/pptx_renderer.py` 文件中
#### Scenario: 主脚本导入 PptxGenerator
- **WHEN** yaml2pptx.py 需要使用 PptxGenerator
- **THEN** 应通过 `from renderers.pptx_renderer import PptxGenerator` 导入
## ADDED Requirements
### Requirement: 渲染器必须通过类型检查分发元素
系统 SHALL 在 `_render_element()` 方法中使用 `isinstance()` 检查元素类型,分发到对应的渲染方法。
#### Scenario: 分发文本元素
- **WHEN** `_render_element()` 接收到 TextElement 对象
- **THEN** 系统应调用 `_render_text(slide, elem)`
#### Scenario: 分发图片元素
- **WHEN** `_render_element()` 接收到 ImageElement 对象
- **THEN** 系统应调用 `_render_image(slide, elem, base_path)`
#### Scenario: 分发形状元素
- **WHEN** `_render_element()` 接收到 ShapeElement 对象
- **THEN** 系统应调用 `_render_shape(slide, elem)`
#### Scenario: 分发表格元素
- **WHEN** `_render_element()` 接收到 TableElement 对象
- **THEN** 系统应调用 `_render_table(slide, elem)`
### Requirement: 渲染方法必须访问元素对象的属性
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`)访问数据,而不是通过字典的 `get()` 方法。
#### Scenario: 访问文本元素属性
- **WHEN** `_render_text()` 渲染文本元素
- **THEN** 应使用 `elem.content`, `elem.box`, `elem.font` 等属性
#### Scenario: 访问图片元素属性
- **WHEN** `_render_image()` 渲染图片元素
- **THEN** 应使用 `elem.src`, `elem.box` 等属性
#### Scenario: 访问形状元素属性
- **WHEN** `_render_shape()` 渲染形状元素
- **THEN** 应使用 `elem.shape`, `elem.box`, `elem.fill`, `elem.line` 等属性
#### Scenario: 访问表格元素属性
- **WHEN** `_render_table()` 渲染表格元素
- **THEN** 应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性

View File

@@ -0,0 +1,99 @@
## 1. 准备工作
- [ ] 1.1 备份原始 yaml2pptx.py 文件为 yaml2pptx.py.backup
- [x] 1.2 创建目录结构core/, loaders/, renderers/, preview/
- [x] 1.3 在各目录下创建 __init__.py 文件
## 2. 基础设施层utils + loaders
- [x] 2.1 创建 utils.py提取日志函数log_info, log_success, log_error, log_progress
- [x] 2.2 在 utils.py 中提取颜色转换函数hex_to_rgb, validate_color
- [x] 2.3 创建 loaders/yaml_loader.py提取 YAMLError 异常类
- [x] 2.4 在 loaders/yaml_loader.py 中提取 load_yaml_file 函数
- [x] 2.5 在 loaders/yaml_loader.py 中提取验证函数validate_presentation_yaml, validate_template_yaml
- [x] 2.6 更新 yaml2pptx.py 的导入语句,导入 utils 和 loaders 模块
- [x] 2.7 测试 YAML 加载功能(运行 `uv run yaml2pptx.py --help` 确认无导入错误)
## 3. 核心抽象层core/elements
- [x] 3.1 创建 core/elements.py定义 TextElement dataclass包含 type, content, box, font 字段)
- [x] 3.2 在 TextElement 中实现 __post_init__ 验证方法(验证 box 长度)
- [x] 3.3 定义 ImageElement dataclass包含 type, src, box 字段及验证)
- [x] 3.4 定义 ShapeElement dataclass包含 type, shape, box, fill, line 字段及验证)
- [x] 3.5 定义 TableElement dataclass包含 type, data, position, col_widths, style 字段及验证)
- [x] 3.6 实现 create_element 工厂函数,根据 type 字段创建对应的元素对象
- [x] 3.7 测试元素创建和验证(创建测试用例验证各元素类型)
## 4. 核心抽象层core/template + presentation
- [x] 4.1 创建 core/template.py提取 Template 类
- [x] 4.2 更新 Template 类的导入,使用 loaders.yaml_loader 和 utils
- [x] 4.3 创建 core/presentation.py提取 Presentation 类
- [x] 4.4 更新 Presentation 类的导入,使用 loaders.yaml_loader 和 core.template
- [x] 4.5 修改 Presentation.render_slide 方法,使元素通过 create_element 转换为元素对象
- [ ] 4.6 测试模板渲染功能(使用现有的 YAML 文件测试)
## 5. PPTX 渲染器
- [x] 5.1 创建 renderers/pptx_renderer.py定义 PptxGenerator 类
- [x] 5.2 在 PptxGenerator.__init__ 中实现幻灯片尺寸设置逻辑
- [x] 5.3 实现 PptxGenerator.add_slide 方法(创建幻灯片、设置背景、调用 _render_element
- [x] 5.4 实现 _render_element 方法(使用 isinstance 检查元素类型并分发)
- [x] 5.5 实现 _render_text 方法(从原 add_text_element 迁移,使用元素对象属性)
- [x] 5.6 实现 _render_image 方法(从原 add_image_element 迁移,使用元素对象属性)
- [x] 5.7 实现 _render_shape 方法(从原 add_shape_element 迁移,使用元素对象属性)
- [x] 5.8 实现 _render_table 方法(从原 add_table_element 迁移,使用元素对象属性)
- [x] 5.9 实现 _render_background 方法(从原 set_slide_background 迁移)
- [x] 5.10 实现 PptxGenerator.save 方法
- [x] 5.11 更新 PptxGenerator 的导入,使用 core.elements 和 utils
- [x] 5.12 测试 PPTX 生成功能(生成测试 PPTX 文件并验证)
## 6. HTML 渲染器
- [x] 6.1 创建 renderers/html_renderer.py定义 HtmlRenderer 类
- [x] 6.2 实现 HtmlRenderer.render_slide 方法(生成幻灯片 HTML 容器)
- [x] 6.3 实现 render_text 方法(从原 render_text_element_to_html 迁移,使用元素对象属性)
- [x] 6.4 实现 render_image 方法(从原 render_image_element_to_html 迁移,使用元素对象属性)
- [x] 6.5 实现 render_shape 方法(从原 render_shape_element_to_html 迁移,使用元素对象属性)
- [x] 6.6 实现 render_table 方法(从原 render_table_element_to_html 迁移,使用元素对象属性)
- [x] 6.7 更新 HtmlRenderer 的导入,使用 core.elements
- [ ] 6.8 测试 HTML 渲染功能(生成测试 HTML 并在浏览器中验证)
## 7. 预览服务器
- [x] 7.1 创建 preview/server.py提取 Flask 应用相关代码
- [x] 7.2 提取 HTML_TEMPLATE 和 ERROR_TEMPLATE 常量
- [x] 7.3 提取 YAMLChangeHandler 类
- [x] 7.4 提取 create_flask_app 函数
- [x] 7.5 提取 start_preview_server 函数
- [x] 7.6 实现 generate_preview_html 函数(使用 HtmlRenderer
- [x] 7.7 更新 preview/server.py 的导入,使用 core.presentation 和 renderers.html_renderer
- [ ] 7.8 测试预览服务器功能(启动预览服务器并验证热重载)
## 8. 入口脚本重构
- [x] 8.1 重构 yaml2pptx.py只保留 `/// script` 头部、parse_args 和 main 函数
- [x] 8.2 更新 main 函数的导入语句(导入所有需要的模块)
- [x] 8.3 更新 main 函数中的 Presentation 和 PptxGenerator 使用(使用新的模块路径)
- [x] 8.4 更新 main 函数中的预览模式调用(使用 preview.server
- [x] 8.5 删除 yaml2pptx.py 中已迁移的代码(保留约 100 行)
- [x] 8.6 验证 yaml2pptx.py 的代码行数(应约为 100 行)
## 9. 完整测试
- [x] 9.1 测试基本 PPTX 生成(`uv run yaml2pptx.py input.yaml output.pptx`
- [x] 9.2 测试自动输出文件名(`uv run yaml2pptx.py input.yaml`
- [x] 9.3 测试模板功能(使用带模板的 YAML 文件)
- [x] 9.4 测试所有元素类型text, image, shape, table
- [ ] 9.5 测试预览模式(`uv run yaml2pptx.py input.yaml --preview`
- [ ] 9.6 测试 --template-dir 参数
- [ ] 9.7 测试错误处理(无效的 YAML、缺失的图片等
- [x] 9.8 对比重构前后生成的 PPTX 文件(确保输出一致)
## 10. 清理和文档
- [x] 10.1 检查所有模块的导入语句,确保没有未使用的导入
- [x] 10.2 检查是否存在循环依赖(运行脚本验证)
- [x] 10.3 验证所有文件的代码行数(每个模块 150-300 行,入口约 100 行)
- [x] 10.4 添加模块级文档字符串(说明每个模块的职责)
- [x] 10.5 删除 yaml2pptx.py.backup 备份文件(如果测试通过)

View File

@@ -5,4 +5,6 @@ context: |
本项目编写的python脚本始终使用uv运行脚本使用Inline script metadata来指定脚本的依赖包和python运行版本
严禁直接使用主机环境的python直接执行脚本严禁在主机环境直接安装python依赖包
本项目编写的测试文件、临时文件必须放在temp目录下
严禁污染主机环境的任何配置,如有需要,必须请求用户审核操作;
严禁污染主机环境的任何配置,如有需要,必须请求用户审核操作;
当前项目的面向用户的使用文档在README.md
当前项目的面向AI和开发者的开发规范文档在README_DEV.md

View File

@@ -0,0 +1,120 @@
# Element Abstraction
## Purpose
定义元素抽象层的规格,使用 dataclass 实现类型安全的元素数据类,支持创建时验证和未来扩展。
## 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

View File

@@ -159,6 +159,63 @@ Element rendering系统负责将 YAML 中定义的各类元素(文本、图片
- **WHEN** 两个元素的位置有重叠
- **THEN** 后定义的元素遮盖先定义的元素
### Requirement: 系统必须使用元素数据类进行渲染
系统 SHALL 使用元素数据类TextElement, ImageElement, ShapeElement, TableElement进行渲染而不是直接处理字典数据。渲染器接收元素对象通过类型检查分发到对应的渲染方法。
#### Scenario: 渲染器接收元素对象
- **WHEN** 渲染器的 `_render_element()` 方法被调用
- **THEN** 系统应接收元素对象(如 TextElement 实例),而不是字典
#### Scenario: 通过类型检查分发渲染
- **WHEN** 渲染器处理元素对象
- **THEN** 系统应使用 `isinstance()` 检查元素类型,分发到对应的渲染方法(如 `_render_text()`, `_render_image()`
#### Scenario: 元素验证在创建时完成
- **WHEN** 元素对象被创建
- **THEN** 系统应在 `__post_init__` 方法中完成验证,渲染时不再需要验证
### Requirement: 渲染器必须内置在生成器中
系统 SHALL 将渲染逻辑内置在 PptxGenerator 类中,作为私有方法(`_render_text()`, `_render_image()` 等),而不是独立的函数。
#### Scenario: 渲染方法作为生成器的私有方法
- **WHEN** 开发者查看 PptxGenerator 类
- **THEN** 应包含 `_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()` 等私有方法
#### Scenario: 渲染方法接收元素对象
- **WHEN** 调用 `_render_text(slide, elem)` 方法
- **THEN** `elem` 参数应为 TextElement 对象,包含 content、box、font 等属性
### Requirement: 元素渲染必须支持元素对象的属性访问
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`, `elem.font`)访问数据,而不是通过字典的 `get()` 方法。
#### Scenario: 访问文本元素的属性
- **WHEN** 渲染文本元素
- **THEN** 系统应使用 `elem.content` 而不是 `elem.get('content', '')`
#### Scenario: 访问图片元素的属性
- **WHEN** 渲染图片元素
- **THEN** 系统应使用 `elem.src``elem.box` 而不是 `elem.get('src')``elem.get('box')`
#### Scenario: 访问形状元素的属性
- **WHEN** 渲染形状元素
- **THEN** 系统应使用 `elem.shape`, `elem.fill`, `elem.line` 等属性
#### Scenario: 访问表格元素的属性
- **WHEN** 渲染表格元素
- **THEN** 系统应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性
### Requirement: 系统必须验证元素类型
系统 SHALL 验证每个元素的 type 字段,仅支持已定义的元素类型。

View File

@@ -197,6 +197,92 @@ HTML Rendering 系统负责将 YAML 中定义的各类元素(文本、图片
- **WHEN** 渲染多个幻灯片
- **THEN** 每个幻灯片之间有 20px 的垂直间距
### Requirement: HTML 渲染必须使用独立的 HtmlRenderer 类
系统 SHALL 将 HTML 渲染逻辑重构为独立的 HtmlRenderer 类,而不是内联函数。
#### Scenario: HtmlRenderer 类定义在独立模块
- **WHEN** 开发者查看项目结构
- **THEN** HtmlRenderer 类应定义在 `renderers/html_renderer.py` 文件中
#### Scenario: HtmlRenderer 包含渲染方法
- **WHEN** 开发者查看 HtmlRenderer 类
- **THEN** 应包含 `render_slide()`, `render_text()`, `render_image()`, `render_shape()`, `render_table()` 等方法
#### Scenario: 预览服务器使用 HtmlRenderer
- **WHEN** 预览服务器生成 HTML
- **THEN** 应创建 HtmlRenderer 实例并调用其渲染方法
### Requirement: HTML 渲染必须接收元素对象
系统 SHALL 使 HtmlRenderer 的渲染方法接收元素对象TextElement, ImageElement 等),而不是字典。
#### Scenario: render_text 接收 TextElement 对象
- **WHEN** 调用 `renderer.render_text(elem)`
- **THEN** `elem` 参数应为 TextElement 对象
#### Scenario: render_image 接收 ImageElement 对象
- **WHEN** 调用 `renderer.render_image(elem, base_path)`
- **THEN** `elem` 参数应为 ImageElement 对象
#### Scenario: render_shape 接收 ShapeElement 对象
- **WHEN** 调用 `renderer.render_shape(elem)`
- **THEN** `elem` 参数应为 ShapeElement 对象
#### Scenario: render_table 接收 TableElement 对象
- **WHEN** 调用 `renderer.render_table(elem)`
- **THEN** `elem` 参数应为 TableElement 对象
### Requirement: HTML 渲染必须通过元素属性访问数据
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`)访问数据,而不是通过字典的 `get()` 方法。
#### Scenario: 访问文本元素属性
- **WHEN** 渲染文本元素
- **THEN** 应使用 `elem.content`, `elem.box`, `elem.font` 等属性
#### Scenario: 访问图片元素属性
- **WHEN** 渲染图片元素
- **THEN** 应使用 `elem.src`, `elem.box` 等属性
#### Scenario: 访问形状元素属性
- **WHEN** 渲染形状元素
- **THEN** 应使用 `elem.shape`, `elem.box`, `elem.fill`, `elem.line` 等属性
#### Scenario: 访问表格元素属性
- **WHEN** 渲染表格元素
- **THEN** 应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性
### Requirement: HtmlRenderer 必须与 PptxRenderer 共享元素抽象层
系统 SHALL 使 HtmlRenderer 和 PptxRenderer 都基于相同的元素数据类(定义在 core/elements.py确保一致性。
#### Scenario: 两个渲染器使用相同的元素类型
- **WHEN** 开发者查看 HtmlRenderer 和 PptxRenderer
- **THEN** 两者都应导入并使用 `from core.elements import TextElement, ImageElement, ShapeElement, TableElement`
#### Scenario: 元素验证在创建时完成
- **WHEN** 元素对象传递给 HtmlRenderer
- **THEN** 元素已经在创建时完成验证HtmlRenderer 不需要再次验证
#### Scenario: 添加新元素类型时两个渲染器同步更新
- **WHEN** 在 core/elements.py 中添加新元素类型(如 VideoElement
- **THEN** 需要在 HtmlRenderer 和 PptxRenderer 中都实现对应的渲染方法
### Requirement: 系统必须复用现有的解析逻辑
系统 SHALL 复用 `yaml2pptx.py` 中现有的 `Presentation` 类和模板渲染逻辑。

View File

@@ -0,0 +1,105 @@
# Modular Architecture
## Purpose
定义项目的模块化架构规格,确保代码按功能职责组织,模块大小适中,依赖关系清晰,易于维护和扩展。
## Requirements
### Requirement: 代码必须按功能模块组织
系统必须将代码按照功能职责拆分为独立的模块文件,每个模块文件的代码行数应控制在 150-300 行之间,确保代码易于阅读和维护。
#### Scenario: 单文件脚本拆分为多个模块
- **WHEN** 开发者查看项目结构
- **THEN** 系统应包含以下模块文件:
- `yaml2pptx.py`(入口脚本,约 100 行)
- `core/elements.py`(元素数据类)
- `core/template.py`(模板系统)
- `core/presentation.py`(演示文稿类)
- `loaders/yaml_loader.py`YAML 加载)
- `renderers/pptx_renderer.py`PPTX 渲染器)
- `renderers/html_renderer.py`HTML 渲染器)
- `preview/server.py`(预览服务器)
- `utils.py`(工具函数)
#### Scenario: 每个模块文件大小适中
- **WHEN** 开发者打开任意模块文件
- **THEN** 文件代码行数应在 150-300 行之间(入口脚本除外,约 100 行)
### Requirement: 模块必须按层次组织
系统必须采用四层架构组织代码入口层yaml2pptx.py、核心层core/、加载层loaders/、渲染层renderers/、预览层preview/),每层职责清晰。
#### Scenario: 核心层包含领域模型
- **WHEN** 开发者查看 core/ 目录
- **THEN** 应包含元素数据类elements.py、模板系统template.py、演示文稿类presentation.py
#### Scenario: 加载层负责数据输入
- **WHEN** 开发者查看 loaders/ 目录
- **THEN** 应包含 YAML 加载和验证逻辑yaml_loader.py
#### Scenario: 渲染层负责数据输出
- **WHEN** 开发者查看 renderers/ 目录
- **THEN** 应包含 PPTX 渲染器pptx_renderer.py和 HTML 渲染器html_renderer.py
#### Scenario: 预览层提供可选功能
- **WHEN** 开发者查看 preview/ 目录
- **THEN** 应包含 Flask 服务器和文件监听逻辑server.py
### Requirement: 模块间依赖关系必须清晰
系统必须保持清晰的依赖方向:入口层 → 渲染层 → 核心层 ← 加载层,避免循环依赖。
#### Scenario: 依赖方向单向流动
- **WHEN** 开发者分析模块导入关系
- **THEN** 依赖关系应为:
- yaml2pptx.py 导入 renderers、loaders、core、preview
- renderers 导入 core
- loaders 导入 core
- core 不导入其他业务模块(只导入标准库和第三方库)
- preview 导入 renderers 和 core
#### Scenario: 不存在循环依赖
- **WHEN** 开发者运行脚本
- **THEN** 系统不应出现循环导入错误
### Requirement: 入口脚本必须保持单一职责
yaml2pptx.py 必须仅包含 CLI 参数解析和主流程编排,所有业务逻辑应委托给相应的模块。
#### Scenario: 入口脚本只负责 CLI 和流程编排
- **WHEN** 开发者查看 yaml2pptx.py
- **THEN** 文件应仅包含:
- `/// script` 依赖声明
- `parse_args()` 函数CLI 参数解析)
- `main()` 函数(流程编排)
- 必要的导入语句
#### Scenario: 入口脚本代码行数精简
- **WHEN** 开发者统计 yaml2pptx.py 的代码行数
- **THEN** 应约为 100 行(不包括注释和空行)
### Requirement: 保持向后兼容的使用方式
系统必须保持 `uv run yaml2pptx.py` 的使用方式不变,用户无需修改现有的调用方式。
#### Scenario: CLI 使用方式不变
- **WHEN** 用户运行 `uv run yaml2pptx.py input.yaml output.pptx`
- **THEN** 系统应正常生成 PPTX 文件,行为与重构前一致
#### Scenario: CLI 参数保持兼容
- **WHEN** 用户使用 `--template-dir``--preview``--port` 等参数
- **THEN** 系统应正确解析并执行相应功能

View File

@@ -153,6 +153,87 @@ PPTX generation 系统负责使用 python-pptx 库创建符合 OOXML 标准的 P
- **WHEN** 需要使用 python-pptx 或 PyYAML
- **THEN** 不使用 `pip install`,而是在 script metadata 中声明依赖
### Requirement: PptxGenerator 必须内置渲染器
系统 SHALL 将 PptxGenerator 类重构为内置渲染器的架构,渲染逻辑作为类的私有方法,而不是独立的函数。
#### Scenario: PptxGenerator 包含渲染方法
- **WHEN** 开发者查看 PptxGenerator 类
- **THEN** 应包含 `_render_element()`, `_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()` 等私有方法
#### Scenario: add_slide 方法调用内置渲染器
- **WHEN** 调用 `generator.add_slide(slide_data, base_path)`
- **THEN** 系统应在方法内部调用 `_render_element()` 来渲染每个元素
#### Scenario: 渲染方法接收元素对象
- **WHEN** 渲染方法被调用
- **THEN** 应接收元素对象(如 TextElement, ImageElement而不是字典
### Requirement: PptxGenerator 必须位于 renderers 模块
系统 SHALL 将 PptxGenerator 类从主脚本提取到 `renderers/pptx_renderer.py` 模块中。
#### Scenario: PptxGenerator 在独立模块中
- **WHEN** 开发者查看项目结构
- **THEN** PptxGenerator 类应定义在 `renderers/pptx_renderer.py` 文件中
#### Scenario: 主脚本导入 PptxGenerator
- **WHEN** yaml2pptx.py 需要使用 PptxGenerator
- **THEN** 应通过 `from renderers.pptx_renderer import PptxGenerator` 导入
### Requirement: 渲染器必须通过类型检查分发元素
系统 SHALL 在 `_render_element()` 方法中使用 `isinstance()` 检查元素类型,分发到对应的渲染方法。
#### Scenario: 分发文本元素
- **WHEN** `_render_element()` 接收到 TextElement 对象
- **THEN** 系统应调用 `_render_text(slide, elem)`
#### Scenario: 分发图片元素
- **WHEN** `_render_element()` 接收到 ImageElement 对象
- **THEN** 系统应调用 `_render_image(slide, elem, base_path)`
#### Scenario: 分发形状元素
- **WHEN** `_render_element()` 接收到 ShapeElement 对象
- **THEN** 系统应调用 `_render_shape(slide, elem)`
#### Scenario: 分发表格元素
- **WHEN** `_render_element()` 接收到 TableElement 对象
- **THEN** 系统应调用 `_render_table(slide, elem)`
### Requirement: 渲染方法必须访问元素对象的属性
系统 SHALL 通过元素对象的属性(如 `elem.content`, `elem.box`)访问数据,而不是通过字典的 `get()` 方法。
#### Scenario: 访问文本元素属性
- **WHEN** `_render_text()` 渲染文本元素
- **THEN** 应使用 `elem.content`, `elem.box`, `elem.font` 等属性
#### Scenario: 访问图片元素属性
- **WHEN** `_render_image()` 渲染图片元素
- **THEN** 应使用 `elem.src`, `elem.box` 等属性
#### Scenario: 访问形状元素属性
- **WHEN** `_render_shape()` 渲染形状元素
- **THEN** 应使用 `elem.shape`, `elem.box`, `elem.fill`, `elem.line` 等属性
#### Scenario: 访问表格元素属性
- **WHEN** `_render_table()` 渲染表格元素
- **THEN** 应使用 `elem.data`, `elem.position`, `elem.col_widths`, `elem.style` 等属性
### Requirement: 系统架构保持简洁
系统 SHALL 采用两层架构(模板 + 演示文稿),颜色和样式直接在模板中定义。