Home

Sass Blog

  • 
  • 
  • Cleaning Up Interpolation

    Posted by Natalie Weizenbaum over 2 years ago

    Interpolation—the ability to add variables and other snippets using #{...}—is one of the handiest all-purpose features of Sass. You can use it just about everywhere you might need to inject a variable, a function call, or some other expression. In most of those places it just plops the value into the surrounding text. It's straightforward, easy to understand, and useful, which is exactly what we want from a feature.

    Unfortunately, that's only true in most places. For complicated historical reasons, there's one place where interpolation goes a little bit bananas: inside an expression but outside quotes. Most of the time, it makes sense; if you write display: -#{$prefix}-box, you'll get what you expect. But if any operators like + are used next to the interpolation, you start to get weird output. For example, $name + #{$counter + 1} might return an unquoted string containing the text name + 3.

    This is really weird behavior. Why does + behave differently here than it does everywhere else? Why is it treated as plain text when $name gets evaluated normally? This behavior is confusing, inconsistent, and not particularly useful, which are very much not things we want in a feature. So why do they exist in the first place?

    Complicated Historical Reasons

    If you don't care for a history lesson, skip on down to A Brave New World.

    Way back in the dawn of time, when the indented syntax was the only syntax, Sass had a distinction between "static" and "dynamic" properties. A static property was basically plain CSS; it was declared using property: value, and the value was used as-is without any further processing. If you wanted to use a variable or a function, you had to use a dynamic property, which was declared using property= value. A You'd see a lot of stylesheets like this:

    .border
    
      border-width: 4px
    
      border-style: solid
    
      border-color= !background_color
    

    Also, in the dawn of time, variables used ! instead of and couldn't include hyphens. The dawn of time kind of sucked. But it was in this context that we first added interpolation. We wanted to allow properties like border with multiple values to be partially dynamic, so we decided to follow in Ruby's footsteps and allow #{} to be used to drop in values. Soon stylesheets started looking like this:

    .border
    
      border: 4px solid #{!background_color}
    

    That's so much better! And for a while, all was calm.

    Then Came SCSS

    It eventually became clear that users really strongly wanted their stylesheets to look like CSS, so we sat down and started work on the syntax that would become SCSS in the release that would become Sass 3. As part of this work, we decided to get rid of the distinction between static and dynamic properties altogether. Having all properties work the same way was obviously great for users, but it meant we had to figure out how to merge the two syntaxes with a minimum of pain.

    This was mostly straightforward, since the old expression syntax was pretty much universally invalid CSS or something that emitted its CSS value anyway. But interpolation proved tricky. Backwards compatibility is really important to us, so we wanted to be sure that all the places interpolation was used—or could theoretically be used—in Sass 2 would continue to work in Sass 3, even though everything around them was now fully parsed.

    Our solution was to make basically anything around #{} that wasn't obviously part of a plain-CSS expression turn into a string. That way, hopefully any weird corner cases that people had would keep working when they upgraded. This led to the weird behavior I described above, but at the time our top priority was making it as easy as possible for users to migrate to Sass 3. We decided the weirdness was worth it, and shipped it.

    A Brave New World

    Flash forward to today. We're now starting work on the next major release, Sass 4, and (I dearly hope) no one's written any Sass 2 stylesheets in years. A major release is a great opportunity to clean up this bit of historical cruft, and after discussing it extensively on the issue tracker we decided to make the change.

    There are three major steps in a backwards-incompatible change like this. The first is to design the new syntax, which was pretty easy here, since it's basically just "do what everyone thought it did already." We just had to take that general notion and suss out the specifics.

    We ended up framing it as #{} being, syntactically, part of an identifier. When you write -#{$prefix}-box, Sass parses it as a single identifier containing "-" followed by the value of $prefix followed by "-box". Even if you write #{$font} all on its own, it's parsed as an identifier that only contains the value of $font. This way, interpolation doesn't have weird behavior around operators any more than identifiers ever did.

    Once we had a design, the second step was to deprecate the old behavior. The meat of deprecation is figuring out when to print a warning, and that was pretty tough here. We didn't want to warn for situations that would continue to work, even when they involved operators—for example, 12px/#{$line-height} will print the right thing in the old and new worlds (although for slightly different reasons), but 12px+#{$line-height} won't.

    I won't go into the gory details of how we got deprecation working here; that's what the GitHub issue is for. Suffice it to say that it involved a lot of special cases, including some where a deprecation warning can be printed based on how a value is used rather than how it's written. I'm pretty happy with where it ended up, though; I suspect it'll catch 99% of cases that will actually break in practice.

    Another exciting bonus was the ability to automatically update code. This doesn't always work when introducing backwards-incompatibilities, but in this case we were able to make sass-convert convert deprecated uses of interpolation into Sass 4-compatible code. It has some false negatives—it only converts cases it can prove will be incompatible—but it's enough to get users a long way there.

    The final step once the deprecation was in place was to move to the master branch (which will eventually become Sass 4), rip out all the old behavior, and implement the new. And it was wonderful. Deleting gross code and replacing it with something clean feels like taking a shower after spending a day hiking through dust under a hot sun. And after working on this feature for weeks, I was happy to see the other end of it.

    Checking it Out

    Sass 3.4.20, released today, was the first release to include the deprecation warnings for the old syntax. If you want to check whether you've got any deprecated interpolations lurking in your stylesheets, just gem install sass and recompile your stylesheet. And if you do find some, try running sass-convert --recursive --in-place . to fix a bunch automatically.

    If you want to try out the new syntax, 4.0.0.alpha.1 was also released today. You can get it with gem install sass --prerelease. But beware: it is alpha software, so it may change in the future. We generally try to keep even our prereleases pretty stable, but there's also a chance you'll run into a bug.

    If you do find a bug, please file it on the issue tracker. Even if it's something as simple as a typo, we want to know. If we've deprecated something that should be valid, we especially want to know. And if you just have a question, feel free to tweet at @SassCSS or post it on the mailing list.

  • Sass 3.4 is Released

    Posted by Natalie Weizenbaum over 3 years ago

    We've been trying to increase the pace of Sass releases, and it looks like we've succeeded. A mere five months after the release of Sass 3.3, we're announcing the release of Sass 3.4.0, codename Selective Steve. Faster releases mean fewer major features per release, so there are only two big new things to talk about (although there are plenty of little improvements you can read about in the changelog). As the version name suggests, both of these features have to do with selectors.

    Using & in SassScript

    "SassScript" is what we call the mini-language Sass uses for variables, property values, and so forth. It's mostly just CSS values, but it also supports custom functions, arithmetic, and so forth. In Sass 3.4, we added support for something new: the parent selector, &.

    Most Sass users will probably recognize & from its previous appearances in selectors around the world, where it's used to explicitly refer to the parent selector. For example, in .parent { .child & { ... } }, & refers to .parent, and this compiles to .child .parent { ... }.

    Now & works much the same way in SassScript. It refers to the same parent selector, but instead of just being dropped in it's exposed as a list of lists to make it easy for functions to inspect and manipulate it. For example, if you write .foo .bar, .baz { $selector: & }, $selector will be ((".foo" ".bar"), (".baz",)).

    We had originally slated this feature for version 3.3, but we took it out when we realized it was really hard to use these selectors in a way that didn't break when they contained commas. Because of that, we decided to delay it for a version to give us time to come up with its compantion feature: selector functions.

    Selector Functions

    The problem with just exposing & was that the only way to use it with other selectors was by glomming them together as strings. This works okay in simple cases, but when you write #{$selector} .child and $selector is .foo, .bar, you want .foo .child, .bar .child but you get .foo, .bar .child. This is no good at all.

    To solve this, we added a slew of functions that use Sass's powerful built-in selector logic to do the right thing. For example, you can now write selector-nest(".foo, .bar", ".child") and get exactly what you want. These functions all return the same sort of nested-list representation that & uses,but they're very liberal in what they accept: anything from nested lists to plain old strings.

    If you want to see every selector function we thought up, check out the changelog. I do want to highlight a few that I'm particularly fond of, though. You've already seen selector-nest(), and selector-append() is a close relative. The difference between them is whitespace: selector-nest() adds a space between its selectors, where selector-append() doesn't. This means that selector-append(".foo, .bar", "-suffix") returns .foo-suffix, .bar-suffix.

    Another function I like a lot is selector-replace(). This does a search-and-replace of one selector within another, but it's a lot more clever than your basic string replace. It uses Sass's @extend logic to replace selectors semantically, as though every element matched by the replacement selector was also matched by the replaced selector. For example, selector-replace(".foo.bar.baz", ".foo.baz", ".qux") returns .bar.qux.

    The last really powerful function I want to draw your attention to is selector-unify(). This takes two selectors and returns a new selector that matches only elements that are matched by both input selectors. This is an operation Sass uses a lot internally, and now users can access it as well. For example, selector-unify(".foo.bar", ".bar.baz") will return .foo.bar.baz.

    What's Next?

    I won't rule out the possibility of Sass 3.5 existing, but Chris and I plan to focus pretty hard on Sass 4.0. The big feature for 4.0 is going to be @import, or rather the lack thereof. Our current import system is beginning to show its age in a major way, and we intend to replace it wholesale, up to and including the name. As of 4.0, the recommended way of pulling in other Sass files will be @use.

    Among the features we're planning for @use are two that have been widely requested. You'll be able to import CSS stylesheets directly into your Sass ones, and each stylesheet will only be run once, no matter how many times it's imported.

    Until then, though, run gem update sass and enjoy Selective Steve!

  • Sass 3.3 is Released

    Posted by Natalie Weizenbaum about 4 years ago

    After ironing out a bunch of bugs in numerous release candidates, we're finally ready to release Sass 3.3.0, codename Maptastic Maple, for public consumption. This release has a lot of exciting new features that you can read about in full in the changelog, but there are three that I want to draw your attention to in particular.

    Maps in SassScript

    As language designers, most of our job is to listen to feedback from users and act upon it. This is tricker than it sounds: users are very good at knowing the precise thing that they want to accomplish, but they tend not to have a sense of how that fits into the big picture. So we take a large volume of user requests, try to distill the core needs that aren't being met, and see if we can come up with features that hit as many of those as possible as simply as possible.

    SassScript maps are a great example of this. We had a lot of users requesting things like variable interpolation, so they could write things like $#{$theme-name}-background-color. Other users wanted built-in functions that worked with lists of pairs, or a way to get the name of a variable that was passed to a function. We eventually realized the underlying feature that people actually wanted: a way to associate values with names.

    Most programming languages have a notion of maps1, which are associations from "key" objects to "value" objects. Sass 3.3 adds support for these as a first-class data structure. The syntax is designed to be very similar to that used for @media queries. They look like this:

    $map: (key1: value1, key2: value2, key3: value3);
    

    Unlike lists, maps must always be surrounded by parentheses. (), which previously referred to an empty list, now also refers to an empty map; both list and map operations will work on it.

    Maps can't be used as CSS values, since they aren't valid CSS syntax. However, there are a number of new built-in functions that allow user-defined mixins and functions to use them. Here are a few particularly useful ones:

    • map-get($map, $key) looks up a value in a map using its key. For example, using the example above, map-get($map, key2) would return value2.

    • map-merge($map1, $map2) merges two maps together. The keys in $map2 overwrite those in $map1, so this is also a good way to add values to a map. For example, map-merge($map, (key1: new-value)) would return (key1: new-value, key2: value2, key3: value3).

    • map-remove($map, $key) removes a value in a map. For example, map-remove($map, $key) would return (key: value2, key3: value3).

    In addition to the new map functions, all the existing list functions also work on maps. The list functions will see each map as a list of pairs. For example, nth($map, 1) will return (key1 value1). Not only that, but @each has new syntax for working with both maps and lists of pairs. For example:

    @each $header, $size in (h1: 2em, h2: 1.5em, h3: 1.2em) {
      #{$header} {
        font-size: $size;
      }
    }
    

    produces:

    h1 {
      font-size: 2em;
    }
    
    h2 {
      font-size: 1.5em;
    }
    
    h3 {
      font-size: 1.2em;
    }
    

    Source Maps

    Continuing the map theme, Sass 3.3 comes with support for generating source maps when compiling to CSS. Source maps are a standard format for telling browsers how files they consume got generated. For Sass, this means that browsers' development tools can now tell you exactly which line of your Sass source file each style rule came from. Currently this is only well-supported in Chrome, but hopefully other browsers will add support soon.

    When compiling Sass from the command line, all you need to do to generate source maps is pass the --sourcemap flag. Sass will automatically generate a .css.map file next to the generated .css file. All you have to do then is make sure your .scss or .sass file is visible to the browser, and you'll be good to go.

    More Flexible &

    When we released Sass 3.02, we added support for SCSS, which meant we had to actually parse all the selectors in the document. This meant that you couldn't just plop the parent selector, &, anywhere in a selector. Overall this was an improvement: it caught more errors and encouraged users to write more flexible mixins.

    Unfortunately, it also made one important use-case harder. With the rise in popularity of BEM, OOCSS, and SMACSS, people became more and more interested in adding suffixes to classes. When using Sass, they wanted to write mixins to do this, and the restrictions on & made that very hard to do.

    In Sass 3.3, we're loosening these restrictions. You can now write &-suffix (or &_suffix, or even &suffix if you really want) and Sass will make it work. If this fails to apply—for example, if & is *—Sass will print a helpful error message.

    Deprecation: Variable Scope and !global

    We don't always get everything right the first time, and in order to make Sass the best language it can be we occasionally have to change old behavior. Sometimes this happens in ways that might make existing stylesheets stop functioning, so we have a policy of printing warnings for stylesheets that are going to change in the future.

    Sass 3.3 adds a number of deprecations, but the biggest one by far has to do with the way variable scope works. Up until now, when you wrote $var: value in a function, mixin, or CSS rule in Sass, it could do one of two things. If there was a global variable named $var, it would overwrite that variable. Otherwise, it would create a local variable that was only visible within the current set of curly braces.

    This was a pretty big problem, since any given variable assignment could potentially be modifying a variable that it had no way of knowing existed. We want to migrate to a better system where assigning to a variable in a local scope won't overwrite a global variable unless the assignment explicitly says to do so, as in $var: value !global.

    In order to avoid breaking existing stylesheets, we haven't made this change yet. Instead, if a global variable is overwritten by a local declaration, we now print a deprecation warning suggesting that the user add !global. Right now, !global doesn't do much other than make the warning go away, but in a future release it will work as I described above.

    That's All

    Actually, there's a lot more, but that's all I have room for in this post. If you want to see the full assortment of new features, check out the changelog. You can also play with the new features on SassMeister or on your own computer by running gem update sass. Enjoy!


    1. Some languages call them "hashes", "dictionaries", or "associative arrays". JavaScript calls them "objects" for weird historical reasons. ↩

    2. Please forgive the awful design of my old blog. ↩

  • A Change in Plans for Sass 3.3

    Posted by Natalie Weizenbaum over 4 years ago

    This was originally published as a gist.

    Sass 3.3 is coming soon, and along with it several major new features. It supports source maps, SassScript maps, and the use of & in SassScript. In preparation for its release, we've put out a couple of release candidates to be sure that everything was set and ready to go. Unfortunately, it wasn't.

    Release candidates often turn up small bugs and inconsistencies in new features, but it's rare that they find anything truly damning. In this case, though, several users noticed an issue with using & in SassScript that rendered a sizable chunk of our plan for that section of 3.3 unworkable. It's not a fatal issue, and we think we have a good plan for dealing with it (I'll get to that in a bit), but it is a problem.

    The Background

    To understand what's wrong, first you need to understand the reason we decided to make & accessible to SassScript in the first place. One thing users want to do pretty often is to add suffixes to classes. Sometimes this takes the place of nesting selectors, sometimes it's just to make a new class based on the old ones -- the reason doesn't matter much to this discussion. When people tried to do this, they'd write something like .foo { &-suffix { ... } }, and it wouldn't work. The reason is that & has the same syntactic function as a type selector (e.g. h1) or a universal selector (*), since it could be replaced by any of those. It doesn't make sense to write *-suffix in a selector, so &-suffix wasn't allowed either.

    This didn't stop people from wanting to do it, though. So we decided, "all right, we already use interpolation (#{}) to support injecting text into selectors -- let's just use that". We decided to add & as a sort of special variable in SassScript that contained a parsed representation of the current selector. You could then mimic &-suffix by doing @at-root #{&}-suffix instead1. Life was peachy, until our intrepid users discovered the problem.

    The Problem

    Here's a small snippet of SCSS that demonstrates the issue. See if you can figure it out:

    .foo, .bar {
      @at-root #{&}-suffix {
        color: blue;
      }
    }
    

    Did you get it? That's right: & in this example is .foo, .bar, which means the selector compiles to .foo, .bar-suffix. Since #{} injects plain old text, there's no chance for Sass to figure out how it should split up the selector.

    Chris and I talked and talked about how to fix this. We considered adding a function to add the suffix, but that was too verbose. We considered making & split the compilation of the CSS rule into several parallel rules which each had a single selector for &, but that was too complicated and fell down in too many edge cases. We eventually concluded that there was no way for SassScript & to cleanly support the use case we designed it for.

    The Solution

    We knew we wanted to support the &-suffix use case, and our clever plan for doing so had failed. We put our heads together and discussed, and decided that the best way to support it was the most straightforward: we'd just allow &-suffix. This was, after all, what most people tried first when they wanted this behavior, and with the & embedded directly in the selector, we can handle selector lists easily.

    This means that &-suffix will be supported in Sass 3.3, without needing #{} or @at-root. I've created issue 1055 to track it. When compiling these selectors, if the parent selector is one that would result in an invalid selector (e.g. *-suffix or :nth-child(1)-suffix), we'll throw an error there describing why that selector was generated.

    We are still worried about cases where people write mixins using &-suffix that will then fail to work with certain parent selectors, but in this case we determined that this would be the least of all available evils.

    The Future of & in SassScript

    In addition to supporting &-suffix, we've decided to pull SassScript & from the 3.3 release. Rest assured that it will return -- we recognize that it has other good use cases, and we intend to bring it back for the next big release (likely 3.4). In addition, it will come with a suite of functions for manipulating the selectors it makes available, so it will be more powerful than ever.

    There are two reasons that we want to hold off on using & in SassScript for now. The first is that we want some time to create the functions that will go along with it and put them through their paces. This may require changing the way it works in various ways, and we don't want to have to make backwards-incompatible changes to do so.

    The second reason is that we've spent a fair amount of energy talking up #{&} as a solution to the &-suffix problem. This is our own fault, clearly, but it's true and it's something we need to deal with. Making &-suffix work is great, but if everyone is using #{&} anyway because that's what we told them about a few months ago, then it's not doing everything it can. Having a release where &-suffix works but #{&} doesn't will help guide users towards the best way to solve their problem, before we make the more advanced functionality available.

    @at-root will still be included in Sass 3.3.

    Releasing 3.3

    Unfortunately, this change will delay the release of 3.3, but hopefully not by too much. I anticipate this being relatively straightforward to implement; the major hurdle was figuring out what to do about it, and that part's done. I plan to devote a large chunk of time to getting 3.3 out the door after I come back from winter vacation, so hopefully (no promises) it'll be released some time in January.


    1. The @at-root is necessary since Sass can't reliably figure out whether & was used in the selector like it can when & is used without #{}. ↩

  • How @extend Works

    Posted by Natalie Weizenbaum over 4 years ago

    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.

    Primitives

    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 @extend is 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.set(Set, Object) and 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:

      map.set([1, 2], 'value1')
      map.set([2, 3], 'value2)
      map.set([3, 4], 'value3')
      map.get([1, 2, 3]) => ['value1', 'value2']
      
    • A selector S1 is a "superselector" of a selector S2 if every element matched by S2 is also matched by S1. For example, .foo is a superselector of .foo.bar, a is a superselector of div a, and * is a superselector of everything. The inverse of a superselector is a "subselector".

    • An operation unify(Compound Selector, Compound Selector) => Compound Selector that returns a selector that matches exactly those elements matched by both input selectors. For example, unify(.foo, .bar) returns .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 null.

    • An operation trim([Selector List]) => Selector List that 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 @extend process won't produce intra-list subselectors. For example, if it's passed [[a], [.foo a]] it would return [a] since .foo a is a subselector of a.

    • An operation paths([[Object]]) => [[Object]] that returns a list of all possible paths through a list of choices for each step. For example, paths([[1, 2], [3], [4, 5, 6]]) returns [[1, 3, 4], [1, 3, 5], [1, 3, 6], [2, 3, 4], [2, 3, 5], [2, 3, 6]].

    The Algorithm

    The @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.

    Recording Pass

    In pseudocode, this pass can be described as follows:

    let MAP be an empty subset map from simple selectors to (complex selector, compound selector) pairs
    for each @extend in the document:
      let EXTENDER be the complex selector of the CSS rule containing the @extend
      let TARGET be the compound selector being @extended
      MAP.set(TARGET, (EXTENDER, TARGET))
    

    Transformation Pass

    The transformation pass is more complicated than the recording pass. It's described in pseudocode below:

    let MAP be the subset map from the recording pass
    
    define extend_complex(COMPLEX, SEEN) to be:
      let CHOICES be an empty list of lists of complex selectors
      for each compound selector COMPOUND in COMPLEX:
        let EXTENDED be extend_compound(COMPOUND, SEEN)
        if no complex selector in EXTENDED is a superselector of COMPOUND:
          add a complex selector composed only of COMPOUND to EXTENDED
        add EXTENDED to CHOICES
    
      let WEAVES be an empty list of selector lists
      for each list of complex selectors PATH in paths(CHOICES):
        add weave(PATH) to WEAVES
      return trim(WEAVES)
    
    define extend_compound(COMPOUND, SEEN) to be:
      let RESULTS be an empty list of complex selectors
      for each (EXTENDER, TARGET) in MAP.get(COMPOUND):
        if SEEN contains TARGET, move to the next iteration
      
        let COMPOUND_WITHOUT_TARGET be COMPOUND without any of the simple selectors in TARGET
        let EXTENDER_COMPOUND be the last compound selector in EXTENDER
        let UNIFIED be unify(EXTENDER_COMPOUND, COMPOUND_WITHOUT_TARGET)
        if UNIFIED is null, move to the next iteration
        
        let UNIFIED_COMPLEX be EXTENDER with the last compound selector replaced with UNIFIED
        with TARGET in SEEN:
          add each complex selector in extend_complex(UNIFIED_COMPLEX, SEEN) to RESULTS
      return RESULTS
    
    for each selector COMPLEX in the document:
      let SEEN be an empty set of compound selectors
      let LIST be a selector list comprised of the complex selectors in extend_complex(COMPLEX, SEEN)
      replace COMPLEX with LIST
    

    A keen reader will have noticed an undefined function used in this pseudocode: weave. weave is much more complicated than the other primitive operations, so I wanted to explain it in detail.

    Weave

    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:

    let PAREN_SELECTOR be the argument to weave(), a list of complex selectors
    let PREFIXES be an empty list of complex selectors
    
    for each complex selector COMPLEX in PAREN_SELECTOR:
      if PREFIXES is empty:
        add COMPLEX to PREFIXES
        move to the next iteration
    
      let COMPLEX_SUFFIX be the final compound selector in COMPLEX
      let COMPLEX_PREFIX be COMPLEX without COMPLEX_SUFFIX
      let NEW_PREFIXES be an empty list of complex selectors
      for each complex selector PREFIX in PREFIXES:
        let WOVEN be subweave(PREFIX, COMPLEX_PREFIX)
        if WOVEN is null, move to the next iteration
        for each complex selector WOVEN_COMPLEX in WOVEN:
          append COMPLEX_SUFFIX to WOVEN_COMPLEX
          add WOVEN_COMPLEX to NEW_PREFIXES
      let PREFIXES be NEW_PREFIXES
    
    return PREFIXES
    

    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.

    Where 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.

Previous

Copyright © 2014 nex3 . Theme iaawd by poying