generator: do argument coercion for |truncate / |center

Don't simply pass any arguments to the filter. The error message won't
be useful otherwise. Also ensure that the argument is a `usize`.
This commit is contained in:
René Kijewski 2025-04-19 01:26:26 +02:00 committed by René Kijewski
parent de9d6b7d0e
commit 4be302338a
5 changed files with 138 additions and 9 deletions

View File

@ -5,7 +5,7 @@ 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::{BUILTIN_FILTERS, BUILTIN_FILTERS_NEED_ALLOC, CompileError, MsgValidEscapers}; use crate::{BUILTIN_FILTERS, CompileError, MsgValidEscapers};
impl<'a> Generator<'a, '_> { impl<'a> Generator<'a, '_> {
pub(super) fn visit_filter( pub(super) fn visit_filter(
@ -18,6 +18,7 @@ impl<'a> Generator<'a, '_> {
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
let filter = match name { let filter = match name {
"center" => Self::visit_center_filter,
"deref" => Self::visit_deref_filter, "deref" => Self::visit_deref_filter,
"escape" | "e" => Self::visit_escape_filter, "escape" | "e" => Self::visit_escape_filter,
"filesizeformat" => Self::visit_humansize, "filesizeformat" => Self::visit_humansize,
@ -32,6 +33,7 @@ impl<'a> Generator<'a, '_> {
"pluralize" => Self::visit_pluralize_filter, "pluralize" => Self::visit_pluralize_filter,
"ref" => Self::visit_ref_filter, "ref" => Self::visit_ref_filter,
"safe" => Self::visit_safe_filter, "safe" => Self::visit_safe_filter,
"truncate" => Self::visit_truncate_filter,
"urlencode" => Self::visit_urlencode_filter, "urlencode" => Self::visit_urlencode_filter,
"urlencode_strict" => Self::visit_urlencode_strict_filter, "urlencode_strict" => Self::visit_urlencode_strict_filter,
"value" => return self.visit_value(ctx, buf, args, generics, node, "`value` filter"), "value" => return self.visit_value(ctx, buf, args, generics, node, "`value` filter"),
@ -79,9 +81,6 @@ impl<'a> Generator<'a, '_> {
generics: &[WithSpan<'a, TyGenerics<'a>>], generics: &[WithSpan<'a, TyGenerics<'a>>],
node: Span<'_>, node: Span<'_>,
) -> Result<DisplayWrap, CompileError> { ) -> Result<DisplayWrap, CompileError> {
if BUILTIN_FILTERS_NEED_ALLOC.contains(&name) {
ensure_filter_has_feature_alloc(ctx, name, node)?;
}
if !generics.is_empty() { if !generics.is_empty() {
return Err( return Err(
ctx.generate_error(format_args!("unexpected generics on filter `{name}`"), node) ctx.generate_error(format_args!("unexpected generics on filter `{name}`"), node)
@ -516,6 +515,60 @@ impl<'a> Generator<'a, '_> {
buf.write(")?"); buf.write(")?");
Ok(DisplayWrap::Unwrapped) Ok(DisplayWrap::Unwrapped)
} }
fn visit_center_filter(
&mut self,
ctx: &Context<'_>,
buf: &mut Buffer,
args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>,
) -> Result<DisplayWrap, CompileError> {
self.visit_center_truncate_filter(ctx, buf, args, node, "center")
}
fn visit_truncate_filter(
&mut self,
ctx: &Context<'_>,
buf: &mut Buffer,
args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>,
) -> Result<DisplayWrap, CompileError> {
self.visit_center_truncate_filter(ctx, buf, args, node, "truncate")
}
fn visit_center_truncate_filter(
&mut self,
ctx: &Context<'_>,
buf: &mut Buffer,
args: &[WithSpan<'a, Expr<'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,
));
};
buf.write(format_args!("askama::filters::{name}("));
self.visit_arg(ctx, buf, arg)?;
buf.write(
"\
,\
askama::helpers::core::primitive::usize::try_from(\
askama::helpers::get_primitive_value(&(",
);
self.visit_arg(ctx, buf, length)?;
buf.write(
"\
))\
).map_err(|_| askama::Error::Fmt)?\
)?",
);
Ok(DisplayWrap::Unwrapped)
}
} }
fn ensure_filter_has_feature_alloc( fn ensure_filter_has_feature_alloc(

View File

@ -602,15 +602,10 @@ pub(crate) use {fmt_left, fmt_right};
// Askama or should refer to a local `filters` module. // Askama or should refer to a local `filters` module.
const BUILTIN_FILTERS: &[&str] = &[ const BUILTIN_FILTERS: &[&str] = &[
"capitalize", "capitalize",
"center",
"lower", "lower",
"lowercase", "lowercase",
"title", "title",
"trim", "trim",
"truncate",
"upper", "upper",
"uppercase", "uppercase",
]; ];
// Built-in filters that need the `alloc` feature.
const BUILTIN_FILTERS_NEED_ALLOC: &[&str] = &["center", "truncate"];

View File

@ -3,6 +3,7 @@
extern crate serde_json; extern crate serde_json;
use askama::Template; use askama::Template;
use assert_matches::assert_matches;
#[cfg(feature = "serde_json")] #[cfg(feature = "serde_json")]
use serde_json::Value; use serde_json::Value;
@ -298,6 +299,32 @@ fn test_filter_truncate() {
foo: "alpha bar".into(), foo: "alpha bar".into(),
}; };
assert_eq!(t.render().unwrap(), "alpha baralpha..."); assert_eq!(t.render().unwrap(), "alpha baralpha...");
#[derive(Template)]
#[template(source = "{{ foo | truncate(length) }}", ext = "txt")]
struct TruncateFilterLength<'a> {
foo: String,
length: &'a &'a &'a i32,
}
assert_eq!(
TruncateFilterLength {
foo: "alpha bar".into(),
length: &&&5,
}
.render()
.unwrap(),
"alpha..."
);
assert_matches!(
TruncateFilterLength {
foo: "alpha bar".into(),
length: &&&-5,
}
.render()
.unwrap_err(),
askama::Error::Fmt
);
} }
#[cfg(feature = "serde_json")] #[cfg(feature = "serde_json")]

View File

@ -0,0 +1,24 @@
use askama::Template;
#[derive(Template)]
#[template(source = r#"{{ text | truncate }}"#, ext = "html")]
struct NoArgument<'a> {
text: &'a str,
}
#[derive(Template)]
#[template(source = r#"{{ text | truncate(length) }}"#, ext = "html")]
struct WrongArgumentType<'a> {
text: &'a str,
length: f32,
}
#[derive(Template)]
#[template(source = r#"{{ text | truncate(length, extra) }}"#, ext = "html")]
struct TooManyArguments<'a> {
text: &'a str,
length: usize,
extra: bool,
}
fn main() {}

View File

@ -0,0 +1,30 @@
error: filter `truncate` needs one argument, the `length`
--> NoArgument.html:1:3
"text | truncate }}"
--> tests/ui/truncate.rs:4:21
|
4 | #[template(source = r#"{{ text | truncate }}"#, ext = "html")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
error: filter `truncate` needs one argument, the `length`
--> TooManyArguments.html:1:3
"text | truncate(length, extra) }}"
--> tests/ui/truncate.rs:17:21
|
17 | #[template(source = r#"{{ text | truncate(length, extra) }}"#, ext = "html")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0277]: the trait bound `usize: TryFrom<f32>` is not satisfied
--> tests/ui/truncate.rs:9:10
|
9 | #[derive(Template)]
| ^^^^^^^^ the trait `From<f32>` is not implemented for `usize`
|
= help: the following other types implement trait `From<T>`:
`usize` implements `From<bool>`
`usize` implements `From<std::ptr::Alignment>`
`usize` implements `From<u16>`
`usize` implements `From<u8>`
= note: required for `f32` to implement `Into<usize>`
= note: required for `usize` to implement `TryFrom<f32>`
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)