mirror of
https://github.com/danog/dart-sass.git
synced 2024-12-02 09:37:49 +01:00
Support get-function().
This commit is contained in:
parent
650ae831ec
commit
9ec89f6944
@ -3,6 +3,7 @@
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
export 'callable/built_in.dart';
|
||||
export 'callable/plain_css.dart';
|
||||
export 'callable/user_defined.dart';
|
||||
|
||||
/// An interface for objects, such as functions and mixins, that can be invoked
|
||||
|
18
lib/src/callable/plain_css.dart
Normal file
18
lib/src/callable/plain_css.dart
Normal 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;
|
||||
}
|
@ -880,6 +880,19 @@ void defineCoreFunctions(Environment environment) {
|
||||
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.
|
||||
|
||||
// ## Miscellaneous
|
||||
|
@ -6,6 +6,7 @@ import 'ast/selector.dart';
|
||||
import 'exception.dart';
|
||||
import 'value/boolean.dart';
|
||||
import 'value/color.dart';
|
||||
import 'value/function.dart';
|
||||
import 'value/list.dart';
|
||||
import 'value/map.dart';
|
||||
import 'value/number.dart';
|
||||
@ -16,6 +17,7 @@ import 'visitor/serialize.dart';
|
||||
export 'value/argument_list.dart';
|
||||
export 'value/boolean.dart';
|
||||
export 'value/color.dart';
|
||||
export 'value/function.dart';
|
||||
export 'value/list.dart';
|
||||
export 'value/map.dart';
|
||||
export 'value/null.dart';
|
||||
@ -79,6 +81,13 @@ abstract class Value {
|
||||
SassColor assertColor([String 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.
|
||||
///
|
||||
/// If this came from a function argument, [name] is the argument name
|
||||
|
28
lib/src/value/function.dart
Normal file
28
lib/src/value/function.dart
Normal 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;
|
||||
}
|
@ -10,6 +10,7 @@ import '../../value.dart';
|
||||
abstract class ValueVisitor<T> {
|
||||
T visitBoolean(SassBoolean value);
|
||||
T visitColor(SassColor value);
|
||||
T visitFunction(SassFunction value);
|
||||
T visitList(SassList value);
|
||||
T visitMap(SassMap value);
|
||||
T visitNull(SassNull value);
|
||||
|
@ -118,15 +118,28 @@ class _PerformVisitor
|
||||
: _loadPaths = loadPaths == null ? const [] : new List.from(loadPaths),
|
||||
_environment = environment ?? new Environment(),
|
||||
_color = color {
|
||||
_environment.defineFunction("call", r"$name, $args...", (arguments) {
|
||||
var name = arguments[0].assertString("name");
|
||||
_environment.defineFunction("call", r"$function, $args...", (arguments) {
|
||||
var function = arguments[0];
|
||||
var args = arguments[1] as SassArgumentList;
|
||||
|
||||
var expression = new FunctionExpression(
|
||||
new Interpolation([name.text], _callableSpan),
|
||||
new ArgumentInvocation([], {}, _callableSpan,
|
||||
rest: new ValueExpression(args)));
|
||||
return expression.accept(this);
|
||||
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(
|
||||
new Interpolation([function.text], _callableSpan), invocation);
|
||||
return expression.accept(this);
|
||||
}
|
||||
|
||||
return _runFunctionCallable(invocation,
|
||||
function.assertFunction("function").callable, _callableSpan);
|
||||
});
|
||||
}
|
||||
|
||||
@ -549,10 +562,10 @@ class _PerformVisitor
|
||||
}
|
||||
|
||||
if (node.children == null) {
|
||||
_runUserDefinedCallable(node, mixin, callback);
|
||||
_runUserDefinedCallable(node.arguments, mixin, node.span, callback);
|
||||
} else {
|
||||
var environment = _environment.closure();
|
||||
_runUserDefinedCallable(node, mixin, () {
|
||||
_runUserDefinedCallable(node.arguments, mixin, node.span, () {
|
||||
_environment.withContent(node.children, environment, callback);
|
||||
});
|
||||
}
|
||||
@ -898,58 +911,27 @@ class _PerformVisitor
|
||||
|
||||
Value visitFunctionExpression(FunctionExpression node) {
|
||||
var plainName = node.name.asPlain;
|
||||
if (plainName != null) {
|
||||
var function = _environment.getFunction(plainName);
|
||||
if (function != null) {
|
||||
if (function is BuiltInCallable) {
|
||||
return _runBuiltInCallable(node, function).withoutSlash();
|
||||
} 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;
|
||||
}
|
||||
var function =
|
||||
(plainName == null ? null : _environment.getFunction(plainName)) ??
|
||||
new PlainCssCallable(_performInterpolation(node.name));
|
||||
|
||||
throw _exception("Function finished without @return.",
|
||||
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(', ') +
|
||||
")");
|
||||
return _runFunctionCallable(node.arguments, function, node.span);
|
||||
}
|
||||
|
||||
/// Evaluates the arguments in [invocation] as applied to [callable], and
|
||||
/// Evaluates the arguments in [arguments] as applied to [callable], and
|
||||
/// invokes [run] in a scope with those arguments defined.
|
||||
Value _runUserDefinedCallable(CallableInvocation invocation,
|
||||
UserDefinedCallable callable, Value run()) {
|
||||
var triple = _evaluateArguments(invocation);
|
||||
Value _runUserDefinedCallable(ArgumentInvocation arguments,
|
||||
UserDefinedCallable callable, FileSpan span, Value run()) {
|
||||
var triple = _evaluateArguments(arguments, span);
|
||||
var positional = triple.item1;
|
||||
var named = triple.item2;
|
||||
var separator = triple.item3;
|
||||
|
||||
return _withStackFrame(callable.name + "()", invocation.span, () {
|
||||
return _withStackFrame(callable.name + "()", span, () {
|
||||
return _withEnvironment(callable.environment, () {
|
||||
return _environment.scope(() {
|
||||
_verifyArguments(positional.length, named,
|
||||
callable.declaration.arguments, invocation.span);
|
||||
_verifyArguments(
|
||||
positional.length, named, callable.declaration.arguments, 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
|
||||
@ -991,29 +973,64 @@ class _PerformVisitor
|
||||
throw _exception(
|
||||
"No ${pluralize('argument', named.keys.length)} named "
|
||||
"${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
|
||||
/// body.
|
||||
Value _runBuiltInCallable(
|
||||
CallableInvocation invocation, BuiltInCallable callable) {
|
||||
var triple = _evaluateArguments(invocation);
|
||||
ArgumentInvocation arguments, BuiltInCallable callable, FileSpan span) {
|
||||
var triple = _evaluateArguments(arguments, span);
|
||||
var positional = triple.item1;
|
||||
var named = triple.item2;
|
||||
var namedSet = named;
|
||||
var separator = triple.item3;
|
||||
|
||||
var oldCallableSpan = _callableSpan;
|
||||
_callableSpan = invocation.span;
|
||||
_callableSpan = span;
|
||||
int overloadIndex;
|
||||
for (var i = 0; i < callable.overloads.length - 1; i++) {
|
||||
try {
|
||||
_verifyArguments(positional.length, namedSet, callable.overloads[i],
|
||||
invocation.span);
|
||||
_verifyArguments(
|
||||
positional.length, namedSet, callable.overloads[i], span);
|
||||
overloadIndex = i;
|
||||
break;
|
||||
} on SassRuntimeException catch (_) {
|
||||
@ -1021,8 +1038,8 @@ class _PerformVisitor
|
||||
}
|
||||
}
|
||||
if (overloadIndex == null) {
|
||||
_verifyArguments(positional.length, namedSet, callable.overloads.last,
|
||||
invocation.span);
|
||||
_verifyArguments(
|
||||
positional.length, namedSet, callable.overloads.last, span);
|
||||
overloadIndex = callable.overloads.length - 1;
|
||||
}
|
||||
|
||||
@ -1052,7 +1069,7 @@ class _PerformVisitor
|
||||
positional.add(argumentList);
|
||||
}
|
||||
|
||||
var result = _addExceptionSpan(invocation.span, () => callback(positional));
|
||||
var result = _addExceptionSpan(span, () => callback(positional));
|
||||
_callableSpan = oldCallableSpan;
|
||||
|
||||
if (argumentList == null) return result;
|
||||
@ -1061,29 +1078,28 @@ class _PerformVisitor
|
||||
throw _exception(
|
||||
"No ${pluralize('argument', named.keys.length)} named "
|
||||
"${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
|
||||
/// list, if any.
|
||||
Tuple3<List<Value>, Map<String, Value>, ListSeparator> _evaluateArguments(
|
||||
CallableInvocation invocation) {
|
||||
var positional = invocation.arguments.positional
|
||||
ArgumentInvocation arguments, FileSpan span) {
|
||||
var positional = arguments.positional
|
||||
.map((expression) => expression.accept(this))
|
||||
.toList();
|
||||
var named = normalizedMapMap/*<String, Expression, Value>*/(
|
||||
invocation.arguments.named,
|
||||
var named = normalizedMapMap/*<String, Expression, Value>*/(arguments.named,
|
||||
value: (_, expression) => expression.accept(this));
|
||||
|
||||
if (invocation.arguments.rest == null) {
|
||||
if (arguments.rest == null) {
|
||||
return new Tuple3(positional, named, ListSeparator.undecided);
|
||||
}
|
||||
|
||||
var rest = invocation.arguments.rest.accept(this);
|
||||
var rest = arguments.rest.accept(this);
|
||||
var separator = ListSeparator.undecided;
|
||||
if (rest is SassMap) {
|
||||
_addRestMap(named, rest, invocation.span);
|
||||
_addRestMap(named, rest, span);
|
||||
} else if (rest is SassList) {
|
||||
positional.addAll(rest.asList);
|
||||
separator = rest.separator;
|
||||
@ -1096,22 +1112,21 @@ class _PerformVisitor
|
||||
positional.add(rest);
|
||||
}
|
||||
|
||||
if (invocation.arguments.keywordRest == null) {
|
||||
if (arguments.keywordRest == null) {
|
||||
return new Tuple3(positional, named, separator);
|
||||
}
|
||||
|
||||
var keywordRest = invocation.arguments.keywordRest.accept(this);
|
||||
var keywordRest = arguments.keywordRest.accept(this);
|
||||
if (keywordRest is SassMap) {
|
||||
_addRestMap(named, keywordRest, invocation.span);
|
||||
_addRestMap(named, keywordRest, span);
|
||||
return new Tuple3(positional, named, separator);
|
||||
} else {
|
||||
throw _exception(
|
||||
"Variable keyword arguments must be a map (was $keywordRest).",
|
||||
invocation.span);
|
||||
"Variable keyword arguments must be a map (was $keywordRest).", 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.
|
||||
///
|
||||
/// Returns the arguments as expressions so that they can be lazily evaluated
|
||||
|
@ -301,6 +301,16 @@ class _SerializeCssVisitor
|
||||
_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) {
|
||||
if (value.hasBrackets) {
|
||||
_buffer.writeCharCode($lbracket);
|
||||
|
Loading…
Reference in New Issue
Block a user