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

@@ -1047,6 +1047,189 @@ uv run yaml2pptx.py preview temp/test.yaml
3. **测试文件隔离**:所有测试文件放在 `temp/` 目录
4. **不污染主机环境**:不修改主机的 Python 配置
## 字体作用域系统
### 概述
字体作用域系统实现了文档和模板库之间的字体隔离和跨域引用控制,确保字体引用的安全性和可维护性。
### 作用域定义
系统定义了两个字体作用域:
1. **文档作用域document**
- 包含文档的 `metadata.fonts` 中定义的字体
- 文档的 `fonts_default` 只能引用文档作用域的字体
- 文档元素可以引用文档作用域和模板库作用域的字体
2. **模板库作用域template**
- 包含模板库的 `metadata.fonts` 中定义的字体
- 模板库的 `fonts_default` 只能引用模板库作用域的字体
- 模板元素只能引用模板库作用域的字体(不能引用文档字体)
### 跨域引用规则
**允许的引用**
- ✅ 文档元素 → 文档字体
- ✅ 文档元素 → 模板库字体(跨域引用)
- ✅ 模板元素 → 模板库字体
- ✅ 文档 fonts_default → 文档字体
- ✅ 模板库 fonts_default → 模板库字体
**禁止的引用**
- ❌ 模板元素 → 文档字体(跨域引用被禁止)
- ❌ 模板库 fonts_default → 文档字体(跨域引用被禁止)
- ❌ 文档 fonts_default → 模板库字体(跨域引用被禁止)
**设计理由**
- 模板库应该是自包含的,不依赖特定文档的字体配置
- 文档可以引用模板库字体,实现样式复用
- 防止模板库与文档之间的紧耦合
### FontResolver 实现
`utils/font_resolver.py` 中的 `FontResolver` 类实现了作用域控制:
```python
class FontResolver:
def __init__(self, fonts, fonts_default, scope="document", template_fonts=None):
"""
Args:
fonts: 当前作用域的字体字典
fonts_default: 当前作用域的默认字体
scope: 作用域标识 ("document" 或 "template")
template_fonts: 模板库字体字典(仅文档作用域需要)
"""
```
**跨域引用检测**
- 使用作用域标签(`doc.@font-name` 或 `template.@font-name`)追踪引用路径
- 检测跨域循环引用(如 `doc.@a → template.@b → doc.@a`
- 在 `parent` 引用时根据作用域限制跨域访问
### fonts_default 级联规则
当元素未指定字体时,按以下顺序查找默认字体:
1. **模板元素**
- 模板库的 `fonts_default`(如果存在)
- 文档的 `fonts_default`(如果存在)
- 系统默认字体
2. **文档元素**
- 文档的 `fonts_default`(如果存在)
- 系统默认字体
**实现位置**
- `core/template.py` - Template.render() 方法
- `core/presentation.py` - Presentation.render_slide() 方法
### 循环引用检测
系统检测两种循环引用:
1. **单域内循环**
```yaml
fonts:
a:
parent: "@b"
b:
parent: "@a"
```
错误信息:`检测到字体引用循环: doc.@a -> doc.@b -> doc.@a`
2. **跨域循环**
```yaml
# 文档
fonts:
doc-font:
parent: "@template-font"
# 模板库
fonts:
template-font:
parent: "@doc-font" # 这会被禁止
```
错误信息:`检测到跨域字体引用循环: doc.@doc-font -> template.@template-font -> doc.@doc-font`
### 错误代码
字体作用域系统相关的错误代码(定义在 `validators/result.py`
#### 模板库 metadata 相关
- `TEMPLATE_LIBRARY_MISSING_METADATA` - 模板库缺少 metadata 字段
- `TEMPLATE_LIBRARY_METADATA_MISSING_SIZE` - 模板库 metadata 缺少 size 字段
- `TEMPLATE_LIBRARY_METADATA_INVALID_SIZE` - 模板库 metadata.size 值无效(必须是 "16:9" 或 "4:3"
#### Size 一致性
- `SIZE_MISMATCH` - 文档和模板库的 size 不一致
#### 字体引用相关
- `TEMPLATE_FONT_REF_DOC_FORBIDDEN` - 模板元素引用文档字体(跨域引用被禁止)
- `TEMPLATE_PARENT_REF_DOC_FORBIDDEN` - 模板库字体的 parent 引用文档字体(跨域引用被禁止)
- `FONT_NOT_FOUND` - 字体配置不存在
- `CIRCULAR_REFERENCE` - 检测到字体引用循环(包括跨域循环)
- `FONT_DEFAULT_INVALID` - fonts_default 引用无效(字体不存在或跨域引用)
### 使用示例
**正确的跨域引用**
```yaml
# templates.yaml模板库
metadata:
size: "16:9"
fonts:
template-title:
family: "cjk-sans"
size: 44
bold: true
fonts_default: "@template-title"
templates:
title-slide:
elements:
- type: text
content: "{title}"
# 未指定 font使用模板库的 fonts_default
# presentation.yaml文档
metadata:
size: "16:9"
fonts:
doc-body:
family: "sans"
size: 18
fonts_default: "@doc-body"
slides:
- template: title-slide
vars:
title: "标题"
- elements:
- type: text
content: "正文"
font: "@template-title" # ✅ 文档元素可以引用模板库字体
```
**错误的跨域引用**
```yaml
# templates.yaml模板库
metadata:
size: "16:9"
fonts_default: "@doc-body" # ❌ 模板库 fonts_default 不能引用文档字体
templates:
title-slide:
elements:
- type: text
content: "{title}"
font: "@doc-body" # ❌ 模板元素不能引用文档字体
```
## 维护指南
### 代码审查要点