minijinja/filters.rs
1//! Filter functions and abstractions.
2//!
3//! MiniJinja inherits from Jinja2 the concept of filter functions. These are functions
4//! which are applied to values to modify them. For example the expression `{{ 42|filter(23) }}`
5//! invokes the filter `filter` with the arguments `42` and `23`.
6//!
7//! MiniJinja comes with some built-in filters that are listed below. To create a
8//! custom filter write a function that takes at least a value, then registers it
9//! with [`add_filter`](crate::Environment::add_filter).
10//!
11//! # Using Filters
12//!
13//! Using filters in templates is possible in all places an expression is permitted.
14//! This means they are not just used for printing but also are useful for iteration
15//! or similar situations.
16//!
17//! Motivating example:
18//!
19//! ```jinja
20//! <dl>
21//! {% for key, value in config|items %}
22//! <dt>{{ key }}
23//! <dd><pre>{{ value|tojson }}</pre>
24//! {% endfor %}
25//! </dl>
26//! ```
27//!
28//! # Custom Filters
29//!
30//! A custom filter is just a simple function which accepts its inputs
31//! as parameters and then returns a new value. For instance the following
32//! shows a filter which takes an input value and replaces whitespace with
33//! dashes and converts it to lowercase:
34//!
35//! ```
36//! # use minijinja::Environment;
37//! # let mut env = Environment::new();
38//! fn slugify(value: String) -> String {
39//! value.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-")
40//! }
41//!
42//! env.add_filter("slugify", slugify);
43//! ```
44//!
45//! MiniJinja will perform the necessary conversions automatically. For more
46//! information see the [`Function`](crate::functions::Function) trait.
47//!
48//! # Accessing State
49//!
50//! In some cases it can be necessary to access the execution [`State`]. Since a borrowed
51//! state implements [`ArgType`](crate::value::ArgType) it's possible to add a
52//! parameter that holds the state. For instance the following filter appends
53//! the current template name to the string:
54//!
55//! ```
56//! # use minijinja::Environment;
57//! # let mut env = Environment::new();
58//! use minijinja::{Value, State};
59//!
60//! fn append_template(state: &State, value: &Value) -> String {
61//! format!("{}-{}", value, state.name())
62//! }
63//!
64//! env.add_filter("append_template", append_template);
65//! ```
66//!
67//! # Filter configuration
68//!
69//! The recommended pattern for filters to change their behavior is to leverage global
70//! variables in the template. For instance take a filter that performs date formatting.
71//! You might want to change the default time format format on a per-template basis
72//! without having to update every filter invocation. In this case the recommended
73//! pattern is to reserve upper case variables and look them up in the filter:
74//!
75//! ```
76//! # use minijinja::Environment;
77//! # let mut env = Environment::new();
78//! # fn format_unix_timestamp(_: f64, _: &str) -> String { "".into() }
79//! use minijinja::State;
80//!
81//! fn timeformat(state: &State, ts: f64) -> String {
82//! let configured_format = state.lookup("TIME_FORMAT");
83//! let format = configured_format
84//! .as_ref()
85//! .and_then(|x| x.as_str())
86//! .unwrap_or("HH:MM:SS");
87//! format_unix_timestamp(ts, format)
88//! }
89//!
90//! env.add_filter("timeformat", timeformat);
91//! ```
92//!
93//! This then later lets a user override the default either by using
94//! [`add_global`](crate::Environment::add_global) or by passing it with the
95//! [`context!`] macro or similar.
96//!
97//! ```
98//! # use minijinja::context;
99//! # let other_variables = context!{};
100//! let ctx = context! {
101//! TIME_FORMAT => "HH:MM",
102//! ..other_variables
103//! };
104//! ```
105//!
106//! # Built-in Filters
107//!
108//! When the `builtins` feature is enabled a range of built-in filters are
109//! automatically added to the environment. These are also all provided in
110//! this module. Note though that these functions are not to be
111//! called from Rust code as their exact interface (arguments and return types)
112//! might change from one MiniJinja version to another.
113//!
114//! Some additional filters are available in the
115//! [`minijinja-contrib`](https://crates.io/crates/minijinja-contrib) crate.
116use std::sync::Arc;
117
118use crate::error::Error;
119use crate::utils::write_escaped;
120use crate::value::Value;
121use crate::vm::State;
122use crate::{AutoEscape, Output};
123
124/// Deprecated alias
125#[deprecated = "Use the minijinja::functions::Function instead"]
126#[doc(hidden)]
127pub use crate::functions::Function as Filter;
128
129/// Marks a value as safe. This converts it into a string.
130///
131/// When a value is marked as safe, no further auto escaping will take place.
132pub fn safe(v: String) -> Value {
133 Value::from_safe_string(v)
134}
135
136/// Escapes a string. By default to HTML.
137///
138/// By default this filter is also registered under the alias `e`. Note that
139/// this filter escapes with the format that is native to the format or HTML
140/// otherwise. This means that if the auto escape setting is set to
141/// `Json` for instance then this filter will serialize to JSON instead.
142pub fn escape(state: &State, v: &Value) -> Result<Value, Error> {
143 if v.is_safe() {
144 return Ok(v.clone());
145 }
146
147 // this tries to use the escaping flag of the current scope, then
148 // of the initial state and if that is also not set it falls back
149 // to HTML.
150 let auto_escape = match state.auto_escape() {
151 AutoEscape::None => match state.env().initial_auto_escape(state.name()) {
152 AutoEscape::None => AutoEscape::Html,
153 other => other,
154 },
155 other => other,
156 };
157 let mut rv = match v.as_str() {
158 Some(s) => String::with_capacity(s.len()),
159 None => String::new(),
160 };
161 let mut out = Output::new(&mut rv);
162 ok!(write_escaped(&mut out, auto_escape, v));
163 Ok(Value::from_safe_string(rv))
164}
165
166#[cfg(feature = "builtins")]
167mod builtins {
168 use super::*;
169
170 use crate::error::ErrorKind;
171 use crate::utils::{safe_sort, splitn_whitespace};
172 use crate::value::ops::{self, as_f64};
173 use crate::value::{Enumerator, Kwargs, Object, ObjectRepr, ValueKind, ValueRepr};
174 use std::borrow::Cow;
175 use std::cmp::Ordering;
176 use std::fmt::Write;
177 use std::mem;
178
179 /// Converts a value to uppercase.
180 ///
181 /// ```jinja
182 /// <h1>{{ chapter.title|upper }}</h1>
183 /// ```
184 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
185 pub fn upper(v: Cow<'_, str>) -> String {
186 v.to_uppercase()
187 }
188
189 /// Converts a value to lowercase.
190 ///
191 /// ```jinja
192 /// <h1>{{ chapter.title|lower }}</h1>
193 /// ```
194 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
195 pub fn lower(v: Cow<'_, str>) -> String {
196 v.to_lowercase()
197 }
198
199 /// Converts a value to title case.
200 ///
201 /// ```jinja
202 /// <h1>{{ chapter.title|title }}</h1>
203 /// ```
204 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
205 pub fn title(v: Cow<'_, str>) -> String {
206 let mut rv = String::new();
207 let mut capitalize = true;
208 for c in v.chars() {
209 if c.is_ascii_punctuation() || c.is_whitespace() {
210 rv.push(c);
211 capitalize = true;
212 } else if capitalize {
213 write!(rv, "{}", c.to_uppercase()).unwrap();
214 capitalize = false;
215 } else {
216 write!(rv, "{}", c.to_lowercase()).unwrap();
217 }
218 }
219 rv
220 }
221
222 /// Convert the string with all its characters lowercased
223 /// apart from the first char which is uppercased.
224 ///
225 /// ```jinja
226 /// <h1>{{ chapter.title|capitalize }}</h1>
227 /// ```
228 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
229 pub fn capitalize(text: Cow<'_, str>) -> String {
230 let mut chars = text.chars();
231 match chars.next() {
232 None => String::new(),
233 Some(f) => f.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
234 }
235 }
236
237 /// Does a string replace.
238 ///
239 /// It replaces all occurrences of the first parameter with the second.
240 ///
241 /// ```jinja
242 /// {{ "Hello World"|replace("Hello", "Goodbye") }}
243 /// -> Goodbye World
244 /// ```
245 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
246 pub fn replace(
247 _state: &State,
248 v: Cow<'_, str>,
249 from: Cow<'_, str>,
250 to: Cow<'_, str>,
251 ) -> String {
252 v.replace(&from as &str, &to as &str)
253 }
254
255 /// Returns the "length" of the value
256 ///
257 /// By default this filter is also registered under the alias `count`.
258 ///
259 /// ```jinja
260 /// <p>Search results: {{ results|length }}
261 /// ```
262 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
263 pub fn length(v: &Value) -> Result<usize, Error> {
264 v.len().ok_or_else(|| {
265 Error::new(
266 ErrorKind::InvalidOperation,
267 format!("cannot calculate length of value of type {}", v.kind()),
268 )
269 })
270 }
271
272 fn cmp_helper(a: &Value, b: &Value, case_sensitive: bool) -> Ordering {
273 if !case_sensitive {
274 if let (Some(a), Some(b)) = (a.as_str(), b.as_str()) {
275 #[cfg(feature = "unicode")]
276 {
277 return unicase::UniCase::new(a).cmp(&unicase::UniCase::new(b));
278 }
279 #[cfg(not(feature = "unicode"))]
280 {
281 return a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase());
282 }
283 }
284 }
285 a.cmp(b)
286 }
287
288 /// Dict sorting functionality.
289 ///
290 /// This filter works like `|items` but sorts the pairs by key first.
291 ///
292 /// The filter accepts a few keyword arguments:
293 ///
294 /// * `case_sensitive`: set to `true` to make the sorting of strings case sensitive.
295 /// * `by`: set to `"value"` to sort by value. Defaults to `"key"`.
296 /// * `reverse`: set to `true` to sort in reverse.
297 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
298 pub fn dictsort(v: &Value, kwargs: Kwargs) -> Result<Value, Error> {
299 if v.kind() != ValueKind::Map {
300 return Err(Error::new(
301 ErrorKind::InvalidOperation,
302 "cannot convert value into pair list",
303 ));
304 }
305
306 let by_value = matches!(ok!(kwargs.get("by")), Some("value"));
307 let case_sensitive = ok!(kwargs.get::<Option<bool>>("case_sensitive")).unwrap_or(false);
308 let mut rv: Vec<_> = ok!(v.try_iter())
309 .map(|key| (key.clone(), v.get_item(&key).unwrap_or(Value::UNDEFINED)))
310 .collect();
311 safe_sort(&mut rv, |a, b| {
312 let (a, b) = if by_value { (&a.1, &b.1) } else { (&a.0, &b.0) };
313 cmp_helper(a, b, case_sensitive)
314 })?;
315 if let Some(true) = ok!(kwargs.get("reverse")) {
316 rv.reverse();
317 }
318 kwargs.assert_all_used()?;
319 Ok(rv
320 .into_iter()
321 .map(|(k, v)| Value::from(vec![k, v]))
322 .collect())
323 }
324
325 /// Returns an iterable of pairs (items) from a mapping.
326 ///
327 /// This can be used to iterate over keys and values of a mapping
328 /// at once. Note that this will use the original order of the map
329 /// which is typically arbitrary unless the `preserve_order` feature
330 /// is used in which case the original order of the map is retained.
331 /// It's generally better to use `|dictsort` which sorts the map by
332 /// key before iterating.
333 ///
334 /// ```jinja
335 /// <dl>
336 /// {% for key, value in my_dict|items %}
337 /// <dt>{{ key }}
338 /// <dd>{{ value }}
339 /// {% endfor %}
340 /// </dl>
341 /// ```
342 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
343 pub fn items(v: &Value) -> Result<Value, Error> {
344 if v.kind() == ValueKind::Map {
345 Ok(Value::make_object_iterable(v.clone(), |v| {
346 match v.as_object().and_then(|v| v.try_iter_pairs()) {
347 Some(iter) => Box::new(iter.map(|(key, value)| Value::from(vec![key, value]))),
348 None => Box::new(
349 // this really should not happen unless the object changes it's shape
350 // after the initial check
351 Some(Value::from(Error::new(
352 ErrorKind::InvalidOperation,
353 format!("{} is not iterable", v.kind()),
354 )))
355 .into_iter(),
356 ),
357 }
358 }))
359 } else {
360 Err(Error::new(
361 ErrorKind::InvalidOperation,
362 "cannot convert value into pairs",
363 ))
364 }
365 }
366
367 /// Reverses an iterable or string
368 ///
369 /// ```jinja
370 /// {% for user in users|reverse %}
371 /// <li>{{ user.name }}
372 /// {% endfor %}
373 /// ```
374 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
375 pub fn reverse(v: &Value) -> Result<Value, Error> {
376 v.reverse()
377 }
378
379 /// Trims a value
380 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
381 pub fn trim(s: Cow<'_, str>, chars: Option<Cow<'_, str>>) -> String {
382 match chars {
383 Some(chars) => {
384 let chars = chars.chars().collect::<Vec<_>>();
385 s.trim_matches(&chars[..]).to_string()
386 }
387 None => s.trim().to_string(),
388 }
389 }
390
391 /// Joins a sequence by a character
392 ///
393 /// ```jinja
394 /// {{ "Foo Bar Baz" | join(", ") }} -> foo, bar, baz
395 /// ```
396 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
397 pub fn join(val: &Value, joiner: Option<Cow<'_, str>>) -> Result<String, Error> {
398 if val.is_undefined() || val.is_none() {
399 return Ok(String::new());
400 }
401
402 let joiner = joiner.as_ref().unwrap_or(&Cow::Borrowed(""));
403 let iter = ok!(val.try_iter().map_err(|err| {
404 Error::new(
405 ErrorKind::InvalidOperation,
406 format!("cannot join value of type {}", val.kind()),
407 )
408 .with_source(err)
409 }));
410
411 let mut rv = String::new();
412 for item in iter {
413 if !rv.is_empty() {
414 rv.push_str(joiner);
415 }
416 if let Some(s) = item.as_str() {
417 rv.push_str(s);
418 } else {
419 write!(rv, "{item}").ok();
420 }
421 }
422 Ok(rv)
423 }
424
425 /// Split a string into its substrings, using `split` as the separator string.
426 ///
427 /// If `split` is not provided or `none` the string is split at all whitespace
428 /// characters and multiple spaces and empty strings will be removed from the
429 /// result.
430 ///
431 /// The `maxsplits` parameter defines the maximum number of splits
432 /// (starting from the left). Note that this follows Python conventions
433 /// rather than Rust ones so `1` means one split and two resulting items.
434 ///
435 /// ```jinja
436 /// {{ "hello world"|split|list }}
437 /// -> ["hello", "world"]
438 ///
439 /// {{ "c,s,v"|split(",")|list }}
440 /// -> ["c", "s", "v"]
441 /// ```
442 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
443 pub fn split(s: Arc<str>, split: Option<Arc<str>>, maxsplits: Option<i64>) -> Value {
444 let maxsplits = maxsplits.and_then(|x| if x >= 0 { Some(x as usize + 1) } else { None });
445
446 Value::make_object_iterable((s, split), move |(s, split)| match (split, maxsplits) {
447 (None, None) => Box::new(s.split_whitespace().map(Value::from)),
448 (Some(split), None) => Box::new(s.split(split as &str).map(Value::from)),
449 (None, Some(n)) => Box::new(splitn_whitespace(s, n).map(Value::from)),
450 (Some(split), Some(n)) => Box::new(s.splitn(n, split as &str).map(Value::from)),
451 })
452 }
453
454 /// Splits a string into lines.
455 ///
456 /// The newline character is removed in the process and not retained. This
457 /// function supports both Windows and UNIX style newlines.
458 ///
459 /// ```jinja
460 /// {{ "foo\nbar\nbaz"|lines }}
461 /// -> ["foo", "bar", "baz"]
462 /// ```
463 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
464 pub fn lines(s: Arc<str>) -> Value {
465 Value::from_iter(s.lines().map(|x| x.to_string()))
466 }
467
468 /// If the value is undefined it will return the passed default value,
469 /// otherwise the value of the variable:
470 ///
471 /// ```jinja
472 /// <p>{{ my_variable|default("my_variable was not defined") }}</p>
473 /// ```
474 ///
475 /// Setting the optional second parameter to `true` will also treat falsy
476 /// values as undefined, e.g. empty strings:
477 ///
478 /// ```jinja
479 /// <p>{{ ""|default("string was empty", true) }}</p>
480 /// ```
481 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
482 pub fn default(value: &Value, other: Option<Value>, lax: Option<bool>) -> Value {
483 if value.is_undefined() {
484 other.unwrap_or_else(|| Value::from(""))
485 } else if lax.unwrap_or(false) && !value.is_true() {
486 other.unwrap_or_else(|| Value::from(""))
487 } else {
488 value.clone()
489 }
490 }
491
492 /// Returns the absolute value of a number.
493 ///
494 /// ```jinja
495 /// |a - b| = {{ (a - b)|abs }}
496 /// -> |2 - 4| = 2
497 /// ```
498 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
499 pub fn abs(value: Value) -> Result<Value, Error> {
500 match value.0 {
501 ValueRepr::U64(_) | ValueRepr::U128(_) => Ok(value),
502 ValueRepr::I64(x) => match x.checked_abs() {
503 Some(rv) => Ok(Value::from(rv)),
504 None => Ok(Value::from((x as i128).abs())), // this cannot overflow
505 },
506 ValueRepr::I128(x) => {
507 x.0.checked_abs()
508 .map(Value::from)
509 .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "overflow on abs"))
510 }
511 ValueRepr::F64(x) => Ok(Value::from(x.abs())),
512 _ => Err(Error::new(
513 ErrorKind::InvalidOperation,
514 "cannot get absolute value",
515 )),
516 }
517 }
518
519 /// Converts a value into an integer.
520 ///
521 /// ```jinja
522 /// {{ "42"|int == 42 }} -> true
523 /// ```
524 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
525 pub fn int(value: &Value) -> Result<Value, Error> {
526 match &value.0 {
527 ValueRepr::Undefined(_) | ValueRepr::None => Ok(Value::from(0)),
528 ValueRepr::Bool(x) => Ok(Value::from(*x as u64)),
529 ValueRepr::U64(_) | ValueRepr::I64(_) | ValueRepr::U128(_) | ValueRepr::I128(_) => {
530 Ok(value.clone())
531 }
532 ValueRepr::F64(v) => Ok(Value::from(*v as i128)),
533 ValueRepr::String(..) | ValueRepr::SmallStr(_) => {
534 let s = value.as_str().unwrap();
535 if let Ok(i) = s.parse::<i128>() {
536 Ok(Value::from(i))
537 } else {
538 match s.parse::<f64>() {
539 Ok(f) => Ok(Value::from(f as i128)),
540 Err(err) => Err(Error::new(ErrorKind::InvalidOperation, err.to_string())),
541 }
542 }
543 }
544 ValueRepr::Bytes(_) | ValueRepr::Object(_) => Err(Error::new(
545 ErrorKind::InvalidOperation,
546 format!("cannot convert {} to integer", value.kind()),
547 )),
548 ValueRepr::Invalid(_) => value.clone().validate(),
549 }
550 }
551
552 /// Converts a value into a float.
553 ///
554 /// ```jinja
555 /// {{ "42.5"|float == 42.5 }} -> true
556 /// ```
557 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
558 pub fn float(value: &Value) -> Result<Value, Error> {
559 match &value.0 {
560 ValueRepr::Undefined(_) | ValueRepr::None => Ok(Value::from(0.0)),
561 ValueRepr::Bool(x) => Ok(Value::from(*x as u64 as f64)),
562 ValueRepr::String(..) | ValueRepr::SmallStr(_) => value
563 .as_str()
564 .unwrap()
565 .parse::<f64>()
566 .map(Value::from)
567 .map_err(|err| Error::new(ErrorKind::InvalidOperation, err.to_string())),
568 ValueRepr::Invalid(_) => value.clone().validate(),
569 _ => as_f64(value, true).map(Value::from).ok_or_else(|| {
570 Error::new(
571 ErrorKind::InvalidOperation,
572 format!("cannot convert {} to float", value.kind()),
573 )
574 }),
575 }
576 }
577
578 /// Sums up all the values in a sequence.
579 ///
580 /// ```jinja
581 /// {{ range(10)|sum }} -> 45
582 /// ```
583 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
584 pub fn sum(state: &State, values: Value) -> Result<Value, Error> {
585 let mut rv = Value::from(0);
586 let iter = ok!(state.undefined_behavior().try_iter(values));
587 for value in iter {
588 if value.is_undefined() {
589 ok!(state.undefined_behavior().handle_undefined(false));
590 continue;
591 } else if !value.is_number() {
592 return Err(Error::new(
593 ErrorKind::InvalidOperation,
594 format!("can only sum numbers, got {}", value.kind()),
595 ));
596 }
597 rv = ok!(ops::add(&rv, &value));
598 }
599
600 Ok(rv)
601 }
602
603 /// Looks up an attribute.
604 ///
605 /// In MiniJinja this is the same as the `[]` operator. In Jinja2 there is a
606 /// small difference which is why this filter is sometimes used in Jinja2
607 /// templates. For compatibility it's provided here as well.
608 ///
609 /// ```jinja
610 /// {{ value['key'] == value|attr('key') }} -> true
611 /// ```
612 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
613 pub fn attr(value: &Value, key: &Value) -> Result<Value, Error> {
614 value.get_item(key)
615 }
616
617 /// Round the number to a given precision.
618 ///
619 /// Round the number to a given precision. The first parameter specifies the
620 /// precision (default is 0).
621 ///
622 /// ```jinja
623 /// {{ 42.55|round }}
624 /// -> 43.0
625 /// ```
626 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
627 pub fn round(value: Value, precision: Option<i32>) -> Result<Value, Error> {
628 match value.0 {
629 ValueRepr::I64(_) | ValueRepr::I128(_) | ValueRepr::U64(_) | ValueRepr::U128(_) => {
630 Ok(value)
631 }
632 ValueRepr::F64(val) => {
633 let x = 10f64.powi(precision.unwrap_or(0));
634 Ok(Value::from((x * val).round() / x))
635 }
636 _ => Err(Error::new(
637 ErrorKind::InvalidOperation,
638 format!("cannot round value ({})", value.kind()),
639 )),
640 }
641 }
642
643 /// Returns the first item from an iterable.
644 ///
645 /// If the list is empty `undefined` is returned.
646 ///
647 /// ```jinja
648 /// <dl>
649 /// <dt>primary email
650 /// <dd>{{ user.email_addresses|first|default('no user') }}
651 /// </dl>
652 /// ```
653 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
654 pub fn first(value: &Value) -> Result<Value, Error> {
655 if let Some(s) = value.as_str() {
656 Ok(s.chars().next().map_or(Value::UNDEFINED, Value::from))
657 } else if let Some(mut iter) = value.as_object().and_then(|x| x.try_iter()) {
658 Ok(iter.next().unwrap_or(Value::UNDEFINED))
659 } else {
660 Err(Error::new(
661 ErrorKind::InvalidOperation,
662 "cannot get first item from value",
663 ))
664 }
665 }
666
667 /// Returns the last item from an iterable.
668 ///
669 /// If the list is empty `undefined` is returned.
670 ///
671 /// ```jinja
672 /// <h2>Most Recent Update</h2>
673 /// {% with update = updates|last %}
674 /// <dl>
675 /// <dt>Location
676 /// <dd>{{ update.location }}
677 /// <dt>Status
678 /// <dd>{{ update.status }}
679 /// </dl>
680 /// {% endwith %}
681 /// ```
682 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
683 pub fn last(value: Value) -> Result<Value, Error> {
684 if let Some(s) = value.as_str() {
685 Ok(s.chars().next_back().map_or(Value::UNDEFINED, Value::from))
686 } else if matches!(value.kind(), ValueKind::Seq | ValueKind::Iterable) {
687 let rev = ok!(value.reverse());
688 let mut iter = ok!(rev.try_iter());
689 Ok(iter.next().unwrap_or_default())
690 } else {
691 Err(Error::new(
692 ErrorKind::InvalidOperation,
693 "cannot get last item from value",
694 ))
695 }
696 }
697
698 /// Returns the smallest item from an iterable.
699 ///
700 /// ```jinja
701 /// {{ [1, 2, 3, 4]|min }} -> 1
702 /// ```
703 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
704 pub fn min(state: &State, value: Value) -> Result<Value, Error> {
705 let iter = ok!(state.undefined_behavior().try_iter(value).map_err(|err| {
706 Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err)
707 }));
708 Ok(iter.min().unwrap_or(Value::UNDEFINED))
709 }
710
711 /// Returns the largest item from an iterable.
712 ///
713 /// ```jinja
714 /// {{ [1, 2, 3, 4]|max }} -> 4
715 /// ```
716 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
717 pub fn max(state: &State, value: Value) -> Result<Value, Error> {
718 let iter = ok!(state.undefined_behavior().try_iter(value).map_err(|err| {
719 Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err)
720 }));
721 Ok(iter.max().unwrap_or(Value::UNDEFINED))
722 }
723
724 /// Returns the sorted version of the given list.
725 ///
726 /// The filter accepts a few keyword arguments:
727 ///
728 /// * `case_sensitive`: set to `true` to make the sorting of strings case sensitive.
729 /// * `attribute`: can be set to an attribute or dotted path to sort by that attribute
730 /// * `reverse`: set to `true` to sort in reverse.
731 ///
732 /// ```jinja
733 /// {{ [1, 3, 2, 4]|sort }} -> [4, 3, 2, 1]
734 /// {{ [1, 3, 2, 4]|sort(reverse=true) }} -> [1, 2, 3, 4]
735 /// # Sort users by age attribute in descending order.
736 /// {{ users|sort(attribute="age") }}
737 /// # Sort users by age attribute in ascending order.
738 /// {{ users|sort(attribute="age", reverse=true) }}
739 /// ```
740 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
741 pub fn sort(state: &State, value: Value, kwargs: Kwargs) -> Result<Value, Error> {
742 let mut items = ok!(state.undefined_behavior().try_iter(value).map_err(|err| {
743 Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err)
744 }))
745 .collect::<Vec<_>>();
746 let case_sensitive = ok!(kwargs.get::<Option<bool>>("case_sensitive")).unwrap_or(false);
747 if let Some(attr) = ok!(kwargs.get::<Option<&str>>("attribute")) {
748 safe_sort(&mut items, |a, b| {
749 match (a.get_path(attr), b.get_path(attr)) {
750 (Ok(a), Ok(b)) => cmp_helper(&a, &b, case_sensitive),
751 _ => Ordering::Equal,
752 }
753 })?;
754 } else {
755 safe_sort(&mut items, |a, b| cmp_helper(a, b, case_sensitive))?;
756 }
757 if let Some(true) = ok!(kwargs.get("reverse")) {
758 items.reverse();
759 }
760 ok!(kwargs.assert_all_used());
761 Ok(Value::from(items))
762 }
763
764 /// Converts the input value into a list.
765 ///
766 /// If the value is already a list, then it's returned unchanged.
767 /// Applied to a map this returns the list of keys, applied to a
768 /// string this returns the characters. If the value is undefined
769 /// an empty list is returned.
770 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
771 pub fn list(state: &State, value: Value) -> Result<Value, Error> {
772 let iter = ok!(state.undefined_behavior().try_iter(value).map_err(|err| {
773 Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err)
774 }));
775 Ok(Value::from(iter.collect::<Vec<_>>()))
776 }
777
778 /// Converts a value into a string if it's not one already.
779 ///
780 /// If the string has been marked as safe, that value is preserved.
781 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
782 pub fn string(value: &Value) -> Value {
783 if value.kind() == ValueKind::String {
784 value.clone()
785 } else {
786 value.to_string().into()
787 }
788 }
789
790 /// Converts the value into a boolean value.
791 ///
792 /// This behaves the same as the if statement does with regards to
793 /// handling of boolean values.
794 ///
795 /// ```jinja
796 /// {{ 42|bool }} -> true
797 /// ```
798 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
799 pub fn bool(value: &Value) -> bool {
800 value.is_true()
801 }
802
803 /// Slice an iterable and return a list of lists containing
804 /// those items.
805 ///
806 /// Useful if you want to create a div containing three ul tags that
807 /// represent columns:
808 ///
809 /// ```jinja
810 /// <div class="columnwrapper">
811 /// {% for column in items|slice(3) %}
812 /// <ul class="column-{{ loop.index }}">
813 /// {% for item in column %}
814 /// <li>{{ item }}</li>
815 /// {% endfor %}
816 /// </ul>
817 /// {% endfor %}
818 /// </div>
819 /// ```
820 ///
821 /// If you pass it a second argument it’s used to fill missing values on the
822 /// last iteration.
823 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
824 pub fn slice(
825 state: &State,
826 value: Value,
827 count: usize,
828 fill_with: Option<Value>,
829 ) -> Result<Value, Error> {
830 if count == 0 {
831 return Err(Error::new(ErrorKind::InvalidOperation, "count cannot be 0"));
832 }
833 let items = ok!(state.undefined_behavior().try_iter(value)).collect::<Vec<_>>();
834 let len = items.len();
835 let items_per_slice = len / count;
836 let slices_with_extra = len % count;
837 let mut offset = 0;
838 let mut rv = Vec::with_capacity(count);
839
840 for slice in 0..count {
841 let start = offset + slice * items_per_slice;
842 if slice < slices_with_extra {
843 offset += 1;
844 }
845 let end = offset + (slice + 1) * items_per_slice;
846 let tmp = &items[start..end];
847
848 if let Some(ref filler) = fill_with {
849 if slice >= slices_with_extra {
850 let mut tmp = tmp.to_vec();
851 tmp.push(filler.clone());
852 rv.push(Value::from(tmp));
853 continue;
854 }
855 }
856
857 rv.push(Value::from(tmp.to_vec()));
858 }
859
860 Ok(Value::from(rv))
861 }
862
863 /// Batch items.
864 ///
865 /// This filter works pretty much like `slice` just the other way round. It
866 /// returns a list of lists with the given number of items. If you provide a
867 /// second parameter this is used to fill up missing items.
868 ///
869 /// ```jinja
870 /// <table>
871 /// {% for row in items|batch(3, ' ') %}
872 /// <tr>
873 /// {% for column in row %}
874 /// <td>{{ column }}</td>
875 /// {% endfor %}
876 /// </tr>
877 /// {% endfor %}
878 /// </table>
879 /// ```
880 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
881 pub fn batch(
882 state: &State,
883 value: Value,
884 count: usize,
885 fill_with: Option<Value>,
886 ) -> Result<Value, Error> {
887 if count == 0 {
888 return Err(Error::new(ErrorKind::InvalidOperation, "count cannot be 0"));
889 }
890 let mut rv = Vec::with_capacity(value.len().unwrap_or(0) / count);
891 let mut tmp = Vec::with_capacity(count);
892
893 for item in ok!(state.undefined_behavior().try_iter(value)) {
894 if tmp.len() == count {
895 rv.push(Value::from(mem::replace(
896 &mut tmp,
897 Vec::with_capacity(count),
898 )));
899 }
900 tmp.push(item);
901 }
902
903 if !tmp.is_empty() {
904 if let Some(filler) = fill_with {
905 for _ in 0..count - tmp.len() {
906 tmp.push(filler.clone());
907 }
908 }
909 rv.push(Value::from(tmp));
910 }
911
912 Ok(Value::from(rv))
913 }
914
915 /// Dumps a value to JSON.
916 ///
917 /// This filter is only available if the `json` feature is enabled. The resulting
918 /// value is safe to use in HTML as well as it will not contain any special HTML
919 /// characters. The optional parameter to the filter can be set to `true` to enable
920 /// pretty printing. Not that the `"` character is left unchanged as it's the
921 /// JSON string delimiter. If you want to pass JSON serialized this way into an
922 /// HTTP attribute use single quoted HTML attributes:
923 ///
924 /// ```jinja
925 /// <script>
926 /// const GLOBAL_CONFIG = {{ global_config|tojson }};
927 /// </script>
928 /// <a href="#" data-info='{{ json_object|tojson }}'>...</a>
929 /// ```
930 ///
931 /// The filter takes one argument `indent` (which can also be passed as keyword
932 /// argument for compatibility with Jinja2) which can be set to `true` to enable
933 /// pretty printing or an integer to control the indentation of the pretty
934 /// printing feature.
935 ///
936 /// ```jinja
937 /// <script>
938 /// const GLOBAL_CONFIG = {{ global_config|tojson(indent=2) }};
939 /// </script>
940 /// ```
941 #[cfg_attr(docsrs, doc(cfg(all(feature = "builtins", feature = "json"))))]
942 #[cfg(feature = "json")]
943 pub fn tojson(value: &Value, indent: Option<Value>, args: Kwargs) -> Result<Value, Error> {
944 let indent = match indent {
945 Some(indent) => Some(indent),
946 None => ok!(args.get("indent")),
947 };
948 let indent = match indent {
949 None => None,
950 Some(ref val) => match bool::try_from(val.clone()).ok() {
951 Some(true) => Some(2),
952 Some(false) => None,
953 None => Some(ok!(usize::try_from(val.clone()))),
954 },
955 };
956 ok!(args.assert_all_used());
957 if let Some(indent) = indent {
958 let mut out = Vec::<u8>::new();
959 let indentation = " ".repeat(indent);
960 let formatter = serde_json::ser::PrettyFormatter::with_indent(indentation.as_bytes());
961 let mut s = serde_json::Serializer::with_formatter(&mut out, formatter);
962 serde::Serialize::serialize(&value, &mut s)
963 .map(|_| unsafe { String::from_utf8_unchecked(out) })
964 } else {
965 serde_json::to_string(&value)
966 }
967 .map_err(|err| {
968 Error::new(ErrorKind::InvalidOperation, "cannot serialize to JSON").with_source(err)
969 })
970 .map(|s| {
971 // When this filter is used the return value is safe for both HTML and JSON
972 let mut rv = String::with_capacity(s.len());
973 for c in s.chars() {
974 match c {
975 '<' => rv.push_str("\\u003c"),
976 '>' => rv.push_str("\\u003e"),
977 '&' => rv.push_str("\\u0026"),
978 '\'' => rv.push_str("\\u0027"),
979 _ => rv.push(c),
980 }
981 }
982 Value::from_safe_string(rv)
983 })
984 }
985
986 /// Indents Value with spaces
987 ///
988 /// The first optional parameter to the filter can be set to `true` to
989 /// indent the first line. The parameter defaults to false.
990 /// the second optional parameter to the filter can be set to `true`
991 /// to indent blank lines. The parameter defaults to false.
992 /// This filter is useful, if you want to template yaml-files
993 ///
994 /// ```jinja
995 /// example:
996 /// config:
997 /// {{ global_config|indent(2) }} # does not indent first line
998 /// {{ global_config|indent(2,true) }} # indent whole Value with two spaces
999 /// {{ global_config|indent(2,true,true)}} # indent whole Value and all blank lines
1000 /// ```
1001 #[cfg_attr(docsrs, doc(cfg(all(feature = "builtins"))))]
1002 pub fn indent(
1003 mut value: String,
1004 width: usize,
1005 indent_first_line: Option<bool>,
1006 indent_blank_lines: Option<bool>,
1007 ) -> String {
1008 fn strip_trailing_newline(input: &mut String) {
1009 if input.ends_with('\n') {
1010 input.truncate(input.len() - 1);
1011 }
1012 if input.ends_with('\r') {
1013 input.truncate(input.len() - 1);
1014 }
1015 }
1016
1017 strip_trailing_newline(&mut value);
1018 let indent_with = " ".repeat(width);
1019 let mut output = String::new();
1020 let mut iterator = value.split('\n');
1021 if !indent_first_line.unwrap_or(false) {
1022 output.push_str(iterator.next().unwrap());
1023 output.push('\n');
1024 }
1025 for line in iterator {
1026 if line.is_empty() {
1027 if indent_blank_lines.unwrap_or(false) {
1028 output.push_str(&indent_with);
1029 }
1030 } else {
1031 write!(output, "{}{}", indent_with, line).ok();
1032 }
1033 output.push('\n');
1034 }
1035 strip_trailing_newline(&mut output);
1036 output
1037 }
1038
1039 /// URL encodes a value.
1040 ///
1041 /// If given a map it encodes the parameters into a query set, otherwise it
1042 /// encodes the stringified value. If the value is none or undefined, an
1043 /// empty string is returned.
1044 ///
1045 /// ```jinja
1046 /// <a href="/search?{{ {"q": "my search", "lang": "fr"}|urlencode }}">Search</a>
1047 /// ```
1048 #[cfg_attr(docsrs, doc(cfg(all(feature = "builtins", feature = "urlencode"))))]
1049 #[cfg(feature = "urlencode")]
1050 pub fn urlencode(value: &Value) -> Result<String, Error> {
1051 const SET: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
1052 .remove(b'/')
1053 .remove(b'.')
1054 .remove(b'-')
1055 .remove(b'_')
1056 .add(b' ');
1057
1058 if value.kind() == ValueKind::Map {
1059 let mut rv = String::new();
1060 for k in ok!(value.try_iter()) {
1061 let v = ok!(value.get_item(&k));
1062 if v.is_none() || v.is_undefined() {
1063 continue;
1064 }
1065 if !rv.is_empty() {
1066 rv.push('&');
1067 }
1068 write!(
1069 rv,
1070 "{}={}",
1071 percent_encoding::utf8_percent_encode(&k.to_string(), SET),
1072 percent_encoding::utf8_percent_encode(&v.to_string(), SET)
1073 )
1074 .unwrap();
1075 }
1076 Ok(rv)
1077 } else {
1078 match &value.0 {
1079 ValueRepr::None | ValueRepr::Undefined(_) => Ok("".into()),
1080 ValueRepr::Bytes(b) => Ok(percent_encoding::percent_encode(b, SET).to_string()),
1081 ValueRepr::String(..) | ValueRepr::SmallStr(_) => Ok(
1082 percent_encoding::utf8_percent_encode(value.as_str().unwrap(), SET).to_string(),
1083 ),
1084 _ => Ok(percent_encoding::utf8_percent_encode(&value.to_string(), SET).to_string()),
1085 }
1086 }
1087 }
1088
1089 fn select_or_reject(
1090 state: &State,
1091 invert: bool,
1092 value: Value,
1093 attr: Option<Cow<'_, str>>,
1094 test_name: Option<Cow<'_, str>>,
1095 args: crate::value::Rest<Value>,
1096 ) -> Result<Vec<Value>, Error> {
1097 let mut rv = vec![];
1098 let test = if let Some(test_name) = test_name {
1099 Some(ok!(state
1100 .env()
1101 .get_test(&test_name)
1102 .ok_or_else(|| Error::from(ErrorKind::UnknownTest))))
1103 } else {
1104 None
1105 };
1106 for value in ok!(state.undefined_behavior().try_iter(value)) {
1107 let test_value = if let Some(ref attr) = attr {
1108 ok!(value.get_path(attr))
1109 } else {
1110 value.clone()
1111 };
1112 let passed = if let Some(test) = test {
1113 let new_args = Some(test_value)
1114 .into_iter()
1115 .chain(args.0.iter().cloned())
1116 .collect::<Vec<_>>();
1117 ok!(test.call(state, &new_args)).is_true()
1118 } else {
1119 test_value.is_true()
1120 };
1121 if passed != invert {
1122 rv.push(value);
1123 }
1124 }
1125 Ok(rv)
1126 }
1127
1128 /// Creates a new sequence of values that pass a test.
1129 ///
1130 /// Filters a sequence of objects by applying a test to each object.
1131 /// Only values that pass the test are included.
1132 ///
1133 /// If no test is specified, each object will be evaluated as a boolean.
1134 ///
1135 /// ```jinja
1136 /// {{ [1, 2, 3, 4]|select("odd") }} -> [1, 3]
1137 /// {{ [false, null, 42]|select }} -> [42]
1138 /// ```
1139 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1140 pub fn select(
1141 state: &State,
1142 value: Value,
1143 test_name: Option<Cow<'_, str>>,
1144 args: crate::value::Rest<Value>,
1145 ) -> Result<Vec<Value>, Error> {
1146 select_or_reject(state, false, value, None, test_name, args)
1147 }
1148
1149 /// Creates a new sequence of values of which an attribute passes a test.
1150 ///
1151 /// This functions like [`select`] but it will test an attribute of the
1152 /// object itself:
1153 ///
1154 /// ```jinja
1155 /// {{ users|selectattr("is_active") }} -> all users where x.is_active is true
1156 /// {{ users|selectattr("id", "even") }} -> returns all users with an even id
1157 /// ```
1158 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1159 pub fn selectattr(
1160 state: &State,
1161 value: Value,
1162 attr: Cow<'_, str>,
1163 test_name: Option<Cow<'_, str>>,
1164 args: crate::value::Rest<Value>,
1165 ) -> Result<Vec<Value>, Error> {
1166 select_or_reject(state, false, value, Some(attr), test_name, args)
1167 }
1168
1169 /// Creates a new sequence of values that don't pass a test.
1170 ///
1171 /// This is the inverse of [`select`].
1172 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1173 pub fn reject(
1174 state: &State,
1175 value: Value,
1176 test_name: Option<Cow<'_, str>>,
1177 args: crate::value::Rest<Value>,
1178 ) -> Result<Vec<Value>, Error> {
1179 select_or_reject(state, true, value, None, test_name, args)
1180 }
1181
1182 /// Creates a new sequence of values of which an attribute does not pass a test.
1183 ///
1184 /// This functions like [`select`] but it will test an attribute of the
1185 /// object itself:
1186 ///
1187 /// ```jinja
1188 /// {{ users|rejectattr("is_active") }} -> all users where x.is_active is false
1189 /// {{ users|rejectattr("id", "even") }} -> returns all users with an odd id
1190 /// ```
1191 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1192 pub fn rejectattr(
1193 state: &State,
1194 value: Value,
1195 attr: Cow<'_, str>,
1196 test_name: Option<Cow<'_, str>>,
1197 args: crate::value::Rest<Value>,
1198 ) -> Result<Vec<Value>, Error> {
1199 select_or_reject(state, true, value, Some(attr), test_name, args)
1200 }
1201
1202 /// Applies a filter to a sequence of objects or looks up an attribute.
1203 ///
1204 /// This is useful when dealing with lists of objects but you are really
1205 /// only interested in a certain value of it.
1206 ///
1207 /// The basic usage is mapping on an attribute. Given a list of users
1208 /// you can for instance quickly select the username and join on it:
1209 ///
1210 /// ```jinja
1211 /// {{ users|map(attribute='username')|join(', ') }}
1212 /// ```
1213 ///
1214 /// You can specify a `default` value to use if an object in the list does
1215 /// not have the given attribute.
1216 ///
1217 /// ```jinja
1218 /// {{ users|map(attribute="username", default="Anonymous")|join(", ") }}
1219 /// ```
1220 ///
1221 /// Alternatively you can have `map` invoke a filter by passing the name of the
1222 /// filter and the arguments afterwards. A good example would be applying a
1223 /// text conversion filter on a sequence:
1224 ///
1225 /// ```jinja
1226 /// Users on this page: {{ titles|map('lower')|join(', ') }}
1227 /// ```
1228 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1229 pub fn map(
1230 state: &State,
1231 value: Value,
1232 args: crate::value::Rest<Value>,
1233 ) -> Result<Vec<Value>, Error> {
1234 let mut rv = Vec::with_capacity(value.len().unwrap_or(0));
1235
1236 // attribute mapping
1237 let (args, kwargs): (&[Value], Kwargs) = crate::value::from_args(&args)?;
1238
1239 if let Some(attr) = ok!(kwargs.get::<Option<Value>>("attribute")) {
1240 if !args.is_empty() {
1241 return Err(Error::from(ErrorKind::TooManyArguments));
1242 }
1243 let default = if kwargs.has("default") {
1244 ok!(kwargs.get::<Value>("default"))
1245 } else {
1246 Value::UNDEFINED
1247 };
1248 for value in ok!(state.undefined_behavior().try_iter(value)) {
1249 let sub_val = match attr.as_str() {
1250 Some(path) => value.get_path(path),
1251 None => value.get_item(&attr),
1252 };
1253 rv.push(match (sub_val, &default) {
1254 (Ok(attr), _) => {
1255 if attr.is_undefined() {
1256 default.clone()
1257 } else {
1258 attr
1259 }
1260 }
1261 (Err(_), default) if !default.is_undefined() => default.clone(),
1262 (Err(err), _) => return Err(err),
1263 });
1264 }
1265 ok!(kwargs.assert_all_used());
1266 return Ok(rv);
1267 }
1268
1269 // filter mapping
1270 let filter_name = ok!(args
1271 .first()
1272 .ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "filter name is required")));
1273 let filter_name = ok!(filter_name.as_str().ok_or_else(|| {
1274 Error::new(ErrorKind::InvalidOperation, "filter name must be a string")
1275 }));
1276
1277 let filter = ok!(state
1278 .env()
1279 .get_filter(filter_name)
1280 .ok_or_else(|| Error::from(ErrorKind::UnknownFilter)));
1281 for value in ok!(state.undefined_behavior().try_iter(value)) {
1282 let new_args = Some(value.clone())
1283 .into_iter()
1284 .chain(args.iter().skip(1).cloned())
1285 .collect::<Vec<_>>();
1286 rv.push(ok!(filter.call(state, &new_args)));
1287 }
1288 Ok(rv)
1289 }
1290
1291 /// Group a sequence of objects by an attribute.
1292 ///
1293 /// The attribute can use dot notation for nested access, like `"address.city"``.
1294 /// The values are sorted first so only one group is returned for each unique value.
1295 /// The attribute can be passed as first argument or as keyword argument named
1296 /// `attribute`.
1297 ///
1298 /// For example, a list of User objects with a city attribute can be
1299 /// rendered in groups. In this example, grouper refers to the city value of
1300 /// the group.
1301 ///
1302 /// ```jinja
1303 /// <ul>{% for city, items in users|groupby("city") %}
1304 /// <li>{{ city }}
1305 /// <ul>{% for user in items %}
1306 /// <li>{{ user.name }}
1307 /// {% endfor %}</ul>
1308 /// </li>
1309 /// {% endfor %}</ul>
1310 /// ```
1311 ///
1312 /// groupby yields named tuples of `(grouper, list)``, which can be used instead
1313 /// of the tuple unpacking above. As such this example is equivalent:
1314 ///
1315 /// ```jinja
1316 /// <ul>{% for group in users|groupby(attribute="city") %}
1317 /// <li>{{ group.grouper }}
1318 /// <ul>{% for user in group.list %}
1319 /// <li>{{ user.name }}
1320 /// {% endfor %}</ul>
1321 /// </li>
1322 /// {% endfor %}</ul>
1323 /// ```
1324 ///
1325 /// You can specify a default value to use if an object in the list does not
1326 /// have the given attribute.
1327 ///
1328 /// ```jinja
1329 /// <ul>{% for city, items in users|groupby("city", default="NY") %}
1330 /// <li>{{ city }}: {{ items|map(attribute="name")|join(", ") }}</li>
1331 /// {% endfor %}</ul>
1332 /// ```
1333 ///
1334 /// Like the [`sort`] filter, sorting and grouping is case-insensitive by default.
1335 /// The key for each group will have the case of the first item in that group
1336 /// of values. For example, if a list of users has cities `["CA", "NY", "ca"]``,
1337 /// the "CA" group will have two values. This can be disabled by passing
1338 /// `case_sensitive=True`.
1339 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1340 pub fn groupby(value: Value, attribute: Option<&str>, kwargs: Kwargs) -> Result<Value, Error> {
1341 let default = ok!(kwargs.get::<Option<Value>>("default")).unwrap_or_default();
1342 let case_sensitive = ok!(kwargs.get::<Option<bool>>("case_sensitive")).unwrap_or(false);
1343 let attr = match attribute {
1344 Some(attr) => attr,
1345 None => ok!(kwargs.get::<&str>("attribute")),
1346 };
1347 let mut items: Vec<Value> = ok!(value.try_iter()).collect();
1348 safe_sort(&mut items, |a, b| {
1349 let a = a.get_path_or_default(attr, &default);
1350 let b = b.get_path_or_default(attr, &default);
1351 cmp_helper(&a, &b, case_sensitive)
1352 })?;
1353 ok!(kwargs.assert_all_used());
1354
1355 #[derive(Debug)]
1356 pub struct GroupTuple {
1357 grouper: Value,
1358 list: Vec<Value>,
1359 }
1360
1361 impl Object for GroupTuple {
1362 fn repr(self: &Arc<Self>) -> ObjectRepr {
1363 ObjectRepr::Seq
1364 }
1365
1366 fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
1367 match (key.as_usize(), key.as_str()) {
1368 (Some(0), None) | (None, Some("grouper")) => Some(self.grouper.clone()),
1369 (Some(1), None) | (None, Some("list")) => {
1370 Some(Value::make_object_iterable(self.clone(), |this| {
1371 Box::new(this.list.iter().cloned())
1372 as Box<dyn Iterator<Item = _> + Send + Sync>
1373 }))
1374 }
1375 _ => None,
1376 }
1377 }
1378
1379 fn enumerate(self: &Arc<Self>) -> Enumerator {
1380 Enumerator::Seq(2)
1381 }
1382 }
1383
1384 let mut rv = Vec::new();
1385 let mut grouper = None::<Value>;
1386 let mut list = Vec::new();
1387
1388 for item in items {
1389 let group_by = item.get_path_or_default(attr, &default);
1390 if let Some(ref last_grouper) = grouper {
1391 if cmp_helper(last_grouper, &group_by, case_sensitive) != Ordering::Equal {
1392 rv.push(Value::from_object(GroupTuple {
1393 grouper: last_grouper.clone(),
1394 list: std::mem::take(&mut list),
1395 }));
1396 }
1397 }
1398 grouper = Some(group_by);
1399 list.push(item);
1400 }
1401
1402 if !list.is_empty() {
1403 rv.push(Value::from_object(GroupTuple {
1404 grouper: grouper.unwrap(),
1405 list,
1406 }));
1407 }
1408
1409 Ok(Value::from_object(rv))
1410 }
1411
1412 /// Returns a list of unique items from the given iterable.
1413 ///
1414 /// ```jinja
1415 /// {{ ['foo', 'bar', 'foobar', 'foobar']|unique|list }}
1416 /// -> ['foo', 'bar', 'foobar']
1417 /// ```
1418 ///
1419 /// The unique items are yielded in the same order as their first occurrence
1420 /// in the iterable passed to the filter. The filter will not detect
1421 /// duplicate objects or arrays, only primitives such as strings or numbers.
1422 ///
1423 /// Optionally the `attribute` keyword argument can be used to make the filter
1424 /// operate on an attribute instead of the value itself. In this case only
1425 /// one city per state would be returned:
1426 ///
1427 /// ```jinja
1428 /// {{ list_of_cities|unique(attribute='state') }}
1429 /// ```
1430 ///
1431 /// Like the [`sort`] filter this operates case-insensitive by default.
1432 /// For example, if a list has the US state codes `["CA", "NY", "ca"]``,
1433 /// the resulting list will have `["CA", "NY"]`. This can be disabled by
1434 /// passing `case_sensitive=True`.
1435 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1436 pub fn unique(state: &State, values: Value, kwargs: Kwargs) -> Result<Value, Error> {
1437 use std::collections::BTreeSet;
1438
1439 let attr = ok!(kwargs.get::<Option<&str>>("attribute"));
1440 let case_sensitive = ok!(kwargs.get::<Option<bool>>("case_sensitive")).unwrap_or(false);
1441 ok!(kwargs.assert_all_used());
1442
1443 let mut rv = Vec::new();
1444 let mut seen = BTreeSet::new();
1445
1446 let iter = ok!(state.undefined_behavior().try_iter(values));
1447 for item in iter {
1448 let value_to_compare = if let Some(attr) = attr {
1449 item.get_path_or_default(attr, &Value::UNDEFINED)
1450 } else {
1451 item.clone()
1452 };
1453 let memorized_value = if case_sensitive {
1454 value_to_compare.clone()
1455 } else if let Some(s) = value_to_compare.as_str() {
1456 Value::from(s.to_lowercase())
1457 } else {
1458 value_to_compare.clone()
1459 };
1460
1461 if !seen.contains(&memorized_value) {
1462 rv.push(item);
1463 seen.insert(memorized_value);
1464 }
1465 }
1466
1467 Ok(Value::from(rv))
1468 }
1469
1470 /// Pretty print a variable.
1471 ///
1472 /// This is useful for debugging as it better shows what's inside an object.
1473 #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1474 pub fn pprint(value: &Value) -> String {
1475 format!("{:#?}", value)
1476 }
1477}
1478
1479#[cfg(feature = "builtins")]
1480pub use self::builtins::*;