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? | 15 # TODO: New name, not "handlebar". |
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. \{{, \{{-. | 16 # TODO: Escaping control characters somehow. e.g. \{{, \{{-. |
22 # TODO: Dump warnings-so-far into the output. | |
23 | 17 |
24 import json | 18 import json |
25 import re | 19 import re |
26 | 20 |
27 '''Handlebar templates are data binding templates more-than-loosely inspired by | 21 '''Handlebar templates are data binding templates more-than-loosely inspired by |
28 ctemplate. Use like: | 22 ctemplate. Use like: |
29 | 23 |
30 from handlebar import Handlebar | 24 from handlebar import Handlebar |
31 | 25 |
32 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') | 26 template = Handlebar('hello {{#foo bar/}} world') |
33 input = { | 27 input = { |
34 'foo': [ | 28 'foo': [ |
35 { 'bar': 1 }, | 29 { 'bar': 1 }, |
36 { 'bar': 2 }, | 30 { 'bar': 2 }, |
37 { 'bar': 3 } | 31 { 'bar': 3 } |
38 ] | 32 ] |
39 } | 33 } |
40 print(template.render(input).text) | 34 print(template.render(input).text) |
41 | 35 |
42 Handlebar will use get() on contexts to return values, so to create custom | 36 Handlebar will use get() on contexts to return values, so to create custom |
(...skipping 15 matching lines...) Expand all Loading... |
58 Exception.__init__(self, error) | 52 Exception.__init__(self, error) |
59 | 53 |
60 class RenderResult(object): | 54 class RenderResult(object): |
61 '''The result of a render operation. | 55 '''The result of a render operation. |
62 ''' | 56 ''' |
63 def __init__(self, text, errors): | 57 def __init__(self, text, errors): |
64 self.text = text; | 58 self.text = text; |
65 self.errors = errors | 59 self.errors = errors |
66 | 60 |
67 def __repr__(self): | 61 def __repr__(self): |
68 return '%s(text=%s, errors=%s)' % ( | 62 return '%s(text=%s, errors=%s)' % (type(self).__name__, |
69 self.__class__.__name__, self.text, self.errors) | 63 self.text, |
| 64 self.errors) |
70 | 65 |
71 def __str__(self): | 66 def __str__(self): |
72 return repr(self) | 67 return repr(self) |
73 | 68 |
74 class _StringBuilder(object): | 69 class _StringBuilder(object): |
75 '''Efficiently builds strings. | 70 '''Efficiently builds strings. |
76 ''' | 71 ''' |
77 def __init__(self): | 72 def __init__(self): |
78 self._buf = [] | 73 self._buf = [] |
79 | 74 |
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
153 def Push(self, context): | 148 def Push(self, context): |
154 self._nodes.append(_Contexts._Node(context)) | 149 self._nodes.append(_Contexts._Node(context)) |
155 | 150 |
156 def Pop(self): | 151 def Pop(self): |
157 node = self._nodes.pop() | 152 node = self._nodes.pop() |
158 assert len(self._nodes) >= self._first_local | 153 assert len(self._nodes) >= self._first_local |
159 for found_key in node.GetKeys(): | 154 for found_key in node.GetKeys(): |
160 # [0] is the stack of nodes that |found_key| has been found in. | 155 # [0] is the stack of nodes that |found_key| has been found in. |
161 self._value_info[found_key][0].pop() | 156 self._value_info[found_key][0].pop() |
162 | 157 |
163 def GetTopLocal(self): | |
164 if len(self._nodes) == self._first_local: | |
165 return None | |
166 return self._nodes[-1]._value | |
167 | |
168 def Resolve(self, path): | 158 def Resolve(self, path): |
169 # This method is only efficient at finding |key|; if |tail| has a value (and | 159 # This method is only efficient at finding |key|; if |tail| has a value (and |
170 # |key| evaluates to an indexable value) we'll need to descend into that. | 160 # |key| evaluates to an indexable value) we'll need to descend into that. |
171 key, tail = path.split('.', 1) if '.' in path else (path, None) | 161 key, tail = path.split('.', 1) if '.' in path else (path, None) |
172 | 162 found = self._FindNodeValue(key) |
173 if key == '@': | |
174 found = self._nodes[-1]._value | |
175 else: | |
176 found = self._FindNodeValue(key) | |
177 | |
178 if tail is None: | 163 if tail is None: |
179 return found | 164 return found |
180 | |
181 for part in tail.split('.'): | 165 for part in tail.split('.'): |
182 if not hasattr(found, 'get'): | 166 if not hasattr(found, 'get'): |
183 return None | 167 return None |
184 found = found.get(part) | 168 found = found.get(part) |
185 return found | 169 return found |
186 | 170 |
| 171 def Scope(self, context, fn, *args): |
| 172 self.Push(context) |
| 173 try: |
| 174 return fn(*args) |
| 175 finally: |
| 176 self.Pop() |
| 177 |
187 def _FindNodeValue(self, key): | 178 def _FindNodeValue(self, key): |
188 # |found_node_list| will be all the nodes that |key| has been found in. | 179 # |found_node_list| will be all the nodes that |key| has been found in. |
189 # |checked_node_set| are those that have been checked. | 180 # |checked_node_set| are those that have been checked. |
190 info = self._value_info.get(key) | 181 info = self._value_info.get(key) |
191 if info is None: | 182 if info is None: |
192 info = ([], set()) | 183 info = ([], set()) |
193 self._value_info[key] = info | 184 self._value_info[key] = info |
194 found_node_list, checked_node_set = info | 185 found_node_list, checked_node_set = info |
195 | 186 |
196 # Check all the nodes not yet checked for |key|. | 187 # Check all the nodes not yet checked for |key|. |
(...skipping 21 matching lines...) Expand all Loading... |
218 self.id_ = id_ | 209 self.id_ = id_ |
219 | 210 |
220 def __init__(self, entries=[]): | 211 def __init__(self, entries=[]): |
221 self.entries = entries | 212 self.entries = entries |
222 | 213 |
223 def Descend(self, name, id_): | 214 def Descend(self, name, id_): |
224 descended = list(self.entries) | 215 descended = list(self.entries) |
225 descended.append(_Stack.Entry(name, id_)) | 216 descended.append(_Stack.Entry(name, id_)) |
226 return _Stack(entries=descended) | 217 return _Stack(entries=descended) |
227 | 218 |
| 219 class _InternalContext(object): |
| 220 def __init__(self): |
| 221 self._render_state = None |
| 222 |
| 223 def SetRenderState(self, render_state): |
| 224 self._render_state = render_state |
| 225 |
| 226 def get(self, key): |
| 227 if key == 'errors': |
| 228 errors = self._render_state._errors |
| 229 return '\n'.join(errors) if errors else None |
| 230 return None |
| 231 |
228 class _RenderState(object): | 232 class _RenderState(object): |
229 '''The state of a render call. | 233 '''The state of a render call. |
230 ''' | 234 ''' |
231 def __init__(self, name, contexts, _stack=_Stack()): | 235 def __init__(self, name, contexts, _stack=_Stack()): |
232 self.text = _StringBuilder() | 236 self.text = _StringBuilder() |
233 self.contexts = contexts | 237 self.contexts = contexts |
234 self._name = name | 238 self._name = name |
235 self._errors = [] | 239 self._errors = [] |
236 self._stack = _stack | 240 self._stack = _stack |
237 | 241 |
238 def AddResolutionError(self, id_): | 242 def AddResolutionError(self, id_, description=None): |
239 self._errors.append( | 243 message = id_.CreateResolutionErrorMessage(self._name, stack=self._stack) |
240 id_.CreateResolutionErrorMessage(self._name, stack=self._stack)) | 244 if description is not None: |
| 245 message = '%s (%s)' % (message, description) |
| 246 self._errors.append(message) |
241 | 247 |
242 def Copy(self): | 248 def Copy(self): |
243 return _RenderState( | 249 return _RenderState( |
244 self._name, self.contexts, _stack=self._stack) | 250 self._name, self.contexts, _stack=self._stack) |
245 | 251 |
246 def ForkPartial(self, custom_name, id_): | 252 def ForkPartial(self, custom_name, id_): |
247 name = custom_name or id_.name | 253 name = custom_name or id_.name |
248 return _RenderState(name, | 254 return _RenderState(name, |
249 self.contexts.CreateFromGlobals(), | 255 self.contexts.CreateFromGlobals(), |
250 _stack=self._stack.Descend(name, id_)) | 256 _stack=self._stack.Descend(name, id_)) |
251 | 257 |
252 def Merge(self, render_state, text_transform=None): | 258 def Merge(self, render_state, text_transform=None): |
253 self._errors.extend(render_state._errors) | 259 self._errors.extend(render_state._errors) |
254 text = render_state.text.ToString() | 260 text = render_state.text.ToString() |
255 if text_transform is not None: | 261 if text_transform is not None: |
256 text = text_transform(text) | 262 text = text_transform(text) |
257 self.text.Append(text) | 263 self.text.Append(text) |
258 | 264 |
259 def GetResult(self): | 265 def GetResult(self): |
260 return RenderResult(self.text.ToString(), self._errors); | 266 return RenderResult(self.text.ToString(), self._errors); |
261 | 267 |
262 class _Identifier(object): | 268 class _Identifier(object): |
263 ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'. | 269 '''An identifier of the form 'foo', 'foo.bar.baz', 'foo-bar.baz', etc. |
264 ''' | 270 ''' |
| 271 _VALID_ID_MATCHER = re.compile(r'^[a-zA-Z0-9@_/-]+$') |
| 272 |
265 def __init__(self, name, line, column): | 273 def __init__(self, name, line, column): |
266 self.name = name | 274 self.name = name |
267 self.line = line | 275 self.line = line |
268 self.column = column | 276 self.column = column |
269 if name == '': | 277 if name == '': |
270 raise ParseException('Empty identifier %s' % self.GetDescription()) | 278 raise ParseException('Empty identifier %s' % self.GetDescription()) |
271 for part in name.split('.'): | 279 for part in name.split('.'): |
272 if part != '@' and not re.match('^[a-zA-Z0-9_/-]+$', part): | 280 if not _Identifier._VALID_ID_MATCHER.match(part): |
273 raise ParseException('Invalid identifier %s' % self.GetDescription()) | 281 raise ParseException('Invalid identifier %s' % self.GetDescription()) |
274 | 282 |
275 def GetDescription(self): | 283 def GetDescription(self): |
276 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) | 284 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) |
277 | 285 |
278 def CreateResolutionErrorMessage(self, name, stack=None): | 286 def CreateResolutionErrorMessage(self, name, stack=None): |
279 message = _StringBuilder() | 287 message = _StringBuilder() |
280 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), | 288 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), |
281 name)) | 289 name)) |
282 if stack is not None: | 290 if stack is not None: |
283 for entry in stack.entries: | 291 for entry in reversed(stack.entries): |
284 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), | 292 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), |
285 entry.name)) | 293 entry.name)) |
286 return message.ToString() | 294 return message.ToString().strip() |
287 | 295 |
288 def __repr__(self): | 296 def __repr__(self): |
289 return self.name | 297 return self.name |
290 | 298 |
291 def __str__(self): | 299 def __str__(self): |
292 return repr(self) | 300 return repr(self) |
293 | 301 |
294 class _Line(object): | 302 class _Node(object): pass |
295 def __init__(self, number): | |
296 self.number = number | |
297 | 303 |
298 def __repr__(self): | 304 class _LeafNode(_Node): |
299 return str(self.number) | |
300 | |
301 def __str__(self): | |
302 return repr(self) | |
303 | |
304 class _LeafNode(object): | |
305 def __init__(self, start_line, end_line): | 305 def __init__(self, start_line, end_line): |
306 self._start_line = start_line | 306 self._start_line = start_line |
307 self._end_line = end_line | 307 self._end_line = end_line |
308 | 308 |
309 def StartsWithNewLine(self): | 309 def StartsWithNewLine(self): |
310 return False | 310 return False |
311 | 311 |
312 def TrimStartingNewLine(self): | 312 def TrimStartingNewLine(self): |
313 pass | 313 pass |
314 | 314 |
315 def TrimEndingSpaces(self): | 315 def TrimEndingSpaces(self): |
316 return 0 | 316 return 0 |
317 | 317 |
318 def TrimEndingNewLine(self): | 318 def TrimEndingNewLine(self): |
319 pass | 319 pass |
320 | 320 |
321 def EndsWithEmptyLine(self): | 321 def EndsWithEmptyLine(self): |
322 return False | 322 return False |
323 | 323 |
324 def GetStartLine(self): | 324 def GetStartLine(self): |
325 return self._start_line | 325 return self._start_line |
326 | 326 |
327 def GetEndLine(self): | 327 def GetEndLine(self): |
328 return self._end_line | 328 return self._end_line |
329 | 329 |
330 class _DecoratorNode(object): | 330 def __str__(self): |
| 331 return repr(self) |
| 332 |
| 333 class _DecoratorNode(_Node): |
331 def __init__(self, content): | 334 def __init__(self, content): |
332 self._content = content | 335 self._content = content |
333 | 336 |
334 def StartsWithNewLine(self): | 337 def StartsWithNewLine(self): |
335 return self._content.StartsWithNewLine() | 338 return self._content.StartsWithNewLine() |
336 | 339 |
337 def TrimStartingNewLine(self): | 340 def TrimStartingNewLine(self): |
338 self._content.TrimStartingNewLine() | 341 self._content.TrimStartingNewLine() |
339 | 342 |
340 def TrimEndingSpaces(self): | 343 def TrimEndingSpaces(self): |
341 return self._content.TrimEndingSpaces() | 344 return self._content.TrimEndingSpaces() |
342 | 345 |
343 def TrimEndingNewLine(self): | 346 def TrimEndingNewLine(self): |
344 self._content.TrimEndingNewLine() | 347 self._content.TrimEndingNewLine() |
345 | 348 |
346 def EndsWithEmptyLine(self): | 349 def EndsWithEmptyLine(self): |
347 return self._content.EndsWithEmptyLine() | 350 return self._content.EndsWithEmptyLine() |
348 | 351 |
349 def GetStartLine(self): | 352 def GetStartLine(self): |
350 return self._content.GetStartLine() | 353 return self._content.GetStartLine() |
351 | 354 |
352 def GetEndLine(self): | 355 def GetEndLine(self): |
353 return self._content.GetEndLine() | 356 return self._content.GetEndLine() |
354 | 357 |
355 def __repr__(self): | 358 def __repr__(self): |
356 return str(self._content) | 359 return str(self._content) |
357 | 360 |
358 def __str__(self): | 361 def __str__(self): |
359 return repr(self) | 362 return repr(self) |
360 | 363 |
361 class _InlineNode(_DecoratorNode): | 364 class _InlineNode(_DecoratorNode): |
362 def __init__(self, content): | 365 def __init__(self, content): |
363 _DecoratorNode.__init__(self, content) | 366 _DecoratorNode.__init__(self, content) |
364 | 367 |
365 def Render(self, render_state): | 368 def Render(self, render_state): |
366 content_render_state = render_state.Copy() | 369 content_render_state = render_state.Copy() |
367 self._content.Render(content_render_state) | 370 self._content.Render(content_render_state) |
368 render_state.Merge(content_render_state, | 371 render_state.Merge(content_render_state, |
369 text_transform=lambda text: text.replace('\n', '')) | 372 text_transform=lambda text: text.replace('\n', '')) |
370 | 373 |
371 class _IndentedNode(_DecoratorNode): | 374 class _IndentedNode(_DecoratorNode): |
372 def __init__(self, content, indentation): | 375 def __init__(self, content, indentation): |
373 _DecoratorNode.__init__(self, content) | 376 _DecoratorNode.__init__(self, content) |
374 self._indent_str = ' ' * indentation | 377 self._indent_str = ' ' * indentation |
375 | 378 |
376 def Render(self, render_state): | 379 def Render(self, render_state): |
377 if isinstance(self._content, _CommentNode): | 380 if isinstance(self._content, _CommentNode): |
378 return | 381 return |
379 content_render_state = render_state.Copy() | 382 def inlinify(text): |
380 self._content.Render(content_render_state) | 383 if len(text) == 0: # avoid rendering a blank line |
381 def AddIndentation(text): | 384 return '' |
382 buf = _StringBuilder() | 385 buf = _StringBuilder() |
383 buf.Append(self._indent_str) | 386 buf.Append(self._indent_str) |
384 buf.Append(text.replace('\n', '\n%s' % self._indent_str)) | 387 buf.Append(text.replace('\n', '\n%s' % self._indent_str)) |
385 buf.Append('\n') | 388 if not text.endswith('\n'): # partials will often already end in a \n |
| 389 buf.Append('\n') |
386 return buf.ToString() | 390 return buf.ToString() |
387 render_state.Merge(content_render_state, text_transform=AddIndentation) | 391 content_render_state = render_state.Copy() |
| 392 self._content.Render(content_render_state) |
| 393 render_state.Merge(content_render_state, text_transform=inlinify) |
388 | 394 |
389 class _BlockNode(_DecoratorNode): | 395 class _BlockNode(_DecoratorNode): |
390 def __init__(self, content): | 396 def __init__(self, content): |
391 _DecoratorNode.__init__(self, content) | 397 _DecoratorNode.__init__(self, content) |
392 content.TrimStartingNewLine() | 398 content.TrimStartingNewLine() |
393 content.TrimEndingSpaces() | 399 content.TrimEndingSpaces() |
394 | 400 |
395 def Render(self, render_state): | 401 def Render(self, render_state): |
396 self._content.Render(render_state) | 402 self._content.Render(render_state) |
397 | 403 |
398 class _NodeCollection(object): | 404 class _NodeCollection(_Node): |
399 def __init__(self, nodes): | 405 def __init__(self, nodes): |
400 assert nodes | 406 assert nodes |
401 self._nodes = nodes | 407 self._nodes = nodes |
402 | 408 |
403 def Render(self, render_state): | 409 def Render(self, render_state): |
404 for node in self._nodes: | 410 for node in self._nodes: |
405 node.Render(render_state) | 411 node.Render(render_state) |
406 | 412 |
407 def StartsWithNewLine(self): | 413 def StartsWithNewLine(self): |
408 return self._nodes[0].StartsWithNewLine() | 414 return self._nodes[0].StartsWithNewLine() |
(...skipping 12 matching lines...) Expand all Loading... |
421 | 427 |
422 def GetStartLine(self): | 428 def GetStartLine(self): |
423 return self._nodes[0].GetStartLine() | 429 return self._nodes[0].GetStartLine() |
424 | 430 |
425 def GetEndLine(self): | 431 def GetEndLine(self): |
426 return self._nodes[-1].GetEndLine() | 432 return self._nodes[-1].GetEndLine() |
427 | 433 |
428 def __repr__(self): | 434 def __repr__(self): |
429 return ''.join(str(node) for node in self._nodes) | 435 return ''.join(str(node) for node in self._nodes) |
430 | 436 |
431 def __str__(self): | 437 class _StringNode(_Node): |
432 return repr(self) | 438 '''Just a string. |
433 | |
434 class _StringNode(object): | |
435 ''' Just a string. | |
436 ''' | 439 ''' |
437 def __init__(self, string, start_line, end_line): | 440 def __init__(self, string, start_line, end_line): |
438 self._string = string | 441 self._string = string |
439 self._start_line = start_line | 442 self._start_line = start_line |
440 self._end_line = end_line | 443 self._end_line = end_line |
441 | 444 |
442 def Render(self, render_state): | 445 def Render(self, render_state): |
443 render_state.text.Append(self._string) | 446 render_state.text.Append(self._string) |
444 | 447 |
445 def StartsWithNewLine(self): | 448 def StartsWithNewLine(self): |
(...skipping 24 matching lines...) Expand all Loading... |
470 | 473 |
471 def GetStartLine(self): | 474 def GetStartLine(self): |
472 return self._start_line | 475 return self._start_line |
473 | 476 |
474 def GetEndLine(self): | 477 def GetEndLine(self): |
475 return self._end_line | 478 return self._end_line |
476 | 479 |
477 def __repr__(self): | 480 def __repr__(self): |
478 return self._string | 481 return self._string |
479 | 482 |
480 def __str__(self): | |
481 return repr(self) | |
482 | |
483 class _EscapedVariableNode(_LeafNode): | 483 class _EscapedVariableNode(_LeafNode): |
484 ''' {{foo}} | 484 '''{{foo}} |
485 ''' | 485 ''' |
486 def __init__(self, id_): | 486 def __init__(self, id_): |
487 _LeafNode.__init__(self, id_.line, id_.line) | 487 _LeafNode.__init__(self, id_.line, id_.line) |
488 self._id = id_ | 488 self._id = id_ |
489 | 489 |
490 def Render(self, render_state): | 490 def Render(self, render_state): |
491 value = render_state.contexts.Resolve(self._id.name) | 491 value = render_state.contexts.Resolve(self._id.name) |
492 if value is None: | 492 if value is None: |
493 render_state.AddResolutionError(self._id) | 493 render_state.AddResolutionError(self._id) |
494 return | 494 return |
495 string = value if isinstance(value, basestring) else str(value) | 495 string = value if isinstance(value, basestring) else str(value) |
496 render_state.text.Append(string.replace('&', '&') | 496 render_state.text.Append(string.replace('&', '&') |
497 .replace('<', '<') | 497 .replace('<', '<') |
498 .replace('>', '>')) | 498 .replace('>', '>')) |
499 | 499 |
500 def __repr__(self): | 500 def __repr__(self): |
501 return '{{%s}}' % self._id | 501 return '{{%s}}' % self._id |
502 | 502 |
503 def __str__(self): | |
504 return repr(self) | |
505 | |
506 class _UnescapedVariableNode(_LeafNode): | 503 class _UnescapedVariableNode(_LeafNode): |
507 ''' {{{foo}}} | 504 '''{{{foo}}} |
508 ''' | 505 ''' |
509 def __init__(self, id_): | 506 def __init__(self, id_): |
510 _LeafNode.__init__(self, id_.line, id_.line) | 507 _LeafNode.__init__(self, id_.line, id_.line) |
511 self._id = id_ | 508 self._id = id_ |
512 | 509 |
513 def Render(self, render_state): | 510 def Render(self, render_state): |
514 value = render_state.contexts.Resolve(self._id.name) | 511 value = render_state.contexts.Resolve(self._id.name) |
515 if value is None: | 512 if value is None: |
516 render_state.AddResolutionError(self._id) | 513 render_state.AddResolutionError(self._id) |
517 return | 514 return |
518 string = value if isinstance(value, basestring) else str(value) | 515 string = value if isinstance(value, basestring) else str(value) |
519 render_state.text.Append(string) | 516 render_state.text.Append(string) |
520 | 517 |
521 def __repr__(self): | 518 def __repr__(self): |
522 return '{{{%s}}}' % self._id | 519 return '{{{%s}}}' % self._id |
523 | 520 |
524 def __str__(self): | |
525 return repr(self) | |
526 | |
527 class _CommentNode(_LeafNode): | 521 class _CommentNode(_LeafNode): |
528 '''{{- This is a comment -}} | 522 '''{{- This is a comment -}} |
529 An empty placeholder node for correct indented rendering behaviour. | 523 An empty placeholder node for correct indented rendering behaviour. |
530 ''' | 524 ''' |
531 def __init__(self, start_line, end_line): | 525 def __init__(self, start_line, end_line): |
532 _LeafNode.__init__(self, start_line, end_line) | 526 _LeafNode.__init__(self, start_line, end_line) |
533 | 527 |
534 def Render(self, render_state): | 528 def Render(self, render_state): |
535 pass | 529 pass |
536 | 530 |
537 def __repr__(self): | 531 def __repr__(self): |
538 return '<comment>' | 532 return '<comment>' |
539 | 533 |
540 def __str__(self): | |
541 return repr(self) | |
542 | |
543 class _SectionNode(_DecoratorNode): | 534 class _SectionNode(_DecoratorNode): |
544 ''' {{#foo}} ... {{/}} | 535 '''{{#var:foo}} ... {{/foo}} |
545 ''' | 536 ''' |
546 def __init__(self, id_, content): | 537 def __init__(self, bind_to, id_, content): |
547 _DecoratorNode.__init__(self, content) | 538 _DecoratorNode.__init__(self, content) |
| 539 self._bind_to = bind_to |
548 self._id = id_ | 540 self._id = id_ |
549 | 541 |
550 def Render(self, render_state): | 542 def Render(self, render_state): |
551 value = render_state.contexts.Resolve(self._id.name) | 543 value = render_state.contexts.Resolve(self._id.name) |
552 if isinstance(value, list): | 544 if isinstance(value, list): |
553 for item in value: | 545 for item in value: |
554 # Always push context, even if it's not "valid", since we want to | 546 if self._bind_to is not None: |
555 # be able to refer to items in a list such as [1,2,3] via @. | 547 render_state.contexts.Scope({self._bind_to.name: item}, |
556 render_state.contexts.Push(item) | 548 self._content.Render, render_state) |
557 self._content.Render(render_state) | 549 else: |
558 render_state.contexts.Pop() | 550 self._content.Render(render_state) |
559 elif hasattr(value, 'get'): | 551 elif hasattr(value, 'get'): |
560 render_state.contexts.Push(value) | 552 if self._bind_to is not None: |
561 self._content.Render(render_state) | 553 render_state.contexts.Scope({self._bind_to.name: value}, |
562 render_state.contexts.Pop() | 554 self._content.Render, render_state) |
| 555 else: |
| 556 render_state.contexts.Scope(value, self._content.Render, render_state) |
563 else: | 557 else: |
564 render_state.AddResolutionError(self._id) | 558 render_state.AddResolutionError(self._id) |
565 | 559 |
566 def __repr__(self): | 560 def __repr__(self): |
567 return '{{#%s}}%s{{/%s}}' % ( | 561 return '{{#%s}}%s{{/%s}}' % ( |
568 self._id, _DecoratorNode.__repr__(self), self._id) | 562 self._id, _DecoratorNode.__repr__(self), self._id) |
569 | 563 |
570 def __str__(self): | |
571 return repr(self) | |
572 | |
573 class _VertedSectionNode(_DecoratorNode): | 564 class _VertedSectionNode(_DecoratorNode): |
574 ''' {{?foo}} ... {{/}} | 565 '''{{?var:foo}} ... {{/foo}} |
575 ''' | 566 ''' |
576 def __init__(self, id_, content): | 567 def __init__(self, bind_to, id_, content): |
577 _DecoratorNode.__init__(self, content) | 568 _DecoratorNode.__init__(self, content) |
| 569 self._bind_to = bind_to |
578 self._id = id_ | 570 self._id = id_ |
579 | 571 |
580 def Render(self, render_state): | 572 def Render(self, render_state): |
581 value = render_state.contexts.Resolve(self._id.name) | 573 value = render_state.contexts.Resolve(self._id.name) |
582 if _VertedSectionNode.ShouldRender(value): | 574 if _VertedSectionNode.ShouldRender(value): |
583 render_state.contexts.Push(value) | 575 if self._bind_to is not None: |
584 self._content.Render(render_state) | 576 render_state.contexts.Scope({self._bind_to.name: value}, |
585 render_state.contexts.Pop() | 577 self._content.Render, render_state) |
| 578 else: |
| 579 self._content.Render(render_state) |
586 | 580 |
587 def __repr__(self): | 581 def __repr__(self): |
588 return '{{?%s}}%s{{/%s}}' % ( | 582 return '{{?%s}}%s{{/%s}}' % ( |
589 self._id, _DecoratorNode.__repr__(self), self._id) | 583 self._id, _DecoratorNode.__repr__(self), self._id) |
590 | 584 |
591 def __str__(self): | |
592 return repr(self) | |
593 | |
594 @staticmethod | 585 @staticmethod |
595 def ShouldRender(value): | 586 def ShouldRender(value): |
596 if value is None: | 587 if value is None: |
597 return False | 588 return False |
598 if isinstance(value, bool): | 589 if isinstance(value, bool): |
599 return value | 590 return value |
600 if isinstance(value, list): | 591 if isinstance(value, list): |
601 return len(value) > 0 | 592 return len(value) > 0 |
602 return True | 593 return True |
603 | 594 |
604 class _InvertedSectionNode(_DecoratorNode): | 595 class _InvertedSectionNode(_DecoratorNode): |
605 ''' {{^foo}} ... {{/}} | 596 '''{{^foo}} ... {{/foo}} |
606 ''' | 597 ''' |
607 def __init__(self, id_, content): | 598 def __init__(self, bind_to, id_, content): |
608 _DecoratorNode.__init__(self, content) | 599 _DecoratorNode.__init__(self, content) |
| 600 if bind_to is not None: |
| 601 raise ParseException('{{^%s:%s}} does not support variable binding' |
| 602 % (bind_to, id_)) |
609 self._id = id_ | 603 self._id = id_ |
610 | 604 |
611 def Render(self, render_state): | 605 def Render(self, render_state): |
612 value = render_state.contexts.Resolve(self._id.name) | 606 value = render_state.contexts.Resolve(self._id.name) |
613 if not _VertedSectionNode.ShouldRender(value): | 607 if not _VertedSectionNode.ShouldRender(value): |
614 self._content.Render(render_state) | 608 self._content.Render(render_state) |
615 | 609 |
616 def __repr__(self): | 610 def __repr__(self): |
617 return '{{^%s}}%s{{/%s}}' % ( | 611 return '{{^%s}}%s{{/%s}}' % ( |
618 self._id, _DecoratorNode.__repr__(self), self._id) | 612 self._id, _DecoratorNode.__repr__(self), self._id) |
619 | 613 |
620 def __str__(self): | 614 class _AssertionNode(_LeafNode): |
621 return repr(self) | 615 '''{{!foo Some comment about foo}} |
| 616 ''' |
| 617 def __init__(self, id_, description): |
| 618 _LeafNode.__init__(self, id_.line, id_.line) |
| 619 self._id = id_ |
| 620 self._description = description |
| 621 |
| 622 def Render(self, render_state): |
| 623 if render_state.contexts.Resolve(self._id.name) is None: |
| 624 render_state.AddResolutionError(self._id, description=self._description) |
| 625 |
| 626 def __repr__(self): |
| 627 return '{{!%s %s}}' % (self._id, self._description) |
622 | 628 |
623 class _JsonNode(_LeafNode): | 629 class _JsonNode(_LeafNode): |
624 ''' {{*foo}} | 630 '''{{*foo}} |
625 ''' | 631 ''' |
626 def __init__(self, id_): | 632 def __init__(self, id_): |
627 _LeafNode.__init__(self, id_.line, id_.line) | 633 _LeafNode.__init__(self, id_.line, id_.line) |
628 self._id = id_ | 634 self._id = id_ |
629 | 635 |
630 def Render(self, render_state): | 636 def Render(self, render_state): |
631 value = render_state.contexts.Resolve(self._id.name) | 637 value = render_state.contexts.Resolve(self._id.name) |
632 if value is None: | 638 if value is None: |
633 render_state.AddResolutionError(self._id) | 639 render_state.AddResolutionError(self._id) |
634 return | 640 return |
635 render_state.text.Append(json.dumps(value, separators=(',',':'))) | 641 render_state.text.Append(json.dumps(value, separators=(',',':'))) |
636 | 642 |
637 def __repr__(self): | 643 def __repr__(self): |
638 return '{{*%s}}' % self._id | 644 return '{{*%s}}' % self._id |
639 | 645 |
640 def __str__(self): | 646 class _PartialNode(_LeafNode): |
641 return repr(self) | 647 '''{{+var:foo}} ... {{/foo}} |
| 648 ''' |
| 649 def __init__(self, bind_to, id_, content): |
| 650 _LeafNode.__init__(self, id_.line, id_.line) |
| 651 self._bind_to = bind_to |
| 652 self._id = id_ |
| 653 self._content = content |
| 654 self._resolved_args = None |
| 655 self._args = None |
| 656 self._pass_through_id = None |
642 | 657 |
643 class _PartialNode(_LeafNode): | 658 @classmethod |
644 ''' {{+foo}} | 659 def Inline(cls, id_): |
645 ''' | 660 return cls(None, id_, None) |
646 def __init__(self, id_): | |
647 _LeafNode.__init__(self, id_.line, id_.line) | |
648 self._id = id_ | |
649 self._args = None | |
650 self._local_context_id = None | |
651 | 661 |
652 def Render(self, render_state): | 662 def Render(self, render_state): |
653 value = render_state.contexts.Resolve(self._id.name) | 663 value = render_state.contexts.Resolve(self._id.name) |
654 if value is None: | 664 if value is None: |
655 render_state.AddResolutionError(self._id) | 665 render_state.AddResolutionError(self._id) |
656 return | 666 return |
657 if not isinstance(value, Handlebar): | 667 if not isinstance(value, (Handlebar, _Node)): |
658 render_state.AddResolutionError(self._id) | 668 render_state.AddResolutionError(self._id, description='not a partial') |
659 return | 669 return |
660 | 670 |
661 partial_render_state = render_state.ForkPartial(value._name, self._id) | 671 if isinstance(value, Handlebar): |
| 672 node, name = value._top_node, value._name |
| 673 else: |
| 674 node, name = value, None |
662 | 675 |
663 # TODO: Don't do this. Force callers to do this by specifying an @ argument. | 676 partial_render_state = render_state.ForkPartial(name, self._id) |
664 top_local = render_state.contexts.GetTopLocal() | |
665 if top_local is not None: | |
666 partial_render_state.contexts.Push(top_local) | |
667 | 677 |
| 678 arg_context = {} |
| 679 if self._pass_through_id is not None: |
| 680 context = render_state.contexts.Resolve(self._pass_through_id.name) |
| 681 if context is not None: |
| 682 arg_context[self._pass_through_id.name] = context |
| 683 if self._resolved_args is not None: |
| 684 arg_context.update(self._resolved_args) |
668 if self._args is not None: | 685 if self._args is not None: |
669 arg_context = {} | 686 def resolve_args(args): |
670 for key, value_id in self._args.items(): | 687 resolved = {} |
671 context = render_state.contexts.Resolve(value_id.name) | 688 for key, value in args.iteritems(): |
672 if context is not None: | 689 if isinstance(value, dict): |
673 arg_context[key] = context | 690 assert len(value.keys()) == 1 |
| 691 inner_id, inner_args = value.items()[0] |
| 692 inner_partial = render_state.contexts.Resolve(inner_id.name) |
| 693 if inner_partial is not None: |
| 694 context = _PartialNode(None, inner_id, inner_partial) |
| 695 context.SetResolvedArguments(resolve_args(inner_args)) |
| 696 resolved[key] = context |
| 697 else: |
| 698 context = render_state.contexts.Resolve(value.name) |
| 699 if context is not None: |
| 700 resolved[key] = context |
| 701 return resolved |
| 702 arg_context.update(resolve_args(self._args)) |
| 703 if self._bind_to and self._content: |
| 704 arg_context[self._bind_to.name] = self._content |
| 705 if arg_context: |
674 partial_render_state.contexts.Push(arg_context) | 706 partial_render_state.contexts.Push(arg_context) |
675 | 707 |
676 if self._local_context_id is not None: | 708 node.Render(partial_render_state) |
677 local_context = render_state.contexts.Resolve(self._local_context_id.name) | |
678 if local_context is not None: | |
679 partial_render_state.contexts.Push(local_context) | |
680 | |
681 value._top_node.Render(partial_render_state) | |
682 | 709 |
683 render_state.Merge( | 710 render_state.Merge( |
684 partial_render_state, | 711 partial_render_state, |
685 text_transform=lambda text: text[:-1] if text.endswith('\n') else text) | 712 text_transform=lambda text: text[:-1] if text.endswith('\n') else text) |
686 | 713 |
687 def AddArgument(self, key, id_): | 714 def SetResolvedArguments(self, args): |
688 if self._args is None: | 715 self._resolved_args = args |
689 self._args = {} | |
690 self._args[key] = id_ | |
691 | 716 |
692 def SetLocalContext(self, id_): | 717 def SetArguments(self, args): |
693 self._local_context_id = id_ | 718 self._args = args |
| 719 |
| 720 def PassThroughArgument(self, id_): |
| 721 self._pass_through_id = id_ |
694 | 722 |
695 def __repr__(self): | 723 def __repr__(self): |
696 return '{{+%s}}' % self._id | 724 return '{{+%s}}' % self._id |
697 | 725 |
698 def __str__(self): | |
699 return repr(self) | |
700 | |
701 _TOKENS = {} | 726 _TOKENS = {} |
702 | 727 |
703 class _Token(object): | 728 class _Token(object): |
704 ''' The tokens that can appear in a template. | 729 '''The tokens that can appear in a template. |
705 ''' | 730 ''' |
706 class Data(object): | 731 class Data(object): |
707 def __init__(self, name, text, clazz): | 732 def __init__(self, name, text, clazz): |
708 self.name = name | 733 self.name = name |
709 self.text = text | 734 self.text = text |
710 self.clazz = clazz | 735 self.clazz = clazz |
711 _TOKENS[text] = self | 736 _TOKENS[text] = self |
712 | 737 |
713 def ElseNodeClass(self): | 738 def ElseNodeClass(self): |
714 if self.clazz == _VertedSectionNode: | 739 if self.clazz == _VertedSectionNode: |
715 return _InvertedSectionNode | 740 return _InvertedSectionNode |
716 if self.clazz == _InvertedSectionNode: | 741 if self.clazz == _InvertedSectionNode: |
717 return _VertedSectionNode | 742 return _VertedSectionNode |
718 raise ValueError('%s cannot have an else clause.' % self.clazz) | 743 return None |
719 | 744 |
720 OPEN_START_SECTION = Data('OPEN_START_SECTION' , '{{#', _Sect
ionNode) | 745 def __repr__(self): |
721 OPEN_START_VERTED_SECTION = Data('OPEN_START_VERTED_SECTION' , '{{?', _Vert
edSectionNode) | 746 return self.name |
722 OPEN_START_INVERTED_SECTION = Data('OPEN_START_INVERTED_SECTION', '{{^', _Inve
rtedSectionNode) | 747 |
723 OPEN_START_JSON = Data('OPEN_START_JSON' , '{{*', _Json
Node) | 748 def __str__(self): |
724 OPEN_START_PARTIAL = Data('OPEN_START_PARTIAL' , '{{+', _Part
ialNode) | 749 return repr(self) |
725 OPEN_ELSE = Data('OPEN_ELSE' , '{{:', None) | 750 |
726 OPEN_END_SECTION = Data('OPEN_END_SECTION' , '{{/', None) | 751 OPEN_START_SECTION = Data( |
727 INLINE_END_SECTION = Data('INLINE_END_SECTION' , '/}}', None) | 752 'OPEN_START_SECTION' , '{{#', _SectionNode) |
728 OPEN_UNESCAPED_VARIABLE = Data('OPEN_UNESCAPED_VARIABLE' , '{{{', _Unes
capedVariableNode) | 753 OPEN_START_VERTED_SECTION = Data( |
729 CLOSE_MUSTACHE3 = Data('CLOSE_MUSTACHE3' , '}}}', None) | 754 'OPEN_START_VERTED_SECTION' , '{{?', _VertedSectionNode) |
730 OPEN_COMMENT = Data('OPEN_COMMENT' , '{{-', _Comm
entNode) | 755 OPEN_START_INVERTED_SECTION = Data( |
731 CLOSE_COMMENT = Data('CLOSE_COMMENT' , '-}}', None) | 756 'OPEN_START_INVERTED_SECTION', '{{^', _InvertedSectionNode) |
732 OPEN_VARIABLE = Data('OPEN_VARIABLE' , '{{' , _Esca
pedVariableNode) | 757 OPEN_ASSERTION = Data( |
733 CLOSE_MUSTACHE = Data('CLOSE_MUSTACHE' , '}}' , None) | 758 'OPEN_ASSERTION' , '{{!', _AssertionNode) |
734 CHARACTER = Data('CHARACTER' , '.' , None) | 759 OPEN_JSON = Data( |
| 760 'OPEN_JSON' , '{{*', _JsonNode) |
| 761 OPEN_PARTIAL = Data( |
| 762 'OPEN_PARTIAL' , '{{+', _PartialNode) |
| 763 OPEN_ELSE = Data( |
| 764 'OPEN_ELSE' , '{{:', None) |
| 765 OPEN_END_SECTION = Data( |
| 766 'OPEN_END_SECTION' , '{{/', None) |
| 767 INLINE_END_SECTION = Data( |
| 768 'INLINE_END_SECTION' , '/}}', None) |
| 769 OPEN_UNESCAPED_VARIABLE = Data( |
| 770 'OPEN_UNESCAPED_VARIABLE' , '{{{', _UnescapedVariableNode) |
| 771 CLOSE_MUSTACHE3 = Data( |
| 772 'CLOSE_MUSTACHE3' , '}}}', None) |
| 773 OPEN_COMMENT = Data( |
| 774 'OPEN_COMMENT' , '{{-', _CommentNode) |
| 775 CLOSE_COMMENT = Data( |
| 776 'CLOSE_COMMENT' , '-}}', None) |
| 777 OPEN_VARIABLE = Data( |
| 778 'OPEN_VARIABLE' , '{{' , _EscapedVariableNode) |
| 779 CLOSE_MUSTACHE = Data( |
| 780 'CLOSE_MUSTACHE' , '}}' , None) |
| 781 CHARACTER = Data( |
| 782 'CHARACTER' , '.' , None) |
735 | 783 |
736 class _TokenStream(object): | 784 class _TokenStream(object): |
737 ''' Tokeniser for template parsing. | 785 '''Tokeniser for template parsing. |
738 ''' | 786 ''' |
739 def __init__(self, string): | 787 def __init__(self, string): |
740 self.next_token = None | 788 self.next_token = None |
741 self.next_line = _Line(1) | 789 self.next_line = 1 |
742 self.next_column = 0 | 790 self.next_column = 0 |
743 self._string = string | 791 self._string = string |
744 self._cursor = 0 | 792 self._cursor = 0 |
745 self.Advance() | 793 self.Advance() |
746 | 794 |
747 def HasNext(self): | 795 def HasNext(self): |
748 return self.next_token is not None | 796 return self.next_token is not None |
749 | 797 |
| 798 def NextCharacter(self): |
| 799 if self.next_token is _Token.CHARACTER: |
| 800 return self._string[self._cursor - 1] |
| 801 return None |
| 802 |
750 def Advance(self): | 803 def Advance(self): |
751 if self._cursor > 0 and self._string[self._cursor - 1] == '\n': | 804 if self._cursor > 0 and self._string[self._cursor - 1] == '\n': |
752 self.next_line = _Line(self.next_line.number + 1) | 805 self.next_line += 1 |
753 self.next_column = 0 | 806 self.next_column = 0 |
754 elif self.next_token is not None: | 807 elif self.next_token is not None: |
755 self.next_column += len(self.next_token.text) | 808 self.next_column += len(self.next_token.text) |
756 | 809 |
757 self.next_token = None | 810 self.next_token = None |
758 | 811 |
759 if self._cursor == len(self._string): | 812 if self._cursor == len(self._string): |
760 return None | 813 return None |
761 assert self._cursor < len(self._string) | 814 assert self._cursor < len(self._string) |
762 | 815 |
763 if (self._cursor + 1 < len(self._string) and | 816 if (self._cursor + 1 < len(self._string) and |
764 self._string[self._cursor + 1] in '{}'): | 817 self._string[self._cursor + 1] in '{}'): |
765 self.next_token = ( | 818 self.next_token = ( |
766 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or | 819 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or |
767 _TOKENS.get(self._string[self._cursor:self._cursor+2])) | 820 _TOKENS.get(self._string[self._cursor:self._cursor+2])) |
768 | 821 |
769 if self.next_token is None: | 822 if self.next_token is None: |
770 self.next_token = _Token.CHARACTER | 823 self.next_token = _Token.CHARACTER |
771 | 824 |
772 self._cursor += len(self.next_token.text) | 825 self._cursor += len(self.next_token.text) |
773 return self | 826 return self |
774 | 827 |
775 def AdvanceOver(self, token): | 828 def AdvanceOver(self, token, description=None): |
776 if self.next_token != token: | 829 parse_error = None |
777 raise ParseException( | 830 if not self.next_token: |
778 'Expecting token %s but got %s at line %s' % (token.name, | 831 parse_error = 'Reached EOF but expected %s' % token.name |
779 self.next_token.name, | 832 elif self.next_token is not token: |
780 self.next_line)) | 833 parse_error = 'Expecting token %s but got %s at line %s' % ( |
| 834 token.name, self.next_token.name, self.next_line) |
| 835 if parse_error: |
| 836 parse_error += ' %s' % description or '' |
| 837 raise ParseException(parse_error) |
781 return self.Advance() | 838 return self.Advance() |
782 | 839 |
| 840 def AdvanceOverSeparator(self, char, description=None): |
| 841 self.SkipWhitespace() |
| 842 next_char = self.NextCharacter() |
| 843 if next_char != char: |
| 844 parse_error = 'Expected \'%s\'. got \'%s\'' % (char, next_char) |
| 845 if description is not None: |
| 846 parse_error += ' (%s)' % description |
| 847 raise ParseException(parse_error) |
| 848 self.AdvanceOver(_Token.CHARACTER) |
| 849 self.SkipWhitespace() |
| 850 |
783 def AdvanceOverNextString(self, excluded=''): | 851 def AdvanceOverNextString(self, excluded=''): |
784 start = self._cursor - len(self.next_token.text) | 852 start = self._cursor - len(self.next_token.text) |
785 while (self.next_token is _Token.CHARACTER and | 853 while (self.next_token is _Token.CHARACTER and |
786 # Can use -1 here because token length of CHARACTER is 1. | 854 # Can use -1 here because token length of CHARACTER is 1. |
787 self._string[self._cursor - 1] not in excluded): | 855 self._string[self._cursor - 1] not in excluded): |
788 self.Advance() | 856 self.Advance() |
789 end = self._cursor - (len(self.next_token.text) if self.next_token else 0) | 857 end = self._cursor - (len(self.next_token.text) if self.next_token else 0) |
790 return self._string[start:end] | 858 return self._string[start:end] |
791 | 859 |
792 def AdvanceToNextWhitespace(self): | 860 def AdvanceToNextWhitespace(self): |
793 return self.AdvanceOverNextString(excluded=' \n\r\t') | 861 return self.AdvanceOverNextString(excluded=' \n\r\t') |
794 | 862 |
795 def SkipWhitespace(self): | 863 def SkipWhitespace(self): |
796 while (self.next_token is _Token.CHARACTER and | 864 while (self.next_token is _Token.CHARACTER and |
797 # Can use -1 here because token length of CHARACTER is 1. | 865 # Can use -1 here because token length of CHARACTER is 1. |
798 self._string[self._cursor - 1] in ' \n\r\t'): | 866 self._string[self._cursor - 1] in ' \n\r\t'): |
799 self.Advance() | 867 self.Advance() |
800 | 868 |
| 869 def __repr__(self): |
| 870 return '%s(next_token=%s, remainder=%s)' % (type(self).__name__, |
| 871 self.next_token, |
| 872 self._string[self._cursor:]) |
| 873 |
| 874 def __str__(self): |
| 875 return repr(self) |
| 876 |
801 class Handlebar(object): | 877 class Handlebar(object): |
802 ''' A handlebar template. | 878 '''A handlebar template. |
803 ''' | 879 ''' |
804 def __init__(self, template, name=None): | 880 def __init__(self, template, name=None): |
805 self.source = template | 881 self.source = template |
806 self._name = name | 882 self._name = name |
807 tokens = _TokenStream(template) | 883 tokens = _TokenStream(template) |
808 self._top_node = self._ParseSection(tokens) | 884 self._top_node = self._ParseSection(tokens) |
809 if not self._top_node: | 885 if not self._top_node: |
810 raise ParseException('Template is empty') | 886 raise ParseException('Template is empty') |
811 if tokens.HasNext(): | 887 if tokens.HasNext(): |
812 raise ParseException('There are still tokens remaining at %s, ' | 888 raise ParseException('There are still tokens remaining at %s, ' |
813 'was there an end-section without a start-section?' | 889 'was there an end-section without a start-section?' % |
814 % tokens.next_line) | 890 tokens.next_line) |
815 | 891 |
816 def _ParseSection(self, tokens): | 892 def _ParseSection(self, tokens): |
817 nodes = [] | 893 nodes = [] |
818 while tokens.HasNext(): | 894 while tokens.HasNext(): |
819 if tokens.next_token in (_Token.OPEN_END_SECTION, | 895 if tokens.next_token in (_Token.OPEN_END_SECTION, |
820 _Token.OPEN_ELSE): | 896 _Token.OPEN_ELSE): |
821 # Handled after running parseSection within the SECTION cases, so this | 897 # Handled after running parseSection within the SECTION cases, so this |
822 # is a terminating condition. If there *is* an orphaned | 898 # is a terminating condition. If there *is* an orphaned |
823 # OPEN_END_SECTION, it will be caught by noticing that there are | 899 # OPEN_END_SECTION, it will be caught by noticing that there are |
824 # leftover tokens after termination. | 900 # leftover tokens after termination. |
(...skipping 11 matching lines...) Expand all Loading... |
836 previous_node = nodes[i - 1] if i > 0 else None | 912 previous_node = nodes[i - 1] if i > 0 else None |
837 next_node = nodes[i + 1] if i < len(nodes) - 1 else None | 913 next_node = nodes[i + 1] if i < len(nodes) - 1 else None |
838 rendered_node = None | 914 rendered_node = None |
839 | 915 |
840 if node.GetStartLine() != node.GetEndLine(): | 916 if node.GetStartLine() != node.GetEndLine(): |
841 rendered_node = _BlockNode(node) | 917 rendered_node = _BlockNode(node) |
842 if previous_node: | 918 if previous_node: |
843 previous_node.TrimEndingSpaces() | 919 previous_node.TrimEndingSpaces() |
844 if next_node: | 920 if next_node: |
845 next_node.TrimStartingNewLine() | 921 next_node.TrimStartingNewLine() |
846 elif (isinstance(node, _LeafNode) and | 922 elif ((not previous_node or previous_node.EndsWithEmptyLine()) and |
847 (not previous_node or previous_node.EndsWithEmptyLine()) and | |
848 (not next_node or next_node.StartsWithNewLine())): | 923 (not next_node or next_node.StartsWithNewLine())): |
849 indentation = 0 | 924 indentation = 0 |
850 if previous_node: | 925 if previous_node: |
851 indentation = previous_node.TrimEndingSpaces() | 926 indentation = previous_node.TrimEndingSpaces() |
852 if next_node: | 927 if next_node: |
853 next_node.TrimStartingNewLine() | 928 next_node.TrimStartingNewLine() |
854 rendered_node = _IndentedNode(node, indentation) | 929 rendered_node = _IndentedNode(node, indentation) |
855 else: | 930 else: |
856 rendered_node = _InlineNode(node) | 931 rendered_node = _InlineNode(node) |
857 | 932 |
858 nodes[i] = rendered_node | 933 nodes[i] = rendered_node |
859 | 934 |
860 if len(nodes) == 0: | 935 if len(nodes) == 0: |
861 return None | 936 return None |
862 if len(nodes) == 1: | 937 if len(nodes) == 1: |
863 return nodes[0] | 938 return nodes[0] |
864 return _NodeCollection(nodes) | 939 return _NodeCollection(nodes) |
865 | 940 |
866 def _ParseNextOpenToken(self, tokens): | 941 def _ParseNextOpenToken(self, tokens): |
867 next_token = tokens.next_token | 942 next_token = tokens.next_token |
868 | 943 |
869 if next_token is _Token.CHARACTER: | 944 if next_token is _Token.CHARACTER: |
| 945 # Plain strings. |
870 start_line = tokens.next_line | 946 start_line = tokens.next_line |
871 string = tokens.AdvanceOverNextString() | 947 string = tokens.AdvanceOverNextString() |
872 return [_StringNode(string, start_line, tokens.next_line)] | 948 return [_StringNode(string, start_line, tokens.next_line)] |
873 elif next_token in (_Token.OPEN_VARIABLE, | 949 elif next_token in (_Token.OPEN_VARIABLE, |
874 _Token.OPEN_UNESCAPED_VARIABLE, | 950 _Token.OPEN_UNESCAPED_VARIABLE, |
875 _Token.OPEN_START_JSON): | 951 _Token.OPEN_JSON): |
876 id_, inline_value_id = self._OpenSectionOrTag(tokens) | 952 # Inline nodes that don't take arguments. |
877 if inline_value_id is not None: | 953 tokens.Advance() |
878 raise ParseException( | 954 close_token = (_Token.CLOSE_MUSTACHE3 |
879 '%s cannot have an inline value' % id_.GetDescription()) | 955 if next_token is _Token.OPEN_UNESCAPED_VARIABLE else |
| 956 _Token.CLOSE_MUSTACHE) |
| 957 id_ = self._NextIdentifier(tokens) |
| 958 tokens.AdvanceOver(close_token) |
880 return [next_token.clazz(id_)] | 959 return [next_token.clazz(id_)] |
881 elif next_token is _Token.OPEN_START_PARTIAL: | 960 elif next_token is _Token.OPEN_ASSERTION: |
| 961 # Inline nodes that take arguments. |
882 tokens.Advance() | 962 tokens.Advance() |
883 column_start = tokens.next_column + 1 | 963 id_ = self._NextIdentifier(tokens) |
884 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), | 964 node = next_token.clazz(id_, tokens.AdvanceOverNextString()) |
885 tokens.next_line, | 965 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
886 column_start) | 966 return [node] |
887 partial_node = _PartialNode(id_) | 967 elif next_token in (_Token.OPEN_PARTIAL, |
888 while tokens.next_token is _Token.CHARACTER: | 968 _Token.OPEN_START_SECTION, |
| 969 _Token.OPEN_START_VERTED_SECTION, |
| 970 _Token.OPEN_START_INVERTED_SECTION): |
| 971 # Block nodes, though they may have inline syntax like {{#foo bar /}}. |
| 972 tokens.Advance() |
| 973 bind_to, id_ = None, self._NextIdentifier(tokens) |
| 974 if tokens.NextCharacter() == ':': |
| 975 # This section has the format {{#bound:id}} as opposed to just {{id}}. |
| 976 # That is, |id_| is actually the identifier to bind what the section |
| 977 # is producing, not the identifier of where to find that content. |
| 978 tokens.AdvanceOverSeparator(':') |
| 979 bind_to, id_ = id_, self._NextIdentifier(tokens) |
| 980 partial_args = None |
| 981 if next_token is _Token.OPEN_PARTIAL: |
| 982 partial_args = self._ParsePartialNodeArgs(tokens) |
| 983 if tokens.next_token is not _Token.CLOSE_MUSTACHE: |
| 984 # Inline syntax for partial types. |
| 985 if bind_to is not None: |
| 986 raise ParseException( |
| 987 'Cannot bind %s to a self-closing partial' % bind_to) |
| 988 tokens.AdvanceOver(_Token.INLINE_END_SECTION) |
| 989 partial_node = _PartialNode.Inline(id_) |
| 990 partial_node.SetArguments(partial_args) |
| 991 return [partial_node] |
| 992 elif tokens.next_token is not _Token.CLOSE_MUSTACHE: |
| 993 # Inline syntax for non-partial types. Support select node types: |
| 994 # variables, partials, JSON. |
| 995 line, column = tokens.next_line, (tokens.next_column + 1) |
| 996 name = tokens.AdvanceToNextWhitespace() |
| 997 clazz = _UnescapedVariableNode |
| 998 if name.startswith('*'): |
| 999 clazz = _JsonNode |
| 1000 elif name.startswith('+'): |
| 1001 clazz = _PartialNode.Inline |
| 1002 if clazz is not _UnescapedVariableNode: |
| 1003 name = name[1:] |
| 1004 column += 1 |
| 1005 inline_node = clazz(_Identifier(name, line, column)) |
| 1006 if isinstance(inline_node, _PartialNode): |
| 1007 inline_node.SetArguments(self._ParsePartialNodeArgs(tokens)) |
| 1008 if bind_to is not None: |
| 1009 inline_node.PassThroughArgument(bind_to) |
889 tokens.SkipWhitespace() | 1010 tokens.SkipWhitespace() |
890 key = tokens.AdvanceOverNextString(excluded=':') | 1011 tokens.AdvanceOver(_Token.INLINE_END_SECTION) |
891 tokens.Advance() | 1012 return [next_token.clazz(bind_to, id_, inline_node)] |
892 column_start = tokens.next_column + 1 | 1013 # Block syntax. |
893 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), | |
894 tokens.next_line, | |
895 column_start) | |
896 if key == '@': | |
897 partial_node.SetLocalContext(id_) | |
898 else: | |
899 partial_node.AddArgument(key, id_) | |
900 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) | 1014 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
901 return [partial_node] | 1015 section = self._ParseSection(tokens) |
902 elif next_token is _Token.OPEN_START_SECTION: | 1016 else_node_class = next_token.ElseNodeClass() # may not have one |
903 id_, inline_node = self._OpenSectionOrTag(tokens) | 1017 else_section = None |
| 1018 if (else_node_class is not None and |
| 1019 tokens.next_token is _Token.OPEN_ELSE): |
| 1020 self._OpenElse(tokens, id_) |
| 1021 else_section = self._ParseSection(tokens) |
| 1022 self._CloseSection(tokens, id_) |
904 nodes = [] | 1023 nodes = [] |
905 if inline_node is None: | 1024 if section is not None: |
906 section = self._ParseSection(tokens) | 1025 node = next_token.clazz(bind_to, id_, section) |
907 self._CloseSection(tokens, id_) | 1026 if partial_args: |
908 nodes = [] | 1027 node.SetArguments(partial_args) |
909 if section is not None: | 1028 nodes.append(node) |
910 nodes.append(_SectionNode(id_, section)) | 1029 if else_section is not None: |
911 else: | 1030 nodes.append(else_node_class(bind_to, id_, else_section)) |
912 nodes.append(_SectionNode(id_, inline_node)) | |
913 return nodes | |
914 elif next_token in (_Token.OPEN_START_VERTED_SECTION, | |
915 _Token.OPEN_START_INVERTED_SECTION): | |
916 id_, inline_node = self._OpenSectionOrTag(tokens) | |
917 nodes = [] | |
918 if inline_node is None: | |
919 section = self._ParseSection(tokens) | |
920 else_section = None | |
921 if tokens.next_token is _Token.OPEN_ELSE: | |
922 self._OpenElse(tokens, id_) | |
923 else_section = self._ParseSection(tokens) | |
924 self._CloseSection(tokens, id_) | |
925 if section: | |
926 nodes.append(next_token.clazz(id_, section)) | |
927 if else_section: | |
928 nodes.append(next_token.ElseNodeClass()(id_, else_section)) | |
929 else: | |
930 nodes.append(next_token.clazz(id_, inline_node)) | |
931 return nodes | 1031 return nodes |
932 elif next_token is _Token.OPEN_COMMENT: | 1032 elif next_token is _Token.OPEN_COMMENT: |
| 1033 # Comments. |
933 start_line = tokens.next_line | 1034 start_line = tokens.next_line |
934 self._AdvanceOverComment(tokens) | 1035 self._AdvanceOverComment(tokens) |
935 return [_CommentNode(start_line, tokens.next_line)] | 1036 return [_CommentNode(start_line, tokens.next_line)] |
936 | 1037 |
937 def _AdvanceOverComment(self, tokens): | 1038 def _AdvanceOverComment(self, tokens): |
938 tokens.AdvanceOver(_Token.OPEN_COMMENT) | 1039 tokens.AdvanceOver(_Token.OPEN_COMMENT) |
939 depth = 1 | 1040 depth = 1 |
940 while tokens.HasNext() and depth > 0: | 1041 while tokens.HasNext() and depth > 0: |
941 if tokens.next_token is _Token.OPEN_COMMENT: | 1042 if tokens.next_token is _Token.OPEN_COMMENT: |
942 depth += 1 | 1043 depth += 1 |
943 elif tokens.next_token is _Token.CLOSE_COMMENT: | 1044 elif tokens.next_token is _Token.CLOSE_COMMENT: |
944 depth -= 1 | 1045 depth -= 1 |
945 tokens.Advance() | 1046 tokens.Advance() |
946 | 1047 |
947 def _OpenSectionOrTag(self, tokens): | |
948 def NextIdentifierArgs(): | |
949 tokens.SkipWhitespace() | |
950 line = tokens.next_line | |
951 column = tokens.next_column + 1 | |
952 name = tokens.AdvanceToNextWhitespace() | |
953 tokens.SkipWhitespace() | |
954 return (name, line, column) | |
955 close_token = (_Token.CLOSE_MUSTACHE3 | |
956 if tokens.next_token is _Token.OPEN_UNESCAPED_VARIABLE else | |
957 _Token.CLOSE_MUSTACHE) | |
958 tokens.Advance() | |
959 id_ = _Identifier(*NextIdentifierArgs()) | |
960 if tokens.next_token is close_token: | |
961 tokens.AdvanceOver(close_token) | |
962 inline_node = None | |
963 else: | |
964 name, line, column = NextIdentifierArgs() | |
965 tokens.AdvanceOver(_Token.INLINE_END_SECTION) | |
966 # Support select other types of nodes, the most useful being partial. | |
967 clazz = _UnescapedVariableNode | |
968 if name.startswith('*'): | |
969 clazz = _JsonNode | |
970 elif name.startswith('+'): | |
971 clazz = _PartialNode | |
972 if clazz is not _UnescapedVariableNode: | |
973 name = name[1:] | |
974 column += 1 | |
975 inline_node = clazz(_Identifier(name, line, column)) | |
976 return (id_, inline_node) | |
977 | |
978 def _CloseSection(self, tokens, id_): | 1048 def _CloseSection(self, tokens, id_): |
979 tokens.AdvanceOver(_Token.OPEN_END_SECTION) | 1049 tokens.AdvanceOver(_Token.OPEN_END_SECTION, |
| 1050 description='to match %s' % id_.GetDescription()) |
980 next_string = tokens.AdvanceOverNextString() | 1051 next_string = tokens.AdvanceOverNextString() |
981 if next_string != '' and next_string != id_.name: | 1052 if next_string != '' and next_string != id_.name: |
982 raise ParseException( | 1053 raise ParseException( |
983 'Start section %s doesn\'t match end %s' % (id_, next_string)) | 1054 'Start section %s doesn\'t match end %s' % (id_, next_string)) |
984 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) | 1055 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
985 | 1056 |
986 def _OpenElse(self, tokens, id_): | 1057 def _OpenElse(self, tokens, id_): |
987 tokens.AdvanceOver(_Token.OPEN_ELSE) | 1058 tokens.AdvanceOver(_Token.OPEN_ELSE) |
988 next_string = tokens.AdvanceOverNextString() | 1059 next_string = tokens.AdvanceOverNextString() |
989 if next_string != '' and next_string != id_.name: | 1060 if next_string != '' and next_string != id_.name: |
990 raise ParseException( | 1061 raise ParseException( |
991 'Start section %s doesn\'t match else %s' % (id_, next_string)) | 1062 'Start section %s doesn\'t match else %s' % (id_, next_string)) |
992 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) | 1063 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
993 | 1064 |
994 def Render(self, *contexts): | 1065 def _ParsePartialNodeArgs(self, tokens): |
| 1066 args = {} |
| 1067 tokens.SkipWhitespace() |
| 1068 while (tokens.next_token is _Token.CHARACTER and |
| 1069 tokens.NextCharacter() != ')'): |
| 1070 key = tokens.AdvanceOverNextString(excluded=':') |
| 1071 tokens.AdvanceOverSeparator(':') |
| 1072 if tokens.NextCharacter() == '(': |
| 1073 tokens.AdvanceOverSeparator('(') |
| 1074 inner_id = self._NextIdentifier(tokens) |
| 1075 inner_args = self._ParsePartialNodeArgs(tokens) |
| 1076 tokens.AdvanceOverSeparator(')') |
| 1077 args[key] = {inner_id: inner_args} |
| 1078 else: |
| 1079 args[key] = self._NextIdentifier(tokens) |
| 1080 return args or None |
| 1081 |
| 1082 def _NextIdentifier(self, tokens): |
| 1083 tokens.SkipWhitespace() |
| 1084 column_start = tokens.next_column + 1 |
| 1085 id_ = _Identifier(tokens.AdvanceOverNextString(excluded=' \n\r\t:()'), |
| 1086 tokens.next_line, |
| 1087 column_start) |
| 1088 tokens.SkipWhitespace() |
| 1089 return id_ |
| 1090 |
| 1091 def Render(self, *user_contexts): |
995 '''Renders this template given a variable number of contexts to read out | 1092 '''Renders this template given a variable number of contexts to read out |
996 values from (such as those appearing in {{foo}}). | 1093 values from (such as those appearing in {{foo}}). |
997 ''' | 1094 ''' |
998 name = self._name or '<root>' | 1095 name = self._name or '<root>' |
999 render_state = _RenderState(name, _Contexts(contexts)) | 1096 internal_context = _InternalContext() |
| 1097 render_state = _RenderState( |
| 1098 name, _Contexts([{'_': internal_context}] + list(user_contexts))) |
| 1099 internal_context.SetRenderState(render_state) |
1000 self._top_node.Render(render_state) | 1100 self._top_node.Render(render_state) |
1001 return render_state.GetResult() | 1101 return render_state.GetResult() |
1002 | 1102 |
1003 def render(self, *contexts): | 1103 def render(self, *contexts): |
1004 return self.Render(*contexts) | 1104 return self.Render(*contexts) |
1005 | 1105 |
| 1106 def __eq__(self, other): |
| 1107 return self.source == other.source and self._name == other._name |
| 1108 |
| 1109 def __ne__(self, other): |
| 1110 return not (self == other) |
| 1111 |
1006 def __repr__(self): | 1112 def __repr__(self): |
1007 return str('%s(%s)' % (self.__class__.__name__, self._top_node)) | 1113 return str('%s(%s)' % (type(self).__name__, self._top_node)) |
1008 | 1114 |
1009 def __str__(self): | 1115 def __str__(self): |
1010 return repr(self) | 1116 return repr(self) |
OLD | NEW |