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 class Template(object):
92 """A compiled template.
94 We compile into Python from the given template_string. You can generate
95 the template from variables with generate().
97 def __init__(self, template_string, name="<string>", loader=None,
98 compress_whitespace=None):
100 if compress_whitespace is None:
101 compress_whitespace = name.endswith(".html") or \
103 reader = _TemplateReader(name, template_string)
104 self.file = _File(_parse(reader))
105 self.code = self._generate_python(loader, compress_whitespace)
107 self.compiled = compile(self.code, self.name, "exec")
109 formatted_code = _format_code(self.code).rstrip()
110 logging.error("%s code:\n%s", self.name, formatted_code)
113 def generate(self, **kwargs):
114 """Generate this template with the given arguments."""
116 "escape": escape.xhtml_escape,
117 "url_escape": escape.url_escape,
118 "json_encode": escape.json_encode,
119 "squeeze": escape.squeeze,
120 "datetime": datetime,
122 namespace.update(kwargs)
123 exec self.compiled in namespace
124 execute = namespace["_execute"]
128 formatted_code = _format_code(self.code).rstrip()
129 logging.error("%s code:\n%s", self.name, formatted_code)
132 def _generate_python(self, loader, compress_whitespace):
133 buffer = cStringIO.StringIO()
136 ancestors = self._get_ancestors(loader)
138 for ancestor in ancestors:
139 ancestor.find_named_blocks(loader, named_blocks)
140 self.file.find_named_blocks(loader, named_blocks)
141 writer = _CodeWriter(buffer, named_blocks, loader, self,
143 ancestors[0].generate(writer)
144 return buffer.getvalue()
148 def _get_ancestors(self, loader):
149 ancestors = [self.file]
150 for chunk in self.file.body.chunks:
151 if isinstance(chunk, _ExtendsBlock):
153 raise ParseError("{% extends %} block found, but no "
155 template = loader.load(chunk.name, self.name)
156 ancestors.extend(template._get_ancestors(loader))
160 class Loader(object):
161 """A template loader that loads from a single root directory.
163 You must use a template loader to use template constructs like
164 {% extends %} and {% include %}. Loader caches all templates after
165 they are loaded the first time.
167 def __init__(self, root_directory):
168 self.root = os.path.abspath(root_directory)
174 def resolve_path(self, name, parent_path=None):
175 if parent_path and not parent_path.startswith("<") and \
176 not parent_path.startswith("/") and \
177 not name.startswith("/"):
178 current_path = os.path.join(self.root, parent_path)
179 file_dir = os.path.dirname(os.path.abspath(current_path))
180 relative_path = os.path.abspath(os.path.join(file_dir, name))
181 if relative_path.startswith(self.root):
182 name = relative_path[len(self.root) + 1:]
185 def load(self, name, parent_path=None):
186 name = self.resolve_path(name, parent_path=parent_path)
187 if name not in self.templates:
188 path = os.path.join(self.root, name)
190 self.templates[name] = Template(f.read(), name=name, loader=self)
192 return self.templates[name]
196 def each_child(self):
199 def generate(self, writer):
200 raise NotImplementedError()
202 def find_named_blocks(self, loader, named_blocks):
203 for child in self.each_child():
204 child.find_named_blocks(loader, named_blocks)
208 def __init__(self, body):
211 def generate(self, writer):
212 writer.write_line("def _execute():")
213 with writer.indent():
214 writer.write_line("_buffer = []")
215 self.body.generate(writer)
216 writer.write_line("return ''.join(_buffer)")
218 def each_child(self):
223 class _ChunkList(_Node):
224 def __init__(self, chunks):
227 def generate(self, writer):
228 for chunk in self.chunks:
229 chunk.generate(writer)
231 def each_child(self):
235 class _NamedBlock(_Node):
236 def __init__(self, name, body=None):
240 def each_child(self):
243 def generate(self, writer):
244 writer.named_blocks[self.name].generate(writer)
246 def find_named_blocks(self, loader, named_blocks):
247 named_blocks[self.name] = self.body
248 _Node.find_named_blocks(self, loader, named_blocks)
251 class _ExtendsBlock(_Node):
252 def __init__(self, name):
256 class _IncludeBlock(_Node):
257 def __init__(self, name, reader):
259 self.template_name = reader.name
261 def find_named_blocks(self, loader, named_blocks):
262 included = loader.load(self.name, self.template_name)
263 included.file.find_named_blocks(loader, named_blocks)
265 def generate(self, writer):
266 included = writer.loader.load(self.name, self.template_name)
267 old = writer.current_template
268 writer.current_template = included
269 included.file.body.generate(writer)
270 writer.current_template = old
273 class _ApplyBlock(_Node):
274 def __init__(self, method, body=None):
278 def each_child(self):
281 def generate(self, writer):
282 method_name = "apply%d" % writer.apply_counter
283 writer.apply_counter += 1
284 writer.write_line("def %s():" % method_name)
285 with writer.indent():
286 writer.write_line("_buffer = []")
287 self.body.generate(writer)
288 writer.write_line("return ''.join(_buffer)")
289 writer.write_line("_buffer.append(%s(%s()))" % (
290 self.method, method_name))
293 class _ControlBlock(_Node):
294 def __init__(self, statement, body=None):
295 self.statement = statement
298 def each_child(self):
301 def generate(self, writer):
302 writer.write_line("%s:" % self.statement)
303 with writer.indent():
304 self.body.generate(writer)
307 class _IntermediateControlBlock(_Node):
308 def __init__(self, statement):
309 self.statement = statement
311 def generate(self, writer):
312 writer.write_line("%s:" % self.statement, writer.indent_size() - 1)
315 class _Statement(_Node):
316 def __init__(self, statement):
317 self.statement = statement
319 def generate(self, writer):
320 writer.write_line(self.statement)
323 class _Expression(_Node):
324 def __init__(self, expression):
325 self.expression = expression
327 def generate(self, writer):
328 writer.write_line("_tmp = %s" % self.expression)
329 writer.write_line("if isinstance(_tmp, str): _buffer.append(_tmp)")
330 writer.write_line("elif isinstance(_tmp, unicode): "
331 "_buffer.append(_tmp.encode('utf-8'))")
332 writer.write_line("else: _buffer.append(str(_tmp))")
336 def __init__(self, value):
339 def generate(self, writer):
342 # Compress lots of white space to a single character. If the whitespace
343 # breaks a line, have it continue to break a line, but just with a
344 # single \n character
345 if writer.compress_whitespace and "<pre>" not in value:
346 value = re.sub(r"([\t ]+)", " ", value)
347 value = re.sub(r"(\s*\n\s*)", "\n", value)
350 writer.write_line('_buffer.append(%r)' % value)
353 class ParseError(Exception):
354 """Raised for template syntax errors."""
358 class _CodeWriter(object):
359 def __init__(self, file, named_blocks, loader, current_template,
360 compress_whitespace):
362 self.named_blocks = named_blocks
364 self.current_template = current_template
365 self.compress_whitespace = compress_whitespace
366 self.apply_counter = 0
372 def indent_size(self):
379 def __exit__(self, *args):
380 assert self._indent > 0
383 def write_line(self, line, indent=None):
385 indent = self._indent
386 for i in xrange(indent):
388 print >> self.file, line
391 class _TemplateReader(object):
392 def __init__(self, name, text):
398 def find(self, needle, start=0, end=None):
399 assert start >= 0, start
403 index = self.text.find(needle, start)
407 index = self.text.find(needle, start, end)
412 def consume(self, count=None):
414 count = len(self.text) - self.pos
415 newpos = self.pos + count
416 self.line += self.text.count("\n", self.pos, newpos)
417 s = self.text[self.pos:newpos]
422 return len(self.text) - self.pos
425 return self.remaining()
427 def __getitem__(self, key):
428 if type(key) is slice:
430 start, stop, step = slice.indices(size)
431 if start is None: start = self.pos
432 else: start += self.pos
433 if stop is not None: stop += self.pos
434 return self.text[slice(start, stop, step)]
436 return self.text[key]
438 return self.text[self.pos + key]
441 return self.text[self.pos:]
444 def _format_code(code):
445 lines = code.splitlines()
446 format = "%%%dd %%s\n" % len(repr(len(lines) + 1))
447 return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
450 def _parse(reader, in_block=None):
451 body = _ChunkList([])
453 # Find next template directive
456 curly = reader.find("{", curly)
457 if curly == -1 or curly + 1 == reader.remaining():
460 raise ParseError("Missing {%% end %%} block for %s" %
462 body.chunks.append(_Text(reader.consume()))
464 # If the first curly brace is not the start of a special token,
465 # start searching from the character after it
466 if reader[curly + 1] not in ("{", "%"):
469 # When there are more than 2 curlies in a row, use the
470 # innermost ones. This is useful when generating languages
471 # like latex where curlies are also meaningful
472 if (curly + 2 < reader.remaining() and
473 reader[curly + 1] == '{' and reader[curly + 2] == '{'):
478 # Append any text before the special token
480 body.chunks.append(_Text(reader.consume(curly)))
482 start_brace = reader.consume(2)
486 if start_brace == "{{":
487 end = reader.find("}}")
488 if end == -1 or reader.find("\n", 0, end) != -1:
489 raise ParseError("Missing end expression }} on line %d" % line)
490 contents = reader.consume(end).strip()
493 raise ParseError("Empty expression on line %d" % line)
494 body.chunks.append(_Expression(contents))
498 assert start_brace == "{%", start_brace
499 end = reader.find("%}")
500 if end == -1 or reader.find("\n", 0, end) != -1:
501 raise ParseError("Missing end block %%} on line %d" % line)
502 contents = reader.consume(end).strip()
505 raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
507 operator, space, suffix = contents.partition(" ")
508 suffix = suffix.strip()
510 # Intermediate ("else", "elif", etc) blocks
511 intermediate_blocks = {
512 "else": set(["if", "for", "while"]),
514 "except": set(["try"]),
515 "finally": set(["try"]),
517 allowed_parents = intermediate_blocks.get(operator)
518 if allowed_parents is not None:
520 raise ParseError("%s outside %s block" %
521 (operator, allowed_parents))
522 if in_block not in allowed_parents:
523 raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))
524 body.chunks.append(_IntermediateControlBlock(contents))
528 elif operator == "end":
530 raise ParseError("Extra {%% end %%} block on line %d" % line)
533 elif operator in ("extends", "include", "set", "import", "comment"):
534 if operator == "comment":
536 if operator == "extends":
537 suffix = suffix.strip('"').strip("'")
539 raise ParseError("extends missing file path on line %d" % line)
540 block = _ExtendsBlock(suffix)
541 elif operator == "import":
543 raise ParseError("import missing statement on line %d" % line)
544 block = _Statement(contents)
545 elif operator == "include":
546 suffix = suffix.strip('"').strip("'")
548 raise ParseError("include missing file path on line %d" % line)
549 block = _IncludeBlock(suffix, reader)
550 elif operator == "set":
552 raise ParseError("set missing statement on line %d" % line)
553 block = _Statement(suffix)
554 body.chunks.append(block)
557 elif operator in ("apply", "block", "try", "if", "for", "while"):
558 # parse inner body recursively
559 block_body = _parse(reader, operator)
560 if operator == "apply":
562 raise ParseError("apply missing method name on line %d" % line)
563 block = _ApplyBlock(suffix, block_body)
564 elif operator == "block":
566 raise ParseError("block missing name on line %d" % line)
567 block = _NamedBlock(suffix, block_body)
569 block = _ControlBlock(contents, block_body)
570 body.chunks.append(block)
574 raise ParseError("unknown operator: %r" % operator)