Loop labels

This commit is contained in:
Ilya Puchka
2017-12-28 14:17:24 +01:00
committed by David Jennes
parent a7448b74cf
commit 91df84b1a5
4 changed files with 112 additions and 11 deletions

View File

@@ -6,10 +6,16 @@ class ForNode: NodeType {
let nodes: [NodeType] let nodes: [NodeType]
let emptyNodes: [NodeType] let emptyNodes: [NodeType]
let `where`: Expression? let `where`: Expression?
let label: String?
let token: Token? let token: Token?
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
let components = token.components var components = token.components
var label: String?
if components.first?.hasSuffix(":") == true {
label = String(components.removeFirst().dropLast())
}
func hasToken(_ token: String, at index: Int) -> Bool { func hasToken(_ token: String, at index: Int) -> Bool {
components.count > (index + 1) && components[index] == token components.count > (index + 1) && components[index] == token
@@ -52,6 +58,7 @@ class ForNode: NodeType {
nodes: forNodes, nodes: forNodes,
emptyNodes: emptyNodes, emptyNodes: emptyNodes,
where: `where`, where: `where`,
label: label,
token: token token: token
) )
} }
@@ -62,6 +69,7 @@ class ForNode: NodeType {
nodes: [NodeType], nodes: [NodeType],
emptyNodes: [NodeType], emptyNodes: [NodeType],
where: Expression? = nil, where: Expression? = nil,
label: String? = nil,
token: Token? = nil token: Token? = nil
) { ) {
self.resolvable = resolvable self.resolvable = resolvable
@@ -69,6 +77,7 @@ class ForNode: NodeType {
self.nodes = nodes self.nodes = nodes
self.emptyNodes = emptyNodes self.emptyNodes = emptyNodes
self.where = `where` self.where = `where`
self.label = label
self.token = token self.token = token
} }
@@ -88,17 +97,27 @@ class ForNode: NodeType {
var result = "" var result = ""
for (index, item) in zip(0..., values) { for (index, item) in zip(0..., values) {
let forContext: [String: Any] = [ var forContext: [String: Any] = [
"first": index == 0, "first": index == 0,
"last": index == (count - 1), "last": index == (count - 1),
"counter": index + 1, "counter": index + 1,
"counter0": index, "counter0": index,
"length": count "length": count
] ]
if let label = label {
forContext["label"] = label
}
var shouldBreak = false var shouldBreak = false
result += try context.push(dictionary: ["forloop": forContext]) { result += try context.push(dictionary: ["forloop": forContext]) {
defer { shouldBreak = context[LoopTerminationNode.breakContextKey] != nil } defer {
// if outer loop should be continued we should break from current loop
if let shouldContinueLabel = context[LoopTerminationNode.continueContextKey] as? String {
shouldBreak = shouldContinueLabel != label || label == nil
} else {
shouldBreak = context[LoopTerminationNode.breakContextKey] != nil
}
}
return try push(value: item, context: context) { return try push(value: item, context: context) {
try renderNodes(nodes, context) try renderNodes(nodes, context)
} }
@@ -187,34 +206,50 @@ struct LoopTerminationNode: NodeType {
static let continueContextKey = "_internal_forloop_continue" static let continueContextKey = "_internal_forloop_continue"
let name: String let name: String
let label: String?
let token: Token? let token: Token?
var contextKey: String { var contextKey: String {
"_internal_forloop_\(name)" "_internal_forloop_\(name)"
} }
private init(name: String, token: Token? = nil) { private init(name: String, label: String? = nil, token: Token? = nil) {
self.name = name self.name = name
self.label = label
self.token = token self.token = token
} }
static func parse(_ parser: TokenParser, token: Token) throws -> LoopTerminationNode { static func parse(_ parser: TokenParser, token: Token) throws -> LoopTerminationNode {
guard token.components.count == 1 else { let components = token.components
throw TemplateSyntaxError("'\(token.contents)' does not accept parameters")
guard components.count <= 2 else {
throw TemplateSyntaxError("'\(token.contents)' can accept only one parameter")
} }
guard parser.hasOpenedForTag() else { guard parser.hasOpenedForTag() else {
throw TemplateSyntaxError("'\(token.contents)' can be used only inside loop body") 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, token: token)
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {
let offset = zip(0..., context.dictionaries).reversed().first { _, dictionary in let offset = zip(0..., context.dictionaries).reversed().first { _, dictionary in
dictionary["forloop"] != nil guard let forContext = dictionary["forloop"] as? [String: Any],
dictionary["forloop"] != nil else { return false }
if let label = label {
return label == forContext["label"] as? String
} else {
return true
}
}?.0 }?.0
if let offset = offset { if let offset = offset {
context.dictionaries[offset][contextKey] = true context.dictionaries[offset][contextKey] = label ?? true
} else if let label = label {
throw TemplateSyntaxError("No loop labeled '\(label)' is currently running")
} else {
throw TemplateSyntaxError("No loop is currently running")
} }
return "" return ""

View File

@@ -54,8 +54,13 @@ public class TokenParser {
return nodes return nodes
} }
if let tag = token.components.first { if var tag = token.components.first {
do { do {
// special case for labeled tags (such as for loops)
if tag.hasSuffix(":") && token.components.count >= 2 {
tag = token.components[1]
}
let parser = try environment.findTag(name: tag) let parser = try environment.findTag(name: tag)
let node = try parser(self, token) let node = try parser(self, token)
nodes.append(node) nodes.append(node)

View File

@@ -45,7 +45,12 @@ extension String {
if !components.isEmpty { if !components.isEmpty {
if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) { if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
components[components.count - 1] += word // special case for labeled for-loops
if components.count == 1 && word == "for" {
components.append(word)
} else {
components[components.count - 1] += word
}
} else if specialCharacters.contains(word) { } else if specialCharacters.contains(word) {
components[components.count - 1] += word components[components.count - 1] += word
} else if word != "(" && word.first == "(" || word != ")" && word.first == ")" { } else if word != "(" && word.first == "(" || word != ")" && word.first == ")" {

View File

@@ -390,6 +390,33 @@ final class ForNodeTests: XCTestCase {
} }
} }
func testBreakLabeled() {
it("breaks labeled loop") {
let template = Template(templateString: """
{% outer: for item in items %}\
outer: {{ item }}
{% for item in items %}\
{% break outer %}\
inner: {{ item }}
{% endfor %}\
{% endfor %}
""")
try expect(template.render(self.context)) == """
outer: 1
"""
}
it("throws when breaking with unknown label") {
let template = Template(templateString: """
{% outer: for item in items %}
{% break inner %}
{% endfor %}
""")
try expect(template.render(self.context)).toThrow()
}
}
func testContinue() { func testContinue() {
it("can continue loop") { it("can continue loop") {
let template = Template(templateString: """ let template = Template(templateString: """
@@ -458,6 +485,35 @@ final class ForNodeTests: XCTestCase {
""" """
} }
} }
func testContinueLabeled() {
it("continues labeled loop") {
let template = Template(templateString: """
{% outer: for item in items %}\
{% for item in items %}\
inner: {{ item }}
{% continue outer %}\
{% endfor %}\
outer: {{ item }}
{% endfor %}
""")
try expect(template.render(self.context)) == """
inner: 1
inner: 1
inner: 1
"""
}
it("throws when continuing with unknown label") {
let template = Template(templateString: """
{% outer: for item in items %}
{% continue inner %}
{% endfor %}
""")
try expect(template.render(self.context)).toThrow()
}
}
} }
// MARK: - Helpers // MARK: - Helpers