diff options
author | cduvall@chromium.org <cduvall@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-31 04:39:40 +0000 |
---|---|---|
committer | cduvall@chromium.org <cduvall@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-31 04:39:40 +0000 |
commit | d025db6868eff21be7403a32d2e3bebdf48748fa (patch) | |
tree | 60fd21fada09533f1dbed781cd046742d7721399 /third_party/handlebar | |
parent | 6ca01ee07675fae6f57992da638c3cbb933de5eb (diff) | |
download | chromium_src-d025db6868eff21be7403a32d2e3bebdf48748fa.zip chromium_src-d025db6868eff21be7403a32d2e3bebdf48748fa.tar.gz chromium_src-d025db6868eff21be7403a32d2e3bebdf48748fa.tar.bz2 |
Adding Handlebar to third_party
Handlebar is needed for the Die build.py project.
Review URL: https://chromiumcodereview.appspot.com/10442046
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@139729 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'third_party/handlebar')
-rw-r--r-- | third_party/handlebar/.gitignore | 1 | ||||
-rw-r--r-- | third_party/handlebar/LICENSE | 202 | ||||
-rw-r--r-- | third_party/handlebar/OWNERS | 1 | ||||
-rw-r--r-- | third_party/handlebar/README.chromium | 11 | ||||
-rw-r--r-- | third_party/handlebar/README.md | 100 | ||||
-rw-r--r-- | third_party/handlebar/handlebar.py | 461 |
6 files changed, 776 insertions, 0 deletions
diff --git a/third_party/handlebar/.gitignore b/third_party/handlebar/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/third_party/handlebar/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/third_party/handlebar/LICENSE b/third_party/handlebar/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/third_party/handlebar/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/handlebar/OWNERS b/third_party/handlebar/OWNERS new file mode 100644 index 0000000..d3c3af29 --- /dev/null +++ b/third_party/handlebar/OWNERS @@ -0,0 +1 @@ +kalman@chromium.org diff --git a/third_party/handlebar/README.chromium b/third_party/handlebar/README.chromium new file mode 100644 index 0000000..e057599 --- /dev/null +++ b/third_party/handlebar/README.chromium @@ -0,0 +1,11 @@ +Name: Handlebar, logic-less templating system +Short Name: handlebar +URL: https://github.com/kalman/templates +Version: 0 +Date: May 30, 2012 +Revision: commit 79b1d45 +License: Apache License, Version 2.0 +Security Critical: no + +Description: +A python logic-less templating system, based on mustache. diff --git a/third_party/handlebar/README.md b/third_party/handlebar/README.md new file mode 100644 index 0000000..266ef11 --- /dev/null +++ b/third_party/handlebar/README.md @@ -0,0 +1,100 @@ +# 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/third_party/handlebar/handlebar.py b/third_party/handlebar/handlebar.py new file mode 100644 index 0000000..d05e15e --- /dev/null +++ b/third_party/handlebar/handlebar.py @@ -0,0 +1,461 @@ +# 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. + +import json +import re + +""" Handlebar templates are logicless templates inspired by ctemplate (or more +specifically mustache templates) then taken in their own direction because I +found those to be inadequate. + +from handlebar import Handlebar + +template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') +input = { + 'foo': [ + { 'bar': 1 }, + { 'bar': 2 }, + { 'bar': 3 } + ] +} +print(template.render(input).text) +""" + +class ParseException(Exception): + """ Exception thrown while parsing the template. + """ + def __init__(self, message): + Exception.__init__(self, message) + +class RenderResult(object): + """ Result of a render operation. + """ + def __init__(self, text, errors): + self.text = text; + self.errors = errors + +class StringBuilder(object): + """ Mimics Java's StringBuilder for easy porting from the Java version of + this file to Python. + """ + def __init__(self): + self._buf = [] + + def append(self, obj): + self._buf.append(str(obj)) + + def toString(self): + return ''.join(self._buf) + +class PathIdentifier(object): + """ An identifier of the form "foo.bar.baz". + """ + def __init__(self, name): + if name == '': + raise ParseException("Cannot have empty identifiers") + if not re.match('^[a-zA-Z0-9._]*$', name): + raise ParseException(name + " is not a valid identifier") + self.path = name.split(".") + + def resolve(self, contexts, errors): + resolved = None + for context in contexts: + if not context: + continue + resolved = self._resolveFrom(context) + if resolved: + return resolved + _RenderError(errors, "Couldn't resolve identifier ", self.path, " in ", contexts) + return None + + def _resolveFrom(self, context): + result = context + for next in self.path: + if not result or type(result) != dict: + return None + result = result.get(next) + return result + + def __str__(self): + return '.'.join(self.path) + +class ThisIdentifier(object): + """ An identifier of the form "@". + """ + def resolve(self, contexts, errors): + return contexts[0] + + def __str__(self): + return '@' + +class SelfClosingNode(object): + """ Nodes which are "self closing", e.g. {{foo}}, {{*foo}}. + """ + def init(self, id): + self.id = id + +class HasChildrenNode(object): + """ Nodes which are not self closing, and have 0..n children. + """ + def init(self, id, children): + self.id = id + self.children = children + +class StringNode(object): + """ Just a string. + """ + def __init__(self, string): + self.string = string + + def render(self, buf, contexts, errors): + buf.append(self.string) + +class EscapedVariableNode(SelfClosingNode): + """ {{foo}} + """ + def render(self, buf, contexts, errors): + value = self.id.resolve(contexts, errors) + if value: + buf.append(self._htmlEscape(str(value))) + + def _htmlEscape(self, unescaped): + escaped = StringBuilder() + for c in unescaped: + if c == '<': + escaped.append("<") + elif c == '>': + escaped.append(">") + elif c == '&': + escaped.append("&") + else: + escaped.append(c) + return escaped.toString() + +class UnescapedVariableNode(SelfClosingNode): + """ {{{foo}}} + """ + def render(self, buf, contexts, errors): + value = self.id.resolve(contexts, errors) + if value: + buf.append(value) + +class SectionNode(HasChildrenNode): + """ {{#foo}} ... {{/}} + """ + def render(self, buf, contexts, errors): + value = self.id.resolve(contexts, errors) + if not value: + return + + type_ = type(value) + if value == None: + pass + elif type_ == list: + for item in value: + contexts.insert(0, item) + _RenderNodes(buf, self.children, contexts, errors) + contexts.pop(0) + elif type_ == dict: + contexts.insert(0, value) + _RenderNodes(buf, self.children, contexts, errors) + contexts.pop(0) + else: + _RenderError(errors, "{{#", self.id, "}} cannot be rendered with a ", type_) + +class VertedSectionNode(HasChildrenNode): + """ {{?foo}} ... {{/}} + """ + def render(self, buf, contexts, errors): + value = self.id.resolve(contexts, errors) + if value and _VertedSectionNodeShouldRender(value): + contexts.insert(0, value) + _RenderNodes(buf, self.children, contexts, errors) + contexts.pop(0) + +def _VertedSectionNodeShouldRender(value): + type_ = type(value) + if value == None: + return False + elif type_ == bool: + return value + elif type_ == int or type_ == float: + return value > 0 + elif type_ == str or type_ == unicode: + return value != '' + elif type_ == list or type_ == dict: + return len(value) > 0 + raise TypeError("Unhandled type: " + str(type_)) + +class InvertedSectionNode(HasChildrenNode): + """ {{^foo}} ... {{/}} + """ + def render(self, buf, contexts, errors): + value = self.id.resolve(contexts, errors) + if not value or not _VertedSectionNodeShouldRender(value): + _RenderNodes(buf, self.children, contexts, errors) + +class JsonNode(SelfClosingNode): + """ {{*foo}} + """ + def render(self, buf, contexts, errors): + value = self.id.resolve(contexts, errors) + if value: + buf.append(json.dumps(value, separators=(',',':'))) + +class PartialNode(SelfClosingNode): + """ {{+foo}} + """ + def render(self, buf, contexts, errors): + value = self.id.resolve(contexts, errors) + if not isinstance(value, Handlebar): + _RenderError(errors, id, " didn't resolve to a Handlebar") + return + _RenderNodes(buf, value.nodes, contexts, errors) + +class SwitchNode(object): + """ {{:foo}} + """ + def __init__(self, id): + self.id = id + self._cases = {} + + def addCase(self, caseValue, caseNode): + self._cases[caseValue] = caseNode + + def render(self, buf, contexts, errors): + value = self.id.resolve(contexts, errors) + if not value: + _RenderError(errors, id, " didn't resolve to any value") + return + if not (type(value) == str or type(value) == unicode): + _RenderError(errors, id, " didn't resolve to a String") + return + caseNode = self._cases.get(value) + if caseNode: + caseNode.render(buf, contexts, errors) + +class CaseNode(object): + """ {{=foo}} + """ + def __init__(self, children): + self.children = children + + def render(self, buf, contexts, errors): + for child in self.children: + child.render(buf, contexts, errors) + +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 + + 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_START_SWITCH = Data("OPEN_START_SWITCH" , "{{:", SwitchNode) + OPEN_CASE = Data("OPEN_CASE" , "{{=", CaseNode) + OPEN_END_SECTION = Data("OPEN_END_SECTION" , "{{/", None) + OPEN_UNESCAPED_VARIABLE = Data("OPEN_UNESCAPED_VARIABLE" , "{{{", UnescapedVariableNode) + CLOSE_MUSTACHE3 = Data("CLOSE_MUSTACHE3" , "}}}", None) + OPEN_VARIABLE = Data("OPEN_VARIABLE" , "{{" , EscapedVariableNode) + CLOSE_MUSTACHE = Data("CLOSE_MUSTACHE" , "}}" , None) + CHARACTER = Data("CHARACTER" , "." , StringNode) + +# List of tokens in order of longest to shortest, to avoid any prefix matching +# issues. +_tokenList = [ + Token.OPEN_START_SECTION, + Token.OPEN_START_VERTED_SECTION, + Token.OPEN_START_INVERTED_SECTION, + Token.OPEN_START_JSON, + Token.OPEN_START_PARTIAL, + Token.OPEN_START_SWITCH, + Token.OPEN_CASE, + Token.OPEN_END_SECTION, + Token.OPEN_UNESCAPED_VARIABLE, + Token.CLOSE_MUSTACHE3, + Token.OPEN_VARIABLE, + Token.CLOSE_MUSTACHE, + Token.CHARACTER +] + +class TokenStream(object): + """ Tokeniser for template parsing. + """ + def __init__(self, string): + self.nextToken = None + self._remainder = string + self._nextContents = None + self.advance() + + def hasNext(self): + return self.nextToken != None + + def advanceOver(self, token): + if self.nextToken != token: + raise ParseException( + "Expecting token " + token.name + " but got " + self.nextToken.name) + return self.advance() + + def advance(self): + self.nextToken = None + self._nextContents = None + + if self._remainder == '': + return None + + for token in _tokenList: + if self._remainder.startswith(token.text): + self.nextToken = token + break + + if self.nextToken == None: + self.nextToken = Token.CHARACTER + + self._nextContents = self._remainder[0:len(self.nextToken.text)] + self._remainder = self._remainder[len(self.nextToken.text):] + return self.nextToken + + def nextString(self): + buf = StringBuilder() + while self.nextToken == Token.CHARACTER: + buf.append(self._nextContents) + self.advance() + return buf.toString() + +def _CreateIdentifier(path): + if path == '@': + return ThisIdentifier() + else: + return PathIdentifier(path) + +class Handlebar(object): + """ A handlebar template. + """ + def __init__(self, template): + self.nodes = [] + self._parseTemplate(template, self.nodes) + + def _parseTemplate(self, template, nodes): + tokens = TokenStream(template) + self._parseSection(tokens, nodes) + if tokens.hasNext(): + raise ParseException("There are still tokens remaining, " + "was there an end-section without a start-section:") + + def _parseSection(self, tokens, nodes): + sectionEnded = False + while tokens.hasNext() and not sectionEnded: + next = tokens.nextToken + if next == Token.CHARACTER: + nodes.append(StringNode(tokens.nextString())) + elif next == Token.OPEN_VARIABLE or \ + next == Token.OPEN_UNESCAPED_VARIABLE or \ + next == Token.OPEN_START_JSON or \ + next == Token.OPEN_START_PARTIAL: + token = tokens.nextToken + id = self._openSection(tokens) + node = token.clazz() + node.init(id) + nodes.append(node) + elif next == Token.OPEN_START_SECTION or \ + next == Token.OPEN_START_VERTED_SECTION or \ + next == Token.OPEN_START_INVERTED_SECTION: + token = tokens.nextToken + id = self._openSection(tokens) + + children = [] + self._parseSection(tokens, children) + self._closeSection(tokens, id) + + node = token.clazz() + node.init(id, children) + nodes.append(node) + elif next == Token.OPEN_START_SWITCH: + id = self._openSection(tokens) + while tokens.nextToken == Token.CHARACTER: + tokens.advanceOver(Token.CHARACTER) + + switchNode = SwitchNode(id) + nodes.append(switchNode) + + while tokens.hasNext() and tokens.nextToken == Token.OPEN_CASE: + tokens.advanceOver(Token.OPEN_CASE) + caseValue = tokens.nextString() + tokens.advanceOver(Token.CLOSE_MUSTACHE) + + caseChildren = [] + self._parseSection(tokens, caseChildren) + + switchNode.addCase(caseValue, CaseNode(caseChildren)) + + self._closeSection(tokens, id) + elif next == Token.OPEN_CASE: + # See below. + sectionEnded = True + elif next == Token.OPEN_END_SECTION: + # 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. + sectionEnded = True + elif Token.CLOSE_MUSTACHE: + raise ParseException("Orphaned " + tokens.nextToken.name) + + def _openSection(self, tokens): + openToken = tokens.nextToken + tokens.advance() + id = _CreateIdentifier(tokens.nextString()) + if openToken == Token.OPEN_UNESCAPED_VARIABLE: + tokens.advanceOver(Token.CLOSE_MUSTACHE3) + else: + tokens.advanceOver(Token.CLOSE_MUSTACHE) + return id + + def _closeSection(self, tokens, id): + tokens.advanceOver(Token.OPEN_END_SECTION) + nextString = tokens.nextString() + if nextString != '' and str(id) != nextString: + raise ParseException( + "Start section " + str(id) + " doesn't match end section " + nextString) + tokens.advanceOver(Token.CLOSE_MUSTACHE) + + def render(self, *contexts): + """ Renders this template given a variable number of "contexts" to read + out values from (such as those appearing in {{foo}}). + """ + contextDeque = [] + for context in contexts: + contextDeque.append(context) + buf = StringBuilder() + errors = [] + _RenderNodes(buf, self.nodes, contextDeque, errors) + return RenderResult(buf.toString(), errors) + +def _RenderNodes(buf, nodes, contexts, errors): + for node in nodes: + node.render(buf, contexts, errors) + +def _RenderError(errors, *messages): + if not errors: + return + buf = StringBuilder() + for message in messages: + buf.append(message) + errors.append(buf.toString()) |