1
0

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>
This commit is contained in:
2026-03-05 18:12:05 +08:00
parent f1aae96a04
commit 98098dc911
25 changed files with 2794 additions and 141 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-05

View File

@@ -0,0 +1,400 @@
# 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不能引用文档
```

View File

@@ -0,0 +1,73 @@
# Proposal: 模板库元数据与跨域字体引用
## Why
当前系统中,文档可以定义 metadata.fonts 来管理字体主题,但模板库缺乏相应的元数据结构,导致:
1. 模板库无法定义自己的字体配置,限制了模板的独立性和复用性
2. 模板中的元素无法引用统一的字体配置
3. 文档和模板库之间缺乏明确的字体引用关系规范
本变更旨在统一文档和模板库的元数据结构,建立清晰的跨域字体引用规则。
## What Changes
- **模板库增加 metadata 结构**
- 模板库必须包含 metadata 字段,与文档使用相同的结构
- metadata.size 必填,必须是 "16:9" 或 "4:3"
- 其他字段version、author、description、fonts、fonts_default可选
- 模板库的 fonts 定义模板级别的字体配置
- **文档 metadata 扩展**
- 新增 version 字段(可选)
- 新增 author 字段(可选)
- **跨域字体引用规则**
- 文档元素和内联模板元素:优先引用文档 fonts文档没有时可引用模板库 fonts
- 外部模板元素:只能引用模板库 fonts不能引用文档 fonts
- 文档 fonts 的 parent可引用文档内部或模板库 fonts
- 模板库 fonts 的 parent只能引用模板库内部 fonts
- **fonts_default 级联规则**
- 模板元素未定义 font 时:优先使用文档 fonts_default文档没有则使用模板库 fonts_default
- **size 一致性校验**
- 文档的 size 必须与模板库的 size 一致
- 不一致时抛出 ERROR 级别错误
## Capabilities
### New Capabilities
### Modified Capabilities
- `template-library`: 扩展模板库能力,支持 metadata 结构和字体配置
- `font-theme`: 扩展字体主题能力,支持跨域引用(文档引用模板库)
## Impact
### 受影响的代码模块
- `loaders/yaml_loader.py`: 添加模板库 metadata 验证逻辑
- `validators/validator.py`: 添加 size 一致性校验
- `core/presentation.py`: 解析和存储模板库 metadata.fonts
- `core/template.py`: 模板渲染时应用字体引用规则
- `utils/font_resolver.py`: 扩展字体解析器,支持跨域引用
### API 变更
- `validate_template_library_yaml()` 增加 metadata 验证
- `Template` 类增加字体解析上下文(是文档作用域还是模板库作用域)
- `FontResolver` 增加跨域引用检测逻辑
### 破坏性变更
- 模板库必须包含 metadata 字段且 size 必填
- 需要清理旧的模板库相关测试,重新设计新测试
### 测试影响
- 清理旧的模板库验证测试
- 重新设计模板库 metadata 相关测试
- 新增跨域字体引用测试
- 新增 size 一致性校验测试
- 新增 fonts_default 级联测试

View File

@@ -0,0 +1,398 @@
# Font Theme Delta Spec
## ADDED Requirements
### Requirement: 文档 metadata 必须支持 version 和 author 字段
文档 metadata SHALL 支持可选的 `version``author` 字段。
#### Scenario: 文档包含 version 和 author
- **WHEN** 文档 metadata 包含 version: "1.0" 和 author: "作者名"
- **THEN** 系统成功解析并存储这些字段
#### Scenario: 文档不包含 version 和 author
- **WHEN** 文档 metadata 不包含 version 和 author 字段
- **THEN** 系统正常处理,这些字段为可选项
### Requirement: 文档元素和内联模板元素必须支持跨域引用字体
文档中的元素和内联模板(文档中定义的 templates中的元素 SHALL 优先引用文档的 fonts文档没有时可引用模板库的 fonts。
#### Scenario: 元素引用文档中存在的字体
- **WHEN** 文档元素定义 font: "@title"
- **AND** title 存在于文档 metadata.fonts 中
- **THEN** 系统使用文档的 title 配置
#### Scenario: 元素引用文档不存在但模板库存在的字体
- **WHEN** 文档元素定义 font: "@template-title"
- **AND** template-title 不存在于文档 fonts 中
- **AND** template-title 存在于模板库 metadata.fonts 中
- **THEN** 系统使用模板库的 template-title 配置
#### Scenario: 元素引用不存在的字体(文档和模板库都没有)
- **WHEN** 文档元素定义 font: "@nonexistent"
- **AND** nonexistent 不存在于文档和模板库的 fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_NOT_FOUND`
- **AND** 错误消息包含"引用的字体配置不存在"
#### Scenario: 同名字体时优先使用文档的
- **WHEN** 文档元素定义 font: "@title"
- **AND** title 同时存在于文档 fonts 和模板库 fonts 中
- **THEN** 系统使用文档的 title 配置(不使用模板库的)
#### Scenario: 内联模板元素引用文档字体
- **WHEN** 内联模板(文档 templates 中定义)的元素定义 font: "@body"
- **AND** body 存在于文档 metadata.fonts 中
- **THEN** 系统使用文档的 body 配置
#### Scenario: 内联模板元素引用模板库字体
- **WHEN** 内联模板的元素定义 font: "@template-body"
- **AND** template-body 只存在于模板库 metadata.fonts 中
- **THEN** 系统使用模板库的 template-body 配置
### Requirement: 文档 fonts 的 parent 必须支持跨域引用
文档 metadata.fonts 中定义的字体配置,其 parent 字段 SHALL 可以引用文档内部的字体配置或模板库的字体配置。
#### Scenario: parent 引用文档内部的字体
- **WHEN** 文档 metadata.fonts 中定义 heading: {parent: "@base"}
- **AND** base 存在于文档 fonts 中
- **THEN** 系统成功继承 base 的属性
#### Scenario: parent 引用模板库的字体
- **WHEN** 文档 metadata.fonts 中定义 custom: {parent: "@template-base"}
- **AND** template-base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功继承 template-base 的属性
#### Scenario: parent 引用不存在的字体(文档和模板库都没有)
- **WHEN** 文档 metadata.fonts 中定义 heading: {parent: "@nonexistent"}
- **AND** nonexistent 不存在于文档和模板库的 fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_NOT_FOUND`
- **AND** 错误消息包含"引用的字体配置不存在"
#### Scenario: 同名字体时 parent 优先引用文档的
- **WHEN** 文档 metadata.fonts 中定义 heading: {parent: "@base"}
- **AND** base 同时存在于文档 fonts 和模板库 fonts 中
- **THEN** 系统使用文档的 base 配置
### Requirement: 系统必须检测跨域循环引用
系统 SHALL 在解析字体引用时检测跨域循环引用,包括文档和模板库之间的循环。
#### Scenario: 文档内部循环引用
- **WHEN** 文档 fonts.a.parent: "@b" 且 fonts.b.parent: "@a"
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
- **AND** 错误消息包含"检测到字体引用循环"和完整路径
#### Scenario: 模板库内部循环引用
- **WHEN** 模板库 fonts.x.parent: "@y" 且 fonts.y.parent: "@x"
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
- **AND** 错误消息包含"检测到字体引用循环"和完整路径
#### Scenario: 跨域循环引用
- **WHEN** 文档 fonts.a.parent: "@template-b"
- **AND** 模板库 fonts.b.parent: "@template-c"
- **AND** 模板库 fonts.c.parent: "@doc-a"
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
- **AND** 错误消息包含"检测到跨域字体引用循环"和完整路径
#### Scenario: 跨域引用链不循环
- **WHEN** 文档 fonts.a.parent: "@template-b"
- **AND** 模板库 fonts.b.parent: "@template-c"
- **AND** 模板库 fonts.c 没有引用其他字体
- **THEN** 系统成功解析,不报错
### Requirement: 文档和模板库的 fonts_default 必须独立验证
文档的 metadata.fonts_default SHALL 只能引用文档或模板库中存在的字体配置,模板库的 metadata.fonts_default SHALL 只能引用模板库中存在的字体配置。
#### Scenario: 文档 fonts_default 引用文档字体
- **WHEN** 文档 metadata.fonts_default: "@body"
- **AND** body 存在于文档 metadata.fonts 中
- **THEN** 系统成功解析默认字体
#### Scenario: 文档 fonts_default 引用模板库字体
- **WHEN** 文档 metadata.fonts_default: "@template-base"
- **AND** template-base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功解析默认字体
#### Scenario: 文档 fonts_default 引用不存在的字体
- **WHEN** 文档 metadata.fonts_default: "@nonexistent"
- **AND** nonexistent 不存在于文档和模板库的 fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
- **AND** 错误消息包含"fonts_default 引用的字体配置不存在"
#### Scenario: 模板库 fonts_default 引用模板库字体
- **WHEN** 模板库 metadata.fonts_default: "@base"
- **AND** base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功解析默认字体
#### Scenario: 模板库 fonts_default 引用文档字体
- **WHEN** 模板库 metadata.fonts_default: "@doc-body"
- **AND** doc-body 只存在于文档 metadata.fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
- **AND** 错误消息包含"模板库 fonts_default 只能引用模板库内部的字体配置"
## MODIFIED Requirements
### Requirement: 系统必须支持在 metadata 中定义 fonts 字段
系统 SHALL 支持在文档和模板库的 YAML metadata 中定义 fonts 字段,用于存储可复用的字体配置。模板库的 fonts 与文档的 fonts 使用相同的定义和验证规则。
#### Scenario: 定义 fonts 字段(文档)
- **WHEN** 文档 metadata 中定义 fonts 字段
- **THEN** 系统成功解析并存储字体配置字典
#### Scenario: 定义 fonts 字段(模板库)
- **WHEN** 模板库 metadata 中定义 fonts 字段
- **THEN** 系统成功解析并存储字体配置字典
#### Scenario: fonts 字段为空字典
- **WHEN** metadata 中定义 fonts: {}
- **THEN** 系统接受空的字体配置字典
#### Scenario: fonts 字段未定义
- **WHEN** metadata 中未定义 fonts 字段
- **THEN** 系统正常处理fonts 为空字典
### Requirement: 系统必须支持 fonts_default 字段
系统 SHALL 支持在文档和模板库的 metadata 中定义可选的 fonts_default 字段,指定默认字体配置的引用。文档的 fonts_default 可以引用文档或模板库的 fonts模板库的 fonts_default 只能引用模板库的 fonts。
#### Scenario: 定义 fonts_default 引用(文档)
- **WHEN** 文档 metadata 中定义 fonts_default: "@body"
- **AND** body 存在于文档 metadata.fonts 中
- **THEN** 系统将 fonts_default 解析为对 fonts.body 的引用
#### Scenario: 定义 fonts_default 引用(模板库)
- **WHEN** 模板库 metadata 中定义 fonts_default: "@base"
- **AND** base 存在于模板库 metadata.fonts 中
- **THEN** 系统将 fonts_default 解析为对 fonts.base 的引用
#### Scenario: 文档 fonts_default 引用模板库字体
- **WHEN** 文档 metadata.fonts_default: "@template-base"
- **AND** template-base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功解析默认字体
#### Scenario: 模板库 fonts_default 不能引用文档字体
- **WHEN** 模板库 metadata.fonts_default: "@doc-body"
- **AND** doc-body 只存在于文档 metadata.fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
#### Scenario: fonts_default 未定义
- **WHEN** metadata 中未定义 fonts_default 字段
- **THEN** 系统使用 python-pptx 的默认字体
#### Scenario: fonts_default 引用不存在的配置
- **WHEN** fonts_default: "@undefined" 且 undefined 不存在
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
#### Scenario: fonts_default 必须是引用格式
- **WHEN** fonts_default: "Arial"(直接字体名称)
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
### Requirement: 元素 font 字段必须支持多种引用方式
元素 font 字段 SHALL 支持字符串引用(整体引用)、字典引用(继承覆盖或独立定义)两种形式。引用的目标根据元素所在作用域决定。
#### Scenario: 文档元素引用文档字体
- **WHEN** 文档元素定义 font: "@title"
- **AND** title 存在于文档 metadata.fonts 中
- **THEN** 系统使用文档的 title 配置
#### Scenario: 文档元素引用模板库字体
- **WHEN** 文档元素定义 font: "@template-title"
- **AND** template-title 只存在于模板库 metadata.fonts 中
- **THEN** 系统使用模板库的 template-title 配置
#### Scenario: 模板元素只能引用模板库字体
- **WHEN** 外部模板元素定义 font: "@title"
- **AND** title 只存在于文档 metadata.fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_FONT_REF_DOC_FORBIDDEN`
#### Scenario: 字典继承覆盖
- **WHEN** 元素定义 font: {parent: "@title", size: 60}
- **THEN** 系统继承引用字体的所有属性,覆盖 size 为 60
#### Scenario: 字典独立定义
- **WHEN** 元素定义 font: {family: "SimSun", size: 24}
- **THEN** 系统使用定义的属性,未定义的属性继承 fonts_default
#### Scenario: font 字段未定义
- **WHEN** 元素未定义 font 字段
- **THEN** 元素使用 fonts_default 或系统默认字体
### Requirement: parent 字段必须引用有效域中的字体配置
font 字典中的 parent 字段 SHALL 引用有效域中的配置。文档 fonts 的 parent 可以引用文档或模板库的 fonts模板库 fonts 的 parent 只能引用模板库的 fonts。
#### Scenario: 文档字体 parent 引用文档字体
- **WHEN** 文档 fonts 中定义 heading: {parent: "@title"}
- **AND** title 存在于文档 metadata.fonts 中
- **THEN** 系统成功继承 title 的属性
#### Scenario: 文档字体 parent 引用模板库字体
- **WHEN** 文档 fonts 中定义 custom: {parent: "@template-base"}
- **AND** template-base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功继承 template-base 的属性
#### Scenario: 模板库字体 parent 引用模板库字体
- **WHEN** 模板库 fonts 中定义 heading: {parent: "@base"}
- **AND** base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功继承 base 的属性
#### Scenario: 模板库字体 parent 引用文档字体(禁止)
- **WHEN** 模板库 fonts 中定义 custom: {parent: "@doc-base"}
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_PARENT_REF_DOC_FORBIDDEN`
#### Scenario: parent 引用不存在的配置
- **WHEN** parent: "@undefined" 且 undefined 不存在
- **THEN** 系统抛出 ERROR错误代码为 `FONT_NOT_FOUND`
#### Scenario: parent 必须是引用格式
- **WHEN** parent: "Arial"(直接字体名称)
- **THEN** 系统抛出 ERROR
### Requirement: 系统必须检测并拒绝引用循环
系统 SHALL 在解析字体引用时检测循环引用,包括单域内部循环和跨域循环,检测到循环时抛出 ERROR。
#### Scenario: 直接循环引用
- **WHEN** fonts.title.parent: "@title"(引用自身)
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
#### Scenario: 间接循环引用(单域)
- **WHEN** fonts.a.parent: "@b" 且 fonts.b.parent: "@a"
- **THEN** 系统抛出 ERROR显示完整的引用循环路径
#### Scenario: 跨域循环引用
- **WHEN** 文档 fonts.a 引用模板库 fonts.b
- **AND** 模板库 fonts.b 引用模板库 fonts.c
- **AND** 模板库 fonts.c 引用文档 fonts.a
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
- **AND** 错误消息包含"检测到跨域字体引用循环"
#### Scenario: 深层引用但不循环
- **WHEN** 引用链深度超过 5 层但没有循环
- **THEN** 系统成功解析
#### Scenario: 引用链深度超过限制
- **WHEN** 引用链深度超过 10 层
- **THEN** 系统抛出 ERROR提示引用深度超限
#### Scenario: 错误信息包含引用路径
- **WHEN** 系统检测到循环引用
- **THEN** 错误信息包含完整的引用路径
### Requirement: 系统必须支持属性继承链
字体属性解析 SHALL 按照优先级顺序继承parent → 当前定义 → fonts_default → 系统默认。跨域继承时,文档可继承模板库的,模板库不能继承文档的。
#### Scenario: parent 定义了基础属性
- **WHEN** fonts.title 定义 size: 44元素定义 font: {parent: "@title", bold: true}
- **THEN** 元素使用 size: 44继承、bold: true覆盖
#### Scenario: 文档字体 parent 继承模板库字体属性
- **WHEN** 文档 fonts.custom 定义 parent: "@template-base"
- **AND** 模板库 fonts.base 定义 family: "Arial", size: 18
- **THEN** custom 继承 family 和 size
#### Scenario: 模板库字体 parent 不能继承文档字体属性
- **WHEN** 模板库 fonts.custom 定义 parent: "@doc-base"
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_PARENT_REF_DOC_FORBIDDEN`
#### Scenario: parent 未定义的属性继承 fonts_default
- **WHEN** fonts_default 定义 size: 18元素定义 font: {parent: "@title"} 且 title 未定义 size
- **THEN** 元素使用 size: 18从 fonts_default 继承)
#### Scenario: 当前定义覆盖 parent
- **WHEN** parent 定义 size: 44当前定义 size: 60
- **THEN** 元素使用 size: 60当前定义覆盖 parent
### Requirement: 模板元素必须支持继承 fonts_default
模板中未定义 font 的元素 SHALL 继承 fonts_default 配置。对于外部模板,级联顺序为:文档 fonts_default → 模板库 fonts_default → 系统默认。
#### Scenario: 外部模板元素未定义 font文档有 fonts_default
- **WHEN** 外部模板元素未定义 font
- **AND** 文档定义了 metadata.fonts_default: "@body"
- **THEN** 元素使用文档的 fonts_default 配置
#### Scenario: 外部模板元素未定义 font文档没有 fonts_default
- **WHEN** 外部模板元素未定义 font
- **AND** 文档未定义 fonts_default
- **AND** 模板库定义了 metadata.fonts_default: "@base"
- **THEN** 元素使用模板库的 fonts_default 配置
#### Scenario: 内联模板元素未定义 font
- **WHEN** 内联模板(文档中定义)元素未定义 font
- **THEN** 元素继承文档的 fonts_default 配置
#### Scenario: 模板元素定义了 font
- **WHEN** 模板元素定义 font: "@title"
- **THEN** 元素使用 font: "@title",不继承 fonts_default
#### Scenario: fonts_default 未定义时模板元素行为
- **WHEN** 模板元素未定义 font
- **AND** 文档和模板库都未定义 fonts_default
- **THEN** 元素使用系统默认字体

View File

@@ -0,0 +1,173 @@
# Template Library Delta Spec
## ADDED Requirements
### Requirement: 模板库必须包含 metadata 结构
模板库文件 SHALL 必须包含 metadata 字段,与文档使用相同的结构,包含 `size`(必填)、`version``author``description``fonts``fonts_default`
#### Scenario: 模板库包含完整的 metadata
- **WHEN** 模板库文件包含 metadata 字段,包含 size、version、author、description、fonts、fonts_default
- **THEN** 系统成功解析并存储这些元数据
#### Scenario: 模板库缺少 metadata 字段
- **WHEN** 模板库文件不包含 metadata 字段
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_LIBRARY_MISSING_METADATA`
- **AND** 错误消息包含"模板库必须包含 metadata 字段"
#### Scenario: 模板库 metadata 缺少 size 字段
- **WHEN** 模板库包含 metadata 但缺少 size 字段
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_LIBRARY_METADATA_MISSING_SIZE`
- **AND** 错误消息包含"模板库 metadata 缺少必填字段 'size'"
#### Scenario: 模板库 metadata 的 size 值无效
- **WHEN** 模板库 metadata.size 不是有效的尺寸值(如 "16:9" 或 "4:3"
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_LIBRARY_METADATA_INVALID_SIZE`
### Requirement: 模板库 metadata 必须使用与文档相同的验证规则
模板库 metadata 的字段验证 SHALL 与文档 metadata 使用相同的规则。
#### Scenario: 模板库 metadata.fonts 验证
- **WHEN** 模板库 metadata.fonts 中定义字体配置
- **THEN** 系统使用与文档 fonts 相同的验证规则
#### Scenario: 模板库 metadata.fonts_default 验证
- **WHEN** 模板库 metadata.fonts_default 引用字体配置
- **THEN** 系统验证引用存在于模板库 metadata.fonts 中
#### Scenario: 模板库 metadata.version 和 author 可选
- **WHEN** 模板库 metadata 包含或不含 version、author 字段
- **THEN** 系统正常处理,这些字段为可选项
### Requirement: 系统必须校验文档和模板库的 size 一致性
系统 SHALL 在加载模板库时校验文档的 metadata.size 与模板库的 metadata.size 是否一致。
#### Scenario: 文档和模板库 size 一致
- **WHEN** 文档 metadata.size 为 "16:9" 且模板库 metadata.size 也为 "16:9"
- **THEN** 系统正常加载,不报错
#### Scenario: 文档和模板库 size 不一致
- **WHEN** 文档 metadata.size 为 "16:9" 但模板库 metadata.size 为 "4:3"
- **THEN** 系统抛出 ERROR错误代码为 `SIZE_MISMATCH`
- **AND** 错误消息包含"文档尺寸 '16:9' 与模板库尺寸 '4:3' 不一致"
### Requirement: 模板库 fonts 的 parent 只能引用模板库内部
模板库 metadata.fonts 中定义的字体配置,其 parent 字段 SHALL 只能引用模板库内部的字体配置。
#### Scenario: parent 引用模板库内部的字体
- **WHEN** 模板库 metadata.fonts 中定义 heading: {parent: "@base"} 且 base 存在于模板库 fonts 中
- **THEN** 系统成功解析继承关系
#### Scenario: parent 引用文档的字体
- **WHEN** 模板库 metadata.fonts 中定义 custom: {parent: "@doc-font"}
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_PARENT_REF_DOC_FORBIDDEN`
- **AND** 错误消息包含"模板库字段的 parent 不能引用文档的字体配置"
#### Scenario: parent 引用不存在的字体
- **WHEN** 模板库 metadata.fonts 中定义 heading: {parent: "@nonexistent"}
- **THEN** 系统抛出 ERROR错误代码为 `FONT_NOT_FOUND`
- **AND** 错误消息包含"引用的字体配置不存在"
### Requirement: 外部模板元素只能引用模板库的 fonts
外部模板(模板库中定义的模板)中的元素 SHALL 只能引用模板库 metadata.fonts 中定义的字体配置。
#### Scenario: 模板元素引用模板库的字体
- **WHEN** 外部模板元素定义 font: "@title" 且 title 存在于模板库 fonts 中
- **THEN** 系统成功应用字体配置
#### Scenario: 模板元素引用文档的字体
- **WHEN** 外部模板元素定义 font: "@doc-title" 且 doc-title 只存在于文档 fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_FONT_REF_DOC_FORBIDDEN`
- **AND** 错误消息包含"模板元素不能引用文档的字体配置"
#### Scenario: 模板元素引用同名字体(只使用模板库的)
- **WHEN** 外部模板元素定义 font: "@title"
- **AND** title 同时存在于文档 fonts 和模板库 fonts 中
- **THEN** 系统只使用模板库的 title 配置(不查找文档)
### Requirement: 模板元素的 fonts_default 级联规则
外部模板元素未定义 font 时 SHALL 级联使用 fonts_default优先文档 fonts_default文档没有则使用模板库 fonts_default。
#### Scenario: 模板元素未定义 font文档有 fonts_default
- **WHEN** 模板元素未定义 font
- **AND** 文档定义了 metadata.fonts_default: "@body"
- **THEN** 元素使用文档的 fonts_default 配置
#### Scenario: 模板元素未定义 font文档没有 fonts_default
- **WHEN** 模板元素未定义 font
- **AND** 文档未定义 fonts_default
- **AND** 模板库定义了 metadata.fonts_default: "@base"
- **THEN** 元素使用模板库的 fonts_default 配置
#### Scenario: 模板元素未定义 font都没有 fonts_default
- **WHEN** 模板元素未定义 font
- **AND** 文档和模板库都未定义 fonts_default
- **THEN** 元素使用系统默认字体
### Requirement: 表格元素的双字体字段应用相同规则
表格元素的 font 和 header_font 字段 SHALL 应用与普通元素相同的字体引用规则。
#### Scenario: 表格的 font 引用模板库字体
- **WHEN** 模板中的表格定义 font: "@table-body"
- **AND** table-body 存在于模板库 fonts 中
- **THEN** 系统成功应用字体配置
#### Scenario: 表格的 header_font 引用模板库字体
- **WHEN** 模板中的表格定义 header_font: "@table-header"
- **AND** table-header 存在于模板库 fonts 中
- **THEN** 系统成功应用字体配置
#### Scenario: 表格的 font 为 null
- **WHEN** 模板中的表格定义 font: null 或未定义
- **THEN** 系统按 fonts_default 级联规则处理
## MODIFIED Requirements
### Requirement: 模板库文件必须包含元数据字段
模板库文件 SHALL 必须包含 metadata 字段,元数据结构与文档相同,包含 `size`(必填)、`version``author``description``fonts``fonts_default`
#### Scenario: 加载包含完整元数据的模板库文件
- **WHEN** 模板库文件包含 metadata 字段,包含 size、version、author、description、fonts、fonts_default
- **THEN** 系统成功加载这些元数据字段
- **AND** size 字段必须为有效值
- **AND** fonts 和 fonts_default 按字体主题规则验证
#### Scenario: 模板库 metadata 缺少必填的 size 字段
- **WHEN** 模板库 metadata 中缺少 size 字段
- **THEN** 系统抛出错误,错误代码为 `TEMPLATE_LIBRARY_METADATA_MISSING_SIZE`
#### Scenario: 模板库缺少 metadata 字段
- **WHEN** 模板库文件不包含 metadata 字段
- **THEN** 系统抛出错误,错误代码为 `TEMPLATE_LIBRARY_MISSING_METADATA`
- **AND** 错误消息包含"模板库必须包含 metadata 字段"

View File

@@ -0,0 +1,84 @@
# Implementation Tasks
## 0. 清理旧代码和测试
- [x] 0.1 识别并标记需要清理的旧测试
- [x] 0.2 清理 tests/unit/test_template.py 中向后兼容性相关测试
- [x] 0.3 清理 tests/integration/test_template_library.py 中向后兼容性相关测试
- [x] 0.4 清理旧版本的模板库元数据处理逻辑
## 1. YAML 加载和验证
- [x] 1.1 在 loaders/yaml_loader.py 中创建 validate_metadata() 函数
- [x] 1.2 修改 validate_presentation_yaml() 调用 validate_metadata()
- [x] 1.3 修改 validate_template_library_yaml() 验证模板库必须包含 metadata
- [x] 1.4 在 validate_template_library_yaml() 中验证模板库 fonts_default 只能引用模板库内部字体
## 2. Size 一致性校验
- [x] 2.1 在 core/presentation.py 中添加 size 一致性校验
- [x] 2.2 添加 size 一致性校验的单元测试
## 3. FontResolver 跨域支持
- [x] 3.1 扩展 FontResolver 类支持 scope 和 template_fonts 参数
- [x] 3.2 修改 _resolve_reference() 方法根据作用域限制引用范围
- [x] 3.3 实现跨域循环引用检测
- [x] 3.4 添加 parent 引用的跨域限制
## 4. Presentation 类扩展
- [x] 4.1 在 Presentation 中解析和存储模板库 metadata.fonts
- [x] 4.2 创建文档作用域的 FontResolver
- [x] 4.3 创建模板库作用域的 FontResolver
## 5. Template 类扩展
- [x] 5.1 修改 Template.from_data() 接收 scope 和 font_resolver 参数
- [x] 5.2 修改 Template.render() 应用正确的 FontResolver 和 fonts_default 级联
- [x] 5.3 添加表格双字体字段的处理
## 6. 错误代码定义
- [x] 6.1 在 validators/result.py 中添加新的错误代码:
- 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 无效
## 7. 单元测试
- [x] 7.1 添加 validate_metadata() 的单元测试
- [x] 7.2 添加 FontResolver 跨域引用的单元测试
- [x] 7.3 添加 fonts_default 级联的单元测试
- [x] 7.4 添加跨域循环引用检测的单元测试(包括单域内循环和跨域循环)
- [x] 7.5 添加模板库 fonts_default 验证的单元测试
## 8. 集成测试
- [x] 8.1 添加跨域字体引用的集成测试
- [x] 8.2 添加 size 一致性校验的集成测试
- [x] 8.3 添加完整渲染流程的集成测试
- [x] 8.4 添加表格双字体字段font 和 header_font的集成测试
## 9. 文档更新
- [x] 9.1 更新 README.md 添加模板库 metadata 说明和示例
- [x] 9.2 更新 README_DEV.md 记录错误代码和作用域规则
- [x] 9.3 添加模板库示例文件
## 10. 代码清理和格式化
- [x] 10.1 验证旧测试已完全清理
- [x] 10.2 代码格式化和添加中文注释
## 11. 最终测试
- [x] 11.1 运行完整的测试套件
- [x] 11.2 手动测试关键场景
- [x] 11.3 确保所有测试通过

View File

@@ -6,6 +6,6 @@ context: |
严禁直接使用主机环境的python直接执行脚本或命令严禁在主机环境直接安装python依赖包
本项目编写的非正式测试文件、临时文件必须放在temp目录下
严禁污染主机环境的任何配置,如有需要,必须请求用户审核操作;
当前项目的面向用户的使用文档在README.md当前项目的面向AI和开发者的开发规范文档在README_DEV.md每次功能迭代都需要同步更新这两份说明文档
当前项目的面向用户的使用文档在README.md当前项目的面向AI和开发者的开发规范文档在README_DEV.md每次功能迭代都需要同步更新这两份说明文档目前项目还没有上线,没有用户使用,遇到破坏性变更不需要特殊说明和迁移说明,只需要正常更新文档的相关内容即可;
所有的文档、日志、说明严禁使用emoji或其他特殊字符保证字符显示的兼容性
所有的需求都必须设计全面、合理、完善、有针对性的测试内容;

View File

@@ -2,17 +2,22 @@
## Purpose
字体主题系统提供可复用的字体配置管理能力,允许用户在 metadata 中定义字体配置模板,通过引用方式应用到元素,实现统一的字体样式管理。
字体主题系统提供可复用的字体配置管理能力,允许用户在 metadata 中定义字体配置模板,通过引用方式应用到元素,实现统一的字体样式管理。支持文档和模板库的字体作用域隔离,以及跨域字体引用。
## Requirements
### Requirement: 系统必须支持在 metadata 中定义 fonts 字段
系统 SHALL 支持在 YAML metadata 中定义 fonts 字段,用于存储可复用的字体配置。
系统 SHALL 支持在文档和模板库的 YAML metadata 中定义 fonts 字段,用于存储可复用的字体配置。模板库的 fonts 与文档的 fonts 使用相同的定义和验证规则。
#### Scenario: 定义 fonts 字段
#### Scenario: 定义 fonts 字段(文档)
- **WHEN** metadata 中定义 fonts 字段
- **WHEN** 文档 metadata 中定义 fonts 字段
- **THEN** 系统成功解析并存储字体配置字典
#### Scenario: 定义 fonts 字段(模板库)
- **WHEN** 模板库 metadata 中定义 fonts 字段
- **THEN** 系统成功解析并存储字体配置字典
#### Scenario: fonts 字段为空字典
@@ -39,15 +44,48 @@ fonts 字段 SHALL 包含一个或多个命名字体配置,每个配置是一
- **WHEN** fonts 中定义 title、subtitle、body 等多个配置
- **THEN** 系统为每个配置创建独立的字体对象
### Requirement: 文档 metadata 必须支持 version 和 author 字段
文档 metadata SHALL 支持可选的 `version``author` 字段。
#### Scenario: 文档包含 version 和 author
- **WHEN** 文档 metadata 包含 version: "1.0" 和 author: "作者名"
- **THEN** 系统成功解析并存储这些字段
#### Scenario: 文档不包含 version 和 author
- **WHEN** 文档 metadata 不包含 version 和 author 字段
- **THEN** 系统正常处理,这些字段为可选项
### Requirement: 系统必须支持 fonts_default 字段
系统 SHALL 支持在 metadata 中定义可选的 fonts_default 字段,指定默认字体配置的引用。
系统 SHALL 支持在文档和模板库的 metadata 中定义可选的 fonts_default 字段,指定默认字体配置的引用。文档的 fonts_default 可以引用文档或模板库的 fonts模板库的 fonts_default 只能引用模板库的 fonts。
#### Scenario: 定义 fonts_default 引用
#### Scenario: 定义 fonts_default 引用(文档)
- **WHEN** metadata 中定义 fonts_default: "@body"
- **WHEN** 文档 metadata 中定义 fonts_default: "@body"
- **AND** body 存在于文档 metadata.fonts 中
- **THEN** 系统将 fonts_default 解析为对 fonts.body 的引用
#### Scenario: 定义 fonts_default 引用(模板库)
- **WHEN** 模板库 metadata 中定义 fonts_default: "@base"
- **AND** base 存在于模板库 metadata.fonts 中
- **THEN** 系统将 fonts_default 解析为对 fonts.base 的引用
#### Scenario: 文档 fonts_default 引用模板库字体
- **WHEN** 文档 metadata.fonts_default: "@template-base"
- **AND** template-base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功解析默认字体
#### Scenario: 模板库 fonts_default 不能引用文档字体
- **WHEN** 模板库 metadata.fonts_default: "@doc-body"
- **AND** doc-body 只存在于文档 metadata.fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
#### Scenario: fonts_default 未定义
- **WHEN** metadata 中未定义 fonts_default 字段
@@ -55,27 +93,82 @@ fonts 字段 SHALL 包含一个或多个命名字体配置,每个配置是一
#### Scenario: fonts_default 引用不存在的配置
- **WHEN** fonts_default: "@undefined" 且 fonts.undefined 不存在
- **THEN** 系统抛出 ERROR提示引用的字体配置不存在
- **WHEN** fonts_default: "@undefined" 且 undefined 不存在
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
#### Scenario: fonts_default 必须引用 fonts 中的配置
#### Scenario: fonts_default 必须引用格式
- **WHEN** fonts_default: "Arial"(直接字体名称)
- **THEN** 系统抛出 ERROR提示 fonts_default 必须是引用格式
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
### Requirement: 元素 font 字段必须支持三种引用方式
### Requirement: 文档元素和内联模板元素必须支持跨域引用字体
元素 font 字段 SHALL 支持字符串引用(整体引用)、字典引用(继承覆盖或独立定义)两种形式
文档中的元素和内联模板(文档中定义的 templates中的元素 SHALL 优先引用文档的 fonts文档没有时可引用模板库的 fonts
#### Scenario: 字符串整体引用
#### Scenario: 元素引用文档中存在的字体
- **WHEN** 元素定义 font: "@title"
- **THEN** 系统使用 fonts.title 的所有属性
- **WHEN** 文档元素定义 font: "@title"
- **AND** title 存在于文档 metadata.fonts 中
- **THEN** 系统使用文档的 title 配置
#### Scenario: 元素引用文档不存在但模板库存在的字体
- **WHEN** 文档元素定义 font: "@template-title"
- **AND** template-title 不存在于文档 fonts 中
- **AND** template-title 存在于模板库 metadata.fonts 中
- **THEN** 系统使用模板库的 template-title 配置
#### Scenario: 元素引用不存在的字体(文档和模板库都没有)
- **WHEN** 文档元素定义 font: "@nonexistent"
- **AND** nonexistent 不存在于文档和模板库的 fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_NOT_FOUND`
- **AND** 错误消息包含"引用的字体配置不存在"
#### Scenario: 同名字体时优先使用文档的
- **WHEN** 文档元素定义 font: "@title"
- **AND** title 同时存在于文档 fonts 和模板库 fonts 中
- **THEN** 系统使用文档的 title 配置(不使用模板库的)
#### Scenario: 内联模板元素引用文档字体
- **WHEN** 内联模板(文档 templates 中定义)的元素定义 font: "@body"
- **AND** body 存在于文档 metadata.fonts 中
- **THEN** 系统使用文档的 body 配置
#### Scenario: 内联模板元素引用模板库字体
- **WHEN** 内联模板的元素定义 font: "@template-body"
- **AND** template-body 只存在于模板库 metadata.fonts 中
- **THEN** 系统使用模板库的 template-body 配置
### Requirement: 元素 font 字段必须支持多种引用方式
元素 font 字段 SHALL 支持字符串引用(整体引用)、字典引用(继承覆盖或独立定义)两种形式。引用的目标根据元素所在作用域决定。
#### Scenario: 文档元素引用文档字体
- **WHEN** 文档元素定义 font: "@title"
- **AND** title 存在于文档 metadata.fonts 中
- **THEN** 系统使用文档的 title 配置
#### Scenario: 文档元素引用模板库字体
- **WHEN** 文档元素定义 font: "@template-title"
- **AND** template-title 只存在于模板库 metadata.fonts 中
- **THEN** 系统使用模板库的 template-title 配置
#### Scenario: 模板元素只能引用模板库字体
- **WHEN** 外部模板元素定义 font: "@title"
- **AND** title 只存在于文档 metadata.fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_FONT_REF_DOC_FORBIDDEN`
#### Scenario: 字典继承覆盖
- **WHEN** 元素定义 font: {parent: "@title", size: 60}
- **THEN** 系统继承 fonts.title 的所有属性,覆盖 size 为 60
- **THEN** 系统继承引用字体的所有属性,覆盖 size 为 60
#### Scenario: 字典独立定义
@@ -87,40 +180,131 @@ fonts 字段 SHALL 包含一个或多个命名字体配置,每个配置是一
- **WHEN** 元素未定义 font 字段
- **THEN** 元素使用 fonts_default 或系统默认字体
### Requirement: parent 字段必须引用 fonts 中的配置
### Requirement: 文档 fonts 的 parent 必须支持跨域引用
font 字典中的 parent 字段 SHALL 引用 fonts 中已定义的配置名称
文档 metadata.fonts 中定义的字体配置,其 parent 字段 SHALL 可以引用文档内部的字体配置或模板库的字体配置
#### Scenario: parent 引用存在的配置
#### Scenario: parent 引用文档内部的字体
- **WHEN** parent: "@title" 且 fonts.title 存在
- **THEN** 系统成功继承 fonts.title 的属性
- **WHEN** 文档 metadata.fonts 中定义 heading: {parent: "@base"}
- **AND** base 存在于文档 fonts 中
- **THEN** 系统成功继承 base 的属性
#### Scenario: parent 引用模板库的字体
- **WHEN** 文档 metadata.fonts 中定义 custom: {parent: "@template-base"}
- **AND** template-base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功继承 template-base 的属性
#### Scenario: parent 引用不存在的字体(文档和模板库都没有)
- **WHEN** 文档 metadata.fonts 中定义 heading: {parent: "@nonexistent"}
- **AND** nonexistent 不存在于文档和模板库的 fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_NOT_FOUND`
- **AND** 错误消息包含"引用的字体配置不存在"
#### Scenario: 同名字体时 parent 优先引用文档的
- **WHEN** 文档 metadata.fonts 中定义 heading: {parent: "@base"}
- **AND** base 同时存在于文档 fonts 和模板库 fonts 中
- **THEN** 系统使用文档的 base 配置
### Requirement: parent 字段必须引用有效域中的字体配置
font 字典中的 parent 字段 SHALL 引用有效域中的配置。文档 fonts 的 parent 可以引用文档或模板库的 fonts模板库 fonts 的 parent 只能引用模板库的 fonts。
#### Scenario: 文档字体 parent 引用文档字体
- **WHEN** 文档 fonts 中定义 heading: {parent: "@title"}
- **AND** title 存在于文档 metadata.fonts 中
- **THEN** 系统成功继承 title 的属性
#### Scenario: 文档字体 parent 引用模板库字体
- **WHEN** 文档 fonts 中定义 custom: {parent: "@template-base"}
- **AND** template-base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功继承 template-base 的属性
#### Scenario: 模板库字体 parent 引用模板库字体
- **WHEN** 模板库 fonts 中定义 heading: {parent: "@base"}
- **AND** base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功继承 base 的属性
#### Scenario: 模板库字体 parent 引用文档字体(禁止)
- **WHEN** 模板库 fonts 中定义 custom: {parent: "@doc-base"}
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_PARENT_REF_DOC_FORBIDDEN`
#### Scenario: parent 引用不存在的配置
- **WHEN** parent: "@undefined" 且 fonts.undefined 不存在
- **THEN** 系统抛出 ERROR提示引用的字体配置不存在
- **WHEN** parent: "@undefined" 且 undefined 不存在
- **THEN** 系统抛出 ERROR错误代码为 `FONT_NOT_FOUND`
#### Scenario: parent 必须是引用格式
- **WHEN** parent: "Arial"(直接字体名称)
- **THEN** 系统抛出 ERROR,提示 parent 必须是引用格式
- **THEN** 系统抛出 ERROR
### Requirement: 系统必须检测跨域循环引用
系统 SHALL 在解析字体引用时检测跨域循环引用,包括文档和模板库之间的循环。
#### Scenario: 文档内部循环引用
- **WHEN** 文档 fonts.a.parent: "@b" 且 fonts.b.parent: "@a"
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
- **AND** 错误消息包含"检测到字体引用循环"和完整路径
#### Scenario: 模板库内部循环引用
- **WHEN** 模板库 fonts.x.parent: "@y" 且 fonts.y.parent: "@x"
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
- **AND** 错误消息包含"检测到字体引用循环"和完整路径
#### Scenario: 跨域循环引用
- **WHEN** 文档 fonts.a.parent: "@template-b"
- **AND** 模板库 fonts.b.parent: "@template-c"
- **AND** 模板库 fonts.c.parent: "@doc-a"
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
- **AND** 错误消息包含"检测到跨域字体引用循环"和完整路径
#### Scenario: 跨域引用链不循环
- **WHEN** 文档 fonts.a.parent: "@template-b"
- **AND** 模板库 fonts.b.parent: "@template-c"
- **AND** 模板库 fonts.c 没有引用其他字体
- **THEN** 系统成功解析,不报错
### Requirement: 系统必须检测并拒绝引用循环
系统 SHALL 在解析字体引用时检测循环引用,检测到循环时抛出 ERROR。
系统 SHALL 在解析字体引用时检测循环引用,包括单域内部循环和跨域循环,检测到循环时抛出 ERROR。
#### Scenario: 直接循环引用
- **WHEN** fonts.title.parent: "@title"(引用自身)
- **THEN** 系统抛出 ERROR提示检测到自引用
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
#### Scenario: 间接循环引用
#### Scenario: 间接循环引用(单域)
- **WHEN** fonts.a.parent: "@b" 且 fonts.b.parent: "@a"
- **THEN** 系统抛出 ERROR显示完整的引用循环路径
#### Scenario: 深层循环引用
#### Scenario: 跨域循环引用
- **WHEN** 文档 fonts.a 引用模板库 fonts.b
- **AND** 模板库 fonts.b 引用模板库 fonts.c
- **AND** 模板库 fonts.c 引用文档 fonts.a
- **THEN** 系统抛出 ERROR错误代码为 `CIRCULAR_REFERENCE`
- **AND** 错误消息包含"检测到跨域字体引用循环"
#### Scenario: 深层引用但不循环
- **WHEN** 引用链深度超过 5 层但没有循环
- **THEN** 系统成功解析
#### Scenario: 引用链深度超过限制
- **WHEN** 引用链深度超过 10 层
- **THEN** 系统抛出 ERROR提示引用深度超限
@@ -128,17 +312,64 @@ font 字典中的 parent 字段 SHALL 引用 fonts 中已定义的配置名称
#### Scenario: 错误信息包含引用路径
- **WHEN** 系统检测到循环引用
- **THEN** 错误信息包含完整的引用路径,如 "fonts.title -> fonts.subtitle -> fonts.title"
- **THEN** 错误信息包含完整的引用路径
### Requirement: 文档和模板库的 fonts_default 必须独立验证
文档的 metadata.fonts_default SHALL 只能引用文档或模板库中存在的字体配置,模板库的 metadata.fonts_default SHALL 只能引用模板库中存在的字体配置。
#### Scenario: 文档 fonts_default 引用文档字体
- **WHEN** 文档 metadata.fonts_default: "@body"
- **AND** body 存在于文档 metadata.fonts 中
- **THEN** 系统成功解析默认字体
#### Scenario: 文档 fonts_default 引用模板库字体
- **WHEN** 文档 metadata.fonts_default: "@template-base"
- **AND** template-base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功解析默认字体
#### Scenario: 文档 fonts_default 引用不存在的字体
- **WHEN** 文档 metadata.fonts_default: "@nonexistent"
- **AND** nonexistent 不存在于文档和模板库的 fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
- **AND** 错误消息包含"fonts_default 引用的字体配置不存在"
#### Scenario: 模板库 fonts_default 引用模板库字体
- **WHEN** 模板库 metadata.fonts_default: "@base"
- **AND** base 存在于模板库 metadata.fonts 中
- **THEN** 系统成功解析默认字体
#### Scenario: 模板库 fonts_default 引用文档字体
- **WHEN** 模板库 metadata.fonts_default: "@doc-body"
- **AND** doc-body 只存在于文档 metadata.fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `FONT_DEFAULT_INVALID`
- **AND** 错误消息包含"模板库 fonts_default 只能引用模板库内部的字体配置"
### Requirement: 系统必须支持属性继承链
字体属性解析 SHALL 按照优先级顺序继承parent → 当前定义 → fonts_default → 系统默认。
字体属性解析 SHALL 按照优先级顺序继承parent → 当前定义 → fonts_default → 系统默认。跨域继承时,文档可继承模板库的,模板库不能继承文档的。
#### Scenario: parent 定义了基础属性
- **WHEN** fonts.title 定义 size: 44元素定义 font: {parent: "@title", bold: true}
- **THEN** 元素使用 size: 44继承、bold: true覆盖
#### Scenario: 文档字体 parent 继承模板库字体属性
- **WHEN** 文档 fonts.custom 定义 parent: "@template-base"
- **AND** 模板库 fonts.base 定义 family: "Arial", size: 18
- **THEN** custom 继承 family 和 size
#### Scenario: 模板库字体 parent 不能继承文档字体属性
- **WHEN** 模板库 fonts.custom 定义 parent: "@doc-base"
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_PARENT_REF_DOC_FORBIDDEN`
#### Scenario: parent 未定义的属性继承 fonts_default
- **WHEN** fonts_default 定义 size: 18元素定义 font: {parent: "@title"} 且 title 未定义 size
@@ -151,12 +382,25 @@ font 字典中的 parent 字段 SHALL 引用 fonts 中已定义的配置名称
### Requirement: 模板元素必须支持继承 fonts_default
模板中未定义 font 的元素 SHALL 继承 metadata.fonts_default 配置
模板中未定义 font 的元素 SHALL 继承 fonts_default 配置。对于外部模板,级联顺序为:文档 fonts_default → 模板库 fonts_default → 系统默认
#### Scenario: 模板元素未定义 font
#### Scenario: 外部模板元素未定义 font,文档有 fonts_default
- **WHEN** 模板元素未定义 font 字段
- **THEN** 元素继承 metadata.fonts_default 的配置
- **WHEN** 外部模板元素未定义 font
- **AND** 文档定义了 metadata.fonts_default: "@body"
- **THEN** 元素使用文档的 fonts_default 配置
#### Scenario: 外部模板元素未定义 font文档没有 fonts_default
- **WHEN** 外部模板元素未定义 font
- **AND** 文档未定义 fonts_default
- **AND** 模板库定义了 metadata.fonts_default: "@base"
- **THEN** 元素使用模板库的 fonts_default 配置
#### Scenario: 内联模板元素未定义 font
- **WHEN** 内联模板(文档中定义)元素未定义 font
- **THEN** 元素继承文档的 fonts_default 配置
#### Scenario: 模板元素定义了 font
@@ -165,7 +409,8 @@ font 字典中的 parent 字段 SHALL 引用 fonts 中已定义的配置名称
#### Scenario: fonts_default 未定义时模板元素行为
- **WHEN** 模板元素未定义 font 且 metadata 未定义 fonts_default
- **WHEN** 模板元素未定义 font
- **AND** 文档和模板库都未定义 fonts_default
- **THEN** 元素使用系统默认字体
### Requirement: 表格元素必须支持 font 和 header_font 字段

View File

@@ -2,7 +2,7 @@
## Purpose
Template library 提供集中的模板管理机制,允许将多个模板定义存储在单个 YAML 文件中,通过模板名称引用。
Template library 提供集中的模板管理机制,允许将多个模板定义存储在单个 YAML 文件中,通过模板名称引用。模板库支持独立的 metadata 结构,包括字体主题和尺寸定义。
## Requirements
@@ -28,20 +28,173 @@ Template library 提供集中的模板管理机制,允许将多个模板定义
- **THEN** 系统抛出错误,错误代码为 `TEMPLATE_LIBRARY_MISSING_TEMPLATES_FIELD`
- **AND** 错误消息包含"'templates' 必须是字典"
### Requirement: 模板库文件必须支持元数据字段
### Requirement: 模板库文件必须包含元数据字段
模板库文件 SHALL 支持可选的元数据字段,包 `description``version``author`
模板库文件 SHALL 必须包含 metadata 字段,元数据结构与文档相同,包 `size`(必填)、`version``author``description``fonts``fonts_default`
#### Scenario: 加载包含元数据的模板库文件
#### Scenario: 加载包含完整元数据的模板库文件
- **WHEN** 模板库文件包含 `description``version``author` 等字段
- **WHEN** 模板库文件包含 metadata 字段,包含 size、versionauthor、description、fonts、fonts_default
- **THEN** 系统成功加载这些元数据字段
- **AND** 元数据不影响模板的加载和使用
- **AND** size 字段必须为有效值
- **AND** fonts 和 fonts_default 按字体主题规则验证
#### Scenario: 不包含元数据字段时正常加载
#### Scenario: 模板库 metadata 缺少必填的 size 字段
- **WHEN** 模板库文件仅包含 `templates` 字段,不包含任何元数据
- **THEN** 系统正常加载,不要求元数据字段
- **WHEN** 模板库 metadata 中缺少 size 字段
- **THEN** 系统抛出错误,错误代码为 `TEMPLATE_LIBRARY_METADATA_MISSING_SIZE`
#### Scenario: 模板库缺少 metadata 字段
- **WHEN** 模板库文件不包含 metadata 字段
- **THEN** 系统抛出错误,错误代码为 `TEMPLATE_LIBRARY_MISSING_METADATA`
- **AND** 错误消息包含"模板库必须包含 metadata 字段"
### Requirement: 模板库必须包含 metadata 结构
模板库文件 SHALL 必须包含 metadata 字段,与文档使用相同的结构,包含 `size`(必填)、`version``author``description``fonts``fonts_default`
#### Scenario: 模板库包含完整的 metadata
- **WHEN** 模板库文件包含 metadata 字段,包含 size、version、author、description、fonts、fonts_default
- **THEN** 系统成功解析并存储这些元数据
#### Scenario: 模板库缺少 metadata 字段
- **WHEN** 模板库文件不包含 metadata 字段
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_LIBRARY_MISSING_METADATA`
- **AND** 错误消息包含"模板库必须包含 metadata 字段"
#### Scenario: 模板库 metadata 缺少 size 字段
- **WHEN** 模板库包含 metadata 但缺少 size 字段
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_LIBRARY_METADATA_MISSING_SIZE`
- **AND** 错误消息包含"模板库 metadata 缺少必填字段 'size'"
#### Scenario: 模板库 metadata 的 size 值无效
- **WHEN** 模板库 metadata.size 不是有效的尺寸值(如 "16:9" 或 "4:3"
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_LIBRARY_METADATA_INVALID_SIZE`
### Requirement: 模板库 metadata 必须使用与文档相同的验证规则
模板库 metadata 的字段验证 SHALL 与文档 metadata 使用相同的规则。
#### Scenario: 模板库 metadata.fonts 验证
- **WHEN** 模板库 metadata.fonts 中定义字体配置
- **THEN** 系统使用与文档 fonts 相同的验证规则
#### Scenario: 模板库 metadata.fonts_default 验证
- **WHEN** 模板库 metadata.fonts_default 引用字体配置
- **THEN** 系统验证引用存在于模板库 metadata.fonts 中
#### Scenario: 模板库 metadata.version 和 author 可选
- **WHEN** 模板库 metadata 包含或不含 version、author 字段
- **THEN** 系统正常处理,这些字段为可选项
### Requirement: 系统必须校验文档和模板库的 size 一致性
系统 SHALL 在加载模板库时校验文档的 metadata.size 与模板库的 metadata.size 是否一致。
#### Scenario: 文档和模板库 size 一致
- **WHEN** 文档 metadata.size 为 "16:9" 且模板库 metadata.size 也为 "16:9"
- **THEN** 系统正常加载,不报错
#### Scenario: 文档和模板库 size 不一致
- **WHEN** 文档 metadata.size 为 "16:9" 但模板库 metadata.size 为 "4:3"
- **THEN** 系统抛出 ERROR错误代码为 `SIZE_MISMATCH`
- **AND** 错误消息包含"文档尺寸 '16:9' 与模板库尺寸 '4:3' 不一致"
### Requirement: 模板库 fonts 的 parent 只能引用模板库内部
模板库 metadata.fonts 中定义的字体配置,其 parent 字段 SHALL 只能引用模板库内部的字体配置。
#### Scenario: parent 引用模板库内部的字体
- **WHEN** 模板库 metadata.fonts 中定义 heading: {parent: "@base"} 且 base 存在于模板库 fonts 中
- **THEN** 系统成功解析继承关系
#### Scenario: parent 引用文档的字体
- **WHEN** 模板库 metadata.fonts 中定义 custom: {parent: "@doc-font"}
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_PARENT_REF_DOC_FORBIDDEN`
- **AND** 错误消息包含"模板库字段的 parent 不能引用文档的字体配置"
#### Scenario: parent 引用不存在的字体
- **WHEN** 模板库 metadata.fonts 中定义 heading: {parent: "@nonexistent"}
- **THEN** 系统抛出 ERROR错误代码为 `FONT_NOT_FOUND`
- **AND** 错误消息包含"引用的字体配置不存在"
### Requirement: 外部模板元素只能引用模板库的 fonts
外部模板(模板库中定义的模板)中的元素 SHALL 只能引用模板库 metadata.fonts 中定义的字体配置。
#### Scenario: 模板元素引用模板库的字体
- **WHEN** 外部模板元素定义 font: "@title" 且 title 存在于模板库 fonts 中
- **THEN** 系统成功应用字体配置
#### Scenario: 模板元素引用文档的字体
- **WHEN** 外部模板元素定义 font: "@doc-title" 且 doc-title 只存在于文档 fonts 中
- **THEN** 系统抛出 ERROR错误代码为 `TEMPLATE_FONT_REF_DOC_FORBIDDEN`
- **AND** 错误消息包含"模板元素不能引用文档的字体配置"
#### Scenario: 模板元素引用同名字体(只使用模板库的)
- **WHEN** 外部模板元素定义 font: "@title"
- **AND** title 同时存在于文档 fonts 和模板库 fonts 中
- **THEN** 系统只使用模板库的 title 配置(不查找文档)
### Requirement: 模板元素的 fonts_default 级联规则
外部模板元素未定义 font 时 SHALL 级联使用 fonts_default优先文档 fonts_default文档没有则使用模板库 fonts_default。
#### Scenario: 模板元素未定义 font文档有 fonts_default
- **WHEN** 模板元素未定义 font
- **AND** 文档定义了 metadata.fonts_default: "@body"
- **THEN** 元素使用文档的 fonts_default 配置
#### Scenario: 模板元素未定义 font文档没有 fonts_default
- **WHEN** 模板元素未定义 font
- **AND** 文档未定义 fonts_default
- **AND** 模板库定义了 metadata.fonts_default: "@base"
- **THEN** 元素使用模板库的 fonts_default 配置
#### Scenario: 模板元素未定义 font都没有 fonts_default
- **WHEN** 模板元素未定义 font
- **AND** 文档和模板库都未定义 fonts_default
- **THEN** 元素使用系统默认字体
### Requirement: 表格元素的双字体字段应用相同规则
表格元素的 font 和 header_font 字段 SHALL 应用与普通元素相同的字体引用规则。
#### Scenario: 表格的 font 引用模板库字体
- **WHEN** 模板中的表格定义 font: "@table-body"
- **AND** table-body 存在于模板库 fonts 中
- **THEN** 系统成功应用字体配置
#### Scenario: 表格的 header_font 引用模板库字体
- **WHEN** 模板中的表格定义 header_font: "@table-header"
- **AND** table-header 存在于模板库 fonts 中
- **THEN** 系统成功应用字体配置
#### Scenario: 表格的 font 为 null
- **WHEN** 模板中的表格定义 font: null 或未定义
- **THEN** 系统按 fonts_default 级联规则处理
### Requirement: 模板库文件必须递归验证每个模板的结构