diff --git a/CHANGELOG.md b/CHANGELOG.md index 4618111fe..2075cc82c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.60.0 + +* Add support for the `pi`, `e`, `infinity`, `-infinity`, and `NaN` constants in + calculations. These will be interpreted as the corresponding numbers. + +* Add support for unknown constants in calculations. These will be interpreted + as unquoted strings. + +* Serialize numbers with value `infinity`, `-infinity`, and `NaN` to `calc()` + expressions rather than CSS-invalid identifiers. Numbers with complex units + still can't be serialized. + ## 1.59.3 * Fix a performance regression introduced in 1.59.0. diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 76ac28360..1ee59febe 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -2984,7 +2984,7 @@ abstract class StylesheetParser extends Parser { /// Parses a single calculation value. Expression _calculationValue() { var next = scanner.peekChar(); - if (next == $plus || next == $minus || next == $dot || isDigit(next)) { + if (next == $plus || next == $dot || isDigit(next)) { return _number(); } else if (next == $dollar) { return _variable(); @@ -3001,13 +3001,14 @@ abstract class StylesheetParser extends Parser { whitespace(); scanner.expectChar($rparen); return ParenthesizedExpression(value, scanner.spanFrom(start)); - } else if (!lookingAtIdentifier()) { - scanner.error("Expected number, variable, function, or calculation."); - } else { + } else if (lookingAtIdentifier()) { var start = scanner.state; var ident = identifier(); if (scanner.scanChar($dot)) return namespacedExpression(ident, start); - if (scanner.peekChar() != $lparen) scanner.error('Expected "(" or ".".'); + if (scanner.peekChar() != $lparen) { + return StringExpression(Interpolation([ident], scanner.spanFrom(start)), + quotes: false); + } var lowerCase = ident.toLowerCase(); var calculation = _tryCalculation(lowerCase, start); @@ -3019,6 +3020,12 @@ abstract class StylesheetParser extends Parser { return FunctionExpression( ident, _argumentInvocation(), scanner.spanFrom(start)); } + } else if (next == $minus) { + // This has to go after [lookingAtIdentifier] because a hyphen can start + // an identifier as well as a number. + return _number(); + } else { + scanner.error("Expected number, variable, function, or calculation."); } } diff --git a/lib/src/value/calculation.dart b/lib/src/value/calculation.dart index d52e5d283..74375b280 100644 --- a/lib/src/value/calculation.dart +++ b/lib/src/value/calculation.dart @@ -38,9 +38,9 @@ class SassCalculation extends Value { /// Creates a new calculation with the given [name] and [arguments] /// that will not be simplified. @internal - static Value unsimplified(String name, Iterable arguments) { - return SassCalculation._(name, List.unmodifiable(arguments)); - } + static SassCalculation unsimplified( + String name, Iterable arguments) => + SassCalculation._(name, List.unmodifiable(arguments)); /// Creates a `calc()` calculation with the given [argument]. /// diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 8160fe3a9..5f44d7a6b 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -2379,7 +2379,28 @@ class _EvaluateVisitor : result; } else if (node is StringExpression) { assert(!node.hasQuotes); - return CalculationInterpolation(await _performInterpolation(node.text)); + var text = node.text.asPlain; + // If there's actual interpolation, create a CalculationInterpolation. + // Otherwise, create an UnquotedString. The main difference is that + // UnquotedStrings don't get extra defensive parentheses. + if (text == null) { + return CalculationInterpolation(await _performInterpolation(node.text)); + } + + switch (text.toLowerCase()) { + case 'pi': + return SassNumber(math.pi); + case 'e': + return SassNumber(math.e); + case 'infinity': + return SassNumber(double.infinity); + case '-infinity': + return SassNumber(double.negativeInfinity); + case 'nan': + return SassNumber(double.nan); + default: + return SassString(text, quotes: false); + } } else if (node is BinaryOperationExpression) { return await _addExceptionSpanAsync( node, diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 6d9c26bb3..a8a064879 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: 8a55729a9dc5dafe90954738907880052d930898 +// Checksum: 06d1dd221c149650242b3e09b3f507125606bf0f // // ignore_for_file: unused_import @@ -2367,7 +2367,28 @@ class _EvaluateVisitor : result; } else if (node is StringExpression) { assert(!node.hasQuotes); - return CalculationInterpolation(_performInterpolation(node.text)); + var text = node.text.asPlain; + // If there's actual interpolation, create a CalculationInterpolation. + // Otherwise, create an UnquotedString. The main difference is that + // UnquotedStrings don't get extra defensive parentheses. + if (text == null) { + return CalculationInterpolation(_performInterpolation(node.text)); + } + + switch (text.toLowerCase()) { + case 'pi': + return SassNumber(math.pi); + case 'e': + return SassNumber(math.e); + case 'infinity': + return SassNumber(double.infinity); + case '-infinity': + return SassNumber(double.negativeInfinity); + case 'nan': + return SassNumber(double.nan); + default: + return SassString(text, quotes: false); + } } else if (node is BinaryOperationExpression) { return _addExceptionSpan( node, diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 562945042..1b8aeefb4 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'dart:typed_data'; import 'package:charcode/charcode.dart'; +import 'package:collection/collection.dart'; import 'package:source_maps/source_maps.dart'; import 'package:string_scanner/string_scanner.dart'; @@ -492,7 +493,35 @@ class _SerializeVisitor } void _writeCalculationValue(Object value) { - if (value is Value) { + if (value is SassNumber && !value.value.isFinite) { + if (value.numeratorUnits.length > 1 || + value.denominatorUnits.isNotEmpty) { + if (!_inspect) { + throw SassScriptException("$value isn't a valid CSS value."); + } + + _writeNumber(value.value); + _buffer.write(value.unitString); + return; + } + + if (value.value == double.infinity) { + _buffer.write('infinity'); + } else if (value.value == double.negativeInfinity) { + _buffer.write('-infinity'); + } else if (value.value.isNaN) { + _buffer.write('NaN'); + } + + var unit = value.numeratorUnits.firstOrNull; + if (unit != null) { + _writeOptionalSpace(); + _buffer.writeCharCode($asterisk); + _writeOptionalSpace(); + _buffer.writeCharCode($1); + _buffer.write(unit); + } + } else if (value is Value) { value.accept(this); } else if (value is CalculationInterpolation) { _buffer.write(value.value); @@ -513,7 +542,11 @@ class _SerializeVisitor var right = value.right; var parenthesizeRight = right is CalculationInterpolation || (right is CalculationOperation && - _parenthesizeCalculationRhs(value.operator, right.operator)); + _parenthesizeCalculationRhs(value.operator, right.operator)) || + (value.operator == CalculationOperator.dividedBy && + right is SassNumber && + !right.value.isFinite && + right.hasUnits); if (parenthesizeRight) _buffer.writeCharCode($lparen); _writeCalculationValue(right); if (parenthesizeRight) _buffer.writeCharCode($rparen); @@ -760,6 +793,11 @@ class _SerializeVisitor return; } + if (!value.value.isFinite) { + visitCalculation(SassCalculation.unsimplified('calc', [value])); + return; + } + _writeNumber(value.value); if (!_inspect) { diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 13b07b4d4..7a48adbe4 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.0 + +* No user-visible changes. + ## 6.0.3 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 9d24dafc6..039c65adf 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ 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: 6.0.3 +version: 6.1.0 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: - sass: 1.59.3 + sass: 1.60.0 dev_dependencies: dartdoc: ^5.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index dd69c476e..d4113c062 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.59.3 +version: 1.60.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass diff --git a/test/cli/shared/repl.dart b/test/cli/shared/repl.dart index 12f33ea2d..eeefe4f42 100644 --- a/test/cli/shared/repl.dart +++ b/test/cli/shared/repl.dart @@ -195,12 +195,14 @@ void sharedTests(Future runSass(Iterable arguments)) { test("a runtime error", () async { var sass = await runSass(["--interactive"]); - sass.stdin.writeln("max(2, 1 + blue)"); + sass.stdin.writeln("@use 'sass:math'"); + sass.stdin.writeln("math.max(2, 1 + blue)"); await expectLater( sass.stdout, emitsInOrder([ - ">> max(2, 1 + blue)", - " ^^^^^^^^", + ">> @use 'sass:math'", + ">> math.max(2, 1 + blue)", + " ^^^^^^^^", 'Error: Undefined operation "1 + blue".' ])); await sass.kill(); @@ -300,13 +302,15 @@ void sharedTests(Future runSass(Iterable arguments)) { group("and colorizes", () { test("an error in the source text", () async { var sass = await runSass(["--interactive", "--color"]); - sass.stdin.writeln("max(2, 1 + blue)"); + sass.stdin.writeln("@use 'sass:math'"); + sass.stdin.writeln("math.max(2, 1 + blue)"); await expectLater( sass.stdout, emitsInOrder([ - ">> max(2, 1 + blue)", - "\u001b[31m\u001b[1F\u001b[10C1 + blue", - " ^^^^^^^^", + ">> @use 'sass:math'", + ">> math.max(2, 1 + blue)", + "\u001b[31m\u001b[1F\u001b[15C1 + blue", + " ^^^^^^^^", '\u001b[0mError: Undefined operation "1 + blue".' ])); await sass.kill(); diff --git a/test/output_test.dart b/test/output_test.dart index ff132d118..0a72f8730 100644 --- a/test/output_test.dart +++ b/test/output_test.dart @@ -92,7 +92,7 @@ void main() { group("for floating-point numbers", () { test("Infinity", () { expect(compileString("a {b: 1e999}"), - equalsIgnoringWhitespace("a { b: Infinity; }")); + equalsIgnoringWhitespace("a { b: calc(infinity); }")); }); test(">= 1e21", () {