use crate::parser::template; use crate::parser::Body; use crate::parser::DustTag; use crate::parser::Special; use crate::parser::Template; use crate::parser::TemplateElement; use crate::renderer::context_element::ContextElement; use crate::renderer::errors::CompileError; use crate::renderer::errors::RenderError; use std::collections::HashMap; #[derive(Clone, Debug)] pub struct CompiledTemplate<'a> { template: Template<'a>, pub name: String, } #[derive(Clone, Debug)] pub struct DustRenderer<'a> { templates: HashMap>, } pub trait RenderWrapper { fn render_body<'a>( &'a self, renderer: &'a DustRenderer, body: &Body, ) -> Result; } impl RenderWrapper for C { fn render_body<'a>( &'a self, renderer: &'a DustRenderer, body: &Body, ) -> Result> { renderer.render_body(body, self) } } pub fn compile_template<'a>( source: &'a str, name: String, ) -> Result, CompileError> { // TODO: Make this all consuming // TODO: This could use better error management let (_remaining, parsed_template) = template(source).expect("Failed to compile template"); Ok(CompiledTemplate { template: parsed_template, name: name, }) } impl<'a> DustRenderer<'a> { pub fn new() -> DustRenderer<'a> { DustRenderer { templates: HashMap::new(), } } pub fn load_source(&mut self, template: &'a CompiledTemplate) { self.templates .insert(template.name.clone(), &template.template); } pub fn new_render( &'a self, name: &str, breadcrumbs: &Vec<&'a dyn ContextElement>, ) -> Result> { let main_template = match self.templates.get(name) { Some(tmpl) => tmpl, None => { return Err(RenderError::Generic(format!( "No template named {} in context", name ))); } }; self.new_render_body(&main_template.contents, breadcrumbs) } pub fn render(&'a self, name: &str, context: &'a C) -> Result> where C: ContextElement, { let main_template = match self.templates.get(name) { Some(tmpl) => tmpl, None => { return Err(RenderError::Generic(format!( "No template named {} in context", name ))); } }; self.render_body(&main_template.contents, context) } fn new_render_body( &'a self, body: &'a Body, breadcrumbs: &Vec<&'a dyn ContextElement>, ) -> Result> { let mut output = String::new(); for elem in &body.elements { match elem { TemplateElement::TEIgnoredWhitespace(_) => {} TemplateElement::TESpan(span) => output.push_str(span.contents), TemplateElement::TETag(dt) => { output.push_str(&self.new_render_tag(dt, breadcrumbs)?); } } } Ok(output) } fn render_body(&'a self, body: &Body, context: &'a C) -> Result> where C: ContextElement, { let mut output = String::new(); for elem in &body.elements { match elem { TemplateElement::TEIgnoredWhitespace(_) => {} TemplateElement::TESpan(span) => output.push_str(span.contents), TemplateElement::TETag(dt) => { output.push_str(&self.render_tag(dt, context)?); } } } Ok(output) } fn new_render_tag( &'a self, tag: &'a DustTag, breadcrumbs: &Vec<&'a dyn ContextElement>, ) -> Result> { match tag { DustTag::DTComment(_comment) => (), DustTag::DTSpecial(special) => { return Ok(match special { Special::Space => " ", Special::NewLine => "\n", Special::CarriageReturn => "\r", Special::LeftCurlyBrace => "{", Special::RightCurlyBrace => "}", } .to_owned()) } DustTag::DTReference(reference) => { let val = new_walk_path(breadcrumbs, &reference.path.keys); if let Err(RenderError::NotFound { .. }) = val { // If reference does not exist in the context, it becomes an empty string return Ok("".to_owned()); } else { return val?.render(&reference.filters); } } DustTag::DTSection(container) => { let val = new_walk_path(breadcrumbs, &container.path.keys); let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val)?; if loop_elements.is_empty() { // Oddly enough if the value is falsey (like // an empty array or null), Dust uses the // original context before walking the path as // the context for rendering the else block // // TODO: do filters apply? I don't think so // but I should test return match &container.else_contents { Some(body) => self.new_render_body(&body, breadcrumbs), None => Ok("".to_owned()), }; } else { match &container.contents { None => return Ok("".to_owned()), Some(body) => { let rendered_results: Result, RenderError> = loop_elements .into_iter() .map(|array_elem| { let mut new_breadcumbs = breadcrumbs.clone(); new_breadcumbs.push(array_elem); self.new_render_body(&body, &new_breadcumbs) }) .collect(); let rendered_slice: &[String] = &rendered_results?; return Ok(rendered_slice.join("")); } } } } _ => (), // TODO: Implement the rest } Ok("".to_owned()) } fn render_tag(&'a self, tag: &DustTag, context: &'a C) -> Result> where C: ContextElement, { match tag { DustTag::DTComment(_comment) => (), DustTag::DTReference(reference) => { let val = walk_path(context, &reference.path.keys); if let Err(RenderError::WontWalk { .. }) = val { // If reference does not exist in the context, it becomes an empty string return Ok("".to_owned()); } else if let Err(RenderError::CantWalk { .. }) = val { // If the context type does not support walking, it becomes an empty string return Ok("".to_owned()); } else { return val?.render(&reference.filters); } } DustTag::DTSection(container) => { let val = walk_path(context, &container.path.keys); let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val)?; if loop_elements.is_empty() { // Oddly enough if the value is falsey (like // an empty array or null), Dust uses the // original context before walking the path as // the context for rendering the else block // // TODO: do filters apply? I don't think so // but I should test return match &container.else_contents { Some(body) => self.render_body(&body, context), None => Ok("".to_owned()), }; } else { match &container.contents { None => return Ok("".to_owned()), Some(body) => { let rendered_results: Result, RenderError> = loop_elements .into_iter() .map(|array_elem| array_elem.render_body(self, &body)) .collect(); let rendered_slice: &[String] = &rendered_results?; return Ok(rendered_slice.join("")); } }; } } DustTag::DTSpecial(special) => { return Ok(match special { Special::Space => " ", Special::NewLine => "\n", Special::CarriageReturn => "\r", Special::LeftCurlyBrace => "{", Special::RightCurlyBrace => "}", } .to_owned()) } _ => (), // TODO: Implement the rest } Ok("".to_owned()) } /// Gets the elements to loop over for a section. /// /// If the value is falsey, and therefore should render the else /// block, this will return an empty vector. fn get_loop_elements<'b>( &'a self, walk_result: Result<&'b dyn ContextElement, RenderError<'b>>, ) -> Result, RenderError<'b>> { if let Err(RenderError::NotFound { .. }) = walk_result { // If reference does not exist in the context, render the else block Ok(vec![]) } else if let Err(RenderError::WontWalk { .. }) = walk_result { // If reference does not exist in the context, render the else block Ok(vec![]) } else if let Err(RenderError::CantWalk { .. }) = walk_result { // If the context type does not support walking, render the else block Ok(vec![]) } else { Ok(walk_result?.get_loop_elements()?) } } } enum WalkResult<'a> { NoWalk, PartialWalk, FullyWalked(&'a dyn ContextElement), } fn walk_path_from_single_level<'a>( context: &'a dyn ContextElement, path: &Vec<&str>, ) -> Result, RenderError<'a>> { if path.is_empty() { return Ok(WalkResult::FullyWalked(context)); } let mut walk_failure = WalkResult::NoWalk; let mut output = context; for elem in path.iter() { let new_val = output.walk(elem); if let Err(RenderError::WontWalk { .. }) = new_val { return Ok(walk_failure); } else if let Err(RenderError::CantWalk { .. }) = new_val { return Ok(walk_failure); } walk_failure = WalkResult::PartialWalk; output = new_val?; } Ok(WalkResult::FullyWalked(output)) } fn new_walk_path<'a>( breadcrumbs: &Vec<&'a dyn ContextElement>, path: &'a Vec<&str>, ) -> Result<&'a dyn ContextElement, RenderError<'a>> { for context in breadcrumbs.iter().rev() { match walk_path_from_single_level(*context, path)? { // If no walking was done at all, keep looping WalkResult::NoWalk => {} // If we partially walked then stop trying to find // anything WalkResult::PartialWalk => { return Err(RenderError::NotFound { path: path, breadcrumbs: breadcrumbs.clone(), }); } WalkResult::FullyWalked(new_context) => return Ok(new_context), } } Err(RenderError::NotFound { path: path, breadcrumbs: breadcrumbs.clone(), }) } fn walk_path<'a>( context: &'a dyn ContextElement, path: &Vec<&str>, ) -> Result<&'a dyn ContextElement, RenderError<'a>> { let mut output = context; for elem in path.iter() { output = output.walk(elem)?; } Ok(output) } #[cfg(test)] mod tests { use super::*; use crate::parser::Filter; use crate::renderer::context_element::Loopable; use crate::renderer::context_element::Renderable; use crate::renderer::context_element::Walkable; impl ContextElement for u32 {} impl ContextElement for &str {} impl ContextElement for HashMap<&str, I> {} impl Renderable for u32 { fn render(&self, _filters: &Vec) -> Result { // TODO: handle the filters Ok(self.to_string()) } } impl Renderable for &str { fn render(&self, _filters: &Vec) -> Result { // TODO: handle the filters Ok(self.to_string()) } } impl Renderable for HashMap<&str, I> { fn render(&self, _filters: &Vec) -> Result { // TODO: handle the filters Err(RenderError::CantRender { elem: self }) } } impl Walkable for HashMap<&str, I> { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { let child = self.get(segment).ok_or(RenderError::WontWalk { segment: segment.to_string(), elem: self, })?; Ok(child) } } impl Walkable for &str { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { Err(RenderError::CantWalk { segment: segment.to_string(), elem: self, }) } } impl Walkable for u32 { fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> { Err(RenderError::CantWalk { segment: segment.to_string(), elem: self, }) } } impl Loopable for &str { fn get_loop_elements(&self) -> Result, RenderError> { if self.is_empty() { Ok(Vec::new()) } else { Ok(vec![self]) } } } impl Loopable for u32 { fn get_loop_elements(&self) -> Result, RenderError> { Ok(vec![self]) } } impl Loopable for HashMap<&str, I> { fn get_loop_elements(&self) -> Result, RenderError> { Ok(vec![self]) } } #[test] fn test_new_walk_path() { let context: HashMap<&str, &str> = [("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")] .iter() .cloned() .collect(); let number_context: HashMap<&str, u32> = [("cat", 1), ("dog", 2), ("tiger", 3)] .iter() .cloned() .collect(); let deep_context: HashMap<&str, HashMap<&str, &str>> = [ ("cat", [("food", "meat")].iter().cloned().collect()), ("dog", [("food", "meat")].iter().cloned().collect()), ("tiger", [("food", "people")].iter().cloned().collect()), ] .iter() .cloned() .collect(); assert_eq!( new_walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"]) .unwrap() .render(&Vec::new()) .unwrap(), "kitty".to_owned() ); } #[test] fn test_walk_path() { let context: HashMap<&str, &str> = [("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")] .iter() .cloned() .collect(); let number_context: HashMap<&str, u32> = [("cat", 1), ("dog", 2), ("tiger", 3)] .iter() .cloned() .collect(); let deep_context: HashMap<&str, HashMap<&str, &str>> = [ ("cat", [("food", "meat")].iter().cloned().collect()), ("dog", [("food", "meat")].iter().cloned().collect()), ("tiger", [("food", "people")].iter().cloned().collect()), ] .iter() .cloned() .collect(); assert_eq!( walk_path(&context, &vec!["cat"]) .unwrap() .render(&Vec::new()) .unwrap(), "kitty".to_owned() ); assert_eq!( walk_path(&number_context, &vec!["tiger"]) .unwrap() .render(&Vec::new()) .unwrap(), "3".to_owned() ); assert_eq!( walk_path(&deep_context, &vec!["tiger", "food"]) .unwrap() .render(&Vec::new()) .unwrap(), "people".to_owned() ); } }