diff --git a/CHANGELOG.md b/CHANGELOG.md index d84c9b50..057283a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,11 @@ [David Jennes](https://github.com/djbe) [#215](https://github.com/stencilproject/Stencil/pull/215) - Adds support for using spaces in filter expression. - [Ilya Puchka](https://github.com/yonaskolb) + [Ilya Puchka](https://github.com/ilyapuchka) [#178](https://github.com/stencilproject/Stencil/pull/178) +- Now boolean expressions results can be rendered, i.e `{{ name == "John" }}` will render `true` or `false` depending on the evaluation result. + [Ilya Puchka](https://github.com/ilyapuchka) + [#164](https://github.com/stencilproject/Stencil/pull/164) ### Bug Fixes diff --git a/Sources/Expression.swift b/Sources/Expression.swift index c7199fc6..e4e7f87a 100644 --- a/Sources/Expression.swift +++ b/Sources/Expression.swift @@ -1,7 +1,14 @@ -protocol Expression: CustomStringConvertible { +protocol Expression: CustomStringConvertible, Resolvable { func evaluate(context: Context) throws -> Bool } +extension Expression { + + func resolve(_ context: Context) throws -> Any? { + return try "\(evaluate(context: context))" + } + +} protocol InfixOperator: Expression { init(lhs: Expression, rhs: Expression) @@ -40,9 +47,13 @@ final class VariableExpression: Expression, CustomStringConvertible { var description: String { return "(variable: \(variable))" } - + + func resolve(_ context: Context) throws -> Any? { + return 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 @@ -63,9 +74,6 @@ final class VariableExpression: Expression, CustomStringConvertible { return truthy } - func evaluate(context: Context) throws -> Bool { - return try resolve(context: context, variable: variable) - } } diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 81a44e10..a2cf0e50 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -40,7 +40,12 @@ public class TokenParser { case .text(let text): nodes.append(TextNode(text: text)) case .variable: - nodes.append(VariableNode(variable: try compileResolvable(token.contents))) + do { + let expression = try parseExpression(components: token.components(), tokenParser: self) + nodes.append(VariableNode(variable: expression)) + } catch { + nodes.append(VariableNode(variable: try compileFilter(token.contents))) + } case .block: if let parse_until = parse_until , parse_until(self, token) { prependToken(token) diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index c41575f4..200d4076 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -9,6 +9,14 @@ func testExpressions() { $0.describe("VariableExpression") { let expression = VariableExpression(variable: Variable("value")) + $0.it("can resolve variable") { + let context = Context(dictionary: ["value": "known"]) + try expect(expression.resolve(context) as? String) == "known" + + try expect(Template(templateString: "{{ value == \"known\" }}").render(["value": "known"])) == "true" + try expect(Template(templateString: "{{ value == \"known\" }}").render(["value": "unknown"])) == "false" + } + $0.it("evaluates to true when value is not nil") { let context = Context(dictionary: ["value": "known"]) try expect(try expression.evaluate(context: context)).to.beTrue() @@ -93,12 +101,12 @@ func testExpressions() { $0.describe("NotExpression") { $0.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() } $0.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() } } @@ -136,167 +144,177 @@ func testExpressions() { } } + func expectExpression(with components: [String], context: [String: Any], toBe expected: Bool, + file: String = #file, line: Int = #line, function: String = #function) throws { + let expression = try parseExpression(components: components, tokenParser: parser) + try expect(expression.evaluate(context: Context(dictionary: context)), file: file, line: line, function: function) == expected + + let template = Template(templateString: "{{ \(components.joined(separator: " ")) }}") + let result = try template.render(context) + try expect(result, file: file, line: line, function: function) == String(expected) + } + $0.describe("or expression") { - let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser) + let components = ["lhs", "or", "rhs"] $0.it("evaluates to true with lhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": true, "rhs": false], toBe: true) } $0.it("evaluates to true with rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": false, "rhs": true], toBe: true) } $0.it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": true, "rhs": true], toBe: true) } $0.it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": false, "rhs": false], toBe: false) } } $0.describe("equality expression") { - let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser) + let components = ["lhs", "==", "rhs"] $0.it("evaluates to true with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": "a", "rhs": "a"], toBe: true) } $0.it("evaluates to false with non equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": "a", "rhs": "b"], toBe: false) } $0.it("evaluates to true with nils") { - try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue() + try expectExpression(with: components, context: [:], toBe: true) } $0.it("evaluates to true with numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": 1, "rhs": 1.0], toBe: true) } $0.it("evaluates to false with non equal numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": 1, "rhs": 1.1], toBe: false) } $0.it("evaluates to true with booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": true, "rhs": true], toBe: true) } $0.it("evaluates to false with falsy booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": true, "rhs": false], toBe: false) } $0.it("evaluates to false with different types") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": true, "rhs": 1], toBe: false) } } $0.describe("inequality expression") { - let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser) + let components = ["lhs", "!=", "rhs"] $0.it("evaluates to true with inequal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": "a", "rhs": "b"], toBe: true) } $0.it("evaluates to false with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": "b", "rhs": "b"], toBe: false) } } $0.describe("more than expression") { - let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser) + let components = ["lhs", ">", "rhs"] $0.it("evaluates to true with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 4], toBe: true) } $0.it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5.0], toBe: false) } } $0.describe("more than equal expression") { - let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser) + let components = ["lhs", ">=", "rhs"] $0.it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5], toBe: true) } $0.it("evaluates to false with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5.1], toBe: false) } } $0.describe("less than expression") { - let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser) + let components = ["lhs", "<", "rhs"] $0.it("evaluates to true with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": 4, "rhs": 4.5], toBe: true) } $0.it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5.0], toBe: false) } } $0.describe("less than equal expression") { - let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser) + let components = ["lhs", "<=", "rhs"] $0.it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5], toBe: true) } $0.it("evaluates to false with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": 5.1, "rhs": 5.0], toBe: false) } } $0.describe("multiple expression") { - let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser) + let components = ["one", "or", "two", "and", "not", "three"] $0.it("evaluates to true with one") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() + try expectExpression(with: components, context: ["one": true], toBe: true) } $0.it("evaluates to true with one and three") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() + try expectExpression(with: components, context: ["one": true, "three": true], toBe: true) } $0.it("evaluates to true with two") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() + try expectExpression(with: components, context: ["two": true], toBe: true) } $0.it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + try expectExpression(with: components, context: ["two": true, "three": true], toBe: false) } $0.it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + try expectExpression(with: components, context: ["two": true, "three": true], toBe: false) } $0.it("evaluates to false with nothing") { - try expect(expression.evaluate(context: Context())).to.beFalse() + try expectExpression(with: components, context: [:], toBe: false) } } $0.describe("in expression") { - let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser) + let components = ["lhs", "in", "rhs"] $0.it("evaluates to true when rhs contains lhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue() + try expectExpression(with: components, context: ["lhs": 1, "rhs": [1, 2, 3]], toBe: true) + try expectExpression(with: components, context: ["lhs": "a", "rhs": ["a", "b", "c"]], toBe: true) + try expectExpression(with: components, context: ["lhs": "a", "rhs": "abc"], toBe: true) + try expectExpression(with: components, context: ["lhs": 1, "rhs": 1...3], toBe: true) + try expectExpression(with: components, context: ["lhs": 1, "rhs": 1..<3], toBe: true) } - + $0.it("evaluates to false when rhs does not contain lhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse() + try expectExpression(with: components, context: ["lhs": 1, "rhs": [2, 3, 4]], toBe: false) + try expectExpression(with: components, context: ["lhs": "a", "rhs": ["b", "c", "d"]], toBe: false) + try expectExpression(with: components, context: ["lhs": "a", "rhs": "bcd"], toBe: false) + try expectExpression(with: components, context: ["lhs": 4, "rhs": 1...3], toBe: false) + try expectExpression(with: components, context: ["lhs": 3, "rhs": 1..<3], toBe: false) } } } diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index a2d6815e..c9ef6ea5 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -202,8 +202,8 @@ func testIfNode() { $0.describe("rendering") { $0.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")]), ]) @@ -212,8 +212,8 @@ func testIfNode() { $0.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")]), ]) @@ -222,8 +222,8 @@ func testIfNode() { $0.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")]), ]) @@ -232,8 +232,8 @@ func testIfNode() { $0.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/docs/templates.rst b/docs/templates.rst index 147be457..6f11956f 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -49,6 +49,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 ~~~~~~~