diff --git a/askama/src/filters/builtin.rs b/askama/src/filters/builtin.rs index 7e3e1f73..9aeec439 100644 --- a/askama/src/filters/builtin.rs +++ b/askama/src/filters/builtin.rs @@ -455,6 +455,65 @@ impl FastWritable for Pluralize { } } +/// Returns an iterator without filtered out values. +/// +/// ``` +/// # use askama::Template; +/// #[derive(Template)] +/// #[template( +/// ext = "html", +/// source = r#"{% for elem in strs|reject("a") %}{{ elem }},{% endfor %}"#, +/// )] +/// struct Example<'a> { +/// strs: Vec<&'a str>, +/// } +/// +/// assert_eq!( +/// Example { strs: vec!["a", "b", "c"] }.to_string(), +/// "b,c," +/// ); +/// ``` +#[inline] +pub fn reject<'a, T: PartialEq + 'a>( + it: impl Iterator + 'a, + filter: &'a T, +) -> Result + 'a, Infallible> { + reject_with(it, move |v| v == filter) +} + +/// Returns an iterator without filtered out values. +/// +/// ``` +/// # use askama::Template; +/// +/// fn is_odd(v: &&u32) -> bool { +/// **v & 1 != 0 +/// } +/// +/// #[derive(Template)] +/// #[template( +/// ext = "html", +/// source = r#"{% for elem in numbers | reject(self::is_odd) %}{{ elem }},{% endfor %}"#, +/// )] +/// struct Example { +/// numbers: Vec, +/// } +/// +/// # fn main() { // so `self::` can be accessed +/// assert_eq!( +/// Example { numbers: vec![1, 2, 3, 4] }.to_string(), +/// "2,4," +/// ); +/// # } +/// ``` +#[inline] +pub fn reject_with( + it: impl Iterator, + mut callback: impl FnMut(&T) -> bool, +) -> Result, Infallible> { + Ok(it.filter(move |v| !callback(v))) +} + #[cfg(all(test, feature = "alloc"))] mod tests { use alloc::string::{String, ToString}; diff --git a/askama/src/filters/mod.rs b/askama/src/filters/mod.rs index 272de02d..fa7c8324 100644 --- a/askama/src/filters/mod.rs +++ b/askama/src/filters/mod.rs @@ -27,7 +27,7 @@ pub use self::alloc::{ AsIndent, capitalize, fmt, format, indent, linebreaks, linebreaksbr, lower, lowercase, paragraphbreaks, title, titlecase, trim, upper, uppercase, wordcount, }; -pub use self::builtin::{PluralizeCount, center, join, pluralize, truncate}; +pub use self::builtin::{PluralizeCount, center, join, pluralize, reject, reject_with, truncate}; pub use self::escape::{ AutoEscape, AutoEscaper, Escaper, Html, HtmlSafe, HtmlSafeOutput, MaybeSafe, Safe, Text, Unsafe, Writable, WriteWritable, e, escape, safe, diff --git a/askama_derive/src/generator/expr.rs b/askama_derive/src/generator/expr.rs index f4f374d8..f095777f 100644 --- a/askama_derive/src/generator/expr.rs +++ b/askama_derive/src/generator/expr.rs @@ -23,6 +23,36 @@ impl<'a> Generator<'a, '_> { Ok(buf.into_string()) } + pub(super) fn visit_loop_iter( + &mut self, + ctx: &Context<'_>, + buf: &mut Buffer, + iter: &WithSpan<'a, Expr<'a>>, + ) -> Result { + let expr_code = self.visit_expr_root(ctx, iter)?; + match &**iter { + Expr::Range(..) => buf.write(expr_code), + Expr::Array(..) => buf.write(format_args!("{expr_code}.iter()")), + // If `iter` is a call then we assume it's something that returns + // an iterator. If not then the user can explicitly add the needed + // call without issues. + Expr::Call { .. } | Expr::Index(..) => { + buf.write(format_args!("({expr_code}).into_iter()")); + } + // If accessing `self` then it most likely needs to be + // borrowed, to prevent an attempt of moving. + _ if expr_code.starts_with("self.") => { + buf.write(format_args!("(&{expr_code}).into_iter()")); + } + // If accessing a field then it most likely needs to be + // borrowed, to prevent an attempt of moving. + Expr::Attr(..) => buf.write(format_args!("(&{expr_code}).into_iter()")), + // Otherwise, we borrow `iter` assuming that it implements `IntoIterator`. + _ => buf.write(format_args!("({expr_code}).into_iter()")), + } + Ok(DisplayWrap::Unwrapped) + } + pub(super) fn visit_expr( &mut self, ctx: &Context<'_>, diff --git a/askama_derive/src/generator/filter.rs b/askama_derive/src/generator/filter.rs index 34681dbc..5b851f89 100644 --- a/askama_derive/src/generator/filter.rs +++ b/askama_derive/src/generator/filter.rs @@ -40,6 +40,7 @@ impl<'a> Generator<'a, '_> { "paragraphbreaks" => Self::visit_paragraphbreaks_filter, "pluralize" => Self::visit_pluralize_filter, "ref" => Self::visit_ref_filter, + "reject" => Self::visit_reject_filter, "safe" => Self::visit_safe_filter, "truncate" => Self::visit_truncate_filter, "urlencode" => Self::visit_urlencode_filter, @@ -241,6 +242,39 @@ impl<'a> Generator<'a, '_> { Ok(DisplayWrap::Unwrapped) } + fn visit_reject_filter( + &mut self, + ctx: &Context<'_>, + buf: &mut Buffer, + args: &[WithSpan<'a, Expr<'a>>], + node: Span<'_>, + ) -> Result { + const ARGUMENTS: &[&FilterArgument; 2] = &[ + FILTER_SOURCE, + &FilterArgument { + name: "filter", + default_value: None, + }, + ]; + let [input, filter] = collect_filter_args(ctx, "reject", node, args, ARGUMENTS)?; + + if matches!(&**filter, Expr::Path(_)) { + buf.write("askama::filters::reject_with("); + self.visit_loop_iter(ctx, buf, input)?; + buf.write(','); + self.visit_arg(ctx, buf, filter)?; + buf.write(")?"); + } else { + buf.write("askama::filters::reject("); + self.visit_loop_iter(ctx, buf, input)?; + buf.write(",(&&&("); // coerce [T, &T, &&T...] to &T + self.visit_arg(ctx, buf, filter)?; + buf.write(")) as &_)?"); + } + + Ok(DisplayWrap::Unwrapped) + } + fn visit_pluralize_filter( &mut self, ctx: &Context<'_>, diff --git a/askama_derive/src/generator/node.rs b/askama_derive/src/generator/node.rs index cf5d49d1..7fdffc40 100644 --- a/askama_derive/src/generator/node.rs +++ b/askama_derive/src/generator/node.rs @@ -502,7 +502,6 @@ impl<'a> Generator<'a, '_> { Ok(flushed + median(&mut arm_sizes)) } - #[allow(clippy::too_many_arguments)] fn write_loop( &mut self, ctx: &Context<'a>, @@ -511,8 +510,6 @@ impl<'a> Generator<'a, '_> { ) -> Result { self.handle_ws(loop_block.ws1); self.push_locals(|this| { - let expr_code = this.visit_expr_root(ctx, &loop_block.iter)?; - let has_else_nodes = !loop_block.else_nodes.is_empty(); let flushed = this.write_buf_writable(ctx, buf)?; @@ -520,38 +517,10 @@ impl<'a> Generator<'a, '_> { if has_else_nodes { buf.write("let mut __askama_did_loop = false;"); } - match &*loop_block.iter { - Expr::Range(_, _, _) => buf.write(format_args!("let __askama_iter = {expr_code};")), - Expr::Array(..) => { - buf.write(format_args!("let __askama_iter = {expr_code}.iter();")); - } - // If `iter` is a call then we assume it's something that returns - // an iterator. If not then the user can explicitly add the needed - // call without issues. - Expr::Call { .. } | Expr::Index(..) => { - buf.write(format_args!( - "let __askama_iter = ({expr_code}).into_iter();" - )); - } - // If accessing `self` then it most likely needs to be - // borrowed, to prevent an attempt of moving. - _ if expr_code.starts_with("self.") => { - buf.write(format_args!( - "let __askama_iter = (&{expr_code}).into_iter();" - )); - } - // If accessing a field then it most likely needs to be - // borrowed, to prevent an attempt of moving. - Expr::Attr(..) => { - buf.write(format_args!( - "let __askama_iter = (&{expr_code}).into_iter();" - )); - } - // Otherwise, we borrow `iter` assuming that it implements `IntoIterator`. - _ => buf.write(format_args!( - "let __askama_iter = ({expr_code}).into_iter();" - )), - } + + buf.write("let __askama_iter ="); + this.visit_loop_iter(ctx, buf, &loop_block.iter)?; + buf.write(';'); if let Some(cond) = &loop_block.cond { this.push_locals(|this| { buf.write("let __askama_iter = __askama_iter.filter(|"); diff --git a/book/src/filters.md b/book/src/filters.md index ec96e678..2c9c4393 100644 --- a/book/src/filters.md +++ b/book/src/filters.md @@ -467,6 +467,49 @@ will become: &self.x ``` +### reject +[#reject]: #reject + +This filter filters out values matching the given value/filter. + +With this data: + +```rust +vec![1, 2, 3, 1] +``` + +And this template: + +```jinja +{% for elem in data|reject(1) %}{{ elem }},{% endfor %} +``` + +Output will be: + +```text +2,3, +``` + +For more control over the filtering, you can use a callback instead. Declare a function: + +```jinja +fn is_odd(value: &&u32) -> bool { + **value % 2 != 0 +} +``` + +Then you can pass the path to the `is_odd` function: + +```jinja +{% for elem in data|reject(crate::is_odd) %}{{ elem }},{% endfor %} +``` + +Output will be: + +```text +2, +``` + ### safe [#safe]: #safe