summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
Diffstat (limited to 'django')
-rw-r--r--django/template/defaulttags.py101
-rw-r--r--django/template/smartif.py192
2 files changed, 231 insertions, 62 deletions
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 6d57cdeef8..77b9b9795c 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -11,6 +11,7 @@ except NameError:
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 @@ class IfEqualNode(Node):
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 @@ class IfNode(Node):
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,47 +808,21 @@ 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':
@@ -853,7 +830,7 @@ def do_if(parser, token):
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
diff --git a/django/template/smartif.py b/django/template/smartif.py
new file mode 100644
index 0000000000..e8a08e9087
--- /dev/null
+++ b/django/template/smartif.py
@@ -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)