more updates on the extension API
[jinja2.git] / jinja2 / ext.py
1 # -*- coding: utf-8 -*-
2 """
3     jinja2.ext
4     ~~~~~~~~~~
5
6     Jinja extensions allow to add custom tags similar to the way django custom
7     tags work.  By default two example extensions exist: an i18n and a cache
8     extension.
9
10     :copyright: Copyright 2008 by Armin Ronacher.
11     :license: BSD.
12 """
13 from collections import deque
14 from jinja2 import nodes
15 from jinja2.environment import get_spontaneous_environment
16 from jinja2.runtime import Undefined, concat
17 from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
18 from jinja2.utils import contextfunction, import_string, Markup
19
20
21 # the only real useful gettext functions for a Jinja template.  Note
22 # that ugettext must be assigned to gettext as Jinja doesn't support
23 # non unicode strings.
24 GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext')
25
26
27 class ExtensionRegistry(type):
28     """Gives the extension a unique identifier."""
29
30     def __new__(cls, name, bases, d):
31         rv = type.__new__(cls, name, bases, d)
32         rv.identifier = rv.__module__ + '.' + rv.__name__
33         return rv
34
35
36 class Extension(object):
37     """Extensions can be used to add extra functionality to the Jinja template
38     system at the parser level.  This is a supported but currently
39     undocumented interface.  Custom extensions are bound to an environment but
40     may not store environment specific data on `self`.  The reason for this is
41     that an extension can be bound to another environment (for overlays) by
42     creating a copy and reassigning the `environment` attribute.
43
44     As extensions are created by the environment they cannot accept any
45     arguments for configuration.  One may want to work around that by using
46     a factory function, but that is not possible as extensions are identified
47     by their import name.  The correct way to configure the extension is
48     storing the configuration values on the environment.  Because this way the
49     environment ends up acting as central configuration storage the
50     attributes may clash which is why extensions have to ensure that the names
51     they choose for configuration are not too generic.  ``prefix`` for example
52     is a terrible name, ``fragment_cache_prefix`` on the other hand is a good
53     name as includes the name of the extension (fragment cache).
54     """
55     __metaclass__ = ExtensionRegistry
56
57     #: if this extension parses this is the list of tags it's listening to.
58     tags = set()
59
60     def __init__(self, environment):
61         self.environment = environment
62
63     def bind(self, environment):
64         """Create a copy of this extension bound to another environment."""
65         rv = object.__new__(self.__class__)
66         rv.__dict__.update(self.__dict__)
67         rv.environment = environment
68         return rv
69
70     def parse(self, parser):
71         """If any of the :attr:`tags` matched this method is called with the
72         parser as first argument.  The token the parser stream is pointing at
73         is the name token that matched.  This method has to return one or a
74         list of multiple nodes.
75         """
76
77     def attr(self, name, lineno=None):
78         """Return an attribute node for the current extension.  This is useful
79         to pass callbacks to template code::
80
81             nodes.Call(self.attr('_my_callback'), args, kwargs, None, None)
82
83         That would call `self._my_callback` when the template is evaluated.
84         """
85         return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
86
87
88 class InternationalizationExtension(Extension):
89     """This extension adds gettext support to Jinja2."""
90     tags = set(['trans'])
91
92     def __init__(self, environment):
93         Extension.__init__(self, environment)
94         environment.globals['_'] = contextfunction(lambda c, x: c['gettext'](x))
95         environment.extend(
96             install_gettext_translations=self._install,
97             install_null_translations=self._install_null,
98             uninstall_gettext_translations=self._uninstall,
99             extract_translations=self._extract
100         )
101
102     def _install(self, translations):
103         self.environment.globals.update(
104             gettext=translations.ugettext,
105             ngettext=translations.ungettext
106         )
107
108     def _install_null(self):
109         self.environment.globals.update(
110             gettext=lambda x: x,
111             ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0]
112         )
113
114     def _uninstall(self, translations):
115         for key in 'gettext', 'ngettext':
116             self.environment.globals.pop(key, None)
117
118     def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS):
119         if isinstance(source, basestring):
120             source = self.environment.parse(source)
121         return extract_from_ast(source, gettext_functions)
122
123     def parse(self, parser):
124         """Parse a translatable tag."""
125         lineno = parser.stream.next().lineno
126
127         # find all the variables referenced.  Additionally a variable can be
128         # defined in the body of the trans block too, but this is checked at
129         # a later state.
130         plural_expr = None
131         variables = {}
132         while parser.stream.current.type is not 'block_end':
133             if variables:
134                 parser.stream.expect('comma')
135
136             # skip colon for python compatibility
137             if parser.skip_colon():
138                 break
139
140             name = parser.stream.expect('name')
141             if name.value in variables:
142                 raise TemplateAssertionError('translatable variable %r defined '
143                                              'twice.' % name.value, name.lineno,
144                                              parser.filename)
145
146             # expressions
147             if parser.stream.current.type is 'assign':
148                 parser.stream.next()
149                 variables[name.value] = var = parser.parse_expression()
150             else:
151                 variables[name.value] = var = nodes.Name(name.value, 'load')
152             if plural_expr is None:
153                 plural_expr = var
154
155         parser.stream.expect('block_end')
156
157         plural = plural_names = None
158         have_plural = False
159         referenced = set()
160
161         # now parse until endtrans or pluralize
162         singular_names, singular = self._parse_block(parser, True)
163         if singular_names:
164             referenced.update(singular_names)
165             if plural_expr is None:
166                 plural_expr = nodes.Name(singular_names[0], 'load')
167
168         # if we have a pluralize block, we parse that too
169         if parser.stream.current.test('name:pluralize'):
170             have_plural = True
171             parser.stream.next()
172             if parser.stream.current.type is not 'block_end':
173                 plural_expr = parser.parse_expression()
174             parser.stream.expect('block_end')
175             plural_names, plural = self._parse_block(parser, False)
176             parser.stream.next()
177             referenced.update(plural_names)
178         else:
179             parser.stream.next()
180
181         # register free names as simple name expressions
182         for var in referenced:
183             if var not in variables:
184                 variables[var] = nodes.Name(var, 'load')
185
186         # no variables referenced?  no need to escape
187         if not referenced:
188             singular = singular.replace('%%', '%')
189             if plural:
190                 plural = plural.replace('%%', '%')
191
192         if not have_plural:
193             plural_expr = None
194         elif plural_expr is None:
195             raise TemplateAssertionError('pluralize without variables',
196                                          lineno, parser.filename)
197
198         if variables:
199             variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
200                                     for x, y in variables.items()])
201         else:
202             variables = None
203
204         node = self._make_node(singular, plural, variables, plural_expr)
205         node.set_lineno(lineno)
206         return node
207
208     def _parse_block(self, parser, allow_pluralize):
209         """Parse until the next block tag with a given name."""
210         referenced = []
211         buf = []
212         while 1:
213             if parser.stream.current.type is 'data':
214                 buf.append(parser.stream.current.value.replace('%', '%%'))
215                 parser.stream.next()
216             elif parser.stream.current.type is 'variable_begin':
217                 parser.stream.next()
218                 name = parser.stream.expect('name').value
219                 referenced.append(name)
220                 buf.append('%%(%s)s' % name)
221                 parser.stream.expect('variable_end')
222             elif parser.stream.current.type is 'block_begin':
223                 parser.stream.next()
224                 if parser.stream.current.test('name:endtrans'):
225                     break
226                 elif parser.stream.current.test('name:pluralize'):
227                     if allow_pluralize:
228                         break
229                     raise TemplateSyntaxError('a translatable section can '
230                                               'have only one pluralize '
231                                               'section',
232                                               parser.stream.current.lineno,
233                                               parser.filename)
234                 raise TemplateSyntaxError('control structures in translatable'
235                                           ' sections are not allowed.',
236                                           parser.stream.current.lineno,
237                                           parser.filename)
238             else:
239                 assert False, 'internal parser error'
240
241         return referenced, concat(buf)
242
243     def _make_node(self, singular, plural, variables, plural_expr):
244         """Generates a useful node from the data provided."""
245         # singular only:
246         if plural_expr is None:
247             gettext = nodes.Name('gettext', 'load')
248             node = nodes.Call(gettext, [nodes.Const(singular)],
249                               [], None, None)
250
251         # singular and plural
252         else:
253             ngettext = nodes.Name('ngettext', 'load')
254             node = nodes.Call(ngettext, [
255                 nodes.Const(singular),
256                 nodes.Const(plural),
257                 plural_expr
258             ], [], None, None)
259
260         # mark the return value as safe if we are in an
261         # environment with autoescaping turned on
262         if self.environment.autoescape:
263             node = nodes.MarkSafe(node)
264
265         if variables:
266             node = nodes.Mod(node, variables)
267         return nodes.Output([node])
268
269
270 def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS):
271     """Extract localizable strings from the given template node.
272
273     For every string found this function yields a ``(lineno, function,
274     message)`` tuple, where:
275
276     * ``lineno`` is the number of the line on which the string was found,
277     * ``function`` is the name of the ``gettext`` function used (if the
278       string was extracted from embedded Python code), and
279     *  ``message`` is the string itself (a ``unicode`` object, or a tuple
280        of ``unicode`` objects for functions with multiple string arguments).
281     """
282     for node in node.find_all(nodes.Call):
283         if not isinstance(node.node, nodes.Name) or \
284            node.node.name not in gettext_functions:
285             continue
286
287         strings = []
288         for arg in node.args:
289             if isinstance(arg, nodes.Const) and \
290                isinstance(arg.value, basestring):
291                 strings.append(arg.value)
292             else:
293                 strings.append(None)
294
295         if len(strings) == 1:
296             strings = strings[0]
297         else:
298             strings = tuple(strings)
299         yield node.lineno, node.node.name, strings
300
301
302 def babel_extract(fileobj, keywords, comment_tags, options):
303     """Babel extraction method for Jinja templates.
304
305     :param fileobj: the file-like object the messages should be extracted from
306     :param keywords: a list of keywords (i.e. function names) that should be
307                      recognized as translation functions
308     :param comment_tags: a list of translator tags to search for and include
309                          in the results.  (Unused)
310     :param options: a dictionary of additional options (optional)
311     :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
312              (comments will be empty currently)
313     """
314     encoding = options.get('encoding', 'utf-8')
315
316     have_trans_extension = False
317     extensions = []
318     for extension in options.get('extensions', '').split(','):
319         extension = extension.strip()
320         if not extension:
321             continue
322         extension = import_string(extension)
323         if extension is InternationalizationExtension:
324             have_trans_extension = True
325         extensions.append(extension)
326     if not have_trans_extension:
327         extensions.append(InternationalizationExtension)
328
329     environment = get_spontaneous_environment(
330         options.get('block_start_string', '{%'),
331         options.get('block_end_string', '%}'),
332         options.get('variable_start_string', '{{'),
333         options.get('variable_end_string', '}}'),
334         options.get('comment_start_string', '{#'),
335         options.get('comment_end_string', '#}'),
336         options.get('line_statement_prefix') or None,
337         options.get('trim_blocks', '').lower() in ('1', 'on', 'yes', 'true'),
338         tuple(extensions),
339         # fill with defaults so that environments are shared
340         # with other spontaneus environments.  The rest of the
341         # arguments are optimizer, undefined, finalize, autoescape,
342         # loader, cache size and auto reloading setting
343         True, Undefined, None, False, None, 0, False
344     )
345
346     node = environment.parse(fileobj.read().decode(encoding))
347     for lineno, func, message in extract_from_ast(node, keywords):
348         yield lineno, func, message, []
349
350
351 #: nicer import names
352 i18n = InternationalizationExtension