use std::fmt; /// The content part of a file that fills in a template. pub struct Templated<'r> { pub frontmatter: Frontmatter<'r>, pub content: &'r str, } impl<'r> Templated<'r> { pub fn parse(raw: &'r str) -> Result { tracing::trace!("parsing Templated"); let (front, content) = match raw.strip_prefix("---\n") { None => return Err(TemplateError::MissingFrontmatter), Some(no_start) => match no_start.split_once("\n---\n") { None => return Err(TemplateError::MissingFrontmatter), Some((front, content)) => (front, content), }, }; tracing::trace!("isolated frontmatter"); Ok(Self { frontmatter: Frontmatter::parse(front)?, content, }) } } /// frontmatter from a template /// /// Frontmatter must always be at the top of the file bounded by `---` /// /// ```plain /// --- /// title=Page Title /// description=OpenGraph description /// --- /// /// Page content /// ``` pub struct Frontmatter<'r> { entries: Vec<(&'r str, &'r str)>, } impl<'r> Frontmatter<'r> { pub fn parse(raw: &'r str) -> Result { let mut entries = vec![]; for line in raw.lines() { let trimmed = line.trim(); if trimmed.is_empty() || trimmed.starts_with('#') { // Skip over blank lines and comments continue; } let (key, value) = match trimmed.split_once('=') { None => { return Err(TemplateError::MalformedFrontmatterEntry { line: trimmed.to_owned(), }); } Some(tup) => tup, }; entries.push((key.trim(), value.trim())); tracing::trace!("pushed frontmatter entry {} successfully", key.trim()); } Ok(Self { entries }) } pub fn get(&self, key: &str) -> Option<&str> { self.entries .iter() .find(|(k, _)| *k == key) .map(|(_, v)| *v) } /// Convenience for `[Frontmatter::get()]` and `to_owned()` pub fn get_owned(&self, key: &str) -> Option { self.entries .iter() .find(|(k, _)| *k == key) .map(|(_, v)| (*v).to_owned()) } pub fn get_many(&self, key: &str) -> Vec<&str> { // this could probably be a filter_map() but the explicit filter and // then map seems cleaner self.entries .iter() .filter(|(k, _)| *k == key) .map(|(_, v)| *v) .collect() } } #[derive(Debug)] pub enum TemplateError { MissingFrontmatter, MalformedFrontmatterEntry { line: String }, } impl std::error::Error for TemplateError {} impl fmt::Display for TemplateError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::MissingFrontmatter => write!(f, "the template does not have frontmatter"), Self::MalformedFrontmatterEntry { line } => { write!(f, "this frontmatter is malformed: {line}") } } } } #[cfg(test)] mod test { use crate::templated::{Frontmatter, Templated}; #[test] fn frontmatter_parse() { let frntmtr = "key=value\nkey2=value2"; assert!(Frontmatter::parse(frntmtr).is_ok()) } #[test] fn frontmatter_fails_on_invalid_line() { let front = "key1=value1\nincorrect line"; assert!(Frontmatter::parse(front).is_err()) } #[test] fn templated_parse() { let tempalted = "---\ntitle=Title!\n---\nContent line!"; assert!(Templated::parse(tempalted).is_ok()) } #[test] fn templated_doesnt_eat_frontmatter_line() { let templated = "---\ntitle=Title---\nContent"; assert!(Templated::parse(templated).is_err()); } #[test] fn templated_parses_correct() { let raw = "---\ntitle=Title!\n---\n

Paragraph!

"; let tmpl = Templated::parse(raw).unwrap(); assert_eq!(tmpl.frontmatter.get("title").unwrap(), "Title!"); assert_eq!(tmpl.content, "

Paragraph!

") } }