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() { fileprivate func registerDefaultTags() {
registerTag("for", parser: ForNode.parse) registerTag("for", parser: ForNode.parse)
registerTag("break", parser: LoopTerminationNode.parse)
registerTag("continue", parser: LoopTerminationNode.parse)
registerTag("if", parser: IfNode.parse) registerTag("if", parser: IfNode.parse)
registerTag("ifnot", parser: IfNode.parse_ifnot) registerTag("ifnot", parser: IfNode.parse_ifnot)
#if !os(Linux) #if !os(Linux)

View File

@@ -85,9 +85,9 @@ class ForNode: NodeType {
if !values.isEmpty { if !values.isEmpty {
let count = values.count let count = values.count
var result = ""
return try zip(0..., values) for (index, item) in zip(0..., values) {
.map { index, item in
let forContext: [String: Any] = [ let forContext: [String: Any] = [
"first": index == 0, "first": index == 0,
"last": index == (count - 1), "last": index == (count - 1),
@@ -96,19 +96,26 @@ class ForNode: NodeType {
"length": count "length": count
] ]
return try context.push(dictionary: ["forloop": forContext]) { var shouldBreak = false
try push(value: item, context: context) { result += try context.push(dictionary: ["forloop": forContext]) {
defer { shouldBreak = context[LoopTerminationNode.breakContextKey] != nil }
return try push(value: item, context: context) {
try renderNodes(nodes, context) try renderNodes(nodes, context)
} }
} }
if shouldBreak {
break
} }
.joined()
} }
return result
} else {
return try context.push { return try context.push {
try renderNodes(emptyNodes, context) try renderNodes(emptyNodes, context)
} }
} }
}
private func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result { private func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
if loopVariables.isEmpty { if loopVariables.isEmpty {
@@ -174,3 +181,53 @@ class ForNode: NodeType {
return values 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 /// Render the collection of nodes in the given context
public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String { public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String {
try nodes var result = ""
.map { node in
for node in nodes {
do { do {
return try node.render(context) result += try node.render(context)
} catch { } catch {
throw error.withToken(node.token) 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 /// 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 public typealias TagParser = (TokenParser, Token) throws -> NodeType
fileprivate var tokens: [Token] fileprivate var tokens: [Token]
fileprivate(set) var parsedTokens: [Token] = []
fileprivate let environment: Environment fileprivate let environment: Environment
fileprivate var previousWhiteSpace: WhitespaceBehaviour.Behaviour? fileprivate var previousWhiteSpace: WhitespaceBehaviour.Behaviour?
@@ -74,7 +75,9 @@ public class TokenParser {
/// Pop the next token (returning it) /// Pop the next token (returning it)
public func nextToken() -> Token? { public func nextToken() -> Token? {
if !tokens.isEmpty { if !tokens.isEmpty {
return tokens.remove(at: 0) let nextToken = tokens.remove(at: 0)
parsedTokens.append(nextToken)
return nextToken
} }
return nil return nil
@@ -87,6 +90,9 @@ public class TokenParser {
/// Insert a token /// Insert a token
public func prependToken(_ token: Token) { public func prependToken(_ token: Token) {
tokens.insert(token, at: 0) tokens.insert(token, at: 0)
if parsedTokens.last == token {
parsedTokens.removeLast()
}
} }
/// Create filter expression from a string contained in provided token /// 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) 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 // MARK: - Helpers