generator: add named arguments for filters

This commit is contained in:
René Kijewski 2025-04-17 17:15:00 +02:00 committed by René Kijewski
parent 6f8de0ca84
commit b402936db3
11 changed files with 625 additions and 127 deletions

View File

@ -65,6 +65,7 @@ impl<'a> Generator<'a, '_> {
Expr::As(ref expr, target) => self.visit_as(ctx, buf, expr, target)?, Expr::As(ref expr, target) => self.visit_as(ctx, buf, expr, target)?,
Expr::Concat(ref exprs) => self.visit_concat(ctx, buf, exprs)?, Expr::Concat(ref exprs) => self.visit_concat(ctx, buf, exprs)?,
Expr::LetCond(ref cond) => self.visit_let_cond(ctx, buf, cond)?, Expr::LetCond(ref cond) => self.visit_let_cond(ctx, buf, cond)?,
Expr::ArgumentPlaceholder => DisplayWrap::Unwrapped,
}) })
} }

View File

@ -1,11 +1,13 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt::{self, Write};
use std::mem::replace;
use parser::{Expr, IntKind, Num, Span, StrLit, StrPrefix, TyGenerics, WithSpan}; use parser::{Expr, IntKind, Num, Span, StrLit, StrPrefix, TyGenerics, WithSpan};
use super::{DisplayWrap, Generator, TargetIsize, TargetUsize}; use super::{DisplayWrap, Generator, TargetIsize, TargetUsize};
use crate::heritage::Context; use crate::heritage::Context;
use crate::integration::Buffer; use crate::integration::Buffer;
use crate::{CompileError, MsgValidEscapers}; use crate::{CompileError, MsgValidEscapers, fmt_left, fmt_right};
impl<'a> Generator<'a, '_> { impl<'a> Generator<'a, '_> {
pub(super) fn visit_filter( pub(super) fn visit_filter(
@ -60,8 +62,9 @@ impl<'a> Generator<'a, '_> {
name: &str, name: &str,
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
generics: &[WithSpan<'a, TyGenerics<'a>>], generics: &[WithSpan<'a, TyGenerics<'a>>],
_node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
ensure_no_named_arguments(ctx, name, args, node)?;
buf.write(format_args!("filters::{name}")); buf.write(format_args!("filters::{name}"));
self.visit_call_generics(buf, generics); self.visit_call_generics(buf, generics);
buf.write('('); buf.write('(');
@ -111,10 +114,11 @@ impl<'a> Generator<'a, '_> {
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
ensure_no_generics(ctx, name, node, generics)?; ensure_no_generics(ctx, name, node, generics)?;
let arg = no_arguments(ctx, name, args)?;
buf.write(format_args!("askama::filters::{name}")); buf.write(format_args!("askama::filters::{name}"));
self.visit_call_generics(buf, generics); self.visit_call_generics(buf, generics);
buf.write('('); buf.write('(');
self.visit_args(ctx, buf, args)?; self.visit_arg(ctx, buf, arg)?;
buf.write(")?"); buf.write(")?");
Ok(DisplayWrap::Unwrapped) Ok(DisplayWrap::Unwrapped)
} }
@ -154,11 +158,12 @@ impl<'a> Generator<'a, '_> {
)); ));
} }
let arg = no_arguments(ctx, name, args)?;
// Both filters return HTML-safe strings. // Both filters return HTML-safe strings.
buf.write(format_args!( buf.write(format_args!(
"askama::filters::HtmlSafeOutput(askama::filters::{name}(", "askama::filters::HtmlSafeOutput(askama::filters::{name}(",
)); ));
self.visit_args(ctx, buf, args)?; self.visit_arg(ctx, buf, arg)?;
buf.write(")?)"); buf.write(")?)");
Ok(DisplayWrap::Unwrapped) Ok(DisplayWrap::Unwrapped)
} }
@ -171,15 +176,10 @@ impl<'a> Generator<'a, '_> {
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
ensure_filter_has_feature_alloc(ctx, "wordcount", node)?; ensure_filter_has_feature_alloc(ctx, "wordcount", node)?;
if args.len() != 1 {
return Err(ctx.generate_error(
format_args!("unexpected argument(s) in `wordcount` filter"),
node,
));
}
let arg = no_arguments(ctx, "wordcount", args)?;
buf.write("match askama::filters::wordcount(&("); buf.write("match askama::filters::wordcount(&(");
self.visit_args(ctx, buf, args)?; self.visit_arg(ctx, buf, arg)?;
buf.write( buf.write(
")) {\ ")) {\
expr0 => {\ expr0 => {\
@ -201,12 +201,13 @@ impl<'a> Generator<'a, '_> {
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
_node: Span<'_>, _node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
let arg = no_arguments(ctx, "humansize", args)?;
// All filters return numbers, and any default formatted number is HTML safe. // All filters return numbers, and any default formatted number is HTML safe.
buf.write(format_args!( buf.write(format_args!(
"askama::filters::HtmlSafeOutput(askama::filters::filesizeformat(\ "askama::filters::HtmlSafeOutput(askama::filters::filesizeformat(\
askama::helpers::get_primitive_value(&(" askama::helpers::get_primitive_value(&("
)); ));
self.visit_args(ctx, buf, args)?; self.visit_arg(ctx, buf, arg)?;
buf.write(")) as askama::helpers::core::primitive::f32)?)"); buf.write(")) as askama::helpers::core::primitive::f32)?)");
Ok(DisplayWrap::Unwrapped) Ok(DisplayWrap::Unwrapped)
} }
@ -228,17 +229,20 @@ impl<'a> Generator<'a, '_> {
prefix: None, prefix: None,
content: "s", content: "s",
})); }));
const ARGUMENTS: &[&FilterArgument; 3] = &[
FILTER_SOURCE,
&FilterArgument {
name: "sg",
default_value: Some(SINGULAR),
},
&FilterArgument {
name: "pl",
default_value: Some(PLURAL),
},
];
let [count, sg, pl] = collect_filter_args(ctx, "pluralize", node, args, ARGUMENTS)?;
let (count, sg, pl) = match args {
[count] => (count, SINGULAR, PLURAL),
[count, sg] => (count, sg, PLURAL),
[count, sg, pl] => (count, sg, pl),
_ => {
return Err(
ctx.generate_error("unexpected argument(s) in `pluralize` filter", node)
);
}
};
if let Some(is_singular) = expr_is_int_lit_plus_minus_one(count) { if let Some(is_singular) = expr_is_int_lit_plus_minus_one(count) {
let value = if is_singular { sg } else { pl }; let value = if is_singular { sg } else { pl };
self.visit_auto_escaped_arg(ctx, buf, value)?; self.visit_auto_escaped_arg(ctx, buf, value)?;
@ -293,16 +297,11 @@ impl<'a> Generator<'a, '_> {
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
ensure_filter_has_feature_alloc(ctx, name, node)?; ensure_filter_has_feature_alloc(ctx, name, node)?;
if args.len() != 1 { let arg = no_arguments(ctx, name, args)?;
return Err(ctx.generate_error(
format_args!("unexpected argument(s) in `{name}` filter"),
node,
));
}
buf.write(format_args!( buf.write(format_args!(
"askama::filters::{name}(&(&&askama::filters::AutoEscaper::new(&(", "askama::filters::{name}(&(&&askama::filters::AutoEscaper::new(&(",
)); ));
self.visit_args(ctx, buf, args)?; self.visit_arg(ctx, buf, arg)?;
// The input is always HTML escaped, regardless of the selected escaper: // The input is always HTML escaped, regardless of the selected escaper:
buf.write("), askama::filters::Html)).askama_auto_escape()?)?"); buf.write("), askama::filters::Html)).askama_auto_escape()?)?");
// The output is marked as HTML safe, not safe in all contexts: // The output is marked as HTML safe, not safe in all contexts:
@ -314,12 +313,9 @@ impl<'a> Generator<'a, '_> {
ctx: &Context<'_>, ctx: &Context<'_>,
buf: &mut Buffer, buf: &mut Buffer,
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>, _node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
let arg = match args { let arg = no_arguments(ctx, "ref", args)?;
[arg] => arg,
_ => return Err(ctx.generate_error("unexpected argument(s) in `as_ref` filter", node)),
};
buf.write('&'); buf.write('&');
self.visit_expr(ctx, buf, arg)?; self.visit_expr(ctx, buf, arg)?;
Ok(DisplayWrap::Unwrapped) Ok(DisplayWrap::Unwrapped)
@ -330,12 +326,9 @@ impl<'a> Generator<'a, '_> {
ctx: &Context<'_>, ctx: &Context<'_>,
buf: &mut Buffer, buf: &mut Buffer,
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>, _node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
let arg = match args { let arg = no_arguments(ctx, "deref", args)?;
[arg] => arg,
_ => return Err(ctx.generate_error("unexpected argument(s) in `deref` filter", node)),
};
buf.write('*'); buf.write('*');
self.visit_expr(ctx, buf, arg)?; self.visit_expr(ctx, buf, arg)?;
Ok(DisplayWrap::Unwrapped) Ok(DisplayWrap::Unwrapped)
@ -348,6 +341,14 @@ impl<'a> Generator<'a, '_> {
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
const ARGUMENTS: &[&FilterArgument; 2] = &[
FILTER_SOURCE,
&FilterArgument {
name: "indent",
default_value: Some(ARGUMENT_PLACEHOLDER),
},
];
if cfg!(not(feature = "serde_json")) { if cfg!(not(feature = "serde_json")) {
return Err(ctx.generate_error( return Err(ctx.generate_error(
"the `json` filter requires the `serde_json` feature to be enabled", "the `json` filter requires the `serde_json` feature to be enabled",
@ -355,14 +356,18 @@ impl<'a> Generator<'a, '_> {
)); ));
} }
let filter = match args.len() { let [value, indent] = collect_filter_args(ctx, "json", node, args, ARGUMENTS)?;
1 => "json", if is_argument_placeholder(indent) {
2 => "json_pretty", buf.write(format_args!("askama::filters::json("));
_ => return Err(ctx.generate_error("unexpected argument(s) in `json` filter", node)), self.visit_arg(ctx, buf, value)?;
}; buf.write(")?");
buf.write(format_args!("askama::filters::{filter}(")); } else {
self.visit_args(ctx, buf, args)?; buf.write(format_args!("askama::filters::json_pretty("));
buf.write(")?"); self.visit_arg(ctx, buf, value)?;
buf.write(',');
self.visit_arg(ctx, buf, indent)?;
buf.write(")?");
}
Ok(DisplayWrap::Unwrapped) Ok(DisplayWrap::Unwrapped)
} }
@ -375,18 +380,25 @@ impl<'a> Generator<'a, '_> {
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
const FALSE: &WithSpan<'static, Expr<'static>> = const FALSE: &WithSpan<'static, Expr<'static>> =
&WithSpan::new_without_span(Expr::BoolLit(false)); &WithSpan::new_without_span(Expr::BoolLit(false));
const ARGUMENTS: &[&FilterArgument; 4] = &[
FILTER_SOURCE,
&FilterArgument {
name: "width",
default_value: None,
},
&FilterArgument {
name: "first",
default_value: Some(FALSE),
},
&FilterArgument {
name: "blank",
default_value: Some(FALSE),
},
];
ensure_filter_has_feature_alloc(ctx, "indent", node)?; ensure_filter_has_feature_alloc(ctx, "indent", node)?;
let (source, indent, first, blank) = let [source, indent, first, blank] =
match args { collect_filter_args(ctx, "indent", node, args, ARGUMENTS)?;
[source, indent] => (source, indent, FALSE, FALSE),
[source, indent, first] => (source, indent, first, FALSE),
[source, indent, first, blank] => (source, indent, first, blank),
_ => return Err(ctx.generate_error(
"filter `indent` needs a `width` argument, and can have two optional arguments",
node,
)),
};
buf.write("askama::filters::indent("); buf.write("askama::filters::indent(");
self.visit_arg(ctx, buf, source)?; self.visit_arg(ctx, buf, source)?;
buf.write(","); buf.write(",");
@ -404,13 +416,11 @@ impl<'a> Generator<'a, '_> {
ctx: &Context<'_>, ctx: &Context<'_>,
buf: &mut Buffer, buf: &mut Buffer,
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>, _node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
if args.len() != 1 { let arg = no_arguments(ctx, "safe", args)?;
return Err(ctx.generate_error("unexpected argument(s) in `safe` filter", node));
}
buf.write("askama::filters::safe("); buf.write("askama::filters::safe(");
self.visit_args(ctx, buf, args)?; self.visit_arg(ctx, buf, arg)?;
buf.write(format_args!(", {})?", self.input.escaper)); buf.write(format_args!(", {})?", self.input.escaper));
Ok(DisplayWrap::Wrapped) Ok(DisplayWrap::Wrapped)
} }
@ -422,31 +432,37 @@ impl<'a> Generator<'a, '_> {
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
if args.len() > 2 { const ARGUMENTS: &[&FilterArgument; 2] = &[
return Err(ctx.generate_error("only two arguments allowed to escape filter", node)); FILTER_SOURCE,
} &FilterArgument {
let opt_escaper = match args.get(1).map(|expr| &**expr) { name: "escaper",
Some(Expr::StrLit(StrLit { prefix, content })) => { default_value: Some(ARGUMENT_PLACEHOLDER),
if let Some(prefix) = prefix { },
let kind = if *prefix == StrPrefix::Binary { ];
"slice"
} else { let [source, opt_escaper] = collect_filter_args(ctx, "escape", node, args, ARGUMENTS)?;
"CStr" let opt_escaper = if !is_argument_placeholder(opt_escaper) {
}; let Expr::StrLit(StrLit { prefix, content }) = **opt_escaper else {
return Err(ctx.generate_error(
format_args!(
"invalid escaper `b{content:?}`. Expected a string, found a {kind}"
),
args[1].span(),
));
}
Some(content)
}
Some(_) => {
return Err(ctx.generate_error("invalid escaper type for escape filter", node)); return Err(ctx.generate_error("invalid escaper type for escape filter", node));
};
if let Some(prefix) = prefix {
let kind = if prefix == StrPrefix::Binary {
"slice"
} else {
"CStr"
};
return Err(ctx.generate_error(
format_args!(
"invalid escaper `b{content:?}`. Expected a string, found a {kind}"
),
opt_escaper.span(),
));
} }
None => None, Some(content)
} else {
None
}; };
let escaper = match opt_escaper { let escaper = match opt_escaper {
Some(name) => self Some(name) => self
.input .input
@ -461,7 +477,8 @@ impl<'a> Generator<'a, '_> {
.ok_or_else(|| { .ok_or_else(|| {
ctx.generate_error( ctx.generate_error(
format_args!( format_args!(
"invalid escaper '{name}' for `escape` filter. {}", "invalid escaper `{}` for `escape` filter. {}",
name.escape_debug(),
MsgValidEscapers(&self.input.config.escapers), MsgValidEscapers(&self.input.config.escapers),
), ),
node, node,
@ -470,7 +487,7 @@ impl<'a> Generator<'a, '_> {
None => self.input.escaper, None => self.input.escaper,
}; };
buf.write("askama::filters::escape("); buf.write("askama::filters::escape(");
self.visit_args(ctx, buf, &args[..1])?; self.visit_arg(ctx, buf, source)?;
buf.write(format_args!(", {escaper})?")); buf.write(format_args!(", {escaper})?"));
Ok(DisplayWrap::Wrapped) Ok(DisplayWrap::Wrapped)
} }
@ -483,6 +500,7 @@ impl<'a> Generator<'a, '_> {
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
ensure_filter_has_feature_alloc(ctx, "format", node)?; ensure_filter_has_feature_alloc(ctx, "format", node)?;
ensure_no_named_arguments(ctx, "format", args, node)?;
if !args.is_empty() { if !args.is_empty() {
if let Expr::StrLit(ref fmt) = *args[0] { if let Expr::StrLit(ref fmt) = *args[0] {
buf.write("askama::helpers::alloc::format!("); buf.write("askama::helpers::alloc::format!(");
@ -495,7 +513,10 @@ impl<'a> Generator<'a, '_> {
return Ok(DisplayWrap::Unwrapped); return Ok(DisplayWrap::Unwrapped);
} }
} }
Err(ctx.generate_error(r#"use filter format like `"a={} b={}"|format(a, b)`"#, node)) Err(ctx.generate_error(
r#"use `format` filter like `"a={} b={}"|format(a, b)`"#,
node,
))
} }
fn visit_fmt_filter( fn visit_fmt_filter(
@ -505,18 +526,25 @@ impl<'a> Generator<'a, '_> {
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
const ARGUMENTS: &[&FilterArgument; 2] = &[
FILTER_SOURCE,
&FilterArgument {
name: "format",
default_value: None,
},
];
ensure_filter_has_feature_alloc(ctx, "fmt", node)?; ensure_filter_has_feature_alloc(ctx, "fmt", node)?;
if let [_, arg2] = args { let [source, fmt] = collect_filter_args(ctx, "fmt", node, args, ARGUMENTS)?;
if let Expr::StrLit(ref fmt) = **arg2 { let Expr::StrLit(ref fmt) = **fmt else {
buf.write("askama::helpers::alloc::format!("); return Err(ctx.generate_error(r#"use `fmt` filter like `value|fmt("{:?}")`"#, node));
self.visit_str_lit(buf, fmt); };
buf.write(','); buf.write("askama::helpers::alloc::format!(");
self.visit_args(ctx, buf, &args[..1])?; self.visit_str_lit(buf, fmt);
buf.write(')'); buf.write(',');
return Ok(DisplayWrap::Unwrapped); self.visit_arg(ctx, buf, source)?;
} buf.write(')');
} Ok(DisplayWrap::Unwrapped)
Err(ctx.generate_error(r#"use filter fmt like `value|fmt("{:?}")`"#, node))
} }
// Force type coercion on first argument to `join` filter (see #39). // Force type coercion on first argument to `join` filter (see #39).
@ -525,18 +553,21 @@ impl<'a> Generator<'a, '_> {
ctx: &Context<'_>, ctx: &Context<'_>,
buf: &mut Buffer, buf: &mut Buffer,
args: &[WithSpan<'a, Expr<'a>>], args: &[WithSpan<'a, Expr<'a>>],
_node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
buf.write("askama::filters::join((&"); const ARGUMENTS: &[&FilterArgument; 2] = &[
for (i, arg) in args.iter().enumerate() { FILTER_SOURCE,
if i > 0 { &FilterArgument {
buf.write(", &"); name: "separator",
} default_value: None,
self.visit_expr(ctx, buf, arg)?; },
if i == 0 { ];
buf.write(").into_iter()");
} let [iterable, separator] = collect_filter_args(ctx, "join", node, args, ARGUMENTS)?;
} buf.write("askama::filters::join((&(");
self.visit_arg(ctx, buf, iterable)?;
buf.write(")).into_iter(),");
self.visit_arg(ctx, buf, separator)?;
buf.write(")?"); buf.write(")?");
Ok(DisplayWrap::Unwrapped) Ok(DisplayWrap::Unwrapped)
} }
@ -569,14 +600,16 @@ impl<'a> Generator<'a, '_> {
node: Span<'_>, node: Span<'_>,
name: &str, name: &str,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
ensure_filter_has_feature_alloc(ctx, name, node)?; const ARGUMENTS: &[&FilterArgument; 2] = &[
let [arg, length] = args else { FILTER_SOURCE,
return Err(ctx.generate_error( &FilterArgument {
format_args!("`{name}` filter needs one argument, the `length`"), name: "length",
node, default_value: None,
)); },
}; ];
ensure_filter_has_feature_alloc(ctx, name, node)?;
let [arg, length] = collect_filter_args(ctx, name, node, args, ARGUMENTS)?;
buf.write(format_args!("askama::filters::{name}(")); buf.write(format_args!("askama::filters::{name}("));
self.visit_arg(ctx, buf, arg)?; self.visit_arg(ctx, buf, arg)?;
buf.write( buf.write(
@ -697,6 +730,185 @@ fn expr_is_int_lit_plus_minus_one(expr: &WithSpan<'_, Expr<'_>>) -> Option<bool>
} }
} }
struct FilterArgument {
name: &'static str,
/// If set to `None`, then a value is needed.
/// If set to `Some(ARGUMENT_PLACEHOLDER)`, then no value has to be assigned.
/// If set to `Some(&WithSpan...)`, then this value will be used if no argument was supplied.
default_value: Option<&'static WithSpan<'static, Expr<'static>>>,
}
/// Must be the first entry to `collect_filter_args()`'s argument `filter_args`.
const FILTER_SOURCE: &FilterArgument = &FilterArgument {
name: "",
default_value: None,
};
const ARGUMENT_PLACEHOLDER: &WithSpan<'_, Expr<'_>> =
&WithSpan::new_without_span(Expr::ArgumentPlaceholder);
#[inline]
fn is_argument_placeholder(arg: &WithSpan<'_, Expr<'_>>) -> bool {
matches!(**arg, Expr::ArgumentPlaceholder)
}
fn no_arguments<'a, 'b>(
ctx: &Context<'_>,
name: &str,
args: &'b [WithSpan<'a, Expr<'a>>],
) -> Result<&'b WithSpan<'a, Expr<'a>>, CompileError> {
match args {
[arg] => Ok(arg),
[_, arg, ..] => Err(ctx.generate_error(
format_args!("`{name}` filter does not have any arguments"),
arg.span(),
)),
_ => unreachable!(),
}
}
#[inline]
fn collect_filter_args<'a, 'b, const N: usize>(
ctx: &Context<'_>,
name: &str,
node: Span<'_>,
input_args: &'b [WithSpan<'a, Expr<'a>>],
filter_args: &'static [&'static FilterArgument; N],
) -> Result<[&'b WithSpan<'a, Expr<'a>>; N], CompileError> {
let mut collected_args = [ARGUMENT_PLACEHOLDER; N];
// rationale: less code duplication by implementing the bulk of the function non-generic
collect_filter_args_inner(
ctx,
name,
node,
input_args,
filter_args,
&mut collected_args,
)?;
Ok(collected_args)
}
fn collect_filter_args_inner<'a, 'b>(
ctx: &Context<'_>,
name: &str,
node: Span<'_>,
input_args: &'b [WithSpan<'a, Expr<'a>>],
filter_args: &'static [&'static FilterArgument],
collected_args: &mut [&'b WithSpan<'a, Expr<'a>>],
) -> Result<(), CompileError> {
// invariant: the parser ensures that named arguments come after positional arguments
let mut arg_idx = 0;
for arg in input_args {
let (idx, value) = if let Expr::NamedArgument(arg_name, ref value) = **arg {
let Some(idx) = filter_args
.iter()
.enumerate()
.find_map(|(idx, arg)| (arg.name == arg_name).then_some(idx))
else {
return Err(ctx.generate_error(
match filter_args.len() {
1 => fmt_left!("`{name}` filter does not have any arguments"),
_ => fmt_right!(
"`{name}` filter does not have an argument `{}`{}",
arg_name.escape_debug(),
ItsArgumentsAre(filter_args),
),
},
arg.span(),
));
};
(idx, &**value)
} else {
let idx = arg_idx;
arg_idx += 1;
(idx, arg)
};
let Some(collected_arg) = collected_args.get_mut(idx) else {
return Err(ctx.generate_error(
format_args!(
"`{name}` filter accepts at most {} argument{}{}",
filter_args.len() - 1,
if filter_args.len() != 2 { "s" } else { "" },
ItsArgumentsAre(filter_args),
),
arg.span(),
));
};
if !is_argument_placeholder(replace(collected_arg, value)) {
return Err(ctx.generate_error(
format_args!(
"`{}` argument to `{}` filter was already set{}",
filter_args[idx].name.escape_debug(),
name.escape_debug(),
ItsArgumentsAre(filter_args),
),
arg.span(),
));
}
}
for (&arg, collected) in filter_args.iter().zip(collected_args) {
if !is_argument_placeholder(collected) {
continue;
} else if let Some(default) = arg.default_value {
*collected = default;
} else {
return Err(ctx.generate_error(
format_args!(
"`{}` argument is missing when calling `{name}` filter{}",
arg.name.escape_debug(),
ItsArgumentsAre(filter_args),
),
node,
));
}
}
Ok(())
}
struct ItsArgumentsAre(&'static [&'static FilterArgument]);
impl fmt::Display for ItsArgumentsAre {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("; its arguments are: (")?;
for (idx, arg) in self.0.iter().enumerate() {
match idx {
0 => continue,
1 => {}
_ => f.write_str(", ")?,
}
if arg.default_value.is_some() {
write!(f, "[{}]", arg.name)?;
} else {
f.write_str(arg.name)?;
}
}
f.write_char(')')
}
}
fn ensure_no_named_arguments(
ctx: &Context<'_>,
name: &str,
args: &[WithSpan<'_, Expr<'_>>],
node: Span<'_>,
) -> Result<(), CompileError> {
for arg in args {
if is_argument_placeholder(arg) {
return Err(ctx.generate_error(
format_args!(
"`{}` filter cannot accept named arguments",
name.escape_debug()
),
node,
));
}
}
Ok(())
}
// These built-in filters take no arguments, no generics, and are not feature gated. // These built-in filters take no arguments, no generics, and are not feature gated.
const BUILTIN_FILTERS: &[&str] = &[]; const BUILTIN_FILTERS: &[&str] = &[];

View File

@ -207,7 +207,8 @@ impl<'a> Generator<'a, '_> {
| Expr::FilterSource | Expr::FilterSource
| Expr::As(_, _) | Expr::As(_, _)
| Expr::Concat(_) | Expr::Concat(_)
| Expr::LetCond(_) => { | Expr::LetCond(_)
| Expr::ArgumentPlaceholder => {
*only_contains_is_defined = false; *only_contains_is_defined = false;
(EvaluatedResult::Unknown, WithSpan::new(expr, span)) (EvaluatedResult::Unknown, WithSpan::new(expr, span))
} }
@ -1559,5 +1560,6 @@ fn is_cacheable(expr: &WithSpan<'_, Expr<'_>>) -> bool {
Expr::RustMacro(_, _) => false, Expr::RustMacro(_, _) => false,
// Should never be encountered: // Should never be encountered:
Expr::FilterSource => unreachable!("FilterSource in expression?"), Expr::FilterSource => unreachable!("FilterSource in expression?"),
Expr::ArgumentPlaceholder => unreachable!("ExpressionPlaceholder in expression?"),
} }
} }

View File

@ -93,6 +93,10 @@ fn check_expr<'a>(
} }
Ok(()) Ok(())
} }
Expr::ArgumentPlaceholder => Err(winnow::error::ErrMode::Cut(ErrorContext::new(
"unreachable",
expr.span,
))),
} }
} }
@ -137,6 +141,9 @@ pub enum Expr<'a> {
Concat(Vec<WithSpan<'a, Expr<'a>>>), Concat(Vec<WithSpan<'a, Expr<'a>>>),
/// If you have `&& let Some(y)`, this variant handles it. /// If you have `&& let Some(y)`, this variant handles it.
LetCond(Box<WithSpan<'a, CondTest<'a>>>), LetCond(Box<WithSpan<'a, CondTest<'a>>>),
/// This variant should never be used directly.
/// It is used for the handling of named arguments in the generator, esp. with filters.
ArgumentPlaceholder,
} }
impl<'a> Expr<'a> { impl<'a> Expr<'a> {
@ -526,7 +533,8 @@ impl<'a> Expr<'a> {
| Self::BinOp(_, _, _) | Self::BinOp(_, _, _)
| Self::Path(_) | Self::Path(_)
| Self::Concat(_) | Self::Concat(_)
| Self::LetCond(_) => false, | Self::LetCond(_)
| Self::ArgumentPlaceholder => false,
} }
} }
} }

View File

@ -1037,7 +1037,7 @@ fn filter<'a>(
cut_err(( cut_err((
ws(identifier), ws(identifier),
opt(|i: &mut _| expr::call_generics(i, level)).map(|generics| generics.unwrap_or_default()), opt(|i: &mut _| expr::call_generics(i, level)).map(|generics| generics.unwrap_or_default()),
opt(|i: &mut _| Expr::arguments(i, level, false)), opt(|i: &mut _| Expr::arguments(i, level, true)),
)) ))
.parse_next(i) .parse_next(i)
} }

View File

@ -0,0 +1,188 @@
use askama::Template;
#[test]
fn test_pluralize_two_positional_args() {
#[derive(Template)]
#[template(
source = r#"I have {{ count }} butterfl{{ count | pluralize("y", "ies") }}."#,
ext = "txt"
)]
struct CountButterflies {
count: usize,
}
assert_eq!(
CountButterflies { count: 0 }.render().unwrap(),
"I have 0 butterflies."
);
assert_eq!(
CountButterflies { count: 1 }.render().unwrap(),
"I have 1 butterfly."
);
assert_eq!(
CountButterflies { count: 2 }.render().unwrap(),
"I have 2 butterflies."
);
}
#[test]
fn test_pluralize_positional_and_named() {
#[derive(Template)]
#[template(
source = r#"I have {{ count }} butterfl{{ count | pluralize("y", pl = "ies") }}."#,
ext = "txt"
)]
struct CountButterflies {
count: usize,
}
assert_eq!(
CountButterflies { count: 0 }.render().unwrap(),
"I have 0 butterflies."
);
assert_eq!(
CountButterflies { count: 1 }.render().unwrap(),
"I have 1 butterfly."
);
assert_eq!(
CountButterflies { count: 2 }.render().unwrap(),
"I have 2 butterflies."
);
}
#[test]
fn test_pluralize_named_reordered() {
#[derive(Template)]
#[template(
source = r#"I have {{ count }} butterfl{{ count | pluralize(pl = "ies", sg = "y") }}."#,
ext = "txt"
)]
struct CountButterflies {
count: usize,
}
assert_eq!(
CountButterflies { count: 0 }.render().unwrap(),
"I have 0 butterflies."
);
assert_eq!(
CountButterflies { count: 1 }.render().unwrap(),
"I have 1 butterfly."
);
assert_eq!(
CountButterflies { count: 2 }.render().unwrap(),
"I have 2 butterflies."
);
}
#[test]
fn test_pluralize_defaulted_and_named() {
#[derive(Template)]
#[template(
source = r#"I have {{ count }} potato{{ count | pluralize(pl = "es") }}."#,
ext = "txt"
)]
struct CountPotatoes {
count: usize,
}
assert_eq!(
CountPotatoes { count: 0 }.render().unwrap(),
"I have 0 potatoes."
);
assert_eq!(
CountPotatoes { count: 1 }.render().unwrap(),
"I have 1 potato."
);
assert_eq!(
CountPotatoes { count: 2 }.render().unwrap(),
"I have 2 potatoes."
);
}
#[test]
fn test_ident() {
const TEXT: &str = "\
Lorem Ipsum has been the industry's\n\
\n\
standard dummy text ever since the 1500s";
#[derive(Template)]
#[template(source = r#"<{{ text | indent("$$", blank = true) }}>"#, ext = "txt")]
struct IdentBlank<'a> {
text: &'a str,
}
assert_eq!(
IdentBlank { text: TEXT }.render().unwrap(),
"\
<Lorem Ipsum has been the industry's\n\
$$\n\
$$standard dummy text ever since the 1500s>"
);
#[derive(Template)]
#[template(source = r#"<{{ text | indent("$$", first = true) }}>"#, ext = "txt")]
struct IdentFirst<'a> {
text: &'a str,
}
assert_eq!(
IdentFirst { text: TEXT }.render().unwrap(),
"\
<$$Lorem Ipsum has been the industry's\n\
\n\
$$standard dummy text ever since the 1500s>"
);
#[derive(Template)]
#[template(
source = r#"<{{ text | indent("$$", blank = true, first = true) }}>"#,
ext = "txt"
)]
struct IdentBoth<'a> {
text: &'a str,
}
assert_eq!(
IdentBoth { text: TEXT }.render().unwrap(),
"\
<$$Lorem Ipsum has been the industry's\n\
$$\n\
$$standard dummy text ever since the 1500s>"
);
#[derive(Template)]
#[template(
source = r#"<{{ text | indent("$$", first = true, blank = true) }}>"#,
ext = "txt"
)]
struct IdentBoth2<'a> {
text: &'a str,
}
assert_eq!(
IdentBoth2 { text: TEXT }.render().unwrap(),
"\
<$$Lorem Ipsum has been the industry's\n\
$$\n\
$$standard dummy text ever since the 1500s>"
);
#[derive(Template)]
#[template(
source = r#"<{{ text | indent(first = true, width = "$$", blank = true) }}>"#,
ext = "txt"
)]
struct IdentBoth3<'a> {
text: &'a str,
}
assert_eq!(
IdentBoth3 { text: TEXT }.render().unwrap(),
"\
<$$Lorem Ipsum has been the industry's\n\
$$\n\
$$standard dummy text ever since the 1500s>"
);
}

View File

@ -1,6 +1,6 @@
error: unexpected argument(s) in `json` filter error: `json` filter accepts at most 1 argument; its arguments are: ([indent])
--> OneTwoThree.txt:1:3 --> OneTwoThree.txt:1:13
"1|json(2, 3) }}" "3) }}"
--> tests/ui/json-too-many-args.rs:6:34 --> tests/ui/json-too-many-args.rs:6:34
| |
6 | #[template(ext = "txt", source = "{{ 1|json(2, 3) }}")] 6 | #[template(ext = "txt", source = "{{ 1|json(2, 3) }}")]

View File

@ -0,0 +1,48 @@
use askama::Template;
#[derive(Template)]
#[template(
source = r#"I have {{ count }} butterfl{{ count | pluralize(pl = "ies", "y") }}."#,
ext = "txt"
)]
struct PositionalAfterNamed {
count: usize,
}
#[derive(Template)]
#[template(
source = r#"I have {{ count }} butterfl{{ count | pluralize(pl = "y", pl = "ies") }}."#,
ext = "txt"
)]
struct NamedRepeated {
count: usize,
}
#[derive(Template)]
#[template(
source = r#"I have {{ count }} butterfl{{ count | pluralize("y", sg = "ies") }}."#,
ext = "txt"
)]
struct NamedButAlreadyPositional {
count: usize,
}
#[derive(Template)]
#[template(
source = r#"I have {{ count }} butterfl{{ count | pluralize("y", plural = "ies") }}."#,
ext = "txt"
)]
struct NoSuchNamedArgument {
count: usize,
}
#[derive(Template)]
#[template(
source = r#"Scream: {{ message | uppercase(case = "upper") }}"#,
ext = "txt"
)]
struct NamedArgumentButNoArgumentExpected<'a> {
message: &'a str,
}
fn main() {}

View File

@ -0,0 +1,39 @@
error: named arguments must always be passed last
--> <source attribute>:1:47
"(pl = \"ies\", \"y\") }}."
--> tests/ui/named_filter_arguments.rs:5:14
|
5 | source = r#"I have {{ count }} butterfl{{ count | pluralize(pl = "ies", "y") }}."#,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: named argument `pl` was passed more than once
--> <source attribute>:1:47
"(pl = \"y\", pl = \"ies\") }}."
--> tests/ui/named_filter_arguments.rs:14:14
|
14 | source = r#"I have {{ count }} butterfl{{ count | pluralize(pl = "y", pl = "ies") }}."#,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `sg` argument to `pluralize` filter was already set; its arguments are: ([sg], [pl])
--> NamedButAlreadyPositional.txt:1:47
"(\"y\", sg = \"ies\") }}."
--> tests/ui/named_filter_arguments.rs:23:14
|
23 | source = r#"I have {{ count }} butterfl{{ count | pluralize("y", sg = "ies") }}."#,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `pluralize` filter does not have an argument `plural`; its arguments are: ([sg], [pl])
--> NoSuchNamedArgument.txt:1:47
"(\"y\", plural = \"ies\") }}."
--> tests/ui/named_filter_arguments.rs:32:14
|
32 | source = r#"I have {{ count }} butterfl{{ count | pluralize("y", plural = "ies") }}."#,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `uppercase` filter does not have any arguments
--> NamedArgumentButNoArgumentExpected.txt:1:30
"(case = \"upper\") }}"
--> tests/ui/named_filter_arguments.rs:41:14
|
41 | source = r#"Scream: {{ message | uppercase(case = "upper") }}"#,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -1,4 +1,4 @@
error: invalid escaper 'latex' for `escape` filter. The available extensions are: "", "askama", "htm", "html", "j2", "jinja", "jinja2", "md", "none", "rinja", "svg", "txt", "xml", "yml" error: invalid escaper `latex` for `escape` filter. The available extensions are: "", "askama", "htm", "html", "j2", "jinja", "jinja2", "md", "none", "rinja", "svg", "txt", "xml", "yml"
--> LocalEscaper.html:1:38 --> LocalEscaper.html:1:38
"text|escape(\"latex\")}}`." "text|escape(\"latex\")}}`."
--> tests/ui/no-such-escaper.rs:6:14 --> tests/ui/no-such-escaper.rs:6:14

View File

@ -1,4 +1,4 @@
error: filter `truncate` needs one argument, the `length` error: `length` argument is missing when calling `truncate` filter; its arguments are: (length)
--> NoArgument.html:1:3 --> NoArgument.html:1:3
"text | truncate }}" "text | truncate }}"
--> tests/ui/truncate.rs:4:21 --> tests/ui/truncate.rs:4:21
@ -6,9 +6,9 @@ error: filter `truncate` needs one argument, the `length`
4 | #[template(source = r#"{{ text | truncate }}"#, ext = "html")] 4 | #[template(source = r#"{{ text | truncate }}"#, ext = "html")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^
error: filter `truncate` needs one argument, the `length` error: `truncate` filter accepts at most 1 argument; its arguments are: (length)
--> TooManyArguments.html:1:3 --> TooManyArguments.html:1:27
"text | truncate(length, extra) }}" "extra) }}"
--> tests/ui/truncate.rs:17:21 --> tests/ui/truncate.rs:17:21
| |
17 | #[template(source = r#"{{ text | truncate(length, extra) }}"#, ext = "html")] 17 | #[template(source = r#"{{ text | truncate(length, extra) }}"#, ext = "html")]