diff --git a/docs/api.rst b/docs/api.rst index 4ce79ff..5a131ed 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -68,13 +68,15 @@ High Level API A dict of filters for this environment. As long as no template was loaded it's safe to add new filters or remove old. For custom filters - see :ref:`writing-filters`. + see :ref:`writing-filters`. For valid filter names have a look at + :ref:`identifier-naming`. .. attribute:: tests A dict of test functions for this environment. As long as no template was loaded it's safe to modify this dict. For custom tests - see :ref:`writing-tests`. + see :ref:`writing-tests`. For valid test names have a look at + :ref:`identifier-naming`. .. attribute:: globals @@ -82,6 +84,7 @@ High Level API in a template and (if the optimizer is enabled) may not be overridden by templates. As long as no template was loaded it's safe to modify this dict. For more details see :ref:`global-namespace`. + For valid object names have a look at :ref:`identifier-naming`. .. automethod:: overlay([options]) @@ -111,6 +114,24 @@ High Level API :members: disable_buffering, enable_buffering +.. _identifier-naming: + +Notes on Identifiers +~~~~~~~~~~~~~~~~~~~~ + +Jinja2 uses the regular Python 2.x naming rules. Valid identifiers have to +match ``[a-zA-Z_][a-zA-Z0-9_]*``. As a matter of fact non ASCII characters +are currently not allowed. This limitation will probably go away as soon as +unicode identifiers are fully specified for Python 3. + +Filters and tests are looked up in separate namespaces and have slightly +modified identifier syntax. Filters and tests may contain dots to group +filters and tests by topic. For example it's perfectly valid to add a +function into the filter dict and call it `to.unicode`. The regular +expression for filter and test identifiers is +``[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*```. + + Undefined Types --------------- diff --git a/docs/extensions.rst b/docs/extensions.rst index 8bd80f5..13fe639 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -102,6 +102,13 @@ task and usually not needed as the default tags and expressions cover all common use cases. The i18n extension is a good example of why extensions are useful, another one would be fragment caching. +When writing extensions you have to keep in mind that you are working with the +Jinja2 template compiler which does not validate the node tree you are possing +to it. If the AST is malformed you will get all kinds of compiler or runtime +errors that are horrible to debug. Always make sure you are using the nodes +you create correctly. The API documentation below shows which nodes exist and +how to use them. + Example Extension ~~~~~~~~~~~~~~~~~ diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 9ee323d..6518427 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -325,6 +325,10 @@ class CodeGenerator(NodeVisitor): # the current line number self.code_lineno = 1 + # registry of all filters and tests (global, not block local) + self.tests = {} + self.filters = {} + # the debug information self.debug_info = [] self._write_debug_info = None @@ -473,10 +477,13 @@ class CodeGenerator(NodeVisitor): visitor = DependencyFinderVisitor() for node in nodes: visitor.visit(node) - for name in visitor.filters: - self.writeline('f_%s = environment.filters[%r]' % (name, name)) - for name in visitor.tests: - self.writeline('t_%s = environment.tests[%r]' % (name, name)) + for dependency in 'filters', 'tests': + mapping = getattr(self, dependency) + for name in getattr(visitor, dependency): + if name not in mapping: + mapping[name] = self.temporary_identifier() + self.writeline('%s = environment.%s[%r]' % + (mapping[name], dependency, name)) def collect_shadowed(self, frame): """This function returns all the shadowed variables in a dict @@ -1215,7 +1222,7 @@ class CodeGenerator(NodeVisitor): self.visit(node.step, frame) def visit_Filter(self, node, frame, initial=None): - self.write('f_%s(' % node.name) + self.write(self.filters[node.name] + '(') func = self.environment.filters.get(node.name) if func is None: raise TemplateAssertionError('no filter named %r' % node.name, @@ -1234,7 +1241,7 @@ class CodeGenerator(NodeVisitor): self.write(')') def visit_Test(self, node, frame): - self.write('t_%s(' % node.name) + self.write(self.tests[node.name] + '(') if node.name not in self.environment.tests: raise TemplateAssertionError('no test named %r' % node.name, node.lineno, self.filename) diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 2519682..180478d 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -727,6 +727,9 @@ class EnvironmentAttribute(Expr): class ExtensionAttribute(Expr): """Returns the attribute of an extension bound to the environment. The identifier is the identifier of the :class:`Extension`. + + This node is usually constructed by calling the + :meth:`~jinja2.ext.Extension.attr` method on an extension. """ fields = ('identifier', 'attr') diff --git a/jinja2/parser.py b/jinja2/parser.py index dae1a6b..8d23b5f 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -676,18 +676,21 @@ class Parser(object): lineno=token.lineno) def parse_filter(self, node, start_inline=False): - lineno = self.stream.current.type while self.stream.current.type == 'pipe' or start_inline: if not start_inline: self.stream.next() token = self.stream.expect('name') + name = token.value + while self.stream.current.type is 'dot': + self.stream.next() + name += '.' + self.stream.expect('name').value if self.stream.current.type is 'lparen': args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None) else: args = [] kwargs = [] dyn_args = dyn_kwargs = None - node = nodes.Filter(node, token.value, args, kwargs, dyn_args, + node = nodes.Filter(node, name, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno) start_inline = False return node @@ -700,6 +703,9 @@ class Parser(object): else: negated = False name = self.stream.expect('name').value + while self.stream.current.type is 'dot': + self.stream.next() + name += '.' + self.stream.expect('name').value dyn_args = dyn_kwargs = None kwargs = [] if self.stream.current.type is 'lparen':