1 # -*- coding: utf-8 -*-
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
10 :copyright: Copyright 2008 by Armin Ronacher.
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
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')
27 class ExtensionRegistry(type):
28 """Gives the extension a unique identifier."""
30 def __new__(cls, name, bases, d):
31 rv = type.__new__(cls, name, bases, d)
32 rv.identifier = rv.__module__ + '.' + rv.__name__
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.
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).
55 __metaclass__ = ExtensionRegistry
57 #: if this extension parses this is the list of tags it's listening to.
60 def __init__(self, environment):
61 self.environment = environment
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
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.
76 raise NotImplementedError()
78 def attr(self, name, lineno=None):
79 """Return an attribute node for the current extension. This is useful
80 to pass constants on extensions to generated template code::
82 self.attr('_my_attribute', lineno=lineno)
84 return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
86 def call_method(self, name, args=None, kwargs=None, dyn_args=None,
87 dyn_kwargs=None, lineno=None):
88 """Call a method of the extension. This is a shortcut for
89 :meth:`attr` + :class:`jinja2.nodes.Call`.
95 return nodes.Call(self.attr(name, lineno=lineno), args, kwargs,
96 dyn_args, dyn_kwargs, lineno=lineno)
99 class InternationalizationExtension(Extension):
100 """This extension adds gettext support to Jinja2."""
101 tags = set(['trans'])
103 def __init__(self, environment):
104 Extension.__init__(self, environment)
105 environment.globals['_'] = contextfunction(lambda c, x: c['gettext'](x))
107 install_gettext_translations=self._install,
108 install_null_translations=self._install_null,
109 uninstall_gettext_translations=self._uninstall,
110 extract_translations=self._extract
113 def _install(self, translations):
114 self.environment.globals.update(
115 gettext=translations.ugettext,
116 ngettext=translations.ungettext
119 def _install_null(self):
120 self.environment.globals.update(
122 ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0]
125 def _uninstall(self, translations):
126 for key in 'gettext', 'ngettext':
127 self.environment.globals.pop(key, None)
129 def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS):
130 if isinstance(source, basestring):
131 source = self.environment.parse(source)
132 return extract_from_ast(source, gettext_functions)
134 def parse(self, parser):
135 """Parse a translatable tag."""
136 lineno = parser.stream.next().lineno
138 # find all the variables referenced. Additionally a variable can be
139 # defined in the body of the trans block too, but this is checked at
143 while parser.stream.current.type is not 'block_end':
145 parser.stream.expect('comma')
147 # skip colon for python compatibility
148 if parser.stream.skip_if('colon'):
151 name = parser.stream.expect('name')
152 if name.value in variables:
153 raise TemplateAssertionError('translatable variable %r defined '
154 'twice.' % name.value, name.lineno,
158 if parser.stream.current.type is 'assign':
160 variables[name.value] = var = parser.parse_expression()
162 variables[name.value] = var = nodes.Name(name.value, 'load')
163 if plural_expr is None:
166 parser.stream.expect('block_end')
168 plural = plural_names = None
172 # now parse until endtrans or pluralize
173 singular_names, singular = self._parse_block(parser, True)
175 referenced.update(singular_names)
176 if plural_expr is None:
177 plural_expr = nodes.Name(singular_names[0], 'load')
179 # if we have a pluralize block, we parse that too
180 if parser.stream.current.test('name:pluralize'):
183 if parser.stream.current.type is not 'block_end':
184 plural_expr = parser.parse_expression()
185 parser.stream.expect('block_end')
186 plural_names, plural = self._parse_block(parser, False)
188 referenced.update(plural_names)
192 # register free names as simple name expressions
193 for var in referenced:
194 if var not in variables:
195 variables[var] = nodes.Name(var, 'load')
197 # no variables referenced? no need to escape
199 singular = singular.replace('%%', '%')
201 plural = plural.replace('%%', '%')
205 elif plural_expr is None:
206 raise TemplateAssertionError('pluralize without variables',
207 lineno, parser.filename)
210 variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
211 for x, y in variables.items()])
215 node = self._make_node(singular, plural, variables, plural_expr)
216 node.set_lineno(lineno)
219 def _parse_block(self, parser, allow_pluralize):
220 """Parse until the next block tag with a given name."""
224 if parser.stream.current.type is 'data':
225 buf.append(parser.stream.current.value.replace('%', '%%'))
227 elif parser.stream.current.type is 'variable_begin':
229 name = parser.stream.expect('name').value
230 referenced.append(name)
231 buf.append('%%(%s)s' % name)
232 parser.stream.expect('variable_end')
233 elif parser.stream.current.type is 'block_begin':
235 if parser.stream.current.test('name:endtrans'):
237 elif parser.stream.current.test('name:pluralize'):
240 raise TemplateSyntaxError('a translatable section can '
241 'have only one pluralize '
243 parser.stream.current.lineno,
245 raise TemplateSyntaxError('control structures in translatable'
246 ' sections are not allowed.',
247 parser.stream.current.lineno,
250 assert False, 'internal parser error'
252 return referenced, concat(buf)
254 def _make_node(self, singular, plural, variables, plural_expr):
255 """Generates a useful node from the data provided."""
257 if plural_expr is None:
258 gettext = nodes.Name('gettext', 'load')
259 node = nodes.Call(gettext, [nodes.Const(singular)],
262 # singular and plural
264 ngettext = nodes.Name('ngettext', 'load')
265 node = nodes.Call(ngettext, [
266 nodes.Const(singular),
271 # mark the return value as safe if we are in an
272 # environment with autoescaping turned on
273 if self.environment.autoescape:
274 node = nodes.MarkSafe(node)
277 node = nodes.Mod(node, variables)
278 return nodes.Output([node])
281 def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS):
282 """Extract localizable strings from the given template node.
284 For every string found this function yields a ``(lineno, function,
285 message)`` tuple, where:
287 * ``lineno`` is the number of the line on which the string was found,
288 * ``function`` is the name of the ``gettext`` function used (if the
289 string was extracted from embedded Python code), and
290 * ``message`` is the string itself (a ``unicode`` object, or a tuple
291 of ``unicode`` objects for functions with multiple string arguments).
293 for node in node.find_all(nodes.Call):
294 if not isinstance(node.node, nodes.Name) or \
295 node.node.name not in gettext_functions:
299 for arg in node.args:
300 if isinstance(arg, nodes.Const) and \
301 isinstance(arg.value, basestring):
302 strings.append(arg.value)
306 if len(strings) == 1:
309 strings = tuple(strings)
310 yield node.lineno, node.node.name, strings
313 def babel_extract(fileobj, keywords, comment_tags, options):
314 """Babel extraction method for Jinja templates.
316 :param fileobj: the file-like object the messages should be extracted from
317 :param keywords: a list of keywords (i.e. function names) that should be
318 recognized as translation functions
319 :param comment_tags: a list of translator tags to search for and include
320 in the results. (Unused)
321 :param options: a dictionary of additional options (optional)
322 :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
323 (comments will be empty currently)
325 encoding = options.get('encoding', 'utf-8')
327 have_trans_extension = False
329 for extension in options.get('extensions', '').split(','):
330 extension = extension.strip()
333 extension = import_string(extension)
334 if extension is InternationalizationExtension:
335 have_trans_extension = True
336 extensions.append(extension)
337 if not have_trans_extension:
338 extensions.append(InternationalizationExtension)
340 environment = get_spontaneous_environment(
341 options.get('block_start_string', '{%'),
342 options.get('block_end_string', '%}'),
343 options.get('variable_start_string', '{{'),
344 options.get('variable_end_string', '}}'),
345 options.get('comment_start_string', '{#'),
346 options.get('comment_end_string', '#}'),
347 options.get('line_statement_prefix') or None,
348 options.get('trim_blocks', '').lower() in ('1', 'on', 'yes', 'true'),
350 # fill with defaults so that environments are shared
351 # with other spontaneus environments. The rest of the
352 # arguments are optimizer, undefined, finalize, autoescape,
353 # loader, cache size and auto reloading setting
354 True, Undefined, None, False, None, 0, False
357 node = environment.parse(fileobj.read().decode(encoding))
358 for lineno, func, message in extract_from_ast(node, keywords):
359 yield lineno, func, message, []
362 #: nicer import names
363 i18n = InternationalizationExtension