Add if().

This commit is contained in:
Natalie Weizenbaum 2016-09-24 02:52:08 -07:00 committed by Natalie Weizenbaum
parent 1987d0b055
commit b47067ea4e
6 changed files with 114 additions and 17 deletions

View File

@ -12,6 +12,7 @@ export 'sass/expression/binary_operation.dart';
export 'sass/expression/boolean.dart';
export 'sass/expression/color.dart';
export 'sass/expression/function.dart';
export 'sass/expression/if.dart';
export 'sass/expression/list.dart';
export 'sass/expression/map.dart';
export 'sass/expression/null.dart';

View File

@ -0,0 +1,27 @@
// 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 '../../../parse.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../argument_invocation.dart';
import '../callable_invocation.dart';
class IfExpression implements Expression, CallableInvocation {
static final declaration =
parseArgumentDeclaration(r"($condition, $if-true, $if-false)");
final ArgumentInvocation arguments;
final FileSpan span;
IfExpression(this.arguments, this.span);
/*=T*/ accept/*<T>*/(ExpressionVisitor/*<T>*/ visitor) =>
visitor.visitIfExpression(this);
String toString() => "if$arguments";
}

View File

@ -840,6 +840,11 @@ void defineCoreFunctions(Environment environment) {
// ## Miscellaneous
// This is only invoked using `call()`. Hand-authored `if()`s are parsed as
// [IfExpression]s.
environment.defineFunction("if", r"$condition, $if-true, $if-false",
(arguments) => arguments[0].isTruthy ? arguments[1] : arguments[2]);
environment.defineFunction("unique-id", "", (arguments) {
// Make it difficult to guess the next ID by randomizing the increase.
_uniqueID += _random.nextInt(36);

View File

@ -1342,17 +1342,20 @@ abstract class StylesheetParser extends Parser {
// TODO: url()
var identifier = _interpolatedIdentifier();
switch (identifier.asPlain) {
case "false":
return new BooleanExpression(false, identifier.span);
case "if":
var invocation = _argumentInvocation();
return new IfExpression(
invocation, spanForList([identifier, invocation]));
case "not":
whitespace();
return new UnaryOperationExpression(
UnaryOperator.not, _singleExpression(), identifier.span);
case "null":
return new NullExpression(identifier.span);
case "true":
return new BooleanExpression(true, identifier.span);
case "false":
return new BooleanExpression(false, identifier.span);
}
return scanner.peekChar() == $lparen

View File

@ -9,6 +9,7 @@ abstract class ExpressionVisitor<T> {
T visitBooleanExpression(BooleanExpression node);
T visitColorExpression(ColorExpression node);
T visitFunctionExpression(FunctionExpression node);
T visitIfExpression(IfExpression node);
T visitListExpression(ListExpression node);
T visitMapExpression(MapExpression node);
T visitNullExpression(NullExpression node);

View File

@ -651,6 +651,21 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
SassBoolean visitBooleanExpression(BooleanExpression node) =>
new SassBoolean(node.value);
Value visitIfExpression(IfExpression node) {
var pair = _evaluateMacroArguments(node);
var positional = pair.item1;
var named = pair.item2;
_verifyArguments(
positional.length, named, IfExpression.declaration, node.span);
var condition = positional.length > 0 ? positional[0] : named["condition"];
var ifTrue = positional.length > 1 ? positional[1] : named["if-true"];
var ifFalse = positional.length > 2 ? positional[2] : named["if-false"];
return (condition.accept(this).isTruthy ? ifTrue : ifFalse).accept(this);
}
SassNull visitNullExpression(NullExpression node) => sassNull;
SassNumber visitNumberExpression(NumberExpression node) =>
@ -725,7 +740,7 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
return _withEnvironment(callable.environment, () {
return _environment.scope(() {
_verifyArguments(
positional, named, callable.arguments, invocation.span);
positional.length, named, callable.arguments, invocation.span);
// TODO: if we get here and there are no rest params involved, mark
// the callable as fast-path and don't do error checking or extra
@ -778,13 +793,14 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
var triple = _evaluateArguments(invocation);
var positional = triple.item1;
var named = triple.item2;
var namedSet = named;
var separator = triple.item3;
int overloadIndex;
for (var i = 0; i < callable.overloads.length - 1; i++) {
try {
_verifyArguments(
positional, named, callable.overloads[i], invocation.span);
_verifyArguments(positional.length, namedSet, callable.overloads[i],
invocation.span);
overloadIndex = i;
break;
} on SassRuntimeException catch (_) {
@ -792,8 +808,8 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
}
}
if (overloadIndex == null) {
_verifyArguments(
positional, named, callable.overloads.last, invocation.span);
_verifyArguments(positional.length, namedSet, callable.overloads.last,
invocation.span);
overloadIndex = callable.overloads.length - 1;
}
@ -878,10 +894,54 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
}
}
void _addRestMap(Map<String, Value> values, SassMap map, FileSpan span) {
Tuple2<List<Expression>, Map<String, Expression>> _evaluateMacroArguments(
CallableInvocation invocation) {
if (invocation.arguments.rest == null) {
return new Tuple2(
invocation.arguments.positional, invocation.arguments.named);
}
var positional = invocation.arguments.positional.toList();
var named = normalizedMap/*<Expression>*/()
..addAll(invocation.arguments.named);
var rest = invocation.arguments.rest.accept(this);
if (rest is SassMap) {
_addRestMap(
named, rest, invocation.span, (value) => new ValueExpression(value));
} else if (rest is SassList) {
positional.addAll(rest.asList.map((value) => new ValueExpression(value)));
if (rest is SassArgumentList) {
rest.keywords.forEach((key, value) {
named[key] = new ValueExpression(value);
});
}
} else {
positional.add(new ValueExpression(rest));
}
if (invocation.arguments.keywordRest == null) {
return new Tuple2(positional, named);
}
var keywordRest = invocation.arguments.keywordRest.accept(this);
if (keywordRest is SassMap) {
_addRestMap(named, keywordRest, invocation.span,
(value) => new ValueExpression(value));
return new Tuple2(positional, named);
} else {
throw _exception(
"Variable keyword arguments must be a map (was $keywordRest).",
invocation.span);
}
}
void _addRestMap/*<T>*/(
Map<String, Object/*=T*/ > values, SassMap map, FileSpan span,
[/*=T*/ convert(Value value)]) {
convert ??= (value) => value as Object/*=T*/;
map.contents.forEach((key, value) {
if (key is SassString) {
values[key.text] = value;
values[key.text] = convert(value);
} else {
throw _exception(
"Variable keyword argument map must have string keys.\n"
@ -891,11 +951,11 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
});
}
void _verifyArguments(List<Value> positional, Map<String, Value> named,
void _verifyArguments(int positional, Map<String, dynamic> named,
ArgumentDeclaration arguments, FileSpan span) {
for (var i = 0; i < arguments.arguments.length; i++) {
var argument = arguments.arguments[i];
if (i < positional.length) {
if (i < positional) {
if (named.containsKey(argument.name)) {
throw _exception(
"Argument \$${argument.name} was passed both by position and by "
@ -910,16 +970,16 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
if (arguments.restArgument != null) return;
if (positional.length > arguments.arguments.length) {
if (positional > arguments.arguments.length) {
throw _exception(
"Only ${arguments.arguments.length} "
"${pluralize('argument', arguments.arguments.length)} allowed, "
"but ${positional.length} "
"${pluralize('was', positional.length, plural: 'were')} passed.",
"${pluralize('argument', arguments.arguments.length)} allowed, but "
"${positional} ${pluralize('was', positional, plural: 'were')} "
"passed.",
span);
}
if (arguments.arguments.length - positional.length < named.length) {
if (arguments.arguments.length - positional < named.length) {
var unknownNames = normalizedSet()
..addAll(named.keys)
..removeAll(arguments.arguments.map((argument) => argument.name));