Skip to content

Commit ca2be2a

Browse files
jerivasjgerigmeyernex3
authored
Expose calculations in JS API (#1988)
Co-authored-by: Jonny Gerig Meyer <[email protected]> Co-authored-by: Natalie Weizenbaum <[email protected]>
1 parent fe7f9a1 commit ca2be2a

21 files changed

+302
-11
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99

1010
### JavaScript API
1111

12+
* Add a new `SassCalculation` type that represents the calculation objects added
13+
in Dart Sass 1.40.0.
14+
15+
* Add `Value.assertCalculation()`, which returns the value if it's a
16+
`SassCalculation` and throws an error otherwise.
17+
1218
* Produce a better error message when an environment that supports some Node.js
1319
APIs loads the browser entrypoint but attempts to access the filesystem.
1420

lib/src/node.dart

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ void main() {
2727
exports.Value = valueClass;
2828
exports.SassBoolean = booleanClass;
2929
exports.SassArgumentList = argumentListClass;
30+
exports.SassCalculation = calculationClass;
31+
exports.CalculationOperation = calculationOperationClass;
32+
exports.CalculationInterpolation = calculationInterpolationClass;
3033
exports.SassColor = colorClass;
3134
exports.SassFunction = functionClass;
3235
exports.SassList = listClass;

lib/src/node/compile.dart

+36-2
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,40 @@ Importer _parseImporter(Object? importer) {
225225
}
226226
}
227227

228+
/// Implements the simplification algorithm for custom function return `Value`s.
229+
/// {@link https://github.com/sass/sass/blob/main/spec/types/calculation.md#simplifying-a-calculationvalue}
230+
Value _simplifyValue(Value value) => switch (value) {
231+
SassCalculation() => switch ((
232+
// Match against...
233+
value.name, // ...the calculation name
234+
value.arguments // ...and simplified arguments
235+
.map(_simplifyCalcArg)
236+
.toList()
237+
)) {
238+
('calc', [var first]) => first as Value,
239+
('calc', _) =>
240+
throw ArgumentError('calc() requires exactly one argument.'),
241+
('clamp', [var min, var value, var max]) =>
242+
SassCalculation.clamp(min, value, max),
243+
('clamp', _) =>
244+
throw ArgumentError('clamp() requires exactly 3 arguments.'),
245+
('min', var args) => SassCalculation.min(args),
246+
('max', var args) => SassCalculation.max(args),
247+
(var name, _) => throw ArgumentError(
248+
'"$name" is not a recognized calculation type.'),
249+
},
250+
_ => value,
251+
};
252+
253+
/// Handles simplifying calculation arguments, which are not guaranteed to be
254+
/// Value instances.
255+
Object _simplifyCalcArg(Object value) => switch (value) {
256+
SassCalculation() => _simplifyValue(value),
257+
CalculationOperation() => SassCalculation.operate(value.operator,
258+
_simplifyCalcArg(value.left), _simplifyCalcArg(value.right)),
259+
_ => value,
260+
};
261+
228262
/// Parses `functions` from [record] into a list of [Callable]s or
229263
/// [AsyncCallable]s.
230264
///
@@ -239,7 +273,7 @@ List<AsyncCallable> _parseFunctions(Object? functions, {bool asynch = false}) {
239273
late Callable callable;
240274
callable = Callable.fromSignature(signature, (arguments) {
241275
var result = (callback as Function)(toJSArray(arguments));
242-
if (result is Value) return result;
276+
if (result is Value) return _simplifyValue(result);
243277
if (isPromise(result)) {
244278
throw 'Invalid return value for custom function '
245279
'"${callable.name}":\n'
@@ -259,7 +293,7 @@ List<AsyncCallable> _parseFunctions(Object? functions, {bool asynch = false}) {
259293
result = await promiseToFuture<Object>(result as Promise);
260294
}
261295

262-
if (result is Value) return result;
296+
if (result is Value) return _simplifyValue(result);
263297
throw 'Invalid return value for custom function '
264298
'"${callable.name}": $result is not a sass.Value.';
265299
});

lib/src/node/exports.dart

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class Exports {
2626
// Value APIs
2727
external set Value(JSClass function);
2828
external set SassArgumentList(JSClass function);
29+
external set SassCalculation(JSClass function);
30+
external set CalculationOperation(JSClass function);
31+
external set CalculationInterpolation(JSClass function);
2932
external set SassBoolean(JSClass function);
3033
external set SassColor(JSClass function);
3134
external set SassFunction(JSClass function);

lib/src/node/reflection.dart

+10
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ extension JSClassExtension on JSClass {
7676
allowInteropCaptureThis((Object self, _, __, [___]) => inspect(self)));
7777
}
7878

79+
/// Defines a static method with the given [name] and [body].
80+
void defineStaticMethod(String name, Function body) {
81+
setProperty(this, name, allowInteropNamed(name, body));
82+
}
83+
84+
/// A shorthand for calling [defineStaticMethod] multiple times.
85+
void defineStaticMethods(Map<String, Function> methods) {
86+
methods.forEach(defineStaticMethod);
87+
}
88+
7989
/// Defines a method with the given [name] and [body].
8090
///
8191
/// The [body] should take an initial `self` parameter, representing the

lib/src/node/value.dart

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'reflection.dart';
1010

1111
export 'value/argument_list.dart';
1212
export 'value/boolean.dart';
13+
export 'value/calculation.dart';
1314
export 'value/color.dart';
1415
export 'value/function.dart';
1516
export 'value/list.dart';
@@ -36,6 +37,8 @@ final JSClass valueClass = () {
3637
'get': (Value self, num index) =>
3738
index < 1 && index >= -1 ? self : undefined,
3839
'assertBoolean': (Value self, [String? name]) => self.assertBoolean(name),
40+
'assertCalculation': (Value self, [String? name]) =>
41+
self.assertCalculation(name),
3942
'assertColor': (Value self, [String? name]) => self.assertColor(name),
4043
'assertFunction': (Value self, [String? name]) => self.assertFunction(name),
4144
'assertMap': (Value self, [String? name]) => self.assertMap(name),

lib/src/node/value/calculation.dart

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2023 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'package:collection/collection.dart';
6+
import 'package:node_interop/js.dart';
7+
import 'package:sass/src/node/immutable.dart';
8+
import 'package:sass/src/node/utils.dart';
9+
10+
import '../../value.dart';
11+
import '../reflection.dart';
12+
13+
/// Check that [arg] is a valid argument to a calculation function.
14+
void _assertCalculationValue(Object arg) => switch (arg) {
15+
SassNumber() ||
16+
SassString(hasQuotes: false) ||
17+
SassCalculation() ||
18+
CalculationOperation() ||
19+
CalculationInterpolation() =>
20+
null,
21+
_ => jsThrow(JsError(
22+
'Argument `$arg` must be one of SassNumber, unquoted SassString, '
23+
'SassCalculation, CalculationOperation, CalculationInterpolation')),
24+
};
25+
26+
/// Check that [arg] is an unquoted string or interpolation.
27+
bool _isValidClampArg(Object? arg) => switch (arg) {
28+
CalculationInterpolation() || SassString(hasQuotes: false) => true,
29+
_ => false,
30+
};
31+
32+
/// The JavaScript `SassCalculation` class.
33+
final JSClass calculationClass = () {
34+
var jsClass =
35+
createJSClass('sass.SassCalculation', (Object self, [Object? _]) {
36+
jsThrow(JsError("new sass.SassCalculation() isn't allowed"));
37+
});
38+
39+
jsClass.defineStaticMethods({
40+
'calc': (Object argument) {
41+
_assertCalculationValue(argument);
42+
return SassCalculation.unsimplified('calc', [argument]);
43+
},
44+
'min': (Object arguments) {
45+
var argList = jsToDartList(arguments).cast<Object>();
46+
argList.forEach(_assertCalculationValue);
47+
return SassCalculation.unsimplified('min', argList);
48+
},
49+
'max': (Object arguments) {
50+
var argList = jsToDartList(arguments).cast<Object>();
51+
argList.forEach(_assertCalculationValue);
52+
return SassCalculation.unsimplified('max', argList);
53+
},
54+
'clamp': (Object min, [Object? value, Object? max]) {
55+
if ((value == null && !_isValidClampArg(min)) ||
56+
(max == null && ![min, value].any(_isValidClampArg))) {
57+
jsThrow(JsError('Expected at least one SassString or '
58+
'CalculationInterpolation in `${[
59+
min,
60+
value,
61+
max
62+
].whereNotNull()}`'));
63+
}
64+
[min, value, max].whereNotNull().forEach(_assertCalculationValue);
65+
return SassCalculation.unsimplified(
66+
'clamp', [min, value, max].whereNotNull());
67+
}
68+
});
69+
70+
jsClass.defineMethods({
71+
'assertCalculation': (SassCalculation self, [String? name]) => self,
72+
});
73+
74+
jsClass.defineGetters({
75+
// The `name` getter is included by default by `createJSClass`
76+
'arguments': (SassCalculation self) => ImmutableList(self.arguments),
77+
});
78+
79+
getJSClass(SassCalculation.unsimplified('calc', [SassNumber(1)]))
80+
.injectSuperclass(jsClass);
81+
return jsClass;
82+
}();
83+
84+
/// The JavaScript `CalculationOperation` class.
85+
final JSClass calculationOperationClass = () {
86+
var jsClass = createJSClass('sass.CalculationOperation',
87+
(Object self, String strOperator, Object left, Object right) {
88+
var operator = CalculationOperator.values
89+
.firstWhereOrNull((value) => value.operator == strOperator);
90+
if (operator == null) {
91+
jsThrow(JsError('Invalid operator: $strOperator'));
92+
}
93+
_assertCalculationValue(left);
94+
_assertCalculationValue(right);
95+
return SassCalculation.operateInternal(operator, left, right,
96+
inMinMax: false, simplify: false);
97+
});
98+
99+
jsClass.defineMethods({
100+
'equals': (CalculationOperation self, Object other) => self == other,
101+
'hashCode': (CalculationOperation self) => self.hashCode,
102+
});
103+
104+
jsClass.defineGetters({
105+
'operator': (CalculationOperation self) => self.operator.operator,
106+
'left': (CalculationOperation self) => self.left,
107+
'right': (CalculationOperation self) => self.right,
108+
});
109+
110+
getJSClass(SassCalculation.operateInternal(
111+
CalculationOperator.plus, SassNumber(1), SassNumber(1),
112+
inMinMax: false, simplify: false))
113+
.injectSuperclass(jsClass);
114+
return jsClass;
115+
}();
116+
117+
/// The JavaScript `CalculationInterpolation` class.
118+
final JSClass calculationInterpolationClass = () {
119+
var jsClass = createJSClass('sass.CalculationInterpolation',
120+
(Object self, String value) => CalculationInterpolation(value));
121+
122+
jsClass.defineMethods({
123+
'equals': (CalculationInterpolation self, Object other) => self == other,
124+
'hashCode': (CalculationInterpolation self) => self.hashCode,
125+
});
126+
127+
jsClass.defineGetters({
128+
'value': (CalculationInterpolation self) => self.value,
129+
});
130+
131+
getJSClass(CalculationInterpolation('')).injectSuperclass(jsClass);
132+
return jsClass;
133+
}();

lib/src/value/calculation.dart

+16-6
Original file line numberDiff line numberDiff line change
@@ -328,22 +328,28 @@ class SassCalculation extends Value {
328328
/// {@category Value}
329329
@sealed
330330
class CalculationOperation {
331+
/// We use a getters to allow overriding the logic in the JS API
332+
/// implementation.
333+
331334
/// The operator.
332-
final CalculationOperator operator;
335+
CalculationOperator get operator => _operator;
336+
final CalculationOperator _operator;
333337

334338
/// The left-hand operand.
335339
///
336340
/// This is either a [SassNumber], a [SassCalculation], an unquoted
337341
/// [SassString], a [CalculationOperation], or a [CalculationInterpolation].
338-
final Object left;
342+
Object get left => _left;
343+
final Object _left;
339344

340345
/// The right-hand operand.
341346
///
342347
/// This is either a [SassNumber], a [SassCalculation], an unquoted
343348
/// [SassString], a [CalculationOperation], or a [CalculationInterpolation].
344-
final Object right;
349+
Object get right => _right;
350+
final Object _right;
345351

346-
CalculationOperation._(this.operator, this.left, this.right);
352+
CalculationOperation._(this._operator, this._left, this._right);
347353

348354
bool operator ==(Object other) =>
349355
other is CalculationOperation &&
@@ -403,9 +409,13 @@ enum CalculationOperator {
403409
/// {@category Value}
404410
@sealed
405411
class CalculationInterpolation {
406-
final String value;
412+
/// We use a getters to allow overriding the logic in the JS API
413+
/// implementation.
414+
415+
String get value => _value;
416+
final String _value;
407417

408-
CalculationInterpolation(this.value);
418+
CalculationInterpolation(this._value);
409419

410420
bool operator ==(Object other) =>
411421
other is CalculationInterpolation && value == other.value;

pkg/sass_api/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 7.2.0
2+
3+
* No user-visible changes.
4+
15
## 7.1.6
26

37
* No user-visible changes.

pkg/sass_api/pubspec.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ name: sass_api
22
# Note: Every time we add a new Sass AST node, we need to bump the *major*
33
# version because it's a breaking change for anyone who's implementing the
44
# visitor interface(s).
5-
version: 7.1.6
5+
version: 7.2.0
66
description: Additional APIs for Dart Sass.
77
homepage: https://github.com/sass/dart-sass
88

99
environment:
1010
sdk: ">=3.0.0 <4.0.0"
1111

1212
dependencies:
13-
sass: 1.63.6
13+
sass: 1.64.0
1414

1515
dev_dependencies:
1616
dartdoc: ^5.0.0

pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.64.0-dev
2+
version: 1.64.0
33
description: A Sass implementation in Dart.
44
homepage: https://github.com/sass/dart-sass
55

test/dart_api/value/boolean_test.dart

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ void main() {
2828
});
2929

3030
test("isn't any other type", () {
31+
expect(value.assertCalculation, throwsSassScriptException);
3132
expect(value.assertColor, throwsSassScriptException);
3233
expect(value.assertFunction, throwsSassScriptException);
3334
expect(value.assertMap, throwsSassScriptException);
@@ -54,6 +55,7 @@ void main() {
5455
});
5556

5657
test("isn't any other type", () {
58+
expect(value.assertCalculation, throwsSassScriptException);
5759
expect(value.assertColor, throwsSassScriptException);
5860
expect(value.assertFunction, throwsSassScriptException);
5961
expect(value.assertMap, throwsSassScriptException);

0 commit comments

Comments
 (0)