Skip to content

Commit

Permalink
implemented parsing of range literals
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyapuchka authored and kylef committed Apr 5, 2018
1 parent 2e6a721 commit d6aa51d
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 16 additions & 13 deletions Sources/ForTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ 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 <x> in <y> [where <condition>]")
}

let loopVariables = components[1].characters
.split(separator: ",")
.map(String.init)
.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }

let variable = components[3]

var emptyNodes = [NodeType]()

let forNodes = try parser.parse(until(["endfor", "empty"]))
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/IfTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ final class IfExpressionParser {
}
}

return .variable(try tokenParser.compileFilter(component))
return .variable(try tokenParser.compileResolvable(component))
}
}

Expand Down
7 changes: 6 additions & 1 deletion Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions Sources/Variable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion Tests/StencilTests/ForNodeSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 <x> in <y> [where <condition>]")
try expect(try parser.parse()).toThrow(error)
}

Expand Down Expand Up @@ -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"
}

}

}
Expand Down
17 changes: 17 additions & 0 deletions Tests/StencilTests/IfNodeSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

}
}
48 changes: 48 additions & 0 deletions Tests/StencilTests/VariableSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

}
}

0 comments on commit d6aa51d

Please sign in to comment.