Home | History | Annotate | Download | only in handlebar
      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)
   1011