Added support for brackets in boolean expressions (#165)
* added support for brackets in boolean expressions * more descriptive error messages * use array slices * added test for nested expressions * removed brackets validation step * address code review comments * added doc comment * simplify expression spec * fixed docs
This commit is contained in:
@@ -16,6 +16,9 @@
|
|||||||
- Now requires Swift 4.1 or newer.
|
- Now requires Swift 4.1 or newer.
|
||||||
[Yonas Kolb](https://github.com/yonaskolb)
|
[Yonas Kolb](https://github.com/yonaskolb)
|
||||||
[#228](https://github.com/stencilproject/Stencil/pull/228)
|
[#228](https://github.com/stencilproject/Stencil/pull/228)
|
||||||
|
- You can now use parentheses in boolean expressions to change operator precedence.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#165](https://github.com/stencilproject/Stencil/pull/165)
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,11 @@ func findOperator(name: String) -> Operator? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum IfToken {
|
indirect enum IfToken {
|
||||||
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
|
case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type)
|
||||||
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
|
case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type)
|
||||||
case variable(Resolvable)
|
case variable(Resolvable)
|
||||||
|
case subExpression(Expression)
|
||||||
case end
|
case end
|
||||||
|
|
||||||
var bindingPower: Int {
|
var bindingPower: Int {
|
||||||
@@ -52,6 +53,8 @@ enum IfToken {
|
|||||||
return bindingPower
|
return bindingPower
|
||||||
case .variable(_):
|
case .variable(_):
|
||||||
return 0
|
return 0
|
||||||
|
case .subExpression(_):
|
||||||
|
return 0
|
||||||
case .end:
|
case .end:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -66,6 +69,8 @@ enum IfToken {
|
|||||||
return op.init(expression: expression)
|
return op.init(expression: expression)
|
||||||
case .variable(let variable):
|
case .variable(let variable):
|
||||||
return VariableExpression(variable: variable)
|
return VariableExpression(variable: variable)
|
||||||
|
case .subExpression(let expression):
|
||||||
|
return expression
|
||||||
case .end:
|
case .end:
|
||||||
throw TemplateSyntaxError("'if' expression error: end")
|
throw TemplateSyntaxError("'if' expression error: end")
|
||||||
}
|
}
|
||||||
@@ -80,6 +85,8 @@ enum IfToken {
|
|||||||
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
|
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
|
||||||
case .variable(let variable):
|
case .variable(let variable):
|
||||||
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
|
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
|
||||||
|
case .subExpression(_):
|
||||||
|
throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side")
|
||||||
case .end:
|
case .end:
|
||||||
throw TemplateSyntaxError("'if' expression error: end")
|
throw TemplateSyntaxError("'if' expression error: end")
|
||||||
}
|
}
|
||||||
@@ -100,21 +107,71 @@ final class IfExpressionParser {
|
|||||||
let tokens: [IfToken]
|
let tokens: [IfToken]
|
||||||
var position: Int = 0
|
var position: Int = 0
|
||||||
|
|
||||||
init(components: [String], tokenParser: TokenParser, token: Token) throws {
|
private init(tokens: [IfToken]) {
|
||||||
self.tokens = try components.map { component in
|
self.tokens = tokens
|
||||||
if let op = findOperator(name: component) {
|
}
|
||||||
switch op {
|
|
||||||
case .infix(let name, let bindingPower, let cls):
|
static func parser(components: [String], tokenParser: TokenParser, token: Token) throws -> IfExpressionParser {
|
||||||
return .infix(name: name, bindingPower: bindingPower, op: cls)
|
return try IfExpressionParser(components: ArraySlice(components), tokenParser: tokenParser, token: token)
|
||||||
case .prefix(let name, let bindingPower, let cls):
|
}
|
||||||
return .prefix(name: name, bindingPower: bindingPower, op: cls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
|
private init(components: ArraySlice<String>, tokenParser: TokenParser, token: Token) throws {
|
||||||
|
var parsedComponents = Set<Int>()
|
||||||
|
var bracketsBalance = 0
|
||||||
|
self.tokens = try zip(components.indices, components).flatMap { (index, component) in
|
||||||
|
guard !parsedComponents.contains(index) else { return nil }
|
||||||
|
|
||||||
|
if component == "(" {
|
||||||
|
bracketsBalance += 1
|
||||||
|
let (expression, parsedCount) = try IfExpressionParser.subExpression(
|
||||||
|
from: components.suffix(from: index + 1),
|
||||||
|
tokenParser: tokenParser,
|
||||||
|
token: token
|
||||||
|
)
|
||||||
|
parsedComponents.formUnion(Set(index...(index + parsedCount)))
|
||||||
|
return .subExpression(expression)
|
||||||
|
} else if component == ")" {
|
||||||
|
bracketsBalance -= 1
|
||||||
|
if bracketsBalance < 0 {
|
||||||
|
throw TemplateSyntaxError("'if' expression error: missing opening bracket")
|
||||||
|
}
|
||||||
|
parsedComponents.insert(index)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
parsedComponents.insert(index)
|
||||||
|
if let op = findOperator(name: component) {
|
||||||
|
switch op {
|
||||||
|
case .infix(let name, let bindingPower, let operatorType):
|
||||||
|
return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||||
|
case .prefix(let name, let bindingPower, let operatorType):
|
||||||
|
return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func subExpression(from components: ArraySlice<String>, tokenParser: TokenParser, token: Token) throws -> (Expression, Int) {
|
||||||
|
var bracketsBalance = 1
|
||||||
|
let subComponents = components
|
||||||
|
.prefix(while: {
|
||||||
|
if $0 == "(" {
|
||||||
|
bracketsBalance += 1
|
||||||
|
} else if $0 == ")" {
|
||||||
|
bracketsBalance -= 1
|
||||||
|
}
|
||||||
|
return bracketsBalance != 0
|
||||||
|
})
|
||||||
|
if bracketsBalance > 0 {
|
||||||
|
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
|
||||||
|
}
|
||||||
|
|
||||||
|
let expressionParser = try IfExpressionParser(components: subComponents, tokenParser: tokenParser, token: token)
|
||||||
|
let expression = try expressionParser.parse()
|
||||||
|
return (expression, subComponents.count)
|
||||||
|
}
|
||||||
|
|
||||||
var currentToken: IfToken {
|
var currentToken: IfToken {
|
||||||
if tokens.count > position {
|
if tokens.count > position {
|
||||||
return tokens[position]
|
return tokens[position]
|
||||||
@@ -154,13 +211,11 @@ final class IfExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func parseExpression(components: [String], tokenParser: TokenParser, token: Token) throws -> Expression {
|
func parseExpression(components: [String], tokenParser: TokenParser, token: Token) throws -> Expression {
|
||||||
let parser = try IfExpressionParser(components: components, tokenParser: tokenParser, token: token)
|
let parser = try IfExpressionParser.parser(components: components, tokenParser: tokenParser, token: token)
|
||||||
return try parser.parse()
|
return try parser.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Represents an if condition and the associated nodes when the condition
|
/// Represents an if condition and the associated nodes when the condition
|
||||||
/// evaluates
|
/// evaluates
|
||||||
final class IfCondition {
|
final class IfCondition {
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ func testExpressions() {
|
|||||||
describe("Expression") {
|
describe("Expression") {
|
||||||
let parser = TokenParser(tokens: [], environment: Environment())
|
let parser = TokenParser(tokens: [], environment: Environment())
|
||||||
|
|
||||||
|
func parseExpression(components: [String]) throws -> Expression {
|
||||||
|
let parser = try IfExpressionParser.parser(components: components, tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
return try parser.parse()
|
||||||
|
}
|
||||||
|
|
||||||
$0.describe("VariableExpression") {
|
$0.describe("VariableExpression") {
|
||||||
let expression = VariableExpression(variable: Variable("value"))
|
let expression = VariableExpression(variable: Variable("value"))
|
||||||
|
|
||||||
@@ -105,19 +110,19 @@ func testExpressions() {
|
|||||||
|
|
||||||
$0.describe("expression parsing") {
|
$0.describe("expression parsing") {
|
||||||
$0.it("can parse a variable expression") {
|
$0.it("can parse a variable expression") {
|
||||||
let expression = try parseExpression(components: ["value"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try parseExpression(components: ["value"])
|
||||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse a not expression") {
|
$0.it("can parse a not expression") {
|
||||||
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try parseExpression(components: ["not", "value"])
|
||||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("and expression") {
|
$0.describe("and expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", "and", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs false") {
|
$0.it("evaluates to false with lhs false") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
||||||
@@ -137,7 +142,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("or expression") {
|
$0.describe("or expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", "or", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs true") {
|
$0.it("evaluates to true with lhs true") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
||||||
@@ -157,7 +162,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("equality expression") {
|
$0.describe("equality expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", "==", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with equal 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 expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
||||||
@@ -193,7 +198,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("inequality expression") {
|
$0.describe("inequality expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with inequal 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 expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
||||||
@@ -205,7 +210,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("more than expression") {
|
$0.describe("more than expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", ">", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with 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 expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
||||||
@@ -217,7 +222,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("more than equal expression") {
|
$0.describe("more than equal expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with 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 expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||||
@@ -229,7 +234,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("less than expression") {
|
$0.describe("less than expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", "<", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with 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 expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
||||||
@@ -241,7 +246,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("less than equal expression") {
|
$0.describe("less than equal expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with 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 expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||||
@@ -253,7 +258,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("multiple expression") {
|
$0.describe("multiple expression") {
|
||||||
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"])
|
||||||
|
|
||||||
$0.it("evaluates to true with one") {
|
$0.it("evaluates to true with one") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
||||||
@@ -281,7 +286,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("in expression") {
|
$0.describe("in expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
let expression = try! parseExpression(components: ["lhs", "in", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true when rhs contains lhs") {
|
$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": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
||||||
@@ -299,6 +304,41 @@ func testExpressions() {
|
|||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.describe("sub expression") {
|
||||||
|
$0.it("evaluates correctly") {
|
||||||
|
let context = Context(dictionary: ["one": false, "two": false, "three": true, "four": true])
|
||||||
|
|
||||||
|
let expression = try! parseExpression(components: ["one", "and", "two", "or", "three", "and", "four"])
|
||||||
|
let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "(", "two", ")", "or", "(", "three", "and", "four", ")", ")"])
|
||||||
|
|
||||||
|
try expect(expression.evaluate(context: context)).to.beTrue()
|
||||||
|
try expect(expressionWithBrackets.evaluate(context: context)).to.beFalse()
|
||||||
|
|
||||||
|
let notExpression = try! parseExpression(components: ["not", "one", "or", "three"])
|
||||||
|
let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"])
|
||||||
|
|
||||||
|
try expect(notExpression.evaluate(context: context)).to.beTrue()
|
||||||
|
try expect(notExpressionWithBrackets.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("fails when brackets are not balanced") {
|
||||||
|
try expect(parseExpression(components: ["(", "lhs", "and", "rhs"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket"))
|
||||||
|
try expect(parseExpression(components: [")", "lhs", "and", "rhs"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
||||||
|
try expect(parseExpression(components: ["lhs", "and", "rhs", ")"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
||||||
|
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket"))
|
||||||
|
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
||||||
|
try expect(parseExpression(components: ["(", "lhs", "and", ")"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: end"))
|
||||||
|
try expect(parseExpression(components: ["(", "and", "rhs", ")"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: infix operator 'and' doesn't have a left hand side"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,19 @@ Will be treated as:
|
|||||||
|
|
||||||
one or (two and three)
|
one or (two and three)
|
||||||
|
|
||||||
|
You can use parentheses to change operator precedence. For example:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% if (one or two) and three %}
|
||||||
|
|
||||||
|
Will be treated as:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
(one or two) and three
|
||||||
|
|
||||||
|
|
||||||
``==`` operator
|
``==`` operator
|
||||||
"""""""""""""""
|
"""""""""""""""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user