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::Concat(ref exprs) => self.visit_concat(ctx, buf, exprs)?,
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::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] = &[];

View File

@ -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?"),
}
}

View File

@ -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,
}
}
}

View File

@ -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)
}

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
--> 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) }}")]

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
"text|escape(\"latex\")}}`."
--> 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
"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")]