mirror of
https://github.com/sass/sass.git
synced 2024-09-21 18:47:25 +00:00
130 lines
5.2 KiB
Markdown
130 lines
5.2 KiB
Markdown
# Extend Specificity
|
|
|
|
It's valuable to be able to optimize away selectors generated by `@extend` if
|
|
they match subsets of the elements matched by other selectors in the same style
|
|
rule. However, optimizing *every* such selector can end up having unexpected
|
|
consequences when it changes the specificity with which the style rule applies
|
|
to a given element. This proposal lays out restrictions on the specificity of
|
|
selectors that result from an `@extend`.
|
|
|
|
First of all, let's define the function `extend(S, A, B)` to be the result of
|
|
taking a selector `S` and extending it by replacing all instances of `A` with
|
|
`A, B` and resolving the result a la `@extend`. Here are some uncontroversial
|
|
examples:
|
|
|
|
```
|
|
extend(a, a, b) = a, b
|
|
extend(a.foo, a, b) = a.foo, b.foo
|
|
extend(c, a, b) = c
|
|
```
|
|
|
|
## 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
|
|
result as `a.foo, a`. However, `a` matches a strict superset of the elements
|
|
that `a.foo` matches, so another interpretation could give the result as just
|
|
`a`. `a` and `a.foo, a` are semantically identical **except** for specificity.
|
|
|
|
Let's define a new function to talk about this: `spec(S)` is the specificity of
|
|
a selector `S`. So `spec(a.foo) = 11`, while `spec(a) = 1`. The nature of CSS
|
|
means that differences in specificity can lead to practical differences in
|
|
styling, so to some degree we clearly need to consider specificity as part of
|
|
the semantics of the selectors we deal with. This is the broad point of this
|
|
issue.
|
|
|
|
Let's get back to the example of `extend(a.foo, .foo, a)`. The first selector in
|
|
the result, `extend(a.foo, .foo, a)[0]`, corresponds to the selector written by
|
|
the user with the goal of directly styling a set of elements. Allowing the
|
|
specificity of this selector to change because an `@extend` was added elsewhere
|
|
in the stylesheet is semantic change at a distance, which is clearly something
|
|
we shouldn't allow. Thus, it should be the case that
|
|
`extend(a.foo, .foo, a)[0] = a.foo` and in general that
|
|
`spec(extend(S, A, B)[0]) >= spec(S)`.
|
|
|
|
In most cases, the first generated selector should be identical to `S`. However,
|
|
this isn't possible when dealing with the `:not()` pseudo-selector. For example,
|
|
|
|
``` scss
|
|
:not(.foo) {...}
|
|
.bar {@extend .foo}
|
|
```
|
|
|
|
Because `:not` specifically declares selectors that the rule **doesn't** apply
|
|
to, extending those selectors will necessarily increase the specificity of the
|
|
base selector. The example above should compile to
|
|
|
|
``` css
|
|
:not(.foo):not(.bar) {...}
|
|
```
|
|
|
|
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)`
|
|
|
|
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
|
|
|
|
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
|
|
well. In order to allow our users to reason about the styling of their page, the
|
|
specificity of the generated selectors should clearly be as consistent as
|
|
possible. In an ideal world, if `@extend` were supported natively in the
|
|
browser, the specificity would be equivalent to that of the original selector;
|
|
that is, `spec(extend(S, A, B)[1]) = spec(S)`. However, that's not always
|
|
possible:
|
|
|
|
```
|
|
extend(a, a, b.foo) = a, b.foo
|
|
spec(a) < spec(b.foo)
|
|
extend(a.foo, a.foo, b) = a.foo, b
|
|
spec(a.foo) > spec(b)
|
|
```
|
|
|
|
Since consistency is desirable, we might be tempted instead to say that
|
|
`spec(extend(S, A, B)[1]) = spec(B)`. But that's not always possible either:
|
|
|
|
```
|
|
extend(a.foo, a, b) = a.foo, b.foo
|
|
spec(b) < spec(b.foo)
|
|
```
|
|
|
|
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)`
|
|
|
|
## 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
|
|
output size, but we don't want these optimizations to break the guarantees we
|
|
offer our users. Which optimizations do the guarantees outlines above allow us,
|
|
and which do they forbid?
|
|
|
|
One optimization that we've been doing for a long time is
|
|
`extend(a.foo, .foo, a) = a`, as discussed above. This violates the first law,
|
|
since `a != a.foo`.
|
|
|
|
Another optimization added in [8f4869e][] is `extend(a, a, a.foo) = a`. This
|
|
violates the second law, since `spec(a) < spec(a.foo)`.
|
|
|
|
[8f4869e]: https://github.com/sass/ruby-sass/commit/8f4869e608e70d7f468bb463ebfe7a939d834e27
|
|
|
|
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
|
|
|
|
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
|
|
they have without any optimizations. That's my proposal: that we support all the
|
|
optimizations we can while still abiding by the two Laws of Extend outlined
|
|
above.
|