diff options
author | Andras Becsi <andras.becsi@digia.com> | 2014-03-18 13:16:26 +0100 |
---|---|---|
committer | Frederik Gladhorn <frederik.gladhorn@digia.com> | 2014-03-20 15:55:39 +0100 |
commit | 3f0f86b0caed75241fa71c95a5d73bc0164348c5 (patch) | |
tree | 92b9fb00f2e9e90b0be2262093876d4f43b6cd13 /chromium/third_party/handlebar | |
parent | e90d7c4b152c56919d963987e2503f9909a666d2 (diff) | |
download | qtwebengine-chromium-3f0f86b0caed75241fa71c95a5d73bc0164348c5.tar.gz |
Update to new stable branch 1750
This also includes an updated ninja and chromium dependencies
needed on Windows.
Change-Id: Icd597d80ed3fa4425933c9f1334c3c2e31291c42
Reviewed-by: Zoltan Arvai <zarvai@inf.u-szeged.hu>
Reviewed-by: Zeno Albisser <zeno.albisser@digia.com>
Diffstat (limited to 'chromium/third_party/handlebar')
-rw-r--r-- | chromium/third_party/handlebar/README.chromium | 4 | ||||
-rw-r--r-- | chromium/third_party/handlebar/README.md | 100 | ||||
-rw-r--r-- | chromium/third_party/handlebar/handlebar.py | 628 |
3 files changed, 390 insertions, 342 deletions
diff --git a/chromium/third_party/handlebar/README.chromium b/chromium/third_party/handlebar/README.chromium index ea454b979fe..06d305b1944 100644 --- a/chromium/third_party/handlebar/README.chromium +++ b/chromium/third_party/handlebar/README.chromium @@ -2,8 +2,8 @@ Name: Handlebar, cross-platform data binding templates. Short Name: handlebar URL: https://github.com/kalman/templates Version: 0 -Date: July 13, 2012 -Revision: commit b00ffd4655708f98092a7ae9203f39c564251ecf +Date: November 16, 2013 +Revision: commit 3e74633948a386d0a77a1c2b1df4c40546f4ec95 License: Apache 2.0 License File: NOT_SHIPPED Security Critical: no diff --git a/chromium/third_party/handlebar/README.md b/chromium/third_party/handlebar/README.md deleted file mode 100644 index 266ef1181fe..00000000000 --- a/chromium/third_party/handlebar/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Handlebar templates - -This repository contains utilities around a templating system that I called "Handlebar". They're a logic-less templating language based on [mustache](http://mustache.github.com/) (obviously) which is itself based on [ctemplate](http://code.google.com/p/ctemplate) apparently. - -The original and reference implementation is written in Java, with a JavaScript (via CoffeeScript) port and a Python port. The tests are cross-platform but controlled from Java with junit; this means that all implementations are hopefully up to date (or else some tests are failing). - -The goal of handlebar is to provide the most complete and convenient way to write logic-less templates across the whole stack of a web application. Write templates once, statically render content on the Java/Python server, then as content is dynamically added render it using the same templates on the JavaScript client. - -## Overview - -A template is text plus "mustache" control characters (thank you mustache for the cool terminology I have adopted). - -A template is rendered by passing it JSON data. I say "JSON" but what I really mean is JSON-like data; the actual input will vary depending on the language bindings (Java uses reflection over properties and maps, JavaScript uses objects, Python uses dictionaries). - -Generally speaking there are two classes of mustaches: "self closing" ones like `{{foo}}` `{{{foo}}}` `{{*foo}}`, and "has children" ones like `{{#foo}}...{{/foo}}` `{{?foo}}...{{/foo}}` `{{^foo}}...{{/foo}}` where the `...` is arbitrary other template data. - -In both cases the `foo` represents a path into the JSON structure, so - - {{foo.bar.baz}} - -is valid for JSON like - - {"foo": {"bar": {"baz": 42 }}} - -(here it would resolve to `42`). - -All libraries also have the behaviour where descending into a section of the JSON will "push" the sub-JSON onto the top of the "context stack", so given JSON like - - {"foo": {"bar": {"foo": 42 }}} - -the template - - {{foo.bar.foo}} {{?foo}}{{bar.foo}} {{?bar}{{foo}}{{/bar}} {{/foo}} - -will correctly resolve all references to be `42`. - -There is an additional identifier `@` representing the "tip" of that context stack, useful when iterating using the `{{#foo}}...{{/foo}}` structure; `{ "list": [1,2,3] }` with `{{#list}} {{@}} {{/list}}` will print ` 1 2 3 `. - -Finally, note that the `{{/foo}}` in `{{#foo}}...{{/foo}}` is actually redundant, and that `{{#foo}}...{{/}}` would be sufficient. Depdencing on the situation one or the other will tend to be more readable (explicitly using `{{/foo}}` will perform more validation). - -## Structures ("mustaches") - -### `{{foo.bar}}` - -Prints out the HTML-escaped value at path `foo.bar`. - -### `{{{foo.bar}}}` - -Prints out value at path `foo.bar` (no escaping). - -### `{{*foo.bar}}}` - -Prints out the JSON serialization of the object at path `foo.bar` (no escaping; this is designed for JavaScript client bootstrapping). - -### `{{+foo.bar}}` - -Inserts the sub-template (aka "partial template") found at path `foo.bar`. Currently, all libraries actually enforce that this is a pre-compiled template (rather than a plain string for example) for efficiency. This lets you do something like: - - template = Handlebar('{{#list}} {{+partial}} {{/}}') - partial = Handlebar('{{foo}}...') - json = { - 'list': [ - { 'foo': 42 }, - { 'foo': 56 }, - { 'foo': 10 } - ] - } - print(template.render(json, {'partial': partial}).text) - > 42... 56... 10... - -Very useful for dynamic web apps, and also just very useful. - -### `{{#foo.bar}}...{{/}}` - -Runs `...` for each item in an array found at path `foo.bar`, or each key/value pair in an object. - -### `{{?foo.bar}}...{{/}}` - -Runs `...` if `foo.bar` resolves to a "value", which is defined based on types. - -* `null` is never considered a value. -* `true` is a valid, `false` isn't. -* Any number other than `0` is a value. -* Any non-empty string is a value. -* Any non-empty array is a value. -* Any non-empty object is a value. - -### `{{^foo.bar}}...{{/}}` - -Runs `...` if `foo.bar` _doesn't_ resolve to a "value". The exact opposite of `{{?foo.bar}}...{{/}}`. - -### `{{:foo.bar}}{{=case1}}...{{=case2}}___{{/}}` - -Ooh a switch statement! Prints `...` if the string found at `foo.bar` is the string `"case1"`, `___` if it's `"case2"`, etc. - -## That's all - -But at the moment this is currently under heavy weekend development. Which is to say a constant trickle of code as I need it. - -Soon: better fault tolerance all round, comments, tidier syntax, less whitespacey output, and moving the Java and JavaScript/CoffeeScript implementations and tests into this repository! And maybe some kind of online playground at some point. diff --git a/chromium/third_party/handlebar/handlebar.py b/chromium/third_party/handlebar/handlebar.py index 0be089c178d..7e06868e193 100644 --- a/chromium/third_party/handlebar/handlebar.py +++ b/chromium/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,7 +155,7 @@ 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): + def FirstLocal(self): if len(self._nodes) == self._first_local: return None return self._nodes[-1]._value @@ -169,21 +164,22 @@ class _Contexts(object): # 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 +221,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 +244,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 +271,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 +282,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 +293,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 +304,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 +332,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 +363,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 +384,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 +406,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 +439,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 +485,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 +505,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 +523,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 +536,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 +566,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 +598,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 +616,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 +648,123 @@ class _JsonNode(_LeafNode): def __repr__(self): return '{{*%s}}' % self._id - def __str__(self): - return repr(self) +# 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): - ''' {{+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._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._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 + 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) - 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 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 +778,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 +833,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 +863,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 +904,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 +924,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 +957,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 +980,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 +1083,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 +1100,59 @@ 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() + 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)' % (self.__class__.__name__, self._top_node)) + return str('%s(%s)' % (type(self).__name__, self._top_node)) def __str__(self): return repr(self) |