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