diff --git a/minijinja/src/lib.rs b/minijinja/src/lib.rs index d05e29e6..11b43c9a 100644 --- a/minijinja/src/lib.rs +++ b/minijinja/src/lib.rs @@ -110,7 +110,8 @@ //! - `sync`: this feature makes MiniJinja's type `Send` and `Sync`. If this feature //! is disabled sending types across threads is often not possible. Thread bounds //! of things like callbacks however are not changing which means code that uses -//! MiniJinja still needs to be threadsafe. +//! MiniJinja still needs to be threadsafe. This also disables some features that +//! require synchronization such as the `loop.changed` feature. //! - `debug`: if this feature is removed some debug functionality of the engine is //! removed as well. This mainly affects the quality of error reporting. //! - `key_interning`: if this feature is removed the automatic string interning in diff --git a/minijinja/src/syntax.rs b/minijinja/src/syntax.rs index 06a01378..309c6b8c 100644 --- a/minijinja/src/syntax.rs +++ b/minijinja/src/syntax.rs @@ -195,9 +195,11 @@ //! - `loop.cycle`: A helper function to cycle between a list of sequences. See the explanation below. //! - `loop.depth`: Indicates how deep in a recursive loop the rendering currently is. Starts at level 1 //! - `loop.depth0`: Indicates how deep in a recursive loop the rendering currently is. Starts at level 0 +//! - `loop.changed(...args)`: Returns true if the passed values have changed since the last time it was called with the same arguments. +//! - `loop.cycle(...args)`: Returns a value from the passed sequence in a cycle. //! //! Within a for-loop, it’s possible to cycle among a list of strings/variables each time through -//! the loop by using the special loop.cycle helper: +//! the loop by using the special `loop.cycle` helper: //! //! ```jinja //! {% for row in rows %} @@ -205,6 +207,19 @@ //! {% endfor %} //! ``` //! +//! If the `sync` feature is not disabled, the `loop.changed` helper is also available +//! which can be used to detect when a value changes between the last iteration and the +//! current one. The method takes one or more arguments that are all compared. +//! +//! ```jinja +//! {% for entry in entries %} +//! {% if loop.changed(entry.category) %} +//!

{{ entry.category }}

+//! {% endif %} +//!

{{ entry.message }}

+//! {% endfor %} +//! ``` +//! //! Unlike in Rust or Python, it’s not possible to break or continue in a loop. You can, //! however, filter the sequence during iteration, which allows you to skip items. The //! following example skips all the users which are hidden: diff --git a/minijinja/src/vm.rs b/minijinja/src/vm.rs index 1c4b2c09..410743d9 100644 --- a/minijinja/src/vm.rs +++ b/minijinja/src/vm.rs @@ -1,6 +1,8 @@ use std::collections::{BTreeMap, HashSet}; use std::fmt::{self, Write}; use std::sync::atomic::{AtomicUsize, Ordering}; +#[cfg(feature = "sync")] +use std::sync::Mutex; use crate::environment::Environment; use crate::error::{Error, ErrorKind}; @@ -16,6 +18,8 @@ pub struct LoopState { len: usize, idx: AtomicUsize, depth: usize, + #[cfg(feature = "sync")] + last_changed_value: Mutex>>, } impl fmt::Debug for LoopState { @@ -68,6 +72,19 @@ impl Object for LoopState { } fn call_method(&self, _state: &State, name: &str, args: Vec) -> Result { + #[cfg(feature = "sync")] + { + if name == "changed" { + let mut last_changed_value = self.last_changed_value.lock().unwrap(); + let changed = last_changed_value.as_ref() != Some(&args); + if changed { + *last_changed_value = Some(args); + return Ok(Value::from(true)); + } + return Ok(Value::from(false)); + } + } + if name == "cycle" { let idx = self.idx.load(Ordering::Relaxed); match args.get(idx % args.len()) { @@ -767,6 +784,8 @@ impl<'env> Vm<'env> { idx: AtomicUsize::new(!0usize), len, depth, + #[cfg(feature = "sync")] + last_changed_value: Mutex::default(), }), }), ..Frame::default() diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs index 2d92b38b..7ebf9697 100644 --- a/minijinja/tests/test_templates.rs +++ b/minijinja/tests/test_templates.rs @@ -118,3 +118,17 @@ fn test_auto_escaping() { let rv = tmpl.render(context!(var => "foo\"bar'baz")).unwrap(); insta::assert_snapshot!(rv, @r###"foo"bar'baz"###); } + +#[cfg(feature = "sync")] +#[test] +fn test_loop_changed() { + let rv = minijinja::render!( + r#" + {%- for i in items -%} + {% if loop.changed(i) %}{{ i }}{% endif %} + {%- endfor -%} + "#, + items => vec![1, 1, 1, 2, 3, 4, 4, 5], + ); + assert_eq!(rv, "12345"); +}