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

This commit is contained in:
Natalie Weizenbaum 2023-10-05 18:07:16 -07:00
commit c52b517334
80 changed files with 4198 additions and 1496 deletions

View File

@ -12,18 +12,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with: {node-version: '${{ env.NODE_VERSION }}'}
- run: npm ci
- run: npm run tangle && npx gts lint && npx tsc --noEmit
- run: >
npm run tangle &&
npx gts lint &&
npx tsc --noEmit &&
npx markdownlint-cli2
toc:
name: Tables of contents
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with: {node-version: '${{ env.NODE_VERSION }}'}
- run: npm ci
@ -34,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with: {node-version: '${{ env.NODE_VERSION }}'}
- run: npm ci
@ -45,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with: {node-version: '${{ env.NODE_VERSION }}'}
- run: npm ci
@ -56,7 +60,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with: {node-version: '${{ env.NODE_VERSION }}'}
- run: npm ci
@ -67,18 +71,18 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- uses: arduino/setup-protoc@v1
with: { version: "3.x", repo-token: "${{ github.token }}" }
- uses: actions/checkout@v4
- uses: bufbuild/buf-setup-action@v1.26.1
with: {github_token: "${{ github.token }}"}
- name: Generate protobuf code
run: protoc --js_out=/tmp spec/embedded_sass.proto
run: buf generate
embedded_protocol_versions:
name: "Validate Embedded Protocol versions"
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Check versions match
run: |
if ! (grep -q '\-dev$' spec/EMBEDDED_PROTOCOL_VERSION ||
@ -96,7 +100,7 @@ jobs:
github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 2
@ -129,12 +133,12 @@ jobs:
github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with: {fetch-depth: 0}
- name: Find changed files in js-api-doc
id: changed-files
uses: tj-actions/changed-files@a96679dfee2a1e64b1db5a210c0ffaf1f2cb24ce
uses: tj-actions/changed-files@8238a4103220c636f2dad328ead8a7c8dbe316a3
with: {files: js-api-doc}
- name: Deploy

1
.gitignore vendored
View File

@ -96,3 +96,4 @@ typings/
# End of https://www.gitignore.io/api/node
/docs
/gen

27
.markdownlint-cli2.yaml Normal file
View File

@ -0,0 +1,27 @@
globs: ["**.md"]
ignores: ["node_modules/**"]
config:
# BNF syntax examples can go over length, and there's no way to exempt them
# specifically.
line-length: false
# We use inline HTML to format BNF syntax definitions.
no-inline-html: false
# We use untagged code blocks for function definitions.
fenced-code-language: false
emphasis-style:
style: asterisk
# We often have to have duplicate subheadings for different parents.
no-duplicate-heading: false
# We use multiple sequential blockquotes to indicate unrelated non-normative
# notes.
no-blanks-blockquote: false
# We use spaces in code formatting because spaces are relevant in Sass.
no-space-in-code: false
first-line-heading: false

View File

@ -16,6 +16,7 @@ process][], very small features can follow the [fast-track process][] instead.
* [Embedded Protocol](#embedded-protocol)
* [Fast Track](#fast-track)
* [Emergency Track](#emergency-track)
* [Large Language Models](#large-language-models)
## Process
@ -172,7 +173,7 @@ accepted.
<x><pre>
**MinMaxExpression** ::= CssMinMax | FunctionExpression
**CssMinMax** ::= ('min(' | 'max(') CalcValue (',' CalcValue)* ')'
**CssMinMax** ::= ('min(' | 'max(') CalcValue (',' CalcValue)\* ')'
**CalcValue** ::= CalcValue (('+' | '-' | '*' | '/') CalcValue)+
&#32; | '(' CalcValue ')'
&#32; | ('calc(' | 'env(' | 'var(') InterpolatedDeclarationValue ')'
@ -214,7 +215,7 @@ accepted.
See [CSS Imports][css-imports deprecation] for a good example of a Deprecation
Process section.
[css-imports deprecation]: https://github.com/sass/dart-sass#compatibility-policy
[css-imports deprecation]: accepted/css-imports.md#deprecation-process
### JavaScript API Proposals
@ -371,3 +372,14 @@ as follows:
2. These pull requests may be merged as soon as they're approved. If the issue
appears outside of work hours, it may be merged without review, but a *post
facto* review should be done as soon as possible.
## Large Language Models
Do not submit any code or prose written or modified by large language models or
"artificial intelligence" such as GitHub Copilot or ChatGPT to this project.
These tools produce code that looks plausible, which means that not only is it
likely to contain bugs those bugs are likely to be difficult to notice on
review. In addition, because these models were trained indiscriminately and
non-consensually on open-source code with a variety of licenses, it's not
obvious that we have the moral or legal right to redistribute code they
generate.

View File

@ -4,7 +4,7 @@
* Clarify the definition of bogus selectors.
* Only omit style rules if _all_ of their complex selectors are bogus.
* Only omit style rules if *all* of their complex selectors are bogus.
* Expand the set of selectors that are treated by the extend algorithm as
matching nothing to include bogus pseudo selectors, since these can never be

View File

@ -119,7 +119,7 @@ combinator].
combinators*) as well as a sequence of [complex selector components]. Either,
but not both, of these sequences may be empty~~
[visible combinators]: #visible-combinator
[visible combinators]: #visible-combinator
[complex selector components]: #complex-selector-component
A *complex selector* is an optional [visible combinator] (its *leading
@ -127,7 +127,7 @@ combinator*) as well as a sequence of [complex selector components]. The
component sequence may be empty only for complex selectors with leading
combinators.
[visible combinator]: #visible-combinator
[visible combinator]: #visible-combinator
### Complex Selector Component
@ -169,9 +169,9 @@ This proposal modifies the existing `ComplexSelector` and
`ComplexSelectorComponent` productions to drop support for multiple combinators:
<x><pre>
~~**ComplexSelector** ::= [\<combinator>]* ComplexSelectorComponent+~~
~~**ComplexSelector** ::= [\<combinator>]\* ComplexSelectorComponent+~~
~~&#32; | [\<combinator>]+~~
~~**ComplexSelectorComponent** ::= CompoundSelector [\<combinator>]*~~
~~**ComplexSelectorComponent** ::= CompoundSelector [\<combinator>]\*~~
**ComplexSelector** ::= [\<combinator>]? ComplexSelectorComponent+
&#32; | [\<combinator>]
**ComplexSelectorComponent** ::= CompoundSelector [\<combinator>]?
@ -228,7 +228,7 @@ In particular:
* The parsing of `ComplexSelector` and `ComplexSelectorComponent` is unchanged.
* A complex selector is instead considered [bogus] if it would be bogus in Phase
2 _or_ if it can be parsed in Phase 1 but not in Phase 2.
2 *or* if it can be parsed in Phase 1 but not in Phase 2.
* The newly-added errors produce deprecation warnings instead.

View File

@ -3,7 +3,7 @@
* Throw an error when serializing a degenerate number with complex units in a
calculation.
* Clarify that we're checking for degenerate _values_ in "Converting a Number to
* Clarify that we're checking for degenerate *values* in "Converting a Number to
a Calculation".
* Use `UnquotedString`s to represent unknown calculation constants rather than

View File

@ -68,16 +68,16 @@ silence real errors without providing any actual value.
[Converting a Number to a Calculation]: #converting-a-number-to-a-calculation
Whether and how we want to support this once browsers _can_ parse it is a
Whether and how we want to support this once browsers *can* parse it is a
question for another time.
## Definitions
### Degenerate Number
The doubles `Infinity`, `-Infinity`, and `NaN` are _degenerate_.
The doubles `Infinity`, `-Infinity`, and `NaN` are *degenerate*.
A number is _degenerate_ if its value is degenerate.
A number is *degenerate* if its value is degenerate.
## Syntax

View File

@ -1,3 +1,63 @@
## Draft 3.2
* Support space-separated lists in calculation expressions without deprecation,
since they're necessary to support the valid CSS construct `calc(1
var(--plus-two))`.
* Always preserve parentheses around unquoted strings.
## Draft 3.1
* Update the definition of potentially slash-separated numbers to reflect the
fact that calculations are no longer determinable syntactically.
* Add a section describing how to restructure slash-separated lists as division
within calculations.
* Don't evaluate `min()`, `max()`, `round()`, or `abs()` as a calculation if it
has keyword or rest arguments.
* During the deprecation period, only consider unbracketed
`SpaceListExpressions` with multiple elements that actually contain
interpolation to be calculation-safe.
* Handle `"*"` and `"/"` tokens when evaluating `SumExpresssion`s and
`ProductExpression`s as calculations.
* Remove the `CalculationInterpolation` type, and deprecate the associated JS
API and embedded protocol field. The problem this type existed to work around
is no longer an issue in the new parsing structure.
* Preserve parentheses around all `var()` strings that get injected from
anywhere, as well as any unquoted strings that appear in `calc()` and might
need parentheses.
* Don't treat `abs()`, `min()`, `max()`, or `round()` as potentially
slash-separated operands..
## Draft 3.0
* Refactor the way calculations are parsed to allow them to coexist with
user-defined Sass functions of the same names.
* No longer forbid user-defined functions with the same names as CSS math
functions and remove the associated deprecation process.
* Drop support for interpolation in calculations outside of identifier position
and add a deprecation process for this.
## Draft 2.1
* Allow custom functions named like vendor-prefixed new CSS functions.
## Draft 2.0
* Explicitly forbid user-defined functions with the same names as CSS math
functions.
* Add a deprecation process for gradually phasing out user-defined functions
with name conflicts.
## Draft 1.5
* Fix the definition of `rem()` to use `result - modulus` rather than `result -
@ -6,7 +66,7 @@
## Draft 1.4
* Don't exempt percentages from most functions' simplification logic, since
those functions don't allow _any_ units in CSS, including percents. Now only
those functions don't allow *any* units in CSS, including percents. Now only
`abs()`, `sign()`, `atan2()`, and `hypot()` check for known units because CSS
allows percentages for them but they aren't linear so they can't be resolved
in terms of percentages in Sass.

View File

@ -1,29 +1,55 @@
# Calculation Functions: Draft 1.5
# Calculation Functions: Draft 3.2
*([Issue](https://github.com/sass/sass/issues/3504))*
*([Issue](https://github.com/sass/sass/issues/3504), [Changelog](calc-functions.changes.md))*
## Table of Contents
* [Background](#background)
* [Summary](#summary)
* [Design Decisions](#design-decisions)
* [Merged Syntax](#merged-syntax)
* [Changing Mod Infinity Behavior](#changing-mod-infinity-behavior)
* [Definitions](#definitions)
* [Calculation-Safe Expression](#calculation-safe-expression)
* [Exact Equality](#exact-equality)
* [Known Units](#known-units)
* [Potentially Slash-Separated Number](#potentially-slash-separated-number)
* [Syntax](#syntax)
* [`FunctionExpression`](#functionexpression)
* [`CssMinMax`](#cssminmax)
* [`CalculationExpression`](#calculationexpression)
* [`CssRound`](#cssround)
* [`CssAbs`](#cssabs)
* [Types](#types)
* [Calculation](#calculation)
* [Operations](#operations)
* [Modulo](#modulo)
* [Procedures](#procedures)
* [Evaluating a `FunctionCall` as a Calculation](#evaluating-a-functioncall-as-a-calculation)
* [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)
* [Calculation Expressions](#calculation-expressions)
* [`FunctionCall`](#functioncall)
* [Calculations](#calculations)
* [`FunctionExpression` and `Variable`](#functionexpression-and-variable)
* [`SumExpression` and `ProductExpression`](#sumexpression-and-productexpression)
* [`SpaceListExpression`](#spacelistexpression)
* [`ParenthesizedExpression`](#parenthesizedexpression)
* [`InterpolatedIdentifier`](#interpolatedidentifier)
* [Interaction with Forward Slash as a Separator](#interaction-with-forward-slash-as-a-separator)
* [Adjusting Slash Precedence](#adjusting-slash-precedence)
* [`SlashListExpression`](#slashlistexpression)
* [API](#api)
* [Types](#types-1)
* [`CalculationInterpolation`](#calculationinterpolation)
* [`internal`](#internal)
* [Constructor](#constructor)
* [`value`](#value)
* [`equals`](#equals)
* [`hashCode`](#hashcode)
* [Embedded Protocol](#embedded-protocol)
* [`CalculationValue.value.interpolation`](#calculationvaluevalueinterpolation)
* [Deprecation Process](#deprecation-process)
* [`abs-percent`](#abs-percent)
## Background
@ -57,12 +83,37 @@ equivalent of `-10%` since percentages are resolved before calculations. To
handle this, we'll deprecate the global `abs()` function with a percentage and
recommend users explicitly write `math.abs()` or `abs(#{})` instead.
This also expands calculation parsing to allow constructs like `calc(1
var(--plus-two))` (where for example `--plus-two: + 2`) which are valid CSS but
weren't supported by the old first-class calculation parsing.
### Design Decisions
#### Merged Syntax
This proposal substantially changes the way calculations are parsed, merging the
syntax with the standard Sass expression syntax. Now the only difference between
a calculation and a normal Sass function is how it's *evaluated*. This has the
notable benefit of allowing calculations to coexist with user-defined Sass
functions of the same name, preserving backwards-compatibility.
Because this overlap is always going to be somewhat confusing for readers, we
considered simply disallowing Sass functions whose names matched CSS
calculations after a suitable deprecation period. However, in addition to the
intrinsic value of avoiding breaking changes, the function name `rem()` in
particular is widely used in Sass libraries as a means of converting pixel
widths to relative ems, so this is a fairly substantial breaking change in
practice.
This does also require its own breaking change to the way interpolation is
handled in calculations—`calc(#{"1px +"} 1%)` was formerly valid but is no
longer. However, this is likely to break many fewer users in practice, and is
relatively easy to continue supporting in a deprecated state in the short term.
#### Changing Mod Infinity Behavior
This proposal changes the behavior of the `%` operation when the right-hand side
is infinite _and_ has a different sign than the left-hand side. Sass used to
is infinite *and* has a different sign than the left-hand side. Sass used to
return the right-hand side in accordance with the floating point specification,
but it now returns NaN to match CSS's `mod()` function.
@ -72,6 +123,26 @@ process for it.
## Definitions
### Calculation-Safe Expression
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
calculation-safe.
* A `Number`.
* A `Variable`.
* An `InterpolatedIdentifier`.
* An unbracketed `SpaceListExpression` with more than one element, whose
elements are all calculation-safe.
[`FunctionExpression`]: ../spec/functions.md#syntax
> Because calculations have special syntax in CSS, only a subset of SassScript
> expressions are valid (and these are interpreted differently than elsewhere).
### Exact Equality
Two [doubles] are said to be *exactly equal* if they are equal according to the
@ -113,114 +184,55 @@ a `/` operator is evaluated and each operand is *syntactically* one of the
following:
* a `Number`,
* a [`Calculation`] **whose name is not `abs`, `max`, `min`, or `round`**, or
* a [`FunctionCall`], or
* a `ProductExpression` that can itself create potentially slash-separated
numbers.
[`FunctionCall`]: ../spec/functions.md#functioncall
> We exclude these four calc functions from producing potentially
> slash-separated numbers to ensure that existing code like `1 / round(1.5)`
> continues to be evaluated as division when the function goes from being
> evaluated as a Sass global function to being evaluated as a calc function.
If the result of evaluating the `ProductExpression` is a number, that number is
potentially slash-separated if all of the following are true:
[`Calculation`]: ../spec/types/calculation.md#syntax
* 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"`.
If both operands are evaluated as numbers, the resulting number is potentially
slash-separated. The first operand is the original numerator of the potentially
slash-separated number returned by the `/` operator, and the second is the
original denominator.
[evaluated as a calculation]: #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.
## Syntax
> Calculations are no longer parsed differently than other Sass productions.
> Instead, they're *evaluated* differently at runtime. This allows them to
> coexist with user-defined Sass functions even when their names overlap.
### `FunctionExpression`
Replace [the definition of `FunctionExpression`] with the following:
Remove `CssMinMax` and `CalculationExpression` from [the definition of
`FunctionExpression`].
[the definition of `FunctionExpression`]: ../spec/functions.md#syntax
<x><pre>
**FunctionExpression**¹ ::= [CssMinMax]
&#32; | [CssRound]
&#32; | [CssAbs]
&#32; | [SpecialFunctionExpression]
&#32; | [CalculationExpression]
&#32; | EmptyFallbackVar
&#32; | FunctionCall
**EmptyFallbackVar** ::= 'var('² Expression ',' ')'
**FunctionCall**³ ::= [NamespacedIdentifier] ArgumentInvocation
</pre></x>
### `CssMinMax`
[CssMinMax]: ../spec/types/calculation.md#cssminmax
[CssRound]: #cssround
[CssAbs]: #cssabs
[SpecialFunctionExpression]: ../spec/syntax.md#specialfunctionexpression
[CalculationExpression]: ../spec/types/calculation.md#calculationexpression
[NamespacedIdentifier]: ../spec/modules.md#syntax
1: `CssMinMax`, `CssRound`, `CssAbs`, and `EmptyFallbackVar` all take precedence
over `FunctionCall` if either could be consumed.
2: `'var('` is matched case-insensitively.
3: `FunctionCall` may not have any whitespace between the `NamespacedIdentifier`
and the `ArgumentInvocation`. It may not start with [`SpecialFunctionName`],
[`UnaryCalcName`], [`BinaryCalcName`], `'hypot('`, or `'clamp('`
(case-insensitively).
[`SpecialFunctionName`]: ../spec/syntax.md#specialfunctionexpression
[`UnaryCalcName`]: #calculationexpression
[`BinaryCalcName`]: #calculationexpression
Remove the `CssMinMax` production.
### `CalculationExpression`
Replace [the definition of `CalculationExpression`] with:
Remove the `CalculationExpression` production.
[the definition of `CalculationExpression`]: ../spec/types/calculation.md#calculationexpression
## Types
<x><pre>
**CalculationExpression** ::= UnaryCalcExpression
&#32; | BinaryCalcExpression
&#32; | ClampExpression
&#32; | HypotExpression
**UnaryCalcExpression** ::= UnaryCalcName CalcArgument ')'
**BinaryCalcExpression** ::= BinaryCalcName CalcArgument (',' CalcArgument)? ')'
**HypotExpression** ::= 'hypot('¹ CalcArgument (',' CalcArgument)\* ')'
**UnaryCalcName**¹ ::= 'calc(' | 'sin(' | 'cos(' | 'tan(' | 'asin('
&#32; | 'acos(' | 'atan(' | 'sqrt(' | 'exp(' | 'sign('
**BinaryCalcName**¹ ::= 'mod(' | 'rem(' | 'atan2(' | 'pow(' | 'log('
</pre></x>
### Calculation
1: The strings `hypot(`, `clamp(`, and `var(` are matched case-insensitively, as
are the productions `UnaryCalcName` and `BinaryCalcName`.
Delete the `CalculationInterpolation` type and remove all references to it.
Remove the existing definition of `CalcExpression`.
> Note: we aren't adding `| CssRound | CssAbs` to the definition of `CalcValue`
> because existing Sass releases already allow the global `round()` and `abs()`
> functions in calculations.
### `CssRound`
Add the following production:
<x><pre>
**CssRound** ::= 'round('¹ CalcArgument (',' CalcArgument){2} ')'
</pre></x>
1: The string `round(` is matched case-insensitively.
> Although the three-argument `round()` function only allows a few values in its
> first argument, for simplicity those are checked at evaluation time rather
> than parse time.
### `CssAbs`
Add the following production:
<x><pre>
**CssAbs** ::= 'abs('¹ CalcArgument ')'
</pre></x>
1: The string `abs(` is matched case-insensitively.
> This type only existed to track where we needed to defensively insert
> parentheses. Now that we track parentheses as part of the calculation AST,
> this is no longer necessary
## Operations
@ -265,6 +277,35 @@ Let `n1` and `n2` be two numbers. To determine `n1 % n2`:
## Procedures
### Evaluating a `FunctionCall` as a Calculation
This algorithm takes a [`FunctionCall`] `call` whose name is a plain identifier
and returns a number or a calculation.
* If `call`'s `ArgumentInvocation` contains one or more `KeywordArgument`s or
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].
[as a calculation value]: #evaluating-an-expression-as-a-calculation-value
* Return the result of [simplifying] `calc`.
### Evaluating an Expression as a Calculation Value
This algorithm takes an expression `expression` and returns a
`CalculationValue`.
* If `expression` isn't [calculation-safe], throw an error.
* Otherwise, evaluate `expression` using the semantics defined in the
[Calculations] specification if there are any, or the standard semantics
otherwise.
[Calculations]: #calculations
### Simplifying a Calculation
Replace [the definition of "Simplifying a Calculation"] with the following:
@ -287,8 +328,8 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
or calculation, return it.
* If `calc`'s name is `"mod"`, `"rem"`, `"atan2"`, or `"pow"`; `arguments` has
fewer than two elements; and none of those are unquoted strings or
`CalculationInterpolation`s, throw an error.
fewer than two elements; and none of those are unquoted strings, throw an
error.
> It's valid to write `pow(var(--two-args))` or `pow(#{"2, 3"})`, but
> otherwise calculations' arguments must match the expected number.
@ -323,7 +364,7 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
> In this case, `number` is either `+0`, `-0`, or NaN.
> To match CSS's behavior, these computations _don't_ use fuzzy comparisons.
> To match CSS's behavior, these computations *don't* use fuzzy comparisons.
* If `calc`'s name is `"log"`:
@ -356,8 +397,8 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
* If `calc`'s name is `"mod"` or `"rem"`:
* If `arguments` has only one element and it's not an unquoted string or a
`CalculationInterpolation`, throw an error.
* If `arguments` has only one element and it's not an unquoted string, throw
an error.
* Otherwise, if `arguments` contains exactly two numbers `dividend` and
`modulus`:
@ -377,6 +418,7 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
* Otherwise, return `result`.
[compatible]: ../spec/types/number.md#compatible-units
[definitely-incompatible]: ../spec/types/number.md#possibly-compatible-numbers
[exactly equals]: #exact-equality
@ -389,7 +431,7 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
* If the first element is an unquoted string or interpolation with value
`"nearest"`, `"up"`, `"down"`, or `"to-zero"`, and the second argument
isn't an unquoted string or `CalculationInterpolation`, throw an error.
isn't an unquoted string, throw an error.
> Normally we allow unquoted strings anywhere in a calculation, but this
> helps catch the likely error of a user accidentally writing `round(up,
@ -398,8 +440,7 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
* Otherwise, set `number` and `step` to the two arguments respectively and
`strategy` to an unquoted string with value `"nearest"`.
* Otherwise, if the single argument isn't an unquoted string or
`CalculationInterpolation`, throw an error.
* Otherwise, if the single argument isn't an unquoted string, throw an error.
* If `strategy`, `number`, and `step` are set:
@ -459,7 +500,7 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
* If `calc`'s name is `"clamp"`:
* If `arguments` has fewer than three elements, and none of those are unquoted
strings or `CalculationInterpolation`s, throw an error.
strings, throw an error.
* Otherwise, if any two elements of `arguments` are [definitely-incompatible]
numbers, throw an error.
@ -498,31 +539,327 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
* Otherwise, return a calculation with the same name as `calc` and `arguments`
as its arguments.
### Simplifying a `CalculationValue`
Replace the block "If `value` is a calculation" in the procedure for
[simplifying a `CalculationValue`] with the following:
[simplifying a `CalculationValue`]: ../spec/types/calculation.md#simplifying-a-calculationvalue
* If `value` is a calculation:
* Let `result` be the result of [simplifying] `value`.
* If `result` isn't a calculation whose name is `"calc"`, return `result`.
* If `result`'s argument isn't an unquoted string, return `result`.
* If `result`'s argument begins case-insensitively with `"var("`; or if it
contains whitespace, `"/"`, or `"*"`; return `"(" +` result's argument `+
")"` as an unquoted string.
> This is ensures that values that could resolve to operations end up
> parenthesized if used in other operations. It's potentially a little
> overzealous, but that's unlikely to be a major problem given that the
> output is still smaller than including the full `calc()` and we don't want
> to encourage users to inject calculations with interpolation anyway.
## Semantics
### Calculation Expressions
### `FunctionCall`
Replace the semantics for `CalcExpression`, `ClampExpression`, and `CssMinMax`
with the following:
Add the following to [the semantics for `FunctionCall`] before checking for a
global function:
To evaluate a `UnaryCalcExpression`, `BinaryCalcExpression`, `HypotExpression`,
`CssMinMax`, `CssRound`, or `CssAbs`:
[the semantics for `FunctionCall`]: ../spec/functions.md#functioncall
* Let `name` be the lower-case value of the expression's first token without the
trailing parenthesis.
* If `function` is null; `name` is case-insensitively equal to `"min"`, `"max"`,
`"round"`, or `"abs"`; `call`'s `ArgumentInvocation` doesn't have any
`KeywordArgument`s or `RestArgument`s; and all arguments in `call`'s
`ArgumentInvocation` are [calculation-safe], return the result of evaluating
`call` [as a calculation].
* Let `calc` be a calculation whose name is `name` and whose arguments are the
results of [evaluating the expression's `CalcArgument`s].
[calculation-safe]: #calculation-safe-expression
[as a calculation]: #evaluating-a-functioncall-as-a-calculation
[evaluating the expression's `CalcArgument`s]: ../spec/types/calculation.md#calcargument
> For calculation functions that overlap with global Sass function names, we
> want anything Sass-specific like this to end up calling the Sass function.
> For all other calculation functions, we want those constructs to throw an
> error (which they do when evaluating `call` [as a calculation]).
* Return the result of [simplifying] `calc`.
* If `function` is null and `name` is case-insensitively equal to `"calc"`,
`"clamp"`, `"hypot"`, `"sin"`, `"cos"`, `"tan"`, `"asin"`, `"acos"`, `"atan"`,
`"sqrt"`, `"exp"`, `"sign"`, `"mod"`, `"rem"`, `"atan2"`, `"pow"`, or `"log"`,
return the result of evaluating `call` [as a calculation].
### Calculations
Remove all prior [semantics for Calculations]. The following semantics apply
only when evaluating expressions [as calculation values].
[semantics for Calculations]: ../spec/types/calculation.md#semantics
[as calculation values]: #evaluating-an-expression-as-a-calculation-value
#### `FunctionExpression` and `Variable`
To evaluate a `FunctionExpression` or a `Variable` as a calculation value,
evaluate it using the standard semantics. If the result is a number, an unquoted
string, or a calculation, return it. Otherwise, throw an error.
> Allowing variables to return unquoted strings here supports referential
> transparency, so that `$var: fn(); calc($var)` works the same as `calc(fn())`.
#### `SumExpression` and `ProductExpression`
To evaluate a `SumExpresssion` or a `ProductExpression` as a calculation value:
* Let `left` be the result of evaluating the first operand as a calculation
value.
* For each remaining `"+"`, `"-"`, `"*"`, or `"/"` token `operator` and operand
`operand`:
* Let `right` be the result of evaluating `operand` 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:
* Let `elements` be the results of evaluating each element as a calculation
value.
* If `elements` has two adjacent elements that aren't unquoted strings, throw an
error.
> This ensures that valid CSS constructs like `calc(1 var(--plus-two))` and
> similar Sass constructs like `calc(1 #{"+ 2"})` work while preventing clear
> errors like `calc(1 2)`.
>
> This does allow errors like `calc(a b)`, but the complexity of verifying
> that the unquoted strings could actually be a partial operation isn't worth
> the benefit of eagerly producing an error in this edge case.
* Let `serialized` be an empty list.
* For each `element` of `elements`:
* Let `css` be the result of [serializing] `element`.
[serializing]: ../spec/types/calculation.md#serialization
* If `element` is a `CalcOperation` that was produced by evaluating a
`ParenthesizedExpression`, set `css` to `"(" + css + ")"`.
* Append `css` to `serialized`.
* Return an unquoted strings whose contents are the elements of `serialized`
separated by `" "`.
#### `ParenthesizedExpression`
> If a `var()` or an interpolation is written directly within parentheses, it's
> necessary to preserve those parentheses. CSS resolves `var()` by literally
> replacing the function with the value of the variable and *then* parsing the
> surrounding context.
>
> For example, if `--ratio: 2/3`, `calc(1 / (var(--ratio)))` is parsed as
> `calc(1 / (2/3)) = calc(3/2)` but `calc(1 / var(--ratio))` is parsed as
> `calc(1 / 2/3) = calc(1/6)`.
To evaluate a `ParenthesizedExpression` with contents `expression` as a
calculation value:
* Let `result` be the result of evaluating `expression` as a calculation value.
* If `result` is an unquoted string, return `"(" + result + ")"` as an unquoted
string.
* Otherwise, return `result`.
#### `InterpolatedIdentifier`
To evaluate an `InterpolatedIdentifier` `ident` as a calculation value:
* If `ident` is case-insensitively equal to `pi`, return 3.141592653589793.
> This is the closest double approximation of the mathematical constant π.
* If `ident` is case-insensitively equal to `e`, return 2.718281828459045.
> This is the closest double approximation of the mathematical constant e.
* If `ident` is case-insensitively equal to `infinity`, return the double
`Infinity`.
* If `ident` is case-insensitively equal to `-infinity`, return the double
`-Infinity`.
* If `ident` is case-insensitively equal to `nan`, return the double `NaN`.
* Otherwise, return the result of evaluating `ident` using standard semantics.
> This will be an `UnquotedString`.
## Interaction with Forward Slash as a Separator
Although the [Forward Slash as a Separator proposal] has not yet been integrated
into the canonical spec, it will affect some of the constructs modified by this
proposal. This section defines additional modifications to the spec *as it will
exist* when that proposal is integrated.
[Forward Slash as a Separator proposal]: slash-separator.md
Remove "or `/`" from the definition of a calculation-safe `ProductExpression`.
Add "An unbracketed `SlashListExpression` with more than one element, all of
which are calculation-safe" to the list of calculation-safe expressions.
Replace "evaluating each `Expression`" with "[adjusting slash precedence] in and
then evaluating each `Expression`" in [evaluting a `FunctionCall` as a
calculation].
[adjusting slash precedence]: #adjusting-slash-precedence
[evaluting a `FunctionCall` as a calculation]: #evaluating-a-functioncall-as-a-calculation
### 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.
* 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.
### `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`.
## API
### Types
#### `CalculationInterpolation`
Replace the definition of this class, other than its TypeScript API, with the
following:
A deprecated alternative JS API representation of an unquoted Sass string that's
always surrounded by parentheses. It's never returned by the Sass compiler, but
for backwards-compatibility users may still construct it and pass it to the Sass
compiler.
> `CalculationInterpolation`s are no longer generated by the Sass compiler,
> because it can now tell at evaluation time whether an interpolation was
> originally surrounded by parentheses. However, until we make a breaking
> revision of the JS API, users may continue to pass `CalculationInterpolation`s
##### `internal`
A private property like [`Value.internal`] that refers to a Sass string.
##### Constructor
Creates a `CalculationInterpolation` with its `internal` set to an unquoted Sass
string with text `"(" + value + ")"` and returns it.
##### `value`
Returns [`internal`](#internal)'s `value` field's text, without the leading and
trailing parentheses.
##### `equals`
Whether `other` is a `CalculationInterpolation` and [`internal`](#internal) is
equal to `other.internal` in Sass.
##### `hashCode`
Returns the same number for any two `CalculationInterpolation`s that are equal
according to [`equals`](#equals).
## Embedded Protocol
### `CalculationValue.value.interpolation`
Add the following to this field's documentation:
The compiler must treat this as identical to a `string` option whose value is
`"(" + interpolation + ")"`.
This field is deprecated and hosts should avoid using it.
## Deprecation Process
Before this specification is applied in full force, it will be applied with the
following modification:
This proposal causes two breaking changes, each of which will be mitigated by
supporting something very close to the old behavior with a deprecation warning
until the next major version release.
* When simplifying a calculation named `"abs"` whose sole argument is a number
_without_ [known units], return the result of calling `math.abs()` with that
number and emit a deprecation warning named `abs-percent`.
### `abs-percent`
> Under this proposal, if a number with unit `%` is passed to the global `abs()`
> function, it will be emitted as a plain CSS `abs()` rather than returning the
> absolute value of the percentage itself.
During the deprecation period, when simplifying a calculation named `"abs"`
whose sole argument is a number *without* [known units], return the result of
calling `math.abs()` with that number and emit a deprecation warning named
`abs-percent`.

View File

@ -290,7 +290,7 @@ export type CalculationOperator = '+' | '-' | '*' | '/';
The JS API representation of a Sass [`CalculationOperation`].
[CalculationOperation]: ../spec/types/calculation.md#types
[`CalculationOperation`]: ../spec/types/calculation.md#types
```ts
export class CalculationOperation implements ValueObject {

View File

@ -106,9 +106,7 @@ The [`SpecialFunctionName`] production will be changed to the following:
### `CalcValue`
The [`CalcValue`] production will be changed to the following:
[`CalcValue`]: ../spec/types/calculation.md#calculationexpression
The `CalcValue` production will be changed to the following:
<x><pre>
**CalcValue** ::= CalcValue (('+' | '-' | '*' | '/') CalcValue)+

View File

@ -3,7 +3,7 @@
*([Issue](https://github.com/sass/sass/issues/2834))*
This proposal adds a new `hwb()` color format to the `sass:color` module, along
with inspection and adjustment options for _whiteness_ and _blackness_.
with inspection and adjustment options for *whiteness* and *blackness*.
## Table of Contents
@ -42,12 +42,12 @@ module to avoid conflicts with the CSS syntax, and will be converted to more
common color-name, hex, or `rgba()` syntax for output -- following the same
logic as our current color functions.
- New `color.hwb()` function describes colors in the sRGB colorspace using
* New `color.hwb()` function describes colors in the sRGB colorspace using
`$hue` (defined identically to the `hsl()` "hue" value), along with
`$whiteness`, `$blackness`, and optional `$alpha` transparency.
- New `color.whiteness()` and `color.blackness()` functions return the respective
* New `color.whiteness()` and `color.blackness()` functions return the respective
values of `w` or `b` for a given color.
- Existing `color.adjust()`, `color.scale()`, and `color.change()` functions will
* Existing `color.adjust()`, `color.scale()`, and `color.change()` functions will
accept additional `$whiteness` and `$blackness` parameters before the final
`$alpha` parameter.
@ -137,7 +137,6 @@ All new functions are part of the `sass:color` built-in module.
[percent-converting]: ../spec/built-in-modules/color.md#percent-converting-a-number
[to RGB]: https://www.w3.org/TR/css-color-4/#hwb-to-rgb
* ```
hwb($channels)
```

View File

@ -4,6 +4,7 @@
[Changelog](deprecations-api.changes.md))*
## Table of Contents
* [Background](#background)
* [Summary](#summary)
* [Design Decisions](#design-decisions)
@ -110,7 +111,7 @@ 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
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.
@ -579,6 +580,7 @@ the compiler must respond with a `CompileFailure` instead of a `CompileSuccess`.
The compiler must emit an event of type `LogEventType.WARNING` if any of the
following is true:
* an invalid deprecation ID is passed
* an obsolete deprecation ID is passed
* a future deprecation ID is passed that is not also passed to

View File

@ -62,7 +62,7 @@ This proposal makes three breaking changes to the embedded Sass protocol:
#### Length Before Compilation ID
This proposal places the compilation ID for each request _after_ the length. The
This proposal places the compilation ID for each request *after* the length. The
length is defined as the length of the protocol buffer plus the length of the
compilation ID.
@ -101,7 +101,7 @@ length of the compilation ID, which is given by the following table:
#### Cross-Compilation State
We have a [future goal] to (optionally) share state _across_ compilations, to
We have a [future goal] to (optionally) share state *across* compilations, to
more efficiently compile projects with many small entrypoints where the bulk of
the complexity is in static shared libraries. If/when we support this, there
could be two broad implementation strategies for a compiler with worker-based
@ -139,8 +139,8 @@ compilation ID will only have one request at a time, so we could just declare
that any response with a given compilation ID is for the single outstanding
request.
However, the _expectation_ that each compilation be single-threaded isn't a
_requirement_. One could imagine a multithreaded Sass compiler that actually is
However, the *expectation* that each compilation be single-threaded isn't a
*requirement*. One could imagine a multithreaded Sass compiler that actually is
capable of fielding multiple concurrent requests as it compiles independent
chunks of a given stylesheet or resolves loads eagerly. We don't want to cut off
this possibility, so we retain the outbound request IDs.
@ -153,12 +153,12 @@ Replace the last paragraph of the [embedded protocol overview] with:
[embedded protocol overview]: ../spec/embedded-protocol.md#overview
Each message in the embedded protocol is sent as a _packet_ which contains two
Each message in the embedded protocol is sent as a *packet* which contains two
values: an unsigned [varint] up to 32 bits long known as the "compilation ID",
and a protocol buffer that contains the protobuf message. For streams (like
standard input and output) that don't have built-in message boundaries, every
packet must begin with another unsigned varint indicating the length in bytes of
the remaining message (_including the compilation ID_). This matches the best
the remaining message (*including the compilation ID*). This matches the best
practice described in [the protocol buffer documentation].
Because JavaScript can't easily represent integers larger than 2^53 - 1, the

View File

@ -18,7 +18,7 @@ extend(a.foo, a, b) = a.foo, b.foo
extend(c, a, b) = c
```
### Specificity of the Base Selector
## Specificity of the Base Selector
Note that so far, it's always the case that `extend(S, A, B)[0] = S`. However,
consider `extend(a.foo, .foo, a)`. One interpretation of this would give the
@ -62,12 +62,12 @@ This new selector has higher specificity than the original. As such, we must
allow the generated selector to have higher specificity than the original in
some cases.
#### First Law of Extend: `spec(extend(S, A, B)[0]) >= spec(S)`
### First Law of Extend: `spec(extend(S, A, B)[0]) >= spec(S)`
This is not always the behavior in Sass, either in master or in stable; this is
clearly a bug that should be fixed.
### Specificity of Generated Selectors
## Specificity of Generated Selectors
Now that we've established what `spec(extend(S, A, B)[0])` should look like,
it's time to think about what `spec(extend(S, A, B)[1])` should look like as
@ -97,9 +97,9 @@ There is one guarantee we can make, though:
`spec(extend(S, A, B)[1]) >= spec(B)`, since everything in `S` is either merged
with or added to `B`.
#### Second Law of Extend: `spec(extend(S, A, B)[1]) >= spec(B)`
### Second Law of Extend: `spec(extend(S, A, B)[1]) >= spec(B)`
### Implications for Optimization
## Implications for Optimization
The ultimate goal of this discussion is, of course, that we want to be able to
perform certain optimizations on the generated selectors in order to reduce
@ -120,7 +120,7 @@ However, many of the optimizations added in [8f4869e][] do still work. For
example, `extend(.bar a, a, a.foo) = .bar a` works because
`spec(.bar a) = spec(a.foo)`.
### Conclusion
## Conclusion
As long as we make the `@extend` optimizer specificity-aware, we can retain a
number of useful optimizations while still providing the same guarantees that

View File

@ -380,8 +380,6 @@ case-insensitively.
†: These productions are invalid in plain CSS syntax.
[`<function-token>`]: https://drafts.csswg.org/css-syntax-3/#ref-for-typedef-function-token%E2%91%A0
> The `CalcArgument` production provides backwards-compatibility with the
> historical use of interpolation to inject SassScript values into `calc()`
> expressions. Because interpolation could inject any part of a `calc()`
@ -391,9 +389,7 @@ case-insensitively.
### `CssMinMax`
This proposal replaces the reference to `CalcValue` in the definition of
[`CssMinMax`] with `CalcArgument`.
[`CssMinMax`]: ../spec/types/calculation.md#cssminmax
`CssMinMax` with `CalcArgument`.
> Note that this increases the number of cases where a `MinMaxExpression` will
> be parsed as a `CssMinMax` rather than a `FunctionExpression` (for example,
@ -544,12 +540,12 @@ This algorithm takes a `CalculationValue` `value` and returns a
* If `value` is a calculation:
* Let `result` be the result of [simplifying] `value`.
* Let `result` be the result of [simplifying] `value`.
* If `result` is a calculation whose name is `"calc"`, return `result`'s
* If `result` is a calculation whose name is `"calc"`, return `result`'s
single argument.
* Otherwise, return `result`.
* Otherwise, return `result`.
[simplifying]: #simplifying-a-calculation

View File

@ -61,7 +61,6 @@ one would with function values:
* `meta.call()` => `meta.apply()`
### JavaScript API Design Decisions
Mixins differ from functions in that the result of their execution is a Sass AST
@ -73,7 +72,6 @@ For this reason, it is not meaningful -- or even possible -- to construct or
execute a mixin through the JavaScript API. A mixin object shall be opaque, and
the only operation available shall be to return the object as-is.
## Types
This proposal promotes the [mixin value] to a Sass value type.
@ -290,4 +288,4 @@ message CompilerMixin {
The protocol allows first-class mixins defined in the compiler to be passed
to the host and vice-versa as `Value.CompilerMixin`s.
Two first-class mixins are equal if they have the same ID.
Two first-class mixins are equal if they have the same ID.

View File

@ -660,8 +660,6 @@ Replace this function's procedure with:
* Let `double` be the value of [converting `$number` to `rad`] allowing
unitless.
[converting `$number` to `rad`]: #converting-a-number-to-units
* Return a unitless number whose value is the result of `sin(double)` as defined
by [IEEE 754 2019], §9.2.
@ -672,8 +670,6 @@ Replace this function's procedure with:
* Let `double` be the value of [converting `$number` to `rad`] allowing
unitless.
[converting `$number` to `rad`]: #converting-a-number-to-units
* Return a unitless number whose value is the result of `tan(double)` as defined
by [IEEE 754 2019], §9.2.

View File

@ -46,23 +46,23 @@ I propose that, as today, `#{}` be allowed either within strings (quoted or
unquoted) or on its own. However, its effect will be limited to the strings that
contain it or to its own value. Specifically:
- When parsing or evaluating a quoted string, treat interpolation the same way
* When parsing or evaluating a quoted string, treat interpolation the same way
it's treated today.
- When parsing an identifier, treat interpolation as though it's an alphabetic
* When parsing an identifier, treat interpolation as though it's an alphabetic
character. When evaluating an interpolated unquoted string, concatenate the
literal identifier characters with the values of the interpolated segments.
- Otherwise, parse an interpolation as an individual expression. When evaluating
* Otherwise, parse an interpolation as an individual expression. When evaluating
it, return its value as an unquoted string.
Here are some examples (I'm including quotes for unquoted strings in the output
to clarify their extents):
- `"a #{b} c"` would continue to produce `"a b c"`.
- `a#{b}c` would continue to produce `"abc"`.
- `a #{b}c` currently produces `"a bc"` but would produce `"a" "bc"`.
- `a#{b} c` currently produces `"ab c"` but would produce `"ab" "c"`.
- `a b#{c}d e` currently produces `"a bcd e"` but would produce `"a" "bcd" "e"`.
- `a #{b} c` currently produces `"a b c"` but would produce `"a" "b" "c"`.
* `"a #{b} c"` would continue to produce `"a b c"`.
* `a#{b}c` would continue to produce `"abc"`.
* `a #{b}c` currently produces `"a bc"` but would produce `"a" "bc"`.
* `a#{b} c` currently produces `"ab c"` but would produce `"ab" "c"`.
* `a b#{c}d e` currently produces `"a bcd e"` but would produce `"a" "bcd" "e"`.
* `a #{b} c` currently produces `"a b c"` but would produce `"a" "b" "c"`.
## Design decisions
@ -77,24 +77,24 @@ various situations.
### Interpolation in unquoted strings
It was tempting to restrict interpolation for use _only_ in quoted strings.
It was tempting to restrict interpolation for use *only* in quoted strings.
Interpolation in unquoted strings can be mimicked using `+`, and allowing it in
unquoted strings could produce the incorrect impression that interpolation is
performed before any other SassScript resolution. However, we decided to allow
this for several reasons:
- Backwards compatibility, as described above.
- Similarity with quoted strings. It's not always obvious that unquoted strings
* Backwards compatibility, as described above.
* Similarity with quoted strings. It's not always obvious that unquoted strings
and quoted strings are the same sorts of value under the hood, but sharing
capabilities helps reinforce that idea.
- Similarity with other identifiers. Interpolation can be used in almost all
* Similarity with other identifiers. Interpolation can be used in almost all
most non-SassScript contexts where identifiers appear, most notably property
names, so it's natural that users would think that all Sass identifiers can be
interpolated.
- Vendor prefixes. It would be very difficult to dynamically choose vendor
* Vendor prefixes. It would be very difficult to dynamically choose vendor
prefixes for function names or other values, since `-` on its own is not an
identifier.
- Aesthetics. Although `font-stretch: $amount + -condensed` is legal, it's less
* Aesthetics. Although `font-stretch: $amount + -condensed` is legal, it's less
clear and less pleasant than `font-stretch: #{$amount}-condensed`.
### Interpolation outside of strings
@ -128,13 +128,13 @@ rules, and E the value of the same expression under the new rules. Let S2 be the
conversion of E to CSS. For example, suppose the expression in question is
`a #{b} + c`. S1 is `"a b + c"`, E is `"a" "bc"`, and S2 is `"a bc"`.
- If S1 and S2 aren't semantically identical when interpreted as CSS, issue a
* If S1 and S2 aren't semantically identical when interpreted as CSS, issue a
warning. This means that `#{a} + b` would emit a warning since S1 is `"a + b"`
but S2 is `"ab"`. However, `#{a} b c` would not emit a warning, since S1 and
S2 are both `"a b c"`. Note that an expressions like `#{a} / b` _should not_
S2 are both `"a b c"`. Note that an expressions like `#{a} / b` *should not*
emit a warning here, since we know that it will produce `a/b` under the new
semantics.
- Otherwise, if E is not a string, set an "interpolated" flag on S1. If any
* Otherwise, if E is not a string, set an "interpolated" flag on S1. If any
operation is performed on S1 that wouldn't first convert it to a string, emit
a warning.
@ -146,14 +146,14 @@ problem in the second case, which we'll get to below.
In service of determining how to go about deprecating the current semantics of
SassScript interpolation, I want to precisely define them. For our purposes, we
only care about _free interpolation_—that is, interpolation outside the context
only care about *free interpolation*—that is, interpolation outside the context
of a string or a special function (e.g. `calc()`) that's parsed like a string.
The grammar for interpolation is straightforward. Note that the representation
below elides much of the unrelated complexity of the SassScript grammar. The
`Operation` and `UnaryOperation` productions should be understood to encompass
all binary and unary operations supported by SassScript, except for `,` which is
handled by the `CommaList` production. Note that this _includes_ the implicit
handled by the `CommaList` production. Note that this *includes* the implicit
adjacency operator that normally creates space-separated lists. `Value` should
be understood to encompass literals, parenthesized expressions, maps, and
function calls.
@ -192,8 +192,8 @@ If there was any whitespace in the source text between the operator and the
| `-#{1}` | ``` `-#{1}` ``` | `-1` |
| `- #{1}` | ``` `- #{1}` ``` | `- 1` |
For an `Operation` production, all _adjacent_ `UnaryOperation` sub-expressions
that are _not_ `Interpolation`s are parsed as normal, and interpolated into the
For an `Operation` production, all *adjacent* `UnaryOperation` sub-expressions
that are *not* `Interpolation`s are parsed as normal, and interpolated into the
ESI alongside the `Interpolation` subexpressions, separated by the operation in
question. As with a `UnaryOperation`, a space will be included before or after
the `Interpolation`s depending on whether whitespace appeared in the
@ -212,7 +212,7 @@ have a space operator.
| `a#{b}c` | ``` `#{a}#{b}#{c}` ``` | `abc` |
Finally, `CommaList` productions behave almost the same as `Operation`s. The
only difference is that if _only_ the first `Operation` sub-expression is an
only difference is that if *only* the first `Operation` sub-expression is an
`Interpolation`, the rest of the list isn't included in the interpolation.
| SassScript | ESI | CSS |
@ -228,8 +228,8 @@ Now that we (hopefully) have a clear idea of how free interpolation works right
now, we can start figuring out the surface area that needs deprecation warnings
when moving to the new semantics.
Ideally, we want to warn only when the new semantics will produce _semantically
different_ CSS output. In practice determining this exactly isn't always
Ideally, we want to warn only when the new semantics will produce *semantically
different* CSS output. In practice determining this exactly isn't always
feasible, since free interpolation produces values that can be used in many
heterogeneous ways, so instead we'll warn if the values they produce are ever
used in a way that will change behavior under the new semantics.
@ -268,7 +268,7 @@ different meanings. This includes any operators that don't insert their own
textual representation when operating on a string.
The following operators and their inverses should produce warnings immediately.
Note that _any expression_ containing free interpolation whose new ESI contains
Note that *any expression* containing free interpolation whose new ESI contains
these operators should have an immediate warning, even if they also include
other operators.
@ -328,7 +328,7 @@ the first and fourth should not, as their stringifications remain the same.
There is one case where the new behavior differs from the old. It comes up when
a dynamic value is included in an interpolated string without an explicit
`#{}`—that is, for every location that doesn't have a `#{}` in the SassScript
but does in the ESI. _If that value is a quoted string_, it will retain its
but does in the ESI. *If that value is a quoted string*, it will retain its
quotes, where if it were explicitly interpolated it would lose them. For
example:
@ -375,30 +375,30 @@ as a list. When passing an interpolation value produced via a list operator to
such a function, the implementation should emit a deprecation warning. Of the
canonical Sass functions, this includes:
- `unquote()`
- `quote()`
- `str-length()`
- The first or second argument of `str-insert()`
- `str-index()`
- The first argument of `str-slice()`
- `to-upper-case()`
- `to-lower-case()`
- `length()`
- The first argument of `nth()`
- The first argument of `set-nth()`
- `join()`
- The first or last argument of `append()`
- `zip()`
- The first argument of `index()`
- `list-separator()`
- `feature-exists()`
- `variable-exists()`
- `global-variable-exists()`
- `function-exists()`
- `mixin-exists()`
- `inspect()`
- `type-of()`
- The first argument of `call()`
* `unquote()`
* `quote()`
* `str-length()`
* The first or second argument of `str-insert()`
* `str-index()`
* The first argument of `str-slice()`
* `to-upper-case()`
* `to-lower-case()`
* `length()`
* The first argument of `nth()`
* The first argument of `set-nth()`
* `join()`
* The first or last argument of `append()`
* `zip()`
* The first argument of `index()`
* `list-separator()`
* `feature-exists()`
* `variable-exists()`
* `global-variable-exists()`
* `function-exists()`
* `mixin-exists()`
* `inspect()`
* `type-of()`
* The first argument of `call()`
It's up to each implementation to determine whether to emit warnings for which
user-defined functions.

View File

@ -131,15 +131,12 @@ This production has the same grammar as [`<ident-token>`][].
This algorithm consumes input from a stream of [code points][] and returns a
sequence of strings and/or expressions.
[code points]: https://infra.spec.whatwg.org/#code-point
The grammar for this production is:
<x><pre>
**InterpolatedIdentifier** ::= ([\<ident-token>][`<ident-token>`] | '-'? Interpolation) ([Name](#consuming-a-name) | Interpolation)*
</pre></x>
[name-start code point]: https://drafts.csswg.org/css-syntax-3/#name-start-code-point
[escape]: https://drafts.csswg.org/css-syntax-3/#escape-diagram
No whitespace is allowed between components of an `InterpolatedIdentifier`.
@ -219,10 +216,9 @@ This production has the same grammar as [`escape`][escape] in CSS Syntax Level 3
[non-printable code point]: https://drafts.csswg.org/css-syntax-3/#non-printable-code-point
[digit]: https://drafts.csswg.org/css-syntax-3/#digit
* Let `code` be the lowercase hexadecimal representation of `codepoint`,
* Let `code` be the lowercase hexadecimal representation of `codepoint`,
with no leading `0`s.
* Return `"\"` + `code` + `" "`.
* Return `"\"` + `code` + `" "`.
* Otherwise, return `"\"` + `character`.

View File

@ -139,7 +139,6 @@ created, if the type is not specified, it is considered *explicit*.
This proposal modifies the fourth bullet of the [Loading Modules][] procedure
within the [module system spec][] to read as follows:
* If `file` has already been [executed][]:
* If `config` is **explicit and** not empty, throw an error.

View File

@ -61,8 +61,8 @@ all identifiers matched case-insensitively):
<x><pre>
**MediaQuery** ::= MediaNot
&#32; | MediaInParens (MediaAnd* | MediaOr*)
&#32; | MediaType ('and' MediaNot | MediaAnd*)
&#32; | MediaInParens (MediaAnd\* | MediaOr\*)
&#32; | MediaType ('and' MediaNot | MediaAnd\*)
**MediaType** ::= [InterpolatedIdentifier] [InterpolatedIdentifier]¹?
**MediaNot**² ::= 'not' MediaOrInterp
**MediaAnd**² ::= 'and' MediaOrInterp
@ -73,7 +73,7 @@ all identifiers matched case-insensitively):
&#32; | '(' Expression³ [\<mf-lt>] Expression³ [\<mf-lt>] Expression³ ')'
&#32; | '(' Expression³ [\<mf-gt>] Expression³ [\<mf-gt>] Expression³ ')'
&#32; | '(' MediaNot ')'
&#32; | '(' MediaInParens (MediaAnd* | MediaOr*) ')'
&#32; | '(' MediaInParens (MediaAnd\* | MediaOr\*) ')'
</pre></x>
[InterpolatedIdentifier]: ../spec/syntax.md#interpolatedidentifier

View File

@ -69,7 +69,6 @@ intended to replace the existing syntax.
&#32; | '(' Expression² <mf-gt> Expression² <mf-gt> Expression² ')'
</pre></x>
1: This `InterpolatedIdentifier` may not be the identifier `"and"`.
2: These `Expression`s may not contain binary operator expressions with the

View File

@ -81,7 +81,7 @@ The grammar for this production is:
<x><pre>
**MinMaxExpression** ::= CssMinMax | FunctionExpression
**CssMinMax** ::= ('min(' | 'max(') CalcValue (',' CalcValue)* ')'
**CssMinMax** ::= ('min(' | 'max(') CalcValue (',' CalcValue)\* ')'
**CalcValue** ::= CalcValue (('+' | '-' | '*' | '/') CalcValue)+
&#32; | '(' CalcValue ')'
&#32; | ('calc(' | 'env(' | 'var(') InterpolatedDeclarationValue ')'

View File

@ -777,7 +777,7 @@ controlled as explicitly as members can.
> ```
>
> isn't identical (from a downstream user's perspective) to
>
>
> ```scss
> .foo, .bar { /* ... */ }
> ```
@ -853,7 +853,6 @@ CSS for *all* modules transitively used or forwarded by `starting-module`.
* Add a copy of `extension` with its extender replaced by `complex` to
`new-extensions[domestic]`.
[the first law of extend]: ../spec/at-rules/extend.md#specificity
[the specificity laws of extend]: ../spec/at-rules/extend.md#specificity
* Let `css` be an empty CSS tree.
@ -867,7 +866,7 @@ CSS for *all* modules transitively used or forwarded by `starting-module`.
> Because this traverses modules depth-first, it emits CSS in reverse
> topological order.
* Let `initial-imports` be the longest initial subsequence of top-level
statements in `domestic`'s CSS that contains only comments and `@import`
rules *and* that ends with an `@import` rule.
@ -888,7 +887,6 @@ CSS for *all* modules transitively used or forwarded by `starting-module`.
* Return `css`.
[queue]: https://en.wikipedia.org/wiki/Queue_(abstract_data_type)
[topological]: https://en.wikipedia.org/wiki/Topological_sorting
### Resolving a `file:` URL
@ -1096,7 +1094,7 @@ Given a source file `file`, a [configuration](#configuration) `config`, and an
* Remove any [complex selectors][] containing a placeholder selector that
begins with `-` or `_` from `css`.
* Remove any style rules that now have no selector from `css`.
* Append `css` to `module`'s CSS.

View File

@ -5,6 +5,7 @@
This proposal adds the following members to the built-in `sass:math` module.
## Table of Contents
* [Background](#background)
* [Summary](#summary)
* [Semantics](#semantics)

View File

@ -23,13 +23,13 @@ setting, and getting elements from nested maps.
Variables have always been a key feature of the Sass language. But these days,
design systems and component libraries form the basis of most CSS projects --
with well organized _design tokens_ as the foundation. While Individual token
with well organized *design tokens* as the foundation. While Individual token
variables can be quite useful, the ability to group tokens into structured and
meaningful relationships is essential for creating resilient systems.
There are many ways to group tokens. The popular [Style Dictionary] recommends a
deep nesting of _category_, _type_, _item_, _sub-item_, and _state_. Other
taxonomies also include concepts like _theme_, or even _operating system_. Most
deep nesting of *category*, *type*, *item*, *sub-item*, and *state*. Other
taxonomies also include concepts like *theme*, or even *operating system*. Most
of the existing tools rely on YAML or JSON objects to achieve that nested
structure, at the expense of other important information. YAML and JSON are not
design languages, and do not understand fundamental CSS concepts like color or

View File

@ -97,9 +97,9 @@ a {
```
Linked order makes sense when using comments to annotate information about
dependencies, but it's counterproductive when a user wants to annotate the _end_
dependencies, but it's counterproductive when a user wants to annotate the *end*
of a module, since that comment would be considered linked to the next module
load. Traversal order handles that case better _and_ matches the old `@import`
load. Traversal order handles that case better *and* matches the old `@import`
behavior, so we chose to use it instead.
## Procedures

View File

@ -41,8 +41,6 @@ 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.
@ -63,10 +61,10 @@ 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
* `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
* `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 proposal.
@ -110,7 +108,7 @@ Providing access to the containing URL puts these invariants at risk in two ways
#### Alternatives Considered
To mitigate these risks, we need to have _some_ restriction on when the
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:
@ -155,7 +153,7 @@ 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
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.
@ -190,7 +188,7 @@ interface FileImporter<sync extends 'sync' | 'async' = 'sync' | 'async'> {
```ts
findFileUrl(
url: string,
options: {fromImport: boolean, containingUrl?: URL}
options: {fromImport: boolean; containingUrl?: URL}
): PromiseOr<URL | null, sync>;
```

View File

@ -33,7 +33,6 @@ However, a numeric integer can include units (e.g. `5px` or `8em`) and the
current behavior [drops the units][issue], which is unexpected for most users.
For example: `math.random(42px) => 28` (there is no `px`).
[random]: https://sass-lang.com/documentation/modules/math#random
[issue]: https://github.com/sass/sass/issues/1890
## Summary
@ -83,9 +82,10 @@ defaults to `null`.
[units] as `$limit`.
> Examples:
> - `math.random(123) => 87`
> - `math.random(123px) => 43px`
> - `math.random(500%) => 238%`
>
> * `math.random(123) => 87`
> * `math.random(123px) => 43px`
> * `math.random(500%) => 238%`
* Otherwise throw an error.

View File

@ -1,3 +1,7 @@
## Draft 3.1
* Update to accommodate new calculation parsing logic.
## Draft 3
* Make a potentially slash-separated number slash-free when passing it as an

View File

@ -1,4 +1,4 @@
# Forward Slash as a Separator: Draft 3
# Forward Slash as a Separator: Draft 3.1
*([Issue](https://github.com/sass/sass/issues/2565), [Changelog](slash-separator.changes.md))*
@ -15,7 +15,13 @@ operator.
* [First-Class `calc()`](#first-class-calc)
* [`math()` Syntax](#math-syntax)
* [Existing Behavior](#existing-behavior)
* [Definitions](#definitions)
* [Calculation-Safe Expression](#calculation-safe-expression)
* [Syntax](#syntax)
* [Procedures](#procedures)
* [Evaluating a `FunctionCall` as a Calculation](#evaluating-a-functioncall-as-a-calculation)
* [Adjusting Slash Precedence](#adjusting-slash-precedence)
* [`SlashListExpression`](#slashlistexpression)
* [Semantics](#semantics)
* [Slash-Separated Lists](#slash-separated-lists)
* [`math.div()` Function](#mathdiv-function)
@ -199,6 +205,16 @@ value, it is written as the original numerator followed by `/` followed by the
original denominator. If either the original numerator or denominator are
themselves slash-separated, they're also written this way.
## Definitions
### Calculation-Safe Expression
Remove "or `/`" from the definition of a [calculation-safe] `ProductExpression`.
Add "An unbracketed `SlashListExpression` with more than one element, all of
which are calculation-safe" to the list of calculation-safe expressions.
[calculation-safe]: ../spec/types/calculation.md#calculation-safe-expression
## Syntax
> Note that the existing productions being modified have not been defined
@ -237,6 +253,86 @@ When a `SlashListExpression` with one or more `/`s is evaluated, it produces a
list object whose contents are the values of its constituent
`SpaceListExpression`s and whose separator is "slash".
## Procedures
### Evaluating a `FunctionCall` as a Calculation
Replace "evaluating each `Expression`" with "[adjusting slash precedence] in and
then evaluating each `Expression`" in [evaluting a `FunctionCall` as a
calculation].
[adjusting slash precedence]: #adjusting-slash-precedence
[evaluting a `FunctionCall` as a calculation]: ../spec/types/calculation.md#evaluating-a-functioncall-as-a-calculation
### 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.
* 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.
### `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`.
## Semantics
### Slash-Separated Lists

View File

@ -5,7 +5,7 @@
## Draft 1.1
* Returns a bracketed list instead of an unbracketed one to be more clear
about what type of value is being returned.
about what type of value is being returned.
## Draft 1

View File

@ -15,22 +15,22 @@ This proposal adds `string.split()` to the `sass:string` module.
> This section is non-normative.
The `sass:string` module contains several functions for manipulating and finding
out information about strings. Currently, though, there is no built-in function
that splits one string into a list of substrings, and authors have been creating
The `sass:string` module contains several functions for manipulating and finding
out information about strings. Currently, though, there is no built-in function
that splits one string into a list of substrings, and authors have been creating
their own versions of functions that achieve this functionality.
## Summary
> This section is non-normative.
This proposal adds the `string.split()` function to the `sass:string` module.
The function takes a string, splits it based on a provided separator, and
This proposal adds the `string.split()` function to the `sass:string` module.
The function takes a string, splits it based on a provided separator, and
returns a bracketed, comma-separated list of substrings.
This could be used to take a string and repurpose parts of it for some other
use. For example, fonts contained in a font stack list could be split into
segments and then used as keys in a new map.
This could be used to take a string and repurpose parts of it for some other
use. For example, fonts contained in a font stack list could be split into
segments and then used as keys in a new map.
Examples:
@ -39,14 +39,13 @@ $fonts: "Helvetica Neue, Helvetica, Arial";
string.split($fonts, ', '); // ["Helvetica Neue", "Helvetica", "Arial"]
```
A third argument can set a limit to the the number of splits performed on the
A third argument can set a limit to the the number of splits performed on the
string:
```scss
string.split($fonts, ', ', 1); // ["Helvetica Neue", "Helvetica, Arial"]
```
An empty `$separator` returns all Unicode code points in the original string:
```scss
@ -54,7 +53,6 @@ $font: "Helvetica"
string.split($font, ''); // ["H", "e", "l", "v", "e", "t", "i", "c", "a"]
```
## Semantics
### `split()`
@ -71,12 +69,12 @@ split($string, $separator, $limit: null)
* If `$limit` is less than 1, throw an error.
* If `$string` is an empty string, return a list with `$string` as the only
* If `$string` is an empty string, return a list with `$string` as the only
item.
* Let `split-list` be an empty list.
* If `$limit` is `null`, set `$limit` to the value of calling
* If `$limit` is `null`, set `$limit` to the value of calling
`string.length($string)`.
* Let `split-counter` equal 0.
@ -87,7 +85,7 @@ split($string, $separator, $limit: null)
* Append `$string` to `split-list`.
* Set `$string` to an empty string.
* Set `$string` to an empty string.
* Otherwise:
@ -103,10 +101,10 @@ split($string, $separator, $limit: null)
* Otherwise:
* Let `index` be the result of calling
* Let `index` be the result of calling
`string.index($string, $separator)`.
* If `index` is `null`, append `$string` to `split-list` and set `$string`
* If `index` is `null`, append `$string` to `split-list` and set `$string`
to an empty string.
* Otherwise:
@ -115,10 +113,10 @@ split($string, $separator, $limit: null)
`string.slice($string, 1, index - 1)`.
* Append `current-substring` to `split-list`.
* Set `$string` to
* Set `$string` to
`string.slice($string, index + string.length($separator))`.
* Increase `split-counter` by 1.
* Return `split-list` as a bracketed, comma-separated list.
* Return `split-list` as a bracketed, comma-separated list.

4
buf.gen.yaml Normal file
View File

@ -0,0 +1,4 @@
version: v1
plugins:
- plugin: buf.build/bufbuild/es
out: gen

2
buf.work.yaml Normal file
View File

@ -0,0 +1,2 @@
version: v1
directories: [spec]

View File

@ -1,6 +1,38 @@
import {Syntax} from './options';
import {PromiseOr} from './util/promise_or';
/**
* Contextual information passed to {@link Importer.canonicalize} and {@link
* FileImporter.findFileUrl}. Not all importers will need this information to
* resolve loads, but some may find it useful.
*/
export interface CanonicalizeContext {
/**
* Whether this is being invoked because of a Sass
* `@import` rule, as opposed to a `@use` or `@forward` rule.
*
* This should *only* be used for determining whether or not to load
* [import-only files](https://sass-lang.com/documentation/at-rules/import#import-only-files).
*/
fromImport: boolean;
/**
* The canonical URL of the file that contains the load, if that information
* is available.
*
* For an {@link Importer}, this is only passed when the `url` parameter is a
* relative URL _or_ when its [URL scheme] is included in {@link
* Importer.nonCanonicalScheme}. This ensures that canonical URLs are always
* resolved the same way regardless of context.
*
* [URL scheme]: https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL#scheme
*
* For a {@link FileImporter}, this is always available as long as Sass knows
* the canonical URL of the containing file.
*/
containingUrl: URL | null;
}
/**
* A special type of importer that redirects all loads to existing files on
* disk. Although this is less powerful than a full {@link Importer}, it
@ -56,12 +88,6 @@ export interface FileImporter<
* @param url - The loaded URL. Since this might be relative, it's represented
* as a string rather than a {@link URL} object.
*
* @param options.fromImport - Whether this is being invoked because of a Sass
* `@import` rule, as opposed to a `@use` or `@forward` rule.
*
* This should *only* be used for determining whether or not to load
* [import-only files](https://sass-lang.com/documentation/at-rules/import#import-only-files).
*
* @returns An absolute `file:` URL if this importer recognizes the `url`.
* This may be only partially resolved: the compiler will automatically look
* for [partials](https://sass-lang.com/documentation/at-rules/use#partials),
@ -85,7 +111,7 @@ export interface FileImporter<
*/
findFileUrl(
url: string,
options: {fromImport: boolean}
context: CanonicalizeContext
): PromiseOr<URL | null, sync>;
/** @hidden */
@ -220,12 +246,6 @@ export interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
* @param url - The loaded URL. Since this might be relative, it's represented
* as a string rather than a {@link URL} object.
*
* @param options.fromImport - Whether this is being invoked because of a Sass
* `@import` rule, as opposed to a `@use` or `@forward` rule.
*
* This should *only* be used for determining whether or not to load
* [import-only files](https://sass-lang.com/documentation/at-rules/import#import-only-files).
*
* @returns An absolute URL if this importer recognizes the `url`, or `null`
* if it doesn't. If this returns `null`, other importers or {@link
* Options.loadPaths | load paths} may handle the load.
@ -242,7 +262,7 @@ export interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
*/
canonicalize(
url: string,
options: {fromImport: boolean}
context: CanonicalizeContext
): PromiseOr<URL | null, sync>;
/**
@ -272,6 +292,20 @@ export interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
/** @hidden */
findFileUrl?: never;
/**
* A URL scheme or set of schemes (without the `:`) that this importer
* promises never to use for URLs returned by {@link canonicalize}. If it does
* return a URL with one of these schemes, that's an error.
*
* If this is set, any call to canonicalize for a URL with a non-canonical
* scheme will be passed {@link CanonicalizeContext.containingUrl} if it's
* known.
*
* These schemes may only contain lowercase ASCII letters, ASCII numerals,
* `+`, `-`, and `.`. They may not be empty.
*/
nonCanonicalScheme?: string | string[];
}
/**

View File

@ -10,7 +10,12 @@ export {
compileStringAsync,
} from './compile';
export {Exception} from './exception';
export {FileImporter, Importer, ImporterResult} from './importer';
export {
CanonicalizeContext,
FileImporter,
Importer,
ImporterResult,
} from './importer';
export {Logger, SourceSpan, SourceLocation} from './logger';
export {
CustomFunction,
@ -35,6 +40,7 @@ export {
SassFunction,
SassList,
SassMap,
SassMixin,
SassNumber,
SassString,
Value,

View File

@ -12,6 +12,13 @@ export class SassColor extends Value {
/**
* Creates an RGB color.
*
* **Only** `undefined` should be passed to indicate a missing `alpha`. If
* `null` is passed instead, it will be treated as a [missing component] in
* future versions of Dart Sass. See [breaking changes] for details.
*
* [missing component]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#missing_color_components
* [breaking changes]: /documentation/breaking-changes/null-alpha
*
* @throws `Error` if `red`, `green`, and `blue` aren't between `0` and
* `255`, or if `alpha` isn't between `0` and `1`.
*/
@ -25,6 +32,13 @@ export class SassColor extends Value {
/**
* Creates an HSL color.
*
* **Only** `undefined` should be passed to indicate a missing `alpha`. If
* `null` is passed instead, it will be treated as a [missing component] in
* future versions of Dart Sass. See [breaking changes] for details.
*
* [missing component]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#missing_color_components
* [breaking changes]: /documentation/breaking-changes/null-alpha
*
* @throws `Error` if `saturation` or `lightness` aren't between `0` and
* `100`, or if `alpha` isn't between `0` and `1`.
*/
@ -38,6 +52,13 @@ export class SassColor extends Value {
/**
* Creates an HWB color.
*
* **Only** `undefined` should be passed to indicate a missing `alpha`. If
* `null` is passed instead, it will be treated as a [missing component] in
* future versions of Dart Sass. See [breaking changes] for details.
*
* [missing component]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#missing_color_components
* [breaking changes]: /documentation/breaking-changes/null-alpha
*
* @throws `Error` if `whiteness` or `blackness` aren't between `0` and `100`,
* or if `alpha` isn't between `0` and `1`.
*/

View File

@ -6,6 +6,7 @@ import {SassColor} from './color';
import {SassFunction} from './function';
import {ListSeparator} from './list';
import {SassMap} from './map';
import {SassMixin} from './mixin';
import {SassNumber} from './number';
import {SassString} from './string';
@ -22,6 +23,7 @@ export {SassColor} from './color';
export {SassFunction} from './function';
export {SassList, ListSeparator} from './list';
export {SassMap} from './map';
export {SassMixin} from './mixin';
export {SassNumber} from './number';
export {SassString} from './string';
@ -156,6 +158,14 @@ export abstract class Value implements ValueObject {
*/
assertMap(name?: string): SassMap;
/**
* Throws if `this` isn't a {@link SassMixin}.
*
* @param name - The name of the function argument `this` came from (without
* the `$`) if it came from an argument. Used for error reporting.
*/
assertMixin(name?: string): SassMixin;
/**
* Throws if `this` isn't a {@link SassNumber}.
*

14
js-api-doc/value/mixin.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import {Value} from './index';
/**
* Sass's [mixin type](https://sass-lang.com/documentation/values/mixins).
*
* @category Custom Function
*/
export class SassMixin extends Value {
/**
* It is not possible to construct a Sass mixin outside of Sass. Attempting to
* construct one will throw an exception.
*/
constructor();
}

1581
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,31 +16,31 @@
"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",
"markdownlint": "npx markdownlint-cli2 '**/*.md' '*.md' --ignore 'node_modules/**'",
"fix": "npm run update-toc && npm run markdownlint -- --fix && 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/node": "^14.11.2",
"@types/prettier": "^2.4.1",
"colors": "^1.3.3",
"diff": "^5.0.0",
"glob": "^10.0.0",
"gts": "^3.1.0",
"immutable": "^4.0.0",
"indent-string": "^4.0.0",
"markdown-link-check": "3.11.1",
"markdown-toc": "^1.2.0",
"markdownlint-cli2": "^0.8.1",
"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.24.7"
},
"devDependencies": {
"@types/node": "^14.11.2",
"gts": "^3.1.0",
"typedoc": "^0.24.7",
"typescript": "^5.0.4"
}
}

View File

@ -0,0 +1,21 @@
## Draft 1.1
* Clarify values in `channels` and `channelsOrNull`.
* Throw an error if construction space can not be determined.
* Remove `alpha` from list of deprecated getters.
* Rename types: `ColorSpaceLAB` to `ColorSpaceLab`, `ChannelNameLAB` to
`ChannelNameLab`.
* Use `Exclude<>` instead of `Omit<>` for union types.
* Make procedure for determining space backwards compatible when using `change`
for legacy colors.
* Fix channel names for `change` with `oklch` and `lch`.
## Draft 1
* Initial draft

View File

@ -0,0 +1,961 @@
# CSS Color Level 4, New Color Spaces JavaScript API: Draft 1.1
*([Issue](https://github.com/sass/sass/issues/2831),
[Changelog](color-4-new-spaces-js.changes.md))*
This proposal updates Sass's JavaScript (JS) API to match the [color spaces
proposal].
[color spaces proposal]: ./color-4-new-spaces.md
## Table of Contents
* [API](#api)
* [Types](#types)
* [Color Space Definitions](#color-space-definitions)
* [New Color Functions](#new-color-functions)
* [`space`](#space)
* [`toSpace`](#tospace)
* [`isLegacy`](#islegacy)
* [`isInGamut`](#isingamut)
* [`toGamut`](#togamut)
* [`channelsOrNull`](#channelsornull)
* [`channels`](#channels)
* [`channel`](#channel)
* [`alpha`](#alpha)
* [`isChannelMissing`](#ischannelmissing)
* [`isAlphaMissing`](#isalphamissing)
* [`isChannelPowerless`](#ischannelpowerless)
* [`interpolate`](#interpolate)
* [Updated Color Functions](#updated-color-functions)
* [`change`](#change)
* [New Constructors](#new-constructors)
* [Lab Channel Constructor](#lab-channel-constructor)
* [LCH Channel Constructor](#lch-channel-constructor)
* [Predefined RGB Channel Constructor](#predefined-rgb-channel-constructor)
* [XYZ Channel Constructor](#xyz-channel-constructor)
* [Modified Legacy Color Constructors](#modified-legacy-color-constructors)
* [HSL Constructor](#hsl-constructor)
* [HWB Constructor](#hwb-constructor)
* [RGB Constructor](#rgb-constructor)
* [Deprecations](#deprecations)
* [Procedures](#procedures)
* [Parsing a Channel Value](#parsing-a-channel-value)
* [Changing a Component Value](#changing-a-component-value)
* [Determining Construction Space](#determining-construction-space)
* [Embedded Protocol](#embedded-protocol)
* [SassColor](#sasscolor)
* [Removed SassScript values](#removed-sassscript-values)
## API
```ts
import {Value} from '../spec/js-api/value';
```
## Types
### Color Space Definitions
```ts
export type ColorSpaceHSL = 'hsl';
export type ChannelNameHSL = 'hue' | 'saturation' | 'lightness';
export type ColorSpaceHWB = 'hwb';
export type ChannelNameHWB = 'hue' | 'whiteness' | 'blackness';
export type ColorSpaceLab = 'lab' | 'oklab';
export type ChannelNameLab = 'lightness' | 'a' | 'b';
export type ColorSpaceLCH = 'lch' | 'oklch';
export type ChannelNameLCH = 'lightness' | 'chroma' | 'hue';
export type ColorSpaceRGB =
| 'a98-rgb'
| 'display-p3'
| 'prophoto-rgb'
| 'rgb'
| 'srgb'
| 'srgb-linear';
export type ChannelNameRGB = 'red' | 'green' | 'blue';
export type ColorSpaceXYZ = 'xyz' | 'xyz-d50' | 'xyz-d65';
export type ChannelNameXYZ = 'x' | 'y' | 'z';
export type ChannelName =
| ChannelNameHSL
| ChannelNameHWB
| ChannelNameLab
| ChannelNameLCH
| ChannelNameRGB
| ChannelNameXYZ;
export type KnownColorSpace =
| ColorSpaceHSL
| ColorSpaceHWB
| ColorSpaceLab
| ColorSpaceLCH
| ColorSpaceRGB
| ColorSpaceXYZ;
export type PolarColorSpace = ColorSpaceHSL | ColorSpaceHWB | ColorSpaceLCH;
export type RectangularColorSpace = Exclude<KnownColorSpace, PolarColorSpace>;
export type HueInterpolationMethod =
| 'decreasing'
| 'increasing'
| 'longer'
| 'shorter';
```
### New Color Functions
```ts
export class SassColor extends Value {
```
#### `space`
Returns the name of [`internal`]'s space.
[`internal`]: ../spec/js-api/value/color.d.ts.md#internal
```ts
get space(): KnownColorSpace;
```
#### `toSpace`
* If `this.space` is equal to `space`, return `this`.
* Otherwise, return the result of [`color.to-space(internal, space)`].
```ts
toSpace(space: KnownColorSpace): SassColor;
```
[`color.to-space(internal, space)`]: ./color-4-new-spaces.md#colorto-space
#### `isLegacy`
Returns whether [`internal`] is in a [legacy color space] (`rgb`, `hsl`, or
`hwb`).
```ts
get isLegacy(): boolean;
```
[legacy color space]: ./color-4-new-spaces.md#legacy-color
#### `isInGamut`
Returns the result of [`color.is-in-gamut(internal, space)`] as a JavaScript
boolean.
```ts
isInGamut(space?: KnownColorSpace): boolean;
```
[`color.is-in-gamut(internal, space)`]: ./color-4-new-spaces.md#coloris-in-gamut
#### `toGamut`
Returns the result of [`color.to-gamut(internal, space)`].
```ts
toGamut(space?: KnownColorSpace): SassColor;
```
[`color.to-gamut(internal, space)`]: ./color-4-new-spaces.md#colorto-gamut-1
#### `channelsOrNull`
Returns an array of channel values (excluding alpha) for [`internal`], with
[missing channels][missing components] converted to `null`.
* Let `space` be the result of [`this.space`].
* Let `components` be the list of channels in `space`.
* Let `channels` be an empty array.
* For each `component` in `components`:
* Let `value` be the channel value in [`internal`] with name of `component`.
* If `value` is `none`, let `value` be `null`.
* Append `value` to `channels`.
* Return `channels`.
```ts
get channelsOrNull(): [number | null, number | null, number | null];
```
[missing components]: ./color-4-new-spaces.md#missing-components
[`this.space`]: #space
#### `channels`
This algorithm returns an array of channel values (excluding alpha) for
[`internal`], with [missing channels][missing components] converted to `0`.
* Let `channelsOrNull` be the result of [`this.channelsOrNull`].
* Let `channels` be an empty array.
* For each `channel` in `channelsOrNull`:
* If `channel` equals `null`, let `value` be 0.
* Append `value` to `channels`.
* Return `channels`.
[`this.channelsOrNull`]: #channelsornull
```ts
get channels(): [number, number, number];
```
#### `channel`
* Let `initialSpace` be the value of [`this.space()`].
* Let `space` be `options.space` if it is defined, and the value of
`initialSpace` otherwise.
* If `channel` is not "alpha" or a channel in `space`, throw an error.
* Let `color` be the result of [`this.toSpace(space)`].
* Let `value` be the channel value in `color` with name of `component`.
* If `value` is null, return 0.
* Otherwise, return `value`.
```ts
channel(channel: ChannelName): number;
channel(
channel: ChannelNameHSL | 'alpha',
options: {space: ColorSpaceHSL}
): number;
channel(
channel: ChannelNameHWB | 'alpha',
options: {space: ColorSpaceHWB}
): number;
channel(
channel: ChannelNameLab | 'alpha',
options: {space: ColorSpaceLab}
): number;
channel(
channel: ChannelNameLCH | 'alpha',
options: {space: ColorSpaceLCH}
): number;
channel(
channel: ChannelNameRGB | 'alpha',
options: {space: ColorSpaceRGB}
): number;
channel(
channel: ChannelNameXYZ | 'alpha',
options: {space: ColorSpaceXYZ}
): number;
```
#### `alpha`
Returns the result of calling [`this.channel('alpha')`].
[`this.channel('alpha')`]: #channel
```ts
get alpha(): number;
```
#### `isChannelMissing`
Returns the result of [`color.is-missing(internal,
channel)`][color.is-missing()] as a JavaScript boolean.
```ts
isChannelMissing(channel: ChannelName | 'alpha'): boolean;
```
[color.is-missing()]: ./color-4-new-spaces.md#coloris-missing-1
#### `isAlphaMissing`
Returns the result of [`color.is-missing(internal,
'alpha')`][color.is-missing()] as a JavaScript boolean.
```ts
get isAlphaMissing(): boolean;
```
#### `isChannelPowerless`
Returns the result of [`color.is-powerless(internal, channel, space)`] as a
JavaScript boolean.
[`color.is-powerless(internal, channel, space)`]: ./color-4-new-spaces.md#coloris-powerless-1
```ts
isChannelPowerless(channel: ChannelName): boolean;
isChannelPowerless(
channel: ChannelNameHSL,
options?: {space: ColorSpaceHSL}
): boolean;
isChannelPowerless(
channel: ChannelNameHWB,
options?: {space: ColorSpaceHWB}
): boolean;
isChannelPowerless(
channel: ChannelNameLab,
options?: {space: ColorSpaceLab}
): boolean;
isChannelPowerless(
channel: ChannelNameLCH,
options?: {space: ColorSpaceLCH}
): boolean;
isChannelPowerless(
channel: ChannelNameRGB,
options?: {space: ColorSpaceRGB}
): boolean;
isChannelPowerless(
channel: ChannelNameXYZ,
options?: {space: ColorSpaceXYZ}
): boolean;
```
#### `interpolate`
* Let `space` be the value of [`this.space()`].
* If `options.method` is set, let `interpolationMethod` be a space separated
list containing the value of `space`, a space, and the value of
`options.method`.
* Otherwise, if `space` is a rectangular color space, let `interpolationMethod`
be `space`.
* Otherwise, let `interpolationMethod` be a space separated list containing the
value of `space`, a space, and the string "shorter".
* Return the result of [`color.mix(internal, options.color2, options.weight, interpolationMethod)`][`color.mix()`].
```ts
interpolate(options: {color2: SassColor; weight?: number}): SassColor;
interpolate(options: {
color2: SassColor;
weight?: number;
method?: HueInterpolationMethod;
}): SassColor;
```
[`color.mix()`]: ./color-4-new-spaces.md#colormix-1
### Updated Color Functions
#### `change`
Replace the definition of [color.change] with the following:
[color.change]: ../spec/js-api/value/color.d.ts.md#change
This algorithm takes a JavaScript object `options` and returns a new SassColor
as the result of changing some of [`internal`]'s components.
> The `space` value defaults to the `space` of [`internal`], and the caller may
> specify any combination of channels and alpha in that space to be changed.
>
> If `space` is not a [legacy color space], a channel value of `null` will
> result in a [missing component][missing components] value for that channel.
* Let `initialSpace` be the value of [`this.space()`].
* Let `spaceSetExplicitly` be `true` if `options.space` is defined, and `false`
otherwise.
* Let `space` be `options.space` if `spaceSetExplicitly` is true, and the value
of `initialSpace` otherwise.
* If `initialSpace` is a [legacy color space] and `spaceSetExplicitly` is false:
* If `options.whiteness` or `options.blackness` is set, let `space` be `hwb`.
* Otherwise, if `options.hue`, `options.saturation`, or `options.lightness` is
set, let `space` be `hsl`.
* Otherwise, if `options.red`, `options.green`, or `options.blue` is set, let
`space` be `rgb`.
* If `initialSpace` is not equal to `space`, emit a deprecation warning named
`color-4-api`.
* Let `changes` be the object `options` without `space` and its value.
* Let `keys` be a list of the keys in `changes`.
* Let `components` be `"alpha"` and the names of the channels in `space`.
* If any key in `keys` is not the name of a channel in `components`, throw an
error.
* Let `color` be the result of [`this.toSpace(space)`].
* Let `changedValue` be a function that takes a string argument for `channel`
and calls the procedure [`Changing a Component Value`] with `changes` and
`this` as `initial`.
* If `space` equals `hsl` and `spaceSetExplicitly` is `false`:
* If any of `options.hue`, `options.saturation`, `options.lightness` or
`options.alpha` equals null, emit a deprecation warning named `null-alpha`.
* Let `changedColor` be the result of:
```js
new SassColor({
hue: options.hue ?? color.channel('hue'),
saturation: options.saturation ?? color.channel('saturation'),
lightness: options.lightness ?? color.channel('lightness'),
alpha: options.alpha ?? color.channel('alpha'),
space: space
})
```
* If `space` equals `hsl` and `spaceSetExplicitly` is `true`, let `changedColor`
be the result of:
```js
new SassColor({
hue: changedValue('hue'),
saturation: changedValue('saturation'),
lightness: changedValue('lightness'),
alpha: changedValue('alpha'),
space: space
})
```
* If `space` equals `hwb` and `spaceSetExplicitly` is `false`:
* If any of `options.hue`, `options.whiteness`, `options.blackness` or
`options.alpha` equals null, emit a deprecation warning named `null-alpha`.
* Let `changedColor` be the result of:
```js
new SassColor({
hue: options.hue ?? color.channel('hue'),
whiteness: options.whiteness ?? color.channel('whiteness'),
blackness: options.blackness ?? color.channel('blackness'),
alpha: options.alpha ?? color.channel('alpha'),
space: space
})
```
* If `space` equals `hwb` and `spaceSetExplicitly` is `true`, let `changedColor`
be the result of:
```js
new SassColor({
hue: changedValue('hue'),
whiteness: changedValue('whiteness'),
blackness: changedValue('blackness'),
alpha: changedValue('alpha'),
space: space
})
```
* If `space` equals `rgb` and `spaceSetExplicitly` is `false`:
* If any of `options.red`, `options.green`, `options.blue` or `options.alpha`
equals null, emit a deprecation warning named `null-alpha`.
* Let `changedColor` be the result of:
```js
new SassColor({
red: options.red ?? color.channel('red'),
green: options.green ?? color.channel('green'),
blue: options.blue ?? color.channel('blue'),
alpha: options.alpha ?? color.channel('alpha'),
space: space
})
```
* If `space` equals `rgb` and `spaceSetExplicitly` is `true`, let `changedColor`
be the result of:
```js
new SassColor({
red: changedValue('red'),
green: changedValue('green'),
blue: changedValue('blue'),
alpha: changedValue('alpha'),
space: space
})
```
* If `space` equals `lab` or `oklab`, let `changedColor` be the result of:
```js
new SassColor({
lightness: changedValue('lightness'),
a: changedValue('a'),
b: changedValue('b'),
alpha: changedValue('alpha'),
space: space
})
```
* If `space` equals `lch` or `oklch`, let `changedColor` be the result of:
```js
new SassColor({
lightness: changedValue('lightness'),
chroma: changedValue('chroma'),
hue: changedValue('hue'),
alpha: changedValue('alpha'),
space: space
})
```
* If `space` equals `a98-rgb`, `display-p3`, `prophoto-rgb`, `srgb`, or
`srgb-linear`, let `changedColor` be the result of:
```js
new SassColor({
red: changedValue('red'),
green: changedValue('green'),
blue: changedValue('blue'),
alpha: changedValue('alpha'),
space: space
})
```
* If `space` equals `xyz`, `xyz-d50`, or `xyz-d65`, let `changedColor` be the
result of:
```js
new SassColor({
y: changedValue('y'),
x: changedValue('x'),
z: changedValue('z'),
alpha: changedValue('alpha'),
space: space
})
```
* Return the result of [`changedColor.toSpace(initialSpace)`].
[`this.space()`]: #space
[`this.toSpace(space)`]: #tospace
[`changedColor.toSpace(initialSpace)`]: #tospace
[`Changing a Component Value`]: #changing-a-component-value
```ts
change(
options: {
[key in ChannelName]?: number | null;
} & {alpha?: number}
): SassColor;
change(
options: {
[key in ChannelNameHSL]?: number | null;
} & {
alpha?: number;
space: ColorSpaceHSL;
}
): SassColor;
change(
options: {
[key in ChannelNameHWB]?: number | null;
} & {
alpha?: number;
space: ColorSpaceHWB;
}
): SassColor;
change(
options: {
[key in ChannelNameLab]?: number | null;
} & {
alpha?: number | null;
space: ColorSpaceLab;
}
): SassColor;
change(
options: {
[key in ChannelNameLCH]?: number | null;
} & {
alpha?: number | null;
space: ColorSpaceLCH;
}
): SassColor;
change(
options: {
[key in ChannelNameRGB]?: number | null;
} & {
alpha?: number | null;
space: ColorSpaceRGB;
}
): SassColor;
change(
options: {
[key in ChannelNameXYZ]?: number | null;
} & {
alpha?: number | null;
space: ColorSpaceXYZ;
}
): SassColor;
```
### New Constructors
* Let `constructionSpace` be the result of [Determining Construction Space] with
the `options` object passed to the constructor.
* Use the constructor that matches `constructionSpace`.
[Determining Construction Space]: #determining-construction-space
#### Lab Channel Constructor
Create a new SassColor in a color space with Lab channels -- `lab` and `oklab`.
* Let `lightness` be the result of [parsing a channel value] with value
`options.lightness`.
* Let `a` be the result of [parsing a channel value] with value `options.a`.
* Let `b` be the result of [parsing a channel value] with value `options.b`.
* If `options.alpha` is not set, let `alpha` be `1`. Otherwise, let `alpha` be
the result of [parsing a channel value] with value `options.alpha`.
* If `options.space` equals `lab`, set [`internal`] to the result of
[`lab(lightness a b / alpha)`].
* Otherwise, if `options.space` equals `oklab`, set [`internal`] to the result
of [`oklab(lightness a b / alpha)`].
[`lab(lightness a b / alpha)`]: ./color-4-new-spaces.md#lab
[`oklab(lightness a b / alpha)`]: ./color-4-new-spaces.md#oklab
[parsing a channel value]: #parsing-a-channel-value
```ts
constructor(options: {
lightness: number | null;
a: number | null;
b: number | null;
alpha?: number | null;
space: ColorSpaceLab;
});
```
#### LCH Channel Constructor
Create a new SassColor in a color space with LCH channels -- `lch` and `oklch`.
* Let `lightness` be the result of [parsing a channel value] with value
`options.lightness`.
* Let `c` be the result of [parsing a channel value] with value `options.c`.
* Let `h` be the result of [parsing a channel value] with value `options.h`.
* If `options.alpha` is not set, let `alpha` be `1`. Otherwise, let `alpha` be
the result of [parsing a channel value] with value `options.alpha`.
* If `options.space` equals `lch`, set [`internal`] to the result of
[`lch(lightness a b / alpha)`].
* Otherwise, if `options.space` equals `oklch`, set [`internal`] to the result
of [`oklch(lightness a b / alpha)`].
[`lch(lightness a b / alpha)`]: ./color-4-new-spaces.md#lch
[`oklch(lightness a b / alpha)`]: ./color-4-new-spaces.md#oklch
```ts
constructor(options: {
lightness: number | null;
chroma: number | null;
hue: number | null;
alpha?: number | null;
space: ColorSpaceLCH;
});
```
#### Predefined RGB Channel Constructor
Create a new SassColor in a color space with RGB channels -- `srgb`,
`srgb-linear`, `display-p3`, `a98-rgb`, and `prophoto-rgb`. `rgb` is supported
through the modified [RGB Constructor].
* Let `red` be the result of [parsing a channel value] with value `options.red`.
* Let `green` be the result of [parsing a channel value] with value
`options.green`.
* Let `blue` be the result of [parsing a channel value] with value
`options.blue`.
* If `options.alpha` is not set, let `alpha` be `1`. Otherwise, let `alpha` be
the result of [parsing a channel value] with value `options.alpha`.
* Let `space` be the unquoted string value of `options.space`.
* Set [`internal`] to the result of [`color(space red green blue / alpha)`].
[`color(space red green blue / alpha)`]: ./color-4-new-spaces.md#color-1
[RGB Constructor]: #rgb-constructor
```ts
constructor(options: {
red: number | null;
green: number | null;
blue: number | null;
alpha?: number | null;
space: Exclude<ColorSpaceRGB, 'rgb'>;
});
```
#### XYZ Channel Constructor
Create a new SassColor in a color space with XYZ channels -- `xyz`, `xyz-d50`,
and `xyz-d65`.
* Let `x` be the result of [parsing a channel value] with value `options.x`.
* Let `y` be the result of [parsing a channel value] with value `options.y`.
* Let `z` be the result of [parsing a channel value] with value `options.z`.
* If `options.alpha` is not set, let `alpha` be `1`. Otherwise, let `alpha` be
the result of [parsing a channel value] with value `options.alpha`.
* Let `space` be the unquoted string value of `options.space`.
* Set [`internal`] to the result of [`color(space x y z / alpha)`].
[`color(space x y z / alpha)`]: ./color-4-new-spaces.md#color-1
```ts
constructor(options: {
x: number | null;
y: number | null;
z: number | null;
alpha?: number | null;
space: ColorSpaceXYZ;
});
```
### Modified Legacy Color Constructors
These will replace the [existing constructors] for legacy colors.
[existing constructors]: ../spec/js-api/value/color.d.ts.md#constructor
#### HSL Constructor
Create a new SassColor in the `hsl` color space.
* If `options.alpha` is `null` and `options.space` is not set, emit a
deprecation warning named `null-alpha`.
* Let `hue` be the result of [parsing a channel value] with value `options.hue`.
* Let `saturation` be the result of [parsing a channel value] with value
`options.saturation`.
* Let `lightness` be the result of [parsing a channel value] with value
`options.lightness`.
* If `options.alpha` is not set, let `alpha` be `1`. Otherwise, let `alpha` be
the result of [parsing a channel value] with value `options.alpha`.
* Set [`internal`] to the result of [`hsl(hue saturation lightness / alpha)`].
[`hsl(hue saturation lightness / alpha)`]: ../spec/functions.md#hsl-and-hsla
```ts
constructor(options: {
hue: number | null;
saturation: number | null;
lightness: number | null;
alpha?: number | null;
space?: ColorSpaceHSL;
});
```
#### HWB Constructor
Create a new SassColor in the `hwb` color space.
* If `options.alpha` is `null` and `options.space` is not set, emit a
deprecation warning named `null-alpha`.
* Let `hue` be the result of [parsing a channel value] with value `options.hue`.
* Let `whiteness` be the result of [parsing a channel value] with value
`options.whiteness`.
* Let `blackness` be the result of [parsing a channel value] with value
`options.blackness`.
* If `options.alpha` is not set, let `alpha` be `1`. Otherwise, let `alpha` be
the result of [parsing a channel value] with value `options.alpha`.
* Set [`internal`] to the result of [`hwb(hue whiteness blackness / alpha)`].
[`hwb(hue whiteness blackness / alpha)`]: ./color-4-new-spaces.md#hwb-1
```ts
constructor(options: {
hue: number | null;
whiteness: number | null;
blackness: number | null;
alpha?: number | null;
space?: ColorSpaceHWB;
});
```
#### RGB Constructor
Create a new SassColor in the `rgb` color space.
* If `options.alpha` is `null` and `options.space` is not set, emit a
deprecation warning named `null-alpha`.
* Let `red` be the result of [parsing a channel value] with value `options.red`.
* Let `green` be the result of [parsing a channel value] with value
`options.green`.
* Let `blue` be the result of [parsing a channel value] with value
`options.blue`.
* If `options.alpha` is not set, let `alpha` be `1`. Otherwise, let `alpha` be
the result of [parsing a channel value] with value `options.alpha`.
* Return the result of [`rgb(red green blue / alpha)`].
[`rgb(red green blue / alpha)`]: ./color-4-new-spaces.md#rgb-and-rgba
```ts
constructor(options: {
red: number | null;
green: number | null;
blue: number | null;
alpha?: number | null;
space?: 'rgb';
});
```
```ts
}
```
### Deprecations
A number of SassColor getters only make sense for [legacy color space], and so
are being deprecated in favor of the new [`channel`] function. This deprecation
is called `color-4-api`.
[`channel`]: #channel
* `red`
* `green`
* `blue`
* `hue`
* `saturation`
* `lightness`
* `whiteness`
* `blackness`
## Procedures
### Parsing a Channel Value
This procedure takes a channel value `value`, and returns the special value
`none` if the value is `null`.
* If `value` is a number, return a Sass number with a value of `value`.
* If `value` is the Javascript value `null`, return the unquoted Sass string
`none`.
### Changing a Component Value
This procedure takes a `channel` name, an object `changes` and a SassColor
`initial` and returns the result of applying the change for `channel` to
`initial`.
* Let `initialValue` be the channel value in `initial` with name of `channel`.
* If `channel` is not a key in `changes`, return `initialValue`.
* Otherwise, return the value for `channel` in `changes`.
### Determining Construction Space
This procedure takes an object `options` with unknown keys and returns a color
space for construction.
* If `options.space` is set, return `options.space`.
* If `options.red` is set, return "rgb".
* If `options.saturation` is set, return "hsl".
* If `options.whiteness` is set, return "hwb".
* Otherwise, throw an error.
## Embedded Protocol
This introduces a breaking change in the Embedded Protocol, as it removes the
legacy SassScript values.
### SassColor
```proto
message SassColor {
// The name of a known color space.
string space = 1;
// The value of the first channel associated with `space`.
double channel1 = 2;
// The value of the second channel associated with `space`.
double channel2 = 3;
// The value of the third channel associated with `space`.
double channel3 = 4;
// The color's alpha channel. Mandatory. Must be between 0 and 1,
// inclusive.
double alpha = 5;
}
```
### Removed SassScript values
The `RgbColor`, `HslColor` and `HwbColor` SassScript values will be removed from
the Embedded Protocol.

View File

@ -1,3 +1,27 @@
## Draft 1.11
* Add support for the relative color syntax in the algorithm parsing color
arguments, for CSS compatibility.
## Draft 1.10
* Properly scale `%` return values for `color.channel()`.
* Clean up some language related to percent-conversion.
## Draft 1.9
* Explicitly define associated units for color space channels.
## Draft 1.8
* Require a quoted string for `color.is-missing()` for consistency with other
color functions and ease of use with channels whose names overlap with colors.
## Draft 1.7
* Resolve missing `alpha` channels *after* premultiplying colors.
## Draft 1.6
* Clarify in the known color space definitions that lightness channels are
@ -84,7 +108,7 @@
function syntax is already explicit about which parameter is where.
* `color.invert()` throws an error when `$weight` would require mixing in an
invalid `color.mix()` _interpolation color space_.
invalid `color.mix()` *interpolation color space*.
* Allow scaling channels with a non-0 minimum value, such as the `a` and `b`
channels in `lab()`/`oklab()`.

View File

@ -1,4 +1,4 @@
# CSS Color Level 4, New Color Spaces: Draft 1.6
# CSS Color Level 4, New Color Spaces: Draft 1.11
*([Issue](https://github.com/sass/sass/issues/2831))*
@ -117,7 +117,7 @@ wider RGB gamuts.
* A *color model* is a mathematical approach to representing colors and their
relationships. Historically, RGB has been the dominant color model for both
computer monitors and web browsers. Lately, CIELab and OKLab models have
computer monitors and web browsers. Lately, CIELab and Oklab models have
shown significant benefits by providing a more *perceptually uniform*
distribution of colors, so that similar mathematical adjustments achieve
visually similar results.
@ -128,7 +128,7 @@ wider RGB gamuts.
project the RGB color model into cubic coordinate systems, while `hsl()`
projects the same color model into a cylindrical (polar-angle) space.
Similarly, `oklab()` and `oklch()` provide different coordinate projections
of the OKLab model.
of the Oklab model.
* A *color gamut* is the full range of colors that can be described in a color
space. Historically, all CSS syntaxes have been limited to the sRGB gamut.
@ -235,8 +235,8 @@ such, `oklch` is often the best space for consistent transforms.
#### `lab()` and `lch()`
The `lab()` and `lch()` functions provide access to an unbounded gamut of colors
in a space that's less perpetually-uniform but more widely-adopted than OKLab
and OKLCH.
in a space that's less perpetually-uniform but more widely-adopted than Oklab
and Oklch.
#### `hwb()`
@ -530,7 +530,7 @@ Legacy colors that have [missing] components are
### Color Equality
For determining _equality_ between two colors:
For determining *equality* between two colors:
* If both colors are [legacy colors](#legacy-color):
@ -552,13 +552,13 @@ Each channel has a name, and an associated unit where allowed. Space and
channel names match unquoted strings, ignoring case. They are always emitted as
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
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_.
*scalable*.
Some color spaces use a _polar angle_ value for the `hue` channel. Polar-angle
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>`
dimension or number (interpreted as a `deg` value), and are serialized with
`deg` units.
@ -577,19 +577,24 @@ The known color spaces and their channels are:
* `hwb` (RGB, legacy):
* `hue`:
* associated unit: `deg`
* degrees: polar angle
* `whiteness`, `blackness`:
* associated unit: `%`
* gamut: bounded
* percentage: `[0%,100%]`
* `hsl` (RGB, legacy):
* `hue`:
* associated unit: `deg`
* degrees: polar angle
* `saturation`:
* gamut: bounded
* associated unit: `%`
* percentage: `[0%,100%]`
* `lightness`:
* gamut: bounded, clamped
* associated unit: `%`
* percentage: `[0%,100%]`
* `srgb`, `srgb-linear`, `display-p3`, `a98-rgb`, `prophoto-rgb`,
@ -610,6 +615,7 @@ The known color spaces and their channels are:
* `lab`:
* `lightness`:
* gamut: un-bounded, clamped
* associated unit: `%`
* number: `[0,100]`
> Percentages `[0%,100%]` map to the `[0,100]` range.
@ -623,6 +629,7 @@ The known color spaces and their channels are:
* `lch`:
* `lightness`:
* gamut: un-bounded, clamped
* associated unit: `%`
* number: `[0,100]`
> Percentages `[0%,100%]` map to the `[0,100]` range.
@ -634,11 +641,13 @@ The known color spaces and their channels are:
> Percentages `[0%,100%]` map to the `[0,150]` range.
* `hue`:
* associated unit: `deg`
* degrees: polar angle
* `oklab`:
* `lightness`:
* gamut: un-bounded, clamped
* associated unit: `%`
* number: `[0,1]`
> Percentages `[0%,100%]` map to the `[0,1]` range.
@ -652,6 +661,7 @@ The known color spaces and their channels are:
* `oklch`:
* `lightness`:
* gamut: un-bounded, clamped
* associated unit: `%`
* number: `[0,1]`
> Percentages `[0%,100%]` map to the `[0,1]` range.
@ -663,13 +673,14 @@ The known color spaces and their channels are:
> Percentages `[0%,100%]` map to the `[0,0.4]` range.
* `hue`:
* associated unit: `deg`
* degrees: polar angle
### Predefined Color Spaces
> 'Predefined color spaces' can be described using the `color()` function.
The _predefined RGB spaces_ are:
The *predefined RGB spaces* are:
* `srgb`
* `srgb-linear`
@ -678,7 +689,7 @@ The _predefined RGB spaces_ are:
* `prophoto-rgb`
* `rec2020`
The _predefined XYZ spaces_ are:
The *predefined XYZ spaces* are:
* `xyz`
* `xyz-d50`
@ -693,7 +704,7 @@ value of that same component in the other color. In all other cases, the
missing value is treated as `0`.
For the sake of [interpolating] between colors with missing components, the
following _analogous components_ are defined by [CSS Color Level 4][color-4]:
following *analogous components* are defined by [CSS Color Level 4][color-4]:
| Category | Components |
| ------------- | ------------------- |
@ -737,7 +748,7 @@ in certain circumstances.
### Color Interpolation Method
A _color interpolation method_ is a space-separated list of unquoted strings,
A *color interpolation method* is a space-separated list of unquoted strings,
parsed according to the following syntax definition:
<x><pre>
@ -751,9 +762,9 @@ parsed according to the following syntax definition:
&#32; ) 'hue'
</pre></x>
A valid _PolarColorSpace_ is the name of a [known color space] with a polar
angle hue channel. A _RectangularColorSpace_ is the name of any other
[known color space], without a polar-angle hue. The _interpolation color space_
A valid *PolarColorSpace* is the name of a [known color space] with a polar
angle hue channel. A *RectangularColorSpace* is the name of any other
[known color space], without a polar-angle hue. The *interpolation color space*
is the result of [looking up a known color space] named by either the
`PolarColorSpace` or `RectangularColorSpace` productions.
@ -891,15 +902,15 @@ The individual conversion algorithms are:
* [sRGB to HWB](https://www.w3.org/TR/css-color-4/#rgb-to-hwb)
* [Lab to LCH, OKLab to OKLCH](https://www.w3.org/TR/css-color-4/#lab-to-lch)
* [Lab to LCH, Oklab to Oklch](https://www.w3.org/TR/css-color-4/#lab-to-lch)
* [LCH to Lab, OKLCH to OKLab](https://www.w3.org/TR/css-color-4/#lch-to-lab)
* [LCH to Lab, Oklch to Oklab](https://www.w3.org/TR/css-color-4/#lch-to-lab)
* [Between predefined RGB spaces](https://www.w3.org/TR/css-color-4/#predefined-to-predefined)
* [Any RGB to Lab/OKLab](https://www.w3.org/TR/css-color-4/#predefined-to-lab-oklab)
* [Any RGB to Lab/Oklab](https://www.w3.org/TR/css-color-4/#predefined-to-lab-oklab)
* [Lab/OKLab to any RGB](https://www.w3.org/TR/css-color-4/#oklab-lab-to-predefined)
* [Lab/Oklab to any RGB](https://www.w3.org/TR/css-color-4/#oklab-lab-to-predefined)
> For additional details, see the [Sample code for color conversions][convert].
@ -950,131 +961,135 @@ three values: a color space, a list of channel values, and an alpha value.
The procedure is:
* If `input` is a [special variable string], return an unquoted string with
the value of `input`.
* If `input` is a [special variable string], return an unquoted string with
the value of `input`.
* If `input` is a bracketed list, or a list with a separator other than
'slash' or 'space', throw an error.
* If `input` is a bracketed list, or a list with a separator other than
'slash' or 'space', throw an error.
* If `input` is a slash-separated list:
* If `input` is a slash-separated list:
* If `input` doesn't have exactly two elements, throw an error.
* If `input` doesn't have exactly two elements, throw an error.
* Otherwise, let `components` be the first element and `alpha` the second
element of `input`.
* Otherwise, let `components` be the first element and `alpha` the second
element of `input`.
* Otherwise:
* Otherwise:
* Let `components` be an unbracketed space separated list of all except the
last element of `input`.
* Let `components` be an unbracketed space separated list of all except the
last element of `input`.
* If the last element of `input` is an unquoted string that contains `/`:
* If the last element of `input` is an unquoted string that contains `/`:
* Let `split-last` be the result calling `string.split()` with the last
element of `input` as the string to split, and `/` as the separator.
* Let `split-last` be the result calling `string.split()` with the last
element of `input` as the string to split, and `/` as the separator.
* If `split-last` has two items, and one or both items are an unquoted
string that's case-insensitively equal to 'none':
* If `split-last` has two items, and one or both items are an unquoted
string that's case-insensitively equal to 'none':
> Special handling for `none/none`, `none/<number>`, and `<number>/none`.
> Special handling for `none/none`, `none/<number>`, and `<number>/none`.
* If either item in `split-last` can be coerced to a number, replace
the current value of the item with the resulting number value.
* If either item in `split-last` can be coerced to a number, replace
the current value of the item with the resulting number value.
* If any item in `split-last` is not a number or an unquoted string
that's case-insensitively equal to 'none', return an unquoted string
with the value of `input`.
* If any item in `split-last` is not a number or an unquoted string
that's case-insensitively equal to 'none', return an unquoted string
with the value of `input`.
* Otherwise, let `alpha` be the second element in `split-last`, and
append the first element of `split-last` to `components`.
* Otherwise, let `alpha` be the second element in `split-last`, and
append the first element of `split-last` to `components`.
* Otherwise, return an unquoted string with the value of `input`.
* Otherwise, return an unquoted string with the value of `input`.
> This solves for a legacy handling of `/` in Sass that would produce an
> unquoted string when the alpha value is a CSS function such as `var()`
> or when either value is `none`.
* Otherwise, if the last element of `input` has preserved its status as two
slash-separated numbers:
* Otherwise, if the last element of `input` has preserved its status as two
slash-separated numbers:
* Let `alpha` be the number after the slash, and append the number before
the slash to `components`.
* Let `alpha` be the number after the slash, and append the number before
the slash to `components`.
* Otherwise, append the last element of `input` to `components`.
* Otherwise, append the last element of `input` to `components`.
* If `components` is an empty list, throw an error.
* If `components` is an empty list, throw an error.
* If `components` is a [special variable string]:
* If `components` is a [special variable string]:
* Let `channels` be the value of `components`.
* Let `channels` be the value of `components`.
* Otherwise:
* Otherwise:
* If `components` is not an unbracketed space-separated list, throw an error.
* If `components` is not an unbracketed space-separated list, throw an error.
* If `space` is null:
* If the first element of `components` is an unquoted string which is
case-insensitively equal to `from`, return an unquoted string with the
value of `input`.
* Let `input-space` be the first element in `components`.
* If `space` is null:
* If `input-space` is a [special variable string], return an unquoted
string with the value of `input`.
* Let `input-space` be the first element in `components`.
* Set `space` be the result of [looking up a known color space] with the
name `input-space`.
* If `input-space` is a [special variable string], return an unquoted
string with the value of `input`.
* If `space` is not a [predefined color space], throw an error.
* Set `space` be the result of [looking up a known color space] with the
name `input-space`.
> Only predefined spaces can be passed in as color syntax components.
> All other known color spaces use explicit functions.
* If `space` is not a [predefined color space], throw an error.
* Let `channels` be an unbracketed space-separated list with the
remaining elements from `components`.
> Only predefined spaces can be passed in as color syntax components.
> All other known color spaces use explicit functions.
* Otherwise, let `channels` be the value of `components`.
* Let `channels` be an unbracketed space-separated list with the
remaining elements from `components`.
* Let `expected` be the number of channels in `space`.
* Otherwise, let `channels` be the value of `components`.
* If any element of `channels` is not either a number, a special variable
string, a [special number], or an unquoted string that's
case-insensitively equal to 'none', throw an error.
* Let `expected` be the number of channels in `space`.
* If `alpha` is null, let `alpha` be `1`.
* If any element of `channels` is not either a number, a special variable
string, a [special number], or an unquoted string that's
case-insensitively equal to 'none', throw an error.
* Otherwise, If `alpha` is not a [special number]:
* If `alpha` is null, let `alpha` be `1`.
* If `alpha` is a number, set `alpha` to the result of
[percent-converting] `alpha` with a max of 1, and then clamping the value
between 0 and 1, inclusive.
* Otherwise, If `alpha` is not a [special number]:
* Otherwise, throw an error.
* If `alpha` is a number, set `alpha` to the result of
[percent-converting] `alpha` with a max of 1, and then clamping the value
between 0 and 1, inclusive.
* If `channels` is a [special variable string], or if `alpha` is a [special
number], return an unquoted string with the value of `input`.
* Otherwise, throw an error.
* If any element of `channels` is a [special number]:
* If `channels` is a [special variable string], or if `alpha` is a [special
number], return an unquoted string with the value of `input`.
* If `space` is a [legacy color] space:
* If any element of `channels` is a [special number]:
* Let `comma-list` be the result of calling
`list.append(channels, alpha, 'comma')`.
* If `space` is a [legacy color] space:
* Return an unquoted string with the value of `comma-list`.
* Let `comma-list` be the result of calling
`list.append(channels, alpha, 'comma')`.
* Otherwise, return an unquoted string with the value of `input`.
* Return an unquoted string with the value of `comma-list`.
* Otherwise, return an unquoted string with the value of `input`.
> Doing this late in the process allows us to throw any obvious syntax
> errors, even for colors that can't be fully resolved during compilation.
* If the length of `channels` is not equal to `expected`, throw an error.
* If the length of `channels` is not equal to `expected`, throw an error.
> Once special values have been handled, any colors remaining should have
> exactly the expected number of channels.
* Set `channels` to the result of [normalizing] `channels` in `space`.
* Set `channels` to the result of [normalizing] `channels` in `space`.
* Let `space-name` be a lowercase unquoted string of the `space` name.
* Let `space-name` be a lowercase unquoted string of the `space` name.
* Return `space-name`, `channels` channels, and `alpha` alpha value.
* Return `space-name`, `channels` channels, and `alpha` alpha value.
[special variable string]: ../spec/functions.md#special-variable-string
[special number]: ../spec/functions.md#special-number
@ -1265,29 +1280,35 @@ input colors.
* If `weight == 1`, return `color1`.
* Let `space` be the _interpolation color space_ specified by the `method`
* Let `space` be the *interpolation color space* specified by the `method`
[color interpolation method].
> Only known color spaces are allowed as part of a color interpolation method.
* If `space` is a [PolarColorSpace][color-method]:
* Let `hue-arc` be the `HueInterpolationMethod` specified in `method`, or
`shorter` if no hue interpolation is specified.
* Let `hue-arc` be the `HueInterpolationMethod` specified in `method`, or
`shorter` if no hue interpolation is specified.
* Set `color1` and `color2` respectively to the results of [converting] `color1`
and `color2` into `space`.
* For each `color` in `color1` and `color2`:
* If any `component` of `color` is `none`, set that `component` to the value
of the corresponding component in the other color.
* If any non-`alpha` `component` of `color` is `none`, set that `component` to
the value of the corresponding component in the other color.
> If both values are `none`, the interpolation result for that component
> will also be `none`.
* Set `color` to the result of [premultiplying] `color`.
* If `color`'s `alpha` component is `none`, set it to the value of the `alpha`
component in the other color.
> This is resolved after premultiplying, because premultiplying has special
> handling for a missing `alpha` component.
* Let `mix` be a new color in the color space `space`, with `none` for all
channel and alpha values.
@ -1554,18 +1575,21 @@ channel($color, $channel, $space: null)
* Let `color` be `$color` if `$space` is null, and the result of calling
`color.to-space($color, $space)` otherwise.
* If `channel` is not the name of a channel in `color`, throw an error.
* Let `channel` be the channel in `color`'s space named `$channel`. Throw an
error if no such channel exists.
* Let `value` be the channel value in `color` with name of `channel`.
* Let `value` be `channel`'s value in `color`, or `0` if the channel's value
is missing.
* Let `unit` be the unit associated with `channel` in `color`'s space, if
defined, and `null` otherwise.
* If `value` is `null`, return `0`.
* If `unit` is `%`, return `value * 100` divided by the maximum of
`channel`'s gamut range with unit `%`.
* If `unit` is not null, return the result of appending `unit` units to `value`.
* Otherwise, if `unit` is not null, return `value` with unit `unit`.
* Return `value`.
* Otherwise, return `value` as a unitless number.
### `color.is-missing()`
@ -1575,7 +1599,7 @@ is-missing($color, $channel)
* If `$color` is not a color, throw an error.
* If `$channel` is not an unquoted string, throw an error.
* If `$channel` is not a quoted string, throw an error.
* If `$channel == alpha` (ignoring case), let `value` be the alpha value of
`$color`.
@ -1677,11 +1701,11 @@ This function is also available as a global function named `change-color()`.
* If the keyword argument `$space` is specified in `$args`:
* Let `known-space` be the result [looking up a known color space] named
`$space`.
* Let `known-space` be the result [looking up a known color space] named
`$space`.
* If `space != origin-space`, set `color` to the result of calling
`color.to-space(color, space)`.
* If `space != origin-space`, set `color` to the result of calling
`color.to-space(color, space)`.
* Otherwise, let `known-space` be `origin-space`.
@ -1689,8 +1713,8 @@ This function is also available as a global function named `change-color()`.
* If the keyword argument `$alpha` is specified in `$args`:
* Set `alpha` to the result of [percent-converting] `$alpha`, and clamping
it between 0 and 1 (inclusive).
* Set `alpha` to the result of [percent-converting] `$alpha` with a `max` of
1, and clamping it between 0 and 1 (inclusive).
* Let `channel-args` be the remaining keyword arguments in `$args`, not
including `$space` or `$alpha` arguments.
@ -1754,11 +1778,11 @@ This function is also available as a global function named `adjust-color()`.
* If the keyword argument `$space` is specified in `$args`:
* Let `known-space` be the result [looking up a known color space] named
`$space`.
* Let `known-space` be the result [looking up a known color space] named
`$space`.
* If `space != origin-space`, set `color` to the result of calling
`color.to-space(color, space)`.
* If `space != origin-space`, set `color` to the result of calling
`color.to-space(color, space)`.
* Otherwise, let `known-space` be `origin-space`.
@ -1766,13 +1790,13 @@ This function is also available as a global function named `adjust-color()`.
* If the keyword argument `$alpha` is specified in `$args`:
* If `alpha == none`, throw an error.
* If `alpha == none`, throw an error.
> This is not the ideal solution for handling `none`, but we want to
> match CSS relative color syntax if possible. Throwing an error for now
> means we can adjust to match the CSS behavior once it is defined.
> This is not the ideal solution for handling `none`, but we want to
> match CSS relative color syntax if possible. Throwing an error for now
> means we can adjust to match the CSS behavior once it is defined.
* Let `new-alpha` be the result of [percent-converting] `$alpha` with a max
* Let `new-alpha` be the result of [percent-converting] `$alpha` with a `max`
of 1.
* Set `alpha` to the value of `new-alpha + alpha` clamped between 0 and 1.
@ -1822,8 +1846,8 @@ This function is also available as a global function named `adjust-color()`.
`%` units to `channel`.
* Otherwise, if `valid` allows percentage mapping, set `adjust` to the
result of [percent-converting] `adjust` with a `min` and `max` defined
by the `valid` channel range.
result of [percent-converting] `adjust` with a `max` given by the maximum
of `valid`'s gamut range.
* Otherwise, throw an error.
@ -1855,10 +1879,10 @@ This function is also available as a global function named `scale-color()`.
* If the keyword argument `$space` is specified in `$args`:
* Let `space` be the result of [looking up a known color space] named
`$space`.
* Let `space` be the result of [looking up a known color space] named
`$space`.
* Let `color` be the result of [converting] `$color` to `space`.
* Let `color` be the result of [converting] `$color` to `space`.
* Otherwise:
@ -1870,13 +1894,13 @@ This function is also available as a global function named `scale-color()`.
* If the keyword argument `$alpha` is specified in `$args`:
* If `alpha == none`, throw an error.
* If `alpha == none`, throw an error.
> This is not the ideal solution for handling `none`, but we want to
> match CSS relative color syntax if possible. Throwing an error for now
> means we can adjust to match the CSS behavior once it is defined.
> This is not the ideal solution for handling `none`, but we want to
> match CSS relative color syntax if possible. Throwing an error for now
> means we can adjust to match the CSS behavior once it is defined.
* Set `alpha` to the result of [scaling] `alpha` by `$alpha` with `max` 1.
* Set `alpha` to the result of [scaling] `alpha` by `$alpha` with `max` 1.
* Let `channel-args` be the remaining keyword arguments in `$args`, not
including `$space` or `$alpha` arguments.
@ -1997,8 +2021,8 @@ This function is also available as a global function named `invert()`.
* If `$weight == 0%`, return the value of `$color`.
* If `space` is not a valid [color interpolation method] _interpolation color
space_, and `$weight != 100%`, throw an error.
* If `space` is not a valid [color interpolation method] *interpolation color
space*, and `$weight != 100%`, throw an error.
* Let `color` be the result of [converting] `$color` into `space`.
@ -2093,7 +2117,6 @@ ie-hex-str($color)
* Return the result of concatenating `hex-list` into a string.
## New Global Functions
These new CSS functions are provided globally.

View File

@ -0,0 +1,13 @@
## 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,559 @@
# Package Importer
*([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.
## Table of Contents
* [Background](#background)
* [Summary](#summary)
* [Node built-in importer](#node-built-in-importer)
* [Design Decisions](#design-decisions)
* [Using a `pkg:` URL scheme](#using-a-pkg-url-scheme)
* [No built-in `pkg:` resolver for browsers](#no-built-in-pkg-resolver-for-browsers)
* [Available as an opt-in importer](#available-as-an-opt-in-importer)
* [Available in legacy API](#available-in-legacy-api)
* [Node Resolution Decisions](#node-resolution-decisions)
* [Types](#types)
* [`nodePackageImporter`](#nodepackageimporter)
* [Updated `importers` option](#updated-importers-option)
* [Legacy API `pkgImporter`](#legacy-api-pkgimporter)
* [Semantics](#semantics)
* [Package Importers](#package-importers)
* [Node Package Importer](#node-package-importer)
* [Procedures](#procedures)
* [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)
* [Embedded Protocol](#embedded-protocol)
* [Ecosystem Notes](#ecosystem-notes)
## Background
> This section is non-normative.
Historically, Sass has not specified a standard method for using packages from
dependencies. A number of domain-specific solutions exist using custom importers
or by specifying a load path. This can lead to Sass code being written in a way
that is tied to a specific domain and make it difficult to rely on dependencies.
## Summary
> This section is non-normative.
Sass users often need to use styles from a dependency to customize an existing
theme or access styling utilities.
This proposal defines a `pkg:` URL scheme for usage with `@use` that directs an
implementation to resolve a URL within a dependency. Sass interfaces may provide
one or more implementations that will resolve the dependency URL using the
resolution standards and conventions for that environment. Once resolved, this
URL will be loaded in the same way as any other `file:` URL.
This proposal also defines a built-in Node importer.
For example, `@use "pkg:bootstrap";` would resolve to the path of a
library-defined export within the `bootstrap` dependency. In Node, that could be
resolved within `node_modules`, using the [Node resolution algorithm].
[node resolution algorithm]: https://nodejs.org/api/packages.html
### Node built-in importer
The built-in Node importer resolves in the following order:
1. `sass`, `style`, or `default` condition in package.json `exports`.
2. If there is not a subpath, then find the root export:
1. `sass` key at package.json root.
2. `style` key at package.json root.
3. `index` file at package root, resolved for file extensions and partials.
3. If there is a subpath, resolve that path relative to the package root, and
resolve for file extensions and partials.
For library creators, the recommended method is to add a `sass` conditional
export to `package.json`. The `style` condition is an acceptable alternative,
but relying on the `default` condition is discouraged. Notably, the key order
matters, and the importer will resolve to the first value with a key that is
`sass`, `style`, or `default`.
```json
{
"exports": {
".": {
"sass": "./dist/scss/index.scss",
"import": "./dist/js/index.mjs",
"default": "./dist/js/index.js"
}
}
}
```
Then, library consumers can use the `pkg:` syntax to get the default export.
```scss
@use 'pkg:library';
```
To better understand and allow for testing against the recommended algorithm, a
[Sass pkg: test] repository has been made with a rudimentary implementation of
the algorithm.
[sass pkg: test]: https://github.com/oddbird/sass-pkg-test
### Design Decisions
#### Using a `pkg:` URL scheme
We could use the `~` popularized by Webpack's `load-sass` format, but this has
been deprecated since 2021. In addition, since this creates a URL that is
syntactically a relative URL, it does not make it clear to the implementation or
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.
#### No built-in `pkg:` resolver for browsers
Dart Sass will not provide a built-in resolver for browsers to use the `pkg:`
scheme. To support a similar functionality, a user would need to ensure that
files are served, and the loader would need to fetch the URL. In order to follow
the same algorithm for [resolving a file: URL], we would need to make many
fetches. If we instead require the browser version to have a fully resolved URL,
we negate many of this spec's benefits. Users may write their own custom
importers to fit their needs.
[resolving a file: url]: ../spec/modules.md#resolving-a-file-url
#### Available as an opt-in importer
The `pkg:` import loader will be exposed as an opt-in importer as it adds the
potential for unexpected file system interaction to `compileString` and
`compileStringAsync`. Specifically, we want people who invoke Sass compilation
functions to have control over what files get accessed, and there's even a risk
of leaking file contents in error messages.
For the modern API, it will be exported from Sass as a constant value that can
be added to the list of `importers`. This allows for multiple Package Importer
types with user-defined order.
#### Available in legacy API
The built-in Node Package Importer will be added to the legacy API in order to
reduce the barrier to adoption. While the legacy API is deprecated, we
anticipate the implementation to be straightforward.
#### Node Resolution Decisions
The current recommendation for resolving packages in Node is to add
`node_modules` to the load paths. We could add `node_modules` to the load paths
by default, but that lacks clarity to the implementation and the reader. In
addition, a file may have access to multiple `node_modules` directories, and
different files may have access to different `node_modules` directories in the
same compilation.
There are a variety of methods currently in use for specifying a location of the
default Sass export for npm packages. For the most part, packages contain both
JavaScript and styles, and use the `main` or `module` root keys to define the
JavaScript entry point. Some packages use the `"sass"` key at the root of their
`package.json`.
Other packages have adopted [conditional exports], driven by build tools like
[Vite], [Parcel] and [Sass Loader for Webpack] which all resolve Sass paths
using the `"sass"` and the `"style"` custom conditions.
[conditional exports]: https://nodejs.org/api/packages.html#conditional-exports
[Vite]: https://github.com/vitejs/vite/pull/7817
[Parcel]: https://github.com/parcel-bundler/parcel/blob/2d2400ded4615375ee6bd53ef77b4857ad1591dd/packages/transformers/sass/src/SassTransformer.js#L163
[Sass Loader for Webpack]: https://github.com/webpack-contrib/sass-loader/blob/02df41203adfda96959e56abb43bd35a89ec11ba/src/utils.js#L514
Because use of conditional exports is flexible and recommended for modern
packages, this will be the primary method used for the Node package importer. We
will support both the `"sass"` and the `"style"` conditions, as Sass can also
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.
```json
{
"exports": {
"_variables.scss": {
"sass": "./src/sass/_variables.scss"
}
}
}
```
Node supports two module resolution algorithms: CommonJS and ECMAScript. While
these are very similar in most cases, there are corner cases that resolve in
different ways. The Node package importer will be implemented based on the
ECMAScript algorithm. This means that the Node package importer will not support
loading from `NODE_PATH` or `GLOBAL_FOLDERS`, as that is only supported in
CommonJS resolution. The Node documentation for [ECMAScript modules] recommends
using symlinks if this behavior is desired.
[ECMAScript modules]: https://nodejs.org/api/esm.html#no-node_path
## Types
```ts
import {FileImporter, Importer} from '../spec/js-api/importer';
```
### `nodePackageImporter`
```ts
type NodePackageImporter = {
_NodePackageImporterBrand: any;
};
export declare const nodePackageImporter: NodePackageImporter;
```
### Updated `importers` option
> On implementation, the option key will continue to be `importers`, and this
> type definition will replace the existing type definition for `importers`.
> Here, we are only specifying it as `importers_new_` to allow for declaration
> merging within the spec.
```ts
declare module '../spec/js-api/options' {
interface Options<sync extends 'sync' | 'async'> {
importers_new_?: (
| Importer<sync>
| FileImporter<sync>
| NodePackageImporter
)[];
}
}
```
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 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`.
* Replace `nodePackageImporter` 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
[Node Package Importer]: #node-package-importer
### Legacy API `pkgImporter`
If set, the compiler will use the specified built-in package importer to resolve
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.
Defaults to undefined.
```ts
declare module '../spec/js-api/legacy/options' {
export interface LegacySharedOptions<sync extends 'sync' | 'async'> {
pkgImporter?: 'node';
}
}
```
## Semantics
### 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]: ../spec/modules.md#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
[parsing a URL]: https://url.spec.whatwg.org/#concept-url-parser
[current source file]: ../spec/spec.md#current-source-file
[resolving a `pkg:` URL as Node]: #node-algorithm-for-resolving-a-pkg-url
## Procedures
### 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]: ../spec/modules.md#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]: ../spec/modules.md#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` + `.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]: ../spec/modules.md#basename
## Embedded Protocol
An Importer that resolves `pkg:` URLs using the [Node resolution algorithm]. It
is instantiated with an associated `entry_point_url`.
```proto
message NodePackageImporter {
string entry_point_url = 1;
}
```
```proto
message CompileRequest {
message Importer {
oneof importer {
NodePackageImporter node_package_importer = 4;
}
}
}
```
## Ecosystem Notes
It may be worth adding a [Community Conditions Definition] to the Node
Documentation. [WinterCG] has a [Runtime Keys proposal specification] underway
in standardizing the usage of custom conditions for runtimes, but Sass doesn't
cleanly fit into that specification.
[community conditions definition]: https://nodejs.org/docs/latest-v20.x/api/packages.html#community-conditions-definitions
[wintercg]: https://wintercg.org/
[runtime keys proposal specification]: https://runtime-keys.proposal.wintercg.org/#adding-a-key

View File

@ -65,7 +65,7 @@ Adjust the list of productions that should produce errors as follows:
* Add "A style rule whose selector contains a trailing combinator."
> While the [bogus combinators] deprecation is in place, style rules with
> trailing combinators that _don't_ have nested rules will produce warnings.
> trailing combinators that *don't* have nested rules will produce warnings.
> Those with nested rules will produce errors since Sass never parsed them
> successfully in the first place.

View File

@ -1 +1 @@
2.1.0
2.3.0

View File

@ -14,13 +14,13 @@ still supported for backwards-compatibility.
## Syntax
<x><pre>
**ImportRule** ::= '@import' (ImportArgumentNoMedia ',')* ImportArgument
**ImportRule** ::= '@import' (ImportArgumentNoMedia ',')\* ImportArgument
**ImportArgumentNoMedia** ::= ImportUrl ImportModifierNoMedia*
**ImportArgument** ::= ImportUrl ImportModifier
**ImportModifierNoMedia** ::= InterpolatedIdentifier* (ImportFunction | ImportSupports)
**ImportModifier** ::= ImportModifierNoMedia* InterpolatedIdentifier* ImportMedia?
**ImportMedia** ::= [MediaFeatureInParens] (',' [MediaQueryList])*
&#32; | InterpolatedIdentifier (',' [MediaQueryList])*
**ImportModifierNoMedia** ::= InterpolatedIdentifier\* (ImportFunction | ImportSupports)
**ImportModifier** ::= ImportModifierNoMedia\* InterpolatedIdentifier\* ImportMedia?
**ImportMedia** ::= [MediaFeatureInParens] (',' [MediaQueryList])\*
&#32; | InterpolatedIdentifier (',' [MediaQueryList])\*
**ImportSupports** ::= 'supports(' SupportsDeclaration ')'
**ImportFunction** ::= [InterpolatedIdentifier]¹ '(' InterpolatedDeclarationValue? ')'
**ImportUrl** ::= QuotedString | [InterpolatedUrl][]
@ -137,10 +137,10 @@ To execute an `@import` rule `rule`:
* Add `imported`'s [extensions][] to the current module.
* If the `@import` rule is nested within at-rules and/or style rules, add each
* If the `@import` rule is nested within at-rules and/or style rules, add each
member in `imported` to the local [scope][].
* Otherwise, add each member in `imported` to the current import context and
* Otherwise, add each member in `imported` to the current import context and
the current module.
> Members defined directly in `imported` will have already been added to

View File

@ -37,7 +37,7 @@ identifiers are matched case-insensitively:
&#32; | '(' Expression³ [\<mf-lt>] Expression³ [\<mf-lt>] Expression³ ')'
&#32; | '(' Expression³ [\<mf-gt>] Expression³ [\<mf-gt>] Expression³ ')'
&#32; | '(' MediaNot ')'
&#32; | '(' MediaInParens (MediaAnd* | MediaOr*) ')'
&#32; | '(' MediaInParens (MediaAnd\*| MediaOr\*) ')'
</pre></x>
[InterpolatedIdentifier]: ../syntax.md#interpolatedidentifier

View File

@ -21,8 +21,6 @@ the current stylesheet, and includes its CSS in the compilation output.
A `@use` rule's *module* is a [module][] associated with a `@use` rule. This
module is only associated once the rule has been [executed](#semantics).
[module]: ../modules.md#module
## Syntax
The grammar for the `@use` rule is as follows:
@ -138,4 +136,3 @@ To execute a `@use` rule `rule`:
* If `variable` wasn't declared with a `!default` flag, throw an error.
* Set [`rule`'s module](#a-use-rules-module) to `module`.

View File

@ -443,7 +443,6 @@ This function is also available as a global function named `hue()`.
[percent-converting]: #percent-converting-a-number
[to RGB]: https://www.w3.org/TR/css-color-4/#hwb-to-rgb
* ```
hwb($channels)
```

View File

@ -221,7 +221,6 @@ hypot($numbers...)
log($number, $base: null)
```
* If `$number` has units, throw an error.
* Return a unitless number whose value is the result of `log($number.value)` as
@ -444,9 +443,10 @@ This function is also available as a global function named `random()`.
units as `$limit`.
> Examples:
> - `math.random(123) => 87`
> - `math.random(123px) => 43px`
> - `math.random(500%) => 238%`
>
> * `math.random(123) => 87`
> * `math.random(123px) => 43px`
> * `math.random(500%) => 238%`
* Otherwise throw an error.

View File

@ -55,7 +55,7 @@ representation of a CSS selector.
* Append a comma to `text` unless `complex` is the last element of
`selector`.
* Otherwise, if `selector` is not a string, throw an error.
* Otherwise, set `text` to the contents of `selector`.
@ -135,8 +135,6 @@ This function is also available as a global function named `selector-replace()`.
* If any of `selector`, `original`, or `replacement` is [bogus], throw an error.
[bogus]: ../selectors.md#bogus-selector
* > Additional semantics have not yet been explicitly written.
### `simple-selectors()`

View File

@ -18,7 +18,6 @@ This built-in module is available from the URL `sass:string`.
## Functions
### `index()`
```
@ -73,12 +72,12 @@ split($string, $separator, $limit: null)
* If `$limit` is less than 1, throw an error.
* If `$string` is an empty string, return a list with `$string` as the only
* If `$string` is an empty string, return a list with `$string` as the only
item.
* Let `split-list` be an empty list.
* If `$limit` is `null`, set `$limit` to the value of calling
* If `$limit` is `null`, set `$limit` to the value of calling
`string.length($string)`.
* Let `split-counter` equal 0.
@ -89,7 +88,7 @@ split($string, $separator, $limit: null)
* Append `$string` to `split-list`.
* Set `$string` to an empty string.
* Set `$string` to an empty string.
* Otherwise:
@ -105,10 +104,10 @@ split($string, $separator, $limit: null)
* Otherwise:
* Let `index` be the result of calling
* Let `index` be the result of calling
`string.index($string, $separator)`.
* If `index` is `null`, append `$string` to `split-list` and set `$string`
* If `index` is `null`, append `$string` to `split-list` and set `$string`
to an empty string.
* Otherwise:
@ -117,12 +116,12 @@ split($string, $separator, $limit: null)
`string.slice($string, 1, index - 1)`.
* Append `current-substring` to `split-list`.
* Set `$string` to
* Set `$string` to
`string.slice($string, index + string.length($separator))`.
* Increase `split-counter` by 1.
* Return `split-list` as a bracketed, comma-separated list.
### `to-lower-case()`
@ -156,4 +155,3 @@ unquote($string)
```
This function is also available as a global function named `unquote()`.

View File

@ -5,7 +5,7 @@ a Sass implementation and a host environment. It allows the host environment to
invoke the Sass compiler on source files, and to define custom functions and
importers in the host language.
Sass implementations are _not_ required to support the embedded protocol.
Sass implementations are *not* required to support the embedded protocol.
However, if they do, they must adhere to the specification given in this file
and [`embedded_sass.proto`] for the compiler endpoint.
@ -60,12 +60,12 @@ its standard input and output streams.
### Packet Structure
Each message in the embedded protocol is sent as a _packet_ which contains two
Each message in the embedded protocol is sent as a *packet* which contains two
values: an unsigned [varint] up to 32 bits long known as the "compilation ID",
and a protocol buffer that contains the protobuf message. For streams (like
standard input and output) that don't have built-in message boundaries, every
packet must begin with another unsigned varint indicating the length in bytes of
the remaining message (_including the compilation ID_). This matches the best
the remaining message (*including the compilation ID*). This matches the best
practice described in [the protocol buffer documentation].
Because JavaScript can't easily represent integers larger than 2^53 - 1, the
@ -319,7 +319,7 @@ tell if a value is "truthy" (one of those values) or "falsey" (`false` or
`null`). It should encourage users to check this rather than directly testing
for `true` or `false`.
Two booleans are equal if they're both `true` or both` false`.
Two booleans are equal if they're both `true` or both `false`.
### Null

View File

@ -2,6 +2,12 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// This protocol buffer uses optional fields, which requires either [buf] or a
// [protoc] version 3.15 or later.
//
// [buf]: https://buf.build/
// [protoc]: https://github.com/protocolbuffers/protobuf#protobuf-compiler-installation
syntax = "proto3";
package sass.embedded_protocol;
@ -93,6 +99,14 @@ message InboundMessage {
// registered for this compilation.
uint32 file_importer_id = 3;
}
// The set of URL schemes that are considered *non-canonical* for this
// importer. This must be empty unless `importer.importer_id` is set.
//
// If any element of this contains a character other than a lowercase
// ASCII letter, an ASCII numeral, U+002B (`+`), U+002D (`-`), or U+002E
// (`.`), the compiler must treat the compilation as failed.
repeated string non_canonical_scheme = 4;
}
// Importers (including load paths on the filesystem) to use when resolving
@ -147,7 +161,9 @@ message InboundMessage {
// The successfully canonicalized URL.
//
// If this is not an absolute URL (including scheme), the compiler must
// treat that as an error thrown by the importer.
// treat that as an error thrown by the importer. If this URL's scheme is
// an `Importer.non_canonical_scheme` for the importer being invoked, the
// compiler must treat that as an error thrown by the importer.
string url = 2;
// An error message explaining why canonicalization failed.
@ -442,15 +458,27 @@ message OutboundMessage {
// That is the result of the import.
string url = 4;
/// Whether this request comes from an `@import` rule.
///
/// When evaluating `@import` rules, URLs should canonicalize to an
/// [import-only file] if one exists for the URL being canonicalized.
/// Otherwise, canonicalization should be identical for `@import` and `@use`
/// rules.
///
/// [import-only file]: https://sass-lang.com/documentation/at-rules/import#import-only-files
// Whether this request comes from an `@import` rule.
//
// When evaluating `@import` rules, URLs should canonicalize to an
// [import-only file] if one exists for the URL being canonicalized.
// Otherwise, canonicalization should be identical for `@import` and `@use`
// rules.
//
// [import-only file]: https://sass-lang.com/documentation/at-rules/import#import-only-files
bool from_import = 5;
// The canonical URL of the [current source file] that contained the load
// to be canonicalized.
//
// [current source file]: ../spec.md#current-source-file
//
// The compiler must set this if and only if `url` is relative or its
// scheme is an `Importer.non_canonical_scheme` for the importer being
// invoked, unless the current source file has no canonical URL.
//
// [non-canonical-proto]: #non_canonical_scheme
optional string containing_url = 6;
}
// A request for a custom importer to load the contents of a stylesheet.
@ -486,8 +514,13 @@ message OutboundMessage {
// * Let `fromImport` be `true` if the importer is being run for an
// `@import` and `false` otherwise.
//
// * Let `containingUrl` be the canonical URL of the [current source file]
// if it has one, or undefined otherwise.
//
//
// * Let `response` be the result of sending a `FileImportRequest` with
// `string` as its `url` and `fromImport` as `from_import`.
// `string` as its `url`, `fromImport` as `from_import`, and
// `containingUrl` as `containing_url`.
//
// * If `response.result` is null, return null.
//
@ -511,6 +544,7 @@ message OutboundMessage {
//
// * Return `text`, `syntax`, and `resolved`.
//
// [current source file]: ../spec.md#current-source-file
// [resolving `url`]: https://github.com/sass/sass/tree/main/spec/modules.md#resolving-a-file-url
message FileImportRequest {
reserved 2;
@ -525,15 +559,20 @@ message OutboundMessage {
// The (non-canonicalized) URL of the import.
string url = 4;
/// Whether this request comes from an `@import` rule.
///
/// When evaluating `@import` rules, filesystem importers should load an
/// [import-only file] if one exists for the URL being canonicalized.
/// Otherwise, canonicalization should be identical for `@import` and `@use`
/// rules.
///
/// [import-only file]: https://sass-lang.com/documentation/at-rules/import#import-only-files
// Whether this request comes from an `@import` rule.
//
// When evaluating `@import` rules, filesystem importers should load an
// [import-only file] if one exists for the URL being canonicalized.
// Otherwise, canonicalization should be identical for `@import` and `@use`
// rules.
//
// [import-only file]: https://sass-lang.com/documentation/at-rules/import#import-only-files
bool from_import = 5;
// The canonical URL of the [current source file] that contained the load
// being resolved. The compiler must set this unless the current source file
// has no canonical URL.
optional string containing_url = 6;
}
// A request to invoke a custom Sass function and return its result.
@ -848,6 +887,17 @@ message Value {
string signature = 2;
}
// A first-class mixin defined in the compiler. New `CompilerMixin`s may
// only be created by the compiler, but the host may pass `CompilerMixin`s
// back to the compiler as long as their IDs match IDs of mixins received
// by the host during that same compilation.
message CompilerMixin {
// A unique ID for this mixin. The compiler is responsible for generating
// this ID and ensuring it's unique across all mixins passed to the host
// for this compilation. Mandatory.
uint32 id = 1;
}
// A SassScript argument list value. This represents rest arguments passed to
// a function's `$arg...` parameter. Unlike a normal `List`, an argument list
// has an associated keywords map which tracks keyword arguments passed in
@ -919,6 +969,11 @@ message Value {
// An unquoted string as created by interpolation for
// backwards-compatibility with older Sass syntax.
//
// The compiler must treat this as identical to a `string` option whose
// value is `"(" + interpolation + ")"`.
//
// This field is deprecated and hosts should avoid using it.
string interpolation = 3;
CalculationOperation operation = 4;
@ -956,6 +1011,7 @@ message Value {
ArgumentList argument_list = 10;
HwbColor hwb_color = 11;
Calculation calculation = 12;
CompilerMixin compiler_mixin = 13;
}
}

View File

@ -7,7 +7,7 @@
* [Special Variable String](#special-variable-string)
* [Syntax](#syntax)
* [Semantics](#semantics)
* [`EmptyFallbackVar`:](#emptyfallbackvar)
* [`EmptyFallbackVar`](#emptyfallbackvar)
* [`FunctionCall`](#functioncall)
* [Global Functions](#global-functions)
* [`adjust-hue()`](#adjust-hue)
@ -47,18 +47,14 @@ matching is case-insensitive.
## Syntax
<x><pre>
**FunctionExpression**¹ ::= [CssMinMax]
&#32; | [SpecialFunctionExpression]
&#32; | [CalculationExpression]
**FunctionExpression**¹ ::= [SpecialFunctionExpression]
&#32; | EmptyFallbackVar
&#32; | FunctionCall
**EmptyFallbackVar**² ::= 'var(' Expression ',' ')'
**FunctionCall**⁴ ::= [NamespacedIdentifier] ArgumentInvocation
</pre></x>
[CssMinMax]: types/calculation.md#cssminmax
[SpecialFunctionExpression]: syntax.md#specialfunctionexpression
[CalculationExpression]: types/calculation.md#calculationexpression
[NamespacedIdentifier]: modules.md#syntax
1: Both `CssMinMax` and `EmptyFallbackVar` take precedence over `FunctionCall`
@ -76,14 +72,12 @@ matching is case-insensitive.
**FunctionCall** ::= [NamespacedIdentifier][] ArgumentInvocation
</pre></x>
[NamespacedIdentifier]: modules.md#syntax
No whitespace is allowed between the `NamespacedIdentifier` and the
`ArgumentInvocation` in `FunctionCall`.
## Semantics
### `EmptyFallbackVar`:
### `EmptyFallbackVar`
To evaluate an `EmptyFallbackVar` `call`:
@ -109,6 +103,25 @@ To evaluate a `FunctionCall` `call`:
* If `function` is null and `name` is not a plain `Identifier`, throw an error.
* If `function` is null; `name` is case-insensitively equal to `"min"`, `"max"`,
`"round"`, or `"abs"`; `call`'s `ArgumentInvocation` doesn't have any
`KeywordArgument`s or `RestArgument`s; and all arguments in `call`'s
`ArgumentInvocation` are [calculation-safe], return the result of evaluating
`call` [as a calculation].
[calculation-safe]: types/calculation.md#calculation-safe-expression
[as a calculation]: types/calculation.md#evaluating-a-functioncall-as-a-calculation
> For calculation functions that overlap with global Sass function names, we
> want anything Sass-specific like this to end up calling the Sass function.
> For all other calculation functions, we want those constructs to throw an
> error (which they do when evaluating `call` [as a calculation]).
* If `function` is null and `name` is case-insensitively equal to `"calc"`,
`"clamp"`, `"hypot"`, `"sin"`, `"cos"`, `"tan"`, `"asin"`, `"acos"`, `"atan"`,
`"sqrt"`, `"exp"`, `"sign"`, `"mod"`, `"rem"`, `"atan2"`, `"pow"`, or `"log"`,
return the result of evaluating `call` [as a calculation].
* If `function` is null, set it to the [global function](#global-functions)
named `name`.
@ -259,6 +272,10 @@ plain CSS function named `"rgb"` that function is named `"rgba"` instead.
* If `rgb` is not an unbracketed space-separated list, throw an error.
* If the first element of `rgb` is an unquoted string which is
case-insensitively equal to `from`, return a plain CSS function string
with the name `"rgb"` and the argument `$channels`.
* If `rgb` has more than three elements, throw an error.
* If `rgb` has fewer than three elements:
@ -276,6 +293,10 @@ plain CSS function named `"rgb"` that function is named `"rgba"` instead.
* 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
with the name `"rgb"` and the argument `$channels`.
* If `$channels` has more than three elements, throw an error.
* If `$channels` has fewer than three elements:
@ -385,6 +406,10 @@ plain CSS function named `"hsl"` that function is named `"hsla"` instead.
* If `hsl` is not an unbracketed space-separated list, throw an error.
* If the first element of `hsl` is an unquoted string which is
case-insensitively equal to `from`, return a plain CSS function string
with the name `"hsl"` and the argument `$channels`.
* If `hsl` has more than three elements, throw an error.
* If `hsl` has fewer than three elements:
@ -402,6 +427,10 @@ plain CSS function named `"hsl"` that function is named `"hsla"` instead.
* 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
with the name `"hsl"` and the argument `$channels`.
* If `$channels` has more than three elements, throw an error.
* If `$channels` has fewer than three elements:
@ -436,8 +465,6 @@ plain CSS function named `"hsl"` that function is named `"hsla"` instead.
* Call `hsl()` with `hue`, `saturation`, `lightness`, and `alpha` (if it's
defined) as arguments and return the result.
[special variable string]: #special-variable-string
### `if()`
```

View File

@ -125,8 +125,6 @@ Compiles the Sass `source`:
> 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

View File

@ -11,12 +11,27 @@ import {PromiseOr} from './util/promise_or';
## Table of Contents
* [Types](#types)
* [`CanonicalizeContext`](#canonicalizecontext)
* [`FileImporter`](#fileimporter)
* [`Importer`](#importer)
* [`nonCanonicalScheme`](#noncanonicalscheme)
* [`ImporterResult`](#importerresult)
## Types
### `CanonicalizeContext`
This is a data object passed into calls to `Importer.canonicalize()` and
`FileImporter.findFileUrl()`. Its fields are set as part of the function
invocations.
```ts
export interface CanonicalizeContext {
fromImport: boolean;
containingUrl: URL | null;
}
```
### `FileImporter`
This interface represents an [importer]. When the importer is invoked with a
@ -33,14 +48,19 @@ 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 `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.
* Let `containingUrl` be the canonical URL of the [current source file] if it
has one, or undefined otherwise.
* Let `url` be the result of calling `findFileUrl` 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.
* If `url` is null, return null.
* If `url`'s scheme is not `file`, throw an error.
[current source file]: ../spec.md#current-source-file
* Let `resolved` be the result of [resolving `url`].
[resolving `url`]: ../modules.md#resolving-a-file-url
@ -65,7 +85,7 @@ export interface FileImporter<
> {
findFileUrl(
url: string,
options: {fromImport: boolean}
context: CanonicalizeContext
): PromiseOr<URL | null, sync>;
canonicalize?: never;
@ -80,9 +100,18 @@ 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 `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.
* 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.
* If the scheme of `url` is [non-canonical] for this importer, throw an error.
[non-canonical]: #noncanonicalscheme
* If `url` is null, return null.
@ -104,13 +133,32 @@ string `string`:
export interface Importer<sync extends 'sync' | 'async' = 'sync' | 'async'> {
canonicalize(
url: string,
options: {fromImport: boolean}
context: CanonicalizeContext
): PromiseOr<URL | null, sync>;
load(canonicalUrl: URL): PromiseOr<ImporterResult | null, sync>;
findFileUrl?: never;
}
```
#### `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 is empty or
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[];
```
```ts
} // Importer
```
### `ImporterResult`

View File

@ -47,7 +47,12 @@ export {
compileStringAsync,
} from './compile';
export {Exception} from './exception';
export {FileImporter, Importer, ImporterResult} from './importer';
export {
CanonicalizeContext,
FileImporter,
Importer,
ImporterResult,
} from './importer';
export {Logger, SourceSpan, SourceLocation} from './logger';
export {
CustomFunction,
@ -72,6 +77,7 @@ export {
SassFunction,
SassList,
SassMap,
SassMixin,
SassNumber,
SassString,
Value,

View File

@ -135,7 +135,6 @@ charset?: boolean;
#### `quietDeps`
If true, the compiler must not print deprecation warnings for stylesheets that
are transitively loaded through an import path.
@ -162,7 +161,6 @@ verbose?: boolean;
#### `logger`
A [custom logger] that provides callbacks for the compiler to use in lieu of its
default messaging behavior.

View File

@ -17,7 +17,6 @@ export {SourceSpan} from './source_span';
* [`Logger`](#logger-1)
* [`silent`](#silent)
## Types
### `Logger`

View File

@ -312,7 +312,7 @@ url?: URL;
### `StringOptionsWithImporter`
> This interface is used for calls to [`compileString()`] and
> [`compileStringAsync()`] that _do_ pass the `importer` parameter, and so _do_
> [`compileStringAsync()`] that *do* pass the `importer` parameter, and so *do*
> support relative imports.
```ts
@ -334,7 +334,7 @@ importer: Importer<sync> | FileImporter<sync>;
The canonical URL of the entrypoint.
> This _must_ be passed when `importer` is passed, since otherwise there's
> This *must* be passed when `importer` is passed, since otherwise there's
> nothing to resolve relative URLs relative to.
```ts

View File

@ -161,8 +161,6 @@ export type CalculationOperator = '+' | '-' | '*' | '/';
The JS API representation of a Sass [`CalculationOperation`].
[CalculationOperation]: ../../types/calculation.md#types
```ts
export class CalculationOperation implements ValueObject {
```
@ -239,9 +237,15 @@ hashCode(): number;
### `CalculationInterpolation`
The JS API representation of a Sass [`CalculationInterpolation`].
A deprecated alternative JS API representation of an unquoted Sass string that's
always surrounded by parentheses. It's never returned by the Sass compiler, but
for backwards-compatibility users may still construct it and pass it to the Sass
compiler.
[`CalculationInterpolation`]: ../../types/calculation.md#types
> `CalculationInterpolation`s are no longer generated by the Sass compiler,
> because it can now tell at evaluation time whether an interpolation was
> originally surrounded by parentheses. However, until we make a breaking
> revision of the JS API, users may continue to pass `CalculationInterpolation`s
```ts
export class CalculationInterpolation implements ValueObject {
@ -249,13 +253,12 @@ export class CalculationInterpolation implements ValueObject {
#### `internal`
A private property like [`Value.internal`] that refers to a Sass
[`CalculationInterpolation`].
A private property like [`Value.internal`] that refers to a Sass string.
#### Constructor
Creates a Sass [`CalculationInterpolation`] by setting the `value` field to the
`value` argument and returns it.
Creates a `CalculationInterpolation` with `internal` set to an unquoted Sass
string with text `"(" + value + ")"` and returns it.
```ts
constructor(value: string);
@ -263,9 +266,10 @@ constructor(value: string);
#### `value`
Returns [`internal`][ci-internal]'s `value` field.
Returns [`internal`][ci-internal]'s `value` field's text, without the leading
and trailing parentheses.
[ci-internal]: #internal-2
[ci-internal]: #internal-1
```ts
get value(): string;
@ -273,7 +277,8 @@ get value(): string;
#### `equals`
Whether [`internal`][ci-internal] is equal to `other.internal` in Sass.
Whether `other` is a `CalculationInterpolation` and [`internal`][ci-internal] is
equal to `other.internal` in Sass.
```ts
equals(other: unknown): boolean;

View File

@ -9,6 +9,7 @@ import {SassColor} from './color';
import {SassFunction} from './function';
import {ListSeparator} from './list';
import {SassMap} from './map';
import {SassMixin} from './mixin';
import {SassNumber} from './number';
import {SassString} from './string';
@ -19,12 +20,13 @@ export {
CalculationValue,
CalculationOperator,
CalculationOperation,
CalculationInterpolation
CalculationInterpolation,
} from './calculation';
export {SassColor} from './color';
export {SassFunction} from './function';
export {SassList, ListSeparator} from './list';
export {SassMap} from './map';
export {SassMixin} from './mixin';
export {SassNumber} from './number';
export {SassString} from './string';
```
@ -48,6 +50,7 @@ export {SassString} from './string';
* [`assertColor`](#assertcolor)
* [`assertFunction`](#assertfunction)
* [`assertMap`](#assertmap)
* [`assertMixin`](#assertmixin)
* [`assertNumber`](#assertnumber)
* [`assertString`](#assertstring)
* [`tryMap`](#trymap)
@ -141,16 +144,16 @@ 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.
* If `sassIndex` is not a unitless Sass number, throw an error.
- Let `value` be the value of `sassIndex`. Let `index` be the result of
* 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
* 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`.
* 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.
@ -231,6 +234,18 @@ Return `this.tryMap()` if it's not null, and throw an error otherwise.
assertMap(name?: string): SassMap;
```
#### `assertMixin`
Returns `this` if it's a [`SassMixin`] and throws an error otherwise.
[`SassMixin`]: mixin.d.ts.md
> The `name` parameter may be used for error reporting.
```ts
assertMixin(name?: string): SassMixin;
```
#### `assertNumber`
Returns `this` if it's a [`SassNumber`] and throws an error otherwise.
@ -285,7 +300,7 @@ Returns the same number for any two `Value`s that are equal according to
[`equals`]: #equals
> This is _not_ required to be different for different values, although having
> This is *not* required to be different for different values, although having
> overlap between common values is likely to cause performance issues.
```ts

View File

@ -33,7 +33,6 @@ The [private `internal` field] refers to a Sass map.
[private `internal` field]: index.d.ts.md#internal
#### Constructor
Creates a Sass map:

View File

@ -0,0 +1,40 @@
# Mixin API
```ts
import {Value} from './index';
```
## Table of Contents
* [Types](#types)
* [`SassMixin`](#sassmixin)
* [`internal`](#internal)
* [Constructor](#constructor)
## Types
### `SassMixin`
The JS API representation of a Sass mixin.
```ts
export class SassMixin extends Value {
```
#### `internal`
The [private `internal` field] refers to a Sass mixin.
[private `internal` field]: index.d.ts.md#internal
#### Constructor
Throws an error.
```ts
constructor();
```
```ts
} // SassMixin
```

View File

@ -291,7 +291,7 @@ 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:
* If `internal` is [unitless], return the result of:
[unitless]: ../../types/number.md#

View File

@ -28,7 +28,7 @@ combinator*) as well as a sequence of [complex selector components]. The
component sequence may be empty only for complex selectors with leading
combinators.
[visible combinator]: #visible-combinator
[visible combinator]: #visible-combinator
[complex selector components]: #complex-selector-component
### Complex Selector Component

View File

@ -102,6 +102,7 @@ a string.
> exposes this to the user.
This algorithm takes:
* a string `string`,
* a syntax `syntax` ("indented", "scss", or "css"),
* an optional URL `url`,
@ -147,8 +148,6 @@ It runs as follows:
* Let `file` be the [source file][] with `ast`, canonical URL `url`, and
importer `importer`.
[source file]: syntax.md#source-file
* Let `module` be the result of [executing](#executing-a-file) `file`.
* Let `css` be the result of [resolving `module`'s extensions][].
@ -166,10 +165,6 @@ It runs as follows:
This algorithm takes a [source file][] `file`, a [configuration][] `config`, an
[import context][] `import`, and returns a [module][].
[module]: modules.md#module
[configuration]: modules.md#configuration
[import context]: modules.md#import-context
* Let `module` be an empty module with source file `file`.
* Let `uses` be an empty map from `@use` rules to modules.

View File

@ -106,7 +106,6 @@ No whitespace is allowed between components of an `InterpolatedUnquotedUrlConten
[\<declaration-value>]: https://www.w3.org/TR/css-syntax-3/#typedef-declaration-value
[\<an+b>]: https://www.w3.org/TR/css-syntax-3/#the-anb-type
[\<ident-token>]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
1: The string `of` is matched case-insensitively. In addition, it must be parsed
as an identifier.
@ -295,7 +294,6 @@ This algorithm consumes input from a stream of [code points][] and returns a
sequence of strings and/or expressions. It follows the grammar for an
[`InterpolatedIdentifier`][].
[code points]: https://infra.spec.whatwg.org/#code-point
[`InterpolatedIdentifier`]: #interpolatedidentifier
* Let `components` be an empty list of strings and/or expressions.

View File

@ -2,88 +2,48 @@
## Table of Contents
* [Syntax](#syntax)
* [`CalculationExpression`](#calculationexpression)
* [`CssMinMax`](#cssminmax)
* [Definitions](#definitions)
* [Calculation-Safe Expression](#calculation-safe-expression)
* [Types](#types)
* [Operations](#operations)
* [Equality](#equality)
* [Serialization](#serialization)
* [Calculation](#calculation)
* [`CalculationOperation`](#calculationoperation)
* [`CalculationInterpolation`](#calculationinterpolation)
* [`Number`](#number)
* [Procedures](#procedures)
* [Evaluating a `FunctionCall` as a Calculation](#evaluating-a-functioncall-as-a-calculation)
* [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)
* [`CalcExpression`](#calcexpression)
* [`ClampExpression`](#clampexpression)
* [`CssMinMax`](#cssminmax-1)
* [`CalcArgument`](#calcargument)
* [`CalcSum`](#calcsum)
* [`CalcProduct`](#calcproduct)
* [`CalcValue`](#calcvalue)
* [`ParenthesizedVar`](#parenthesizedvar)
* [`FunctionExpression` and `Variable`](#functionexpression-and-variable)
* [`SumExpression` and `ProductExpression`](#sumexpression-and-productexpression)
* [`SpaceListExpression`](#spacelistexpression)
* [`ParenthesizedExpression`](#parenthesizedexpression)
* [`InterpolatedIdentifier`](#interpolatedidentifier)
## Syntax
## Definitions
### `CalculationExpression`
### Calculation-Safe Expression
This production is parsed in a SassScript context when an expression is expected
and the input stream starts with an identifier with value `calc` or `clamp`
(ignoring case) followed immediately by `(`.
An expression is "calculation-safe" if it is one of:
The grammar for this production is:
* 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
calculation-safe.
* A `Number`.
* A `Variable`.
* An `InterpolatedIdentifier`.
* An unbracketed `SpaceListExpression` with more than one element, whose
elements are all calculation-safe.
<x><pre>
**CalculationExpression** ::= CalcExpression | ClampExpression
**CalcExpression** ::= 'calc('¹ CalcArgument ')'
**ClampExpression** ::= 'clamp('¹ CalcArgument ( ',' CalcArgument ){2} ')'
**CalcArgument**² ::= InterpolatedDeclarationValue† | CalcSum
**CalcSum** ::= CalcProduct (('+' | '-')³ CalcProduct)\*
**CalcProduct** ::= CalcValue (('\*' | '/') CalcValue)\*
**CalcValue** ::= ParenthesizedVar
&#32; | '(' CalcArgument⁴ ')'
&#32; | CalculationExpression
&#32; | CssMinMax
&#32; | FunctionExpression⁵
&#32; | Number
&#32; | Variable†
&#32; | [\<ident-token>]
**ParenthesizedVar** ::= '(' 'var('¹ ArgumentInvocation ')' ')'
</pre></x>
[`FunctionExpression`]: ../functions.md#syntax
[\<ident-token>]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
1: The strings `calc(`, `clamp(`, and `var(` are matched case-insensitively.
2: A `CalcArgument` is only parsed as an `InterpolatedDeclarationValue` if it
includes interpolation, unless that interpolation is within a region bounded by
parentheses (a `FunctionExpression` counts as parentheses).
3: Whitespace is required around these `"+"` and `"-"` tokens.
4: This `CalcArgument` cannot begin with `var(`, case-insensitively.
5: This `FunctionExpression` cannot begin with `min(`, `max(`, or `clamp(`,
case-insensitively.
†: These productions are invalid in plain CSS syntax.
> The `CalcArgument` production provides backwards-compatibility with the
> historical use of interpolation to inject SassScript values into `calc()`
> expressions. Because interpolation could inject any part of a `calc()`
> expression regardless of syntax, for full compatibility it's necessary to
> parse it very expansively.
### `CssMinMax`
<x><pre>
**CssMinMax** ::= ('min(' | 'max(')¹ CalcArgument (',' CalcArgument)* ')'
</pre></x>
1: The strings `min(` and `max(` are matched case-insensitively.
> Because calculations have special syntax in CSS, only a subset of SassScript
> expressions are valid (and these are interpreted differently than elsewhere).
## Types
@ -98,14 +58,9 @@ interface Calculation {
type CalculationValue =
| Number
| UnquotedString
| CalculationInterpolation
| CalculationOperation
| Calculation;
interface CalculationInterpolation {
value: string;
}
interface CalculationOperation {
operator: '+' | '-' | '*' | '/';
left: CalculationValue;
@ -136,8 +91,8 @@ Two calculations are considered equal if their names are equal, they have the
same number of arguments, and each argument in one calculation is equal to the
corresponding argument in the other.
`CalculationOperation` and `CalculationInterpolation` values are equal if each
field in one value is equal to the corresponding field in the other.
`CalculationOperation` values are equal if each field in one value is equal to
the corresponding field in the other.
### Serialization
@ -153,19 +108,14 @@ To serialize a `CalculationOperation`:
* Let `left` and `right` be the result of serializing the left and right values,
respectively.
* If either:
* the left value is a `CalculationInterpolation`, or
* the operator is `"*"` or `"/"` and the left value is a
`CalculationOperation` with operator `"+"` or `"-"`,
emit `"("` followed by `left` followed by `")"`. Otherwise, emit `left`.
* If the operator is `"*"` or `"/"` and the left value is a
`CalculationOperation` with operator `"+"` or `"-"`, emit `"("` followed by
`left` followed by `")"`. Otherwise, emit `left`.
* Emit `" "`, then the operator, then `" "`.
* If either:
* the right value is a `CalculationInterpolation`, or
* the operator is `"*"` or `"-"` and the right value is a
`CalculationOperation` with operator `"+"` or `"-"`, or
* the operator is `"/"` and the right value is a `CalculationOperation`,
@ -174,10 +124,6 @@ To serialize a `CalculationOperation`:
emit `"("` followed by `right` followed by `")"`. Otherwise, emit `right`.
#### `CalculationInterpolation`
To serialize a `CalculationInterpolation`, emit its `value`.
#### `Number`
To serialize a `Number` within a `CalculationExpression`:
@ -197,6 +143,34 @@ To serialize a `Number` within a `CalculationExpression`:
## Procedures
### Evaluating a `FunctionCall` as a Calculation
This algorithm takes a [`FunctionCall`] `call` whose name is a plain identifier
and returns a number or a calculation.
* If `call`'s `ArgumentInvocation` contains one or more `KeywordArgument`s or
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].
[as a calculation value]: #evaluating-an-expression-as-a-calculation-value
* Return the result of [simplifying](#simplifying-a-calculation) `calc`.
### Evaluating an Expression as a Calculation Value
This algorithm takes an expression `expression` and returns a
`CalculationValue`.
* If `expression` isn't [calculation-safe], throw an error.
* Otherwise, evaluate `expression` using the semantics defined in the
[Semantics] section if available, or the standard semantics otherwise.
[Semantics]: #semantics
### Simplifying a Calculation
This algorithm takes a calculation `calc` and returns a number or a calculation.
@ -207,31 +181,205 @@ This algorithm takes a calculation `calc` and returns a number or a calculation.
* If `calc` was parsed from an expression within a `SupportsDeclaration`'s
`Expression`, but outside any interpolation, return a `calc` as-is.
* Let `arguments` be the result of [simplifying](#simplifying-a-calculationvalue) each
of `calc`'s arguments.
* Let `arguments` be the result of [simplifying] each of `calc`'s arguments.
* If `calc`'s name is `"calc"`, the syntax guarantees that `arguments` contain
only a single argument. If that argument is a number or calculation, return
it.
[simplifying]: #simplifying-a-calculationvalue
* If `calc`'s name is `"clamp"`, `arguments` has fewer than three elements, and
none of those are unquoted strings or `CalculationInterpolation`s, throw an
* If `calc`'s name is `"calc"` and `arguments` contains exactly a single number
or calculation, return it.
* If `calc`'s name is `"mod"`, `"rem"`, `"atan2"`, or `"pow"`; `arguments` has
fewer than two elements; and none of those are unquoted strings, throw an
error.
> It's valid to write `clamp(var(--three-args))` or `clamp(#{"1, 2, 3"})`, but
> otherwise `clamp()` has to have three physical arguments.
> It's valid to write `pow(var(--two-args))` or `pow(#{"2, 3"})`, but
> otherwise calculations' arguments must match the expected number.
* If `calc`'s name is `"clamp"` and `arguments` are all
numbers:
* If `calc`'s name is `"sin"`, `"cos"`, `"tan"`, `"asin"`, `"acos"`, `"atan"`,
`"sqrt"`, `"log"`, or `"round"` and `arguments` contains exactly a single
number, return the result of passing that number to the function in
[`sass:math`] whose name matches `calc`'s.
* If those arguments' are mutually [compatible], return the result of calling
`math.clamp()` with those arguments.
[`sass:math`]: ../built-in-modules/math.md
* Otherwise, if any two of those arguments are [definitely-incompatible],
throw an error.
> The `sass:math` functions will check units here for the functions that
> require specific or no units.
* If `calc`'s name is `"abs"` and `arguments` contains exactly a single number
with [known units], return the result of passing that number to the function
in [`sass:math`] whose name matches `calc`'s.
[known units]: number.md#known-units
* If `calc`'s name is `"exp"` and `arguments` contains exactly a single number
`number`, return the result of calling `math.pow(math.$e, number)`.
> This will throw an error if the argument has units.
* If `calc`'s name is `"sign"` and `arguments` contains exactly a single number
`number` with [known units]:
* If `number`'s value is positive, return `1`.
* If `number`'s value is negative, return `-1`.
* Otherwise, return a unitless number with the same value as `number`.
> In this case, `number` is either `+0`, `-0`, or NaN.
> To match CSS's behavior, these computations *don't* use fuzzy comparisons.
* If `calc`'s name is `"log"`:
* If any argument is a number with units, throw an error.
* Otherwise, if `arguments` contains exactly two numbers, return the result of
passing its arguments to the [`log()` function] in [`sass:math`].
[`log()` function]: ../built-in-modules/math.md#log
* If `calc`'s name is `"pow"`:
* If any argument is a number with units, throw an error.
* Otherwise, if `arguments` contains exactly two numbers, return the result of
passing those numbers to the [`pow()` function] in [`sass:math`].
[`pow()` function]: ../built-in-modules/math.md#pow
* If `calc`'s name is `"atan2"` and `arguments` contains two numbers which both
have [known units], return the result of passing those numbers to the
[`atan2()` function] in [`sass:math`].
> This will throw an error if either argument has units.
>
> `atan2()` passes percentages along to the browser because they may resolve
> to negative values, and `atan2(-x, -y) != atan2(x, y)`.
[`atan2()` function]: ../built-in-modules/math.md#atan2
* If `calc`'s name is `"mod"` or `"rem"`:
* If `arguments` has only one element and it's not an unquoted string, throw
an error.
* Otherwise, if `arguments` contains exactly two numbers `dividend` and
`modulus`:
* If `dividend` and `modulus` are [definitely-incompatible], throw an error.
* If `dividend` and `modulus` are mutually [compatible]:
* Let `result` be the result of `dividend % modulus`.
* If `calc`'s name is `"rem"`, and if `dividend` is positive and `modulus`
is negative or vice versa:
* If `modulus` is infinite, return `dividend`.
* If `result` [exactly equals] 0, return `-result`.
* Otherwise, return `result - modulus`.
* Otherwise, return `result`.
[compatible]: number.md#compatible-units
[definitely-incompatible]: number.md#possibly-compatible-numbers
[exactly equals]: number.md#exact-equality
* If `calc`'s name is `"round"`:
* If `arguments` has exactly three elements, set `strategy`, `number`, and
`step` to those arguments respectively.
* Otherwise, if `arguments` has exactly two elements:
* If the first element is an unquoted string or interpolation with value
`"nearest"`, `"up"`, `"down"`, or `"to-zero"`, and the second argument
isn't an unquoted string, throw an error.
> Normally we allow unquoted strings anywhere in a calculation, but this
> helps catch the likely error of a user accidentally writing `round(up,
> 10px)` without realizing that it needs a third argument.
* Otherwise, set `number` and `step` to the two arguments respectively and
`strategy` to an unquoted string with value `"nearest"`.
* Otherwise, if the single argument isn't an unquoted string, throw an error.
* If `strategy`, `number`, and `step` are set:
* If `strategy` isn't a [special variable string], nor is it an unquoted
string or interpolation with value `"nearest"`, `"up"`, `"down"`, or
`"to-zero"`, throw an error.
* If `strategy` is an unquoted string or interpolation and both `number` and
`step` are numbers:
* If `number` and `step` are [definitely-incompatible], throw an error.
* If `number` and `step` are mutually [compatible]:
* If `number`'s and `step`'s values are both infinite, if `step` is
[exactly equal] to 0, or if either `number`'s or `step`'s values are
NaN, return NaN with the same units as `number`.
* If `number`'s value is infinite, return `number`.
* If `step`'s value is infinite:
* If `strategy`'s value is `"nearest"` or `"to-zero"`, return `+0` if
`number`'s value is positive or `+0`, and `-0` otherwise.
* If `strategy`'s value is `"up"`, return positive infinity if
`number`'s value is positive, `+0` if `number`'s value is `+0`, and
`-0` otherwise.
* If `strategy`'s value is `"down"`, return negative infinity if
`number`'s value is negative, `-0` if `number`'s value is `-0`, and
`+0` otherwise.
* Set `number` and `step` to the result of [matching units] for `number`
and `step`.
* If `number`'s value is [exactly equal] to `step`'s, return `number`.
* Let `upper` and `lower` be the two integer multiples of `step` which
are closest to `number` such that `upper` is greater than `lower`. If
`upper` would be 0, it's specifically `-0`; if `lower` would be zero,
it's specifically `-0`.
* If `strategy`'s value is `"nearest"`, return whichever of `upper` and
`lower` has the smallest absolute distance from `number`. If both have
an equal difference, return `upper`.
* If `strategy`'s value is `"up"`, return `upper`.
* If `strategy`'s value is `"down"`, return `lower`.
* If `strategy`'s value is `"to-zero"`, return whichever of `upper` and
`lower` has the smallest absolute difference from 0.
[special variable string]: ../functions.md#special-variable-string
* If `calc`'s name is `"clamp"`:
* If `arguments` has fewer than three elements, and none of those are unquoted
strings, throw an error.
* Otherwise, if any two elements of `arguments` are [definitely-incompatible]
numbers, throw an error.
* Otherwise, if `arguments` are all mutually [compatible] numbers, return the
result of calling `math.clamp()` with those arguments.
* If `calc`'s name is `"hypot"`:
* If any two elements of `arguments` are [definitely-incompatible] numbers,
throw an error.
* Otherwise, if all `arguments` are all numbers with [known units] that are
mutually [compatible], return the result of calling `math.hypot()` with
those arguments.
> `hypot()` has an exemption for percentages because it squares its inputs,
> so `hypot(-x, -y) != -hypot(x, y)`.
* If `calc`'s name is `"min"` or `"max"` and `arguments` are all numbers:
@ -260,19 +408,25 @@ This algorithm takes a `CalculationValue` `value` and returns a
> This algorithm is intended to return a value that's CSS-semantically identical
> to the input.
* If `value` is a number, unquoted string, or `CalculationInterpolation`, return
it as-is.
* If `value` is a number or unquoted string, return it as-is.
* If `value` is a calculation:
* Let `result` be the result of [simplifying] `value`.
* Let `result` be the result of [simplifying] `value`.
* If `result` is a calculation whose name is `"calc"`, return `result`'s
single argument.
* If `result` isn't a calculation whose name is `"calc"`, return `result`.
* Otherwise, return `result`.
* If `result`'s argument isn't an unquoted string, return `result`'s argument.
[simplifying]: #simplifying-a-calculation
* If `result`'s argument begins case-insensitively with `"var("`; or if it
contains whitespace, `"/"`, or `"*"`; return `"(" +` result's argument `+
")"` as an unquoted string.
> This is ensures that values that could resolve to operations end up
> parenthesized if used in other operations. It's potentially a little
> overzealous, but that's unlikely to be a major problem given that the
> output is still smaller than including the full `calc()` and we don't want
> to encourage users to inject calculations with interpolation anyway.
* Otherwise, `value` must be a `CalculationOperation`. Let `left` and `right` be
the result of simplifying `value.left` and `value.right`, respectively.
@ -313,133 +467,111 @@ This algorithm takes a `CalculationValue` `value` and returns a
## Semantics
### `CalcExpression`
The following semantics only apply when evaluating expressions [as calculation
values].
To evaluate a `CalcExpression`:
[as calculation values]: #evaluating-an-expression-as-a-calculation-value
* Let `calc` be a calculation whose name is `"calc"` and whose only argument is
the result of [evaluating the expression's `CalcArgument`](#calcargument).
### `FunctionExpression` and `Variable`
* Return the result of [simplifying] `calc`.
To evaluate a `FunctionExpression` or a `Variable` as a calculation value,
evaluate it using the standard semantics. If the result is a number, an unquoted
string, or a calculation, return it. Otherwise, throw an error.
### `ClampExpression`
> Allowing variables to return unquoted strings here supports referential
> transparency, so that `$var: fn(); calc($var)` works the same as `calc(fn())`.
To evaluate a `ClampExpression`:
### `SumExpression` and `ProductExpression`
* Let `clamp` be a calculation whose name is `"clamp"` and whose arguments are the
results of [evaluating the expression's `CalcArgument`s](#calcargument).
To evaluate a `SumExpresssion` or a `ProductExpression` as a calculation value:
* Return the result of [simplifying] `clamp`.
* Let `left` be the result of evaluating the first operand as a calculation
value.
### `CssMinMax`
* For each remaining `"+"`, `"-"`, `"*"`, or `"/"` token `operator` and operand
`operand`:
To evaluate a `CssMinMax`:
* Let `calc` be a calculation whose name is `"min"` or `"max"` according to the
`CssMinMax`'s first token, and whose arguments are the results of [evaluating
the expression's `CalcArgument`s](#calcargument).
* Return the result of [simplifying] `calc`.
### `CalcArgument`
To evaluate a `CalcArgument` production `argument` into a `CalculationValue` object:
* If `argument` is an `InterpolatedDeclarationValue`, evaluate it and return a
`CalculationInterpolation` whose `value` is the resulting string.
* Otherwise, return the result of [evaluating `argument`'s
`CalcValue`](#calcvalue).
### `CalcSum`
To evaluate a `CalcSum` production `sum` into a `CalculationValue` object:
* Left `left` be the result of evaluating the first `CalcProduct`.
* For each remaining "+" or "-" token `operator` and `CalcProduct` `product`:
* Let `right` be the result of evaluating `product`.
* Let `right` be the result of evaluating `operand` as a calculation value.
* Set `left` to a `CalcOperation` with `operator`, `left`, and `right`.
* Return `left`.
### `CalcProduct`
### `SpaceListExpression`
To evaluate a `CalcProduct` production `product` into a `CalculationValue`
object:
To evaluate a `SpaceListExpresssion` as a calculation value:
* Left `left` be the result of evaluating the first `CalcValue`.
* Let `elements` be the results of evaluating each element as a calculation
value.
* For each remaining "*" or "/" token `operator` and `CalcValue` `value`:
* If `elements` has two adjacent elements that aren't unquoted strings, throw an
error.
* Let `right` be the result of evaluating `value`.
> This ensures that valid CSS constructs like `calc(1 var(--plus-two))` and
> similar Sass constructs like `calc(1 #{"+ 2"})` work while preventing clear
> errors like `calc(1 2)`.
>
> This does allow errors like `calc(a b)`, but the complexity of verifying
> that the unquoted strings could actually be a partial operation isn't worth
> the benefit of eagerly producing an error in this edge case.
* Set `left` to a `CalcOperation` with `operator`, `left`, and `right` as its
values.
* Let `serialized` be an empty list.
* Return `left`.
* For each `element` of `elements`:
### `CalcValue`
* Let `css` be the result of [serializing] `element`.
To evaluate a `CalcValue` production `value` into a `CalculationValue` object:
[serializing]: #serialization
* If `value` is a `CalcArgument`, `CssMinMax`, `Number`, or `ParenthesizedVar`,
return the result of evaluating it.
* If `element` is a `CalcOperation` that was produced by evaluating a
`ParenthesizedExpression`, set `css` to `"(" + css + ")"`.
* If `value` is a `FunctionExpression` or `Variable`, evaluate it. If the result
is a number, an unquoted string, or a calculation, return it. Otherwise, throw
an error.
* Append `css` to `serialized`.
> Allowing variables to return unquoted strings here supports referential
> transparency, so that `$var: fn(); calc($var)` works the same as
> `calc(fn())`.
* Return an unquoted strings whose contents are the elements of `serialized`
separated by `" "`.
* If `value` is case-insensitively equal to `pi`, return 3.141592653589793.
### `ParenthesizedExpression`
> This is the closest double approximation of the mathematical constant π.
* If `value` is case-insensitively equal to `e`, return 2.718281828459045.
> This is the closest double approximation of the mathematical constant e.
* If `value` is case-insensitively equal to `infinity`, return the double
`Infinity`.
* If `value` is case-insensitively equal to `-infinity`, return the double
`-Infinity`.
* If `value` is case-insensitively equal to `nan`, return the double `NaN`.
* If `value` is any other `<identifier>`, return an `UnquotedString` with
`value` as its contents.
### `ParenthesizedVar`
> If a `var()` is written directly within parentheses, it's necessary to
> preserve those parentheses. CSS resolves `var()` by literally replacing the
> function with the value of the variable and *then* parsing the surrounding
> context.
> If a `var()` or an interpolation is written directly within parentheses, it's
> necessary to preserve those parentheses. CSS resolves `var()` by literally
> replacing the function with the value of the variable and *then* parsing the
> surrounding context.
>
> For example, if `--ratio: 2/3`, `calc(1 / (var(--ratio)))` is parsed as
> `calc(1 / (2/3)) = calc(3/2)` but `calc(1 / var(--ratio))` is parsed as
> `calc(1 / 2/3) = calc(1/6)`.
To evaluate a `ParenthesizedVar` production `value` into an unquoted string:
To evaluate a `ParenthesizedExpression` with contents `expression` as a
calculation value:
* Let `function` be a [`FunctionCall`] with `"var"` as its
[`NamespacedIdentifier`] and with `value`'s `ArgumentInvocation`.
* Let `result` be the result of evaluating `expression` as a calculation value.
[`FunctionCall`]: ../functions.md#syntax
[`NamespacedIdentifier`]: ../modules.md#syntax
* If `result` is an unquoted string, return `"(" + result + ")"` as an unquoted
string.
* Let `result` be the result of evaluating `function`.
* Otherwise, return `result`.
* If `result` is a number or a calculation, return it.
### `InterpolatedIdentifier`
> This could happen if the user defines a `var` function in Sass.
To evaluate an `InterpolatedIdentifier` `ident` as a calculation value:
* If `result` is not an unquoted string, throw an error.
* If `ident` is case-insensitively equal to `pi`, return 3.141592653589793.
* Return `"(" + result + ")"` as an unquoted string.
> This is the closest double approximation of the mathematical constant π.
* If `ident` is case-insensitively equal to `e`, return 2.718281828459045.
> This is the closest double approximation of the mathematical constant e.
* If `ident` is case-insensitively equal to `infinity`, return the double
`Infinity`.
* If `ident` is case-insensitively equal to `-infinity`, return the double
`-Infinity`.
* If `ident` is case-insensitively equal to `nan`, return the double `NaN`.
* Otherwise, return the result of evaluating `ident` using standard semantics.
> This will be an `UnquotedString`.

View File

@ -10,6 +10,8 @@
* [Compatible Units](#compatible-units)
* [Possibly-Compatible Units](#possibly-compatible-units)
* [Possibly-Compatible Numbers](#possibly-compatible-numbers)
* [Known Units](#known-units)
* [Exact Equality](#exact-equality)
* [Fuzzy Equality](#fuzzy-equality)
* [Integer](#integer)
* [Potentially Slash-Separated Number](#potentially-slash-separated-number)
@ -51,9 +53,9 @@ as defined by [IEEE 754 2019], §3.2-3.3.
### Degenerate Number
The doubles `Infinity`, `-Infinity`, and `NaN` are _degenerate_.
The doubles `Infinity`, `-Infinity`, and `NaN` are *degenerate*.
A number is _degenerate_ if its value is degenerate.
A number is *degenerate* if its value is degenerate.
### Conversion Factors
@ -132,8 +134,6 @@ possible-compatibility.
> units; this preserves forwards-compatibility with new units that are
> introduced in browsers over time.
[CSS Values and Units]: https://www.w3.org/TR/css-values-3/
| Type | Units |
| -------------- | -------------------------------------------------------------------------------------------- |
| `<length>` | `em`, `ex`, `ch`, `rem`, `vw`, `vh`, `vmin`, `vmax`, `cm`, `mm`, `Q`, `in`, `pt`, `pc`, `px` |
@ -161,12 +161,33 @@ Two numbers are *definitely-incompatible* if they are not possibly-compatible.
> complex units, but in practice these numbers are already flagged as errors
> prior to any possible-compatibility checks.
### Known Units
A number has *known units* unless it has unit `%`.
> This is relevant for calculations, because in plain CSS they resolve
> percentages before doing their operations. This means that any non-linear
> operations involving percentages must be passed through to plain CSS rather
> than handled by Sass.
>
> More complex units involving percentages are allowed because any non-linear
> function will throw for complex units anyway.
### Exact Equality
Two [doubles] are said to be *exactly equal* if they are equal according to the
`compareQuietEqual` predicate as defined by [IEEE 754 2019], §5.11.
[doubles]: #double
> This is as opposed to [fuzzy equality].
>
> [fuzzy equality]: #fuzzy-equality
### Fuzzy Equality
Two [doubles] are said to be *fuzzy equal* to one another if either:
[doubles]: #double
* They are equal according to the `compareQuietEqual` predicate as defined
by [IEEE 754 2019], §5.11.
@ -197,47 +218,28 @@ 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 both operands are *syntactically* one of the
a `/` operator is evaluated and each operand is *syntactically* one of the
following:
* `Number`s,
* [`Calculation`]s, or
* `ProductExpression`s that can themselves create potentially slash-separated
* a `Number`,
* a [`FunctionCall`], or
* a `ProductExpression` that can itself create potentially slash-separated
numbers.
[`Calculation`]: calculation.md#syntax
If both operands are evaluated as numbers, the resulting number is potentially
slash-separated. The first operand is the original numerator of the potentially
slash-separated number returned by the `/` operator, and the second is the
original denominator.
[`FunctionCall`]: ../functions.md#functioncall
A potentially slash-separated number is converted to a slash-free number when:
If the result of evaluating the `ProductExpression` is a number, that number is
potentially slash-separated if all of the following are true:
* It is the value of a `ParenthesizedExpression`.
* 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"`.
> That is, it's in parentheses, such as in `(1 / 2)`. Note that if it's in a
> list that's in parentheses, it's *not* converted to a slash-free number.
[evaluated as a calculation]: calculation.md#evaluating-a-functioncall-as-a-calculation
* It is stored in a Sass variable.
* It is passed to a function or mixin.
* It is returned by a function.
> Any expressions that normally produce a new number (such as other mathematical
> operations) always produce slash-free numbers, even when their arguments are
> slash-separated.
>
> When a potentially slash-separated number is "converted" to a slash-free
> number, a slash-free copy is made of the original. Sass values are always
> immutable.
When a potentially slash-separated number is converted to CSS, either when
converted to a string via interpolation or when included in a declaration's
value, it is written as the original numerator followed by `/` followed by the
original denominator. If either the original numerator or denominator are
themselves slash-separated, they're also written this way.
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
@ -247,8 +249,6 @@ The value type known as a *number* has three components:
* A list of strings called *numerator units*.
* A list of strings called *denominator units*.
[double]: #doubles
Several shorthands exist when referring to numbers:
* A number's *units* refers to the [set of units] containing its numerator units
@ -345,23 +345,29 @@ Let `n1` and `n2` be two numbers. To determine `n1 % n2`:
* Let `c1` and `c2` be the result of [matching units] for `n1` and `n2` allowing
unitless.
* If `c2` is infinity and has a different sign than `c1` (including
oppositely-signed zero), return NaN with the same units as `c1`.
> This matches the behavior of CSS's `mod()` function.
* Let `remainder` be a number whose value is the result of `remainder(c1.value,
c2.value)` as defined by [IEEE 754 2019], §5.3.1; and whose units are the same
as `c1`'s.
* If `c2`'s value is less than 0 and `remainder`'s value isn't `0` or `-0`,
return `result - c2`.
* If `c2`'s value is less than 0 and `remainder`'s value isn't [exactly equal]
to `0`, return `remainder - c2`.
[exactly equal]: #exact-equality
> This is known as [floored division]. It differs from the standard IEEE 754
> specification because it was originally inherited from Ruby when that was
> used for Sass's original implementation.
> specification, but matches the behavior of CSS's `mod()` function.
>
> Note: These comparisons are not the same as `c2 < 0` or `remainder == 0`,
> because they don't do fuzzy equality.
[floored division]: https://en.wikipedia.org/wiki/Modulo_operation#Variants_of_the_definition
* Otherwise, return `result`.
* Otherwise, return `remainder`.
#### Negation

View File

@ -104,6 +104,8 @@ function runLinkCheck(
{pattern: /^https?:\/\/twitter\.com(\/|$)/},
// tcort/markdown-link-check#260
{pattern: /^https?:\/\/blogs\.msdn\.microsoft\.com(\/|$)/},
// Link consistently fails within CI
{pattern: /^https:\/\/runtime-keys\.proposal\.wintercg\.org(\/|$)/},
],
},
(error, results) => {

View File

@ -2,7 +2,7 @@ import * as glob from 'glob';
import markdownToc = require('markdown-toc');
/** Files that may contain tables of contents. */
export const files = glob.sync('**/*.md', {
export const files = glob.sync('**/*.md?(.d.ts)', {
ignore: ['node_modules/**/*.md', '**/*.changes.md'],
});