From 9ac48fbc055551f0717ee4fc3b8e078c6f19c95b Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 19 Sep 2016 08:50:39 -0700 Subject: [PATCH] Add support for binary operations. --- differences.md | 4 +- lib/src/ast/sass.dart | 1 + .../ast/sass/expression/binary_operation.dart | 77 ++++++++++ lib/src/parse/stylesheet.dart | 145 +++++++++++++++--- lib/src/value.dart | 37 +++++ lib/src/value/boolean.dart | 4 + lib/src/value/color.dart | 21 +++ lib/src/value/identifier.dart | 6 +- lib/src/value/null.dart | 4 + lib/src/value/number.dart | 48 ++++++ lib/src/value/string.dart | 12 ++ lib/src/visitor/interface/expression.dart | 1 + lib/src/visitor/perform.dart | 37 +++++ 13 files changed, 370 insertions(+), 27 deletions(-) create mode 100644 lib/src/ast/sass/expression/binary_operation.dart diff --git a/differences.md b/differences.md index 70d212de..461fdb56 100644 --- a/differences.md +++ b/differences.md @@ -96,8 +96,10 @@ official behavior. indentation across the whole document. This doesn't have an issue yet; I need to talk to Chris to determine if it's actually the right way forward. +6. Colors do not support channel-by-channel arithmetic. See [issue 2144][]. + [issue 1599]: https://github.com/sass/sass/issues/1599 [issue 1126]: https://github.com/sass/sass/issues/1126 [issue 2120]: https://github.com/sass/sass/issues/2120 [issue 1122]: https://github.com/sass/sass/issues/1122 - +[issue 2144]: https://github.com/sass/sass/issues/2144 diff --git a/lib/src/ast/sass.dart b/lib/src/ast/sass.dart index 5aa7d395..6f9fcf01 100644 --- a/lib/src/ast/sass.dart +++ b/lib/src/ast/sass.dart @@ -8,6 +8,7 @@ export 'sass/argument_invocation.dart'; export 'sass/callable_declaration.dart'; export 'sass/callable_invocation.dart'; export 'sass/expression.dart'; +export 'sass/expression/binary_operation.dart'; export 'sass/expression/boolean.dart'; export 'sass/expression/color.dart'; export 'sass/expression/function.dart'; diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart new file mode 100644 index 00000000..1b16ea76 --- /dev/null +++ b/lib/src/ast/sass/expression/binary_operation.dart @@ -0,0 +1,77 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; +import 'package:charcode/charcode.dart'; + +import '../../../utils.dart'; +import '../../../visitor/interface/expression.dart'; +import '../expression.dart'; + +class BinaryOperationExpression implements Expression { + final BinaryOperator operator; + + final Expression left; + + final Expression right; + + FileSpan get span => spanForList([left, right]); + + BinaryOperationExpression(this.operator, this.left, this.right); + + /*=T*/ accept/**/(ExpressionVisitor/**/ visitor) => + visitor.visitBinaryOperationExpression(this); + + String toString() { + var buffer = new StringBuffer(); + + var left = this.left; // Hack to make analysis work. + var leftNeedsParens = left is BinaryOperationExpression && + left.operator.precedence < operator.precedence; + if (leftNeedsParens) buffer.writeCharCode($lparen); + buffer.write(left); + if (leftNeedsParens) buffer.writeCharCode($rparen); + + buffer.writeCharCode($space); + buffer.write(operator.operator); + buffer.writeCharCode($space); + + var right = this.right; // Hack to make analysis work. + var rightNeedsParens = right is BinaryOperationExpression && + right.operator.precedence <= operator.precedence; + if (rightNeedsParens) buffer.writeCharCode($lparen); + buffer.write(right); + if (rightNeedsParens) buffer.writeCharCode($rparen); + + return buffer.toString(); + } +} + +class BinaryOperator { + static const or = const BinaryOperator._("or", "or", 0); + static const and = const BinaryOperator._("and", "and", 1); + static const equals = const BinaryOperator._("equals", "==", 2); + static const notEquals = const BinaryOperator._("not equals", "!=", 2); + static const greaterThan = const BinaryOperator._("greater than", ">", 3); + static const greaterThanOrEquals = + const BinaryOperator._("greater than or equals", ">=", 3); + static const lessThan = const BinaryOperator._("less than", "<", 3); + static const lessThanOrEquals = + const BinaryOperator._("less than or equals", "<=", 3); + static const plus = const BinaryOperator._("plus", "+", 4); + static const minus = const BinaryOperator._("minus", "-", 4); + static const times = const BinaryOperator._("times", "*", 5); + static const dividedBy = const BinaryOperator._("divided by", "/", 5); + static const modulo = const BinaryOperator._("modulo", "%", 5); + + final String name; + + final String operator; + + final int precedence; + + const BinaryOperator._(this.name, this.operator, this.precedence); + + String toString() => name; +} diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index dcb76767..79349beb 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -679,16 +679,63 @@ abstract class StylesheetParser extends Parser { List commaExpressions; List spaceExpressions; + + // Operators whose right-hand operands are not fully parsed yet, in order of + // appearance in the document. Because a low-precedence operator will cause + // parsing to finish for all preceding higher-precedence operators, this is + // naturally ordered from lowest to highest precedence. + List operators; + + // The left-hand sides of [operators]. `operands[n]` is the left-hand side + // of `operators[n]`. + List operands; var singleExpression = _singleExpression(); + resolveOneOperation() { + assert(singleExpression != null); + singleExpression = new BinaryOperationExpression( + operators.removeLast(), operands.removeLast(), singleExpression); + } + + resolveOperations() { + if (operators == null) return; + while (!operators.isEmpty) { + resolveOneOperation(); + } + } + addSingleExpression(Expression expression) { if (singleExpression != null) { spaceExpressions ??= []; + resolveOperations(); spaceExpressions.add(singleExpression); } singleExpression = expression; } + addOperator(BinaryOperator operator) { + operators ??= []; + operands ??= []; + while (operators.isNotEmpty && + operators.last.precedence >= operator.precedence) { + resolveOneOperation(); + } + operators.add(operator); + + assert(singleExpression != null); + operands.add(singleExpression); + singleExpression = null; + } + + resolveSpaceExpressions() { + if (singleExpression != null) resolveOperations(); + if (spaceExpressions == null) return; + if (singleExpression != null) spaceExpressions.add(singleExpression); + singleExpression = + new ListExpression(spaceExpressions, ListSeparator.space); + spaceExpressions = null; + } + loop: while (true) { whitespace(); @@ -700,10 +747,6 @@ abstract class StylesheetParser extends Parser { addSingleExpression(_parentheses()); break; - case $slash: - addSingleExpression(_unaryOperation()); - break; - case $lbracket: addSingleExpression(_bracketedList()); break; @@ -725,12 +768,61 @@ abstract class StylesheetParser extends Parser { addSingleExpression(_hashExpression()); break; + case $equal: + scanner.readChar(); + scanner.expectChar($equal); + addOperator(BinaryOperator.equals); + break; + + case $exclamation: + scanner.readChar(); + scanner.expectChar($equal); + addOperator(BinaryOperator.notEquals); + break; + + case $langle: + scanner.readChar(); + addOperator(scanner.scanChar($equal) + ? BinaryOperator.lessThanOrEquals + : BinaryOperator.lessThan); + break; + + case $rangle: + scanner.readChar(); + addOperator(scanner.scanChar($equal) + ? BinaryOperator.greaterThanOrEquals + : BinaryOperator.greaterThan); + break; + + case $asterisk: + scanner.readChar(); + addOperator(BinaryOperator.times); + break; + case $plus: - addSingleExpression(_plusExpression()); + scanner.readChar(); + addOperator(BinaryOperator.plus); break; case $minus: - addSingleExpression(_minusExpression()); + var next = scanner.peekChar(1); + if (isDigit(next) || next == $dot) { + addSingleExpression(_number()); + } else if (_lookingAtInterpolatedIdentifier()) { + addSingleExpression(_identifierLike()); + } else { + addOperator(BinaryOperator.minus); + } + break; + + case $slash: + scanner.readChar(); + addOperator(BinaryOperator.dividedBy); + break; + + case $percent: + scanner.readChar(); + addOperator(BinaryOperator.modulo); break; case $0: @@ -748,6 +840,21 @@ abstract class StylesheetParser extends Parser { break; case $a: + if (scanIdentifier("and")) { + addOperator(BinaryOperator.and); + } else { + addSingleExpression(_identifierLike()); + } + break; + + case $o: + if (scanIdentifier("or")) { + addOperator(BinaryOperator.and); + } else { + addSingleExpression(_identifierLike()); + } + break; + case $b: case $c: case $d: @@ -761,7 +868,6 @@ abstract class StylesheetParser extends Parser { case $l: case $m: case $n: - case $o: case $p: case $q: case $r: @@ -806,19 +912,12 @@ abstract class StylesheetParser extends Parser { case $comma: commaExpressions ??= []; - if (spaceExpressions != null) { - spaceExpressions.add(singleExpression); - commaExpressions - .add(new ListExpression(spaceExpressions, ListSeparator.space)); - spaceExpressions = null; - singleExpression = null; - } else if (singleExpression != null) { - commaExpressions.add(singleExpression); - singleExpression = null; - } else { - scanner.error("Expected expression."); - } + if (singleExpression == null) scanner.error("Expected expression."); + + resolveSpaceExpressions(); + commaExpressions.add(singleExpression); scanner.readChar(); + singleExpression = null; break; default: @@ -835,16 +934,12 @@ abstract class StylesheetParser extends Parser { } } - if (spaceExpressions != null) { - if (singleExpression != null) spaceExpressions.add(singleExpression); - singleExpression = - new ListExpression(spaceExpressions, ListSeparator.space); - } - + resolveSpaceExpressions(); if (commaExpressions != null) { if (singleExpression != null) commaExpressions.add(singleExpression); return new ListExpression(commaExpressions, ListSeparator.comma); } else { + assert(singleExpression != null); return singleExpression; } } diff --git a/lib/src/value.dart b/lib/src/value.dart index 45b91503..4685f742 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -5,6 +5,7 @@ import 'exception.dart'; import 'value/boolean.dart'; import 'value/identifier.dart'; +import 'value/string.dart'; import 'visitor/interface/value.dart'; import 'visitor/serialize.dart'; @@ -31,6 +32,42 @@ abstract class Value { /*=T*/ accept/**/(ValueVisitor/**/ visitor); + Value or(Value other) => this; + + Value and(Value other) => other; + + SassBoolean greaterThan(Value other) => + throw new InternalException('Undefined operation "$this > $other".'); + + SassBoolean greaterThanOrEquals(Value other) => + throw new InternalException('Undefined operation "$this >= $other".'); + + SassBoolean lessThan(Value other) => + throw new InternalException('Undefined operation "$this < $other".'); + + SassBoolean lessThanOrEquals(Value other) => + throw new InternalException('Undefined operation "$this <= $other".'); + + Value times(Value other) => + throw new InternalException('Undefined operation "$this * $other".'); + + Value modulo(Value other) => + throw new InternalException('Undefined operation "$this % $other".'); + + Value plus(Value other) { + if (other is SassString) { + return new SassString(valueToCss(this) + other.text); + } else { + return new SassIdentifier(valueToCss(this) + valueToCss(other)); + } + } + + Value minus(Value other) => + new SassIdentifier("${valueToCss(this)}-${valueToCss(other)}"); + + Value dividedBy(Value other) => + new SassIdentifier("${valueToCss(this)}/${valueToCss(other)}"); + Value unaryPlus() => new SassIdentifier("+${valueToCss(this)}"); Value unaryMinus() => new SassIdentifier("-${valueToCss(this)}"); diff --git a/lib/src/value/boolean.dart b/lib/src/value/boolean.dart index c2038ab9..f8351a64 100644 --- a/lib/src/value/boolean.dart +++ b/lib/src/value/boolean.dart @@ -20,5 +20,9 @@ class SassBoolean extends Value { /*=T*/ accept/**/(ValueVisitor/**/ visitor) => visitor.visitBoolean(this); + Value or(Value other) => value ? this : other; + + Value and(Value other) => value ? other : this; + Value unaryNot() => value ? sassFalse : sassTrue; } diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 3a8a2ed5..fb626a73 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -2,6 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import '../exception.dart'; import '../visitor/interface/value.dart'; import '../value.dart'; @@ -16,6 +17,26 @@ class SassColor extends Value { /*=T*/ accept/**/(ValueVisitor/**/ visitor) => visitor.visitColor(this); + Value plus(Value other) { + if (other is! SassNumber && other is! SassColor) return super.plus(other); + throw new InternalException('Undefined operation "$this + $other".'); + } + + Value minus(Value other) { + if (other is! SassNumber && other is! SassColor) return super.minus(other); + throw new InternalException('Undefined operation "$this - $other".'); + } + + Value dividedBy(Value other) { + if (other is! SassNumber && other is! SassColor) { + return super.dividedBy(other); + } + throw new InternalException('Undefined operation "$this / $other".'); + } + + Value modulo(Value other) => + throw new InternalException('Undefined operation "$this % $other".'); + bool operator ==(other) => other is SassColor && other.red == red && diff --git a/lib/src/value/identifier.dart b/lib/src/value/identifier.dart index c907e9ff..7f7b4dba 100644 --- a/lib/src/value/identifier.dart +++ b/lib/src/value/identifier.dart @@ -15,7 +15,11 @@ class SassIdentifier extends Value { /*=T*/ accept/**/(ValueVisitor/**/ visitor) => visitor.visitIdentifier(this); - bool operator ==(other) => other is SassIdentifier && other.text == text; + bool operator ==(other) { + if (other is SassString) return text == other.text; + if (other is SassIdentifier) return text == other.text; + return false; + } int get hashCode => text.hashCode; } diff --git a/lib/src/value/null.dart b/lib/src/value/null.dart index ee13c5bd..70b3b501 100644 --- a/lib/src/value/null.dart +++ b/lib/src/value/null.dart @@ -14,5 +14,9 @@ class SassNull extends Value { /*=T*/ accept/**/(ValueVisitor/**/ visitor) => visitor.visitNull(this); + Value or(Value other) => other; + + Value and(Value other) => this; + Value unaryNot() => sassTrue; } diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 0b878eb6..97ef49fc 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -24,6 +24,54 @@ class SassNumber extends Value { /*=T*/ accept/**/(ValueVisitor/**/ visitor) => visitor.visitNumber(this); + SassBoolean greaterThan(Value other) { + if (other is SassNumber) return new SassBoolean(value > other.value); + throw new InternalException('Undefined operation "$this > $other".'); + } + + SassBoolean greaterThanOrEquals(Value other) { + if (other is SassNumber) return new SassBoolean(value >= other.value); + throw new InternalException('Undefined operation "$this >= $other".'); + } + + SassBoolean lessThan(Value other) { + if (other is SassNumber) return new SassBoolean(value < other.value); + throw new InternalException('Undefined operation "$this < $other".'); + } + + SassBoolean lessThanOrEquals(Value other) { + if (other is SassNumber) return new SassBoolean(value <= other.value); + throw new InternalException('Undefined operation "$this <= $other".'); + } + + Value times(Value other) { + if (other is SassNumber) return new SassNumber(value * other.value); + throw new InternalException('Undefined operation "$this * $other".'); + } + + Value modulo(Value other) { + if (other is SassNumber) return new SassNumber(value % other.value); + throw new InternalException('Undefined operation "$this % $other".'); + } + + Value plus(Value other) { + if (other is SassNumber) return new SassNumber(value + other.value); + if (other is! SassColor) return super.plus(other); + throw new InternalException('Undefined operation "$this + $other".'); + } + + Value minus(Value other) { + if (other is SassNumber) return new SassNumber(value - other.value); + if (other is! SassColor) return super.minus(other); + throw new InternalException('Undefined operation "$this - $other".'); + } + + Value dividedBy(Value other) { + if (other is SassNumber) return new SassNumber(value / other.value); + if (other is! SassColor) super.dividedBy(other); + throw new InternalException('Undefined operation "$this / $other".'); + } + Value unaryPlus() => this; Value unaryMinus() => new SassNumber(-value); diff --git a/lib/src/value/string.dart b/lib/src/value/string.dart index 1e116b19..50ed8d50 100644 --- a/lib/src/value/string.dart +++ b/lib/src/value/string.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import '../visitor/interface/value.dart'; +import '../visitor/serialize.dart'; import '../value.dart'; class SassString extends Value { @@ -12,4 +13,15 @@ class SassString extends Value { /*=T*/ accept/**/(ValueVisitor/**/ visitor) => visitor.visitString(this); + + Value plus(Value other) => new SassString( + text + (other is SassString ? other.text : valueToCss(other))); + + bool operator ==(other) { + if (other is SassString) return text == other.text; + if (other is SassIdentifier) return text == other.text; + return false; + } + + int get hashCode => text.hashCode; } diff --git a/lib/src/visitor/interface/expression.dart b/lib/src/visitor/interface/expression.dart index 55c256e3..89d007ae 100644 --- a/lib/src/visitor/interface/expression.dart +++ b/lib/src/visitor/interface/expression.dart @@ -5,6 +5,7 @@ import '../../ast/sass.dart'; abstract class ExpressionVisitor { + T visitBinaryOperationExpression(BinaryOperationExpression node); T visitBooleanExpression(BooleanExpression node); T visitColorExpression(ColorExpression node); T visitFunctionExpression(FunctionExpression node); diff --git a/lib/src/visitor/perform.dart b/lib/src/visitor/perform.dart index ab0a19a3..07fe1c99 100644 --- a/lib/src/visitor/perform.dart +++ b/lib/src/visitor/perform.dart @@ -570,6 +570,43 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor { // ## Expressions + Value visitBinaryOperationExpression(BinaryOperationExpression node) { + return _addExceptionSpan(() { + var left = node.left.accept(this); + var right = node.right.accept(this); + switch (node.operator) { + case BinaryOperator.or: + return left.or(right); + case BinaryOperator.and: + return left.and(right); + case BinaryOperator.equals: + return new SassBoolean(left == right); + case BinaryOperator.notEquals: + return new SassBoolean(left != right); + case BinaryOperator.greaterThan: + return left.greaterThan(right); + case BinaryOperator.greaterThanOrEquals: + return left.greaterThanOrEquals(right); + case BinaryOperator.lessThan: + return left.lessThan(right); + case BinaryOperator.lessThanOrEquals: + return left.lessThanOrEquals(right); + case BinaryOperator.plus: + return left.plus(right); + case BinaryOperator.minus: + return left.minus(right); + case BinaryOperator.times: + return left.times(right); + case BinaryOperator.dividedBy: + return left.dividedBy(right); + case BinaryOperator.modulo: + return left.modulo(right); + default: + return null; + } + }, node.span); + } + Value visitVariableExpression(VariableExpression node) { var result = _environment.getVariable(node.name); if (result != null) return result;