Index: third_party/handlebar/handlebar.py |
diff --git a/third_party/handlebar/handlebar.py b/third_party/handlebar/handlebar.py |
index 0be089c178d9616c9c4b7cdfbecc46d47d29618b..f35764f4b8277cbc663739079d375ea886d753bd 100644 |
--- a/third_party/handlebar/handlebar.py |
+++ b/third_party/handlebar/handlebar.py |
@@ -12,14 +12,8 @@ |
# 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: New name, not "handlebar". |
# TODO: Escaping control characters somehow. e.g. \{{, \{{-. |
-# TODO: Dump warnings-so-far into the output. |
import json |
import re |
@@ -29,7 +23,7 @@ ctemplate. Use like: |
from handlebar import Handlebar |
- template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') |
+ template = Handlebar('hello {{#foo bar/}} world') |
input = { |
'foo': [ |
{ 'bar': 1 }, |
@@ -65,8 +59,9 @@ class RenderResult(object): |
self.errors = errors |
def __repr__(self): |
- return '%s(text=%s, errors=%s)' % ( |
- self.__class__.__name__, self.text, self.errors) |
+ return '%s(text=%s, errors=%s)' % (type(self).__name__, |
+ self.text, |
+ self.errors) |
def __str__(self): |
return repr(self) |
@@ -160,30 +155,26 @@ class _Contexts(object): |
# [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) |
- |
+ 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. |
@@ -225,6 +216,19 @@ class _Stack(object): |
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. |
''' |
@@ -235,9 +239,11 @@ class _RenderState(object): |
self._errors = [] |
self._stack = _stack |
- def AddResolutionError(self, id_): |
- self._errors.append( |
- id_.CreateResolutionErrorMessage(self._name, stack=self._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( |
@@ -260,8 +266,10 @@ class _RenderState(object): |
return RenderResult(self.text.ToString(), self._errors); |
class _Identifier(object): |
- ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'. |
+ '''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 |
@@ -269,7 +277,7 @@ class _Identifier(object): |
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): |
+ if not _Identifier._VALID_ID_MATCHER.match(part): |
raise ParseException('Invalid identifier %s' % self.GetDescription()) |
def GetDescription(self): |
@@ -280,10 +288,10 @@ class _Identifier(object): |
message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), |
name)) |
if stack is not None: |
- for entry in stack.entries: |
+ for entry in reversed(stack.entries): |
message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), |
entry.name)) |
- return message.ToString() |
+ return message.ToString().strip() |
def __repr__(self): |
return self.name |
@@ -291,17 +299,9 @@ class _Identifier(object): |
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 _Node(object): pass |
-class _LeafNode(object): |
+class _LeafNode(_Node): |
def __init__(self, start_line, end_line): |
self._start_line = start_line |
self._end_line = end_line |
@@ -327,7 +327,10 @@ class _LeafNode(object): |
def GetEndLine(self): |
return self._end_line |
-class _DecoratorNode(object): |
+ def __str__(self): |
+ return repr(self) |
+ |
+class _DecoratorNode(_Node): |
def __init__(self, content): |
self._content = content |
@@ -355,8 +358,8 @@ class _DecoratorNode(object): |
def __repr__(self): |
return str(self._content) |
- def __str__(self): |
- return repr(self) |
+ def __str__(self): |
+ return repr(self) |
class _InlineNode(_DecoratorNode): |
def __init__(self, content): |
@@ -376,15 +379,18 @@ class _IndentedNode(_DecoratorNode): |
def Render(self, render_state): |
if isinstance(self._content, _CommentNode): |
return |
- content_render_state = render_state.Copy() |
- self._content.Render(content_render_state) |
- def AddIndentation(text): |
+ 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)) |
- buf.Append('\n') |
+ if not text.endswith('\n'): # partials will often already end in a \n |
+ buf.Append('\n') |
return buf.ToString() |
- render_state.Merge(content_render_state, text_transform=AddIndentation) |
+ 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): |
@@ -395,7 +401,7 @@ class _BlockNode(_DecoratorNode): |
def Render(self, render_state): |
self._content.Render(render_state) |
-class _NodeCollection(object): |
+class _NodeCollection(_Node): |
def __init__(self, nodes): |
assert nodes |
self._nodes = nodes |
@@ -428,11 +434,8 @@ class _NodeCollection(object): |
def __repr__(self): |
return ''.join(str(node) for node in self._nodes) |
- def __str__(self): |
- return repr(self) |
- |
-class _StringNode(object): |
- ''' Just a string. |
+class _StringNode(_Node): |
+ '''Just a string. |
''' |
def __init__(self, string, start_line, end_line): |
self._string = string |
@@ -477,11 +480,8 @@ class _StringNode(object): |
def __repr__(self): |
return self._string |
- def __str__(self): |
- return repr(self) |
- |
class _EscapedVariableNode(_LeafNode): |
- ''' {{foo}} |
+ '''{{foo}} |
''' |
def __init__(self, id_): |
_LeafNode.__init__(self, id_.line, id_.line) |
@@ -500,11 +500,8 @@ class _EscapedVariableNode(_LeafNode): |
def __repr__(self): |
return '{{%s}}' % self._id |
- def __str__(self): |
- return repr(self) |
- |
class _UnescapedVariableNode(_LeafNode): |
- ''' {{{foo}}} |
+ '''{{{foo}}} |
''' |
def __init__(self, id_): |
_LeafNode.__init__(self, id_.line, id_.line) |
@@ -521,9 +518,6 @@ class _UnescapedVariableNode(_LeafNode): |
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. |
@@ -537,29 +531,29 @@ class _CommentNode(_LeafNode): |
def __repr__(self): |
return '<comment>' |
- def __str__(self): |
- return repr(self) |
- |
class _SectionNode(_DecoratorNode): |
- ''' {{#foo}} ... {{/}} |
+ '''{{#var:foo}} ... {{/foo}} |
''' |
- def __init__(self, id_, content): |
+ 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: |
- # 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() |
+ 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'): |
- render_state.contexts.Push(value) |
- self._content.Render(render_state) |
- render_state.contexts.Pop() |
+ 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) |
@@ -567,30 +561,27 @@ class _SectionNode(_DecoratorNode): |
return '{{#%s}}%s{{/%s}}' % ( |
self._id, _DecoratorNode.__repr__(self), self._id) |
- def __str__(self): |
- return repr(self) |
- |
class _VertedSectionNode(_DecoratorNode): |
- ''' {{?foo}} ... {{/}} |
+ '''{{?var:foo}} ... {{/foo}} |
''' |
- def __init__(self, id_, content): |
+ 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): |
- render_state.contexts.Push(value) |
- self._content.Render(render_state) |
- render_state.contexts.Pop() |
+ 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) |
- def __str__(self): |
- return repr(self) |
- |
@staticmethod |
def ShouldRender(value): |
if value is None: |
@@ -602,10 +593,13 @@ class _VertedSectionNode(_DecoratorNode): |
return True |
class _InvertedSectionNode(_DecoratorNode): |
- ''' {{^foo}} ... {{/}} |
+ '''{{^foo}} ... {{/foo}} |
''' |
- def __init__(self, id_, content): |
+ 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): |
@@ -617,11 +611,23 @@ class _InvertedSectionNode(_DecoratorNode): |
return '{{^%s}}%s{{/%s}}' % ( |
self._id, _DecoratorNode.__repr__(self), self._id) |
- def __str__(self): |
- return repr(self) |
+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}} |
+ '''{{*foo}} |
''' |
def __init__(self, id_): |
_LeafNode.__init__(self, id_.line, id_.line) |
@@ -637,71 +643,90 @@ class _JsonNode(_LeafNode): |
def __repr__(self): |
return '{{*%s}}' % self._id |
- def __str__(self): |
- return repr(self) |
- |
class _PartialNode(_LeafNode): |
- ''' {{+foo}} |
+ '''{{+var:foo}} ... {{/foo}} |
''' |
- def __init__(self, id_): |
+ 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._resolved_args = None |
self._args = None |
- self._local_context_id = 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): |
- render_state.AddResolutionError(self._id) |
+ if not isinstance(value, (Handlebar, _Node)): |
+ render_state.AddResolutionError(self._id, description='not a partial') |
return |
- partial_render_state = render_state.ForkPartial(value._name, self._id) |
+ if isinstance(value, Handlebar): |
+ node, name = value._top_node, value._name |
+ else: |
+ node, name = value, None |
- # 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) |
+ 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._resolved_args is not None: |
+ arg_context.update(self._resolved_args) |
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 |
+ def resolve_args(args): |
+ resolved = {} |
+ for key, value in args.iteritems(): |
+ if isinstance(value, dict): |
+ assert len(value.keys()) == 1 |
+ inner_id, inner_args = value.items()[0] |
+ inner_partial = render_state.contexts.Resolve(inner_id.name) |
+ if inner_partial is not None: |
+ context = _PartialNode(None, inner_id, inner_partial) |
+ context.SetResolvedArguments(resolve_args(inner_args)) |
+ resolved[key] = context |
+ 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] = self._content |
+ if arg_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) |
+ 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 SetResolvedArguments(self, args): |
+ self._resolved_args = args |
+ |
+ def SetArguments(self, args): |
+ self._args = args |
- def SetLocalContext(self, id_): |
- self._local_context_id = id_ |
+ def PassThroughArgument(self, id_): |
+ self._pass_through_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. |
+ '''The tokens that can appear in a template. |
''' |
class Data(object): |
def __init__(self, name, text, clazz): |
@@ -715,30 +740,53 @@ class _Token(object): |
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) |
+ 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. |
+ '''Tokeniser for template parsing. |
''' |
def __init__(self, string): |
self.next_token = None |
- self.next_line = _Line(1) |
+ self.next_line = 1 |
self.next_column = 0 |
self._string = string |
self._cursor = 0 |
@@ -747,9 +795,14 @@ class _TokenStream(object): |
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 = _Line(self.next_line.number + 1) |
+ self.next_line += 1 |
self.next_column = 0 |
elif self.next_token is not None: |
self.next_column += len(self.next_token.text) |
@@ -772,14 +825,29 @@ class _TokenStream(object): |
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)) |
+ 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 |
@@ -798,8 +866,16 @@ class _TokenStream(object): |
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. |
+ '''A handlebar template. |
''' |
def __init__(self, template, name=None): |
self.source = template |
@@ -810,8 +886,8 @@ class Handlebar(object): |
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) |
+ 'was there an end-section without a start-section?' % |
+ tokens.next_line) |
def _ParseSection(self, tokens): |
nodes = [] |
@@ -843,8 +919,7 @@ class Handlebar(object): |
previous_node.TrimEndingSpaces() |
if next_node: |
next_node.TrimStartingNewLine() |
- elif (isinstance(node, _LeafNode) and |
- (not previous_node or previous_node.EndsWithEmptyLine()) and |
+ elif ((not previous_node or previous_node.EndsWithEmptyLine()) and |
(not next_node or next_node.StartsWithNewLine())): |
indentation = 0 |
if previous_node: |
@@ -867,69 +942,95 @@ class Handlebar(object): |
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_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()) |
+ _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_START_PARTIAL: |
+ elif next_token is _Token.OPEN_ASSERTION: |
+ # Inline nodes that take arguments. |
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_) |
+ id_ = self._NextIdentifier(tokens) |
+ node = next_token.clazz(id_, tokens.AdvanceOverNextString()) |
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, |
+ return [node] |
+ elif next_token in (_Token.OPEN_PARTIAL, |
+ _Token.OPEN_START_SECTION, |
+ _Token.OPEN_START_VERTED_SECTION, |
_Token.OPEN_START_INVERTED_SECTION): |
- id_, inline_node = self._OpenSectionOrTag(tokens) |
+ # 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 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)) |
+ 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)] |
@@ -944,39 +1045,9 @@ class Handlebar(object): |
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) |
+ 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( |
@@ -991,20 +1062,55 @@ class Handlebar(object): |
'Start section %s doesn\'t match else %s' % (id_, next_string)) |
tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
- def Render(self, *contexts): |
+ 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}}). |
''' |
name = self._name or '<root>' |
- render_state = _RenderState(name, _Contexts(contexts)) |
+ internal_context = _InternalContext() |
+ render_state = _RenderState( |
+ name, _Contexts([{'_': internal_context}] + list(user_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)' % (self.__class__.__name__, self._top_node)) |
+ return str('%s(%s)' % (type(self).__name__, self._top_node)) |
def __str__(self): |
return repr(self) |