Loop labels
This commit is contained in:
committed by
David Jennes
parent
a7448b74cf
commit
91df84b1a5
@@ -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 ""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 == ")" {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user