Skip to content

Commit

Permalink
Add loop.changed() (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Sep 2, 2022
1 parent d45e599 commit 82c53bc
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 2 deletions.
3 changes: 2 additions & 1 deletion minijinja/src/lib.rs
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion minijinja/src/syntax.rs
Expand Up @@ -195,16 +195,31 @@
//! - `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 %}
//! <li class="{{ loop.cycle('odd', 'even') }}">{{ row }}</li>
//! {% 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) %}
//! <h2>{{ entry.category }}</h2>
//! {% endif %}
//! <p>{{ entry.message }}</p>
//! {% 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:
Expand Down
19 changes: 19 additions & 0 deletions 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};
Expand All @@ -16,6 +18,8 @@ pub struct LoopState {
len: usize,
idx: AtomicUsize,
depth: usize,
#[cfg(feature = "sync")]
last_changed_value: Mutex<Option<Vec<Value>>>,
}

impl fmt::Debug for LoopState {
Expand Down Expand Up @@ -68,6 +72,19 @@ impl Object for LoopState {
}

fn call_method(&self, _state: &State, name: &str, args: Vec<Value>) -> Result<Value, Error> {
#[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()) {
Expand Down Expand Up @@ -767,6 +784,8 @@ impl<'env> Vm<'env> {
idx: AtomicUsize::new(!0usize),
len,
depth,
#[cfg(feature = "sync")]
last_changed_value: Mutex::default(),
}),
}),
..Frame::default()
Expand Down
14 changes: 14 additions & 0 deletions minijinja/tests/test_templates.rs
Expand Up @@ -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");
}

0 comments on commit 82c53bc

Please sign in to comment.