Add JS type defs for calculations (#3605)

Co-authored-by: Jonny Gerig Meyer <jonny@oddbird.net>
This commit is contained in:
Ed Rivas 2023-06-20 16:55:26 -06:00 committed by GitHub
parent 0cc3d56656
commit 39915044f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 534 additions and 27 deletions

View File

@ -1,3 +1,11 @@
## Draft 3
* Make `CalculationOperation` and `CalculationInterpolation` concrete rather
than abstract classes.
* Export `CalculationValue` and `CalculationOperator` types.
* Adjust `SassCalculation.clamp` to interpret comma-separated `min` values as
valid input for `value` and `max`.
## Draft 2
* Simplify calculations at the point at which they're returned from the JS API,

View File

@ -169,7 +169,7 @@ functions?: Record<string, CustomFunction<sync>>;
The type of values that can be arguments to a [`SassCalculation`].
```ts
type CalculationValue =
export type CalculationValue =
| SassNumber
| SassCalculation
| SassString
@ -247,8 +247,10 @@ Creates a value that represents `calc(min, value, max)` expression.
* If `value` is undefined and `max` is not undefined, throw an error.
* If `value` or `max` is undefined and neither `min` nor `value` is a
`SassString` that begins with `"var("`, throw an error.
* If `value` or `max` is undefined and `min` is not a `SassString` or
`CalculationInterpolation` that contains comma-separated values that can be
interpreted as values for `value` and `max` (for example `clamp(#{"1, 2,
3"})`).
* Return a calculation with name `"clamp"` and `min`, `value`, and `max` as its
arguments, excluding any arguments that are undefined.
@ -286,7 +288,7 @@ get arguments(): List<CalculationValue>;
The set of possible operators in a Sass calculation.
```ts
type CalculationOperator = '+' | '-' | '*' | '/';
export type CalculationOperator = '+' | '-' | '*' | '/';
```
### `CalculationOperation`
@ -296,7 +298,7 @@ The JS API representation of a Sass [`CalculationOperation`].
[CalculationOperation]: ../spec/types/calculation.md#types
```ts
export abstract class CalculationOperation implements ValueObject {
export class CalculationOperation implements ValueObject {
```
#### `internal`
@ -310,7 +312,7 @@ A private property like [`Value.internal`] that refers to a Sass
Creates a Sass CalculationOperation by setting the fields to the arguments of
the corresponding names, and returns it.
```ts
constructor(
@ -374,7 +376,7 @@ The JS API representation of a Sass [`CalculationInterpolation`].
[`CalculationInterpolation`]: ../spec/types/calculation.md#types
```ts
export abstract class CalculationInterpolation implements ValueObject {
export class CalculationInterpolation implements ValueObject {
```
#### `internal`

View File

@ -23,9 +23,14 @@ export {
} from './options';
export {PromiseOr} from './util/promise_or';
export {
CalculationInterpolation,
CalculationOperation,
CalculationOperator,
CalculationValue,
ListSeparator,
SassArgumentList,
SassBoolean,
SassCalculation,
SassColor,
SassFunction,
SassList,

View File

@ -131,7 +131,8 @@ export interface Options<sync extends 'sync' | 'async'> {
*
* Functions are passed JavaScript representations of [Sass value
* types](https://sass-lang.com/documentation/js-api#value-types), and must
* return the same.
* return the same. If the return value includes {@link SassCalculation}s
* they will be simplified before being returned.
*
* When writing custom functions, it's important to make them as user-friendly
* and as close to the standards set by Sass's core functions as possible. Some

139
js-api-doc/value/calculation.d.ts vendored Normal file
View File

@ -0,0 +1,139 @@
import {List, ValueObject} from 'immutable';
import {Value, SassNumber, SassString} from './index';
/**
* The type of values that can be arguments to a {@link SassCalculation}.
* @category Custom Function
* */
export type CalculationValue =
| SassNumber
| SassCalculation
| SassString
| CalculationOperation
| CalculationInterpolation;
/**
* Sass's [calculation
* type](https://sass-lang.com/documentation/values/calculations).
*
* Note: in the JS API calculations are not simplified eagerly. This also means
* that unsimplified calculations are not equal to the numbers they would be
* simplified to.
*
* @category Custom Function
*/
export class SassCalculation extends Value {
/**
* Creates a value that represents `calc(argument)`.
*
* @throws `Error` if `argument` is or transitively contains a quoted
* {@link SassString}
* @returns A calculation with the name `calc` and `argument` as its single
* argument.
*/
static calc(argument: CalculationValue): SassCalculation;
/**
* Creates a value that represents `min(arguments...)`.
*
* @throws `Error` if any of `arguments` are or transitively contain a quoted
* {@link SassString}
* @returns A calculation with the name `min` and `arguments` as its arguments.
*/
static min(
arguments: CalculationValue[] | List<CalculationValue>
): SassCalculation;
/**
* Creates a value that represents `max(arguments...)`.
*
* @throws `Error` if any of `arguments` are or transitively contain a quoted
* {@link SassString}
* @returns A calculation with the name `max` and `arguments` as its arguments.
*/
static max(
arguments: CalculationValue[] | List<CalculationValue>
): SassCalculation;
/**
* Creates a value that represents `clamp(value, min, max)`.
*
* @throws `Error` if any of `value`, `min`, or `max` are or transitively
* contain a quoted {@link SassString}.
* @throws `Error` if `value` is undefined and `max` is not undefined.
* @throws `Error` if `value` or `max` is undefined and `min` is not a
{@link SassString} or {@link CalculationInterpolation} that contains
comma-separated values that can be interpreted as values for `value` and
`max` (for example `clamp(#{"1, 2, 3"})`).
@returns A calculation with the name `clamp` and `min`, `value`, and `max`
as it's arguments, excluding any arguments that are undefined.
*/
static clamp(
min: CalculationValue,
value?: CalculationValue,
max?: CalculationValue
): SassCalculation;
/** Returns the calculation's `name` field. */
get name(): string;
/** Returns a list of the calculation's `arguments` */
get arguments(): List<CalculationValue>;
}
/**
* The set of possible operators in a Sass calculation.
* @category Custom Function
*/
export type CalculationOperator = '+' | '-' | '*' | '/';
/**
* A binary operation that can appear in a {@link SassCalculation}.
* @category Custom Function
*/
export class CalculationOperation implements ValueObject {
/**
* Creates a Sass CalculationOperation with the given `operator`, `left`, and
* `right` values.
*/
constructor(
operator: CalculationOperator,
left: CalculationValue,
right: CalculationValue
);
/** Returns the operation's `operator` field. */
get operator(): CalculationOperator;
/** Returns the operation's `left` field. */
get left(): CalculationValue;
/** Returns the operation's `right` field. */
get right(): CalculationValue;
equals(other: CalculationOperation): boolean;
hashCode(): number;
}
/**
* A string injected into a {@link SassCalculation} using interpolation. Unlike
* unquoted strings, interpolations are always surrounded in parentheses when
* they appear in {@link CalculationOperation}s.
* @category Custom Function
*/
export class CalculationInterpolation implements ValueObject {
/**
* Creates a Sass CalculationInterpolation with the given `value`.
*/
constructor(value: string);
/**
* Returns the interpolation's `value` field.
*/
get value(): string;
equals(other: CalculationInterpolation): boolean;
hashCode(): number;
}

View File

@ -1,6 +1,7 @@
import {List, ValueObject} from 'immutable';
import {SassBoolean} from './boolean';
import {SassCalculation} from './calculation';
import {SassColor} from './color';
import {SassFunction} from './function';
import {ListSeparator} from './list';
@ -10,6 +11,13 @@ import {SassString} from './string';
export {SassArgumentList} from './argument_list';
export {SassBoolean, sassTrue, sassFalse} from './boolean';
export {
SassCalculation,
CalculationValue,
CalculationOperator,
CalculationOperation,
CalculationInterpolation,
} from './calculation';
export {SassColor} from './color';
export {SassFunction} from './function';
export {SassList, ListSeparator} from './list';
@ -116,6 +124,14 @@ export abstract class Value implements ValueObject {
*/
assertBoolean(name?: string): SassBoolean;
/**
* Throws if `this` isn't a {@link SassCalculation}.
*
* @param name - The name of the function argument `this` came from (without
* the `$`) if it came from an argument. Used for error reporting.
*/
assertCalculation(name?: string): SassCalculation;
/**
* Throws if `this` isn't a {@link SassColor}.
*

View File

@ -60,9 +60,14 @@ export {
} from './options';
export {PromiseOr} from './util/promise_or';
export {
CalculationInterpolation,
CalculationOperation,
CalculationOperator,
CalculationValue,
ListSeparator,
SassArgumentList,
SassBoolean,
SassCalculation,
SassColor,
SassFunction,
SassList,

View File

@ -121,30 +121,44 @@ charset?: boolean;
#### `functions`
When the compiler encounters a global function call with a signature that does
not match that of a built-in function, but matches a key in this record, it must
call the associated `CustomFunction` and return its result. If the function
throws an error or returns anything other than a `Value`, the compiler should
treat it as the Sass function throwing an error.
Before beginning compilation:
> As in the rest of Sass, `_`s and `-`s are considered equivalent when
> determining which function signatures match.
* For each key/value pair `signature`/`function` in this record:
Before beginning compilation, if any key in this record is not an
[<ident-token>] followed immediately by an `ArgumentDeclaration`, the compiler
must throw an error.
* If `signature` isn't an [<ident-token>] followed immediately by an
`ArgumentDeclaration`, throw an error.
* Let `name` be `signature`'s <ident-token>.
* If there's already a global function whose name is underscore-insensitively
equal to `name`, continue to the next key/value pair.
* Otherwise, add a global function whose signature is `signature`. When this
function is called:
* Let `result` be the result of calling the associated `CustomFunction` with
the given arguments. If this call throws an error, treat it as a Sass
error thrown by the Sass function.
> As in the rest of Sass, `_`s and `-`s are considered equivalent when
> determining which function signatures match.
* Throw an error if `result` is or transitively contains:
* An object that's not an instance of the `Value` class.
* A [`SassFunction`] whose `signature` field isn't a valid Sass function
signature that could appear after the `@function` directive in a Sass
stylesheet.
* Return a copy of `result.internal` with all calculations it transitively
contains (including the return value itself if it's a calculation)
replaced with the result of [simplifying] those calculations.
[<ident-token>]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
[`SassFunction`]: value/function.d.ts.md
[simplifying]: https://github.com/sass/sass/tree/main/spec/types/calculation.md#simplifying-a-calculation
If the `CustomFunction` returns an invalid value, or a value that transitively
contains an invalid value, the compiler must treat that as the Sass function
throwing an error. The following values are considered invalid:
* An object that's not an instance of the `Value` class.
* A `SassFunction` whose `signature` field isn't a valid Sass function signature
that could appear after the `@function` directive in a Sass stylesheet.
```ts
functions?: Record<string, CustomFunction<sync>>;
```

View File

@ -0,0 +1,296 @@
# Calculation API
```ts
import {List, ValueObject} from 'immutable';
import {Value, SassNumber, SassString} from './index';
```
## Table of Contents
* [Types](#types)
* [`CalculationValue`](#calculationvalue)
* [`SassCalculation`](#sasscalculation)
* [`internal`](#internal)
* [`calc`](#calc)
* [`min`](#min)
* [`max`](#max)
* [`clamp`](#clamp)
* [`name`](#name)
* [`CalculationOperator`](#calculationoperator)
* [`CalculationOperation`](#calculationoperation)
* [`internal`](#internal-1)
* [Constructor](#constructor)
* [`operator`](#operator)
* [`left`](#left)
* [`right`](#right)
* [`equals`](#equals)
* [`hashCode`](#hashcode)
* [`CalculationInterpolation`](#calculationinterpolation)
* [`internal`](#internal-2)
* [Constructor](#constructor-1)
* [`value`](#value)
* [`equals`](#equals-1)
* [`hashCode`](#hashcode-1)
## Types
### `CalculationValue`
The type of values that can be arguments to a [`SassCalculation`].
```ts
export type CalculationValue =
| SassNumber
| SassCalculation
| SassString
| CalculationOperation
| CalculationInterpolation;
```
### `SassCalculation`
The JS API representation of a Sass [calculation].
> Note: in the JS API calculations are not simplified eagerly. This also
> means that unsimplified calculations are not equal to the numbers they
> would be simplified to.
```ts
export class SassCalculation extends Value {
```
#### `internal`
The [private `internal` field] refers to a Sass [calculation].
[private `internal` field]: index.d.ts.md#internal
[calculation]: ../../types/calculation.md
#### `calc`
Creates a value that represents `calc(argument)`.
* If `argument` is or transitively contains a quoted `SassString`, throw an
error.
* Return a calculation with name `"calc"` and `argument` as its single argument.
```ts
static calc(argument: CalculationValue): SassCalculation;
```
#### `min`
Creates a value that represents `min(...arguments)`.
* If `argument` is or transitively contains a quoted `SassString`, throw an
error.
* Return a calculation with name `"min"` and `arguments` as its arguments.
```ts
static min(
arguments: CalculationValue[] | List<CalculationValue>
): SassCalculation;
```
#### `max`
Creates a value that represents `max(...arguments)`.
* If `arguments` transitively contains a quoted `SassString`, throw an error.
* Return a calculation with name `"max"` and `arguments` as its arguments.
```ts
static max(
arguments: CalculationValue[] | List<CalculationValue>
): SassCalculation;
```
#### `clamp`
Creates a value that represents `calc(min, value, max)` expression.
* If `min`, `max`, or `clamp` is or transitively contains a quoted `SassString`,
throw an error.
* If `value` is undefined and `max` is not undefined, throw an error.
* If `value` or `max` is undefined and `min` is not a `SassString` or
`CalculationInterpolation` that contains comma-separated values that can be
interpreted as values for `value` and `max` (for example `clamp(#{"1, 2,
3"})`).
* Return a calculation with name `"clamp"` and `min`, `value`, and `max` as its
arguments, excluding any arguments that are undefined.
```ts
static clamp(
min: CalculationValue,
value?: CalculationValue,
max?: CalculationValue
): SassCalculation;
```
#### `name`
Returns [`internal`]'s `name` field.
[`internal`]: #internal
```ts
get name(): string;
```
Returns a list of [`internal`]'s arguments.
```ts
get arguments(): List<CalculationValue>;
```
```ts
} // SassCalculation
```
### `CalculationOperator`
The set of possible operators in a Sass calculation.
```ts
export type CalculationOperator = '+' | '-' | '*' | '/';
```
### `CalculationOperation`
The JS API representation of a Sass [`CalculationOperation`].
[CalculationOperation]: ../../types/calculation.md#types
```ts
export class CalculationOperation implements ValueObject {
```
#### `internal`
A private property like [`Value.internal`] that refers to a Sass
[`CalculationOperation`].
[`Value.internal`]: index.d.ts.md
#### Constructor
Creates a Sass CalculationOperation by setting the fields to the arguments of
the corresponding names, and returns it.
```ts
constructor(
operator: CalculationOperator,
left: CalculationValue,
right: CalculationValue
);
```
#### `operator`
Returns [`internal`][co-internal]'s `operator` field.
[co-internal]: #internal-1
```ts
get operator(): CalculationOperator;
```
#### `left`
Returns [`internal`][co-internal]'s `left` field.
```ts
get left(): CalculationValue;
```
#### `right`
Returns [`internal`][co-internal]'s `right` field.
```ts
get right(): CalculationValue;
```
#### `equals`
Whether [`internal`][co-internal] is equal to `other.internal` in Sass
```ts
equals(other: CalculationOperation): boolean;
```
#### `hashCode`
Returns the same number for any two `CalculationOperation`s that are equal
according to [`equals`](#equals).
```ts
hashCode(): number;
```
```ts
} // CalculationOperation
```
### `CalculationInterpolation`
The JS API representation of a Sass [`CalculationInterpolation`].
[`CalculationInterpolation`]: ../../types/calculation.md#types
```ts
export class CalculationInterpolation implements ValueObject {
```
#### `internal`
A private property like [`Value.internal`] that refers to a Sass
[`CalculationInterpolation`].
#### Constructor
Creates a Sass [`CalculationInterpolation`] by setting the `value` field to the
`value` argument and returns it.
```ts
constructor(value: string);
```
#### `value`
Returns [`internal`][ci-internal]'s `value` field.
[ci-internal]: #internal-2
```ts
get value(): string;
```
#### `equals`
Whether [`internal`][ci-internal] is equal to `other.internal` in Sass.
```ts
equals(other: CalculationInterpolation): boolean;
```
#### `hashCode`
Returns the same number for any two `CalculationInterpolation`s that are equal
according to [`equals`](#equals-1).
```ts
hashCode(): number;
```
```ts
} // CalculationInterpolation
```

View File

@ -4,6 +4,7 @@
import {List, ValueObject} from 'immutable';
import {SassBoolean} from './boolean';
import {SassCalculation} from './calculation';
import {SassColor} from './color';
import {SassFunction} from './function';
import {ListSeparator} from './list';
@ -13,6 +14,13 @@ import {SassString} from './string';
export {SassArgumentList} from './argument_list';
export {SassBoolean, sassTrue, sassFalse} from './boolean';
export {
SassCalculation,
CalculationValue,
CalculationOperator,
CalculationOperation,
CalculationInterpolation
} from './calculation';
export {SassColor} from './color';
export {SassFunction} from './function';
export {SassList, ListSeparator} from './list';
@ -36,6 +44,7 @@ export {SassString} from './string';
* [`sassIndexToListIndex`](#sassindextolistindex)
* [`get`](#get)
* [`assertBoolean`](#assertboolean)
* [`assertCalculation`](#assertcalculation)
* [`assertColor`](#assertcolor)
* [`assertFunction`](#assertfunction)
* [`assertMap`](#assertmap)
@ -176,6 +185,18 @@ Returns `this` if it's a [`SassBoolean`] and throws an error otherwise.
assertBoolean(name?: string): SassBoolean;
```
#### `assertCalculation`
Returns `this` if it's a [`SassCalculation`] and throws an error otherwise.
[`SassCalculation`]: calculation.d.ts.md
> The `name` parameter may be used for error reporting.
```ts
assertCalculation(name?: string): SassCalculation;
```
#### `assertColor`
Returns `this` if it's a [`SassColor`] and throws an error otherwise.