""" A really stupid python template language inspired by coffeekup, markaby. Do not use this code, it will ruin your day. A byproduct of insomnia. TL;DR ----- This module defines a template language that allows us to do: d = Doc() with d.html: with d.head: d.title ('example page') d.link (rel='stylesheet', href='/style.css', type='text/css') with d.body (style='foo'): d.a ('other stuff on another page', href='/other.html') d.p ('stuff on this page') Motivation ---------- Python templating has always been a problem for me. You normally have to write in your target language (i.e. HTML) with some horrible syntax for inlining python. For example, this is a Cheetah [1] template:
| $client.surname, $client.firstname | $client.email |
and this is a parsed fragment
") ) ) Now, this code is python, but logic expressed this way still requires management of parens and is not indent based - so is not really 'pythonic'. It also cannot really use conditionals and loops beyond ternarys and list comprehensions. So I wondered if we could hook up a bit of magic using some other method. So which way to go? Function definitions would require too much magic, so it seems that the best way is the _with_ statement. The result is the style shown in the TL;DR above. We can also wrap this in a template method to use python conditional logic based on supplied data: def example_template(items): d = Doc() with d.html: with d.head: d.title ('other stuff on this page') d.link (rel='stylesheet', href='/style.css', type='text/css') with d.body (style='foo'): d.a ('other stuff on another page', href='/other.html') d.p ('stuff on this page') with d.ul: for i in items: with d.li: d.a (str(i), href=str(i) + '.html') return d.to_string() The code below also shows template inheritance from python classes. This is currently based on the lxml builder, and so it is slightly slower than the elementree benchmarks [7] (which is pretty slow). The lazy decorator that allows us to miss out brackets on with statements is probably going to make you cry. Still this seems like a reasonable template language in about 40 lines of code. [1] http://www.cheetahtemplate.org/examples.html [2] https://bitbucket.org/tavisrudd/throw-out-your-templates/src/98c5afba7f35/throw_out_your_templates.py [3] http://haml-lang.com/ [4] http://coffeekup.org/ [5] http://lxml.de/dev/api/lxml.builder.ElementMaker-class.html [6] http://breve.twisty-industries.com/ [7] http://spitfire.googlecode.com/svn/trunk/tests/perf/bigtable.py Comments, abuse, etc to twitter @casualbon or casbon (at) gmail.com """ import lxml import lxml.html import lxml.html.builder import lxml.etree class TagContext(object): """ The context manager for an HTML tag """ __slots__ = ['doc', 'node', 'tag'] def __init__(self, tag, doc, *content, **props): """ create an html tag belonging to doc with given content and properties""" self.doc = doc self.tag = tag def __call__(self, *content, **props): text = '' for c in content: if len(c) and c[0] == '#': props['id'] = c[1:] elif len(c) and c[0] == '.': props['class'] = c[1:] else: text += c text += props.get('text', '') self.node = getattr(lxml.html.builder, self.tag.upper())(**props) if self.doc.stack: self.doc.stack[-1].append(self.node) if content: self.node.text = text self.doc.tail_node = self.node return self def __enter__(self): """ entering the node appends it to the document stack """ self.doc.stack.append(self.node) self.doc.tail_node = None def __exit__(self, t, v, tb): """ exiting the node pops it from the stack """ if len(self.doc.stack) > 1: self.doc.stack.pop() self.doc.tail_node = self.node def each(self, iterable): for i in iterable: with self(): yield self, i def set(self, key, value): self.node.set(key, value) class Lazydec(object): """ Horrible decorator to allow the with statement to omit brackets by tracking if context returned by doc has been entered """ __slots__ = ['x'] def __init__(self, x): self.x = x def __call__(self, *args, **kws): return self.x(*args, **kws) def __enter__(self): if self.x.__name__ == 'tagcallable': self.x = self.x() return self.x.__enter__() def __exit__(self, *args): return self.x.__exit__(*args) class Doc(object): """ A document manages a stack, the head of which is the current context node """ Context = TagContext def __init__(self, *args, **kws): self.stack = [] self.tail_node = None # this node is set if text needs to be put in its tail def __getattr__(self, name): """ Override getattr for quick tag access This means Doc.html returns a tag """ return self.Context(name, self) def text(self, text): """ append raw text """ if self.tail_node is not None: self.tail_node.tail = (self.tail_node.tail or '') + text else: self.stack[-1].text = (self.stack[-1].text or '') + text def fragment(self, html): """ append a html fragment (must be a single element) """ try: self.stack[-1].append(lxml.html.fragment_fromstring(html)) except lxml.etree.ParserError: els = lxml.html.fragments_fromstring(html) for e in els: self.stack[-1].append(e) def to_string(self, pretty_print=True): """ convert this document to string """ return lxml.html.tostring(self.stack[0], pretty_print=pretty_print, doctype='') class BootstrapContext(TagContext): def _add_class(self, props, cls): if 'class' not in props: props['class'] = cls else: props['class'] = props['class'] + ' ' + cls def __call__(self, *content, **props): # support span and offset for gridcls in ['span', 'offset']: if gridcls in props: self._add_class(props, gridcls + str(props[gridcls])) props.pop(gridcls) return TagContext.__call__(self, *content, **props) class Bootstrap(Doc): Context = BootstrapContext if __name__ == '__main__': def example_template(items): """ template function example""" d = Doc() p = d.html() print p.__class__ with d.html(): d.fragment('