1
0
Files
PPTX/README_DEV.md
lanyuanxiaoyao f1aae96a04 refactor: 重构外部模板系统,改为单文件模板库模式
主要变更:
- 将 templates_dir 参数改为 template_file,支持单个模板库 YAML 文件
- 添加模板库 YAML 验证功能
- 为模板添加 base_dir 支持,正确解析相对路径资源
- 内联模板与外部模板同名时改为警告(内联优先)
- 移除模板缓存机制,直接使用模板库字典
- 更新所有相关测试以适配新的模板加载方式

此重构简化了模板管理,使模板资源的路径解析更加清晰明确。
2026-03-05 13:27:12 +08:00

35 KiB
Raw Blame History

开发文档

本文档说明 yaml2pptx 项目的代码结构、开发规范和技术决策。

项目概述

yaml2pptx 是一个将 YAML 格式的演示文稿源文件转换为 PPTX 文件的工具,支持模板系统和浏览器预览功能。

代码结构

项目采用模块化架构,按功能职责组织代码:

html2pptx/
├── yaml2pptx.py (200 行)          # 入口脚本CLI + main 函数
├── utils.py (74 行)               # 工具函数(日志、颜色转换)
├── core/                          # 核心领域模型
│   ├── elements.py (200 行)       # 元素抽象层dataclass + validate
│   ├── template.py (191 行)       # 模板系统
│   ├── condition_evaluator.py     # 条件表达式评估器(新增)
│   └── presentation.py (91 行)    # 演示文稿类
├── loaders/                       # 数据加载层
│   └── yaml_loader.py (113 行)    # YAML 加载和验证
├── validators/                    # 验证层
│   ├── __init__.py                # 导出主验证器
│   ├── result.py (70 行)          # 验证结果数据结构
│   ├── validator.py (150 行)      # 主验证器
│   ├── geometry.py (120 行)       # 几何验证器
│   └── resource.py (110 行)       # 资源验证器
├── renderers/                     # 渲染层
│   ├── pptx_renderer.py (292 行)  # PPTX 渲染器
│   └── html_renderer.py (172 行)  # HTML 渲染器(预览)
└── preview/                       # 预览功能
    └── server.py (244 行)         # Flask 服务器 + 文件监听

依赖关系

yaml2pptx.py (入口)
    ↓
    ├─→ utils (工具函数)
    ├─→ loaders.yaml_loader (YAML 加载)
    ├─→ validators.validator (验证器)
    │       ↓
    │       ├─→ validators.result (验证结果)
    │       ├─→ validators.geometry (几何验证)
    │       ├─→ validators.resource (资源验证)
    │       └─→ core.elements (元素验证)
    ├─→ core.presentation (演示文稿)
    │       ↓
    │       ├─→ core.template (模板)
    │       └─→ core.elements (元素)
    ├─→ renderers.pptx_renderer (PPTX 生成)
    │       ↓
    │       └─→ core.elements
    └─→ preview.server (预览服务)
            ↓
            └─→ renderers.html_renderer
                    ↓
                    └─→ core.elements

依赖原则

  • 单向依赖:入口 → 验证/渲染 → 核心 ← 加载
  • 无循环依赖
  • 核心层不依赖其他业务模块
  • 验证层可以调用核心层的元素验证方法

模块职责

1. yaml2pptx.py入口层

  • 职责CLI 参数解析、流程编排
  • 行数:约 100-150 行
  • 包含
    • /// script 依赖声明
    • parse_args() - 命令行参数解析
    • main() - 主流程编排
    • handle_convert() - 转换流程,包含页面级 enabled 检查
  • 不包含:业务逻辑、数据处理
  • enabled 实现细节
    • 在主渲染循环中检查 slide_data.get('enabled', True)
    • 跳过 enabled=false 的幻灯片,不调用 render_slide()
    • 维护独立的 slide_index 计数器,只统计实际渲染的幻灯片
    • 进度日志显示准确的渲染数量(不包括禁用的幻灯片)

2. utils/(工具层)

  • 职责:通用工具函数
  • 包含
    • utils/__init__.py - 日志和颜色工具
      • 日志函数:log_info(), log_success(), log_error(), log_progress()
      • 颜色转换:hex_to_rgb(), validate_color()
  • 依赖
    • Pillow (PIL) - 保留用于未来可能的图片处理需求

3. loaders/yaml_loader.py加载层

  • 职责YAML 文件加载和验证
  • 包含
    • YAMLError - 自定义异常
    • load_yaml_file() - 加载 YAML 文件
    • validate_presentation_yaml() - 验证演示文稿结构,调用 validate_templates_yaml() 验证内联模板,验证幻灯片 enabled 字段
    • validate_template_yaml() - 验证外部模板结构
    • validate_templates_yaml() - 验证内联模板结构templates 字段)
  • 特点
    • 内联模板验证包括:结构验证、元素验证、变量定义验证、默认值验证
    • 检测默认值中引用不存在的变量
    • 验证 enabled 字段必须是布尔值,拒绝字符串或条件表达式

4. core/elements.py核心层 - 元素抽象)

  • 职责:定义元素数据类和工厂函数
  • 包含
    • _is_valid_color() - 颜色格式验证工具函数
    • FontConfig - 字体配置数据类(新增)
      • 支持所有字体属性parent、family、size、bold、italic、underline、strikethrough、color、align、line_spacing、space_before、space_after、baseline、caps
      • __post_init__ 中验证 baseline 和 caps 枚举值
    • TextElement - 文本元素dataclass + validate
      • font 字段类型:FontConfig | str | dict
    • ImageElement - 图片元素dataclass + validate
      • 新增字段:fit (适配模式), background (背景色)
      • 支持四种适配模式stretch默认、contain、cover、center
    • ShapeElement - 形状元素dataclass + validate
    • TableElement - 表格元素dataclass + validate
      • 新增字段:font(表格数据单元格字体)、header_font(表头字体)
    • create_element() - 元素工厂函数
      • 自动将字典形式的 font 转换为 FontConfig 对象
  • 特点
    • 使用 @dataclass 装饰器
    • __post_init__ 中进行创建时验证
    • validate() 方法中进行元素级验证
    • 元素类负责自身属性的验证(颜色格式、字体大小、枚举值等)

4.3. utils/font_resolver.py工具层 - 字体解析)

  • 职责:字体引用解析、继承链处理和预设类别映射
  • 包含
    • PRESET_FONT_MAPPING - 预设字体类别映射常量
      • sans → Arial
      • serif → Times New Roman
      • mono → Courier New
      • cjk-sans → Microsoft YaHei
      • cjk-serif → SimSun
    • FontResolver
      • __init__(fonts, fonts_default) - 初始化解析器
      • resolve_font(font_config) - 解析字体配置(主入口)
      • _resolve_reference(reference, visited) - 解析字体引用
      • _resolve_font_dict(font_dict, visited) - 解析字体字典
      • _get_default_config(visited) - 获取默认字体配置
      • _merge_with_default(config, default) - 合并配置与默认值
  • 特点
    • 支持三种引用方式:整体引用("@xxx")、继承覆盖({parent: "@xxx"})、独立定义
    • 实现属性继承链parent → 当前 → fonts_default → 系统默认
    • 引用循环检测:维护已访问集合,检测重复引用
    • 最大引用深度限制10 层
    • 预设类别自动映射:在 family 字段中识别预设类别名称
  • 字体引用解析逻辑
    1. 如果 font_config 为 None使用 fonts_default
    2. 如果是字符串("@xxx"),解析为整体引用
    3. 如果是字典且包含 parent先解析 parent 再覆盖当前属性
    4. 如果是字典且不包含 parent直接使用字典属性
    5. 未定义的属性从 fonts_default 继承
    6. 如果 family 是预设类别,映射到具体字体名称
    7. 返回完整的 FontConfig 对象

4.5. validators/(验证层)

  • 职责YAML 文件验证,在转换前检查问题
  • 包含
    • validators/result.py - 验证结果数据结构
      • ValidationIssue - 验证问题level, message, location, code
      • ValidationResult - 验证结果errors, warnings, infos
    • validators/geometry.py - 几何验证器
      • GeometryValidator - 检查元素边界、页面范围
      • 支持 0.1 英寸容忍度
    • validators/resource.py - 资源验证器
      • ResourceValidator - 检查图片、模板文件存在性
      • 验证模板文件结构
    • validators/validator.py - 主验证器
      • Validator - 协调所有子验证器
      • 集成元素级验证、几何验证、资源验证
  • 特点
    • 分级错误报告ERROR/WARNING/INFO
    • ERROR 阻止转换WARNING 不阻止
    • 验证职责分层:元素级验证在元素类中,系统级验证在验证器中

5. core/template.py核心层 - 模板)

  • 职责:模板加载和变量解析
  • 包含
    • Template
    • from_data() 类方法:从字典创建模板实例(用于内联模板)
    • 变量解析:resolve_value(), resolve_element()
    • 条件渲染:evaluate_condition() - 委托给 ConditionEvaluator
    • 模板渲染:render()
  • 特点
    • 支持外部模板(从文件加载)和内联模板(从字典创建)

5.5. core/condition_evaluator.py核心层 - 条件评估)

  • 职责:安全地评估条件表达式
  • 包含
    • ConditionEvaluator
    • evaluate_condition() - 主评估方法
    • _get_evaluator() - 配置 simpleeval 实例
    • _extract_expression() - 提取表达式内容
  • 技术实现
    • 使用 simpleeval 库的 EvalWithCompoundTypes 进行安全评估
    • 支持比较运算符(==, !=, >, <, >=, <=
    • 支持逻辑运算符and, or, not
    • 支持成员测试in, not in
    • 支持列表/元组字面量
    • 支持数学运算(+, -, *, /, %, **
    • 支持内置函数int, float, str, len, bool, abs, min, max
  • 安全策略
    • 表达式最大长度限制500 字符
    • 禁止属性访问obj.attr
    • 禁止函数定义lambda, def
    • 禁止模块导入import
    • 白名单函数限制
    • 详细的错误信息映射
  • 错误处理
    • NameNotDefined → "条件表达式中的变量未定义"
    • FunctionNotDefined → "条件表达式中使用了不支持的函数"
    • FeatureNotAvailable → "条件表达式使用了不支持的语法特性"
    • AttributeDoesNotExist → "不支持属性访问"
    • SyntaxError → "条件表达式语法错误"
    • 内联模板通过 from_data() 类方法创建,避免修改现有 __init__
    • 禁止内联模板相互引用,在渲染时检测并报错

6. core/presentation.py核心层 - 演示文稿)

  • 职责:演示文稿管理和幻灯片渲染
  • 包含
    • Presentation
    • 内联模板存储:__init__ 中解析并保存 templates 字段到 self.inline_templates
    • 模板查找:get_template() 优先查找内联模板,然后回退到外部模板
    • 同名检测:_external_template_exists() 检查外部模板是否存在,防止命名冲突
    • 模板缓存:外部模板使用 template_cache 缓存
    • 幻灯片渲染:render_slide() - 支持三种模式
  • 特点
    • 将元素字典转换为元素对象
    • 使用 create_element() 工厂函数
    • 内联和外部模板同名时抛出 ERROR 错误
    • 内联模板不需要缓存(已在内存中)

幻灯片渲染模式

render_slide() 方法支持三种渲染模式:

  1. 纯模板模式:只有 template 字段

    • 渲染模板元素
    • 向后兼容原有行为
  2. 纯自定义模式:只有 elements 字段

    • 直接使用自定义元素
    • 向后兼容原有行为
  3. 混合模式:同时有 templateelements 字段(新功能)

    • 先渲染模板元素
    • 自定义元素使用模板变量解析(通过 Template.resolve_element()
    • 合并策略:简单追加(template_elements + custom_elements
    • z 轴顺序:模板元素在底层,自定义元素在上层

元素合并策略

混合模式采用简单追加策略:

# 步骤1渲染模板如果有
elements_from_template = template.render(vars_values)

# 步骤2处理自定义元素如果有
if has_custom_elements and has_template:
    # 使用模板变量解析自定义元素
    elements_from_custom = [
        template.resolve_element(elem, vars_values)
        for elem in custom_elements
    ]

# 步骤3合并元素模板在前自定义在后
final_elements = elements_from_template + elements_from_custom

设计决策

  • 不支持按 key/id 合并(避免引入元素 ID 概念)
  • 不支持位置感知合并(保持简单)
  • 不检测元素重叠(用户通过预览模式查看效果)
  • elements: [] 等同于不指定 elements

7. renderers/pptx_renderer.py渲染层 - PPTX

  • 职责PPTX 文件生成
  • 包含
    • PptxGenerator
    • 渲染方法:_render_text(), _render_image(), _render_shape(), _render_table()
    • 元素分发:_render_element()
  • 特点
    • 渲染器内置在生成器中
    • 使用 isinstance() 检查元素类型
    • 通过元素对象的属性访问数据

8. renderers/html_renderer.py渲染层 - HTML

  • 职责HTML 预览渲染
  • 包含
    • HtmlRenderer
    • 渲染方法:render_text(), render_image(), render_shape(), render_table()
  • 特点
    • 与 PptxRenderer 共享元素抽象层
    • 使用固定 DPI (96) 进行单位转换

9. preview/server.py预览层

  • 职责:浏览器预览和热重载
  • 包含
    • Flask 应用:create_flask_app()
    • 文件监听:YAMLChangeHandler
    • 预览服务器:start_preview_server()
    • HTML 模板:HTML_TEMPLATE, ERROR_TEMPLATE

开发规范

1. Python 环境

必须使用 uv 运行脚本

# 正确
uv run yaml2pptx.py input.yaml output.pptx

# 错误 - 严禁直接使用主机环境的 python
python yaml2pptx.py input.yaml output.pptx

依赖管理

  • 所有依赖在 pyproject.toml[project.dependencies] 中声明
  • uv 会自动安装依赖,无需手动 pip install

2. 命令行接口

子命令架构

# 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-validationconvert 专用,跳过自动验证
  • --force/-fconvert 专用,强制覆盖已存在文件
  • --portpreview 专用,指定端口(默认随机 30000-40000
  • --hostpreview 专用,指定主机地址(默认 127.0.0.1
  • --no-browserpreview 专用,不自动打开浏览器

3. 文件组织

代码文件

  • 每个模块文件控制在 150-300 行
  • 入口脚本约 200 行
  • 使用有意义的文件名和目录结构

测试文件

  • 所有测试文件、临时文件必须放在 temp/ 目录下
  • 不污染项目根目录

4. 代码风格

导入顺序

# 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__ 在创建时自动调用
  • 可扩展性:未来可以添加方法

示例

@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 类中

理由

  • 封装性:渲染逻辑与生成器紧密相关
  • 简单性:不需要额外的渲染器接口
  • 性能:避免额外的方法调用开销

示例

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() 方法中)
    • 颜色格式验证
    • 字体大小合理性
    • 枚举值检查(如形状类型)
    • 表格数据一致性

系统级验证职责

  • 几何验证(元素是否在页面范围内,需要知道页面尺寸)
  • 资源验证(文件是否存在,需要知道文件路径)
  • 跨元素验证(如果未来需要)

示例

# 元素级验证(在元素类中)
@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)在视觉上几乎不可见
  • 避免误报,提升用户体验

实现

TOLERANCE = 0.1  # 英寸

if right > slide_width + TOLERANCE:
    # 报告 WARNING

7. 模板系统架构

yaml2pptx 支持两种模板方式:内联模板和外部模板库。

7.1 内联模板系统

决策:支持在 YAML 源文件中定义内联模板,与外部模板系统共存

理由

  • 降低使用门槛:简单场景无需创建单独的模板文件
  • 保持灵活性:复杂场景仍可使用外部模板
  • 向后兼容:不影响现有外部模板功能

实现要点

  1. 模板定义:在 YAML 顶层添加 templates 字段

    templates:
      my-template:
        vars: [...]
        elements: [...]
    
  2. 模板创建:使用 Template.from_data() 类方法

    @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
    
  3. 模板查找Presentation.get_template() 优先查找内联模板

    def get_template(self, template_name):
        # 1. 检查内联模板
        if template_name in self.inline_templates:
            # 2. 检查同名冲突
            if self._external_template_exists(template_name):
                # 发出 WARNING优先使用内联模板
                self.validation_issues.append(ValidationIssue(
                    level="WARNING",
                    code="TEMPLATE_NAME_CONFLICT",
                    message=f"模板名称冲突: '{template_name}'"
                ))
            return Template.from_data(
                self.inline_templates[template_name],
                template_name,
                base_dir=self.pres_base_dir
            )
        # 3. 回退到外部模板
        return self._get_external_template(template_name)
    
  4. 同名处理:内联和外部模板同名时发出警告,优先使用内联模板

    • 内联模板更贴近文档,优先级更高
    • 发出 WARNING 提醒用户注意冲突
    • 不阻止转换,保持灵活性
  5. 限制:禁止内联模板相互引用

    • 降低实现复杂度
    • 内联模板适合简单场景
    • 复杂引用应使用外部模板
  6. 验证validate_templates_yaml() 验证内联模板结构

    • 检查 templates 是否为字典
    • 检查每个模板是否有必需的 elements 字段
    • 检查变量定义是否有必需的 name 字段
    • 检测默认值中引用不存在的变量

7.2 外部模板库系统

决策:使用单个 YAML 文件作为模板库,包含多个模板定义

理由

  • 统一数据结构:外部模板和内联模板使用相同的字典结构
  • 简化管理:单个文件包含所有模板,便于版本控制和分发
  • 支持元数据:模板库可以包含 description、version、author 等元数据
  • 统一查询:外部模板和内联模板使用相同的查询接口

模板库文件格式

# 模板库元数据(可选)
description: "公司标准模板库"
version: "1.0.0"
author: "设计团队"

# 模板定义(必需)
templates:
  template-name-1:
    description: "模板描述(可选)"
    vars: [...]
    elements: [...]

  template-name-2:
    description: "模板描述(可选)"
    vars: [...]
    elements: [...]

实现要点

  1. 命令行参数--template 指定模板库文件路径

    yaml2pptx.py convert input.yaml --template ./templates.yaml
    
  2. 模板库加载:在 Presentation.__init__ 中加载

    def __init__(self, yaml_path, template_file=None):
        self.template_file = template_file
        self.template_library = None
        self.template_base_dir = None
    
        if template_file:
            self.template_base_dir = template_file.parent
            self.template_library = load_yaml_file(template_file)
            validate_template_library_yaml(self.template_library, str(template_file))
    
  3. 模板查询:从模板库字典查找

    def _external_template_exists(self, template_name):
        if not self.template_library:
            return False
        return template_name in self.template_library.get('templates', {})
    
    def _get_external_template(self, template_name):
        if not self.template_library:
            raise YAMLError(f"未指定模板库文件")
    
        templates = self.template_library.get('templates', {})
        if template_name not in templates:
            raise YAMLError(f"模板库中找不到模板: {template_name}")
    
        return Template.from_data(
            templates[template_name],
            template_name,
            base_dir=self.template_base_dir
        )
    
  4. 资源路径解析:统一在渲染时解析

    • 外部模板元素:相对于模板库文件所在目录(template_base_dir
    • 内联模板元素:相对于文档 YAML 所在目录(pres_base_dir
    • 自定义元素:相对于文档 YAML 所在目录(pres_base_dir
    • Template.render()Presentation.render_slide() 中实现
  5. 验证validate_template_library_yaml() 验证模板库结构

    def validate_template_library_yaml(data, file_path):
        # 检查必需的 templates 字段
        if 'templates' not in data:
            raise YAMLError(f"模板库文件缺少 'templates' 字段: {file_path}")
    
        if not isinstance(data['templates'], dict):
            raise YAMLError(f"'templates' 必须是字典类型: {file_path}")
    
        # 递归验证每个模板
        for name, template_data in data['templates'].items():
            validate_template_yaml(template_data, f"{file_path}::{name}")
    
  6. 错误类型区分

    • TEMPLATE_FILE_NOT_SPECIFIED: 使用模板但未指定 --template
    • TEMPLATE_LIBRARY_FILE_NOT_FOUND: 模板库文件不存在
    • TEMPLATE_NOT_FOUND_IN_LIBRARY: 模板名称在模板库中不存在
    • TEMPLATE_NAME_CONFLICT: 内联和外部模板同名WARNING

架构优势

  • 统一数据结构:内联和外部模板使用相同的字典格式
  • 统一查询接口:get_template() 方法处理两种模板
  • 统一资源解析:通过 base_dir 参数统一处理路径
  • 简化分发:单个文件包含所有模板
  • 支持元数据:模板库和模板都可以有 description
  • 移除缓存:简化代码,模板库加载开销小

8. description 字段

决策:为 metadata、模板和幻灯片添加可选的 description 字段

理由

  • 自文档化:提高模板和演示文稿的可读性和可维护性
  • 备注支持:幻灯片 description 会写入 PPT 备注页,方便演讲者查看
  • 完全向后兼容:字段为可选,现有文件无需修改

实现要点

  1. 数据模型

    • Presentation.description:从 metadata.description 读取,用于描述整个演示文稿
    • Template.description:从模板文件的 description 字段读取,描述模板用途
    • Slide.description:在 render_slide() 返回值中保留,会写入 PPT 备注页
  2. YAML 解析

    • 使用 .get('description') 自动处理缺失情况(返回 None
    • YAML 原生支持多行文本格式
  3. PPT 备注功能

    • 仅幻灯片级别的 description 会写入 PPT 备注页
    • 模板的 description 不继承到幻灯片备注
    • metadata.description 不写入单个幻灯片备注
    • 如果幻灯片没有 description,则不设置备注
  4. 渲染实现

    • PptxGenerator._set_notes():设置幻灯片备注的私有方法
    • PptxGenerator.add_slide():调用 _set_notes() 设置备注

示例

# metadata description - 描述整个演示文稿
metadata:
  description: "2024年度项目进展总结"

# 模板 description - 描述模板用途
templates:
  title-slide:
    description: "用于章节标题页的模板"
    elements: [...]

# 幻灯片 description - 写入 PPT 备注页
slides:
  - description: "介绍项目背景和目标,包含以下要点:..."
    template: title-slide
    vars:
      title: "项目背景"

扩展指南

添加新元素类型

假设要添加 VideoElement

1. 在 core/elements.py 中定义数据类

@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. 在工厂函数中添加分支

def create_element(elem_dict: dict):
    elem_type = elem_dict.get('type')
    # ... 其他类型 ...
    elif elem_type == 'video':
        return VideoElement(**elem_dict)

3. 在 PptxGenerator 中实现渲染方法

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 中实现渲染方法

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

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 模式

from renderers.pdf_renderer import PdfRenderer

def main():
    # ... 解析参数 ...
    if args.pdf:
        # PDF 生成模式
        generator = PdfRenderer()
        # ... 渲染逻辑

测试规范

测试框架

项目使用 pytest 作为测试框架,测试代码位于 tests/ 目录。

测试结构

tests/
├── conftest.py              # pytest 配置和共享 fixtures
├── conftest_pptx.py         # PPTX 文件验证工具
├── unit/                    # 单元测试
│   ├── test_elements.py     # 元素类测试
│   ├── test_template.py     # 模板系统测试
│   ├── test_utils.py        # 工具函数测试
│   ├── test_validators/     # 验证器测试
│   │   ├── test_geometry.py
│   │   ├── test_resource.py
│   │   ├── test_result.py
│   │   └── test_validator.py
│   └── test_loaders/        # 加载器测试
│       └── test_yaml_loader.py
├── integration/             # 集成测试
│   ├── test_presentation.py
│   ├── test_rendering_flow.py
│   └── test_validation_flow.py
├── e2e/                     # 端到端测试
│   ├── test_convert_cmd.py
│   ├── test_check_cmd.py
│   └── test_preview_cmd.py
└── fixtures/                # 测试数据
    ├── yaml_samples/        # YAML 样本
    ├── templates/           # 测试模板
    └── images/              # 测试图片

运行测试

# 安装测试依赖
uv pip install -e ".[dev]"

# 运行所有测试
uv run pytest

# 运行特定类型的测试
uv run pytest tests/unit/       # 单元测试
uv run pytest tests/integration/  # 集成测试
uv run pytest tests/e2e/        # 端到端测试

# 运行特定测试文件
uv run pytest tests/unit/test_elements.py

# 显示详细输出
uv run pytest -v

# 显示测试覆盖率
uv run pytest --cov=. --cov-report=html

编写测试

测试类命名:使用 Test<ClassName> 格式

测试方法命名:使用 test_<what_is_being_tested> 格式

class TestTextElement:
    """TextElement 测试类"""

    def test_create_with_defaults(self):
        """测试使用默认值创建 TextElement"""
        elem = TextElement()
        assert elem.type == 'text'

    def test_invalid_color_raises_error(self):
        """测试无效颜色会引发错误"""
        with pytest.raises(ValueError, match="无效颜色"):
            TextElement(font={"color": "red"})

Fixtures

共享 fixtures 定义在 tests/conftest.py 中:

  • temp_dir: 临时目录
  • sample_yaml: 最小测试 YAML 文件
  • sample_image: 测试图片
  • sample_template: 测试模板
  • pptx_validator: PPTX 验证器
def test_with_fixture(sample_yaml):
    """使用 fixture 的测试"""
    assert sample_yaml.exists()

PPTX 验证

使用 PptxFileValidator 验证生成的 PPTX 文件:

def test_pptx_generation(temp_dir, pptx_validator):
    """测试 PPTX 生成"""
    # ... 生成 PPTX ...
    output_path = temp_dir / "output.pptx"

    # 验证文件
    assert pptx_validator.validate_file(output_path) is True

    # 验证内容
    prs = Presentation(str(output_path))
    assert pptx_validator.validate_text_element(
        prs.slides[0],
        index=0,
        expected_content="Test"
    ) is True

手动测试

# 验证 YAML 文件
uv run yaml2pptx.py check temp/test.yaml

# 使用模板时验证
uv run yaml2pptx.py check temp/demo.yaml --template-dir temp/templates

# 转换 YAML 为 PPTX
uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx

# 自动生成输出文件名
uv run yaml2pptx.py convert temp/test.yaml

# 跳过自动验证
uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --skip-validation

# 强制覆盖已存在文件
uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --force

# 使用模板
uv run yaml2pptx.py convert temp/demo.yaml temp/output.pptx --template-dir temp/templates

# 启动预览服务器
uv run yaml2pptx.py preview temp/test.yaml

# 指定端口
uv run yaml2pptx.py preview temp/test.yaml --port 8080

# 允许局域网访问
uv run yaml2pptx.py preview temp/test.yaml --host 0.0.0.0

# 不自动打开浏览器
uv run yaml2pptx.py preview temp/test.yaml --no-browser

测试文件位置

  • 自动化测试tests/ 目录
  • 手动测试文件temp/ 目录
    • temp/*.yaml - 手动测试用的 YAML 文件
    • temp/*.pptx - 生成的 PPTX 文件
    • temp/templates/ - 手动测试用的模板文件

常见问题

Q: 为什么不能直接使用 python 运行脚本?

A: 项目使用 uv 和 pyproject.toml 来管理依赖。直接使用 python 会导致依赖缺失。必须使用 uv run yaml2pptx.py

Q: 如何添加新的依赖?

A: 在 pyproject.toml[project.dependencies] 中添加:

[project]
dependencies = [
    "python-pptx",
    "pyyaml",
    "flask",
    "watchdog",
    "new-dependency",  # 添加新依赖
]

Q: 为什么元素使用 dataclass 而不是普通字典?

A: dataclass 提供:

  1. 类型安全和 IDE 支持
  2. 自动生成的方法(__init__, __repr__
  3. 创建时验证(__post_init__
  4. 更好的可维护性和可扩展性

Q: 如何调试渲染问题?

A: 使用预览模式:

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 高效监听文件变化