diff --git a/js/test_cases/helpers_size/README.md b/js/test_cases/helpers_size/README.md new file mode 100644 index 0000000..76b797a --- /dev/null +++ b/js/test_cases/helpers_size/README.md @@ -0,0 +1,18 @@ +Excerpt from [the DustJS tutorial](https://github.com/linkedin/dustjs/wiki/Dust-Tutorial#size_keyxxx___size_helper_Available_in_Dust_V11_release): + + Array - number of elements, [1,2,3,4] has size=4 + String - length of the string, "abcdef" has size=6 + Object - Number of properties in the object, {a:4, b:8, c:15, d:16} has size=4 + Number - Value of the number, 23 has size 23 and 3.14 has size 3.14 + Undefined, 0, empty string - zero + Any other value - length after conversion to string + +This appears to be inaccurate (probably out of date because that tutorial was deprecated in favor of [dustjs.com](https://www.dustjs.com/), but the latter is incomplete and lacking any reference to the `@size` helper. + +Corrections +----------- +- Booleans are 0, not converted to strings + +Oddities +-------- +Reference parameters (like `foo="{bar}"`) are usually treated as strings but it seems if it contains ONLY a reference to a value and not anything else, then it is still treated as a number. diff --git a/js/test_cases/helpers_size/inputarray.json b/js/test_cases/helpers_size/inputarray.json new file mode 100644 index 0000000..98f6406 --- /dev/null +++ b/js/test_cases/helpers_size/inputarray.json @@ -0,0 +1,7 @@ +{ + "val": [ + "Alice", + "Bob", + "Chris" + ] +} diff --git a/js/test_cases/helpers_size/inputemptystring.json b/js/test_cases/helpers_size/inputemptystring.json new file mode 100644 index 0000000..f911fb5 --- /dev/null +++ b/js/test_cases/helpers_size/inputemptystring.json @@ -0,0 +1,3 @@ +{ + "val": "" +} diff --git a/js/test_cases/helpers_size/inputfalse.json b/js/test_cases/helpers_size/inputfalse.json new file mode 100644 index 0000000..9cc7fda --- /dev/null +++ b/js/test_cases/helpers_size/inputfalse.json @@ -0,0 +1,3 @@ +{ + "val": false +} diff --git a/js/test_cases/helpers_size/inputmissing.json b/js/test_cases/helpers_size/inputmissing.json new file mode 100644 index 0000000..95cf907 --- /dev/null +++ b/js/test_cases/helpers_size/inputmissing.json @@ -0,0 +1,3 @@ +{ + "notval": "Alice" +} diff --git a/js/test_cases/helpers_size/inputnumber.json b/js/test_cases/helpers_size/inputnumber.json new file mode 100644 index 0000000..378b4a9 --- /dev/null +++ b/js/test_cases/helpers_size/inputnumber.json @@ -0,0 +1,3 @@ +{ + "val": 7.21 +} diff --git a/js/test_cases/helpers_size/inputobject.json b/js/test_cases/helpers_size/inputobject.json new file mode 100644 index 0000000..e511117 --- /dev/null +++ b/js/test_cases/helpers_size/inputobject.json @@ -0,0 +1,6 @@ +{ + "val": { + "name": "fluffy", + "pet": "cat" + } +} diff --git a/js/test_cases/helpers_size/inputstring.json b/js/test_cases/helpers_size/inputstring.json new file mode 100644 index 0000000..fea025d --- /dev/null +++ b/js/test_cases/helpers_size/inputstring.json @@ -0,0 +1,3 @@ +{ + "val": "7.99" +} diff --git a/js/test_cases/helpers_size/inputtrue.json b/js/test_cases/helpers_size/inputtrue.json new file mode 100644 index 0000000..25d6b1f --- /dev/null +++ b/js/test_cases/helpers_size/inputtrue.json @@ -0,0 +1,3 @@ +{ + "val": true +} diff --git a/js/test_cases/helpers_size/inputzero.json b/js/test_cases/helpers_size/inputzero.json new file mode 100644 index 0000000..d6110e8 --- /dev/null +++ b/js/test_cases/helpers_size/inputzero.json @@ -0,0 +1,3 @@ +{ + "val": 0 +} diff --git a/js/test_cases/helpers_size/main.dust b/js/test_cases/helpers_size/main.dust new file mode 100644 index 0000000..52b398c --- /dev/null +++ b/js/test_cases/helpers_size/main.dust @@ -0,0 +1,10 @@ +The size of val ({val|js|s}) is {@size key=val /}{~n} +The size of "{~lb}val{~rb}" is {@size key="{val}" /}{~n} +{#val foo="{val}"} + The size of foo ({foo|js|s}) is {@size key=foo /}{~n} + The size of "{~lb}foo{~rb}" is {@size key="{foo}" /}{~n} +{:else} + The size of foo ({foo|js|s}) is {@size key=foo /}{~n} + The size of "{~lb}foo{~rb}" is {@size key="{foo}" /}{~n} +{/val} +The size with no key is {@size /}{~n} diff --git a/src/bin.rs b/src/bin.rs index 3c4c8c2..d5e89d8 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -15,6 +15,7 @@ use renderer::Loopable; use renderer::MathNumber; use renderer::RenderError; use renderer::Renderable; +use renderer::Sizable; use renderer::Truthiness; use renderer::WalkError; use renderer::Walkable; @@ -313,6 +314,29 @@ impl Loopable for serde_json::Value { } } +impl Sizable for serde_json::Value { + fn get_size<'a>(&'a self) -> Option> { + match self { + serde_json::Value::Null => { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) + } + serde_json::Value::Bool(_boolean) => { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) + } + serde_json::Value::Number(_num) => Some(IceResult::from_borrowed(self)), + serde_json::Value::String(text) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(text.len().try_into().unwrap()), + )), + serde_json::Value::Array(arr) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(arr.len().try_into().unwrap()), + )), + serde_json::Value::Object(obj) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(obj.len().try_into().unwrap()), + )), + } + } +} + impl Castable for serde_json::Value { fn cast_to_type<'a>(&'a self, target: &str) -> Option> { match (self, target) { @@ -329,6 +353,10 @@ impl Castable for serde_json::Value { }) .ok(), (serde_json::Value::Number(_), "number") => Some(IceResult::from_borrowed(self)), + (serde_json::Value::Null, "number") => None, + (serde_json::Value::Bool(_), "number") => None, + (serde_json::Value::Array(_), "number") => None, + (serde_json::Value::Object(_), "number") => None, (_, _) => panic!("Unimplemented cast"), } } diff --git a/src/parser/parser.rs b/src/parser/parser.rs index 88e09f2..c25ba08 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -48,6 +48,8 @@ pub enum DustTag<'a> { DTHelperAny(ParameterizedBlock<'a>), DTHelperNone(ParameterizedBlock<'a>), DTHelperMath(ParameterizedBlock<'a>), + DTHelperSize(ParameterizedBlock<'a>), + DTHelperContextDump(ParameterizedBlock<'a>), } #[derive(Clone, Debug, PartialEq)] @@ -286,6 +288,14 @@ fn dust_tag_helper(i: &str) -> IResult<&str, DustTag> { parameterized_block("{@", &tag_to_path("math")), DustTag::DTHelperMath, ), + map( + parameterized_block("{@", &tag_to_path("size")), + DustTag::DTHelperSize, + ), + map( + parameterized_block("{@", &tag_to_path("contextDump")), + DustTag::DTHelperContextDump, + ), ))(i) } @@ -348,7 +358,10 @@ fn key_to_path<'a>(i: &'a str) -> IResult<&str, Path<'a>> { fn postitive_integer_literal(i: &str) -> IResult<&str, u64> { map( verify( - map(digit1, |number_string: &str| number_string.parse::()), + map( + recognize(tuple((opt(tag("+")), digit1))), + |number_string: &str| number_string.parse::(), + ), |parse_result| parse_result.is_ok(), ), |parsed_number| parsed_number.unwrap(), @@ -424,7 +437,11 @@ fn key_value_pair(i: &str) -> IResult<&str, KVPair> { /// Display a value from the context fn reference(i: &str) -> IResult<&str, Reference> { - let (remaining, (p, filters)) = delimited(tag("{"), tuple((path, many0(filter))), tag("}"))(i)?; + let (remaining, (p, filters)) = delimited( + tag("{"), + tuple((path, many0(filter))), + preceded(space0, tag("}")), + )(i)?; Ok(( remaining, Reference { @@ -474,12 +491,8 @@ where preceded(tag(open_matcher), name_matcher), opt(preceded(tag(":"), path)), terminated( - opt(delimited( - space1, - separated_list1(space1, key_value_pair), - space0, - )), - tag("}"), + opt(preceded(space1, separated_list1(space1, key_value_pair))), + preceded(space0, tag("}")), ), opt(body), opt(preceded(tag("{:else}"), opt(body))), @@ -515,12 +528,8 @@ where preceded(tag(open_matcher), name_matcher), opt(preceded(tag(":"), path)), terminated( - opt(delimited( - space1, - separated_list1(space1, key_value_pair), - space0, - )), - tag("}"), + opt(preceded(space1, separated_list1(space1, key_value_pair))), + preceded(space0, tag("}")), ), opt(body), delimited(tag("{/"), name_matcher, tag("}")), @@ -554,13 +563,9 @@ where tuple(( name_matcher, opt(preceded(tag(":"), path)), - opt(delimited( - space1, - separated_list1(space1, key_value_pair), - space0, - )), + opt(preceded(space1, separated_list1(space1, key_value_pair))), )), - tag("/}"), + preceded(space0, tag("/}")), )(i)?; Ok(( @@ -585,13 +590,9 @@ fn partial_with_plain_tag<'a>( tuple(( key, opt(preceded(tag(":"), path)), - opt(delimited( - space1, - separated_list1(space1, key_value_pair), - space0, - )), + opt(preceded(space1, separated_list1(space1, key_value_pair))), )), - tag("/}"), + preceded(space0, tag("/}")), )(i)?; Ok(( @@ -623,13 +624,9 @@ fn partial_with_quoted_tag<'a>( tuple(( template_string_rvalue, opt(preceded(tag(":"), path)), - opt(delimited( - space1, - separated_list1(space1, key_value_pair), - space0, - )), + opt(preceded(space1, separated_list1(space1, key_value_pair))), )), - tag("/}"), + preceded(space0, tag("/}")), )(i)?; Ok(( diff --git a/src/renderer/context_element.rs b/src/renderer/context_element.rs index 56d194c..01281d3 100644 --- a/src/renderer/context_element.rs +++ b/src/renderer/context_element.rs @@ -22,6 +22,7 @@ pub trait ContextElement: + FromContextElement + IntoRcIce + Castable + + Sizable { } @@ -63,6 +64,10 @@ pub trait Castable { fn cast_to_type<'a>(&'a self, target: &str) -> Option>; } +pub trait Sizable { + fn get_size<'a>(&'a self) -> Option>; +} + pub trait CastToAny { fn to_any(&self) -> &dyn Any; } diff --git a/src/renderer/inline_partial_tree.rs b/src/renderer/inline_partial_tree.rs index f7cbfdf..fffe0a8 100644 --- a/src/renderer/inline_partial_tree.rs +++ b/src/renderer/inline_partial_tree.rs @@ -112,7 +112,9 @@ fn extract_inline_partials_from_tag<'a, 'b>( | DustTag::DTHelperSelect(parameterized_block) | DustTag::DTHelperAny(parameterized_block) | DustTag::DTHelperNone(parameterized_block) - | DustTag::DTHelperMath(parameterized_block) => { + | DustTag::DTHelperMath(parameterized_block) + | DustTag::DTHelperSize(parameterized_block) + | DustTag::DTHelperContextDump(parameterized_block) => { match ¶meterized_block.contents { None => (), Some(body) => extract_inline_partials_from_body(blocks, &body), diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 571a02b..9b1f5b7 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -18,6 +18,7 @@ pub use context_element::IceResult; pub use context_element::IntoContextElement; pub use context_element::Loopable; pub use context_element::Renderable; +pub use context_element::Sizable; pub use context_element::Truthiness; pub use context_element::Walkable; pub use errors::CompileError; diff --git a/src/renderer/parameters_context.rs b/src/renderer/parameters_context.rs index a886586..94a8e13 100644 --- a/src/renderer/parameters_context.rs +++ b/src/renderer/parameters_context.rs @@ -1,6 +1,7 @@ use crate::parser::Filter; use crate::parser::KVPair; use crate::parser::OwnedLiteral; +use crate::parser::PartialNameElement; use crate::parser::RValue; use crate::renderer::breadcrumb_tree::BreadcrumbTreeElement; use crate::renderer::context_element::CompareContextElement; @@ -14,12 +15,14 @@ use crate::renderer::DustRenderer; use crate::renderer::Loopable; use crate::renderer::RenderError; use crate::renderer::Renderable; +use crate::renderer::Sizable; use crate::renderer::Truthiness; use crate::renderer::WalkError; use crate::renderer::Walkable; use std::borrow::Borrow; use std::cmp::Ordering; use std::collections::HashMap; +use std::convert::TryInto; #[derive(Debug)] pub struct ParametersContext<'a> { @@ -189,6 +192,19 @@ impl Walkable for OwnedLiteral { } } +impl Sizable for OwnedLiteral { + fn get_size<'a>(&'a self) -> Option> { + match self { + OwnedLiteral::LFloat(_) => Some(IceResult::from_borrowed(self)), + OwnedLiteral::LPositiveInteger(_) => Some(IceResult::from_borrowed(self)), + OwnedLiteral::LNegativeInteger(_) => Some(IceResult::from_borrowed(self)), + OwnedLiteral::LString(text) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(text.len().try_into().unwrap()), + )), + } + } +} + impl Castable for OwnedLiteral { fn cast_to_type<'a>(&'a self, target: &str) -> Option> { match (self, target) { diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index cf48524..561d224 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -722,6 +722,85 @@ impl<'a> DustRenderer<'a> { } } } + DustTag::DTHelperSize(parameterized_block) => { + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params, None); + let value = self.tap(breadcrumbs, ¶m_map, "key"); + let value_ce = value.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + // .map(|ce| ce.get_size()) + }); + match value_ce { + // If "key" is not on the @size tag at all, render 0. + None => return Ok("0".to_owned()), + // If the key value could not be found in the context, render 0. + Some(Err(_)) => return Ok("0".to_owned()), + Some(Ok(ce)) => { + // The @size helper attempts to cast values to + // numbers, and if that succeeds it uses the + // number, otherwise we'll get the size of the + // original type. + match ce.cast_to_type("number") { + Some(ice) => { + return ice + .get_context_element_reference() + .get_size() + .map(|ce_size| { + ce_size.get_context_element_reference().render(&Vec::new()) + }) + .unwrap_or(Ok("".to_owned())) + } + None => { + return ce + .get_size() + .map(|ce_size| { + ce_size.get_context_element_reference().render(&Vec::new()) + }) + .unwrap_or(Ok("".to_owned())) + } + } + } + } + } + DustTag::DTHelperContextDump(parameterized_block) => { + let param_map = + ParametersContext::new(self, breadcrumbs, ¶meterized_block.params, None); + let value = self.tap(breadcrumbs, ¶m_map, "key"); + let destination = self.tap(breadcrumbs, ¶m_map, "to"); + let value_rendered = match value.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + .map(|ce| ce.render(&Vec::new())) + }) { + Some(Ok(Ok(val))) => val, + _ => "current".to_owned(), + }; + let destination_rendered = match destination.as_ref().map(|maybe_ice| { + maybe_ice + .as_ref() + .map(|ice| ice.get_context_element_reference()) + .map(|ce| ce.render(&Vec::new())) + }) { + Some(Ok(Ok(val))) => val, + _ => "output".to_owned(), + }; + match ( + value_rendered.as_str(), + destination_rendered.as_str(), + breadcrumbs.last(), + ) { + ("current", "output", None) => return Ok("{}".to_owned()), + ("current", "console", None) => println!("{{}}"), + ("current", "output", Some(bc)) => return Ok(format!("{:?}", bc)), + ("current", "console", Some(bc)) => println!("{:?}", bc), + ("full", "output", _) => return Ok(format!("{:?}", breadcrumbs)), + ("full", "console", _) => println!("{:?}", breadcrumbs), + _ => panic!("Unhandled contextDump parameters."), + } + } } Ok("".to_owned())