Added break and continue nodes
This commit is contained in:
committed by
David Jennes
parent
248d664d4a
commit
a7448b74cf
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user