use serde_yaml::{Mapping, Value};
#[allow(dead_code)] pub const KNOWN_PLATFORM_IDS: &[&str] = &[
"antigravity",
"augment",
"claude",
"claude-plugin",
"codex",
"copilot",
"cursor",
"factory",
"gemini",
"junie",
"kilo",
"kiro",
"opencode",
"qwen",
"roo",
"warp",
"windsurf",
];
pub fn parse_frontmatter_and_body(content: &str) -> Option<(Value, String)> {
let lines: Vec<&str> = content.lines().collect();
if lines.len() < 3 || lines[0].trim() != "---" {
return None;
}
let end_idx = lines[1..].iter().position(|l| l.trim() == "---")?;
let end_idx = end_idx + 1;
let frontmatter_str = lines[1..end_idx].join("\n");
let body = lines[end_idx + 1..].join("\n");
let value: Value = serde_yaml::from_str(&frontmatter_str).ok()?;
if value.as_mapping().is_none() && !value.is_null() {
return None;
}
Some((value, body))
}
pub fn merge_frontmatter_for_platform(
frontmatter: &Value,
platform_id: &str,
known_platform_ids: &[String],
) -> Value {
let mapping = match frontmatter.as_mapping() {
Some(m) => m,
None => return frontmatter.clone(),
};
let known: std::collections::HashSet<_> =
known_platform_ids.iter().map(String::as_str).collect();
let mut out = Mapping::new();
let mut platform_block = None;
for (k, v) in mapping {
let key_str = k.as_str().unwrap_or("");
if key_str == platform_id {
platform_block = Some(v.clone());
} else if !known.contains(key_str) {
out.insert(k.clone(), v.clone());
}
}
if let Some(block) = platform_block {
if let Some(block_map) = block.as_mapping() {
for (k, v) in block_map {
out.insert(k.clone(), v.clone());
}
}
}
Value::Mapping(out)
}
pub fn serialize_to_yaml(value: &Value) -> String {
serde_yaml::to_string(value).unwrap_or_else(|_| String::new())
}
pub fn get_str(value: &Value, key: &str) -> Option<String> {
let mapping = value.as_mapping()?;
let v = mapping.get(Value::String(key.to_string()))?;
match v {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_no_frontmatter() {
let content = "just body\nno delimiters";
assert!(parse_frontmatter_and_body(content).is_none());
}
#[test]
fn test_parse_frontmatter_and_body() {
let content = "---\ndescription: hello\n---\n\nbody here";
let (fm, body) = parse_frontmatter_and_body(content).unwrap();
assert_eq!(get_str(&fm, "description").as_deref(), Some("hello"));
assert_eq!(body.trim(), "body here");
}
#[test]
fn parse_with_platform_block() {
let content = r#"---
description: common
opencode:
mode: subagent
model: claude-sonnet
---
body"#;
let (fm, _) = parse_frontmatter_and_body(content).unwrap();
let known: Vec<String> = KNOWN_PLATFORM_IDS.iter().map(|s| s.to_string()).collect();
let merged = merge_frontmatter_for_platform(&fm, "opencode", &known);
assert_eq!(get_str(&merged, "description").as_deref(), Some("common"));
assert_eq!(get_str(&merged, "mode").as_deref(), Some("subagent"));
}
#[test]
fn merge_platform_overrides_common() {
let content = "---\ndescription: common\ncursor:\n description: cursor-desc\n---\n";
let (fm, _) = parse_frontmatter_and_body(content).unwrap();
let known: Vec<String> = KNOWN_PLATFORM_IDS.iter().map(|s| s.to_string()).collect();
let merged = merge_frontmatter_for_platform(&fm, "cursor", &known);
assert_eq!(
get_str(&merged, "description").as_deref(),
Some("cursor-desc")
);
}
}