diff options
Diffstat (limited to 'src/templated.rs')
-rw-r--r-- | src/templated.rs | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/src/templated.rs b/src/templated.rs new file mode 100644 index 0000000..c6daac8 --- /dev/null +++ b/src/templated.rs @@ -0,0 +1,125 @@ +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<Self, Self::Err> { + 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), + }, + }; + + 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<Self, Self::Err> { + 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())) + } + + 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<p>Paragraph!</p>"; + let tmpl = Templated::from_str(raw).unwrap(); + assert_eq!(tmpl.frontmatter.get("title").unwrap(), "Title!"); + assert_eq!(tmpl.content, "<p>Paragraph!</p>") + } +} |