Support get-function().

This commit is contained in:
Natalie Weizenbaum 2016-10-28 15:28:05 -07:00
parent 650ae831ec
commit 9ec89f6944
8 changed files with 170 additions and 75 deletions

View File

@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
export 'callable/built_in.dart'; export 'callable/built_in.dart';
export 'callable/plain_css.dart';
export 'callable/user_defined.dart'; export 'callable/user_defined.dart';
/// An interface for objects, such as functions and mixins, that can be invoked /// An interface for objects, such as functions and mixins, that can be invoked

View File

@ -0,0 +1,18 @@
// 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 '../callable.dart';
/// A callable that emits a plain CSS function.
///
/// This can't be used for mixins.
class PlainCssCallable implements Callable {
final String name;
PlainCssCallable(this.name);
bool operator ==(other) => other is PlainCssCallable && name == other.name;
int get hashCode => name.hashCode;
}

View File

@ -880,6 +880,19 @@ void defineCoreFunctions(Environment environment) {
return new SassBoolean(number1.isComparableTo(number2)); return new SassBoolean(number1.isComparableTo(number2));
}); });
environment.defineFunction("get-function", r"$name, $css: false",
(arguments) {
var name = arguments[0].assertString("name");
var css = arguments[1].isTruthy;
var callable = css
? new PlainCssCallable(name.text)
: environment.getFunction(name.text);
if (callable != null) return new SassFunction(callable);
throw new SassScriptException("Function not found: $name");
});
// call() is defined in _PerformVisitor to provide it access to private APIs. // call() is defined in _PerformVisitor to provide it access to private APIs.
// ## Miscellaneous // ## Miscellaneous

View File

@ -6,6 +6,7 @@ import 'ast/selector.dart';
import 'exception.dart'; import 'exception.dart';
import 'value/boolean.dart'; import 'value/boolean.dart';
import 'value/color.dart'; import 'value/color.dart';
import 'value/function.dart';
import 'value/list.dart'; import 'value/list.dart';
import 'value/map.dart'; import 'value/map.dart';
import 'value/number.dart'; import 'value/number.dart';
@ -16,6 +17,7 @@ import 'visitor/serialize.dart';
export 'value/argument_list.dart'; export 'value/argument_list.dart';
export 'value/boolean.dart'; export 'value/boolean.dart';
export 'value/color.dart'; export 'value/color.dart';
export 'value/function.dart';
export 'value/list.dart'; export 'value/list.dart';
export 'value/map.dart'; export 'value/map.dart';
export 'value/null.dart'; export 'value/null.dart';
@ -79,6 +81,13 @@ abstract class Value {
SassColor assertColor([String name]) => SassColor assertColor([String name]) =>
throw _exception("$this is not a color.", name); throw _exception("$this is not a color.", name);
/// Throws a [SassScriptException] if [this] isn't a function reference.
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`). It's used for debugging.
SassFunction assertFunction([String name]) =>
throw _exception("$this is not a function reference.", name);
/// Throws a [SassScriptException] if [this] isn't a map. /// Throws a [SassScriptException] if [this] isn't a map.
/// ///
/// If this came from a function argument, [name] is the argument name /// If this came from a function argument, [name] is the argument name

View File

@ -0,0 +1,28 @@
// 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 '../callable.dart';
import '../visitor/interface/value.dart';
import '../value.dart';
/// A SassScript function reference.
///
/// A function reference captures a function from the local environment so that
/// it may be passed between modules.
class SassFunction extends Value {
/// The callable that this function invokes.
final Callable callable;
SassFunction(this.callable);
/*=T*/ accept/*<T>*/(ValueVisitor/*<T>*/ visitor) =>
visitor.visitFunction(this);
SassFunction assertFunction([String name]) => this;
bool operator ==(other) =>
other is SassFunction && callable == other.callable;
int get hashCode => callable.hashCode;
}

View File

@ -10,6 +10,7 @@ import '../../value.dart';
abstract class ValueVisitor<T> { abstract class ValueVisitor<T> {
T visitBoolean(SassBoolean value); T visitBoolean(SassBoolean value);
T visitColor(SassColor value); T visitColor(SassColor value);
T visitFunction(SassFunction value);
T visitList(SassList value); T visitList(SassList value);
T visitMap(SassMap value); T visitMap(SassMap value);
T visitNull(SassNull value); T visitNull(SassNull value);

View File

@ -118,15 +118,28 @@ class _PerformVisitor
: _loadPaths = loadPaths == null ? const [] : new List.from(loadPaths), : _loadPaths = loadPaths == null ? const [] : new List.from(loadPaths),
_environment = environment ?? new Environment(), _environment = environment ?? new Environment(),
_color = color { _color = color {
_environment.defineFunction("call", r"$name, $args...", (arguments) { _environment.defineFunction("call", r"$function, $args...", (arguments) {
var name = arguments[0].assertString("name"); var function = arguments[0];
var args = arguments[1] as SassArgumentList; var args = arguments[1] as SassArgumentList;
var invocation = new ArgumentInvocation([], {}, _callableSpan,
rest: new ValueExpression(args));
if (function is SassString) {
warn(
"DEPRECATION WARNING: Passing a string to call() is deprecated and "
"will be illegal\n"
"in Sass 4.0. Use call(get-function($function)) instead.",
_callableSpan,
color: _color);
var expression = new FunctionExpression( var expression = new FunctionExpression(
new Interpolation([name.text], _callableSpan), new Interpolation([function.text], _callableSpan), invocation);
new ArgumentInvocation([], {}, _callableSpan,
rest: new ValueExpression(args)));
return expression.accept(this); return expression.accept(this);
}
return _runFunctionCallable(invocation,
function.assertFunction("function").callable, _callableSpan);
}); });
} }
@ -549,10 +562,10 @@ class _PerformVisitor
} }
if (node.children == null) { if (node.children == null) {
_runUserDefinedCallable(node, mixin, callback); _runUserDefinedCallable(node.arguments, mixin, node.span, callback);
} else { } else {
var environment = _environment.closure(); var environment = _environment.closure();
_runUserDefinedCallable(node, mixin, () { _runUserDefinedCallable(node.arguments, mixin, node.span, () {
_environment.withContent(node.children, environment, callback); _environment.withContent(node.children, environment, callback);
}); });
} }
@ -898,58 +911,27 @@ class _PerformVisitor
Value visitFunctionExpression(FunctionExpression node) { Value visitFunctionExpression(FunctionExpression node) {
var plainName = node.name.asPlain; var plainName = node.name.asPlain;
if (plainName != null) { var function =
var function = _environment.getFunction(plainName); (plainName == null ? null : _environment.getFunction(plainName)) ??
if (function != null) { new PlainCssCallable(_performInterpolation(node.name));
if (function is BuiltInCallable) {
return _runBuiltInCallable(node, function).withoutSlash(); return _runFunctionCallable(node.arguments, function, node.span);
} else if (function is UserDefinedCallable) {
return _runUserDefinedCallable(node, function, () {
for (var statement in function.declaration.children) {
var returnValue = statement.accept(this);
if (returnValue is Value) return returnValue;
} }
throw _exception("Function finished without @return.", /// Evaluates the arguments in [arguments] as applied to [callable], and
function.declaration.span);
}).withoutSlash();
} else {
return null;
}
}
}
if (node.arguments.named.isNotEmpty || node.arguments.keywordRest != null) {
throw _exception(
"Plain CSS functions don't support keyword arguments.", node.span);
}
var name = _performInterpolation(node.name);
var arguments = node.arguments.positional
.map((expression) => expression.accept(this))
.toList();
// TODO: if rest is an arglist that has keywords, error out.
var rest = node.arguments.rest?.accept(this);
if (rest != null) arguments.add(rest);
return new SassString("$name(" +
arguments.map((argument) => argument.toCssString()).join(', ') +
")");
}
/// Evaluates the arguments in [invocation] as applied to [callable], and
/// invokes [run] in a scope with those arguments defined. /// invokes [run] in a scope with those arguments defined.
Value _runUserDefinedCallable(CallableInvocation invocation, Value _runUserDefinedCallable(ArgumentInvocation arguments,
UserDefinedCallable callable, Value run()) { UserDefinedCallable callable, FileSpan span, Value run()) {
var triple = _evaluateArguments(invocation); var triple = _evaluateArguments(arguments, span);
var positional = triple.item1; var positional = triple.item1;
var named = triple.item2; var named = triple.item2;
var separator = triple.item3; var separator = triple.item3;
return _withStackFrame(callable.name + "()", invocation.span, () { return _withStackFrame(callable.name + "()", span, () {
return _withEnvironment(callable.environment, () { return _withEnvironment(callable.environment, () {
return _environment.scope(() { return _environment.scope(() {
_verifyArguments(positional.length, named, _verifyArguments(
callable.declaration.arguments, invocation.span); positional.length, named, callable.declaration.arguments, span);
// TODO: if we get here and there are no rest params involved, mark // 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 // the callable as fast-path and don't do error checking or extra
@ -991,29 +973,64 @@ class _PerformVisitor
throw _exception( throw _exception(
"No ${pluralize('argument', named.keys.length)} named " "No ${pluralize('argument', named.keys.length)} named "
"${toSentence(named.keys.map((name) => "\$$name"), 'or')}.", "${toSentence(named.keys.map((name) => "\$$name"), 'or')}.",
invocation.span); span);
}); });
}); });
}); });
} }
/// Evaluates [arguments] as applied to [callable].
Value _runFunctionCallable(
ArgumentInvocation arguments, Callable callable, FileSpan span) {
if (callable is BuiltInCallable) {
return _runBuiltInCallable(arguments, callable, span).withoutSlash();
} else if (callable is UserDefinedCallable) {
return _runUserDefinedCallable(arguments, callable, span, () {
for (var statement in callable.declaration.children) {
var returnValue = statement.accept(this);
if (returnValue is Value) return returnValue;
}
throw _exception(
"Function finished without @return.", callable.declaration.span);
}).withoutSlash();
} else if (callable is PlainCssCallable) {
if (arguments.named.isNotEmpty || arguments.keywordRest != null) {
throw _exception(
"Plain CSS functions don't support keyword arguments.", span);
}
var argumentValues = arguments.positional
.map((expression) => expression.accept(this))
.toList();
// TODO: if rest is an arglist that has keywords, error out.
var rest = arguments.rest?.accept(this);
if (rest != null) argumentValues.add(rest);
return new SassString("${callable.name}(" +
argumentValues.map((argument) => argument.toCssString()).join(', ') +
")");
} else {
return null;
}
}
/// Evaluates [invocation] as applied to [callable], and invokes [callable]'s /// Evaluates [invocation] as applied to [callable], and invokes [callable]'s
/// body. /// body.
Value _runBuiltInCallable( Value _runBuiltInCallable(
CallableInvocation invocation, BuiltInCallable callable) { ArgumentInvocation arguments, BuiltInCallable callable, FileSpan span) {
var triple = _evaluateArguments(invocation); var triple = _evaluateArguments(arguments, span);
var positional = triple.item1; var positional = triple.item1;
var named = triple.item2; var named = triple.item2;
var namedSet = named; var namedSet = named;
var separator = triple.item3; var separator = triple.item3;
var oldCallableSpan = _callableSpan; var oldCallableSpan = _callableSpan;
_callableSpan = invocation.span; _callableSpan = span;
int overloadIndex; int overloadIndex;
for (var i = 0; i < callable.overloads.length - 1; i++) { for (var i = 0; i < callable.overloads.length - 1; i++) {
try { try {
_verifyArguments(positional.length, namedSet, callable.overloads[i], _verifyArguments(
invocation.span); positional.length, namedSet, callable.overloads[i], span);
overloadIndex = i; overloadIndex = i;
break; break;
} on SassRuntimeException catch (_) { } on SassRuntimeException catch (_) {
@ -1021,8 +1038,8 @@ class _PerformVisitor
} }
} }
if (overloadIndex == null) { if (overloadIndex == null) {
_verifyArguments(positional.length, namedSet, callable.overloads.last, _verifyArguments(
invocation.span); positional.length, namedSet, callable.overloads.last, span);
overloadIndex = callable.overloads.length - 1; overloadIndex = callable.overloads.length - 1;
} }
@ -1052,7 +1069,7 @@ class _PerformVisitor
positional.add(argumentList); positional.add(argumentList);
} }
var result = _addExceptionSpan(invocation.span, () => callback(positional)); var result = _addExceptionSpan(span, () => callback(positional));
_callableSpan = oldCallableSpan; _callableSpan = oldCallableSpan;
if (argumentList == null) return result; if (argumentList == null) return result;
@ -1061,29 +1078,28 @@ class _PerformVisitor
throw _exception( throw _exception(
"No ${pluralize('argument', named.keys.length)} named " "No ${pluralize('argument', named.keys.length)} named "
"${toSentence(named.keys.map((name) => "\$$name"), 'or')}.", "${toSentence(named.keys.map((name) => "\$$name"), 'or')}.",
invocation.span); span);
} }
/// Evaluates the arguments in [invocation] and returns the positional and /// Evaluates the arguments in [arguments] and returns the positional and
/// named arguments, as well as the [ListSeparator] for the rest argument /// named arguments, as well as the [ListSeparator] for the rest argument
/// list, if any. /// list, if any.
Tuple3<List<Value>, Map<String, Value>, ListSeparator> _evaluateArguments( Tuple3<List<Value>, Map<String, Value>, ListSeparator> _evaluateArguments(
CallableInvocation invocation) { ArgumentInvocation arguments, FileSpan span) {
var positional = invocation.arguments.positional var positional = arguments.positional
.map((expression) => expression.accept(this)) .map((expression) => expression.accept(this))
.toList(); .toList();
var named = normalizedMapMap/*<String, Expression, Value>*/( var named = normalizedMapMap/*<String, Expression, Value>*/(arguments.named,
invocation.arguments.named,
value: (_, expression) => expression.accept(this)); value: (_, expression) => expression.accept(this));
if (invocation.arguments.rest == null) { if (arguments.rest == null) {
return new Tuple3(positional, named, ListSeparator.undecided); return new Tuple3(positional, named, ListSeparator.undecided);
} }
var rest = invocation.arguments.rest.accept(this); var rest = arguments.rest.accept(this);
var separator = ListSeparator.undecided; var separator = ListSeparator.undecided;
if (rest is SassMap) { if (rest is SassMap) {
_addRestMap(named, rest, invocation.span); _addRestMap(named, rest, span);
} else if (rest is SassList) { } else if (rest is SassList) {
positional.addAll(rest.asList); positional.addAll(rest.asList);
separator = rest.separator; separator = rest.separator;
@ -1096,22 +1112,21 @@ class _PerformVisitor
positional.add(rest); positional.add(rest);
} }
if (invocation.arguments.keywordRest == null) { if (arguments.keywordRest == null) {
return new Tuple3(positional, named, separator); return new Tuple3(positional, named, separator);
} }
var keywordRest = invocation.arguments.keywordRest.accept(this); var keywordRest = arguments.keywordRest.accept(this);
if (keywordRest is SassMap) { if (keywordRest is SassMap) {
_addRestMap(named, keywordRest, invocation.span); _addRestMap(named, keywordRest, span);
return new Tuple3(positional, named, separator); return new Tuple3(positional, named, separator);
} else { } else {
throw _exception( throw _exception(
"Variable keyword arguments must be a map (was $keywordRest).", "Variable keyword arguments must be a map (was $keywordRest).", span);
invocation.span);
} }
} }
/// Evaluates the arguments in [invocation] only as much as necessary to /// Evaluates the arguments in [arguments] only as much as necessary to
/// separate out positional and named arguments. /// separate out positional and named arguments.
/// ///
/// Returns the arguments as expressions so that they can be lazily evaluated /// Returns the arguments as expressions so that they can be lazily evaluated

View File

@ -301,6 +301,16 @@ class _SerializeCssVisitor
_buffer.writeCharCode(hexCharFor(color & 0xF)); _buffer.writeCharCode(hexCharFor(color & 0xF));
} }
void visitFunction(SassFunction function) {
if (!_inspect) {
throw new SassScriptException("$function isn't a valid CSS value.");
}
_buffer.write("get-function(");
_visitQuotedString(function.callable.name);
_buffer.writeCharCode($rparen);
}
void visitList(SassList value) { void visitList(SassList value) {
if (value.hasBrackets) { if (value.hasBrackets) {
_buffer.writeCharCode($lbracket); _buffer.writeCharCode($lbracket);