~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/support/werkzeug/templates.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mto: This revision was merged to the branch mainline in revision 18.
  • Revision ID: james.westby@ubuntu.com-20080622211713-inlv5k4eifxckelr
ImportĀ upstreamĀ versionĀ 1.7.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
r"""
3
 
    werkzeug.templates
4
 
    ~~~~~~~~~~~~~~~~~~
5
 
 
6
 
    A minimal template engine.
7
 
 
8
 
    :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
9
 
    :license: BSD License.
10
 
"""
11
 
import sys
12
 
import re
13
 
import __builtin__ as builtins
14
 
from compiler import ast, parse
15
 
from compiler.consts import SC_LOCAL, SC_GLOBAL, SC_FREE, SC_CELL
16
 
from compiler.pycodegen import ModuleCodeGenerator
17
 
from tokenize import PseudoToken
18
 
from werkzeug import utils
19
 
from werkzeug._internal import _decode_unicode
20
 
 
21
 
 
22
 
# Copyright notice: The `parse_data` method uses the string interpolation
23
 
# algorithm by Ka-Ping Yee which originally was part of `ltpl20.py`_
24
 
#
25
 
# .. _ltipl20.py: http://lfw.org/python/Itpl20.py
26
 
 
27
 
 
28
 
token_re = re.compile('%s|%s(?s)' % (
29
 
    r'[uU]?[rR]?("""|\'\'\')((?<!\\)\\\1|.)*?\1',
30
 
    PseudoToken
31
 
))
32
 
directive_re = re.compile(r'(?<!\\)<%(?:(#)|(py(?:thon)?\b)|'
33
 
                          r'(?:\s*(\w+))\s*)(.*?)\s*%>\n?(?s)')
34
 
escape_re = re.compile(r'\\\n|\\(\\|<%)')
35
 
namestart_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
36
 
undefined = type('UndefinedType', (object,), {
37
 
    '__iter__': lambda x: iter(()),
38
 
    '__repr__': lambda x: 'Undefined',
39
 
    '__str__':  lambda x: ''
40
 
})()
41
 
runtime_vars = dict.fromkeys(('Undefined', '__to_unicode', '__context',
42
 
                              '__write', '__write_many'))
43
 
 
44
 
 
45
 
def call_stmt(func, args, lineno):
46
 
    return ast.CallFunc(ast.Name(func, lineno=lineno),
47
 
                        args, lineno=lineno)
48
 
 
49
 
 
50
 
def tokenize(source, filename):
51
 
    escape = escape_re.sub
52
 
    escape_repl = lambda m: m.group(1) or ''
53
 
    lineno = 1
54
 
    pos = 0
55
 
 
56
 
    for match in directive_re.finditer(source):
57
 
        start, end = match.span()
58
 
        if start > pos:
59
 
            data = source[pos:start]
60
 
            yield lineno, 'data', escape(escape_repl, data)
61
 
            lineno += data.count('\n')
62
 
        is_comment, is_code, cmd, args = match.groups()
63
 
        if is_code:
64
 
            yield lineno, 'code', args
65
 
        elif not is_comment:
66
 
            yield lineno, 'cmd', (cmd, args)
67
 
        lineno += source[start:end].count('\n')
68
 
        pos = end
69
 
 
70
 
    if pos < len(source):
71
 
        yield lineno, 'data', escape(escape_repl, source[pos:])
72
 
 
73
 
 
74
 
def transform(node, filename):
75
 
    root = ast.Module(None, node, lineno=1)
76
 
    nodes = [root]
77
 
    while nodes:
78
 
        node = nodes.pop()
79
 
        node.filename = filename
80
 
        if node.__class__ in (ast.Printnl, ast.Print):
81
 
            node.dest = ast.Name('__context')
82
 
        elif node.__class__ is ast.Const and isinstance(node.value, str):
83
 
            try:
84
 
                node.value.decode('ascii')
85
 
            except UnicodeError:
86
 
                node.value = node.value.decode('utf-8')
87
 
        nodes.extend(node.getChildNodes())
88
 
    return root
89
 
 
90
 
 
91
 
class TemplateSyntaxError(SyntaxError):
92
 
 
93
 
    def __init__(self, msg, filename, lineno):
94
 
        from linecache import getline
95
 
        l = getline(filename, lineno)
96
 
        SyntaxError.__init__(self, msg, (filename, lineno, len(l) or 1, l))
97
 
 
98
 
 
99
 
class Parser(object):
100
 
 
101
 
    def __init__(self, gen, filename):
102
 
        self.gen = gen
103
 
        self.filename = filename
104
 
        self.lineno = 1
105
 
 
106
 
    def fail(self, msg):
107
 
        raise TemplateSyntaxError(msg, self.filename, self.lineno)
108
 
 
109
 
    def parse_python(self, expr, type='exec'):
110
 
        if isinstance(expr, unicode):
111
 
            expr = '\xef\xbb\xbf' + expr.encode('utf-8')
112
 
        try:
113
 
            node = parse(expr, type)
114
 
        except SyntaxError, e:
115
 
            raise TemplateSyntaxError(str(e), self.filename,
116
 
                                      self.lineno + e.lineno - 1)
117
 
        nodes = [node]
118
 
        while nodes:
119
 
            n = nodes.pop()
120
 
            if hasattr(n, 'lineno'):
121
 
                n.lineno = (n.lineno or 1) + self.lineno - 1
122
 
            nodes.extend(n.getChildNodes())
123
 
        return node.node
124
 
 
125
 
    def parse(self, needle=()):
126
 
        start_lineno = self.lineno
127
 
        result = []
128
 
        add = result.append
129
 
        for self.lineno, token, value in self.gen:
130
 
            if token == 'data':
131
 
                add(self.parse_data(value))
132
 
            elif token == 'code':
133
 
                add(self.parse_code(value.splitlines()))
134
 
            elif token == 'cmd':
135
 
                name, args = value
136
 
                if name in needle:
137
 
                    return name, args, ast.Stmt(result, lineno=start_lineno)
138
 
                if name in ('for', 'while'):
139
 
                    add(self.parse_loop(args, name))
140
 
                elif name == 'if':
141
 
                    add(self.parse_if(args))
142
 
                else:
143
 
                    self.fail('unknown directive %s' % name)
144
 
        if needle:
145
 
            self.fail('unexpected end of template')
146
 
        return ast.Stmt(result, lineno=start_lineno)
147
 
 
148
 
    def parse_loop(self, args, type):
149
 
        rv = self.parse_python('%s %s: pass' % (type, args), 'exec').nodes[0]
150
 
        tag, value, rv.body = self.parse(('end' + type, 'else'))
151
 
        if value:
152
 
            self.fail('unexpected data after ' + tag)
153
 
        if tag == 'else':
154
 
            tag, value, rv.else_ = self.parse(('end' + type,))
155
 
            if value:
156
 
                self.fail('unexpected data after else')
157
 
        return rv
158
 
 
159
 
    def parse_if(self, args):
160
 
        cond = self.parse_python('if %s: pass' % args).nodes[0]
161
 
        tag, value, body = self.parse(('else', 'elif', 'endif'))
162
 
        cond.tests[0] = (cond.tests[0][0], body)
163
 
        while 1:
164
 
            if tag == 'else':
165
 
                if value:
166
 
                    self.fail('unexpected data after else')
167
 
                tag, value, cond.else_ = self.parse(('endif',))
168
 
            elif tag == 'elif':
169
 
                expr = self.parse_python(value, 'eval')
170
 
                tag, value, body = self.parse(('else', 'elif', 'endif'))
171
 
                cond.tests.append((expr, body))
172
 
                continue
173
 
            break
174
 
        if value:
175
 
            self.fail('unexpected data after endif')
176
 
        return cond
177
 
 
178
 
    def parse_code(self, lines):
179
 
        margin = sys.maxint
180
 
        for line in lines[1:]:
181
 
            content = len(line.lstrip())
182
 
            if content:
183
 
                indent = len(line) - content
184
 
                margin = min(margin, indent)
185
 
        if lines:
186
 
            lines[0] = lines[0].lstrip()
187
 
        if margin < sys.maxint:
188
 
            for i in xrange(1, len(lines)):
189
 
                lines[i] = lines[i][margin:]
190
 
        while lines and not lines[-1]:
191
 
            lines.pop()
192
 
        while lines and not lines[0]:
193
 
            lines.pop(0)
194
 
        return self.parse_python('\n'.join(lines))
195
 
 
196
 
    def parse_data(self, text):
197
 
        start_lineno = lineno = self.lineno
198
 
        pos = 0
199
 
        end = len(text)
200
 
        nodes = []
201
 
 
202
 
        def match_or_fail(pos):
203
 
            match = token_re.match(text, pos)
204
 
            if match is None:
205
 
                self.fail('invalid syntax')
206
 
            return match.group().strip(), match.end()
207
 
 
208
 
        def write_expr(code):
209
 
            node = self.parse_python(code, 'eval')
210
 
            nodes.append(call_stmt('__to_unicode', [node], lineno))
211
 
            return code.count('\n')
212
 
 
213
 
        def write_data(value):
214
 
            if value:
215
 
                nodes.append(ast.Const(value, lineno=lineno))
216
 
                return value.count('\n')
217
 
            return 0
218
 
 
219
 
        while 1:
220
 
            offset = text.find('$', pos)
221
 
            if offset < 0:
222
 
                break
223
 
            next = text[offset + 1]
224
 
 
225
 
            if next == '{':
226
 
                lineno += write_data(text[pos:offset])
227
 
                pos = offset + 2
228
 
                level = 1
229
 
                while level:
230
 
                    token, pos = match_or_fail(pos)
231
 
                    if token in ('{', '}'):
232
 
                        level += token == '{' and 1 or -1
233
 
                lineno += write_expr(text[offset + 2:pos - 1])
234
 
            elif next in namestart_chars:
235
 
                lineno += write_data(text[pos:offset])
236
 
                token, pos = match_or_fail(offset + 1)
237
 
                while pos < end:
238
 
                    if text[pos] == '.' and pos + 1 < end and \
239
 
                       text[pos + 1] in namestart_chars:
240
 
                        token, pos = match_or_fail(pos + 1)
241
 
                    elif text[pos] in '([':
242
 
                        pos += 1
243
 
                        level = 1
244
 
                        while level:
245
 
                            token, pos = match_or_fail(pos)
246
 
                            if token in ('(', ')', '[', ']'):
247
 
                                level += token in '([' and 1 or -1
248
 
                    else:
249
 
                        break
250
 
                lineno += write_expr(text[offset + 1:pos])
251
 
            else:
252
 
                lineno += write_data(text[pos:offset + 1])
253
 
                pos = offset + 1 + (next == '$')
254
 
        write_data(text[pos:])
255
 
 
256
 
        return ast.Discard(call_stmt(len(nodes) == 1 and '__write' or
257
 
                           '__write_many', nodes, start_lineno),
258
 
                           lineno=start_lineno)
259
 
 
260
 
 
261
 
class Context(object):
262
 
 
263
 
    def __init__(self, namespace, charset, errors):
264
 
        self.charset = charset
265
 
        self.errors = errors
266
 
        self._namespace = namespace
267
 
        self._buffer = []
268
 
        self._write = self._buffer.append
269
 
        _extend = self._buffer.extend
270
 
        self.runtime = dict(
271
 
            Undefined=undefined,
272
 
            __to_unicode=self.to_unicode,
273
 
            __context=self,
274
 
            __write=self._write,
275
 
            __write_many=lambda *a: _extend(a)
276
 
        )
277
 
 
278
 
    def write(self, value):
279
 
        self._write(self.to_unicode(value))
280
 
 
281
 
    def to_unicode(self, value):
282
 
        if isinstance(value, str):
283
 
            return _decode_unicode(value, self.charset, self.errors)
284
 
        return unicode(value)
285
 
 
286
 
    def get_value(self, as_unicode=True):
287
 
        rv = u''.join(self._buffer)
288
 
        if not as_unicode:
289
 
            return rv.encode(self.charset, self.errors)
290
 
        return rv
291
 
 
292
 
    def __getitem__(self, key, default=undefined):
293
 
        try:
294
 
            return self._namespace[key]
295
 
        except KeyError:
296
 
            return getattr(builtins, key, default)
297
 
 
298
 
    def get(self, key, default=None):
299
 
        return self.__getitem__(key, default)
300
 
 
301
 
    def __setitem__(self, key, value):
302
 
        self._namespace[key] = value
303
 
 
304
 
    def __delitem__(self, key):
305
 
        del self._namespace[key]
306
 
 
307
 
 
308
 
class TemplateCodeGenerator(ModuleCodeGenerator):
309
 
 
310
 
    def __init__(self, node, filename):
311
 
        ModuleCodeGenerator.__init__(self, transform(node, filename))
312
 
 
313
 
    def _nameOp(self, prefix, name):
314
 
        if name in runtime_vars:
315
 
            return self.emit(prefix + '_GLOBAL', name)
316
 
        return ModuleCodeGenerator._nameOp(self, prefix, name)
317
 
 
318
 
 
319
 
class Template(object):
320
 
    """Represents a simple text based template.  It's a good idea to load such
321
 
    templates from files on the file system to get better debug output.
322
 
    """
323
 
    default_context = {
324
 
        'escape':           utils.escape,
325
 
        'url_quote':        utils.url_quote,
326
 
        'url_quote_plus':   utils.url_quote_plus,
327
 
        'url_encode':       utils.url_encode
328
 
    }
329
 
 
330
 
    def __init__(self, source, filename='<template>', charset='utf-8',
331
 
                 errors='strict', unicode_mode=True):
332
 
        if isinstance(source, str):
333
 
            source = _decode_unicode(source, charset, errors)
334
 
        if isinstance(filename, unicode):
335
 
            filename = filename.encode('utf-8')
336
 
        node = Parser(tokenize(u'\n'.join(source.splitlines()),
337
 
                               filename), filename).parse()
338
 
        self.code = TemplateCodeGenerator(node, filename).getCode()
339
 
        self.filename = filename
340
 
        self.charset = charset
341
 
        self.errors = errors
342
 
        self.unicode_mode = unicode_mode
343
 
 
344
 
    @classmethod
345
 
    def from_file(cls, file, charset='utf-8', errors='strict',
346
 
                  unicode_mode=True, encoding=None):
347
 
        """Load a template from a file.
348
 
 
349
 
        .. versionchanged:: 0.5
350
 
            The encoding parameter was renamed to charset.
351
 
 
352
 
        :param file: a filename or file object to load the template from.
353
 
        :param charset: the charset of the template to load.
354
 
        :param errors: the error behavior of the charset decoding.
355
 
        :param unicode_mode: set to `False` to disable unicode mode.
356
 
        :return: a template
357
 
        """
358
 
        if encoding is not None:
359
 
            from warnings import warn
360
 
            warn(DeprecationWarning('the encoding parameter is deprecated. '
361
 
                                    'use charset instead.'), stacklevel=2)
362
 
            charset = encoding
363
 
        close = False
364
 
        if isinstance(file, basestring):
365
 
            f = open(file, 'r')
366
 
            close = True
367
 
        try:
368
 
            data = _decode_unicode(f.read(), charset, errors)
369
 
        finally:
370
 
            if close:
371
 
                f.close()
372
 
        return cls(data, getattr(f, 'name', '<template>'), charset,
373
 
                   errors, unicode_mode)
374
 
 
375
 
    def render(self, *args, **kwargs):
376
 
        """This function accepts either a dict or some keyword arguments which
377
 
        will then be the context the template is evaluated in.  The return
378
 
        value will be the rendered template.
379
 
 
380
 
        :param context: the function accepts the same arguments as the
381
 
                        :class:`dict` constructor.
382
 
        :return: the rendered template as string
383
 
        """
384
 
        ns = self.default_context.copy()
385
 
        if len(args) == 1 and isinstance(args[0], utils.MultiDict):
386
 
            ns.update(args[0].to_dict(flat=True))
387
 
        else:
388
 
            ns.update(dict(*args))
389
 
        if kwargs:
390
 
            ns.update(kwargs)
391
 
        context = Context(ns, self.charset, self.errors)
392
 
        exec self.code in context.runtime, context
393
 
        return context.get_value(self.unicode_mode)
394
 
 
395
 
    def substitute(self, *args, **kwargs):
396
 
        """For API compatibility with `string.Template`."""
397
 
        return self.render(*args, **kwargs)