移除图片 fit 和 background 参数支持,简化图片渲染逻辑。系统恢复到直接使用 python-pptx 原生图片添加功能,图片将被拉伸到指定尺寸。 变更内容: - 移除 ImageElement 的 fit 和 background 字段 - 移除 metadata.dpi 配置 - 删除 utils/image_utils.py 图片处理工具模块 - 删除 validators/image_config.py 验证器 - 简化 PPTX 和 HTML 渲染器的图片处理逻辑 - HTML 渲染器使用硬编码 DPI=96(Web 标准) - 删除相关测试文件(单元测试、集成测试、e2e 测试) - 更新规格文档和用户文档 - 保留 Pillow 依赖用于未来可能的图片处理需求 影响: - 删除 11 个文件 - 修改 10 个文件 - 净减少 1558 行代码 - 所有 402 个测试通过 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
899 lines
28 KiB
Markdown
899 lines
28 KiB
Markdown
# 开发文档
|
||
|
||
本文档说明 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
|
||
```
|
||
|
||
**依赖原则**:
|
||
- 单向依赖:入口 → 验证/渲染 → 核心 ← 加载
|
||
- 无循环依赖
|
||
- 核心层不依赖其他业务模块
|
||
- 验证层可以调用核心层的元素验证方法
|
||
|
||
## 模块职责
|
||
|
||
### 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()`
|
||
- **依赖**:
|
||
- Pillow (PIL) - 保留用于未来可能的图片处理需求
|
||
|
||
### 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/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` 字段
|
||
- 检测默认值中引用不存在的变量
|
||
|
||
### 8. description 字段
|
||
|
||
**决策**:为 metadata、模板和幻灯片添加可选的 `description` 字段
|
||
|
||
**理由**:
|
||
- 自文档化:提高模板和演示文稿的可读性和可维护性
|
||
- 不影响渲染:description 仅用于文档目的,不参与 PPTX 生成
|
||
- 完全向后兼容:字段为可选,现有文件无需修改
|
||
|
||
**实现要点**:
|
||
|
||
1. **数据模型**:
|
||
- `Presentation.description`:从 `metadata.description` 读取
|
||
- `Template.description`:从模板文件的 `description` 字段读取
|
||
- `Slide.description`:在 `render_slide()` 返回值中保留
|
||
|
||
2. **YAML 解析**:
|
||
- 使用 `.get('description')` 自动处理缺失情况(返回 None)
|
||
- YAML 原生支持多行文本格式
|
||
|
||
3. **不参与渲染**:
|
||
- description 字段不传递给渲染器
|
||
- 不写入最终的 PPTX 文件
|
||
|
||
**示例**:
|
||
```yaml
|
||
# metadata description
|
||
metadata:
|
||
description: "2024年度项目进展总结"
|
||
|
||
# 模板 description
|
||
templates:
|
||
title-slide:
|
||
description: "用于章节标题页的模板"
|
||
elements: [...]
|
||
|
||
# 幻灯片 description
|
||
slides:
|
||
- description: "介绍项目背景"
|
||
template: title-slide
|
||
```
|
||
|
||
## 扩展指南
|
||
|
||
### 添加新元素类型
|
||
|
||
假设要添加 `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 高效监听文件变化
|