Skip to main content

mofa_plugins/skill/
parser.rs

1//! SKILL.md 文件解析器
2
3use crate::skill::metadata::SkillMetadata;
4use anyhow::Result;
5use regex::Regex;
6use std::fs;
7use std::path::Path;
8
9/// SKILL.md 解析器
10pub struct SkillParser;
11
12impl SkillParser {
13    /// 解析 YAML frontmatter
14    pub fn parse_frontmatter(content: &str) -> Result<(SkillMetadata, String)> {
15        // Use [\s\S]*? instead of .*? to match newlines in YAML content
16        let frontmatter_regex = Regex::new(r"^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$").unwrap();
17
18        if let Some(caps) = frontmatter_regex.captures(content) {
19            let yaml = &caps[1];
20            let markdown = &caps[2];
21
22            let metadata: SkillMetadata = serde_yaml::from_str(yaml)
23                .map_err(|e| anyhow::anyhow!("Failed to parse YAML frontmatter: {}", e))?;
24
25            Ok((metadata, markdown.to_string()))
26        } else {
27            anyhow::bail!("SKILL.md must start with YAML frontmatter")
28        }
29    }
30
31    /// 从 SKILL.md 文件解析元数据
32    pub fn parse_from_file(skill_md_path: impl AsRef<Path>) -> Result<(SkillMetadata, String)> {
33        let content = fs::read_to_string(skill_md_path.as_ref())?;
34        Self::parse_frontmatter(&content)
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn test_parse_frontmatter() {
44        let content = r#"---
45name: test_skill
46description: A test skill
47category: test
48tags: [test, example]
49version: "1.0.0"
50---
51
52# Test Skill
53
54This is a test skill."#;
55
56        let (metadata, markdown) = SkillParser::parse_frontmatter(content).unwrap();
57
58        assert_eq!(metadata.name, "test_skill");
59        assert_eq!(metadata.description, "A test skill");
60        assert_eq!(metadata.category, Some("test".to_string()));
61        assert_eq!(metadata.tags, vec!["test", "example"]);
62        assert_eq!(metadata.version, Some("1.0.0".to_string()));
63        assert!(markdown.contains("# Test Skill"));
64    }
65
66    #[test]
67    fn test_parse_frontmatter_no_frontmatter() {
68        let content = "# No Frontmatter\nJust content";
69        let result = SkillParser::parse_frontmatter(content);
70        assert!(result.is_err());
71    }
72}