blob: e140fbceccc62022bc54fc3757373b513166dd7a [file] [log] [blame]
Armin Ronacher05530932008-04-20 13:27:49 +02001# -*- coding: utf-8 -*-
2"""
3 jinja2.ext
4 ~~~~~~~~~~
5
Armin Ronacherb5124e62008-04-25 00:36:14 +02006 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.
Armin Ronacher05530932008-04-20 13:27:49 +02009
10 :copyright: Copyright 2008 by Armin Ronacher.
11 :license: BSD.
12"""
Armin Ronacherb5124e62008-04-25 00:36:14 +020013from collections import deque
Armin Ronacher05530932008-04-20 13:27:49 +020014from jinja2 import nodes
Armin Ronacher4f5008f2008-05-23 23:36:07 +020015from jinja2.defaults import *
Armin Ronacherb5124e62008-04-25 00:36:14 +020016from jinja2.environment import get_spontaneous_environment
Armin Ronacher2feed1d2008-04-26 16:26:52 +020017from jinja2.runtime import Undefined, concat
Benjamin Wieganda3152742008-04-28 18:07:52 +020018from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
Armin Ronachered98cac2008-05-07 08:42:11 +020019from jinja2.utils import contextfunction, import_string, Markup
Armin Ronacherb5124e62008-04-25 00:36:14 +020020
21
22# the only real useful gettext functions for a Jinja template. Note
23# that ugettext must be assigned to gettext as Jinja doesn't support
24# non unicode strings.
25GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext')
Armin Ronacher05530932008-04-20 13:27:49 +020026
27
Armin Ronacher023b5e92008-05-08 11:03:10 +020028class ExtensionRegistry(type):
Armin Ronacher5c047ea2008-05-23 22:26:45 +020029 """Gives the extension an unique identifier."""
Armin Ronacher023b5e92008-05-08 11:03:10 +020030
31 def __new__(cls, name, bases, d):
32 rv = type.__new__(cls, name, bases, d)
33 rv.identifier = rv.__module__ + '.' + rv.__name__
34 return rv
35
36
Armin Ronacher05530932008-04-20 13:27:49 +020037class Extension(object):
Armin Ronacher7259c762008-04-30 13:03:59 +020038 """Extensions can be used to add extra functionality to the Jinja template
Armin Ronacher9d42abf2008-05-14 18:10:41 +020039 system at the parser level. Custom extensions are bound to an environment
40 but may not store environment specific data on `self`. The reason for
41 this is that an extension can be bound to another environment (for
42 overlays) by creating a copy and reassigning the `environment` attribute.
Armin Ronacher762079c2008-05-08 23:57:56 +020043
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).
Armin Ronacher7259c762008-04-30 13:03:59 +020054 """
Armin Ronacher023b5e92008-05-08 11:03:10 +020055 __metaclass__ = ExtensionRegistry
Armin Ronacher05530932008-04-20 13:27:49 +020056
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
Armin Ronacher7259c762008-04-30 13:03:59 +020063 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
Armin Ronacher9ad96e72008-06-13 22:44:01 +020070 def preprocess(self, source, name, filename=None):
71 """This method is called before the actual lexing and can be used to
72 preprocess the source. The `filename` is optional. The return value
73 must be the preprocessed source.
74 """
75 return source
76
77 def filter_stream(self, stream):
78 """It's passed a :class:`~jinja2.lexer.TokenStream` that can be used
79 to filter tokens returned. This method has to return an iterable of
80 :class:`~jinja2.lexer.Token`\s, but it doesn't have to return a
81 :class:`~jinja2.lexer.TokenStream`.
Armin Ronacherd02fc7d2008-06-14 14:19:47 +020082
83 In the `ext` folder of the Jinja2 source distribution there is a file
84 called `inlinegettext.py` which implements a filter that utilizes this
85 method.
Armin Ronacher9ad96e72008-06-13 22:44:01 +020086 """
87 return stream
88
Armin Ronacher05530932008-04-20 13:27:49 +020089 def parse(self, parser):
Armin Ronacher023b5e92008-05-08 11:03:10 +020090 """If any of the :attr:`tags` matched this method is called with the
91 parser as first argument. The token the parser stream is pointing at
92 is the name token that matched. This method has to return one or a
93 list of multiple nodes.
94 """
Armin Ronacher27069d72008-05-11 19:48:12 +020095 raise NotImplementedError()
Armin Ronacher023b5e92008-05-08 11:03:10 +020096
97 def attr(self, name, lineno=None):
98 """Return an attribute node for the current extension. This is useful
Armin Ronacher69e12db2008-05-12 09:00:03 +020099 to pass constants on extensions to generated template code::
Armin Ronacher023b5e92008-05-08 11:03:10 +0200100
Armin Ronacher69e12db2008-05-12 09:00:03 +0200101 self.attr('_my_attribute', lineno=lineno)
Armin Ronacher023b5e92008-05-08 11:03:10 +0200102 """
103 return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
Armin Ronacher05530932008-04-20 13:27:49 +0200104
Armin Ronacher27069d72008-05-11 19:48:12 +0200105 def call_method(self, name, args=None, kwargs=None, dyn_args=None,
106 dyn_kwargs=None, lineno=None):
Armin Ronacher69e12db2008-05-12 09:00:03 +0200107 """Call a method of the extension. This is a shortcut for
108 :meth:`attr` + :class:`jinja2.nodes.Call`.
109 """
Armin Ronacher27069d72008-05-11 19:48:12 +0200110 if args is None:
111 args = []
112 if kwargs is None:
113 kwargs = []
114 return nodes.Call(self.attr(name, lineno=lineno), args, kwargs,
115 dyn_args, dyn_kwargs, lineno=lineno)
116
Armin Ronacher05530932008-04-20 13:27:49 +0200117
Armin Ronacher5c047ea2008-05-23 22:26:45 +0200118@contextfunction
119def _gettext_alias(context, string):
120 return context.resolve('gettext')(string)
121
122
Armin Ronachered98cac2008-05-07 08:42:11 +0200123class InternationalizationExtension(Extension):
Armin Ronacher762079c2008-05-08 23:57:56 +0200124 """This extension adds gettext support to Jinja2."""
Armin Ronacherb5124e62008-04-25 00:36:14 +0200125 tags = set(['trans'])
126
Armin Ronacher4720c362008-09-06 16:15:38 +0200127 # TODO: the i18n extension is currently reevaluating values in a few
128 # situations. Take this example:
129 # {% trans count=something() %}{{ count }} foo{% pluralize
130 # %}{{ count }} fooss{% endtrans %}
131 # something is called twice here. One time for the gettext value and
132 # the other time for the n-parameter of the ngettext function.
133
Armin Ronacherb5124e62008-04-25 00:36:14 +0200134 def __init__(self, environment):
135 Extension.__init__(self, environment)
Armin Ronacher5c047ea2008-05-23 22:26:45 +0200136 environment.globals['_'] = _gettext_alias
Armin Ronacher762079c2008-05-08 23:57:56 +0200137 environment.extend(
138 install_gettext_translations=self._install,
139 install_null_translations=self._install_null,
140 uninstall_gettext_translations=self._uninstall,
141 extract_translations=self._extract
142 )
143
144 def _install(self, translations):
145 self.environment.globals.update(
146 gettext=translations.ugettext,
147 ngettext=translations.ungettext
148 )
149
150 def _install_null(self):
151 self.environment.globals.update(
152 gettext=lambda x: x,
153 ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0]
154 )
155
156 def _uninstall(self, translations):
157 for key in 'gettext', 'ngettext':
158 self.environment.globals.pop(key, None)
159
160 def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS):
161 if isinstance(source, basestring):
162 source = self.environment.parse(source)
163 return extract_from_ast(source, gettext_functions)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200164
165 def parse(self, parser):
166 """Parse a translatable tag."""
167 lineno = parser.stream.next().lineno
168
Armin Ronacherb5124e62008-04-25 00:36:14 +0200169 # find all the variables referenced. Additionally a variable can be
170 # defined in the body of the trans block too, but this is checked at
171 # a later state.
172 plural_expr = None
173 variables = {}
174 while parser.stream.current.type is not 'block_end':
175 if variables:
176 parser.stream.expect('comma')
Armin Ronacher023b5e92008-05-08 11:03:10 +0200177
178 # skip colon for python compatibility
Armin Ronacherfdf95302008-05-11 22:20:51 +0200179 if parser.stream.skip_if('colon'):
Armin Ronacher023b5e92008-05-08 11:03:10 +0200180 break
181
Armin Ronacherb5124e62008-04-25 00:36:14 +0200182 name = parser.stream.expect('name')
183 if name.value in variables:
Armin Ronacher7f15ef82008-05-16 09:11:39 +0200184 parser.fail('translatable variable %r defined twice.' %
185 name.value, name.lineno,
186 exc=TemplateAssertionError)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200187
188 # expressions
189 if parser.stream.current.type is 'assign':
190 parser.stream.next()
191 variables[name.value] = var = parser.parse_expression()
192 else:
193 variables[name.value] = var = nodes.Name(name.value, 'load')
194 if plural_expr is None:
195 plural_expr = var
Armin Ronacher023b5e92008-05-08 11:03:10 +0200196
Armin Ronacherb5124e62008-04-25 00:36:14 +0200197 parser.stream.expect('block_end')
198
199 plural = plural_names = None
200 have_plural = False
201 referenced = set()
202
203 # now parse until endtrans or pluralize
204 singular_names, singular = self._parse_block(parser, True)
205 if singular_names:
206 referenced.update(singular_names)
207 if plural_expr is None:
208 plural_expr = nodes.Name(singular_names[0], 'load')
209
210 # if we have a pluralize block, we parse that too
211 if parser.stream.current.test('name:pluralize'):
212 have_plural = True
213 parser.stream.next()
214 if parser.stream.current.type is not 'block_end':
Armin Ronacher4720c362008-09-06 16:15:38 +0200215 name = parser.stream.expect('name')
216 if name.value not in variables:
217 parser.fail('unknown variable %r for pluralization' %
218 name.value, name.lineno,
219 exc=TemplateAssertionError)
220 plural_expr = variables[name.value]
Armin Ronacherb5124e62008-04-25 00:36:14 +0200221 parser.stream.expect('block_end')
222 plural_names, plural = self._parse_block(parser, False)
223 parser.stream.next()
224 referenced.update(plural_names)
225 else:
226 parser.stream.next()
227
228 # register free names as simple name expressions
229 for var in referenced:
230 if var not in variables:
231 variables[var] = nodes.Name(var, 'load')
232
233 # no variables referenced? no need to escape
234 if not referenced:
235 singular = singular.replace('%%', '%')
236 if plural:
237 plural = plural.replace('%%', '%')
238
239 if not have_plural:
240 plural_expr = None
241 elif plural_expr is None:
Armin Ronacher7f15ef82008-05-16 09:11:39 +0200242 parser.fail('pluralize without variables', lineno)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200243
244 if variables:
245 variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
246 for x, y in variables.items()])
247 else:
248 variables = None
249
250 node = self._make_node(singular, plural, variables, plural_expr)
251 node.set_lineno(lineno)
252 return node
253
254 def _parse_block(self, parser, allow_pluralize):
255 """Parse until the next block tag with a given name."""
256 referenced = []
257 buf = []
258 while 1:
259 if parser.stream.current.type is 'data':
260 buf.append(parser.stream.current.value.replace('%', '%%'))
261 parser.stream.next()
262 elif parser.stream.current.type is 'variable_begin':
263 parser.stream.next()
264 name = parser.stream.expect('name').value
265 referenced.append(name)
266 buf.append('%%(%s)s' % name)
267 parser.stream.expect('variable_end')
268 elif parser.stream.current.type is 'block_begin':
269 parser.stream.next()
270 if parser.stream.current.test('name:endtrans'):
271 break
272 elif parser.stream.current.test('name:pluralize'):
273 if allow_pluralize:
274 break
Armin Ronacher7f15ef82008-05-16 09:11:39 +0200275 parser.fail('a translatable section can have only one '
276 'pluralize section')
277 parser.fail('control structures in translatable sections are '
278 'not allowed')
Armin Ronacherd02fc7d2008-06-14 14:19:47 +0200279 elif parser.stream.eos:
280 parser.fail('unclosed translation block')
Armin Ronacherb5124e62008-04-25 00:36:14 +0200281 else:
282 assert False, 'internal parser error'
283
Armin Ronacher2feed1d2008-04-26 16:26:52 +0200284 return referenced, concat(buf)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200285
286 def _make_node(self, singular, plural, variables, plural_expr):
287 """Generates a useful node from the data provided."""
288 # singular only:
289 if plural_expr is None:
290 gettext = nodes.Name('gettext', 'load')
291 node = nodes.Call(gettext, [nodes.Const(singular)],
292 [], None, None)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200293
294 # singular and plural
295 else:
296 ngettext = nodes.Name('ngettext', 'load')
297 node = nodes.Call(ngettext, [
298 nodes.Const(singular),
299 nodes.Const(plural),
300 plural_expr
301 ], [], None, None)
Armin Ronacherd84ec462008-04-29 13:43:16 +0200302
303 # mark the return value as safe if we are in an
304 # environment with autoescaping turned on
305 if self.environment.autoescape:
306 node = nodes.MarkSafe(node)
307
308 if variables:
309 node = nodes.Mod(node, variables)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200310 return nodes.Output([node])
311
312
Armin Ronacher5d2733f2008-05-15 23:26:52 +0200313class ExprStmtExtension(Extension):
314 """Adds a `do` tag to Jinja2 that works like the print statement just
315 that it doesn't print the return value.
316 """
317 tags = set(['do'])
318
319 def parse(self, parser):
320 node = nodes.ExprStmt(lineno=parser.stream.next().lineno)
321 node.node = parser.parse_tuple()
322 return node
323
324
Armin Ronacher3da90312008-05-23 16:37:28 +0200325class LoopControlExtension(Extension):
326 """Adds break and continue to the template engine."""
327 tags = set(['break', 'continue'])
328
329 def parse(self, parser):
330 token = parser.stream.next()
331 if token.value == 'break':
332 return nodes.Break(lineno=token.lineno)
333 return nodes.Continue(lineno=token.lineno)
334
335
Armin Ronacherabd36572008-06-27 08:45:19 +0200336def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS,
337 babel_style=True):
338 """Extract localizable strings from the given template node. Per
339 default this function returns matches in babel style that means non string
340 parameters as well as keyword arguments are returned as `None`. This
341 allows Babel to figure out what you really meant if you are using
342 gettext functions that allow keyword arguments for placeholder expansion.
343 If you don't want that behavior set the `babel_style` parameter to `False`
344 which causes only strings to be returned and parameters are always stored
345 in tuples. As a consequence invalid gettext calls (calls without a single
346 string parameter or string parameters after non-string parameters) are
347 skipped.
348
349 This example explains the behavior:
350
351 >>> from jinja2 import Environment
352 >>> env = Environment()
353 >>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}')
354 >>> list(extract_from_ast(node))
355 [(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))]
356 >>> list(extract_from_ast(node, babel_style=False))
357 [(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))]
Armin Ronacherb5124e62008-04-25 00:36:14 +0200358
359 For every string found this function yields a ``(lineno, function,
360 message)`` tuple, where:
361
362 * ``lineno`` is the number of the line on which the string was found,
363 * ``function`` is the name of the ``gettext`` function used (if the
364 string was extracted from embedded Python code), and
365 * ``message`` is the string itself (a ``unicode`` object, or a tuple
366 of ``unicode`` objects for functions with multiple string arguments).
367 """
368 for node in node.find_all(nodes.Call):
369 if not isinstance(node.node, nodes.Name) or \
370 node.node.name not in gettext_functions:
371 continue
372
373 strings = []
374 for arg in node.args:
375 if isinstance(arg, nodes.Const) and \
376 isinstance(arg.value, basestring):
377 strings.append(arg.value)
378 else:
379 strings.append(None)
380
Armin Ronacherabd36572008-06-27 08:45:19 +0200381 for arg in node.kwargs:
382 strings.append(None)
383 if node.dyn_args is not None:
384 strings.append(None)
385 if node.dyn_kwargs is not None:
386 strings.append(None)
387
388 if not babel_style:
389 strings = tuple(x for x in strings if x is not None)
390 if not strings:
391 continue
Armin Ronacherb5124e62008-04-25 00:36:14 +0200392 else:
Armin Ronacherabd36572008-06-27 08:45:19 +0200393 if len(strings) == 1:
394 strings = strings[0]
395 else:
396 strings = tuple(strings)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200397 yield node.lineno, node.node.name, strings
398
399
400def babel_extract(fileobj, keywords, comment_tags, options):
401 """Babel extraction method for Jinja templates.
402
403 :param fileobj: the file-like object the messages should be extracted from
404 :param keywords: a list of keywords (i.e. function names) that should be
405 recognized as translation functions
406 :param comment_tags: a list of translator tags to search for and include
407 in the results. (Unused)
408 :param options: a dictionary of additional options (optional)
409 :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
410 (comments will be empty currently)
411 """
Armin Ronacher4f5008f2008-05-23 23:36:07 +0200412 extensions = set()
Armin Ronacherb5124e62008-04-25 00:36:14 +0200413 for extension in options.get('extensions', '').split(','):
414 extension = extension.strip()
415 if not extension:
416 continue
Armin Ronacher4f5008f2008-05-23 23:36:07 +0200417 extensions.add(import_string(extension))
418 if InternationalizationExtension not in extensions:
419 extensions.add(InternationalizationExtension)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200420
421 environment = get_spontaneous_environment(
Armin Ronacher4f5008f2008-05-23 23:36:07 +0200422 options.get('block_start_string', BLOCK_START_STRING),
423 options.get('block_end_string', BLOCK_END_STRING),
424 options.get('variable_start_string', VARIABLE_START_STRING),
425 options.get('variable_end_string', VARIABLE_END_STRING),
426 options.get('comment_start_string', COMMENT_START_STRING),
427 options.get('comment_end_string', COMMENT_END_STRING),
428 options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX,
429 str(options.get('trim_blocks', TRIM_BLOCKS)).lower() in \
430 ('1', 'on', 'yes', 'true'),
431 NEWLINE_SEQUENCE, frozenset(extensions),
Armin Ronacherb5124e62008-04-25 00:36:14 +0200432 # fill with defaults so that environments are shared
Armin Ronacher7259c762008-04-30 13:03:59 +0200433 # with other spontaneus environments. The rest of the
434 # arguments are optimizer, undefined, finalize, autoescape,
435 # loader, cache size and auto reloading setting
436 True, Undefined, None, False, None, 0, False
Armin Ronacherb5124e62008-04-25 00:36:14 +0200437 )
438
Armin Ronacher4f5008f2008-05-23 23:36:07 +0200439 source = fileobj.read().decode(options.get('encoding', 'utf-8'))
Armin Ronacherc670b112008-06-29 17:23:04 +0200440 try:
441 node = environment.parse(source)
442 except TemplateSyntaxError, e:
443 # skip templates with syntax errors
444 return
Armin Ronacherb5124e62008-04-25 00:36:14 +0200445 for lineno, func, message in extract_from_ast(node, keywords):
446 yield lineno, func, message, []
Armin Ronachered98cac2008-05-07 08:42:11 +0200447
448
449#: nicer import names
450i18n = InternationalizationExtension
Armin Ronacher5d2733f2008-05-15 23:26:52 +0200451do = ExprStmtExtension
Armin Ronacher3da90312008-05-23 16:37:28 +0200452loopcontrols = LoopControlExtension