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 support for constants in calculations #1922

Merged
merged 5 commits into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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'");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, this change is not required as part of the PR, and it is just a fly-by cleanup, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's required—we actually consider max(2, 1 + blue) to be valid CSS now because max() is a calculation context and blue is an identifier.

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
Original file line number Diff line number Diff line change
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