1
0

feat: add YAML validation with check command and auto-validation

Implements comprehensive validation before PPTX conversion to catch errors early. Includes element-level validation (colors, fonts, table consistency) and system-level validation (geometry, resources). Supports standalone check command and automatic validation during conversion.
This commit is contained in:
2026-03-02 18:14:45 +08:00
parent d598de27b3
commit 83ff827ad1
16 changed files with 1742 additions and 95 deletions

View File

@@ -5,6 +5,7 @@
## ✨ 功能特性
- 📝 **YAML 声明式语法** - 使用简单易读的 YAML 定义演示文稿
-**智能验证** - 转换前自动检查 YAML 文件,提前发现问题
- 🎨 **模板系统** - 支持参数化模板,复用幻灯片布局
- 🧩 **丰富的元素类型** - 文本、图片、形状、表格
- 👁️ **实时预览** - 浏览器预览模式,支持热重载
@@ -20,28 +21,83 @@
### 基本用法
```bash
# 生成 PPTX 文件(自动命名为 input.pptx
uv run yaml2pptx.py presentation.yaml
# 使用 convert 子命令(推荐
uv run yaml2pptx.py convert presentation.yaml output.pptx
# 指定输出文件名
uv run yaml2pptx.py presentation.yaml output.pptx
# 自动生成输出文件名
uv run yaml2pptx.py convert presentation.yaml
# 使用模板
uv run yaml2pptx.py presentation.yaml output.pptx --template-dir ./templates
uv run yaml2pptx.py convert presentation.yaml output.pptx --template-dir ./templates
# 传统用法(向后兼容)
uv run yaml2pptx.py presentation.yaml output.pptx
```
### 实时预览
```bash
# 启动预览服务器(自动打开浏览器)
uv run yaml2pptx.py presentation.yaml --preview
uv run yaml2pptx.py convert presentation.yaml --preview
# 指定端口
uv run yaml2pptx.py presentation.yaml --preview --port 8080
uv run yaml2pptx.py convert presentation.yaml --preview --port 8080
# 传统用法(向后兼容)
uv run yaml2pptx.py presentation.yaml --preview
```
预览模式会自动监听文件变化,修改 YAML 文件后浏览器会自动刷新。
### 验证功能
在转换前验证 YAML 文件,提前发现问题:
```bash
# 独立验证命令
uv run yaml2pptx.py check presentation.yaml
# 使用模板时验证
uv run yaml2pptx.py check presentation.yaml --template-dir ./templates
```
验证功能会检查:
- ✅ YAML 语法和结构
- ✅ 元素是否超出页面范围
- ✅ 图片和模板文件是否存在
- ✅ 颜色格式是否正确
- ✅ 字体大小是否合理
- ✅ 表格数据是否一致
**自动验证**:转换时默认会自动验证,如果发现错误会终止转换。可以使用 `--no-check` 跳过验证:
```bash
# 跳过自动验证convert 子命令)
uv run yaml2pptx.py convert presentation.yaml --no-check
# 跳过自动验证(传统用法)
uv run yaml2pptx.py presentation.yaml --no-check
```
**验证结果示例**
```
🔍 正在检查 YAML 文件...
❌ 错误 (2):
[幻灯片 2, 元素 1] 无效的颜色格式: red (应为 #RRGGBB)
[幻灯片 3, 元素 2] 图片文件不存在: logo.png
⚠️ 警告 (1):
[幻灯片 1, 元素 1] 元素右边界超出: 10.50 > 10
检查完成: 发现 2 个错误, 1 个警告
```
- **ERROR**:阻止转换的严重问题(文件不存在、语法错误等)
- **WARNING**:影响视觉效果的问题(元素超出页面、字体太小等)
- **INFO**:优化建议
## 📖 YAML 语法
### 最小示例

View File

@@ -12,14 +12,20 @@ yaml2pptx 是一个将 YAML 格式的演示文稿源文件转换为 PPTX 文件
```
html2pptx/
├── yaml2pptx.py (127 行) # 入口脚本CLI + main 函数
├── yaml2pptx.py (200 行) # 入口脚本CLI + main 函数
├── utils.py (74 行) # 工具函数(日志、颜色转换)
├── core/ # 核心领域模型
│ ├── elements.py (96 行) # 元素抽象层dataclass
│ ├── 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 渲染器(预览)
@@ -34,6 +40,12 @@ yaml2pptx.py (入口)
├─→ utils (工具函数)
├─→ loaders.yaml_loader (YAML 加载)
├─→ validators.validator (验证器)
│ ↓
│ ├─→ validators.result (验证结果)
│ ├─→ validators.geometry (几何验证)
│ ├─→ validators.resource (资源验证)
│ └─→ core.elements (元素验证)
├─→ core.presentation (演示文稿)
│ ↓
│ ├─→ core.template (模板)
@@ -49,9 +61,10 @@ yaml2pptx.py (入口)
```
**依赖原则**
- 单向依赖:入口 → 渲染 → 核心 ← 加载
- 单向依赖:入口 → 验证/渲染 → 核心 ← 加载
- 无循环依赖
- 核心层不依赖其他业务模块
- 验证层可以调用核心层的元素验证方法
## 模块职责
@@ -81,15 +94,37 @@ yaml2pptx.py (入口)
### 4. core/elements.py核心层 - 元素抽象)
- **职责**:定义元素数据类和工厂函数
- **包含**
- `TextElement` - 文本元素dataclass
- `ImageElement` - 图片元素dataclass
- `ShapeElement` - 形状元素dataclass
- `TableElement` - 表格元素dataclass
- `_is_valid_color()` - 颜色格式验证工具函数
- `TextElement` - 文本元素dataclass + validate
- `ImageElement` - 图片元素dataclass + validate
- `ShapeElement` - 形状元素dataclass + validate
- `TableElement` - 表格元素dataclass + validate
- `create_element()` - 元素工厂函数
- **特点**
- 使用 `@dataclass` 装饰器
-`__post_init__` 中进行验证
- 创建时验证,尽早发现错误
-`__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核心层 - 模板)
- **职责**:模板加载和变量解析
@@ -256,6 +291,72 @@ class PptxGenerator:
- 类型安全:进行类型检查
- 易于扩展:添加新元素类型只需添加一个分支
### 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
```
## 扩展指南
### 添加新元素类型
@@ -351,17 +452,29 @@ def main():
### 运行测试
```bash
# 基本 PPTX 生成
# 验证 YAML 文件
uv run yaml2pptx.py check temp/test.yaml
# 使用模板时验证
uv run yaml2pptx.py check temp/demo.yaml --template-dir temp/templates
# 使用 convert 子命令转换(推荐)
uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx
# 传统模式转换(向后兼容)
uv run yaml2pptx.py temp/test.yaml temp/output.pptx
# 跳过自动验证
uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --no-check
# 使用模板
uv run yaml2pptx.py temp/demo.yaml temp/output.pptx --template-dir temp/templates
uv run yaml2pptx.py convert temp/demo.yaml temp/output.pptx --template-dir temp/templates
# 预览模式
uv run yaml2pptx.py temp/test.yaml --preview
uv run yaml2pptx.py convert temp/test.yaml --preview
# 指定端口
uv run yaml2pptx.py temp/test.yaml --preview --port 8080
uv run yaml2pptx.py convert temp/test.yaml --preview --port 8080
```
### 测试文件位置

View File

@@ -5,7 +5,22 @@
"""
from dataclasses import dataclass, field
from typing import Optional
from typing import Optional, List
import re
def _is_valid_color(color: str) -> bool:
"""
验证颜色格式是否正确
Args:
color: 颜色字符串
Returns:
是否为有效的颜色格式(#RGB 或 #RRGGBB
"""
pattern = r'^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$'
return bool(re.match(pattern, color))
@dataclass
@@ -21,6 +36,41 @@ class TextElement:
if not isinstance(self.box, list) or len(self.box) != 4:
raise ValueError("box 必须是包含 4 个数字的列表")
def validate(self) -> List:
"""验证元素自身的完整性"""
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"
))
# 检查字体大小
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
@dataclass
class ImageElement:
@@ -36,6 +86,12 @@ class ImageElement:
if not isinstance(self.box, list) or len(self.box) != 4:
raise ValueError("box 必须是包含 4 个数字的列表")
def validate(self) -> List:
"""验证元素自身的完整性"""
# ImageElement 的必需字段已在 __post_init__ 中检查
# 这里返回空列表,资源验证由 ResourceValidator 负责
return []
@dataclass
class ShapeElement:
@@ -51,6 +107,42 @@ class ShapeElement:
if not isinstance(self.box, list) or len(self.box) != 4:
raise ValueError("box 必须是包含 4 个数字的列表")
def validate(self) -> List:
"""验证元素自身的完整性"""
from validators.result import ValidationIssue
issues = []
# 检查形状类型枚举
valid_shapes = ['rectangle', 'ellipse', 'rounded_rectangle']
if self.shape not in valid_shapes:
issues.append(ValidationIssue(
level="ERROR",
message=f"不支持的形状类型: {self.shape} (支持: {', '.join(valid_shapes)})",
location="",
code="INVALID_SHAPE_TYPE"
))
# 检查填充颜色格式
if self.fill and not _is_valid_color(self.fill):
issues.append(ValidationIssue(
level="ERROR",
message=f"无效的填充颜色格式: {self.fill} (应为 #RRGGBB)",
location="",
code="INVALID_COLOR_FORMAT"
))
# 检查线条颜色格式
if self.line and self.line.get('color'):
if not _is_valid_color(self.line['color']):
issues.append(ValidationIssue(
level="ERROR",
message=f"无效的线条颜色格式: {self.line['color']} (应为 #RRGGBB)",
location="",
code="INVALID_COLOR_FORMAT"
))
return issues
@dataclass
class TableElement:
@@ -68,6 +160,37 @@ class TableElement:
if not isinstance(self.position, list) or len(self.position) != 2:
raise ValueError("position 必须是包含 2 个数字的列表")
def validate(self) -> List:
"""验证元素自身的完整性"""
from validators.result import ValidationIssue
issues = []
# 检查表格数据行列数一致性
if self.data:
first_row_cols = len(self.data[0]) if isinstance(self.data[0], list) else 0
for i, row in enumerate(self.data[1:], start=1):
if isinstance(row, list):
if len(row) != first_row_cols:
issues.append(ValidationIssue(
level="ERROR",
message=f"表格数据行列数不一致: 第 {i+1} 行有 {len(row)} 列,期望 {first_row_cols}",
location="",
code="TABLE_INCONSISTENT_COLUMNS"
))
# 检查 col_widths 与列数是否匹配
if self.col_widths and self.data:
first_row_cols = len(self.data[0]) if isinstance(self.data[0], list) else 0
if len(self.col_widths) != first_row_cols:
issues.append(ValidationIssue(
level="WARNING",
message=f"col_widths 数量 ({len(self.col_widths)}) 与表格列数 ({first_row_cols}) 不匹配",
location="",
code="TABLE_COL_WIDTHS_MISMATCH"
))
return issues
def create_element(elem_dict: dict):
"""

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-02

View File

@@ -0,0 +1,284 @@
## Context
当前 yaml2pptx 系统已经有基本的 YAML 加载和元素验证功能:
- `loaders/yaml_loader.py` 提供基本的 YAML 语法检查和结构验证
- `core/elements.py` 定义了元素数据类,在 `__post_init__` 中进行基本验证
但现有验证不够全面,用户经常在转换后才发现问题(元素超出页面、文件不存在等),需要反复修改和转换。
项目约束:
- 使用 uv 运行 Python 脚本
- 无新增外部依赖
- 测试文件放在 temp 目录
- 面向中文开发者
## Goals / Non-Goals
**Goals:**
- 提供全面的 YAML 验证功能,在转换前发现问题
- 支持独立 check 命令和转换前自动检查
- 实现分级错误报告ERROR/WARNING/INFO
- 保持验证逻辑的可扩展性和可维护性
**Non-Goals:**
- 不实现自动修复功能(只检测,不修改)
- 不支持第三阶段的质量警告(元素重叠、文本溢出等)
- 不添加 GUI 或 Web 界面
- 不实现配置文件来自定义验证规则
## Decisions
### 决策 1验证职责分层
**决策**:将验证逻辑分为两层:
1. **元素级验证**:放在元素类本身(`core/elements.py`
2. **系统级验证**:放在独立的验证器模块(`validators/`
**理由**
- 元素类最了解自己的约束,应该负责自身的完整性验证
- 系统级验证需要全局上下文(如页面尺寸、文件路径),适合集中处理
- 符合单一职责原则,便于扩展和维护
**元素级验证职责**
- 必需字段检查(如 image 必须有 src
- 数据类型检查(如 box 必须是 4 个数字)
- 值的有效性检查(如颜色格式、枚举值)
- 元素内部逻辑一致性
**系统级验证职责**
- 几何验证(元素是否在页面范围内,需要知道页面尺寸)
- 资源验证(文件是否存在,需要知道文件路径)
- 跨元素验证(如果未来需要)
**替代方案**
- 方案 A所有验证集中在验证器中 → 验证器会变得臃肿,元素类失去封装性
- 方案 B所有验证都在元素类中 → 元素类需要知道全局上下文,违反依赖倒置原则
### 决策 2验证器模块结构
**决策**:创建 `validators/` 目录,包含以下模块:
```
validators/
├── __init__.py # 导出主验证器
├── validator.py # 主验证器,协调各子验证器
├── geometry.py # 几何验证器
├── resource.py # 资源验证器
└── result.py # 验证结果数据结构
```
**理由**
- 模块化设计,每个验证器职责单一
- 便于测试和扩展
- 主验证器作为门面,简化调用
**验证流程**
```
Validator.validate(yaml_path, template_dir)
1. 加载 YAML复用 yaml_loader
2. 元素级验证(调用元素类的验证方法)
3. 几何验证GeometryValidator
4. 资源验证ResourceValidator
返回 ValidationResult
```
**替代方案**
- 方案 A单一验证器文件 → 代码过长,难以维护
- 方案 B每种验证一个独立包 → 过度设计,增加复杂度
### 决策 3验证结果数据结构
**决策**:定义 `ValidationResult``ValidationIssue` 类:
```python
@dataclass
class ValidationIssue:
level: str # "ERROR" | "WARNING" | "INFO"
message: str
location: str # "幻灯片 2, 元素 3"
code: str # "ELEMENT_OUT_OF_BOUNDS"
@dataclass
class ValidationResult:
valid: bool # 是否有 ERROR
errors: List[ValidationIssue]
warnings: List[ValidationIssue]
infos: List[ValidationIssue]
def has_errors(self) -> bool
def format_output(self) -> str # 格式化为命令行输出
```
**理由**
- 结构化的结果便于测试和扩展
- `code` 字段便于未来实现错误码查询或国际化
- `format_output()` 方法封装输出格式,便于修改
**替代方案**
- 方案 A直接返回字符串 → 难以测试,不便于扩展
- 方案 B返回字典 → 缺少类型安全,容易出错
### 决策 4命令行接口设计
**决策**:修改 `yaml2pptx.py`,使用子命令模式:
```python
# 独立验证
yaml2pptx.py check input.yaml [--template-dir DIR]
# 转换(默认自动验证)
yaml2pptx.py input.yaml [output.pptx] [--template-dir DIR] [--no-check]
```
**实现方式**
- 使用 `argparse``subparsers` 实现子命令
- `check` 子命令调用验证器并输出结果
- 转换命令在加载 YAML 后、渲染前调用验证器
- 如果验证失败(有 ERROR终止转换并返回非零退出码
**理由**
- 子命令模式清晰,符合 CLI 工具惯例
- 默认启用自动验证,提升用户体验
- `--no-check` 提供灵活性,适合调试场景
**替代方案**
- 方案 A独立的 `yaml2pptx-check` 命令 → 增加维护成本,用户需要记住两个命令
- 方案 B只有自动验证没有独立命令 → 用户无法单独验证
### 决策 5元素类验证方法增强
**决策**:在 `core/elements.py` 中为每个元素类添加 `validate()` 方法:
```python
@dataclass
class TextElement:
# ... 现有字段 ...
def validate(self) -> List[ValidationIssue]:
"""验证元素自身的完整性"""
issues = []
# 检查颜色格式
if self.font.get('color'):
if not self._is_valid_color(self.font['color']):
issues.append(ValidationIssue(
level="ERROR",
message=f"无效的颜色格式: {self.font['color']}",
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"
))
return issues
```
**理由**
- 元素类封装自己的验证逻辑
- `__post_init__` 保留用于阻止创建无效对象(如 box 不是 4 个数字)
- `validate()` 用于检测可以创建但不推荐的情况(如字体太小)
**替代方案**
- 方案 A只在 `__post_init__` 中验证 → 无法区分致命错误和警告
- 方案 B不修改元素类所有验证在验证器中 → 违反封装原则
### 决策 6边界检查容忍度
**决策**:几何验证时,允许 0.1 英寸的容忍度:
```python
TOLERANCE = 0.1 # 英寸
if right > slide_width + TOLERANCE:
# 报告 WARNING
```
**理由**
- 浮点数计算可能有精度误差
- 0.1 英寸(约 2.54mm)在视觉上几乎不可见
- 避免误报,提升用户体验
**替代方案**
- 方案 A零容忍 → 可能产生大量误报
- 方案 B更大的容忍度如 0.5 英寸)→ 可能漏掉真正的问题
## Risks / Trade-offs
### 风险 1验证性能影响
**风险**:验证可能增加转换时间,特别是资源验证(检查文件存在性)。
**缓解措施**
- 验证是可选的(`--no-check` 跳过)
- 资源验证只检查文件存在性,不读取文件内容
- 未来可以考虑并行验证(如果性能成为问题)
### 风险 2元素类验证方法的维护成本
**风险**:每个元素类都需要实现 `validate()` 方法,增加维护成本。
**缓解措施**
- 提供基类或工具函数来复用常见验证逻辑(如颜色格式检查)
- 验证逻辑相对稳定,不会频繁修改
- 好处是验证逻辑和元素定义在一起,便于理解和修改
### 风险 3错误消息的可读性
**风险**:错误消息可能不够清晰,用户难以理解如何修复。
**缓解措施**
- 错误消息包含具体的位置信息(幻灯片、元素)
- 错误消息包含期望值和实际值(如 "right=10.5 > 10.0"
- 未来可以添加错误码和文档链接
### Trade-off验证完整性 vs 性能
**选择**:优先保证验证完整性,性能其次。
**理由**
- 验证是可选的,性能敏感的场景可以跳过
- 提前发现问题比快速转换更重要
- 当前验证项不多,性能影响可控
## Migration Plan
**部署步骤**
1. 实现验证器模块(不影响现有功能)
2. 增强元素类的验证方法(向后兼容)
3. 修改 `yaml2pptx.py` 添加 check 子命令
4. 添加自动验证逻辑(默认启用)
5. 编写测试用例(在 temp 目录)
6. 更新 README.md 文档
**回滚策略**
- 如果验证器有问题,用户可以使用 `--no-check` 跳过
- 验证器是独立模块,可以快速禁用或修复
- 不影响现有的转换功能
**兼容性**
- 完全向后兼容,现有 YAML 文件和命令行用法不受影响
- 新增的 check 子命令和 --no-check 选项是可选的
## Open Questions
1. **是否需要配置文件来自定义验证规则?**
- 当前决策:不需要,保持简单
- 未来可以考虑添加 `.yaml2pptx.yaml` 配置文件
2. **是否需要支持 JSON 格式的验证结果输出?**
- 当前决策:只支持命令行文本输出
- 未来可以添加 `--format json` 选项
3. **字体大小的合理范围是否需要可配置?**
- 当前决策:硬编码 8pt-100pt
- 未来可以考虑通过配置文件自定义

View File

@@ -0,0 +1,40 @@
## Why
用户在编写 YAML 演示文稿源文件时,经常遇到元素超出页面范围、图片文件路径错误、语法问题等错误,这些问题只有在转换成 PPTX 后打开查看才能发现,导致需要反复修改和转换,效率低下。需要提供一个验证功能,在转换前检查 YAML 文件的各种潜在问题,让用户能够提前发现并修复错误。
## What Changes
- 添加 YAML 文件验证功能,支持多层次的检查
- 结构验证:必需字段、数据类型、枚举值
- 几何验证:元素边界、页面范围检查
- 资源验证:图片文件、模板文件存在性
- 样式验证:颜色格式、字体大小合理性
- 添加独立的 `check` 子命令:`yaml2pptx.py check input.yaml`
- 添加转换前自动检查选项:默认启用,可通过 `--no-check` 跳过
- 实现分级的错误报告系统
- ERROR阻止转换的严重问题文件不存在、语法错误、关键参数缺失
- WARNING影响视觉效果的问题元素超出页面、字体太小
- INFO优化建议
- 输出清晰的命令行报告,显示问题位置(幻灯片、元素)和具体描述
## Capabilities
### New Capabilities
- `yaml-validation`: YAML 演示文稿文件的验证能力,包括结构验证、几何验证、资源验证和样式验证
### Modified Capabilities
无。这是新增功能,不修改现有能力的需求。
## Impact
- **主程序**:修改 `yaml2pptx.py`,添加 `check` 子命令和 `--no-check` 选项
- **新增模块**:创建验证器模块(建议在 `validators/` 目录下)
- 结构验证器
- 几何验证器
- 资源验证器
- 样式验证器
- **现有功能**:不影响现有的转换功能,验证是可选的
- **依赖**:无新增外部依赖,使用现有的 PyYAML 和标准库
- **用户体验**:提升开发效率,减少调试时间

View File

@@ -0,0 +1,187 @@
## ADDED Requirements
### Requirement: 独立验证命令
系统必须提供独立的 check 子命令,用于验证 YAML 文件而不执行转换。
#### Scenario: 执行独立验证
- **WHEN** 用户运行 `yaml2pptx.py check input.yaml`
- **THEN** 系统验证 YAML 文件并输出验证结果,不生成 PPTX 文件
#### Scenario: 验证时指定模板目录
- **WHEN** 用户运行 `yaml2pptx.py check input.yaml --template-dir ./templates`
- **THEN** 系统在验证时使用指定的模板目录来检查模板文件
### Requirement: 转换前自动验证
系统必须在转换 YAML 为 PPTX 之前自动执行验证,除非用户明确跳过。
#### Scenario: 默认自动验证
- **WHEN** 用户运行 `yaml2pptx.py input.yaml output.pptx`
- **THEN** 系统在转换前自动验证 YAML 文件
#### Scenario: 跳过自动验证
- **WHEN** 用户运行 `yaml2pptx.py input.yaml output.pptx --no-check`
- **THEN** 系统跳过验证,直接执行转换
#### Scenario: 验证失败阻止转换
- **WHEN** 自动验证发现 ERROR 级别的问题
- **THEN** 系统输出错误信息并终止转换,返回非零退出码
### Requirement: 元素自验证
系统中的每个元素类型必须能够验证自身的完整性和有效性。
#### Scenario: 元素验证自身属性
- **WHEN** 创建或验证元素对象
- **THEN** 元素类检查自身的必需字段、数据类型、值有效性(如颜色格式、枚举值、字体大小)
#### Scenario: 元素验证与系统验证分离
- **WHEN** 执行验证
- **THEN** 元素类负责自身属性验证(不需要全局上下文的验证),系统验证器负责需要全局上下文的验证(如几何验证需要页面尺寸、资源验证需要文件路径)
### Requirement: 结构验证
系统必须验证 YAML 文件的结构完整性和数据类型正确性。
#### Scenario: 检测缺少必需字段
- **WHEN** YAML 文件缺少 slides 字段
- **THEN** 系统报告 ERROR 级别错误:"缺少必需字段 'slides'"
#### Scenario: 检测数据类型错误
- **WHEN** slides 字段不是列表类型
- **THEN** 系统报告 ERROR 级别错误:"'slides' 必须是一个列表"
#### Scenario: 检测元素类型无效
- **WHEN** 元素的 type 字段值不是 text/image/shape/table 之一
- **THEN** 系统报告 ERROR 级别错误:"不支持的元素类型: {type}"
#### Scenario: 检测无效的枚举值
- **WHEN** shape 元素的 shape 字段值不是 rectangle/ellipse/rounded_rectangle 之一
- **THEN** 系统报告 ERROR 级别错误:"不支持的形状类型: {shape}"
### Requirement: 几何验证
系统必须验证元素的位置和尺寸是否在页面范围内。
#### Scenario: 检测元素超出页面右边界
- **WHEN** 元素的 left + width 超出页面宽度 0.1 英寸以上
- **THEN** 系统报告 WARNING 级别警告:"元素右边界超出: {right} > {width}"
#### Scenario: 检测元素超出页面下边界
- **WHEN** 元素的 top + height 超出页面高度 0.1 英寸以上
- **THEN** 系统报告 WARNING 级别警告:"元素下边界超出: {bottom} > {height}"
#### Scenario: 容忍计算精度误差
- **WHEN** 元素的边界超出页面范围不超过 0.1 英寸
- **THEN** 系统不报告警告
#### Scenario: 检测元素完全在页面外
- **WHEN** 元素的 left >= 页面宽度 或 top >= 页面高度
- **THEN** 系统报告 WARNING 级别警告:"元素完全在页面外"
#### Scenario: 检测表格超出页面范围
- **WHEN** 表格的 position[0] + sum(col_widths) 超出页面宽度 0.1 英寸以上
- **THEN** 系统报告 WARNING 级别警告:"表格超出页面宽度"
### Requirement: 资源验证
系统必须验证 YAML 文件引用的外部资源是否存在。
#### Scenario: 检测图片文件不存在
- **WHEN** image 元素的 src 指向的文件不存在
- **THEN** 系统报告 ERROR 级别错误:"图片文件不存在: {src}"
#### Scenario: 支持相对路径图片
- **WHEN** image 元素的 src 是相对路径
- **THEN** 系统相对于 YAML 文件所在目录解析路径
#### Scenario: 检测模板文件不存在
- **WHEN** 幻灯片引用的 template 文件不存在
- **THEN** 系统报告 ERROR 级别错误:"模板文件不存在: {template}"
#### Scenario: 验证模板文件结构
- **WHEN** 模板文件存在但结构不正确(缺少 elements 字段)
- **THEN** 系统报告 ERROR 级别错误:"模板文件结构错误: {template}"
### Requirement: 样式验证
系统必须验证样式相关属性的有效性和合理性。
#### Scenario: 检测无效的颜色格式
- **WHEN** 颜色值不符合 #RGB#RRGGBB 格式
- **THEN** 系统报告 ERROR 级别错误:"无效的颜色格式: {color}"
#### Scenario: 检测字体过小
- **WHEN** 字体大小小于 8pt
- **THEN** 系统报告 WARNING 级别警告:"字体太小: {size}pt (建议 >= 8pt)"
#### Scenario: 检测字体过大
- **WHEN** 字体大小大于 100pt
- **THEN** 系统报告 WARNING 级别警告:"字体太大: {size}pt (建议 <= 100pt)"
### Requirement: 分级错误报告
系统必须按照严重程度对验证问题进行分级报告。
#### Scenario: ERROR 级别阻止转换
- **WHEN** 验证发现 ERROR 级别问题
- **THEN** 系统在独立验证模式下返回非零退出码,在自动验证模式下阻止转换
#### Scenario: WARNING 级别不阻止转换
- **WHEN** 验证仅发现 WARNING 级别问题
- **THEN** 系统输出警告信息但允许转换继续,返回零退出码
#### Scenario: 显示问题位置
- **WHEN** 验证发现问题
- **THEN** 系统输出包含幻灯片编号和元素编号的位置信息:"[幻灯片 {n}, 元素 {m}]"
#### Scenario: 分类统计问题数量
- **WHEN** 验证完成
- **THEN** 系统输出问题统计:"发现 {n} 个错误, {m} 个警告"
### Requirement: 清晰的命令行输出
系统必须以清晰易读的格式输出验证结果。
#### Scenario: 使用图标区分级别
- **WHEN** 输出验证结果
- **THEN** 系统使用 "❌" 表示 ERROR"⚠️" 表示 WARNING"" 表示 INFO
#### Scenario: 按级别分组显示
- **WHEN** 输出验证结果
- **THEN** 系统按 ERROR、WARNING、INFO 分组显示问题
#### Scenario: 验证通过的提示
- **WHEN** 验证未发现任何问题
- **THEN** 系统输出:"✅ 验证通过,未发现问题"

View File

@@ -0,0 +1,75 @@
## 1. 验证结果数据结构
- [x] 1.1 创建 validators/ 目录结构
- [x] 1.2 实现 validators/result.py 中的 ValidationIssue 数据类
- [x] 1.3 实现 validators/result.py 中的 ValidationResult 数据类
- [x] 1.4 实现 ValidationResult.has_errors() 方法
- [x] 1.5 实现 ValidationResult.format_output() 方法,支持图标和分组显示
## 2. 元素类验证增强
- [x] 2.1 在 core/elements.py 中添加颜色格式验证工具函数 _is_valid_color()
- [x] 2.2 为 TextElement 类添加 validate() 方法,检查颜色格式和字体大小
- [x] 2.3 为 ImageElement 类添加 validate() 方法,检查必需字段
- [x] 2.4 为 ShapeElement 类添加 validate() 方法,检查颜色格式和形状类型枚举
- [x] 2.5 为 TableElement 类添加 validate() 方法,检查数据完整性
## 3. 几何验证器
- [x] 3.1 创建 validators/geometry.py 文件
- [x] 3.2 实现 GeometryValidator 类
- [x] 3.3 实现元素边界检查方法,支持 0.1 英寸容忍度
- [x] 3.4 实现表格边界检查方法
- [x] 3.5 实现元素完全在页面外的检查
## 4. 资源验证器
- [x] 4.1 创建 validators/resource.py 文件
- [x] 4.2 实现 ResourceValidator 类
- [x] 4.3 实现图片文件存在性检查,支持相对路径和绝对路径
- [x] 4.4 实现模板文件存在性检查
- [x] 4.5 实现模板文件结构验证(检查 elements 字段)
## 5. 主验证器
- [x] 5.1 创建 validators/validator.py 文件
- [x] 5.2 实现 Validator 类的 validate() 方法
- [x] 5.3 集成 YAML 加载(复用 loaders/yaml_loader.py
- [x] 5.4 集成元素级验证(调用元素类的 validate() 方法)
- [x] 5.5 集成几何验证器
- [x] 5.6 集成资源验证器
- [x] 5.7 在 validators/__init__.py 中导出主验证器
## 6. 命令行接口 - check 子命令
- [x] 6.1 修改 yaml2pptx.py使用 argparse 的 subparsers 添加 check 子命令
- [x] 6.2 实现 check 子命令的参数解析input, --template-dir
- [x] 6.3 实现 check 子命令的处理函数,调用验证器
- [x] 6.4 实现验证结果的命令行输出
- [x] 6.5 根据验证结果返回正确的退出码(有 ERROR 返回非零)
## 7. 命令行接口 - 自动验证
- [x] 7.1 在转换命令中添加 --no-check 选项
- [x] 7.2 在转换流程中添加自动验证逻辑(加载 YAML 后、渲染前)
- [x] 7.3 实现验证失败时终止转换的逻辑
- [x] 7.4 确保 --no-check 选项可以跳过验证
## 8. 测试
- [x] 8.1 在 temp/ 目录创建测试用的 YAML 文件(包含各种错误场景)
- [x] 8.2 测试独立 check 命令的各种场景
- [x] 8.3 测试自动验证功能
- [x] 8.4 测试 --no-check 选项
- [x] 8.5 测试元素超出页面边界的检测
- [x] 8.6 测试图片文件不存在的检测
- [x] 8.7 测试模板文件验证
- [x] 8.8 测试颜色格式验证
- [x] 8.9 测试字体大小警告
- [x] 8.10 测试容忍度0.1 英寸)
## 9. 文档更新
- [x] 9.1 更新 README.md添加 check 命令的使用说明
- [x] 9.2 更新 README.md说明自动验证功能和 --no-check 选项
- [x] 9.3 添加验证错误码和消息的说明(如果需要)

View File

@@ -2,8 +2,8 @@ schema: spec-driven
context: |
本项目始终面向中文开发者,使用中文进行注释、交流等内容的处理,不考虑多语言;
本项目编写的python脚本始终使用uv运行脚本使用Inline script metadata来指定脚本的依赖包和python运行版本
严禁直接使用主机环境的python直接执行脚本严禁在主机环境直接安装python依赖包
本项目编写的python脚本和任何python命令都始终使用uv运行脚本使用Inline script metadata来指定脚本的依赖包和python运行版本命令使用uv run python -c "xxx"执行命令
严禁直接使用主机环境的python直接执行脚本或命令严禁在主机环境直接安装python依赖包
本项目编写的测试文件、临时文件必须放在temp目录下
严禁污染主机环境的任何配置,如有需要,必须请求用户审核操作;
当前项目的面向用户的使用文档在README.md

View File

@@ -0,0 +1,193 @@
# YAML Validation
## Purpose
提供 YAML 演示文稿文件的验证能力,在转换为 PPTX 之前检查各种潜在问题,包括结构验证、几何验证、资源验证和样式验证。支持独立验证命令和转换前自动验证。
## Requirements
### Requirement: 独立验证命令
系统必须提供独立的 check 子命令,用于验证 YAML 文件而不执行转换。
#### Scenario: 执行独立验证
- **WHEN** 用户运行 `yaml2pptx.py check input.yaml`
- **THEN** 系统验证 YAML 文件并输出验证结果,不生成 PPTX 文件
#### Scenario: 验证时指定模板目录
- **WHEN** 用户运行 `yaml2pptx.py check input.yaml --template-dir ./templates`
- **THEN** 系统在验证时使用指定的模板目录来检查模板文件
### Requirement: 转换前自动验证
系统必须在转换 YAML 为 PPTX 之前自动执行验证,除非用户明确跳过。
#### Scenario: 默认自动验证
- **WHEN** 用户运行 `yaml2pptx.py input.yaml output.pptx`
- **THEN** 系统在转换前自动验证 YAML 文件
#### Scenario: 跳过自动验证
- **WHEN** 用户运行 `yaml2pptx.py input.yaml output.pptx --no-check`
- **THEN** 系统跳过验证,直接执行转换
#### Scenario: 验证失败阻止转换
- **WHEN** 自动验证发现 ERROR 级别的问题
- **THEN** 系统输出错误信息并终止转换,返回非零退出码
### Requirement: 元素自验证
系统中的每个元素类型必须能够验证自身的完整性和有效性。
#### Scenario: 元素验证自身属性
- **WHEN** 创建或验证元素对象
- **THEN** 元素类检查自身的必需字段、数据类型、值有效性(如颜色格式、枚举值、字体大小)
#### Scenario: 元素验证与系统验证分离
- **WHEN** 执行验证
- **THEN** 元素类负责自身属性验证(不需要全局上下文的验证),系统验证器负责需要全局上下文的验证(如几何验证需要页面尺寸、资源验证需要文件路径)
### Requirement: 结构验证
系统必须验证 YAML 文件的结构完整性和数据类型正确性。
#### Scenario: 检测缺少必需字段
- **WHEN** YAML 文件缺少 slides 字段
- **THEN** 系统报告 ERROR 级别错误:"缺少必需字段 'slides'"
#### Scenario: 检测数据类型错误
- **WHEN** slides 字段不是列表类型
- **THEN** 系统报告 ERROR 级别错误:"'slides' 必须是一个列表"
#### Scenario: 检测元素类型无效
- **WHEN** 元素的 type 字段值不是 text/image/shape/table 之一
- **THEN** 系统报告 ERROR 级别错误:"不支持的元素类型: {type}"
#### Scenario: 检测无效的枚举值
- **WHEN** shape 元素的 shape 字段值不是 rectangle/ellipse/rounded_rectangle 之一
- **THEN** 系统报告 ERROR 级别错误:"不支持的形状类型: {shape}"
### Requirement: 几何验证
系统必须验证元素的位置和尺寸是否在页面范围内。
#### Scenario: 检测元素超出页面右边界
- **WHEN** 元素的 left + width 超出页面宽度 0.1 英寸以上
- **THEN** 系统报告 WARNING 级别警告:"元素右边界超出: {right} > {width}"
#### Scenario: 检测元素超出页面下边界
- **WHEN** 元素的 top + height 超出页面高度 0.1 英寸以上
- **THEN** 系统报告 WARNING 级别警告:"元素下边界超出: {bottom} > {height}"
#### Scenario: 容忍计算精度误差
- **WHEN** 元素的边界超出页面范围不超过 0.1 英寸
- **THEN** 系统不报告警告
#### Scenario: 检测元素完全在页面外
- **WHEN** 元素的 left >= 页面宽度 或 top >= 页面高度
- **THEN** 系统报告 WARNING 级别警告:"元素完全在页面外"
#### Scenario: 检测表格超出页面范围
- **WHEN** 表格的 position[0] + sum(col_widths) 超出页面宽度 0.1 英寸以上
- **THEN** 系统报告 WARNING 级别警告:"表格超出页面宽度"
### Requirement: 资源验证
系统必须验证 YAML 文件引用的外部资源是否存在。
#### Scenario: 检测图片文件不存在
- **WHEN** image 元素的 src 指向的文件不存在
- **THEN** 系统报告 ERROR 级别错误:"图片文件不存在: {src}"
#### Scenario: 支持相对路径图片
- **WHEN** image 元素的 src 是相对路径
- **THEN** 系统相对于 YAML 文件所在目录解析路径
#### Scenario: 检测模板文件不存在
- **WHEN** 幻灯片引用的 template 文件不存在
- **THEN** 系统报告 ERROR 级别错误:"模板文件不存在: {template}"
#### Scenario: 验证模板文件结构
- **WHEN** 模板文件存在但结构不正确(缺少 elements 字段)
- **THEN** 系统报告 ERROR 级别错误:"模板文件结构错误: {template}"
### Requirement: 样式验证
系统必须验证样式相关属性的有效性和合理性。
#### Scenario: 检测无效的颜色格式
- **WHEN** 颜色值不符合 #RGB#RRGGBB 格式
- **THEN** 系统报告 ERROR 级别错误:"无效的颜色格式: {color}"
#### Scenario: 检测字体过小
- **WHEN** 字体大小小于 8pt
- **THEN** 系统报告 WARNING 级别警告:"字体太小: {size}pt (建议 >= 8pt)"
#### Scenario: 检测字体过大
- **WHEN** 字体大小大于 100pt
- **THEN** 系统报告 WARNING 级别警告:"字体太大: {size}pt (建议 <= 100pt)"
### Requirement: 分级错误报告
系统必须按照严重程度对验证问题进行分级报告。
#### Scenario: ERROR 级别阻止转换
- **WHEN** 验证发现 ERROR 级别问题
- **THEN** 系统在独立验证模式下返回非零退出码,在自动验证模式下阻止转换
#### Scenario: WARNING 级别不阻止转换
- **WHEN** 验证仅发现 WARNING 级别问题
- **THEN** 系统输出警告信息但允许转换继续,返回零退出码
#### Scenario: 显示问题位置
- **WHEN** 验证发现问题
- **THEN** 系统输出包含幻灯片编号和元素编号的位置信息:"[幻灯片 {n}, 元素 {m}]"
#### Scenario: 分类统计问题数量
- **WHEN** 验证完成
- **THEN** 系统输出问题统计:"发现 {n} 个错误, {m} 个警告"
### Requirement: 清晰的命令行输出
系统必须以清晰易读的格式输出验证结果。
#### Scenario: 使用图标区分级别
- **WHEN** 输出验证结果
- **THEN** 系统使用 "❌" 表示 ERROR"⚠️" 表示 WARNING"" 表示 INFO
#### Scenario: 按级别分组显示
- **WHEN** 输出验证结果
- **THEN** 系统按 ERROR、WARNING、INFO 分组显示问题
#### Scenario: 验证通过的提示
- **WHEN** 验证未发现任何问题
- **THEN** 系统输出:"✅ 验证通过,未发现问题"

10
validators/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
"""
验证器模块
提供 YAML 文件验证功能。
"""
from validators.validator import Validator
from validators.result import ValidationResult, ValidationIssue
__all__ = ['Validator', 'ValidationResult', 'ValidationIssue']

131
validators/geometry.py Normal file
View File

@@ -0,0 +1,131 @@
"""
几何验证器
验证元素的位置和尺寸是否在页面范围内。
"""
from validators.result import ValidationIssue
# 容忍度0.1 英寸
TOLERANCE = 0.1
class GeometryValidator:
"""几何验证器"""
def __init__(self, slide_width: float, slide_height: float):
"""
初始化几何验证器
Args:
slide_width: 幻灯片宽度(英寸)
slide_height: 幻灯片高度(英寸)
"""
self.slide_width = slide_width
self.slide_height = slide_height
def validate_element(self, element, slide_index: int, elem_index: int) -> list:
"""
验证元素的几何属性
Args:
element: 元素对象
slide_index: 幻灯片索引(从 1 开始)
elem_index: 元素索引(从 1 开始)
Returns:
验证问题列表
"""
issues = []
location = f"幻灯片 {slide_index}, 元素 {elem_index}"
# 检查元素边界
if hasattr(element, 'box'):
issues.extend(self._check_element_bounds(
element.box, location
))
# 检查表格边界
if hasattr(element, 'position') and hasattr(element, 'col_widths'):
if element.col_widths:
issues.extend(self._check_table_bounds(
element.position, element.col_widths, location
))
return issues
def _check_element_bounds(self, box: list, location: str) -> list:
"""
检查元素边界是否在页面范围内
Args:
box: [left, top, width, height]
location: 位置信息
Returns:
验证问题列表
"""
issues = []
left, top, width, height = box
right = left + width
bottom = top + height
# 检查元素是否完全在页面外
if (right <= 0 or bottom <= 0 or
left >= self.slide_width or top >= self.slide_height):
issues.append(ValidationIssue(
level="WARNING",
message="元素完全在页面外",
location=location,
code="ELEMENT_COMPLETELY_OUT_OF_BOUNDS"
))
return issues
# 检查右边界
if right > self.slide_width + TOLERANCE:
issues.append(ValidationIssue(
level="WARNING",
message=f"元素右边界超出: {right:.2f} > {self.slide_width}",
location=location,
code="ELEMENT_OUT_OF_BOUNDS"
))
# 检查下边界
if bottom > self.slide_height + TOLERANCE:
issues.append(ValidationIssue(
level="WARNING",
message=f"元素下边界超出: {bottom:.2f} > {self.slide_height}",
location=location,
code="ELEMENT_OUT_OF_BOUNDS"
))
return issues
def _check_table_bounds(self, position: list, col_widths: list, location: str) -> list:
"""
检查表格边界是否在页面范围内
Args:
position: [left, top]
col_widths: 列宽列表
location: 位置信息
Returns:
验证问题列表
"""
issues = []
left, top = position
table_width = sum(col_widths)
right = left + table_width
# 检查表格右边界
if right > self.slide_width + TOLERANCE:
issues.append(ValidationIssue(
level="WARNING",
message=f"表格超出页面宽度: {right:.2f} > {self.slide_width}",
location=location,
code="TABLE_OUT_OF_BOUNDS"
))
return issues

122
validators/resource.py Normal file
View File

@@ -0,0 +1,122 @@
"""
资源验证器
验证 YAML 文件引用的外部资源是否存在。
"""
from pathlib import Path
from validators.result import ValidationIssue
from loaders.yaml_loader import load_yaml_file, validate_template_yaml
class ResourceValidator:
"""资源验证器"""
def __init__(self, yaml_dir: Path, template_dir: Path = None):
"""
初始化资源验证器
Args:
yaml_dir: YAML 文件所在目录
template_dir: 模板文件目录(可选)
"""
self.yaml_dir = yaml_dir
self.template_dir = template_dir
def validate_image(self, element, slide_index: int, elem_index: int) -> list:
"""
验证图片文件是否存在
Args:
element: 图片元素对象
slide_index: 幻灯片索引(从 1 开始)
elem_index: 元素索引(从 1 开始)
Returns:
验证问题列表
"""
issues = []
location = f"幻灯片 {slide_index}, 元素 {elem_index}"
if not hasattr(element, 'src'):
return issues
src = element.src
if not src:
return issues
# 解析路径(支持相对路径和绝对路径)
src_path = Path(src)
if not src_path.is_absolute():
src_path = self.yaml_dir / src
# 检查文件是否存在
if not src_path.exists():
issues.append(ValidationIssue(
level="ERROR",
message=f"图片文件不存在: {src}",
location=location,
code="IMAGE_FILE_NOT_FOUND"
))
return issues
def validate_template(self, slide_data: dict, slide_index: int) -> list:
"""
验证模板文件是否存在且结构正确
Args:
slide_data: 幻灯片数据字典
slide_index: 幻灯片索引(从 1 开始)
Returns:
验证问题列表
"""
issues = []
location = f"幻灯片 {slide_index}"
if 'template' not in slide_data:
return issues
template_name = slide_data['template']
if not template_name:
return issues
# 检查是否提供了模板目录
if not self.template_dir:
issues.append(ValidationIssue(
level="ERROR",
message=f"使用了模板但未指定模板目录: {template_name}",
location=location,
code="TEMPLATE_DIR_NOT_SPECIFIED"
))
return issues
# 解析模板文件路径
template_path = self.template_dir / template_name
if not template_path.suffix:
template_path = template_path.with_suffix('.yaml')
# 检查模板文件是否存在
if not template_path.exists():
issues.append(ValidationIssue(
level="ERROR",
message=f"模板文件不存在: {template_name}",
location=location,
code="TEMPLATE_FILE_NOT_FOUND"
))
return issues
# 验证模板文件结构
try:
template_data = load_yaml_file(template_path)
validate_template_yaml(template_data, str(template_path))
except Exception as e:
issues.append(ValidationIssue(
level="ERROR",
message=f"模板文件结构错误: {template_name} - {str(e)}",
location=location,
code="TEMPLATE_STRUCTURE_ERROR"
))
return issues

73
validators/result.py Normal file
View File

@@ -0,0 +1,73 @@
"""
验证结果数据结构
定义验证问题和验证结果的数据类。
"""
from dataclasses import dataclass, field
from typing import List
@dataclass
class ValidationIssue:
"""验证问题"""
level: str # "ERROR" | "WARNING" | "INFO"
message: str
location: str # "幻灯片 2, 元素 3"
code: str # "ELEMENT_OUT_OF_BOUNDS"
@dataclass
class ValidationResult:
"""验证结果"""
valid: bool # 是否有 ERROR
errors: List[ValidationIssue] = field(default_factory=list)
warnings: List[ValidationIssue] = field(default_factory=list)
infos: List[ValidationIssue] = field(default_factory=list)
def has_errors(self) -> bool:
"""是否有错误"""
return len(self.errors) > 0
def format_output(self) -> str:
"""格式化为命令行输出"""
lines = []
lines.append("🔍 正在检查 YAML 文件...\n")
# 错误
if self.errors:
lines.append(f"❌ 错误 ({len(self.errors)}):")
for issue in self.errors:
location_str = f"[{issue.location}] " if issue.location else ""
lines.append(f" {location_str}{issue.message}")
lines.append("")
# 警告
if self.warnings:
lines.append(f"⚠️ 警告 ({len(self.warnings)}):")
for issue in self.warnings:
location_str = f"[{issue.location}] " if issue.location else ""
lines.append(f" {location_str}{issue.message}")
lines.append("")
# 提示
if self.infos:
lines.append(f" 提示 ({len(self.infos)}):")
for issue in self.infos:
location_str = f"[{issue.location}] " if issue.location else ""
lines.append(f" {location_str}{issue.message}")
lines.append("")
# 总结
if not self.errors and not self.warnings and not self.infos:
lines.append("✅ 验证通过,未发现问题")
else:
summary_parts = []
if self.errors:
summary_parts.append(f"{len(self.errors)} 个错误")
if self.warnings:
summary_parts.append(f"{len(self.warnings)} 个警告")
lines.append(f"检查完成: 发现 {', '.join(summary_parts)}")
return "\n".join(lines)

160
validators/validator.py Normal file
View File

@@ -0,0 +1,160 @@
"""
主验证器
协调各子验证器,执行完整的 YAML 文件验证。
"""
from pathlib import Path
from loaders.yaml_loader import load_yaml_file, validate_presentation_yaml, YAMLError
from validators.result import ValidationResult, ValidationIssue
from validators.geometry import GeometryValidator
from validators.resource import ResourceValidator
from core.elements import create_element
class Validator:
"""主验证器"""
# 幻灯片尺寸映射(英寸)
SLIDE_SIZES = {
"16:9": (10, 5.625),
"4:3": (10, 7.5),
}
def __init__(self):
"""初始化验证器"""
pass
def validate(self, yaml_path: Path, template_dir: Path = None) -> ValidationResult:
"""
验证 YAML 文件
Args:
yaml_path: YAML 文件路径
template_dir: 模板文件目录(可选)
Returns:
验证结果
"""
errors = []
warnings = []
infos = []
# 1. 加载 YAML
try:
data = load_yaml_file(yaml_path)
validate_presentation_yaml(data, str(yaml_path))
except YAMLError as e:
errors.append(ValidationIssue(
level="ERROR",
message=str(e),
location="",
code="YAML_ERROR"
))
return ValidationResult(
valid=False,
errors=errors,
warnings=warnings,
infos=infos
)
except Exception as e:
errors.append(ValidationIssue(
level="ERROR",
message=f"加载 YAML 文件失败: {str(e)}",
location="",
code="YAML_LOAD_ERROR"
))
return ValidationResult(
valid=False,
errors=errors,
warnings=warnings,
infos=infos
)
# 获取幻灯片尺寸
size_str = data.get('metadata', {}).get('size', '16:9')
slide_width, slide_height = self.SLIDE_SIZES.get(size_str, (10, 5.625))
# 初始化子验证器
geometry_validator = GeometryValidator(slide_width, slide_height)
resource_validator = ResourceValidator(
yaml_dir=yaml_path.parent,
template_dir=template_dir
)
# 2. 验证每个幻灯片
slides = data.get('slides', [])
for slide_index, slide_data in enumerate(slides, start=1):
# 验证模板
template_issues = resource_validator.validate_template(slide_data, slide_index)
self._categorize_issues(template_issues, errors, warnings, infos)
# 验证元素
elements = slide_data.get('elements', [])
for elem_index, elem_dict in enumerate(elements, start=1):
# 3. 元素级验证
try:
element = create_element(elem_dict)
# 调用元素的 validate() 方法
if hasattr(element, 'validate'):
elem_issues = element.validate()
# 填充位置信息
for issue in elem_issues:
issue.location = f"幻灯片 {slide_index}, 元素 {elem_index}"
self._categorize_issues(elem_issues, errors, warnings, infos)
# 4. 几何验证
geom_issues = geometry_validator.validate_element(
element, slide_index, elem_index
)
self._categorize_issues(geom_issues, errors, warnings, infos)
# 5. 资源验证(图片)
if elem_dict.get('type') == 'image':
img_issues = resource_validator.validate_image(
element, slide_index, elem_index
)
self._categorize_issues(img_issues, errors, warnings, infos)
except ValueError as e:
# 元素创建失败__post_init__ 中的验证)
errors.append(ValidationIssue(
level="ERROR",
message=str(e),
location=f"幻灯片 {slide_index}, 元素 {elem_index}",
code="ELEMENT_VALIDATION_ERROR"
))
except Exception as e:
errors.append(ValidationIssue(
level="ERROR",
message=f"验证元素时出错: {str(e)}",
location=f"幻灯片 {slide_index}, 元素 {elem_index}",
code="ELEMENT_VALIDATION_ERROR"
))
# 返回验证结果
return ValidationResult(
valid=len(errors) == 0,
errors=errors,
warnings=warnings,
infos=infos
)
def _categorize_issues(self, issues: list, errors: list, warnings: list, infos: list):
"""
将问题按级别分类
Args:
issues: 问题列表
errors: 错误列表
warnings: 警告列表
infos: 提示列表
"""
for issue in issues:
if issue.level == "ERROR":
errors.append(issue)
elif issue.level == "WARNING":
warnings.append(issue)
elif issue.level == "INFO":
infos.append(issue)

View File

@@ -14,6 +14,11 @@ YAML to PPTX Converter
将 YAML 格式的演示文稿源文件转换为 PPTX 文件
使用方法:
# 子命令模式(推荐)
uv run yaml2pptx.py convert input.yaml output.pptx
uv run yaml2pptx.py check input.yaml
# 传统模式(向后兼容)
uv run yaml2pptx.py input.yaml output.pptx
uv run yaml2pptx.py input.yaml # 自动生成 input.pptx
"""
@@ -28,42 +33,78 @@ from loaders.yaml_loader import YAMLError
from core.presentation import Presentation
from renderers.pptx_renderer import PptxGenerator
from preview.server import start_preview_server
from validators import Validator
def parse_args():
"""解析命令行参数"""
# 检查是否使用了子命令
if len(sys.argv) > 1 and sys.argv[1] in ['check', 'convert']:
# 使用子命令解析
parser = argparse.ArgumentParser(
description="将 YAML 格式的演示文稿源文件转换为 PPTX 文件"
)
parser.add_argument(
"input",
type=str,
help="输入的 YAML 文件路径"
)
parser.add_argument(
"output",
type=str,
nargs="?",
help="PPTX 文件路径(可选,默认为输入文件名.pptx"
)
parser.add_argument(
"--template-dir",
type=str,
default=None,
help="模板文件目录路径(如果 YAML 中使用了模板则必须指定)。可以是绝对路径或相对路径(相对于当前工作目录)"
)
parser.add_argument(
"--preview",
action="store_true",
help="启动浏览器预览模式,而不是生成 PPTX 文件"
)
parser.add_argument(
"--port",
type=int,
default=None,
help="预览服务器端口(默认随机选择 20000-30000 之间的端口)"
)
subparsers = parser.add_subparsers(dest='command')
# check 子命令
check_parser = subparsers.add_parser('check', help='验证 YAML 文件')
check_parser.add_argument("input", type=str, help="输入的 YAML 文件路径")
check_parser.add_argument("--template-dir", type=str, default=None, help="模板文件目录路径")
# convert 子命令
convert_parser = subparsers.add_parser('convert', help='转换 YAML 为 PPTX')
convert_parser.add_argument("input", type=str, help="YAML 文件路径")
convert_parser.add_argument("output", type=str, nargs="?", help="输出的 PPTX 文件路径(可选,默认为输入文件名.pptx")
convert_parser.add_argument("--template-dir", type=str, default=None, help="模板文件目录路径")
convert_parser.add_argument("--preview", action="store_true", help="启动浏览器预览模式")
convert_parser.add_argument("--port", type=int, default=None, help="预览服务器端口")
convert_parser.add_argument("--no-check", action="store_true", help="跳过自动验证")
return parser.parse_args()
else:
# 使用传统解析(转换命令)- 向后兼容
parser = argparse.ArgumentParser(
description="将 YAML 格式的演示文稿源文件转换为 PPTX 文件"
)
parser.add_argument("input", type=str, help="输入的 YAML 文件路径")
parser.add_argument("output", type=str, nargs="?", help="输出的 PPTX 文件路径(可选,默认为输入文件名.pptx")
parser.add_argument("--template-dir", type=str, default=None, help="模板文件目录路径")
parser.add_argument("--preview", action="store_true", help="启动浏览器预览模式")
parser.add_argument("--port", type=int, default=None, help="预览服务器端口")
parser.add_argument("--no-check", action="store_true", help="跳过自动验证")
args = parser.parse_args()
args.command = None # 标记为非子命令(向后兼容模式)
return args
def handle_check(args):
"""处理 check 子命令"""
try:
input_path = Path(args.input)
# 处理模板目录
template_dir = None
if args.template_dir:
template_dir = Path(args.template_dir)
# 执行验证
validator = Validator()
result = validator.validate(input_path, template_dir)
# 输出验证结果
print(result.format_output())
# 返回退出码
if result.has_errors():
sys.exit(1)
else:
sys.exit(0)
except Exception as e:
log_error(f"验证失败: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
def main():
@@ -71,6 +112,34 @@ def main():
try:
args = parse_args()
# 处理 check 子命令
if args.command == 'check':
handle_check(args)
return
# 处理 convert 子命令或传统模式(向后兼容)
# convert 子命令和无子命令的行为相同
if args.command == 'convert' or args.command is None:
handle_convert(args)
return
except YAMLError as e:
log_error(str(e))
sys.exit(1)
except Exception as e:
log_error(f"未知错误: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
def handle_convert(args):
"""处理 convert 子命令或传统转换模式"""
# 检查是否提供了输入文件
if not args.input:
log_error("错误: 缺少输入文件参数")
sys.exit(1)
# 处理输入文件路径
input_path = Path(args.input)
@@ -90,6 +159,24 @@ def main():
log_info(f"开始转换: {input_path}")
log_info(f"输出文件: {output_path}")
# 自动验证(除非使用 --no-check
if not args.no_check:
log_info("验证 YAML 文件...")
template_dir = Path(args.template_dir) if args.template_dir else None
validator = Validator()
result = validator.validate(input_path, template_dir)
# 如果有错误,输出并终止
if result.has_errors():
print("\n" + result.format_output())
log_error("验证失败,转换已终止")
sys.exit(1)
# 如果有警告,输出但继续
if result.warnings:
print("\n" + result.format_output())
print() # 空行
# 1. 加载演示文稿
log_info("加载演示文稿...")
pres = Presentation(input_path, templates_dir=args.template_dir)
@@ -113,15 +200,6 @@ def main():
log_success(f"转换完成: {output_path}")
except YAMLError as e:
log_error(str(e))
sys.exit(1)
except Exception as e:
log_error(f"未知错误: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()