| Index: third_party/handlebar/handlebar.py
|
| diff --git a/third_party/handlebar/handlebar.py b/third_party/handlebar/handlebar.py
|
| index c5a2c101629ab0ed9e25444a9d053d941982b7bd..cd381b9c43afd2f00c37940967d90dfa73face63 100644
|
| --- a/third_party/handlebar/handlebar.py
|
| +++ b/third_party/handlebar/handlebar.py
|
| @@ -12,54 +12,68 @@
|
| # See the License for the specific language governing permissions and
|
| # limitations under the License.
|
|
|
| +# TODO: Some character other than {{{ }}} to print unescaped content?
|
| +# TODO: Only have @ while in a loop, and only defined in the top context of
|
| +# the loop.
|
| +# TODO: Consider trimming spaces around identifers like {{?t foo}}.
|
| +# TODO: Only transfer global contexts into partials, not the top local.
|
| +# TODO: Pragmas for asserting the presence of variables.
|
| +# TODO: Escaping control characters somehow. e.g. \{{, \{{-.
|
| +# TODO: Dump warnings-so-far into the output.
|
| +
|
| import json
|
| import re
|
|
|
| -""" Handlebar templates are mostly-logicless templates inspired by ctemplate
|
| -(or more specifically mustache templates) then taken in their own direction
|
| -because I found those to be inadequate.
|
| +'''Handlebar templates are data binding templates more-than-loosely inspired by
|
| +ctemplate. Use like:
|
|
|
| -from handlebar import Handlebar
|
| + from handlebar import Handlebar
|
|
|
| -template = Handlebar('hello {{#foo}}{{bar}}{{/}} world')
|
| -input = {
|
| - 'foo': [
|
| - { 'bar': 1 },
|
| - { 'bar': 2 },
|
| - { 'bar': 3 }
|
| - ]
|
| -}
|
| -print(template.render(input).text)
|
| + 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 (e.g. something that populates values lazily from keys) just add
|
| -a get() method.
|
| +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
|
| + class CustomContext(object):
|
| + def get(self, key):
|
| + return 10
|
| + print(Handlebar('hello {{world}}').render(CustomContext()).text)
|
|
|
| -# Any time {{ }} is used, will fill it with 10.
|
| -print(Handlebar('hello {{world}}').render(CustomContext()).text)
|
| -"""
|
| +will print 'hello 10'.
|
| +'''
|
|
|
| class ParseException(Exception):
|
| - """ Exception thrown while parsing the template.
|
| - """
|
| - def __init__(self, error, line):
|
| - Exception.__init__(self, "%s (line %s)" % (error, line.number))
|
| + '''The exception thrown while parsing a template.
|
| + '''
|
| + def __init__(self, error):
|
| + Exception.__init__(self, error)
|
|
|
| class RenderResult(object):
|
| - """ Result of a render operation.
|
| - """
|
| + '''The result of a render operation.
|
| + '''
|
| def __init__(self, text, errors):
|
| self.text = text;
|
| self.errors = errors
|
|
|
| -class StringBuilder(object):
|
| - """ Mimics Java's StringBuilder for easy porting from the Java version of
|
| - this file to Python.
|
| - """
|
| + def __str__(self):
|
| + return self.text
|
| +
|
| + def __repr__(self):
|
| + return '%s(text=%s, errors=%s)' % (
|
| + self.__class__.__name__, self.text, self.errors)
|
| +
|
| +class _StringBuilder(object):
|
| + '''Efficiently builds strings.
|
| + '''
|
| def __init__(self):
|
| self._buf = []
|
|
|
| @@ -67,680 +81,843 @@ class StringBuilder(object):
|
| self._Collapse()
|
| return len(self._buf[0])
|
|
|
| - def append(self, string):
|
| + def Append(self, string):
|
| + if not isinstance(string, basestring):
|
| + string = str(string)
|
| self._buf.append(string)
|
|
|
| - def toString(self):
|
| + def ToString(self):
|
| self._Collapse()
|
| return self._buf[0]
|
|
|
| def __str__(self):
|
| - return self.toString()
|
| + return self.ToString()
|
|
|
| def _Collapse(self):
|
| self._buf = [u''.join(self._buf)]
|
|
|
| -class RenderState(object):
|
| - """ The state of a render call.
|
| - """
|
| - def __init__(self, globalContexts, localContexts):
|
| - self.globalContexts = globalContexts
|
| - self.localContexts = localContexts
|
| - self.text = StringBuilder()
|
| - self.errors = []
|
| - self._errorsDisabled = False
|
| -
|
| - def inSameContext(self):
|
| - return RenderState(self.globalContexts, self.localContexts)
|
| -
|
| - def getFirstContext(self):
|
| - if len(self.localContexts) > 0:
|
| - return self.localContexts[0]
|
| - if len(self.globalContexts) > 0:
|
| - return self.globalContexts[0]
|
| - return None
|
| -
|
| - def disableErrors(self):
|
| - self._errorsDisabled = True
|
| - return 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 __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 GetTopLocal(self):
|
| + if len(self._nodes) == self._first_local:
|
| + return None
|
| + return self._nodes[-1]._value
|
|
|
| - def addError(self, *messages):
|
| - if self._errorsDisabled:
|
| - return self
|
| - buf = StringBuilder()
|
| - for message in messages:
|
| - buf.append(str(message))
|
| - self.errors.append(buf.toString())
|
| - return self
|
| + 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)
|
|
|
| - def getResult(self):
|
| - return RenderResult(self.text.toString(), self.errors);
|
| -
|
| -class Identifier(object):
|
| - """ An identifier of the form "@", "foo.bar.baz", or "@.foo.bar.baz".
|
| - """
|
| - def __init__(self, name, line):
|
| - self._isThis = (name == '@')
|
| - if self._isThis:
|
| - self._startsWithThis = False
|
| - self._path = []
|
| - return
|
| + if key == '@':
|
| + found = self._nodes[-1]._value
|
| + else:
|
| + found = self._FindNodeValue(key)
|
|
|
| - thisDot = '@.'
|
| - self._startsWithThis = name.startswith(thisDot)
|
| - if self._startsWithThis:
|
| - name = name[len(thisDot):]
|
| -
|
| - if not re.match('^[a-zA-Z0-9._\\-/]+$', name):
|
| - raise ParseException(name + " is not a valid identifier", line)
|
| - self._path = name.split('.')
|
| -
|
| - def resolve(self, renderState):
|
| - if self._isThis:
|
| - return renderState.getFirstContext()
|
| -
|
| - if self._startsWithThis:
|
| - return self._resolveFromContext(renderState.getFirstContext())
|
| -
|
| - resolved = self._resolveFromContexts(renderState.localContexts)
|
| - if resolved is None:
|
| - resolved = self._resolveFromContexts(renderState.globalContexts)
|
| - if resolved is None:
|
| - renderState.addError("Couldn't resolve identifier ", self._path)
|
| - return resolved
|
| -
|
| - def _resolveFromContexts(self, contexts):
|
| - for context in contexts:
|
| - resolved = self._resolveFromContext(context)
|
| - if resolved is not None:
|
| - return resolved
|
| - return None
|
| -
|
| - def _resolveFromContext(self, context):
|
| - result = context
|
| - for next in self._path:
|
| - # Only require that contexts provide a get method, meaning that callers
|
| - # can provide dict-like contexts (for example, to populate values lazily).
|
| - if result is None or not getattr(result, "get", None):
|
| + if tail is None:
|
| + return found
|
| +
|
| + for part in tail.split('.'):
|
| + if not hasattr(found, 'get'):
|
| return None
|
| - result = result.get(next)
|
| - return result
|
| + found = found.get(part)
|
| + return found
|
| +
|
| + 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[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 _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_):
|
| + self._errors.append(
|
| + id_.CreateResolutionErrorMessage(self._name, stack=self._stack))
|
| +
|
| + 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.bar.baz', or '@.foo.bar.baz'.
|
| + '''
|
| + 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 part != '@' and not re.match('^[a-zA-Z0-9_/-]+$', 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 stack.entries:
|
| + message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(),
|
| + entry.name))
|
| + return message.ToString()
|
|
|
| def __str__(self):
|
| - if self._isThis:
|
| - return '@'
|
| - name = '.'.join(self._path)
|
| - return ('@.' + name) if self._startsWithThis else name
|
| + raise ValueError()
|
|
|
| -class Line(object):
|
| +class _Line(object):
|
| def __init__(self, number):
|
| self.number = number
|
|
|
| -class LeafNode(object):
|
| - def __init__(self, line):
|
| - self._line = line
|
| + def __str__(self):
|
| + return str(self.number)
|
|
|
| - def startsWithNewLine(self):
|
| +class _LeafNode(object):
|
| + 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):
|
| + def TrimStartingNewLine(self):
|
| pass
|
|
|
| - def trimEndingSpaces(self):
|
| + def TrimEndingSpaces(self):
|
| return 0
|
|
|
| - def trimEndingNewLine(self):
|
| + def TrimEndingNewLine(self):
|
| pass
|
|
|
| - def endsWithEmptyLine(self):
|
| + def EndsWithEmptyLine(self):
|
| return False
|
|
|
| - def getStartLine(self):
|
| - return self._line
|
| + def GetStartLine(self):
|
| + return self._start_line
|
|
|
| - def getEndLine(self):
|
| - return self._line
|
| + def GetEndLine(self):
|
| + return self._end_line
|
|
|
| -class DecoratorNode(object):
|
| +class _DecoratorNode(object):
|
| def __init__(self, content):
|
| self._content = content
|
|
|
| - def startsWithNewLine(self):
|
| - return self._content.startsWithNewLine()
|
| + def StartsWithNewLine(self):
|
| + return self._content.StartsWithNewLine()
|
|
|
| - def trimStartingNewLine(self):
|
| - self._content.trimStartingNewLine()
|
| + def TrimStartingNewLine(self):
|
| + self._content.TrimStartingNewLine()
|
|
|
| - def trimEndingSpaces(self):
|
| - return self._content.trimEndingSpaces()
|
| + def TrimEndingSpaces(self):
|
| + return self._content.TrimEndingSpaces()
|
|
|
| - def trimEndingNewLine(self):
|
| - self._content.trimEndingNewLine()
|
| + def TrimEndingNewLine(self):
|
| + self._content.TrimEndingNewLine()
|
|
|
| - def endsWithEmptyLine(self):
|
| - return self._content.endsWithEmptyLine()
|
| + def EndsWithEmptyLine(self):
|
| + return self._content.EndsWithEmptyLine()
|
|
|
| - def getStartLine(self):
|
| - return self._content.getStartLine()
|
| + def GetStartLine(self):
|
| + return self._content.GetStartLine()
|
|
|
| - def getEndLine(self):
|
| - return self._content.getEndLine()
|
| + def GetEndLine(self):
|
| + return self._content.GetEndLine()
|
|
|
| -class InlineNode(DecoratorNode):
|
| +class _InlineNode(_DecoratorNode):
|
| def __init__(self, content):
|
| - DecoratorNode.__init__(self, content)
|
| -
|
| - def render(self, renderState):
|
| - contentRenderState = renderState.inSameContext()
|
| - self._content.render(contentRenderState)
|
| + _DecoratorNode.__init__(self, content)
|
|
|
| - renderState.errors.extend(contentRenderState.errors)
|
| - renderState.text.append(
|
| - contentRenderState.text.toString().replace('\n', ''))
|
| + 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):
|
| +class _IndentedNode(_DecoratorNode):
|
| def __init__(self, content, indentation):
|
| - DecoratorNode.__init__(self, content)
|
| + _DecoratorNode.__init__(self, content)
|
| self._indent_str = ' ' * indentation
|
|
|
| - def render(self, renderState):
|
| - contentRenderState = renderState.inSameContext()
|
| - self._content.render(contentRenderState)
|
| -
|
| - renderState.errors.extend(contentRenderState.errors)
|
| - renderState.text.append(self._indent_str)
|
| - # TODO: this might introduce an extra \n at the end? need test.
|
| - renderState.text.append(
|
| - contentRenderState.text.toString().replace('\n',
|
| - '\n' + self._indent_str))
|
| - renderState.text.append('\n')
|
| -
|
| -class BlockNode(DecoratorNode):
|
| + def Render(self, render_state):
|
| + if isinstance(self._content, _CommentNode):
|
| + return
|
| + content_render_state = render_state.Copy()
|
| + self._content.Render(content_render_state)
|
| + def AddIndentation(text):
|
| + buf = _StringBuilder()
|
| + buf.Append(self._indent_str)
|
| + buf.Append(text.replace('\n', '\n%s' % self._indent_str))
|
| + buf.Append('\n')
|
| + return buf.ToString()
|
| + render_state.Merge(content_render_state, text_transform=AddIndentation)
|
| +
|
| +class _BlockNode(_DecoratorNode):
|
| def __init__(self, content):
|
| - DecoratorNode.__init__(self, content)
|
| - content.trimStartingNewLine()
|
| - content.trimEndingSpaces()
|
| + _DecoratorNode.__init__(self, content)
|
| + content.TrimStartingNewLine()
|
| + content.TrimEndingSpaces()
|
|
|
| - def render(self, renderState):
|
| - self._content.render(renderState)
|
| + def Render(self, render_state):
|
| + self._content.Render(render_state)
|
|
|
| -class NodeCollection(object):
|
| +class _NodeCollection(object):
|
| def __init__(self, nodes):
|
| - if len(nodes) == 0:
|
| - raise ValueError()
|
| + assert nodes
|
| self._nodes = nodes
|
|
|
| - def render(self, renderState):
|
| + def Render(self, render_state):
|
| for node in self._nodes:
|
| - node.render(renderState)
|
| + node.Render(render_state)
|
|
|
| - def startsWithNewLine(self):
|
| - return self._nodes[0].startsWithNewLine()
|
| + def StartsWithNewLine(self):
|
| + return self._nodes[0].StartsWithNewLine()
|
|
|
| - def trimStartingNewLine(self):
|
| - self._nodes[0].trimStartingNewLine()
|
| + def TrimStartingNewLine(self):
|
| + self._nodes[0].TrimStartingNewLine()
|
|
|
| - def trimEndingSpaces(self):
|
| - return self._nodes[-1].trimEndingSpaces()
|
| + def TrimEndingSpaces(self):
|
| + return self._nodes[-1].TrimEndingSpaces()
|
|
|
| - def trimEndingNewLine(self):
|
| - self._nodes[-1].trimEndingNewLine()
|
| + def TrimEndingNewLine(self):
|
| + self._nodes[-1].TrimEndingNewLine()
|
|
|
| - def endsWithEmptyLine(self):
|
| - return self._nodes[-1].endsWithEmptyLine()
|
| + def EndsWithEmptyLine(self):
|
| + return self._nodes[-1].EndsWithEmptyLine()
|
|
|
| - def getStartLine(self):
|
| - return self._nodes[0].getStartLine()
|
| + def GetStartLine(self):
|
| + return self._nodes[0].GetStartLine()
|
|
|
| - def getEndLine(self):
|
| - return self._nodes[-1].getEndLine()
|
| + def GetEndLine(self):
|
| + return self._nodes[-1].GetEndLine()
|
|
|
| -class StringNode(object):
|
| - """ Just a string.
|
| - """
|
| - def __init__(self, string, startLine, endLine):
|
| +class _StringNode(object):
|
| + ''' Just a string.
|
| + '''
|
| + def __init__(self, string, start_line, end_line):
|
| self._string = string
|
| - self._startLine = startLine
|
| - self._endLine = endLine
|
| + self._start_line = start_line
|
| + self._end_line = end_line
|
|
|
| - def render(self, renderState):
|
| - renderState.text.append(self._string)
|
| + def Render(self, render_state):
|
| + render_state.text.Append(self._string)
|
|
|
| - def startsWithNewLine(self):
|
| + def StartsWithNewLine(self):
|
| return self._string.startswith('\n')
|
|
|
| - def trimStartingNewLine(self):
|
| - if self.startsWithNewLine():
|
| + def TrimStartingNewLine(self):
|
| + if self.StartsWithNewLine():
|
| self._string = self._string[1:]
|
|
|
| - def trimEndingSpaces(self):
|
| - originalLength = len(self._string)
|
| - self._string = self._string[:self._lastIndexOfSpaces()]
|
| - return originalLength - len(self._string)
|
| + def TrimEndingSpaces(self):
|
| + original_length = len(self._string)
|
| + self._string = self._string[:self._LastIndexOfSpaces()]
|
| + return original_length - len(self._string)
|
|
|
| - def trimEndingNewLine(self):
|
| + def TrimEndingNewLine(self):
|
| if self._string.endswith('\n'):
|
| self._string = self._string[:len(self._string) - 1]
|
|
|
| - def endsWithEmptyLine(self):
|
| - index = self._lastIndexOfSpaces()
|
| + def EndsWithEmptyLine(self):
|
| + index = self._LastIndexOfSpaces()
|
| return index == 0 or self._string[index - 1] == '\n'
|
|
|
| - def _lastIndexOfSpaces(self):
|
| + def _LastIndexOfSpaces(self):
|
| index = len(self._string)
|
| while index > 0 and self._string[index - 1] == ' ':
|
| index -= 1
|
| return index
|
|
|
| - def getStartLine(self):
|
| - return self._startLine
|
| + def GetStartLine(self):
|
| + return self._start_line
|
|
|
| - def getEndLine(self):
|
| - return self._endLine
|
| + def GetEndLine(self):
|
| + return self._end_line
|
|
|
| -class EscapedVariableNode(LeafNode):
|
| - """ {{foo}}
|
| - """
|
| - def __init__(self, id, line):
|
| - LeafNode.__init__(self, line)
|
| - self._id = id
|
| +class _EscapedVariableNode(_LeafNode):
|
| + ''' {{foo}}
|
| + '''
|
| + def __init__(self, id_):
|
| + _LeafNode.__init__(self, id_.line, id_.line)
|
| + self._id = id_
|
|
|
| - def render(self, renderState):
|
| - value = self._id.resolve(renderState)
|
| + 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)
|
| - renderState.text.append(string.replace('&', '&')
|
| - .replace('<', '<')
|
| - .replace('>', '>'))
|
| -
|
| -class UnescapedVariableNode(LeafNode):
|
| - """ {{{foo}}}
|
| - """
|
| - def __init__(self, id, line):
|
| - LeafNode.__init__(self, line)
|
| - self._id = id
|
| -
|
| - def render(self, renderState):
|
| - value = self._id.resolve(renderState)
|
| - if value is None:
|
| - return
|
| - renderState.text.append(
|
| - value if isinstance(value, basestring) else str(value))
|
| -
|
| -class SectionNode(DecoratorNode):
|
| - """ {{#foo}} ... {{/}}
|
| - """
|
| - def __init__(self, id, content):
|
| - DecoratorNode.__init__(self, content)
|
| - self._id = id
|
| -
|
| - def render(self, renderState):
|
| - value = self._id.resolve(renderState)
|
| + render_state.text.Append(string.replace('&', '&')
|
| + .replace('<', '<')
|
| + .replace('>', '>'))
|
| +
|
| +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)
|
| +
|
| +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
|
|
|
| +class _SectionNode(_DecoratorNode):
|
| + ''' {{#foo}} ... {{/}}
|
| + '''
|
| + def __init__(self, id_, content):
|
| + _DecoratorNode.__init__(self, content)
|
| + self._id = id_
|
| +
|
| + def Render(self, render_state):
|
| + value = render_state.contexts.Resolve(self._id.name)
|
| if isinstance(value, list):
|
| for item in value:
|
| - renderState.localContexts.insert(0, item)
|
| - self._content.render(renderState)
|
| - renderState.localContexts.pop(0)
|
| - elif isinstance(value, dict):
|
| - renderState.localContexts.insert(0, value)
|
| - self._content.render(renderState)
|
| - renderState.localContexts.pop(0)
|
| + # Always push context, even if it's not "valid", since we want to
|
| + # be able to refer to items in a list such as [1,2,3] via @.
|
| + render_state.contexts.Push(item)
|
| + self._content.Render(render_state)
|
| + render_state.contexts.Pop()
|
| + elif hasattr(value, 'get'):
|
| + render_state.contexts.Push(value)
|
| + self._content.Render(render_state)
|
| + render_state.contexts.Pop()
|
| else:
|
| - renderState.addError("{{#", self._id,
|
| - "}} cannot be rendered with a ", type(value))
|
| -
|
| -class VertedSectionNode(DecoratorNode):
|
| - """ {{?foo}} ... {{/}}
|
| - """
|
| - def __init__(self, id, content):
|
| - DecoratorNode.__init__(self, content)
|
| - self._id = id
|
| -
|
| - def render(self, renderState):
|
| - value = self._id.resolve(renderState.inSameContext().disableErrors())
|
| - if _VertedSectionNodeShouldRender(value):
|
| - renderState.localContexts.insert(0, value)
|
| - self._content.render(renderState)
|
| - renderState.localContexts.pop(0)
|
| -
|
| -def _VertedSectionNodeShouldRender(value):
|
| - if value is None:
|
| - return False
|
| - if isinstance(value, bool):
|
| - return value
|
| - if (isinstance(value, int) or
|
| - isinstance(value, long) or
|
| - isinstance(value, float)):
|
| - return True
|
| - if isinstance(value, basestring):
|
| - return True
|
| - if isinstance(value, list):
|
| - return len(value) > 0
|
| - if isinstance(value, dict):
|
| + render_state.AddResolutionError(self._id)
|
| +
|
| +class _VertedSectionNode(_DecoratorNode):
|
| + ''' {{?foo}} ... {{/}}
|
| + '''
|
| + def __init__(self, id_, content):
|
| + _DecoratorNode.__init__(self, content)
|
| + self._id = id_
|
| +
|
| + def Render(self, render_state):
|
| + value = render_state.contexts.Resolve(self._id.name)
|
| + if _VertedSectionNode.ShouldRender(value):
|
| + render_state.contexts.Push(value)
|
| + self._content.Render(render_state)
|
| + render_state.contexts.Pop()
|
| +
|
| + @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
|
| - raise TypeError("Unhandled type %s" % type(value))
|
| -
|
| -class InvertedSectionNode(DecoratorNode):
|
| - """ {{^foo}} ... {{/}}
|
| - """
|
| - def __init__(self, id, content):
|
| - DecoratorNode.__init__(self, content)
|
| - self._id = id
|
| -
|
| - def render(self, renderState):
|
| - value = self._id.resolve(renderState.inSameContext().disableErrors())
|
| - if not _VertedSectionNodeShouldRender(value):
|
| - self._content.render(renderState)
|
| -
|
| -class JsonNode(LeafNode):
|
| - """ {{*foo}}
|
| - """
|
| - def __init__(self, id, line):
|
| - LeafNode.__init__(self, line)
|
| - self._id = id
|
| -
|
| - def render(self, renderState):
|
| - value = self._id.resolve(renderState)
|
| +
|
| +class _InvertedSectionNode(_DecoratorNode):
|
| + ''' {{^foo}} ... {{/}}
|
| + '''
|
| + def __init__(self, id_, content):
|
| + _DecoratorNode.__init__(self, content)
|
| + 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)
|
| +
|
| +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
|
| - renderState.text.append(json.dumps(value, separators=(',',':')))
|
| -
|
| -class PartialNode(LeafNode):
|
| - """ {{+foo}}
|
| - """
|
| - def __init__(self, id, line):
|
| - LeafNode.__init__(self, line)
|
| - self._id = id
|
| + render_state.text.Append(json.dumps(value, separators=(',',':')))
|
| +
|
| +class _PartialNode(_LeafNode):
|
| + ''' {{+foo}}
|
| + '''
|
| + def __init__(self, id_):
|
| + _LeafNode.__init__(self, id_.line, id_.line)
|
| + self._id = id_
|
| self._args = None
|
| + self._local_context_id = None
|
|
|
| - def render(self, renderState):
|
| - value = self._id.resolve(renderState)
|
| + 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):
|
| - renderState.addError(self._id, " didn't resolve to a Handlebar")
|
| + render_state.AddResolutionError(self._id)
|
| return
|
|
|
| - argContext = []
|
| - if len(renderState.localContexts) > 0:
|
| - argContext.append(renderState.localContexts[0])
|
| + partial_render_state = render_state.ForkPartial(value._name, self._id)
|
|
|
| - if self._args:
|
| - argContextMap = {}
|
| - for key, valueId in self._args.items():
|
| - context = valueId.resolve(renderState)
|
| - if context:
|
| - argContextMap[key] = context
|
| - argContext.append(argContextMap)
|
| + # TODO: Don't do this. Force callers to do this by specifying an @ argument.
|
| + top_local = render_state.contexts.GetTopLocal()
|
| + if top_local is not None:
|
| + partial_render_state.contexts.Push(top_local)
|
|
|
| - partialRenderState = RenderState(renderState.globalContexts, argContext)
|
| - value._topNode.render(partialRenderState)
|
| + if self._args is not None:
|
| + arg_context = {}
|
| + for key, value_id in self._args.items():
|
| + context = render_state.contexts.Resolve(value_id.name)
|
| + if context is not None:
|
| + arg_context[key] = context
|
| + partial_render_state.contexts.Push(arg_context)
|
|
|
| - text = partialRenderState.text.toString()
|
| - if len(text) > 0 and text[-1] == '\n':
|
| - text = text[:-1]
|
| + if self._local_context_id is not None:
|
| + local_context = render_state.contexts.Resolve(self._local_context_id.name)
|
| + if local_context is not None:
|
| + partial_render_state.contexts.Push(local_context)
|
|
|
| - renderState.text.append(text)
|
| - renderState.errors.extend(partialRenderState.errors)
|
| + value._top_node.Render(partial_render_state)
|
|
|
| - def addArgument(self, key, valueId):
|
| - if not self._args:
|
| + render_state.Merge(
|
| + partial_render_state,
|
| + text_transform=lambda text: text[:-1] if text.endswith('\n') else text)
|
| +
|
| + def AddArgument(self, key, id_):
|
| + if self._args is None:
|
| self._args = {}
|
| - self._args[key] = valueId
|
| + self._args[key] = id_
|
| +
|
| + def SetLocalContext(self, id_):
|
| + self._local_context_id = id_
|
|
|
| -# List of tokens in order of longest to shortest, to avoid any prefix matching
|
| -# issues.
|
| -TokenValues = []
|
| +_TOKENS = {}
|
|
|
| -class Token(object):
|
| - """ The tokens that can appear in a template.
|
| - """
|
| +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
|
| - TokenValues.append(self)
|
| -
|
| - def elseNodeClass(self):
|
| - if self.clazz == VertedSectionNode:
|
| - return InvertedSectionNode
|
| - if self.clazz == InvertedSectionNode:
|
| - return VertedSectionNode
|
| - raise ValueError(self.clazz + " can not have an else clause.")
|
| -
|
| - 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_START_JSON = Data("OPEN_START_JSON" , "{{*", JsonNode)
|
| - OPEN_START_PARTIAL = Data("OPEN_START_PARTIAL" , "{{+", PartialNode)
|
| - OPEN_ELSE = Data("OPEN_ELSE" , "{{:", None)
|
| - OPEN_END_SECTION = Data("OPEN_END_SECTION" , "{{/", None)
|
| - OPEN_UNESCAPED_VARIABLE = Data("OPEN_UNESCAPED_VARIABLE" , "{{{", UnescapedVariableNode)
|
| - CLOSE_MUSTACHE3 = Data("CLOSE_MUSTACHE3" , "}}}", None)
|
| - OPEN_COMMENT = Data("OPEN_COMMENT" , "{{-", None)
|
| - 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.
|
| - """
|
| + _TOKENS[text] = self
|
| +
|
| + def ElseNodeClass(self):
|
| + if self.clazz == _VertedSectionNode:
|
| + return _InvertedSectionNode
|
| + if self.clazz == _InvertedSectionNode:
|
| + return _VertedSectionNode
|
| + raise ValueError('%s cannot have an else clause.' % self.clazz)
|
| +
|
| + def __str__(self):
|
| + return '%s(%s)' % (self.name, self.text)
|
| +
|
| + 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_START_JSON = Data('OPEN_START_JSON' , '{{*', _JsonNode)
|
| + OPEN_START_PARTIAL = Data('OPEN_START_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._remainder = string
|
| -
|
| - self.nextToken = None
|
| - self.nextContents = None
|
| - self.nextLine = Line(1)
|
| - self.advance()
|
| + self.next_token = None
|
| + self.next_line = _Line(1)
|
| + self.next_column = 0
|
| + self._string = string
|
| + self._cursor = 0
|
| + self.Advance()
|
|
|
| - def hasNext(self):
|
| - return self.nextToken is not None
|
| + def HasNext(self):
|
| + return self.next_token is not None
|
|
|
| - def advance(self):
|
| - if self.nextContents == '\n':
|
| - self.nextLine = Line(self.nextLine.number + 1)
|
| + def Advance(self):
|
| + if self._cursor > 0 and self._string[self._cursor - 1] == '\n':
|
| + self.next_line = _Line(self.next_line.number + 1)
|
| + self.next_column = 0
|
| + elif self.next_token is not None:
|
| + self.next_column += len(self.next_token.text)
|
|
|
| - self.nextToken = None
|
| - self.nextContents = None
|
| + self.next_token = None
|
|
|
| - if self._remainder == '':
|
| + if self._cursor == len(self._string):
|
| return None
|
| + assert self._cursor < len(self._string)
|
|
|
| - for token in TokenValues:
|
| - if self._remainder.startswith(token.text):
|
| - self.nextToken = token
|
| - break
|
| + 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 not self.nextToken:
|
| - self.nextToken = Token.CHARACTER
|
| + if self.next_token is None:
|
| + self.next_token = _Token.CHARACTER
|
|
|
| - self.nextContents = self._remainder[0:len(self.nextToken.text)]
|
| - self._remainder = self._remainder[len(self.nextToken.text):]
|
| + self._cursor += len(self.next_token.text)
|
| return self
|
|
|
| - def advanceOver(self, token):
|
| - if self.nextToken != token:
|
| + def AdvanceOver(self, token):
|
| + if self.next_token != token:
|
| raise ParseException(
|
| - "Expecting token " + token.name + " but got " + self.nextToken.name,
|
| - self.nextLine)
|
| - return self.advance()
|
| -
|
| - def advanceOverNextString(self, excluded=''):
|
| - buf = StringBuilder()
|
| - while self.nextToken == Token.CHARACTER and \
|
| - excluded.find(self.nextContents) == -1:
|
| - buf.append(self.nextContents)
|
| - self.advance()
|
| - return buf.toString()
|
| -
|
| - def advanceToNextWhitespace(self):
|
| - return self.advanceOverNextString(excluded=' \n\r\t')
|
| -
|
| - def skipWhitespace(self):
|
| - while len(self.nextContents) > 0 and \
|
| - ' \n\r\t'.find(self.nextContents) >= 0:
|
| - self.advance()
|
| + 'Expecting token %s but got %s at line %s' % (token.name,
|
| + self.next_token.name,
|
| + self.next_line))
|
| + return self.Advance()
|
| +
|
| + 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()
|
|
|
| class Handlebar(object):
|
| - """ A handlebar template.
|
| - """
|
| - def __init__(self, template):
|
| + ''' A handlebar template.
|
| + '''
|
| + def __init__(self, template, name=None):
|
| self.source = template
|
| - tokens = TokenStream(template)
|
| - self._topNode = self._parseSection(tokens)
|
| - if not self._topNode:
|
| - raise ParseException("Template is empty", tokens.nextLine)
|
| - if tokens.hasNext():
|
| - raise ParseException("There are still tokens remaining, "
|
| - "was there an end-section without a start-section:",
|
| - tokens.nextLine)
|
| -
|
| - def _parseSection(self, tokens):
|
| + 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 = []
|
| - sectionEnded = False
|
| -
|
| - while tokens.hasNext() and not sectionEnded:
|
| - token = tokens.nextToken
|
| -
|
| - if token == Token.CHARACTER:
|
| - startLine = tokens.nextLine
|
| - string = tokens.advanceOverNextString()
|
| - nodes.append(StringNode(string, startLine, tokens.nextLine))
|
| - elif token == Token.OPEN_VARIABLE or \
|
| - token == Token.OPEN_UNESCAPED_VARIABLE or \
|
| - token == Token.OPEN_START_JSON:
|
| - id = self._openSectionOrTag(tokens)
|
| - nodes.append(token.clazz(id, tokens.nextLine))
|
| - elif token == Token.OPEN_START_PARTIAL:
|
| - tokens.advance()
|
| - id = Identifier(tokens.advanceToNextWhitespace(),
|
| - tokens.nextLine)
|
| - partialNode = PartialNode(id, tokens.nextLine)
|
| -
|
| - while tokens.nextToken == Token.CHARACTER:
|
| - tokens.skipWhitespace()
|
| - key = tokens.advanceOverNextString(excluded=':')
|
| - tokens.advance()
|
| - partialNode.addArgument(
|
| - key,
|
| - Identifier(tokens.advanceToNextWhitespace(),
|
| - tokens.nextLine))
|
| -
|
| - tokens.advanceOver(Token.CLOSE_MUSTACHE)
|
| - nodes.append(partialNode)
|
| - elif token == Token.OPEN_START_SECTION:
|
| - id = self._openSectionOrTag(tokens)
|
| - section = self._parseSection(tokens)
|
| - self._closeSection(tokens, id)
|
| - if section:
|
| - nodes.append(SectionNode(id, section))
|
| - elif token == Token.OPEN_START_VERTED_SECTION or \
|
| - token == Token.OPEN_START_INVERTED_SECTION:
|
| - id = self._openSectionOrTag(tokens)
|
| - section = self._parseSection(tokens)
|
| - elseSection = None
|
| - if tokens.nextToken == Token.OPEN_ELSE:
|
| - self._openElse(tokens, id)
|
| - elseSection = self._parseSection(tokens)
|
| - self._closeSection(tokens, id)
|
| - if section:
|
| - nodes.append(token.clazz(id, section))
|
| - if elseSection:
|
| - nodes.append(token.elseNodeClass()(id, elseSection))
|
| - elif token == Token.OPEN_COMMENT:
|
| - self._advanceOverComment(tokens)
|
| - elif token == Token.OPEN_END_SECTION or \
|
| - token == 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.
|
| - sectionEnded = True
|
| - elif Token.CLOSE_MUSTACHE:
|
| - raise ParseException("Orphaned " + tokens.nextToken.name,
|
| - tokens.nextLine)
|
| + 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):
|
| + if isinstance(node, _StringNode):
|
| continue
|
|
|
| - previousNode = nodes[i - 1] if i > 0 else None
|
| - nextNode = nodes[i + 1] if i < len(nodes) - 1 else None
|
| - renderedNode = None
|
| -
|
| - if node.getStartLine() != node.getEndLine():
|
| - renderedNode = BlockNode(node)
|
| - if previousNode:
|
| - previousNode.trimEndingSpaces()
|
| - if nextNode:
|
| - nextNode.trimStartingNewLine()
|
| - elif isinstance(node, LeafNode) and \
|
| - (not previousNode or previousNode.endsWithEmptyLine()) and \
|
| - (not nextNode or nextNode.startsWithNewLine()):
|
| + 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 (isinstance(node, _LeafNode) and
|
| + (not previous_node or previous_node.EndsWithEmptyLine()) and
|
| + (not next_node or next_node.StartsWithNewLine())):
|
| indentation = 0
|
| - if previousNode:
|
| - indentation = previousNode.trimEndingSpaces()
|
| - if nextNode:
|
| - nextNode.trimStartingNewLine()
|
| - renderedNode = IndentedNode(node, indentation)
|
| + if previous_node:
|
| + indentation = previous_node.TrimEndingSpaces()
|
| + if next_node:
|
| + next_node.TrimStartingNewLine()
|
| + rendered_node = _IndentedNode(node, indentation)
|
| else:
|
| - renderedNode = InlineNode(node)
|
| + rendered_node = _InlineNode(node)
|
|
|
| - nodes[i] = renderedNode
|
| + nodes[i] = rendered_node
|
|
|
| if len(nodes) == 0:
|
| return None
|
| if len(nodes) == 1:
|
| return nodes[0]
|
| - return NodeCollection(nodes)
|
| -
|
| - def _advanceOverComment(self, tokens):
|
| - tokens.advanceOver(Token.OPEN_COMMENT)
|
| + return _NodeCollection(nodes)
|
| +
|
| + def _ParseNextOpenToken(self, tokens):
|
| + next_token = tokens.next_token
|
| +
|
| + if next_token is _Token.CHARACTER:
|
| + 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_START_JSON):
|
| + id_, inline_value_id = self._OpenSectionOrTag(tokens)
|
| + if inline_value_id is not None:
|
| + raise ParseException(
|
| + '%s cannot have an inline value' % id_.GetDescription())
|
| + return [next_token.clazz(id_)]
|
| + elif next_token is _Token.OPEN_START_PARTIAL:
|
| + tokens.Advance()
|
| + column_start = tokens.next_column + 1
|
| + id_ = _Identifier(tokens.AdvanceToNextWhitespace(),
|
| + tokens.next_line,
|
| + column_start)
|
| + partial_node = _PartialNode(id_)
|
| + while tokens.next_token is _Token.CHARACTER:
|
| + tokens.SkipWhitespace()
|
| + key = tokens.AdvanceOverNextString(excluded=':')
|
| + tokens.Advance()
|
| + column_start = tokens.next_column + 1
|
| + id_ = _Identifier(tokens.AdvanceToNextWhitespace(),
|
| + tokens.next_line,
|
| + column_start)
|
| + if key == '@':
|
| + partial_node.SetLocalContext(id_)
|
| + else:
|
| + partial_node.AddArgument(key, id_)
|
| + tokens.AdvanceOver(_Token.CLOSE_MUSTACHE)
|
| + return [partial_node]
|
| + elif next_token is _Token.OPEN_START_SECTION:
|
| + id_, inline_node = self._OpenSectionOrTag(tokens)
|
| + nodes = []
|
| + if inline_node is None:
|
| + section = self._ParseSection(tokens)
|
| + self._CloseSection(tokens, id_)
|
| + nodes = []
|
| + if section is not None:
|
| + nodes.append(_SectionNode(id_, section))
|
| + else:
|
| + nodes.append(_SectionNode(id_, inline_node))
|
| + return nodes
|
| + elif next_token in (_Token.OPEN_START_VERTED_SECTION,
|
| + _Token.OPEN_START_INVERTED_SECTION):
|
| + id_, inline_node = self._OpenSectionOrTag(tokens)
|
| + nodes = []
|
| + if inline_node is None:
|
| + section = self._ParseSection(tokens)
|
| + else_section = None
|
| + if tokens.next_token is _Token.OPEN_ELSE:
|
| + self._OpenElse(tokens, id_)
|
| + else_section = self._ParseSection(tokens)
|
| + self._CloseSection(tokens, id_)
|
| + if section:
|
| + nodes.append(next_token.clazz(id_, section))
|
| + if else_section:
|
| + nodes.append(next_token.ElseNodeClass()(id_, else_section))
|
| + else:
|
| + nodes.append(next_token.clazz(id_, inline_node))
|
| + return nodes
|
| + elif next_token is _Token.OPEN_COMMENT:
|
| + 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.nextToken == Token.OPEN_COMMENT:
|
| + while tokens.HasNext() and depth > 0:
|
| + if tokens.next_token is _Token.OPEN_COMMENT:
|
| depth += 1
|
| - elif tokens.nextToken == Token.CLOSE_COMMENT:
|
| + elif tokens.next_token is _Token.CLOSE_COMMENT:
|
| depth -= 1
|
| - tokens.advance()
|
| -
|
| - def _openSectionOrTag(self, tokens):
|
| - openToken = tokens.nextToken
|
| - tokens.advance()
|
| - id = Identifier(tokens.advanceOverNextString(), tokens.nextLine)
|
| - if openToken == Token.OPEN_UNESCAPED_VARIABLE:
|
| - tokens.advanceOver(Token.CLOSE_MUSTACHE3)
|
| + tokens.Advance()
|
| +
|
| + def _OpenSectionOrTag(self, tokens):
|
| + def NextIdentifierArgs():
|
| + tokens.SkipWhitespace()
|
| + line = tokens.next_line
|
| + column = tokens.next_column + 1
|
| + name = tokens.AdvanceToNextWhitespace()
|
| + tokens.SkipWhitespace()
|
| + return (name, line, column)
|
| + close_token = (_Token.CLOSE_MUSTACHE3
|
| + if tokens.next_token is _Token.OPEN_UNESCAPED_VARIABLE else
|
| + _Token.CLOSE_MUSTACHE)
|
| + tokens.Advance()
|
| + id_ = _Identifier(*NextIdentifierArgs())
|
| + if tokens.next_token is close_token:
|
| + tokens.AdvanceOver(close_token)
|
| + inline_node = None
|
| else:
|
| - tokens.advanceOver(Token.CLOSE_MUSTACHE)
|
| - return id
|
| -
|
| - def _closeSection(self, tokens, id):
|
| - tokens.advanceOver(Token.OPEN_END_SECTION)
|
| - nextString = tokens.advanceOverNextString()
|
| - if nextString != '' and nextString != str(id):
|
| + name, line, column = NextIdentifierArgs()
|
| + tokens.AdvanceOver(_Token.INLINE_END_SECTION)
|
| + # Support select other types of nodes, the most useful being partial.
|
| + clazz = _UnescapedVariableNode
|
| + if name.startswith('*'):
|
| + clazz = _JsonNode
|
| + elif name.startswith('+'):
|
| + clazz = _PartialNode
|
| + if clazz is not _UnescapedVariableNode:
|
| + name = name[1:]
|
| + column += 1
|
| + inline_node = clazz(_Identifier(name, line, column))
|
| + return (id_, inline_node)
|
| +
|
| + def _CloseSection(self, tokens, id_):
|
| + tokens.AdvanceOver(_Token.OPEN_END_SECTION)
|
| + next_string = tokens.AdvanceOverNextString()
|
| + if next_string != '' and next_string != id_.name:
|
| raise ParseException(
|
| - "Start section " + str(id) + " doesn't match end " + nextString)
|
| - tokens.advanceOver(Token.CLOSE_MUSTACHE)
|
| + '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)
|
| - nextString = tokens.advanceOverNextString()
|
| - if nextString != '' and nextString != str(id):
|
| + 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 " + str(id) + " doesn't match else " + nextString)
|
| - tokens.advanceOver(Token.CLOSE_MUSTACHE)
|
| + 'Start section %s doesn\'t match else %s' % (id_, next_string))
|
| + tokens.AdvanceOver(_Token.CLOSE_MUSTACHE)
|
| +
|
| + def Render(self, *contexts):
|
| + '''Renders this template given a variable number of contexts to read out
|
| + values from (such as those appearing in {{foo}}).
|
| + '''
|
| + name = self._name or '<root>'
|
| + render_state = _RenderState(name, _Contexts(contexts))
|
| + self._top_node.Render(render_state)
|
| + return render_state.GetResult()
|
|
|
| def render(self, *contexts):
|
| - """ Renders this template given a variable number of "contexts" to read
|
| - out values from (such as those appearing in {{foo}}).
|
| - """
|
| - globalContexts = []
|
| - for context in contexts:
|
| - globalContexts.append(context)
|
| - renderState = RenderState(globalContexts, [])
|
| - self._topNode.render(renderState)
|
| - return renderState.getResult()
|
| + return self.Render(*contexts)
|
|
|