diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c8e68d4..5d10e2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Added support for iterating arrays of tuples - Added support for ranges in if-in expression - Added property `forloop.length` to get number of items in the loop +- Now you can construct ranges for loops using `a...b` syntax, i.e. `for i in 1...array.count` ### Bug Fixes diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 9a7d7ec1..cb96657d 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -10,9 +10,15 @@ class ForNode : NodeType { class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { let components = token.components() - guard components.count >= 3 && 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)`.") + func hasToken(_ token: String, at index: Int) -> Bool { + return components.count > (index + 1) && components[index] == token + } + func endsOrHasToken(_ token: String, at index: Int) -> Bool { + return components.count == index || hasToken(token, at: index) + } + + guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else { + throw TemplateSyntaxError("'for' statements should use the syntax: `for in [where ]") } let loopVariables = components[1].characters @@ -20,8 +26,6 @@ class ForNode : NodeType { .map(String.init) .map { $0.trimmingCharacters(in: CharacterSet.whitespaces) } - let variable = components[3] - var emptyNodes = [NodeType]() let forNodes = try parser.parse(until(["endfor", "empty"])) @@ -35,14 +39,13 @@ class ForNode : NodeType { _ = parser.nextToken() } - let filter = try parser.compileFilter(variable) - 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, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`) + let resolvable = try parser.compileResolvable(components[3]) + + let `where` = hasToken("where", at: 4) + ? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser) + : nil + + return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`) } init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) { diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 8f3b0fda..a857d3eb 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -111,7 +111,7 @@ final class IfExpressionParser { } } - return .variable(try tokenParser.compileFilter(component)) + return .variable(try tokenParser.compileResolvable(component)) } } diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 5d9a1ec5..81a44e10 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -40,7 +40,7 @@ public class TokenParser { case .text(let text): nodes.append(TextNode(text: text)) case .variable: - nodes.append(VariableNode(variable: try compileFilter(token.contents))) + nodes.append(VariableNode(variable: try compileResolvable(token.contents))) case .block: if let parse_until = parse_until , parse_until(self, token) { prependToken(token) @@ -114,6 +114,11 @@ public class TokenParser { return try FilterExpression(token: token, parser: self) } + public func compileResolvable(_ token: String) throws -> Resolvable { + return try RangeVariable(token, parser: self) + ?? compileFilter(token) + } + } // https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 74839b7c..9563c89b 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -130,6 +130,42 @@ public func ==(lhs: Variable, rhs: Variable) -> Bool { return lhs.variable == rhs.variable } +/// A structure used to represet range of two integer values expressed as `from...to`. +/// Values should be numbers (they will be converted to integers). +/// Rendering this variable produces array from range `from...to`. +/// If `from` is more than `to` array will contain values of reversed range. +public struct RangeVariable: Resolvable { + public let from: Resolvable + public let to: Resolvable + + public init?(_ token: String, parser: TokenParser) throws { + let components = token.components(separatedBy: "...") + guard components.count == 2 else { + return nil + } + + self.from = try parser.compileFilter(components[0]) + self.to = try parser.compileFilter(components[1]) + } + + public func resolve(_ context: Context) throws -> Any? { + let fromResolved = try from.resolve(context) + let toResolved = try to.resolve(context) + + guard let from = fromResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { + throw TemplateSyntaxError("'from' value is not an Integer (\(fromResolved ?? "nil"))") + } + + guard let to = toResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { + throw TemplateSyntaxError("'to' value is not an Integer (\(toResolved ?? "nil") )") + } + + let range = min(from, to)...max(from, to) + return from > to ? Array(range.reversed()) : Array(range) + } + +} + func normalize(_ current: Any?) -> Any? { if let current = current as? Normalizable { diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index b3d717d8..a8b90dfd 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -227,7 +227,7 @@ func testForNode() { .block(value: "for i"), ] let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `for i`.") + let error = TemplateSyntaxError("'for' statements should use the syntax: `for in [where ]") try expect(try parser.parse()).toThrow(error) } @@ -306,6 +306,11 @@ func testForNode() { try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n" } + $0.it("can iterate in range of variables") { + let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}" + try expect(try template.render(Context(dictionary: ["j": 3]))) == "123" + } + } } diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index c77122f9..a2d6815e 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -270,5 +270,22 @@ func testIfNode() { let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()])) try expect(result) == "" } + + $0.it("supports closed range variables") { + let tokens: [Token] = [ + .block(value: "if value in 1...3"), + .text(value: "true"), + .block(value: "else"), + .text(value: "false"), + .block(value: "endif") + ] + + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + + try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true" + try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false" + } + } } diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index d860433d..3ca28cbc 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -189,4 +189,52 @@ func testVariable() { try expect(result) == 2 } } + + describe("RangeVariable") { + + let context: Context = { + let ext = Extension() + ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 }) + let environment = Environment(extensions: [ext]) + return Context(dictionary: [:], environment: environment) + }() + + func makeVariable(_ token: String) throws -> RangeVariable? { + return try RangeVariable(token, parser: TokenParser(tokens: [], environment: context.environment)) + } + + $0.it("can resolve closed range as array") { + let result = try makeVariable("1...3")?.resolve(context) as? [Int] + try expect(result) == [1, 2, 3] + } + + $0.it("can resolve decreasing closed range as reversed array") { + let result = try makeVariable("3...1")?.resolve(context) as? [Int] + try expect(result) == [3, 2, 1] + } + + $0.it("can use filter on range variables") { + let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int] + try expect(result) == [2, 3, 4] + } + + $0.it("throws when left value is not int") { + let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}" + try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow() + } + + $0.it("throws when right value is not int") { + let variable = try makeVariable("k...j") + try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow() + } + + $0.it("throws is left range value is missing") { + try expect(makeVariable("...1")).toThrow() + } + + $0.it("throws is right range value is missing") { + try expect(makeVariable("1...")).toThrow() + } + + } }