From 943e064f701ff12ebb922a608ab459970440261a Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 20:00:31 +0100 Subject: [PATCH] added support for brackets in boolean expressions --- CHANGELOG.md | 1 + Sources/IfTag.swift | 69 +++++++++++++++++++++---- Tests/StencilTests/ExpressionSpec.swift | 29 +++++++++++ 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8e81f8..4b8b7656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements - Added support for resolving superclass properties for not-NSObject subclasses +- You can now use brackets in boolean expressions to change operators precedence ### Bug Fixes diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 8f3b0fda..56a8ed6b 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -38,10 +38,11 @@ func findOperator(name: String) -> Operator? { } -enum IfToken { +indirect enum IfToken { case infix(name: String, bindingPower: Int, op: InfixOperator.Type) case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type) case variable(Resolvable) + case subExpression(IfExpressionParser) case end var bindingPower: Int { @@ -52,6 +53,8 @@ enum IfToken { return bindingPower case .variable(_): return 0 + case .subExpression(_): + return 0 case .end: return 0 } @@ -66,6 +69,8 @@ enum IfToken { return op.init(expression: expression) case .variable(let variable): return VariableExpression(variable: variable) + case .subExpression(let parser): + return try parser.expression() case .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") case .variable(let variable): 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: throw TemplateSyntaxError("'if' expression error: end") } @@ -101,17 +108,59 @@ final class IfExpressionParser { var position: Int = 0 init(components: [String], tokenParser: TokenParser) throws { - self.tokens = try 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) + guard try components.reduce(0, { + var balance = $0 + if $1 == "(" { balance += 1 } + else if $1 == ")" { balance -= 1 } + if balance < 0 { throw TemplateSyntaxError("unbalanced brackets") } + return balance + }) == 0 else { + throw TemplateSyntaxError("unbalanced brackets") + } + + var parsedComponents = [String]() + + func parseSubExpression(from index: Int, components: [String], tokenParser: TokenParser) throws -> (IfExpressionParser, [String]) { + var bracketsBalance = 1 + let subComponents = Array(components + .suffix(from: index) + .prefix(while: { + if $0 == "(" { bracketsBalance += 1 } + else if $0 == ")" { bracketsBalance -= 1 } + return bracketsBalance != 0 + })) + + let expression = try IfExpressionParser(components: subComponents, tokenParser: tokenParser) + return (expression, ["("] + subComponents + [")"]) + } + + self.tokens = try components.enumerated().flatMap { index, component in + if component == "(" { + let (expression, parsed) = try parseSubExpression( + from: index + 1, + components: components, + tokenParser: tokenParser + ) + parsedComponents.append(contentsOf: parsed) + return .subExpression(expression) + } + else if component == ")" { + parsedComponents.append(component) + return nil + } else if index >= parsedComponents.count { + parsedComponents.append(component) + 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(try tokenParser.compileFilter(component)) + } else { + return nil } - - return .variable(try tokenParser.compileFilter(component)) } } diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index 4b7958d5..af3f96f1 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -295,6 +295,35 @@ func testExpressions() { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse() } } + + $0.describe("sub expression") { + $0.it("evaluates correctly") { + let context = Context(dictionary: ["one": false, "two": false, "three": true]) + + let expression = try! parseExpression(components: ["one", "and", "two", "or", "three"], tokenParser: parser) + let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "two", "or", "three", ")"], tokenParser: parser) + + 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"], tokenParser: parser) + let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"], tokenParser: parser) + + 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"], tokenParser: parser)).toThrow() + try expect(parseExpression(components: [")", "lhs", "and", "rhs"], tokenParser: parser)).toThrow() + try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("], tokenParser: parser)).toThrow() + try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"], tokenParser: parser)).toThrow() + try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"], tokenParser: parser)).toThrow() + try expect(parseExpression(components: ["(", "lhs", "and", ")"], tokenParser: parser)).toThrow() + try expect(parseExpression(components: ["(", "and", "rhs", ")"], tokenParser: parser)).toThrow() + } + } + } } }