Skip to content

Commit

Permalink
Add support for constants in calculations (#1922)
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Mar 23, 2023
1 parent 09a5f09 commit f5a3dea
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 24 deletions.
12 changes: 12 additions & 0 deletions 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.
Expand Down
17 changes: 12 additions & 5 deletions lib/src/parse/stylesheet.dart
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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.");
}
}

Expand Down
6 changes: 3 additions & 3 deletions lib/src/value/calculation.dart
Expand Up @@ -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<Object> arguments) {
return SassCalculation._(name, List.unmodifiable(arguments));
}
static SassCalculation unsimplified(
String name, Iterable<Object> arguments) =>
SassCalculation._(name, List.unmodifiable(arguments));

/// Creates a `calc()` calculation with the given [argument].
///
Expand Down
23 changes: 22 additions & 1 deletion lib/src/visitor/async_evaluate.dart
Expand Up @@ -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,
Expand Down
25 changes: 23 additions & 2 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: 8a55729a9dc5dafe90954738907880052d930898
// Checksum: 06d1dd221c149650242b3e09b3f507125606bf0f
//
// ignore_for_file: unused_import

Expand Down Expand Up @@ -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,
Expand Down
42 changes: 40 additions & 2 deletions lib/src/visitor/serialize.dart
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -760,6 +793,11 @@ class _SerializeVisitor
return;
}

if (!value.value.isFinite) {
visitCalculation(SassCalculation.unsimplified('calc', [value]));
return;
}

_writeNumber(value.value);

if (!_inspect) {
Expand Down
4 changes: 4 additions & 0 deletions pkg/sass_api/CHANGELOG.md
@@ -1,3 +1,7 @@
## 6.1.0

* No user-visible changes.

## 6.0.3

* 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: 6.0.3
version: 6.1.0
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass

environment:
sdk: ">=2.17.0 <3.0.0"

dependencies:
sass: 1.59.3
sass: 1.60.0

dev_dependencies:
dartdoc: ^5.0.0
Expand Down
2 changes: 1 addition & 1 deletion 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

Expand Down
18 changes: 11 additions & 7 deletions test/cli/shared/repl.dart
Expand Up @@ -195,12 +195,14 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> 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();
Expand Down Expand Up @@ -300,13 +302,15 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> 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();
Expand Down
2 changes: 1 addition & 1 deletion test/output_test.dart
Expand Up @@ -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", () {
Expand Down

0 comments on commit f5a3dea

Please sign in to comment.