diff options
author | kalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-08-22 23:26:25 +0000 |
---|---|---|
committer | kalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-08-22 23:28:26 +0000 |
commit | 68edde91fe6afbf2af183ca3d451c466ca4196a6 (patch) | |
tree | 1723e0534e24327eb83d44eea97c226cab0bdd2f /third_party/motemplate | |
parent | 59b165d93221b2d55d96996dafcf46044b23161c (diff) | |
download | chromium_src-68edde91fe6afbf2af183ca3d451c466ca4196a6.zip chromium_src-68edde91fe6afbf2af183ca3d451c466ca4196a6.tar.gz chromium_src-68edde91fe6afbf2af183ca3d451c466ca4196a6.tar.bz2 |
Rename the docserver third_party dependency "handlebar" to "motemplate", as part
of updating that templating library.
History: this dependency is owned by me, and I changed the name to avoid
confusion with the "handlebars" templating system.
R=yoz@chromium.org, brettw@chromium.org
Review URL: https://codereview.chromium.org/498813002
Cr-Commit-Position: refs/heads/master@{#291550}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@291550 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'third_party/motemplate')
-rw-r--r-- | third_party/motemplate/.gitignore | 1 | ||||
-rw-r--r-- | third_party/motemplate/LICENSE | 202 | ||||
-rw-r--r-- | third_party/motemplate/OWNERS | 1 | ||||
-rw-r--r-- | third_party/motemplate/README.chromium | 12 | ||||
-rw-r--r-- | third_party/motemplate/motemplate.py | 1155 |
5 files changed, 1371 insertions, 0 deletions
diff --git a/third_party/motemplate/.gitignore b/third_party/motemplate/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/third_party/motemplate/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/third_party/motemplate/LICENSE b/third_party/motemplate/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/third_party/motemplate/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/motemplate/OWNERS b/third_party/motemplate/OWNERS new file mode 100644 index 0000000..d3c3af29 --- /dev/null +++ b/third_party/motemplate/OWNERS @@ -0,0 +1 @@ +kalman@chromium.org diff --git a/third_party/motemplate/README.chromium b/third_party/motemplate/README.chromium new file mode 100644 index 0000000..c46c025 --- /dev/null +++ b/third_party/motemplate/README.chromium @@ -0,0 +1,12 @@ +Name: Motemplate, cross-platform data binding templates. +Short Name: motemplate +URL: https://github.com/kalman/templates +Version: 0 +Date: August 22, 2014 +Revision: commit 76237c8ece0272dc02d104eef24b66365b4feaf3 +License: Apache 2.0 +License File: NOT_SHIPPED +Security Critical: no + +Description: +A data binding template language for Python inspired by ctemplate. diff --git a/third_party/motemplate/motemplate.py b/third_party/motemplate/motemplate.py new file mode 100644 index 0000000..397228b --- /dev/null +++ b/third_party/motemplate/motemplate.py @@ -0,0 +1,1155 @@ +# Copyright 2012 Benjamin Kalman +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# TODO: Escaping control characters somehow. e.g. \{{, \{{-. + +import json +import re + +'''Motemplate templates are data binding templates more-than-loosely inspired by +ctemplate. Use like: + + from motemplate import Motemplate + + template = Motemplate('hello {{#foo bar/}} world') + input = { + 'foo': [ + { 'bar': 1 }, + { 'bar': 2 }, + { 'bar': 3 } + ] + } + print(template.render(input).text) + +Motemplate will use get() on contexts to return values, so to create custom +getters (for example, something that populates values lazily from keys), just +provide an object with a get() method. + + class CustomContext(object): + def get(self, key): + return 10 + print(Motemplate('hello {{world}}').render(CustomContext()).text) + +will print 'hello 10'. +''' + +class ParseException(Exception): + '''The exception thrown while parsing a template. + ''' + def __init__(self, error): + Exception.__init__(self, error) + +class RenderResult(object): + '''The result of a render operation. + ''' + def __init__(self, text, errors): + self.text = text; + self.errors = errors + + def __repr__(self): + return '%s(text=%s, errors=%s)' % (type(self).__name__, + self.text, + self.errors) + + def __str__(self): + return repr(self) + +class _StringBuilder(object): + '''Efficiently builds strings. + ''' + def __init__(self): + self._buf = [] + + def __len__(self): + self._Collapse() + return len(self._buf[0]) + + def Append(self, string): + if not isinstance(string, basestring): + string = str(string) + self._buf.append(string) + + def ToString(self): + self._Collapse() + return self._buf[0] + + def _Collapse(self): + self._buf = [u''.join(self._buf)] + + def __repr__(self): + return self.ToString() + + def __str__(self): + return repr(self) + +class _Contexts(object): + '''Tracks a stack of context objects, providing efficient key/value retrieval. + ''' + class _Node(object): + '''A node within the stack. Wraps a real context and maintains the key/value + pairs seen so far. + ''' + def __init__(self, value): + self._value = value + self._value_has_get = hasattr(value, 'get') + self._found = {} + + def GetKeys(self): + '''Returns the list of keys that |_value| contains. + ''' + return self._found.keys() + + def Get(self, key): + '''Returns the value for |key|, or None if not found (including if + |_value| doesn't support key retrieval). + ''' + if not self._value_has_get: + return None + value = self._found.get(key) + if value is not None: + return value + value = self._value.get(key) + if value is not None: + self._found[key] = value + return value + + def __repr__(self): + return 'Node(value=%s, found=%s)' % (self._value, self._found) + + def __str__(self): + return repr(self) + + def __init__(self, globals_): + '''Initializes with the initial global contexts, listed in order from most + to least important. + ''' + self._nodes = map(_Contexts._Node, globals_) + self._first_local = len(self._nodes) + self._value_info = {} + + def CreateFromGlobals(self): + new = _Contexts([]) + new._nodes = self._nodes[:self._first_local] + new._first_local = self._first_local + return new + + def Push(self, context): + self._nodes.append(_Contexts._Node(context)) + + def Pop(self): + node = self._nodes.pop() + assert len(self._nodes) >= self._first_local + for found_key in node.GetKeys(): + # [0] is the stack of nodes that |found_key| has been found in. + self._value_info[found_key][0].pop() + + def FirstLocal(self): + if len(self._nodes) == self._first_local: + return None + return self._nodes[-1]._value + + def Resolve(self, path): + # This method is only efficient at finding |key|; if |tail| has a value (and + # |key| evaluates to an indexable value) we'll need to descend into that. + key, tail = path.split('.', 1) if '.' in path else (path, None) + found = self._FindNodeValue(key) + if tail is None: + return found + for part in tail.split('.'): + if not hasattr(found, 'get'): + return None + found = found.get(part) + return found + + def Scope(self, context, fn, *args): + self.Push(context) + try: + return fn(*args) + finally: + self.Pop() + + def _FindNodeValue(self, key): + # |found_node_list| will be all the nodes that |key| has been found in. + # |checked_node_set| are those that have been checked. + info = self._value_info.get(key) + if info is None: + info = ([], set()) + self._value_info[key] = info + found_node_list, checked_node_set = info + + # Check all the nodes not yet checked for |key|. + newly_found = [] + for node in reversed(self._nodes): + if node in checked_node_set: + break + value = node.Get(key) + if value is not None: + newly_found.append(node) + checked_node_set.add(node) + + # The nodes will have been found in reverse stack order. After extending + # the found nodes, the freshest value will be at the tip of the stack. + found_node_list.extend(reversed(newly_found)) + if not found_node_list: + return None + + return found_node_list[-1]._value.get(key) + +class _Stack(object): + class Entry(object): + def __init__(self, name, id_): + self.name = name + self.id_ = id_ + + def __init__(self, entries=[]): + self.entries = entries + + def Descend(self, name, id_): + descended = list(self.entries) + descended.append(_Stack.Entry(name, id_)) + return _Stack(entries=descended) + +class _InternalContext(object): + def __init__(self): + self._render_state = None + + def SetRenderState(self, render_state): + self._render_state = render_state + + def get(self, key): + if key == 'errors': + errors = self._render_state._errors + return '\n'.join(errors) if errors else None + return None + +class _RenderState(object): + '''The state of a render call. + ''' + def __init__(self, name, contexts, _stack=_Stack()): + self.text = _StringBuilder() + self.contexts = contexts + self._name = name + self._errors = [] + self._stack = _stack + + def AddResolutionError(self, id_, description=None): + message = id_.CreateResolutionErrorMessage(self._name, stack=self._stack) + if description is not None: + message = '%s (%s)' % (message, description) + self._errors.append(message) + + def Copy(self): + return _RenderState( + self._name, self.contexts, _stack=self._stack) + + def ForkPartial(self, custom_name, id_): + name = custom_name or id_.name + return _RenderState(name, + self.contexts.CreateFromGlobals(), + _stack=self._stack.Descend(name, id_)) + + def Merge(self, render_state, text_transform=None): + self._errors.extend(render_state._errors) + text = render_state.text.ToString() + if text_transform is not None: + text = text_transform(text) + self.text.Append(text) + + def GetResult(self): + return RenderResult(self.text.ToString(), self._errors); + +class _Identifier(object): + '''An identifier of the form 'foo', 'foo.bar.baz', 'foo-bar.baz', etc. + ''' + _VALID_ID_MATCHER = re.compile(r'^[a-zA-Z0-9@_/-]+$') + + def __init__(self, name, line, column): + self.name = name + self.line = line + self.column = column + if name == '': + raise ParseException('Empty identifier %s' % self.GetDescription()) + for part in name.split('.'): + if not _Identifier._VALID_ID_MATCHER.match(part): + raise ParseException('Invalid identifier %s' % self.GetDescription()) + + def GetDescription(self): + return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) + + def CreateResolutionErrorMessage(self, name, stack=None): + message = _StringBuilder() + message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), + name)) + if stack is not None: + for entry in reversed(stack.entries): + message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), + entry.name)) + return message.ToString().strip() + + def __repr__(self): + return self.name + + def __str__(self): + return repr(self) + +class _Node(object): pass + +class _LeafNode(_Node): + def __init__(self, start_line, end_line): + self._start_line = start_line + self._end_line = end_line + + def StartsWithNewLine(self): + return False + + def TrimStartingNewLine(self): + pass + + def TrimEndingSpaces(self): + return 0 + + def TrimEndingNewLine(self): + pass + + def EndsWithEmptyLine(self): + return False + + def GetStartLine(self): + return self._start_line + + def GetEndLine(self): + return self._end_line + + def __str__(self): + return repr(self) + +class _DecoratorNode(_Node): + def __init__(self, content): + self._content = content + + def StartsWithNewLine(self): + return self._content.StartsWithNewLine() + + def TrimStartingNewLine(self): + self._content.TrimStartingNewLine() + + def TrimEndingSpaces(self): + return self._content.TrimEndingSpaces() + + def TrimEndingNewLine(self): + self._content.TrimEndingNewLine() + + def EndsWithEmptyLine(self): + return self._content.EndsWithEmptyLine() + + def GetStartLine(self): + return self._content.GetStartLine() + + def GetEndLine(self): + return self._content.GetEndLine() + + def __repr__(self): + return str(self._content) + + def __str__(self): + return repr(self) + +class _InlineNode(_DecoratorNode): + def __init__(self, content): + _DecoratorNode.__init__(self, content) + + def Render(self, render_state): + content_render_state = render_state.Copy() + self._content.Render(content_render_state) + render_state.Merge(content_render_state, + text_transform=lambda text: text.replace('\n', '')) + +class _IndentedNode(_DecoratorNode): + def __init__(self, content, indentation): + _DecoratorNode.__init__(self, content) + self._indent_str = ' ' * indentation + + def Render(self, render_state): + if isinstance(self._content, _CommentNode): + return + def inlinify(text): + if len(text) == 0: # avoid rendering a blank line + return '' + buf = _StringBuilder() + buf.Append(self._indent_str) + buf.Append(text.replace('\n', '\n%s' % self._indent_str)) + if not text.endswith('\n'): # partials will often already end in a \n + buf.Append('\n') + return buf.ToString() + content_render_state = render_state.Copy() + self._content.Render(content_render_state) + render_state.Merge(content_render_state, text_transform=inlinify) + +class _BlockNode(_DecoratorNode): + def __init__(self, content): + _DecoratorNode.__init__(self, content) + content.TrimStartingNewLine() + content.TrimEndingSpaces() + + def Render(self, render_state): + self._content.Render(render_state) + +class _NodeCollection(_Node): + def __init__(self, nodes): + assert nodes + self._nodes = nodes + + def Render(self, render_state): + for node in self._nodes: + node.Render(render_state) + + def StartsWithNewLine(self): + return self._nodes[0].StartsWithNewLine() + + def TrimStartingNewLine(self): + self._nodes[0].TrimStartingNewLine() + + def TrimEndingSpaces(self): + return self._nodes[-1].TrimEndingSpaces() + + def TrimEndingNewLine(self): + self._nodes[-1].TrimEndingNewLine() + + def EndsWithEmptyLine(self): + return self._nodes[-1].EndsWithEmptyLine() + + def GetStartLine(self): + return self._nodes[0].GetStartLine() + + def GetEndLine(self): + return self._nodes[-1].GetEndLine() + + def __repr__(self): + return ''.join(str(node) for node in self._nodes) + +class _StringNode(_Node): + '''Just a string. + ''' + def __init__(self, string, start_line, end_line): + self._string = string + self._start_line = start_line + self._end_line = end_line + + def Render(self, render_state): + render_state.text.Append(self._string) + + def StartsWithNewLine(self): + return self._string.startswith('\n') + + def TrimStartingNewLine(self): + if self.StartsWithNewLine(): + self._string = self._string[1:] + + def TrimEndingSpaces(self): + original_length = len(self._string) + self._string = self._string[:self._LastIndexOfSpaces()] + return original_length - len(self._string) + + def TrimEndingNewLine(self): + if self._string.endswith('\n'): + self._string = self._string[:len(self._string) - 1] + + def EndsWithEmptyLine(self): + index = self._LastIndexOfSpaces() + return index == 0 or self._string[index - 1] == '\n' + + def _LastIndexOfSpaces(self): + index = len(self._string) + while index > 0 and self._string[index - 1] == ' ': + index -= 1 + return index + + def GetStartLine(self): + return self._start_line + + def GetEndLine(self): + return self._end_line + + def __repr__(self): + return self._string + +class _EscapedVariableNode(_LeafNode): + '''{{foo}} + ''' + def __init__(self, id_): + _LeafNode.__init__(self, id_.line, id_.line) + self._id = id_ + + def Render(self, render_state): + value = render_state.contexts.Resolve(self._id.name) + if value is None: + render_state.AddResolutionError(self._id) + return + string = value if isinstance(value, basestring) else str(value) + render_state.text.Append(string.replace('&', '&') + .replace('<', '<') + .replace('>', '>')) + + def __repr__(self): + return '{{%s}}' % self._id + +class _UnescapedVariableNode(_LeafNode): + '''{{{foo}}} + ''' + def __init__(self, id_): + _LeafNode.__init__(self, id_.line, id_.line) + self._id = id_ + + def Render(self, render_state): + value = render_state.contexts.Resolve(self._id.name) + if value is None: + render_state.AddResolutionError(self._id) + return + string = value if isinstance(value, basestring) else str(value) + render_state.text.Append(string) + + def __repr__(self): + return '{{{%s}}}' % self._id + +class _CommentNode(_LeafNode): + '''{{- This is a comment -}} + An empty placeholder node for correct indented rendering behaviour. + ''' + def __init__(self, start_line, end_line): + _LeafNode.__init__(self, start_line, end_line) + + def Render(self, render_state): + pass + + def __repr__(self): + return '<comment>' + +class _SectionNode(_DecoratorNode): + '''{{#var:foo}} ... {{/foo}} + ''' + def __init__(self, bind_to, id_, content): + _DecoratorNode.__init__(self, content) + self._bind_to = bind_to + self._id = id_ + + def Render(self, render_state): + value = render_state.contexts.Resolve(self._id.name) + if isinstance(value, list): + for item in value: + if self._bind_to is not None: + render_state.contexts.Scope({self._bind_to.name: item}, + self._content.Render, render_state) + else: + self._content.Render(render_state) + elif hasattr(value, 'get'): + if self._bind_to is not None: + render_state.contexts.Scope({self._bind_to.name: value}, + self._content.Render, render_state) + else: + render_state.contexts.Scope(value, self._content.Render, render_state) + else: + render_state.AddResolutionError(self._id) + + def __repr__(self): + return '{{#%s}}%s{{/%s}}' % ( + self._id, _DecoratorNode.__repr__(self), self._id) + +class _VertedSectionNode(_DecoratorNode): + '''{{?var:foo}} ... {{/foo}} + ''' + def __init__(self, bind_to, id_, content): + _DecoratorNode.__init__(self, content) + self._bind_to = bind_to + self._id = id_ + + def Render(self, render_state): + value = render_state.contexts.Resolve(self._id.name) + if _VertedSectionNode.ShouldRender(value): + if self._bind_to is not None: + render_state.contexts.Scope({self._bind_to.name: value}, + self._content.Render, render_state) + else: + self._content.Render(render_state) + + def __repr__(self): + return '{{?%s}}%s{{/%s}}' % ( + self._id, _DecoratorNode.__repr__(self), self._id) + + @staticmethod + def ShouldRender(value): + if value is None: + return False + if isinstance(value, bool): + return value + if isinstance(value, list): + return len(value) > 0 + return True + +class _InvertedSectionNode(_DecoratorNode): + '''{{^foo}} ... {{/foo}} + ''' + def __init__(self, bind_to, id_, content): + _DecoratorNode.__init__(self, content) + if bind_to is not None: + raise ParseException('{{^%s:%s}} does not support variable binding' + % (bind_to, id_)) + self._id = id_ + + def Render(self, render_state): + value = render_state.contexts.Resolve(self._id.name) + if not _VertedSectionNode.ShouldRender(value): + self._content.Render(render_state) + + def __repr__(self): + return '{{^%s}}%s{{/%s}}' % ( + self._id, _DecoratorNode.__repr__(self), self._id) + +class _AssertionNode(_LeafNode): + '''{{!foo Some comment about foo}} + ''' + def __init__(self, id_, description): + _LeafNode.__init__(self, id_.line, id_.line) + self._id = id_ + self._description = description + + def Render(self, render_state): + if render_state.contexts.Resolve(self._id.name) is None: + render_state.AddResolutionError(self._id, description=self._description) + + def __repr__(self): + return '{{!%s %s}}' % (self._id, self._description) + +class _JsonNode(_LeafNode): + '''{{*foo}} + ''' + def __init__(self, id_): + _LeafNode.__init__(self, id_.line, id_.line) + self._id = id_ + + def Render(self, render_state): + value = render_state.contexts.Resolve(self._id.name) + if value is None: + render_state.AddResolutionError(self._id) + return + render_state.text.Append(json.dumps(value, separators=(',',':'))) + + def __repr__(self): + return '{{*%s}}' % self._id + +class _PartialNodeWithArguments(_DecoratorNode): + def __init__(self, partial, args): + if isinstance(partial, Motemplate): + # 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, Motemplate): + # Preserve any get() method that the caller has added. + if hasattr(partial, 'get'): + self.get = partial.get + partial = partial._top_node + _DecoratorNode.__init__(self, partial) + self._partial = partial + self._context = context + + def Render(self, render_state): + original_contexts = render_state.contexts + try: + render_state.contexts = self._context + render_state.contexts.Scope( + # The first local context of |original_contexts| will be the + # arguments that were passed to the partial, if any. + original_contexts.FirstLocal() or {}, + self._partial.Render, render_state) + finally: + render_state.contexts = original_contexts + +class _PartialNode(_LeafNode): + '''{{+var:foo}} ... {{/foo}} + ''' + def __init__(self, bind_to, id_, content): + _LeafNode.__init__(self, id_.line, id_.line) + self._bind_to = bind_to + self._id = id_ + self._content = content + self._args = None + self._pass_through_id = None + + @classmethod + def Inline(cls, id_): + return cls(None, id_, None) + + def Render(self, render_state): + value = render_state.contexts.Resolve(self._id.name) + if value is None: + render_state.AddResolutionError(self._id) + return + if not isinstance(value, (Motemplate, _Node)): + render_state.AddResolutionError(self._id, description='not a partial') + return + + if isinstance(value, Motemplate): + node, name = value._top_node, value._name + else: + node, name = value, None + + partial_render_state = render_state.ForkPartial(name, self._id) + + arg_context = {} + if self._pass_through_id is not None: + context = render_state.contexts.Resolve(self._pass_through_id.name) + if context is not None: + arg_context[self._pass_through_id.name] = context + if self._args is not None: + def resolve_args(args): + resolved = {} + for key, value in args.iteritems(): + if isinstance(value, dict): + assert len(value.keys()) == 1 + id_of_partial, partial_args = value.items()[0] + partial = render_state.contexts.Resolve(id_of_partial.name) + if partial is not None: + resolved[key] = _PartialNodeWithArguments( + partial, resolve_args(partial_args)) + else: + context = render_state.contexts.Resolve(value.name) + if context is not None: + resolved[key] = context + return resolved + arg_context.update(resolve_args(self._args)) + if self._bind_to and self._content: + arg_context[self._bind_to.name] = _PartialNodeInContext( + self._content, render_state.contexts) + if arg_context: + partial_render_state.contexts.Push(arg_context) + + node.Render(partial_render_state) + + render_state.Merge( + partial_render_state, + text_transform=lambda text: text[:-1] if text.endswith('\n') else text) + + def SetArguments(self, args): + self._args = args + + def PassThroughArgument(self, id_): + self._pass_through_id = id_ + + def __repr__(self): + return '{{+%s}}' % self._id + +_TOKENS = {} + +class _Token(object): + '''The tokens that can appear in a template. + ''' + class Data(object): + def __init__(self, name, text, clazz): + self.name = name + self.text = text + self.clazz = clazz + _TOKENS[text] = self + + def ElseNodeClass(self): + if self.clazz == _VertedSectionNode: + return _InvertedSectionNode + if self.clazz == _InvertedSectionNode: + return _VertedSectionNode + return None + + def __repr__(self): + return self.name + + def __str__(self): + return repr(self) + + OPEN_START_SECTION = Data( + 'OPEN_START_SECTION' , '{{#', _SectionNode) + OPEN_START_VERTED_SECTION = Data( + 'OPEN_START_VERTED_SECTION' , '{{?', _VertedSectionNode) + OPEN_START_INVERTED_SECTION = Data( + 'OPEN_START_INVERTED_SECTION', '{{^', _InvertedSectionNode) + OPEN_ASSERTION = Data( + 'OPEN_ASSERTION' , '{{!', _AssertionNode) + OPEN_JSON = Data( + 'OPEN_JSON' , '{{*', _JsonNode) + OPEN_PARTIAL = Data( + 'OPEN_PARTIAL' , '{{+', _PartialNode) + OPEN_ELSE = Data( + 'OPEN_ELSE' , '{{:', None) + OPEN_END_SECTION = Data( + 'OPEN_END_SECTION' , '{{/', None) + INLINE_END_SECTION = Data( + 'INLINE_END_SECTION' , '/}}', None) + OPEN_UNESCAPED_VARIABLE = Data( + 'OPEN_UNESCAPED_VARIABLE' , '{{{', _UnescapedVariableNode) + CLOSE_MUSTACHE3 = Data( + 'CLOSE_MUSTACHE3' , '}}}', None) + OPEN_COMMENT = Data( + 'OPEN_COMMENT' , '{{-', _CommentNode) + CLOSE_COMMENT = Data( + 'CLOSE_COMMENT' , '-}}', None) + OPEN_VARIABLE = Data( + 'OPEN_VARIABLE' , '{{' , _EscapedVariableNode) + CLOSE_MUSTACHE = Data( + 'CLOSE_MUSTACHE' , '}}' , None) + CHARACTER = Data( + 'CHARACTER' , '.' , None) + +class _TokenStream(object): + '''Tokeniser for template parsing. + ''' + def __init__(self, string): + self.next_token = None + self.next_line = 1 + self.next_column = 0 + self._string = string + self._cursor = 0 + self.Advance() + + def HasNext(self): + return self.next_token is not None + + def NextCharacter(self): + if self.next_token is _Token.CHARACTER: + return self._string[self._cursor - 1] + return None + + def Advance(self): + if self._cursor > 0 and self._string[self._cursor - 1] == '\n': + self.next_line += 1 + self.next_column = 0 + elif self.next_token is not None: + self.next_column += len(self.next_token.text) + + self.next_token = None + + if self._cursor == len(self._string): + return None + assert self._cursor < len(self._string) + + if (self._cursor + 1 < len(self._string) and + self._string[self._cursor + 1] in '{}'): + self.next_token = ( + _TOKENS.get(self._string[self._cursor:self._cursor+3]) or + _TOKENS.get(self._string[self._cursor:self._cursor+2])) + + if self.next_token is None: + self.next_token = _Token.CHARACTER + + self._cursor += len(self.next_token.text) + return self + + def AdvanceOver(self, token, description=None): + parse_error = None + if not self.next_token: + parse_error = 'Reached EOF but expected %s' % token.name + elif self.next_token is not token: + parse_error = 'Expecting token %s but got %s at line %s' % ( + token.name, self.next_token.name, self.next_line) + if parse_error: + parse_error += ' %s' % description or '' + raise ParseException(parse_error) + return self.Advance() + + def AdvanceOverSeparator(self, char, description=None): + self.SkipWhitespace() + next_char = self.NextCharacter() + if next_char != char: + parse_error = 'Expected \'%s\'. got \'%s\'' % (char, next_char) + if description is not None: + parse_error += ' (%s)' % description + raise ParseException(parse_error) + self.AdvanceOver(_Token.CHARACTER) + self.SkipWhitespace() + + def AdvanceOverNextString(self, excluded=''): + start = self._cursor - len(self.next_token.text) + while (self.next_token is _Token.CHARACTER and + # Can use -1 here because token length of CHARACTER is 1. + self._string[self._cursor - 1] not in excluded): + self.Advance() + end = self._cursor - (len(self.next_token.text) if self.next_token else 0) + return self._string[start:end] + + def AdvanceToNextWhitespace(self): + return self.AdvanceOverNextString(excluded=' \n\r\t') + + def SkipWhitespace(self): + while (self.next_token is _Token.CHARACTER and + # Can use -1 here because token length of CHARACTER is 1. + self._string[self._cursor - 1] in ' \n\r\t'): + self.Advance() + + def __repr__(self): + return '%s(next_token=%s, remainder=%s)' % (type(self).__name__, + self.next_token, + self._string[self._cursor:]) + + def __str__(self): + return repr(self) + +class Motemplate(object): + '''A motemplate template. + ''' + def __init__(self, template, name=None): + self.source = template + self._name = name + tokens = _TokenStream(template) + self._top_node = self._ParseSection(tokens) + if not self._top_node: + raise ParseException('Template is empty') + if tokens.HasNext(): + raise ParseException('There are still tokens remaining at %s, ' + 'was there an end-section without a start-section?' % + tokens.next_line) + + def _ParseSection(self, tokens): + nodes = [] + while tokens.HasNext(): + if tokens.next_token in (_Token.OPEN_END_SECTION, + _Token.OPEN_ELSE): + # Handled after running parseSection within the SECTION cases, so this + # is a terminating condition. If there *is* an orphaned + # OPEN_END_SECTION, it will be caught by noticing that there are + # leftover tokens after termination. + break + elif tokens.next_token in (_Token.CLOSE_MUSTACHE, + _Token.CLOSE_MUSTACHE3): + raise ParseException('Orphaned %s at line %s' % (tokens.next_token.name, + tokens.next_line)) + nodes += self._ParseNextOpenToken(tokens) + + for i, node in enumerate(nodes): + if isinstance(node, _StringNode): + continue + + previous_node = nodes[i - 1] if i > 0 else None + next_node = nodes[i + 1] if i < len(nodes) - 1 else None + rendered_node = None + + if node.GetStartLine() != node.GetEndLine(): + rendered_node = _BlockNode(node) + if previous_node: + previous_node.TrimEndingSpaces() + if next_node: + next_node.TrimStartingNewLine() + elif ((not previous_node or previous_node.EndsWithEmptyLine()) and + (not next_node or next_node.StartsWithNewLine())): + indentation = 0 + if previous_node: + indentation = previous_node.TrimEndingSpaces() + if next_node: + next_node.TrimStartingNewLine() + rendered_node = _IndentedNode(node, indentation) + else: + rendered_node = _InlineNode(node) + + nodes[i] = rendered_node + + if len(nodes) == 0: + return None + if len(nodes) == 1: + return nodes[0] + return _NodeCollection(nodes) + + def _ParseNextOpenToken(self, tokens): + next_token = tokens.next_token + + if next_token is _Token.CHARACTER: + # Plain strings. + start_line = tokens.next_line + string = tokens.AdvanceOverNextString() + return [_StringNode(string, start_line, tokens.next_line)] + elif next_token in (_Token.OPEN_VARIABLE, + _Token.OPEN_UNESCAPED_VARIABLE, + _Token.OPEN_JSON): + # Inline nodes that don't take arguments. + tokens.Advance() + close_token = (_Token.CLOSE_MUSTACHE3 + if next_token is _Token.OPEN_UNESCAPED_VARIABLE else + _Token.CLOSE_MUSTACHE) + id_ = self._NextIdentifier(tokens) + tokens.AdvanceOver(close_token) + return [next_token.clazz(id_)] + elif next_token is _Token.OPEN_ASSERTION: + # Inline nodes that take arguments. + tokens.Advance() + id_ = self._NextIdentifier(tokens) + node = next_token.clazz(id_, tokens.AdvanceOverNextString()) + tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) + return [node] + elif next_token in (_Token.OPEN_PARTIAL, + _Token.OPEN_START_SECTION, + _Token.OPEN_START_VERTED_SECTION, + _Token.OPEN_START_INVERTED_SECTION): + # Block nodes, though they may have inline syntax like {{#foo bar /}}. + tokens.Advance() + bind_to, id_ = None, self._NextIdentifier(tokens) + if tokens.NextCharacter() == ':': + # This section has the format {{#bound:id}} as opposed to just {{id}}. + # That is, |id_| is actually the identifier to bind what the section + # is producing, not the identifier of where to find that content. + tokens.AdvanceOverSeparator(':') + bind_to, id_ = id_, self._NextIdentifier(tokens) + partial_args = None + if next_token is _Token.OPEN_PARTIAL: + partial_args = self._ParsePartialNodeArgs(tokens) + if tokens.next_token is not _Token.CLOSE_MUSTACHE: + # Inline syntax for partial types. + if bind_to is not None: + raise ParseException( + 'Cannot bind %s to a self-closing partial' % bind_to) + tokens.AdvanceOver(_Token.INLINE_END_SECTION) + partial_node = _PartialNode.Inline(id_) + partial_node.SetArguments(partial_args) + return [partial_node] + elif tokens.next_token is not _Token.CLOSE_MUSTACHE: + # Inline syntax for non-partial types. Support select node types: + # variables, partials, JSON. + line, column = tokens.next_line, (tokens.next_column + 1) + name = tokens.AdvanceToNextWhitespace() + clazz = _UnescapedVariableNode + if name.startswith('*'): + clazz = _JsonNode + elif name.startswith('+'): + clazz = _PartialNode.Inline + if clazz is not _UnescapedVariableNode: + name = name[1:] + column += 1 + inline_node = clazz(_Identifier(name, line, column)) + if isinstance(inline_node, _PartialNode): + inline_node.SetArguments(self._ParsePartialNodeArgs(tokens)) + if bind_to is not None: + inline_node.PassThroughArgument(bind_to) + tokens.SkipWhitespace() + tokens.AdvanceOver(_Token.INLINE_END_SECTION) + return [next_token.clazz(bind_to, id_, inline_node)] + # Block syntax. + tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) + section = self._ParseSection(tokens) + else_node_class = next_token.ElseNodeClass() # may not have one + else_section = None + if (else_node_class is not None and + tokens.next_token is _Token.OPEN_ELSE): + self._OpenElse(tokens, id_) + else_section = self._ParseSection(tokens) + self._CloseSection(tokens, id_) + nodes = [] + if section is not None: + node = next_token.clazz(bind_to, id_, section) + if partial_args: + node.SetArguments(partial_args) + nodes.append(node) + if else_section is not None: + nodes.append(else_node_class(bind_to, id_, else_section)) + return nodes + elif next_token is _Token.OPEN_COMMENT: + # Comments. + start_line = tokens.next_line + self._AdvanceOverComment(tokens) + return [_CommentNode(start_line, tokens.next_line)] + + def _AdvanceOverComment(self, tokens): + tokens.AdvanceOver(_Token.OPEN_COMMENT) + depth = 1 + while tokens.HasNext() and depth > 0: + if tokens.next_token is _Token.OPEN_COMMENT: + depth += 1 + elif tokens.next_token is _Token.CLOSE_COMMENT: + depth -= 1 + tokens.Advance() + + def _CloseSection(self, tokens, id_): + tokens.AdvanceOver(_Token.OPEN_END_SECTION, + description='to match %s' % id_.GetDescription()) + next_string = tokens.AdvanceOverNextString() + if next_string != '' and next_string != id_.name: + raise ParseException( + 'Start section %s doesn\'t match end %s' % (id_, next_string)) + tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) + + def _OpenElse(self, tokens, id_): + tokens.AdvanceOver(_Token.OPEN_ELSE) + next_string = tokens.AdvanceOverNextString() + if next_string != '' and next_string != id_.name: + raise ParseException( + 'Start section %s doesn\'t match else %s' % (id_, next_string)) + tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) + + def _ParsePartialNodeArgs(self, tokens): + args = {} + tokens.SkipWhitespace() + while (tokens.next_token is _Token.CHARACTER and + tokens.NextCharacter() != ')'): + key = tokens.AdvanceOverNextString(excluded=':') + tokens.AdvanceOverSeparator(':') + if tokens.NextCharacter() == '(': + tokens.AdvanceOverSeparator('(') + inner_id = self._NextIdentifier(tokens) + inner_args = self._ParsePartialNodeArgs(tokens) + tokens.AdvanceOverSeparator(')') + args[key] = {inner_id: inner_args} + else: + args[key] = self._NextIdentifier(tokens) + return args or None + + def _NextIdentifier(self, tokens): + tokens.SkipWhitespace() + column_start = tokens.next_column + 1 + id_ = _Identifier(tokens.AdvanceOverNextString(excluded=' \n\r\t:()'), + tokens.next_line, + column_start) + tokens.SkipWhitespace() + return id_ + + def Render(self, *user_contexts): + '''Renders this template given a variable number of contexts to read out + values from (such as those appearing in {{foo}}). + ''' + internal_context = _InternalContext() + contexts = list(user_contexts) + contexts.append({ + '_': internal_context, + 'false': False, + 'true': True, + }) + render_state = _RenderState(self._name or '<root>', _Contexts(contexts)) + internal_context.SetRenderState(render_state) + self._top_node.Render(render_state) + return render_state.GetResult() + + def render(self, *contexts): + return self.Render(*contexts) + + def __eq__(self, other): + return self.source == other.source and self._name == other._name + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return str('%s(%s)' % (type(self).__name__, self._top_node)) + + def __str__(self): + return repr(self) |