Skip to content

Commit

Permalink
Add SemiStrict and improve Jinja2 compatibility (#687)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko authored Jan 29, 2025
1 parent b6dd665 commit 5a36842
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 48 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to MiniJinja are documented here.

## 2.8.0

- Added `SemiStrict` undefined mode that is like strict but allows
to be checked for truthiness. Additionally an if expression without
an else block will always produce a silent undefined object that
never errors for compatibility with Jinja2. #687

## 2.7.0

- Removed string interning. #675
Expand Down
7 changes: 5 additions & 2 deletions minijinja/src/compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::compiler::instructions::{
use crate::compiler::tokens::Span;
use crate::output::CaptureMode;
use crate::value::ops::neg;
use crate::value::{Kwargs, Value, ValueMap};
use crate::value::{Kwargs, Value, ValueMap, ValueRepr};

#[cfg(test)]
use similar_asserts::assert_eq;
Expand Down Expand Up @@ -660,7 +660,10 @@ impl<'source> CodeGenerator<'source> {
if let Some(ref false_expr) = i.false_expr {
self.compile_expr(false_expr);
} else {
self.add(Instruction::LoadConst(Value::UNDEFINED));
// special behavior: missing false block have a silent undefined
// to permit special casing. This is for compatibility also with
// what Jinja2 does.
self.add(Instruction::LoadConst(ValueRepr::SilentUndefined.into()));
}
self.end_if();
}
Expand Down
14 changes: 9 additions & 5 deletions minijinja/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::expression::Expression;
use crate::output::Output;
use crate::template::{CompiledTemplate, CompiledTemplateRef, Template, TemplateConfig};
use crate::utils::{AutoEscape, BTreeMapKeysDebug, UndefinedBehavior};
use crate::value::{FunctionArgs, FunctionResult, Value};
use crate::value::{FunctionArgs, FunctionResult, Value, ValueRepr};
use crate::vm::State;
use crate::{defaults, filters, functions, tests};

Expand Down Expand Up @@ -796,10 +796,14 @@ impl<'source> Environment<'source> {
state: &State,
out: &mut Output,
) -> Result<(), Error> {
if value.is_undefined() && matches!(self.undefined_behavior, UndefinedBehavior::Strict) {
Err(Error::from(ErrorKind::UndefinedError))
} else {
(self.formatter)(out, state, value)
match (self.undefined_behavior, &value.0) {
// this intentionally does not check for SilentUndefined. SilentUndefined is
// exclusively used in the missing else condition of an if expression to match
// Jinja2 behavior. Those go straight to the formatter.
(UndefinedBehavior::Strict | UndefinedBehavior::SemiStrict, &ValueRepr::Undefined) => {
Err(Error::from(ErrorKind::UndefinedError))
}
_ => (self.formatter)(out, state, value),
}
}

Expand Down
12 changes: 9 additions & 3 deletions minijinja/src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,9 @@ mod builtins {
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn int(value: &Value) -> Result<Value, Error> {
match &value.0 {
ValueRepr::Undefined | ValueRepr::None => Ok(Value::from(0)),
ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => {
Ok(Value::from(0))
}
ValueRepr::Bool(x) => Ok(Value::from(*x as u64)),
ValueRepr::U64(_) | ValueRepr::I64(_) | ValueRepr::U128(_) | ValueRepr::I128(_) => {
Ok(value.clone())
Expand Down Expand Up @@ -673,7 +675,9 @@ mod builtins {
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
pub fn float(value: &Value) -> Result<Value, Error> {
match &value.0 {
ValueRepr::Undefined | ValueRepr::None => Ok(Value::from(0.0)),
ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => {
Ok(Value::from(0.0))
}
ValueRepr::Bool(x) => Ok(Value::from(*x as u64 as f64)),
ValueRepr::String(..) | ValueRepr::SmallStr(_) => value
.as_str()
Expand Down Expand Up @@ -1190,7 +1194,9 @@ mod builtins {
Ok(rv)
} else {
match &value.0 {
ValueRepr::None | ValueRepr::Undefined => Ok("".into()),
ValueRepr::None | ValueRepr::Undefined | ValueRepr::SilentUndefined => {
Ok("".into())
}
ValueRepr::Bytes(b) => Ok(percent_encoding::percent_encode(b, SET).to_string()),
ValueRepr::String(..) | ValueRepr::SmallStr(_) => Ok(
percent_encoding::utf8_percent_encode(value.as_str().unwrap(), SET).to_string(),
Expand Down
2 changes: 1 addition & 1 deletion minijinja/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ mod builtins {
let mut rv = match value {
None => ValueMap::default(),
Some(value) => match value.0 {
ValueRepr::Undefined => ValueMap::default(),
ValueRepr::Undefined | ValueRepr::SilentUndefined => ValueMap::default(),
ValueRepr::Object(obj) if obj.repr() == ObjectRepr::Map => {
obj.try_iter_pairs().into_iter().flatten().collect()
}
Expand Down
9 changes: 9 additions & 0 deletions minijinja/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@
//! {{ title|upper if title }}
//! ```
//!
//! Note that for compatibility with Jinja2, when the `else` block is missing the undefined
//! value will be marked as "silent". This means even if strict undefined behavior is
//! requested, this undefined value will print to an empty string. This means
//! that this is always valid:
//!
//! ```jinja
//! {{ value if false }} -> prints an empty string (silent undefined returned from else)
//! ```
//!
//! # Tags
//!
//! Tags control logic in templates. The following tags exist:
Expand Down
48 changes: 31 additions & 17 deletions minijinja/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ pub enum AutoEscape {

/// Defines the behavior of undefined values in the engine.
///
/// At present there are three types of behaviors available which mirror the behaviors
/// that Jinja2 provides out of the box.
/// At present there are three types of behaviors available which mirror the
/// behaviors that Jinja2 provides out of the box and an extra option called
/// `SemiStrict` which is a slightly less strict undefined.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum UndefinedBehavior {
Expand All @@ -116,38 +117,47 @@ pub enum UndefinedBehavior {
/// * **printing:** allowed (returns empty string)
/// * **iteration:** allowed (returns empty array)
/// * **attribute access of undefined values:** fails
/// * **if true:** allowed (is considered false)
#[default]
Lenient,
/// Like `Lenient`, but also allows chaining of undefined lookups.
///
/// * **printing:** allowed (returns empty string)
/// * **iteration:** allowed (returns empty array)
/// * **attribute access of undefined values:** allowed (returns [`undefined`](Value::UNDEFINED))
/// * **if true:** allowed (is considered false)
Chainable,
/// Like strict, but does not error when the undefined is checked for truthyness.
///
/// * **printing:** fails
/// * **iteration:** fails
/// * **attribute access of undefined values:** fails
/// * **if true:** allowed (is considered false)
SemiStrict,
/// Complains very quickly about undefined values.
///
/// * **printing:** fails
/// * **iteration:** fails
/// * **attribute access of undefined values:** fails
/// * **if true:** fails
Strict,
}

impl UndefinedBehavior {
/// Utility method used in the engine to determine what to do when an undefined is
/// encountered.
///
/// The flag indicates if this is the first or second level of undefined value. If
/// `parent_was_undefined` is set to `true`, the undefined was created by looking up
/// a missing attribute on an undefined value. If `false` the undefined was created by
/// looking up a missing attribute on a defined value.
/// The flag indicates if this is the first or second level of undefined value. The
/// parent value is passed too.
pub(crate) fn handle_undefined(self, parent_was_undefined: bool) -> Result<Value, Error> {
match (self, parent_was_undefined) {
(UndefinedBehavior::Lenient, false)
| (UndefinedBehavior::Strict, false)
| (UndefinedBehavior::SemiStrict, false)
| (UndefinedBehavior::Chainable, _) => Ok(Value::UNDEFINED),
(UndefinedBehavior::Lenient, true) | (UndefinedBehavior::Strict, true) => {
Err(Error::from(ErrorKind::UndefinedError))
}
(UndefinedBehavior::Lenient, true)
| (UndefinedBehavior::Strict, true)
| (UndefinedBehavior::SemiStrict, true) => Err(Error::from(ErrorKind::UndefinedError)),
}
}

Expand All @@ -156,10 +166,12 @@ impl UndefinedBehavior {
/// This fails only for strict undefined values.
#[inline]
pub(crate) fn is_true(self, value: &Value) -> Result<bool, Error> {
if matches!(self, UndefinedBehavior::Strict) && value.is_undefined() {
Err(Error::from(ErrorKind::UndefinedError))
} else {
Ok(value.is_true())
match (self, &value.0) {
// silent undefined doesn't error, even in strict mode
(UndefinedBehavior::Strict, &ValueRepr::Undefined) => {
Err(Error::from(ErrorKind::UndefinedError))
}
_ => Ok(value.is_true()),
}
}

Expand All @@ -176,10 +188,12 @@ impl UndefinedBehavior {
/// Are we strict on iteration?
#[inline]
pub(crate) fn assert_iterable(self, value: &Value) -> Result<(), Error> {
if matches!(self, UndefinedBehavior::Strict) && value.is_undefined() {
Err(Error::from(ErrorKind::UndefinedError))
} else {
Ok(())
match (self, &value.0) {
// silent undefined doesn't error, even in strict mode
(UndefinedBehavior::Strict | UndefinedBehavior::SemiStrict, &ValueRepr::Undefined) => {
Err(Error::from(ErrorKind::UndefinedError))
}
_ => Ok(()),
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion minijinja/src/value/argtypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,9 @@ impl TryFrom<Value> for Kwargs {

fn try_from(value: Value) -> Result<Self, Self::Error> {
match value.0 {
ValueRepr::Undefined => Ok(Kwargs::new(Default::default())),
ValueRepr::Undefined | ValueRepr::SilentUndefined => {
Ok(Kwargs::new(Default::default()))
}
ValueRepr::Object(_) => {
Kwargs::extract(&value).ok_or_else(|| Error::from(ErrorKind::InvalidOperation))
}
Expand Down
10 changes: 7 additions & 3 deletions minijinja/src/value/deserialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ impl<'de> Deserializer<'de> for Value {
ValueRepr::F64(v) => visitor.visit_f64(v),
ValueRepr::String(ref v, _) => visitor.visit_str(v),
ValueRepr::SmallStr(v) => visitor.visit_str(v.as_str()),
ValueRepr::Undefined | ValueRepr::None => visitor.visit_unit(),
ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => {
visitor.visit_unit()
}
ValueRepr::Bytes(ref v) => visitor.visit_bytes(v),
ValueRepr::Object(o) => match o.repr() {
ObjectRepr::Plain => Err(de::Error::custom("cannot deserialize plain objects")),
Expand All @@ -197,7 +199,9 @@ impl<'de> Deserializer<'de> for Value {

fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
match self.0 {
ValueRepr::None | ValueRepr::Undefined => visitor.visit_unit(),
ValueRepr::None | ValueRepr::Undefined | ValueRepr::SilentUndefined => {
visitor.visit_unit()
}
_ => visitor.visit_some(self),
}
}
Expand Down Expand Up @@ -405,7 +409,7 @@ impl de::Error for Error {

fn value_to_unexpected(value: &Value) -> Unexpected {
match value.0 {
ValueRepr::Undefined | ValueRepr::None => Unexpected::Unit,
ValueRepr::Undefined | ValueRepr::SilentUndefined | ValueRepr::None => Unexpected::Unit,
ValueRepr::Bool(val) => Unexpected::Bool(val),
ValueRepr::U64(val) => Unexpected::Unsigned(val),
ValueRepr::I64(val) => Unexpected::Signed(val),
Expand Down
Loading

0 comments on commit 5a36842

Please sign in to comment.