use gray_matter::Matter; use gray_matter::engine::YAML; use serde::de::DeserializeOwned; /// Parse a markdown document with YAML frontmatter into `(frontmatter, body)`. /// /// Tolerates CRLF, CR, and LF line endings. A `---` appearing inside the body /// is not treated as a delimiter (only the leading frontmatter block is split). /// /// - Returns `Err("missing YAML frontmatter block")` when no leading `---` /// delimiter is present. /// - Returns `Err("invalid YAML frontmatter: {e}")` when the block is present /// but the YAML fails to parse or deserialize into `T`. pub fn parse(content: &str) -> Result<(T, String), String> { let normalized = content.replace("\r\n", "\n").replace('\r', "\n"); if !normalized.starts_with("---\n") { return Err("missing YAML frontmatter block".to_string()); } let matter = Matter::::new(); let result = matter.parse(&normalized); let pod = result .data .ok_or_else(|| "invalid YAML frontmatter".to_string())?; let data: T = pod .deserialize() .map_err(|e| format!("invalid YAML frontmatter: {}", e))?; Ok((data, result.content)) } #[cfg(test)] mod tests { use super::*; use serde::Deserialize; #[derive(Debug, Deserialize, PartialEq)] struct FrontMatter { description: String, #[serde(default)] name: Option, } #[test] fn parses_lf_endings() { let input = "---\ndescription: demo\n---\nbody text"; let (fm, body) = parse::(input).unwrap(); assert_eq!( fm, FrontMatter { description: "demo".to_string(), name: None } ); assert_eq!(body, "body text"); } #[test] fn parses_crlf_endings() { let input = "---\r\ndescription: demo\r\n---\r\nbody text"; let (fm, body) = parse::(input).unwrap(); assert_eq!(fm.description, "demo"); assert_eq!(body, "body text"); } #[test] fn parses_cr_endings() { let input = "---\rdescription: demo\r---\rbody text"; let (fm, body) = parse::(input).unwrap(); assert_eq!(fm.description, "demo"); assert_eq!(body, "body text"); } #[test] fn missing_block_is_rejected() { let err = parse::("no front matter here").unwrap_err(); assert_eq!(err, "missing YAML frontmatter block"); } #[test] fn invalid_yaml_is_rejected() { let err = parse::("---\n: : bad\n---\nbody").unwrap_err(); assert!(err.starts_with("invalid YAML frontmatter")); } #[test] fn body_with_inner_delimiter_is_preserved() { let input = "---\ndescription: demo\n---\nexcerpt\n---\nmore content"; let (_fm, body) = parse::(input).unwrap(); assert_eq!(body, "excerpt\n---\nmore content"); } }