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

Calc functions implementation #1970

Merged
merged 14 commits into from
Aug 9, 2023
Merged
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
69 changes: 69 additions & 0 deletions lib/src/ast/sass/expression/calculation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/src/deprecation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
88 changes: 22 additions & 66 deletions lib/src/functions/math.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);

///
pamelalozano16 marked this conversation as resolved.
Show resolved Hide resolved
/// 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 _sin = _function(
"sin",
r"$number",
(arguments) => SassNumber(math.sin(arguments[0]
.assertNumber("number")
.coerceValueToUnit("rad", "number"))));

final _tan = _function(
"tan",
r"$number",
(arguments) => SassNumber(math.tan(arguments[0]
.assertNumber("number")
.coerceValueToUnit("rad", "number"))));
final _cos = _singleArgumentMathFunc("cos", cos);

final _sin = _singleArgumentMathFunc("sin", sin);

final _tan = _singleArgumentMathFunc("tan", tan);

///
/// Unit functions
Expand Down Expand Up @@ -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)) {
Expand Down
4 changes: 2 additions & 2 deletions lib/src/js/value/calculation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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;
}();
Expand Down
53 changes: 43 additions & 10 deletions lib/src/parse/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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":
pamelalozano16 marked this conversation as resolved.
Show resolved Hide resolved
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.
///
Expand Down
Loading
Loading