3
# Copyright 2009 Facebook
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6
# not use this file except in compliance with the License. You may obtain
7
# a copy of the License at
9
# http://www.apache.org/licenses/LICENSE-2.0
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations
17
"""A simple template system that compiles templates to Python code.
19
Basic usage looks like:
21
t = template.Template("<html>{{ myvalue }}</html>")
22
print t.generate(myvalue="XXX")
24
Loader is a class that loads templates from a root directory and caches
25
the compiled templates:
27
loader = template.Loader("/home/btaylor")
28
print loader.load("test.html").generate(myvalue="XXX")
30
We compile all templates to raw Python. Error-reporting is currently... uh,
31
interesting. Syntax for the templates
36
<title>{% block title %}Default title{% end %}</title>
40
{% for student in students %}
42
<li>{{ escape(student.name) }}</li>
50
{% extends "base.html" %}
52
{% block title %}A bolder title{% end %}
55
<li><span style="bold">{{ escape(student.name) }}</span></li>
58
Unlike most other template systems, we do not put any restrictions on the
59
expressions you can include in your statements. if and for blocks get
60
translated exactly into Python, do you can do complex expressions like:
62
{% for student in [p for p in people if p.student and p.age > 23] %}
63
<li>{{ escape(student.name) }}</li>
66
Translating directly to Python means you can apply functions to expressions
67
easily, like the escape() function in the examples above. You can pass
68
functions in to your template just like any other variable:
73
template.execute(add=add)
78
We provide the functions escape(), url_escape(), json_encode(), and squeeze()
79
to all templates by default.
82
from __future__ import with_statement
91
_log = logging.getLogger('tornado.template')
93
class Template(object):
94
"""A compiled template.
96
We compile into Python from the given template_string. You can generate
97
the template from variables with generate().
99
def __init__(self, template_string, name="<string>", loader=None,
100
compress_whitespace=None):
102
if compress_whitespace is None:
103
compress_whitespace = name.endswith(".html") or \
105
reader = _TemplateReader(name, template_string)
106
self.file = _File(_parse(reader))
107
self.code = self._generate_python(loader, compress_whitespace)
109
self.compiled = compile(self.code, self.name, "exec")
111
formatted_code = _format_code(self.code).rstrip()
112
_log.error("%s code:\n%s", self.name, formatted_code)
115
def generate(self, **kwargs):
116
"""Generate this template with the given arguments."""
118
"escape": escape.xhtml_escape,
119
"url_escape": escape.url_escape,
120
"json_encode": escape.json_encode,
121
"squeeze": escape.squeeze,
122
"datetime": datetime,
124
namespace.update(kwargs)
125
exec self.compiled in namespace
126
execute = namespace["_execute"]
130
formatted_code = _format_code(self.code).rstrip()
131
_log.error("%s code:\n%s", self.name, formatted_code)
134
def _generate_python(self, loader, compress_whitespace):
135
buffer = cStringIO.StringIO()
138
ancestors = self._get_ancestors(loader)
140
for ancestor in ancestors:
141
ancestor.find_named_blocks(loader, named_blocks)
142
self.file.find_named_blocks(loader, named_blocks)
143
writer = _CodeWriter(buffer, named_blocks, loader, self,
145
ancestors[0].generate(writer)
146
return buffer.getvalue()
150
def _get_ancestors(self, loader):
151
ancestors = [self.file]
152
for chunk in self.file.body.chunks:
153
if isinstance(chunk, _ExtendsBlock):
155
raise ParseError("{% extends %} block found, but no "
157
template = loader.load(chunk.name, self.name)
158
ancestors.extend(template._get_ancestors(loader))
162
class Loader(object):
163
"""A template loader that loads from a single root directory.
165
You must use a template loader to use template constructs like
166
{% extends %} and {% include %}. Loader caches all templates after
167
they are loaded the first time.
169
def __init__(self, root_directory):
170
self.root = os.path.abspath(root_directory)
176
def resolve_path(self, name, parent_path=None):
177
if parent_path and not parent_path.startswith("<") and \
178
not parent_path.startswith("/") and \
179
not name.startswith("/"):
180
current_path = os.path.join(self.root, parent_path)
181
file_dir = os.path.dirname(os.path.abspath(current_path))
182
relative_path = os.path.abspath(os.path.join(file_dir, name))
183
if relative_path.startswith(self.root):
184
name = relative_path[len(self.root) + 1:]
187
def load(self, name, parent_path=None):
188
name = self.resolve_path(name, parent_path=parent_path)
189
if name not in self.templates:
190
path = os.path.join(self.root, name)
192
self.templates[name] = Template(f.read(), name=name, loader=self)
194
return self.templates[name]
198
def each_child(self):
201
def generate(self, writer):
202
raise NotImplementedError()
204
def find_named_blocks(self, loader, named_blocks):
205
for child in self.each_child():
206
child.find_named_blocks(loader, named_blocks)
210
def __init__(self, body):
213
def generate(self, writer):
214
writer.write_line("def _execute():")
215
with writer.indent():
216
writer.write_line("_buffer = []")
217
self.body.generate(writer)
218
writer.write_line("return ''.join(_buffer)")
220
def each_child(self):
225
class _ChunkList(_Node):
226
def __init__(self, chunks):
229
def generate(self, writer):
230
for chunk in self.chunks:
231
chunk.generate(writer)
233
def each_child(self):
237
class _NamedBlock(_Node):
238
def __init__(self, name, body=None):
242
def each_child(self):
245
def generate(self, writer):
246
writer.named_blocks[self.name].generate(writer)
248
def find_named_blocks(self, loader, named_blocks):
249
named_blocks[self.name] = self.body
250
_Node.find_named_blocks(self, loader, named_blocks)
253
class _ExtendsBlock(_Node):
254
def __init__(self, name):
258
class _IncludeBlock(_Node):
259
def __init__(self, name, reader):
261
self.template_name = reader.name
263
def find_named_blocks(self, loader, named_blocks):
264
included = loader.load(self.name, self.template_name)
265
included.file.find_named_blocks(loader, named_blocks)
267
def generate(self, writer):
268
included = writer.loader.load(self.name, self.template_name)
269
old = writer.current_template
270
writer.current_template = included
271
included.file.body.generate(writer)
272
writer.current_template = old
275
class _ApplyBlock(_Node):
276
def __init__(self, method, body=None):
280
def each_child(self):
283
def generate(self, writer):
284
method_name = "apply%d" % writer.apply_counter
285
writer.apply_counter += 1
286
writer.write_line("def %s():" % method_name)
287
with writer.indent():
288
writer.write_line("_buffer = []")
289
self.body.generate(writer)
290
writer.write_line("return ''.join(_buffer)")
291
writer.write_line("_buffer.append(%s(%s()))" % (
292
self.method, method_name))
295
class _ControlBlock(_Node):
296
def __init__(self, statement, body=None):
297
self.statement = statement
300
def each_child(self):
303
def generate(self, writer):
304
writer.write_line("%s:" % self.statement)
305
with writer.indent():
306
self.body.generate(writer)
309
class _IntermediateControlBlock(_Node):
310
def __init__(self, statement):
311
self.statement = statement
313
def generate(self, writer):
314
writer.write_line("%s:" % self.statement, writer.indent_size() - 1)
317
class _Statement(_Node):
318
def __init__(self, statement):
319
self.statement = statement
321
def generate(self, writer):
322
writer.write_line(self.statement)
325
class _Expression(_Node):
326
def __init__(self, expression):
327
self.expression = expression
329
def generate(self, writer):
330
writer.write_line("_tmp = %s" % self.expression)
331
writer.write_line("if isinstance(_tmp, str): _buffer.append(_tmp)")
332
writer.write_line("elif isinstance(_tmp, unicode): "
333
"_buffer.append(_tmp.encode('utf-8'))")
334
writer.write_line("else: _buffer.append(str(_tmp))")
338
def __init__(self, value):
341
def generate(self, writer):
344
# Compress lots of white space to a single character. If the whitespace
345
# breaks a line, have it continue to break a line, but just with a
346
# single \n character
347
if writer.compress_whitespace and "<pre>" not in value:
348
value = re.sub(r"([\t ]+)", " ", value)
349
value = re.sub(r"(\s*\n\s*)", "\n", value)
352
writer.write_line('_buffer.append(%r)' % value)
355
class ParseError(Exception):
356
"""Raised for template syntax errors."""
360
class _CodeWriter(object):
361
def __init__(self, file, named_blocks, loader, current_template,
362
compress_whitespace):
364
self.named_blocks = named_blocks
366
self.current_template = current_template
367
self.compress_whitespace = compress_whitespace
368
self.apply_counter = 0
374
def indent_size(self):
381
def __exit__(self, *args):
382
assert self._indent > 0
385
def write_line(self, line, indent=None):
387
indent = self._indent
388
for i in xrange(indent):
390
print >> self.file, line
393
class _TemplateReader(object):
394
def __init__(self, name, text):
400
def find(self, needle, start=0, end=None):
401
assert start >= 0, start
405
index = self.text.find(needle, start)
409
index = self.text.find(needle, start, end)
414
def consume(self, count=None):
416
count = len(self.text) - self.pos
417
newpos = self.pos + count
418
self.line += self.text.count("\n", self.pos, newpos)
419
s = self.text[self.pos:newpos]
424
return len(self.text) - self.pos
427
return self.remaining()
429
def __getitem__(self, key):
430
if type(key) is slice:
432
start, stop, step = slice.indices(size)
433
if start is None: start = self.pos
434
else: start += self.pos
435
if stop is not None: stop += self.pos
436
return self.text[slice(start, stop, step)]
438
return self.text[key]
440
return self.text[self.pos + key]
443
return self.text[self.pos:]
446
def _format_code(code):
447
lines = code.splitlines()
448
format = "%%%dd %%s\n" % len(repr(len(lines) + 1))
449
return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
452
def _parse(reader, in_block=None):
453
body = _ChunkList([])
455
# Find next template directive
458
curly = reader.find("{", curly)
459
if curly == -1 or curly + 1 == reader.remaining():
462
raise ParseError("Missing {%% end %%} block for %s" %
464
body.chunks.append(_Text(reader.consume()))
466
# If the first curly brace is not the start of a special token,
467
# start searching from the character after it
468
if reader[curly + 1] not in ("{", "%"):
471
# When there are more than 2 curlies in a row, use the
472
# innermost ones. This is useful when generating languages
473
# like latex where curlies are also meaningful
474
if (curly + 2 < reader.remaining() and
475
reader[curly + 1] == '{' and reader[curly + 2] == '{'):
480
# Append any text before the special token
482
body.chunks.append(_Text(reader.consume(curly)))
484
start_brace = reader.consume(2)
488
if start_brace == "{{":
489
end = reader.find("}}")
490
if end == -1 or reader.find("\n", 0, end) != -1:
491
raise ParseError("Missing end expression }} on line %d" % line)
492
contents = reader.consume(end).strip()
495
raise ParseError("Empty expression on line %d" % line)
496
body.chunks.append(_Expression(contents))
500
assert start_brace == "{%", start_brace
501
end = reader.find("%}")
502
if end == -1 or reader.find("\n", 0, end) != -1:
503
raise ParseError("Missing end block %%} on line %d" % line)
504
contents = reader.consume(end).strip()
507
raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
509
operator, space, suffix = contents.partition(" ")
510
suffix = suffix.strip()
512
# Intermediate ("else", "elif", etc) blocks
513
intermediate_blocks = {
514
"else": set(["if", "for", "while"]),
516
"except": set(["try"]),
517
"finally": set(["try"]),
519
allowed_parents = intermediate_blocks.get(operator)
520
if allowed_parents is not None:
522
raise ParseError("%s outside %s block" %
523
(operator, allowed_parents))
524
if in_block not in allowed_parents:
525
raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))
526
body.chunks.append(_IntermediateControlBlock(contents))
530
elif operator == "end":
532
raise ParseError("Extra {%% end %%} block on line %d" % line)
535
elif operator in ("extends", "include", "set", "import", "comment"):
536
if operator == "comment":
538
if operator == "extends":
539
suffix = suffix.strip('"').strip("'")
541
raise ParseError("extends missing file path on line %d" % line)
542
block = _ExtendsBlock(suffix)
543
elif operator == "import":
545
raise ParseError("import missing statement on line %d" % line)
546
block = _Statement(contents)
547
elif operator == "include":
548
suffix = suffix.strip('"').strip("'")
550
raise ParseError("include missing file path on line %d" % line)
551
block = _IncludeBlock(suffix, reader)
552
elif operator == "set":
554
raise ParseError("set missing statement on line %d" % line)
555
block = _Statement(suffix)
556
body.chunks.append(block)
559
elif operator in ("apply", "block", "try", "if", "for", "while"):
560
# parse inner body recursively
561
block_body = _parse(reader, operator)
562
if operator == "apply":
564
raise ParseError("apply missing method name on line %d" % line)
565
block = _ApplyBlock(suffix, block_body)
566
elif operator == "block":
568
raise ParseError("block missing name on line %d" % line)
569
block = _NamedBlock(suffix, block_body)
571
block = _ControlBlock(contents, block_body)
572
body.chunks.append(block)
576
raise ParseError("unknown operator: %r" % operator)