Skip to content

Commit ce545c2

Browse files
connorskeesnex3
andauthoredOct 5, 2023
Implement first class mixins (#2073)
Co-authored-by: Natalie Weizenbaum <[email protected]>
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+
}();

0 commit comments

Comments
 (0)
Please sign in to comment.