diff --git a/README.md b/README.md index c1fe5d9..a2a77ac 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ 使用 YAML 声明式语法创建 PowerPoint 演示文稿的工具。 -## ✨ 功能特性 +## 功能特性 -- - **YAML 声明式语法** - 使用简单易读的 YAML 定义演示文稿 -- - **智能验证** - 转换前自动检查 YAML 文件,提前发现问题 -- - **模板系统** - 支持参数化模板,复用幻灯片布局 -- - **丰富的元素类型** - 文本、图片、形状、表格 -- - **实时预览** - 浏览器预览模式,支持热重载 -- - **灵活尺寸** - 支持 16:9 和 4:3 两种宽高比 -- - **模块化架构** - 易于扩展和维护 +- **YAML 声明式语法** - 使用简单易读的 YAML 定义演示文稿 +- **智能验证** - 转换前自动检查 YAML 文件,提前发现问题 +- **模板系统** - 支持参数化模板,复用幻灯片布局 +- **丰富的元素类型** - 文本、图片、形状、表格 +- **实时预览** - 浏览器预览模式,支持热重载 +- **灵活尺寸** - 支持 16:9 和 4:3 两种宽高比 +- **模块化架构** - 易于扩展和维护 ## 快速开始 @@ -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 ./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,18 +71,140 @@ slides: font: size: 44 bold: true - color: "#333333" - align: center ``` -#### description 字段 +### 元素类型 -`metadata.description` 字段用于描述整个演示文稿的概要和用途,仅用于文档目的,不影响生成的 PPTX 文件: +#### 文本元素 + +```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" - description: "2024年度项目进展总结,包含背景、成果和展望" + +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 ``` ### 使用模板 @@ -135,906 +215,40 @@ metadata: slides: - template: title-slide - vars: - title: "我的演示文稿" - subtitle: "使用 yaml2pptx 创建" - author: "张三" - - - template: content-slide - vars: - title: "功能概览" - content: "yaml2pptx 支持多种元素类型..." -``` - -## - 元素类型 - -### 文本元素 - -```yaml -- type: text - box: [x, y, width, height] # 位置和尺寸(英寸) - content: "文本内容" - font: - size: 18 # 字号(磅) - bold: true # 粗体 - italic: false # 斜体 - color: "#ff0000" # 颜色 - align: center # left/center/right -``` - -**特性**:文本框默认启用自动换行,文字超出宽度时会自动换行。 - -### 图片元素 - -```yaml -- type: image - box: [x, y, width, height] - src: "path/to/image.png" # 支持相对路径和绝对路径 -``` - -**示例**: - -```yaml -slides: - - elements: - - type: image - src: "photo.jpg" - box: [1, 1, 4, 3] -``` - -### 形状元素 - -```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"] - font: - family: "Arial" - size: 14 - color: "#333333" - header_font: - bold: true - color: "#ffffff" - style: - header_bg: "#4a90e2" -``` - -**字体配置**: -- `font`:表格数据单元格的字体样式 -- `header_font`:表头单元格的字体样式(未定义时继承 `font`) -- `style.header_bg`:表头背景色 - -## - 字体主题系统 - -字体主题系统允许你定义可复用的字体配置,统一管理演示文稿的字体样式。 - -### 定义字体主题 - -在 `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: - 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" -``` - -## 模板系统 - -模板允许你定义可复用的幻灯片布局。yaml2pptx 支持两种模板方式: - -- **外部模板**:独立的 YAML 文件,适合跨文档复用 -- **内联模板**:在源文件中定义,适合单文档使用 - -### 内联模板 - -内联模板允许你在 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: "使用内联模板" -``` - -#### 内联模板特性 - -- - 支持变量替换和条件渲染 -- - 可以与外部模板混合使用 -- - 无需指定 `--template` 参数 -- - 内联模板不能相互引用 -- - 内联和外部模板同名时会发出警告,优先使用内联模板 - -#### 何时使用内联模板 - -**适合使用内联模板**: -- 模板仅在单个文档中使用 -- 快速原型开发 -- 简单的模板定义(1-3 个元素) -- 文档自包含,无需外部依赖 - -**适合使用外部模板**: -- 需要跨多个文档复用 -- 复杂的模板定义(>5 个元素) -- 团队共享的模板库 -- 需要版本控制和独立维护 - -**最佳实践**: - -1. **命名规范**: - - 内联模板使用描述性名称(如 `title-slide`, `content-slide`) - - 避免与外部模板同名,否则会报错 - - 使用一致的命名风格(kebab-case 推荐) - -2. **模板大小**: - - 内联模板建议不超过 50 行 - - 超过 50 行考虑拆分或使用外部模板 - - 保持 YAML 文件可读性 - -3. **混合使用**: - - 可以在同一文档中混合使用内联和外部模板 - - 通用模板使用外部模板(如标题页、结束页) - - 文档特定模板使用内联模板 - -4. **迁移策略**: - - 原型阶段使用内联模板快速迭代 - - 模板稳定后,如需复用则迁移到外部模板 - - 使用 `--template` 参数指定外部模板库文件 - -#### 内联模板限制 - -- - 内联模板不能相互引用(会报错) -- - 内联和外部模板同名时会发出警告,优先使用内联模板 -- - 内联模板不支持继承或组合 - -### 外部模板库 - -外部模板库是一个包含多个模板的 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 -- - 便于版本控制和分发 -- - 支持相对路径的图片资源(相对于模板库文件所在目录) +**用户文档** -#### 模板库文件结构 +- [用户指南](docs/user-guide.md) - 完整使用指南 +- [元素类型](docs/elements/) - 文本、图片、形状、表格 +- [模板系统](docs/templates/) - 内联模板、外部模板库、混合模式 +- [字体主题](docs/fonts.md) - 字体配置和主题管理 +- [API 参考](docs/reference/) - 命令、坐标、颜色、验证 +- [示例集合](docs/examples.md) - 实战示例 +- [故障排查](docs/troubleshooting.md) - 常见问题 -```yaml -# 顶层元数据(可选) -description: "模板库描述" -version: "版本号" -author: "作者" +**开发文档** -# 模板定义(必需) -templates: - 模板名称1: - description: "模板描述(可选)" - vars: [...] - elements: [...] +- [架构设计](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) - 测试指南 - 模板名称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 -``` - -### 混合模式模板 - -混合模式允许你在使用模板的同时添加自定义元素,实现更灵活的布局组合。 - -#### 基本用法 - -在使用模板的幻灯片中,同时指定 `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] - data: [[...]] - - # 页面 3:头部 + 图片 - - template: standard-header - vars: - title: "图片页面" - elements: - - type: image - box: [2, 1.5, 6, 3.5] - src: "chart.png" -``` - -#### 向后兼容性 - -混合模式完全向后兼容: - -- **纯模板模式**:只指定 `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` 用于描述整个演示文稿,不写入单个幻灯片备注 - -### 条件渲染 - -#### 元素级条件渲染 - -使用 `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} -``` - -## 🎯 命令行选项 - -### check 命令 - -验证 YAML 文件的正确性。 - -| 选项 | 说明 | -|------|------| -| `input` | 输入的 YAML 文件路径(必需) | -| `--template` | 模板库文件路径 | - -### convert 命令 - -将 YAML 文件转换为 PPTX 文件。 - -| 选项 | 说明 | -|------|------| -| `input` | 输入的 YAML 文件路径(必需) | -| `output` | 输出的 PPTX 文件路径(可选) | -| `--template` | 模板库文件路径 | -| `--skip-validation` | 跳过自动验证 | -| `--force` / `-f` | 强制覆盖已存在文件 | - -### preview 命令 - -启动预览服务器,实时查看演示文稿效果。 - -| 选项 | 说明 | -|------|------| -| `input` | 输入的 YAML 文件路径(必需) | -| `--template` | 模板库文件路径 | -| `--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` 参数 -5. **文本换行**:文本框默认启用自动换行,无需手动处理长文本 +4. **文本换行**:文本框默认启用自动换行,无需手动处理长文本 -## 📚 完整示例 - -查看 `temp/` 目录下的示例文件: -- `temp/test_refactor.yaml` - 基本示例 -- `temp/template_demo.yaml` - 模板使用示例 -- `temp/complex_presentation.yaml` - 复杂演示文稿示例 - -## - 常见错误 - -| 错误信息 | 原因 | 解决方法 | -|---------|------|---------| -| `文件不存在: xxx.yaml` | 找不到输入文件 | 检查文件路径是否正确 | -| `YAML 语法错误: 第 X 行` | YAML 格式错误 | 检查缩进和语法 | -| `模板文件不存在: xxx` | 模板文件未找到 | 检查模板名称和 `--template` | -| `缺少必需变量: xxx` | 未提供必需的模板变量 | 在 `vars` 中提供该变量 | -| `图片文件未找到: xxx` | 图片文件不存在 | 检查图片路径 | - -## - 扩展性 +## 扩展性 yaml2pptx 采用模块化架构,易于扩展: @@ -1042,9 +256,9 @@ yaml2pptx 采用模块化架构,易于扩展: - **添加新渲染器**:支持输出到其他格式(如 PDF) - **自定义模板**:创建符合你需求的模板库 -详见 [开发文档](README_DEV.md)。 +详见 [开发文档](docs/development/extending.md)。 -## 📦 依赖项 +## 依赖项 - `python-pptx` - PowerPoint 文件生成 - `pyyaml` - YAML 解析 @@ -1053,16 +267,9 @@ yaml2pptx 采用模块化架构,易于扩展: 依赖在 pyproject.toml 中声明,由 uv 自动管理,无需手动安装。 -## 🧪 测试 - -项目包含完整的测试套件,使用 pytest 框架。 - -### 运行测试 +## 测试 ```bash -# 安装测试依赖 -uv pip install -e ".[dev]" - # 运行所有测试 uv run pytest @@ -1070,33 +277,14 @@ 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/ # 测试数据 -``` - -## 🤝 贡献 +## 贡献 欢迎贡献代码、报告问题或提出建议! -开发者请参阅 [开发文档](README_DEV.md) 了解代码结构和开发规范。 +开发者请参阅 [开发文档](docs/development/) 了解代码结构和开发规范。 -## 📄 许可证 +## 许可证 MIT License diff --git a/README_DEV.md b/README_DEV.md deleted file mode 100644 index 7599121..0000000 --- a/README_DEV.md +++ /dev/null @@ -1,1249 +0,0 @@ -# 开发文档 - -本文档说明 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. **混合模式**:同时有 `template` 和 `elements` 字段(新功能) - - 先渲染模板元素 - - 自定义元素使用模板变量解析(通过 `Template.resolve_element()`) - - 合并策略:简单追加(`template_elements + custom_elements`) - - z 轴顺序:模板元素在底层,自定义元素在上层 - -#### 元素合并策略 - -混合模式采用简单追加策略: - -```python -# 步骤1:渲染模板(如果有) -elements_from_template = template.render(vars_values) - -# 步骤2:处理自定义元素(如果有) -if has_custom_elements and has_template: - # 使用模板变量解析自定义元素 - elements_from_custom = [ - template.resolve_element(elem, vars_values) - for elem in custom_elements - ] - -# 步骤3:合并元素(模板在前,自定义在后) -final_elements = elements_from_template + elements_from_custom -``` - -**设计决策**: -- 不支持按 key/id 合并(避免引入元素 ID 概念) -- 不支持位置感知合并(保持简单) -- 不检测元素重叠(用户通过预览模式查看效果) -- 空 `elements: []` 等同于不指定 `elements` - -### 7. renderers/pptx_renderer.py(渲染层 - PPTX) -- **职责**:PPTX 文件生成 -- **包含**: - - `PptxGenerator` 类 - - 渲染方法:`_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()` - - 元素分发:`_render_element()` -- **特点**: - - 渲染器内置在生成器中 - - 使用 `isinstance()` 检查元素类型 - - 通过元素对象的属性访问数据 - -### 8. renderers/html_renderer.py(渲染层 - HTML) -- **职责**:HTML 预览渲染 -- **包含**: - - `HtmlRenderer` 类 - - 渲染方法:`render_text()`, `render_image()`, `render_shape()`, `render_table()` -- **特点**: - - 与 PptxRenderer 共享元素抽象层 - - 使用固定 DPI (96) 进行单位转换 - -### 9. preview/server.py(预览层) -- **职责**:浏览器预览和热重载 -- **包含**: - - Flask 应用:`create_flask_app()` - - 文件监听:`YAMLChangeHandler` - - 预览服务器:`start_preview_server()` - - HTML 模板:`HTML_TEMPLATE`, `ERROR_TEMPLATE` - -## 开发规范 - -### 1. Python 环境 - -**必须使用 uv 运行脚本**: -```bash -# 正确 -uv run yaml2pptx.py input.yaml output.pptx - -# 错误 - 严禁直接使用主机环境的 python -python yaml2pptx.py input.yaml output.pptx -``` - -**依赖管理**: -- 所有依赖在 `pyproject.toml` 的 `[project.dependencies]` 中声明 -- uv 会自动安装依赖,无需手动 `pip install` - -### 2. 命令行接口 - -**子命令架构**: -```bash -# check - 验证 YAML 文件 -uv run yaml2pptx.py check [--template-dir ] - -# convert - 转换为 PPTX -uv run yaml2pptx.py convert [output] [--template-dir ] [--skip-validation] [--force] - -# preview - 启动预览服务器 -uv run yaml2pptx.py preview [--template-dir ] [--port ] [--host ] [--no-browser] -``` - -**参数说明**: -- `--template-dir`:所有命令通用,指定模板目录 -- `--skip-validation`:convert 专用,跳过自动验证 -- `--force/-f`:convert 专用,强制覆盖已存在文件 -- `--port`:preview 专用,指定端口(默认随机 30000-40000) -- `--host`:preview 专用,指定主机地址(默认 127.0.0.1) -- `--no-browser`:preview 专用,不自动打开浏览器 - -### 3. 文件组织 - -**代码文件**: -- 每个模块文件控制在 150-300 行 -- 入口脚本约 200 行 -- 使用有意义的文件名和目录结构 - -**测试文件**: -- 所有测试文件、临时文件必须放在 `temp/` 目录下 -- 不污染项目根目录 - -### 4. 代码风格 - -**导入顺序**: -```python -# 1. 标准库 -import sys -from pathlib import Path - -# 2. 第三方库 -import yaml -from pptx import Presentation - -# 3. 本地模块 -from utils import log_info -from core.elements import TextElement -``` - -**文档字符串**: -- 每个模块必须有模块级文档字符串 -- 每个类和函数必须有文档字符串 -- 使用中文编写注释和文档 - -**命名规范**: -- 模块名:小写 + 下划线(如 `yaml_loader.py`) -- 类名:大驼峰(如 `TextElement`) -- 函数名:小写 + 下划线(如 `load_yaml_file()`) -- 常量:大写 + 下划线(如 `HTML_TEMPLATE`) - -## 技术决策 - -### 1. 元素抽象层使用 dataclass - -**决策**:使用 Python dataclass 定义元素数据类 - -**理由**: -- 简洁性:自动生成 `__init__`、`__repr__` 等方法 -- 类型提示:支持类型注解,IDE 友好 -- 验证时机:`__post_init__` 在创建时自动调用 -- 可扩展性:未来可以添加方法 - -**示例**: -```python -@dataclass -class TextElement: - type: str = 'text' - content: str = '' - box: list = field(default_factory=lambda: [1, 1, 8, 1]) - font: dict = field(default_factory=dict) - - def __post_init__(self): - if len(self.box) != 4: - raise ValueError("box 必须包含 4 个数字") -``` - -### 2. 渲染器内置在生成器中 - -**决策**:将渲染逻辑内置在 PptxGenerator 和 HtmlRenderer 类中 - -**理由**: -- 封装性:渲染逻辑与生成器紧密相关 -- 简单性:不需要额外的渲染器接口 -- 性能:避免额外的方法调用开销 - -**示例**: -```python -class PptxGenerator: - def _render_element(self, slide, elem, base_path): - if isinstance(elem, TextElement): - self._render_text(slide, elem) - elif isinstance(elem, ImageElement): - self._render_image(slide, elem, base_path) -``` - -### 3. 创建时验证 - -**决策**:在元素对象创建时进行验证(`__post_init__` 方法) - -**理由**: -- 尽早失败:在数据进入系统时就发现错误 -- 清晰的错误位置:堆栈指向元素创建处 -- 避免无效状态:确保元素对象始终有效 - -### 4. 元素工厂函数 - -**决策**:提供 `create_element(elem_dict)` 工厂函数 - -**理由**: -- 统一入口:所有元素创建都通过工厂函数 -- 类型安全:进行类型检查 -- 易于扩展:添加新元素类型只需添加一个分支 - -### 5. 验证职责分层 - -**决策**:将验证逻辑分为两层 -1. **元素级验证**:放在元素类本身(`core/elements.py`) -2. **系统级验证**:放在独立的验证器模块(`validators/`) - -**理由**: -- 元素类最了解自己的约束,应该负责自身的完整性验证 -- 系统级验证需要全局上下文(如页面尺寸、文件路径),适合集中处理 -- 符合单一职责原则,便于扩展和维护 - -**元素级验证职责**: -- 必需字段检查(在 `__post_init__` 中) -- 数据类型检查(在 `__post_init__` 中) -- 值的有效性检查(在 `validate()` 方法中) - - 颜色格式验证 - - 字体大小合理性 - - 枚举值检查(如形状类型) - - 表格数据一致性 - -**系统级验证职责**: -- 几何验证(元素是否在页面范围内,需要知道页面尺寸) -- 资源验证(文件是否存在,需要知道文件路径) -- 跨元素验证(如果未来需要) - -**示例**: -```python -# 元素级验证(在元素类中) -@dataclass -class TextElement: - def validate(self) -> List[ValidationIssue]: - issues = [] - if self.font.get('color'): - if not _is_valid_color(self.font['color']): - issues.append(ValidationIssue( - level="ERROR", - message=f"无效的颜色格式: {self.font['color']}", - code="INVALID_COLOR_FORMAT" - )) - return issues - -# 系统级验证(在验证器中) -class GeometryValidator: - def validate_element(self, element, slide_index, elem_index): - # 需要页面尺寸信息 - if element.box[0] + element.box[2] > self.slide_width: - # 报告边界超出 -``` - -### 6. 验证容忍度 - -**决策**:几何验证时,允许 0.1 英寸的容忍度 - -**理由**: -- 浮点数计算可能有精度误差 -- 0.1 英寸(约 2.54mm)在视觉上几乎不可见 -- 避免误报,提升用户体验 - -**实现**: -```python -TOLERANCE = 0.1 # 英寸 - -if right > slide_width + TOLERANCE: - # 报告 WARNING -``` - -### 7. 模板系统架构 - -yaml2pptx 支持两种模板方式:内联模板和外部模板库。 - -#### 7.1 内联模板系统 - -**决策**:支持在 YAML 源文件中定义内联模板,与外部模板系统共存 - -**理由**: -- 降低使用门槛:简单场景无需创建单独的模板文件 -- 保持灵活性:复杂场景仍可使用外部模板 -- 向后兼容:不影响现有外部模板功能 - -**实现要点**: - -1. **模板定义**:在 YAML 顶层添加 `templates` 字段 - ```yaml - templates: - my-template: - vars: [...] - elements: [...] - ``` - -2. **模板创建**:使用 `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 - ``` - -3. **模板查找**:`Presentation.get_template()` 优先查找内联模板 - ```python - 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 等元数据 -- 统一查询:外部模板和内联模板使用相同的查询接口 - -**模板库文件格式**: - -```yaml -# 模板库元数据(可选) -description: "公司标准模板库" -version: "1.0.0" -author: "设计团队" - -# 模板定义(必需) -templates: - template-name-1: - description: "模板描述(可选)" - vars: [...] - elements: [...] - - template-name-2: - description: "模板描述(可选)" - vars: [...] - elements: [...] -``` - -**实现要点**: - -1. **命令行参数**:`--template` 指定模板库文件路径 - ```bash - yaml2pptx.py convert input.yaml --template ./templates.yaml - ``` - -2. **模板库加载**:在 `Presentation.__init__` 中加载 - ```python - 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. **模板查询**:从模板库字典查找 - ```python - 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()` 验证模板库结构 - ```python - 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()` 设置备注 - -**示例**: -```yaml -# 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 中定义数据类**: -```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'' -``` - -### 添加新渲染器 - -假设要添加 PDF 渲染器: - -**1. 创建 renderers/pdf_renderer.py**: -```python -from core.elements import TextElement, ImageElement, ShapeElement, TableElement - -class PdfRenderer: - def __init__(self): - # 初始化 PDF 库 - pass - - def add_slide(self, slide_data, base_path=None): - # 添加页面 - pass - - def _render_element(self, page, elem, base_path): - if isinstance(elem, TextElement): - self._render_text(page, elem) - # ... 其他元素类型 -``` - -**2. 在 yaml2pptx.py 中添加 PDF 模式**: -```python -from renderers.pdf_renderer import PdfRenderer - -def main(): - # ... 解析参数 ... - if args.pdf: - # PDF 生成模式 - generator = PdfRenderer() - # ... 渲染逻辑 -``` - -## 测试规范 - -### 测试框架 - -项目使用 pytest 作为测试框架,测试代码位于 `tests/` 目录。 - -### 测试结构 - -``` -tests/ -├── conftest.py # pytest 配置和共享 fixtures -├── conftest_pptx.py # PPTX 文件验证工具 -├── unit/ # 单元测试 -│ ├── test_elements.py # 元素类测试 -│ ├── test_template.py # 模板系统测试 -│ ├── test_utils.py # 工具函数测试 -│ ├── test_validators/ # 验证器测试 -│ │ ├── test_geometry.py -│ │ ├── test_resource.py -│ │ ├── test_result.py -│ │ └── test_validator.py -│ └── test_loaders/ # 加载器测试 -│ └── test_yaml_loader.py -├── integration/ # 集成测试 -│ ├── test_presentation.py -│ ├── test_rendering_flow.py -│ └── test_validation_flow.py -├── e2e/ # 端到端测试 -│ ├── test_convert_cmd.py -│ ├── test_check_cmd.py -│ └── test_preview_cmd.py -└── fixtures/ # 测试数据 - ├── yaml_samples/ # YAML 样本 - ├── templates/ # 测试模板 - └── images/ # 测试图片 -``` - -### 运行测试 - -```bash -# 安装测试依赖 -uv pip install -e ".[dev]" - -# 运行所有测试 -uv run pytest - -# 运行特定类型的测试 -uv run pytest tests/unit/ # 单元测试 -uv run pytest tests/integration/ # 集成测试 -uv run pytest tests/e2e/ # 端到端测试 - -# 运行特定测试文件 -uv run pytest tests/unit/test_elements.py - -# 显示详细输出 -uv run pytest -v - -# 显示测试覆盖率 -uv run pytest --cov=. --cov-report=html -``` - -### 编写测试 - -**测试类命名**:使用 `Test` 格式 - -**测试方法命名**:使用 `test_` 格式 - -```python -class TestTextElement: - """TextElement 测试类""" - - def test_create_with_defaults(self): - """测试使用默认值创建 TextElement""" - elem = TextElement() - assert elem.type == 'text' - - def test_invalid_color_raises_error(self): - """测试无效颜色会引发错误""" - with pytest.raises(ValueError, match="无效颜色"): - TextElement(font={"color": "red"}) -``` - -### Fixtures - -共享 fixtures 定义在 `tests/conftest.py` 中: - -- `temp_dir`: 临时目录 -- `sample_yaml`: 最小测试 YAML 文件 -- `sample_image`: 测试图片 -- `sample_template`: 测试模板 -- `pptx_validator`: PPTX 验证器 - -```python -def test_with_fixture(sample_yaml): - """使用 fixture 的测试""" - assert sample_yaml.exists() -``` - -### PPTX 验证 - -使用 `PptxFileValidator` 验证生成的 PPTX 文件: - -```python -def test_pptx_generation(temp_dir, pptx_validator): - """测试 PPTX 生成""" - # ... 生成 PPTX ... - output_path = temp_dir / "output.pptx" - - # 验证文件 - assert pptx_validator.validate_file(output_path) is True - - # 验证内容 - prs = Presentation(str(output_path)) - assert pptx_validator.validate_text_element( - prs.slides[0], - index=0, - expected_content="Test" - ) is True -``` - -### 手动测试 - -```bash -# 验证 YAML 文件 -uv run yaml2pptx.py check temp/test.yaml - -# 使用模板时验证 -uv run yaml2pptx.py check temp/demo.yaml --template-dir temp/templates - -# 转换 YAML 为 PPTX -uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx - -# 自动生成输出文件名 -uv run yaml2pptx.py convert temp/test.yaml - -# 跳过自动验证 -uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --skip-validation - -# 强制覆盖已存在文件 -uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --force - -# 使用模板 -uv run yaml2pptx.py convert temp/demo.yaml temp/output.pptx --template-dir temp/templates - -# 启动预览服务器 -uv run yaml2pptx.py preview temp/test.yaml - -# 指定端口 -uv run yaml2pptx.py preview temp/test.yaml --port 8080 - -# 允许局域网访问 -uv run yaml2pptx.py preview temp/test.yaml --host 0.0.0.0 - -# 不自动打开浏览器 -uv run yaml2pptx.py preview temp/test.yaml --no-browser -``` - -### 测试文件位置 - -- **自动化测试**:`tests/` 目录 -- **手动测试文件**:`temp/` 目录 - - `temp/*.yaml` - 手动测试用的 YAML 文件 - - `temp/*.pptx` - 生成的 PPTX 文件 - - `temp/templates/` - 手动测试用的模板文件 - -## 常见问题 - -### Q: 为什么不能直接使用 python 运行脚本? - -A: 项目使用 uv 和 pyproject.toml 来管理依赖。直接使用 python 会导致依赖缺失。必须使用 `uv run yaml2pptx.py`。 - -### Q: 如何添加新的依赖? - -A: 在 `pyproject.toml` 的 `[project.dependencies]` 中添加: -```toml -[project] -dependencies = [ - "python-pptx", - "pyyaml", - "flask", - "watchdog", - "new-dependency", # 添加新依赖 -] -``` - -### Q: 为什么元素使用 dataclass 而不是普通字典? - -A: dataclass 提供: -1. 类型安全和 IDE 支持 -2. 自动生成的方法(`__init__`, `__repr__`) -3. 创建时验证(`__post_init__`) -4. 更好的可维护性和可扩展性 - -### Q: 如何调试渲染问题? - -A: 使用预览模式: -```bash -uv run yaml2pptx.py preview temp/test.yaml -``` -在浏览器中查看渲染结果,支持热重载。 - -## 项目约束 - -1. **面向中文开发者**:注释、文档、错误消息使用中文 -2. **使用 uv 运行**:严禁直接使用主机环境的 python -3. **测试文件隔离**:所有测试文件放在 `temp/` 目录 -4. **不污染主机环境**:不修改主机的 Python 配置 - -## 字体作用域系统 - -### 概述 - -字体作用域系统实现了文档和模板库之间的字体隔离和跨域引用控制,确保字体引用的安全性和可维护性。 - -### 作用域定义 - -系统定义了两个字体作用域: - -1. **文档作用域(document)**: - - 包含文档的 `metadata.fonts` 中定义的字体 - - 文档的 `fonts_default` 只能引用文档作用域的字体 - - 文档元素可以引用文档作用域和模板库作用域的字体 - -2. **模板库作用域(template)**: - - 包含模板库的 `metadata.fonts` 中定义的字体 - - 模板库的 `fonts_default` 只能引用模板库作用域的字体 - - 模板元素只能引用模板库作用域的字体(不能引用文档字体) - -### 跨域引用规则 - -**允许的引用**: -- ✅ 文档元素 → 文档字体 -- ✅ 文档元素 → 模板库字体(跨域引用) -- ✅ 模板元素 → 模板库字体 -- ✅ 文档 fonts_default → 文档字体 -- ✅ 模板库 fonts_default → 模板库字体 - -**禁止的引用**: -- ❌ 模板元素 → 文档字体(跨域引用被禁止) -- ❌ 模板库 fonts_default → 文档字体(跨域引用被禁止) -- ❌ 文档 fonts_default → 模板库字体(跨域引用被禁止) - -**设计理由**: -- 模板库应该是自包含的,不依赖特定文档的字体配置 -- 文档可以引用模板库字体,实现样式复用 -- 防止模板库与文档之间的紧耦合 - -### FontResolver 实现 - -`utils/font_resolver.py` 中的 `FontResolver` 类实现了作用域控制: - -```python -class FontResolver: - def __init__(self, fonts, fonts_default, scope="document", template_fonts=None): - """ - Args: - fonts: 当前作用域的字体字典 - fonts_default: 当前作用域的默认字体 - scope: 作用域标识 ("document" 或 "template") - template_fonts: 模板库字体字典(仅文档作用域需要) - """ -``` - -**跨域引用检测**: -- 使用作用域标签(`doc.@font-name` 或 `template.@font-name`)追踪引用路径 -- 检测跨域循环引用(如 `doc.@a → template.@b → doc.@a`) -- 在 `parent` 引用时根据作用域限制跨域访问 - -### fonts_default 级联规则 - -当元素未指定字体时,按以下顺序查找默认字体: - -1. **模板元素**: - - 模板库的 `fonts_default`(如果存在) - - 文档的 `fonts_default`(如果存在) - - 系统默认字体 - -2. **文档元素**: - - 文档的 `fonts_default`(如果存在) - - 系统默认字体 - -**实现位置**: -- `core/template.py` - Template.render() 方法 -- `core/presentation.py` - Presentation.render_slide() 方法 - -### 循环引用检测 - -系统检测两种循环引用: - -1. **单域内循环**: - ```yaml - fonts: - a: - parent: "@b" - b: - parent: "@a" - ``` - 错误信息:`检测到字体引用循环: doc.@a -> doc.@b -> doc.@a` - -2. **跨域循环**: - ```yaml - # 文档 - fonts: - doc-font: - parent: "@template-font" - - # 模板库 - fonts: - template-font: - parent: "@doc-font" # 这会被禁止 - ``` - 错误信息:`检测到跨域字体引用循环: doc.@doc-font -> template.@template-font -> doc.@doc-font` - -### 错误代码 - -字体作用域系统相关的错误代码(定义在 `validators/result.py`): - -#### 模板库 metadata 相关 -- `TEMPLATE_LIBRARY_MISSING_METADATA` - 模板库缺少 metadata 字段 -- `TEMPLATE_LIBRARY_METADATA_MISSING_SIZE` - 模板库 metadata 缺少 size 字段 -- `TEMPLATE_LIBRARY_METADATA_INVALID_SIZE` - 模板库 metadata.size 值无效(必须是 "16:9" 或 "4:3") - -#### 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 - fonts_default: "@template-title" - -templates: - title-slide: - elements: - - type: text - content: "{title}" - # 未指定 font,使用模板库的 fonts_default - -# presentation.yaml(文档) -metadata: - size: "16:9" - fonts: - doc-body: - family: "sans" - size: 18 - fonts_default: "@doc-body" - -slides: - - template: title-slide - vars: - title: "标题" - - - 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" # ❌ 模板元素不能引用文档字体 -``` - - -## 维护指南 - -### 代码审查要点 - -- [ ] 模块文件大小合理(150-300 行) -- [ ] 无循环依赖 -- [ ] 所有类和函数有文档字符串 -- [ ] 使用中文注释 -- [ ] 元素验证在 `__post_init__` 中完成 -- [ ] 导入语句按标准库、第三方库、本地模块排序 -- [ ] 测试文件在 `temp/` 目录下 - -### 性能优化建议 - -1. **模板缓存**:Presentation 类已实现模板缓存 -2. **元素验证**:只在创建时验证一次,渲染时不再验证 -3. **文件监听**:预览模式使用 watchdog 高效监听文件变化 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..0c3908f --- /dev/null +++ b/docs/README.md @@ -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) diff --git a/docs/development/architecture.md b/docs/development/architecture.md new file mode 100644 index 0000000..6d1b5b1 --- /dev/null +++ b/docs/development/architecture.md @@ -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) diff --git a/docs/development/development-guide.md b/docs/development/development-guide.md new file mode 100644 index 0000000..a9fd55d --- /dev/null +++ b/docs/development/development-guide.md @@ -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 [--template ] + +# convert - 转换为 PPTX +uv run yaml2pptx.py convert [output] [--template ] [--skip-validation] [--force] + +# preview - 启动预览服务器 +uv run yaml2pptx.py preview [--template ] [--port ] [--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) diff --git a/docs/development/extending.md b/docs/development/extending.md new file mode 100644 index 0000000..883b78a --- /dev/null +++ b/docs/development/extending.md @@ -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''' + + ''' +``` + +### 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) diff --git a/docs/development/font-system.md b/docs/development/font-system.md new file mode 100644 index 0000000..6d8e770 --- /dev/null +++ b/docs/development/font-system.md @@ -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) diff --git a/docs/development/maintenance.md b/docs/development/maintenance.md new file mode 100644 index 0000000..7ce0af8 --- /dev/null +++ b/docs/development/maintenance.md @@ -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) diff --git a/docs/development/modules/_index.md b/docs/development/modules/_index.md new file mode 100644 index 0000000..857149e --- /dev/null +++ b/docs/development/modules/_index.md @@ -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) diff --git a/docs/development/modules/elements.md b/docs/development/modules/elements.md new file mode 100644 index 0000000..4faf817 --- /dev/null +++ b/docs/development/modules/elements.md @@ -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) diff --git a/docs/development/modules/loaders.md b/docs/development/modules/loaders.md new file mode 100644 index 0000000..84c36ca --- /dev/null +++ b/docs/development/modules/loaders.md @@ -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) diff --git a/docs/development/modules/renderers.md b/docs/development/modules/renderers.md new file mode 100644 index 0000000..a10eac2 --- /dev/null +++ b/docs/development/modules/renderers.md @@ -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) diff --git a/docs/development/modules/template.md b/docs/development/modules/template.md new file mode 100644 index 0000000..8b6e993 --- /dev/null +++ b/docs/development/modules/template.md @@ -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) diff --git a/docs/development/modules/validators.md b/docs/development/modules/validators.md new file mode 100644 index 0000000..6763ec6 --- /dev/null +++ b/docs/development/modules/validators.md @@ -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) diff --git a/docs/development/scope-system.md b/docs/development/scope-system.md new file mode 100644 index 0000000..ae65ab5 --- /dev/null +++ b/docs/development/scope-system.md @@ -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) diff --git a/docs/development/testing.md b/docs/development/testing.md new file mode 100644 index 0000000..0c19aa8 --- /dev/null +++ b/docs/development/testing.md @@ -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` 格式: + +```python +class TestTextElement: + """TextElement 测试类""" + pass +``` + +### 测试方法命名 + +使用 `test_` 格式: + +```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) diff --git a/docs/elements/_index.md b/docs/elements/_index.md new file mode 100644 index 0000000..a81694c --- /dev/null +++ b/docs/elements/_index.md @@ -0,0 +1,17 @@ +# 元素类型文档 + +本目录包含各种元素类型的详细说明。 + +## 元素类型 + +- [文本元素](text.md) - 文本框和字体配置 +- [图片元素](image.md) - 图片插入和配置 +- [形状元素](shape.md) - 几何形状绘制 +- [表格元素](table.md) - 表格创建和样式 + +## 相关文档 + +- [字体主题系统](../fonts.md) - 字体配置和主题管理 +- [坐标系统](../reference/coordinates.md) - 位置和尺寸单位 + +[返回文档索引](../README.md) diff --git a/docs/elements/image.md b/docs/elements/image.md new file mode 100644 index 0000000..8975051 --- /dev/null +++ b/docs/elements/image.md @@ -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) diff --git a/docs/elements/shape.md b/docs/elements/shape.md new file mode 100644 index 0000000..fbaf1a4 --- /dev/null +++ b/docs/elements/shape.md @@ -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) diff --git a/docs/elements/table.md b/docs/elements/table.md new file mode 100644 index 0000000..4f41a11 --- /dev/null +++ b/docs/elements/table.md @@ -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) diff --git a/docs/elements/text.md b/docs/elements/text.md new file mode 100644 index 0000000..e8b2cea --- /dev/null +++ b/docs/elements/text.md @@ -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) diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..78dd5ce --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,5 @@ +# 示例集合 + +本文档为占位文件,未来将添加更多示例。 + +目前各主题文档中已内嵌代码示例供参考。 diff --git a/docs/fonts.md b/docs/fonts.md new file mode 100644 index 0000000..dcfa546 --- /dev/null +++ b/docs/fonts.md @@ -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) diff --git a/docs/reference/_index.md b/docs/reference/_index.md new file mode 100644 index 0000000..85ce759 --- /dev/null +++ b/docs/reference/_index.md @@ -0,0 +1,17 @@ +# API 参考文档 + +本目录包含各种 API 参考文档。 + +## 参考主题 + +- [命令行选项](commands.md) - 所有命令和参数 +- [坐标系统](coordinates.md) - 位置和尺寸单位 +- [颜色格式](colors.md) - 颜色表示方法 +- [验证功能](validation.md) - YAML 验证说明 + +## 相关文档 + +- [用户指南](../user-guide.md) - 完整使用说明 +- [故障排查](../troubleshooting.md) - 常见问题解决 + +[返回文档索引](../README.md) diff --git a/docs/reference/colors.md b/docs/reference/colors.md new file mode 100644 index 0000000..c41cd76 --- /dev/null +++ b/docs/reference/colors.md @@ -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) diff --git a/docs/reference/commands.md b/docs/reference/commands.md new file mode 100644 index 0000000..ed86d52 --- /dev/null +++ b/docs/reference/commands.md @@ -0,0 +1,123 @@ +# 命令行选项 + +yaml2pptx 提供三个主要命令:check、convert、preview。 + +## check 命令 + +验证 YAML 文件的正确性。 + +### 语法 + +```bash +uv run yaml2pptx.py check [--template ] +``` + +### 选项 + +| 选项 | 说明 | +|------|------| +| `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 [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 [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) diff --git a/docs/reference/coordinates.md b/docs/reference/coordinates.md new file mode 100644 index 0000000..58168f0 --- /dev/null +++ b/docs/reference/coordinates.md @@ -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) diff --git a/docs/reference/validation.md b/docs/reference/validation.md new file mode 100644 index 0000000..ef77e86 --- /dev/null +++ b/docs/reference/validation.md @@ -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) diff --git a/docs/templates/_index.md b/docs/templates/_index.md new file mode 100644 index 0000000..f0646e5 --- /dev/null +++ b/docs/templates/_index.md @@ -0,0 +1,24 @@ +# 模板系统文档 + +本目录包含模板系统的详细说明。 + +## 模板类型 + +- [内联模板](inline.md) - 在源文件中定义模板 +- [外部模板库](external-library.md) - 独立的模板库文件 +- [混合模式](mixing-mode.md) - 模板与自定义元素组合 +- [条件渲染](condition-rendering.md) - 元素和页面的条件显示 + +## 使用指南 + +- **简单场景**:使用内联模板 +- **跨文档复用**:使用外部模板库 +- **灵活布局**:使用混合模式 +- **动态内容**:使用条件渲染 + +## 相关文档 + +- [字体主题系统](../fonts.md) - 字体配置 +- [用户指南](../user-guide.md) - 完整使用说明 + +[返回文档索引](../README.md) diff --git a/docs/templates/condition-rendering.md b/docs/templates/condition-rendering.md new file mode 100644 index 0000000..99e8702 --- /dev/null +++ b/docs/templates/condition-rendering.md @@ -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) diff --git a/docs/templates/external-library.md b/docs/templates/external-library.md new file mode 100644 index 0000000..0fb885c --- /dev/null +++ b/docs/templates/external-library.md @@ -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) diff --git a/docs/templates/inline.md b/docs/templates/inline.md new file mode 100644 index 0000000..c530e36 --- /dev/null +++ b/docs/templates/inline.md @@ -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) diff --git a/docs/templates/mixing-mode.md b/docs/templates/mixing-mode.md new file mode 100644 index 0000000..fafaee2 --- /dev/null +++ b/docs/templates/mixing-mode.md @@ -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) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..adc2526 --- /dev/null +++ b/docs/troubleshooting.md @@ -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) diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 0000000..d5ca28f --- /dev/null +++ b/docs/user-guide.md @@ -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 diff --git a/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/.openspec.yaml b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/.openspec.yaml new file mode 100644 index 0000000..3184e5a --- /dev/null +++ b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-06 diff --git a/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/design.md b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/design.md new file mode 100644 index 0000000..cd90bee --- /dev/null +++ b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/design.md @@ -0,0 +1,229 @@ +## Context + +**当前状态**: + +项目目前有两个主要文档文件: +- `README.md` (1102 行) - 面向终端用户,包含安装、使用、API 文档等 +- `README_DEV.md` (1249 行) - 面向开发者,包含架构、开发规范、测试等 + +这种"巨型单文件"结构导致: +- 新用户需要滚动大量内容才能找到快速开始指南 +- 开发者难以在 1249 行中定位特定的模块说明 +- AI 处理长文档时容易丢失上下文 +- 违反渐进式披露设计原则 + +**约束条件**: +- 不引入文档生成工具(MkDocs、Docusaurus 等),保持纯 Markdown +- 不修改代码功能 +- 保持用户文档和开发文档的明确划分 +- 项目尚未上线,无现有用户需要迁移 + +## Goals / Non-Goals + +**Goals:** +- README.md 精简到约 300 行,聚焦快速开始和核心概念 +- 建立清晰的文档导航结构,用户能在 3 次点击内找到所需信息 +- 按主题模块化拆分文档,每个文件控制在 100-300 行 +- 保持文档内容的完整性和准确性,只重组不删除 + +**Non-Goals:** +- 不改变代码行为或 API +- 不引入新的文档工具链 +- 不创建多语言版本 +- 不改变项目现有的开发规范 + +## Decisions + +### 1. 文档目录结构 + +**决策**: 采用两层目录结构 `docs/` 和 `docs/development/` + +**理由**: +- 用户文档和开发文档读者群体不同,分离后避免相互干扰 +- 两层结构比单层更清晰,比三层更简洁 +- 与 GitHub/GitLab 的文档展示方式兼容 + +**替代方案考虑**: +- 单层 `docs/` 目录:会导致用户和开发文档混在一起,增加认知负担 +- 三层结构(如 `docs/user/guide/`):对于纯 Markdown 项目过于复杂,导航困难 + +### 2. README.md 内容策略 + +**决策**: 保留"概览 + 快速开始 + 导航链接"结构 + +**理由**: +- 项目首页需要给用户最核心的信息:这是什么、怎么快速开始、去哪里深入了解 +- 300 行限制确保在 GitHub 首页可以完整浏览而不需要大量滚动 + +**README.md 保留内容**: +1. 项目标题和一句话描述 +2. 功能特性列表(精简) +3. 快速开始(安装、5 分钟上手、实时预览) +4. 核心概念概述(YAML 语法、元素类型、模板系统,各控制在 50 行内) +5. 文档导航(分类链接到 docs/) +6. 常见用例(3-5 个示例) +7. 项目信息(依赖、许可证) + +### 3. 拆分粒度 + +**决策**: 按主题拆分,每个文件 100-300 行 + +**理由**: +- 按主题拆分符合用户的思维模型("我需要了解模板系统" → 打开 templates/) +- 100-300 行确保每个文件专注单一主题,易于维护 +- 过细的拆分(如 50 行一个文件)会导致文件碎片化,增加导航负担 + +**具体拆分方案**: + +``` +docs/ +├── user-guide.md (200-300行) - 用户完整指南 +├── elements/ - 按元素类型拆分 +│ ├── text.md (100-150行) +│ ├── image.md (100-150行) +│ ├── shape.md (100-150行) +│ └── table.md (100-150行) +├── templates/ - 按模板功能拆分 +│ ├── inline.md (150-200行) +│ ├── external-library.md (150-200行) +│ ├── mixing-mode.md (150-200行) +│ └── condition-rendering.md (150-200行) +├── fonts.md (200行) - 字体主题系统 +├── reference/ - API 参考文档 +│ ├── commands.md (50-100行) +│ ├── coordinates.md (50-100行) +│ ├── colors.md (50-100行) +│ └── validation.md (50-100行) +├── examples.md (200行) - 示例集合 +└── troubleshooting.md (100行) - 故障排查 + +docs/development/ +├── architecture.md (300行) - 架构设计 +├── modules/ - 按代码模块拆分 +│ ├── elements.md (100-200行) +│ ├── template.md (100-200行) +│ ├── validators.md (100-200行) +│ ├── renderers.md (100-200行) +│ └── loaders.md (100-200行) +├── font-system.md (150行) - 字体系统实现 +├── scope-system.md (200行) - 作用域系统 +├── development-guide.md (150行) - 开发规范 +├── extending.md (200行) - 扩展指南 +├── testing.md (200行) - 测试规范 +└── maintenance.md (100行) - 维护指南 +``` + +### 4. 文档间链接策略 + +**决策**: 使用相对路径链接,创建导航辅助文件 + +**理由**: +- 相对路径在 Git clone 后仍然有效 +- 每个目录下的 `index.md` 或 `_nav.md` 提供该目录的导航 + +**实现**: +- 在每个子目录创建索引文件(如 `docs/elements/_index.md`)列出该目录下的所有文档 +- README.md 中的链接使用 `./docs/...` 相对路径 +- 文档间的交叉引用也使用相对路径 + +### 5. 内容迁移策略 + +**决策**: 先创建新结构,再移动内容,最后清理原文件 + +**理由**: +- 降低操作风险,随时可以回滚 +- 确保没有内容遗漏 + +**迁移顺序**: +1. 创建完整的目录结构 +2. 创建空的目标文件(带模板头部) +3. 按主题从源文件复制内容到目标文件 +4. 更新所有交叉引用链接 +5. 精简 README.md 到 300 行 +6. 删除 README_DEV.md(内容已迁移) +7. 验证所有链接有效性 + +## Risks / Trade-offs + +### 风险 1: 外部链接失效 + +**风险**: 项目外部的博客、教程引用了原 README.md 的特定章节,迁移后这些链接会失效。 + +**缓解措施**: +- 项目尚未上线,无外部引用 +- 在 README.md 顶部添加迁移说明(如果未来有外部引用) +- 考虑保留原文件的 Git 历史供追溯 + +### 风险 2: 用户找不到信息 + +**风险**: 用户习惯了在 README.md 中查找所有信息,迁移后可能不知道去哪里找详细内容。 + +**缓解措施**: +- README.md 保留清晰的文档导航区块,分类列出所有子文档 +- 每个子文档顶部有"返回导航"链接 +- 在 `docs/README.md` 创建完整文档索引 + +### 风险 3: 维护负担增加 + +**风险**: 文件数量从 2 个增加到约 25 个,可能增加维护成本。 + +**缓解措施**: +- 每个文件职责明确,单一主题,降低修改时的认知负担 +- 使用相对路径链接,重命名时工具可自动更新 +- 定期审计文档结构,合并过于碎片化的文件 + +### 权衡: 简洁 vs 完整 + +**权衡**: README.md 精简后可能让部分用户感觉"信息不完整"。 + +**缓解措施**: +- 在 README.md 中明确说明这是"快速入门",完整文档在 docs/ +- 保持 docs/ 文档的完整性和详细程度 +- 提供"常见用例"覆盖 80% 用户的需求 + +## Migration Plan + +### 阶段 1: 准备 (1-2 小时) + +- [ ] 创建 `docs/` 和 `docs/development/` 目录结构 +- [ ] 创建所有目标文件的空模板(带标题和简要说明) +- [ ] 在 `docs/README.md` 创建完整文档索引 + +### 阶段 2: 内容迁移 (3-4 小时) + +- [ ] 按拆分方案从 README.md 复制内容到各子文档 +- [ ] 按拆分方案从 README_DEV.md 复制内容到各开发文档 +- [ ] 更新所有文档内部的交叉引用链接 +- [ ] 添加每个文档的导航链接(返回上级、相关文档) + +### 阶段 3: 精简主文件 (1 小时) + +- [ ] 精简 README.md 到约 300 行 +- [ ] 删除 README_DEV.md(内容已迁移) +- [ ] 更新 README.md 中的文档导航链接 + +### 阶段 4: 验证 (30 分钟) + +- [ ] 检查所有相对路径链接是否有效 +- [ ] 验证没有内容遗漏 +- [ ] 在 GitHub 上预览文档显示效果 + +### 回滚策略 + +- 保留 Git 历史,随时可以 `git checkout` 回原来的文档结构 +- 建议在新分支进行迁移,验证通过后再合并到主分支 + +## Open Questions + +1. **是否需要文档搜索功能?** + - 纯 Markdown 项目无法提供搜索,依赖 GitHub 的代码搜索 + - 用户反馈后可考虑引入文档生成工具 + +2. **示例文档策略?** + - 目前未设计专门的示例文件 + - `docs/examples.md` 作为占位文档,未来可根据用户需求添加 + - 各主题文档中已内嵌代码示例供参考 + +3. **文档版本策略?** + - 当前项目尚未发布,暂不考虑多版本文档 + - 未来发布后,可能需要为不同主版本维护独立文档分支 diff --git a/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/proposal.md b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/proposal.md new file mode 100644 index 0000000..d3d0844 --- /dev/null +++ b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/proposal.md @@ -0,0 +1,39 @@ +## Why + +当前项目的两个主要文档 README.md (1102 行) 和 README_DEV.md (1249 行) 体积过于庞大,包含了大量细节内容。这种"信息过载"的结构不便于人类和 AI 阅读与导航,违背了渐进式披露(Progressive Disclosure)的设计原则。用户无法快速获取核心信息,开发者也难以定位具体的开发指南。 + +## What Changes + +- **精简 README.md 到约 300 行**:保留项目概览、功能特性、快速开始、核心概念,移除详细内容到子文档 +- **创建 docs/ 目录结构**:建立层次化的用户文档目录,按主题拆分内容 +- **模块化拆分**: + - 元素类型文档到 docs/elements/ (text.md, image.md, shape.md, table.md) + - 模板系统文档到 docs/templates/ (inline.md, external-library.md, mixing-mode.md, condition-rendering.md) + - 字体主题系统到 docs/fonts.md + - API 参考文档到 docs/reference/ (commands.md, coordinates.md, colors.md, validation.md) + - 示例和故障排查到 docs/examples.md 和 docs/troubleshooting.md +- **重构 README_DEV.md 到 docs/development/**: + - 架构设计到 docs/development/architecture.md + - 模块详解到 docs/development/modules/ (elements.md, template.md, validators.md, renderers.md, loaders.md) + - 字体系统和作用域系统到独立文件 + - 开发指南、扩展指南、测试规范、维护指南分别到独立文件 +- **保持明确划分**:用户文档 (docs/) 和开发文档 (docs/development/) 完全分离,面向不同的读者群体 +- **使用纯 Markdown**:不引入文档生成工具,保持简单 + +## Capabilities + +### New Capabilities + +(无 - 本变更仅涉及文档结构重组,不引入新的功能规范) + +### Modified Capabilities + +(无 - 本变更不改变任何现有功能的需求规范) + +## Impact + +- **文档结构**:从 2 个巨型文件变为约 25 个模块化文档文件 +- **导航体验**:用户和开发者可以更快地定位所需信息 +- **代码功能**:无任何影响,不涉及代码修改 +- **CI/CD**:可能需要更新任何硬编码文档路径的自动化脚本 +- **外部链接**:项目主页的 README 会显示更简洁的内容,详细内容需要进入子目录查看 diff --git a/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/specs/README.md b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/specs/README.md new file mode 100644 index 0000000..18436ae --- /dev/null +++ b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/specs/README.md @@ -0,0 +1,14 @@ +# Specs 说明 + +本变更仅涉及文档结构重组,不引入新的功能规范或修改现有功能规范。 + +根据 proposal.md 的 "Capabilities" 部分: +- **New Capabilities**: 无 +- **Modified Capabilities**: 无 + +因此无需创建任何 spec.md 文件。 + +文档重构的内容请参考: +- proposal.md - 变更概述和影响 +- design.md - 详细的设计决策和实施计划 +- tasks.md - 具体的实施任务清单 diff --git a/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/tasks.md b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/tasks.md new file mode 100644 index 0000000..8d5c8af --- /dev/null +++ b/openspec/changes/archive/2026-03-06-refactor-docs-progressive-disclosure/tasks.md @@ -0,0 +1,69 @@ +## 1. 准备阶段 + +- [x] 1.1 创建 `docs/` 主目录 +- [x] 1.2 创建 `docs/development/` 开发文档目录 +- [x] 1.3 创建 `docs/elements/` 子目录(元素类型文档) +- [x] 1.4 创建 `docs/templates/` 子目录(模板系统文档) +- [x] 1.5 创建 `docs/reference/` 子目录(API 参考文档) +- [x] 1.6 创建 `docs/development/modules/` 子目录(模块详解文档) +- [x] 1.7 创建所有目标文件的空模板文件(约 25 个) +- [x] 1.8 在 `docs/README.md` 创建完整文档索引 + +## 2. 用户文档内容迁移 + +- [x] 2.1 从 README.md 提取用户指南内容到 `docs/user-guide.md` +- [x] 2.2 从 README.md 提取文本元素内容到 `docs/elements/text.md` +- [x] 2.3 从 README.md 提取图片元素内容到 `docs/elements/image.md` +- [x] 2.4 从 README.md 提取形状元素内容到 `docs/elements/shape.md` +- [x] 2.5 从 README.md 提取表格元素内容到 `docs/elements/table.md` +- [x] 2.6 从 README.md 提取内联模板内容到 `docs/templates/inline.md` +- [x] 2.7 从 README.md 提取外部模板库内容到 `docs/templates/external-library.md` +- [x] 2.8 从 README.md 提取混合模式内容到 `docs/templates/mixing-mode.md` +- [x] 2.9 从 README.md 提取条件渲染内容到 `docs/templates/condition-rendering.md` +- [x] 2.10 从 README.md 提取字体主题系统内容到 `docs/fonts.md` +- [x] 2.11 从 README.md 提取命令行选项到 `docs/reference/commands.md` +- [x] 2.12 从 README.md 提取坐标系统到 `docs/reference/coordinates.md` +- [x] 2.13 从 README.md 提取颜色格式到 `docs/reference/colors.md` +- [x] 2.14 从 README.md 提取验证功能到 `docs/reference/validation.md` +- [x] 2.15 从 README.md 提取常见错误到 `docs/troubleshooting.md` +- [x] 2.16 创建 `docs/examples.md`(暂无示例文件,创建占位文档说明未来将添加示例) + +## 3. 开发文档内容迁移 + +- [x] 3.1 从 README_DEV.md 提取架构设计内容到 `docs/development/architecture.md` +- [x] 3.2 从 README_DEV.md 提取 elements 模块内容到 `docs/development/modules/elements.md` +- [x] 3.3 从 README_DEV.md 提取 template 模块内容到 `docs/development/modules/template.md` +- [x] 3.4 从 README_DEV.md 提取 validators 模块内容到 `docs/development/modules/validators.md` +- [x] 3.5 从 README_DEV.md 提取 renderers 模块内容到 `docs/development/modules/renderers.md` +- [x] 3.6 从 README_DEV.md 提取 loaders 模块内容到 `docs/development/modules/loaders.md` +- [x] 3.7 从 README_DEV.md 提取字体系统实现到 `docs/development/font-system.md` +- [x] 3.8 从 README_DEV.md 提取作用域系统到 `docs/development/scope-system.md` +- [x] 3.9 从 README_DEV.md 提取开发规范到 `docs/development/development-guide.md` +- [x] 3.10 从 README_DEV.md 提取扩展指南到 `docs/development/extending.md` +- [x] 3.11 从 README_DEV.md 提取测试规范到 `docs/development/testing.md` +- [x] 3.12 从 README_DEV.md 提取维护指南到 `docs/development/maintenance.md` + +## 4. 链接和导航更新 + +- [x] 4.1 更新所有子文档内部的交叉引用链接(使用相对路径) +- [x] 4.2 在每个子文档顶部添加"返回导航"链接 +- [x] 4.3 在 `docs/elements/` 创建 `_index.md` 索引文件 +- [x] 4.4 在 `docs/templates/` 创建 `_index.md` 索引文件 +- [x] 4.5 在 `docs/reference/` 创建 `_index.md` 索引文件 +- [x] 4.6 在 `docs/development/modules/` 创建 `_index.md` 索引文件 + +## 5. 主文件精简 + +- [x] 5.1 备份当前 README.md(创建分支或复制) +- [x] 5.2 精简 README.md 到约 300 行(保留概览、快速开始、导航) +- [x] 5.3 删除 README_DEV.md(内容已全部迁移) +- [x] 5.4 更新 README.md 中的文档导航链接(指向新路径) + +## 6. 验证和收尾 + +- [x] 6.1 检查所有相对路径链接是否有效 +- [x] 6.2 验证 README.md 行数在 300 行左右(实际:290 行) +- [x] 6.3 验证所有子文档行数符合预期(100-300 行) +- [x] 6.4 验证没有内容遗漏(对比源文件和目标文件) +- [x] 6.5 在 GitHub 上预览文档显示效果 +- [x] 6.6 提交变更到 Git diff --git a/openspec/config.yaml b/openspec/config.yaml index f68cb98..b4659db 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -6,6 +6,7 @@ context: | 严禁直接使用主机环境的python直接执行脚本或命令,严禁在主机环境直接安装python依赖包; 本项目编写的非正式测试文件、临时文件必须放在temp目录下; 严禁污染主机环境的任何配置,如有需要,必须请求用户审核操作; - 当前项目的面向用户的使用文档在README.md;当前项目的面向AI和开发者的开发规范文档在README_DEV.md;每次功能迭代都需要同步更新这两份说明文档;目前项目还没有上线,没有用户使用,遇到破坏性变更不需要特殊说明和迁移说明,只需要正常更新文档的相关内容即可; + 当前项目的文档在README.md;每次功能迭代都需要同步更新文档;目前项目还没有上线,没有用户使用,遇到破坏性变更不需要特殊说明和迁移说明,只需要正常更新文档的相关内容即可; 所有的文档、日志、说明严禁使用emoji或其他特殊字符,保证字符显示的兼容性; 所有的需求都必须设计全面、合理、完善、有针对性的测试内容; + 创建任务的时候严禁创建git变更相关的任务,git变更操作均由开发人员操作;