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) |