feat(for loop): Support range literals (#192)

This commit is contained in:
Ilya Puchka
2018-04-05 01:56:58 +01:00
committed by Kyle Fuller
parent 2e6a7215c5
commit fe01beb4bb
8 changed files with 131 additions and 16 deletions

View File

@@ -15,6 +15,7 @@
- Added support for iterating arrays of tuples - Added support for iterating arrays of tuples
- Added support for ranges in if-in expression - Added support for ranges in if-in expression
- Added property `forloop.length` to get number of items in the loop - 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 ### Bug Fixes

View File

@@ -10,9 +10,15 @@ class ForNode : NodeType {
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 >= 3 && components[2] == "in" && func hasToken(_ token: String, at index: Int) -> Bool {
(components.count == 4 || (components.count >= 6 && components[4] == "where")) else { return components.count > (index + 1) && components[index] == token
throw TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `\(token.contents)`.") }
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 <x> in <y> [where <condition>]")
} }
let loopVariables = components[1].characters let loopVariables = components[1].characters
@@ -20,8 +26,6 @@ class ForNode : NodeType {
.map(String.init) .map(String.init)
.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) } .map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }
let variable = components[3]
var emptyNodes = [NodeType]() var emptyNodes = [NodeType]()
let forNodes = try parser.parse(until(["endfor", "empty"])) let forNodes = try parser.parse(until(["endfor", "empty"]))
@@ -35,14 +39,13 @@ class ForNode : NodeType {
_ = parser.nextToken() _ = parser.nextToken()
} }
let filter = try parser.compileFilter(variable) let resolvable = try parser.compileResolvable(components[3])
let `where`: Expression?
if components.count >= 6 { let `where` = hasToken("where", at: 4)
`where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser) ? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
} else { : nil
`where` = nil
} return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
} }
init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) { init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) {

View File

@@ -111,7 +111,7 @@ final class IfExpressionParser {
} }
} }
return .variable(try tokenParser.compileFilter(component)) return .variable(try tokenParser.compileResolvable(component))
} }
} }

View File

@@ -40,7 +40,7 @@ public class TokenParser {
case .text(let text): case .text(let text):
nodes.append(TextNode(text: text)) nodes.append(TextNode(text: text))
case .variable: case .variable:
nodes.append(VariableNode(variable: try compileFilter(token.contents))) nodes.append(VariableNode(variable: try compileResolvable(token.contents)))
case .block: case .block:
if let parse_until = parse_until , parse_until(self, token) { if let parse_until = parse_until , parse_until(self, token) {
prependToken(token) prependToken(token)
@@ -114,6 +114,11 @@ public class TokenParser {
return try FilterExpression(token: token, parser: self) 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 // https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows

View File

@@ -130,6 +130,42 @@ public func ==(lhs: Variable, rhs: Variable) -> Bool {
return lhs.variable == rhs.variable 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? { func normalize(_ current: Any?) -> Any? {
if let current = current as? Normalizable { if let current = current as? Normalizable {

View File

@@ -227,7 +227,7 @@ func testForNode() {
.block(value: "for i"), .block(value: "for i"),
] ]
let parser = TokenParser(tokens: tokens, environment: Environment()) 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 <x> in <y> [where <condition>]")
try expect(try parser.parse()).toThrow(error) try expect(try parser.parse()).toThrow(error)
} }
@@ -306,6 +306,11 @@ func testForNode() {
try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n" 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"
}
} }
} }

View File

@@ -270,5 +270,22 @@ func testIfNode() {
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()])) let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
try expect(result) == "" 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"
}
} }
} }

View File

@@ -189,4 +189,52 @@ func testVariable() {
try expect(result) == 2 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()
}
}
} }