use super::take_until_parser_matches::take_until_parser_matches; use nom::branch::alt; use nom::bytes::complete::escaped_transform; use nom::bytes::complete::is_a; use nom::bytes::complete::is_not; use nom::bytes::complete::{tag, take_until}; use nom::character::complete::line_ending; use nom::character::complete::multispace0; use nom::character::complete::one_of; use nom::character::complete::{digit1, space0, space1}; use nom::combinator::all_consuming; use nom::combinator::map; use nom::combinator::opt; use nom::combinator::recognize; use nom::combinator::value; use nom::combinator::verify; use nom::multi::many0; use nom::multi::many1; use nom::multi::separated_list1; use nom::sequence::delimited; use nom::sequence::preceded; use nom::sequence::separated_pair; use nom::sequence::terminated; use nom::sequence::tuple; use nom::IResult; #[derive(Debug, PartialEq)] pub enum DustTag<'a> { DTSpecial(Special), DTComment(Comment<'a>), DTLiteralStringBlock(&'a str), DTReference(Reference<'a>), DTSection(ParameterizedBlock<'a>), DTExists(ParameterizedBlock<'a>), DTNotExists(ParameterizedBlock<'a>), DTBlock(ParameterizedBlock<'a>), DTInlinePartial(ParameterizedBlock<'a>), DTPartial(Partial<'a>), DTHelperEquals(ParameterizedBlock<'a>), DTHelperNotEquals(ParameterizedBlock<'a>), DTHelperGreaterThan(ParameterizedBlock<'a>), DTHelperLessThan(ParameterizedBlock<'a>), DTHelperGreaterThanOrEquals(ParameterizedBlock<'a>), DTHelperLessThanOrEquals(ParameterizedBlock<'a>), DTHelperSep(ParameterizedBlock<'a>), DTHelperFirst(ParameterizedBlock<'a>), DTHelperLast(ParameterizedBlock<'a>), DTHelperSelect(ParameterizedBlock<'a>), DTHelperAny(ParameterizedBlock<'a>), DTHelperNone(ParameterizedBlock<'a>), DTHelperMath(ParameterizedBlock<'a>), DTHelperSize(ParameterizedBlock<'a>), DTHelperContextDump(ParameterizedBlock<'a>), } #[derive(Clone, Debug, PartialEq)] pub enum Special { Space, NewLine, CarriageReturn, LeftCurlyBrace, RightCurlyBrace, } #[derive(Debug, PartialEq)] pub enum IgnoredWhitespace<'a> { StartOfLine(&'a str), } #[derive(Debug, PartialEq)] pub struct Comment<'a> { value: &'a str, } /// 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(Debug, PartialEq)] pub struct Path<'a> { pub keys: Vec<&'a str>, } #[derive(Debug, PartialEq)] pub struct Reference<'a> { pub path: Path<'a>, pub filters: Vec, } #[derive(Clone, Debug, PartialEq)] pub enum Filter { HtmlEncode, DisableHtmlEncode, JavascriptStringEncode, EncodeUri, EncodeUriComponent, JsonStringify, JsonParse, } #[derive(Debug, PartialEq)] pub struct Span<'a> { pub contents: &'a str, } #[derive(Debug, PartialEq)] pub struct ParameterizedBlock<'a> { pub path: Path<'a>, pub explicit_context: Option>, pub params: Vec>, pub contents: Option>, pub else_contents: Option>, } #[derive(Debug, PartialEq)] pub struct Partial<'a> { pub name: Vec, pub explicit_context: Option>, pub params: Vec>, } #[derive(Debug, PartialEq)] pub enum OwnedLiteral { LString(String), LPositiveInteger(u64), LNegativeInteger(i64), LFloat(f64), // Unlike the other OwnedLiterals, booleans cannot occur in DustJS // templates because true/false are not reserved // names. Regardless, they are needed here for type casting in the // renderer. LBoolean(bool), } #[derive(Debug, PartialEq)] pub enum RValue<'a> { RVPath(Path<'a>), RVTemplate(Vec), RVLiteral(OwnedLiteral), } #[derive(Debug, PartialEq)] pub struct KVPair<'a> { pub key: &'a str, pub value: RValue<'a>, } #[derive(Debug, PartialEq)] pub enum PartialNameElement { PNSpan { contents: String, }, PNReference { path: Vec, filters: Vec, }, } #[derive(Debug, PartialEq)] pub struct Body<'a> { pub elements: Vec>, } #[derive(Debug, PartialEq)] pub struct Template<'a> { pub contents: Body<'a>, } #[derive(Debug, PartialEq)] pub enum TemplateElement<'a> { TESpan(Span<'a>), TETag(DustTag<'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 fn dust_tag(i: &str) -> IResult<&str, DustTag> { alt(( map(special, DustTag::DTSpecial), map(comment, DustTag::DTComment), map(literal_string_block, DustTag::DTLiteralStringBlock), map(reference, DustTag::DTReference), map(parameterized_block("{#", path), DustTag::DTSection), map(parameterized_block("{?", path), DustTag::DTExists), map(parameterized_block("{^", path), DustTag::DTNotExists), map( parameterized_block_without_else("{+", key_to_path), DustTag::DTBlock, ), map( parameterized_block_without_else("{<", key_to_path), DustTag::DTInlinePartial, ), partial("{>", DustTag::DTPartial), dust_tag_helper, ))(i) } /// Nom's alt() is limited to 21 possibilities, so I pushed this out /// into its own parser. Otherwise there is no reason for this not to /// be part of the dust_tag parser. fn dust_tag_helper(i: &str) -> IResult<&str, DustTag> { alt(( map( parameterized_block("{@", &tag_to_path("gte")), DustTag::DTHelperGreaterThanOrEquals, ), map( parameterized_block("{@", &tag_to_path("lte")), DustTag::DTHelperLessThanOrEquals, ), map( parameterized_block("{@", &tag_to_path("eq")), DustTag::DTHelperEquals, ), map( parameterized_block("{@", &tag_to_path("ne")), DustTag::DTHelperNotEquals, ), map( parameterized_block("{@", &tag_to_path("gt")), DustTag::DTHelperGreaterThan, ), map( parameterized_block("{@", &tag_to_path("lt")), DustTag::DTHelperLessThan, ), map( parameterized_block("{@", &tag_to_path("sep")), DustTag::DTHelperSep, ), map( parameterized_block("{@", &tag_to_path("first")), DustTag::DTHelperFirst, ), map( parameterized_block("{@", &tag_to_path("last")), DustTag::DTHelperLast, ), map( parameterized_block("{@", &tag_to_path("select")), DustTag::DTHelperSelect, ), map( parameterized_block("{@", &tag_to_path("any")), DustTag::DTHelperAny, ), map( parameterized_block("{@", &tag_to_path("none")), DustTag::DTHelperNone, ), map( 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) } /// Special characters fn special(i: &str) -> IResult<&str, Special> { delimited( tag("{~"), alt(( value(Special::LeftCurlyBrace, tag("lb")), value(Special::RightCurlyBrace, tag("rb")), value(Special::Space, tag("s")), value(Special::NewLine, tag("n")), value(Special::CarriageReturn, tag("r")), )), tag("}"), )(i) } /// Part of a dust template that does not get rendered fn comment(i: &str) -> IResult<&str, Comment> { map(delimited(tag("{!"), take_until("!}"), tag("!}")), |body| { Comment { value: body } })(i) } /// A single element of a path fn key(i: &str) -> IResult<&str, &str> { recognize(tuple(( one_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$"), opt(is_a( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789-", )), )))(i) } /// A series of keys separated by '.' to reference a variable in the context fn path(i: &str) -> IResult<&str, Path> { alt(( map(separated_list1(tag("."), key), |body| Path { keys: body }), map( tuple((tag("."), separated_list1(tag("."), key))), |(dot, mut body)| { body.insert(0, dot); Path { keys: body } }, ), map(tag("."), |dot| Path { keys: vec![dot] }), ))(i) } fn tag_to_path<'a>(text: &'static str) -> impl Fn(&'a str) -> IResult<&str, Path<'a>> { move |i: &'a str| map(tag(text), |t| Path { keys: vec![t] })(i) } fn key_to_path<'a>(i: &'a str) -> IResult<&str, Path<'a>> { map(key, |k| Path { keys: vec![k] })(i) } /// Just digits, no signs or decimals fn postitive_integer_literal(i: &str) -> IResult<&str, u64> { map( verify( map( recognize(tuple((opt(tag("+")), digit1))), |number_string: &str| number_string.parse::(), ), |parse_result| parse_result.is_ok(), ), |parsed_number| parsed_number.unwrap(), )(i) } /// No decimals, just the sign and digits fn negative_integer_literal(i: &str) -> IResult<&str, i64> { map( verify( map( recognize(tuple((tag("-"), digit1))), |number_string: &str| number_string.parse::(), ), |parse_result| parse_result.is_ok(), ), |parsed_number| parsed_number.unwrap(), )(i) } /// A non-scientific notation float (sign, digits, decimal, and more digits) fn float_literal(i: &str) -> IResult<&str, f64> { map( verify( map( recognize(tuple((opt(one_of("+-")), digit1, tag("."), digit1))), |number_string: &str| number_string.parse::(), ), |parse_result| parse_result.is_ok(), ), |parsed_number| parsed_number.unwrap(), )(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(template_string_rvalue, RValue::RVTemplate), map(float_literal, |num| { RValue::RVLiteral(OwnedLiteral::LFloat(num)) }), map(negative_integer_literal, |num| { RValue::RVLiteral(OwnedLiteral::LNegativeInteger(num)) }), map(postitive_integer_literal, |num| { RValue::RVLiteral(OwnedLiteral::LPositiveInteger(num)) }), ))(i) } /// Parameters for a partial fn key_value_pair(i: &str) -> IResult<&str, KVPair> { map(separated_pair(key, tag("="), rvalue), |(k, v)| KVPair { key: k, value: v, })(i) } /// Display a value from the context fn reference(i: &str) -> IResult<&str, Reference> { let (remaining, (p, filters)) = delimited( tag("{"), tuple((path, many0(filter))), preceded(space0, tag("}")), )(i)?; Ok(( remaining, Reference { path: p, filters: filters, }, )) } fn parameterized_block<'a, F>( open_matcher: &'static str, name_matcher: F, ) -> impl FnMut(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> where F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, { alt(( parameterized_block_with_body(open_matcher, name_matcher), parameterized_self_closing_block(open_matcher, name_matcher), )) } fn parameterized_block_without_else<'a, F>( open_matcher: &'static str, name_matcher: F, ) -> impl FnMut(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> where F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, { alt(( parameterized_block_with_body_without_else(open_matcher, name_matcher), parameterized_self_closing_block(open_matcher, name_matcher), )) } fn parameterized_block_with_body<'a, F>( open_matcher: &'static str, name_matcher: F, ) -> impl Fn(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> where F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, { move |i: &'a str| { let (i, (opening_name, maybe_explicit_context, params, inner, maybe_else, _closing_name)) = verify( tuple(( preceded(tag(open_matcher), name_matcher), opt(preceded(tag(":"), path)), terminated( opt(preceded(space1, separated_list1(space1, key_value_pair))), preceded(space0, tag("}")), ), opt(body), opt(preceded(tag("{:else}"), opt(body))), delimited(tag("{/"), name_matcher, tag("}")), )), |(open, _maybe_explicit, _params, _inn, _maybe_else, close)| open == close, )(i)?; Ok(( i, ParameterizedBlock { path: opening_name, explicit_context: maybe_explicit_context, params: params.unwrap_or(Vec::new()), contents: inner, else_contents: maybe_else.flatten(), }, )) } } fn parameterized_block_with_body_without_else<'a, F>( open_matcher: &'static str, name_matcher: F, ) -> impl Fn(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> where F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, { move |i: &'a str| { let (i, (opening_name, maybe_explicit_context, params, inner, _closing_name)) = verify( tuple(( preceded(tag(open_matcher), name_matcher), opt(preceded(tag(":"), path)), terminated( opt(preceded(space1, separated_list1(space1, key_value_pair))), preceded(space0, tag("}")), ), opt(body), delimited(tag("{/"), name_matcher, tag("}")), )), |(open, _maybe_explicit, _params, _inn, close)| open == close, )(i)?; Ok(( i, ParameterizedBlock { path: opening_name, explicit_context: maybe_explicit_context, params: params.unwrap_or(Vec::new()), contents: inner, else_contents: None, }, )) } } fn parameterized_self_closing_block<'a, F>( open_matcher: &'static str, name_matcher: F, ) -> impl Fn(&'a str) -> IResult<&'a str, ParameterizedBlock<'a>> where F: Copy + Fn(&'a str) -> IResult<&'a str, Path<'a>>, { move |i: &'a str| { let (i, (opening_name, maybe_explicit_context, params)) = delimited( tag(open_matcher), tuple(( name_matcher, opt(preceded(tag(":"), path)), opt(preceded(space1, separated_list1(space1, key_value_pair))), )), preceded(space0, tag("/}")), )(i)?; Ok(( i, ParameterizedBlock { path: opening_name, explicit_context: maybe_explicit_context, params: params.unwrap_or(Vec::new()), contents: None, else_contents: None, }, )) } } fn partial_with_plain_tag<'a>( open_matcher: &'static str, ) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> { move |i: &'a str| { let (i, (name, maybe_explicit_context, params)) = delimited( tag(open_matcher), tuple(( key, opt(preceded(tag(":"), path)), opt(preceded(space1, separated_list1(space1, key_value_pair))), )), preceded(space0, tag("/}")), )(i)?; Ok(( i, Partial { name: vec![PartialNameElement::PNSpan { contents: name.to_owned(), }], explicit_context: maybe_explicit_context, 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, maybe_explicit_context, params)) = delimited( tag(open_matcher), tuple(( template_string_rvalue, opt(preceded(tag(":"), path)), opt(preceded(space1, separated_list1(space1, key_value_pair))), )), preceded(space0, tag("/}")), )(i)?; Ok(( i, Partial { name: name, explicit_context: maybe_explicit_context, 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("|"), alt(( value(Filter::JsonStringify, tag("js")), value(Filter::JsonParse, tag("jp")), value(Filter::EncodeUriComponent, tag("uc")), value(Filter::HtmlEncode, tag("h")), value(Filter::DisableHtmlEncode, tag("s")), value(Filter::JavascriptStringEncode, tag("j")), value(Filter::EncodeUri, tag("u")), )), )(i) } /// Whitespace at the beginning of lines is ignored so we are matching /// a newline character followed by as much contiguous whitespace as /// possible, all of which will be thrown away by other parsers. fn ignore_new_line_leading_whitespace(i: &str) -> IResult<&str, IgnoredWhitespace> { map( recognize(tuple((line_ending, multispace0))), IgnoredWhitespace::StartOfLine, )(i) } fn literal_string_block(i: &str) -> IResult<&str, &str> { delimited(tag("{`"), take_until("`}"), tag("`}"))(i) } /// Any text that is not a Dust element or ignored whitespace fn span(i: &str) -> IResult<&str, Span> { let (remaining, line) = verify( take_until_parser_matches(alt(( tag("{"), line_ending, recognize(all_consuming(eof_whitespace)), ))), |s: &str| s.len() > 0, )(i)?; Ok((remaining, Span { contents: line })) } fn body(i: &str) -> IResult<&str, Body> { let (remaining, template_elements) = many1(alt(( map( ignore_new_line_leading_whitespace, TemplateElement::TEIgnoredWhitespace, ), map(span, TemplateElement::TESpan), map(dust_tag, TemplateElement::TETag), )))(i)?; Ok(( remaining, Body { elements: template_elements, }, )) } pub fn template(i: &str) -> IResult<&str, Template> { // DustJS ignores all preceding whitespace (tabs, newlines, spaces) but only ignores trailing newlines let (remaining, contents) = all_consuming(delimited(multispace0, body, eof_whitespace))(i)?; Ok((remaining, Template { contents: contents })) } fn quoted_string(i: &str) -> IResult<&str, String> { delimited( tag(r#"""#), escaped_transform(is_not(r#"\""#), '\\', one_of(r#"\""#)), tag(r#"""#), )(i) } fn eof_whitespace(i: &str) -> IResult<&str, Vec<&str>> { many0(line_ending)(i) } #[cfg(test)] mod tests { use super::*; use nom::bytes::complete::is_a; use nom::error::ErrorKind; use nom::Err::Error; #[test] fn test_direct_literal() { assert_eq!(super::float_literal("-17.4"), Ok(("", -17.4))); assert_eq!(super::float_literal("17.1"), Ok(("", 17.1))); assert_eq!(super::negative_integer_literal("-12"), Ok(("", -12))); } #[test] fn test_reference() { assert_eq!( super::reference("{foo.bar.baz|js|s}"), Ok(( "", Reference { path: Path { keys: vec!["foo", "bar", "baz"] }, filters: vec![Filter::JsonStringify, Filter::DisableHtmlEncode], } )) ); } #[test] fn test_reference_to_variable() { assert_eq!( super::reference("{$idx}"), Ok(( "", Reference { path: Path { keys: vec!["$idx"] }, filters: Vec::new(), } )) ); } #[test] fn test_path() { assert_eq!( is_a::<_, _, (_, ErrorKind)>("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$")( "foo" ), Ok(("", "foo")) ); assert_eq!( super::path("foo.bar.baz"), Ok(( "", Path { keys: vec!["foo", "bar", "baz"] } )) ); } #[test] fn test_special() { assert_eq!(super::special("{~s}"), Ok(("", Special::Space))); assert_eq!(super::special("{~n}"), Ok(("", Special::NewLine))); assert_eq!(super::special("{~r}"), Ok(("", Special::CarriageReturn))); assert_eq!(super::special("{~lb}"), Ok(("", Special::LeftCurlyBrace))); assert_eq!(super::special("{~rb}"), Ok(("", Special::RightCurlyBrace))); assert_eq!( super::special("{~zzz}"), Err(Error(nom::error::Error { input: "zzz}", code: ErrorKind::Tag })) ); } #[test] fn test_comment() { assert_eq!( super::comment("{! yo dawg} this is a comment !}"), Ok(( "", Comment { value: " yo dawg} this is a comment " } )) ); assert_eq!( super::special("{! this is a comment without a close"), Err(Error(nom::error::Error { input: "{! this is a comment without a close", code: ErrorKind::Tag })) ); } #[test] fn test_span_end_of_line() { assert_eq!( super::ignore_new_line_leading_whitespace("\n \t \n\nfoo"), Ok(("foo", IgnoredWhitespace::StartOfLine("\n \t \n\n"))) ); } #[test] fn test_span() { assert_eq!( super::span("this is just some text"), Ok(( "", Span { contents: "this is just some text" } )) ); assert_eq!( super::span("this is just some text {~lb}"), Ok(( "{~lb}", Span { contents: "this is just some text " } )) ); assert_eq!( super::span("{~lb}"), Err(Error(nom::error::Error { input: "{~lb}", code: ErrorKind::Verify })) ); assert_eq!( super::body("this is \t \n\n \t \n \t multiline text\n {foo}"), Ok(( "", Body { elements: vec![ TemplateElement::TESpan(Span { contents: "this is \t " }), TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( "\n\n \t \n \t " )), TemplateElement::TESpan(Span { contents: "multiline text" }), TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( "\n " )), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["foo"] }, filters: vec![] })) ] } )) ); assert_eq!( super::body("\n leading whitespace"), Ok(( "", Body { elements: vec![ TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( "\n " )), TemplateElement::TESpan(Span { contents: "leading whitespace" }), ] } )) ); } #[test] fn test_section_mismatched_paths() { assert_eq!( super::dust_tag("{#foo.bar}{/baz}"), Err(Error(nom::error::Error { input: "{#foo.bar}{/baz}", code: ErrorKind::Tag })) ); } #[test] fn test_empty_section() { assert_eq!( super::dust_tag("{#foo.bar}{/foo.bar}"), Ok(( "", DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["foo", "bar"] }, explicit_context: None, params: Vec::new(), contents: None, else_contents: None, }) )) ); } #[test] fn test_self_closing_section() { assert_eq!( super::dust_tag("{#foo.bar/}"), Ok(( "", DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["foo", "bar"] }, explicit_context: None, params: Vec::new(), contents: None, else_contents: None, }) )) ); } #[test] fn test_section_with_body() { assert_eq!( super::dust_tag("{#foo.bar}hello {name}{/foo.bar}"), Ok(( "", DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["foo", "bar"] }, explicit_context: None, params: Vec::new(), contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "hello " }), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["name"] }, filters: Vec::new() })) ] }), else_contents: None, }) )) ); } #[test] fn test_section_with_else_body() { assert_eq!( super::dust_tag("{#greeting}hello {name}{:else}goodbye {name}{/greeting}"), Ok(( "", DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["greeting"] }, explicit_context: None, params: Vec::new(), contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "hello " }), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["name"] }, filters: Vec::new() })) ] }), else_contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "goodbye " }), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["name"] }, filters: Vec::new() })) ] }), }) )) ); } #[test] fn test_empty_section_with_explicit_context() { assert_eq!( super::dust_tag("{#foo.bar:baz.ipsum}{/foo.bar}"), Ok(( "", DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["foo", "bar"] }, explicit_context: Some(Path { keys: vec!["baz", "ipsum"] }), params: Vec::new(), contents: None, else_contents: None, }) )) ); } #[test] fn test_self_closing_section_with_explicit_context() { assert_eq!( super::dust_tag("{#foo.bar:$idx/}"), Ok(( "", DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["foo", "bar"] }, explicit_context: Some(Path { keys: vec!["$idx"] }), params: Vec::new(), contents: None, else_contents: None, }) )) ); } #[test] fn test_self_closing_block() { assert_eq!( super::dust_tag("{+foo/}"), Ok(( "", DustTag::DTBlock(ParameterizedBlock { path: Path { keys: vec!["foo"] }, explicit_context: None, params: Vec::new(), contents: None, else_contents: None }) )) ); } #[test] fn test_block() { assert_eq!( super::dust_tag("{+foo}hello {name}{/foo}"), Ok(( "", DustTag::DTBlock(ParameterizedBlock { path: Path { keys: vec!["foo"] }, explicit_context: None, params: Vec::new(), contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "hello " }), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["name"] }, filters: Vec::new() })) ] }), else_contents: None }) )) ); } #[test] fn test_self_closing_block_with_explicit_context() { assert_eq!( super::dust_tag("{+foo:bar.baz/}"), Ok(( "", DustTag::DTBlock(ParameterizedBlock { path: Path { keys: vec!["foo"] }, explicit_context: Some(Path { keys: vec!["bar", "baz"] }), params: Vec::new(), contents: None, else_contents: None }) )) ); } #[test] fn test_block_with_explicit_context() { assert_eq!( super::dust_tag("{+foo:bar.baz}hello {name}{/foo}"), Ok(( "", DustTag::DTBlock(ParameterizedBlock { path: Path { keys: vec!["foo"] }, explicit_context: Some(Path { keys: vec!["bar", "baz"] }), params: Vec::new(), contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "hello " }), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["name"] }, filters: Vec::new() })) ] }), else_contents: None }) )) ); } #[test] fn test_self_closing_inline_partial() { assert_eq!( super::dust_tag("{foo bar=baz animal="cat"/}"#), Ok(( "", DustTag::DTPartial(Partial { name: vec![PartialNameElement::PNSpan { contents: "foo".to_owned() },], explicit_context: None, params: vec![ KVPair { key: "bar", value: RValue::RVPath(Path { keys: vec!["baz"] }) }, KVPair { key: "animal", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "cat".to_owned() }]) } ] }) )) ); } #[test] fn test_quoted_partial() { assert_eq!( dust_tag(r#"{>"template name * with * special \" characters" bar=baz animal="cat"/}"#), Ok(( "", DustTag::DTPartial(Partial { name: vec![PartialNameElement::PNSpan { contents: r#"template name * with * special " characters"#.to_owned() },], explicit_context: None, params: vec![ KVPair { key: "bar", value: RValue::RVPath(Path { keys: vec!["baz"] }) }, KVPair { key: "animal", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "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() } ], explicit_context: None, params: vec![ KVPair { key: "bar", value: RValue::RVPath(Path { keys: vec!["baz"] }) }, KVPair { key: "animal", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "cat".to_owned() }]) } ] }) )) ); } #[test] fn test_unquoted_partial_with_explicit_context() { assert_eq!( dust_tag(r#"{>foo:foo.bar bar=baz animal="cat"/}"#), Ok(( "", DustTag::DTPartial(Partial { name: vec![PartialNameElement::PNSpan { contents: "foo".to_owned() },], explicit_context: Some(Path { keys: vec!["foo", "bar"] }), params: vec![ KVPair { key: "bar", value: RValue::RVPath(Path { keys: vec!["baz"] }) }, KVPair { key: "animal", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "cat".to_owned() }]) } ] }) )) ); } #[test] fn test_quoted_partial_with_explicit_context() { assert_eq!( dust_tag( r#"{>"template name * with * special \" characters":foo.bar bar=baz animal="cat"/}"# ), Ok(( "", DustTag::DTPartial(Partial { name: vec![PartialNameElement::PNSpan { contents: r#"template name * with * special " characters"#.to_owned() },], explicit_context: Some(Path { keys: vec!["foo", "bar"] }), params: vec![ KVPair { key: "bar", value: RValue::RVPath(Path { keys: vec!["baz"] }) }, KVPair { key: "animal", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "cat".to_owned() }]) } ] }) )) ); } #[test] fn test_literals() { assert_eq!( dust_tag(r#"{>foo a="foo" b=179 c=17.1 d=-12 e=-17.4/}"#), Ok(( "", DustTag::DTPartial(Partial { name: vec![PartialNameElement::PNSpan { contents: "foo".to_owned() },], explicit_context: None, params: vec![ KVPair { key: "a", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "foo".to_owned() }]) }, KVPair { key: "b", value: RValue::RVLiteral(OwnedLiteral::LPositiveInteger(179)) }, KVPair { key: "c", value: RValue::RVLiteral(OwnedLiteral::LFloat(17.1)) }, KVPair { key: "d", value: RValue::RVLiteral(OwnedLiteral::LNegativeInteger(-12)) }, KVPair { key: "e", value: RValue::RVLiteral(OwnedLiteral::LFloat(-17.4)) } ] }) )) ); } #[test] fn test_helper() { assert_eq!( dust_tag(r#"{@eq key=name value="cat"}Pet the {name}!{/eq}"#), Ok(( "", DustTag::DTHelperEquals(ParameterizedBlock { path: Path { keys: vec!["eq"] }, explicit_context: None, params: vec![ KVPair { key: "key", value: RValue::RVPath(Path { keys: vec!["name"] }) }, KVPair { key: "value", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "cat".to_owned() }]) } ], contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "Pet the " }), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["name"] }, filters: Vec::new() })), TemplateElement::TESpan(Span { contents: "!" }) ] }), else_contents: None }) )) ); } #[test] fn test_self_closing_helper() { assert_eq!( dust_tag(r#"{@eq key=name value="cat"/}"#), Ok(( "", DustTag::DTHelperEquals(ParameterizedBlock { path: Path { keys: vec!["eq"] }, explicit_context: None, params: vec![ KVPair { key: "key", value: RValue::RVPath(Path { keys: vec!["name"] }) }, KVPair { key: "value", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "cat".to_owned() }]) } ], contents: None, else_contents: None }) )) ); } #[test] fn test_helper_with_explicit_context() { assert_eq!( dust_tag(r#"{@eq:foo.bar key=name value="cat"}Pet the {name}!{/eq}"#), Ok(( "", DustTag::DTHelperEquals(ParameterizedBlock { path: Path { keys: vec!["eq"] }, explicit_context: Some(Path { keys: vec!["foo", "bar"] }), params: vec![ KVPair { key: "key", value: RValue::RVPath(Path { keys: vec!["name"] }) }, KVPair { key: "value", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "cat".to_owned() }]) } ], contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "Pet the " }), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["name"] }, filters: Vec::new() })), TemplateElement::TESpan(Span { contents: "!" }) ] }), else_contents: None }) )) ); } #[test] fn test_self_closing_helper_with_explicit_context() { assert_eq!( dust_tag(r#"{@eq:foo.bar key=name value="cat"/}"#), Ok(( "", DustTag::DTHelperEquals(ParameterizedBlock { path: Path { keys: vec!["eq"] }, explicit_context: Some(Path { keys: vec!["foo", "bar"] }), params: vec![ KVPair { key: "key", value: RValue::RVPath(Path { keys: vec!["name"] }) }, KVPair { key: "value", value: RValue::RVTemplate(vec![PartialNameElement::PNSpan { contents: "cat".to_owned() }]) } ], contents: None, else_contents: None }) )) ); } #[test] fn test_full_document_new_line_equality() { assert_eq!( super::template( "- simple -{~n} {#names}{.}{/names} {~n}- new lines -{~n} {#names} {.} {/names}" ), Ok::<_, nom::Err>>(( "", Template { contents: Body { elements: vec![ TemplateElement::TESpan(Span { contents: "- simple -" }), TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)), TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( "\n" )), TemplateElement::TETag(DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["names"] }, explicit_context: None, params: Vec::new(), contents: Some(Body { elements: vec![TemplateElement::TETag(DustTag::DTReference( Reference { path: Path { keys: vec!["."] }, filters: vec![] } ))] }), else_contents: None, })), TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( "\n" )), TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)), TemplateElement::TESpan(Span { contents: "- new lines -" }), TemplateElement::TETag(DustTag::DTSpecial(Special::NewLine)), TemplateElement::TEIgnoredWhitespace(IgnoredWhitespace::StartOfLine( "\n" )), TemplateElement::TETag(DustTag::DTSection(ParameterizedBlock { path: Path { keys: vec!["names"] }, explicit_context: None, params: Vec::new(), contents: Some(Body { elements: vec![ TemplateElement::TEIgnoredWhitespace( IgnoredWhitespace::StartOfLine("\n") ), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["."] }, filters: vec![] })), TemplateElement::TEIgnoredWhitespace( IgnoredWhitespace::StartOfLine("\n") ) ] }), else_contents: None, })), ] } } )) ); } #[test] fn test_full_document_parameterized_partial() { assert_eq!( super::template( r#"{#level3.level4}{>partialtwo v1="b" v2="b" v3="b" v4="b" v5="b" /}{/level3.level4}"# ), Ok::<_, nom::Err>>(( "", Template { contents: Body { elements: vec![TemplateElement::TETag(DustTag::DTSection( ParameterizedBlock { path: Path { keys: vec!["level3", "level4"] }, explicit_context: None, params: Vec::new(), contents: Some(Body { elements: vec![TemplateElement::TETag(DustTag::DTPartial( Partial { name: vec![PartialNameElement::PNSpan { contents: "partialtwo".to_owned() },], explicit_context: None, params: vec![ KVPair { key: "v1", value: RValue::RVTemplate(vec![ PartialNameElement::PNSpan { contents: "b".to_owned() } ]) }, KVPair { key: "v2", value: RValue::RVTemplate(vec![ PartialNameElement::PNSpan { contents: "b".to_owned() } ]) }, KVPair { key: "v3", value: RValue::RVTemplate(vec![ PartialNameElement::PNSpan { contents: "b".to_owned() } ]) }, KVPair { key: "v4", value: RValue::RVTemplate(vec![ PartialNameElement::PNSpan { contents: "b".to_owned() } ]) }, KVPair { key: "v5", value: RValue::RVTemplate(vec![ PartialNameElement::PNSpan { contents: "b".to_owned() } ]) } ] } ))] }), else_contents: None } ))] } } )) ); } }