mirror of
https://github.com/askama-rs/askama.git
synced 2025-09-27 13:00:57 +00:00
generator: add named arguments for filters
This commit is contained in:
parent
6f8de0ca84
commit
b402936db3
@ -65,6 +65,7 @@ impl<'a> Generator<'a, '_> {
|
||||
Expr::As(ref expr, target) => self.visit_as(ctx, buf, expr, target)?,
|
||||
Expr::Concat(ref exprs) => self.visit_concat(ctx, buf, exprs)?,
|
||||
Expr::LetCond(ref cond) => self.visit_let_cond(ctx, buf, cond)?,
|
||||
Expr::ArgumentPlaceholder => DisplayWrap::Unwrapped,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Write};
|
||||
use std::mem::replace;
|
||||
|
||||
use parser::{Expr, IntKind, Num, Span, StrLit, StrPrefix, TyGenerics, WithSpan};
|
||||
|
||||
use super::{DisplayWrap, Generator, TargetIsize, TargetUsize};
|
||||
use crate::heritage::Context;
|
||||
use crate::integration::Buffer;
|
||||
use crate::{CompileError, MsgValidEscapers};
|
||||
use crate::{CompileError, MsgValidEscapers, fmt_left, fmt_right};
|
||||
|
||||
impl<'a> Generator<'a, '_> {
|
||||
pub(super) fn visit_filter(
|
||||
@ -60,8 +62,9 @@ impl<'a> Generator<'a, '_> {
|
||||
name: &str,
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
generics: &[WithSpan<'a, TyGenerics<'a>>],
|
||||
_node: Span<'_>,
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
ensure_no_named_arguments(ctx, name, args, node)?;
|
||||
buf.write(format_args!("filters::{name}"));
|
||||
self.visit_call_generics(buf, generics);
|
||||
buf.write('(');
|
||||
@ -111,10 +114,11 @@ impl<'a> Generator<'a, '_> {
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
ensure_no_generics(ctx, name, node, generics)?;
|
||||
let arg = no_arguments(ctx, name, args)?;
|
||||
buf.write(format_args!("askama::filters::{name}"));
|
||||
self.visit_call_generics(buf, generics);
|
||||
buf.write('(');
|
||||
self.visit_args(ctx, buf, args)?;
|
||||
self.visit_arg(ctx, buf, arg)?;
|
||||
buf.write(")?");
|
||||
Ok(DisplayWrap::Unwrapped)
|
||||
}
|
||||
@ -154,11 +158,12 @@ impl<'a> Generator<'a, '_> {
|
||||
));
|
||||
}
|
||||
|
||||
let arg = no_arguments(ctx, name, args)?;
|
||||
// Both filters return HTML-safe strings.
|
||||
buf.write(format_args!(
|
||||
"askama::filters::HtmlSafeOutput(askama::filters::{name}(",
|
||||
));
|
||||
self.visit_args(ctx, buf, args)?;
|
||||
self.visit_arg(ctx, buf, arg)?;
|
||||
buf.write(")?)");
|
||||
Ok(DisplayWrap::Unwrapped)
|
||||
}
|
||||
@ -171,15 +176,10 @@ impl<'a> Generator<'a, '_> {
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
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(&(");
|
||||
self.visit_args(ctx, buf, args)?;
|
||||
self.visit_arg(ctx, buf, arg)?;
|
||||
buf.write(
|
||||
")) {\
|
||||
expr0 => {\
|
||||
@ -201,12 +201,13 @@ impl<'a> Generator<'a, '_> {
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
_node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
let arg = no_arguments(ctx, "humansize", args)?;
|
||||
// All filters return numbers, and any default formatted number is HTML safe.
|
||||
buf.write(format_args!(
|
||||
"askama::filters::HtmlSafeOutput(askama::filters::filesizeformat(\
|
||||
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)?)");
|
||||
Ok(DisplayWrap::Unwrapped)
|
||||
}
|
||||
@ -228,17 +229,20 @@ impl<'a> Generator<'a, '_> {
|
||||
prefix: None,
|
||||
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) {
|
||||
let value = if is_singular { sg } else { pl };
|
||||
self.visit_auto_escaped_arg(ctx, buf, value)?;
|
||||
@ -293,16 +297,11 @@ impl<'a> Generator<'a, '_> {
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
ensure_filter_has_feature_alloc(ctx, name, node)?;
|
||||
if args.len() != 1 {
|
||||
return Err(ctx.generate_error(
|
||||
format_args!("unexpected argument(s) in `{name}` filter"),
|
||||
node,
|
||||
));
|
||||
}
|
||||
let arg = no_arguments(ctx, name, args)?;
|
||||
buf.write(format_args!(
|
||||
"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:
|
||||
buf.write("), askama::filters::Html)).askama_auto_escape()?)?");
|
||||
// The output is marked as HTML safe, not safe in all contexts:
|
||||
@ -314,12 +313,9 @@ impl<'a> Generator<'a, '_> {
|
||||
ctx: &Context<'_>,
|
||||
buf: &mut Buffer,
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
node: Span<'_>,
|
||||
_node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
let arg = match args {
|
||||
[arg] => arg,
|
||||
_ => return Err(ctx.generate_error("unexpected argument(s) in `as_ref` filter", node)),
|
||||
};
|
||||
let arg = no_arguments(ctx, "ref", args)?;
|
||||
buf.write('&');
|
||||
self.visit_expr(ctx, buf, arg)?;
|
||||
Ok(DisplayWrap::Unwrapped)
|
||||
@ -330,12 +326,9 @@ impl<'a> Generator<'a, '_> {
|
||||
ctx: &Context<'_>,
|
||||
buf: &mut Buffer,
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
node: Span<'_>,
|
||||
_node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
let arg = match args {
|
||||
[arg] => arg,
|
||||
_ => return Err(ctx.generate_error("unexpected argument(s) in `deref` filter", node)),
|
||||
};
|
||||
let arg = no_arguments(ctx, "deref", args)?;
|
||||
buf.write('*');
|
||||
self.visit_expr(ctx, buf, arg)?;
|
||||
Ok(DisplayWrap::Unwrapped)
|
||||
@ -348,6 +341,14 @@ impl<'a> Generator<'a, '_> {
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
const ARGUMENTS: &[&FilterArgument; 2] = &[
|
||||
FILTER_SOURCE,
|
||||
&FilterArgument {
|
||||
name: "indent",
|
||||
default_value: Some(ARGUMENT_PLACEHOLDER),
|
||||
},
|
||||
];
|
||||
|
||||
if cfg!(not(feature = "serde_json")) {
|
||||
return Err(ctx.generate_error(
|
||||
"the `json` filter requires the `serde_json` feature to be enabled",
|
||||
@ -355,14 +356,18 @@ impl<'a> Generator<'a, '_> {
|
||||
));
|
||||
}
|
||||
|
||||
let filter = match args.len() {
|
||||
1 => "json",
|
||||
2 => "json_pretty",
|
||||
_ => return Err(ctx.generate_error("unexpected argument(s) in `json` filter", node)),
|
||||
};
|
||||
buf.write(format_args!("askama::filters::{filter}("));
|
||||
self.visit_args(ctx, buf, args)?;
|
||||
buf.write(")?");
|
||||
let [value, indent] = collect_filter_args(ctx, "json", node, args, ARGUMENTS)?;
|
||||
if is_argument_placeholder(indent) {
|
||||
buf.write(format_args!("askama::filters::json("));
|
||||
self.visit_arg(ctx, buf, value)?;
|
||||
buf.write(")?");
|
||||
} else {
|
||||
buf.write(format_args!("askama::filters::json_pretty("));
|
||||
self.visit_arg(ctx, buf, value)?;
|
||||
buf.write(',');
|
||||
self.visit_arg(ctx, buf, indent)?;
|
||||
buf.write(")?");
|
||||
}
|
||||
Ok(DisplayWrap::Unwrapped)
|
||||
}
|
||||
|
||||
@ -375,18 +380,25 @@ impl<'a> Generator<'a, '_> {
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
const FALSE: &WithSpan<'static, Expr<'static>> =
|
||||
&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)?;
|
||||
let (source, indent, first, blank) =
|
||||
match args {
|
||||
[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,
|
||||
)),
|
||||
};
|
||||
let [source, indent, first, blank] =
|
||||
collect_filter_args(ctx, "indent", node, args, ARGUMENTS)?;
|
||||
buf.write("askama::filters::indent(");
|
||||
self.visit_arg(ctx, buf, source)?;
|
||||
buf.write(",");
|
||||
@ -404,13 +416,11 @@ impl<'a> Generator<'a, '_> {
|
||||
ctx: &Context<'_>,
|
||||
buf: &mut Buffer,
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
node: Span<'_>,
|
||||
_node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
if args.len() != 1 {
|
||||
return Err(ctx.generate_error("unexpected argument(s) in `safe` filter", node));
|
||||
}
|
||||
let arg = no_arguments(ctx, "safe", args)?;
|
||||
buf.write("askama::filters::safe(");
|
||||
self.visit_args(ctx, buf, args)?;
|
||||
self.visit_arg(ctx, buf, arg)?;
|
||||
buf.write(format_args!(", {})?", self.input.escaper));
|
||||
Ok(DisplayWrap::Wrapped)
|
||||
}
|
||||
@ -422,31 +432,37 @@ impl<'a> Generator<'a, '_> {
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
if args.len() > 2 {
|
||||
return Err(ctx.generate_error("only two arguments allowed to escape filter", node));
|
||||
}
|
||||
let opt_escaper = match args.get(1).map(|expr| &**expr) {
|
||||
Some(Expr::StrLit(StrLit { prefix, content })) => {
|
||||
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}"
|
||||
),
|
||||
args[1].span(),
|
||||
));
|
||||
}
|
||||
Some(content)
|
||||
}
|
||||
Some(_) => {
|
||||
const ARGUMENTS: &[&FilterArgument; 2] = &[
|
||||
FILTER_SOURCE,
|
||||
&FilterArgument {
|
||||
name: "escaper",
|
||||
default_value: Some(ARGUMENT_PLACEHOLDER),
|
||||
},
|
||||
];
|
||||
|
||||
let [source, opt_escaper] = collect_filter_args(ctx, "escape", node, args, ARGUMENTS)?;
|
||||
let opt_escaper = if !is_argument_placeholder(opt_escaper) {
|
||||
let Expr::StrLit(StrLit { prefix, content }) = **opt_escaper else {
|
||||
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 {
|
||||
Some(name) => self
|
||||
.input
|
||||
@ -461,7 +477,8 @@ impl<'a> Generator<'a, '_> {
|
||||
.ok_or_else(|| {
|
||||
ctx.generate_error(
|
||||
format_args!(
|
||||
"invalid escaper '{name}' for `escape` filter. {}",
|
||||
"invalid escaper `{}` for `escape` filter. {}",
|
||||
name.escape_debug(),
|
||||
MsgValidEscapers(&self.input.config.escapers),
|
||||
),
|
||||
node,
|
||||
@ -470,7 +487,7 @@ impl<'a> Generator<'a, '_> {
|
||||
None => self.input.escaper,
|
||||
};
|
||||
buf.write("askama::filters::escape(");
|
||||
self.visit_args(ctx, buf, &args[..1])?;
|
||||
self.visit_arg(ctx, buf, source)?;
|
||||
buf.write(format_args!(", {escaper})?"));
|
||||
Ok(DisplayWrap::Wrapped)
|
||||
}
|
||||
@ -483,6 +500,7 @@ impl<'a> Generator<'a, '_> {
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
ensure_filter_has_feature_alloc(ctx, "format", node)?;
|
||||
ensure_no_named_arguments(ctx, "format", args, node)?;
|
||||
if !args.is_empty() {
|
||||
if let Expr::StrLit(ref fmt) = *args[0] {
|
||||
buf.write("askama::helpers::alloc::format!(");
|
||||
@ -495,7 +513,10 @@ impl<'a> Generator<'a, '_> {
|
||||
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(
|
||||
@ -505,18 +526,25 @@ impl<'a> Generator<'a, '_> {
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
const ARGUMENTS: &[&FilterArgument; 2] = &[
|
||||
FILTER_SOURCE,
|
||||
&FilterArgument {
|
||||
name: "format",
|
||||
default_value: None,
|
||||
},
|
||||
];
|
||||
|
||||
ensure_filter_has_feature_alloc(ctx, "fmt", node)?;
|
||||
if let [_, arg2] = args {
|
||||
if let Expr::StrLit(ref fmt) = **arg2 {
|
||||
buf.write("askama::helpers::alloc::format!(");
|
||||
self.visit_str_lit(buf, fmt);
|
||||
buf.write(',');
|
||||
self.visit_args(ctx, buf, &args[..1])?;
|
||||
buf.write(')');
|
||||
return Ok(DisplayWrap::Unwrapped);
|
||||
}
|
||||
}
|
||||
Err(ctx.generate_error(r#"use filter fmt like `value|fmt("{:?}")`"#, node))
|
||||
let [source, fmt] = collect_filter_args(ctx, "fmt", node, args, ARGUMENTS)?;
|
||||
let Expr::StrLit(ref fmt) = **fmt else {
|
||||
return Err(ctx.generate_error(r#"use `fmt` filter like `value|fmt("{:?}")`"#, node));
|
||||
};
|
||||
buf.write("askama::helpers::alloc::format!(");
|
||||
self.visit_str_lit(buf, fmt);
|
||||
buf.write(',');
|
||||
self.visit_arg(ctx, buf, source)?;
|
||||
buf.write(')');
|
||||
Ok(DisplayWrap::Unwrapped)
|
||||
}
|
||||
|
||||
// Force type coercion on first argument to `join` filter (see #39).
|
||||
@ -525,18 +553,21 @@ impl<'a> Generator<'a, '_> {
|
||||
ctx: &Context<'_>,
|
||||
buf: &mut Buffer,
|
||||
args: &[WithSpan<'a, Expr<'a>>],
|
||||
_node: Span<'_>,
|
||||
node: Span<'_>,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
buf.write("askama::filters::join((&");
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
if i > 0 {
|
||||
buf.write(", &");
|
||||
}
|
||||
self.visit_expr(ctx, buf, arg)?;
|
||||
if i == 0 {
|
||||
buf.write(").into_iter()");
|
||||
}
|
||||
}
|
||||
const ARGUMENTS: &[&FilterArgument; 2] = &[
|
||||
FILTER_SOURCE,
|
||||
&FilterArgument {
|
||||
name: "separator",
|
||||
default_value: None,
|
||||
},
|
||||
];
|
||||
|
||||
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(")?");
|
||||
Ok(DisplayWrap::Unwrapped)
|
||||
}
|
||||
@ -569,14 +600,16 @@ impl<'a> Generator<'a, '_> {
|
||||
node: Span<'_>,
|
||||
name: &str,
|
||||
) -> Result<DisplayWrap, CompileError> {
|
||||
ensure_filter_has_feature_alloc(ctx, name, node)?;
|
||||
let [arg, length] = args else {
|
||||
return Err(ctx.generate_error(
|
||||
format_args!("`{name}` filter needs one argument, the `length`"),
|
||||
node,
|
||||
));
|
||||
};
|
||||
const ARGUMENTS: &[&FilterArgument; 2] = &[
|
||||
FILTER_SOURCE,
|
||||
&FilterArgument {
|
||||
name: "length",
|
||||
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}("));
|
||||
self.visit_arg(ctx, buf, arg)?;
|
||||
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.
|
||||
const BUILTIN_FILTERS: &[&str] = &[];
|
||||
|
||||
|
@ -207,7 +207,8 @@ impl<'a> Generator<'a, '_> {
|
||||
| Expr::FilterSource
|
||||
| Expr::As(_, _)
|
||||
| Expr::Concat(_)
|
||||
| Expr::LetCond(_) => {
|
||||
| Expr::LetCond(_)
|
||||
| Expr::ArgumentPlaceholder => {
|
||||
*only_contains_is_defined = false;
|
||||
(EvaluatedResult::Unknown, WithSpan::new(expr, span))
|
||||
}
|
||||
@ -1559,5 +1560,6 @@ fn is_cacheable(expr: &WithSpan<'_, Expr<'_>>) -> bool {
|
||||
Expr::RustMacro(_, _) => false,
|
||||
// Should never be encountered:
|
||||
Expr::FilterSource => unreachable!("FilterSource in expression?"),
|
||||
Expr::ArgumentPlaceholder => unreachable!("ExpressionPlaceholder in expression?"),
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,10 @@ fn check_expr<'a>(
|
||||
}
|
||||
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>>>),
|
||||
/// If you have `&& let Some(y)`, this variant handles it.
|
||||
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> {
|
||||
@ -526,7 +533,8 @@ impl<'a> Expr<'a> {
|
||||
| Self::BinOp(_, _, _)
|
||||
| Self::Path(_)
|
||||
| Self::Concat(_)
|
||||
| Self::LetCond(_) => false,
|
||||
| Self::LetCond(_)
|
||||
| Self::ArgumentPlaceholder => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1037,7 +1037,7 @@ fn filter<'a>(
|
||||
cut_err((
|
||||
ws(identifier),
|
||||
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)
|
||||
}
|
||||
|
188
testing/tests/named_filter_arguments.rs
Normal file
188
testing/tests/named_filter_arguments.rs
Normal 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>"
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
error: unexpected argument(s) in `json` filter
|
||||
--> OneTwoThree.txt:1:3
|
||||
"1|json(2, 3) }}"
|
||||
error: `json` filter accepts at most 1 argument; its arguments are: ([indent])
|
||||
--> OneTwoThree.txt:1:13
|
||||
"3) }}"
|
||||
--> tests/ui/json-too-many-args.rs:6:34
|
||||
|
|
||||
6 | #[template(ext = "txt", source = "{{ 1|json(2, 3) }}")]
|
||||
|
48
testing/tests/ui/named_filter_arguments.rs
Normal file
48
testing/tests/ui/named_filter_arguments.rs
Normal 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() {}
|
39
testing/tests/ui/named_filter_arguments.stderr
Normal file
39
testing/tests/ui/named_filter_arguments.stderr
Normal 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") }}"#,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
@ -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
|
||||
"text|escape(\"latex\")}}`."
|
||||
--> tests/ui/no-such-escaper.rs:6:14
|
||||
|
@ -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
|
||||
"text | truncate }}"
|
||||
--> 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")]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: filter `truncate` needs one argument, the `length`
|
||||
--> TooManyArguments.html:1:3
|
||||
"text | truncate(length, extra) }}"
|
||||
error: `truncate` filter accepts at most 1 argument; its arguments are: (length)
|
||||
--> TooManyArguments.html:1:27
|
||||
"extra) }}"
|
||||
--> tests/ui/truncate.rs:17:21
|
||||
|
|
||||
17 | #[template(source = r#"{{ text | truncate(length, extra) }}"#, ext = "html")]
|
||||
|
Loading…
x
Reference in New Issue
Block a user