| Index: third_party/handlebar/handlebar.py
|
| diff --git a/third_party/handlebar/handlebar.py b/third_party/handlebar/handlebar.py
|
| deleted file mode 100644
|
| index 7e06868e19380cfdcc4faa8c49f4620e4c0a5d1d..0000000000000000000000000000000000000000
|
| --- a/third_party/handlebar/handlebar.py
|
| +++ /dev/null
|
| @@ -1,1158 +0,0 @@
|
| -# Copyright 2012 Benjamin Kalman
|
| -#
|
| -# Licensed under the Apache License, Version 2.0 (the "License");
|
| -# you may not use this file except in compliance with the License.
|
| -# You may obtain a copy of the License at
|
| -#
|
| -# http://www.apache.org/licenses/LICENSE-2.0
|
| -#
|
| -# Unless required by applicable law or agreed to in writing, software
|
| -# distributed under the License is distributed on an "AS IS" BASIS,
|
| -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| -# See the License for the specific language governing permissions and
|
| -# limitations under the License.
|
| -
|
| -# TODO: New name, not "handlebar".
|
| -# TODO: Escaping control characters somehow. e.g. \{{, \{{-.
|
| -
|
| -import json
|
| -import re
|
| -
|
| -'''Handlebar templates are data binding templates more-than-loosely inspired by
|
| -ctemplate. Use like:
|
| -
|
| - from handlebar import Handlebar
|
| -
|
| - template = Handlebar('hello {{#foo bar/}} world')
|
| - input = {
|
| - 'foo': [
|
| - { 'bar': 1 },
|
| - { 'bar': 2 },
|
| - { 'bar': 3 }
|
| - ]
|
| - }
|
| - print(template.render(input).text)
|
| -
|
| -Handlebar will use get() on contexts to return values, so to create custom
|
| -getters (for example, something that populates values lazily from keys), just
|
| -provide an object with a get() method.
|
| -
|
| - class CustomContext(object):
|
| - def get(self, key):
|
| - return 10
|
| - print(Handlebar('hello {{world}}').render(CustomContext()).text)
|
| -
|
| -will print 'hello 10'.
|
| -'''
|
| -
|
| -class ParseException(Exception):
|
| - '''The exception thrown while parsing a template.
|
| - '''
|
| - def __init__(self, error):
|
| - Exception.__init__(self, error)
|
| -
|
| -class RenderResult(object):
|
| - '''The result of a render operation.
|
| - '''
|
| - def __init__(self, text, errors):
|
| - self.text = text;
|
| - self.errors = errors
|
| -
|
| - def __repr__(self):
|
| - return '%s(text=%s, errors=%s)' % (type(self).__name__,
|
| - self.text,
|
| - self.errors)
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
| -
|
| -class _StringBuilder(object):
|
| - '''Efficiently builds strings.
|
| - '''
|
| - def __init__(self):
|
| - self._buf = []
|
| -
|
| - def __len__(self):
|
| - self._Collapse()
|
| - return len(self._buf[0])
|
| -
|
| - def Append(self, string):
|
| - if not isinstance(string, basestring):
|
| - string = str(string)
|
| - self._buf.append(string)
|
| -
|
| - def ToString(self):
|
| - self._Collapse()
|
| - return self._buf[0]
|
| -
|
| - def _Collapse(self):
|
| - self._buf = [u''.join(self._buf)]
|
| -
|
| - def __repr__(self):
|
| - return self.ToString()
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
| -
|
| -class _Contexts(object):
|
| - '''Tracks a stack of context objects, providing efficient key/value retrieval.
|
| - '''
|
| - class _Node(object):
|
| - '''A node within the stack. Wraps a real context and maintains the key/value
|
| - pairs seen so far.
|
| - '''
|
| - def __init__(self, value):
|
| - self._value = value
|
| - self._value_has_get = hasattr(value, 'get')
|
| - self._found = {}
|
| -
|
| - def GetKeys(self):
|
| - '''Returns the list of keys that |_value| contains.
|
| - '''
|
| - return self._found.keys()
|
| -
|
| - def Get(self, key):
|
| - '''Returns the value for |key|, or None if not found (including if
|
| - |_value| doesn't support key retrieval).
|
| - '''
|
| - if not self._value_has_get:
|
| - return None
|
| - value = self._found.get(key)
|
| - if value is not None:
|
| - return value
|
| - value = self._value.get(key)
|
| - if value is not None:
|
| - self._found[key] = value
|
| - return value
|
| -
|
| - def __repr__(self):
|
| - return 'Node(value=%s, found=%s)' % (self._value, self._found)
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
| -
|
| - def __init__(self, globals_):
|
| - '''Initializes with the initial global contexts, listed in order from most
|
| - to least important.
|
| - '''
|
| - self._nodes = map(_Contexts._Node, globals_)
|
| - self._first_local = len(self._nodes)
|
| - self._value_info = {}
|
| -
|
| - def CreateFromGlobals(self):
|
| - new = _Contexts([])
|
| - new._nodes = self._nodes[:self._first_local]
|
| - new._first_local = self._first_local
|
| - return new
|
| -
|
| - def Push(self, context):
|
| - self._nodes.append(_Contexts._Node(context))
|
| -
|
| - def Pop(self):
|
| - node = self._nodes.pop()
|
| - assert len(self._nodes) >= self._first_local
|
| - for found_key in node.GetKeys():
|
| - # [0] is the stack of nodes that |found_key| has been found in.
|
| - self._value_info[found_key][0].pop()
|
| -
|
| - def FirstLocal(self):
|
| - if len(self._nodes) == self._first_local:
|
| - return None
|
| - return self._nodes[-1]._value
|
| -
|
| - def Resolve(self, path):
|
| - # This method is only efficient at finding |key|; if |tail| has a value (and
|
| - # |key| evaluates to an indexable value) we'll need to descend into that.
|
| - key, tail = path.split('.', 1) if '.' in path else (path, None)
|
| - found = self._FindNodeValue(key)
|
| - if tail is None:
|
| - return found
|
| - for part in tail.split('.'):
|
| - if not hasattr(found, 'get'):
|
| - return None
|
| - found = found.get(part)
|
| - return found
|
| -
|
| - def Scope(self, context, fn, *args):
|
| - self.Push(context)
|
| - try:
|
| - return fn(*args)
|
| - finally:
|
| - self.Pop()
|
| -
|
| - def _FindNodeValue(self, key):
|
| - # |found_node_list| will be all the nodes that |key| has been found in.
|
| - # |checked_node_set| are those that have been checked.
|
| - info = self._value_info.get(key)
|
| - if info is None:
|
| - info = ([], set())
|
| - self._value_info[key] = info
|
| - found_node_list, checked_node_set = info
|
| -
|
| - # Check all the nodes not yet checked for |key|.
|
| - newly_found = []
|
| - for node in reversed(self._nodes):
|
| - if node in checked_node_set:
|
| - break
|
| - value = node.Get(key)
|
| - if value is not None:
|
| - newly_found.append(node)
|
| - checked_node_set.add(node)
|
| -
|
| - # The nodes will have been found in reverse stack order. After extending
|
| - # the found nodes, the freshest value will be at the tip of the stack.
|
| - found_node_list.extend(reversed(newly_found))
|
| - if not found_node_list:
|
| - return None
|
| -
|
| - return found_node_list[-1]._value.get(key)
|
| -
|
| -class _Stack(object):
|
| - class Entry(object):
|
| - def __init__(self, name, id_):
|
| - self.name = name
|
| - self.id_ = id_
|
| -
|
| - def __init__(self, entries=[]):
|
| - self.entries = entries
|
| -
|
| - def Descend(self, name, id_):
|
| - descended = list(self.entries)
|
| - descended.append(_Stack.Entry(name, id_))
|
| - return _Stack(entries=descended)
|
| -
|
| -class _InternalContext(object):
|
| - def __init__(self):
|
| - self._render_state = None
|
| -
|
| - def SetRenderState(self, render_state):
|
| - self._render_state = render_state
|
| -
|
| - def get(self, key):
|
| - if key == 'errors':
|
| - errors = self._render_state._errors
|
| - return '\n'.join(errors) if errors else None
|
| - return None
|
| -
|
| -class _RenderState(object):
|
| - '''The state of a render call.
|
| - '''
|
| - def __init__(self, name, contexts, _stack=_Stack()):
|
| - self.text = _StringBuilder()
|
| - self.contexts = contexts
|
| - self._name = name
|
| - self._errors = []
|
| - self._stack = _stack
|
| -
|
| - def AddResolutionError(self, id_, description=None):
|
| - message = id_.CreateResolutionErrorMessage(self._name, stack=self._stack)
|
| - if description is not None:
|
| - message = '%s (%s)' % (message, description)
|
| - self._errors.append(message)
|
| -
|
| - def Copy(self):
|
| - return _RenderState(
|
| - self._name, self.contexts, _stack=self._stack)
|
| -
|
| - def ForkPartial(self, custom_name, id_):
|
| - name = custom_name or id_.name
|
| - return _RenderState(name,
|
| - self.contexts.CreateFromGlobals(),
|
| - _stack=self._stack.Descend(name, id_))
|
| -
|
| - def Merge(self, render_state, text_transform=None):
|
| - self._errors.extend(render_state._errors)
|
| - text = render_state.text.ToString()
|
| - if text_transform is not None:
|
| - text = text_transform(text)
|
| - self.text.Append(text)
|
| -
|
| - def GetResult(self):
|
| - return RenderResult(self.text.ToString(), self._errors);
|
| -
|
| -class _Identifier(object):
|
| - '''An identifier of the form 'foo', 'foo.bar.baz', 'foo-bar.baz', etc.
|
| - '''
|
| - _VALID_ID_MATCHER = re.compile(r'^[a-zA-Z0-9@_/-]+$')
|
| -
|
| - def __init__(self, name, line, column):
|
| - self.name = name
|
| - self.line = line
|
| - self.column = column
|
| - if name == '':
|
| - raise ParseException('Empty identifier %s' % self.GetDescription())
|
| - for part in name.split('.'):
|
| - if not _Identifier._VALID_ID_MATCHER.match(part):
|
| - raise ParseException('Invalid identifier %s' % self.GetDescription())
|
| -
|
| - def GetDescription(self):
|
| - return '\'%s\' at line %s column %s' % (self.name, self.line, self.column)
|
| -
|
| - def CreateResolutionErrorMessage(self, name, stack=None):
|
| - message = _StringBuilder()
|
| - message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(),
|
| - name))
|
| - if stack is not None:
|
| - for entry in reversed(stack.entries):
|
| - message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(),
|
| - entry.name))
|
| - return message.ToString().strip()
|
| -
|
| - def __repr__(self):
|
| - return self.name
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
| -
|
| -class _Node(object): pass
|
| -
|
| -class _LeafNode(_Node):
|
| - def __init__(self, start_line, end_line):
|
| - self._start_line = start_line
|
| - self._end_line = end_line
|
| -
|
| - def StartsWithNewLine(self):
|
| - return False
|
| -
|
| - def TrimStartingNewLine(self):
|
| - pass
|
| -
|
| - def TrimEndingSpaces(self):
|
| - return 0
|
| -
|
| - def TrimEndingNewLine(self):
|
| - pass
|
| -
|
| - def EndsWithEmptyLine(self):
|
| - return False
|
| -
|
| - def GetStartLine(self):
|
| - return self._start_line
|
| -
|
| - def GetEndLine(self):
|
| - return self._end_line
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
| -
|
| -class _DecoratorNode(_Node):
|
| - def __init__(self, content):
|
| - self._content = content
|
| -
|
| - def StartsWithNewLine(self):
|
| - return self._content.StartsWithNewLine()
|
| -
|
| - def TrimStartingNewLine(self):
|
| - self._content.TrimStartingNewLine()
|
| -
|
| - def TrimEndingSpaces(self):
|
| - return self._content.TrimEndingSpaces()
|
| -
|
| - def TrimEndingNewLine(self):
|
| - self._content.TrimEndingNewLine()
|
| -
|
| - def EndsWithEmptyLine(self):
|
| - return self._content.EndsWithEmptyLine()
|
| -
|
| - def GetStartLine(self):
|
| - return self._content.GetStartLine()
|
| -
|
| - def GetEndLine(self):
|
| - return self._content.GetEndLine()
|
| -
|
| - def __repr__(self):
|
| - return str(self._content)
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
| -
|
| -class _InlineNode(_DecoratorNode):
|
| - def __init__(self, content):
|
| - _DecoratorNode.__init__(self, content)
|
| -
|
| - def Render(self, render_state):
|
| - content_render_state = render_state.Copy()
|
| - self._content.Render(content_render_state)
|
| - render_state.Merge(content_render_state,
|
| - text_transform=lambda text: text.replace('\n', ''))
|
| -
|
| -class _IndentedNode(_DecoratorNode):
|
| - def __init__(self, content, indentation):
|
| - _DecoratorNode.__init__(self, content)
|
| - self._indent_str = ' ' * indentation
|
| -
|
| - def Render(self, render_state):
|
| - if isinstance(self._content, _CommentNode):
|
| - return
|
| - def inlinify(text):
|
| - if len(text) == 0: # avoid rendering a blank line
|
| - return ''
|
| - buf = _StringBuilder()
|
| - buf.Append(self._indent_str)
|
| - buf.Append(text.replace('\n', '\n%s' % self._indent_str))
|
| - if not text.endswith('\n'): # partials will often already end in a \n
|
| - buf.Append('\n')
|
| - return buf.ToString()
|
| - content_render_state = render_state.Copy()
|
| - self._content.Render(content_render_state)
|
| - render_state.Merge(content_render_state, text_transform=inlinify)
|
| -
|
| -class _BlockNode(_DecoratorNode):
|
| - def __init__(self, content):
|
| - _DecoratorNode.__init__(self, content)
|
| - content.TrimStartingNewLine()
|
| - content.TrimEndingSpaces()
|
| -
|
| - def Render(self, render_state):
|
| - self._content.Render(render_state)
|
| -
|
| -class _NodeCollection(_Node):
|
| - def __init__(self, nodes):
|
| - assert nodes
|
| - self._nodes = nodes
|
| -
|
| - def Render(self, render_state):
|
| - for node in self._nodes:
|
| - node.Render(render_state)
|
| -
|
| - def StartsWithNewLine(self):
|
| - return self._nodes[0].StartsWithNewLine()
|
| -
|
| - def TrimStartingNewLine(self):
|
| - self._nodes[0].TrimStartingNewLine()
|
| -
|
| - def TrimEndingSpaces(self):
|
| - return self._nodes[-1].TrimEndingSpaces()
|
| -
|
| - def TrimEndingNewLine(self):
|
| - self._nodes[-1].TrimEndingNewLine()
|
| -
|
| - def EndsWithEmptyLine(self):
|
| - return self._nodes[-1].EndsWithEmptyLine()
|
| -
|
| - def GetStartLine(self):
|
| - return self._nodes[0].GetStartLine()
|
| -
|
| - def GetEndLine(self):
|
| - return self._nodes[-1].GetEndLine()
|
| -
|
| - def __repr__(self):
|
| - return ''.join(str(node) for node in self._nodes)
|
| -
|
| -class _StringNode(_Node):
|
| - '''Just a string.
|
| - '''
|
| - def __init__(self, string, start_line, end_line):
|
| - self._string = string
|
| - self._start_line = start_line
|
| - self._end_line = end_line
|
| -
|
| - def Render(self, render_state):
|
| - render_state.text.Append(self._string)
|
| -
|
| - def StartsWithNewLine(self):
|
| - return self._string.startswith('\n')
|
| -
|
| - def TrimStartingNewLine(self):
|
| - if self.StartsWithNewLine():
|
| - self._string = self._string[1:]
|
| -
|
| - def TrimEndingSpaces(self):
|
| - original_length = len(self._string)
|
| - self._string = self._string[:self._LastIndexOfSpaces()]
|
| - return original_length - len(self._string)
|
| -
|
| - def TrimEndingNewLine(self):
|
| - if self._string.endswith('\n'):
|
| - self._string = self._string[:len(self._string) - 1]
|
| -
|
| - def EndsWithEmptyLine(self):
|
| - index = self._LastIndexOfSpaces()
|
| - return index == 0 or self._string[index - 1] == '\n'
|
| -
|
| - def _LastIndexOfSpaces(self):
|
| - index = len(self._string)
|
| - while index > 0 and self._string[index - 1] == ' ':
|
| - index -= 1
|
| - return index
|
| -
|
| - def GetStartLine(self):
|
| - return self._start_line
|
| -
|
| - def GetEndLine(self):
|
| - return self._end_line
|
| -
|
| - def __repr__(self):
|
| - return self._string
|
| -
|
| -class _EscapedVariableNode(_LeafNode):
|
| - '''{{foo}}
|
| - '''
|
| - def __init__(self, id_):
|
| - _LeafNode.__init__(self, id_.line, id_.line)
|
| - self._id = id_
|
| -
|
| - def Render(self, render_state):
|
| - value = render_state.contexts.Resolve(self._id.name)
|
| - if value is None:
|
| - render_state.AddResolutionError(self._id)
|
| - return
|
| - string = value if isinstance(value, basestring) else str(value)
|
| - render_state.text.Append(string.replace('&', '&')
|
| - .replace('<', '<')
|
| - .replace('>', '>'))
|
| -
|
| - def __repr__(self):
|
| - return '{{%s}}' % self._id
|
| -
|
| -class _UnescapedVariableNode(_LeafNode):
|
| - '''{{{foo}}}
|
| - '''
|
| - def __init__(self, id_):
|
| - _LeafNode.__init__(self, id_.line, id_.line)
|
| - self._id = id_
|
| -
|
| - def Render(self, render_state):
|
| - value = render_state.contexts.Resolve(self._id.name)
|
| - if value is None:
|
| - render_state.AddResolutionError(self._id)
|
| - return
|
| - string = value if isinstance(value, basestring) else str(value)
|
| - render_state.text.Append(string)
|
| -
|
| - def __repr__(self):
|
| - return '{{{%s}}}' % self._id
|
| -
|
| -class _CommentNode(_LeafNode):
|
| - '''{{- This is a comment -}}
|
| - An empty placeholder node for correct indented rendering behaviour.
|
| - '''
|
| - def __init__(self, start_line, end_line):
|
| - _LeafNode.__init__(self, start_line, end_line)
|
| -
|
| - def Render(self, render_state):
|
| - pass
|
| -
|
| - def __repr__(self):
|
| - return '<comment>'
|
| -
|
| -class _SectionNode(_DecoratorNode):
|
| - '''{{#var:foo}} ... {{/foo}}
|
| - '''
|
| - def __init__(self, bind_to, id_, content):
|
| - _DecoratorNode.__init__(self, content)
|
| - self._bind_to = bind_to
|
| - self._id = id_
|
| -
|
| - def Render(self, render_state):
|
| - value = render_state.contexts.Resolve(self._id.name)
|
| - if isinstance(value, list):
|
| - for item in value:
|
| - if self._bind_to is not None:
|
| - render_state.contexts.Scope({self._bind_to.name: item},
|
| - self._content.Render, render_state)
|
| - else:
|
| - self._content.Render(render_state)
|
| - elif hasattr(value, 'get'):
|
| - if self._bind_to is not None:
|
| - render_state.contexts.Scope({self._bind_to.name: value},
|
| - self._content.Render, render_state)
|
| - else:
|
| - render_state.contexts.Scope(value, self._content.Render, render_state)
|
| - else:
|
| - render_state.AddResolutionError(self._id)
|
| -
|
| - def __repr__(self):
|
| - return '{{#%s}}%s{{/%s}}' % (
|
| - self._id, _DecoratorNode.__repr__(self), self._id)
|
| -
|
| -class _VertedSectionNode(_DecoratorNode):
|
| - '''{{?var:foo}} ... {{/foo}}
|
| - '''
|
| - def __init__(self, bind_to, id_, content):
|
| - _DecoratorNode.__init__(self, content)
|
| - self._bind_to = bind_to
|
| - self._id = id_
|
| -
|
| - def Render(self, render_state):
|
| - value = render_state.contexts.Resolve(self._id.name)
|
| - if _VertedSectionNode.ShouldRender(value):
|
| - if self._bind_to is not None:
|
| - render_state.contexts.Scope({self._bind_to.name: value},
|
| - self._content.Render, render_state)
|
| - else:
|
| - self._content.Render(render_state)
|
| -
|
| - def __repr__(self):
|
| - return '{{?%s}}%s{{/%s}}' % (
|
| - self._id, _DecoratorNode.__repr__(self), self._id)
|
| -
|
| - @staticmethod
|
| - def ShouldRender(value):
|
| - if value is None:
|
| - return False
|
| - if isinstance(value, bool):
|
| - return value
|
| - if isinstance(value, list):
|
| - return len(value) > 0
|
| - return True
|
| -
|
| -class _InvertedSectionNode(_DecoratorNode):
|
| - '''{{^foo}} ... {{/foo}}
|
| - '''
|
| - def __init__(self, bind_to, id_, content):
|
| - _DecoratorNode.__init__(self, content)
|
| - if bind_to is not None:
|
| - raise ParseException('{{^%s:%s}} does not support variable binding'
|
| - % (bind_to, id_))
|
| - self._id = id_
|
| -
|
| - def Render(self, render_state):
|
| - value = render_state.contexts.Resolve(self._id.name)
|
| - if not _VertedSectionNode.ShouldRender(value):
|
| - self._content.Render(render_state)
|
| -
|
| - def __repr__(self):
|
| - return '{{^%s}}%s{{/%s}}' % (
|
| - self._id, _DecoratorNode.__repr__(self), self._id)
|
| -
|
| -class _AssertionNode(_LeafNode):
|
| - '''{{!foo Some comment about foo}}
|
| - '''
|
| - def __init__(self, id_, description):
|
| - _LeafNode.__init__(self, id_.line, id_.line)
|
| - self._id = id_
|
| - self._description = description
|
| -
|
| - def Render(self, render_state):
|
| - if render_state.contexts.Resolve(self._id.name) is None:
|
| - render_state.AddResolutionError(self._id, description=self._description)
|
| -
|
| - def __repr__(self):
|
| - return '{{!%s %s}}' % (self._id, self._description)
|
| -
|
| -class _JsonNode(_LeafNode):
|
| - '''{{*foo}}
|
| - '''
|
| - def __init__(self, id_):
|
| - _LeafNode.__init__(self, id_.line, id_.line)
|
| - self._id = id_
|
| -
|
| - def Render(self, render_state):
|
| - value = render_state.contexts.Resolve(self._id.name)
|
| - if value is None:
|
| - render_state.AddResolutionError(self._id)
|
| - return
|
| - render_state.text.Append(json.dumps(value, separators=(',',':')))
|
| -
|
| - def __repr__(self):
|
| - return '{{*%s}}' % self._id
|
| -
|
| -# TODO: Better common model of _PartialNodeWithArguments, _PartialNodeInContext,
|
| -# and _PartialNode.
|
| -class _PartialNodeWithArguments(_DecoratorNode):
|
| - def __init__(self, partial, args):
|
| - if isinstance(partial, Handlebar):
|
| - # Preserve any get() method that the caller has added.
|
| - if hasattr(partial, 'get'):
|
| - self.get = partial.get
|
| - partial = partial._top_node
|
| - _DecoratorNode.__init__(self, partial)
|
| - self._partial = partial
|
| - self._args = args
|
| -
|
| - def Render(self, render_state):
|
| - render_state.contexts.Scope(self._args, self._partial.Render, render_state)
|
| -
|
| -class _PartialNodeInContext(_DecoratorNode):
|
| - def __init__(self, partial, context):
|
| - if isinstance(partial, Handlebar):
|
| - # Preserve any get() method that the caller has added.
|
| - if hasattr(partial, 'get'):
|
| - self.get = partial.get
|
| - partial = partial._top_node
|
| - _DecoratorNode.__init__(self, partial)
|
| - self._partial = partial
|
| - self._context = context
|
| -
|
| - def Render(self, render_state):
|
| - original_contexts = render_state.contexts
|
| - try:
|
| - render_state.contexts = self._context
|
| - render_state.contexts.Scope(
|
| - # The first local context of |original_contexts| will be the
|
| - # arguments that were passed to the partial, if any.
|
| - original_contexts.FirstLocal() or {},
|
| - self._partial.Render, render_state)
|
| - finally:
|
| - render_state.contexts = original_contexts
|
| -
|
| -class _PartialNode(_LeafNode):
|
| - '''{{+var:foo}} ... {{/foo}}
|
| - '''
|
| - def __init__(self, bind_to, id_, content):
|
| - _LeafNode.__init__(self, id_.line, id_.line)
|
| - self._bind_to = bind_to
|
| - self._id = id_
|
| - self._content = content
|
| - self._args = None
|
| - self._pass_through_id = None
|
| -
|
| - @classmethod
|
| - def Inline(cls, id_):
|
| - return cls(None, id_, None)
|
| -
|
| - def Render(self, render_state):
|
| - value = render_state.contexts.Resolve(self._id.name)
|
| - if value is None:
|
| - render_state.AddResolutionError(self._id)
|
| - return
|
| - if not isinstance(value, (Handlebar, _Node)):
|
| - render_state.AddResolutionError(self._id, description='not a partial')
|
| - return
|
| -
|
| - if isinstance(value, Handlebar):
|
| - node, name = value._top_node, value._name
|
| - else:
|
| - node, name = value, None
|
| -
|
| - partial_render_state = render_state.ForkPartial(name, self._id)
|
| -
|
| - arg_context = {}
|
| - if self._pass_through_id is not None:
|
| - context = render_state.contexts.Resolve(self._pass_through_id.name)
|
| - if context is not None:
|
| - arg_context[self._pass_through_id.name] = context
|
| - if self._args is not None:
|
| - def resolve_args(args):
|
| - resolved = {}
|
| - for key, value in args.iteritems():
|
| - if isinstance(value, dict):
|
| - assert len(value.keys()) == 1
|
| - id_of_partial, partial_args = value.items()[0]
|
| - partial = render_state.contexts.Resolve(id_of_partial.name)
|
| - if partial is not None:
|
| - resolved[key] = _PartialNodeWithArguments(
|
| - partial, resolve_args(partial_args))
|
| - else:
|
| - context = render_state.contexts.Resolve(value.name)
|
| - if context is not None:
|
| - resolved[key] = context
|
| - return resolved
|
| - arg_context.update(resolve_args(self._args))
|
| - if self._bind_to and self._content:
|
| - arg_context[self._bind_to.name] = _PartialNodeInContext(
|
| - self._content, render_state.contexts)
|
| - if arg_context:
|
| - partial_render_state.contexts.Push(arg_context)
|
| -
|
| - node.Render(partial_render_state)
|
| -
|
| - render_state.Merge(
|
| - partial_render_state,
|
| - text_transform=lambda text: text[:-1] if text.endswith('\n') else text)
|
| -
|
| - def SetArguments(self, args):
|
| - self._args = args
|
| -
|
| - def PassThroughArgument(self, id_):
|
| - self._pass_through_id = id_
|
| -
|
| - def __repr__(self):
|
| - return '{{+%s}}' % self._id
|
| -
|
| -_TOKENS = {}
|
| -
|
| -class _Token(object):
|
| - '''The tokens that can appear in a template.
|
| - '''
|
| - class Data(object):
|
| - def __init__(self, name, text, clazz):
|
| - self.name = name
|
| - self.text = text
|
| - self.clazz = clazz
|
| - _TOKENS[text] = self
|
| -
|
| - def ElseNodeClass(self):
|
| - if self.clazz == _VertedSectionNode:
|
| - return _InvertedSectionNode
|
| - if self.clazz == _InvertedSectionNode:
|
| - return _VertedSectionNode
|
| - return None
|
| -
|
| - def __repr__(self):
|
| - return self.name
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
| -
|
| - OPEN_START_SECTION = Data(
|
| - 'OPEN_START_SECTION' , '{{#', _SectionNode)
|
| - OPEN_START_VERTED_SECTION = Data(
|
| - 'OPEN_START_VERTED_SECTION' , '{{?', _VertedSectionNode)
|
| - OPEN_START_INVERTED_SECTION = Data(
|
| - 'OPEN_START_INVERTED_SECTION', '{{^', _InvertedSectionNode)
|
| - OPEN_ASSERTION = Data(
|
| - 'OPEN_ASSERTION' , '{{!', _AssertionNode)
|
| - OPEN_JSON = Data(
|
| - 'OPEN_JSON' , '{{*', _JsonNode)
|
| - OPEN_PARTIAL = Data(
|
| - 'OPEN_PARTIAL' , '{{+', _PartialNode)
|
| - OPEN_ELSE = Data(
|
| - 'OPEN_ELSE' , '{{:', None)
|
| - OPEN_END_SECTION = Data(
|
| - 'OPEN_END_SECTION' , '{{/', None)
|
| - INLINE_END_SECTION = Data(
|
| - 'INLINE_END_SECTION' , '/}}', None)
|
| - OPEN_UNESCAPED_VARIABLE = Data(
|
| - 'OPEN_UNESCAPED_VARIABLE' , '{{{', _UnescapedVariableNode)
|
| - CLOSE_MUSTACHE3 = Data(
|
| - 'CLOSE_MUSTACHE3' , '}}}', None)
|
| - OPEN_COMMENT = Data(
|
| - 'OPEN_COMMENT' , '{{-', _CommentNode)
|
| - CLOSE_COMMENT = Data(
|
| - 'CLOSE_COMMENT' , '-}}', None)
|
| - OPEN_VARIABLE = Data(
|
| - 'OPEN_VARIABLE' , '{{' , _EscapedVariableNode)
|
| - CLOSE_MUSTACHE = Data(
|
| - 'CLOSE_MUSTACHE' , '}}' , None)
|
| - CHARACTER = Data(
|
| - 'CHARACTER' , '.' , None)
|
| -
|
| -class _TokenStream(object):
|
| - '''Tokeniser for template parsing.
|
| - '''
|
| - def __init__(self, string):
|
| - self.next_token = None
|
| - self.next_line = 1
|
| - self.next_column = 0
|
| - self._string = string
|
| - self._cursor = 0
|
| - self.Advance()
|
| -
|
| - def HasNext(self):
|
| - return self.next_token is not None
|
| -
|
| - def NextCharacter(self):
|
| - if self.next_token is _Token.CHARACTER:
|
| - return self._string[self._cursor - 1]
|
| - return None
|
| -
|
| - def Advance(self):
|
| - if self._cursor > 0 and self._string[self._cursor - 1] == '\n':
|
| - self.next_line += 1
|
| - self.next_column = 0
|
| - elif self.next_token is not None:
|
| - self.next_column += len(self.next_token.text)
|
| -
|
| - self.next_token = None
|
| -
|
| - if self._cursor == len(self._string):
|
| - return None
|
| - assert self._cursor < len(self._string)
|
| -
|
| - if (self._cursor + 1 < len(self._string) and
|
| - self._string[self._cursor + 1] in '{}'):
|
| - self.next_token = (
|
| - _TOKENS.get(self._string[self._cursor:self._cursor+3]) or
|
| - _TOKENS.get(self._string[self._cursor:self._cursor+2]))
|
| -
|
| - if self.next_token is None:
|
| - self.next_token = _Token.CHARACTER
|
| -
|
| - self._cursor += len(self.next_token.text)
|
| - return self
|
| -
|
| - def AdvanceOver(self, token, description=None):
|
| - parse_error = None
|
| - if not self.next_token:
|
| - parse_error = 'Reached EOF but expected %s' % token.name
|
| - elif self.next_token is not token:
|
| - parse_error = 'Expecting token %s but got %s at line %s' % (
|
| - token.name, self.next_token.name, self.next_line)
|
| - if parse_error:
|
| - parse_error += ' %s' % description or ''
|
| - raise ParseException(parse_error)
|
| - return self.Advance()
|
| -
|
| - def AdvanceOverSeparator(self, char, description=None):
|
| - self.SkipWhitespace()
|
| - next_char = self.NextCharacter()
|
| - if next_char != char:
|
| - parse_error = 'Expected \'%s\'. got \'%s\'' % (char, next_char)
|
| - if description is not None:
|
| - parse_error += ' (%s)' % description
|
| - raise ParseException(parse_error)
|
| - self.AdvanceOver(_Token.CHARACTER)
|
| - self.SkipWhitespace()
|
| -
|
| - def AdvanceOverNextString(self, excluded=''):
|
| - start = self._cursor - len(self.next_token.text)
|
| - while (self.next_token is _Token.CHARACTER and
|
| - # Can use -1 here because token length of CHARACTER is 1.
|
| - self._string[self._cursor - 1] not in excluded):
|
| - self.Advance()
|
| - end = self._cursor - (len(self.next_token.text) if self.next_token else 0)
|
| - return self._string[start:end]
|
| -
|
| - def AdvanceToNextWhitespace(self):
|
| - return self.AdvanceOverNextString(excluded=' \n\r\t')
|
| -
|
| - def SkipWhitespace(self):
|
| - while (self.next_token is _Token.CHARACTER and
|
| - # Can use -1 here because token length of CHARACTER is 1.
|
| - self._string[self._cursor - 1] in ' \n\r\t'):
|
| - self.Advance()
|
| -
|
| - def __repr__(self):
|
| - return '%s(next_token=%s, remainder=%s)' % (type(self).__name__,
|
| - self.next_token,
|
| - self._string[self._cursor:])
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
| -
|
| -class Handlebar(object):
|
| - '''A handlebar template.
|
| - '''
|
| - def __init__(self, template, name=None):
|
| - self.source = template
|
| - self._name = name
|
| - tokens = _TokenStream(template)
|
| - self._top_node = self._ParseSection(tokens)
|
| - if not self._top_node:
|
| - raise ParseException('Template is empty')
|
| - if tokens.HasNext():
|
| - raise ParseException('There are still tokens remaining at %s, '
|
| - 'was there an end-section without a start-section?' %
|
| - tokens.next_line)
|
| -
|
| - def _ParseSection(self, tokens):
|
| - nodes = []
|
| - while tokens.HasNext():
|
| - if tokens.next_token in (_Token.OPEN_END_SECTION,
|
| - _Token.OPEN_ELSE):
|
| - # Handled after running parseSection within the SECTION cases, so this
|
| - # is a terminating condition. If there *is* an orphaned
|
| - # OPEN_END_SECTION, it will be caught by noticing that there are
|
| - # leftover tokens after termination.
|
| - break
|
| - elif tokens.next_token in (_Token.CLOSE_MUSTACHE,
|
| - _Token.CLOSE_MUSTACHE3):
|
| - raise ParseException('Orphaned %s at line %s' % (tokens.next_token.name,
|
| - tokens.next_line))
|
| - nodes += self._ParseNextOpenToken(tokens)
|
| -
|
| - for i, node in enumerate(nodes):
|
| - if isinstance(node, _StringNode):
|
| - continue
|
| -
|
| - previous_node = nodes[i - 1] if i > 0 else None
|
| - next_node = nodes[i + 1] if i < len(nodes) - 1 else None
|
| - rendered_node = None
|
| -
|
| - if node.GetStartLine() != node.GetEndLine():
|
| - rendered_node = _BlockNode(node)
|
| - if previous_node:
|
| - previous_node.TrimEndingSpaces()
|
| - if next_node:
|
| - next_node.TrimStartingNewLine()
|
| - elif ((not previous_node or previous_node.EndsWithEmptyLine()) and
|
| - (not next_node or next_node.StartsWithNewLine())):
|
| - indentation = 0
|
| - if previous_node:
|
| - indentation = previous_node.TrimEndingSpaces()
|
| - if next_node:
|
| - next_node.TrimStartingNewLine()
|
| - rendered_node = _IndentedNode(node, indentation)
|
| - else:
|
| - rendered_node = _InlineNode(node)
|
| -
|
| - nodes[i] = rendered_node
|
| -
|
| - if len(nodes) == 0:
|
| - return None
|
| - if len(nodes) == 1:
|
| - return nodes[0]
|
| - return _NodeCollection(nodes)
|
| -
|
| - def _ParseNextOpenToken(self, tokens):
|
| - next_token = tokens.next_token
|
| -
|
| - if next_token is _Token.CHARACTER:
|
| - # Plain strings.
|
| - start_line = tokens.next_line
|
| - string = tokens.AdvanceOverNextString()
|
| - return [_StringNode(string, start_line, tokens.next_line)]
|
| - elif next_token in (_Token.OPEN_VARIABLE,
|
| - _Token.OPEN_UNESCAPED_VARIABLE,
|
| - _Token.OPEN_JSON):
|
| - # Inline nodes that don't take arguments.
|
| - tokens.Advance()
|
| - close_token = (_Token.CLOSE_MUSTACHE3
|
| - if next_token is _Token.OPEN_UNESCAPED_VARIABLE else
|
| - _Token.CLOSE_MUSTACHE)
|
| - id_ = self._NextIdentifier(tokens)
|
| - tokens.AdvanceOver(close_token)
|
| - return [next_token.clazz(id_)]
|
| - elif next_token is _Token.OPEN_ASSERTION:
|
| - # Inline nodes that take arguments.
|
| - tokens.Advance()
|
| - id_ = self._NextIdentifier(tokens)
|
| - node = next_token.clazz(id_, tokens.AdvanceOverNextString())
|
| - tokens.AdvanceOver(_Token.CLOSE_MUSTACHE)
|
| - return [node]
|
| - elif next_token in (_Token.OPEN_PARTIAL,
|
| - _Token.OPEN_START_SECTION,
|
| - _Token.OPEN_START_VERTED_SECTION,
|
| - _Token.OPEN_START_INVERTED_SECTION):
|
| - # Block nodes, though they may have inline syntax like {{#foo bar /}}.
|
| - tokens.Advance()
|
| - bind_to, id_ = None, self._NextIdentifier(tokens)
|
| - if tokens.NextCharacter() == ':':
|
| - # This section has the format {{#bound:id}} as opposed to just {{id}}.
|
| - # That is, |id_| is actually the identifier to bind what the section
|
| - # is producing, not the identifier of where to find that content.
|
| - tokens.AdvanceOverSeparator(':')
|
| - bind_to, id_ = id_, self._NextIdentifier(tokens)
|
| - partial_args = None
|
| - if next_token is _Token.OPEN_PARTIAL:
|
| - partial_args = self._ParsePartialNodeArgs(tokens)
|
| - if tokens.next_token is not _Token.CLOSE_MUSTACHE:
|
| - # Inline syntax for partial types.
|
| - if bind_to is not None:
|
| - raise ParseException(
|
| - 'Cannot bind %s to a self-closing partial' % bind_to)
|
| - tokens.AdvanceOver(_Token.INLINE_END_SECTION)
|
| - partial_node = _PartialNode.Inline(id_)
|
| - partial_node.SetArguments(partial_args)
|
| - return [partial_node]
|
| - elif tokens.next_token is not _Token.CLOSE_MUSTACHE:
|
| - # Inline syntax for non-partial types. Support select node types:
|
| - # variables, partials, JSON.
|
| - line, column = tokens.next_line, (tokens.next_column + 1)
|
| - name = tokens.AdvanceToNextWhitespace()
|
| - clazz = _UnescapedVariableNode
|
| - if name.startswith('*'):
|
| - clazz = _JsonNode
|
| - elif name.startswith('+'):
|
| - clazz = _PartialNode.Inline
|
| - if clazz is not _UnescapedVariableNode:
|
| - name = name[1:]
|
| - column += 1
|
| - inline_node = clazz(_Identifier(name, line, column))
|
| - if isinstance(inline_node, _PartialNode):
|
| - inline_node.SetArguments(self._ParsePartialNodeArgs(tokens))
|
| - if bind_to is not None:
|
| - inline_node.PassThroughArgument(bind_to)
|
| - tokens.SkipWhitespace()
|
| - tokens.AdvanceOver(_Token.INLINE_END_SECTION)
|
| - return [next_token.clazz(bind_to, id_, inline_node)]
|
| - # Block syntax.
|
| - tokens.AdvanceOver(_Token.CLOSE_MUSTACHE)
|
| - section = self._ParseSection(tokens)
|
| - else_node_class = next_token.ElseNodeClass() # may not have one
|
| - else_section = None
|
| - if (else_node_class is not None and
|
| - tokens.next_token is _Token.OPEN_ELSE):
|
| - self._OpenElse(tokens, id_)
|
| - else_section = self._ParseSection(tokens)
|
| - self._CloseSection(tokens, id_)
|
| - nodes = []
|
| - if section is not None:
|
| - node = next_token.clazz(bind_to, id_, section)
|
| - if partial_args:
|
| - node.SetArguments(partial_args)
|
| - nodes.append(node)
|
| - if else_section is not None:
|
| - nodes.append(else_node_class(bind_to, id_, else_section))
|
| - return nodes
|
| - elif next_token is _Token.OPEN_COMMENT:
|
| - # Comments.
|
| - start_line = tokens.next_line
|
| - self._AdvanceOverComment(tokens)
|
| - return [_CommentNode(start_line, tokens.next_line)]
|
| -
|
| - def _AdvanceOverComment(self, tokens):
|
| - tokens.AdvanceOver(_Token.OPEN_COMMENT)
|
| - depth = 1
|
| - while tokens.HasNext() and depth > 0:
|
| - if tokens.next_token is _Token.OPEN_COMMENT:
|
| - depth += 1
|
| - elif tokens.next_token is _Token.CLOSE_COMMENT:
|
| - depth -= 1
|
| - tokens.Advance()
|
| -
|
| - def _CloseSection(self, tokens, id_):
|
| - tokens.AdvanceOver(_Token.OPEN_END_SECTION,
|
| - description='to match %s' % id_.GetDescription())
|
| - next_string = tokens.AdvanceOverNextString()
|
| - if next_string != '' and next_string != id_.name:
|
| - raise ParseException(
|
| - 'Start section %s doesn\'t match end %s' % (id_, next_string))
|
| - tokens.AdvanceOver(_Token.CLOSE_MUSTACHE)
|
| -
|
| - def _OpenElse(self, tokens, id_):
|
| - tokens.AdvanceOver(_Token.OPEN_ELSE)
|
| - next_string = tokens.AdvanceOverNextString()
|
| - if next_string != '' and next_string != id_.name:
|
| - raise ParseException(
|
| - 'Start section %s doesn\'t match else %s' % (id_, next_string))
|
| - tokens.AdvanceOver(_Token.CLOSE_MUSTACHE)
|
| -
|
| - def _ParsePartialNodeArgs(self, tokens):
|
| - args = {}
|
| - tokens.SkipWhitespace()
|
| - while (tokens.next_token is _Token.CHARACTER and
|
| - tokens.NextCharacter() != ')'):
|
| - key = tokens.AdvanceOverNextString(excluded=':')
|
| - tokens.AdvanceOverSeparator(':')
|
| - if tokens.NextCharacter() == '(':
|
| - tokens.AdvanceOverSeparator('(')
|
| - inner_id = self._NextIdentifier(tokens)
|
| - inner_args = self._ParsePartialNodeArgs(tokens)
|
| - tokens.AdvanceOverSeparator(')')
|
| - args[key] = {inner_id: inner_args}
|
| - else:
|
| - args[key] = self._NextIdentifier(tokens)
|
| - return args or None
|
| -
|
| - def _NextIdentifier(self, tokens):
|
| - tokens.SkipWhitespace()
|
| - column_start = tokens.next_column + 1
|
| - id_ = _Identifier(tokens.AdvanceOverNextString(excluded=' \n\r\t:()'),
|
| - tokens.next_line,
|
| - column_start)
|
| - tokens.SkipWhitespace()
|
| - return id_
|
| -
|
| - def Render(self, *user_contexts):
|
| - '''Renders this template given a variable number of contexts to read out
|
| - values from (such as those appearing in {{foo}}).
|
| - '''
|
| - internal_context = _InternalContext()
|
| - contexts = list(user_contexts)
|
| - contexts.append({
|
| - '_': internal_context,
|
| - 'false': False,
|
| - 'true': True,
|
| - })
|
| - render_state = _RenderState(self._name or '<root>', _Contexts(contexts))
|
| - internal_context.SetRenderState(render_state)
|
| - self._top_node.Render(render_state)
|
| - return render_state.GetResult()
|
| -
|
| - def render(self, *contexts):
|
| - return self.Render(*contexts)
|
| -
|
| - def __eq__(self, other):
|
| - return self.source == other.source and self._name == other._name
|
| -
|
| - def __ne__(self, other):
|
| - return not (self == other)
|
| -
|
| - def __repr__(self):
|
| - return str('%s(%s)' % (type(self).__name__, self._top_node))
|
| -
|
| - def __str__(self):
|
| - return repr(self)
|
|
|