See #3504
23 KiB
Forward Slash as a Separator: Draft 3.1
This proposal modifies the /
character to be used exclusively as a separator,
and lays out a process for deprecating its existing usage as a division
operator.
Table of Contents
- Background
- Summary
- Alternatives Considered
- Existing Behavior
- Definitions
- Syntax
- Procedures
- Semantics
- Deprecation Process
- Timeline
Background
This section is non-normative.
Early on in Sass's history, the decision was made to use /
as a division
operator, since that was (and is) by far the most common representation across
programming languages. The /
character was used in very few plain CSS
properties, and for those it was an optional shorthand. So Sass defined a set
of heuristics that defined when /
would be rendered as a literal slash
versus treated as an operator.
For a long time, these heuristics worked pretty well. In recent years, however,
new additions to CSS such as CSS Grid and CSS Color Level 4 have been
using /
as a separator increasingly often. Using the same character for both
division and slash-separation is becoming more and more annoying to users, and
will likely eventually become untenable.
Summary
This section is non-normative.
We will redefine /
to be only a separator. Rather than creating an unquoted
string (as it currently does when at least one operand isn't a number), it will
create a slash-separated list. As such, lists will now have three possible
separators: space, comma, and slash.
Division will instead be written as a function, math.div()
. Eventually, it
will also be possible to write Sass-compatible division in calc()
expressions;
however, this is not going to be implemented immediately and is outside the
scope of this proposal.
This is a major breaking change to existing Sass semantics, so we'll roll it out in a three-stage process:
-
The first stage won't introduce any breaking changes. It will:
-
Add a
math.div()
function which will work exactly like the/
operator does today, except that it will produce deprecation warnings for any non-number arguments. -
Add slash-separated lists to Sass's object models, without a literal syntax for creating them. That will come later, since it would otherwise be a breaking change.
-
Add a
list.slash()
function that will create slash-separated lists. -
Produce deprecation warnings for all
/
operations that are interpreted as division.
-
-
The second stage will be a breaking change. It will:
-
Make
/
exclusively a list separator. -
Make
math.div()
throw errors for non-number arguments. -
Deprecate the
list.slash()
function, since it will now be redundant.
-
-
The third stage will just remove the
list.slash()
function. This is not a priority, and will be delayed until the next major version release.
Alternatives Considered
This section is non-normative.
Different Operator Syntax
One other possible fix would be to change the syntax for division to another punctuation-based operator. We ended up deciding that anything we chose would be so different from every other programming language as to be unreadable for anyone unfamiliar with the language.
In addition, the best candidate operator we found was ~
, as an ASCII character
that wasn't already in use in CSS or Sass value syntax. But ~
is difficult to
type on many non-English keyboard layouts, which makes it only marginally more
efficient to write than a function call for many Sass users.
First-Class calc()
We eventually want to add native Sass support for parsing calc()
expressions,
resolving them at compile-time if possible, and producing a new Sass value that
can have arithmetic performed on it if necessary. This is known as first-class
calc()
, and it would mean that division could be written unambiguously
using /
in the context of a calc()
expression. For example, $width / 2
would be instead written calc($width / 2)
.
However, first-class calc()
is likely to be a very complex feature to design
and implement. Most of the resources available for large-scale language features
are currently focused on the new module system, so it's likely that a full
implementation of first-class calc()
won't land until mid-to-late 2020. And
the full implementation is a prerequisite for even beginning the deprecation
cycle for /
-as-division, which means we probably wouldn't fully support
/
-as-separator for another three to six months after that point. This is just
too much time to wait on giving users a good solution for writing /
-separated
properties.
math()
Syntax
A possible middle ground between calc()
and the current syntax would be using
a special math()
expression as a way of signaling a syntactic context where
/
is interpreted as division without needing to fully support all the edge
cases of calc()
. For example, $width / 2
would be instead written
math($width / 2)
.
Unfortunately, this is highly likely to confuse users. They may think that
math()
is necessary for all mathematical operations, when in fact it's only
necessary for division, which would lead to confusing and unnecessary math()
expressions popping up all over the place. They may also think it's a Sass
library function or a plain CSS feature, neither of which is true, and look in
the wrong place for documentation.
Worse, once we did add support for first-class calc()
, there would then be
two different ways of wrapping mathematical expressions which had slightly but
meaningfully different semantics. This is a recipe for making users feel
confused and overwhelmed.
Existing Behavior
To precisely describe when a deprecation warning should be emitted, we must first describe the existing heuristic behavior.
A Sass number may be potentially slash-separated. If it is, it is associated with two additional Sass numbers, the original numerator and the original 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 either literal
Number
s or ProductExpression
s that can themselves create potentially
slash-separated numbers. In this case, both operands are guaranteed to be
evaluated as numbers. The first operand is the original numerator of the
potentially slash-separated number returned by the /
operator, and the second
is the original denominator.
A potentially slash-separated number is converted to a slash-free number when:
-
It is the value of a
ParenthesizedExpression
.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. -
It is stored in a Sass variable.
-
It is passed into a user-defined 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.
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.
Syntax
Note that the existing productions being modified have not been defined explicitly before this document. The old definitions are listed in strikethrough mode to clarify the change.
This proposal modifies the existing CommaListExpression
production to add
support for slash-separated lists. The new grammar for this production is:
CommaListExpression ::= SpaceListExpression (',' SpaceListExpression)*CommaListExpression ::= SlashListExpression (',' SlashListExpression)* SlashListExpression ::= SpaceListExpression ('/' SpaceListExpression)*
Note that
/
may not be used in single-element lists the way,
is. That is,(foo,)
is valid, but(foo/)
is not.This defines
/
to bind tighter than,
but looser than space-separated lists. This was chosen because most common uses of/
in CSS conceptually bind looser than space-separated values. The only exception is thefont
shorthand syntax, which is used much more rarely will still work (albeit with an unintuitive SassScript representation) with a loose-binding/
.
It also modifies the existing ProductExpression
production to remove /
as an
operator. The new grammar for this production is:
ProductExpression ::= (ProductExpression ('*' | '/' | '%'))? UnaryPlusExpressionProductExpression ::= (ProductExpression ('*' | '%'))? UnaryPlusExpression
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
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 eachSlashListExpression
:-
Let
left
be the first element of the list. -
For each remaining element
right
:-
If
left
andright
are bothSumExpression
s:-
Let
last-left
be the last operand ofleft
andfirst-right
the first operand ofright
. -
Set
left
to aSumExpression
that begins with all operands and operators ofleft
exceptlast-left
, followed by aSlashListExpression
with elementslast-left
andfirst-right
, followed by all operators and operands ofright
exceptfirst-right
.For example,
slash-list(1 + 2, 3 + 4)
becomes1 + (2 / 3) + 4
.
-
-
Otherwise, if
left
is aSumExpression
:-
Let
last-left
be the last operand ofleft
. -
Set
left
to aSumExpression
that begins with all operands and operators ofleft
exceptlast-left
, followed by aSlashListExpression
with elementslast-left
andright
.For example,
slash-list(1 + 2, 3)
becomes1 + (2 / 3)
.
-
-
Otherwise, if
right
is aSumExpression
or aProductExpression
:-
Let
first-right
be the first operand ofright
. -
Set
left
to an expression of the same type asright
that begins aSlashListExpression
with elementsleft
andfirst-right
, followed by operators and operands ofright
exceptfirst-right
.For example,
slash-list(1, 2 * 3)
becomes(1 / 2) * 3
.
-
-
Otherwise, if
left
is a slash-separated list, addright
to the end. -
Otherwise, set
left
to a slash-separated list containingleft
andright
.
-
-
Replace each element in
left
with the result of adjusting slash precedence in that element. -
Replace the
SlashListExpression
withleft
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 evaluatingelement
as a calculation value. -
Set
left
to aCalcOperation
with operator"/"
,left
, andright
.
-
-
Return
left
.
Semantics
Slash-Separated Lists
A new list separator, known as "slash", will be added. The string "slash"
may
be passed to the $separator
argument of append()
and join()
, and may be
returned by list.separator()
. When converted to CSS, slash-separated lists
must have exactly one /
between each adjacent pair of elements.
Although CSS doesn't currently make use of this syntax, there's nothing stopping a list from being both bracketed and slash-separated.
math.div()
Function
The div()
function in the sass:math
module has the following signature:
math.div($number1, $number2)
It throws an error if either argument is not a number. If both are numbers, it
returns the same result that the /
operator did prior to this proposal.
list.slash()
Function
The slash()
function in the sass:list
module has the following signature:
list.slash($elements...)
It throws an error if zero or one arguments are passed. It returns an unbracketed slash-separated list containing the given elements.
rgb()
Function
This proposal modifies the existing behavior of the rgb($channels)
overload to be the following:
-
If
$channels
is a special variable string, return a plain CSS function string with the name"rgb"
and the argument$channels
. -
If
$channels
is an unbracketed slash-separated list:-
If
$channels
doesn't have exactly two elements, throw an error. Otherwise, letrgb
be the first element andalpha
the second element. -
If either
rgb
oralpha
is a special variable string, return a plain CSS function string with the name"rgb"
and the argument$channels
. -
If
rgb
is not an unbracketed space-separated list, throw an error. -
If
rgb
has more than three elements, throw an error. -
If
rgb
has fewer than three elements:-
If any element of
rgb
is a special variable string, return a plain CSS function string with the name"rgb"
and the argument$channels
. -
Otherwise, throw an error.
-
-
Let
red
,green
, andblue
be the three elements ofrgb
. -
Call
rgb()
withred
,green
,blue
, andalpha
as arguments and return the result.
-
-
Otherwise, proceed with the existing definition of the function.
This ensures that calling (for example)
rgb(0 0 100% / 80%)
will continue to work when/
is parsed as a slash separator. It's important to define this runtime behavior in phase 1 so that users manually constructing slash-separated lists can use them as expected.
hsl()
Function
This proposal modifies the existing behavior of the hsl($channels)
overload to be the following:
-
If
$channels
is a special variable string, return a plain CSS function string with the name"hsl"
and the argument$channels
. -
If
$channels
is an unbracketed slash-separated list:-
If
$channels
doesn't have exactly two elements, throw an error. Otherwise, lethsl
be the first element andalpha
the second element. -
If either
hsl
oralpha
is a special variable string, return a plain CSS function string with the name"hsl"
and the argument$channels
. -
If
hsl
is not an unbracketed space-separated list, throw an error. -
If
hsl
has more than three elements, throw an error. -
If
hsl
has fewer than three elements:-
If any element of
hsl
is a special variable string, return a plain CSS function string with the name"hsl"
and the argument$channels
. -
Otherwise, throw an error.
-
-
Let
hue
,saturation
, andlightness
be the three elements ofhsl
. -
Call
hsl()
withhue
,saturation
,lightness
, andalpha
as arguments and return the result.
-
-
Otherwise, proceed with the existing definition of the function.
Selector Functions
This proposal modifies the "Parse a Selector From a SassScript Object" procedure to throw an error whenever it encounters a slash-separated list.
Slash-Free Numbers
This proposal adds one additional scenario in which potentially slash-separated numbers are converted into slash-free numbers:
- When a number is passed to a built-in function or mixin.
This change makes built-in functions/mixins consistent with user-defined ones, which do make their arguments slash-free. It also combines with Phase 1 of the deprecation process to ensure that all uses of
/
-as-division will produce warnings.This could potentially be a breaking change. While most functions that could take potentially slash-separated numbers will either ignore the slash-separation or return the number and cause it to become slash-free that way, it's possible for a user to pass it to a function that puts it in a data structure, as in
list.join(1/2, ())
which returns a single-element list containing a potentially slash-separated number. However, this breakage is considered exceedingly unlikely and it's easy to work around usinglist.slash()
so we aren't considering it a blocker.
Deprecation Process
The deprecation process will be divided into three phases:
Phase 1
This phase will add no breaking changes, and will be implemented as soon as
possible. Its purpose is to notify users that /
-as-division will eventually be
removed and give them alternatives to migrate to that will continue to work when
/
's behavior is changed.
Phase 1 implements none of the syntactic changes described above. It
implements all the semantics, with the exception that math.div()
allows non-number arguments. If either argument is not a number, it emits a
deprecation warning.
If either argument is not a number,
math.div()
still returns the same result as the/
operator, which in that case will be concatenating the two arguments into an unquoted string separated by/
. This behavior is supported for the time being to make it easier to automatically migrate users tomath.div()
without causing runtime errors.
While phase 1 will continue to support /
as a division operator, the use of
the operator in this way will produce a deprecation warning. Specifically, a
deprecation warning is emitted when a potentially slash-separated
number is converted to a slash-free number, or when a /
operation returns a slash-free number.
In phase 1, we recommend authors write division and slash-separated lists like so:
@use 'sass:list; @use 'sass:math'; $ratio: math.div(12rem, 1px); $row: list.slash(span 3, 6); .grid .item1 { // It's always safe to use `/` as a separator directly in a CSS property. grid-row: span 2 / 7; }
Phase 2
This phase will introduce breaking changes to the language. It implements both
the syntactic changes and the semantic changes exactly
described above (so math.div()
will only accept numbers). In phase 2, the
list.slash()
function will emit a deprecation warning whenever it's called.
It's recommended that implementations increment their major version numbers with the release of phase 2, in accordance with semantic versioning.
In phase 2 and 3, we recommend authors write division and slash-separated lists like so:
@use 'sass:math'; $ratio: math.div(12rem, 1px); // As of phase 2, `/` is always parsed as a slash-separated list. $row: span 3 / 6; .grid .item1 { grid-row: span 2 / 7; }
Phase 3
This phase will introduce a final breaking change, removing the now-unnecessary
list.slash()
function.
It's recommended that implementations increment their major version numbers again with the release of phase 3.
Timeline
-
Phase 1 was originally scheduled to be implemented by Dart Sass as soon as the proposal was accepted. However, it was delayed considerably in the hope that it would also be implemented by LibSass. Since LibSass is now deprecated, we plan to release Phase 1 in Q2 2021.
-
Phase 2 will be released in Dart Sass 2.0.0. There's no solid release date for this yet, and it may or may not be concurrent with the removal of support for
@import
depending on how quickly the module system is adopted and how urgent the need for a syntactic slash separator becomes. -
Phase 3 will be released in Dart Sass 3.0.0, whenever that ends up happening. Removing
list.slash()
is not considered a priority, so this will wait until we have additional, more-compelling breaking changes we want to release.