Skip to main content

cc_audit/parser/
frontmatter.rs

1//! YAML frontmatter parser for markdown files.
2
3/// Parses YAML frontmatter from markdown files.
4pub struct FrontmatterParser;
5
6impl FrontmatterParser {
7    /// Extract frontmatter content from a markdown file.
8    ///
9    /// Frontmatter is delimited by `---` at the start and end.
10    /// Returns None if no valid frontmatter is found.
11    ///
12    /// # Example
13    /// ```
14    /// use cc_audit::parser::FrontmatterParser;
15    /// let content = "---\nname: test\n---\n# Content";
16    /// let frontmatter = FrontmatterParser::extract(content);
17    /// assert_eq!(frontmatter, Some("\nname: test\n"));
18    /// ```
19    pub fn extract(content: &str) -> Option<&str> {
20        content.strip_prefix("---").and_then(|after_start| {
21            after_start
22                .find("---")
23                .map(|end_idx| &after_start[..end_idx])
24        })
25    }
26
27    /// Parse frontmatter as a YAML value.
28    ///
29    /// Returns None if no frontmatter exists or parsing fails.
30    pub fn parse_yaml(content: &str) -> Option<serde_yaml::Value> {
31        Self::extract(content).and_then(|fm| serde_yaml::from_str(fm.trim()).ok())
32    }
33
34    /// Parse frontmatter as a JSON value.
35    ///
36    /// Returns None if no frontmatter exists or parsing fails.
37    pub fn parse_json(content: &str) -> Option<serde_json::Value> {
38        Self::extract(content)
39            .and_then(|fm| serde_yaml::from_str::<serde_json::Value>(fm.trim()).ok())
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn test_valid_frontmatter() {
49        let content = "---\nname: test\ndescription: A test\n---\n# Content";
50        let result = FrontmatterParser::extract(content);
51        assert_eq!(result, Some("\nname: test\ndescription: A test\n"));
52    }
53
54    #[test]
55    fn test_frontmatter_with_allowed_tools() {
56        let content = "---\nname: skill\nallowed-tools: Read, Write\n---\n# Skill";
57        let result = FrontmatterParser::extract(content);
58        assert!(result.is_some());
59        assert!(result.unwrap().contains("allowed-tools"));
60    }
61
62    #[test]
63    fn test_no_frontmatter() {
64        let content = "# Just Markdown\nNo frontmatter here.";
65        assert!(FrontmatterParser::extract(content).is_none());
66    }
67
68    #[test]
69    fn test_incomplete_frontmatter() {
70        let content = "---\nname: test\nNo closing dashes";
71        assert!(FrontmatterParser::extract(content).is_none());
72    }
73
74    #[test]
75    fn test_empty_frontmatter() {
76        let content = "------\n# Content";
77        let result = FrontmatterParser::extract(content);
78        assert_eq!(result, Some(""));
79    }
80
81    #[test]
82    fn test_frontmatter_with_nested_dashes() {
83        let content = "---\nname: test\ndata: \"some---thing\"\n---\n# Content";
84        let result = FrontmatterParser::extract(content);
85        // Should extract up to the first closing ---
86        assert!(result.is_some());
87    }
88
89    #[test]
90    fn test_content_not_starting_with_dashes() {
91        let content = "# Title\n---\nname: test\n---";
92        assert!(FrontmatterParser::extract(content).is_none());
93    }
94
95    #[test]
96    fn test_parse_yaml() {
97        let content = "---\nname: test\nversion: 1.0\n---\n# Content";
98        let yaml = FrontmatterParser::parse_yaml(content);
99        assert!(yaml.is_some());
100        let yaml = yaml.unwrap();
101        assert_eq!(yaml["name"].as_str(), Some("test"));
102    }
103
104    #[test]
105    fn test_parse_json() {
106        let content = "---\nname: test\nversion: 1.0\n---\n# Content";
107        let json = FrontmatterParser::parse_json(content);
108        assert!(json.is_some());
109        let json = json.unwrap();
110        assert_eq!(json["name"].as_str(), Some("test"));
111    }
112}