]> git.r.bdr.sh - rbdr/blog/blobdiff - src/template.rs
Attempt to build with sourcehut
[rbdr/blog] / src / template.rs
index 37906ede58842f9fb321747090a8bd5501c3af71..0213fc3c0d5c849a68fa824c6ad36ecd54c16529 100644 (file)
@@ -1,3 +1,5 @@
+use std::io::{Error, ErrorKind::Other, Result};
+use std::collections::HashMap;
 use std::fs::File;
 use std::path::PathBuf;
 use std::io::Read;
@@ -29,7 +31,7 @@ impl std::fmt::Display for Token {
                 write!(f, "\n]]]")
             },
             Token::IteratorDirective{collection, member_label, children} => {
-                write!(f, "IteratorDirective {}: {} [[[\n", collection, member_label)?;
+                write!(f, "{} in {}\n", collection, member_label)?;
                 for child in children {
                     write!(f, "\t{}\n", child)?;
                 }
@@ -39,24 +41,123 @@ impl std::fmt::Display for Token {
     }
 }
 
+#[derive(Clone)]
+pub enum TemplateValue {
+    String(String),
+    Unsigned(u64),
+    Bool(bool),
+    Collection(Vec<TemplateContext>),
+    Context(TemplateContext)
+}
+
+impl TemplateValue {
+    fn render(&self) -> String {
+        match self {
+            TemplateValue::String(string) => string.to_string(),
+            TemplateValue::Unsigned(number) => format!("{}", number),
+            TemplateValue::Bool(bool) => format!("{}", bool),
+            _ => "".to_string()
+        }
+    }
+}
+
+pub type TemplateContext = HashMap<String, TemplateValue>;
+
+struct TemplateContextGetter {}
+impl TemplateContextGetter {
+    fn get(context: &TemplateContext, path: &str) -> Option<TemplateValue> {
+        let path_parts: Vec<&str> = path.split('.').collect();
+        TemplateContextGetter::recursively_get_value(context, &path_parts)
+    }
+
+    fn recursively_get_value(context: &TemplateContext, path: &[&str]) -> Option<TemplateValue> {
+        match context.get(path[0]) {
+            Some(TemplateValue::Context(next)) if path.len() > 1 => TemplateContextGetter::recursively_get_value(next, &path[1..]),
+            Some(value) if path.len() == 1 => Some(value.clone()),
+            _ => None
+        }
+    }
+}
+
 pub struct ParsedTemplate {
     pub tokens: Vec<Token>
 }
 
-pub fn parse(template: &str) -> ParsedTemplate {
+impl ParsedTemplate {
+    pub fn render(&self, context: &TemplateContext) -> Result<String> {
+        ParsedTemplate::render_tokens(&self.tokens, context)
+    }
+
+    pub fn render_tokens(tokens: &Vec<Token>, context: &TemplateContext) -> Result<String> {
+        let mut rendered_template: String = String::new();
+        for token in tokens {
+            match token {
+                Token::Text(contents) => rendered_template.push_str(&contents),
+                Token::DisplayDirective { content } => {
+                    let value = TemplateContextGetter::get(context, &content)
+                        .ok_or_else(|| Error::new(Other, format!("{} is not a valid key", content)))?;
+                    rendered_template.push_str(&value.render());
+                },
+                Token::ConditionalDirective { condition, children} => {
+                    let mut negator = false;
+                    let mut condition = condition.to_string();
+                    if condition.starts_with('!') {
+                        negator = true;
+                        condition = condition[1..].to_string();
+                    }
+
+                    let value = TemplateContextGetter::get(context, &condition)
+                        .ok_or_else(|| Error::new(Other, format!("{} is not a valid key", condition)))?;
+
+                    match value {
+                        TemplateValue::Bool(value) => {
+                            if negator ^ value {
+                                rendered_template.push_str(&ParsedTemplate::render_tokens(children, context)?)
+                            }
+                            Ok(())
+                        },
+                        _ => Err(Error::new(Other, format!("{} is not a boolean value", condition))),
+                    }?;
+                },
+                Token::IteratorDirective { collection, member_label, children } => {
+                    let value = TemplateContextGetter::get(context, &collection)
+                        .ok_or_else(|| Error::new(Other, format!("{} is not a valid key", collection)))?;
+
+                    match value {
+                        TemplateValue::Collection(collection) => {
+                            for member in collection {
+                                let mut child_context = context.clone();
+                                child_context.insert(
+                                    member_label.to_string(),
+                                    TemplateValue::Context(member)
+                                );
+                                rendered_template.push_str(&ParsedTemplate::render_tokens(&children, &child_context)?)
+                            }
+                            Ok(())
+                        },
+                        _ => Err(Error::new(Other, format!("{} is not a collection", collection))),
+                    }?;
+                }
+            }
+        }
+        Ok(rendered_template)
+    }
+}
+
+pub fn parse(template: &str) -> Option<ParsedTemplate> {
     let mut tokens = Vec::new();
-    tokenize(template, &mut tokens);
-    ParsedTemplate {
+    tokenize(template, &mut tokens).ok()?;
+    Some(ParsedTemplate {
         tokens
-    }
+    })
 }
 
-fn tokenize(template: &str, tokens: &mut Vec<Token>) {
+fn tokenize(template: &str, tokens: &mut Vec<Token>) -> Result<()> {
     let mut remaining_template = template;
 
     while !remaining_template.is_empty() && remaining_template.contains("{{") {
         let directive_start_index = remaining_template.find("{{")
-            .expect("Was expecting at least one tag opener");
+            .ok_or_else(|| Error::new(Other, "Was expecting at least one tag opener"))?;
         if directive_start_index > 0 {
             let text = remaining_template[..directive_start_index].to_string();
             tokens.push(Token::Text(text.to_string()));
@@ -64,7 +165,7 @@ fn tokenize(template: &str, tokens: &mut Vec<Token>) {
         remaining_template = &remaining_template[directive_start_index..];
 
         let directive_end_index = remaining_template.find("}}")
-            .expect("Was expecting }} after {{") + 2;
+            .ok_or_else(|| Error::new(Other, "Was expecting }} after {{"))? + 2;
         let directive = &remaining_template[..directive_end_index];
         remaining_template = &remaining_template[directive_end_index..];
 
@@ -87,7 +188,7 @@ fn tokenize(template: &str, tokens: &mut Vec<Token>) {
                         let closing_block = remaining_template.find("{{?}}").unwrap();
                         let directive_block = &remaining_template[..closing_block];
                         remaining_template = &remaining_template[closing_block + 5..];
-                        tokenize(directive_block, &mut children);
+                        tokenize(directive_block, &mut children)?;
                         tokens.push(Token::ConditionalDirective{
                             condition: content.to_string(),
                             children
@@ -98,7 +199,7 @@ fn tokenize(template: &str, tokens: &mut Vec<Token>) {
                         let closing_block = remaining_template.find("{{~}}").unwrap();
                         let directive_block = &remaining_template[..closing_block];
                         remaining_template = &remaining_template[closing_block + 5..];
-                        tokenize(directive_block, &mut children);
+                        tokenize(directive_block, &mut children)?;
                         if parts.len() == 2 {
                             tokens.push(Token::IteratorDirective {
                                 collection: parts[0].trim().to_string(),
@@ -114,6 +215,7 @@ fn tokenize(template: &str, tokens: &mut Vec<Token>) {
         }
     }
     tokens.push(Token::Text(remaining_template.to_string()));
+    Ok(())
 }
 
 // File helpers.