Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add back support for min/max calculations #1485

Merged
merged 3 commits into from Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,13 @@
## 1.42.0

* `min()` and `max()` expressions are once again parsed as calculations as long
as they contain only syntax that's allowed in calculation expressions. To
avoid the backwards-compatibility issues that were present in 1.40.0, they now
allow unitless numbers to be mixed with numbers with units just like the
global `min()` and `max()` functions. Similarly, `+` and `-` operations within
`min()` and `max()` functions allow unitless numbers to be mixed with numbers
with units.

## 1.41.0

* Calculation values can now be combined with strings using the `+` operator.
Expand Down
180 changes: 14 additions & 166 deletions lib/src/parse/stylesheet.dart
Expand Up @@ -2714,25 +2714,6 @@ abstract class StylesheetParser extends Parser {
..writeCharCode($lparen);
break;

case "min":
case "max":
// min() and max() are parsed as the plain CSS mathematical functions if
// possible, and otherwise are parsed as normal Sass functions.
var beginningOfContents = scanner.state;
if (!scanner.scanChar($lparen)) return null;
whitespace();

var buffer = InterpolationBuffer()
..write(name)
..writeCharCode($lparen);

if (!_tryMinMaxContents(buffer)) {
scanner.state = beginningOfContents;
return null;
}

return StringExpression(buffer.interpolation(scanner.spanFrom(start)));

case "progid":
if (!scanner.scanChar($colon)) return null;
buffer = InterpolationBuffer()
Expand Down Expand Up @@ -2767,11 +2748,7 @@ abstract class StylesheetParser extends Parser {
///
/// Assumes the scanner is positioned immediately before the opening
/// parenthesis of the argument list.
///
/// If [allowMinMax] is `true`, this parses `min()` and `max()` functions as
/// calculations.
CalculationExpression? _tryCalculation(String name, LineScannerState start,
{bool allowMinMax = false}) {
CalculationExpression? _tryCalculation(String name, LineScannerState start) {
assert(scanner.peekChar() == $lparen);
switch (name) {
case "calc":
Expand All @@ -2780,9 +2757,18 @@ abstract class StylesheetParser extends Parser {

case "min":
case "max":
if (!allowMinMax) return null;
return CalculationExpression(
name, _calculationArguments(), scanner.spanFrom(start));
// min() and max() are parsed as calculations if possible, and otherwise
// are parsed as normal Sass functions.
var beforeArguments = scanner.state;
List<Expression> arguments;
try {
arguments = _calculationArguments();
} on FormatException catch (_) {
scanner.state = beforeArguments;
return null;
}

return CalculationExpression(name, arguments, scanner.spanFrom(start));

case "clamp":
var arguments = _calculationArguments(3);
Expand All @@ -2793,144 +2779,6 @@ abstract class StylesheetParser extends Parser {
}
}

/// Consumes the contents of a plain-CSS `min()` or `max()` function into
/// [buffer] if one is available.
///
/// Returns whether this succeeded.
///
/// If [allowComma] is `true` (the default), this allows `CalcValue`
/// productions separated by commas.
bool _tryMinMaxContents(InterpolationBuffer buffer,
{bool allowComma = true}) {
whitespace();

// The number of open parentheses that need to be closed.
while (true) {
var next = scanner.peekChar();
switch (next) {
case $minus:
case $plus:
case $0:
case $1:
case $2:
case $3:
case $4:
case $5:
case $6:
case $7:
case $8:
case $9:
case $dot:
try {
buffer.write(rawText(_number));
} on FormatException catch (_) {
return false;
}
break;

case $hash:
if (scanner.peekChar(1) != $lbrace) return false;
buffer.add(singleInterpolation());
break;

case $c:
case $C:
switch (scanner.peekChar(1)) {
case $a:
case $A:
if (!_tryMinMaxFunction(buffer, "calc")) return false;
break;

case $l:
case $L:
if (!_tryMinMaxFunction(buffer, "clamp")) return false;
break;
}
break;

case $e:
case $E:
if (!_tryMinMaxFunction(buffer, "env")) return false;
break;

case $v:
case $V:
if (!_tryMinMaxFunction(buffer, "var")) return false;
break;

case $lparen:
buffer.writeCharCode(scanner.readChar());
if (!_tryMinMaxContents(buffer, allowComma: false)) return false;
break;

case $m:
case $M:
scanner.readChar();
if (scanIdentChar($i)) {
if (!scanIdentChar($n)) return false;
buffer.write("min(");
} else if (scanIdentChar($a)) {
if (!scanIdentChar($x)) return false;
buffer.write("max(");
} else {
return false;
}
if (!scanner.scanChar($lparen)) return false;

if (!_tryMinMaxContents(buffer)) return false;
break;

default:
return false;
}

whitespace();

next = scanner.peekChar();
switch (next) {
case $rparen:
buffer.writeCharCode(scanner.readChar());
return true;

case $plus:
case $minus:
case $asterisk:
case $slash:
buffer.writeCharCode($space);
buffer.writeCharCode(scanner.readChar());
buffer.writeCharCode($space);
break;

case $comma:
if (!allowComma) return false;
buffer.writeCharCode(scanner.readChar());
buffer.writeCharCode($space);
break;

default:
return false;
}

whitespace();
}
}

/// Consumes a function named [name] containing an
/// `InterpolatedDeclarationValue` if possible, and adds its text to [buffer].
///
/// Returns whether such a function could be consumed.
bool _tryMinMaxFunction(InterpolationBuffer buffer, String name) {
if (!scanIdentifier(name)) return false;
if (!scanner.scanChar($lparen)) return false;
buffer
..write(name)
..writeCharCode($lparen)
..addInterpolation(_interpolatedDeclarationValue(allowEmpty: true))
..writeCharCode($rparen);
if (!scanner.scanChar($rparen)) return false;
return true;
}

/// Consumes and returns arguments for a calculation expression, including the
/// opening and closing parentheses.
///
Expand Down Expand Up @@ -3034,7 +2882,7 @@ abstract class StylesheetParser extends Parser {
if (scanner.peekChar() != $lparen) scanner.error('Expected "(" or ".".');

var lowerCase = ident.toLowerCase();
var calculation = _tryCalculation(lowerCase, start, allowMinMax: true);
var calculation = _tryCalculation(lowerCase, start);
if (calculation != null) {
return calculation;
} else if (lowerCase == "if") {
Expand Down
21 changes: 17 additions & 4 deletions lib/src/value/calculation.dart
Expand Up @@ -69,7 +69,7 @@ class SassCalculation extends Value {
SassNumber? minimum;
for (var arg in args) {
if (arg is! SassNumber ||
(minimum != null && !minimum.hasCompatibleUnits(arg))) {
(minimum != null && !minimum.isComparableTo(arg))) {
minimum = null;
break;
} else if (minimum == null || minimum.greaterThan(arg).isTruthy) {
Expand Down Expand Up @@ -100,7 +100,7 @@ class SassCalculation extends Value {
SassNumber? maximum;
for (var arg in args) {
if (arg is! SassNumber ||
(maximum != null && !maximum.hasCompatibleUnits(arg))) {
(maximum != null && !maximum.isComparableTo(arg))) {
maximum = null;
break;
} else if (maximum == null || maximum.lessThan(arg).isTruthy) {
Expand Down Expand Up @@ -161,15 +161,28 @@ class SassCalculation extends Value {
/// [SassCalculation], an unquoted [SassString], a [CalculationOperation], or
/// a [CalculationInterpolation].
static Object operate(
CalculationOperator operator, Object left, Object right) {
CalculationOperator operator, Object left, Object right) =>
operateInternal(operator, left, right, inMinMax: false);

/// Like operator, but with the internal-only [inMinMax] parameter.
nex3 marked this conversation as resolved.
Show resolved Hide resolved
///
/// If [inMinMax] is `true`, this allows unitless numbers to be added and
/// subtracted with numbers with units, for backwards-compatibility with the
/// old global `min()` and `max()` functions.
@internal
static Object operateInternal(
CalculationOperator operator, Object left, Object right,
{required bool inMinMax}) {
left = _simplify(left);
right = _simplify(right);

if (operator == CalculationOperator.plus ||
operator == CalculationOperator.minus) {
if (left is SassNumber &&
right is SassNumber &&
left.hasCompatibleUnits(right)) {
(inMinMax
? left.isComparableTo(right)
: left.hasCompatibleUnits(right))) {
return operator == CalculationOperator.plus
? left.plus(right)
: left.minus(right);
Expand Down
19 changes: 13 additions & 6 deletions lib/src/visitor/async_evaluate.dart
Expand Up @@ -2222,7 +2222,8 @@ class _EvaluateVisitor
Future<Value> visitCalculationExpression(CalculationExpression node) async {
var arguments = [
for (var argument in node.arguments)
await _visitCalculationValue(argument)
await _visitCalculationValue(argument,
inMinMax: node.name == 'min' || node.name == 'max')
];

try {
Expand Down Expand Up @@ -2289,19 +2290,25 @@ class _EvaluateVisitor
}

/// Evaluates [node] as a component of a calculation.
Future<Object> _visitCalculationValue(Expression node) async {
///
/// If [inMinMax] is `true`, this allows unitless numbers to be added and
/// subtracted with numbers with units, for backwards-compatibility with the
/// old global `min()` and `max()` functions.
Future<Object> _visitCalculationValue(Expression node,
{required bool inMinMax}) async {
if (node is ParenthesizedExpression) {
return await _visitCalculationValue(node.expression);
return await _visitCalculationValue(node.expression, inMinMax: inMinMax);
} else if (node is StringExpression) {
assert(!node.hasQuotes);
return CalculationInterpolation(await _performInterpolation(node.text));
} else if (node is BinaryOperationExpression) {
return await _addExceptionSpanAsync(
node,
() async => SassCalculation.operate(
() async => SassCalculation.operateInternal(
_binaryOperatorToCalculationOperator(node.operator),
await _visitCalculationValue(node.left),
await _visitCalculationValue(node.right)));
await _visitCalculationValue(node.left, inMinMax: inMinMax),
await _visitCalculationValue(node.right, inMinMax: inMinMax),
inMinMax: inMinMax));
} else {
assert(node is NumberExpression ||
node is CalculationExpression ||
Expand Down
21 changes: 14 additions & 7 deletions lib/src/visitor/evaluate.dart
Expand Up @@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: 3bcae71014e2ef5626aae9e5e0a5b49c499a226f
// Checksum: f1dd8f0cf56216c204a866a63e22bec1092c2093
//
// ignore_for_file: unused_import

Expand Down Expand Up @@ -2210,7 +2210,9 @@ class _EvaluateVisitor

Value visitCalculationExpression(CalculationExpression node) {
var arguments = [
for (var argument in node.arguments) _visitCalculationValue(argument)
for (var argument in node.arguments)
_visitCalculationValue(argument,
inMinMax: node.name == 'min' || node.name == 'max')
];

try {
Expand Down Expand Up @@ -2277,19 +2279,24 @@ class _EvaluateVisitor
}

/// Evaluates [node] as a component of a calculation.
Object _visitCalculationValue(Expression node) {
///
/// If [inMinMax] is `true`, this allows unitless numbers to be added and
/// subtracted with numbers with units, for backwards-compatibility with the
/// old global `min()` and `max()` functions.
Object _visitCalculationValue(Expression node, {required bool inMinMax}) {
if (node is ParenthesizedExpression) {
return _visitCalculationValue(node.expression);
return _visitCalculationValue(node.expression, inMinMax: inMinMax);
} else if (node is StringExpression) {
assert(!node.hasQuotes);
return CalculationInterpolation(_performInterpolation(node.text));
} else if (node is BinaryOperationExpression) {
return _addExceptionSpan(
node,
() => SassCalculation.operate(
() => SassCalculation.operateInternal(
_binaryOperatorToCalculationOperator(node.operator),
_visitCalculationValue(node.left),
_visitCalculationValue(node.right)));
_visitCalculationValue(node.left, inMinMax: inMinMax),
_visitCalculationValue(node.right, inMinMax: inMinMax),
inMinMax: inMinMax));
} else {
assert(node is NumberExpression ||
node is CalculationExpression ||
Expand Down
4 changes: 4 additions & 0 deletions pkg/sass_api/CHANGELOG.md
@@ -1,3 +1,7 @@
## 1.0.0-beta.12

* No user-visible changes.

## 1.0.0-beta.11

* No user-visible changes.
Expand Down
4 changes: 2 additions & 2 deletions pkg/sass_api/pubspec.yaml
Expand Up @@ -2,15 +2,15 @@ name: sass_api
# Note: Every time we add a new Sass AST node, we need to bump the *major*
# version because it's a breaking change for anyone who's implementing the
# visitor interface(s).
version: 1.0.0-beta.11
version: 1.0.0-beta.12
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass

environment:
sdk: '>=2.12.0 <3.0.0'

dependencies:
sass: 1.41.0
sass: 1.42.0

dependency_overrides:
sass: {path: ../..}