use std::str::FromStr; /// The content part of a file that fills in a template. pub struct Templated { pub frontmatter: Frontmatter, pub content: String, } impl FromStr for Templated { type Err = TemplateError; fn from_str(raw: &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: front.parse()?, content: content.to_owned(), }) } } pub struct Frontmatter { entries: Vec<(String, String)>, } impl FromStr for Frontmatter { type Err = TemplateError; fn from_str(raw: &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().to_owned(), value.trim().to_owned())); tracing::trace!("pushed frontmatter entry {} successfully", key.trim()); } Ok(Self { entries }) } } impl Frontmatter { pub fn get(&self, key: &str) -> Option<&str> { self.entries .iter() .find(|(k, _)| k == key) .map(|(_, v)| v.as_str()) } 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.as_str()) .collect() } } #[derive(Debug, snafu::Snafu)] pub enum TemplateError { #[snafu(display("file is missing the frontmatter"))] MissingFrontmatter, #[snafu(display("the frontmatter entry is malformed: {line}"))] MalformedFrontmatterEntry { line: String }, } #[cfg(test)] mod test { use std::str::FromStr; use crate::templated::{Frontmatter, Templated}; #[test] fn frontmatter_parse() { let frntmtr = "key=value\nkey2=value2"; assert!(Frontmatter::from_str(frntmtr).is_ok()) } #[test] fn frontmatter_fails_on_invalid_line() { let front = "key1=value1\nincorrect line"; assert!(Frontmatter::from_str(front).is_err()) } #[test] fn templated_parse() { let tempalted = "---\ntitle=Title!\n---\nContent line!"; assert!(Templated::from_str(tempalted).is_ok()) } #[test] fn templated_doesnt_eat_frontmatter_line() { let templated = "---\ntitle=Title---\nContent"; assert!(Templated::from_str(templated).is_err()); } #[test] fn templated_parses_correct() { let raw = "---\ntitle=Title!\n---\n

Paragraph!

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

Paragraph!

") } }