feat(for tag): Support where expressions (#96)

This commit is contained in:
Ilya Puchka
2017-04-11 12:39:00 +02:00
committed by Kyle Fuller
parent dc8759ba34
commit d1717df6ff
4 changed files with 60 additions and 16 deletions

View File

@@ -4,6 +4,7 @@
### Enhancements ### Enhancements
- `for` block now can contain `where` expression to filter array items. For example `{% for item in items where item > 1 %}` is now supported.
- `if` blocks may now contain else if (`elif`) conditions. - `if` blocks may now contain else if (`elif`) conditions.
```html+django ```html+django
@@ -16,7 +17,6 @@
{% endif %} {% endif %}
``` ```
## 0.8.0 ## 0.8.0
### Breaking ### Breaking

View File

@@ -3,12 +3,14 @@ class ForNode : NodeType {
let loopVariable:String let loopVariable:String
let nodes:[NodeType] let nodes:[NodeType]
let emptyNodes: [NodeType] let emptyNodes: [NodeType]
let `where`: Expression?
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components() let components = token.components()
guard components.count == 4 && components[2] == "in" else { guard components.count >= 2 && components[2] == "in" &&
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.") (components.count == 4 || (components.count >= 6 && components[4] == "where")) else {
throw TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `\(token.contents)`.")
} }
let loopVariable = components[1] let loopVariable = components[1]
@@ -28,20 +30,35 @@ class ForNode : NodeType {
} }
let filter = try parser.compileFilter(variable) let filter = try parser.compileFilter(variable)
return ForNode(resolvable: filter, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes) let `where`: Expression?
if components.count >= 6 {
`where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
} else {
`where` = nil
}
return ForNode(resolvable: filter, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
} }
init(resolvable: Resolvable, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) { init(resolvable: Resolvable, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) {
self.resolvable = resolvable self.resolvable = resolvable
self.loopVariable = loopVariable self.loopVariable = loopVariable
self.nodes = nodes self.nodes = nodes
self.emptyNodes = emptyNodes self.emptyNodes = emptyNodes
self.where = `where`
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {
let values = try resolvable.resolve(context) let values = try resolvable.resolve(context)
if let values = values as? [Any] , values.count > 0 { if var values = values as? [Any], values.count > 0 {
if let `where` = self.where {
values = try values.filter({ item -> Bool in
return try context.push(dictionary: [loopVariable: item]) { () -> Bool in
try `where`.evaluate(context: context)
}
})
}
if values.count > 0 {
let count = values.count let count = values.count
return try values.enumerated().map { index, item in return try values.enumerated().map { index, item in
let forContext: [String: Any] = [ let forContext: [String: Any] = [
@@ -55,6 +72,7 @@ class ForNode : NodeType {
} }
}.joined(separator: "") }.joined(separator: "")
} }
}
return try context.push { return try context.push {
try renderNodes(emptyNodes, context) try renderNodes(emptyNodes, context)

View File

@@ -63,6 +63,21 @@ func testForNode() {
try expect(try node.render(context)) == "112233" try expect(try node.render(context)) == "112233"
} }
$0.it("renders the given nodes while filtering items using where expression") {
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()))
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [], where: `where`)
try expect(try node.render(context)) == "2132"
}
$0.it("renders the given empty nodes when all items filtered out with where expression") {
let nodes: [NodeType] = [VariableNode(variable: "item")]
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()))
let node = ForNode(resolvable: Variable("emptyItems"), loopVariable: "item", nodes: nodes, emptyNodes: emptyNodes, where: `where`)
try expect(try node.render(context)) == "empty"
}
$0.it("can render a filter") { $0.it("can render a filter") {
let templateString = "{% for article in ars|default:articles %}" + let templateString = "{% for article in ars|default:articles %}" +
"- {{ article.title }} by {{ article.author }}.\n" + "- {{ article.title }} by {{ article.author }}.\n" +

View File

@@ -19,6 +19,17 @@ A for loop allows you to iterate over an array found by variable lookup.
{% endfor %} {% endfor %}
</ul> </ul>
The ``for`` tag can contain optional ``where`` expression to filter out
elements on which this expression evaluates to false.
.. code-block:: html+django
<ul>
{% for user in users where user.name != "Kyle" %}
<li>{{ user }}</li>
{% endfor %}
</ul>
The ``for`` tag can take an optional ``{% empty %}`` block that will be The ``for`` tag can take an optional ``{% empty %}`` block that will be
displayed if the given list is empty or could not be found. displayed if the given list is empty or could not be found.