From e40e93796f45d34d038c6a20c4b034eb3b384b12 Mon Sep 17 00:00:00 2001
From: Dirkjan Ochtman <dirkjan@ochtman.nl>
Date: Sat, 1 Jul 2023 16:00:06 +0200
Subject: Extract askama_parser crate

---
 Cargo.toml                        |   2 +
 askama_derive/Cargo.toml          |   2 +-
 askama_derive/src/config.rs       |   2 +-
 askama_derive/src/generator.rs    |   2 +-
 askama_derive/src/heritage.rs     |   2 +-
 askama_derive/src/input.rs        |   2 +-
 askama_derive/src/lib.rs          |   4 +-
 askama_derive/src/parser/expr.rs  | 285 ---------------
 askama_derive/src/parser/mod.rs   | 381 --------------------
 askama_derive/src/parser/node.rs  | 674 ------------------------------------
 askama_derive/src/parser/tests.rs | 712 --------------------------------------
 askama_parser/Cargo.toml          |  17 +
 askama_parser/src/expr.rs         | 285 +++++++++++++++
 askama_parser/src/lib.rs          | 384 ++++++++++++++++++++
 askama_parser/src/node.rs         | 674 ++++++++++++++++++++++++++++++++++++
 askama_parser/src/tests.rs        | 712 ++++++++++++++++++++++++++++++++++++++
 16 files changed, 2081 insertions(+), 2059 deletions(-)
 delete mode 100644 askama_derive/src/parser/expr.rs
 delete mode 100644 askama_derive/src/parser/mod.rs
 delete mode 100644 askama_derive/src/parser/node.rs
 delete mode 100644 askama_derive/src/parser/tests.rs
 create mode 100644 askama_parser/Cargo.toml
 create mode 100644 askama_parser/src/expr.rs
 create mode 100644 askama_parser/src/lib.rs
 create mode 100644 askama_parser/src/node.rs
 create mode 100644 askama_parser/src/tests.rs

diff --git a/Cargo.toml b/Cargo.toml
index bd8b8da..60326cd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
     "askama_derive",
     "askama_escape",
     "askama_mendes",
+    "askama_parser",
     "askama_rocket",
     "askama_tide",
     "askama_warp",
@@ -18,5 +19,6 @@ default-members = [
     "askama",
     "askama_derive",
     "askama_escape",
+    "askama_parser",
     "testing",
 ]
diff --git a/askama_derive/Cargo.toml b/askama_derive/Cargo.toml
index 5b33e6d..cb4635f 100644
--- a/askama_derive/Cargo.toml
+++ b/askama_derive/Cargo.toml
@@ -31,9 +31,9 @@ with-tide = []
 with-warp = []
 
 [dependencies]
+parser = { package = "askama_parser", version = "0.1", path = "../askama_parser" }
 mime = "0.3"
 mime_guess = "2"
-nom = "7"
 proc-macro2 = "1"
 quote = "1"
 serde = { version = "1.0", optional = true, features = ["derive"] }
diff --git a/askama_derive/src/config.rs b/askama_derive/src/config.rs
index e456c67..6533fdc 100644
--- a/askama_derive/src/config.rs
+++ b/askama_derive/src/config.rs
@@ -5,8 +5,8 @@ use std::{env, fs};
 #[cfg(feature = "serde")]
 use serde::Deserialize;
 
-use crate::parser::{Syntax, Whitespace};
 use crate::CompileError;
+use parser::{Syntax, Whitespace};
 
 #[derive(Debug)]
 pub(crate) struct Config<'a> {
diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs
index b6c6151..0c482b5 100644
--- a/askama_derive/src/generator.rs
+++ b/askama_derive/src/generator.rs
@@ -1,8 +1,8 @@
 use crate::config::{get_template_source, read_config_file, Config, WhitespaceHandling};
 use crate::heritage::{Context, Heritage};
 use crate::input::{Print, Source, TemplateInput};
-use crate::parser::{Cond, CondTest, Expr, Loop, Node, Parsed, Target, When, Whitespace, Ws};
 use crate::CompileError;
+use parser::{Cond, CondTest, Expr, Loop, Node, Parsed, Target, When, Whitespace, Ws};
 
 use proc_macro::TokenStream;
 use quote::{quote, ToTokens};
diff --git a/askama_derive/src/heritage.rs b/askama_derive/src/heritage.rs
index dbb2b1f..38c2cc8 100644
--- a/askama_derive/src/heritage.rs
+++ b/askama_derive/src/heritage.rs
@@ -2,8 +2,8 @@ use std::collections::HashMap;
 use std::path::{Path, PathBuf};
 
 use crate::config::Config;
-use crate::parser::{Loop, Macro, Node};
 use crate::CompileError;
+use parser::{Loop, Macro, Node};
 
 pub(crate) struct Heritage<'a> {
     pub(crate) root: &'a Context<'a>,
diff --git a/askama_derive/src/input.rs b/askama_derive/src/input.rs
index be425a3..d4e8ad9 100644
--- a/askama_derive/src/input.rs
+++ b/askama_derive/src/input.rs
@@ -1,7 +1,7 @@
 use crate::config::Config;
 use crate::generator::TemplateArgs;
-use crate::parser::Syntax;
 use crate::CompileError;
+use parser::Syntax;
 
 use std::path::{Path, PathBuf};
 use std::str::FromStr;
diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs
index 1483438..8a737aa 100644
--- a/askama_derive/src/lib.rs
+++ b/askama_derive/src/lib.rs
@@ -7,12 +7,12 @@ use std::fmt;
 use proc_macro::TokenStream;
 use proc_macro2::Span;
 
+use parser::ParseError;
+
 mod config;
 mod generator;
 mod heritage;
 mod input;
-mod parser;
-use parser::ParseError;
 
 #[proc_macro_derive(Template, attributes(template))]
 pub fn derive_template(input: TokenStream) -> TokenStream {
diff --git a/askama_derive/src/parser/expr.rs b/askama_derive/src/parser/expr.rs
deleted file mode 100644
index deefb48..0000000
--- a/askama_derive/src/parser/expr.rs
+++ /dev/null
@@ -1,285 +0,0 @@
-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::error::ErrorKind;
-use nom::multi::{fold_many0, many0, separated_list0, separated_list1};
-use nom::sequence::{delimited, pair, preceded, terminated, tuple};
-use nom::{error_position, 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(Vec<&'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)
-    }
-}
-
-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_array_lit,
-        expr_var,
-        expr_group,
-    ))(i)
-}
-
-enum Suffix<'a> {
-    Attr(&'a str),
-    Index(Expr<'a>),
-    Call(Vec<Expr<'a>>),
-    // The value is the arguments of the macro call.
-    MacroCall(&'a str),
-    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_macro(i: &str) -> IResult<&str, Suffix<'_>> {
-    preceded(
-        pair(ws(char('!')), char('(')),
-        cut(terminated(
-            map(recognize(nested_parenthesis), Suffix::MacroCall),
-            char(')'),
-        )),
-    )(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, expr_macro,
-        )))(i)?;
-        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()),
-            Some(Suffix::MacroCall(args)) => match expr {
-                Expr::Path(path) => expr = Expr::RustMacro(path, args),
-                Expr::Var(name) => expr = Expr::RustMacro(vec![name], args),
-                _ => return Err(nom::Err::Failure(error_position!(i, ErrorKind::Tag))),
-            },
-            None => break,
-        }
-        i = j;
-    }
-    Ok((i, expr))
-}
-
-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
deleted file mode 100644
index 8da96f5..0000000
--- a/askama_derive/src/parser/mod.rs
+++ /dev/null
@@ -1,381 +0,0 @@
-use std::cell::Cell;
-use std::{fmt, 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};
-
-mod expr;
-mod node;
-#[cfg(test)]
-mod tests;
-
-struct State<'a> {
-    syntax: &'a Syntax<'a>,
-    loop_depth: Cell<usize>,
-}
-
-impl<'a> State<'a> {
-    fn new(syntax: &'a Syntax<'a>) -> State<'a> {
-        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"),
-        }
-    }
-}
-
-mod _parsed {
-    use std::mem;
-
-    use super::{parse, Node, ParseError, Syntax};
-
-    pub(crate) struct Parsed {
-        #[allow(dead_code)]
-        source: String,
-        nodes: Vec<Node<'static>>,
-    }
-
-    impl Parsed {
-        pub(crate) fn new(source: String, syntax: &Syntax<'_>) -> Result<Self, ParseError> {
-            // Self-referential borrowing: `self` will keep the source alive as `String`,
-            // internally we will transmute it to `&'static str` to satisfy the compiler.
-            // However, we only expose the nodes with a lifetime limited to `self`.
-            let src = unsafe { mem::transmute::<&str, &'static str>(source.as_str()) };
-            let nodes = match parse(src, syntax) {
-                Ok(nodes) => nodes,
-                Err(e) => return Err(e),
-            };
-
-            Ok(Self { source, nodes })
-        }
-
-        // The return value's lifetime must be limited to `self` to uphold the unsafe invariant.
-        pub(crate) fn nodes(&self) -> &[Node<'_>] {
-            &self.nodes
-        }
-    }
-}
-
-pub(crate) use _parsed::Parsed;
-
-pub(crate) fn parse<'a>(src: &'a str, syntax: &Syntax<'_>) -> Result<Vec<Node<'a>>, ParseError> {
-    match Node::parse(src, &State::new(syntax)) {
-        Ok((left, res)) => {
-            if !left.is_empty() {
-                Err(ParseError(format!("unable to parse template:\n\n{left:?}")))
-            } 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(ParseError(msg))
-        }
-
-        Err(nom::Err::Incomplete(_)) => Err(ParseError("parsing incomplete".into())),
-    }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct ParseError(String);
-
-impl std::error::Error for ParseError {}
-
-impl fmt::Display for ParseError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-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),
-        tag(s.syntax.comment_start),
-        tag(s.syntax.expr_start),
-    ));
-
-    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)(i)
-}
-
-fn tag_block_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
-    tag(s.syntax.block_end)(i)
-}
-
-fn tag_comment_start<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
-    tag(s.syntax.comment_start)(i)
-}
-
-fn tag_comment_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
-    tag(s.syntax.comment_end)(i)
-}
-
-fn tag_expr_start<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
-    tag(s.syntax.expr_start)(i)
-}
-
-fn tag_expr_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
-    tag(s.syntax.expr_end)(i)
-}
-
-#[derive(Debug)]
-pub(crate) struct Syntax<'a> {
-    pub(crate) block_start: &'a str,
-    pub(crate) block_end: &'a str,
-    pub(crate) expr_start: &'a str,
-    pub(crate) expr_end: &'a str,
-    pub(crate) comment_start: &'a str,
-    pub(crate) comment_end: &'a str,
-}
-
-impl Default for Syntax<'static> {
-    fn default() -> Self {
-        Self {
-            block_start: "{%",
-            block_end: "%}",
-            expr_start: "{{",
-            expr_end: "}}",
-            comment_start: "{#",
-            comment_end: "#}",
-        }
-    }
-}
diff --git a/askama_derive/src/parser/node.rs b/askama_derive/src/parser/node.rs
deleted file mode 100644
index ce303bc..0000000
--- a/askama_derive/src/parser/node.rs
+++ /dev/null
@@ -1,674 +0,0 @@
-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),
-            opt(ws(Expr::parse_arguments)),
-            opt(expr_handle_ws),
-        ))),
-    ));
-    let (i, (pws, _, (scope, name, args, nws))) = p(i)?;
-    let scope = scope.map(|(scope, _)| scope);
-    let args = args.unwrap_or_default();
-    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),
-            opt(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'");
-
-    let params = params.unwrap_or_default();
-
-    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)(i)?;
-        match take_until::<_, _, Error<_>>(s.syntax.comment_start)(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
deleted file mode 100644
index 0e785eb..0000000
--- a/askama_derive/src/parser/tests.rs
+++ /dev/null
@@ -1,712 +0,0 @@
-use super::{Expr, Node, Syntax, 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 test_rust_macro() {
-    let syntax = Syntax::default();
-    assert_eq!(
-        super::parse("{{ vec!(1, 2, 3) }}", &syntax).unwrap(),
-        vec![Node::Expr(
-            Ws(None, None),
-            Expr::RustMacro(vec!["vec"], "1, 2, 3",),
-        )],
-    );
-    assert_eq!(
-        super::parse("{{ alloc::vec!(1, 2, 3) }}", &syntax).unwrap(),
-        vec![Node::Expr(
-            Ws(None, None),
-            Expr::RustMacro(vec!["alloc", "vec"], "1, 2, 3",),
-        )],
-    );
-    assert_eq!(
-        super::parse("{{a!()}}", &syntax).unwrap(),
-        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["a"], ""))],
-    );
-    assert_eq!(
-        super::parse("{{a !()}}", &syntax).unwrap(),
-        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["a"], ""))],
-    );
-    assert_eq!(
-        super::parse("{{a! ()}}", &syntax).unwrap(),
-        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["a"], ""))],
-    );
-    assert_eq!(
-        super::parse("{{a ! ()}}", &syntax).unwrap(),
-        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["a"], ""))],
-    );
-    assert_eq!(
-        super::parse("{{A!()}}", &syntax).unwrap(),
-        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["A"], ""),)],
-    );
-    assert_eq!(
-        &*super::parse("{{a.b.c!( hello )}}", &syntax)
-            .unwrap_err()
-            .to_string(),
-        "problems parsing template source at row 1, column 7 near:\n\"!( hello )}}\"",
-    );
-}
-
-#[test]
-fn change_delimiters_parse_filter() {
-    let syntax = Syntax {
-        expr_start: "{=",
-        expr_end: "=}",
-        ..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.to_string(),
-        "unable to parse template:\n\n\"{%leta=b%}\""
-    ));
-}
diff --git a/askama_parser/Cargo.toml b/askama_parser/Cargo.toml
new file mode 100644
index 0000000..8af58df
--- /dev/null
+++ b/askama_parser/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "askama_parser"
+version = "0.1.0"
+description = "Parser for Askama templates"
+documentation = "https://docs.rs/askama"
+keywords = ["markup", "template", "jinja2", "html"]
+categories = ["template-engine"]
+homepage = "https://github.com/djc/askama"
+repository = "https://github.com/djc/askama"
+license = "MIT OR Apache-2.0"
+workspace = ".."
+readme = "README.md"
+edition = "2021"
+rust-version = "1.58"
+
+[dependencies]
+nom = { version = "7", default-features = false, features = ["alloc"] }
diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs
new file mode 100644
index 0000000..03620e7
--- /dev/null
+++ b/askama_parser/src/expr.rs
@@ -0,0 +1,285 @@
+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::error::ErrorKind;
+use nom::multi::{fold_many0, many0, separated_list0, separated_list1};
+use nom::sequence::{delimited, pair, preceded, terminated, tuple};
+use nom::{error_position, IResult};
+
+use super::{
+    bool_lit, char_lit, identifier, nested_parenthesis, not_ws, num_lit, path, str_lit, ws,
+};
+
+#[derive(Debug, PartialEq)]
+pub 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(Vec<&'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)
+    }
+}
+
+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_array_lit,
+        expr_var,
+        expr_group,
+    ))(i)
+}
+
+enum Suffix<'a> {
+    Attr(&'a str),
+    Index(Expr<'a>),
+    Call(Vec<Expr<'a>>),
+    // The value is the arguments of the macro call.
+    MacroCall(&'a str),
+    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_macro(i: &str) -> IResult<&str, Suffix<'_>> {
+    preceded(
+        pair(ws(char('!')), char('(')),
+        cut(terminated(
+            map(recognize(nested_parenthesis), Suffix::MacroCall),
+            char(')'),
+        )),
+    )(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, expr_macro,
+        )))(i)?;
+        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()),
+            Some(Suffix::MacroCall(args)) => match expr {
+                Expr::Path(path) => expr = Expr::RustMacro(path, args),
+                Expr::Var(name) => expr = Expr::RustMacro(vec![name], args),
+                _ => return Err(nom::Err::Failure(error_position!(i, ErrorKind::Tag))),
+            },
+            None => break,
+        }
+        i = j;
+    }
+    Ok((i, expr))
+}
+
+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_parser/src/lib.rs b/askama_parser/src/lib.rs
new file mode 100644
index 0000000..336004e
--- /dev/null
+++ b/askama_parser/src/lib.rs
@@ -0,0 +1,384 @@
+#![deny(unreachable_pub)]
+#![deny(elided_lifetimes_in_paths)]
+
+use std::cell::Cell;
+use std::{fmt, 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 use self::expr::Expr;
+pub use self::node::{Cond, CondTest, Loop, Macro, Node, Target, When, Whitespace, Ws};
+
+mod expr;
+mod node;
+#[cfg(test)]
+mod tests;
+
+struct State<'a> {
+    syntax: &'a Syntax<'a>,
+    loop_depth: Cell<usize>,
+}
+
+impl<'a> State<'a> {
+    fn new(syntax: &'a Syntax<'a>) -> State<'a> {
+        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"),
+        }
+    }
+}
+
+mod _parsed {
+    use std::mem;
+
+    use super::{parse, Node, ParseError, Syntax};
+
+    pub struct Parsed {
+        #[allow(dead_code)]
+        source: String,
+        nodes: Vec<Node<'static>>,
+    }
+
+    impl Parsed {
+        pub fn new(source: String, syntax: &Syntax<'_>) -> Result<Self, ParseError> {
+            // Self-referential borrowing: `self` will keep the source alive as `String`,
+            // internally we will transmute it to `&'static str` to satisfy the compiler.
+            // However, we only expose the nodes with a lifetime limited to `self`.
+            let src = unsafe { mem::transmute::<&str, &'static str>(source.as_str()) };
+            let nodes = match parse(src, syntax) {
+                Ok(nodes) => nodes,
+                Err(e) => return Err(e),
+            };
+
+            Ok(Self { source, nodes })
+        }
+
+        // The return value's lifetime must be limited to `self` to uphold the unsafe invariant.
+        pub fn nodes(&self) -> &[Node<'_>] {
+            &self.nodes
+        }
+    }
+}
+
+pub use _parsed::Parsed;
+
+pub fn parse<'a>(src: &'a str, syntax: &Syntax<'_>) -> Result<Vec<Node<'a>>, ParseError> {
+    match Node::parse(src, &State::new(syntax)) {
+        Ok((left, res)) => {
+            if !left.is_empty() {
+                Err(ParseError(format!("unable to parse template:\n\n{left:?}")))
+            } 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(ParseError(msg))
+        }
+
+        Err(nom::Err::Incomplete(_)) => Err(ParseError("parsing incomplete".into())),
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ParseError(String);
+
+impl std::error::Error for ParseError {}
+
+impl fmt::Display for ParseError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+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),
+        tag(s.syntax.comment_start),
+        tag(s.syntax.expr_start),
+    ));
+
+    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)(i)
+}
+
+fn tag_block_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+    tag(s.syntax.block_end)(i)
+}
+
+fn tag_comment_start<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+    tag(s.syntax.comment_start)(i)
+}
+
+fn tag_comment_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+    tag(s.syntax.comment_end)(i)
+}
+
+fn tag_expr_start<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+    tag(s.syntax.expr_start)(i)
+}
+
+fn tag_expr_end<'a>(i: &'a str, s: &State<'_>) -> IResult<&'a str, &'a str> {
+    tag(s.syntax.expr_end)(i)
+}
+
+#[derive(Debug)]
+pub struct Syntax<'a> {
+    pub block_start: &'a str,
+    pub block_end: &'a str,
+    pub expr_start: &'a str,
+    pub expr_end: &'a str,
+    pub comment_start: &'a str,
+    pub comment_end: &'a str,
+}
+
+impl Default for Syntax<'static> {
+    fn default() -> Self {
+        Self {
+            block_start: "{%",
+            block_end: "%}",
+            expr_start: "{{",
+            expr_end: "}}",
+            comment_start: "{#",
+            comment_end: "#}",
+        }
+    }
+}
diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs
new file mode 100644
index 0000000..8719d38
--- /dev/null
+++ b/askama_parser/src/node.rs
@@ -0,0 +1,674 @@
+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 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 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 enum Whitespace {
+    Preserve,
+    Suppress,
+    Minimize,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Loop<'a> {
+    pub ws1: Ws,
+    pub var: Target<'a>,
+    pub iter: Expr<'a>,
+    pub cond: Option<Expr<'a>>,
+    pub body: Vec<Node<'a>>,
+    pub ws2: Ws,
+    pub else_block: Vec<Node<'a>>,
+    pub ws3: Ws,
+}
+
+pub type When<'a> = (Ws, Target<'a>, Vec<Node<'a>>);
+
+#[derive(Debug, PartialEq)]
+pub struct Macro<'a> {
+    pub ws1: Ws,
+    pub args: Vec<&'a str>,
+    pub nodes: Vec<Node<'a>>,
+    pub 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 struct Ws(pub Option<Whitespace>, pub Option<Whitespace>);
+
+pub type Cond<'a> = (Ws, Option<CondTest<'a>>, Vec<Node<'a>>);
+
+#[derive(Debug, PartialEq)]
+pub struct CondTest<'a> {
+    pub target: Option<Target<'a>>,
+    pub 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),
+            opt(ws(Expr::parse_arguments)),
+            opt(expr_handle_ws),
+        ))),
+    ));
+    let (i, (pws, _, (scope, name, args, nws))) = p(i)?;
+    let scope = scope.map(|(scope, _)| scope);
+    let args = args.unwrap_or_default();
+    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),
+            opt(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'");
+
+    let params = params.unwrap_or_default();
+
+    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)(i)?;
+        match take_until::<_, _, Error<_>>(s.syntax.comment_start)(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_parser/src/tests.rs b/askama_parser/src/tests.rs
new file mode 100644
index 0000000..0e785eb
--- /dev/null
+++ b/askama_parser/src/tests.rs
@@ -0,0 +1,712 @@
+use super::{Expr, Node, Syntax, 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 test_rust_macro() {
+    let syntax = Syntax::default();
+    assert_eq!(
+        super::parse("{{ vec!(1, 2, 3) }}", &syntax).unwrap(),
+        vec![Node::Expr(
+            Ws(None, None),
+            Expr::RustMacro(vec!["vec"], "1, 2, 3",),
+        )],
+    );
+    assert_eq!(
+        super::parse("{{ alloc::vec!(1, 2, 3) }}", &syntax).unwrap(),
+        vec![Node::Expr(
+            Ws(None, None),
+            Expr::RustMacro(vec!["alloc", "vec"], "1, 2, 3",),
+        )],
+    );
+    assert_eq!(
+        super::parse("{{a!()}}", &syntax).unwrap(),
+        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["a"], ""))],
+    );
+    assert_eq!(
+        super::parse("{{a !()}}", &syntax).unwrap(),
+        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["a"], ""))],
+    );
+    assert_eq!(
+        super::parse("{{a! ()}}", &syntax).unwrap(),
+        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["a"], ""))],
+    );
+    assert_eq!(
+        super::parse("{{a ! ()}}", &syntax).unwrap(),
+        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["a"], ""))],
+    );
+    assert_eq!(
+        super::parse("{{A!()}}", &syntax).unwrap(),
+        [Node::Expr(Ws(None, None), Expr::RustMacro(vec!["A"], ""),)],
+    );
+    assert_eq!(
+        &*super::parse("{{a.b.c!( hello )}}", &syntax)
+            .unwrap_err()
+            .to_string(),
+        "problems parsing template source at row 1, column 7 near:\n\"!( hello )}}\"",
+    );
+}
+
+#[test]
+fn change_delimiters_parse_filter() {
+    let syntax = Syntax {
+        expr_start: "{=",
+        expr_end: "=}",
+        ..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.to_string(),
+        "unable to parse template:\n\n\"{%leta=b%}\""
+    ));
+}
-- 
cgit