Repository: Keats/tera (rust)

Issue:
Add support for Django's {% empty %} in for-loops
I am missing support for an `{% empty %}` statement in for-loops. I know the [`{% empty %}` statement](https://docs.djangoproject.com/en/2.1/ref/templates/builtins/#for-empty) from Django's templating language:

> The for tag can take an optional {% empty %} clause whose text is displayed if the given array is empty or could not be found:
> 
> ```
> <ul>
> {% for athlete in athlete_list %}
>     <li>{{ athlete.name }}</li>
> {% empty %}
>     <li>Sorry, no athletes in this list.</li>
> {% endfor %}
> </ul>
> ```

Pull request description:
Implemented feature from issue #395 ("else" case in for loops)
Fixes #395 

I've implemented the functionality requested in issue #395 along with some test cases that cover the code.

I've made the following decisions that I'd like some feedback on:

### Renamed {% empty %} -> {% else %}
I've taken the liberty of naming the fail-case "else" instead of "empty" since I feel like it better conveys the control flow, "empty" feels a little ambiguous, at least as someone not familiar with Django. 

### Silently render _else_ case if iterating over undefined variable
As requested, this functionality also covers the case where you attempt to iterate over an undefined variable, defaulting to the _else_ block, and throwing an error if no _else_ block is defined.

This will fail to compile:
```
{% for a in undefined_variable %}
    {{a}}
{% endfor %}
```
and yield the error:  `Tried to iterate undefined container 'undefined_variable'. To treat undefined containers the same as empty ones, explicitly define an {% else %} block within the for loop`


This will render "hello world"
```
{% for a in undefined_variable %}
    {{a}}
{% else %}
    hello world
{% endfor %}
```

I'm unsure of whether this is a good compromise, and if the "undefined" case should even be covered by the _else_ case, or if additional checks by the user should be required in that case.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 0183f32..93eb0b9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,7 +5,7 @@ matrix:
   include:
     # The earliest stable Rust version that works
     - env: TARGET=x86_64-unknown-linux-gnu
-      rust: 1.31.0
+      rust: 1.32.0
     - env: TARGET=x86_64-unknown-linux-gnu
       rust: beta
     - env: TARGET=x86_64-unknown-linux-gnu
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9928f37..670dcda 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
 
 ### Breaking changes
 
-- Now requires Rust 1.31
+- Now requires Rust 1.32
 - Removed error-chain errors and added rich Error enum instead
 - Filter, Tester and Function are now traits and now take borrowed values instead of owned
 - Updated for 2018 edition
diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md
index 2234044..54ea4e5 100644
--- a/docs/content/docs/_index.md
+++ b/docs/content/docs/_index.md
@@ -981,7 +981,7 @@ Escapes a string's HTML. Specifically, it makes these replacements:
 - `>` is converted to `&gt;`
 - `"` (double quote) is converted to `&quot;`
 - `'` (single quote) is converted to `&#x27;`
-- `/` is converted to `&#x27;`
+- `/` is converted to `&#x2F;`
 - `` ` `` is converted to `&#96;`
 
 #### safe
diff --git a/src/parser/ast.rs b/src/parser/ast.rs
index ccffc78..b2317f2 100644
--- a/src/parser/ast.rs
+++ b/src/parser/ast.rs
@@ -295,6 +295,8 @@ pub struct Forloop {
     pub container: Expr,
     /// What's in the forloop itself
     pub body: Vec<Node>,
+    /// The body to execute in case of an empty object
+    pub empty_body: Option<Vec<Node>>,
 }
 
 /// An if/elif/else condition with their respective body
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index f621c97..7d456da 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -748,6 +748,7 @@ fn parse_forloop(pair: Pair<Rule>) -> TeraResult<Node> {
     let mut value = None;
     let mut container = None;
     let mut body = vec![];
+    let mut empty_body: Option<Vec<Node>> = None;
 
     for p in pair.into_inner() {
         match p.as_rule() {
@@ -778,7 +779,13 @@ fn parse_forloop(pair: Pair<Rule>) -> TeraResult<Node> {
             | Rule::block_content
             | Rule::filter_section_content
             | Rule::for_content => {
-                body.extend(parse_content(p)?);
+                match empty_body {
+                    Some(ref mut empty_body) => empty_body.extend(parse_content(p)?),
+                    None => body.extend(parse_content(p)?),
+                };
+            }
+            Rule::else_tag => {
+                empty_body = Some(vec![]);
             }
             Rule::endfor_tag => {
                 for p2 in p.into_inner() {
@@ -796,7 +803,7 @@ fn parse_forloop(pair: Pair<Rule>) -> TeraResult<Node> {
 
     Ok(Node::Forloop(
         start_ws,
-        Forloop { key, value: value.unwrap(), container: container.unwrap(), body },
+        Forloop { key, value: value.unwrap(), container: container.unwrap(), body, empty_body },
         end_ws,
     ))
 }
diff --git a/src/parser/tera.pest b/src/parser/tera.pest
index 31f0462..f597e8c 100644
--- a/src/parser/tera.pest
+++ b/src/parser/tera.pest
@@ -197,7 +197,7 @@ raw        = !{ raw_tag ~ raw_text ~ endraw_tag }
 
 filter_section = !{ filter_tag ~ filter_section_content* ~ endfilter_tag }
 
-forloop = ${ for_tag ~ for_content* ~ endfor_tag }
+forloop = ${ for_tag ~ for_content* ~ (else_tag ~ for_content*)* ~ endfor_tag }
 
 macro_if          = ${ if_tag ~ macro_content* ~ (elif_tag ~ macro_content*)* ~ (else_tag ~ macro_content*)? ~ endif_tag }
 block_if          = ${ if_tag ~ block_content* ~ (elif_tag ~ block_content*)* ~ (else_tag ~ block_content*)? ~ endif_tag }
diff --git a/src/renderer/processor.rs b/src/renderer/processor.rs
index 852d5fe..beb9ea6 100644
--- a/src/renderer/processor.rs
+++ b/src/renderer/processor.rs
@@ -168,10 +168,11 @@ impl<'a> Processor<'a> {
             ))),
         };
 
-        let container_val = self.safe_eval_expression(&for_loop.container)?;
-
         let for_loop_name = &for_loop.value;
         let for_loop_body = &for_loop.body;
+        let for_loop_empty_body = &for_loop.empty_body;
+
+        let container_val = self.safe_eval_expression(&for_loop.container)?;
 
         let for_loop = match *container_val {
             Value::Array(_) => {
@@ -210,22 +211,28 @@ impl<'a> Processor<'a> {
         };
 
         let len = for_loop.len();
-        self.call_stack.push_for_loop_frame(for_loop_name, for_loop);
+        match (len, for_loop_empty_body) {
+            (0, Some(empty_body)) => Ok(self.render_body(&empty_body)?),
+            (0, _) => Ok("".to_string()),
+            (_, _) => {
+                self.call_stack.push_for_loop_frame(for_loop_name, for_loop);
+
+                let mut output = String::with_capacity(len * 20);
+                for _ in 0..len {
+                    output.push_str(&self.render_body(&for_loop_body)?);
+
+                    if self.call_stack.should_break_for_loop() {
+                        break;
+                    }
 
-        let mut output = String::with_capacity(len * 20);
-        for _ in 0..len {
-            output.push_str(&self.render_body(&for_loop_body)?);
+                    self.call_stack.increment_for_loop()?;
+                }
 
-            if self.call_stack.should_break_for_loop() {
-                break;
-            }
+                self.call_stack.pop();
 
-            self.call_stack.increment_for_loop()?;
+                Ok(output)
+            }
         }
-
-        self.call_stack.pop();
-
-        Ok(output)
     }
 
     fn render_if_node(self: &mut Self, if_node: &'a If) -> Result<String> {
diff --git a/src/sort_utils.rs b/src/sort_utils.rs
index a67cae7..dc30a8b 100644
--- a/src/sort_utils.rs
+++ b/src/sort_utils.rs
@@ -98,7 +98,7 @@ impl<K: GetSortKey> SortStrategy for SortPairs<K> {
     }
 }
 
-pub fn get_sort_strategy_for_type(ty: &Value) -> Result<Box<SortStrategy>> {
+pub fn get_sort_strategy_for_type(ty: &Value) -> Result<Box<dyn SortStrategy>> {
     use crate::Value::*;
     match *ty {
         Null => Err(Error::msg("Null is not a sortable value")),


Test patch:
diff --git a/src/parser/tests/parser.rs b/src/parser/tests/parser.rs
index 504e80d..f9514fc 100644
--- a/src/parser/tests/parser.rs
+++ b/src/parser/tests/parser.rs
@@ -639,6 +639,7 @@ fn parse_value_forloop() {
                     vec![FunctionCall { name: "reverse".to_string(), args: HashMap::new() },],
                 ),
                 body: vec![Node::Text("A".to_string())],
+                empty_body: None,
             },
             end_ws,
         )
@@ -664,6 +665,7 @@ fn parse_key_value_forloop() {
                     args: HashMap::new(),
                 },)),
                 body: vec![Node::Text("A".to_string())],
+                empty_body: None,
             },
             end_ws,
         )
@@ -689,6 +691,33 @@ fn parse_value_forloop_array() {
                     Expr::new(ExprVal::Int(2)),
                 ])),
                 body: vec![Node::Text("A".to_string())],
+                empty_body: None,
+            },
+            end_ws,
+        )
+    );
+}
+
+#[test]
+fn parse_value_forloop_empty() {
+    let ast = parse("{% for item in [1,2,] %}A{% else %}B{%- endfor %}").unwrap();
+    let start_ws = WS::default();
+    let mut end_ws = WS::default();
+    end_ws.left = true;
+
+    assert_eq!(
+        ast[0],
+        Node::Forloop(
+            start_ws,
+            Forloop {
+                key: None,
+                value: "item".to_string(),
+                container: Expr::new(ExprVal::Array(vec![
+                    Expr::new(ExprVal::Int(1)),
+                    Expr::new(ExprVal::Int(2)),
+                ])),
+                body: vec![Node::Text("A".to_string())],
+                empty_body: Some(vec![Node::Text("B".to_string())]),
             },
             end_ws,
         )
@@ -748,6 +777,7 @@ fn parse_break() {
                 value: "item".to_string(),
                 container: Expr::new(ExprVal::Ident("items".to_string())),
                 body: vec![Node::Break(WS { left: false, right: true }),],
+                empty_body: None,
             },
             for_ws,
         )
@@ -767,6 +797,7 @@ fn parse_continue() {
                 value: "item".to_string(),
                 container: Expr::new(ExprVal::Ident("items".to_string())),
                 body: vec![Node::Continue(WS { left: false, right: true }),],
+                empty_body: None,
             },
             for_ws,
         )
diff --git a/src/parser/tests/whitespace.rs b/src/parser/tests/whitespace.rs
index 50e68c2..32b1f99 100644
--- a/src/parser/tests/whitespace.rs
+++ b/src/parser/tests/whitespace.rs
@@ -104,6 +104,7 @@ fn handle_ws_both_sides_for_forloop_tag_and_remove_empty_node() {
                 container: Expr::new(ExprVal::Int(1)),
                 // not valid but we don't care about it here
                 body: vec![Node::Text("   ".to_string()), Node::Text("hey   ".to_string())],
+                empty_body: None,
             },
             end_ws,
         ),
@@ -121,6 +122,7 @@ fn handle_ws_both_sides_for_forloop_tag_and_remove_empty_node() {
                     container: Expr::new(ExprVal::Int(1)),
                     // not valid but we don't care about it here
                     body: vec![Node::Text("hey".to_string())],
+                    empty_body: None,
                 },
                 end_ws,
             ),
diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs
index 2f5b223..9f74f1b 100644
--- a/src/renderer/tests/basic.rs
+++ b/src/renderer/tests/basic.rs
@@ -493,7 +493,20 @@ fn render_for() {
         (
             "{% set start = 0 %}{% set end = start + 3 %}{% for i in range(start=start, end=end) %}{{ i }}{% endfor%}",
             "012"
-        )
+        ),
+        // https://github.com/Keats/tera/issues/395
+        (
+            "{% for a in [] %}{{a}}{% else %}hello{% endfor %}",
+            "hello"
+        ),
+        (
+            "{% for a in undefined_variable | default(value=[]) %}{{a}}{% else %}hello{% endfor %}",
+            "hello"
+        ),        
+        (
+            "{% for a in [] %}{{a}}{% else %}{% if 1 == 2 %}A{% else %}B{% endif %}{% endfor %}",
+            "B"
+        ),
     ];
 
     for (input, expected) in inputs {