1
0

refactor: 重构外部模板系统,改为单文件模板库模式

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

此重构简化了模板管理,使模板资源的路径解析更加清晰明确。
This commit is contained in:
2026-03-05 13:26:29 +08:00
parent bd12fce14b
commit f1aae96a04
27 changed files with 2141 additions and 1988 deletions

View File

@@ -516,7 +516,11 @@ if right > slide_width + TOLERANCE:
# 报告 WARNING
```
### 7. 内联模板系统
### 7. 模板系统架构
yaml2pptx 支持两种模板方式:内联模板和外部模板库。
#### 7.1 内联模板系统
**决策**:支持在 YAML 源文件中定义内联模板,与外部模板系统共存
@@ -538,10 +542,11 @@ if right > slide_width + TOLERANCE:
2. **模板创建**:使用 `Template.from_data()` 类方法
```python
@classmethod
def from_data(cls, template_data, template_name):
"""从字典创建模板(内联模板)"""
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
@@ -551,19 +556,28 @@ if right > slide_width + TOLERANCE:
```python
def get_template(self, template_name):
# 1. 检查内联模板
if hasattr(self, 'inline_templates') and template_name in self.inline_templates:
if template_name in self.inline_templates:
# 2. 检查同名冲突
if self._external_template_exists(template_name):
raise YAMLError(f"模板名称冲突: '{template_name}'")
return Template.from_data(self.inline_templates[template_name], template_name)
# 发出 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. **同名检测**禁止内联和外部模板同名
- 显式报错比隐式选择更安全
- 强制用户明确模板来源
- 降低调试难度
4. **同名处理**:内联和外部模板同名时发出警告,优先使用内联模板
- 内联模板更贴近文档,优先级更高
- 发出 WARNING 提醒用户注意冲突
- 不阻止转换,保持灵活性
5. **限制**:禁止内联模板相互引用
- 降低实现复杂度
@@ -576,6 +590,115 @@ if right > slide_width + TOLERANCE:
- 检查变量定义是否有必需的 `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` 字段