Merge branch 'main' of github.com:sass/sass into feature.color-4

This commit is contained in:
Natalie Weizenbaum 2024-03-27 12:44:29 -07:00
commit 9db0409090
34 changed files with 1641 additions and 183 deletions

View File

@ -72,7 +72,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-setup-action@v1.28.0
- uses: bufbuild/buf-setup-action@v1.30.0
with: {github_token: "${{ github.token }}"}
- name: Generate protobuf code
run: buf generate
@ -138,7 +138,7 @@ jobs:
- name: Find changed files in js-api-doc
id: changed-files
uses: tj-actions/changed-files@25ef3926d147cd02fc7e931c1ef50772bbb0d25d
uses: tj-actions/changed-files@20576b4b9ed46d41e2d45a2256e5e2316dde6834
with: {files: js-api-doc}
- name: Deploy

View File

@ -4,6 +4,24 @@
* Remove `RgbColor`, `HslColor` and `HwbColor` SassScript values.
## 2.5.0
* Add `NodePackageImporter` as a built-in Package Importer, resolving `pkg:`
URLs using the standards and conventions of the Node ecosystem.
## 2.4.0
* Add `CompileRequest.silent` option that suppresses all `LogEvent`s.
## 2.3.0
* Add a `Value.CompilerMixin` value type to represent first-class mixins.
## 2.2.0
* Deprecate the `Value.Calculation.CalculationValue.value.interpolation` option,
and change how it's interpreted by the compiler.
## 2.1.0
* Use the Java package `com.sass_lang.embedded_protocol` and generate multiple

View File

@ -172,7 +172,7 @@ A number has *known units* unless it has unit `%`.
Replace [the definition of `Potentially Slash-Separated Number`] with the
following:
[the definition of `Potentially Slash-Separated Number`]: ../spec/types/number.md#potentially-slash-separated-number
[the definition of `Potentially Slash-Separated Number`]: slash-separator.md#existing-behavior
A Sass number may be *potentially slash-separated*. If it is, it is associated
with two additional Sass numbers, the *original numerator* and the *original

View File

@ -325,7 +325,7 @@ numbers.
Add `CalcExpression`s, `ClampExpression`s, `CssMinMax`es to the list of operands
of the `/` operator that can create a [potentially slash-separated number].
[potentially slash-separated number]: ../spec/types/number.md#potentially-slash-separated-number
[potentially slash-separated number]: slash-separator.md#existing-behavior
## Syntax

View File

@ -0,0 +1,43 @@
## Draft 1.6
* Change from an entry point path to entry point directory.
* Add a private readonly symbol key to `NodePackageImporter` type to
distinguish it from arbitrary objects.
## Draft 1.5
* Specify that Package Importers must not reject URL patterns that other Package
Importers may be able to canonicalize.
* Specify that the Node Package Importer throws if `entryPointPath` is not
passed and `require.main.filename` is not available.
## Draft 1.4
* Allow NodePackageImporter to accept an entry point path.
* Change Embedded protocol entry point to a path.
## Draft 1.3
* Handle empty subpath in "Resolving package exports" subprocedure.
## Draft 1.2
* Export `NodePackageImporter` type, and set `_NodePackageImporterBrand` to
unknown.
## Draft 1.1
* Throw an error if `nodePackageImporter` is used in the browser or other
environment without filesystem access.
* Remove specified order in the global import list, as users can specify the
order within the `importers` option.
* Specify importer ordering for the Legacy API.
## Draft 1
* Initial draft

View File

@ -1,11 +1,10 @@
# Package Importer: Draft 1.3
# Package Importer: Draft 1.6
*([Issue](https://github.com/sass/sass/issues/2739))*
This proposal introduces the semantics for a Package Importer and defines the
`pkg:` URL scheme to indicate Sass package imports in an implementation-agnostic
format. It also defines the semantics for a new built-in Node Package
Importer.
format. It also defines the semantics for a new built-in Node Package Importer.
## Table of Contents
@ -19,7 +18,7 @@ Importer.
* [Available in legacy API](#available-in-legacy-api)
* [Node Resolution Decisions](#node-resolution-decisions)
* [Types](#types)
* [`nodePackageImporter`](#nodepackageimporter)
* [`NodePackageImporter`](#nodepackageimporter)
* [Updated `importers` option](#updated-importers-option)
* [Legacy API `pkgImporter`](#legacy-api-pkgimporter)
* [Semantics](#semantics)
@ -123,9 +122,9 @@ the reader where to find the file.
While the Dart Sass implementation allows for the use of the `package:` URL
scheme, a similar standard doesn't exist in Node. We chose the `pkg:` URL scheme
as it clearly communicates to both the user and compiler that the specified files
are from a dependency. The `pkg:` URL scheme also does not have known conflicts
in the ecosystem.
as it clearly communicates to both the user and compiler that the specified
files are from a dependency. The `pkg:` URL scheme also does not have known
conflicts in the ecosystem.
#### No built-in `pkg:` resolver for browsers
@ -188,13 +187,14 @@ use the CSS exports exposed through `"style"`. While in practice, `"style"`
tends to be used solely for `css` files, we will support `scss`, `sass` and
`css` files for either `"sass"` or `"style"`.
While conditional exports allows package authors to define specific aliases to internal
files, we will still use the Sass conventions for resolving file paths with
partials, extensions and indices to discover the intended export alias. However,
we will not apply that logic to the destination, and will expect library authors
to map the export to the correct place. In other words, given a `package.json`
with `exports` as below, The Node package importer will resolve a
`@use "pkg:pkgName/variables";` to the destination of the `_variables.scss` export.
While conditional exports allows package authors to define specific aliases to
internal files, we will still use the Sass conventions for resolving file paths
with partials, extensions and indices to discover the intended export alias.
However, we will not apply that logic to the destination, and will expect
library authors to map the export to the correct place. In other words, given a
`package.json` with `exports` as below, The Node package importer will resolve a
`@use "pkg:pkgName/variables";` to the destination of the `_variables.scss`
export.
```json
{
@ -216,19 +216,33 @@ using symlinks if this behavior is desired.
[ECMAScript modules]: https://nodejs.org/api/esm.html#no-node_path
The [Node resolution algorithm] requires a `parentURL`, used for determining
where in the file system to start searching for a module if a `pkg:` URL is
being resolved in a source somewhere other than a file on disk. For instance,
when compiling a string like `compileString('@use "pkg:bootstrap";')`, we don't
know where to start looking for the Bootstrap module. We considered
`require.main.filename` and the current working directory, but found that
neither would allow for all use cases. We decided to allow users to specify an
entry point directory, defaulting to the parent directory of
`require.main.filename`.
## Types
```ts
import {FileImporter, Importer} from '../spec/js-api/importer';
```
### `nodePackageImporter`
### `NodePackageImporter`
```ts
export type NodePackageImporter = {
_NodePackageImporterBrand: unknown;
};
export declare const nodePackageImporter: NodePackageImporter;
declare const nodePackageImporterKey: unique symbol;
export class NodePackageImporter {
/** Used to distinguish this type from any arbitrary object. */
private readonly [nodePackageImporterKey]: true;
constructor(entryPointDirectory?: string);
}
```
### Updated `importers` option
@ -253,19 +267,23 @@ declare module '../spec/js-api/options' {
Before the first bullet points in [`compile`] and [`compileString`] in the
Javascript Compile API, insert:
* If any object in `options.importers` is exactly equal to the object
`nodePackageImporter`:
* If any item in `options.importers` is an instance of the `NodePackageImporter`
class:
* If no filesystem is available, throw an error.
> This primarily refers to a browser environment, but applies to other
> sandboxed JavaScript environments as well.
* Let `pkgImporter` be a [Node Package Importer] with an associated
`entryPointURL` of `require.main.filename`.
* Let `entryPointDirectory` be the class's `entryPointDirectory` value if set,
resolved relative to the current working directory, and otherwise the parent
directory of `require.main.filename`. If `entryPointDirectory` is not passed
and `require.main.filename` is not available, throw an error.
* Replace `nodePackageImporter` with `pkgImporter` in a copy of
`options.importers`.
* Let `pkgImporter` be a [Node Package Importer] with an associated
`entryPointURL` of the absolute file URL for `entryPointDirectory`.
* Replace the item with `pkgImporter` in a copy of `options.importers`.
[`compile`]: ../spec/js-api/compile.d.ts.md#compile
[`compileString`]: ../spec/js-api/compile.d.ts.md#compilestring
@ -278,15 +296,22 @@ any URL with the `pkg:` scheme. This step will be inserted immediately before
the existing legacy importer logic, and if the package importer returns `null`,
the legacy importer logic will be invoked.
Currently, the only available package importer is `node`, which follows Node
resolution logic to locate Sass files.
Currently, the only available package importer is `NodePackageImporter`, which
follows Node resolution logic to locate Sass files.
An optional `entryPointDirectory` path can be passed to the
`NodePackageImporter` to provide a starting `parentURL` for the Node package
resolution algorithm. If not set, the default value is the parent directory of
`require.main.filename`.
Defaults to undefined.
```ts
import {NodePackageImporter as BaseNodePackageImporter} from '../spec/js-api/importer';
declare module '../spec/js-api/legacy/options' {
export interface LegacySharedOptions<sync extends 'sync' | 'async'> {
pkgImporter?: 'node';
pkgImporter?: BaseNodePackageImporter;
}
}
```
@ -297,7 +322,8 @@ declare module '../spec/js-api/legacy/options' {
This proposal defines the requirements for Package Importers written by users or
provided by implementations. It is a type of [Importer] and, in addition to the
standard requirements for importers, it must handle only non-canonical URLs that:
standard requirements for importers, it must handle only non-canonical URLs
that:
* have the scheme `pkg`, and
* whose path begins with a package name, and
@ -315,6 +341,11 @@ Package Importers must reject the following patterns:
* A URL whose path begins with `/`.
* A URL with non-empty/null username, password, host, port, query, or fragment.
If the conventions or specifications for an environment disallow any other URL
patterns, the Package Importer must return null rather than throwing an error.
This allows subsequent Package Importers to attempt to resolve with their
conventions.
[importer]: ../spec/modules.md#importer
### Node Package Importer
@ -344,8 +375,8 @@ When the Node Package Importer is invoked with a string named `string`:
* Otherwise, let `baseURL` be `entryPointURL`.
* Let `resolved` be the result of [resolving a `pkg:` URL as Node] with `url` and
`baseURL`.
* Let `resolved` be the result of [resolving a `pkg:` URL as Node] with `url`
and `baseURL`.
* If `resolved` is null, return null.
@ -415,8 +446,8 @@ It returns a canonical `file:` URL or null.
This algorithm takes a string, `path`, and returns the portion that identifies
the Node package.
* If `path` starts with `@`, it is a scoped package. Return the first 2 [URL path
segments], including the separating `/`.
* If `path` starts with `@`, it is a scoped package. Return the first 2 [URL
path segments], including the separating `/`.
* Otherwise, return the first URL path segment.
@ -426,9 +457,13 @@ This algorithm takes a string, `packageName`, and an absolute URL `baseURL`, and
returns an absolute URL to the root directory for the most proximate installed
`packageName`.
* Return the result of `PACKAGE_RESOLVE(packageName, baseURL)` as defined in
the [Node resolution algorithm specification].
* Let `baseDirectory` be `baseURL` appended with a [single-dot URL path
segment].
* Return the result of `PACKAGE_RESOLVE(packageName, baseDirectory)` as defined
in the [Node resolution algorithm specification].
[single-dot URL path segment]: https://url.spec.whatwg.org/#single-dot-path-segment
[Node resolution algorithm specification]: https://nodejs.org/api/esm.html#resolution-algorithm-specification
### Resolving package exports
@ -460,7 +495,8 @@ This algorithm takes a package.json value `packageManifest`, a directory URL
* Let `subpathIndex` be `subpath` + `"/index"`.
* Let `subpathIndexVariants` be the result of [Export load paths] with `subpathIndex`.
* Let `subpathIndexVariants` be the result of [Export load paths] with
`subpathIndex`.
* Let `resolvedIndexPaths` be a list of the results of calling
`PACKAGE_EXPORTS_RESOLVE(packageRoot, subpathVariant, exports, ["sass",
@ -518,8 +554,8 @@ potential subpaths, resolving for partials and file extensions.
* Add `subpath` to `paths`.
* Otherwise, add `subpath` + `.scss`, `subpath` + `.sass`, and `subpath` +
`.css` to `paths`.
* Otherwise, add `subpath`, `subpath` + `.scss`, `subpath` + `.sass`, and
`subpath` + `.css` to `paths`.
* If `subpath`'s [basename] does not start with `_`, for each `item` in
`paths`, prepend `"_"` to the basename, and add to `paths`.
@ -531,11 +567,12 @@ potential subpaths, resolving for partials and file extensions.
## Embedded Protocol
An Importer that resolves `pkg:` URLs using the [Node resolution algorithm]. It
is instantiated with an associated `entry_point_url`.
is instantiated with an associated `entry_point_directory`, which is either
absolute or will be resolved relative to the current working directory.
```proto
message NodePackageImporter {
string entry_point_url = 1;
string entry_point_directory = 1;
}
```

View File

@ -1,3 +1,9 @@
## Draft 1.2
* Trigger plain CSS nesting behavior based on the type of *the parent* rule's
stylesheet, so that we don't unexpectedly generate plain-CSS nesting for
previously un-nested `@import` and `meta.load-css()` rules.
## Draft 1.1
* Trigger plain CSS nesting behavior based on the type of a rule's stylesheet,

View File

@ -1,4 +1,4 @@
# Plain CSS Nesting: Draft 1
# Plain CSS Nesting: Draft 1.2
*([Issue](https://github.com/sass/sass/issues/3524),
[Changelog](plain-css-nesting.changes.md))*
@ -93,7 +93,12 @@ To execute a style rule `rule`:
* Let `selector` be the result of evaluating all interpolation in `rule`'s
selector and parsing the result as a selector list.
* **If `rule`'s stylesheet wasn't [parsed as CSS]**:
* **Let `parent` by the [current style rule] or current at-rule if one exists,
or the innermost if both exist.**
[current style rule]: ../spec/style-rules.md#current-style-rule
* **If `parent` is a style rule whose stylesheet wasn't [parsed as CSS]:**
[parsed as CSS]: ../spec/syntax.md#parsing-text-as-css
@ -101,20 +106,18 @@ To execute a style rule `rule`:
> behavior occurs even when plain CSS is evaluated in a Sass context, such as
> through a nested `@import` or a `meta.load-css()` call.
* If there is a [current style rule]:
* **If `selector` contains one or more parent selectors and `rule`'s
stylesheet wasn't [parsed as CSS], replace those parent selectors with the
current style rule's selector and set `selector` to the result.**
* If `selector` contains one or more parent selectors, replace them with the
current style rule's selector and set `selector` to the result.
* **Otherwise, nest `selector` within the current style rule's selector using
the [descendant combinator] and set `selector` to the result.**
* Otherwise, nest `selector` within the current style rule's selector using
the [descendant combinator] and set `selector` to the result.
* Otherwise, if `selector` contains one or more parent selectors, throw an
error.
[current style rule]: ../spec/style-rules.md#current-style-rule
[descendant combinator]: https://www.w3.org/TR/selectors-3/#descendant-combinators
* **Otherwise, if `selector` contains one or more parent selectors and `rule`'s
stylesheet wasn't [parsed as CSS], throw an error.**
* Let `css` be a CSS style rule with selector `selector`.
* Execute each child `child` of `rule`.
@ -129,12 +132,11 @@ To execute a style rule `rule`:
[complex selectors]: https://drafts.csswg.org/selectors-4/#complex
* Unless `css`'s selector is now empty:
* **If `parent` is set and its stylesheet was [parsed as CSS], append `css` to
`parent`**
* **If `rule`'s stylesheet was [parsed as CSS] and there is a [current style
rule] or a current at-rule, append `css` to whichever of the two exists, or
the innermost if both exist.**
* **If there is a current at-rule, append `css` to its children.**
* **Otherwise, if there is a current at-rule, append `css` to its children.**
> This was intended to be in the current spec, but was overlooked.

View File

@ -0,0 +1,19 @@
## Draft 1.3
* Expose `Compiler` and `AsyncCompiler` classes.
* Specify that `Compiler` and `AsyncCompiler` classes throw errors if
* constructed without going through the `init` methods.
## Draft 1.2
* Await method calls in example code.
* Use `compiler.compile*()` methods in example code.
## Draft 1.1
* Remove unneeded returned/resolved value from `dispose`.
## Draft 1
* Initial draft

View File

@ -0,0 +1,240 @@
# Shared Resources in JavaScript API: Draft 1.3
*([Issue](https://github.com/sass/sass/issues/3296))*
This proposal adds an API design that allows for sharing resources across
multiple invocations of the Sass compiler's JavaScript API. This will provide
Sass's users with a more efficient way of running Sass compilations across
multiple files.
## Table of Contents
* [Summary](#summary)
* [Design Decisions](#design-decisions)
* [Splitting Sync and Async Compilers](#splitting-sync-and-async-compilers)
* [Limited API interface](#limited-api-interface)
* [Flexibility for interfaces on process management](#flexibility-for-interfaces-on-process-management)
* [No shared state](#no-shared-state)
* [Example](#example)
* [Sync Compiler](#sync-compiler)
* [Async Compiler](#async-compiler)
* [API](#api)
* [Types](#types)
* [initCompiler()](#initcompiler)
* [initAsyncCompiler()](#initasynccompiler)
* [Compiler](#compiler)
* [`compile()` and `compileString()`](#compile-and-compilestring)
* [dispose()](#dispose)
* [Async Compiler](#async-compiler-1)
* [`compileAsync()` and `compileStringAsync()`](#compileasync-and-compilestringasync)
* [dispose()](#dispose-1)
## Summary
Currently, the JavaScript API for Sass only accommodates a single compilation
per process. In practice, we have observed build tools are compiling multiple
times in response to a single user action. For instance, Vue.js authors using
Vite will see a Sass compilation for each `<style lang="scss">` tag that appears
in their codebase.
While processes can be spun up and down quickly, the combined time can add up to
a noticeable impact on performance. The embedded client supports long running
processes, and this proposal adds a method for the embedded host to manage the
lifecycle of these processes through a Compiler interface.
### Design Decisions
#### Splitting Sync and Async Compilers
When creating a Compiler, users will need to choose either a compiler that
provides access to synchronous or asynchronous compilation. While providing both
simultaneously from a single Compiler would offer more flexibility, it also adds
significant complexity to the API. In practice, we expect most users will only
want to use one mode, generally in the mode that is the fastest for the
implementation. If synchronous and asynchronous compilations are both needed,
users can create multiple Compilers.
#### Limited API interface
Many build tools allow passing the Sass module as a parameter, which offers
flexibility to users on what implementation of Sass is used. Because users may
still want to use portions of the JavaScript API unrelated to compilation, we
considered having the Compiler interface mirror the top level Sass interface,
which would allow users to replace instances of the imported `sass` class with
an instance of the compiler. However, this adds an additional cost to ongoing
Sass development. The proposed API does not eliminate this as a possibility in
the future.
#### Flexibility for interfaces on process management
In environments without access to a long-running compiler—for instance, the Dart
Sass implementation—the Compiler interface will continue to perform a single
compilation per process.
#### No shared state
This proposal does not change how a single compilation is done, and no state is
shared across compilations. Options and importers must be set for each
compilation. Future enhancements may introduce [shared state], but this proposal
only adds the ability to run multiple compilations on a single process.
[shared state]: https://github.com/sass/sass/issues/3296
This also means that the proposal makes no assertions about whether file content
has changed. It is also up to the user to determine when to start and stop a
long-running compiler process.
### Example
#### Sync Compiler
```js
import * as sass from 'sass';
function setup() {
const compiler = sass.initCompiler();
const result1 = compiler.compileString('a {b: c}').css;
const result2 = compiler.compileString('a {b: c}').css;
compiler.dispose();
// throws error
const result3 = compiler.compileString('a {b: c}').css;
}
```
#### Async Compiler
```js
import * as sass from 'sass';
async function setup() {
const compiler = await sass.initAsyncCompiler();
const result1 = await compiler.compileStringAsync('a {b: c}').css;
const result2 = await compiler.compileStringAsync('a {b: c}').css;
await compiler.dispose();
// throws error
const result3 = await compiler.compileStringAsync('a {b: c}').css;
}
```
## API
### Types
```ts
import {CompileResult} from '../spec/js-api/compile';
import {Options, StringOptions} from '../spec/js-api/options';
```
#### initCompiler()
Returns a synchronous [Compiler].
[Compiler]: #compiler
```ts
export function initCompiler(): Compiler;
```
#### initAsyncCompiler()
Resolves with an [Async Compiler].
[Async Compiler]: #async-compiler
```ts
export function initAsyncCompiler(): Promise<AsyncCompiler>;
```
#### Compiler
An instance of the synchronous Compiler.
```ts
export class Compiler {
```
If the Compiler is directly constructed (as opposed to the Compiler being
created via `initCompiler`), throw an error.
```ts
private constructor();
```
##### `compile()` and `compileString()`
Only synchronous compilation methods [`compile()`] and [`compileString()`] must be
included, and must have with identical semantics to the [Sass interface].
[`compile()`]: ../spec/js-api/compile.d.ts.md#compile
[`compilestring()`]: ../spec/js-api/compile.d.ts.md#compilestring
[Sass interface]: ../spec/js-api/index.d.ts.md
```ts
compile(path: string, options?: Options<'sync'>): CompileResult;
compileString(source: string, options?: StringOptions<'sync'>): CompileResult;
```
##### dispose()
When `dispose` is invoked on a Compiler:
* Any subsequent invocations of `compile` and `compileString` must throw an
error.
```ts
dispose(): void;
}
```
#### Async Compiler
An instance of the asynchronous Compiler.
```ts
export class AsyncCompiler {
```
If the AsyncCompiler is directly constructed (as opposed to the Compiler being
created via `initAsyncCompiler`), throw an error.
```ts
private constructor();
```
##### `compileAsync()` and `compileStringAsync()`
Only asynchronous compilation methods [`compileAsync()`] and
[`compileStringAsync()`] must be included, and must have with identical
semantics to the [Sass interface].
[`compileasync()`]: ../spec/js-api/compile.d.ts.md#compileasync
[`compilestringasync()`]: ../spec/js-api/compile.d.ts.md#compilestringasync
```ts
compileAsync(
path: string,
options?: Options<'async'>
): Promise<CompileResult>;
compileStringAsync(
source: string,
options?: StringOptions<'async'>
): Promise<CompileResult>;
```
##### dispose()
When `dispose` is invoked on an Async Compiler:
* Any subsequent invocations of `compileAsync` and `compileStringAsync` must
throw an error.
* Any compilations that have not yet been settled must be allowed to settle, and
not be cancelled.
* Resolves a Promise when all compilations have been settled, and disposal is
complete.
```ts
dispose(): Promise<void>;
}
```

View File

@ -1,13 +1,21 @@
The [`sass` package] on npm is a pure-JavaScript package built from the [Dart
Sass] implementation. In addition to Dart Sass's [command-line interface], it
provides a JavaScript API that can be used to drive Sass compilations from
The Sass JavaScript API can be used to to drive Sass Compilations from
JavaScript. It even allows an application to control {@link Options.importers |
how stylesheets are loaded} and {@link Options.functions | define custom
functions}.
The [`sass` package] on npm is a pure-JavaScript package built from the [Dart
Sass] implementation, and includes Dart Sass's [command-line interface].
The [`sass-embedded` package] on npm is a JavaScript wrapper around a native
Dart executable, and in general is faster than `sass`.
Both `sass` and `sass-embedded` provide the same JavaScript API using the same
underlying [Dart Sass] implementation, but have speed and platform tradeoffs.
[`sass` package]: https://www.npmjs.com/package/sass
[Dart Sass]: https://sass-lang.com/dart-sass
[command-line interface]: https://sass-lang.com/documentation/cli/dart-sass
[`sass-embedded` package]: https://www.npmjs.com/package/sass-embedded
## Usage
@ -112,3 +120,38 @@ of Sass code by passing in a {@link LegacyStringOptions}.
}
});
```
## Speed
While multiple factors go into how long Sass compilations take, there are
general speed trends that can help you minimize your compilation time.
### With the `sass` package
With the `sass` package, the synchronous calls will be faster than asynchronous
calls due to the overhead of making the entire evaluation process asynchronous.
While the {@link Compiler} and {@link AsyncCompiler} class are available, they
aren't faster than than the module-level compilation methods when using `sass`.
### With the `sass-embedded` package
The `sass-embedded` package provides significant speed improvements in certain
situations, and is generally faster than `sass` for large or frequent
compilations. When using the module-level compilation methods, asynchronous
calls are generally faster than synchronous ones due to the overhead of
emulating synchronous messaging with worker threads and concurrent compilations
being blocked on the main thread.
The {@link Compiler} and {@link AsyncCompiler} classes provide significant
improvements when using the `sass-embedded` package. These classes persist and
reuse a single Dart process across multiple compilations, avoiding the need to
repeatedly start up and tear down a process.
When compiling a single file using `sass-embedded`, there is not much difference
between the synchronous and asynchronous methods. When running multiple
compilations at the same time, an {@link AsyncCompiler} will be considerably
faster than a synchronous {@link Compiler}.
Other factors like {@link Functions}, {@link Importers} and the complexity of
your Sass files may also impact what compilation methods work best for your
particular use case.

View File

@ -37,6 +37,104 @@ export interface CompileResult {
sourceMap?: RawSourceMap;
}
/**
* The result of creating a synchronous compiler. Returned by
* {@link initCompiler}.
*
* @category Compile
*/
export class Compiler {
/**
* Throws an error if constructed directly, instead of via
* {@link initCompiler}.
*/
private constructor();
/**
* The {@link compile} method exposed through a Compiler instance while it is
* active. If this is called after {@link dispose} on the Compiler
* instance, an error will be thrown.
*
* During the Compiler instance's lifespan, given the same input, this will
* return an identical result to the {@link compile} method exposed at the
* module root.
*/
compile(path: string, options?: Options<'sync'>): CompileResult;
/**
* The {@link compileString} method exposed through a Compiler instance while
* it is active. If this is called after {@link dispose} on the Compiler
* instance, an error will be thrown.
*
* During the Compiler instance's lifespan, given the same input, this will
* return an identical result to the {@link compileString} method exposed at
* the module root.
*/
compileString(source: string, options?: StringOptions<'sync'>): CompileResult;
/**
* Ends the lifespan of this Compiler instance. After this is invoked, all
* calls to the Compiler instance's {@link compile} or {@link compileString}
* methods will result in an error.
*/
dispose(): void;
}
/**
* The result of creating an asynchronous compiler. Returned by
* {@link initAsyncCompiler}.
*
* @category Compile
*/
export class AsyncCompiler {
/**
* Throws an error if constructed directly, instead of via
* {@link initAsyncCompiler}.
*/
private constructor();
/**
* The {@link compileAsync} method exposed through an Async Compiler instance
* while it is active. If this is called after {@link dispose} on the Async
* Compiler instance, an error will be thrown.
*
* During the Async Compiler instance's lifespan, given the same input, this
* will return an identical result to the {@link compileAsync} method exposed
* at the module root.
*/
compileAsync(
path: string,
options?: Options<'async'>
): Promise<CompileResult>;
/**
* The {@link compileStringAsync} method exposed through an Async Compiler
* instance while it is active. If this is called after {@link dispose} on the
* Async Compiler instance, an error will be thrown.
*
* During the Async Compiler instance's lifespan, given the same input, this
* will return an identical result to the {@link compileStringAsync} method
* exposed at the module root.
*/
compileStringAsync(
source: string,
options?: StringOptions<'async'>
): Promise<CompileResult>;
/**
* Ends the lifespan of this Async Compiler instance. After this is invoked,
* all subsequent calls to the Compiler instance's `compileAsync` or
* `compileStringAsync` methods will result in an error.
*
* Any compilations that are submitted before `dispose` will not be cancelled,
* and will be allowed to settle.
*
* After all compilations have been settled and Sass completes any internal
* task cleanup, `dispose` will resolve its promise.
*/
dispose(): Promise<void>;
}
/**
* Synchronously compiles the Sass file at `path` to CSS. If it succeeds it
* returns a {@link CompileResult}, and if it fails it throws an {@link
@ -44,10 +142,16 @@ export interface CompileResult {
*
* This only allows synchronous {@link Importer}s and {@link CustomFunction}s.
*
* **Heads up!** When using the `sass-embedded` npm package,
* **{@link compileAsync} is almost always faster than {@link compile}**, due to
* the overhead of emulating synchronous messaging with worker threads and
* concurrent compilations being blocked on main thread.
* **Heads up!** When using the [sass-embedded] npm package for single
* compilations, **{@link compileAsync} is almost always faster than
* {@link compile}**, due to the overhead of emulating synchronous messaging
* with worker threads and concurrent compilations being blocked on main thread.
*
* If you are running multiple compilations with the [sass-embedded] npm
* package, using a {@link Compiler} will provide some speed improvements over
* the module-level methods, and an {@link AsyncCompiler} will be much faster.
*
* [sass-embedded]: https://www.npmjs.com/package/sass-embedded
*
* @example
*
@ -99,12 +203,18 @@ export function compileAsync(
*
* This only allows synchronous {@link Importer}s and {@link CustomFunction}s.
*
* **Heads up!** When using the `sass-embedded` npm package,
* **{@link compileStringAsync} is almost always faster than
* **Heads up!** When using the [sass-embedded] npm package for single
* compilations, **{@link compileStringAsync} is almost always faster than
* {@link compileString}**, due to the overhead of emulating synchronous
* messaging with worker threads and concurrent compilations being blocked on
* main thread.
*
* If you are running multiple compilations with the [sass-embedded] npm
* package, using a {@link Compiler} will provide some speed improvements over
* the module-level methods, and an {@link AsyncCompiler} will be much faster.
*
* [sass-embedded]: https://www.npmjs.com/package/sass-embedded
*
* @example
*
* ```js
@ -162,3 +272,71 @@ export function compileStringAsync(
source: string,
options?: StringOptions<'async'>
): Promise<CompileResult>;
/**
* Creates a synchronous {@link Compiler}. Each compiler instance exposes the
* {@link compile} and {@link compileString} methods within the lifespan of the
* Compiler. Given identical input, these methods will return results identical
* to their counterparts exposed at the module root. To use asynchronous
* compilation, use {@link initAsyncCompiler}.
*
* When calling the compile functions multiple times, using a compiler instance
* with the [sass-embedded] npm package is much faster than using the top-level
* compilation methods or the [sass] npm package.
*
* [sass-embedded]: https://www.npmjs.com/package/sass-embedded
*
* [sass]: https://www.npmjs.com/package/sass
*
* @example
*
* ```js
* const sass = require('sass');
* function setup() {
* const compiler = sass.initCompiler();
* const result1 = compiler.compileString('a {b: c}').css;
* const result2 = compiler.compileString('a {b: c}').css;
* compiler.dispose();
*
* // throws error
* const result3 = sass.compileString('a {b: c}').css;
* }
* ```
* @category Compile
* @compatibility dart: "1.70.0", node: false
*/
export function initCompiler(): Compiler;
/**
* Creates an asynchronous {@link AsyncCompiler}. Each compiler
* instance exposes the {@link compileAsync} and {@link compileStringAsync}
* methods within the lifespan of the Compiler. Given identical input, these
* methods will return results identical to their counterparts exposed at the
* module root. To use synchronous compilation, use {@link initCompiler};
*
* When calling the compile functions multiple times, using a compiler instance
* with the [sass-embedded] npm package is much faster than using the top-level
* compilation methods or the [sass] npm package.
*
* [sass-embedded]: https://www.npmjs.com/package/sass-embedded
*
* [sass]: https://www.npmjs.com/package/sass
*
* @example
*
* ```js
* const sass = require('sass');
* async function setup() {
* const compiler = await sass.initAsyncCompiler();
* const result1 = await compiler.compileStringAsync('a {b: c}').css;
* const result2 = await compiler.compileStringAsync('a {b: c}').css;
* await compiler.dispose();
*
* // throws error
* const result3 = await sass.compileStringAsync('a {b: c}').css;
* }
* ```
* @category Compile
* @compatibility dart: "1.70.0", node: false
*/
export function initAsyncCompiler(): Promise<AsyncCompiler>;

View File

@ -308,6 +308,139 @@ export interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
nonCanonicalScheme?: string | string[];
}
declare const nodePackageImporterKey: unique symbol;
/**
* The built-in Node.js package importer. This loads pkg: URLs from node_modules
* according to the standard Node.js resolution algorithm.
*
* A Node.js package importer is exposed as a class that can be added to the
* `importers` option.
*
*```js
* const sass = require('sass');
* sass.compileString('@use "pkg:vuetify', {
* importers: [new sass.NodePackageImporter()]
* });
*```
*
* ## Writing Sass packages
*
* Package authors can control what is exposed to their users through their
* `package.json` manifest. The recommended method is to add a `sass`
* conditional export to `package.json`.
*
* ```json
* // node_modules/uicomponents/package.json
* {
* "exports": {
* ".": {
* "sass": "./src/scss/index.scss",
* "import": "./dist/js/index.mjs",
* "default": "./dist/js/index.js"
* }
* }
* }
* ```
*
* This allows a package user to write `@use "pkg:uicomponents"` to load the
* file at `node_modules/uicomponents/src/scss/index.scss`.
*
* The Node.js package importer supports the variety of formats supported by
* Node.js [package entry points], allowing authors to expose multiple subpaths.
*
* [package entry points]:
* https://nodejs.org/api/packages.html#package-entry-points
*
* ```json
* // node_modules/uicomponents/package.json
* {
* "exports": {
* ".": {
* "sass": "./src/scss/index.scss",
* },
* "./colors.scss": {
* "sass": "./src/scss/_colors.scss",
* },
* "./theme/*.scss": {
* "sass": "./src/scss/theme/*.scss",
* },
* }
* }
* ```
*
* This allows a package user to write:
*
* - `@use "pkg:uicomponents";` to import the root export.
* - `@use "pkg:uicomponents/colors";` to import the colors partial.
* - `@use "pkg:uicomponents/theme/purple";` to import a purple theme.
*
* Note that while library users can rely on the importer to resolve
* [partials](https://sass-lang.com/documentation/at-rules/use#partials), [index
* files](https://sass-lang.com/documentation/at-rules/use#index-files), and
* extensions, library authors must specify the entire file path in `exports`.
*
* In addition to the `sass` condition, the `style` condition is also
* acceptable. Sass will match the `default` condition if it's a relevant file
* type, but authors are discouraged from relying on this. Notably, the key
* order matters, and the importer will resolve to the first value with a key
* that is `sass`, `style`, or `default`, so you should always put `default`
* last.
*
* To help package authors who haven't transitioned to package entry points
* using the `exports` field, the Node.js package importer provides several
* fallback options. If the `pkg:` URL does not have a subpath, the Node.js
* package importer will look for a `sass` or `style` key at the root of
* `package.json`.
*
* ```json
* // node_modules/uicomponents/package.json
* {
* "sass": "./src/scss/index.scss",
* }
* ```
*
* This allows a user to write `@use "pkg:uicomponents";` to import the
* `index.scss` file.
*
* Finally, the Node.js package importer will look for an `index` file at the
* package root, resolving partials and extensions. For example, if the file
* `_index.scss` exists in the package root of `uicomponents`, a user can import
* that with `@use "pkg:uicomponents";`.
*
* If a `pkg:` URL includes a subpath that doesn't have a match in package entry
* points, the Node.js importer will attempt to find that file relative to the
* package root, resolving for file extensions, partials and index files. For
* example, if the file `src/sass/_colors.scss` exists in the `uicomponents`
* package, a user can import that file using `@use
* "pkg:uicomponents/src/sass/colors";`.
*
* @compatibility dart: "1.71.0", node: false
* @category Importer
*/
export class NodePackageImporter {
/** Used to distinguish this type from any arbitrary object. */
private readonly [nodePackageImporterKey]: true;
/**
* The NodePackageImporter has an optional `entryPointDirectory` option, which
* is the directory where the Node Package Importer should start when
* resolving `pkg:` URLs in sources other than files on disk. This will be
* used as the `parentURL` in the [Node Module
* Resolution](https://nodejs.org/api/esm.html#resolution-algorithm-specification)
* algorithm.
*
* In order to be found by the Node Package Importer, a package will need to
* be inside a node_modules folder located in the `entryPointDirectory`, or
* one of its parent directories, up to the filesystem root.
*
* Relative paths will be resolved relative to the current working directory.
* If a path is not provided, this defaults to the parent directory of the
* Node.js entrypoint. If that's not available, this will throw an error.
*/
constructor(entryPointDirectory?: string);
}
/**
* The result of successfully loading a stylesheet with an {@link Importer}.
*

View File

@ -3,11 +3,15 @@
// implementations.
export {
AsyncCompiler,
CompileResult,
Compiler,
compile,
compileAsync,
compileString,
compileStringAsync,
initCompiler,
initAsyncCompiler,
} from './compile';
export {Exception} from './exception';
export {
@ -15,6 +19,7 @@ export {
FileImporter,
Importer,
ImporterResult,
NodePackageImporter,
} from './importer';
export {Logger, SourceSpan, SourceLocation} from './logger';
export {

View File

@ -1,6 +1,7 @@
import {Logger} from '../logger';
import {LegacyImporter} from './importer';
import {LegacyFunction} from './function';
import {NodePackageImporter} from '../importer';
/**
* Options for {@link render} and {@link renderSync} that are shared between
@ -508,6 +509,24 @@ export interface LegacySharedOptions<sync extends 'sync' | 'async'> {
* @compatibility dart: "1.43.0", node: false
*/
logger?: Logger;
/**
* If this option is set to an instance of `NodePackageImporter`, Sass will
* use the built-in Node.js package importer to resolve Sass files with a
* `pkg:` URL scheme. Details for library authors and users can be found in
* the {@link NodePackageImporter} documentation.
*
* @example
* ```js
* sass.renderSync({
* data: '@use "pkg:vuetify";',
* pkgImporter: new sass.NodePackageImporter()
* });
* ```
* @category Plugins
* @compatibility dart: "2.0", node: false
*/
pkgImporter?: NodePackageImporter;
}
/**

View File

@ -1,4 +1,4 @@
import {FileImporter, Importer} from './importer';
import {FileImporter, Importer, NodePackageImporter} from './importer';
import {Logger} from './logger';
import {Value} from './value';
import {PromiseOr} from './util/promise_or';
@ -208,8 +208,8 @@ export interface Options<sync extends 'sync' | 'async'> {
* - The importer that was used to load the current stylesheet, with the
* loaded URL resolved relative to the current stylesheet's canonical URL.
*
* - Each {@link Importer} or {@link FileImporter} in {@link importers}, in
* order.
* - Each {@link Importer}, {@link FileImporter}, or
* {@link NodePackageImporter} in {@link importers}, in order.
*
* - Each load path in {@link loadPaths}, in order.
*
@ -218,7 +218,7 @@ export interface Options<sync extends 'sync' | 'async'> {
*
* @category Plugins
*/
importers?: (Importer<sync> | FileImporter<sync>)[];
importers?: (Importer<sync> | FileImporter<sync> | NodePackageImporter)[];
/**
* Paths in which to look for stylesheets loaded by rules like

View File

@ -31,6 +31,12 @@ colors outside the sRGB gamut.
* [`color.mix()`](#colormix)
* [Deprecations](#deprecations)
* [Design Decisions](#design-decisions)
* [Unclamped Channels](#unclamped-channels)
* [Clamped Channels](#clamped-channels)
* [Changing Color Spaces](#changing-color-spaces)
* [Gamut Mapping](#gamut-mapping)
* [CSS Color 5](#css-color-5)
* [Special Thanks](#special-thanks)
* [Definitions](#definitions)
* [Color](#color)
* [Legacy Color](#legacy-color)
@ -42,11 +48,12 @@ colors outside the sRGB gamut.
* [Color Interpolation Method](#color-interpolation-method)
* [Serialization](#serialization)
* [Serialization of Non-Legacy Colors](#serialization-of-non-legacy-colors)
* [Serialization of Out-of-Gamut RGB Colors](#serialization-of-out-of-gamut-rgb-colors)
* [Procedures](#procedures)
* [Looking Up a Known Color Space](#looking-up-a-known-color-space)
* [Converting a Color](#converting-a-color)
* [CSS-Converting a Color Space](#css-converting-a-color-space)
* [Gamut Mapping](#gamut-mapping)
* [CSS-Converting a Color Space](#css-converting-a-color-space)
* [Gamut Mapping](#gamut-mapping-1)
* [Parsing Color Components](#parsing-color-components)
* [Percent-Converting a Number](#percent-converting-a-number)
* [Validating a Color Channel](#validating-a-color-channel)
@ -420,21 +427,66 @@ being deprecated in favor of color-space-friendly functions like
### Design Decisions
Most of the design decisions involved in the proposal are based on the
[CSS Color Level 4][color-4] specification, which we have tried to emulate as
closely as possible, while maintaining support for legacy projects. In some
cases, that required major changes to the way Sass handles colors:
#### Unclamped Channels
Most of the design decisions involved in the proposal are based on the [CSS
Color Level 4][color-4] specification, which we have tried to emulate as closely
as possible while maintaining support for legacy projects. In some cases, that
required major changes to the way Sass handles colors:
1. Channel values are no longer internally clamped to the gamut of a color
space. By default Sass will output CSS with out-of-gamut colors, because
these colors are handled differently when doing interpolation across
different color spaces. Browsers may also eventually handle gamut-mapping for
these colors, although at the time of writing they do not. Authors can also
use the provided `color.to-gamut()` function to force a color to be mapped
into the gamut of its native color space.
1. RGB-style channel values are no longer clamped to the gamut of a color space,
except for the `hsl` and `hwb` spaces, which are unable to represent
out-of-gamut colors. By default Sass will output CSS with out-of-gamut
colors, because browsers can provide better gamut mapping based on the user
device capabilities. However, authors can use the provided `color.to-gamut()`
function to enforce mapping a color into a specific gamut.
2. RGB-style channel values are no longer rounded to the nearest integer, since
the spec now requires maintaining precision wherever possible. This is
especially important in RGB spaces, where color distribution is inconsistent.
#### Clamped Channels
Per the CSS specs, certain channels are clamped at parse time [but *not* in
interpolation] for specific color functions:
[but *not* in interpolation]: https://github.com/w3c/csswg-drafts/issues/9484
* All channels of the `rgb()` and `rgba()` functions.
* The lightness channel of the `lab()`, `lch()`, `oklab()`, and `oklch()`
functions.
* The lower bound of the chroma channel of the `lch()` and `oklch()` functions.
However, it's necessary to use out-of-gamut values in these spaces to represent
valid colors from other spaces—for example, `color(xyz 1 1 1)` is equivalent to
`lab(100.12% 9.0645 5.8018)` and `color(prophoto-rgb 0 1 0)` is equivalent to
`rgb(-221.6192400378, 279.4082218845, -109.1140773956)`. To match the behavior
of CSS, we have to clamp these channels when constructing colors directly
through color functions, so writing `lab(110% 0 0)` will return `lab(100% 0 0)`.
On the other hand, to preserve colors on a round trip between spaces, we need to
allow the internal representation of these colors to go out-of-gamut.
The question remains as to how to handle cases that don't directly correspond to
CSS, such as the `color.change()` function. Because out-of-gamut clamped
channels are meaningful in CSS, we've chosen the design principle of preserving
them in all situations where clamping isn't specifically mandated by CSS. In
addition, to ensure that that the colors represent the same values in CSS that
they do in Sass, they'll be serialized to special formats that preserves their
out-of-gamut values:
* Out-of-gamut RGB colors are serialized to `hsl()`, the lightness channel and
saturation upper bound of which per spec are not clamped at parse-time
(although in practice browsers do clamp them at time of writing). This ensures
that older browsers will still handle these colors somewhat correctly while
preserving the unclamped value for modern browsers (once they work per spec).
* Out-of-gamut Lab, LCH, OKLab, and OKLCH colors are serialized to `color-mix(in
..., color(xyz ...) 100%, black)` to preserve both the original color space
and the unclamped value in conformant browsers.
#### Changing Color Spaces
Different color spaces often represent different color-gamuts, which can present
a new set of problems for authors. Some color manipulations are best handled
in a wide-gamut space like `oklch`, but (for now) authors will likely prefer
@ -454,6 +506,8 @@ and mapping in Sass color functions:
* No color function performs gamut-mapping on out-of-gamut channels, except
`color.to-gamut()`, which can be used for manual gamut-mapping.
#### Gamut Mapping
Browsers currently use channel-clipping rather than the proposed
[css gamut mapping algorithm][css-mapping] to handle colors that cannot be
shown correctly on a given display. We've decided to provide `color.to-gamut()`
@ -463,6 +517,8 @@ will consider adding an additional algorithm-selection argument. However, the
primary goal of this function is not to match CSS behavior, but to provide a
better mapping than the default channel-clipping.
#### CSS Color 5
We are not attempting to support all of [CSS Color Level 5][color-5] at this
point, since it is not yet implemented in browsers. However, we have used it as
a reference while updating color manipulation functions such as `color.mix()`.
@ -479,6 +535,8 @@ provides us with the most flexibility to change our behavior in the future.
[color-5]: https://www.w3.org/TR/css-color-5/
[relative color syntax]: https://drafts.csswg.org/css-color-5/#relative-colors
#### Special Thanks
Thanks to the editors of the CSS Color Level 4 specification (Tab Atkins Jr.,
Chris Lilley, and Lea Verou) for answering our many questions along the way. We
also used Chris and Lea's [Color.js](https://colorjs.io/) library as a
@ -554,9 +612,10 @@ unquoted lowercase strings by inspection functions.
Values outside a *bounded gamut* range (including infinity or negative infinity)
are valid but are considered *out of gamut* for the given color space. They
remain un-clamped unless the gamut is specifically marked as "clamped". If the
channel is bounded, or has a percentage mapping, then the channel is considered
*scalable*.
remain un-clamped unless the gamut is specifically marked as "clamped", in which
case they're clamped *only* when constructing the color from its global
constructor function. If the channel is bounded, or has a percentage mapping,
then the channel is considered *scalable*.
Some color spaces use a *polar angle* value for the `hue` channel. Polar-angle
hues represent an angle position around a given hue wheel, using a CSS `<angle>`
@ -570,7 +629,7 @@ The known color spaces and their channels are:
* `rgb` (RGB, legacy):
* `red`, `green`, `blue`:
* gamut: bounded
* gamut: bounded, clamped
* number: `[0,255]`
> Percentages `[0%,100%]` map to the `[0,255]` range.
@ -589,11 +648,11 @@ The known color spaces and their channels are:
* associated unit: `deg`
* degrees: polar angle
* `saturation`:
* gamut: bounded
* gamut: bounded, clamped (lower bound only)
* associated unit: `%`
* percentage: `[0%,100%]`
* `lightness`:
* gamut: bounded, clamped
* gamut: bounded
* associated unit: `%`
* percentage: `[0%,100%]`
@ -635,7 +694,7 @@ The known color spaces and their channels are:
> Percentages `[0%,100%]` map to the `[0,100]` range.
* `chroma`:
* gamut: un-bounded
* gamut: un-bounded, clamped (lower bound only)
* number: `[0,150]`
> Percentages `[0%,100%]` map to the `[0,150]` range.
@ -667,7 +726,7 @@ The known color spaces and their channels are:
> Percentages `[0%,100%]` map to the `[0,1]` range.
* `chroma`:
* gamut: un-bounded
* gamut: un-bounded, clamped (lower bound only)
* number: `[0,0.4]`
> Percentages `[0%,100%]` map to the `[0,0.4]` range.
@ -786,6 +845,18 @@ To serialize a non-legacy color `color`:
* Let `space-name` be an unquoted lowercase string of `color`'s space name.
* If `color` has a clamped channel whose value is out-of-bounds:
* Let `xyz` be the result of [converting] `color` into the `xyz` color space.
* Emit "color-mix(in ", followed by `space-name`, ", ", the result of
serializing `xyz`, and then " 100%, black)".
> This syntax effectively converts the `xyz` color back into the original
> color space. The second argument to `color-mix()` is irrelevant when the
> first color has weight 100%, so implementations are free to change it (for
> example, an implementation might use "red" instead to minimize bytes).
* Let `known-space` be the result of [looking up a known color space] with a
`name` of `space-name`.
@ -824,6 +895,14 @@ To serialize a non-legacy color `color`:
[predefined color space]: #predefined-color-spaces
### Serialization of Out-of-Gamut RGB Colors
To serialize an out-of-gamut color `color` in the `rgb` space:
* Let `hsl` be the result of [converting] `color` into the `hsl` space.
* Return the result of serializing `hsl`.
## Procedures
### Looking Up a Known Color Space
@ -881,7 +960,7 @@ returns a color `color`.
[missing]: #missing-components
[powerless]: #powerless-components
## CSS-Converting a Color Space
### CSS-Converting a Color Space
[css-converting]: #css-converting-a-color-space
@ -1147,9 +1226,10 @@ normalized channel value otherwise.
* Otherwise, set `channel` to the result of [percent-converting] `channel`
with a `min` and `max` defined by the `valid` channel range.
* If `valid` is a `lightness` channel, and `space` is not a [legacy color]
space, set `channel` to the result of clamping the `channel` value between
0 and 100, inclusive.
* If this was (transitively) invoked from the global [`rgb()`, `lab()`,
`lch()`, `oklab()`, `oklch()`, or `color()`] function and `valid` is a
clamped channel, return the result of clamping `channel` to its native
range.
* Return `channel`.
@ -1159,7 +1239,7 @@ normalized channel value otherwise.
[normalizing]: #normalizing-color-channels
This process accepts a list of `channels` to validate, and a [known color space]
This process accepts a list of `channels` to validate and a [known color space]
`space` to normalize against. It throws an error if any channel is invalid for
the color space, or returns a normalized list of valid channels otherwise.

View File

@ -1,22 +0,0 @@
## Draft 1.3
* Handle empty subpath in "Resolving package exports" subprocedure.
## Draft 1.2
* Export `NodePackageImporter` type, and set `_NodePackageImporterBrand` to
unknown.
## Draft 1.1
* Throw an error if `nodePackageImporter` is used in the browser or other
environment without filesystem access.
* Remove specified order in the global import list, as users can specify the
order within the `importers` option.
* Specify importer ordering for the Legacy API.
## Draft 1
* Initial draft

View File

@ -0,0 +1,47 @@
# `feature-exists` removal: Draft 1.0
*([Issue](https://github.com/sass/sass/issues/3702))*
This proposal removes `feature-exists` in the built-in `sass:meta` module.
## Table of Contents
* [Background](#background)
* [Summary](#summary)
* [Deprecation Process](#deprecation-process)
* [`feature-exists`](#feature-exists)
## Background
> This section is non-normative.
`feature-exists` is ill-defined as nothing specifies what each feature actually
requires to be considered as supported. The shared `sass-spec` testsuite does
not cover this either. Thus, no new feature identifier has been added in it
for years.
New Sass features essentially fall into one of three categories:
1. New built-in functions or mixins, which are easy to detect using other
`sass:meta` functions.
2. Language-level features which are relatively easy to detect on their own
(for example, first-class calc can be detected with `calc(1 + 1) == 2)`.
3. New syntax which can't even be parsed in implementations that don't support
it, for which feature detection isn't particularly useful anyway.
## Summary
> This section is non-normative.
The `feature-exists` function of `sass:meta` and its global alias will be
removed without any direct replacement.
## Deprecation Process
The `feature-exists` function will be supported until the next major version
with a deprecation warning.
### `feature-exists`
During the deprecation period, when invoking the `feature-exists` function,
emit a deprecation warning named `feature-exists`.

View File

@ -447,24 +447,54 @@ This function is also available as a global function named `hue()`.
hwb($channels)
```
* If `$channels` is not an unbracketed space-separated list, throw an error.
* If `$channels` is an unbracketed slash-separated list:
* If `$channels` does not includes exactly three elements, throw an error.
* If `$channels` doesn't have exactly two elements, throw an error.
Otherwise, let `hwb` be the first element and `alpha` the second element.
* Let `hue` and `whiteness` be the first two elements of `$channels`
* If either `hwb` or `alpha` is a [special variable string], return a plain
CSS function string with the name `"hwb"` and the argument `$channels`.
* If the third element of `$channels` has preserved its status as
two slash-separated numbers:
* If `hwb` is not an unbracketed space-separated list, throw an error.
* Let `blackness` be the number before the slash and `alpha` the number
after the slash.
* If the first element of `hwb` is an unquoted string which is
case-insensitively equal to `from`, return a plain CSS function string
with the name `"hwb"` and the argument `$channels`.
* Otherwise:
* If `hwb` has more than three elements, throw an error.
* Let `blackness` be the third element of `$channels`.
* If `hwb` has fewer than three elements:
* Call `hwb()` with `hue`, `whiteness`, `blackness`, and `alpha` (if it's
defined) as arguments and return the result.
* If any element of `hwb` is a special variable string, return a plain
CSS function string with the name `"hwb"` and the argument `$channels`.
* Otherwise, throw an error.
* Let `hue`, `whiteness`, and `blackness` be the three elements of `hwb`.
* Call `hwb()` with `hue`, `whiteness`, `blackness`, and `alpha` as
arguments and return the result.
* Otherwise, If `$channels` is not an unbracketed space-separated list, throw an error.
* If `$channels` does not includes exactly three elements, throw an error.
* Let `hue` and `whiteness` be the first two elements of `$channels`
* If the third element of `$channels` has preserved its status as
two slash-separated numbers:
* Let `blackness` be the number before the slash and `alpha` the number
after the slash.
* Otherwise:
* Let `blackness` be the third element of `$channels`.
* Call `hwb()` with `hue`, `whiteness`, `blackness`, and `alpha` (if it's
defined) as arguments and return the result.
[special variable string]: ../functions.md#special-variable-string
### `ie-hex-str()`

View File

@ -72,6 +72,7 @@ This function is also available as a global function named `list-separator()`.
slash($elements...)
```
* Emit a deprecation warning.
* If `$elements` contains zero or one values, throw an error.
* Return an unbracketed slash-separated list containing `$elements`.

View File

@ -98,6 +98,13 @@ message InboundMessage {
// generating this ID and ensuring that it's unique across all importers
// registered for this compilation.
uint32 file_importer_id = 3;
// The [Node.js package importer], which is a built-in Package Importer
// with an associated `entry_point_directory` that resolves `pkg:` URLs
// using the standards and conventions of the Node ecosystem.
//
// [Node.js package importer]: https://github.com/sass/sass/tree/main/spec/modules.md#node-package-importer
NodePackageImporter node_package_importer = 5;
}
// The set of URL schemes that are considered *non-canonical* for this
@ -148,6 +155,9 @@ message InboundMessage {
// Whether to emit a `@charset`/BOM for non-ASCII stylesheets.
bool charset = 13;
// Whether to silently suppresses all `LogEvent`s.
bool silent = 14;
}
// A response indicating the result of canonicalizing an imported URL.
@ -1018,3 +1028,16 @@ enum CalculationOperator {
// The division operator.
DIVIDE = 3;
}
// The built-in Node.js Package Importer, which is a Package Importer that
// resolves using the standards and conventions of the Node.js ecosystem. It
// enables a `pkg:` URL scheme for usage with `@use` that directs an
// implementation to resolve a URL within a dependency.
message NodePackageImporter {
// The absolute path to associate with the Node Package Importer, with
// semantics identical to the [entryPointDirectory option] in the JavaScript
// API.
//
// [entryPointDirectory option]: https://sass-lang.com/documentation/js-api/classes/NodePackageImporter.html#constructor
string entry_point_directory = 1;
}

View File

@ -291,7 +291,8 @@ plain CSS function named `"rgb"` that function is named `"rgba"` instead.
* Call `rgb()` with `red`, `green`, `blue`, and `alpha` as arguments and
return the result.
* If `$channels` is not an unbracketed space-separated list, throw an error.
* Otherwise, if `$channels` is not an unbracketed space-separated list, throw
an error.
* If the first element of `$channels` is an unquoted string which is
case-insensitively equal to `from`, return a plain CSS function string
@ -425,7 +426,8 @@ plain CSS function named `"hsl"` that function is named `"hsla"` instead.
* Call `hsl()` with `hue`, `saturation`, `lightness`, and `alpha` as
arguments and return the result.
* If `$channels` is not an unbracketed space-separated list, throw an error.
* Otherwise, if `$channels` is not an unbracketed space-separated list, throw
an error.
* If the first element of `$channels` is an unquoted string which is
case-insensitively equal to `from`, return a plain CSS function string

View File

@ -12,11 +12,17 @@ import {Options, StringOptions} from './options';
* [Types](#types)
* [`CompileResult`](#compileresult)
* [`Compiler`](#compiler)
* [`dispose()`](#dispose)
* [`AsyncCompiler`](#asynccompiler)
* [`dispose()`](#dispose-1)
* [Functions](#functions)
* [`compile`](#compile)
* [`compileAsync`](#compileasync)
* [`compileString`](#compilestring)
* [`compileStringAsync`](#compilestringasync)
* [`initCompiler`](#initcompiler)
* [`initAsyncCompiler`](#initasynccompiler)
## Types
@ -34,12 +40,109 @@ export interface CompileResult {
}
```
### `Compiler`
The class returned by creating a synchronous compiler with [`initCompiler()`].
If the class is directly constructed (as opposed to the Compiler being created
via `initCompiler`), throw an error.
Only synchronous compilation methods [`compile()`] and [`compileString()`] must
be included, and must have identical semantics to the [Sass interface].
[`initCompiler()`]: #initcompiler
[`compile()`]: #compile
[`compilestring()`]: #compilestring
[Sass interface]: ./index.d.ts.md
```ts
export class Compiler {
private constructor();
compile(path: string, options?: Options<'sync'>): CompileResult;
compileString(source: string, options?: StringOptions<'sync'>): CompileResult;
```
#### `dispose()`
When `dispose` is invoked on a Compiler:
* Any subsequent invocations of `compile` and `compileString` must throw an
error.
```ts
dispose(): void;
}
```
### `AsyncCompiler`
The object returned by creating an asynchronous compiler with
[`initAsyncCompiler()`]. If the class is directly constructed (as opposed to the
AsyncCompiler being created via `initAsyncCompiler`), throw an error.
Only asynchronous compilation methods [`compileAsync()`] and
[`compileStringAsync()`] must be included, and must have identical semantics to
the [Sass interface].
[`initAsyncCompiler()`]: #initasynccompiler
[`compileasync()`]: #compileasync
[`compilestringasync()`]: #compilestringasync
```ts
export class AsyncCompiler {
private constructor();
compileAsync(
path: string,
options?: Options<'async'>
): Promise<CompileResult>;
compileStringAsync(
source: string,
options?: StringOptions<'async'>
): Promise<CompileResult>;
```
#### `dispose()`
When `dispose` is invoked on an Async Compiler:
* Any subsequent invocations of `compileAsync` and `compileStringAsync` must
throw an error.
* Any compilations that have not yet been settled must be allowed to settle, and
not be cancelled.
* Resolves a Promise when all compilations have been settled, and disposal is
complete.
```ts
dispose(): Promise<void>;
}
```
## Functions
### `compile`
Compiles the Sass file at `path`:
* If any item in `options.importers` is an instance of the `NodePackageImporter`
class:
* If no filesystem is available, throw an error.
> This primarily refers to a browser environment, but applies to other
> sandboxed JavaScript environments as well.
* Let `pkgImporter` be a [Node Package Importer] with an associated
`entryPointURL` of the absolute file URL for the object's
[`entryPointDirectory`].
[Node Package Importer]: ../modules.md#node-package-importer
[`entryPointDirectory`]: importer.d.ts.md#constructor
* Replace the item with `pkgImporter` in a copy of `options.importers`.
* If any object in `options.importers` has both `findFileUrl` and `canonicalize`
fields, throw an error.
@ -91,6 +194,20 @@ export function compileAsync(
Compiles the Sass `source`:
* If any item in `options.importers` is an instance of the `NodePackageImporter`
class:
* If no filesystem is available, throw an error.
> This primarily refers to a browser environment, but applies to other
> sandboxed JavaScript environments as well.
* Let `pkgImporter` be a [Node Package Importer] with an associated
`entryPointURL` of the absolute file URL for the object's
[`entryPointDirectory`].
* Replace the item with `pkgImporter` in a copy of `options.importers`.
* If `options.importer` or any object in `options.importers` has both
`findFileUrl` and `canonicalize` fields, throw an error.
@ -146,3 +263,23 @@ export function compileStringAsync(
options?: StringOptions<'async'>
): Promise<CompileResult>;
```
### `initCompiler`
Returns a synchronous [Compiler].
[Compiler]: #compiler
```ts
export function initCompiler(): Compiler;
```
### `initAsyncCompiler`
Resolves with an [Async Compiler].
[Async Compiler]: #asynccompiler
```ts
export function initAsyncCompiler(): Promise<AsyncCompiler>;
```

View File

@ -15,6 +15,9 @@ import {PromiseOr} from './util/promise_or';
* [`FileImporter`](#fileimporter)
* [`Importer`](#importer)
* [`nonCanonicalScheme`](#noncanonicalscheme)
* [`NodePackageImporter`](#nodepackageimporter)
* [`entryPointDirectory`](#entrypointdirectory)
* [Constructor](#constructor)
* [`ImporterResult`](#importerresult)
## Types
@ -161,6 +164,56 @@ nonCanonicalScheme?: string | string[];
} // Importer
```
### `NodePackageImporter`
```ts
declare const nodePackageImporterKey: unique symbol;
export class NodePackageImporter {
/** Used to distinguish this type from any arbitrary object. */
private readonly [nodePackageImporterKey]: true;
```
#### `entryPointDirectory`
This is a private, spec-only string property that's used to determine the value
of the `parentURL` parameter to `PACKAGE_RESOLVE` function from the [Node.js
Resolution Algorithm].
[Node.js Resolution Algorithm]: https://nodejs.org/api/esm.html#resolution-algorithm-specification
#### Constructor
Creates a `NodePackageImporter`:
* If no filesystem is available, throw an error.
> This primarily refers to a browser environment, but applies to other
> sandboxed JavaScript environments as well.
* If `entryPointDirectory` is not `undefined`, set the `entryPointDirectory`
property to its value.
* Otherwise, if the path to the first JavaScript file that was loaded from the
filesystem exists, set the `entryPointDirectory` property to the directory
containing that path.
> In Node.js, this can be accessed using `require.main.filename` if the
> entrypoint is CommonJS, but otherwise must be determined using
> `process.argv[1]`. See nodejs/node#49440.
* Otherwise, throw an error.
* Return `this`.
```ts
constructor(entryPointDirectory?: string);
```
```ts
} // NodePackageImporter
```
### `ImporterResult`
```ts

View File

@ -40,11 +40,15 @@ proposals.
```ts
export {
AsyncCompiler,
CompileResult,
Compiler,
compile,
compileAsync,
compileString,
compileStringAsync,
initCompiler,
initAsyncCompiler,
} from './compile';
export {Exception} from './exception';
export {
@ -52,6 +56,7 @@ export {
FileImporter,
Importer,
ImporterResult,
NodePackageImporter,
} from './importer';
export {Logger, SourceSpan, SourceLocation} from './logger';
export {

View File

@ -4,6 +4,7 @@
import {Logger} from '../logger';
import {LegacyImporter} from './importer';
import {LegacyFunction} from './function';
import {NodePackageImporter} from '../importer';
```
## Table of Contents
@ -27,6 +28,7 @@ import {LegacyFunction} from './function';
* [`quietDeps`](#quietdeps)
* [`verbose`](#verbose)
* [`logger`](#logger)
* [`pkgImporter`](#pkgimporter)
* [`LegacyFileOptions`](#legacyfileoptions)
* [`LegacyStringOptions`](#legacystringoptions)
* [`LegacyOptions`](#legacyoptions)
@ -173,6 +175,19 @@ doesn't have `warn` or `debug` fields.
logger?: Logger;
```
#### `pkgImporter`
If set to an instance of [NodePackageImporter], the compiler will use it as the
specified built-in package importer to resolve any URL with the `pkg:` scheme.
This importer will be checked immediately before the existing legacy importer
logic, and if it returns `null`, the legacy importer logic will be invoked.
[NodePackageImporter]: ../importer.d.ts.md#nodepackageimporter
```ts
pkgImporter?: NodePackageImporter;
```
```ts
} // LegacySharedOptions
```

View File

@ -6,7 +6,7 @@
> [compile API]: compile.d.ts.md
```ts
import {FileImporter, Importer} from './importer';
import {FileImporter, Importer, NodePackageImporter} from './importer';
import {Logger} from './logger';
import {Value} from './value';
import {PromiseOr} from './util/promise_or';
@ -165,12 +165,14 @@ functions?: Record<string, CustomFunction<sync>>;
#### `importers`
The list of [custom importers] to use to resolve file loads.
The list of [custom importers] or [package importers] to use to resolve file
loads.
[custom importers]: importer.d.ts.md
[package importers]: importer.d.ts.md
```ts
importers?: (Importer<sync> | FileImporter<sync>)[];
importers?: (Importer<sync> | FileImporter<sync> | NodePackageImporter)[];
```
#### `loadPaths`

View File

@ -12,6 +12,8 @@
* [Built-In Module](#built-in-module)
* [Importer](#importer)
* [Filesystem Importer](#filesystem-importer)
* [Package Importers](#package-importers)
* [Node Package Importer](#node-package-importer)
* [Global Importer List](#global-importer-list)
* [Basename](#basename)
* [Dirname](#dirname)
@ -23,6 +25,13 @@
* [Resolving a `file:` URL for Extensions](#resolving-a-file-url-for-extensions)
* [Resolving a `file:` URL for Partials](#resolving-a-file-url-for-partials)
* [Resolving a Member](#resolving-a-member)
* [Node Package Importer Semantics](#node-package-importer-semantics)
* [Node Algorithm for Resolving a `pkg:` URL](#node-algorithm-for-resolving-a-pkg-url)
* [Resolving a package name](#resolving-a-package-name)
* [Resolving the root directory for a package](#resolving-the-root-directory-for-a-package)
* [Resolving package exports](#resolving-package-exports)
* [Resolving package root values](#resolving-package-root-values)
* [Export Load Paths](#export-load-paths)
## Definitions
@ -211,6 +220,81 @@ named `string`:
[parsing a URL]: https://url.spec.whatwg.org/#concept-url-parser
### Package Importers
This proposal defines the requirements for Package Importers written by users or
provided by implementations. It is a type of [Importer] and, in addition to the
standard requirements for importers, it must handle only non-canonical URLs that:
* have the scheme `pkg`, and
* whose path begins with a package name, and
* optionally followed by a path, with path segments separated with a forward
slash.
The package name will often be the first path segment, but the importer may take
into account any conventions in the environment. For instance, Node supports
scoped package names, which start with `@` followed by 2 path segments. Note
that package names that contain non-alphanumeric characters may be less portable
across different package importers.
Package Importers must reject the following patterns:
* A URL whose path begins with `/`.
* A URL with non-empty/null username, password, host, port, query, or fragment.
[importer]: #importer
### Node Package Importer
The Node Package Importer is an implementation of a [Package Importer] using the
standards and conventions of the Node ecosystem. It has an associated absolute
`file:` URL named `entryPointURL`.
When the Node Package Importer is invoked with a string named `string`:
* If `string` is a relative URL, return null.
* Let `url` be the result of [parsing `string` as a URL][parsing a URL]. If this
returns a failure, throw that failure.
* If `url`'s scheme is not `pkg:`, return null.
* If `url`'s path begins with a `/` or is empty, throw an error.
* If `url` contains a username, password, host, port, query, or fragment, throw
an error.
* Let `sourceFile` be the canonical URL of the [current source file] that
contained the load.
* If `sourceFile`'s scheme is `file:`, let `baseURL` be `sourceFile`.
* Otherwise, let `baseURL` be `entryPointURL`.
* Let `resolved` be the result of [resolving a `pkg:` URL as Node] with `url` and
`baseURL`.
* If `resolved` is null, return null.
* Let `text` be the contents of the file at `resolved`.
* Let `syntax` be:
* "scss" if `resolved` ends in `.scss`.
* "indented" if `resolved` ends in `.sass`.
* "css" if `resolved` ends in `.css`.
> The algorithm for [resolving a `pkg:` URL as Node] guarantees that
> `resolved` will have one of these extensions.
* Return `text`, `syntax`, and `resolved`.
[Package Importer]: #package-importers
[current source file]: ./spec.md#current-source-file
[resolving a `pkg:` URL as Node]: #node-algorithm-for-resolving-a-pkg-url
### Global Importer List
The *global importer list* is a list of importers that's set for the entire
@ -322,7 +406,6 @@ null.
* Return null.
[current source file]: spec.md#current-source-file
[parsing]: syntax.md#parsing-text
### Resolving a `file:` URL
@ -506,3 +589,160 @@ and returns a member of type `type` or null.
> implementation's host language API.
* Otherwise, return null.
### Node Package Importer Semantics
#### Node Algorithm for Resolving a `pkg:` URL
This algorithm takes a URL with scheme `pkg:` named `url`, and a URL `baseURL`.
It returns a canonical `file:` URL or null.
* Let `fullPath` be `url`'s path.
* Let `packageName` be the result of [resolving a package name] with `fullPath`,
and `subpath` be `fullPath` without the `packageName`.
* Let `packageRoot` be the result of [resolving the root directory for a
package] with `packageName` and `baseURL`.
* If a `package.json` file does not exist at `packageRoot`, throw an error.
* Let `packageManifest` be the result of parsing the `package.json` file at
`packageRoot` as [JSON].
* Let `resolved` be the result of [resolving package exports] with
`packageRoot`, `subpath`, and `packageManifest`.
* If `resolved` has the scheme `file:` and an extension of `sass`, `scss` or
`css`, return it.
* Otherwise, if `resolved` is not null, throw an error.
* If `subpath` is empty, return the result of [resolving package root values].
* Let `resolved` be `subpath` resolved relative to `packageRoot`.
* Return the result of [resolving a `file:` URL] with `resolved`.
[Resolving package exports]: #resolving-package-exports
[resolving package root values]: #resolving-package-root-values
[resolving a package name]: #resolving-a-package-name
[JSON]: https://datatracker.ietf.org/doc/html/rfc8259
[resolving the root directory for a package]: #resolving-the-root-directory-for-a-package
[resolving a `file:` URL]: #resolving-a-file-url
#### Resolving a package name
This algorithm takes a string, `path`, and returns the portion that identifies
the Node package.
* If `path` starts with `@`, it is a scoped package. Return the first 2 [URL path
segments], including the separating `/`.
* Otherwise, return the first URL path segment.
#### Resolving the root directory for a package
This algorithm takes a string, `packageName`, and an absolute URL `baseURL`, and
returns an absolute URL to the root directory for the most proximate installed
`packageName`.
* Return the result of `PACKAGE_RESOLVE(packageName, baseURL)` as defined in
the [Node resolution algorithm specification].
[Node resolution algorithm specification]: https://nodejs.org/api/esm.html#resolution-algorithm-specification
#### Resolving package exports
This algorithm takes a package.json value `packageManifest`, a directory URL
`packageRoot` and a relative URL path `subpath`. It returns a file URL or null.
* Let `exports` be the value of `packageManifest.exports`.
* If `exports` is undefined, return null.
* Let `subpathVariants` be the result of [Export load paths] with `subpath`.
* Let `resolvedPaths` be a list of the results of calling
`PACKAGE_EXPORTS_RESOLVE(packageRoot, subpathVariant, exports, ["sass",
"style"])` as defined in the [Node resolution algorithm specification], with
each `subpathVariants` as `subpathVariant`.
> The PACKAGE_EXPORTS_RESOLVE algorithm always includes a `default` condition,
> so one does not have to be passed here.
* If `resolvedPaths` contains more than one resolved URL, throw an error.
* If `resolvedPaths` contains exactly one resolved URL, return it.
* If `subpath` has an extension, return null.
* Let `subpathIndex` be `subpath` + `"/index"`.
* Let `subpathIndexVariants` be the result of [Export load paths] with `subpathIndex`.
* Let `resolvedIndexPaths` be a list of the results of calling
`PACKAGE_EXPORTS_RESOLVE(packageRoot, subpathVariant, exports, ["sass",
"style"])` as defined in the [Node resolution algorithm specification], with
each `subpathIndexVariants` as `subpathVariant`.
* If `resolvedIndexPaths` contains more than one resolved URL, throw an error.
* If `resolvedIndexPaths` contains exactly one resolved URL, return it.
* Return null.
> Where possible in Node, implementations can use [resolve.exports] which
> exposes the Node resolution algorithm, allowing for per-path custom
> conditions, and without needing filesystem access.
[Export load paths]: #export-load-paths
[resolve.exports]: https://github.com/lukeed/resolve.exports
#### Resolving package root values
This algorithm takes a string `packagePath`, which is the root directory for a
package, and `packageManifest`, which is the contents of that package's
`package.json` file, and returns a file URL.
* Let `sassValue` be the value of `sass` in `packageManifest`.
* If `sassValue` is a relative path with an extension of `sass`, `scss` or
`css`:
* Return the canonicalized `file:` URL for `${packagePath}/${sassValue}`.
* Let `styleValue` be the value of `style` in `packageManifest`.
* If `styleValue` is a relative path with an extension of `sass`, `scss` or
`css`:
* Return the canonicalized `file:` URL for `${packagePath}/${styleValue}`.
* Otherwise return the result of [resolving a `file:` URL for extensions] with
`packagePath + "/index"`.
[resolving a `file:` URL for extensions]: #resolving-a-file-url-for-extensions
[URL path segments]: https://url.spec.whatwg.org/#url-path-segment
#### Export Load Paths
This algorithm takes a relative URL path `subpath` and returns a list of
potential subpaths, resolving for partials and file extensions.
* Let `paths` be a list.
* If `subpath` ends in `.scss`, `.sass`, or `.css`:
* Add `subpath` to `paths`.
* Otherwise, add `subpath`, `subpath` + `.scss`, `subpath` + `.sass`, and
`subpath` + `.css` to `paths`.
* If `subpath`'s [basename] does not start with `_`, for each `item` in
`paths`, prepend `"_"` to the basename, and add to `paths`.
* Return `paths`.
[basename]: #basename

View File

@ -5,12 +5,13 @@
* [Definitions](#definitions)
* [Source File](#source-file)
* [Vendor Prefix](#vendor-prefix)
* [Grammar](#grammar)
* [Syntax](#syntax-1)
* [`InterpolatedIdentifier`](#interpolatedidentifier)
* [`InterpolatedUrl`](#interpolatedurl)
* [`Name`](#name)
* [`SpecialFunctionExpression`](#specialfunctionexpression)
* [`PseudoSelector`](#pseudoselector)
* [`ProductExpression`](#productexpression)
* [Procedures](#procedures)
* [Parsing Text](#parsing-text)
* [Parsing Text as CSS](#parsing-text-as-css)
@ -38,7 +39,23 @@ points followed by another U+002D. An identifier only has a vendor prefix if the
final U+002D is followed by additional text. This additional text is referred to
as the *unprefixed identifier*.
## Grammar
## Syntax
Unless otherwise specified, if a grammar production's value is parsed as a
single other named Sass production, then the AST does not contain a
representation of the intermediate production.
> For example, suppose we have the productions:
>
> <x><pre>
> **Foo** ::= Bar '*'?
> **Bar** ::= \<ident-token>
> </pre></x>
>
> Parsing `"ident*"` creates an AST that contains a `Foo` that contains a `Bar`.
> But parsing `"ident"` alone creates an AST that only contains a `Bar`—the
> `Foo` wrapper is removed. (The `Bar` wrapper is *not* removed because
> `<ident-token>` is a CSS production, not a Sass production.)
### `InterpolatedIdentifier`
@ -121,6 +138,12 @@ followed by a parenthesis, it must be parsed as a `SelectorPseudo` or an
No whitespace is allowed anywhere in a `PseudoSelector` except within
parentheses.
### `ProductExpression`
<x><pre>
**ProductExpression** ::= (ProductExpression ('*' | '%'))? UnaryPlusExpression
</pre></x>
## Procedures
### Parsing Text
@ -219,7 +242,6 @@ modifications. The following productions should produce errors:
* All SassScript operations *except for*:
* `/`
* `not`
* `or`
* `and`
@ -235,7 +257,7 @@ modifications. The following productions should produce errors:
* Uses or declarations of Sass variables.
* `//`-style ("silent") comments.
* `//`-style ("silent") comments outside of an expression context.
In addition, some productions should be parsed differently than they would be in
SCSS:
@ -249,10 +271,9 @@ SCSS:
* The tokens `not`, `or`, `and`, and `null` should be parsed as unquoted
strings.
> The `/` operation should be parsed as normal. Because variables,
> parentheses, functions that return numbers, and all other arithmetic
> expressions are disallowed, it will always compile to slash-separated values
> rather than performing division.
* In an expression context, `//` is not parsed as a silent comment. Instead, two
adjacent `/`s in a [`SlashListExpression`] may have no whitespace between
them, so `//` is parsed as two slash separators in a slash-separated list.
### Consuming an Identifier

View File

@ -13,12 +13,14 @@
* [`Number`](#number)
* [Procedures](#procedures)
* [Evaluating a `FunctionCall` as a Calculation](#evaluating-a-functioncall-as-a-calculation)
* [Adjusting Slash Precedence](#adjusting-slash-precedence)
* [Evaluating an Expression as a Calculation Value](#evaluating-an-expression-as-a-calculation-value)
* [Simplifying a Calculation](#simplifying-a-calculation)
* [Simplifying a `CalculationValue`](#simplifying-a-calculationvalue)
* [Semantics](#semantics)
* [`FunctionExpression` and `Variable`](#functionexpression-and-variable)
* [`SumExpression` and `ProductExpression`](#sumexpression-and-productexpression)
* [`SlashListExpression`](#slashlistexpression)
* [`SpaceListExpression`](#spacelistexpression)
* [`ParenthesizedExpression`](#parenthesizedexpression)
* [`InterpolatedIdentifier`](#interpolatedidentifier)
@ -32,13 +34,13 @@ An expression is "calculation-safe" if it is one of:
* A [`FunctionExpression`].
* A `ParenthesizedExpression` whose contents is calculation-safe.
* A `SumExpression` whose operands are calculation-safe.
* A `ProductExpression` whose operator is `*` or `/` and whose operands are
* A `ProductExpression` whose operator is `*` and whose operands are
calculation-safe.
* A `Number`.
* A `Variable`.
* An `InterpolatedIdentifier`.
* An unbracketed `SpaceListExpression` with more than one element, whose
elements are all calculation-safe.
* An unbracketed `SpaceListExpression` or `SlashListExpression` with more than
one element, whose elements are all calculation-safe.
[`FunctionExpression`]: ../functions.md#syntax
@ -152,13 +154,71 @@ and returns a number or a calculation.
one or more `RestArgument`s, throw an error.
* Let `calc` be a calculation whose name is the lower-case value of `call`'s
name and whose arguments are the result of evaluating each `Expression` in
`call`'s `ArgumentInvocation` [as a calculation value].
name and whose arguments are the result of "[adjusting slash precedence] in
and then evaluating each `Expression`" in `call`'s `ArgumentInvocation` [as a
calculation value].
[adjusting slash precedence]: #adjusting-slash-precedence
[as a calculation value]: #evaluating-an-expression-as-a-calculation-value
* Return the result of [simplifying](#simplifying-a-calculation) `calc`.
### Adjusting Slash Precedence
This algorithm takes a calculation-safe expression `expression` and returns
another calculation-safe expression with the precedence of
[`SlashListExpression`]s adjusted to match division precedence.
[`SlashListExpression`]: list.md#slashlistexpression
* Return a copy of `expression` except, for each `SlashListExpression`:
* Let `left` be the first element of the list.
* For each remaining element `right`:
* If `left` and `right` are both `SumExpression`s:
* Let `last-left` be the last operand of `left` and `first-right` the
first operand of `right`.
* Set `left` to a `SumExpression` that begins with all operands and
operators of `left` except `last-left`, followed by a
`SlashListExpression` with elements `last-left` and `first-right`,
followed by all operators and operands of `right` except `first-right`.
> For example, `slash-list(1 + 2, 3 + 4)` becomes `1 + (2 / 3) + 4`.
* Otherwise, if `left` is a `SumExpression`:
* Let `last-left` be the last operand of `left`.
* Set `left` to a `SumExpression` that begins with all operands and
operators of `left` except `last-left`, followed by a
`SlashListExpression` with elements `last-left` and `right`.
> For example, `slash-list(1 + 2, 3)` becomes `1 + (2 / 3)`.
* Otherwise, if `right` is a `SumExpression` or a `ProductExpression`:
* Let `first-right` be the first operand of `right`.
* Set `left` to an expression of the same type as `right` that begins a
`SlashListExpression` with elements `left` and `first-right`, followed
by operators and operands of `right` except `first-right`.
> For example, `slash-list(1, 2 * 3)` becomes `(1 / 2) * 3`.
* Otherwise, if `left` is a slash-separated list, add `right` to the end.
* Otherwise, set `left` to a slash-separated list containing `left` and
`right`.
* Replace each element in `left` with the result of adjusting slash precedence
in that element.
* Replace the `SlashListExpression` with `left` in the returned expression.
### Evaluating an Expression as a Calculation Value
This algorithm takes an expression `expression` and returns a
@ -497,6 +557,21 @@ To evaluate a `SumExpresssion` or a `ProductExpression` as a calculation value:
* Return `left`.
### `SlashListExpression`
To evaluate a `SlashListExpression` as a calculation value:
* Let `left` be the result of evaluating the first element of the list as a
calculation value.
* For each remaining element `element`:
* Let `right` be the result of evaluating `element` as a calculation value.
* Set `left` to a `CalcOperation` with operator `"/"`, `left`, and `right`.
* Return `left`.
### `SpaceListExpression`
To evaluate a `SpaceListExpresssion` as a calculation value:

View File

@ -6,6 +6,9 @@
* [List](#list)
* [List Value](#list-value)
* [Index](#index)
* [Syntax](#syntax)
* [Semantics](#semantics)
* [`SlashListExpression`](#slashlistexpression)
## Definitions
@ -44,3 +47,36 @@ absolute value is larger than the length of that list.
> * `"c"`: 3, -1
[integer]: number.md#integer
## Syntax
<x><pre>
**BracketedListExpression** ::= '[' ContainedListExpression ']'
**ContainedListExpression** ::= CommaListExpression ','?
**CommaListExpression** ::= SlashListExpression (',' SlashListExpression)*
**SlashListExpression** ::= SpaceListExpression (('/' SpaceListExpression?)* '/' SpaceListExpression)?
**SpaceListExpression** ::= SumExpression+
</pre></x>
Every pair of adjacent `/`s in a `SlashListExpression` must be separated by
whitespace or comments, unless the stylesheet is being parsed as CSS.
> Note that `/` may *not* be used in single-element lists the way `,` is. That
> is, `(foo,)` is valid, but `(foo/)` is not.
>
> This defines `/` to bind tighter than `,` but looser than space-separated
> lists. This was chosen because most common uses of `/` in CSS conceptually
> bind looser than space-separated values. The only exception is the [`font`
> shorthand syntax], which is used much more rarely will still work (albeit with
> an unintuitive SassScript representation) with a loose-binding `/`.
>
> [`font` shorthand syntax]: https://developer.mozilla.org/en-US/docs/Web/CSS/font
## Semantics
### `SlashListExpression`
To evaluate a `SlashListExpression`, evaluate each of its `SpaceListExpression`s
and return a slash-separated list that contains each of the results in order. If
any `/` isn't followed by a `SpaceListExpression`, use an empty unquoted string
as its value instead.

View File

@ -14,7 +14,6 @@
* [Exact Equality](#exact-equality)
* [Fuzzy Equality](#fuzzy-equality)
* [Integer](#integer)
* [Potentially Slash-Separated Number](#potentially-slash-separated-number)
* [Types](#types)
* [Operations](#operations)
* [Equality](#equality)
@ -210,37 +209,6 @@ If `m` exists, we say that `n`'s *integer value* is the double that represents
> To avoid ambiguity, specification text will generally use the term
> "mathematical integer" when referring to the abstract mathematical objects.
### Potentially Slash-Separated Number
A Sass number may be *potentially slash-separated*. If it is, it is associated
with two additional Sass numbers, the *original numerator* and the *original
denominator*. A number that is not potentially slash-separated is known as
*slash-free*.
A potentially slash-separated number is created when a `ProductExpression` with
a `/` operator is evaluated and each operand is *syntactically* one of the
following:
* a `Number`,
* a [`FunctionCall`], or
* a `ProductExpression` that can itself create potentially slash-separated
numbers.
[`FunctionCall`]: ../functions.md#functioncall
If the result of evaluating the `ProductExpression` is a number, that number is
potentially slash-separated if all of the following are true:
* the results of evaluating both operands were numbers, and
* if either operand was a `FunctionCall`, it was [evaluated as a calculation]
and its name was not `"abs"`, `"max"`, `"min"`, or `"round"`.
[evaluated as a calculation]: calculation.md#evaluating-a-functioncall-as-a-calculation
If both of these are true, the first operand is the original numerator of the
potentially slash-separated number returned by the `/` operator, and the second
is the original denominator.
## Types
The value type known as a *number* has three components:

View File

@ -106,6 +106,8 @@ function runLinkCheck(
{pattern: /^https?:\/\/blogs\.msdn\.microsoft\.com(\/|$)/},
// Link consistently fails within CI
{pattern: /^https:\/\/runtime-keys\.proposal\.wintercg\.org(\/|$)/},
// Stackoverflow links can get rate-limited on GitHub Actions
{pattern: /^https:\/\/stackoverflow\.com(\/|$)/},
],
},
(error, results) => {