refactor: 重构外部模板系统,改为单文件模板库模式
主要变更: - 将 templates_dir 参数改为 template_file,支持单个模板库 YAML 文件 - 添加模板库 YAML 验证功能 - 为模板添加 base_dir 支持,正确解析相对路径资源 - 内联模板与外部模板同名时改为警告(内联优先) - 移除模板缓存机制,直接使用模板库字典 - 更新所有相关测试以适配新的模板加载方式 此重构简化了模板管理,使模板资源的路径解析更加清晰明确。
This commit is contained in:
143
README_DEV.md
143
README_DEV.md
@@ -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` 字段
|
||||
|
||||
Reference in New Issue
Block a user