- add sources.
[platform/framework/web/crosswalk.git] / src / third_party / handlebar / handlebar.py
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: Some character other than {{{ }}} to print unescaped content?
16 # TODO: Only have @ while in a loop, and only defined in the top context of
17 #       the loop.
18 # TODO: Consider trimming spaces around identifers like {{?t foo}}.
19 # TODO: Only transfer global contexts into partials, not the top local.
20 # TODO: Pragmas for asserting the presence of variables.
21 # TODO: Escaping control characters somehow. e.g. \{{, \{{-.
22 # TODO: Dump warnings-so-far into the output.
23
24 import json
25 import re
26
27 '''Handlebar templates are data binding templates more-than-loosely inspired by
28 ctemplate. Use like:
29
30   from handlebar import Handlebar
31
32   template = Handlebar('hello {{#foo}}{{bar}}{{/}} world')
33   input = {
34     'foo': [
35       { 'bar': 1 },
36       { 'bar': 2 },
37       { 'bar': 3 }
38     ]
39   }
40   print(template.render(input).text)
41
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.
45
46   class CustomContext(object):
47     def get(self, key):
48       return 10
49   print(Handlebar('hello {{world}}').render(CustomContext()).text)
50
51 will print 'hello 10'.
52 '''
53
54 class ParseException(Exception):
55   '''The exception thrown while parsing a template.
56   '''
57   def __init__(self, error):
58     Exception.__init__(self, error)
59
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
66
67   def __repr__(self):
68     return '%s(text=%s, errors=%s)' % (
69         self.__class__.__name__, self.text, self.errors)
70
71   def __str__(self):
72     return repr(self)
73
74 class _StringBuilder(object):
75   '''Efficiently builds strings.
76   '''
77   def __init__(self):
78     self._buf = []
79
80   def __len__(self):
81     self._Collapse()
82     return len(self._buf[0])
83
84   def Append(self, string):
85     if not isinstance(string, basestring):
86       string = str(string)
87     self._buf.append(string)
88
89   def ToString(self):
90     self._Collapse()
91     return self._buf[0]
92
93   def _Collapse(self):
94     self._buf = [u''.join(self._buf)]
95
96   def __repr__(self):
97     return self.ToString()
98
99   def __str__(self):
100     return repr(self)
101
102 class _Contexts(object):
103   '''Tracks a stack of context objects, providing efficient key/value retrieval.
104   '''
105   class _Node(object):
106     '''A node within the stack. Wraps a real context and maintains the key/value
107     pairs seen so far.
108     '''
109     def __init__(self, value):
110       self._value = value
111       self._value_has_get = hasattr(value, 'get')
112       self._found = {}
113
114     def GetKeys(self):
115       '''Returns the list of keys that |_value| contains.
116       '''
117       return self._found.keys()
118
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).
122       '''
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
132
133     def __repr__(self):
134       return 'Node(value=%s, found=%s)' % (self._value, self._found)
135
136     def __str__(self):
137       return repr(self)
138
139   def __init__(self, globals_):
140     '''Initializes with the initial global contexts, listed in order from most
141     to least important.
142     '''
143     self._nodes = map(_Contexts._Node, globals_)
144     self._first_local = len(self._nodes)
145     self._value_info = {}
146
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
152
153   def Push(self, context):
154     self._nodes.append(_Contexts._Node(context))
155
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()
162
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):
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)
172
173     if key == '@':
174       found = self._nodes[-1]._value
175     else:
176       found = self._FindNodeValue(key)
177
178     if tail is None:
179       return found
180
181     for part in tail.split('.'):
182       if not hasattr(found, 'get'):
183         return None
184       found = found.get(part)
185     return found
186
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
195
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)
205
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
211
212     return found_node_list[-1]._value.get(key)
213
214 class _Stack(object):
215   class Entry(object):
216     def __init__(self, name, id_):
217       self.name = name
218       self.id_ = id_
219
220   def __init__(self, entries=[]):
221     self.entries = entries
222
223   def Descend(self, name, id_):
224     descended = list(self.entries)
225     descended.append(_Stack.Entry(name, id_))
226     return _Stack(entries=descended)
227
228 class _RenderState(object):
229   '''The state of a render call.
230   '''
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
237
238   def AddResolutionError(self, id_):
239     self._errors.append(
240         id_.CreateResolutionErrorMessage(self._name, stack=self._stack))
241
242   def Copy(self):
243     return _RenderState(
244         self._name, self.contexts, _stack=self._stack)
245
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_))
251
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)
258
259   def GetResult(self):
260     return RenderResult(self.text.ToString(), self._errors);
261
262 class _Identifier(object):
263   ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'.
264   '''
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())
274
275   def GetDescription(self):
276     return '\'%s\' at line %s column %s' % (self.name, self.line, self.column)
277
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()
287
288   def __repr__(self):
289     return self.name
290
291   def __str__(self):
292     return repr(self)
293
294 class _Line(object):
295   def __init__(self, number):
296     self.number = number
297
298   def __repr__(self):
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):
306     self._start_line = start_line
307     self._end_line = end_line
308
309   def StartsWithNewLine(self):
310     return False
311
312   def TrimStartingNewLine(self):
313     pass
314
315   def TrimEndingSpaces(self):
316     return 0
317
318   def TrimEndingNewLine(self):
319     pass
320
321   def EndsWithEmptyLine(self):
322     return False
323
324   def GetStartLine(self):
325     return self._start_line
326
327   def GetEndLine(self):
328     return self._end_line
329
330 class _DecoratorNode(object):
331   def __init__(self, content):
332     self._content = content
333
334   def StartsWithNewLine(self):
335     return self._content.StartsWithNewLine()
336
337   def TrimStartingNewLine(self):
338     self._content.TrimStartingNewLine()
339
340   def TrimEndingSpaces(self):
341     return self._content.TrimEndingSpaces()
342
343   def TrimEndingNewLine(self):
344     self._content.TrimEndingNewLine()
345
346   def EndsWithEmptyLine(self):
347     return self._content.EndsWithEmptyLine()
348
349   def GetStartLine(self):
350     return self._content.GetStartLine()
351
352   def GetEndLine(self):
353     return self._content.GetEndLine()
354
355   def __repr__(self):
356     return str(self._content)
357
358     def __str__(self):
359       return repr(self)
360
361 class _InlineNode(_DecoratorNode):
362   def __init__(self, content):
363     _DecoratorNode.__init__(self, content)
364
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', ''))
370
371 class _IndentedNode(_DecoratorNode):
372   def __init__(self, content, indentation):
373     _DecoratorNode.__init__(self, content)
374     self._indent_str = ' ' * indentation
375
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)
388
389 class _BlockNode(_DecoratorNode):
390   def __init__(self, content):
391     _DecoratorNode.__init__(self, content)
392     content.TrimStartingNewLine()
393     content.TrimEndingSpaces()
394
395   def Render(self, render_state):
396     self._content.Render(render_state)
397
398 class _NodeCollection(object):
399   def __init__(self, nodes):
400     assert nodes
401     self._nodes = nodes
402
403   def Render(self, render_state):
404     for node in self._nodes:
405       node.Render(render_state)
406
407   def StartsWithNewLine(self):
408     return self._nodes[0].StartsWithNewLine()
409
410   def TrimStartingNewLine(self):
411     self._nodes[0].TrimStartingNewLine()
412
413   def TrimEndingSpaces(self):
414     return self._nodes[-1].TrimEndingSpaces()
415
416   def TrimEndingNewLine(self):
417     self._nodes[-1].TrimEndingNewLine()
418
419   def EndsWithEmptyLine(self):
420     return self._nodes[-1].EndsWithEmptyLine()
421
422   def GetStartLine(self):
423     return self._nodes[0].GetStartLine()
424
425   def GetEndLine(self):
426     return self._nodes[-1].GetEndLine()
427
428   def __repr__(self):
429     return ''.join(str(node) for node in self._nodes)
430
431   def __str__(self):
432     return repr(self)
433
434 class _StringNode(object):
435   ''' Just a string.
436   '''
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
441
442   def Render(self, render_state):
443     render_state.text.Append(self._string)
444
445   def StartsWithNewLine(self):
446     return self._string.startswith('\n')
447
448   def TrimStartingNewLine(self):
449     if self.StartsWithNewLine():
450       self._string = self._string[1:]
451
452   def TrimEndingSpaces(self):
453     original_length = len(self._string)
454     self._string = self._string[:self._LastIndexOfSpaces()]
455     return original_length - len(self._string)
456
457   def TrimEndingNewLine(self):
458     if self._string.endswith('\n'):
459       self._string = self._string[:len(self._string) - 1]
460
461   def EndsWithEmptyLine(self):
462     index = self._LastIndexOfSpaces()
463     return index == 0 or self._string[index - 1] == '\n'
464
465   def _LastIndexOfSpaces(self):
466     index = len(self._string)
467     while index > 0 and self._string[index - 1] == ' ':
468       index -= 1
469     return index
470
471   def GetStartLine(self):
472     return self._start_line
473
474   def GetEndLine(self):
475     return self._end_line
476
477   def __repr__(self):
478     return self._string
479
480   def __str__(self):
481     return repr(self)
482
483 class _EscapedVariableNode(_LeafNode):
484   ''' {{foo}}
485   '''
486   def __init__(self, id_):
487     _LeafNode.__init__(self, id_.line, id_.line)
488     self._id = id_
489
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;'))
499
500   def __repr__(self):
501     return '{{%s}}' % self._id
502
503   def __str__(self):
504     return repr(self)
505
506 class _UnescapedVariableNode(_LeafNode):
507   ''' {{{foo}}}
508   '''
509   def __init__(self, id_):
510     _LeafNode.__init__(self, id_.line, id_.line)
511     self._id = id_
512
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)
520
521   def __repr__(self):
522     return '{{{%s}}}' % self._id
523
524   def __str__(self):
525     return repr(self)
526
527 class _CommentNode(_LeafNode):
528   '''{{- This is a comment -}}
529   An empty placeholder node for correct indented rendering behaviour.
530   '''
531   def __init__(self, start_line, end_line):
532     _LeafNode.__init__(self, start_line, end_line)
533
534   def Render(self, render_state):
535     pass
536
537   def __repr__(self):
538     return '<comment>'
539
540   def __str__(self):
541     return repr(self)
542
543 class _SectionNode(_DecoratorNode):
544   ''' {{#foo}} ... {{/}}
545   '''
546   def __init__(self, id_, content):
547     _DecoratorNode.__init__(self, content)
548     self._id = id_
549
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)
565
566   def __repr__(self):
567     return '{{#%s}}%s{{/%s}}' % (
568         self._id, _DecoratorNode.__repr__(self), self._id)
569
570   def __str__(self):
571     return repr(self)
572
573 class _VertedSectionNode(_DecoratorNode):
574   ''' {{?foo}} ... {{/}}
575   '''
576   def __init__(self, id_, content):
577     _DecoratorNode.__init__(self, content)
578     self._id = id_
579
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()
586
587   def __repr__(self):
588     return '{{?%s}}%s{{/%s}}' % (
589         self._id, _DecoratorNode.__repr__(self), self._id)
590
591   def __str__(self):
592     return repr(self)
593
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
603
604 class _InvertedSectionNode(_DecoratorNode):
605   ''' {{^foo}} ... {{/}}
606   '''
607   def __init__(self, id_, content):
608     _DecoratorNode.__init__(self, content)
609     self._id = id_
610
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)
615
616   def __repr__(self):
617     return '{{^%s}}%s{{/%s}}' % (
618         self._id, _DecoratorNode.__repr__(self), self._id)
619
620   def __str__(self):
621     return repr(self)
622
623 class _JsonNode(_LeafNode):
624   ''' {{*foo}}
625   '''
626   def __init__(self, id_):
627     _LeafNode.__init__(self, id_.line, id_.line)
628     self._id = id_
629
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=(',',':')))
636
637   def __repr__(self):
638     return '{{*%s}}' % self._id
639
640   def __str__(self):
641     return repr(self)
642
643 class _PartialNode(_LeafNode):
644   ''' {{+foo}}
645   '''
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
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
660
661     partial_render_state = render_state.ForkPartial(value._name, self._id)
662
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)
667
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)
675
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)
680
681     value._top_node.Render(partial_render_state)
682
683     render_state.Merge(
684         partial_render_state,
685         text_transform=lambda text: text[:-1] if text.endswith('\n') else text)
686
687   def AddArgument(self, key, id_):
688     if self._args is None:
689       self._args = {}
690     self._args[key] = id_
691
692   def SetLocalContext(self, id_):
693     self._local_context_id = id_
694
695   def __repr__(self):
696     return '{{+%s}}' % self._id
697
698   def __str__(self):
699     return repr(self)
700
701 _TOKENS = {}
702
703 class _Token(object):
704   ''' The tokens that can appear in a template.
705   '''
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
712
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)
719
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)
735
736 class _TokenStream(object):
737   ''' Tokeniser for template parsing.
738   '''
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()
746
747   def HasNext(self):
748     return self.next_token is not None
749
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)
756
757     self.next_token = None
758
759     if self._cursor == len(self._string):
760       return None
761     assert self._cursor < len(self._string)
762
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]))
768
769     if self.next_token is None:
770       self.next_token = _Token.CHARACTER
771
772     self._cursor += len(self.next_token.text)
773     return self
774
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()
782
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]
791
792   def AdvanceToNextWhitespace(self):
793     return self.AdvanceOverNextString(excluded=' \n\r\t')
794
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()
800
801 class Handlebar(object):
802   ''' A handlebar template.
803   '''
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)
815
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)
831
832     for i, node in enumerate(nodes):
833       if isinstance(node, _StringNode):
834         continue
835
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
839
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)
857
858       nodes[i] = rendered_node
859
860     if len(nodes) == 0:
861       return None
862     if len(nodes) == 1:
863       return nodes[0]
864     return _NodeCollection(nodes)
865
866   def _ParseNextOpenToken(self, tokens):
867     next_token = tokens.next_token
868
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)]
936
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()
946
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_):
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)
985
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)
993
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}}).
997     '''
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()
1002
1003   def render(self, *contexts):
1004     return self.Render(*contexts)
1005
1006   def __repr__(self):
1007     return str('%s(%s)' % (self.__class__.__name__, self._top_node))
1008
1009   def __str__(self):
1010     return repr(self)