diff --git a/CHANGELOG.md b/CHANGELOG.md index d192a3f9..4db3a8a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to MiniJinja are documented here. +## 2.0.2 + +- Implemented sequence (+ some iterator) and string repeating with the `*` + operator to match Jinja2 behavior. #519 + ## 2.0.1 - Fixed an issue that caused custom delimiters to not work in the Python diff --git a/Makefile b/Makefile index 3090dec9..20c41b8a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ DOC_FEATURES=loader,json,urlencode,custom_syntax,fuel -TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax +TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax,deserialization .PHONY: all all: test diff --git a/minijinja/src/value/ops.rs b/minijinja/src/value/ops.rs index 62eff79a..eab8298e 100644 --- a/minijinja/src/value/ops.rs +++ b/minijinja/src/value/ops.rs @@ -1,5 +1,5 @@ use crate::error::{Error, ErrorKind}; -use crate::value::{ObjectRepr, Value, ValueKind, ValueRepr}; +use crate::value::{DynObject, ObjectRepr, Value, ValueKind, ValueRepr}; const MIN_I128_AS_POS_U128: u128 = 170141183460469231731687303715884105728; @@ -204,9 +204,93 @@ pub fn add(lhs: &Value, rhs: &Value) -> Result { } math_binop!(sub, checked_sub, -); -math_binop!(mul, checked_mul, *); math_binop!(rem, checked_rem_euclid, %); +pub fn mul(lhs: &Value, rhs: &Value) -> Result { + if let Some((s, n)) = lhs + .as_str() + .map(|s| (s, rhs)) + .or_else(|| rhs.as_str().map(|s| (s, lhs))) + { + return Ok(Value::from(s.repeat(ok!(n.as_usize().ok_or_else(|| { + Error::new( + ErrorKind::InvalidOperation, + "strings can only be multiplied with integers", + ) + }))))); + } else if let Some((seq, n)) = lhs + .as_object() + .map(|s| (s, rhs)) + .or_else(|| rhs.as_object().map(|s| (s, lhs))) + .filter(|x| matches!(x.0.repr(), ObjectRepr::Iterable | ObjectRepr::Seq)) + { + return repeat_iterable(n, seq); + } + + match coerce(lhs, rhs) { + Some(CoerceResult::I128(a, b)) => match a.checked_mul(b) { + Some(val) => Ok(int_as_value(val)), + None => Err(failed_op(stringify!(*), lhs, rhs)), + }, + Some(CoerceResult::F64(a, b)) => Ok((a * b).into()), + _ => Err(impossible_op(stringify!(*), lhs, rhs)), + } +} + +fn repeat_iterable(n: &Value, seq: &DynObject) -> Result { + struct LenIterWrap(usize, I); + + impl + Send + Sync> Iterator for LenIterWrap { + type Item = Value; + + #[inline(always)] + fn next(&mut self) -> Option { + self.1.next() + } + + #[inline(always)] + fn size_hint(&self) -> (usize, Option) { + (self.0, Some(self.0)) + } + } + + let n = ok!(n.as_usize().ok_or_else(|| { + Error::new( + ErrorKind::InvalidOperation, + "sequences and iterables can only be multiplied with integers", + ) + })); + + let len = ok!(seq.enumerator_len().ok_or_else(|| { + Error::new( + ErrorKind::InvalidOperation, + "cannot repeat unsized iterables", + ) + })); + + // This is not optimal. We only query the enumerator for the length once + // but we support repeated iteration. We could both lie about our length + // here and we could actually deal with an object that changes how much + // data it returns. This is not really permissible so we won't try to + // improve on this here. + Ok(Value::make_object_iterable(seq.clone(), move |seq| { + Box::new(LenIterWrap( + len * n, + (0..n).flat_map(move |_| { + seq.try_iter().unwrap_or_else(|| { + Box::new( + std::iter::repeat(Value::from(Error::new( + ErrorKind::InvalidOperation, + "iterable did not iterate against expectations", + ))) + .take(len), + ) + }) + }), + )) + })) +} + pub fn div(lhs: &Value, rhs: &Value) -> Result { fn do_it(lhs: &Value, rhs: &Value) -> Option { let a = some!(as_f64(lhs)); diff --git a/minijinja/tests/inputs/err_repeat_iterator_with_object.txt b/minijinja/tests/inputs/err_repeat_iterator_with_object.txt new file mode 100644 index 00000000..31211c16 --- /dev/null +++ b/minijinja/tests/inputs/err_repeat_iterator_with_object.txt @@ -0,0 +1,4 @@ +{ +} +--- +{{ [1, 2, 3] * {} }} diff --git a/minijinja/tests/inputs/err_repeat_iterator_with_string.txt b/minijinja/tests/inputs/err_repeat_iterator_with_string.txt new file mode 100644 index 00000000..1ee5840b --- /dev/null +++ b/minijinja/tests/inputs/err_repeat_iterator_with_string.txt @@ -0,0 +1,4 @@ +{ +} +--- +{{ [1, 2, 3] * "3" }} diff --git a/minijinja/tests/inputs/err_repeat_true_iterator.txt b/minijinja/tests/inputs/err_repeat_true_iterator.txt new file mode 100644 index 00000000..01cec6e6 --- /dev/null +++ b/minijinja/tests/inputs/err_repeat_true_iterator.txt @@ -0,0 +1,4 @@ +{ +} +--- +{{ one_shot_iterator * 3 }} diff --git a/minijinja/tests/inputs/mul.txt b/minijinja/tests/inputs/mul.txt new file mode 100644 index 00000000..1763f271 --- /dev/null +++ b/minijinja/tests/inputs/mul.txt @@ -0,0 +1,9 @@ +{ +} +--- +{{ range(3) * 3 }} +{{ "foo" * 3 }} +{{ [1, 2] * 2 }} +{{ [1, 2, 3, 4][:2] * 2 }} +{{ 2 * [1, 2] }} +{{ 2 * "foo" }} diff --git a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap index 85b7daea..be74d65a 100644 --- a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap +++ b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap @@ -19,6 +19,7 @@ State { depth: 0, }, "f": minijinja::functions::builtins::range, + "one_shot_iterator": , "upper": 1, }, env: Environment { diff --git a/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_object.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_object.txt.snap new file mode 100644 index 00000000..ab17664e --- /dev/null +++ b/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_object.txt.snap @@ -0,0 +1,22 @@ +--- +source: minijinja/tests/test_templates.rs +description: "{{ [1, 2, 3] * {} }}" +info: {} +input_file: minijinja/tests/inputs/err_repeat_iterator_with_object.txt +--- +!!!ERROR!!! + +Error { + kind: InvalidOperation, + detail: "sequences and iterables can only be multiplied with integers", + name: "err_repeat_iterator_with_object.txt", + line: 1, +} + +invalid operation: sequences and iterables can only be multiplied with integers (in err_repeat_iterator_with_object.txt:1) +--------------------- err_repeat_iterator_with_object.txt --------------------- + 1 > {{ [1, 2, 3] * {} }} + i ^^^^^^^^^^^^^^ invalid operation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +No referenced variables +------------------------------------------------------------------------------- diff --git a/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_string.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_string.txt.snap new file mode 100644 index 00000000..edddc19e --- /dev/null +++ b/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_string.txt.snap @@ -0,0 +1,22 @@ +--- +source: minijinja/tests/test_templates.rs +description: "{{ [1, 2, 3] * \"3\" }}" +info: {} +input_file: minijinja/tests/inputs/err_repeat_iterator_with_string.txt +--- +!!!ERROR!!! + +Error { + kind: InvalidOperation, + detail: "strings can only be multiplied with integers", + name: "err_repeat_iterator_with_string.txt", + line: 1, +} + +invalid operation: strings can only be multiplied with integers (in err_repeat_iterator_with_string.txt:1) +--------------------- err_repeat_iterator_with_string.txt --------------------- + 1 > {{ [1, 2, 3] * "3" }} + i ^^^^^^^^^^^^^^^ invalid operation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +No referenced variables +------------------------------------------------------------------------------- diff --git a/minijinja/tests/snapshots/test_templates__vm@err_repeat_true_iterator.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_repeat_true_iterator.txt.snap new file mode 100644 index 00000000..52dc281d --- /dev/null +++ b/minijinja/tests/snapshots/test_templates__vm@err_repeat_true_iterator.txt.snap @@ -0,0 +1,24 @@ +--- +source: minijinja/tests/test_templates.rs +description: "{{ one_shot_iterator * 3 }}" +info: {} +input_file: minijinja/tests/inputs/err_repeat_true_iterator.txt +--- +!!!ERROR!!! + +Error { + kind: InvalidOperation, + detail: "cannot repeat unsized iterables", + name: "err_repeat_true_iterator.txt", + line: 1, +} + +invalid operation: cannot repeat unsized iterables (in err_repeat_true_iterator.txt:1) +------------------------ err_repeat_true_iterator.txt ------------------------- + 1 > {{ one_shot_iterator * 3 }} + i ^^^^^^^^^^^^^^^^^^^^^ invalid operation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Referenced variables: { + one_shot_iterator: , +} +------------------------------------------------------------------------------- diff --git a/minijinja/tests/snapshots/test_templates__vm@mul.txt.snap b/minijinja/tests/snapshots/test_templates__vm@mul.txt.snap new file mode 100644 index 00000000..ede04b17 --- /dev/null +++ b/minijinja/tests/snapshots/test_templates__vm@mul.txt.snap @@ -0,0 +1,12 @@ +--- +source: minijinja/tests/test_templates.rs +description: "{{ range(3) * 3 }}\n{{ \"foo\" * 3 }}\n{{ [1, 2] * 2 }}\n{{ [1, 2, 3, 4][:2] * 2 }}\n{{ 2 * [1, 2] }}\n{{ 2 * \"foo\" }}" +info: {} +input_file: minijinja/tests/inputs/mul.txt +--- +[0, 1, 2, 0, 1, 2, 0, 1, 2] +foofoofoo +[1, 2, 1, 2] +[1, 2, 1, 2] +[1, 2, 1, 2] +foofoo diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs index 8e6fefed..9b387117 100644 --- a/minijinja/tests/test_templates.rs +++ b/minijinja/tests/test_templates.rs @@ -2,7 +2,8 @@ feature = "multi_template", feature = "macros", feature = "builtins", - feature = "adjacent_loop_items" + feature = "adjacent_loop_items", + feature = "deserialization" ))] use std::collections::BTreeMap; use std::fmt::Write; @@ -35,7 +36,7 @@ fn test_vm() { let contents = std::fs::read_to_string(path).unwrap(); let mut iter = contents.splitn(2, "\n---\n"); let mut env = Environment::new(); - let ctx: serde_json::Value = serde_json::from_str(iter.next().unwrap()).unwrap(); + let ctx: Value = serde_json::from_str(iter.next().unwrap()).unwrap(); for (path, source) in &refs { let ref_filename = path.file_name().unwrap().to_str().unwrap(); @@ -50,7 +51,11 @@ fn test_vm() { } else { let template = env.get_template(filename).unwrap(); - match template.render(&ctx) { + let actual_context = context! { + one_shot_iterator => Value::make_one_shot_iterator(0..3), + ..ctx.clone() + }; + match template.render(&actual_context) { Ok(mut rendered) => { rendered.push('\n'); rendered