web/lib/django/template/smartif.py
changeset 29 cc9b7e14412b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/lib/django/template/smartif.py	Tue May 25 02:43:45 2010 +0200
@@ -0,0 +1,206 @@
+"""
+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(context, self.first, self.second)
+            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(context, self.first)
+            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 '='.
+# We defer variable evaluation to the lambda to ensure that terms are
+# lazily evaluated using Python's boolean parsing logic.
+OPERATORS = {
+    'or': infix(6, lambda context, x, y: x.eval(context) or y.eval(context)),
+    'and': infix(7, lambda context, x, y: x.eval(context) and y.eval(context)),
+    'not': prefix(8, lambda context, x: not x.eval(context)),
+    'in': infix(9, lambda context, x, y: x.eval(context) in y.eval(context)),
+    'not in': infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)),
+    '=': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
+    '==': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
+    '!=': infix(10, lambda context, x, y: x.eval(context) != y.eval(context)),
+    '>': infix(10, lambda context, x, y: x.eval(context) > y.eval(context)),
+    '>=': infix(10, lambda context, x, y: x.eval(context) >= y.eval(context)),
+    '<': infix(10, lambda context, x, y: x.eval(context) < y.eval(context)),
+    '<=': infix(10, lambda context, x, y: x.eval(context) <= y.eval(context)),
+}
+
+# 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):
+        # pre-pass necessary to turn  'not','in' into single token
+        l = len(tokens)
+        mapped_tokens = []
+        i = 0
+        while i < l:
+            token = tokens[i]
+            if token == "not" and i + 1 < l and tokens[i+1] == "in":
+                token = "not in"
+                i += 1 # skip 'in'
+            mapped_tokens.append(self.translate_token(token))
+            i += 1
+
+        self.tokens = mapped_tokens
+        self.pos = 0
+        self.current_token = self.next()
+
+    def translate_token(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)