diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index 8f0b7b31a..f1d5bbeab 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -727,6 +727,42 @@ impl<'a> Generator<'a> { args.len() ))); } + let mut output_args = Vec::new(); + let args = if args + .iter() + .any(|arg| matches!(arg, Expr::NamedArgument(_, _))) + { + // First we check that all named arguments actually exist in the called item. + for arg in args.iter() { + if let Expr::NamedArgument(arg_name, _) = arg { + if !def.args.iter().any(|arg| arg == arg_name) { + return Err(CompileError::from(format!( + "no argument named `{arg_name}` in macro {name:?}" + ))); + } + } + } + // We have named arguments, so we need to ensure that they are passed in the right + // order. + // + // To make things easier, we split them in two different vectors to make things simpler. + let (mut named_arguments, mut non_named_arguments): (Vec<_>, Vec<_>) = args + .iter() + .partition(|item| matches!(item, Expr::NamedArgument(_, _))); + for arg in &def.args { + if let Some(index) = named_arguments.iter().position(|item| match item { + Expr::NamedArgument(arg_name, _) => arg_name == arg, + _ => false, + }) { + output_args.push(named_arguments.remove(index).clone()); + } else { + output_args.push(non_named_arguments.remove(0).clone()); + } + } + &output_args + } else { + args + }; for (expr, arg) in std::iter::zip(args, &def.args) { match expr { // If `expr` is already a form of variable then @@ -1104,6 +1140,7 @@ impl<'a> Generator<'a> { Expr::RustMacro(ref path, args) => self.visit_rust_macro(buf, path, args), Expr::Try(ref expr) => self.visit_try(buf, expr.as_ref())?, Expr::Tuple(ref exprs) => self.visit_tuple(buf, exprs)?, + Expr::NamedArgument(_, ref expr) => self.visit_named_argument(buf, expr)?, }) } @@ -1504,6 +1541,15 @@ impl<'a> Generator<'a> { Ok(DisplayWrap::Unwrapped) } + fn visit_named_argument( + &mut self, + buf: &mut Buffer, + expr: &Expr<'_>, + ) -> Result { + self.visit_expr(buf, expr)?; + Ok(DisplayWrap::Unwrapped) + } + fn visit_array( &mut self, buf: &mut Buffer, @@ -1913,6 +1959,7 @@ pub(crate) fn is_cacheable(expr: &Expr<'_>) -> bool { } Expr::Group(arg) => is_cacheable(arg), Expr::Tuple(args) => args.iter().all(is_cacheable), + Expr::NamedArgument(_, expr) => is_cacheable(expr), // We have too little information to tell if the expression is pure: Expr::Call(_, _) => false, Expr::RustMacro(_, _) => false, diff --git a/askama_parser/src/expr.rs b/askama_parser/src/expr.rs index 76691be58..62da36b47 100644 --- a/askama_parser/src/expr.rs +++ b/askama_parser/src/expr.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; +use std::collections::HashSet; use std::str; use nom::branch::alt; @@ -12,7 +14,7 @@ use nom::sequence::{pair, preceded, terminated, tuple}; use super::{ char_lit, identifier, not_ws, num_lit, path_or_identifier, str_lit, ws, Level, PathOrIdentifier, }; -use crate::ParseResult; +use crate::{ErrorContext, ParseResult}; macro_rules! expr_prec_layer { ( $name:ident, $inner:ident, $op:expr ) => { @@ -61,6 +63,7 @@ pub enum Expr<'a> { Attr(Box>, &'a str), Index(Box>, Box>), Filter(&'a str, Vec>), + NamedArgument(&'a str, Box>), Unary(&'a str, Box>), BinOp(&'a str, Box>, Box>), Range(&'a str, Option>>, Option>>), @@ -74,15 +77,45 @@ pub enum Expr<'a> { impl<'a> Expr<'a> { pub(super) fn arguments(i: &'a str, level: Level) -> ParseResult<'a, Vec> { let (_, level) = level.nest(i)?; + let mut named_arguments = HashSet::new(); + let start = i; + preceded( ws(char('(')), cut(terminated( - separated_list0(char(','), ws(move |i| Self::parse(i, level))), + separated_list0( + char(','), + ws(alt(( + move |i| Self::named_argument(i, level, &mut named_arguments, start), + move |i| Self::parse(i, level), + ))), + ), char(')'), )), )(i) } + pub(super) fn named_argument( + i: &'a str, + level: Level, + named_arguments: &mut HashSet<&'a str>, + start: &'a str, + ) -> ParseResult<'a, Self> { + let (_, level) = level.nest(i)?; + let (i, (argument, _, value)) = + tuple((identifier, ws(char('=')), move |i| Self::parse(i, level)))(i)?; + if !named_arguments.insert(argument) { + Err(nom::Err::Failure(ErrorContext { + input: start, + message: Some(Cow::Owned(format!( + "named argument `{argument}` was passed more than once" + ))), + })) + } else { + Ok((i, Self::NamedArgument(argument, Box::new(value)))) + } + } + pub(super) fn parse(i: &'a str, level: Level) -> ParseResult<'a, Self> { let (_, level) = level.nest(i)?; let range_right = move |i| {