1
0
Files
PPTX/openspec/changes/archive/2026-03-05-template-metadata-and-font-ref/design.md
lanyuanxiaoyao 98098dc911 feat: 实现模板库metadata和跨域字体引用系统
实现了统一的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>
2026-03-05 18:12:05 +08:00

401 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
# 文档作用域或允许跨域:优先 fontsfallback 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不能引用文档
```