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