use std::collections::BTreeMap;
use super::permission::Permission;
#[derive(Debug, Clone, Default)]
pub struct FrontMatter {
pub name: Option<String>,
pub description: Option<String>,
pub allowed_tools: Vec<Permission>,
pub extra: BTreeMap<String, serde_yml::Value>,
}
pub trait FrontMatterMapper {
fn parse_frontmatter(&self, yaml: &str) -> anyhow::Result<FrontMatter>;
fn parse_permission(&self, token: &str) -> Permission;
}
pub fn split_frontmatter(content: &str) -> (Option<&str>, &str) {
let trimmed = content.trim_start();
if !trimmed.starts_with("---") {
return (None, content);
}
let after_open = &trimmed[3..];
let after_open = match after_open.find('\n') {
Some(i) => &after_open[i + 1..],
None => return (None, content),
};
match after_open.find("\n---") {
Some(i) => {
let yaml = &after_open[..i];
let rest = &after_open[i + 4..];
let body = match rest.find('\n') {
Some(j) => &rest[j + 1..],
None => "",
};
(Some(yaml), body)
}
None => (None, content),
}
}
pub fn join_frontmatter(yaml: &str, body: &str) -> String {
let mut out = String::with_capacity(yaml.len() + body.len() + 10);
out.push_str("---\n");
out.push_str(yaml);
if !yaml.ends_with('\n') {
out.push('\n');
}
out.push_str("---\n");
out.push_str(body);
out
}
pub fn extract_string(map: &BTreeMap<String, serde_yml::Value>, key: &str) -> Option<String> {
map.get(key).and_then(|v| v.as_str()).map(|s| s.to_string())
}
pub fn parse_standard_frontmatter(
yaml: &str,
mapper: &dyn FrontMatterMapper,
) -> anyhow::Result<FrontMatter> {
let raw: BTreeMap<String, serde_yml::Value> = serde_yml::from_str(yaml)?;
let mut fm = FrontMatter::default();
fm.name = extract_string(&raw, "name");
fm.description = extract_string(&raw, "description");
if let Some(tools_str) = extract_string(&raw, "allowed-tools") {
fm.allowed_tools = tokenize_tools(&tools_str)
.into_iter()
.map(|t| mapper.parse_permission(&t))
.collect();
}
fm.extra = raw
.into_iter()
.filter(|(k, _)| !matches!(k.as_str(), "name" | "description" | "allowed-tools"))
.collect();
Ok(fm)
}
pub fn tokenize_tools(s: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
let mut depth = 0u32;
for ch in s.chars() {
match ch {
'(' => {
depth += 1;
current.push(ch);
}
')' => {
depth = depth.saturating_sub(1);
current.push(ch);
}
' ' | '\t' if depth == 0 => {
let t = current.trim().to_string();
if !t.is_empty() {
tokens.push(t);
}
current.clear();
}
_ => current.push(ch),
}
}
let t = current.trim().to_string();
if !t.is_empty() {
tokens.push(t);
}
tokens
}