Four types of template engines

33 views
Skip to first unread message

Magnus Holm

unread,
Jan 15, 2010, 5:46:48 PM1/15/10
to guardians-o...@googlegroups.com
Temple takes one approach to rendering templates, but it's not the only (or an all-around best) approach. Just thought I should write down four ways to implement a template engine.

I should say that I'm doing this not only to share knowledge about template engine, but to learn more about template engines. I've simply checked out the alternatives, but I wouldn't call myself an expert at all. So if you believe I am wrong: Awesome! I love to be proven wrong :-)

Parse & interpret
This is the technique Mustache started with, and works by parsing and evaluating the template every time it renders. This is often implemented with a while-loop + regexp and can give you quite a lot of freedom (since the evaluation can change the way parsing works later). On the other hand, this is very, very, very slow, and its parser is often written with a non-parser tool (*cough* regexp *cough*). To relate to programming languages, this is how MRI 1.8 works (well, except for the regexp-part).

def render
  while parsing
    render
  end
end

I do not recommend writing such a template engine, mostly because it's *always* going to be slow. If you want to start write some code early on, it's far better to start with some tests and rather implement it properly later.

Parse & compile
This is what Mustache uses now. It works by parsing the template and generate Ruby code as it parses. At render-time it's simply calling eval(generated_ruby_code). This is very fast (or, that of course depends on the generated Ruby code), but it's often difficult to directly compile complex template languages.

def setup
  while parsing
    code << generate
  end
end

def render
  eval(code)
end

If you have a simply template syntax, this is a quick and dirty way to implement a fast engine. 

Bytecode & interpret
This is how Liquid works. It works by parsing the template completely (into some sort of bytecode/AST/sexp) and at render-time you look through the bytecode and render the template. This allows you to use a proper parser (you hear what I'm saying Liquid?) and acceptable performance. Another interesting aspect is that you can now traverse the bytecode - it's optimizable!

This is also how virtual machines works (if we ignore JIT).

def setup
  while parsing
    bytecode << bytecompile
  end
end

def render
  bytecode.each { |b| b.render }
end

Bytecode & compile
This is the approach Temple (maybe Erubis too - I haven't checked out their internals yet) is taking. You parse the template into bytecode/AST/sexp and then you compile it into Ruby code. Then you get excellent performance, possibility for compiling in several steps and you can optimize it.

def setup
  while parsing
    bytecode << bytecompile
  end
  code = bytecode.map { |b| b.compile }
end

def render
  eval(code)
end


// Magnus Holm

Reply all
Reply to author
Forward
0 new messages