mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-26 20:24:42 +01:00
Add support for binary operations.
This commit is contained in:
parent
7fb7804a52
commit
9ac48fbc05
@ -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
|
||||
|
@ -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';
|
||||
|
77
lib/src/ast/sass/expression/binary_operation.dart
Normal file
77
lib/src/ast/sass/expression/binary_operation.dart
Normal file
@ -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/*<T>*/(ExpressionVisitor/*<T>*/ 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;
|
||||
}
|
@ -679,16 +679,63 @@ abstract class StylesheetParser extends Parser {
|
||||
|
||||
List<Expression> commaExpressions;
|
||||
List<Expression> 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<BinaryOperator> operators;
|
||||
|
||||
// The left-hand sides of [operators]. `operands[n]` is the left-hand side
|
||||
// of `operators[n]`.
|
||||
List<Expression> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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/*<T>*/(ValueVisitor/*<T>*/ 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)}");
|
||||
|
@ -20,5 +20,9 @@ class SassBoolean extends Value {
|
||||
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) =>
|
||||
visitor.visitBoolean(this);
|
||||
|
||||
Value or(Value other) => value ? this : other;
|
||||
|
||||
Value and(Value other) => value ? other : this;
|
||||
|
||||
Value unaryNot() => value ? sassFalse : sassTrue;
|
||||
}
|
||||
|
@ -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/*<T>*/(ValueVisitor/*<T>*/ 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 &&
|
||||
|
@ -15,7 +15,11 @@ class SassIdentifier extends Value {
|
||||
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ 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;
|
||||
}
|
||||
|
@ -14,5 +14,9 @@ class SassNull extends Value {
|
||||
|
||||
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) => visitor.visitNull(this);
|
||||
|
||||
Value or(Value other) => other;
|
||||
|
||||
Value and(Value other) => this;
|
||||
|
||||
Value unaryNot() => sassTrue;
|
||||
}
|
||||
|
@ -24,6 +24,54 @@ class SassNumber extends Value {
|
||||
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ 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);
|
||||
|
@ -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/*<T>*/(ValueVisitor/*<T>*/ 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;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
import '../../ast/sass.dart';
|
||||
|
||||
abstract class ExpressionVisitor<T> {
|
||||
T visitBinaryOperationExpression(BinaryOperationExpression node);
|
||||
T visitBooleanExpression(BooleanExpression node);
|
||||
T visitColorExpression(ColorExpression node);
|
||||
T visitFunctionExpression(FunctionExpression node);
|
||||
|
@ -570,6 +570,43 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
|
||||
|
||||
// ## 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;
|
||||
|
Loading…
Reference in New Issue
Block a user