diff --git a/lib/src/ast/sass.dart b/lib/src/ast/sass.dart index e7c777e9..96f665f9 100644 --- a/lib/src/ast/sass.dart +++ b/lib/src/ast/sass.dart @@ -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'; diff --git a/lib/src/ast/sass/expression/if.dart b/lib/src/ast/sass/expression/if.dart new file mode 100644 index 00000000..7da5651e --- /dev/null +++ b/lib/src/ast/sass/expression/if.dart @@ -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/**/(ExpressionVisitor/**/ visitor) => + visitor.visitIfExpression(this); + + String toString() => "if$arguments"; +} diff --git a/lib/src/functions.dart b/lib/src/functions.dart index d7e084e3..6ff9c773 100644 --- a/lib/src/functions.dart +++ b/lib/src/functions.dart @@ -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); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 085a259d..14049f39 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -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 diff --git a/lib/src/visitor/interface/expression.dart b/lib/src/visitor/interface/expression.dart index 353042af..74e21547 100644 --- a/lib/src/visitor/interface/expression.dart +++ b/lib/src/visitor/interface/expression.dart @@ -9,6 +9,7 @@ abstract class ExpressionVisitor { 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); diff --git a/lib/src/visitor/perform.dart b/lib/src/visitor/perform.dart index fcef41e4..d2c54d79 100644 --- a/lib/src/visitor/perform.dart +++ b/lib/src/visitor/perform.dart @@ -651,6 +651,21 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor { 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 { 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 { 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 { } } 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 { } } - void _addRestMap(Map values, SassMap map, FileSpan span) { + Tuple2, Map> _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/**/() + ..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/**/( + Map 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 { }); } - void _verifyArguments(List positional, Map named, + void _verifyArguments(int positional, Map 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 { 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));