diff --git a/CHANGELOG.md b/CHANGELOG.md index de58a4d..5613545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 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. ```html+django @@ -16,7 +17,6 @@ {% endif %} ``` - ## 0.8.0 ### Breaking diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 5628bbe..55663bd 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -3,12 +3,14 @@ class ForNode : NodeType { let loopVariable:String let nodes:[NodeType] let emptyNodes: [NodeType] + let `where`: Expression? class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { let components = token.components() - guard components.count == 4 && components[2] == "in" else { - throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.") + guard components.count >= 2 && components[2] == "in" && + (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] @@ -28,32 +30,48 @@ class ForNode : NodeType { } 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.loopVariable = loopVariable self.nodes = nodes self.emptyNodes = emptyNodes + self.where = `where` } func render(_ context: Context) throws -> String { let values = try resolvable.resolve(context) - if let values = values as? [Any] , values.count > 0 { - let count = values.count - return try values.enumerated().map { index, item in - let forContext: [String: Any] = [ - "first": index == 0, - "last": index == (count - 1), - "counter": index + 1, - ] + 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 + return try values.enumerated().map { index, item in + let forContext: [String: Any] = [ + "first": index == 0, + "last": index == (count - 1), + "counter": index + 1, + ] - return try context.push(dictionary: [loopVariable: item, "forloop": forContext]) { + return try context.push(dictionary: [loopVariable: item, "forloop": forContext]) { try renderNodes(nodes, context) - } - }.joined(separator: "") + } + }.joined(separator: "") + } } return try context.push { diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 666971e..1440b03 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -63,6 +63,21 @@ func testForNode() { 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") { let templateString = "{% for article in ars|default:articles %}" + "- {{ article.title }} by {{ article.author }}.\n" + diff --git a/docs/builtins.rst b/docs/builtins.rst index 424c4ec..dc7d23f 100644 --- a/docs/builtins.rst +++ b/docs/builtins.rst @@ -19,6 +19,17 @@ A for loop allows you to iterate over an array found by variable lookup. {% endfor %} +The ``for`` tag can contain optional ``where`` expression to filter out +elements on which this expression evaluates to false. + +.. code-block:: html+django + + + The ``for`` tag can take an optional ``{% empty %}`` block that will be displayed if the given list is empty or could not be found.