From 825fa5c7df5a738a1414ae428b53956d2f5df123 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 20 Sep 2021 23:58:47 +0000 Subject: [PATCH] Add back support for min/max calculations (#1485) See sass/sass#3142 See sass/sass#3150 --- CHANGELOG.md | 10 ++ lib/src/parse/stylesheet.dart | 180 +++------------------------- lib/src/value/calculation.dart | 21 +++- lib/src/visitor/async_evaluate.dart | 19 ++- lib/src/visitor/evaluate.dart | 21 ++-- pkg/sass_api/pubspec.yaml | 2 +- pubspec.yaml | 2 +- 7 files changed, 70 insertions(+), 185 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 330453dd1..f6f811082 100644 --- a/CHANGELOG.md +++ b/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.1 * Preserve parentheses around `var()` functions in calculations, because they diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 678710fc3..d092618d2 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -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() @@ -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": @@ -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 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); @@ -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. /// @@ -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") { diff --git a/lib/src/value/calculation.dart b/lib/src/value/calculation.dart index b48f75bef..f9aae45ac 100644 --- a/lib/src/value/calculation.dart +++ b/lib/src/value/calculation.dart @@ -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) { @@ -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) { @@ -161,7 +161,18 @@ 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 [operate], but with the internal-only [inMinMax] parameter. + /// + /// 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); @@ -169,7 +180,9 @@ class SassCalculation extends Value { 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); diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 15141b2ef..9f0a8a27a 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -2222,7 +2222,8 @@ class _EvaluateVisitor Future 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 { @@ -2289,10 +2290,15 @@ class _EvaluateVisitor } /// Evaluates [node] as a component of a calculation. - Future _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 _visitCalculationValue(Expression node, + {required bool inMinMax}) async { if (node is ParenthesizedExpression) { var inner = node.expression; - var result = await _visitCalculationValue(inner); + var result = await _visitCalculationValue(inner, inMinMax: inMinMax); return inner is FunctionExpression && inner.name.toLowerCase() == 'var' && result is SassString && @@ -2305,10 +2311,11 @@ class _EvaluateVisitor } 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 || diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 53a15cc07..da2d24482 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 58efc9d3f1a86c811ca30cfd7dcbeb01a6945d89 +// Checksum: 72516268980b2e5ece8c1eb38f024f22e96d5f15 // // ignore_for_file: unused_import @@ -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 { @@ -2277,10 +2279,14 @@ 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) { var inner = node.expression; - var result = _visitCalculationValue(inner); + var result = _visitCalculationValue(inner, inMinMax: inMinMax); return inner is FunctionExpression && inner.name.toLowerCase() == 'var' && result is SassString && @@ -2293,10 +2299,11 @@ class _EvaluateVisitor } 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 || diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index db9d80895..f9fca7d7d 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -10,7 +10,7 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: - sass: 1.41.1 + sass: 1.42.0 dependency_overrides: sass: {path: ../..} diff --git a/pubspec.yaml b/pubspec.yaml index 1005de674..809775c23 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.41.1 +version: 1.42.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass