"""YAML frontmatter 解析工具 — 供 skills_linter / agents_linter 共用."""
import yaml
class YAMLFixSuggestion(Exception):
"""YAML 解析失败但可自动修复时抛出,携带修复建议."""
def __init__(self, original_error: str, suggestion: str):
self.original_error = original_error
self.suggestion = suggestion
super().__init__(original_error)
def parse_frontmatter(content: str) -> dict:
"""从 Markdown 文件中提取 YAML frontmatter.
Args:
content: Markdown 文件的完整内容.
Returns:
解析后的 YAML frontmatter 字典.
Raises:
ValueError: 当文件不以 '---' 开始或缺少闭合分隔符时抛出.
YAMLFixSuggestion: YAML 解析失败但可通过加引号修复时抛出(携带建议).
yaml.YAMLError: YAML 解析失败且无法自动修复时抛出.
"""
lines = content.split('\n')
if not lines or lines[0].strip() != '---':
raise ValueError("Markdown file does not start with YAML frontmatter delimiter '---'")
frontmatter_lines = []
found_closing = False
for i in range(1, len(lines)):
if lines[i].strip() == '---':
found_closing = True
break
frontmatter_lines.append(lines[i])
if not found_closing:
raise ValueError("Markdown file does not have closing YAML frontmatter delimiter '---'")
frontmatter_text = '\n'.join(frontmatter_lines)
try:
data = yaml.safe_load(frontmatter_text)
if not isinstance(data, dict):
raise ValueError("YAML frontmatter must be a mapping (dict), but got "
f"{type(data).__name__ if data is not None else 'None'}")
return data
except yaml.YAMLError as original_exc:
fixed_lines = []
fix_applied = []
for line in frontmatter_lines:
stripped = line.strip()
if ':' in stripped and not stripped.startswith('#'):
key_part, _, val_part = stripped.partition(':')
val = val_part.strip()
if val and not val.startswith(('"', "'", '>', '|', '[', '{', '&', '*')):
key = key_part.strip()
safe_scalar = yaml.dump(
{key: val},
default_flow_style=False,
allow_unicode=True,
).strip()
if safe_scalar != stripped:
line = safe_scalar
fix_applied.append(safe_scalar)
fixed_lines.append(line)
if not fix_applied:
raise
fixed_frontmatter_text = '\n'.join(fixed_lines)
fixed_data = yaml.safe_load(fixed_frontmatter_text)
if isinstance(fixed_data, dict):
suggestion_lines = '\n'.join(f" {s}" for s in fix_applied)
suggestion = f"将包含保留字符的值用引号包裹:\n{suggestion_lines}"
raise YAMLFixSuggestion(str(original_exc), suggestion) from original_exc
raise