Repository: mitsuhiko/minijinja
Pull request #57 (2022-03-04 22:12:27)

Added support for choices to include
This lets the `include` block try one of multiple choices like in Jinja2.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9bd69f8..c673ef7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to MiniJinja are documented here.
 - Added `bool` filter.
 - Added `meta` API. (#55)
 - Added support for `ignore missing` in include tags. (#56)
+- Added support for choices in include tags. (#57)
 
 # 0.13.0
 
diff --git a/minijinja/src/syntax.rs b/minijinja/src/syntax.rs
index e1ccd23..d8d0be8 100644
--- a/minijinja/src/syntax.rs
+++ b/minijinja/src/syntax.rs
@@ -371,6 +371,16 @@
 //! ```jinja
 //! {% include 'customization.html' ignore missing %}
 //! ```
+//!
+//! You can also provide a list of templates that are checked for existence
+//! before inclusion. The first template that exists will be included. If `ignore
+//! missing` is set, it will fall back to rendering nothing if none of the
+//! templates exist, otherwise it will fail with an error.
+//!
+//! ```jinja
+//! {% include ['page_detailed.html', 'page.html'] %}
+//! {% include ['special_sidebar.html', 'sidebar.html'] ignore missing %}
+//! ```
 //!  
 //! Included templates have access to the variables of the active context.
 //!
diff --git a/minijinja/src/vm.rs b/minijinja/src/vm.rs
index 8e7da1d..dde252a 100644
--- a/minijinja/src/vm.rs
+++ b/minijinja/src/vm.rs
@@ -9,7 +9,7 @@ use crate::instructions::{
 };
 use crate::key::Key;
 use crate::utils::matches;
-use crate::value::{self, Object, RcType, Value, ValueIterator};
+use crate::value::{self, Object, RcType, Value, ValueIterator, ValueRepr};
 use crate::AutoEscape;
 
 pub struct LoopState {
@@ -803,34 +803,64 @@ impl<'env> Vm<'env> {
                 }
                 Instruction::Include(ignore_missing) => {
                     let name = stack.pop();
-                    let name = try_ctx!(name.as_str().ok_or_else(|| {
-                        Error::new(
-                            ErrorKind::ImpossibleOperation,
-                            "template name was not a string",
-                        )
-                    }));
-                    let tmpl = match self.env.get_template(name) {
-                        Ok(tmpl) => tmpl,
-                        Err(err) => {
-                            if *ignore_missing {
-                                pc += 1;
+                    let choices = if let ValueRepr::Seq(ref choices) = name.0 {
+                        &choices[..]
+                    } else {
+                        std::slice::from_ref(&name)
+                    };
+                    let mut templates_tried = vec![];
+                    for name in choices {
+                        let name = try_ctx!(name.as_str().ok_or_else(|| {
+                            Error::new(
+                                ErrorKind::ImpossibleOperation,
+                                "template name was not a string",
+                            )
+                        }));
+                        let tmpl = match self.env.get_template(name) {
+                            Ok(tmpl) => tmpl,
+                            Err(err) => {
+                                if err.kind() == ErrorKind::TemplateNotFound {
+                                    templates_tried.push(name);
+                                } else {
+                                    bail!(err);
+                                }
                                 continue;
-                            } else {
-                                bail!(err);
                             }
+                        };
+                        let instructions = tmpl.instructions();
+                        let mut referenced_blocks = BTreeMap::new();
+                        for (&name, instr) in tmpl.blocks().iter() {
+                            referenced_blocks.insert(name, vec![instr]);
+                        }
+                        sub_eval!(
+                            instructions,
+                            referenced_blocks,
+                            None,
+                            tmpl.initial_auto_escape()
+                        );
+                        templates_tried.clear();
+                        break;
+                    }
+
+                    if !templates_tried.is_empty() && !*ignore_missing {
+                        if templates_tried.len() == 1 {
+                            bail!(Error::new(
+                                ErrorKind::TemplateNotFound,
+                                format!(
+                                    "tried to include non-existing template {:?}",
+                                    templates_tried[0]
+                                )
+                            ));
+                        } else {
+                            bail!(Error::new(
+                                ErrorKind::TemplateNotFound,
+                                format!(
+                                    "tried to include one of multiple templates, none of which existed {:?}",
+                                    templates_tried
+                                )
+                            ));
                         }
-                    };
-                    let instructions = tmpl.instructions();
-                    let mut referenced_blocks = BTreeMap::new();
-                    for (&name, instr) in tmpl.blocks().iter() {
-                        referenced_blocks.insert(name, vec![instr]);
                     }
-                    sub_eval!(
-                        instructions,
-                        referenced_blocks,
-                        None,
-                        tmpl.initial_auto_escape()
-                    );
                 }
                 Instruction::PushAutoEscape => {
                     let value = stack.pop();


Test patch:
diff --git a/minijinja/tests/inputs/include_choice_none.txt b/minijinja/tests/inputs/include_choice_none.txt
new file mode 100644
index 0000000..a44dcbd
--- /dev/null
+++ b/minijinja/tests/inputs/include_choice_none.txt
@@ -0,0 +1,5 @@
+{}
+---
+Before
+{% include ["missing_template1.txt", "missing_template2.txt"] %}
+After
diff --git a/minijinja/tests/inputs/include_ignore_choice.txt b/minijinja/tests/inputs/include_ignore_choice.txt
new file mode 100644
index 0000000..7047dae
--- /dev/null
+++ b/minijinja/tests/inputs/include_ignore_choice.txt
@@ -0,0 +1,5 @@
+template: "simple_include.txt"
+---
+Before
+{% include ["missing_template.txt", template] %}
+After
diff --git a/minijinja/tests/inputs/include_ignore_missing.txt b/minijinja/tests/inputs/include_ignore_missing.txt
new file mode 100644
index 0000000..6635e1b
--- /dev/null
+++ b/minijinja/tests/inputs/include_ignore_missing.txt
@@ -0,0 +1,5 @@
+template: "missing_template.txt"
+---
+Before
+{% include template ignore missing %}
+After
diff --git a/minijinja/tests/inputs/include_missing.txt b/minijinja/tests/inputs/include_missing.txt
new file mode 100644
index 0000000..eeaa82c
--- /dev/null
+++ b/minijinja/tests/inputs/include_missing.txt
@@ -0,0 +1,5 @@
+template: "missing_template.txt"
+---
+Before
+{% include template %}
+After
diff --git a/minijinja/tests/snapshots/test_lexer__lexer@not_a_variable.txt.snap b/minijinja/tests/snapshots/test_lexer__lexer@not_a_variable.txt.snap
deleted file mode 100644
index 4a0998c..0000000
--- a/minijinja/tests/snapshots/test_lexer__lexer@not_a_variable.txt.snap
+++ /dev/null
@@ -1,9 +0,0 @@
----
-source: tests/test_lexer.rs
-expression: "&tokens"
-input_file: tests/lexer-inputs/not_a_variable.txt
-
----
-[
-    TEMPLATE_DATA("{\"foo\":\"bar\"}\n"),
-]
diff --git a/minijinja/tests/snapshots/test_templates__vm@include_choice_none.txt.snap b/minijinja/tests/snapshots/test_templates__vm@include_choice_none.txt.snap
new file mode 100644
index 0000000..757e9fb
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@include_choice_none.txt.snap
@@ -0,0 +1,12 @@
+---
+source: minijinja/tests/test_templates.rs
+assertion_line: 44
+expression: "&rendered"
+input_file: minijinja/tests/inputs/include_choice_none.txt
+
+---
+!!!ERROR!!!
+
+Error { kind: TemplateNotFound, detail: Some("tried to include one of multiple templates, none of which existed [\"missing_template1.txt\", \"missing_template2.txt\"]"), name: Some("include_choice_none.txt"), lineno: 2, source: None }
+
+
diff --git a/minijinja/tests/snapshots/test_templates__vm@include_ignore_choice.txt.snap b/minijinja/tests/snapshots/test_templates__vm@include_ignore_choice.txt.snap
new file mode 100644
index 0000000..79fc40c
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@include_ignore_choice.txt.snap
@@ -0,0 +1,11 @@
+---
+source: minijinja/tests/test_templates.rs
+assertion_line: 44
+expression: "&rendered"
+input_file: minijinja/tests/inputs/include_ignore_choice.txt
+
+---
+Before
+Hello  from included template!
+After
+
diff --git a/minijinja/tests/snapshots/test_templates__vm@include_ignore_missing.txt.snap b/minijinja/tests/snapshots/test_templates__vm@include_ignore_missing.txt.snap
new file mode 100644
index 0000000..2cbfdbd
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@include_ignore_missing.txt.snap
@@ -0,0 +1,11 @@
+---
+source: minijinja/tests/test_templates.rs
+assertion_line: 44
+expression: "&rendered"
+input_file: minijinja/tests/inputs/include_ignore_missing.txt
+
+---
+Before
+
+After
+
diff --git a/minijinja/tests/snapshots/test_templates__vm@include_missing.txt.snap b/minijinja/tests/snapshots/test_templates__vm@include_missing.txt.snap
new file mode 100644
index 0000000..e248e49
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@include_missing.txt.snap
@@ -0,0 +1,12 @@
+---
+source: minijinja/tests/test_templates.rs
+assertion_line: 44
+expression: "&rendered"
+input_file: minijinja/tests/inputs/include_missing.txt
+
+---
+!!!ERROR!!!
+
+Error { kind: TemplateNotFound, detail: Some("tried to include non-existing template \"missing_template.txt\""), name: Some("include_missing.txt"), lineno: 2, source: None }
+
+