diff --git a/askama/src/filters/builtin.rs b/askama/src/filters/builtin.rs index 7e3e1f73..0ab6a365 100644 --- a/askama/src/filters/builtin.rs +++ b/askama/src/filters/builtin.rs @@ -455,6 +455,63 @@ 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," +/// ); +/// ``` +pub fn reject( + it: impl IntoIterator, + filter: T, +) -> Result, Infallible> { + Ok(it.into_iter().filter(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() { +/// assert_eq!( +/// Example { numbers: vec![1, 2, 3, 4] }.to_string(), +/// "2,4," +/// ); +/// } +/// ``` +pub fn reject_with bool>( + it: impl IntoIterator, + callback: F, +) -> Result, Infallible> { + Ok(it.into_iter().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 9b63cb11..c1a53aeb 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/filter.rs b/askama_derive/src/generator/filter.rs index 1b641199..9d10e287 100644 --- a/askama_derive/src/generator/filter.rs +++ b/askama_derive/src/generator/filter.rs @@ -34,6 +34,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, @@ -212,6 +213,42 @@ 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)?; + + let extra_ampersand = match &**filter { + Expr::Path(_) => { + buf.write("askama::filters::reject_with("); + false + } + _ => { + buf.write("askama::filters::reject("); + true + } + }; + self.visit_arg(ctx, buf, input)?; + buf.write(','); + if extra_ampersand { + buf.write('&'); + } + self.visit_arg(ctx, buf, filter)?; + buf.write(")?"); + Ok(DisplayWrap::Wrapped) + } + fn visit_pluralize_filter( &mut self, ctx: &Context<'_>, diff --git a/book/src/filters.md b/book/src/filters.md index 6e1a6742..cbff40bb 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