Merge pull request #175 from stencilproject/break-continue
Break, continue and loops' labels
This commit is contained in:
@@ -30,6 +30,12 @@
|
|||||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#158](https://github.com/stencilproject/Stencil/issues/158)
|
[#158](https://github.com/stencilproject/Stencil/issues/158)
|
||||||
[#182](https://github.com/stencilproject/Stencil/pull/182)
|
[#182](https://github.com/stencilproject/Stencil/pull/182)
|
||||||
|
- Added `break` and `continue` tags to break or continue current loop.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#175](https://github.com/stencilproject/Stencil/pull/175)
|
||||||
|
- You can now access outer loop's scope by labeling it: `{% outer: for ... %}... {% for ... %} {{ outer.counter }} {% endfor %}{% endfor %}`.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#175](https://github.com/stencilproject/Stencil/pull/175)
|
||||||
|
|
||||||
### Deprecations
|
### Deprecations
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,28 +94,51 @@ class ForNode: NodeType {
|
|||||||
|
|
||||||
if !values.isEmpty {
|
if !values.isEmpty {
|
||||||
let count = values.count
|
let count = values.count
|
||||||
|
var result = ""
|
||||||
|
|
||||||
return try zip(0..., values)
|
// collect parent loop contexts
|
||||||
.map { index, item in
|
let parentLoopContexts = (context["forloop"] as? [String: Any])?
|
||||||
let forContext: [String: Any] = [
|
.filter { ($1 as? [String: Any])?["label"] != nil } ?? [:]
|
||||||
"first": index == 0,
|
|
||||||
"last": index == (count - 1),
|
|
||||||
"counter": index + 1,
|
|
||||||
"counter0": index,
|
|
||||||
"length": count
|
|
||||||
]
|
|
||||||
|
|
||||||
return try context.push(dictionary: ["forloop": forContext]) {
|
for (index, item) in zip(0..., values) {
|
||||||
try push(value: item, context: context) {
|
var forContext: [String: Any] = [
|
||||||
try renderNodes(nodes, context)
|
"first": index == 0,
|
||||||
|
"last": index == (count - 1),
|
||||||
|
"counter": index + 1,
|
||||||
|
"counter0": index,
|
||||||
|
"length": count
|
||||||
|
]
|
||||||
|
if let label = label {
|
||||||
|
forContext["label"] = label
|
||||||
|
forContext[label] = forContext
|
||||||
|
}
|
||||||
|
forContext.merge(parentLoopContexts) { lhs, _ in lhs }
|
||||||
|
|
||||||
|
var shouldBreak = false
|
||||||
|
result += try context.push(dictionary: ["forloop": forContext]) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.joined()
|
|
||||||
}
|
|
||||||
|
|
||||||
return try context.push {
|
if shouldBreak {
|
||||||
try renderNodes(emptyNodes, context)
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
return try context.push {
|
||||||
|
try renderNodes(emptyNodes, context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,3 +206,69 @@ 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 label: String?
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
|
var contextKey: String {
|
||||||
|
"_internal_forloop_\(name)"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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: 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
|
||||||
|
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] = 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 ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
|
||||||
do {
|
for node in nodes {
|
||||||
return try node.render(context)
|
do {
|
||||||
} catch {
|
result += try node.render(context)
|
||||||
throw error.withToken(node.token)
|
} catch {
|
||||||
}
|
throw error.withToken(node.token)
|
||||||
}
|
}
|
||||||
.joined()
|
|
||||||
|
let shouldBreak = context[LoopTerminationNode.breakContextKey] != nil
|
||||||
|
let shouldContinue = context[LoopTerminationNode.continueContextKey] != nil
|
||||||
|
|
||||||
|
if shouldBreak || shouldContinue {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
@@ -53,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)
|
||||||
@@ -74,7 +80,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 +95,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
|
||||||
|
|||||||
@@ -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 == ")" {
|
||||||
|
|||||||
@@ -312,6 +312,253 @@ 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 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: """
|
||||||
|
{% 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
|
||||||
|
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccessLabeled() {
|
||||||
|
it("can access labeled outer loop context from inner loop") {
|
||||||
|
let template = Template(templateString: """
|
||||||
|
{% outer: for item in 1...2 %}\
|
||||||
|
{% for item in items %}\
|
||||||
|
{{ forloop.counter }}-{{ forloop.outer.counter }},\
|
||||||
|
{% endfor %}---\
|
||||||
|
{% endfor %}
|
||||||
|
""")
|
||||||
|
try expect(template.render(self.context)) == """
|
||||||
|
1-1,2-1,3-1,---1-2,2-2,3-2,---
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can access labeled outer loop from double inner loop") {
|
||||||
|
let template = Template(templateString: """
|
||||||
|
{% outer: for item in 1...2 %}{% for item in 1...2 %}\
|
||||||
|
{% for item in items %}\
|
||||||
|
{{ forloop.counter }}-{{ forloop.outer.counter }},\
|
||||||
|
{% endfor %}---{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
""")
|
||||||
|
try expect(template.render(self.context)) == """
|
||||||
|
1-1,2-1,3-1,---1-1,2-1,3-1,---
|
||||||
|
1-2,2-2,3-2,---1-2,2-2,3-2,---
|
||||||
|
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can access two labeled outer loop contexts from inner loop") {
|
||||||
|
let template = Template(templateString: """
|
||||||
|
{% outer1: for item in 1...2 %}{% outer2: for item in 1...2 %}\
|
||||||
|
{% for item in items %}\
|
||||||
|
{{ forloop.counter }}-{{ forloop.outer2.counter }}-{{ forloop.outer1.counter }},\
|
||||||
|
{% endfor %}---{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
""")
|
||||||
|
try expect(template.render(self.context)) == """
|
||||||
|
1-1-1,2-1-1,3-1-1,---1-2-1,2-2-1,3-2-1,---
|
||||||
|
1-1-2,2-1-2,3-1-2,---1-2-2,2-2-2,3-2-2,---
|
||||||
|
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ You can iterate over range literals created using ``N...M`` syntax, both in asce
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
The ``for`` tag can contain optional ``where`` expression to filter out
|
The ``for`` tag can contain optional ``where`` expression to filter out elements on which this expression evaluates to false.
|
||||||
elements on which this expression evaluates to false.
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -52,8 +51,7 @@ elements on which this expression evaluates to false.
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
The ``for`` tag can take an optional ``{% empty %}`` block that will be
|
The ``for`` tag can take an optional ``{% empty %}`` block that will be displayed if the given list is empty or could not be found.
|
||||||
displayed if the given list is empty or could not be found.
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -89,12 +87,74 @@ For example:
|
|||||||
This is user number {{ forloop.counter }} user.
|
This is user number {{ forloop.counter }} user.
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
The ``for`` tag accepts an optional label, so that it may later be referred to by name. The contexts of parent labeled loops can be accessed via the `forloop` property:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% outer: for item in users %}
|
||||||
|
{% for item in 1..3 %}
|
||||||
|
{% if forloop.outer.first %}
|
||||||
|
This is the first user.
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
``break``
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
The ``break`` tag lets you jump out of a for loop, for example if a certain condition is met:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% for user in users %}
|
||||||
|
{% if user.inaccessible %}
|
||||||
|
{% break %}
|
||||||
|
{% endif %}
|
||||||
|
This is user {{ user.name }}.
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
Break tags accept an optional label parameter, so that you may break out of multiple loops:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% outer: for user in users %}
|
||||||
|
{% for address in user.addresses %}
|
||||||
|
{% if address.isInvalid %}
|
||||||
|
{% break outer %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
``continue``
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
The ``continue`` tag lets you skip the rest of the blocks in a loop, for example if a certain condition is met:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% for user in users %}
|
||||||
|
{% if user.inaccessible %}
|
||||||
|
{% continue %}
|
||||||
|
{% endif %}
|
||||||
|
This is user {{ user.name }}.
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
Continue tags accept an optional label parameter, so that you may skip the execution of multiple loops:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% outer: for user in users %}
|
||||||
|
{% for address in user.addresses %}
|
||||||
|
{% if address.isInvalid %}
|
||||||
|
{% continue outer %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
``if``
|
``if``
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
The ``{% if %}`` tag evaluates a variable, and if that variable evaluates to
|
The ``{% if %}`` tag evaluates a variable, and if that variable evaluates to true the contents of the block are processed. Being true is defined as:
|
||||||
true the contents of the block are processed. Being true is defined as:
|
|
||||||
|
|
||||||
* Present in the context
|
* Present in the context
|
||||||
* Being non-empty (dictionaries or arrays)
|
* Being non-empty (dictionaries or arrays)
|
||||||
@@ -115,8 +175,7 @@ true the contents of the block are processed. Being true is defined as:
|
|||||||
Operators
|
Operators
|
||||||
^^^^^^^^^
|
^^^^^^^^^
|
||||||
|
|
||||||
``if`` tags may combine ``and``, ``or`` and ``not`` to test multiple variables
|
``if`` tags may combine ``and``, ``or`` and ``not`` to test multiple variables or to negate a variable.
|
||||||
or to negate a variable.
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -279,8 +338,7 @@ By default the included file gets passed the current context. You can pass a sub
|
|||||||
|
|
||||||
{% include "comment.html" comment %}
|
{% include "comment.html" comment %}
|
||||||
|
|
||||||
The `include` tag requires you to provide a loader which will be used to lookup
|
The `include` tag requires you to provide a loader which will be used to lookup the template.
|
||||||
the template.
|
|
||||||
|
|
||||||
.. code-block:: swift
|
.. code-block:: swift
|
||||||
|
|
||||||
@@ -301,8 +359,7 @@ See :ref:`template-inheritance` for more information.
|
|||||||
``block``
|
``block``
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
Defines a block that can be overridden by child templates. See
|
Defines a block that can be overridden by child templates. See :ref:`template-inheritance` for more information.
|
||||||
:ref:`template-inheritance` for more information.
|
|
||||||
|
|
||||||
.. _built-in-filters:
|
.. _built-in-filters:
|
||||||
|
|
||||||
@@ -312,8 +369,7 @@ Built-in Filters
|
|||||||
``capitalize``
|
``capitalize``
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The capitalize filter allows you to capitalize a string.
|
The capitalize filter allows you to capitalize a string. For example, `stencil` to `Stencil`. Can be applied to array of strings to change each string.
|
||||||
For example, `stencil` to `Stencil`. Can be applied to array of strings to change each string.
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -322,8 +378,7 @@ For example, `stencil` to `Stencil`. Can be applied to array of strings to chang
|
|||||||
``uppercase``
|
``uppercase``
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
The uppercase filter allows you to transform a string to uppercase.
|
The uppercase filter allows you to transform a string to uppercase. For example, `Stencil` to `STENCIL`. Can be applied to array of strings to change each string.
|
||||||
For example, `Stencil` to `STENCIL`. Can be applied to array of strings to change each string.
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -332,8 +387,7 @@ For example, `Stencil` to `STENCIL`. Can be applied to array of strings to chang
|
|||||||
``lowercase``
|
``lowercase``
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
The uppercase filter allows you to transform a string to lowercase.
|
The uppercase filter allows you to transform a string to lowercase. For example, `Stencil` to `stencil`. Can be applied to array of strings to change each string.
|
||||||
For example, `Stencil` to `stencil`. Can be applied to array of strings to change each string.
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -342,8 +396,7 @@ For example, `Stencil` to `stencil`. Can be applied to array of strings to chang
|
|||||||
``default``
|
``default``
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
If a variable not present in the context, use given default. Otherwise, use the
|
If a variable not present in the context, use given default. Otherwise, use the value of the variable. For example:
|
||||||
value of the variable. For example:
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user