X-Git-Url: https://git.r.bdr.sh/rbdr/blog/blobdiff_plain/2998247083406f914b3647cedd19abf5507bf2c6..f7f4335199beaad56d9c962ac84e92a3f78907a6:/src/template.rs diff --git a/src/template.rs b/src/template.rs index 37906ed..0213fc3 100644 --- a/src/template.rs +++ b/src/template.rs @@ -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), + 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; + +struct TemplateContextGetter {} +impl TemplateContextGetter { + fn get(context: &TemplateContext, path: &str) -> Option { + let path_parts: Vec<&str> = path.split('.').collect(); + TemplateContextGetter::recursively_get_value(context, &path_parts) + } + + fn recursively_get_value(context: &TemplateContext, path: &[&str]) -> Option { + 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 } -pub fn parse(template: &str) -> ParsedTemplate { +impl ParsedTemplate { + pub fn render(&self, context: &TemplateContext) -> Result { + ParsedTemplate::render_tokens(&self.tokens, context) + } + + pub fn render_tokens(tokens: &Vec, context: &TemplateContext) -> Result { + 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 { 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) { +fn tokenize(template: &str, tokens: &mut Vec) -> 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) { 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) { 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) { 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) { } } tokens.push(Token::Text(remaining_template.to_string())); + Ok(()) } // File helpers.