sass/accepted/supports-general.md
Natalie Weizenbaum 54b9bbed50
[Supports General] Disambiguate SupportsCondition/SupportsAnything (#2938)
Within parentheses, a production like `#{$value} and ...` could be
parsed as either branch, but *should* be parsed as a
SupportsCondition.

See #2780
2020-11-04 14:26:39 -08:00

8.2 KiB

@supports <general-enclosed>: Draft 2.1

(Issue, Changelog)

This proposal defines how Sass parses supports queries that use the <general-enclosed> production.

Table of Contents

Background

This section is non-normative.

Historically, Sass has supported the @supports condition syntax as defined in the April 2013 CSS Conditional Rules Level 3 Candidate Recommendation, with the addition of supporting raw SassScript expressions in the declaration syntax. The Editor's Draft (as published October 2019) expands this syntax with a <general-enclosed> production to ensure parser forwards-compatibility with future CSS, which Sass does not yet support. The Level 4 Editor's Draft (as of March 2019) further adds a selector() function syntax (which is covered by <general-enclosed>), which is supported by Firefox as of version 69.

Sass's current @supports rule syntax allows SassScript expressions in place of either the declaration name or value in <supports-decl>. This syntax doesn't need to be wrapped in interpolation, which means that many expressions that would be parsed in CSS as <general-enclosed> are currently parsed by Sass as <supports-decl>, and even more expressions need arbitrary look-ahead to determine whether they have a : that would distinguish them between <supports-decl> and <general-enclosed>.

Summary

This section is non-normative.

Sass will parse the <general-enclosed> production essentially as defined by CSS, with the following exceptions:

  • SassScript can be injected in the productions using interpolation.

  • The (<ident> <any-value>) syntax for <general-enclosed> may not contain top-level ":" tokens. This preserves backwards-compatibility with existing @supports rules that use non-interpolated SassScript expressions in <supports-decl> and may help catch accidental syntax errors.

Design Decisions

How Much To Support

Per Sass's CSS compatibility policy, it must add support for at least the selector() function, since it's been shipped in a browser. Everything else under <general-enclosed> is optional, though, since the spec says "Authors must not use <general-enclosed> in their stylesheets." We could choose to avoid implementing it entirely and just special-case selector(), or we could choose to implement only the function syntax and ignore the (<ident> <any-value>) syntax.

This proposal covers the full <general-enclosed> syntax for the same reason CSS does: forwards compatibility. Sass has a general policy of loose coupling with CSS, so that Sass needs as few updates as possible as CSS continues to evolve. Supporting the full range of potential syntax for @supports conditions now means that we'll need fewer proposals like this in the future as new conditions are added in practice.

SassScript Injection

There are two possible ways to allow SassScript to be injected into the <general-enclosed> production. One is to support it only via interpolation, allowing the production to otherwise exactly match the CSS syntax; the other is to allow raw Sass-script values to be used within the parentheses.

The latter option is appealing at first glance. It requires fewer characters, and matches the parsing of <supports-decl>, which allows non-interpolated expressions on either side of the ":". However, it's only CSS-compatible as long as all the syntax CSS uses in those positions is also a valid SassScript expression. If not, Sass must continually update its syntax to support new CSS constructs.

We've been burned by this in the past. Both this proposal and the media ranges proposal would have been unnecessary (or at least much simpler) if the rules in questions had only allowed SassScript via interpolation in the first place. And it's likely that this syntax will evolve in ways that aren't SassScript-compatible, as with the selector() function mentioned above.

Given this, it's much safer to only allow SassScript within interpolation.

Backwards Compatibility

The <general-enclosed> syntax is very broad, so care is needed to ensure that adding support for it doesn't break existing Sass stylesheets—particularly those that are relying on SassScript in <supports-decl>. For example, strictly speaking (foo + bar: baz) is valid CSS that parses as <general-enclosed>, but Sass needs to continue to parse it as a declaration containing a SassScript expression on the left-hand side.

The question then is how to balance backwards-compatibility with CSS compatibility. In most cases, CSS takes clear precedence, and we could manage that here by declaring that <supports-decl>s that begin with an InterpolatedIdentifier must immediately follow it with a ":" and allowing all other syntax to be parsed as <general-enclosed>. However, this approach is not without its downsides. It would require a deprecation period, and it would parse <general-enclosed> in a number of cases (such as (foo: ) or (fo o: bar)) that are much more likely to be typos.

Fortunately, <general-enclosed> is explicitly specified for forwards-compatibility rather than having CSS semantics in and of itself, so we have more flexibility to limit compatibility with it in favor of Sass backwards compatibility. This allows us to limit the syntax we parse as <general-enclosed> to forbid a top-level ":", which ensures it's never ambiguous with a declaration and thus with raw SassScript.

Syntax

SupportsCondition

SupportsCondition   ::= 'not' SupportsInParens
                      | SupportsInParens ('and' SupportsInParens)*
                      | SupportsInParens ('or' SupportsInParens)*
SupportsInParens    ::= '(' (SupportsCondition | SupportsDeclaration | SupportsAnything) ')'
                      | SupportsFunction | Interpolation
SupportsDeclaration ::= Expression¹ ':' Expression
SupportsAnything    ::= InterpolatedIdentifier² InterpolatedAnyValue³?
SupportsFunction    ::= InterpolatedIdentifier⁴ '(' InterpolatedAnyValue? ')'

1: This Expression may not begin with the identifier "not" or the token "(".

2: This InterpolatedIdentifier may not be the identifier "not".

3: This InterpolatedAnyValue may not contain a top-level ":", and it may not begin with the identifier tokens "and" or "or".

4: This InterpolatedIdentifier may not be the identifier "not". No whitespace is allowed between it and the following "(".

The identifiers "not", "and", and "or" are matched case-insensitively for the purposes of this production.

Implementations must perform some amount of lookahead to disambiguate between SupportsDeclaration, SupportsAnything, and SupportsFunction. When doing so, it may be helpful to note that the contents of a SupportsInParens must be parsed as a SupportsDeclaration if and only if it contains a top-level ":".

InterpolatedAnyValue

The InterpolatedAnyValue production is identical to CSS's <any-value> except that after it parses "#{", it parses an Expression which must be followed by "}".