#!/usr/bin/env python # # Templite+ # A light-weight, fully functional, general purpose templating engine # # Copyright (c) 2009 joonis new media # Author: Thimo Kraemer <thimo.kraemer@joonis.de> # # Based on Templite - Tomer Filiba # http://code.activestate.com/recipes/496702/ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # import sys, re class Templite(object): auto_emit = re.compile('(^[\'\"])|(^[a-zA-Z0-9_\[\]\'\"]+$)') def __init__(self, template, start='${', end='}$'): if len(start) != 2 or len(end) != 2: raise ValueError('each delimiter must be two characters long') delimiter = re.compile('%s(.*?)%s' % (re.escape(start), re.escape(end)), re.DOTALL) offset = 0 tokens = [] for i, part in enumerate(delimiter.split(template)): part = part.replace('\\'.join(list(start)), start) part = part.replace('\\'.join(list(end)), end) if i % 2 == 0: if not part: continue part = part.replace('\\', '\\\\').replace('"', '\\"') part = '\t' * offset + 'emit("""%s""")' % part else: part = part.rstrip() if not part: continue if part.lstrip().startswith(':'): if not offset: raise SyntaxError('no block statement to terminate: ${%s}$' % part) offset -= 1 part = part.lstrip()[1:] if not part.endswith(':'): continue elif self.auto_emit.match(part.lstrip()): part = 'emit(%s)' % part.lstrip() lines = part.splitlines() margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip()) part = '\n'.join('\t' * offset + l[margin:] for l in lines) if part.endswith(':'): offset += 1 tokens.append(part) if offset: raise SyntaxError('%i block statement(s) not terminated' % offset) self.__code = compile('\n'.join(tokens), '<templite %r>' % template[:20], 'exec') def render(self, __namespace=None, **kw): """ renders the template according to the given namespace. __namespace - a dictionary serving as a namespace for evaluation **kw - keyword arguments which are added to the namespace """ namespace = {} if __namespace: namespace.update(__namespace) if kw: namespace.update(kw) namespace['emit'] = self.write __stdout = sys.stdout sys.stdout = self self.__output = [] eval(self.__code, namespace) sys.stdout = __stdout return ''.join(self.__output) def write(self, *args): for a in args: self.__output.append(str(a)) if __name__ == '__main__': template = r""" This we already know: <html> <body> ${ def say_hello(arg): emit("hello ", arg, "<br>") }$ <table> ${ for i in range(10): emit("<tr><td> ") say_hello(i) emit(" </tr></td>\n") }$ </table> ${emit("hi")}$ tralala ${if x > 7: say_hello("big x")}$ lala $\{this is escaped starting delimiter ${emit("this }\$ is an escaped ending delimiter")}$ ${# this is a python comment }$ </body> </html> But this is completely new: ${if x > 7:}$ x is ${emit('greater')}$ than ${print x-1}$ Well, the print statement produces a newline. ${:else:}$ This terminates the previous code block and starts an else code block Also this would work: $\{:end}\$$\{else:}\$, but not this: $\{:end}\$ $\{else:}\$ ${:this terminates the else-block only the starting colon is essential}$ So far you had to write: ${ if x > 3: emit(''' After a condition you could not continue your template. You had to write pure python code. The only way was to use %%-based substitutions %s ''' % x) }$ ${if x > 6:}$ Now you do not need to break your template ${print x}$ ${:elif x > 3:}$ This is great ${:endif}$ ${for i in range(x-1):}$ Of course you can use any type of block statement ${i}$ ${"fmt: %s" % (i*2)}$ ${:else:}$ Single variables and expressions starting with quotes are substituted automatically. Instead $\{emit(x)}\$ you can write $\{x}\$ or $\{'%s' % x}\$ or $\{"", x}\$ Therefore standalone statements like break, continue or pass must be enlosed by a semicolon: $\{continue;}\$ The end ${:end-for}$ """ t = Templite(template) print t.render(x=8) # Output is: """ This we already know: <html> <body> <table> <tr><td> hello 0<br> </tr></td> <tr><td> hello 1<br> </tr></td> <tr><td> hello 2<br> </tr></td> <tr><td> hello 3<br> </tr></td> <tr><td> hello 4<br> </tr></td> <tr><td> hello 5<br> </tr></td> <tr><td> hello 6<br> </tr></td> <tr><td> hello 7<br> </tr></td> <tr><td> hello 8<br> </tr></td> <tr><td> hello 9<br> </tr></td> </table> hi tralala hello big x<br> lala ${this is escaped starting delimiter this }$ is an escaped ending delimiter </body> </html> But this is completely new: x is greater than 7 Well, the print statement produces a newline. So far you had to write: After a condition you could not continue your template. You had to write pure python code. The only way was to use %-based substitutions 8 Now you do not need to break your template 8 Of course you can use any type of block statement 0 fmt: 0 Of course you can use any type of block statement 1 fmt: 2 Of course you can use any type of block statement 2 fmt: 4 Of course you can use any type of block statement 3 fmt: 6 Of course you can use any type of block statement 4 fmt: 8 Of course you can use any type of block statement 5 fmt: 10 Of course you can use any type of block statement 6 fmt: 12 Single variables and expressions starting with quotes are substituted automatically. Instead ${emit(x)}$ you can write ${x}$ or ${'%s' % x}$ or ${"", x}$ Therefore standalone statements like break, continue or pass must be enlosed by a semicolon: ${continue;}$ The end """