1"""A lightweight python templating engine.    Templet version 2 beta.
2
3Slighlty modifed version for MDP (different indentation handling).
4
5
6Supports two templating idioms:
7        1. template functions using @stringfunction and @unicodefunction
8        2. template classes inheriting from StringTemplate and UnicodeTemplate
9
10Each template function is marked with the attribute @stringfunction
11or @unicodefunction.    Template functions will be rewritten to expand
12their document string as a template and return the string result.
13For example:
14
15        @stringtemplate
16        def myTemplate(animal, thing):
17            "the $animal jumped over the $thing."
18
19        print myTemplate('cow', 'moon')
20
21The template language understands the following forms:
22
23        $myvar - inserts the value of the variable 'myvar'
24        ${...} - evaluates the expression and inserts the result
25        ${{...}} - executes enclosed code; use 'out.append(text)' to insert text
26        $$ - an escape for a single $
27        $ (at the end of the line) - a line continuation
28
29Template functions are compiled into code that accumulates a list of
30strings in a local variable 'out', and then returns the concatenation
31of them.    If you want do do complicated computation, you can append
32to 'out' directly inside a ${{...}} block.
33
34Another alternative is to use template classes.
35
36Each template class is a subclass of StringTemplate or UnicodeTemplate.
37Template classes should define a class attribute 'template' that
38contains the template code.    Also, any class attribute ending with
39'_template' will be compiled into a template method.
40
41Use a template class by instantiating it with a dictionary or
42keyword arguments.    Get the expansion by converting the instance
43to a string.    For example:
44
45        class MyTemplate(templet.Template):
46            template = "the $animal jumped over the $thing."
47
48        print MyTemplate(animal='cow', thing='moon')
49
50Within a template class, the template language is similar to a template
51function, but 'self.write' should be used to build the string inside
52${{..}} blocks.    Also, there is a shorthand for calling template methods:
53
54        $<sub_template> - shorthand for '${{self.sub_template(vars())}}'
55
56This idiom is helpful for decomposing a template and when subclassing.
57
58A longer example:
59
60    import cgi
61    class RecipeTemplate(templet.Template):
62        template = r'''
63            <html><head><title>$dish</title></head>
64            <body>
65            $<header_template>
66            $<body_template>
67            </body></html>
68        '''
69        header_template = r'''
70            <h1>${cgi.escape(dish)}</h1>
71        '''
72        body_template = r'''
73            <ol>
74            ${{
75                for item in ingredients:
76                    self.write('<li>', item, '\n')
77            }}
78            </ol>
79        '''
80
81This template can be expanded as follows:
82
83    print RecipeTemplate(dish='burger', ingredients=['bun', 'beef', 'lettuce'])
84
85And it can be subclassed like this:
86
87    class RecipeWithPriceTemplate(RecipeTemplate):
88        header_template = "<h1>${cgi.escape(dish)} - $$$price</h1>\n"
89
90Templet is by David Bau and was inspired by Tomer Filiba's Templite class.
91For details, see http://davidbau.com/templet
92
93Templet is posted by David Bau under BSD-license terms.
94
95Copyright (c) 2007, David Bau
96All rights reserved.
97
98Redistribution and use in source and binary forms, with or without
99modification, are permitted provided that the following conditions are met:
100
101    1. Redistributions of source code must retain the above copyright notice,
102         this list of conditions and the following disclaimer.
103
104    2. Redistributions in binary form must reproduce the above copyright
105         notice, this list of conditions and the following disclaimer in the
106         documentation and/or other materials provided with the distribution.
107
108    3. Neither the name of Templet nor the names of its contributors may
109         be used to endorse or promote products derived from this software
110         without specific prior written permission.
111
112THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
113ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
114WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
115DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
116ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
117(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
118LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
119ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
120(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
121SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
122"""
123from __future__ import print_function
124from builtins import str
125from past.builtins import basestring
126from builtins import object
127
128import sys, re, inspect
129from future.utils import with_metaclass
130
131
132class _TemplateBuilder(object):
133
134    __pattern = re.compile(r"""\$(   # Directives begin with a $
135                \$                 | # $$ is an escape for $
136                [^\S\n]*\n         | # $\n is a line continuation
137                [_a-z][_a-z0-9]*   | # $simple Python identifier
138                \{(?!\{)[^\}]*\}   | # ${...} expression to eval
139                \{\{.*?\}\}        | # ${{...}} multiline code to exec
140                <[_a-z][_a-z0-9]*> | # $<sub_template> method call
141            )(?:(?:(?<=\}\})|(?<=>))[^\S\n]*\n)?  # eat some trailing newlines
142        """, re.IGNORECASE | re.VERBOSE | re.DOTALL)
143
144    def __init__(self, constpat, emitpat, callpat=None):
145        self.constpat, self.emitpat, self.callpat = constpat, emitpat, callpat
146
147    def __realign(self, str, spaces=''):
148        """Removes any leading empty columns of spaces and an initial
149        empty line.
150
151        This is important for embedded Python code.
152        """
153        lines = str.splitlines()
154        if lines and not lines[0].strip(): del lines[0]
155        lspace = [len(l) - len(l.lstrip()) for l in lines if l.lstrip()]
156        margin = len(lspace) and min(lspace)
157        return '\n'.join((spaces + l[margin:]) for l in lines)
158
159    def build(self, template, filename, s=''):
160        code = []
161        for i, part in enumerate(self.__pattern.split(template)):
162            if i % 2 == 0:
163                if part:
164                    code.append(s + self.constpat % repr(part))
165            else:
166                if not part or (part.startswith('<') and self.callpat is None):
167                    raise SyntaxError('Unescaped $ in ' + filename)
168                elif part.endswith('\n'):
169                    continue
170                elif part == '$':
171                    code.append(s + self.emitpat % '"$"')
172                elif part.startswith('{{'):
173                    code.append(self.__realign(part[2:-2], s))
174                elif part.startswith('{'):
175                    code.append(s + self.emitpat % part[1:-1])
176                elif part.startswith('<'):
177                    code.append(s + self.callpat % part[1:-1])
178                else:
179                    code.append(s + self.emitpat % part)
180        return '\n'.join(code)
181
182def _exec(code, globals, locals):
183    exec(code, globals, locals)
184
185class _TemplateMetaClass(type):
186
187    __builder = _TemplateBuilder(
188        'self.out.append(%s)', 'self.write(%s)', 'self.%s(vars())')
189
190    def __compile(cls, template, n):
191        globals = sys.modules[cls.__module__].__dict__
192        if '__file__' not in globals: filename = '<%s %s>' % (cls.__name__, n)
193        else: filename = '%s: <%s %s>' % (globals['__file__'], cls.__name__, n)
194        code = compile(cls.__builder.build(template, filename), filename, 'exec')
195        def expand(self, __dict = None, **kw):
196            if __dict: kw.update([i for i in __dict.items() if i[0] not in kw])
197            kw['self'] = self
198            _exec(code, globals, kw)
199        return expand
200
201    def __init__(cls, *args):
202        for attr, val in list(cls.__dict__.items()):
203            if attr == 'template' or attr.endswith('_template'):
204                if isinstance(val, basestring):
205                    setattr(cls, attr, cls.__compile(val, attr))
206        type.__init__(cls, *args)
207
208
209class StringTemplate(with_metaclass(_TemplateMetaClass, object)):
210    """A base class for string template classes."""
211
212    def __init__(self, *args, **kw):
213        self.out = []
214        self.template(*args, **kw)
215
216    def write(self, *args):
217        self.out.extend([str(a) for a in args])
218
219    def __str__(self):
220        return ''.join(self.out)
221
222
223# The original version of templet called StringTemplate "Template"
224Template = StringTemplate
225
226
227class UnicodeTemplate(with_metaclass(_TemplateMetaClass, object)):
228    """A base class for unicode template classes."""
229
230    def __init__(self, *args, **kw):
231        self.out = []
232        self.template(*args, **kw)
233
234    def write(self, *args):
235        self.out.extend([str(a) for a in args])
236
237    def __unicode__(self):
238        return u''.join(self.out)
239
240    def __str__(self):
241        return str(self).encode('utf-8')
242
243
244def _templatefunction(func, listname, stringtype):
245    globals, locals = sys.modules[func.__module__].__dict__, {}
246    if '__file__' not in globals: filename = '<%s>' % func.__name__
247    else: filename = '%s: <%s>' % (globals['__file__'], func.__name__)
248    builder = _TemplateBuilder('%s.append(%%s)' % listname,
249                                                         '%s.append(%s(%%s))' % (listname, stringtype))
250    args = inspect.getargspec(func)
251    code = [
252        'def %s%s:' % (func.__name__, inspect.formatargspec(*args)),
253        ' %s = []' % listname,
254        builder.build(func.__doc__, filename, ' '),
255        ' return "".join(%s)' % listname]
256    code = compile('\n'.join(code), filename, 'exec')
257    exec(code, globals, locals)
258    return locals[func.__name__]
259
260def stringfunction(func):
261    """Function attribute for string template functions"""
262    return _templatefunction(func, listname='out', stringtype='str')
263
264def unicodefunction(func):
265    """Function attribute for unicode template functions"""
266    return _templatefunction(func, listname='out', stringtype='unicode')
267
268
269# When executed as a script, run some testing code.
270if __name__ == '__main__':
271    ok = True
272    def expect(actual, expected):
273        global ok
274        if expected != actual:
275            print("error - got:\n%s" % repr(actual))
276            ok = False
277    class TestAll(Template):
278        """A test of all the $ forms"""
279        template = r"""
280            Bought: $count ${name}s$
281             at $$$price.
282            ${{
283                for i in xrange(count):
284                    self.write(TestCalls(vars()), "\n")    # inherit all the local $vars
285            }}
286            Total: $$${"%.2f" % (count * price)}
287        """
288    class TestCalls(Template):
289        """A recursive test"""
290        template = "$name$i ${*[TestCalls(name=name[0], i=n) for n in xrange(i)]}"
291    expect(
292        str(TestAll(count=5, name="template call", price=1.23)),
293        "Bought: 5 template calls at $1.23.\n"
294        "template call0 \n"
295        "template call1 t0 \n"
296        "template call2 t0 t1 t0 \n"
297        "template call3 t0 t1 t0 t2 t0 t1 t0 \n"
298        "template call4 t0 t1 t0 t2 t0 t1 t0 t3 t0 t1 t0 t2 t0 t1 t0 \n"
299        "Total: $6.15\n")
300    class TestBase(Template):
301        template = r"""
302            <head>$<head_template></head>
303            <body>$<body_template></body>
304        """
305    class TestDerived(TestBase):
306        head_template = "<title>$name</title>"
307        body_template = "${TestAll(vars())}"
308    expect(
309        str(TestDerived(count=4, name="template call", price=2.88)),
310        "<head><title>template call</title></head>\n"
311        "<body>"
312        "Bought: 4 template calls at $2.88.\n"
313        "template call0 \n"
314        "template call1 t0 \n"
315        "template call2 t0 t1 t0 \n"
316        "template call3 t0 t1 t0 t2 t0 t1 t0 \n"
317        "Total: $11.52\n"
318        "</body>\n")
319    class TestUnicode(UnicodeTemplate):
320        template = u"""
321            \N{Greek Small Letter Pi} = $pi
322        """
323    expect(
324        str(TestUnicode(pi = 3.14)),
325        u"\N{Greek Small Letter Pi} = 3.14\n")
326    goterror = False
327    try:
328        class TestError(Template):
329            template = 'Cost of an error: $0'
330    except SyntaxError:
331        goterror = True
332    if not goterror:
333        print('TestError failed')
334        ok = False
335    @stringfunction
336    def testBasic(name):
337        "Hello $name."
338    expect(testBasic('Henry'), "Hello Henry.")
339    @stringfunction
340    def testReps(a, count=5): r"""
341        ${{ if count == 0: return '' }}
342        $a${testReps(a, count - 1)}"""
343    expect(
344        testReps('foo'),
345        "foofoofoofoofoo")
346    @unicodefunction
347    def testUnicode(count=4): u"""
348        ${{ if not count: return '' }}
349        \N{BLACK STAR}${testUnicode(count - 1)}"""
350    expect(
351        testUnicode(count=10),
352        u"\N{BLACK STAR}" * 10)
353    if ok: print("OK")
354