claude_agent/common/
frontmatter.rs1use serde::de::DeserializeOwned;
7
8pub struct ParsedDocument<F> {
10 pub frontmatter: F,
11 pub body: String,
12}
13
14pub fn strip_frontmatter(content: &str) -> &str {
30 if let Some(after_first) = content.strip_prefix("---")
31 && let Some(end_pos) = after_first.find("---")
32 {
33 return after_first[end_pos + 3..].trim_start();
34 }
35 content
36}
37
38pub fn parse_frontmatter<F: DeserializeOwned>(content: &str) -> crate::Result<ParsedDocument<F>> {
42 if !content.starts_with("---") {
43 return Err(crate::Error::Config(
44 "Document must have YAML frontmatter (starting with ---)".to_string(),
45 ));
46 }
47
48 let after_first = &content[3..];
49 let end_pos = after_first.find("---").ok_or_else(|| {
50 crate::Error::Config("Frontmatter not properly terminated with ---".to_string())
51 })?;
52
53 let frontmatter_str = after_first[..end_pos].trim();
54 let body = after_first[end_pos + 3..].trim().to_string();
55
56 let frontmatter: F = serde_yaml_bw::from_str(frontmatter_str)
57 .map_err(|e| crate::Error::Config(format!("Failed to parse frontmatter: {}", e)))?;
58
59 Ok(ParsedDocument { frontmatter, body })
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use serde::Deserialize;
66
67 #[derive(Debug, Deserialize, PartialEq)]
68 struct TestFrontmatter {
69 name: String,
70 #[serde(default)]
71 description: String,
72 }
73
74 #[test]
75 fn test_parse_valid() {
76 let content = r#"---
77name: test
78description: A test
79---
80
81Body content here."#;
82
83 let doc = parse_frontmatter::<TestFrontmatter>(content).unwrap();
84 assert_eq!(doc.frontmatter.name, "test");
85 assert_eq!(doc.frontmatter.description, "A test");
86 assert_eq!(doc.body, "Body content here.");
87 }
88
89 #[test]
90 fn test_parse_no_frontmatter() {
91 let content = "Just content without frontmatter";
92 let result = parse_frontmatter::<TestFrontmatter>(content);
93 assert!(result.is_err());
94 }
95
96 #[test]
97 fn test_parse_unterminated() {
98 let content = "---\nname: test\nNo closing delimiter";
99 let result = parse_frontmatter::<TestFrontmatter>(content);
100 assert!(result.is_err());
101 }
102
103 #[test]
104 fn test_parse_empty_body() {
105 let content = r#"---
106name: minimal
107---
108"#;
109
110 let doc = parse_frontmatter::<TestFrontmatter>(content).unwrap();
111 assert_eq!(doc.frontmatter.name, "minimal");
112 assert!(doc.body.is_empty());
113 }
114}