diff --git a/js/run_compliance_suite.bash b/js/run_compliance_suite.bash index c7d5296..48324d7 100755 --- a/js/run_compliance_suite.bash +++ b/js/run_compliance_suite.bash @@ -27,27 +27,17 @@ EOF done while read -r test_group; do - test_group_name=$(basename "$test_group") - while read -r test_case; do - test_case_file_name=$(basename "$test_case") - test_case_name=${test_case_file_name%.*} - set +e - ( - if cmp -s <(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) node "$DIR/dustjs_shim.js" < "$test_case") <(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) "$DIR/../target/debug/duster-cli" < "$test_case"); then - echo "$test_group_name::$test_case_name PASSED" - else - echo "$test_group_name::$test_case_name FAILED" - if [ $show_diff -eq 1 ]; then - diff --label "dustjs-linkedin" --label "duster" <(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) node "$DIR/dustjs_shim.js" < "$test_case" 2>/dev/null ) <(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) "$DIR/../target/debug/duster-cli" < "$test_case" 2>/dev/null ) - fi - exit 1 - fi - ) - if [ $? -ne 0 ]; then - failed_count=$((failed_count + 1)) - fi - set -e - done <<<"$(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.json' | sort)" + set +e + if [ $show_diff -eq 1 ]; then + "$DIR/run_single_test.bash" --show-diff "$test_group" + else + "$DIR/run_single_test.bash" --show-diff "$test_group" + fi + result=$? + if [ $result -ne 0 ]; then + failed_count=$((failed_count + result)) + fi + set -e done <<<"$(find "$DIR/test_cases" -maxdepth 1 -mindepth 1 -type d ! -name '_*' | sort)" ignored_count=$(find "$DIR/test_cases" -maxdepth 1 -mindepth 1 -type d -name '_*' | wc -l) diff --git a/js/run_single_test.bash b/js/run_single_test.bash new file mode 100755 index 0000000..d3394e9 --- /dev/null +++ b/js/run_single_test.bash @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# Runs a single test against LinkedIn DustJS and duster to compare the result +set -euo pipefail +IFS=$'\n\t' +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +test_group="" +test_mode="" + +function show_help { + cat< + +Options: + --show-diff Shows the difference between the two dust implementations + --dustjs Print the output of dustjs instead of comparing + --duster Print the output of duster instead of comparing +EOF +} + +while (( "$#" )); do + if [ "$1" = "--help" ]; then + show_help + exit 0 + elif [ "$1" = "--show-diff" ]; then + show_diff=1 + elif [ "$1" = "--dustjs" ]; then + test_mode="dustjs" + elif [ "$1" = "--duster" ]; then + test_mode="duster" + elif [ ! "$1" = -* ]; then + test_group="$1" + else + (>&2 echo "Unrecognized option: $1") + exit 1 + fi + shift +done + +# Assert a test group was specified +if [ "$test_group" = "" ]; then + show_help + exit 1 +fi + +failed_count=0 +test_group_name=$(basename "$test_group") +while read -r test_case; do + test_case_file_name=$(basename "$test_case") + test_case_name=${test_case_file_name%.*} + set +e + if [ "$test_mode" = "dustjs" ] || [ "$test_mode" = "" ]; then + dustjs_output=$(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) node "$DIR/dustjs_shim.js" < "$test_case") + fi + if [ "$test_mode" = "duster" ] || [ "$test_mode" = "" ]; then + duster_output=$(xargs -a <(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name 'main.dust'; find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.dust' ! -name 'main.dust' | sort) "$DIR/../target/debug/duster-cli" < "$test_case") + fi + + if [ "$test_mode" = "dustjs" ]; then + cat <<<"$dustjs_output" + elif [ "$test_mode" = "duster" ]; then + cat <<<"$duster_output" + else + ( + if cmp -s <(cat <<<"$dustjs_output") <(cat <<<"$duster_output"); then + echo "$test_group_name::$test_case_name PASSED" + else + echo "$test_group_name::$test_case_name FAILED" + if [ $show_diff -eq 1 ]; then + diff --label "dustjs-linkedin" --label "duster" <(cat <<<"$dustjs_output") <(cat <<<"$duster_output") + fi + exit 1 + fi + ) + if [ $? -ne 0 ]; then + failed_count=$((failed_count + 1)) + fi + fi + set -e +done <<<"$(find "$test_group" -maxdepth 1 -mindepth 1 -type f -name '*.json' | sort)" + +exit "$failed_count" diff --git a/js/test_cases/dynamic_partial/input1.json b/js/test_cases/dynamic_partial/input1.json new file mode 100644 index 0000000..e696696 --- /dev/null +++ b/js/test_cases/dynamic_partial/input1.json @@ -0,0 +1,3 @@ +{ + "name": "beta" +} diff --git a/js/test_cases/dynamic_partial/main.dust b/js/test_cases/dynamic_partial/main.dust new file mode 100644 index 0000000..b4f3ce7 --- /dev/null +++ b/js/test_cases/dynamic_partial/main.dust @@ -0,0 +1,2 @@ +Testing dynamic partials{~n} +{>"tmpl{name}"/} diff --git a/js/test_cases/dynamic_partial/tmplalpha.dust b/js/test_cases/dynamic_partial/tmplalpha.dust new file mode 100644 index 0000000..4cfa36d --- /dev/null +++ b/js/test_cases/dynamic_partial/tmplalpha.dust @@ -0,0 +1 @@ +beta template{~n} diff --git a/js/test_cases/dynamic_partial/tmplbeta.dust b/js/test_cases/dynamic_partial/tmplbeta.dust new file mode 100644 index 0000000..4cfa36d --- /dev/null +++ b/js/test_cases/dynamic_partial/tmplbeta.dust @@ -0,0 +1 @@ +beta template{~n} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 58e38e3..30184b2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8,6 +8,7 @@ pub use parser::DustTag; pub use parser::Filter; pub use parser::KVPair; pub use parser::OwnedLiteral; +pub use parser::PartialNameElement; pub use parser::RValue; pub use parser::Special; pub use parser::Template; diff --git a/src/parser/parser.rs b/src/parser/parser.rs index a1ed027..4c0cfd2 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -114,7 +114,7 @@ pub struct ParameterizedBlock<'a> { #[derive(Clone, Debug, PartialEq)] pub struct Partial<'a> { - pub name: String, + pub name: Vec, pub params: Vec>, } @@ -136,6 +136,17 @@ pub struct KVPair<'a> { pub value: RValue<'a>, } +#[derive(Clone, Debug, PartialEq)] +pub enum PartialNameElement { + PNSpan { + contents: String, + }, + PNReference { + path: Vec, + filters: Vec, + }, +} + #[derive(Clone, Debug, PartialEq)] pub struct Body<'a> { pub elements: Vec>, @@ -153,6 +164,46 @@ pub enum TemplateElement<'a> { TEIgnoredWhitespace(IgnoredWhitespace<'a>), } +impl From> for PartialNameElement { + fn from(original: TemplateElement) -> Self { + match original { + TemplateElement::TESpan(span) => PartialNameElement::PNSpan { + contents: span.contents.to_owned(), + }, + TemplateElement::TETag(DustTag::DTReference(reference)) => { + PartialNameElement::PNReference { + path: reference + .path + .keys + .into_iter() + .map(|s| s.to_owned()) + .collect(), + filters: reference.filters, + } + } + _ => panic!("Only spans and references can be used in partial names."), + } + } +} + +impl<'a> From<&'a PartialNameElement> for TemplateElement<'a> { + fn from(original: &'a PartialNameElement) -> Self { + match original { + PartialNameElement::PNSpan { contents } => { + TemplateElement::TESpan(Span { contents: contents }) + } + PartialNameElement::PNReference { path, filters } => { + TemplateElement::TETag(DustTag::DTReference(Reference { + path: Path { + keys: path.into_iter().map(|s| s.as_str()).collect(), + }, + filters: filters.into_iter().map(|f| f.clone()).collect(), + })) + } + } + } +} + /// Any element significant to dust that isn't plain text /// /// These elements are always wrapped in curly braces @@ -467,18 +518,14 @@ where } } -fn partial<'a, F>( +fn partial_with_plain_tag<'a>( open_matcher: &'static str, - constructor: F, -) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> -where - F: Fn(Partial<'a>) -> DustTag<'a>, -{ +) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> { move |i: &'a str| { let (i, (name, params)) = delimited( tag(open_matcher), tuple(( - alt((map(key, String::from), quoted_string)), + key, opt(delimited( space1, separated_list1(space1, key_value_pair), @@ -490,14 +537,71 @@ where Ok(( i, - constructor(Partial { - name: name, + Partial { + name: vec![PartialNameElement::PNSpan { + contents: name.to_owned(), + }], params: params.unwrap_or(Vec::new()), - }), + }, )) } } +fn partial_quoted_tag(i: &str) -> IResult<&str, Vec> { + all_consuming(many1(alt(( + map(span, TemplateElement::TESpan), + map(map(reference, DustTag::DTReference), TemplateElement::TETag), + ))))(i) +} + +fn partial_with_quoted_tag<'a>( + open_matcher: &'static str, +) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> { + move |i: &'a str| { + let (i, (name, params)) = delimited( + tag(open_matcher), + tuple(( + verify(quoted_string, |s: &String| { + partial_quoted_tag(s.as_str()).is_ok() + }), + opt(delimited( + space1, + separated_list1(space1, key_value_pair), + space0, + )), + )), + 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, + params: params.unwrap_or(Vec::new()), + }, + )) + } +} + +fn partial<'a, F>( + open_matcher: &'static str, + constructor: F, +) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> +where + F: Fn(Partial<'a>) -> DustTag<'a>, +{ + let plain = partial_with_plain_tag(open_matcher); + let quoted = partial_with_quoted_tag(open_matcher); + move |i: &'a str| map(alt((&plain, "ed)), &constructor)(i) +} + fn filter(i: &str) -> IResult<&str, Filter> { preceded( tag("|"), @@ -912,7 +1016,10 @@ mod tests { Ok(( "", DustTag::DTPartial(Partial { - name: "foo".to_owned(), + name: vec![PartialNameElement::PNSpan { + contents: "foo".to_owned() + },], + params: vec![ KVPair { key: "bar", @@ -935,7 +1042,43 @@ mod tests { Ok(( "", DustTag::DTPartial(Partial { - name: r#"template name * with * special " characters"#.to_owned(), + name: vec![PartialNameElement::PNSpan { + contents: r#"template name * with * special " characters"#.to_owned() + },], + params: vec![ + KVPair { + key: "bar", + value: RValue::RVPath(Path { keys: vec!["baz"] }) + }, + KVPair { + key: "animal", + value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned())) + } + ] + }) + )) + ); + } + + #[test] + fn test_dynamic_partial() { + assert_eq!( + dust_tag(r#"{>"dynamic{ref}template" bar=baz animal="cat"/}"#), + Ok(( + "", + DustTag::DTPartial(Partial { + name: vec![ + PartialNameElement::PNSpan { + contents: "dynamic".to_owned() + }, + PartialNameElement::PNReference { + path: vec!["ref".to_owned()], + filters: Vec::new() + }, + PartialNameElement::PNSpan { + contents: "template".to_owned() + } + ], params: vec![ KVPair { key: "bar", @@ -958,7 +1101,9 @@ mod tests { Ok(( "", DustTag::DTPartial(Partial { - name: "foo".to_owned(), + name: vec![PartialNameElement::PNSpan { + contents: "foo".to_owned() + },], params: vec![ KVPair { key: "a", @@ -1127,7 +1272,9 @@ mod tests { contents: Some(Body { elements: vec![TemplateElement::TETag(DustTag::DTPartial( Partial { - name: "partialtwo".to_owned(), + name: vec![PartialNameElement::PNSpan { + contents: "partialtwo".to_owned() + },], params: vec![ KVPair { key: "v1", diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs index bf0b083..1546e12 100644 --- a/src/renderer/renderer.rs +++ b/src/renderer/renderer.rs @@ -2,6 +2,7 @@ use crate::parser::template; use crate::parser::Body; use crate::parser::DustTag; use crate::parser::KVPair; +use crate::parser::PartialNameElement; use crate::parser::RValue; use crate::parser::Special; use crate::parser::Template; @@ -108,6 +109,23 @@ impl<'a> DustRenderer<'a> { Ok(output) } + fn render_partial_name( + &'a self, + body: &'a Vec, + breadcrumbs: &Vec<&'a dyn ContextElement>, + blocks: &'a InlinePartialTreeElement<'a>, + ) -> Result { + let converted_to_template_elements: Vec> = + body.into_iter().map(|e| e.into()).collect(); + self.render_body( + &Body { + elements: converted_to_template_elements, + }, + breadcrumbs, + blocks, + ) + } + fn render_tag( &'a self, tag: &'a DustTag, @@ -186,16 +204,17 @@ impl<'a> DustRenderer<'a> { }; } DustTag::DTPartial(partial) => { + let partial_name = self.render_partial_name(&partial.name, breadcrumbs, blocks)?; if partial.params.is_empty() { let rendered_content = - self.render_template(&partial.name, breadcrumbs, Some(blocks))?; + self.render_template(&partial_name, breadcrumbs, Some(blocks))?; return Ok(rendered_content); } else { let injected_context = ParametersContext::new(breadcrumbs, &partial.params); let mut new_breadcrumbs = breadcrumbs.clone(); new_breadcrumbs.insert(new_breadcrumbs.len() - 1, &injected_context); let rendered_content = - self.render_template(&partial.name, &new_breadcrumbs, Some(blocks))?; + self.render_template(&partial_name, &new_breadcrumbs, Some(blocks))?; return Ok(rendered_content); } } @@ -431,7 +450,6 @@ impl<'a> DustRenderer<'a> { } } } - _ => (), // TODO: Implement the rest } Ok("".to_owned()) }