Welcome, guest | Sign In | My Account | Store | Cart

A light-weight, fully functional, general purpose templating engine, in just 40 lines of code

Python, 136 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import re


class Templite(object):
    delimiter = re.compile(r"\$\{(.*?)\}\$", re.DOTALL)
    
    def __init__(self, template):
        self.tokens = self.compile(template)
    
    @classmethod
    def from_file(cls, file):
        """
        loads a template from a file. `file` can be either a string, specifying
        a filename, or a file-like object, supporting read() directly
        """
        if isinstance(file, basestring):
            file = open(file)
        return cls(file.read())
    
    @classmethod
    def compile(cls, template):
        tokens = []
        for i, part in enumerate(cls.delimiter.split(template)):
            if i % 2 == 0:
                if part:
                    tokens.append((False, part.replace("$\\{", "${")))
            else:
                if not part.strip():
                    continue
                lines = part.replace("}\\$", "}$").splitlines()
                margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip())
                realigned = "\n".join(l[margin:] for l in lines)
                code = compile(realigned, "<templite %r>" % (realigned[:20],), "exec")
                tokens.append((True, code))
        return tokens
    
    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)
        
        def emitter(*args):
            for a in args: output.append(str(a))
        def fmt_emitter(fmt, *args):
            output.append(fmt % args)
        namespace["emit"] = emitter
        namespace["emitf"] = fmt_emitter
        
        output = []
        for is_code, value in __self.tokens:
            if is_code:
                eval(value, namespace)
            else:
                output.append(value)
        return "".join(output)
    
    # shorthand
    __call__ = render

---------
example:
---------
>>> from templite import Templite
>>>
>>> demo = r"""
... <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>
... """
>>>
>>> t = Templite(demo)
>>> print t(x = 8)

<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>

>>>

Templite -- A light-weight, fully functional, general purpose templating engine, allowing you to embed python code directly into your text. This engine is suitable for any templating (not only HTML/XML), and is minimal (40 lines of code!) and fast (all preprocessing is done in "compile time")

All text between ${ and }$ is considered python code, and is evaluated when the Templite is rendered. You can escape the ${ delimiter by $\{ and the }$ delimiter by }\$.

Emitting output is done with the emit() function, which accepts any number of arguments, converts them to a string, and appends to the output where the template was located.

Security notice: IT'S NOT SECURE, as the template-generating code is arbitrary python code. So be sure you don't just evaluate user-provided Templites, at least not before taking a look at them. It's meant to be a light and fast templating engine, for trusted server side code (generating reports, etc.).

7 comments

Oscar Micheli 17 years, 11 months ago  # | flag

There are a couple of errors in published code. I've found in the following lines a missing '[' and ']' in string comprehension

  1. padding = min([len(l) - len(l.lstrip()) for l in lines if l.strip()])

  2. unpadded = "\n".join([l[padding:] for l in lines])

Thanks for the code. Nice indeed

Oscar

Ori Peleg 17 years, 11 months ago  # | flag

Shrewd! And totally cool.

tomer filiba (author) 17 years, 11 months ago  # | flag

these are generator expressions. python 2.4 has generator expressions, i.e.

>>> (x for x in range(10))

<generator object at 0x009ED0F8>

-tomer

Tal Einat 17 years, 11 months ago  # | flag

Escaping delimiters. Having to escape only '${' outside code and only '}$' inside code can be a bit confusing, and IMO should be more noticeably and clearly noted in the documentation (or changed). I admit that the choice of delimiters is such that not much escaping will probably be needed in day-to-day use; still, someone getting the escaping wrong could have a time understanding why.

Personally I would prefer to have to escape both '${' and '}$' everywhere, for better uniformity. Of course, this is a personal preference.

Josiah Carlson 17 years, 8 months ago  # | flag

Very nice. That's just the right amount of markup for embedding Python into html documents.

Ehud Ben-Reuven 17 years, 7 months ago  # | flag

exec instead of eval. According to Python doc "eval(value, namespace)" should only be used for value compiled as "eval" and for "exec" you should use instead "exec value in namespace". Which worked.

Thimo Kraemer 15 years, 2 months ago  # | flag

Revised version where you do not need to break your template within block statements.

Created by tomer filiba on Sun, 14 May 2006 (PSF)
Python recipes (4591)
tomer filiba's recipes (12)

Required Modules

  • (none specified)

Other Information and Tasks