Skip to content

Commit

Permalink
loop labels
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyapuchka committed Dec 28, 2017
1 parent eb98323 commit 87cd89f
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 17 deletions.
56 changes: 43 additions & 13 deletions Sources/ForTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ class ForNode : NodeType {
let nodes:[NodeType]
let emptyNodes: [NodeType]
let `where`: Expression?
let label: String?

class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
var components = token.components()

var label: String? = nil
if components.first?.hasSuffix(":") == true {
label = String(components.removeFirst().dropLast())
}

guard components.count >= 3 && components[2] == "in" &&
(components.count == 4 || (components.count >= 6 && components[4] == "where")) else {
Expand Down Expand Up @@ -42,15 +48,16 @@ class ForNode : NodeType {
} else {
`where` = nil
}
return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, label: label)
}

init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) {
init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil, label: String? = nil) {
self.resolvable = resolvable
self.loopVariables = loopVariables
self.nodes = nodes
self.emptyNodes = emptyNodes
self.where = `where`
self.label = label
}

func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result {
Expand Down Expand Up @@ -111,19 +118,26 @@ class ForNode : NodeType {
var result = ""

for (index, item) in values.enumerated() {
let forContext: [String: Any] = [
var forContext: [String: Any] = [
"first": index == 0,
"last": index == (count - 1),
"counter": index + 1,
"counter0": index,
"counter0": index
]
if let label = label {
forContext["label"] = label
}

var shouldBreak: Bool = false
result += try context.push(dictionary: ["forloop": forContext]) {
let result = try push(value: item, context: context) {
try renderNodes(nodes, context)
}
shouldBreak = context[LoopTerminationNode.break.terminator] as? Bool ?? false
shouldBreak = context[LoopTerminationNode.break.terminator] != nil
// if outer loop should be continued we should break from current loop
if let shouldContinueLabel = context[LoopTerminationNode.continue.terminator] as? String {
shouldBreak = shouldContinueLabel != label || label == nil
}
return result
}
if shouldBreak { break }
Expand All @@ -142,31 +156,47 @@ struct LoopTerminationNode: NodeType {
static let `continue` = LoopTerminationNode(name: "continue")

let name: String
let label: String?
var terminator: String {
return "forloop_\(name)"
}

private init(name: String) {
private init(name: String, label: String? = nil) {
self.name = name
self.label = label
}

static func parse(_ parser:TokenParser, token:Token) throws -> LoopTerminationNode {
guard token.components().count == 1 else {
throw TemplateSyntaxError("'\(token.contents)' does not accept parameters")
let components = token.components()

guard token.components().count <= 2 else {
throw TemplateSyntaxError("'\(token.contents)' can accept only one parameter")
}
guard parser.hasOpenedForTag() else {
throw TemplateSyntaxError("'\(token.contents)' can be used only inside loop body")
}
return LoopTerminationNode(name: token.contents)
return LoopTerminationNode(name: components[0], label: components.count == 2 ? components[1] : nil)
}

func render(_ context: Context) throws -> String {
guard let offset = context.dictionaries.reversed().enumerated().first(where: {
$0.element["forloop"] != nil
})?.offset else { return "" }
guard let forContext = $0.element["forloop"] as? [String: Any] else { return false }
guard $0.element["forloop"] != nil else { return false }
if let label = label {
return label == forContext["label"] as? String
} else {
return true
}
})?.offset else {
if let label = label {
throw TemplateSyntaxError("No loop labeled '\(label)' is currently running")
} else {
throw TemplateSyntaxError("No loop is currently running")
}
}

let depth = context.dictionaries.count - offset - 1
context.dictionaries[depth][terminator] = true
context.dictionaries[depth][terminator] = label ?? true

return ""
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String
for node in nodes {
renderedNodes += try node.render(context)

let shouldBreak = context[LoopTerminationNode.break.terminator] as? Bool ?? false
let shouldContinue = context[LoopTerminationNode.continue.terminator] as? Bool ?? false
let shouldBreak = context[LoopTerminationNode.break.terminator] != nil
let shouldContinue = context[LoopTerminationNode.continue.terminator] != nil

if shouldBreak || shouldContinue {
break
Expand Down
6 changes: 5 additions & 1 deletion Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ public class TokenParser {
return nodes
}

if let tag = token.components().first {
let components = token.components()
if var tag = components.first {
if tag.hasSuffix(":") && components.count >= 2 {
tag = components[1]
}
let parser = try findTag(name: tag)
nodes.append(try parser(self, token))
}
Expand Down
46 changes: 45 additions & 1 deletion Tests/StencilTests/ForNodeSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func testForNode() {

try expect(template.render(context)) == "outer: 1\ninner: 1\nouter: 2\ninner: 1\nouter: 3\ninner: 1\n\n"
}

$0.it("continues outer loop") {
let template = Template(templateString: "{% for item in items %}" +
"{% for item in items %}" +
Expand All @@ -277,6 +277,50 @@ func testForNode() {

try expect(template.render(context)) == "outer: 1\nouter: 2\nouter: 3\n\n"
}

$0.context("given labeled loop") {

$0.it("breaks labeled loop") {
let template = Template(templateString: "{% outer: for item in items %}" +
"outer: {{ item }}\n" +
"{% for item in items %}" +
"{% break outer %}" +
"inner: {{ item }}\n" +
"{% endfor %}" +
"{% endfor %}\n")

try expect(template.render(context)) == "outer: 1\n\n"
}

$0.it("continues labeled loop") {
let template = Template(templateString: "{% outer: for item in items %}" +
"{% for item in items %}" +
"inner: {{ item }}\n" +
"{% continue outer %}" +
"{% endfor %}" +
"outer: {{ item }}\n" +
"{% endfor %}\n")

try expect(template.render(context)) == "inner: 1\ninner: 1\ninner: 1\n\n"
}

$0.it("throws when breaking with unknown label") {
let template = Template(templateString: "{% outer: for item in items %}" +
"{% break inner %}" +
"{% endfor %}\n")

try expect(template.render(context)).toThrow()
}

$0.it("throws when continuing with unknown label") {
let template = Template(templateString: "{% outer: for item in items %}" +
"{% continue inner %}" +
"{% endfor %}\n")

try expect(template.render(context)).toThrow()
}

}
}

}
Expand Down

0 comments on commit 87cd89f

Please sign in to comment.