blob: f60b85a17d28f69e54276ee1ea0d0cee6c20e8df [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 Ronacherb5124e62008-04-25 00:36:14 +020015from jinja2.environment import get_spontaneous_environment
Armin Ronacher2feed1d2008-04-26 16:26:52 +020016from jinja2.runtime import Undefined, concat
Benjamin Wieganda3152742008-04-28 18:07:52 +020017from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
Armin Ronachered98cac2008-05-07 08:42:11 +020018from jinja2.utils import contextfunction, import_string, Markup
Armin Ronacherb5124e62008-04-25 00:36:14 +020019
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.
24GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext')
Armin Ronacher05530932008-04-20 13:27:49 +020025
26
Armin Ronacher023b5e92008-05-08 11:03:10 +020027class 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
Armin Ronacher05530932008-04-20 13:27:49 +020036class Extension(object):
Armin Ronacher7259c762008-04-30 13:03:59 +020037 """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.
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 Ronacher05530932008-04-20 13:27:49 +020070 def parse(self, parser):
Armin Ronacher023b5e92008-05-08 11:03:10 +020071 """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)
Armin Ronacher05530932008-04-20 13:27:49 +020086
87
Armin Ronachered98cac2008-05-07 08:42:11 +020088class InternationalizationExtension(Extension):
Armin Ronacher762079c2008-05-08 23:57:56 +020089 """This extension adds gettext support to Jinja2."""
Armin Ronacherb5124e62008-04-25 00:36:14 +020090 tags = set(['trans'])
91
92 def __init__(self, environment):
93 Extension.__init__(self, environment)
Armin Ronacher762079c2008-05-08 23:57:56 +020094 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)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200122
123 def parse(self, parser):
124 """Parse a translatable tag."""
125 lineno = parser.stream.next().lineno
126
Armin Ronacherb5124e62008-04-25 00:36:14 +0200127 # 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')
Armin Ronacher023b5e92008-05-08 11:03:10 +0200135
136 # skip colon for python compatibility
Armin Ronacher762079c2008-05-08 23:57:56 +0200137 if parser.skip_colon():
Armin Ronacher023b5e92008-05-08 11:03:10 +0200138 break
139
Armin Ronacherb5124e62008-04-25 00:36:14 +0200140 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
Armin Ronacher023b5e92008-05-08 11:03:10 +0200154
Armin Ronacherb5124e62008-04-25 00:36:14 +0200155 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
Armin Ronacher2feed1d2008-04-26 16:26:52 +0200241 return referenced, concat(buf)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200242
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)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200250
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)
Armin Ronacherd84ec462008-04-29 13:43:16 +0200259
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)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200267 return nodes.Output([node])
268
269
270def 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
302def 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)
Armin Ronachered98cac2008-05-07 08:42:11 +0200323 if extension is InternationalizationExtension:
Armin Ronacherb5124e62008-04-25 00:36:14 +0200324 have_trans_extension = True
325 extensions.append(extension)
326 if not have_trans_extension:
Armin Ronachered98cac2008-05-07 08:42:11 +0200327 extensions.append(InternationalizationExtension)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200328
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
Armin Ronacher7259c762008-04-30 13:03:59 +0200340 # 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
Armin Ronacherb5124e62008-04-25 00:36:14 +0200344 )
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, []
Armin Ronachered98cac2008-05-07 08:42:11 +0200349
350
351#: nicer import names
352i18n = InternationalizationExtension