Tuesday 30 August 2011

A little Lua DSL for generating CSS


Lua has always been a good data description language and this makes it well suited to internal DSLs, that is, domain specific languages which are implemented in the language itself.

Consider the task of generating CSS. A very powerful notation, but not a complete language; there are no variables, functions or control structures. A common strategy is to generate the CSS from a template. The CSS for Blogger sites is embedded in an XML file which parameterizes the colours and widths involved. Like most XML formats it is clumsy and not exactly friendly to work with.

Apart from the parameterization problem, I personally find it hard to remember the formats of some common CSS properties and wanted a notation that fitted my head better.

This is what we will be working towards initially:

 css "ul li" {
     list_style = "none inside"
 }

Lua provides convenient syntactical sugar for function calls with a single argument that is either a string or a table constructor; the parentheses may be omitted. So the implementation is straightforward; css is a function which is given a selector and returns a function which processes the 'spec' table.

 function css (selector)
     out:write(selector..' ')
     return function (spec)
         out:write '{\n'
         for prop, val in pairs(spec) do
             prop = prop:gsub('_','-')
             out:write('\t'..prop..' = '..val..';\n')
         end
         out:write '}\n'
     end
 end

Here out can be any object which supports the write method, so that out = io.stdout is a good start for testing. There is a little massaging of the property names so that we can use underscores instead of hyphens, but otherwise it's a fairly literal translation.

As a special case, we'd like to write margin or padding using these forms:

 margin = 5;
 margin = {left='0.5em',right='0.5em'}

Sizes are either numbers or strings; if they are numbers they are explicitly converted into pixel values. The second declaration translates into two CSS properties:

 margin-left = 0.5em;
 margin-right = 0.5em;

so the strategy will be to go through the 'spec' (table of Lua key/value pairs) and expand these.

 function process_margin (tbl,spec,side)
    if type(spec) ~= 'table' then
       tbl['margin'..side_str(side)] = size_str(spec)
    elseif has_sides(spec) then
       for side, sspec in pairs(spec) do
          process_margin(tbl,sspec,side)
       end
    else
       error('must contain only top, left, right and bottom keys')
    end
 end

size_str will take a number like 5 and return '5px', pass strings through and otherwise throw an error. side_str('left') expands to '-left', and side_str(nil) expands to ''. The other auxilliary function has_sides is only true if the keys of the spec table is one of the side names; in this case the function is applied recursively to each of the values. A simple generalization also covers the similar padding case.

Similar recursive logic gives us this notation for border:

 border = true; -- default border
 border = {width=2}; -- border with width 2px
 border = {left=true}; -- default border on left side only
 border = {left={color='#DDD'}} -- left border with given colour

Apart from being more explicit, this format is easier to parameterize; the sizes are all numbers, and the colours are separate strings.

Currently, the selectors are still plain strings. That is fine enough, but we can do better:

 css.h2 {
    color = '#999',
    border = {bottom=true}
 }
 css.id.left {
    float = 'left',
    width = leftm,
    border = {right = {color = '#AAA'}}
 }
 css.id.left.ul {
    list_style = 'none inside',
    padding = {left = 0}
 }

Here the word id is special; it will translate as '#'. But how to make css.id.left generate the selector string '#left'?

Chains of properties can be easily converted into a set of operations with the 'dot builder' pattern.

 obj = setmetatable({},{
     __index = function(self,key)
         self[#self+1] = key
         return self
     end
 })

The __index metamethod only fires if the object does not contain the key. This is exactly what we want here, because the object is just used as an array, and has no key/value associations. Each 'lookup' merely returns the object, so it is executed purely for the side-effect of updating the array.

 > = obj.fred.alice.jane
 table: 0x00376d80
 > for i = 1,#obj do print(obj[i]) end
 fred
 alice
 jane
 > = table.concat(obj,',')
 fred,alice,jane

With a little special handling of id and class, this gives us the desired notation.

Flexible layouts can be easily generated dynamically using this DSL:

 require 'css'
 width = 500
 lmargin = 50
 leftm = 150
 gap = 10
 fore = '#FFFFFF'
 back = '#000000'
 left_fore = '#FFFFFF'
 left_back = '#000000'
 css.body {
    width = width,
    margin = {left=lmargin},
    color = fore,
    background_colour = back
 }
 css.id.left {
    float = 'left',
    width = leftm,
    color = left_fore,
    background_colour = left_back
 }
 css.id.content {
    margin = {left=leftm+gap},
 }
 print(tostring(css))  -- could write to file, etc

I present this more as an example of how flexible Lua DSLs can be, rather than a working solution. Mostly there's a good reason to keep CSS files separate from code, but at the least this provides a flexible way to generate that CSS.

With Lua web frameworks such as Orbiter, this can be directly used for interactive style modification/customization.

The full source for css.lua is available here

No comments:

Post a Comment