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

Expose calculations in JS API #1988

Merged
merged 28 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f5ce090
Expose calculations in JS API
jerivas Jun 6, 2023
fbfe7f1
Merge branch 'main' into js-api-calculations
jgerigmeyer Jun 7, 2023
8c9a7d8
Use strings for CalculationOperator in JS
jerivas Jun 7, 2023
64d50c8
Merge branch 'main' into js-api-calculations
jgerigmeyer Jun 8, 2023
fb9bb04
Update date
jerivas Jun 8, 2023
3f7b3ab
Fix call stack errors in JS
jerivas Jun 9, 2023
bdb6773
Refactor static methods
jerivas Jun 9, 2023
f09910f
Return an immutable list of arguments
jerivas Jun 9, 2023
13b4f33
No need to define left/right
jerivas Jun 13, 2023
71aa9a2
Fix call stack errors on operator access
jerivas Jun 16, 2023
6277580
Export calculations classes to the browser
jerivas Jun 17, 2023
60243ca
Simplify custom function return values
jerivas Jun 22, 2023
c63fa7e
lint
jerivas Jun 22, 2023
70e567e
Parse `value` and `max` from `min`
jerivas Jul 6, 2023
b4fff6c
Merge branch 'main' into js-api-calculations
jerivas Jul 6, 2023
b4e9ade
Address changes to the clamp spec
jerivas Jul 11, 2023
024c239
Improve simplification implementation
jerivas Jul 12, 2023
399dd25
Update after spec changes
jerivas Jul 13, 2023
b292fb8
Streamline simplification
jerivas Jul 13, 2023
e300e41
Check for Value before and after simplification
jerivas Jul 13, 2023
e70e5e3
Address review
jerivas Jul 14, 2023
d30acbd
Uniform error messages
jerivas Jul 18, 2023
cd06089
Address review
jerivas Jul 18, 2023
f3d169e
Clean up
jerivas Jul 18, 2023
6b3b5b8
Test assertCalculation in other types
jerivas Jul 18, 2023
52ab266
Add basic calculation tests
jerivas Jul 19, 2023
2928f3e
Update pubspec and changelog
nex3 Jul 19, 2023
859069e
Merge branch 'main' into js-api-calculations
nex3 Jul 19, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@

### JavaScript API

* Add a new `SassCalculation` type that represents the calculation objects added
in Dart Sass 1.40.0.

* Add `Value.assertCalculation()`, which returns the value if it's a
`SassCalculation` and throws an error otherwise.

* Produce a better error message when an environment that supports some Node.js
APIs loads the browser entrypoint but attempts to access the filesystem.

Expand Down
3 changes: 3 additions & 0 deletions lib/src/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ void main() {
exports.Value = valueClass;
exports.SassBoolean = booleanClass;
exports.SassArgumentList = argumentListClass;
exports.SassCalculation = calculationClass;
exports.CalculationOperation = calculationOperationClass;
exports.CalculationInterpolation = calculationInterpolationClass;
exports.SassColor = colorClass;
exports.SassFunction = functionClass;
exports.SassList = listClass;
Expand Down
38 changes: 36 additions & 2 deletions lib/src/node/compile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,40 @@ Importer _parseImporter(Object? importer) {
}
}

/// Implements the simplification algorithm for custom function return `Value`s.
/// {@link https://github.com/sass/sass/blob/main/spec/types/calculation.md#simplifying-a-calculationvalue}
Value _simplifyValue(Value value) => switch (value) {
SassCalculation() => switch ((
// Match against...
value.name, // ...the calculation name
value.arguments // ...and simplified arguments
.map(_simplifyCalcArg)
.toList()
)) {
('calc', [var first]) => first as Value,
('calc', _) =>
throw ArgumentError('calc() requires exactly one argument.'),
('clamp', [var min, var value, var max]) =>
SassCalculation.clamp(min, value, max),
('clamp', _) =>
throw ArgumentError('clamp() requires exactly 3 arguments.'),
('min', var args) => SassCalculation.min(args),
('max', var args) => SassCalculation.max(args),
(var name, _) => throw ArgumentError(
'"$name" is not a recognized calculation type.'),
},
_ => value,
};

/// Handles simplifying calculation arguments, which are not guaranteed to be
/// Value instances.
Object _simplifyCalcArg(Object value) => switch (value) {
SassCalculation() => _simplifyValue(value),
CalculationOperation() => SassCalculation.operate(value.operator,
_simplifyCalcArg(value.left), _simplifyCalcArg(value.right)),
_ => value,
};

/// Parses `functions` from [record] into a list of [Callable]s or
/// [AsyncCallable]s.
///
Expand All @@ -239,7 +273,7 @@ List<AsyncCallable> _parseFunctions(Object? functions, {bool asynch = false}) {
late Callable callable;
callable = Callable.fromSignature(signature, (arguments) {
var result = (callback as Function)(toJSArray(arguments));
if (result is Value) return result;
if (result is Value) return _simplifyValue(result);
if (isPromise(result)) {
throw 'Invalid return value for custom function '
'"${callable.name}":\n'
Expand All @@ -259,7 +293,7 @@ List<AsyncCallable> _parseFunctions(Object? functions, {bool asynch = false}) {
result = await promiseToFuture<Object>(result as Promise);
}

if (result is Value) return result;
if (result is Value) return _simplifyValue(result);
throw 'Invalid return value for custom function '
'"${callable.name}": $result is not a sass.Value.';
});
Expand Down
3 changes: 3 additions & 0 deletions lib/src/node/exports.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class Exports {
// Value APIs
external set Value(JSClass function);
external set SassArgumentList(JSClass function);
external set SassCalculation(JSClass function);
external set CalculationOperation(JSClass function);
external set CalculationInterpolation(JSClass function);
external set SassBoolean(JSClass function);
external set SassColor(JSClass function);
external set SassFunction(JSClass function);
Expand Down
10 changes: 10 additions & 0 deletions lib/src/node/reflection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ extension JSClassExtension on JSClass {
allowInteropCaptureThis((Object self, _, __, [___]) => inspect(self)));
}

/// Defines a static method with the given [name] and [body].
void defineStaticMethod(String name, Function body) {
setProperty(this, name, allowInteropNamed(name, body));
}

/// A shorthand for calling [defineStaticMethod] multiple times.
void defineStaticMethods(Map<String, Function> methods) {
methods.forEach(defineStaticMethod);
}

/// Defines a method with the given [name] and [body].
///
/// The [body] should take an initial `self` parameter, representing the
Expand Down
3 changes: 3 additions & 0 deletions lib/src/node/value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'reflection.dart';

export 'value/argument_list.dart';
export 'value/boolean.dart';
export 'value/calculation.dart';
export 'value/color.dart';
export 'value/function.dart';
export 'value/list.dart';
Expand All @@ -36,6 +37,8 @@ final JSClass valueClass = () {
'get': (Value self, num index) =>
index < 1 && index >= -1 ? self : undefined,
'assertBoolean': (Value self, [String? name]) => self.assertBoolean(name),
'assertCalculation': (Value self, [String? name]) =>
self.assertCalculation(name),
'assertColor': (Value self, [String? name]) => self.assertColor(name),
'assertFunction': (Value self, [String? name]) => self.assertFunction(name),
'assertMap': (Value self, [String? name]) => self.assertMap(name),
Expand Down
133 changes: 133 additions & 0 deletions lib/src/node/value/calculation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2023 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'package:collection/collection.dart';
import 'package:node_interop/js.dart';
import 'package:sass/src/node/immutable.dart';
import 'package:sass/src/node/utils.dart';

import '../../value.dart';
import '../reflection.dart';

/// Check that [arg] is a valid argument to a calculation function.
void _assertCalculationValue(Object arg) => switch (arg) {
SassNumber() ||
SassString(hasQuotes: false) ||
SassCalculation() ||
CalculationOperation() ||
CalculationInterpolation() =>
null,
_ => jsThrow(JsError(
'Argument `$arg` must be one of SassNumber, unquoted SassString, '
'SassCalculation, CalculationOperation, CalculationInterpolation')),
};

/// Check that [arg] is an unquoted string or interpolation.
bool _isValidClampArg(Object? arg) => switch (arg) {
CalculationInterpolation() || SassString(hasQuotes: false) => true,
_ => false,
};

/// The JavaScript `SassCalculation` class.
final JSClass calculationClass = () {
var jsClass =
createJSClass('sass.SassCalculation', (Object self, [Object? _]) {
jsThrow(JsError("new sass.SassCalculation() isn't allowed"));
jerivas marked this conversation as resolved.
Show resolved Hide resolved
});

jsClass.defineStaticMethods({
'calc': (Object argument) {
_assertCalculationValue(argument);
return SassCalculation.unsimplified('calc', [argument]);
},
'min': (Object arguments) {
var argList = jsToDartList(arguments).cast<Object>();
argList.forEach(_assertCalculationValue);
return SassCalculation.unsimplified('min', argList);
},
'max': (Object arguments) {
var argList = jsToDartList(arguments).cast<Object>();
argList.forEach(_assertCalculationValue);
return SassCalculation.unsimplified('max', argList);
},
'clamp': (Object min, [Object? value, Object? max]) {
if ((value == null && !_isValidClampArg(min)) ||
(max == null && ![min, value].any(_isValidClampArg))) {
jsThrow(JsError('Expected at least one SassString or '
'CalculationInterpolation in `${[
min,
value,
max
].whereNotNull()}`'));
}
[min, value, max].whereNotNull().forEach(_assertCalculationValue);
return SassCalculation.unsimplified(
'clamp', [min, value, max].whereNotNull());
}
});

jsClass.defineMethods({
'assertCalculation': (SassCalculation self, [String? name]) => self,
});

jsClass.defineGetters({
// The `name` getter is included by default by `createJSClass`
jerivas marked this conversation as resolved.
Show resolved Hide resolved
'arguments': (SassCalculation self) => ImmutableList(self.arguments),
});

getJSClass(SassCalculation.unsimplified('calc', [SassNumber(1)]))
.injectSuperclass(jsClass);
jerivas marked this conversation as resolved.
Show resolved Hide resolved
return jsClass;
}();

/// The JavaScript `CalculationOperation` class.
final JSClass calculationOperationClass = () {
var jsClass = createJSClass('sass.CalculationOperation',
(Object self, String strOperator, Object left, Object right) {
var operator = CalculationOperator.values
.firstWhereOrNull((value) => value.operator == strOperator);
if (operator == null) {
jsThrow(JsError('Invalid operator: $strOperator'));
}
_assertCalculationValue(left);
_assertCalculationValue(right);
return SassCalculation.operateInternal(operator, left, right,
inMinMax: false, simplify: false);
});

jsClass.defineMethods({
'equals': (CalculationOperation self, Object other) => self == other,
'hashCode': (CalculationOperation self) => self.hashCode,
});

jsClass.defineGetters({
'operator': (CalculationOperation self) => self.operator.operator,
jerivas marked this conversation as resolved.
Show resolved Hide resolved
jerivas marked this conversation as resolved.
Show resolved Hide resolved
'left': (CalculationOperation self) => self.left,
'right': (CalculationOperation self) => self.right,
});

getJSClass(SassCalculation.operateInternal(
CalculationOperator.plus, SassNumber(1), SassNumber(1),
inMinMax: false, simplify: false))
.injectSuperclass(jsClass);
return jsClass;
}();

/// The JavaScript `CalculationInterpolation` class.
final JSClass calculationInterpolationClass = () {
jerivas marked this conversation as resolved.
Show resolved Hide resolved
var jsClass = createJSClass('sass.CalculationInterpolation',
(Object self, String value) => CalculationInterpolation(value));

jsClass.defineMethods({
'equals': (CalculationInterpolation self, Object other) => self == other,
'hashCode': (CalculationInterpolation self) => self.hashCode,
});

jsClass.defineGetters({
'value': (CalculationInterpolation self) => self.value,
});

getJSClass(CalculationInterpolation('')).injectSuperclass(jsClass);
return jsClass;
}();
22 changes: 16 additions & 6 deletions lib/src/value/calculation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -328,22 +328,28 @@ class SassCalculation extends Value {
/// {@category Value}
@sealed
class CalculationOperation {
/// We use a getters to allow overriding the logic in the JS API
/// implementation.

/// The operator.
final CalculationOperator operator;
CalculationOperator get operator => _operator;
final CalculationOperator _operator;

/// The left-hand operand.
///
/// This is either a [SassNumber], a [SassCalculation], an unquoted
/// [SassString], a [CalculationOperation], or a [CalculationInterpolation].
final Object left;
Object get left => _left;
final Object _left;

/// The right-hand operand.
///
/// This is either a [SassNumber], a [SassCalculation], an unquoted
/// [SassString], a [CalculationOperation], or a [CalculationInterpolation].
final Object right;
Object get right => _right;
final Object _right;

CalculationOperation._(this.operator, this.left, this.right);
CalculationOperation._(this._operator, this._left, this._right);

bool operator ==(Object other) =>
other is CalculationOperation &&
Expand Down Expand Up @@ -403,9 +409,13 @@ enum CalculationOperator {
/// {@category Value}
@sealed
class CalculationInterpolation {
final String value;
/// We use a getters to allow overriding the logic in the JS API
/// implementation.

String get value => _value;
final String _value;

CalculationInterpolation(this.value);
CalculationInterpolation(this._value);

bool operator ==(Object other) =>
other is CalculationInterpolation && value == other.value;
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 @@
## 7.2.0

* No user-visible changes.

## 7.1.6

* 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: 7.1.6
version: 7.2.0
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass

environment:
sdk: ">=3.0.0 <4.0.0"

dependencies:
sass: 1.63.6
sass: 1.64.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.64.0-dev
version: 1.64.0
description: A Sass implementation in Dart.
homepage: https://github.com/sass/dart-sass

Expand Down
2 changes: 2 additions & 0 deletions test/dart_api/value/boolean_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ void main() {
});

test("isn't any other type", () {
expect(value.assertCalculation, throwsSassScriptException);
expect(value.assertColor, throwsSassScriptException);
expect(value.assertFunction, throwsSassScriptException);
expect(value.assertMap, throwsSassScriptException);
Expand All @@ -54,6 +55,7 @@ void main() {
});

test("isn't any other type", () {
expect(value.assertCalculation, throwsSassScriptException);
expect(value.assertColor, throwsSassScriptException);
expect(value.assertFunction, throwsSassScriptException);
expect(value.assertMap, throwsSassScriptException);
Expand Down
Loading