1
# -*- coding: utf-8 -*-
6
A minimal template engine.
8
:copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
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
22
# Copyright notice: The `parse_data` method uses the string interpolation
23
# algorithm by Ka-Ping Yee which originally was part of `ltpl20.py`_
25
# .. _ltipl20.py: http://lfw.org/python/Itpl20.py
28
token_re = re.compile('%s|%s(?s)' % (
29
r'[uU]?[rR]?("""|\'\'\')((?<!\\)\\\1|.)*?\1',
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: ''
41
runtime_vars = dict.fromkeys(('Undefined', '__to_unicode', '__context',
42
'__write', '__write_many'))
45
def call_stmt(func, args, lineno):
46
return ast.CallFunc(ast.Name(func, lineno=lineno),
50
def tokenize(source, filename):
51
escape = escape_re.sub
52
escape_repl = lambda m: m.group(1) or ''
56
for match in directive_re.finditer(source):
57
start, end = match.span()
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()
64
yield lineno, 'code', args
66
yield lineno, 'cmd', (cmd, args)
67
lineno += source[start:end].count('\n')
71
yield lineno, 'data', escape(escape_repl, source[pos:])
74
def transform(node, filename):
75
root = ast.Module(None, node, lineno=1)
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):
84
node.value.decode('ascii')
86
node.value = node.value.decode('utf-8')
87
nodes.extend(node.getChildNodes())
91
class TemplateSyntaxError(SyntaxError):
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))
101
def __init__(self, gen, filename):
103
self.filename = filename
107
raise TemplateSyntaxError(msg, self.filename, self.lineno)
109
def parse_python(self, expr, type='exec'):
110
if isinstance(expr, unicode):
111
expr = '\xef\xbb\xbf' + expr.encode('utf-8')
113
node = parse(expr, type)
114
except SyntaxError, e:
115
raise TemplateSyntaxError(str(e), self.filename,
116
self.lineno + e.lineno - 1)
120
if hasattr(n, 'lineno'):
121
n.lineno = (n.lineno or 1) + self.lineno - 1
122
nodes.extend(n.getChildNodes())
125
def parse(self, needle=()):
126
start_lineno = self.lineno
129
for self.lineno, token, value in self.gen:
131
add(self.parse_data(value))
132
elif token == 'code':
133
add(self.parse_code(value.splitlines()))
137
return name, args, ast.Stmt(result, lineno=start_lineno)
138
if name in ('for', 'while'):
139
add(self.parse_loop(args, name))
141
add(self.parse_if(args))
143
self.fail('unknown directive %s' % name)
145
self.fail('unexpected end of template')
146
return ast.Stmt(result, lineno=start_lineno)
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'))
152
self.fail('unexpected data after ' + tag)
154
tag, value, rv.else_ = self.parse(('end' + type,))
156
self.fail('unexpected data after else')
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)
166
self.fail('unexpected data after else')
167
tag, value, cond.else_ = self.parse(('endif',))
169
expr = self.parse_python(value, 'eval')
170
tag, value, body = self.parse(('else', 'elif', 'endif'))
171
cond.tests.append((expr, body))
175
self.fail('unexpected data after endif')
178
def parse_code(self, lines):
180
for line in lines[1:]:
181
content = len(line.lstrip())
183
indent = len(line) - content
184
margin = min(margin, indent)
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]:
192
while lines and not lines[0]:
194
return self.parse_python('\n'.join(lines))
196
def parse_data(self, text):
197
start_lineno = lineno = self.lineno
202
def match_or_fail(pos):
203
match = token_re.match(text, pos)
205
self.fail('invalid syntax')
206
return match.group().strip(), match.end()
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')
213
def write_data(value):
215
nodes.append(ast.Const(value, lineno=lineno))
216
return value.count('\n')
220
offset = text.find('$', pos)
223
next = text[offset + 1]
226
lineno += write_data(text[pos:offset])
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)
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 '([':
245
token, pos = match_or_fail(pos)
246
if token in ('(', ')', '[', ']'):
247
level += token in '([' and 1 or -1
250
lineno += write_expr(text[offset + 1:pos])
252
lineno += write_data(text[pos:offset + 1])
253
pos = offset + 1 + (next == '$')
254
write_data(text[pos:])
256
return ast.Discard(call_stmt(len(nodes) == 1 and '__write' or
257
'__write_many', nodes, start_lineno),
261
class Context(object):
263
def __init__(self, namespace, charset, errors):
264
self.charset = charset
266
self._namespace = namespace
268
self._write = self._buffer.append
269
_extend = self._buffer.extend
272
__to_unicode=self.to_unicode,
275
__write_many=lambda *a: _extend(a)
278
def write(self, value):
279
self._write(self.to_unicode(value))
281
def to_unicode(self, value):
282
if isinstance(value, str):
283
return _decode_unicode(value, self.charset, self.errors)
284
return unicode(value)
286
def get_value(self, as_unicode=True):
287
rv = u''.join(self._buffer)
289
return rv.encode(self.charset, self.errors)
292
def __getitem__(self, key, default=undefined):
294
return self._namespace[key]
296
return getattr(builtins, key, default)
298
def get(self, key, default=None):
299
return self.__getitem__(key, default)
301
def __setitem__(self, key, value):
302
self._namespace[key] = value
304
def __delitem__(self, key):
305
del self._namespace[key]
308
class TemplateCodeGenerator(ModuleCodeGenerator):
310
def __init__(self, node, filename):
311
ModuleCodeGenerator.__init__(self, transform(node, filename))
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)
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.
324
'escape': utils.escape,
325
'url_quote': utils.url_quote,
326
'url_quote_plus': utils.url_quote_plus,
327
'url_encode': utils.url_encode
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
342
self.unicode_mode = unicode_mode
345
def from_file(cls, file, charset='utf-8', errors='strict',
346
unicode_mode=True, encoding=None):
347
"""Load a template from a file.
349
.. versionchanged:: 0.5
350
The encoding parameter was renamed to charset.
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.
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)
364
if isinstance(file, basestring):
368
data = _decode_unicode(f.read(), charset, errors)
372
return cls(data, getattr(f, 'name', '<template>'), charset,
373
errors, unicode_mode)
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.
380
:param context: the function accepts the same arguments as the
381
:class:`dict` constructor.
382
:return: the rendered template as string
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))
388
ns.update(dict(*args))
391
context = Context(ns, self.charset, self.errors)
392
exec self.code in context.runtime, context
393
return context.get_value(self.unicode_mode)
395
def substitute(self, *args, **kwargs):
396
"""For API compatibility with `string.Template`."""
397
return self.render(*args, **kwargs)