Automated merge with ssh://team@pocoo.org/jinja2-main
[jinja2.git] / jinja2 / ext.py
index 230c022fbed6a3d3ed9b3043d5a2634ebbdc6598..93cde8343a8888cf135f195fcaf8a84337667cee 100644 (file)
@@ -3,21 +3,56 @@
     jinja2.ext
     ~~~~~~~~~~
 
-    Jinja extensions (EXPERIMENAL)
-
-    The plan: i18n and caching becomes a parser extension. cache/endcache
-    as well as trans/endtrans are not keyword and don't have nodes but
-    translate into regular jinja nodes so that the person who writes such
-    custom tags doesn't have to generate python code himself.
+    Jinja extensions allow to add custom tags similar to the way django custom
+    tags work.  By default two example extensions exist: an i18n and a cache
+    extension.
 
     :copyright: Copyright 2008 by Armin Ronacher.
     :license: BSD.
 """
+from collections import deque
 from jinja2 import nodes
+from jinja2.defaults import *
+from jinja2.environment import get_spontaneous_environment
+from jinja2.runtime import Undefined, concat
+from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
+from jinja2.utils import contextfunction, import_string, Markup
+
+
+# the only real useful gettext functions for a Jinja template.  Note
+# that ugettext must be assigned to gettext as Jinja doesn't support
+# non unicode strings.
+GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext')
+
+
+class ExtensionRegistry(type):
+    """Gives the extension an unique identifier."""
+
+    def __new__(cls, name, bases, d):
+        rv = type.__new__(cls, name, bases, d)
+        rv.identifier = rv.__module__ + '.' + rv.__name__
+        return rv
 
 
 class Extension(object):
-    """Instances of this class store parser extensions."""
+    """Extensions can be used to add extra functionality to the Jinja template
+    system at the parser level.  Custom extensions are bound to an environment
+    but may not store environment specific data on `self`.  The reason for
+    this is that an extension can be bound to another environment (for
+    overlays) by creating a copy and reassigning the `environment` attribute.
+
+    As extensions are created by the environment they cannot accept any
+    arguments for configuration.  One may want to work around that by using
+    a factory function, but that is not possible as extensions are identified
+    by their import name.  The correct way to configure the extension is
+    storing the configuration values on the environment.  Because this way the
+    environment ends up acting as central configuration storage the
+    attributes may clash which is why extensions have to ensure that the names
+    they choose for configuration are not too generic.  ``prefix`` for example
+    is a terrible name, ``fragment_cache_prefix`` on the other hand is a good
+    name as includes the name of the extension (fragment cache).
+    """
+    __metaclass__ = ExtensionRegistry
 
     #: if this extension parses this is the list of tags it's listening to.
     tags = set()
@@ -25,25 +60,324 @@ class Extension(object):
     def __init__(self, environment):
         self.environment = environment
 
-    def update_globals(self, globals):
-        """Called to inject runtime variables into the globals."""
-        pass
+    def bind(self, environment):
+        """Create a copy of this extension bound to another environment."""
+        rv = object.__new__(self.__class__)
+        rv.__dict__.update(self.__dict__)
+        rv.environment = environment
+        return rv
 
     def parse(self, parser):
-        """Called if one of the tags matched."""
+        """If any of the :attr:`tags` matched this method is called with the
+        parser as first argument.  The token the parser stream is pointing at
+        is the name token that matched.  This method has to return one or a
+        list of multiple nodes.
+        """
+        raise NotImplementedError()
+
+    def attr(self, name, lineno=None):
+        """Return an attribute node for the current extension.  This is useful
+        to pass constants on extensions to generated template code::
 
+            self.attr('_my_attribute', lineno=lineno)
+        """
+        return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
 
-class CacheExtension(Extension):
-    """An example extension that adds cacheable blocks."""
-    tags = set(['cache'])
+    def call_method(self, name, args=None, kwargs=None, dyn_args=None,
+                    dyn_kwargs=None, lineno=None):
+        """Call a method of the extension.  This is a shortcut for
+        :meth:`attr` + :class:`jinja2.nodes.Call`.
+        """
+        if args is None:
+            args = []
+        if kwargs is None:
+            kwargs = []
+        return nodes.Call(self.attr(name, lineno=lineno), args, kwargs,
+                          dyn_args, dyn_kwargs, lineno=lineno)
+
+
+@contextfunction
+def _gettext_alias(context, string):
+    return context.resolve('gettext')(string)
+
+
+class InternationalizationExtension(Extension):
+    """This extension adds gettext support to Jinja2."""
+    tags = set(['trans'])
+
+    def __init__(self, environment):
+        Extension.__init__(self, environment)
+        environment.globals['_'] = _gettext_alias
+        environment.extend(
+            install_gettext_translations=self._install,
+            install_null_translations=self._install_null,
+            uninstall_gettext_translations=self._uninstall,
+            extract_translations=self._extract
+        )
+
+    def _install(self, translations):
+        self.environment.globals.update(
+            gettext=translations.ugettext,
+            ngettext=translations.ungettext
+        )
+
+    def _install_null(self):
+        self.environment.globals.update(
+            gettext=lambda x: x,
+            ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0]
+        )
+
+    def _uninstall(self, translations):
+        for key in 'gettext', 'ngettext':
+            self.environment.globals.pop(key, None)
+
+    def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS):
+        if isinstance(source, basestring):
+            source = self.environment.parse(source)
+        return extract_from_ast(source, gettext_functions)
 
     def parse(self, parser):
+        """Parse a translatable tag."""
         lineno = parser.stream.next().lineno
-        args = [parser.parse_expression()]
-        if self.stream.current.type is 'comma':
-            args.append(parser.parse_expression())
-        body = parser.parse_statements(('name:endcache',), drop_needle=True)
-        return nodes.CallBlock(
-            nodes.Call(nodes.Name('cache_support'), args, [], None, None),
-            [], [], body
-        )
+
+        # find all the variables referenced.  Additionally a variable can be
+        # defined in the body of the trans block too, but this is checked at
+        # a later state.
+        plural_expr = None
+        variables = {}
+        while parser.stream.current.type is not 'block_end':
+            if variables:
+                parser.stream.expect('comma')
+
+            # skip colon for python compatibility
+            if parser.stream.skip_if('colon'):
+                break
+
+            name = parser.stream.expect('name')
+            if name.value in variables:
+                parser.fail('translatable variable %r defined twice.' %
+                            name.value, name.lineno,
+                            exc=TemplateAssertionError)
+
+            # expressions
+            if parser.stream.current.type is 'assign':
+                parser.stream.next()
+                variables[name.value] = var = parser.parse_expression()
+            else:
+                variables[name.value] = var = nodes.Name(name.value, 'load')
+            if plural_expr is None:
+                plural_expr = var
+
+        parser.stream.expect('block_end')
+
+        plural = plural_names = None
+        have_plural = False
+        referenced = set()
+
+        # now parse until endtrans or pluralize
+        singular_names, singular = self._parse_block(parser, True)
+        if singular_names:
+            referenced.update(singular_names)
+            if plural_expr is None:
+                plural_expr = nodes.Name(singular_names[0], 'load')
+
+        # if we have a pluralize block, we parse that too
+        if parser.stream.current.test('name:pluralize'):
+            have_plural = True
+            parser.stream.next()
+            if parser.stream.current.type is not 'block_end':
+                plural_expr = parser.parse_expression()
+            parser.stream.expect('block_end')
+            plural_names, plural = self._parse_block(parser, False)
+            parser.stream.next()
+            referenced.update(plural_names)
+        else:
+            parser.stream.next()
+
+        # register free names as simple name expressions
+        for var in referenced:
+            if var not in variables:
+                variables[var] = nodes.Name(var, 'load')
+
+        # no variables referenced?  no need to escape
+        if not referenced:
+            singular = singular.replace('%%', '%')
+            if plural:
+                plural = plural.replace('%%', '%')
+
+        if not have_plural:
+            plural_expr = None
+        elif plural_expr is None:
+            parser.fail('pluralize without variables', lineno)
+
+        if variables:
+            variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
+                                    for x, y in variables.items()])
+        else:
+            variables = None
+
+        node = self._make_node(singular, plural, variables, plural_expr)
+        node.set_lineno(lineno)
+        return node
+
+    def _parse_block(self, parser, allow_pluralize):
+        """Parse until the next block tag with a given name."""
+        referenced = []
+        buf = []
+        while 1:
+            if parser.stream.current.type is 'data':
+                buf.append(parser.stream.current.value.replace('%', '%%'))
+                parser.stream.next()
+            elif parser.stream.current.type is 'variable_begin':
+                parser.stream.next()
+                name = parser.stream.expect('name').value
+                referenced.append(name)
+                buf.append('%%(%s)s' % name)
+                parser.stream.expect('variable_end')
+            elif parser.stream.current.type is 'block_begin':
+                parser.stream.next()
+                if parser.stream.current.test('name:endtrans'):
+                    break
+                elif parser.stream.current.test('name:pluralize'):
+                    if allow_pluralize:
+                        break
+                    parser.fail('a translatable section can have only one '
+                                'pluralize section')
+                parser.fail('control structures in translatable sections are '
+                            'not allowed')
+            else:
+                assert False, 'internal parser error'
+
+        return referenced, concat(buf)
+
+    def _make_node(self, singular, plural, variables, plural_expr):
+        """Generates a useful node from the data provided."""
+        # singular only:
+        if plural_expr is None:
+            gettext = nodes.Name('gettext', 'load')
+            node = nodes.Call(gettext, [nodes.Const(singular)],
+                              [], None, None)
+
+        # singular and plural
+        else:
+            ngettext = nodes.Name('ngettext', 'load')
+            node = nodes.Call(ngettext, [
+                nodes.Const(singular),
+                nodes.Const(plural),
+                plural_expr
+            ], [], None, None)
+
+        # mark the return value as safe if we are in an
+        # environment with autoescaping turned on
+        if self.environment.autoescape:
+            node = nodes.MarkSafe(node)
+
+        if variables:
+            node = nodes.Mod(node, variables)
+        return nodes.Output([node])
+
+
+class ExprStmtExtension(Extension):
+    """Adds a `do` tag to Jinja2 that works like the print statement just
+    that it doesn't print the return value.
+    """
+    tags = set(['do'])
+
+    def parse(self, parser):
+        node = nodes.ExprStmt(lineno=parser.stream.next().lineno)
+        node.node = parser.parse_tuple()
+        return node
+
+
+class LoopControlExtension(Extension):
+    """Adds break and continue to the template engine."""
+    tags = set(['break', 'continue'])
+
+    def parse(self, parser):
+        token = parser.stream.next()
+        if token.value == 'break':
+            return nodes.Break(lineno=token.lineno)
+        return nodes.Continue(lineno=token.lineno)
+
+
+def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS):
+    """Extract localizable strings from the given template node.
+
+    For every string found this function yields a ``(lineno, function,
+    message)`` tuple, where:
+
+    * ``lineno`` is the number of the line on which the string was found,
+    * ``function`` is the name of the ``gettext`` function used (if the
+      string was extracted from embedded Python code), and
+    *  ``message`` is the string itself (a ``unicode`` object, or a tuple
+       of ``unicode`` objects for functions with multiple string arguments).
+    """
+    for node in node.find_all(nodes.Call):
+        if not isinstance(node.node, nodes.Name) or \
+           node.node.name not in gettext_functions:
+            continue
+
+        strings = []
+        for arg in node.args:
+            if isinstance(arg, nodes.Const) and \
+               isinstance(arg.value, basestring):
+                strings.append(arg.value)
+            else:
+                strings.append(None)
+
+        if len(strings) == 1:
+            strings = strings[0]
+        else:
+            strings = tuple(strings)
+        yield node.lineno, node.node.name, strings
+
+
+def babel_extract(fileobj, keywords, comment_tags, options):
+    """Babel extraction method for Jinja templates.
+
+    :param fileobj: the file-like object the messages should be extracted from
+    :param keywords: a list of keywords (i.e. function names) that should be
+                     recognized as translation functions
+    :param comment_tags: a list of translator tags to search for and include
+                         in the results.  (Unused)
+    :param options: a dictionary of additional options (optional)
+    :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
+             (comments will be empty currently)
+    """
+    extensions = set()
+    for extension in options.get('extensions', '').split(','):
+        extension = extension.strip()
+        if not extension:
+            continue
+        extensions.add(import_string(extension))
+    if InternationalizationExtension not in extensions:
+        extensions.add(InternationalizationExtension)
+
+    environment = get_spontaneous_environment(
+        options.get('block_start_string', BLOCK_START_STRING),
+        options.get('block_end_string', BLOCK_END_STRING),
+        options.get('variable_start_string', VARIABLE_START_STRING),
+        options.get('variable_end_string', VARIABLE_END_STRING),
+        options.get('comment_start_string', COMMENT_START_STRING),
+        options.get('comment_end_string', COMMENT_END_STRING),
+        options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX,
+        str(options.get('trim_blocks', TRIM_BLOCKS)).lower() in \
+            ('1', 'on', 'yes', 'true'),
+        NEWLINE_SEQUENCE, frozenset(extensions),
+        # fill with defaults so that environments are shared
+        # with other spontaneus environments.  The rest of the
+        # arguments are optimizer, undefined, finalize, autoescape,
+        # loader, cache size and auto reloading setting
+        True, Undefined, None, False, None, 0, False
+    )
+
+    source = fileobj.read().decode(options.get('encoding', 'utf-8'))
+    node = environment.parse(source)
+    for lineno, func, message in extract_from_ast(node, keywords):
+        yield lineno, func, message, []
+
+
+#: nicer import names
+i18n = InternationalizationExtension
+do = ExprStmtExtension
+loopcontrols = LoopControlExtension