Added break and continue nodes

This commit is contained in:
Ilya Puchka
2017-12-23 18:54:04 +01:00
committed by David Jennes
parent 248d664d4a
commit a7448b74cf
5 changed files with 246 additions and 26 deletions

View File

@@ -56,6 +56,8 @@ class DefaultExtension: Extension {
fileprivate func registerDefaultTags() {
registerTag("for", parser: ForNode.parse)
registerTag("break", parser: LoopTerminationNode.parse)
registerTag("continue", parser: LoopTerminationNode.parse)
registerTag("if", parser: IfNode.parse)
registerTag("ifnot", parser: IfNode.parse_ifnot)
#if !os(Linux)

View File

@@ -85,9 +85,9 @@ class ForNode: NodeType {
if !values.isEmpty {
let count = values.count
var result = ""
return try zip(0..., values)
.map { index, item in
for (index, item) in zip(0..., values) {
let forContext: [String: Any] = [
"first": index == 0,
"last": index == (count - 1),
@@ -96,19 +96,26 @@ class ForNode: NodeType {
"length": count
]
return try context.push(dictionary: ["forloop": forContext]) {
try push(value: item, context: context) {
var shouldBreak = false
result += try context.push(dictionary: ["forloop": forContext]) {
defer { shouldBreak = context[LoopTerminationNode.breakContextKey] != nil }
return try push(value: item, context: context) {
try renderNodes(nodes, context)
}
}
if shouldBreak {
break
}
.joined()
}
return result
} else {
return try context.push {
try renderNodes(emptyNodes, context)
}
}
}
private func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
if loopVariables.isEmpty {
@@ -174,3 +181,53 @@ class ForNode: NodeType {
return values
}
}
struct LoopTerminationNode: NodeType {
static let breakContextKey = "_internal_forloop_break"
static let continueContextKey = "_internal_forloop_continue"
let name: String
let token: Token?
var contextKey: String {
"_internal_forloop_\(name)"
}
private init(name: String, token: Token? = nil) {
self.name = name
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")
}
guard parser.hasOpenedForTag() else {
throw TemplateSyntaxError("'\(token.contents)' can be used only inside loop body")
}
return LoopTerminationNode(name: token.contents)
}
func render(_ context: Context) throws -> String {
let offset = zip(0..., context.dictionaries).reversed().first { _, dictionary in
dictionary["forloop"] != nil
}?.0
if let offset = offset {
context.dictionaries[offset][contextKey] = true
}
return ""
}
}
private extension TokenParser {
func hasOpenedForTag() -> Bool {
var openForCount = 0
for parsedToken in parsedTokens.reversed() where parsedToken.kind == .block {
if parsedToken.components.first == "endfor" { openForCount -= 1 }
if parsedToken.components.first == "for" { openForCount += 1 }
}
return openForCount > 0
}
}

View File

@@ -11,15 +11,24 @@ public protocol NodeType {
/// Render the collection of nodes in the given context
public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String {
try nodes
.map { node in
var result = ""
for node in nodes {
do {
return try node.render(context)
result += try node.render(context)
} catch {
throw error.withToken(node.token)
}
let shouldBreak = context[LoopTerminationNode.breakContextKey] != nil
let shouldContinue = context[LoopTerminationNode.continueContextKey] != nil
if shouldBreak || shouldContinue {
break
}
.joined()
}
return result
}
/// Simple node, used for triggering a closure during rendering

View File

@@ -18,6 +18,7 @@ public class TokenParser {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
fileprivate var tokens: [Token]
fileprivate(set) var parsedTokens: [Token] = []
fileprivate let environment: Environment
fileprivate var previousWhiteSpace: WhitespaceBehaviour.Behaviour?
@@ -74,7 +75,9 @@ public class TokenParser {
/// Pop the next token (returning it)
public func nextToken() -> Token? {
if !tokens.isEmpty {
return tokens.remove(at: 0)
let nextToken = tokens.remove(at: 0)
parsedTokens.append(nextToken)
return nextToken
}
return nil
@@ -87,6 +90,9 @@ public class TokenParser {
/// Insert a token
public func prependToken(_ token: Token) {
tokens.insert(token, at: 0)
if parsedTokens.last == token {
parsedTokens.removeLast()
}
}
/// Create filter expression from a string contained in provided token

View File

@@ -312,6 +312,152 @@ final class ForNodeTests: XCTestCase {
)
try expect(try parser.parse()).toThrow(error)
}
func testBreak() {
it("can break from loop") {
let template = Template(templateString: """
{% for item in items %}\
{{ item }}{% break %}\
{% endfor %}
""")
try expect(template.render(self.context)) == """
1
"""
}
it("can break from inner node") {
let template = Template(templateString: """
{% for item in items %}\
{{ item }}\
{% if forloop.first %}<{% break %}>{% endif %}!\
{% endfor %}
""")
try expect(template.render(self.context)) == """
1<
"""
}
it("does not allow break outside loop") {
let template = Template(templateString: "{% for item in items %}{% endfor %}{% break %}")
let error = self.expectedSyntaxError(
token: "break",
template: template,
description: "'break' can be used only inside loop body"
)
try expect(template.render(self.context)).toThrow(error)
}
}
func testBreakNested() {
it("breaks outer loop") {
let template = Template(templateString: """
{% for item in items %}\
outer: {{ item }}
{% for item in items %}\
inner: {{ item }}
{% endfor %}\
{% break %}\
{% endfor %}
""")
try expect(template.render(self.context)) == """
outer: 1
inner: 1
inner: 2
inner: 3
"""
}
it("breaks inner loop") {
let template = Template(templateString: """
{% for item in items %}\
outer: {{ item }}
{% for item in items %}\
inner: {{ item }}
{% break %}\
{% endfor %}\
{% endfor %}
""")
try expect(template.render(self.context)) == """
outer: 1
inner: 1
outer: 2
inner: 1
outer: 3
inner: 1
"""
}
}
func testContinue() {
it("can continue loop") {
let template = Template(templateString: """
{% for item in items %}\
{{ item }}{% continue %}!\
{% endfor %}
""")
try expect(template.render(self.context)) == "123"
}
it("can continue from inner node") {
let template = Template(templateString: """
{% for item in items %}\
{% if forloop.last %}<{% continue %}>{% endif %}!\
{{ item }}\
{% endfor %}
""")
try expect(template.render(self.context)) == "!1!2<"
}
it("does not allow continue outside loop") {
let template = Template(templateString: "{% for item in items %}{% endfor %}{% continue %}")
let error = self.expectedSyntaxError(
token: "continue",
template: template,
description: "'continue' can be used only inside loop body"
)
try expect(template.render(self.context)).toThrow(error)
}
}
func testContinueNested() {
it("breaks outer loop") {
let template = Template(templateString: """
{% for item in items %}\
{% for item in items %}\
inner: {{ item }}\
{% endfor %}
{% continue %}
outer: {{ item }}
{% endfor %}
""")
try expect(template.render(self.context)) == """
inner: 1inner: 2inner: 3
inner: 1inner: 2inner: 3
inner: 1inner: 2inner: 3
"""
}
it("breaks inner loop") {
let template = Template(templateString: """
{% for item in items %}\
{% for item in items %}\
{% continue %}\
inner: {{ item }}
{% endfor %}\
outer: {{ item }}
{% endfor %}
""")
try expect(template.render(self.context)) == """
outer: 1
outer: 2
outer: 3
"""
}
}
}
// MARK: - Helpers