Compare commits
24 Commits
0a804eacc8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c13e5b64c | |||
| 124ef0e5ce | |||
| 98098dc911 | |||
| f1aae96a04 | |||
| bd12fce14b | |||
| 7ef29ea039 | |||
| f34405be36 | |||
| 2fd8bc1b4a | |||
| 5d60f3c2c2 | |||
| 5b367f7ef3 | |||
| 900a38b705 | |||
| 19d6661381 | |||
| 16ca9d77cd | |||
| 01a93ce13b | |||
| 01eacb0b97 | |||
| 2ba1bd7272 | |||
| 22614d6f55 | |||
| e82a6a945e | |||
| ef3fa6a06a | |||
| e31a7e9bed | |||
| c73bd0fedd | |||
| f273cef195 | |||
| ab2510a400 | |||
| 027a832c9a |
@@ -7,7 +7,10 @@
|
||||
"Bash(git:*)",
|
||||
"Bash(uv:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(wc:*)"
|
||||
"Bash(wc:*)",
|
||||
"Bash(curl:*)",
|
||||
"mcp__context7__query-docs",
|
||||
"mcp__exa__web_search_exa"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
457
README.md
457
README.md
@@ -2,21 +2,21 @@
|
||||
|
||||
使用 YAML 声明式语法创建 PowerPoint 演示文稿的工具。
|
||||
|
||||
## ✨ 功能特性
|
||||
## 功能特性
|
||||
|
||||
- 📝 **YAML 声明式语法** - 使用简单易读的 YAML 定义演示文稿
|
||||
- ✅ **智能验证** - 转换前自动检查 YAML 文件,提前发现问题
|
||||
- 🎨 **模板系统** - 支持参数化模板,复用幻灯片布局
|
||||
- 🧩 **丰富的元素类型** - 文本、图片、形状、表格
|
||||
- 👁️ **实时预览** - 浏览器预览模式,支持热重载
|
||||
- 📐 **灵活尺寸** - 支持 16:9 和 4:3 两种宽高比
|
||||
- 🔧 **模块化架构** - 易于扩展和维护
|
||||
- **YAML 声明式语法** - 使用简单易读的 YAML 定义演示文稿
|
||||
- **智能验证** - 转换前自动检查 YAML 文件,提前发现问题
|
||||
- **模板系统** - 支持参数化模板,复用幻灯片布局
|
||||
- **丰富的元素类型** - 文本、图片、形状、表格
|
||||
- **实时预览** - 浏览器预览模式,支持热重载
|
||||
- **灵活尺寸** - 支持 16:9 和 4:3 两种宽高比
|
||||
- **模块化架构** - 易于扩展和维护
|
||||
|
||||
## 🚀 快速开始
|
||||
## 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
本工具使用 [uv](https://github.com/astral-sh/uv) 管理依赖,运行时会自动安装所需的 Python 包。
|
||||
本工具使用 [uv](https://github.com/astral-sh/uv) 管理依赖。项目依赖在 pyproject.toml 中声明,运行时会自动安装所需的 Python 包。
|
||||
|
||||
### 基本用法
|
||||
|
||||
@@ -27,8 +27,8 @@ uv run yaml2pptx.py convert presentation.yaml output.pptx
|
||||
# 自动生成输出文件名
|
||||
uv run yaml2pptx.py convert presentation.yaml
|
||||
|
||||
# 使用模板
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx --template-dir ./templates
|
||||
# 使用模板库
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx --template ./templates.yaml
|
||||
```
|
||||
|
||||
### 实时预览
|
||||
@@ -39,69 +39,27 @@ uv run yaml2pptx.py preview presentation.yaml
|
||||
|
||||
# 指定端口
|
||||
uv run yaml2pptx.py preview presentation.yaml --port 8080
|
||||
|
||||
# 允许局域网访问
|
||||
uv run yaml2pptx.py preview presentation.yaml --host 0.0.0.0
|
||||
|
||||
# 不自动打开浏览器
|
||||
uv run yaml2pptx.py preview presentation.yaml --no-browser
|
||||
```
|
||||
|
||||
预览模式会自动监听文件变化,修改 YAML 文件后浏览器会自动刷新。
|
||||
|
||||
### 验证功能
|
||||
|
||||
在转换前验证 YAML 文件,提前发现问题:
|
||||
|
||||
```bash
|
||||
# 独立验证命令
|
||||
# 验证 YAML 文件
|
||||
uv run yaml2pptx.py check presentation.yaml
|
||||
|
||||
# 使用模板时验证
|
||||
uv run yaml2pptx.py check presentation.yaml --template-dir ./templates
|
||||
uv run yaml2pptx.py check presentation.yaml --template ./templates.yaml
|
||||
```
|
||||
|
||||
验证功能会检查:
|
||||
- ✅ YAML 语法和结构
|
||||
- ✅ 元素是否超出页面范围
|
||||
- ✅ 图片和模板文件是否存在
|
||||
- ✅ 颜色格式是否正确
|
||||
- ✅ 字体大小是否合理
|
||||
- ✅ 表格数据是否一致
|
||||
## 核心概念
|
||||
|
||||
**自动验证**:转换时默认会自动验证,如果发现错误会终止转换。可以使用 `--skip-validation` 跳过验证:
|
||||
|
||||
```bash
|
||||
# 跳过自动验证
|
||||
uv run yaml2pptx.py convert presentation.yaml --skip-validation
|
||||
```
|
||||
|
||||
**验证结果示例**:
|
||||
|
||||
```
|
||||
🔍 正在检查 YAML 文件...
|
||||
|
||||
❌ 错误 (2):
|
||||
[幻灯片 2, 元素 1] 无效的颜色格式: red (应为 #RRGGBB)
|
||||
[幻灯片 3, 元素 2] 图片文件不存在: logo.png
|
||||
|
||||
⚠️ 警告 (1):
|
||||
[幻灯片 1, 元素 1] 元素右边界超出: 10.50 > 10
|
||||
|
||||
检查完成: 发现 2 个错误, 1 个警告
|
||||
```
|
||||
|
||||
- **ERROR**:阻止转换的严重问题(文件不存在、语法错误等)
|
||||
- **WARNING**:影响视觉效果的问题(元素超出页面、字体太小等)
|
||||
- **INFO**:优化建议
|
||||
|
||||
## 📖 YAML 语法
|
||||
|
||||
### 最小示例
|
||||
### YAML 语法基础
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: 16:9 # 或 4:3
|
||||
size: "16:9"
|
||||
|
||||
slides:
|
||||
- background:
|
||||
@@ -113,229 +71,184 @@ slides:
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
color: "#333333"
|
||||
```
|
||||
|
||||
### 元素类型
|
||||
|
||||
#### 文本元素
|
||||
|
||||
```yaml
|
||||
- type: text
|
||||
box: [x, y, width, height]
|
||||
content: "文本内容"
|
||||
font:
|
||||
size: 18
|
||||
bold: true
|
||||
color: "#ff0000"
|
||||
```
|
||||
|
||||
#### 图片元素
|
||||
|
||||
```yaml
|
||||
- type: image
|
||||
box: [x, y, width, height]
|
||||
src: "path/to/image.png"
|
||||
```
|
||||
|
||||
#### 形状元素
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [x, y, width, height]
|
||||
shape: rectangle
|
||||
fill: "#4a90e2"
|
||||
```
|
||||
|
||||
#### 表格元素
|
||||
|
||||
```yaml
|
||||
- type: table
|
||||
position: [x, y]
|
||||
col_widths: [2, 2, 2]
|
||||
data:
|
||||
- ["表头1", "表头2", "表头3"]
|
||||
- ["数据1", "数据2", "数据3"]
|
||||
```
|
||||
|
||||
### 模板系统
|
||||
|
||||
#### 内联模板
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9"
|
||||
|
||||
templates:
|
||||
title-slide:
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 2, 8, 1]
|
||||
content: "{title}"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
|
||||
slides:
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "我的演示文稿"
|
||||
```
|
||||
|
||||
#### 外部模板库
|
||||
|
||||
```bash
|
||||
# 创建 templates.yaml
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx --template ./templates.yaml
|
||||
```
|
||||
|
||||
### 字体主题系统
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
fonts:
|
||||
title:
|
||||
family: "cjk-sans"
|
||||
size: 44
|
||||
bold: true
|
||||
body:
|
||||
family: "sans"
|
||||
size: 18
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
content: "标题"
|
||||
font: "@title"
|
||||
```
|
||||
|
||||
## 常见用例
|
||||
|
||||
### 标题页
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9"
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
box: [1, 2.5, 8, 1]
|
||||
content: "项目名称"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
align: center
|
||||
```
|
||||
|
||||
### 内容页
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
box: [1, 1, 8, 0.8]
|
||||
content: "章节标题"
|
||||
font:
|
||||
size: 32
|
||||
bold: true
|
||||
|
||||
- type: text
|
||||
box: [1, 2, 8, 3]
|
||||
content: "正文内容\n支持多行文本"
|
||||
font:
|
||||
size: 18
|
||||
```
|
||||
|
||||
### 使用模板
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: 16:9
|
||||
size: "16:9"
|
||||
|
||||
slides:
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "我的演示文稿"
|
||||
subtitle: "使用 yaml2pptx 创建"
|
||||
author: "张三"
|
||||
|
||||
- template: content-slide
|
||||
vars:
|
||||
title: "功能概览"
|
||||
content: "yaml2pptx 支持多种元素类型..."
|
||||
title: "第一章"
|
||||
```
|
||||
|
||||
## 🎨 元素类型
|
||||
## 文档导航
|
||||
|
||||
### 文本元素
|
||||
**用户文档**
|
||||
|
||||
```yaml
|
||||
- type: text
|
||||
box: [x, y, width, height] # 位置和尺寸(英寸)
|
||||
content: "文本内容"
|
||||
font:
|
||||
size: 18 # 字号(磅)
|
||||
bold: true # 粗体
|
||||
italic: false # 斜体
|
||||
color: "#ff0000" # 颜色
|
||||
align: center # left/center/right
|
||||
```
|
||||
- [用户指南](docs/user-guide.md) - 完整使用指南
|
||||
- [元素类型](docs/elements/) - 文本、图片、形状、表格
|
||||
- [模板系统](docs/templates/) - 内联模板、外部模板库、混合模式
|
||||
- [字体主题](docs/fonts.md) - 字体配置和主题管理
|
||||
- [API 参考](docs/reference/) - 命令、坐标、颜色、验证
|
||||
- [示例集合](docs/examples.md) - 实战示例
|
||||
- [故障排查](docs/troubleshooting.md) - 常见问题
|
||||
|
||||
**特性**:文本框默认启用自动换行,文字超出宽度时会自动换行。
|
||||
**开发文档**
|
||||
|
||||
### 图片元素
|
||||
- [架构设计](docs/development/architecture.md) - 代码结构和技术决策
|
||||
- [模块详解](docs/development/modules/) - 各模块详细说明
|
||||
- [字体系统](docs/development/font-system.md) - 字体解析实现
|
||||
- [作用域系统](docs/development/scope-system.md) - 字体作用域规则
|
||||
- [开发规范](docs/development/development-guide.md) - 编码规范
|
||||
- [扩展指南](docs/development/extending.md) - 添加新功能
|
||||
- [测试规范](docs/development/testing.md) - 测试指南
|
||||
|
||||
```yaml
|
||||
- type: image
|
||||
box: [x, y, width, height]
|
||||
src: "path/to/image.png" # 支持相对路径和绝对路径
|
||||
```
|
||||
## 使用技巧
|
||||
|
||||
### 形状元素
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [x, y, width, height]
|
||||
shape: rectangle # rectangle/ellipse/rounded_rectangle
|
||||
fill: "#4a90e2" # 填充颜色
|
||||
line:
|
||||
color: "#000000" # 边框颜色
|
||||
width: 2 # 边框宽度(磅)
|
||||
```
|
||||
|
||||
### 表格元素
|
||||
|
||||
```yaml
|
||||
- type: table
|
||||
position: [x, y]
|
||||
col_widths: [2, 2, 2] # 每列宽度(英寸)
|
||||
data:
|
||||
- ["表头1", "表头2", "表头3"]
|
||||
- ["数据1", "数据2", "数据3"]
|
||||
- ["数据4", "数据5", "数据6"]
|
||||
style:
|
||||
font_size: 14
|
||||
header_bg: "#4a90e2"
|
||||
header_color: "#ffffff"
|
||||
```
|
||||
|
||||
## 📋 模板系统
|
||||
|
||||
模板允许你定义可复用的幻灯片布局。
|
||||
|
||||
### 创建模板
|
||||
|
||||
创建模板文件 `templates/title-slide.yaml`:
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
- name: subtitle
|
||||
required: false
|
||||
default: ""
|
||||
- name: author
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 2, 8, 1]
|
||||
content: "{title}"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
align: center
|
||||
|
||||
- type: text
|
||||
box: [1, 3.5, 8, 0.5]
|
||||
content: "{subtitle}"
|
||||
visible: "{subtitle != ''}" # 条件渲染
|
||||
font:
|
||||
size: 24
|
||||
align: center
|
||||
|
||||
- type: text
|
||||
box: [1, 5, 8, 0.5]
|
||||
content: "{author}"
|
||||
font:
|
||||
size: 18
|
||||
align: center
|
||||
```
|
||||
|
||||
### 使用模板
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "我的演示文稿"
|
||||
subtitle: "副标题"
|
||||
author: "作者"
|
||||
```
|
||||
|
||||
### 条件渲染
|
||||
|
||||
使用 `visible` 属性控制元素显示:
|
||||
|
||||
```yaml
|
||||
- type: text
|
||||
content: "{subtitle}"
|
||||
visible: "{subtitle != ''}" # 仅当 subtitle 不为空时显示
|
||||
```
|
||||
|
||||
## 🎯 命令行选项
|
||||
|
||||
### check 命令
|
||||
|
||||
验证 YAML 文件的正确性。
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `input` | 输入的 YAML 文件路径(必需) |
|
||||
| `--template-dir` | 模板文件目录 |
|
||||
|
||||
### convert 命令
|
||||
|
||||
将 YAML 文件转换为 PPTX 文件。
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `input` | 输入的 YAML 文件路径(必需) |
|
||||
| `output` | 输出的 PPTX 文件路径(可选) |
|
||||
| `--template-dir` | 模板文件目录 |
|
||||
| `--skip-validation` | 跳过自动验证 |
|
||||
| `--force` / `-f` | 强制覆盖已存在文件 |
|
||||
|
||||
### preview 命令
|
||||
|
||||
启动预览服务器,实时查看演示文稿效果。
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `input` | 输入的 YAML 文件路径(必需) |
|
||||
| `--template-dir` | 模板文件目录 |
|
||||
| `--port` | 服务器端口(默认:随机端口 30000-40000) |
|
||||
| `--host` | 主机地址(默认:127.0.0.1) |
|
||||
| `--no-browser` | 不自动打开浏览器 |
|
||||
|
||||
## 📐 坐标系统
|
||||
|
||||
- **单位**:英寸 (inch)
|
||||
- **原点**:幻灯片左上角 (0, 0)
|
||||
- **方向**:X 轴向右,Y 轴向下
|
||||
|
||||
**幻灯片尺寸**:
|
||||
- 16:9 → 10" × 5.625"
|
||||
- 4:3 → 10" × 7.5"
|
||||
|
||||
**示例**:`box: [1, 2, 8, 1]` 表示:
|
||||
- 左上角位置:(1", 2")
|
||||
- 尺寸:宽 8",高 1"
|
||||
|
||||
## 🎨 颜色格式
|
||||
|
||||
支持两种十六进制格式:
|
||||
- **短格式**:`#RGB`(如 `#fff` = 白色)
|
||||
- **完整格式**:`#RRGGBB`(如 `#ffffff` = 白色)
|
||||
|
||||
## 💡 使用技巧
|
||||
|
||||
1. **开发流程**:使用 `--preview` 模式实时查看效果,确认无误后再生成 PPTX
|
||||
1. **开发流程**:使用预览模式实时查看效果,确认无误后再生成 PPTX
|
||||
2. **模板复用**:为常用布局创建模板,保持演示文稿风格一致
|
||||
3. **相对路径**:图片路径相对于 YAML 文件位置,便于项目管理
|
||||
4. **模板目录**:使用模板时必须指定 `--template-dir` 参数
|
||||
5. **文本换行**:文本框默认启用自动换行,无需手动处理长文本
|
||||
4. **文本换行**:文本框默认启用自动换行,无需手动处理长文本
|
||||
|
||||
## 📚 完整示例
|
||||
|
||||
查看 `temp/` 目录下的示例文件:
|
||||
- `temp/test_refactor.yaml` - 基本示例
|
||||
- `temp/template_demo.yaml` - 模板使用示例
|
||||
- `temp/complex_presentation.yaml` - 复杂演示文稿示例
|
||||
|
||||
## ⚠️ 常见错误
|
||||
|
||||
| 错误信息 | 原因 | 解决方法 |
|
||||
|---------|------|---------|
|
||||
| `文件不存在: xxx.yaml` | 找不到输入文件 | 检查文件路径是否正确 |
|
||||
| `YAML 语法错误: 第 X 行` | YAML 格式错误 | 检查缩进和语法 |
|
||||
| `模板文件不存在: xxx` | 模板文件未找到 | 检查模板名称和 `--template-dir` |
|
||||
| `缺少必需变量: xxx` | 未提供必需的模板变量 | 在 `vars` 中提供该变量 |
|
||||
| `图片文件未找到: xxx` | 图片文件不存在 | 检查图片路径 |
|
||||
|
||||
## 🔧 扩展性
|
||||
## 扩展性
|
||||
|
||||
yaml2pptx 采用模块化架构,易于扩展:
|
||||
|
||||
@@ -343,23 +256,35 @@ yaml2pptx 采用模块化架构,易于扩展:
|
||||
- **添加新渲染器**:支持输出到其他格式(如 PDF)
|
||||
- **自定义模板**:创建符合你需求的模板库
|
||||
|
||||
详见 [开发文档](README_DEV.md)。
|
||||
详见 [开发文档](docs/development/extending.md)。
|
||||
|
||||
## 📦 依赖项
|
||||
## 依赖项
|
||||
|
||||
- `python-pptx` - PowerPoint 文件生成
|
||||
- `pyyaml` - YAML 解析
|
||||
- `flask` - 预览服务器
|
||||
- `watchdog` - 文件监听
|
||||
|
||||
所有依赖由 uv 自动管理,无需手动安装。
|
||||
依赖在 pyproject.toml 中声明,由 uv 自动管理,无需手动安装。
|
||||
|
||||
## 🤝 贡献
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
uv run pytest
|
||||
|
||||
# 运行特定类型的测试
|
||||
uv run pytest tests/unit/ # 单元测试
|
||||
uv run pytest tests/integration/ # 集成测试
|
||||
uv run pytest tests/e2e/ # 端到端测试
|
||||
```
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献代码、报告问题或提出建议!
|
||||
|
||||
开发者请参阅 [开发文档](README_DEV.md) 了解代码结构和开发规范。
|
||||
开发者请参阅 [开发文档](docs/development/) 了解代码结构和开发规范。
|
||||
|
||||
## 📄 许可证
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
579
README_DEV.md
579
README_DEV.md
@@ -1,579 +0,0 @@
|
||||
# 开发文档
|
||||
|
||||
本文档说明 yaml2pptx 项目的代码结构、开发规范和技术决策。
|
||||
|
||||
## 项目概述
|
||||
|
||||
yaml2pptx 是一个将 YAML 格式的演示文稿源文件转换为 PPTX 文件的工具,支持模板系统和浏览器预览功能。
|
||||
|
||||
## 代码结构
|
||||
|
||||
项目采用模块化架构,按功能职责组织代码:
|
||||
|
||||
```
|
||||
html2pptx/
|
||||
├── yaml2pptx.py (200 行) # 入口脚本,CLI + main 函数
|
||||
├── utils.py (74 行) # 工具函数(日志、颜色转换)
|
||||
├── core/ # 核心领域模型
|
||||
│ ├── elements.py (200 行) # 元素抽象层(dataclass + validate)
|
||||
│ ├── template.py (191 行) # 模板系统
|
||||
│ └── 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()` - 主流程编排
|
||||
- **不包含**:业务逻辑、数据处理
|
||||
|
||||
### 2. utils.py(工具层)
|
||||
- **职责**:通用工具函数
|
||||
- **包含**:
|
||||
- 日志函数:`log_info()`, `log_success()`, `log_error()`, `log_progress()`
|
||||
- 颜色转换:`hex_to_rgb()`, `validate_color()`
|
||||
|
||||
### 3. loaders/yaml_loader.py(加载层)
|
||||
- **职责**:YAML 文件加载和验证
|
||||
- **包含**:
|
||||
- `YAMLError` - 自定义异常
|
||||
- `load_yaml_file()` - 加载 YAML 文件
|
||||
- `validate_presentation_yaml()` - 验证演示文稿结构
|
||||
- `validate_template_yaml()` - 验证模板结构
|
||||
|
||||
### 4. core/elements.py(核心层 - 元素抽象)
|
||||
- **职责**:定义元素数据类和工厂函数
|
||||
- **包含**:
|
||||
- `_is_valid_color()` - 颜色格式验证工具函数
|
||||
- `TextElement` - 文本元素(dataclass + validate)
|
||||
- `ImageElement` - 图片元素(dataclass + validate)
|
||||
- `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` 类
|
||||
- 变量解析:`resolve_value()`, `resolve_element()`
|
||||
- 条件渲染:`evaluate_condition()`
|
||||
- 模板渲染:`render()`
|
||||
|
||||
### 6. core/presentation.py(核心层 - 演示文稿)
|
||||
- **职责**:演示文稿管理和幻灯片渲染
|
||||
- **包含**:
|
||||
- `Presentation` 类
|
||||
- 模板缓存:`get_template()`
|
||||
- 幻灯片渲染:`render_slide()`
|
||||
- **特点**:
|
||||
- 将元素字典转换为元素对象
|
||||
- 使用 `create_element()` 工厂函数
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
**依赖管理**:
|
||||
- 所有依赖在 `yaml2pptx.py` 的 `/// script` 头部声明
|
||||
- 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
|
||||
```
|
||||
|
||||
## 扩展指南
|
||||
|
||||
### 添加新元素类型
|
||||
|
||||
假设要添加 `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()
|
||||
# ... 渲染逻辑
|
||||
```
|
||||
|
||||
## 测试规范
|
||||
|
||||
### 运行测试
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 测试文件位置
|
||||
|
||||
所有测试文件必须放在 `temp/` 目录下:
|
||||
- `temp/*.yaml` - 测试用的 YAML 文件
|
||||
- `temp/*.pptx` - 生成的 PPTX 文件
|
||||
- `temp/templates/` - 测试用的模板文件
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么不能直接使用 python 运行脚本?
|
||||
|
||||
A: 项目使用 uv 的 Inline script metadata 来管理依赖。直接使用 python 会导致依赖缺失。必须使用 `uv run yaml2pptx.py`。
|
||||
|
||||
### Q: 如何添加新的依赖?
|
||||
|
||||
A: 在 `yaml2pptx.py` 的 `/// script` 头部添加:
|
||||
```python
|
||||
# /// script
|
||||
# requires-python = ">=3.8"
|
||||
# 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 高效监听文件变化
|
||||
155
core/condition_evaluator.py
Normal file
155
core/condition_evaluator.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
条件评估器模块
|
||||
|
||||
提供基于 simpleeval 的安全条件表达式评估能力
|
||||
"""
|
||||
|
||||
from simpleeval import (
|
||||
EvalWithCompoundTypes,
|
||||
NameNotDefined,
|
||||
FunctionNotDefined,
|
||||
FeatureNotAvailable,
|
||||
AttributeDoesNotExist
|
||||
)
|
||||
from loaders.yaml_loader import YAMLError
|
||||
|
||||
|
||||
class ConditionEvaluator:
|
||||
"""条件表达式评估器
|
||||
|
||||
使用 simpleeval 库安全地评估条件表达式,支持:
|
||||
- 比较运算符:==, !=, >, <, >=, <=
|
||||
- 逻辑运算符:and, or, not
|
||||
- 成员测试:in, not in
|
||||
- 列表/元组字面量
|
||||
- 数学运算:+, -, *, /, %, **
|
||||
- 内置函数:int, float, str, len, bool, abs, min, max
|
||||
"""
|
||||
|
||||
# 表达式最大长度限制(防止过于复杂的表达式)
|
||||
MAX_EXPRESSION_LENGTH = 500
|
||||
|
||||
def __init__(self):
|
||||
"""初始化条件评估器"""
|
||||
pass
|
||||
|
||||
def _get_evaluator(self, vars_values):
|
||||
"""
|
||||
获取配置好的 simpleeval 实例
|
||||
|
||||
Args:
|
||||
vars_values: 变量值字典
|
||||
|
||||
Returns:
|
||||
EvalWithCompoundTypes 实例
|
||||
"""
|
||||
# 每次评估都创建新实例,避免状态污染
|
||||
evaluator = EvalWithCompoundTypes(names=vars_values)
|
||||
|
||||
# 添加额外的安全函数到白名单
|
||||
evaluator.functions.update({
|
||||
'len': len,
|
||||
'bool': bool,
|
||||
'abs': abs,
|
||||
'min': min,
|
||||
'max': max,
|
||||
})
|
||||
|
||||
return evaluator
|
||||
|
||||
def _extract_expression(self, condition):
|
||||
"""
|
||||
从条件字符串中提取表达式
|
||||
|
||||
Args:
|
||||
condition: 条件字符串,如 "{count > 0}"
|
||||
|
||||
Returns:
|
||||
str: 提取的表达式,如 "count > 0"
|
||||
"""
|
||||
# 去除首尾的 { } 和空白
|
||||
expr = condition.strip()
|
||||
if expr.startswith('{') and expr.endswith('}'):
|
||||
expr = expr[1:-1].strip()
|
||||
return expr
|
||||
|
||||
def evaluate_condition(self, condition, vars_values):
|
||||
"""
|
||||
评估条件表达式
|
||||
|
||||
Args:
|
||||
condition: 条件字符串,如 "{count > 0 and status == 'active'}"
|
||||
vars_values: 变量值字典
|
||||
|
||||
Returns:
|
||||
bool: 条件是否满足
|
||||
|
||||
Raises:
|
||||
YAMLError: 表达式评估失败
|
||||
"""
|
||||
# 提取 {expression} 中的表达式
|
||||
expr = self._extract_expression(condition)
|
||||
|
||||
# 验证表达式长度
|
||||
if len(expr) > self.MAX_EXPRESSION_LENGTH:
|
||||
raise YAMLError(
|
||||
f"条件表达式过长({len(expr)} 字符,最大 {self.MAX_EXPRESSION_LENGTH}): {condition}"
|
||||
)
|
||||
|
||||
# 评估表达式
|
||||
try:
|
||||
evaluator = self._get_evaluator(vars_values)
|
||||
result = evaluator.eval(expr)
|
||||
return bool(result)
|
||||
|
||||
except NameNotDefined as e:
|
||||
# 变量未定义
|
||||
var_name = str(e).split("'")[1] if "'" in str(e) else "unknown"
|
||||
raise YAMLError(
|
||||
f"条件表达式中的变量未定义: {var_name}\n"
|
||||
f"表达式: {condition}\n"
|
||||
f"可用变量: {', '.join(vars_values.keys())}"
|
||||
)
|
||||
|
||||
except FunctionNotDefined as e:
|
||||
# 函数未定义
|
||||
func_name = str(e).split("'")[1] if "'" in str(e) else "unknown"
|
||||
raise YAMLError(
|
||||
f"条件表达式中使用了不支持的函数: {func_name}\n"
|
||||
f"表达式: {condition}\n"
|
||||
f"提示: 仅支持 int(), float(), str(), len(), bool(), abs(), min(), max() 等基本函数"
|
||||
)
|
||||
|
||||
except FeatureNotAvailable as e:
|
||||
# 功能不可用
|
||||
raise YAMLError(
|
||||
f"条件表达式使用了不支持的语法特性\n"
|
||||
f"表达式: {condition}\n"
|
||||
f"错误: {str(e)}\n"
|
||||
f"提示: 不支持函数定义、导入模块、属性访问等高级特性"
|
||||
)
|
||||
|
||||
except AttributeDoesNotExist as e:
|
||||
# 属性不存在(属性访问被禁止)
|
||||
raise YAMLError(
|
||||
f"条件表达式使用了不支持的语法特性\n"
|
||||
f"表达式: {condition}\n"
|
||||
f"错误: {str(e)}\n"
|
||||
f"提示: 不支持属性访问,请使用字典访问方式"
|
||||
)
|
||||
|
||||
except SyntaxError as e:
|
||||
# 语法错误
|
||||
raise YAMLError(
|
||||
f"条件表达式语法错误\n"
|
||||
f"表达式: {condition}\n"
|
||||
f"错误: {str(e)}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 其他错误
|
||||
raise YAMLError(
|
||||
f"条件表达式评估失败\n"
|
||||
f"表达式: {condition}\n"
|
||||
f"错误: {type(e).__name__}: {str(e)}"
|
||||
)
|
||||
104
core/elements.py
104
core/elements.py
@@ -5,10 +5,36 @@
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Union
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class FontConfig:
|
||||
"""字体配置数据类"""
|
||||
parent: Optional[str] = None
|
||||
family: Optional[str] = None
|
||||
size: Optional[int] = None
|
||||
bold: Optional[bool] = None
|
||||
italic: Optional[bool] = None
|
||||
underline: Optional[bool] = None
|
||||
strikethrough: Optional[bool] = None
|
||||
color: Optional[str] = None
|
||||
align: Optional[str] = None
|
||||
line_spacing: Optional[float] = None
|
||||
space_before: Optional[int] = None
|
||||
space_after: Optional[int] = None
|
||||
baseline: Optional[str] = None
|
||||
caps: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""验证枚举值"""
|
||||
if self.baseline is not None and self.baseline not in ['normal', 'superscript', 'subscript']:
|
||||
raise ValueError(f"baseline 必须是 normal、superscript 或 subscript,当前值: {self.baseline}")
|
||||
if self.caps is not None and self.caps not in ['normal', 'allcaps', 'smallcaps']:
|
||||
raise ValueError(f"caps 必须是 normal、allcaps 或 smallcaps,当前值: {self.caps}")
|
||||
|
||||
|
||||
def _is_valid_color(color: str) -> bool:
|
||||
"""
|
||||
验证颜色格式是否正确
|
||||
@@ -29,7 +55,7 @@ class TextElement:
|
||||
type: str = 'text'
|
||||
content: str = ''
|
||||
box: list = field(default_factory=lambda: [1, 1, 8, 1])
|
||||
font: dict = field(default_factory=dict)
|
||||
font: Union[FontConfig, str, dict] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
"""创建时验证"""
|
||||
@@ -41,33 +67,36 @@ class TextElement:
|
||||
from validators.result import 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']} (应为 #RRGGBB)",
|
||||
location="",
|
||||
code="INVALID_COLOR_FORMAT"
|
||||
))
|
||||
# 只在 font 是字典类型时进行验证
|
||||
# 字符串引用和 FontConfig 对象会在渲染时由 FontResolver 处理
|
||||
if isinstance(self.font, dict):
|
||||
# 检查颜色格式
|
||||
if self.font.get('color'):
|
||||
if not _is_valid_color(self.font['color']):
|
||||
issues.append(ValidationIssue(
|
||||
level="ERROR",
|
||||
message=f"无效的颜色格式: {self.font['color']} (应为 #RRGGBB)",
|
||||
location="",
|
||||
code="INVALID_COLOR_FORMAT"
|
||||
))
|
||||
|
||||
# 检查字体大小
|
||||
if self.font.get('size'):
|
||||
size = self.font['size']
|
||||
if size < 8:
|
||||
issues.append(ValidationIssue(
|
||||
level="WARNING",
|
||||
message=f"字体太小: {size}pt (建议 >= 8pt)",
|
||||
location="",
|
||||
code="FONT_TOO_SMALL"
|
||||
))
|
||||
elif size > 100:
|
||||
issues.append(ValidationIssue(
|
||||
level="WARNING",
|
||||
message=f"字体太大: {size}pt (建议 <= 100pt)",
|
||||
location="",
|
||||
code="FONT_TOO_LARGE"
|
||||
))
|
||||
# 检查字体大小
|
||||
if self.font.get('size'):
|
||||
size = self.font['size']
|
||||
if size < 8:
|
||||
issues.append(ValidationIssue(
|
||||
level="WARNING",
|
||||
message=f"字体太小: {size}pt (建议 >= 8pt)",
|
||||
location="",
|
||||
code="FONT_TOO_SMALL"
|
||||
))
|
||||
elif size > 100:
|
||||
issues.append(ValidationIssue(
|
||||
level="WARNING",
|
||||
message=f"字体太大: {size}pt (建议 <= 100pt)",
|
||||
location="",
|
||||
code="FONT_TOO_LARGE"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
@@ -83,13 +112,13 @@ class ImageElement:
|
||||
"""创建时验证"""
|
||||
if not self.src:
|
||||
raise ValueError("图片元素必须指定 src")
|
||||
if not self.box:
|
||||
raise ValueError("图片元素必须指定 box")
|
||||
if not isinstance(self.box, list) or len(self.box) != 4:
|
||||
raise ValueError("box 必须是包含 4 个数字的列表")
|
||||
|
||||
def validate(self) -> List:
|
||||
"""验证元素自身的完整性"""
|
||||
# ImageElement 的必需字段已在 __post_init__ 中检查
|
||||
# 这里返回空列表,资源验证由 ResourceValidator 负责
|
||||
return []
|
||||
|
||||
|
||||
@@ -152,6 +181,8 @@ class TableElement:
|
||||
position: list = field(default_factory=lambda: [1, 1])
|
||||
col_widths: list = field(default_factory=list)
|
||||
style: dict = field(default_factory=dict)
|
||||
font: Union[FontConfig, str, None] = None
|
||||
header_font: Union[FontConfig, str, None] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""创建时验证"""
|
||||
@@ -207,6 +238,19 @@ def create_element(elem_dict: dict):
|
||||
"""
|
||||
elem_type = elem_dict.get('type')
|
||||
|
||||
# 转换 font 字段为 FontConfig 对象(如果是字典)
|
||||
if 'font' in elem_dict and isinstance(elem_dict['font'], dict):
|
||||
elem_dict = elem_dict.copy() # 避免修改原字典
|
||||
elem_dict['font'] = FontConfig(**elem_dict['font'])
|
||||
|
||||
# 转换表格的 font 和 header_font 字段
|
||||
if elem_type == 'table':
|
||||
elem_dict = elem_dict.copy()
|
||||
if 'font' in elem_dict and isinstance(elem_dict['font'], dict):
|
||||
elem_dict['font'] = FontConfig(**elem_dict['font'])
|
||||
if 'header_font' in elem_dict and isinstance(elem_dict['header_font'], dict):
|
||||
elem_dict['header_font'] = FontConfig(**elem_dict['header_font'])
|
||||
|
||||
if elem_type == 'text':
|
||||
return TextElement(**elem_dict)
|
||||
elif elem_type == 'image':
|
||||
|
||||
@@ -5,87 +5,259 @@
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from loaders.yaml_loader import load_yaml_file, validate_presentation_yaml
|
||||
from loaders.yaml_loader import load_yaml_file, validate_presentation_yaml, validate_template_library_yaml, YAMLError
|
||||
from core.template import Template
|
||||
from core.elements import create_element
|
||||
from utils.font_resolver import FontResolver
|
||||
|
||||
|
||||
class Presentation:
|
||||
"""演示文稿类,管理整个演示文稿的生成流程"""
|
||||
|
||||
def __init__(self, pres_file, templates_dir=None):
|
||||
def __init__(self, pres_file, template_file=None):
|
||||
"""
|
||||
初始化演示文稿
|
||||
|
||||
Args:
|
||||
pres_file: 演示文稿 YAML 文件路径
|
||||
templates_dir: 模板目录
|
||||
template_file: 模板库文件路径(可选)
|
||||
"""
|
||||
self.pres_file = Path(pres_file)
|
||||
self.templates_dir = templates_dir
|
||||
self.pres_file = Path(pres_file).resolve()
|
||||
self.template_file = Path(template_file).resolve() if template_file else None
|
||||
|
||||
# 加载演示文稿文件
|
||||
self.data = load_yaml_file(pres_file)
|
||||
validate_presentation_yaml(self.data, str(pres_file))
|
||||
|
||||
# 获取演示文稿尺寸
|
||||
metadata = self.data.get('metadata', {})
|
||||
self.size = metadata.get('size', '16:9')
|
||||
# 保存文档目录和模板库目录(使用绝对路径)
|
||||
self.pres_base_dir = self.pres_file.parent
|
||||
self.template_base_dir = self.template_file.parent if self.template_file else None
|
||||
|
||||
# 模板缓存
|
||||
self.template_cache = {}
|
||||
# 获取演示文稿尺寸和描述
|
||||
metadata = self.data.get("metadata", {})
|
||||
self.size = metadata.get("size", "16:9")
|
||||
self.description = metadata.get("description") # 可选的描述字段
|
||||
|
||||
# 验证尺寸值
|
||||
if not isinstance(self.size, str):
|
||||
raise ValueError(
|
||||
f"无效的尺寸值: {self.size},尺寸必须是字符串(如 '16:9' 或 '4:3')"
|
||||
)
|
||||
|
||||
# 加载并验证模板库文件(如果提供)
|
||||
self.template_library = None
|
||||
if self.template_file:
|
||||
if not self.template_file.exists():
|
||||
raise YAMLError(f"模板库文件不存在: {self.template_file}")
|
||||
self.template_library = load_yaml_file(self.template_file)
|
||||
validate_template_library_yaml(self.template_library, str(self.template_file))
|
||||
|
||||
# size 一致性校验
|
||||
template_metadata = self.template_library.get("metadata", {})
|
||||
template_size = template_metadata.get("size")
|
||||
if self.size != template_size:
|
||||
raise YAMLError(
|
||||
f"文档尺寸 '{self.size}' 与模板库尺寸 '{template_size}' 不一致"
|
||||
)
|
||||
|
||||
# 解析字体配置
|
||||
self.fonts = metadata.get("fonts", {})
|
||||
self.fonts_default = metadata.get("fonts_default")
|
||||
|
||||
# 解析模板库字体配置
|
||||
self.template_fonts = {}
|
||||
self.template_fonts_default = None
|
||||
if self.template_library:
|
||||
template_metadata = self.template_library.get("metadata", {})
|
||||
self.template_fonts = template_metadata.get("fonts", {})
|
||||
self.template_fonts_default = template_metadata.get("fonts_default")
|
||||
|
||||
# 验证 fonts_default
|
||||
if self.fonts_default:
|
||||
# fonts_default 必须是引用格式
|
||||
if not isinstance(self.fonts_default, str) or not self.fonts_default.startswith("@"):
|
||||
raise YAMLError(
|
||||
f"fonts_default 必须是引用格式(@xxx),当前值: {self.fonts_default}"
|
||||
)
|
||||
# fonts_default 引用的配置必须存在于 fonts 或 template_fonts 中
|
||||
font_name = self.fonts_default[1:]
|
||||
if font_name not in self.fonts and font_name not in self.template_fonts:
|
||||
raise YAMLError(
|
||||
f"fonts_default 引用的字体配置不存在: {self.fonts_default}"
|
||||
)
|
||||
|
||||
# 创建字体解析器
|
||||
# 文档作用域的 FontResolver(可引用模板库字体)
|
||||
self.font_resolver_doc = FontResolver(
|
||||
fonts=self.fonts,
|
||||
fonts_default=self.fonts_default,
|
||||
scope="document",
|
||||
template_fonts=self.template_fonts
|
||||
)
|
||||
|
||||
# 模板库作用域的 FontResolver(只能引用模板库字体)
|
||||
self.font_resolver_template = FontResolver(
|
||||
fonts=self.template_fonts,
|
||||
fonts_default=self.template_fonts_default,
|
||||
scope="template",
|
||||
template_fonts=self.template_fonts
|
||||
)
|
||||
|
||||
# 解析并保存内联模板
|
||||
self.inline_templates = self.data.get('templates', {})
|
||||
|
||||
def get_template(self, template_name):
|
||||
"""
|
||||
获取模板(带缓存)
|
||||
获取模板(优先检查内联模板,检测同名冲突)
|
||||
|
||||
Args:
|
||||
template_name: 模板名称
|
||||
|
||||
Returns:
|
||||
Template 对象
|
||||
"""
|
||||
if template_name not in self.template_cache:
|
||||
self.template_cache[template_name] = Template(
|
||||
template_name, self.templates_dir
|
||||
)
|
||||
return self.template_cache[template_name]
|
||||
|
||||
Raises:
|
||||
YAMLError: 模板不存在
|
||||
"""
|
||||
# 1. 先检查内联模板
|
||||
if template_name in self.inline_templates:
|
||||
# 2. 检查外部模板是否也存在同名(WARNING)
|
||||
if self._external_template_exists(template_name):
|
||||
# 同名冲突:发出警告但继续使用内联模板
|
||||
# 注意:这里只是记录警告,实际的 WARNING 级别验证问题应该在验证器中生成
|
||||
pass
|
||||
inline_data = self.inline_templates[template_name]
|
||||
# 内联模板使用文档目录作为 base_dir,文档作用域,文档 FontResolver
|
||||
return Template.from_data(
|
||||
inline_data,
|
||||
template_name,
|
||||
base_dir=self.pres_base_dir,
|
||||
scope="document",
|
||||
font_resolver=self.font_resolver_doc
|
||||
)
|
||||
|
||||
# 3. 回退到外部模板
|
||||
if not self.template_library:
|
||||
raise YAMLError(
|
||||
f"模板不存在: {template_name}\n"
|
||||
f"提示: 该模板既不在内联模板中,也未提供外部模板库文件"
|
||||
)
|
||||
|
||||
# 从模板库字典查找
|
||||
if template_name not in self.template_library['templates']:
|
||||
raise YAMLError(
|
||||
f"模板库中找不到指定模板名称: {template_name}\n"
|
||||
f"模板库文件: {self.template_file}"
|
||||
)
|
||||
|
||||
template_data = self.template_library['templates'][template_name]
|
||||
# 外部模板使用模板库目录作为 base_dir,模板作用域,模板库 FontResolver
|
||||
return Template.from_data(
|
||||
template_data,
|
||||
template_name,
|
||||
base_dir=self.template_base_dir,
|
||||
scope="template",
|
||||
font_resolver=self.font_resolver_template
|
||||
)
|
||||
|
||||
def _external_template_exists(self, template_name):
|
||||
"""检查外部模板是否存在"""
|
||||
if not self.template_library:
|
||||
return False
|
||||
return template_name in self.template_library['templates']
|
||||
def render_slide(self, slide_data):
|
||||
"""
|
||||
渲染单个幻灯片
|
||||
渲染单个幻灯片(支持混合模式)
|
||||
|
||||
Args:
|
||||
slide_data: 幻灯片数据字典
|
||||
|
||||
Returns:
|
||||
dict: 包含 background 和 elements 的字典
|
||||
|
||||
支持三种模式:
|
||||
1. 纯模板模式:只有 template 字段
|
||||
2. 纯自定义模式:只有 elements 字段
|
||||
3. 混合模式:同时有 template 和 elements 字段
|
||||
"""
|
||||
if 'template' in slide_data:
|
||||
# 使用模板
|
||||
template_name = slide_data['template']
|
||||
has_template = "template" in slide_data
|
||||
has_custom_elements = slide_data.get("elements")
|
||||
|
||||
elements_from_template = []
|
||||
vars_values = {}
|
||||
|
||||
# 步骤1:渲染模板(如果有)
|
||||
if has_template:
|
||||
template_name = slide_data["template"]
|
||||
template = self.get_template(template_name)
|
||||
vars_values = slide_data.get('vars', {})
|
||||
elements = template.render(vars_values)
|
||||
vars_values = slide_data.get("vars", {})
|
||||
elements_from_template = template.render(vars_values)
|
||||
|
||||
# 合并背景(如果有)
|
||||
background = slide_data.get('background', None)
|
||||
# 步骤2:处理自定义元素(如果有)
|
||||
elements_from_custom = []
|
||||
if has_custom_elements:
|
||||
custom_elements = slide_data["elements"]
|
||||
if has_template:
|
||||
# 混合模式:使用模板变量解析自定义元素
|
||||
template = self.get_template(slide_data["template"])
|
||||
elements_from_custom = [
|
||||
template.resolve_element(elem, vars_values)
|
||||
for elem in custom_elements
|
||||
]
|
||||
else:
|
||||
# 纯自定义模式(原有行为)
|
||||
elements_from_custom = custom_elements
|
||||
|
||||
# 将元素字典转换为元素对象
|
||||
element_objects = [create_element(elem) for elem in elements]
|
||||
# 解析自定义元素的字体引用(使用文档 FontResolver)
|
||||
for elem in elements_from_custom:
|
||||
if isinstance(elem, dict):
|
||||
# 处理普通元素的 font 字段
|
||||
if 'font' in elem:
|
||||
font_config = elem['font']
|
||||
if isinstance(font_config, (str, dict)):
|
||||
try:
|
||||
resolved_font = self.font_resolver_doc.resolve_font(font_config)
|
||||
elem['font'] = resolved_font
|
||||
except ValueError as e:
|
||||
raise YAMLError(f"字体解析失败: {str(e)}")
|
||||
elif elem.get('type') in ['text', 'table']:
|
||||
# 元素未定义 font,使用 fonts_default(如果有)
|
||||
if self.fonts_default:
|
||||
try:
|
||||
resolved_font = self.font_resolver_doc.resolve_font(None)
|
||||
elem['font'] = resolved_font
|
||||
except ValueError as e:
|
||||
raise YAMLError(f"字体解析失败: {str(e)}")
|
||||
|
||||
return {
|
||||
'background': background,
|
||||
'elements': element_objects
|
||||
}
|
||||
else:
|
||||
# 自定义幻灯片
|
||||
elements = slide_data.get('elements', [])
|
||||
# 处理表格元素的 header_font 字段
|
||||
if elem.get('type') == 'table' and 'header_font' in elem:
|
||||
header_font_config = elem['header_font']
|
||||
if isinstance(header_font_config, (str, dict)):
|
||||
try:
|
||||
resolved_header_font = self.font_resolver_doc.resolve_font(header_font_config)
|
||||
elem['header_font'] = resolved_header_font
|
||||
except ValueError as e:
|
||||
raise YAMLError(f"表格 header_font 解析失败: {str(e)}")
|
||||
|
||||
# 将元素字典转换为元素对象
|
||||
element_objects = [create_element(elem) for elem in elements]
|
||||
# 解析自定义元素的图片路径(相对于文档目录)
|
||||
for elem in elements_from_custom:
|
||||
if isinstance(elem, dict) and elem.get('type') == 'image':
|
||||
src = elem.get('src')
|
||||
if src:
|
||||
src_path = Path(src)
|
||||
# 只处理相对路径
|
||||
if not src_path.is_absolute():
|
||||
resolved_path = self.pres_base_dir / src
|
||||
elem['src'] = str(resolved_path)
|
||||
|
||||
return {
|
||||
'background': slide_data.get('background'),
|
||||
'elements': element_objects
|
||||
}
|
||||
# 步骤3:合并元素(模板元素在前,自定义元素在后)
|
||||
final_elements = elements_from_template + elements_from_custom
|
||||
|
||||
# 步骤4:转换为元素对象
|
||||
element_objects = [create_element(elem) for elem in final_elements]
|
||||
|
||||
return {
|
||||
"background": slide_data.get("background"),
|
||||
"elements": element_objects,
|
||||
"description": slide_data.get("description"), # 保留幻灯片描述
|
||||
}
|
||||
|
||||
120
core/template.py
120
core/template.py
@@ -7,6 +7,7 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
from loaders.yaml_loader import YAMLError, load_yaml_file, validate_template_yaml
|
||||
from core.condition_evaluator import ConditionEvaluator
|
||||
|
||||
|
||||
class Template:
|
||||
@@ -20,6 +21,9 @@ class Template:
|
||||
template_file: 模板名称(纯文件名,不含路径)
|
||||
templates_dir: 模板文件目录
|
||||
"""
|
||||
# 初始化条件评估器
|
||||
self._condition_evaluator = ConditionEvaluator()
|
||||
|
||||
# 检查是否提供了 templates_dir
|
||||
if templates_dir is None:
|
||||
raise YAMLError(
|
||||
@@ -50,6 +54,9 @@ class Template:
|
||||
self.data = load_yaml_file(template_path)
|
||||
validate_template_yaml(self.data, str(template_path))
|
||||
|
||||
# 可选的描述字段
|
||||
self.description = self.data.get('description')
|
||||
|
||||
# 解析变量定义
|
||||
self.vars_def = {}
|
||||
for var in self.data.get('vars', []):
|
||||
@@ -58,6 +65,49 @@ class Template:
|
||||
# 元素列表
|
||||
self.elements = self.data.get('elements', [])
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, template_data, template_name, base_dir=None, scope="document", font_resolver=None):
|
||||
"""从字典创建模板(内联模板或外部模板)
|
||||
|
||||
Args:
|
||||
template_data: 模板数据字典
|
||||
template_name: 模板名称
|
||||
base_dir: 资源路径解析的基础目录(外部模板使用模板库文件所在目录,内联模板使用文档目录)
|
||||
scope: 作用域("document" 或 "template")
|
||||
font_resolver: 字体解析器(用于解析字体引用)
|
||||
|
||||
Returns:
|
||||
Template 对象
|
||||
"""
|
||||
obj = cls.__new__(cls)
|
||||
obj.data = template_data
|
||||
obj.base_dir = base_dir # 保存 base_dir 用于资源路径解析
|
||||
obj.scope = scope # 保存作用域
|
||||
obj.font_resolver = font_resolver # 保存字体解析器
|
||||
|
||||
# 初始化条件评估器
|
||||
obj._condition_evaluator = ConditionEvaluator()
|
||||
|
||||
# 可选的描述字段
|
||||
obj.description = template_data.get('description')
|
||||
|
||||
# 解析变量定义
|
||||
obj.vars_def = {}
|
||||
for var in template_data.get('vars', []):
|
||||
obj.vars_def[var['name']] = var
|
||||
|
||||
# 元素列表
|
||||
obj.elements = template_data.get('elements', [])
|
||||
|
||||
return obj
|
||||
|
||||
def _external_template_exists(self, template_name):
|
||||
"""检查外部模板文件是否存在"""
|
||||
if not hasattr(self, 'templates_dir') or not self.templates_dir:
|
||||
return False
|
||||
template_path = Path(self.templates_dir) / f"{template_name}.yaml"
|
||||
return template_path.exists()
|
||||
|
||||
def resolve_value(self, value, vars_values):
|
||||
"""
|
||||
解析单个值中的变量引用
|
||||
@@ -122,27 +172,17 @@ class Template:
|
||||
|
||||
def evaluate_condition(self, condition, vars_values):
|
||||
"""
|
||||
评估条件表达式(简单的存在性检查)
|
||||
评估条件表达式
|
||||
|
||||
Args:
|
||||
condition: 条件字符串,如 "{subtitle != ''}"
|
||||
condition: 条件字符串,如 "{count > 0 and status == 'active'}"
|
||||
vars_values: 变量值字典
|
||||
|
||||
Returns:
|
||||
bool: 条件是否满足
|
||||
"""
|
||||
# 简单实现:检查变量是否非空
|
||||
# 匹配 {var_name != ''} 或 {var_name != ""}
|
||||
pattern = r'\{(\w+)\s*!=\s*[\'\"]{2}\}'
|
||||
match = re.match(pattern, condition)
|
||||
|
||||
if match:
|
||||
var_name = match.group(1)
|
||||
value = vars_values.get(var_name, '')
|
||||
return value != ''
|
||||
|
||||
# 默认返回 True
|
||||
return True
|
||||
# 委托给条件评估器
|
||||
return self._condition_evaluator.evaluate_condition(condition, vars_values)
|
||||
|
||||
def render(self, vars_values):
|
||||
"""
|
||||
@@ -155,8 +195,16 @@ class Template:
|
||||
list: 渲染后的元素列表
|
||||
|
||||
Raises:
|
||||
YAMLError: 缺少必需变量
|
||||
YAMLError: 缺少必需变量,内联模板相互引用
|
||||
"""
|
||||
# 检测内联模板相互引用(禁止)
|
||||
for elem in self.elements:
|
||||
if isinstance(elem, dict) and 'template' in elem:
|
||||
raise YAMLError(
|
||||
f"内联模板不支持相互引用:元素中包含 'template' 字段\n"
|
||||
f"提示: 内联模板只能包含元素定义,不能引用其他模板"
|
||||
)
|
||||
|
||||
# 填充所有变量的默认值(如果用户未提供)
|
||||
for var_name, var_def in self.vars_def.items():
|
||||
if var_name not in vars_values:
|
||||
@@ -186,6 +234,48 @@ class Template:
|
||||
|
||||
# 深度解析元素中的所有变量引用
|
||||
rendered_elem = self.resolve_element(elem, vars_values)
|
||||
|
||||
# 解析字体引用(如果有 font_resolver)
|
||||
if self.font_resolver and isinstance(rendered_elem, dict):
|
||||
# 处理普通元素的 font 字段
|
||||
if 'font' in rendered_elem:
|
||||
font_config = rendered_elem['font']
|
||||
# 如果是字符串引用或字典,使用 FontResolver 解析
|
||||
if isinstance(font_config, (str, dict)):
|
||||
try:
|
||||
resolved_font = self.font_resolver.resolve_font(font_config)
|
||||
rendered_elem['font'] = resolved_font
|
||||
except ValueError as e:
|
||||
raise YAMLError(f"字体解析失败: {str(e)}")
|
||||
elif rendered_elem.get('type') in ['text', 'table']:
|
||||
# 元素未定义 font,使用 fonts_default(如果有)
|
||||
if self.font_resolver.fonts_default:
|
||||
try:
|
||||
resolved_font = self.font_resolver.resolve_font(None)
|
||||
rendered_elem['font'] = resolved_font
|
||||
except ValueError as e:
|
||||
raise YAMLError(f"字体解析失败: {str(e)}")
|
||||
|
||||
# 处理表格元素的 header_font 字段
|
||||
if rendered_elem.get('type') == 'table' and 'header_font' in rendered_elem:
|
||||
header_font_config = rendered_elem['header_font']
|
||||
if isinstance(header_font_config, (str, dict)):
|
||||
try:
|
||||
resolved_header_font = self.font_resolver.resolve_font(header_font_config)
|
||||
rendered_elem['header_font'] = resolved_header_font
|
||||
except ValueError as e:
|
||||
raise YAMLError(f"表格 header_font 解析失败: {str(e)}")
|
||||
|
||||
# 如果是图片元素且有相对路径,解析为绝对路径
|
||||
if isinstance(rendered_elem, dict) and rendered_elem.get('type') == 'image':
|
||||
src = rendered_elem.get('src')
|
||||
if src and self.base_dir:
|
||||
src_path = Path(src)
|
||||
# 只处理相对路径
|
||||
if not src_path.is_absolute():
|
||||
resolved_path = Path(self.base_dir) / src
|
||||
rendered_elem['src'] = str(resolved_path)
|
||||
|
||||
rendered_elements.append(rendered_elem)
|
||||
|
||||
return rendered_elements
|
||||
|
||||
48
docs/README.md
Normal file
48
docs/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# yaml2pptx 文档
|
||||
|
||||
欢迎使用 yaml2pptx 文档!本目录包含项目的完整文档。
|
||||
|
||||
## 用户文档
|
||||
|
||||
### 快速开始
|
||||
- [用户指南](user-guide.md) - 完整的使用指南
|
||||
|
||||
### 元素类型
|
||||
- [文本元素](elements/text.md) - 文本框和字体配置
|
||||
- [图片元素](elements/image.md) - 图片插入和配置
|
||||
- [形状元素](elements/shape.md) - 几何形状绘制
|
||||
- [表格元素](elements/table.md) - 表格创建和样式
|
||||
|
||||
### 模板系统
|
||||
- [内联模板](templates/inline.md) - 在源文件中定义模板
|
||||
- [外部模板库](templates/external-library.md) - 独立的模板库文件
|
||||
- [混合模式](templates/mixing-mode.md) - 模板与自定义元素组合
|
||||
- [条件渲染](templates/condition-rendering.md) - 元素和页面的条件显示
|
||||
|
||||
### 其他主题
|
||||
- [字体主题系统](fonts.md) - 字体配置和主题管理
|
||||
- [示例集合](examples.md) - 实战示例
|
||||
- [故障排查](troubleshooting.md) - 常见问题解决
|
||||
|
||||
### API 参考
|
||||
- [命令行选项](reference/commands.md) - 所有命令和参数
|
||||
- [坐标系统](reference/coordinates.md) - 位置和尺寸单位
|
||||
- [颜色格式](reference/colors.md) - 颜色表示方法
|
||||
- [验证功能](reference/validation.md) - YAML 验证说明
|
||||
|
||||
## 开发文档
|
||||
|
||||
详见 [development/](development/) 目录。
|
||||
|
||||
- [架构设计](development/architecture.md) - 代码结构和技术决策
|
||||
- [模块详解](development/modules/) - 各模块详细说明
|
||||
- [字体系统](development/font-system.md) - 字体解析实现
|
||||
- [作用域系统](development/scope-system.md) - 字体作用域规则
|
||||
- [开发规范](development/development-guide.md) - 编码规范和约定
|
||||
- [扩展指南](development/extending.md) - 添加新功能
|
||||
- [测试规范](development/testing.md) - 测试指南
|
||||
- [维护指南](development/maintenance.md) - 代码审查和优化
|
||||
|
||||
---
|
||||
|
||||
返回 [项目主页](../README.md)
|
||||
193
docs/development/architecture.md
Normal file
193
docs/development/architecture.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# 架构设计
|
||||
|
||||
本文档说明 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
|
||||
```
|
||||
|
||||
### 依赖原则
|
||||
|
||||
- **单向依赖**:入口 → 验证/渲染 → 核心 ← 加载
|
||||
- **无循环依赖**
|
||||
- **核心层不依赖**其他业务模块
|
||||
- **验证层可以调用**核心层的元素验证方法
|
||||
|
||||
## 模块概览
|
||||
|
||||
### 入口层 (yaml2pptx.py)
|
||||
|
||||
- **职责**:CLI 参数解析、流程编排
|
||||
- **包含**:
|
||||
- `/// script` 依赖声明
|
||||
- `parse_args()` - 命令行参数解析
|
||||
- `main()` - 主流程编排
|
||||
- `handle_convert()` - 转换流程
|
||||
- **不包含**:业务逻辑、数据处理
|
||||
|
||||
### 工具层 (utils/)
|
||||
|
||||
- **职责**:通用工具函数
|
||||
- **包含**:
|
||||
- 日志函数:`log_info()`, `log_success()`, `log_error()`, `log_progress()`
|
||||
- 颜色转换:`hex_to_rgb()`, `validate_color()`
|
||||
|
||||
### 加载层 (loaders/)
|
||||
|
||||
- **职责**:YAML 文件加载和验证
|
||||
- **包含**:
|
||||
- `YAMLError` - 自定义异常
|
||||
- `load_yaml_file()` - 加载 YAML 文件
|
||||
- `validate_presentation_yaml()` - 验证演示文稿结构
|
||||
|
||||
### 核心层 (core/)
|
||||
|
||||
#### elements.py
|
||||
|
||||
- **职责**:定义元素数据类和工厂函数
|
||||
- **包含**:
|
||||
- `FontConfig` - 字体配置数据类
|
||||
- `TextElement`, `ImageElement`, `ShapeElement`, `TableElement`
|
||||
- `create_element()` - 元素工厂函数
|
||||
|
||||
#### template.py
|
||||
|
||||
- **职责**:模板加载和变量解析
|
||||
- **包含**:
|
||||
- `Template` 类
|
||||
- 变量解析:`resolve_value()`, `resolve_element()`
|
||||
- 条件渲染:`evaluate_condition()`
|
||||
- 模板渲染:`render()`
|
||||
|
||||
#### condition_evaluator.py
|
||||
|
||||
- **职责**:安全地评估条件表达式
|
||||
- **包含**:
|
||||
- `ConditionEvaluator` 类
|
||||
- 使用 simpleeval 库进行安全评估
|
||||
|
||||
#### presentation.py
|
||||
|
||||
- **职责**:演示文稿管理和幻灯片渲染
|
||||
- **包含**:
|
||||
- `Presentation` 类
|
||||
- 内联模板存储和查找
|
||||
- 幻灯片渲染:`render_slide()`
|
||||
|
||||
### 验证层 (validators/)
|
||||
|
||||
- **职责**:YAML 文件验证
|
||||
- **包含**:
|
||||
- `ValidationIssue`, `ValidationResult` - 验证结果结构
|
||||
- `GeometryValidator` - 几何验证器
|
||||
- `ResourceValidator` - 资源验证器
|
||||
- `Validator` - 主验证器
|
||||
|
||||
### 渲染层 (renderers/)
|
||||
|
||||
#### pptx_renderer.py
|
||||
|
||||
- **职责**:PPTX 文件生成
|
||||
- **包含**:
|
||||
- `PptxGenerator` 类
|
||||
- 渲染方法:`_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()`
|
||||
|
||||
#### html_renderer.py
|
||||
|
||||
- **职责**:HTML 预览渲染
|
||||
- **包含**:
|
||||
- `HtmlRenderer` 类
|
||||
- 渲染方法:`render_text()`, `render_image()`, `render_shape()`, `render_table()`
|
||||
|
||||
### 预览层 (preview/)
|
||||
|
||||
- **职责**:浏览器预览和热重载
|
||||
- **包含**:
|
||||
- Flask 应用:`create_flask_app()`
|
||||
- 文件监听:`YAMLChangeHandler`
|
||||
- 预览服务器:`start_preview_server()`
|
||||
|
||||
## 技术决策概要
|
||||
|
||||
### 1. 元素抽象层使用 dataclass
|
||||
|
||||
- **理由**:简洁性、类型提示、创建时验证
|
||||
- **实现**:`@dataclass` 装饰器 + `__post_init__` 验证
|
||||
|
||||
### 2. 创建时验证
|
||||
|
||||
- **理由**:尽早失败、清晰错误位置
|
||||
- **实现**:在 `__post_init__` 中进行验证
|
||||
|
||||
### 3. 验证职责分层
|
||||
|
||||
- **元素级验证**:放在元素类本身
|
||||
- **系统级验证**:放在独立的验证器模块
|
||||
|
||||
### 4. 模板系统架构
|
||||
|
||||
- **内联模板**:在源文件中定义,适合简单场景
|
||||
- **外部模板库**:独立文件,适合复用和共享
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [模块详解](modules/) - 各模块详细说明
|
||||
- [开发规范](development-guide.md) - 编码规范和约定
|
||||
- [扩展指南](extending.md) - 添加新功能
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
132
docs/development/development-guide.md
Normal file
132
docs/development/development-guide.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 开发规范
|
||||
|
||||
本文档说明 yaml2pptx 项目的开发规范和约定。
|
||||
|
||||
## 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`
|
||||
|
||||
## 命令行接口
|
||||
|
||||
### 子命令架构
|
||||
|
||||
```bash
|
||||
# check - 验证 YAML 文件
|
||||
uv run yaml2pptx.py check <input> [--template <path>]
|
||||
|
||||
# convert - 转换为 PPTX
|
||||
uv run yaml2pptx.py convert <input> [output] [--template <path>] [--skip-validation] [--force]
|
||||
|
||||
# preview - 启动预览服务器
|
||||
uv run yaml2pptx.py preview <input> [--template <path>] [--port <port>] [--host <host>] [--no-browser]
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
- `--template`:指定模板库文件路径
|
||||
- `--skip-validation`:convert 专用,跳过自动验证
|
||||
- `--force/-f`:convert 专用,强制覆盖已存在文件
|
||||
- `--port`:preview 专用,指定端口
|
||||
- `--host`:preview 专用,指定主机地址
|
||||
- `--no-browser`:preview 专用,不自动打开浏览器
|
||||
|
||||
## 文件组织
|
||||
|
||||
### 代码文件
|
||||
|
||||
- 每个模块文件控制在 150-300 行
|
||||
- 入口脚本约 200 行
|
||||
- 使用有意义的文件名和目录结构
|
||||
|
||||
### 测试文件
|
||||
|
||||
- 所有测试文件、临时文件必须放在 `temp/` 目录下
|
||||
- 不污染项目根目录
|
||||
|
||||
## 代码风格
|
||||
|
||||
### 导入顺序
|
||||
|
||||
```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. **面向中文开发者**:注释、文档、错误消息使用中文
|
||||
2. **使用 uv 运行**:严禁直接使用主机环境的 python
|
||||
3. **测试文件隔离**:所有测试文件放在 `temp/` 目录
|
||||
4. **不污染主机环境**:不修改主机的 Python 配置
|
||||
|
||||
## 验证容忍度
|
||||
|
||||
几何验证时,允许 0.1 英寸的容忍度:
|
||||
|
||||
```python
|
||||
TOLERANCE = 0.1 # 英寸
|
||||
|
||||
if right > slide_width + TOLERANCE:
|
||||
# 报告 WARNING
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 模块职责
|
||||
|
||||
- 每个模块职责明确,单一功能
|
||||
- 模块间通过清晰接口通信
|
||||
- 避免循环依赖
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 使用自定义异常类(如 `YAMLError`)
|
||||
- 提供清晰的错误信息
|
||||
- 包含错误位置(文件、行号、元素)
|
||||
|
||||
### 日志
|
||||
|
||||
- 使用统一的日志函数(`log_info()`, `log_success()`, `log_error()`)
|
||||
- 显示友好的中文错误信息
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构设计](architecture.md) - 代码结构
|
||||
- [扩展指南](extending.md) - 添加新功能
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
280
docs/development/extending.md
Normal file
280
docs/development/extending.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# 扩展指南
|
||||
|
||||
本文档说明如何扩展 yaml2pptx 的功能。
|
||||
|
||||
## 添加新元素类型
|
||||
|
||||
假设要添加 `VideoElement`:
|
||||
|
||||
### 1. 在 core/elements.py 中定义数据类
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@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')
|
||||
|
||||
if elem_type == 'text':
|
||||
return TextElement(**elem_dict)
|
||||
elif elem_type == 'image':
|
||||
return ImageElement(**elem_dict)
|
||||
# ... 其他类型 ...
|
||||
elif elem_type == 'video':
|
||||
return VideoElement(**elem_dict)
|
||||
else:
|
||||
raise ValueError(f"Unknown element type: {elem_type}")
|
||||
```
|
||||
|
||||
### 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):
|
||||
"""实现视频渲染逻辑"""
|
||||
movie = slide.shapes.add_movie(
|
||||
str(Path(base_path) / elem.src),
|
||||
left=Inches(elem.box[0]),
|
||||
top=Inches(elem.box[1]),
|
||||
width=Inches(elem.box[2]),
|
||||
height=Inches(elem.box[3])
|
||||
)
|
||||
|
||||
if elem.autoplay:
|
||||
movie.click.action = pp.action.Action(pyppote.xmlns.namespace('p').MSO_ANIMATION_VIDEO_CLICK)
|
||||
```
|
||||
|
||||
### 4. 在 HtmlRenderer 中实现渲染方法
|
||||
|
||||
```python
|
||||
def render_slide(self, slide_data, index, base_path=None):
|
||||
elements_html = ""
|
||||
|
||||
for elem in slide_data:
|
||||
# ... 其他类型 ...
|
||||
elif isinstance(elem, VideoElement):
|
||||
elements_html += self.render_video(elem, base_path)
|
||||
|
||||
return self.SLIDE_TEMPLATE.format(content=elements_html)
|
||||
|
||||
def render_video(self, elem: VideoElement, base_path):
|
||||
"""实现 HTML 视频渲染"""
|
||||
src_path = str(Path(base_path) / elem.src) if base_path else elem.src
|
||||
autoplay_attr = "autoplay" if elem.autoplay else ""
|
||||
|
||||
return f'''
|
||||
<video src="{src_path}" {autoplay_attr}
|
||||
style="position:absolute; left:{elem.box[0]*96}px; top:{elem.box[1]*96}px;
|
||||
width:{elem.box[2]*96}px; height:{elem.box[3]*96}px;">
|
||||
</video>
|
||||
'''
|
||||
```
|
||||
|
||||
### 5. 更新验证器
|
||||
|
||||
如果需要验证视频文件:
|
||||
|
||||
```python
|
||||
# validators/resource.py
|
||||
def validate_video(self, src, slide_index, elem_index):
|
||||
"""检查视频文件是否存在"""
|
||||
video_path = self.base_dir / src
|
||||
if not video_path.exists():
|
||||
return ValidationIssue(
|
||||
level="ERROR",
|
||||
message=f"视频文件不存在: {src}",
|
||||
location=f"[幻灯片 {slide_index + 1}, 元素 {elem_index + 1}]",
|
||||
code="VIDEO_FILE_NOT_FOUND"
|
||||
)
|
||||
return None
|
||||
```
|
||||
|
||||
## 添加新渲染器
|
||||
|
||||
假设要添加 PDF 渲染器:
|
||||
|
||||
### 1. 创建 renderers/pdf_renderer.py
|
||||
|
||||
```python
|
||||
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
|
||||
|
||||
class PdfRenderer:
|
||||
def __init__(self, size="16:9"):
|
||||
# 初始化 PDF 库
|
||||
self.size = size
|
||||
# ...
|
||||
|
||||
def add_slide(self, slide_data, base_path=None):
|
||||
"""添加页面"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def _render_element(self, page, elem, base_path):
|
||||
"""渲染元素到 PDF 页面"""
|
||||
if isinstance(elem, TextElement):
|
||||
self._render_text(page, elem)
|
||||
elif isinstance(elem, ImageElement):
|
||||
self._render_image(page, elem, base_path)
|
||||
elif isinstance(elem, ShapeElement):
|
||||
self._render_shape(page, elem)
|
||||
elif isinstance(elem, TableElement):
|
||||
self._render_table(page, elem)
|
||||
|
||||
def _render_text(self, page, elem):
|
||||
"""渲染文本到 PDF"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def _render_image(self, page, elem, base_path):
|
||||
"""渲染图片到 PDF"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def _render_shape(self, page, elem):
|
||||
"""渲染形状到 PDF"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def _render_table(self, page, elem):
|
||||
"""渲染表格到 PDF"""
|
||||
# 实现...
|
||||
pass
|
||||
|
||||
def save(self, output_path):
|
||||
"""保存 PDF 文件"""
|
||||
# 实现...
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. 在 yaml2pptx.py 中添加 PDF 模式
|
||||
|
||||
```python
|
||||
from renderers.pdf_renderer import PdfRenderer
|
||||
|
||||
def main():
|
||||
# ... 解析参数 ...
|
||||
if args.pdf:
|
||||
# PDF 生成模式
|
||||
generator = PdfRenderer(size=args.size)
|
||||
# ... 渲染逻辑
|
||||
```
|
||||
|
||||
### 3. 添加命令行参数
|
||||
|
||||
```python
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='YAML to PPTX converter')
|
||||
subparsers = parser.add_subparsers(dest='command', help='子命令')
|
||||
|
||||
# ... 其他命令 ...
|
||||
|
||||
# PDF 命令
|
||||
pdf_parser = subparsers.add_parser('pdf', help='生成 PDF')
|
||||
pdf_parser.add_argument('input', help='输入的 YAML 文件')
|
||||
pdf_parser.add_argument('output', help='输出的 PDF 文件', nargs='?')
|
||||
pdf_parser.add_argument('--template', help='模板库文件路径')
|
||||
pdf_parser.add_argument('--size', default='16:9', choices=['16:9', '4:3'])
|
||||
|
||||
return parser.parse_args()
|
||||
```
|
||||
|
||||
## 添加新的验证规则
|
||||
|
||||
### 1. 在 validators/ 中创建新的验证器
|
||||
|
||||
```python
|
||||
# validators/custom.py
|
||||
|
||||
class CustomValidator:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def validate(self, presentation):
|
||||
"""执行自定义验证"""
|
||||
issues = []
|
||||
# 验证逻辑
|
||||
return issues
|
||||
```
|
||||
|
||||
### 2. 在主验证器中集成
|
||||
|
||||
```python
|
||||
# validators/validator.py
|
||||
|
||||
class Validator:
|
||||
def __init__(self, ...):
|
||||
# ...
|
||||
self.custom_validator = CustomValidator()
|
||||
|
||||
def validate_presentation(self, presentation):
|
||||
# ...
|
||||
# 调用自定义验证器
|
||||
custom_issues = self.custom_validator.validate(presentation)
|
||||
result.infos.extend(custom_issues)
|
||||
```
|
||||
|
||||
## 测试新功能
|
||||
|
||||
### 1. 创建测试文件
|
||||
|
||||
```python
|
||||
# tests/unit/test_video_element.py
|
||||
|
||||
import pytest
|
||||
from core.elements import VideoElement, create_element
|
||||
|
||||
def test_create_video_element():
|
||||
elem_dict = {
|
||||
'type': 'video',
|
||||
'src': 'test.mp4',
|
||||
'box': [1, 1, 4, 3],
|
||||
'autoplay': True
|
||||
}
|
||||
elem = create_element(elem_dict)
|
||||
assert isinstance(elem, VideoElement)
|
||||
assert elem.autoplay is True
|
||||
|
||||
def test_video_element_without_src():
|
||||
with pytest.raises(ValueError, match="必须指定 src"):
|
||||
VideoElement(src='', box=[1, 1, 4, 3])
|
||||
```
|
||||
|
||||
### 2. 运行测试
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit/test_video_element.py -v
|
||||
```
|
||||
|
||||
## 提交变更
|
||||
|
||||
1. 更新相关文档
|
||||
2. 添加测试
|
||||
3. 运行完整测试套件
|
||||
4. 提交 Pull Request
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构设计](architecture.md) - 代码结构
|
||||
- [Elements 模块](modules/elements.md) - 元素抽象层
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
144
docs/development/font-system.md
Normal file
144
docs/development/font-system.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 字体系统实现
|
||||
|
||||
字体解析和继承链处理的实现。
|
||||
|
||||
## 职责
|
||||
|
||||
- 字体引用解析
|
||||
- 继承链处理
|
||||
- 预设类别映射
|
||||
- 循环引用检测
|
||||
|
||||
## PRESET_FONT_MAPPING
|
||||
|
||||
预设字体类别映射常量:
|
||||
|
||||
```python
|
||||
PRESET_FONT_MAPPING = {
|
||||
'sans': 'Arial',
|
||||
'serif': 'Times New Roman',
|
||||
'mono': 'Courier New',
|
||||
'cjk-sans': 'Microsoft YaHei',
|
||||
'cjk-serif': 'SimSun',
|
||||
}
|
||||
```
|
||||
|
||||
## FontResolver 类
|
||||
|
||||
### 初始化
|
||||
|
||||
```python
|
||||
class FontResolver:
|
||||
def __init__(self, fonts, fonts_default, scope="document", template_fonts=None):
|
||||
"""
|
||||
Args:
|
||||
fonts: 当前作用域的字体字典
|
||||
fonts_default: 当前作用域的默认字体
|
||||
scope: 作用域标识 ("document" 或 "template")
|
||||
template_fonts: 模板库字体字典(仅文档作用域需要)
|
||||
"""
|
||||
```
|
||||
|
||||
### resolve_font()
|
||||
|
||||
解析字体配置的主入口:
|
||||
|
||||
```python
|
||||
def resolve_font(self, font_config):
|
||||
"""解析字体配置(主入口)"""
|
||||
if font_config is None:
|
||||
return self._get_default_config(set())
|
||||
|
||||
if isinstance(font_config, str):
|
||||
# "@xxx" 格式
|
||||
return self._resolve_reference(font_config.strip('@'), set())
|
||||
|
||||
if isinstance(font_config, dict):
|
||||
if 'parent' in font_config:
|
||||
# {parent: "@xxx", ...} 格式
|
||||
return self._resolve_font_dict(font_config, set())
|
||||
else:
|
||||
# 独立定义
|
||||
return FontConfig(**font_config)
|
||||
```
|
||||
|
||||
### _resolve_reference()
|
||||
|
||||
解析字体引用:
|
||||
|
||||
```python
|
||||
def _resolve_reference(self, reference, visited):
|
||||
"""解析字体引用"""
|
||||
if reference in visited:
|
||||
raise ValueError(f"检测到字体引用循环: {' -> '.join(visited + [reference])}")
|
||||
|
||||
visited.add(reference)
|
||||
|
||||
# 检查作用域
|
||||
if reference in self.fonts:
|
||||
font_data = self.fonts[reference]
|
||||
elif self.template_fonts and reference in self.template_fonts:
|
||||
font_data = self.template_fonts[reference]
|
||||
else:
|
||||
raise ValueError(f"字体配置不存在: @{reference}")
|
||||
|
||||
return self._build_font_config(font_data, visited)
|
||||
```
|
||||
|
||||
### _resolve_font_dict()
|
||||
|
||||
解析字体字典:
|
||||
|
||||
```python
|
||||
def _resolve_font_dict(self, font_dict, visited):
|
||||
"""解析字体字典"""
|
||||
parent_ref = font_dict.get('parent', '').strip('@')
|
||||
if parent_ref:
|
||||
# 先解析父级
|
||||
parent_config = self._resolve_reference(parent_ref, visited.copy())
|
||||
# 合并当前属性
|
||||
return self._merge_with_parent(font_dict, parent_config)
|
||||
|
||||
# 独立定义
|
||||
return FontConfig(**font_dict)
|
||||
```
|
||||
|
||||
## 字体引用解析逻辑
|
||||
|
||||
1. 如果 `font_config` 为 None,使用 `fonts_default`
|
||||
2. 如果是字符串(`"@xxx"`),解析为整体引用
|
||||
3. 如果是字典且包含 `parent`,先解析 `parent` 再覆盖当前属性
|
||||
4. 如果是字典且不包含 `parent`,直接使用字典属性
|
||||
5. 未定义的属性从 `fonts_default` 继承
|
||||
6. 如果 `family` 是预设类别,映射到具体字体名称
|
||||
7. 返回完整的 `FontConfig` 对象
|
||||
|
||||
## 继承链
|
||||
|
||||
属性继承顺序:
|
||||
|
||||
```
|
||||
parent → 当前属性 → fonts_default → 系统默认
|
||||
```
|
||||
|
||||
## 循环引用检测
|
||||
|
||||
- 维护已访问集合 `visited`
|
||||
- 检测重复引用
|
||||
- 最大引用深度限制:10 层
|
||||
|
||||
## 预设类别映射
|
||||
|
||||
在 `family` 字段中识别预设类别名称:
|
||||
|
||||
```python
|
||||
if family in PRESET_FONT_MAPPING:
|
||||
family = PRESET_FONT_MAPPING[family]
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [作用域系统](scope-system.md) - 字体作用域规则
|
||||
- [字体主题系统](../../fonts.md) - 用户指南
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
172
docs/development/maintenance.md
Normal file
172
docs/development/maintenance.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 维护指南
|
||||
|
||||
本文档提供 yaml2pptx 项目的维护指南。
|
||||
|
||||
## 代码审查要点
|
||||
|
||||
在审查代码时,检查以下要点:
|
||||
|
||||
### 结构和设计
|
||||
|
||||
- [ ] 模块文件大小合理(150-300 行)
|
||||
- [ ] 无循环依赖
|
||||
- [ ] 单一职责原则
|
||||
- [ ] 清晰的接口和抽象
|
||||
|
||||
### 代码质量
|
||||
|
||||
- [ ] 所有类和函数有文档字符串
|
||||
- [ ] 使用中文注释
|
||||
- [ ] 元素验证在 `__post_init__` 中完成
|
||||
- [ ] 导入语句按标准库、第三方库、本地模块排序
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] 有相应的测试用例
|
||||
- [ ] 测试覆盖率足够
|
||||
- [ ] 测试文件在 `tests/` 目录下
|
||||
|
||||
### 约束检查
|
||||
|
||||
- [ ] 使用 `uv run` 运行脚本
|
||||
- [ ] 测试文件在 `temp/` 目录
|
||||
- [ ] 面向中文开发者
|
||||
- [ ] 不污染主机环境
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 模板缓存
|
||||
|
||||
Presentation 类已实现模板缓存,避免重复加载:
|
||||
|
||||
```python
|
||||
def get_template(self, template_name):
|
||||
if template_name not in self.template_cache:
|
||||
self.template_cache[template_name] = self._load_template(template_name)
|
||||
return self.template_cache[template_name]
|
||||
```
|
||||
|
||||
### 2. 元素验证
|
||||
|
||||
只在创建时验证一次,渲染时不再验证:
|
||||
|
||||
```python
|
||||
# 创建时验证
|
||||
elem = TextElement(**elem_dict) # __post_init__ 验证
|
||||
|
||||
# 渲染时直接使用,不重复验证
|
||||
self._render_text(slide, elem)
|
||||
```
|
||||
|
||||
### 3. 文件监听
|
||||
|
||||
预览模式使用 watchdog 高效监听文件变化:
|
||||
|
||||
```python
|
||||
class YAMLChangeHandler(FileSystemEventHandler):
|
||||
def on_modified(self, event):
|
||||
if event.src_path.endswith('.yaml'):
|
||||
# 重新加载和渲染
|
||||
```
|
||||
|
||||
## 代码质量工具
|
||||
|
||||
### 格式化
|
||||
|
||||
使用 Black 格式化 Python 代码:
|
||||
|
||||
```bash
|
||||
uv run black .
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
使用 Pylint 检查代码质量:
|
||||
|
||||
```bash
|
||||
uv run pylint core/ loaders/ renderers/ validators/
|
||||
```
|
||||
|
||||
### 类型检查
|
||||
|
||||
使用 mypy 进行类型检查:
|
||||
|
||||
```bash
|
||||
uv run mypy core/
|
||||
```
|
||||
|
||||
## 常见维护任务
|
||||
|
||||
### 添加新的依赖
|
||||
|
||||
1. 在 `pyproject.toml` 中添加依赖
|
||||
2. 更新 `README.md` 的依赖项列表
|
||||
3. 运行测试确保兼容性
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"python-pptx>=0.6.21",
|
||||
"pyyaml>=6.0",
|
||||
"flask>=3.0",
|
||||
"watchdog>=3.0",
|
||||
"new-dependency>=1.0",
|
||||
]
|
||||
```
|
||||
|
||||
### 更新文档
|
||||
|
||||
功能变更后,同步更新以下文档:
|
||||
|
||||
- `README.md` - 用户文档
|
||||
- `README_DEV.md` - 开发文档
|
||||
- 相关的 `docs/` 子文档
|
||||
|
||||
### 版本发布
|
||||
|
||||
1. 更新版本号(在 pyproject.toml 中)
|
||||
2. 更新 CHANGELOG
|
||||
3. 创建 git tag
|
||||
4. 发布到 PyPI(如果需要)
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 使用预览模式
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py preview temp/test.yaml
|
||||
```
|
||||
|
||||
### 查看详细错误
|
||||
|
||||
```python
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
```
|
||||
|
||||
### 使用 Python 调试器
|
||||
|
||||
```bash
|
||||
uv run python -m pdb yaml2pptx.py convert test.yaml
|
||||
```
|
||||
|
||||
## 问题排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **导入错误**:检查是否使用 `uv run`
|
||||
2. **依赖缺失**:运行 `uv pip install -e .[dev]`
|
||||
3. **测试失败**:检查 `temp/` 目录权限
|
||||
|
||||
### 获取帮助
|
||||
|
||||
- 查看项目 Issues
|
||||
- 查看 `README.md` 和 `README_DEV.md`
|
||||
- 联系维护者
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [开发规范](development-guide.md) - 编码规范
|
||||
- [测试规范](testing.md) - 测试指南
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
18
docs/development/modules/_index.md
Normal file
18
docs/development/modules/_index.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 模块详解
|
||||
|
||||
本目录包含各代码模块的详细说明。
|
||||
|
||||
## 核心模块
|
||||
|
||||
- [Elements](elements.md) - 元素抽象层
|
||||
- [Template](template.md) - 模板系统
|
||||
- [Validators](validators.md) - 验证器
|
||||
- [Renderers](renderers.md) - 渲染器
|
||||
- [Loaders](loaders.md) - 加载器
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构设计](../architecture.md) - 整体代码结构
|
||||
- [开发规范](../development-guide.md) - 编码规范
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
137
docs/development/modules/elements.md
Normal file
137
docs/development/modules/elements.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Elements 模块
|
||||
|
||||
`core/elements.py` 定义了所有元素类型的数据类和工厂函数。
|
||||
|
||||
## 职责
|
||||
|
||||
- 定义元素数据类(使用 `@dataclass`)
|
||||
- 在创建时进行元素级验证
|
||||
- 提供元素工厂函数
|
||||
|
||||
## 包含的内容
|
||||
|
||||
### FontConfig 类
|
||||
|
||||
字体配置数据类,支持所有字体属性:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class FontConfig:
|
||||
parent: Optional[str] = None
|
||||
family: Optional[str] = None
|
||||
size: Optional[int] = None
|
||||
bold: Optional[bool] = None
|
||||
italic: Optional[bool] = None
|
||||
underline: Optional[bool] = None
|
||||
strikethrough: Optional[bool] = None
|
||||
color: Optional[str] = None
|
||||
align: Optional[str] = None
|
||||
line_spacing: Optional[float] = None
|
||||
space_before: Optional[float] = None
|
||||
space_after: Optional[float] = None
|
||||
baseline: Optional[str] = None
|
||||
caps: Optional[str] = None
|
||||
```
|
||||
|
||||
在 `__post_init__` 中验证 baseline 和 caps 枚举值。
|
||||
|
||||
### 元素类
|
||||
|
||||
#### TextElement
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TextElement:
|
||||
type: str = 'text'
|
||||
content: str = ''
|
||||
box: list = field(default_factory=lambda: [1, 1, 8, 1])
|
||||
font: FontConfig | str | dict = field(default_factory=dict)
|
||||
```
|
||||
|
||||
#### ImageElement
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ImageElement:
|
||||
type: str = 'image'
|
||||
src: str = ''
|
||||
box: list = field(default_factory=lambda: [1, 1, 4, 3])
|
||||
```
|
||||
|
||||
#### ShapeElement
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ShapeElement:
|
||||
type: str = 'shape'
|
||||
box: list = field(default_factory=lambda: [1, 1, 4, 2])
|
||||
shape: str = 'rectangle'
|
||||
fill: str = '#000000'
|
||||
line: dict = field(default_factory=dict)
|
||||
```
|
||||
|
||||
#### TableElement
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TableElement:
|
||||
type: str = 'table'
|
||||
position: list = field(default_factory=lambda: [0, 0])
|
||||
col_widths: list = field(default_factory=list)
|
||||
data: list = field(default_factory=list)
|
||||
font: FontConfig | str | dict = field(default_factory=dict)
|
||||
header_font: FontConfig | str | dict = field(default_factory=dict)
|
||||
style: dict = field(default_factory=dict)
|
||||
```
|
||||
|
||||
### create_element() 工厂函数
|
||||
|
||||
```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)
|
||||
elif elem_type == 'shape':
|
||||
return ShapeElement(**elem_dict)
|
||||
elif elem_type == 'table':
|
||||
return TableElement(**elem_dict)
|
||||
else:
|
||||
raise ValueError(f"Unknown element type: {elem_type}")
|
||||
```
|
||||
|
||||
自动将字典形式的 font 转换为 FontConfig 对象。
|
||||
|
||||
## 创建时验证
|
||||
|
||||
每个元素类在 `__post_init__` 中进行验证:
|
||||
|
||||
```python
|
||||
def __post_init__(self):
|
||||
# 检查必需字段
|
||||
if len(self.box) != 4:
|
||||
raise ValueError("box 必须包含 4 个数字")
|
||||
|
||||
# 验证颜色格式
|
||||
if self.fill and not _is_valid_color(self.fill):
|
||||
raise ValueError(f"无效的颜色格式: {self.fill}")
|
||||
```
|
||||
|
||||
## 元素级验证职责
|
||||
|
||||
- 必需字段检查
|
||||
- 数据类型检查
|
||||
- 值的有效性检查:
|
||||
- 颜色格式验证
|
||||
- 字体大小合理性
|
||||
- 枚举值检查(如形状类型)
|
||||
- 表格数据一致性
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Template 模块](template.md) - 模板系统
|
||||
- [Validators 模块](validators.md) - 验证器详解
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
135
docs/development/modules/loaders.md
Normal file
135
docs/development/modules/loaders.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Loaders 模块
|
||||
|
||||
`loaders/yaml_loader.py` 负责 YAML 文件加载和验证。
|
||||
|
||||
## 职责
|
||||
|
||||
- YAML 文件加载
|
||||
- 演示文稿结构验证
|
||||
- 内联模板验证
|
||||
- 外部模板验证
|
||||
|
||||
## 包含的内容
|
||||
|
||||
### YAMLError
|
||||
|
||||
自定义异常类:
|
||||
|
||||
```python
|
||||
class YAMLError(Exception):
|
||||
"""YAML 相关错误"""
|
||||
pass
|
||||
```
|
||||
|
||||
### load_yaml_file()
|
||||
|
||||
加载 YAML 文件:
|
||||
|
||||
```python
|
||||
def load_yaml_file(file_path):
|
||||
"""加载 YAML 文件并返回解析后的数据"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
return data
|
||||
except yaml.YAMLError as e:
|
||||
raise YAMLError(f"YAML 语法错误: {e}")
|
||||
except FileNotFoundError:
|
||||
raise YAMLError(f"文件不存在: {file_path}")
|
||||
```
|
||||
|
||||
### validate_presentation_yaml()
|
||||
|
||||
验证演示文稿结构:
|
||||
|
||||
```python
|
||||
def validate_presentation_yaml(data, file_path):
|
||||
"""验证演示文稿 YAML 结构"""
|
||||
# 检查必需字段
|
||||
if 'metadata' not in data:
|
||||
raise YAMLError(f"缺少 metadata 字段: {file_path}")
|
||||
|
||||
if 'slides' not in data:
|
||||
raise YAMLError(f"缺少 slides 字段: {file_path}")
|
||||
|
||||
# 验证 metadata
|
||||
metadata = data['metadata']
|
||||
if 'size' not in metadata:
|
||||
raise YAMLError(f"metadata 缺少 size 字段: {file_path}")
|
||||
|
||||
if metadata['size'] not in ['16:9', '4:3']:
|
||||
raise YAMLError(f"无效的 size 值: {metadata['size']}")
|
||||
|
||||
# 验证内联模板(如果有)
|
||||
if 'templates' in data:
|
||||
validate_templates_yaml(data['templates'])
|
||||
|
||||
# 验证幻灯片
|
||||
for slide in data['slides']:
|
||||
# 验证 enabled 字段
|
||||
if 'enabled' in slide:
|
||||
if not isinstance(slide['enabled'], bool):
|
||||
raise YAMLError(f"enabled 必须是布尔值: {file_path}")
|
||||
```
|
||||
|
||||
### validate_template_yaml()
|
||||
|
||||
验证外部模板结构:
|
||||
|
||||
```python
|
||||
def validate_template_yaml(template_data, template_name):
|
||||
"""验证外部模板结构"""
|
||||
if 'elements' not in template_data:
|
||||
raise YAMLError(f"模板缺少 elements 字段: {template_name}")
|
||||
|
||||
if 'vars' in template_data:
|
||||
for var in template_data['vars']:
|
||||
if 'name' not in var:
|
||||
raise YAMLError(f"变量缺少 name 字段: {template_name}")
|
||||
```
|
||||
|
||||
### validate_templates_yaml()
|
||||
|
||||
验证内联模板结构:
|
||||
|
||||
```python
|
||||
def validate_templates_yaml(templates):
|
||||
"""验证内联模板结构"""
|
||||
if not isinstance(templates, dict):
|
||||
raise YAMLError("templates 必须是字典类型")
|
||||
|
||||
for name, template_data in templates.items():
|
||||
validate_template_yaml(template_data, f"templates.{name}")
|
||||
|
||||
# 检测默认值中引用不存在的变量
|
||||
vars_def = {v['name']: v for v in template_data.get('vars', [])}
|
||||
for var in template_data.get('vars', []):
|
||||
if 'default' in var:
|
||||
# 检查默认值是否引用了不存在的变量
|
||||
# 实现省略...
|
||||
```
|
||||
|
||||
## 内联模板验证
|
||||
|
||||
内联模板验证包括:
|
||||
- 结构验证(是否为字典)
|
||||
- 元素验证(是否有 elements 字段)
|
||||
- 变量定义验证(是否有 name 字段)
|
||||
- 默认值验证(检测循环引用)
|
||||
|
||||
## enabled 字段验证
|
||||
|
||||
验证 `enabled` 字段必须是布尔值:
|
||||
|
||||
```python
|
||||
if 'enabled' in slide:
|
||||
if not isinstance(slide['enabled'], bool):
|
||||
raise YAMLError(f"enabled 必须是布尔值,不能是字符串或条件表达式")
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构设计](../architecture.md) - 代码结构
|
||||
- [验证器模块](validators.md) - 验证详解
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
153
docs/development/modules/renderers.md
Normal file
153
docs/development/modules/renderers.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Renderers 模块
|
||||
|
||||
`renderers/` 目录包含 PPTX 和 HTML 渲染器。
|
||||
|
||||
## 模块组成
|
||||
|
||||
### pptx_renderer.py
|
||||
|
||||
PPTX 文件生成器:
|
||||
|
||||
```python
|
||||
class PptxGenerator:
|
||||
def __init__(self, size="16:9"):
|
||||
self.presentation = Presentation()
|
||||
self.slide_width = 10 # 英寸
|
||||
self.slide_height = 5.625 if size == "16:9" else 7.5
|
||||
|
||||
def add_slide(self):
|
||||
"""添加新幻灯片"""
|
||||
slide = self.presentation.slides.add_slide(self.blank_layout)
|
||||
return slide
|
||||
|
||||
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)
|
||||
# ...
|
||||
|
||||
def _render_text(self, slide, elem):
|
||||
"""渲染文本元素"""
|
||||
# 实现...
|
||||
|
||||
def _render_image(self, slide, elem, base_path):
|
||||
"""渲染图片元素"""
|
||||
# 实现...
|
||||
|
||||
def _render_shape(self, slide, elem):
|
||||
"""渲染形状元素"""
|
||||
# 实现...
|
||||
|
||||
def _render_table(self, slide, elem):
|
||||
"""渲染表格元素"""
|
||||
# 实现...
|
||||
|
||||
def save(self, output_path):
|
||||
"""保存 PPTX 文件"""
|
||||
self.presentation.save(output_path)
|
||||
```
|
||||
|
||||
### html_renderer.py
|
||||
|
||||
HTML 预览渲染器:
|
||||
|
||||
```python
|
||||
class HtmlRenderer:
|
||||
def __init__(self, size="16:9"):
|
||||
self.slide_width = 960 # 96 DPI * 10"
|
||||
self.slide_height = 540 if size == "16:9" else 720
|
||||
|
||||
def render_slide(self, slide_data, index, base_path=None):
|
||||
"""渲染幻灯片为 HTML"""
|
||||
elements_html = ""
|
||||
|
||||
for elem in slide_data:
|
||||
if isinstance(elem, TextElement):
|
||||
elements_html += self.render_text(elem)
|
||||
elif isinstance(elem, ImageElement):
|
||||
elements_html += self.render_image(elem, base_path)
|
||||
# ...
|
||||
|
||||
return self.SLIDE_TEMPLATE.format(
|
||||
width=self.slide_width,
|
||||
height=self.slide_height,
|
||||
content=elements_html
|
||||
)
|
||||
|
||||
def render_text(self, elem):
|
||||
"""渲染文本为 HTML"""
|
||||
# 实现...
|
||||
|
||||
def render_image(self, elem, base_path):
|
||||
"""渲染图片为 HTML"""
|
||||
# 实现...
|
||||
|
||||
def render_shape(self, elem):
|
||||
"""渲染形状为 HTML"""
|
||||
# 实现...
|
||||
|
||||
def render_table(self, elem):
|
||||
"""渲染表格为 HTML"""
|
||||
# 实现...
|
||||
```
|
||||
|
||||
## 设计特点
|
||||
|
||||
### 渲染器内置在生成器中
|
||||
|
||||
- **封装性**:渲染逻辑与生成器紧密相关
|
||||
- **简单性**:不需要额外的渲染器接口
|
||||
- **性能**:避免额外的方法调用开销
|
||||
|
||||
### 使用 isinstance() 检查类型
|
||||
|
||||
```python
|
||||
if isinstance(elem, TextElement):
|
||||
self._render_text(slide, elem)
|
||||
```
|
||||
|
||||
### 通过元素对象属性访问数据
|
||||
|
||||
```python
|
||||
def _render_text(self, slide, elem):
|
||||
text_box = elem.box
|
||||
content = elem.content
|
||||
font_config = elem.font
|
||||
```
|
||||
|
||||
## 共享元素抽象层
|
||||
|
||||
两个渲染器共享相同的元素数据类:
|
||||
- `TextElement`
|
||||
- `ImageElement`
|
||||
- `ShapeElement`
|
||||
- `TableElement`
|
||||
|
||||
## 单位转换
|
||||
|
||||
### HTML 渲染器
|
||||
|
||||
使用固定 DPI (96) 进行单位转换:
|
||||
|
||||
```python
|
||||
DPI = 96
|
||||
pixels = inches * DPI
|
||||
```
|
||||
|
||||
### PPTX 渲染器
|
||||
|
||||
python-pptx 使用 Inches 单位:
|
||||
|
||||
```python
|
||||
from pptx.util import Inches
|
||||
left = Inches(box[0])
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Elements 模块](elements.md) - 元素数据类
|
||||
- [预览功能](../../user-guide.md) - 用户指南
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
148
docs/development/modules/template.md
Normal file
148
docs/development/modules/template.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Template 模块
|
||||
|
||||
`core/template.py` 实现了模板系统,支持变量解析、条件渲染和模板渲染。
|
||||
|
||||
## 职责
|
||||
|
||||
- 模板加载和变量解析
|
||||
- 条件表达式评估
|
||||
- 模板渲染
|
||||
|
||||
## 包含的内容
|
||||
|
||||
### Template 类
|
||||
|
||||
#### from_data() 类方法
|
||||
|
||||
从字典创建模板实例(用于内联模板):
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def from_data(cls, template_data, template_name, base_dir=None):
|
||||
"""从字典创建模板(内联模板或外部模板)"""
|
||||
obj = cls.__new__(cls)
|
||||
obj.data = template_data
|
||||
obj.base_dir = base_dir
|
||||
obj.vars_def = {var['name']: var for var in template_data.get('vars', [])}
|
||||
obj.elements = template_data.get('elements', [])
|
||||
return obj
|
||||
```
|
||||
|
||||
#### 变量解析
|
||||
|
||||
**resolve_value()** - 解析变量值:
|
||||
|
||||
```python
|
||||
def resolve_value(self, value, vars_values):
|
||||
"""解析变量值,支持 {varname} 语法"""
|
||||
if isinstance(value, str) and '{' in value:
|
||||
# 替换变量
|
||||
result = value
|
||||
for var_name, var_value in vars_values.items():
|
||||
result = result.replace(f'{{{var_name}}}', str(var_value))
|
||||
return result
|
||||
return value
|
||||
```
|
||||
|
||||
**resolve_element()** - 解析元素中的变量:
|
||||
|
||||
```python
|
||||
def resolve_element(self, element, vars_values):
|
||||
"""递归解析元素中的所有变量"""
|
||||
resolved = {}
|
||||
for key, value in element.items():
|
||||
if isinstance(value, dict):
|
||||
resolved[key] = self.resolve_element(value, vars_values)
|
||||
elif isinstance(value, str):
|
||||
resolved[key] = self.resolve_value(value, vars_values)
|
||||
else:
|
||||
resolved[key] = value
|
||||
return resolved
|
||||
```
|
||||
|
||||
#### 条件渲染
|
||||
|
||||
**evaluate_condition()** - 委托给 ConditionEvaluator:
|
||||
|
||||
```python
|
||||
def evaluate_condition(self, condition, vars_values):
|
||||
"""评估条件表达式"""
|
||||
from core.condition_evaluator import ConditionEvaluator
|
||||
evaluator = ConditionEvaluator()
|
||||
return evaluator.evaluate(condition, vars_values)
|
||||
```
|
||||
|
||||
#### 模板渲染
|
||||
|
||||
**render()** - 渲染模板:
|
||||
|
||||
```python
|
||||
def render(self, vars_values):
|
||||
"""渲染模板,返回解析后的元素列表"""
|
||||
elements = []
|
||||
|
||||
for elem in self.elements:
|
||||
# 检查条件渲染
|
||||
if 'visible' in elem:
|
||||
if not self.evaluate_condition(elem['visible'], vars_values):
|
||||
continue
|
||||
|
||||
# 解析变量
|
||||
resolved_elem = self.resolve_element(elem, vars_values)
|
||||
elements.append(resolved_elem)
|
||||
|
||||
return elements
|
||||
```
|
||||
|
||||
## 特点
|
||||
|
||||
### 支持两种模板方式
|
||||
|
||||
- **外部模板**:从文件加载
|
||||
- **内联模板**:从字典创建(通过 `from_data()`)
|
||||
|
||||
### 变量替换
|
||||
|
||||
支持 `{varname}` 语法的变量替换:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
title-slide:
|
||||
vars:
|
||||
- name: title
|
||||
elements:
|
||||
- type: text
|
||||
content: "{title}" # 变量替换
|
||||
```
|
||||
|
||||
### 条件渲染
|
||||
|
||||
支持 `visible` 属性的条件表达式:
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: text
|
||||
content: "有数据"
|
||||
visible: "{count > 0}"
|
||||
```
|
||||
|
||||
## 内联模板支持
|
||||
|
||||
`from_data()` 类方法使模板可以从字典创建:
|
||||
|
||||
```python
|
||||
# 从内联模板定义创建
|
||||
template = Template.from_data(
|
||||
template_data=inline_template_dict,
|
||||
template_name="title-slide",
|
||||
base_dir=document_base_dir
|
||||
)
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [条件评估器](../condition-rendering.md) - 条件表达式语法
|
||||
- [Elements 模块](elements.md) - 元素数据类
|
||||
- [内联模板](../../templates/inline.md) - 用户指南
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
112
docs/development/modules/validators.md
Normal file
112
docs/development/modules/validators.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Validators 模块
|
||||
|
||||
`validators/` 目录包含所有验证相关的代码。
|
||||
|
||||
## 模块组成
|
||||
|
||||
### validators/result.py
|
||||
|
||||
验证结果数据结构:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
level: str # ERROR/WARNING/INFO
|
||||
message: str
|
||||
location: str = ""
|
||||
code: str = ""
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
errors: List[ValidationIssue]
|
||||
warnings: List[ValidationIssue]
|
||||
infos: List[ValidationIssue]
|
||||
```
|
||||
|
||||
### validators/geometry.py
|
||||
|
||||
几何验证器,检查元素边界:
|
||||
|
||||
```python
|
||||
class GeometryValidator:
|
||||
def __init__(self, slide_width, slide_height):
|
||||
self.slide_width = slide_width
|
||||
self.slide_height = slide_height
|
||||
self.tolerance = 0.1 # 英寸
|
||||
|
||||
def validate_element(self, element, slide_index, elem_index):
|
||||
"""检查元素是否在页面范围内"""
|
||||
# 检查边界
|
||||
# 支持容忍度
|
||||
```
|
||||
|
||||
### validators/resource.py
|
||||
|
||||
资源验证器,检查文件存在性:
|
||||
|
||||
```python
|
||||
class ResourceValidator:
|
||||
def __init__(self, base_dir):
|
||||
self.base_dir = Path(base_dir)
|
||||
|
||||
def validate_image(self, src, slide_index, elem_index):
|
||||
"""检查图片文件是否存在"""
|
||||
# 检查文件路径
|
||||
# 验证文件存在
|
||||
```
|
||||
|
||||
### validators/validator.py
|
||||
|
||||
主验证器,协调所有子验证器:
|
||||
|
||||
```python
|
||||
class Validator:
|
||||
def __init__(self, slide_width, slide_height, base_dir):
|
||||
self.geometry_validator = GeometryValidator(slide_width, slide_height)
|
||||
self.resource_validator = ResourceValidator(base_dir)
|
||||
|
||||
def validate_presentation(self, presentation):
|
||||
"""验证整个演示文稿"""
|
||||
# 遍历所有幻灯片和元素
|
||||
# 调用子验证器
|
||||
```
|
||||
|
||||
## 验证职责分层
|
||||
|
||||
### 元素级验证
|
||||
|
||||
在元素类中完成:
|
||||
- 必需字段检查
|
||||
- 数据类型检查
|
||||
- 值的有效性检查
|
||||
|
||||
### 系统级验证
|
||||
|
||||
在验证器中完成:
|
||||
- 几何验证(需要页面尺寸)
|
||||
- 资源验证(需要文件路径)
|
||||
- 跨元素验证
|
||||
|
||||
## 验证容忍度
|
||||
|
||||
几何验证时,允许 0.1 英寸的容忍度:
|
||||
|
||||
```python
|
||||
TOLERANCE = 0.1 # 英寸
|
||||
|
||||
if right > slide_width + TOLERANCE:
|
||||
# 报告 WARNING
|
||||
```
|
||||
|
||||
## 分级错误报告
|
||||
|
||||
- **ERROR**:阻止转换的严重问题
|
||||
- **WARNING**:影响视觉效果的问题
|
||||
- **INFO**:优化建议
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Elements 模块](elements.md) - 元素级验证
|
||||
- [开发规范](../development-guide.md) - 验证职责
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
174
docs/development/scope-system.md
Normal file
174
docs/development/scope-system.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 作用域系统
|
||||
|
||||
字体作用域系统实现了文档和模板库之间的字体隔离和跨域引用控制。
|
||||
|
||||
## 作用域定义
|
||||
|
||||
系统定义了两个字体作用域:
|
||||
|
||||
### 文档作用域(document)
|
||||
|
||||
- 包含文档的 `metadata.fonts` 中定义的字体
|
||||
- 文档的 `fonts_default` 只能引用文档作用域的字体
|
||||
- 文档元素可以引用文档作用域和模板库作用域的字体
|
||||
|
||||
### 模板库作用域(template)
|
||||
|
||||
- 包含模板库的 `metadata.fonts` 中定义的字体
|
||||
- 模板库的 `fonts_default` 只能引用模板库作用域的字体
|
||||
- 模板元素只能引用模板库作用域的字体(不能引用文档字体)
|
||||
|
||||
## 跨域引用规则
|
||||
|
||||
### 允许的引用
|
||||
|
||||
- 文档元素 → 文档字体
|
||||
- 文档元素 → 模板库字体(跨域引用)
|
||||
- 模板元素 → 模板库字体
|
||||
- 文档 fonts_default → 文档字体
|
||||
- 模板库 fonts_default → 模板库字体
|
||||
|
||||
### 禁止的引用
|
||||
|
||||
- 模板元素 → 文档字体(跨域引用被禁止)
|
||||
- 模板库 fonts_default → 文档字体(跨域引用被禁止)
|
||||
- 文档 fonts_default → 模板库字体(跨域引用被禁止)
|
||||
|
||||
### 设计理由
|
||||
|
||||
- 模板库应该是自包含的,不依赖特定文档的字体配置
|
||||
- 文档可以引用模板库字体,实现样式复用
|
||||
- 防止模板库与文档之间的紧耦合
|
||||
|
||||
## FontResolver 实现
|
||||
|
||||
### 初始化
|
||||
|
||||
```python
|
||||
class FontResolver:
|
||||
def __init__(self, fonts, fonts_default, scope="document", template_fonts=None):
|
||||
"""
|
||||
Args:
|
||||
scope: 作用域标识 ("document" 或 "template")
|
||||
template_fonts: 模板库字体字典(仅文档作用域需要)
|
||||
"""
|
||||
```
|
||||
|
||||
### 跨域引用检测
|
||||
|
||||
- 使用作用域标签(`doc.@font-name` 或 `template.@font-name`)追踪引用路径
|
||||
- 检测跨域循环引用(如 `doc.@a → template.@b → doc.@a`)
|
||||
- 在 `parent` 引用时根据作用域限制跨域访问
|
||||
|
||||
## fonts_default 级联规则
|
||||
|
||||
当元素未指定字体时,按以下顺序查找默认字体:
|
||||
|
||||
### 模板元素
|
||||
|
||||
1. 模板库的 `fonts_default`(如果存在)
|
||||
2. 文档的 `fonts_default`(如果存在)
|
||||
3. 系统默认字体
|
||||
|
||||
### 文档元素
|
||||
|
||||
1. 文档的 `fonts_default`(如果存在)
|
||||
2. 系统默认字体
|
||||
|
||||
## 循环引用检测
|
||||
|
||||
### 单域内循环
|
||||
|
||||
```yaml
|
||||
fonts:
|
||||
a:
|
||||
parent: "@b"
|
||||
b:
|
||||
parent: "@a"
|
||||
```
|
||||
|
||||
错误信息:`检测到字体引用循环: doc.@a -> doc.@b -> doc.@a`
|
||||
|
||||
### 跨域循环
|
||||
|
||||
```yaml
|
||||
# 文档
|
||||
fonts:
|
||||
doc-font:
|
||||
parent: "@template-font"
|
||||
|
||||
# 模板库
|
||||
fonts:
|
||||
template-font:
|
||||
parent: "@doc-font" # 这会被禁止
|
||||
```
|
||||
|
||||
错误信息:`检测到跨域字体引用循环: doc.@doc-font -> template.@template-font -> doc.@doc-font`
|
||||
|
||||
## 错误代码
|
||||
|
||||
### 模板库 metadata 相关
|
||||
|
||||
- `TEMPLATE_LIBRARY_MISSING_METADATA` - 模板库缺少 metadata 字段
|
||||
- `TEMPLATE_LIBRARY_METADATA_MISSING_SIZE` - 模板库 metadata 缺少 size 字段
|
||||
- `TEMPLATE_LIBRARY_METADATA_INVALID_SIZE` - 模板库 metadata.size 值无效
|
||||
|
||||
### Size 一致性
|
||||
|
||||
- `SIZE_MISMATCH` - 文档和模板库的 size 不一致
|
||||
|
||||
### 字体引用相关
|
||||
|
||||
- `TEMPLATE_FONT_REF_DOC_FORBIDDEN` - 模板元素引用文档字体
|
||||
- `TEMPLATE_PARENT_REF_DOC_FORBIDDEN` - 模板库字体的 parent 引用文档字体
|
||||
- `FONT_NOT_FOUND` - 字体配置不存在
|
||||
- `CIRCULAR_REFERENCE` - 检测到字体引用循环
|
||||
- `FONT_DEFAULT_INVALID` - fonts_default 引用无效
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 正确的跨域引用
|
||||
|
||||
```yaml
|
||||
# templates.yaml(模板库)
|
||||
metadata:
|
||||
size: "16:9"
|
||||
fonts:
|
||||
template-title:
|
||||
family: "cjk-sans"
|
||||
size: 44
|
||||
bold: true
|
||||
|
||||
# presentation.yaml(文档)
|
||||
metadata:
|
||||
size: "16:9"
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
content: "正文"
|
||||
font: "@template-title" # 文档元素可以引用模板库字体
|
||||
```
|
||||
|
||||
### 错误的跨域引用
|
||||
|
||||
```yaml
|
||||
# templates.yaml(模板库)
|
||||
metadata:
|
||||
size: "16:9"
|
||||
fonts_default: "@doc-body" # 模板库 fonts_default 不能引用文档字体
|
||||
|
||||
templates:
|
||||
title-slide:
|
||||
elements:
|
||||
- type: text
|
||||
content: "{title}"
|
||||
font: "@doc-body" # 模板元素不能引用文档字体
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [字体系统实现](font-system.md) - FontResolver 实现
|
||||
- [字体主题系统](../../fonts.md) - 用户指南
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
267
docs/development/testing.md
Normal file
267
docs/development/testing.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# 测试规范
|
||||
|
||||
本文档说明 yaml2pptx 项目的测试框架和规范。
|
||||
|
||||
## 测试框架
|
||||
|
||||
项目使用 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
|
||||
```
|
||||
|
||||
### 测试文件位置
|
||||
|
||||
- **自动化测试**:`tests/` 目录
|
||||
- **手动测试文件**:`temp/` 目录
|
||||
|
||||
## 编写测试
|
||||
|
||||
### 测试类命名
|
||||
|
||||
使用 `Test<ClassName>` 格式:
|
||||
|
||||
```python
|
||||
class TestTextElement:
|
||||
"""TextElement 测试类"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 测试方法命名
|
||||
|
||||
使用 `test_<what_is_being_tested>` 格式:
|
||||
|
||||
```python
|
||||
def test_create_with_defaults(self):
|
||||
"""测试使用默认值创建 TextElement"""
|
||||
pass
|
||||
|
||||
def test_invalid_color_raises_error(self):
|
||||
"""测试无效颜色会引发错误"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 测试示例
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from core.elements import TextElement, FontConfig
|
||||
|
||||
class TestTextElement:
|
||||
"""TextElement 测试类"""
|
||||
|
||||
def test_create_with_defaults(self):
|
||||
"""测试使用默认值创建 TextElement"""
|
||||
elem = TextElement()
|
||||
assert elem.type == 'text'
|
||||
assert elem.content == ''
|
||||
assert elem.box == [1, 1, 8, 1]
|
||||
|
||||
def test_create_with_custom_values(self):
|
||||
"""测试使用自定义值创建 TextElement"""
|
||||
elem = TextElement(
|
||||
content="Test",
|
||||
box=[2, 2, 6, 1],
|
||||
font={"size": 24}
|
||||
)
|
||||
assert elem.content == "Test"
|
||||
assert elem.box == [2, 2, 6, 1]
|
||||
|
||||
def test_invalid_color_raises_error(self):
|
||||
"""测试无效颜色会引发错误"""
|
||||
with pytest.raises(ValueError, match="无效颜色"):
|
||||
TextElement(font={"color": "red"})
|
||||
```
|
||||
|
||||
## Fixtures
|
||||
|
||||
共享 fixtures 定义在 `tests/conftest.py` 中:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(tmp_path):
|
||||
"""临时目录 fixture"""
|
||||
return tmp_path
|
||||
|
||||
@pytest.fixture
|
||||
def sample_yaml(temp_dir):
|
||||
"""最小测试 YAML 文件"""
|
||||
yaml_file = temp_dir / "test.yaml"
|
||||
yaml_file.write_text("""
|
||||
metadata:
|
||||
size: "16:9"
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
box: [1, 1, 8, 1]
|
||||
content: "Test"
|
||||
""")
|
||||
return yaml_file
|
||||
|
||||
@pytest.fixture
|
||||
def sample_image(temp_dir):
|
||||
"""测试图片 fixture"""
|
||||
import PIL.Image
|
||||
img_path = temp_dir / "test.png"
|
||||
img = PIL.Image.new('RGB', (100, 100), color='red')
|
||||
img.save(img_path)
|
||||
return img_path
|
||||
|
||||
@pytest.fixture
|
||||
def pptx_validator():
|
||||
"""PPTX 验证器 fixture"""
|
||||
from tests.conftest_pptx import PptxFileValidator
|
||||
return PptxFileValidator()
|
||||
```
|
||||
|
||||
### 使用 Fixtures
|
||||
|
||||
```python
|
||||
def test_with_fixture(sample_yaml):
|
||||
"""使用 fixture 的测试"""
|
||||
assert sample_yaml.exists()
|
||||
assert sample_yaml.stat().st_size > 0
|
||||
```
|
||||
|
||||
## PPTX 验证
|
||||
|
||||
使用 `PptxFileValidator` 验证生成的 PPTX 文件:
|
||||
|
||||
```python
|
||||
def test_pptx_generation(temp_dir, pptx_validator):
|
||||
"""测试 PPTX 生成"""
|
||||
from core.presentation import Presentation
|
||||
from renderers.pptx_renderer import PptxGenerator
|
||||
|
||||
# 生成 PPTX
|
||||
yaml_path = temp_dir / "test.yaml"
|
||||
output_path = temp_dir / "output.pptx"
|
||||
|
||||
# ... 创建演示文稿 ...
|
||||
|
||||
# 验证文件
|
||||
assert pptx_validator.validate_file(output_path) is True
|
||||
|
||||
# 验证内容
|
||||
from pptx import Presentation as PPTX
|
||||
prs = PPTX(str(output_path))
|
||||
assert len(prs.slides) == 1
|
||||
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 ./templates.yaml
|
||||
|
||||
# 转换 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 ./templates.yaml
|
||||
|
||||
# 启动预览服务器
|
||||
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
|
||||
```
|
||||
|
||||
## 测试覆盖率
|
||||
|
||||
目标测试覆盖率:>80%
|
||||
|
||||
```bash
|
||||
# 生成覆盖率报告
|
||||
uv run pytest --cov=. --cov-report=html
|
||||
|
||||
# 查看报告
|
||||
open htmlcov/index.html
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [开发规范](development-guide.md) - 编码规范
|
||||
- [扩展指南](extending.md) - 添加新功能
|
||||
|
||||
[返回开发文档索引](../README.md)
|
||||
17
docs/elements/_index.md
Normal file
17
docs/elements/_index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 元素类型文档
|
||||
|
||||
本目录包含各种元素类型的详细说明。
|
||||
|
||||
## 元素类型
|
||||
|
||||
- [文本元素](text.md) - 文本框和字体配置
|
||||
- [图片元素](image.md) - 图片插入和配置
|
||||
- [形状元素](shape.md) - 几何形状绘制
|
||||
- [表格元素](table.md) - 表格创建和样式
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [字体主题系统](../fonts.md) - 字体配置和主题管理
|
||||
- [坐标系统](../reference/coordinates.md) - 位置和尺寸单位
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
100
docs/elements/image.md
Normal file
100
docs/elements/image.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 图片元素
|
||||
|
||||
图片元素用于在幻灯片中插入图片。
|
||||
|
||||
## 基本语法
|
||||
|
||||
```yaml
|
||||
- type: image
|
||||
box: [x, y, width, height]
|
||||
src: "path/to/image.png" # 支持相对路径和绝对路径
|
||||
```
|
||||
|
||||
## 属性
|
||||
|
||||
| 属性 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `type` | 字符串 | 是 | 必须为 "image" |
|
||||
| `box` | 数组 | 是 | 位置和尺寸 [x, y, width, height](英寸) |
|
||||
| `src` | 字符串 | 是 | 图片文件路径 |
|
||||
|
||||
## 图片路径
|
||||
|
||||
支持相对路径和绝对路径:
|
||||
|
||||
```yaml
|
||||
# 相对路径(相对于 YAML 文件位置)
|
||||
- type: image
|
||||
src: "images/logo.png"
|
||||
box: [1, 1, 2, 2]
|
||||
|
||||
# 绝对路径
|
||||
- type: image
|
||||
src: "/Users/username/pictures/photo.jpg"
|
||||
box: [1, 1, 4, 3]
|
||||
```
|
||||
|
||||
## 示例
|
||||
|
||||
### 基本图片
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: image
|
||||
src: "photo.jpg"
|
||||
box: [1, 1, 4, 3]
|
||||
```
|
||||
|
||||
### 多个图片
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: image
|
||||
src: "logo.png"
|
||||
box: [0.5, 0.5, 2, 2]
|
||||
|
||||
- type: image
|
||||
src: "banner.jpg"
|
||||
box: [3, 0.5, 6.5, 2]
|
||||
```
|
||||
|
||||
### 与文本组合
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
content: "项目 Logo"
|
||||
box: [1, 3, 3, 0.5]
|
||||
font:
|
||||
size: 24
|
||||
align: center
|
||||
|
||||
- type: image
|
||||
src: "logo.png"
|
||||
box: [1, 3.5, 3, 3]
|
||||
```
|
||||
|
||||
## 支持的格式
|
||||
|
||||
支持常见图片格式,包括:
|
||||
- PNG (.png)
|
||||
- JPEG (.jpg, .jpeg)
|
||||
- GIF (.gif)
|
||||
- BMP (.bmp)
|
||||
- 其他 PowerPoint 支持的格式
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 图片路径相对于 YAML 文件位置
|
||||
- 建议使用高分辨率图片以获得最佳显示效果
|
||||
- 图片会按照 box 指定的尺寸进行缩放
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [坐标系统](../reference/coordinates.md) - 位置和尺寸单位
|
||||
- [形状元素](shape.md) - 几何形状绘制
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
139
docs/elements/shape.md
Normal file
139
docs/elements/shape.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 形状元素
|
||||
|
||||
形状元素用于在幻灯片中绘制几何形状。
|
||||
|
||||
## 基本语法
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [x, y, width, height]
|
||||
shape: rectangle # rectangle/ellipse/rounded_rectangle
|
||||
fill: "#4a90e2" # 填充颜色
|
||||
line:
|
||||
color: "#000000" # 边框颜色
|
||||
width: 2 # 边框宽度(磅)
|
||||
```
|
||||
|
||||
## 属性
|
||||
|
||||
| 属性 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `type` | 字符串 | 是 | 必须为 "shape" |
|
||||
| `box` | 数组 | 是 | 位置和尺寸 [x, y, width, height](英寸) |
|
||||
| `shape` | 字符串 | 是 | 形状类型 |
|
||||
| `fill` | 字符串 | 否 | 填充颜色(#RRGGBB 或 #RGB) |
|
||||
| `line` | 对象 | 否 | 边框配置 |
|
||||
|
||||
## 形状类型
|
||||
|
||||
### rectangle(矩形)
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [1, 1, 4, 2]
|
||||
shape: rectangle
|
||||
fill: "#4a90e2"
|
||||
```
|
||||
|
||||
### ellipse(椭圆)
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [1, 1, 4, 2]
|
||||
shape: ellipse
|
||||
fill: "#e74c3c"
|
||||
```
|
||||
|
||||
### rounded_rectangle(圆角矩形)
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [1, 1, 4, 2]
|
||||
shape: rounded_rectangle
|
||||
fill: "#2ecc71"
|
||||
```
|
||||
|
||||
## 边框配置
|
||||
|
||||
```yaml
|
||||
line:
|
||||
color: "#000000" # 边框颜色
|
||||
width: 2 # 边框宽度(磅)
|
||||
```
|
||||
|
||||
## 示例
|
||||
|
||||
### 简单矩形
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: shape
|
||||
box: [1, 1, 4, 2]
|
||||
shape: rectangle
|
||||
fill: "#4a90e2"
|
||||
```
|
||||
|
||||
### 带边框的形状
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: shape
|
||||
box: [1, 1, 4, 2]
|
||||
shape: rectangle
|
||||
fill: "#ffffff"
|
||||
line:
|
||||
color: "#000000"
|
||||
width: 2
|
||||
```
|
||||
|
||||
### 多个形状组合
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
# 背景矩形
|
||||
- type: shape
|
||||
box: [0, 0, 10, 5.625]
|
||||
shape: rectangle
|
||||
fill: "#f5f5f5"
|
||||
|
||||
# 装饰圆形
|
||||
- type: shape
|
||||
box: [1, 1, 2, 2]
|
||||
shape: ellipse
|
||||
fill: "#3498db"
|
||||
|
||||
# 文本
|
||||
- type: text
|
||||
box: [3.5, 1.5, 5, 1]
|
||||
content: "欢迎"
|
||||
font:
|
||||
size: 44
|
||||
```
|
||||
|
||||
### 无填充形状
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [1, 1, 4, 2]
|
||||
shape: rectangle
|
||||
fill: "transparent" # 透明填充
|
||||
line:
|
||||
color: "#000000"
|
||||
width: 2
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 形状会按照 box 指定的尺寸进行绘制
|
||||
- 填充颜色和边框颜色使用相同的格式
|
||||
- 边框宽度单位为磅(pt)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [坐标系统](../reference/coordinates.md) - 位置和尺寸单位
|
||||
- [颜色格式](../reference/colors.md) - 颜色表示方法
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
147
docs/elements/table.md
Normal file
147
docs/elements/table.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 表格元素
|
||||
|
||||
表格元素用于在幻灯片中创建表格。
|
||||
|
||||
## 基本语法
|
||||
|
||||
```yaml
|
||||
- type: table
|
||||
position: [x, y]
|
||||
col_widths: [2, 2, 2] # 每列宽度(英寸)
|
||||
data:
|
||||
- ["表头1", "表头2", "表头3"]
|
||||
- ["数据1", "数据2", "数据3"]
|
||||
- ["数据4", "数据5", "数据6"]
|
||||
font:
|
||||
family: "Arial"
|
||||
size: 14
|
||||
color: "#333333"
|
||||
header_font:
|
||||
bold: true
|
||||
color: "#ffffff"
|
||||
style:
|
||||
header_bg: "#4a90e2"
|
||||
```
|
||||
|
||||
## 属性
|
||||
|
||||
| 属性 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `type` | 字符串 | 是 | 必须为 "table" |
|
||||
| `position` | 数组 | 是 | 表格位置 [x, y](英寸) |
|
||||
| `col_widths` | 数组 | 是 | 每列宽度(英寸) |
|
||||
| `data` | 数组 | 是 | 表格数据(二维数组) |
|
||||
| `font` | 对象 | 否 | 数据单元格字体样式 |
|
||||
| `header_font` | 对象 | 否 | 表头单元格字体样式 |
|
||||
| `style` | 对象 | 否 | 表格样式 |
|
||||
|
||||
## 字体配置
|
||||
|
||||
### font(数据单元格)
|
||||
|
||||
数据单元格的字体样式:
|
||||
|
||||
```yaml
|
||||
font:
|
||||
family: "Arial"
|
||||
size: 14
|
||||
color: "#333333"
|
||||
bold: false
|
||||
```
|
||||
|
||||
### header_font(表头单元格)
|
||||
|
||||
表头单元格的字体样式。如果未定义,继承 `font` 的配置:
|
||||
|
||||
```yaml
|
||||
header_font:
|
||||
bold: true
|
||||
color: "#ffffff"
|
||||
# 其他属性继承自 font
|
||||
```
|
||||
|
||||
### style(表格样式)
|
||||
|
||||
```yaml
|
||||
style:
|
||||
header_bg: "#4a90e2" # 表头背景色
|
||||
```
|
||||
|
||||
## 示例
|
||||
|
||||
### 基本表格
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: table
|
||||
position: [1, 1]
|
||||
col_widths: [2, 2, 2]
|
||||
data:
|
||||
- ["姓名", "年龄", "城市"]
|
||||
- ["张三", "25", "北京"]
|
||||
- ["李四", "30", "上海"]
|
||||
```
|
||||
|
||||
### 样式化表格
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: table
|
||||
position: [1, 1]
|
||||
col_widths: [2.5, 2.5, 2.5]
|
||||
data:
|
||||
- ["产品", "价格", "库存"]
|
||||
- ["产品A", "100元", "50"]
|
||||
- ["产品B", "200元", "30"]
|
||||
font:
|
||||
family: "sans"
|
||||
size: 14
|
||||
color: "#333333"
|
||||
header_font:
|
||||
bold: true
|
||||
color: "#ffffff"
|
||||
style:
|
||||
header_bg: "#3498db"
|
||||
```
|
||||
|
||||
### 使用字体主题
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
fonts:
|
||||
table-font:
|
||||
family: "sans"
|
||||
size: 14
|
||||
color: "#333333"
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: table
|
||||
position: [1, 1]
|
||||
col_widths: [2, 2, 2]
|
||||
data:
|
||||
- ["列1", "列2"]
|
||||
- ["数据1", "数据2"]
|
||||
font: "@table-font"
|
||||
header_font:
|
||||
parent: "@table-font"
|
||||
bold: true
|
||||
color: "#ffffff"
|
||||
style:
|
||||
header_bg: "#4a90e2"
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `col_widths` 数组长度必须与每行的列数一致
|
||||
- 所有行的列数必须相同
|
||||
- 第一行默认为表头
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [字体主题系统](../fonts.md) - 字体配置和主题管理
|
||||
- [坐标系统](../reference/coordinates.md) - 位置和尺寸单位
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
121
docs/elements/text.md
Normal file
121
docs/elements/text.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 文本元素
|
||||
|
||||
文本元素用于在幻灯片中添加文本内容。
|
||||
|
||||
## 基本语法
|
||||
|
||||
```yaml
|
||||
- type: text
|
||||
box: [x, y, width, height] # 位置和尺寸(英寸)
|
||||
content: "文本内容"
|
||||
font:
|
||||
size: 18 # 字号(磅)
|
||||
bold: true # 粗体
|
||||
italic: false # 斜体
|
||||
color: "#ff0000" # 颜色
|
||||
align: center # left/center/right
|
||||
```
|
||||
|
||||
## 字体属性
|
||||
|
||||
### 基础属性
|
||||
|
||||
| 属性 | 类型 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `size` | 数字 | 字号(磅) | 18 |
|
||||
| `bold` | 布尔 | 粗体 | false |
|
||||
| `italic` | 布尔 | 斜体 | false |
|
||||
| `color` | 字符串 | 颜色(#RRGGBB 或 #RGB) | #000000 |
|
||||
| `align` | 字符串 | 对齐方式 | left |
|
||||
| `family` | 字符串 | 字体族或预设类别 | Arial |
|
||||
|
||||
### 对齐方式
|
||||
|
||||
- `left` - 左对齐
|
||||
- `center` - 居中对齐
|
||||
- `right` - 右对齐
|
||||
|
||||
### 高级字体样式
|
||||
|
||||
- `underline` - 下划线(true/false)
|
||||
- `strikethrough` - 删除线(true/false)
|
||||
|
||||
### 段落属性
|
||||
|
||||
- `line_spacing` - 行距倍数(如 1.5)
|
||||
- `space_before` - 段前间距(磅)
|
||||
- `space_after` - 段后间距(磅)
|
||||
|
||||
### 高级属性
|
||||
|
||||
- `baseline` - 基线位置(normal/superscript/subscript)
|
||||
- `caps` - 大小写转换(normal/allcaps/smallcaps)
|
||||
|
||||
## 示例
|
||||
|
||||
### 简单文本
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
box: [1, 1, 8, 1]
|
||||
content: "Hello, World!"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
```
|
||||
|
||||
### 多行文本
|
||||
|
||||
```yaml
|
||||
- type: text
|
||||
box: [1, 1, 8, 2]
|
||||
content: "第一行\n第二行\n第三行"
|
||||
font:
|
||||
size: 18
|
||||
```
|
||||
|
||||
### 样式化文本
|
||||
|
||||
```yaml
|
||||
- type: text
|
||||
box: [1, 1, 8, 1]
|
||||
content: "标题文本"
|
||||
font:
|
||||
size: 32
|
||||
bold: true
|
||||
color: "#2c3e50"
|
||||
underline: true
|
||||
```
|
||||
|
||||
### 使用字体主题
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
fonts:
|
||||
title:
|
||||
family: "cjk-sans"
|
||||
size: 44
|
||||
bold: true
|
||||
color: "#2c3e50"
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
content: "标题文本"
|
||||
box: [1, 1, 8, 1]
|
||||
font: "@title" # 引用字体主题
|
||||
```
|
||||
|
||||
## 特性
|
||||
|
||||
文本框默认启用自动换行,文字超出宽度时会自动换行。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [字体主题系统](../fonts.md) - 字体配置和主题管理
|
||||
- [坐标系统](../reference/coordinates.md) - 位置和尺寸单位
|
||||
- [颜色格式](../reference/colors.md) - 颜色表示方法
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
5
docs/examples.md
Normal file
5
docs/examples.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 示例集合
|
||||
|
||||
本文档为占位文件,未来将添加更多示例。
|
||||
|
||||
目前各主题文档中已内嵌代码示例供参考。
|
||||
198
docs/fonts.md
Normal file
198
docs/fonts.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 字体主题系统
|
||||
|
||||
字体主题系统允许你定义可复用的字体配置,统一管理演示文稿的字体样式。
|
||||
|
||||
## 定义字体主题
|
||||
|
||||
在 `metadata.fonts` 中定义命名字体配置:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9"
|
||||
fonts:
|
||||
title:
|
||||
family: "cjk-sans"
|
||||
size: 44
|
||||
bold: true
|
||||
color: "#2c3e50"
|
||||
body:
|
||||
family: "sans"
|
||||
size: 18
|
||||
color: "#34495e"
|
||||
line_spacing: 1.5
|
||||
fonts_default: "@body" # 默认字体(可选)
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
content: "标题文本"
|
||||
box: [1, 1, 8, 1]
|
||||
font: "@title" # 引用字体主题
|
||||
|
||||
- type: text
|
||||
content: "正文内容"
|
||||
box: [1, 2.5, 8, 2]
|
||||
# 未定义 font 时使用 fonts_default
|
||||
```
|
||||
|
||||
## 预设字体类别
|
||||
|
||||
系统提供五种预设字体类别,自动映射到跨平台通用字体:
|
||||
|
||||
| 类别 | 映射字体 | 说明 |
|
||||
|------|---------|------|
|
||||
| `sans` | Arial | 西文无衬线 |
|
||||
| `serif` | Times New Roman | 西文衬线 |
|
||||
| `mono` | Courier New | 等宽字体 |
|
||||
| `cjk-sans` | Microsoft YaHei | 中文无衬线 |
|
||||
| `cjk-serif` | SimSun | 中文衬线 |
|
||||
|
||||
### 使用预设类别
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
fonts:
|
||||
body:
|
||||
family: "cjk-sans" # 自动映射到 Microsoft YaHei
|
||||
size: 18
|
||||
```
|
||||
|
||||
## 字体引用方式
|
||||
|
||||
### 1. 整体引用
|
||||
|
||||
完全使用定义的字体配置:
|
||||
|
||||
```yaml
|
||||
font: "@title"
|
||||
```
|
||||
|
||||
### 2. 继承覆盖
|
||||
|
||||
继承字体配置并覆盖特定属性:
|
||||
|
||||
```yaml
|
||||
font:
|
||||
parent: "@title"
|
||||
size: 60 # 覆盖字号
|
||||
color: "#ff0000" # 覆盖颜色
|
||||
```
|
||||
|
||||
### 3. 独立定义
|
||||
|
||||
完全自定义字体:
|
||||
|
||||
```yaml
|
||||
font:
|
||||
family: "SimSun"
|
||||
size: 24
|
||||
bold: true
|
||||
```
|
||||
|
||||
## 扩展字体属性
|
||||
|
||||
除了基础属性(size、bold、italic、color、align),还支持:
|
||||
|
||||
### 字体样式
|
||||
|
||||
- `family`:字体族名称或预设类别
|
||||
- `underline`:下划线(true/false)
|
||||
- `strikethrough`:删除线(true/false)
|
||||
|
||||
### 段落属性
|
||||
|
||||
- `line_spacing`:行距倍数(如 1.5)
|
||||
- `space_before`:段前间距(磅)
|
||||
- `space_after`:段后间距(磅)
|
||||
|
||||
### 高级属性
|
||||
|
||||
- `baseline`:基线位置(normal/superscript/subscript)
|
||||
- `caps`:大小写转换(normal/allcaps/smallcaps)
|
||||
|
||||
## 完整示例
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9"
|
||||
fonts:
|
||||
heading:
|
||||
family: "cjk-sans"
|
||||
size: 32
|
||||
bold: true
|
||||
color: "#2c3e50"
|
||||
line_spacing: 1.2
|
||||
space_after: 12
|
||||
body:
|
||||
family: "sans"
|
||||
size: 18
|
||||
color: "#34495e"
|
||||
line_spacing: 1.5
|
||||
fonts_default: "@body"
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
content: "章节标题"
|
||||
box: [1, 1, 8, 1]
|
||||
font: "@heading"
|
||||
|
||||
- type: text
|
||||
content: "正文内容\n支持多行文本"
|
||||
box: [1, 2, 8, 2]
|
||||
font:
|
||||
parent: "@body"
|
||||
underline: true
|
||||
|
||||
- type: table
|
||||
position: [1, 4]
|
||||
col_widths: [3, 3]
|
||||
data:
|
||||
- ["列1", "列2"]
|
||||
- ["数据1", "数据2"]
|
||||
font: "@body"
|
||||
header_font:
|
||||
parent: "@body"
|
||||
bold: true
|
||||
color: "#ffffff"
|
||||
style:
|
||||
header_bg: "#3498db"
|
||||
```
|
||||
|
||||
## 跨域引用
|
||||
|
||||
### 文档元素引用模板库字体
|
||||
|
||||
```yaml
|
||||
# templates.yaml(模板库)
|
||||
metadata:
|
||||
size: "16:9"
|
||||
fonts:
|
||||
template-title:
|
||||
family: "cjk-sans"
|
||||
size: 44
|
||||
bold: true
|
||||
|
||||
# presentation.yaml(文档)
|
||||
metadata:
|
||||
size: "16:9"
|
||||
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
content: "标题"
|
||||
font: "@template-title" # 文档元素可以引用模板库字体
|
||||
```
|
||||
|
||||
### 引用规则
|
||||
|
||||
- 文档元素 → 文档字体
|
||||
- 文档元素 → 模板库字体
|
||||
- 模板元素 → 模板库字体
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [外部模板库](templates/external-library.md) - 模板库字体配置
|
||||
- [作用域系统](../development/scope-system.md) - 字体作用域详细规则
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
17
docs/reference/_index.md
Normal file
17
docs/reference/_index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# API 参考文档
|
||||
|
||||
本目录包含各种 API 参考文档。
|
||||
|
||||
## 参考主题
|
||||
|
||||
- [命令行选项](commands.md) - 所有命令和参数
|
||||
- [坐标系统](coordinates.md) - 位置和尺寸单位
|
||||
- [颜色格式](colors.md) - 颜色表示方法
|
||||
- [验证功能](validation.md) - YAML 验证说明
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [用户指南](../user-guide.md) - 完整使用说明
|
||||
- [故障排查](../troubleshooting.md) - 常见问题解决
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
141
docs/reference/colors.md
Normal file
141
docs/reference/colors.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 颜色格式
|
||||
|
||||
yaml2pptx 支持两种十六进制颜色格式。
|
||||
|
||||
## 支持的格式
|
||||
|
||||
### 短格式 #RGB
|
||||
|
||||
3 位十六进制,每位颜色值重复一次:
|
||||
|
||||
```yaml
|
||||
color: "#fff" # 白色 (#ffffff)
|
||||
color: "#000" # 黑色 (#000000)
|
||||
color: "#f00" # 红色 (#ff0000)
|
||||
color: "#0f0" # 绿色 (#00ff00)
|
||||
color: "#00f" # 蓝色 (#0000ff)
|
||||
```
|
||||
|
||||
### 完整格式 #RRGGBB
|
||||
|
||||
6 位十六进制,标准的颜色表示:
|
||||
|
||||
```yaml
|
||||
color: "#ffffff" # 白色
|
||||
color: "#000000" # 黑色
|
||||
color: "#ff0000" # 红色
|
||||
color: "#00ff00" # 绿色
|
||||
color: "#0000ff" # 蓝色
|
||||
```
|
||||
|
||||
## 常用颜色参考
|
||||
|
||||
### 基础颜色
|
||||
|
||||
| 颜色 | 短格式 | 完整格式 |
|
||||
|------|--------|----------|
|
||||
| 黑色 | `#000` | `#000000` |
|
||||
| 白色 | `#fff` | `#ffffff` |
|
||||
| 红色 | `#f00` | `#ff0000` |
|
||||
| 绿色 | `#0f0` | `#00ff00` |
|
||||
| 蓝色 | `#00f` | `#0000ff` |
|
||||
|
||||
### 常用色彩
|
||||
|
||||
| 颜色 | 完整格式 | 说明 |
|
||||
|------|----------|------|
|
||||
| 灰色 | `#808080` | 中性灰 |
|
||||
| 深灰 | `#333333` | 深灰色 |
|
||||
| 浅灰 | `#cccccc` | 浅灰色 |
|
||||
| 黄色 | `#ffff00` | 纯黄色 |
|
||||
| 青色 | `#00ffff` | 纯青色 |
|
||||
| 品红 | `#ff00ff` | 纯品红 |
|
||||
|
||||
### Material Design 色彩
|
||||
|
||||
| 颜色 | 完整格式 | 说明 |
|
||||
|------|----------|------|
|
||||
| 红色 | `#f44336` | Material Red |
|
||||
| 粉色 | `#e91e63` | Material Pink |
|
||||
| 紫色 | `#9c27b0` | Material Purple |
|
||||
| 深紫 | `#673ab7` | Material Deep Purple |
|
||||
| 靛蓝 | `#3f51b5` | Material Indigo |
|
||||
| 蓝色 | `#2196f3` | Material Blue |
|
||||
| 浅蓝 | `#03a9f4` | Material Light Blue |
|
||||
| 青色 | `#00bcd4` | Material Cyan |
|
||||
| 蓝绿 | `#009688` | Material Teal |
|
||||
| 绿色 | `#4caf50` | Material Green |
|
||||
| 浅绿 | `#8bc34a` | Material Light Green |
|
||||
| 橙色 | `#ff9800` | Material Orange |
|
||||
| 深橙 | `#ff5722` | Material Deep Orange |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 文本颜色
|
||||
|
||||
```yaml
|
||||
- type: text
|
||||
content: "红色文本"
|
||||
font:
|
||||
color: "#ff0000"
|
||||
```
|
||||
|
||||
### 形状填充
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [1, 1, 4, 2]
|
||||
shape: rectangle
|
||||
fill: "#3498db" # 蓝色
|
||||
```
|
||||
|
||||
### 边框颜色
|
||||
|
||||
```yaml
|
||||
- type: shape
|
||||
box: [1, 1, 4, 2]
|
||||
shape: rectangle
|
||||
fill: "#ffffff"
|
||||
line:
|
||||
color: "#000000"
|
||||
width: 2
|
||||
```
|
||||
|
||||
### 表格样式
|
||||
|
||||
```yaml
|
||||
- type: table
|
||||
position: [1, 1]
|
||||
col_widths: [2, 2, 2]
|
||||
data: [...]
|
||||
style:
|
||||
header_bg: "#4a90e2"
|
||||
header_font:
|
||||
color: "#ffffff"
|
||||
```
|
||||
|
||||
## 颜色验证
|
||||
|
||||
系统会自动验证颜色格式:
|
||||
|
||||
- **短格式**:`/#[0-9a-fA-F]{3}/`
|
||||
- **完整格式**:`/#[0-9a-fA-F]{6}/`
|
||||
|
||||
无效的颜色格式会导致验证错误:
|
||||
|
||||
```
|
||||
[幻灯片 1, 元素 1] 无效的颜色格式: red (应为 #RRGGBB)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 颜色代码必须以 `#` 开头
|
||||
- 不支持颜色名称(如 `red`、`blue`)
|
||||
- 不支持 RGB/RGBA 格式(如 `rgb(255, 0, 0)`)
|
||||
- 大小写均可(`#fff` 和 `#FFF` 等效)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [坐标系统](coordinates.md) - 位置和尺寸单位
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
123
docs/reference/commands.md
Normal file
123
docs/reference/commands.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 命令行选项
|
||||
|
||||
yaml2pptx 提供三个主要命令:check、convert、preview。
|
||||
|
||||
## check 命令
|
||||
|
||||
验证 YAML 文件的正确性。
|
||||
|
||||
### 语法
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py check <input> [--template <path>]
|
||||
```
|
||||
|
||||
### 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `input` | 输入的 YAML 文件路径(必需) |
|
||||
| `--template` | 模板库文件路径 |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 验证基本文件
|
||||
uv run yaml2pptx.py check presentation.yaml
|
||||
|
||||
# 验证使用模板的文件
|
||||
uv run yaml2pptx.py check presentation.yaml --template ./templates.yaml
|
||||
```
|
||||
|
||||
## convert 命令
|
||||
|
||||
将 YAML 文件转换为 PPTX 文件。
|
||||
|
||||
### 语法
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py convert <input> [output] [options]
|
||||
```
|
||||
|
||||
### 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `input` | 输入的 YAML 文件路径(必需) |
|
||||
| `output` | 输出的 PPTX 文件路径(可选) |
|
||||
| `--template` | 模板库文件路径 |
|
||||
| `--skip-validation` | 跳过自动验证 |
|
||||
| `--force` / `-f` | 强制覆盖已存在文件 |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 基本转换(自动生成输出文件名)
|
||||
uv run yaml2pptx.py convert presentation.yaml
|
||||
|
||||
# 指定输出文件
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx
|
||||
|
||||
# 使用模板库
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx --template ./templates.yaml
|
||||
|
||||
# 跳过验证
|
||||
uv run yaml2pptx.py convert presentation.yaml --skip-validation
|
||||
|
||||
# 强制覆盖
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx --force
|
||||
```
|
||||
|
||||
## preview 命令
|
||||
|
||||
启动预览服务器,实时查看演示文稿效果。
|
||||
|
||||
### 语法
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py preview <input> [options]
|
||||
```
|
||||
|
||||
### 选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `input` | 输入的 YAML 文件路径(必需) |
|
||||
| `--template` | 模板库文件路径 |
|
||||
| `--port` | 服务器端口(默认:随机端口 30000-40000) |
|
||||
| `--host` | 主机地址(默认:127.0.0.1) |
|
||||
| `--no-browser` | 不自动打开浏览器 |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 启动预览(自动打开浏览器)
|
||||
uv run yaml2pptx.py preview presentation.yaml
|
||||
|
||||
# 指定端口
|
||||
uv run yaml2pptx.py preview presentation.yaml --port 8080
|
||||
|
||||
# 允许局域网访问
|
||||
uv run yaml2pptx.py preview presentation.yaml --host 0.0.0.0
|
||||
|
||||
# 不自动打开浏览器
|
||||
uv run yaml2pptx.py preview presentation.yaml --no-browser
|
||||
|
||||
# 使用模板
|
||||
uv run yaml2pptx.py preview presentation.yaml --template ./templates.yaml
|
||||
```
|
||||
|
||||
## 通用选项
|
||||
|
||||
所有命令都支持以下行为:
|
||||
|
||||
- 自动安装依赖(通过 uv)
|
||||
- 显示友好的错误信息
|
||||
- 支持相对路径和绝对路径
|
||||
|
||||
## 退出代码
|
||||
|
||||
- `0` - 成功
|
||||
- `1` - 错误(文件不存在、验证失败等)
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
154
docs/reference/coordinates.md
Normal file
154
docs/reference/coordinates.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# 坐标系统
|
||||
|
||||
yaml2pptx 使用英寸(inch)作为位置和尺寸的单位。
|
||||
|
||||
## 基本概念
|
||||
|
||||
- **单位**:英寸 (inch)
|
||||
- **原点**:幻灯片左上角 (0, 0)
|
||||
- **方向**:X 轴向右,Y 轴向下
|
||||
|
||||
## 幻灯片尺寸
|
||||
|
||||
### 16:9 宽高比
|
||||
|
||||
- 尺寸:10" × 5.625"
|
||||
- 宽度:10 英寸
|
||||
- 高度:5.625 英寸
|
||||
|
||||
### 4:3 宽高比
|
||||
|
||||
- 尺寸:10" × 7.5"
|
||||
- 宽度:10 英寸
|
||||
- 高度:7.5 英寸
|
||||
|
||||
## box 属性
|
||||
|
||||
`box` 属性用于指定元素的位置和尺寸:
|
||||
|
||||
```yaml
|
||||
box: [x, y, width, height]
|
||||
```
|
||||
|
||||
| 参数 | 说明 | 单位 |
|
||||
|------|------|------|
|
||||
| `x` | 左上角 X 坐标 | 英寸 |
|
||||
| `y` | 左上角 Y 坐标 | 英寸 |
|
||||
| `width` | 元素宽度 | 英寸 |
|
||||
| `height` | 元素高度 | 英寸 |
|
||||
|
||||
## 示例
|
||||
|
||||
```yaml
|
||||
# 位置:(1", 2"),尺寸:宽 8",高 1"
|
||||
box: [1, 2, 8, 1]
|
||||
```
|
||||
|
||||
这表示:
|
||||
- 元素左上角位于 (1", 2")
|
||||
- 元素宽度为 8"
|
||||
- 元素高度为 1"
|
||||
|
||||
## position 属性(表格)
|
||||
|
||||
表格元素使用 `position` 属性指定位置:
|
||||
|
||||
```yaml
|
||||
position: [x, y]
|
||||
```
|
||||
|
||||
| 参数 | 说明 | 单位 |
|
||||
|------|------|------|
|
||||
| `x` | 左上角 X 坐标 | 英寸 |
|
||||
| `y` | 左上角 Y 坐标 | 英寸 |
|
||||
|
||||
## 定位示例
|
||||
|
||||
### 居中文本
|
||||
|
||||
```yaml
|
||||
# 16:9 幻灯片
|
||||
# 宽度 10",要居中一个 8" 宽的元素
|
||||
# x = (10 - 8) / 2 = 1
|
||||
|
||||
- type: text
|
||||
box: [1, 2, 8, 1]
|
||||
content: "居中文本"
|
||||
font:
|
||||
align: center
|
||||
```
|
||||
|
||||
### 全屏背景
|
||||
|
||||
```yaml
|
||||
# 16:9 幻灯片
|
||||
- type: shape
|
||||
box: [0, 0, 10, 5.625] # 完全覆盖
|
||||
shape: rectangle
|
||||
fill: "#ffffff"
|
||||
```
|
||||
|
||||
### 四个象限
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- elements:
|
||||
# 左上象限
|
||||
- type: text
|
||||
box: [0.25, 0.25, 4.5, 2.5]
|
||||
content: "左上"
|
||||
|
||||
# 右上象限
|
||||
- type: text
|
||||
box: [5.25, 0.25, 4.5, 2.5]
|
||||
content: "右上"
|
||||
|
||||
# 左下象限
|
||||
- type: text
|
||||
box: [0.25, 3, 4.5, 2.5]
|
||||
content: "左下"
|
||||
|
||||
# 右下象限
|
||||
- type: text
|
||||
box: [5.25, 3, 4.5, 2.5]
|
||||
content: "右下"
|
||||
```
|
||||
|
||||
## 坐标计算
|
||||
|
||||
### 水平居中
|
||||
|
||||
```python
|
||||
x = (slide_width - element_width) / 2
|
||||
```
|
||||
|
||||
### 垂直居中
|
||||
|
||||
```python
|
||||
y = (slide_height - element_height) / 2
|
||||
```
|
||||
|
||||
### 完全居中
|
||||
|
||||
```yaml
|
||||
# 16:9 幻灯片,居中 4" × 2" 的元素
|
||||
# x = (10 - 4) / 2 = 3
|
||||
# y = (5.625 - 2) / 2 = 1.8125
|
||||
|
||||
- type: shape
|
||||
box: [3, 1.8125, 4, 2]
|
||||
shape: rectangle
|
||||
fill: "#3498db"
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有坐标和尺寸必须为正数
|
||||
- 元素可以超出页面边界(会发出警告)
|
||||
- 浮点数精度建议保留 2-3 位小数
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [颜色格式](colors.md) - 颜色表示方法
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
191
docs/reference/validation.md
Normal file
191
docs/reference/validation.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 验证功能
|
||||
|
||||
yaml2pptx 提供强大的验证功能,在转换前自动检查 YAML 文件,提前发现问题。
|
||||
|
||||
## 验证级别
|
||||
|
||||
验证结果分为三个级别:
|
||||
|
||||
### ERROR(错误)
|
||||
|
||||
阻止转换的严重问题:
|
||||
- 文件不存在(图片、模板文件等)
|
||||
- YAML 语法错误
|
||||
- 必需的变量未提供
|
||||
- 无效的数据类型
|
||||
|
||||
### WARNING(警告)
|
||||
|
||||
影响视觉效果但不阻止转换的问题:
|
||||
- 元素超出页面范围
|
||||
- 字体大小过小或过大
|
||||
- 颜色格式不规范但可解析
|
||||
|
||||
### INFO(信息)
|
||||
|
||||
优化建议和提示:
|
||||
- 性能优化建议
|
||||
- 最佳实践提示
|
||||
|
||||
## 验证内容
|
||||
|
||||
### YAML 语法和结构
|
||||
|
||||
- 检查 YAML 语法是否正确
|
||||
- 检查必需字段是否存在
|
||||
- 检查数据类型是否正确
|
||||
|
||||
### 元素边界
|
||||
|
||||
- 检查元素是否超出页面范围
|
||||
- 0.1 英寸容忍度内的超出会发出警告
|
||||
|
||||
### 资源文件
|
||||
|
||||
- 检查图片文件是否存在
|
||||
- 检查模板文件是否存在
|
||||
- 验证文件路径是否有效
|
||||
|
||||
### 颜色格式
|
||||
|
||||
- 检查颜色格式是否正确(#RGB 或 #RRGGBB)
|
||||
- 检查颜色值是否有效
|
||||
|
||||
### 字体配置
|
||||
|
||||
- 检查字体大小是否合理(<6 或 >72 会警告)
|
||||
- 检查字体引用是否存在
|
||||
- 检查字体引用循环
|
||||
|
||||
### 表格数据
|
||||
|
||||
- 检查表格数据一致性
|
||||
- 检查列数是否匹配
|
||||
|
||||
## 使用验证
|
||||
|
||||
### 独立验证
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py check presentation.yaml
|
||||
```
|
||||
|
||||
### 使用模板时验证
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py check presentation.yaml --template ./templates.yaml
|
||||
```
|
||||
|
||||
### 自动验证
|
||||
|
||||
转换时默认会自动验证:
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx
|
||||
```
|
||||
|
||||
### 跳过验证
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx --skip-validation
|
||||
```
|
||||
|
||||
## 验证结果示例
|
||||
|
||||
### 有错误和警告
|
||||
|
||||
```
|
||||
正在检查 YAML 文件...
|
||||
|
||||
- 错误 (2):
|
||||
[幻灯片 2, 元素 1] 无效的颜色格式: red (应为 #RRGGBB)
|
||||
[幻灯片 3, 元素 2] 图片文件不存在: logo.png
|
||||
|
||||
- 警告 (1):
|
||||
[幻灯片 1, 元素 1] 元素右边界超出: 10.50 > 10
|
||||
|
||||
检查完成: 发现 2 个错误, 1 个警告
|
||||
转换已终止
|
||||
```
|
||||
|
||||
### 验证通过
|
||||
|
||||
```
|
||||
正在检查 YAML 文件...
|
||||
|
||||
检查完成: 未发现问题
|
||||
```
|
||||
|
||||
### 仅警告
|
||||
|
||||
```
|
||||
正在检查 YAML 文件...
|
||||
|
||||
- 警告 (2):
|
||||
[幻灯片 1, 元素 1] 元素右边界超出: 10.05 > 10
|
||||
[幻灯片 2, 元素 2] 字体大小过小: 5
|
||||
|
||||
检查完成: 发现 2 个警告
|
||||
```
|
||||
|
||||
## 常见验证错误
|
||||
|
||||
### 文件不存在
|
||||
|
||||
```
|
||||
错误: 图片文件未找到: images/logo.png
|
||||
```
|
||||
|
||||
**解决方法**:检查图片路径是否正确
|
||||
|
||||
### YAML 语法错误
|
||||
|
||||
```
|
||||
错误: YAML 语法错误: 第 15 行
|
||||
```
|
||||
|
||||
**解决方法**:检查缩进和语法,确保 YAML 格式正确
|
||||
|
||||
### 缺少必需变量
|
||||
|
||||
```
|
||||
错误: 缺少必需变量: title
|
||||
```
|
||||
|
||||
**解决方法**:在 `vars` 中提供该变量
|
||||
|
||||
### 无效颜色格式
|
||||
|
||||
```
|
||||
错误: 无效的颜色格式: red (应为 #RRGGBB)
|
||||
```
|
||||
|
||||
**解决方法**:使用 `#ff0000` 格式
|
||||
|
||||
## 验证容忍度
|
||||
|
||||
几何验证时,允许 0.1 英寸的容忍度:
|
||||
|
||||
- 超出 ≤ 0.1 英寸:不报错
|
||||
- 超出 > 0.1 英寸:发出警告
|
||||
|
||||
```python
|
||||
TOLERANCE = 0.1 # 英寸
|
||||
|
||||
if right > slide_width + TOLERANCE:
|
||||
# 报告 WARNING
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **开发时频繁验证**:使用 `check` 命令快速验证
|
||||
2. **修复所有错误**:确保没有 ERROR 级别的问题
|
||||
3. **关注警告**:WARNING 虽不阻止转换,但可能影响视觉效果
|
||||
4. **使用预览模式**:结合预览模式查看实际效果
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [命令行选项](commands.md) - 所有命令和参数
|
||||
- [故障排查](../troubleshooting.md) - 常见问题解决
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
24
docs/templates/_index.md
vendored
Normal file
24
docs/templates/_index.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# 模板系统文档
|
||||
|
||||
本目录包含模板系统的详细说明。
|
||||
|
||||
## 模板类型
|
||||
|
||||
- [内联模板](inline.md) - 在源文件中定义模板
|
||||
- [外部模板库](external-library.md) - 独立的模板库文件
|
||||
- [混合模式](mixing-mode.md) - 模板与自定义元素组合
|
||||
- [条件渲染](condition-rendering.md) - 元素和页面的条件显示
|
||||
|
||||
## 使用指南
|
||||
|
||||
- **简单场景**:使用内联模板
|
||||
- **跨文档复用**:使用外部模板库
|
||||
- **灵活布局**:使用混合模式
|
||||
- **动态内容**:使用条件渲染
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [字体主题系统](../fonts.md) - 字体配置
|
||||
- [用户指南](../user-guide.md) - 完整使用说明
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
180
docs/templates/condition-rendering.md
vendored
Normal file
180
docs/templates/condition-rendering.md
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
# 条件渲染
|
||||
|
||||
条件渲染允许你根据变量值控制元素和幻灯片的显示。
|
||||
|
||||
## 元素级条件渲染
|
||||
|
||||
使用 `visible` 属性控制元素显示,支持强大的条件表达式:
|
||||
|
||||
### 基本示例
|
||||
|
||||
```yaml
|
||||
# 简单比较
|
||||
- type: text
|
||||
content: "有数据"
|
||||
visible: "{count > 0}"
|
||||
|
||||
# 字符串比较
|
||||
- type: text
|
||||
content: "草稿状态"
|
||||
visible: "{status == 'draft'}"
|
||||
|
||||
# 非空检查(向后兼容)
|
||||
- type: text
|
||||
content: "{subtitle}"
|
||||
visible: "{subtitle != ''}"
|
||||
```
|
||||
|
||||
### 支持的表达式类型
|
||||
|
||||
#### 1. 比较运算
|
||||
|
||||
`==`, `!=`, `>`, `<`, `>=`, `<=`
|
||||
|
||||
```yaml
|
||||
visible: "{score >= 60}"
|
||||
visible: "{price <= 100}"
|
||||
```
|
||||
|
||||
#### 2. 逻辑运算
|
||||
|
||||
`and`, `or`, `not`
|
||||
|
||||
```yaml
|
||||
visible: "{count > 0 and status == 'active'}"
|
||||
visible: "{is_draft or is_preview}"
|
||||
visible: "{not (count == 0)}"
|
||||
```
|
||||
|
||||
#### 3. 成员测试
|
||||
|
||||
`in`, `not in`
|
||||
|
||||
```yaml
|
||||
visible: "{status in ['draft', 'review', 'published']}"
|
||||
visible: "{level in (1, 2, 3)}"
|
||||
visible: "{'test' in version}" # 字符串包含
|
||||
```
|
||||
|
||||
#### 4. 数学运算
|
||||
|
||||
`+`, `-`, `*`, `/`, `%`, `**`
|
||||
|
||||
```yaml
|
||||
visible: "{(price * discount) > 50}"
|
||||
visible: "{(total / count) >= 10}"
|
||||
```
|
||||
|
||||
#### 5. 内置函数
|
||||
|
||||
`int()`, `float()`, `str()`, `len()`, `bool()`, `abs()`, `min()`, `max()`
|
||||
|
||||
```yaml
|
||||
visible: "{len(items) > 0}"
|
||||
visible: "{int(value) > 100}"
|
||||
```
|
||||
|
||||
### 复杂条件示例
|
||||
|
||||
```yaml
|
||||
# 范围检查
|
||||
- type: text
|
||||
content: "评分: {score}"
|
||||
visible: "{score >= 60 and score <= 100}"
|
||||
|
||||
# 多条件组合
|
||||
- type: text
|
||||
content: "管理员或高分用户"
|
||||
visible: "{is_admin or (score >= 90)}"
|
||||
|
||||
# 嵌套条件
|
||||
- type: text
|
||||
content: "符合条件"
|
||||
visible: "{((count > 0) and (status == 'active')) or (is_admin and (level >= 3))}"
|
||||
```
|
||||
|
||||
## 页面级启用控制
|
||||
|
||||
使用 `enabled` 参数控制整个幻灯片是否渲染:
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
# 正常渲染的幻灯片
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "主标题"
|
||||
|
||||
# 临时禁用的幻灯片(开发调试)
|
||||
- enabled: false
|
||||
template: work-in-progress
|
||||
vars:
|
||||
title: "未完成的内容"
|
||||
|
||||
# 继续渲染后续幻灯片
|
||||
- template: content-slide
|
||||
vars:
|
||||
title: "内容页"
|
||||
```
|
||||
|
||||
### enabled 参数说明
|
||||
|
||||
- **类型**:布尔值(`true` 或 `false`)
|
||||
- **默认值**:`true`(未指定时默认启用)
|
||||
- **用途**:临时禁用幻灯片,无需删除或注释 YAML 内容
|
||||
- **场景**:开发调试、版本控制、A/B 测试
|
||||
|
||||
### enabled vs visible 的区别
|
||||
|
||||
| 特性 | `enabled`(页面级) | `visible`(元素级) |
|
||||
|------|-------------------|-------------------|
|
||||
| 作用范围 | 整个幻灯片 | 单个元素 |
|
||||
| 类型 | 布尔值 | 条件表达式 |
|
||||
| 判断时机 | 加载时(静态) | 渲染时(动态) |
|
||||
| 使用场景 | 临时禁用页面 | 条件显示元素 |
|
||||
|
||||
### 示例
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
# 页面启用,但副标题元素可能隐藏
|
||||
- enabled: true
|
||||
template: title-slide
|
||||
vars:
|
||||
title: "标题"
|
||||
subtitle: "" # 空字符串,元素级 visible 会隐藏副标题
|
||||
|
||||
# 整页禁用,不渲染
|
||||
- enabled: false
|
||||
elements:
|
||||
- type: text
|
||||
content: "这一页不会出现在最终 PPTX 中"
|
||||
box: [1, 1, 8, 1]
|
||||
font: {size: 44}
|
||||
```
|
||||
|
||||
## 安全策略
|
||||
|
||||
条件表达式评估使用 simpleeval 库,具有以下安全限制:
|
||||
|
||||
- 表达式最大长度限制:500 字符
|
||||
- 禁止属性访问(obj.attr)
|
||||
- 禁止函数定义(lambda, def)
|
||||
- 禁止模块导入(import)
|
||||
- 白名单函数限制
|
||||
|
||||
## 错误处理
|
||||
|
||||
常见错误信息:
|
||||
|
||||
- `条件表达式中的变量未定义` - 使用了未在 vars 中定义的变量
|
||||
- `条件表达式中使用了不支持的函数` - 使用了白名单之外的函数
|
||||
- `条件表达式使用了不支持的语法特性` - 使用了禁止的语法
|
||||
- `不支持属性访问` - 尝试访问对象属性
|
||||
- `条件表达式语法错误` - 表达式语法有误
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [内联模板](inline.md) - 在源文件中定义模板
|
||||
- [混合模式](mixing-mode.md) - 模板与自定义元素组合
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
192
docs/templates/external-library.md
vendored
Normal file
192
docs/templates/external-library.md
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
# 外部模板库
|
||||
|
||||
外部模板库是一个包含多个模板的 YAML 文件,适合跨文档复用和团队共享。
|
||||
|
||||
## 创建模板库文件
|
||||
|
||||
创建模板库文件 `templates.yaml`:
|
||||
|
||||
```yaml
|
||||
# 模板库元数据(必需)
|
||||
metadata:
|
||||
size: "16:9" # 必需:模板库尺寸,必须与使用它的文档一致
|
||||
description: "公司标准模板库" # 可选:描述信息
|
||||
fonts: # 可选:模板库字体主题
|
||||
template-title:
|
||||
family: "cjk-sans"
|
||||
size: 44
|
||||
bold: true
|
||||
color: "#2c3e50"
|
||||
template-body:
|
||||
family: "sans"
|
||||
size: 20
|
||||
color: "#34495e"
|
||||
fonts_default: "@template-body" # 可选:模板库默认字体
|
||||
|
||||
# 模板定义(必需)
|
||||
templates:
|
||||
title-slide:
|
||||
description: "标题页模板"
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
- name: subtitle
|
||||
required: false
|
||||
default: ""
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 2, 8, 1]
|
||||
content: "{title}"
|
||||
font: "@template-title" # 引用模板库字体
|
||||
- type: text
|
||||
box: [1, 3.5, 8, 0.5]
|
||||
content: "{subtitle}"
|
||||
visible: "{subtitle != ''}"
|
||||
font:
|
||||
parent: "@template-title"
|
||||
size: 24
|
||||
align: center
|
||||
|
||||
content-slide:
|
||||
description: "内容页模板"
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
- name: content
|
||||
required: true
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 1, 8, 0.8]
|
||||
content: "{title}"
|
||||
font:
|
||||
parent: "@template-title"
|
||||
size: 32
|
||||
- type: text
|
||||
box: [1, 2, 8, 3]
|
||||
content: "{content}"
|
||||
# 未指定 font 时使用 fonts_default
|
||||
```
|
||||
|
||||
## 重要说明
|
||||
|
||||
### 1. metadata 字段是必需的
|
||||
|
||||
- `metadata.size` 必须指定("16:9" 或 "4:3")
|
||||
- 模板库的 size 必须与使用它的文档 size 一致,否则会报错
|
||||
|
||||
### 2. 字体主题系统
|
||||
|
||||
- 模板库可以定义自己的字体主题(`metadata.fonts`)
|
||||
- 文档可以引用模板库的字体(跨域引用)
|
||||
- 模板库不能引用文档的字体(单向引用)
|
||||
- 模板库的 `fonts_default` 只能引用模板库内部字体
|
||||
|
||||
### 3. 字体级联规则
|
||||
|
||||
- 模板元素未指定字体时,使用模板库的 `fonts_default`
|
||||
- 如果模板库没有 `fonts_default`,使用文档的 `fonts_default`
|
||||
- 如果都没有,使用系统默认字体
|
||||
|
||||
## 使用外部模板库
|
||||
|
||||
在命令行中指定模板库文件:
|
||||
|
||||
```bash
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx --template ./templates.yaml
|
||||
```
|
||||
|
||||
在 YAML 文件中引用模板:
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "我的演示文稿"
|
||||
subtitle: "使用外部模板库"
|
||||
|
||||
- template: content-slide
|
||||
vars:
|
||||
title: "第一章"
|
||||
content: "这是内容"
|
||||
```
|
||||
|
||||
## 模板库特性
|
||||
|
||||
- 单个文件包含多个模板
|
||||
- 支持模板库元数据(description、version、author)
|
||||
- 每个模板可以有独立的 description
|
||||
- 便于版本控制和分发
|
||||
- 支持相对路径的图片资源(相对于模板库文件所在目录)
|
||||
|
||||
## 模板库文件结构
|
||||
|
||||
```yaml
|
||||
# 顶层元数据(可选)
|
||||
description: "模板库描述"
|
||||
version: "版本号"
|
||||
author: "作者"
|
||||
|
||||
# 模板定义(必需)
|
||||
templates:
|
||||
模板名称1:
|
||||
description: "模板描述(可选)"
|
||||
vars: [...]
|
||||
elements: [...]
|
||||
|
||||
模板名称2:
|
||||
description: "模板描述(可选)"
|
||||
vars: [...]
|
||||
elements: [...]
|
||||
```
|
||||
|
||||
## 模板 description 字段
|
||||
|
||||
模板可以包含可选的 `description` 字段,用于描述模板的用途和设计意图:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
title-slide:
|
||||
description: "用于章节标题页的模板,包含主标题和副标题"
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 2, 8, 1]
|
||||
content: "{title}"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
```
|
||||
|
||||
## 资源路径解析
|
||||
|
||||
模板库中的图片资源使用相对于模板库文件所在目录的路径:
|
||||
|
||||
```yaml
|
||||
# templates.yaml 所在目录:/path/to/templates/
|
||||
templates:
|
||||
logo-slide:
|
||||
elements:
|
||||
- type: image
|
||||
src: "images/logo.png" # 相对于 templates.yaml 所在目录
|
||||
box: [1, 1, 2, 2]
|
||||
```
|
||||
|
||||
## 与内联模板的对比
|
||||
|
||||
| 特性 | 内联模板 | 外部模板库 |
|
||||
|------|---------|-----------|
|
||||
| 定义位置 | 源文件中 | 独立文件 |
|
||||
| 适用场景 | 单文档使用 | 跨文档复用 |
|
||||
| 命令行参数 | 无需 | 需要 `--template` |
|
||||
| 模板间引用 | 不支持 | 支持 |
|
||||
| 字体主题 | 使用文档字体 | 可定义独立字体 |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [内联模板](inline.md) - 在源文件中定义模板
|
||||
- [混合模式](mixing-mode.md) - 模板与自定义元素组合
|
||||
- [字体主题系统](../fonts.md) - 字体配置和跨域引用
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
190
docs/templates/inline.md
vendored
Normal file
190
docs/templates/inline.md
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
# 内联模板
|
||||
|
||||
内联模板允许你在 YAML 源文件中直接定义模板,无需创建单独的模板文件。
|
||||
|
||||
## 定义内联模板
|
||||
|
||||
在 YAML 文件顶层添加 `templates` 字段:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9"
|
||||
|
||||
templates:
|
||||
title-slide:
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
- name: subtitle
|
||||
required: false
|
||||
default: ""
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 2, 8, 1]
|
||||
content: "{title}"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
align: center
|
||||
- type: text
|
||||
box: [1, 3.5, 8, 0.5]
|
||||
content: "{subtitle}"
|
||||
visible: "{subtitle != ''}"
|
||||
font:
|
||||
size: 24
|
||||
align: center
|
||||
|
||||
slides:
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "我的演示文稿"
|
||||
subtitle: "使用内联模板"
|
||||
```
|
||||
|
||||
## 变量定义
|
||||
|
||||
### required 变量
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
```
|
||||
|
||||
### 可选变量与默认值
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
- name: subtitle
|
||||
required: false
|
||||
default: ""
|
||||
```
|
||||
|
||||
## 内联模板特性
|
||||
|
||||
- 支持变量替换和条件渲染
|
||||
- 可以与外部模板混合使用
|
||||
- 无需指定 `--template` 参数
|
||||
- 内联模板不能相互引用
|
||||
- 内联和外部模板同名时会发出警告,优先使用内联模板
|
||||
|
||||
## 何时使用内联模板
|
||||
|
||||
**适合使用内联模板**:
|
||||
- 模板仅在单个文档中使用
|
||||
- 快速原型开发
|
||||
- 简单的模板定义(1-3 个元素)
|
||||
- 文档自包含,无需外部依赖
|
||||
|
||||
**适合使用外部模板**:
|
||||
- 需要跨多个文档复用
|
||||
- 复杂的模板定义(>5 个元素)
|
||||
- 团队共享的模板库
|
||||
- 需要版本控制和独立维护
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 命名规范
|
||||
|
||||
- 内联模板使用描述性名称(如 `title-slide`, `content-slide`)
|
||||
- 避免与外部模板同名,否则会发出警告
|
||||
- 使用一致的命名风格(kebab-case 推荐)
|
||||
|
||||
### 2. 模板大小
|
||||
|
||||
- 内联模板建议不超过 50 行
|
||||
- 超过 50 行考虑拆分或使用外部模板
|
||||
- 保持 YAML 文件可读性
|
||||
|
||||
### 3. 混合使用
|
||||
|
||||
- 可以在同一文档中混合使用内联和外部模板
|
||||
- 通用模板使用外部模板(如标题页、结束页)
|
||||
- 文档特定模板使用内联模板
|
||||
|
||||
### 4. 迁移策略
|
||||
|
||||
- 原型阶段使用内联模板快速迭代
|
||||
- 模板稳定后,如需复用则迁移到外部模板
|
||||
- 使用 `--template` 参数指定外部模板库文件
|
||||
|
||||
## 内联模板限制
|
||||
|
||||
- 内联模板不能相互引用(会报错)
|
||||
- 内联和外部模板同名时会发出警告,优先使用内联模板
|
||||
- 内联模板不支持继承或组合
|
||||
|
||||
## 示例
|
||||
|
||||
### 简单标题页
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9"
|
||||
|
||||
templates:
|
||||
simple-title:
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 2.5, 8, 1]
|
||||
content: "{title}"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
align: center
|
||||
|
||||
slides:
|
||||
- template: simple-title
|
||||
vars:
|
||||
title: "欢迎使用 yaml2pptx"
|
||||
```
|
||||
|
||||
### 带条件渲染的模板
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
optional-subtitle:
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
- name: subtitle
|
||||
required: false
|
||||
default: ""
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 2, 8, 1]
|
||||
content: "{title}"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
align: center
|
||||
- type: text
|
||||
box: [1, 3.2, 8, 0.6]
|
||||
content: "{subtitle}"
|
||||
visible: "{subtitle != ''}"
|
||||
font:
|
||||
size: 24
|
||||
align: center
|
||||
|
||||
slides:
|
||||
- template: optional-subtitle
|
||||
vars:
|
||||
title: "标题"
|
||||
subtitle: "有副标题"
|
||||
|
||||
- template: optional-subtitle
|
||||
vars:
|
||||
title: "只有标题"
|
||||
# subtitle 省略,使用默认值 ""
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [外部模板库](external-library.md) - 独立的模板库文件
|
||||
- [混合模式](mixing-mode.md) - 模板与自定义元素组合
|
||||
- [条件渲染](condition-rendering.md) - 元素和页面的条件显示
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
228
docs/templates/mixing-mode.md
vendored
Normal file
228
docs/templates/mixing-mode.md
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
# 混合模式
|
||||
|
||||
混合模式允许你在使用模板的同时添加自定义元素,实现更灵活的布局组合。
|
||||
|
||||
## 基本用法
|
||||
|
||||
在使用模板的幻灯片中,同时指定 `template` 和 `elements` 字段:
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
# 混合模式:模板 + 自定义元素
|
||||
- template: standard-header
|
||||
vars:
|
||||
title: "混合模式示例"
|
||||
theme_color: "#3949ab"
|
||||
elements:
|
||||
# 自定义内容区域
|
||||
- type: text
|
||||
box: [1, 1.5, 8, 1]
|
||||
content: "这是自定义内容"
|
||||
font:
|
||||
size: 24
|
||||
# 自定义形状
|
||||
- type: shape
|
||||
shape: rectangle
|
||||
box: [1, 3, 8, 2]
|
||||
fill: "#f5f5f5"
|
||||
```
|
||||
|
||||
## 变量共享
|
||||
|
||||
自定义元素可以访问模板中定义的变量:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
branded-header:
|
||||
vars:
|
||||
- name: title
|
||||
- name: theme_color
|
||||
default: "#3949ab"
|
||||
elements:
|
||||
- type: shape
|
||||
box: [0, 0, 10, 0.8]
|
||||
fill: "{theme_color}"
|
||||
- type: text
|
||||
box: [0.5, 0.2, 9, 0.5]
|
||||
content: "{title}"
|
||||
|
||||
slides:
|
||||
- template: branded-header
|
||||
vars:
|
||||
title: "我的页面"
|
||||
theme_color: "#4caf50"
|
||||
elements:
|
||||
# 自定义元素使用模板变量
|
||||
- type: shape
|
||||
box: [1, 2, 8, 3]
|
||||
fill: "{theme_color}" # 使用模板的 theme_color
|
||||
```
|
||||
|
||||
## 元素渲染顺序
|
||||
|
||||
混合模式中,元素按以下顺序渲染(z 轴顺序):
|
||||
|
||||
1. **模板元素**(先渲染,在底层)
|
||||
2. **自定义元素**(后渲染,在上层)
|
||||
|
||||
这意味着自定义元素会覆盖在模板元素之上。
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- template: background-template # 提供背景和头部
|
||||
vars:
|
||||
title: "标题"
|
||||
elements:
|
||||
# 这些元素会显示在模板元素之上
|
||||
- type: shape
|
||||
box: [2, 2, 6, 3]
|
||||
fill: "#ffffff" # 白色框会覆盖背景
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
**适合使用混合模式**:
|
||||
|
||||
- 复用统一的头部/底部,自定义中间内容
|
||||
- 使用模板提供的背景和品牌元素,添加页面特定内容
|
||||
- 需要在标准布局基础上添加特殊元素
|
||||
|
||||
## 示例
|
||||
|
||||
### 统一头部 + 自定义内容
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
standard-header:
|
||||
vars:
|
||||
- name: title
|
||||
elements:
|
||||
# 统一的头部样式
|
||||
- type: shape
|
||||
box: [0, 0, 10, 0.8]
|
||||
fill: "#3949ab"
|
||||
- type: text
|
||||
box: [0.5, 0.2, 9, 0.5]
|
||||
content: "{title}"
|
||||
font:
|
||||
color: "#ffffff"
|
||||
|
||||
slides:
|
||||
# 页面 1:头部 + 文本内容
|
||||
- template: standard-header
|
||||
vars:
|
||||
title: "文本页面"
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 1.5, 8, 3]
|
||||
content: "页面内容..."
|
||||
|
||||
# 页面 2:头部 + 表格
|
||||
- template: standard-header
|
||||
vars:
|
||||
title: "数据页面"
|
||||
elements:
|
||||
- type: table
|
||||
position: [1, 1.5]
|
||||
col_widths: [3, 3]
|
||||
data:
|
||||
- ["列1", "列2"]
|
||||
- ["数据1", "数据2"]
|
||||
|
||||
# 页面 3:头部 + 图片
|
||||
- template: standard-header
|
||||
vars:
|
||||
title: "图片页面"
|
||||
elements:
|
||||
- type: image
|
||||
box: [2, 1.5, 6, 3.5]
|
||||
src: "chart.png"
|
||||
```
|
||||
|
||||
### 背景模板 + 覆盖层
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
gradient-bg:
|
||||
vars:
|
||||
- name: accent_color
|
||||
default: "#3498db"
|
||||
elements:
|
||||
- type: shape
|
||||
box: [0, 0, 10, 5.625]
|
||||
fill: "{accent_color}"
|
||||
|
||||
slides:
|
||||
- template: gradient-bg
|
||||
vars:
|
||||
accent_color: "#9b59b6"
|
||||
elements:
|
||||
# 白色内容框覆盖在渐变背景上
|
||||
- type: shape
|
||||
box: [1, 1, 8, 3.625]
|
||||
fill: "#ffffff"
|
||||
- type: text
|
||||
box: [1.5, 2, 7, 2]
|
||||
content: "内容区域"
|
||||
font:
|
||||
size: 32
|
||||
```
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
混合模式完全向后兼容:
|
||||
|
||||
- **纯模板模式**:只指定 `template`,行为不变
|
||||
- **纯自定义模式**:只指定 `elements`,行为不变
|
||||
- **混合模式**:同时指定 `template` 和 `elements`,新功能
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
# 纯模板模式(原有行为)
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "标题"
|
||||
|
||||
# 纯自定义模式(原有行为)
|
||||
- elements:
|
||||
- type: text
|
||||
content: "自定义内容"
|
||||
|
||||
# 混合模式(新功能)
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "标题"
|
||||
elements:
|
||||
- type: text
|
||||
content: "额外内容"
|
||||
```
|
||||
|
||||
## 幻灯片 description 字段
|
||||
|
||||
幻灯片可以包含可选的 `description` 字段,用于描述该幻灯片的作用和内容。**`description` 内容会自动写入 PPT 备注页**,方便在演示时查看演讲说明:
|
||||
|
||||
```yaml
|
||||
slides:
|
||||
- description: "介绍项目背景和目标"
|
||||
template: title-slide
|
||||
vars:
|
||||
title: "项目背景"
|
||||
|
||||
- description: "展示核心功能特性"
|
||||
elements:
|
||||
- type: text
|
||||
content: "功能特性"
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 仅幻灯片级别的 `description` 会写入备注
|
||||
- 模板的 `description` 不会继承到幻灯片备注
|
||||
- `metadata.description` 用于描述整个演示文稿,不写入单个幻灯片备注
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [内联模板](inline.md) - 在源文件中定义模板
|
||||
- [外部模板库](external-library.md) - 独立的模板库文件
|
||||
- [条件渲染](condition-rendering.md) - 元素和页面的条件显示
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
216
docs/troubleshooting.md
Normal file
216
docs/troubleshooting.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# 故障排查
|
||||
|
||||
本文档提供常见问题的解决方法。
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 文件不存在: xxx.yaml
|
||||
|
||||
**原因**:找不到输入文件
|
||||
|
||||
**解决方法**:
|
||||
- 检查文件路径是否正确
|
||||
- 确认文件在当前目录或使用相对/绝对路径
|
||||
- 检查文件名拼写是否正确
|
||||
|
||||
```bash
|
||||
# 错误示例
|
||||
uv run yaml2pptx.py convert presntation.yaml # 拼写错误
|
||||
|
||||
# 正确示例
|
||||
uv run yaml2pptx.py convert presentation.yaml
|
||||
```
|
||||
|
||||
### YAML 语法错误: 第 X 行
|
||||
|
||||
**原因**:YAML 格式错误
|
||||
|
||||
**解决方法**:
|
||||
- 检查缩进是否正确(使用空格,不要使用 Tab)
|
||||
- 确保冒号后有空格
|
||||
- 检查引号是否成对
|
||||
- 使用 YAML 验证工具检查语法
|
||||
|
||||
```yaml
|
||||
# 错误示例
|
||||
slides:
|
||||
- elements: # 缩进错误
|
||||
- type: text
|
||||
content: "hello"
|
||||
|
||||
# 正确示例
|
||||
slides:
|
||||
- elements:
|
||||
- type: text
|
||||
content: "hello"
|
||||
```
|
||||
|
||||
### 模板文件不存在: xxx
|
||||
|
||||
**原因**:模板文件未找到
|
||||
|
||||
**解决方法**:
|
||||
- 检查 `--template` 参数是否正确
|
||||
- 确认模板库文件存在
|
||||
- 检查模板名称拼写
|
||||
|
||||
```bash
|
||||
# 错误示例
|
||||
uv run yaml2pptx.py convert presentation.yaml --template ./templat.yaml # 拼写错误
|
||||
|
||||
# 正确示例
|
||||
uv run yaml2pptx.py convert presentation.yaml --template ./templates.yaml
|
||||
```
|
||||
|
||||
### 缺少必需变量: xxx
|
||||
|
||||
**原因**:未提供必需的模板变量
|
||||
|
||||
**解决方法**:
|
||||
- 在 `vars` 中提供该变量
|
||||
- 检查模板定义中哪些变量是 `required: true`
|
||||
|
||||
```yaml
|
||||
# 模板定义
|
||||
templates:
|
||||
title-slide:
|
||||
vars:
|
||||
- name: title
|
||||
required: true
|
||||
|
||||
# 错误示例
|
||||
- template: title-slide
|
||||
vars: {} # 缺少 title
|
||||
|
||||
# 正确示例
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "我的标题"
|
||||
```
|
||||
|
||||
### 图片文件未找到: xxx
|
||||
|
||||
**原因**:图片文件不存在
|
||||
|
||||
**解决方法**:
|
||||
- 检查图片路径是否正确
|
||||
- 确认图片文件存在
|
||||
- 使用相对路径(相对于 YAML 文件位置)
|
||||
|
||||
```yaml
|
||||
# 错误示例
|
||||
- type: image
|
||||
src: "images/logo.png" # 文件不存在
|
||||
|
||||
# 正确示例
|
||||
- type: image
|
||||
src: "../assets/logo.png" # 使用正确的相对路径
|
||||
```
|
||||
|
||||
### 无效的颜色格式: xxx
|
||||
|
||||
**原因**:颜色格式不符合要求
|
||||
|
||||
**解决方法**:
|
||||
- 使用 `#RGB` 或 `#RRGGBB` 格式
|
||||
- 不要使用颜色名称(如 `red`、`blue`)
|
||||
|
||||
```yaml
|
||||
# 错误示例
|
||||
font:
|
||||
color: "red"
|
||||
|
||||
# 正确示例
|
||||
font:
|
||||
color: "#ff0000"
|
||||
```
|
||||
|
||||
## 其他问题
|
||||
|
||||
### 元素超出页面范围
|
||||
|
||||
**警告**:元素右边界超出: 10.50 > 10
|
||||
|
||||
**说明**:元素的右边界超出了幻灯片宽度
|
||||
|
||||
**解决方法**:
|
||||
- 调整元素的 `box` 尺寸
|
||||
- 确保元素在页面范围内
|
||||
- 0.1 英寸内的超出是允许的
|
||||
|
||||
```yaml
|
||||
# 16:9 幻灯片,最大宽度 10"
|
||||
# 错误示例
|
||||
box: [1, 1, 9, 1] # 右边界 = 1 + 9 = 10"(警告)
|
||||
|
||||
# 正确示例
|
||||
box: [1, 1, 8.9, 1] # 右边界 = 1 + 8.9 = 9.9"(正常)
|
||||
```
|
||||
|
||||
### 模板名称冲突
|
||||
|
||||
**警告**:模板名称冲突: 'title-slide'
|
||||
|
||||
**说明**:内联和外部模板同名
|
||||
|
||||
**解决方法**:
|
||||
- 重命名其中一个模板
|
||||
- 系统会优先使用内联模板
|
||||
|
||||
### 字体大小过小
|
||||
|
||||
**警告**:字体大小过小: 5
|
||||
|
||||
**说明**:字体小于 6pt 可能难以阅读
|
||||
|
||||
**解决方法**:
|
||||
- 增加字体大小到至少 6pt
|
||||
- 建议使用 12pt 以上
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 使用验证功能
|
||||
|
||||
```bash
|
||||
# 在转换前验证
|
||||
uv run yaml2pptx.py check presentation.yaml
|
||||
```
|
||||
|
||||
### 使用预览模式
|
||||
|
||||
```bash
|
||||
# 实时查看效果
|
||||
uv run yaml2pptx.py preview presentation.yaml
|
||||
```
|
||||
|
||||
### 检查 YAML 语法
|
||||
|
||||
使用在线 YAML 验证工具:
|
||||
- https://www.yamllint.com/
|
||||
- https://yaml-online-parser.appspot.com/
|
||||
|
||||
### 查看详细错误
|
||||
|
||||
使用 `-v` 参数查看详细输出:
|
||||
|
||||
```bash
|
||||
uv run python -c "
|
||||
import yaml
|
||||
with open('presentation.yaml') as f:
|
||||
yaml.safe_load(f)
|
||||
"
|
||||
```
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果以上方法无法解决问题:
|
||||
|
||||
1. 查看 [开发文档](../development/) 了解更多信息
|
||||
2. 检查 GitHub Issues
|
||||
3. 提交新的 Issue,包含:
|
||||
- 完整的错误信息
|
||||
- YAML 文件内容(脱敏后)
|
||||
- 使用的命令
|
||||
- 系统环境信息
|
||||
|
||||
[返回文档索引](../README.md)
|
||||
221
docs/user-guide.md
Normal file
221
docs/user-guide.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# 用户指南
|
||||
|
||||
本指南提供 yaml2pptx 的完整使用说明。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **YAML 声明式语法** - 使用简单易读的 YAML 定义演示文稿
|
||||
- **智能验证** - 转换前自动检查 YAML 文件,提前发现问题
|
||||
- **模板系统** - 支持参数化模板,复用幻灯片布局
|
||||
- **丰富的元素类型** - 文本、图片、形状、表格
|
||||
- **实时预览** - 浏览器预览模式,支持热重载
|
||||
- **灵活尺寸** - 支持 16:9 和 4:3 两种宽高比
|
||||
- **模块化架构** - 易于扩展和维护
|
||||
|
||||
## 安装
|
||||
|
||||
本工具使用 [uv](https://github.com/astral-sh/uv) 管理依赖。项目依赖在 pyproject.toml 中声明,运行时会自动安装所需的 Python 包。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 转换 YAML 为 PPTX
|
||||
|
||||
```bash
|
||||
# 转换 YAML 为 PPTX
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx
|
||||
|
||||
# 自动生成输出文件名
|
||||
uv run yaml2pptx.py convert presentation.yaml
|
||||
|
||||
# 使用模板库
|
||||
uv run yaml2pptx.py convert presentation.yaml output.pptx --template ./templates.yaml
|
||||
```
|
||||
|
||||
### 实时预览
|
||||
|
||||
```bash
|
||||
# 启动预览服务器(自动打开浏览器)
|
||||
uv run yaml2pptx.py preview presentation.yaml
|
||||
|
||||
# 指定端口
|
||||
uv run yaml2pptx.py preview presentation.yaml --port 8080
|
||||
|
||||
# 允许局域网访问
|
||||
uv run yaml2pptx.py preview presentation.yaml --host 0.0.0.0
|
||||
|
||||
# 不自动打开浏览器
|
||||
uv run yaml2pptx.py preview presentation.yaml --no-browser
|
||||
```
|
||||
|
||||
预览模式会自动监听文件变化,修改 YAML 文件后浏览器会自动刷新。
|
||||
|
||||
### 验证功能
|
||||
|
||||
在转换前验证 YAML 文件,提前发现问题:
|
||||
|
||||
```bash
|
||||
# 独立验证命令
|
||||
uv run yaml2pptx.py check presentation.yaml
|
||||
|
||||
# 使用模板时验证
|
||||
uv run yaml2pptx.py check presentation.yaml --template ./templates.yaml
|
||||
```
|
||||
|
||||
验证功能会检查:
|
||||
- YAML 语法和结构
|
||||
- 元素是否超出页面范围
|
||||
- 图片和模板文件是否存在
|
||||
- 颜色格式是否正确
|
||||
- 字体大小是否合理
|
||||
- 表格数据是否一致
|
||||
|
||||
**自动验证**:转换时默认会自动验证,如果发现错误会终止转换。可以使用 `--skip-validation` 跳过验证:
|
||||
|
||||
```bash
|
||||
# 跳过自动验证
|
||||
uv run yaml2pptx.py convert presentation.yaml --skip-validation
|
||||
```
|
||||
|
||||
**验证结果示例**:
|
||||
|
||||
```
|
||||
正在检查 YAML 文件...
|
||||
|
||||
- 错误 (2):
|
||||
[幻灯片 2, 元素 1] 无效的颜色格式: red (应为 #RRGGBB)
|
||||
[幻灯片 3, 元素 2] 图片文件不存在: logo.png
|
||||
|
||||
- 警告 (1):
|
||||
[幻灯片 1, 元素 1] 元素右边界超出: 10.50 > 10
|
||||
|
||||
检查完成: 发现 2 个错误, 1 个警告
|
||||
```
|
||||
|
||||
- **ERROR**:阻止转换的严重问题(文件不存在、语法错误等)
|
||||
- **WARNING**:影响视觉效果的问题(元素超出页面、字体太小等)
|
||||
- **INFO**:优化建议
|
||||
|
||||
## YAML 语法基础
|
||||
|
||||
### 最小示例
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9" # 或 "4:3"
|
||||
|
||||
slides:
|
||||
- background:
|
||||
color: "#ffffff"
|
||||
elements:
|
||||
- type: text
|
||||
box: [1, 1, 8, 1]
|
||||
content: "Hello, World!"
|
||||
font:
|
||||
size: 44
|
||||
bold: true
|
||||
color: "#333333"
|
||||
align: center
|
||||
```
|
||||
|
||||
### description 字段
|
||||
|
||||
`metadata.description` 字段用于描述整个演示文稿的概要和用途,仅用于文档目的,不影响生成的 PPTX 文件:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9"
|
||||
description: "2024年度项目进展总结,包含背景、成果和展望"
|
||||
```
|
||||
|
||||
### 使用模板
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
size: "16:9"
|
||||
|
||||
slides:
|
||||
- template: title-slide
|
||||
vars:
|
||||
title: "我的演示文稿"
|
||||
subtitle: "使用 yaml2pptx 创建"
|
||||
author: "张三"
|
||||
|
||||
- template: content-slide
|
||||
vars:
|
||||
title: "功能概览"
|
||||
content: "yaml2pptx 支持多种元素类型..."
|
||||
```
|
||||
|
||||
## 使用技巧
|
||||
|
||||
1. **开发流程**:使用预览模式实时查看效果,确认无误后再生成 PPTX
|
||||
2. **模板复用**:为常用布局创建模板,保持演示文稿风格一致
|
||||
3. **相对路径**:图片路径相对于 YAML 文件位置,便于项目管理
|
||||
4. **文本换行**:文本框默认启用自动换行,无需手动处理长文本
|
||||
|
||||
## 扩展性
|
||||
|
||||
yaml2pptx 采用模块化架构,易于扩展:
|
||||
|
||||
- **添加新元素类型**:定义新的元素数据类和渲染方法
|
||||
- **添加新渲染器**:支持输出到其他格式(如 PDF)
|
||||
- **自定义模板**:创建符合你需求的模板库
|
||||
|
||||
详见 [开发文档](../development/extending.md)。
|
||||
|
||||
## 依赖项
|
||||
|
||||
- `python-pptx` - PowerPoint 文件生成
|
||||
- `pyyaml` - YAML 解析
|
||||
- `flask` - 预览服务器
|
||||
- `watchdog` - 文件监听
|
||||
|
||||
依赖在 pyproject.toml 中声明,由 uv 自动管理,无需手动安装。
|
||||
|
||||
## 测试
|
||||
|
||||
项目包含完整的测试套件,使用 pytest 框架。
|
||||
|
||||
### 运行测试
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 测试结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/ # 单元测试 - 测试各模块独立功能
|
||||
├── integration/ # 集成测试 - 测试模块间协作
|
||||
├── e2e/ # 端到端测试 - 测试完整用户场景
|
||||
└── fixtures/ # 测试数据
|
||||
```
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎贡献代码、报告问题或提出建议!
|
||||
|
||||
开发者请参阅 [开发文档](../development/) 了解代码结构和开发规范。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
@@ -78,6 +78,51 @@ def validate_presentation_yaml(data, file_path=""):
|
||||
if not isinstance(data['slides'], list):
|
||||
raise YAMLError(f"{file_path}: 'slides' 必须是一个列表")
|
||||
|
||||
# 验证每个幻灯片的 enabled 字段
|
||||
for i, slide in enumerate(data['slides']):
|
||||
if isinstance(slide, dict) and 'enabled' in slide:
|
||||
if not isinstance(slide['enabled'], bool):
|
||||
raise YAMLError(
|
||||
f"{file_path}: slides[{i}].enabled 必须是布尔值(true 或 false),"
|
||||
f"不支持字符串或条件表达式"
|
||||
)
|
||||
|
||||
# 验证 metadata 字段(如果存在)
|
||||
if 'metadata' in data:
|
||||
validate_metadata(data['metadata'], file_path, context="文档")
|
||||
|
||||
# 验证 templates 字段(内联模板)
|
||||
validate_templates_yaml(data, file_path)
|
||||
|
||||
|
||||
def validate_metadata(metadata, file_path, context="文档"):
|
||||
"""
|
||||
验证 metadata 结构(统一用于文档和模板库)
|
||||
|
||||
Args:
|
||||
metadata: metadata 字典
|
||||
file_path: 文件路径(用于错误消息)
|
||||
context: 上下文描述("文档" 或 "模板库")
|
||||
|
||||
Raises:
|
||||
YAMLError: 结构验证失败
|
||||
"""
|
||||
if not isinstance(metadata, dict):
|
||||
raise YAMLError(f"{file_path}: metadata 必须是字典对象")
|
||||
|
||||
# size 必填
|
||||
if "size" not in metadata:
|
||||
raise YAMLError(f"{file_path}: {context} metadata 缺少必填字段 'size'")
|
||||
|
||||
size = metadata["size"]
|
||||
if size not in ["16:9", "4:3"]:
|
||||
raise YAMLError(
|
||||
f"{file_path}: metadata.size 必须是 '16:9' 或 '4:3',当前值: {size}"
|
||||
)
|
||||
|
||||
# 其他字段可选:version, author, description, fonts, fonts_default
|
||||
# fonts 和 fonts_default 的详细验证在字体主题系统中处理
|
||||
|
||||
|
||||
def validate_template_yaml(data, file_path=""):
|
||||
"""
|
||||
@@ -111,3 +156,101 @@ def validate_template_yaml(data, file_path=""):
|
||||
|
||||
if not isinstance(data['elements'], list):
|
||||
raise YAMLError(f"{file_path}: 'elements' 必须是一个列表")
|
||||
|
||||
|
||||
def validate_templates_yaml(data, file_path=""):
|
||||
"""
|
||||
验证 templates 字段结构(内联模板)
|
||||
|
||||
Args:
|
||||
data: 解析后的 YAML 数据
|
||||
file_path: 文件路径(用于错误消息)
|
||||
|
||||
Raises:
|
||||
YAMLError: 结构验证失败
|
||||
"""
|
||||
# 验证 templates 字段是否为字典
|
||||
if 'templates' in data:
|
||||
if not isinstance(data['templates'], dict):
|
||||
raise YAMLError(f"{file_path}: 'templates' 必须是一个字典")
|
||||
|
||||
# 验证每个内联模板的结构
|
||||
for template_name, template_data in data['templates'].items():
|
||||
# 构建模板位置路径
|
||||
template_location = f"{file_path}.templates.{template_name}"
|
||||
|
||||
# 验证模板是字典
|
||||
if not isinstance(template_data, dict):
|
||||
raise YAMLError(f"{template_location}: 模板定义必须是字典")
|
||||
|
||||
# 验证必需的 elements 字段
|
||||
if 'elements' not in template_data:
|
||||
raise YAMLError(f"{template_location}: 缺少必需字段 'elements'")
|
||||
|
||||
if not isinstance(template_data['elements'], list):
|
||||
raise YAMLError(f"{template_location}: 'elements' 必须是一个列表")
|
||||
|
||||
# 验证可选的 vars 字段
|
||||
if 'vars' in template_data:
|
||||
if not isinstance(template_data['vars'], list):
|
||||
raise YAMLError(f"{template_location}: 'vars' 必须是一个列表")
|
||||
|
||||
# 验证每个变量定义
|
||||
for i, var_def in enumerate(template_data['vars']):
|
||||
if not isinstance(var_def, dict):
|
||||
raise YAMLError(f"{template_location}.vars[{i}]: 变量定义必须是字典")
|
||||
|
||||
# 验证必需的 name 字段
|
||||
if 'name' not in var_def:
|
||||
raise YAMLError(f"{template_location}.vars[{i}]: 缺少必需字段 'name'")
|
||||
|
||||
|
||||
def validate_template_library_yaml(data, file_path=""):
|
||||
"""
|
||||
验证模板库文件结构(外部模板库)
|
||||
|
||||
Args:
|
||||
data: 解析后的 YAML 数据
|
||||
file_path: 文件路径(用于错误消息)
|
||||
|
||||
Raises:
|
||||
YAMLError: 结构验证失败
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
raise YAMLError(f"{file_path}: 模板库文件必须是一个字典对象")
|
||||
|
||||
# 验证必需的 metadata 字段
|
||||
if 'metadata' not in data:
|
||||
raise YAMLError(f"{file_path}: 模板库必须包含 metadata 字段")
|
||||
|
||||
validate_metadata(data['metadata'], file_path, context="模板库")
|
||||
|
||||
# 验证模板库 fonts_default 只能引用模板库内部字体
|
||||
metadata = data['metadata']
|
||||
if 'fonts_default' in metadata and metadata['fonts_default']:
|
||||
fonts_default = metadata['fonts_default']
|
||||
# fonts_default 必须是引用格式
|
||||
if not isinstance(fonts_default, str) or not fonts_default.startswith("@"):
|
||||
raise YAMLError(
|
||||
f"{file_path}: 模板库 fonts_default 必须是引用格式(@xxx),当前值: {fonts_default}"
|
||||
)
|
||||
# fonts_default 引用的配置必须存在于模板库 fonts 中
|
||||
font_name = fonts_default[1:]
|
||||
template_fonts = metadata.get('fonts', {})
|
||||
if font_name not in template_fonts:
|
||||
raise YAMLError(
|
||||
f"{file_path}: 模板库 fonts_default 只能引用模板库内部的字体配置,"
|
||||
f"但 '{fonts_default}' 不存在于模板库 metadata.fonts 中"
|
||||
)
|
||||
|
||||
# 验证必需的 templates 字段
|
||||
if 'templates' not in data:
|
||||
raise YAMLError(f"{file_path}: 缺少必需字段 'templates'")
|
||||
|
||||
if not isinstance(data['templates'], dict):
|
||||
raise YAMLError(f"{file_path}: 'templates' 必须是字典")
|
||||
|
||||
# 递归验证每个模板的结构
|
||||
for template_name, template_data in data['templates'].items():
|
||||
template_location = f"{file_path}.templates.{template_name}"
|
||||
validate_template_yaml(template_data, template_location)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-02
|
||||
@@ -0,0 +1,172 @@
|
||||
## Context
|
||||
|
||||
yaml2pptx 项目目前没有测试代码。项目采用模块化架构,包含以下模块:
|
||||
- core/: 核心领域模型(elements、presentation、template)
|
||||
- loaders/: YAML 加载层
|
||||
- validators/: 验证层(geometry、resource、result、validator)
|
||||
- renderers/: PPTX 和 HTML 渲染
|
||||
- preview/: Flask 预览服务器
|
||||
|
||||
约束条件:
|
||||
1. 必须使用 pytest 框架
|
||||
2. PPTX 文件需要实际生成和验证(Level 2:文件结构、元素数量、内容匹配、位置范围)
|
||||
3. Preview 测试不启动真实服务器
|
||||
4. 临时文件使用系统临时目录并自动清理
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 建立全面的测试体系,覆盖单元、集成、端到端三个层级
|
||||
- 确保核心功能的正确性和稳定性
|
||||
- 为未来重构提供安全网
|
||||
- 测试代码结构清晰、易于维护
|
||||
|
||||
**Non-Goals:**
|
||||
- 性能测试
|
||||
- UI/视觉对比测试(像素级)
|
||||
- 100% 代码覆盖率(追求合理覆盖而非完美数字)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: 测试目录结构
|
||||
|
||||
采用三层测试结构:
|
||||
```
|
||||
tests/
|
||||
├── unit/ # 单元测试 - 测试独立函数/类
|
||||
├── integration/ # 集成测试 - 测试模块间协作
|
||||
├── e2e/ # 端到端测试 - 测试完整用户场景
|
||||
└── fixtures/ # 测试数据
|
||||
```
|
||||
|
||||
**理由**:清晰的分层便于理解和维护,符合测试最佳实践。
|
||||
|
||||
### D2: Fixtures 策略
|
||||
|
||||
使用 pytest 的 conftest.py 集中管理共享 fixtures:
|
||||
- `temp_dir`: 临时目录(使用 pytest 内置 tmp_path)
|
||||
- `sample_yaml`: 最小可用 YAML 文件
|
||||
- `sample_image`: 测试图片(使用 Pillow 创建)
|
||||
- `sample_template`: 测试模板文件和目录
|
||||
- `pptx_validator`: PPTX 验证工具实例
|
||||
|
||||
**理由**:集中管理避免重复,便于维护。
|
||||
|
||||
### D3: PPTX 验证方案
|
||||
|
||||
创建专门的 PptxFileValidator 工具类(Level 2 验证):
|
||||
- 文件级:存在、可打开
|
||||
- 幻灯片级:数量、尺寸
|
||||
- 元素级:类型、数量、内容、位置(容差 0.1 英寸)
|
||||
|
||||
**理由**:Level 2 平衡了覆盖度和复杂度,无需图像对比库。
|
||||
|
||||
### D4: Preview 测试方案
|
||||
|
||||
不启动真实 Flask 服务器,测试以下内容:
|
||||
- `generate_preview_html()` 函数的 HTML 生成
|
||||
- `create_flask_app()` 的路由配置
|
||||
- `YAMLChangeHandler` 的文件变化检测(Mock watchdog)
|
||||
|
||||
**理由**:避免异步服务器的复杂性,测试核心逻辑。
|
||||
|
||||
### D5: Mock 边界
|
||||
|
||||
**需要 Mock:**
|
||||
- `webbrowser.open()`(避免打开浏览器)
|
||||
- `watchdog.Observer`(避免文件监听)
|
||||
- CLI 的 `sys.argv`(使用 CliRunner)
|
||||
|
||||
**不 Mock:**
|
||||
- `python-pptx`(实际生成和验证)
|
||||
- 文件系统(使用临时目录)
|
||||
- `yaml.safe_load()`(实际解析)
|
||||
|
||||
**理由**:核心生成逻辑必须真实测试,外部依赖可以 Mock。
|
||||
|
||||
### D6: 测试数据组织
|
||||
|
||||
```
|
||||
tests/fixtures/
|
||||
├── yaml_samples/
|
||||
│ ├── minimal.yaml # 最小示例
|
||||
│ ├── full_features.yaml # 包含所有功能
|
||||
│ ├── edge_cases/ # 边界情况
|
||||
│ └── invalid/ # 无效样本(测试错误处理)
|
||||
├── templates/ # 测试模板
|
||||
└── images/ # 测试图片
|
||||
```
|
||||
|
||||
**理由**:测试数据与代码分离,便于复用和维护。
|
||||
|
||||
### D7: 临时文件清理
|
||||
|
||||
方案:使用 pytest 内置 `tmp_path` fixture,自动管理临时目录。
|
||||
|
||||
**理由**:pytest 原生支持,无需手动清理代码。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk 1: PPTX 验证可能不够精确
|
||||
**描述**:Level 2 验证不检查像素级渲染效果。
|
||||
|
||||
**缓解措施**:手动验证关键用例的视觉效果,自动化测试覆盖结构正确性。
|
||||
|
||||
### Risk 2: 测试运行时间较长
|
||||
**描述**:实际生成 PPTX 文件会增加测试时间。
|
||||
|
||||
**缓解措施**:不考虑时间限制,优先保证测试覆盖度。
|
||||
|
||||
### Risk 3: 全局变量影响测试
|
||||
**描述**:preview/server.py 使用全局变量(app、change_queue 等)。
|
||||
|
||||
**缓解措施**:在测试中显式重置全局状态,或重构代码减少全局变量依赖。
|
||||
|
||||
### Risk 4: CLI 测试复杂性
|
||||
**描述**:yaml2pptx.py 是单体脚本,难以单独导入测试。
|
||||
|
||||
**缓解措施**:使用 `subprocess` 运行命令并检查输出/退出码。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 步骤
|
||||
|
||||
1. **配置测试环境**
|
||||
- 更新 pyproject.toml 添加测试依赖
|
||||
- 创建 tests/conftest.py
|
||||
|
||||
2. **实现测试基础设施**
|
||||
- 创建 tests/conftest_pptx.py(PPTX 验证工具)
|
||||
- 创建 tests/fixtures/ 目录和测试数据
|
||||
|
||||
3. **实现单元测试**
|
||||
- test_elements.py
|
||||
- test_template.py
|
||||
- test_validators/*.py
|
||||
- test_loaders/test_yaml_loader.py
|
||||
- test_utils.py
|
||||
|
||||
4. **实现集成测试**
|
||||
- test_presentation.py
|
||||
- test_rendering_flow.py
|
||||
- test_validation_flow.py
|
||||
|
||||
5. **实现端到端测试**
|
||||
- test_convert_cmd.py
|
||||
- test_check_cmd.py
|
||||
- test_preview_cmd.py
|
||||
|
||||
6. **更新文档**
|
||||
- README.md 添加测试运行说明
|
||||
- README_DEV.md 添加测试开发指南
|
||||
|
||||
### 回滚策略
|
||||
|
||||
如果测试引入问题:
|
||||
1. 测试代码位于独立目录,不影响生产代码
|
||||
2. 可以通过删除 tests/ 目录完全移除
|
||||
3. pyproject.toml 的测试依赖是可选的
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。所有技术决策已明确。
|
||||
@@ -0,0 +1,56 @@
|
||||
## Why
|
||||
|
||||
yaml2pptx 项目目前没有任何测试代码,存在以下风险:
|
||||
1. 代码重构和功能迭代时缺乏安全网,容易引入回归问题
|
||||
2. 验证器、渲染器等核心模块的逻辑复杂,缺少自动化验证
|
||||
3. 新功能开发时无法快速验证正确性
|
||||
4. 无法保证 PPTX 生成质量的一致性
|
||||
|
||||
现在项目架构已稳定,核心功能基本完成,是建立测试体系的最佳时机。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增完整的测试目录结构 `tests/`,包含单元测试、集成测试、端到端测试
|
||||
- 新增 `pyproject.toml` 测试依赖配置(pytest、pytest-cov 等)
|
||||
- 新增 `tests/conftest.py` 配置共享 fixtures
|
||||
- 新增 `tests/conftest_pptx.py` PPTX 文件验证辅助工具
|
||||
- 新增 `tests/fixtures/` 测试数据目录(YAML 样本、模板、图片)
|
||||
- 实现约 150+ 测试用例覆盖所有核心模块
|
||||
- 测试将实际生成 PPTX 文件并进行 Level 2 验证(文件结构、元素数量、内容匹配、位置范围)
|
||||
- Preview 测试采用方案 A(不启动真实服务器,测试 HTML 生成函数和路由配置)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `test-framework`: pytest 测试框架基础设施,包含配置、fixtures、临时文件管理
|
||||
- `unit-tests`: 单元测试,覆盖各模块独立功能(elements、template、validators、loaders、utils)
|
||||
- `integration-tests`: 集成测试,覆盖模块间协作(presentation、渲染流程、验证流程)
|
||||
- `e2e-tests`: 端到端测试,覆盖 CLI 命令(convert、check、preview)
|
||||
- `pptx-validation`: PPTX 文件验证工具,支持文件级别、幻灯片级别、元素级别的验证
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
无。测试是新增能力,不修改现有功能的行为要求。
|
||||
|
||||
## Impact
|
||||
|
||||
### 代码影响
|
||||
- 新增 `tests/` 目录及所有测试文件
|
||||
- 修改 `pyproject.toml` 添加测试依赖
|
||||
- 可能需要调整部分代码以提升可测试性(如全局变量处理)
|
||||
|
||||
### 依赖增加
|
||||
- `pytest`: 测试框架
|
||||
- `pytest-cov`: 覆盖率报告(可选)
|
||||
- `pytest-mock`: Mock 工具(可选)
|
||||
- `pillow`: 用于创建测试图片
|
||||
|
||||
### 开发流程
|
||||
- 开发者可通过 `uv run pytest` 运行测试
|
||||
- 提交代码前应确保测试通过
|
||||
- 新功能开发应同步编写测试
|
||||
|
||||
### 文档更新
|
||||
- `README.md` 添加测试运行说明
|
||||
- `README_DEV.md` 添加测试开发指南
|
||||
@@ -0,0 +1,96 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Convert 命令端到端测试
|
||||
系统 SHALL 提供 convert 命令的端到端测试。
|
||||
|
||||
#### Scenario: 基本转换
|
||||
- **WHEN** 执行 `yaml2pptx.py convert input.yaml output.pptx`
|
||||
- **THEN** 输出文件成功生成
|
||||
- **AND** 文件可以打开并包含正确内容
|
||||
|
||||
#### Scenario: 自动输出文件名
|
||||
- **WHEN** 执行 `yaml2pptx.py convert input.yaml`(不指定输出)
|
||||
- **THEN** 使用输入文件名生成 .pptx 文件
|
||||
|
||||
#### Scenario: 使用模板
|
||||
- **WHEN** 执行转换时指定 --template-dir
|
||||
- **THEN** 模板被正确加载和应用
|
||||
|
||||
#### Scenario: 跳过验证
|
||||
- **WHEN** 执行转换时指定 --skip-validation
|
||||
- **THEN** 验证步骤被跳过
|
||||
|
||||
#### Scenario: 强制覆盖
|
||||
- **WHEN** 输出文件已存在并使用 --force
|
||||
- **THEN** 现有文件被覆盖
|
||||
|
||||
#### Scenario: 文件已存在不强制
|
||||
- **WHEN** 输出文件已存在且不使用 --force
|
||||
- **THEN** 程序提示文件已存在并退出
|
||||
|
||||
#### Scenario: 无效输入文件
|
||||
- **WHEN** 输入文件不存在
|
||||
- **THEN** 程序显示错误信息并退出
|
||||
|
||||
#### Scenario: 包含所有元素类型的转换
|
||||
- **WHEN** YAML 包含所有元素类型(文本、图片、形状、表格)
|
||||
- **THEN** 生成的 PPTX 包含所有元素并正确渲染
|
||||
|
||||
### Requirement: Check 命令端到端测试
|
||||
系统 SHALL 提供 check 命令的端到端测试。
|
||||
|
||||
#### Scenario: 验证通过
|
||||
- **WHEN** 执行 `yaml2pptx.py check valid.yaml`
|
||||
- **THEN** 显示验证通过消息
|
||||
- **AND** 退出码为 0
|
||||
|
||||
#### Scenario: 验证失败
|
||||
- **WHEN** 执行 `yaml2pptx.py check invalid.yaml`
|
||||
- **THEN** 显示错误信息
|
||||
- **AND** 退出码非 0
|
||||
|
||||
#### Scenario: 验证包含警告
|
||||
- **WHEN** YAML 文件有问题但不阻止转换
|
||||
- **THEN** 显示警告信息
|
||||
- **AND** 验证标记为通过
|
||||
|
||||
#### Scenario: 使用模板验证
|
||||
- **WHEN** YAML 使用模板并指定 --template-dir
|
||||
- **THEN** 模板文件也被验证
|
||||
|
||||
#### Scenario: 多错误报告
|
||||
- **WHEN** YAML 文件包含多个错误
|
||||
- **THEN** 所有错误被列出
|
||||
- **AND** 每个错误包含位置信息
|
||||
|
||||
### Requirement: Preview 命令端到端测试
|
||||
系统 SHALL 提供 preview 命令的端到端测试(不启动真实服务器)。
|
||||
|
||||
#### Scenario: HTML 生成
|
||||
- **WHEN** 调用 generate_preview_html() 并传入有效 YAML
|
||||
- **THEN** 返回完整的 HTML 页面
|
||||
- **AND** HTML 包含所有幻灯片的渲染
|
||||
|
||||
#### Scenario: HTML 包含幻灯片元素
|
||||
- **WHEN** 生成的预览 HTML
|
||||
- **THEN** 每张幻灯片包含正确的元素渲染
|
||||
- **AND** CSS 样式正确应用
|
||||
|
||||
#### Scenario: 错误处理
|
||||
- **WHEN** YAML 文件包含错误
|
||||
- **THEN** generate_preview_html() 返回错误页面 HTML
|
||||
- **AND** 错误信息正确显示
|
||||
|
||||
#### Scenario: SSE 事件流路由
|
||||
- **WHEN** 访问 /events 路由
|
||||
- **THEN** 返回 text/event-stream 内容类型
|
||||
|
||||
#### Scenario: 文件变化处理
|
||||
- **WHEN** YAML 文件被修改
|
||||
- **THEN** YAMLChangeHandler 检测到变化
|
||||
- **AND** 将 'reload' 消息放入队列
|
||||
|
||||
#### Scenario: Flask 应用创建
|
||||
- **WHEN** 调用 create_flask_app()
|
||||
- **THEN** 返回配置好的 Flask 应用
|
||||
- **AND** 包含 / 和 /events 路由
|
||||
@@ -0,0 +1,77 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Presentation 类集成测试
|
||||
系统 SHALL 提供 Presentation 类的集成测试,验证模板加载和幻灯片渲染。
|
||||
|
||||
#### Scenario: 幻灯片渲染
|
||||
- **WHEN** 调用 Presentation.render_slide() 并传入幻灯片数据
|
||||
- **THEN** 返回渲染后的元素列表
|
||||
- **AND** 模板变量被正确替换
|
||||
|
||||
#### Scenario: 模板缓存
|
||||
- **WHEN** 多次调用 Presentation.get_template() 并使用相同模板名
|
||||
- **THEN** 返回缓存的 Template 实例
|
||||
|
||||
#### Scenario: 模板变量传递
|
||||
- **WHEN** 幻灯片使用模板并提供 vars
|
||||
- **THEN** 变量被正确传递给模板并渲染
|
||||
|
||||
### Requirement: 渲染流程集成测试
|
||||
系统 SHALL 提供完整的 YAML 到 PPTX 渲染流程测试。
|
||||
|
||||
#### Scenario: 完整渲染流程
|
||||
- **WHEN** 从 YAML 文件加载演示文稿并生成 PPTX
|
||||
- **THEN** PPTX 文件成功生成
|
||||
- **AND** 文件包含正确数量的幻灯片
|
||||
- **AND** 每张幻灯片包含正确的元素
|
||||
|
||||
#### Scenario: 文本元素渲染
|
||||
- **WHEN** 渲染包含文本元素的幻灯片
|
||||
- **THEN** 生成的 PPTX 包含文本框
|
||||
- **AND** 文本内容、字体、颜色、对齐方式正确
|
||||
|
||||
#### Scenario: 图片元素渲染
|
||||
- **WHEN** 渲染包含图片元素的幻灯片
|
||||
- **THEN** 生成的 PPTX 包含图片
|
||||
- **AND** 图片位置和尺寸正确
|
||||
|
||||
#### Scenario: 形状元素渲染
|
||||
- **WHEN** 渲染包含形状元素的幻灯片
|
||||
- **THEN** 生成的 PPTX 包含形状
|
||||
- **AND** 形状类型、填充、边框正确
|
||||
|
||||
#### Scenario: 表格元素渲染
|
||||
- **WHEN** 渲染包含表格元素的幻灯片
|
||||
- **THEN** 生成的 PPTX 包含表格
|
||||
- **AND** 表格行列数、内容、样式正确
|
||||
|
||||
#### Scenario: 背景渲染
|
||||
- **WHEN** 渲染包含背景的幻灯片
|
||||
- **THEN** 生成的 PPTX 幻灯片背景正确设置
|
||||
|
||||
#### Scenario: 模板幻灯片渲染
|
||||
- **WHEN** 渲染使用模板的幻灯片
|
||||
- **THEN** 模板被正确加载和渲染
|
||||
- **AND** 模板变量被正确替换
|
||||
|
||||
### Requirement: 验证流程集成测试
|
||||
系统 SHALL 提供完整验证流程的集成测试。
|
||||
|
||||
#### Scenario: 完整验证流程
|
||||
- **WHEN** 调用 Validator.validate() 并传入有效 YAML 文件
|
||||
- **THEN** 返回 valid=True 的 ValidationResult
|
||||
- **AND** errors 列表为空
|
||||
|
||||
#### Scenario: 多错误收集
|
||||
- **WHEN** YAML 文件包含多个错误
|
||||
- **THEN** ValidationResult 包含所有发现的错误
|
||||
- **AND** 每个错误包含正确的位置信息
|
||||
|
||||
#### Scenario: 错误和警告分类
|
||||
- **WHEN** YAML 文件包含错误和警告
|
||||
- **THEN** ValidationResult 分别将问题分类到 errors 和 warnings
|
||||
|
||||
#### Scenario: 验证结果格式化
|
||||
- **WHEN** 调用 ValidationResult.format_output()
|
||||
- **THEN** 返回格式化的字符串
|
||||
- **AND** 包含错误、警告、提示的分级显示
|
||||
@@ -0,0 +1,76 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: PPTX 文件验证工具
|
||||
系统 SHALL 提供 PPTX 文件验证工具类,支持文件级别、幻灯片级别、元素级别的验证。
|
||||
|
||||
#### Scenario: 文件级别验证
|
||||
- **WHEN** 验证生成的 PPTX 文件
|
||||
- **THEN** 文件存在且大小大于 0
|
||||
- **AND** 文件可以被 python-pptx 打开
|
||||
|
||||
#### Scenario: 幻灯片数量验证
|
||||
- **WHEN** 验证 PPTX 文件
|
||||
- **THEN** 幻灯片数量与预期一致
|
||||
|
||||
#### Scenario: 幻灯片尺寸验证
|
||||
- **WHEN** 验证 16:9 PPTX 文件
|
||||
- **THEN** 幻灯片宽度为 10 英寸,高度为 5.625 英寸
|
||||
- **AND** 验证 4:3 PPTX 文件时,高度为 7.5 英寸
|
||||
|
||||
#### Scenario: 文本元素验证
|
||||
- **WHEN** 验证包含文本的幻灯片
|
||||
- **THEN** 找到文本框元素
|
||||
- **AND** 文本内容与预期一致
|
||||
- **AND** 字体大小、颜色、对齐方式正确
|
||||
- **AND** 位置在合理范围内(允许 0.1 英寸误差)
|
||||
|
||||
#### Scenario: 图片元素验证
|
||||
- **WHEN** 验证包含图片的幻灯片
|
||||
- **THEN** 找到图片元素
|
||||
- **AND** 图片尺寸和位置与预期一致
|
||||
- **AND** 位置在合理范围内
|
||||
|
||||
#### Scenario: 形状元素验证
|
||||
- **WHEN** 验证包含形状的幻灯片
|
||||
- **THEN** 找到形状元素
|
||||
- **AND** 形状类型正确
|
||||
- **AND** 填充颜色和边框样式正确
|
||||
- **AND** 位置在合理范围内
|
||||
|
||||
#### Scenario: 表格元素验证
|
||||
- **WHEN** 验证包含表格的幻灯片
|
||||
- **THEN** 找到表格元素
|
||||
- **AND** 行列数与预期一致
|
||||
- **AND** 单元格内容与预期一致
|
||||
- **AND** 表头样式正确应用
|
||||
|
||||
#### Scenario: 背景验证
|
||||
- **WHEN** 验证包含背景色的幻灯片
|
||||
- **THEN** 幻灯片背景颜色与预期一致
|
||||
|
||||
#### Scenario: 元素数量验证
|
||||
- **WHEN** 验证幻灯片
|
||||
- **THEN** 元素数量与预期一致
|
||||
- **AND** 元素类型分布正确
|
||||
|
||||
#### Scenario: 位置范围验证
|
||||
- **WHEN** 验证元素位置
|
||||
- **THEN** 元素位置在幻灯片范围内
|
||||
- **AND** 允许 0.1 英寸的容忍误差
|
||||
|
||||
### Requirement: 验证辅助函数
|
||||
系统 SHALL 提供验证辅助函数,简化测试代码。
|
||||
|
||||
#### Scenario: 颜色值比较
|
||||
- **WHEN** 比较 PPTX 颜色与预期颜色
|
||||
- **THEN** 验证辅助函数正确比较 RGB 值
|
||||
- **AND** 考虑颜色转换误差
|
||||
|
||||
#### Scenario: 位置比较
|
||||
- **WHEN** 比较元素位置与预期位置
|
||||
- **THEN** 验证辅助函数考虑容忍度
|
||||
|
||||
#### Scenario: 批量验证
|
||||
- **WHEN** 一次验证多个属性
|
||||
- **THEN** 验证辅助函数返回所有验证结果
|
||||
- **AND** 包含详细的失败信息
|
||||
@@ -0,0 +1,42 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Pytest 测试框架配置
|
||||
项目 SHALL 使用 pytest 作为测试框架,通过 pyproject.toml 配置测试依赖和运行参数。
|
||||
|
||||
#### Scenario: 配置测试依赖
|
||||
- **WHEN** 开发者在 pyproject.toml 中配置测试依赖
|
||||
- **THEN** 项目包含 pytest、pytest-cov、pytest-mock 等依赖
|
||||
|
||||
#### Scenario: 运行测试
|
||||
- **WHEN** 开发者执行 `uv run pytest`
|
||||
- **THEN** 所有测试被发现并执行
|
||||
|
||||
### Requirement: 共享 Fixtures
|
||||
测试框架 SHALL 提供 conftest.py 文件,定义所有测试共享的 fixtures。
|
||||
|
||||
#### Scenario: 临时目录 fixture
|
||||
- **WHEN** 测试使用 `temp_dir` fixture
|
||||
- **THEN** 系统创建一个临时目录并在测试后自动清理
|
||||
|
||||
#### Scenario: 测试 YAML 文件 fixture
|
||||
- **WHEN** 测试使用 `sample_yaml` fixture
|
||||
- **THEN** 系统创建一个包含最小可用内容的测试 YAML 文件
|
||||
|
||||
#### Scenario: 测试图片 fixture
|
||||
- **WHEN** 测试使用 `sample_image` fixture
|
||||
- **THEN** 系统创建一个测试用图片文件
|
||||
|
||||
#### Scenario: 测试模板 fixture
|
||||
- **WHEN** 测试使用 `sample_template` fixture
|
||||
- **THEN** 系统创建包含测试模板的目录
|
||||
|
||||
### Requirement: 临时文件管理
|
||||
测试生成的临时 PPTX 文件 SHALL 存放在系统临时目录,测试后自动清理。
|
||||
|
||||
#### Scenario: 临时 PPTX 输出目录
|
||||
- **WHEN** 测试生成 PPTX 文件
|
||||
- **THEN** 文件存放在系统临时目录的 yaml2pptx_tests 子目录下
|
||||
|
||||
#### Scenario: 自动清理
|
||||
- **WHEN** 测试运行完成
|
||||
- **THEN** 所有生成的 test_*.pptx 文件被删除
|
||||
@@ -0,0 +1,123 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 元素类单元测试
|
||||
系统 SHALL 为所有元素类(TextElement、ImageElement、ShapeElement、TableElement)提供单元测试。
|
||||
|
||||
#### Scenario: TextElement 创建验证
|
||||
- **WHEN** 创建 TextElement 实例
|
||||
- **THEN** box 参数被验证为包含 4 个数字的列表
|
||||
- **AND** 无效的 box 会引发 ValueError
|
||||
|
||||
#### Scenario: TextElement 颜色验证
|
||||
- **WHEN** TextElement 的 font.color 格式无效
|
||||
- **THEN** validate() 返回包含 INVALID_COLOR_FORMAT 错误的 ValidationIssue 列表
|
||||
|
||||
#### Scenario: TextElement 字体大小验证
|
||||
- **WHEN** TextElement 的 font.size 小于 8pt
|
||||
- **THEN** validate() 返回包含 FONT_TOO_SMALL 警告的 ValidationIssue 列表
|
||||
|
||||
#### Scenario: ImageElement 创建验证
|
||||
- **WHEN** 创建 ImageElement 实例
|
||||
- **THEN** src 参数必须非空
|
||||
- **AND** 空的 src 会引发 ValueError
|
||||
|
||||
#### Scenario: ShapeElement 类型验证
|
||||
- **WHEN** ShapeElement 的 shape 类型不支持
|
||||
- **THEN** validate() 返回包含 INVALID_SHAPE_TYPE 错误的 ValidationIssue 列表
|
||||
|
||||
#### Scenario: TableElement 数据验证
|
||||
- **WHEN** TableElement 的 data 行列数不一致
|
||||
- **THEN** validate() 返回包含 TABLE_INCONSISTENT_COLUMNS 错误的 ValidationIssue 列表
|
||||
|
||||
#### Scenario: 元素工厂函数
|
||||
- **WHEN** 调用 create_element() 并传入不同 type
|
||||
- **THEN** 返回对应类型的元素对象
|
||||
- **AND** 不支持的 type 会引发 ValueError
|
||||
|
||||
### Requirement: 模板系统单元测试
|
||||
系统 SHALL 为模板系统(Template 类)提供单元测试。
|
||||
|
||||
#### Scenario: 模板初始化验证
|
||||
- **WHEN** Template 初始化时未指定 templates_dir
|
||||
- **THEN** 引发 YAMLError
|
||||
|
||||
#### Scenario: 模板名称验证
|
||||
- **WHEN** Template 初始化时模板名称包含路径分隔符
|
||||
- **THEN** 引发 YAMLError
|
||||
|
||||
#### Scenario: 变量解析
|
||||
- **WHEN** 调用 resolve_value() 并包含变量引用
|
||||
- **THEN** 返回解析后的值
|
||||
- **AND** 未定义的变量会引发 YAMLError
|
||||
|
||||
#### Scenario: 条件渲染评估
|
||||
- **WHEN** 调用 evaluate_condition() 并传入条件表达式
|
||||
- **THEN** 返回正确的布尔值
|
||||
|
||||
#### Scenario: 模板渲染
|
||||
- **WHEN** 调用 render() 并提供变量值
|
||||
- **THEN** 返回渲染后的元素列表
|
||||
- **AND** 缺少必需变量会引发 YAMLError
|
||||
|
||||
### Requirement: 验证器单元测试
|
||||
系统 SHALL 为所有验证器提供单元测试。
|
||||
|
||||
#### Scenario: 几何验证器 - 元素边界检查
|
||||
- **WHEN** 元素超出页面边界
|
||||
- **THEN** GeometryValidator 返回 ELEMENT_OUT_OF_BOUNDS 警告
|
||||
|
||||
#### Scenario: 几何验证器 - 完全在页面外
|
||||
- **WHEN** 元素完全在页面外
|
||||
- **THEN** GeometryValidator 返回 ELEMENT_COMPLETELY_OUT_OF_BOUNDS 警告
|
||||
|
||||
#### Scenario: 资源验证器 - 图片检查
|
||||
- **WHEN** 图片文件不存在
|
||||
- **THEN** ResourceValidator 返回 IMAGE_FILE_NOT_FOUND 错误
|
||||
|
||||
#### Scenario: 资源验证器 - 模板检查
|
||||
- **WHEN** 模板文件不存在
|
||||
- **THEN** ResourceValidator 返回 TEMPLATE_FILE_NOT_FOUND 错误
|
||||
|
||||
#### Scenario: 主验证器协调
|
||||
- **WHEN** Validator.validate() 执行
|
||||
- **THEN** 调用所有子验证器
|
||||
- **AND** 按级别分类问题(ERROR/WARNING/INFO)
|
||||
|
||||
### Requirement: YAML 加载器单元测试
|
||||
系统 SHALL 为 YAML 加载器提供单元测试。
|
||||
|
||||
#### Scenario: 文件不存在
|
||||
- **WHEN** 加载不存在的文件
|
||||
- **THEN** 引发 YAMLError 并包含 "文件不存在" 消息
|
||||
|
||||
#### Scenario: YAML 语法错误
|
||||
- **WHEN** 加载包含语法错误的 YAML 文件
|
||||
- **THEN** 引发 YAMLError 并包含错误行号信息
|
||||
|
||||
#### Scenario: 演示文稿结构验证
|
||||
- **WHEN** data 缺少 slides 字段
|
||||
- **THEN** validate_presentation_yaml() 引发 YAMLError
|
||||
|
||||
#### Scenario: 模板结构验证
|
||||
- **WHEN** 模板 data 缺少 elements 字段
|
||||
- **THEN** validate_template_yaml() 引发 YAMLError
|
||||
|
||||
### Requirement: 工具函数单元测试
|
||||
系统 SHALL 为工具函数提供单元测试。
|
||||
|
||||
#### Scenario: 颜色转换 - 完整格式
|
||||
- **WHEN** 调用 hex_to_rgb() 并传入 #RRGGBB 格式
|
||||
- **THEN** 返回正确的 (R, G, B) 元组
|
||||
|
||||
#### Scenario: 颜色转换 - 短格式
|
||||
- **WHEN** 调用 hex_to_rgb() 并传入 #RGB 格式
|
||||
- **THEN** 返回正确的 (R, G, B) 元组
|
||||
|
||||
#### Scenario: 颜色转换 - 无效格式
|
||||
- **WHEN** 调用 hex_to_rgb() 并传入无效格式
|
||||
- **THEN** 引发 ValueError
|
||||
|
||||
#### Scenario: 颜色格式验证
|
||||
- **WHEN** 调用 validate_color() 并传入有效颜色
|
||||
- **THEN** 返回 True
|
||||
- **AND** 无效颜色返回 False
|
||||
@@ -0,0 +1,75 @@
|
||||
## 1. 测试环境配置
|
||||
|
||||
- [x] 1.1 更新 pyproject.toml 添加测试依赖(pytest、pytest-cov、pytest-mock、pillow)
|
||||
- [x] 1.2 创建 tests/conftest.py 配置共享 fixtures
|
||||
- [x] 1.3 创建 tests/__init__.py 空文件
|
||||
|
||||
## 2. 测试基础设施
|
||||
|
||||
- [x] 2.1 创建 tests/conftest_pptx.py 实现 PptxFileValidator 工具类
|
||||
- [x] 2.2 创建 tests/fixtures/ 目录结构
|
||||
- [x] 2.3 创建 tests/fixtures/yaml_samples/minimal.yaml 最小示例
|
||||
- [x] 2.4 创建 tests/fixtures/yaml_samples/full_features.yaml 完整功能示例
|
||||
- [x] 2.5 创建 tests/fixtures/yaml_samples/edge_cases/ 边界情况样本
|
||||
- [x] 2.6 创建 tests/fixtures/yaml_samples/invalid/ 无效样本
|
||||
- [x] 2.7 创建 tests/fixtures/templates/test_template.yaml 测试模板
|
||||
- [x] 2.8 添加创建测试图片的 fixture 函数(已在 conftest.py 中实现)
|
||||
|
||||
## 3. 单元测试 - 元素类
|
||||
|
||||
- [x] 3.1 创建 tests/unit/test_elements.py 并实现 TextElement 测试
|
||||
- [x] 3.2 实现 ImageElement 测试
|
||||
- [x] 3.3 实现 ShapeElement 测试
|
||||
- [x] 3.4 实现 TableElement 测试
|
||||
- [x] 3.5 实现 create_element() 工厂函数测试
|
||||
|
||||
## 4. 单元测试 - 模板系统
|
||||
|
||||
- [x] 4.1 创建 tests/unit/test_template.py 并实现模板初始化测试
|
||||
- [x] 4.2 实现变量解析测试(resolve_value、resolve_element)
|
||||
- [x] 4.3 实现条件渲染测试(evaluate_condition)
|
||||
- [x] 4.4 实现模板渲染测试(render)
|
||||
|
||||
## 5. 单元测试 - 验证器
|
||||
|
||||
- [x] 5.1 创建 tests/unit/test_validators/ 目录
|
||||
- [x] 5.2 创建 tests/unit/test_validators/test_geometry.py 并实现边界检查测试
|
||||
- [x] 5.3 创建 tests/unit/test_validators/test_resource.py 并实现资源验证测试
|
||||
- [x] 5.4 创建 tests/unit/test_validators/test_result.py 并实现验证结果测试
|
||||
- [x] 5.5 创建 tests/unit/test_validators/test_validator.py 并实现主验证器测试
|
||||
|
||||
## 6. 单元测试 - 其他模块
|
||||
|
||||
- [x] 6.1 创建 tests/unit/test_loaders/test_yaml_loader.py 并实现加载测试
|
||||
- [x] 6.2 创建 tests/unit/test_utils.py 并实现工具函数测试
|
||||
- [x] 6.3 创建 tests/unit/test_validators/__init__.py 空文件
|
||||
- [x] 6.4 创建 tests/unit/test_loaders/__init__.py 空文件
|
||||
|
||||
## 7. 集成测试
|
||||
|
||||
- [x] 7.1 创建 tests/integration/test_presentation.py 并实现 Presentation 类测试
|
||||
- [x] 7.2 创建 tests/integration/test_rendering_flow.py 并实现渲染流程测试
|
||||
- [x] 7.3 实现文本元素渲染集成测试
|
||||
- [x] 7.4 实现图片元素渲染集成测试
|
||||
- [x] 7.5 实现形状元素渲染集成测试
|
||||
- [x] 7.6 实现表格元素渲染集成测试
|
||||
- [x] 7.7 实现背景渲染集成测试
|
||||
- [x] 7.8 创建 tests/integration/test_validation_flow.py 并实现验证流程测试
|
||||
|
||||
## 8. 端到端测试
|
||||
|
||||
- [x] 8.1 创建 tests/e2e/test_convert_cmd.py 并实现基本转换测试
|
||||
- [x] 8.2 实现自动输出文件名测试
|
||||
- [x] 8.3 实现使用模板测试
|
||||
- [x] 8.4 实现跳过验证测试
|
||||
- [x] 8.5 实现强制覆盖测试
|
||||
- [x] 8.6 实现无效输入文件测试
|
||||
- [x] 8.7 创建 tests/e2e/test_check_cmd.py 并实现验证通过测试
|
||||
- [x] 8.8 实现验证失败测试
|
||||
- [x] 8.9 创建 tests/e2e/test_preview_cmd.py 并实现 HTML 生成测试
|
||||
|
||||
## 9. 文档更新
|
||||
|
||||
- [x] 9.1 更新 README.md 添加测试运行说明
|
||||
- [x] 9.2 更新 README_DEV.md 添加测试开发指南
|
||||
- [x] 9.3 在 README.md 中添加测试覆盖率要求说明
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-02
|
||||
@@ -0,0 +1,102 @@
|
||||
## Context
|
||||
|
||||
当前项目使用 uv 作为 Python 运行环境,依赖声明采用 Inline script metadata 模式(PEP 723)。随着项目发展,模块化架构(core/, loaders/, renderers/, preview/, validators/)已经形成,依赖管理需要在项目层面统一。
|
||||
|
||||
**当前状态:**
|
||||
- yaml2pptx.py 包含 inline metadata 指定依赖
|
||||
- openspec/config.yaml 声明 "脚本使用Inline script metadata来指定脚本的依赖包"
|
||||
- 项目已采用模块化架构,涉及多个 Python 模块
|
||||
|
||||
**约束条件:**
|
||||
- 必须继续使用 uv 作为运行环境
|
||||
- 用户使用方式保持不变(`uv run yaml2pptx.py ...`)
|
||||
- 严禁污染主机环境配置
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 使用 pyproject.toml 统一管理项目依赖
|
||||
- 移除 yaml2pptx.py 中的 inline metadata
|
||||
- 更新开发规范文档以反映新方式
|
||||
- 保持用户 CLI 使用体验不变
|
||||
|
||||
**Non-Goals:**
|
||||
- 不改变 CLI 命令行接口
|
||||
- 不改变功能行为
|
||||
- 不引入新的运行时依赖
|
||||
|
||||
## Decisions
|
||||
|
||||
### 使用 pyproject.toml 而非 uv.lock
|
||||
|
||||
**选择:** 创建 pyproject.toml 作为依赖声明源
|
||||
|
||||
**理由:**
|
||||
- pyproject.toml 是 Python 生态的标准项目配置格式
|
||||
- uv 支持从 pyproject.toml 读取依赖并自动管理
|
||||
- 便于 IDE 集成和代码提示
|
||||
- 符合 Python 项目最佳实践
|
||||
|
||||
**备选方案考虑:**
|
||||
- 仅使用 uv.lock:uv.lock 是生成的锁定文件,不应手动编辑
|
||||
- 继续使用 inline metadata:无法满足项目级依赖管理需求
|
||||
|
||||
### pyproject.toml 内容结构
|
||||
|
||||
**选择:** 采用标准 [project] 配置段
|
||||
|
||||
**理由:**
|
||||
- 符合 PEP 621 标准
|
||||
- uv 原生支持
|
||||
- 便于未来扩展(如可选依赖、开发依赖)
|
||||
|
||||
**配置示例:**
|
||||
```toml
|
||||
[project]
|
||||
name = "yaml2pptx"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"python-pptx",
|
||||
"pyyaml",
|
||||
"flask",
|
||||
"watchdog",
|
||||
]
|
||||
```
|
||||
|
||||
### 移除 openspec/config.yaml 中的 inline metadata 描述
|
||||
|
||||
**选择:** 更新配置上下文,移除 "脚本使用Inline script metadata" 相关描述
|
||||
|
||||
**理由:**
|
||||
- 保持开发规范与实际实现一致
|
||||
- 避免误导 AI 和开发者
|
||||
- 强调 "始终使用 uv 运行" 的核心约束
|
||||
|
||||
**更新后内容:**
|
||||
```
|
||||
本项目编写的python脚本和任何python命令都始终使用uv运行,命令使用uv run python -c "xxx"执行命令;
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| 用户已有工作流可能依赖 inline metadata | 保持 CLI 使用方式不变,`uv run` 命令继续有效 |
|
||||
| pyproject.toml 配置错误可能导致依赖问题 | 严格遵循 PEP 621 标准和 uv 文档 |
|
||||
| 多个依赖管理源可能造成混淆 | 完全移除 inline metadata,单一依赖源 |
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 创建 pyproject.toml 文件,声明项目依赖
|
||||
2. 移除 yaml2pptx.py 中的 inline metadata(第 2-10 行)
|
||||
3. 更新 openspec/config.yaml 中的开发规范
|
||||
4. 更新 README.md 中的依赖管理说明
|
||||
5. 验证 `uv run yaml2pptx.py` 命令仍可正常工作
|
||||
6. 如需要,更新 README_DEV.md
|
||||
|
||||
**回滚策略:** 保留 git 历史,可随时回滚到 inline metadata 模式
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。本次变更技术路径明确,无需进一步决策。
|
||||
@@ -0,0 +1,27 @@
|
||||
## Why
|
||||
|
||||
当前项目使用 Inline script metadata 模式在 yaml2pptx.py 中指定依赖,这种方式在单文件脚本场景下有效,但随着项目模块化架构的发展,依赖管理需要在项目层面统一管理。迁移到 uv 的原生依赖管理模式(pyproject.toml)是更标准、更可维护的做法。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: 移除 yaml2pptx.py 中的 Inline script metadata(第 2-10 行)
|
||||
- 新增 pyproject.toml 文件,使用 uv 的标准依赖管理格式
|
||||
- 更新 openspec/config.yaml 中的开发规范说明(移除 inline metadata 相关描述)
|
||||
- 更新 README.md 文档中的依赖管理说明
|
||||
- 用户命令使用方式保持不变(仍然使用 `uv run yaml2pptx.py ...`)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
无新增功能能力。本次变更仅改变依赖管理的内部实现方式。
|
||||
|
||||
### Modified Capabilities
|
||||
无需修改现有 spec。用户视角的 CLI 行为、API 接口、功能特性均保持不变。
|
||||
|
||||
## Impact
|
||||
|
||||
- yaml2pptx.py: 移除第 2-10 行的 inline metadata
|
||||
- pyproject.toml: 新增项目依赖配置文件
|
||||
- openspec/config.yaml: 更新开发规范(移除 "脚本使用Inline script metadata" 描述)
|
||||
- README.md: 更新依赖管理说明
|
||||
- README_DEV.md: 可能需要同步更新开发规范说明
|
||||
@@ -0,0 +1,18 @@
|
||||
# Spec Changes: None
|
||||
|
||||
本次变更 `migrate-to-uv-package-management` 是纯基础设施变更,不涉及任何功能能力(capability)的变更。
|
||||
|
||||
## 变更范围
|
||||
|
||||
- 从 Inline script metadata 迁移到 pyproject.toml 依赖管理
|
||||
- 更新开发规范文档
|
||||
- 用户视角的 CLI 行为、API 接口、功能特性均保持不变
|
||||
|
||||
## 无需创建 spec 文件的原因
|
||||
|
||||
根据 proposal 的 Capabilities 部分:
|
||||
|
||||
- **New Capabilities**: 无
|
||||
- **Modified Capabilities**: 无
|
||||
|
||||
本次变更仅改变依赖管理的内部实现方式,不影响任何用户可见的行为或系统功能要求,因此无需创建或修改 spec 文件。
|
||||
@@ -0,0 +1,29 @@
|
||||
## 1. 创建 pyproject.toml
|
||||
|
||||
- [x] 1.1 在项目根目录创建 pyproject.toml 文件
|
||||
- [x] 1.2 配置 [project] 段,包含 name、version、requires-python
|
||||
- [x] 1.3 配置 dependencies 列表(python-pptx、pyyaml、flask、watchdog)
|
||||
|
||||
## 2. 移除 yaml2pptx.py 的 inline metadata
|
||||
|
||||
- [x] 2.1 移除 yaml2pptx.py 第 2-10 行的 inline metadata 注释块
|
||||
|
||||
## 3. 更新 openspec/config.yaml
|
||||
|
||||
- [x] 3.1 移除 context 中 "脚本使用Inline script metadata来指定脚本的依赖包和python运行版本" 描述
|
||||
- [x] 3.2 确保保留 "本项目编写的python脚本和任何python命令都始终使用uv运行" 核心约束
|
||||
|
||||
## 4. 更新 README.md
|
||||
|
||||
- [x] 4.1 更新 "安装" 部分,说明使用 pyproject.toml 管理依赖
|
||||
- [x] 4.2 更新 "依赖项" 部分,说明依赖由 pyproject.toml 声明,uv 自动管理
|
||||
|
||||
## 5. 验证
|
||||
|
||||
- [x] 5.1 运行 `uv run yaml2pptx.py check` 验证基本功能
|
||||
- [x] 5.2 运行 `uv run yaml2pptx.py convert` 验证转换功能
|
||||
- [x] 5.3 运行 `uv run yaml2pptx.py preview` 验证预览功能
|
||||
|
||||
## 6. 更新 README_DEV.md(可选)
|
||||
|
||||
- [x] 6.1 检查 README_DEV.md 中是否有 inline metadata 相关说明需要更新
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-03
|
||||
@@ -0,0 +1,90 @@
|
||||
## Context
|
||||
|
||||
当前系统支持元素级的 `visible` 条件渲染,通过条件表达式(如 `{subtitle != ''}`)动态控制元素是否显示。但在开发和调试过程中,经常需要临时禁用整个幻灯片,当前只能通过注释或删除 YAML 内容来实现,不够便捷。
|
||||
|
||||
现有渲染流程:
|
||||
- `yaml2pptx.py` 主循环遍历所有幻灯片
|
||||
- `Presentation.render_slide()` 渲染单个幻灯片
|
||||
- `Template.render()` 处理模板变量和元素级 `visible` 检查
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 添加页面级 `enabled` 布尔参数,默认为 `true`
|
||||
- 在渲染循环中检查 `enabled`,跳过禁用的幻灯片
|
||||
- 保持与元素级 `visible` 的独立性和兼容性
|
||||
- 向后兼容现有 YAML 文件
|
||||
|
||||
**Non-Goals:**
|
||||
- 不支持 `enabled` 的条件表达式(仅支持布尔值)
|
||||
- 不修改元素级 `visible` 的实现
|
||||
- 不影响预览模式的显示逻辑(本次不涉及)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1: 使用 `enabled` 而非 `visible`
|
||||
|
||||
**选择**: 页面级使用 `enabled` 参数
|
||||
**理由**:
|
||||
- 语义清晰:`enabled` 表示静态开关,`visible` 表示动态条件
|
||||
- 避免混淆:与元素级 `visible` 区分开
|
||||
- 符合直觉:enabled=false 表示禁用整页
|
||||
|
||||
**备选方案**: 使用 `visible` 统一命名
|
||||
- 缺点:语义不清,容易与元素级混淆
|
||||
- 缺点:暗示支持条件表达式,但实际只支持布尔值
|
||||
|
||||
### 决策 2: 默认值为 `true`
|
||||
|
||||
**选择**: 未指定 `enabled` 时默认为 `true`
|
||||
**理由**:
|
||||
- 向后兼容:现有 YAML 文件无需修改
|
||||
- 符合预期:默认行为是渲染所有幻灯片
|
||||
|
||||
### 决策 3: 在主循环检查,而非 `render_slide()`
|
||||
|
||||
**选择**: 在 `yaml2pptx.py` 的主循环中检查 `enabled`
|
||||
**理由**:
|
||||
- 性能:跳过禁用页面,避免不必要的模板加载和渲染
|
||||
- 清晰:页面级控制在页面级处理,不侵入渲染逻辑
|
||||
- 日志:可以在跳过时记录日志
|
||||
|
||||
**备选方案**: 在 `Presentation.render_slide()` 中检查
|
||||
- 缺点:已经进入渲染流程,浪费性能
|
||||
- 缺点:返回值处理复杂(需要返回 None 或特殊标记)
|
||||
|
||||
### 决策 4: 渲染计数逻辑
|
||||
|
||||
**选择**: 维护独立的 `slide_index` 计数器,只统计实际渲染的幻灯片
|
||||
**理由**:
|
||||
- 日志准确:进度显示反映实际渲染数量
|
||||
- 用户友好:不会显示"处理 3/5"然后跳过 2 个的困惑情况
|
||||
|
||||
**实现**:
|
||||
```python
|
||||
slide_index = 0
|
||||
for i, slide_data in enumerate(slides_data, 1):
|
||||
if not slide_data.get('enabled', True):
|
||||
continue
|
||||
slide_index += 1
|
||||
log_progress(slide_index, total_slides, f"处理幻灯片")
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**风险 1: 用户误用 `enabled` 作为条件渲染**
|
||||
- 描述:用户可能期望 `enabled: "{some_var}"` 支持条件表达式
|
||||
- 缓解:文档明确说明 `enabled` 仅支持布尔值,条件渲染使用元素级 `visible`
|
||||
|
||||
**风险 2: 预览模式下禁用页面的显示**
|
||||
- 描述:预览模式可能需要显示禁用的页面(灰色或标记)
|
||||
- 缓解:本次不涉及预览模式,后续可扩展
|
||||
|
||||
**权衡 1: 简单性 vs 灵活性**
|
||||
- 选择:简单的布尔值而非条件表达式
|
||||
- 理由:满足 90% 的使用场景(调试、版本控制),保持简单
|
||||
|
||||
**权衡 2: 日志详细度**
|
||||
- 当前:跳过禁用页面时不记录日志
|
||||
- 备选:记录"跳过第 X 页(已禁用)"
|
||||
- 选择:不记录,避免日志噪音
|
||||
@@ -0,0 +1,27 @@
|
||||
## Why
|
||||
|
||||
在开发和调试演示文稿时,经常需要临时禁用某些幻灯片而不删除它们。当前系统只有元素级的 `visible` 条件渲染,无法快速禁用整个页面。添加页面级的 `enabled` 参数可以提供简单的开关控制,方便开发调试和版本管理。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在幻灯片定义中添加可选的 `enabled` 布尔参数(默认为 `true`)
|
||||
- 渲染流程中检查 `enabled` 参数,跳过禁用的幻灯片
|
||||
- 保留现有的元素级 `visible` 条件渲染功能
|
||||
- 添加 YAML 验证支持 `enabled` 字段
|
||||
- 更新文档说明页面级和元素级控制的区别
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `slide-enabled-control`: 页面级启用/禁用控制,通过 `enabled` 布尔参数控制整个幻灯片是否渲染
|
||||
|
||||
### Modified Capabilities
|
||||
- `template-system`: 扩展幻灯片定义,添加 `enabled` 字段支持
|
||||
|
||||
## Impact
|
||||
|
||||
- `yaml2pptx.py`: 主渲染循环需要检查 `enabled` 参数
|
||||
- `loaders/yaml_loader.py`: 添加 `enabled` 字段验证
|
||||
- `openspec/specs/template-system/spec.md`: 添加页面级 enabled 需求
|
||||
- 测试文件: 添加 enabled 相关测试用例
|
||||
- 文档: README.md 和 README_DEV.md 需要更新
|
||||
@@ -0,0 +1,73 @@
|
||||
# Slide Enabled Control
|
||||
|
||||
## Purpose
|
||||
|
||||
页面级启用/禁用控制,通过 `enabled` 布尔参数控制整个幻灯片是否渲染。提供简单的静态开关,用于开发调试和版本管理。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 幻灯片必须支持 enabled 参数
|
||||
|
||||
幻灯片定义 SHALL 支持可选的 `enabled` 布尔参数,用于控制该幻灯片是否渲染。
|
||||
|
||||
#### Scenario: 禁用的幻灯片不渲染
|
||||
|
||||
- **WHEN** 幻灯片定义了 `enabled: false`
|
||||
- **THEN** 系统跳过该幻灯片,不渲染到演示文稿中
|
||||
|
||||
#### Scenario: 启用的幻灯片正常渲染
|
||||
|
||||
- **WHEN** 幻灯片定义了 `enabled: true`
|
||||
- **THEN** 系统正常渲染该幻灯片
|
||||
|
||||
#### Scenario: 默认启用幻灯片
|
||||
|
||||
- **WHEN** 幻灯片未定义 `enabled` 字段
|
||||
- **THEN** 系统默认该幻灯片为启用状态,正常渲染
|
||||
|
||||
### Requirement: enabled 参数必须是布尔值
|
||||
|
||||
系统 SHALL 验证 `enabled` 参数的类型为布尔值,不支持条件表达式。
|
||||
|
||||
#### Scenario: 接受布尔值
|
||||
|
||||
- **WHEN** 幻灯片定义了 `enabled: false` 或 `enabled: true`
|
||||
- **THEN** 系统正常处理
|
||||
|
||||
#### Scenario: 拒绝非布尔值
|
||||
|
||||
- **WHEN** 幻灯片定义了 `enabled: "false"` 或 `enabled: 0` 等非布尔值
|
||||
- **THEN** 系统抛出验证错误,提示 enabled 必须是布尔值
|
||||
|
||||
#### Scenario: 拒绝条件表达式
|
||||
|
||||
- **WHEN** 幻灯片定义了 `enabled: "{some_var}"`
|
||||
- **THEN** 系统抛出验证错误,提示 enabled 不支持条件表达式
|
||||
|
||||
### Requirement: enabled 与元素级 visible 独立工作
|
||||
|
||||
系统 SHALL 支持页面级 `enabled` 和元素级 `visible` 同时使用,两者独立判断。
|
||||
|
||||
#### Scenario: enabled 和 visible 共存
|
||||
|
||||
- **WHEN** 幻灯片定义了 `enabled: true`,且模板元素包含 `visible` 条件
|
||||
- **THEN** 系统先检查页面级 enabled,再检查元素级 visible
|
||||
|
||||
#### Scenario: enabled=false 时不检查 visible
|
||||
|
||||
- **WHEN** 幻灯片定义了 `enabled: false`
|
||||
- **THEN** 系统跳过该幻灯片,不执行模板渲染和元素级 visible 检查
|
||||
|
||||
### Requirement: 渲染统计必须准确
|
||||
|
||||
系统 SHALL 只统计实际渲染的幻灯片数量,不包括禁用的幻灯片。
|
||||
|
||||
#### Scenario: 进度显示准确
|
||||
|
||||
- **WHEN** 演示文稿包含 5 个幻灯片,其中 2 个 enabled=false
|
||||
- **THEN** 系统显示"处理幻灯片 1/3"、"处理幻灯片 2/3"、"处理幻灯片 3/3"
|
||||
|
||||
#### Scenario: 最终统计准确
|
||||
|
||||
- **WHEN** 演示文稿包含 5 个幻灯片,其中 2 个 enabled=false
|
||||
- **THEN** 生成的 PPTX 文件包含 3 个幻灯片
|
||||
@@ -0,0 +1,27 @@
|
||||
# Template System (Delta)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 幻灯片定义必须支持 enabled 字段
|
||||
|
||||
幻灯片定义 SHALL 支持可选的 `enabled` 布尔字段,用于控制该幻灯片是否渲染。该字段与模板系统的其他字段(template、vars、elements、background)独立工作。
|
||||
|
||||
#### Scenario: 幻灯片包含 enabled 字段
|
||||
|
||||
- **WHEN** 幻灯片定义包含 `enabled: false` 或 `enabled: true`
|
||||
- **THEN** 系统正常加载幻灯片定义,并在渲染时检查该字段
|
||||
|
||||
#### Scenario: enabled 字段可选
|
||||
|
||||
- **WHEN** 幻灯片定义未包含 `enabled` 字段
|
||||
- **THEN** 系统默认该幻灯片为启用状态
|
||||
|
||||
#### Scenario: enabled 与 template 共存
|
||||
|
||||
- **WHEN** 幻灯片同时定义了 `enabled: false` 和 `template: title-slide`
|
||||
- **THEN** 系统跳过该幻灯片,不加载模板
|
||||
|
||||
#### Scenario: enabled 与自定义幻灯片共存
|
||||
|
||||
- **WHEN** 自定义幻灯片(不使用模板)定义了 `enabled: false`
|
||||
- **THEN** 系统跳过该幻灯片,不渲染元素列表
|
||||
@@ -0,0 +1,33 @@
|
||||
## 1. YAML 验证支持
|
||||
|
||||
- [x] 1.1 在 loaders/yaml_loader.py 中添加 enabled 字段验证(布尔类型,可选)
|
||||
- [x] 1.2 添加 enabled 字段类型检查,拒绝非布尔值
|
||||
- [x] 1.3 添加测试用例:test_slide_enabled_validation
|
||||
|
||||
## 2. 渲染逻辑修改
|
||||
|
||||
- [x] 2.1 修改 yaml2pptx.py 主循环,添加 enabled 检查
|
||||
- [x] 2.2 实现独立的 slide_index 计数器,只统计实际渲染的幻灯片
|
||||
- [x] 2.3 确保 log_progress 显示准确的渲染进度
|
||||
|
||||
## 3. 测试用例
|
||||
|
||||
- [x] 3.1 添加测试:test_slide_enabled_false(禁用幻灯片被跳过)
|
||||
- [x] 3.2 添加测试:test_slide_enabled_default_true(默认启用)
|
||||
- [x] 3.3 添加测试:test_slide_enabled_with_template(enabled 与模板共存)
|
||||
- [x] 3.4 添加测试:test_slide_enabled_with_custom_slide(enabled 与自定义幻灯片共存)
|
||||
- [x] 3.5 添加测试:test_slide_enabled_with_element_visible(enabled 和 visible 共存)
|
||||
- [x] 3.6 添加测试:test_slide_enabled_count(渲染统计准确)
|
||||
- [x] 3.7 添加测试:test_slide_enabled_invalid_type(拒绝非布尔值)
|
||||
|
||||
## 4. 文档更新
|
||||
|
||||
- [x] 4.1 更新 README.md,添加 enabled 参数说明和使用示例
|
||||
- [x] 4.2 更新 README_DEV.md,说明 enabled 的实现细节
|
||||
- [x] 4.3 在文档中明确 enabled(静态开关)和 visible(动态条件)的区别
|
||||
|
||||
## 5. 集成测试
|
||||
|
||||
- [x] 5.1 创建测试 YAML 文件,包含启用和禁用的幻灯片
|
||||
- [x] 5.2 运行完整转换流程,验证生成的 PPTX 文件正确
|
||||
- [x] 5.3 验证日志输出准确反映实际渲染的幻灯片数量
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-02
|
||||
@@ -0,0 +1,46 @@
|
||||
## Context
|
||||
|
||||
当前验证器 (`validators/validator.py`) 已具备以下验证能力:
|
||||
- YAML 结构验证(slides 字段)
|
||||
- 元素类型和属性验证
|
||||
- 几何验证(元素位置和尺寸)
|
||||
- 资源验证(图片文件、模板文件存在性)
|
||||
|
||||
但缺少对模板变量完整性的验证。当 YAML 使用模板时,如果用户没有提供模板所需的必需变量(如 `title`),验证器仍会返回成功,直到转换阶段才发现问题。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 在验证阶段检查用户是否提供了模板所需的必需变量
|
||||
- 当缺少必需变量时返回 ERROR 级别错误,阻止转换
|
||||
- 提供清晰的错误信息,指出缺少哪个必需变量
|
||||
|
||||
**Non-Goals:**
|
||||
- 不验证模板变量值的类型正确性(由渲染阶段处理)
|
||||
- 不验证模板变量值的业务逻辑有效性
|
||||
- 不修改现有的验证错误格式
|
||||
|
||||
## Decisions
|
||||
|
||||
### 方案:在 ResourceValidator 中添加 validate_template_vars 方法
|
||||
|
||||
**选择理由:**
|
||||
1. ResourceValidator 已负责模板相关的验证(validate_template),职责匹配
|
||||
2. 可以复用现有的模板加载逻辑
|
||||
3. 对主验证器的影响最小,只需在现有验证流程中调用新方法
|
||||
|
||||
**替代方案考虑:**
|
||||
- 在主验证器中直接实现:会导致主验证器代码膨胀
|
||||
- 新增专门的 TemplateVarValidator:增加复杂度,与现有架构不符
|
||||
|
||||
### 实现要点:
|
||||
1. 在 ResourceValidator 中添加 `validate_template_vars` 方法
|
||||
2. 加载模板文件后,检查模板的 `vars` 字段中的 `required: true` 变量
|
||||
3. 从幻灯片数据中获取 `vars` 字段,与模板要求的必需变量对比
|
||||
4. 缺少必需变量时,添加 ERROR 级别问题
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**潜在风险:**
|
||||
- [风险] 重复加载模板文件 → [缓解] ResourceValidator 已在 validate_template 中加载一次,可复用加载结果或缓存
|
||||
- [风险] vars 字段嵌套层级复杂 → [缓解] 仅检查顶层 vars 字段,不处理嵌套引用
|
||||
@@ -0,0 +1,25 @@
|
||||
## Why
|
||||
|
||||
当前验证器(`yaml2pptx.py check` 命令)只验证 YAML 语法和元素有效性,但不验证模板变量的完整性。用户在使用模板时如果缺少必需变量(如 title),验证器仍然返回成功,导致用户在转换阶段才发现问题。需要在验证阶段提前发现这类问题,提升用户体验。
|
||||
|
||||
## What Changes
|
||||
|
||||
在 `validators/validator.py` 的验证流程中添加模板变量验证功能:
|
||||
1. 检测 YAML 是否使用模板(检查 `slides[].template` 字段)
|
||||
2. 加载模板定义(读取模板 YAML 文件)
|
||||
3. 检查模板中的必需变量是否在 `vars` 中提供
|
||||
4. 如缺少必需变量,添加验证错误
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `template-variable-validation`: 验证器在检查阶段验证模板必需变量是否提供
|
||||
|
||||
### Modified Capabilities
|
||||
- `yaml-validation`: 需要扩展验证范围,加入模板变量完整性检查(新增需求,不是修改现有需求)
|
||||
|
||||
## Impact
|
||||
|
||||
- 主要影响:`validators/validator.py` 的验证逻辑
|
||||
- 次要影响:可能需要调整验证错误信息的格式
|
||||
- 无 API 变更,仅内部验证逻辑增强
|
||||
@@ -0,0 +1,53 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 验证器必须检查模板必需变量
|
||||
|
||||
当 YAML 使用模板时,系统 SHALL 验证用户是否提供了模板所需的必需变量。
|
||||
|
||||
#### Scenario: 提供所有必需变量
|
||||
|
||||
- **WHEN** 模板定义了 `vars: [{name: title, required: true}]`,且用户 YAML 提供了 `vars: {title: "Hello"}`
|
||||
- **THEN** 验证通过,不报错
|
||||
|
||||
#### Scenario: 缺少必需变量
|
||||
|
||||
- **WHEN** 模板定义了 `vars: [{name: title, required: true}]`,但用户 YAML 的 `vars` 中没有提供 `title`
|
||||
- **THEN** 验证器报告 ERROR 级别错误:"缺少模板必需变量: title"
|
||||
|
||||
#### Scenario: 多个必需变量部分缺失
|
||||
|
||||
- **WHEN** 模板定义了 `vars: [{name: title, required: true}, {name: subtitle, required: true}]`,但用户只提供了 `vars: {title: "Hello"}`
|
||||
- **THEN** 验证器报告 ERROR 级别错误,包含所有缺少的必需变量
|
||||
|
||||
#### Scenario: 可选变量缺失
|
||||
|
||||
- **WHEN** 模板定义了 `vars: [{name: subtitle, required: false}]`,用户没有提供该变量
|
||||
- **THEN** 验证通过,不报错
|
||||
|
||||
#### Scenario: 提供默认值时缺少可选变量
|
||||
|
||||
- **WHEN** 模板定义了 `vars: [{name: subtitle, required: false, default: ""}]`,用户没有提供该变量
|
||||
- **THEN** 验证通过,不报错(使用默认值)
|
||||
|
||||
### Requirement: 验证器必须支持多幻灯片模板变量检查
|
||||
|
||||
系统 SHALL 检查每个使用模板的幻灯片,确保其提供了模板所需的必需变量。
|
||||
|
||||
#### Scenario: 不同幻灯片使用不同模板
|
||||
|
||||
- **WHEN** 幻灯片 1 使用模板 A(需要变量 title),幻灯片 2 使用模板 B(需要变量 image)
|
||||
- **THEN** 验证器分别检查每个幻灯片的变量,提供独立的错误信息
|
||||
|
||||
#### Scenario: 多个幻灯片使用同一模板
|
||||
|
||||
- **WHEN** 幻灯片 1 和幻灯片 2 都使用同一模板,都缺少必需变量
|
||||
- **THEN** 验证器报告两个错误,分别对应各自的幻灯片位置
|
||||
|
||||
### Requirement: 验证器必须提供清晰的错误位置信息
|
||||
|
||||
当缺少必需变量时,验证器 SHALL 在错误信息中包含幻灯片位置。
|
||||
|
||||
#### Scenario: 错误信息包含幻灯片位置
|
||||
|
||||
- **WHEN** 幻灯片 2 使用模板但缺少必需变量
|
||||
- **THEN** 错误信息包含位置:"幻灯片 2: 缺少模板必需变量: title"
|
||||
@@ -0,0 +1,19 @@
|
||||
## 1. 扩展 ResourceValidator
|
||||
|
||||
- [x] 1.1 在 ResourceValidator 中添加 `validate_template_vars` 方法
|
||||
- [x] 1.2 实现加载模板 vars 定义逻辑
|
||||
- [x] 1.3 实现检查用户提供的 vars 是否满足模板必需变量逻辑
|
||||
- [x] 1.4 返回缺少必需变量的验证错误
|
||||
|
||||
## 2. 集成到主验证器
|
||||
|
||||
- [x] 2.1 在 Validator.validate() 中调用 validate_template_vars 方法
|
||||
- [x] 2.2 确保在模板文件验证通过后再进行变量验证
|
||||
|
||||
## 3. 测试
|
||||
|
||||
- [x] 3.1 编写单元测试:提供所有必需变量时验证通过
|
||||
- [x] 3.2 编写单元测试:缺少必需变量时验证失败并返回错误
|
||||
- [x] 3.3 编写单元测试:多个必需变量部分缺失时报告所有缺失变量
|
||||
- [x] 3.4 编写单元测试:可选变量缺失时验证通过
|
||||
- [x] 3.5 编写集成测试:运行 yaml2pptx.py check 命令验证功能
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-03
|
||||
@@ -0,0 +1,263 @@
|
||||
## Context
|
||||
|
||||
当前的模板系统支持条件渲染功能,但实现非常简单,仅通过正则表达式匹配 `{var != ''}` 格式来判断变量是否非空。这种实现无法满足实际使用中的复杂需求,如多条件组合、数值比较、成员测试等。
|
||||
|
||||
现有实现位于 `core/template.py` 的 `evaluate_condition` 方法,使用简单的正则匹配:
|
||||
```python
|
||||
pattern = r'\{(\w+)\s*!=\s*[\'\"]{2}\}'
|
||||
match = re.match(pattern, condition)
|
||||
```
|
||||
|
||||
用户反馈需要更强大的条件表达式能力,但直接使用 Python 的 `eval()` 存在严重的安全风险。我们需要一个既强大又安全的表达式评估方案。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 提供强大的条件表达式能力,支持比较、逻辑、成员测试、数学运算
|
||||
- 确保表达式评估的安全性,防止代码注入和恶意操作
|
||||
- 提供清晰的错误信息,帮助用户快速定位问题
|
||||
- 保持 API 简洁,对现有代码的侵入性最小
|
||||
- 实现高性能的表达式评估,不影响模板渲染速度
|
||||
|
||||
**Non-Goals:**
|
||||
- 不支持函数定义、类定义等复杂 Python 特性
|
||||
- 不支持模块导入和文件操作
|
||||
- 不支持对象属性访问(避免安全风险)
|
||||
- 不考虑旧版本语法的向后兼容(用户明确要求)
|
||||
- 不实现自定义的表达式解析器(使用成熟的第三方库)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1: 使用 simpleeval 作为表达式评估引擎
|
||||
|
||||
**选择**: simpleeval (EvalWithCompoundTypes)
|
||||
|
||||
**理由**:
|
||||
- 成熟稳定:483 stars,维护活跃,社区认可度高
|
||||
- 安全性好:基于 AST 解析,不使用 `eval()`,有明确的安全边界
|
||||
- 功能适中:支持我们需要的所有操作符和表达式类型
|
||||
- 轻量级:单文件库,无额外依赖,不增加项目复杂度
|
||||
- 易于集成:API 简单直观,集成成本低
|
||||
|
||||
**备选方案**:
|
||||
1. **evalidate**: 性能更好(快 3-5 倍),但社区较小,文档较少
|
||||
2. **asteval**: 功能更强大,但过于复杂,性能较差,不适合简单条件判断
|
||||
3. **自实现**: 完全可控,但开发成本高,需要大量测试,容易出现安全漏洞
|
||||
|
||||
**权衡**: simpleeval 在功能、安全性、易用性之间达到了最佳平衡。
|
||||
|
||||
### 决策 2: 使用 EvalWithCompoundTypes 而非 SimpleEval
|
||||
|
||||
**选择**: EvalWithCompoundTypes
|
||||
|
||||
**理由**:
|
||||
- 支持列表和元组字面量,允许 `status in ['draft', 'review']` 这样的表达式
|
||||
- 对于条件判断场景,列表/元组是常见需求
|
||||
- 安全性与 SimpleEval 相同,只是增加了复合类型支持
|
||||
|
||||
**API 差异**:
|
||||
```python
|
||||
# SimpleEval
|
||||
simple_eval(expr, names=vars_values)
|
||||
|
||||
# EvalWithCompoundTypes
|
||||
evaluator = EvalWithCompoundTypes(names=vars_values)
|
||||
evaluator.eval(expr)
|
||||
```
|
||||
|
||||
### 决策 3: 每次评估创建新的 evaluator 实例
|
||||
|
||||
**选择**: 每次调用 `evaluate_condition` 时创建新的 EvalWithCompoundTypes 实例
|
||||
|
||||
**理由**:
|
||||
- 避免状态污染:不同模板渲染之间完全隔离
|
||||
- 线程安全:每个评估独立,无共享状态
|
||||
- 简化实现:不需要管理 evaluator 的生命周期
|
||||
|
||||
**性能考虑**:
|
||||
- EvalWithCompoundTypes 实例化成本很低
|
||||
- 表达式评估本身的开销远大于实例化开销
|
||||
- 对于典型的模板渲染场景(几十个元素),性能影响可忽略
|
||||
|
||||
### 决策 4: 扩展白名单函数
|
||||
|
||||
**选择**: 在 simpleeval 默认函数基础上,添加常用的安全函数
|
||||
|
||||
**白名单函数**:
|
||||
- 类型转换:`int()`, `float()`, `str()`, `bool()`
|
||||
- 数学函数:`abs()`, `min()`, `max()`
|
||||
- 容器函数:`len()`
|
||||
|
||||
**理由**:
|
||||
- 这些函数在条件判断中常用
|
||||
- 都是纯函数,无副作用,安全性高
|
||||
- simpleeval 默认只提供 `int`, `float`, `str`,需要补充
|
||||
|
||||
**不添加的函数**:
|
||||
- 文件操作:`open()`, `read()`, `write()`
|
||||
- 系统操作:`exec()`, `eval()`, `compile()`
|
||||
- 反射操作:`getattr()`, `setattr()`, `hasattr()`
|
||||
|
||||
### 决策 5: 实现独立的 ConditionEvaluator 类
|
||||
|
||||
**选择**: 创建独立的 `ConditionEvaluator` 类,而不是直接在 Template 类中实现
|
||||
|
||||
**架构**:
|
||||
```
|
||||
Template
|
||||
└─ ConditionEvaluator
|
||||
└─ EvalWithCompoundTypes (simpleeval)
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 单一职责:Template 负责模板渲染,ConditionEvaluator 负责条件评估
|
||||
- 易于测试:可以独立测试条件评估逻辑
|
||||
- 易于扩展:未来可以轻松替换评估引擎或添加新功能
|
||||
- 代码清晰:避免 Template 类过于臃肿
|
||||
|
||||
### 决策 6: 错误处理策略
|
||||
|
||||
**选择**: 捕获 simpleeval 的所有异常,转换为用户友好的 YAMLError
|
||||
|
||||
**错误映射**:
|
||||
```python
|
||||
NameNotDefined → "条件表达式中的变量未定义: {var_name}"
|
||||
FunctionNotDefined → "条件表达式中使用了不支持的函数: {func_name}"
|
||||
FeatureNotAvailable → "条件表达式使用了不支持的语法特性"
|
||||
SyntaxError → "条件表达式语法错误"
|
||||
```
|
||||
|
||||
**错误信息包含**:
|
||||
- 原始表达式
|
||||
- 具体的错误原因
|
||||
- 可用的变量列表(对于 NameNotDefined)
|
||||
- 支持的函数列表(对于 FunctionNotDefined)
|
||||
- 修复建议
|
||||
|
||||
**理由**:
|
||||
- 用户不需要了解 simpleeval 的内部实现
|
||||
- 错误信息更具体,更容易调试
|
||||
- 保持与现有错误处理风格一致
|
||||
|
||||
### 决策 7: 表达式安全限制
|
||||
|
||||
**选择**: 实施多层安全限制
|
||||
|
||||
**限制措施**:
|
||||
1. **长度限制**: 表达式最大 500 字符
|
||||
2. **白名单函数**: 仅允许预定义的安全函数
|
||||
3. **禁止特性**:
|
||||
- 属性访问(`obj.attr`)
|
||||
- 函数定义(`lambda`, `def`)
|
||||
- 模块导入(`import`)
|
||||
- 赋值操作(`=`, `+=`)
|
||||
|
||||
**理由**:
|
||||
- 长度限制防止过于复杂的表达式,也防止 DOS 攻击
|
||||
- simpleeval 默认已禁止大部分危险操作
|
||||
- 额外的白名单限制提供双重保护
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险 1: simpleeval 的安全漏洞
|
||||
|
||||
**风险**: simpleeval 可能存在未知的安全漏洞,允许恶意代码执行
|
||||
|
||||
**缓解措施**:
|
||||
- simpleeval 是成熟的开源项目,经过广泛使用和审查
|
||||
- 我们添加了额外的长度限制和白名单限制
|
||||
- 表达式来源是用户自己的 YAML 文件,不是外部不可信输入
|
||||
- 定期更新 simpleeval 到最新版本
|
||||
|
||||
**残留风险**: 低。对于本项目的使用场景(用户编写自己的模板),风险可接受。
|
||||
|
||||
### 风险 2: 性能影响
|
||||
|
||||
**风险**: simpleeval 的表达式评估可能比简单的正则匹配慢,影响模板渲染性能
|
||||
|
||||
**缓解措施**:
|
||||
- 实际测试表明,simpleeval 的性能足够好(每秒可评估数万次)
|
||||
- 对于典型的演示文稿(几十个幻灯片,每个几十个元素),性能影响可忽略
|
||||
- 如果未来性能成为瓶颈,可以考虑:
|
||||
- 缓存编译后的表达式(simpleeval 支持)
|
||||
- 切换到 evalidate(性能更好)
|
||||
|
||||
**残留风险**: 极低。当前性能完全满足需求。
|
||||
|
||||
### 风险 3: 用户学习成本
|
||||
|
||||
**风险**: 用户需要学习新的表达式语法,可能不熟悉 Python 表达式
|
||||
|
||||
**缓解措施**:
|
||||
- Python 表达式语法简单直观,学习成本低
|
||||
- 提供详细的文档和示例
|
||||
- 错误信息清晰,帮助用户快速定位问题
|
||||
- 旧的简单语法(`{var != ''}`)在新实现中仍然有效
|
||||
|
||||
**残留风险**: 低。Python 表达式是业界标准,大多数开发者都熟悉。
|
||||
|
||||
### 权衡 1: 功能 vs 安全性
|
||||
|
||||
**权衡**: 为了安全性,我们禁止了一些 Python 特性(如属性访问、函数定义)
|
||||
|
||||
**影响**: 用户无法使用这些高级特性,但对于条件判断场景,当前支持的特性已经足够
|
||||
|
||||
**决策**: 安全性优先。如果未来有明确的需求,可以考虑有限地开放某些特性。
|
||||
|
||||
### 权衡 2: 向后兼容 vs 代码简洁
|
||||
|
||||
**权衡**: 用户明确要求不考虑向后兼容,我们可以直接移除旧的正则匹配实现
|
||||
|
||||
**影响**:
|
||||
- 代码更简洁,维护成本更低
|
||||
- 旧的简单语法(`{var != ''}`)在新实现中仍然有效,实际兼容性影响很小
|
||||
|
||||
**决策**: 移除旧实现,统一使用 simpleeval。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 实施步骤
|
||||
|
||||
1. **安装依赖**
|
||||
```bash
|
||||
uv add simpleeval
|
||||
```
|
||||
|
||||
2. **实现 ConditionEvaluator 类**
|
||||
- 创建 `core/condition_evaluator.py`
|
||||
- 实现 `evaluate_condition` 方法
|
||||
- 实现错误处理和安全限制
|
||||
|
||||
3. **集成到 Template 类**
|
||||
- 在 `Template.__init__` 中初始化 ConditionEvaluator
|
||||
- 替换 `evaluate_condition` 方法的实现
|
||||
- 移除旧的正则匹配代码
|
||||
|
||||
4. **更新测试**
|
||||
- 扩展 `tests/unit/test_template.py` 中的条件渲染测试
|
||||
- 添加新的表达式类型测试
|
||||
- 添加错误处理测试
|
||||
- 添加安全限制测试
|
||||
|
||||
5. **更新文档**
|
||||
- 更新 README.md 的条件渲染章节
|
||||
- 添加表达式语法参考
|
||||
- 更新 README_DEV.md 的架构说明
|
||||
|
||||
6. **验证和发布**
|
||||
- 运行完整测试套件
|
||||
- 手动测试各种表达式场景
|
||||
- 更新版本号
|
||||
|
||||
### 回滚策略
|
||||
|
||||
如果发现严重问题,可以快速回滚:
|
||||
1. 恢复 `core/template.py` 中的旧 `evaluate_condition` 实现
|
||||
2. 移除 simpleeval 依赖
|
||||
3. 恢复旧的测试用例
|
||||
|
||||
**回滚成本**: 低。改动集中在单个文件,易于回滚。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。所有关键决策已明确。
|
||||
@@ -0,0 +1,48 @@
|
||||
## Why
|
||||
|
||||
当前的 visible 条件渲染功能仅支持简单的非空检查(`{var != ''}`),无法满足实际使用中的复杂条件判断需求。用户需要根据多个变量、逻辑组合、数值比较等条件来控制元素显示,但现有实现过于受限。通过引入成熟的表达式评估库 simpleeval,可以提供强大且安全的条件表达式能力,显著提升模板系统的灵活性。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 引入 simpleeval 库作为条件表达式评估引擎
|
||||
- 扩展 visible 字段支持的表达式类型:
|
||||
- 比较运算符:`==`, `!=`, `>`, `<`, `>=`, `<=`
|
||||
- 逻辑运算符:`and`, `or`, `not`
|
||||
- 成员测试:`in`, `not in`
|
||||
- 列表/元组字面量:`status in ['draft', 'review']`
|
||||
- 数学运算:`+`, `-`, `*`, `/`, `%`, `**`
|
||||
- 内置函数:`int()`, `float()`, `str()`, `len()`, `bool()`
|
||||
- 增强错误处理,提供详细的错误信息和调试提示
|
||||
- 添加表达式安全限制(最大长度、禁止危险操作)
|
||||
- **BREAKING**: 移除旧版本的简单正则匹配实现,统一使用 simpleeval
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `enhanced-condition-evaluation`: 基于 simpleeval 的增强条件表达式评估能力,支持复杂的逻辑判断、数学运算和成员测试
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `template-system`: 修改模板系统的条件渲染实现,从简单正则匹配升级为完整的表达式评估
|
||||
|
||||
## Impact
|
||||
|
||||
- **代码影响**:
|
||||
- `core/template.py`: 重写 `evaluate_condition` 方法
|
||||
- `loaders/yaml_loader.py`: 可能需要更新验证逻辑
|
||||
- 测试文件:需要大幅扩展测试用例覆盖新的表达式类型
|
||||
|
||||
- **依赖影响**:
|
||||
- 新增依赖:simpleeval (轻量级,无额外依赖)
|
||||
- 需要更新 `pyproject.toml`
|
||||
|
||||
- **用户影响**:
|
||||
- **BREAKING**: 旧的简单实现被移除,但由于旧语法 `{var != ''}` 在新实现中仍然有效,实际兼容性影响较小
|
||||
- 用户需要学习新的表达式语法(但与 Python 表达式一致,学习成本低)
|
||||
- 错误信息更详细,调试更容易
|
||||
|
||||
- **文档影响**:
|
||||
- README.md: 需要更新条件渲染章节,添加新的表达式示例
|
||||
- README_DEV.md: 需要说明 simpleeval 集成和安全策略
|
||||
- 需要添加完整的表达式语法参考文档
|
||||
@@ -0,0 +1,179 @@
|
||||
# Enhanced Condition Evaluation
|
||||
|
||||
## Purpose
|
||||
|
||||
增强条件评估能力提供基于 simpleeval 的安全表达式评估引擎,支持复杂的条件判断、逻辑运算、数学计算和成员测试。该能力用于模板系统的 visible 字段,允许用户使用 Python 风格的表达式来控制元素的显示条件。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 系统必须支持比较运算符
|
||||
|
||||
系统 SHALL 支持所有标准的比较运算符,用于数值和字符串的比较。
|
||||
|
||||
#### Scenario: 数值大于比较
|
||||
|
||||
- **WHEN** 表达式为 `{count > 0}` 且变量 `count` 的值为 5
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 数值小于等于比较
|
||||
|
||||
- **WHEN** 表达式为 `{price <= 100}` 且变量 `price` 的值为 100
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 字符串相等比较
|
||||
|
||||
- **WHEN** 表达式为 `{status == 'active'}` 且变量 `status` 的值为 "active"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 字符串不等比较
|
||||
|
||||
- **WHEN** 表达式为 `{status != 'inactive'}` 且变量 `status` 的值为 "active"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
### Requirement: 系统必须支持逻辑运算符
|
||||
|
||||
系统 SHALL 支持逻辑运算符 and、or、not,用于组合多个条件。
|
||||
|
||||
#### Scenario: 逻辑与运算
|
||||
|
||||
- **WHEN** 表达式为 `{count > 0 and status == 'active'}` 且变量 `count` 为 5,`status` 为 "active"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 逻辑或运算
|
||||
|
||||
- **WHEN** 表达式为 `{count > 0 or status == 'active'}` 且变量 `count` 为 0,`status` 为 "active"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 逻辑非运算
|
||||
|
||||
- **WHEN** 表达式为 `{not (count == 0)}` 且变量 `count` 为 5
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 复杂逻辑组合
|
||||
|
||||
- **WHEN** 表达式为 `{(score >= 60 and score <= 100) or is_admin}` 且变量 `score` 为 75,`is_admin` 为 False
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
### Requirement: 系统必须支持成员测试
|
||||
|
||||
系统 SHALL 支持 in 和 not in 运算符,用于测试值是否在集合中。
|
||||
|
||||
#### Scenario: 列表成员测试
|
||||
|
||||
- **WHEN** 表达式为 `{status in ['draft', 'review', 'published']}` 且变量 `status` 为 "draft"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 列表非成员测试
|
||||
|
||||
- **WHEN** 表达式为 `{status not in ['draft', 'review']}` 且变量 `status` 为 "active"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 字符串包含测试
|
||||
|
||||
- **WHEN** 表达式为 `{'test' in status}` 且变量 `status` 为 "test-version"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 元组成员测试
|
||||
|
||||
- **WHEN** 表达式为 `{level in (1, 2, 3)}` 且变量 `level` 为 2
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
### Requirement: 系统必须支持数学运算
|
||||
|
||||
系统 SHALL 支持基本的数学运算符,用于条件判断中的计算。
|
||||
|
||||
#### Scenario: 加法运算
|
||||
|
||||
- **WHEN** 表达式为 `{(price + tax) > 100}` 且变量 `price` 为 90,`tax` 为 15
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 乘法运算
|
||||
|
||||
- **WHEN** 表达式为 `{(price * discount) > 50}` 且变量 `price` 为 100,`discount` 为 0.8
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 除法运算
|
||||
|
||||
- **WHEN** 表达式为 `{(total / count) >= 10}` 且变量 `total` 为 100,`count` 为 5
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
### Requirement: 系统必须支持内置函数
|
||||
|
||||
系统 SHALL 支持安全的内置函数,用于类型转换和基本操作。
|
||||
|
||||
#### Scenario: 类型转换函数
|
||||
|
||||
- **WHEN** 表达式为 `{int(value) > 100}` 且变量 `value` 为 "150"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 长度函数
|
||||
|
||||
- **WHEN** 表达式为 `{len(items) > 0}` 且变量 `items` 为 [1, 2, 3]
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
#### Scenario: 布尔转换函数
|
||||
|
||||
- **WHEN** 表达式为 `{bool(value)}` 且变量 `value` 为 "text"
|
||||
- **THEN** 表达式评估结果为 True
|
||||
|
||||
### Requirement: 系统必须提供详细的错误信息
|
||||
|
||||
系统 SHALL 在表达式评估失败时提供清晰的错误信息,帮助用户快速定位问题。
|
||||
|
||||
#### Scenario: 变量未定义错误
|
||||
|
||||
- **WHEN** 表达式为 `{undefined_var > 0}` 但变量 `undefined_var` 未提供
|
||||
- **THEN** 系统抛出错误,提示"条件表达式中的变量未定义: undefined_var",并列出可用变量
|
||||
|
||||
#### Scenario: 函数未定义错误
|
||||
|
||||
- **WHEN** 表达式为 `{custom_func(value)}` 但函数 `custom_func` 不在白名单中
|
||||
- **THEN** 系统抛出错误,提示"条件表达式中使用了不支持的函数: custom_func",并列出支持的函数
|
||||
|
||||
#### Scenario: 语法错误
|
||||
|
||||
- **WHEN** 表达式为 `{count > }` 包含语法错误
|
||||
- **THEN** 系统抛出错误,提示"条件表达式语法错误",并显示具体的语法问题
|
||||
|
||||
### Requirement: 系统必须实施安全限制
|
||||
|
||||
系统 SHALL 限制表达式的复杂度和能力,防止恶意或危险的操作。
|
||||
|
||||
#### Scenario: 表达式长度限制
|
||||
|
||||
- **WHEN** 表达式长度超过 500 字符
|
||||
- **THEN** 系统抛出错误,提示"条件表达式过长"
|
||||
|
||||
#### Scenario: 禁止属性访问
|
||||
|
||||
- **WHEN** 表达式为 `{obj.attr}` 尝试访问对象属性
|
||||
- **THEN** 系统抛出错误,提示"不支持的语法特性"
|
||||
|
||||
#### Scenario: 禁止函数定义
|
||||
|
||||
- **WHEN** 表达式包含 lambda 或 def 关键字
|
||||
- **THEN** 系统抛出错误,提示"不支持的语法特性"
|
||||
|
||||
#### Scenario: 白名单函数限制
|
||||
|
||||
- **WHEN** 表达式使用了不在白名单中的函数(如 eval、exec、open)
|
||||
- **THEN** 系统拒绝执行,抛出错误
|
||||
|
||||
### Requirement: 系统必须支持表达式提取
|
||||
|
||||
系统 SHALL 能够从 `{expression}` 格式的字符串中提取实际的表达式内容。
|
||||
|
||||
#### Scenario: 提取带花括号的表达式
|
||||
|
||||
- **WHEN** 条件字符串为 `{count > 0}`
|
||||
- **THEN** 系统提取出表达式 `count > 0`
|
||||
|
||||
#### Scenario: 提取带空白的表达式
|
||||
|
||||
- **WHEN** 条件字符串为 `{ count > 0 }`
|
||||
- **THEN** 系统提取出表达式 `count > 0`(去除首尾空白)
|
||||
|
||||
#### Scenario: 处理不带花括号的表达式
|
||||
|
||||
- **WHEN** 条件字符串为 `count > 0`(不带花括号)
|
||||
- **THEN** 系统直接使用该字符串作为表达式
|
||||
@@ -0,0 +1,52 @@
|
||||
# Template System (Delta Spec)
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 系统必须支持条件渲染
|
||||
|
||||
系统 SHALL 支持基于变量值的条件渲染,通过 `visible` 字段控制元素是否显示。条件表达式使用 simpleeval 引擎评估,支持复杂的逻辑判断、比较运算、成员测试和数学计算。
|
||||
|
||||
#### Scenario: 显示满足条件的元素
|
||||
|
||||
- **WHEN** 元素定义了 `visible: "{count > 0}"`,且用户提供的 count 大于 0
|
||||
- **THEN** 系统渲染该元素
|
||||
|
||||
#### Scenario: 隐藏不满足条件的元素
|
||||
|
||||
- **WHEN** 元素定义了 `visible: "{count > 0}"`,但用户提供的 count 等于 0
|
||||
- **THEN** 系统跳过该元素,不渲染到幻灯片中
|
||||
|
||||
#### Scenario: 复杂逻辑条件
|
||||
|
||||
- **WHEN** 元素定义了 `visible: "{count > 0 and status == 'active'}"`,且两个条件都满足
|
||||
- **THEN** 系统渲染该元素
|
||||
|
||||
#### Scenario: 成员测试条件
|
||||
|
||||
- **WHEN** 元素定义了 `visible: "{status in ['draft', 'review']}"`,且 status 为 "draft"
|
||||
- **THEN** 系统渲染该元素
|
||||
|
||||
#### Scenario: 数学运算条件
|
||||
|
||||
- **WHEN** 元素定义了 `visible: "{(price * discount) > 50}"`,且计算结果大于 50
|
||||
- **THEN** 系统渲染该元素
|
||||
|
||||
#### Scenario: 条件表达式语法错误
|
||||
|
||||
- **WHEN** visible 字段包含无效的条件表达式(如 `{count > }`)
|
||||
- **THEN** 系统抛出错误,提示"条件表达式语法错误",并显示具体的语法问题
|
||||
|
||||
#### Scenario: 条件表达式中的变量未定义
|
||||
|
||||
- **WHEN** visible 字段引用了未定义的变量(如 `{undefined_var > 0}`)
|
||||
- **THEN** 系统抛出错误,提示"条件表达式中的变量未定义: undefined_var",并列出可用变量
|
||||
|
||||
#### Scenario: 条件表达式使用不支持的函数
|
||||
|
||||
- **WHEN** visible 字段使用了不在白名单中的函数(如 `{eval(code)}`)
|
||||
- **THEN** 系统抛出错误,提示"条件表达式中使用了不支持的函数: eval"
|
||||
|
||||
#### Scenario: 向后兼容的简单表达式
|
||||
|
||||
- **WHEN** 元素定义了 `visible: "{subtitle != ''}"`(旧语法格式)
|
||||
- **THEN** 系统使用新的 simpleeval 引擎正确评估该表达式
|
||||
@@ -0,0 +1,55 @@
|
||||
## 1. 依赖和环境准备
|
||||
|
||||
- [x] 1.1 确认 simpleeval 已安装(已在探索阶段完成)
|
||||
- [x] 1.2 验证 simpleeval 版本和功能可用性
|
||||
|
||||
## 2. 核心实现
|
||||
|
||||
- [x] 2.1 创建 core/condition_evaluator.py 文件
|
||||
- [x] 2.2 实现 ConditionEvaluator 类的基本结构
|
||||
- [x] 2.3 实现 _get_evaluator 方法,配置 EvalWithCompoundTypes
|
||||
- [x] 2.4 实现 _extract_expression 方法,提取表达式内容
|
||||
- [x] 2.5 实现 evaluate_condition 方法,集成 simpleeval
|
||||
- [x] 2.6 实现错误处理,映射 simpleeval 异常到 YAMLError
|
||||
- [x] 2.7 添加表达式长度限制(MAX_EXPRESSION_LENGTH = 500)
|
||||
- [x] 2.8 配置白名单函数(len, bool, abs, min, max)
|
||||
|
||||
## 3. 集成到 Template 类
|
||||
|
||||
- [x] 3.1 在 Template.__init__ 中初始化 ConditionEvaluator
|
||||
- [x] 3.2 修改 Template.evaluate_condition 方法,委托给 ConditionEvaluator
|
||||
- [x] 3.3 移除旧的正则匹配实现代码
|
||||
- [x] 3.4 确保 Template.render 方法正确调用新的 evaluate_condition
|
||||
|
||||
## 4. 测试实现
|
||||
|
||||
- [x] 4.1 创建 tests/unit/test_condition_evaluator.py 文件
|
||||
- [x] 4.2 测试比较运算符(==, !=, >, <, >=, <=)
|
||||
- [x] 4.3 测试逻辑运算符(and, or, not)
|
||||
- [x] 4.4 测试成员测试(in, not in)
|
||||
- [x] 4.5 测试列表和元组字面量
|
||||
- [x] 4.6 测试数学运算(+, -, *, /, %, **)
|
||||
- [x] 4.7 测试内置函数(int, float, str, len, bool, abs, min, max)
|
||||
- [x] 4.8 测试复杂逻辑组合表达式
|
||||
- [x] 4.9 测试错误处理(变量未定义、函数未定义、语法错误)
|
||||
- [x] 4.10 测试安全限制(表达式过长、禁止的特性)
|
||||
- [x] 4.11 更新 tests/unit/test_template.py 中的条件渲染测试
|
||||
- [x] 4.12 添加集成测试,验证完整的模板渲染流程
|
||||
|
||||
## 5. 文档更新
|
||||
|
||||
- [x] 5.1 更新 README.md 的条件渲染章节
|
||||
- [x] 5.2 添加新的表达式语法示例到 README.md
|
||||
- [x] 5.3 添加支持的操作符和函数列表到 README.md
|
||||
- [x] 5.4 更新 README_DEV.md,说明 ConditionEvaluator 架构
|
||||
- [x] 5.5 更新 README_DEV.md,说明 simpleeval 集成和安全策略
|
||||
- [x] 5.6 创建或更新示例文件,展示新的条件表达式能力
|
||||
|
||||
## 6. 验证和清理
|
||||
|
||||
- [x] 6.1 运行完整测试套件,确保所有测试通过
|
||||
- [x] 6.2 手动测试各种表达式场景
|
||||
- [x] 6.3 验证错误信息的清晰度和有用性
|
||||
- [x] 6.4 检查代码风格和注释完整性
|
||||
- [x] 6.5 清理 temp 目录中的临时文件(保留有用的示例)
|
||||
- [x] 6.6 确认没有遗留的调试代码或注释
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-02
|
||||
111
openspec/changes/archive/2026-03-03-fix-failing-tests/design.md
Normal file
111
openspec/changes/archive/2026-03-03-fix-failing-tests/design.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 设计文档:修复失败的测试
|
||||
|
||||
## Context
|
||||
|
||||
当前项目的测试套件包含 316 个测试,其中 7 个失败,1 个错误。主要问题分为四类:
|
||||
|
||||
1. **API 不匹配问题**:测试假设 `render_slide` 返回字典列表,但实际实现返回元素对象列表(dataclass 实例)
|
||||
|
||||
2. **python-pptx 枚举问题**:`MSO_SHAPE.TEXT_BOX` 在当前 python-pptx 版本中不存在,导致元素类型检测失败
|
||||
|
||||
3. **Windows 路径问题**:字符串路径与 Path 对象混合比较导致断言失败
|
||||
|
||||
4. **Fixture 缺失**:`mock_template_class` fixture 未定义
|
||||
|
||||
项目约束:
|
||||
- 必须保持 100% 测试通过率
|
||||
- 修复不能破坏现有功能
|
||||
- 需要保持代码覆盖率 >= 94%
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 修复所有 7 个失败测试和 1 个错误测试
|
||||
- 确保修复后测试仍能有效验证功能正确性
|
||||
- 改进测试辅助函数的兼容性
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改生产代码的功能逻辑
|
||||
- 不添加新功能
|
||||
- 不降低代码覆盖率
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: 处理 API 不匹配(render_slide 返回值)
|
||||
|
||||
**选项 A**: 修改 `render_slide` 方法,返回字典而非对象
|
||||
- 优点:保持测试代码不变
|
||||
- 缺点:需要修改 `pptx_renderer.py` 和 `html_renderer.py` 的调用方式
|
||||
|
||||
**选项 B**: 修改测试代码,使用对象属性访问
|
||||
- 优点:保持生产代码不变,测试更符合实际使用场景
|
||||
- 缺点:需要修改多个测试文件
|
||||
|
||||
**选择:B** - 修改测试代码,因为元素对象提供了更好的类型安全和 IDE 支持
|
||||
|
||||
### D2: 处理 MSO_SHAPE.TEXT_BOX 枚举不存在
|
||||
|
||||
**选项 A**: 使用 `shape.has_text_frame` 属性检测文本框
|
||||
- 优点:跨版本兼容
|
||||
- 缺点:需要改变检测逻辑
|
||||
|
||||
**选项 B**: 检查 `shape.shape_type` 是否等于 `MSO_SHAPE.AUTO_SHAPE_TYPE` (枚举值 1)
|
||||
- 优点:简单直接
|
||||
- 缺点:可能不准确
|
||||
|
||||
**选项 C**: 改用 `isinstance(shape, pptx.shapes.text_frame.TextFrameProxy)` 或类似方法
|
||||
- 优点:准确
|
||||
- 缺点:需要了解内部实现
|
||||
|
||||
**选择:A** - 使用 `has_text_frame` 属性,这是检测形状是否有文本框的标准方法
|
||||
|
||||
### D3: 处理 Windows 路径比较
|
||||
|
||||
**选项 A**: 在测试中使用 `str()` 转换 Path 对象
|
||||
- 优点:简单
|
||||
- 缺点:可能引入平台特定代码
|
||||
|
||||
**选项 B**: 在 `Presentation` 类中标准化路径存储
|
||||
- 优点:一劳永逸
|
||||
- 缺点:影响较大
|
||||
|
||||
**选择:A** - 在测试中转换类型,因为这是测试代码问题,不是生产代码问题
|
||||
|
||||
### D4: 处理缺失的 Fixture
|
||||
|
||||
**选项 A**: 添加 `mock_template_class` fixture
|
||||
- 优点:保持测试完整
|
||||
- 缺点:需要了解 mock 的使用方式
|
||||
|
||||
**选项 B**: 删除该测试(如果功能已被其他测试覆盖)
|
||||
- 优点:减少维护负担
|
||||
- 缺点:可能降低覆盖度
|
||||
|
||||
**选择:A** - 添加 fixture,因为该测试验证的功能未被其他测试覆盖
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| 风险 | 描述 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 修复后功能回归 | 修改测试可能隐藏真正的 bug | 修复后运行完整测试套件,确认所有测试通过 |
|
||||
| 版本兼容性 | 不同 python-pptx 版本可能有不同行为 | 使用跨版本兼容的 API(如 `has_text_frame`) |
|
||||
| 测试脆弱性 | 测试与实现耦合过紧 | 使用公共 API,减少内部实现细节依赖 |
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 备份当前测试代码
|
||||
2. 修改 `tests/conftest_pptx.py` 中的 `count_elements_by_type` 方法
|
||||
3. 修改 `tests/integration/test_presentation.py` 中的测试
|
||||
4. 修改 `tests/integration/test_rendering_flow.py` 中的测试
|
||||
5. 修改 `tests/unit/test_presentation.py` 中的测试
|
||||
6. 添加缺失的 fixture
|
||||
7. 运行完整测试套件验证修复
|
||||
8. 确保代码覆盖率不下降
|
||||
|
||||
## Open Questions
|
||||
|
||||
**Q1**: 是否需要为元素类型检测添加版本兼容性检查?
|
||||
- 当前选择使用 `has_text_frame` 属性,兼容性较好
|
||||
|
||||
**Q2**: 是否应该将测试辅助函数移至独立的工具模块?
|
||||
- 当前保持现状,避免引入不必要的重构
|
||||
@@ -0,0 +1,51 @@
|
||||
# 修复失败的测试
|
||||
|
||||
## Why
|
||||
|
||||
当前项目有 7 个测试失败和 1 个测试错误,导致测试通过率从 100% 降至 97.5%。这些问题主要是由于:
|
||||
1. 测试代码与实际 API 行为不一致
|
||||
2. python-pptx 库版本变化导致的 API 差异
|
||||
3. Windows 平台路径处理差异
|
||||
4. 测试 fixture 定义缺失
|
||||
|
||||
如果不修复这些问题,将影响持续集成的可靠性,并阻碍后续功能开发。
|
||||
|
||||
## What Changes
|
||||
|
||||
### 测试修复
|
||||
- 修复 `test_render_slide_with_template` - 调整测试以适应返回的元素对象
|
||||
- 修复 `test_render_slide_with_variables` - 调整测试以适应返回的元素对象
|
||||
- 修复 `test_render_direct_elements` - 调整测试以适应返回的元素对象
|
||||
- 修复 `test_image_element_rendering` - 使用正确的枚举或替代方法检测文本框
|
||||
- 修复 `test_shape_element_rendering` - 使用正确的枚举或替代方法检测形状
|
||||
- 修复 `test_table_element_rendering` - 使用正确的枚举或替代方法检测表格
|
||||
- 修复 `test_init_with_templates_dir` - 统一路径类型比较
|
||||
- 修复 `test_render_slide_with_template_merges_background` - 添加缺失的 fixture
|
||||
|
||||
### 代码改进
|
||||
- 在 `tests/conftest_pptx.py` 中改进元素类型检测逻辑
|
||||
- 添加更健壮的 python-pptx 版本兼容性处理
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `test-fix-framework`: 建立测试修复的标准框架,确保未来 API 变更时测试能同步更新
|
||||
|
||||
### Modified Capabilities
|
||||
- 无(现有功能的测试修复,不改变功能需求)
|
||||
|
||||
## Impact
|
||||
|
||||
### 受影响代码
|
||||
- `tests/integration/test_presentation.py` - 3 个失败测试
|
||||
- `tests/integration/test_rendering_flow.py` - 3 个失败测试
|
||||
- `tests/unit/test_presentation.py` - 1 个失败测试 + 1 个错误
|
||||
- `tests/conftest_pptx.py` - 辅助验证函数
|
||||
|
||||
### 测试影响
|
||||
- 修复后测试通过率: 100% (316/316)
|
||||
- 代码覆盖率: 维持 94%
|
||||
|
||||
### 依赖影响
|
||||
- 无新增依赖
|
||||
- 保持与 python-pptx 现有版本的兼容性
|
||||
@@ -0,0 +1,33 @@
|
||||
# 测试修复规格说明
|
||||
|
||||
本变更是对现有测试代码的修复,不引入新功能,也不修改现有功能的行为。
|
||||
|
||||
## 不适用说明
|
||||
|
||||
此变更不涉及:
|
||||
- 新功能需求
|
||||
- 现有功能需求的修改
|
||||
- 新增 API 或接口
|
||||
|
||||
因此,不需要创建新的规格说明文件。
|
||||
|
||||
所有需要修复的测试已经在现有测试套件中定义,修复的是测试代码与实现之间的不一致,而非功能需求的变化。
|
||||
|
||||
## 验证方式
|
||||
|
||||
修复完成后,运行以下命令验证:
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
uv run pytest -v
|
||||
|
||||
# 验证测试通过率
|
||||
uv run pytest --tb=short
|
||||
|
||||
# 检查代码覆盖率
|
||||
uv run pytest --cov --cov-report=term-missing
|
||||
```
|
||||
|
||||
预期结果:
|
||||
- 所有 316 个测试通过
|
||||
- 代码覆盖率 >= 94%
|
||||
@@ -0,0 +1,25 @@
|
||||
# 任务清单:修复失败的测试
|
||||
|
||||
## 1. 修复 conftest_pptx.py 中的元素类型检测
|
||||
|
||||
- [x] 1.1 修复 `count_elements_by_type` 方法:将 `MSO_SHAPE.TEXT_BOX` 替换为使用 `shape.has_text_frame` 属性检测文本框
|
||||
- [x] 1.2 运行修复后的测试验证 `test_image_element_rendering`
|
||||
- [x] 1.3 运行修复后的测试验证 `test_shape_element_rendering`
|
||||
- [x] 1.4 运行修复后的测试验证 `test_table_element_rendering`
|
||||
|
||||
## 2. 修复 integration/test_presentation.py 中的测试
|
||||
|
||||
- [x] 2.1 修复 `test_render_slide_with_template`:使用 `element.type` 和 `element.content` 属性访问,而非字典的 `get` 方法
|
||||
- [x] 2.2 修复 `test_render_slide_with_variables`:同上
|
||||
- [x] 2.3 修复 `test_render_direct_elements`:使用对象属性访问替换字典访问
|
||||
|
||||
## 3. 修复 unit/test_presentation.py 中的测试
|
||||
|
||||
- [x] 3.1 修复 `test_init_with_templates_dir`:将断言中的字符串转换为 Path 对象后比较
|
||||
- [x] 3.2 添加缺失的 `mock_template_class` fixture 到 conftest.py
|
||||
|
||||
## 4. 验证和回归测试
|
||||
|
||||
- [x] 4.1 运行完整测试套件,确认所有 316 个测试通过
|
||||
- [x] 4.2 运行代码覆盖率检查,确认覆盖率 >= 94%
|
||||
- [x] 4.3 运行 e2e 测试验证整体功能未受影响
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-03
|
||||
185
openspec/changes/archive/2026-03-03-inline-templates/design.md
Normal file
185
openspec/changes/archive/2026-03-03-inline-templates/design.md
Normal file
@@ -0,0 +1,185 @@
|
||||
## Context
|
||||
|
||||
**当前状态**:
|
||||
- 项目已有完整的外部模板系统(core/template.py),包括变量替换、条件渲染等功能
|
||||
- 模板系统采用文件隔离方式,模板定义在独立的 YAML 文件中
|
||||
- 通过 `template: <name>` 引用外部模板,使用 `--template-dir` 指定模板目录
|
||||
- 项目中没有任何实际使用模板的案例,说明外部模板的使用门槛较高
|
||||
|
||||
**问题分析**:
|
||||
通过代码分析和 grep 搜索发现:
|
||||
1. 整个项目中没有包含 `template:` 字段的 YAML 文件
|
||||
2. 所有模板测试用例都是动态生成的,没有真实的用户使用场景
|
||||
3. 外部模板需要额外的文件管理工作,增加了使用复杂度
|
||||
|
||||
**项目约束**:
|
||||
- 面向中文开发者,使用中文编写注释和文档
|
||||
- 使用 uv 运行 Python 脚本,禁止直接使用主机环境 Python
|
||||
- 测试文件必须放在 temp/ 目录
|
||||
- 每次功能迭代需要更新 README.md 和 README_DEV.md
|
||||
- 所有需求必须设计全面、合理的测试内容
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 支持在 YAML 源文件中定义和使用内联模板
|
||||
- 保持与外部模板系统的兼容性,允许混合使用
|
||||
- 禁止内联和外部模板同名,要求模板名称必须唯一
|
||||
- 提供完整的错误处理和验证机制
|
||||
- 保持代码向后兼容,不破坏现有功能
|
||||
|
||||
**Non-Goals:**
|
||||
- 不支持内联模板之间的相互引用
|
||||
- 不支持内联模板引用外部模板
|
||||
- 不实现模板继承或组合功能
|
||||
- 不修改外部模板的现有功能
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1: 使用 `templates` 字段定义内联模板
|
||||
|
||||
**选择**: 在 YAML 文件顶层新增 `templates` 字段,字典类型,键为模板名称,值为模板定义
|
||||
|
||||
**理由**:
|
||||
- 保持 YAML 结构清晰,与 `metadata`、`slides` 字段同级
|
||||
- 字典结构便于查找和管理
|
||||
- 不与现有字段冲突,向后兼容
|
||||
|
||||
**替代方案考虑**:
|
||||
- 方案 A: 在 `metadata` 中定义 templates - 会造成 metadata 职责混乱
|
||||
- 方案 B: 使用 YAML 锚点语法 - 无法支持变量替换和条件渲染
|
||||
- 方案 C: 在每个 slide 中定义内联模板 - 无法实现模板复用
|
||||
|
||||
### 决策 2: 禁止内联和外部模板同名
|
||||
|
||||
**选择**: 当同名模板同时存在于内联和外部时,直接抛出 ERROR 级别错误,不允许使用该模板
|
||||
|
||||
**理由**:
|
||||
- 显式报错比隐式选择更安全,避免用户意外使用了错误的模板
|
||||
- 内联和外部模板混合使用的情况较少,同名通常是错误或命名不当
|
||||
- 强制用户明确模板来源,提升代码清晰度
|
||||
- 降低调试难度,错误立即暴露而不是隐式选择
|
||||
|
||||
**实现**:
|
||||
```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}' 同时存在于内联模板和外部模板目录\n"
|
||||
f"请使用不同的模板名称以避免冲突"
|
||||
)
|
||||
inline_data = self.inline_templates[template_name]
|
||||
return Template.from_data(inline_data, template_name)
|
||||
|
||||
# 3. 回退到外部模板
|
||||
if template_name not in self.template_cache:
|
||||
self.template_cache[template_name] = Template(
|
||||
template_name, self.templates_dir
|
||||
)
|
||||
return self.template_cache[template_name]
|
||||
|
||||
# 辅助方法:检查外部模板是否存在
|
||||
def _external_template_exists(self, template_name):
|
||||
"""检查外部模板文件是否存在"""
|
||||
if not self.templates_dir:
|
||||
return False
|
||||
template_path = Path(self.templates_dir) / f"{template_name}.yaml"
|
||||
return template_path.exists()
|
||||
```
|
||||
|
||||
### 决策 3: 新增 `Template.from_data()` 类方法
|
||||
|
||||
**选择**: 添加类方法从字典创建模板实例,而不是修改现有的 `__init__`
|
||||
|
||||
**理由**:
|
||||
- 保持现有 `Template.__init__` 的不变性,专注于外部模板加载
|
||||
- 区分内联和外部模板的创建路径,降低耦合度
|
||||
- 类方法语义清晰:`Template.from_data(data, name)` vs `Template(name, dir)`
|
||||
|
||||
**实现**:
|
||||
```python
|
||||
@classmethod
|
||||
def from_data(cls, template_data, template_name):
|
||||
"""从字典创建模板(内联模板)"""
|
||||
obj = cls.__new__(cls)
|
||||
obj.data = template_data
|
||||
obj.vars_def = {}
|
||||
for var in template_data.get('vars', []):
|
||||
obj.vars_def[var['name']] = var
|
||||
obj.elements = template_data.get('elements', [])
|
||||
return obj
|
||||
```
|
||||
|
||||
### 决策 4: 禁止内联模板相互引用
|
||||
|
||||
**选择**: 在设计阶段明确禁止内联模板相互引用,不在实现中处理此场景
|
||||
|
||||
**理由**:
|
||||
- 降低实现复杂度,避免循环引用检测逻辑
|
||||
- 内联模板主要用于单文档简单场景,不需要复杂引用
|
||||
- 外部模板系统已经支持跨文档复用,复杂场景应使用外部模板
|
||||
|
||||
**错误处理**:
|
||||
- 如果检测到内联模板引用其他内联模板,抛出 ERROR 级别错误
|
||||
- 明确在文档中说明此限制
|
||||
|
||||
### 决策 5: 完整的错误处理机制
|
||||
|
||||
**选择**: 实现四层错误处理:模板定义错误、模板引用错误、变量传递错误、循环引用错误
|
||||
|
||||
**理由**:
|
||||
- 确保用户能够快速定位和修复问题
|
||||
- 提前发现潜在的错误,避免在渲染时才暴露
|
||||
- 符合项目现有的错误处理模式(YAMLError 异常)
|
||||
|
||||
**错误场景**:
|
||||
1. 模板定义错误: `templates` 不是字典、缺少 `elements` 字段、变量定义错误
|
||||
2. 模板引用错误: 引用的内联模板不存在、外部模板不存在
|
||||
3. 变量传递错误: 缺少必需变量、传递未定义变量
|
||||
4. 循环引用错误: 内联模板自引用(虽然禁止相互引用,但需要检测自引用)
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险 1: 内存占用增加
|
||||
|
||||
**风险**: 内联模板数据保存在内存中,可能增加内存占用
|
||||
|
||||
**缓解措施**:
|
||||
- 单个模板数据量通常很小(<1KB),内存增加可忽略
|
||||
- 内联模板不需要缓存(已在内存中),不会重复占用
|
||||
- 未来可以添加内存监控,如果问题严重再优化
|
||||
|
||||
### 风险 2: YAML 文件可读性下降
|
||||
|
||||
**风险**: 大量的内联模板定义可能使 YAML 文件过长,降低可读性
|
||||
|
||||
**缓解措施**:
|
||||
- 内联模板适合简单场景,复杂场景建议使用外部模板
|
||||
- 在文档中提供最佳实践建议
|
||||
- IDE 的折叠功能可以帮助管理长文件
|
||||
|
||||
### 风险 3: 模板命名冲突强制检测
|
||||
|
||||
**风险**: 内联和外部模板同名时,用户可能期望使用外部模板,但系统直接报错
|
||||
|
||||
**缓解措施**:
|
||||
- 明确的错误消息,指出模板冲突的具体位置(内联 templates 字段 vs 外部模板目录)
|
||||
- 在文档中详细说明命名规则,建议使用不同的命名空间或前缀
|
||||
- 在示例代码中展示正确的命名方式
|
||||
- 提供 migration 指南,帮助用户解决命名冲突
|
||||
|
||||
### 风险 4: 循环引用误判
|
||||
|
||||
**风险**: 虽然禁止相互引用,但可能存在复杂的循环引用路径
|
||||
|
||||
**缓解措施**:
|
||||
- 实现简单的循环引用检测(DFS 遍历)
|
||||
- 在设计阶段就明确规则,降低实现复杂度
|
||||
- 添加充分的测试用例覆盖边界情况
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。设计阶段已明确所有关键决策。
|
||||
@@ -0,0 +1,41 @@
|
||||
## Why
|
||||
|
||||
当前外部模板系统虽然功能完整,但需要单独管理模板文件,使用门槛较高。项目中没有任何实际使用的案例,说明用户体验有待改善。通过支持在 YAML 源文件中内联定义模板,可以在不需要跨文档复用的情况下,大幅简化单文档的编写流程,提升开发效率。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在 YAML 文件中新增 `templates` 字段,允许在源文件中定义模板
|
||||
- 修改 `Presentation.get_template()` 方法,支持内联模板和外部模板的查找,内联模板优先
|
||||
- 在 `Template` 类中新增 `from_data()` 类方法,支持从字典创建模板实例
|
||||
- 在 `yaml_loader` 中新增 `validate_templates_yaml()` 函数,验证 templates 字段结构
|
||||
- 修改现有模板验证逻辑,支持内联模板和外部模板共存
|
||||
- 添加完整的错误处理机制,包括模板定义错误、引用错误、变量传递错误和循环引用检测
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `inline-templates`: 支持 YAML 源文件中定义和使用内联模板,包括模板定义、变量替换、条件渲染等功能
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
无。此变更为新增功能,不修改现有功能的需求行为。
|
||||
|
||||
## Impact
|
||||
|
||||
**受影响的代码模块**:
|
||||
- `core/presentation.py`: 修改 `__init__` 保存 templates 字段,修改 `get_template()` 支持内联模板查找
|
||||
- `core/template.py`: 新增 `from_data()` 类方法,支持从字典创建模板
|
||||
- `loaders/yaml_loader.py`: 新增 `validate_templates_yaml()` 验证函数
|
||||
- `validators/`: 可能需要扩展验证器以支持 templates 字段的验证
|
||||
|
||||
**受影响的测试**:
|
||||
- 需要新增内联模板的单元测试和集成测试
|
||||
- 现有模板测试需要覆盖内联模板场景
|
||||
|
||||
**向后兼容性**:
|
||||
- 完全向后兼容。不使用 `templates` 字段时,系统行为与现有版本完全一致
|
||||
- 外部模板功能保持不变,可以与内联模板混合使用
|
||||
|
||||
**文档**:
|
||||
- 需要更新 README.md 和 README_DEV.md,说明内联模板的语法和使用方法
|
||||
@@ -0,0 +1,112 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: YAML 文件支持内联模板定义
|
||||
|
||||
系统 SHALL 允许用户在 YAML 源文件的 `templates` 字段中定义内联模板,模板定义包括变量和元素。
|
||||
|
||||
#### Scenario: 定义简单的内联模板
|
||||
- **WHEN** 用户在 YAML 文件的 `templates` 字段中定义一个名为 `title-slide` 的模板,包含 `vars` 和 `elements` 字段
|
||||
- **THEN** 系统 SHALL 成功解析模板定义,并将模板存储在 `inline_templates` 字典中
|
||||
|
||||
#### Scenario: 定义多个内联模板
|
||||
- **WHEN** 用户在 YAML 文件的 `templates` 字段中定义多个不同的内联模板
|
||||
- **THEN** 系统 SHALL 成功解析所有模板定义,并允许通过模板名称引用任何一个模板
|
||||
|
||||
### Requirement: 内联模板支持变量替换
|
||||
|
||||
系统 SHALL 支持在内联模板的元素中使用 `{variable}` 语法定义变量占位符,并在渲染时替换为实际值。
|
||||
|
||||
#### Scenario: 简单变量替换
|
||||
- **WHEN** 用户定义内联模板,在元素的 `content` 字段中使用 `{title}` 占位符,并在使用模板时提供 `title: "My Title"` 变量值
|
||||
- **THEN** 系统 SHALL 将占位符替换为 `My Title`
|
||||
|
||||
#### Scenario: 嵌套变量替换
|
||||
- **WHEN** 用户的模板默认值引用其他变量,如 `default: "{title} - Extended"`
|
||||
- **THEN** 系统 SHALL 递归解析所有变量引用,生成完整的替换结果
|
||||
|
||||
### Requirement: 内联模板支持条件渲染
|
||||
|
||||
系统 SHALL 支持通过 `visible` 字段控制元素的显示,基于变量值的条件表达式。
|
||||
|
||||
#### Scenario: 条件渲染为真
|
||||
- **WHEN** 元素的 `visible` 字段设置为 `"{subtitle != ''}"`,且用户提供非空的 `subtitle` 值
|
||||
- **THEN** 系统 SHALL 在渲染结果中包含该元素
|
||||
|
||||
#### Scenario: 条件渲染为假
|
||||
- **WHEN** 元素的 `visible` 字段设置为 `"{subtitle != ''}"`,且用户提供空的 `subtitle` 值
|
||||
- **THEN** 系统 SHALL 在渲染结果中排除该元素
|
||||
|
||||
### Requirement: 内联模板支持变量默认值
|
||||
|
||||
系统 SHALL 支持在模板的 `vars` 定义中为可选变量提供 `default` 值。
|
||||
|
||||
#### Scenario: 使用默认值
|
||||
- **WHEN** 用户的模板定义 `subtitle` 变量为可选(`required: false`),并提供 `default: ""`
|
||||
- **THEN** 系统 SHALL 在用户未提供 `subtitle` 变量时使用默认值
|
||||
|
||||
#### Scenario: 提供值覆盖默认值
|
||||
- **WHEN** 用户的模板定义了默认值,且用户在使用模板时提供了该变量的值
|
||||
- **THEN** 系统 SHALL 使用用户提供的值,而不是默认值
|
||||
|
||||
### Requirement: 禁止内联和外部模板同名
|
||||
|
||||
系统 SHALL 检测内联模板和外部模板的名称冲突,当同名模板同时存在时,抛出 ERROR 级别错误。
|
||||
|
||||
#### Scenario: 内联和外部模板同名
|
||||
- **WHEN** 同名模板同时存在于内联 `templates` 字段和外部模板目录中
|
||||
- **THEN** 系统 SHALL 抛出 ERROR 级别错误,明确指出模板名称冲突,并禁止使用该模板
|
||||
|
||||
#### Scene: 内联和外部模板名称唯一
|
||||
- **WHEN** 内联模板和外部模板的名称都不相同
|
||||
- **THEN** 系统 SHALL 正常加载和使用模板,不产生任何错误
|
||||
### Requirement: 禁止内联模板相互引用
|
||||
|
||||
系统 SHALL 检测并禁止内联模板之间的相互引用。
|
||||
|
||||
#### Scenario: 检测到内联模板相互引用
|
||||
- **WHEN** 一个内联模板的 `elements` 中引用了另一个内联模板
|
||||
- **THEN** 系统 SHALL 抛出 ERROR 级别错误,明确指出内联模板不支持相互引用
|
||||
|
||||
#### Scenario: 内联模板正常使用
|
||||
- **WHEN** 用户使用内联模板,且模板的 `elements` 中不包含其他模板引用
|
||||
- **THEN** 系统 SHALL 正常渲染该模板
|
||||
|
||||
### Requirement: 完整的错误处理
|
||||
|
||||
系统 SHALL 对内联模板的错误场景提供清晰的错误消息和适当的错误级别。
|
||||
|
||||
#### Scenario: 模板定义结构错误
|
||||
- **WHEN** `templates` 字段不是字典类型,或某个模板缺少必需的 `elements` 字段
|
||||
- **THEN** 系统 SHALL 在加载时抛出 ERROR 级别错误,包含具体的错误位置和原因
|
||||
|
||||
#### Scenario: 变量定义错误
|
||||
- **WHEN** 模板的 `vars` 定义中某个变量缺少必需的 `name` 字段
|
||||
- **THEN** 系统 SHALL 在加载时抛出 ERROR 级别错误
|
||||
|
||||
#### Scenario: 变量传递错误
|
||||
- **WHEN** 用户使用模板时缺少必需的变量,或传递了未定义的变量
|
||||
- **THEN** 系统 SHALL 在渲染时抛出 ERROR 或 WARNING 级别错误,明确指出问题所在
|
||||
|
||||
### Requirement: 向后兼容
|
||||
|
||||
系统 SHALL 在不使用 `templates` 字段时,保持与现有版本完全一致的行为。
|
||||
|
||||
#### Scenario: 不使用 templates 字段
|
||||
- **WHEN** 用户的 YAML 文件不包含 `templates` 字段,只使用外部模板或直接定义元素
|
||||
- **THEN** 系统 SHALL 表现与现有版本完全一致,不产生任何副作用
|
||||
|
||||
#### Scenario: 混合使用内联和外部模板
|
||||
- **WHEN** 用户的 YAML 文件同时定义了内联模板和使用外部模板
|
||||
- **THEN** 系统 SHALL 支持两种模板类型在同一演示文稿中混合使用
|
||||
|
||||
### Requirement: 内联模板数据验证
|
||||
|
||||
系统 SHALL 在加载 YAML 文件时验证内联模板的结构和内容。
|
||||
|
||||
#### Scenario: 验证模板结构
|
||||
- **WHEN** 用户定义的内联模板包含 `vars` 和 `elements` 字段,且结构正确
|
||||
- **THEN** 系统 SHALL 验证通过,允许后续使用该模板
|
||||
|
||||
#### Scenario: 验证元素结构
|
||||
- **WHEN** 用户的内联模板 `elements` 中定义了有效的元素类型和字段
|
||||
- **THEN** 系统 SHALL 验证通过,元素在渲染时正常工作
|
||||
@@ -0,0 +1,54 @@
|
||||
## 1. 核心功能实现
|
||||
|
||||
- [x] 1.1 在 `core/template.py` 中新增 `from_data()` 类方法,支持从字典创建模板实例
|
||||
- [x] 1.2 修改 `core/presentation.py` 的 `__init__` 方法,解析并保存 `templates` 字段到 `self.inline_templates`
|
||||
- [x] 1.3 修改 `core/presentation.py` 的 `get_template()` 方法,实现同名模板检测并抛出 ERROR 错误
|
||||
- [x] 1.4 在 `loaders/yaml_loader.py` 中新增 `validate_templates_yaml()` 函数,验证 `templates` 字段结构
|
||||
- [x] 1.5 修改 `loaders/yaml_loader.py` 的 `validate_presentation_yaml()` 函数,调用 `validate_templates_yaml()` 进行验证
|
||||
|
||||
## 2. 验证逻辑实现
|
||||
|
||||
- [x] 2.1 实现 `validate_templates_yaml()` 中的模板结构验证:检查 `templates` 是否为字典
|
||||
- [x] 2.2 实现 `validate_templates_yaml()` 中的模板元素验证:检查每个模板是否有必需的 `elements` 字段
|
||||
- [x] 2.3 实现 `validate_templates_yaml()` 中的变量定义验证:检查每个变量是否有必需的 `name` 字段
|
||||
- [x] 2.4 实现 `validate_templates_yaml()` 中的默认值验证:检测默认值中引用不存在的变量
|
||||
|
||||
## 3. 错误处理实现
|
||||
|
||||
- [x] 3.1 在 `core/presentation.py` 的 `get_template()` 中添加同名模板检测逻辑,抛出 ERROR 错误
|
||||
- [x] 3.2 在 `core/template.py` 的 `render()` 方法中添加内联模板相互引用的检测逻辑
|
||||
- [x] 3.3 优化错误消息格式,包含模板名称和具体位置信息(已在上述实现中完成)
|
||||
- [x] 3.4 实现循环引用检测:防止内联模板自引用(已在任务 3.2 中实现)
|
||||
|
||||
## 4. 测试用例实现
|
||||
|
||||
- [x] 4.1 在 `tests/unit/test_template.py` 中新增 `from_data()` 方法的单元测试
|
||||
- [x] 4.2 在 `tests/unit/test_presentation.py` 中新增内联模板查找的单元测试
|
||||
- [x] 4.3 在 `tests/unit/test_loaders/test_yaml_loader.py` 中新增 `validate_templates_yaml()` 的测试用例
|
||||
- [x] 4.4 在 `tests/integration/test_presentation.py` 中新增内联模板集成测试
|
||||
- [x] 4.5 新增内联模板基本使用场景测试:定义、引用、变量替换、条件渲染
|
||||
- [x] 4.6 新增同名模板报错测试:验证内联和外部模板同名时系统抛出 ERROR
|
||||
- [x] 4.7 新增内联模板错误处理测试:验证各种错误场景的错误消息和级别
|
||||
- [x] 4.8 新增向后兼容性测试:验证不使用 `templates` 字段时系统行为不变
|
||||
- [x] 4.2 在 `tests/unit/test_presentation.py` 中新增内联模板查找的单元测试
|
||||
- [x] 4.3 在 `tests/unit/test_loaders/test_yaml_loader.py` 中新增 `validate_templates_yaml()` 的测试用例
|
||||
- [x] 4.4 在 `tests/integration/test_presentation.py` 中新增内联模板集成测试
|
||||
- [x] 4.5 新增内联模板基本使用场景测试:定义、引用、变量替换、条件渲染
|
||||
- [x] 4.6 新增同名模板报错测试:验证内联和外部模板同名时系统抛出 ERROR
|
||||
- [x] 4.7 新增内联模板错误处理测试:验证各种错误场景的错误消息和级别
|
||||
- [x] 4.8 新增向后兼容性测试:验证不使用 `templates` 字段时系统行为不变
|
||||
|
||||
## 5. 文档更新
|
||||
|
||||
- [x] 5.1 更新 `README.md`,添加内联模板的语法说明和使用示例
|
||||
- [x] 5.2 更新 `README_DEV.md`,说明内联模板的实现细节和设计决策
|
||||
- [x] 5.3 添加内联模板的最佳实践指南,说明何时使用内联模板 vs 外部模板
|
||||
|
||||
## 6. 验证和收尾
|
||||
|
||||
- [x] 6.1 运行完整的测试套件,确保所有测试通过
|
||||
- [x] 6.2 使用 `uv run pytest -v` 验证新增测试用例
|
||||
- [x] 6.3 手动测试:创建包含内联模板的示例 YAML 文件,验证转换功能正常
|
||||
- [x] 6.4 手动测试:创建混合使用内联和外部模板的示例,验证优先级规则
|
||||
- [x] 6.5 使用 `uv run pytest --cov=. --cov-report=html` 检查测试覆盖率
|
||||
- [x] 6.6 使用 `uv run yaml2pptx.py check` 验证 YAML 验证功能正常
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-02
|
||||
@@ -0,0 +1,42 @@
|
||||
## Context
|
||||
|
||||
当前项目测试套件包含307个测试用例,其中42个失败,1个错误。失败原因主要分为三类:
|
||||
1. E2E测试使用错误的命令执行方式
|
||||
2. 测试代码本身存在缺陷(fixture缺失、Mock配置错误)
|
||||
3. 项目代码中存在真实bug
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 修复所有42个失败的测试用例
|
||||
- 确保E2E测试可以正确执行命令
|
||||
- 修复模板变量替换的bug
|
||||
- 修复验证结果格式化输出问题
|
||||
|
||||
**Non-Goals:**
|
||||
- 不添加新的测试用例
|
||||
- 不重构项目架构
|
||||
- 不修改核心业务逻辑
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. E2E测试命令执行方式
|
||||
- **决定**: 修改E2E测试中的命令调用方式
|
||||
- **理由**: 当前使用 `python -m uv run python` 导致虚拟环境中没有uv模块而失败
|
||||
- **替代方案**: 直接使用 `uv run` 作为命令前缀
|
||||
|
||||
### 2. Mock对象配置修复
|
||||
- **决定**: 修正Mock对象的配置,确保返回值正确设置
|
||||
- **理由**: 多个测试中Mock对象没有正确配置返回值
|
||||
|
||||
### 3. Fixture缺失问题
|
||||
- **决定**: 在conftest.py中添加缺失的fixture定义
|
||||
- **理由**: 部分测试引用的fixture未定义
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **风险**: 修改测试代码可能导致测试行为变化
|
||||
- **缓解**: 逐一验证每个修复后的测试用例
|
||||
|
||||
- **风险**: 模板变量替换修复可能影响现有功能
|
||||
- **缓解**: 运行所有相关测试确保功能正常
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
项目测试套件当前存在42个失败的测试用例,主要分为三类问题:测试运行方式错误(E2E测试使用`python -m uv run`导致命令执行失败)、测试代码本身的缺陷(缺失fixture、Mock对象配置不当),以及项目代码中的真实bug(模板变量替换不完整、验证结果格式化错误)。这些失败会影响持续集成流程和代码质量保证,必须立即修复。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 修复E2E测试中的命令执行方式,将`python -m uv run python`改为正确的`uv run`调用方式
|
||||
- 添加缺失的测试fixture(mock_template_class等)
|
||||
- 修正Mock对象的配置,确保返回值正确
|
||||
- 修复模板系统中变量替换的bug(字体变量替换、条件渲染)
|
||||
- 修复验证结果格式化输出(提示信息显示)
|
||||
- 修复HTML渲染器特殊字符转义问题(&字符)
|
||||
- 修复Presentation尺寸验证(防止16:9被解析为数学表达式)
|
||||
- 修复PPTX验证器的文本框检测方式
|
||||
- 确保所有测试通过,保持代码库的测试覆盖率
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
本变更不引入新功能。
|
||||
|
||||
### Modified Capabilities
|
||||
- `test-framework`: 修复测试框架相关的问题,确保测试可以正确运行
|
||||
|
||||
## Impact
|
||||
|
||||
- 测试文件:tests/e2e/*.py, tests/unit/test_*.py, tests/integration/*.py
|
||||
- 核心模块:core/template.py(模板变量替换)、core/presentation.py(尺寸验证)
|
||||
- 验证模块:validators/result.py(结果格式化)
|
||||
- 渲染模块:renderers/html_renderer.py(HTML转义)
|
||||
@@ -0,0 +1,61 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: E2E测试命令执行正常
|
||||
E2E测试必须能够正确执行命令行工具,不应出现模块未找到的错误。
|
||||
|
||||
#### Scenario: convert命令执行成功
|
||||
- **WHEN** 运行 `uv run yaml2pptx.py convert input.yaml output.pptx`
|
||||
- **THEN** 命令返回码为0,输出文件被创建
|
||||
|
||||
#### Scenario: check命令执行成功
|
||||
- **WHEN** 运行 `uv run yaml2pptx.py check input.yaml`
|
||||
- **THEN** 命令返回码为0(验证通过)或1(验证失败),但不出现模块错误
|
||||
|
||||
### Requirement: 测试Fixture完整定义
|
||||
所有测试使用的fixture必须在conftest.py中正确定义。
|
||||
|
||||
#### Scenario: fixture依赖可用
|
||||
- **WHEN** 测试引用sample_template fixture
|
||||
- **THEN** fixture被正确解析并返回临时模板目录路径
|
||||
|
||||
### Requirement: Mock对象配置正确
|
||||
Mock对象在测试中必须正确配置返回值。
|
||||
|
||||
#### Scenario: 表格渲染Mock配置
|
||||
- **WHEN** 测试渲染表格元素时使用Mock对象
|
||||
- **THEN** Mock对象支持列宽设置的订阅操作
|
||||
|
||||
### Requirement: 模板变量替换功能正常
|
||||
模板系统必须正确替换所有变量,包括字体属性中的变量。
|
||||
|
||||
#### Scenario: 字体属性变量替换
|
||||
- **WHEN** 模板包含 `{variable_name}` 在font属性中
|
||||
- **THEN** 变量被正确替换为实际值(需要用引号包裹)
|
||||
|
||||
### Requirement: 验证结果格式化输出正确
|
||||
验证结果必须正确格式化所有类型的消息。
|
||||
|
||||
#### Scenario: 提示信息显示
|
||||
- **WHEN** 验证结果包含INFO级别的问题
|
||||
- **THEN** 输出中应包含"个提示"文字
|
||||
|
||||
### Requirement: HTML渲染器特殊字符转义
|
||||
HTML渲染器必须正确转义特殊HTML字符。
|
||||
|
||||
#### Scenario: &字符转义
|
||||
- **WHEN** 文本内容包含 `&` 字符
|
||||
- **THEN** 输出中应包含 `&`
|
||||
|
||||
### Requirement: Presentation尺寸值验证
|
||||
Presentation类必须验证尺寸值的类型,防止YAML解析错误。
|
||||
|
||||
#### Scenario: 非字符串尺寸值
|
||||
- **WHEN** YAML中 `size: 16:9`(无引号)
|
||||
- **THEN** 抛出明确的错误信息
|
||||
|
||||
### Requirement: PPTX验证器兼容性
|
||||
PPTX文件验证器必须兼容不同版本的python-pptx。
|
||||
|
||||
#### Scenario: 文本框检测
|
||||
- **WHEN** 验证PPTX中的文本元素
|
||||
- **THEN** 通过检查text_frame属性来判断是否是文本框
|
||||
@@ -0,0 +1,52 @@
|
||||
## 1. 修复E2E测试命令执行问题
|
||||
|
||||
- [x] 1.1 修改 tests/e2e/test_convert_cmd.py 中的命令执行方式,将 `python -m uv run python` 改为 `uv run`
|
||||
- [x] 1.2 修改 tests/e2e/test_check_cmd.py 中的命令执行方式
|
||||
- [x] 1.3 运行E2E测试验证修复是否成功
|
||||
|
||||
## 2. 修复测试Fixture缺失问题
|
||||
|
||||
- [x] 2.1 在 tests/conftest.py 中添加 mock_template_class fixture
|
||||
- [x] 2.2 检查并修复 sample_template 变量引用问题
|
||||
- [x] 2.3 运行单元测试验证fixture修复
|
||||
|
||||
## 3. 修复Mock对象配置问题
|
||||
|
||||
- [x] 3.1 修复 tests/unit/test_renderers/test_pptx_renderer.py 中表格渲染的Mock配置
|
||||
- [x] 3.2 修复 tests/unit/test_renderers/test_html_renderer.py 中的Mock配置
|
||||
- [x] 3.3 验证渲染器测试通过
|
||||
|
||||
## 4. 修复模板变量替换Bug
|
||||
|
||||
- [x] 4.1 检查 core/template.py 中的变量替换逻辑
|
||||
- [x] 4.2 修复字体属性中的变量替换问题(测试YAML语法修正)
|
||||
- [x] 4.3 修复条件渲染中的变量求值问题
|
||||
- [x] 4.4 运行模板测试验证修复
|
||||
|
||||
## 5. 修复验证结果格式化问题
|
||||
|
||||
- [x] 5.1 检查 validators/result.py 中的格式化逻辑
|
||||
- [x] 5.2 修复提示信息(INFO)的显示问题
|
||||
- [x] 5.3 运行验证器测试确认修复
|
||||
|
||||
## 6. 修复HTML渲染器问题
|
||||
|
||||
- [x] 6.1 修复特殊字符&的HTML转义
|
||||
- [x] 6.2 修复测试期望值(px单位问题)
|
||||
- [x] 6.3 修复图片渲染测试
|
||||
|
||||
## 7. 修复PPTX验证器问题
|
||||
|
||||
- [x] 7.1 修复文本框检测方式(兼容不同版本python-pptx)
|
||||
- [x] 7.2 运行集成测试验证
|
||||
|
||||
## 8. 修复Presentation验证问题
|
||||
|
||||
- [x] 8.1 添加尺寸值类型验证
|
||||
- [x] 8.2 修复测试YAML语法(尺寸需要引号)
|
||||
|
||||
## 9. 最终验证
|
||||
|
||||
- [x] 9.1 运行全部测试套件
|
||||
- [ ] 9.2 确认所有测试通过(剩余9个失败为需要实现的功能增强)
|
||||
- [x] 9.3 记录修复结果
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-04
|
||||
@@ -0,0 +1,82 @@
|
||||
# Design: Add Description Field to Metadata, Templates, and Slides
|
||||
|
||||
## Context
|
||||
|
||||
当前项目使用YAML格式的模板文件和演示文稿定义。YAML文件包含metadata(元数据)部分和slides列表。模板系统(`template-system` spec)定义了模板的结构,包括vars、elements等字段。演示文稿通过YAML文件定义slides列表,每个slide可以引用模板或定义自定义元素。
|
||||
|
||||
项目目前没有内置的机制来描述文档整体、页面或幻灯片的用途。开发者只能通过元素内容和变量名来推断,这降低了演示文稿、模板和幻灯片的可维护性。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 为metadata添加可选的description字段
|
||||
- 为模板文件添加可选的description字段
|
||||
- 为幻灯片定义添加可选的description字段
|
||||
- 确保description字段在数据模型中被正确解析和保留
|
||||
- 保持完全的向后兼容性
|
||||
- 不影响渲染逻辑和输出结果
|
||||
|
||||
**Non-Goals:**
|
||||
- 不将description写入最终的PPTX文件(PowerPoint备注功能不在本次范围)
|
||||
- 不实现description的验证逻辑(如长度限制、格式要求)
|
||||
- 不实现基于description的搜索或过滤功能
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Description字段为可选的字符串类型
|
||||
|
||||
**决策:** description字段为可选字段,类型为字符串,可以为空字符串。
|
||||
|
||||
**理由:**
|
||||
- 保持简单,不过度设计
|
||||
- 用户可以自由描述,不受格式限制
|
||||
- 可选设计确保向后兼容
|
||||
|
||||
**替代方案考虑:**
|
||||
- 使用结构化格式(如对象,包含title、content等子字段)- 过于复杂,当前需求不需要
|
||||
|
||||
### 2. Description不参与渲染
|
||||
|
||||
**决策:** description字段仅用于文档目的,不传递给渲染器,不影响PPTX生成。
|
||||
|
||||
**理由:**
|
||||
- PowerPoint的备注功能需要额外处理,且不在当前需求范围
|
||||
- 保持渲染逻辑简单,避免引入不必要的复杂性
|
||||
|
||||
**替代方案考虑:**
|
||||
- 将description写入PPTX备注 - 需要额外的python-pptx API调用,增加复杂度
|
||||
|
||||
### 3. 在数据模型层面实现
|
||||
|
||||
**决策:** 在数据模型类(Metadata/Document、Template、Slide)中添加description属性,在YAML解析时读取该字段。
|
||||
|
||||
**理由:**
|
||||
- 与现有代码结构一致
|
||||
- 便于统一处理和访问
|
||||
- metadata中的description可以描述整个文档
|
||||
|
||||
**替代方案考虑:**
|
||||
- 仅在YAML解析时保留,不添加到数据模型 - 会导致信息丢失,不利于后续使用
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| description字段可能被滥用或包含不恰当内容 | 依赖用户自律,不添加内容验证 |
|
||||
| 增加数据模型的字段数量 | 影响很小,仅增加一个可选字符串字段 |
|
||||
| 用户可能期望description能影响输出 | 在文档中明确说明description仅用于文档目的 |
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 更新数据模型类,添加description属性(Metadata、Template、Slide)
|
||||
2. 更新YAML解析器,读取metadata、模板和幻灯片的description字段
|
||||
3. 添加测试用例验证description正确读取和保留
|
||||
4. 更新文档说明新字段的用法
|
||||
|
||||
**回滚策略:**
|
||||
- 由于是新增可选字段,移除该字段不会影响现有功能
|
||||
- 如需回滚,只需从数据模型和解析器中移除description相关代码
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。本设计较为简单直接,没有未解决的技术问题。
|
||||
@@ -0,0 +1,36 @@
|
||||
# Proposal: Add Description Field to Pages and Slides
|
||||
|
||||
## Why
|
||||
|
||||
当前项目和模板系统中缺乏对文档、页面和幻灯片用途的内置说明机制。当用户或开发者查看演示文稿YAML时,难以快速理解整个文档的概要、某个模板的设计意图或幻灯片的使用场景。添加description字段可以提供自文档化的能力,提高演示文稿、模板和幻灯片的可维护性和可读性。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 为metadata添加可选的 `description` 字段,用于描述整个演示文稿的概要和用途
|
||||
- 为模板文件添加可选的 `description` 字段,用于说明该模板的用途和设计意图
|
||||
- 为幻灯片定义添加可选的 `description` 字段,用于说明该幻灯片的作用和内容
|
||||
- description字段为纯文本字符串,完全可选,不影响现有渲染逻辑
|
||||
- 保留description字段以供工具和文档生成使用
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `page-description`: 为模板页面和幻灯片添加描述字段的功能
|
||||
|
||||
### Modified Capabilities
|
||||
无现有能力的需求变更,仅新增可选字段
|
||||
|
||||
## Impact
|
||||
|
||||
**受影响的代码模块:**
|
||||
- `models/` - 元数据、模板和幻灯片数据模型需要支持description字段
|
||||
- `yaml_parsing/` - YAML解析器需要读取metadata和模板、幻灯片的description字段
|
||||
- `specs/` - 新增page-description规范文档
|
||||
|
||||
**不受影响的模块:**
|
||||
- 渲染逻辑 - description字段不影响视觉渲染
|
||||
- 输出PPTX - description不写入最终的PPTX文件
|
||||
|
||||
**向后兼容性:**
|
||||
- 完全向后兼容,description为可选字段
|
||||
- 现有模板和YAML文件无需修改即可继续使用
|
||||
@@ -0,0 +1,130 @@
|
||||
# Page Description
|
||||
|
||||
## Purpose
|
||||
|
||||
Page Description功能允许用户为文档元数据、模板和幻灯片添加描述信息,用于说明文档概要、页面的用途、设计意图或内容概要。这提高了演示文稿、模板和幻灯片的可读性和可维护性。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: metadata必须支持可选的description字段
|
||||
|
||||
演示文稿的metadata SHALL 支持可选的 `description` 字段,用于描述整个演示文稿的概要和用途。
|
||||
|
||||
#### Scenario: metadata包含description字段
|
||||
|
||||
- **WHEN** YAML文件的metadata定义了 `description: "这是关于项目年度总结的演示文稿"`
|
||||
- **THEN** 系统成功加载该描述字段,可通过metadata对象访问
|
||||
|
||||
#### Scenario: metadata不包含description字段
|
||||
|
||||
- **WHEN** YAML文件的metadata未定义 `description` 字段
|
||||
- **THEN** 系统正常加载演示文稿,description属性为None或空字符串
|
||||
|
||||
#### Scenario: metadata description为空字符串
|
||||
|
||||
- **WHEN** metadata定义了 `description: ""`
|
||||
- **THEN** 系统接受空字符串作为有效值
|
||||
|
||||
### Requirement: 模板必须支持可选的description字段
|
||||
|
||||
模板 SHALL 支持可选的 `description` 字段,用于描述该模板的用途和设计意图。
|
||||
|
||||
#### Scenario: 模板包含description字段
|
||||
|
||||
- **WHEN** 模板文件定义了 `description: "用于章节标题页的模板,包含主标题和副标题"`
|
||||
- **THEN** 系统成功加载该描述字段,可通过模板对象访问
|
||||
|
||||
#### Scenario: 模板不包含description字段
|
||||
|
||||
- **WHEN** 模板文件未定义 `description` 字段
|
||||
- **THEN** 系统正常加载模板,description属性为None或空字符串
|
||||
|
||||
#### Scenario: description为空字符串
|
||||
|
||||
- **WHEN** 模板文件定义了 `description: ""`
|
||||
- **THEN** 系统接受空字符串作为有效值
|
||||
|
||||
### Requirement: 幻灯片必须支持可选的description字段
|
||||
|
||||
幻灯片定义 SHALL 支持可选的 `description` 字段,用于描述该幻灯片的作用和内容。
|
||||
|
||||
#### Scenario: 幻灯片包含description字段
|
||||
|
||||
- **WHEN** 幻灯片定义了 `description: "介绍项目背景和目标"`
|
||||
- **THEN** 系统成功加载该描述字段,可通过幻灯片对象访问
|
||||
|
||||
#### Scenario: 幻灯片不包含description字段
|
||||
|
||||
- **WHEN** 幻灯片定义未包含 `description` 字段
|
||||
- **THEN** 系统正常处理幻灯片,description属性为None或空字符串
|
||||
|
||||
### Requirement: description字段不得影响渲染逻辑
|
||||
|
||||
系统 SHALL 在渲染过程中忽略 `description` 字段,不影响最终的PPTX输出。
|
||||
|
||||
#### Scenario: 渲染包含description的模板
|
||||
|
||||
- **WHEN** 系统渲染包含 `description` 字段的模板
|
||||
- **THEN** description不参与元素渲染,不影响输出结果
|
||||
|
||||
#### Scenario: 渲染包含description的幻灯片
|
||||
|
||||
- **WHEN** 系统渲染包含 `description` 字段的幻灯片
|
||||
- **THEN** description不写入PPTX文件,不影响输出结果
|
||||
|
||||
### Requirement: YAML解析器必须正确解析description字段
|
||||
|
||||
系统 SHALL 从YAML文件中正确读取 `description` 字段,并将其传递给数据模型。
|
||||
|
||||
#### Scenario: 解析metadata中的description
|
||||
|
||||
- **WHEN** YAML文件的metadata包含 `description: "文档描述"`
|
||||
- **THEN** 解析器将该字符串传递给metadata对象的description属性
|
||||
|
||||
#### Scenario: 解析模板中的description
|
||||
|
||||
- **WHEN** YAML文件中的模板包含 `description: "这是描述"`
|
||||
- **THEN** 解析器将该字符串传递给模板对象的description属性
|
||||
|
||||
#### Scenario: 解析幻灯片中的description
|
||||
|
||||
- **WHEN** YAML文件中的幻灯片包含 `description: "幻灯片描述"`
|
||||
- **THEN** 解析器将该字符串传递给幻灯片对象的description属性
|
||||
|
||||
### Requirement: 数据模型必须包含description属性
|
||||
|
||||
元数据、模板和幻灯片数据模型 SHALL 包含 `description` 属性,用于存储描述信息。
|
||||
|
||||
#### Scenario: metadata对象包含description属性
|
||||
|
||||
- **WHEN** 开发者访问metadata对象
|
||||
- **THEN** 可以通过 `metadata.description` 获取描述信息
|
||||
|
||||
#### Scenario: 模板对象包含description属性
|
||||
|
||||
- **WHEN** 开发者访问模板对象
|
||||
- **THEN** 可以通过 `template.description` 获取描述信息
|
||||
|
||||
#### Scenario: 幻灯片对象包含description属性
|
||||
|
||||
- **WHEN** 开发者访问幻灯片对象
|
||||
- **THEN** 可以通过 `slide.description` 获取描述信息
|
||||
|
||||
### Requirement: description字段必须支持中文字符
|
||||
|
||||
系统 SHALL 支持在 `description` 字段中使用中文字符和其他Unicode字符。
|
||||
|
||||
#### Scenario: metadata description包含中文
|
||||
|
||||
- **WHEN** metadata的description包含中文字符,如 "这是关于项目的演示文稿"
|
||||
- **THEN** 系统正确处理,不出现编码错误
|
||||
|
||||
#### Scenario: description包含中文
|
||||
|
||||
- **WHEN** 模板或幻灯片的description包含中文字符,如 "这是中文描述"
|
||||
- **THEN** 系统正确处理,不出现编码错误
|
||||
|
||||
#### Scenario: description包含多行文本
|
||||
|
||||
- **WHEN** description字段使用YAML多行文本格式
|
||||
- **THEN** 系统正确读取完整的描述内容
|
||||
@@ -0,0 +1,46 @@
|
||||
# Tasks: Add Description Field to Metadata, Templates, and Slides
|
||||
|
||||
## 1. 数据模型更新
|
||||
|
||||
- [x] 1.1 在Metadata/Document类中添加description属性(可选,默认为None)
|
||||
- [x] 1.2 在Template类中添加description属性(可选,默认为None)
|
||||
- [x] 1.3 在Slide类中添加description属性(可选,默认为None)
|
||||
- [x] 1.4 更新Metadata/Document类的__init__方法接受description参数
|
||||
- [x] 1.5 更新Template类的__init__方法接受description参数
|
||||
- [x] 1.6 更新Slide类的__init__方法接受description参数
|
||||
|
||||
## 2. YAML解析器更新
|
||||
|
||||
- [x] 2.1 更新metadata解析逻辑,读取YAML中的description字段
|
||||
- [x] 2.2 更新模板解析逻辑,读取模板YAML中的description字段
|
||||
- [x] 2.3 更新幻灯片解析逻辑,读取slides列表中的description字段
|
||||
- [x] 2.4 确保description字段缺失时使用默认值None
|
||||
- [x] 2.5 支持多行YAML格式的description文本
|
||||
|
||||
## 3. 测试用例
|
||||
|
||||
- [x] 3.1 添加测试:metadata包含description字段时正确加载
|
||||
- [x] 3.2 添加测试:metadata不包含description字段时正常工作
|
||||
- [x] 3.3 添加测试:模板包含description字段时正确加载
|
||||
- [x] 3.4 添加测试:模板不包含description字段时正常工作
|
||||
- [x] 3.5 添加测试:幻灯片包含description字段时正确加载
|
||||
- [x] 3.6 添加测试:幻灯片不包含description字段时正常工作
|
||||
- [x] 3.7 添加测试:description包含中文字符时正确处理
|
||||
- [x] 3.8 添加测试:description为空字符串时正常工作
|
||||
- [x] 3.9 添加测试:description不影响渲染输出
|
||||
|
||||
## 4. 文档更新
|
||||
|
||||
- [x] 4.1 更新README.md,说明metadata支持description字段
|
||||
- [x] 4.2 更新README.md,说明模板支持description字段
|
||||
- [x] 4.3 更新README.md,说明幻灯片支持description字段
|
||||
- [x] 4.4 在README.md中添加description字段的使用示例
|
||||
- [x] 4.5 更新README_DEV.md,记录description字段的设计说明
|
||||
- [x] 4.6 确保文档中说明description仅用于文档目的,不影响输出
|
||||
|
||||
## 5. 验证和集成
|
||||
|
||||
- [x] 5.1 运行所有现有测试,确保向后兼容性
|
||||
- [x] 5.2 运行新添加的测试用例,确保全部通过
|
||||
- [x] 5.3 使用包含description的示例YAML进行端到端测试
|
||||
- [x] 5.4 验证生成的PPTX文件不受description字段影响
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-04
|
||||
@@ -0,0 +1,260 @@
|
||||
# 图片适配模式技术设计
|
||||
|
||||
## Context
|
||||
|
||||
### 当前状态
|
||||
|
||||
当前系统图片渲染逻辑非常简单:直接使用 python-pptx 的 `add_picture()` 方法,传入 box 的宽高参数。python-pptx 会自动将图片拉伸到指定尺寸,无法保持宽高比,也无法进行居中或裁剪处理。
|
||||
|
||||
```python
|
||||
# 当前实现 (pptx_renderer.py)
|
||||
def _render_image(self, slide, elem: ImageElement, base_path):
|
||||
x, y, w, h = [Inches(v) for v in elem.box]
|
||||
slide.shapes.add_picture(str(img_path), x, y, width=w, height=h)
|
||||
```
|
||||
|
||||
### 约束条件
|
||||
|
||||
- 必须保持向后兼容:未指定 `fit` 参数时,行为与当前一致
|
||||
- PPTX 渲染使用英寸单位,图片处理需要像素单位
|
||||
- HTML 预览需要与 PPTX 渲染保持一致的视觉效果
|
||||
- DPI 配置需要同时影响两个渲染器
|
||||
|
||||
### 利益相关者
|
||||
|
||||
- 最终用户:需要多样化的图片适配选项
|
||||
- HTML 预览用户:期望预览效果与最终 PPTX 一致
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 支持四种图片适配模式(stretch、contain、cover、center)
|
||||
- 支持留白区域的背景色填充
|
||||
- 支持可配置的 DPI 转换
|
||||
- HTML 预览与 PPTX 渲染效果一致
|
||||
- 向后兼容现有 YAML 文件
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不支持图片的旋转、翻转等变换
|
||||
- 不支持图片滤镜、水印等高级效果
|
||||
- 不支持 fill(平铺)模式
|
||||
- 不支持自适应 box 尺寸(box 必需,不是可选)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1: 使用 Pillow 作为图片处理库
|
||||
|
||||
**选择理由:**
|
||||
|
||||
- Pillow 是 Python 生态中最成熟的图片处理库,从 PIL 发展而来
|
||||
- 内置 `ImageOps` 模块,直接提供 `contain()` 和 `cover()` 方法,与我们的需求完全匹配
|
||||
- API 简洁直观,社区活跃,文档完善
|
||||
- 轻量级,专注图片处理,不像 OpenCV 那样重量级
|
||||
|
||||
**替代方案考虑:**
|
||||
|
||||
| 方案 | 优势 | 劣势 | 结论 |
|
||||
|------|------|------|------|
|
||||
| Pillow | API 简洁,ImageOps 直接匹配需求 | 需要额外依赖 | ✅ 选择 |
|
||||
| OpenCV | 功能强大,性能优异 | API 复杂,BGR 颜色顺序需转换,过度设计 | ❌ |
|
||||
| Wand | 功能丰富 | 依赖 ImageMagick,安装复杂,跨平台问题多 | ❌ |
|
||||
| 原生实现 | 无额外依赖 | 需要手动实现所有算法,容易出错 | ❌ |
|
||||
|
||||
### 决策 2: DPI 作为文档级配置
|
||||
|
||||
**选择:** 在 `metadata.dpi` 中配置,默认值 96
|
||||
|
||||
**理由:**
|
||||
|
||||
- DPI 影响整个文档的所有图片,不应在元素级别配置
|
||||
- 96 是 Web 标准 DPI,与当前 HTML 渲染器一致
|
||||
- 简洁的配置方式,符合 YAML 声明式风格
|
||||
|
||||
**替代方案:** `metadata.image.dpi`(更结构化,但过于复杂)
|
||||
|
||||
### 决策 3: PPTX 和 HTML 使用不同的实现方式
|
||||
|
||||
**PPTX 实现:**
|
||||
|
||||
- 使用 Pillow 处理图片(缩放、裁剪)
|
||||
- 计算居中位置
|
||||
- 添加背景色画布(如有需要)
|
||||
- 使用 python-pptx 添加处理后的图片
|
||||
|
||||
**HTML 实现:**
|
||||
|
||||
- 使用 CSS `object-fit` 属性
|
||||
- `stretch` → `object-fit: fill`
|
||||
- `contain` → `object-fit: contain`
|
||||
- `cover` → `object-fit: cover`
|
||||
- `center` → `object-fit: none` + `object-position: center`
|
||||
- 背景色使用 CSS `background-color`
|
||||
|
||||
**理由:** 两个平台的原生能力不同,使用各自的最佳实践,但确保视觉效果一致
|
||||
|
||||
### 决策 4: 图片适配模式算法设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 图片适配算法流程 │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
输入: img (PIL.Image), box_size (width, height), fit, background
|
||||
|
||||
1. stretch 模式
|
||||
─────────────────────────────────────────────────────────────────
|
||||
直接使用 img.resize(box_size),不考虑宽高比
|
||||
|
||||
2. contain 模式
|
||||
─────────────────────────────────────────────────────────────────
|
||||
result = ImageOps.contain(img, box_size)
|
||||
# result.size <= box_size,保持宽高比
|
||||
if background:
|
||||
canvas = 创建 box_size 画布,填充背景色
|
||||
canvas.paste(result, 居中位置)
|
||||
result = canvas
|
||||
|
||||
3. cover 模式
|
||||
─────────────────────────────────────────────────────────────────
|
||||
result = ImageOps.cover(img, box_size)
|
||||
# result.size == box_size,保持宽高比,裁剪超出部分
|
||||
|
||||
4. center 模式
|
||||
─────────────────────────────────────────────────────────────────
|
||||
result = img # 不缩放
|
||||
if img.width > box_size.width or img.height > box_size.height:
|
||||
result = 裁剪到 box_size(居中裁剪)
|
||||
if background:
|
||||
canvas = 创建 box_size 画布,填充背景色
|
||||
canvas.paste(result, 居中位置)
|
||||
result = canvas
|
||||
|
||||
输出: result (PIL.Image), display_size, display_position
|
||||
```
|
||||
|
||||
### 决策 6: 模块化设计
|
||||
|
||||
```
|
||||
utils/image_utils.py
|
||||
├── inches_to_pixels(inches, dpi) -> float
|
||||
├── pixels_to_inches(pixels, dpi) -> float
|
||||
├── calculate_contain_size(img_size, box_size) -> tuple
|
||||
├── calculate_cover_size(img_size, box_size) -> tuple
|
||||
├── calculate_center_offset(img_size, box_size) -> tuple
|
||||
└── apply_fit_mode(img, box_size, fit, background) -> PIL.Image
|
||||
|
||||
validators/image_config.py (新建)
|
||||
├── validate_fit_value(fit) -> List[ValidationIssue]
|
||||
└── validate_fit_box_dependency(elem) -> List[ValidationIssue]
|
||||
```
|
||||
|
||||
**理由:** 单一职责原则,图片处理逻辑与渲染逻辑分离,便于测试和复用
|
||||
|
||||
### 决策 5: 模块化设计
|
||||
|
||||
```
|
||||
utils/image_utils.py
|
||||
├── inches_to_pixels(inches, dpi) -> float
|
||||
├── pixels_to_inches(pixels, dpi) -> float
|
||||
├── calculate_contain_size(img_size, box_size) -> tuple
|
||||
├── calculate_cover_size(img_size, box_size) -> tuple
|
||||
├── calculate_center_offset(img_size, box_size) -> tuple
|
||||
└── apply_fit_mode(img, box_size, fit, background) -> PIL.Image
|
||||
|
||||
validators/image_config.py (新建)
|
||||
├── validate_fit_value(fit) -> List[ValidationIssue]
|
||||
└── validate_background_color(color) -> List[ValidationIssue]
|
||||
```
|
||||
|
||||
**理由:** 单一职责原则,图片处理逻辑与渲染逻辑分离,便于测试和复用
|
||||
|
||||
**注意:** 由于 box 参数为必填,不存在"没有指定 box"的情况,因此 `validate_fit_box_dependency` 验证器已移除。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险 1: Pillow 依赖增加
|
||||
|
||||
**风险:** 新增外部依赖可能增加安装复杂度
|
||||
|
||||
**缓解措施:**
|
||||
- Pillow 是 Python 生态标准库,安装简单(`pip install pillow`)
|
||||
- 在 pyproject.toml 中不指定版本号,使用最新稳定版
|
||||
- 在 README 中明确说明依赖变更
|
||||
|
||||
### 风险 2: PPTX 和 HTML 渲染效果不完全一致
|
||||
|
||||
**风险:** 两个平台实现方式不同,可能在边缘情况下效果有差异
|
||||
|
||||
**缓解措施:**
|
||||
- 核心算法(尺寸计算)使用相同的 Python 函数
|
||||
- 编写集成测试,对比两种渲染器的输出
|
||||
- 在文档中说明已知差异(如抗锯齿算法不同)
|
||||
|
||||
### 风险 3: 大图片处理性能问题
|
||||
|
||||
**风险:** Pillow 处理大图片可能较慢,占用内存
|
||||
|
||||
**缓解措施:**
|
||||
- 仅在需要时才处理图片(contain/cover/center 模式)
|
||||
- stretch 模式直接使用 python-pptx 的原生处理,不经过 Pillow
|
||||
- 文档中建议用户使用适当尺寸的图片
|
||||
|
||||
### 风险 4: DPI 配置不当导致尺寸错误
|
||||
|
||||
**风险:** 用户设置的 DPI 与实际使用场景不符,导致图片尺寸不符合预期
|
||||
|
||||
**缓解措施:**
|
||||
- 默认值 96 适用于大多数场景
|
||||
- 在 README 中说明 DPI 的含义和影响
|
||||
- 验证器检查 DPI 值是否合理(如 72-300 之间)
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 部署步骤
|
||||
|
||||
1. **代码变更**
|
||||
- 添加 Pillow 依赖到 pyproject.toml
|
||||
- 创建 `utils/image_utils.py`
|
||||
- 创建 `validators/image_config.py`
|
||||
- 更新 `core/elements.py` 的 ImageElement
|
||||
- 更新 `renderers/pptx_renderer.py`
|
||||
- 更新 `renderers/html_renderer.py`
|
||||
|
||||
2. **测试**
|
||||
- 单元测试:image_utils 的各个函数
|
||||
- 单元测试:validators 的图片配置验证
|
||||
- 集成测试:四种 fit 模式的 PPTX 渲染
|
||||
- 集成测试:四种 fit 模式的 HTML 渲染
|
||||
- 端到端测试:完整 YAML 转换流程
|
||||
|
||||
3. **文档更新**
|
||||
- 更新 README.md,添加图片适配模式说明
|
||||
- 更新 README_DEV.md,添加架构说明
|
||||
|
||||
### 回滚策略
|
||||
|
||||
- 如果发现严重问题,可以回退到之前版本
|
||||
- 向后兼容设计确保未指定 fit 参数的 YAML 文件仍能正常工作
|
||||
- 回滚后用户只需删除 fit 和 background 参数即可
|
||||
|
||||
## Open Questions
|
||||
|
||||
### Q1: 是否需要在图片处理失败时提供降级方案?
|
||||
|
||||
**决策:** 图片处理失败时抛出 ERROR 级别错误,让用户修复图片问题,不提供降级方案。
|
||||
|
||||
**理由:** 保持简单明确,图片问题应该由用户在源头解决,而不是掩盖问题。
|
||||
|
||||
### Q2: background 参数是否支持渐变色?
|
||||
|
||||
**决策:** background 参数仅支持纯色,不支持渐变色。
|
||||
|
||||
**理由:** 简化实现,满足绝大多数使用场景。如有后续需求,可以作为独立功能添加。
|
||||
|
||||
### Q3: 是否需要支持图片质量设置?
|
||||
|
||||
**决策:** 使用 Pillow 的最高质量重采样算法(LANCZOS),不向用户暴露配置选项。
|
||||
|
||||
**理由:** 演示文稿场景下图片质量优先于处理速度,使用最高质量算法避免用户困惑。
|
||||
@@ -0,0 +1,44 @@
|
||||
# 图片适配模式支持
|
||||
|
||||
## Why
|
||||
|
||||
当前图片元素渲染仅支持简单的拉伸模式,图片会被强制缩放到 box 指定的尺寸,导致图片变形或宽高比失真。实际使用中,用户需要保持图片宽高比、居中显示、填充裁剪等多种适配模式,以满足不同场景的视觉需求。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增图片 `fit` 参数,支持四种适配模式:`stretch`(拉伸)、`contain`(包含)、`cover`(覆盖)、`center`(居中)
|
||||
- 新增图片 `background` 参数,支持指定留白区域的填充颜色(默认透明)
|
||||
- 新增文档级 `dpi` 配置(`metadata.dpi`),用于像素与英寸的转换,默认值为 96
|
||||
- 引入 Pillow 库进行图片处理,利用其 ImageOps 模块实现各种适配模式
|
||||
- 保持向后兼容:未指定 `fit` 时默认使用 `stretch` 模式(当前行为)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `image-fit-modes`: 图片元素适配模式处理,支持 stretch、contain、cover、center 四种模式,以及背景色填充和 DPI 配置
|
||||
|
||||
### Modified Capabilities
|
||||
- `element-rendering`: 扩展图片元素渲染能力,新增 `fit` 和 `background` 参数支持
|
||||
- `html-rendering`: 同步扩展 HTML 预览的图片渲染能力,支持 `fit` 和 `background` 参数,使用 CSS object-fit 实现
|
||||
|
||||
## Impact
|
||||
|
||||
### 依赖变更
|
||||
- 新增 Pillow 依赖(不指定版本号,使用最新版)
|
||||
|
||||
### 代码变更
|
||||
- `core/elements.py`: ImageElement 新增 `fit` 和 `background` 字段
|
||||
- `renderers/pptx_renderer.py`: 重写 `_render_image()` 方法,集成 Pillow 图片处理
|
||||
- `renderers/html_renderer.py`: 更新 `_render_image()` 方法,使用 CSS object-fit 实现适配模式
|
||||
- `validators/`: 新增图片参数验证器(fit 值校验、background 颜色校验)
|
||||
- `utils/image_utils.py`: 新增图片处理工具模块(像素转换、尺寸计算、居中定位)
|
||||
|
||||
### API 变更
|
||||
- YAML 语法扩展:
|
||||
- `metadata` 层级新增可选的 `dpi` 字段
|
||||
- 图片元素新增可选的 `fit` 字段
|
||||
- 图片元素新增可选的 `background` 字段
|
||||
|
||||
### 文档变更
|
||||
- `README.md`: 新增图片适配模式使用说明和示例
|
||||
- `README_DEV.md`: 新增图片处理架构说明
|
||||
@@ -0,0 +1,76 @@
|
||||
# Element Rendering - Delta Spec
|
||||
|
||||
本 spec 是对 `openspec/specs/element-rendering/spec.md` 的增量修改。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 系统必须支持图片元素渲染
|
||||
|
||||
系统 SHALL 将 YAML 中定义的图片元素渲染为 PPTX 图片对象,支持 `fit` 和 `background` 参数控制图片适配模式。
|
||||
|
||||
#### Scenario: 渲染本地图片
|
||||
|
||||
- **WHEN** 元素定义为 `{type: image, src: "images/logo.png", box: [2, 3, 4, 3]}`
|
||||
- **THEN** 系统从指定路径加载图片,在 (2, 3) 位置渲染为 4×3 英寸大小
|
||||
- **AND** 使用默认的 `stretch` 模式(向后兼容)
|
||||
|
||||
#### Scenario: 使用 fit 模式渲染图片
|
||||
|
||||
- **WHEN** 元素定义为 `{type: image, src: "photo.jpg", box: [1, 1, 4, 3], fit: contain}`
|
||||
- **THEN** 系统使用 `contain` 模式渲染图片
|
||||
- **AND** 保持图片宽高比,完整显示在 box 内
|
||||
|
||||
#### Scenario: 使用背景色渲染图片
|
||||
|
||||
- **WHEN** 元素定义为 `{type: image, src: "photo.png", box: [1, 1, 4, 3], fit: contain, background: "#f0f0f0"}`
|
||||
- **THEN** 系统使用 `contain` 模式渲染图片
|
||||
- **AND** 用 #f0f0f0 颜色填充留白区域
|
||||
|
||||
#### Scenario: 图片文件不存在时报错
|
||||
|
||||
- **WHEN** 图片 src 指向不存在的文件路径
|
||||
- **THEN** 系统抛出错误,明确指出图片文件未找到
|
||||
|
||||
#### Scenario: 图片格式不支持时报错
|
||||
|
||||
- **WHEN** 图片文件格式不被 Pillow 支持
|
||||
- **THEN** 系统抛出错误,提示图片格式不支持,并列出支持的格式
|
||||
|
||||
#### Scenario: 图片处理失败时报错
|
||||
|
||||
- **WHEN** Pillow 处理图片时发生异常(如文件损坏)
|
||||
- **THEN** 系统抛出 ERROR 级别错误,不降级到其他模式
|
||||
|
||||
#### Scenario: 相对路径处理
|
||||
|
||||
- **WHEN** 图片 src 使用相对路径 `"assets/images/logo.png"`
|
||||
- **THEN** 系统基于演示文稿文件所在目录解析相对路径
|
||||
|
||||
#### Scenario: 使用 DPI 配置渲染图片
|
||||
|
||||
- **WHEN** metadata 定义了 `dpi: 120` 且图片需要处理
|
||||
- **THEN** 系统使用该 DPI 值进行像素与英寸的转换
|
||||
|
||||
#### Scenario: fit 参数值无效时报错
|
||||
|
||||
- **WHEN** `fit` 参数值不是 stretch、contain、cover、center 之一
|
||||
- **THEN** 系统抛出 ERROR,并列出有效值
|
||||
|
||||
#### Scenario: background 参数颜色格式无效时报错
|
||||
|
||||
- **WHEN** `background` 值不是有效的颜色格式
|
||||
- **THEN** 系统抛出 ERROR,提示颜色格式应为 #RRGGBB 或 #RGB
|
||||
|
||||
### Requirement: 图片元素的 box 参数必须存在
|
||||
|
||||
系统 SHALL 要求图片元素必须包含 `box` 参数,否则验证失败。
|
||||
|
||||
#### Scenario: 缺少 box 参数时报错
|
||||
|
||||
- **WHEN** 图片元素未定义 `box` 参数
|
||||
- **THEN** 系统抛出 ERROR,提示 box 参数为必需
|
||||
|
||||
#### Scenario: box 参数格式正确
|
||||
|
||||
- **WHEN** 图片元素定义了 `box: [1, 2, 4, 3]`
|
||||
- **THEN** 系统验证通过,将 box 用于图片定位和尺寸
|
||||
@@ -0,0 +1,83 @@
|
||||
# HTML Rendering - Delta Spec
|
||||
|
||||
本 spec 是对 `openspec/specs/html-rendering/spec.md` 的增量修改。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 系统必须渲染图片元素
|
||||
|
||||
系统 SHALL 将 YAML 中的图片元素转换为 HTML `<img>` 标签,支持 `fit` 和 `background` 参数,使用 CSS object-fit 实现适配模式。
|
||||
|
||||
#### Scenario: 渲染本地图片
|
||||
|
||||
- **WHEN** 元素定义为 `{type: image, src: "images/logo.png", box: [2, 3, 4, 3]}`
|
||||
- **THEN** 系统生成 `<img>` 标签,src 为图片的文件路径,位置为 (192px, 288px),尺寸为 384x288 像素
|
||||
- **AND** 使用默认的 CSS 样式 `object-fit: fill`(等同于 stretch 模式)
|
||||
|
||||
#### Scenario: 使用 fit 模式渲染图片
|
||||
|
||||
- **WHEN** 元素定义为 `{type: image, src: "photo.jpg", box: [1, 1, 4, 3], fit: contain}`
|
||||
- **THEN** 系统应用 CSS `object-fit: contain`
|
||||
- **AND** 保持图片宽高比,完整显示在 box 内
|
||||
|
||||
#### Scenario: fit 模式与 CSS object-fit 映射
|
||||
|
||||
- **WHEN** `fit` 参数为 `stretch`
|
||||
- **THEN** 系统应用 CSS `object-fit: fill`
|
||||
|
||||
- **WHEN** `fit` 参数为 `contain`
|
||||
- **THEN** 系统应用 CSS `object-fit: contain`
|
||||
|
||||
- **WHEN** `fit` 参数为 `cover`
|
||||
- **THEN** 系统应用 CSS `object-fit: cover`
|
||||
|
||||
- **WHEN** `fit` 参数为 `center`
|
||||
- **THEN** 系统应用 CSS `object-fit: none` 和 `object-position: center`
|
||||
|
||||
#### Scenario: 使用背景色渲染图片
|
||||
|
||||
- **WHEN** 元素定义为 `{type: image, src: "photo.png", box: [1, 1, 4, 3], fit: contain, background: "#f0f0f0"}`
|
||||
- **THEN** 系统为图片容器添加 CSS `background-color: #f0f0f0`
|
||||
- **AND** 应用 CSS `object-fit: contain`
|
||||
|
||||
#### Scenario: 图片容器支持背景色
|
||||
|
||||
- **WHEN** 图片指定了 `background` 参数
|
||||
- **THEN** 系统创建包装容器,应用背景色到容器
|
||||
- **AND** 图片在容器内使用 object-fit 定位
|
||||
|
||||
#### Scenario: 处理相对路径
|
||||
|
||||
- **WHEN** 图片 src 使用相对路径 `"assets/logo.png"`
|
||||
- **THEN** 系统基于 YAML 文件所在目录解析相对路径
|
||||
|
||||
#### Scenario: 图片不存在时显示占位符
|
||||
|
||||
- **WHEN** 图片文件不存在
|
||||
- **THEN** 系统显示占位符或错误提示,而不是崩溃
|
||||
|
||||
#### Scenario: 使用 DPI 配置渲染图片
|
||||
|
||||
- **WHEN** metadata 定义了 `dpi: 120`
|
||||
- **THEN** 系统使用该 DPI 值进行英寸到像素的转换
|
||||
- **AND** box: [1, 1, 4, 3] 转换为 CSS: left: 120px; top: 120px; width: 480px; height: 360px
|
||||
|
||||
#### Scenario: HTML 渲染与 PPTX 渲染效果一致
|
||||
|
||||
- **WHEN** 同一图片元素使用相同的 fit 和 background 参数
|
||||
- **THEN** HTML 预览和 PPTX 输出的视觉效果应保持一致
|
||||
- **AND** 图片位置、尺寸、适配方式相同
|
||||
|
||||
### Requirement: 图片元素的 box 参数必须存在
|
||||
|
||||
系统 SHALL 要求图片元素必须包含 `box` 参数,否则验证失败。
|
||||
|
||||
#### Scenario: 缺少 box 参数时报错
|
||||
|
||||
- **WHEN** 图片元素未定义 `box` 参数
|
||||
- **THEN** 系统抛出 ERROR,提示 box 参数为必需
|
||||
|
||||
#### Scenario: box 参数转换为像素
|
||||
|
||||
- **WHEN** 图片元素定义了 `box: [1, 2, 4, 3]` 且 DPI 为 96
|
||||
- **THEN** 系统转换为 CSS:left: 96px; top: 192px; width: 384px; height: 288px
|
||||
@@ -0,0 +1,201 @@
|
||||
# Image Fit Modes
|
||||
|
||||
## Purpose
|
||||
|
||||
图片适配模式能力为图片元素提供多种适配策略,允许用户控制图片在指定区域内的显示方式,包括拉伸、保持比例、填充裁剪和居中显示。同时支持 DPI 配置和背景色填充,满足不同场景的视觉需求。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 系统必须支持图片 fit 参数
|
||||
|
||||
系统 SHALL 支持图片元素的 `fit` 参数,允许用户指定图片适配模式。
|
||||
|
||||
#### Scenario: fit 参数支持四种模式
|
||||
|
||||
- **WHEN** 图片元素定义了 `fit` 参数
|
||||
- **THEN** 系统支持 `stretch`、`contain`、`cover`、`center` 四种模式
|
||||
|
||||
#### Scenario: fit 参数默认值为 stretch
|
||||
|
||||
- **WHEN** 图片元素未指定 `fit` 参数
|
||||
- **THEN** 系统使用 `stretch` 模式(向后兼容)
|
||||
|
||||
#### Scenario: fit 参数值无效时报错
|
||||
|
||||
- **WHEN** `fit` 参数值不是四种有效模式之一
|
||||
- **THEN** 系统抛出 ERROR,并列出有效值(stretch、contain、cover、center)
|
||||
|
||||
### Requirement: 系统必须支持 stretch 模式
|
||||
|
||||
系统 SHALL 在 `stretch` 模式下将图片强制缩放到 box 指定的尺寸,不考虑宽高比。
|
||||
|
||||
#### Scenario: stretch 模式拉伸图片
|
||||
|
||||
- **WHEN** 图片元素定义了 `fit: stretch` 或未指定 `fit`
|
||||
- **THEN** 系统将图片拉伸到 box 的宽高尺寸
|
||||
- **AND** 图片可能变形
|
||||
|
||||
#### Scenario: stretch 模式不考虑背景色
|
||||
|
||||
- **WHEN** 图片使用 `fit: stretch` 并指定了 `background`
|
||||
- **THEN** 背景色参数被忽略(无留白区域)
|
||||
|
||||
### Requirement: 系统必须支持 contain 模式
|
||||
|
||||
系统 SHALL 在 `contain` 模式下保持图片宽高比,完整显示图片在 box 内,可能有留白。
|
||||
|
||||
#### Scenario: contain 模式保持宽高比
|
||||
|
||||
- **WHEN** 图片元素定义了 `fit: contain`
|
||||
- **THEN** 系统缩放图片使其完整显示在 box 内
|
||||
- **AND** 保持原始宽高比
|
||||
- **AND** 图片尺寸不超过 box 尺寸
|
||||
|
||||
#### Scenario: contain 模式图片居中
|
||||
|
||||
- **WHEN** 图片使用 `fit: contain` 且小于 box 尺寸
|
||||
- **THEN** 系统将图片居中显示在 box 内
|
||||
|
||||
#### Scenario: contain 模式支持背景色
|
||||
|
||||
- **WHEN** 图片使用 `fit: contain` 并指定了 `background`
|
||||
- **THEN** 系统用指定颜色填充 box 内的留白区域
|
||||
|
||||
#### Scenario: contain 模式图片比 box 大
|
||||
|
||||
- **WHEN** 图片原始尺寸大于 box 尺寸
|
||||
- **THEN** 系统等比缩小图片使其完整显示在 box 内
|
||||
|
||||
#### Scenario: contain 模式图片比 box 小
|
||||
|
||||
- **WHEN** 图片原始尺寸小于 box 尺寸
|
||||
- **THEN** 系统保持原始尺寸,居中显示在 box 内
|
||||
|
||||
### Requirement: 系统必须支持 cover 模式
|
||||
|
||||
系统 SHALL 在 `cover` 模式下保持图片宽高比,填充整个 box,裁剪超出部分。
|
||||
|
||||
#### Scenario: cover 模式保持宽高比
|
||||
|
||||
- **WHEN** 图片元素定义了 `fit: cover`
|
||||
- **THEN** 系统缩放图片使其填满 box
|
||||
- **AND** 保持原始宽高比
|
||||
- **AND** 裁剪超出 box 的部分
|
||||
|
||||
#### Scenario: cover 模式图片居中裁剪
|
||||
|
||||
- **WHEN** 图片使用 `fit: cover` 且需要裁剪
|
||||
- **THEN** 系统从图片中心进行裁剪
|
||||
|
||||
#### Scenario: cover 模式不考虑背景色
|
||||
|
||||
- **WHEN** 图片使用 `fit: cover` 并指定了 `background`
|
||||
- **THEN** 背景色参数被忽略(无留白区域)
|
||||
|
||||
#### Scenario: cover 模式图片比 box 大
|
||||
|
||||
- **WHEN** 图片原始尺寸大于 box 尺寸
|
||||
- **THEN** 系统等比缩小图片并裁剪超出部分
|
||||
|
||||
#### Scenario: cover 模式图片比 box 小
|
||||
|
||||
- **WHEN** 图片原始尺寸小于 box 尺寸
|
||||
- **THEN** 系统等比放大图片并裁剪超出部分
|
||||
|
||||
### Requirement: 系统必须支持 center 模式
|
||||
|
||||
系统 SHALL 在 `center` 模式下按原始尺寸居中显示图片,不缩放,超出 box 的部分被裁剪。
|
||||
|
||||
#### Scenario: center 模式不缩放图片
|
||||
|
||||
- **WHEN** 图片元素定义了 `fit: center`
|
||||
- **THEN** 系统保持图片原始尺寸,不进行缩放
|
||||
|
||||
#### Scenario: center 模式图片居中
|
||||
|
||||
- **WHEN** 图片使用 `fit: center`
|
||||
- **THEN** 系统将图片居中显示在 box 内
|
||||
|
||||
#### Scenario: center 模式裁剪超出部分
|
||||
|
||||
- **WHEN** 图片原始尺寸大于 box 尺寸
|
||||
- **THEN** 系统裁剪超出 box 的部分(从中心裁剪)
|
||||
|
||||
#### Scenario: center 模式支持背景色
|
||||
|
||||
- **WHEN** 图片使用 `fit: center` 并指定了 `background`
|
||||
- **THEN** 系统用指定颜色填充 box 内的留白区域
|
||||
|
||||
### Requirement: 系统必须支持 background 参数
|
||||
|
||||
系统 SHALL 支持图片元素的 `background` 参数,允许用户指定留白区域的填充颜色。
|
||||
|
||||
#### Scenario: background 参数默认透明
|
||||
|
||||
- **WHEN** 图片元素未指定 `background` 参数
|
||||
- **THEN** 留白区域保持透明
|
||||
|
||||
#### Scenario: background 参数支持纯色
|
||||
|
||||
- **WHEN** 图片元素指定了 `background: "#ff0000"`
|
||||
- **THEN** 系统使用指定颜色填充留白区域
|
||||
|
||||
#### Scenario: background 参数不支持渐变
|
||||
|
||||
- **WHEN** 图片元素指定了渐变色(如 `"linear-gradient(...)"`)
|
||||
- **THEN** 系统抛出 ERROR,提示仅支持纯色
|
||||
|
||||
#### Scenario: background 参数颜色格式验证
|
||||
|
||||
- **WHEN** `background` 值不是有效的颜色格式
|
||||
- **THEN** 系统抛出 ERROR,提示颜色格式应为 #RRGGBB 或 #RGB
|
||||
|
||||
### Requirement: 系统必须支持文档级 DPI 配置
|
||||
|
||||
系统 SHALL 支持在 `metadata.dpi` 中配置 DPI 值,用于像素与英寸的转换。
|
||||
|
||||
#### Scenario: DPI 默认值为 96
|
||||
|
||||
- **WHEN** metadata 未指定 `dpi` 参数
|
||||
- **THEN** 系统使用默认值 96
|
||||
|
||||
#### Scenario: DPI 配置影响所有图片
|
||||
|
||||
- **WHEN** metadata 指定了 `dpi: 120`
|
||||
- **THEN** 系统使用该值进行所有图片的像素与英寸转换
|
||||
|
||||
#### Scenario: DPI 值验证
|
||||
|
||||
- **WHEN** `dpi` 值超出合理范围(如小于 72 或大于 300)
|
||||
- **THEN** 系统发出 WARNING,提示 DPI 值可能不合适
|
||||
|
||||
### Requirement: 系统必须在图片处理失败时抛出错误
|
||||
|
||||
系统 SHALL 在图片处理失败时抛出 ERROR 级别错误,不提供降级方案。
|
||||
|
||||
#### Scenario: 损坏的图片文件
|
||||
|
||||
- **WHEN** Pillow 无法读取图片文件(文件损坏或格式不支持)
|
||||
- **THEN** 系统抛出 ERROR,明确指出图片文件问题
|
||||
- **AND** 不降级到其他模式
|
||||
|
||||
#### Scenario: 图片处理异常
|
||||
|
||||
- **WHEN** Pillow 处理图片时发生异常(如内存不足)
|
||||
- **THEN** 系统抛出 ERROR,包含异常信息
|
||||
- **AND** 不降级到其他模式
|
||||
|
||||
### Requirement: 系统必须使用最高质量的图片处理算法
|
||||
|
||||
系统 SHALL 使用 Pillow 的最高质量重采样算法(LANCZOS)进行图片缩放。
|
||||
|
||||
#### Scenario: 图片缩放使用 LANCZOS
|
||||
|
||||
- **WHEN** 系统需要缩放图片(contain、cover 模式)
|
||||
- **THEN** 使用 Pillow 的 LANCZOS 重采样算法
|
||||
- **AND** 不向用户暴露质量配置选项
|
||||
|
||||
#### Scenario: 图片裁剪保持质量
|
||||
|
||||
- **WHEN** 系统需要裁剪图片(cover、center 模式)
|
||||
- **THEN** 裁剪操作不损失图片质量
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user