mirror of
https://github.com/danog/dart-sass.git
synced 2024-12-02 17:49:38 +01:00
Support get-function().
This commit is contained in:
parent
650ae831ec
commit
9ec89f6944
@ -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
|
||||||
|
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));
|
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
|
||||||
|
@ -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
|
||||||
|
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> {
|
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);
|
||||||
|
@ -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 expression = new FunctionExpression(
|
var invocation = new ArgumentInvocation([], {}, _callableSpan,
|
||||||
new Interpolation([name.text], _callableSpan),
|
rest: new ValueExpression(args));
|
||||||
new ArgumentInvocation([], {}, _callableSpan,
|
|
||||||
rest: new ValueExpression(args)));
|
if (function is SassString) {
|
||||||
return expression.accept(this);
|
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) {
|
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();
|
|
||||||
} 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.",
|
return _runFunctionCallable(node.arguments, function, node.span);
|
||||||
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
|
/// Evaluates the arguments in [arguments] 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
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user