blob: 22adf8289652e61821af543e833d8cb038aa1fbf [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.
43 """
Armin Ronacher023b5e92008-05-08 11:03:10 +020044 __metaclass__ = ExtensionRegistry
Armin Ronacher05530932008-04-20 13:27:49 +020045
46 #: if this extension parses this is the list of tags it's listening to.
47 tags = set()
48
49 def __init__(self, environment):
50 self.environment = environment
51
Armin Ronacher7259c762008-04-30 13:03:59 +020052 def bind(self, environment):
53 """Create a copy of this extension bound to another environment."""
54 rv = object.__new__(self.__class__)
55 rv.__dict__.update(self.__dict__)
56 rv.environment = environment
57 return rv
58
Armin Ronacher05530932008-04-20 13:27:49 +020059 def parse(self, parser):
Armin Ronacher023b5e92008-05-08 11:03:10 +020060 """If any of the :attr:`tags` matched this method is called with the
61 parser as first argument. The token the parser stream is pointing at
62 is the name token that matched. This method has to return one or a
63 list of multiple nodes.
64 """
65
66 def attr(self, name, lineno=None):
67 """Return an attribute node for the current extension. This is useful
68 to pass callbacks to template code::
69
70 nodes.Call(self.attr('_my_callback'), args, kwargs, None, None)
71
72 That would call `self._my_callback` when the template is evaluated.
73 """
74 return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
Armin Ronacher05530932008-04-20 13:27:49 +020075
76
77class CacheExtension(Extension):
Armin Ronacher2b60fe52008-04-21 08:23:59 +020078 """An example extension that adds cacheable blocks."""
Armin Ronacher05530932008-04-20 13:27:49 +020079 tags = set(['cache'])
80
Armin Ronacher203bfcb2008-04-24 21:54:44 +020081 def __init__(self, environment):
82 Extension.__init__(self, environment)
Armin Ronacher023b5e92008-05-08 11:03:10 +020083 environment.globals['__cache_ext_support'] = self.cache_support
84
85 def cache_support(self, name, timeout, caller):
86 """Helper for the cache_fragment function."""
87 if not hasattr(environment, 'cache_support'):
88 return caller()
89 args = [name]
90 if timeout is not None:
91 args.append(timeout)
92 return self.environment.cache_support(generate=caller, *args)
Armin Ronacher203bfcb2008-04-24 21:54:44 +020093
Armin Ronacher05530932008-04-20 13:27:49 +020094 def parse(self, parser):
95 lineno = parser.stream.next().lineno
96 args = [parser.parse_expression()]
Armin Ronacher4f7d2d52008-04-22 10:40:26 +020097 if parser.stream.current.type is 'comma':
98 parser.stream.next()
Armin Ronacher05530932008-04-20 13:27:49 +020099 args.append(parser.parse_expression())
Armin Ronacher023b5e92008-05-08 11:03:10 +0200100 else:
101 args.append(nodes.Const(None, lineno=lineno))
Armin Ronacher05530932008-04-20 13:27:49 +0200102 body = parser.parse_statements(('name:endcache',), drop_needle=True)
103 return nodes.CallBlock(
Armin Ronacher023b5e92008-05-08 11:03:10 +0200104 nodes.Call(nodes.Name('__cache_ext_support', 'load', lineno=lineno),
105 args, [], None, None), [], [], body, lineno=lineno
Armin Ronacher05530932008-04-20 13:27:49 +0200106 )
Armin Ronacherb5124e62008-04-25 00:36:14 +0200107
108
Armin Ronachered98cac2008-05-07 08:42:11 +0200109class InternationalizationExtension(Extension):
Armin Ronacherb5124e62008-04-25 00:36:14 +0200110 """This extension adds gettext support to Jinja."""
111 tags = set(['trans'])
112
113 def __init__(self, environment):
114 Extension.__init__(self, environment)
115 environment.globals.update({
Armin Ronachered98cac2008-05-07 08:42:11 +0200116 '_': contextfunction(lambda c, x: c['gettext'](x)),
Armin Ronacherb5124e62008-04-25 00:36:14 +0200117 'gettext': lambda x: x,
118 'ngettext': lambda s, p, n: (s, p)[n != 1]
119 })
120
121 def parse(self, parser):
122 """Parse a translatable tag."""
123 lineno = parser.stream.next().lineno
124
Armin Ronacherb5124e62008-04-25 00:36:14 +0200125 # find all the variables referenced. Additionally a variable can be
126 # defined in the body of the trans block too, but this is checked at
127 # a later state.
128 plural_expr = None
129 variables = {}
130 while parser.stream.current.type is not 'block_end':
131 if variables:
132 parser.stream.expect('comma')
Armin Ronacher023b5e92008-05-08 11:03:10 +0200133
134 # skip colon for python compatibility
135 if parser.ignore_colon():
136 break
137
Armin Ronacherb5124e62008-04-25 00:36:14 +0200138 name = parser.stream.expect('name')
139 if name.value in variables:
140 raise TemplateAssertionError('translatable variable %r defined '
141 'twice.' % name.value, name.lineno,
142 parser.filename)
143
144 # expressions
145 if parser.stream.current.type is 'assign':
146 parser.stream.next()
147 variables[name.value] = var = parser.parse_expression()
148 else:
149 variables[name.value] = var = nodes.Name(name.value, 'load')
150 if plural_expr is None:
151 plural_expr = var
Armin Ronacher023b5e92008-05-08 11:03:10 +0200152
Armin Ronacherb5124e62008-04-25 00:36:14 +0200153 parser.stream.expect('block_end')
154
155 plural = plural_names = None
156 have_plural = False
157 referenced = set()
158
159 # now parse until endtrans or pluralize
160 singular_names, singular = self._parse_block(parser, True)
161 if singular_names:
162 referenced.update(singular_names)
163 if plural_expr is None:
164 plural_expr = nodes.Name(singular_names[0], 'load')
165
166 # if we have a pluralize block, we parse that too
167 if parser.stream.current.test('name:pluralize'):
168 have_plural = True
169 parser.stream.next()
170 if parser.stream.current.type is not 'block_end':
171 plural_expr = parser.parse_expression()
172 parser.stream.expect('block_end')
173 plural_names, plural = self._parse_block(parser, False)
174 parser.stream.next()
175 referenced.update(plural_names)
176 else:
177 parser.stream.next()
178
179 # register free names as simple name expressions
180 for var in referenced:
181 if var not in variables:
182 variables[var] = nodes.Name(var, 'load')
183
184 # no variables referenced? no need to escape
185 if not referenced:
186 singular = singular.replace('%%', '%')
187 if plural:
188 plural = plural.replace('%%', '%')
189
190 if not have_plural:
191 plural_expr = None
192 elif plural_expr is None:
193 raise TemplateAssertionError('pluralize without variables',
194 lineno, parser.filename)
195
196 if variables:
197 variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
198 for x, y in variables.items()])
199 else:
200 variables = None
201
202 node = self._make_node(singular, plural, variables, plural_expr)
203 node.set_lineno(lineno)
204 return node
205
206 def _parse_block(self, parser, allow_pluralize):
207 """Parse until the next block tag with a given name."""
208 referenced = []
209 buf = []
210 while 1:
211 if parser.stream.current.type is 'data':
212 buf.append(parser.stream.current.value.replace('%', '%%'))
213 parser.stream.next()
214 elif parser.stream.current.type is 'variable_begin':
215 parser.stream.next()
216 name = parser.stream.expect('name').value
217 referenced.append(name)
218 buf.append('%%(%s)s' % name)
219 parser.stream.expect('variable_end')
220 elif parser.stream.current.type is 'block_begin':
221 parser.stream.next()
222 if parser.stream.current.test('name:endtrans'):
223 break
224 elif parser.stream.current.test('name:pluralize'):
225 if allow_pluralize:
226 break
227 raise TemplateSyntaxError('a translatable section can '
228 'have only one pluralize '
229 'section',
230 parser.stream.current.lineno,
231 parser.filename)
232 raise TemplateSyntaxError('control structures in translatable'
233 ' sections are not allowed.',
234 parser.stream.current.lineno,
235 parser.filename)
236 else:
237 assert False, 'internal parser error'
238
Armin Ronacher2feed1d2008-04-26 16:26:52 +0200239 return referenced, concat(buf)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200240
241 def _make_node(self, singular, plural, variables, plural_expr):
242 """Generates a useful node from the data provided."""
243 # singular only:
244 if plural_expr is None:
245 gettext = nodes.Name('gettext', 'load')
246 node = nodes.Call(gettext, [nodes.Const(singular)],
247 [], None, None)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200248
249 # singular and plural
250 else:
251 ngettext = nodes.Name('ngettext', 'load')
252 node = nodes.Call(ngettext, [
253 nodes.Const(singular),
254 nodes.Const(plural),
255 plural_expr
256 ], [], None, None)
Armin Ronacherd84ec462008-04-29 13:43:16 +0200257
258 # mark the return value as safe if we are in an
259 # environment with autoescaping turned on
260 if self.environment.autoescape:
261 node = nodes.MarkSafe(node)
262
263 if variables:
264 node = nodes.Mod(node, variables)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200265 return nodes.Output([node])
266
267
268def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS):
269 """Extract localizable strings from the given template node.
270
271 For every string found this function yields a ``(lineno, function,
272 message)`` tuple, where:
273
274 * ``lineno`` is the number of the line on which the string was found,
275 * ``function`` is the name of the ``gettext`` function used (if the
276 string was extracted from embedded Python code), and
277 * ``message`` is the string itself (a ``unicode`` object, or a tuple
278 of ``unicode`` objects for functions with multiple string arguments).
279 """
280 for node in node.find_all(nodes.Call):
281 if not isinstance(node.node, nodes.Name) or \
282 node.node.name not in gettext_functions:
283 continue
284
285 strings = []
286 for arg in node.args:
287 if isinstance(arg, nodes.Const) and \
288 isinstance(arg.value, basestring):
289 strings.append(arg.value)
290 else:
291 strings.append(None)
292
293 if len(strings) == 1:
294 strings = strings[0]
295 else:
296 strings = tuple(strings)
297 yield node.lineno, node.node.name, strings
298
299
300def babel_extract(fileobj, keywords, comment_tags, options):
301 """Babel extraction method for Jinja templates.
302
303 :param fileobj: the file-like object the messages should be extracted from
304 :param keywords: a list of keywords (i.e. function names) that should be
305 recognized as translation functions
306 :param comment_tags: a list of translator tags to search for and include
307 in the results. (Unused)
308 :param options: a dictionary of additional options (optional)
309 :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
310 (comments will be empty currently)
311 """
312 encoding = options.get('encoding', 'utf-8')
313
314 have_trans_extension = False
315 extensions = []
316 for extension in options.get('extensions', '').split(','):
317 extension = extension.strip()
318 if not extension:
319 continue
320 extension = import_string(extension)
Armin Ronachered98cac2008-05-07 08:42:11 +0200321 if extension is InternationalizationExtension:
Armin Ronacherb5124e62008-04-25 00:36:14 +0200322 have_trans_extension = True
323 extensions.append(extension)
324 if not have_trans_extension:
Armin Ronachered98cac2008-05-07 08:42:11 +0200325 extensions.append(InternationalizationExtension)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200326
327 environment = get_spontaneous_environment(
328 options.get('block_start_string', '{%'),
329 options.get('block_end_string', '%}'),
330 options.get('variable_start_string', '{{'),
331 options.get('variable_end_string', '}}'),
332 options.get('comment_start_string', '{#'),
333 options.get('comment_end_string', '#}'),
334 options.get('line_statement_prefix') or None,
335 options.get('trim_blocks', '').lower() in ('1', 'on', 'yes', 'true'),
336 tuple(extensions),
337 # fill with defaults so that environments are shared
Armin Ronacher7259c762008-04-30 13:03:59 +0200338 # with other spontaneus environments. The rest of the
339 # arguments are optimizer, undefined, finalize, autoescape,
340 # loader, cache size and auto reloading setting
341 True, Undefined, None, False, None, 0, False
Armin Ronacherb5124e62008-04-25 00:36:14 +0200342 )
343
344 node = environment.parse(fileobj.read().decode(encoding))
345 for lineno, func, message in extract_from_ast(node, keywords):
346 yield lineno, func, message, []
Armin Ronachered98cac2008-05-07 08:42:11 +0200347
348
349#: nicer import names
350i18n = InternationalizationExtension