1
0
Files
PPTX/loaders/yaml_loader.py
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

257 lines
8.7 KiB
Python
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.
"""
YAML 加载和验证模块
负责加载 YAML 文件并验证其结构。
"""
from pathlib import Path
import yaml
# ============= YAML 解析和验证 =============
class YAMLError(Exception):
"""YAML 相关错误"""
pass
def load_yaml_file(file_path):
"""
加载 YAML 文件UTF-8 编码,错误处理)
Args:
file_path: 文件路径(字符串或 Path 对象)
Returns:
解析后的 Python 字典
Raises:
YAMLError: 文件不存在、权限不足、YAML 语法错误等
"""
file_path = Path(file_path)
# 检查文件是否存在
if not file_path.exists():
raise YAMLError(f"文件不存在: {file_path}")
# 检查是否有读取权限
if not file_path.is_file():
raise YAMLError(f"不是有效的文件: {file_path}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
return data
except PermissionError:
raise YAMLError(f"权限不足,无法读取文件: {file_path}")
except yaml.YAMLError as e:
# 提取行号信息
if hasattr(e, 'problem_mark'):
mark = e.problem_mark
raise YAMLError(
f"YAML 语法错误: {file_path}, 第 {mark.line + 1} 行: {e.problem}"
)
else:
raise YAMLError(f"YAML 解析错误: {file_path}: {str(e)}")
except Exception as e:
raise YAMLError(f"读取文件失败: {file_path}: {str(e)}")
def validate_presentation_yaml(data, file_path=""):
"""
验证演示文稿 YAML 结构必需字段slides
Args:
data: 解析后的 YAML 数据
file_path: 文件路径(用于错误消息)
Raises:
YAMLError: 结构验证失败
"""
if not isinstance(data, dict):
raise YAMLError(f"{file_path}: 演示文稿必须是一个字典对象")
# 验证 slides 字段
if 'slides' not in data:
raise YAMLError(f"{file_path}: 缺少必需字段 'slides'")
if not isinstance(data['slides'], list):
raise YAMLError(f"{file_path}: 'slides' 必须是一个列表")
# 验证每个幻灯片的 enabled 字段
for i, slide in enumerate(data['slides']):
if isinstance(slide, dict) and 'enabled' in slide:
if not isinstance(slide['enabled'], bool):
raise YAMLError(
f"{file_path}: slides[{i}].enabled 必须是布尔值true 或 false"
f"不支持字符串或条件表达式"
)
# 验证 metadata 字段(如果存在)
if 'metadata' in data:
validate_metadata(data['metadata'], file_path, context="文档")
# 验证 templates 字段(内联模板)
validate_templates_yaml(data, file_path)
def validate_metadata(metadata, file_path, context="文档"):
"""
验证 metadata 结构(统一用于文档和模板库)
Args:
metadata: metadata 字典
file_path: 文件路径(用于错误消息)
context: 上下文描述("文档""模板库"
Raises:
YAMLError: 结构验证失败
"""
if not isinstance(metadata, dict):
raise YAMLError(f"{file_path}: metadata 必须是字典对象")
# size 必填
if "size" not in metadata:
raise YAMLError(f"{file_path}: {context} 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
# fonts 和 fonts_default 的详细验证在字体主题系统中处理
def validate_template_yaml(data, file_path=""):
"""
验证模板 YAML 结构vars, elements
Args:
data: 解析后的 YAML 数据
file_path: 文件路径(用于错误消息)
Raises:
YAMLError: 结构验证失败
"""
if not isinstance(data, dict):
raise YAMLError(f"{file_path}: 模板必须是一个字典对象")
# 验证 vars 字段
if 'vars' in data:
if not isinstance(data['vars'], list):
raise YAMLError(f"{file_path}: 'vars' 必须是一个列表")
# 验证每个变量定义
for i, var_def in enumerate(data['vars']):
if not isinstance(var_def, dict):
raise YAMLError(f"{file_path}: vars[{i}] 必须是一个字典对象")
if 'name' not in var_def:
raise YAMLError(f"{file_path}: vars[{i}] 缺少必需字段 'name'")
# 验证 elements 字段
if 'elements' not in data:
raise YAMLError(f"{file_path}: 缺少必需字段 'elements'")
if not isinstance(data['elements'], list):
raise YAMLError(f"{file_path}: 'elements' 必须是一个列表")
def validate_templates_yaml(data, file_path=""):
"""
验证 templates 字段结构(内联模板)
Args:
data: 解析后的 YAML 数据
file_path: 文件路径(用于错误消息)
Raises:
YAMLError: 结构验证失败
"""
# 验证 templates 字段是否为字典
if 'templates' in data:
if not isinstance(data['templates'], dict):
raise YAMLError(f"{file_path}: 'templates' 必须是一个字典")
# 验证每个内联模板的结构
for template_name, template_data in data['templates'].items():
# 构建模板位置路径
template_location = f"{file_path}.templates.{template_name}"
# 验证模板是字典
if not isinstance(template_data, dict):
raise YAMLError(f"{template_location}: 模板定义必须是字典")
# 验证必需的 elements 字段
if 'elements' not in template_data:
raise YAMLError(f"{template_location}: 缺少必需字段 'elements'")
if not isinstance(template_data['elements'], list):
raise YAMLError(f"{template_location}: 'elements' 必须是一个列表")
# 验证可选的 vars 字段
if 'vars' in template_data:
if not isinstance(template_data['vars'], list):
raise YAMLError(f"{template_location}: 'vars' 必须是一个列表")
# 验证每个变量定义
for i, var_def in enumerate(template_data['vars']):
if not isinstance(var_def, dict):
raise YAMLError(f"{template_location}.vars[{i}]: 变量定义必须是字典")
# 验证必需的 name 字段
if 'name' not in var_def:
raise YAMLError(f"{template_location}.vars[{i}]: 缺少必需字段 'name'")
def validate_template_library_yaml(data, file_path=""):
"""
验证模板库文件结构(外部模板库)
Args:
data: 解析后的 YAML 数据
file_path: 文件路径(用于错误消息)
Raises:
YAMLError: 结构验证失败
"""
if not isinstance(data, dict):
raise YAMLError(f"{file_path}: 模板库文件必须是一个字典对象")
# 验证必需的 metadata 字段
if 'metadata' not in data:
raise YAMLError(f"{file_path}: 模板库必须包含 metadata 字段")
validate_metadata(data['metadata'], file_path, context="模板库")
# 验证模板库 fonts_default 只能引用模板库内部字体
metadata = data['metadata']
if 'fonts_default' in metadata and metadata['fonts_default']:
fonts_default = metadata['fonts_default']
# fonts_default 必须是引用格式
if not isinstance(fonts_default, str) or not fonts_default.startswith("@"):
raise YAMLError(
f"{file_path}: 模板库 fonts_default 必须是引用格式(@xxx当前值: {fonts_default}"
)
# fonts_default 引用的配置必须存在于模板库 fonts 中
font_name = fonts_default[1:]
template_fonts = metadata.get('fonts', {})
if font_name not in template_fonts:
raise YAMLError(
f"{file_path}: 模板库 fonts_default 只能引用模板库内部的字体配置,"
f"'{fonts_default}' 不存在于模板库 metadata.fonts 中"
)
# 验证必需的 templates 字段
if 'templates' not in data:
raise YAMLError(f"{file_path}: 缺少必需字段 'templates'")
if not isinstance(data['templates'], dict):
raise YAMLError(f"{file_path}: 'templates' 必须是字典")
# 递归验证每个模板的结构
for template_name, template_data in data['templates'].items():
template_location = f"{file_path}.templates.{template_name}"
validate_template_yaml(template_data, template_location)