aboutsummaryrefslogtreecommitdiffstats
path: root/askama_derive/src/parser
diff options
context:
space:
mode:
Diffstat (limited to 'askama_derive/src/parser')
-rw-r--r--askama_derive/src/parser/expr.rs346
-rw-r--r--askama_derive/src/parser/mod.rs314
-rw-r--r--askama_derive/src/parser/node.rs671
-rw-r--r--askama_derive/src/parser/tests.rs668
4 files changed, 1999 insertions, 0 deletions
diff --git a/askama_derive/src/parser/expr.rs b/askama_derive/src/parser/expr.rs
new file mode 100644
index 0000000..4546e04
--- /dev/null
+++ b/askama_derive/src/parser/expr.rs
@@ -0,0 +1,346 @@
+use std::str;
+
+use nom::branch::alt;
+use nom::bytes::complete::{tag, take_till};
+use nom::character::complete::char;
+use nom::combinator::{cut, map, not, opt, peek, recognize};
+use nom::multi::{fold_many0, many0, separated_list0, separated_list1};
+use nom::sequence::{delimited, pair, preceded, terminated, tuple};
+use nom::IResult;
+
+use super::{
+ bool_lit, char_lit, identifier, nested_parenthesis, not_ws, num_lit, path, str_lit, ws,
+};
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum Expr<'a> {
+ BoolLit(&'a str),
+ NumLit(&'a str),
+ StrLit(&'a str),
+ CharLit(&'a str),
+ Var(&'a str),
+ Path(Vec<&'a str>),
+ Array(Vec<Expr<'a>>),
+ Attr(Box<Expr<'a>>, &'a str),
+ Index(Box<Expr<'a>>, Box<Expr<'a>>),
+ Filter(&'a str, Vec<Expr<'a>>),
+ Unary(&'a str, Box<Expr<'a>>),
+ BinOp(&'a str, Box<Expr<'a>>, Box<Expr<'a>>),
+ Range(&'a str, Option<Box<Expr<'a>>>, Option<Box<Expr<'a>>>),
+ Group(Box<Expr<'a>>),
+ Tuple(Vec<Expr<'a>>),
+ Call(Box<Expr<'a>>, Vec<Expr<'a>>),
+ RustMacro(&'a str, &'a str),
+ Try(Box<Expr<'a>>),
+}
+
+impl Expr<'_> {
+ pub(super) fn parse(i: &str) -> IResult<&str, Expr<'_>> {
+ expr_any(i)
+ }
+
+ pub(super) fn parse_arguments(i: &str) -> IResult<&str, Vec<Expr<'_>>> {
+ arguments(i)
+ }
+
+ /// Returns `true` if enough assumptions can be made,
+ /// to determine that `self` is copyable.
+ pub(crate) fn is_copyable(&self) -> bool {
+ self.is_copyable_within_op(false)
+ }
+
+ fn is_copyable_within_op(&self, within_op: bool) -> bool {
+ use Expr::*;
+ match self {
+ BoolLit(_) | NumLit(_) | StrLit(_) | CharLit(_) => true,
+ Unary(.., expr) => expr.is_copyable_within_op(true),
+ BinOp(_, lhs, rhs) => {
+ lhs.is_copyable_within_op(true) && rhs.is_copyable_within_op(true)
+ }
+ Range(..) => true,
+ // The result of a call likely doesn't need to be borrowed,
+ // as in that case the call is more likely to return a
+ // reference in the first place then.
+ Call(..) | Path(..) => true,
+ // If the `expr` is within a `Unary` or `BinOp` then
+ // an assumption can be made that the operand is copy.
+ // If not, then the value is moved and adding `.clone()`
+ // will solve that issue. However, if the operand is
+ // implicitly borrowed, then it's likely not even possible
+ // to get the template to compile.
+ _ => within_op && self.is_attr_self(),
+ }
+ }
+
+ /// Returns `true` if this is an `Attr` where the `obj` is `"self"`.
+ pub(crate) fn is_attr_self(&self) -> bool {
+ match self {
+ Expr::Attr(obj, _) if matches!(obj.as_ref(), Expr::Var("self")) => true,
+ Expr::Attr(obj, _) if matches!(obj.as_ref(), Expr::Attr(..)) => obj.is_attr_self(),
+ _ => false,
+ }
+ }
+
+ /// Returns `true` if the outcome of this expression may be used multiple times in the same
+ /// `write!()` call, without evaluating the expression again, i.e. the expression should be
+ /// side-effect free.
+ pub(crate) fn is_cachable(&self) -> bool {
+ match self {
+ // Literals are the definition of pure:
+ Expr::BoolLit(_) => true,
+ Expr::NumLit(_) => true,
+ Expr::StrLit(_) => true,
+ Expr::CharLit(_) => true,
+ // fmt::Display should have no effects:
+ Expr::Var(_) => true,
+ Expr::Path(_) => true,
+ // Check recursively:
+ Expr::Array(args) => args.iter().all(|arg| arg.is_cachable()),
+ Expr::Attr(lhs, _) => lhs.is_cachable(),
+ Expr::Index(lhs, rhs) => lhs.is_cachable() && rhs.is_cachable(),
+ Expr::Filter(_, args) => args.iter().all(|arg| arg.is_cachable()),
+ Expr::Unary(_, arg) => arg.is_cachable(),
+ Expr::BinOp(_, lhs, rhs) => lhs.is_cachable() && rhs.is_cachable(),
+ Expr::Range(_, lhs, rhs) => {
+ lhs.as_ref().map_or(true, |v| v.is_cachable())
+ && rhs.as_ref().map_or(true, |v| v.is_cachable())
+ }
+ Expr::Group(arg) => arg.is_cachable(),
+ Expr::Tuple(args) => args.iter().all(|arg| arg.is_cachable()),
+ // We have too little information to tell if the expression is pure:
+ Expr::Call(_, _) => false,
+ Expr::RustMacro(_, _) => false,
+ Expr::Try(_) => false,
+ }
+ }
+}
+
+fn expr_bool_lit(i: &str) -> IResult<&str, Expr<'_>> {
+ map(bool_lit, Expr::BoolLit)(i)
+}
+
+fn expr_num_lit(i: &str) -> IResult<&str, Expr<'_>> {
+ map(num_lit, Expr::NumLit)(i)
+}
+
+fn expr_array_lit(i: &str) -> IResult<&str, Expr<'_>> {
+ delimited(
+ ws(char('[')),
+ map(separated_list1(ws(char(',')), expr_any), Expr::Array),
+ ws(char(']')),
+ )(i)
+}
+
+fn expr_str_lit(i: &str) -> IResult<&str, Expr<'_>> {
+ map(str_lit, Expr::StrLit)(i)
+}
+
+fn expr_char_lit(i: &str) -> IResult<&str, Expr<'_>> {
+ map(char_lit, Expr::CharLit)(i)
+}
+
+fn expr_var(i: &str) -> IResult<&str, Expr<'_>> {
+ map(identifier, Expr::Var)(i)
+}
+
+fn expr_path(i: &str) -> IResult<&str, Expr<'_>> {
+ let (i, path) = path(i)?;
+ Ok((i, Expr::Path(path)))
+}
+
+fn expr_group(i: &str) -> IResult<&str, Expr<'_>> {
+ let (i, expr) = preceded(ws(char('(')), opt(expr_any))(i)?;
+ let expr = match expr {
+ Some(expr) => expr,
+ None => {
+ let (i, _) = char(')')(i)?;
+ return Ok((i, Expr::Tuple(vec![])));
+ }
+ };
+
+ let (i, comma) = ws(opt(peek(char(','))))(i)?;
+ if comma.is_none() {
+ let (i, _) = char(')')(i)?;
+ return Ok((i, Expr::Group(Box::new(expr))));
+ }
+
+ let mut exprs = vec![expr];
+ let (i, _) = fold_many0(
+ preceded(char(','), ws(expr_any)),
+ || (),
+ |_, expr| {
+ exprs.push(expr);
+ },
+ )(i)?;
+ let (i, _) = pair(ws(opt(char(','))), char(')'))(i)?;
+ Ok((i, Expr::Tuple(exprs)))
+}
+
+fn expr_single(i: &str) -> IResult<&str, Expr<'_>> {
+ alt((
+ expr_bool_lit,
+ expr_num_lit,
+ expr_str_lit,
+ expr_char_lit,
+ expr_path,
+ expr_rust_macro,
+ expr_array_lit,
+ expr_var,
+ expr_group,
+ ))(i)
+}
+
+enum Suffix<'a> {
+ Attr(&'a str),
+ Index(Expr<'a>),
+ Call(Vec<Expr<'a>>),
+ Try,
+}
+
+fn expr_attr(i: &str) -> IResult<&str, Suffix<'_>> {
+ map(
+ preceded(
+ ws(pair(char('.'), not(char('.')))),
+ cut(alt((num_lit, identifier))),
+ ),
+ Suffix::Attr,
+ )(i)
+}
+
+fn expr_index(i: &str) -> IResult<&str, Suffix<'_>> {
+ map(
+ preceded(ws(char('[')), cut(terminated(expr_any, ws(char(']'))))),
+ Suffix::Index,
+ )(i)
+}
+
+fn expr_call(i: &str) -> IResult<&str, Suffix<'_>> {
+ map(arguments, Suffix::Call)(i)
+}
+
+fn expr_try(i: &str) -> IResult<&str, Suffix<'_>> {
+ map(preceded(take_till(not_ws), char('?')), |_| Suffix::Try)(i)
+}
+
+fn filter(i: &str) -> IResult<&str, (&str, Option<Vec<Expr<'_>>>)> {
+ let (i, (_, fname, args)) = tuple((char('|'), ws(identifier), opt(arguments)))(i)?;
+ Ok((i, (fname, args)))
+}
+
+fn expr_filtered(i: &str) -> IResult<&str, Expr<'_>> {
+ let (i, (obj, filters)) = tuple((expr_prefix, many0(filter)))(i)?;
+
+ let mut res = obj;
+ for (fname, args) in filters {
+ res = Expr::Filter(fname, {
+ let mut args = match args {
+ Some(inner) => inner,
+ None => Vec::new(),
+ };
+ args.insert(0, res);
+ args
+ });
+ }
+
+ Ok((i, res))
+}
+
+fn expr_prefix(i: &str) -> IResult<&str, Expr<'_>> {
+ let (i, (ops, mut expr)) = pair(many0(ws(alt((tag("!"), tag("-"))))), expr_suffix)(i)?;
+ for op in ops.iter().rev() {
+ expr = Expr::Unary(op, Box::new(expr));
+ }
+ Ok((i, expr))
+}
+
+fn expr_suffix(i: &str) -> IResult<&str, Expr<'_>> {
+ let (mut i, mut expr) = expr_single(i)?;
+ loop {
+ let (j, suffix) = opt(alt((expr_attr, expr_index, expr_call, expr_try)))(i)?;
+ i = j;
+ match suffix {
+ Some(Suffix::Attr(attr)) => expr = Expr::Attr(expr.into(), attr),
+ Some(Suffix::Index(index)) => expr = Expr::Index(expr.into(), index.into()),
+ Some(Suffix::Call(args)) => expr = Expr::Call(expr.into(), args),
+ Some(Suffix::Try) => expr = Expr::Try(expr.into()),
+ None => break,
+ }
+ }
+ Ok((i, expr))
+}
+
+fn macro_arguments(i: &str) -> IResult<&str, &str> {
+ delimited(char('('), recognize(nested_parenthesis), char(')'))(i)
+}
+
+fn expr_rust_macro(i: &str) -> IResult<&str, Expr<'_>> {
+ let (i, (mname, _, args)) = tuple((identifier, char('!'), macro_arguments))(i)?;
+ Ok((i, Expr::RustMacro(mname, args)))
+}
+
+macro_rules! expr_prec_layer {
+ ( $name:ident, $inner:ident, $op:expr ) => {
+ fn $name(i: &str) -> IResult<&str, Expr<'_>> {
+ let (i, left) = $inner(i)?;
+ let (i, right) = many0(pair(
+ ws(tag($op)),
+ $inner,
+ ))(i)?;
+ Ok((
+ i,
+ right.into_iter().fold(left, |left, (op, right)| {
+ Expr::BinOp(op, Box::new(left), Box::new(right))
+ }),
+ ))
+ }
+ };
+ ( $name:ident, $inner:ident, $( $op:expr ),+ ) => {
+ fn $name(i: &str) -> IResult<&str, Expr<'_>> {
+ let (i, left) = $inner(i)?;
+ let (i, right) = many0(pair(
+ ws(alt(($( tag($op) ),+,))),
+ $inner,
+ ))(i)?;
+ Ok((
+ i,
+ right.into_iter().fold(left, |left, (op, right)| {
+ Expr::BinOp(op, Box::new(left), Box::new(right))
+ }),
+ ))
+ }
+ }
+}
+
+expr_prec_layer!(expr_muldivmod, expr_filtered, "*", "/", "%");
+expr_prec_layer!(expr_addsub, expr_muldivmod, "+", "-");
+expr_prec_layer!(expr_shifts, expr_addsub, ">>", "<<");
+expr_prec_layer!(expr_band, expr_shifts, "&");
+expr_prec_layer!(expr_bxor, expr_band, "^");
+expr_prec_layer!(expr_bor, expr_bxor, "|");
+expr_prec_layer!(expr_compare, expr_bor, "==", "!=", ">=", ">", "<=", "<");
+expr_prec_layer!(expr_and, expr_compare, "&&");
+expr_prec_layer!(expr_or, expr_and, "||");
+
+fn expr_any(i: &str) -> IResult<&str, Expr<'_>> {
+ let range_right = |i| pair(ws(alt((tag("..="), tag("..")))), opt(expr_or))(i);
+ alt((
+ map(range_right, |(op, right)| {
+ Expr::Range(op, None, right.map(Box::new))
+ }),
+ map(
+ pair(expr_or, opt(range_right)),
+ |(left, right)| match right {
+ Some((op, right)) => Expr::Range(op, Some(Box::new(left)), right.map(Box::new)),
+ None => left,
+ },
+ ),
+ ))(i)
+}
+
+fn arguments(i: &str) -> IResult<&str, Vec<Expr<'_>>> {
+ delimited(
+ ws(char('(')),
+ separated_list0(char(','), ws(expr_any)),
+ ws(char(')')),
+ )(i)
+}
diff --git a/askama_derive/src/parser/mod.rs b/askama_derive/src/parser/mod.rs
new file mode 100644
index 0000000..d345a81
--- /dev/null
+++ b/askama_derive/src/parser/mod.rs
@@ -0,0 +1,314 @@
+use std::cell::Cell;
+use std::str;
+
+use nom::branch::alt;
+use nom::bytes::complete::{escaped, is_not, tag, take_till};
+use nom::character::complete::char;
+use nom::character::complete::{anychar, digit1};
+use nom::combinator::{eof, map, not, opt, recognize, value};
+use nom::error::ErrorKind;
+use nom::multi::separated_list1;
+use nom::sequence::{delimited, pair, tuple};
+use nom::{error_position, AsChar, IResult, InputTakeAtPosition};
+
+pub(crate) use self::expr::Expr;
+pub(crate) use self::node::{Cond, CondTest, Loop, Macro, Node, Target, When, Whitespace, Ws};
+use crate::config::Syntax;
+use crate::CompileError;
+
+mod expr;
+mod node;
+#[cfg(test)]
+mod tests;
+
+struct State<'a> {
+ syntax: &'a Syntax,
+ loop_depth: Cell<usize>,
+}
+
+impl State<'_> {
+ fn new(syntax: &Syntax) -> State<'_> {
+ State {
+ syntax,
+ loop_depth: Cell::new(0),
+ }
+ }
+
+ fn enter_loop(&self) {
+ self.loop_depth.set(self.loop_depth.get() + 1);
+ }
+
+ fn leave_loop(&self) {
+ self.loop_depth.set(self.loop_depth.get() - 1);
+ }
+
+ fn is_in_loop(&self) -> bool {
+ self.loop_depth.get() > 0
+ }
+}
+
+impl From<char> for Whitespace {
+ fn from(c: char) -> Self {
+ match c {
+ '+' => Self::Preserve,
+ '-' => Self::Suppress,
+ '~' => Self::Minimize,
+ _ => panic!("unsupported `Whitespace` conversion"),
+ }
+ }
+}
+
+pub(crate) fn parse<'a>(src: &'a str, syntax: &'a Syntax) -> Result<Vec<Node<'a>>, CompileError> {
+ match Node::parse(src, &State::new(syntax)) {
+ Ok((left, res)) => {
+ if !left.is_empty() {
+ Err(format!("unable to parse template:\n\n{left:?}").into())
+ } else {
+ Ok(res)
+ }
+ }
+
+ Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
+ let nom::error::Error { input, .. } = err;
+ let offset = src.len() - input.len();
+ let (source_before, source_after) = src.split_at(offset);
+
+ let source_after = match source_after.char_indices().enumerate().take(41).last() {
+ Some((40, (i, _))) => format!("{:?}...", &source_after[..i]),
+ _ => format!("{source_after:?}"),
+ };
+
+ let (row, last_line) = source_before.lines().enumerate().last().unwrap();
+ let column = last_line.chars().count();
+
+ let msg = format!(
+ "problems parsing template source at row {}, column {} near:\n{}",
+ row + 1,
+ column,
+ source_after,
+ );
+ Err(msg.into())
+ }
+
+ Err(nom::Err::Incomplete(_)) => Err("parsing incomplete".into()),
+ }
+}
+
+fn is_ws(c: char) -> bool {
+ matches!(c, ' ' | '\t' | '\r' | '\n')
+}
+
+fn not_ws(c: char) -> bool {
+ !is_ws(c)
+}
+
+fn ws<'a, O>(
+ inner: impl FnMut(&'a str) -> IResult<&'a str, O>,
+) -> impl FnMut(&'a str) -> IResult<&'a str, O> {
+ delimited(take_till(not_ws), inner, take_till(not_ws))
+}
+
+fn split_ws_parts(s: &str) -> Node<'_> {
+ let trimmed_start = s.trim_start_matches(is_ws);
+ let len_start = s.len() - trimmed_start.len();
+ let trimmed = trimmed_start.trim_end_matches(is_ws);
+ Node::Lit(&s[..len_start], trimmed, &trimmed_start[trimmed.len()..])
+}
+
+/// Skips input until `end` was found, but does not consume it.
+/// Returns tuple that would be returned when parsing `end`.
+fn skip_till<'a, O>(
+ end: impl FnMut(&'a str) -> IResult<&'a str, O>,
+) -> impl FnMut(&'a str) -> IResult<&'a str, (&'a str, O)> {
+ enum Next<O> {
+ IsEnd(O),
+ NotEnd(char),
+ }
+ let mut next = alt((map(end, Next::IsEnd), map(anychar, Next::NotEnd)));
+ move |start: &'a str| {
+ let mut i = start;
+ loop {
+ let (j, is_end) = next(i)?;
+ match is_end {
+ Next::IsEnd(lookahead) => return Ok((i, (j, lookahead))),
+ Next::NotEnd(_) => i = j,
+ }
+ }
+ }
+}
+
+fn keyword<'a>(k: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
+ move |i: &'a str| -> IResult<&'a str, &'a str> {
+ let (j, v) = identifier(i)?;
+ if k == v {
+ Ok((j, v))
+ } else {
+ Err(nom::Err::Error(error_position!(i, ErrorKind::Tag)))
+ }
+ }
+}
+
+fn identifier(input: &str) -> IResult<&str, &str> {
+ recognize(pair(identifier_start, opt(identifier_tail)))(input)
+}
+
+fn identifier_start(s: &str) -> IResult<&str, &str> {
+ s.split_at_position1_complete(
+ |c| !(c.is_alpha() || c == '_' || c >= '\u{0080}'),
+ nom::error::ErrorKind::Alpha,
+ )
+}
+
+fn identifier_tail(s: &str) -> IResult<&str, &str> {
+ s.split_at_position1_complete(
+ |c| !(c.is_alphanum() || c == '_' || c >= '\u{0080}'),
+ nom::error::ErrorKind::Alpha,
+ )
+}
+
+fn bool_lit(i: &str) -> IResult<&str, &str> {
+ alt((keyword("false"), keyword("true")))(i)
+}
+
+fn num_lit(i: &str) -> IResult<&str, &str> {
+ recognize(pair(digit1, opt(pair(char('.'), digit1))))(i)
+}
+
+fn str_lit(i: &str) -> IResult<&str, &str> {
+ let (i, s) = delimited(
+ char('"'),
+ opt(escaped(is_not("\\\""), '\\', anychar)),
+ char('"'),
+ )(i)?;
+ Ok((i, s.unwrap_or_default()))
+}
+
+fn char_lit(i: &str) -> IResult<&str, &str> {
+ let (i, s) = delimited(
+ char('\''),
+ opt(escaped(is_not("\\\'"), '\\', anychar)),
+ char('\''),
+ )(i)?;
+ Ok((i, s.unwrap_or_default()))
+}
+
+fn nested_parenthesis(i: &str) -> IResult<&str, ()> {
+ let mut nested = 0;
+ let mut last = 0;
+ let mut in_str = false;
+ let mut escaped = false;
+
+ for (i, b) in i.chars().enumerate() {
+ if !(b == '(' || b == ')') || !in_str {
+ match b {
+ '(' => nested += 1,
+ ')' => {
+ if nested == 0 {
+ last = i;
+ break;
+ }
+ nested -= 1;
+ }
+ '"' => {
+ if in_str {
+ if !escaped {
+ in_str = false;
+ }
+ } else {
+ in_str = true;
+ }
+ }
+ '\\' => {
+ escaped = !escaped;
+ }
+ _ => (),
+ }
+ }
+
+ if escaped && b != '\\' {
+ escaped = false;
+ }
+ }
+
+ if nested == 0 {
+ Ok((&i[last..], ()))
+ } else {
+ Err(nom::Err::Error(error_position!(
+ i,
+ ErrorKind::SeparatedNonEmptyList
+ )))
+ }
+}
+
+fn path(i: &str) -> IResult<&str, Vec<&str>> {
+ let root = opt(value("", ws(tag("::"))));
+ let tail = separated_list1(ws(tag("::")), identifier);
+
+ match tuple((root, identifier, ws(tag("::")), tail))(i) {
+ Ok((i, (root, start, _, rest))) => {
+ let mut path = Vec::new();
+ path.extend(root);
+ path.push(start);
+ path.extend(rest);
+ Ok((i, path))
+ }
+ Err(err) => {
+ if let Ok((i, name)) = identifier(i) {
+ // The returned identifier can be assumed to be path if:
+ // - Contains both a lowercase and uppercase character, i.e. a type name like `None`
+ // - Doesn't contain any lowercase characters, i.e. it's a constant
+ // In short, if it contains any uppercase characters it's a path.
+ if name.contains(char::is_uppercase) {
+ return Ok((i, vec![name]));
+ }
+ }
+
+ // If `identifier()` fails then just return the original error
+ Err(err)
+ }
+ }
+}
+
+fn take_content<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let p_start = alt((
+ tag(s.syntax.block_start.as_str()),
+ tag(s.syntax.comment_start.as_str()),
+ tag(s.syntax.expr_start.as_str()),
+ ));
+
+ let (i, _) = not(eof)(i)?;
+ let (i, content) = opt(recognize(skip_till(p_start)))(i)?;
+ let (i, content) = match content {
+ Some("") => {
+ // {block,comment,expr}_start follows immediately.
+ return Err(nom::Err::Error(error_position!(i, ErrorKind::TakeUntil)));
+ }
+ Some(content) => (i, content),
+ None => ("", i), // there is no {block,comment,expr}_start: take everything
+ };
+ Ok((i, split_ws_parts(content)))
+}
+
+fn tag_block_start<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+ tag(s.syntax.block_start.as_str())(i)
+}
+
+fn tag_block_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+ tag(s.syntax.block_end.as_str())(i)
+}
+
+fn tag_comment_start<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+ tag(s.syntax.comment_start.as_str())(i)
+}
+
+fn tag_comment_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+ tag(s.syntax.comment_end.as_str())(i)
+}
+
+fn tag_expr_start<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+ tag(s.syntax.expr_start.as_str())(i)
+}
+
+fn tag_expr_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+ tag(s.syntax.expr_end.as_str())(i)
+}
diff --git a/askama_derive/src/parser/node.rs b/askama_derive/src/parser/node.rs
new file mode 100644
index 0000000..3bd30d8
--- /dev/null
+++ b/askama_derive/src/parser/node.rs
@@ -0,0 +1,671 @@
+use std::str;
+
+use nom::branch::alt;
+use nom::bytes::complete::{tag, take_until};
+use nom::character::complete::char;
+use nom::combinator::{complete, consumed, cut, map, opt, peek, value};
+use nom::error::{Error, ErrorKind};
+use nom::multi::{fold_many0, many0, many1, separated_list0, separated_list1};
+use nom::sequence::{delimited, pair, preceded, terminated, tuple};
+use nom::{error_position, IResult};
+
+use super::{
+ bool_lit, char_lit, identifier, keyword, num_lit, path, skip_till, split_ws_parts, str_lit,
+ tag_block_end, tag_block_start, tag_comment_end, tag_comment_start, tag_expr_end,
+ tag_expr_start, take_content, ws, Expr, State,
+};
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum Node<'a> {
+ Lit(&'a str, &'a str, &'a str),
+ Comment(Ws),
+ Expr(Ws, Expr<'a>),
+ Call(Ws, Option<&'a str>, &'a str, Vec<Expr<'a>>),
+ LetDecl(Ws, Target<'a>),
+ Let(Ws, Target<'a>, Expr<'a>),
+ Cond(Vec<Cond<'a>>, Ws),
+ Match(Ws, Expr<'a>, Vec<When<'a>>, Ws),
+ Loop(Loop<'a>),
+ Extends(&'a str),
+ BlockDef(Ws, &'a str, Vec<Node<'a>>, Ws),
+ Include(Ws, &'a str),
+ Import(Ws, &'a str, &'a str),
+ Macro(&'a str, Macro<'a>),
+ Raw(Ws, &'a str, &'a str, &'a str, Ws),
+ Break(Ws),
+ Continue(Ws),
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum Target<'a> {
+ Name(&'a str),
+ Tuple(Vec<&'a str>, Vec<Target<'a>>),
+ Struct(Vec<&'a str>, Vec<(&'a str, Target<'a>)>),
+ NumLit(&'a str),
+ StrLit(&'a str),
+ CharLit(&'a str),
+ BoolLit(&'a str),
+ Path(Vec<&'a str>),
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub(crate) enum Whitespace {
+ Preserve,
+ Suppress,
+ Minimize,
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) struct Loop<'a> {
+ pub(crate) ws1: Ws,
+ pub(crate) var: Target<'a>,
+ pub(crate) iter: Expr<'a>,
+ pub(crate) cond: Option<Expr<'a>>,
+ pub(crate) body: Vec<Node<'a>>,
+ pub(crate) ws2: Ws,
+ pub(crate) else_block: Vec<Node<'a>>,
+ pub(crate) ws3: Ws,
+}
+
+pub(crate) type When<'a> = (Ws, Target<'a>, Vec<Node<'a>>);
+
+#[derive(Debug, PartialEq)]
+pub(crate) struct Macro<'a> {
+ pub(crate) ws1: Ws,
+ pub(crate) args: Vec<&'a str>,
+ pub(crate) nodes: Vec<Node<'a>>,
+ pub(crate) ws2: Ws,
+}
+
+/// First field is "minus/plus sign was used on the left part of the item".
+///
+/// Second field is "minus/plus sign was used on the right part of the item".
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub(crate) struct Ws(pub(crate) Option<Whitespace>, pub(crate) Option<Whitespace>);
+
+pub(crate) type Cond<'a> = (Ws, Option<CondTest<'a>>, Vec<Node<'a>>);
+
+#[derive(Debug, PartialEq)]
+pub(crate) struct CondTest<'a> {
+ pub(crate) target: Option<Target<'a>>,
+ pub(crate) expr: Expr<'a>,
+}
+
+impl Node<'_> {
+ pub(super) fn parse<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Vec<Node<'a>>> {
+ parse_template(i, s)
+ }
+}
+
+impl Target<'_> {
+ pub(super) fn parse(i: &str) -> IResult<&str, Target<'_>> {
+ target(i)
+ }
+}
+
+fn expr_handle_ws(i: &str) -> IResult<&str, Whitespace> {
+ alt((char('-'), char('+'), char('~')))(i).map(|(s, r)| (s, Whitespace::from(r)))
+}
+
+fn parameters(i: &str) -> IResult<&str, Vec<&str>> {
+ delimited(
+ ws(char('(')),
+ separated_list0(char(','), ws(identifier)),
+ ws(char(')')),
+ )(i)
+}
+
+fn block_call(i: &str) -> IResult<&str, Node<'_>> {
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("call")),
+ cut(tuple((
+ opt(tuple((ws(identifier), ws(tag("::"))))),
+ ws(identifier),
+ ws(Expr::parse_arguments),
+ opt(expr_handle_ws),
+ ))),
+ ));
+ let (i, (pws, _, (scope, name, args, nws))) = p(i)?;
+ let scope = scope.map(|(scope, _)| scope);
+ Ok((i, Node::Call(Ws(pws, nws), scope, name, args)))
+}
+
+fn cond_if(i: &str) -> IResult<&str, CondTest<'_>> {
+ let mut p = preceded(
+ ws(keyword("if")),
+ cut(tuple((
+ opt(delimited(
+ ws(alt((keyword("let"), keyword("set")))),
+ ws(Target::parse),
+ ws(char('=')),
+ )),
+ ws(Expr::parse),
+ ))),
+ );
+ let (i, (target, expr)) = p(i)?;
+ Ok((i, CondTest { target, expr }))
+}
+
+fn cond_block<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Cond<'a>> {
+ let mut p = tuple((
+ |i| tag_block_start(i, s),
+ opt(expr_handle_ws),
+ ws(keyword("else")),
+ cut(tuple((
+ opt(cond_if),
+ opt(expr_handle_ws),
+ |i| tag_block_end(i, s),
+ cut(|i| parse_template(i, s)),
+ ))),
+ ));
+ let (i, (_, pws, _, (cond, nws, _, block))) = p(i)?;
+ Ok((i, (Ws(pws, nws), cond, block)))
+}
+
+fn block_if<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ cond_if,
+ cut(tuple((
+ opt(expr_handle_ws),
+ |i| tag_block_end(i, s),
+ cut(tuple((
+ |i| parse_template(i, s),
+ many0(|i| cond_block(i, s)),
+ cut(tuple((
+ |i| tag_block_start(i, s),
+ opt(expr_handle_ws),
+ ws(keyword("endif")),
+ opt(expr_handle_ws),
+ ))),
+ ))),
+ ))),
+ ));
+ let (i, (pws1, cond, (nws1, _, (block, elifs, (_, pws2, _, nws2))))) = p(i)?;
+
+ let mut res = vec![(Ws(pws1, nws1), Some(cond), block)];
+ res.extend(elifs);
+ Ok((i, Node::Cond(res, Ws(pws2, nws2))))
+}
+
+fn match_else_block<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, When<'a>> {
+ let mut p = tuple((
+ |i| tag_block_start(i, s),
+ opt(expr_handle_ws),
+ ws(keyword("else")),
+ cut(tuple((
+ opt(expr_handle_ws),
+ |i| tag_block_end(i, s),
+ cut(|i| parse_template(i, s)),
+ ))),
+ ));
+ let (i, (_, pws, _, (nws, _, block))) = p(i)?;
+ Ok((i, (Ws(pws, nws), Target::Name("_"), block)))
+}
+
+fn when_block<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, When<'a>> {
+ let mut p = tuple((
+ |i| tag_block_start(i, s),
+ opt(expr_handle_ws),
+ ws(keyword("when")),
+ cut(tuple((
+ ws(Target::parse),
+ opt(expr_handle_ws),
+ |i| tag_block_end(i, s),
+ cut(|i| parse_template(i, s)),
+ ))),
+ ));
+ let (i, (_, pws, _, (target, nws, _, block))) = p(i)?;
+ Ok((i, (Ws(pws, nws), target, block)))
+}
+
+fn block_match<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("match")),
+ cut(tuple((
+ ws(Expr::parse),
+ opt(expr_handle_ws),
+ |i| tag_block_end(i, s),
+ cut(tuple((
+ ws(many0(ws(value((), |i| block_comment(i, s))))),
+ many1(|i| when_block(i, s)),
+ cut(tuple((
+ opt(|i| match_else_block(i, s)),
+ cut(tuple((
+ ws(|i| tag_block_start(i, s)),
+ opt(expr_handle_ws),
+ ws(keyword("endmatch")),
+ opt(expr_handle_ws),
+ ))),
+ ))),
+ ))),
+ ))),
+ ));
+ let (i, (pws1, _, (expr, nws1, _, (_, arms, (else_arm, (_, pws2, _, nws2)))))) = p(i)?;
+
+ let mut arms = arms;
+ if let Some(arm) = else_arm {
+ arms.push(arm);
+ }
+
+ Ok((i, Node::Match(Ws(pws1, nws1), expr, arms, Ws(pws2, nws2))))
+}
+
+fn block_let(i: &str) -> IResult<&str, Node<'_>> {
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(alt((keyword("let"), keyword("set")))),
+ cut(tuple((
+ ws(Target::parse),
+ opt(tuple((ws(char('=')), ws(Expr::parse)))),
+ opt(expr_handle_ws),
+ ))),
+ ));
+ let (i, (pws, _, (var, val, nws))) = p(i)?;
+
+ Ok((
+ i,
+ if let Some((_, val)) = val {
+ Node::Let(Ws(pws, nws), var, val)
+ } else {
+ Node::LetDecl(Ws(pws, nws), var)
+ },
+ ))
+}
+
+fn parse_loop_content<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Vec<Node<'a>>> {
+ s.enter_loop();
+ let result = parse_template(i, s);
+ s.leave_loop();
+ result
+}
+
+fn block_for<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let if_cond = preceded(ws(keyword("if")), cut(ws(Expr::parse)));
+ let else_block = |i| {
+ let mut p = preceded(
+ ws(keyword("else")),
+ cut(tuple((
+ opt(expr_handle_ws),
+ delimited(
+ |i| tag_block_end(i, s),
+ |i| parse_template(i, s),
+ |i| tag_block_start(i, s),
+ ),
+ opt(expr_handle_ws),
+ ))),
+ );
+ let (i, (pws, nodes, nws)) = p(i)?;
+ Ok((i, (pws, nodes, nws)))
+ };
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("for")),
+ cut(tuple((
+ ws(Target::parse),
+ ws(keyword("in")),
+ cut(tuple((
+ ws(Expr::parse),
+ opt(if_cond),
+ opt(expr_handle_ws),
+ |i| tag_block_end(i, s),
+ cut(tuple((
+ |i| parse_loop_content(i, s),
+ cut(tuple((
+ |i| tag_block_start(i, s),
+ opt(expr_handle_ws),
+ opt(else_block),
+ ws(keyword("endfor")),
+ opt(expr_handle_ws),
+ ))),
+ ))),
+ ))),
+ ))),
+ ));
+ let (i, (pws1, _, (var, _, (iter, cond, nws1, _, (body, (_, pws2, else_block, _, nws2)))))) =
+ p(i)?;
+ let (nws3, else_block, pws3) = else_block.unwrap_or_default();
+ Ok((
+ i,
+ Node::Loop(Loop {
+ ws1: Ws(pws1, nws1),
+ var,
+ iter,
+ cond,
+ body,
+ ws2: Ws(pws2, nws3),
+ else_block,
+ ws3: Ws(pws3, nws2),
+ }),
+ ))
+}
+
+fn block_extends(i: &str) -> IResult<&str, Node<'_>> {
+ let (i, (_, name)) = tuple((ws(keyword("extends")), ws(str_lit)))(i)?;
+ Ok((i, Node::Extends(name)))
+}
+
+fn block_block<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut start = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("block")),
+ cut(tuple((ws(identifier), opt(expr_handle_ws), |i| {
+ tag_block_end(i, s)
+ }))),
+ ));
+ let (i, (pws1, _, (name, nws1, _))) = start(i)?;
+
+ let mut end = cut(tuple((
+ |i| parse_template(i, s),
+ cut(tuple((
+ |i| tag_block_start(i, s),
+ opt(expr_handle_ws),
+ ws(keyword("endblock")),
+ cut(tuple((opt(ws(keyword(name))), opt(expr_handle_ws)))),
+ ))),
+ )));
+ let (i, (contents, (_, pws2, _, (_, nws2)))) = end(i)?;
+
+ Ok((
+ i,
+ Node::BlockDef(Ws(pws1, nws1), name, contents, Ws(pws2, nws2)),
+ ))
+}
+
+fn block_include(i: &str) -> IResult<&str, Node<'_>> {
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("include")),
+ cut(pair(ws(str_lit), opt(expr_handle_ws))),
+ ));
+ let (i, (pws, _, (name, nws))) = p(i)?;
+ Ok((i, Node::Include(Ws(pws, nws), name)))
+}
+
+fn block_import(i: &str) -> IResult<&str, Node<'_>> {
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("import")),
+ cut(tuple((
+ ws(str_lit),
+ ws(keyword("as")),
+ cut(pair(ws(identifier), opt(expr_handle_ws))),
+ ))),
+ ));
+ let (i, (pws, _, (name, _, (scope, nws)))) = p(i)?;
+ Ok((i, Node::Import(Ws(pws, nws), name, scope)))
+}
+
+fn block_macro<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut start = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("macro")),
+ cut(tuple((
+ ws(identifier),
+ ws(parameters),
+ opt(expr_handle_ws),
+ |i| tag_block_end(i, s),
+ ))),
+ ));
+ let (i, (pws1, _, (name, params, nws1, _))) = start(i)?;
+
+ let mut end = cut(tuple((
+ |i| parse_template(i, s),
+ cut(tuple((
+ |i| tag_block_start(i, s),
+ opt(expr_handle_ws),
+ ws(keyword("endmacro")),
+ cut(tuple((opt(ws(keyword(name))), opt(expr_handle_ws)))),
+ ))),
+ )));
+ let (i, (contents, (_, pws2, _, (_, nws2)))) = end(i)?;
+
+ assert_ne!(name, "super", "invalid macro name 'super'");
+
+ Ok((
+ i,
+ Node::Macro(
+ name,
+ Macro {
+ ws1: Ws(pws1, nws1),
+ args: params,
+ nodes: contents,
+ ws2: Ws(pws2, nws2),
+ },
+ ),
+ ))
+}
+
+fn block_raw<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let endraw = tuple((
+ |i| tag_block_start(i, s),
+ opt(expr_handle_ws),
+ ws(keyword("endraw")),
+ opt(expr_handle_ws),
+ peek(|i| tag_block_end(i, s)),
+ ));
+
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("raw")),
+ cut(tuple((
+ opt(expr_handle_ws),
+ |i| tag_block_end(i, s),
+ consumed(skip_till(endraw)),
+ ))),
+ ));
+
+ let (_, (pws1, _, (nws1, _, (contents, (i, (_, pws2, _, nws2, _)))))) = p(i)?;
+ let (lws, val, rws) = match split_ws_parts(contents) {
+ Node::Lit(lws, val, rws) => (lws, val, rws),
+ _ => unreachable!(),
+ };
+ let ws1 = Ws(pws1, nws1);
+ let ws2 = Ws(pws2, nws2);
+ Ok((i, Node::Raw(ws1, lws, val, rws, ws2)))
+}
+
+fn break_statement<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("break")),
+ opt(expr_handle_ws),
+ ));
+ let (j, (pws, _, nws)) = p(i)?;
+ if !s.is_in_loop() {
+ return Err(nom::Err::Failure(error_position!(i, ErrorKind::Tag)));
+ }
+ Ok((j, Node::Break(Ws(pws, nws))))
+}
+
+fn continue_statement<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut p = tuple((
+ opt(expr_handle_ws),
+ ws(keyword("continue")),
+ opt(expr_handle_ws),
+ ));
+ let (j, (pws, _, nws)) = p(i)?;
+ if !s.is_in_loop() {
+ return Err(nom::Err::Failure(error_position!(i, ErrorKind::Tag)));
+ }
+ Ok((j, Node::Continue(Ws(pws, nws))))
+}
+
+fn block_node<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut p = tuple((
+ |i| tag_block_start(i, s),
+ alt((
+ block_call,
+ block_let,
+ |i| block_if(i, s),
+ |i| block_for(i, s),
+ |i| block_match(i, s),
+ block_extends,
+ block_include,
+ block_import,
+ |i| block_block(i, s),
+ |i| block_macro(i, s),
+ |i| block_raw(i, s),
+ |i| break_statement(i, s),
+ |i| continue_statement(i, s),
+ )),
+ cut(|i| tag_block_end(i, s)),
+ ));
+ let (i, (_, contents, _)) = p(i)?;
+ Ok((i, contents))
+}
+
+fn block_comment_body<'a>(mut i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+ let mut level = 0;
+ loop {
+ let (end, tail) = take_until(s.syntax.comment_end.as_str())(i)?;
+ match take_until::<_, _, Error<_>>(s.syntax.comment_start.as_str())(i) {
+ Ok((start, _)) if start.as_ptr() < end.as_ptr() => {
+ level += 1;
+ i = &start[2..];
+ }
+ _ if level > 0 => {
+ level -= 1;
+ i = &end[2..];
+ }
+ _ => return Ok((end, tail)),
+ }
+ }
+}
+
+fn block_comment<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut p = tuple((
+ |i| tag_comment_start(i, s),
+ cut(tuple((
+ opt(expr_handle_ws),
+ |i| block_comment_body(i, s),
+ |i| tag_comment_end(i, s),
+ ))),
+ ));
+ let (i, (_, (pws, tail, _))) = p(i)?;
+ let nws = if tail.ends_with('-') {
+ Some(Whitespace::Suppress)
+ } else if tail.ends_with('+') {
+ Some(Whitespace::Preserve)
+ } else if tail.ends_with('~') {
+ Some(Whitespace::Minimize)
+ } else {
+ None
+ };
+ Ok((i, Node::Comment(Ws(pws, nws))))
+}
+
+fn expr_node<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Node<'a>> {
+ let mut p = tuple((
+ |i| tag_expr_start(i, s),
+ cut(tuple((
+ opt(expr_handle_ws),
+ ws(Expr::parse),
+ opt(expr_handle_ws),
+ |i| tag_expr_end(i, s),
+ ))),
+ ));
+ let (i, (_, (pws, expr, nws, _))) = p(i)?;
+ Ok((i, Node::Expr(Ws(pws, nws), expr)))
+}
+
+fn parse_template<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, Vec<Node<'a>>> {
+ many0(alt((
+ complete(|i| take_content(i, s)),
+ complete(|i| block_comment(i, s)),
+ complete(|i| expr_node(i, s)),
+ complete(|i| block_node(i, s)),
+ )))(i)
+}
+
+fn variant_lit(i: &str) -> IResult<&str, Target<'_>> {
+ alt((
+ map(str_lit, Target::StrLit),
+ map(char_lit, Target::CharLit),
+ map(num_lit, Target::NumLit),
+ map(bool_lit, Target::BoolLit),
+ ))(i)
+}
+
+fn target(i: &str) -> IResult<&str, Target<'_>> {
+ let mut opt_opening_paren = map(opt(ws(char('('))), |o| o.is_some());
+ let mut opt_closing_paren = map(opt(ws(char(')'))), |o| o.is_some());
+ let mut opt_opening_brace = map(opt(ws(char('{'))), |o| o.is_some());
+
+ let (i, lit) = opt(variant_lit)(i)?;
+ if let Some(lit) = lit {
+ return Ok((i, lit));
+ }
+
+ // match tuples and unused parentheses
+ let (i, target_is_tuple) = opt_opening_paren(i)?;
+ if target_is_tuple {
+ let (i, is_empty_tuple) = opt_closing_paren(i)?;
+ if is_empty_tuple {
+ return Ok((i, Target::Tuple(Vec::new(), Vec::new())));
+ }
+
+ let (i, first_target) = target(i)?;
+ let (i, is_unused_paren) = opt_closing_paren(i)?;
+ if is_unused_paren {
+ return Ok((i, first_target));
+ }
+
+ let mut targets = vec![first_target];
+ let (i, _) = cut(tuple((
+ fold_many0(
+ preceded(ws(char(',')), target),
+ || (),
+ |_, target| {
+ targets.push(target);
+ },
+ ),
+ opt(ws(char(','))),
+ ws(cut(char(')'))),
+ )))(i)?;
+ return Ok((i, Target::Tuple(Vec::new(), targets)));
+ }
+
+ // match structs
+ let (i, path) = opt(path)(i)?;
+ if let Some(path) = path {
+ let i_before_matching_with = i;
+ let (i, _) = opt(ws(keyword("with")))(i)?;
+
+ let (i, is_unnamed_struct) = opt_opening_paren(i)?;
+ if is_unnamed_struct {
+ let (i, targets) = alt((
+ map(char(')'), |_| Vec::new()),
+ terminated(
+ cut(separated_list1(ws(char(',')), target)),
+ pair(opt(ws(char(','))), ws(cut(char(')')))),
+ ),
+ ))(i)?;
+ return Ok((i, Target::Tuple(path, targets)));
+ }
+
+ let (i, is_named_struct) = opt_opening_brace(i)?;
+ if is_named_struct {
+ let (i, targets) = alt((
+ map(char('}'), |_| Vec::new()),
+ terminated(
+ cut(separated_list1(ws(char(',')), named_target)),
+ pair(opt(ws(char(','))), ws(cut(char('}')))),
+ ),
+ ))(i)?;
+ return Ok((i, Target::Struct(path, targets)));
+ }
+
+ return Ok((i_before_matching_with, Target::Path(path)));
+ }
+
+ // neither literal nor struct nor path
+ map(identifier, Target::Name)(i)
+}
+
+fn named_target(i: &str) -> IResult<&str, (&str, Target<'_>)> {
+ let (i, (src, target)) = pair(identifier, opt(preceded(ws(char(':')), target)))(i)?;
+ Ok((i, (src, target.unwrap_or(Target::Name(src)))))
+}
diff --git a/askama_derive/src/parser/tests.rs b/askama_derive/src/parser/tests.rs
new file mode 100644
index 0000000..045bf28
--- /dev/null
+++ b/askama_derive/src/parser/tests.rs
@@ -0,0 +1,668 @@
+use crate::config::Syntax;
+use crate::parser::{Expr, Node, Whitespace, Ws};
+
+fn check_ws_split(s: &str, res: &(&str, &str, &str)) {
+ match super::split_ws_parts(s) {
+ Node::Lit(lws, s, rws) => {
+ assert_eq!(lws, res.0);
+ assert_eq!(s, res.1);
+ assert_eq!(rws, res.2);
+ }
+ _ => {
+ panic!("fail");
+ }
+ }
+}
+
+#[test]
+fn test_ws_splitter() {
+ 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() {
+ super::parse("{% extend \"blah\" %}", &Syntax::default()).unwrap();
+}
+
+#[test]
+fn test_parse_filter() {
+ use Expr::*;
+ let syntax = Syntax::default();
+ assert_eq!(
+ super::parse("{{ strvar|e }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Filter("e", vec![Var("strvar")]),)],
+ );
+ assert_eq!(
+ super::parse("{{ 2|abs }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Filter("abs", vec![NumLit("2")]),)],
+ );
+ assert_eq!(
+ super::parse("{{ -2|abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Filter("abs", vec![Unary("-", NumLit("2").into())]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1 - 2)|abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Filter(
+ "abs",
+ vec![Group(
+ BinOp("-", NumLit("1").into(), NumLit("2").into()).into()
+ )]
+ ),
+ )],
+ );
+}
+
+#[test]
+fn test_parse_numbers() {
+ let syntax = Syntax::default();
+ assert_eq!(
+ super::parse("{{ 2 }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::NumLit("2"),)],
+ );
+ assert_eq!(
+ super::parse("{{ 2.5 }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::NumLit("2.5"),)],
+ );
+}
+
+#[test]
+fn test_parse_var() {
+ let s = Syntax::default();
+
+ assert_eq!(
+ super::parse("{{ foo }}", &s).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::Var("foo"))],
+ );
+ assert_eq!(
+ super::parse("{{ foo_bar }}", &s).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::Var("foo_bar"))],
+ );
+
+ assert_eq!(
+ super::parse("{{ none }}", &s).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::Var("none"))],
+ );
+}
+
+#[test]
+fn test_parse_const() {
+ let s = Syntax::default();
+
+ assert_eq!(
+ super::parse("{{ FOO }}", &s).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::Path(vec!["FOO"]))],
+ );
+ assert_eq!(
+ super::parse("{{ FOO_BAR }}", &s).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::Path(vec!["FOO_BAR"]))],
+ );
+
+ assert_eq!(
+ super::parse("{{ NONE }}", &s).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::Path(vec!["NONE"]))],
+ );
+}
+
+#[test]
+fn test_parse_path() {
+ let s = Syntax::default();
+
+ assert_eq!(
+ super::parse("{{ None }}", &s).unwrap(),
+ vec![Node::Expr(Ws(None, None), Expr::Path(vec!["None"]))],
+ );
+ assert_eq!(
+ super::parse("{{ Some(123) }}", &s).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Call(
+ Box::new(Expr::Path(vec!["Some"])),
+ vec![Expr::NumLit("123")]
+ ),
+ )],
+ );
+
+ assert_eq!(
+ super::parse("{{ Ok(123) }}", &s).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Call(Box::new(Expr::Path(vec!["Ok"])), vec![Expr::NumLit("123")]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ Err(123) }}", &s).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Call(Box::new(Expr::Path(vec!["Err"])), vec![Expr::NumLit("123")]),
+ )],
+ );
+}
+
+#[test]
+fn test_parse_var_call() {
+ assert_eq!(
+ super::parse("{{ function(\"123\", 3) }}", &Syntax::default()).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Call(
+ Box::new(Expr::Var("function")),
+ vec![Expr::StrLit("123"), Expr::NumLit("3")]
+ ),
+ )],
+ );
+}
+
+#[test]
+fn test_parse_path_call() {
+ let s = Syntax::default();
+
+ assert_eq!(
+ super::parse("{{ Option::None }}", &s).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Path(vec!["Option", "None"])
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ Option::Some(123) }}", &s).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Call(
+ Box::new(Expr::Path(vec!["Option", "Some"])),
+ vec![Expr::NumLit("123")],
+ ),
+ )],
+ );
+
+ assert_eq!(
+ super::parse("{{ self::function(\"123\", 3) }}", &s).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Call(
+ Box::new(Expr::Path(vec!["self", "function"])),
+ vec![Expr::StrLit("123"), Expr::NumLit("3")],
+ ),
+ )],
+ );
+}
+
+#[test]
+fn test_parse_root_path() {
+ let syntax = Syntax::default();
+ assert_eq!(
+ super::parse("{{ std::string::String::new() }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Call(
+ Box::new(Expr::Path(vec!["std", "string", "String", "new"])),
+ vec![]
+ ),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ ::std::string::String::new() }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Expr::Call(
+ Box::new(Expr::Path(vec!["", "std", "string", "String", "new"])),
+ vec![]
+ ),
+ )],
+ );
+}
+
+#[test]
+fn change_delimiters_parse_filter() {
+ let syntax = Syntax {
+ expr_start: "{=".to_owned(),
+ expr_end: "=}".to_owned(),
+ ..Syntax::default()
+ };
+
+ super::parse("{= strvar|e =}", &syntax).unwrap();
+}
+
+#[test]
+fn test_precedence() {
+ use Expr::*;
+ let syntax = Syntax::default();
+ assert_eq!(
+ super::parse("{{ a + b == c }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "==",
+ BinOp("+", Var("a").into(), Var("b").into()).into(),
+ Var("c").into(),
+ )
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ a + b * c - d / e }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "-",
+ BinOp(
+ "+",
+ Var("a").into(),
+ BinOp("*", Var("b").into(), Var("c").into()).into(),
+ )
+ .into(),
+ BinOp("/", Var("d").into(), Var("e").into()).into(),
+ )
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ a * (b + c) / -d }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "/",
+ BinOp(
+ "*",
+ Var("a").into(),
+ Group(BinOp("+", Var("b").into(), Var("c").into()).into()).into()
+ )
+ .into(),
+ Unary("-", Var("d").into()).into()
+ )
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ a || b && c || d && e }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "||",
+ BinOp(
+ "||",
+ Var("a").into(),
+ BinOp("&&", Var("b").into(), Var("c").into()).into(),
+ )
+ .into(),
+ BinOp("&&", Var("d").into(), Var("e").into()).into(),
+ )
+ )],
+ );
+}
+
+#[test]
+fn test_associativity() {
+ use Expr::*;
+ let syntax = Syntax::default();
+ assert_eq!(
+ super::parse("{{ a + b + c }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "+",
+ BinOp("+", Var("a").into(), Var("b").into()).into(),
+ Var("c").into()
+ )
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ a * b * c }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "*",
+ BinOp("*", Var("a").into(), Var("b").into()).into(),
+ Var("c").into()
+ )
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ a && b && c }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "&&",
+ BinOp("&&", Var("a").into(), Var("b").into()).into(),
+ Var("c").into()
+ )
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ a + b - c + d }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "+",
+ BinOp(
+ "-",
+ BinOp("+", Var("a").into(), Var("b").into()).into(),
+ Var("c").into()
+ )
+ .into(),
+ Var("d").into()
+ )
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ a == b != c > d > e == f }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "==",
+ BinOp(
+ ">",
+ BinOp(
+ ">",
+ BinOp(
+ "!=",
+ BinOp("==", Var("a").into(), Var("b").into()).into(),
+ Var("c").into()
+ )
+ .into(),
+ Var("d").into()
+ )
+ .into(),
+ Var("e").into()
+ )
+ .into(),
+ Var("f").into()
+ )
+ )],
+ );
+}
+
+#[test]
+fn test_odd_calls() {
+ use Expr::*;
+ let syntax = Syntax::default();
+ assert_eq!(
+ super::parse("{{ a[b](c) }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Call(
+ Box::new(Index(Box::new(Var("a")), Box::new(Var("b")))),
+ vec![Var("c")],
+ ),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (a + b)(c) }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Call(
+ Box::new(Group(Box::new(BinOp(
+ "+",
+ Box::new(Var("a")),
+ Box::new(Var("b"))
+ )))),
+ vec![Var("c")],
+ ),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ a + b(c) }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "+",
+ Box::new(Var("a")),
+ Box::new(Call(Box::new(Var("b")), vec![Var("c")])),
+ ),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (-a)(b) }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Call(
+ Box::new(Group(Box::new(Unary("-", Box::new(Var("a")))))),
+ vec![Var("b")],
+ ),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ -a(b) }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Unary("-", Box::new(Call(Box::new(Var("a")), vec![Var("b")])),),
+ )],
+ );
+}
+
+#[test]
+fn test_parse_comments() {
+ let s = &Syntax::default();
+
+ assert_eq!(
+ super::parse("{##}", s).unwrap(),
+ vec![Node::Comment(Ws(None, None))],
+ );
+ assert_eq!(
+ super::parse("{#- #}", s).unwrap(),
+ vec![Node::Comment(Ws(Some(Whitespace::Suppress), None))],
+ );
+ assert_eq!(
+ super::parse("{# -#}", s).unwrap(),
+ vec![Node::Comment(Ws(None, Some(Whitespace::Suppress)))],
+ );
+ assert_eq!(
+ super::parse("{#--#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Suppress),
+ Some(Whitespace::Suppress)
+ ))],
+ );
+ assert_eq!(
+ super::parse("{#- foo\n bar -#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Suppress),
+ Some(Whitespace::Suppress)
+ ))],
+ );
+ assert_eq!(
+ super::parse("{#- foo\n {#- bar\n -#} baz -#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Suppress),
+ Some(Whitespace::Suppress)
+ ))],
+ );
+ assert_eq!(
+ super::parse("{#+ #}", s).unwrap(),
+ vec![Node::Comment(Ws(Some(Whitespace::Preserve), None))],
+ );
+ assert_eq!(
+ super::parse("{# +#}", s).unwrap(),
+ vec![Node::Comment(Ws(None, Some(Whitespace::Preserve)))],
+ );
+ assert_eq!(
+ super::parse("{#++#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Preserve),
+ Some(Whitespace::Preserve)
+ ))],
+ );
+ assert_eq!(
+ super::parse("{#+ foo\n bar +#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Preserve),
+ Some(Whitespace::Preserve)
+ ))],
+ );
+ assert_eq!(
+ super::parse("{#+ foo\n {#+ bar\n +#} baz -+#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Preserve),
+ Some(Whitespace::Preserve)
+ ))],
+ );
+ assert_eq!(
+ super::parse("{#~ #}", s).unwrap(),
+ vec![Node::Comment(Ws(Some(Whitespace::Minimize), None))],
+ );
+ assert_eq!(
+ super::parse("{# ~#}", s).unwrap(),
+ vec![Node::Comment(Ws(None, Some(Whitespace::Minimize)))],
+ );
+ assert_eq!(
+ super::parse("{#~~#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Minimize),
+ Some(Whitespace::Minimize)
+ ))],
+ );
+ assert_eq!(
+ super::parse("{#~ foo\n bar ~#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Minimize),
+ Some(Whitespace::Minimize)
+ ))],
+ );
+ assert_eq!(
+ super::parse("{#~ foo\n {#~ bar\n ~#} baz -~#}", s).unwrap(),
+ vec![Node::Comment(Ws(
+ Some(Whitespace::Minimize),
+ Some(Whitespace::Minimize)
+ ))],
+ );
+
+ assert_eq!(
+ super::parse("{# foo {# bar #} {# {# baz #} qux #} #}", s).unwrap(),
+ vec![Node::Comment(Ws(None, None))],
+ );
+}
+
+#[test]
+fn test_parse_tuple() {
+ use super::Expr::*;
+ let syntax = Syntax::default();
+ assert_eq!(
+ super::parse("{{ () }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Tuple(vec![]),)],
+ );
+ assert_eq!(
+ super::parse("{{ (1) }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Group(Box::new(NumLit("1"))),)],
+ );
+ assert_eq!(
+ super::parse("{{ (1,) }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Tuple(vec![NumLit("1")]),)],
+ );
+ assert_eq!(
+ super::parse("{{ (1, ) }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Tuple(vec![NumLit("1")]),)],
+ );
+ assert_eq!(
+ super::parse("{{ (1 ,) }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Tuple(vec![NumLit("1")]),)],
+ );
+ assert_eq!(
+ super::parse("{{ (1 , ) }}", &syntax).unwrap(),
+ vec![Node::Expr(Ws(None, None), Tuple(vec![NumLit("1")]),)],
+ );
+ assert_eq!(
+ super::parse("{{ (1, 2) }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Tuple(vec![NumLit("1"), NumLit("2")]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1, 2,) }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Tuple(vec![NumLit("1"), NumLit("2")]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1, 2, 3) }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Tuple(vec![NumLit("1"), NumLit("2"), NumLit("3")]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ ()|abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Filter("abs", vec![Tuple(vec![])]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ () | abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp("|", Box::new(Tuple(vec![])), Box::new(Var("abs"))),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1)|abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Filter("abs", vec![Group(Box::new(NumLit("1")))]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1) | abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "|",
+ Box::new(Group(Box::new(NumLit("1")))),
+ Box::new(Var("abs"))
+ ),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1,)|abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Filter("abs", vec![Tuple(vec![NumLit("1")])]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1,) | abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "|",
+ Box::new(Tuple(vec![NumLit("1")])),
+ Box::new(Var("abs"))
+ ),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1, 2)|abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ Filter("abs", vec![Tuple(vec![NumLit("1"), NumLit("2")])]),
+ )],
+ );
+ assert_eq!(
+ super::parse("{{ (1, 2) | abs }}", &syntax).unwrap(),
+ vec![Node::Expr(
+ Ws(None, None),
+ BinOp(
+ "|",
+ Box::new(Tuple(vec![NumLit("1"), NumLit("2")])),
+ Box::new(Var("abs"))
+ ),
+ )],
+ );
+}
+
+#[test]
+fn test_missing_space_after_kw() {
+ let syntax = Syntax::default();
+ let err = super::parse("{%leta=b%}", &syntax).unwrap_err();
+ assert!(matches!(
+ &*err.msg,
+ "unable to parse template:\n\n\"{%leta=b%}\""
+ ));
+}