Skip to content

Commit

Permalink
Implement first class mixins (#2073)
Browse files Browse the repository at this point in the history
Co-authored-by: Natalie Weizenbaum <nweiz@google.com>
  • Loading branch information
connorskees and nex3 committed Oct 5, 2023
1 parent 310904e commit ce545c2
Show file tree
Hide file tree
Showing 20 changed files with 396 additions and 91 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
10 changes: 8 additions & 2 deletions lib/src/callable/async_built_in.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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].
///
Expand All @@ -52,7 +57,7 @@ class AsyncBuiltInCallable implements AsyncCallable {
/// defined.
AsyncBuiltInCallable.mixin(String name, String arguments,
FutureOr<void> callback(List<Value> arguments),
{Object? url})
{Object? url, bool acceptsContent = false})
: this.parsed(name,
ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url),
(arguments) async {
Expand All @@ -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.
Expand Down
17 changes: 11 additions & 6 deletions lib/src/callable/built_in.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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].
///
Expand Down Expand Up @@ -48,18 +50,19 @@ final class BuiltInCallable implements Callable, AsyncBuiltInCallable {
/// defined.
BuiltInCallable.mixin(
String name, String arguments, void callback(List<Value> 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<Value> arguments))
Value callback(List<Value> arguments),
{this.acceptsContent = false})
: _overloads = [(arguments, callback)];

/// Creates a function with multiple implementations.
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
9 changes: 6 additions & 3 deletions lib/src/embedded/dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -109,7 +111,8 @@ final class Dispatcher {

OutboundMessage_CompileResponse _compile(
InboundMessage_CompileRequest request) {
var functions = FunctionRegistry();
var functions = OpaqueRegistry<SassFunction>();
var mixins = OpaqueRegistry<SassMixin>();

var style = request.style == OutputStyle.COMPRESSED
? sass.OutputStyle.compressed
Expand All @@ -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()) {
Expand Down
33 changes: 0 additions & 33 deletions lib/src/embedded/function_registry.dart

This file was deleted.

11 changes: 8 additions & 3 deletions lib/src/embedded/host_callable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<SassFunction> functions,
OpaqueRegistry<SassMixin> 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)]);
Expand Down
30 changes: 30 additions & 0 deletions lib/src/embedded/opaque_registry.dart
Original file line number Diff line number Diff line change
@@ -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<T> {
/// Instantiations of `T` that have been sent to the host.
///
/// The values are located at indexes in the list matching their IDs.
final _elementsById = <T>[];

/// A reverse map from elements to their indexes in [_elementsById].
final _idsByElement = <T, int>{};

/// 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];
}
25 changes: 20 additions & 5 deletions lib/src/embedded/protofier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -21,7 +21,10 @@ final class Protofier {
final Dispatcher _dispatcher;

/// The IDs of first-class functions.
final FunctionRegistry _functions;
final OpaqueRegistry<SassFunction> _functions;

/// The IDs of first-class mixins.
final OpaqueRegistry<SassMixin> _mixins;

/// Any argument lists transitively contained in [value].
///
Expand All @@ -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) {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);

Expand Down
13 changes: 13 additions & 0 deletions lib/src/functions/meta.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]}"
Expand Down Expand Up @@ -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.")
});
})
]);

Expand Down
1 change: 1 addition & 0 deletions lib/src/js.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions lib/src/js/exports.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions lib/src/js/value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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(),
Expand Down
22 changes: 22 additions & 0 deletions lib/src/js/value/mixin.dart
Original file line number Diff line number Diff line change
@@ -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;
}();
Loading

0 comments on commit ce545c2

Please sign in to comment.