diff --git a/js/test_cases/helpers_eq/main.dust b/js/test_cases/helpers_eq/main.dust index 3d182d5..aad3f75 100644 --- a/js/test_cases/helpers_eq/main.dust +++ b/js/test_cases/helpers_eq/main.dust @@ -21,3 +21,9 @@ {@eq key=array_of_some_obj value=array_of_some_obj}array_of_some_obj is equal to array_of_some_obj{:else}array_of_some_obj does not equal array_of_some_obj{/eq}{~n} {@eq key=array_of_some_obj value=copy_array_of_some_obj}array_of_some_obj is equal to copy_array_of_some_obj{:else}array_of_some_obj does not equal copy_array_of_some_obj{/eq}{~n} {@eq key=array_of_some_obj value=array_of_other_obj}array_of_some_obj is equal to array_of_other_obj{:else}array_of_some_obj does not equal array_of_other_obj{/eq}{~n} + +Do objects with different paths referencing the same variable match?{~n} +===================================================================={~n} +{#int renamed=some_obj} + {@eq key=some_obj value=renamed}some_obj equals renamed{:else}some_obj does not equal renamed{/eq}{~n} +{/int} diff --git a/js/test_cases/reference_parameters/README.md b/js/test_cases/reference_parameters/README.md new file mode 100644 index 0000000..7fe6fd4 --- /dev/null +++ b/js/test_cases/reference_parameters/README.md @@ -0,0 +1 @@ +Reference parameters are evaluated at the time of render, as opposed to direct parameters which are evaluated at the time of assignment. diff --git a/js/test_cases/reference_parameters/input1.json b/js/test_cases/reference_parameters/input1.json new file mode 100644 index 0000000..7a597e1 --- /dev/null +++ b/js/test_cases/reference_parameters/input1.json @@ -0,0 +1,32 @@ +{ + "name": "Bob", + "people": [ + { + "name": "Alice", + "petname": "rover" + } + ], + "truthy": "some truthy value", + "other_petname": [ + { + "petname": "spot" + } + ], + "array_petname": [ + { + "petname": [ + "foo", + "bar" + ] + } + ], + "some_object": { + "foo": "bar" + }, + "some_same_object": { + "foo": "bar" + }, + "some_different_object": { + "foo": "baz" + } +} diff --git a/js/test_cases/reference_parameters/main.dust b/js/test_cases/reference_parameters/main.dust new file mode 100644 index 0000000..4308d5a --- /dev/null +++ b/js/test_cases/reference_parameters/main.dust @@ -0,0 +1,64 @@ +Hello {name}, nice {pet}{~n} +{#people} + Hello {name}, nice {pet}{~n} +{/people} +{#people name="chris" pet="cat"} + Hello {name}, nice {pet}{~n} +{/people} + +Direct Parameters{~n} +================={~n} +{#people name="chris" pet=petname petname="whiskers"} + Hello {name}, nice {pet}{~n} +{/people} +{#people} + {#truthy name="chris" pet=petname petname="whiskers"} + Hello {name}, nice {pet}{~n} + {/truthy} +{/people} +{#people name="chris" pet=petname petname="whiskers"} + {#other_petname} + Hello {name}, nice {pet}{~n} + {/other_petname} +{/people} + +Reference Parameters{~n} +===================={~n} +{#people name="chris" pet="{petname}" petname="whiskers"} + Hello {name}, nice {pet}{~n} +{/people} +{#people} + {#truthy name="chris" pet="{petname}" petname="whiskers"} + Hello {name}, nice {pet}{~n} + {/truthy} +{/people} +{#people name="chris" pet="{petname}" petname="whiskers"} + {#other_petname} + Hello {name}, nice {pet}{~n} + {/other_petname} +{/people} +{! Can you have additional text in reference parameters, or just the reference !} +{#people name="chris" pet="{petname}!" petname="whiskers"} + {#other_petname} + Hello {name}, nice {pet}{~n} + {/other_petname} +{/people} +{! Can you have filters !} +{#people name="chris" pet="{petname|js}" petname="whiskers"} + {#other_petname} + Hello {name}, nice {pet}{~n} + {/other_petname} +{/people} +{! Can you go through multiple levels of references !} +{#truthy name="chris" pet="{petname}" petname="{deeperpetname}" deeperpetname="fluffy"} + Hello {name}, nice {pet}{~n} +{/truthy} + +Equality{~n} +========{~n} +{@eq key=some_object value=some_object}some_object equals some_object{:else}some_object does not equal some_object{/eq}{~n} +{@eq key=some_object value=some_same_object}some_object equals some_same_object{:else}some_object does not equal some_same_object{/eq}{~n} +{@eq key=some_object value="{some_object}"}some_object equals reference(some_object){:else}some_object does not equal reference(some_object){/eq}{~n} +{@eq key="{some_object}" value="{some_object}"}reference(some_object) equals reference(some_object){:else}reference(some_object) does not equal reference(some_object){/eq}{~n} +{@eq key="{some_object}" value="{some_same_object}"}reference(some_object) equals reference(some_same_object){:else}reference(some_object) does not equal reference(some_same_object){/eq}{~n} +{@eq key="{some_object}" value="{some_different_object}"}reference(some_object) equals reference(some_different_object){:else}reference(some_object) does not equal reference(some_different_object){/eq}{~n} diff --git a/src/bin.rs b/src/bin.rs index 2ca7e1a..b58c8ef 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -3,10 +3,12 @@ extern crate nom; use crate::renderer::CompareContextElement; use parser::Filter; use parser::OwnedLiteral; +use parser::Template; use renderer::compile_template; -use renderer::CompiledTemplate; +use renderer::CompileError; use renderer::ContextElement; use renderer::DustRenderer; +use renderer::IntoContextElement; use renderer::Loopable; use renderer::RenderError; use renderer::Renderable; @@ -37,32 +39,40 @@ fn main() { (p.to_string(), template_content) }) .collect(); - let compiled_templates: Vec = template_contents - .iter() - .map(|(p, contents)| template_from_file(p, contents)) - .collect(); - let mut dust_renderer = DustRenderer::new(); - compiled_templates.iter().for_each(|template| { - dust_renderer.load_source(template); - }); + let compiled_templates_result: Result, CompileError> = + template_contents + .iter() + .map(|(p, contents)| template_from_file(p, contents)) + .collect(); + let compiled_templates = compiled_templates_result.unwrap(); let main_template_name = &compiled_templates .first() .expect("There should be more than 1 template") - .name; - let breadcrumbs = vec![&context as &dyn ContextElement]; + .0; + let mut dust_renderer = DustRenderer::new(); + compiled_templates.iter().for_each(|(name, template)| { + dust_renderer.load_source(template, name.to_owned()); + }); println!( "{}", dust_renderer - .render(main_template_name, &breadcrumbs) + .render(main_template_name, Some(&context)) .expect("Failed to render") ); } -fn template_from_file<'a>(file_path: &str, file_contents: &'a str) -> CompiledTemplate<'a> { +fn template_from_file<'a>( + file_path: &str, + file_contents: &'a str, +) -> Result<(String, Template<'a>), CompileError> { let path: &Path = Path::new(file_path); - let name = path.file_stem().unwrap(); - compile_template(file_contents, name.to_string_lossy().to_string()) - .expect("Failed to compile template") + let name = path.file_stem().ok_or(CompileError { + message: format!("Failed to get file stem on {}", file_path), + })?; + Ok(( + name.to_string_lossy().to_string(), + compile_template(file_contents)?, + )) } fn read_context_from_stdin() -> serde_json::Value { @@ -165,61 +175,61 @@ fn apply_filter( filter: &Filter, ) -> Result { match (json_value, filter) { - // Html escape filter - (serde_json::Value::String(string), Filter::HtmlEncode) => { - Ok(serde_json::Value::String(html_escape(string))) - } - (_, Filter::HtmlEncode) => Ok(serde_json::Value::String(html_escape( - &json_value.render(&Vec::new())?, - ))), - // Disable html escape filter - (_, Filter::DisableHtmlEncode) => panic!("The |s filter is automatically removed by the renderer since it is a no-op during rendering."), - // Parse JSON filter - (serde_json::Value::String(string), Filter::JsonParse) => { - serde_json::from_str(&string).or(Err(RenderError::InvalidJson(string.to_owned()))) - } - (_, Filter::JsonParse) => { - let rendered_value = json_value.render(&Vec::new())?; - serde_json::from_str(&rendered_value).or(Err(RenderError::InvalidJson(rendered_value))) - } - // Json Stringify filter - (_, Filter::JsonStringify) => { - Ok(serde_json::Value::String(json_value.to_string())) - } - // Javascript escape filter - (serde_json::Value::String(string), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::String(javascript_escape(string))) - } - (serde_json::Value::Bool(boolean), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::Bool(*boolean)) - } - (serde_json::Value::Number(number), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::Number(number.clone())) - } - (serde_json::Value::Array(arr), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::Array(arr.clone())) - } - (serde_json::Value::Object(obj), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::Object(obj.clone())) - } - (_, Filter::JavascriptStringEncode) => Ok(serde_json::Value::String(javascript_escape( - &json_value.render(&Vec::new())?, - ))), - // EncodeURI filter - (serde_json::Value::String(string), Filter::EncodeUri) => { - Ok(serde_json::Value::String(encode_uri(string))) - } - (_, Filter::EncodeUri) => Ok(serde_json::Value::String(encode_uri( - &json_value.render(&Vec::new())?, - ))), - // EncodeURIComponent filter - (serde_json::Value::String(string), Filter::EncodeUriComponent) => { - Ok(serde_json::Value::String(encode_uri_component(string))) - } - (_, Filter::EncodeUriComponent) => Ok(serde_json::Value::String(encode_uri_component( - &json_value.render(&Vec::new())?, - ))), - } + // Html escape filter + (serde_json::Value::String(string), Filter::HtmlEncode) => { + Ok(serde_json::Value::String(html_escape(string))) + } + (_, Filter::HtmlEncode) => Ok(serde_json::Value::String(html_escape( + &json_value.render(&Vec::new())?, + ))), + // Disable html escape filter + (_, Filter::DisableHtmlEncode) => panic!("The |s filter is automatically removed by the renderer since it is a no-op during rendering."), + // Parse JSON filter + (serde_json::Value::String(string), Filter::JsonParse) => { + serde_json::from_str(&string).or(Err(RenderError::InvalidJson(string.to_owned()))) + } + (_, Filter::JsonParse) => { + let rendered_value = json_value.render(&Vec::new())?; + serde_json::from_str(&rendered_value).or(Err(RenderError::InvalidJson(rendered_value))) + } + // Json Stringify filter + (_, Filter::JsonStringify) => { + Ok(serde_json::Value::String(json_value.to_string())) + } + // Javascript escape filter + (serde_json::Value::String(string), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::String(javascript_escape(string))) + } + (serde_json::Value::Bool(boolean), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Bool(*boolean)) + } + (serde_json::Value::Number(number), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Number(number.clone())) + } + (serde_json::Value::Array(arr), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Array(arr.clone())) + } + (serde_json::Value::Object(obj), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Object(obj.clone())) + } + (_, Filter::JavascriptStringEncode) => Ok(serde_json::Value::String(javascript_escape( + &json_value.render(&Vec::new())?, + ))), + // EncodeURI filter + (serde_json::Value::String(string), Filter::EncodeUri) => { + Ok(serde_json::Value::String(encode_uri(string))) + } + (_, Filter::EncodeUri) => Ok(serde_json::Value::String(encode_uri( + &json_value.render(&Vec::new())?, + ))), + // EncodeURIComponent filter + (serde_json::Value::String(string), Filter::EncodeUriComponent) => { + Ok(serde_json::Value::String(encode_uri_component(string))) + } + (_, Filter::EncodeUriComponent) => Ok(serde_json::Value::String(encode_uri_component( + &json_value.render(&Vec::new())?, + ))), + } } fn apply_filters( @@ -276,7 +286,7 @@ impl Renderable for serde_json::Value { } impl Walkable for serde_json::Value { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { match self { serde_json::Value::Null => Err(WalkError::CantWalk), serde_json::Value::Bool(_boolean) => Err(WalkError::CantWalk), diff --git a/src/parser/parser.rs b/src/parser/parser.rs index 3e0696a..8598f9f 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -23,7 +23,7 @@ use nom::sequence::terminated; use nom::sequence::tuple; use nom::IResult; -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub enum DustTag<'a> { DTSpecial(Special), DTComment(Comment<'a>), @@ -52,12 +52,12 @@ pub enum Special { RightCurlyBrace, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub enum IgnoredWhitespace<'a> { StartOfLine(&'a str), } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Comment<'a> { value: &'a str, } @@ -65,12 +65,12 @@ pub struct Comment<'a> { /// A series of keys separated by '.' to reference a variable in the context /// /// Special case: If the path is just "." then keys will be an empty vec -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Path<'a> { pub keys: Vec<&'a str>, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Reference<'a> { pub path: Path<'a>, pub filters: Vec, @@ -87,12 +87,12 @@ pub enum Filter { JsonParse, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Span<'a> { pub contents: &'a str, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct ParameterizedBlock<'a> { pub path: Path<'a>, pub explicit_context: Option>, @@ -101,32 +101,33 @@ pub struct ParameterizedBlock<'a> { pub else_contents: Option>, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Partial<'a> { pub name: Vec, pub explicit_context: Option>, pub params: Vec>, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub enum OwnedLiteral { LString(String), LPositiveInteger(u64), } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub enum RValue<'a> { RVPath(Path<'a>), + RVTemplate(Vec), RVLiteral(OwnedLiteral), } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct KVPair<'a> { pub key: &'a str, pub value: RValue<'a>, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub enum PartialNameElement { PNSpan { contents: String, @@ -137,17 +138,17 @@ pub enum PartialNameElement { }, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Body<'a> { pub elements: Vec>, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Template<'a> { pub contents: Body<'a>, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, PartialEq)] pub enum TemplateElement<'a> { TESpan(Span<'a>), TETag(DustTag<'a>), @@ -308,13 +309,25 @@ fn postitive_integer_literal(i: &str) -> IResult<&str, u64> { )(i) } +fn template_string_rvalue(i: &str) -> IResult<&str, Vec> { + let (i, template_string) = verify(quoted_string, |s: &String| { + partial_quoted_tag(s.as_str()).is_ok() + })(i)?; + + let (_remaining, parsed_template_elements) = partial_quoted_tag(template_string.as_str()) + .expect("A successful parse was verified earlier with a call to verify()"); + let converted_template_elements = parsed_template_elements + .into_iter() + .map(|e| e.into()) + .collect(); + Ok((i, converted_template_elements)) +} + /// Either a literal or a path to a value fn rvalue(i: &str) -> IResult<&str, RValue> { alt(( map(path, RValue::RVPath), - map(quoted_string, |s| { - RValue::RVLiteral(OwnedLiteral::LString(s)) - }), + map(template_string_rvalue, RValue::RVTemplate), map(postitive_integer_literal, |num| { RValue::RVLiteral(OwnedLiteral::LPositiveInteger(num)) }), @@ -528,9 +541,7 @@ fn partial_with_quoted_tag<'a>( let (i, (name, maybe_explicit_context, params)) = delimited( tag(open_matcher), tuple(( - verify(quoted_string, |s: &String| { - partial_quoted_tag(s.as_str()).is_ok() - }), + template_string_rvalue, opt(preceded(tag(":"), path)), opt(delimited( space1, @@ -541,17 +552,10 @@ fn partial_with_quoted_tag<'a>( tag("/}"), )(i)?; - let (_remaining, template_name_elements) = partial_quoted_tag(name.as_str()) - .expect("A successful parse was verified earlier with a call to verify()"); - let partial_name_elements = template_name_elements - .into_iter() - .map(|e| e.into()) - .collect(); - Ok(( i, Partial { - name: partial_name_elements, + name: name, explicit_context: maybe_explicit_context, params: params.unwrap_or(Vec::new()), }, diff --git a/src/renderer/breadcrumb_tree.rs b/src/renderer/breadcrumb_tree.rs new file mode 100644 index 0000000..fd70bd0 --- /dev/null +++ b/src/renderer/breadcrumb_tree.rs @@ -0,0 +1,59 @@ +use crate::renderer::context_element::ContextElement; +use crate::renderer::context_element::IceResult; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::context_element::IntoRcIce; +use std::borrow::Borrow; +use std::rc::Rc; + +#[derive(Clone, Debug)] +pub enum BreadcrumbTreeElement<'a> { + // Using Rc so that when we need to create BreadcrumbTrees with + // the same BreadcrumbTreeElement but a different parent (for + // example, when inserting behind the tail), we don't need to the + // copy the already owned/malloc'd data. + Owned(Rc), + Borrowed(&'a dyn IntoContextElement), +} + +impl<'a> BreadcrumbTreeElement<'a> { + pub fn from_owned(val: I) -> BreadcrumbTreeElement<'a> { + BreadcrumbTreeElement::Owned(Rc::new(val)) + } + + pub fn from_borrowed(val: &'a dyn IntoContextElement) -> BreadcrumbTreeElement<'a> { + BreadcrumbTreeElement::Borrowed(val) + } +} + +impl<'a> From<&'a IceResult<'a>> for BreadcrumbTreeElement<'a> { + fn from(inp: &'a IceResult<'a>) -> Self { + match inp { + IceResult::Owned(rc_ce) => { + BreadcrumbTreeElement::from_borrowed(rc_ce.from_context_element()) + } + IceResult::Borrowed(ce) => { + BreadcrumbTreeElement::from_borrowed(ce.from_context_element()) + } + } + } +} + +impl<'a> From> for BreadcrumbTreeElement<'a> { + fn from(inp: IceResult<'a>) -> Self { + match inp { + IceResult::Owned(rc_ce) => BreadcrumbTreeElement::Owned(rc_ce.into_rc_ice()), + IceResult::Borrowed(ce) => { + BreadcrumbTreeElement::from_borrowed(ce.from_context_element()) + } + } + } +} + +impl<'a> Borrow for BreadcrumbTreeElement<'a> { + fn borrow(&self) -> &(dyn IntoContextElement + 'a) { + match self { + BreadcrumbTreeElement::Owned(ice) => ice.as_ref(), + BreadcrumbTreeElement::Borrowed(ice) => *ice, + } + } +} diff --git a/src/renderer/context_element.rs b/src/renderer/context_element.rs index ec2e5a0..8be76b9 100644 --- a/src/renderer/context_element.rs +++ b/src/renderer/context_element.rs @@ -1,7 +1,10 @@ use crate::parser::Filter; +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; use crate::renderer::errors::RenderError; use crate::renderer::errors::WalkError; +use crate::renderer::DustRenderer; use std::any::Any; +use std::rc::Rc; use std::{cmp::Ordering, fmt::Debug}; pub trait ContextElement: @@ -10,8 +13,10 @@ pub trait ContextElement: + Walkable + Renderable + Loopable - + CloneIntoBoxedContextElement + // + CloneIntoBoxedContextElement + CompareContextElement + + FromContextElement + + IntoRcIce { } @@ -20,7 +25,7 @@ pub trait Truthiness { } pub trait Walkable { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError>; + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError>; /// If an element contains meta information and should not be /// returned as the final result of a walk, this function should @@ -59,15 +64,15 @@ pub trait CompareContextElement: CastToAny { fn partial_compare(&self, other: &dyn ContextElement) -> Option; } -pub trait CloneIntoBoxedContextElement { - fn clone_to_box(&self) -> Box; -} +// pub trait CloneIntoBoxedContextElement { +// fn clone_to_box(&self) -> Box; +// } -impl CloneIntoBoxedContextElement for C { - fn clone_to_box(&self) -> Box { - Box::new(self.clone()) - } -} +// impl CloneIntoBoxedContextElement for C { +// fn clone_to_box(&self) -> Box { +// Box::new(self.clone()) +// } +// } impl CastToAny for C { fn to_any(&self) -> &dyn Any { @@ -86,3 +91,87 @@ impl<'a, 'b> PartialOrd<&'b dyn ContextElement> for &'a dyn ContextElement { self.partial_compare(*other) } } + +pub trait FromContextElement { + fn from_context_element(&self) -> &dyn IntoContextElement; +} + +impl FromContextElement for C { + fn from_context_element(&self) -> &dyn IntoContextElement { + self + } +} + +pub trait IntoContextElement: Debug + Walkable /* + CloneIntoBoxedContextElement*/ { + fn into_context_element<'a>( + &'a self, + renderer: &DustRenderer, + breadcrumbs: &'a Vec>, + ) -> Option>; +} + +impl IntoContextElement for C { + fn into_context_element<'a>( + &'a self, + renderer: &DustRenderer, + breadcrumbs: &'a Vec>, + ) -> Option> { + Some(IceResult::from_borrowed(self)) + } +} + +pub trait IntoRcIce { + fn into_rc_ice(self: Rc) -> Rc; +} + +impl IntoRcIce for C { + fn into_rc_ice(self: Rc) -> Rc { + Rc::clone(&self) as Rc + } +} + +#[derive(Clone, Debug)] +pub enum IceResult<'a> { + // Using Rc so that when we need to create BreadcrumbTrees with + // the same BreadcrumbTreeElement but a different parent (for + // example, when inserting behind the tail), we don't need to the + // copy the already owned/malloc'd data. + Owned(Rc), + Borrowed(&'a dyn ContextElement), +} + +impl<'a> IceResult<'a> { + pub fn from_owned(val: C) -> IceResult<'a> { + IceResult::Owned(Rc::new(val)) + } + + pub fn from_borrowed(val: &'a dyn ContextElement) -> IceResult<'a> { + IceResult::Borrowed(val) + } + + pub fn get_context_element_reference(&self) -> &dyn ContextElement { + match self { + IceResult::Owned(rc_ce) => rc_ce.as_ref(), + IceResult::Borrowed(ce) => *ce, + } + } +} + +impl<'a> IntoContextElement for IceResult<'a> { + fn into_context_element<'b>( + &'b self, + renderer: &DustRenderer, + breadcrumbs: &'b Vec>, + ) -> Option> { + match self { + IceResult::Owned(rc_ce) => Some(IceResult::from_borrowed(rc_ce.as_ref())), + IceResult::Borrowed(ce) => Some(IceResult::from_borrowed(*ce)), + } + } +} + +impl<'a> Walkable for IceResult<'a> { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + self.get_context_element_reference().walk(segment) + } +} diff --git a/src/renderer/iteration_context.rs b/src/renderer/iteration_context.rs index f6a3998..687fb71 100644 --- a/src/renderer/iteration_context.rs +++ b/src/renderer/iteration_context.rs @@ -1,5 +1,9 @@ +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; use crate::renderer::context_element::CompareContextElement; use crate::renderer::context_element::ContextElement; +use crate::renderer::context_element::IceResult; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::DustRenderer; use crate::renderer::Loopable; use crate::renderer::RenderError; use crate::renderer::Renderable; @@ -15,7 +19,7 @@ use std::cmp::Ordering; /// Functions the same as the injected parameters contexts for /// helpers/partials with parameters but this has no need for storing /// breadcrumbs since its simply storing two integers. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct IterationContext { idx: OwnedLiteral, len: OwnedLiteral, @@ -31,37 +35,18 @@ impl IterationContext { } } -impl ContextElement for IterationContext {} - -impl Truthiness for IterationContext { - fn is_truthy(&self) -> bool { - // TODO: Would this even ever be called? Won't matter, but I'd - // like to know. Since it is injected 1 above the current - // context, we wouldn't be able to access it with `{.}`. - true - } -} - -impl Renderable for IterationContext { - fn render(&self, _filters: &Vec) -> Result { - // TODO: Would this even ever be called? Won't matter, but I'd - // like to know. Since it is injected 1 above the current - // context, we wouldn't be able to access it with `{.}`. - Ok("[object Object]".to_owned()) - } -} - -impl Loopable for IterationContext { - fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { - // TODO: Would this even ever be called? Won't matter, but I'd - // like to know. Since it is injected 1 above the current - // context, we wouldn't be able to access it with `{.}`. - Vec::new() +impl IntoContextElement for IterationContext { + fn into_context_element<'b>( + &'b self, + renderer: &DustRenderer, + breadcrumbs: &'b Vec>, + ) -> Option> { + panic!("into_context_element cannot be called on pseudo elements"); } } impl Walkable for IterationContext { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { match segment { "$idx" => Ok(&self.idx), "$len" => Ok(&self.len), @@ -73,15 +58,3 @@ impl Walkable for IterationContext { true } } - -impl CompareContextElement for IterationContext { - fn equals(&self, other: &dyn ContextElement) -> bool { - // TODO: Does this ever happen? perhaps I should have a panic here. - false - } - - fn partial_compare(&self, other: &dyn ContextElement) -> Option { - // TODO: Does this ever happen? perhaps I should have a panic here. - None - } -} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index ff0729b..11f75f8 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -1,16 +1,19 @@ //! This module contains a renderer for a rust implementation of LinkedIn Dust +mod breadcrumb_tree; mod context_element; mod errors; mod inline_partial_tree; mod iteration_context; mod parameters_context; mod renderer; +mod tree_walking; mod walking; -pub use context_element::CloneIntoBoxedContextElement; +// pub use context_element::CloneIntoBoxedContextElement; pub use context_element::CompareContextElement; pub use context_element::ContextElement; +pub use context_element::IntoContextElement; pub use context_element::Loopable; pub use context_element::Renderable; pub use context_element::Truthiness; @@ -19,5 +22,5 @@ pub use errors::CompileError; pub use errors::RenderError; pub use errors::WalkError; pub use renderer::compile_template; -pub use renderer::CompiledTemplate; +// pub use renderer::CompiledTemplate; pub use renderer::DustRenderer; diff --git a/src/renderer/parameters_context.rs b/src/renderer/parameters_context.rs index 1d4dd8b..926def2 100644 --- a/src/renderer/parameters_context.rs +++ b/src/renderer/parameters_context.rs @@ -1,106 +1,94 @@ +use crate::parser::Filter; use crate::parser::KVPair; -use crate::parser::{Filter, OwnedLiteral, RValue}; +use crate::parser::OwnedLiteral; +use crate::parser::RValue; +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; use crate::renderer::context_element::CompareContextElement; use crate::renderer::context_element::ContextElement; -use crate::renderer::walking::walk_path; +use crate::renderer::context_element::IceResult; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::tree_walking::walk_path; +use crate::renderer::DustRenderer; use crate::renderer::Loopable; use crate::renderer::RenderError; use crate::renderer::Renderable; use crate::renderer::Truthiness; use crate::renderer::WalkError; use crate::renderer::Walkable; -use std::{cmp::Ordering, collections::HashMap}; - -/// Copy the data from an RValue to an Owned struct -/// -/// In order to get comparisons to work for our `ContextElement` trait -/// objects, we need to be able to use `std::any::Any`. Unfortunately, -/// `Any` requires that the structs do not have a lifetime (so they -/// will have a `'static` lifetime. This means that we cannot have a -/// `<'a>` appended to the struct type, so the struct cannot contain -/// any borrows. Rather than impose the copy cost in the parser, we -/// are imposing the cost of copying the data in the renderer because -/// the parser has no reason to not be able to reference data from the -/// input string. -#[derive(Clone, Debug)] -pub enum OwnedRValue { - RVPath(OwnedPath), - RVLiteral(OwnedLiteral), -} - -#[derive(Clone, Debug, PartialEq)] -pub struct OwnedPath { - pub keys: Vec, -} - -impl From<&RValue<'_>> for OwnedRValue { - fn from(original: &RValue<'_>) -> Self { - match original { - RValue::RVLiteral(literal) => OwnedRValue::RVLiteral(literal.clone()), - RValue::RVPath(path) => OwnedRValue::RVPath(OwnedPath { - keys: path.keys.iter().map(|k| k.to_string()).collect(), - }), - } - } -} +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::rc::Rc; #[derive(Debug)] -pub struct ParametersContext { - params: HashMap, - breadcrumbs: Vec>, +pub struct ParametersContext<'a> { + params: HashMap<&'a str, (&'a RValue<'a>, Option>)>, } -impl ParametersContext { - pub fn new(breadcrumbs: &Vec<&dyn ContextElement>, params: &Vec) -> ParametersContext { - let owned_params: HashMap = params - .iter() - .map(|kvpair| (kvpair.key.to_string(), OwnedRValue::from(&kvpair.value))) - .collect(); - let owned_breadcrumbs: Vec> = - breadcrumbs.iter().map(|ce| ce.clone_to_box()).collect(); +impl<'a> ParametersContext<'a> { + pub fn new( + renderer: &DustRenderer, + breadcrumbs: &'a Vec>, + params: &'a Vec, + ) -> Self { + // If the parameter is a Path, then we resolve it immediately + // to a context element because those are resolved using the + // breadcrumbs at the time of assignment. + // + // If the parameter is a template (for example `foo="{bar}"`) + // then those are resolved at the time of access rather than + // the time of assignment, so we leave them into their + // original IntoContextElement state. + let rendered_params: HashMap<&'a str, (&'a RValue<'a>, Option>)> = + params + .iter() + .map(|kvpair| { + let k = kvpair.key; + let v: Option> = match &kvpair.value { + RValue::RVLiteral(owned_literal) => { + Some(BreadcrumbTreeElement::from_borrowed(&kvpair.value)) + } + RValue::RVPath(path) => kvpair + .value + .into_context_element(renderer, breadcrumbs) + .map(std::convert::From::from), + RValue::RVTemplate(template) => { + Some(BreadcrumbTreeElement::from_borrowed(&kvpair.value)) + } + }; + (k, (&kvpair.value, v)) + }) + .collect(); ParametersContext { - params: owned_params, - breadcrumbs: owned_breadcrumbs, + params: rendered_params, } } -} -impl ContextElement for ParametersContext {} + pub fn get_original_rvalue(&self, segment: &str) -> Option<&'a RValue<'a>> { + self.params.get(segment).map(|(rvalue, _bte)| *rvalue) + } -impl Truthiness for ParametersContext { - fn is_truthy(&self) -> bool { - // TODO: Would this even ever be called? Won't matter, but I'd - // like to know. Since it is injected 1 above the current - // context, we wouldn't be able to access it with `{.}`. - true + pub fn contains_key(&self, segment: &str) -> bool { + self.params.contains_key(segment) } } -impl Renderable for ParametersContext { - fn render(&self, _filters: &Vec) -> Result { - // TODO: Would this even ever be called? Won't matter, but I'd - // like to know. Since it is injected 1 above the current - // context, we wouldn't be able to access it with `{.}`. - Ok("[object Object]".to_owned()) +impl<'a> IntoContextElement for ParametersContext<'a> { + fn into_context_element<'b>( + &'b self, + renderer: &DustRenderer, + breadcrumbs: &'b Vec>, + ) -> Option> { + panic!("into_context_element cannot be called on pseudo elements"); } } -impl Loopable for ParametersContext { - fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { - // TODO: Would this even ever be called? Won't matter, but I'd - // like to know. Since it is injected 1 above the current - // context, we wouldn't be able to access it with `{.}`. - Vec::new() - } -} - -impl Walkable for ParametersContext { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { - let rval = self.params.get(segment).ok_or(WalkError::CantWalk)?; - match rval { - OwnedRValue::RVPath(path) => walk_path(&self.breadcrumbs, &path.keys), - OwnedRValue::RVLiteral(literal) => Ok(literal), +impl<'a> Walkable for ParametersContext<'a> { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + match self.params.get(segment).map(|(_rvalue, bte)| bte) { + Some(Some(bte)) => Ok(bte.borrow()), + _ => Err(WalkError::CantWalk), } } @@ -109,34 +97,30 @@ impl Walkable for ParametersContext { } } -impl Clone for ParametersContext { - fn clone(&self) -> Self { - let new_params: HashMap = self - .params - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - let new_breadcrumbs: Vec> = self - .breadcrumbs - .iter() - .map(|bread| bread.clone_to_box()) - .collect(); - ParametersContext { - params: new_params, - breadcrumbs: new_breadcrumbs, +impl<'a> IntoContextElement for RValue<'a> { + fn into_context_element<'b>( + &'b self, + renderer: &DustRenderer, + breadcrumbs: &'b Vec>, + ) -> Option> { + match self { + RValue::RVLiteral(owned_literal) => Some(IceResult::from_borrowed(owned_literal)), + RValue::RVPath(path) => walk_path(breadcrumbs, &path.keys) + .map(|ice| ice.into_context_element(renderer, breadcrumbs)) + .ok() + .flatten(), + RValue::RVTemplate(template) => renderer + .render_partial_name(template, breadcrumbs) + .map(|rendered| OwnedLiteral::LString(rendered)) + .ok() + .map(|owned_literal| IceResult::from_owned(owned_literal)), } } } -impl CompareContextElement for ParametersContext { - fn equals(&self, other: &dyn ContextElement) -> bool { - // TODO: Does this ever happen? perhaps I should have a panic here. - false - } - - fn partial_compare(&self, other: &dyn ContextElement) -> Option { - // TODO: Does this ever happen? perhaps I should have a panic here. - None +impl<'a> Walkable for RValue<'a> { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + Err(WalkError::CantWalk) } } @@ -167,7 +151,7 @@ impl Loopable for OwnedLiteral { } impl Walkable for OwnedLiteral { - fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { Err(WalkError::CantWalk) } } diff --git a/src/renderer/parameters_context_old.rs b/src/renderer/parameters_context_old.rs new file mode 100644 index 0000000..0fb4abf --- /dev/null +++ b/src/renderer/parameters_context_old.rs @@ -0,0 +1,264 @@ +use crate::parser::KVPair; +use crate::parser::{Filter, OwnedLiteral, PartialNameElement, RValue}; +use crate::renderer::context_element::CompareContextElement; +use crate::renderer::context_element::ContextElement; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::walking::walk_path; +use crate::renderer::DustRenderer; +use crate::renderer::Loopable; +use crate::renderer::RenderError; +use crate::renderer::Renderable; +use crate::renderer::Truthiness; +use crate::renderer::WalkError; +use crate::renderer::Walkable; +use std::{cmp::Ordering, collections::HashMap}; + +/// Copy the data from an RValue to an Owned struct +/// +/// In order to get comparisons to work for our `ContextElement` trait +/// objects, we need to be able to use `std::any::Any`. Unfortunately, +/// `Any` requires that the structs do not have a lifetime (so they +/// will have a `'static` lifetime. This means that we cannot have a +/// `<'a>` appended to the struct type, so the struct cannot contain +/// any borrows. Rather than impose the copy cost in the parser, we +/// are imposing the cost of copying the data in the renderer because +/// the parser has no reason to not be able to reference data from the +/// input string. +#[derive(Clone, Debug)] +pub enum OwnedRValue { + RVPath(OwnedPath), + RVTemplate(Vec), + RVLiteral(OwnedLiteral), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct OwnedPath { + pub keys: Vec, +} + +impl From<&RValue<'_>> for OwnedRValue { + fn from(original: &RValue<'_>) -> Self { + match original { + RValue::RVLiteral(literal) => OwnedRValue::RVLiteral(literal.clone()), + RValue::RVTemplate(template) => OwnedRValue::RVTemplate(template.clone()), + RValue::RVPath(path) => OwnedRValue::RVPath(OwnedPath { + keys: path.keys.iter().map(|k| k.to_string()).collect(), + }), + } + } +} + +#[derive(Debug)] +pub struct ParametersContext { + params: HashMap, + breadcrumbs: Vec>, +} + +impl ParametersContext { + pub fn new( + breadcrumbs: &Vec<&dyn IntoContextElement>, + params: &Vec, + ) -> ParametersContext { + let owned_params: HashMap = params + .iter() + .map(|kvpair| (kvpair.key.to_string(), OwnedRValue::from(&kvpair.value))) + .collect(); + let owned_breadcrumbs: Vec> = + breadcrumbs.iter().map(|ce| ce.clone_to_box()).collect(); + + ParametersContext { + params: owned_params, + breadcrumbs: owned_breadcrumbs, + } + } +} + +impl ContextElement for ParametersContext {} + +impl Truthiness for ParametersContext { + fn is_truthy(&self) -> bool { + // TODO: Would this even ever be called? Won't matter, but I'd + // like to know. Since it is injected 1 above the current + // context, we wouldn't be able to access it with `{.}`. + true + } +} + +impl Renderable for ParametersContext { + fn render(&self, _filters: &Vec) -> Result { + // TODO: Would this even ever be called? Won't matter, but I'd + // like to know. Since it is injected 1 above the current + // context, we wouldn't be able to access it with `{.}`. + Ok("[object Object]".to_owned()) + } +} + +impl Loopable for ParametersContext { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + // TODO: Would this even ever be called? Won't matter, but I'd + // like to know. Since it is injected 1 above the current + // context, we wouldn't be able to access it with `{.}`. + Vec::new() + } +} + +impl Walkable for ParametersContext { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + let rval = self.params.get(segment).ok_or(WalkError::CantWalk)?; + match rval { + OwnedRValue::RVPath(path) => walk_path(&self.breadcrumbs, &path.keys), + OwnedRValue::RVTemplate(template) => Ok(template), + OwnedRValue::RVLiteral(literal) => Ok(literal), + } + } + + fn is_pseudo_element(&self) -> bool { + true + } +} + +impl Clone for ParametersContext { + fn clone(&self) -> Self { + let new_params: HashMap = self + .params + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let new_breadcrumbs: Vec> = self + .breadcrumbs + .iter() + .map(|bread| bread.clone_to_box()) + .collect(); + ParametersContext { + params: new_params, + breadcrumbs: new_breadcrumbs, + } + } +} + +impl CompareContextElement for ParametersContext { + fn equals(&self, other: &dyn ContextElement) -> bool { + // TODO: Does this ever happen? perhaps I should have a panic here. + false + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // TODO: Does this ever happen? perhaps I should have a panic here. + None + } +} + +impl ContextElement for OwnedLiteral {} + +impl Truthiness for OwnedLiteral { + fn is_truthy(&self) -> bool { + match self { + OwnedLiteral::LString(text) => !text.is_empty(), + OwnedLiteral::LPositiveInteger(num) => true, + } + } +} + +impl Renderable for OwnedLiteral { + fn render(&self, _filters: &Vec) -> Result { + match self { + OwnedLiteral::LString(text) => Ok(text.clone()), + OwnedLiteral::LPositiveInteger(num) => Ok(num.to_string()), + } + } +} + +impl Loopable for OwnedLiteral { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + Vec::new() + } +} + +impl Walkable for OwnedLiteral { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + Err(WalkError::CantWalk) + } +} + +impl CompareContextElement for OwnedLiteral { + fn equals(&self, other: &dyn ContextElement) -> bool { + // println!("equals literal {:?} | {:?}", self, other); + // If its an OwnedLiteral then compare them directly, + // otherwise defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => other.equals(self), + Some(other_literal) => match (self, other_literal) { + (OwnedLiteral::LString(self_text), OwnedLiteral::LString(other_text)) => { + self_text == other_text + } + (OwnedLiteral::LPositiveInteger(self_num), OwnedLiteral::LString(other_text)) => { + &self_num.to_string() == other_text + } + (OwnedLiteral::LString(self_text), OwnedLiteral::LPositiveInteger(other_num)) => { + self_text == &other_num.to_string() + } + ( + OwnedLiteral::LPositiveInteger(self_num), + OwnedLiteral::LPositiveInteger(other_num), + ) => self_num == other_num, + }, + } + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // println!("partial_compare literal {:?} | {:?}", self, other); + // If its an OwnedLiteral then compare them directly, + // otherwise defer to the other type's implementation of + // CompareContextElement since the end user could add any + // type. + match other.to_any().downcast_ref::() { + None => match other.partial_compare(self) { + None => None, + Some(ord) => match ord { + Ordering::Equal => Some(Ordering::Equal), + Ordering::Greater => Some(Ordering::Less), + Ordering::Less => Some(Ordering::Greater), + }, + }, + Some(other_literal) => match (self, other_literal) { + (OwnedLiteral::LString(self_text), OwnedLiteral::LString(other_text)) => { + self_text.partial_cmp(other_text) + } + (OwnedLiteral::LPositiveInteger(self_num), OwnedLiteral::LString(other_text)) => { + self_num.to_string().partial_cmp(other_text) + } + (OwnedLiteral::LString(self_text), OwnedLiteral::LPositiveInteger(other_num)) => { + self_text.partial_cmp(&other_num.to_string()) + } + ( + OwnedLiteral::LPositiveInteger(self_num), + OwnedLiteral::LPositiveInteger(other_num), + ) => self_num.partial_cmp(other_num), + }, + } + } +} + +impl IntoContextElement for Vec { + fn into_context_element( + &self, + renderer: &DustRenderer, + breadcrumbs: &Vec<&dyn IntoContextElement>, + ) -> &dyn ContextElement { + // OwnedLiteral::LString( + // renderer + // .render_partial_name(self, breadcrumbs) + // .expect("TODO: Make into_context_element return a RenderError"), + // ) + // TODO + &OwnedLiteral::LPositiveInteger(1) + } +} + +impl Walkable for Vec { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + Err(WalkError::CantWalk) + } +} diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index c572d16..6759c53 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -1,14 +1,19 @@ use crate::parser::template; use crate::parser::Body; use crate::parser::DustTag; +use crate::parser::Filter; 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::parser::TemplateElement; +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; use crate::renderer::context_element::ContextElement; +use crate::renderer::context_element::IceResult; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::context_element::Walkable; use crate::renderer::errors::CompileError; use crate::renderer::errors::RenderError; use crate::renderer::errors::WalkError; @@ -16,31 +21,21 @@ 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 crate::renderer::tree_walking::walk_path; use std::borrow::Borrow; use std::collections::HashMap; - -#[derive(Clone, Debug)] -pub struct CompiledTemplate<'a> { - template: Template<'a>, - pub name: String, -} +use std::rc::Rc; #[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, - }) +pub fn compile_template<'a>(source: &'a str) -> Result, CompileError> { + let (_remaining, parsed_template) = template(source).map_err(|err| CompileError { + message: "Failed to compile template".to_owned(), + })?; + Ok(parsed_template) } impl<'a> DustRenderer<'a> { @@ -50,23 +45,24 @@ impl<'a> DustRenderer<'a> { } } - pub fn load_source(&mut self, template: &'a CompiledTemplate) { - self.templates - .insert(template.name.clone(), &template.template); + pub fn load_source(&mut self, template: &'a Template, name: String) { + self.templates.insert(name, template); } - pub fn render( - &'a self, - name: &str, - breadcrumbs: &Vec<&'a dyn ContextElement>, - ) -> Result { - self.render_template(name, breadcrumbs, None) + pub fn render(&'a self, name: &str, context: Option<&C>) -> Result + where + C: IntoContextElement, + { + let breadcrumbs = context + .map(|ctx| vec![BreadcrumbTreeElement::from_borrowed(ctx)]) + .unwrap_or(Vec::new()); + self.render_template(name, breadcrumbs.as_ref(), None) } - fn render_template( + pub fn render_template( &'a self, name: &str, - breadcrumbs: &Vec<&'a dyn ContextElement>, + breadcrumbs: &'a Vec>, blocks: Option<&'a InlinePartialTreeElement<'a>>, ) -> Result { let main_template = match self.templates.get(name) { @@ -87,7 +83,7 @@ impl<'a> DustRenderer<'a> { fn render_maybe_body( &'a self, body: &'a Option, - breadcrumbs: &Vec<&'a dyn ContextElement>, + breadcrumbs: &'a Vec>, blocks: &'a BlockContext<'a>, ) -> Result { match body { @@ -99,7 +95,7 @@ impl<'a> DustRenderer<'a> { fn render_body( &'a self, body: &'a Body, - breadcrumbs: &Vec<&'a dyn ContextElement>, + breadcrumbs: &'a Vec>, blocks: &'a BlockContext<'a>, ) -> Result { let mut output = String::new(); @@ -115,28 +111,34 @@ impl<'a> DustRenderer<'a> { Ok(output) } - /// For rendering a dynamic partial's name - fn render_partial_name( + /// 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 ContextElement>, - blocks: &'a BlockContext<'a>, + breadcrumbs: &'a Vec>, ) -> 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, - blocks, + &empty_block_context, ) } fn render_tag( &'a self, tag: &'a DustTag, - breadcrumbs: &Vec<&'a dyn ContextElement>, + breadcrumbs: &'a Vec>, blocks: &'a BlockContext<'a>, ) -> Result { match tag { @@ -153,12 +155,15 @@ impl<'a> DustRenderer<'a> { } DustTag::DTLiteralStringBlock(literal) => return Ok((*literal).to_owned()), DustTag::DTReference(reference) => { - let val = walk_path(breadcrumbs, &reference.path.keys); + 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)) + Err(WalkError::CantWalk) | Ok(None) => return Ok("".to_owned()), + Ok(Some(final_val)) => { + return if final_val.get_context_element_reference().is_truthy() { + final_val + .get_context_element_reference() + .render(&Self::preprocess_filters(&reference.filters)) } else { Ok("".to_owned()) }; @@ -166,11 +171,12 @@ impl<'a> DustRenderer<'a> { } } DustTag::DTSection(container) => { - let injected_context = ParametersContext::new(breadcrumbs, &container.params); - let val = walk_path(breadcrumbs, &container.path.keys); + let injected_context = ParametersContext::new(self, 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( + Err(WalkError::CantWalk) | Ok(None) => { + let new_breadcrumbs = self.new_breadcrumbs_section( breadcrumbs, None, Some(&injected_context), @@ -183,25 +189,22 @@ impl<'a> DustRenderer<'a> { blocks, ); } - Ok(final_val) => { - return if final_val.is_truthy() { + Ok(Some(final_val)) => { + let context_element = final_val.get_context_element_reference(); + return if context_element.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(); + context_element.get_loop_elements(); if loop_elements.is_empty() { // Scalar value - let new_breadcrumbs = Self::new_breadcrumbs_section( + let new_breadcrumbs = self.new_breadcrumbs_section( breadcrumbs, None, Some(&injected_context), &container.explicit_context, - Some(final_val), + Some(context_element), ); self.render_body( body, @@ -218,8 +221,8 @@ impl<'a> DustRenderer<'a> { .map(|(i, array_elem)| { let index_context = IterationContext::new(i, total_length); - let new_breadcrumbs = - Self::new_breadcrumbs_section( + let new_breadcrumbs = self + .new_breadcrumbs_section( breadcrumbs, Some(&index_context), Some(&injected_context), @@ -245,70 +248,72 @@ impl<'a> DustRenderer<'a> { // 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( + let new_breadcrumbs = self.new_breadcrumbs_section( breadcrumbs, None, Some(&injected_context), &container.explicit_context, None, ); - return self.render_maybe_body( + 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( + let new_breadcrumbs = self.new_breadcrumbs_partial( breadcrumbs, breadcrumbs, None, &container.explicit_context, ); - let val = walk_path(breadcrumbs, &container.path.keys); - 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( + let val = walk_path(breadcrumbs, &container.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + return match val { + Ok(Some(v)) if v.get_context_element_reference().is_truthy() => self + .render_maybe_body( + &container.contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ), + _ => 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( + let new_breadcrumbs = self.new_breadcrumbs_partial( breadcrumbs, breadcrumbs, None, &container.explicit_context, ); - let val = walk_path(breadcrumbs, &container.path.keys); - return if !val.map(|v| v.is_truthy()).unwrap_or(false) { - self.render_maybe_body( + let val = walk_path(breadcrumbs, &container.path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)); + return match val { + Ok(Some(v)) if v.get_context_element_reference().is_truthy() => self + .render_maybe_body( + &container.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ), + _ => 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, blocks)?; + let partial_name = self.render_partial_name(&partial.name, breadcrumbs)?; if partial.params.is_empty() { - let new_breadcrumbs = Self::new_breadcrumbs_partial( + let new_breadcrumbs = self.new_breadcrumbs_partial( breadcrumbs, breadcrumbs, None, @@ -321,8 +326,9 @@ impl<'a> DustRenderer<'a> { )?; return Ok(rendered_content); } else { - let injected_context = ParametersContext::new(breadcrumbs, &partial.params); - let new_breadcrumbs = Self::new_breadcrumbs_partial( + let injected_context = + ParametersContext::new(self, breadcrumbs, &partial.params); + let new_breadcrumbs = self.new_breadcrumbs_partial( breadcrumbs, breadcrumbs, Some(&injected_context), @@ -341,7 +347,7 @@ impl<'a> DustRenderer<'a> { return Ok("".to_owned()); } DustTag::DTBlock(named_block) => { - let new_breadcrumbs = Self::new_breadcrumbs_partial( + let new_breadcrumbs = self.new_breadcrumbs_partial( breadcrumbs, blocks.breadcrumbs, None, @@ -361,42 +367,41 @@ impl<'a> DustRenderer<'a> { }; } DustTag::DTHelperEquals(parameterized_block) => { - let new_breadcrumbs = Self::new_breadcrumbs_partial( + 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 param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params); + + if !param_map.contains_key("key") { + return Ok("".to_owned()); } - - let left_side: Result<&dyn ContextElement, WalkError> = - match Self::get_rval(breadcrumbs, ¶m_map, "key") { - None => return Ok("".to_owned()), - Some(res) => res, - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)); - - if left_side == right_side { + let left_side = param_map + .walk("key") + .map(|ice| ice.into_context_element(self, breadcrumbs)); + let right_side = param_map + .walk("value") + .map(|ice| ice.into_context_element(self, breadcrumbs)); + // Special case: when comparing two RVPaths, if the + // path points to the same value then they are + // equal. This is particularly important for objects + // which compare memory locations rather than contents + // (javascript object equality). + if Self::new_are_paths_identical(&left_side, &right_side) + || left_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }) == right_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }) + { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), @@ -411,81 +416,78 @@ impl<'a> DustRenderer<'a> { } } DustTag::DTHelperNotEquals(parameterized_block) => { - let new_breadcrumbs = Self::new_breadcrumbs_partial( + 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, - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)); - if left_side != right_side { + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params); + + if !param_map.contains_key("key") { + return Ok("".to_owned()); + } + let left_side = param_map + .walk("key") + .map(|ice| ice.into_context_element(self, breadcrumbs)); + let right_side = param_map + .walk("value") + .map(|ice| ice.into_context_element(self, breadcrumbs)); + // Special case: when comparing two RVPaths, if the + // path points to the same value then they are + // equal. This is particularly important for objects + // which compare memory locations rather than contents + // (javascript object equality). + if Self::new_are_paths_identical(&left_side, &right_side) + || left_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }) == right_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }) + { return self.render_maybe_body( - ¶meterized_block.contents, + ¶meterized_block.else_contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } else { return self.render_maybe_body( - ¶meterized_block.else_contents, + ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), blocks, ); } } DustTag::DTHelperGreaterThan(parameterized_block) => { - let new_breadcrumbs = Self::new_breadcrumbs_partial( + 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, - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)); + + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params); + if !param_map.contains_key("key") { + return Ok("".to_owned()); + } + let left_side = param_map + .walk("key") + .map(|ice| ice.into_context_element(self, breadcrumbs)); + let right_side = param_map + .walk("value") + .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 { + (Ok(Some(left_side_unwrapped)), Ok(Some(right_side_unwrapped))) => { + if left_side_unwrapped.get_context_element_reference() + > right_side_unwrapped.get_context_element_reference() + { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), @@ -499,35 +501,39 @@ impl<'a> DustRenderer<'a> { ); } } + _ => { + 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( + 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, - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)); + + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params); + if !param_map.contains_key("key") { + return Ok("".to_owned()); + } + let left_side = param_map + .walk("key") + .map(|ice| ice.into_context_element(self, breadcrumbs)); + let right_side = param_map + .walk("value") + .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 { + (Ok(Some(left_side_unwrapped)), Ok(Some(right_side_unwrapped))) => { + if left_side_unwrapped.get_context_element_reference() + >= right_side_unwrapped.get_context_element_reference() + { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), @@ -541,35 +547,39 @@ impl<'a> DustRenderer<'a> { ); } } + _ => { + 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( + 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, - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)); + + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params); + if !param_map.contains_key("key") { + return Ok("".to_owned()); + } + let left_side = param_map + .walk("key") + .map(|ice| ice.into_context_element(self, breadcrumbs)); + let right_side = param_map + .walk("value") + .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 { + (Ok(Some(left_side_unwrapped)), Ok(Some(right_side_unwrapped))) => { + if left_side_unwrapped.get_context_element_reference() + < right_side_unwrapped.get_context_element_reference() + { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), @@ -583,35 +593,39 @@ impl<'a> DustRenderer<'a> { ); } } + _ => { + 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( + 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, - }; - let right_side: Result<&dyn ContextElement, WalkError> = - Self::get_rval(breadcrumbs, ¶m_map, "value") - .unwrap_or(Err(WalkError::CantWalk)); + + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params); + if !param_map.contains_key("key") { + return Ok("".to_owned()); + } + let left_side = param_map + .walk("key") + .map(|ice| ice.into_context_element(self, breadcrumbs)); + let right_side = param_map + .walk("value") + .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 { + (Ok(Some(left_side_unwrapped)), Ok(Some(right_side_unwrapped))) => { + if left_side_unwrapped.get_context_element_reference() + <= right_side_unwrapped.get_context_element_reference() + { return self.render_maybe_body( ¶meterized_block.contents, new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), @@ -625,27 +639,135 @@ impl<'a> DustRenderer<'a> { ); } } + _ => { + return self.render_maybe_body( + ¶meterized_block.else_contents, + new_breadcrumbs.as_ref().unwrap_or(breadcrumbs), + blocks, + ) + } } } + _ => panic!("Unsupported tag"), } + 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 new_breadcrumbs_section<'b>( + &'b self, + breadcrumbs: &'b Vec>, + 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, + _ => (), } + + // If there is an explicit context, then drop all the current + // context + 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)) + .ok() + .flatten() + .map(|val| { + if val.get_context_element_reference().is_truthy() { + new_stack.push(std::convert::From::from(val)) + } + }); + }); + injected_context.map(|ctx| new_stack.push(BreadcrumbTreeElement::from_borrowed(ctx))); + new_context_element.map(|ctx| { + new_stack.push(BreadcrumbTreeElement::from_borrowed( + ctx.from_context_element(), + )) + }); + index_context.map(|ctx| new_stack.push(BreadcrumbTreeElement::from_borrowed(ctx))); + + Some(new_stack) } - fn are_paths_identical<'b>(param_map: &HashMap<&str, &RValue<'b>>) -> bool { - match (param_map.get("key"), param_map.get("value")) { + fn new_breadcrumbs_partial<'b>( + &'b self, + breadcrumbs: &'b Vec>, + explicit_context_breadcrumbs: &'a Vec>, + 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 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), + BreadcrumbTreeElement::from_borrowed(ctx), + ), + _ => new_stack.push(BreadcrumbTreeElement::from_borrowed(ctx)), + } + }); + + explicit_context.as_ref().map(|path| { + // TODO: should resolving the value here use + // explicit_context_maybe_breadcrumbs or + // maybe_breadcrumbs? + walk_path(explicit_context_breadcrumbs, &path.keys) + .map(|ice| ice.into_context_element(self, breadcrumbs)) + .ok() + .flatten() + .map(|val| { + if val.get_context_element_reference().is_truthy() { + new_stack.push(std::convert::From::from(val)); + } + }); + }); + + Some(new_stack) + } + + fn are_paths_identical<'b>( + param_map: &ParametersContext<'b>, + left_key: &str, + right_key: &str, + ) -> bool { + match ( + param_map.get_original_rvalue(left_key), + param_map.get_original_rvalue(right_key), + ) { (None, _) => false, (_, None) => false, (Some(key_rval), Some(value_rval)) => match (key_rval, value_rval) { @@ -657,24 +779,23 @@ impl<'a> DustRenderer<'a> { } } - 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 ContextElement>, - 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::RVPath(path) => Some(walk_path(breadcrumbs, &path.keys)), - }, + fn new_are_paths_identical<'b>( + left_side: &Result>, WalkError>, + right_side: &Result>, WalkError>, + ) -> bool { + let left_resolved = left_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }); + let right_resolved = right_side.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + }); + match (left_resolved, right_resolved) { + (Ok(Some(lce)), Ok(Some(rce))) => lce as *const _ == rce as *const _, + _ => false, } } @@ -693,303 +814,17 @@ impl<'a> DustRenderer<'a> { final_filters } - fn new_breadcrumbs_section<'b>( - breadcrumbs: &'b Vec<&'b dyn ContextElement>, - index_context: Option<&'b dyn ContextElement>, - injected_context: Option<&'b dyn ContextElement>, - 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(|val| { - if val.is_truthy() { - new_stack.push(val) - } - }); - }); - injected_context.map(|ctx| new_stack.push(ctx)); - new_context_element.map(|ctx| new_stack.push(ctx)); - index_context.map(|ctx| new_stack.push(ctx)); - Some(new_stack) - } - - fn new_breadcrumbs_partial<'b>( - breadcrumbs: &'b Vec<&'b dyn ContextElement>, - explicit_context_breadcrumbs: &'b Vec<&'b dyn ContextElement>, - injected_context: Option<&'b dyn ContextElement>, - 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).map(|val| { - if val.is_truthy() { - new_stack.push(val) - } - }); - }); - 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()) + fn get_index_of_first_non_pseudo_element<'b>( + breadcrumbs: &'b Vec>, + ) -> Option { + breadcrumbs.iter().rposition(|b| { + !std::borrow::Borrow::::borrow(b).is_pseudo_element() + }) } } struct BlockContext<'a> { /// The breadcrumbs at the time of entering the current partial - breadcrumbs: &'a Vec<&'a dyn ContextElement>, + breadcrumbs: &'a Vec>, 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/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_walking.rs b/src/renderer/tree_walking.rs new file mode 100644 index 0000000..023c8b7 --- /dev/null +++ b/src/renderer/tree_walking.rs @@ -0,0 +1,94 @@ +use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; +use crate::renderer::context_element::IntoContextElement; +use crate::renderer::WalkError; +use std::borrow::Borrow; + +enum WalkResult<'a> { + NoWalk, + PartialWalk, + FullyWalked(&'a dyn IntoContextElement), +} + +fn walk_path_from_single_level<'a, P>( + context: &'a dyn IntoContextElement, + path: &[P], +) -> WalkResult<'a> +where + P: Borrow, +{ + if path.is_empty() { + return WalkResult::FullyWalked(context); + } + + let mut walk_failure = WalkResult::NoWalk; + let mut output = context; + for elem in path.iter() { + match output.walk(elem.borrow()) { + Err(WalkError::CantWalk { .. }) => { + return walk_failure; + } + Ok(new_val) => { + walk_failure = WalkResult::PartialWalk; + output = new_val; + } + } + } + + WalkResult::FullyWalked(output) +} + +fn get_first_non_pseudo_element<'a>( + breadcrumbs: &'a Vec>, +) -> Option<&'a BreadcrumbTreeElement<'a>> { + breadcrumbs + .iter() + .rev() + .filter(|b| { + !std::borrow::Borrow::::borrow(*b).is_pseudo_element() + }) + .next() +} + +pub fn walk_path<'a, P>( + breadcrumbs: &'a Vec>, + path: &Vec

, +) -> Result<&'a dyn IntoContextElement, WalkError> +where + P: Borrow, +{ + match (breadcrumbs.last(), path.first()) { + (None, _) => return Err(WalkError::CantWalk), + (Some(last_elem), None) => return Ok(last_elem.borrow()), + (Some(_), Some(path_first)) if path_first.borrow() == "." => { + let first_non_pseudo_element = get_first_non_pseudo_element(breadcrumbs); + return match first_non_pseudo_element { + None => Err(WalkError::CantWalk), + Some(current_context) => { + match walk_path_from_single_level(current_context.borrow(), &path[1..]) { + // If no walking was done at all or we partially walked + // then stop trying to find anything because '.' restricts + // us to the current scope + WalkResult::NoWalk | WalkResult::PartialWalk => Err(WalkError::CantWalk), + WalkResult::FullyWalked(new_context) => Ok(new_context), + } + } + }; + } + (Some(_), Some(path_first)) => { + for context in breadcrumbs.iter().rev() { + match walk_path_from_single_level(context.borrow(), 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(WalkError::CantWalk); + } + WalkResult::FullyWalked(new_context) => return Ok(new_context), + } + } + } + } + + Err(WalkError::CantWalk) +} diff --git a/src/renderer/walking.rs b/src/renderer/walking.rs index c0c2365..d56956c 100644 --- a/src/renderer/walking.rs +++ b/src/renderer/walking.rs @@ -1,18 +1,18 @@ use crate::renderer::context_element::ContextElement; -use crate::renderer::context_element::Walkable; +use crate::renderer::context_element::IntoContextElement; use crate::renderer::WalkError; use std::borrow::Borrow; enum WalkResult<'a> { NoWalk, PartialWalk, - FullyWalked(&'a dyn ContextElement), + FullyWalked(&'a dyn IntoContextElement), } fn walk_path_from_single_level<'a, P, C>(context: &'a C, path: &[P]) -> WalkResult<'a> where P: Borrow, - C: Borrow, + C: Borrow, { if path.is_empty() { return WalkResult::FullyWalked(context.borrow()); @@ -37,7 +37,7 @@ where pub fn get_first_non_pseudo_element<'a, B>(breadcrumbs: &'a Vec) -> Option<&B> where - B: Borrow, + B: Borrow, { breadcrumbs .iter() @@ -49,9 +49,9 @@ where pub fn walk_path<'a, B, P>( breadcrumbs: &'a Vec, path: &Vec

, -) -> Result<&'a dyn ContextElement, WalkError> +) -> Result<&'a dyn IntoContextElement, WalkError> where - B: Borrow, + B: Borrow, P: Borrow, { if breadcrumbs.is_empty() {