Repository: Keats/tera (rust)

Issue:
Expressions for String Arrays
Not sure if this is currently possible in tera, but it would be nice to be able to create arrays.

```
{% set vars = ["A","B","C"] %}
```

Also in for loops it would be handy:

```
{% for var in ["A","B","C"] %}
{{var}}
{% endfor %}
```


Pull request description:
Add array literals
Closes #274

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55277d2..979ad01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 0.11.7 (unreleased)
+
+- Add array literal instantiation from inside Tera for set, set_global, kwargs
+and for loop container
+
 ## 0.11.6 (2018-03-25)
 
 - Add `break` and `continue` to forloops
@@ -262,7 +267,7 @@ Thanks to @SergioBenitez and @andrelmartins for the contributions!
 - Added filters, see README for current list
 - Added tests, only `defined` for now
 
-Thanks to @SergioBenitez, @orhanbalci, @foophoof and @Peternator7 for the 
+Thanks to @SergioBenitez, @orhanbalci, @foophoof and @Peternator7 for the
 contribution!
 
 ## 0.1.3 (2016/08/14)
diff --git a/Cargo.toml b/Cargo.toml
index f06ca8e..18a2bac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "tera"
-version = "0.11.6"
+version = "0.11.7"
 authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
 license = "MIT"
 readme = "README.md"
diff --git a/docs/content/docs/templates.md b/docs/content/docs/templates.md
index ce89bac..26bdd61 100644
--- a/docs/content/docs/templates.md
+++ b/docs/content/docs/templates.md
@@ -21,6 +21,7 @@ Tera has a few literals that can be used:
 - integers
 - floats
 - strings: text delimited by `""`, `''` or backticks
+- arrays: a list of literals and/or idents by `[` and `]` and comma separated (trailing comma allowed)
 
 ## Variables
 
@@ -40,7 +41,7 @@ Specific members of an array or tuple are accessed by using the `.i` notation, w
 A more powerful alternative to (`.`) is to use square brackets (`[ ]`).
 Variables can be rendering using the notation `{{product['name']}}` or `{{product["name"]}}`.
 
-If the item is not in quotes it will be treated as a variable. 
+If the item is not in quotes it will be treated as a variable.
 Assuming you have the following objects in your context `product = Product{ name: "Fred" }`
 and `my_field = "name"`, calling `{{product[my_field]}}` will resolve to: `{{product.name}}`.
 
@@ -84,11 +85,11 @@ The priority of operations is the following, from lowest to highest:
 
 ## Filters
 
-You can modify variables using **filters**. 
-Filters are separated from the variable by a pipe symbol (|) and may have named arguments in parentheses. 
+You can modify variables using **filters**.
+Filters are separated from the variable by a pipe symbol (|) and may have named arguments in parentheses.
 Multiple filters can be chained: the output of one filter is applied to the next.
 
-For example, `{{ name | lower | replace(from="doctor", to="Dr.") }}` will take a variable called name, make it lowercase and then replace instances of `doctor` by `Dr.`. 
+For example, `{{ name | lower | replace(from="doctor", to="Dr.") }}` will take a variable called name, make it lowercase and then replace instances of `doctor` by `Dr.`.
 It is equivalent to `replace(lower(name), from="doctor", to="Dr.")` if we were to look at it as functions.
 
 Calling filters on a incorrect type like trying to capitalize an array or using invalid types for arguments will result in a error.
@@ -127,8 +128,8 @@ This example transforms the text `Hello` in all upper-case (`HELLO`).
 
 ## Tests
 
-Tests can be used against an expression to check some condition on it and 
-are made in `if` blocks using the `is` keyword. 
+Tests can be used against an expression to check some condition on it and
+are made in `if` blocks using the `is` keyword.
 For example, you would write the following to test if an expression is odd:
 
 ```jinja2
@@ -149,7 +150,7 @@ Tera has many [built-in tests](./docs/templates.md#built-in-tests) that you can
 Global functions are Rust code that return a `Result<Value>` from the given params.
 
 Quite often, global functions will need to capture some external variables, such as a `url_for` global function needing
-the list of URLs for example. 
+the list of URLs for example.
 To make that work, the type of `GlobalFn` is a boxed closure: `Box<Fn(HashMap<String, Value>) -> Result<Value> + Sync + Send>`.
 
 Here's an example on how to implement a very basic global function:
@@ -187,7 +188,7 @@ Currently global functions can be called in two places in templates:
 Tera comes with some [built-in global functions](./docs/templates.md#built-in-global-functions).
 
 ## Assignments
-You can assign values to variables during the rendering. 
+You can assign values to variables during the rendering.
 Assignments in for loops and macros are scoped to their context but
 assignments outside of those will be set in the global context.
 
@@ -197,6 +198,7 @@ assignments outside of those will be set in the global context.
 {% set my_var = some_var %}
 {% set my_var = macros::some_macro() %}
 {% set my_var = global_fn() %}
+{% set my_var = [1, true, some_var | round] %}
 ```
 
 If you want to assign a value in the global context while in a forloop, you can use `set_global`:
@@ -207,6 +209,7 @@ If you want to assign a value in the global context while in a forloop, you can
 {% set_global my_var = some_var %}
 {% set_global my_var = macros::some_macro() %}
 {% set_global my_var = global_fn() %}
+{% set_global my_var = [1, true, some_var | round] %}
 ```
 
 ## Comments
@@ -275,6 +278,15 @@ If you are iterating on an array, you can also apply filters to the container:
 {% endfor %}
 ```
 
+Lastly, you can iterate on array literals:
+
+```jinja2
+{% for a in [1,2,3,] %}
+  {{a}}
+{% endfor %}
+```
+
+
 ### Loop Controls
 
 Within a loop, `break` and `continue` may be used to control iteration.
@@ -304,17 +316,17 @@ You can include a template to be rendered using the current context with the `in
 {% include "included.html" %}
 ```
 
-Tera doesn't offer passing a custom context to the `include` tag. 
+Tera doesn't offer passing a custom context to the `include` tag.
 If you want to do that, use [macros](./docs/templates.md#macros).
 
 ## Inheritance
 
-Tera uses the same kind of inheritance as Jinja2 and Django templates: 
+Tera uses the same kind of inheritance as Jinja2 and Django templates:
 you define a base template and extends it in child templates through blocks.
 There can be multiple levels of inheritance (i.e. A extends B that extends C).
 
 #### Base template
-A base template typically contains the basic document structure as well as 
+A base template typically contains the basic document structure as well as
 several `blocks` that can have content.
 
 For example, here's a `base.html` almost copied from the Jinja2 documentation:
@@ -340,7 +352,7 @@ For example, here's a `base.html` almost copied from the Jinja2 documentation:
 ```
 The only difference with Jinja2 being that the `endblock` tags have to be named.
 
-This `base.html` template defines 4 `block` tag that child templates can override. 
+This `base.html` template defines 4 `block` tag that child templates can override.
 The `head` and `footer` block have some content already which will be rendered if they are not overridden.
 
 #### Child template
@@ -386,7 +398,7 @@ The block `ending` is nested in the `hey` block. Rendering the `child` template
 
 - Find the first base template: `grandparent`
 - See `hey` block in it and checks if it is in `child` and `parent` template
-- It is in `child` so we render it, it contains a `super()` call so we render the `hey` block from `parent`, 
+- It is in `child` so we render it, it contains a `super()` call so we render the `hey` block from `parent`,
 which also contains a `super()` so we render the `hey` block of the `grandparent` template as well
 - See `ending` block in `child`, render it and also renders the `ending` block of `parent` as there is a `super()`
 
@@ -488,7 +500,7 @@ Returns number of words in a string
 Returns the string with all its character lowercased apart from the first char which is uppercased.
 
 ### replace
-Takes 2 mandatory string named arguments: `from` and `to`. It will return a string with all instances of 
+Takes 2 mandatory string named arguments: `from` and `to`. It will return a string with all instances of
 the `from` string with the `to` string.
 
 Example: `{{ name | replace(from="Robert", to="Bob")}}`
@@ -496,12 +508,12 @@ Example: `{{ name | replace(from="Robert", to="Bob")}}`
 ### addslashes
 Adds slashes before quotes.
 
-Example: `{{ value | addslashes }}` 
+Example: `{{ value | addslashes }}`
 
 If value is "I'm using Tera", the output will be "I\'m using Tera".
 
 ### slugify
-Transform a string into ASCII, lowercase it, trim it, converts spaces to hyphens and 
+Transform a string into ASCII, lowercase it, trim it, converts spaces to hyphens and
 remove all characters that are not numbers, lowercase letters or hyphens.
 
 Example: `{{ value | slugify }}`
@@ -531,6 +543,9 @@ Example: `{{ value | striptags}}`
 
 If value is "<b>Joel</b>", the output will be "Joel".
 
+Note that if the template you using it in is automatically escaped, you will need to call the `safe` filter
+before `striptags`.
+
 ### first
 Returns the first element of an array.
 If the array is empty, returns empty string.
@@ -612,8 +627,8 @@ Example: `{{ value | urlencode }}`
 
 If value is `/foo?a=b&c=d`, the output will be `/foo%3Fa%3Db%26c%3Dd`.
 
-Takes an optional argument of characters that shouldn't be percent-encoded (`/` by default). 
-So, to encode slashes as well, you can do `{{ value | urlencode(safe="") }}`. 
+Takes an optional argument of characters that shouldn't be percent-encoded (`/` by default).
+So, to encode slashes as well, you can do `{{ value | urlencode(safe="") }}`.
 
 ### pluralize
 Returns a suffix if the value is greater or equal than 2. Suffix defaults to `s`
@@ -638,7 +653,7 @@ Example: `{{ num | filesizeformat }}`
 
 ### date
 Parse a timestamp into a date(time) string. Defaults to `YYYY-MM-DD` format.
-Time formatting syntax is inspired from strftime and a full reference is available 
+Time formatting syntax is inspired from strftime and a full reference is available
 on [chrono docs](https://lifthrasiir.github.io/rust-chrono/chrono/format/strftime/index.html).
 
 Example: `{{ ts | date }} {{ ts | date(format="%Y-%m-%d %H:%M") }}`
@@ -751,7 +766,7 @@ Tera comes with some built-in global functions.
 
 ### range
 
-Returns an array of integers created using the arguments given. 
+Returns an array of integers created using the arguments given.
 There are 3 arguments, all integers:
 
 - `end`: where to stop, mandatory
diff --git a/src/parser/ast.rs b/src/parser/ast.rs
index 5bc6e8e..c209870 100644
--- a/src/parser/ast.rs
+++ b/src/parser/ast.rs
@@ -124,6 +124,9 @@ pub enum ExprVal {
     Test(Test),
     MacroCall(MacroCall),
     FunctionCall(FunctionCall),
+    // A vec of Expr, not ExprVal since filters are allowed
+    // on values inside arrays
+    Array(Vec<Expr>),
 }
 
 #[derive(Clone, Debug, PartialEq)]
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 04f8b79..1236470 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -63,6 +63,7 @@ fn parse_kwarg(pair: Pair<Rule>) -> (String, Expr) {
         match p.as_rule() {
             Rule::ident => name = Some(p.into_span().as_str().to_string()),
             Rule::logic_expr => val = Some(parse_logic_expr(p)),
+            Rule::array => val = Some(Expr::new(parse_array(p))),
             _ => unreachable!("{:?} not supposed to get there (parse_kwarg)!", p.as_rule()),
         };
     }
@@ -300,6 +301,21 @@ fn parse_logic_expr(pair: Pair<Rule>) -> Expr {
     }
 }
 
+fn parse_array(pair: Pair<Rule>) -> ExprVal {
+    let mut vals = vec![];
+
+    for p in pair.into_inner() {
+        match p.as_rule() {
+            Rule::basic_expr_filter => {
+                vals.push(parse_basic_expr_with_filters(p));
+            },
+            _ => unreachable!("Got {:?} in parse_array", p.as_rule()),
+        }
+    }
+
+    ExprVal::Array(vals)
+}
+
 fn parse_macro_call(pair: Pair<Rule>) -> MacroCall {
     let mut namespace = None;
     let mut name = None;
@@ -389,6 +405,7 @@ fn parse_set_tag(pair: Pair<Rule>, global: bool) -> Node {
             }
             Rule::ident => key = Some(p.as_str().to_string()),
             Rule::logic_expr => expr = Some(parse_logic_expr(p)),
+            Rule::array => expr = Some(Expr::new(parse_array(p))),
             _ => unreachable!("unexpected {:?} rule in parse_set_tag", p.as_rule()),
         }
     }
@@ -565,7 +582,8 @@ fn parse_forloop(pair: Pair<Rule>) -> Node {
                         Rule::ident => idents.push(p2.as_str().to_string()),
                         Rule::basic_expr_filter => {
                             container = Some(parse_basic_expr_with_filters(p2));
-                        }
+                        },
+                        Rule::array => container = Some(Expr::new(parse_array(p2))),
                         _ => unreachable!(),
                     };
                 }
@@ -777,6 +795,7 @@ pub fn parse(input: &str) -> TeraResult<Vec<Node>> {
                         "a string".to_string()
                     }
                     Rule::all_chars => "a character".to_string(),
+                    Rule::array => "an array of values".to_string(),
                     Rule::basic_val => "a value".to_string(),
                     Rule::basic_op => "a mathematical operator".to_string(),
                     Rule::comparison_op => "a comparison operator".to_string(),
diff --git a/src/parser/tera.pest b/src/parser/tera.pest
index 0ec6e82..6d46605 100644
--- a/src/parser/tera.pest
+++ b/src/parser/tera.pest
@@ -89,13 +89,14 @@ comparison_expr = { comparison_val ~ (comparison_op ~ comparison_val)* }
 logic_val  = { op_not? ~ comparison_expr }
 logic_expr = { logic_val ~ ((op_or | op_and) ~ logic_val)* }
 
+array = { "[" ~ (basic_expr_filter ~ ",")* ~ basic_expr_filter? ~ "]"}
 
 // ----------------------------------------------------
 
 /// FUNCTIONS & FILTERS
 
 // A keyword argument: something=10, something="a value", something=1+10 etc
-kwarg   = { ident ~ "=" ~ logic_expr}
+kwarg   = { ident ~ "=" ~ (logic_expr | array) }
 kwargs  = _{ kwarg ~ ("," ~ kwarg )* }
 fn_call = { ident ~ "(" ~ kwargs? ~ ")" }
 filter  = { "|" ~ (fn_call | ident) }
@@ -147,10 +148,10 @@ macro_tag        = !{ tag_start ~ "macro" ~ macro_fn ~ tag_end }
 if_tag           = !{ tag_start ~ "if" ~ logic_expr ~ tag_end }
 elif_tag         = !{ tag_start ~ "elif" ~ logic_expr ~ tag_end }
 else_tag         = !{ tag_start ~ "else" ~ tag_end }
-for_tag          = !{ tag_start ~ "for" ~ ident ~ ("," ~ ident)? ~ "in" ~ basic_expr_filter ~ tag_end }
+for_tag          = !{ tag_start ~ "for" ~ ident ~ ("," ~ ident)? ~ "in" ~ (basic_expr_filter | array) ~ tag_end }
 filter_tag       = !{ tag_start ~ "filter" ~ (fn_call | ident) ~ tag_end }
-set_tag          = !{ tag_start ~ "set" ~ ident ~ "=" ~ logic_expr ~ tag_end }
-set_global_tag   = !{ tag_start ~ "set_global" ~ ident ~ "=" ~ logic_expr ~ tag_end }
+set_tag          = !{ tag_start ~ "set" ~ ident ~ "=" ~ (logic_expr | array) ~ tag_end }
+set_global_tag   = !{ tag_start ~ "set_global" ~ ident ~ "=" ~ (logic_expr | array) ~ tag_end }
 endblock_tag     = !{ tag_start ~ "endblock" ~ ident? ~ tag_end }
 endmacro_tag     = !{ tag_start ~ "endmacro" ~ ident? ~ tag_end }
 endif_tag        = !{ tag_start ~ "endif" ~ tag_end }
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
index 7c45f4f..c959f9f 100644
--- a/src/renderer/mod.rs
+++ b/src/renderer/mod.rs
@@ -451,10 +451,17 @@ impl<'a> Renderer<'a> {
         let mut needs_escape = false;
 
         let mut res = match expr.val {
+            ExprVal::Array(ref arr) => {
+                let mut vals = vec![];
+                for v in arr {
+                    vals.push(self.eval_expression(v)?);
+                }
+                Value::Array(vals)
+            },
             ExprVal::String(ref val) => {
                 needs_escape = true;
                 Value::String(val.to_string())
-            }
+            },
             ExprVal::Int(val) => Value::Number(val.into()),
             ExprVal::Float(val) => Value::Number(Number::from_f64(val).unwrap()),
             ExprVal::Bool(val) => Value::Bool(val),
@@ -551,6 +558,7 @@ impl<'a> Renderer<'a> {
         let container_name = match node.container.val {
             ExprVal::Ident(ref ident) => ident,
             ExprVal::FunctionCall(FunctionCall { ref name, .. }) => name,
+            ExprVal::Array(_) => "an array literal",
             _ => bail!(
                 "Forloop containers have to be an ident or a function call (tried to iterate on '{:?}')",
                 node.container.val,


Test patch:
diff --git a/src/parser/tests/lexer.rs b/src/parser/tests/lexer.rs
index 88296ae..dee39dd 100644
--- a/src/parser/tests/lexer.rs
+++ b/src/parser/tests/lexer.rs
@@ -103,6 +103,22 @@ fn lex_dotted_square_bracket_ident() {
     }
 }
 
+#[test]
+fn lex_array() {
+    let inputs = vec![
+        "[]",
+        "[1,2,3]",
+        "[1, 2,3,]",
+        "[1 + 1, 2,3 * 2,]",
+        "[1,true,'string', 0.5, hello(), macros::hey(arg=1)]",
+    ];
+
+    for i in inputs {
+        assert_lex_rule!(Rule::array, i);
+    }
+}
+
+
 #[test]
 fn lex_basic_expr() {
     let inputs = vec![
@@ -255,6 +271,8 @@ fn lex_kwarg() {
     let inputs = vec![
         "hello=1",
         "hello=1+1",
+        "hello=[]",
+        "hello=[true, false]",
         "hello1=true",
         "hello=name",
         "hello=name|filter",
@@ -417,6 +435,8 @@ fn lex_for_tag() {
         "{% for a, b in object -%}",
         "{% for a, b in fn_call() %}",
         "{% for a in fn_call() %}",
+        "{% for a in [] %}",
+        "{% for a in [1,2,3,] %}",
         "{% for a,b in fn_call(with_args=true, name=name) %}",
         "{% for client in clients | slice(start=1, end=9) %}",
     ];
@@ -451,6 +471,7 @@ fn lex_set_tag() {
     let inputs = vec![
         "{%- set a = true %}",
         "{% set a = object -%}",
+        "{% set a = [1,2,3, 'hey'] -%}",
         "{% set a = fn_call() %}",
         "{% set a = fn_call(with_args=true, name=name) %}",
         "{% set a = macros::fn_call(with_args=true, name=name) %}",
@@ -467,6 +488,7 @@ fn lex_set_tag() {
 fn lex_set_global_tag() {
     let inputs = vec![
         "{% set_global a = 1 %}",
+        "{% set_global a = [1,2,3, 'hey'] -%}",
         "{% set_global a = another_var %}",
         "{% set_global a = another_var | filter %}",
         "{% set_global a = var +1 >= 2%}",
diff --git a/src/parser/tests/parser.rs b/src/parser/tests/parser.rs
index ea716b8..f97a2f3 100644
--- a/src/parser/tests/parser.rs
+++ b/src/parser/tests/parser.rs
@@ -324,6 +324,30 @@ fn parse_variable_tag_macro_call() {
     );
 }
 
+// smoke test for array in kwargs
+#[test]
+fn parse_variable_tag_macro_call_with_array() {
+    let ast = parse("{{ macros::get_time(some=[1, 2]) }}").unwrap();
+    let mut args = HashMap::new();
+    args.insert(
+        "some".to_string(),
+        Expr::new(ExprVal::Array(vec![
+            Expr::new(ExprVal::Int(1)),
+            Expr::new(ExprVal::Int(2)),
+        ]))
+    );
+
+    assert_eq!(
+        ast[0],
+        Node::VariableBlock(Expr::new(ExprVal::MacroCall(
+            MacroCall {
+                namespace: "macros".to_string(),
+                name: "get_time".to_string(),
+                args,
+            }
+        )))
+    );
+}
 #[test]
 fn parse_variable_tag_macro_call_with_filter() {
     let ast = parse("{{ macros::get_time(some=1) | round }}").unwrap();
@@ -444,6 +468,26 @@ fn parse_set_tag_fn_call() {
     );
 }
 
+#[test]
+fn parse_set_array() {
+    let ast = parse("{% set hello = [1, true, 'hello'] %}").unwrap();
+    assert_eq!(
+        ast[0],
+        Node::Set(
+            WS::default(),
+            Set {
+                key: "hello".to_string(),
+                value: Expr::new(ExprVal::Array(vec![
+                    Expr::new(ExprVal::Int(1)),
+                    Expr::new(ExprVal::Bool(true)),
+                    Expr::new(ExprVal::String("hello".to_string())),
+                ])),
+                global: false,
+            },
+        )
+    );
+}
+
 #[test]
 fn parse_set_global_tag() {
     let ast = parse("{% set_global hello = utcnow() %}").unwrap();
@@ -615,6 +659,31 @@ fn parse_key_value_forloop() {
     );
 }
 
+#[test]
+fn parse_value_forloop_array() {
+    let ast = parse("{% for item in [1,2,] %}A{%- 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())],
+            },
+            end_ws,
+        )
+    );
+}
+
 #[test]
 fn parse_if() {
     let ast = parse("{% if item or admin %}A {%- elif 1 > 2 %}B{% else -%} C{%- endif %}").unwrap();
diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs
index 8a416ae..2f62938 100644
--- a/src/renderer/tests/basic.rs
+++ b/src/renderer/tests/basic.rs
@@ -231,6 +231,7 @@ fn add_set_values_in_context() {
         ("{% set i = admin or true %}{{ i }}", "true"),
         ("{% set i = admin and num > 0 %}{{ i }}", "true"),
         ("{% set i = 0 / 0 %}{{ i }}", "NaN"),
+        ("{% set i = [1,2] %}{{ i }}", "[1, 2]"),
     ];
 
     for (input, expected) in inputs {
@@ -369,6 +370,10 @@ fn render_for() {
             "{% for v in vectors %}{% for i in v %}{% if i == 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}{% endfor %}",
             "06147"
         ),
+        (
+            "{% for a in [1, true, 1.1, 'hello'] %}{{a}}{% endfor %}",
+            "1true1.1hello"
+        ),
     ];
 
     for (input, expected) in inputs {