use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct FrontMatter {
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(flatten)]
#[allow(dead_code)]
pub extra: std::collections::HashMap<String, serde_yaml::Value>,
}
#[derive(Debug)]
pub struct ParsedContent {
pub front_matter: Option<FrontMatter>,
pub content: String,
}
pub fn parse_front_matter(content: &str) -> ParsedContent {
let trimmed = content.trim_start_matches('\u{FEFF}').trim_start();
if !trimmed.starts_with("---") {
return ParsedContent {
front_matter: None,
content: content.to_string(),
};
}
let after_opening = match trimmed.strip_prefix("---") {
Some(rest) => rest,
None => {
return ParsedContent {
front_matter: None,
content: content.to_string(),
}
}
};
let after_opening = after_opening.trim_start_matches([' ', '\t']);
let after_opening = if let Some(rest) = after_opening.strip_prefix("\r\n") {
rest
} else if let Some(rest) = after_opening.strip_prefix('\n') {
rest
} else if after_opening.is_empty() {
after_opening
} else {
return ParsedContent {
front_matter: None,
content: content.to_string(),
};
};
let (yaml_content, remaining) = if let Some(rest) = after_opening.strip_prefix("---\r\n") {
("", rest)
} else if let Some(rest) = after_opening.strip_prefix("---\n") {
("", rest)
} else if after_opening == "---" {
("", "")
} else {
let mut found: Option<(usize, usize)> = None; let mut search = 0usize;
while let Some(nl) = after_opening[search..].find('\n') {
let line_start = search + nl + 1;
let line_end = after_opening[line_start..]
.find('\n')
.map(|p| line_start + p)
.unwrap_or(after_opening.len());
let line = after_opening[line_start..line_end].trim_end_matches('\r');
if line == "---" {
let body_start = if line_end < after_opening.len() {
line_end + 1
} else {
after_opening.len()
};
found = Some((search + nl, body_start));
break;
}
search = line_start;
}
match found {
Some((yaml_end, body_start)) => {
let yaml = after_opening[..yaml_end].trim_end_matches('\r');
let after_closing = &after_opening[body_start..];
(yaml, after_closing)
}
None => {
return ParsedContent {
front_matter: None,
content: content.to_string(),
};
}
}
};
match serde_yaml::from_str::<FrontMatter>(yaml_content) {
Ok(fm) => ParsedContent {
front_matter: Some(fm),
content: remaining.to_string(),
},
Err(_) => {
ParsedContent {
front_matter: None,
content: content.to_string(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_with_front_matter() {
let content = r#"---
title: My Custom Title
description: This is a description
---
# Hello World
Some content here.
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_some());
let fm = parsed.front_matter.unwrap();
assert_eq!(fm.title.as_deref(), Some("My Custom Title"));
assert_eq!(fm.description.as_deref(), Some("This is a description"));
assert!(parsed.content.contains("# Hello World"));
assert!(!parsed.content.contains("My Custom Title"));
}
#[test]
fn test_parse_without_front_matter() {
let content = r#"# Hello World
Some content here.
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_none());
assert_eq!(parsed.content, content);
}
#[test]
fn test_parse_with_only_title() {
let content = r#"---
title: Only Title
---
Content
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_some());
let fm = parsed.front_matter.unwrap();
assert_eq!(fm.title.as_deref(), Some("Only Title"));
assert!(fm.description.is_none());
}
#[test]
fn test_parse_with_empty_front_matter() {
let content = r#"---
---
Content
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_some());
let fm = parsed.front_matter.unwrap();
assert!(fm.title.is_none());
assert!(fm.description.is_none());
}
#[test]
fn test_parse_with_extra_fields() {
let content = r#"---
title: Test
author: John Doe
custom_field: value
---
Content
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_some());
let fm = parsed.front_matter.unwrap();
assert_eq!(fm.title.as_deref(), Some("Test"));
assert!(fm.extra.contains_key("author"));
assert!(fm.extra.contains_key("custom_field"));
}
#[test]
fn test_parse_invalid_yaml() {
let content = r#"---
title: [invalid yaml
---
Content
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_none());
assert_eq!(parsed.content, content);
}
#[test]
fn test_parse_not_at_start() {
let content = r#"Some text first
---
title: Not Front Matter
---
Content
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_none());
assert_eq!(parsed.content, content);
}
#[test]
fn test_parse_no_closing_delimiter() {
let content = r#"---
title: No Closing
Content
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_none());
assert_eq!(parsed.content, content);
}
#[test]
fn test_parse_japanese_content() {
let content = r#"---
title: Japanese Title
description: Japanese description
---
# Japanese Content
"#;
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_some());
let fm = parsed.front_matter.unwrap();
assert_eq!(fm.title.as_deref(), Some("Japanese Title"));
assert_eq!(fm.description.as_deref(), Some("Japanese description"));
}
#[test]
fn test_fuzz_empty_input() {
let parsed = parse_front_matter("");
assert!(parsed.front_matter.is_none());
}
#[test]
fn test_fuzz_only_delimiters() {
let parsed = parse_front_matter("---\n---");
assert!(parsed.front_matter.is_some());
}
#[test]
fn test_fuzz_triple_delimiters() {
let parsed = parse_front_matter("---\ntitle: A\n---\n---\ntitle: B\n---");
assert!(parsed.front_matter.is_some());
}
#[test]
fn test_fuzz_binary_in_yaml() {
let content = "---\ntitle: \x00\x01\x7f\n---\nContent";
let parsed = parse_front_matter(content);
let _ = parsed;
}
#[test]
fn test_fuzz_very_long_yaml() {
let title = "T".repeat(100_000);
let content = format!("---\ntitle: {}\n---\nContent", title);
let parsed = parse_front_matter(&content);
assert!(parsed.front_matter.is_some());
}
#[test]
fn test_fuzz_nested_yaml() {
let content = "---\ntitle:\n nested:\n deep: value\n---\nContent";
let parsed = parse_front_matter(content);
let _ = parsed;
}
#[test]
fn test_fuzz_yaml_with_special_chars() {
let content = "---\ntitle: \"quote: \\\"escaped\\\"\"\ndescription: 'single: \\'quoted\\''\n---\nContent";
let parsed = parse_front_matter(content);
let _ = parsed;
}
#[test]
fn test_four_dash_line_is_not_closing_delimiter() {
let content = "---\ntitle: Test\n----\nBody content here\n";
let parsed = parse_front_matter(content);
assert!(parsed.front_matter.is_none());
assert_eq!(parsed.content, content);
}
#[test]
fn test_dash_ruler_in_body_not_confused() {
let content = "---\ntitle: Test\n---\nBody\n\n----\n\nMore body\n";
let parsed = parse_front_matter(content);
assert_eq!(
parsed.front_matter.and_then(|f| f.title),
Some("Test".to_string())
);
assert!(parsed.content.contains("----"));
assert!(parsed.content.contains("More body"));
}
#[test]
fn test_bom_prefixed_front_matter_detected() {
let content = "\u{FEFF}---\ntitle: Bom\n---\nBody\n";
let parsed = parse_front_matter(content);
assert_eq!(
parsed.front_matter.and_then(|f| f.title),
Some("Bom".to_string())
);
assert_eq!(parsed.content, "Body\n");
}
#[test]
fn test_closing_delimiter_with_crlf() {
let content = "---\r\ntitle: Test\r\n---\r\nBody\r\n";
let parsed = parse_front_matter(content);
assert_eq!(
parsed.front_matter.and_then(|f| f.title),
Some("Test".to_string())
);
assert!(parsed.content.starts_with("Body"));
}
}