Calc functions implementation (#1970)

* Sqrt calc function implementation

* Pow calc function

* Calc round function

* Corrections and rename parameter to inLegacySassFunction

* Unary calc functions

* Arguments with no units correction

* Refactor round function

* Update modulo function to return SassNumber and corrections

* Round accepting fake units fix

* Up/Down round strategy fix

* Return incompatible message fix
This commit is contained in:
Pamela Lozano 2023-08-09 21:14:08 +00:00 committed by GitHub
parent 4c3bd0e89f
commit e4c8cd67e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 834 additions and 106 deletions

View File

@ -1,3 +1,14 @@
## 1.65.0
* All functions defined in CSS Values and Units 4 are now parsed as calculation
objects: `round()`, `mod()`, `rem()`, `sin()`, `cos()`, `tan()`, `asin()`,
`acos()`, `atan()`, `atan2()`, `pow()`, `sqrt()`, `hypot()`, `log()`, `exp()`,
`abs()`, and `sign()`.
* Deprecate explicitly passing the `%` unit to the global `abs()` function. In
future releases, this will emit a CSS abs() function to be resolved by the
browser. This deprecation is named `abs-percent`.
## 1.64.3
### Dart API

View File

@ -40,6 +40,10 @@ final class CalculationExpression implements Expression {
}
}
/// Returns a `hypot()` calculation expression.
CalculationExpression.hypot(Iterable<Expression> arguments, FileSpan span)
: this("hypot", arguments, span);
/// Returns a `max()` calculation expression.
CalculationExpression.max(Iterable<Expression> arguments, this.span)
: name = "max",
@ -49,11 +53,76 @@ final class CalculationExpression implements Expression {
}
}
/// Returns a `sqrt()` calculation expression.
CalculationExpression.sqrt(Expression argument, FileSpan span)
: this("sqrt", [argument], span);
/// Returns a `sin()` calculation expression.
CalculationExpression.sin(Expression argument, FileSpan span)
: this("sin", [argument], span);
/// Returns a `cos()` calculation expression.
CalculationExpression.cos(Expression argument, FileSpan span)
: this("cos", [argument], span);
/// Returns a `tan()` calculation expression.
CalculationExpression.tan(Expression argument, FileSpan span)
: this("tan", [argument], span);
/// Returns a `asin()` calculation expression.
CalculationExpression.asin(Expression argument, FileSpan span)
: this("asin", [argument], span);
/// Returns a `acos()` calculation expression.
CalculationExpression.acos(Expression argument, FileSpan span)
: this("acos", [argument], span);
/// Returns a `atan()` calculation expression.
CalculationExpression.atan(Expression argument, FileSpan span)
: this("atan", [argument], span);
/// Returns a `abs()` calculation expression.
CalculationExpression.abs(Expression argument, FileSpan span)
: this("abs", [argument], span);
/// Returns a `sign()` calculation expression.
CalculationExpression.sign(Expression argument, FileSpan span)
: this("sign", [argument], span);
/// Returns a `exp()` calculation expression.
CalculationExpression.exp(Expression argument, FileSpan span)
: this("exp", [argument], span);
/// Returns a `clamp()` calculation expression.
CalculationExpression.clamp(
Expression min, Expression value, Expression max, FileSpan span)
: this("clamp", [min, max, value], span);
/// Returns a `pow()` calculation expression.
CalculationExpression.pow(Expression base, Expression exponent, FileSpan span)
: this("pow", [base, exponent], span);
/// Returns a `log()` calculation expression.
CalculationExpression.log(Expression number, Expression base, FileSpan span)
: this("log", [number, base], span);
/// Returns a `round()` calculation expression.
CalculationExpression.round(
Expression strategy, Expression number, Expression step, FileSpan span)
: this("round", [strategy, number, step], span);
/// Returns a `atan2()` calculation expression.
CalculationExpression.atan2(Expression y, Expression x, FileSpan span)
: this("atan2", [y, x], span);
/// Returns a `mod()` calculation expression.
CalculationExpression.mod(Expression y, Expression x, FileSpan span)
: this("mod", [y, x], span);
/// Returns a `rem()` calculation expression.
CalculationExpression.rem(Expression y, Expression x, FileSpan span)
: this("rem", [y, x], span);
/// Returns a calculation expression with the given name and arguments.
///
/// Unlike the other constructors, this doesn't verify that the arguments are

View File

@ -55,6 +55,11 @@ enum Deprecation {
deprecatedIn: '1.56.0',
description: 'Passing invalid units to built-in functions.'),
/// Deprecation for passing percentages to the Sass abs() function.
absPercent('abs-percent',
deprecatedIn: '1.64.0',
description: 'Passing percentages to the Sass abs() function.'),
duplicateVariableFlags('duplicate-var-flags',
deprecatedIn: '1.62.0',
description:

View File

@ -12,6 +12,7 @@ import '../deprecation.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../module/built_in.dart';
import '../util/number.dart';
import '../value.dart';
/// The global definitions of Sass math functions.
@ -132,87 +133,32 @@ final _log = _function("log", r"$number, $base: null", (arguments) {
final _pow = _function("pow", r"$base, $exponent", (arguments) {
var base = arguments[0].assertNumber("base");
var exponent = arguments[1].assertNumber("exponent");
if (base.hasUnits) {
throw SassScriptException("\$base: Expected $base to have no units.");
} else if (exponent.hasUnits) {
throw SassScriptException(
"\$exponent: Expected $exponent to have no units.");
} else {
return SassNumber(math.pow(base.value, exponent.value));
}
return pow(base, exponent);
});
final _sqrt = _function("sqrt", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
} else {
return SassNumber(math.sqrt(number.value));
}
});
final _sqrt = _singleArgumentMathFunc("sqrt", sqrt);
///
/// Trigonometric functions
///
final _acos = _function("acos", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
} else {
return SassNumber.withUnits(math.acos(number.value) * 180 / math.pi,
numeratorUnits: ['deg']);
}
});
final _acos = _singleArgumentMathFunc("acos", acos);
final _asin = _function("asin", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
} else {
return SassNumber.withUnits(math.asin(number.value) * 180 / math.pi,
numeratorUnits: ['deg']);
}
});
final _asin = _singleArgumentMathFunc("asin", asin);
final _atan = _function("atan", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
} else {
return SassNumber.withUnits(math.atan(number.value) * 180 / math.pi,
numeratorUnits: ['deg']);
}
});
final _atan = _singleArgumentMathFunc("atan", atan);
final _atan2 = _function("atan2", r"$y, $x", (arguments) {
var y = arguments[0].assertNumber("y");
var x = arguments[1].assertNumber("x");
return SassNumber.withUnits(
math.atan2(y.value, x.convertValueToMatch(y, 'x', 'y')) * 180 / math.pi,
numeratorUnits: ['deg']);
return atan2(y, x);
});
final _cos = _function(
"cos",
r"$number",
(arguments) => SassNumber(math.cos(arguments[0]
.assertNumber("number")
.coerceValueToUnit("rad", "number"))));
final _cos = _singleArgumentMathFunc("cos", cos);
final _sin = _function(
"sin",
r"$number",
(arguments) => SassNumber(math.sin(arguments[0]
.assertNumber("number")
.coerceValueToUnit("rad", "number"))));
final _sin = _singleArgumentMathFunc("sin", sin);
final _tan = _function(
"tan",
r"$number",
(arguments) => SassNumber(math.tan(arguments[0]
.assertNumber("number")
.coerceValueToUnit("rad", "number"))));
final _tan = _singleArgumentMathFunc("tan", tan);
///
/// Unit functions
@ -288,6 +234,16 @@ final _div = _function("div", r"$number1, $number2", (arguments) {
/// Helpers
///
/// Returns a [Callable] named [name] that calls a single argument
/// math function.
BuiltInCallable _singleArgumentMathFunc(
String name, SassNumber mathFunc(SassNumber value)) {
return _function(name, r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return mathFunc(number);
});
}
/// Returns a [Callable] named [name] that transforms a number's value
/// using [transform] and preserves its units.
BuiltInCallable _numberFunction(String name, double transform(double value)) {

View File

@ -93,7 +93,7 @@ final JSClass calculationOperationClass = () {
_assertCalculationValue(left);
_assertCalculationValue(right);
return SassCalculation.operateInternal(operator, left, right,
inMinMax: false, simplify: false);
inLegacySassFunction: false, simplify: false);
});
jsClass.defineMethods({
@ -109,7 +109,7 @@ final JSClass calculationOperationClass = () {
getJSClass(SassCalculation.operateInternal(
CalculationOperator.plus, SassNumber(1), SassNumber(1),
inMinMax: false, simplify: false))
inLegacySassFunction: false, simplify: false))
.injectSuperclass(jsClass);
return jsClass;
}();

View File

@ -2065,7 +2065,8 @@ abstract class StylesheetParser extends Parser {
/// produces a potentially slash-separated number.
bool _isSlashOperand(Expression expression) =>
expression is NumberExpression ||
expression is CalculationExpression ||
(expression is CalculationExpression &&
!{'min', 'max', 'round', 'abs'}.contains(expression.name)) ||
(expression is BinaryOperationExpression && expression.allowsSlash);
/// Consumes an expression that doesn't contain any top-level whitespace.
@ -2652,32 +2653,64 @@ abstract class StylesheetParser extends Parser {
assert(scanner.peekChar() == $lparen);
switch (name) {
case "calc":
case "sqrt":
case "sin":
case "cos":
case "tan":
case "asin":
case "acos":
case "atan":
case "exp":
case "sign":
var arguments = _calculationArguments(1);
return CalculationExpression(name, arguments, scanner.spanFrom(start));
case "abs":
return _tryArgumentsCalculation(name, start, 1);
case "hypot":
var arguments = _calculationArguments();
return CalculationExpression(name, arguments, scanner.spanFrom(start));
case "min" || "max":
// 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 _tryArgumentsCalculation(name, start, null);
case "pow":
case "log":
case "atan2":
case "mod":
case "rem":
var arguments = _calculationArguments(2);
return CalculationExpression(name, arguments, scanner.spanFrom(start));
case "clamp":
var arguments = _calculationArguments(3);
return CalculationExpression(name, arguments, scanner.spanFrom(start));
case "round":
return _tryArgumentsCalculation(name, start, 3);
case _:
return null;
}
}
// Returns a CalculationExpression if the function can be parsed as a calculation,
// otherwise, returns null and the function is parsed as a normal Sass function.
CalculationExpression? _tryArgumentsCalculation(
String name, LineScannerState start, int? maxArgs) {
var beforeArguments = scanner.state;
try {
var arguments = _calculationArguments(maxArgs);
return CalculationExpression(name, arguments, scanner.spanFrom(start));
} on FormatException catch (_) {
scanner.state = beforeArguments;
return null;
}
}
/// Consumes and returns arguments for a calculation expression, including the
/// opening and closing parentheses.
///

View File

@ -110,6 +110,7 @@ double fuzzyAssertRange(double number, int min, int max, [String? name]) {
///
/// [floored division]: https://en.wikipedia.org/wiki/Modulo_operation#Variants_of_the_definition
double moduloLikeSass(double num1, double num2) {
if (num2.isInfinite && num1.sign != num2.sign) return double.nan;
if (num2 > 0) return num1 % num2;
if (num2 == 0) return double.nan;
@ -118,3 +119,78 @@ double moduloLikeSass(double num1, double num2) {
var result = num1 % num2;
return result == 0 ? 0 : result + num2;
}
/// Returns the square root of [number].
SassNumber sqrt(SassNumber number) {
number.assertNoUnits("number");
return SassNumber(math.sqrt(number.value));
}
/// Returns the sine of [number].
SassNumber sin(SassNumber number) =>
SassNumber(math.sin(number.coerceValueToUnit("rad", "number")));
/// Returns the cosine of [number].
SassNumber cos(SassNumber number) =>
SassNumber(math.cos(number.coerceValueToUnit("rad", "number")));
/// Returns the tangent of [number].
SassNumber tan(SassNumber number) =>
SassNumber(math.tan(number.coerceValueToUnit("rad", "number")));
/// Returns the arctangent of [number].
SassNumber atan(SassNumber number) {
number.assertNoUnits("number");
return SassNumber.withUnits(math.atan(number.value) * 180 / math.pi,
numeratorUnits: ['deg']);
}
/// Returns the arcsine of [number].
SassNumber asin(SassNumber number) {
number.assertNoUnits("number");
return SassNumber.withUnits(math.asin(number.value) * 180 / math.pi,
numeratorUnits: ['deg']);
}
/// Returns the arccosine of [number]
SassNumber acos(SassNumber number) {
number.assertNoUnits("number");
return SassNumber.withUnits(math.acos(number.value) * 180 / math.pi,
numeratorUnits: ['deg']);
}
/// Returns the absolute value of [number].
SassNumber abs(SassNumber number) =>
SassNumber(number.value.abs()).coerceToMatch(number);
/// Returns the logarithm of [number] with respect to [base].
SassNumber log(SassNumber number, SassNumber? base) {
if (base != null) {
return SassNumber(math.log(number.value) / math.log(base.value));
}
return SassNumber(math.log(number.value));
}
/// Returns the value of [base] raised to the power of [exponent].
SassNumber pow(SassNumber base, SassNumber exponent) {
base.assertNoUnits("base");
exponent.assertNoUnits("exponent");
return SassNumber(math.pow(base.value, exponent.value));
}
/// Returns the arctangent for [y] and [x].
SassNumber atan2(SassNumber y, SassNumber x) {
return SassNumber.withUnits(
math.atan2(y.value, x.convertValueToMatch(y, 'x', 'y')) * 180 / math.pi,
numeratorUnits: ['deg']);
}
/// Extension methods to get the sign of the double's numerical value,
/// including positive and negative zero.
extension DoubleWithSignedZero on double {
double get signIncludingZero {
if (identical(this, -0.0)) return -1.0;
if (this == 0) return 1.0;
return sign;
}
}

View File

@ -2,11 +2,16 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:math' as math;
import 'package:meta/meta.dart';
import '../deprecation.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../callable.dart';
import '../util/nullable.dart';
import '../util/number.dart';
import '../util/number.dart' as number_lib;
import '../utils.dart';
import '../value.dart';
import '../visitor/interface/value.dart';
@ -119,6 +124,187 @@ final class SassCalculation extends Value {
return SassCalculation._("max", args);
}
/// Creates a `hypot()` calculation with the given [arguments].
///
/// Each argument must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation]. It must be passed at least one argument.
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value hypot(Iterable<Object> arguments) {
var args = _simplifyArguments(arguments);
if (args.isEmpty) {
throw ArgumentError("hypot() must have at least one argument.");
}
_verifyCompatibleNumbers(args);
var subtotal = 0.0;
var first = args.first;
if (first is! SassNumber || first.hasUnit('%')) {
return SassCalculation._("hypot", args);
}
for (var i = 0; i < args.length; i++) {
var number = args.elementAt(i);
if (number is! SassNumber || !number.hasCompatibleUnits(first)) {
return SassCalculation._("hypot", args);
}
var value =
number.convertValueToMatch(first, "numbers[${i + 1}]", "numbers[1]");
subtotal += value * value;
}
return SassNumber.withUnits(math.sqrt(subtotal),
numeratorUnits: first.numeratorUnits,
denominatorUnits: first.denominatorUnits);
}
/// Creates a `sqrt()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value sqrt(Object argument) =>
_singleArgument("sqrt", argument, number_lib.sqrt, forbidUnits: true);
/// Creates a `sin()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value sin(Object argument) =>
_singleArgument("sin", argument, number_lib.sin);
/// Creates a `cos()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value cos(Object argument) =>
_singleArgument("cos", argument, number_lib.cos);
/// Creates a `tan()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value tan(Object argument) =>
_singleArgument("tan", argument, number_lib.tan);
/// Creates an `atan()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value atan(Object argument) =>
_singleArgument("atan", argument, number_lib.atan, forbidUnits: true);
/// Creates an `asin()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value asin(Object argument) =>
_singleArgument("asin", argument, number_lib.asin, forbidUnits: true);
/// Creates an `acos()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value acos(Object argument) =>
_singleArgument("acos", argument, number_lib.acos, forbidUnits: true);
/// Creates an `abs()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value abs(Object argument) {
argument = _simplify(argument);
if (argument is! SassNumber) return SassCalculation._("abs", [argument]);
if (argument.hasUnit("%")) {
warnForDeprecation(
"Passing percentage units to the global abs() function is deprecated.\n"
"In the future, this will emit a CSS abs() function to be resolved by the browser.\n"
"To preserve current behavior: math.abs($argument)"
"\n"
"To emit a CSS abs() now: abs(#{$argument})\n"
"More info: https://sass-lang.com/d/abs-percent",
Deprecation.absPercent);
}
return number_lib.abs(argument);
}
/// Creates an `exp()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value exp(Object argument) {
argument = _simplify(argument);
if (argument is! SassNumber) {
return SassCalculation._("exp", [argument]);
}
argument.assertNoUnits();
return number_lib.pow(SassNumber(math.e), argument);
}
/// Creates a `sign()` calculation with the given [argument].
///
/// The [argument] must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
static Value sign(Object argument) {
argument = _simplify(argument);
return switch (argument) {
SassNumber(value: double(isNaN: true) || 0) => argument,
SassNumber arg when !arg.hasUnit('%') =>
SassNumber(arg.value.sign).coerceToMatch(argument),
_ => SassCalculation._("sign", [argument]),
};
}
/// Creates a `clamp()` calculation with the given [min], [value], and [max].
///
/// Each argument must be either a [SassNumber], a [SassCalculation], an
@ -157,6 +343,255 @@ final class SassCalculation extends Value {
return SassCalculation._("clamp", args);
}
/// Creates a `pow()` calculation with the given [base] and [exponent].
///
/// Each argument must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
///
/// This may be passed fewer than two arguments, but only if one of the
/// arguments is an unquoted `var()` string.
static Value pow(Object base, Object? exponent) {
var args = [base, if (exponent != null) exponent];
_verifyLength(args, 2);
base = _simplify(base);
exponent = exponent.andThen(_simplify);
if (base is! SassNumber || exponent is! SassNumber) {
return SassCalculation._("pow", args);
}
base.assertNoUnits();
exponent.assertNoUnits();
return number_lib.pow(base, exponent);
}
/// Creates a `log()` calculation with the given [number] and [base].
///
/// Each argument must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
///
/// If arguments contains exactly a single argument,
/// the base is set to `math.e` by default.
static Value log(Object number, Object? base) {
number = _simplify(number);
base = base.andThen(_simplify);
var args = [number, if (base != null) base];
if (number is! SassNumber || (base != null && base is! SassNumber)) {
return SassCalculation._("log", args);
}
number.assertNoUnits();
if (base is SassNumber) {
base.assertNoUnits();
return number_lib.log(number, base);
}
return number_lib.log(number, null);
}
/// Creates a `atan2()` calculation for [y] and [x].
///
/// Each argument must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
///
/// This may be passed fewer than two arguments, but only if one of the
/// arguments is an unquoted `var()` string.
static Value atan2(Object y, Object? x) {
y = _simplify(y);
x = x.andThen(_simplify);
var args = [y, if (x != null) x];
_verifyLength(args, 2);
_verifyCompatibleNumbers(args);
if (y is! SassNumber ||
x is! SassNumber ||
y.hasUnit('%') ||
x.hasUnit('%') ||
!y.hasCompatibleUnits(x)) {
return SassCalculation._("atan2", args);
}
return number_lib.atan2(y, x);
}
/// Creates a `rem()` calculation with the given [dividend] and [modulus].
///
/// Each argument must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
///
/// This may be passed fewer than two arguments, but only if one of the
/// arguments is an unquoted `var()` string.
static Value rem(Object dividend, Object? modulus) {
dividend = _simplify(dividend);
modulus = modulus.andThen(_simplify);
var args = [dividend, if (modulus != null) modulus];
_verifyLength(args, 2);
_verifyCompatibleNumbers(args);
if (dividend is! SassNumber ||
modulus is! SassNumber ||
!dividend.hasCompatibleUnits(modulus)) {
return SassCalculation._("rem", args);
}
var result = dividend.modulo(modulus);
if (modulus.value.signIncludingZero != dividend.value.signIncludingZero) {
if (modulus.value.isInfinite) return dividend;
if (result.value == 0) {
return result.unaryMinus();
}
return result.minus(modulus);
}
return result;
}
/// Creates a `mod()` calculation with the given [dividend] and [modulus].
///
/// Each argument must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
///
/// This may be passed fewer than two arguments, but only if one of the
/// arguments is an unquoted `var()` string.
static Value mod(Object dividend, Object? modulus) {
dividend = _simplify(dividend);
modulus = modulus.andThen(_simplify);
var args = [dividend, if (modulus != null) modulus];
_verifyLength(args, 2);
_verifyCompatibleNumbers(args);
if (dividend is! SassNumber ||
modulus is! SassNumber ||
!dividend.hasCompatibleUnits(modulus)) {
return SassCalculation._("mod", args);
}
return dividend.modulo(modulus);
}
/// Creates a `round()` calculation with the given [strategyOrNumber], [numberOrStep], and [step].
/// Strategy must be either nearest, up, down or to-zero.
///
/// Number and step must be either a [SassNumber], a [SassCalculation], an
/// unquoted [SassString], a [CalculationOperation], or a
/// [CalculationInterpolation].
///
/// This automatically simplifies the calculation, so it may return a
/// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
/// can determine that the calculation will definitely produce invalid CSS.
///
/// This may be passed fewer than two arguments, but only if one of the
/// arguments is an unquoted `var()` string.
static Value round(Object strategyOrNumber,
[Object? numberOrStep, Object? step]) {
switch ((
_simplify(strategyOrNumber),
numberOrStep.andThen(_simplify),
step.andThen(_simplify)
)) {
case (SassNumber number, null, null):
return _matchUnits(number.value.round().toDouble(), number);
case (SassNumber number, SassNumber step, null)
when !number.hasCompatibleUnits(step):
_verifyCompatibleNumbers([number, step]);
return SassCalculation._("round", [number, step]);
case (SassNumber number, SassNumber step, null):
_verifyCompatibleNumbers([number, step]);
return _roundWithStep('nearest', number, step);
case (
SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') &&
var strategy,
SassNumber number,
SassNumber step
)
when !number.hasCompatibleUnits(step):
_verifyCompatibleNumbers([number, step]);
return SassCalculation._("round", [strategy, number, step]);
case (
SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') &&
var strategy,
SassNumber number,
SassNumber step
):
_verifyCompatibleNumbers([number, step]);
return _roundWithStep(strategy.text, number, step);
case (
SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') &&
var strategy,
(SassString() || CalculationInterpolation()) && var rest?,
null
):
return SassCalculation._("round", [strategy, rest]);
case (
SassString(text: 'nearest' || 'up' || 'down' || 'to-zero'),
_?,
null
):
throw SassScriptException("If strategy is not null, step is required.");
case (
SassString(text: 'nearest' || 'up' || 'down' || 'to-zero'),
null,
null
):
throw SassScriptException(
"Number to round and step arguments are required.");
case (
(SassString() || CalculationInterpolation()) && var rest,
null,
null
):
return SassCalculation._("round", [rest]);
case (var number, null, null):
throw SassScriptException(
"Single argument $number expected to be simplifiable.");
case (var number, var step?, null):
return SassCalculation._("round", [number, step]);
case (
(SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') ||
SassString(isVar: true)) &&
var strategy,
var number?,
var step?
):
return SassCalculation._("round", [strategy, number, step]);
case (_, _?, _?):
throw SassScriptException(
"$strategyOrNumber must be either nearest, up, down or to-zero.");
case (_, null, _?):
// TODO(pamelalozano): Get rid of this case once dart-lang/sdk#52908 is solved.
// ignore: unreachable_switch_case
case (_, _, _):
throw SassScriptException("Invalid parameters.");
}
}
/// Creates and simplifies a [CalculationOperation] with the given [operator],
/// [left], and [right].
///
@ -168,11 +603,12 @@ final class SassCalculation extends Value {
/// a [CalculationInterpolation].
static Object operate(
CalculationOperator operator, Object left, Object right) =>
operateInternal(operator, left, right, inMinMax: false, simplify: true);
operateInternal(operator, left, right,
inLegacySassFunction: false, simplify: true);
/// Like [operate], but with the internal-only [inMinMax] parameter.
/// Like [operate], but with the internal-only [inLegacySassFunction] parameter.
///
/// If [inMinMax] is `true`, this allows unitless numbers to be added and
/// If [inLegacySassFunction] 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.
///
@ -180,7 +616,7 @@ final class SassCalculation extends Value {
@internal
static Object operateInternal(
CalculationOperator operator, Object left, Object right,
{required bool inMinMax, required bool simplify}) {
{required bool inLegacySassFunction, required bool simplify}) {
if (!simplify) return CalculationOperation._(operator, left, right);
left = _simplify(left);
right = _simplify(right);
@ -188,7 +624,7 @@ final class SassCalculation extends Value {
if (operator case CalculationOperator.plus || CalculationOperator.minus) {
if (left is SassNumber &&
right is SassNumber &&
(inMinMax
(inLegacySassFunction
? left.isComparableTo(right)
: left.hasCompatibleUnits(right))) {
return operator == CalculationOperator.plus
@ -198,7 +634,7 @@ final class SassCalculation extends Value {
_verifyCompatibleNumbers([left, right]);
if (right is SassNumber && fuzzyLessThan(right.value, 0)) {
if (right is SassNumber && number_lib.fuzzyLessThan(right.value, 0)) {
right = right.times(SassNumber(-1));
operator = operator == CalculationOperator.plus
? CalculationOperator.minus
@ -219,6 +655,70 @@ final class SassCalculation extends Value {
/// simplification.
SassCalculation._(this.name, this.arguments);
// Returns [value] coerced to [number]'s units.
static SassNumber _matchUnits(double value, SassNumber number) =>
SassNumber.withUnits(value,
numeratorUnits: number.numeratorUnits,
denominatorUnits: number.denominatorUnits);
/// Returns a rounded [number] based on a selected rounding [strategy],
/// to the nearest integer multiple of [step].
static SassNumber _roundWithStep(
String strategy, SassNumber number, SassNumber step) {
if (!{'nearest', 'up', 'down', 'to-zero'}.contains(strategy)) {
throw ArgumentError(
"$strategy must be either nearest, up, down or to-zero.");
}
if (number.value.isInfinite && step.value.isInfinite ||
step.value == 0 ||
number.value.isNaN ||
step.value.isNaN) {
return _matchUnits(double.nan, number);
}
if (number.value.isInfinite) return number;
if (step.value.isInfinite) {
return switch ((strategy, number.value)) {
(_, 0) => number,
('nearest' || 'to-zero', > 0) => _matchUnits(0.0, number),
('nearest' || 'to-zero', _) => _matchUnits(-0.0, number),
('up', > 0) => _matchUnits(double.infinity, number),
('up', _) => _matchUnits(-0.0, number),
('down', < 0) => _matchUnits(-double.infinity, number),
('down', _) => _matchUnits(0, number),
(_, _) => throw UnsupportedError("Invalid argument: $strategy.")
};
}
var stepWithNumberUnit = step.convertValueToMatch(number);
return switch (strategy) {
'nearest' => _matchUnits(
(number.value / stepWithNumberUnit).round() * stepWithNumberUnit,
number),
'up' => _matchUnits(
(step.value < 0
? (number.value / stepWithNumberUnit).floor()
: (number.value / stepWithNumberUnit).ceil()) *
stepWithNumberUnit,
number),
'down' => _matchUnits(
(step.value < 0
? (number.value / stepWithNumberUnit).ceil()
: (number.value / stepWithNumberUnit).floor()) *
stepWithNumberUnit,
number),
'to-zero' => number.value < 0
? _matchUnits(
(number.value / stepWithNumberUnit).ceil() * stepWithNumberUnit,
number)
: _matchUnits(
(number.value / stepWithNumberUnit).floor() * stepWithNumberUnit,
number),
_ => _matchUnits(double.nan, number)
};
}
/// Returns an unmodifiable list of [args], with each argument simplified.
static List<Object> _simplifyArguments(Iterable<Object> args) =>
List.unmodifiable(args.map(_simplify));
@ -279,6 +779,21 @@ final class SassCalculation extends Value {
"${pluralize('was', args.length, plural: 'were')} passed.");
}
/// Returns a [Callable] named [name] that calls a single argument
/// math function.
///
/// If [forbidUnits] is `true` it will throw an error if [argument] has units.
static Value _singleArgument(
String name, Object argument, SassNumber mathFunc(SassNumber value),
{bool forbidUnits = false}) {
argument = _simplify(argument);
if (argument is! SassNumber) {
return SassCalculation._(name, [argument]);
}
if (forbidUnits) argument.assertNoUnits();
return mathFunc(argument);
}
/// @nodoc
@internal
T accept<T>(ValueVisitor<T> visitor) => visitor.visitCalculation(this);

View File

@ -710,7 +710,7 @@ abstract class SassNumber extends Value {
/// @nodoc
@internal
Value modulo(Value other) {
SassNumber modulo(Value other) {
if (other is SassNumber) {
return withValue(_coerceUnits(other, moduloLikeSass));
}

View File

@ -98,7 +98,7 @@ class UnitlessSassNumber extends SassNumber {
return super.lessThanOrEquals(other);
}
Value modulo(Value other) {
SassNumber modulo(Value other) {
if (other is SassNumber) {
return other.withValue(moduloLikeSass(value, other.value));
}

View File

@ -2300,7 +2300,8 @@ final class _EvaluateVisitor
var arguments = [
for (var argument in node.arguments)
await _visitCalculationValue(argument,
inMinMax: node.name == 'min' || node.name == 'max')
inLegacySassFunction:
{'min', 'max', 'round', 'abs'}.contains(node.name))
];
if (_inSupportsDeclaration) {
return SassCalculation.unsimplified(node.name, arguments);
@ -2309,8 +2310,31 @@ final class _EvaluateVisitor
try {
return switch (node.name) {
"calc" => SassCalculation.calc(arguments[0]),
"sqrt" => SassCalculation.sqrt(arguments[0]),
"sin" => SassCalculation.sin(arguments[0]),
"cos" => SassCalculation.cos(arguments[0]),
"tan" => SassCalculation.tan(arguments[0]),
"asin" => SassCalculation.asin(arguments[0]),
"acos" => SassCalculation.acos(arguments[0]),
"atan" => SassCalculation.atan(arguments[0]),
"abs" => SassCalculation.abs(arguments[0]),
"exp" => SassCalculation.exp(arguments[0]),
"sign" => SassCalculation.sign(arguments[0]),
"min" => SassCalculation.min(arguments),
"max" => SassCalculation.max(arguments),
"hypot" => SassCalculation.hypot(arguments),
"pow" =>
SassCalculation.pow(arguments[0], arguments.elementAtOrNull(1)),
"atan2" =>
SassCalculation.atan2(arguments[0], arguments.elementAtOrNull(1)),
"log" =>
SassCalculation.log(arguments[0], arguments.elementAtOrNull(1)),
"mod" =>
SassCalculation.mod(arguments[0], arguments.elementAtOrNull(1)),
"rem" =>
SassCalculation.rem(arguments[0], arguments.elementAtOrNull(1)),
"round" => SassCalculation.round(arguments[0],
arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)),
"clamp" => SassCalculation.clamp(arguments[0],
arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)),
_ => throw UnsupportedError('Unknown calculation name "${node.name}".')
@ -2319,7 +2343,9 @@ final class _EvaluateVisitor
// The simplification logic in the [SassCalculation] static methods will
// throw an error if the arguments aren't compatible, but we have access
// to the original spans so we can throw a more informative error.
_verifyCompatibleNumbers(arguments, node.arguments);
if (error.message.contains("compatible")) {
_verifyCompatibleNumbers(arguments, node.arguments);
}
throwWithTrace(_exception(error.message, node.span), error, stackTrace);
}
}
@ -2361,14 +2387,15 @@ final class _EvaluateVisitor
/// Evaluates [node] as a component of a calculation.
///
/// If [inMinMax] is `true`, this allows unitless numbers to be added and
/// If [inLegacySassFunction] 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.
/// old global `min()`, `max()`, `round()`, and `abs()` functions.
Future<Object> _visitCalculationValue(Expression node,
{required bool inMinMax}) async {
{required bool inLegacySassFunction}) async {
switch (node) {
case ParenthesizedExpression(expression: var inner):
var result = await _visitCalculationValue(inner, inMinMax: inMinMax);
var result = await _visitCalculationValue(inner,
inLegacySassFunction: inLegacySassFunction);
return inner is FunctionExpression &&
inner.name.toLowerCase() == 'var' &&
result is SassString &&
@ -2399,9 +2426,11 @@ final class _EvaluateVisitor
node,
() async => SassCalculation.operateInternal(
_binaryOperatorToCalculationOperator(operator),
await _visitCalculationValue(left, inMinMax: inMinMax),
await _visitCalculationValue(right, inMinMax: inMinMax),
inMinMax: inMinMax,
await _visitCalculationValue(left,
inLegacySassFunction: inLegacySassFunction),
await _visitCalculationValue(right,
inLegacySassFunction: inLegacySassFunction),
inLegacySassFunction: inLegacySassFunction,
simplify: !_inSupportsDeclaration));
case _:

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: 6eb7f76735562eba91e9460af796b269b3b0aaf7
// Checksum: e4d8cd913b88b73d11417b5ccda03a6313a5bb78
//
// ignore_for_file: unused_import
@ -2282,7 +2282,8 @@ final class _EvaluateVisitor
var arguments = [
for (var argument in node.arguments)
_visitCalculationValue(argument,
inMinMax: node.name == 'min' || node.name == 'max')
inLegacySassFunction:
{'min', 'max', 'round', 'abs'}.contains(node.name))
];
if (_inSupportsDeclaration) {
return SassCalculation.unsimplified(node.name, arguments);
@ -2291,8 +2292,31 @@ final class _EvaluateVisitor
try {
return switch (node.name) {
"calc" => SassCalculation.calc(arguments[0]),
"sqrt" => SassCalculation.sqrt(arguments[0]),
"sin" => SassCalculation.sin(arguments[0]),
"cos" => SassCalculation.cos(arguments[0]),
"tan" => SassCalculation.tan(arguments[0]),
"asin" => SassCalculation.asin(arguments[0]),
"acos" => SassCalculation.acos(arguments[0]),
"atan" => SassCalculation.atan(arguments[0]),
"abs" => SassCalculation.abs(arguments[0]),
"exp" => SassCalculation.exp(arguments[0]),
"sign" => SassCalculation.sign(arguments[0]),
"min" => SassCalculation.min(arguments),
"max" => SassCalculation.max(arguments),
"hypot" => SassCalculation.hypot(arguments),
"pow" =>
SassCalculation.pow(arguments[0], arguments.elementAtOrNull(1)),
"atan2" =>
SassCalculation.atan2(arguments[0], arguments.elementAtOrNull(1)),
"log" =>
SassCalculation.log(arguments[0], arguments.elementAtOrNull(1)),
"mod" =>
SassCalculation.mod(arguments[0], arguments.elementAtOrNull(1)),
"rem" =>
SassCalculation.rem(arguments[0], arguments.elementAtOrNull(1)),
"round" => SassCalculation.round(arguments[0],
arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)),
"clamp" => SassCalculation.clamp(arguments[0],
arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)),
_ => throw UnsupportedError('Unknown calculation name "${node.name}".')
@ -2301,7 +2325,9 @@ final class _EvaluateVisitor
// The simplification logic in the [SassCalculation] static methods will
// throw an error if the arguments aren't compatible, but we have access
// to the original spans so we can throw a more informative error.
_verifyCompatibleNumbers(arguments, node.arguments);
if (error.message.contains("compatible")) {
_verifyCompatibleNumbers(arguments, node.arguments);
}
throwWithTrace(_exception(error.message, node.span), error, stackTrace);
}
}
@ -2343,13 +2369,15 @@ final class _EvaluateVisitor
/// Evaluates [node] as a component of a calculation.
///
/// If [inMinMax] is `true`, this allows unitless numbers to be added and
/// If [inLegacySassFunction] 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}) {
/// old global `min()`, `max()`, `round()`, and `abs()` functions.
Object _visitCalculationValue(Expression node,
{required bool inLegacySassFunction}) {
switch (node) {
case ParenthesizedExpression(expression: var inner):
var result = _visitCalculationValue(inner, inMinMax: inMinMax);
var result = _visitCalculationValue(inner,
inLegacySassFunction: inLegacySassFunction);
return inner is FunctionExpression &&
inner.name.toLowerCase() == 'var' &&
result is SassString &&
@ -2380,9 +2408,11 @@ final class _EvaluateVisitor
node,
() => SassCalculation.operateInternal(
_binaryOperatorToCalculationOperator(operator),
_visitCalculationValue(left, inMinMax: inMinMax),
_visitCalculationValue(right, inMinMax: inMinMax),
inMinMax: inMinMax,
_visitCalculationValue(left,
inLegacySassFunction: inLegacySassFunction),
_visitCalculationValue(right,
inLegacySassFunction: inLegacySassFunction),
inLegacySassFunction: inLegacySassFunction,
simplify: !_inSupportsDeclaration));
case _:

View File

@ -1,3 +1,7 @@
## 8.1.0
* No user-visible changes.
## 8.0.0
* Various classes now use Dart 3 [class modifiers] to more specifically restrict

View File

@ -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: 8.0.0
version: 8.1.0
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass
@ -10,7 +10,7 @@ environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
sass: 1.64.3
sass: 1.65.0
dev_dependencies:
dartdoc: ^5.0.0

View File

@ -1,5 +1,5 @@
name: sass
version: 1.64.3
version: 1.65.0
description: A Sass implementation in Dart.
homepage: https://github.com/sass/dart-sass