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.
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::
81 nodes.Call(self.attr('_my_callback'), args, kwargs, None, None)
83 That would call `self._my_callback` when the template is evaluated.
85 return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
88 class InternationalizationExtension(Extension):
89 """This extension adds gettext support to Jinja2."""
92 def __init__(self, environment):
93 Extension.__init__(self, environment)
94 environment.globals['_'] = contextfunction(lambda c, x: c['gettext'](x))
96 install_gettext_translations=self._install,
97 install_null_translations=self._install_null,
98 uninstall_gettext_translations=self._uninstall,
99 extract_translations=self._extract
102 def _install(self, translations):
103 self.environment.globals.update(
104 gettext=translations.ugettext,
105 ngettext=translations.ungettext
108 def _install_null(self):
109 self.environment.globals.update(
111 ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0]
114 def _uninstall(self, translations):
115 for key in 'gettext', 'ngettext':
116 self.environment.globals.pop(key, None)
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)
123 def parse(self, parser):
124 """Parse a translatable tag."""
125 lineno = parser.stream.next().lineno
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
132 while parser.stream.current.type is not 'block_end':
134 parser.stream.expect('comma')
136 # skip colon for python compatibility
137 if parser.skip_colon():
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,
147 if parser.stream.current.type is 'assign':
149 variables[name.value] = var = parser.parse_expression()
151 variables[name.value] = var = nodes.Name(name.value, 'load')
152 if plural_expr is None:
155 parser.stream.expect('block_end')
157 plural = plural_names = None
161 # now parse until endtrans or pluralize
162 singular_names, singular = self._parse_block(parser, True)
164 referenced.update(singular_names)
165 if plural_expr is None:
166 plural_expr = nodes.Name(singular_names[0], 'load')
168 # if we have a pluralize block, we parse that too
169 if parser.stream.current.test('name:pluralize'):
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)
177 referenced.update(plural_names)
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')
186 # no variables referenced? no need to escape
188 singular = singular.replace('%%', '%')
190 plural = plural.replace('%%', '%')
194 elif plural_expr is None:
195 raise TemplateAssertionError('pluralize without variables',
196 lineno, parser.filename)
199 variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
200 for x, y in variables.items()])
204 node = self._make_node(singular, plural, variables, plural_expr)
205 node.set_lineno(lineno)
208 def _parse_block(self, parser, allow_pluralize):
209 """Parse until the next block tag with a given name."""
213 if parser.stream.current.type is 'data':
214 buf.append(parser.stream.current.value.replace('%', '%%'))
216 elif parser.stream.current.type is 'variable_begin':
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':
224 if parser.stream.current.test('name:endtrans'):
226 elif parser.stream.current.test('name:pluralize'):
229 raise TemplateSyntaxError('a translatable section can '
230 'have only one pluralize '
232 parser.stream.current.lineno,
234 raise TemplateSyntaxError('control structures in translatable'
235 ' sections are not allowed.',
236 parser.stream.current.lineno,
239 assert False, 'internal parser error'
241 return referenced, concat(buf)
243 def _make_node(self, singular, plural, variables, plural_expr):
244 """Generates a useful node from the data provided."""
246 if plural_expr is None:
247 gettext = nodes.Name('gettext', 'load')
248 node = nodes.Call(gettext, [nodes.Const(singular)],
251 # singular and plural
253 ngettext = nodes.Name('ngettext', 'load')
254 node = nodes.Call(ngettext, [
255 nodes.Const(singular),
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)
266 node = nodes.Mod(node, variables)
267 return nodes.Output([node])
270 def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS):
271 """Extract localizable strings from the given template node.
273 For every string found this function yields a ``(lineno, function,
274 message)`` tuple, where:
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).
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:
288 for arg in node.args:
289 if isinstance(arg, nodes.Const) and \
290 isinstance(arg.value, basestring):
291 strings.append(arg.value)
295 if len(strings) == 1:
298 strings = tuple(strings)
299 yield node.lineno, node.node.name, strings
302 def babel_extract(fileobj, keywords, comment_tags, options):
303 """Babel extraction method for Jinja templates.
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)
314 encoding = options.get('encoding', 'utf-8')
316 have_trans_extension = False
318 for extension in options.get('extensions', '').split(','):
319 extension = extension.strip()
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)
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'),
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
346 node = environment.parse(fileobj.read().decode(encoding))
347 for lineno, func, message in extract_from_ast(node, keywords):
348 yield lineno, func, message, []
351 #: nicer import names
352 i18n = InternationalizationExtension