blob: 53b4041ce03a619fac30ffadd71a6cd8d14ba82a [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 """
Armin Ronacher27069d72008-05-11 19:48:12 +020076 raise NotImplementedError()
Armin Ronacher023b5e92008-05-08 11:03:10 +020077
78 def attr(self, name, lineno=None):
79 """Return an attribute node for the current extension. This is useful
80 to pass callbacks to template code::
81
82 nodes.Call(self.attr('_my_callback'), args, kwargs, None, None)
83
84 That would call `self._my_callback` when the template is evaluated.
85 """
86 return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
Armin Ronacher05530932008-04-20 13:27:49 +020087
Armin Ronacher27069d72008-05-11 19:48:12 +020088 def call_method(self, name, args=None, kwargs=None, dyn_args=None,
89 dyn_kwargs=None, lineno=None):
90 """Call a method of the extension."""
91 if args is None:
92 args = []
93 if kwargs is None:
94 kwargs = []
95 return nodes.Call(self.attr(name, lineno=lineno), args, kwargs,
96 dyn_args, dyn_kwargs, lineno=lineno)
97
Armin Ronacher05530932008-04-20 13:27:49 +020098
Armin Ronachered98cac2008-05-07 08:42:11 +020099class InternationalizationExtension(Extension):
Armin Ronacher762079c2008-05-08 23:57:56 +0200100 """This extension adds gettext support to Jinja2."""
Armin Ronacherb5124e62008-04-25 00:36:14 +0200101 tags = set(['trans'])
102
103 def __init__(self, environment):
104 Extension.__init__(self, environment)
Armin Ronacher762079c2008-05-08 23:57:56 +0200105 environment.globals['_'] = contextfunction(lambda c, x: c['gettext'](x))
106 environment.extend(
107 install_gettext_translations=self._install,
108 install_null_translations=self._install_null,
109 uninstall_gettext_translations=self._uninstall,
110 extract_translations=self._extract
111 )
112
113 def _install(self, translations):
114 self.environment.globals.update(
115 gettext=translations.ugettext,
116 ngettext=translations.ungettext
117 )
118
119 def _install_null(self):
120 self.environment.globals.update(
121 gettext=lambda x: x,
122 ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0]
123 )
124
125 def _uninstall(self, translations):
126 for key in 'gettext', 'ngettext':
127 self.environment.globals.pop(key, None)
128
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)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200133
134 def parse(self, parser):
135 """Parse a translatable tag."""
136 lineno = parser.stream.next().lineno
137
Armin Ronacherb5124e62008-04-25 00:36:14 +0200138 # 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
140 # a later state.
141 plural_expr = None
142 variables = {}
143 while parser.stream.current.type is not 'block_end':
144 if variables:
145 parser.stream.expect('comma')
Armin Ronacher023b5e92008-05-08 11:03:10 +0200146
147 # skip colon for python compatibility
Armin Ronacher762079c2008-05-08 23:57:56 +0200148 if parser.skip_colon():
Armin Ronacher023b5e92008-05-08 11:03:10 +0200149 break
150
Armin Ronacherb5124e62008-04-25 00:36:14 +0200151 name = parser.stream.expect('name')
152 if name.value in variables:
153 raise TemplateAssertionError('translatable variable %r defined '
154 'twice.' % name.value, name.lineno,
155 parser.filename)
156
157 # expressions
158 if parser.stream.current.type is 'assign':
159 parser.stream.next()
160 variables[name.value] = var = parser.parse_expression()
161 else:
162 variables[name.value] = var = nodes.Name(name.value, 'load')
163 if plural_expr is None:
164 plural_expr = var
Armin Ronacher023b5e92008-05-08 11:03:10 +0200165
Armin Ronacherb5124e62008-04-25 00:36:14 +0200166 parser.stream.expect('block_end')
167
168 plural = plural_names = None
169 have_plural = False
170 referenced = set()
171
172 # now parse until endtrans or pluralize
173 singular_names, singular = self._parse_block(parser, True)
174 if singular_names:
175 referenced.update(singular_names)
176 if plural_expr is None:
177 plural_expr = nodes.Name(singular_names[0], 'load')
178
179 # if we have a pluralize block, we parse that too
180 if parser.stream.current.test('name:pluralize'):
181 have_plural = True
182 parser.stream.next()
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)
187 parser.stream.next()
188 referenced.update(plural_names)
189 else:
190 parser.stream.next()
191
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')
196
197 # no variables referenced? no need to escape
198 if not referenced:
199 singular = singular.replace('%%', '%')
200 if plural:
201 plural = plural.replace('%%', '%')
202
203 if not have_plural:
204 plural_expr = None
205 elif plural_expr is None:
206 raise TemplateAssertionError('pluralize without variables',
207 lineno, parser.filename)
208
209 if variables:
210 variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
211 for x, y in variables.items()])
212 else:
213 variables = None
214
215 node = self._make_node(singular, plural, variables, plural_expr)
216 node.set_lineno(lineno)
217 return node
218
219 def _parse_block(self, parser, allow_pluralize):
220 """Parse until the next block tag with a given name."""
221 referenced = []
222 buf = []
223 while 1:
224 if parser.stream.current.type is 'data':
225 buf.append(parser.stream.current.value.replace('%', '%%'))
226 parser.stream.next()
227 elif parser.stream.current.type is 'variable_begin':
228 parser.stream.next()
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':
234 parser.stream.next()
235 if parser.stream.current.test('name:endtrans'):
236 break
237 elif parser.stream.current.test('name:pluralize'):
238 if allow_pluralize:
239 break
240 raise TemplateSyntaxError('a translatable section can '
241 'have only one pluralize '
242 'section',
243 parser.stream.current.lineno,
244 parser.filename)
245 raise TemplateSyntaxError('control structures in translatable'
246 ' sections are not allowed.',
247 parser.stream.current.lineno,
248 parser.filename)
249 else:
250 assert False, 'internal parser error'
251
Armin Ronacher2feed1d2008-04-26 16:26:52 +0200252 return referenced, concat(buf)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200253
254 def _make_node(self, singular, plural, variables, plural_expr):
255 """Generates a useful node from the data provided."""
256 # singular only:
257 if plural_expr is None:
258 gettext = nodes.Name('gettext', 'load')
259 node = nodes.Call(gettext, [nodes.Const(singular)],
260 [], None, None)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200261
262 # singular and plural
263 else:
264 ngettext = nodes.Name('ngettext', 'load')
265 node = nodes.Call(ngettext, [
266 nodes.Const(singular),
267 nodes.Const(plural),
268 plural_expr
269 ], [], None, None)
Armin Ronacherd84ec462008-04-29 13:43:16 +0200270
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)
275
276 if variables:
277 node = nodes.Mod(node, variables)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200278 return nodes.Output([node])
279
280
281def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS):
282 """Extract localizable strings from the given template node.
283
284 For every string found this function yields a ``(lineno, function,
285 message)`` tuple, where:
286
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).
292 """
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:
296 continue
297
298 strings = []
299 for arg in node.args:
300 if isinstance(arg, nodes.Const) and \
301 isinstance(arg.value, basestring):
302 strings.append(arg.value)
303 else:
304 strings.append(None)
305
306 if len(strings) == 1:
307 strings = strings[0]
308 else:
309 strings = tuple(strings)
310 yield node.lineno, node.node.name, strings
311
312
313def babel_extract(fileobj, keywords, comment_tags, options):
314 """Babel extraction method for Jinja templates.
315
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)
324 """
325 encoding = options.get('encoding', 'utf-8')
326
327 have_trans_extension = False
328 extensions = []
329 for extension in options.get('extensions', '').split(','):
330 extension = extension.strip()
331 if not extension:
332 continue
333 extension = import_string(extension)
Armin Ronachered98cac2008-05-07 08:42:11 +0200334 if extension is InternationalizationExtension:
Armin Ronacherb5124e62008-04-25 00:36:14 +0200335 have_trans_extension = True
336 extensions.append(extension)
337 if not have_trans_extension:
Armin Ronachered98cac2008-05-07 08:42:11 +0200338 extensions.append(InternationalizationExtension)
Armin Ronacherb5124e62008-04-25 00:36:14 +0200339
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'),
349 tuple(extensions),
350 # fill with defaults so that environments are shared
Armin Ronacher7259c762008-04-30 13:03:59 +0200351 # 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
Armin Ronacherb5124e62008-04-25 00:36:14 +0200355 )
356
357 node = environment.parse(fileobj.read().decode(encoding))
358 for lineno, func, message in extract_from_ast(node, keywords):
359 yield lineno, func, message, []
Armin Ronachered98cac2008-05-07 08:42:11 +0200360
361
362#: nicer import names
363i18n = InternationalizationExtension