1
0
Files
PPTX/README_DEV.md
lanyuanxiaoyao 5d60f3c2c2 feat: 实现模板元素混合模式功能
新增混合模式,允许幻灯片同时使用 template 和 elements,实现更灵活的布局组合。

核心变更:
- core/presentation.py: 修改 render_slide() 支持三种模式(纯模板/纯自定义/混合模式)
- 自定义元素可访问模板变量,实现主题色等值的统一控制
- 元素采用简单追加策略合并(模板元素在前,自定义元素在后)
- 完全向后兼容现有用法

测试覆盖:
- 新增 TestRenderSlideHybridMode 测试类,包含 8 个测试用例
- 验证向后兼容性(纯模板模式、纯自定义模式)
- 验证混合模式功能(变量共享、空元素列表、元素顺序等)
- 所有 79 个测试通过

文档更新:
- README.md: 新增"混合模式模板"章节,包含语法示例和使用场景
- README_DEV.md: 更新开发文档,说明元素合并策略和实现细节

规范更新:
- openspec/specs/template-system/spec.md:
  - 修改"系统必须支持自定义幻灯片"需求,支持混合模式
  - 新增 4 个需求:变量共享、元素合并策略、向后兼容、内联模板支持
  - 新增 13 个场景定义

归档:
- openspec/changes/archive/2026-03-04-template-element-composition/: 完整变更记录
2026-03-04 13:12:51 +08:00

913 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发文档
本文档说明 yaml2pptx 项目的代码结构、开发规范和技术决策。
## 项目概述
yaml2pptx 是一个将 YAML 格式的演示文稿源文件转换为 PPTX 文件的工具,支持模板系统和浏览器预览功能。
## 代码结构
项目采用模块化架构,按功能职责组织代码:
```
html2pptx/
├── yaml2pptx.py (200 行) # 入口脚本CLI + main 函数
├── utils.py (74 行) # 工具函数(日志、颜色转换)
├── core/ # 核心领域模型
│ ├── elements.py (200 行) # 元素抽象层dataclass + validate
│ ├── template.py (191 行) # 模板系统
│ ├── condition_evaluator.py # 条件表达式评估器(新增)
│ └── presentation.py (91 行) # 演示文稿类
├── loaders/ # 数据加载层
│ └── yaml_loader.py (113 行) # YAML 加载和验证
├── validators/ # 验证层
│ ├── __init__.py # 导出主验证器
│ ├── result.py (70 行) # 验证结果数据结构
│ ├── validator.py (150 行) # 主验证器
│ ├── geometry.py (120 行) # 几何验证器
│ └── resource.py (110 行) # 资源验证器
├── renderers/ # 渲染层
│ ├── pptx_renderer.py (292 行) # PPTX 渲染器
│ └── html_renderer.py (172 行) # HTML 渲染器(预览)
└── preview/ # 预览功能
└── server.py (244 行) # Flask 服务器 + 文件监听
```
### 依赖关系
```
yaml2pptx.py (入口)
├─→ utils (工具函数)
├─→ loaders.yaml_loader (YAML 加载)
├─→ validators.validator (验证器)
│ ↓
│ ├─→ validators.result (验证结果)
│ ├─→ validators.geometry (几何验证)
│ ├─→ validators.resource (资源验证)
│ └─→ core.elements (元素验证)
├─→ core.presentation (演示文稿)
│ ↓
│ ├─→ core.template (模板)
│ └─→ core.elements (元素)
├─→ renderers.pptx_renderer (PPTX 生成)
│ ↓
│ └─→ core.elements
└─→ preview.server (预览服务)
└─→ renderers.html_renderer
└─→ core.elements
```
**依赖原则**
- 单向依赖:入口 → 验证/渲染 → 核心 ← 加载
- 无循环依赖
- 核心层不依赖其他业务模块
- 验证层可以调用核心层的元素验证方法
### 图片处理架构
图片适配模式功能采用分层设计,将图片处理逻辑与渲染逻辑分离:
```
ImageElement (核心层)
↓ 定义 fit/background 字段
utils/image_utils.py (工具层)
↓ 提供图片处理函数
├─→ apply_fit_mode() - 应用适配模式
├─→ create_canvas_with_background() - 创建背景画布
└─→ inches_to_pixels() / pixels_to_inches() - 单位转换
↓ 使用 Pillow (PIL)
renderers/pptx_renderer.py (渲染层)
↓ 调用 image_utils 处理图片
└─→ 将处理后的图片添加到 PPTX
renderers/html_renderer.py (渲染层)
└─→ 使用 CSS object-fit 实现相同效果
```
**设计要点**
1. **Pillow 依赖**
- 用于高质量图片处理(缩放、裁剪、画布创建)
- 使用 LANCZOS 重采样算法确保最佳质量
- 支持 RGB 模式和透明度处理
2. **适配模式实现**
- **stretch**:直接使用 `Image.resize()`
- **contain**:使用 `ImageOps.contain()` 保持宽高比
- **cover**:使用 `ImageOps.cover()` + 中心裁剪
- **center**:不缩放,使用 `Image.crop()` 裁剪超出部分
3. **DPI 配置**
-`metadata.dpi` 配置,默认 96
- 通过 Presentation → Renderer 传递
- 用于像素与英寸的转换计算
4. **向后兼容性**
- `fit``background` 参数可选
- 未指定 `fit` 时默认使用 `stretch` 模式
- 现有 YAML 文件无需修改即可正常工作
## 模块职责
### 1. yaml2pptx.py入口层
- **职责**CLI 参数解析、流程编排
- **行数**:约 100-150 行
- **包含**
- `/// script` 依赖声明
- `parse_args()` - 命令行参数解析
- `main()` - 主流程编排
- `handle_convert()` - 转换流程,包含页面级 `enabled` 检查
- **不包含**:业务逻辑、数据处理
- **enabled 实现细节**
- 在主渲染循环中检查 `slide_data.get('enabled', True)`
- 跳过 `enabled=false` 的幻灯片,不调用 `render_slide()`
- 维护独立的 `slide_index` 计数器,只统计实际渲染的幻灯片
- 进度日志显示准确的渲染数量(不包括禁用的幻灯片)
### 2. utils/(工具层)
- **职责**:通用工具函数和图片处理
- **包含**
- `utils/__init__.py` - 日志和颜色工具
- 日志函数:`log_info()`, `log_success()`, `log_error()`, `log_progress()`
- 颜色转换:`hex_to_rgb()`, `validate_color()`
- `utils/image_utils.py` - 图片处理工具
- 单位转换:`inches_to_pixels()`, `pixels_to_inches()`
- 图片适配:`apply_fit_mode()` - 支持 stretch/contain/cover/center 四种模式
- 画布创建:`create_canvas_with_background()` - 创建带背景色的画布
- **依赖**
- Pillow (PIL) - 用于高质量图片处理
- 使用 LANCZOS 重采样算法确保最佳质量
- **设计原则**
- 图片处理与渲染逻辑分离
- 支持 DPI 配置,灵活控制图片分辨率
- 所有图片操作返回 PIL Image 对象,便于后续处理
### 3. loaders/yaml_loader.py加载层
- **职责**YAML 文件加载和验证
- **包含**
- `YAMLError` - 自定义异常
- `load_yaml_file()` - 加载 YAML 文件
- `validate_presentation_yaml()` - 验证演示文稿结构,调用 `validate_templates_yaml()` 验证内联模板,验证幻灯片 `enabled` 字段
- `validate_template_yaml()` - 验证外部模板结构
- `validate_templates_yaml()` - 验证内联模板结构templates 字段)
- **特点**
- 内联模板验证包括:结构验证、元素验证、变量定义验证、默认值验证
- 检测默认值中引用不存在的变量
- 验证 `enabled` 字段必须是布尔值,拒绝字符串或条件表达式
### 4. core/elements.py核心层 - 元素抽象)
- **职责**:定义元素数据类和工厂函数
- **包含**
- `_is_valid_color()` - 颜色格式验证工具函数
- `TextElement` - 文本元素dataclass + validate
- `ImageElement` - 图片元素dataclass + validate
- 新增字段:`fit` (适配模式), `background` (背景色)
- 支持四种适配模式stretch默认、contain、cover、center
- `ShapeElement` - 形状元素dataclass + validate
- `TableElement` - 表格元素dataclass + validate
- `create_element()` - 元素工厂函数
- **特点**
- 使用 `@dataclass` 装饰器
-`__post_init__` 中进行创建时验证
-`validate()` 方法中进行元素级验证
- 元素类负责自身属性的验证(颜色格式、字体大小、枚举值等)
### 4.5. validators/(验证层)
- **职责**YAML 文件验证,在转换前检查问题
- **包含**
- `validators/result.py` - 验证结果数据结构
- `ValidationIssue` - 验证问题level, message, location, code
- `ValidationResult` - 验证结果errors, warnings, infos
- `validators/geometry.py` - 几何验证器
- `GeometryValidator` - 检查元素边界、页面范围
- 支持 0.1 英寸容忍度
- `validators/resource.py` - 资源验证器
- `ResourceValidator` - 检查图片、模板文件存在性
- 验证模板文件结构
- `validators/image_config.py` - 图片配置验证器
- `validate_fit_value()` - 验证 fit 参数值
- `validate_background_color()` - 验证背景色格式
- `validate_dpi_value()` - 验证 DPI 值范围
- `validators/validator.py` - 主验证器
- `Validator` - 协调所有子验证器
- 集成元素级验证、几何验证、资源验证
- **特点**
- 分级错误报告ERROR/WARNING/INFO
- ERROR 阻止转换WARNING 不阻止
- 验证职责分层:元素级验证在元素类中,系统级验证在验证器中
### 5. core/template.py核心层 - 模板)
- **职责**:模板加载和变量解析
- **包含**
- `Template`
- `from_data()` 类方法:从字典创建模板实例(用于内联模板)
- 变量解析:`resolve_value()`, `resolve_element()`
- 条件渲染:`evaluate_condition()` - 委托给 ConditionEvaluator
- 模板渲染:`render()`
- **特点**
- 支持外部模板(从文件加载)和内联模板(从字典创建)
### 5.5. core/condition_evaluator.py核心层 - 条件评估)
- **职责**:安全地评估条件表达式
- **包含**
- `ConditionEvaluator`
- `evaluate_condition()` - 主评估方法
- `_get_evaluator()` - 配置 simpleeval 实例
- `_extract_expression()` - 提取表达式内容
- **技术实现**
- 使用 simpleeval 库的 `EvalWithCompoundTypes` 进行安全评估
- 支持比较运算符(==, !=, >, <, >=, <=
- 支持逻辑运算符and, or, not
- 支持成员测试in, not in
- 支持列表/元组字面量
- 支持数学运算(+, -, *, /, %, **
- 支持内置函数int, float, str, len, bool, abs, min, max
- **安全策略**
- 表达式最大长度限制500 字符
- 禁止属性访问obj.attr
- 禁止函数定义lambda, def
- 禁止模块导入import
- 白名单函数限制
- 详细的错误信息映射
- **错误处理**
- `NameNotDefined` → "条件表达式中的变量未定义"
- `FunctionNotDefined` → "条件表达式中使用了不支持的函数"
- `FeatureNotAvailable` → "条件表达式使用了不支持的语法特性"
- `AttributeDoesNotExist` → "不支持属性访问"
- `SyntaxError` → "条件表达式语法错误"
- 内联模板通过 `from_data()` 类方法创建,避免修改现有 `__init__`
- 禁止内联模板相互引用,在渲染时检测并报错
### 6. core/presentation.py核心层 - 演示文稿)
- **职责**:演示文稿管理和幻灯片渲染
- **包含**
- `Presentation`
- 内联模板存储:`__init__` 中解析并保存 `templates` 字段到 `self.inline_templates`
- 模板查找:`get_template()` 优先查找内联模板,然后回退到外部模板
- 同名检测:`_external_template_exists()` 检查外部模板是否存在,防止命名冲突
- 模板缓存:外部模板使用 `template_cache` 缓存
- 幻灯片渲染:`render_slide()` - 支持三种模式
- **特点**
- 将元素字典转换为元素对象
- 使用 `create_element()` 工厂函数
- 内联和外部模板同名时抛出 ERROR 错误
- 内联模板不需要缓存(已在内存中)
#### 幻灯片渲染模式
`render_slide()` 方法支持三种渲染模式:
1. **纯模板模式**:只有 `template` 字段
- 渲染模板元素
- 向后兼容原有行为
2. **纯自定义模式**:只有 `elements` 字段
- 直接使用自定义元素
- 向后兼容原有行为
3. **混合模式**:同时有 `template``elements` 字段(新功能)
- 先渲染模板元素
- 自定义元素使用模板变量解析(通过 `Template.resolve_element()`
- 合并策略:简单追加(`template_elements + custom_elements`
- z 轴顺序:模板元素在底层,自定义元素在上层
#### 元素合并策略
混合模式采用简单追加策略:
```python
# 步骤1渲染模板如果有
elements_from_template = template.render(vars_values)
# 步骤2处理自定义元素如果有
if has_custom_elements and has_template:
# 使用模板变量解析自定义元素
elements_from_custom = [
template.resolve_element(elem, vars_values)
for elem in custom_elements
]
# 步骤3合并元素模板在前自定义在后
final_elements = elements_from_template + elements_from_custom
```
**设计决策**
- 不支持按 key/id 合并(避免引入元素 ID 概念)
- 不支持位置感知合并(保持简单)
- 不检测元素重叠(用户通过预览模式查看效果)
-`elements: []` 等同于不指定 `elements`
### 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
```
**依赖管理**
- 所有依赖在 `pyproject.toml``[project.dependencies]` 中声明
- uv 会自动安装依赖,无需手动 `pip install`
### 2. 命令行接口
**子命令架构**
```bash
# check - 验证 YAML 文件
uv run yaml2pptx.py check <input> [--template-dir <dir>]
# convert - 转换为 PPTX
uv run yaml2pptx.py convert <input> [output] [--template-dir <dir>] [--skip-validation] [--force]
# preview - 启动预览服务器
uv run yaml2pptx.py preview <input> [--template-dir <dir>] [--port <port>] [--host <host>] [--no-browser]
```
**参数说明**
- `--template-dir`:所有命令通用,指定模板目录
- `--skip-validation`convert 专用,跳过自动验证
- `--force/-f`convert 专用,强制覆盖已存在文件
- `--port`preview 专用,指定端口(默认随机 30000-40000
- `--host`preview 专用,指定主机地址(默认 127.0.0.1
- `--no-browser`preview 专用,不自动打开浏览器
### 3. 文件组织
**代码文件**
- 每个模块文件控制在 150-300 行
- 入口脚本约 200 行
- 使用有意义的文件名和目录结构
**测试文件**
- 所有测试文件、临时文件必须放在 `temp/` 目录下
- 不污染项目根目录
### 4. 代码风格
**导入顺序**
```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)` 工厂函数
**理由**
- 统一入口:所有元素创建都通过工厂函数
- 类型安全:进行类型检查
- 易于扩展:添加新元素类型只需添加一个分支
### 5. 验证职责分层
**决策**:将验证逻辑分为两层
1. **元素级验证**:放在元素类本身(`core/elements.py`
2. **系统级验证**:放在独立的验证器模块(`validators/`
**理由**
- 元素类最了解自己的约束,应该负责自身的完整性验证
- 系统级验证需要全局上下文(如页面尺寸、文件路径),适合集中处理
- 符合单一职责原则,便于扩展和维护
**元素级验证职责**
- 必需字段检查(在 `__post_init__` 中)
- 数据类型检查(在 `__post_init__` 中)
- 值的有效性检查(在 `validate()` 方法中)
- 颜色格式验证
- 字体大小合理性
- 枚举值检查(如形状类型)
- 表格数据一致性
**系统级验证职责**
- 几何验证(元素是否在页面范围内,需要知道页面尺寸)
- 资源验证(文件是否存在,需要知道文件路径)
- 跨元素验证(如果未来需要)
**示例**
```python
# 元素级验证(在元素类中)
@dataclass
class TextElement:
def validate(self) -> List[ValidationIssue]:
issues = []
if self.font.get('color'):
if not _is_valid_color(self.font['color']):
issues.append(ValidationIssue(
level="ERROR",
message=f"无效的颜色格式: {self.font['color']}",
code="INVALID_COLOR_FORMAT"
))
return issues
# 系统级验证(在验证器中)
class GeometryValidator:
def validate_element(self, element, slide_index, elem_index):
# 需要页面尺寸信息
if element.box[0] + element.box[2] > self.slide_width:
# 报告边界超出
```
### 6. 验证容忍度
**决策**:几何验证时,允许 0.1 英寸的容忍度
**理由**
- 浮点数计算可能有精度误差
- 0.1 英寸(约 2.54mm)在视觉上几乎不可见
- 避免误报,提升用户体验
**实现**
```python
TOLERANCE = 0.1 # 英寸
if right > slide_width + TOLERANCE:
# 报告 WARNING
```
### 7. 内联模板系统
**决策**:支持在 YAML 源文件中定义内联模板,与外部模板系统共存
**理由**
- 降低使用门槛:简单场景无需创建单独的模板文件
- 保持灵活性:复杂场景仍可使用外部模板
- 向后兼容:不影响现有外部模板功能
**实现要点**
1. **模板定义**:在 YAML 顶层添加 `templates` 字段
```yaml
templates:
my-template:
vars: [...]
elements: [...]
```
2. **模板创建**:使用 `Template.from_data()` 类方法
```python
@classmethod
def from_data(cls, template_data, template_name):
"""从字典创建模板(内联模板)"""
obj = cls.__new__(cls)
obj.data = template_data
obj.vars_def = {var['name']: var for var in template_data.get('vars', [])}
obj.elements = template_data.get('elements', [])
return obj
```
3. **模板查找**`Presentation.get_template()` 优先查找内联模板
```python
def get_template(self, template_name):
# 1. 检查内联模板
if hasattr(self, 'inline_templates') and template_name in self.inline_templates:
# 2. 检查同名冲突
if self._external_template_exists(template_name):
raise YAMLError(f"模板名称冲突: '{template_name}'")
return Template.from_data(self.inline_templates[template_name], template_name)
# 3. 回退到外部模板
return self._get_external_template(template_name)
```
4. **同名检测**:禁止内联和外部模板同名
- 显式报错比隐式选择更安全
- 强制用户明确模板来源
- 降低调试难度
5. **限制**:禁止内联模板相互引用
- 降低实现复杂度
- 内联模板适合简单场景
- 复杂引用应使用外部模板
6. **验证**`validate_templates_yaml()` 验证内联模板结构
- 检查 `templates` 是否为字典
- 检查每个模板是否有必需的 `elements` 字段
- 检查变量定义是否有必需的 `name` 字段
- 检测默认值中引用不存在的变量
## 扩展指南
### 添加新元素类型
假设要添加 `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()
# ... 渲染逻辑
```
## 测试规范
### 测试框架
项目使用 pytest 作为测试框架,测试代码位于 `tests/` 目录。
### 测试结构
```
tests/
├── conftest.py # pytest 配置和共享 fixtures
├── conftest_pptx.py # PPTX 文件验证工具
├── unit/ # 单元测试
│ ├── test_elements.py # 元素类测试
│ ├── test_template.py # 模板系统测试
│ ├── test_utils.py # 工具函数测试
│ ├── test_validators/ # 验证器测试
│ │ ├── test_geometry.py
│ │ ├── test_resource.py
│ │ ├── test_result.py
│ │ └── test_validator.py
│ └── test_loaders/ # 加载器测试
│ └── test_yaml_loader.py
├── integration/ # 集成测试
│ ├── test_presentation.py
│ ├── test_rendering_flow.py
│ └── test_validation_flow.py
├── e2e/ # 端到端测试
│ ├── test_convert_cmd.py
│ ├── test_check_cmd.py
│ └── test_preview_cmd.py
└── fixtures/ # 测试数据
├── yaml_samples/ # YAML 样本
├── templates/ # 测试模板
└── images/ # 测试图片
```
### 运行测试
```bash
# 安装测试依赖
uv pip install -e ".[dev]"
# 运行所有测试
uv run pytest
# 运行特定类型的测试
uv run pytest tests/unit/ # 单元测试
uv run pytest tests/integration/ # 集成测试
uv run pytest tests/e2e/ # 端到端测试
# 运行特定测试文件
uv run pytest tests/unit/test_elements.py
# 显示详细输出
uv run pytest -v
# 显示测试覆盖率
uv run pytest --cov=. --cov-report=html
```
### 编写测试
**测试类命名**:使用 `Test<ClassName>` 格式
**测试方法命名**:使用 `test_<what_is_being_tested>` 格式
```python
class TestTextElement:
"""TextElement 测试类"""
def test_create_with_defaults(self):
"""测试使用默认值创建 TextElement"""
elem = TextElement()
assert elem.type == 'text'
def test_invalid_color_raises_error(self):
"""测试无效颜色会引发错误"""
with pytest.raises(ValueError, match="无效颜色"):
TextElement(font={"color": "red"})
```
### Fixtures
共享 fixtures 定义在 `tests/conftest.py` 中:
- `temp_dir`: 临时目录
- `sample_yaml`: 最小测试 YAML 文件
- `sample_image`: 测试图片
- `sample_template`: 测试模板
- `pptx_validator`: PPTX 验证器
```python
def test_with_fixture(sample_yaml):
"""使用 fixture 的测试"""
assert sample_yaml.exists()
```
### PPTX 验证
使用 `PptxFileValidator` 验证生成的 PPTX 文件:
```python
def test_pptx_generation(temp_dir, pptx_validator):
"""测试 PPTX 生成"""
# ... 生成 PPTX ...
output_path = temp_dir / "output.pptx"
# 验证文件
assert pptx_validator.validate_file(output_path) is True
# 验证内容
prs = Presentation(str(output_path))
assert pptx_validator.validate_text_element(
prs.slides[0],
index=0,
expected_content="Test"
) is True
```
### 手动测试
```bash
# 验证 YAML 文件
uv run yaml2pptx.py check temp/test.yaml
# 使用模板时验证
uv run yaml2pptx.py check temp/demo.yaml --template-dir temp/templates
# 转换 YAML 为 PPTX
uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx
# 自动生成输出文件名
uv run yaml2pptx.py convert temp/test.yaml
# 跳过自动验证
uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --skip-validation
# 强制覆盖已存在文件
uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --force
# 使用模板
uv run yaml2pptx.py convert temp/demo.yaml temp/output.pptx --template-dir temp/templates
# 启动预览服务器
uv run yaml2pptx.py preview temp/test.yaml
# 指定端口
uv run yaml2pptx.py preview temp/test.yaml --port 8080
# 允许局域网访问
uv run yaml2pptx.py preview temp/test.yaml --host 0.0.0.0
# 不自动打开浏览器
uv run yaml2pptx.py preview temp/test.yaml --no-browser
```
### 测试文件位置
- **自动化测试**`tests/` 目录
- **手动测试文件**`temp/` 目录
- `temp/*.yaml` - 手动测试用的 YAML 文件
- `temp/*.pptx` - 生成的 PPTX 文件
- `temp/templates/` - 手动测试用的模板文件
## 常见问题
### Q: 为什么不能直接使用 python 运行脚本?
A: 项目使用 uv 和 pyproject.toml 来管理依赖。直接使用 python 会导致依赖缺失。必须使用 `uv run yaml2pptx.py`。
### Q: 如何添加新的依赖?
A: 在 `pyproject.toml` 的 `[project.dependencies]` 中添加:
```toml
[project]
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 preview temp/test.yaml
```
在浏览器中查看渲染结果,支持热重载。
## 项目约束
1. **面向中文开发者**:注释、文档、错误消息使用中文
2. **使用 uv 运行**:严禁直接使用主机环境的 python
3. **测试文件隔离**:所有测试文件放在 `temp/` 目录
4. **不污染主机环境**:不修改主机的 Python 配置
## 维护指南
### 代码审查要点
- [ ] 模块文件大小合理150-300 行)
- [ ] 无循环依赖
- [ ] 所有类和函数有文档字符串
- [ ] 使用中文注释
- [ ] 元素验证在 `__post_init__` 中完成
- [ ] 导入语句按标准库、第三方库、本地模块排序
- [ ] 测试文件在 `temp/` 目录下
### 性能优化建议
1. **模板缓存**Presentation 类已实现模板缓存
2. **元素验证**:只在创建时验证一次,渲染时不再验证
3. **文件监听**:预览模式使用 watchdog 高效监听文件变化