""" 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)