From 91df84b1a5491f920f4fcbf1d0c7ea4c4448bfd7 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Thu, 28 Dec 2017 14:17:24 +0100 Subject: [PATCH] Loop labels --- Sources/Stencil/ForTag.swift | 53 +++++++++++++++++++++----- Sources/Stencil/Parser.swift | 7 +++- Sources/Stencil/Tokenizer.swift | 7 +++- Tests/StencilTests/ForNodeSpec.swift | 56 ++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 11 deletions(-) diff --git a/Sources/Stencil/ForTag.swift b/Sources/Stencil/ForTag.swift index 3d34eb8..e735457 100644 --- a/Sources/Stencil/ForTag.swift +++ b/Sources/Stencil/ForTag.swift @@ -6,10 +6,16 @@ class ForNode: NodeType { let nodes: [NodeType] let emptyNodes: [NodeType] let `where`: Expression? + let label: String? let token: Token? 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 { components.count > (index + 1) && components[index] == token @@ -52,6 +58,7 @@ class ForNode: NodeType { nodes: forNodes, emptyNodes: emptyNodes, where: `where`, + label: label, token: token ) } @@ -62,6 +69,7 @@ class ForNode: NodeType { nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, + label: String? = nil, token: Token? = nil ) { self.resolvable = resolvable @@ -69,6 +77,7 @@ class ForNode: NodeType { self.nodes = nodes self.emptyNodes = emptyNodes self.where = `where` + self.label = label self.token = token } @@ -88,17 +97,27 @@ class ForNode: NodeType { var result = "" for (index, item) in zip(0..., values) { - let forContext: [String: Any] = [ + var forContext: [String: Any] = [ "first": index == 0, "last": index == (count - 1), "counter": index + 1, "counter0": index, "length": count ] + if let label = label { + forContext["label"] = label + } var shouldBreak = false 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) { try renderNodes(nodes, context) } @@ -187,34 +206,50 @@ struct LoopTerminationNode: NodeType { static let continueContextKey = "_internal_forloop_continue" let name: String + let label: String? let token: Token? var contextKey: String { "_internal_forloop_\(name)" } - private init(name: String, token: Token? = nil) { + private init(name: String, label: String? = nil, token: Token? = nil) { self.name = name + self.label = label self.token = token } 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 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, token: token) } func render(_ context: Context) throws -> String { 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 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 "" diff --git a/Sources/Stencil/Parser.swift b/Sources/Stencil/Parser.swift index be786be..f213ba1 100644 --- a/Sources/Stencil/Parser.swift +++ b/Sources/Stencil/Parser.swift @@ -54,8 +54,13 @@ public class TokenParser { return nodes } - if let tag = token.components.first { + if var tag = token.components.first { 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 node = try parser(self, token) nodes.append(node) diff --git a/Sources/Stencil/Tokenizer.swift b/Sources/Stencil/Tokenizer.swift index ac4c7f3..af01a15 100644 --- a/Sources/Stencil/Tokenizer.swift +++ b/Sources/Stencil/Tokenizer.swift @@ -45,7 +45,12 @@ extension String { if !components.isEmpty { 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) { components[components.count - 1] += word } else if word != "(" && word.first == "(" || word != ")" && word.first == ")" { diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index c551233..ab8def5 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -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() { it("can continue loop") { 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