summaryrefslogtreecommitdiff
path: root/chromium/third_party/handlebar
diff options
context:
space:
mode:
authorAndras Becsi <andras.becsi@digia.com>2014-03-18 13:16:26 +0100
committerFrederik Gladhorn <frederik.gladhorn@digia.com>2014-03-20 15:55:39 +0100
commit3f0f86b0caed75241fa71c95a5d73bc0164348c5 (patch)
tree92b9fb00f2e9e90b0be2262093876d4f43b6cd13 /chromium/third_party/handlebar
parente90d7c4b152c56919d963987e2503f9909a666d2 (diff)
downloadqtwebengine-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.chromium4
-rw-r--r--chromium/third_party/handlebar/README.md100
-rw-r--r--chromium/third_party/handlebar/handlebar.py628
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)