123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- """
- Parser and utilities for the smart 'if' tag
- """
- # 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:
- """
- 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):
- """
- Return 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):
- """
- Create 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):
- """
- Create 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.
- # 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)),
- 'is': infix(10, lambda context, x, y: x.eval(context) is y.eval(context)),
- 'is not': infix(10, lambda context, x, y: x.eval(context) is not 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:
- error_class = ValueError
-
- def __init__(self, tokens):
- # Turn 'is','not' and 'not','in' into single tokens.
- num_tokens = len(tokens)
- mapped_tokens = []
- i = 0
- while i < num_tokens:
- token = tokens[i]
- if token == "is" and i + 1 < num_tokens and tokens[i + 1] == "not":
- token = "is not"
- i += 1 # skip 'not'
- elif token == "not" and i + 1 < num_tokens 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_token()
-
- def translate_token(self, token):
- try:
- op = OPERATORS[token]
- except (KeyError, TypeError):
- return self.create_var(token)
- else:
- return op()
-
- def next_token(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_token()
- left = t.nud(self)
- while rbp < self.current_token.lbp:
- t = self.current_token
- self.current_token = self.next_token()
- left = t.led(left, self)
- return left
-
- def create_var(self, value):
- return Literal(value)
|