Use literate programming for JS API specs (#3552)

This commit is contained in:
Natalie Weizenbaum 2023-04-27 13:50:53 -07:00 committed by GitHub
parent 8dd2d5e020
commit 7abda4e2f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 4286 additions and 2617 deletions

View File

@ -16,7 +16,7 @@ jobs:
- uses: actions/setup-node@v3
with: {node-version: '${{ env.NODE_VERSION }}'}
- run: npm ci
- run: npx gts lint && npx tsc --noEmit
- run: npm run tangle && npx gts lint && npx tsc --noEmit
toc:
name: Tables of contents

6
.gitignore vendored
View File

@ -1,3 +1,9 @@
# Ignore the tangled output of literate TypeScript specs.
/proposal/*.d.ts
/accepted/*.d.ts
/spec/**/*.d.ts
!/spec/util/**/*.d.ts
# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node

View File

@ -1,251 +0,0 @@
/**
* # JavaScript Calculation API: Draft 2
*
* *([Issue](https://github.com/sass/sass/issues/818),
* [Changelog](calculation-api.changes.md))*
*
* ## Background
*
* > This section is non-normative.
*
* This proposal simply exposes the [calculation type] to the JavaScript API.
*
* [calculation type]: ../accepted/first-class-calc.md
*
* ## Summary
*
* > This section is non-normative.
*
* ### Design Decisions
*
* #### Simplification
*
* We considered eagerly simplifying calculations as they were constructed to
* match the behavior of values in Sass itself. However, this poses a problem
* for API implementations that don't have direct access to compiler logic, such
* as the Node.js embedded host: they would need to implement the simplification
* logic locally, which is relatively complex and opens a broad surface area for
* subtle cross-implementation incompatibilities.
*
* This could potentially be solved by adding an explicit request to the
* embedded protocol, but this would pose its own problems given that JS is
* strict about separating asynchronous calls (like those across process
* boundaries) and synchronous calls (like this API).
*
* Given that, we chose instead to handle simplification only at the custom
* function boundary rather than when a calculation is constructed.
*/
/* ## API */
import {List, ValueObject} from 'immutable';
import {Value, SassNumber, SassString} from '../spec/js-api/value';
declare module '../spec/js-api/value' {
interface Value {
/**
* Asserts that `this` is a `SassCalculation`:
*
* - If `internal` is a Sass calculation, return `this`.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertCalculation(name?: string): SassCalculation;
}
}
declare module '../spec/js-api/options' {
interface Options<sync extends 'sync' | 'async'> {
/**
* Replace this option's specification with:
*
* Before beginning compilation:
*
* - For each key/value pair `signature`/`function` in this record:
*
* - If `signature` isn't an [<ident-token>] followed immediately by an
* `ArgumentDeclaration`, throw an error.
*
* [<ident-token>]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
*
* - 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.
*
* [simplifying]: ../spec/types/calculation.md#simplifying-a-calculation
*/
functions?: Record<string, CustomFunction<sync>>;
}
}
/** The type of values that can be arguments to a `SassCalculation`. */
type CalculationValue =
| SassNumber
| SassCalculation
| SassString
| CalculationOperation
| CalculationInterpolation;
/**
* The JS API representation of a Sass [calculation].
*
* [calculation]: ../spec/types/calculation.md#types
*
* `internal` refers to 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.
*/
export class SassCalculation extends Value {
/**
* Creates a value that represents `calc(argument)` expression.
*
* - If `argument` is or transitively contains a quoted `SassString`, throw an
* error.
*
* - Return a calculation with name `"calc"` and a `argument` as its single
* argument.
*/
static calc(argument: CalculationValue): SassCalculation;
/**
* 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.
*/
static min(
arguments: CalculationValue[] | List<CalculationValue>
): SassCalculation;
/**
* Creates a value that represents `min(...arguments)`.
*
* - If `arguments` transitively contains a quoted `SassString`, throw an
* error.
*
* - Return a calculation with name `"max"` and `arguments` as its arguments.
*/
static max(
arguments: CalculationValue[] | List<CalculationValue>
): SassCalculation;
/**
* 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 neither `min` nor `value` is a
* `SassString` that begins with `"var("`, throw an error.
*
* - Return a calculation with name `"clamp"` and `min`, `value`, and `max` as
* its arguments, excluding any arguments that are undefined.
*/
static clamp(
min: CalculationValue,
value?: CalculationValue,
max?: CalculationValue
): SassCalculation;
/** `internal`'s `name` field. */
get name(): string;
/** A list of `internal`'s arguments. */
get arguments(): List<CalculationValue>;
}
/** The set of possible operators in a Sass calculation. */
type CalculationOperator = '+' | '-' | '*' | '/';
/**
* The JS API representation of a Sass [CalculationOperation].
*
* [CalculationOperation]: ../spec/types/calculation.md#types
*
* `internal` refers to a Sass CalculationOperation.
*/
export abstract class CalculationOperation implements ValueObject {
/**
* Creates a Sass CalculationOperation by setting the fields to the arguments
* of the corresponding names, and returns it.
*/
constructor(
operator: CalculationOperator,
left: CalculationValue,
right: CalculationValue
);
/** `internal`'s `operator` field. */
get operator(): CalculationOperator;
/** `internal`'s `left` field. */
get left(): CalculationValue;
/** `internal`'s `right` field. */
get right(): CalculationValue;
/** Whether `internal` is equal to `other.internal` in Sass. */
equals(other: CalculationOperation): boolean;
/** Must be the same for two equal values. */
hashCode(): number;
}
/**
* The JS API representation of a Sass [CalculationInterpolation].
*
* [CalculationInterpolation]: ../spec/types/calculation.md#types
*
* `internal` refers to a Sass CalculationInterpolation.
*
* Two `CalculationInterpolation`s are equal if their `value` fields are equal.
*/
export abstract class CalculationInterpolation implements ValueObject {
/**
* Creates a Sass CalculationInterpolation by setting the `value` field to the
* argument of the corresponding name, and returns it.
*/
constructor(value: string);
/** `internal`'s `value` field. */
get value(): string;
/** Whether `internal` is equal to `other.internal` in Sass. */
equals(other: CalculationOperation): boolean;
/** Must be the same for two equal values. */
hashCode(): number;
}

View File

@ -0,0 +1,423 @@
# JavaScript Calculation API: Draft 2
*([Issue](https://github.com/sass/sass/issues/818),
[Changelog](calculation-api.changes.md))*
## Table of Contents
* [Background](#background)
* [Summary](#summary)
* [Design Decisions](#design-decisions)
* [Simplification](#simplification)
* [API](#api)
* [Types](#types)
* [`Value`](#value)
* [`assertCalculation`](#assertcalculation)
* [`Options`](#options)
* [`functions`](#functions)
* [`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)
## Background
> This section is non-normative.
This proposal simply exposes the [calculation type] to the JavaScript API.
[calculation type]: ../accepted/first-class-calc.md
## Summary
> This section is non-normative.
### Design Decisions
#### Simplification
We considered eagerly simplifying calculations as they were constructed to
match the behavior of values in Sass itself. However, this poses a problem
for API implementations that don't have direct access to compiler logic, such
as the Node.js embedded host: they would need to implement the simplification
logic locally, which is relatively complex and opens a broad surface area for
subtle cross-implementation incompatibilities.
This could potentially be solved by adding an explicit request to the
embedded protocol, but this would pose its own problems given that JS is
strict about separating asynchronous calls (like those across process
boundaries) and synchronous calls (like this API).
Given that, we chose instead to handle simplification only at the custom
function boundary rather than when a calculation is constructed.
## API
```ts
import {List, ValueObject} from 'immutable';
import {Value, SassNumber, SassString} from '../spec/js-api/value';
```
## Types
### `Value`
```ts
declare module '../spec/js-api/value' {
interface Value {
```
#### `assertCalculation`
Returns `this` if it's a [`SassCalculation`] and throws an error otherwise.
[`SassCalculation`]: #sasscalculation
> The `name` parameter may be used for error reporting.
```ts
assertCalculation(name?: string): SassCalculation;
```
```ts
} // Value
} // module
```
### `Options`
```ts
declare module '../spec/js-api/options' {
interface Options<sync extends 'sync' | 'async'> {
```
#### `functions`
Replace this option's specification with:
Before beginning compilation:
* For each key/value pair `signature`/`function` in this record:
* 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`]: ../spec/js-api/value/function.d.ts.md
[simplifying]: ../spec/types/calculation.md#simplifying-a-calculation
```ts
functions?: Record<string, CustomFunction<sync>>;
```
```ts
} // Options
} // module
```
### `CalculationValue`
The type of values that can be arguments to a [`SassCalculation`].
```ts
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]: ../spec/js-api/value/index.d.ts.md#internal
[calculation]: ../spec/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 neither `min` nor `value` is a
`SassString` that begins with `"var("`, throw an error.
* 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
type CalculationOperator = '+' | '-' | '*' | '/';
```
### `CalculationOperation`
The JS API representation of a Sass [`CalculationOperation`].
[CalculationOperation]: ../spec/types/calculation.md#types
```ts
export abstract class CalculationOperation implements ValueObject {
```
#### `internal`
A private property like [`Value.internal`] that refers to a Sass
[`CalculationOperation`].
[`Value.internal`]: ../spec/js-api/value/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`]: ../spec/types/calculation.md#types
```ts
export abstract 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: CalculationOperation): boolean;
```
#### `hashCode`
Returns the same number for any two `CalculationInterpolation`s that are equal
according to [`equals`](#equals-1).
```ts
hashCode(): number;
```
```ts
} // CalculationInterpolation
```

26
package-lock.json generated
View File

@ -8,6 +8,7 @@
"dependencies": {
"@types/diff": "^5.0.1",
"@types/glob": "^7.1.4",
"@types/marked": "^4.0.8",
"@types/prettier": "^2.4.1",
"colors": "^1.3.3",
"diff": "^5.0.0",
@ -16,8 +17,10 @@
"indent-string": "^4.0.0",
"markdown-link-check": "3.11.1",
"markdown-toc": "^1.2.0",
"marked": "^4.3.0",
"prettier": "^2.4.1",
"source-map-js": "^0.6.2",
"ts-dedent": "^2.2.0",
"ts-node": "^10.2.1",
"typedoc": "^0.22.4"
},
@ -276,6 +279,11 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"node_modules/@types/marked": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.8.tgz",
"integrity": "sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw=="
},
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@ -3742,6 +3750,14 @@
"node": ">=8"
}
},
"node_modules/ts-dedent": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
"engines": {
"node": ">=6.10"
}
},
"node_modules/ts-node": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.1.tgz",
@ -4249,6 +4265,11 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"@types/marked": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.0.8.tgz",
"integrity": "sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw=="
},
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@ -6804,6 +6825,11 @@
"integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
"dev": true
},
"ts-dedent": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="
},
"ts-node": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.1.tgz",

View File

@ -12,14 +12,17 @@
"link-check": "npx ts-node test/link-check.ts",
"toc-check": "npx ts-node test/toc-check.ts",
"update-toc": "npx ts-node tool/update-toc.ts",
"js-api-doc-check": "npx ts-node test/js-api-doc-check.ts",
"typedoc": "npx typedoc --treatWarningsAsErrors js-api-doc/index.d.ts",
"fix": "gts fix",
"test": "gts lint && tsc --noEmit && npm run toc-check && npm run link-check && npm run js-api-doc-check && npm run typedoc"
"js-api-doc-check": "npm run tangle && npx ts-node test/js-api-doc-check.ts",
"typedoc": "npm run tangle && npx typedoc --treatWarningsAsErrors js-api-doc/index.d.ts",
"tangle": "npx ts-node tool/tangle.ts",
"untangle": "npx ts-node tool/untangle.ts",
"fix": "npm run update-toc && npm run tangle && gts fix && npm run untangle",
"test": "npm run tangle && gts lint && tsc --noEmit && npm run toc-check && npm run link-check && npm run js-api-doc-check && npm run typedoc"
},
"dependencies": {
"@types/diff": "^5.0.1",
"@types/glob": "^7.1.4",
"@types/marked": "^4.0.8",
"@types/prettier": "^2.4.1",
"colors": "^1.3.3",
"diff": "^5.0.0",
@ -28,8 +31,10 @@
"indent-string": "^4.0.0",
"markdown-link-check": "3.11.1",
"markdown-toc": "^1.2.0",
"marked": "^4.3.0",
"prettier": "^2.4.1",
"source-map-js": "^0.6.2",
"ts-dedent": "^2.2.0",
"ts-node": "^10.2.1",
"typedoc": "^0.22.4"
},

View File

@ -1,356 +0,0 @@
/**
* # JavaScript Deprecations API: Draft 1.1
*
* *([Issue](https://github.com/sass/sass/issues/3520),
* [Changelog](js-deprecations.changes.md))*
*
* ## Background
*
* > This section is non-normative.
*
* We recently added support to Dart Sass that allowed users to opt in to
* treating deprecation warnings as errors (on a per-deprecation basis), as
* well as opting in early to certain future deprecations. This is currently
* supported on the command line and via the Dart API, but we'd like to extend
* this support to the JS API as well.
*
* We would also like to add support for silencing a particular deprecation's
* warnings, primarily to enable a gentler process for deprecating `@import`.
*
* ## Summary
*
* > This section is non-normative.
*
* This proposal adds a new `Deprecation` interface and `Version` class to the
* JS API, three new optional properties on `Options` (`fatalDeprecations`,
* `silenceDeprecations`, and `futureDeprecations`), a new parameter on
* `Logger.warn` (`options.deprecationType`) two type aliases (`DeprecationOrId`
* and `DeprecationStatus`) and a new object `deprecations` that contains the
* various `Deprecation` objects.
*
* All deprecations are specified in `deprecations`, and any new deprecations
* added in the future (even those specific to a particular implementation)
* should update the specification accordingly. Deprecations should never be
* removed from the specification; when the behavior being deprecated is removed
* (i.e. there's a major version release), the deprecation status should be
* changed to obsolete, but remain in the specification.
*
* Every `Deprecation` has a unique `id`, one of four `status` values, and
* (optionally) a human-readable `description`. Depending on the status, each
* deprecation may also have a `deprecatedIn` version and an `obsoleteIn`
* version that specify the compiler versions the deprecation became active
* and became obsolete in, respectively.
*
* ### Design Decisions
*
* #### Exposing the Full `Deprecation` Interface
*
* One alternative to specifying a full `Deprecation` interface is to just have
* the relevant APIs take in string IDs. We considered this, but concluded that
* each deprecation has additional metadata that users of the API may wish to
* access (for example, a bundler may wish to surface the `description` and
* `deprecatedIn` version to its users).
*
* #### Formally Specifying the Deprecations
*
* We chose to make the list of deprecations part of the specification itself,
* as this ensures that the language-wide deprecations are consistent across
* implementations. However, if an implementation wishes to add a deprecation
* that applies only to itself, it may still do so.
*
* Additionally, we chose to leave the `status`, `deprecatedIn` version, and
* `obsoleteIn` version of each deprecation out of the specification. As the
* two current implementers of this API are both based on Dart Sass, these
* fields are _currently_ consistent across implementations in practice, but
* the status of each deprecation in other potential future implementers would
* not always match Dart Sass, and the relevent versions would almost certainly
* not match.
*
* #### Warnings for Invalid Deprecations and Precedence of Options
*
* Whenever potentially invalid sets of deprecations are passed to any of the
* options, we choose to emit warnings rather than errors, as the status of
* each deprecation can change over time, and users may share a configuration
* when compiling across multiple implementations/versions whose dependency
* statuses may not be in sync.
*
* The situations we chose to warn for are:
*
* - an invalid string ID.
*
* This is disallowed by the API's types, but may still occur at runtime,
* and should be warned for accordingly.
*
* - a future deprecation is passed to `fatalDeprecations` but not
* `futureDeprecations`.
*
* In this scenario, the future deprecation will still be treated as fatal,
* but we want to warn users to prevent situtations where a user tries to
* make every deprecation fatal and ends up including future ones too.
*
* - an obsolete deprecation is passed to `fatalDeprecations`.
*
* If a deprecation is obsolete, that means the breaking change has already
* happened, so making it fatal is a no-op.
*
* - passing anything other than an active deprecation to `silenceDeprecations`.
*
* This is particularly important for obsolete deprecations, since otherwise
* users may not be aware of a subtle breaking change for which they were
* previously silencing warnings. We also warn for passing
* `Deprecation.userAuthored`, since there's no way to distinguish between
* different deprecations from user-authored code, so silencing them as a
* group is inadvisable. Passing a future deprecation here is either a no-op,
* or cancels out passing it to `futureDeprecations`, so we warn for that as
* well.
*
* - passing a non-future deprecation to `futureDeprecations`.
*
* This is a no-op, so we should warn users so they can clean up their
* configuration.
*/
/* ## API */
import {SourceSpan} from '../spec/js-api';
declare module '../spec/js-api' {
interface Options<sync extends 'sync' | 'async'> {
/**
* A set of deprecations to treat as fatal.
*
* If a deprecation warning of any provided type is encountered during
* compilation, the compiler must error instead.
*
* The compiler should convert any string passed here to a `Deprecation`
* by indexing `deprecations`. *
* If a version is passed here, it should be treated equivalently to passing
* all active deprecations whose `deprecatedIn` version is less than or
* equal to it.
*
* The compiler must error if a future deprecation is included here, unless
* that future deprecation is also passed to `futureDeprecations`. It must
* emit a warning if an obsolete deprecation is included here.
*
* If a deprecation is passed both here and to `silenceDeprecations`, a
* warning must be emitted, but making the deprecation fatal must take
* precedence.
*/
fatalDeprecations?: (DeprecationOrId | Version)[];
/**
* A set of active deprecations to ignore.
*
* If a deprecation warning of any provided type is encountered during
* compilation, the compiler must ignore it.
*
* The compiler should convert any string passed here to a `Deprecation`
* by indexing `Deprecations`.
*
* The compiler must error if an obsolete deprecation or
* `deprecations['user-authored']` is included here. It must emit a warning
* if a future deprecation is included here, but silencing it takes
* precedence over `futureDeprecations` enabling it.
*/
silenceDeprecations?: DeprecationOrId[];
/**
* A set of future deprecations to opt into early.
*
* For each future deprecation provided here, the compiler must treat that
* deprecation as if it is active, emitting warnings as necessary (subject
* to `fatalDeprecations` and `silenceDeprecations`).
*
* The compiler should convert any string passed here to a `Deprecation`
* by indexing `Deprecations`.
*
* The compiler must emit a warning if a non-future deprecation is included
* here.
*/
futureDeprecations?: DeprecationOrId[];
}
interface Logger {
/**
* Update the third sub-bullet of bullet two to read:
*
* If this warning is caused by behavior that used to be allowed but will
* be disallowed in the future, set `options.deprecation` to `true` and
* set `options.deprecationType` to the relevant `Deprecation`. Otherwise,
* set `options.deprecation` to `false` and leave `options.deprecationType`
* undefined.
*/
warn?(
message: string,
options: {
deprecation: boolean;
deprecationType?: Deprecation;
span?: SourceSpan;
stack?: string;
}
): void;
}
/**
* An object containing all of the deprecations.
*/
export const deprecations: Deprecations;
}
interface Deprecations {
/** Deprecation for passing a string to `call` instead of `get-function`. */
'call-string': Deprecation<'call-string'>;
/** Deprecation for `@elseif`. */
elseif: Deprecation<'elseif'>;
/** Deprecation for parsing `@-moz-document`. */
'moz-document': Deprecation<'moz-document'>;
/** Deprecation for importers using relative canonical URLs. */
'relative-canonical': Deprecation<'relative-canonical'>;
/** Deprecation for declaring new variables with `!global`. */
'new-global': Deprecation<'new-global'>;
/**
* Deprecation for certain functions in the color module matching the
* behavior of their global counterparts for compatibility reasons.
*/
'color-module-compat': Deprecation<'color-module-compat'>;
/**
* Deprecation for treaing `/` as division.
*
* Update the proposal for forward slash as a separator to say that it emits
* deprecation warnings with ID 'slash-div'.
*/
'slash-div': Deprecation<'slash-div'>;
/**
* Deprecation for leading, trailing, and repeated combinators.
*
* Update the proposal for bogus combinators to say that it emits deprecation
* warnings with ID 'bogus-combinators'.
*/
'bogus-combinators': Deprecation<'bogus-combinators'>;
/**
* Deprecation for ambiguous `+` and `-` operators.
*
* Update the proposal for strict unary operators to say that it emits
* deprecation warnings with ID 'strict-unary'.
*/
'strict-unary': Deprecation<'strict-unary'>;
/**
* Deprecation for passing invalid units to certain built-in functions.
*
* Update the proposals for function units, random with units, and angle units
* to say that they emit deprecation warnings with ID 'function-units'.
*/
'function-units': Deprecation<'function-units'>;
/**
* Deprecation for using multiple `!global` or `!default` flags on a single
* variable.
*
* > This deprecation was never explicitly listed in a proposal.
*/
'duplicate-var-flags': Deprecation<'duplicate-var-flags'>;
/**
* Deprecation for `@import` rules.
*
* Update the proposal for the module system to say that, when `@import` is
* deprecated, Sass will emit deprecation warnings with ID 'import' when
* `@import` rules are encountered.
*/
import: Deprecation<'import'>;
/** Used for deprecations coming from user-authored code. */
'user-authored': Deprecation<'user-authored', 'user'>;
}
/** A deprecation, or the ID of one. */
export type DeprecationOrId = Deprecation | keyof Deprecations;
/** A deprecation's status. */
export type DeprecationStatus = 'active' | 'user' | 'future' | 'obsolete';
/** A deprecated feature in the language. */
export interface Deprecation<
id extends keyof Deprecations = keyof Deprecations,
status extends DeprecationStatus = DeprecationStatus
> {
/**
* A kebab-case ID for this deprecation.
*/
id: id;
/**
* The status of this deprecation.
*
* - 'active' means this deprecation is currently enabled. `deprecatedIn` is
* non-null and `obsoleteIn` is null.
* - 'user' means this deprecation is from user-authored code. Both
* `deprecatedIn` and `obsoleteIn` are null.
* - 'future' means this deprecation is not yet enabled. Both `deprecatedIn`
* and `obsoleteIn` are null.
* - 'obsolete' means this deprecation is now obsolete, as the feature it was
* for has been fully removed. Both `deprecatedIn` and `obsoleteIn` are
* non-null.
*/
status: status;
/** A brief user-readable description of this deprecation. */
description?: string;
/**
* The compiler version this feature was first deprecated in.
*
* This is implementation-dependent, so versions are not guaranteed to be
* consistent between different compilers. For future deprecations, or those
* originating from user-authored code, this is null.
*/
deprecatedIn: status extends 'future' | 'user' ? null : Version;
/**
* The compiler version this feature was fully removed in, making the
* deprecation obsolete.
*
* This is null for active and future deprecations.
*/
obsoleteIn: status extends 'obsolete' ? Version : null;
}
/** A semantic version of the compiler. */
export class Version {
/**
* The major version.
*
* This must be a non-negative integer.
*/
readonly major: number;
/**
* The minor version.
*
* This must be a non-negative integer.
*/
readonly minor: number;
/**
* The patch version.
*
* This must be a non-negative integer.
*/
readonly patch: number;
constructor(major: number, minor: number, patch: number);
/**
* Parses a string in the form "major.minor.patch" into a `Version`.
*/
static parse(version: string): Version;
}

View File

@ -0,0 +1,516 @@
# JavaScript Deprecations API: Draft 1.1
*([Issue](https://github.com/sass/sass/issues/3520),
[Changelog](js-deprecations.changes.md))*
## Background
> This section is non-normative.
We recently added support to Dart Sass that allowed users to opt in to
treating deprecation warnings as errors (on a per-deprecation basis), as
well as opting in early to certain future deprecations. This is currently
supported on the command line and via the Dart API, but we'd like to extend
this support to the JS API as well.
We would also like to add support for silencing a particular deprecation's
warnings, primarily to enable a gentler process for deprecating `@import`.
## Summary
> This section is non-normative.
This proposal adds a new `Deprecation` interface and `Version` class to the
JS API, three new optional properties on `Options` (`fatalDeprecations`,
`silenceDeprecations`, and `futureDeprecations`), a new parameter on
`Logger.warn` (`options.deprecationType`) two type aliases (`DeprecationOrId`
and `DeprecationStatus`) and a new object `deprecations` that contains the
various `Deprecation` objects.
All deprecations are specified in `deprecations`, and any new deprecations
added in the future (even those specific to a particular implementation)
should update the specification accordingly. Deprecations should never be
removed from the specification; when the behavior being deprecated is removed
(i.e. there's a major version release), the deprecation status should be
changed to obsolete, but remain in the specification.
Every `Deprecation` has a unique `id`, one of four `status` values, and
(optionally) a human-readable `description`. Depending on the status, each
deprecation may also have a `deprecatedIn` version and an `obsoleteIn`
version that specify the compiler versions the deprecation became active
and became obsolete in, respectively.
### Design Decisions
#### Exposing the Full `Deprecation` Interface
One alternative to specifying a full `Deprecation` interface is to just have
the relevant APIs take in string IDs. We considered this, but concluded that
each deprecation has additional metadata that users of the API may wish to
access (for example, a bundler may wish to surface the `description` and
`deprecatedIn` version to its users).
#### Formally Specifying the Deprecations
We chose to make the list of deprecations part of the specification itself,
as this ensures that the language-wide deprecations are consistent across
implementations. However, if an implementation wishes to add a deprecation
that applies only to itself, it may still do so.
Additionally, while a deprecation's status is part of the specification, we
chose to leave the `deprecatedIn` and `obsoleteIn` versions of each
deprecation out of the specification. As the two current implementers of this
API are both based on Dart Sass, these versions are _currently_ consistent
across implementations in practice, potential future implementers should not
need to be tied to Dart Sass's versioning.
#### Warnings for Invalid Deprecations and Precedence of Options
Whenever potentially invalid sets of deprecations are passed to any of the
options, we choose to emit warnings rather than errors, as the status of
each deprecation can change over time, and users may share a configuration
when compiling across multiple implementations/versions whose dependency
statuses may not be in sync.
The situations we chose to warn for are:
* an invalid string ID.
This is disallowed by the API's types, but may still occur at runtime,
and should be warned for accordingly.
* a future deprecation is passed to `fatalDeprecations` but not
`futureDeprecations`.
In this scenario, the future deprecation will still be treated as fatal,
but we want to warn users to prevent situtations where a user tries to
make every deprecation fatal and ends up including future ones too.
* an obsolete deprecation is passed to `fatalDeprecations`.
If a deprecation is obsolete, that means the breaking change has already
happened, so making it fatal is a no-op.
* passing anything other than an active deprecation to `silenceDeprecations`.
This is particularly important for obsolete deprecations, since otherwise
users may not be aware of a subtle breaking change for which they were
previously silencing warnings. We also warn for passing
`Deprecation.userAuthored`, since there's no way to distinguish between
different deprecations from user-authored code, so silencing them as a
group is inadvisable. Passing a future deprecation here is either a no-op,
or cancels out passing it to `futureDeprecations`, so we warn for that as
well.
* passing a non-future deprecation to `futureDeprecations`.
This is a no-op, so we should warn users so they can clean up their
configuration.
## API
```ts
import {SourceSpan} from '../spec/js-api';
```
## Types
### `Options`
```ts
declare module '../spec/js-api' {
interface Options<sync extends 'sync' | 'async'> {
```
#### `fatalDeprecations`
A set of deprecations to treat as fatal.
If a deprecation warning of any provided type is encountered during compilation,
the compiler must error instead.
The compiler should convert any string passed here to a `Deprecation` by
indexing `deprecations`. If a version is passed here, it should be treated
equivalently to passing all active deprecations whose `deprecatedIn` version is
less than or equal to it.
The compiler must error if a future deprecation is included here, unless that
future deprecation is also passed to `futureDeprecations`. It must emit a
warning if an obsolete deprecation is included here.
If a deprecation is passed both here and to `silenceDeprecations`, a warning
must be emitted, but making the deprecation fatal must take precedence.
```ts
fatalDeprecations?: (DeprecationOrId | Version)[];
```
#### `silenceDeprecations`
A set of active deprecations to ignore.
If a deprecation warning of any provided type is encountered during compilation,
the compiler must ignore it.
The compiler should convert any string passed here to a `Deprecation` by
indexing `Deprecations`.
The compiler must error if an obsolete deprecation or
`deprecations['user-authored']` is included here. It must emit a warning if a
future deprecation is included here, but silencing it takes precedence over
`futureDeprecations` enabling it.
```ts
silenceDeprecations?: DeprecationOrId[];
```
#### `futureDeprecations`
A set of future deprecations to opt into early.
For each future deprecation provided here, the compiler must treat that
deprecation as if it is active, emitting warnings as necessary (subject to
`fatalDeprecations` and `silenceDeprecations`).
The compiler should convert any string passed here to a `Deprecation` by
indexing `Deprecations`.
The compiler must emit a warning if a non-future deprecation is included here.
```ts
futureDeprecations?: DeprecationOrId[];
```
```ts
} // Options
```
### `Logger`
```ts
interface Logger {
```
#### `warn`
Update the third sub-bullet of bullet two to read:
If this warning is caused by behavior that used to be allowed but will be
disallowed in the future, set `options.deprecation` to `true` and set
`options.deprecationType` to the relevant `Deprecation`. Otherwise, set
`options.deprecation` to `false` and leave `options.deprecationType` undefined.
```ts
warn?(
message: string,
options: {
deprecation: boolean;
deprecationType?: Deprecation;
span?: SourceSpan;
stack?: string;
}
): void;
```
```ts
} // Logger
} // module
```
### `Deprecations`
```ts
interface Deprecations {
```
#### `call-string`
Deprecation for passing a string to `call` instead of `get-function`.
```ts
'call-string': Deprecation<'call-string'>;
```
#### `elseif`
Deprecation for `@elseif`.
```ts
elseif: Deprecation<'elseif'>;
```
#### `moz-document`
Deprecation for parsing `@-moz-document`.
```ts
'moz-document': Deprecation<'moz-document'>;
```
#### `relative-canonical`
Deprecation for importers using relative canonical URLs.
```ts
'relative-canonical': Deprecation<'relative-canonical'>;
```
#### `new-global`
Deprecation for declaring new variables with `!global`.
```ts
'new-global': Deprecation<'new-global'>;
```
#### `color-module-compat`
Deprecation for certain functions in the color module matching the
behavior of their global counterparts for compatibility reasons.
```ts
'color-module-compat': Deprecation<'color-module-compat'>;
```
#### `slash-div`
Deprecation for treaing `/` as division.
Update the proposal for forward slash as a separator to say that it emits
deprecation warnings with ID 'slash-div'.
```ts
'slash-div': Deprecation<'slash-div'>;
```
#### `bogus-combinators`
Deprecation for leading, trailing, and repeated combinators.
Update the proposal for bogus combinators to say that it emits deprecation
warnings with ID 'bogus-combinators'.
```ts
'bogus-combinators': Deprecation<'bogus-combinators'>;
```
#### `strict-unary`
Deprecation for ambiguous `+` and `-` operators.
Update the proposal for strict unary operators to say that it emits deprecation
warnings with ID 'strict-unary'.
```ts
'strict-unary': Deprecation<'strict-unary'>;
```
#### `function-units`
Deprecation for passing invalid units to certain built-in functions.
Update the proposals for function units, random with units, and angle units to
say that they emit deprecation warnings with ID 'function-units'.
```ts
'function-units': Deprecation<'function-units'>;
```
#### `duplicate-var-flags`
Deprecation for using multiple `!global` or `!default` flags on a single
variable.
> This deprecation was never explicitly listed in a proposal.
```ts
'duplicate-var-flags': Deprecation<'duplicate-var-flags'>;
```
#### `import`
Deprecation for `@import` rules.
Update the proposal for the module system to say that, when `@import` is
deprecated, Sass will emit deprecation warnings with ID 'import' when `@import`
rules are encountered.
```ts
import: Deprecation<'import'>;
```
#### `user-authored`
Used for deprecations coming from user-authored code.
```ts
'user-authored': Deprecation<'user-authored', 'user'>;
```
```ts
} // Deprecations
```
### `DeprecationOrId`
A deprecation, or the ID of one.
```ts
export type DeprecationOrId = Deprecation | keyof Deprecations;
```
### `DeprecationStatus`
A deprecation's status.
```ts
export type DeprecationStatus = 'active' | 'user' | 'future' | 'obsolete';
```
### `Deprecation`
A deprecated feature in the language.
```ts
export interface Deprecation<
id extends keyof Deprecations = keyof Deprecations,
status extends DeprecationStatus = DeprecationStatus
> {
```
#### `id`
A kebab-case ID for this deprecation.
```ts
id: id;
```
The status of this deprecation.
* 'active' means this deprecation is currently enabled. `deprecatedIn` is
non-null and `obsoleteIn` is null.
* 'user' means this deprecation is from user-authored code. Both `deprecatedIn`
and `obsoleteIn` are null.
* 'future' means this deprecation is not yet enabled. Both `deprecatedIn` and
`obsoleteIn` are null.
* 'obsolete' means this deprecation is now obsolete, as the feature it was for
has been fully removed. Both `deprecatedIn` and `obsoleteIn` are non-null.
```ts
status: status;
```
#### `description`
A brief user-readable description of this deprecation.
```ts
description?: string;
```
#### `deprecatedIn`
The compiler version this feature was first deprecated in.
This is implementation-dependent, so versions are not guaranteed to be
consistent between different compilers. For future deprecations, or those
originating from user-authored code, this is null.
```ts
deprecatedIn: status extends 'future' | 'user' ? null : Version;
```
#### `obsoleteIn`
The compiler version this feature was fully removed in, making the deprecation
obsolete.
This is null for active and future deprecations.
```ts
obsoleteIn: status extends 'obsolete' ? Version : null;
```
```ts
} // Deprecation
```
### `Version`
A [semantic version] of the compiler.
[semantic version]: https://semver.org/
```ts
export class Version {
```
#### Constructor
Creates a new `Version` with its `major`, `minor`, and `patch` fields set to the
corresponding arguments.
```ts
constructor(major: number, minor: number, patch: number);
```
#### `major`
The major version.
This must be a non-negative integer.
```ts
readonly major: number;
```
#### `minor`
The minor version.
This must be a non-negative integer.
```ts
readonly minor: number;
```
#### `patch`
The patch version.
This must be a non-negative integer.
```ts
readonly patch: number;
```
#### `parse`
Parses a string in the form "major.minor.patch" into a `Version`.
```ts
static parse(version: string): Version;
```
```ts
} // Version
```
## Top-Level Members
```ts
declare module '../spec/js-api' {
```
### `deprecations`
An object containing all of the deprecations.
```ts
export const deprecations: Deprecations;
```
```ts
} // module
```

185
proposal/prev-url.d.ts vendored
View File

@ -1,185 +0,0 @@
/**
* # Containing URL: Draft 1.0
*
* *([Issue](https://github.com/sass/sass/issues/3247))*
*
* ## Background
*
* > This section is non-normative.
*
* Among many other changes, the [new importer API] dropped an importer's
* ability to access the URL of the stylesheet that contained the load, known in
* the legacy API as the "previous URL". This was an intentional design choice
* which enforced the invariant that the same canonical URL always refers to the
* same file.
*
* [new import API]: ../accepted/new-js-importer.d.ts
*
* However, this restriction makes it difficult for importers to work as
* expected in certain contexts. For example, in the Node.js ecosystem JS loads
* depend on the structure of the `node_modules` directory closest to the
* containing file. The new import API can't match this behavior.
*
* This is particularly problematic for the widely-used Webpack importer, which
* expands on the concept of directory-specific load contexts to allow users to
* do fine-grained customization of how differnt files will load their
* dependencies. In order to ease migration to the new API for this plugin and
* its users, and to better match external ecosystems' load semantics, a
* solution is needed.
*
* ## Summary
*
* > This section is non-normative.
*
* This proposal adds an additional option to the `Importer.canonicalize()` API
* that provides the canonical URL of the containing file (the "containing
* URL"). However, in order to preserve the desired invariants, this option is
* only provided when either:
*
* - `Importer.canonicalize()` is being passed a relative URL (which means the
* URL has already been tried as a load relative to the current canonical
* URL), or
*
* - `Importer.canonicalize()` is passed an absolute URL whose scheme the
* importer has declared as non-canonical.
*
* A "non-canonical" scheme is a new concept introduced by this proposals.
* Importers will optionally be able to provide a `nonCanonicalScheme` field
* which will declare one or more URL schemes that they'll never return from
* `canonicalize()`. (If they do, Sass will throw an error.)
*
* ### Design Decisions
*
* #### Invariants
*
* The specific restrictions for this API were put in place to preserve the
* following invariants:
*
* 1. There must be a one-to-one mapping between canonical URLs and stylesheets.
* This means that even when a user loads a stylesheet using a relative URL,
* that stylesheet must have an absolute canonical URL associated with it
* *and* loading that canonical URL must return the same stylesheet. This
* means that any stylesheet can *always* be unambiguously loaded using its
* canonical URL.
*
* 2. Relative URLs are resolved like paths and HTTP URLs. For example, within
* `scheme:a/b/c.scss`, the URL `../d` should be resolved to `scheme:a/d`.
*
* 3. Loads relative to the current stylesheet always take precedence over loads
* from importers, so if `scheme:a/b/x.scss` exists then `@use "x"` within
* `scheme:a/b/c.scss` will always load it.
*
* #### Risks
*
* Providing access to the containing URL puts these invariants at risk in two ways:
*
* 1. Access to the containing URL in addition to a canonical URL makes it
* possible for importer authors to handle the same canonical URL differently
* depending in different contexts, violating invariant (1).
*
* 2. It's likely that importer authors familiar with the legacy API will
* incorrectly assume that any containing URL that exists is the best way to
* handle relative loads, since the only way to do so in the legacy API was
* to manually resolve them relative to the `prev` parameter. Doing so will
* almost certainly lead to violations of invariant (3) and possibly (2).
*
* #### Alternatives Considered
*
* To mitigate these risks, we need to have _some_ restriction on when the
* containing URL is available to importers. We considered the following
* alternative restrictions before settling on the current one:
*
* ##### Unavailable for Pre-Resolved Loads
*
* **Don't provide the containing URL when the `canonicalize()` function is
* called for pre-resolved relative loads.** When the user loads a relative URL,
* the Sass compiler first resolves that URL against the current canonical URL
* and passes the resulting absolute URL to the current importer's
* `canonicalize()` function. This invocation would not have access to the
* containing URL; all other invocations would, including when Sass passes the
* relative URL as-is to `canonicalize()`.
*
* This mitigates risk (2) by ensuring that all relative URL resolution is
* handled by the compiler by default. The importer will be invoked with an
* absolute URL and no containing URL first for each relative load, which will
* break for any importers that naïvely try to use the containing URL in all
* cases.
*
* This has several drawbacks. First, a badly-behaved importer could work around
* this by returning `null` for all relative loads and then manually resolving
* relative URLs as part of its load path resolution, thus continuing to violate
* invariant (3). Second, this provides no protection against risk (1) since the
* stylesheet author may still directly load a canonical URL.
*
* ##### Unavailable for Absolute Loads
*
* **Don't provide the containing URL when the `canonicalize()` function is
* being called for any absolute URL.** Since relative loads always pass
* absolute URLs to their importers first, this is a superset of "Unavailable
* for Pre-Resolved Loads". In addition, it protects against risk (1) by
* ensuring that all absolute URLs (which are a superset of canonical URLs) are
* canonicalized without regard to context.
*
* However, this limits the functionality of importers that use a custom URL
* scheme for *non-canonical* URLs. For example, if we choose to support
* [package imports] by claiming the `pkg:` scheme as a "built-in package
* importer", implementations of this scheme wouldn't be able to do
* context-sensitive resolution. This would make the scheme useless for
* supporting Node-style resolution, a core use-case. Given that we want to
* encourage users to use URL schemes rather than relative URLs, this is a
* blocking limitation.
*
* [package imports]: https://github.com/sass/sass/issues/2739
*
* Thus we arrive at the actual behavior, which makes the containing URL
* unavailable for absolute loads _unless_ they have a URL scheme declared
* explicitly non-canonical. This supports the `pkg:` use-case while still
* protecting against risk (1), since the containing URL is never available for
* canonical resolutions.
*/
import {PromiseOr} from '../spec/js-api/util/promise_or';
declare module '../spec/js-api/importer' {
/**
* Replace the first two bullet points for [invoking an importer with a
* string] with:
*
* [invoking an importer with a string]: ../spec/js-api/importer.d.ts
*
* - Let `fromImport` be `true` if the importer is being run for an `@import`
* and `false` otherwise.
*
* - If `string` is a relative URL, or if it's an absolute URL whose scheme is
* non-canonical for this importer, let `containingUrl` be the canonical URL
* of the [current source file]. Otherwise, or if the current source file
* has no canonical URL, let `containingUrl` be undefined.
*
* [current source file]: ../spec/spec.md#current-source-file
*
* - Let `url` be the result of calling `canonicalize` with `string`,
* `fromImport`, and `containingUrl`. If it returns a promise, wait for it
* to complete and use its value instead, or rethrow its error if it
* rejects.
*/
interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
/**
* The set of URL schemes that are considered *non-canonical* for this
* importer. If this is a single string, treat it as a list containing that
* string.
*
* Before beginning compilation, throw an error if any element of this
* contains a character other than a lowercase ASCII letter, an ASCII
* numeral, U+002B (`+`), U+002D (`-`), or U+002E (`.`).
*
* > Uppercase letters are normalized to lowercase in the `URL` constructor,
* > so for simplicity and efficiency we only allow lowercase here.
*/
nonCanonicalScheme?: string | string[];
canonicalize(
url: string,
options: {fromImport: boolean; containingUrl?: URL}
): PromiseOr<URL | null, sync>;
}
}

211
proposal/prev-url.d.ts.md Normal file
View File

@ -0,0 +1,211 @@
# Containing URL: Draft 1.0
*([Issue](https://github.com/sass/sass/issues/3247))*
```ts
import {PromiseOr} from '../spec/js-api/util/promise_or';
```
## Table of Contents
* [Background](#background)
* [Summary](#summary)
* [Design Decisions](#design-decisions)
* [Invariants](#invariants)
* [Risks](#risks)
* [Alternatives Considered](#alternatives-considered)
* [Unavailable for Pre-Resolved Loads](#unavailable-for-pre-resolved-loads)
* [Unavailable for Absolute Loads](#unavailable-for-absolute-loads)
* [Types](#types)
* [`Importer`](#importer)
* [`nonCanonicalScheme`](#noncanonicalscheme)
* [`canonicalize`](#canonicalize)
## Background
> This section is non-normative.
Among many other changes, the [new importer API] dropped an importer's ability
to access the URL of the stylesheet that contained the load, known in the legacy
API as the "previous URL". This was an intentional design choice which enforced
the invariant that the same canonical URL always refers to the same file.
[new import API]: ../accepted/new-js-importer.d.ts
However, this restriction makes it difficult for importers to work as expected
in certain contexts. For example, in the Node.js ecosystem JS loads depend on
the structure of the `node_modules` directory closest to the containing file.
The new import API can't match this behavior.
This is particularly problematic for the widely-used Webpack importer, which
expands on the concept of directory-specific load contexts to allow users to do
fine-grained customization of how differnt files will load their dependencies.
In order to ease migration to the new API for this plugin and its users, and to
better match external ecosystems' load semantics, a solution is needed.
## Summary
> This section is non-normative.
This proposal adds an additional option to the `Importer.canonicalize()` API
that provides the canonical URL of the containing file (the "containing URL").
However, in order to preserve the desired invariants, this option is only
provided when either:
- `Importer.canonicalize()` is being passed a relative URL (which means the URL
has already been tried as a load relative to the current canonical URL), or
- `Importer.canonicalize()` is passed an absolute URL whose scheme the importer
has declared as non-canonical.
A "non-canonical" scheme is a new concept introduced by this proposals.
Importers will optionally be able to provide a `nonCanonicalScheme` field which
will declare one or more URL schemes that they'll never return from
`canonicalize()`. (If they do, Sass will throw an error.)
### Design Decisions
#### Invariants
The specific restrictions for this API were put in place to preserve the
following invariants:
1. There must be a one-to-one mapping between canonical URLs and stylesheets.
This means that even when a user loads a stylesheet using a relative URL,
that stylesheet must have an absolute canonical URL associated with it *and*
loading that canonical URL must return the same stylesheet. This means that
any stylesheet can *always* be unambiguously loaded using its canonical URL.
2. Relative URLs are resolved like paths and HTTP URLs. For example, within
`scheme:a/b/c.scss`, the URL `../d` should be resolved to `scheme:a/d`.
3. Loads relative to the current stylesheet always take precedence over loads
from importers, so if `scheme:a/b/x.scss` exists then `@use "x"` within
`scheme:a/b/c.scss` will always load it.
#### Risks
Providing access to the containing URL puts these invariants at risk in two ways:
1. Access to the containing URL in addition to a canonical URL makes it possible
for importer authors to handle the same canonical URL differently depending
in different contexts, violating invariant (1).
2. It's likely that importer authors familiar with the legacy API will
incorrectly assume that any containing URL that exists is the best way to
handle relative loads, since the only way to do so in the legacy API was to
manually resolve them relative to the `prev` parameter. Doing so will almost
certainly lead to violations of invariant (3) and possibly (2).
#### Alternatives Considered
To mitigate these risks, we need to have _some_ restriction on when the
containing URL is available to importers. We considered the following
alternative restrictions before settling on the current one:
##### Unavailable for Pre-Resolved Loads
**Don't provide the containing URL when the `canonicalize()` function is called
for pre-resolved relative loads.** When the user loads a relative URL, the Sass
compiler first resolves that URL against the current canonical URL and passes
the resulting absolute URL to the current importer's `canonicalize()` function.
This invocation would not have access to the containing URL; all other
invocations would, including when Sass passes the relative URL as-is to
`canonicalize()`.
This mitigates risk (2) by ensuring that all relative URL resolution is handled
by the compiler by default. The importer will be invoked with an absolute URL
and no containing URL first for each relative load, which will break for any
importers that naïvely try to use the containing URL in all cases.
This has several drawbacks. First, a badly-behaved importer could work around
this by returning `null` for all relative loads and then manually resolving
relative URLs as part of its load path resolution, thus continuing to violate
invariant (3). Second, this provides no protection against risk (1) since the
stylesheet author may still directly load a canonical URL.
##### Unavailable for Absolute Loads
**Don't provide the containing URL when the `canonicalize()` function is being
called for any absolute URL.** Since relative loads always pass absolute URLs to
their importers first, this is a superset of "Unavailable for Pre-Resolved
Loads". In addition, it protects against risk (1) by ensuring that all absolute
URLs (which are a superset of canonical URLs) are canonicalized without regard
to context.
However, this limits the functionality of importers that use a custom URL scheme
for *non-canonical* URLs. For example, if we choose to support [package imports]
by claiming the `pkg:` scheme as a "built-in package importer", implementations
of this scheme wouldn't be able to do context-sensitive resolution. This would
make the scheme useless for supporting Node-style resolution, a core use-case.
Given that we want to encourage users to use URL schemes rather than relative
URLs, this is a blocking limitation.
[package imports]: https://github.com/sass/sass/issues/2739
Thus we arrive at the actual behavior, which makes the containing URL
unavailable for absolute loads _unless_ they have a URL scheme declared
explicitly non-canonical. This supports the `pkg:` use-case while still
protecting against risk (1), since the containing URL is never available for
canonical resolutions.
## Types
```ts
declare module '../spec/js-api/importer' {
```
### `Importer`
Replace the first two bullet points for [invoking an importer with a string]
with:
[invoking an importer with a string]: ../spec/js-api/importer.d.ts.md
* Let `fromImport` be `true` if the importer is being run for an `@import` and
`false` otherwise.
* If `string` is a relative URL, or if it's an absolute URL whose scheme is
non-canonical for this importer, let `containingUrl` be the canonical URL of
the [current source file]. Otherwise, or if the current source file has no
canonical URL, let `containingUrl` be undefined.
[current source file]: ../spec/spec.md#current-source-file
* Let `url` be the result of calling `canonicalize` with `string`, `fromImport`,
and `containingUrl`. If it returns a promise, wait for it to complete and use
its value instead, or rethrow its error if it rejects.
```ts
interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
```
#### `nonCanonicalScheme`
The set of URL schemes that are considered *non-canonical* for this importer. If
this is a single string, treat it as a list containing that string.
Before beginning compilation, throw an error if any element of this contains a
character other than a lowercase ASCII letter, an ASCII numeral, U+002B (`+`),
U+002D (`-`), or U+002E (`.`).
> Uppercase letters are normalized to lowercase in the `URL` constructor, so for
> simplicity and efficiency we only allow lowercase here.
```ts
nonCanonicalScheme?: string | string[];
```
#### `canonicalize`
```ts
canonicalize(
url: string,
options: {fromImport: boolean; containingUrl?: URL}
): PromiseOr<URL | null, sync>;
}
```
```ts
} // module
```

View File

@ -1,111 +0,0 @@
import {RawSourceMap} from 'source-map-js'; // https://www.npmjs.com/package/source-map-js
import {Options, StringOptions} from './options';
/** The object returned by the compiler when a Sass compilation succeeds. */
export interface CompileResult {
css: string;
loadedUrls: URL[];
sourceMap?: RawSourceMap;
}
/**
* Compiles the Sass file at `path`:
*
* - If any object in `options.importers` has both `findFileUrl` and
* `canonicalize` fields, throw an error.
*
* - Let `css` be the result of [compiling `path`] with `options.importers` as
* `importers` and `options.loadPaths` as `load-paths`. The compiler must
* respect the configuration specified by the `options` object.
*
* [compiling `path`]: ../spec/spec.md#compiling-a-path
*
* - If the compilation succeeds, return a `CompileResult` object composed as
* follows:
*
* - Set `CompileResult.css` to `css`.
*
* - Set `CompileResult.loadedUrls` to a list of unique canonical URLs of
* source files [loaded] during the compilation. The order of URLs is not
* guaranteed.
*
* [loaded]: ../spec/modules.md#loading-a-source-file
*
* - If `options.sourceMap` is `true`, set `CompileResult.sourceMap` to a
* sourceMap object describing how sections of the Sass input correspond to
* sections of the CSS output.
*
* > The structure of the sourceMap can vary from implementation to
* > implementation.
*
* - Otherwise, throw an `Exception`.
*/
export function compile(path: string, options?: Options<'sync'>): CompileResult;
/**
* Like `compile`, but runs asynchronously.
*
* The compiler must support asynchronous plugins when running in this mode.
*/
export function compileAsync(
path: string,
options?: Options<'async'>
): Promise<CompileResult>;
/**
* Compiles the Sass `source`:
*
* - If `options.importer` or any object in `options.importers` has both
* `findFileUrl` and `canonicalize` fields, throw an error.
*
* - Let `css` be the result of [compiling a string] with:
* - `options.source` as `string`;
* - `options.syntax` as `syntax`, or "scss" if `options.syntax` is not set;
* - `options.url` as `url`;
* - `options.importer` as `importer`;
* - `options.importers` as `importers`;
* - `options.loadPaths` as `load-paths`.
* The compiler must respect the configuration specified by the `options`
* object.
*
* [compiling a string]: ../spec/spec.md#compiling-a-string
*
* - If the compilation succeeds, return a `CompileResult` object composed as
* follows:
*
* - Set `CompileResult.css` to `css`.
*
* - Set `CompileResult.loadedUrls` to a list of unique canonical URLs of
* source files [loaded] during the compilation. The order of URLs is not
* guaranteed.
* - If `options.url` is set, include it in the list.
* - Otherwise, do not include a URL for `source`.
*
* [loaded]: ../spec/modules.md#loading-a-source-file
*
* - If `options.sourceMap` is `true`, set `CompileResult.sourceMap` to a
* sourceMap object describing how sections of the Sass input correspond to
* sections of the CSS output.
*
* > The structure of the sourceMap can vary from implementation to
* > implementation.
*
* - If the compilation fails, throw an `Exception`.
*/
export function compileString(
source: string,
options?: StringOptions<'sync'>
): CompileResult;
/**
* Like `compileString`, but runs asynchronously.
*
* The compiler must support asynchronous plugins when running in this mode.
*/
export function compileStringAsync(
source: string,
options?: StringOptions<'async'>
): Promise<CompileResult>;

150
spec/js-api/compile.d.ts.md Normal file
View File

@ -0,0 +1,150 @@
# Compile API
> These APIs are the entrypoints for compiling Sass to CSS.
```ts
import {RawSourceMap} from 'source-map-js'; // https://www.npmjs.com/package/source-map-js
import {Options, StringOptions} from './options';
```
## Table of Contents
* [Types](#types)
* [`CompileResult`](#compileresult)
* [Functions](#functions)
* [`compile`](#compile)
* [`compileAsync`](#compileasync)
* [`compileString`](#compilestring)
* [`compileStringAsync`](#compilestringasync)
## Types
### `CompileResult`
The object returned by the compiler when a Sass compilation succeeds.
```ts
export interface CompileResult {
css: string;
loadedUrls: URL[];
sourceMap?: RawSourceMap;
}
```
## Functions
### `compile`
Compiles the Sass file at `path`:
* If any object in `options.importers` has both `findFileUrl` and `canonicalize`
fields, throw an error.
* Let `css` be the result of [compiling `path`] with `options.importers` as
`importers` and `options.loadPaths` as `load-paths`. The compiler must respect
the configuration specified by the `options` object.
[compiling `path`]: ../spec.md#compiling-a-path
* If the compilation succeeds, return a `CompileResult` object composed as
follows:
* Set `CompileResult.css` to `css`.
* Set `CompileResult.loadedUrls` to a list of unique canonical URLs of source
files [loaded] during the compilation. The order of URLs is not guaranteed.
[loaded]: ../modules.md#loading-a-source-file
* If `options.sourceMap` is `true`, set `CompileResult.sourceMap` to a
sourceMap object describing how sections of the Sass input correspond to
sections of the CSS output.
> The structure of the sourceMap can vary from implementation to
> implementation.
* Otherwise, throw an `Exception`.
```ts
export function compile(path: string, options?: Options<'sync'>): CompileResult;
```
### `compileAsync`
Like [`compile`], but runs asynchronously.
[`compile`]: #compile
The compiler must support asynchronous plugins when running in this mode.
```ts
export function compileAsync(
path: string,
options?: Options<'async'>
): Promise<CompileResult>;
```
### `compileString`
Compiles the Sass `source`:
* If `options.importer` or any object in `options.importers` has both
`findFileUrl` and `canonicalize` fields, throw an error.
* Let `css` be the result of [compiling a string] with:
* `options.source` as `string`;
* `options.syntax` as `syntax`, or "scss" if `options.syntax` is not set;
* `options.url` as `url`;
* `options.importer` as `importer`;
* `options.importers` as `importers`;
* `options.loadPaths` as `load-paths`.
The compiler must respect the configuration specified by the `options` object.
[compiling a string]: ../spec.md#compiling-a-string
* If the compilation succeeds, return a `CompileResult` object composed as
follows:
* Set `CompileResult.css` to `css`.
* Set `CompileResult.loadedUrls` to a list of unique canonical URLs of source
files [loaded] during the compilation. The order of URLs is not guaranteed.
* If `options.url` is set, include it in the list.
* Otherwise, do not include a URL for `source`.
* If `options.sourceMap` is `true`, set `CompileResult.sourceMap` to a
sourceMap object describing how sections of the Sass input correspond to
sections of the CSS output.
> The structure of the sourceMap can vary from implementation to
> implementation.
[loaded]: ../modules.md#loading-a-source-file
* If the compilation fails, throw an `Exception`.
```ts
export function compileString(
source: string,
options?: StringOptions<'sync'>
): CompileResult;
```
### `compileStringAsync`
Like `compileString`, but runs asynchronously.
The compiler must support asynchronous plugins when running in this mode.
```ts
export function compileStringAsync(
source: string,
options?: StringOptions<'async'>
): Promise<CompileResult>;
```

View File

@ -1,57 +0,0 @@
import {SourceSpan} from './logger';
/**
* The error thrown by the compiler when a Sass compilation fails. This should
* *not* be thrown for errors that occur outside of Sass compilation, such as
* argument verification errors.
*/
export class Exception extends Error {
private constructor();
/**
* The compiler supplies this error message to the JS runtime. This should
* contain the description of the Sass exception as well as human-friendly
* representations of `span` and `sassStack` (if they're set).
*
* This message must be passed directly to the super constructor.
*
* > The format can vary from implementation to implementation.
*/
message: string;
/**
* The Sass error message, excluding the human-friendly representation of
* `span` and `sassStack`.
*
* > The format can vary from implementation to implementation.
*/
readonly sassMessage: string;
/**
* A human-friendly representation of the loads, function calls, and mixin
* includes that were active when this error was thrown.
*
* > The format can vary from implementation to implementation.
*/
readonly sassStack: string;
/**
* A span whose `url` is the canonical URL of the stylesheet being parsed or
* evaluated, and whose `start` points to the line in that stylesheet on which
* the error occurred.
*
* > The other details of this span can vary from implementation to
* > implementation, but implementations are strongly encouraged to ensure
* > that this covers a span of text that clearly indicates the location of
* > the error.
*/
readonly span: SourceSpan;
/**
* Provides a formatted string with useful information about the error.
*
* > This likely includes the Sass error message, span, and stack. The format
* > can vary from implementation to implementation.
*/
toString(): string; // TODO(awjin): Mark this as `override` once TS 4.3 is released.
}

View File

@ -0,0 +1,93 @@
# Exception API
The error thrown by the compiler when a Sass compilation fails. This should
*not* be thrown for errors that occur outside of Sass compilation, such as
argument verification errors.
```ts
import {SourceSpan} from './logger';
```
## Table of Contents
* [Types](#types)
* [`Exception`](#exception)
* [`message`](#message)
* [`sassMessage`](#sassmessage)
* [`sassStack`](#sassstack)
* [`span`](#span)
* [`toString()`](#tostring)
## Types
### `Exception`
```ts
export class Exception extends Error {
private constructor();
```
#### `message`
The compiler supplies this error message to the JS runtime. This should contain
the description of the Sass exception as well as human-friendly representations
of `span` and `sassStack` (if they're set).
This message must be passed directly to the super constructor.
> The format can vary from implementation to implementation.
```ts
message: string;
```
#### `sassMessage`
The Sass error message, excluding the human-friendly representation of `span`
and `sassStack`.
> The format can vary from implementation to implementation.
```ts
readonly sassMessage: string;
```
#### `sassStack`
A human-friendly representation of the loads, function calls, and mixin includes
that were active when this error was thrown.
> The format can vary from implementation to implementation.
```ts
readonly sassStack: string;
```
#### `span`
A span whose `url` is the canonical URL of the stylesheet being parsed or
evaluated, and whose `start` points to the line in that stylesheet on which the
error occurred.
> The other details of this span can vary from implementation to implementation,
> but implementations are strongly encouraged to ensure that this covers a span
> of text that clearly indicates the location of the error.
```ts
readonly span: SourceSpan;
```
#### `toString()`
Provides a formatted string with useful information about the error.
> This likely includes the Sass error message, span, and stack. The format can
> vary from implementation to implementation.
```ts
toString(): string; // TODO(awjin): Mark this as `override` once TS 4.3 is released.
```
```ts
} // Exception
```

View File

@ -1,113 +0,0 @@
import {Syntax} from './options';
import {PromiseOr} from './util/promise_or';
/**
* This interface represents an [importer]. When the importer is invoked with a
* string `string`:
*
* [importer]: ../spec/modules.md#importer
*
* - If `string` is an absolute URL whose scheme is `file`:
*
* - Let `url` be string.
*
* - Otherwise:
*
* - Let `fromImport` be `true` if the importer is being run for an `@import`
* and `false` otherwise.
*
* - Let `url` be the result of calling `findFileUrl` with `string` and
* `fromImport`. If it returns a promise, wait for it to complete and use
* its value instead, or rethrow its error if it rejects.
*
* - If `url` is null, return null.
*
* - If `url`'s scheme is not `file`, throw an error.
*
* - Let `resolved` be the result of [resolving `url`].
*
* - If `resolved` is null, return null.
*
* - Let `text` be the contents of the file at `resolved`.
*
* - Let `syntax` be:
* - "scss" if `url` ends in `.scss`.
* - "indented" if `url` ends in `.sass`.
* - "css" if `url` ends in `.css`.
*
* > The algorithm for resolving a `file:` URL guarantees that `url` will have
* > one of these extensions.
*
* - Return `text`, `syntax`, and `resolved`.
*
* [resolving `url`]: ../spec/modules.md#resolving-a-file-url
*/
export interface FileImporter<
sync extends 'sync' | 'async' = 'sync' | 'async'
> {
findFileUrl(
url: string,
options: {fromImport: boolean}
): PromiseOr<URL | null, sync>;
canonicalize?: never;
}
/**
* This interface represents an [importer]. When the importer is invoked with a
* string `string`:
*
* [importer]: ../spec/modules.md#importer
*
* - Let `fromImport` be `true` if the importer is being run for an `@import`
* and `false` otherwise.
*
* - Let `url` be the result of calling `canonicalize` with `url` and
* `fromImport`. If it returns a promise, wait for it to complete and use its
* value instead, or rethrow its error if it rejects.
*
* - If `url` is null, return null.
*
* - Let `result` be the result of calling `load` with `url`. If it returns a
* promise, wait for it to complete and use its value instead, or rethrow its
* error if it rejects.
*
* - If `result` is null, return null.
*
* - Let `syntax` be `result.syntax`.
*
* - Throw an error if `syntax` is not "scss", "indented", or "css".
*
* - Otherwise, throw an error.
*
* - Return `result.contents`, `syntax`, and `url`.
*
* [resolving `url`]: ../spec/modules.md#resolving-a-file-url
*/
export interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
canonicalize(
url: string,
options: {fromImport: boolean}
): PromiseOr<URL | null, sync>;
load(canonicalUrl: URL): PromiseOr<ImporterResult | null, sync>;
findFileUrl?: never;
}
export interface ImporterResult {
/** The contents of stylesheet loaded by an importer. */
contents: string;
/** The syntax to use to parse `css`. */
syntax: Syntax;
/**
* A browser-accessible URL indicating the resolved location of the imported
* stylesheet.
*
* The implementation must use this URL in source maps to refer to source
* spans in `css`.
*/
sourceMapUrl?: URL;
}

View File

@ -0,0 +1,126 @@
# Importer API
> Interfaces for user-declared importers that customize how Sass loads
> stylesheet dependencies.
```ts
import {Syntax} from './options';
import {PromiseOr} from './util/promise_or';
```
## Table of Contents
* [Types](#types)
* [`FileImporter`](#fileimporter)
* [`Importer`](#importer)
* [`ImporterResult`](#importerresult)
## Types
### `FileImporter`
This interface represents an [importer]. When the importer is invoked with a
string `string`:
[importer]: ../modules.md#importer
* If `string` is an absolute URL whose scheme is `file`:
* Let `url` be string.
* Otherwise:
* Let `fromImport` be `true` if the importer is being run for an `@import` and
`false` otherwise.
* Let `url` be the result of calling `findFileUrl` with `string` and
`fromImport`. If it returns a promise, wait for it to complete and use its
value instead, or rethrow its error if it rejects.
* If `url` is null, return null.
* If `url`'s scheme is not `file`, throw an error.
* Let `resolved` be the result of [resolving `url`].
[resolving `url`]: ../modules.md#resolving-a-file-url
* If `resolved` is null, return null.
* Let `text` be the contents of the file at `resolved`.
* Let `syntax` be:
* "scss" if `url` ends in `.scss`.
* "indented" if `url` ends in `.sass`.
* "css" if `url` ends in `.css`.
> The algorithm for resolving a `file:` URL guarantees that `url` will have
> one of these extensions.
* Return `text`, `syntax`, and `resolved`.
```ts
export interface FileImporter<
sync extends 'sync' | 'async' = 'sync' | 'async'
> {
findFileUrl(
url: string,
options: {fromImport: boolean}
): PromiseOr<URL | null, sync>;
canonicalize?: never;
}
```
### `Importer`
This interface represents an [importer]. When the importer is invoked with a
string `string`:
* Let `fromImport` be `true` if the importer is being run for an `@import` and
`false` otherwise.
* Let `url` be the result of calling `canonicalize` with `url` and `fromImport`.
If it returns a promise, wait for it to complete and use its value instead, or
rethrow its error if it rejects.
* If `url` is null, return null.
* Let `result` be the result of calling `load` with `url`. If it returns a
promise, wait for it to complete and use its value instead, or rethrow its
error if it rejects.
* If `result` is null, return null.
* Throw an error if `result.syntax` is not "scss", "indented", or "css".
* If `result.sourceMapUrl` is defined and the implementation generates a source
map, the implementation must use this URL in the source map to refer to source
spans in `result.contents`.
* Return `result.contents`, `result.syntax`, and `url`.
```ts
export interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
canonicalize(
url: string,
options: {fromImport: boolean}
): PromiseOr<URL | null, sync>;
load(canonicalUrl: URL): PromiseOr<ImporterResult | null, sync>;
findFileUrl?: never;
}
```
### `ImporterResult`
```ts
export interface ImporterResult {
contents: string;
syntax: Syntax;
sourceMapUrl?: URL;
}
```

105
spec/js-api/index.d.ts vendored
View File

@ -1,105 +0,0 @@
/**
* # JavaScript API
*
* Sass implementations that are available for use via JavaScript must expose
* the following JavaScript API. As with the rest of this specification, they
* must not add custom extensions that aren't shared across all implementations.
*
* > Having a shared, consistent API makes it easy for users to move between
* > Sass implementations with minimal disruption, and for build system plugins
* > to seamlessly work with multiple implementations.
*
* The JS API is specified as a TypeScript type declaration. Implementations
* must adhere to this declaration and to the behavioral specifications written
* in JSDoc comments on the declarations. Implementations may throw errors when
* user code passes in values that don't adhere to the type declaration, but
* unless otherwise indicated they may also handle these values in undefined
* ways in accordance with the common JavaScript pattern of avoiding explicit
* type checks. This must not be used as a way of adding custom extensions that
* aren't shared across all implementations.
*
* Certain interfaces in the JS API are defined within the `legacy` directory,
* indicating that they're part of the legacy Node Sass API. This API is
* deprecated and implementations are not required to support it. However, at
* least partial support is recommended for compatibility with older
* applications and particularly build system plugins.
*
* As with other sections of this specification, the specification of the legacy
* JS API is incomplete, and is added to *lazily*. This means that portions of
* the specparticularly the documentation comments that serve as a behavioral
* specificationare only written when they're necessary as background for new
* API proposals. */
export {
CompileResult,
compile,
compileAsync,
compileString,
compileStringAsync,
} from './compile';
export {Exception} from './exception';
export {FileImporter, Importer, ImporterResult} from './importer';
export {Logger, SourceSpan, SourceLocation} from './logger';
export {
CustomFunction,
Options,
OutputStyle,
StringOptions,
StringOptionsWithImporter,
StringOptionsWithoutImporter,
Syntax,
} from './options';
export {PromiseOr} from './util/promise_or';
export {
ListSeparator,
SassArgumentList,
SassBoolean,
SassColor,
SassFunction,
SassList,
SassMap,
SassNumber,
SassString,
Value,
sassFalse,
sassNull,
sassTrue,
} from './value';
// Legacy APIs
export {LegacyException} from './legacy/exception';
export {
FALSE,
LegacyAsyncFunction,
LegacyAsyncFunctionDone,
LegacyFunction,
LegacySyncFunction,
LegacyValue,
NULL,
TRUE,
types,
} from './legacy/function';
export {
LegacyAsyncImporter,
LegacyImporter,
LegacyImporterResult,
LegacyImporterThis,
LegacySyncImporter,
} from './legacy/importer';
export {
LegacySharedOptions,
LegacyFileOptions,
LegacyStringOptions,
LegacyOptions,
} from './legacy/options';
export {LegacyPluginThis} from './legacy/plugin_this';
export {LegacyResult, render, renderSync} from './legacy/render';
/**
* Information about the Sass implementation. This must begin with a unique
* identifier for this package (typically but not necessarily the npm package
* name), followed by U+0009 TAB, followed by its npm package version. It may
* contain another tab character followed by additional information, but this is
* not required.
*/
export const info: string;

123
spec/js-api/index.d.ts.md Normal file
View File

@ -0,0 +1,123 @@
# JavaScript API
Sass implementations that are available for use via JavaScript must expose the
following JavaScript API. As with the rest of this specification, they must not
add custom extensions that aren't shared across all implementations.
> Having a shared, consistent API makes it easy for users to move between Sass
> implementations with minimal disruption, and for build system plugins to
> seamlessly work with multiple implementations.
The JS API is specified as a TypeScript type declaration. Implementations must
adhere to this declaration and to the behavioral specifications written in JSDoc
comments on the declarations. Implementations may throw errors when user code
passes in values that don't adhere to the type declaration, but unless otherwise
indicated they may also handle these values in undefined ways in accordance with
the common JavaScript pattern of avoiding explicit type checks. This must not be
used as a way of adding custom extensions that aren't shared across all
implementations.
Certain interfaces in the JS API are defined within the `legacy` directory,
indicating that they're part of the legacy Node Sass API. This API is deprecated
and implementations are not required to support it. However, at least partial
support is recommended for compatibility with older applications and
particularly build system plugins.
As with other sections of this specification, the specification of the legacy JS
API is incomplete, and is added to *lazily*. This means that portions of the
spec—particularly the documentation comments that serve as a behavioral
specification—are only written when they're necessary as background for new API
proposals.
## Table of Contents
* [Modern APIs](#modern-apis)
* [Legacy APIs](#legacy-apis)
* [Top-Level Members](#top-level-members)
* [`info`](#info)
## Modern APIs
```ts
export {
CompileResult,
compile,
compileAsync,
compileString,
compileStringAsync,
} from './compile';
export {Exception} from './exception';
export {FileImporter, Importer, ImporterResult} from './importer';
export {Logger, SourceSpan, SourceLocation} from './logger';
export {
CustomFunction,
Options,
OutputStyle,
StringOptions,
StringOptionsWithImporter,
StringOptionsWithoutImporter,
Syntax,
} from './options';
export {PromiseOr} from './util/promise_or';
export {
ListSeparator,
SassArgumentList,
SassBoolean,
SassColor,
SassFunction,
SassList,
SassMap,
SassNumber,
SassString,
Value,
sassFalse,
sassNull,
sassTrue,
} from './value';
```
## Legacy APIs
```ts
export {LegacyException} from './legacy/exception';
export {
FALSE,
LegacyAsyncFunction,
LegacyAsyncFunctionDone,
LegacyFunction,
LegacySyncFunction,
LegacyValue,
NULL,
TRUE,
types,
} from './legacy/function';
export {
LegacyAsyncImporter,
LegacyImporter,
LegacyImporterResult,
LegacyImporterThis,
LegacySyncImporter,
} from './legacy/importer';
export {
LegacySharedOptions,
LegacyFileOptions,
LegacyStringOptions,
LegacyOptions,
} from './legacy/options';
export {LegacyPluginThis} from './legacy/plugin_this';
export {LegacyResult, render, renderSync} from './legacy/render';
```
## Top-Level Members
### `info`
Information about the Sass implementation. This must begin with a unique
identifier for this package (typically but not necessarily the npm package
name), followed by U+0009 TAB, followed by its npm package version. It may
contain another tab character followed by additional information, but this is
not required.
```ts
export const info: string;
```

View File

@ -1,39 +0,0 @@
import {LegacyPluginThis} from './plugin_this';
/**
* The interface for the `this` keyword for custom importers. The implementation
* must invoke importers with an appropriate `this`.
*/
interface LegacyImporterThis extends LegacyPluginThis {
/**
* `true` if this importer invocation was caused by an `@import` statement and
* `false` otherwise.
*
* > This allows importers to look for `.import.scss` stylesheets if and only
* > if an `@import` is being resolved.
*/
fromImport: boolean;
}
export type LegacyImporterResult =
| {file: string}
| {contents: string}
| Error
| null;
type LegacySyncImporter = (
this: LegacyImporterThis,
url: string,
prev: string
) => LegacyImporterResult;
type LegacyAsyncImporter = (
this: LegacyImporterThis,
url: string,
prev: string,
done: (result: LegacyImporterResult) => void
) => void;
export type LegacyImporter<sync = 'sync' | 'async'> = sync extends 'async'
? LegacySyncImporter | LegacyAsyncImporter
: LegacySyncImporter;

View File

@ -0,0 +1,81 @@
# Legacy Importer API
```ts
import {LegacyPluginThis} from './plugin_this';
```
## Table of Contents
* [Types](#types)
* [`LegacyImporterThis`](#legacyimporterthis)
* [`fromImport`](#fromimport)
* [`LegacyImporterResult`](#legacyimporterresult)
* [`LegacySyncImporter`](#legacysyncimporter)
* [`LegacyAsyncImporter`](#legacyasyncimporter)
* [`LegacyImporter`](#legacyimporter)
## Types
### `LegacyImporterThis`
The interface for the `this` keyword for custom importers. The implementation
must invoke importers with an appropriate `this`.
```ts
interface LegacyImporterThis extends LegacyPluginThis {
```
#### `fromImport`
The implementation must set this field to true if this importer invocation was
caused by an `@import` statement and `false` otherwise.
> This allows importers to look for `.import.scss` stylesheets if and only if an
> `@import` is being resolved.
```ts
fromImport: boolean;
```
```ts
} // LegacyImporterThis
```
### `LegacyImporterResult`
```ts
export type LegacyImporterResult =
| {file: string}
| {contents: string}
| Error
| null;
```
### `LegacySyncImporter`
```ts
type LegacySyncImporter = (
this: LegacyImporterThis,
url: string,
prev: string
) => LegacyImporterResult;
```
### `LegacyAsyncImporter`
```ts
type LegacyAsyncImporter = (
this: LegacyImporterThis,
url: string,
prev: string,
done: (result: LegacyImporterResult) => void
) => void;
```
### `LegacyImporter`
```ts
export type LegacyImporter<sync = 'sync' | 'async'> = sync extends 'async'
? LegacySyncImporter | LegacyAsyncImporter
: LegacySyncImporter;
```

View File

@ -1,95 +0,0 @@
import {Logger} from '../logger';
import {LegacyImporter} from './importer';
import {LegacyFunction} from './function';
/**
* All the options for a Sass compilation except those that specify the specific
* input format.
*/
export interface LegacySharedOptions<sync extends 'sync' | 'async'> {
includePaths?: string[];
indentType?: 'space' | 'tab';
indentWidth?: number;
linefeed?: 'cr' | 'crlf' | 'lf' | 'lfcr';
omitSourceMapUrl?: boolean;
outFile?: string;
outputStyle?: 'compressed' | 'expanded' | 'nested' | 'compact';
sourceMap?: boolean | string;
sourceMapContents?: boolean;
sourceMapEmbed?: boolean;
sourceMapRoot?: string;
importer?: LegacyImporter<sync> | LegacyImporter<sync>[];
functions?: {[key: string]: LegacyFunction<sync>};
/**
* If `true`, the compiler may prepend `@charset "UTF-8";` or U+FEFF
* (byte-order marker) if it outputs non-ASCII CSS.
*
* If `false`, the compiler never emits these byte sequences. This is ideal
* when concatenating or embedding in HTML `<style>` tags. (The output will
* still be UTF-8.)
*
* @default true
*/
charset?: boolean;
/**
* If true, the compiler must not print deprecation warnings for stylesheets
* that are transitively loaded through an import path or importer.
*
* @default false
*/
quietDeps?: boolean;
/**
* If `true`, the compiler must print every single deprecation warning it
* encounters.
*
* If `false`, the compiler may choose not to print repeated deprecation
* warnings.
*
* @default false
*/
verbose?: boolean;
/**
* An object that provides callbacks for the compiler to use in lieu of its
* default messaging behavior.
*
* The compiler must treat an `undefined` logger identically to an object that
* doesn't have `warn` or `debug` fields.
*/
logger?: Logger;
}
export interface LegacyFileOptions<sync extends 'sync' | 'async'>
extends LegacySharedOptions<sync> {
file: string;
data?: never;
}
export interface LegacyStringOptions<sync extends 'sync' | 'async'>
extends LegacySharedOptions<sync> {
data: string;
file?: string;
indentedSyntax?: boolean;
}
export type LegacyOptions<sync extends 'sync' | 'async'> =
| LegacyFileOptions<sync>
| LegacyStringOptions<sync>;

View File

@ -0,0 +1,212 @@
# Legacy API Options
```ts
import {Logger} from '../logger';
import {LegacyImporter} from './importer';
import {LegacyFunction} from './function';
```
## Table of Contents
* [Types](#types)
* [`LegacySharedOptions`](#legacysharedoptions)
* [`includePaths`](#includepaths)
* [`indentType`](#indenttype)
* [`indentWidth`](#indentwidth)
* [`linefeed`](#linefeed)
* [`omitSourceMapUrl`](#omitsourcemapurl)
* [`outFile`](#outfile)
* [`outputStyle`](#outputstyle)
* [`sourceMap`](#sourcemap)
* [`sourceMapContents`](#sourcemapcontents)
* [`sourceMapEmbed`](#sourcemapembed)
* [`sourceMapRoot`](#sourcemaproot)
* [`importer`](#importer)
* [`functions`](#functions)
* [`charset`](#charset)
* [`quietDeps`](#quietdeps)
* [`verbose`](#verbose)
* [`logger`](#logger)
* [`LegacyFileOptions`](#legacyfileoptions)
* [`LegacyStringOptions`](#legacystringoptions)
* [`LegacyOptions`](#legacyoptions)
## Types
### `LegacySharedOptions`
All the options for a legacy Sass compilation except those that specify the
specific input format.
```ts
export interface LegacySharedOptions<sync extends 'sync' | 'async'> {
```
#### `includePaths`
```ts
includePaths?: string[];
```
#### `indentType`
```ts
indentType?: 'space' | 'tab';
```
#### `indentWidth`
```ts
indentWidth?: number;
```
#### `linefeed`
```ts
linefeed?: 'cr' | 'crlf' | 'lf' | 'lfcr';
```
#### `omitSourceMapUrl`
```ts
omitSourceMapUrl?: boolean;
```
#### `outFile`
```ts
outFile?: string;
```
#### `outputStyle`
```ts
outputStyle?: 'compressed' | 'expanded' | 'nested' | 'compact';
```
#### `sourceMap`
```ts
sourceMap?: boolean | string;
```
#### `sourceMapContents`
```ts
sourceMapContents?: boolean;
```
#### `sourceMapEmbed`
```ts
sourceMapEmbed?: boolean;
```
#### `sourceMapRoot`
```ts
sourceMapRoot?: string;
```
#### `importer`
```ts
importer?: LegacyImporter<sync> | LegacyImporter<sync>[];
```
#### `functions`
```ts
functions?: {[key: string]: LegacyFunction<sync>};
```
#### `charset`
If true, the compiler must prepend `@charset "UTF-8";` or U+FEFF (byte-order
marker) if it emits non-ASCII CSS.
If false, the compiler must not prepend these byte sequences.
Defaults to true.
```ts
charset?: boolean;
```
#### `quietDeps`
If true, the compiler must not print deprecation warnings for stylesheets that
are transitively loaded through an import path.
Defaults to false.
```ts
quietDeps?: boolean;
```
#### `verbose`
If true, the compiler must print every single deprecation warning it encounters
(except for those silenced by [`quietDeps`]).
[`quietDeps`]: #quietdeps
If false, the compiler may choose not to print repeated deprecation warnings.
Defaults to false.
```ts
verbose?: boolean;
```
#### `logger`
A [custom logger] that provides callbacks for the compiler to use in lieu of its
default messaging behavior.
[custom logger]: ../logger/index.d.ts.md
The compiler must treat an `undefined` logger identically to an object that
doesn't have `warn` or `debug` fields.
```ts
logger?: Logger;
```
```ts
} // LegacySharedOptions
```
### `LegacyFileOptions`
```ts
export interface LegacyFileOptions<sync extends 'sync' | 'async'>
extends LegacySharedOptions<sync> {
file: string;
data?: never;
}
```
### `LegacyStringOptions`
```ts
export interface LegacyStringOptions<sync extends 'sync' | 'async'>
extends LegacySharedOptions<sync> {
data: string;
file?: string;
indentedSyntax?: boolean;
}
```
### `LegacyOptions`
```ts
export type LegacyOptions<sync extends 'sync' | 'async'> =
| LegacyFileOptions<sync>
| LegacyStringOptions<sync>;
```

View File

@ -1,72 +0,0 @@
/**
* The shared interface for the `this` keyword for custom importers and custom
* functions. The implementation must invoke importers and custom functions with
* an appropriate `this`.
*/
export interface LegacyPluginThis {
options: {
/** The same `LegacyPluginThis` instance that contains this object. */
context: LegacyPluginThis;
/** The `file` option passed to the `render()` or `renderSync()` call. */
file?: string;
/** The `data` option passed to the `render()` or `renderSync()` call. */
data?: string;
/**
* A string that contains the current working directory followed by strings
* passed in the `includePaths` option, separated by `";"` on Windows and
* `":"` elsewhere.
*/
includePaths: string;
precision: 10;
/**
* An integer. The specific semantics of this are left up to the
* implementation. (The reference implementation always returns 1.)
*/
style: 1;
/**
* The number 1 if the `indentType` option was `tab`. The number 0
* otherwise.
*/
indentType: 1 | 0;
/**
* An integer indicating the number of spaces or tabs emitted by the
* compiler for each level of indentation.
*/
indentWidth: number;
/**
* A value based on the `linefeed` option passed to the `render()` or
* `renderSync()`:
*
* * If `linefeed` is `"cr"`, this must be `"\r"`.
* * If `linefeed` is `"crlf"`, this must be `"\r\n"`.
* * If `linefeed` is `"lf"` or `undefined`, this must be `"\n"`.
* * If `linefeed` is `"lfcr"`, this must be `"\n\r"`.
*/
linefeed: '\r' | '\r\n' | '\n' | '\n\r';
result: {
stats: {
/**
* The number of milliseconds since the Unix epoch (1 January 1970
* 00:00:00 UT) at the point at which the user called `render()` or
* `renderSync()`.
*/
start: number;
/**
* The `file` option passed to the `render()` call, or the string
* `"data"` if no file was passed.
*/
entry: string;
};
};
};
}

View File

@ -0,0 +1,156 @@
# Legacy Plugin Context
The shared interface for the `this` keyword for custom importers and custom
functions. The implementation must invoke importers and custom functions with an
appropriate `this`.
## Table of Contents
* [Types](#types)
* [`LegacyPluginThis`](#legacypluginthis)
* [`context`](#context)
* [`file`](#file)
* [`data`](#data)
* [`includePaths`](#includepaths)
* [`precision`](#precision)
* [`style`](#style)
* [`indentType`](#indenttype)
* [`indentWidth`](#indentwidth)
* [`linefeed`](#linefeed)
* [`result`](#result)
* [`result.stats.start`](#resultstatsstart)
* [`result.stats.entry`](#resultstatsentry)
## Types
### `LegacyPluginThis`
This class contains a single field, `options`, which contains all its metadata.
```ts
export interface LegacyPluginThis {
options: {
```
#### `context`
The same `LegacyPluginThis` instance that contains the `options` object.
```ts
context: LegacyPluginThis;
```
#### `file`
The [`file` option] passed to the `render()` or `renderSync()` call.
[`file` option]: options.d.ts.md#legacyfileoptions
```ts
file?: string;
```
#### `data`
The [`data` option] passed to the `render()` or `renderSync()` call.
[`data` option]: options.d.ts.md#legacystringoptions
```ts
data?: string;
```
#### `includePaths`
A string that contains the current working directory followed by strings passed
in the `includePaths` option, separated by `";"` on Windows and `":"` elsewhere.
```ts
includePaths: string;
```
#### `precision`
```ts
precision: 10;
```
#### `style`
The integer 1.
> Older implementations returned other values for this, but that behavior is
> deprecated and should not be reproduced by new implementations.
```ts
style: 1;
```
#### `indentType`
The number 1 if the [`indentType` option] was `'tab'`. The number 0 otherwise.
[`indentType` option]: options.d.ts.md#indenttype
```ts
indentType: 1 | 0;
```
#### `indentWidth`
An integer indicating the number of spaces or tabs emitted by the compiler for
each level of indentation.
```ts
indentWidth: number;
```
#### `linefeed`
A value based on the [`linefeed` option] passed to the `render()` or
`renderSync()`:
[`linefeed` option]: options.d.ts.md#linefeed
* If `linefeed` is `"cr"`, this must be `"\r"`.
* If `linefeed` is `"crlf"`, this must be `"\r\n"`.
* If `linefeed` is `"lf"` or `undefined`, this must be `"\n"`.
* If `linefeed` is `"lfcr"`, this must be `"\n\r"`.
```ts
linefeed: '\r' | '\r\n' | '\n' | '\n\r';
```
#### `result`
An object with a single field, `stats`, which contains several subfields.
```ts
result: {
stats: {
```
##### `result.stats.start`
The number of milliseconds since the Unix epoch (1 January 1970 00:00:00 UT) at
the point at which the user called `render()` or `renderSync()`.
```ts
start: number;
```
##### `result.stats.entry`
The [`file` option] passed to the `render()` call, or the string `"data"` if no
file was passed.
```ts
entry: string;
```
```ts
}; // options.result.stats
}; // options.result
}; // options
} // LegacyPluginThis
```

View File

@ -1,75 +0,0 @@
import {SourceSpan} from './source_span';
export {SourceLocation} from './source_location';
export {SourceSpan} from './source_span';
/**
* An object that provides callbacks for handling messages from the
* compiler.
*/
export interface Logger {
/**
* If this field is defined, the compiler must invoke it under the following
* circumstances:
*
* * When it encounters a `@warn` rule:
* * Let `value` be the result of evaluating the rule's expression.
* * Let `message` be `value`'s text if it's a string, or the result of
* serializing `value` if it's not.
* * Invoke `warn` with `message` and an object with `deprecation` set to
* `false` and `stack` set to a string representation of the current Sass
* stack trace.
*
* > The specific format of the stack trace may vary from implementation
* > to implementation.
*
* * When it encounters anything else that the user needs to be warned about:
*
* > This is intentionally vague about what counts as a warning.
* > Implementations have a considerable degree of flexibility in defining
* > this for themselves, although in some cases warnings are mandated by
* > the specification (such as in preparation for a breaking change).
*
* * Let `options` be an empty object.
* * If this warning is caused by behavior that used to be allowed but will
* be disallowed in the future, set `options.deprecation` to `true`.
* Otherwise, set `options.deprecation` to `false`.
* * If this warning is associated with a specific span of a Sass
* stylesheet, set `options.span` to a `SourceSpan` that covers that span.
* * If this warning occurred during execution of a stylesheet, set
* `options.stack` to a string representation of the current Sass stack
* trace.
* * Invoke `warn` with a string describing the warning and `options`.
*
* If this field is defined, the compiler must not surface warnings in any way
* other than inkoving `warn`.
*/
warn?(
message: string,
options: {
deprecation: boolean;
span?: SourceSpan;
stack?: string;
}
): void;
/**
* If this field is defined, the compiler must invoke it when it encounters a
* `@debug` rule using the following procedure:
*
* * Let `value` be the result of evaluating the rule's expression.
* * Let `message` be `value`'s text if it's a string, or the result of
* serializing `value` if it's not.
* * Invoke `debug` with `message` and an object with `span` set to the span
* covering the `@debug` rule and its expression.
*
* If this field is defined, the compiler must not surface debug messages in
* any way other than invoking `debug`.
*/
debug?(message: string, options: {span: SourceSpan}): void;
}
export namespace Logger {
/** A Logger that does nothing when it warn or debug methods are called. */
export const silent: Logger;
}

View File

@ -0,0 +1,122 @@
# Logger API
```ts
import {SourceSpan} from './source_span';
export {SourceLocation} from './source_location';
export {SourceSpan} from './source_span';
```
## Table of Contents
* [Types](#types)
* [`Logger`](#logger)
* [`warn`](#warn)
* [`debug`](#debug)
* [Fields](#fields)
* [`Logger`](#logger-1)
* [`silent`](#silent)
## Types
### `Logger`
An object that provides callbacks for handling messages from the compiler.
```ts
export interface Logger {
```
#### `warn`
If this field is defined, the compiler must invoke it under the following
circumstances:
* When it encounters a `@warn` rule:
* Let `value` be the result of evaluating the rule's expression.
* Let `message` be `value`'s text if it's a string, or the result of
serializing `value` if it's not.
* Invoke `warn` with `message` and an object with `deprecation` set to `false`
and `stack` set to a string representation of the current Sass stack trace.
> The specific format of the stack trace may vary from implementation to
> implementation.
* When it encounters anything else that the user needs to be warned about:
> This is intentionally vague about what counts as a warning. Implementations
> have a considerable degree of flexibility in defining this for themselves,
> although in some cases warnings are mandated by the specification (such as
> in preparation for a breaking change).
* Let `options` be an empty object.
* If this warning is caused by behavior that used to be allowed but will be
disallowed in the future, set `options.deprecation` to `true`. Otherwise,
set `options.deprecation` to `false`.
* If this warning is associated with a specific span of a Sass stylesheet, set
`options.span` to a `SourceSpan` that covers that span.
* If this warning occurred during execution of a stylesheet, set
`options.stack` to a string representation of the current Sass stack trace.
* Invoke `warn` with a string describing the warning and `options`.
If this field is defined, the compiler must not surface warnings in any way
other than inkoving `warn`.
```ts
warn?(
message: string,
options: {
deprecation: boolean;
span?: SourceSpan;
stack?: string;
}
): void;
```
#### `debug`
If this field is defined, the compiler must invoke it when it encounters a
`@debug` rule using the following procedure:
* Let `value` be the result of evaluating the rule's expression.
* Let `message` be `value`'s text if it's a string, or the result of serializing
`value` if it's not.
* Invoke `debug` with `message` and an object with `span` set to the span
covering the `@debug` rule and its expression.
If this field is defined, the compiler must not surface debug messages in any
way other than invoking `debug`.
```ts
debug?(message: string, options: {span: SourceSpan}): void;
```
```ts
} // interface Logger
```
## Fields
### `Logger`
A namespace for built-in logger implementations.
```ts
export namespace Logger {
```
#### `silent`
A [`Logger`] that does nothing when it warn or debug methods are called.
[`Logger`]: #logger
```ts
export const silent: Logger;
```
```ts
} // namespace Logger
```

View File

@ -1,24 +0,0 @@
/** An interface that represents a location in a text file. */
export interface SourceLocation {
/**
* The 0-based offset of this location within the file it refers to, in terms
* of UTF-16 code units.
*/
offset: number;
/**
* The number of U+000A LINE FEED characters between the beginning of the file
* and `offset`, exclusive.
*
* > In other words, this location's 0-based line.
*/
line: number;
/**
* The number of UTF-16 code points between the last U+000A LINE FEED
* character before `offset` and `offset`, exclusive.
*
* > In other words, this location's 0-based column.
*/
column: number;
}

View File

@ -0,0 +1,54 @@
# Source Location
## Table of Contents
* [Types](#types)
* [`SourceLocation`](#sourcelocation)
* [`offset`](#offset)
* [`line`](#line)
* [`column`](#column)
## Types
### `SourceLocation`
An interface that represents a location in a text file.
```ts
export interface SourceLocation {
```
#### `offset`
The 0-based offset of this location within the file it refers to, in terms of
UTF-16 code units.
```ts
offset: number;
```
#### `line`
The number of U+000A LINE FEED characters between the beginning of the file and
`offset`, exclusive.
> In other words, this location's 0-based line.
```ts
line: number;
```
#### `column`
The number of UTF-16 code points between the last U+000A LINE FEED character
before `offset` and `offset`, exclusive.
> In other words, this location's 0-based column.
```ts
column: number;
```
```ts
} // SourceLocation
```

View File

@ -1,50 +0,0 @@
import {SourceLocation} from './source_location';
/**
* An interface that represents a contiguous section ("span") of a text file.
* This section may be empty if the `start` and `end` are the same location,
* in which case it indicates a single position in the file.
*/
export interface SourceSpan {
/**
* The location of the first character of this span, unless `end` points to
* the same character, in which case the span is empty and refers to the point
* between this character and the one before it.
*/
start: SourceLocation;
/**
* The location of the first character after this span. This must point to a
* location after `start`.
*/
end: SourceLocation;
/**
* The canonical URL of the file that this span refers to. For files on disk,
* this must be a `file://` URL.
*
* This must be `undefined` for files that are passed to the compiler without
* a URL. It must not be `undefined` for any files that are importable.
*/
url?: URL;
/**
* The text covered by the span. This must be the text between `start.offset`
* (inclusive) and `end.offset` (exclusive) of the file referred by this
* span. Its length must be `end.offset - start.offset`.
*/
text: string;
/**
* Additional source text surrounding this span.
*
* The compiler may choose to omit this. If it's not `undefined`, it must
* contain `text`. Furthermore, `text` must begin at column `start.column` of
* a line in `context`.
*
* > This usually contains the full lines the span begins and ends on if the
* > span itself doesn't cover the full lines, but the specific scope is up to
* > the compiler.
*/
context?: string;
}

View File

@ -0,0 +1,88 @@
# Source Span
```ts
import {SourceLocation} from './source_location';
```
## Table of Contents
* [Types](#types)
* [`SourceSpan`](#sourcespan)
* [`start`](#start)
* [`end`](#end)
* [`url`](#url)
* [`text`](#text)
* [`context`](#context)
## Types
### `SourceSpan`
An interface that represents a contiguous section ("span") of a text file. This
section may be empty if the `start` and `end` are the same location, in which
case it indicates a single position in the file.
```ts
export interface SourceSpan {
```
#### `start`
The location of the first character of this span, unless `end` points to the
same character, in which case the span is empty and refers to the point between
this character and the one before it.
```ts
start: SourceLocation;
```
#### `end`
The location of the first character after this span. This must point to a
location after `start`.
```ts
end: SourceLocation;
```
#### `url`
The canonical URL of the file that this span refers to. For files on disk, this
must be a `file://` URL.
This must be `undefined` for files that are passed to the compiler without a
URL. It must not be `undefined` for any files that are importable.
```ts
url?: URL;
```
#### `text`
The text covered by the span. This must be the text between `start.offset`
(inclusive) and `end.offset` (exclusive) of the file referred by this span. Its
length must be `end.offset - start.offset`.
```ts
text: string;
```
#### `context`
Additional source text surrounding this span.
The compiler may choose to omit this. If it's not `undefined`, it must contain
`text`. Furthermore, `text` must begin at column `start.column` of a line in
`context`.
> This usually contains the full lines the span begins and ends on if the span
> itself doesn't cover the full lines, but the specific scope is up to the
> compiler.
```ts
context?: string;
```
```ts
} // SourceSpan
```

View File

@ -1,174 +0,0 @@
import {FileImporter, Importer} from './importer';
import {Logger} from './logger';
import {Value} from './value';
import {PromiseOr} from './util/promise_or';
/** The types of input syntax that the compiler can parse. */
export type Syntax = 'scss' | 'indented' | 'css';
/**
* The ways in which the compiler can format the emitted CSS.
*
* > The specifics of each format can vary from implementation to
* > implementation. If an implementation wants to add a new OutputStyle, they
* > should expand this type.
*/
export type OutputStyle = 'expanded' | 'compressed';
/** A custom function that can be called from Sass stylesheets. */
export type CustomFunction<sync extends 'sync' | 'async'> = (
args: Value[]
) => PromiseOr<Value, sync>;
/**
* All of the options for a Sass compilation that are shared by compiling from a
* path and by compiling from a string.
*/
export interface Options<sync extends 'sync' | 'async'> {
/**
* If true, the compiler must use only ASCII characters in the formatted
* message of errors and logs that aren't handled by a `logger`.
*
* @default false
*/
alertAscii?: boolean;
/**
* If true, the compiler may use terminal colors in the formatted message of
* errors and logs that aren't handled by a `logger`. Implementations may
* choose the default value for this based on their own heuristics of whether
* colored output would be useful or render appropriately. Implementations are
* not obligated to use colors even if this is `true`.
*
* > The specific format can vary from implementation to implementation.
*/
alertColor?: boolean;
/**
* If `true`, the compiler may prepend `@charset "UTF-8";` or U+FEFF
* (byte-order marker) if it outputs non-ASCII CSS.
*
* If `false`, the compiler never emits these byte sequences. This is ideal
* when concatenating or embedding in HTML `<style>` tags. (The output will
* still be UTF-8.)
*
* @default true
*/
charset?: boolean;
/**
* 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.
*
* > As in the rest of Sass, `_`s and `-`s are considered equivalent when
* > determining which function signatures match.
*
* 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.
*
* [<ident-token>]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
*
* 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.
*/
functions?: Record<string, CustomFunction<sync>>;
/** The list of of custom importers to use to resolve file loads. */
importers?: (Importer<sync> | FileImporter<sync>)[];
/** If set, the compiler must use these paths to resolve imports. */
loadPaths?: string[];
/**
* An object that provides callbacks for the compiler to use in lieu of its
* default messaging behavior.
*
* The compiler must treat an `undefined` logger identically to an object
* that doesn't have `warn` or `debug` fields.
*/
logger?: Logger;
/**
* If true, the compiler must not print deprecation warnings for stylesheets
* that are transitively loaded through an import path.
*
* @default false
*/
quietDeps?: boolean;
/**
* If true, the compiler must set the sourceMap field of the `CompileResult`
* to a sourceMap object.
*
* @default false
*/
sourceMap?: boolean;
/**
* If true, the compiler must include the sources in the sourceMap of the
* `CompileResult`.
*
* @default false
*/
sourceMapIncludeSources?: boolean;
/**
* If present, the compiler must format the emitted CSS in this style.
*
* Implementations may support any amount of options, provided that:
* - They support the 'expanded' option.
* - They produce CSS that is semantically equivalent regardless of style.
* - They throw an error if they receive a value for this option that they
* do not support.
*/
style?: OutputStyle;
/**
* If `true`, the compiler must print every single deprecation warning it
* encounters.
*
* If `false`, the compiler may choose not to print repeated deprecation
* warnings.
*
* @default false
*/
verbose?: boolean;
}
export interface StringOptionsWithoutImporter<sync extends 'sync' | 'async'>
extends Options<sync> {
/**
* The compiler must parse `source` using this syntax.
*
* @default 'scss'
*/
syntax?: Syntax;
/** When `importer` isn't passed, this is purely advisory. */
url?: URL;
}
export interface StringOptionsWithImporter<sync extends 'sync' | 'async'>
extends StringOptionsWithoutImporter<sync> {
/** The importer to use to resolve relative imports in the entrypoint. */
importer: Importer<sync> | FileImporter<sync>;
/** The canonical URL of the entrypoint. */
url: URL;
}
export type StringOptions<sync extends 'sync' | 'async'> =
| StringOptionsWithImporter<sync>
| StringOptionsWithoutImporter<sync>;

340
spec/js-api/options.d.ts.md Normal file
View File

@ -0,0 +1,340 @@
## API Options
> The options object that's passed to the [compile API] to control various
> aspects of Sass compilation.
>
> [compile API]: compile.d.ts.md
```ts
import {FileImporter, Importer} from './importer';
import {Logger} from './logger';
import {Value} from './value';
import {PromiseOr} from './util/promise_or';
```
## Table of Contents
* [Types](#types)
* [`Syntax`](#syntax)
* [`OutputStyle`](#outputstyle)
* [`CustomFunction`](#customfunction)
* [`Options`](#options)
* [`alertAscii`](#alertascii)
* [`alertColor`](#alertcolor)
* [`charset`](#charset)
* [`functions`](#functions)
* [`importers`](#importers)
* [`loadPaths`](#loadpaths)
* [`logger`](#logger)
* [`quietDeps`](#quietdeps)
* [`sourceMap`](#sourcemap)
* [`sourceMapIncludeSources`](#sourcemapincludesources)
* [`style`](#style)
* [`verbose`](#verbose)
* [`StringOptionsWithoutImporter`](#stringoptionswithoutimporter)
* [`syntax`](#syntax)
* [`url`](#url)
* [`StringOptionsWithImporter`](#stringoptionswithimporter)
* [`importer`](#importer)
* [`url`](#url-1)
* [`StringOptions`](#stringoptions)
## Types
### `Syntax`
The types of input syntax that the compiler can parse.
```ts
export type Syntax = 'scss' | 'indented' | 'css';
```
### `OutputStyle`
The ways in which the compiler can format the emitted CSS. See [`Options.style`]
for details.
[`Options.style`]: #style
```ts
export type OutputStyle = 'expanded' | 'compressed';
```
### `CustomFunction`
A custom function that can be called from Sass stylesheets.
```ts
export type CustomFunction<sync extends 'sync' | 'async'> = (
args: Value[]
) => PromiseOr<Value, sync>;
```
### `Options`
All of the options for a Sass compilation that are shared between compiling from
a path and by compiling from a string.
```ts
export interface Options<sync extends 'sync' | 'async'> {
```
#### `alertAscii`
If true, the compiler must use only ASCII characters in the formatted message of
errors and logs that aren't handled by a `logger`. Defaults to false.
```ts
alertAscii?: boolean;
```
#### `alertColor`
If true, the compiler may use terminal colors in the formatted message of errors
and logs that aren't handled by a `logger`. Implementations may choose the
default value for this based on their own heuristics of whether colored output
would be useful or render appropriately. Implementations are not obligated to
use colors even if this is `true`.
> The specific format of colored output can vary from implementation to
> implementation.
```ts
alertColor?: boolean;
```
#### `charset`
If true, the compiler must prepend `@charset "UTF-8";` or U+FEFF (byte-order
marker) if it emits non-ASCII CSS.
If false, the compiler must not prepend these byte sequences.
Defaults to true.
> This is ideal when concatenating CSS or embedding it in HTML `<style>` tags.
> Note that the output will still be UTF-8 regardless of this option.
```ts
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.
> As in the rest of Sass, `_`s and `-`s are considered equivalent when
> determining which function signatures match.
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.
[<ident-token>]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
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>>;
```
#### `importers`
The list of [custom importers] to use to resolve file loads.
[custom importers]: importer.d.ts.md
```ts
importers?: (Importer<sync> | FileImporter<sync>)[];
```
#### `loadPaths`
If set, the compiler must use these paths to resolve imports.
```ts
loadPaths?: string[];
```
#### `logger`
A [custom logger] that provides callbacks for the compiler to use in lieu of its
default messaging behavior.
[custom logger]: logger/index.d.ts.md
The compiler must treat an `undefined` logger identically to an object that
doesn't have `warn` or `debug` fields.
```ts
logger?: Logger;
```
#### `quietDeps`
If true, the compiler must not print deprecation warnings for stylesheets that
are transitively loaded through an import path.
Defaults to false.
```ts
quietDeps?: boolean;
```
#### `sourceMap`
If true, the compiler must set [`CompileResult.sourceMap`] to a sourceMap object
that represents the mapping between the generated CSS and the source files.
[`CompileResult.sourceMap`]: compile.d.ts.md#compileresult
Defaults to false.
> Except as otherwise specified, the exact structure of this file and how it
> maps between CSS and Sass is left up to the implementation.
```ts
sourceMap?: boolean;
```
#### `sourceMapIncludeSources`
If true, the compiler must include the full Sass source text in
[`CompileResult.sourceMap`].
Defaults to false.
```ts
sourceMapIncludeSources?: boolean;
```
#### `style`
If present, the compiler must format the emitted CSS in this style.
Implementations may support any subset of `OutputStyle`s, provided that:
* They support the `'expanded'` style.
* They produce CSS that is semantically equivalent regardless of style.
* They throw an error if they receive a value for this option that they do not
support.
> The specifics of each format can vary from implementation to implementation.
> If an implementation wants to add a new `OutputStyle`, the `OutputStyle` type
> should be expanded in this spec first to ensure that style names and
> TypeScript types remain consistent across implementations.
```ts
style?: OutputStyle;
```
#### `verbose`
If true, the compiler must print every single deprecation warning it encounters
(except for those silenced by [`quietDeps`]).
[`quietDeps`]: #quietdeps
If false, the compiler may choose not to print repeated deprecation warnings.
Defaults to false.
```ts
verbose?: boolean;
```
```ts
} // Options
```
### `StringOptionsWithoutImporter`
> This interface is used for calls to [`compileString()`] and
> [`compileStringAsync()`] that don't pass the `importer` parameter, and so
> don't support relative imports.
>
> [`compileString()`]: compile.d.ts.md#compilestring
> [`compileStringAsync()`]: compile.d.ts.md#compilestringasync
```ts
export interface StringOptionsWithoutImporter<sync extends 'sync' | 'async'>
extends Options<sync> {
```
#### `syntax`
The compiler must parse `source` using this syntax. Defaults to `'scss'`.
```ts
syntax?: Syntax;
```
#### `url`
The URL of the stylesheet being parsed.
> When `importer` isn't passed, this is purely advisory and only used for error
> reporting.
```ts
url?: URL;
```
```ts
} // StringOptionsWithoutImporter
```
### `StringOptionsWithImporter`
> This interface is used for calls to [`compileString()`] and
> [`compileStringAsync()`] that _do_ pass the `importer` parameter, and so _do_
> support relative imports.
```ts
export interface StringOptionsWithImporter<sync extends 'sync' | 'async'>
extends StringOptionsWithoutImporter<sync> {
```
#### `importer`
The [importer] to use to resolve relative imports in the entrypoint.
[importer]: importer.d.ts.md
```ts
importer: Importer<sync> | FileImporter<sync>;
```
#### `url`
The canonical URL of the entrypoint.
> This _must_ be passed when `importer` is passed, since otherwise there's
> nothing to resolve relative URLs relative to.
```ts
url: URL;
```
```ts
} // StringOptionsWithImporter
```
### `StringOptions`
```ts
export type StringOptions<sync extends 'sync' | 'async'> =
| StringOptionsWithImporter<sync>
| StringOptionsWithoutImporter<sync>;
```

View File

@ -1,28 +0,0 @@
import {List, OrderedMap} from 'immutable';
import {Value} from './index';
import {SassList, ListSeparator} from './list';
/**
* The JS API representation of a Sass argument list.
*
* `internal` refers to a Sass argument list.
*/
export class SassArgumentList extends SassList {
/**
* Creates a Sass argument list:
*
* - Set `internal` to a Sass argument list with contents set to `contents`,
* keywords set to `keywords`, and list separator set to `separator`.
* - Return `this`.
*/
constructor(
contents: Value[] | List<Value>,
keywords: Record<string, Value> | OrderedMap<string, Value>,
/** @default ',' */
separator?: ListSeparator
);
/** `internal`'s keywords. */
get keywords(): OrderedMap<string, Value>;
}

View File

@ -0,0 +1,62 @@
# Argument List API
```ts
import {List, OrderedMap} from 'immutable';
import {Value} from './index';
import {SassList, ListSeparator} from './list';
```
## Table of Contents
* [Types](#types)
* [`SassArgumentList`](#sassargumentlist)
* [`internal`](#internal)
* [Constructor](#constructor)
* [`keywords`](#keywords)
## Types
### `SassArgumentList`
The JS API representation of a Sass argument list.
```ts
export class SassArgumentList extends SassList {
```
#### `internal`
The [private `internal` field] refers to a Sass argument list.
[private `internal` field]: index.d.ts.md#internal
#### Constructor
Creates a Sass argument list:
* Set `internal` to a Sass argument list with contents set to `contents`,
keywords set to `keywords`, and list separator set to `separator`.
* Return `this`.
```ts
constructor(
contents: Value[] | List<Value>,
keywords: Record<string, Value> | OrderedMap<string, Value>,
/** @default ',' */
separator?: ListSeparator
);
```
#### `keywords`
`internal`'s keywords.
```ts
get keywords(): OrderedMap<string, Value>;
```
```ts
} // SassArgumentList
```

View File

@ -1,14 +0,0 @@
import {Value} from './index';
/** The JS API representation of the SassScript true singleton. */
export const sassTrue: SassBoolean;
/** The JS API representation of the SassScript false singleton. */
export const sassFalse: SassBoolean;
/** The JS API representation of a Sass boolean. */
export class SassBoolean extends Value {
private constructor();
get value(): boolean;
}

View File

@ -0,0 +1,55 @@
# Boolean API
```ts
import {Value} from './index';
```
## Table of Contents
* [Fields](#fields)
* [`sassTrue`](#sasstrue)
* [`sassFalse`](#sassfalse)
* [Types](#types)
* [`SassBoolean`](#sassboolean)
* [`value`](#value)
## Fields
### `sassTrue`
A `Value` whose [`internal`] is the SassScript true value.
[`internal`]: index.d.ts.md#internal
```ts
export const sassTrue: SassBoolean;
```
### `sassFalse`
A `Value` whose [`internal`] is the SassScript false value.
```ts
export const sassFalse: SassBoolean;
```
## Types
### `SassBoolean`
The JS API representation of a Sass boolean.
```ts
export class SassBoolean extends Value {
private constructor();
```
#### `value`
```ts
get value(): boolean;
```
```ts
} // SassBoolean
```

View File

@ -1,205 +0,0 @@
import {Value} from './index';
/**
* The JS API representation of a Sass color.
*
* `internal` refers to a Sass color.
*/
export class SassColor extends Value {
/**
* - If `options.red` is set:
*
* - Let `red` be a Sass number with a value of `options.red` `fuzzyRound`ed
* to the nearest integer.
*
* - Let `green` be a Sass number with a value of `options.green`
* `fuzzyRound`ed to the nearest integer.
*
* - Let `blue` be a Sass number with a value of `options.blue`
* `fuzzyRound`ed to the nearest integer.
*
* - If `options.alpha` is set, let `alpha` be a Sass number with a value of
* `options.alpha`. Otherwise, let `alpha` be `null`.
*
* - Set `internal` to the result of running [`rgb()`] with `$red`, `$green`,
* `$blue`, and `$alpha`.
*
* [`rgb()`]: ../../functions.md#rgb-and-rgba
*
* - Otherwise, if `options.saturation` is set:
*
* - Let `hue` be a Sass number with a value of `options.hue`.
*
* - Let `saturation` be a Sass number with a value of `options.saturation`.
*
* - Let `lightness` be a Sass number with a value of `options.lightness`.
*
* - If `options.alpha` is set, let `alpha` be a Sass number with a value of
* `options.alpha`. Otherwise, let `alpha` be `null`.
*
* - Set `internal` to the result of running [`hsl()`] with `$hue`, `$saturation`,
* `$lightness`, and `$alpha`.
*
* [`hsl()`]: ../../functions.md#hsl-and-hsla
*
* - Otherwise, if `options.whiteness` is set:
*
* - Let `hue` be a Sass number with a value of `options.hue`.
*
* - Let `whiteness` be a Sass number with a value of `options.whiteness`.
*
* - Let `blackness` be a Sass number with a value of `options.blackness`.
*
* - If `options.alpha` is set, let `alpha` be a Sass number with a value of
* `options.alpha`. Otherwise, let `alpha` be `null`.
*
* - Set `internal` to the result of running [`hwb()`] with `$hue`, `$whiteness`,
* `$blackness`, and `$alpha`.
*
* [`hwb()`]: ../../built-in-modules/color.md#hwb
*
* - Return `this`.
*/
constructor(options: {
red: number;
green: number;
blue: number;
alpha?: number;
});
constructor(options: {
hue: number;
saturation: number;
lightness: number;
alpha?: number;
});
constructor(options: {
hue: number;
whiteness: number;
blackness: number;
alpha?: number;
});
/** `internal`'s red channel. */
get red(): number;
/** `internal`'s green channel. */
get green(): number;
/** `internal`'s blue channel. */
get blue(): number;
/**
* Returns the value of the result of [`hue(internal)`][hue].
*
* [hue]: ../spec/built-in-modules/color.md#hue
*/
get hue(): number;
/**
* Returns the value of the result of [`saturation(internal)`][saturation].
*
* [saturation]: ../spec/built-in-modules/color.md#saturation
*/
get saturation(): number;
/**
* Returns the value of the result of [`lightness(internal)`][lightness].
*
* [lightness]: ../spec/built-in-modules/color.md#lightness
*/
get lightness(): number;
/**
* Returns the value of the result of [`whiteness(internal)`][whiteness].
*
* [whiteness]: ../spec/built-in-modules/color.md#whiteness
*/
get whiteness(): number;
/**
* Returns the value of the result of [`blackness(internal)`][blackness].
*
* [blackness]: ../spec/built-in-modules/color.md#blackness
*/
get blackness(): number;
/**
* Returns the value of the result of [`alpha(internal)`][alpha].
*
* [alpha]: ../spec/built-in-modules/color.md#alpha
*/
get alpha(): number;
/**
* Returns a new copy of `this` with one or more changes made to the RGB
* channels:
*
* - If `options.whiteness` or `options.blackness` is set:
*
* - Let `hue` be `options.hue` if it was passed, or `this.hue` otherwise.
*
* - Let `whiteness` be `options.whiteness` if it was passed, or
* `this.whiteness` otherwise.
*
* - Let `blackness` be `options.blackness` if it was passed, or
* `this.blackness` otherwise.
*
* - Let `alpha` be `options.alpha` if it was passed, or `this.alpha`
* otherwise.
*
* - Return the result of `SassColor({hue, whiteness, blackness, alpha})`.
*
* - Otherwise, if `options.hue`, `options.saturation`, or `options.lightness`
* is set:
*
* - Let `hue` be `options.hue` if it was passed, or `this.hue` otherwise.
*
* - Let `saturation` be `options.saturation` if it was passed, or
* `this.saturation` otherwise.
*
* - Let `lightness` be `options.lightness` if it was passed, or
* `this.lightness` otherwise.
*
* - Let `alpha` be `options.alpha` if it was passed, or `this.alpha`
* otherwise.
*
* - Return the result of `SassColor({hue, saturation, lightness, alpha})`.
*
* - Otherwise:
*
* - Let `red` be `options.red` if it was passed, or `this.red` otherwise.
*
* - Let `green` be `options.green` if it was passed, or `this.green`
* otherwise.
*
* - Let `blue` be `options.blue` if it was passed, or `this.blue`
* otherwise.
*
* - Let `alpha` be `options.alpha` if it was passed, or `this.alpha`
* otherwise.
*
* - Return the result of `SassColor({red, green, blue, alpha})`.
*/
change(options: {
red?: number;
green?: number;
blue?: number;
alpha?: number;
}): SassColor;
change(options: {
hue?: number;
saturation?: number;
lightness?: number;
alpha?: number;
}): SassColor;
change(options: {
hue?: number;
whiteness?: number;
blackness?: number;
alpha?: number;
}): SassColor;
}

View File

@ -0,0 +1,264 @@
# Color API
```ts
import {Value} from './index';
```
## Table of Contents
* [Types](#types)
* [`SassColor`](#sasscolor)
* [`internal`](#internal)
* [Constructor](#constructor)
* [`red`](#red)
* [`green`](#green)
* [`blue`](#blue)
* [`hue`](#hue)
* [`saturation`](#saturation)
* [`lightness`](#lightness)
* [`whiteness`](#whiteness)
* [`blackness`](#blackness)
* [`alpha`](#alpha)
* [`change`](#change)
## Types
### `SassColor`
The JS API representation of a Sass color.
```ts
export class SassColor extends Value {
```
#### `internal`
The [private `internal` field] refers to a Sass color.
[private `internal` field]: index.d.ts.md#internal
#### Constructor
* If `options.red` is set:
* Let `red` be a Sass number with a value of `options.red` `fuzzyRound`ed
to the nearest integer.
* Let `green` be a Sass number with a value of `options.green`
`fuzzyRound`ed to the nearest integer.
* Let `blue` be a Sass number with a value of `options.blue`
`fuzzyRound`ed to the nearest integer.
* If `options.alpha` is set, let `alpha` be a Sass number with a value of
`options.alpha`. Otherwise, let `alpha` be `null`.
* Set [`internal`] to the result of [`rgb(red, green, blue, alpha)`].
[`internal`]: #internal
[`rgb(red, green, blue, alpha)`]: ../../functions.md#rgb-and-rgba
* Otherwise, if `options.saturation` is set:
* Let `hue` be a Sass number with a value of `options.hue`.
* Let `saturation` be a Sass number with a value of `options.saturation`.
* Let `lightness` be a Sass number with a value of `options.lightness`.
* If `options.alpha` is set, let `alpha` be a Sass number with a value of
`options.alpha`. Otherwise, let `alpha` be `null`.
* Set [`internal`] to the result of [`hsl(hue, saturation, lightness,
alpha)`].
[`hsl(hue, saturation, lightness, alpha)`]: ../../functions.md#hsl-and-hsla
* Otherwise, if `options.whiteness` is set:
* Let `hue` be a Sass number with a value of `options.hue`.
* Let `whiteness` be a Sass number with a value of `options.whiteness`.
* Let `blackness` be a Sass number with a value of `options.blackness`.
* If `options.alpha` is set, let `alpha` be a Sass number with a value of
`options.alpha`. Otherwise, let `alpha` be `null`.
* Set [`internal`] to the result of [`hwb(hue, whiteness, blackness, alpha)`].
[`hwb(hue, whiteness, blackness, alpha)`]: ../../built-in-modules/color.md#hwb
* Return `this`.
```ts
constructor(options: {
red: number;
green: number;
blue: number;
alpha?: number;
});
constructor(options: {
hue: number;
saturation: number;
lightness: number;
alpha?: number;
});
constructor(options: {
hue: number;
whiteness: number;
blackness: number;
alpha?: number;
});
```
#### `red`
Returns [`internal`]'s red channel.
```ts
get red(): number;
```
#### `green`
Returns [`internal`]'s green channel.
```ts
get green(): number;
```
#### `blue`
Returns [`internal`]'s blue channel.
```ts
get blue(): number;
```
#### `hue`
Returns the value of the result of [`hue(internal)`].
[`hue(internal)`]: ../../built-in-modules/color.md#hue
```ts
get hue(): number;
```
#### `saturation`
Returns the value of the result of [`saturation(internal)`].
[`saturation(internal)`]: ../../built-in-modules/color.md#saturation
```ts
get saturation(): number;
```
#### `lightness`
Returns the value of the result of [`lightness(internal)`].
[`lightness(internal)`]: ../../built-in-modules/color.md#lightness
```ts
get lightness(): number;
```
#### `whiteness`
Returns the value of the result of [`whiteness(internal)`].
[`whiteness(internal)`]: ../../built-in-modules/color.md#whiteness
```ts
get whiteness(): number;
```
#### `blackness`
Returns the value of the result of [`blackness(internal)`].
[`blackness(internal)`]: ../../built-in-modules/color.md#blackness
```ts
get blackness(): number;
```
#### `alpha`
Returns the value of the result of [`alpha(internal)`].
[`alpha(internal)`]: ../../built-in-modules/color.md#alpha
```ts
get alpha(): number;
```
#### `change`
Returns a new color created by changing some of this color's channels:
* If `options.whiteness` or `options.blackness` is set, return the result of:
```js
SassColor({
hue: options.hue ?? this.hue,
whiteness: options.whiteness ?? this.whiteness,
blackness: options.blackness ?? this.blackness,
alpha: options.alpha ?? this.alpha
})
```
* Otherwise, if `options.hue`, `options.saturation`, or `options.lightness`
is set, return the result of:
```js
SassColor({
hue: options.hue ?? this.hue,
saturation: options.saturation ?? this.saturation,
lightness: options.lightness ?? this.lightness,
alpha: options.alpha ?? this.alpha
})
```
* Otherwise, return the result of:
```js
SassColor({
red: options.red ?? this.red,
green: options.green ?? this.gren,
blue: options.blue ?? this.blue,
alpha: options.alpha ?? this.alpha
})
```
```ts
change(options: {
red?: number;
green?: number;
blue?: number;
alpha?: number;
}): SassColor;
change(options: {
hue?: number;
saturation?: number;
lightness?: number;
alpha?: number;
}): SassColor;
change(options: {
hue?: number;
whiteness?: number;
blackness?: number;
alpha?: number;
}): SassColor;
```
```ts
} // SassColor
```

View File

@ -1,36 +0,0 @@
import {Value} from './index';
/**
* The JS API representation of a Sass function.
*
* `internal` refers to a Sass function.
*/
export class SassFunction extends Value {
/**
* Creates a Sass function:
*
* - If `signature` isn't a valid Sass function signature that could appear
* after the `@function` directive in a Sass stylesheet (such as
* `mix($color1, $color2, $weight: 50%)`), the implementation *may* throw an
* error.
*
* > This is optional to allow for implementations of the value API that
* > don't have easy access to a Sass parser, such as the embedded host.
* > These implementations must instead throw an error when the invalid
* > function is returned from the custom function.
*
* - Set `internal` to a Sass function with signature set to `signature` that,
* upon execution, runs `callback` and returns the result.
*
* - Return `this`.
*/
constructor(
/**
* Must be a valid Sass function signature that could appear after the
* `@function` directive in a Sass stylesheet, such as
* `mix($color1, $color2, $weight: 50%)`.
*/
signature: string,
callback: (args: Value[]) => Value
);
}

View File

@ -0,0 +1,54 @@
# Function Value API
```ts
import {Value} from './index';
```
## Table of Contents
* [Types](#types)
* [`SassFunction`](#sassfunction)
* [`internal`](#internal)
* [Constructor](#constructor)
## Types
### `SassFunction`
The JS API representation of a Sass function.
```ts
export class SassFunction extends Value {
```
#### `internal`
The [private `internal` field] refers to a Sass function.
[private `internal` field]: index.d.ts.md#internal
#### Constructor
Creates a Sass function:
* If `signature` isn't a valid Sass function signature that could appear after
the `@function` directive in a Sass stylesheet (such as `mix($color1, $color2,
$weight: 50%)`), the implementation *may* throw an error.
> This is optional to allow for implementations of the value API that don't
> have easy access to a Sass parser, such as the embedded host. These
> implementations must instead throw an error when the invalid function is
> returned from the custom function.
* Set `internal` to a Sass function with signature set to `signature` that, upon
execution, runs `callback` and returns the result.
* Return `this`.
```ts
constructor(signature: string, callback: (args: Value[]) => Value);
```
```ts
} // SassFunction
```

View File

@ -1,187 +0,0 @@
import {List, ValueObject} from 'immutable';
import {SassBoolean} from './boolean';
import {SassColor} from './color';
import {SassFunction} from './function';
import {ListSeparator} from './list';
import {SassMap} from './map';
import {SassNumber} from './number';
import {SassString} from './string';
export {SassArgumentList} from './argument_list';
export {SassBoolean, sassTrue, sassFalse} from './boolean';
export {SassColor} from './color';
export {SassFunction} from './function';
export {SassList, ListSeparator} from './list';
export {SassMap} from './map';
export {SassNumber} from './number';
export {SassString} from './string';
/** The JS API representation of the SassScript null singleton. */
export const sassNull: Value;
/**
* The JS API representation of a Sass value.
*
* Sass values are immutable. Therefore, all subclasses of Value must have an
* API that obeys immutability. Their APIs must not expose ways to modify
* Sass values, including lists and maps. An API call that returns a new copy
* of a Sass value must ensure that the copy preserves the metadata of the
* original value (e.g. units).
*
* > To make the spec terser and easier to author, each subclass that extends
* > `Value` has a virtual, private property named `internal` that refers to the
* > Sass value it represents.
*/
export abstract class Value implements ValueObject {
protected constructor();
/**
* Returns `this` as an array:
*
* - If `internal` is a Sass list, return an array of its contents.
* - If `internal` is a Sass map, return an array of its keys and values as
* two-element `SassList`s.
* - Otherwise, return an array containing `this`.
*/
get asList(): List<Value>;
/** Whether `internal` is a bracketed Sass list. */
get hasBrackets(): boolean;
/** Whether `this` is truthy. */
get isTruthy(): boolean;
/** Returns JS null if `internal` is Sass null. Otherwise, returns `this`. */
get realNull(): null | Value;
/**
* Returns `internal`'s list separator:
*
* - If `internal` is a Sass list, return its separator.
* - Otherwise, return `null`.
*/
get separator(): ListSeparator;
/**
* Converts the Sass index `sassIndex` to a JS index into the array returned
* by `asList`:
*
* - If `sassIndex` is not a unitless Sass number, throw an error.
*
* - Let `value` be the value of `sassIndex`. Let `index` be the result of
* `fuzzyAsInt(value)`. If `index === null`, throw an error.
*
* - If `index === 0`, or the absolute value of `index` is greater than
* `asList.length`, throw an error.
*
* - If `index > 0`, return `index - 1`.
* - Otherwise, if `index < 0`, return `asList.length + index`.
*
* > Sass indices start counting at 1, and may be negative in order to index
* > from the end of the list.
*
* > The `name` parameter may be used for error reporting.
*/
sassIndexToListIndex(sassIndex: Value, name?: string): number;
/**
* - Return `this.asList.get(index)`.
*
* > Note that the `immutable` package uses zero-based indexing, with negative
* > numbers indexing backwards from the end of the list. Non-integer indices
* > are rounded down.
*/
get(index: number): Value | undefined;
/**
* Asserts that `this` is a `SassBoolean`:
*
* - If `internal` is a Sass boolean, return `this`.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertBoolean(name?: string): SassBoolean;
/**
* Asserts that `this` is a `SassColor`:
*
* - If `internal` is a Sass color, return `this`.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertColor(name?: string): SassColor;
/**
* Asserts that `this` is a `SassFunction`:
*
* - If `internal` is a Sass function, return `this`.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertFunction(name?: string): SassFunction;
/**
* Asserts that `this` is a `SassMap`:
*
* - If `internal` is a Sass map, return `this`.
* - If `internal` is an empty Sass list, return a `SassMap` with `internal`
* set to an empty map.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertMap(name?: string): SassMap;
/**
* Asserts that `this` is a `SassNumber`:
*
* - If `internal` is a Sass number, return `this`.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertNumber(name?: string): SassNumber;
/**
* Asserts that `this` is a `SassString`:
*
* - If `internal` is a Sass string, return `this`.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertString(name?: string): SassString;
/**
* Returns `this` interpreted as a map.
*
* - If `this` is a `SassMap`, return `this`.
*
* - Otherwise, if `internal` is an empty Sass list, return a `SassMap` with
* its `internal` set to an empty `OrderedMap`.
*
* - Otherwise, return `null`.
*/
tryMap(): SassMap | null;
/** Whether `this == other` in SassScript. */
equals(other: Value): boolean;
/**
* Must be the same for `Value`s that are equal to each other according to the
* `==` SassScript operator.
*/
hashCode(): number;
/**
* Returns a serialized representation of `this`.
*
* > The specific format can vary from implementation to implementation and is
* > not guaranteed to be valid Sass source code.
*/
toString(): string;
}

View File

@ -0,0 +1,287 @@
# Value API
```ts
import {List, ValueObject} from 'immutable';
import {SassBoolean} from './boolean';
import {SassColor} from './color';
import {SassFunction} from './function';
import {ListSeparator} from './list';
import {SassMap} from './map';
import {SassNumber} from './number';
import {SassString} from './string';
export {SassArgumentList} from './argument_list';
export {SassBoolean, sassTrue, sassFalse} from './boolean';
export {SassColor} from './color';
export {SassFunction} from './function';
export {SassList, ListSeparator} from './list';
export {SassMap} from './map';
export {SassNumber} from './number';
export {SassString} from './string';
```
## Table of Contents
* [Fields](#fields)
* [`sassNull`](#sassnull)
* [Types](#types)
* [`Value`](#value)
* [`internal`](#internal)
* [`asList`](#aslist)
* [`hasBrackets`](#hasbrackets)
* [`isTruthy`](#istruthy)
* [`realNull`](#realnull)
* [`separator`](#separator)
* [`sassIndexToListIndex`](#sassindextolistindex)
* [`get`](#get)
* [`assertBoolean`](#assertboolean)
* [`assertColor`](#assertcolor)
* [`assertFunction`](#assertfunction)
* [`assertMap`](#assertmap)
* [`assertNumber`](#assertnumber)
* [`assertString`](#assertstring)
* [`tryMap`](#trymap)
* [`equals`](#equals)
* [`hashCode`](#hashcode)
* [`toString`](#tostring)
## Fields
### `sassNull`
A `Value` whose [`internal`] is the SassScript null value.
[`internal`]: #internal
```ts
export const sassNull: Value;
```
## Types
### `Value`
The JS API representation of a Sass value.
Sass values are immutable. Therefore, all subclasses of Value must have an API
that obeys immutability. Their APIs must not expose ways to modify Sass values,
including lists and maps. An API call that returns a new copy of a Sass value
must ensure that the copy preserves the metadata of the original value (e.g.
units).
```ts
export abstract class Value implements ValueObject {
protected constructor();
```
#### `internal`
To make the spec terser and easier to author, each `Value` instance has a
private property named `internal` that refers to the Sass value it represents.
This property is only used for spec purposes and is not visible in any sense to
JavaScript.
#### `asList`
Returns `this` as an array:
* If [`internal`] is a Sass list, return an array of its contents.
* If [`internal`] is a Sass map, return an array of its keys and values as
two-element `SassList`s.
* Otherwise, return a list containing `this`.
```ts
get asList(): List<Value>;
```
#### `hasBrackets`
Whether [`internal`] is a bracketed Sass list.
```ts
get hasBrackets(): boolean;
```
#### `isTruthy`
Whether `this` is truthy.
```ts
get isTruthy(): boolean;
```
#### `realNull`
Returns JS null if [`internal`] is Sass null. Otherwise, returns `this`.
```ts
get realNull(): null | Value;
```
#### `separator`
Return [`internal`]'s separator if it's a Sass list, and `null` otherwise.
```ts
get separator(): ListSeparator;
```
#### `sassIndexToListIndex`
Converts the Sass index `sassIndex` to a JS index into the array returned by
`asList`:
- If `sassIndex` is not a unitless Sass number, throw an error.
- Let `value` be the value of `sassIndex`. Let `index` be the result of
`fuzzyAsInt(value)`. If `index === null`, throw an error.
- If `index === 0`, or the absolute value of `index` is greater than
`asList.length`, throw an error.
- If `index > 0`, return `index - 1`.
- Otherwise, if `index < 0`, return `asList.length + index`.
> Sass indices start counting at 1, and may be negative in order to index from
> the end of the list.
> The `name` parameter may be used for error reporting.
```ts
sassIndexToListIndex(sassIndex: Value, name?: string): number;
```
#### `get`
Returns `this.asList.get(index)`.
> Note that the `immutable` package uses zero-based indexing, with negative
> numbers indexing backwards from the end of the list. Non-integer indices are
> rounded down.
```ts
get(index: number): Value | undefined;
```
#### `assertBoolean`
Returns `this` if it's a [`SassBoolean`] and throws an error otherwise.
[`SassBoolean`]: boolean.d.ts.md
> The `name` parameter may be used for error reporting.
```ts
assertBoolean(name?: string): SassBoolean;
```
#### `assertColor`
Returns `this` if it's a [`SassColor`] and throws an error otherwise.
[`SassColor`]: color.d.ts.md
> The `name` parameter may be used for error reporting.
```ts
assertColor(name?: string): SassColor;
```
#### `assertFunction`
Returns `this` if it's a [`SassFunction`] and throws an error otherwise.
[`SassFunction`]: function.d.ts.md
> The `name` parameter may be used for error reporting.
```ts
assertFunction(name?: string): SassFunction;
```
#### `assertMap`
Return `this.tryMap()` if it's not null, and throw an error otherwise.
> The `name` parameter may be used for error reporting.
```ts
assertMap(name?: string): SassMap;
```
#### `assertNumber`
Returns `this` if it's a [`SassNumber`] and throws an error otherwise.
[`SassNumber`]: number.d.ts.md
> The `name` parameter may be used for error reporting.
```ts
assertNumber(name?: string): SassNumber;
```
#### `assertString`
Returns `this` if it's a [`SassString`] and throws an error otherwise.
[`SassString`]: string.d.ts.md
> The `name` parameter may be used for error reporting.
```ts
assertString(name?: string): SassString;
```
#### `tryMap`
Returns `this` interpreted as a map.
* If `this` is a [`SassMap`], return `this`.
* Otherwise, if [`internal`] is an empty Sass list, return a `SassMap` with its
`internal` set to an empty map.
* Otherwise, return `null`.
```ts
tryMap(): SassMap | null;
```
#### `equals`
Returns whether [`internal`] is `==` to `other`'s `internal` in SassScript.
```ts
equals(other: Value): boolean;
```
#### `hashCode`
Returns the same number for any two `Value`s that are equal according to
[`equals`].
[`equals`]: #equals
> This is _not_ required to be different for different values, although having
> overlap between common values is likely to cause performance issues.
```ts
hashCode(): number;
```
#### `toString`
Returns a string representation of `this`.
> The specific format can vary from implementation to implementation and is not
> guaranteed to be valid Sass source code.
```ts
toString(): string;
```
```ts
} // Value
```

View File

@ -1,50 +0,0 @@
import {List} from 'immutable';
import {Value} from './index';
/**
* The JS API representation of a Sass list separator.
*
* > `null` represents the undecided separator type.
*/
export type ListSeparator = ',' | '/' | ' ' | null;
/**
* The JS API representation of a Sass list.
*
* `internal` refers to a Sass list.
*/
export class SassList extends Value {
/**
* Creates a Sass list:
*
* - If the first argument is an `Array` or a `List`:
* - Let `contents` be the first argument.
* - Let `options` be the second argument, or `{}` if it's undefined.
*
* - Otherwise:
* - Let `contents` be `[]`.
* - Let `options` be the first argument, or `{}` if it's undefined.
*
* - Let `separator` be `options.separator`, or `','` if that's undefined.
*
* - Let `brackets` be `options.brackets`, or `false` if that's undefined.
*
* - Set `internal` to a Sass list with contents set to `contents`, separator
* set to `separator`, and brackets set to `brackets`.
*
* - Return `this`.
*/
constructor(
contents: Value[] | List<Value>,
options?: {
separator?: ListSeparator;
brackets?: boolean;
}
);
constructor(options?: {separator?: ListSeparator; brackets?: boolean});
/** `internal`'s list separator. */
get separator(): ListSeparator;
}

View File

@ -0,0 +1,89 @@
# List API
```ts
import {List} from 'immutable';
import {Value} from './index';
```
## Table of Contents
* [Types](#types)
* [`ListSeparator`](#listseparator)
* [`SassList`](#sasslist)
* [`internal`](#internal)
* [Constructor](#constructor)
* [`separator`](#separator)
## Types
### `ListSeparator`
The JS API representation of a Sass list separator. null represents the
undecided separator type.
```ts
export type ListSeparator = ',' | '/' | ' ' | null;
```
### `SassList`
The JS API representation of a Sass list.
```ts
export class SassList extends Value {
```
#### `internal`
The [private `internal` field] refers to [a Sass list].
[private `internal` field]: index.d.ts.md#internal
[a Sass list]: ../../types/list.md
#### Constructor
Creates a Sass list:
* If the first argument is an `Array` or a `List`:
* Let `contents` be the first argument.
* Let `options` be the second argument, or `{}` if it's undefined.
* Otherwise:
* Let `contents` be `[]`.
* Let `options` be the first argument, or `{}` if it's undefined.
* Let `separator` be `options.separator`, or `','` if that's undefined.
* Let `brackets` be `options.brackets`, or `false` if that's undefined.
* Set `internal` to a Sass list with contents set to `contents`, separator set
to `separator`, and brackets set to `brackets`.
* Return `this`.
```ts
constructor(
contents: Value[] | List<Value>,
options?: {
separator?: ListSeparator;
brackets?: boolean;
}
);
constructor(options?: {separator?: ListSeparator; brackets?: boolean});
```
#### `separator`
[`internal`]'s list separator.
[`internal`]: #internal
```ts
get separator(): ListSeparator;
```
```ts
} // SassList
```

View File

@ -1,41 +0,0 @@
import {OrderedMap} from 'immutable';
import {SassList} from './list';
import {Value} from './index';
/**
* The JS API representation of a Sass map.
*
* `internal` refers to a Sass map.
*/
export class SassMap extends Value {
/**
* Creates a Sass map:
*
* - If `contents` is undefined, set it to an empty `OrderedMap`.
* - Set `internal` to a Sass map with contents set to `contents`.
* - Return `this`.
*/
constructor(contents?: OrderedMap<Value, Value>);
/**
* Returns a map containing `internal`'s contents:
*
* - Let `result` be an empty `OrderedMap`.
* - Add each key and value from `internal`'s contents to `result`, in order.
* - Return `result`.
*/
get contents(): OrderedMap<Value, Value>;
/**
* - If the first argument is a JavaScript number, pass it to
* `this.asList.get` and return the result.
*
* - Otherwise, pass it to `this.contents.get` and return the result.
*/
get(key: Value): Value | undefined;
get(index: number): SassList | undefined;
tryMap(): SassMap;
}

View File

@ -0,0 +1,82 @@
# Map API
```ts
import {OrderedMap} from 'immutable';
import {SassList} from './list';
import {Value} from './index';
```
## Table of Contents
* [Types](#types)
* [`SassMap`](#sassmap)
* [`internal`](#internal)
* [Constructor](#constructor)
* [`contents`](#contents)
* [`get`](#get)
* [`tryMap`](#trymap)
## Types
### `SassMap`
The JS API representation of a Sass map.
```ts
export class SassMap extends Value {
```
#### `internal`
The [private `internal` field] refers to a Sass map.
[private `internal` field]: index.d.ts.md#internal
#### Constructor
Creates a Sass map:
* If `contents` is undefined, set it to an empty `OrderedMap`.
* Set `internal` to a Sass map with contents set to `contents`.
* Return `this`.
```ts
constructor(contents?: OrderedMap<Value, Value>);
```
#### `contents`
Returns a map containing `internal`'s contents:
* Let `result` be an empty `OrderedMap`.
* Add each key and value from `internal`'s contents to `result`, in order.
* Return `result`.
```ts
get contents(): OrderedMap<Value, Value>;
```
#### `get`
* If the first argument is a JavaScript number, pass it to `this.asList.get` and
return the result.
* Otherwise, pass it to `this.contents.get` and return the result.
```ts
get(key: Value): Value | undefined;
get(index: number): SassList | undefined;
```
#### `tryMap`
```ts
tryMap(): SassMap;
```
```ts
} // SassMap
```

View File

@ -1,258 +0,0 @@
import {List} from 'immutable';
import {Value} from './index';
/**
* The JS API representation of a Sass number.
*
* `internal` refers to a Sass number.
*/
export class SassNumber extends Value {
/**
* Creates a Sass number:
*
* - If the second argument is undefined:
*
* - Set `internal` to a Sass number with a value of `value`.
*
* - Otherwise, if the second argument is a string:
*
* - Set `internal` to a Sass number with a value of `value` and that string
* as its single numerator unit.
*
* - Otherwise,
*
* - Let `options` be the second argument.
*
* - Set `internal` to a Sass number with a value of `value`,
* `options.numeratorUnits` as its numerator units (if passed), and
* `options.denominatorUnits` as its denominator units (if passed).
*
* - Return `this`.
*/
constructor(
value: number,
unit?:
| string
| {
numeratorUnits?: string[] | List<string>;
denominatorUnits?: string[] | List<string>;
}
);
/** `internal`'s value. */
get value(): number;
/** Whether `internal`'s value `fuzzyEquals` an integer. */
get isInt(): boolean;
/**
* Returns `internal`'s value as an integer:
*
* - If `internal`'s value `fuzzyEquals` an integer, return that integer.
* - Otherwise, return `null`.
*/
get asInt(): number | null;
/** `internal`'s numerator units. */
get numeratorUnits(): List<string>;
/** `internal`'s denominator units. */
get denominatorUnits(): List<string>;
/** Whether `internal` has numerator or denominator units. */
get hasUnits(): boolean;
/**
* Asserts that `internal`'s value is an integer:
*
* - If `internal`'s value `fuzzyEquals` an integer, return that integer.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertInt(name?: string): number;
/**
* Asserts that `internal`'s value is within the specified range:
*
* - If `internal`'s value is `fuzzyGreaterThan` `min` and `fuzzyLessThan`
* `max`, return it.
* - Otherwise, if `internal`'s value `fuzzyEquals` `min`, return `min`.
* - Otherwise, if `internal`'s value `fuzzyEquals` `max`, return `max`.
* - Otherwise, throw an error.
*
* > The `name` parameter may be used for error reporting.
*/
assertInRange(min: number, max: number, name?: string): number;
/**
* Asserts that `internal` is unitless:
*
* - If `internal` has any numerator or denominator units, throw an error.
* - Otherwise, return `this`.
*
* > The `name` parameter may be used for error reporting.
*/
assertNoUnits(name?: string): SassNumber;
/**
* Asserts the type of `internal`'s unit:
*
* - If `internal` has any denominator units, or if `unit` is not `internal`'s
* only numerator unit, throw an error.
* - Otherwise, return `this`.
*
* > The `name` parameter may be used for error reporting.
*/
assertUnit(unit: string, name?: string): SassNumber;
/**
* Whether `internal` has the specified unit:
*
* - If `internal` has any denominator units, return false.
* - Otherwise, return whether `unit` is `internal`'s only numerator unit.
*/
hasUnit(unit: string): boolean;
/**
* Whether `internal` is [compatible] with `unit`.
*
* [compatible]: ../../types/number.md#compatible-units
*/
compatibleWithUnit(unit: string): boolean;
/**
* Creates a new copy of `this` with its units converted to those represented
* by `newNumerators` and `newDenominators`:
*
* - Let `converter` be the result of
*
* ```
* withUnits(0, {
* numeratorUnits: newNumerators,
* denominatorUnits: newDenominators,
* });
* ```
*
* - If `converter` is not [compatible] with `internal`, throw an error.
*
* - Set `converter` to the result of `simplify`ing `converter`.
*
* - Return a new `SassNumber` with `internal` set to the result of the
* SassScript expression `converter + internal`.
*
* > The `name` parameter may be used for error reporting.
*/
convert(
newNumerators: string[] | List<string>,
newDenominators: string[] | List<string>,
name?: string
): SassNumber;
/**
* Creates a new copy of `this` with its units converted to the units of
* `other`:
*
* - Let `newNumerators` be the numerator units of `other`.
* - Let `newDenominators` be the denominator units of `other`.
* - Return the result of `convert(newNumerators, newDenominators)`.
*
* > The `name` and `otherName` parameters may be used for error reporting.
*/
convertToMatch(
other: SassNumber,
name?: string,
otherName?: string
): SassNumber;
/**
* Return the value of the result of `convert(newNumerators,
* newDenominators)`.
*
* > The `name` parameter may be used for error reporting.
*/
convertValue(
newNumerators: string[] | List<string>,
newDenominators: string[] | List<string>,
name?: string
): number;
/**
* Returns the value of the result of `convertToMatch(other)`.
*
* > The `name` and `otherName` parameters may be used for error reporting.
*/
convertValueToMatch(
other: SassNumber,
name?: string,
otherName?: string
): number;
/**
* Creates a new copy of `this` with its units converted to those represented
* by `newNumerators` and `newDenominators`:
*
* - Support converting to/from unitless:
*
* - If `internal` is unitless:
*
* - If `newNumerators` and `newDenominators` are both empty, return
* `this`.
*
* - Otherwise, for the duration of this procedure, let `internal` behave
* as if its numerator units were equal to `newNumerators` and its
* denominator units were equal to `newDenominators`.
*
* - Otherwise, if `newNumerators` and `newDenominators` are both empty, set
* `newNumerators` to `internal`'s numerator units and `newDenominators`
* to `internal`'s denominator units.
*
* - Return the result of `convert(newNumerators, newDenominators)`.
*
* > The `name` parameter may be used for error reporting.
*/
coerce(
newNumerators: string[] | List<string>,
newDenominators: string[] | List<string>,
name?: string
): SassNumber;
/**
* Creates a new copy of `this` with its units converted to the units of
* `other`:
*
* - Let `newNumerators` be the numerator units of `other`.
* - Let `newDenominators` be the denominator units of `other`.
* - Return the result of `coerce(newNumerators, newDenominators)`.
*
* > The `name` and `otherName` parameters may be used for error reporting.
*/
coerceToMatch(
other: SassNumber,
name?: string,
otherName?: string
): SassNumber;
/**
* Return the value of the result of `coerce(newNumerators, newDenominators)`.
*
* > The `name` parameter may be used for error reporting.
*/
coerceValue(
newNumerators: string[] | List<string>,
newDenominators: string[] | List<string>,
name?: string
): number;
/**
* Returns the value of the result of `coerceToMatch(other)`.
*
* > The `name` and `otherName` parameters may be used for error reporting.
*/
coerceValueToMatch(
other: SassNumber,
name?: string,
otherName?: string
): number;
}

View File

@ -0,0 +1,361 @@
# Number API
```ts
import {List} from 'immutable';
import {Value} from './index';
```
## Table of Contents
* [Types](#types)
* [`SassNumber`](#sassnumber)
* [`internal`](#internal)
* [Constructor](#constructor)
* [`value`](#value)
* [`isInt`](#isint)
* [`asInt`](#asint)
* [`numeratorUnits`](#numeratorunits)
* [`denominatorUnits`](#denominatorunits)
* [`hasUnits`](#hasunits)
* [`assertInt`](#assertint)
* [`assertInRange`](#assertinrange)
* [`assertUnitless`](#assertunitless)
* [`assertUnit`](#assertunit)
* [`hasUnit`](#hasunit)
* [`compatibleWithUnit`](#compatiblewithunit)
* [`convert`](#convert)
* [`convertToMatch`](#converttomatch)
* [`convertValue`](#convertvalue)
* [`convertValueToMatch`](#convertvaluetomatch)
* [`coerce`](#coerce)
* [`coerceToMatch`](#coercetomatch)
* [`coerce`](#coerce-1)
* [`coerceValueToMatch`](#coercevaluetomatch)
## Types
### `SassNumber`
The JS API representation of a Sass number.
```ts
export class SassNumber extends Value {
```
#### `internal`
The [private `internal` field] refers to [a Sass number].
[private `internal` field]: index.d.ts.md#internal
[a Sass number]: ../../types/number.md
#### Constructor
Creates a Sass number:
* If the second argument is undefined:
* Set `internal` to a Sass number with a value of `value`.
* Otherwise, if the second argument is a string:
* Set `internal` to a Sass number with a value of `value` and that string as
its single numerator unit.
* Otherwise,
* Let `options` be the second argument.
* Set `internal` to a Sass number with a value of `value`,
`options.numeratorUnits` as its numerator units (if passed), and
`options.denominatorUnits` as its denominator units (if passed).
* Return `this`.
```ts
constructor(
value: number,
unit?:
| string
| {
numeratorUnits?: string[] | List<string>;
denominatorUnits?: string[] | List<string>;
}
);
```
#### `value`
Returns [`internal`]'s value.
[`internal`]: #internal
```ts
get value(): number;
```
#### `isInt`
Whether [`internal`] is an [integer].
[integer]: ../../types/number.md#integer
```ts
get isInt(): boolean;
```
#### `asInt`
Returns [`internal`]'s [integer value] if it has one, or null if it doesn't.
[integer value]: ../../types/number.md#integer
```ts
get asInt(): number | null;
```
#### `numeratorUnits`
Returns [`internal`]'s numerator units.
```ts
get numeratorUnits(): List<string>;
```
#### `denominatorUnits`
Returns [`internal`]'s denominator units.
```ts
get denominatorUnits(): List<string>;
```
#### `hasUnits`
Whether [`internal`] has numerator or denominator units.
```ts
get hasUnits(): boolean;
```
#### `assertInt`
Returns [`internal`]'s [integer value] if it has one, and throws an error if it
doesn't.
> The `name` parameter may be used for error reporting.
```ts
assertInt(name?: string): number;
```
#### `assertInRange`
Asserts that [`internal`]'s value is within the specified range:
* If `internal`'s value is greater than `min` and less than `max`, return it.
* Otherwise, if `internal`'s value [fuzzy equals] `min`, return `min`.
* Otherwise, if `internal`'s value fuzzy equals `max`, return `max`.
* Otherwise, throw an error.
[fuzzy equals]: ../../types/number.md#fuzzy-equality
> The `name` parameter may be used for error reporting.
```ts
assertInRange(min: number, max: number, name?: string): number;
```
#### `assertUnitless`
Returns `this` if [`internal`] has no numerator or denominator units, and throws
an error otherwise.
> The `name` parameter may be used for error reporting.
```ts
assertNoUnits(name?: string): SassNumber;
```
#### `assertUnit`
Asserts the type of [`internal`]'s unit:
* If `internal` has any denominator units, or if `unit` is not `internal`'s
only numerator unit, throw an error.
* Otherwise, return `this`.
> The `name` parameter may be used for error reporting.
```ts
assertUnit(unit: string, name?: string): SassNumber;
```
#### `hasUnit`
Returns whether `unit` is [`internal`]'s only numerator unit and `internal` has no
denominator units.
```ts
hasUnit(unit: string): boolean;
```
#### `compatibleWithUnit`
Whether `internal` is [compatible] with `unit`.
[compatible]: ../../types/number.md#compatible-units
```ts
compatibleWithUnit(unit: string): boolean;
```
#### `convert`
* Let `converter` be the [`internal`] field of the result of
```js
withUnits(0, {
numeratorUnits: newNumerators,
denominatorUnits: newDenominators,
});
```
* If `converter` is not [compatible] with `internal`, throw an error.
* Set `converter` to the result of [simplifying] `converter`.
[simplifying]: ../../types/number.md#simplifying-a-number
* Return a new `SassNumber` with `internal` set to the result of the
SassScript expression `converter + internal`.
> The `name` parameter may be used for error reporting.
```ts
convert(
newNumerators: string[] | List<string>,
newDenominators: string[] | List<string>,
name?: string
): SassNumber;
```
#### `convertToMatch`
Return the result of `convert(other.numeratorUnits, other.denominatorUnits)`.
> The `name` and `otherName` parameters may be used for error reporting.
```ts
convertToMatch(
other: SassNumber,
name?: string,
otherName?: string
): SassNumber;
```
#### `convertValue`
Return the result of `convert(newNumerators, newDenominators).value`.
> The `name` parameter may be used for error reporting.
```ts
convertValue(
newNumerators: string[] | List<string>,
newDenominators: string[] | List<string>,
name?: string
): number;
```
#### `convertValueToMatch`
Returns the result of `convertToMatch(other).value`.
> The `name` and `otherName` parameters may be used for error reporting.
```ts
convertValueToMatch(
other: SassNumber,
name?: string,
otherName?: string
): number;
```
#### `coerce`
Creates a new copy of `this` with its units converted to those represented
by `newNumerators` and `newDenominators`:
* If `newNumerators` and `newDenominators` are both empty, return the result of
`new SassNumber(this.value)`.
* If `internal` is unitless, return the result of:
[unitless]: ../../types/number.md#
```js
new SassNumber(this.value, {
numeratorUnits: newNumerators,
denominatorUnits: newDenominators
});
```
* Return the result of `convert(newNumerators, newDenominators)`.
> The `name` parameter may be used for error reporting.
```ts
coerce(
newNumerators: string[] | List<string>,
newDenominators: string[] | List<string>,
name?: string
): SassNumber;
```
#### `coerceToMatch`
Return the result of `coerce(other.numeratorUnits, other.denominatorUnits)`.
> The `name` and `otherName` parameters may be used for error reporting.
```ts
coerceToMatch(
other: SassNumber,
name?: string,
otherName?: string
): SassNumber;
```
#### `coerce`
Return the result of `coerce(newNumerators, newDenominators).value`.
> The `name` parameter may be used for error reporting.
```ts
coerceValue(
newNumerators: string[] | List<string>,
newDenominators: string[] | List<string>,
name?: string
): number;
```
#### `coerceValueToMatch`
Returns the value of the result of `coerceToMatch(other)`.
> The `name` and `otherName` parameters may be used for error reporting.
```ts
coerceValueToMatch(
other: SassNumber,
name?: string,
otherName?: string
): number;
```
```ts
} // SassNumber
```

View File

@ -1,78 +0,0 @@
import {Value} from './index';
/**
* The JS API representation of a Sass string.
*
* `internal` refers to a Sass string.
*/
export class SassString extends Value {
/**
* Creates a Sass string:
*
* - If the first argument is a string:
* - Let `text` be the first argument.
* - Let `options` be the second argument, or `{}` if it's undefined.
*
* - Otherwise:
* - Let `text` be `""`.
* - Let `options` be the first argument, or `{}` if it's undefined.
*
* - Let `quotes` be `options.quotes`, or `true` if that's undefined.
*
* - Set `internal` to a Sass string with contents set to `text` and quoted
* set to `quotes`.
*
* - Return `this`.
*/
constructor(
text: string,
options?: {
quotes?: boolean;
}
);
constructor(options?: {/** @default true */ quotes?: boolean});
/**
* Creates an empty Sass string:
*
* - Set `internal` to an empty Sass string with quoted value set to
* `options.quotes`.
* - Return `this`.
*/
/** The contents of `internal` serialized as UTF-16 code units. */
get text(): string;
/** Whether `internal` has quotes. */
get hasQuotes(): boolean;
/** The number of Unicode code points in `text`. */
get sassLength(): number;
/**
* Converts the Sass index `sassIndex` to a JS index into `text`:
*
* - If `sassIndex` is not a unitless Sass number, throw an error.
*
* - Let `value` be the value of `sassIndex`. Let `index` be the result of
* `fuzzyAsInt(value)`. If `index === null`, throw an error.
*
* - If `index === 0`, or the absolute value of `index` is greater than
* the length of `sassLength`, throw an error.
*
* - If `index > 0`, let `normalizedIndex = index - 1`.
* - Otherwise, if `index < 0`, let `normalizedIndex = sassLength + index`.
*
* - Let `jsIndex` be a JS index. Set `jsIndex` to the first code
* unit of the Unicode code point that `normalizedIndex` points to.
*
* > Sass indices count Unicode code points, whereas JS indices count
* > UTF-16 code units.
*
* - Return `jsIndex`.
*
* > The `name` parameter may be used for error reporting.
*/
sassIndexToStringIndex(sassIndex: Value, name?: string): number;
}

View File

@ -0,0 +1,117 @@
# String API
```ts
import {Value} from './index';
```
## Table of Contents
* [Types](#types)
* [`SassString`](#sassstring)
* [`internal`](#internal)
* [Constructor](#constructor)
* [`text`](#text)
* [`hasQuotes`](#hasquotes)
* [`sassLength`](#sasslength)
* [`sassIndexToStringIndex`](#sassindextostringindex)
## Types
### `SassString`
The JS API representation of a Sass string.
```ts
export class SassString extends Value {
```
#### `internal`
The [private `internal` field] refers to a Sass string.
[private `internal` field]: index.d.ts.md#internal
#### Constructor
Creates a Sass string:
* If the first argument is a string:
* Let `text` be the first argument.
* Let `options` be the second argument, or `{}` if it's undefined.
* Otherwise:
* Let `text` be `""`.
* Let `options` be the first argument, or `{}` if it's undefined.
* Let `quotes` be `options.quotes`, or `true` if that's undefined.
* Set [`internal`] to a Sass string with contents set to `text` and quoted set
to `quotes`.
* Return `this`.
```ts
constructor(
text: string,
options?: {
quotes?: boolean;
}
);
constructor(options?: {/** @default true */ quotes?: boolean});
```
#### `text`
The contents of [`internal`] serialized as UTF-16 code units.
```ts
get text(): string;
```
#### `hasQuotes`
Whether [`internal`] has quotes.
```ts
get hasQuotes(): boolean;
```
#### `sassLength`
The number of Unicode code points in [`internal`]'s contents.
```ts
get sassLength(): number;
```
#### `sassIndexToStringIndex`
Converts the Sass index `sassIndex` to a JS index into `text`:
* If `sassIndex` is not a unitless Sass number, throw an error.
* Let `value` be the value of `sassIndex`. Let `index` be the result of
`fuzzyAsInt(value)`. If `index === null`, throw an error.
* If `index === 0`, or the absolute value of `index` is greater than the length
of `sassLength`, throw an error.
* If `index > 0`, let `normalizedIndex = index * 1`.
* Otherwise, if `index < 0`, let `normalizedIndex = sassLength + index`.
* Return the index in `text` of the first code unit of the Unicode code point
that `normalizedIndex` points to.
> Sass indices count Unicode code points, whereas JS indices count UTF-16 code
> units.
> The `name` parameter may be used for error reporting.
```ts
sassIndexToStringIndex(sassIndex: Value, name?: string): number;
```
```ts
} // SassString
```

View File

@ -17,10 +17,12 @@ const parser: prettier.CustomParser = (text, parsers) => {
};
for (const specPath of glob.sync('spec/js-api/**/*.d.ts')) {
const specFile = prettier.format(fs.readFileSync(specPath, 'utf-8'), {
filepath: specPath,
parser,
});
const specFile = prettier
.format(fs.readFileSync(specPath, 'utf-8'), {
filepath: specPath,
parser,
})
.replace(/\n\n+/g, '\n');
const docPath = p.join('js-api-doc', p.relative('spec/js-api', specPath));
if (!fs.existsSync(docPath)) {
@ -31,10 +33,12 @@ for (const specPath of glob.sync('spec/js-api/**/*.d.ts')) {
continue;
}
const docFile = prettier.format(fs.readFileSync(docPath, 'utf-8'), {
filepath: specPath,
parser,
});
const docFile = prettier
.format(fs.readFileSync(docPath, 'utf-8'), {
filepath: specPath,
parser,
})
.replace(/\n\n+/g, '\n');
if (specFile === docFile) continue;
const diffResult = diff.createTwoFilesPatch(

62
tool/tangle.ts Normal file
View File

@ -0,0 +1,62 @@
// The literate programming "tangle" tool: takes literate programming files (in
// this case, Markdown files with embedded TypeScript) and converts them to the
// corresponding physical TypeScript files.
import * as colors from 'colors/safe';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as glob from 'glob';
import {marked} from 'marked';
import * as p from 'path';
import * as prettier from 'prettier';
const args = process.argv.slice(2);
const files = glob.sync(args.length === 0 ? '**/*.d.ts.md' : args, {
ignore: ['node_modules/**/*.d.ts.md'],
});
(async () => {
for (const file of files) {
if (!file.endsWith('.md')) {
console.error(
`${colors.red(colors.bold('Error:'))} Can't tangle non-.md file ` +
`"${file}".`
);
process.exitCode = 1;
continue;
}
const outputFile = file.substring(0, file.length - 3);
const outputType = p.extname(outputFile).substring(1);
if (outputType === '') {
console.error(
`${colors.yellow(colors.bold('Warning:'))} File "${file}" has no ` +
"additional extension, so there's nothing to tangle it to."
);
}
const source = fs.readFileSync(file, 'utf8');
const hash = crypto.createHash('sha256').update(source).digest('hex');
const codeBlocks: string[] = [`// <[tangle hash]> ${hash}`];
marked.parse(source, {
walkTokens: token => {
if (token.type === 'code' && token.lang === outputType) {
codeBlocks.push(token.text);
codeBlocks.push('// ==<[tangle boundary]>==');
}
},
});
codeBlocks.push('');
const config = (await prettier.resolveConfig(outputFile)) ?? {};
fs.writeFileSync(
outputFile,
prettier.format(codeBlocks.join('\n'), {
filepath: outputFile,
...config,
})
);
console.log(colors.grey(`Tangled ${file} to ${outputFile}`));
}
})();

104
tool/untangle.ts Normal file
View File

@ -0,0 +1,104 @@
// The inverse of `tangle.ts`, which takes the generated TypeScript and injects
// it back into the Markdown document. This allow us to format the TypeScript
// files using standard tools while keeping their canonical sources in Markdown.
import * as colors from 'colors/safe';
import * as crypto from 'crypto';
import dedent from 'ts-dedent';
import * as fs from 'fs';
import * as glob from 'glob';
import {marked} from 'marked';
import * as p from 'path';
const args = process.argv.slice(2);
const files = glob.sync(args.length === 0 ? '**/*.d.ts.md' : args, {
ignore: ['node_modules/**/*.d.ts.md'],
});
for (const file of files) {
if (!file.endsWith('.md')) {
console.error(
`${colors.red(colors.bold('Error:'))} Can't untangle non-.md file ` +
`"${file}".`
);
process.exitCode = 1;
continue;
}
const tangledFile = file.substring(0, file.length - 3);
const tangledType = p.extname(tangledFile).substring(1);
if (tangledType === '') {
console.error(
`${colors.yellow(colors.bold('Warning:'))} File "${file}" has no ` +
"additional extension, so there's nothing to untangle it from."
);
continue;
}
if (!fs.existsSync(tangledFile)) {
console.error(
`${colors.red(colors.bold('Error:'))} Can't untangle "${file}" ` +
`because "${tangledFile}" doesn't exist.`
);
process.exitCode = 1;
continue;
}
const codeBlocks = fs
.readFileSync(tangledFile, 'utf8')
.split(/\n *\/\/ ==<\[tangle boundary\]>==\n/);
const match = /^\/\/ <\[tangle hash\]> (.*?)\n/.exec(codeBlocks[0]);
if (!match) {
console.error(
`${colors.red(colors.bold('Error:'))} "${tangledFile}" is missing ` +
'the initial hash.'
);
process.exitCode = 1;
continue;
}
const expectedHash = match[1];
codeBlocks[0] = codeBlocks[0].split('\n').slice(1).join('\n');
let source = fs.readFileSync(file, 'utf8');
const hash = crypto.createHash('sha256').update(source).digest('hex');
if (hash !== expectedHash) {
console.error(
`${colors.red(colors.bold('Error:'))} "${file}" was modified after ` +
`"${tangledFile}" was tangled.`
);
process.exitCode = 1;
continue;
}
let failed = false;
marked.parse(source, {
walkTokens: token => {
if (failed) return;
if (token.type === 'code' && token.lang === tangledType) {
let codeBlock = codeBlocks.shift();
if (codeBlock === undefined) {
console.error(
`${colors.red(colors.bold('Error:'))} "${tangledFile}" has fewer ` +
`code blocks than "${file}".`
);
process.exitCode = 1;
failed = true;
codeBlock = '';
}
source = source.replace(
token.raw,
// Add a leading newline to satisfy dedent().
`\`\`\`${token.lang}\n${dedent('\n' + codeBlock)}\n\`\`\``
);
}
},
});
if (failed) continue;
if (!source.endsWith('\n')) source += '\n';
fs.writeFileSync(file, source);
console.log(colors.grey(`Untangled ${tangledFile} to ${file}`));
}