]> arthur.barton.de Git - bup.git/blob - lib/tornado/template.py
Always publish (l)utimes in helpers when available and fix type conversions.
[bup.git] / lib / tornado / template.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2009 Facebook
4 #
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
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
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
15 # under the License.
16
17 """A simple template system that compiles templates to Python code.
18
19 Basic usage looks like:
20
21     t = template.Template("<html>{{ myvalue }}</html>")
22     print t.generate(myvalue="XXX")
23
24 Loader is a class that loads templates from a root directory and caches
25 the compiled templates:
26
27     loader = template.Loader("/home/btaylor")
28     print loader.load("test.html").generate(myvalue="XXX")
29
30 We compile all templates to raw Python. Error-reporting is currently... uh,
31 interesting. Syntax for the templates
32
33     ### base.html
34     <html>
35       <head>
36         <title>{% block title %}Default title{% end %}</title>
37       </head>
38       <body>
39         <ul>
40           {% for student in students %}
41             {% block student %}
42               <li>{{ escape(student.name) }}</li>
43             {% end %}
44           {% end %}
45         </ul>
46       </body>
47     </html>
48
49     ### bold.html
50     {% extends "base.html" %}
51
52     {% block title %}A bolder title{% end %}
53
54     {% block student %}
55       <li><span style="bold">{{ escape(student.name) }}</span></li>
56     {% block %}
57
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:
61
62    {% for student in [p for p in people if p.student and p.age > 23] %}
63      <li>{{ escape(student.name) }}</li>
64    {% end %}
65
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:
69
70    ### Python code
71    def add(x, y):
72       return x + y
73    template.execute(add=add)
74
75    ### The template
76    {{ add(1, 2) }}
77
78 We provide the functions escape(), url_escape(), json_encode(), and squeeze()
79 to all templates by default.
80 """
81
82 from __future__ import with_statement
83
84 import cStringIO
85 import datetime
86 import escape
87 import logging
88 import os.path
89 import re
90
91 class Template(object):
92     """A compiled template.
93
94     We compile into Python from the given template_string. You can generate
95     the template from variables with generate().
96     """
97     def __init__(self, template_string, name="<string>", loader=None,
98                  compress_whitespace=None):
99         self.name = name
100         if compress_whitespace is None:
101             compress_whitespace = name.endswith(".html") or \
102                 name.endswith(".js")
103         reader = _TemplateReader(name, template_string)
104         self.file = _File(_parse(reader))
105         self.code = self._generate_python(loader, compress_whitespace)
106         try:
107             self.compiled = compile(self.code, self.name, "exec")
108         except:
109             formatted_code = _format_code(self.code).rstrip()
110             logging.error("%s code:\n%s", self.name, formatted_code)
111             raise
112
113     def generate(self, **kwargs):
114         """Generate this template with the given arguments."""
115         namespace = {
116             "escape": escape.xhtml_escape,
117             "url_escape": escape.url_escape,
118             "json_encode": escape.json_encode,
119             "squeeze": escape.squeeze,
120             "datetime": datetime,
121         }
122         namespace.update(kwargs)
123         exec self.compiled in namespace
124         execute = namespace["_execute"]
125         try:
126             return execute()
127         except:
128             formatted_code = _format_code(self.code).rstrip()
129             logging.error("%s code:\n%s", self.name, formatted_code)
130             raise
131
132     def _generate_python(self, loader, compress_whitespace):
133         buffer = cStringIO.StringIO()
134         try:
135             named_blocks = {}
136             ancestors = self._get_ancestors(loader)
137             ancestors.reverse()
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,
142                                  compress_whitespace)
143             ancestors[0].generate(writer)
144             return buffer.getvalue()
145         finally:
146             buffer.close()
147
148     def _get_ancestors(self, loader):
149         ancestors = [self.file]
150         for chunk in self.file.body.chunks:
151             if isinstance(chunk, _ExtendsBlock):
152                 if not loader:
153                     raise ParseError("{% extends %} block found, but no "
154                                      "template loader")
155                 template = loader.load(chunk.name, self.name)
156                 ancestors.extend(template._get_ancestors(loader))
157         return ancestors
158
159
160 class Loader(object):
161     """A template loader that loads from a single root directory.
162
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.
166     """
167     def __init__(self, root_directory):
168         self.root = os.path.abspath(root_directory)
169         self.templates = {}
170
171     def reset(self):
172       self.templates = {}
173
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:]
183         return name
184
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)
189             f = open(path, "r")
190             self.templates[name] = Template(f.read(), name=name, loader=self)
191             f.close()
192         return self.templates[name]
193
194
195 class _Node(object):
196     def each_child(self):
197         return ()
198
199     def generate(self, writer):
200         raise NotImplementedError()
201
202     def find_named_blocks(self, loader, named_blocks):
203         for child in self.each_child():
204             child.find_named_blocks(loader, named_blocks)
205
206
207 class _File(_Node):
208     def __init__(self, body):
209         self.body = body
210
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)")
217
218     def each_child(self):
219         return (self.body,)
220
221
222
223 class _ChunkList(_Node):
224     def __init__(self, chunks):
225         self.chunks = chunks
226
227     def generate(self, writer):
228         for chunk in self.chunks:
229             chunk.generate(writer)
230
231     def each_child(self):
232         return self.chunks
233
234
235 class _NamedBlock(_Node):
236     def __init__(self, name, body=None):
237         self.name = name
238         self.body = body
239
240     def each_child(self):
241         return (self.body,)
242
243     def generate(self, writer):
244         writer.named_blocks[self.name].generate(writer)
245
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)
249
250
251 class _ExtendsBlock(_Node):
252     def __init__(self, name):
253         self.name = name
254
255
256 class _IncludeBlock(_Node):
257     def __init__(self, name, reader):
258         self.name = name
259         self.template_name = reader.name
260
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)
264
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
271
272
273 class _ApplyBlock(_Node):
274     def __init__(self, method, body=None):
275         self.method = method
276         self.body = body
277
278     def each_child(self):
279         return (self.body,)
280
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))
291
292
293 class _ControlBlock(_Node):
294     def __init__(self, statement, body=None):
295         self.statement = statement
296         self.body = body
297
298     def each_child(self):
299         return (self.body,)
300
301     def generate(self, writer):
302         writer.write_line("%s:" % self.statement)
303         with writer.indent():
304             self.body.generate(writer)
305
306
307 class _IntermediateControlBlock(_Node):
308     def __init__(self, statement):
309         self.statement = statement
310
311     def generate(self, writer):
312         writer.write_line("%s:" % self.statement, writer.indent_size() - 1)
313
314
315 class _Statement(_Node):
316     def __init__(self, statement):
317         self.statement = statement
318
319     def generate(self, writer):
320         writer.write_line(self.statement)
321
322
323 class _Expression(_Node):
324     def __init__(self, expression):
325         self.expression = expression
326
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))")
333
334
335 class _Text(_Node):
336     def __init__(self, value):
337         self.value = value
338
339     def generate(self, writer):
340         value = self.value
341
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)
348
349         if value:
350             writer.write_line('_buffer.append(%r)' % value)
351
352
353 class ParseError(Exception):
354     """Raised for template syntax errors."""
355     pass
356
357
358 class _CodeWriter(object):
359     def __init__(self, file, named_blocks, loader, current_template,
360                  compress_whitespace):
361         self.file = file
362         self.named_blocks = named_blocks
363         self.loader = loader
364         self.current_template = current_template
365         self.compress_whitespace = compress_whitespace
366         self.apply_counter = 0
367         self._indent = 0
368
369     def indent(self):
370         return self
371
372     def indent_size(self):
373         return self._indent
374
375     def __enter__(self):
376         self._indent += 1
377         return self
378
379     def __exit__(self, *args):
380         assert self._indent > 0
381         self._indent -= 1
382
383     def write_line(self, line, indent=None):
384         if indent == None:
385             indent = self._indent
386         for i in xrange(indent):
387             self.file.write("    ")
388         print >> self.file, line
389
390
391 class _TemplateReader(object):
392     def __init__(self, name, text):
393         self.name = name
394         self.text = text
395         self.line = 0
396         self.pos = 0
397
398     def find(self, needle, start=0, end=None):
399         assert start >= 0, start
400         pos = self.pos
401         start += pos
402         if end is None:
403             index = self.text.find(needle, start)
404         else:
405             end += pos
406             assert end >= start
407             index = self.text.find(needle, start, end)
408         if index != -1:
409             index -= pos
410         return index
411
412     def consume(self, count=None):
413         if count is 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]
418         self.pos = newpos
419         return s
420
421     def remaining(self):
422         return len(self.text) - self.pos
423
424     def __len__(self):
425         return self.remaining()
426
427     def __getitem__(self, key):
428         if type(key) is slice:
429             size = len(self)
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)]
435         elif key < 0:
436             return self.text[key]
437         else:
438             return self.text[self.pos + key]
439
440     def __str__(self):
441         return self.text[self.pos:]
442
443
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)])
448
449
450 def _parse(reader, in_block=None):
451     body = _ChunkList([])
452     while True:
453         # Find next template directive
454         curly = 0
455         while True:
456             curly = reader.find("{", curly)
457             if curly == -1 or curly + 1 == reader.remaining():
458                 # EOF
459                 if in_block:
460                     raise ParseError("Missing {%% end %%} block for %s" %
461                                      in_block)
462                 body.chunks.append(_Text(reader.consume()))
463                 return body
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 ("{", "%"):
467                 curly += 1
468                 continue
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] == '{'):
474                 curly += 1
475                 continue
476             break
477
478         # Append any text before the special token
479         if curly > 0:
480             body.chunks.append(_Text(reader.consume(curly)))
481
482         start_brace = reader.consume(2)
483         line = reader.line
484
485         # Expression
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()
491             reader.consume(2)
492             if not contents:
493                 raise ParseError("Empty expression on line %d" % line)
494             body.chunks.append(_Expression(contents))
495             continue
496
497         # Block
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()
503         reader.consume(2)
504         if not contents:
505             raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
506
507         operator, space, suffix = contents.partition(" ")
508         suffix = suffix.strip()
509
510         # Intermediate ("else", "elif", etc) blocks
511         intermediate_blocks = {
512             "else": set(["if", "for", "while"]),
513             "elif": set(["if"]),
514             "except": set(["try"]),
515             "finally": set(["try"]),
516         }
517         allowed_parents = intermediate_blocks.get(operator)
518         if allowed_parents is not None:
519             if not in_block:
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))
525             continue
526
527         # End tag
528         elif operator == "end":
529             if not in_block:
530                 raise ParseError("Extra {%% end %%} block on line %d" % line)
531             return body
532
533         elif operator in ("extends", "include", "set", "import", "comment"):
534             if operator == "comment":
535                 continue
536             if operator == "extends":
537                 suffix = suffix.strip('"').strip("'")
538                 if not suffix:
539                     raise ParseError("extends missing file path on line %d" % line)
540                 block = _ExtendsBlock(suffix)
541             elif operator == "import":
542                 if not suffix:
543                     raise ParseError("import missing statement on line %d" % line)
544                 block = _Statement(contents)
545             elif operator == "include":
546                 suffix = suffix.strip('"').strip("'")
547                 if not suffix:
548                     raise ParseError("include missing file path on line %d" % line)
549                 block = _IncludeBlock(suffix, reader)
550             elif operator == "set":
551                 if not suffix:
552                     raise ParseError("set missing statement on line %d" % line)
553                 block = _Statement(suffix)
554             body.chunks.append(block)
555             continue
556
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":
561                 if not suffix:
562                     raise ParseError("apply missing method name on line %d" % line)
563                 block = _ApplyBlock(suffix, block_body)
564             elif operator == "block":
565                 if not suffix:
566                     raise ParseError("block missing name on line %d" % line)
567                 block = _NamedBlock(suffix, block_body)
568             else:
569                 block = _ControlBlock(contents, block_body)
570             body.chunks.append(block)
571             continue
572
573         else:
574             raise ParseError("unknown operator: %r" % operator)