cc: Added inline to Tile::IsReadyToDraw
[chromium-blink-merge.git] / third_party / handlebar / handlebar.py
blob0be089c178d9616c9c4b7cdfbecc46d47d29618b
1 # Copyright 2012 Benjamin Kalman
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # TODO: Some character other than {{{ }}} to print unescaped content?
16 # TODO: Only have @ while in a loop, and only defined in the top context of
17 # the loop.
18 # TODO: Consider trimming spaces around identifers like {{?t foo}}.
19 # TODO: Only transfer global contexts into partials, not the top local.
20 # TODO: Pragmas for asserting the presence of variables.
21 # TODO: Escaping control characters somehow. e.g. \{{, \{{-.
22 # TODO: Dump warnings-so-far into the output.
24 import json
25 import re
27 '''Handlebar templates are data binding templates more-than-loosely inspired by
28 ctemplate. Use like:
30 from handlebar import Handlebar
32 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world')
33 input = {
34 'foo': [
35 { 'bar': 1 },
36 { 'bar': 2 },
37 { 'bar': 3 }
40 print(template.render(input).text)
42 Handlebar will use get() on contexts to return values, so to create custom
43 getters (for example, something that populates values lazily from keys), just
44 provide an object with a get() method.
46 class CustomContext(object):
47 def get(self, key):
48 return 10
49 print(Handlebar('hello {{world}}').render(CustomContext()).text)
51 will print 'hello 10'.
52 '''
54 class ParseException(Exception):
55 '''The exception thrown while parsing a template.
56 '''
57 def __init__(self, error):
58 Exception.__init__(self, error)
60 class RenderResult(object):
61 '''The result of a render operation.
62 '''
63 def __init__(self, text, errors):
64 self.text = text;
65 self.errors = errors
67 def __repr__(self):
68 return '%s(text=%s, errors=%s)' % (
69 self.__class__.__name__, self.text, self.errors)
71 def __str__(self):
72 return repr(self)
74 class _StringBuilder(object):
75 '''Efficiently builds strings.
76 '''
77 def __init__(self):
78 self._buf = []
80 def __len__(self):
81 self._Collapse()
82 return len(self._buf[0])
84 def Append(self, string):
85 if not isinstance(string, basestring):
86 string = str(string)
87 self._buf.append(string)
89 def ToString(self):
90 self._Collapse()
91 return self._buf[0]
93 def _Collapse(self):
94 self._buf = [u''.join(self._buf)]
96 def __repr__(self):
97 return self.ToString()
99 def __str__(self):
100 return repr(self)
102 class _Contexts(object):
103 '''Tracks a stack of context objects, providing efficient key/value retrieval.
105 class _Node(object):
106 '''A node within the stack. Wraps a real context and maintains the key/value
107 pairs seen so far.
109 def __init__(self, value):
110 self._value = value
111 self._value_has_get = hasattr(value, 'get')
112 self._found = {}
114 def GetKeys(self):
115 '''Returns the list of keys that |_value| contains.
117 return self._found.keys()
119 def Get(self, key):
120 '''Returns the value for |key|, or None if not found (including if
121 |_value| doesn't support key retrieval).
123 if not self._value_has_get:
124 return None
125 value = self._found.get(key)
126 if value is not None:
127 return value
128 value = self._value.get(key)
129 if value is not None:
130 self._found[key] = value
131 return value
133 def __repr__(self):
134 return 'Node(value=%s, found=%s)' % (self._value, self._found)
136 def __str__(self):
137 return repr(self)
139 def __init__(self, globals_):
140 '''Initializes with the initial global contexts, listed in order from most
141 to least important.
143 self._nodes = map(_Contexts._Node, globals_)
144 self._first_local = len(self._nodes)
145 self._value_info = {}
147 def CreateFromGlobals(self):
148 new = _Contexts([])
149 new._nodes = self._nodes[:self._first_local]
150 new._first_local = self._first_local
151 return new
153 def Push(self, context):
154 self._nodes.append(_Contexts._Node(context))
156 def Pop(self):
157 node = self._nodes.pop()
158 assert len(self._nodes) >= self._first_local
159 for found_key in node.GetKeys():
160 # [0] is the stack of nodes that |found_key| has been found in.
161 self._value_info[found_key][0].pop()
163 def GetTopLocal(self):
164 if len(self._nodes) == self._first_local:
165 return None
166 return self._nodes[-1]._value
168 def Resolve(self, path):
169 # 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.
171 key, tail = path.split('.', 1) if '.' in path else (path, None)
173 if key == '@':
174 found = self._nodes[-1]._value
175 else:
176 found = self._FindNodeValue(key)
178 if tail is None:
179 return found
181 for part in tail.split('.'):
182 if not hasattr(found, 'get'):
183 return None
184 found = found.get(part)
185 return found
187 def _FindNodeValue(self, key):
188 # |found_node_list| will be all the nodes that |key| has been found in.
189 # |checked_node_set| are those that have been checked.
190 info = self._value_info.get(key)
191 if info is None:
192 info = ([], set())
193 self._value_info[key] = info
194 found_node_list, checked_node_set = info
196 # Check all the nodes not yet checked for |key|.
197 newly_found = []
198 for node in reversed(self._nodes):
199 if node in checked_node_set:
200 break
201 value = node.Get(key)
202 if value is not None:
203 newly_found.append(node)
204 checked_node_set.add(node)
206 # The nodes will have been found in reverse stack order. After extending
207 # the found nodes, the freshest value will be at the tip of the stack.
208 found_node_list.extend(reversed(newly_found))
209 if not found_node_list:
210 return None
212 return found_node_list[-1]._value.get(key)
214 class _Stack(object):
215 class Entry(object):
216 def __init__(self, name, id_):
217 self.name = name
218 self.id_ = id_
220 def __init__(self, entries=[]):
221 self.entries = entries
223 def Descend(self, name, id_):
224 descended = list(self.entries)
225 descended.append(_Stack.Entry(name, id_))
226 return _Stack(entries=descended)
228 class _RenderState(object):
229 '''The state of a render call.
231 def __init__(self, name, contexts, _stack=_Stack()):
232 self.text = _StringBuilder()
233 self.contexts = contexts
234 self._name = name
235 self._errors = []
236 self._stack = _stack
238 def AddResolutionError(self, id_):
239 self._errors.append(
240 id_.CreateResolutionErrorMessage(self._name, stack=self._stack))
242 def Copy(self):
243 return _RenderState(
244 self._name, self.contexts, _stack=self._stack)
246 def ForkPartial(self, custom_name, id_):
247 name = custom_name or id_.name
248 return _RenderState(name,
249 self.contexts.CreateFromGlobals(),
250 _stack=self._stack.Descend(name, id_))
252 def Merge(self, render_state, text_transform=None):
253 self._errors.extend(render_state._errors)
254 text = render_state.text.ToString()
255 if text_transform is not None:
256 text = text_transform(text)
257 self.text.Append(text)
259 def GetResult(self):
260 return RenderResult(self.text.ToString(), self._errors);
262 class _Identifier(object):
263 ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'.
265 def __init__(self, name, line, column):
266 self.name = name
267 self.line = line
268 self.column = column
269 if name == '':
270 raise ParseException('Empty identifier %s' % self.GetDescription())
271 for part in name.split('.'):
272 if part != '@' and not re.match('^[a-zA-Z0-9_/-]+$', part):
273 raise ParseException('Invalid identifier %s' % self.GetDescription())
275 def GetDescription(self):
276 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column)
278 def CreateResolutionErrorMessage(self, name, stack=None):
279 message = _StringBuilder()
280 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(),
281 name))
282 if stack is not None:
283 for entry in stack.entries:
284 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(),
285 entry.name))
286 return message.ToString()
288 def __repr__(self):
289 return self.name
291 def __str__(self):
292 return repr(self)
294 class _Line(object):
295 def __init__(self, number):
296 self.number = number
298 def __repr__(self):
299 return str(self.number)
301 def __str__(self):
302 return repr(self)
304 class _LeafNode(object):
305 def __init__(self, start_line, end_line):
306 self._start_line = start_line
307 self._end_line = end_line
309 def StartsWithNewLine(self):
310 return False
312 def TrimStartingNewLine(self):
313 pass
315 def TrimEndingSpaces(self):
316 return 0
318 def TrimEndingNewLine(self):
319 pass
321 def EndsWithEmptyLine(self):
322 return False
324 def GetStartLine(self):
325 return self._start_line
327 def GetEndLine(self):
328 return self._end_line
330 class _DecoratorNode(object):
331 def __init__(self, content):
332 self._content = content
334 def StartsWithNewLine(self):
335 return self._content.StartsWithNewLine()
337 def TrimStartingNewLine(self):
338 self._content.TrimStartingNewLine()
340 def TrimEndingSpaces(self):
341 return self._content.TrimEndingSpaces()
343 def TrimEndingNewLine(self):
344 self._content.TrimEndingNewLine()
346 def EndsWithEmptyLine(self):
347 return self._content.EndsWithEmptyLine()
349 def GetStartLine(self):
350 return self._content.GetStartLine()
352 def GetEndLine(self):
353 return self._content.GetEndLine()
355 def __repr__(self):
356 return str(self._content)
358 def __str__(self):
359 return repr(self)
361 class _InlineNode(_DecoratorNode):
362 def __init__(self, content):
363 _DecoratorNode.__init__(self, content)
365 def Render(self, render_state):
366 content_render_state = render_state.Copy()
367 self._content.Render(content_render_state)
368 render_state.Merge(content_render_state,
369 text_transform=lambda text: text.replace('\n', ''))
371 class _IndentedNode(_DecoratorNode):
372 def __init__(self, content, indentation):
373 _DecoratorNode.__init__(self, content)
374 self._indent_str = ' ' * indentation
376 def Render(self, render_state):
377 if isinstance(self._content, _CommentNode):
378 return
379 content_render_state = render_state.Copy()
380 self._content.Render(content_render_state)
381 def AddIndentation(text):
382 buf = _StringBuilder()
383 buf.Append(self._indent_str)
384 buf.Append(text.replace('\n', '\n%s' % self._indent_str))
385 buf.Append('\n')
386 return buf.ToString()
387 render_state.Merge(content_render_state, text_transform=AddIndentation)
389 class _BlockNode(_DecoratorNode):
390 def __init__(self, content):
391 _DecoratorNode.__init__(self, content)
392 content.TrimStartingNewLine()
393 content.TrimEndingSpaces()
395 def Render(self, render_state):
396 self._content.Render(render_state)
398 class _NodeCollection(object):
399 def __init__(self, nodes):
400 assert nodes
401 self._nodes = nodes
403 def Render(self, render_state):
404 for node in self._nodes:
405 node.Render(render_state)
407 def StartsWithNewLine(self):
408 return self._nodes[0].StartsWithNewLine()
410 def TrimStartingNewLine(self):
411 self._nodes[0].TrimStartingNewLine()
413 def TrimEndingSpaces(self):
414 return self._nodes[-1].TrimEndingSpaces()
416 def TrimEndingNewLine(self):
417 self._nodes[-1].TrimEndingNewLine()
419 def EndsWithEmptyLine(self):
420 return self._nodes[-1].EndsWithEmptyLine()
422 def GetStartLine(self):
423 return self._nodes[0].GetStartLine()
425 def GetEndLine(self):
426 return self._nodes[-1].GetEndLine()
428 def __repr__(self):
429 return ''.join(str(node) for node in self._nodes)
431 def __str__(self):
432 return repr(self)
434 class _StringNode(object):
435 ''' Just a string.
437 def __init__(self, string, start_line, end_line):
438 self._string = string
439 self._start_line = start_line
440 self._end_line = end_line
442 def Render(self, render_state):
443 render_state.text.Append(self._string)
445 def StartsWithNewLine(self):
446 return self._string.startswith('\n')
448 def TrimStartingNewLine(self):
449 if self.StartsWithNewLine():
450 self._string = self._string[1:]
452 def TrimEndingSpaces(self):
453 original_length = len(self._string)
454 self._string = self._string[:self._LastIndexOfSpaces()]
455 return original_length - len(self._string)
457 def TrimEndingNewLine(self):
458 if self._string.endswith('\n'):
459 self._string = self._string[:len(self._string) - 1]
461 def EndsWithEmptyLine(self):
462 index = self._LastIndexOfSpaces()
463 return index == 0 or self._string[index - 1] == '\n'
465 def _LastIndexOfSpaces(self):
466 index = len(self._string)
467 while index > 0 and self._string[index - 1] == ' ':
468 index -= 1
469 return index
471 def GetStartLine(self):
472 return self._start_line
474 def GetEndLine(self):
475 return self._end_line
477 def __repr__(self):
478 return self._string
480 def __str__(self):
481 return repr(self)
483 class _EscapedVariableNode(_LeafNode):
484 ''' {{foo}}
486 def __init__(self, id_):
487 _LeafNode.__init__(self, id_.line, id_.line)
488 self._id = id_
490 def Render(self, render_state):
491 value = render_state.contexts.Resolve(self._id.name)
492 if value is None:
493 render_state.AddResolutionError(self._id)
494 return
495 string = value if isinstance(value, basestring) else str(value)
496 render_state.text.Append(string.replace('&', '&')
497 .replace('<', '&lt;')
498 .replace('>', '&gt;'))
500 def __repr__(self):
501 return '{{%s}}' % self._id
503 def __str__(self):
504 return repr(self)
506 class _UnescapedVariableNode(_LeafNode):
507 ''' {{{foo}}}
509 def __init__(self, id_):
510 _LeafNode.__init__(self, id_.line, id_.line)
511 self._id = id_
513 def Render(self, render_state):
514 value = render_state.contexts.Resolve(self._id.name)
515 if value is None:
516 render_state.AddResolutionError(self._id)
517 return
518 string = value if isinstance(value, basestring) else str(value)
519 render_state.text.Append(string)
521 def __repr__(self):
522 return '{{{%s}}}' % self._id
524 def __str__(self):
525 return repr(self)
527 class _CommentNode(_LeafNode):
528 '''{{- This is a comment -}}
529 An empty placeholder node for correct indented rendering behaviour.
531 def __init__(self, start_line, end_line):
532 _LeafNode.__init__(self, start_line, end_line)
534 def Render(self, render_state):
535 pass
537 def __repr__(self):
538 return '<comment>'
540 def __str__(self):
541 return repr(self)
543 class _SectionNode(_DecoratorNode):
544 ''' {{#foo}} ... {{/}}
546 def __init__(self, id_, content):
547 _DecoratorNode.__init__(self, content)
548 self._id = id_
550 def Render(self, render_state):
551 value = render_state.contexts.Resolve(self._id.name)
552 if isinstance(value, list):
553 for item in value:
554 # Always push context, even if it's not "valid", since we want to
555 # be able to refer to items in a list such as [1,2,3] via @.
556 render_state.contexts.Push(item)
557 self._content.Render(render_state)
558 render_state.contexts.Pop()
559 elif hasattr(value, 'get'):
560 render_state.contexts.Push(value)
561 self._content.Render(render_state)
562 render_state.contexts.Pop()
563 else:
564 render_state.AddResolutionError(self._id)
566 def __repr__(self):
567 return '{{#%s}}%s{{/%s}}' % (
568 self._id, _DecoratorNode.__repr__(self), self._id)
570 def __str__(self):
571 return repr(self)
573 class _VertedSectionNode(_DecoratorNode):
574 ''' {{?foo}} ... {{/}}
576 def __init__(self, id_, content):
577 _DecoratorNode.__init__(self, content)
578 self._id = id_
580 def Render(self, render_state):
581 value = render_state.contexts.Resolve(self._id.name)
582 if _VertedSectionNode.ShouldRender(value):
583 render_state.contexts.Push(value)
584 self._content.Render(render_state)
585 render_state.contexts.Pop()
587 def __repr__(self):
588 return '{{?%s}}%s{{/%s}}' % (
589 self._id, _DecoratorNode.__repr__(self), self._id)
591 def __str__(self):
592 return repr(self)
594 @staticmethod
595 def ShouldRender(value):
596 if value is None:
597 return False
598 if isinstance(value, bool):
599 return value
600 if isinstance(value, list):
601 return len(value) > 0
602 return True
604 class _InvertedSectionNode(_DecoratorNode):
605 ''' {{^foo}} ... {{/}}
607 def __init__(self, id_, content):
608 _DecoratorNode.__init__(self, content)
609 self._id = id_
611 def Render(self, render_state):
612 value = render_state.contexts.Resolve(self._id.name)
613 if not _VertedSectionNode.ShouldRender(value):
614 self._content.Render(render_state)
616 def __repr__(self):
617 return '{{^%s}}%s{{/%s}}' % (
618 self._id, _DecoratorNode.__repr__(self), self._id)
620 def __str__(self):
621 return repr(self)
623 class _JsonNode(_LeafNode):
624 ''' {{*foo}}
626 def __init__(self, id_):
627 _LeafNode.__init__(self, id_.line, id_.line)
628 self._id = id_
630 def Render(self, render_state):
631 value = render_state.contexts.Resolve(self._id.name)
632 if value is None:
633 render_state.AddResolutionError(self._id)
634 return
635 render_state.text.Append(json.dumps(value, separators=(',',':')))
637 def __repr__(self):
638 return '{{*%s}}' % self._id
640 def __str__(self):
641 return repr(self)
643 class _PartialNode(_LeafNode):
644 ''' {{+foo}}
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
652 def Render(self, render_state):
653 value = render_state.contexts.Resolve(self._id.name)
654 if value is None:
655 render_state.AddResolutionError(self._id)
656 return
657 if not isinstance(value, Handlebar):
658 render_state.AddResolutionError(self._id)
659 return
661 partial_render_state = render_state.ForkPartial(value._name, self._id)
663 # TODO: Don't do this. Force callers to do this by specifying an @ argument.
664 top_local = render_state.contexts.GetTopLocal()
665 if top_local is not None:
666 partial_render_state.contexts.Push(top_local)
668 if self._args is not None:
669 arg_context = {}
670 for key, value_id in self._args.items():
671 context = render_state.contexts.Resolve(value_id.name)
672 if context is not None:
673 arg_context[key] = context
674 partial_render_state.contexts.Push(arg_context)
676 if self._local_context_id is not None:
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)
681 value._top_node.Render(partial_render_state)
683 render_state.Merge(
684 partial_render_state,
685 text_transform=lambda text: text[:-1] if text.endswith('\n') else text)
687 def AddArgument(self, key, id_):
688 if self._args is None:
689 self._args = {}
690 self._args[key] = id_
692 def SetLocalContext(self, id_):
693 self._local_context_id = id_
695 def __repr__(self):
696 return '{{+%s}}' % self._id
698 def __str__(self):
699 return repr(self)
701 _TOKENS = {}
703 class _Token(object):
704 ''' The tokens that can appear in a template.
706 class Data(object):
707 def __init__(self, name, text, clazz):
708 self.name = name
709 self.text = text
710 self.clazz = clazz
711 _TOKENS[text] = self
713 def ElseNodeClass(self):
714 if self.clazz == _VertedSectionNode:
715 return _InvertedSectionNode
716 if self.clazz == _InvertedSectionNode:
717 return _VertedSectionNode
718 raise ValueError('%s cannot have an else clause.' % self.clazz)
720 OPEN_START_SECTION = Data('OPEN_START_SECTION' , '{{#', _SectionNode)
721 OPEN_START_VERTED_SECTION = Data('OPEN_START_VERTED_SECTION' , '{{?', _VertedSectionNode)
722 OPEN_START_INVERTED_SECTION = Data('OPEN_START_INVERTED_SECTION', '{{^', _InvertedSectionNode)
723 OPEN_START_JSON = Data('OPEN_START_JSON' , '{{*', _JsonNode)
724 OPEN_START_PARTIAL = Data('OPEN_START_PARTIAL' , '{{+', _PartialNode)
725 OPEN_ELSE = Data('OPEN_ELSE' , '{{:', None)
726 OPEN_END_SECTION = Data('OPEN_END_SECTION' , '{{/', None)
727 INLINE_END_SECTION = Data('INLINE_END_SECTION' , '/}}', None)
728 OPEN_UNESCAPED_VARIABLE = Data('OPEN_UNESCAPED_VARIABLE' , '{{{', _UnescapedVariableNode)
729 CLOSE_MUSTACHE3 = Data('CLOSE_MUSTACHE3' , '}}}', None)
730 OPEN_COMMENT = Data('OPEN_COMMENT' , '{{-', _CommentNode)
731 CLOSE_COMMENT = Data('CLOSE_COMMENT' , '-}}', None)
732 OPEN_VARIABLE = Data('OPEN_VARIABLE' , '{{' , _EscapedVariableNode)
733 CLOSE_MUSTACHE = Data('CLOSE_MUSTACHE' , '}}' , None)
734 CHARACTER = Data('CHARACTER' , '.' , None)
736 class _TokenStream(object):
737 ''' Tokeniser for template parsing.
739 def __init__(self, string):
740 self.next_token = None
741 self.next_line = _Line(1)
742 self.next_column = 0
743 self._string = string
744 self._cursor = 0
745 self.Advance()
747 def HasNext(self):
748 return self.next_token is not None
750 def Advance(self):
751 if self._cursor > 0 and self._string[self._cursor - 1] == '\n':
752 self.next_line = _Line(self.next_line.number + 1)
753 self.next_column = 0
754 elif self.next_token is not None:
755 self.next_column += len(self.next_token.text)
757 self.next_token = None
759 if self._cursor == len(self._string):
760 return None
761 assert self._cursor < len(self._string)
763 if (self._cursor + 1 < len(self._string) and
764 self._string[self._cursor + 1] in '{}'):
765 self.next_token = (
766 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or
767 _TOKENS.get(self._string[self._cursor:self._cursor+2]))
769 if self.next_token is None:
770 self.next_token = _Token.CHARACTER
772 self._cursor += len(self.next_token.text)
773 return self
775 def AdvanceOver(self, token):
776 if self.next_token != token:
777 raise ParseException(
778 'Expecting token %s but got %s at line %s' % (token.name,
779 self.next_token.name,
780 self.next_line))
781 return self.Advance()
783 def AdvanceOverNextString(self, excluded=''):
784 start = self._cursor - len(self.next_token.text)
785 while (self.next_token is _Token.CHARACTER and
786 # Can use -1 here because token length of CHARACTER is 1.
787 self._string[self._cursor - 1] not in excluded):
788 self.Advance()
789 end = self._cursor - (len(self.next_token.text) if self.next_token else 0)
790 return self._string[start:end]
792 def AdvanceToNextWhitespace(self):
793 return self.AdvanceOverNextString(excluded=' \n\r\t')
795 def SkipWhitespace(self):
796 while (self.next_token is _Token.CHARACTER and
797 # Can use -1 here because token length of CHARACTER is 1.
798 self._string[self._cursor - 1] in ' \n\r\t'):
799 self.Advance()
801 class Handlebar(object):
802 ''' A handlebar template.
804 def __init__(self, template, name=None):
805 self.source = template
806 self._name = name
807 tokens = _TokenStream(template)
808 self._top_node = self._ParseSection(tokens)
809 if not self._top_node:
810 raise ParseException('Template is empty')
811 if tokens.HasNext():
812 raise ParseException('There are still tokens remaining at %s, '
813 'was there an end-section without a start-section?'
814 % tokens.next_line)
816 def _ParseSection(self, tokens):
817 nodes = []
818 while tokens.HasNext():
819 if tokens.next_token in (_Token.OPEN_END_SECTION,
820 _Token.OPEN_ELSE):
821 # Handled after running parseSection within the SECTION cases, so this
822 # is a terminating condition. If there *is* an orphaned
823 # OPEN_END_SECTION, it will be caught by noticing that there are
824 # leftover tokens after termination.
825 break
826 elif tokens.next_token in (_Token.CLOSE_MUSTACHE,
827 _Token.CLOSE_MUSTACHE3):
828 raise ParseException('Orphaned %s at line %s' % (tokens.next_token.name,
829 tokens.next_line))
830 nodes += self._ParseNextOpenToken(tokens)
832 for i, node in enumerate(nodes):
833 if isinstance(node, _StringNode):
834 continue
836 previous_node = nodes[i - 1] if i > 0 else None
837 next_node = nodes[i + 1] if i < len(nodes) - 1 else None
838 rendered_node = None
840 if node.GetStartLine() != node.GetEndLine():
841 rendered_node = _BlockNode(node)
842 if previous_node:
843 previous_node.TrimEndingSpaces()
844 if next_node:
845 next_node.TrimStartingNewLine()
846 elif (isinstance(node, _LeafNode) and
847 (not previous_node or previous_node.EndsWithEmptyLine()) and
848 (not next_node or next_node.StartsWithNewLine())):
849 indentation = 0
850 if previous_node:
851 indentation = previous_node.TrimEndingSpaces()
852 if next_node:
853 next_node.TrimStartingNewLine()
854 rendered_node = _IndentedNode(node, indentation)
855 else:
856 rendered_node = _InlineNode(node)
858 nodes[i] = rendered_node
860 if len(nodes) == 0:
861 return None
862 if len(nodes) == 1:
863 return nodes[0]
864 return _NodeCollection(nodes)
866 def _ParseNextOpenToken(self, tokens):
867 next_token = tokens.next_token
869 if next_token is _Token.CHARACTER:
870 start_line = tokens.next_line
871 string = tokens.AdvanceOverNextString()
872 return [_StringNode(string, start_line, tokens.next_line)]
873 elif next_token in (_Token.OPEN_VARIABLE,
874 _Token.OPEN_UNESCAPED_VARIABLE,
875 _Token.OPEN_START_JSON):
876 id_, inline_value_id = self._OpenSectionOrTag(tokens)
877 if inline_value_id is not None:
878 raise ParseException(
879 '%s cannot have an inline value' % id_.GetDescription())
880 return [next_token.clazz(id_)]
881 elif next_token is _Token.OPEN_START_PARTIAL:
882 tokens.Advance()
883 column_start = tokens.next_column + 1
884 id_ = _Identifier(tokens.AdvanceToNextWhitespace(),
885 tokens.next_line,
886 column_start)
887 partial_node = _PartialNode(id_)
888 while tokens.next_token is _Token.CHARACTER:
889 tokens.SkipWhitespace()
890 key = tokens.AdvanceOverNextString(excluded=':')
891 tokens.Advance()
892 column_start = tokens.next_column + 1
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)
901 return [partial_node]
902 elif next_token is _Token.OPEN_START_SECTION:
903 id_, inline_node = self._OpenSectionOrTag(tokens)
904 nodes = []
905 if inline_node is None:
906 section = self._ParseSection(tokens)
907 self._CloseSection(tokens, id_)
908 nodes = []
909 if section is not None:
910 nodes.append(_SectionNode(id_, section))
911 else:
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
932 elif next_token is _Token.OPEN_COMMENT:
933 start_line = tokens.next_line
934 self._AdvanceOverComment(tokens)
935 return [_CommentNode(start_line, tokens.next_line)]
937 def _AdvanceOverComment(self, tokens):
938 tokens.AdvanceOver(_Token.OPEN_COMMENT)
939 depth = 1
940 while tokens.HasNext() and depth > 0:
941 if tokens.next_token is _Token.OPEN_COMMENT:
942 depth += 1
943 elif tokens.next_token is _Token.CLOSE_COMMENT:
944 depth -= 1
945 tokens.Advance()
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)
978 def _CloseSection(self, tokens, id_):
979 tokens.AdvanceOver(_Token.OPEN_END_SECTION)
980 next_string = tokens.AdvanceOverNextString()
981 if next_string != '' and next_string != id_.name:
982 raise ParseException(
983 'Start section %s doesn\'t match end %s' % (id_, next_string))
984 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE)
986 def _OpenElse(self, tokens, id_):
987 tokens.AdvanceOver(_Token.OPEN_ELSE)
988 next_string = tokens.AdvanceOverNextString()
989 if next_string != '' and next_string != id_.name:
990 raise ParseException(
991 'Start section %s doesn\'t match else %s' % (id_, next_string))
992 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE)
994 def Render(self, *contexts):
995 '''Renders this template given a variable number of contexts to read out
996 values from (such as those appearing in {{foo}}).
998 name = self._name or '<root>'
999 render_state = _RenderState(name, _Contexts(contexts))
1000 self._top_node.Render(render_state)
1001 return render_state.GetResult()
1003 def render(self, *contexts):
1004 return self.Render(*contexts)
1006 def __repr__(self):
1007 return str('%s(%s)' % (self.__class__.__name__, self._top_node))
1009 def __str__(self):
1010 return repr(self)