#[derive(Debug, Clone)]
pub struct FrontMatter {
pub header: &'static str,
pub footer: &'static str,
}
pub const DASH: FrontMatter = FrontMatter {
header: "---\n",
footer: "\n---\n",
};
pub const EQUAL: FrontMatter = FrontMatter {
header: "===\n",
footer: "\n===\n",
};
pub const PLUS: FrontMatter = FrontMatter {
header: "+++\n",
footer: "\n+++\n",
};
pub const LUA: FrontMatter = FrontMatter {
header: "--[===[\n",
footer: "\n]===]\n",
};
impl FrontMatter {
pub fn parse<'a>(&self, input: &'a str) -> (Option<&'a str>, &'a str) {
let input = strip_bom(input);
if !input.starts_with(self.header) {
return (None, input);
}
let start = self.header.len();
let rest = &input[start..];
if let Some(end) = rest.find(self.footer) {
let front_matter = &rest[..end];
let body_start = start + end + self.footer.len();
let body = &input[body_start..];
(Some(front_matter), body)
} else {
(None, input)
}
}
pub fn parse_in_place(&self, input: &mut String) -> Option<String> {
let mut start = bom_len(input);
if !input[start..].starts_with(self.header) {
return None;
}
start += self.header.len();
if let Some(end) = input[start..].find(self.footer) {
let front_matter = input[start..end + start].to_string();
let _ = input.drain(0..start + end + self.footer.len());
Some(front_matter)
} else {
None
}
}
}
fn strip_bom(s: &str) -> &str {
s.strip_prefix("\u{FEFF}").unwrap_or(s)
}
fn bom_len(s: &str) -> usize {
const BOM: &str = "\u{FEFF}";
if s.starts_with(BOM) { BOM.len() } else { 0 }
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(input: &str) -> (Option<&str>, &str) {
EQUAL.parse(input)
}
fn parse_in_place(input: &mut String) -> Option<String> {
EQUAL.parse_in_place(input)
}
#[test]
fn test_with_front_matter() {
let input = "\
===
title: Test
date: 2025-05-27
===
# Content
This is the body.";
let (fm, body) = parse(input);
assert_eq!(fm, Some("title: Test\ndate: 2025-05-27"));
assert_eq!(body, "# Content\nThis is the body.");
}
#[test]
fn test_with_lua_front_matter() {
let input = "\
--[===[
title: Test
date: 2025-05-27
]===]
# Content
This is the body.";
let (fm, body) = LUA.parse(input);
assert_eq!(fm, Some("title: Test\ndate: 2025-05-27"));
assert_eq!(body, "# Content\nThis is the body.");
}
#[test]
fn test_without_front_matter() {
let input = "# Content\nNo front matter here.";
let (fm, body) = parse(input);
assert!(fm.is_none());
assert_eq!(body, input);
}
#[test]
fn test_front_matter_without_end() {
let input = "\
===
title: Test
Still in front matter
No closing delimiter";
let (fm, body) = parse(input);
assert!(fm.is_none());
assert_eq!(body, input);
}
#[test]
fn test_empty_input() {
let input = "";
let (fm, body) = parse(input);
assert!(fm.is_none());
assert_eq!(body, "");
}
#[test]
fn test_only_front_matter() {
let input = "\
===
foo: bar
===
";
let (fm, body) = parse(input);
assert_eq!(fm, Some("foo: bar"));
assert_eq!(body, "");
}
mod in_place {
use super::*;
#[test]
fn test_with_front_matter() {
let mut input = "\
===\ntitle: Test\ndate: 2025-05-27\n===\n# Content\nBody text."
.to_string();
let fm = parse_in_place(&mut input);
assert_eq!(fm, Some("title: Test\ndate: 2025-05-27".to_string()));
assert_eq!(input, "# Content\nBody text.");
}
#[test]
fn test_without_front_matter() {
let original = "# Content\nNo front matter here.";
let mut input = original.to_string();
let fm = parse_in_place(&mut input);
assert_eq!(fm, None);
assert_eq!(input, original);
}
#[test]
fn test_front_matter_without_end() {
let original = "\
===\ntitle: Incomplete\nno end marker";
let mut input = original.to_string();
assert_eq!(original, "===\ntitle: Incomplete\nno end marker");
let fm = parse_in_place(&mut input);
assert_eq!(fm, None);
assert_eq!(input, original);
}
#[test]
fn test_empty_input() {
let mut input = "".to_string();
let fm = parse_in_place(&mut input);
assert_eq!(fm, None);
assert_eq!(input, "");
}
#[test]
fn test_only_front_matter() {
let mut input = "===\nfoo: bar\n===\n".to_string();
let fm = parse_in_place(&mut input);
assert_eq!(fm, Some("foo: bar".to_string()));
assert_eq!(input, "");
}
#[test]
fn test_trailing_newlines_after_front_matter() {
let mut input = "===\nfoo: bar\n===\n\n\nHello".to_string();
let fm = parse_in_place(&mut input);
assert_eq!(fm, Some("foo: bar".to_string()));
}
}
}