use anyhow::{Context, Result};
pub fn split(raw: &str) -> Option<(&str, &str)> {
let rest = raw
.strip_prefix("---\n")
.or_else(|| raw.strip_prefix("---\r\n"))?;
let mut idx = 0;
for line in rest.split_inclusive('\n') {
let trimmed = line.trim_end_matches(['\n', '\r']);
if trimmed == "---" {
let fm = &rest[..idx];
let body_start = idx + line.len();
return Some((fm, &rest[body_start..]));
}
idx += line.len();
}
None
}
pub fn parse_mapping(fm: &str) -> serde_yaml::Mapping {
serde_yaml::from_str(fm).unwrap_or_default()
}
pub fn serialize(meta: &serde_yaml::Mapping, body: &str) -> Result<String> {
let fm = serde_yaml::to_string(meta).context("failed to serialize frontmatter")?;
let body_trimmed = body.trim_start_matches(['\n', '\r']);
Ok(format!("---\n{fm}---\n\n{body_trimmed}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_lf() {
let raw = "---\nfoo: 1\nbar: two\n---\n\n# Body\n";
let (fm, body) = split(raw).unwrap();
assert_eq!(fm, "foo: 1\nbar: two\n");
assert_eq!(body, "\n# Body\n");
}
#[test]
fn split_crlf() {
let raw = "---\r\nfoo: 1\r\n---\r\nbody\r\n";
let (fm, body) = split(raw).unwrap();
assert_eq!(fm, "foo: 1\r\n");
assert_eq!(body, "body\r\n");
}
#[test]
fn split_empty_frontmatter() {
let raw = "---\n---\nbody\n";
let (fm, body) = split(raw).unwrap();
assert_eq!(fm, "");
assert_eq!(body, "body\n");
}
#[test]
fn split_no_frontmatter() {
assert!(split("# Just markdown\n").is_none());
assert!(split("").is_none());
assert!(split("---\nno closing delimiter\n").is_none());
}
#[test]
fn parse_mapping_accepts_empty() {
assert!(parse_mapping("").is_empty());
}
#[test]
fn parse_mapping_preserves_keys() {
let m = parse_mapping("foo: 1\nbar: two\n");
assert_eq!(m.get("foo").and_then(|v| v.as_i64()), Some(1));
assert_eq!(m.get("bar").and_then(|v| v.as_str()), Some("two"));
}
#[test]
fn serialize_roundtrip() {
let original = "---\nfoo: 1\nbar: two\n---\n\nbody text\n";
let (fm, body) = split(original).unwrap();
let mapping = parse_mapping(fm);
let out = serialize(&mapping, body).unwrap();
let (fm2, body2) = split(&out).unwrap();
let mapping2 = parse_mapping(fm2);
assert_eq!(mapping, mapping2);
assert_eq!(body2.trim(), "body text");
}
}