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

@@ -21,16 +21,26 @@ PRESET_FONT_MAPPING = {
class FontResolver:
"""字体解析器,处理字体引用、继承和预设类别映射"""
def __init__(self, fonts: Optional[Dict] = None, fonts_default: Optional[str] = None):
def __init__(
self,
fonts: Optional[Dict] = None,
fonts_default: Optional[str] = None,
scope: str = "document",
template_fonts: Optional[Dict] = None
):
"""
初始化字体解析器
Args:
fonts: 字体配置字典,键为字体名称,值为字体配置
fonts_default: 默认字体引用(格式:@xxx
scope: 作用域("document""template"
template_fonts: 模板库字体配置字典(用于跨域引用)
"""
self.fonts = fonts or {}
self.fonts_default = fonts_default
self.scope = scope
self.template_fonts = template_fonts or {}
self._max_depth = 10 # 最大引用深度,防止循环引用
def resolve_font(self, font_config: Union[FontConfig, str, dict, None]) -> FontConfig:
@@ -68,13 +78,14 @@ class FontResolver:
raise ValueError(f"不支持的字体配置类型: {type(font_config)}")
def _resolve_reference(self, reference: str, visited: Set[str]) -> FontConfig:
def _resolve_reference(self, reference: str, visited: Set[str], allow_cross_domain: bool = True) -> FontConfig:
"""
解析字体引用
Args:
reference: 引用字符串(格式:@xxx
visited: 已访问的引用集合,用于检测循环引用
allow_cross_domain: 是否允许跨域引用(元素引用时为 Trueparent 引用时根据作用域决定)
Returns:
解析后的 FontConfig 对象
@@ -87,24 +98,49 @@ class FontResolver:
font_name = reference[1:] # 移除 @ 前缀
# 检测循环引用
if reference in visited:
path = " -> ".join(visited) + f" -> {reference}"
raise ValueError(f"检测到字体引用循环: {path}")
# 为引用添加作用域标签,用于跨域循环检测
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}"
# 检查是否为跨域循环
has_doc = any("doc." in v for v in visited)
has_template = any("template." in v for v in visited)
if has_doc and has_template:
raise ValueError(f"检测到跨域字体引用循环: {path}")
else:
raise ValueError(f"检测到字体引用循环: {path}")
# 检查引用深度
if len(visited) >= self._max_depth:
raise ValueError(f"字体引用深度超过限制 ({self._max_depth} 层)")
# 检查引用是否存在
if font_name not in self.fonts:
raise ValueError(f"引用的字体配置不存在: {reference}")
# 添加到已访问集合
visited.add(reference)
visited.add(tagged_ref)
# 获取引用的字体配置
font_dict = self.fonts[font_name]
# 根据作用域查找字体配置
font_dict = None
if self.scope == "template":
# 模板库作用域:只能引用 template_fonts
if font_name in self.template_fonts:
font_dict = self.template_fonts[font_name]
elif not allow_cross_domain:
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]
# 检查引用是否存在
if font_dict is None:
raise ValueError(f"引用的字体配置不存在: {reference}")
# 递归解析
return self._resolve_font_dict(font_dict, visited.copy())
@@ -123,7 +159,9 @@ class FontResolver:
# 处理 parent 继承
parent_config = FontConfig()
if "parent" in font_dict and font_dict["parent"]:
parent_config = self._resolve_reference(font_dict["parent"], visited)
# parent 引用的跨域限制:模板库 fonts 的 parent 不能引用文档 fonts
allow_cross_domain = self.scope == "document"
parent_config = self._resolve_reference(font_dict["parent"], visited, allow_cross_domain)
# 创建当前配置(从 parent 继承)
config = FontConfig(
@@ -168,10 +206,12 @@ class FontResolver:
return FontConfig()
# 避免在获取默认配置时再次访问 fonts_default
if self.fonts_default in visited:
scope_tag = "doc" if self.scope == "document" else "template"
tagged_default = f"{scope_tag}.{self.fonts_default}"
if tagged_default in visited:
return FontConfig()
return self._resolve_reference(self.fonts_default, visited.copy())
return self._resolve_reference(self.fonts_default, visited.copy(), allow_cross_domain=True)
def _merge_with_default(self, config: FontConfig, default: FontConfig) -> FontConfig:
"""