Merge pull request #325 from stencilproject/feature/resolvable-expressions
Resolvable boolean expressions
This commit is contained in:
@@ -36,6 +36,11 @@
|
|||||||
- You can now access outer loop's scope by labeling it: `{% outer: for ... %}... {% for ... %} {{ outer.counter }} {% endfor %}{% endfor %}`.
|
- You can now access outer loop's scope by labeling it: `{% outer: for ... %}... {% for ... %} {{ outer.counter }} {% endfor %}{% endfor %}`.
|
||||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#175](https://github.com/stencilproject/Stencil/pull/175)
|
[#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
|
### Deprecations
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
public protocol Expression: CustomStringConvertible {
|
public protocol Expression: CustomStringConvertible, Resolvable {
|
||||||
func evaluate(context: Context) throws -> Bool
|
func evaluate(context: Context) throws -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Expression {
|
||||||
|
func resolve(_ context: Context) throws -> Any? {
|
||||||
|
try "\(evaluate(context: context))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protocol InfixOperator: Expression {
|
protocol InfixOperator: Expression {
|
||||||
init(lhs: Expression, rhs: Expression)
|
init(lhs: Expression, rhs: Expression)
|
||||||
}
|
}
|
||||||
@@ -37,8 +43,12 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
|||||||
"(variable: \(variable))"
|
"(variable: \(variable))"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolve(_ context: Context) throws -> Any? {
|
||||||
|
try variable.resolve(context)
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolves a variable in the given context as boolean
|
/// 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)
|
let result = try variable.resolve(context)
|
||||||
var truthy = false
|
var truthy = false
|
||||||
|
|
||||||
@@ -58,10 +68,6 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
|||||||
|
|
||||||
return truthy
|
return truthy
|
||||||
}
|
}
|
||||||
|
|
||||||
func evaluate(context: Context) throws -> Bool {
|
|
||||||
try resolve(context: context, variable: variable)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||||
|
|||||||
@@ -93,30 +93,36 @@ public class VariableNode: NodeType {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
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 condition: Expression?
|
||||||
let elseExpression: Resolvable?
|
let elseExpression: Resolvable?
|
||||||
|
|
||||||
if hasToken("if", at: 1) {
|
if hasToken("if", at: 1) {
|
||||||
|
variable = try compileResolvable([components[0]], containedIn: token)
|
||||||
|
|
||||||
let components = components.suffix(from: 2)
|
let components = components.suffix(from: 2)
|
||||||
if let elseIndex = components.firstIndex(of: "else") {
|
if let elseIndex = components.firstIndex(of: "else") {
|
||||||
condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token)
|
condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token)
|
||||||
let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ")
|
let elseToken = Array(components.suffix(from: elseIndex.advanced(by: 1)))
|
||||||
elseExpression = try parser.compileResolvable(elseToken, containedIn: token)
|
elseExpression = try compileResolvable(elseToken, containedIn: token)
|
||||||
} else {
|
} else {
|
||||||
condition = try parser.compileExpression(components: Array(components), token: token)
|
condition = try parser.compileExpression(components: Array(components), token: token)
|
||||||
elseExpression = nil
|
elseExpression = nil
|
||||||
}
|
}
|
||||||
} else {
|
} else if !components.isEmpty {
|
||||||
|
variable = try compileResolvable(components, containedIn: token)
|
||||||
condition = nil
|
condition = nil
|
||||||
elseExpression = nil
|
elseExpression = nil
|
||||||
}
|
} else {
|
||||||
|
|
||||||
guard let resolvable = components.first else {
|
|
||||||
throw TemplateSyntaxError(reason: "Missing variable name", token: token)
|
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) {
|
public init(variable: Resolvable, token: Token? = nil) {
|
||||||
|
|||||||
@@ -115,12 +115,12 @@ final class ExpressionsTests: XCTestCase {
|
|||||||
|
|
||||||
func testNotExpression() {
|
func testNotExpression() {
|
||||||
it("returns truthy for positive expressions") {
|
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()
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
it("returns falsy for negative expressions") {
|
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()
|
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,8 +200,8 @@ final class IfNodeTests: XCTestCase {
|
|||||||
func testRendering() {
|
func testRendering() {
|
||||||
it("renders a true expression") {
|
it("renders a true expression") {
|
||||||
let node = IfNode(conditions: [
|
let node = IfNode(conditions: [
|
||||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]),
|
IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "1")]),
|
||||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]),
|
||||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -210,8 +210,8 @@ final class IfNodeTests: XCTestCase {
|
|||||||
|
|
||||||
it("renders the first true expression") {
|
it("renders the first true expression") {
|
||||||
let node = IfNode(conditions: [
|
let node = IfNode(conditions: [
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]),
|
||||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]),
|
||||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
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") {
|
it("renders the empty expression when other conditions are falsy") {
|
||||||
let node = IfNode(conditions: [
|
let node = IfNode(conditions: [
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]),
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")]),
|
||||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -230,8 +230,8 @@ final class IfNodeTests: XCTestCase {
|
|||||||
|
|
||||||
it("renders empty when no truthy conditions") {
|
it("renders empty when no truthy conditions") {
|
||||||
let node = IfNode(conditions: [
|
let node = IfNode(conditions: [
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]),
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")])
|
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")])
|
||||||
])
|
])
|
||||||
|
|
||||||
try expect(try node.render(Context())) == ""
|
try expect(try node.render(Context())) == ""
|
||||||
|
|||||||
@@ -90,4 +90,22 @@ final class NodeTests: XCTestCase {
|
|||||||
try expect(try renderNodes(nodes, self.context)).toThrow(TemplateSyntaxError("Custom Error"))
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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
|
Filters
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user