Permalink
Browse files

Implemented 'smart if' template tag, allowing filters and various ope…

…rators to be used in the 'if' tag

Thanks to Chris Beaven for the initial patch, Fredrik Lundh for the basis
of the parser methodology and Russell Keith-Magee for code reviews.

There are some BACKWARDS INCOMPATIBILITIES in rare cases - in particular, if
you were using the keywords 'and', 'or' or 'not' as variable names within
the 'if' expression, which was previously allowed in some cases.



git-svn-id: http://code.djangoproject.com/svn/django/trunk@11806 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent 25020dd commit 2c2f5aee4d44836779fcd74c7782368914f9cfd1 @spookylukey spookylukey committed Dec 9, 2009
@@ -11,6 +11,7 @@
from django.template import Node, NodeList, Template, Context, Variable
from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END
from django.template import get_library, Library, InvalidTemplateLibrary
+from django.template.smartif import IfParser, Literal
from django.conf import settings
from django.utils.encoding import smart_str, smart_unicode
from django.utils.itercompat import groupby
@@ -227,10 +228,9 @@ def render(self, context):
return self.nodelist_false.render(context)
class IfNode(Node):
- def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type):
- self.bool_exprs = bool_exprs
+ def __init__(self, var, nodelist_true, nodelist_false=None):
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
- self.link_type = link_type
+ self.var = var
def __repr__(self):
return "<If node>"
@@ -250,28 +250,10 @@ def get_nodes_by_type(self, nodetype):
return nodes
def render(self, context):
- if self.link_type == IfNode.LinkTypes.or_:
- for ifnot, bool_expr in self.bool_exprs:
- try:
- value = bool_expr.resolve(context, True)
- except VariableDoesNotExist:
- value = None
- if (value and not ifnot) or (ifnot and not value):
- return self.nodelist_true.render(context)
- return self.nodelist_false.render(context)
- else:
- for ifnot, bool_expr in self.bool_exprs:
- try:
- value = bool_expr.resolve(context, True)
- except VariableDoesNotExist:
- value = None
- if not ((value and not ifnot) or (ifnot and not value)):
- return self.nodelist_false.render(context)
+ if self.var.eval(context):
return self.nodelist_true.render(context)
-
- class LinkTypes:
- and_ = 0,
- or_ = 1
+ else:
+ return self.nodelist_false.render(context)
class RegroupNode(Node):
def __init__(self, target, expression, var_name):
@@ -761,6 +743,27 @@ def ifnotequal(parser, token):
return do_ifequal(parser, token, True)
ifnotequal = register.tag(ifnotequal)
+class TemplateLiteral(Literal):
+ def __init__(self, value, text):
+ self.value = value
+ self.text = text # for better error messages
+
+ def display(self):
+ return self.text
+
+ def eval(self, context):
+ return self.value.resolve(context, ignore_failures=True)
+
+class TemplateIfParser(IfParser):
+ error_class = TemplateSyntaxError
+
+ def __init__(self, parser, *args, **kwargs):
+ self.template_parser = parser
+ return super(TemplateIfParser, self).__init__(*args, **kwargs)
+
+ def create_var(self, value):
+ return TemplateLiteral(self.template_parser.compile_filter(value), value)
+
#@register.tag(name="if")
def do_if(parser, token):
"""
@@ -805,55 +808,29 @@ def do_if(parser, token):
There are some athletes and absolutely no coaches.
{% endif %}
- ``if`` tags do not allow ``and`` and ``or`` clauses with the same tag,
- because the order of logic would be ambigous. For example, this is
- invalid::
+ Comparison operators are also available, and the use of filters is also
+ allowed, for example:
- {% if athlete_list and coach_list or cheerleader_list %}
+ {% if articles|length >= 5 %}...{% endif %}
- If you need to combine ``and`` and ``or`` to do advanced logic, just use
- nested if tags. For example::
+ Arguments and operators _must_ have a space between them, so
+ ``{% if 1>2 %}`` is not a valid if tag.
- {% if athlete_list %}
- {% if coach_list or cheerleader_list %}
- We have athletes, and either coaches or cheerleaders!
- {% endif %}
- {% endif %}
+ All supported operators are: ``or``, ``and``, ``in``, ``==`` (or ``=``),
+ ``!=``, ``>``, ``>=``, ``<`` and ``<=``.
+
+ Operator precedence follows Python.
"""
- bits = token.contents.split()
- del bits[0]
- if not bits:
- raise TemplateSyntaxError("'if' statement requires at least one argument")
- # Bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
- bitstr = ' '.join(bits)
- boolpairs = bitstr.split(' and ')
- boolvars = []
- if len(boolpairs) == 1:
- link_type = IfNode.LinkTypes.or_
- boolpairs = bitstr.split(' or ')
- else:
- link_type = IfNode.LinkTypes.and_
- if ' or ' in bitstr:
- raise TemplateSyntaxError, "'if' tags can't mix 'and' and 'or'"
- for boolpair in boolpairs:
- if ' ' in boolpair:
- try:
- not_, boolvar = boolpair.split()
- except ValueError:
- raise TemplateSyntaxError, "'if' statement improperly formatted"
- if not_ != 'not':
- raise TemplateSyntaxError, "Expected 'not' in if statement"
- boolvars.append((True, parser.compile_filter(boolvar)))
- else:
- boolvars.append((False, parser.compile_filter(boolpair)))
+ bits = token.split_contents()[1:]
+ var = TemplateIfParser(parser, bits).parse()
nodelist_true = parser.parse(('else', 'endif'))
token = parser.next_token()
if token.contents == 'else':
nodelist_false = parser.parse(('endif',))
parser.delete_first_token()
else:
nodelist_false = NodeList()
- return IfNode(boolvars, nodelist_true, nodelist_false, link_type)
+ return IfNode(var, nodelist_true, nodelist_false)
do_if = register.tag("if", do_if)
#@register.tag
@@ -0,0 +1,192 @@
+"""
+Parser and utilities for the smart 'if' tag
+"""
+import operator
+
+# Using a simple top down parser, as described here:
+# http://effbot.org/zone/simple-top-down-parsing.htm.
+# 'led' = left denotation
+# 'nud' = null denotation
+# 'bp' = binding power (left = lbp, right = rbp)
+
+class TokenBase(object):
+ """
+ Base class for operators and literals, mainly for debugging and for throwing
+ syntax errors.
+ """
+ id = None # node/token type name
+ value = None # used by literals
+ first = second = None # used by tree nodes
+
+ def nud(self, parser):
+ # Null denotation - called in prefix context
+ raise parser.error_class(
+ "Not expecting '%s' in this position in if tag." % self.id
+ )
+
+ def led(self, left, parser):
+ # Left denotation - called in infix context
+ raise parser.error_class(
+ "Not expecting '%s' as infix operator in if tag." % self.id
+ )
+
+ def display(self):
+ """
+ Returns what to display in error messages for this node
+ """
+ return self.id
+
+ def __repr__(self):
+ out = [str(x) for x in [self.id, self.first, self.second] if x is not None]
+ return "(" + " ".join(out) + ")"
+
+
+def infix(bp, func):
+ """
+ Creates an infix operator, given a binding power and a function that
+ evaluates the node
+ """
+ class Operator(TokenBase):
+ lbp = bp
+
+ def led(self, left, parser):
+ self.first = left
+ self.second = parser.expression(bp)
+ return self
+
+ def eval(self, context):
+ try:
+ return func(self.first.eval(context), self.second.eval(context))
+ except Exception:
+ # Templates shouldn't throw exceptions when rendering. We are
+ # most likely to get exceptions for things like {% if foo in bar
+ # %} where 'bar' does not support 'in', so default to False
+ return False
+
+ return Operator
+
+
+def prefix(bp, func):
+ """
+ Creates a prefix operator, given a binding power and a function that
+ evaluates the node.
+ """
+ class Operator(TokenBase):
+ lbp = bp
+
+ def nud(self, parser):
+ self.first = parser.expression(bp)
+ self.second = None
+ return self
+
+ def eval(self, context):
+ try:
+ return func(self.first.eval(context))
+ except Exception:
+ return False
+
+ return Operator
+
+
+# Operator precedence follows Python.
+# NB - we can get slightly more accurate syntax error messages by not using the
+# same object for '==' and '='.
+
+OPERATORS = {
+ 'or': infix(6, lambda x, y: x or y),
+ 'and': infix(7, lambda x, y: x and y),
+ 'not': prefix(8, operator.not_),
+ 'in': infix(9, lambda x, y: x in y),
+ '=': infix(10, operator.eq),
+ '==': infix(10, operator.eq),
+ '!=': infix(10, operator.ne),
+ '>': infix(10, operator.gt),
+ '>=': infix(10, operator.ge),
+ '<': infix(10, operator.lt),
+ '<=': infix(10, operator.le),
+}
+
+# Assign 'id' to each:
+for key, op in OPERATORS.items():
+ op.id = key
+
+
+class Literal(TokenBase):
+ """
+ A basic self-resolvable object similar to a Django template variable.
+ """
+ # IfParser uses Literal in create_var, but TemplateIfParser overrides
+ # create_var so that a proper implementation that actually resolves
+ # variables, filters etc is used.
+ id = "literal"
+ lbp = 0
+
+ def __init__(self, value):
+ self.value = value
+
+ def display(self):
+ return repr(self.value)
+
+ def nud(self, parser):
+ return self
+
+ def eval(self, context):
+ return self.value
+
+ def __repr__(self):
+ return "(%s %r)" % (self.id, self.value)
+
+
+class EndToken(TokenBase):
+ lbp = 0
+
+ def nud(self, parser):
+ raise parser.error_class("Unexpected end of expression in if tag.")
+
+EndToken = EndToken()
+
+
+class IfParser(object):
+ error_class = ValueError
+
+ def __init__(self, tokens):
+ self.tokens = map(self.translate_tokens, tokens)
+ self.pos = 0
+ self.current_token = self.next()
+
+ def translate_tokens(self, token):
+ try:
+ op = OPERATORS[token]
+ except (KeyError, TypeError):
+ return self.create_var(token)
+ else:
+ return op()
+
+ def next(self):
+ if self.pos >= len(self.tokens):
+ return EndToken
+ else:
+ retval = self.tokens[self.pos]
+ self.pos += 1
+ return retval
+
+ def parse(self):
+ retval = self.expression()
+ # Check that we have exhausted all the tokens
+ if self.current_token is not EndToken:
+ raise self.error_class("Unused '%s' at end of if expression." %
+ self.current_token.display())
+ return retval
+
+ def expression(self, rbp=0):
+ t = self.current_token
+ self.current_token = self.next()
+ left = t.nud(self)
+ while rbp < self.current_token.lbp:
+ t = self.current_token
+ self.current_token = self.next()
+ left = t.led(left, self)
+ return left
+
+ def create_var(self, value):
+ return Literal(value)
Oops, something went wrong. Retry.

0 comments on commit 2c2f5ae

Please sign in to comment.