From 6649b7e716bb381c22765a9dba7afdd4e9033ec6 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 23 Dec 2017 22:01:04 +0100 Subject: [PATCH 1/3] Parse variables as expressions removed static boolean expressions added test for rendering template with boolean expression --- Sources/Stencil/Expression.swift | 18 ++++++++++++------ Sources/Stencil/Node.swift | 22 ++++++++++++++-------- Tests/StencilTests/ExpressionSpec.swift | 4 ++-- Tests/StencilTests/IfNodeSpec.swift | 16 ++++++++-------- Tests/StencilTests/NodeSpec.swift | 18 ++++++++++++++++++ 5 files changed, 54 insertions(+), 24 deletions(-) diff --git a/Sources/Stencil/Expression.swift b/Sources/Stencil/Expression.swift index 1af0102..1e8ab18 100644 --- a/Sources/Stencil/Expression.swift +++ b/Sources/Stencil/Expression.swift @@ -1,7 +1,13 @@ -public protocol Expression: CustomStringConvertible { +public protocol Expression: CustomStringConvertible, Resolvable { func evaluate(context: Context) throws -> Bool } +extension Expression { + func resolve(_ context: Context) throws -> Any? { + try "\(evaluate(context: context))" + } +} + protocol InfixOperator: Expression { init(lhs: Expression, rhs: Expression) } @@ -37,8 +43,12 @@ final class VariableExpression: Expression, CustomStringConvertible { "(variable: \(variable))" } + func resolve(_ context: Context) throws -> Any? { + try variable.resolve(context) + } + /// Resolves a variable in the given context as boolean - func resolve(context: Context, variable: Resolvable) throws -> Bool { + func evaluate(context: Context) throws -> Bool { let result = try variable.resolve(context) var truthy = false @@ -58,10 +68,6 @@ final class VariableExpression: Expression, CustomStringConvertible { return truthy } - - func evaluate(context: Context) throws -> Bool { - try resolve(context: context, variable: variable) - } } final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { diff --git a/Sources/Stencil/Node.swift b/Sources/Stencil/Node.swift index f06f7ed..7998e76 100644 --- a/Sources/Stencil/Node.swift +++ b/Sources/Stencil/Node.swift @@ -93,30 +93,36 @@ public class VariableNode: NodeType { func hasToken(_ token: String, at index: Int) -> Bool { components.count > (index + 1) && components[index] == token } + func compileResolvable(_ components: [String], containedIn token: Token) throws -> Resolvable { + try (try? parser.compileExpression(components: components, token: token)) ?? + parser.compileFilter(components.joined(separator: " "), containedIn: token) + } + let variable: Resolvable let condition: Expression? let elseExpression: Resolvable? if hasToken("if", at: 1) { + variable = try compileResolvable([components[0]], containedIn: token) + let components = components.suffix(from: 2) if let elseIndex = components.firstIndex(of: "else") { condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token) - let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ") - elseExpression = try parser.compileResolvable(elseToken, containedIn: token) + let elseToken = Array(components.suffix(from: elseIndex.advanced(by: 1))) + elseExpression = try compileResolvable(elseToken, containedIn: token) } else { condition = try parser.compileExpression(components: Array(components), token: token) elseExpression = nil } - } else { + } else if !components.isEmpty { + variable = try compileResolvable(components, containedIn: token) condition = nil elseExpression = nil - } - - guard let resolvable = components.first else { + } else { throw TemplateSyntaxError(reason: "Missing variable name", token: token) } - let filter = try parser.compileResolvable(resolvable, containedIn: token) - return VariableNode(variable: filter, token: token, condition: condition, elseExpression: elseExpression) + + return VariableNode(variable: variable, token: token, condition: condition, elseExpression: elseExpression) } public init(variable: Resolvable, token: Token? = nil) { diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index 6b34697..03d50b9 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -115,12 +115,12 @@ final class ExpressionsTests: XCTestCase { func testNotExpression() { it("returns truthy for positive expressions") { - let expression = NotExpression(expression: StaticExpression(value: true)) + let expression = NotExpression(expression: VariableExpression(variable: Variable("true"))) try expect(expression.evaluate(context: Context())).to.beFalse() } it("returns falsy for negative expressions") { - let expression = NotExpression(expression: StaticExpression(value: false)) + let expression = NotExpression(expression: VariableExpression(variable: Variable("false"))) try expect(expression.evaluate(context: Context())).to.beTrue() } } diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index a4c59e9..8107b9c 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -200,8 +200,8 @@ final class IfNodeTests: XCTestCase { func testRendering() { it("renders a true expression") { let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), + IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "1")]), + IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]), IfCondition(expression: nil, nodes: [TextNode(text: "3")]) ]) @@ -210,8 +210,8 @@ final class IfNodeTests: XCTestCase { it("renders the first true expression") { let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), + IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]), IfCondition(expression: nil, nodes: [TextNode(text: "3")]) ]) @@ -220,8 +220,8 @@ final class IfNodeTests: XCTestCase { it("renders the empty expression when other conditions are falsy") { let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]), + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")]), IfCondition(expression: nil, nodes: [TextNode(text: "3")]) ]) @@ -230,8 +230,8 @@ final class IfNodeTests: XCTestCase { it("renders empty when no truthy conditions") { let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]) + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")]) ]) try expect(try node.render(Context())) == "" diff --git a/Tests/StencilTests/NodeSpec.swift b/Tests/StencilTests/NodeSpec.swift index 36f45b2..c7659f6 100644 --- a/Tests/StencilTests/NodeSpec.swift +++ b/Tests/StencilTests/NodeSpec.swift @@ -90,4 +90,22 @@ final class NodeTests: XCTestCase { try expect(try renderNodes(nodes, self.context)).toThrow(TemplateSyntaxError("Custom Error")) } } + + func testRenderingBooleans() { + it("can render true & false") { + try expect(Template(templateString: "{{ true }}").render()) == "true" + try expect(Template(templateString: "{{ false }}").render()) == "false" + } + + it("can resolve variable") { + let template = Template(templateString: "{{ value == \"known\" }}") + try expect(template.render(["value": "known"])) == "true" + try expect(template.render(["value": "unknown"])) == "false" + } + + it("can render a boolean expression") { + try expect(Template(templateString: "{{ 1 > 0 }}").render()) == "true" + try expect(Template(templateString: "{{ 1 == 2 }}").render()) == "false" + } + } } From 479fdad30b8e121f6bb51b9c7e7c1404aceb86bf Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 12 Aug 2018 20:37:10 +0100 Subject: [PATCH 2/3] Update docs --- docs/templates.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/templates.rst b/docs/templates.rst index 0e605af..c242b4b 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -50,6 +50,17 @@ For example, if you have the following context: The result of {{ item[key] }} will be the same as {{ item.name }}. It will first evaluate the result of {{ key }}, and only then evaluate the lookup expression. +Boolean expressions +------------------- + +Boolean expressions can be rendered using ``{{ ... }}`` tag. +For example, this will output string `true` if variable is equal to 1 and `false` otherwise: + +.. code-block:: html+django + + {{ variable == 1 }} + + Filters ~~~~~~~ From 0fa830c5cb45a676523da9981e180c97a0478a30 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Fri, 29 Jul 2022 01:40:23 +0200 Subject: [PATCH 3/3] Changelog entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aea0e2..2ab7021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,11 @@ - 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) +- Boolean expressions can now be rendered, i.e `{{ name == "John" }}` will render `true` or `false` depending on the evaluation result. + [Ilya Puchka](https://github.com/ilyapuchka) + [David Jennes](https://github.com/djbe) + [#164](https://github.com/stencilproject/Stencil/pull/164) + [#325](https://github.com/stencilproject/Stencil/pull/325) ### Deprecations