Repository: Keats/tera (rust)

Issue:
Add loop controls
`break` and `continue`: see http://jinja.pocoo.org/docs/dev/templates/#loop-controls in Jinja2

This would avoid ugly things like https://github.com/Keats/book/blob/master/templates/page.html#L29-L43

Pull request description:
Add loop controls (`break` and `continue`)
Resolves #267.

Most rendering functions return `Result<String>`. I wanted to avoid reusing `Result` to indicate that a `break` or `continue` was encountered, so I changed the return type of some functions to `Result<LoopControl>`.

`LoopControl` is similar to `Result`, but doesn't indicate failure. This ensures that a break isn't ignored or permitted outside loops. Outside a loop where rendering must finish normally, `LoopControl.into_normal_result()` allows obtaining a `Result<String>` while checking break/continue was not encountered.

If you have a different approach in mind, or have other feedback, please let me know!


Patch:
diff --git a/docs/content/docs/templates.md b/docs/content/docs/templates.md
index 6e28f1e..b1c4a85 100644
--- a/docs/content/docs/templates.md
+++ b/docs/content/docs/templates.md
@@ -274,6 +274,27 @@ If you are iterating on an array, you can also apply filters to the container:
   {{loop.index}}. {{product.name}}
 {% endfor %}
 ```
+
+### Loop Controls
+
+Within a loop, `break` and `continue` may be used to control iteration.
+
+To stop iterating when `target_id` is reached:
+```jinja2
+{% for product in products %}
+  {% if product.id == target_id %}{% break %}{% endif %}
+  {{loop.index}}. {{product.name}}
+{% endfor %}
+```
+
+To skip even-numbered items:
+```jinja2
+{% for product in products %}
+  {% if loop.index is even %}{% continue %}{% endif %}
+  {{loop.index}}. {{product.name}}
+{% endfor %}
+```
+
 ## Include
 
 You can include a template to be rendered using the current context with the `include` tag.
diff --git a/src/parser/ast.rs b/src/parser/ast.rs
index 0186441..5bc6e8e 100644
--- a/src/parser/ast.rs
+++ b/src/parser/ast.rs
@@ -271,4 +271,9 @@ pub enum Node {
 
     /// A if/elif/else block, WS for the if/elif/else is directly in the struct
     If(If, WS),
+
+    /// The `{% break %}` tag
+    Break(WS),
+    /// The `{% continue %}` tag
+    Continue(WS),
 }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index d747030..bcc3ec1 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -436,7 +436,10 @@ fn parse_filter_section(pair: Pair<Rule>) -> Node {
                     _ => unreachable!("Got {:?} while parsing filter_tag", p2),
                 }
             },
-            Rule::content | Rule::macro_content | Rule::block_content => {
+            Rule::content
+            | Rule::macro_content
+            | Rule::block_content
+            | Rule::for_content => {
                 body.extend(parse_content(p));
             }
             Rule::endfilter_tag => for p2 in p.into_inner() {
@@ -562,7 +565,10 @@ fn parse_forloop(pair: Pair<Rule>) -> Node {
                     value = Some(idents[1].clone());
                 }
             }
-            Rule::content | Rule::macro_content | Rule::block_content => {
+            Rule::content
+            | Rule::macro_content
+            | Rule::block_content
+            | Rule::for_content => {
                 body.extend(parse_content(p));
             }
             Rule::endfor_tag => for p2 in p.into_inner() {
@@ -589,6 +595,42 @@ fn parse_forloop(pair: Pair<Rule>) -> Node {
     )
 }
 
+fn parse_break_tag(pair: Pair<Rule>) -> Node {
+    let mut ws = WS::default();
+
+    for p in pair.into_inner() {
+        match p.as_rule() {
+            Rule::tag_start => {
+                ws.left = p.into_span().as_str() == "{%-";
+            },
+            Rule::tag_end => {
+                ws.right = p.into_span().as_str() == "-%}";
+            }
+            _ => unreachable!()
+        };
+    };
+
+    Node::Break(ws)
+}
+
+fn parse_continue_tag(pair: Pair<Rule>) -> Node {
+    let mut ws = WS::default();
+
+    for p in pair.into_inner() {
+        match p.as_rule() {
+            Rule::tag_start => {
+                ws.left = p.into_span().as_str() == "{%-";
+            },
+            Rule::tag_end => {
+                ws.right = p.into_span().as_str() == "-%}";
+            }
+            _ => unreachable!()
+        };
+    };
+
+    Node::Continue(ws)
+}
+
 fn parse_if(pair: Pair<Rule>) -> Node {
     // the `endif` tag ws handling
     let mut end_ws = WS::default();
@@ -621,7 +663,7 @@ fn parse_if(pair: Pair<Rule>) -> Node {
                     };
                 }
             }
-            Rule::content | Rule::macro_content | Rule::block_content => {
+            Rule::content | Rule::macro_content | Rule::block_content | Rule::for_content => {
                 current_body.extend(parse_content(p))
             }
             Rule::else_tag => {
@@ -683,10 +725,12 @@ fn parse_content(pair: Pair<Rule>) -> Vec<Node> {
             Rule::variable_tag => nodes.push(parse_variable_tag(p)),
             Rule::import_macro_tag => nodes.push(parse_import_macro(p)),
             Rule::macro_definition => nodes.push(parse_macro_definition(p)),
-            Rule::forloop | Rule::macro_forloop | Rule::block_forloop => {
-                nodes.push(parse_forloop(p))
-            }
-            Rule::content_if | Rule::macro_if | Rule::block_if => nodes.push(parse_if(p)),
+            Rule::forloop => nodes.push(parse_forloop(p)),
+            Rule::break_tag => nodes.push(parse_break_tag(p)),
+            Rule::continue_tag => nodes.push(parse_continue_tag(p)),
+            Rule::content_if | Rule::macro_if | Rule::block_if | Rule::for_if => {
+                nodes.push(parse_if(p))
+            },
             Rule::filter_section | Rule::macro_filter_section | Rule::block_filter_section => {
                 nodes.push(parse_filter_section(p))
             }
@@ -762,7 +806,9 @@ pub fn parse(input: &str) -> TeraResult<Vec<Node>> {
                     Rule::set_tag => "a `set` tag`".to_string(),
                     Rule::set_global_tag => "a `set_global` tag`".to_string(),
                     Rule::endif_tag => "a `endif` tag`".to_string(),
-                    Rule::block_content | Rule::content => "some content".to_string(),
+                    Rule::block_content | Rule::content | Rule::for_content => {
+                        "some content".to_string()
+                    },
                     Rule::text => "some text".to_string(),
                     // Pest will error an unexpected tag as Rule::tag_start
                     // and just showing `{%` is not clear as some other valid
@@ -780,15 +826,16 @@ pub fn parse(input: &str) -> TeraResult<Vec<Node>> {
                     Rule::filter_tag
                     | Rule::filter_section
                     | Rule::block_filter_section
-                    | Rule::macro_filter_section => {
+                    | Rule::macro_filter_section
+                    | Rule::for_filter_section => {
                         "a filter section (`{% filter something %}...{% endfilter %}`)".to_string()
                     }
-                    Rule::for_tag | Rule::forloop | Rule::block_forloop | Rule::macro_forloop => {
+                    Rule::for_tag | Rule::forloop => {
                         "a forloop (`{% for i in something %}...{% endfor %}".to_string()
-                    }
+                    },
                     Rule::endfilter_tag => "an endfilter tag (`{% endfilter %}`)".to_string(),
                     Rule::endfor_tag => "an endfor tag (`{% endfor %}`)".to_string(),
-                    Rule::if_tag | Rule::content_if | Rule::block_if | Rule::macro_if => {
+                    Rule::if_tag | Rule::content_if | Rule::block_if | Rule::macro_if | Rule::for_if => {
                         "a `if` tag".to_string()
                     }
                     Rule::elif_tag => "an `elif` tag".to_string(),
@@ -807,6 +854,8 @@ pub fn parse(input: &str) -> TeraResult<Vec<Node>> {
                     | Rule::macro_tag => r#"a macro definition tag (`{% macro my_macro() %}`"#.to_string(),
                     Rule::extends_tag => r#"an extends tag (`{% extends "myfile" %}`"#.to_string(),
                     Rule::template => "a template".to_string(),
+                    Rule::break_tag => "a break tag".to_string(),
+                    Rule::continue_tag => "a continue tag".to_string(),
                 }
             });
             bail!("{}", fancy_e)
diff --git a/src/parser/tera.pest b/src/parser/tera.pest
index 325c45a..5e2b724 100644
--- a/src/parser/tera.pest
+++ b/src/parser/tera.pest
@@ -156,6 +156,8 @@ endmacro_tag     = !{ tag_start ~ "endmacro" ~ ident? ~ tag_end }
 endif_tag        = !{ tag_start ~ "endif" ~ tag_end }
 endfor_tag       = !{ tag_start ~ "endfor" ~ tag_end }
 endfilter_tag    = !{ tag_start ~ "endfilter" ~ tag_end }
+break_tag        = !{ tag_start ~ "break" ~ tag_end }
+continue_tag     = !{ tag_start ~ "continue" ~ tag_end }
 
 variable_tag     = !{ variable_start ~ logic_expr ~ variable_end }
 super_tag        = !{ variable_start ~ "super()" ~ variable_end }
@@ -170,14 +172,14 @@ raw        = !{ raw_tag ~ raw_text ~ endraw_tag }
 macro_filter_section = !{ filter_tag ~ macro_content* ~ endfilter_tag }
 block_filter_section = !{ filter_tag ~ block_content* ~ endfilter_tag }
 filter_section       = !{ filter_tag ~ content* ~ endfilter_tag }
+for_filter_section   = !{ filter_tag ~ for_content* ~ endfilter_tag }
 
-macro_forloop = ${ for_tag ~ macro_content* ~ endfor_tag }
-block_forloop = ${ for_tag ~ content* ~ endfor_tag }
-forloop       = ${ for_tag ~ block_content* ~ endfor_tag }
+forloop = ${ for_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 }
 content_if = ${ if_tag ~ content* ~ (elif_tag ~ content*)* ~ (else_tag ~ content*)? ~ endif_tag }
+for_if = ${ if_tag ~ for_content* ~ (elif_tag ~ for_content*)* ~ (else_tag ~ for_content*)? ~ endif_tag }
 
 
 block            = ${ block_tag ~ block_content* ~ endblock_tag }
@@ -191,7 +193,7 @@ macro_content = @{
     set_tag |
     set_global_tag |
     macro_if |
-    macro_forloop |
+    forloop |
     raw |
     macro_filter_section |
     text
@@ -207,12 +209,28 @@ block_content = @{
     set_global_tag |
     block |
     block_if |
-    block_forloop |
+    forloop |
     block_filter_section |
     raw |
     text
 }
 
+// set of allowed content inside for loops
+for_content = @{
+    include_tag |
+    variable_tag |
+    comment_tag |
+    set_tag |
+    set_global_tag |
+    for_if |
+    forloop |
+    break_tag |
+    continue_tag |
+    for_filter_section |
+    raw |
+    text
+}
+
 content = @{
     include_tag |
     import_macro_tag |
diff --git a/src/parser/whitespace.rs b/src/parser/whitespace.rs
index c3d6cab..c47d1d0 100644
--- a/src/parser/whitespace.rs
+++ b/src/parser/whitespace.rs
@@ -56,7 +56,9 @@ pub fn remove_whitespace(nodes: Vec<Node>, body_ws: Option<WS>) -> Vec<Node> {
             Node::ImportMacro(ws, _, _)
             | Node::Extends(ws, _)
             | Node::Include(ws, _)
-            | Node::Set(ws, _) => {
+            | Node::Set(ws, _)
+            | Node::Break(ws)
+            | Node::Continue(ws) => {
                 trim_right_previous!(previous_was_text && ws.left, res);
                 previous_was_text = false;
                 trim_left_next = ws.right;
diff --git a/src/renderer/forloop.rs b/src/renderer/forloop.rs
index ffb385a..6540a45 100644
--- a/src/renderer/forloop.rs
+++ b/src/renderer/forloop.rs
@@ -8,6 +8,13 @@ pub enum ForLoopKind {
     KeyValue,
 }
 
+#[derive(PartialEq, Debug)]
+pub enum ForLoopState {
+    Normal,
+    Break,
+    Continue,
+}
+
 // We need to have some data in the renderer for when we are in a ForLoop
 // For example, accessing the local variable would fail when
 // looking it up in the global context
@@ -25,6 +32,8 @@ pub struct ForLoop {
     pub kind: ForLoopKind,
     /// Values set using the {% set %} tag in forloops
     pub extra_values: Map<String, Value>,
+    /// Has the for loop encountered break or continue?
+    pub state: ForLoopState,
 }
 
 impl ForLoop {
@@ -40,6 +49,7 @@ impl ForLoop {
             values: for_values,
             kind: ForLoopKind::Value,
             extra_values: Map::new(),
+            state: ForLoopState::Normal,
         }
     }
 
@@ -56,12 +66,24 @@ impl ForLoop {
             values: for_values,
             kind: ForLoopKind::KeyValue,
             extra_values: Map::new(),
+            state: ForLoopState::Normal,
         }
     }
 
     #[inline]
     pub fn increment(&mut self) {
         self.current += 1;
+        self.state = ForLoopState::Normal;
+    }
+
+    #[inline]
+    pub fn break_loop(&mut self) {
+        self.state = ForLoopState::Break;
+    }
+
+    #[inline]
+    pub fn continue_loop(&mut self) {
+        self.state = ForLoopState::Continue;
     }
 
     #[inline]
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
index 2317e4c..7c45f4f 100644
--- a/src/renderer/mod.rs
+++ b/src/renderer/mod.rs
@@ -9,7 +9,7 @@ use serde_json::to_string_pretty;
 use serde_json::value::{to_value, Number, Value};
 use serde_json::map::Map as JsonMap;
 
-use self::forloop::ForLoop;
+use self::forloop::{ForLoop, ForLoopState};
 use self::square_brackets::pull_out_square_bracket;
 use parser::ast::*;
 use template::Template;
@@ -43,7 +43,6 @@ pub struct Renderer<'a> {
     blocks: Vec<(String, usize)>,
 }
 
-
 impl<'a> Renderer<'a> {
     pub fn new(template: &'a Template, tera: &'a Tera, context: Value) -> Renderer<'a> {
         let should_escape = tera.autoescape_suffixes.iter().any(|ext| {
@@ -206,6 +205,20 @@ impl<'a> Renderer<'a> {
         find_variable(context, key, &self.template.name)
     }
 
+    fn current_for_loop_mut(&mut self) -> Option<&mut ForLoop> {
+        match self.macro_context.last_mut() {
+            Some(&mut (_, ref mut for_loops)) => for_loops.last_mut(),
+            None => self.for_loops.last_mut(),
+        }
+    }
+
+    fn current_for_loop(&self) -> Option<&ForLoop> {
+        match self.macro_context.last() {
+            Some(&(_, ref for_loops)) => for_loops.last(),
+            None => self.for_loops.last(),
+        }
+    }
+
     /// Inserts the result of the expression in the right context with `key` as the ... key
     fn eval_set(&mut self, set: &Set) -> Result<()> {
         let val = self.safe_eval_expression(&set.value)?;
@@ -586,6 +599,11 @@ impl<'a> Renderer<'a> {
 
         for _ in 0..length {
             output.push_str(&self.render_body(&node.body)?);
+
+            if self.current_for_loop_mut().unwrap().state == ForLoopState::Break {
+                break
+            }
+
             // Safe unwrap
             match self.macro_context.last_mut() {
                 Some(m) => m.1.last_mut().unwrap().increment(),
@@ -724,6 +742,14 @@ impl<'a> Renderer<'a> {
             Node::ImportMacro(_, _, _) => String::new(),
             Node::If(ref if_node, _) => self.render_if(if_node)?,
             Node::Forloop(_, ref forloop, _) => self.render_for(forloop)?,
+            Node::Break(_) => {
+                self.current_for_loop_mut().unwrap().break_loop();
+                String::new()
+            },
+            Node::Continue(_) => {
+                self.current_for_loop_mut().unwrap().continue_loop();
+                String::new()
+            },
             Node::Block(_, ref block, _) => self.render_block(block, 0)?,
             Node::Super => self.do_super()?,
             Node::Include(_, ref tpl_name) => {
@@ -745,6 +771,13 @@ impl<'a> Renderer<'a> {
 
         for n in body {
             output.push_str(&self.render_node(n)?);
+            if let Some(for_loop) = self.current_for_loop() {
+                match for_loop.state {
+                    ForLoopState::Continue
+                    | ForLoopState::Break => break,
+                    ForLoopState::Normal => {},
+                }
+            }
         }
 
         Ok(output)


Test patch:
diff --git a/src/parser/tests/errors.rs b/src/parser/tests/errors.rs
index 28bfa6d..d929beb 100644
--- a/src/parser/tests/errors.rs
+++ b/src/parser/tests/errors.rs
@@ -325,3 +325,27 @@ fn invalid_test_argument() {
 fn unterminated_raw_tag() {
     assert_err_msg(r#"{% raw %}sd"#, &["1:12", "expected `{% endraw %}`"]);
 }
+
+#[test]
+fn invalid_break_outside_loop() {
+    assert_err_msg(
+        r#"{% break %}"#,
+        &[
+            "1:1",
+            "{% break %}",
+            "expected a template"
+        ],
+    );
+}
+
+#[test]
+fn invalid_continue_outside_loop() {
+    assert_err_msg(
+        r#"{% continue %}"#,
+        &[
+            "1:1",
+            "{% continue %}",
+            "expected a template"
+        ],
+    );
+}
diff --git a/src/parser/tests/lexer.rs b/src/parser/tests/lexer.rs
index 68d9ae9..5654a9b 100644
--- a/src/parser/tests/lexer.rs
+++ b/src/parser/tests/lexer.rs
@@ -426,6 +426,26 @@ fn lex_for_tag() {
     }
 }
 
+#[test]
+fn lex_break_tag() {
+    assert!(
+        TeraParser::parse(
+            Rule::break_tag,
+            "{% break %}"
+        ).is_ok()
+    );
+}
+
+#[test]
+fn lex_continue_tag() {
+    assert!(
+        TeraParser::parse(
+            Rule::continue_tag,
+            "{% continue %}"
+        ).is_ok()
+    );
+}
+
 #[test]
 fn lex_set_tag() {
     let inputs = vec![
diff --git a/src/parser/tests/parser.rs b/src/parser/tests/parser.rs
index f0e5b10..ea716b8 100644
--- a/src/parser/tests/parser.rs
+++ b/src/parser/tests/parser.rs
@@ -654,3 +654,41 @@ fn parse_if() {
         )
     );
 }
+
+#[test]
+fn parse_break() {
+    let ast = parse("{% for item in items %}{% break -%}{% endfor %}").unwrap();
+    let for_ws = WS::default();
+    assert_eq!(
+        ast[0],
+        Node::Forloop(
+            for_ws,
+            Forloop {
+                key: None,
+                value: "item".to_string(),
+                container: Expr::new(ExprVal::Ident("items".to_string())),
+                body: vec![Node::Break(WS {left:false, right:true})],
+            },
+            for_ws,
+        )
+    );
+}
+
+#[test]
+fn parse_continue() {
+    let ast = parse("{% for item in items %}{% continue -%}{% endfor %}").unwrap();
+    let for_ws = WS::default();
+    assert_eq!(
+        ast[0],
+        Node::Forloop(
+            for_ws,
+            Forloop {
+                key: None,
+                value: "item".to_string(),
+                container: Expr::new(ExprVal::Ident("items".to_string())),
+                body: vec![Node::Continue(WS {left:false, right:true})],
+            },
+            for_ws,
+        )
+    );
+}
diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs
index 141d3cc..16126e1 100644
--- a/src/renderer/tests/basic.rs
+++ b/src/renderer/tests/basic.rs
@@ -330,6 +330,24 @@ fn render_for() {
         ("{% for note in notes %}{{ note }}{% endfor %}", "123"),
         ("{% for note in notes | reverse %}{{ note }}{% endfor %}", "321"),
         ("{% for v in vectors %}{{ v.0 }}{% endfor %}", "01"),
+        // Loop control (`break` and `continue`)
+        // https://github.com/Keats/tera/issues/267
+        (
+            "{% for i in data %}{{ i }}{% if i == 2 %}{% break %}{% endif %}{% endfor %}",
+            "12"
+        ),
+        (
+            "{% for i in data %}{% if i == 2 %}{% continue %}{% endif %}{{ i }}{% endfor %}",
+            "13"
+        ),
+        (
+            "{% for v in vectors %}{% for i in v %}{% if i == 3 %}{% break %}{% endif %}{{ i }}{% endfor %}{% endfor %}",
+            "0147"
+        ),
+        (
+            "{% for v in vectors %}{% for i in v %}{% if i == 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}{% endfor %}",
+            "06147"
+        ),
     ];
 
     for (input, expected) in inputs {