# 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: 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 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)' % ( self.__class__.__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 GetTopLocal(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) if key == '@': found = self._nodes[-1]._value else: 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 _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 _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 __repr__(self): return self.name def __str__(self): return repr(self) class _Line(object): def __init__(self, number): self.number = number def __repr__(self): return str(self.number) def __str__(self): return repr(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): 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 class _DecoratorNode(object): 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 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() def Render(self, render_state): self._content.Render(render_state) class _NodeCollection(object): 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) def __str__(self): return repr(self) class _StringNode(object): ''' 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 def __str__(self): return repr(self) 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 def __str__(self): return repr(self) 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 def __str__(self): return repr(self) 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 '' def __str__(self): return repr(self) 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: # 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: render_state.AddResolutionError(self._id) def __repr__(self): return '{{#%s}}%s{{/%s}}' % ( self._id, _DecoratorNode.__repr__(self), self._id) def __str__(self): return repr(self) 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() def __repr__(self): return '{{?%s}}%s{{/%s}}' % ( self._id, _DecoratorNode.__repr__(self), self._id) def __str__(self): return repr(self) @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}} ... {{/}} ''' 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) def __repr__(self): return '{{^%s}}%s{{/%s}}' % ( self._id, _DecoratorNode.__repr__(self), self._id) def __str__(self): return repr(self) 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 def __str__(self): return repr(self) 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, 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): render_state.AddResolutionError(self._id) return partial_render_state = render_state.ForkPartial(value._name, self._id) # 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) 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) 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) value._top_node.Render(partial_render_state) 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] = id_ def SetLocalContext(self, id_): self._local_context_id = id_ def __repr__(self): return '{{+%s}}' % self._id def __str__(self): return repr(self) _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 raise ValueError('%s cannot have an else clause.' % self.clazz) 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.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.next_token is not None 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.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): if self.next_token != token: raise ParseException( '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, 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 (isinstance(node, _LeafNode) and (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: 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.next_token is _Token.OPEN_COMMENT: depth += 1 elif tokens.next_token is _Token.CLOSE_COMMENT: depth -= 1 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: 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 %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 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 '' render_state = _RenderState(name, _Contexts(contexts)) self._top_node.Render(render_state) return render_state.GetResult() def render(self, *contexts): return self.Render(*contexts) def __repr__(self): return str('%s(%s)' % (self.__class__.__name__, self._top_node)) def __str__(self): return repr(self)