OLD | NEW |
1 # Copyright 2012 Benjamin Kalman | 1 # Copyright 2012 Benjamin Kalman |
2 # | 2 # |
3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
4 # you may not use this file except in compliance with the License. | 4 # you may not use this file except in compliance with the License. |
5 # You may obtain a copy of the License at | 5 # You may obtain a copy of the License at |
6 # | 6 # |
7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
8 # | 8 # |
9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 # See the License for the specific language governing permissions and | 12 # See the License for the specific language governing permissions and |
13 # limitations under the License. | 13 # limitations under the License. |
14 | 14 |
| 15 # TODO: Some character other than {{{ }}} to print unescaped content? |
| 16 # TODO: Only have @ while in a loop, and only defined in the top context of |
| 17 # the loop. |
| 18 # TODO: Consider trimming spaces around identifers like {{?t foo}}. |
| 19 # TODO: Only transfer global contexts into partials, not the top local. |
| 20 # TODO: Pragmas for asserting the presence of variables. |
| 21 # TODO: Escaping control characters somehow. e.g. \{{, \{{-. |
| 22 # TODO: Dump warnings-so-far into the output. |
| 23 |
15 import json | 24 import json |
16 import re | 25 import re |
17 | 26 |
18 """ Handlebar templates are mostly-logicless templates inspired by ctemplate | 27 '''Handlebar templates are data binding templates more-than-loosely inspired by |
19 (or more specifically mustache templates) then taken in their own direction | 28 ctemplate. Use like: |
20 because I found those to be inadequate. | |
21 | 29 |
22 from handlebar import Handlebar | 30 from handlebar import Handlebar |
23 | 31 |
24 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') | 32 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') |
25 input = { | 33 input = { |
26 'foo': [ | 34 'foo': [ |
27 { 'bar': 1 }, | 35 { 'bar': 1 }, |
28 { 'bar': 2 }, | 36 { 'bar': 2 }, |
29 { 'bar': 3 } | 37 { 'bar': 3 } |
30 ] | 38 ] |
31 } | 39 } |
32 print(template.render(input).text) | 40 print(template.render(input).text) |
33 | 41 |
34 Handlebar will use get() on contexts to return values, so to create custom | 42 Handlebar will use get() on contexts to return values, so to create custom |
35 getters (e.g. something that populates values lazily from keys) just add | 43 getters (for example, something that populates values lazily from keys), just |
36 a get() method. | 44 provide an object with a get() method. |
37 | 45 |
38 class CustomContext(object): | 46 class CustomContext(object): |
39 def get(self, key): | 47 def get(self, key): |
40 return 10 | 48 return 10 |
| 49 print(Handlebar('hello {{world}}').render(CustomContext()).text) |
41 | 50 |
42 # Any time {{ }} is used, will fill it with 10. | 51 will print 'hello 10'. |
43 print(Handlebar('hello {{world}}').render(CustomContext()).text) | 52 ''' |
44 """ | |
45 | 53 |
46 class ParseException(Exception): | 54 class ParseException(Exception): |
47 """ Exception thrown while parsing the template. | 55 '''The exception thrown while parsing a template. |
48 """ | 56 ''' |
49 def __init__(self, error, line): | 57 def __init__(self, error): |
50 Exception.__init__(self, "%s (line %s)" % (error, line.number)) | 58 Exception.__init__(self, error) |
51 | 59 |
52 class RenderResult(object): | 60 class RenderResult(object): |
53 """ Result of a render operation. | 61 '''The result of a render operation. |
54 """ | 62 ''' |
55 def __init__(self, text, errors): | 63 def __init__(self, text, errors): |
56 self.text = text; | 64 self.text = text; |
57 self.errors = errors | 65 self.errors = errors |
58 | 66 |
59 class StringBuilder(object): | 67 def __str__(self): |
60 """ Mimics Java's StringBuilder for easy porting from the Java version of | 68 return self.text |
61 this file to Python. | 69 |
62 """ | 70 def __repr__(self): |
| 71 return '%s(text=%s, errors=%s)' % ( |
| 72 self.__class__.__name__, self.text, self.errors) |
| 73 |
| 74 class _StringBuilder(object): |
| 75 '''Efficiently builds strings. |
| 76 ''' |
63 def __init__(self): | 77 def __init__(self): |
64 self._buf = [] | 78 self._buf = [] |
65 | 79 |
66 def __len__(self): | 80 def __len__(self): |
67 self._Collapse() | 81 self._Collapse() |
68 return len(self._buf[0]) | 82 return len(self._buf[0]) |
69 | 83 |
70 def append(self, string): | 84 def Append(self, string): |
| 85 if not isinstance(string, basestring): |
| 86 string = str(string) |
71 self._buf.append(string) | 87 self._buf.append(string) |
72 | 88 |
73 def toString(self): | 89 def ToString(self): |
74 self._Collapse() | 90 self._Collapse() |
75 return self._buf[0] | 91 return self._buf[0] |
76 | 92 |
77 def __str__(self): | 93 def __str__(self): |
78 return self.toString() | 94 return self.ToString() |
79 | 95 |
80 def _Collapse(self): | 96 def _Collapse(self): |
81 self._buf = [u''.join(self._buf)] | 97 self._buf = [u''.join(self._buf)] |
82 | 98 |
83 class RenderState(object): | 99 class _Contexts(object): |
84 """ The state of a render call. | 100 '''Tracks a stack of context objects, providing efficient key/value retrieval. |
85 """ | 101 ''' |
86 def __init__(self, globalContexts, localContexts): | 102 class _Node(object): |
87 self.globalContexts = globalContexts | 103 '''A node within the stack. Wraps a real context and maintains the key/value |
88 self.localContexts = localContexts | 104 pairs seen so far. |
89 self.text = StringBuilder() | 105 ''' |
90 self.errors = [] | 106 def __init__(self, value): |
91 self._errorsDisabled = False | 107 self._value = value |
92 | 108 self._value_has_get = hasattr(value, 'get') |
93 def inSameContext(self): | 109 self._found = {} |
94 return RenderState(self.globalContexts, self.localContexts) | 110 |
95 | 111 def GetKeys(self): |
96 def getFirstContext(self): | 112 '''Returns the list of keys that |_value| contains. |
97 if len(self.localContexts) > 0: | 113 ''' |
98 return self.localContexts[0] | 114 return self._found.keys() |
99 if len(self.globalContexts) > 0: | 115 |
100 return self.globalContexts[0] | 116 def Get(self, key): |
101 return None | 117 '''Returns the value for |key|, or None if not found (including if |
102 | 118 |_value| doesn't support key retrieval). |
103 def disableErrors(self): | 119 ''' |
104 self._errorsDisabled = True | 120 if not self._value_has_get: |
105 return self | |
106 | |
107 def addError(self, *messages): | |
108 if self._errorsDisabled: | |
109 return self | |
110 buf = StringBuilder() | |
111 for message in messages: | |
112 buf.append(str(message)) | |
113 self.errors.append(buf.toString()) | |
114 return self | |
115 | |
116 def getResult(self): | |
117 return RenderResult(self.text.toString(), self.errors); | |
118 | |
119 class Identifier(object): | |
120 """ An identifier of the form "@", "foo.bar.baz", or "@.foo.bar.baz". | |
121 """ | |
122 def __init__(self, name, line): | |
123 self._isThis = (name == '@') | |
124 if self._isThis: | |
125 self._startsWithThis = False | |
126 self._path = [] | |
127 return | |
128 | |
129 thisDot = '@.' | |
130 self._startsWithThis = name.startswith(thisDot) | |
131 if self._startsWithThis: | |
132 name = name[len(thisDot):] | |
133 | |
134 if not re.match('^[a-zA-Z0-9._\\-/]+$', name): | |
135 raise ParseException(name + " is not a valid identifier", line) | |
136 self._path = name.split('.') | |
137 | |
138 def resolve(self, renderState): | |
139 if self._isThis: | |
140 return renderState.getFirstContext() | |
141 | |
142 if self._startsWithThis: | |
143 return self._resolveFromContext(renderState.getFirstContext()) | |
144 | |
145 resolved = self._resolveFromContexts(renderState.localContexts) | |
146 if resolved is None: | |
147 resolved = self._resolveFromContexts(renderState.globalContexts) | |
148 if resolved is None: | |
149 renderState.addError("Couldn't resolve identifier ", self._path) | |
150 return resolved | |
151 | |
152 def _resolveFromContexts(self, contexts): | |
153 for context in contexts: | |
154 resolved = self._resolveFromContext(context) | |
155 if resolved is not None: | |
156 return resolved | |
157 return None | |
158 | |
159 def _resolveFromContext(self, context): | |
160 result = context | |
161 for next in self._path: | |
162 # Only require that contexts provide a get method, meaning that callers | |
163 # can provide dict-like contexts (for example, to populate values lazily). | |
164 if result is None or not getattr(result, "get", None): | |
165 return None | 121 return None |
166 result = result.get(next) | 122 value = self._found.get(key) |
167 return result | 123 if value is not None: |
| 124 return value |
| 125 value = self._value.get(key) |
| 126 if value is not None: |
| 127 self._found[key] = value |
| 128 return value |
| 129 |
| 130 def __init__(self, globals_): |
| 131 '''Initializes with the initial global contexts, listed in order from most |
| 132 to least important. |
| 133 ''' |
| 134 self._nodes = map(_Contexts._Node, globals_) |
| 135 self._first_local = len(self._nodes) |
| 136 self._value_info = {} |
| 137 |
| 138 def CreateFromGlobals(self): |
| 139 new = _Contexts([]) |
| 140 new._nodes = self._nodes[:self._first_local] |
| 141 new._first_local = self._first_local |
| 142 return new |
| 143 |
| 144 def Push(self, context): |
| 145 self._nodes.append(_Contexts._Node(context)) |
| 146 |
| 147 def Pop(self): |
| 148 node = self._nodes.pop() |
| 149 assert len(self._nodes) >= self._first_local |
| 150 for found_key in node.GetKeys(): |
| 151 # [0] is the stack of nodes that |found_key| has been found in. |
| 152 self._value_info[found_key][0].pop() |
| 153 |
| 154 def GetTopLocal(self): |
| 155 if len(self._nodes) == self._first_local: |
| 156 return None |
| 157 return self._nodes[-1]._value |
| 158 |
| 159 def Resolve(self, path): |
| 160 # This method is only efficient at finding |key|; if |tail| has a value (and |
| 161 # |key| evaluates to an indexable value) we'll need to descend into that. |
| 162 key, tail = path.split('.', 1) if '.' in path else (path, None) |
| 163 |
| 164 if key == '@': |
| 165 found = self._nodes[-1]._value |
| 166 else: |
| 167 found = self._FindNodeValue(key) |
| 168 |
| 169 if tail is None: |
| 170 return found |
| 171 |
| 172 for part in tail.split('.'): |
| 173 if not hasattr(found, 'get'): |
| 174 return None |
| 175 found = found.get(part) |
| 176 return found |
| 177 |
| 178 def _FindNodeValue(self, key): |
| 179 # |found_node_list| will be all the nodes that |key| has been found in. |
| 180 # |checked_node_set| are those that have been checked. |
| 181 info = self._value_info.get(key) |
| 182 if info is None: |
| 183 info = ([], set()) |
| 184 self._value_info[key] = info |
| 185 found_node_list, checked_node_set = info |
| 186 |
| 187 # Check all the nodes not yet checked for |key|. |
| 188 newly_found = [] |
| 189 for node in reversed(self._nodes): |
| 190 if node in checked_node_set: |
| 191 break |
| 192 value = node.Get(key) |
| 193 if value is not None: |
| 194 newly_found.append(node) |
| 195 checked_node_set.add(node) |
| 196 |
| 197 # The nodes will have been found in reverse stack order. After extending |
| 198 # the found nodes, the freshest value will be at the tip of the stack. |
| 199 found_node_list.extend(reversed(newly_found)) |
| 200 if not found_node_list: |
| 201 return None |
| 202 |
| 203 return found_node_list[-1]._value[key] |
| 204 |
| 205 class _Stack(object): |
| 206 class Entry(object): |
| 207 def __init__(self, name, id_): |
| 208 self.name = name |
| 209 self.id_ = id_ |
| 210 |
| 211 def __init__(self, entries=[]): |
| 212 self.entries = entries |
| 213 |
| 214 def Descend(self, name, id_): |
| 215 descended = list(self.entries) |
| 216 descended.append(_Stack.Entry(name, id_)) |
| 217 return _Stack(entries=descended) |
| 218 |
| 219 class _RenderState(object): |
| 220 '''The state of a render call. |
| 221 ''' |
| 222 def __init__(self, name, contexts, _stack=_Stack()): |
| 223 self.text = _StringBuilder() |
| 224 self.contexts = contexts |
| 225 self._name = name |
| 226 self._errors = [] |
| 227 self._stack = _stack |
| 228 |
| 229 def AddResolutionError(self, id_): |
| 230 self._errors.append( |
| 231 id_.CreateResolutionErrorMessage(self._name, stack=self._stack)) |
| 232 |
| 233 def Copy(self): |
| 234 return _RenderState( |
| 235 self._name, self.contexts, _stack=self._stack) |
| 236 |
| 237 def ForkPartial(self, custom_name, id_): |
| 238 name = custom_name or id_.name |
| 239 return _RenderState(name, |
| 240 self.contexts.CreateFromGlobals(), |
| 241 _stack=self._stack.Descend(name, id_)) |
| 242 |
| 243 def Merge(self, render_state, text_transform=None): |
| 244 self._errors.extend(render_state._errors) |
| 245 text = render_state.text.ToString() |
| 246 if text_transform is not None: |
| 247 text = text_transform(text) |
| 248 self.text.Append(text) |
| 249 |
| 250 def GetResult(self): |
| 251 return RenderResult(self.text.ToString(), self._errors); |
| 252 |
| 253 class _Identifier(object): |
| 254 ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'. |
| 255 ''' |
| 256 def __init__(self, name, line, column): |
| 257 self.name = name |
| 258 self.line = line |
| 259 self.column = column |
| 260 if name == '': |
| 261 raise ParseException('Empty identifier %s' % self.GetDescription()) |
| 262 for part in name.split('.'): |
| 263 if part != '@' and not re.match('^[a-zA-Z0-9_/-]+$', part): |
| 264 raise ParseException('Invalid identifier %s' % self.GetDescription()) |
| 265 |
| 266 def GetDescription(self): |
| 267 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) |
| 268 |
| 269 def CreateResolutionErrorMessage(self, name, stack=None): |
| 270 message = _StringBuilder() |
| 271 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), |
| 272 name)) |
| 273 if stack is not None: |
| 274 for entry in stack.entries: |
| 275 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), |
| 276 entry.name)) |
| 277 return message.ToString() |
168 | 278 |
169 def __str__(self): | 279 def __str__(self): |
170 if self._isThis: | 280 raise ValueError() |
171 return '@' | 281 |
172 name = '.'.join(self._path) | 282 class _Line(object): |
173 return ('@.' + name) if self._startsWithThis else name | |
174 | |
175 class Line(object): | |
176 def __init__(self, number): | 283 def __init__(self, number): |
177 self.number = number | 284 self.number = number |
178 | 285 |
179 class LeafNode(object): | 286 def __str__(self): |
180 def __init__(self, line): | 287 return str(self.number) |
181 self._line = line | 288 |
182 | 289 class _LeafNode(object): |
183 def startsWithNewLine(self): | 290 def __init__(self, start_line, end_line): |
| 291 self._start_line = start_line |
| 292 self._end_line = end_line |
| 293 |
| 294 def StartsWithNewLine(self): |
184 return False | 295 return False |
185 | 296 |
186 def trimStartingNewLine(self): | 297 def TrimStartingNewLine(self): |
187 pass | 298 pass |
188 | 299 |
189 def trimEndingSpaces(self): | 300 def TrimEndingSpaces(self): |
190 return 0 | 301 return 0 |
191 | 302 |
192 def trimEndingNewLine(self): | 303 def TrimEndingNewLine(self): |
193 pass | 304 pass |
194 | 305 |
195 def endsWithEmptyLine(self): | 306 def EndsWithEmptyLine(self): |
196 return False | 307 return False |
197 | 308 |
198 def getStartLine(self): | 309 def GetStartLine(self): |
199 return self._line | 310 return self._start_line |
200 | 311 |
201 def getEndLine(self): | 312 def GetEndLine(self): |
202 return self._line | 313 return self._end_line |
203 | 314 |
204 class DecoratorNode(object): | 315 class _DecoratorNode(object): |
205 def __init__(self, content): | 316 def __init__(self, content): |
206 self._content = content | 317 self._content = content |
207 | 318 |
208 def startsWithNewLine(self): | 319 def StartsWithNewLine(self): |
209 return self._content.startsWithNewLine() | 320 return self._content.StartsWithNewLine() |
210 | 321 |
211 def trimStartingNewLine(self): | 322 def TrimStartingNewLine(self): |
212 self._content.trimStartingNewLine() | 323 self._content.TrimStartingNewLine() |
213 | 324 |
214 def trimEndingSpaces(self): | 325 def TrimEndingSpaces(self): |
215 return self._content.trimEndingSpaces() | 326 return self._content.TrimEndingSpaces() |
216 | 327 |
217 def trimEndingNewLine(self): | 328 def TrimEndingNewLine(self): |
218 self._content.trimEndingNewLine() | 329 self._content.TrimEndingNewLine() |
219 | 330 |
220 def endsWithEmptyLine(self): | 331 def EndsWithEmptyLine(self): |
221 return self._content.endsWithEmptyLine() | 332 return self._content.EndsWithEmptyLine() |
222 | 333 |
223 def getStartLine(self): | 334 def GetStartLine(self): |
224 return self._content.getStartLine() | 335 return self._content.GetStartLine() |
225 | 336 |
226 def getEndLine(self): | 337 def GetEndLine(self): |
227 return self._content.getEndLine() | 338 return self._content.GetEndLine() |
228 | 339 |
229 class InlineNode(DecoratorNode): | 340 class _InlineNode(_DecoratorNode): |
230 def __init__(self, content): | 341 def __init__(self, content): |
231 DecoratorNode.__init__(self, content) | 342 _DecoratorNode.__init__(self, content) |
232 | 343 |
233 def render(self, renderState): | 344 def Render(self, render_state): |
234 contentRenderState = renderState.inSameContext() | 345 content_render_state = render_state.Copy() |
235 self._content.render(contentRenderState) | 346 self._content.Render(content_render_state) |
236 | 347 render_state.Merge(content_render_state, |
237 renderState.errors.extend(contentRenderState.errors) | 348 text_transform=lambda text: text.replace('\n', '')) |
238 renderState.text.append( | 349 |
239 contentRenderState.text.toString().replace('\n', '')) | 350 class _IndentedNode(_DecoratorNode): |
240 | |
241 class IndentedNode(DecoratorNode): | |
242 def __init__(self, content, indentation): | 351 def __init__(self, content, indentation): |
243 DecoratorNode.__init__(self, content) | 352 _DecoratorNode.__init__(self, content) |
244 self._indent_str = ' ' * indentation | 353 self._indent_str = ' ' * indentation |
245 | 354 |
246 def render(self, renderState): | 355 def Render(self, render_state): |
247 contentRenderState = renderState.inSameContext() | 356 if isinstance(self._content, _CommentNode): |
248 self._content.render(contentRenderState) | 357 return |
249 | 358 content_render_state = render_state.Copy() |
250 renderState.errors.extend(contentRenderState.errors) | 359 self._content.Render(content_render_state) |
251 renderState.text.append(self._indent_str) | 360 def AddIndentation(text): |
252 # TODO: this might introduce an extra \n at the end? need test. | 361 buf = _StringBuilder() |
253 renderState.text.append( | 362 buf.Append(self._indent_str) |
254 contentRenderState.text.toString().replace('\n', | 363 buf.Append(text.replace('\n', '\n%s' % self._indent_str)) |
255 '\n' + self._indent_str)) | 364 buf.Append('\n') |
256 renderState.text.append('\n') | 365 return buf.ToString() |
257 | 366 render_state.Merge(content_render_state, text_transform=AddIndentation) |
258 class BlockNode(DecoratorNode): | 367 |
| 368 class _BlockNode(_DecoratorNode): |
259 def __init__(self, content): | 369 def __init__(self, content): |
260 DecoratorNode.__init__(self, content) | 370 _DecoratorNode.__init__(self, content) |
261 content.trimStartingNewLine() | 371 content.TrimStartingNewLine() |
262 content.trimEndingSpaces() | 372 content.TrimEndingSpaces() |
263 | 373 |
264 def render(self, renderState): | 374 def Render(self, render_state): |
265 self._content.render(renderState) | 375 self._content.Render(render_state) |
266 | 376 |
267 class NodeCollection(object): | 377 class _NodeCollection(object): |
268 def __init__(self, nodes): | 378 def __init__(self, nodes): |
269 if len(nodes) == 0: | 379 assert nodes |
270 raise ValueError() | |
271 self._nodes = nodes | 380 self._nodes = nodes |
272 | 381 |
273 def render(self, renderState): | 382 def Render(self, render_state): |
274 for node in self._nodes: | 383 for node in self._nodes: |
275 node.render(renderState) | 384 node.Render(render_state) |
276 | 385 |
277 def startsWithNewLine(self): | 386 def StartsWithNewLine(self): |
278 return self._nodes[0].startsWithNewLine() | 387 return self._nodes[0].StartsWithNewLine() |
279 | 388 |
280 def trimStartingNewLine(self): | 389 def TrimStartingNewLine(self): |
281 self._nodes[0].trimStartingNewLine() | 390 self._nodes[0].TrimStartingNewLine() |
282 | 391 |
283 def trimEndingSpaces(self): | 392 def TrimEndingSpaces(self): |
284 return self._nodes[-1].trimEndingSpaces() | 393 return self._nodes[-1].TrimEndingSpaces() |
285 | 394 |
286 def trimEndingNewLine(self): | 395 def TrimEndingNewLine(self): |
287 self._nodes[-1].trimEndingNewLine() | 396 self._nodes[-1].TrimEndingNewLine() |
288 | 397 |
289 def endsWithEmptyLine(self): | 398 def EndsWithEmptyLine(self): |
290 return self._nodes[-1].endsWithEmptyLine() | 399 return self._nodes[-1].EndsWithEmptyLine() |
291 | 400 |
292 def getStartLine(self): | 401 def GetStartLine(self): |
293 return self._nodes[0].getStartLine() | 402 return self._nodes[0].GetStartLine() |
294 | 403 |
295 def getEndLine(self): | 404 def GetEndLine(self): |
296 return self._nodes[-1].getEndLine() | 405 return self._nodes[-1].GetEndLine() |
297 | 406 |
298 class StringNode(object): | 407 class _StringNode(object): |
299 """ Just a string. | 408 ''' Just a string. |
300 """ | 409 ''' |
301 def __init__(self, string, startLine, endLine): | 410 def __init__(self, string, start_line, end_line): |
302 self._string = string | 411 self._string = string |
303 self._startLine = startLine | 412 self._start_line = start_line |
304 self._endLine = endLine | 413 self._end_line = end_line |
305 | 414 |
306 def render(self, renderState): | 415 def Render(self, render_state): |
307 renderState.text.append(self._string) | 416 render_state.text.Append(self._string) |
308 | 417 |
309 def startsWithNewLine(self): | 418 def StartsWithNewLine(self): |
310 return self._string.startswith('\n') | 419 return self._string.startswith('\n') |
311 | 420 |
312 def trimStartingNewLine(self): | 421 def TrimStartingNewLine(self): |
313 if self.startsWithNewLine(): | 422 if self.StartsWithNewLine(): |
314 self._string = self._string[1:] | 423 self._string = self._string[1:] |
315 | 424 |
316 def trimEndingSpaces(self): | 425 def TrimEndingSpaces(self): |
317 originalLength = len(self._string) | 426 original_length = len(self._string) |
318 self._string = self._string[:self._lastIndexOfSpaces()] | 427 self._string = self._string[:self._LastIndexOfSpaces()] |
319 return originalLength - len(self._string) | 428 return original_length - len(self._string) |
320 | 429 |
321 def trimEndingNewLine(self): | 430 def TrimEndingNewLine(self): |
322 if self._string.endswith('\n'): | 431 if self._string.endswith('\n'): |
323 self._string = self._string[:len(self._string) - 1] | 432 self._string = self._string[:len(self._string) - 1] |
324 | 433 |
325 def endsWithEmptyLine(self): | 434 def EndsWithEmptyLine(self): |
326 index = self._lastIndexOfSpaces() | 435 index = self._LastIndexOfSpaces() |
327 return index == 0 or self._string[index - 1] == '\n' | 436 return index == 0 or self._string[index - 1] == '\n' |
328 | 437 |
329 def _lastIndexOfSpaces(self): | 438 def _LastIndexOfSpaces(self): |
330 index = len(self._string) | 439 index = len(self._string) |
331 while index > 0 and self._string[index - 1] == ' ': | 440 while index > 0 and self._string[index - 1] == ' ': |
332 index -= 1 | 441 index -= 1 |
333 return index | 442 return index |
334 | 443 |
335 def getStartLine(self): | 444 def GetStartLine(self): |
336 return self._startLine | 445 return self._start_line |
337 | 446 |
338 def getEndLine(self): | 447 def GetEndLine(self): |
339 return self._endLine | 448 return self._end_line |
340 | 449 |
341 class EscapedVariableNode(LeafNode): | 450 class _EscapedVariableNode(_LeafNode): |
342 """ {{foo}} | 451 ''' {{foo}} |
343 """ | 452 ''' |
344 def __init__(self, id, line): | 453 def __init__(self, id_): |
345 LeafNode.__init__(self, line) | 454 _LeafNode.__init__(self, id_.line, id_.line) |
346 self._id = id | 455 self._id = id_ |
347 | 456 |
348 def render(self, renderState): | 457 def Render(self, render_state): |
349 value = self._id.resolve(renderState) | 458 value = render_state.contexts.Resolve(self._id.name) |
350 if value is None: | 459 if value is None: |
| 460 render_state.AddResolutionError(self._id) |
351 return | 461 return |
352 | |
353 string = value if isinstance(value, basestring) else str(value) | 462 string = value if isinstance(value, basestring) else str(value) |
354 renderState.text.append(string.replace('&', '&') | 463 render_state.text.Append(string.replace('&', '&') |
355 .replace('<', '<') | 464 .replace('<', '<') |
356 .replace('>', '>')) | 465 .replace('>', '>')) |
357 | 466 |
358 class UnescapedVariableNode(LeafNode): | 467 class _UnescapedVariableNode(_LeafNode): |
359 """ {{{foo}}} | 468 ''' {{{foo}}} |
360 """ | 469 ''' |
361 def __init__(self, id, line): | 470 def __init__(self, id_): |
362 LeafNode.__init__(self, line) | 471 _LeafNode.__init__(self, id_.line, id_.line) |
363 self._id = id | 472 self._id = id_ |
364 | 473 |
365 def render(self, renderState): | 474 def Render(self, render_state): |
366 value = self._id.resolve(renderState) | 475 value = render_state.contexts.Resolve(self._id.name) |
367 if value is None: | 476 if value is None: |
| 477 render_state.AddResolutionError(self._id) |
368 return | 478 return |
369 renderState.text.append( | 479 string = value if isinstance(value, basestring) else str(value) |
370 value if isinstance(value, basestring) else str(value)) | 480 render_state.text.Append(string) |
371 | 481 |
372 class SectionNode(DecoratorNode): | 482 class _CommentNode(_LeafNode): |
373 """ {{#foo}} ... {{/}} | 483 '''{{- This is a comment -}} |
374 """ | 484 An empty placeholder node for correct indented rendering behaviour. |
375 def __init__(self, id, content): | 485 ''' |
376 DecoratorNode.__init__(self, content) | 486 def __init__(self, start_line, end_line): |
377 self._id = id | 487 _LeafNode.__init__(self, start_line, end_line) |
378 | 488 |
379 def render(self, renderState): | 489 def Render(self, render_state): |
380 value = self._id.resolve(renderState) | 490 pass |
381 if value is None: | 491 |
382 return | 492 class _SectionNode(_DecoratorNode): |
383 | 493 ''' {{#foo}} ... {{/}} |
| 494 ''' |
| 495 def __init__(self, id_, content): |
| 496 _DecoratorNode.__init__(self, content) |
| 497 self._id = id_ |
| 498 |
| 499 def Render(self, render_state): |
| 500 value = render_state.contexts.Resolve(self._id.name) |
384 if isinstance(value, list): | 501 if isinstance(value, list): |
385 for item in value: | 502 for item in value: |
386 renderState.localContexts.insert(0, item) | 503 # Always push context, even if it's not "valid", since we want to |
387 self._content.render(renderState) | 504 # be able to refer to items in a list such as [1,2,3] via @. |
388 renderState.localContexts.pop(0) | 505 render_state.contexts.Push(item) |
389 elif isinstance(value, dict): | 506 self._content.Render(render_state) |
390 renderState.localContexts.insert(0, value) | 507 render_state.contexts.Pop() |
391 self._content.render(renderState) | 508 elif hasattr(value, 'get'): |
392 renderState.localContexts.pop(0) | 509 render_state.contexts.Push(value) |
| 510 self._content.Render(render_state) |
| 511 render_state.contexts.Pop() |
393 else: | 512 else: |
394 renderState.addError("{{#", self._id, | 513 render_state.AddResolutionError(self._id) |
395 "}} cannot be rendered with a ", type(value)) | 514 |
396 | 515 class _VertedSectionNode(_DecoratorNode): |
397 class VertedSectionNode(DecoratorNode): | 516 ''' {{?foo}} ... {{/}} |
398 """ {{?foo}} ... {{/}} | 517 ''' |
399 """ | 518 def __init__(self, id_, content): |
400 def __init__(self, id, content): | 519 _DecoratorNode.__init__(self, content) |
401 DecoratorNode.__init__(self, content) | 520 self._id = id_ |
402 self._id = id | 521 |
403 | 522 def Render(self, render_state): |
404 def render(self, renderState): | 523 value = render_state.contexts.Resolve(self._id.name) |
405 value = self._id.resolve(renderState.inSameContext().disableErrors()) | 524 if _VertedSectionNode.ShouldRender(value): |
406 if _VertedSectionNodeShouldRender(value): | 525 render_state.contexts.Push(value) |
407 renderState.localContexts.insert(0, value) | 526 self._content.Render(render_state) |
408 self._content.render(renderState) | 527 render_state.contexts.Pop() |
409 renderState.localContexts.pop(0) | 528 |
410 | 529 @staticmethod |
411 def _VertedSectionNodeShouldRender(value): | 530 def ShouldRender(value): |
412 if value is None: | 531 if value is None: |
413 return False | 532 return False |
414 if isinstance(value, bool): | 533 if isinstance(value, bool): |
415 return value | 534 return value |
416 if (isinstance(value, int) or | 535 if isinstance(value, list): |
417 isinstance(value, long) or | 536 return len(value) > 0 |
418 isinstance(value, float)): | |
419 return True | 537 return True |
420 if isinstance(value, basestring): | 538 |
421 return True | 539 class _InvertedSectionNode(_DecoratorNode): |
422 if isinstance(value, list): | 540 ''' {{^foo}} ... {{/}} |
423 return len(value) > 0 | 541 ''' |
424 if isinstance(value, dict): | 542 def __init__(self, id_, content): |
425 return True | 543 _DecoratorNode.__init__(self, content) |
426 raise TypeError("Unhandled type %s" % type(value)) | 544 self._id = id_ |
427 | 545 |
428 class InvertedSectionNode(DecoratorNode): | 546 def Render(self, render_state): |
429 """ {{^foo}} ... {{/}} | 547 value = render_state.contexts.Resolve(self._id.name) |
430 """ | 548 if not _VertedSectionNode.ShouldRender(value): |
431 def __init__(self, id, content): | 549 self._content.Render(render_state) |
432 DecoratorNode.__init__(self, content) | 550 |
433 self._id = id | 551 class _JsonNode(_LeafNode): |
434 | 552 ''' {{*foo}} |
435 def render(self, renderState): | 553 ''' |
436 value = self._id.resolve(renderState.inSameContext().disableErrors()) | 554 def __init__(self, id_): |
437 if not _VertedSectionNodeShouldRender(value): | 555 _LeafNode.__init__(self, id_.line, id_.line) |
438 self._content.render(renderState) | 556 self._id = id_ |
439 | 557 |
440 class JsonNode(LeafNode): | 558 def Render(self, render_state): |
441 """ {{*foo}} | 559 value = render_state.contexts.Resolve(self._id.name) |
442 """ | |
443 def __init__(self, id, line): | |
444 LeafNode.__init__(self, line) | |
445 self._id = id | |
446 | |
447 def render(self, renderState): | |
448 value = self._id.resolve(renderState) | |
449 if value is None: | 560 if value is None: |
| 561 render_state.AddResolutionError(self._id) |
450 return | 562 return |
451 renderState.text.append(json.dumps(value, separators=(',',':'))) | 563 render_state.text.Append(json.dumps(value, separators=(',',':'))) |
452 | 564 |
453 class PartialNode(LeafNode): | 565 class _PartialNode(_LeafNode): |
454 """ {{+foo}} | 566 ''' {{+foo}} |
455 """ | 567 ''' |
456 def __init__(self, id, line): | 568 def __init__(self, id_): |
457 LeafNode.__init__(self, line) | 569 _LeafNode.__init__(self, id_.line, id_.line) |
458 self._id = id | 570 self._id = id_ |
459 self._args = None | 571 self._args = None |
460 | 572 self._local_context_id = None |
461 def render(self, renderState): | 573 |
462 value = self._id.resolve(renderState) | 574 def Render(self, render_state): |
| 575 value = render_state.contexts.Resolve(self._id.name) |
| 576 if value is None: |
| 577 render_state.AddResolutionError(self._id) |
| 578 return |
463 if not isinstance(value, Handlebar): | 579 if not isinstance(value, Handlebar): |
464 renderState.addError(self._id, " didn't resolve to a Handlebar") | 580 render_state.AddResolutionError(self._id) |
465 return | 581 return |
466 | 582 |
467 argContext = [] | 583 partial_render_state = render_state.ForkPartial(value._name, self._id) |
468 if len(renderState.localContexts) > 0: | 584 |
469 argContext.append(renderState.localContexts[0]) | 585 # TODO: Don't do this. Force callers to do this by specifying an @ argument. |
470 | 586 top_local = render_state.contexts.GetTopLocal() |
471 if self._args: | 587 if top_local is not None: |
472 argContextMap = {} | 588 partial_render_state.contexts.Push(top_local) |
473 for key, valueId in self._args.items(): | 589 |
474 context = valueId.resolve(renderState) | 590 if self._args is not None: |
475 if context: | 591 arg_context = {} |
476 argContextMap[key] = context | 592 for key, value_id in self._args.items(): |
477 argContext.append(argContextMap) | 593 context = render_state.contexts.Resolve(value_id.name) |
478 | 594 if context is not None: |
479 partialRenderState = RenderState(renderState.globalContexts, argContext) | 595 arg_context[key] = context |
480 value._topNode.render(partialRenderState) | 596 partial_render_state.contexts.Push(arg_context) |
481 | 597 |
482 text = partialRenderState.text.toString() | 598 if self._local_context_id is not None: |
483 if len(text) > 0 and text[-1] == '\n': | 599 local_context = render_state.contexts.Resolve(self._local_context_id.name) |
484 text = text[:-1] | 600 if local_context is not None: |
485 | 601 partial_render_state.contexts.Push(local_context) |
486 renderState.text.append(text) | 602 |
487 renderState.errors.extend(partialRenderState.errors) | 603 value._top_node.Render(partial_render_state) |
488 | 604 |
489 def addArgument(self, key, valueId): | 605 render_state.Merge( |
490 if not self._args: | 606 partial_render_state, |
| 607 text_transform=lambda text: text[:-1] if text.endswith('\n') else text) |
| 608 |
| 609 def AddArgument(self, key, id_): |
| 610 if self._args is None: |
491 self._args = {} | 611 self._args = {} |
492 self._args[key] = valueId | 612 self._args[key] = id_ |
493 | 613 |
494 # List of tokens in order of longest to shortest, to avoid any prefix matching | 614 def SetLocalContext(self, id_): |
495 # issues. | 615 self._local_context_id = id_ |
496 TokenValues = [] | 616 |
497 | 617 _TOKENS = {} |
498 class Token(object): | 618 |
499 """ The tokens that can appear in a template. | 619 class _Token(object): |
500 """ | 620 ''' The tokens that can appear in a template. |
| 621 ''' |
501 class Data(object): | 622 class Data(object): |
502 def __init__(self, name, text, clazz): | 623 def __init__(self, name, text, clazz): |
503 self.name = name | 624 self.name = name |
504 self.text = text | 625 self.text = text |
505 self.clazz = clazz | 626 self.clazz = clazz |
506 TokenValues.append(self) | 627 _TOKENS[text] = self |
507 | 628 |
508 def elseNodeClass(self): | 629 def ElseNodeClass(self): |
509 if self.clazz == VertedSectionNode: | 630 if self.clazz == _VertedSectionNode: |
510 return InvertedSectionNode | 631 return _InvertedSectionNode |
511 if self.clazz == InvertedSectionNode: | 632 if self.clazz == _InvertedSectionNode: |
512 return VertedSectionNode | 633 return _VertedSectionNode |
513 raise ValueError(self.clazz + " can not have an else clause.") | 634 raise ValueError('%s cannot have an else clause.' % self.clazz) |
514 | 635 |
515 OPEN_START_SECTION = Data("OPEN_START_SECTION" , "{{#", Secti
onNode) | 636 def __str__(self): |
516 OPEN_START_VERTED_SECTION = Data("OPEN_START_VERTED_SECTION" , "{{?", Verte
dSectionNode) | 637 return '%s(%s)' % (self.name, self.text) |
517 OPEN_START_INVERTED_SECTION = Data("OPEN_START_INVERTED_SECTION", "{{^", Inver
tedSectionNode) | 638 |
518 OPEN_START_JSON = Data("OPEN_START_JSON" , "{{*", JsonN
ode) | 639 OPEN_START_SECTION = Data('OPEN_START_SECTION' , '{{#', _Sect
ionNode) |
519 OPEN_START_PARTIAL = Data("OPEN_START_PARTIAL" , "{{+", Parti
alNode) | 640 OPEN_START_VERTED_SECTION = Data('OPEN_START_VERTED_SECTION' , '{{?', _Vert
edSectionNode) |
520 OPEN_ELSE = Data("OPEN_ELSE" , "{{:", None) | 641 OPEN_START_INVERTED_SECTION = Data('OPEN_START_INVERTED_SECTION', '{{^', _Inve
rtedSectionNode) |
521 OPEN_END_SECTION = Data("OPEN_END_SECTION" , "{{/", None) | 642 OPEN_START_JSON = Data('OPEN_START_JSON' , '{{*', _Json
Node) |
522 OPEN_UNESCAPED_VARIABLE = Data("OPEN_UNESCAPED_VARIABLE" , "{{{", Unesc
apedVariableNode) | 643 OPEN_START_PARTIAL = Data('OPEN_START_PARTIAL' , '{{+', _Part
ialNode) |
523 CLOSE_MUSTACHE3 = Data("CLOSE_MUSTACHE3" , "}}}", None) | 644 OPEN_ELSE = Data('OPEN_ELSE' , '{{:', None) |
524 OPEN_COMMENT = Data("OPEN_COMMENT" , "{{-", None) | 645 OPEN_END_SECTION = Data('OPEN_END_SECTION' , '{{/', None) |
525 CLOSE_COMMENT = Data("CLOSE_COMMENT" , "-}}", None) | 646 INLINE_END_SECTION = Data('INLINE_END_SECTION' , '/}}', None) |
526 OPEN_VARIABLE = Data("OPEN_VARIABLE" , "{{" , Escap
edVariableNode) | 647 OPEN_UNESCAPED_VARIABLE = Data('OPEN_UNESCAPED_VARIABLE' , '{{{', _Unes
capedVariableNode) |
527 CLOSE_MUSTACHE = Data("CLOSE_MUSTACHE" , "}}" , None) | 648 CLOSE_MUSTACHE3 = Data('CLOSE_MUSTACHE3' , '}}}', None) |
528 CHARACTER = Data("CHARACTER" , "." , None) | 649 OPEN_COMMENT = Data('OPEN_COMMENT' , '{{-', _Comm
entNode) |
529 | 650 CLOSE_COMMENT = Data('CLOSE_COMMENT' , '-}}', None) |
530 class TokenStream(object): | 651 OPEN_VARIABLE = Data('OPEN_VARIABLE' , '{{' , _Esca
pedVariableNode) |
531 """ Tokeniser for template parsing. | 652 CLOSE_MUSTACHE = Data('CLOSE_MUSTACHE' , '}}' , None) |
532 """ | 653 CHARACTER = Data('CHARACTER' , '.' , None) |
| 654 |
| 655 class _TokenStream(object): |
| 656 ''' Tokeniser for template parsing. |
| 657 ''' |
533 def __init__(self, string): | 658 def __init__(self, string): |
534 self._remainder = string | 659 self.next_token = None |
535 | 660 self.next_line = _Line(1) |
536 self.nextToken = None | 661 self.next_column = 0 |
537 self.nextContents = None | 662 self._string = string |
538 self.nextLine = Line(1) | 663 self._cursor = 0 |
539 self.advance() | 664 self.Advance() |
540 | 665 |
541 def hasNext(self): | 666 def HasNext(self): |
542 return self.nextToken is not None | 667 return self.next_token is not None |
543 | 668 |
544 def advance(self): | 669 def Advance(self): |
545 if self.nextContents == '\n': | 670 if self._cursor > 0 and self._string[self._cursor - 1] == '\n': |
546 self.nextLine = Line(self.nextLine.number + 1) | 671 self.next_line = _Line(self.next_line.number + 1) |
547 | 672 self.next_column = 0 |
548 self.nextToken = None | 673 elif self.next_token is not None: |
549 self.nextContents = None | 674 self.next_column += len(self.next_token.text) |
550 | 675 |
551 if self._remainder == '': | 676 self.next_token = None |
| 677 |
| 678 if self._cursor == len(self._string): |
552 return None | 679 return None |
553 | 680 assert self._cursor < len(self._string) |
554 for token in TokenValues: | 681 |
555 if self._remainder.startswith(token.text): | 682 if (self._cursor + 1 < len(self._string) and |
556 self.nextToken = token | 683 self._string[self._cursor + 1] in '{}'): |
| 684 self.next_token = ( |
| 685 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or |
| 686 _TOKENS.get(self._string[self._cursor:self._cursor+2])) |
| 687 |
| 688 if self.next_token is None: |
| 689 self.next_token = _Token.CHARACTER |
| 690 |
| 691 self._cursor += len(self.next_token.text) |
| 692 return self |
| 693 |
| 694 def AdvanceOver(self, token): |
| 695 if self.next_token != token: |
| 696 raise ParseException( |
| 697 'Expecting token %s but got %s at line %s' % (token.name, |
| 698 self.next_token.name, |
| 699 self.next_line)) |
| 700 return self.Advance() |
| 701 |
| 702 def AdvanceOverNextString(self, excluded=''): |
| 703 start = self._cursor - len(self.next_token.text) |
| 704 while (self.next_token is _Token.CHARACTER and |
| 705 # Can use -1 here because token length of CHARACTER is 1. |
| 706 self._string[self._cursor - 1] not in excluded): |
| 707 self.Advance() |
| 708 end = self._cursor - (len(self.next_token.text) if self.next_token else 0) |
| 709 return self._string[start:end] |
| 710 |
| 711 def AdvanceToNextWhitespace(self): |
| 712 return self.AdvanceOverNextString(excluded=' \n\r\t') |
| 713 |
| 714 def SkipWhitespace(self): |
| 715 while (self.next_token is _Token.CHARACTER and |
| 716 # Can use -1 here because token length of CHARACTER is 1. |
| 717 self._string[self._cursor - 1] in ' \n\r\t'): |
| 718 self.Advance() |
| 719 |
| 720 class Handlebar(object): |
| 721 ''' A handlebar template. |
| 722 ''' |
| 723 def __init__(self, template, name=None): |
| 724 self.source = template |
| 725 self._name = name |
| 726 tokens = _TokenStream(template) |
| 727 self._top_node = self._ParseSection(tokens) |
| 728 if not self._top_node: |
| 729 raise ParseException('Template is empty') |
| 730 if tokens.HasNext(): |
| 731 raise ParseException('There are still tokens remaining at %s, ' |
| 732 'was there an end-section without a start-section?' |
| 733 % tokens.next_line) |
| 734 |
| 735 def _ParseSection(self, tokens): |
| 736 nodes = [] |
| 737 while tokens.HasNext(): |
| 738 if tokens.next_token in (_Token.OPEN_END_SECTION, |
| 739 _Token.OPEN_ELSE): |
| 740 # Handled after running parseSection within the SECTION cases, so this |
| 741 # is a terminating condition. If there *is* an orphaned |
| 742 # OPEN_END_SECTION, it will be caught by noticing that there are |
| 743 # leftover tokens after termination. |
557 break | 744 break |
558 | 745 elif tokens.next_token in (_Token.CLOSE_MUSTACHE, |
559 if not self.nextToken: | 746 _Token.CLOSE_MUSTACHE3): |
560 self.nextToken = Token.CHARACTER | 747 raise ParseException('Orphaned %s at line %s' % (tokens.next_token.name, |
561 | 748 tokens.next_line)) |
562 self.nextContents = self._remainder[0:len(self.nextToken.text)] | 749 nodes += self._ParseNextOpenToken(tokens) |
563 self._remainder = self._remainder[len(self.nextToken.text):] | |
564 return self | |
565 | |
566 def advanceOver(self, token): | |
567 if self.nextToken != token: | |
568 raise ParseException( | |
569 "Expecting token " + token.name + " but got " + self.nextToken.name, | |
570 self.nextLine) | |
571 return self.advance() | |
572 | |
573 def advanceOverNextString(self, excluded=''): | |
574 buf = StringBuilder() | |
575 while self.nextToken == Token.CHARACTER and \ | |
576 excluded.find(self.nextContents) == -1: | |
577 buf.append(self.nextContents) | |
578 self.advance() | |
579 return buf.toString() | |
580 | |
581 def advanceToNextWhitespace(self): | |
582 return self.advanceOverNextString(excluded=' \n\r\t') | |
583 | |
584 def skipWhitespace(self): | |
585 while len(self.nextContents) > 0 and \ | |
586 ' \n\r\t'.find(self.nextContents) >= 0: | |
587 self.advance() | |
588 | |
589 class Handlebar(object): | |
590 """ A handlebar template. | |
591 """ | |
592 def __init__(self, template): | |
593 self.source = template | |
594 tokens = TokenStream(template) | |
595 self._topNode = self._parseSection(tokens) | |
596 if not self._topNode: | |
597 raise ParseException("Template is empty", tokens.nextLine) | |
598 if tokens.hasNext(): | |
599 raise ParseException("There are still tokens remaining, " | |
600 "was there an end-section without a start-section:", | |
601 tokens.nextLine) | |
602 | |
603 def _parseSection(self, tokens): | |
604 nodes = [] | |
605 sectionEnded = False | |
606 | |
607 while tokens.hasNext() and not sectionEnded: | |
608 token = tokens.nextToken | |
609 | |
610 if token == Token.CHARACTER: | |
611 startLine = tokens.nextLine | |
612 string = tokens.advanceOverNextString() | |
613 nodes.append(StringNode(string, startLine, tokens.nextLine)) | |
614 elif token == Token.OPEN_VARIABLE or \ | |
615 token == Token.OPEN_UNESCAPED_VARIABLE or \ | |
616 token == Token.OPEN_START_JSON: | |
617 id = self._openSectionOrTag(tokens) | |
618 nodes.append(token.clazz(id, tokens.nextLine)) | |
619 elif token == Token.OPEN_START_PARTIAL: | |
620 tokens.advance() | |
621 id = Identifier(tokens.advanceToNextWhitespace(), | |
622 tokens.nextLine) | |
623 partialNode = PartialNode(id, tokens.nextLine) | |
624 | |
625 while tokens.nextToken == Token.CHARACTER: | |
626 tokens.skipWhitespace() | |
627 key = tokens.advanceOverNextString(excluded=':') | |
628 tokens.advance() | |
629 partialNode.addArgument( | |
630 key, | |
631 Identifier(tokens.advanceToNextWhitespace(), | |
632 tokens.nextLine)) | |
633 | |
634 tokens.advanceOver(Token.CLOSE_MUSTACHE) | |
635 nodes.append(partialNode) | |
636 elif token == Token.OPEN_START_SECTION: | |
637 id = self._openSectionOrTag(tokens) | |
638 section = self._parseSection(tokens) | |
639 self._closeSection(tokens, id) | |
640 if section: | |
641 nodes.append(SectionNode(id, section)) | |
642 elif token == Token.OPEN_START_VERTED_SECTION or \ | |
643 token == Token.OPEN_START_INVERTED_SECTION: | |
644 id = self._openSectionOrTag(tokens) | |
645 section = self._parseSection(tokens) | |
646 elseSection = None | |
647 if tokens.nextToken == Token.OPEN_ELSE: | |
648 self._openElse(tokens, id) | |
649 elseSection = self._parseSection(tokens) | |
650 self._closeSection(tokens, id) | |
651 if section: | |
652 nodes.append(token.clazz(id, section)) | |
653 if elseSection: | |
654 nodes.append(token.elseNodeClass()(id, elseSection)) | |
655 elif token == Token.OPEN_COMMENT: | |
656 self._advanceOverComment(tokens) | |
657 elif token == Token.OPEN_END_SECTION or \ | |
658 token == Token.OPEN_ELSE: | |
659 # Handled after running parseSection within the SECTION cases, so this i
s a | |
660 # terminating condition. If there *is* an orphaned OPEN_END_SECTION, it
will be caught | |
661 # by noticing that there are leftover tokens after termination. | |
662 sectionEnded = True | |
663 elif Token.CLOSE_MUSTACHE: | |
664 raise ParseException("Orphaned " + tokens.nextToken.name, | |
665 tokens.nextLine) | |
666 | 750 |
667 for i, node in enumerate(nodes): | 751 for i, node in enumerate(nodes): |
668 if isinstance(node, StringNode): | 752 if isinstance(node, _StringNode): |
669 continue | 753 continue |
670 | 754 |
671 previousNode = nodes[i - 1] if i > 0 else None | 755 previous_node = nodes[i - 1] if i > 0 else None |
672 nextNode = nodes[i + 1] if i < len(nodes) - 1 else None | 756 next_node = nodes[i + 1] if i < len(nodes) - 1 else None |
673 renderedNode = None | 757 rendered_node = None |
674 | 758 |
675 if node.getStartLine() != node.getEndLine(): | 759 if node.GetStartLine() != node.GetEndLine(): |
676 renderedNode = BlockNode(node) | 760 rendered_node = _BlockNode(node) |
677 if previousNode: | 761 if previous_node: |
678 previousNode.trimEndingSpaces() | 762 previous_node.TrimEndingSpaces() |
679 if nextNode: | 763 if next_node: |
680 nextNode.trimStartingNewLine() | 764 next_node.TrimStartingNewLine() |
681 elif isinstance(node, LeafNode) and \ | 765 elif (isinstance(node, _LeafNode) and |
682 (not previousNode or previousNode.endsWithEmptyLine()) and \ | 766 (not previous_node or previous_node.EndsWithEmptyLine()) and |
683 (not nextNode or nextNode.startsWithNewLine()): | 767 (not next_node or next_node.StartsWithNewLine())): |
684 indentation = 0 | 768 indentation = 0 |
685 if previousNode: | 769 if previous_node: |
686 indentation = previousNode.trimEndingSpaces() | 770 indentation = previous_node.TrimEndingSpaces() |
687 if nextNode: | 771 if next_node: |
688 nextNode.trimStartingNewLine() | 772 next_node.TrimStartingNewLine() |
689 renderedNode = IndentedNode(node, indentation) | 773 rendered_node = _IndentedNode(node, indentation) |
690 else: | 774 else: |
691 renderedNode = InlineNode(node) | 775 rendered_node = _InlineNode(node) |
692 | 776 |
693 nodes[i] = renderedNode | 777 nodes[i] = rendered_node |
694 | 778 |
695 if len(nodes) == 0: | 779 if len(nodes) == 0: |
696 return None | 780 return None |
697 if len(nodes) == 1: | 781 if len(nodes) == 1: |
698 return nodes[0] | 782 return nodes[0] |
699 return NodeCollection(nodes) | 783 return _NodeCollection(nodes) |
700 | 784 |
701 def _advanceOverComment(self, tokens): | 785 def _ParseNextOpenToken(self, tokens): |
702 tokens.advanceOver(Token.OPEN_COMMENT) | 786 next_token = tokens.next_token |
| 787 |
| 788 if next_token is _Token.CHARACTER: |
| 789 start_line = tokens.next_line |
| 790 string = tokens.AdvanceOverNextString() |
| 791 return [_StringNode(string, start_line, tokens.next_line)] |
| 792 elif next_token in (_Token.OPEN_VARIABLE, |
| 793 _Token.OPEN_UNESCAPED_VARIABLE, |
| 794 _Token.OPEN_START_JSON): |
| 795 id_, inline_value_id = self._OpenSectionOrTag(tokens) |
| 796 if inline_value_id is not None: |
| 797 raise ParseException( |
| 798 '%s cannot have an inline value' % id_.GetDescription()) |
| 799 return [next_token.clazz(id_)] |
| 800 elif next_token is _Token.OPEN_START_PARTIAL: |
| 801 tokens.Advance() |
| 802 column_start = tokens.next_column + 1 |
| 803 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), |
| 804 tokens.next_line, |
| 805 column_start) |
| 806 partial_node = _PartialNode(id_) |
| 807 while tokens.next_token is _Token.CHARACTER: |
| 808 tokens.SkipWhitespace() |
| 809 key = tokens.AdvanceOverNextString(excluded=':') |
| 810 tokens.Advance() |
| 811 column_start = tokens.next_column + 1 |
| 812 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), |
| 813 tokens.next_line, |
| 814 column_start) |
| 815 if key == '@': |
| 816 partial_node.SetLocalContext(id_) |
| 817 else: |
| 818 partial_node.AddArgument(key, id_) |
| 819 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 820 return [partial_node] |
| 821 elif next_token is _Token.OPEN_START_SECTION: |
| 822 id_, inline_node = self._OpenSectionOrTag(tokens) |
| 823 nodes = [] |
| 824 if inline_node is None: |
| 825 section = self._ParseSection(tokens) |
| 826 self._CloseSection(tokens, id_) |
| 827 nodes = [] |
| 828 if section is not None: |
| 829 nodes.append(_SectionNode(id_, section)) |
| 830 else: |
| 831 nodes.append(_SectionNode(id_, inline_node)) |
| 832 return nodes |
| 833 elif next_token in (_Token.OPEN_START_VERTED_SECTION, |
| 834 _Token.OPEN_START_INVERTED_SECTION): |
| 835 id_, inline_node = self._OpenSectionOrTag(tokens) |
| 836 nodes = [] |
| 837 if inline_node is None: |
| 838 section = self._ParseSection(tokens) |
| 839 else_section = None |
| 840 if tokens.next_token is _Token.OPEN_ELSE: |
| 841 self._OpenElse(tokens, id_) |
| 842 else_section = self._ParseSection(tokens) |
| 843 self._CloseSection(tokens, id_) |
| 844 if section: |
| 845 nodes.append(next_token.clazz(id_, section)) |
| 846 if else_section: |
| 847 nodes.append(next_token.ElseNodeClass()(id_, else_section)) |
| 848 else: |
| 849 nodes.append(next_token.clazz(id_, inline_node)) |
| 850 return nodes |
| 851 elif next_token is _Token.OPEN_COMMENT: |
| 852 start_line = tokens.next_line |
| 853 self._AdvanceOverComment(tokens) |
| 854 return [_CommentNode(start_line, tokens.next_line)] |
| 855 |
| 856 def _AdvanceOverComment(self, tokens): |
| 857 tokens.AdvanceOver(_Token.OPEN_COMMENT) |
703 depth = 1 | 858 depth = 1 |
704 while tokens.hasNext() and depth > 0: | 859 while tokens.HasNext() and depth > 0: |
705 if tokens.nextToken == Token.OPEN_COMMENT: | 860 if tokens.next_token is _Token.OPEN_COMMENT: |
706 depth += 1 | 861 depth += 1 |
707 elif tokens.nextToken == Token.CLOSE_COMMENT: | 862 elif tokens.next_token is _Token.CLOSE_COMMENT: |
708 depth -= 1 | 863 depth -= 1 |
709 tokens.advance() | 864 tokens.Advance() |
710 | 865 |
711 def _openSectionOrTag(self, tokens): | 866 def _OpenSectionOrTag(self, tokens): |
712 openToken = tokens.nextToken | 867 def NextIdentifierArgs(): |
713 tokens.advance() | 868 tokens.SkipWhitespace() |
714 id = Identifier(tokens.advanceOverNextString(), tokens.nextLine) | 869 line = tokens.next_line |
715 if openToken == Token.OPEN_UNESCAPED_VARIABLE: | 870 column = tokens.next_column + 1 |
716 tokens.advanceOver(Token.CLOSE_MUSTACHE3) | 871 name = tokens.AdvanceToNextWhitespace() |
| 872 tokens.SkipWhitespace() |
| 873 return (name, line, column) |
| 874 close_token = (_Token.CLOSE_MUSTACHE3 |
| 875 if tokens.next_token is _Token.OPEN_UNESCAPED_VARIABLE else |
| 876 _Token.CLOSE_MUSTACHE) |
| 877 tokens.Advance() |
| 878 id_ = _Identifier(*NextIdentifierArgs()) |
| 879 if tokens.next_token is close_token: |
| 880 tokens.AdvanceOver(close_token) |
| 881 inline_node = None |
717 else: | 882 else: |
718 tokens.advanceOver(Token.CLOSE_MUSTACHE) | 883 name, line, column = NextIdentifierArgs() |
719 return id | 884 tokens.AdvanceOver(_Token.INLINE_END_SECTION) |
720 | 885 # Support select other types of nodes, the most useful being partial. |
721 def _closeSection(self, tokens, id): | 886 clazz = _UnescapedVariableNode |
722 tokens.advanceOver(Token.OPEN_END_SECTION) | 887 if name.startswith('*'): |
723 nextString = tokens.advanceOverNextString() | 888 clazz = _JsonNode |
724 if nextString != '' and nextString != str(id): | 889 elif name.startswith('+'): |
| 890 clazz = _PartialNode |
| 891 if clazz is not _UnescapedVariableNode: |
| 892 name = name[1:] |
| 893 column += 1 |
| 894 inline_node = clazz(_Identifier(name, line, column)) |
| 895 return (id_, inline_node) |
| 896 |
| 897 def _CloseSection(self, tokens, id_): |
| 898 tokens.AdvanceOver(_Token.OPEN_END_SECTION) |
| 899 next_string = tokens.AdvanceOverNextString() |
| 900 if next_string != '' and next_string != id_.name: |
725 raise ParseException( | 901 raise ParseException( |
726 "Start section " + str(id) + " doesn't match end " + nextString) | 902 'Start section %s doesn\'t match end %s' % (id_, next_string)) |
727 tokens.advanceOver(Token.CLOSE_MUSTACHE) | 903 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
728 | 904 |
729 def _openElse(self, tokens, id): | 905 def _OpenElse(self, tokens, id_): |
730 tokens.advanceOver(Token.OPEN_ELSE) | 906 tokens.AdvanceOver(_Token.OPEN_ELSE) |
731 nextString = tokens.advanceOverNextString() | 907 next_string = tokens.AdvanceOverNextString() |
732 if nextString != '' and nextString != str(id): | 908 if next_string != '' and next_string != id_.name: |
733 raise ParseException( | 909 raise ParseException( |
734 "Start section " + str(id) + " doesn't match else " + nextString) | 910 'Start section %s doesn\'t match else %s' % (id_, next_string)) |
735 tokens.advanceOver(Token.CLOSE_MUSTACHE) | 911 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 912 |
| 913 def Render(self, *contexts): |
| 914 '''Renders this template given a variable number of contexts to read out |
| 915 values from (such as those appearing in {{foo}}). |
| 916 ''' |
| 917 name = self._name or '<root>' |
| 918 render_state = _RenderState(name, _Contexts(contexts)) |
| 919 self._top_node.Render(render_state) |
| 920 return render_state.GetResult() |
736 | 921 |
737 def render(self, *contexts): | 922 def render(self, *contexts): |
738 """ Renders this template given a variable number of "contexts" to read | 923 return self.Render(*contexts) |
739 out values from (such as those appearing in {{foo}}). | |
740 """ | |
741 globalContexts = [] | |
742 for context in contexts: | |
743 globalContexts.append(context) | |
744 renderState = RenderState(globalContexts, []) | |
745 self._topNode.render(renderState) | |
746 return renderState.getResult() | |
OLD | NEW |