mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-30 04:39:03 +01:00
Merge branch 'master' into math-functions
This commit is contained in:
commit
4c0c6b48e4
@ -27,6 +27,11 @@
|
||||
|
||||
* Add the variables `$pi` and `$e` to the built-in "sass:math" module.
|
||||
|
||||
## 1.24.5
|
||||
|
||||
* Highlight contextually-relevant sections of the stylesheet in error messages,
|
||||
rather than only highlighting the section where the error was detected.
|
||||
|
||||
## 1.24.4
|
||||
|
||||
### JavaScript API
|
||||
|
@ -11,4 +11,24 @@ abstract class AstNode {
|
||||
/// This indicates where in the source Sass or SCSS stylesheet the node was
|
||||
/// defined.
|
||||
FileSpan get span;
|
||||
|
||||
/// Returns an [AstNode] that doesn't have any data and whose span is
|
||||
/// generated by [callback].
|
||||
///
|
||||
/// A number of APIs take [AstNode]s instead of spans because computing spans
|
||||
/// eagerly can be expensive. This allows arbitrary spans to be passed to
|
||||
/// those callbacks while still being lazily computed.
|
||||
factory AstNode.fake(FileSpan Function() callback) = _FakeAstNode;
|
||||
|
||||
AstNode();
|
||||
}
|
||||
|
||||
/// An [AstNode] that just exposes a single span generated by a callback.
|
||||
class _FakeAstNode implements AstNode {
|
||||
FileSpan get span => _callback();
|
||||
|
||||
/// The callback to use to generate [span].
|
||||
final FileSpan Function() _callback;
|
||||
|
||||
_FakeAstNode(this._callback);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import '../../exception.dart';
|
||||
import '../../logger.dart';
|
||||
import '../../parse/scss.dart';
|
||||
import '../../utils.dart';
|
||||
import '../../util/character.dart';
|
||||
import 'argument.dart';
|
||||
import 'node.dart';
|
||||
|
||||
@ -22,6 +23,32 @@ class ArgumentDeclaration implements SassNode {
|
||||
|
||||
final FileSpan span;
|
||||
|
||||
/// Returns [span] expanded to include an identifier immediately before the
|
||||
/// declaration, if possible.
|
||||
FileSpan get spanWithName {
|
||||
var text = span.file.getText(0);
|
||||
|
||||
// Move backwards through and whitspace between the name and the arguments.
|
||||
var i = span.start.offset - 1;
|
||||
while (i > 0 && isWhitespace(text.codeUnitAt(i))) {
|
||||
i--;
|
||||
}
|
||||
|
||||
// Then move backwards through the name itself.
|
||||
if (!isName(text.codeUnitAt(i))) return span;
|
||||
i--;
|
||||
while (i >= 0 && isName(text.codeUnitAt(i))) {
|
||||
i--;
|
||||
}
|
||||
|
||||
// If the name didn't start with [isNameStart], it's not a valid identifier.
|
||||
if (!isNameStart(text.codeUnitAt(i + 1))) return span;
|
||||
|
||||
// Trim because it's possible that this span is empty (for example, a mixin
|
||||
// may be declared without an argument list).
|
||||
return span.file.span(i + 1, span.end.offset).trim();
|
||||
}
|
||||
|
||||
/// The name of the rest argument as written in the document, without
|
||||
/// underscores converted to hyphens and including the leading `$`.
|
||||
///
|
||||
@ -47,16 +74,15 @@ class ArgumentDeclaration implements SassNode {
|
||||
: arguments = const [],
|
||||
restArgument = null;
|
||||
|
||||
/// Parses an argument declaration from [contents], which should not include
|
||||
/// parentheses.
|
||||
/// Parses an argument declaration from [contents], which should be of the
|
||||
/// form `@rule name(args) {`.
|
||||
///
|
||||
/// If passed, [url] is the name of the file from which [contents] comes.
|
||||
///
|
||||
/// Throws a [SassFormatException] if parsing fails.
|
||||
factory ArgumentDeclaration.parse(String contents,
|
||||
{Object url, Logger logger}) =>
|
||||
ScssParser("($contents)", url: url, logger: logger)
|
||||
.parseArgumentDeclaration();
|
||||
ScssParser(contents, url: url, logger: logger).parseArgumentDeclaration();
|
||||
|
||||
/// Throws a [SassScriptException] if [positional] and [names] aren't valid
|
||||
/// for this argument declaration.
|
||||
@ -73,27 +99,34 @@ class ArgumentDeclaration implements SassNode {
|
||||
} else if (names.contains(argument.name)) {
|
||||
namedUsed++;
|
||||
} else if (argument.defaultValue == null) {
|
||||
throw SassScriptException(
|
||||
"Missing argument ${_originalArgumentName(argument.name)}.");
|
||||
throw MultiSpanSassScriptException(
|
||||
"Missing argument ${_originalArgumentName(argument.name)}.",
|
||||
"invocation",
|
||||
{spanWithName: "declaration"});
|
||||
}
|
||||
}
|
||||
|
||||
if (restArgument != null) return;
|
||||
|
||||
if (positional > arguments.length) {
|
||||
throw SassScriptException("Only ${arguments.length} "
|
||||
"${names.isEmpty ? '' : 'positional '}"
|
||||
"${pluralize('argument', arguments.length)} allowed, but "
|
||||
"${positional} ${pluralize('was', positional, plural: 'were')} "
|
||||
"passed.");
|
||||
throw MultiSpanSassScriptException(
|
||||
"Only ${arguments.length} "
|
||||
"${names.isEmpty ? '' : 'positional '}"
|
||||
"${pluralize('argument', arguments.length)} allowed, but "
|
||||
"${positional} ${pluralize('was', positional, plural: 'were')} "
|
||||
"passed.",
|
||||
"invocation",
|
||||
{spanWithName: "declaration"});
|
||||
}
|
||||
|
||||
if (namedUsed < names.length) {
|
||||
var unknownNames = Set.of(names)
|
||||
..removeAll(arguments.map((argument) => argument.name));
|
||||
throw SassScriptException(
|
||||
throw MultiSpanSassScriptException(
|
||||
"No ${pluralize('argument', unknownNames.length)} named "
|
||||
"${toSentence(unknownNames.map((name) => "\$$name"), 'or')}.");
|
||||
"${toSentence(unknownNames.map((name) => "\$$name"), 'or')}.",
|
||||
"invocation",
|
||||
{spanWithName: "declaration"});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,8 @@ import '../callable_invocation.dart';
|
||||
/// evaluated.
|
||||
class IfExpression implements Expression, CallableInvocation {
|
||||
/// The declaration of `if()`, as though it were a normal function.
|
||||
static final declaration =
|
||||
ArgumentDeclaration.parse(r"$condition, $if-true, $if-false");
|
||||
static final declaration = ArgumentDeclaration.parse(
|
||||
r"@function if($condition, $if-true, $if-false) {");
|
||||
|
||||
/// The arguments passed to `if()`.
|
||||
final ArgumentInvocation arguments;
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'package:source_span/source_span.dart';
|
||||
|
||||
import '../../../utils.dart';
|
||||
import '../../../visitor/interface/statement.dart';
|
||||
import '../argument_invocation.dart';
|
||||
import '../callable_invocation.dart';
|
||||
@ -29,6 +30,11 @@ class IncludeRule implements Statement, CallableInvocation {
|
||||
|
||||
final FileSpan span;
|
||||
|
||||
/// Returns this include's span, without its content block (if it has one).
|
||||
FileSpan get spanWithoutContent => content == null
|
||||
? span
|
||||
: span.file.span(span.start.offset, arguments.span.end.offset).trim();
|
||||
|
||||
IncludeRule(this.name, this.arguments, this.span,
|
||||
{this.namespace, this.content});
|
||||
|
||||
|
@ -35,16 +35,32 @@ class AsyncEnvironment {
|
||||
Map<String, Module> get modules => UnmodifiableMapView(_modules);
|
||||
final Map<String, Module> _modules;
|
||||
|
||||
/// A map from module namespaces to the nodes whose spans indicate where those
|
||||
/// modules were originally loaded.
|
||||
final Map<String, AstNode> _namespaceNodes;
|
||||
|
||||
/// The namespaceless modules used in the current scope.
|
||||
///
|
||||
/// This is `null` if there are no namespaceless modules.
|
||||
Set<Module> _globalModules;
|
||||
|
||||
/// A map from modules in [_globalModules] to the nodes whose spans
|
||||
/// indicate where those modules were originally loaded.
|
||||
///
|
||||
/// This is `null` if there are no namespaceless modules.
|
||||
Map<Module, AstNode> _globalModuleNodes;
|
||||
|
||||
/// The modules forwarded by this module.
|
||||
///
|
||||
/// This is `null` if there are no forwarded modules.
|
||||
List<Module> _forwardedModules;
|
||||
|
||||
/// A map from modules in [_forwardedModules] to the nodes whose spans
|
||||
/// indicate where those modules were originally forwarded.
|
||||
///
|
||||
/// This is `null` if there are no forwarded modules.
|
||||
Map<Module, AstNode> _forwardedModuleNodes;
|
||||
|
||||
/// Modules forwarded by nested imports at each lexical scope level *beneath
|
||||
/// the global scope*.
|
||||
///
|
||||
@ -136,8 +152,11 @@ class AsyncEnvironment {
|
||||
/// If [sourceMap] is `true`, this tracks variables' source locations
|
||||
AsyncEnvironment({bool sourceMap = false})
|
||||
: _modules = {},
|
||||
_namespaceNodes = {},
|
||||
_globalModules = null,
|
||||
_globalModuleNodes = null,
|
||||
_forwardedModules = null,
|
||||
_forwardedModuleNodes = null,
|
||||
_nestedForwardedModules = null,
|
||||
_allModules = [],
|
||||
_variables = [{}],
|
||||
@ -150,8 +169,11 @@ class AsyncEnvironment {
|
||||
|
||||
AsyncEnvironment._(
|
||||
this._modules,
|
||||
this._namespaceNodes,
|
||||
this._globalModules,
|
||||
this._globalModuleNodes,
|
||||
this._forwardedModules,
|
||||
this._forwardedModuleNodes,
|
||||
this._nestedForwardedModules,
|
||||
this._allModules,
|
||||
this._variables,
|
||||
@ -174,8 +196,11 @@ class AsyncEnvironment {
|
||||
/// when the closure was created will be reflected.
|
||||
AsyncEnvironment closure() => AsyncEnvironment._(
|
||||
_modules,
|
||||
_namespaceNodes,
|
||||
_globalModules,
|
||||
_globalModuleNodes,
|
||||
_forwardedModules,
|
||||
_forwardedModuleNodes,
|
||||
_nestedForwardedModules,
|
||||
_allModules,
|
||||
_variables.toList(),
|
||||
@ -190,6 +215,9 @@ class AsyncEnvironment {
|
||||
/// functions, and mixins, but not its modules.
|
||||
AsyncEnvironment global() => AsyncEnvironment._(
|
||||
{},
|
||||
{},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@ -202,16 +230,20 @@ class AsyncEnvironment {
|
||||
|
||||
/// Adds [module] to the set of modules visible in this environment.
|
||||
///
|
||||
/// [nodeWithSpan]'s span is used to report any errors with the module.
|
||||
///
|
||||
/// If [namespace] is passed, the module is made available under that
|
||||
/// namespace.
|
||||
///
|
||||
/// Throws a [SassScriptException] if there's already a module with the given
|
||||
/// [namespace], or if [namespace] is `null` and [module] defines a variable
|
||||
/// with the same name as a variable defined in this environment.
|
||||
void addModule(Module module, {String namespace}) {
|
||||
void addModule(Module module, AstNode nodeWithSpan, {String namespace}) {
|
||||
if (namespace == null) {
|
||||
_globalModules ??= {};
|
||||
_globalModuleNodes ??= {};
|
||||
_globalModules.add(module);
|
||||
_globalModuleNodes[module] = nodeWithSpan;
|
||||
_allModules.add(module);
|
||||
|
||||
for (var name in _variables.first.keys) {
|
||||
@ -223,11 +255,14 @@ class AsyncEnvironment {
|
||||
}
|
||||
} else {
|
||||
if (_modules.containsKey(namespace)) {
|
||||
throw SassScriptException(
|
||||
"There's already a module with namespace \"$namespace\".");
|
||||
throw MultiSpanSassScriptException(
|
||||
"There's already a module with namespace \"$namespace\".",
|
||||
"new @use",
|
||||
{_namespaceNodes[namespace].span: "original @use"});
|
||||
}
|
||||
|
||||
_modules[namespace] = module;
|
||||
_namespaceNodes[namespace] = nodeWithSpan;
|
||||
_allModules.add(module);
|
||||
}
|
||||
}
|
||||
@ -236,12 +271,15 @@ class AsyncEnvironment {
|
||||
/// defined in this module, according to the modifications defined by [rule].
|
||||
void forwardModule(Module module, ForwardRule rule) {
|
||||
_forwardedModules ??= [];
|
||||
_forwardedModuleNodes ??= {};
|
||||
|
||||
var view = ForwardedModuleView(module, rule);
|
||||
for (var other in _forwardedModules) {
|
||||
_assertNoConflicts(view.variables, other.variables, "variable", other);
|
||||
_assertNoConflicts(view.functions, other.functions, "function", other);
|
||||
_assertNoConflicts(view.mixins, other.mixins, "mixin", other);
|
||||
_assertNoConflicts(
|
||||
view.variables, other.variables, "variable", other, rule);
|
||||
_assertNoConflicts(
|
||||
view.functions, other.functions, "function", other, rule);
|
||||
_assertNoConflicts(view.mixins, other.mixins, "mixin", other, rule);
|
||||
}
|
||||
|
||||
// Add the original module to [_allModules] (rather than the
|
||||
@ -250,14 +288,19 @@ class AsyncEnvironment {
|
||||
// CSS, not for the members they expose.
|
||||
_allModules.add(module);
|
||||
_forwardedModules.add(view);
|
||||
_forwardedModuleNodes[view] = rule;
|
||||
}
|
||||
|
||||
/// Throws a [SassScriptException] if [newMembers] has any keys that overlap
|
||||
/// with [oldMembers].
|
||||
///
|
||||
/// The [type] and [oldModule] is used for error reporting.
|
||||
void _assertNoConflicts(Map<String, Object> newMembers,
|
||||
Map<String, Object> oldMembers, String type, Module oldModule) {
|
||||
/// The [type], [other], [newModuleNodeWithSpan] are used for error reporting.
|
||||
void _assertNoConflicts(
|
||||
Map<String, Object> newMembers,
|
||||
Map<String, Object> oldMembers,
|
||||
String type,
|
||||
Module other,
|
||||
AstNode newModuleNodeWithSpan) {
|
||||
Map<String, Object> smaller;
|
||||
Map<String, Object> larger;
|
||||
if (newMembers.length < oldMembers.length) {
|
||||
@ -271,9 +314,10 @@ class AsyncEnvironment {
|
||||
for (var name in smaller.keys) {
|
||||
if (larger.containsKey(name)) {
|
||||
if (type == "variable") name = "\$$name";
|
||||
throw SassScriptException(
|
||||
'Module ${p.prettyUri(oldModule.url)} and the new module both '
|
||||
'forward a $type named $name.');
|
||||
throw MultiSpanSassScriptException(
|
||||
'Two forwarded modules both define a $type named $name.',
|
||||
"new @forward",
|
||||
{_forwardedModuleNodes[other].span: "original @forward"});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -288,7 +332,9 @@ class AsyncEnvironment {
|
||||
if (forwarded == null) return;
|
||||
|
||||
_globalModules ??= {};
|
||||
_globalModuleNodes ??= {};
|
||||
_forwardedModules ??= [];
|
||||
_forwardedModuleNodes ??= {};
|
||||
|
||||
var forwardedVariableNames =
|
||||
forwarded.expand((module) => module.variables.keys).toSet();
|
||||
@ -308,6 +354,7 @@ class AsyncEnvironment {
|
||||
if (shadowed != null) {
|
||||
_globalModules.remove(module);
|
||||
_globalModules.add(shadowed);
|
||||
_globalModuleNodes[shadowed] = _globalModuleNodes.remove(module);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < _forwardedModules.length; i++) {
|
||||
@ -316,11 +363,17 @@ class AsyncEnvironment {
|
||||
variables: forwardedVariableNames,
|
||||
mixins: forwardedMixinNames,
|
||||
functions: forwardedFunctionNames);
|
||||
if (shadowed != null) _forwardedModules[i] = shadowed;
|
||||
if (shadowed != null) {
|
||||
_forwardedModules[i] = shadowed;
|
||||
_forwardedModuleNodes[shadowed] =
|
||||
_forwardedModuleNodes.remove(module);
|
||||
}
|
||||
}
|
||||
|
||||
_globalModules.addAll(forwarded);
|
||||
_globalModuleNodes.addAll(module._environment._forwardedModuleNodes);
|
||||
_forwardedModules.addAll(forwarded);
|
||||
_forwardedModuleNodes.addAll(module._environment._forwardedModuleNodes);
|
||||
} else {
|
||||
_nestedForwardedModules ??=
|
||||
List.generate(_variables.length - 1, (_) => []);
|
||||
@ -746,7 +799,7 @@ class AsyncEnvironment {
|
||||
configuration[name] = ConfiguredValue(values[name], null, nodes[name]);
|
||||
}
|
||||
}
|
||||
return Configuration(configuration, isImplicit: true);
|
||||
return Configuration.implicit(configuration);
|
||||
}
|
||||
|
||||
/// Returns a module that represents the top-level members defined in [this],
|
||||
@ -795,11 +848,12 @@ class AsyncEnvironment {
|
||||
if (valueInModule == null) continue;
|
||||
|
||||
if (value != null) {
|
||||
throw SassScriptException(
|
||||
'This $type is available from multiple global modules:\n' +
|
||||
bulletedList(_globalModules
|
||||
.where((module) => callback(module) != null)
|
||||
.map((module) => p.prettyUri(module.url))));
|
||||
throw MultiSpanSassScriptException(
|
||||
'This $type is available from multiple global modules.',
|
||||
'$type use', {
|
||||
for (var entry in _globalModuleNodes.entries)
|
||||
if (callback(entry.key) != null) entry.value.span: 'includes $type'
|
||||
});
|
||||
}
|
||||
|
||||
value = valueInModule;
|
||||
|
@ -15,8 +15,8 @@ export 'callable/built_in.dart';
|
||||
export 'callable/plain_css.dart';
|
||||
export 'callable/user_defined.dart';
|
||||
|
||||
/// An interface functions and mixins that can be invoked from Sass by passing
|
||||
/// in arguments.
|
||||
/// An interface for functions and mixins that can be invoked from Sass by
|
||||
/// passing in arguments.
|
||||
///
|
||||
/// This extends [AsyncCallable] because all synchronous callables are also
|
||||
/// usable in asynchronous contexts. [Callable]s are usable with both the
|
||||
@ -65,7 +65,12 @@ export 'callable/user_defined.dart';
|
||||
/// access a string's length in code points.
|
||||
@sealed
|
||||
abstract class Callable extends AsyncCallable {
|
||||
/// Creates a callable with the given [name] and [arguments] that runs
|
||||
@Deprecated('Use `Callable.function` instead.')
|
||||
factory Callable(String name, String arguments,
|
||||
ext.Value callback(List<ext.Value> arguments)) =>
|
||||
Callable.function(name, arguments, callback);
|
||||
|
||||
/// Creates a function with the given [name] and [arguments] that runs
|
||||
/// [callback] when called.
|
||||
///
|
||||
/// The argument declaration is parsed from [arguments], which uses the same
|
||||
@ -80,7 +85,8 @@ abstract class Callable extends AsyncCallable {
|
||||
/// For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// new Callable("str-split", r'$string, $divider: " "', (arguments) {
|
||||
/// new Callable.function("str-split", r'$string, $divider: " "',
|
||||
/// (arguments) {
|
||||
/// var string = arguments[0].assertString("string");
|
||||
/// var divider = arguments[1].assertString("divider");
|
||||
/// return new SassList(
|
||||
@ -90,12 +96,12 @@ abstract class Callable extends AsyncCallable {
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Callables may also take variable length argument lists. These are declared
|
||||
/// Functions may also take variable length argument lists. These are declared
|
||||
/// the same way as in Sass, and are passed as the final argument to the
|
||||
/// callback. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// new Callable("str-join", r'$strings...', (arguments) {
|
||||
/// new Callable.function("str-join", r'$strings...', (arguments) {
|
||||
/// var args = arguments.first as SassArgumentList;
|
||||
/// var strings = args.map((arg) => arg.assertString()).toList();
|
||||
/// return new SassString(strings.map((string) => string.text).join(),
|
||||
@ -106,8 +112,8 @@ abstract class Callable extends AsyncCallable {
|
||||
/// Note that the argument list is always an instance of [SassArgumentList],
|
||||
/// which provides access to keyword arguments using
|
||||
/// [SassArgumentList.keywords].
|
||||
factory Callable(String name, String arguments,
|
||||
factory Callable.function(String name, String arguments,
|
||||
ext.Value callback(List<ext.Value> arguments)) =>
|
||||
BuiltInCallable(
|
||||
BuiltInCallable.function(
|
||||
name, arguments, (arguments) => callback(arguments) as Value);
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ import '../value.dart';
|
||||
import '../value/external/value.dart' as ext;
|
||||
import 'async_built_in.dart';
|
||||
|
||||
/// An interface functions and mixins that can be invoked from Sass by passing
|
||||
/// in arguments.
|
||||
/// An interface for functions and mixins that can be invoked from Sass by
|
||||
/// passing in arguments.
|
||||
///
|
||||
/// This class represents callables that *need* to do asynchronous work. It's
|
||||
/// only compatible with the asynchonous `compile()` methods. If a callback can
|
||||
@ -23,6 +23,11 @@ abstract class AsyncCallable {
|
||||
/// The callable's name.
|
||||
String get name;
|
||||
|
||||
@Deprecated('Use `AsyncCallable.function` instead.')
|
||||
factory AsyncCallable(String name, String arguments,
|
||||
FutureOr<ext.Value> callback(List<ext.Value> arguments)) =>
|
||||
AsyncCallable.function(name, arguments, callback);
|
||||
|
||||
/// Creates a callable with the given [name] and [arguments] that runs
|
||||
/// [callback] when called.
|
||||
///
|
||||
@ -30,9 +35,9 @@ abstract class AsyncCallable {
|
||||
/// include parentheses. Throws a [SassFormatException] if parsing fails.
|
||||
///
|
||||
/// See [new Callable] for more details.
|
||||
factory AsyncCallable(String name, String arguments,
|
||||
factory AsyncCallable.function(String name, String arguments,
|
||||
FutureOr<ext.Value> callback(List<ext.Value> arguments)) =>
|
||||
AsyncBuiltInCallable(name, arguments, (arguments) {
|
||||
AsyncBuiltInCallable.function(name, arguments, (arguments) {
|
||||
var result = callback(arguments);
|
||||
if (result is ext.Value) return result as Value;
|
||||
return (result as Future<ext.Value>).then((value) => value as Value);
|
||||
|
@ -22,36 +22,49 @@ typedef _Callback = FutureOr<Value> Function(List<Value> arguments);
|
||||
class AsyncBuiltInCallable implements AsyncCallable {
|
||||
final String name;
|
||||
|
||||
/// The overloads declared for this callable.
|
||||
final _overloads = <Tuple2<ArgumentDeclaration, _Callback>>[];
|
||||
/// This callable's arguments.
|
||||
final ArgumentDeclaration _arguments;
|
||||
|
||||
/// Creates a callable with a single [arguments] declaration and a single
|
||||
/// The callback to run when executing this callable.
|
||||
final _Callback _callback;
|
||||
|
||||
/// Creates a function with a single [arguments] declaration and a single
|
||||
/// [callback].
|
||||
///
|
||||
/// The argument declaration is parsed from [arguments], which should not
|
||||
/// include parentheses. Throws a [SassFormatException] if parsing fails.
|
||||
AsyncBuiltInCallable(String name, String arguments,
|
||||
FutureOr<Value> callback(List<Value> arguments))
|
||||
: this.parsed(name, ArgumentDeclaration.parse(arguments), callback);
|
||||
///
|
||||
/// If passed, [url] is the URL of the module in which the function is
|
||||
/// defined.
|
||||
AsyncBuiltInCallable.function(String name, String arguments,
|
||||
FutureOr<Value> callback(List<Value> arguments), {Object url})
|
||||
: this.parsed(
|
||||
name,
|
||||
ArgumentDeclaration.parse('@function $name($arguments) {',
|
||||
url: url),
|
||||
callback);
|
||||
|
||||
/// Creates a mixin with a single [arguments] declaration and a single
|
||||
/// [callback].
|
||||
///
|
||||
/// The argument declaration is parsed from [arguments], which should not
|
||||
/// include parentheses. Throws a [SassFormatException] if parsing fails.
|
||||
///
|
||||
/// If passed, [url] is the URL of the module in which the mixin is
|
||||
/// defined.
|
||||
AsyncBuiltInCallable.mixin(String name, String arguments,
|
||||
FutureOr<void> callback(List<Value> arguments),
|
||||
{Object url})
|
||||
: this.parsed(name,
|
||||
ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url),
|
||||
(arguments) async {
|
||||
await callback(arguments);
|
||||
return null;
|
||||
});
|
||||
|
||||
/// Creates a callable with a single [arguments] declaration and a single
|
||||
/// [callback].
|
||||
AsyncBuiltInCallable.parsed(this.name, ArgumentDeclaration arguments,
|
||||
FutureOr<Value> callback(List<Value> arguments)) {
|
||||
_overloads.add(Tuple2(arguments, callback));
|
||||
}
|
||||
|
||||
/// Creates a callable with multiple implementations.
|
||||
///
|
||||
/// Each key/value pair in [overloads] defines the argument declaration for
|
||||
/// the overload (which should not include parentheses), and the callback to
|
||||
/// execute if that argument declaration matches. Throws a
|
||||
/// [SassFormatException] if parsing fails.
|
||||
AsyncBuiltInCallable.overloaded(this.name, Map<String, _Callback> overloads) {
|
||||
overloads.forEach((arguments, callback) {
|
||||
_overloads.add(Tuple2(ArgumentDeclaration.parse(arguments), callback));
|
||||
});
|
||||
}
|
||||
AsyncBuiltInCallable.parsed(this.name, this._arguments, this._callback);
|
||||
|
||||
/// Returns the argument declaration and Dart callback for the given
|
||||
/// positional and named arguments.
|
||||
@ -60,28 +73,6 @@ class AsyncBuiltInCallable implements AsyncCallable {
|
||||
/// doesn't guarantee that [positional] and [names] are valid for the returned
|
||||
/// [ArgumentDeclaration].
|
||||
Tuple2<ArgumentDeclaration, _Callback> callbackFor(
|
||||
int positional, Set<String> names) {
|
||||
Tuple2<ArgumentDeclaration, _Callback> fuzzyMatch;
|
||||
int minMismatchDistance;
|
||||
|
||||
for (var overload in _overloads) {
|
||||
// Ideally, find an exact match.
|
||||
if (overload.item1.matches(positional, names)) return overload;
|
||||
|
||||
var mismatchDistance = overload.item1.arguments.length - positional;
|
||||
|
||||
if (minMismatchDistance != null) {
|
||||
if (mismatchDistance.abs() > minMismatchDistance.abs()) continue;
|
||||
// If two overloads have the same mismatch distance, favor the overload
|
||||
// that has more arguments.
|
||||
if (mismatchDistance.abs() == minMismatchDistance.abs() &&
|
||||
mismatchDistance < 0) continue;
|
||||
}
|
||||
|
||||
minMismatchDistance = mismatchDistance;
|
||||
fuzzyMatch = overload;
|
||||
}
|
||||
|
||||
return fuzzyMatch;
|
||||
}
|
||||
int positional, Set<String> names) =>
|
||||
Tuple2(_arguments, _callback);
|
||||
}
|
||||
|
@ -23,14 +23,40 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable {
|
||||
/// The overloads declared for this callable.
|
||||
final List<Tuple2<ArgumentDeclaration, _Callback>> _overloads;
|
||||
|
||||
/// Creates a callable with a single [arguments] declaration and a single
|
||||
/// Creates a function with a single [arguments] declaration and a single
|
||||
/// [callback].
|
||||
///
|
||||
/// The argument declaration is parsed from [arguments], which should not
|
||||
/// include parentheses. Throws a [SassFormatException] if parsing fails.
|
||||
BuiltInCallable(
|
||||
String name, String arguments, Value callback(List<Value> arguments))
|
||||
: this.parsed(name, ArgumentDeclaration.parse(arguments), callback);
|
||||
///
|
||||
/// If passed, [url] is the URL of the module in which the function is
|
||||
/// defined.
|
||||
BuiltInCallable.function(
|
||||
String name, String arguments, Value callback(List<Value> arguments),
|
||||
{Object url})
|
||||
: this.parsed(
|
||||
name,
|
||||
ArgumentDeclaration.parse('@function $name($arguments) {',
|
||||
url: url),
|
||||
callback);
|
||||
|
||||
/// Creates a mixin with a single [arguments] declaration and a single
|
||||
/// [callback].
|
||||
///
|
||||
/// The argument declaration is parsed from [arguments], which should not
|
||||
/// include parentheses. Throws a [SassFormatException] if parsing fails.
|
||||
///
|
||||
/// If passed, [url] is the URL of the module in which the mixin is
|
||||
/// defined.
|
||||
BuiltInCallable.mixin(
|
||||
String name, String arguments, void callback(List<Value> arguments),
|
||||
{Object url})
|
||||
: this.parsed(name,
|
||||
ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url),
|
||||
(arguments) {
|
||||
callback(arguments);
|
||||
return null;
|
||||
});
|
||||
|
||||
/// Creates a callable with a single [arguments] declaration and a single
|
||||
/// [callback].
|
||||
@ -38,16 +64,24 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable {
|
||||
Value callback(List<Value> arguments))
|
||||
: _overloads = [Tuple2(arguments, callback)];
|
||||
|
||||
/// Creates a callable with multiple implementations.
|
||||
/// Creates a function with multiple implementations.
|
||||
///
|
||||
/// Each key/value pair in [overloads] defines the argument declaration for
|
||||
/// the overload (which should not include parentheses), and the callback to
|
||||
/// execute if that argument declaration matches. Throws a
|
||||
/// [SassFormatException] if parsing fails.
|
||||
BuiltInCallable.overloaded(this.name, Map<String, _Callback> overloads)
|
||||
///
|
||||
/// If passed, [url] is the URL of the module in which the function is
|
||||
/// defined.
|
||||
BuiltInCallable.overloadedFunction(
|
||||
this.name, Map<String, _Callback> overloads,
|
||||
{Object url})
|
||||
: _overloads = [
|
||||
for (var entry in overloads.entries)
|
||||
Tuple2(ArgumentDeclaration.parse(entry.key), entry.value)
|
||||
Tuple2(
|
||||
ArgumentDeclaration.parse('@function $name(${entry.key}) {',
|
||||
url: url),
|
||||
entry.value)
|
||||
];
|
||||
|
||||
BuiltInCallable._(this.name, this._overloads);
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'ast/node.dart';
|
||||
import 'ast/sass.dart';
|
||||
import 'configured_value.dart';
|
||||
import 'util/limited_map_view.dart';
|
||||
@ -19,6 +20,11 @@ class Configuration {
|
||||
Map<String, ConfiguredValue> get values => UnmodifiableMapView(_values);
|
||||
final Map<String, ConfiguredValue> _values;
|
||||
|
||||
/// The node whose span indicates where the configuration was declared.
|
||||
///
|
||||
/// This is `null` for implicit configurations.
|
||||
final AstNode nodeWithSpan;
|
||||
|
||||
/// Whether or not this configuration is implicit.
|
||||
///
|
||||
/// Implicit configurations are created when a file containing a `@forward`
|
||||
@ -31,8 +37,18 @@ class Configuration {
|
||||
/// silently ignored in this case.
|
||||
final bool isImplicit;
|
||||
|
||||
Configuration(Map<String, ConfiguredValue> values, {this.isImplicit = false})
|
||||
: _values = values;
|
||||
/// Creates an explicit configuration with the given [values].
|
||||
Configuration(Map<String, ConfiguredValue> values, this.nodeWithSpan)
|
||||
: _values = values,
|
||||
isImplicit = false;
|
||||
|
||||
/// Creates an implicit configuration with the given [values].
|
||||
///
|
||||
/// See [isImplicit] for details.
|
||||
Configuration.implicit(Map<String, ConfiguredValue> values)
|
||||
: _values = values,
|
||||
nodeWithSpan = null,
|
||||
isImplicit = true;
|
||||
|
||||
/// The empty configuration, which indicates that the module has not been
|
||||
/// configured.
|
||||
@ -41,6 +57,7 @@ class Configuration {
|
||||
/// ignored if the module has already been loaded.
|
||||
const Configuration.empty()
|
||||
: _values = const {},
|
||||
nodeWithSpan = null,
|
||||
isImplicit = true;
|
||||
|
||||
bool get isEmpty => values.isEmpty;
|
||||
@ -67,6 +84,8 @@ class Configuration {
|
||||
} else if (forward.hiddenVariables?.isNotEmpty ?? false) {
|
||||
newValues = LimitedMapView.blocklist(newValues, forward.hiddenVariables);
|
||||
}
|
||||
return Configuration(newValues, isImplicit: isImplicit);
|
||||
return isImplicit
|
||||
? Configuration.implicit(newValues)
|
||||
: Configuration(newValues, nodeWithSpan);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
// DO NOT EDIT. This file was generated from async_environment.dart.
|
||||
// See tool/grind/synchronize.dart for details.
|
||||
//
|
||||
// Checksum: 7da67a8956ec74db270764e941b674dcc315a488
|
||||
// Checksum: b497eab76eb15ba7bfc4f1cecf71ff9f9c1fb2a5
|
||||
//
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
@ -41,16 +41,32 @@ class Environment {
|
||||
Map<String, Module<Callable>> get modules => UnmodifiableMapView(_modules);
|
||||
final Map<String, Module<Callable>> _modules;
|
||||
|
||||
/// A map from module namespaces to the nodes whose spans indicate where those
|
||||
/// modules were originally loaded.
|
||||
final Map<String, AstNode> _namespaceNodes;
|
||||
|
||||
/// The namespaceless modules used in the current scope.
|
||||
///
|
||||
/// This is `null` if there are no namespaceless modules.
|
||||
Set<Module<Callable>> _globalModules;
|
||||
|
||||
/// A map from modules in [_globalModules] to the nodes whose spans
|
||||
/// indicate where those modules were originally loaded.
|
||||
///
|
||||
/// This is `null` if there are no namespaceless modules.
|
||||
Map<Module<Callable>, AstNode> _globalModuleNodes;
|
||||
|
||||
/// The modules forwarded by this module.
|
||||
///
|
||||
/// This is `null` if there are no forwarded modules.
|
||||
List<Module<Callable>> _forwardedModules;
|
||||
|
||||
/// A map from modules in [_forwardedModules] to the nodes whose spans
|
||||
/// indicate where those modules were originally forwarded.
|
||||
///
|
||||
/// This is `null` if there are no forwarded modules.
|
||||
Map<Module<Callable>, AstNode> _forwardedModuleNodes;
|
||||
|
||||
/// Modules forwarded by nested imports at each lexical scope level *beneath
|
||||
/// the global scope*.
|
||||
///
|
||||
@ -142,8 +158,11 @@ class Environment {
|
||||
/// If [sourceMap] is `true`, this tracks variables' source locations
|
||||
Environment({bool sourceMap = false})
|
||||
: _modules = {},
|
||||
_namespaceNodes = {},
|
||||
_globalModules = null,
|
||||
_globalModuleNodes = null,
|
||||
_forwardedModules = null,
|
||||
_forwardedModuleNodes = null,
|
||||
_nestedForwardedModules = null,
|
||||
_allModules = [],
|
||||
_variables = [{}],
|
||||
@ -156,8 +175,11 @@ class Environment {
|
||||
|
||||
Environment._(
|
||||
this._modules,
|
||||
this._namespaceNodes,
|
||||
this._globalModules,
|
||||
this._globalModuleNodes,
|
||||
this._forwardedModules,
|
||||
this._forwardedModuleNodes,
|
||||
this._nestedForwardedModules,
|
||||
this._allModules,
|
||||
this._variables,
|
||||
@ -180,8 +202,11 @@ class Environment {
|
||||
/// when the closure was created will be reflected.
|
||||
Environment closure() => Environment._(
|
||||
_modules,
|
||||
_namespaceNodes,
|
||||
_globalModules,
|
||||
_globalModuleNodes,
|
||||
_forwardedModules,
|
||||
_forwardedModuleNodes,
|
||||
_nestedForwardedModules,
|
||||
_allModules,
|
||||
_variables.toList(),
|
||||
@ -196,6 +221,9 @@ class Environment {
|
||||
/// functions, and mixins, but not its modules.
|
||||
Environment global() => Environment._(
|
||||
{},
|
||||
{},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@ -208,16 +236,21 @@ class Environment {
|
||||
|
||||
/// Adds [module] to the set of modules visible in this environment.
|
||||
///
|
||||
/// [nodeWithSpan]'s span is used to report any errors with the module.
|
||||
///
|
||||
/// If [namespace] is passed, the module is made available under that
|
||||
/// namespace.
|
||||
///
|
||||
/// Throws a [SassScriptException] if there's already a module with the given
|
||||
/// [namespace], or if [namespace] is `null` and [module] defines a variable
|
||||
/// with the same name as a variable defined in this environment.
|
||||
void addModule(Module<Callable> module, {String namespace}) {
|
||||
void addModule(Module<Callable> module, AstNode nodeWithSpan,
|
||||
{String namespace}) {
|
||||
if (namespace == null) {
|
||||
_globalModules ??= {};
|
||||
_globalModuleNodes ??= {};
|
||||
_globalModules.add(module);
|
||||
_globalModuleNodes[module] = nodeWithSpan;
|
||||
_allModules.add(module);
|
||||
|
||||
for (var name in _variables.first.keys) {
|
||||
@ -229,11 +262,14 @@ class Environment {
|
||||
}
|
||||
} else {
|
||||
if (_modules.containsKey(namespace)) {
|
||||
throw SassScriptException(
|
||||
"There's already a module with namespace \"$namespace\".");
|
||||
throw MultiSpanSassScriptException(
|
||||
"There's already a module with namespace \"$namespace\".",
|
||||
"new @use",
|
||||
{_namespaceNodes[namespace].span: "original @use"});
|
||||
}
|
||||
|
||||
_modules[namespace] = module;
|
||||
_namespaceNodes[namespace] = nodeWithSpan;
|
||||
_allModules.add(module);
|
||||
}
|
||||
}
|
||||
@ -242,12 +278,15 @@ class Environment {
|
||||
/// defined in this module, according to the modifications defined by [rule].
|
||||
void forwardModule(Module<Callable> module, ForwardRule rule) {
|
||||
_forwardedModules ??= [];
|
||||
_forwardedModuleNodes ??= {};
|
||||
|
||||
var view = ForwardedModuleView(module, rule);
|
||||
for (var other in _forwardedModules) {
|
||||
_assertNoConflicts(view.variables, other.variables, "variable", other);
|
||||
_assertNoConflicts(view.functions, other.functions, "function", other);
|
||||
_assertNoConflicts(view.mixins, other.mixins, "mixin", other);
|
||||
_assertNoConflicts(
|
||||
view.variables, other.variables, "variable", other, rule);
|
||||
_assertNoConflicts(
|
||||
view.functions, other.functions, "function", other, rule);
|
||||
_assertNoConflicts(view.mixins, other.mixins, "mixin", other, rule);
|
||||
}
|
||||
|
||||
// Add the original module to [_allModules] (rather than the
|
||||
@ -256,14 +295,19 @@ class Environment {
|
||||
// CSS, not for the members they expose.
|
||||
_allModules.add(module);
|
||||
_forwardedModules.add(view);
|
||||
_forwardedModuleNodes[view] = rule;
|
||||
}
|
||||
|
||||
/// Throws a [SassScriptException] if [newMembers] has any keys that overlap
|
||||
/// with [oldMembers].
|
||||
///
|
||||
/// The [type] and [oldModule] is used for error reporting.
|
||||
void _assertNoConflicts(Map<String, Object> newMembers,
|
||||
Map<String, Object> oldMembers, String type, Module<Callable> oldModule) {
|
||||
/// The [type], [other], [newModuleNodeWithSpan] are used for error reporting.
|
||||
void _assertNoConflicts(
|
||||
Map<String, Object> newMembers,
|
||||
Map<String, Object> oldMembers,
|
||||
String type,
|
||||
Module<Callable> other,
|
||||
AstNode newModuleNodeWithSpan) {
|
||||
Map<String, Object> smaller;
|
||||
Map<String, Object> larger;
|
||||
if (newMembers.length < oldMembers.length) {
|
||||
@ -277,9 +321,10 @@ class Environment {
|
||||
for (var name in smaller.keys) {
|
||||
if (larger.containsKey(name)) {
|
||||
if (type == "variable") name = "\$$name";
|
||||
throw SassScriptException(
|
||||
'Module ${p.prettyUri(oldModule.url)} and the new module both '
|
||||
'forward a $type named $name.');
|
||||
throw MultiSpanSassScriptException(
|
||||
'Two forwarded modules both define a $type named $name.',
|
||||
"new @forward",
|
||||
{_forwardedModuleNodes[other].span: "original @forward"});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -294,7 +339,9 @@ class Environment {
|
||||
if (forwarded == null) return;
|
||||
|
||||
_globalModules ??= {};
|
||||
_globalModuleNodes ??= {};
|
||||
_forwardedModules ??= [];
|
||||
_forwardedModuleNodes ??= {};
|
||||
|
||||
var forwardedVariableNames =
|
||||
forwarded.expand((module) => module.variables.keys).toSet();
|
||||
@ -314,6 +361,7 @@ class Environment {
|
||||
if (shadowed != null) {
|
||||
_globalModules.remove(module);
|
||||
_globalModules.add(shadowed);
|
||||
_globalModuleNodes[shadowed] = _globalModuleNodes.remove(module);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < _forwardedModules.length; i++) {
|
||||
@ -322,11 +370,17 @@ class Environment {
|
||||
variables: forwardedVariableNames,
|
||||
mixins: forwardedMixinNames,
|
||||
functions: forwardedFunctionNames);
|
||||
if (shadowed != null) _forwardedModules[i] = shadowed;
|
||||
if (shadowed != null) {
|
||||
_forwardedModules[i] = shadowed;
|
||||
_forwardedModuleNodes[shadowed] =
|
||||
_forwardedModuleNodes.remove(module);
|
||||
}
|
||||
}
|
||||
|
||||
_globalModules.addAll(forwarded);
|
||||
_globalModuleNodes.addAll(module._environment._forwardedModuleNodes);
|
||||
_forwardedModules.addAll(forwarded);
|
||||
_forwardedModuleNodes.addAll(module._environment._forwardedModuleNodes);
|
||||
} else {
|
||||
_nestedForwardedModules ??=
|
||||
List.generate(_variables.length - 1, (_) => []);
|
||||
@ -750,7 +804,7 @@ class Environment {
|
||||
configuration[name] = ConfiguredValue(values[name], null, nodes[name]);
|
||||
}
|
||||
}
|
||||
return Configuration(configuration, isImplicit: true);
|
||||
return Configuration.implicit(configuration);
|
||||
}
|
||||
|
||||
/// Returns a module that represents the top-level members defined in [this],
|
||||
@ -799,11 +853,12 @@ class Environment {
|
||||
if (valueInModule == null) continue;
|
||||
|
||||
if (value != null) {
|
||||
throw SassScriptException(
|
||||
'This $type is available from multiple global modules:\n' +
|
||||
bulletedList(_globalModules
|
||||
.where((module) => callback(module) != null)
|
||||
.map((module) => p.prettyUri(module.url))));
|
||||
throw MultiSpanSassScriptException(
|
||||
'This $type is available from multiple global modules.',
|
||||
'$type use', {
|
||||
for (var entry in _globalModuleNodes.entries)
|
||||
if (callback(entry.key) != null) entry.value.span: 'includes $type'
|
||||
});
|
||||
}
|
||||
|
||||
value = valueInModule;
|
||||
|
@ -77,6 +77,43 @@ body::before {
|
||||
}
|
||||
}
|
||||
|
||||
/// A [SassException] that's also a [MultiSpanSassException].
|
||||
class MultiSpanSassException extends SassException
|
||||
implements MultiSourceSpanException {
|
||||
final String primaryLabel;
|
||||
final Map<FileSpan, String> secondarySpans;
|
||||
|
||||
MultiSpanSassException(String message, FileSpan span, this.primaryLabel,
|
||||
Map<FileSpan, String> secondarySpans)
|
||||
: secondarySpans = Map.unmodifiable(secondarySpans),
|
||||
super(message, span);
|
||||
|
||||
String toString({Object color, String secondaryColor}) {
|
||||
var useColor = false;
|
||||
String primaryColor;
|
||||
if (color is String) {
|
||||
useColor = true;
|
||||
primaryColor = color;
|
||||
} else if (color == true) {
|
||||
useColor = true;
|
||||
}
|
||||
|
||||
var buffer = StringBuffer()
|
||||
..writeln("Error: $message")
|
||||
..write(span.highlightMultiple(primaryLabel, secondarySpans,
|
||||
color: useColor,
|
||||
primaryColor: primaryColor,
|
||||
secondaryColor: secondaryColor));
|
||||
|
||||
for (var frame in trace.toString().split("\n")) {
|
||||
if (frame.isEmpty) continue;
|
||||
buffer.writeln();
|
||||
buffer.write(" $frame");
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// An exception thrown by Sass while evaluating a stylesheet.
|
||||
class SassRuntimeException extends SassException {
|
||||
final Trace trace;
|
||||
@ -85,6 +122,16 @@ class SassRuntimeException extends SassException {
|
||||
: super(message, span);
|
||||
}
|
||||
|
||||
/// A [SassRuntimeException] that's also a [MultiSpanSassException].
|
||||
class MultiSpanSassRuntimeException extends MultiSpanSassException
|
||||
implements SassRuntimeException {
|
||||
final Trace trace;
|
||||
|
||||
MultiSpanSassRuntimeException(String message, FileSpan span,
|
||||
String primaryLabel, Map<FileSpan, String> secondarySpans, this.trace)
|
||||
: super(message, span, primaryLabel, secondarySpans);
|
||||
}
|
||||
|
||||
/// An exception thrown when Sass parsing has failed.
|
||||
class SassFormatException extends SassException
|
||||
implements SourceSpanFormatException {
|
||||
@ -108,3 +155,18 @@ class SassScriptException {
|
||||
|
||||
String toString() => "$message\n\nBUG: This should include a source span!";
|
||||
}
|
||||
|
||||
/// A [SassScriptException] that contains one or more additional spans to
|
||||
/// display as points of reference.
|
||||
class MultiSpanSassScriptException extends SassScriptException {
|
||||
/// See [MultiSourceSpanException.primaryLabel].
|
||||
final String primaryLabel;
|
||||
|
||||
/// See [MultiSourceSpanException.secondarySpans].
|
||||
final Map<FileSpan, String> secondarySpans;
|
||||
|
||||
MultiSpanSassScriptException(
|
||||
String message, this.primaryLabel, Map<FileSpan, String> secondarySpans)
|
||||
: secondarySpans = Map.unmodifiable(secondarySpans),
|
||||
super(message);
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ final List<BuiltInCallable> globalFunctions = UnmodifiableListView([
|
||||
|
||||
// This is only invoked using `call()`. Hand-authored `if()`s are parsed as
|
||||
// [IfExpression]s.
|
||||
BuiltInCallable("if", r"$condition, $if-true, $if-false",
|
||||
BuiltInCallable.function("if", r"$condition, $if-true, $if-false",
|
||||
(arguments) => arguments[0].isTruthy ? arguments[1] : arguments[2]),
|
||||
]);
|
||||
|
||||
|
@ -23,7 +23,7 @@ final global = UnmodifiableListView([
|
||||
// ### RGB
|
||||
_red, _green, _blue, _mix,
|
||||
|
||||
BuiltInCallable.overloaded("rgb", {
|
||||
BuiltInCallable.overloadedFunction("rgb", {
|
||||
r"$red, $green, $blue, $alpha": (arguments) => _rgb("rgb", arguments),
|
||||
r"$red, $green, $blue": (arguments) => _rgb("rgb", arguments),
|
||||
r"$color, $alpha": (arguments) => _rgbTwoArg("rgb", arguments),
|
||||
@ -34,7 +34,7 @@ final global = UnmodifiableListView([
|
||||
}
|
||||
}),
|
||||
|
||||
BuiltInCallable.overloaded("rgba", {
|
||||
BuiltInCallable.overloadedFunction("rgba", {
|
||||
r"$red, $green, $blue, $alpha": (arguments) => _rgb("rgba", arguments),
|
||||
r"$red, $green, $blue": (arguments) => _rgb("rgba", arguments),
|
||||
r"$color, $alpha": (arguments) => _rgbTwoArg("rgba", arguments),
|
||||
@ -47,7 +47,7 @@ final global = UnmodifiableListView([
|
||||
}
|
||||
}),
|
||||
|
||||
BuiltInCallable("invert", r"$color, $weight: 100%", (arguments) {
|
||||
_function("invert", r"$color, $weight: 100%", (arguments) {
|
||||
var weight = arguments[1].assertNumber("weight");
|
||||
if (arguments[0] is SassNumber) {
|
||||
if (weight.value != 100 || !weight.hasUnit("%")) {
|
||||
@ -68,7 +68,7 @@ final global = UnmodifiableListView([
|
||||
// ### HSL
|
||||
_hue, _saturation, _lightness, _complement,
|
||||
|
||||
BuiltInCallable.overloaded("hsl", {
|
||||
BuiltInCallable.overloadedFunction("hsl", {
|
||||
r"$hue, $saturation, $lightness, $alpha": (arguments) =>
|
||||
_hsl("hsl", arguments),
|
||||
r"$hue, $saturation, $lightness": (arguments) => _hsl("hsl", arguments),
|
||||
@ -88,7 +88,7 @@ final global = UnmodifiableListView([
|
||||
}
|
||||
}),
|
||||
|
||||
BuiltInCallable.overloaded("hsla", {
|
||||
BuiltInCallable.overloadedFunction("hsla", {
|
||||
r"$hue, $saturation, $lightness, $alpha": (arguments) =>
|
||||
_hsl("hsla", arguments),
|
||||
r"$hue, $saturation, $lightness": (arguments) => _hsl("hsla", arguments),
|
||||
@ -108,7 +108,7 @@ final global = UnmodifiableListView([
|
||||
}
|
||||
}),
|
||||
|
||||
BuiltInCallable("grayscale", r"$color", (arguments) {
|
||||
_function("grayscale", r"$color", (arguments) {
|
||||
if (arguments[0] is SassNumber) {
|
||||
return _functionString('grayscale', arguments);
|
||||
}
|
||||
@ -117,13 +117,13 @@ final global = UnmodifiableListView([
|
||||
return color.changeHsl(saturation: 0);
|
||||
}),
|
||||
|
||||
BuiltInCallable("adjust-hue", r"$color, $degrees", (arguments) {
|
||||
_function("adjust-hue", r"$color, $degrees", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
var degrees = arguments[1].assertNumber("degrees");
|
||||
return color.changeHsl(hue: color.hue + degrees.value);
|
||||
}),
|
||||
|
||||
BuiltInCallable("lighten", r"$color, $amount", (arguments) {
|
||||
_function("lighten", r"$color, $amount", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
var amount = arguments[1].assertNumber("amount");
|
||||
return color.changeHsl(
|
||||
@ -131,7 +131,7 @@ final global = UnmodifiableListView([
|
||||
.clamp(0, 100));
|
||||
}),
|
||||
|
||||
BuiltInCallable("darken", r"$color, $amount", (arguments) {
|
||||
_function("darken", r"$color, $amount", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
var amount = arguments[1].assertNumber("amount");
|
||||
return color.changeHsl(
|
||||
@ -139,7 +139,7 @@ final global = UnmodifiableListView([
|
||||
.clamp(0, 100));
|
||||
}),
|
||||
|
||||
BuiltInCallable.overloaded("saturate", {
|
||||
BuiltInCallable.overloadedFunction("saturate", {
|
||||
r"$amount": (arguments) {
|
||||
var number = arguments[0].assertNumber("amount");
|
||||
return SassString("saturate(${number.toCssString()})", quotes: false);
|
||||
@ -153,7 +153,7 @@ final global = UnmodifiableListView([
|
||||
}
|
||||
}),
|
||||
|
||||
BuiltInCallable("desaturate", r"$color, $amount", (arguments) {
|
||||
_function("desaturate", r"$color, $amount", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
var amount = arguments[1].assertNumber("amount");
|
||||
return color.changeHsl(
|
||||
@ -162,12 +162,12 @@ final global = UnmodifiableListView([
|
||||
}),
|
||||
|
||||
// ### Opacity
|
||||
BuiltInCallable("opacify", r"$color, $amount", _opacify),
|
||||
BuiltInCallable("fade-in", r"$color, $amount", _opacify),
|
||||
BuiltInCallable("transparentize", r"$color, $amount", _transparentize),
|
||||
BuiltInCallable("fade-out", r"$color, $amount", _transparentize),
|
||||
_function("opacify", r"$color, $amount", _opacify),
|
||||
_function("fade-in", r"$color, $amount", _opacify),
|
||||
_function("transparentize", r"$color, $amount", _transparentize),
|
||||
_function("fade-out", r"$color, $amount", _transparentize),
|
||||
|
||||
BuiltInCallable.overloaded("alpha", {
|
||||
BuiltInCallable.overloadedFunction("alpha", {
|
||||
r"$color": (arguments) {
|
||||
var argument = arguments[0];
|
||||
if (argument is SassString &&
|
||||
@ -201,7 +201,7 @@ final global = UnmodifiableListView([
|
||||
}
|
||||
}),
|
||||
|
||||
BuiltInCallable("opacity", r"$color", (arguments) {
|
||||
_function("opacity", r"$color", (arguments) {
|
||||
if (arguments[0] is SassNumber) {
|
||||
return _functionString("opacity", arguments);
|
||||
}
|
||||
@ -222,7 +222,7 @@ final module = BuiltInModule("color", functions: [
|
||||
// ### RGB
|
||||
_red, _green, _blue, _mix,
|
||||
|
||||
BuiltInCallable("invert", r"$color, $weight: 100%", (arguments) {
|
||||
_function("invert", r"$color, $weight: 100%", (arguments) {
|
||||
var weight = arguments[1].assertNumber("weight");
|
||||
if (arguments[0] is SassNumber) {
|
||||
if (weight.value != 100 || !weight.hasUnit("%")) {
|
||||
@ -252,7 +252,7 @@ final module = BuiltInModule("color", functions: [
|
||||
_removedColorFunction("saturate", "saturation"),
|
||||
_removedColorFunction("desaturate", "saturation", negative: true),
|
||||
|
||||
BuiltInCallable("grayscale", r"$color", (arguments) {
|
||||
_function("grayscale", r"$color", (arguments) {
|
||||
if (arguments[0] is SassNumber) {
|
||||
var result = _functionString("grayscale", arguments.take(1));
|
||||
warn("Passing a number to color.grayscale() is deprecated.\n"
|
||||
@ -271,7 +271,7 @@ final module = BuiltInModule("color", functions: [
|
||||
_removedColorFunction("transparentize", "alpha", negative: true),
|
||||
_removedColorFunction("fade-out", "alpha", negative: true),
|
||||
|
||||
BuiltInCallable.overloaded("alpha", {
|
||||
BuiltInCallable.overloadedFunction("alpha", {
|
||||
r"$color": (arguments) {
|
||||
var argument = arguments[0];
|
||||
if (argument is SassString &&
|
||||
@ -306,7 +306,7 @@ final module = BuiltInModule("color", functions: [
|
||||
}
|
||||
}),
|
||||
|
||||
BuiltInCallable("opacity", r"$color", (arguments) {
|
||||
_function("opacity", r"$color", (arguments) {
|
||||
if (arguments[0] is SassNumber) {
|
||||
var result = _functionString("opacity", arguments);
|
||||
warn("Passing a number to color.opacity() is deprecated.\n"
|
||||
@ -325,20 +325,19 @@ final module = BuiltInModule("color", functions: [
|
||||
|
||||
// ### RGB
|
||||
|
||||
final _red = BuiltInCallable("red", r"$color", (arguments) {
|
||||
final _red = _function("red", r"$color", (arguments) {
|
||||
return SassNumber(arguments.first.assertColor("color").red);
|
||||
});
|
||||
|
||||
final _green = BuiltInCallable("green", r"$color", (arguments) {
|
||||
final _green = _function("green", r"$color", (arguments) {
|
||||
return SassNumber(arguments.first.assertColor("color").green);
|
||||
});
|
||||
|
||||
final _blue = BuiltInCallable("blue", r"$color", (arguments) {
|
||||
final _blue = _function("blue", r"$color", (arguments) {
|
||||
return SassNumber(arguments.first.assertColor("color").blue);
|
||||
});
|
||||
|
||||
final _mix =
|
||||
BuiltInCallable("mix", r"$color1, $color2, $weight: 50%", (arguments) {
|
||||
final _mix = _function("mix", r"$color1, $color2, $weight: 50%", (arguments) {
|
||||
var color1 = arguments[0].assertColor("color1");
|
||||
var color2 = arguments[1].assertColor("color2");
|
||||
var weight = arguments[2].assertNumber("weight");
|
||||
@ -347,29 +346,29 @@ final _mix =
|
||||
|
||||
// ### HSL
|
||||
|
||||
final _hue = BuiltInCallable("hue", r"$color",
|
||||
final _hue = _function("hue", r"$color",
|
||||
(arguments) => SassNumber(arguments.first.assertColor("color").hue, "deg"));
|
||||
|
||||
final _saturation = BuiltInCallable(
|
||||
final _saturation = _function(
|
||||
"saturation",
|
||||
r"$color",
|
||||
(arguments) =>
|
||||
SassNumber(arguments.first.assertColor("color").saturation, "%"));
|
||||
|
||||
final _lightness = BuiltInCallable(
|
||||
final _lightness = _function(
|
||||
"lightness",
|
||||
r"$color",
|
||||
(arguments) =>
|
||||
SassNumber(arguments.first.assertColor("color").lightness, "%"));
|
||||
|
||||
final _complement = BuiltInCallable("complement", r"$color", (arguments) {
|
||||
final _complement = _function("complement", r"$color", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
return color.changeHsl(hue: color.hue + 180);
|
||||
});
|
||||
|
||||
// Miscellaneous
|
||||
|
||||
final _adjust = BuiltInCallable("adjust", r"$color, $kwargs...", (arguments) {
|
||||
final _adjust = _function("adjust", r"$color, $kwargs...", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
var argumentList = arguments[1] as SassArgumentList;
|
||||
if (argumentList.asList.isNotEmpty) {
|
||||
@ -422,7 +421,7 @@ final _adjust = BuiltInCallable("adjust", r"$color, $kwargs...", (arguments) {
|
||||
}
|
||||
});
|
||||
|
||||
final _scale = BuiltInCallable("scale", r"$color, $kwargs...", (arguments) {
|
||||
final _scale = _function("scale", r"$color, $kwargs...", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
var argumentList = arguments[1] as SassArgumentList;
|
||||
if (argumentList.asList.isNotEmpty) {
|
||||
@ -483,7 +482,7 @@ final _scale = BuiltInCallable("scale", r"$color, $kwargs...", (arguments) {
|
||||
}
|
||||
});
|
||||
|
||||
final _change = BuiltInCallable("change", r"$color, $kwargs...", (arguments) {
|
||||
final _change = _function("change", r"$color, $kwargs...", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
var argumentList = arguments[1] as SassArgumentList;
|
||||
if (argumentList.asList.isNotEmpty) {
|
||||
@ -529,7 +528,7 @@ final _change = BuiltInCallable("change", r"$color, $kwargs...", (arguments) {
|
||||
}
|
||||
});
|
||||
|
||||
final _ieHexStr = BuiltInCallable("ie-hex-str", r"$color", (arguments) {
|
||||
final _ieHexStr = _function("ie-hex-str", r"$color", (arguments) {
|
||||
var color = arguments[0].assertColor("color");
|
||||
String hexString(int component) =>
|
||||
component.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
@ -548,14 +547,14 @@ SassString _functionString(String name, Iterable<Value> arguments) =>
|
||||
")",
|
||||
quotes: false);
|
||||
|
||||
/// Returns a [BuiltInCallable] that throws an error indicating that
|
||||
/// Returns a [_function] that throws an error indicating that
|
||||
/// `color.adjust()` should be used instead.
|
||||
///
|
||||
/// This prints a suggested `color.adjust()` call that passes the adjustment
|
||||
/// value to [argument], with a leading minus sign if [negative] is `true`.
|
||||
BuiltInCallable _removedColorFunction(String name, String argument,
|
||||
{bool negative = false}) =>
|
||||
BuiltInCallable(name, r"$color, $amount", (arguments) {
|
||||
_function(name, r"$color, $amount", (arguments) {
|
||||
throw SassScriptException(
|
||||
"The function $name() isn't in the sass:color module.\n"
|
||||
"\n"
|
||||
@ -776,3 +775,9 @@ SassColor _transparentize(List<Value> arguments) {
|
||||
|
||||
/// Like [fuzzyRound], but returns `null` if [number] is `null`.
|
||||
int _fuzzyRoundOrNull(num number) => number == null ? null : fuzzyRound(number);
|
||||
|
||||
/// Like [new BuiltInCallable.function], but always sets the URL to
|
||||
/// `sass:color`.
|
||||
BuiltInCallable _function(
|
||||
String name, String arguments, Value callback(List<Value> arguments)) =>
|
||||
BuiltInCallable.function(name, arguments, callback, url: "sass:color");
|
||||
|
@ -23,16 +23,16 @@ final module = BuiltInModule("list", functions: [
|
||||
_separator
|
||||
]);
|
||||
|
||||
final _length = BuiltInCallable(
|
||||
final _length = _function(
|
||||
"length", r"$list", (arguments) => SassNumber(arguments[0].asList.length));
|
||||
|
||||
final _nth = BuiltInCallable("nth", r"$list, $n", (arguments) {
|
||||
final _nth = _function("nth", r"$list, $n", (arguments) {
|
||||
var list = arguments[0];
|
||||
var index = arguments[1];
|
||||
return list.asList[list.sassIndexToListIndex(index, "n")];
|
||||
});
|
||||
|
||||
final _setNth = BuiltInCallable("set-nth", r"$list, $n, $value", (arguments) {
|
||||
final _setNth = _function("set-nth", r"$list, $n, $value", (arguments) {
|
||||
var list = arguments[0];
|
||||
var index = arguments[1];
|
||||
var value = arguments[2];
|
||||
@ -41,7 +41,7 @@ final _setNth = BuiltInCallable("set-nth", r"$list, $n, $value", (arguments) {
|
||||
return arguments[0].changeListContents(newList);
|
||||
});
|
||||
|
||||
final _join = BuiltInCallable(
|
||||
final _join = _function(
|
||||
"join", r"$list1, $list2, $separator: auto, $bracketed: auto", (arguments) {
|
||||
var list1 = arguments[0];
|
||||
var list2 = arguments[1];
|
||||
@ -75,7 +75,7 @@ final _join = BuiltInCallable(
|
||||
});
|
||||
|
||||
final _append =
|
||||
BuiltInCallable("append", r"$list, $val, $separator: auto", (arguments) {
|
||||
_function("append", r"$list, $val, $separator: auto", (arguments) {
|
||||
var list = arguments[0];
|
||||
var value = arguments[1];
|
||||
var separatorParam = arguments[2].assertString("separator");
|
||||
@ -98,7 +98,7 @@ final _append =
|
||||
return list.changeListContents(newList, separator: separator);
|
||||
});
|
||||
|
||||
final _zip = BuiltInCallable("zip", r"$lists...", (arguments) {
|
||||
final _zip = _function("zip", r"$lists...", (arguments) {
|
||||
var lists = arguments[0].asList.map((list) => list.asList).toList();
|
||||
if (lists.isEmpty) {
|
||||
return const SassList.empty(separator: ListSeparator.comma);
|
||||
@ -113,7 +113,7 @@ final _zip = BuiltInCallable("zip", r"$lists...", (arguments) {
|
||||
return SassList(results, ListSeparator.comma);
|
||||
});
|
||||
|
||||
final _index = BuiltInCallable("index", r"$list, $value", (arguments) {
|
||||
final _index = _function("index", r"$list, $value", (arguments) {
|
||||
var list = arguments[0].asList;
|
||||
var value = arguments[1];
|
||||
|
||||
@ -121,12 +121,17 @@ final _index = BuiltInCallable("index", r"$list, $value", (arguments) {
|
||||
return index == -1 ? sassNull : SassNumber(index + 1);
|
||||
});
|
||||
|
||||
final _separator = BuiltInCallable(
|
||||
final _separator = _function(
|
||||
"separator",
|
||||
r"$list",
|
||||
(arguments) => arguments[0].separator == ListSeparator.comma
|
||||
? SassString("comma", quotes: false)
|
||||
: SassString("space", quotes: false));
|
||||
|
||||
final _isBracketed = BuiltInCallable("is-bracketed", r"$list",
|
||||
final _isBracketed = _function("is-bracketed", r"$list",
|
||||
(arguments) => SassBoolean(arguments[0].hasBrackets));
|
||||
|
||||
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:list`.
|
||||
BuiltInCallable _function(
|
||||
String name, String arguments, Value callback(List<Value> arguments)) =>
|
||||
BuiltInCallable.function(name, arguments, callback, url: "sass:list");
|
||||
|
@ -24,19 +24,19 @@ final global = UnmodifiableListView([
|
||||
final module = BuiltInModule("map",
|
||||
functions: [_get, _merge, _remove, _keys, _values, _hasKey]);
|
||||
|
||||
final _get = BuiltInCallable("get", r"$map, $key", (arguments) {
|
||||
final _get = _function("get", r"$map, $key", (arguments) {
|
||||
var map = arguments[0].assertMap("map");
|
||||
var key = arguments[1];
|
||||
return map.contents[key] ?? sassNull;
|
||||
});
|
||||
|
||||
final _merge = BuiltInCallable("merge", r"$map1, $map2", (arguments) {
|
||||
final _merge = _function("merge", r"$map1, $map2", (arguments) {
|
||||
var map1 = arguments[0].assertMap("map1");
|
||||
var map2 = arguments[1].assertMap("map2");
|
||||
return SassMap({...map1.contents, ...map2.contents});
|
||||
});
|
||||
|
||||
final _remove = BuiltInCallable.overloaded("remove", {
|
||||
final _remove = BuiltInCallable.overloadedFunction("remove", {
|
||||
// Because the signature below has an explicit `$key` argument, it doesn't
|
||||
// allow zero keys to be passed. We want to allow that case, so we add an
|
||||
// explicit overload for it.
|
||||
@ -55,20 +55,25 @@ final _remove = BuiltInCallable.overloaded("remove", {
|
||||
}
|
||||
});
|
||||
|
||||
final _keys = BuiltInCallable(
|
||||
final _keys = _function(
|
||||
"keys",
|
||||
r"$map",
|
||||
(arguments) => SassList(
|
||||
arguments[0].assertMap("map").contents.keys, ListSeparator.comma));
|
||||
|
||||
final _values = BuiltInCallable(
|
||||
final _values = _function(
|
||||
"values",
|
||||
r"$map",
|
||||
(arguments) => SassList(
|
||||
arguments[0].assertMap("map").contents.values, ListSeparator.comma));
|
||||
|
||||
final _hasKey = BuiltInCallable("has-key", r"$map, $key", (arguments) {
|
||||
final _hasKey = _function("has-key", r"$map, $key", (arguments) {
|
||||
var map = arguments[0].assertMap("map");
|
||||
var key = arguments[1];
|
||||
return SassBoolean(map.contents.containsKey(key));
|
||||
});
|
||||
|
||||
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:map`.
|
||||
BuiltInCallable _function(
|
||||
String name, String arguments, Value callback(List<Value> arguments)) =>
|
||||
BuiltInCallable.function(name, arguments, callback, url: "sass:map");
|
||||
|
@ -31,24 +31,13 @@ final module = BuiltInModule("math", functions: [
|
||||
"pi": SassNumber(math.pi),
|
||||
});
|
||||
|
||||
/// Returns a [Callable] named [name] that transforms a number's value
|
||||
/// using [transform] and preserves its units.
|
||||
BuiltInCallable _numberFunction(String name, num transform(num value)) {
|
||||
return BuiltInCallable(name, r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
return SassNumber.withUnits(transform(number.value),
|
||||
numeratorUnits: number.numeratorUnits,
|
||||
denominatorUnits: number.denominatorUnits);
|
||||
});
|
||||
}
|
||||
|
||||
///
|
||||
/// Bounding functions
|
||||
///
|
||||
|
||||
final _ceil = _numberFunction("ceil", (value) => value.ceil());
|
||||
|
||||
final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
|
||||
final _clamp = _function("clamp", r"$min, $number, $max", (arguments) {
|
||||
var min = arguments[0].assertNumber("min");
|
||||
var number = arguments[1].assertNumber("number");
|
||||
var max = arguments[2].assertNumber("max");
|
||||
@ -70,7 +59,7 @@ final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
|
||||
|
||||
final _floor = _numberFunction("floor", (value) => value.floor());
|
||||
|
||||
final _max = BuiltInCallable("max", r"$numbers...", (arguments) {
|
||||
final _max = _function("max", r"$numbers...", (arguments) {
|
||||
SassNumber max;
|
||||
for (var value in arguments[0].asList) {
|
||||
var number = value.assertNumber();
|
||||
@ -80,7 +69,7 @@ final _max = BuiltInCallable("max", r"$numbers...", (arguments) {
|
||||
throw SassScriptException("At least one argument must be passed.");
|
||||
});
|
||||
|
||||
final _min = BuiltInCallable("min", r"$numbers...", (arguments) {
|
||||
final _min = _function("min", r"$numbers...", (arguments) {
|
||||
SassNumber min;
|
||||
for (var value in arguments[0].asList) {
|
||||
var number = value.assertNumber();
|
||||
@ -98,7 +87,7 @@ final _round = _numberFunction("round", fuzzyRound);
|
||||
|
||||
final _abs = _numberFunction("abs", (value) => value.abs());
|
||||
|
||||
final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
|
||||
final _hypot = _function("hypot", r"$numbers...", (arguments) {
|
||||
var numbers =
|
||||
arguments[0].asList.map((argument) => argument.assertNumber()).toList();
|
||||
if (numbers.isEmpty) {
|
||||
@ -132,13 +121,13 @@ final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
|
||||
/// Exponential functions
|
||||
///
|
||||
|
||||
final _log = BuiltInCallable("log", r"$number, $base: null", (arguments) {
|
||||
final _log = _function("log", r"$number, $base: null", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
if (number.hasUnits) {
|
||||
throw SassScriptException("\$number: Expected $number to have no units.");
|
||||
}
|
||||
|
||||
var numberValue = fuzzyRoundIfZero(number.value);
|
||||
var numberValue = _fuzzyRoundIfZero(number.value);
|
||||
if (arguments[1] == sassNull) return SassNumber(math.log(numberValue));
|
||||
|
||||
var base = arguments[1].assertNumber("base");
|
||||
@ -148,11 +137,11 @@ final _log = BuiltInCallable("log", r"$number, $base: null", (arguments) {
|
||||
|
||||
var baseValue = fuzzyEquals(base.value, 1)
|
||||
? fuzzyRound(base.value)
|
||||
: fuzzyRoundIfZero(base.value);
|
||||
: _fuzzyRoundIfZero(base.value);
|
||||
return SassNumber(math.log(numberValue) / math.log(baseValue));
|
||||
});
|
||||
|
||||
final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
|
||||
final _pow = _function("pow", r"$base, $exponent", (arguments) {
|
||||
var base = arguments[0].assertNumber("base");
|
||||
var exponent = arguments[1].assertNumber("exponent");
|
||||
if (base.hasUnits) {
|
||||
@ -164,8 +153,8 @@ final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
|
||||
|
||||
// Exponentiating certain real numbers leads to special behaviors. Ensure that
|
||||
// these behaviors are consistent for numbers within the precision limit.
|
||||
var baseValue = fuzzyRoundIfZero(base.value);
|
||||
var exponentValue = fuzzyRoundIfZero(exponent.value);
|
||||
var baseValue = _fuzzyRoundIfZero(base.value);
|
||||
var exponentValue = _fuzzyRoundIfZero(exponent.value);
|
||||
if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) {
|
||||
return SassNumber(double.nan);
|
||||
} else if (fuzzyEquals(baseValue, 0)) {
|
||||
@ -189,26 +178,21 @@ final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
|
||||
return SassNumber(math.pow(baseValue, exponentValue));
|
||||
});
|
||||
|
||||
final _sqrt = BuiltInCallable("sqrt", r"$number", (arguments) {
|
||||
final _sqrt = _function("sqrt", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
if (number.hasUnits) {
|
||||
throw SassScriptException("\$number: Expected $number to have no units.");
|
||||
}
|
||||
|
||||
var numberValue = fuzzyRoundIfZero(number.value);
|
||||
var numberValue = _fuzzyRoundIfZero(number.value);
|
||||
return SassNumber(math.sqrt(numberValue));
|
||||
});
|
||||
|
||||
num fuzzyRoundIfZero(num number) {
|
||||
if (!fuzzyEquals(number, 0)) return number;
|
||||
return number.isNegative ? -0.0 : 0;
|
||||
}
|
||||
|
||||
///
|
||||
/// Trigonometric functions
|
||||
///
|
||||
|
||||
final _acos = BuiltInCallable("acos", r"$number", (arguments) {
|
||||
final _acos = _function("acos", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
if (number.hasUnits) {
|
||||
throw SassScriptException("\$number: Expected $number to have no units.");
|
||||
@ -221,7 +205,7 @@ final _acos = BuiltInCallable("acos", r"$number", (arguments) {
|
||||
return SassNumber.withUnits(acos, numeratorUnits: ['deg']);
|
||||
});
|
||||
|
||||
final _asin = BuiltInCallable("asin", r"$number", (arguments) {
|
||||
final _asin = _function("asin", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
if (number.hasUnits) {
|
||||
throw SassScriptException("\$number: Expected $number to have no units.");
|
||||
@ -229,23 +213,23 @@ final _asin = BuiltInCallable("asin", r"$number", (arguments) {
|
||||
|
||||
var numberValue = fuzzyEquals(number.value.abs(), 1)
|
||||
? fuzzyRound(number.value)
|
||||
: fuzzyRoundIfZero(number.value);
|
||||
: _fuzzyRoundIfZero(number.value);
|
||||
var asin = math.asin(numberValue) * 180 / math.pi;
|
||||
return SassNumber.withUnits(asin, numeratorUnits: ['deg']);
|
||||
});
|
||||
|
||||
final _atan = BuiltInCallable("atan", r"$number", (arguments) {
|
||||
final _atan = _function("atan", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
if (number.hasUnits) {
|
||||
throw SassScriptException("\$number: Expected $number to have no units.");
|
||||
}
|
||||
|
||||
var numberValue = fuzzyRoundIfZero(number.value);
|
||||
var numberValue = _fuzzyRoundIfZero(number.value);
|
||||
var atan = math.atan(numberValue) * 180 / math.pi;
|
||||
return SassNumber.withUnits(atan, numeratorUnits: ['deg']);
|
||||
});
|
||||
|
||||
final _atan2 = BuiltInCallable("atan2", r"$y, $x", (arguments) {
|
||||
final _atan2 = _function("atan2", r"$y, $x", (arguments) {
|
||||
var y = arguments[0].assertNumber("y");
|
||||
var x = arguments[1].assertNumber("x");
|
||||
if (y.hasUnits != x.hasUnits) {
|
||||
@ -257,24 +241,24 @@ final _atan2 = BuiltInCallable("atan2", r"$y, $x", (arguments) {
|
||||
}
|
||||
|
||||
x = x.coerce(y.numeratorUnits, y.denominatorUnits);
|
||||
var xValue = fuzzyRoundIfZero(x.value);
|
||||
var yValue = fuzzyRoundIfZero(y.value);
|
||||
var xValue = _fuzzyRoundIfZero(x.value);
|
||||
var yValue = _fuzzyRoundIfZero(y.value);
|
||||
var atan2 = math.atan2(yValue, xValue) * 180 / math.pi;
|
||||
return SassNumber.withUnits(atan2, numeratorUnits: ['deg']);
|
||||
});
|
||||
|
||||
final _cos = BuiltInCallable("cos", r"$number", (arguments) {
|
||||
final _cos = _function("cos", r"$number", (arguments) {
|
||||
var number = _coerceToRad(arguments[0].assertNumber("number"));
|
||||
return SassNumber(math.cos(number.value));
|
||||
});
|
||||
|
||||
final _sin = BuiltInCallable("sin", r"$number", (arguments) {
|
||||
final _sin = _function("sin", r"$number", (arguments) {
|
||||
var number = _coerceToRad(arguments[0].assertNumber("number"));
|
||||
var numberValue = fuzzyRoundIfZero(number.value);
|
||||
var numberValue = _fuzzyRoundIfZero(number.value);
|
||||
return SassNumber(math.sin(numberValue));
|
||||
});
|
||||
|
||||
final _tan = BuiltInCallable("tan", r"$number", (arguments) {
|
||||
final _tan = _function("tan", r"$number", (arguments) {
|
||||
var number = _coerceToRad(arguments[0].assertNumber("number"));
|
||||
var asymptoteInterval = 0.5 * math.pi;
|
||||
var tanPeriod = 2 * math.pi;
|
||||
@ -283,11 +267,61 @@ final _tan = BuiltInCallable("tan", r"$number", (arguments) {
|
||||
} else if (fuzzyEquals((number.value + asymptoteInterval) % tanPeriod, 0)) {
|
||||
return SassNumber(double.negativeInfinity);
|
||||
} else {
|
||||
var numberValue = fuzzyRoundIfZero(number.value);
|
||||
var numberValue = _fuzzyRoundIfZero(number.value);
|
||||
return SassNumber(math.tan(numberValue));
|
||||
}
|
||||
});
|
||||
|
||||
///
|
||||
/// Unit functions
|
||||
///
|
||||
|
||||
final _compatible = _function("compatible", r"$number1, $number2", (arguments) {
|
||||
var number1 = arguments[0].assertNumber("number1");
|
||||
var number2 = arguments[1].assertNumber("number2");
|
||||
return SassBoolean(number1.isComparableTo(number2));
|
||||
});
|
||||
|
||||
final _isUnitless = _function("is-unitless", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
return SassBoolean(!number.hasUnits);
|
||||
});
|
||||
|
||||
final _unit = _function("unit", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
return SassString(number.unitString, quotes: true);
|
||||
});
|
||||
|
||||
///
|
||||
/// Other functions
|
||||
///
|
||||
|
||||
final _percentage = _function("percentage", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
number.assertNoUnits("number");
|
||||
return SassNumber(number.value * 100, '%');
|
||||
});
|
||||
|
||||
final _random = math.Random();
|
||||
|
||||
final _randomFunction = _function("random", r"$limit: null", (arguments) {
|
||||
if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
|
||||
var limit = arguments[0].assertNumber("limit").assertInt("limit");
|
||||
if (limit < 1) {
|
||||
throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
|
||||
}
|
||||
return SassNumber(_random.nextInt(limit) + 1);
|
||||
});
|
||||
|
||||
///
|
||||
/// Helpers
|
||||
///
|
||||
|
||||
num _fuzzyRoundIfZero(num number) {
|
||||
if (!fuzzyEquals(number, 0)) return number;
|
||||
return number.isNegative ? -0.0 : 0;
|
||||
}
|
||||
|
||||
SassNumber _coerceToRad(SassNumber number) {
|
||||
try {
|
||||
return number.coerce(['rad'], []);
|
||||
@ -297,44 +331,18 @@ SassNumber _coerceToRad(SassNumber number) {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Unit functions
|
||||
///
|
||||
/// Returns a [Callable] named [name] that transforms a number's value
|
||||
/// using [transform] and preserves its units.
|
||||
BuiltInCallable _numberFunction(String name, num transform(num value)) {
|
||||
return _function(name, r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
return SassNumber.withUnits(transform(number.value),
|
||||
numeratorUnits: number.numeratorUnits,
|
||||
denominatorUnits: number.denominatorUnits);
|
||||
});
|
||||
}
|
||||
|
||||
final _compatible =
|
||||
BuiltInCallable("compatible", r"$number1, $number2", (arguments) {
|
||||
var number1 = arguments[0].assertNumber("number1");
|
||||
var number2 = arguments[1].assertNumber("number2");
|
||||
return SassBoolean(number1.isComparableTo(number2));
|
||||
});
|
||||
|
||||
final _isUnitless = BuiltInCallable("is-unitless", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
return SassBoolean(!number.hasUnits);
|
||||
});
|
||||
|
||||
final _unit = BuiltInCallable("unit", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
return SassString(number.unitString, quotes: true);
|
||||
});
|
||||
|
||||
///
|
||||
/// Other functions
|
||||
///
|
||||
|
||||
final _percentage = BuiltInCallable("percentage", r"$number", (arguments) {
|
||||
var number = arguments[0].assertNumber("number");
|
||||
number.assertNoUnits("number");
|
||||
return SassNumber(number.value * 100, '%');
|
||||
});
|
||||
|
||||
final _random = math.Random();
|
||||
|
||||
final _randomFunction = BuiltInCallable("random", r"$limit: null", (arguments) {
|
||||
if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
|
||||
var limit = arguments[0].assertNumber("limit").assertInt("limit");
|
||||
if (limit < 1) {
|
||||
throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
|
||||
}
|
||||
return SassNumber(_random.nextInt(limit) + 1);
|
||||
});
|
||||
/// Like [new _function.function], but always sets the URL to `sass:math`.
|
||||
BuiltInCallable _function(
|
||||
String name, String arguments, Value callback(List<Value> arguments)) =>
|
||||
BuiltInCallable.function(name, arguments, callback, url: "sass:math");
|
||||
|
@ -23,15 +23,15 @@ final global = UnmodifiableListView([
|
||||
// This is only a partial list of meta functions. The rest are defined in the
|
||||
// evaluator, because they need access to context that's only available at
|
||||
// runtime.
|
||||
BuiltInCallable("feature-exists", r"$feature", (arguments) {
|
||||
_function("feature-exists", r"$feature", (arguments) {
|
||||
var feature = arguments[0].assertString("feature");
|
||||
return SassBoolean(_features.contains(feature.text));
|
||||
}),
|
||||
|
||||
BuiltInCallable("inspect", r"$value",
|
||||
_function("inspect", r"$value",
|
||||
(arguments) => SassString(arguments.first.toString(), quotes: false)),
|
||||
|
||||
BuiltInCallable("type-of", r"$value", (arguments) {
|
||||
_function("type-of", r"$value", (arguments) {
|
||||
var value = arguments[0];
|
||||
if (value is SassArgumentList) {
|
||||
return SassString("arglist", quotes: false);
|
||||
@ -47,7 +47,7 @@ final global = UnmodifiableListView([
|
||||
return SassString("string", quotes: false);
|
||||
}),
|
||||
|
||||
BuiltInCallable("keywords", r"$args", (arguments) {
|
||||
_function("keywords", r"$args", (arguments) {
|
||||
var argumentList = arguments[0];
|
||||
if (argumentList is SassArgumentList) {
|
||||
return SassMap(mapMap(argumentList.keywords,
|
||||
@ -57,3 +57,8 @@ final global = UnmodifiableListView([
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:meta`.
|
||||
BuiltInCallable _function(
|
||||
String name, String arguments, Value callback(List<Value> arguments)) =>
|
||||
BuiltInCallable.function(name, arguments, callback, url: "sass:meta");
|
||||
|
@ -37,7 +37,7 @@ final module = BuiltInModule("selector", functions: [
|
||||
_unify
|
||||
]);
|
||||
|
||||
final _nest = BuiltInCallable("nest", r"$selectors...", (arguments) {
|
||||
final _nest = _function("nest", r"$selectors...", (arguments) {
|
||||
var selectors = arguments[0].asList;
|
||||
if (selectors.isEmpty) {
|
||||
throw SassScriptException(
|
||||
@ -50,7 +50,7 @@ final _nest = BuiltInCallable("nest", r"$selectors...", (arguments) {
|
||||
.asSassList;
|
||||
});
|
||||
|
||||
final _append = BuiltInCallable("append", r"$selectors...", (arguments) {
|
||||
final _append = _function("append", r"$selectors...", (arguments) {
|
||||
var selectors = arguments[0].asList;
|
||||
if (selectors.isEmpty) {
|
||||
throw SassScriptException(
|
||||
@ -77,7 +77,7 @@ final _append = BuiltInCallable("append", r"$selectors...", (arguments) {
|
||||
});
|
||||
|
||||
final _extend =
|
||||
BuiltInCallable("extend", r"$selector, $extendee, $extender", (arguments) {
|
||||
_function("extend", r"$selector, $extendee, $extender", (arguments) {
|
||||
var selector = arguments[0].assertSelector(name: "selector");
|
||||
var target = arguments[1].assertSelector(name: "extendee");
|
||||
var source = arguments[2].assertSelector(name: "extender");
|
||||
@ -85,8 +85,8 @@ final _extend =
|
||||
return Extender.extend(selector, source, target).asSassList;
|
||||
});
|
||||
|
||||
final _replace = BuiltInCallable(
|
||||
"replace", r"$selector, $original, $replacement", (arguments) {
|
||||
final _replace =
|
||||
_function("replace", r"$selector, $original, $replacement", (arguments) {
|
||||
var selector = arguments[0].assertSelector(name: "selector");
|
||||
var target = arguments[1].assertSelector(name: "original");
|
||||
var source = arguments[2].assertSelector(name: "replacement");
|
||||
@ -94,7 +94,7 @@ final _replace = BuiltInCallable(
|
||||
return Extender.replace(selector, source, target).asSassList;
|
||||
});
|
||||
|
||||
final _unify = BuiltInCallable("unify", r"$selector1, $selector2", (arguments) {
|
||||
final _unify = _function("unify", r"$selector1, $selector2", (arguments) {
|
||||
var selector1 = arguments[0].assertSelector(name: "selector1");
|
||||
var selector2 = arguments[1].assertSelector(name: "selector2");
|
||||
|
||||
@ -103,7 +103,7 @@ final _unify = BuiltInCallable("unify", r"$selector1, $selector2", (arguments) {
|
||||
});
|
||||
|
||||
final _isSuperselector =
|
||||
BuiltInCallable("is-superselector", r"$super, $sub", (arguments) {
|
||||
_function("is-superselector", r"$super, $sub", (arguments) {
|
||||
var selector1 = arguments[0].assertSelector(name: "super");
|
||||
var selector2 = arguments[1].assertSelector(name: "sub");
|
||||
|
||||
@ -111,7 +111,7 @@ final _isSuperselector =
|
||||
});
|
||||
|
||||
final _simpleSelectors =
|
||||
BuiltInCallable("simple-selectors", r"$selector", (arguments) {
|
||||
_function("simple-selectors", r"$selector", (arguments) {
|
||||
var selector = arguments[0].assertCompoundSelector(name: "selector");
|
||||
|
||||
return SassList(
|
||||
@ -120,7 +120,7 @@ final _simpleSelectors =
|
||||
ListSeparator.comma);
|
||||
});
|
||||
|
||||
final _parse = BuiltInCallable("parse", r"$selector",
|
||||
final _parse = _function("parse", r"$selector",
|
||||
(arguments) => arguments[0].assertSelector(name: "selector").asSassList);
|
||||
|
||||
/// Adds a [ParentSelector] to the beginning of [compound], or returns `null` if
|
||||
@ -138,3 +138,9 @@ CompoundSelector _prependParent(CompoundSelector compound) {
|
||||
return CompoundSelector([ParentSelector(), ...compound.components]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [new BuiltInCallable.function], but always sets the URL to
|
||||
/// `sass:selector`.
|
||||
BuiltInCallable _function(
|
||||
String name, String arguments, Value callback(List<Value> arguments)) =>
|
||||
BuiltInCallable.function(name, arguments, callback, url: "sass:selector");
|
||||
|
@ -34,25 +34,24 @@ final module = BuiltInModule("string", functions: [
|
||||
_slice, _uniqueId,
|
||||
]);
|
||||
|
||||
final _unquote = BuiltInCallable("unquote", r"$string", (arguments) {
|
||||
final _unquote = _function("unquote", r"$string", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
if (!string.hasQuotes) return string;
|
||||
return SassString(string.text, quotes: false);
|
||||
});
|
||||
|
||||
final _quote = BuiltInCallable("quote", r"$string", (arguments) {
|
||||
final _quote = _function("quote", r"$string", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
if (string.hasQuotes) return string;
|
||||
return SassString(string.text, quotes: true);
|
||||
});
|
||||
|
||||
final _length = BuiltInCallable("length", r"$string", (arguments) {
|
||||
final _length = _function("length", r"$string", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
return SassNumber(string.sassLength);
|
||||
});
|
||||
|
||||
final _insert =
|
||||
BuiltInCallable("insert", r"$string, $insert, $index", (arguments) {
|
||||
final _insert = _function("insert", r"$string, $insert, $index", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
var insert = arguments[1].assertString("insert");
|
||||
var index = arguments[2].assertNumber("index");
|
||||
@ -78,7 +77,7 @@ final _insert =
|
||||
quotes: string.hasQuotes);
|
||||
});
|
||||
|
||||
final _index = BuiltInCallable("index", r"$string, $substring", (arguments) {
|
||||
final _index = _function("index", r"$string, $substring", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
var substring = arguments[1].assertString("substring");
|
||||
|
||||
@ -90,7 +89,7 @@ final _index = BuiltInCallable("index", r"$string, $substring", (arguments) {
|
||||
});
|
||||
|
||||
final _slice =
|
||||
BuiltInCallable("slice", r"$string, $start-at, $end-at: -1", (arguments) {
|
||||
_function("slice", r"$string, $start-at, $end-at: -1", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
var start = arguments[1].assertNumber("start-at");
|
||||
var end = arguments[2].assertNumber("end-at");
|
||||
@ -120,7 +119,7 @@ final _slice =
|
||||
quotes: string.hasQuotes);
|
||||
});
|
||||
|
||||
final _toUpperCase = BuiltInCallable("to-upper-case", r"$string", (arguments) {
|
||||
final _toUpperCase = _function("to-upper-case", r"$string", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
var buffer = StringBuffer();
|
||||
for (var i = 0; i < string.text.length; i++) {
|
||||
@ -129,7 +128,7 @@ final _toUpperCase = BuiltInCallable("to-upper-case", r"$string", (arguments) {
|
||||
return SassString(buffer.toString(), quotes: string.hasQuotes);
|
||||
});
|
||||
|
||||
final _toLowerCase = BuiltInCallable("to-lower-case", r"$string", (arguments) {
|
||||
final _toLowerCase = _function("to-lower-case", r"$string", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
var buffer = StringBuffer();
|
||||
for (var i = 0; i < string.text.length; i++) {
|
||||
@ -138,7 +137,7 @@ final _toLowerCase = BuiltInCallable("to-lower-case", r"$string", (arguments) {
|
||||
return SassString(buffer.toString(), quotes: string.hasQuotes);
|
||||
});
|
||||
|
||||
final _uniqueId = BuiltInCallable("unique-id", "", (arguments) {
|
||||
final _uniqueId = _function("unique-id", "", (arguments) {
|
||||
// Make it difficult to guess the next ID by randomizing the increase.
|
||||
_previousUniqueId += _random.nextInt(36) + 1;
|
||||
if (_previousUniqueId > math.pow(36, 6)) {
|
||||
@ -168,3 +167,9 @@ int _codepointForIndex(int index, int lengthInCodepoints,
|
||||
if (result < 0 && !allowNegative) return 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Like [new BuiltInCallable.function], but always sets the URL to
|
||||
/// `sass:string`.
|
||||
BuiltInCallable _function(
|
||||
String name, String arguments, Value callback(List<Value> arguments)) =>
|
||||
BuiltInCallable.function(name, arguments, callback, url: "sass:string");
|
||||
|
@ -101,8 +101,16 @@ abstract class StylesheetParser extends Parser {
|
||||
});
|
||||
}
|
||||
|
||||
ArgumentDeclaration parseArgumentDeclaration() =>
|
||||
_parseSingleProduction(_argumentDeclaration);
|
||||
ArgumentDeclaration parseArgumentDeclaration() => _parseSingleProduction(() {
|
||||
scanner.expectChar($at, name: "@-rule");
|
||||
identifier();
|
||||
whitespace();
|
||||
identifier();
|
||||
var arguments = _argumentDeclaration();
|
||||
whitespace();
|
||||
scanner.expectChar($lbrace);
|
||||
return arguments;
|
||||
});
|
||||
|
||||
Expression parseExpression() => _parseSingleProduction(expression);
|
||||
|
||||
@ -1177,14 +1185,12 @@ abstract class StylesheetParser extends Parser {
|
||||
|
||||
ContentBlock content;
|
||||
if (contentArguments != null || lookingAtChildren()) {
|
||||
contentArguments ??= ArgumentDeclaration.empty(span: scanner.emptySpan);
|
||||
|
||||
var wasInContentBlock = _inContentBlock;
|
||||
_inContentBlock = true;
|
||||
content = _withChildren(_statement, start, (children, span) {
|
||||
return ContentBlock(
|
||||
contentArguments ??
|
||||
ArgumentDeclaration.empty(span: scanner.emptySpan),
|
||||
children,
|
||||
span);
|
||||
return ContentBlock(contentArguments, children, span);
|
||||
});
|
||||
_inContentBlock = wasInContentBlock;
|
||||
} else {
|
||||
|
@ -411,3 +411,24 @@ Map<K1, Map<K2, V>> copyMapOfMap<K1, K2, V>(Map<K1, Map<K2, V>> map) =>
|
||||
/// Returns a deep copy of a map that contains lists.
|
||||
Map<K, List<E>> copyMapOfList<K, E>(Map<K, List<E>> map) =>
|
||||
mapMap<K, List<E>, K, List<E>>(map, value: (_, list) => list.toList());
|
||||
|
||||
extension SpanExtensions on FileSpan {
|
||||
/// Returns this span with all whitespace trimmed from both sides.
|
||||
FileSpan trim() {
|
||||
var text = this.text;
|
||||
|
||||
var start = 0;
|
||||
while (isWhitespace(text.codeUnitAt(start))) {
|
||||
start++;
|
||||
}
|
||||
|
||||
var end = text.length - 1;
|
||||
while (isWhitespace(text.codeUnitAt(end))) {
|
||||
end--;
|
||||
}
|
||||
|
||||
return start == 0 && end == text.length - 1
|
||||
? this
|
||||
: file.span(this.start.offset + start, this.start.offset + end + 1);
|
||||
}
|
||||
}
|
||||
|
@ -139,6 +139,13 @@ class _EvaluateVisitor
|
||||
/// All modules that have been loaded and evaluated so far.
|
||||
final _modules = <Uri, Module>{};
|
||||
|
||||
/// A map from canonical module URLs to the nodes whose spans indicate where
|
||||
/// those modules were originally loaded.
|
||||
///
|
||||
/// This is not guaranteed to have a node for every module in [_modules]. For
|
||||
/// example, the entrypoint module was not loaded by a node.
|
||||
final _moduleNodes = <Uri, AstNode>{};
|
||||
|
||||
/// The logger to use to print warnings.
|
||||
final Logger _logger;
|
||||
|
||||
@ -199,11 +206,16 @@ class _EvaluateVisitor
|
||||
/// imports, it contains the URL passed to the `@import`.
|
||||
final _includedFiles = <String>{};
|
||||
|
||||
/// The set of canonical URLs for modules (or imported files) that are
|
||||
/// currently being evaluated.
|
||||
/// A map from canonical URLs for modules (or imported files) that are
|
||||
/// currently being evaluated to AST nodes whose spans indicate the original
|
||||
/// loads for those modules.
|
||||
///
|
||||
/// Map values may be `null`, which indicates an active module that doesn't
|
||||
/// have a source span associated with its original load (such as the
|
||||
/// entrypoint module).
|
||||
///
|
||||
/// This is used to ensure that we don't get into an infinite load loop.
|
||||
final _activeModules = <Uri>{};
|
||||
final _activeModules = <Uri, AstNode>{};
|
||||
|
||||
/// The dynamic call stack representing function invocations, mixin
|
||||
/// invocations, and imports surrounding the current context.
|
||||
@ -276,47 +288,49 @@ class _EvaluateVisitor
|
||||
var metaFunctions = [
|
||||
// These functions are defined in the context of the evaluator because
|
||||
// they need access to the [_environment] or other local state.
|
||||
BuiltInCallable("global-variable-exists", r"$name, $module: null",
|
||||
(arguments) {
|
||||
BuiltInCallable.function(
|
||||
"global-variable-exists", r"$name, $module: null", (arguments) {
|
||||
var variable = arguments[0].assertString("name");
|
||||
var module = arguments[1].realNull?.assertString("module");
|
||||
return SassBoolean(_environment.globalVariableExists(
|
||||
variable.text.replaceAll("_", "-"),
|
||||
namespace: module?.text));
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("variable-exists", r"$name", (arguments) {
|
||||
BuiltInCallable.function("variable-exists", r"$name", (arguments) {
|
||||
var variable = arguments[0].assertString("name");
|
||||
return SassBoolean(
|
||||
_environment.variableExists(variable.text.replaceAll("_", "-")));
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("function-exists", r"$name, $module: null", (arguments) {
|
||||
BuiltInCallable.function("function-exists", r"$name, $module: null",
|
||||
(arguments) {
|
||||
var variable = arguments[0].assertString("name");
|
||||
var module = arguments[1].realNull?.assertString("module");
|
||||
return SassBoolean(_environment.functionExists(
|
||||
variable.text.replaceAll("_", "-"),
|
||||
namespace: module?.text) ||
|
||||
_builtInFunctions.containsKey(variable.text));
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("mixin-exists", r"$name, $module: null", (arguments) {
|
||||
BuiltInCallable.function("mixin-exists", r"$name, $module: null",
|
||||
(arguments) {
|
||||
var variable = arguments[0].assertString("name");
|
||||
var module = arguments[1].realNull?.assertString("module");
|
||||
return SassBoolean(_environment.mixinExists(
|
||||
variable.text.replaceAll("_", "-"),
|
||||
namespace: module?.text));
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("content-exists", "", (arguments) {
|
||||
BuiltInCallable.function("content-exists", "", (arguments) {
|
||||
if (!_environment.inMixin) {
|
||||
throw SassScriptException(
|
||||
"content-exists() may only be called within a mixin.");
|
||||
}
|
||||
return SassBoolean(_environment.content != null);
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("module-variables", r"$module", (arguments) {
|
||||
BuiltInCallable.function("module-variables", r"$module", (arguments) {
|
||||
var namespace = arguments[0].assertString("module");
|
||||
var module = _environment.modules[namespace.text];
|
||||
if (module == null) {
|
||||
@ -327,9 +341,9 @@ class _EvaluateVisitor
|
||||
for (var entry in module.variables.entries)
|
||||
SassString(entry.key): entry.value
|
||||
});
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("module-functions", r"$module", (arguments) {
|
||||
BuiltInCallable.function("module-functions", r"$module", (arguments) {
|
||||
var namespace = arguments[0].assertString("module");
|
||||
var module = _environment.modules[namespace.text];
|
||||
if (module == null) {
|
||||
@ -340,10 +354,10 @@ class _EvaluateVisitor
|
||||
for (var entry in module.functions.entries)
|
||||
SassString(entry.key): SassFunction(entry.value)
|
||||
});
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("get-function", r"$name, $css: false, $module: null",
|
||||
(arguments) {
|
||||
BuiltInCallable.function(
|
||||
"get-function", r"$name, $css: false, $module: null", (arguments) {
|
||||
var name = arguments[0].assertString("name");
|
||||
var css = arguments[1].isTruthy;
|
||||
var module = arguments[2].realNull?.assertString("module");
|
||||
@ -361,9 +375,10 @@ class _EvaluateVisitor
|
||||
if (callable != null) return SassFunction(callable);
|
||||
|
||||
throw "Function not found: $name";
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
AsyncBuiltInCallable("call", r"$function, $args...", (arguments) async {
|
||||
AsyncBuiltInCallable.function("call", r"$function, $args...",
|
||||
(arguments) async {
|
||||
var function = arguments[0];
|
||||
var args = arguments[1] as SassArgumentList;
|
||||
|
||||
@ -400,11 +415,11 @@ class _EvaluateVisitor
|
||||
"The function ${callable.name} is asynchronous.\n"
|
||||
"This is probably caused by a bug in a Sass plugin.");
|
||||
}
|
||||
})
|
||||
}, url: "sass:meta")
|
||||
];
|
||||
|
||||
var metaMixins = [
|
||||
AsyncBuiltInCallable("load-css", r"$module, $with: null",
|
||||
AsyncBuiltInCallable.mixin("load-css", r"$module, $with: null",
|
||||
(arguments) async {
|
||||
var url = Uri.parse(arguments[0].assertString("module").text);
|
||||
var withMap = arguments[1].realNull?.assertMap("with")?.contents;
|
||||
@ -422,7 +437,7 @@ class _EvaluateVisitor
|
||||
|
||||
values[name] = ConfiguredValue(value, span);
|
||||
});
|
||||
configuration = Configuration(values);
|
||||
configuration = Configuration(values, _callableNode);
|
||||
}
|
||||
|
||||
await _loadModule(url, "load-css()", _callableNode,
|
||||
@ -433,7 +448,7 @@ class _EvaluateVisitor
|
||||
_assertConfigurationIsEmpty(configuration, nameInError: true);
|
||||
|
||||
return null;
|
||||
})
|
||||
}, url: "sass:meta")
|
||||
];
|
||||
|
||||
var metaModule = BuiltInModule("meta",
|
||||
@ -453,7 +468,7 @@ class _EvaluateVisitor
|
||||
return _withWarnCallback(() async {
|
||||
var url = node.span?.sourceUrl;
|
||||
if (url != null) {
|
||||
_activeModules.add(url);
|
||||
_activeModules[url] = null;
|
||||
if (_asNodeSass) {
|
||||
if (url.scheme == 'file') {
|
||||
_includedFiles.add(p.fromUri(url));
|
||||
@ -487,13 +502,13 @@ class _EvaluateVisitor
|
||||
}
|
||||
|
||||
/// Runs [callback] with [importer] as [_importer] and a fake [_stylesheet]
|
||||
/// with [nodeForSpan]'s source span.
|
||||
Future<T> _withFakeStylesheet<T>(AsyncImporter importer, AstNode nodeForSpan,
|
||||
/// with [nodeWithSpan]'s source span.
|
||||
Future<T> _withFakeStylesheet<T>(AsyncImporter importer, AstNode nodeWithSpan,
|
||||
FutureOr<T> callback()) async {
|
||||
var oldImporter = _importer;
|
||||
_importer = importer;
|
||||
var oldStylesheet = _stylesheet;
|
||||
_stylesheet = Stylesheet(const [], nodeForSpan.span);
|
||||
_stylesheet = Stylesheet(const [], nodeWithSpan.span);
|
||||
|
||||
try {
|
||||
return await callback();
|
||||
@ -517,9 +532,9 @@ class _EvaluateVisitor
|
||||
/// configured variables in errors relating to them. This should only be
|
||||
/// `true` if the names won't be obvious from the source span.
|
||||
///
|
||||
/// The [stackFrame] and [nodeForSpan] are used for the name and location of
|
||||
/// The [stackFrame] and [nodeWithSpan] are used for the name and location of
|
||||
/// the stack frame for the duration of the [callback].
|
||||
Future<void> _loadModule(Uri url, String stackFrame, AstNode nodeForSpan,
|
||||
Future<void> _loadModule(Uri url, String stackFrame, AstNode nodeWithSpan,
|
||||
void callback(Module module),
|
||||
{Uri baseUrl,
|
||||
Configuration configuration,
|
||||
@ -531,32 +546,39 @@ class _EvaluateVisitor
|
||||
namesInErrors
|
||||
? "Built-in module $url can't be configured."
|
||||
: "Built-in modules can't be configured.",
|
||||
nodeForSpan.span);
|
||||
nodeWithSpan.span);
|
||||
}
|
||||
|
||||
callback(builtInModule);
|
||||
return;
|
||||
}
|
||||
|
||||
await _withStackFrame(stackFrame, nodeForSpan, () async {
|
||||
var result = await _loadStylesheet(url.toString(), nodeForSpan.span,
|
||||
await _withStackFrame(stackFrame, nodeWithSpan, () async {
|
||||
var result = await _loadStylesheet(url.toString(), nodeWithSpan.span,
|
||||
baseUrl: baseUrl);
|
||||
var importer = result.item1;
|
||||
var stylesheet = result.item2;
|
||||
|
||||
var canonicalUrl = stylesheet.span.sourceUrl;
|
||||
if (_activeModules.contains(canonicalUrl)) {
|
||||
throw _exception(namesInErrors
|
||||
if (_activeModules.containsKey(canonicalUrl)) {
|
||||
var message = namesInErrors
|
||||
? "Module loop: ${p.prettyUri(canonicalUrl)} is already being "
|
||||
"loaded."
|
||||
: "Module loop: this module is already being loaded.");
|
||||
: "Module loop: this module is already being loaded.";
|
||||
var previousLoad = _activeModules[canonicalUrl];
|
||||
throw previousLoad == null
|
||||
? _exception(message)
|
||||
: _multiSpanException(
|
||||
message, "new load", {previousLoad.span: "original load"});
|
||||
}
|
||||
_activeModules.add(canonicalUrl);
|
||||
_activeModules[canonicalUrl] = nodeWithSpan;
|
||||
|
||||
Module module;
|
||||
try {
|
||||
module = await _execute(importer, stylesheet,
|
||||
configuration: configuration, namesInErrors: namesInErrors);
|
||||
configuration: configuration,
|
||||
nodeWithSpan: nodeWithSpan,
|
||||
namesInErrors: namesInErrors);
|
||||
} finally {
|
||||
_activeModules.remove(canonicalUrl);
|
||||
}
|
||||
@ -565,8 +587,14 @@ class _EvaluateVisitor
|
||||
await callback(module);
|
||||
} on SassRuntimeException {
|
||||
rethrow;
|
||||
} on MultiSpanSassException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(error.message, error.span,
|
||||
error.primaryLabel, error.secondarySpans, _stackTrace(error.span));
|
||||
} on SassException catch (error) {
|
||||
throw _exception(error.message, error.span);
|
||||
} on MultiSpanSassScriptException catch (error) {
|
||||
throw _multiSpanException(
|
||||
error.message, error.primaryLabel, error.secondarySpans);
|
||||
} on SassScriptException catch (error) {
|
||||
throw _exception(error.message);
|
||||
}
|
||||
@ -582,17 +610,30 @@ class _EvaluateVisitor
|
||||
/// relating to them. This should only be `true` if the names won't be obvious
|
||||
/// from the source span.
|
||||
Future<Module> _execute(AsyncImporter importer, Stylesheet stylesheet,
|
||||
{Configuration configuration, bool namesInErrors = false}) async {
|
||||
{Configuration configuration,
|
||||
AstNode nodeWithSpan,
|
||||
bool namesInErrors = false}) async {
|
||||
var url = stylesheet.span.sourceUrl;
|
||||
|
||||
var alreadyLoaded = _modules[url];
|
||||
if (alreadyLoaded != null) {
|
||||
if (!(configuration ?? _configuration).isImplicit) {
|
||||
throw _exception(namesInErrors
|
||||
var message = namesInErrors
|
||||
? "${p.prettyUri(url)} was already loaded, so it can't be "
|
||||
"configured using \"with\"."
|
||||
: "This module was already loaded, so it can't be configured using "
|
||||
"\"with\".");
|
||||
"\"with\".";
|
||||
|
||||
var existingNode = _moduleNodes[url];
|
||||
var secondarySpans = {
|
||||
if (existingNode != null) existingNode.span: "original load",
|
||||
if (configuration == null)
|
||||
_configuration.nodeWithSpan.span: "configuration"
|
||||
};
|
||||
|
||||
throw secondarySpans.isEmpty
|
||||
? _exception(message)
|
||||
: _multiSpanException(message, "new load", secondarySpans);
|
||||
}
|
||||
|
||||
return alreadyLoaded;
|
||||
@ -654,6 +695,7 @@ class _EvaluateVisitor
|
||||
|
||||
var module = environment.toModule(css, extender);
|
||||
_modules[url] = module;
|
||||
_moduleNodes[url] = nodeWithSpan;
|
||||
return module;
|
||||
}
|
||||
|
||||
@ -1022,12 +1064,12 @@ class _EvaluateVisitor
|
||||
|
||||
Future<Value> visitEachRule(EachRule node) async {
|
||||
var list = await node.list.accept(this);
|
||||
var nodeForSpan = _expressionNode(node.list);
|
||||
var nodeWithSpan = _expressionNode(node.list);
|
||||
var setVariables = node.variables.length == 1
|
||||
? (Value value) => _environment.setLocalVariable(
|
||||
node.variables.first, value.withoutSlash(), nodeForSpan)
|
||||
node.variables.first, value.withoutSlash(), nodeWithSpan)
|
||||
: (Value value) =>
|
||||
_setMultipleVariables(node.variables, value, nodeForSpan);
|
||||
_setMultipleVariables(node.variables, value, nodeWithSpan);
|
||||
return _environment.scope(() {
|
||||
return _handleReturn<Value>(list.asList, (element) {
|
||||
setVariables(element);
|
||||
@ -1040,15 +1082,15 @@ class _EvaluateVisitor
|
||||
/// Destructures [value] and assigns it to [variables], as in an `@each`
|
||||
/// statement.
|
||||
void _setMultipleVariables(
|
||||
List<String> variables, Value value, AstNode nodeForSpan) {
|
||||
List<String> variables, Value value, AstNode nodeWithSpan) {
|
||||
var list = value.asList;
|
||||
var minLength = math.min(variables.length, list.length);
|
||||
for (var i = 0; i < minLength; i++) {
|
||||
_environment.setLocalVariable(
|
||||
variables[i], list[i].withoutSlash(), nodeForSpan);
|
||||
variables[i], list[i].withoutSlash(), nodeWithSpan);
|
||||
}
|
||||
for (var i = minLength; i < variables.length; i++) {
|
||||
_environment.setLocalVariable(variables[i], sassNull, nodeForSpan);
|
||||
_environment.setLocalVariable(variables[i], sassNull, nodeWithSpan);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1172,10 +1214,10 @@ class _EvaluateVisitor
|
||||
if (from == to) return null;
|
||||
|
||||
return _environment.scope(() async {
|
||||
var nodeForSpan = _expressionNode(node.from);
|
||||
var nodeWithSpan = _expressionNode(node.from);
|
||||
for (var i = from; i != to; i += direction) {
|
||||
_environment.setLocalVariable(
|
||||
node.variable, SassNumber(i), nodeForSpan);
|
||||
node.variable, SassNumber(i), nodeWithSpan);
|
||||
var result = await _handleReturn<Statement>(
|
||||
node.children, (child) => child.accept(this));
|
||||
if (result != null) return result;
|
||||
@ -1237,7 +1279,7 @@ class _EvaluateVisitor
|
||||
_expressionNode(variable.expression));
|
||||
}
|
||||
|
||||
return Configuration(newValues);
|
||||
return Configuration(newValues, node);
|
||||
}
|
||||
|
||||
/// Remove configured values from [upstream] that have been removed from
|
||||
@ -1315,11 +1357,14 @@ class _EvaluateVisitor
|
||||
var stylesheet = result.item2;
|
||||
|
||||
var url = stylesheet.span.sourceUrl;
|
||||
if (!_activeModules.add(url)) {
|
||||
throw _exception("This file is already being loaded.");
|
||||
if (_activeModules.containsKey(url)) {
|
||||
var previousLoad = _activeModules[url];
|
||||
throw previousLoad == null
|
||||
? _exception("This file is already being loaded.")
|
||||
: _multiSpanException("This file is already being loaded.",
|
||||
"new load", {previousLoad.span: "original load"});
|
||||
}
|
||||
|
||||
_activeModules.add(url);
|
||||
_activeModules[url] = import;
|
||||
|
||||
// If the imported stylesheet doesn't use any modules, we can inject its
|
||||
// CSS directly into the current stylesheet. If it does use modules, we
|
||||
@ -1499,27 +1544,34 @@ class _EvaluateVisitor
|
||||
throw _exception("Undefined mixin.", node.span);
|
||||
}
|
||||
|
||||
var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent);
|
||||
if (mixin is AsyncBuiltInCallable) {
|
||||
if (node.content != null) {
|
||||
throw _exception("Mixin doesn't accept a content block.", node.span);
|
||||
}
|
||||
|
||||
await _runBuiltInCallable(node.arguments, mixin, node);
|
||||
await _runBuiltInCallable(node.arguments, mixin, nodeWithSpan);
|
||||
} else if (mixin is UserDefinedCallable<AsyncEnvironment>) {
|
||||
if (node.content != null &&
|
||||
!(mixin.declaration as MixinRule).hasContent) {
|
||||
throw _exception("Mixin doesn't accept a content block.", node.span);
|
||||
throw MultiSpanSassRuntimeException(
|
||||
"Mixin doesn't accept a content block.",
|
||||
node.spanWithoutContent,
|
||||
"invocation",
|
||||
{mixin.declaration.arguments.spanWithName: "declaration"},
|
||||
_stackTrace(node.spanWithoutContent));
|
||||
}
|
||||
|
||||
var contentCallable = node.content == null
|
||||
? null
|
||||
: UserDefinedCallable(node.content, _environment.closure());
|
||||
|
||||
await _runUserDefinedCallable(node.arguments, mixin, node, () async {
|
||||
await _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan,
|
||||
() async {
|
||||
await _environment.withContent(contentCallable, () async {
|
||||
await _environment.asMixin(() async {
|
||||
for (var statement in mixin.declaration.children) {
|
||||
await _addErrorSpan(node, () => statement.accept(this));
|
||||
await _addErrorSpan(nodeWithSpan, () => statement.accept(this));
|
||||
}
|
||||
});
|
||||
return null;
|
||||
@ -1829,10 +1881,10 @@ class _EvaluateVisitor
|
||||
(await variable.expression.accept(this)).withoutSlash(),
|
||||
variable.span,
|
||||
_expressionNode(variable.expression))
|
||||
});
|
||||
}, node);
|
||||
|
||||
await _loadModule(node.url, "@use", node, (module) {
|
||||
_environment.addModule(module, namespace: node.namespace);
|
||||
_environment.addModule(module, node, namespace: node.namespace);
|
||||
}, configuration: configuration);
|
||||
_assertConfigurationIsEmpty(configuration);
|
||||
|
||||
@ -1994,13 +2046,20 @@ class _EvaluateVisitor
|
||||
|
||||
Future<SassMap> visitMapExpression(MapExpression node) async {
|
||||
var map = <Value, Value>{};
|
||||
var keyNodes = <Value, AstNode>{};
|
||||
for (var pair in node.pairs) {
|
||||
var keyValue = await pair.item1.accept(this);
|
||||
var valueValue = await pair.item2.accept(this);
|
||||
if (map.containsKey(keyValue)) {
|
||||
throw _exception('Duplicate key.', pair.item1.span);
|
||||
throw MultiSpanSassRuntimeException(
|
||||
'Duplicate key.',
|
||||
pair.item1.span,
|
||||
'second key',
|
||||
{keyNodes[keyValue].span: 'first key'},
|
||||
_stackTrace(pair.item1.span));
|
||||
}
|
||||
map[keyValue] = valueValue;
|
||||
keyNodes[keyValue] = pair.item1;
|
||||
}
|
||||
return SassMap(map);
|
||||
}
|
||||
@ -2113,8 +2172,12 @@ class _EvaluateVisitor
|
||||
var argumentWord = pluralize('argument', evaluated.named.keys.length);
|
||||
var argumentNames =
|
||||
toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or');
|
||||
throw _exception(
|
||||
"No $argumentWord named $argumentNames.", nodeWithSpan.span);
|
||||
throw MultiSpanSassRuntimeException(
|
||||
"No $argumentWord named $argumentNames.",
|
||||
nodeWithSpan.span,
|
||||
"invocation",
|
||||
{callable.declaration.arguments.spanWithName: "declaration"},
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2222,6 +2285,16 @@ class _EvaluateVisitor
|
||||
result = await callback(evaluated.positional);
|
||||
} on SassRuntimeException {
|
||||
rethrow;
|
||||
} on MultiSpanSassScriptException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(
|
||||
error.message,
|
||||
nodeWithSpan.span,
|
||||
error.primaryLabel,
|
||||
error.secondarySpans,
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
} on MultiSpanSassException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(error.message, error.span,
|
||||
error.primaryLabel, error.secondarySpans, _stackTrace(error.span));
|
||||
} catch (error) {
|
||||
String message;
|
||||
try {
|
||||
@ -2236,10 +2309,13 @@ class _EvaluateVisitor
|
||||
if (argumentList == null) return result;
|
||||
if (evaluated.named.isEmpty) return result;
|
||||
if (argumentList.wereKeywordsAccessed) return result;
|
||||
throw _exception(
|
||||
throw MultiSpanSassRuntimeException(
|
||||
"No ${pluralize('argument', evaluated.named.keys.length)} named "
|
||||
"${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
|
||||
nodeWithSpan.span);
|
||||
"${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
|
||||
nodeWithSpan.span,
|
||||
"invocation",
|
||||
{overload.spanWithName: "declaration"},
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
}
|
||||
|
||||
/// Returns the evaluated values of the given [arguments].
|
||||
@ -2367,7 +2443,7 @@ class _EvaluateVisitor
|
||||
|
||||
/// Adds the values in [map] to [values].
|
||||
///
|
||||
/// Throws a [SassRuntimeException] associated with [nodeForSpan]'s source
|
||||
/// Throws a [SassRuntimeException] associated with [nodeWithSpan]'s source
|
||||
/// span if any [map] keys aren't strings.
|
||||
///
|
||||
/// If [convert] is passed, that's used to convert the map values to the value
|
||||
@ -2376,7 +2452,7 @@ class _EvaluateVisitor
|
||||
/// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
|
||||
/// [AstNode.span] if the span isn't required, since some nodes need to do
|
||||
/// real work to manufacture a source span.
|
||||
void _addRestMap<T>(Map<String, T> values, SassMap map, AstNode nodeForSpan,
|
||||
void _addRestMap<T>(Map<String, T> values, SassMap map, AstNode nodeWithSpan,
|
||||
[T convert(Value value)]) {
|
||||
convert ??= (value) => value as T;
|
||||
map.contents.forEach((key, value) {
|
||||
@ -2386,7 +2462,7 @@ class _EvaluateVisitor
|
||||
throw _exception(
|
||||
"Variable keyword argument map must have string keys.\n"
|
||||
"$key is not a string in $map.",
|
||||
nodeForSpan.span);
|
||||
nodeWithSpan.span);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -2843,13 +2919,22 @@ class _EvaluateVisitor
|
||||
_logger.warn(message,
|
||||
span: span, trace: _stackTrace(span), deprecation: deprecation);
|
||||
|
||||
/// Throws a [SassRuntimeException] with the given [message].
|
||||
/// Returns a [SassRuntimeException] with the given [message].
|
||||
///
|
||||
/// If [span] is passed, it's used for the innermost stack frame.
|
||||
SassRuntimeException _exception(String message, [FileSpan span]) =>
|
||||
SassRuntimeException(
|
||||
message, span ?? _stack.last.item2.span, _stackTrace(span));
|
||||
|
||||
/// Returns a [MultiSpanSassRuntimeException] with the given [message],
|
||||
/// [primaryLabel], and [secondaryLabels].
|
||||
///
|
||||
/// The primary span is taken from the current stack trace span.
|
||||
SassRuntimeException _multiSpanException(String message, String primaryLabel,
|
||||
Map<FileSpan, String> secondaryLabels) =>
|
||||
MultiSpanSassRuntimeException(message, _stack.last.item2.span,
|
||||
primaryLabel, secondaryLabels, _stackTrace());
|
||||
|
||||
/// Runs [callback], and adjusts any [SassFormatException] to be within
|
||||
/// [nodeWithSpan]'s source span.
|
||||
///
|
||||
@ -2887,6 +2972,13 @@ class _EvaluateVisitor
|
||||
T _addExceptionSpan<T>(AstNode nodeWithSpan, T callback()) {
|
||||
try {
|
||||
return callback();
|
||||
} on MultiSpanSassScriptException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(
|
||||
error.message,
|
||||
nodeWithSpan.span,
|
||||
error.primaryLabel,
|
||||
error.secondarySpans,
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
} on SassScriptException catch (error) {
|
||||
throw _exception(error.message, nodeWithSpan.span);
|
||||
}
|
||||
@ -2897,6 +2989,13 @@ class _EvaluateVisitor
|
||||
AstNode nodeWithSpan, Future<T> callback()) async {
|
||||
try {
|
||||
return await callback();
|
||||
} on MultiSpanSassScriptException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(
|
||||
error.message,
|
||||
nodeWithSpan.span,
|
||||
error.primaryLabel,
|
||||
error.secondarySpans,
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
} on SassScriptException catch (error) {
|
||||
throw _exception(error.message, nodeWithSpan.span);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
// DO NOT EDIT. This file was generated from async_evaluate.dart.
|
||||
// See tool/grind/synchronize.dart for details.
|
||||
//
|
||||
// Checksum: 98bd4418d21d4f005485e343869b458b44841450
|
||||
// Checksum: eb095e782e2983223945d189caadc649b081a676
|
||||
//
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
@ -147,6 +147,13 @@ class _EvaluateVisitor
|
||||
/// All modules that have been loaded and evaluated so far.
|
||||
final _modules = <Uri, Module<Callable>>{};
|
||||
|
||||
/// A map from canonical module URLs to the nodes whose spans indicate where
|
||||
/// those modules were originally loaded.
|
||||
///
|
||||
/// This is not guaranteed to have a node for every module in [_modules]. For
|
||||
/// example, the entrypoint module was not loaded by a node.
|
||||
final _moduleNodes = <Uri, AstNode>{};
|
||||
|
||||
/// The logger to use to print warnings.
|
||||
final Logger _logger;
|
||||
|
||||
@ -207,11 +214,16 @@ class _EvaluateVisitor
|
||||
/// imports, it contains the URL passed to the `@import`.
|
||||
final _includedFiles = <String>{};
|
||||
|
||||
/// The set of canonical URLs for modules (or imported files) that are
|
||||
/// currently being evaluated.
|
||||
/// A map from canonical URLs for modules (or imported files) that are
|
||||
/// currently being evaluated to AST nodes whose spans indicate the original
|
||||
/// loads for those modules.
|
||||
///
|
||||
/// Map values may be `null`, which indicates an active module that doesn't
|
||||
/// have a source span associated with its original load (such as the
|
||||
/// entrypoint module).
|
||||
///
|
||||
/// This is used to ensure that we don't get into an infinite load loop.
|
||||
final _activeModules = <Uri>{};
|
||||
final _activeModules = <Uri, AstNode>{};
|
||||
|
||||
/// The dynamic call stack representing function invocations, mixin
|
||||
/// invocations, and imports surrounding the current context.
|
||||
@ -284,47 +296,49 @@ class _EvaluateVisitor
|
||||
var metaFunctions = [
|
||||
// These functions are defined in the context of the evaluator because
|
||||
// they need access to the [_environment] or other local state.
|
||||
BuiltInCallable("global-variable-exists", r"$name, $module: null",
|
||||
(arguments) {
|
||||
BuiltInCallable.function(
|
||||
"global-variable-exists", r"$name, $module: null", (arguments) {
|
||||
var variable = arguments[0].assertString("name");
|
||||
var module = arguments[1].realNull?.assertString("module");
|
||||
return SassBoolean(_environment.globalVariableExists(
|
||||
variable.text.replaceAll("_", "-"),
|
||||
namespace: module?.text));
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("variable-exists", r"$name", (arguments) {
|
||||
BuiltInCallable.function("variable-exists", r"$name", (arguments) {
|
||||
var variable = arguments[0].assertString("name");
|
||||
return SassBoolean(
|
||||
_environment.variableExists(variable.text.replaceAll("_", "-")));
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("function-exists", r"$name, $module: null", (arguments) {
|
||||
BuiltInCallable.function("function-exists", r"$name, $module: null",
|
||||
(arguments) {
|
||||
var variable = arguments[0].assertString("name");
|
||||
var module = arguments[1].realNull?.assertString("module");
|
||||
return SassBoolean(_environment.functionExists(
|
||||
variable.text.replaceAll("_", "-"),
|
||||
namespace: module?.text) ||
|
||||
_builtInFunctions.containsKey(variable.text));
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("mixin-exists", r"$name, $module: null", (arguments) {
|
||||
BuiltInCallable.function("mixin-exists", r"$name, $module: null",
|
||||
(arguments) {
|
||||
var variable = arguments[0].assertString("name");
|
||||
var module = arguments[1].realNull?.assertString("module");
|
||||
return SassBoolean(_environment.mixinExists(
|
||||
variable.text.replaceAll("_", "-"),
|
||||
namespace: module?.text));
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("content-exists", "", (arguments) {
|
||||
BuiltInCallable.function("content-exists", "", (arguments) {
|
||||
if (!_environment.inMixin) {
|
||||
throw SassScriptException(
|
||||
"content-exists() may only be called within a mixin.");
|
||||
}
|
||||
return SassBoolean(_environment.content != null);
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("module-variables", r"$module", (arguments) {
|
||||
BuiltInCallable.function("module-variables", r"$module", (arguments) {
|
||||
var namespace = arguments[0].assertString("module");
|
||||
var module = _environment.modules[namespace.text];
|
||||
if (module == null) {
|
||||
@ -335,9 +349,9 @@ class _EvaluateVisitor
|
||||
for (var entry in module.variables.entries)
|
||||
SassString(entry.key): entry.value
|
||||
});
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("module-functions", r"$module", (arguments) {
|
||||
BuiltInCallable.function("module-functions", r"$module", (arguments) {
|
||||
var namespace = arguments[0].assertString("module");
|
||||
var module = _environment.modules[namespace.text];
|
||||
if (module == null) {
|
||||
@ -348,10 +362,10 @@ class _EvaluateVisitor
|
||||
for (var entry in module.functions.entries)
|
||||
SassString(entry.key): SassFunction(entry.value)
|
||||
});
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("get-function", r"$name, $css: false, $module: null",
|
||||
(arguments) {
|
||||
BuiltInCallable.function(
|
||||
"get-function", r"$name, $css: false, $module: null", (arguments) {
|
||||
var name = arguments[0].assertString("name");
|
||||
var css = arguments[1].isTruthy;
|
||||
var module = arguments[2].realNull?.assertString("module");
|
||||
@ -369,9 +383,9 @@ class _EvaluateVisitor
|
||||
if (callable != null) return SassFunction(callable);
|
||||
|
||||
throw "Function not found: $name";
|
||||
}),
|
||||
}, url: "sass:meta"),
|
||||
|
||||
BuiltInCallable("call", r"$function, $args...", (arguments) {
|
||||
BuiltInCallable.function("call", r"$function, $args...", (arguments) {
|
||||
var function = arguments[0];
|
||||
var args = arguments[1] as SassArgumentList;
|
||||
|
||||
@ -407,11 +421,11 @@ class _EvaluateVisitor
|
||||
"The function ${callable.name} is asynchronous.\n"
|
||||
"This is probably caused by a bug in a Sass plugin.");
|
||||
}
|
||||
})
|
||||
}, url: "sass:meta")
|
||||
];
|
||||
|
||||
var metaMixins = [
|
||||
BuiltInCallable("load-css", r"$module, $with: null", (arguments) {
|
||||
BuiltInCallable.mixin("load-css", r"$module, $with: null", (arguments) {
|
||||
var url = Uri.parse(arguments[0].assertString("module").text);
|
||||
var withMap = arguments[1].realNull?.assertMap("with")?.contents;
|
||||
|
||||
@ -428,7 +442,7 @@ class _EvaluateVisitor
|
||||
|
||||
values[name] = ConfiguredValue(value, span);
|
||||
});
|
||||
configuration = Configuration(values);
|
||||
configuration = Configuration(values, _callableNode);
|
||||
}
|
||||
|
||||
_loadModule(url, "load-css()", _callableNode,
|
||||
@ -439,7 +453,7 @@ class _EvaluateVisitor
|
||||
_assertConfigurationIsEmpty(configuration, nameInError: true);
|
||||
|
||||
return null;
|
||||
})
|
||||
}, url: "sass:meta")
|
||||
];
|
||||
|
||||
var metaModule = BuiltInModule("meta",
|
||||
@ -459,7 +473,7 @@ class _EvaluateVisitor
|
||||
return _withWarnCallback(() {
|
||||
var url = node.span?.sourceUrl;
|
||||
if (url != null) {
|
||||
_activeModules.add(url);
|
||||
_activeModules[url] = null;
|
||||
if (_asNodeSass) {
|
||||
if (url.scheme == 'file') {
|
||||
_includedFiles.add(p.fromUri(url));
|
||||
@ -493,13 +507,13 @@ class _EvaluateVisitor
|
||||
}
|
||||
|
||||
/// Runs [callback] with [importer] as [_importer] and a fake [_stylesheet]
|
||||
/// with [nodeForSpan]'s source span.
|
||||
/// with [nodeWithSpan]'s source span.
|
||||
T _withFakeStylesheet<T>(
|
||||
Importer importer, AstNode nodeForSpan, T callback()) {
|
||||
Importer importer, AstNode nodeWithSpan, T callback()) {
|
||||
var oldImporter = _importer;
|
||||
_importer = importer;
|
||||
var oldStylesheet = _stylesheet;
|
||||
_stylesheet = Stylesheet(const [], nodeForSpan.span);
|
||||
_stylesheet = Stylesheet(const [], nodeWithSpan.span);
|
||||
|
||||
try {
|
||||
return callback();
|
||||
@ -523,9 +537,9 @@ class _EvaluateVisitor
|
||||
/// configured variables in errors relating to them. This should only be
|
||||
/// `true` if the names won't be obvious from the source span.
|
||||
///
|
||||
/// The [stackFrame] and [nodeForSpan] are used for the name and location of
|
||||
/// The [stackFrame] and [nodeWithSpan] are used for the name and location of
|
||||
/// the stack frame for the duration of the [callback].
|
||||
void _loadModule(Uri url, String stackFrame, AstNode nodeForSpan,
|
||||
void _loadModule(Uri url, String stackFrame, AstNode nodeWithSpan,
|
||||
void callback(Module<Callable> module),
|
||||
{Uri baseUrl, Configuration configuration, bool namesInErrors = false}) {
|
||||
var builtInModule = _builtInModules[url];
|
||||
@ -535,32 +549,39 @@ class _EvaluateVisitor
|
||||
namesInErrors
|
||||
? "Built-in module $url can't be configured."
|
||||
: "Built-in modules can't be configured.",
|
||||
nodeForSpan.span);
|
||||
nodeWithSpan.span);
|
||||
}
|
||||
|
||||
callback(builtInModule);
|
||||
return;
|
||||
}
|
||||
|
||||
_withStackFrame(stackFrame, nodeForSpan, () {
|
||||
_withStackFrame(stackFrame, nodeWithSpan, () {
|
||||
var result =
|
||||
_loadStylesheet(url.toString(), nodeForSpan.span, baseUrl: baseUrl);
|
||||
_loadStylesheet(url.toString(), nodeWithSpan.span, baseUrl: baseUrl);
|
||||
var importer = result.item1;
|
||||
var stylesheet = result.item2;
|
||||
|
||||
var canonicalUrl = stylesheet.span.sourceUrl;
|
||||
if (_activeModules.contains(canonicalUrl)) {
|
||||
throw _exception(namesInErrors
|
||||
if (_activeModules.containsKey(canonicalUrl)) {
|
||||
var message = namesInErrors
|
||||
? "Module loop: ${p.prettyUri(canonicalUrl)} is already being "
|
||||
"loaded."
|
||||
: "Module loop: this module is already being loaded.");
|
||||
: "Module loop: this module is already being loaded.";
|
||||
var previousLoad = _activeModules[canonicalUrl];
|
||||
throw previousLoad == null
|
||||
? _exception(message)
|
||||
: _multiSpanException(
|
||||
message, "new load", {previousLoad.span: "original load"});
|
||||
}
|
||||
_activeModules.add(canonicalUrl);
|
||||
_activeModules[canonicalUrl] = nodeWithSpan;
|
||||
|
||||
Module<Callable> module;
|
||||
try {
|
||||
module = _execute(importer, stylesheet,
|
||||
configuration: configuration, namesInErrors: namesInErrors);
|
||||
configuration: configuration,
|
||||
nodeWithSpan: nodeWithSpan,
|
||||
namesInErrors: namesInErrors);
|
||||
} finally {
|
||||
_activeModules.remove(canonicalUrl);
|
||||
}
|
||||
@ -569,8 +590,14 @@ class _EvaluateVisitor
|
||||
callback(module);
|
||||
} on SassRuntimeException {
|
||||
rethrow;
|
||||
} on MultiSpanSassException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(error.message, error.span,
|
||||
error.primaryLabel, error.secondarySpans, _stackTrace(error.span));
|
||||
} on SassException catch (error) {
|
||||
throw _exception(error.message, error.span);
|
||||
} on MultiSpanSassScriptException catch (error) {
|
||||
throw _multiSpanException(
|
||||
error.message, error.primaryLabel, error.secondarySpans);
|
||||
} on SassScriptException catch (error) {
|
||||
throw _exception(error.message);
|
||||
}
|
||||
@ -586,17 +613,30 @@ class _EvaluateVisitor
|
||||
/// relating to them. This should only be `true` if the names won't be obvious
|
||||
/// from the source span.
|
||||
Module<Callable> _execute(Importer importer, Stylesheet stylesheet,
|
||||
{Configuration configuration, bool namesInErrors = false}) {
|
||||
{Configuration configuration,
|
||||
AstNode nodeWithSpan,
|
||||
bool namesInErrors = false}) {
|
||||
var url = stylesheet.span.sourceUrl;
|
||||
|
||||
var alreadyLoaded = _modules[url];
|
||||
if (alreadyLoaded != null) {
|
||||
if (!(configuration ?? _configuration).isImplicit) {
|
||||
throw _exception(namesInErrors
|
||||
var message = namesInErrors
|
||||
? "${p.prettyUri(url)} was already loaded, so it can't be "
|
||||
"configured using \"with\"."
|
||||
: "This module was already loaded, so it can't be configured using "
|
||||
"\"with\".");
|
||||
"\"with\".";
|
||||
|
||||
var existingNode = _moduleNodes[url];
|
||||
var secondarySpans = {
|
||||
if (existingNode != null) existingNode.span: "original load",
|
||||
if (configuration == null)
|
||||
_configuration.nodeWithSpan.span: "configuration"
|
||||
};
|
||||
|
||||
throw secondarySpans.isEmpty
|
||||
? _exception(message)
|
||||
: _multiSpanException(message, "new load", secondarySpans);
|
||||
}
|
||||
|
||||
return alreadyLoaded;
|
||||
@ -658,6 +698,7 @@ class _EvaluateVisitor
|
||||
|
||||
var module = environment.toModule(css, extender);
|
||||
_modules[url] = module;
|
||||
_moduleNodes[url] = nodeWithSpan;
|
||||
return module;
|
||||
}
|
||||
|
||||
@ -1025,12 +1066,12 @@ class _EvaluateVisitor
|
||||
|
||||
Value visitEachRule(EachRule node) {
|
||||
var list = node.list.accept(this);
|
||||
var nodeForSpan = _expressionNode(node.list);
|
||||
var nodeWithSpan = _expressionNode(node.list);
|
||||
var setVariables = node.variables.length == 1
|
||||
? (Value value) => _environment.setLocalVariable(
|
||||
node.variables.first, value.withoutSlash(), nodeForSpan)
|
||||
node.variables.first, value.withoutSlash(), nodeWithSpan)
|
||||
: (Value value) =>
|
||||
_setMultipleVariables(node.variables, value, nodeForSpan);
|
||||
_setMultipleVariables(node.variables, value, nodeWithSpan);
|
||||
return _environment.scope(() {
|
||||
return _handleReturn<Value>(list.asList, (element) {
|
||||
setVariables(element);
|
||||
@ -1043,15 +1084,15 @@ class _EvaluateVisitor
|
||||
/// Destructures [value] and assigns it to [variables], as in an `@each`
|
||||
/// statement.
|
||||
void _setMultipleVariables(
|
||||
List<String> variables, Value value, AstNode nodeForSpan) {
|
||||
List<String> variables, Value value, AstNode nodeWithSpan) {
|
||||
var list = value.asList;
|
||||
var minLength = math.min(variables.length, list.length);
|
||||
for (var i = 0; i < minLength; i++) {
|
||||
_environment.setLocalVariable(
|
||||
variables[i], list[i].withoutSlash(), nodeForSpan);
|
||||
variables[i], list[i].withoutSlash(), nodeWithSpan);
|
||||
}
|
||||
for (var i = minLength; i < variables.length; i++) {
|
||||
_environment.setLocalVariable(variables[i], sassNull, nodeForSpan);
|
||||
_environment.setLocalVariable(variables[i], sassNull, nodeWithSpan);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1171,10 +1212,10 @@ class _EvaluateVisitor
|
||||
if (from == to) return null;
|
||||
|
||||
return _environment.scope(() {
|
||||
var nodeForSpan = _expressionNode(node.from);
|
||||
var nodeWithSpan = _expressionNode(node.from);
|
||||
for (var i = from; i != to; i += direction) {
|
||||
_environment.setLocalVariable(
|
||||
node.variable, SassNumber(i), nodeForSpan);
|
||||
node.variable, SassNumber(i), nodeWithSpan);
|
||||
var result = _handleReturn<Statement>(
|
||||
node.children, (child) => child.accept(this));
|
||||
if (result != null) return result;
|
||||
@ -1236,7 +1277,7 @@ class _EvaluateVisitor
|
||||
_expressionNode(variable.expression));
|
||||
}
|
||||
|
||||
return Configuration(newValues);
|
||||
return Configuration(newValues, node);
|
||||
}
|
||||
|
||||
/// Remove configured values from [upstream] that have been removed from
|
||||
@ -1313,11 +1354,14 @@ class _EvaluateVisitor
|
||||
var stylesheet = result.item2;
|
||||
|
||||
var url = stylesheet.span.sourceUrl;
|
||||
if (!_activeModules.add(url)) {
|
||||
throw _exception("This file is already being loaded.");
|
||||
if (_activeModules.containsKey(url)) {
|
||||
var previousLoad = _activeModules[url];
|
||||
throw previousLoad == null
|
||||
? _exception("This file is already being loaded.")
|
||||
: _multiSpanException("This file is already being loaded.",
|
||||
"new load", {previousLoad.span: "original load"});
|
||||
}
|
||||
|
||||
_activeModules.add(url);
|
||||
_activeModules[url] = import;
|
||||
|
||||
// If the imported stylesheet doesn't use any modules, we can inject its
|
||||
// CSS directly into the current stylesheet. If it does use modules, we
|
||||
@ -1496,27 +1540,33 @@ class _EvaluateVisitor
|
||||
throw _exception("Undefined mixin.", node.span);
|
||||
}
|
||||
|
||||
var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent);
|
||||
if (mixin is BuiltInCallable) {
|
||||
if (node.content != null) {
|
||||
throw _exception("Mixin doesn't accept a content block.", node.span);
|
||||
}
|
||||
|
||||
_runBuiltInCallable(node.arguments, mixin, node);
|
||||
_runBuiltInCallable(node.arguments, mixin, nodeWithSpan);
|
||||
} else if (mixin is UserDefinedCallable<Environment>) {
|
||||
if (node.content != null &&
|
||||
!(mixin.declaration as MixinRule).hasContent) {
|
||||
throw _exception("Mixin doesn't accept a content block.", node.span);
|
||||
throw MultiSpanSassRuntimeException(
|
||||
"Mixin doesn't accept a content block.",
|
||||
node.spanWithoutContent,
|
||||
"invocation",
|
||||
{mixin.declaration.arguments.spanWithName: "declaration"},
|
||||
_stackTrace(node.spanWithoutContent));
|
||||
}
|
||||
|
||||
var contentCallable = node.content == null
|
||||
? null
|
||||
: UserDefinedCallable(node.content, _environment.closure());
|
||||
|
||||
_runUserDefinedCallable(node.arguments, mixin, node, () {
|
||||
_runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, () {
|
||||
_environment.withContent(contentCallable, () {
|
||||
_environment.asMixin(() {
|
||||
for (var statement in mixin.declaration.children) {
|
||||
_addErrorSpan(node, () => statement.accept(this));
|
||||
_addErrorSpan(nodeWithSpan, () => statement.accept(this));
|
||||
}
|
||||
});
|
||||
return null;
|
||||
@ -1821,10 +1871,10 @@ class _EvaluateVisitor
|
||||
variable.expression.accept(this).withoutSlash(),
|
||||
variable.span,
|
||||
_expressionNode(variable.expression))
|
||||
});
|
||||
}, node);
|
||||
|
||||
_loadModule(node.url, "@use", node, (module) {
|
||||
_environment.addModule(module, namespace: node.namespace);
|
||||
_environment.addModule(module, node, namespace: node.namespace);
|
||||
}, configuration: configuration);
|
||||
_assertConfigurationIsEmpty(configuration);
|
||||
|
||||
@ -1981,13 +2031,20 @@ class _EvaluateVisitor
|
||||
|
||||
SassMap visitMapExpression(MapExpression node) {
|
||||
var map = <Value, Value>{};
|
||||
var keyNodes = <Value, AstNode>{};
|
||||
for (var pair in node.pairs) {
|
||||
var keyValue = pair.item1.accept(this);
|
||||
var valueValue = pair.item2.accept(this);
|
||||
if (map.containsKey(keyValue)) {
|
||||
throw _exception('Duplicate key.', pair.item1.span);
|
||||
throw MultiSpanSassRuntimeException(
|
||||
'Duplicate key.',
|
||||
pair.item1.span,
|
||||
'second key',
|
||||
{keyNodes[keyValue].span: 'first key'},
|
||||
_stackTrace(pair.item1.span));
|
||||
}
|
||||
map[keyValue] = valueValue;
|
||||
keyNodes[keyValue] = pair.item1;
|
||||
}
|
||||
return SassMap(map);
|
||||
}
|
||||
@ -2100,8 +2157,12 @@ class _EvaluateVisitor
|
||||
var argumentWord = pluralize('argument', evaluated.named.keys.length);
|
||||
var argumentNames =
|
||||
toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or');
|
||||
throw _exception(
|
||||
"No $argumentWord named $argumentNames.", nodeWithSpan.span);
|
||||
throw MultiSpanSassRuntimeException(
|
||||
"No $argumentWord named $argumentNames.",
|
||||
nodeWithSpan.span,
|
||||
"invocation",
|
||||
{callable.declaration.arguments.spanWithName: "declaration"},
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2207,6 +2268,16 @@ class _EvaluateVisitor
|
||||
result = callback(evaluated.positional);
|
||||
} on SassRuntimeException {
|
||||
rethrow;
|
||||
} on MultiSpanSassScriptException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(
|
||||
error.message,
|
||||
nodeWithSpan.span,
|
||||
error.primaryLabel,
|
||||
error.secondarySpans,
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
} on MultiSpanSassException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(error.message, error.span,
|
||||
error.primaryLabel, error.secondarySpans, _stackTrace(error.span));
|
||||
} catch (error) {
|
||||
String message;
|
||||
try {
|
||||
@ -2221,10 +2292,13 @@ class _EvaluateVisitor
|
||||
if (argumentList == null) return result;
|
||||
if (evaluated.named.isEmpty) return result;
|
||||
if (argumentList.wereKeywordsAccessed) return result;
|
||||
throw _exception(
|
||||
throw MultiSpanSassRuntimeException(
|
||||
"No ${pluralize('argument', evaluated.named.keys.length)} named "
|
||||
"${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
|
||||
nodeWithSpan.span);
|
||||
"${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
|
||||
nodeWithSpan.span,
|
||||
"invocation",
|
||||
{overload.spanWithName: "declaration"},
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
}
|
||||
|
||||
/// Returns the evaluated values of the given [arguments].
|
||||
@ -2351,7 +2425,7 @@ class _EvaluateVisitor
|
||||
|
||||
/// Adds the values in [map] to [values].
|
||||
///
|
||||
/// Throws a [SassRuntimeException] associated with [nodeForSpan]'s source
|
||||
/// Throws a [SassRuntimeException] associated with [nodeWithSpan]'s source
|
||||
/// span if any [map] keys aren't strings.
|
||||
///
|
||||
/// If [convert] is passed, that's used to convert the map values to the value
|
||||
@ -2360,7 +2434,7 @@ class _EvaluateVisitor
|
||||
/// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
|
||||
/// [AstNode.span] if the span isn't required, since some nodes need to do
|
||||
/// real work to manufacture a source span.
|
||||
void _addRestMap<T>(Map<String, T> values, SassMap map, AstNode nodeForSpan,
|
||||
void _addRestMap<T>(Map<String, T> values, SassMap map, AstNode nodeWithSpan,
|
||||
[T convert(Value value)]) {
|
||||
convert ??= (value) => value as T;
|
||||
map.contents.forEach((key, value) {
|
||||
@ -2370,7 +2444,7 @@ class _EvaluateVisitor
|
||||
throw _exception(
|
||||
"Variable keyword argument map must have string keys.\n"
|
||||
"$key is not a string in $map.",
|
||||
nodeForSpan.span);
|
||||
nodeWithSpan.span);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -2816,13 +2890,22 @@ class _EvaluateVisitor
|
||||
_logger.warn(message,
|
||||
span: span, trace: _stackTrace(span), deprecation: deprecation);
|
||||
|
||||
/// Throws a [SassRuntimeException] with the given [message].
|
||||
/// Returns a [SassRuntimeException] with the given [message].
|
||||
///
|
||||
/// If [span] is passed, it's used for the innermost stack frame.
|
||||
SassRuntimeException _exception(String message, [FileSpan span]) =>
|
||||
SassRuntimeException(
|
||||
message, span ?? _stack.last.item2.span, _stackTrace(span));
|
||||
|
||||
/// Returns a [MultiSpanSassRuntimeException] with the given [message],
|
||||
/// [primaryLabel], and [secondaryLabels].
|
||||
///
|
||||
/// The primary span is taken from the current stack trace span.
|
||||
SassRuntimeException _multiSpanException(String message, String primaryLabel,
|
||||
Map<FileSpan, String> secondaryLabels) =>
|
||||
MultiSpanSassRuntimeException(message, _stack.last.item2.span,
|
||||
primaryLabel, secondaryLabels, _stackTrace());
|
||||
|
||||
/// Runs [callback], and adjusts any [SassFormatException] to be within
|
||||
/// [nodeWithSpan]'s source span.
|
||||
///
|
||||
@ -2860,6 +2943,13 @@ class _EvaluateVisitor
|
||||
T _addExceptionSpan<T>(AstNode nodeWithSpan, T callback()) {
|
||||
try {
|
||||
return callback();
|
||||
} on MultiSpanSassScriptException catch (error) {
|
||||
throw MultiSpanSassRuntimeException(
|
||||
error.message,
|
||||
nodeWithSpan.span,
|
||||
error.primaryLabel,
|
||||
error.secondarySpans,
|
||||
_stackTrace(nodeWithSpan.span));
|
||||
} on SassScriptException catch (error) {
|
||||
throw _exception(error.message, nodeWithSpan.span);
|
||||
}
|
||||
|
@ -346,6 +346,9 @@ class _SerializeVisitor
|
||||
try {
|
||||
_buffer.forSpan(
|
||||
node.valueSpanForMap, () => node.value.value.accept(this));
|
||||
} on MultiSpanSassScriptException catch (error) {
|
||||
throw MultiSpanSassException(error.message, node.value.span,
|
||||
error.primaryLabel, error.secondarySpans);
|
||||
} on SassScriptException catch (error) {
|
||||
throw SassException(error.message, node.value.span);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: sass
|
||||
version: 1.25.0-dev
|
||||
version: 1.25.0
|
||||
description: A Sass implementation in Dart.
|
||||
author: Sass Team
|
||||
homepage: https://github.com/sass/dart-sass
|
||||
@ -21,9 +21,7 @@ dependencies:
|
||||
package_resolver: "^1.0.0"
|
||||
path: "^1.6.0"
|
||||
source_maps: "^0.10.5"
|
||||
|
||||
# Temporarily limit this until #926 lands.
|
||||
source_span: ">=1.4.0 <1.6.0"
|
||||
source_span: "^1.6.0"
|
||||
stack_trace: ">=0.9.0 <2.0.0"
|
||||
stream_transform: ">=0.0.20 <2.0.0"
|
||||
string_scanner: ">=0.1.5 <2.0.0"
|
||||
|
@ -132,7 +132,7 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
|
||||
"Error: Expected expression.",
|
||||
"\u001b[34m ,\u001b[0m",
|
||||
"\u001b[34m1 |\u001b[0m a {b: \u001b[31m\u001b[0m}",
|
||||
"\u001b[34m |\u001b[0m \u001b[31m^\u001b[0m",
|
||||
"\u001b[34m |\u001b[0m \u001b[31m ^\u001b[0m",
|
||||
"\u001b[34m '\u001b[0m",
|
||||
" test.scss 1:7 root stylesheet",
|
||||
]));
|
||||
|
54
test/double_check_test.dart
Normal file
54
test/double_check_test.dart
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
@TestOn('vm')
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import '../tool/grind/synchronize.dart' as synchronize;
|
||||
|
||||
/// Tests that double-check that everything in the repo looks sensible.
|
||||
void main() {
|
||||
group("synchronized file is up-to-date:", () {
|
||||
/// The pattern of a checksum in a generated file.
|
||||
var checksumPattern = RegExp(r"^// Checksum: (.*)$", multiLine: true);
|
||||
|
||||
synchronize.sources.forEach((sourcePath, targetPath) {
|
||||
test(targetPath, () {
|
||||
var target = File(targetPath).readAsStringSync();
|
||||
var actualHash = checksumPattern.firstMatch(target)[1];
|
||||
|
||||
var source = File(sourcePath).readAsBytesSync();
|
||||
var expectedHash = sha1.convert(source).toString();
|
||||
expect(actualHash, equals(expectedHash),
|
||||
reason: "$targetPath is out-of-date.\n"
|
||||
"Run pub run grinder to update it.");
|
||||
});
|
||||
});
|
||||
},
|
||||
// Windows sees different bytes than other OSes, possibly because of
|
||||
// newline normalization issues.
|
||||
testOn: "!windows");
|
||||
|
||||
test("pubspec version matches CHANGELOG version", () {
|
||||
var firstLine = const LineSplitter()
|
||||
.convert(File("CHANGELOG.md").readAsStringSync())
|
||||
.first;
|
||||
expect(firstLine, startsWith("## "));
|
||||
var changelogVersion = firstLine.substring(3);
|
||||
|
||||
var pubspec = loadYaml(File("pubspec.yaml").readAsStringSync(),
|
||||
sourceUrl: "pubspec.yaml") as Map<Object, Object>;
|
||||
expect(pubspec, containsPair("version", isA<String>()));
|
||||
var pubspecVersion = pubspec["version"] as String;
|
||||
|
||||
expect(pubspecVersion,
|
||||
anyOf(equals(changelogVersion), equals("$changelogVersion-dev")));
|
||||
});
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
// Copyright 2017 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.
|
||||
|
||||
// Windows sees different bytes than other OSes, possibly because of newline
|
||||
// normalization issues.
|
||||
@TestOn('vm && !windows')
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../tool/grind/synchronize.dart' as synchronize;
|
||||
|
||||
/// The pattern of a checksum in a generated file.
|
||||
final _checksumPattern = RegExp(r"^// Checksum: (.*)$", multiLine: true);
|
||||
|
||||
void main() {
|
||||
synchronize.sources.forEach((sourcePath, targetPath) {
|
||||
test("synchronized file $targetPath is up-to-date", () {
|
||||
var target = File(targetPath).readAsStringSync();
|
||||
var actualHash = _checksumPattern.firstMatch(target)[1];
|
||||
|
||||
var source = File(sourcePath).readAsBytesSync();
|
||||
var expectedHash = sha1.convert(source).toString();
|
||||
expect(actualHash, equals(expectedHash),
|
||||
reason: "$targetPath is out-of-date.\n"
|
||||
"Run pub run grinder to update it.");
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user