实现了统一的metadata结构和字体作用域系统,支持文档和模板库之间的单向字体引用。 主要变更: - 模板库必须包含metadata字段(包括size、fonts、fonts_default) - 实现文档和模板库的size一致性校验 - 实现字体作用域系统(文档可引用模板库字体,反之不可) - 实现跨域循环引用检测 - 实现fonts_default级联规则(模板库→文档→系统默认) - 添加错误代码常量(SIZE_MISMATCH、FONT_NOT_FOUND等) - 更新文档和开发者指南 测试覆盖: - 新增33个测试(单元测试20个,集成测试13个) - 所有457个测试通过 Breaking Changes: - 模板库文件必须包含metadata字段 - 模板库metadata.size为必填字段 - 文档和模板库的size必须一致 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
401 lines
13 KiB
Markdown
401 lines
13 KiB
Markdown
# Design: 模板库元数据与跨域字体引用
|
||
|
||
## Context
|
||
|
||
### 当前状态
|
||
|
||
当前系统中:
|
||
- 文档支持 metadata 结构,包含 size、description、fonts、fonts_default 字段
|
||
- 模板库仅支持 templates 字段和可选的简单元数据(description、version、author)
|
||
- 字体引用系统(FontResolver)只处理单一作用域的字体配置
|
||
- 模板中的元素无法引用字体配置,只能直接定义字体属性
|
||
|
||
### 约束条件
|
||
|
||
- 模板库必须包含 metadata 字段且 size 必填(不向后兼容)
|
||
- 模板库和文档的 metadata 使用相同的验证规则
|
||
- 字体引用规则是单向的:文档可引用模板库,模板库不能引用文档
|
||
- 变量解析逻辑保持现状,不扩展到 font 字段
|
||
|
||
### 涉及模块
|
||
|
||
- `loaders/yaml_loader.py` - YAML 加载和验证
|
||
- `validators/validator.py` - 主验证器
|
||
- `core/presentation.py` - 演示文稿管理
|
||
- `core/template.py` - 模板渲染
|
||
- `utils/font_resolver.py` - 字体解析器
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
|
||
- 统一文档和模板库的 metadata 结构,支持相同的字段定义
|
||
- 模板库必须包含 metadata 且 size 必填(破坏性变更)
|
||
- 建立清晰的跨域字体引用规则,支持文档引用模板库的字体配置
|
||
- 实现 size 一致性校验,确保文档和模板库尺寸匹配
|
||
- 扩展 FontResolver 支持跨域引用和循环检测
|
||
|
||
**Non-Goals:**
|
||
|
||
- 不支持模板库引用文档的字体配置(严格单向)
|
||
- 不修改现有的变量解析逻辑
|
||
- 不引入新的作用域前缀语法(如 @template:xxx)
|
||
- 不改变 font 字段的变量替换行为(font 字段不被变量替换)
|
||
|
||
## Decisions
|
||
|
||
### Decision 1: 统一的 Metadata 验证函数
|
||
|
||
**决策**: 创建统一的 metadata 验证函数,同时用于文档和模板库。
|
||
|
||
**理由**:
|
||
- 减少代码重复
|
||
- 确保文档和模板库的 metadata 使用完全相同的验证规则
|
||
- 未来扩展 metadata 字段时只需修改一处
|
||
|
||
**实现**:
|
||
```python
|
||
# loaders/yaml_loader.py
|
||
def validate_metadata(metadata, file_path, context="文档"):
|
||
"""验证 metadata 结构
|
||
|
||
Args:
|
||
metadata: metadata 字典
|
||
file_path: 文件路径(用于错误消息)
|
||
context: 上下文描述("文档" 或 "模板库")
|
||
"""
|
||
if not isinstance(metadata, dict):
|
||
raise YAMLError(f"{file_path}: metadata 必须是字典对象")
|
||
|
||
# size 必填
|
||
if "size" not in metadata:
|
||
raise YAMLError(f"{file_path}: metadata 缺少必填字段 'size'")
|
||
|
||
size = metadata["size"]
|
||
if size not in ["16:9", "4:3"]:
|
||
raise YAMLError(f"{file_path}: metadata.size 必须是 '16:9' 或 '4:3',当前值: {size}")
|
||
|
||
# 其他字段可选
|
||
# version, author, description: 不验证格式
|
||
# fonts, fonts_default: 使用字体主题系统的验证逻辑
|
||
```
|
||
|
||
### Decision 2: FontResolver 作用域感知
|
||
|
||
**决策**: 扩展 FontResolver 支持作用域参数,区分文档作用域和模板库作用域。
|
||
|
||
**理由**:
|
||
- 保持现有 FontResolver API 的简洁性
|
||
- 通过作用域参数控制引用行为
|
||
- 支持跨域引用检测
|
||
|
||
**实现**:
|
||
```python
|
||
# utils/font_resolver.py
|
||
class FontResolver:
|
||
def __init__(self, fonts=None, fonts_default=None, scope="document", template_fonts=None):
|
||
self.fonts = fonts or {}
|
||
self.fonts_default = fonts_default
|
||
self.scope = scope # "document" 或 "template"
|
||
self.template_fonts = template_fonts or {}
|
||
self._max_depth = 10
|
||
|
||
def _resolve_reference(self, reference, visited, allow_cross_domain=False):
|
||
# 解析引用时考虑作用域规则
|
||
if self.scope == "template" and not allow_cross_domain:
|
||
# 模板库作用域:只能引用 template_fonts
|
||
font_dict = self.template_fonts.get(font_name)
|
||
if font_dict is None:
|
||
raise ValueError(
|
||
f"模板元素不能引用文档的字体配置: {reference},"
|
||
f"只能引用模板库中定义的字体"
|
||
)
|
||
else:
|
||
# 文档作用域或允许跨域:优先 fonts,fallback template_fonts
|
||
if font_name in self.fonts:
|
||
font_dict = self.fonts[font_name]
|
||
elif font_name in self.template_fonts:
|
||
font_dict = self.template_fonts[font_name]
|
||
else:
|
||
raise ValueError(f"引用的字体配置不存在: {reference}")
|
||
```
|
||
|
||
### Decision 3: 跨域循环引用检测
|
||
|
||
**决策**: 在循环引用检测中增加作用域追踪,检测跨域循环。
|
||
|
||
**理由**:
|
||
- 防止文档和模板库之间形成循环引用
|
||
- 提供清晰的错误消息,帮助用户定位问题
|
||
|
||
**实现**:
|
||
```python
|
||
def _resolve_reference(self, reference, visited, allow_cross_domain=False):
|
||
# 检测循环时,记录引用的作用域
|
||
scope_tag = "doc" if self.scope == "document" else "template"
|
||
tagged_ref = f"{scope_tag}.{reference}"
|
||
|
||
if tagged_ref in visited:
|
||
path = " -> ".join(visited) + f" -> {tagged_ref}"
|
||
if any("doc." in v and "template." in v for v in visited):
|
||
raise ValueError(f"检测到跨域字体引用循环: {path}")
|
||
else:
|
||
raise ValueError(f"检测到字体引用循环: {path}")
|
||
|
||
visited.add(tagged_ref)
|
||
# ... 继续解析
|
||
```
|
||
|
||
### Decision 4: Size 一致性校验时机
|
||
|
||
**决策**: 在 Presentation 初始化时,加载模板库后立即进行 size 一致性校验。
|
||
|
||
**理由**:
|
||
- 早期失败,避免后续处理才发现问题
|
||
- 错误消息更清晰,用户可以立即修正
|
||
|
||
**实现**:
|
||
```python
|
||
# core/presentation.py
|
||
def __init__(self, pres_file, template_file=None):
|
||
# ... 加载文档
|
||
|
||
# 加载并验证模板库文件(如果提供)
|
||
if self.template_file:
|
||
self.template_library = load_yaml_file(self.template_file)
|
||
validate_template_library_yaml(self.template_library, str(self.template_file))
|
||
|
||
# 验证模板库 metadata(必须存在)
|
||
if "metadata" not in self.template_library:
|
||
raise YAMLError(
|
||
f"{self.template_file}: 模板库必须包含 metadata 字段"
|
||
)
|
||
|
||
validate_metadata(
|
||
self.template_library["metadata"],
|
||
str(self.template_file),
|
||
context="模板库"
|
||
)
|
||
|
||
# size 一致性校验
|
||
doc_size = self.data.get("metadata", {}).get("size")
|
||
template_size = self.template_library["metadata"].get("size")
|
||
if doc_size != template_size:
|
||
raise YAMLError(
|
||
f"文档尺寸 '{doc_size}' 与模板库尺寸 '{template_size}' 不一致"
|
||
)
|
||
```
|
||
|
||
### Decision 5: fonts_default 级联处理
|
||
|
||
**决策**: 在模板渲染时,如果元素未定义 font,级联查找 fonts_default:先文档后模板库。
|
||
|
||
**理由**:
|
||
- 文档优先级更高,允许文档覆盖模板库的默认值
|
||
- 保持灵活性,模板库提供基础配置
|
||
|
||
**实现**:
|
||
```python
|
||
# core/template.py 或渲染器中
|
||
def resolve_element_font(self, font_config, doc_fonts_default, template_fonts_default):
|
||
if font_config is None:
|
||
# 级联:文档 fonts_default → 模板库 fonts_default → 系统默认
|
||
if doc_fonts_default:
|
||
return self.font_resolver_doc.resolve_font(doc_fonts_default)
|
||
elif template_fonts_default:
|
||
return self.font_resolver_template.resolve_font(template_fonts_default)
|
||
else:
|
||
return FontConfig()
|
||
else:
|
||
return self.font_resolver.resolve_font(font_config)
|
||
```
|
||
|
||
### Decision 6: 错误代码标准化
|
||
|
||
**决策**: 定义统一的错误代码和错误消息模板。
|
||
|
||
**理由**:
|
||
- 便于测试和文档维护
|
||
- 用户提供一致的错误体验
|
||
|
||
**错误代码**:
|
||
```
|
||
SIZE_MISMATCH - 文档和模板库尺寸不一致
|
||
TEMPLATE_FONT_REF_DOC_FORBIDDEN - 模板元素引用文档字体
|
||
TEMPLATE_PARENT_REF_DOC_FORBIDDEN - 模板库 parent 引用文档字体
|
||
FONT_NOT_FOUND - 字体配置不存在
|
||
CIRCULAR_REFERENCE - 循环引用(包括跨域)
|
||
FONT_DEFAULT_INVALID - fonts_default 引用无效
|
||
TEMPLATE_LIBRARY_MISSING_METADATA - 模板库缺少 metadata 字段
|
||
TEMPLATE_LIBRARY_METADATA_MISSING_SIZE - 模板库 metadata 缺少 size
|
||
TEMPLATE_LIBRARY_METADATA_INVALID_SIZE - 模板库 metadata.size 无效
|
||
```
|
||
|
||
## Risks / Trade-offs
|
||
|
||
### Risk 1: 旧测试和代码清理
|
||
|
||
**风险**: 破坏性变更可能遗留旧的测试代码和逻辑,导致测试失败或行为不一致。
|
||
|
||
**缓解措施**:
|
||
- 系统性识别所有假设模板库没有 metadata 的测试
|
||
- 移除向后兼容性相关的代码路径
|
||
- 确保所有错误代码和错误消息一致
|
||
|
||
### Risk 2: 跨域引用的复杂性
|
||
|
||
**风险**: 跨域引用规则增加理解和使用复杂度。
|
||
|
||
**缓解措施**:
|
||
- 提供清晰的错误消息,明确指出违反了哪条规则
|
||
- 在文档中详细说明引用规则
|
||
- 添加验证测试覆盖所有场景
|
||
|
||
### Risk 3: 性能影响
|
||
|
||
**风险**: 跨域引用和循环检测可能增加解析时间。
|
||
|
||
**缓解措施**:
|
||
- 最大深度限制(10 层)防止无限循环
|
||
- 早期失败机制避免无效配置的深度解析
|
||
- 缓存已解析的字体配置(如果需要)
|
||
|
||
### Risk 4: 测试覆盖不足
|
||
|
||
**风险**: 跨域场景复杂,可能遗漏边界情况。
|
||
|
||
**缓解措施**:
|
||
- 编写全面的单元测试和集成测试
|
||
- 使用属性测试(property-based testing)覆盖边界情况
|
||
- 添加循环引用的压力测试
|
||
|
||
## 实施步骤
|
||
|
||
### 阶段 1: 清理旧测试
|
||
1. 识别并移除与旧模板库逻辑相关的测试
|
||
2. 移除假设模板库没有 metadata 的测试场景
|
||
|
||
### 阶段 2: 实现核心功能
|
||
1. 添加统一 metadata 验证函数
|
||
2. 扩展 FontResolver 支持作用域
|
||
3. 实现跨域引用和循环检测
|
||
4. 添加 size 一致性校验
|
||
|
||
### 阶段 3: 添加新测试
|
||
1. 添加 metadata 验证相关测试
|
||
2. 添加跨域字体引用测试
|
||
3. 添加 size 一致性校验测试
|
||
4. 添加 fonts_default 级联测试
|
||
|
||
### 阶段 4: 更新文档
|
||
1. 更新 README.md 和 README_DEV.md
|
||
2. 添加示例配置文件
|
||
3. 同步更新相关规范文档
|
||
|
||
## Open Questions
|
||
|
||
### Q1: 是否需要支持模板库的 metadata 继承?
|
||
|
||
**问题**: 如果文档和模板库都有 metadata,是否允许模板库的某些字段被文档覆盖?
|
||
|
||
**当前决策**: 不支持继承,size 必须严格一致,其他字段各自独立。
|
||
|
||
### Q2: fonts_default 的级联是否应该在验证阶段检测?
|
||
|
||
**问题**: 是否需要在验证阶段检查 fonts_default 引用的有效性?
|
||
|
||
**当前决策**: 在渲染时动态解析,验证阶段只检查格式。
|
||
|
||
### Q3: 是否需要提供诊断命令?
|
||
|
||
**问题**: 是否需要提供一个命令来显示字体引用链,帮助用户调试?
|
||
|
||
**当前决策**: 暂不实现,通过错误消息提供足够信息。
|
||
|
||
## Implementation Notes
|
||
|
||
### 代码组织
|
||
|
||
```
|
||
loaders/yaml_loader.py
|
||
- validate_metadata() # 新增:统一 metadata 验证
|
||
- validate_template_library_yaml() # 修改:调用 validate_metadata
|
||
|
||
validators/validator.py
|
||
- Validator # 修改:添加 size 一致性校验
|
||
|
||
core/presentation.py
|
||
- Presentation.__init__() # 修改:解析模板库 metadata
|
||
- Presentation.get_template() # 修改:传递字体配置
|
||
|
||
core/template.py
|
||
- Template.from_data() # 修改:接收作用域参数
|
||
- Template.render() # 修改:应用正确的字体解析器
|
||
|
||
utils/font_resolver.py
|
||
- FontResolver.__init__() # 修改:支持作用域和跨域
|
||
- FontResolver._resolve_reference() # 修改:跨域引用检测
|
||
- FontResolver._check_circular() # 新增:跨域循环检测
|
||
```
|
||
|
||
### 测试策略
|
||
|
||
#### 清理旧测试
|
||
|
||
```
|
||
tests/unit/test_template.py
|
||
- 移除假设模板库没有 metadata 的测试场景
|
||
- 移除向后兼容性相关的测试
|
||
|
||
tests/integration/test_template_library.py
|
||
- 移除假设模板库可选 metadata 的测试
|
||
- 重新设计基于新 metadata 结构的测试
|
||
```
|
||
|
||
#### 添加新测试
|
||
|
||
```
|
||
tests/unit/test_yaml_loader.py
|
||
- 添加 validate_metadata() 测试
|
||
- 添加模板库 metadata 必须存在的测试
|
||
|
||
tests/unit/test_font_resolver.py
|
||
- 添加跨域引用测试
|
||
- 添加跨域循环检测测试
|
||
- 添加作用域限制测试
|
||
|
||
tests/integration/test_font_cross_domain.py # 新增
|
||
- 测试文档元素引用模板库字体
|
||
- 测试模板元素只能引用模板库字体
|
||
- 测试 parent 跨域引用规则
|
||
- 测试 fonts_default 级联
|
||
|
||
tests/integration/test_size_validation.py # 新增
|
||
- 测试 size 一致性校验
|
||
- 测试模板库缺少 metadata
|
||
- 测试模板库缺少 size
|
||
- 测试模板库 size 无效
|
||
```
|
||
|
||
### 数据流
|
||
|
||
```
|
||
文档加载
|
||
↓
|
||
验证文档 metadata
|
||
↓
|
||
加载模板库(如果有)
|
||
↓
|
||
验证模板库 metadata
|
||
↓
|
||
size 一致性校验
|
||
↓
|
||
创建 FontResolver(文档作用域)
|
||
创建 FontResolver(模板库作用域)
|
||
↓
|
||
渲染幻灯片
|
||
- 文档元素:使用文档 FontResolver(可引用模板库)
|
||
- 内联模板元素:使用文档 FontResolver
|
||
- 外部模板元素:使用模板库 FontResolver(不能引用文档)
|
||
```
|