Modular CSS preprocessing with rework

Several months ago I started a project named Rework, a very fast, simple, flexible, and modular CSS preprocessor. The biggest and most obvious question I get is how this tool compares to something like Stylus, LESS, or Sass, and why would you want to use it.

The simple answer is that Rework caters to a different audience, and provide you the flexibility to craft the preprocessor you want, not the choices the author(s) have force on you.

Our goal with Rework contrasts the others, as it does not provide a higher level language within the CSS documents, Rework’s goal is to make those constructs unnecessary, being the most transparent CSS pre-processor out there.

Speed

Rework was designed to be simple and fast out of the box, without implementing complex caching models allowing everyone to hack on Rework itself or associate plugins easily.

Powering rework is a css parser written in JavaScript that may be used with node.js or in the browser. On the other end there is the css compiler which accepts the AST from css-parse, outputting CSS.

So where does Rework come in? Rework is the plugin and transformation engine that sits in-between these two tools. The AST is manipulated by Rework and its plugins (or user-defined plugins), which is then passed to the compiler.

Because these transformations are performed in JavaScript and not in a specialized preprocessor language they are simple to implement and efficient.

Let’s check out what it can do!

Modularity & Flexibility

Before diving into the plugins bundled with Rework, let’s take a look at how they can be applied. Rework’s API is very simple, you can specify vendor prefixes with .vendors(), which all plugins have access to, or you may pass specific vendor prefixes to most plugins themselves. Plugins are passed to a .use() method, and then finally the CSS output is returned with .toString(). In practice this looks something like:

var rework = require('rework');

var css = rework('string of css here')
  .vendors(['-webkit-', '-moz-'])
  .use(rework.prefixValue('linear-gradient'))
  .use(rework.prefixValue('radial-gradient'))
  .use(rework.prefixValue('transform'))
  .use(rework.vars())
  .use(rework.colors())
  .toString()

Out of the box rework provides plugins for the following, which may be mixed and matched as you wish:

  • media macros — define your own @media queries
  • ease — several additional easing functions
  • at2x — serve high resolution images
  • prefix — add vendor prefixes to properties
  • prefixValue — add vendor prefixes to values
  • prefixSelectors — add prefixes to selectors
  • opacity — add IE opacity support
  • url — rewrite url()s with a callback function
  • vars — add css variable support
  • keyframes — add @keyframe vendor prefixing
  • colors — add colour helpers like rgba(#fc0, .5)
  • references — add property references support height: @width etc
  • mixin — add custom property logic with mixing
  • extend — add extend: selector support

Let’s look at some examples!

Vendor prefixes

The prefix, prefixValue, and keyframes plugins make vendor prefixing extremely simple. Here the prefix plugin is applied to only two properties for illustration:

var rework = require('..')
var read = require('fs').readFileSync

var css = rework(read('examples/prefix.css', 'utf8'))
  .vendors(['-webkit-', '-moz-'])
  .use(rework.prefix('border-radius'))
  .use(rework.prefix('box-shadow'))
  .toString()

console.log(css)

With the prefix.css file containing the following CSS:

.button {
  border-radius: 5px;
  box-shadow: inset 0 0 1px white;
}

Rework yields the following prefixed output:

.button {
  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  border-radius: 5px;
  -webkit-box-shadow: inset 0 0 1px white;
  -moz-box-shadow: inset 0 0 1px white;
  box-shadow: inset 0 0 1px white
}

Extending

The extend plugin adds support for a special property named extend, which propagates the selector back up the AST so that the styles are applied without duplication. For example here’s a contrived green join button implementation:

button {
  padding: 5px 10px;
  border: 1px solid #eee;
  border-bottom-color: #ddd;
}

.green {
  background: green;
  padding: 10px 15px
}

a.join {
  extend: button;
  extend: .green;
  padding: 20px;
}

With this plugin enabled both of the extend properties are gone, and you’re left with the CSS you might expect:

button,
a.join {
  padding: 5px 10px;
  border: 1px solid #eee;
  border-bottom-color: #ddd
}

.green,
a.join {
  background: green;
  padding: 10px 15px
}

a.join {
  padding: 20px
}

Better rgba()

Ever had a color hex and then wanted to alter the alpha channel? Then you probably know how annoying this is with CSS. The colors plugin allows you to pass hex values to rgba(), for example rgba(#c00, .5) which is then compiled to rgba(204,0,0,0.5).

Writing your own plugins

Writing plugins for Rework is simple, suppose for example you want the ability to create your own properties, here expanding single-line position properties:

#logo {
  absolute: top left;
}

#logo {
  relative: top 5px left;
}

#logo {
  fixed: top 5px left 10px;
}

Into the valid CSS shown below:

#logo {
  position: absolute;
  top: 0;
  left: 0
}

#logo {
  position: relative;
  top: 5px;
  left: 0
}

#logo {
  position: fixed;
  top: 5px;
  left: 10px
}

The AST css-parse provides you is made up of simple objects and arrays, making it extremely easy to manipulate without extra knowledge of how the nodes are constructed or strange internal APIs. In the following plugin function we walk the declarations, check if the property is one of the positions, parse the value by splitting on whitespace and then inject the new css properties.

function positions() {
  var positions = ['absolute', 'relative', 'fixed'];

  return function(style){
    rework.visit.declarations(style, function(declarations){
      declarations.forEach(function(decl, i){
        if (!~positions.indexOf(decl.property)) return;
        var args = decl.value.split(/\s+/);
        var arg, n;

        // remove original
        declarations.splice(i, 1);

        // position prop
        declarations.push({
          property: 'position',
          value: decl.property
        });

        // position
        while (args.length) {
          arg = args.shift();
          n = parseFloat(args[0]) ? args.shift() : 0;
          declarations.push({
            property: arg,
            value: n
          });
        }
      });
    });
  }
}

To use this plugin you would simply pass it to .use():

var css = rework('#logo { absolute: top left; }')
  .use(positions())
  .toString()

Significant whitespace

Since CSS has a relatively simple grammar significant whitespace can help improve your productivity without turning the whole thing into an unreadable mess, this is especially true since Rework does not provide the higher level language constructs, which can get lost in the indentation. This is where css-whitespace comes in! It lets you write things like:

ul
  li
    background: #eee
    &:hover
      background: white
    &:active
      background: #ddd

Which yields:

ul li:hover {
  background: white;
}

ul li:active {
  background: #ddd;
}

ul li {
  background: #eee;
}

Future

In the near future we’ll be providing a more intuitive way to apply all or large chunks of the functionality Rework provides without manually adding all of the plugins, as well as improving the rework(1) executable.

Stylus vs SASS vs LESS error reporting

I was curious what SASS did as far as error reporting goes, so I tried the following snippet with SASS, LESS as well as Stylus.

body {
  form input {
    background: foo[fail];
  }
}

LESS

The output from LESS was terrible:

  Syntax Error on line 4

note: that for LESS I had to tweak the input to “foo[fail’]’;” since it simply consumes the input above

SASS

The SASS output was not bad, it shows you a tiny chunk of the line for context:

Syntax error: Invalid CSS after "...background: foo": expected ";", was "[fail];"
        on line 4 of standard input
  Use --trace for backtrace.

note: —trace is for the ruby stack trace, not SASS

Stylus

Then we have Stylus, showing you ~8 lines of context (by default), and even a detailed stack trace including the call sites much like you would find in other languages.

    Error: /tmp/stylus/test.styl:4
       1| 
       2| body {
       3|   form input {
     > 4|     background: foo[fail];
       5|   }
       6| }
       7| 

    cannot perform foo[(fail)]
        at "form input" (/tmp/stylus/test.styl:4)
        at "body" (/tmp/stylus/test.styl:3)

Express vs Sinatra Benchmarks

So I have been setting up benchmark scripts for Express today, and so far some of the results have been quite interesting! The numbers shown should be taken lightly, however they consistently show that Express is quite fast.

If you are interested in benchmarking your own web applications you might want to read my last article ApacheBench Gnuplot Graphing Benchmarks.

All of the following benchmarks were generated using ApacheBench with a concurrency of 50 and performs 2000 requests. Keep in mind that Thin is used to serve Sinatra requests.

Express vs Sinatra

For those who dont know Express is a NodeJS framework inspired by Ruby’s Sinatra. Below we have the benchmark results for a typical “Hello World” response, which include nodejs benchmarks without the overhead of features provided by Express:

express vs sinatraexpress sinatra requests per second

Haml.js vs Ruby Haml

Next up is my JavaScript Haml implementation. Below are the results of running Express & haml.js vs Sinatra & Haml. Both serve a layout template, as well as a page specific template.

javascript haml vs ruby hamlrequests per second

Sass.js vs Ruby Sass

Finally we have JavaScript Sass vs the regular Ruby implementation packaged with haml. Both implementations serve the same 80 line stylesheet.

sass js vs ruby sasssass benchmarks

Stay tuned for more benchmarks!