CSS preprocessors seem to be really popular these days. At least there are many to choose from. See Stylus, Sass and LESS for example. These kind of tools provide some functionality missing from vanilla CSS and make it more fun to write. Some popular features include variables, functions, mixins, nesting and overall lighter syntax.
In this post I'm going to show you how to use Node to write a CSS preprocessor with a selected few features. Since "less is more" I'm simply going to call this little project as "more.js". Hopefully this post gives you some ideas on how you might write your own preprocessor.
In principle they're very simple. You just take some data in, transform it and finally spit it out. Most of the magic happens in that transformation stage. I'll start with the ends (read file, write file) and move onto actual business logic right after that.
I'll be using Node v0.6.6 since that's what I've installed on my Mac at the moment. If you are a Mac person too, you can easily install it via MacPorts simply by invoking "sudo port install nodejs" at terminal. If you are using Linux you should be able to find Node from your local package management system. Note that it might not be the freshest version available so you're probably better off installing it by other means. You could just grab a package directly from their site and install that. There's a Windows version available too.
Dealing with File Input and Output
In order to get our preprocessor work we'll obviously need some way to read our definition file and then output that as a CSS file. Since our library will be known as "more.js", it probably makes sense to mark our inputs using ".more" suffix. Let's write some code to convert "sample.more" to "sample.css". The content of the file shall remain the same this time.
The implementation parses the input file name from argv, constructs target name based on that and finally reads the source and writes it to the target. Note that I didn't enforce the input file format specifically. It's probably cool just to stick to a convention here.
File manipulation is done in asynchronous manner using Node's facilities. I pretty much just copied that from their documentation. Note that "readfile" returns a Buffer by default so you'll need to set its format explicitly the way I've done here to get anything useful done.
This boilerplate should be enough for our purposes. Now we just need to drop in some logic.
Braces and Semicolons
Given I dig Python and YAML a lot, it would be cool to have a syntax that doesn't use braces nor semicolons. Just indentation will do. This means we'll need some way to analyze the input and then add braces to the right places. I've tried to sketch out some initial syntax in the next snippet:
We could go on step further and get rid of the colons too but I kind of like them as separators so let's leave them alone. We'll need to transform that into something like this for it to become valid CSS:
Let's get to work. I'm going to do this in two steps. First I'll perform some lexical analysis to deal with indentation. I'll attach the bits needed by CSS based on that. Here's the code I came up with:
Basically we just split lines a bit in order to separate indentation from actual line content and then transform based on that. It's not the prettiest code around but does the trick in this case. We'll need to evolve it further anyway so better not to get too attached to it.
Wouldn't it be cool if we could write declarations inside declarations and have that generate valid CSS for us? The following example illustrates this better:
This should get compiled into CSS like this:
As you can see the nested blocks within the definition get compiled into CSS blocks which contain the parent. This is something we'll need to keep track off. In addition we'll need some way to detect whether or not we're dealing with a nested block. Sounds like we need some extra analysis to make it easier to implement the transformation.
It really sounds we could use some kind of a tree structure to deal with nesting. In the above case we might want to end up with something like this:
Note how the usage of a syntax tree decouples us from indentation. Now it's just a matter of implementing a tree generator and actual transformation. Here's my go at the syntax tree generator:
There's some extra data in the tree. I added a parent reference so it's easier to define actual transformations. This will be needed for constructing CSS block names for instance. The bit that does the transformation looks like this:
It's quite a bit of code. I ended up treating nesting as a special case. I render the nested bits right after I've gone through the regular ones. Given we're dealing with a tree it was natural to use a recursive solution.
There are still a few features to go in order to consider this little hack "feature complete" at least on a basic level. I'm going to show you how to plug in variables and mixins to the system next. Now that we've got the basic architecture together this should be a bit easier.
The idea of variables is quite simple. Given the following syntax:
We should end up with something like this:
As you can see, it's just a matter of doing a simple replacement. We'll need to detect the variables somehow and then apply their value at appropriate places. The following bits of code should illustrate the idea further:
This is a very basic implementation. It would be interesting to add support for simple math (ie. @var + 5 or @var1 + @var2). I'll tackle this later if the need arises. For now it's an exercise for reader (pro tip: look into eval to get started).
We're still missing one vital bit, mixins. They allow you to reuse functionality easily. For instance you might want to define a mixin for rounded corners like this:
This in turn will get converted to CSS such as this:
Here's my implementation:
There are a few interesting bits in the code. First I seek possible mixins. I also mark them as deleted so I can avoid them later while processing. Mixin parameters have to be parsed and plugged into place so that caused some extra code to appear. In case that looks cryptic to you, check out the whole code. That should provide some additional context.
Writing a CSS preprocessor isn't entirely trivial. It started out quite simply but ended up somewhere around 225 lines of code. It's still missing some advanced functionality. I got the basics done at least!
Further development will be done at GitHub. If you have any ideas how to make it better, let me know and I'll look into it. You are probably better off using some established alternatives at the moment, though. I'll likely use this in some of my own work so the development will go on at least in some pace.