mirror of
https://github.com/danog/dart-sass.git
synced 2025-01-22 22:02:00 +01:00
Add support for argument lists
This commit is contained in:
parent
5ff4e84b4a
commit
e98484afca
@ -1,5 +1,8 @@
|
||||
## 1.0.0-beta.10
|
||||
|
||||
* Support version 1.0.0-beta.12 of the Sass embedded protocol:
|
||||
* Support `Value.ArgumentList`.
|
||||
|
||||
* Support slash-separated lists.
|
||||
|
||||
## 1.0.0-beta.9
|
||||
|
@ -10,8 +10,8 @@ import 'package:sass/sass.dart' as sass;
|
||||
import 'dispatcher.dart';
|
||||
import 'embedded_sass.pb.dart';
|
||||
import 'function_registry.dart';
|
||||
import 'protofier.dart';
|
||||
import 'utils.dart';
|
||||
import 'value.dart';
|
||||
|
||||
/// Returns a Sass callable that invokes a function defined on the host with the
|
||||
/// given [signature].
|
||||
@ -41,11 +41,11 @@ sass.Callable hostCallable(Dispatcher dispatcher, FunctionRegistry functions,
|
||||
return sass.Callable.function(
|
||||
name, signature.substring(openParen + 1, signature.length - 1),
|
||||
(arguments) {
|
||||
var protofier = Protofier(dispatcher, functions, compilationId);
|
||||
var request = OutboundMessage_FunctionCallRequest()
|
||||
..compilationId = compilationId
|
||||
..arguments.addAll([
|
||||
for (var argument in arguments) protofyValue(functions, argument)
|
||||
]);
|
||||
..arguments.addAll(
|
||||
[for (var argument in arguments) protofier.protofy(argument)]);
|
||||
|
||||
if (id != null) {
|
||||
request.functionId = id;
|
||||
@ -57,8 +57,7 @@ sass.Callable hostCallable(Dispatcher dispatcher, FunctionRegistry functions,
|
||||
try {
|
||||
switch (response.whichResult()) {
|
||||
case InboundMessage_FunctionCallResponse_Result.success:
|
||||
return deprotofyValue(
|
||||
dispatcher, functions, compilationId, response.success);
|
||||
return protofier.deprotofyResponse(response);
|
||||
|
||||
case InboundMessage_FunctionCallResponse_Result.error:
|
||||
throw response.error;
|
||||
|
274
lib/src/protofier.dart
Normal file
274
lib/src/protofier.dart
Normal file
@ -0,0 +1,274 @@
|
||||
// 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 'package:sass/sass.dart' as sass;
|
||||
|
||||
import 'dispatcher.dart';
|
||||
import 'embedded_sass.pb.dart';
|
||||
import 'function_registry.dart';
|
||||
import 'host_callable.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// A class that converts Sass [sass.Value] objects into [Value] protobufs.
|
||||
///
|
||||
/// A given [Protofier] instance is valid only within the scope of a single
|
||||
/// custom function call.
|
||||
class Protofier {
|
||||
/// The dispatcher, for invoking deprotofied [Value_HostFunction]s.
|
||||
final Dispatcher _dispatcher;
|
||||
|
||||
/// The IDs of first-class functions.
|
||||
final FunctionRegistry _functions;
|
||||
|
||||
/// The ID of the current compilation.
|
||||
final int _compilationId;
|
||||
|
||||
/// Any argument lists transitively contained in [value].
|
||||
///
|
||||
/// The IDs of the [Value_ArgumentList] protobufs are always one greater than
|
||||
/// the index of the corresponding list in this array (since 0 is reserved for
|
||||
/// argument lists created by the host).
|
||||
final _argumentLists = <sass.SassArgumentList>[];
|
||||
|
||||
/// Creates a [Protofier] that's valid within the scope of a single custom
|
||||
/// function call.
|
||||
///
|
||||
/// 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, this._compilationId);
|
||||
|
||||
/// Converts [value] to its protocol buffer representation.
|
||||
Value protofy(sass.Value value) {
|
||||
var result = Value();
|
||||
if (value is sass.SassString) {
|
||||
result.string = Value_String()
|
||||
..text = value.text
|
||||
..quoted = value.hasQuotes;
|
||||
} else if (value is sass.SassNumber) {
|
||||
var number = Value_Number()..value = value.value * 1.0;
|
||||
number.numerators.addAll(value.numeratorUnits);
|
||||
number.denominators.addAll(value.denominatorUnits);
|
||||
result.number = number;
|
||||
} else if (value is sass.SassColor) {
|
||||
// TODO(nweiz): If the color is represented as HSL internally, this coerces
|
||||
// it to RGB. Is it worth providing some visibility into its internal
|
||||
// representation so we can serialize without converting?
|
||||
result.rgbColor = Value_RgbColor()
|
||||
..red = value.red
|
||||
..green = value.green
|
||||
..blue = value.blue
|
||||
..alpha = value.alpha * 1.0;
|
||||
} else if (value is sass.SassArgumentList) {
|
||||
_argumentLists.add(value);
|
||||
var argList = Value_ArgumentList()
|
||||
..id = _argumentLists.length
|
||||
..separator = _protofySeparator(value.separator)
|
||||
..contents.addAll([for (var element in value.asList) protofy(element)]);
|
||||
value.keywordsWithoutMarking.forEach((key, value) {
|
||||
argList.keywords[key] = protofy(value);
|
||||
});
|
||||
|
||||
result.argumentList = argList;
|
||||
} else if (value is sass.SassList) {
|
||||
result.list = Value_List()
|
||||
..separator = _protofySeparator(value.separator)
|
||||
..hasBrackets = value.hasBrackets
|
||||
..contents.addAll([for (var element in value.asList) protofy(element)]);
|
||||
} else if (value is sass.SassMap) {
|
||||
var map = Value_Map();
|
||||
value.contents.forEach((key, value) {
|
||||
map.entries.add(Value_Map_Entry()
|
||||
..key = protofy(key)
|
||||
..value = protofy(value));
|
||||
});
|
||||
result.map = map;
|
||||
} else if (value is sass.SassFunction) {
|
||||
result.compilerFunction = _functions.protofy(value);
|
||||
} else if (value == sass.sassTrue) {
|
||||
result.singleton = SingletonValue.TRUE;
|
||||
} else if (value == sass.sassFalse) {
|
||||
result.singleton = SingletonValue.FALSE;
|
||||
} else if (value == sass.sassNull) {
|
||||
result.singleton = SingletonValue.NULL;
|
||||
} else {
|
||||
throw "Unknown Value $value";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Converts [separator] to its protocol buffer representation.
|
||||
ListSeparator _protofySeparator(sass.ListSeparator separator) {
|
||||
switch (separator) {
|
||||
case sass.ListSeparator.comma:
|
||||
return ListSeparator.COMMA;
|
||||
case sass.ListSeparator.space:
|
||||
return ListSeparator.SPACE;
|
||||
case sass.ListSeparator.slash:
|
||||
return ListSeparator.SLASH;
|
||||
case sass.ListSeparator.undecided:
|
||||
return ListSeparator.UNDECIDED;
|
||||
default:
|
||||
throw "Unknown ListSeparator $separator";
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts [response]'s return value to its Sass representation.
|
||||
sass.Value deprotofyResponse(InboundMessage_FunctionCallResponse response) {
|
||||
for (var id in response.accessedArgumentLists) {
|
||||
// Mark the `keywords` field as accessed.
|
||||
_argumentListForId(id).keywords;
|
||||
}
|
||||
|
||||
return _deprotofy(response.success);
|
||||
}
|
||||
|
||||
/// Converts [value] to its Sass representation.
|
||||
sass.Value _deprotofy(Value value) {
|
||||
try {
|
||||
switch (value.whichValue()) {
|
||||
case Value_Value.string:
|
||||
return value.string.text.isEmpty
|
||||
? sass.SassString.empty(quotes: value.string.quoted)
|
||||
: sass.SassString(value.string.text, quotes: value.string.quoted);
|
||||
|
||||
case Value_Value.number:
|
||||
return sass.SassNumber.withUnits(value.number.value,
|
||||
numeratorUnits: value.number.numerators,
|
||||
denominatorUnits: value.number.denominators);
|
||||
|
||||
case Value_Value.rgbColor:
|
||||
return sass.SassColor.rgb(value.rgbColor.red, value.rgbColor.green,
|
||||
value.rgbColor.blue, value.rgbColor.alpha);
|
||||
|
||||
case Value_Value.hslColor:
|
||||
return sass.SassColor.hsl(
|
||||
value.hslColor.hue,
|
||||
value.hslColor.saturation,
|
||||
value.hslColor.lightness,
|
||||
value.hslColor.alpha);
|
||||
|
||||
case Value_Value.argumentList:
|
||||
if (value.argumentList.id != 0) {
|
||||
return _argumentListForId(value.argumentList.id);
|
||||
}
|
||||
|
||||
var separator = _deprotofySeparator(value.argumentList.separator);
|
||||
var length = value.argumentList.contents.length;
|
||||
if (separator == sass.ListSeparator.undecided && length > 1) {
|
||||
throw paramsError(
|
||||
"List $value can't have an undecided separator because it has "
|
||||
"$length elements");
|
||||
}
|
||||
|
||||
return sass.SassArgumentList([
|
||||
for (var element in value.argumentList.contents) _deprotofy(element)
|
||||
], {
|
||||
for (var entry in value.argumentList.keywords.entries)
|
||||
entry.key: _deprotofy(entry.value)
|
||||
}, separator);
|
||||
|
||||
case Value_Value.list:
|
||||
var separator = _deprotofySeparator(value.list.separator);
|
||||
if (value.list.contents.isEmpty) {
|
||||
return sass.SassList.empty(
|
||||
separator: separator, brackets: value.list.hasBrackets);
|
||||
}
|
||||
|
||||
var length = value.list.contents.length;
|
||||
if (separator == sass.ListSeparator.undecided && length > 1) {
|
||||
throw paramsError(
|
||||
"List $value can't have an undecided separator because it has "
|
||||
"$length elements");
|
||||
}
|
||||
|
||||
return sass.SassList([
|
||||
for (var element in value.list.contents) _deprotofy(element)
|
||||
], separator, brackets: value.list.hasBrackets);
|
||||
|
||||
case Value_Value.map:
|
||||
return value.map.entries.isEmpty
|
||||
? const sass.SassMap.empty()
|
||||
: sass.SassMap({
|
||||
for (var entry in value.map.entries)
|
||||
_deprotofy(entry.key): _deprotofy(entry.value)
|
||||
});
|
||||
|
||||
case Value_Value.compilerFunction:
|
||||
var id = value.compilerFunction.id;
|
||||
var function = _functions[id];
|
||||
if (function == null) {
|
||||
throw paramsError(
|
||||
"CompilerFunction.id $id doesn't match any known functions");
|
||||
}
|
||||
|
||||
return function;
|
||||
|
||||
case Value_Value.hostFunction:
|
||||
return sass.SassFunction(hostCallable(_dispatcher, _functions,
|
||||
_compilationId, value.hostFunction.signature,
|
||||
id: value.hostFunction.id));
|
||||
|
||||
case Value_Value.singleton:
|
||||
switch (value.singleton) {
|
||||
case SingletonValue.TRUE:
|
||||
return sass.sassTrue;
|
||||
case SingletonValue.FALSE:
|
||||
return sass.sassFalse;
|
||||
case SingletonValue.NULL:
|
||||
return sass.sassNull;
|
||||
default:
|
||||
throw "Unknown Value.singleton ${value.singleton}";
|
||||
}
|
||||
|
||||
case Value_Value.notSet:
|
||||
throw mandatoryError("Value.value");
|
||||
}
|
||||
} on RangeError catch (error) {
|
||||
var name = error.name;
|
||||
if (name == null || error.start == null || error.end == null) {
|
||||
throw paramsError(error.toString());
|
||||
}
|
||||
|
||||
if (value.whichValue() == Value_Value.rgbColor) {
|
||||
name = 'RgbColor.$name';
|
||||
} else if (value.whichValue() == Value_Value.hslColor) {
|
||||
name = 'HslColor.$name';
|
||||
}
|
||||
|
||||
throw paramsError(
|
||||
'$name must be between ${error.start} and ${error.end}, was '
|
||||
'${error.invalidValue}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the argument list in [_argumentLists] that corresponds to [id].
|
||||
sass.SassArgumentList _argumentListForId(int id) {
|
||||
if (id < 1) {
|
||||
throw paramsError(
|
||||
"Value.ArgumentList.id $id can't be marked as accessed");
|
||||
} else if (id > _argumentLists.length) {
|
||||
throw paramsError(
|
||||
"Value.ArgumentList.id $id doesn't match any known argument "
|
||||
"lists");
|
||||
} else {
|
||||
return _argumentLists[id - 1];
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts [separator] to its Sass representation.
|
||||
sass.ListSeparator _deprotofySeparator(ListSeparator separator) {
|
||||
switch (separator) {
|
||||
case ListSeparator.COMMA:
|
||||
return sass.ListSeparator.comma;
|
||||
case ListSeparator.SPACE:
|
||||
return sass.ListSeparator.space;
|
||||
case ListSeparator.SLASH:
|
||||
return sass.ListSeparator.slash;
|
||||
case ListSeparator.UNDECIDED:
|
||||
return sass.ListSeparator.undecided;
|
||||
default:
|
||||
throw "Unknown separator $separator";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
name: sass_embedded
|
||||
version: 1.0.0-dev
|
||||
version: 1.0.0-beta.10
|
||||
description: An implementation of the Sass embedded protocol using Dart Sass.
|
||||
author: Sass Team
|
||||
homepage: https://github.com/sass/dart-sass-embedded
|
||||
@ -14,7 +14,7 @@ dependencies:
|
||||
async: ">=1.13.0 <3.0.0"
|
||||
meta: ^1.1.0
|
||||
protobuf: ^2.0.0
|
||||
sass: ^1.36.0
|
||||
sass: ^1.38.0
|
||||
source_span: ^1.1.0
|
||||
stack_trace: ^1.6.0
|
||||
stream_channel: ">=1.6.0 <3.0.0"
|
||||
|
@ -133,22 +133,74 @@ void main() {
|
||||
await _process.kill();
|
||||
});
|
||||
|
||||
test("from argument lists", () async {
|
||||
_process.inbound.add(compileString("a {b: foo(true, false, null)}",
|
||||
functions: [r"foo($arg, $args...)"]));
|
||||
var request = getFunctionCallRequest(await _process.outbound.next);
|
||||
group("from argument lists", () {
|
||||
test("with no named arguments", () async {
|
||||
_process.inbound.add(compileString("a {b: foo(true, false, null)}",
|
||||
functions: [r"foo($arg, $args...)"]));
|
||||
var request = getFunctionCallRequest(await _process.outbound.next);
|
||||
|
||||
expect(
|
||||
request.arguments,
|
||||
equals([
|
||||
_true,
|
||||
Value()
|
||||
..list = (Value_List()
|
||||
..separator = ListSeparator.COMMA
|
||||
..hasBrackets = false
|
||||
..contents.addAll([_false, _null]))
|
||||
]));
|
||||
await _process.kill();
|
||||
expect(
|
||||
request.arguments,
|
||||
equals([
|
||||
_true,
|
||||
Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..id = 1
|
||||
..separator = ListSeparator.COMMA
|
||||
..contents.addAll([_false, _null]))
|
||||
]));
|
||||
await _process.kill();
|
||||
});
|
||||
|
||||
test("with named arguments", () async {
|
||||
_process.inbound.add(compileString(r"a {b: foo(true, $arg: false)}",
|
||||
functions: [r"foo($args...)"]));
|
||||
var request = getFunctionCallRequest(await _process.outbound.next);
|
||||
|
||||
expect(
|
||||
request.arguments,
|
||||
equals([
|
||||
Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..id = 1
|
||||
..separator = ListSeparator.COMMA
|
||||
..contents.addAll([_true])
|
||||
..keywords.addAll({"arg": _false}))
|
||||
]));
|
||||
await _process.kill();
|
||||
});
|
||||
|
||||
test("throws if named arguments are unused", () async {
|
||||
_process.inbound.add(compileString(r"a {b: foo($arg: false)}",
|
||||
functions: [r"foo($args...)"]));
|
||||
var request = getFunctionCallRequest(await _process.outbound.next);
|
||||
|
||||
_process.inbound.add(InboundMessage()
|
||||
..functionCallResponse = (InboundMessage_FunctionCallResponse()
|
||||
..id = request.id
|
||||
..success = _true));
|
||||
|
||||
var failure = getCompileFailure(await _process.outbound.next);
|
||||
expect(failure.message, equals(r"No argument named $arg."));
|
||||
await _process.kill();
|
||||
});
|
||||
|
||||
test("doesn't throw if named arguments are used", () async {
|
||||
_process.inbound.add(compileString(r"a {b: foo($arg: false)}",
|
||||
functions: [r"foo($args...)"]));
|
||||
var request = getFunctionCallRequest(await _process.outbound.next);
|
||||
|
||||
_process.inbound.add(InboundMessage()
|
||||
..functionCallResponse = (InboundMessage_FunctionCallResponse()
|
||||
..id = request.id
|
||||
..accessedArgumentLists
|
||||
.add(request.arguments.first.argumentList.id)
|
||||
..success = _true));
|
||||
|
||||
await expectLater(_process.outbound,
|
||||
emits(isSuccess(equals("a {\n b: true;\n}"))));
|
||||
await _process.kill();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -646,6 +698,48 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group("an argument list", () {
|
||||
test("that's empty", () async {
|
||||
var list = (await _protofy(r"capture-args()")).argumentList;
|
||||
expect(list.contents, isEmpty);
|
||||
expect(list.keywords, isEmpty);
|
||||
expect(list.separator, equals(ListSeparator.COMMA));
|
||||
});
|
||||
|
||||
test("with arguments", () async {
|
||||
var list =
|
||||
(await _protofy(r"capture-args(true, null, false)")).argumentList;
|
||||
expect(list.contents, [_true, _null, _false]);
|
||||
expect(list.keywords, isEmpty);
|
||||
expect(list.separator, equals(ListSeparator.COMMA));
|
||||
});
|
||||
|
||||
test("with a space separator", () async {
|
||||
var list =
|
||||
(await _protofy(r"capture-args(true null false...)")).argumentList;
|
||||
expect(list.contents, [_true, _null, _false]);
|
||||
expect(list.keywords, isEmpty);
|
||||
expect(list.separator, equals(ListSeparator.SPACE));
|
||||
});
|
||||
|
||||
test("with a slash separator", () async {
|
||||
var list =
|
||||
(await _protofy(r"capture-args(list.slash(true, null, false)...)"))
|
||||
.argumentList;
|
||||
expect(list.contents, [_true, _null, _false]);
|
||||
expect(list.keywords, isEmpty);
|
||||
expect(list.separator, equals(ListSeparator.SLASH));
|
||||
});
|
||||
|
||||
test("with keywords", () async {
|
||||
var list = (await _protofy(r"capture-args($arg1: true, $arg2: false)"))
|
||||
.argumentList;
|
||||
expect(list.contents, isEmpty);
|
||||
expect(list.keywords, equals({"arg1": _true, "arg2": _false}));
|
||||
expect(list.separator, equals(ListSeparator.COMMA));
|
||||
});
|
||||
});
|
||||
|
||||
group("a map", () {
|
||||
test("with no elements", () async {
|
||||
expect((await _protofy("map.remove((1: 2), 1)")).map.entries, isEmpty);
|
||||
@ -1159,6 +1253,71 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group("an argument list", () {
|
||||
test("with no elements", () async {
|
||||
expect(
|
||||
await _roundTrip(Value()
|
||||
..argumentList =
|
||||
(Value_ArgumentList()..separator = ListSeparator.UNDECIDED)),
|
||||
equals(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..id = 1
|
||||
..separator = ListSeparator.UNDECIDED)));
|
||||
});
|
||||
|
||||
test("with comma separator", () async {
|
||||
expect(
|
||||
await _roundTrip(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..contents.addAll([_true, _false, _null])
|
||||
..separator = ListSeparator.COMMA)),
|
||||
equals(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..id = 1
|
||||
..contents.addAll([_true, _false, _null])
|
||||
..separator = ListSeparator.COMMA)));
|
||||
});
|
||||
|
||||
test("with space separator", () async {
|
||||
expect(
|
||||
await _roundTrip(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..contents.addAll([_true, _false, _null])
|
||||
..separator = ListSeparator.SPACE)),
|
||||
equals(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..id = 1
|
||||
..contents.addAll([_true, _false, _null])
|
||||
..separator = ListSeparator.SPACE)));
|
||||
});
|
||||
|
||||
test("with slash separator", () async {
|
||||
expect(
|
||||
await _roundTrip(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..contents.addAll([_true, _false, _null])
|
||||
..separator = ListSeparator.SLASH)),
|
||||
equals(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..id = 1
|
||||
..contents.addAll([_true, _false, _null])
|
||||
..separator = ListSeparator.SLASH)));
|
||||
});
|
||||
|
||||
test("with keywords", () async {
|
||||
expect(
|
||||
await _roundTrip(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..keywords.addAll({"arg1": _true, "arg2": _false})
|
||||
..separator = ListSeparator.COMMA)),
|
||||
equals(Value()
|
||||
..argumentList = (Value_ArgumentList()
|
||||
..id = 1
|
||||
..keywords.addAll({"arg1": _true, "arg2": _false})
|
||||
..separator = ListSeparator.COMMA)));
|
||||
});
|
||||
});
|
||||
|
||||
group("a map", () {
|
||||
group("with no elements", () {
|
||||
_testSerializationAndRoundTrip(Value()..map = Value_Map(), "()",
|
||||
@ -1289,6 +1448,13 @@ void main() {
|
||||
endsWith("can't have an undecided separator because it has 2 "
|
||||
"elements"));
|
||||
});
|
||||
|
||||
test("an arglist with an unknown id", () async {
|
||||
await _expectDeprotofyError(
|
||||
Value()..argumentList = (Value_ArgumentList()..id = 1),
|
||||
equals(
|
||||
"Value.ArgumentList.id 1 doesn't match any known argument lists"));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1300,6 +1466,12 @@ Future<Value> _protofy(String sassScript) async {
|
||||
@use 'sass:list';
|
||||
@use 'sass:map';
|
||||
@use 'sass:math';
|
||||
@use 'sass:meta';
|
||||
|
||||
@function capture-args(\$args...) {
|
||||
\$_: meta.keywords(\$args);
|
||||
@return \$args;
|
||||
}
|
||||
|
||||
\$_: foo(($sassScript));
|
||||
""", functions: [r"foo($arg)"]));
|
||||
|
Loading…
x
Reference in New Issue
Block a user