diff --git a/src/bin.rs b/src/bin.rs index cb0373f..ec05dd1 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -51,12 +51,12 @@ fn main() { .expect("There should be more than 1 template") .name; let breadcrumbs = vec![&context as &dyn IntoContextElement]; - println!( - "{}", - dust_renderer - .render(main_template_name, &breadcrumbs) - .expect("Failed to render") - ); + // println!( + // "{}", + // dust_renderer + // .render(main_template_name, &breadcrumbs) + // .expect("Failed to render") + // ); } fn template_from_file<'a>(file_path: &str, file_contents: &'a str) -> CompiledTemplate<'a> { diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 7debf9a..7eecacb 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -7,7 +7,6 @@ mod inline_partial_tree; mod iteration_context; mod parameters_context; mod renderer; -mod tree_renderer; mod tree_walking; mod walking; diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index adc8dec..455dfd9 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -1,24 +1,12 @@ use crate::parser::template; -use crate::parser::Body; -use crate::parser::DustTag; -use crate::parser::KVPair; -use crate::parser::PartialNameElement; use crate::parser::Path; -use crate::parser::RValue; -use crate::parser::Special; use crate::parser::Template; -use crate::parser::{Filter, TemplateElement}; +use crate::renderer::breadcrumb_tree::BreadcrumbTree; +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; use crate::renderer::context_element::ContextElement; use crate::renderer::context_element::IntoContextElement; use crate::renderer::errors::CompileError; -use crate::renderer::errors::RenderError; -use crate::renderer::errors::WalkError; -use crate::renderer::inline_partial_tree::extract_inline_partials; -use crate::renderer::inline_partial_tree::InlinePartialTreeElement; -use crate::renderer::iteration_context::IterationContext; -use crate::renderer::parameters_context::ParametersContext; -use crate::renderer::walking::walk_path; -use std::borrow::Borrow; +use crate::renderer::tree_walking::walk_path; use std::collections::HashMap; #[derive(Clone, Debug)] @@ -56,670 +44,23 @@ impl<'a> DustRenderer<'a> { .insert(template.name.clone(), &template.template); } - pub fn render( - &'a self, - name: &str, - breadcrumbs: &Vec<&'a dyn IntoContextElement>, - ) -> Result { - self.render_template(name, breadcrumbs, None) - } - - fn render_template( - &'a self, - name: &str, - breadcrumbs: &Vec<&'a dyn IntoContextElement>, - blocks: Option<&'a InlinePartialTreeElement<'a>>, - ) -> Result { - let main_template = match self.templates.get(name) { - Some(tmpl) => tmpl, - None => { - return Err(RenderError::TemplateNotFound(name.to_owned())); - } - }; - let extracted_inline_partials = extract_inline_partials(main_template); - let new_blocks = InlinePartialTreeElement::new(blocks, extracted_inline_partials); - let new_block_context = BlockContext { - breadcrumbs: breadcrumbs, - blocks: &new_blocks, - }; - self.render_body(&main_template.contents, breadcrumbs, &new_block_context) - } - - fn render_maybe_body( - &'a self, - body: &'a Option, - breadcrumbs: &Vec<&'a dyn IntoContextElement>, - blocks: &'a BlockContext<'a>, - ) -> Result { - match body { - None => Ok("".to_owned()), - Some(body) => Ok(self.render_body(body, breadcrumbs, blocks)?), - } - } - - fn render_body( - &'a self, - body: &'a Body, - breadcrumbs: &Vec<&'a dyn IntoContextElement>, - blocks: &'a BlockContext<'a>, - ) -> 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.render_tag(dt, breadcrumbs, blocks)?); - } - } - } - Ok(output) - } - - /// For rendering a dynamic partial's name or an rvalue template - pub fn render_partial_name( - &'a self, - body: &'a Vec, - breadcrumbs: &Vec<&'a dyn IntoContextElement>, - ) -> Result { - let converted_to_template_elements: Vec> = - body.into_iter().map(|e| e.into()).collect(); - // Simple templates like partial names and reference rvalues - // cannot contain blocks or inline partials, so we use a blank - // BlockContext. - let empty_block_context = BlockContext { - breadcrumbs: &Vec::new(), - blocks: &InlinePartialTreeElement::new(None, HashMap::new()), - }; - self.render_body( - &Body { - elements: converted_to_template_elements, - }, - breadcrumbs, - &empty_block_context, - ) - } - - fn render_tag( - &'a self, - tag: &'a DustTag, - breadcrumbs: &Vec<&'a dyn IntoContextElement>, - blocks: &'a BlockContext<'a>, - ) -> 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::DTLiteralStringBlock(literal) => return Ok((*literal).to_owned()), - DustTag::DTReference(reference) => { - let val = walk_path(breadcrumbs, &reference.path.keys) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - match val { - Err(WalkError::CantWalk) => return Ok("".to_owned()), - Ok(final_val) => { - return if final_val.is_truthy() { - final_val.render(&Self::preprocess_filters(&reference.filters)) - } else { - Ok("".to_owned()) - }; - } - } - } - DustTag::DTSection(container) => { - let injected_context = ParametersContext::new(breadcrumbs, &container.params); - let val = walk_path(breadcrumbs, &container.path.keys) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - match val { - Err(WalkError::CantWalk) => { - let new_breadcrumbs = self.new_breadcrumbs_section( - breadcrumbs, - None, - Some(&injected_context), - &container.explicit_context, - None, - ); - return self.render_maybe_body( - &container.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } - Ok(final_val) => { - return if final_val.is_truthy() { - match &container.contents { - // If the body is empty, just shortcut - // to an empty string now rather than - // generating intermediate contexts - // and iterating for nothing. - None => Ok("".to_owned()), - Some(body) => { - let loop_elements: Vec<&dyn ContextElement> = - final_val.get_loop_elements(); - if loop_elements.is_empty() { - // Scalar value - let new_breadcrumbs = self.new_breadcrumbs_section( - breadcrumbs, - None, - Some(&injected_context), - &container.explicit_context, - Some(final_val), - ); - self.render_body( - body, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - } else { - // Array-like value - let total_length = loop_elements.len(); - let rendered_results: Result, RenderError> = - loop_elements - .into_iter() - .enumerate() - .map(|(i, array_elem)| { - let index_context = - IterationContext::new(i, total_length); - let new_breadcrumbs = self - .new_breadcrumbs_section( - breadcrumbs, - Some(&index_context), - Some(&injected_context), - &container.explicit_context, - Some(array_elem), - ); - self.render_body( - &body, - new_breadcrumbs - .as_ref() - .unwrap_or(breadcrumbs), - blocks, - ) - }) - .collect(); - let rendered_slice: &[String] = &rendered_results?; - return Ok(rendered_slice.join("")); - } - } - } - } else { - // 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 - let new_breadcrumbs = self.new_breadcrumbs_section( - breadcrumbs, - None, - Some(&injected_context), - &container.explicit_context, - None, - ); - return self.render_maybe_body( - &container.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - }; - } - } - } - DustTag::DTExists(container) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - &container.explicit_context, - ); - let val = walk_path(breadcrumbs, &container.path.keys) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - return if val.map(|v| v.is_truthy()).unwrap_or(false) { - self.render_maybe_body( - &container.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - } else { - self.render_maybe_body( - &container.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - }; - } - DustTag::DTNotExists(container) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - &container.explicit_context, - ); - let val = walk_path(breadcrumbs, &container.path.keys) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - return if !val.map(|v| v.is_truthy()).unwrap_or(false) { - self.render_maybe_body( - &container.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - } else { - self.render_maybe_body( - &container.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - }; - } - DustTag::DTPartial(partial) => { - let partial_name = self.render_partial_name(&partial.name, breadcrumbs)?; - if partial.params.is_empty() { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - &partial.explicit_context, - ); - let rendered_content = self.render_template( - &partial_name, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - Some(blocks.blocks), - )?; - return Ok(rendered_content); - } else { - let injected_context = ParametersContext::new(breadcrumbs, &partial.params); - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - Some(&injected_context), - &partial.explicit_context, - ); - let rendered_content = self.render_template( - &partial_name, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - Some(blocks.blocks), - )?; - return Ok(rendered_content); - } - } - DustTag::DTInlinePartial(_named_block) => { - // Inline partials are blank during rendering (they get injected into blocks) - return Ok("".to_owned()); - } - DustTag::DTBlock(named_block) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - blocks.breadcrumbs, - None, - &named_block.explicit_context, - ); - return match blocks.blocks.get_block(named_block.path.keys[0]) { - None => self.render_maybe_body( - &named_block.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ), - Some(inline_partial) => self.render_maybe_body( - inline_partial, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ), - }; - } - DustTag::DTHelperEquals(parameterized_block) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - ¶meterized_block.explicit_context, - ); - let param_map: HashMap<&str, &RValue<'a>> = - Self::get_rval_map(¶meterized_block.params); - // Special case: when comparing two RVPaths, if the - // path is equal then dust assumes the values are - // equal (otherwise, non-scalar values are - // automatically not equal) - if Self::are_paths_identical(¶m_map) { - return match ¶meterized_block.contents { - None => Ok("".to_owned()), - Some(body) => { - let rendered_content = self.render_body( - body, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - )?; - Ok(rendered_content) - } - }; - } - - let left_side: Result<&dyn ContextElement, WalkError> = - match Self::get_rval(breadcrumbs, ¶m_map, "key") { - None => return Ok("".to_owned()), - Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - - if left_side == right_side { - return self.render_maybe_body( - ¶meterized_block.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } else { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } - } - DustTag::DTHelperNotEquals(parameterized_block) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - ¶meterized_block.explicit_context, - ); - let param_map: HashMap<&str, &RValue<'a>> = - Self::get_rval_map(¶meterized_block.params); - // Special case: when comparing two RVPaths, if the - // path is equal then dust assumes the values are - // equal (otherwise, non-scalar values are - // automatically not equal) - if Self::are_paths_identical(¶m_map) { - return match ¶meterized_block.else_contents { - None => Ok("".to_owned()), - Some(body) => { - let rendered_content = self.render_body( - body, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - )?; - Ok(rendered_content) - } - }; - } - - let left_side: Result<&dyn ContextElement, WalkError> = - match Self::get_rval(breadcrumbs, ¶m_map, "key") { - None => return Ok("".to_owned()), - Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - if left_side != right_side { - return self.render_maybe_body( - ¶meterized_block.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } else { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } - } - DustTag::DTHelperGreaterThan(parameterized_block) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - ¶meterized_block.explicit_context, - ); - let param_map: HashMap<&str, &RValue<'a>> = - Self::get_rval_map(¶meterized_block.params); - let left_side: Result<&dyn ContextElement, WalkError> = - match Self::get_rval(breadcrumbs, ¶m_map, "key") { - None => return Ok("".to_owned()), - Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - match (left_side, right_side) { - (Err(_), _) | (_, Err(_)) => { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - } - (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { - if left_side_unwrapped > right_side_unwrapped { - return self.render_maybe_body( - ¶meterized_block.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } else { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } - } - } - } - DustTag::DTHelperGreaterThanOrEquals(parameterized_block) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - ¶meterized_block.explicit_context, - ); - let param_map: HashMap<&str, &RValue<'a>> = - Self::get_rval_map(¶meterized_block.params); - let left_side: Result<&dyn ContextElement, WalkError> = - match Self::get_rval(breadcrumbs, ¶m_map, "key") { - None => return Ok("".to_owned()), - Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - match (left_side, right_side) { - (Err(_), _) | (_, Err(_)) => { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - } - (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { - if left_side_unwrapped >= right_side_unwrapped { - return self.render_maybe_body( - ¶meterized_block.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } else { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } - } - } - } - DustTag::DTHelperLessThan(parameterized_block) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - ¶meterized_block.explicit_context, - ); - let param_map: HashMap<&str, &RValue<'a>> = - Self::get_rval_map(¶meterized_block.params); - let left_side: Result<&dyn ContextElement, WalkError> = - match Self::get_rval(breadcrumbs, ¶m_map, "key") { - None => return Ok("".to_owned()), - Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - match (left_side, right_side) { - (Err(_), _) | (_, Err(_)) => { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - } - (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { - if left_side_unwrapped < right_side_unwrapped { - return self.render_maybe_body( - ¶meterized_block.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } else { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } - } - } - } - DustTag::DTHelperLessThanOrEquals(parameterized_block) => { - let new_breadcrumbs = self.new_breadcrumbs_partial( - breadcrumbs, - breadcrumbs, - None, - ¶meterized_block.explicit_context, - ); - let param_map: HashMap<&str, &RValue<'a>> = - Self::get_rval_map(¶meterized_block.params); - let left_side: Result<&dyn ContextElement, WalkError> = - match Self::get_rval(breadcrumbs, ¶m_map, "key") { - None => return Ok("".to_owned()), - Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)) - .map(|ice| ice.into_context_element(self, breadcrumbs)); - match (left_side, right_side) { - (Err(_), _) | (_, Err(_)) => { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ) - } - (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { - if left_side_unwrapped <= right_side_unwrapped { - return self.render_maybe_body( - ¶meterized_block.contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } else { - return self.render_maybe_body( - ¶meterized_block.else_contents, - new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), - blocks, - ); - } - } - } - } - } - Ok("".to_owned()) - } - - /// Gets the elements to loop over for a section. + /// Returns a option of a tuple of (parent, new_node_elements) + /// which can then be formed into new BreadcrumbTreeNodes /// - /// If the value is falsey, and therefore should render the else - /// block, this will return an empty vector. - fn get_loop_elements<'b>( - walk_result: Result<&'b dyn ContextElement, WalkError>, - ) -> Vec<&'b dyn ContextElement> { - match walk_result { - Err(WalkError::CantWalk) => Vec::new(), - Ok(walk_target) => walk_target.get_loop_elements(), - } - } - - fn are_paths_identical<'b>(param_map: &HashMap<&str, &RValue<'b>>) -> bool { - match (param_map.get("key"), param_map.get("value")) { - (None, _) => false, - (_, None) => false, - (Some(key_rval), Some(value_rval)) => match (key_rval, value_rval) { - (RValue::RVPath(key_path), RValue::RVPath(value_path)) => { - key_path.keys == value_path.keys - } - _ => false, - }, - } - } - - fn get_rval_map<'b>(params: &'b Vec>) -> HashMap<&'b str, &'b RValue<'b>> { - params - .iter() - .map(|pair: &KVPair<'b>| (pair.key, &pair.value)) - .collect() - } - - fn get_rval<'b>( - breadcrumbs: &'b Vec<&'b dyn IntoContextElement>, - param_map: &HashMap<&str, &'b RValue<'b>>, - key: &str, - ) -> Option> { - match param_map.get(key) { - None => None, - Some(rval) => match rval { - RValue::RVLiteral(literal) => Some(Ok(literal)), - RValue::RVTemplate(template) => None, - // TODO: Do I resolve the IntoContextElement here for RVPath? - RValue::RVPath(path) => Some(walk_path(breadcrumbs, &path.keys)), - }, - } - } - - fn preprocess_filters(filters: &Vec) -> Vec { - let mut final_filters: Vec = filters - .into_iter() - .filter(|f| f != &&Filter::DisableHtmlEncode) - .map(|f| f.clone()) - .collect(); - - // If the user has not specified any escaping filter (|s or - // |h), automatically add an html escape filter - if !filters.iter().any(|f| f == &Filter::DisableHtmlEncode) { - final_filters.push(Filter::HtmlEncode); - } - final_filters - } - + /// If None is returned, then it is a signal to simply re-use the + /// existing breadcrumbs. + /// + /// Otherwise, the parent (which may be None, especially for + /// explicit contexts) and the additional node elements (which may + /// be empty) should be combined into a final BreadcrumbTreeNode fn new_breadcrumbs_section<'b>( &self, - breadcrumbs: &'b Vec<&'b dyn IntoContextElement>, + maybe_breadcrumbs: Option<&'a BreadcrumbTree>, index_context: Option<&'b dyn IntoContextElement>, injected_context: Option<&'b dyn IntoContextElement>, explicit_context: &Option>, new_context_element: Option<&'b dyn ContextElement>, - ) -> Option> { + ) -> Option<(Option<&'b BreadcrumbTree>, Vec>)> { // If none of the additional contexts are present, return None // to signal that the original breadcrumbs should be used // rather than incurring a copy here. @@ -732,290 +73,29 @@ impl<'a> DustRenderer<'a> { (None, None, None, None) => return None, _ => (), } - let mut new_stack = match explicit_context { - Some(_) => Vec::with_capacity(4), - None => breadcrumbs.clone(), - }; - explicit_context.as_ref().map(|path| { - walk_path(breadcrumbs, &path.keys) - .map(|ice| ice.into_context_element(self, breadcrumbs)) - .map(|val| { - if val.is_truthy() { - new_stack.push(val.from_context_element()) - } - }); - }); - injected_context.map(|ctx| new_stack.push(ctx)); - new_context_element.map(|ctx| new_stack.push(ctx.from_context_element())); - index_context.map(|ctx| new_stack.push(ctx)); - Some(new_stack) - } - fn new_breadcrumbs_partial<'b>( - &self, - breadcrumbs: &'b Vec<&'b dyn IntoContextElement>, - explicit_context_breadcrumbs: &'b Vec<&'b dyn IntoContextElement>, - injected_context: Option<&'b dyn IntoContextElement>, - explicit_context: &Option>, - ) -> Option> { - // If none of the additional contexts are present, return None - // to signal that the original breadcrumbs should be used - // rather than incurring a copy here. - match (injected_context, explicit_context) { - (None, None) => return None, - _ => (), + // If there is an explicit context, then drop all the current + // context + let mut parent = match explicit_context { + Some(_) => None, + None => maybe_breadcrumbs, }; - let mut new_stack = match explicit_context { - Some(_) => Vec::with_capacity(3), - None => breadcrumbs.clone(), - }; - injected_context.map(|ctx| { - // Special case: when there is no explicit context, the - // injected context gets inserted 1 spot behind the - // current context. Otherwise, the injected context gets - // added after the current context but before the explicit - // context. - match explicit_context { - None => new_stack.insert( - Self::get_index_of_first_non_pseudo_element(&new_stack).unwrap_or(0), - ctx, - ), - _ => new_stack.push(ctx), - } - }); - explicit_context.as_ref().map(|path| { - walk_path(explicit_context_breadcrumbs, &path.keys) - // TODO should resolving the value here use explicit_context_breadcrumbs or breadcrumbs? - .map(|ice| ice.into_context_element(self, explicit_context_breadcrumbs)) - .map(|val| { - if val.is_truthy() { - new_stack.push(val.from_context_element()) - } - }); - }); - Some(new_stack) - } + let mut new_nodes = Vec::new(); - fn get_index_of_first_non_pseudo_element<'b, B>(breadcrumbs: &'b Vec) -> Option - where - B: Borrow, - { - breadcrumbs - .iter() - .rposition(|b| !(*b).borrow().is_pseudo_element()) - } -} - -struct BlockContext<'a> { - /// The breadcrumbs at the time of entering the current partial - breadcrumbs: &'a Vec<&'a dyn IntoContextElement>, - blocks: &'a InlinePartialTreeElement<'a>, -} - -#[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::Truthiness; - use crate::renderer::context_element::Walkable; - use crate::renderer::CompareContextElement; - use std::cmp::Ordering; - - impl ContextElement for String {} - - impl Truthiness for String { - fn is_truthy(&self) -> bool { - !self.is_empty() - } - } - - impl Renderable for String { - fn render(&self, _filters: &Vec) -> Result { - Ok(self.clone()) - } - } - - impl Loopable for String { - fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { - Vec::new() - } - } - - impl Walkable for String { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { - Err(WalkError::CantWalk) - } - } - - impl CompareContextElement for String { - fn equals(&self, other: &dyn ContextElement) -> bool { - match other.to_any().downcast_ref::() { - None => false, - Some(other_string) => self == other_string, - } - } - - fn partial_compare(&self, other: &dyn ContextElement) -> Option { - match other.to_any().downcast_ref::() { - None => None, - Some(other_string) => self.partial_cmp(other_string), - } - } - } - impl ContextElement for u64 {} - - impl Truthiness for u64 { - fn is_truthy(&self) -> bool { - true - } - } - - impl Renderable for u64 { - fn render(&self, _filters: &Vec) -> Result { - Ok(self.to_string()) - } - } - - impl Loopable for u64 { - fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { - Vec::new() - } - } - - impl Walkable for u64 { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { - Err(WalkError::CantWalk) - } - } - - impl CompareContextElement for u64 { - fn equals(&self, other: &dyn ContextElement) -> bool { - match other.to_any().downcast_ref::() { - None => false, - Some(other_num) => self == other_num, - } - } - - fn partial_compare(&self, other: &dyn ContextElement) -> Option { - match other.to_any().downcast_ref::() { - None => None, - Some(other_num) => self.partial_cmp(other_num), - } - } - } - - impl ContextElement for HashMap {} - - impl Truthiness for HashMap { - fn is_truthy(&self) -> bool { - true - } - } - - impl Renderable for HashMap { - fn render(&self, _filters: &Vec) -> Result { - // TODO: handle the filters - Ok("[object Object]".to_owned()) - } - } - - impl Walkable for HashMap { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { - let child = self.get(segment).ok_or(WalkError::CantWalk)?; - Ok(child) - } - } - - impl Loopable for HashMap { - fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { - Vec::new() - } - } - - impl CompareContextElement for HashMap { - fn equals(&self, other: &dyn ContextElement) -> bool { - false - } - - fn partial_compare(&self, other: &dyn ContextElement) -> Option { - // TODO: Implement - None - } - } - - #[test] - fn test_walk_path() { - let context: HashMap = [ - ("cat".to_string(), "kitty".to_string()), - ("dog".to_string(), "doggy".to_string()), - ("tiger".to_string(), "murderkitty".to_string()), - ] - .iter() - .cloned() - .collect(); - let number_context: HashMap = [ - ("cat".to_string(), 1), - ("dog".to_string(), 2), - ("tiger".to_string(), 3), - ] - .iter() - .cloned() - .collect(); - let deep_context: HashMap> = [ - ( - "cat".to_string(), - [("food".to_string(), "meat".to_string())] - .iter() - .cloned() - .collect(), - ), - ( - "dog".to_string(), - [("food".to_string(), "meat".to_string())] - .iter() - .cloned() - .collect(), - ), - ( - "tiger".to_string(), - [("food".to_string(), "people".to_string())] - .iter() - .cloned() - .collect(), - ), - ] - .iter() - .cloned() - .collect(); - - assert_eq!( - walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"]) - .unwrap() - .render(&Vec::new()) - .unwrap(), - "kitty".to_owned() - ); - assert_eq!( - walk_path( - &vec![&number_context as &dyn ContextElement], - &vec!["tiger"] - ) - .unwrap() - .render(&Vec::new()) - .unwrap(), - "3".to_owned() - ); - assert_eq!( - walk_path( - &vec![&deep_context as &dyn ContextElement], - &vec!["tiger", "food"] - ) - .unwrap() - .render(&Vec::new()) - .unwrap(), - "people".to_owned() - ); + // explicit_context.as_ref().map(|path| { + // let x = walk_path(maybe_breadcrumbs, &path.keys); + // x.map(|ice| ice.into_context_element(self, breadcrumbs)) + // .map(|val| { + // if val.is_truthy() { + // new_nodes.push(val.from_context_element()) + // } + // }); + // }); + injected_context.map(|ctx| new_nodes.push(BreadcrumbTreeElement::Borrowed(ctx))); + new_context_element + .map(|ctx| new_nodes.push(BreadcrumbTreeElement::Borrowed(ctx.from_context_element()))); + index_context.map(|ctx| new_nodes.push(BreadcrumbTreeElement::Borrowed(ctx))); + + Some((parent, new_nodes)) } } diff --git a/src/renderer/renderer_old.rs b/src/renderer/renderer_old.rs new file mode 100644 index 0000000..adc8dec --- /dev/null +++ b/src/renderer/renderer_old.rs @@ -0,0 +1,1021 @@ +use crate::parser::template; +use crate::parser::Body; +use crate::parser::DustTag; +use crate::parser::KVPair; +use crate::parser::PartialNameElement; +use crate::parser::Path; +use crate::parser::RValue; +use crate::parser::Special; +use crate::parser::Template; +use crate::parser::{Filter, TemplateElement}; +use crate::renderer::context_element::ContextElement; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::errors::CompileError; +use crate::renderer::errors::RenderError; +use crate::renderer::errors::WalkError; +use crate::renderer::inline_partial_tree::extract_inline_partials; +use crate::renderer::inline_partial_tree::InlinePartialTreeElement; +use crate::renderer::iteration_context::IterationContext; +use crate::renderer::parameters_context::ParametersContext; +use crate::renderer::walking::walk_path; +use std::borrow::Borrow; +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 fn compile_template<'a>( + source: &'a str, + name: String, +) -> Result, CompileError> { + // 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 render( + &'a self, + name: &str, + breadcrumbs: &Vec<&'a dyn IntoContextElement>, + ) -> Result { + self.render_template(name, breadcrumbs, None) + } + + fn render_template( + &'a self, + name: &str, + breadcrumbs: &Vec<&'a dyn IntoContextElement>, + blocks: Option<&'a InlinePartialTreeElement<'a>>, + ) -> Result { + let main_template = match self.templates.get(name) { + Some(tmpl) => tmpl, + None => { + return Err(RenderError::TemplateNotFound(name.to_owned())); + } + }; + let extracted_inline_partials = extract_inline_partials(main_template); + let new_blocks = InlinePartialTreeElement::new(blocks, extracted_inline_partials); + let new_block_context = BlockContext { + breadcrumbs: breadcrumbs, + blocks: &new_blocks, + }; + self.render_body(&main_template.contents, breadcrumbs, &new_block_context) + } + + fn render_maybe_body( + &'a self, + body: &'a Option, + breadcrumbs: &Vec<&'a dyn IntoContextElement>, + blocks: &'a BlockContext<'a>, + ) -> Result { + match body { + None => Ok("".to_owned()), + Some(body) => Ok(self.render_body(body, breadcrumbs, blocks)?), + } + } + + fn render_body( + &'a self, + body: &'a Body, + breadcrumbs: &Vec<&'a dyn IntoContextElement>, + blocks: &'a BlockContext<'a>, + ) -> 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.render_tag(dt, breadcrumbs, blocks)?); + } + } + } + Ok(output) + } + + /// For rendering a dynamic partial's name or an rvalue template + pub fn render_partial_name( + &'a self, + body: &'a Vec, + breadcrumbs: &Vec<&'a dyn IntoContextElement>, + ) -> Result { + let converted_to_template_elements: Vec> = + body.into_iter().map(|e| e.into()).collect(); + // Simple templates like partial names and reference rvalues + // cannot contain blocks or inline partials, so we use a blank + // BlockContext. + let empty_block_context = BlockContext { + breadcrumbs: &Vec::new(), + blocks: &InlinePartialTreeElement::new(None, HashMap::new()), + }; + self.render_body( + &Body { + elements: converted_to_template_elements, + }, + breadcrumbs, + &empty_block_context, + ) + } + + fn render_tag( + &'a self, + tag: &'a DustTag, + breadcrumbs: &Vec<&'a dyn IntoContextElement>, + blocks: &'a BlockContext<'a>, + ) -> 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::DTLiteralStringBlock(literal) => return Ok((*literal).to_owned()), + DustTag::DTReference(reference) => { + let val = walk_path(breadcrumbs, &reference.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match val { + Err(WalkError::CantWalk) => return Ok("".to_owned()), + Ok(final_val) => { + return if final_val.is_truthy() { + final_val.render(&Self::preprocess_filters(&reference.filters)) + } else { + Ok("".to_owned()) + }; + } + } + } + DustTag::DTSection(container) => { + let injected_context = ParametersContext::new(breadcrumbs, &container.params); + let val = walk_path(breadcrumbs, &container.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match val { + Err(WalkError::CantWalk) => { + let new_breadcrumbs = self.new_breadcrumbs_section( + breadcrumbs, + None, + Some(&injected_context), + &container.explicit_context, + None, + ); + return self.render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } + Ok(final_val) => { + return if final_val.is_truthy() { + match &container.contents { + // If the body is empty, just shortcut + // to an empty string now rather than + // generating intermediate contexts + // and iterating for nothing. + None => Ok("".to_owned()), + Some(body) => { + let loop_elements: Vec<&dyn ContextElement> = + final_val.get_loop_elements(); + if loop_elements.is_empty() { + // Scalar value + let new_breadcrumbs = self.new_breadcrumbs_section( + breadcrumbs, + None, + Some(&injected_context), + &container.explicit_context, + Some(final_val), + ); + self.render_body( + body, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + } else { + // Array-like value + let total_length = loop_elements.len(); + let rendered_results: Result, RenderError> = + loop_elements + .into_iter() + .enumerate() + .map(|(i, array_elem)| { + let index_context = + IterationContext::new(i, total_length); + let new_breadcrumbs = self + .new_breadcrumbs_section( + breadcrumbs, + Some(&index_context), + Some(&injected_context), + &container.explicit_context, + Some(array_elem), + ); + self.render_body( + &body, + new_breadcrumbs + .as_ref() + .unwrap_or(breadcrumbs), + blocks, + ) + }) + .collect(); + let rendered_slice: &[String] = &rendered_results?; + return Ok(rendered_slice.join("")); + } + } + } + } else { + // 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 + let new_breadcrumbs = self.new_breadcrumbs_section( + breadcrumbs, + None, + Some(&injected_context), + &container.explicit_context, + None, + ); + return self.render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + }; + } + } + } + DustTag::DTExists(container) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + &container.explicit_context, + ); + let val = walk_path(breadcrumbs, &container.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + return if val.map(|v| v.is_truthy()).unwrap_or(false) { + self.render_maybe_body( + &container.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + } else { + self.render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + }; + } + DustTag::DTNotExists(container) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + &container.explicit_context, + ); + let val = walk_path(breadcrumbs, &container.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + return if !val.map(|v| v.is_truthy()).unwrap_or(false) { + self.render_maybe_body( + &container.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + } else { + self.render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + }; + } + DustTag::DTPartial(partial) => { + let partial_name = self.render_partial_name(&partial.name, breadcrumbs)?; + if partial.params.is_empty() { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + &partial.explicit_context, + ); + let rendered_content = self.render_template( + &partial_name, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + Some(blocks.blocks), + )?; + return Ok(rendered_content); + } else { + let injected_context = ParametersContext::new(breadcrumbs, &partial.params); + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + Some(&injected_context), + &partial.explicit_context, + ); + let rendered_content = self.render_template( + &partial_name, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + Some(blocks.blocks), + )?; + return Ok(rendered_content); + } + } + DustTag::DTInlinePartial(_named_block) => { + // Inline partials are blank during rendering (they get injected into blocks) + return Ok("".to_owned()); + } + DustTag::DTBlock(named_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + blocks.breadcrumbs, + None, + &named_block.explicit_context, + ); + return match blocks.blocks.get_block(named_block.path.keys[0]) { + None => self.render_maybe_body( + &named_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ), + Some(inline_partial) => self.render_maybe_body( + inline_partial, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ), + }; + } + DustTag::DTHelperEquals(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + // Special case: when comparing two RVPaths, if the + // path is equal then dust assumes the values are + // equal (otherwise, non-scalar values are + // automatically not equal) + if Self::are_paths_identical(¶m_map) { + return match ¶meterized_block.contents { + None => Ok("".to_owned()), + Some(body) => { + let rendered_content = self.render_body( + body, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + )?; + Ok(rendered_content) + } + }; + } + + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + + if left_side == right_side { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } + } + DustTag::DTHelperNotEquals(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + // Special case: when comparing two RVPaths, if the + // path is equal then dust assumes the values are + // equal (otherwise, non-scalar values are + // automatically not equal) + if Self::are_paths_identical(¶m_map) { + return match ¶meterized_block.else_contents { + None => Ok("".to_owned()), + Some(body) => { + let rendered_content = self.render_body( + body, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + )?; + Ok(rendered_content) + } + }; + } + + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + if left_side != right_side { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } + } + DustTag::DTHelperGreaterThan(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match (left_side, right_side) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + } + (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { + if left_side_unwrapped > right_side_unwrapped { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } + } + } + } + DustTag::DTHelperGreaterThanOrEquals(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match (left_side, right_side) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + } + (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { + if left_side_unwrapped >= right_side_unwrapped { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } + } + } + } + DustTag::DTHelperLessThan(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match (left_side, right_side) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + } + (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { + if left_side_unwrapped < right_side_unwrapped { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } + } + } + } + DustTag::DTHelperLessThanOrEquals(parameterized_block) => { + let new_breadcrumbs = self.new_breadcrumbs_partial( + breadcrumbs, + breadcrumbs, + None, + ¶meterized_block.explicit_context, + ); + let param_map: HashMap<&str, &RValue<'a>> = + Self::get_rval_map(¶meterized_block.params); + let left_side: Result<&dyn ContextElement, WalkError> = + match Self::get_rval(breadcrumbs, ¶m_map, "key") { + None => return Ok("".to_owned()), + Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)), + }; + let right_side: Result<&dyn ContextElement, WalkError> = + Self::get_rval(breadcrumbs, ¶m_map, "value") + .unwrap_or(Err(WalkError::CantWalk)) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + match (left_side, right_side) { + (Err(_), _) | (_, Err(_)) => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + } + (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => { + if left_side_unwrapped <= right_side_unwrapped { + return self.render_maybe_body( + ¶meterized_block.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } else { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ); + } + } + } + } + } + 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>( + walk_result: Result<&'b dyn ContextElement, WalkError>, + ) -> Vec<&'b dyn ContextElement> { + match walk_result { + Err(WalkError::CantWalk) => Vec::new(), + Ok(walk_target) => walk_target.get_loop_elements(), + } + } + + fn are_paths_identical<'b>(param_map: &HashMap<&str, &RValue<'b>>) -> bool { + match (param_map.get("key"), param_map.get("value")) { + (None, _) => false, + (_, None) => false, + (Some(key_rval), Some(value_rval)) => match (key_rval, value_rval) { + (RValue::RVPath(key_path), RValue::RVPath(value_path)) => { + key_path.keys == value_path.keys + } + _ => false, + }, + } + } + + fn get_rval_map<'b>(params: &'b Vec>) -> HashMap<&'b str, &'b RValue<'b>> { + params + .iter() + .map(|pair: &KVPair<'b>| (pair.key, &pair.value)) + .collect() + } + + fn get_rval<'b>( + breadcrumbs: &'b Vec<&'b dyn IntoContextElement>, + param_map: &HashMap<&str, &'b RValue<'b>>, + key: &str, + ) -> Option> { + match param_map.get(key) { + None => None, + Some(rval) => match rval { + RValue::RVLiteral(literal) => Some(Ok(literal)), + RValue::RVTemplate(template) => None, + // TODO: Do I resolve the IntoContextElement here for RVPath? + RValue::RVPath(path) => Some(walk_path(breadcrumbs, &path.keys)), + }, + } + } + + fn preprocess_filters(filters: &Vec) -> Vec { + let mut final_filters: Vec = filters + .into_iter() + .filter(|f| f != &&Filter::DisableHtmlEncode) + .map(|f| f.clone()) + .collect(); + + // If the user has not specified any escaping filter (|s or + // |h), automatically add an html escape filter + if !filters.iter().any(|f| f == &Filter::DisableHtmlEncode) { + final_filters.push(Filter::HtmlEncode); + } + final_filters + } + + fn new_breadcrumbs_section<'b>( + &self, + breadcrumbs: &'b Vec<&'b dyn IntoContextElement>, + index_context: Option<&'b dyn IntoContextElement>, + injected_context: Option<&'b dyn IntoContextElement>, + explicit_context: &Option>, + new_context_element: Option<&'b dyn ContextElement>, + ) -> Option> { + // If none of the additional contexts are present, return None + // to signal that the original breadcrumbs should be used + // rather than incurring a copy here. + match ( + index_context, + injected_context, + explicit_context, + new_context_element, + ) { + (None, None, None, None) => return None, + _ => (), + } + let mut new_stack = match explicit_context { + Some(_) => Vec::with_capacity(4), + None => breadcrumbs.clone(), + }; + explicit_context.as_ref().map(|path| { + walk_path(breadcrumbs, &path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)) + .map(|val| { + if val.is_truthy() { + new_stack.push(val.from_context_element()) + } + }); + }); + injected_context.map(|ctx| new_stack.push(ctx)); + new_context_element.map(|ctx| new_stack.push(ctx.from_context_element())); + index_context.map(|ctx| new_stack.push(ctx)); + Some(new_stack) + } + + fn new_breadcrumbs_partial<'b>( + &self, + breadcrumbs: &'b Vec<&'b dyn IntoContextElement>, + explicit_context_breadcrumbs: &'b Vec<&'b dyn IntoContextElement>, + injected_context: Option<&'b dyn IntoContextElement>, + explicit_context: &Option>, + ) -> Option> { + // If none of the additional contexts are present, return None + // to signal that the original breadcrumbs should be used + // rather than incurring a copy here. + match (injected_context, explicit_context) { + (None, None) => return None, + _ => (), + }; + let mut new_stack = match explicit_context { + Some(_) => Vec::with_capacity(3), + None => breadcrumbs.clone(), + }; + injected_context.map(|ctx| { + // Special case: when there is no explicit context, the + // injected context gets inserted 1 spot behind the + // current context. Otherwise, the injected context gets + // added after the current context but before the explicit + // context. + match explicit_context { + None => new_stack.insert( + Self::get_index_of_first_non_pseudo_element(&new_stack).unwrap_or(0), + ctx, + ), + _ => new_stack.push(ctx), + } + }); + explicit_context.as_ref().map(|path| { + walk_path(explicit_context_breadcrumbs, &path.keys) + // TODO should resolving the value here use explicit_context_breadcrumbs or breadcrumbs? + .map(|ice| ice.into_context_element(self, explicit_context_breadcrumbs)) + .map(|val| { + if val.is_truthy() { + new_stack.push(val.from_context_element()) + } + }); + }); + Some(new_stack) + } + + fn get_index_of_first_non_pseudo_element<'b, B>(breadcrumbs: &'b Vec) -> Option + where + B: Borrow, + { + breadcrumbs + .iter() + .rposition(|b| !(*b).borrow().is_pseudo_element()) + } +} + +struct BlockContext<'a> { + /// The breadcrumbs at the time of entering the current partial + breadcrumbs: &'a Vec<&'a dyn IntoContextElement>, + blocks: &'a InlinePartialTreeElement<'a>, +} + +#[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::Truthiness; + use crate::renderer::context_element::Walkable; + use crate::renderer::CompareContextElement; + use std::cmp::Ordering; + + impl ContextElement for String {} + + impl Truthiness for String { + fn is_truthy(&self) -> bool { + !self.is_empty() + } + } + + impl Renderable for String { + fn render(&self, _filters: &Vec) -> Result { + Ok(self.clone()) + } + } + + impl Loopable for String { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + Vec::new() + } + } + + impl Walkable for String { + fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { + Err(WalkError::CantWalk) + } + } + + impl CompareContextElement for String { + fn equals(&self, other: &dyn ContextElement) -> bool { + match other.to_any().downcast_ref::() { + None => false, + Some(other_string) => self == other_string, + } + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + match other.to_any().downcast_ref::() { + None => None, + Some(other_string) => self.partial_cmp(other_string), + } + } + } + impl ContextElement for u64 {} + + impl Truthiness for u64 { + fn is_truthy(&self) -> bool { + true + } + } + + impl Renderable for u64 { + fn render(&self, _filters: &Vec) -> Result { + Ok(self.to_string()) + } + } + + impl Loopable for u64 { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + Vec::new() + } + } + + impl Walkable for u64 { + fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { + Err(WalkError::CantWalk) + } + } + + impl CompareContextElement for u64 { + fn equals(&self, other: &dyn ContextElement) -> bool { + match other.to_any().downcast_ref::() { + None => false, + Some(other_num) => self == other_num, + } + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + match other.to_any().downcast_ref::() { + None => None, + Some(other_num) => self.partial_cmp(other_num), + } + } + } + + impl ContextElement for HashMap {} + + impl Truthiness for HashMap { + fn is_truthy(&self) -> bool { + true + } + } + + impl Renderable for HashMap { + fn render(&self, _filters: &Vec) -> Result { + // TODO: handle the filters + Ok("[object Object]".to_owned()) + } + } + + impl Walkable for HashMap { + fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { + let child = self.get(segment).ok_or(WalkError::CantWalk)?; + Ok(child) + } + } + + impl Loopable for HashMap { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + Vec::new() + } + } + + impl CompareContextElement for HashMap { + fn equals(&self, other: &dyn ContextElement) -> bool { + false + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // TODO: Implement + None + } + } + + #[test] + fn test_walk_path() { + let context: HashMap = [ + ("cat".to_string(), "kitty".to_string()), + ("dog".to_string(), "doggy".to_string()), + ("tiger".to_string(), "murderkitty".to_string()), + ] + .iter() + .cloned() + .collect(); + let number_context: HashMap = [ + ("cat".to_string(), 1), + ("dog".to_string(), 2), + ("tiger".to_string(), 3), + ] + .iter() + .cloned() + .collect(); + let deep_context: HashMap> = [ + ( + "cat".to_string(), + [("food".to_string(), "meat".to_string())] + .iter() + .cloned() + .collect(), + ), + ( + "dog".to_string(), + [("food".to_string(), "meat".to_string())] + .iter() + .cloned() + .collect(), + ), + ( + "tiger".to_string(), + [("food".to_string(), "people".to_string())] + .iter() + .cloned() + .collect(), + ), + ] + .iter() + .cloned() + .collect(); + + assert_eq!( + walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"]) + .unwrap() + .render(&Vec::new()) + .unwrap(), + "kitty".to_owned() + ); + assert_eq!( + walk_path( + &vec![&number_context as &dyn ContextElement], + &vec!["tiger"] + ) + .unwrap() + .render(&Vec::new()) + .unwrap(), + "3".to_owned() + ); + assert_eq!( + walk_path( + &vec![&deep_context as &dyn ContextElement], + &vec!["tiger", "food"] + ) + .unwrap() + .render(&Vec::new()) + .unwrap(), + "people".to_owned() + ); + } +} diff --git a/src/renderer/tree_renderer.rs b/src/renderer/tree_renderer.rs deleted file mode 100644 index 455dfd9..0000000 --- a/src/renderer/tree_renderer.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::parser::template; -use crate::parser::Path; -use crate::parser::Template; -use crate::renderer::breadcrumb_tree::BreadcrumbTree; -use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; -use crate::renderer::context_element::ContextElement; -use crate::renderer::context_element::IntoContextElement; -use crate::renderer::errors::CompileError; -use crate::renderer::tree_walking::walk_path; -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 fn compile_template<'a>( - source: &'a str, - name: String, -) -> Result, CompileError> { - // 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); - } - - /// Returns a option of a tuple of (parent, new_node_elements) - /// which can then be formed into new BreadcrumbTreeNodes - /// - /// If None is returned, then it is a signal to simply re-use the - /// existing breadcrumbs. - /// - /// Otherwise, the parent (which may be None, especially for - /// explicit contexts) and the additional node elements (which may - /// be empty) should be combined into a final BreadcrumbTreeNode - fn new_breadcrumbs_section<'b>( - &self, - maybe_breadcrumbs: Option<&'a BreadcrumbTree>, - index_context: Option<&'b dyn IntoContextElement>, - injected_context: Option<&'b dyn IntoContextElement>, - explicit_context: &Option>, - new_context_element: Option<&'b dyn ContextElement>, - ) -> Option<(Option<&'b BreadcrumbTree>, Vec>)> { - // If none of the additional contexts are present, return None - // to signal that the original breadcrumbs should be used - // rather than incurring a copy here. - match ( - index_context, - injected_context, - explicit_context, - new_context_element, - ) { - (None, None, None, None) => return None, - _ => (), - } - - // If there is an explicit context, then drop all the current - // context - let mut parent = match explicit_context { - Some(_) => None, - None => maybe_breadcrumbs, - }; - let mut new_nodes = Vec::new(); - - // explicit_context.as_ref().map(|path| { - // let x = walk_path(maybe_breadcrumbs, &path.keys); - // x.map(|ice| ice.into_context_element(self, breadcrumbs)) - // .map(|val| { - // if val.is_truthy() { - // new_nodes.push(val.from_context_element()) - // } - // }); - // }); - injected_context.map(|ctx| new_nodes.push(BreadcrumbTreeElement::Borrowed(ctx))); - new_context_element - .map(|ctx| new_nodes.push(BreadcrumbTreeElement::Borrowed(ctx.from_context_element()))); - index_context.map(|ctx| new_nodes.push(BreadcrumbTreeElement::Borrowed(ctx))); - - Some((parent, new_nodes)) - } -}