Skip to content

Commit e4c8cd6

Browse files
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
1 parent 4c3bd0e commit e4c8cd6

15 files changed

+837
-109
lines changed

CHANGELOG.md

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

314
### Dart API

lib/src/ast/sass/expression/calculation.dart

+69
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ final class CalculationExpression implements Expression {
4040
}
4141
}
4242

43+
/// Returns a `hypot()` calculation expression.
44+
CalculationExpression.hypot(Iterable<Expression> arguments, FileSpan span)
45+
: this("hypot", arguments, span);
46+
4347
/// Returns a `max()` calculation expression.
4448
CalculationExpression.max(Iterable<Expression> arguments, this.span)
4549
: name = "max",
@@ -49,11 +53,76 @@ final class CalculationExpression implements Expression {
4953
}
5054
}
5155

56+
/// Returns a `sqrt()` calculation expression.
57+
CalculationExpression.sqrt(Expression argument, FileSpan span)
58+
: this("sqrt", [argument], span);
59+
60+
/// Returns a `sin()` calculation expression.
61+
CalculationExpression.sin(Expression argument, FileSpan span)
62+
: this("sin", [argument], span);
63+
64+
/// Returns a `cos()` calculation expression.
65+
CalculationExpression.cos(Expression argument, FileSpan span)
66+
: this("cos", [argument], span);
67+
68+
/// Returns a `tan()` calculation expression.
69+
CalculationExpression.tan(Expression argument, FileSpan span)
70+
: this("tan", [argument], span);
71+
72+
/// Returns a `asin()` calculation expression.
73+
CalculationExpression.asin(Expression argument, FileSpan span)
74+
: this("asin", [argument], span);
75+
76+
/// Returns a `acos()` calculation expression.
77+
CalculationExpression.acos(Expression argument, FileSpan span)
78+
: this("acos", [argument], span);
79+
80+
/// Returns a `atan()` calculation expression.
81+
CalculationExpression.atan(Expression argument, FileSpan span)
82+
: this("atan", [argument], span);
83+
84+
/// Returns a `abs()` calculation expression.
85+
CalculationExpression.abs(Expression argument, FileSpan span)
86+
: this("abs", [argument], span);
87+
88+
/// Returns a `sign()` calculation expression.
89+
CalculationExpression.sign(Expression argument, FileSpan span)
90+
: this("sign", [argument], span);
91+
92+
/// Returns a `exp()` calculation expression.
93+
CalculationExpression.exp(Expression argument, FileSpan span)
94+
: this("exp", [argument], span);
95+
5296
/// Returns a `clamp()` calculation expression.
5397
CalculationExpression.clamp(
5498
Expression min, Expression value, Expression max, FileSpan span)
5599
: this("clamp", [min, max, value], span);
56100

101+
/// Returns a `pow()` calculation expression.
102+
CalculationExpression.pow(Expression base, Expression exponent, FileSpan span)
103+
: this("pow", [base, exponent], span);
104+
105+
/// Returns a `log()` calculation expression.
106+
CalculationExpression.log(Expression number, Expression base, FileSpan span)
107+
: this("log", [number, base], span);
108+
109+
/// Returns a `round()` calculation expression.
110+
CalculationExpression.round(
111+
Expression strategy, Expression number, Expression step, FileSpan span)
112+
: this("round", [strategy, number, step], span);
113+
114+
/// Returns a `atan2()` calculation expression.
115+
CalculationExpression.atan2(Expression y, Expression x, FileSpan span)
116+
: this("atan2", [y, x], span);
117+
118+
/// Returns a `mod()` calculation expression.
119+
CalculationExpression.mod(Expression y, Expression x, FileSpan span)
120+
: this("mod", [y, x], span);
121+
122+
/// Returns a `rem()` calculation expression.
123+
CalculationExpression.rem(Expression y, Expression x, FileSpan span)
124+
: this("rem", [y, x], span);
125+
57126
/// Returns a calculation expression with the given name and arguments.
58127
///
59128
/// Unlike the other constructors, this doesn't verify that the arguments are

lib/src/deprecation.dart

+5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ enum Deprecation {
5555
deprecatedIn: '1.56.0',
5656
description: 'Passing invalid units to built-in functions.'),
5757

58+
/// Deprecation for passing percentages to the Sass abs() function.
59+
absPercent('abs-percent',
60+
deprecatedIn: '1.64.0',
61+
description: 'Passing percentages to the Sass abs() function.'),
62+
5863
duplicateVariableFlags('duplicate-var-flags',
5964
deprecatedIn: '1.62.0',
6065
description:

lib/src/functions/math.dart

+22-66
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../deprecation.dart';
1212
import '../evaluation_context.dart';
1313
import '../exception.dart';
1414
import '../module/built_in.dart';
15+
import '../util/number.dart';
1516
import '../value.dart';
1617

1718
/// The global definitions of Sass math functions.
@@ -132,87 +133,32 @@ final _log = _function("log", r"$number, $base: null", (arguments) {
132133
final _pow = _function("pow", r"$base, $exponent", (arguments) {
133134
var base = arguments[0].assertNumber("base");
134135
var exponent = arguments[1].assertNumber("exponent");
135-
if (base.hasUnits) {
136-
throw SassScriptException("\$base: Expected $base to have no units.");
137-
} else if (exponent.hasUnits) {
138-
throw SassScriptException(
139-
"\$exponent: Expected $exponent to have no units.");
140-
} else {
141-
return SassNumber(math.pow(base.value, exponent.value));
142-
}
136+
return pow(base, exponent);
143137
});
144138

145-
final _sqrt = _function("sqrt", r"$number", (arguments) {
146-
var number = arguments[0].assertNumber("number");
147-
if (number.hasUnits) {
148-
throw SassScriptException("\$number: Expected $number to have no units.");
149-
} else {
150-
return SassNumber(math.sqrt(number.value));
151-
}
152-
});
139+
final _sqrt = _singleArgumentMathFunc("sqrt", sqrt);
153140

154141
///
155142
/// Trigonometric functions
156143
///
157144
158-
final _acos = _function("acos", r"$number", (arguments) {
159-
var number = arguments[0].assertNumber("number");
160-
if (number.hasUnits) {
161-
throw SassScriptException("\$number: Expected $number to have no units.");
162-
} else {
163-
return SassNumber.withUnits(math.acos(number.value) * 180 / math.pi,
164-
numeratorUnits: ['deg']);
165-
}
166-
});
145+
final _acos = _singleArgumentMathFunc("acos", acos);
167146

168-
final _asin = _function("asin", r"$number", (arguments) {
169-
var number = arguments[0].assertNumber("number");
170-
if (number.hasUnits) {
171-
throw SassScriptException("\$number: Expected $number to have no units.");
172-
} else {
173-
return SassNumber.withUnits(math.asin(number.value) * 180 / math.pi,
174-
numeratorUnits: ['deg']);
175-
}
176-
});
147+
final _asin = _singleArgumentMathFunc("asin", asin);
177148

178-
final _atan = _function("atan", r"$number", (arguments) {
179-
var number = arguments[0].assertNumber("number");
180-
if (number.hasUnits) {
181-
throw SassScriptException("\$number: Expected $number to have no units.");
182-
} else {
183-
return SassNumber.withUnits(math.atan(number.value) * 180 / math.pi,
184-
numeratorUnits: ['deg']);
185-
}
186-
});
149+
final _atan = _singleArgumentMathFunc("atan", atan);
187150

188151
final _atan2 = _function("atan2", r"$y, $x", (arguments) {
189152
var y = arguments[0].assertNumber("y");
190153
var x = arguments[1].assertNumber("x");
191-
return SassNumber.withUnits(
192-
math.atan2(y.value, x.convertValueToMatch(y, 'x', 'y')) * 180 / math.pi,
193-
numeratorUnits: ['deg']);
154+
return atan2(y, x);
194155
});
195156

196-
final _cos = _function(
197-
"cos",
198-
r"$number",
199-
(arguments) => SassNumber(math.cos(arguments[0]
200-
.assertNumber("number")
201-
.coerceValueToUnit("rad", "number"))));
202-
203-
final _sin = _function(
204-
"sin",
205-
r"$number",
206-
(arguments) => SassNumber(math.sin(arguments[0]
207-
.assertNumber("number")
208-
.coerceValueToUnit("rad", "number"))));
209-
210-
final _tan = _function(
211-
"tan",
212-
r"$number",
213-
(arguments) => SassNumber(math.tan(arguments[0]
214-
.assertNumber("number")
215-
.coerceValueToUnit("rad", "number"))));
157+
final _cos = _singleArgumentMathFunc("cos", cos);
158+
159+
final _sin = _singleArgumentMathFunc("sin", sin);
160+
161+
final _tan = _singleArgumentMathFunc("tan", tan);
216162

217163
///
218164
/// Unit functions
@@ -288,6 +234,16 @@ final _div = _function("div", r"$number1, $number2", (arguments) {
288234
/// Helpers
289235
///
290236
237+
/// Returns a [Callable] named [name] that calls a single argument
238+
/// math function.
239+
BuiltInCallable _singleArgumentMathFunc(
240+
String name, SassNumber mathFunc(SassNumber value)) {
241+
return _function(name, r"$number", (arguments) {
242+
var number = arguments[0].assertNumber("number");
243+
return mathFunc(number);
244+
});
245+
}
246+
291247
/// Returns a [Callable] named [name] that transforms a number's value
292248
/// using [transform] and preserves its units.
293249
BuiltInCallable _numberFunction(String name, double transform(double value)) {

lib/src/js/value/calculation.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ final JSClass calculationOperationClass = () {
9393
_assertCalculationValue(left);
9494
_assertCalculationValue(right);
9595
return SassCalculation.operateInternal(operator, left, right,
96-
inMinMax: false, simplify: false);
96+
inLegacySassFunction: false, simplify: false);
9797
});
9898

9999
jsClass.defineMethods({
@@ -109,7 +109,7 @@ final JSClass calculationOperationClass = () {
109109

110110
getJSClass(SassCalculation.operateInternal(
111111
CalculationOperator.plus, SassNumber(1), SassNumber(1),
112-
inMinMax: false, simplify: false))
112+
inLegacySassFunction: false, simplify: false))
113113
.injectSuperclass(jsClass);
114114
return jsClass;
115115
}();

lib/src/parse/stylesheet.dart

+43-10
Original file line numberDiff line numberDiff line change
@@ -2065,7 +2065,8 @@ abstract class StylesheetParser extends Parser {
20652065
/// produces a potentially slash-separated number.
20662066
bool _isSlashOperand(Expression expression) =>
20672067
expression is NumberExpression ||
2068-
expression is CalculationExpression ||
2068+
(expression is CalculationExpression &&
2069+
!{'min', 'max', 'round', 'abs'}.contains(expression.name)) ||
20692070
(expression is BinaryOperationExpression && expression.allowsSlash);
20702071

20712072
/// Consumes an expression that doesn't contain any top-level whitespace.
@@ -2652,32 +2653,64 @@ abstract class StylesheetParser extends Parser {
26522653
assert(scanner.peekChar() == $lparen);
26532654
switch (name) {
26542655
case "calc":
2656+
case "sqrt":
2657+
case "sin":
2658+
case "cos":
2659+
case "tan":
2660+
case "asin":
2661+
case "acos":
2662+
case "atan":
2663+
case "exp":
2664+
case "sign":
26552665
var arguments = _calculationArguments(1);
26562666
return CalculationExpression(name, arguments, scanner.spanFrom(start));
26572667

2668+
case "abs":
2669+
return _tryArgumentsCalculation(name, start, 1);
2670+
2671+
case "hypot":
2672+
var arguments = _calculationArguments();
2673+
return CalculationExpression(name, arguments, scanner.spanFrom(start));
2674+
26582675
case "min" || "max":
26592676
// min() and max() are parsed as calculations if possible, and otherwise
26602677
// are parsed as normal Sass functions.
2661-
var beforeArguments = scanner.state;
2662-
List<Expression> arguments;
2663-
try {
2664-
arguments = _calculationArguments();
2665-
} on FormatException catch (_) {
2666-
scanner.state = beforeArguments;
2667-
return null;
2668-
}
2669-
2678+
return _tryArgumentsCalculation(name, start, null);
2679+
2680+
case "pow":
2681+
case "log":
2682+
case "atan2":
2683+
case "mod":
2684+
case "rem":
2685+
var arguments = _calculationArguments(2);
26702686
return CalculationExpression(name, arguments, scanner.spanFrom(start));
26712687

26722688
case "clamp":
26732689
var arguments = _calculationArguments(3);
26742690
return CalculationExpression(name, arguments, scanner.spanFrom(start));
26752691

2692+
case "round":
2693+
return _tryArgumentsCalculation(name, start, 3);
2694+
26762695
case _:
26772696
return null;
26782697
}
26792698
}
26802699

2700+
// Returns a CalculationExpression if the function can be parsed as a calculation,
2701+
// otherwise, returns null and the function is parsed as a normal Sass function.
2702+
CalculationExpression? _tryArgumentsCalculation(
2703+
String name, LineScannerState start, int? maxArgs) {
2704+
var beforeArguments = scanner.state;
2705+
try {
2706+
var arguments = _calculationArguments(maxArgs);
2707+
return CalculationExpression(name, arguments, scanner.spanFrom(start));
2708+
} on FormatException catch (_) {
2709+
scanner.state = beforeArguments;
2710+
return null;
2711+
}
2712+
}
2713+
26812714
/// Consumes and returns arguments for a calculation expression, including the
26822715
/// opening and closing parentheses.
26832716
///

0 commit comments

Comments
 (0)