Deprecate /-as-division and add replacements

Partially addresses #663
This commit is contained in:
Natalie Weizenbaum 2019-06-02 18:29:06 +01:00
parent 742023a877
commit be1a414f07
13 changed files with 338 additions and 105 deletions

View File

@ -1,3 +1,12 @@
## 1.33.0
* Deprecate the use of `/` for division. The new `math.div()` function should be
used instead. See [this page][] for details.
[this page]: https://sass-lang.com/documentation/breaking-changes/slash-div
* Add a `list.slash()` function that returns a slash-separated list.
## 1.32.13
* Use the proper parameter names in error messages about `string.slice`

View File

@ -705,6 +705,25 @@ Object /* SassString | List<Value> */ _parseChannels(
String name, List<String> argumentNames, Value channels) {
if (channels.isVar) return _functionString(name, [channels]);
var originalChannels = channels;
Value? alphaFromSlashList;
if (channels.separator == ListSeparator.slash) {
var list = channels.asList;
if (list.length != 2) {
throw SassScriptException(
"Only 2 slash-separated elements allowed, but ${list.length} "
"${pluralize('was', list.length, plural: 'were')} passed.");
}
channels = list[0];
alphaFromSlashList = list[1];
if (!alphaFromSlashList.isSpecialNumber) {
alphaFromSlashList.assertNumber("alpha");
}
if (list[0].isVar) return _functionString(name, [originalChannels]);
}
var isCommaSeparated = channels.separator == ListSeparator.comma;
var isBracketed = channels.hasBrackets;
if (isCommaSeparated || isBracketed) {
@ -720,18 +739,20 @@ Object /* SassString | List<Value> */ _parseChannels(
var list = channels.asList;
if (list.length > 3) {
throw SassScriptException(
"Only 3 elements allowed, but ${list.length} were passed.");
throw SassScriptException("Only 3 elements allowed, but ${list.length} "
"${pluralize('was', list.length, plural: 'were')} passed.");
} else if (list.length < 3) {
if (list.any((value) => value.isVar) ||
(list.isNotEmpty && _isVarSlash(list.last))) {
return _functionString(name, [channels]);
return _functionString(name, [originalChannels]);
} else {
var argument = argumentNames[list.length];
throw SassScriptException("Missing element $argument.");
}
}
if (alphaFromSlashList != null) return [...list, alphaFromSlashList];
var maybeSlashSeparated = list[2];
if (maybeSlashSeparated is SassNumber) {
var slash = maybeSlashSeparated.asSlash;

View File

@ -20,7 +20,7 @@ final global = UnmodifiableListView([
/// The Sass list module.
final module = BuiltInModule("list", functions: [
_length, _nth, _setNth, _join, _append, _zip, _index, _isBracketed, //
_separator
_separator, _slash
]);
final _length = _function(
@ -61,9 +61,11 @@ final _join = _function(
separator = ListSeparator.space;
} else if (separatorParam.text == "comma") {
separator = ListSeparator.comma;
} else if (separatorParam.text == "slash") {
separator = ListSeparator.slash;
} else {
throw SassScriptException(
'\$separator: Must be "space", "comma", or "auto".');
'\$separator: Must be "space", "comma", "slash", or "auto".');
}
var bracketed = bracketedParam is SassString && bracketedParam.text == 'auto'
@ -89,9 +91,11 @@ final _append =
separator = ListSeparator.space;
} else if (separatorParam.text == "comma") {
separator = ListSeparator.comma;
} else if (separatorParam.text == "slash") {
separator = ListSeparator.slash;
} else {
throw SassScriptException(
'\$separator: Must be "space", "comma", or "auto".');
'\$separator: Must be "space", "comma", "slash", or "auto".');
}
var newList = [...list.asList, value];
@ -121,16 +125,29 @@ final _index = _function("index", r"$list, $value", (arguments) {
return index == -1 ? sassNull : SassNumber(index + 1);
});
final _separator = _function(
"separator",
r"$list",
(arguments) => arguments[0].separator == ListSeparator.comma
? SassString("comma", quotes: false)
: SassString("space", quotes: false));
final _separator = _function("separator", r"$list", (arguments) {
switch (arguments[0].separator) {
case ListSeparator.comma:
return SassString("comma", quotes: false);
case ListSeparator.slash:
return SassString("slash", quotes: false);
default:
return SassString("space", quotes: false);
}
});
final _isBracketed = _function("is-bracketed", r"$list",
(arguments) => SassBoolean(arguments[0].hasBrackets));
final _slash = _function("slash", r"$elements...", (arguments) {
var list = arguments[0].asList;
if (list.length < 2) {
throw SassScriptException("At least two elements are required.");
}
return SassList(list, ListSeparator.slash);
});
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:list`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>

View File

@ -12,6 +12,7 @@ import '../exception.dart';
import '../module/built_in.dart';
import '../util/number.dart';
import '../value.dart';
import '../warn.dart';
/// The global definitions of Sass math functions.
final global = UnmodifiableListView([
@ -25,7 +26,7 @@ final global = UnmodifiableListView([
final module = BuiltInModule("math", functions: [
_abs, _acos, _asin, _atan, _atan2, _ceil, _clamp, _cos, _compatible, //
_floor, _hypot, _isUnitless, _log, _max, _min, _percentage, _pow, //
_randomFunction, _round, _sin, _sqrt, _tan, _unit,
_randomFunction, _round, _sin, _sqrt, _tan, _unit, _div
], variables: {
"e": SassNumber(math.e),
"pi": SassNumber(math.pi),
@ -295,6 +296,18 @@ final _randomFunction = _function("random", r"$limit: null", (arguments) {
return SassNumber(_random.nextInt(limit) + 1);
});
final _div = _function("div", r"$number1, $number2", (arguments) {
var number1 = arguments[0];
var number2 = arguments[1];
if (number1 is! SassNumber || number2 is! SassNumber) {
warn("math.div() will only support number arguments in a future release.\n"
"Use list.slash() instead for a slash separator.");
}
return number1.dividedBy(number2);
});
///
/// Helpers
///

View File

@ -195,27 +195,32 @@ abstract class Value implements ext.Value {
if (list.asList.isEmpty) return null;
var result = <String>[];
if (list.separator == ListSeparator.comma) {
for (var complex in list.asList) {
if (complex is SassString) {
result.add(complex.text);
} else if (complex is SassList &&
complex.separator == ListSeparator.space) {
var string = complex._selectorStringOrNull();
if (string == null) return null;
result.add(string);
} else {
return null;
switch (list.separator) {
case ListSeparator.comma:
for (var complex in list.asList) {
if (complex is SassString) {
result.add(complex.text);
} else if (complex is SassList &&
complex.separator == ListSeparator.space) {
var string = complex._selectorStringOrNull();
if (string == null) return null;
result.add(string);
} else {
return null;
}
}
}
} else {
for (var compound in list.asList) {
if (compound is SassString) {
result.add(compound.text);
} else {
return null;
break;
case ListSeparator.slash:
return null;
default:
for (var compound in list.asList) {
if (compound is SassString) {
result.add(compound.text);
} else {
return null;
}
}
}
break;
}
return result.join(list.separator == ListSeparator.comma ? ', ' : ' ');
}

View File

@ -67,6 +67,9 @@ class ListSeparator {
/// A comma-separated list.
static const comma = ListSeparator._("comma", ",");
/// A slash-separated list.
static const slash = ListSeparator._("slash", "/");
/// A separator that hasn't yet been determined.
///
/// Singleton lists and empty lists don't have separators defined. This means

View File

@ -1135,8 +1135,8 @@ class _EvaluateVisitor
var list = await node.list.accept(this);
var nodeWithSpan = _expressionNode(node.list);
var setVariables = node.variables.length == 1
? (Value value) => _environment.setLocalVariable(
node.variables.first, value.withoutSlash(), nodeWithSpan)
? (Value value) => _environment.setLocalVariable(node.variables.first,
_withoutSlash(value, nodeWithSpan), nodeWithSpan)
: (Value value) =>
_setMultipleVariables(node.variables, value, nodeWithSpan);
return _environment.scope(() {
@ -1156,7 +1156,7 @@ class _EvaluateVisitor
var minLength = math.min(variables.length, list.length);
for (var i = 0; i < minLength; i++) {
_environment.setLocalVariable(
variables[i], list[i].withoutSlash(), nodeWithSpan);
variables[i], _withoutSlash(list[i], nodeWithSpan), nodeWithSpan);
}
for (var i = minLength; i < variables.length; i++) {
_environment.setLocalVariable(variables[i], sassNull, nodeWithSpan);
@ -1346,10 +1346,12 @@ class _EvaluateVisitor
}
}
var variableNodeWithSpan = _expressionNode(variable.expression);
newValues[variable.name] = ConfiguredValue.explicit(
(await variable.expression.accept(this)).withoutSlash(),
_withoutSlash(
await variable.expression.accept(this), variableNodeWithSpan),
variable.span,
_expressionNode(variable.expression));
variableNodeWithSpan);
}
if (configuration is ExplicitConfiguration || configuration.isEmpty) {
@ -1761,8 +1763,8 @@ class _EvaluateVisitor
return queries;
}
Future<Value> visitReturnRule(ReturnRule node) =>
node.expression.accept(this);
Future<Value> visitReturnRule(ReturnRule node) async =>
_withoutSlash(await node.expression.accept(this), node.expression);
Future<Value?> visitSilentComment(SilentComment node) async => null;
@ -1949,7 +1951,8 @@ class _EvaluateVisitor
deprecation: true);
}
var value = (await node.expression.accept(this)).withoutSlash();
var value =
_withoutSlash(await node.expression.accept(this), node.expression);
_addExceptionSpan(node, () {
_environment.setVariable(
node.name, value, _expressionNode(node.expression),
@ -1959,15 +1962,19 @@ class _EvaluateVisitor
}
Future<Value?> visitUseRule(UseRule node) async {
var configuration = node.configuration.isEmpty
? const Configuration.empty()
: ExplicitConfiguration({
for (var variable in node.configuration)
variable.name: ConfiguredValue.explicit(
(await variable.expression.accept(this)).withoutSlash(),
variable.span,
_expressionNode(variable.expression))
}, node);
var configuration = const Configuration.empty();
if (node.configuration.isNotEmpty) {
var values = <String, ConfiguredValue>{};
for (var variable in node.configuration) {
var variableNodeWithSpan = _expressionNode(variable.expression);
values[variable.name] = ConfiguredValue.explicit(
_withoutSlash(
await variable.expression.accept(this), variableNodeWithSpan),
variable.span,
variableNodeWithSpan);
}
configuration = ExplicitConfiguration(values, node);
}
await _loadModule(node.url, "@use", node, (module) {
_environment.addModule(module, node, namespace: node.namespace);
@ -2055,6 +2062,29 @@ class _EvaluateVisitor
if (node.allowsSlash && left is SassNumber && right is SassNumber) {
return (result as SassNumber).withSlash(left, right);
} else {
if (left is SassNumber && right is SassNumber) {
String recommendation(Expression expression) {
if (expression is BinaryOperationExpression &&
expression.operator == BinaryOperator.dividedBy) {
return "math.div(${recommendation(expression.left)}, "
"${recommendation(expression.right)})";
} else {
return expression.toString();
}
}
_warn(
"Using / for division is deprecated and will be removed in "
"Dart Sass 2.0.0.\n"
"\n"
"Recommendation: ${recommendation(node)}\n"
"\n"
"More info and automated migrator: "
"https://sass-lang.com/d/slash-div",
node.span,
deprecation: true);
}
return result;
}
@ -2199,6 +2229,8 @@ class _EvaluateVisitor
UserDefinedCallable<AsyncEnvironment> callable,
AstNode nodeWithSpan,
Future<V> run()) async {
// TODO(nweiz): Set [trackSpans] to `null` once we're no longer emitting
// deprecation warnings for /-as-division.
var evaluated = await _evaluateArguments(arguments);
var name = callable.name;
@ -2216,10 +2248,11 @@ class _EvaluateVisitor
var minLength =
math.min(evaluated.positional.length, declaredArguments.length);
for (var i = 0; i < minLength; i++) {
var nodeForSpan = evaluated.positionalNodes[i];
_environment.setLocalVariable(
declaredArguments[i].name,
evaluated.positional[i].withoutSlash(),
evaluated.positionalNodes[i]);
_withoutSlash(evaluated.positional[i], nodeForSpan),
nodeForSpan);
}
for (var i = evaluated.positional.length;
@ -2228,11 +2261,10 @@ class _EvaluateVisitor
var argument = declaredArguments[i];
var value = evaluated.named.remove(argument.name) ??
await argument.defaultValue!.accept<Future<Value>>(this);
var nodeForSpan = evaluated.namedNodes[argument.name] ??
_expressionNode(argument.defaultValue!);
_environment.setLocalVariable(
argument.name,
value.withoutSlash(),
evaluated.namedNodes[argument.name] ??
_expressionNode(argument.defaultValue!));
argument.name, _withoutSlash(value, nodeForSpan), nodeForSpan);
}
SassArgumentList? argumentList;
@ -2275,11 +2307,12 @@ class _EvaluateVisitor
Future<Value> _runFunctionCallable(ArgumentInvocation arguments,
AsyncCallable? callable, AstNode nodeWithSpan) async {
if (callable is AsyncBuiltInCallable) {
return (await _runBuiltInCallable(arguments, callable, nodeWithSpan))
.withoutSlash();
return _withoutSlash(
await _runBuiltInCallable(arguments, callable, nodeWithSpan),
nodeWithSpan);
} else if (callable is UserDefinedCallable<AsyncEnvironment>) {
return (await _runUserDefinedCallable(arguments, callable, nodeWithSpan,
() async {
return await _runUserDefinedCallable(arguments, callable, nodeWithSpan,
() async {
for (var statement in callable.declaration.children) {
var returnValue = await statement.accept(this);
if (returnValue is Value) return returnValue;
@ -2287,8 +2320,7 @@ class _EvaluateVisitor
throw _exception(
"Function finished without @return.", callable.declaration.span);
}))
.withoutSlash();
});
} else if (callable is PlainCssCallable) {
if (arguments.named.isNotEmpty || arguments.keywordRest != null) {
throw _exception("Plain CSS functions don't support keyword arguments.",
@ -2880,9 +2912,9 @@ class _EvaluateVisitor
/// Returns the [AstNode] whose span should be used for [expression].
///
/// If [expression] is a variable reference, [AstNode]'s span will be the span
/// where that variable was originally declared. Otherwise, this will just
/// return [expression].
/// If [expression] is a variable reference and [_sourceMap] is `true`,
/// [AstNode]'s span will be the span where that variable was originally
/// declared. Otherwise, this will just return [expression].
///
/// This returns an [AstNode] rather than a [FileSpan] so we can avoid calling
/// [AstNode.span] if the span isn't required, since some nodes need to do
@ -2894,8 +2926,10 @@ class _EvaluateVisitor
// are gone we should go back to short-circuiting.
if (expression is VariableExpression) {
return _environment.getVariableNode(expression.name,
namespace: expression.namespace) ??
return _addExceptionSpan(
expression,
() => _environment.getVariableNode(expression.name,
namespace: expression.namespace)) ??
expression;
} else {
return expression;
@ -2994,6 +3028,35 @@ class _EvaluateVisitor
return result;
}
/// Like [Value.withoutSlash], but produces a deprecation warning if [value]
/// was a slash-separated number.
Value _withoutSlash(Value value, AstNode nodeForSpan) {
if (value is SassNumber && value.asSlash != null) {
String recommendation(SassNumber number) {
var asSlash = number.asSlash;
if (asSlash != null) {
return "math.div(${recommendation(asSlash.item1)}, "
"${recommendation(asSlash.item2)})";
} else {
return number.toString();
}
}
_warn(
"Using / for division is deprecated and will be removed in Dart Sass "
"2.0.0.\n"
"\n"
"Recommendation: ${recommendation(value)}\n"
"\n"
"More info and automated migrator: "
"https://sass-lang.com/d/slash-div",
nodeForSpan.span,
deprecation: true);
}
return value.withoutSlash();
}
/// Creates a new stack frame with location information from [member] and
/// [span].
Frame _stackFrame(String member, FileSpan span) => frameForSpan(span, member,

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: 6351ed2d303a58943ce6be39dc794fb46286fd64
// Checksum: 3e8746ad4514a9aff33701f3ca4fa02f3594fb3a
//
// ignore_for_file: unused_import
@ -1139,8 +1139,8 @@ class _EvaluateVisitor
var list = node.list.accept(this);
var nodeWithSpan = _expressionNode(node.list);
var setVariables = node.variables.length == 1
? (Value value) => _environment.setLocalVariable(
node.variables.first, value.withoutSlash(), nodeWithSpan)
? (Value value) => _environment.setLocalVariable(node.variables.first,
_withoutSlash(value, nodeWithSpan), nodeWithSpan)
: (Value value) =>
_setMultipleVariables(node.variables, value, nodeWithSpan);
return _environment.scope(() {
@ -1160,7 +1160,7 @@ class _EvaluateVisitor
var minLength = math.min(variables.length, list.length);
for (var i = 0; i < minLength; i++) {
_environment.setLocalVariable(
variables[i], list[i].withoutSlash(), nodeWithSpan);
variables[i], _withoutSlash(list[i], nodeWithSpan), nodeWithSpan);
}
for (var i = minLength; i < variables.length; i++) {
_environment.setLocalVariable(variables[i], sassNull, nodeWithSpan);
@ -1347,10 +1347,11 @@ class _EvaluateVisitor
}
}
var variableNodeWithSpan = _expressionNode(variable.expression);
newValues[variable.name] = ConfiguredValue.explicit(
variable.expression.accept(this).withoutSlash(),
_withoutSlash(variable.expression.accept(this), variableNodeWithSpan),
variable.span,
_expressionNode(variable.expression));
variableNodeWithSpan);
}
if (configuration is ExplicitConfiguration || configuration.isEmpty) {
@ -1756,7 +1757,8 @@ class _EvaluateVisitor
return queries;
}
Value visitReturnRule(ReturnRule node) => node.expression.accept(this);
Value visitReturnRule(ReturnRule node) =>
_withoutSlash(node.expression.accept(this), node.expression);
Value? visitSilentComment(SilentComment node) => null;
@ -1941,7 +1943,7 @@ class _EvaluateVisitor
deprecation: true);
}
var value = node.expression.accept(this).withoutSlash();
var value = _withoutSlash(node.expression.accept(this), node.expression);
_addExceptionSpan(node, () {
_environment.setVariable(
node.name, value, _expressionNode(node.expression),
@ -1951,15 +1953,19 @@ class _EvaluateVisitor
}
Value? visitUseRule(UseRule node) {
var configuration = node.configuration.isEmpty
? const Configuration.empty()
: ExplicitConfiguration({
for (var variable in node.configuration)
variable.name: ConfiguredValue.explicit(
variable.expression.accept(this).withoutSlash(),
variable.span,
_expressionNode(variable.expression))
}, node);
var configuration = const Configuration.empty();
if (node.configuration.isNotEmpty) {
var values = <String, ConfiguredValue>{};
for (var variable in node.configuration) {
var variableNodeWithSpan = _expressionNode(variable.expression);
values[variable.name] = ConfiguredValue.explicit(
_withoutSlash(
variable.expression.accept(this), variableNodeWithSpan),
variable.span,
variableNodeWithSpan);
}
configuration = ExplicitConfiguration(values, node);
}
_loadModule(node.url, "@use", node, (module) {
_environment.addModule(module, node, namespace: node.namespace);
@ -2046,6 +2052,29 @@ class _EvaluateVisitor
if (node.allowsSlash && left is SassNumber && right is SassNumber) {
return (result as SassNumber).withSlash(left, right);
} else {
if (left is SassNumber && right is SassNumber) {
String recommendation(Expression expression) {
if (expression is BinaryOperationExpression &&
expression.operator == BinaryOperator.dividedBy) {
return "math.div(${recommendation(expression.left)}, "
"${recommendation(expression.right)})";
} else {
return expression.toString();
}
}
_warn(
"Using / for division is deprecated and will be removed in "
"Dart Sass 2.0.0.\n"
"\n"
"Recommendation: ${recommendation(node)}\n"
"\n"
"More info and automated migrator: "
"https://sass-lang.com/d/slash-div",
node.span,
deprecation: true);
}
return result;
}
@ -2186,6 +2215,8 @@ class _EvaluateVisitor
UserDefinedCallable<Environment> callable,
AstNode nodeWithSpan,
V run()) {
// TODO(nweiz): Set [trackSpans] to `null` once we're no longer emitting
// deprecation warnings for /-as-division.
var evaluated = _evaluateArguments(arguments);
var name = callable.name;
@ -2203,10 +2234,11 @@ class _EvaluateVisitor
var minLength =
math.min(evaluated.positional.length, declaredArguments.length);
for (var i = 0; i < minLength; i++) {
var nodeForSpan = evaluated.positionalNodes[i];
_environment.setLocalVariable(
declaredArguments[i].name,
evaluated.positional[i].withoutSlash(),
evaluated.positionalNodes[i]);
_withoutSlash(evaluated.positional[i], nodeForSpan),
nodeForSpan);
}
for (var i = evaluated.positional.length;
@ -2215,11 +2247,10 @@ class _EvaluateVisitor
var argument = declaredArguments[i];
var value = evaluated.named.remove(argument.name) ??
argument.defaultValue!.accept<Value>(this);
var nodeForSpan = evaluated.namedNodes[argument.name] ??
_expressionNode(argument.defaultValue!);
_environment.setLocalVariable(
argument.name,
value.withoutSlash(),
evaluated.namedNodes[argument.name] ??
_expressionNode(argument.defaultValue!));
argument.name, _withoutSlash(value, nodeForSpan), nodeForSpan);
}
SassArgumentList? argumentList;
@ -2262,8 +2293,8 @@ class _EvaluateVisitor
Value _runFunctionCallable(
ArgumentInvocation arguments, Callable? callable, AstNode nodeWithSpan) {
if (callable is BuiltInCallable) {
return _runBuiltInCallable(arguments, callable, nodeWithSpan)
.withoutSlash();
return _withoutSlash(
_runBuiltInCallable(arguments, callable, nodeWithSpan), nodeWithSpan);
} else if (callable is UserDefinedCallable<Environment>) {
return _runUserDefinedCallable(arguments, callable, nodeWithSpan, () {
for (var statement in callable.declaration.children) {
@ -2273,7 +2304,7 @@ class _EvaluateVisitor
throw _exception(
"Function finished without @return.", callable.declaration.span);
}).withoutSlash();
});
} else if (callable is PlainCssCallable) {
if (arguments.named.isNotEmpty || arguments.keywordRest != null) {
throw _exception("Plain CSS functions don't support keyword arguments.",
@ -2857,9 +2888,9 @@ class _EvaluateVisitor
/// Returns the [AstNode] whose span should be used for [expression].
///
/// If [expression] is a variable reference, [AstNode]'s span will be the span
/// where that variable was originally declared. Otherwise, this will just
/// return [expression].
/// If [expression] is a variable reference and [_sourceMap] is `true`,
/// [AstNode]'s span will be the span where that variable was originally
/// declared. Otherwise, this will just return [expression].
///
/// This returns an [AstNode] rather than a [FileSpan] so we can avoid calling
/// [AstNode.span] if the span isn't required, since some nodes need to do
@ -2871,8 +2902,10 @@ class _EvaluateVisitor
// are gone we should go back to short-circuiting.
if (expression is VariableExpression) {
return _environment.getVariableNode(expression.name,
namespace: expression.namespace) ??
return _addExceptionSpan(
expression,
() => _environment.getVariableNode(expression.name,
namespace: expression.namespace)) ??
expression;
} else {
return expression;
@ -2967,6 +3000,35 @@ class _EvaluateVisitor
return result;
}
/// Like [Value.withoutSlash], but produces a deprecation warning if [value]
/// was a slash-separated number.
Value _withoutSlash(Value value, AstNode nodeForSpan) {
if (value is SassNumber && value.asSlash != null) {
String recommendation(SassNumber number) {
var asSlash = number.asSlash;
if (asSlash != null) {
return "math.div(${recommendation(asSlash.item1)}, "
"${recommendation(asSlash.item2)})";
} else {
return number.toString();
}
}
_warn(
"Using / for division is deprecated and will be removed in Dart Sass "
"2.0.0.\n"
"\n"
"Recommendation: ${recommendation(value)}\n"
"\n"
"More info and automated migrator: "
"https://sass-lang.com/d/slash-div",
nodeForSpan.span,
deprecation: true);
}
return value.withoutSlash();
}
/// Creates a new stack frame with location information from [member] and
/// [span].
Frame _stackFrame(String member, FileSpan span) => frameForSpan(span, member,

View File

@ -559,14 +559,15 @@ class _SerializeVisitor
var singleton = _inspect &&
value.asList.length == 1 &&
value.separator == ListSeparator.comma;
(value.separator == ListSeparator.comma ||
value.separator == ListSeparator.slash);
if (singleton && !value.hasBrackets) _buffer.writeCharCode($lparen);
_writeBetween<Value>(
_inspect
? value.asList
: value.asList.where((element) => !element.isBlank),
value.separator == ListSeparator.space ? " " : _commaSeparator,
_separatorString(value.separator),
_inspect
? (element) {
var needsParens = _elementNeedsParens(value.separator, element);
@ -579,22 +580,45 @@ class _SerializeVisitor
});
if (singleton) {
_buffer.writeCharCode($comma);
_buffer.write(value.separator.separator);
if (!value.hasBrackets) _buffer.writeCharCode($rparen);
}
if (value.hasBrackets) _buffer.writeCharCode($rbracket);
}
/// Returns the string to use to separate list items for lists with the given [separator].
String _separatorString(ListSeparator separator) {
switch (separator) {
case ListSeparator.comma:
return _commaSeparator;
case ListSeparator.slash:
return _isCompressed ? "/" : " / ";
case ListSeparator.space:
return " ";
default:
// This should never be used, but it may still be returned since
// [_separatorString] is invoked eagerly by [writeList] even for lists
// with only one elements.
return "";
}
}
/// Returns whether [value] needs parentheses as an element in a list with the
/// given [separator].
bool _elementNeedsParens(ListSeparator separator, Value value) {
if (value is SassList) {
if (value.asList.length < 2) return false;
if (value.hasBrackets) return false;
return separator == ListSeparator.comma
? value.separator == ListSeparator.comma
: value.separator != ListSeparator.undecided;
switch (separator) {
case ListSeparator.comma:
return value.separator == ListSeparator.comma;
case ListSeparator.slash:
return value.separator == ListSeparator.comma ||
value.separator == ListSeparator.slash;
default:
return value.separator != ListSeparator.undecided;
}
}
return false;
}

View File

@ -1,5 +1,5 @@
name: sass
version: 1.32.13-dev
version: 1.33.0
description: A Sass implementation in Dart.
author: Sass Team
homepage: https://github.com/sass/dart-sass

View File

@ -109,6 +109,13 @@ void main() {
expect(_compile("a {b: x, y, z}"), equals("a{b:x,y,z}"));
});
test("don't include spaces around slashes", () {
expect(_compile("""
@use "sass:list";
a {b: list.slash(x, y, z)}
"""), equals("a{b:x/y/z}"));
});
test("do include spaces when space-separated", () {
expect(_compile("a {b: x y z}"), equals("a{b:x y z}"));
});

View File

@ -116,6 +116,11 @@ void main() {
});
});
test("a slash-separated list is space-separated", () {
expect(parseValue("list.slash(a, b, c)").separator,
equals(ListSeparator.slash));
});
test("a space-separated list is space-separated", () {
expect(parseValue("a, b, c").separator, equals(ListSeparator.comma));
});

View File

@ -10,7 +10,11 @@ import 'package:sass/src/exception.dart';
/// Parses [source] by way of a function call.
Value parseValue(String source) {
late Value value;
compileString("a {b: foo(($source))}", functions: [
compileString("""
@use "sass:list";
a {b: foo(($source))}
""", functions: [
Callable("foo", r"$arg", expectAsync1((arguments) {
expect(arguments, hasLength(1));
value = arguments.first;