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