This was originally published as a gist.
Aaron Leung is working on hcatlin/libsass and was wondering how
@extend is implemented in the Ruby implementation of Sass. Rather than just tell him, I thought I'd write up a public document about it so anyone who's porting Sass or is just curious about how it works can see.
Note that this explanation is simplified in numerous ways. It's intended to explain the most complex parts of a basic correct
@extend transformation, but it leaves out numerous details that will be important if full Sass compatibility is desired. This should be considered an explication of the groundwork for
@extend, upon which full support can be built. For a complete understanding of
@extend, there's no substitute for consulting the Ruby Sass code and its tests.
This document assumes familiarity with the selector terminology defined in the Selectors Level 4 spec. Throughout the document, selectors will be treated interchangeably with lists or sets of their components. For example, a complex selector may be treated as a list of compound selectors or a list of lists of simple selectors.
Following are a set of primitive objects, definitions, and operations that are necessary for implementing
@extend. Implementing these is left as an exercise for the reader.
A selector object is obviously necessary, since
@extendis all about selectors. Selectors will need to be parsed thoroughly and semantically. It's necessary for the implementation to know a fair amount of the meaning behind the various different forms of selectors.
A custom data structure I call a "subset map" is also necessary. A subset map has two operations:
Map.get(Set) => [Object]. The former associates a value with a set of keys in the map. The latter looks up all values that are associated with subsets of a set of keys. For example:
S1is a "superselector" of a selector
S2if every element matched by
S2is also matched by
S1. For example,
.foois a superselector of
ais a superselector of
div a, and
*is a superselector of everything. The inverse of a superselector is a "subselector".
unify(Compound Selector, Compound Selector) => Compound Selectorthat returns a selector that matches exactly those elements matched by both input selectors. For example,
.foo.bar. This only needs to work for compound or simpler selectors. This operation can fail (e.g.
unify(a, h1)), in which case it should return
trim([Selector List]) => Selector Listthat removes complex selectors that are subselectors of other complex selectors in the input. It takes the input as multiple selector lists and only checks for subselectors across these lists since the prior
@extendprocess won't produce intra-list subselectors. For example, if it's passed
[[a], [.foo a]]it would return
.foo ais a subselector of
paths([[Object]]) => [[Object]]that returns a list of all possible paths through a list of choices for each step. For example,
paths([[1, 2], , [4, 5, 6]])returns
[[1, 3, 4], [1, 3, 5], [1, 3, 6], [2, 3, 4], [2, 3, 5], [2, 3, 6]].
@extend algorithm requires two passes: one to record the
@extends that are declared in the stylesheet, and another to transform selectors using those
@extends. This is necessary, since
@extends can affect selectors earlier in the stylesheet as well.
In pseudocode, this pass can be described as follows:
The transformation pass is more complicated than the recording pass. It's described in pseudocode below:
A keen reader will have noticed an undefined function used in this pseudocode:
weave is much more complicated than the other primitive operations, so I wanted to explain it in detail.
At a high level, the "weave" operation is pretty easy to understand. It's best to think of it as expanding a "parenthesized selector". Imagine you could write
.foo (.bar a) and it would match every
a element that has both a
.foo parent element and a
.bar parent element.
weave makes this happen.
In order to match this
a element, you need to expand
.foo (.bar a) into the following selector list:
.foo .bar a, .foo.bar a, .bar .foo a. This matches all possible ways that
a could have both a
.foo parent and a
.bar parent. However,
weave does not in fact emit
.foo.bar a; including merged selectors like it would cause exponential output size and provide very little utility.
This parenthesized selector is passed in to
weave as a list of complex selectors. For example,
.foo (.bar a) would be passed in as
[.foo, .bar a]. Similarly,
(.foo div) (.bar a) (.baz h1 span) would be passed in as
[.foo div, .bar a, .baz h1 span].
weave works by moving left-to-right through the parenthesized selector, building up a list of all possible prefixes and adding to this list as each parenthesized component is encountered. Here's the pseudocode:
This includes yet another undefined function,
subweave, which contains most of the logic of weaving together selectors. It's one of the most complicated pieces of logic in the entire
@extend algorithm -- it handles selector combinators, superselectors, subject selectors, and more. However, the semantics are extremely simple, and writing a baseline version of it is very easy.
weave weaves together many complex selectors,
subweave just weaves two. The complex selectors it weaves together are considered to have an implicit identical trailing compound selector; for example, if it's passed
.foo .bar and
.x .y .z, it weaves them together as though they were
.foo .bar E and
.x .y .z E. In addition, it doesn't merge the two selectors in most cases, so it would just return
.foo .bar .x .y .z, .x .y .z .foo .bar in this case. An extremely naive implementation could just return the two orderings of the two arguments and be correct a majority of the time.
Delving into the full complexity of
subweave is out of scope here, since it falls almost entirely into the category of advanced functionality that this document is intentionally avoiding. The code for it is located in
lib/sass/selector/sequence.rb and should be consulted when attempting a serious implementation.