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; use nom::bytes::complete::take_until; use nom::bytes::complete::take_until_parser_matches; use nom::character::complete::line_ending; use nom::character::complete::multispace0; use nom::character::complete::one_of; use nom::character::complete::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(Clone, Debug, PartialEq)] pub enum DustTag<'a> { DTSpecial(Special), DTComment(Comment<'a>), DTReference(Reference<'a>), DTSection(Container<'a>), DTExists(Container<'a>), DTNotExists(Container<'a>), DTBlock(NamedBlock<'a>), DTInlinePartial(NamedBlock<'a>), DTPartial(Partial<'a>), DTHelperEquals(ParameterizedBlock<'a>), DTHelperNotEquals(ParameterizedBlock<'a>), DTHelperGreaterThan(ParameterizedBlock<'a>), DTHelperLessThan(ParameterizedBlock<'a>), DTHelperGreaterThenOrEquals(ParameterizedBlock<'a>), DTHelperLessThenOrEquals(ParameterizedBlock<'a>), } #[derive(Clone, Debug, PartialEq)] pub enum Special { Space, NewLine, CarriageReturn, LeftCurlyBrace, RightCurlyBrace, } #[derive(Clone, 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(Clone, Debug, PartialEq)] pub struct Path<'a> { pub keys: Vec<&'a str>, } #[derive(Clone, 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(Clone, Debug, PartialEq)] pub struct Span<'a> { pub contents: &'a str, } #[derive(Clone, Debug, PartialEq)] pub struct Container<'a> { pub path: Path<'a>, pub contents: Option>, pub else_contents: Option>, } #[derive(Clone, Debug, PartialEq)] pub struct NamedBlock<'a> { name: &'a str, contents: Option>, } #[derive(Clone, Debug, PartialEq)] pub struct ParameterizedBlock<'a> { name: &'a str, params: Vec>, contents: Option>, else_contents: Option>, } #[derive(Clone, Debug, PartialEq)] pub struct Partial<'a> { name: String, params: Vec>, } #[derive(Clone, Debug, PartialEq)] enum RValue<'a> { RVPath(Path<'a>), RVString(String), } #[derive(Clone, Debug, PartialEq)] struct KVPair<'a> { key: &'a str, value: RValue<'a>, } #[derive(Clone, Debug, PartialEq)] pub struct Body<'a> { pub elements: Vec>, } #[derive(Clone, Debug)] pub struct Template<'a> { pub contents: Body<'a>, } #[derive(Clone, Debug, PartialEq)] pub enum TemplateElement<'a> { TESpan(Span<'a>), TETag(DustTag<'a>), } /// 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(reference, DustTag::DTReference), conditional("{#", DustTag::DTSection), conditional("{?", DustTag::DTExists), conditional("{^", DustTag::DTNotExists), named_block("{+", DustTag::DTBlock), named_block("{<", DustTag::DTInlinePartial), partial("{>", DustTag::DTPartial), parameterized_block("{@", "gte", DustTag::DTHelperGreaterThenOrEquals), parameterized_block("{@", "lte", DustTag::DTHelperLessThenOrEquals), parameterized_block("{@", "eq", DustTag::DTHelperEquals), parameterized_block("{@", "ne", DustTag::DTHelperNotEquals), parameterized_block("{@", "gt", DustTag::DTHelperGreaterThan), parameterized_block("{@", "lt", DustTag::DTHelperLessThan), ))(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 }), value(Path { keys: Vec::new() }, tag(".")), ))(i) } /// Either a literal or a path to a value fn rvalue(i: &str) -> IResult<&str, RValue> { alt(( map(path, RValue::RVPath), map(quoted_string, RValue::RVString), ))(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))), tag("}"))(i)?; Ok(( remaining, Reference { path: p, filters: filters, }, )) } fn conditional<'a, F>( open_matcher: &'static str, constructor: F, ) -> impl FnMut(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Copy + Fn(Container<'a>) -> DustTag<'a>, { alt(( conditional_with_body(open_matcher, constructor), self_closing_conditional(open_matcher, constructor), )) } fn conditional_with_body<'a, F>( open_matcher: &'static str, constructor: F, ) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Fn(Container<'a>) -> DustTag<'a>, { move |i: &'a str| { let (i, (opening_name, inner, maybe_else, _closing_name)) = verify( tuple(( delimited(tag(open_matcher), path, tag("}")), opt(body), opt(preceded(tag("{:else}"), opt(body))), delimited(tag("{/"), path, tag("}")), )), |(open, _inn, _maybe_else, close)| open == close, )(i)?; Ok(( i, constructor(Container { path: opening_name, contents: inner, else_contents: maybe_else.flatten(), }), )) } } fn self_closing_conditional<'a, F>( open_matcher: &'static str, constructor: F, ) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Fn(Container<'a>) -> DustTag<'a>, { move |i: &'a str| { let (i, path) = delimited(tag(open_matcher), path, tag("/}"))(i)?; Ok(( i, constructor(Container { path: path, contents: None, else_contents: None, }), )) } } fn named_block<'a, F>( open_matcher: &'static str, constructor: F, ) -> impl FnMut(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Copy + Fn(NamedBlock<'a>) -> DustTag<'a>, { alt(( named_block_with_body(open_matcher, constructor), self_closing_named_block(open_matcher, constructor), )) } fn named_block_with_body<'a, F>( open_matcher: &'static str, constructor: F, ) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Fn(NamedBlock<'a>) -> DustTag<'a>, { move |i: &'a str| { let (i, (opening_name, inner, _closing_name)) = verify( tuple(( delimited(tag(open_matcher), key, tag("}")), opt(body), delimited(tag("{/"), key, tag("}")), )), |(open, _inn, close)| open == close, )(i)?; Ok(( i, constructor(NamedBlock { name: opening_name, contents: inner, }), )) } } fn self_closing_named_block<'a, F>( open_matcher: &'static str, constructor: F, ) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Fn(NamedBlock<'a>) -> DustTag<'a>, { move |i: &'a str| { let (i, name) = delimited(tag(open_matcher), key, tag("/}"))(i)?; Ok(( i, constructor(NamedBlock { name: name, contents: None, }), )) } } fn parameterized_block<'a, F>( open_matcher: &'static str, tag_name: &'static str, constructor: F, ) -> impl FnMut(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Copy + Fn(ParameterizedBlock<'a>) -> DustTag<'a>, { alt(( parameterized_block_with_body(open_matcher, tag_name, constructor), parameterized_self_closing_block(open_matcher, tag_name, constructor), )) } fn parameterized_block_with_body<'a, F>( open_matcher: &'static str, tag_name: &'static str, constructor: F, ) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Fn(ParameterizedBlock<'a>) -> DustTag<'a>, { move |i: &'a str| { let (i, (name, params, inner, maybe_else, _closing_name)) = tuple(( preceded(tag(open_matcher), tag(tag_name)), terminated( opt(preceded(space1, separated_list1(space1, key_value_pair))), tag("}"), ), opt(body), opt(preceded(tag("{:else}"), opt(body))), delimited(tag("{/"), tag(tag_name), tag("}")), ))(i)?; Ok(( i, constructor(ParameterizedBlock { name: name, params: params.unwrap_or(Vec::new()), contents: inner, else_contents: maybe_else.flatten(), }), )) } } fn parameterized_self_closing_block<'a, F>( open_matcher: &'static str, tag_name: &'static str, constructor: F, ) -> impl Fn(&'a str) -> IResult<&'a str, DustTag<'a>> where F: Fn(ParameterizedBlock<'a>) -> DustTag<'a>, { move |i: &'a str| { let (i, (name, params)) = delimited( tag(open_matcher), tuple(( tag(tag_name), opt(preceded(space1, separated_list1(space1, key_value_pair))), )), tag("/}"), )(i)?; Ok(( i, constructor(ParameterizedBlock { name: name, params: params.unwrap_or(Vec::new()), contents: None, else_contents: None, }), )) } } 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>, { move |i: &'a str| { let (i, (name, params)) = delimited( tag(open_matcher), tuple(( alt((map(key, String::from), quoted_string)), opt(preceded(space1, separated_list1(space1, key_value_pair))), )), tag("/}"), )(i)?; Ok(( i, constructor(Partial { name: name, params: params.unwrap_or(Vec::new()), }), )) } } 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) } /// Any text that is not a Dust element fn span(i: &str) -> IResult<&str, Span> { let (remaining, body) = verify( alt(( take_until("{"), take_until_parser_matches(all_consuming(eof_whitespace)), )), |s: &str| s.len() > 0, )(i)?; Ok((remaining, Span { contents: body })) } fn body(i: &str) -> IResult<&str, Body> { let (remaining, template_elements) = many1(alt(( 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) = 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_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_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(("zzz}", 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(( "{! this is a comment without a close", ErrorKind::Tag ))) ); } #[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(("{~lb}", ErrorKind::Verify))) ); } #[test] fn test_section_mismatched_paths() { assert_eq!( super::dust_tag("{#foo.bar}{/baz}"), Err(Error(("{#foo.bar}{/baz}", ErrorKind::Tag))) ); } #[test] fn test_empty_section() { assert_eq!( super::dust_tag("{#foo.bar}{/foo.bar}"), Ok(( "", DustTag::DTSection(Container { path: Path { keys: vec!["foo", "bar"] }, contents: None, else_contents: None, }) )) ); } #[test] fn test_self_closing_section() { assert_eq!( super::dust_tag("{#foo.bar/}"), Ok(( "", DustTag::DTSection(Container { path: Path { keys: vec!["foo", "bar"] }, 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(Container { path: Path { keys: vec!["foo", "bar"] }, 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(Container { path: Path { keys: vec!["greeting"] }, 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_self_closing_block() { assert_eq!( super::dust_tag("{+foo/}"), Ok(( "", DustTag::DTBlock(NamedBlock { name: "foo", contents: None }) )) ); } #[test] fn test_block() { assert_eq!( super::dust_tag("{+foo}hello {name}{/foo}"), Ok(( "", DustTag::DTBlock(NamedBlock { name: "foo", contents: Some(Body { elements: vec![ TemplateElement::TESpan(Span { contents: "hello " }), TemplateElement::TETag(DustTag::DTReference(Reference { path: Path { keys: vec!["name"] }, filters: Vec::new() })) ] }) }) )) ); } #[test] fn test_self_closing_inline_partial() { assert_eq!( super::dust_tag("{foo bar=baz animal="cat"/}"#), Ok(( "", DustTag::DTPartial(Partial { name: "foo".to_owned(), params: vec![ KVPair { key: "bar", value: RValue::RVPath(Path { keys: vec!["baz"] }) }, KVPair { key: "animal", value: RValue::RVString("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: 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::RVString("cat".to_owned()) } ] }) )) ); } #[test] fn test_helper() { assert_eq!( dust_tag(r#"{@eq key=name value="cat"}Pet the {name}!{/eq}"#), Ok(( "", DustTag::DTHelperEquals(ParameterizedBlock { name: "eq", params: vec![ KVPair { key: "key", value: RValue::RVPath(Path { keys: vec!["name"] }) }, KVPair { key: "value", value: RValue::RVString("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 { name: "eq", params: vec![ KVPair { key: "key", value: RValue::RVPath(Path { keys: vec!["name"] }) }, KVPair { key: "value", value: RValue::RVString("cat".to_owned()) } ], contents: None, else_contents: None }) )) ); } }