use std::cell::Cell; use winnow::{LocatingSlice, Parser}; use crate::expr::BinOp; use crate::node::{Let, Lit, Raw, Whitespace, Ws}; use crate::{ Ast, Expr, Filter, InnerSyntax, InputStream, Level, Node, Num, PathComponent, PathOrIdentifier, State, StrLit, Syntax, SyntaxBuilder, Target, WithSpan, }; fn as_path<'a>(path: &'a [&'a str]) -> Vec> { path.iter() .map(|name| PathComponent { name: WithSpan::no_span(name), generics: None, }) .collect::>() } #[test] fn test_ws_splitter() { #[track_caller] fn check_ws_split(s: &str, &(lws, val, rws): &(&str, &str, &str)) { let s = Lit::split_ws_parts(WithSpan::no_span(s)); assert_eq!(*s.lws, lws); assert_eq!(*s.val, val); assert_eq!(*s.rws, rws); } check_ws_split("", &("", "", "")); check_ws_split("a", &("", "a", "")); check_ws_split("\ta", &("\t", "a", "")); check_ws_split("b\n", &("", "b", "\n")); check_ws_split(" \t\r\n", &(" \t\r\n", "", "")); } #[test] #[should_panic] fn test_invalid_block() { let syntax = Syntax::default(); Ast::from_str("{% extend \"blah\" %}", None, &syntax).unwrap(); } fn int_lit<'a>(i: &'a str) -> WithSpan>> { WithSpan::no_span(Box::new(Expr::NumLit(i, Num::Int(i, None)))) } fn bin_op<'a>( op: &'a str, lhs: WithSpan>>, rhs: WithSpan>>, ) -> WithSpan>> { WithSpan::no_span(Box::new(Expr::BinOp(crate::expr::BinOp { op, lhs, rhs }))) } fn call<'a>( path: WithSpan>>, args: Vec>>>, ) -> WithSpan>> { WithSpan::no_span(Box::new(Expr::Call(crate::expr::Call { path, generics: None, args, }))) } #[test] fn test_parse_filter() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ strvar|e }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("e")), arguments: vec![WithSpan::no_span(Box::new(Expr::Var("strvar")))], }))), ))], ); assert_eq!( Ast::from_str("{{ 2|abs }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("abs")), arguments: vec![int_lit("2")], }))), ))], ); assert_eq!( Ast::from_str("{{ -2|abs }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("abs")), arguments: vec![WithSpan::no_span(Box::new(Expr::Unary("-", int_lit("2"))))], }))), ))], ); assert_eq!( Ast::from_str("{{ (1 - 2)|abs }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("abs")), arguments: vec![WithSpan::no_span(Box::new(Expr::Group(bin_op( "-", int_lit("1"), int_lit("2"), ))))], }))), ))], ); } #[test] fn test_parse_numbers() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ 2 }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr(Ws(None, None), int_lit("2")))], ); assert_eq!( Ast::from_str("{{ 2.5 }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::NumLit("2.5", Num::Float("2.5", None)))) ))], ); } #[test] fn test_parse_var() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ foo }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Var("foo"))) ))] ); assert_eq!( Ast::from_str("{{ foo_bar }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Var("foo_bar"))) ))], ); assert_eq!( Ast::from_str("{{ none }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Var("none"))) ))] ); } #[test] fn test_parse_const() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ FOO }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Path(as_path(&["FOO"])))) ))] ); assert_eq!( Ast::from_str("{{ FOO_BAR }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Path(as_path(&["FOO_BAR"])))) ))], ); assert_eq!( Ast::from_str("{{ NONE }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Path(as_path(&["NONE"])))) ))] ); } #[test] fn test_parse_path() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ None }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Path(as_path(&["None"])))), ))] ); assert_eq!( Ast::from_str("{{ Some(123) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Path(as_path(&["Some"])))), vec![int_lit("123")], ), ))], ); assert_eq!( Ast::from_str("{{ Ok(123) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Path(as_path(&["Ok"])))), vec![int_lit("123")], ), ))], ); assert_eq!( Ast::from_str("{{ Err(123) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Path(as_path(&["Err"])))), vec![int_lit("123")], ), ))], ); } #[test] fn test_parse_var_call() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ function(\"123\", 3) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Var("function"))), vec![ WithSpan::no_span(Box::new(Expr::StrLit(StrLit { content: "123", prefix: None, contains_null: false, contains_unicode_character: false, contains_unicode_escape: false, contains_high_ascii: false, }))), int_lit("3") ], ), ))], ); } #[test] fn test_parse_path_call() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ Option::None }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Path(as_path(&["Option", "None"])))), ))], ); assert_eq!( Ast::from_str("{{ Option::Some(123) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Path(as_path(&["Option", "Some"])))), vec![int_lit("123")], ) ))], ); assert_eq!( Ast::from_str("{{ self::function(\"123\", 3) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Path(as_path(&["self", "function"])))), vec![ WithSpan::no_span(Box::new(Expr::StrLit(StrLit { content: "123", prefix: None, contains_null: false, contains_unicode_character: false, contains_unicode_escape: false, contains_high_ascii: false, }))), int_lit("3") ], ) ))], ); } #[test] fn test_parse_root_path() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ std::string::String::new() }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Path(as_path(&[ "std", "string", "String", "new" ])))), vec![], ), ))], ); assert_eq!( Ast::from_str("{{ ::std::string::String::new() }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Path(as_path(&[ "", "std", "string", "String", "new" ])))), vec![], ), ))], ); } #[test] fn test_rust_macro() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ vec!(1, 2, 3) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("vec")], WithSpan::no_span("1, 2, 3") ))), ))], ); assert_eq!( Ast::from_str("{{ alloc::vec!(1, 2, 3) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("alloc"), WithSpan::no_span("vec")], WithSpan::no_span("1, 2, 3") ))), ))], ); assert_eq!( Ast::from_str("{{a!()}}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("a")], WithSpan::no_span("") ))) ))] ); assert_eq!( Ast::from_str("{{a !()}}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("a")], WithSpan::no_span("") ))) ))] ); assert_eq!( Ast::from_str("{{a! ()}}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("a")], WithSpan::no_span("") ))) ))] ); assert_eq!( Ast::from_str("{{a ! ()}}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("a")], WithSpan::no_span("") ))) ))] ); assert_eq!( Ast::from_str("{{A!()}}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("A")], WithSpan::no_span("") ))) ))] ); assert_eq!( &*Ast::from_str("{{a.b.c!( hello )}}", None, &syntax) .unwrap_err() .to_string(), "failed to parse template source near offset 7", ); } #[test] fn change_delimiters_parse_filter() { let syntax = Syntax(InnerSyntax { expr_start: "{=", expr_end: "=}", ..InnerSyntax::default() }); Ast::from_str("{= strvar|e =}", None, &syntax).unwrap(); } #[test] fn unicode_delimiters_in_syntax() { let syntax = Syntax(InnerSyntax { expr_start: "🖎", // U+1F58E == b"\xf0\x9f\x96\x8e" expr_end: "✍", // U+270D = b'\xe2\x9c\x8d' ..InnerSyntax::default() }); assert_eq!( Ast::from_str("Here comes the expression: 🖎 e ✍.", None, &syntax) .unwrap() .nodes(), [ Box::new(Node::Lit(WithSpan::no_span(Lit { lws: WithSpan::no_span(""), val: WithSpan::no_span("Here comes the expression:"), rws: WithSpan::no_span(" "), }))), Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Var("e"))) )), Box::new(Node::Lit(WithSpan::no_span(Lit { lws: WithSpan::no_span(""), val: WithSpan::no_span("."), rws: WithSpan::no_span(""), }))), ], ); } #[test] fn test_precedence() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ a + b == c }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "==", bin_op( "+", WithSpan::no_span(Box::new(Expr::Var("a"))), WithSpan::no_span(Box::new(Expr::Var("b"))) ), WithSpan::no_span(Box::new(Expr::Var("c"))) ) ))], ); assert_eq!( Ast::from_str("{{ a + b * c - d / e }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "-", bin_op( "+", WithSpan::no_span(Box::new(Expr::Var("a"))), bin_op( "*", WithSpan::no_span(Box::new(Expr::Var("b"))), WithSpan::no_span(Box::new(Expr::Var("c"))) ) ), bin_op( "/", WithSpan::no_span(Box::new(Expr::Var("d"))), WithSpan::no_span(Box::new(Expr::Var("e"))) ), ) ))], ); assert_eq!( Ast::from_str("{{ a * (b + c) / -d }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "/", bin_op( "*", WithSpan::no_span(Box::new(Expr::Var("a"))), WithSpan::no_span(Box::new(Expr::Group(bin_op( "+", WithSpan::no_span(Box::new(Expr::Var("b"))), WithSpan::no_span(Box::new(Expr::Var("c"))) )))) ), WithSpan::no_span(Box::new(Expr::Unary( "-", WithSpan::no_span(Box::new(Expr::Var("d"))) ))) ) ))], ); assert_eq!( Ast::from_str("{{ a || b && c || d && e }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "||", bin_op( "||", WithSpan::no_span(Box::new(Expr::Var("a"))), bin_op( "&&", WithSpan::no_span(Box::new(Expr::Var("b"))), WithSpan::no_span(Box::new(Expr::Var("c"))) ), ), bin_op( "&&", WithSpan::no_span(Box::new(Expr::Var("d"))), WithSpan::no_span(Box::new(Expr::Var("e"))) ), ) ))], ); } #[test] fn test_associativity() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ a + b + c }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "+", bin_op( "+", WithSpan::no_span(Box::new(Expr::Var("a"))), WithSpan::no_span(Box::new(Expr::Var("b"))) ), WithSpan::no_span(Box::new(Expr::Var("c"))) ) ))], ); assert_eq!( Ast::from_str("{{ a * b * c }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "*", bin_op( "*", WithSpan::no_span(Box::new(Expr::Var("a"))), WithSpan::no_span(Box::new(Expr::Var("b"))) ), WithSpan::no_span(Box::new(Expr::Var("c"))) ) ))], ); assert_eq!( Ast::from_str("{{ a && b && c }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "&&", bin_op( "&&", WithSpan::no_span(Box::new(Expr::Var("a"))), WithSpan::no_span(Box::new(Expr::Var("b"))) ), WithSpan::no_span(Box::new(Expr::Var("c"))) ) ))], ); assert_eq!( Ast::from_str("{{ a + b - c + d }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "+", bin_op( "-", bin_op( "+", WithSpan::no_span(Box::new(Expr::Var("a"))), WithSpan::no_span(Box::new(Expr::Var("b"))) ), WithSpan::no_span(Box::new(Expr::Var("c"))) ), WithSpan::no_span(Box::new(Expr::Var("d"))) ) ))], ); } #[test] fn test_odd_calls() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ a[b](c) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Index( WithSpan::no_span(Box::new(Expr::Var("a"))), WithSpan::no_span(Box::new(Expr::Var("b"))) ))), vec![WithSpan::no_span(Box::new(Expr::Var("c")))], ) ))], ); assert_eq!( Ast::from_str("{{ (a + b)(c) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Group(bin_op( "+", WithSpan::no_span(Box::new(Expr::Var("a"))), WithSpan::no_span(Box::new(Expr::Var("b"))) )))), vec![WithSpan::no_span(Box::new(Expr::Var("c")))], ) ))], ); assert_eq!( Ast::from_str("{{ a + b(c) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), bin_op( "+", WithSpan::no_span(Box::new(Expr::Var("a"))), call( WithSpan::no_span(Box::new(Expr::Var("b"))), vec![WithSpan::no_span(Box::new(Expr::Var("c")))], ), ), ))], ); assert_eq!( Ast::from_str("{{ (-a)(b) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), call( WithSpan::no_span(Box::new(Expr::Group(WithSpan::no_span(Box::new( Expr::Unary("-", WithSpan::no_span(Box::new(Expr::Var("a")))) ))))), vec![WithSpan::no_span(Box::new(Expr::Var("b")))], ) ))], ); assert_eq!( Ast::from_str("{{ -a(b) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Unary( "-", call( WithSpan::no_span(Box::new(Expr::Var("a"))), vec![WithSpan::no_span(Box::new(Expr::Var("b")))], ) ))) ))], ); assert_eq!( Ast::from_str("{{ a(b)|c }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("c")), arguments: vec![call( WithSpan::no_span(Box::new(Expr::Var("a"))), vec![WithSpan::no_span(Box::new(Expr::Var("b")))], )], }))) ))] ); assert_eq!( Ast::from_str("{{ a(b)| c }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("c")), arguments: vec![call( WithSpan::no_span(Box::new(Expr::Var("a"))), vec![WithSpan::no_span(Box::new(Expr::Var("b")))], )], }))), ))] ); } #[test] fn test_parse_comments() { #[track_caller] fn one_comment_ws(source: &str, ws: Ws) { let syntax = Syntax::default(); let mut nodes = Ast::from_str(source, None, &syntax).unwrap().nodes; assert_eq!(nodes.len(), 1, "expected to parse one node"); match *nodes.pop().unwrap() { Node::Comment(comment) => assert_eq!(comment.ws, ws), node => panic!("expected a comment not, but parsed {node:?}"), } } one_comment_ws("{##}", Ws(None, None)); one_comment_ws("{#- #}", Ws(Some(Whitespace::Suppress), None)); one_comment_ws("{# -#}", Ws(None, Some(Whitespace::Suppress))); one_comment_ws( "{#--#}", Ws(Some(Whitespace::Suppress), Some(Whitespace::Suppress)), ); one_comment_ws( "{#- foo\n bar -#}", Ws(Some(Whitespace::Suppress), Some(Whitespace::Suppress)), ); one_comment_ws( "{#- foo\n {#- bar\n -#} baz -#}", Ws(Some(Whitespace::Suppress), Some(Whitespace::Suppress)), ); one_comment_ws("{#+ #}", Ws(Some(Whitespace::Preserve), None)); one_comment_ws("{# +#}", Ws(None, Some(Whitespace::Preserve))); one_comment_ws( "{#++#}", Ws(Some(Whitespace::Preserve), Some(Whitespace::Preserve)), ); one_comment_ws( "{#+ foo\n bar +#}", Ws(Some(Whitespace::Preserve), Some(Whitespace::Preserve)), ); one_comment_ws( "{#+ foo\n {#+ bar\n +#} baz -+#}", Ws(Some(Whitespace::Preserve), Some(Whitespace::Preserve)), ); one_comment_ws("{#~ #}", Ws(Some(Whitespace::Minimize), None)); one_comment_ws("{# ~#}", Ws(None, Some(Whitespace::Minimize))); one_comment_ws( "{#~~#}", Ws(Some(Whitespace::Minimize), Some(Whitespace::Minimize)), ); one_comment_ws( "{#~ foo\n bar ~#}", Ws(Some(Whitespace::Minimize), Some(Whitespace::Minimize)), ); one_comment_ws( "{#~ foo\n {#~ bar\n ~#} baz -~#}", Ws(Some(Whitespace::Minimize), Some(Whitespace::Minimize)), ); one_comment_ws("{# foo {# bar #} {# {# baz #} qux #} #}", Ws(None, None)); } #[test] fn test_parse_tuple() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ () }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Tuple(vec![]))) ))], ); assert_eq!( Ast::from_str("{{ (1) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Group(int_lit("1")))) ))], ); assert_eq!( Ast::from_str("{{ (1,) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Tuple(vec![int_lit("1")]))), ))], ); assert_eq!( Ast::from_str("{{ (1, ) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Tuple(vec![int_lit("1")]))), ))], ); assert_eq!( Ast::from_str("{{ (1 ,) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Tuple(vec![int_lit("1")]))), ))], ); assert_eq!( Ast::from_str("{{ (1 , ) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Tuple(vec![int_lit("1")]))), ))], ); assert_eq!( Ast::from_str("{{ (1, 2) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Tuple(vec![int_lit("1"), int_lit("2")]))), ))], ); assert_eq!( Ast::from_str("{{ (1, 2,) }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Tuple(vec![int_lit("1"), int_lit("2")]))), ))], ); assert_eq!( Ast::from_str("{{ (1, 2, 3) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Tuple(vec![ int_lit("1"), int_lit("2"), int_lit("3") ]))), ))], ); assert_eq!( Ast::from_str("{{ ()|abs }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("abs")), arguments: vec![WithSpan::no_span(Box::new(Expr::Tuple(vec![])))], }))), ))], ); assert_eq!( Ast::from_str("{{ (1)|abs }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("abs")), arguments: vec![WithSpan::no_span(Box::new(Expr::Group(int_lit("1"))))], }))), ))], ); assert_eq!( Ast::from_str("{{ (1,)|abs }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("abs")), arguments: vec![WithSpan::no_span(Box::new(Expr::Tuple(vec![int_lit("1")])))], }))), ))], ); assert_eq!( Ast::from_str("{{ (1, 2)|abs }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("abs")), arguments: vec![WithSpan::no_span(Box::new(Expr::Tuple(vec![ int_lit("1"), int_lit("2") ])))], }))), ))], ); } #[test] fn test_missing_space_after_kw() { let syntax = Syntax::default(); let err = Ast::from_str("{%leta=b%}", None, &syntax).unwrap_err(); assert_eq!( err.to_string(), "unknown node `leta`\nfailed to parse template source near offset 2", ); } #[test] fn test_parse_array() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ [] }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![]))) ))], ); assert_eq!( Ast::from_str("{{ [1] }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("1")]))) ))], ); assert_eq!( Ast::from_str("{{ [ 1] }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("1")]))) ))], ); assert_eq!( Ast::from_str("{{ [1 ] }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("1")]))) ))], ); assert_eq!( Ast::from_str("{{ [1,2] }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("1"), int_lit("2")]))) ))], ); assert_eq!( Ast::from_str("{{ [1 ,2] }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("1"), int_lit("2")]))) ))], ); assert_eq!( Ast::from_str("{{ [1, 2] }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("1"), int_lit("2")]))) ))], ); assert_eq!( Ast::from_str("{{ [1,2 ] }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("1"), int_lit("2")]))) ))], ); assert_eq!( Ast::from_str("{{ []|foo }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("foo")), arguments: vec![WithSpan::no_span(Box::new(Expr::Array(vec![])))], }))) ))], ); assert_eq!( Ast::from_str("{{ []| foo }}", None, &syntax).unwrap().nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("foo")), arguments: vec![WithSpan::no_span(Box::new(Expr::Array(vec![])))], }))) ))], ); let n = || { Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Array(vec![WithSpan::no_span(Box::new( Expr::NumLit("1", Num::Int("1", None)), ))]))), )) }; assert_eq!( Ast::from_str( "{{ [1,] }}{{ [1 ,] }}{{ [1, ] }}{{ [1 , ] }}", None, &syntax ) .unwrap() .nodes, [n(), n(), n(), n()], ); } #[test] fn fuzzed_unicode_slice() { let d = "{eeuuu{b&{!!&{!!11{{ 0!(!1q҄א!)!!!!!!n!"; assert!(Ast::from_str(d, None, &Syntax::default()).is_err()); } #[test] fn fuzzed_macro_no_end() { let s = "{%macro super%}{%endmacro"; assert!(Ast::from_str(s, None, &Syntax::default()).is_err()); } #[test] fn fuzzed_target_recursion() { const TEMPLATE: &str = include_str!("../tests/target-recursion.txt"); assert!(Ast::from_str(TEMPLATE, None, &Syntax::default()).is_err()); } #[test] fn fuzzed_unary_recursion() { const TEMPLATE: &str = include_str!("../tests/unary-recursion.txt"); assert!(Ast::from_str(TEMPLATE, None, &Syntax::default()).is_err()); } #[test] fn fuzzed_comment_depth() { let syntax = Syntax::default(); let (sender, receiver) = std::sync::mpsc::channel(); let test = std::thread::spawn(move || { const TEMPLATE: &str = include_str!("../tests/comment-depth.txt"); assert!(Ast::from_str(TEMPLATE, None, &syntax).is_ok()); sender.send(()).unwrap(); }); receiver .recv_timeout(std::time::Duration::from_secs(3)) .expect("timeout"); test.join().unwrap(); } #[test] fn let_set() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{% let a %}", None, &syntax).unwrap().nodes(), Ast::from_str("{% set a %}", None, &syntax).unwrap().nodes(), ); } #[test] fn fuzzed_filter_recursion() { const TEMPLATE: &str = include_str!("../tests/filter-recursion.txt"); assert!(Ast::from_str(TEMPLATE, None, &Syntax::default()).is_err()); } #[test] fn fuzzed_excessive_syntax_lengths() { const LONG_DELIM: Option<&str> = Some("\0]***NEWFILE\u{1f}***:7/v/.-3/\u{1b}/~~~~z~0/*:7/v/./t/t/.p//NEWVILE**::7/v"); for (kind, syntax_builder) in [ ( "opening block", SyntaxBuilder { block_start: LONG_DELIM, ..SyntaxBuilder::default() }, ), ( "closing block", SyntaxBuilder { block_end: LONG_DELIM, ..SyntaxBuilder::default() }, ), ( "opening expression", SyntaxBuilder { expr_start: LONG_DELIM, ..SyntaxBuilder::default() }, ), ( "closing expression", SyntaxBuilder { expr_end: LONG_DELIM, ..SyntaxBuilder::default() }, ), ( "opening comment", SyntaxBuilder { comment_start: LONG_DELIM, ..SyntaxBuilder::default() }, ), ( "closing comment", SyntaxBuilder { comment_end: LONG_DELIM, ..SyntaxBuilder::default() }, ), ] { let err = syntax_builder.to_syntax().unwrap_err(); assert_eq!( err, format!( "delimiters must be at most 32 characters long. The {kind} delimiter \ (\"\\0]***NEWFILE\\u{{1f}}***\"...) is too long" ), ); } } #[test] fn extends_with_whitespace_control() { const CONTROL: &[&str] = &["", "\t", "-", "+", "~"]; let syntax = Syntax::default(); let expected = Ast::from_str(r#"front {% extends "nothing" %} back"#, None, &syntax).unwrap(); for front in CONTROL { for back in CONTROL { let src = format!(r#"front {{%{front} extends "nothing" {back}%}} back"#); let actual = Ast::from_str(&src, None, &syntax).unwrap(); assert_eq!(expected.nodes(), actual.nodes(), "source: {src:?}"); } } } #[test] fn fuzzed_span_is_not_substring_of_source() { let _: Result, crate::ParseError> = Ast::from_str( include_str!("../tests/fuzzed_span_is_not_substring_of_source.bin"), None, &Syntax::default(), ); } #[test] fn fuzzed_excessive_filter_block() { let src = include_str!("../tests/excessive_filter_block.txt"); let err = Ast::from_str(src, None, &Syntax::default()).unwrap_err(); assert_eq!( err.to_string().lines().next(), Some("your template code is too deeply nested, or the last expression is too complex"), ); } #[test] fn test_generics_parsing() { let syntax = Syntax::default(); // Method call. Ast::from_str("{{ a.b::<&str, H>>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<&str, H , &u32>>() }}", None, &syntax).unwrap(); // Call. Ast::from_str("{{ a::<&str, H , &u32>>() }}", None, &syntax).unwrap(); // Filter. Ast::from_str("{{ 12 | a::<&str> }}", None, &syntax).unwrap(); Ast::from_str("{{ 12 | a::<&str, u32>('a') }}", None, &syntax).unwrap(); // Unclosed `<`. assert!(Ast::from_str("{{ a.b::<&str, H , &u32>() }}", None, &syntax).is_err()); // With path and spaces Ast::from_str("{{ a.b::<&&core::primitive::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b ::<&&core::primitive::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b:: <&&core::primitive::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::< &&core::primitive::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<& &core::primitive::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<&& core::primitive::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<&&core ::primitive::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<&&core:: primitive::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<&&core::primitive ::str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<&&core::primitive:: str>() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<&&core::primitive::str >() }}", None, &syntax).unwrap(); Ast::from_str("{{ a.b::<&&core::primitive::str> () }}", None, &syntax).unwrap(); } #[test] fn fuzzed_deeply_tested_if_let() { let src = include_str!("../tests/fuzzed-deeply-tested-if-let.txt"); let syntax = Syntax::default(); let err = Ast::from_str(src, None, &syntax).unwrap_err(); assert_eq!( err.to_string().lines().next(), Some("your template code is too deeply nested, or the last expression is too complex"), ); } #[test] fn test_filter_with_path() { let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ strvar|::e }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Path(as_path(&["", "e"])), arguments: vec![WithSpan::no_span(Box::new(Expr::Var("strvar")))], }))), ))], ); assert_eq!( Ast::from_str("{{ strvar|::e::f }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Path(as_path(&["", "e", "f"])), arguments: vec![WithSpan::no_span(Box::new(Expr::Var("strvar")))], }))), ))], ); assert_eq!( Ast::from_str("{{ strvar|e::f }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Path(as_path(&["e", "f"])), arguments: vec![WithSpan::no_span(Box::new(Expr::Var("strvar")))], }))), ))], ); assert_eq!( Ast::from_str("{{ strvar|e::f() }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Path(as_path(&["e", "f"])), arguments: vec![WithSpan::no_span(Box::new(Expr::Var("strvar")))], }))), ))], ); assert_eq!( Ast::from_str("{{ strvar|e()::f }}", None, &syntax) .unwrap_err() .to_string(), "failed to parse template source near offset 13", ); assert_eq!( Ast::from_str("{{ strvar|e::f()::g }}", None, &syntax) .unwrap_err() .to_string(), "failed to parse template source near offset 16", ); } #[test] fn underscore_is_an_identifier() { let state = State { syntax: Syntax::default(), loop_depth: Cell::new(0), level: Level::default(), }; let mut input = InputStream { input: LocatingSlice::new("_"), state: &state, }; let result = crate::identifier.parse_next(&mut input); assert_eq!(result.unwrap(), "_"); assert_eq!(**input, ""); } #[test] fn there_is_no_digit_two_in_a_binary_integer() { let syntax = Syntax::default(); assert!(Ast::from_str("{{ 0b2 }}", None, &syntax).is_err()); assert!(Ast::from_str("{{ 0o9 }}", None, &syntax).is_err()); assert!(Ast::from_str("{{ 0xg }}", None, &syntax).is_err()); } #[test] fn comparison_operators_cannot_be_chained() { const OPS: &[&str] = &["==", "!=", ">=", ">", "<=", "<"]; let syntax = Syntax::default(); for op1 in OPS { assert!(Ast::from_str(&format!("{{{{ a {op1} b }}}}"), None, &syntax).is_ok()); for op2 in OPS { assert!(Ast::from_str(&format!("{{{{ a {op1} b {op2} c }}}}"), None, &syntax).is_err()); for op3 in OPS { assert!( Ast::from_str( &format!("{{{{ a {op1} b {op2} c {op3} d }}}}"), None, &syntax, ) .is_err() ); } } } } #[test] fn macro_calls_can_have_raw_prefixes() { // Related to issue . let syntax = Syntax::default(); let inner = r####"r#"test"# r##"test"## r###"test"### r#loop"####; assert_eq!( Ast::from_str(&format!("{{{{ z!{{{inner}}} }}}}"), None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("z")], WithSpan::no_span(inner) ))), ))], ); } #[test] fn macro_comments_in_macro_calls() { // Related to . let syntax = Syntax::default(); assert!(Ast::from_str("{{ e!(// hello) }}", None, &syntax).is_err()); assert!(Ast::from_str("{{ e!(/// hello) }}", None, &syntax).is_err()); assert!(Ast::from_str("{{ e!(// hello)\n }}", None, &syntax).is_err()); assert!(Ast::from_str("{{ e!(/// hello)\n }}", None, &syntax).is_err()); assert_eq!( Ast::from_str("{{ e!(// hello\n) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("e")], WithSpan::no_span("// hello\n") ))), ))], ); assert_eq!( Ast::from_str("{{ e!(/// hello\n) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("e")], WithSpan::no_span("/// hello\n") ))), ))], ); assert_eq!( Ast::from_str("{{ e!(//! hello\n) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("e")], WithSpan::no_span("//! hello\n") ))), ))], ); assert_eq!( Ast::from_str("{{ e!(/* hello */) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("e")], WithSpan::no_span("/* hello */") ))), ))], ); assert_eq!( Ast::from_str("{{ e!(/** hello */) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("e")], WithSpan::no_span("/** hello */") ))), ))], ); assert_eq!( Ast::from_str("{{ e!(/*! hello */) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("e")], WithSpan::no_span("/*! hello */") ))), ))], ); } #[test] fn test_raw() { let syntax = Syntax::default(); let val = "hello {{ endraw %} my {%* endraw %} green {% endraw }} world"; assert_eq!( Ast::from_str( &format!("{{%+ raw -%}} {val} {{%~ endraw ~%}}"), None, &syntax ) .unwrap() .nodes, [Box::new(Node::Raw(WithSpan::no_span(Raw { ws1: Ws(Some(Whitespace::Preserve), Some(Whitespace::Suppress)), lit: WithSpan::no_span(Lit { lws: WithSpan::no_span(" "), val: WithSpan::no_span(val), rws: WithSpan::no_span(" "), }), ws2: Ws(Some(Whitespace::Minimize), Some(Whitespace::Minimize)), })))], ); // We must make sure that the character for whitespace handling, e.g. `-` is not consumed, // unless `{% endraw %}` was actually found. Otherwise opening block delimiters that begin with // `-`, `~` or `+` may break. let syntax = SyntaxBuilder { block_start: Some("-$"), block_end: Some("$-"), ..SyntaxBuilder::default() }; let syntax = syntax.to_syntax().unwrap(); assert_eq!( Ast::from_str("-$- raw -$- -$- endraw -$ endraw -$-", None, &syntax) .unwrap() .nodes, [Box::new(Node::Raw(WithSpan::no_span(Raw { ws1: Ws(Some(Whitespace::Suppress), Some(Whitespace::Suppress)), lit: WithSpan::no_span(Lit { lws: WithSpan::no_span(" "), val: WithSpan::no_span("-$- endraw"), rws: WithSpan::no_span(" "), }), ws2: Ws(None, Some(Whitespace::Suppress)), })))], ); } #[test] fn test_macro_call_nested_comments() { // Regression test for . let syntax = Syntax::default(); assert_eq!( Ast::from_str("{{ x!(/*/*/*)*/*/*/) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("x")], WithSpan::no_span("/*/*/*)*/*/*/") ))), ))], ); let msg = Ast::from_str("{{ x!(/*/*/) }}", None, &syntax) .unwrap_err() .to_string(); assert!(msg.contains("missing `*/` to close block comment")); assert_eq!( Ast::from_str("{{ x!(/**/) }}", None, &syntax) .unwrap() .nodes, [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::RustMacro( vec![WithSpan::no_span("x")], WithSpan::no_span("/**/") ))), ))], ); } #[test] fn test_try_reserved_raw_identifier() { // Regression test for . let syntax = Syntax::default(); for id in ["crate", "super", "Self"] { let msg = format!("`{id}` cannot be used as an identifier"); assert!( Ast::from_str(&format!("{{{{ {id}? }}}}"), None, &syntax) .unwrap_err() .to_string() .contains(&msg), ); assert!( Ast::from_str(&format!("{{{{ {id}|filter }}}}"), None, &syntax) .unwrap_err() .to_string() .contains(&msg), ); assert!( Ast::from_str( &format!("{{{{ var|filter(arg1, {id}, arg3) }}}}"), None, &syntax ) .unwrap_err() .to_string() .contains(&msg), ); assert!( Ast::from_str( &format!("{{{{ var|filter(arg1=arg1, arg2={id}, arg3=arg3) }}}}"), None, &syntax ) .unwrap_err() .to_string() .contains(&msg), ); } } #[test] fn test_isolated_cr_in_raw_string() { // Regression test for . let syntax = Syntax::default(); assert!( Ast::from_str("{{ x!(\"hello\rworld\") }}", None, &syntax) .unwrap_err() .to_string() .contains("a bare CR (Mac linebreak) is not allowed in string literals"), ); assert!( Ast::from_str("{{ x!(c\"hello\rworld\") }}", None, &syntax) .unwrap_err() .to_string() .contains("a bare CR (Mac linebreak) is not allowed in string literals"), ); assert!( Ast::from_str("{{ x!(b\"hello\rworld\") }}", None, &syntax) .unwrap_err() .to_string() .contains("a bare CR (Mac linebreak) is not allowed in string literals"), ); assert!( Ast::from_str("{{ x!(r\"hello\rworld\") }}", None, &syntax) .unwrap_err() .to_string() .contains("a bare CR (Mac linebreak) is not allowed in string literals"), ); assert!( Ast::from_str("{{ x!(cr\"hello\rworld\") }}", None, &syntax) .unwrap_err() .to_string() .contains("a bare CR (Mac linebreak) is not allowed in string literals"), ); assert!( Ast::from_str("{{ x!(br\"hello\rworld\") }}", None, &syntax) .unwrap_err() .to_string() .contains("a bare CR (Mac linebreak) is not allowed in string literals"), ); } #[test] fn test_macro_call_illegal_raw_identifier() { // Regression test for . let syntax = Syntax::default(); for id in ["crate", "self", "Self", "super"] { assert!( Ast::from_str(&format!("{{{{ z!(r#{id}) }}}}"), None, &syntax) .unwrap_err() .to_string() .contains("cannot be a raw identifier"), ); } } #[test] fn regression_tests_span_change() { // This test contains regression test for errors occurred during the big refactoring: // "Add a nightly feature which allows to manipulate spans to underline which part of the // template is failing compilation" let syntax = Syntax::default(); assert_eq!( Ast::from_str("{%- let [_] = [2] -%}", None, &syntax) .unwrap() .nodes(), [Box::new(Node::Let(WithSpan::no_span(Let { ws: Ws(Some(Whitespace::Suppress), Some(Whitespace::Suppress)), var: Target::Array(WithSpan::no_span(vec![Target::Placeholder( WithSpan::no_span(()) )])), val: Some(WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("2")])))), is_mutable: false, })))], ); assert_eq!( Ast::from_str("{%- let (_) = [2] -%}", None, &syntax) .unwrap() .nodes(), [Box::new(Node::Let(WithSpan::no_span(Let { ws: Ws(Some(Whitespace::Suppress), Some(Whitespace::Suppress)), var: Target::Placeholder(WithSpan::no_span(())), val: Some(WithSpan::no_span(Box::new(Expr::Array(vec![int_lit("2")])))), is_mutable: false, })))], ); assert_eq!( Ast::from_str("Hello, {{ user | cased }}!", None, &syntax) .unwrap() .nodes(), [ Box::new(Node::Lit(WithSpan::no_span(Lit { lws: WithSpan::no_span(""), val: WithSpan::no_span("Hello,"), rws: WithSpan::no_span(" "), }))), Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Filter(Filter { name: PathOrIdentifier::Identifier(WithSpan::no_span("cased")), arguments: vec![WithSpan::no_span(Box::new(Expr::Var("user")))], }))) )), Box::new(Node::Lit(WithSpan::no_span(Lit { lws: WithSpan::no_span(""), val: WithSpan::no_span("!"), rws: WithSpan::no_span(""), }))), ], ); assert_eq!( Ast::from_str("{{ ( 0 + 1 ) }}", None, &syntax) .unwrap() .nodes(), [Box::new(Node::Expr( Ws(None, None), WithSpan::no_span(Box::new(Expr::Group(WithSpan::no_span(Box::new( Expr::BinOp(BinOp { op: "+", lhs: int_lit("0"), rhs: int_lit("1"), }) ))))) ))] ); }