diff --git a/CHANGELOG.md b/CHANGELOG.md index abb205336..e5f4fa3ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ ## 1.69.0 +* Add a `meta.get-mixin()` function that returns a mixin as a first-class Sass + value. + +* Add a `meta.apply()` mixin that includes a mixin value. + +* Add a `meta.module-mixins()` function which returns a map from mixin names in + a module to the first-class mixins that belong to those names. + +* Add a `meta.accepts-content()` function which returns whether or not a mixin + value can take a content block. + * Add support for the relative color syntax from CSS Color 5. This syntax cannot be used to create Sass color values. It is always emitted as-is in the CSS output. diff --git a/lib/src/callable/async_built_in.dart b/lib/src/callable/async_built_in.dart index 0132b787f..7dba7e1cd 100644 --- a/lib/src/callable/async_built_in.dart +++ b/lib/src/callable/async_built_in.dart @@ -26,6 +26,11 @@ class AsyncBuiltInCallable implements AsyncCallable { /// The callback to run when executing this callable. final Callback _callback; + /// Whether this callable could potentially accept an `@content` block. + /// + /// This can only be true for mixins. + final bool acceptsContent; + /// Creates a function with a single [arguments] declaration and a single /// [callback]. /// @@ -52,7 +57,7 @@ class AsyncBuiltInCallable implements AsyncCallable { /// defined. AsyncBuiltInCallable.mixin(String name, String arguments, FutureOr callback(List arguments), - {Object? url}) + {Object? url, bool acceptsContent = false}) : this.parsed(name, ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url), (arguments) async { @@ -66,7 +71,8 @@ class AsyncBuiltInCallable implements AsyncCallable { /// Creates a callable with a single [arguments] declaration and a single /// [callback]. - AsyncBuiltInCallable.parsed(this.name, this._arguments, this._callback); + AsyncBuiltInCallable.parsed(this.name, this._arguments, this._callback, + {this.acceptsContent = false}); /// Returns the argument declaration and Dart callback for the given /// positional and named arguments. diff --git a/lib/src/callable/built_in.dart b/lib/src/callable/built_in.dart index 905d11e56..a52662fa0 100644 --- a/lib/src/callable/built_in.dart +++ b/lib/src/callable/built_in.dart @@ -21,6 +21,8 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable { /// The overloads declared for this callable. final List<(ArgumentDeclaration, Callback)> _overloads; + final bool acceptsContent; + /// Creates a function with a single [arguments] declaration and a single /// [callback]. /// @@ -48,18 +50,19 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable { /// defined. BuiltInCallable.mixin( String name, String arguments, void callback(List arguments), - {Object? url}) + {Object? url, bool acceptsContent = false}) : this.parsed(name, ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url), (arguments) { callback(arguments); return sassNull; - }); + }, acceptsContent: acceptsContent); /// Creates a callable with a single [arguments] declaration and a single /// [callback]. BuiltInCallable.parsed(this.name, ArgumentDeclaration arguments, - Value callback(List arguments)) + Value callback(List arguments), + {this.acceptsContent = false}) : _overloads = [(arguments, callback)]; /// Creates a function with multiple implementations. @@ -79,9 +82,10 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable { ArgumentDeclaration.parse('@function $name($args) {', url: url), callback ) - ]; + ], + acceptsContent = false; - BuiltInCallable._(this.name, this._overloads); + BuiltInCallable._(this.name, this._overloads, this.acceptsContent); /// Returns the argument declaration and Dart callback for the given /// positional and named arguments. @@ -117,5 +121,6 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable { } /// Returns a copy of this callable with the given [name]. - BuiltInCallable withName(String name) => BuiltInCallable._(name, _overloads); + BuiltInCallable withName(String name) => + BuiltInCallable._(name, _overloads, acceptsContent); } diff --git a/lib/src/embedded/dispatcher.dart b/lib/src/embedded/dispatcher.dart index a1b20b5d4..40e81863d 100644 --- a/lib/src/embedded/dispatcher.dart +++ b/lib/src/embedded/dispatcher.dart @@ -12,8 +12,10 @@ import 'package:path/path.dart' as p; import 'package:protobuf/protobuf.dart'; import 'package:sass/sass.dart' as sass; +import '../value/function.dart'; +import '../value/mixin.dart'; import 'embedded_sass.pb.dart'; -import 'function_registry.dart'; +import 'opaque_registry.dart'; import 'host_callable.dart'; import 'importer/file.dart'; import 'importer/host.dart'; @@ -109,7 +111,8 @@ final class Dispatcher { OutboundMessage_CompileResponse _compile( InboundMessage_CompileRequest request) { - var functions = FunctionRegistry(); + var functions = OpaqueRegistry(); + var mixins = OpaqueRegistry(); var style = request.style == OutputStyle.COMPRESSED ? sass.OutputStyle.compressed @@ -123,7 +126,7 @@ final class Dispatcher { (throw mandatoryError("Importer.importer"))); var globalFunctions = request.globalFunctions - .map((signature) => hostCallable(this, functions, signature)); + .map((signature) => hostCallable(this, functions, mixins, signature)); late sass.CompileResult result; switch (request.whichInput()) { diff --git a/lib/src/embedded/function_registry.dart b/lib/src/embedded/function_registry.dart deleted file mode 100644 index b288fbd8d..000000000 --- a/lib/src/embedded/function_registry.dart +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2019 Google Inc. Use of this source code is governed by an -// MIT-style license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -import '../value/function.dart'; -import 'embedded_sass.pb.dart'; - -/// A registry of [SassFunction]s indexed by ID so that the host can invoke -/// them. -final class FunctionRegistry { - /// First-class functions that have been sent to the host. - /// - /// The functions are located at indexes in the list matching their IDs. - final _functionsById = []; - - /// A reverse map from functions to their indexes in [_functionsById]. - final _idsByFunction = {}; - - /// Converts [function] to a protocol buffer to send to the host. - Value_CompilerFunction protofy(SassFunction function) { - var id = _idsByFunction.putIfAbsent(function, () { - _functionsById.add(function); - return _functionsById.length - 1; - }); - - return Value_CompilerFunction()..id = id; - } - - /// Returns the compiler-side function associated with [id]. - /// - /// If no such function exists, returns `null`. - SassFunction? operator [](int id) => _functionsById[id]; -} diff --git a/lib/src/embedded/host_callable.dart b/lib/src/embedded/host_callable.dart index 448cce217..3518b57e2 100644 --- a/lib/src/embedded/host_callable.dart +++ b/lib/src/embedded/host_callable.dart @@ -4,9 +4,11 @@ import '../callable.dart'; import '../exception.dart'; +import '../value/function.dart'; +import '../value/mixin.dart'; import 'dispatcher.dart'; import 'embedded_sass.pb.dart'; -import 'function_registry.dart'; +import 'opaque_registry.dart'; import 'protofier.dart'; import 'utils.dart'; @@ -19,11 +21,14 @@ import 'utils.dart'; /// /// Throws a [SassException] if [signature] is invalid. Callable hostCallable( - Dispatcher dispatcher, FunctionRegistry functions, String signature, + Dispatcher dispatcher, + OpaqueRegistry functions, + OpaqueRegistry mixins, + String signature, {int? id}) { late Callable callable; callable = Callable.fromSignature(signature, (arguments) { - var protofier = Protofier(dispatcher, functions); + var protofier = Protofier(dispatcher, functions, mixins); var request = OutboundMessage_FunctionCallRequest() ..arguments.addAll( [for (var argument in arguments) protofier.protofy(argument)]); diff --git a/lib/src/embedded/opaque_registry.dart b/lib/src/embedded/opaque_registry.dart new file mode 100644 index 000000000..bf9adaab2 --- /dev/null +++ b/lib/src/embedded/opaque_registry.dart @@ -0,0 +1,30 @@ +// Copyright 2019 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. + +/// A registry of some `T` indexed by ID so that the host can invoke +/// them. +final class OpaqueRegistry { + /// Instantiations of `T` that have been sent to the host. + /// + /// The values are located at indexes in the list matching their IDs. + final _elementsById = []; + + /// A reverse map from elements to their indexes in [_elementsById]. + final _idsByElement = {}; + + /// Returns the compiler-side id associated with [element]. + int getId(T element) { + var id = _idsByElement.putIfAbsent(element, () { + _elementsById.add(element); + return _elementsById.length - 1; + }); + + return id; + } + + /// Returns the compiler-side element associated with [id]. + /// + /// If no such element exists, returns `null`. + T? operator [](int id) => _elementsById[id]; +} diff --git a/lib/src/embedded/protofier.dart b/lib/src/embedded/protofier.dart index 6ad083ca4..eec7e3d05 100644 --- a/lib/src/embedded/protofier.dart +++ b/lib/src/embedded/protofier.dart @@ -8,8 +8,8 @@ import '../value.dart'; import 'dispatcher.dart'; import 'embedded_sass.pb.dart' as proto; import 'embedded_sass.pb.dart' hide Value, ListSeparator, CalculationOperator; -import 'function_registry.dart'; import 'host_callable.dart'; +import 'opaque_registry.dart'; import 'utils.dart'; /// A class that converts Sass [Value] objects into [Value] protobufs. @@ -21,7 +21,10 @@ final class Protofier { final Dispatcher _dispatcher; /// The IDs of first-class functions. - final FunctionRegistry _functions; + final OpaqueRegistry _functions; + + /// The IDs of first-class mixins. + final OpaqueRegistry _mixins; /// Any argument lists transitively contained in [value]. /// @@ -35,7 +38,10 @@ final class Protofier { /// /// The [functions] tracks the IDs of first-class functions so that the host /// can pass them back to the compiler. - Protofier(this._dispatcher, this._functions); + /// + /// Similarly, the [mixins] tracks the IDs of first-class mixins so that the + /// host can pass them back to the compiler. + Protofier(this._dispatcher, this._functions, this._mixins); /// Converts [value] to its protocol buffer representation. proto.Value protofy(Value value) { @@ -84,7 +90,10 @@ final class Protofier { case SassCalculation(): result.calculation = _protofyCalculation(value); case SassFunction(): - result.compilerFunction = _functions.protofy(value); + result.compilerFunction = + Value_CompilerFunction(id: _functions.getId(value)); + case SassMixin(): + result.compilerMixin = Value_CompilerMixin(id: _mixins.getId(value)); case sassTrue: result.singleton = SingletonValue.TRUE; case sassFalse: @@ -238,9 +247,15 @@ final class Protofier { case Value_Value.hostFunction: return SassFunction(hostCallable( - _dispatcher, _functions, value.hostFunction.signature, + _dispatcher, _functions, _mixins, value.hostFunction.signature, id: value.hostFunction.id)); + case Value_Value.compilerMixin: + var id = value.compilerMixin.id; + if (_mixins[id] case var mixin?) return mixin; + throw paramsError( + "CompilerMixin.id $id doesn't match any known mixins"); + case Value_Value.calculation: return _deprotofyCalculation(value.calculation); diff --git a/lib/src/functions/meta.dart b/lib/src/functions/meta.dart index 41537d55d..a3b683b9a 100644 --- a/lib/src/functions/meta.dart +++ b/lib/src/functions/meta.dart @@ -6,6 +6,7 @@ import 'dart:collection'; import 'package:collection/collection.dart'; +import '../ast/sass/statement/mixin_rule.dart'; import '../callable.dart'; import '../util/map.dart'; import '../value.dart'; @@ -45,6 +46,7 @@ final global = UnmodifiableListView([ sassNull => "null", SassNumber() => "number", SassFunction() => "function", + SassMixin() => "mixin", SassCalculation() => "calculation", SassString() => "string", _ => throw "[BUG] Unknown value type ${arguments[0]}" @@ -77,6 +79,17 @@ final local = UnmodifiableListView([ ? argument : SassString(argument.toString(), quotes: false)), ListSeparator.comma); + }), + _function("accepts-content", r"$mixin", (arguments) { + var mixin = arguments[0].assertMixin("mixin"); + return SassBoolean(switch (mixin.callable) { + AsyncBuiltInCallable(acceptsContent: var acceptsContent) || + BuiltInCallable(acceptsContent: var acceptsContent) => + acceptsContent, + UserDefinedCallable(declaration: MixinRule(hasContent: var hasContent)) => + hasContent, + _ => throw UnsupportedError("Unknown callable type $mixin.") + }); }) ]); diff --git a/lib/src/js.dart b/lib/src/js.dart index cd1480719..9a2c51c06 100644 --- a/lib/src/js.dart +++ b/lib/src/js.dart @@ -32,6 +32,7 @@ void main() { exports.CalculationInterpolation = calculationInterpolationClass; exports.SassColor = colorClass; exports.SassFunction = functionClass; + exports.SassMixin = mixinClass; exports.SassList = listClass; exports.SassMap = mapClass; exports.SassNumber = numberClass; diff --git a/lib/src/js/exports.dart b/lib/src/js/exports.dart index 0dff13698..ee5a74471 100644 --- a/lib/src/js/exports.dart +++ b/lib/src/js/exports.dart @@ -32,6 +32,7 @@ class Exports { external set SassBoolean(JSClass function); external set SassColor(JSClass function); external set SassFunction(JSClass function); + external set SassMixin(JSClass mixin); external set SassList(JSClass function); external set SassMap(JSClass function); external set SassNumber(JSClass function); diff --git a/lib/src/js/value.dart b/lib/src/js/value.dart index f8697efea..c57621c45 100644 --- a/lib/src/js/value.dart +++ b/lib/src/js/value.dart @@ -15,6 +15,7 @@ export 'value/color.dart'; export 'value/function.dart'; export 'value/list.dart'; export 'value/map.dart'; +export 'value/mixin.dart'; export 'value/number.dart'; export 'value/string.dart'; @@ -42,6 +43,7 @@ final JSClass valueClass = () { 'assertColor': (Value self, [String? name]) => self.assertColor(name), 'assertFunction': (Value self, [String? name]) => self.assertFunction(name), 'assertMap': (Value self, [String? name]) => self.assertMap(name), + 'assertMixin': (Value self, [String? name]) => self.assertMixin(name), 'assertNumber': (Value self, [String? name]) => self.assertNumber(name), 'assertString': (Value self, [String? name]) => self.assertString(name), 'tryMap': (Value self) => self.tryMap(), diff --git a/lib/src/js/value/mixin.dart b/lib/src/js/value/mixin.dart new file mode 100644 index 000000000..a41b394d2 --- /dev/null +++ b/lib/src/js/value/mixin.dart @@ -0,0 +1,22 @@ +// Copyright 2021 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:node_interop/js.dart'; + +import '../../callable.dart'; +import '../../value.dart'; +import '../reflection.dart'; +import '../utils.dart'; + +/// The JavaScript `SassMixin` class. +final JSClass mixinClass = () { + var jsClass = createJSClass('sass.SassMixin', (Object self) { + jsThrow(JsError( + 'It is not possible to construct a SassMixin through the JavaScript API')); + }); + + getJSClass(SassMixin(Callable('f', '', (_) => sassNull))) + .injectSuperclass(jsClass); + return jsClass; +}(); diff --git a/lib/src/value.dart b/lib/src/value.dart index 4b21e2434..3149435a0 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -15,6 +15,7 @@ import 'value/color.dart'; import 'value/function.dart'; import 'value/list.dart'; import 'value/map.dart'; +import 'value/mixin.dart'; import 'value/number.dart'; import 'value/string.dart'; import 'visitor/interface/value.dart'; @@ -27,6 +28,7 @@ export 'value/color.dart'; export 'value/function.dart'; export 'value/list.dart'; export 'value/map.dart'; +export 'value/mixin.dart'; export 'value/null.dart'; export 'value/number.dart' hide conversionFactor; export 'value/string.dart'; @@ -177,6 +179,13 @@ abstract class Value { SassFunction assertFunction([String? name]) => throw SassScriptException("$this is not a function reference.", name); + /// Throws a [SassScriptException] if [this] isn't a mixin reference. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. + SassMixin assertMixin([String? name]) => + throw SassScriptException("$this is not a mixin reference.", name); + /// Throws a [SassScriptException] if [this] isn't a map. /// /// If this came from a function argument, [name] is the argument name diff --git a/lib/src/value/mixin.dart b/lib/src/value/mixin.dart new file mode 100644 index 000000000..79091579d --- /dev/null +++ b/lib/src/value/mixin.dart @@ -0,0 +1,40 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:meta/meta.dart'; + +import '../callable.dart'; +import '../visitor/interface/value.dart'; +import '../value.dart'; + +/// A SassScript mixin reference. +/// +/// A mixin reference captures a mixin from the local environment so that +/// it may be passed between modules. +/// +/// {@category Value} +final class SassMixin extends Value { + /// The callable that this mixin invokes. + /// + /// Note that this is typed as an [AsyncCallable] so that it will work with + /// both synchronous and asynchronous evaluate visitors, but in practice the + /// synchronous evaluate visitor will crash if this isn't a [Callable]. + /// + /// @nodoc + @internal + final AsyncCallable callable; + + SassMixin(this.callable); + + /// @nodoc + @internal + T accept(ValueVisitor visitor) => visitor.visitMixin(this); + + SassMixin assertMixin([String? name]) => this; + + bool operator ==(Object other) => + other is SassMixin && callable == other.callable; + + int get hashCode => callable.hashCode; +} diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 13258ed04..389c6991a 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -418,6 +418,19 @@ final class _EvaluateVisitor }); }, url: "sass:meta"), + BuiltInCallable.function("module-mixins", r"$module", (arguments) { + var namespace = arguments[0].assertString("module"); + var module = _environment.modules[namespace.text]; + if (module == null) { + throw 'There is no module with namespace "${namespace.text}".'; + } + + return SassMap({ + for (var (name, value) in module.mixins.pairs) + SassString(name): SassMixin(value) + }); + }, url: "sass:meta"), + BuiltInCallable.function( "get-function", r"$name, $css: false, $module: null", (arguments) { var name = arguments[0].assertString("name"); @@ -444,6 +457,20 @@ final class _EvaluateVisitor return SassFunction(callable); }, url: "sass:meta"), + BuiltInCallable.function("get-mixin", r"$name, $module: null", + (arguments) { + var name = arguments[0].assertString("name"); + var module = arguments[1].realNull?.assertString("module"); + + var callable = _addExceptionSpan( + _callableNode!, + () => _environment.getMixin(name.text.replaceAll("_", "-"), + namespace: module?.text)); + if (callable == null) throw "Mixin not found: $name"; + + return SassMixin(callable); + }, url: "sass:meta"), + AsyncBuiltInCallable.function("call", r"$function, $args...", (arguments) async { var function = arguments[0]; @@ -517,7 +544,32 @@ final class _EvaluateVisitor configuration: configuration, namesInErrors: true); _assertConfigurationIsEmpty(configuration, nameInError: true); - }, url: "sass:meta") + }, url: "sass:meta"), + BuiltInCallable.mixin("apply", r"$mixin, $args...", (arguments) async { + var mixin = arguments[0]; + var args = arguments[1] as SassArgumentList; + + var callableNode = _callableNode!; + var invocation = ArgumentInvocation( + const [], + const {}, + callableNode.span, + rest: ValueExpression(args, callableNode.span), + ); + + var callable = mixin.assertMixin("mixin").callable; + var content = _environment.content; + + // ignore: unnecessary_type_check + if (callable is AsyncCallable) { + await _applyMixin( + callable, content, invocation, callableNode, callableNode); + } else { + throw SassScriptException( + "The mixin ${callable.name} is asynchronous.\n" + "This is probably caused by a bug in a Sass plugin."); + } + }, url: "sass:meta", acceptsContent: true), ]; var metaModule = BuiltInModule("meta", @@ -1733,41 +1785,57 @@ final class _EvaluateVisitor } } - Future visitIncludeRule(IncludeRule node) async { - var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent); - var mixin = _addExceptionSpan(node, - () => _environment.getMixin(node.name, namespace: node.namespace)); + /// Evaluate a given [mixin] with [arguments] and [contentCallable] + Future _applyMixin( + AsyncCallable? mixin, + UserDefinedCallable? contentCallable, + ArgumentInvocation arguments, + AstNode nodeWithSpan, + AstNode nodeWithSpanWithoutContent) async { switch (mixin) { case null: - throw _exception("Undefined mixin.", node.span); - - case AsyncBuiltInCallable() when node.content != null: - throw _exception("Mixin doesn't accept a content block.", node.span); - + throw _exception("Undefined mixin.", nodeWithSpan.span); + + case AsyncBuiltInCallable(acceptsContent: false) + when contentCallable != null: + { + var evaluated = await _evaluateArguments(arguments); + var (overload, _) = mixin.callbackFor( + evaluated.positional.length, MapKeySet(evaluated.named)); + throw MultiSpanSassRuntimeException( + "Mixin doesn't accept a content block.", + nodeWithSpanWithoutContent.span, + "invocation", + {overload.spanWithName: "declaration"}, + _stackTrace(nodeWithSpanWithoutContent.span)); + } case AsyncBuiltInCallable(): - await _runBuiltInCallable(node.arguments, mixin, nodeWithSpan); + await _environment.withContent(contentCallable, () async { + await _environment.asMixin(() async { + await _runBuiltInCallable( + arguments, mixin, nodeWithSpanWithoutContent); + }); + }); case UserDefinedCallable( declaration: MixinRule(hasContent: false) ) - when node.content != null: + when contentCallable != null: throw MultiSpanSassRuntimeException( "Mixin doesn't accept a content block.", - node.spanWithoutContent, + nodeWithSpanWithoutContent.span, "invocation", {mixin.declaration.arguments.spanWithName: "declaration"}, - _stackTrace(node.spanWithoutContent)); + _stackTrace(nodeWithSpanWithoutContent.span)); case UserDefinedCallable(): - var contentCallable = node.content.andThen((content) => - UserDefinedCallable(content, _environment.closure(), - inDependency: _inDependency)); - await _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, - () async { + await _runUserDefinedCallable( + arguments, mixin, nodeWithSpanWithoutContent, () async { await _environment.withContent(contentCallable, () async { await _environment.asMixin(() async { for (var statement in mixin.declaration.children) { - await _addErrorSpan(nodeWithSpan, () => statement.accept(this)); + await _addErrorSpan( + nodeWithSpanWithoutContent, () => statement.accept(this)); } }); }); @@ -1776,6 +1844,20 @@ final class _EvaluateVisitor case _: throw UnsupportedError("Unknown callable type $mixin."); } + } + + Future visitIncludeRule(IncludeRule node) async { + var mixin = _addExceptionSpan(node, + () => _environment.getMixin(node.name, namespace: node.namespace)); + var contentCallable = node.content.andThen((content) => UserDefinedCallable( + content, _environment.closure(), + inDependency: _inDependency)); + + var nodeWithSpanWithoutContent = + AstNode.fake(() => node.spanWithoutContent); + + await _applyMixin(mixin, contentCallable, node.arguments, node, + nodeWithSpanWithoutContent); return null; } diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index de45a4c56..0aaecec85 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 1c3027293ac9cb8a0d03b18c9ca447d62c2733d7 +// Checksum: 358960b72c6e4f48d3e2e9d52be3abbe9e8b5a9f // // ignore_for_file: unused_import @@ -426,6 +426,19 @@ final class _EvaluateVisitor }); }, url: "sass:meta"), + BuiltInCallable.function("module-mixins", r"$module", (arguments) { + var namespace = arguments[0].assertString("module"); + var module = _environment.modules[namespace.text]; + if (module == null) { + throw 'There is no module with namespace "${namespace.text}".'; + } + + return SassMap({ + for (var (name, value) in module.mixins.pairs) + SassString(name): SassMixin(value) + }); + }, url: "sass:meta"), + BuiltInCallable.function( "get-function", r"$name, $css: false, $module: null", (arguments) { var name = arguments[0].assertString("name"); @@ -452,6 +465,20 @@ final class _EvaluateVisitor return SassFunction(callable); }, url: "sass:meta"), + BuiltInCallable.function("get-mixin", r"$name, $module: null", + (arguments) { + var name = arguments[0].assertString("name"); + var module = arguments[1].realNull?.assertString("module"); + + var callable = _addExceptionSpan( + _callableNode!, + () => _environment.getMixin(name.text.replaceAll("_", "-"), + namespace: module?.text)); + if (callable == null) throw "Mixin not found: $name"; + + return SassMixin(callable); + }, url: "sass:meta"), + BuiltInCallable.function("call", r"$function, $args...", (arguments) { var function = arguments[0]; var args = arguments[1] as SassArgumentList; @@ -522,7 +549,32 @@ final class _EvaluateVisitor configuration: configuration, namesInErrors: true); _assertConfigurationIsEmpty(configuration, nameInError: true); - }, url: "sass:meta") + }, url: "sass:meta"), + BuiltInCallable.mixin("apply", r"$mixin, $args...", (arguments) { + var mixin = arguments[0]; + var args = arguments[1] as SassArgumentList; + + var callableNode = _callableNode!; + var invocation = ArgumentInvocation( + const [], + const {}, + callableNode.span, + rest: ValueExpression(args, callableNode.span), + ); + + var callable = mixin.assertMixin("mixin").callable; + var content = _environment.content; + + // ignore: unnecessary_type_check + if (callable is Callable) { + _applyMixin( + callable, content, invocation, callableNode, callableNode); + } else { + throw SassScriptException( + "The mixin ${callable.name} is asynchronous.\n" + "This is probably caused by a bug in a Sass plugin."); + } + }, url: "sass:meta", acceptsContent: true), ]; var metaModule = BuiltInModule("meta", @@ -1730,40 +1782,55 @@ final class _EvaluateVisitor } } - Value? visitIncludeRule(IncludeRule node) { - var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent); - var mixin = _addExceptionSpan(node, - () => _environment.getMixin(node.name, namespace: node.namespace)); + /// Evaluate a given [mixin] with [arguments] and [contentCallable] + void _applyMixin( + Callable? mixin, + UserDefinedCallable? contentCallable, + ArgumentInvocation arguments, + AstNode nodeWithSpan, + AstNode nodeWithSpanWithoutContent) { switch (mixin) { case null: - throw _exception("Undefined mixin.", node.span); - - case BuiltInCallable() when node.content != null: - throw _exception("Mixin doesn't accept a content block.", node.span); + throw _exception("Undefined mixin.", nodeWithSpan.span); + case BuiltInCallable(acceptsContent: false) when contentCallable != null: + { + var evaluated = _evaluateArguments(arguments); + var (overload, _) = mixin.callbackFor( + evaluated.positional.length, MapKeySet(evaluated.named)); + throw MultiSpanSassRuntimeException( + "Mixin doesn't accept a content block.", + nodeWithSpanWithoutContent.span, + "invocation", + {overload.spanWithName: "declaration"}, + _stackTrace(nodeWithSpanWithoutContent.span)); + } case BuiltInCallable(): - _runBuiltInCallable(node.arguments, mixin, nodeWithSpan); + _environment.withContent(contentCallable, () { + _environment.asMixin(() { + _runBuiltInCallable(arguments, mixin, nodeWithSpanWithoutContent); + }); + }); case UserDefinedCallable( declaration: MixinRule(hasContent: false) ) - when node.content != null: + when contentCallable != null: throw MultiSpanSassRuntimeException( "Mixin doesn't accept a content block.", - node.spanWithoutContent, + nodeWithSpanWithoutContent.span, "invocation", {mixin.declaration.arguments.spanWithName: "declaration"}, - _stackTrace(node.spanWithoutContent)); + _stackTrace(nodeWithSpanWithoutContent.span)); case UserDefinedCallable(): - var contentCallable = node.content.andThen((content) => - UserDefinedCallable(content, _environment.closure(), - inDependency: _inDependency)); - _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, () { + _runUserDefinedCallable(arguments, mixin, nodeWithSpanWithoutContent, + () { _environment.withContent(contentCallable, () { _environment.asMixin(() { for (var statement in mixin.declaration.children) { - _addErrorSpan(nodeWithSpan, () => statement.accept(this)); + _addErrorSpan( + nodeWithSpanWithoutContent, () => statement.accept(this)); } }); }); @@ -1772,6 +1839,20 @@ final class _EvaluateVisitor case _: throw UnsupportedError("Unknown callable type $mixin."); } + } + + Value? visitIncludeRule(IncludeRule node) { + var mixin = _addExceptionSpan(node, + () => _environment.getMixin(node.name, namespace: node.namespace)); + var contentCallable = node.content.andThen((content) => UserDefinedCallable( + content, _environment.closure(), + inDependency: _inDependency)); + + var nodeWithSpanWithoutContent = + AstNode.fake(() => node.spanWithoutContent); + + _applyMixin(mixin, contentCallable, node.arguments, node, + nodeWithSpanWithoutContent); return null; } diff --git a/lib/src/visitor/interface/value.dart b/lib/src/visitor/interface/value.dart index db25c86d5..e25f5ba11 100644 --- a/lib/src/visitor/interface/value.dart +++ b/lib/src/visitor/interface/value.dart @@ -12,6 +12,7 @@ abstract interface class ValueVisitor { T visitCalculation(SassCalculation value); T visitColor(SassColor value); T visitFunction(SassFunction value); + T visitMixin(SassMixin value); T visitList(SassList value); T visitMap(SassMap value); T visitNull(); diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index f86d6c7fb..7073c4b4f 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -671,6 +671,16 @@ final class _SerializeVisitor _buffer.writeCharCode($rparen); } + void visitMixin(SassMixin mixin) { + if (!_inspect) { + throw SassScriptException("$mixin isn't a valid CSS value."); + } + + _buffer.write("get-mixin("); + _visitQuotedString(mixin.callable.name); + _buffer.writeCharCode($rparen); + } + void visitList(SassList value) { if (value.hasBrackets) { _buffer.writeCharCode($lbracket); diff --git a/tool/grind.dart b/tool/grind.dart index ce65138df..425730c03 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -61,6 +61,7 @@ void main(List args) { 'SassFunction', 'SassList', 'SassMap', + 'SassMixin', 'SassNumber', 'SassString', 'Value',