Skip to content

Commit ce545c2

Browse files
connorskeesnex3
andauthoredOct 5, 2023
Implement first class mixins (#2073)
Co-authored-by: Natalie Weizenbaum <nweiz@google.com>
1 parent 310904e commit ce545c2

20 files changed

+396
-91
lines changed
 

‎CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
## 1.69.0
22

3+
* Add a `meta.get-mixin()` function that returns a mixin as a first-class Sass
4+
value.
5+
6+
* Add a `meta.apply()` mixin that includes a mixin value.
7+
8+
* Add a `meta.module-mixins()` function which returns a map from mixin names in
9+
a module to the first-class mixins that belong to those names.
10+
11+
* Add a `meta.accepts-content()` function which returns whether or not a mixin
12+
value can take a content block.
13+
314
* Add support for the relative color syntax from CSS Color 5. This syntax
415
cannot be used to create Sass color values. It is always emitted as-is in the
516
CSS output.

‎lib/src/callable/async_built_in.dart

+8-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ class AsyncBuiltInCallable implements AsyncCallable {
2626
/// The callback to run when executing this callable.
2727
final Callback _callback;
2828

29+
/// Whether this callable could potentially accept an `@content` block.
30+
///
31+
/// This can only be true for mixins.
32+
final bool acceptsContent;
33+
2934
/// Creates a function with a single [arguments] declaration and a single
3035
/// [callback].
3136
///
@@ -52,7 +57,7 @@ class AsyncBuiltInCallable implements AsyncCallable {
5257
/// defined.
5358
AsyncBuiltInCallable.mixin(String name, String arguments,
5459
FutureOr<void> callback(List<Value> arguments),
55-
{Object? url})
60+
{Object? url, bool acceptsContent = false})
5661
: this.parsed(name,
5762
ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url),
5863
(arguments) async {
@@ -66,7 +71,8 @@ class AsyncBuiltInCallable implements AsyncCallable {
6671

6772
/// Creates a callable with a single [arguments] declaration and a single
6873
/// [callback].
69-
AsyncBuiltInCallable.parsed(this.name, this._arguments, this._callback);
74+
AsyncBuiltInCallable.parsed(this.name, this._arguments, this._callback,
75+
{this.acceptsContent = false});
7076

7177
/// Returns the argument declaration and Dart callback for the given
7278
/// positional and named arguments.

‎lib/src/callable/built_in.dart

+11-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable {
2121
/// The overloads declared for this callable.
2222
final List<(ArgumentDeclaration, Callback)> _overloads;
2323

24+
final bool acceptsContent;
25+
2426
/// Creates a function with a single [arguments] declaration and a single
2527
/// [callback].
2628
///
@@ -48,18 +50,19 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable {
4850
/// defined.
4951
BuiltInCallable.mixin(
5052
String name, String arguments, void callback(List<Value> arguments),
51-
{Object? url})
53+
{Object? url, bool acceptsContent = false})
5254
: this.parsed(name,
5355
ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url),
5456
(arguments) {
5557
callback(arguments);
5658
return sassNull;
57-
});
59+
}, acceptsContent: acceptsContent);
5860

5961
/// Creates a callable with a single [arguments] declaration and a single
6062
/// [callback].
6163
BuiltInCallable.parsed(this.name, ArgumentDeclaration arguments,
62-
Value callback(List<Value> arguments))
64+
Value callback(List<Value> arguments),
65+
{this.acceptsContent = false})
6366
: _overloads = [(arguments, callback)];
6467

6568
/// Creates a function with multiple implementations.
@@ -79,9 +82,10 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable {
7982
ArgumentDeclaration.parse('@function $name($args) {', url: url),
8083
callback
8184
)
82-
];
85+
],
86+
acceptsContent = false;
8387

84-
BuiltInCallable._(this.name, this._overloads);
88+
BuiltInCallable._(this.name, this._overloads, this.acceptsContent);
8589

8690
/// Returns the argument declaration and Dart callback for the given
8791
/// positional and named arguments.
@@ -117,5 +121,6 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable {
117121
}
118122

119123
/// Returns a copy of this callable with the given [name].
120-
BuiltInCallable withName(String name) => BuiltInCallable._(name, _overloads);
124+
BuiltInCallable withName(String name) =>
125+
BuiltInCallable._(name, _overloads, acceptsContent);
121126
}

‎lib/src/embedded/dispatcher.dart

+6-3
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import 'package:path/path.dart' as p;
1212
import 'package:protobuf/protobuf.dart';
1313
import 'package:sass/sass.dart' as sass;
1414

15+
import '../value/function.dart';
16+
import '../value/mixin.dart';
1517
import 'embedded_sass.pb.dart';
16-
import 'function_registry.dart';
18+
import 'opaque_registry.dart';
1719
import 'host_callable.dart';
1820
import 'importer/file.dart';
1921
import 'importer/host.dart';
@@ -109,7 +111,8 @@ final class Dispatcher {
109111

110112
OutboundMessage_CompileResponse _compile(
111113
InboundMessage_CompileRequest request) {
112-
var functions = FunctionRegistry();
114+
var functions = OpaqueRegistry<SassFunction>();
115+
var mixins = OpaqueRegistry<SassMixin>();
113116

114117
var style = request.style == OutputStyle.COMPRESSED
115118
? sass.OutputStyle.compressed
@@ -123,7 +126,7 @@ final class Dispatcher {
123126
(throw mandatoryError("Importer.importer")));
124127

125128
var globalFunctions = request.globalFunctions
126-
.map((signature) => hostCallable(this, functions, signature));
129+
.map((signature) => hostCallable(this, functions, mixins, signature));
127130

128131
late sass.CompileResult result;
129132
switch (request.whichInput()) {

‎lib/src/embedded/function_registry.dart

-33
This file was deleted.

‎lib/src/embedded/host_callable.dart

+8-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import '../callable.dart';
66
import '../exception.dart';
7+
import '../value/function.dart';
8+
import '../value/mixin.dart';
79
import 'dispatcher.dart';
810
import 'embedded_sass.pb.dart';
9-
import 'function_registry.dart';
11+
import 'opaque_registry.dart';
1012
import 'protofier.dart';
1113
import 'utils.dart';
1214

@@ -19,11 +21,14 @@ import 'utils.dart';
1921
///
2022
/// Throws a [SassException] if [signature] is invalid.
2123
Callable hostCallable(
22-
Dispatcher dispatcher, FunctionRegistry functions, String signature,
24+
Dispatcher dispatcher,
25+
OpaqueRegistry<SassFunction> functions,
26+
OpaqueRegistry<SassMixin> mixins,
27+
String signature,
2328
{int? id}) {
2429
late Callable callable;
2530
callable = Callable.fromSignature(signature, (arguments) {
26-
var protofier = Protofier(dispatcher, functions);
31+
var protofier = Protofier(dispatcher, functions, mixins);
2732
var request = OutboundMessage_FunctionCallRequest()
2833
..arguments.addAll(
2934
[for (var argument in arguments) protofier.protofy(argument)]);

‎lib/src/embedded/opaque_registry.dart

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2019 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
/// A registry of some `T` indexed by ID so that the host can invoke
6+
/// them.
7+
final class OpaqueRegistry<T> {
8+
/// Instantiations of `T` that have been sent to the host.
9+
///
10+
/// The values are located at indexes in the list matching their IDs.
11+
final _elementsById = <T>[];
12+
13+
/// A reverse map from elements to their indexes in [_elementsById].
14+
final _idsByElement = <T, int>{};
15+
16+
/// Returns the compiler-side id associated with [element].
17+
int getId(T element) {
18+
var id = _idsByElement.putIfAbsent(element, () {
19+
_elementsById.add(element);
20+
return _elementsById.length - 1;
21+
});
22+
23+
return id;
24+
}
25+
26+
/// Returns the compiler-side element associated with [id].
27+
///
28+
/// If no such element exists, returns `null`.
29+
T? operator [](int id) => _elementsById[id];
30+
}

‎lib/src/embedded/protofier.dart

+20-5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import '../value.dart';
88
import 'dispatcher.dart';
99
import 'embedded_sass.pb.dart' as proto;
1010
import 'embedded_sass.pb.dart' hide Value, ListSeparator, CalculationOperator;
11-
import 'function_registry.dart';
1211
import 'host_callable.dart';
12+
import 'opaque_registry.dart';
1313
import 'utils.dart';
1414

1515
/// A class that converts Sass [Value] objects into [Value] protobufs.
@@ -21,7 +21,10 @@ final class Protofier {
2121
final Dispatcher _dispatcher;
2222

2323
/// The IDs of first-class functions.
24-
final FunctionRegistry _functions;
24+
final OpaqueRegistry<SassFunction> _functions;
25+
26+
/// The IDs of first-class mixins.
27+
final OpaqueRegistry<SassMixin> _mixins;
2528

2629
/// Any argument lists transitively contained in [value].
2730
///
@@ -35,7 +38,10 @@ final class Protofier {
3538
///
3639
/// The [functions] tracks the IDs of first-class functions so that the host
3740
/// can pass them back to the compiler.
38-
Protofier(this._dispatcher, this._functions);
41+
///
42+
/// Similarly, the [mixins] tracks the IDs of first-class mixins so that the
43+
/// host can pass them back to the compiler.
44+
Protofier(this._dispatcher, this._functions, this._mixins);
3945

4046
/// Converts [value] to its protocol buffer representation.
4147
proto.Value protofy(Value value) {
@@ -84,7 +90,10 @@ final class Protofier {
8490
case SassCalculation():
8591
result.calculation = _protofyCalculation(value);
8692
case SassFunction():
87-
result.compilerFunction = _functions.protofy(value);
93+
result.compilerFunction =
94+
Value_CompilerFunction(id: _functions.getId(value));
95+
case SassMixin():
96+
result.compilerMixin = Value_CompilerMixin(id: _mixins.getId(value));
8897
case sassTrue:
8998
result.singleton = SingletonValue.TRUE;
9099
case sassFalse:
@@ -238,9 +247,15 @@ final class Protofier {
238247

239248
case Value_Value.hostFunction:
240249
return SassFunction(hostCallable(
241-
_dispatcher, _functions, value.hostFunction.signature,
250+
_dispatcher, _functions, _mixins, value.hostFunction.signature,
242251
id: value.hostFunction.id));
243252

253+
case Value_Value.compilerMixin:
254+
var id = value.compilerMixin.id;
255+
if (_mixins[id] case var mixin?) return mixin;
256+
throw paramsError(
257+
"CompilerMixin.id $id doesn't match any known mixins");
258+
244259
case Value_Value.calculation:
245260
return _deprotofyCalculation(value.calculation);
246261

‎lib/src/functions/meta.dart

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:collection';
66

77
import 'package:collection/collection.dart';
88

9+
import '../ast/sass/statement/mixin_rule.dart';
910
import '../callable.dart';
1011
import '../util/map.dart';
1112
import '../value.dart';
@@ -45,6 +46,7 @@ final global = UnmodifiableListView([
4546
sassNull => "null",
4647
SassNumber() => "number",
4748
SassFunction() => "function",
49+
SassMixin() => "mixin",
4850
SassCalculation() => "calculation",
4951
SassString() => "string",
5052
_ => throw "[BUG] Unknown value type ${arguments[0]}"
@@ -77,6 +79,17 @@ final local = UnmodifiableListView([
7779
? argument
7880
: SassString(argument.toString(), quotes: false)),
7981
ListSeparator.comma);
82+
}),
83+
_function("accepts-content", r"$mixin", (arguments) {
84+
var mixin = arguments[0].assertMixin("mixin");
85+
return SassBoolean(switch (mixin.callable) {
86+
AsyncBuiltInCallable(acceptsContent: var acceptsContent) ||
87+
BuiltInCallable(acceptsContent: var acceptsContent) =>
88+
acceptsContent,
89+
UserDefinedCallable(declaration: MixinRule(hasContent: var hasContent)) =>
90+
hasContent,
91+
_ => throw UnsupportedError("Unknown callable type $mixin.")
92+
});
8093
})
8194
]);
8295

‎lib/src/js.dart

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ void main() {
3232
exports.CalculationInterpolation = calculationInterpolationClass;
3333
exports.SassColor = colorClass;
3434
exports.SassFunction = functionClass;
35+
exports.SassMixin = mixinClass;
3536
exports.SassList = listClass;
3637
exports.SassMap = mapClass;
3738
exports.SassNumber = numberClass;

‎lib/src/js/exports.dart

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class Exports {
3232
external set SassBoolean(JSClass function);
3333
external set SassColor(JSClass function);
3434
external set SassFunction(JSClass function);
35+
external set SassMixin(JSClass mixin);
3536
external set SassList(JSClass function);
3637
external set SassMap(JSClass function);
3738
external set SassNumber(JSClass function);

‎lib/src/js/value.dart

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export 'value/color.dart';
1515
export 'value/function.dart';
1616
export 'value/list.dart';
1717
export 'value/map.dart';
18+
export 'value/mixin.dart';
1819
export 'value/number.dart';
1920
export 'value/string.dart';
2021

@@ -42,6 +43,7 @@ final JSClass valueClass = () {
4243
'assertColor': (Value self, [String? name]) => self.assertColor(name),
4344
'assertFunction': (Value self, [String? name]) => self.assertFunction(name),
4445
'assertMap': (Value self, [String? name]) => self.assertMap(name),
46+
'assertMixin': (Value self, [String? name]) => self.assertMixin(name),
4547
'assertNumber': (Value self, [String? name]) => self.assertNumber(name),
4648
'assertString': (Value self, [String? name]) => self.assertString(name),
4749
'tryMap': (Value self) => self.tryMap(),

‎lib/src/js/value/mixin.dart

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2021 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'package:node_interop/js.dart';
6+
7+
import '../../callable.dart';
8+
import '../../value.dart';
9+
import '../reflection.dart';
10+
import '../utils.dart';
11+
12+
/// The JavaScript `SassMixin` class.
13+
final JSClass mixinClass = () {
14+
var jsClass = createJSClass('sass.SassMixin', (Object self) {
15+
jsThrow(JsError(
16+
'It is not possible to construct a SassMixin through the JavaScript API'));
17+
});
18+
19+
getJSClass(SassMixin(Callable('f', '', (_) => sassNull)))
20+
.injectSuperclass(jsClass);
21+
return jsClass;
22+
}();

‎lib/src/value.dart

+9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'value/color.dart';
1515
import 'value/function.dart';
1616
import 'value/list.dart';
1717
import 'value/map.dart';
18+
import 'value/mixin.dart';
1819
import 'value/number.dart';
1920
import 'value/string.dart';
2021
import 'visitor/interface/value.dart';
@@ -27,6 +28,7 @@ export 'value/color.dart';
2728
export 'value/function.dart';
2829
export 'value/list.dart';
2930
export 'value/map.dart';
31+
export 'value/mixin.dart';
3032
export 'value/null.dart';
3133
export 'value/number.dart' hide conversionFactor;
3234
export 'value/string.dart';
@@ -177,6 +179,13 @@ abstract class Value {
177179
SassFunction assertFunction([String? name]) =>
178180
throw SassScriptException("$this is not a function reference.", name);
179181

182+
/// Throws a [SassScriptException] if [this] isn't a mixin reference.
183+
///
184+
/// If this came from a function argument, [name] is the argument name
185+
/// (without the `$`). It's used for error reporting.
186+
SassMixin assertMixin([String? name]) =>
187+
throw SassScriptException("$this is not a mixin reference.", name);
188+
180189
/// Throws a [SassScriptException] if [this] isn't a map.
181190
///
182191
/// If this came from a function argument, [name] is the argument name

‎lib/src/value/mixin.dart

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2023 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'package:meta/meta.dart';
6+
7+
import '../callable.dart';
8+
import '../visitor/interface/value.dart';
9+
import '../value.dart';
10+
11+
/// A SassScript mixin reference.
12+
///
13+
/// A mixin reference captures a mixin from the local environment so that
14+
/// it may be passed between modules.
15+
///
16+
/// {@category Value}
17+
final class SassMixin extends Value {
18+
/// The callable that this mixin invokes.
19+
///
20+
/// Note that this is typed as an [AsyncCallable] so that it will work with
21+
/// both synchronous and asynchronous evaluate visitors, but in practice the
22+
/// synchronous evaluate visitor will crash if this isn't a [Callable].
23+
///
24+
/// @nodoc
25+
@internal
26+
final AsyncCallable callable;
27+
28+
SassMixin(this.callable);
29+
30+
/// @nodoc
31+
@internal
32+
T accept<T>(ValueVisitor<T> visitor) => visitor.visitMixin(this);
33+
34+
SassMixin assertMixin([String? name]) => this;
35+
36+
bool operator ==(Object other) =>
37+
other is SassMixin && callable == other.callable;
38+
39+
int get hashCode => callable.hashCode;
40+
}

‎lib/src/visitor/async_evaluate.dart

+102-20
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,19 @@ final class _EvaluateVisitor
418418
});
419419
}, url: "sass:meta"),
420420

421+
BuiltInCallable.function("module-mixins", r"$module", (arguments) {
422+
var namespace = arguments[0].assertString("module");
423+
var module = _environment.modules[namespace.text];
424+
if (module == null) {
425+
throw 'There is no module with namespace "${namespace.text}".';
426+
}
427+
428+
return SassMap({
429+
for (var (name, value) in module.mixins.pairs)
430+
SassString(name): SassMixin(value)
431+
});
432+
}, url: "sass:meta"),
433+
421434
BuiltInCallable.function(
422435
"get-function", r"$name, $css: false, $module: null", (arguments) {
423436
var name = arguments[0].assertString("name");
@@ -444,6 +457,20 @@ final class _EvaluateVisitor
444457
return SassFunction(callable);
445458
}, url: "sass:meta"),
446459

460+
BuiltInCallable.function("get-mixin", r"$name, $module: null",
461+
(arguments) {
462+
var name = arguments[0].assertString("name");
463+
var module = arguments[1].realNull?.assertString("module");
464+
465+
var callable = _addExceptionSpan(
466+
_callableNode!,
467+
() => _environment.getMixin(name.text.replaceAll("_", "-"),
468+
namespace: module?.text));
469+
if (callable == null) throw "Mixin not found: $name";
470+
471+
return SassMixin(callable);
472+
}, url: "sass:meta"),
473+
447474
AsyncBuiltInCallable.function("call", r"$function, $args...",
448475
(arguments) async {
449476
var function = arguments[0];
@@ -517,7 +544,32 @@ final class _EvaluateVisitor
517544
configuration: configuration,
518545
namesInErrors: true);
519546
_assertConfigurationIsEmpty(configuration, nameInError: true);
520-
}, url: "sass:meta")
547+
}, url: "sass:meta"),
548+
BuiltInCallable.mixin("apply", r"$mixin, $args...", (arguments) async {
549+
var mixin = arguments[0];
550+
var args = arguments[1] as SassArgumentList;
551+
552+
var callableNode = _callableNode!;
553+
var invocation = ArgumentInvocation(
554+
const [],
555+
const {},
556+
callableNode.span,
557+
rest: ValueExpression(args, callableNode.span),
558+
);
559+
560+
var callable = mixin.assertMixin("mixin").callable;
561+
var content = _environment.content;
562+
563+
// ignore: unnecessary_type_check
564+
if (callable is AsyncCallable) {
565+
await _applyMixin(
566+
callable, content, invocation, callableNode, callableNode);
567+
} else {
568+
throw SassScriptException(
569+
"The mixin ${callable.name} is asynchronous.\n"
570+
"This is probably caused by a bug in a Sass plugin.");
571+
}
572+
}, url: "sass:meta", acceptsContent: true),
521573
];
522574

523575
var metaModule = BuiltInModule("meta",
@@ -1733,41 +1785,57 @@ final class _EvaluateVisitor
17331785
}
17341786
}
17351787

1736-
Future<Value?> visitIncludeRule(IncludeRule node) async {
1737-
var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent);
1738-
var mixin = _addExceptionSpan(node,
1739-
() => _environment.getMixin(node.name, namespace: node.namespace));
1788+
/// Evaluate a given [mixin] with [arguments] and [contentCallable]
1789+
Future<void> _applyMixin(
1790+
AsyncCallable? mixin,
1791+
UserDefinedCallable<AsyncEnvironment>? contentCallable,
1792+
ArgumentInvocation arguments,
1793+
AstNode nodeWithSpan,
1794+
AstNode nodeWithSpanWithoutContent) async {
17401795
switch (mixin) {
17411796
case null:
1742-
throw _exception("Undefined mixin.", node.span);
1743-
1744-
case AsyncBuiltInCallable() when node.content != null:
1745-
throw _exception("Mixin doesn't accept a content block.", node.span);
1746-
1797+
throw _exception("Undefined mixin.", nodeWithSpan.span);
1798+
1799+
case AsyncBuiltInCallable(acceptsContent: false)
1800+
when contentCallable != null:
1801+
{
1802+
var evaluated = await _evaluateArguments(arguments);
1803+
var (overload, _) = mixin.callbackFor(
1804+
evaluated.positional.length, MapKeySet(evaluated.named));
1805+
throw MultiSpanSassRuntimeException(
1806+
"Mixin doesn't accept a content block.",
1807+
nodeWithSpanWithoutContent.span,
1808+
"invocation",
1809+
{overload.spanWithName: "declaration"},
1810+
_stackTrace(nodeWithSpanWithoutContent.span));
1811+
}
17471812
case AsyncBuiltInCallable():
1748-
await _runBuiltInCallable(node.arguments, mixin, nodeWithSpan);
1813+
await _environment.withContent(contentCallable, () async {
1814+
await _environment.asMixin(() async {
1815+
await _runBuiltInCallable(
1816+
arguments, mixin, nodeWithSpanWithoutContent);
1817+
});
1818+
});
17491819

17501820
case UserDefinedCallable<AsyncEnvironment>(
17511821
declaration: MixinRule(hasContent: false)
17521822
)
1753-
when node.content != null:
1823+
when contentCallable != null:
17541824
throw MultiSpanSassRuntimeException(
17551825
"Mixin doesn't accept a content block.",
1756-
node.spanWithoutContent,
1826+
nodeWithSpanWithoutContent.span,
17571827
"invocation",
17581828
{mixin.declaration.arguments.spanWithName: "declaration"},
1759-
_stackTrace(node.spanWithoutContent));
1829+
_stackTrace(nodeWithSpanWithoutContent.span));
17601830

17611831
case UserDefinedCallable<AsyncEnvironment>():
1762-
var contentCallable = node.content.andThen((content) =>
1763-
UserDefinedCallable(content, _environment.closure(),
1764-
inDependency: _inDependency));
1765-
await _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan,
1766-
() async {
1832+
await _runUserDefinedCallable(
1833+
arguments, mixin, nodeWithSpanWithoutContent, () async {
17671834
await _environment.withContent(contentCallable, () async {
17681835
await _environment.asMixin(() async {
17691836
for (var statement in mixin.declaration.children) {
1770-
await _addErrorSpan(nodeWithSpan, () => statement.accept(this));
1837+
await _addErrorSpan(
1838+
nodeWithSpanWithoutContent, () => statement.accept(this));
17711839
}
17721840
});
17731841
});
@@ -1776,6 +1844,20 @@ final class _EvaluateVisitor
17761844
case _:
17771845
throw UnsupportedError("Unknown callable type $mixin.");
17781846
}
1847+
}
1848+
1849+
Future<Value?> visitIncludeRule(IncludeRule node) async {
1850+
var mixin = _addExceptionSpan(node,
1851+
() => _environment.getMixin(node.name, namespace: node.namespace));
1852+
var contentCallable = node.content.andThen((content) => UserDefinedCallable(
1853+
content, _environment.closure(),
1854+
inDependency: _inDependency));
1855+
1856+
var nodeWithSpanWithoutContent =
1857+
AstNode.fake(() => node.spanWithoutContent);
1858+
1859+
await _applyMixin(mixin, contentCallable, node.arguments, node,
1860+
nodeWithSpanWithoutContent);
17791861

17801862
return null;
17811863
}

‎lib/src/visitor/evaluate.dart

+100-19
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// DO NOT EDIT. This file was generated from async_evaluate.dart.
66
// See tool/grind/synchronize.dart for details.
77
//
8-
// Checksum: 1c3027293ac9cb8a0d03b18c9ca447d62c2733d7
8+
// Checksum: 358960b72c6e4f48d3e2e9d52be3abbe9e8b5a9f
99
//
1010
// ignore_for_file: unused_import
1111

@@ -426,6 +426,19 @@ final class _EvaluateVisitor
426426
});
427427
}, url: "sass:meta"),
428428

429+
BuiltInCallable.function("module-mixins", r"$module", (arguments) {
430+
var namespace = arguments[0].assertString("module");
431+
var module = _environment.modules[namespace.text];
432+
if (module == null) {
433+
throw 'There is no module with namespace "${namespace.text}".';
434+
}
435+
436+
return SassMap({
437+
for (var (name, value) in module.mixins.pairs)
438+
SassString(name): SassMixin(value)
439+
});
440+
}, url: "sass:meta"),
441+
429442
BuiltInCallable.function(
430443
"get-function", r"$name, $css: false, $module: null", (arguments) {
431444
var name = arguments[0].assertString("name");
@@ -452,6 +465,20 @@ final class _EvaluateVisitor
452465
return SassFunction(callable);
453466
}, url: "sass:meta"),
454467

468+
BuiltInCallable.function("get-mixin", r"$name, $module: null",
469+
(arguments) {
470+
var name = arguments[0].assertString("name");
471+
var module = arguments[1].realNull?.assertString("module");
472+
473+
var callable = _addExceptionSpan(
474+
_callableNode!,
475+
() => _environment.getMixin(name.text.replaceAll("_", "-"),
476+
namespace: module?.text));
477+
if (callable == null) throw "Mixin not found: $name";
478+
479+
return SassMixin(callable);
480+
}, url: "sass:meta"),
481+
455482
BuiltInCallable.function("call", r"$function, $args...", (arguments) {
456483
var function = arguments[0];
457484
var args = arguments[1] as SassArgumentList;
@@ -522,7 +549,32 @@ final class _EvaluateVisitor
522549
configuration: configuration,
523550
namesInErrors: true);
524551
_assertConfigurationIsEmpty(configuration, nameInError: true);
525-
}, url: "sass:meta")
552+
}, url: "sass:meta"),
553+
BuiltInCallable.mixin("apply", r"$mixin, $args...", (arguments) {
554+
var mixin = arguments[0];
555+
var args = arguments[1] as SassArgumentList;
556+
557+
var callableNode = _callableNode!;
558+
var invocation = ArgumentInvocation(
559+
const [],
560+
const {},
561+
callableNode.span,
562+
rest: ValueExpression(args, callableNode.span),
563+
);
564+
565+
var callable = mixin.assertMixin("mixin").callable;
566+
var content = _environment.content;
567+
568+
// ignore: unnecessary_type_check
569+
if (callable is Callable) {
570+
_applyMixin(
571+
callable, content, invocation, callableNode, callableNode);
572+
} else {
573+
throw SassScriptException(
574+
"The mixin ${callable.name} is asynchronous.\n"
575+
"This is probably caused by a bug in a Sass plugin.");
576+
}
577+
}, url: "sass:meta", acceptsContent: true),
526578
];
527579

528580
var metaModule = BuiltInModule("meta",
@@ -1730,40 +1782,55 @@ final class _EvaluateVisitor
17301782
}
17311783
}
17321784

1733-
Value? visitIncludeRule(IncludeRule node) {
1734-
var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent);
1735-
var mixin = _addExceptionSpan(node,
1736-
() => _environment.getMixin(node.name, namespace: node.namespace));
1785+
/// Evaluate a given [mixin] with [arguments] and [contentCallable]
1786+
void _applyMixin(
1787+
Callable? mixin,
1788+
UserDefinedCallable<Environment>? contentCallable,
1789+
ArgumentInvocation arguments,
1790+
AstNode nodeWithSpan,
1791+
AstNode nodeWithSpanWithoutContent) {
17371792
switch (mixin) {
17381793
case null:
1739-
throw _exception("Undefined mixin.", node.span);
1740-
1741-
case BuiltInCallable() when node.content != null:
1742-
throw _exception("Mixin doesn't accept a content block.", node.span);
1794+
throw _exception("Undefined mixin.", nodeWithSpan.span);
17431795

1796+
case BuiltInCallable(acceptsContent: false) when contentCallable != null:
1797+
{
1798+
var evaluated = _evaluateArguments(arguments);
1799+
var (overload, _) = mixin.callbackFor(
1800+
evaluated.positional.length, MapKeySet(evaluated.named));
1801+
throw MultiSpanSassRuntimeException(
1802+
"Mixin doesn't accept a content block.",
1803+
nodeWithSpanWithoutContent.span,
1804+
"invocation",
1805+
{overload.spanWithName: "declaration"},
1806+
_stackTrace(nodeWithSpanWithoutContent.span));
1807+
}
17441808
case BuiltInCallable():
1745-
_runBuiltInCallable(node.arguments, mixin, nodeWithSpan);
1809+
_environment.withContent(contentCallable, () {
1810+
_environment.asMixin(() {
1811+
_runBuiltInCallable(arguments, mixin, nodeWithSpanWithoutContent);
1812+
});
1813+
});
17461814

17471815
case UserDefinedCallable<Environment>(
17481816
declaration: MixinRule(hasContent: false)
17491817
)
1750-
when node.content != null:
1818+
when contentCallable != null:
17511819
throw MultiSpanSassRuntimeException(
17521820
"Mixin doesn't accept a content block.",
1753-
node.spanWithoutContent,
1821+
nodeWithSpanWithoutContent.span,
17541822
"invocation",
17551823
{mixin.declaration.arguments.spanWithName: "declaration"},
1756-
_stackTrace(node.spanWithoutContent));
1824+
_stackTrace(nodeWithSpanWithoutContent.span));
17571825

17581826
case UserDefinedCallable<Environment>():
1759-
var contentCallable = node.content.andThen((content) =>
1760-
UserDefinedCallable(content, _environment.closure(),
1761-
inDependency: _inDependency));
1762-
_runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, () {
1827+
_runUserDefinedCallable(arguments, mixin, nodeWithSpanWithoutContent,
1828+
() {
17631829
_environment.withContent(contentCallable, () {
17641830
_environment.asMixin(() {
17651831
for (var statement in mixin.declaration.children) {
1766-
_addErrorSpan(nodeWithSpan, () => statement.accept(this));
1832+
_addErrorSpan(
1833+
nodeWithSpanWithoutContent, () => statement.accept(this));
17671834
}
17681835
});
17691836
});
@@ -1772,6 +1839,20 @@ final class _EvaluateVisitor
17721839
case _:
17731840
throw UnsupportedError("Unknown callable type $mixin.");
17741841
}
1842+
}
1843+
1844+
Value? visitIncludeRule(IncludeRule node) {
1845+
var mixin = _addExceptionSpan(node,
1846+
() => _environment.getMixin(node.name, namespace: node.namespace));
1847+
var contentCallable = node.content.andThen((content) => UserDefinedCallable(
1848+
content, _environment.closure(),
1849+
inDependency: _inDependency));
1850+
1851+
var nodeWithSpanWithoutContent =
1852+
AstNode.fake(() => node.spanWithoutContent);
1853+
1854+
_applyMixin(mixin, contentCallable, node.arguments, node,
1855+
nodeWithSpanWithoutContent);
17751856

17761857
return null;
17771858
}

‎lib/src/visitor/interface/value.dart

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ abstract interface class ValueVisitor<T> {
1212
T visitCalculation(SassCalculation value);
1313
T visitColor(SassColor value);
1414
T visitFunction(SassFunction value);
15+
T visitMixin(SassMixin value);
1516
T visitList(SassList value);
1617
T visitMap(SassMap value);
1718
T visitNull();

‎lib/src/visitor/serialize.dart

+10
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,16 @@ final class _SerializeVisitor
671671
_buffer.writeCharCode($rparen);
672672
}
673673

674+
void visitMixin(SassMixin mixin) {
675+
if (!_inspect) {
676+
throw SassScriptException("$mixin isn't a valid CSS value.");
677+
}
678+
679+
_buffer.write("get-mixin(");
680+
_visitQuotedString(mixin.callable.name);
681+
_buffer.writeCharCode($rparen);
682+
}
683+
674684
void visitList(SassList value) {
675685
if (value.hasBrackets) {
676686
_buffer.writeCharCode($lbracket);

‎tool/grind.dart

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ void main(List<String> args) {
6161
'SassFunction',
6262
'SassList',
6363
'SassMap',
64+
'SassMixin',
6465
'SassNumber',
6566
'SassString',
6667
'Value',

0 commit comments

Comments
 (0)
Please sign in to comment.