Skip to content

Commit

Permalink
Add back support for min/max calculations (#1485)
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Sep 20, 2021
1 parent e3370e6 commit 825fa5c
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 185 deletions.
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.1

* Preserve parentheses around `var()` functions in calculations, because they
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 [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);

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,10 +2290,15 @@ 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) {
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 &&
Expand All @@ -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 ||
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: 58efc9d3f1a86c811ca30cfd7dcbeb01a6945d89
// Checksum: 72516268980b2e5ece8c1eb38f024f22e96d5f15
//
// 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,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 &&
Expand All @@ -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 ||
Expand Down
2 changes: 1 addition & 1 deletion pkg/sass_api/pubspec.yaml
Expand Up @@ -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: ../..}
2 changes: 1 addition & 1 deletion 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

Expand Down

0 comments on commit 825fa5c

Please sign in to comment.