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:
@@ -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: 是否允许跨域引用(元素引用时为 True,parent 引用时根据作用域决定)
|
||||
|
||||
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:
|
||||
# 文档作用域:优先 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]
|
||||
|
||||
# 检查引用是否存在
|
||||
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:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user