|
|
@@ -0,0 +1,273 @@ |
|
|
""" |
|
|
A really stupid python template language inspired by coffeekup, markaby. |
|
|
Do not use this code, it will ruin your day. A byproduct of insomnia. |
|
|
|
|
|
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: |
|
|
|
|
|
<html> |
|
|
<head><title>$title</title></head> |
|
|
<body> |
|
|
<table> |
|
|
#for $client in $clients |
|
|
<tr> |
|
|
<td>$client.surname, $client.firstname</td> |
|
|
<td><a href="mailto:$client.email">$client.email</a></td> |
|
|
</tr> |
|
|
#end for |
|
|
</table> |
|
|
</body> |
|
|
</html> |
|
|
|
|
|
We have '$' and '#' to inline python code and variables. This to me is the |
|
|
wrong way round. I want to write python and not html. I do not want to manage |
|
|
html braces. Travis Rudd has pointed out that many of the arguments for |
|
|
non python templates are unfounded [2] and I agree with him. This has been |
|
|
further enforced for me by using haml [3] and coffeekup [4], which leaves us in the |
|
|
ridiculous position that the best indent based templating DSLs for html are in |
|
|
non indent based languages (ruby, javascript). |
|
|
|
|
|
This is why we can't have nice things |
|
|
------------------------------------- |
|
|
|
|
|
Coffescript and ruby have some advantage over python for a DSL including the |
|
|
way anonymous blocks are defined and the ability to omit brackets and still get |
|
|
function execution. This means that the most direct python templates, such as |
|
|
lxml builder[5] and breve [6] end up with a nested structure. For example, |
|
|
this is an lxml template: |
|
|
|
|
|
html = E.HTML( |
|
|
E.HEAD( |
|
|
E.LINK(rel="stylesheet", href="great.css", type="text/css"), |
|
|
E.TITLE("Best Page Ever") |
|
|
), |
|
|
E.BODY( |
|
|
E.H1(E.CLASS("heading"), "Top News"), |
|
|
E.P("World News only on this page", style="font-size: 200%"), |
|
|
"Ah, and here's some more text, by the way.", |
|
|
lxml.html.fromstring("<p> and this is a parsed fragment </p>") |
|
|
) |
|
|
) |
|
|
|
|
|
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. |
|
|
|
|
|
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') |
|
|
|
|
|
|
|
|
We can 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() |
|
|
|
|
|
|
|
|
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/ |
|
|
|
|
|
Comments, abuse, etc to twitter @casualbon or casbon (at) gmail.com |
|
|
""" |
|
|
from lxml.html import builder as E |
|
|
import lxml |
|
|
from contextlib import contextmanager |
|
|
|
|
|
|
|
|
class TagContext(object): |
|
|
""" The context manager for an HTML tag """ |
|
|
|
|
|
def __init__(self, tag, doc, *content, **props): |
|
|
""" create an html tag belonging to doc with given content and properties""" |
|
|
self.tag = tag |
|
|
self.doc = doc |
|
|
self.content = content |
|
|
self.props = props |
|
|
self.stack = self.doc.stack |
|
|
self.node = self.make_node() |
|
|
|
|
|
def make_node(self): |
|
|
""" create an lxml element and append it to the node at the top |
|
|
of the document tack """ |
|
|
node = getattr(E, self.tag.upper())(*self.content, **self.props) |
|
|
if self.stack: |
|
|
self.stack[-1].append(node) |
|
|
return node |
|
|
|
|
|
def __enter__(self): |
|
|
""" entering the node appends it to the document stack """ |
|
|
self.stack.append(self.node) |
|
|
|
|
|
def __exit__(self, t, v, tb): |
|
|
""" exiting the node pops it from the stack """ |
|
|
if len(self.stack) > 1: |
|
|
self.stack.pop() |
|
|
|
|
|
|
|
|
class Lazydec(object): |
|
|
""" Horrible decorator to allow the with statement to omit |
|
|
brackets by tracking if callable returned by doc has been called |
|
|
""" |
|
|
|
|
|
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 """ |
|
|
|
|
|
def __init__(self, *args, **kws): |
|
|
self.stack = [] |
|
|
|
|
|
def __getattr__(self, attr): |
|
|
""" Override getattr for quick tag access |
|
|
|
|
|
This means Doc.html returns a tag |
|
|
""" |
|
|
# TODO: incomplete list of tags |
|
|
if attr in ['html', 'head', 'body', 'a', 'p', 'ul', 'li', 'div', |
|
|
'table', 'tr', 'td', 'link', 'title', 'span']: |
|
|
return self.make_tag(attr) |
|
|
else: |
|
|
return object.getattr(self, attr) |
|
|
|
|
|
def to_string(self, pretty_print=True): |
|
|
""" convert this document to string """ |
|
|
return lxml.html.tostring(self.stack[0], pretty_print=pretty_print) |
|
|
|
|
|
def make_tag(self, name): |
|
|
""" create a tag by creating a TagContext and then decorating it with the lazy decorator""" |
|
|
def tagcallable(*content, **props): |
|
|
return TagContext(name, self, *content, **props) |
|
|
f = Lazydec(tagcallable) |
|
|
return f |
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|
|
def example_template(items): |
|
|
""" template function example""" |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
class BaseTemplate(object): |
|
|
""" template inheritance example """ |
|
|
|
|
|
title = 'base' |
|
|
|
|
|
def render(self): |
|
|
d = Doc() |
|
|
with d.html: |
|
|
|
|
|
with d.head: |
|
|
d.title (self.title) |
|
|
d.link (rel='stylesheet', href='/style.css', type='text/css') |
|
|
|
|
|
with d.body (style='foo'): |
|
|
with d.div (id='header'): |
|
|
self.header(d) |
|
|
with d.div (id='content'): |
|
|
self.content(d) |
|
|
with d.div (id='footer'): |
|
|
self.footer(d) |
|
|
|
|
|
return d.to_string() |
|
|
|
|
|
def header(self, d): |
|
|
pass |
|
|
|
|
|
def content(self, d): |
|
|
pass |
|
|
|
|
|
def footer(self, d): |
|
|
d.span('Never read this content') |
|
|
|
|
|
|
|
|
class ItemTemplate(BaseTemplate): |
|
|
|
|
|
title = 'item view' |
|
|
|
|
|
def __init__(self, items): |
|
|
self.items = items |
|
|
|
|
|
def header(self, d): |
|
|
d.p('view of %s items' % len(self.items)) |
|
|
|
|
|
def content(self, d): |
|
|
with d.ul: |
|
|
for i in self.items: |
|
|
with d.li: |
|
|
d.a (str(i), href=str(i) + '.html') |
|
|
|
|
|
print example_template(range(10)) |
|
|
print |
|
|
print ItemTemplate(range(10)).render() |