Skip to content

Commit

Permalink
added parsing code for "for item in seq recursive" and improved parse…
Browse files Browse the repository at this point in the history
…r interface a bit

--HG--
branch : trunk
  • Loading branch information
mitsuhiko committed May 11, 2008
1 parent 27069d7 commit fdf9530
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 60 deletions.
2 changes: 1 addition & 1 deletion docs/cache_extension.py
Expand Up @@ -27,7 +27,7 @@ def parse(self, parser):

# 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))
Expand Down
4 changes: 2 additions & 2 deletions docs/extensions.rst
Expand Up @@ -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

Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Expand Up @@ -15,6 +15,7 @@ fast and secure.
extensions
integration
switching
tricks

changelog

Expand Down
3 changes: 2 additions & 1 deletion docs/templates.rst
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions docs/tricks.rst
@@ -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>
...
4 changes: 2 additions & 2 deletions jinja2/compiler.py
Expand Up @@ -1175,8 +1175,8 @@ def visitor(self, node, frame):
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(', ')
Expand Down
2 changes: 1 addition & 1 deletion jinja2/ext.py
Expand Up @@ -145,7 +145,7 @@ def parse(self, parser):
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')
Expand Down
30 changes: 19 additions & 11 deletions jinja2/lexer.py
Expand Up @@ -219,19 +219,27 @@ def skip(self, n=1):
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):
Expand Down
2 changes: 1 addition & 1 deletion jinja2/nodes.py
Expand Up @@ -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):
Expand Down
74 changes: 33 additions & 41 deletions jinja2/parser.py
Expand Up @@ -36,24 +36,12 @@ def __init__(self, environment, source, filename=None):
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):
Expand Down Expand Up @@ -107,7 +95,7 @@ def parse_statements(self, end_tokens, drop_needle=False):
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.
Expand All @@ -121,19 +109,21 @@ def parse_statements(self, end_tokens, drop_needle=False):
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."""
Expand Down Expand Up @@ -214,8 +204,7 @@ def parse_context():
'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:
Expand All @@ -226,8 +215,7 @@ def parse_context():
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):
Expand All @@ -238,8 +226,7 @@ def parse_signature(self, node):
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')
Expand Down Expand Up @@ -283,19 +270,22 @@ def parse_print(self):
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')
Expand All @@ -317,8 +307,7 @@ def parse_expression(self, with_condexpr=True):
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()
Expand All @@ -329,8 +318,7 @@ def parse_condexpr(self):
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
Expand All @@ -339,8 +327,7 @@ def parse_or(self):
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
Expand All @@ -355,8 +342,7 @@ def parse_compare(self):
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'):
Expand Down Expand Up @@ -495,7 +481,8 @@ def parse_primary(self, with_postfix=True):
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
Expand All @@ -504,6 +491,11 @@ def parse_tuple(self, simplified=False, with_condexpr=True):
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:
Expand All @@ -517,7 +509,7 @@ def parse_tuple(self, simplified=False, with_condexpr=True):
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':
Expand Down

0 comments on commit fdf9530

Please sign in to comment.