1"""
2Parser and utilities for the smart 'if' tag
3"""
4
5# Using a simple top down parser, as described here:
6# https://11l-lang.org/archive/simple-top-down-parsing/
7# 'led' = left denotation
8# 'nud' = null denotation
9# 'bp' = binding power (left = lbp, right = rbp)
10
11
12class TokenBase:
13 """
14 Base class for operators and literals, mainly for debugging and for throwing
15 syntax errors.
16 """
17
18 id = None # node/token type name
19 value = None # used by literals
20 first = second = None # used by tree nodes
21
22 def nud(self, parser):
23 # Null denotation - called in prefix context
24 raise parser.error_class(
25 "Not expecting '%s' in this position in if tag." % self.id
26 )
27
28 def led(self, left, parser):
29 # Left denotation - called in infix context
30 raise parser.error_class(
31 "Not expecting '%s' as infix operator in if tag." % self.id
32 )
33
34 def display(self):
35 """
36 Return what to display in error messages for this node
37 """
38 return self.id
39
40 def __repr__(self):
41 out = [str(x) for x in [self.id, self.first, self.second] if x is not None]
42 return "(" + " ".join(out) + ")"
43
44
45def infix(bp, func):
46 """
47 Create an infix operator, given a binding power and a function that
48 evaluates the node.
49 """
50
51 class Operator(TokenBase):
52 lbp = bp
53
54 def led(self, left, parser):
55 self.first = left
56 self.second = parser.expression(bp)
57 return self
58
59 def eval(self, context):
60 try:
61 return func(context, self.first, self.second)
62 except Exception:
63 # Templates shouldn't throw exceptions when rendering. We are
64 # most likely to get exceptions for things like {% if foo in bar
65 # %} where 'bar' does not support 'in', so default to False
66 return False
67
68 return Operator
69
70
71def prefix(bp, func):
72 """
73 Create a prefix operator, given a binding power and a function that
74 evaluates the node.
75 """
76
77 class Operator(TokenBase):
78 lbp = bp
79
80 def nud(self, parser):
81 self.first = parser.expression(bp)
82 self.second = None
83 return self
84
85 def eval(self, context):
86 try:
87 return func(context, self.first)
88 except Exception:
89 return False
90
91 return Operator
92
93
94# Operator precedence follows Python.
95# We defer variable evaluation to the lambda to ensure that terms are
96# lazily evaluated using Python's boolean parsing logic.
97OPERATORS = {
98 "or": infix(6, lambda context, x, y: x.eval(context) or y.eval(context)),
99 "and": infix(7, lambda context, x, y: x.eval(context) and y.eval(context)),
100 "not": prefix(8, lambda context, x: not x.eval(context)),
101 "in": infix(9, lambda context, x, y: x.eval(context) in y.eval(context)),
102 "not in": infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)),
103 "is": infix(10, lambda context, x, y: x.eval(context) is y.eval(context)),
104 "is not": infix(10, lambda context, x, y: x.eval(context) is not y.eval(context)),
105 "==": infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
106 "!=": infix(10, lambda context, x, y: x.eval(context) != y.eval(context)),
107 ">": infix(10, lambda context, x, y: x.eval(context) > y.eval(context)),
108 ">=": infix(10, lambda context, x, y: x.eval(context) >= y.eval(context)),
109 "<": infix(10, lambda context, x, y: x.eval(context) < y.eval(context)),
110 "<=": infix(10, lambda context, x, y: x.eval(context) <= y.eval(context)),
111}
112
113# Assign 'id' to each:
114for key, op in OPERATORS.items():
115 op.id = key
116
117
118class Literal(TokenBase):
119 """
120 A basic self-resolvable object similar to a Django template variable.
121 """
122
123 # IfParser uses Literal in create_var, but TemplateIfParser overrides
124 # create_var so that a proper implementation that actually resolves
125 # variables, filters etc. is used.
126 id = "literal"
127 lbp = 0
128
129 def __init__(self, value):
130 self.value = value
131
132 def display(self):
133 return repr(self.value)
134
135 def nud(self, parser):
136 return self
137
138 def eval(self, context):
139 return self.value
140
141 def __repr__(self):
142 return "(%s %r)" % (self.id, self.value)
143
144
145class EndToken(TokenBase):
146 lbp = 0
147
148 def nud(self, parser):
149 raise parser.error_class("Unexpected end of expression in if tag.")
150
151
152EndToken = EndToken()
153
154
155class IfParser:
156 error_class = ValueError
157
158 def __init__(self, tokens):
159 # Turn 'is','not' and 'not','in' into single tokens.
160 num_tokens = len(tokens)
161 mapped_tokens = []
162 i = 0
163 while i < num_tokens:
164 token = tokens[i]
165 if token == "is" and i + 1 < num_tokens and tokens[i + 1] == "not":
166 token = "is not"
167 i += 1 # skip 'not'
168 elif token == "not" and i + 1 < num_tokens and tokens[i + 1] == "in":
169 token = "not in"
170 i += 1 # skip 'in'
171 mapped_tokens.append(self.translate_token(token))
172 i += 1
173
174 self.tokens = mapped_tokens
175 self.pos = 0
176 self.current_token = self.next_token()
177
178 def translate_token(self, token):
179 try:
180 op = OPERATORS[token]
181 except (KeyError, TypeError):
182 return self.create_var(token)
183 else:
184 return op()
185
186 def next_token(self):
187 if self.pos >= len(self.tokens):
188 return EndToken
189 else:
190 retval = self.tokens[self.pos]
191 self.pos += 1
192 return retval
193
194 def parse(self):
195 retval = self.expression()
196 # Check that we have exhausted all the tokens
197 if self.current_token is not EndToken:
198 raise self.error_class(
199 "Unused '%s' at end of if expression." % self.current_token.display()
200 )
201 return retval
202
203 def expression(self, rbp=0):
204 t = self.current_token
205 self.current_token = self.next_token()
206 left = t.nud(self)
207 while rbp < self.current_token.lbp:
208 t = self.current_token
209 self.current_token = self.next_token()
210 left = t.led(left, self)
211 return left
212
213 def create_var(self, value):
214 return Literal(value)