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