@@ -11,6 +11,12 @@
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
- If tags can now use prefix and infix operators such as `not`, `and` and `or`.
|
||||||
|
|
||||||
|
```html+django
|
||||||
|
{% if one or two and not three %}
|
||||||
|
```
|
||||||
|
|
||||||
- You may now register custom template filters which make use of arguments.
|
- You may now register custom template filters which make use of arguments.
|
||||||
- There is now a `default` filter.
|
- There is now a `default` filter.
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,48 @@
|
|||||||
class IfNode : NodeType {
|
protocol Expression: CustomStringConvertible {
|
||||||
|
func evaluate(context: Context) throws -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protocol InfixOperator: Expression {
|
||||||
|
init(lhs: Expression, rhs: Expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protocol PrefixOperator: Expression {
|
||||||
|
init(expression: Expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class StaticExpression: Expression, CustomStringConvertible {
|
||||||
|
let value: Bool
|
||||||
|
|
||||||
|
init(value: Bool) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(context: Context) throws -> Bool {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
return "\(value)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class VariableExpression: Expression, CustomStringConvertible {
|
||||||
let variable: Variable
|
let variable: Variable
|
||||||
let trueNodes:[NodeType]
|
|
||||||
let falseNodes:[NodeType]
|
|
||||||
|
|
||||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
init(variable: Variable) {
|
||||||
let components = token.components()
|
self.variable = variable
|
||||||
guard components.count == 2 else {
|
|
||||||
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
|
|
||||||
}
|
|
||||||
let variable = components[1]
|
|
||||||
var trueNodes = [NodeType]()
|
|
||||||
var falseNodes = [NodeType]()
|
|
||||||
|
|
||||||
trueNodes = try parser.parse(until(["endif", "else"]))
|
|
||||||
|
|
||||||
guard let token = parser.nextToken() else {
|
|
||||||
throw TemplateSyntaxError("`endif` was not found.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if token.contents == "else" {
|
var description: String {
|
||||||
falseNodes = try parser.parse(until(["endif"]))
|
return "(variable: \(variable.variable))"
|
||||||
_ = parser.nextToken()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
/// Resolves a variable in the given context as boolean
|
||||||
}
|
func resolve(context: Context, variable: Variable) throws -> Bool {
|
||||||
|
|
||||||
class func parse_ifnot(_ parser:TokenParser, token:Token) throws -> NodeType {
|
|
||||||
let components = token.components()
|
|
||||||
guard components.count == 2 else {
|
|
||||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.")
|
|
||||||
}
|
|
||||||
let variable = components[1]
|
|
||||||
var trueNodes = [NodeType]()
|
|
||||||
var falseNodes = [NodeType]()
|
|
||||||
|
|
||||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
|
||||||
|
|
||||||
guard let token = parser.nextToken() else {
|
|
||||||
throw TemplateSyntaxError("`endif` was not found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if token.contents == "else" {
|
|
||||||
trueNodes = try parser.parse(until(["endif"]))
|
|
||||||
_ = parser.nextToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
|
|
||||||
self.variable = Variable(variable)
|
|
||||||
self.trueNodes = trueNodes
|
|
||||||
self.falseNodes = falseNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
|
||||||
let result = try variable.resolve(context)
|
let result = try variable.resolve(context)
|
||||||
var truthy = false
|
var truthy = false
|
||||||
|
|
||||||
@@ -75,6 +62,298 @@ class IfNode : NodeType {
|
|||||||
truthy = true
|
truthy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return truthy
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(context: Context) throws -> Bool {
|
||||||
|
return try resolve(context: context, variable: variable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||||
|
let expression: Expression
|
||||||
|
|
||||||
|
init(expression: Expression) {
|
||||||
|
self.expression = expression
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
return "not \(expression)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(context: Context) throws -> Bool {
|
||||||
|
return try !expression.evaluate(context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||||
|
let lhs: Expression
|
||||||
|
let rhs: Expression
|
||||||
|
|
||||||
|
init(lhs: Expression, rhs: Expression) {
|
||||||
|
self.lhs = lhs
|
||||||
|
self.rhs = rhs
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
return "(\(lhs) or \(rhs))"
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(context: Context) throws -> Bool {
|
||||||
|
let lhs = try self.lhs.evaluate(context: context)
|
||||||
|
if lhs {
|
||||||
|
return lhs
|
||||||
|
}
|
||||||
|
|
||||||
|
return try rhs.evaluate(context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||||
|
let lhs: Expression
|
||||||
|
let rhs: Expression
|
||||||
|
|
||||||
|
init(lhs: Expression, rhs: Expression) {
|
||||||
|
self.lhs = lhs
|
||||||
|
self.rhs = rhs
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
return "(\(lhs) and \(rhs))"
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(context: Context) throws -> Bool {
|
||||||
|
let lhs = try self.lhs.evaluate(context: context)
|
||||||
|
if !lhs {
|
||||||
|
return lhs
|
||||||
|
}
|
||||||
|
|
||||||
|
return try rhs.evaluate(context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum Operator {
|
||||||
|
case infix(String, Int, InfixOperator.Type)
|
||||||
|
case prefix(String, Int, PrefixOperator.Type)
|
||||||
|
|
||||||
|
var name: String {
|
||||||
|
switch self {
|
||||||
|
case .infix(let name, _, _):
|
||||||
|
return name
|
||||||
|
case .prefix(let name, _, _):
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let operators: [Operator] = [
|
||||||
|
.infix("or", 6, OrExpression.self),
|
||||||
|
.infix("and", 7, AndExpression.self),
|
||||||
|
.prefix("not", 8, NotExpression.self),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func findOperator(name: String) -> Operator? {
|
||||||
|
for op in operators {
|
||||||
|
if op.name == name {
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum IfToken {
|
||||||
|
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
|
||||||
|
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
|
||||||
|
case variable(Variable)
|
||||||
|
case end
|
||||||
|
|
||||||
|
var bindingPower: Int {
|
||||||
|
switch self {
|
||||||
|
case .infix(_, let bindingPower, _):
|
||||||
|
return bindingPower
|
||||||
|
case .prefix(_, let bindingPower, _):
|
||||||
|
return bindingPower
|
||||||
|
case .variable(_):
|
||||||
|
return 0
|
||||||
|
case .end:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullDenotation(parser: IfExpressionParser) throws -> Expression {
|
||||||
|
switch self {
|
||||||
|
case .infix(let name, _, _):
|
||||||
|
throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side")
|
||||||
|
case .prefix(_, let bindingPower, let op):
|
||||||
|
let expression = try parser.expression(bindingPower: bindingPower)
|
||||||
|
return op.init(expression: expression)
|
||||||
|
case .variable(let variable):
|
||||||
|
return VariableExpression(variable: variable)
|
||||||
|
case .end:
|
||||||
|
throw TemplateSyntaxError("'if' expression error: end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression {
|
||||||
|
switch self {
|
||||||
|
case .infix(_, let bindingPower, let op):
|
||||||
|
let right = try parser.expression(bindingPower: bindingPower)
|
||||||
|
return op.init(lhs: left, rhs: right)
|
||||||
|
case .prefix(let name, _, _):
|
||||||
|
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
|
||||||
|
case .variable(let variable):
|
||||||
|
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
|
||||||
|
case .end:
|
||||||
|
throw TemplateSyntaxError("'if' expression error: end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEnd: Bool {
|
||||||
|
switch self {
|
||||||
|
case .end:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class IfExpressionParser {
|
||||||
|
let tokens: [IfToken]
|
||||||
|
var position: Int = 0
|
||||||
|
|
||||||
|
init(components: [String]) {
|
||||||
|
self.tokens = components.map { component in
|
||||||
|
if let op = findOperator(name: component) {
|
||||||
|
switch op {
|
||||||
|
case .infix(let name, let bindingPower, let cls):
|
||||||
|
return .infix(name: name, bindingPower: bindingPower, op: cls)
|
||||||
|
case .prefix(let name, let bindingPower, let cls):
|
||||||
|
return .prefix(name: name, bindingPower: bindingPower, op: cls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .variable(Variable(component))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentToken: IfToken {
|
||||||
|
if tokens.count > position {
|
||||||
|
return tokens[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
return .end
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextToken: IfToken {
|
||||||
|
position += 1
|
||||||
|
return currentToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse() throws -> Expression {
|
||||||
|
let expression = try self.expression()
|
||||||
|
|
||||||
|
if !currentToken.isEnd {
|
||||||
|
throw TemplateSyntaxError("'if' expression error: dangling token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func expression(bindingPower: Int = 0) throws -> Expression {
|
||||||
|
var token = currentToken
|
||||||
|
position += 1
|
||||||
|
|
||||||
|
var left = try token.nullDenotation(parser: self)
|
||||||
|
|
||||||
|
while bindingPower < currentToken.bindingPower {
|
||||||
|
token = currentToken
|
||||||
|
position += 1
|
||||||
|
left = try token.leftDenotation(left: left, parser: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func parseExpression(components: [String]) throws -> Expression {
|
||||||
|
let parser = IfExpressionParser(components: components)
|
||||||
|
return try parser.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IfNode : NodeType {
|
||||||
|
let expression: Expression
|
||||||
|
let trueNodes: [NodeType]
|
||||||
|
let falseNodes: [NodeType]
|
||||||
|
|
||||||
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
|
var components = token.components()
|
||||||
|
guard components.count == 2 else {
|
||||||
|
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
|
||||||
|
}
|
||||||
|
components.removeFirst()
|
||||||
|
var trueNodes = [NodeType]()
|
||||||
|
var falseNodes = [NodeType]()
|
||||||
|
|
||||||
|
trueNodes = try parser.parse(until(["endif", "else"]))
|
||||||
|
|
||||||
|
guard let token = parser.nextToken() else {
|
||||||
|
throw TemplateSyntaxError("`endif` was not found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.contents == "else" {
|
||||||
|
falseNodes = try parser.parse(until(["endif"]))
|
||||||
|
_ = parser.nextToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
let expression = try parseExpression(components: components)
|
||||||
|
return IfNode(expression: expression, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
|
var components = token.components()
|
||||||
|
guard components.count == 2 else {
|
||||||
|
throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.")
|
||||||
|
}
|
||||||
|
components.removeFirst()
|
||||||
|
var trueNodes = [NodeType]()
|
||||||
|
var falseNodes = [NodeType]()
|
||||||
|
|
||||||
|
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||||
|
|
||||||
|
guard let token = parser.nextToken() else {
|
||||||
|
throw TemplateSyntaxError("`endif` was not found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.contents == "else" {
|
||||||
|
trueNodes = try parser.parse(until(["endif"]))
|
||||||
|
_ = parser.nextToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
let expression = try parseExpression(components: components)
|
||||||
|
return IfNode(expression: expression, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(expression: Expression, trueNodes: [NodeType], falseNodes: [NodeType]) {
|
||||||
|
self.expression = expression
|
||||||
|
self.trueNodes = trueNodes
|
||||||
|
self.falseNodes = falseNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(_ context: Context) throws -> String {
|
||||||
|
let truthy = try expression.evaluate(context: context)
|
||||||
|
|
||||||
return try context.push {
|
return try context.push {
|
||||||
if truthy {
|
if truthy {
|
||||||
return try renderNodes(trueNodes, context)
|
return try renderNodes(trueNodes, context)
|
||||||
|
|||||||
@@ -3,6 +3,172 @@ import Spectre
|
|||||||
|
|
||||||
|
|
||||||
func testIfNode() {
|
func testIfNode() {
|
||||||
|
describe("Expression") {
|
||||||
|
$0.describe("VariableExpression") {
|
||||||
|
let expression = VariableExpression(variable: Variable("value"))
|
||||||
|
|
||||||
|
$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()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false when value is unset") {
|
||||||
|
let context = Context()
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true when array variable is not empty") {
|
||||||
|
let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]]
|
||||||
|
let context = Context(dictionary: ["value": [items]])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false when array value is empty") {
|
||||||
|
let emptyItems = [[String: Any]]()
|
||||||
|
let context = Context(dictionary: ["value": emptyItems])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false when dictionary value is empty") {
|
||||||
|
let emptyItems = [String:Any]()
|
||||||
|
let context = Context(dictionary: ["value": emptyItems])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false when Array<Any> value is empty") {
|
||||||
|
let context = Context(dictionary: ["value": ([] as [Any])])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true when integer value is above 0") {
|
||||||
|
let context = Context(dictionary: ["value": 1])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false when integer value is below 0 or below") {
|
||||||
|
let context = Context(dictionary: ["value": 0])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
|
||||||
|
let negativeContext = Context(dictionary: ["value": 0])
|
||||||
|
try expect(try expression.evaluate(context: negativeContext)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true when float value is above 0") {
|
||||||
|
let context = Context(dictionary: ["value": Float(0.5)])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false when float is 0 or below") {
|
||||||
|
let context = Context(dictionary: ["value": Float(0)])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true when double value is above 0") {
|
||||||
|
let context = Context(dictionary: ["value": Double(0.5)])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false when double is 0 or below") {
|
||||||
|
let context = Context(dictionary: ["value": Double(0)])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.describe("NotExpression") {
|
||||||
|
$0.it("returns truthy for positive expressions") {
|
||||||
|
let expression = NotExpression(expression: StaticExpression(value: true))
|
||||||
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("returns falsy for negative expressions") {
|
||||||
|
let expression = NotExpression(expression: StaticExpression(value: false))
|
||||||
|
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.describe("expression parsing") {
|
||||||
|
$0.it("can parse a variable expression") {
|
||||||
|
let expression = try parseExpression(components: ["value"])
|
||||||
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can parse a not expression") {
|
||||||
|
let expression = try parseExpression(components: ["not", "value"])
|
||||||
|
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.describe("and expression") {
|
||||||
|
let expression = try! parseExpression(components: ["lhs", "and", "rhs"])
|
||||||
|
|
||||||
|
$0.it("evaluates to false with lhs false") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false with rhs false") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false with lhs and rhs false") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true with lhs and rhs true") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.describe("or expression") {
|
||||||
|
let expression = try! parseExpression(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true with rhs true") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true with lhs and rhs true") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false with lhs and rhs false") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.describe("multiple expression") {
|
||||||
|
let expression = try! parseExpression(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true with one and three") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to true with two") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false with two and three") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false with two and three") {
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("evaluates to false with nothing") {
|
||||||
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("IfNode") {
|
describe("IfNode") {
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("can parse an if block") {
|
$0.it("can parse an if block") {
|
||||||
@@ -21,7 +187,6 @@ func testIfNode() {
|
|||||||
let falseNode = node?.falseNodes.first as? TextNode
|
let falseNode = node?.falseNodes.first as? TextNode
|
||||||
|
|
||||||
try expect(nodes.count) == 1
|
try expect(nodes.count) == 1
|
||||||
try expect(node?.variable.variable) == "value"
|
|
||||||
try expect(node?.trueNodes.count) == 1
|
try expect(node?.trueNodes.count) == 1
|
||||||
try expect(trueNode?.text) == "true"
|
try expect(trueNode?.text) == "true"
|
||||||
try expect(node?.falseNodes.count) == 1
|
try expect(node?.falseNodes.count) == 1
|
||||||
@@ -44,7 +209,6 @@ func testIfNode() {
|
|||||||
let falseNode = node?.falseNodes.first as? TextNode
|
let falseNode = node?.falseNodes.first as? TextNode
|
||||||
|
|
||||||
try expect(nodes.count) == 1
|
try expect(nodes.count) == 1
|
||||||
try expect(node?.variable.variable) == "value"
|
|
||||||
try expect(node?.trueNodes.count) == 1
|
try expect(node?.trueNodes.count) == 1
|
||||||
try expect(trueNode?.text) == "true"
|
try expect(trueNode?.text) == "true"
|
||||||
try expect(node?.falseNodes.count) == 1
|
try expect(node?.falseNodes.count) == 1
|
||||||
@@ -74,84 +238,13 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.describe("rendering") {
|
$0.describe("rendering") {
|
||||||
$0.it("renders the truth when expression evaluates to true") {
|
$0.it("renders the truth when expression evaluates to true") {
|
||||||
let context = Context(dictionary: ["items": true])
|
let node = IfNode(expression: StaticExpression(value: true), trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
try expect(try node.render(Context())) == "true"
|
||||||
try expect(try node.render(context)) == "true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders the false when expression evaluates to false") {
|
$0.it("renders the false when expression evaluates to false") {
|
||||||
let context = Context(dictionary: ["items": false])
|
let node = IfNode(expression: StaticExpression(value: false), trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
try expect(try node.render(Context())) == "false"
|
||||||
try expect(try node.render(context)) == "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the truth when expression is not nil") {
|
|
||||||
let context = Context(dictionary: ["known": "known"])
|
|
||||||
let node = IfNode(variable: "known", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(context)) == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the false when expression is nil") {
|
|
||||||
let context = Context(dictionary: [:])
|
|
||||||
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(context)) == "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the truth when array expression is not empty") {
|
|
||||||
let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]]
|
|
||||||
let arrayContext = Context(dictionary: ["items": [items]])
|
|
||||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(arrayContext)) == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the false when array expression is empty") {
|
|
||||||
let emptyItems = [[String: Any]]()
|
|
||||||
let arrayContext = Context(dictionary: ["items": emptyItems])
|
|
||||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(arrayContext)) == "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the false when dictionary expression is empty") {
|
|
||||||
let emptyItems = [String:Any]()
|
|
||||||
let arrayContext = Context(dictionary: ["items": emptyItems])
|
|
||||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(arrayContext)) == "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the false when Array<Any> variable is empty") {
|
|
||||||
let arrayContext = Context(dictionary: ["items": ([] as [Any])])
|
|
||||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(arrayContext)) == "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the false when integer is below 1") {
|
|
||||||
let context = Context(dictionary: ["value": 0])
|
|
||||||
let node = IfNode(variable: "value", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(context)) == "false"
|
|
||||||
|
|
||||||
let negativeContext = Context(dictionary: ["value": -5])
|
|
||||||
let negativeNode = IfNode(variable: "value", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try negativeNode.render(negativeContext)) == "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the false when float is below 1") {
|
|
||||||
let context = Context(dictionary: ["value": Float(0)])
|
|
||||||
let node = IfNode(variable: "value", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(context)) == "false"
|
|
||||||
|
|
||||||
let negativeContext = Context(dictionary: ["value": Float(-5)])
|
|
||||||
let negativeNode = IfNode(variable: "value", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try negativeNode.render(negativeContext)) == "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the false when double is below 1") {
|
|
||||||
let context = Context(dictionary: ["value": Double(0)])
|
|
||||||
let node = IfNode(variable: "value", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try node.render(context)) == "false"
|
|
||||||
|
|
||||||
let negativeContext = Context(dictionary: ["value": Double(-5)])
|
|
||||||
let negativeNode = IfNode(variable: "value", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
|
||||||
try expect(try negativeNode.render(negativeContext)) == "false"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ The for block sets a few variables available within the loop:
|
|||||||
``if``
|
``if``
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
* Present in the context
|
||||||
|
* Being non-empty (dictionaries or arrays)
|
||||||
|
* Not being a false boolean value
|
||||||
|
* Not being a numerical value of 0 or below
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
{% if variable %}
|
{% if variable %}
|
||||||
@@ -49,6 +57,43 @@ The for block sets a few variables available within the loop:
|
|||||||
The variable was not found.
|
The variable was not found.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
Operators
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
``if`` tags may combine ``and``, ``or`` and ``not`` to test multiple variables
|
||||||
|
or to negate a variable.
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% if one and two %}
|
||||||
|
Both one and two evaluate to true.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not one %}
|
||||||
|
One evaluates to false
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if one or two %}
|
||||||
|
Either one or two evaluates to true.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not one or two %}
|
||||||
|
One does not evaluate to false or two evaluates to true.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
You may use ``and``, ``or`` and ``not`` multiple times together. ``not`` has
|
||||||
|
higest prescidence followed by ``and``. For example:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% if one or two and three %}
|
||||||
|
|
||||||
|
Will be treated as:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
one or (two and three)
|
||||||
|
|
||||||
``ifnot``
|
``ifnot``
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user