ai_agent/utils/plugins/
frontmatter_parser.rs1use serde_yaml;
9
10pub fn parse_frontmatter<'a>(content: &'a str, path: &str) -> (serde_json::Map<String, serde_json::Value>, &'a str) {
12 let content = content.trim_start();
13
14 if !content.starts_with("---") {
16 return (serde_json::Map::new(), content);
17 }
18
19 let rest = &content[3..]; if let Some(end_pos) = rest.find("\n---") {
22 let frontmatter_yaml = &rest[..end_pos];
23 let markdown_body = &rest[end_pos + 4..]; match serde_yaml::from_str::<serde_yaml::Value>(frontmatter_yaml) {
27 Ok(yaml_value) => {
28 let fm = yaml_to_json_map(&yaml_value);
29 (fm, markdown_body.trim_start())
30 }
31 Err(e) => {
32 log::warn!(
33 "[frontmatter] Failed to parse frontmatter in {}: {}",
34 path,
35 e
36 );
37 (serde_json::Map::new(), content)
39 }
40 }
41 } else {
42 (serde_json::Map::new(), content)
44 }
45}
46
47fn yaml_to_json_map(value: &serde_yaml::Value) -> serde_json::Map<String, serde_json::Value> {
49 match value {
50 serde_yaml::Value::Mapping(map) => {
51 let mut result = serde_json::Map::new();
52 for (k, v) in map {
53 if let Some(key) = k.as_str() {
54 result.insert(key.to_string(), yaml_value_to_json(v));
55 }
56 }
57 result
58 }
59 _ => serde_json::Map::new(),
60 }
61}
62
63fn yaml_value_to_json(value: &serde_yaml::Value) -> serde_json::Value {
65 match value {
66 serde_yaml::Value::Null => serde_json::Value::Null,
67 serde_yaml::Value::Bool(b) => serde_json::Value::Bool(*b),
68 serde_yaml::Value::Number(n) => {
69 if let Some(i) = n.as_i64() {
70 serde_json::Value::Number(serde_json::Number::from(i))
71 } else if let Some(f) = n.as_f64() {
72 serde_json::Number::from_f64(f)
73 .map(serde_json::Value::Number)
74 .unwrap_or(serde_json::Value::Null)
75 } else {
76 serde_json::Value::Null
77 }
78 }
79 serde_yaml::Value::String(s) => serde_json::Value::String(s.clone()),
80 serde_yaml::Value::Sequence(seq) => {
81 serde_json::Value::Array(seq.iter().map(yaml_value_to_json).collect())
82 }
83 serde_yaml::Value::Mapping(map) => {
84 let mut obj = serde_json::Map::new();
85 for (k, v) in map {
86 if let Some(key) = k.as_str() {
87 obj.insert(key.to_string(), yaml_value_to_json(v));
88 }
89 }
90 serde_json::Value::Object(obj)
91 }
92 serde_yaml::Value::Tagged(tagged) => {
93 yaml_value_to_json(&tagged.value)
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn test_parse_frontmatter_basic() {
105 let content = "---\nname: My Agent\ndescription: A test agent\n---\n\n# Agent body";
106 let (fm, body) = parse_frontmatter(content, "test.md");
107 assert_eq!(fm.get("name").and_then(|v| v.as_str()), Some("My Agent"));
108 assert_eq!(fm.get("description").and_then(|v| v.as_str()), Some("A test agent"));
109 assert_eq!(body, "# Agent body");
110 }
111
112 #[test]
113 fn test_parse_frontmatter_no_frontmatter() {
114 let content = "# Just markdown";
115 let (fm, body) = parse_frontmatter(content, "test.md");
116 assert!(fm.is_empty());
117 assert_eq!(body, "# Just markdown");
118 }
119
120 #[test]
121 fn test_parse_frontmatter_no_closing_delimiter() {
122 let content = "---\nname: unclosed";
123 let (fm, body) = parse_frontmatter(content, "test.md");
124 assert!(fm.is_empty());
125 assert_eq!(body, "---\nname: unclosed");
126 }
127
128 #[test]
129 fn test_parse_frontmatter_array_value() {
130 let content = "---\nname: Test\ntools:\n - Read\n - Bash\n---\n\nBody";
131 let (fm, _body) = parse_frontmatter(content, "test.md");
132 let tools = fm.get("tools").and_then(|v| v.as_array());
133 assert!(tools.is_some());
134 let tools = tools.unwrap();
135 assert_eq!(tools.len(), 2);
136 assert_eq!(tools[0].as_str(), Some("Read"));
137 assert_eq!(tools[1].as_str(), Some("Bash"));
138 }
139
140 #[test]
141 fn test_parse_frontmatter_empty() {
142 let content = "";
143 let (fm, body) = parse_frontmatter(content, "test.md");
144 assert!(fm.is_empty());
145 assert_eq!(body, "");
146 }
147
148 #[test]
149 fn test_parse_frontmatter_leading_whitespace() {
150 let content = " \n---\nname: Test\n---\nBody";
151 let (fm, body) = parse_frontmatter(content, "test.md");
152 assert_eq!(fm.get("name").and_then(|v| v.as_str()), Some("Test"));
153 assert_eq!(body, "Body");
154 }
155
156 #[test]
157 fn test_parse_frontmatter_boolean_value() {
158 let content = "---\nname: Test\nbackground: true\n---\n\nBody";
159 let (fm, _body) = parse_frontmatter(content, "test.md");
160 assert_eq!(fm.get("background").and_then(|v| v.as_bool()), Some(true));
161 }
162
163 #[test]
164 fn test_parse_frontmatter_nested_object() {
165 let content = "---\nname: Test\nconfig:\n key: value\n---\n\nBody";
166 let (fm, _body) = parse_frontmatter(content, "test.md");
167 let config = fm.get("config").and_then(|v| v.as_object());
168 assert!(config.is_some());
169 let config = config.unwrap();
170 assert_eq!(config.get("key").and_then(|v| v.as_str()), Some("value"));
171 }
172}