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