added parsing code for "for item in seq recursive" and improved parser interface...
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 11 May 2008 20:20:51 +0000 (22:20 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 11 May 2008 20:20:51 +0000 (22:20 +0200)
--HG--
branch : trunk

docs/cache_extension.py
docs/extensions.rst
docs/index.rst
docs/templates.rst
docs/tricks.rst [new file with mode: 0644]
jinja2/compiler.py
jinja2/ext.py
jinja2/lexer.py
jinja2/nodes.py
jinja2/parser.py

index 9f324d2cb94bd7739cca1672b5641745150f66b5..ec0067bb2b2958452cd097bb2b116fd1e45c3471 100644 (file)
@@ -27,7 +27,7 @@ class FragmentCacheExtension(Extension):
 
         # if there is a comma, the user provided a timeout.  If not use
         # None as second parameter.
-        if parser.skip_comma():
+        if parser.stream.skip_if('comma'):
             args.append(parser.parse_expression())
         else:
             args.append(nodes.Const(None))
index f5527f7c1156988a2ad877beaa69f011ab7160b4..63e9cd93dd8d820027e9daa9f843995bf0bea2f8 100644 (file)
@@ -155,7 +155,7 @@ extensions:
 
 .. autoclass:: jinja2.parser.Parser
     :members: parse_expression, parse_tuple, parse_assign_target,
-              parse_statements, skip_colon, skip_comma, free_identifier
+              parse_statements, free_identifier
 
     .. attribute:: filename
 
@@ -169,7 +169,7 @@ extensions:
         The current :class:`~jinja2.lexer.TokenStream`
 
 .. autoclass:: jinja2.lexer.TokenStream
-   :members: push, look, eos, skip, next, expect
+   :members: push, look, eos, skip, next, next_if, skip_if, expect
 
    .. attribute:: current
 
index 308563a4ef348d436a002ce1bc06af17d785efda..43f36809de78e4451c9a0aed7b6776a1f8a4e4a1 100644 (file)
@@ -15,6 +15,7 @@ fast and secure.
    extensions
    integration
    switching
+   tricks
 
    changelog
 
index d7a0f914487229bf7ecdac55083e384c43533023..ea19e2d55abcdd525235e8a972e0a73854394514 100644 (file)
@@ -281,7 +281,8 @@ The ``{% extends %}`` tag is the key here. It tells the template engine that
 this template "extends" another template.  When the template system evaluates
 this template, first it locates the parent.  The extends tag should be the
 first tag in the template.  Everything before it is printed out normally and
-may cause confusion.
+may cause confusion.  For details about this behavior and how to take
+advantage of it, see :ref:`null-master-fallback`.
 
 The filename of the template depends on the template loader.  For example the
 :class:`FileSystemLoader` allows you to access other templates by giving the
diff --git a/docs/tricks.rst b/docs/tricks.rst
new file mode 100644 (file)
index 0000000..6029ec5
--- /dev/null
@@ -0,0 +1,81 @@
+Tipps and Tricks
+================
+
+.. highlight:: html+jinja
+
+This part of the documentation shows some tipps and tricks for Jinja2
+templates.
+
+
+.. _null-master-fallback:
+
+Null-Master Fallback
+--------------------
+
+Jinja2 supports dynamic inheritance and does not distinguish between parent
+and child template as long as no `extends` tag is visited.  While this leads
+to the surprising behavior that everything before the first `extends` tag
+including whitespace is printed out instead of being igored, it can be used
+for a neat trick.
+
+Usually child templates extend from one template that adds a basic HTML
+skeleton.  However it's possible put the `extends` tag into an `if` tag to
+only extend from the layout template if the `standalone` variable evaluates
+to false which it does per default if it's not defined.  Additionally a very
+basic skeleton is added to the file so that if it's indeed rendered with
+`standalone` set to `True` a very basic HTML skeleton is added::
+
+    {% if not standalone %}{% extends 'master.html' %}{% endif -%}
+    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+    <title>{% block title %}The Page Title{% endblock %}</title>
+    <link rel="stylesheet" href="style.css" type="text/css">
+    {% block body %}
+      <p>This is the page body.</p>
+    {% endblock %}
+
+
+Alternating Rows
+----------------
+
+If you want to have different styles for each row of a table or
+list you can use the `cycle` method on the `loop` object::
+
+    <ul>
+    {% for row in rows %}
+      <li class="{{ loop.cycle('odd', 'even') }}">{{ row }}</li>
+    {% endfor %}
+    </ul>
+
+`cycle` can take an unlimited amount of strings.  Each time this
+tag is encountered the next item from the list is rendered.
+
+
+Highlighting Active Menu Items
+------------------------------
+
+Often you want to have a navigation bar with an active navigation
+item.  This is really simple to achieve.  Because assignments outside
+of `block`\s in child templates are global and executed before the layout
+template is evaluated it's possible to define the active menu item in the
+child template::
+
+    {% extends "layout.html" %}
+    {% set active_page = "index" %}
+
+The layout template can then access `active_page`.  Additionally it makes
+sense to defined a default for that variable::
+
+    {% navigation_bar = [
+        ('/', 'index', 'Index'),
+        ('/downloads/', 'downloads', 'Downloads'),
+        ('/about/', 'about', 'About')
+    ] -%}
+    {% active_page = active_page|default('index') -%}
+    ...
+    <ul id="navigation">
+    {% for href, id, caption in navigation_bar %}
+      <li{% if id == active_page %} class="active"{% endif
+      %}><a href="{{ href|e }}">{{ caption|e }}</a>/li>
+    {% endfor %}
+    </ul>
+    ...
index 7b8366cd8f6da813b50bc76e3665ce57991ce343..df1a85eb094a99925ceb6ab6796f059a6b3bdbf3 100644 (file)
@@ -1175,8 +1175,8 @@ class CodeGenerator(NodeVisitor):
     del binop, uaop
 
     def visit_Concat(self, node, frame):
-        self.write('%s((' % self.environment.autoescape and
-                   'markup_join' or 'unicode_join')
+        self.write('%s((' % (self.environment.autoescape and
+                             'markup_join' or 'unicode_join'))
         for arg in node.nodes:
             self.visit(arg, frame)
             self.write(', ')
index 53b4041ce03a619fac30ffadd71a6cd8d14ba82a..aac752870b0c36cf22bafb9f1b1bf878536f389b 100644 (file)
@@ -145,7 +145,7 @@ class InternationalizationExtension(Extension):
                 parser.stream.expect('comma')
 
             # skip colon for python compatibility
-            if parser.skip_colon():
+            if parser.stream.skip_if('colon'):
                 break
 
             name = parser.stream.expect('name')
index d950025b70cc5306b72253f62ecc9b5b71d5fa2a..639b2852cd5ab96fe0d269b1c3b2164d848145a0 100644 (file)
@@ -219,19 +219,27 @@ class TokenStream(object):
         for x in xrange(n):
             self.next()
 
-    def next(self, skip_eol=True):
+    def next_if(self, expr):
+        """Perform the token test and return the token if it matched.
+        Otherwise the return value is `None`.
+        """
+        if self.current.test(expr):
+            return self.next()
+
+    def skip_if(self, expr):
+        """Like `next_if` but only returns `True` or `False`."""
+        return self.next_if(expr) is not None
+
+    def next(self):
         """Go one token ahead and return the old one"""
         rv = self.current
-        while 1:
-            if self._pushed:
-                self.current = self._pushed.popleft()
-            elif self.current.type is not 'eof':
-                try:
-                    self.current = self._next()
-                except StopIteration:
-                    self.close()
-            if not skip_eol or self.current.type is not 'eol':
-                break
+        if self._pushed:
+            self.current = self._pushed.popleft()
+        elif self.current.type is not 'eof':
+            try:
+                self.current = self._next()
+            except StopIteration:
+                self.close()
         return rv
 
     def close(self):
index 180478d749b02f3be852002e4a83c26b0daca556..67f7559b3b37bebbade6e6b6026a8c2ec8a1f7ea 100644 (file)
@@ -273,7 +273,7 @@ class For(Stmt):
 
     For filtered nodes an expression can be stored as `test`, otherwise `None`.
     """
-    fields = ('target', 'iter', 'body', 'else_', 'test')
+    fields = ('target', 'iter', 'body', 'else_', 'test', 'recursive')
 
 
 class If(Stmt):
index 8d23b5f75caa6f527c4a689931716d75f7becddf..941e037c2003a02633a97180b1998ad16172e7ee 100644 (file)
@@ -36,24 +36,12 @@ class Parser(object):
                 self.extensions[tag] = extension.parse
         self._last_identifier = 0
 
-    def is_tuple_end(self):
+    def is_tuple_end(self, extra_end_rules=None):
         """Are we at the end of a tuple?"""
-        return self.stream.current.type in ('variable_end', 'block_end',
-                                            'rparen') or \
-               self.stream.current.test('name:in')
-
-    def skip_colon(self):
-        """If there is a colon, skip it and return `True`, else `False`."""
-        if self.stream.current.type is 'colon':
-            self.stream.next()
-            return True
-        return False
-
-    def skip_comma(self):
-        """If there is a comma, skip it and return `True`, else `False`."""
-        if self.stream.current.type is 'comma':
-            self.stream.next()
+        if self.stream.current.type in ('variable_end', 'block_end', 'rparen'):
             return True
+        elif extra_end_rules is not None:
+            return self.stream.current.test_any(extra_end_rules)
         return False
 
     def free_identifier(self, lineno=None):
@@ -107,7 +95,7 @@ class Parser(object):
         can be set to `True` and the end token is removed.
         """
         # the first token may be a colon for python compatibility
-        self.skip_colon()
+        self.stream.skip_if('colon')
 
         # in the future it would be possible to add whole code sections
         # by adding some sort of end of statement token and parsing those here.
@@ -121,19 +109,21 @@ class Parser(object):
     def parse_for(self):
         """Parse a for loop."""
         lineno = self.stream.expect('name:for').lineno
-        target = self.parse_assign_target()
+        target = self.parse_assign_target(extra_end_rules=('name:in',))
         self.stream.expect('name:in')
-        iter = self.parse_tuple(with_condexpr=False)
+        iter = self.parse_tuple(with_condexpr=False,
+                                extra_end_rules=('name:recursive',))
         test = None
-        if self.stream.current.test('name:if'):
-            self.stream.next()
+        if self.stream.skip_if('name:if'):
             test = self.parse_expression()
+        recursive = self.stream.skip_if('name:recursive')
         body = self.parse_statements(('name:endfor', 'name:else'))
         if self.stream.next().value == 'endfor':
             else_ = []
         else:
             else_ = self.parse_statements(('name:endfor',), drop_needle=True)
-        return nodes.For(target, iter, body, else_, test, lineno=lineno)
+        return nodes.For(target, iter, body, else_, test,
+                         recursive, lineno=lineno)
 
     def parse_if(self):
         """Parse an if construct."""
@@ -214,8 +204,7 @@ class Parser(object):
                                                  'underscores can not be '
                                                  'imported', target.lineno,
                                                  self.filename)
-                if self.stream.current.test('name:as'):
-                    self.stream.next()
+                if self.stream.skip_if('name:as'):
                     alias = self.parse_assign_target(name_only=True)
                     node.names.append((target.name, alias.name))
                 else:
@@ -226,8 +215,7 @@ class Parser(object):
                 break
         if not hasattr(node, 'with_context'):
             node.with_context = False
-            if self.stream.current.type is 'comma':
-                self.stream.next()
+            self.stream.skip_if('comma')
         return node
 
     def parse_signature(self, node):
@@ -238,8 +226,7 @@ class Parser(object):
             if args:
                 self.stream.expect('comma')
             arg = self.parse_assign_target(name_only=True)
-            if self.stream.current.type is 'assign':
-                self.stream.next()
+            if self.stream.skip_if('assign'):
                 defaults.append(self.parse_expression())
             args.append(arg)
         self.stream.expect('rparen')
@@ -283,19 +270,22 @@ class Parser(object):
             node.nodes.append(self.parse_expression())
         return node
 
-    def parse_assign_target(self, with_tuple=True, name_only=False):
+    def parse_assign_target(self, with_tuple=True, name_only=False,
+                            extra_end_rules=None):
         """Parse an assignment target.  As Jinja2 allows assignments to
         tuples, this function can parse all allowed assignment targets.  Per
         default assignments to tuples are parsed, that can be disable however
         by setting `with_tuple` to `False`.  If only assignments to names are
-        wanted `name_only` can be set to `True`.
+        wanted `name_only` can be set to `True`.  The `extra_end_rules`
+        parameter is forwarded to the tuple parsing function.
         """
         if name_only:
             token = self.stream.expect('name')
             target = nodes.Name(token.value, 'store', lineno=token.lineno)
         else:
             if with_tuple:
-                target = self.parse_tuple(simplified=True)
+                target = self.parse_tuple(simplified=True,
+                                          extra_end_rules=extra_end_rules)
             else:
                 target = self.parse_primary(with_postfix=False)
             target.set_ctx('store')
@@ -317,8 +307,7 @@ class Parser(object):
     def parse_condexpr(self):
         lineno = self.stream.current.lineno
         expr1 = self.parse_or()
-        while self.stream.current.test('name:if'):
-            self.stream.next()
+        while self.stream.skip_if('name:if'):
             expr2 = self.parse_or()
             self.stream.expect('name:else')
             expr3 = self.parse_condexpr()
@@ -329,8 +318,7 @@ class Parser(object):
     def parse_or(self):
         lineno = self.stream.current.lineno
         left = self.parse_and()
-        while self.stream.current.test('name:or'):
-            self.stream.next()
+        while self.stream.skip_if('name:or'):
             right = self.parse_and()
             left = nodes.Or(left, right, lineno=lineno)
             lineno = self.stream.current.lineno
@@ -339,8 +327,7 @@ class Parser(object):
     def parse_and(self):
         lineno = self.stream.current.lineno
         left = self.parse_compare()
-        while self.stream.current.test('name:and'):
-            self.stream.next()
+        while self.stream.skip_if('name:and'):
             right = self.parse_compare()
             left = nodes.And(left, right, lineno=lineno)
             lineno = self.stream.current.lineno
@@ -355,8 +342,7 @@ class Parser(object):
             if token_type in _compare_operators:
                 self.stream.next()
                 ops.append(nodes.Operand(token_type, self.parse_add()))
-            elif self.stream.current.test('name:in'):
-                self.stream.next()
+            elif self.stream.skip_if('name:in'):
                 ops.append(nodes.Operand('in', self.parse_add()))
             elif self.stream.current.test('name:not') and \
                  self.stream.look().test('name:in'):
@@ -495,7 +481,8 @@ class Parser(object):
             node = self.parse_postfix(node)
         return node
 
-    def parse_tuple(self, simplified=False, with_condexpr=True):
+    def parse_tuple(self, simplified=False, with_condexpr=True,
+                    extra_end_rules=None):
         """Works like `parse_expression` but if multiple expressions are
         delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created.
         This method could also return a regular expression instead of a tuple
@@ -504,6 +491,11 @@ class Parser(object):
         The default parsing mode is a full tuple.  If `simplified` is `True`
         only names and literals are parsed.  The `no_condexpr` parameter is
         forwarded to :meth:`parse_expression`.
+
+        Because tuples do not require delimiters and may end in a bogus comma
+        an extra hint is needed that marks the end of a tuple.  For example
+        for loops support tuples between `for` and `in`.  In that case the
+        `extra_end_rules` is set to ``['name:in']``.
         """
         lineno = self.stream.current.lineno
         if simplified:
@@ -517,7 +509,7 @@ class Parser(object):
         while 1:
             if args:
                 self.stream.expect('comma')
-            if self.is_tuple_end():
+            if self.is_tuple_end(extra_end_rules):
                 break
             args.append(parse())
             if self.stream.current.type is 'comma':