about summary refs log tree commit diff
path: root/src/templated.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/templated.rs')
-rw-r--r--src/templated.rs125
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>")
+	}
+}