mirror of
https://github.com/askama-rs/askama.git
synced 2025-09-27 13:00:57 +00:00
1713 lines
53 KiB
Rust
1713 lines
53 KiB
Rust
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<PathComponent<'a>> {
|
|
path.iter()
|
|
.map(|name| PathComponent {
|
|
name: WithSpan::no_span(name),
|
|
generics: None,
|
|
})
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
#[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<Box<Expr<'a>>> {
|
|
WithSpan::no_span(Box::new(Expr::NumLit(i, Num::Int(i, None))))
|
|
}
|
|
|
|
fn bin_op<'a>(
|
|
op: &'a str,
|
|
lhs: WithSpan<Box<Expr<'a>>>,
|
|
rhs: WithSpan<Box<Expr<'a>>>,
|
|
) -> WithSpan<Box<Expr<'a>>> {
|
|
WithSpan::no_span(Box::new(Expr::BinOp(crate::expr::BinOp { op, lhs, rhs })))
|
|
}
|
|
|
|
fn call<'a>(
|
|
path: WithSpan<Box<Expr<'a>>>,
|
|
args: Vec<WithSpan<Box<Expr<'a>>>>,
|
|
) -> WithSpan<Box<Expr<'a>>> {
|
|
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<Ast<'_>, 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<B<C>>>() }}", None, &syntax).unwrap();
|
|
Ast::from_str("{{ a.b::<&str, H<B<C> , &u32>>() }}", None, &syntax).unwrap();
|
|
|
|
// Call.
|
|
Ast::from_str("{{ a::<&str, H<B<C> , &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<B<C> , &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 <https://github.com/askama-rs/askama/issues/475>.
|
|
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 <https://issues.oss-fuzz.com/issues/425145246>.
|
|
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 <https://issues.oss-fuzz.com/issues/427825995>.
|
|
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 <https://issues.oss-fuzz.com/issues/429130577>.
|
|
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 <https://issues.oss-fuzz.com/issues/429645376>.
|
|
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 <https://issues.oss-fuzz.com/issues/435218013>.
|
|
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" <https://github.com/askama-rs/askama/issues/420>
|
|
|
|
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"),
|
|
})
|
|
)))))
|
|
))]
|
|
);
|
|
}
|