diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ae319c7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,23 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/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/NOTICE b/NOTICE
new file mode 100644
index 0000000..1527b34
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,6 @@
+Apache Velocity
+
+Copyright (C) 2000-2007 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..839c002
--- /dev/null
+++ b/README.md
@@ -0,0 +1,344 @@
+# EscapeVelocity summary
+
+EscapeVelocity is a templating engine that can be used from Java. It is a reimplementation of a subset of
+functionality from [Apache Velocity](http://velocity.apache.org/).
+
+This is not an official Google product.
+
+For a fuller explanation of Velocity's functioning, see its
+[User Guide](http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html)
+
+If EscapeVelocity successfully produces a result from a template evaluation, that result should be
+the exact same string that Velocity produces. If not, that is a bug.
+
+EscapeVelocity has no facilities for HTML escaping and it is not appropriate for producing
+HTML output that might include portions of untrusted input.
+
+
+[TOC]
+
+
+## Motivation
+
+Velocity has a convenient templating language. It is easy to read, and it has widespread support
+from tools such as editors and coding websites. However, *using* Velocity can prove difficult.
+Its use to generate Java code in the [AutoValue][AutoValue] annotation processor required many
+[workarounds][VelocityHacks]. The way it dynamically loads classes as part of its standard operation
+makes it hard to [shade](https://maven.apache.org/plugins/maven-shade-plugin/) it, which in the case
+of AutoValue led to interference if Velocity was used elsewhere in a project.
+
+EscapeVelocity has a simple API that does not involve any class-loading or other sources of
+problems. It and its dependencies can be shaded with no difficulty.
+
+## Loading a template
+
+The entry point for EscapeVelocity is the `Template` class. To obtain an instance, use
+`Template.from(Reader)`. If a template is stored in a file, that file conventionally has the
+suffix `.vm` (for Velocity Macros). But since the argument is a `Reader`, you can also load
+a template directly from a Java string, using `StringReader`.
+
+Here's how you might make a `Template` instance from a template file that is packaged as a resource
+in the same package as the calling class:
+
+```java
+InputStream in = getClass().getResourceAsStream("foo.vm");
+if (in == null) {
+ throw new IllegalArgumentException("Could not find resource foo.vm");
+}
+Reader reader = new BufferedReader(new InputStreamReader(in));
+Template template = Template.parseFrom(reader);
+```
+
+## Expanding a template
+
+Once you have a `Template` object, you can use it to produce a string where the variables in the
+template are given the values you provide. You can do this any number of times, specifying the
+same or different values each time.
+
+Suppose you have this template:
+
+```
+The $language word for $original is $translated.
+```
+
+You might write this code:
+
+```java
+Map vars = new HashMap<>();
+vars.put("language", "French");
+vars.put("original", "toe");
+vars.put("translated", "orteil");
+String result = template.evaluate(vars);
+```
+
+The `result` string would then be: `The French word for toe is orteil.`
+
+## Comments
+
+The characters `##` introduce a comment. Characters from `##` up to and including the following
+newline are omitted from the template. This template has comments:
+
+```
+Line 1 ## with a comment
+Line 2
+```
+
+It is the same as this template:
+```
+Line 1 Line 2
+```
+
+## References
+
+EscapeVelocity supports most of the reference types described in the
+[Velocity User Guide](http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html#References)
+
+### Variables
+
+A variable has an ASCII name that starts with a letter (a-z or A-Z) and where any other characters
+are also letters or digits or hyphens (-) or underscores (_). A variable reference can be written
+as `$foo` or as `${foo}`. The value of a variable can be of any Java type. If the value `v` of
+variable `foo` is not a String then the result of `$foo` in a template will be `String.valueOf(v)`.
+Variables must be defined before they are referenced; otherwise an `EvaluationException` will be
+thrown.
+
+Variable names are case-sensitive: `$foo` is not the same variable as `$Foo` or `$FOO`.
+
+Initially the values of variables come from the Map that is passed to `Template.evaluate`. Those
+values can be changed, and new ones defined, using the `#set` directive in the template:
+
+```
+#set ($foo = "bar")
+```
+
+Setting a variable affects later references to it in the template, but has no effect on the
+`Map` that was passed in or on later template evaluations.
+
+### Properties
+
+If a reference looks like `$purchase.Total` then the value of the `$purchase` variable must be a
+Java object that has a public method `getTotal()` or `gettotal()`, or a method called `isTotal()` or
+`istotal()` that returns `boolean`. The result of `$purchase.Total` is then the result of calling
+that method on the `$purchase` object.
+
+If you want to have a period (`.`) after a variable reference *without* it being a property
+reference, you can use braces like this: `${purchase}.Total`. If, after a property reference, you
+have a further period, you can put braces around the reference like this:
+`${purchase.Total}.nonProperty`.
+
+### Methods
+
+If a reference looks like `$purchase.addItem("scones", 23)` then the value of the `$purchase`
+variable must be a Java object that has a public method `addItem` with two parameters that match
+the given values. Unlike Velocity, EscapeVelocity requires that there be exactly one such method.
+It is OK if there are other `addItem` methods provided they are not compatible with the
+arguments provided.
+
+Properties are in fact a special case of methods: instead of writing `$purchase.Total` you could
+write `$purchase.getTotal()`. Braces can be used to make the method invocation explicit
+(`${purchase.getTotal()}`) or to prevent method invocation (`${purchase}.getTotal()`).
+
+### Indexing
+
+If a reference looks like `$indexme[$i]` then the value of the `$indexme` variable must be a Java
+object that has a public `get` method that takes one argument that is compatible with the index.
+For example, `$indexme` might be a `List` and `$i` might be an integer. Then the reference would
+be the result of `List.get(int)` for that list and that integer. Or, `$indexme` might be a `Map`,
+and the reference would be the result of `Map.get(Object)` for the object `$i`. In general,
+`$indexme[$i]` is equivalent to `$indexme.get($i)`.
+
+Unlike Velocity, EscapeVelocity does not allow `$indexme` to be a Java array.
+
+### Undefined references
+
+If a variable has not been given a value, either by being in the initial Map argument or by being
+set in the template, then referencing it will provoke an `EvaluationException`. There is
+a special case for `#if`: if you write `#if ($var)` then it is allowed for `$var` not to be defined,
+and it is treated as false.
+
+### Setting properties and indexes: not supported
+
+Unlke Velocity, EscapeVelocity does not allow `#set` assignments with properties or indexes:
+
+```
+#set ($data.User = "jon") ## Allowed in Velocity but not in EscapeVelocity
+#set ($map["apple"] = "orange") ## Allowed in Velocity but not in EscapeVelocity
+```
+
+## Expressions
+
+In certain contexts, such as the `#set` directive we have just seen or certain other directives,
+EscapeVelocity can evaluate expressions. An expression can be any of these:
+
+* A reference, of the kind we have just seen. The value is the value of the reference.
+* A string literal enclosed in double quotes, like `"this"`. A string literal must appear on
+ one line. EscapeVelocity does not support the characters `$` or `\\` in a string literal.
+* An integer literal such as `23` or `-100`. EscapeVelocity does not support floating-point
+ literals.
+* A Boolean literal, `true` or `false`.
+* Simpler expressions joined together with operators that have the same meaning as in Java:
+ `!`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `+`, `-`, `*`, `/`, `%`. The operators have the
+ same precedence as in Java.
+* A simpler expression in parentheses, for example `(2 + 3)`.
+
+Velocity supports string literals with single quotes, like `'this`' and also references within
+strings, like `"a $reference in a string"`, but EscapeVelocity does not.
+
+## Directives
+
+A directive is introduced by a `#` character followed by a word. We have already seen the `#set`
+directive, which sets the value of a variable. The other directives are listed below.
+
+Directives can be spelled with or without braces, so `#set` or `#{set}`.
+
+### `#if`/`#elseif`/`#else`
+
+The `#if` directive selects parts of the template according as a condition is true or false.
+The simplest case looks like this:
+
+```
+#if ($condition) yes #end
+```
+
+This evaluates to the string ` yes ` if the variable `$condition` is defined and has a true value,
+and to the empty string otherwise. It is allowed for `$condition` not to be defined in this case,
+and then it is treated as false.
+
+The expression in `#if` (here `$condition`) is considered true if its value is not null and not
+equal to the Boolean value `false`.
+
+An `#if` directive can also have an `#else` part, for example:
+
+```
+#if ($condition) yes #else no #end
+```
+
+This evaluates to the string ` yes ` if the condition is true or the string ` no ` if it is not.
+
+An `#if` directive can have any number of `#elseif` parts. For example:
+
+```
+#if ($i == 0) zero #elseif ($i == 1) one #elseif ($i == 2) two #else many #end
+```
+
+### `#foreach`
+
+The `#foreach` directive repeats a part of the template once for each value in a list.
+
+```
+#foreach ($product in $allProducts)
+ ${product}!
+#end
+```
+
+This will produce one line for each value in the `$allProducts` variable. The value of
+`$allProducts` can be a Java `Iterable`, such as a `List` or `Set`; or it can be an object array;
+or it can be a Java `Map`. When it is a `Map` the `#foreach` directive loops over every *value*
+in the `Map`.
+
+If `$allProducts` is a `List` containing the strings `oranges` and `lemons` then the result of the
+`#foreach` would be this:
+
+```
+
+ oranges!
+
+
+ lemons!
+
+```
+
+When the `#foreach` completes, the loop variable (`$product` in the example) goes back to whatever
+value it had before, or to being undefined if it was undefined before.
+
+Within the `#foreach`, a special variable `$foreach` is defined, such that you can write
+`$foreach.hasNext`, which will be true if there are more values after this one or false if this
+is the last value. For example:
+
+```
+#foreach ($product in $allProducts)${product}#if ($foreach.hasNext), #end#end
+```
+
+This would produce the output `oranges, lemons` for the list above. (The example is scrunched up
+to avoid introducing extraneous spaces, as described in the [section](#spaces) on spaces
+below.)
+
+Velocity gives the `$foreach` variable other properties (`index` and `count`) but EscapeVelocity
+does not.
+
+### Macros
+
+A macro is a part of the template that can be reused in more than one place, potentially with
+different parameters each time. In the simplest case, a macro has no arguments:
+
+```
+#macro (hello) bonjour #end
+```
+
+Then the macro can be referenced by writing `#hello()` and the result will be the string ` bonjour `
+inserted at that point.
+
+Macros can also have parameters:
+
+```
+#macro (greet $hello $world) $hello, $world! #end
+```
+
+Then `#greet("bonjour", "monde")` would produce ` bonjour, monde! `. The comma is optional, so
+you could also write `#greet("bonjour" "monde")`.
+
+When a macro completes, the parameters (`$hello` and `$world` in the example) go back to whatever
+values they had before, or to being undefined if they were undefined before.
+
+All macro definitions take effect before the template is evaluated, so you can use a macro at a
+point in the template that is before the point where it is defined. This also means that you can't
+define a macro conditionally:
+
+```
+## This doesn't work!
+#if ($language == "French")
+#macro (hello) bonjour #end
+#else
+#macro (hello) hello #end
+#end
+```
+
+There is no particular reason to define the same macro more than once, but if you do it is the
+first definition that is retained. In the `#if` example just above, the `bonjour` version will
+always be used.
+
+Macros can make templates hard to understand. You may prefer to put the logic in a Java method
+rather than a macro, and call the method from the template using `$methods.doSomething("foo")`
+or whatever.
+
+## Spaces
+
+For the most part, spaces and newlines in the template are preserved exactly in the output.
+To avoid unwanted newlines, you may end up using `##` comments. In the `#foreach` example above
+we had this:
+
+```
+#foreach ($product in $allProducts)${product}#if ($foreach.hasNext), #end#end
+```
+
+That was to avoid introducing unwanted spaces and newlines. A more readable way to achieve the same
+result is this:
+
+```
+#foreach ($product in $allProducts)##
+${product}##
+#if ($foreach.hasNext), #end##
+#end
+```
+
+Spaces are ignored between the `#` of a directive and the `)` that closes it, so there is no trace
+in the output of the spaces in `#foreach ($product in $allProducts)` or `#if ($foreach.hasNext)`.
+Spaces are also ignored inside references, such as `$indexme[ $i ]` or `$callme( $i , $j )`.
+
+If you are concerned about the detailed formatting of the text from the template, you may want to
+post-process it. For example, if it is Java code, you could use a formatter such as
+[google-java-format](https://github.com/google/google-java-format). Then you shouldn't have to
+worry about extraneous spaces.
+
+[VelocityHacks]: https://github.com/google/auto/blob/ca2384d5ad15a0c761b940384083cf5c50c6e839/value/src/main/java/com/google/auto/value/processor/TemplateVars.java#L54
+[AutoValue]: https://github.com/google/auto/tree/master/value
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..a58896d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,98 @@
+
+
+
+ 4.0.0
+
+ com.google.escapevelocity
+ escapevelocity
+ 0.9-SNAPSHOT
+ EscapeVelocity
+
+ A reimplementation of a subset of the Apache Velocity templating system.
+
+
+
+
+
+
+ com.google.guava
+ guava
+ 23.5-jre
+ test
+
+
+
+ org.apache.velocity
+ velocity
+ 1.7
+ test
+
+
+ com.google.guava
+ guava-testlib
+ 23.5-jre
+ test
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ com.google.truth
+ truth
+ 0.36
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.7.0
+
+ 1.7
+ 1.7
+ -Xlint:all
+ true
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.0.2
+
+
+ org.apache.maven.plugins
+ maven-invoker-plugin
+ 3.0.1
+
+
+
+
diff --git a/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java b/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java
new file mode 100644
index 0000000..a4dfe17
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+/**
+ * A node in the parse tree representing a constant value. Evaluating the node yields the constant
+ * value. Instances of this class are used both in expressions, like the {@code 23} in
+ * {@code #set ($x = 23)}, and for literal text in templates. In the template...
+ *
{@code
+ * abc#{if}($x == 5)def#{end}xyz
+ * }
+ * ...each of the strings {@code abc}, {@code def}, {@code xyz} is represented by an instance of
+ * this class that {@linkplain #evaluate evaluates} to that string, and the value {@code 5} is
+ * represented by an instance of this class that evaluates to the integer 5.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+class ConstantExpressionNode extends ExpressionNode {
+ private final Object value;
+
+ ConstantExpressionNode(String resourceName, int lineNumber, Object value) {
+ super(resourceName, lineNumber);
+ this.value = value;
+ }
+
+ @Override
+ Object evaluate(EvaluationContext context) {
+ return value;
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/DirectiveNode.java b/src/main/java/com/google/escapevelocity/DirectiveNode.java
new file mode 100644
index 0000000..cf33f55
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/DirectiveNode.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A node in the parse tree that is a directive such as {@code #set ($x = $y)}
+ * or {@code #if ($x) y #end}.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+abstract class DirectiveNode extends Node {
+ DirectiveNode(String resourceName, int lineNumber) {
+ super(resourceName, lineNumber);
+ }
+
+ /**
+ * A node in the parse tree representing a {@code #set} construct. Evaluating
+ * {@code #set ($x = 23)} will set {@code $x} to the value 23. It does not in itself produce
+ * any text in the output.
+ *
+ *
Velocity supports setting values within arrays or collections, with for example
+ * {@code $set ($x[$i] = $y)}. That is not currently supported here.
+ */
+ static class SetNode extends DirectiveNode {
+ private final String var;
+ private final Node expression;
+
+ SetNode(String var, Node expression) {
+ super(expression.resourceName, expression.lineNumber);
+ this.var = var;
+ this.expression = expression;
+ }
+
+ @Override
+ Object evaluate(EvaluationContext context) {
+ context.setVar(var, expression.evaluate(context));
+ return "";
+ }
+ }
+
+ /**
+ * A node in the parse tree representing an {@code #if} construct. All instances of this class
+ * have a true subtree and a false subtree. For a plain {@code #if (cond) body
+ * #end}, the false subtree will be empty. For {@code #if (cond1) body1 #elseif (cond2) body2
+ * #else body3 #end}, the false subtree will contain a nested {@code IfNode}, as if {@code #else
+ * #if} had been used instead of {@code #elseif}.
+ */
+ static class IfNode extends DirectiveNode {
+ private final ExpressionNode condition;
+ private final Node truePart;
+ private final Node falsePart;
+
+ IfNode(
+ String resourceName,
+ int lineNumber,
+ ExpressionNode condition,
+ Node trueNode,
+ Node falseNode) {
+ super(resourceName, lineNumber);
+ this.condition = condition;
+ this.truePart = trueNode;
+ this.falsePart = falseNode;
+ }
+
+ @Override Object evaluate(EvaluationContext context) {
+ Node branch = condition.isDefinedAndTrue(context) ? truePart : falsePart;
+ return branch.evaluate(context);
+ }
+ }
+
+ /**
+ * A node in the parse tree representing a {@code #foreach} construct. While evaluating
+ * {@code #foreach ($x in $things)}, {$code $x} will be set to each element of {@code $things} in
+ * turn. Once the loop completes, {@code $x} will go back to whatever value it had before, which
+ * might be undefined. During loop execution, the variable {@code $foreach} is also defined.
+ * Velocity defines a number of properties in this variable, but here we only support
+ * {@code $foreach.hasNext}.
+ */
+ static class ForEachNode extends DirectiveNode {
+ private final String var;
+ private final ExpressionNode collection;
+ private final Node body;
+
+ ForEachNode(String resourceName, int lineNumber, String var, ExpressionNode in, Node body) {
+ super(resourceName, lineNumber);
+ this.var = var;
+ this.collection = in;
+ this.body = body;
+ }
+
+ @Override
+ Object evaluate(EvaluationContext context) {
+ Object collectionValue = collection.evaluate(context);
+ Iterable> iterable;
+ if (collectionValue instanceof Iterable>) {
+ iterable = (Iterable>) collectionValue;
+ } else if (collectionValue instanceof Object[]) {
+ iterable = Arrays.asList((Object[]) collectionValue);
+ } else if (collectionValue instanceof Map, ?>) {
+ iterable = ((Map, ?>) collectionValue).values();
+ } else {
+ throw evaluationException("Not iterable: " + collectionValue);
+ }
+ Runnable undo = context.setVar(var, null);
+ StringBuilder sb = new StringBuilder();
+ Iterator> it = iterable.iterator();
+ Runnable undoForEach = context.setVar("foreach", new ForEachVar(it));
+ while (it.hasNext()) {
+ context.setVar(var, it.next());
+ sb.append(body.evaluate(context));
+ }
+ undoForEach.run();
+ undo.run();
+ return sb.toString();
+ }
+
+ /**
+ * This class is the type of the variable {@code $foreach} that is defined within
+ * {@code #foreach} loops. Its {@link #getHasNext()} method means that we can write
+ * {@code #if ($foreach.hasNext)}.
+ */
+ private static class ForEachVar {
+ private final Iterator> iterator;
+
+ ForEachVar(Iterator> iterator) {
+ this.iterator = iterator;
+ }
+
+ public boolean getHasNext() {
+ return iterator.hasNext();
+ }
+ }
+ }
+
+ /**
+ * A node in the parse tree representing a macro call. If the template contains a definition like
+ * {@code #macro (mymacro $x $y) ... #end}, then a call of that macro looks like
+ * {@code #mymacro (xvalue yvalue)}. The call is represented by an instance of this class. The
+ * definition itself does not appear in the parse tree.
+ *
+ *
Evaluating a macro involves temporarily setting the parameter variables ({@code $x $y} in
+ * the example) to thunks representing the argument expressions, evaluating the macro body, and
+ * restoring any previous values that the parameter variables had.
+ */
+ static class MacroCallNode extends DirectiveNode {
+ private final String name;
+ private final ImmutableList thunks;
+ private Macro macro;
+
+ MacroCallNode(
+ String resourceName,
+ int lineNumber,
+ String name,
+ ImmutableList argumentNodes) {
+ super(resourceName, lineNumber);
+ this.name = name;
+ this.thunks = argumentNodes;
+ }
+
+ String name() {
+ return name;
+ }
+
+ int argumentCount() {
+ return thunks.size();
+ }
+
+ void setMacro(Macro macro) {
+ this.macro = macro;
+ }
+
+ @Override
+ Object evaluate(EvaluationContext context) {
+ assert macro != null : "Macro should have been linked: #" + name;
+ return macro.evaluate(context, thunks);
+ }
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/EvaluationContext.java b/src/main/java/com/google/escapevelocity/EvaluationContext.java
new file mode 100644
index 0000000..43b7868
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/EvaluationContext.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * The context of a template evaluation. This consists of the template variables and the template
+ * macros. The template variables start with the values supplied by the evaluation call, and can
+ * be changed by {@code #set} directives and during the execution of {@code #foreach} and macro
+ * calls. The macros are extracted from the template during parsing and never change thereafter.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+interface EvaluationContext {
+ Object getVar(String var);
+
+ boolean varIsDefined(String var);
+
+ /**
+ * Sets the given variable to the given value.
+ *
+ * @return a Runnable that will restore the variable to the value it had before. If the variable
+ * was undefined before this method was executed, the Runnable will make it undefined again.
+ * This allows us to restore the state of {@code $x} after {@code #foreach ($x in ...)}.
+ */
+ Runnable setVar(final String var, Object value);
+
+ class PlainEvaluationContext implements EvaluationContext {
+ private final Map vars;
+
+ PlainEvaluationContext(Map vars) {
+ this.vars = new TreeMap(vars);
+ }
+
+ @Override
+ public Object getVar(String var) {
+ return vars.get(var);
+ }
+
+ @Override
+ public boolean varIsDefined(String var) {
+ return vars.containsKey(var);
+ }
+
+ @Override
+ public Runnable setVar(final String var, Object value) {
+ Runnable undo;
+ if (vars.containsKey(var)) {
+ final Object oldValue = vars.get(var);
+ undo = new Runnable() {
+ @Override public void run() {
+ vars.put(var, oldValue);
+ }
+ };
+ } else {
+ undo = new Runnable() {
+ @Override public void run() {
+ vars.remove(var);
+ }
+ };
+ }
+ vars.put(var, value);
+ return undo;
+ }
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/EvaluationException.java b/src/main/java/com/google/escapevelocity/EvaluationException.java
new file mode 100644
index 0000000..67aa15c
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/EvaluationException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+/**
+ * An exception that occurred while evaluating a template, such as an undefined variable reference
+ * or a division by zero.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+public class EvaluationException extends RuntimeException {
+ private static final long serialVersionUID = 1;
+
+ EvaluationException(String message) {
+ super(message);
+ }
+
+ EvaluationException(String message, Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/ExpressionNode.java b/src/main/java/com/google/escapevelocity/ExpressionNode.java
new file mode 100644
index 0000000..4ee29c5
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/ExpressionNode.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import com.google.escapevelocity.Parser.Operator;
+
+/**
+ * A node in the parse tree representing an expression. Expressions appear inside directives,
+ * specifically {@code #set}, {@code #if}, {@code #foreach}, and macro calls. Expressions can
+ * also appear inside indices in references, like {@code $x[$i]}.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+abstract class ExpressionNode extends Node {
+ ExpressionNode(String resourceName, int lineNumber) {
+ super(resourceName, lineNumber);
+ }
+
+ /**
+ * True if evaluating this expression yields a value that is considered true by Velocity's
+ *
+ * rules. A value is false if it is null or equal to Boolean.FALSE.
+ * Every other value is true.
+ *
+ *
Note that the text at the similar link
+ * here
+ * states that empty collections and empty strings are also considered false, but that is not
+ * true.
+ */
+ boolean isTrue(EvaluationContext context) {
+ Object value = evaluate(context);
+ if (value instanceof Boolean) {
+ return (Boolean) value;
+ } else {
+ return value != null;
+ }
+ }
+
+ /**
+ * True if this is a defined value and it evaluates to true. This is the same as {@link #isTrue}
+ * except that it is allowed for this to be undefined variable, in which it evaluates to false.
+ * The method is overridden for plain references so that undefined is the same as false.
+ * The reason is to support Velocity's idiom {@code #if ($var)}, where it is not an error
+ * if {@code $var} is undefined.
+ */
+ boolean isDefinedAndTrue(EvaluationContext context) {
+ return isTrue(context);
+ }
+
+ /**
+ * The integer result of evaluating this expression.
+ *
+ * @throws EvaluationException if evaluating the expression produces an exception, or if it
+ * yields a value that is not an integer.
+ */
+ int intValue(EvaluationContext context) {
+ Object value = evaluate(context);
+ if (!(value instanceof Integer)) {
+ throw evaluationException("Arithemtic is only available on integers, not " + show(value));
+ }
+ return (Integer) value;
+ }
+
+ /**
+ * Returns a string representing the given value, for use in error messages. The string
+ * includes both the value's {@code toString()} and its type.
+ */
+ private static String show(Object value) {
+ if (value == null) {
+ return "null";
+ } else {
+ return value + " (a " + value.getClass().getName() + ")";
+ }
+ }
+
+ /**
+ * Represents all binary expressions. In {@code #set ($a = $b + $c)}, this will be the type
+ * of the node representing {@code $b + $c}.
+ */
+ static class BinaryExpressionNode extends ExpressionNode {
+ final ExpressionNode lhs;
+ final Operator op;
+ final ExpressionNode rhs;
+
+ BinaryExpressionNode(ExpressionNode lhs, Operator op, ExpressionNode rhs) {
+ super(lhs.resourceName, lhs.lineNumber);
+ this.lhs = lhs;
+ this.op = op;
+ this.rhs = rhs;
+ }
+
+ @Override Object evaluate(EvaluationContext context) {
+ switch (op) {
+ case OR:
+ return lhs.isTrue(context) || rhs.isTrue(context);
+ case AND:
+ return lhs.isTrue(context) && rhs.isTrue(context);
+ case EQUAL:
+ return equal(context);
+ case NOT_EQUAL:
+ return !equal(context);
+ default: // fall out
+ }
+ int lhsInt = lhs.intValue(context);
+ int rhsInt = rhs.intValue(context);
+ switch (op) {
+ case LESS:
+ return lhsInt < rhsInt;
+ case LESS_OR_EQUAL:
+ return lhsInt <= rhsInt;
+ case GREATER:
+ return lhsInt > rhsInt;
+ case GREATER_OR_EQUAL:
+ return lhsInt >= rhsInt;
+ case PLUS:
+ return lhsInt + rhsInt;
+ case MINUS:
+ return lhsInt - rhsInt;
+ case TIMES:
+ return lhsInt * rhsInt;
+ case DIVIDE:
+ return lhsInt / rhsInt;
+ case REMAINDER:
+ return lhsInt % rhsInt;
+ default:
+ throw new AssertionError(op);
+ }
+ }
+
+ /**
+ * Returns true if {@code lhs} and {@code rhs} are equal according to Velocity.
+ *
+ *
Velocity's definition
+ * of equality differs depending on whether the objects being compared are of the same
+ * class. If so, equality comes from {@code Object.equals} as you would expect. But if they
+ * are not of the same class, they are considered equal if their {@code toString()} values are
+ * equal. This means that integer 123 equals long 123L and also string {@code "123"}. It also
+ * means that equality isn't always transitive. For example, two StringBuilder objects each
+ * containing {@code "123"} will not compare equal, even though the string {@code "123"}
+ * compares equal to each of them.
+ */
+ private boolean equal(EvaluationContext context) {
+ Object lhsValue = lhs.evaluate(context);
+ Object rhsValue = rhs.evaluate(context);
+ if (lhsValue == rhsValue) {
+ return true;
+ }
+ if (lhsValue == null || rhsValue == null) {
+ return false;
+ }
+ if (lhsValue.getClass().equals(rhsValue.getClass())) {
+ return lhsValue.equals(rhsValue);
+ }
+ // Funky equals behaviour specified by Velocity.
+ return lhsValue.toString().equals(rhsValue.toString());
+ }
+ }
+
+ /**
+ * A node in the parse tree representing an expression like {@code !$a}.
+ */
+ static class NotExpressionNode extends ExpressionNode {
+ private final ExpressionNode expr;
+
+ NotExpressionNode(ExpressionNode expr) {
+ super(expr.resourceName, expr.lineNumber);
+ this.expr = expr;
+ }
+
+ @Override Object evaluate(EvaluationContext context) {
+ return !expr.isTrue(context);
+ }
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/ImmutableAsciiSet.java b/src/main/java/com/google/escapevelocity/ImmutableAsciiSet.java
new file mode 100644
index 0000000..96a126c
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/ImmutableAsciiSet.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import java.util.AbstractSet;
+import java.util.BitSet;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * An immutable set of ASCII characters.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+class ImmutableAsciiSet extends AbstractSet {
+ private final BitSet bits;
+
+ ImmutableAsciiSet(BitSet bits) {
+ this.bits = bits;
+ }
+
+ static ImmutableAsciiSet of(char c) {
+ return ofRange(c, c);
+ }
+
+ static ImmutableAsciiSet ofRange(char from, char to) {
+ if (from > to) {
+ throw new IllegalArgumentException("from > to");
+ }
+ if (to >= 128) {
+ throw new IllegalArgumentException("Not ASCII");
+ }
+ BitSet bits = new BitSet();
+ bits.set(from, to + 1);
+ return new ImmutableAsciiSet(bits);
+ }
+
+ ImmutableAsciiSet union(ImmutableAsciiSet that) {
+ BitSet union = (BitSet) bits.clone();
+ union.or(that.bits);
+ return new ImmutableAsciiSet(union);
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ int i = -1;
+ if (o instanceof Character) {
+ i = (Character) o;
+ } else if (o instanceof Integer) {
+ i = (Integer) o;
+ }
+ return contains(i);
+ }
+
+ boolean contains(int i) {
+ if (i < 0) {
+ return false;
+ } else {
+ return bits.get(i);
+ }
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new Iterator() {
+ private int index;
+
+ @Override
+ public boolean hasNext() {
+ return bits.nextSetBit(index) >= 0;
+ }
+
+ @Override
+ public Integer next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ int next = bits.nextSetBit(index);
+ index = next + 1;
+ return next;
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return bits.cardinality();
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/ImmutableList.java b/src/main/java/com/google/escapevelocity/ImmutableList.java
new file mode 100644
index 0000000..0b903f7
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/ImmutableList.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * An immutable list.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+class ImmutableList extends AbstractList {
+ private static final ImmutableList> EMPTY = new ImmutableList<>(new Object[0]);
+
+ private final E[] elements;
+
+ private ImmutableList(E[] elements) {
+ this.elements = elements;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return Arrays.asList(elements).iterator();
+ }
+
+ @Override
+ public E get(int index) {
+ if (index < 0 || index >= elements.length) {
+ throw new IndexOutOfBoundsException(String.valueOf(index));
+ }
+ return elements[index];
+ }
+
+ @Override
+ public int size() {
+ return elements.length;
+ }
+
+ static ImmutableList of() {
+ @SuppressWarnings("unchecked")
+ ImmutableList empty = (ImmutableList) EMPTY;
+ return empty;
+ }
+
+ @SafeVarargs
+ static ImmutableList of(E... elements) {
+ return new ImmutableList<>(elements.clone());
+ }
+
+ static ImmutableList copyOf(List list) {
+ @SuppressWarnings("unchecked")
+ E[] elements = (E[]) new Object[list.size()];
+ list.toArray(elements);
+ return new ImmutableList<>(elements);
+ }
+
+ static Builder builder() {
+ return new Builder();
+ }
+
+ static class Builder {
+ private final List list = new ArrayList<>();
+
+ void add(E element) {
+ list.add(element);
+ }
+
+ ImmutableList build() {
+ if (list.isEmpty()) {
+ return ImmutableList.of();
+ }
+ @SuppressWarnings("unchecked")
+ E[] elements = (E[]) new Object[list.size()];
+ list.toArray(elements);
+ return new ImmutableList<>(elements);
+ }
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/ImmutableSet.java b/src/main/java/com/google/escapevelocity/ImmutableSet.java
new file mode 100644
index 0000000..f4e8e9f
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/ImmutableSet.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * An immutable set. This implementation is only suitable for sets with a small number of elements.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+class ImmutableSet extends AbstractSet {
+ private final E[] elements;
+
+ private ImmutableSet(E[] elements) {
+ this.elements = elements;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return Arrays.asList(elements).iterator();
+ }
+
+ @Override
+ public int size() {
+ return elements.length;
+ }
+
+ @SafeVarargs
+ static ImmutableSet of(E... elements) {
+ int len = elements.length;
+ for (int i = 0; i < len - 1; i++) {
+ for (int j = len - 1; j > i; j--) {
+ if (elements[i].equals(elements[j])) {
+ // We want to exclude elements[j] from the final set. We can do that by copying the
+ // current last element in place of j (this might be j itself) and then reducing the
+ // size of the set.
+ elements[j] = elements[len - 1];
+ len--;
+ }
+ }
+ }
+ return new ImmutableSet<>(Arrays.copyOf(elements, len));
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/Macro.java b/src/main/java/com/google/escapevelocity/Macro.java
new file mode 100644
index 0000000..151ded2
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/Macro.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * A macro definition. Macros appear in templates using the syntax {@code #macro (m $x $y) ... #end}
+ * and each one produces an instance of this class. Evaluating a macro involves setting the
+ * parameters (here {$x $y)} and evaluating the macro body. Macro arguments are call-by-name, which
+ * means that we need to set each parameter variable to the node in the parse tree that corresponds
+ * to it, and arrange for that node to be evaluated when the variable is actually referenced.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+class Macro {
+ private final int definitionLineNumber;
+ private final String name;
+ private final ImmutableList parameterNames;
+ private final Node body;
+
+ Macro(int definitionLineNumber, String name, List parameterNames, Node body) {
+ this.definitionLineNumber = definitionLineNumber;
+ this.name = name;
+ this.parameterNames = ImmutableList.copyOf(parameterNames);
+ this.body = body;
+ }
+
+ String name() {
+ return name;
+ }
+
+ int parameterCount() {
+ return parameterNames.size();
+ }
+
+ Object evaluate(EvaluationContext context, List thunks) {
+ try {
+ assert thunks.size() == parameterNames.size() : "Argument mistmatch for " + name;
+ Map parameterThunks = new LinkedHashMap<>();
+ for (int i = 0; i < parameterNames.size(); i++) {
+ parameterThunks.put(parameterNames.get(i), thunks.get(i));
+ }
+ EvaluationContext newContext = new MacroEvaluationContext(parameterThunks, context);
+ return body.evaluate(newContext);
+ } catch (EvaluationException e) {
+ EvaluationException newException = new EvaluationException(
+ "In macro #" + name + " defined on line " + definitionLineNumber + ": " + e.getMessage());
+ newException.setStackTrace(e.getStackTrace());
+ throw e;
+ }
+ }
+
+ /**
+ * The context for evaluation within macros. This wraps an existing {@code EvaluationContext}
+ * but intercepts reads of the macro's parameters so that they result in a call-by-name evaluation
+ * of whatever was passed as the parameter. For example, if you write...
+ *
+ * ...then the {@code #mymacro} call will result in {@code $foo.bar(23)} being evaluated twice,
+ * once for each time {@code $x} appears. The way this works is that {@code $x} is a thunk.
+ * Historically a thunk is a piece of code to evaluate an expression in the context where it
+ * occurs, for call-by-name procedures as in Algol 60. Here, it is not exactly a piece of code,
+ * but it has the same responsibility.
+ */
+ static class MacroEvaluationContext implements EvaluationContext {
+ private final Map parameterThunks;
+ private final EvaluationContext originalEvaluationContext;
+
+ MacroEvaluationContext(
+ Map parameterThunks, EvaluationContext originalEvaluationContext) {
+ this.parameterThunks = parameterThunks;
+ this.originalEvaluationContext = originalEvaluationContext;
+ }
+
+ @Override
+ public Object getVar(String var) {
+ Node thunk = parameterThunks.get(var);
+ if (thunk == null) {
+ return originalEvaluationContext.getVar(var);
+ } else {
+ // Evaluate the thunk in the context where it appeared, not in this context. Otherwise
+ // if you pass $x to a parameter called $x you would get an infinite recursion. Likewise
+ // if you had #macro(mymacro $x $y) and a call #mymacro($y 23), you would expect that $x
+ // would expand to whatever $y meant at the call site, rather than to the value of the $y
+ // parameter.
+ return thunk.evaluate(originalEvaluationContext);
+ }
+ }
+
+ @Override
+ public boolean varIsDefined(String var) {
+ return parameterThunks.containsKey(var) || originalEvaluationContext.varIsDefined(var);
+ }
+
+ @Override
+ public Runnable setVar(final String var, Object value) {
+ // Copy the behaviour that #set will shadow a macro parameter, even though the Velocity peeps
+ // seem to agree that that is not good.
+ final Node thunk = parameterThunks.get(var);
+ if (thunk == null) {
+ return originalEvaluationContext.setVar(var, value);
+ } else {
+ parameterThunks.remove(var);
+ final Runnable originalUndo = originalEvaluationContext.setVar(var, value);
+ return new Runnable() {
+ @Override
+ public void run() {
+ originalUndo.run();
+ parameterThunks.put(var, thunk);
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/Node.java b/src/main/java/com/google/escapevelocity/Node.java
new file mode 100644
index 0000000..eca745f
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/Node.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+/**
+ * A node in the parse tree.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+abstract class Node {
+ final String resourceName;
+ final int lineNumber;
+
+ Node(String resourceName, int lineNumber) {
+ this.resourceName = resourceName;
+ this.lineNumber = lineNumber;
+ }
+
+ /**
+ * Returns the result of evaluating this node in the given context. This result may be used as
+ * part of a further operation, for example evaluating {@code 2 + 3} to 5 in order to set
+ * {@code $x} to 5 in {@code #set ($x = 2 + 3)}. Or it may be used directly as part of the
+ * template output, for example evaluating replacing {@code name} by {@code Fred} in
+ * {@code My name is $name.}.
+ */
+ abstract Object evaluate(EvaluationContext context);
+
+ private String where() {
+ String where = "In expression on line " + lineNumber;
+ if (resourceName != null) {
+ where += " of " + resourceName;
+ }
+ return where;
+ }
+
+ EvaluationException evaluationException(String message) {
+ return new EvaluationException(where() + ": " + message);
+ }
+
+ EvaluationException evaluationException(Throwable cause) {
+ return new EvaluationException(where() + ": " + cause, cause);
+ }
+
+ /**
+ * Returns an empty node in the parse tree. This is used for example to represent the trivial
+ * "else" part of an {@code #if} that does not have an explicit {@code #else}.
+ */
+ static Node emptyNode(String resourceName, int lineNumber) {
+ return new Cons(resourceName, lineNumber, ImmutableList.of());
+ }
+
+
+ /**
+ * Create a new parse tree node that is the concatenation of the given ones. Evaluating the
+ * new node produces the same string as evaluating each of the given nodes and concatenating the
+ * result.
+ */
+ static Node cons(String resourceName, int lineNumber, ImmutableList nodes) {
+ return new Cons(resourceName, lineNumber, nodes);
+ }
+
+ private static final class Cons extends Node {
+ private final ImmutableList nodes;
+
+ Cons(String resourceName, int lineNumber, ImmutableList nodes) {
+ super(resourceName, lineNumber);
+ this.nodes = nodes;
+ }
+
+ @Override Object evaluate(EvaluationContext context) {
+ StringBuilder sb = new StringBuilder();
+ for (Node node : nodes) {
+ sb.append(node.evaluate(context));
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/ParseException.java b/src/main/java/com/google/escapevelocity/ParseException.java
new file mode 100644
index 0000000..241a192
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/ParseException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+/**
+ * An exception that occurred while parsing a template.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+public class ParseException extends RuntimeException {
+ private static final long serialVersionUID = 1;
+
+ ParseException(String message, String resourceName, int lineNumber) {
+ super(message + ", " + where(resourceName, lineNumber));
+ }
+
+ ParseException(String message, String resourceName, int lineNumber, String context) {
+ super(message + ", " + where(resourceName, lineNumber) + ", at text starting: " + context);
+ }
+
+ private static String where(String resourceName, int lineNumber) {
+ if (resourceName == null) {
+ return "on line " + lineNumber;
+ } else {
+ return "on line " + lineNumber + " of " + resourceName;
+ }
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/Parser.java b/src/main/java/com/google/escapevelocity/Parser.java
new file mode 100644
index 0000000..9982be3
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/Parser.java
@@ -0,0 +1,963 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import com.google.escapevelocity.DirectiveNode.SetNode;
+import com.google.escapevelocity.ExpressionNode.BinaryExpressionNode;
+import com.google.escapevelocity.ExpressionNode.NotExpressionNode;
+import com.google.escapevelocity.ReferenceNode.IndexReferenceNode;
+import com.google.escapevelocity.ReferenceNode.MemberReferenceNode;
+import com.google.escapevelocity.ReferenceNode.MethodReferenceNode;
+import com.google.escapevelocity.ReferenceNode.PlainReferenceNode;
+import com.google.escapevelocity.TokenNode.CommentTokenNode;
+import com.google.escapevelocity.TokenNode.ElseIfTokenNode;
+import com.google.escapevelocity.TokenNode.ElseTokenNode;
+import com.google.escapevelocity.TokenNode.EndTokenNode;
+import com.google.escapevelocity.TokenNode.EofNode;
+import com.google.escapevelocity.TokenNode.ForEachTokenNode;
+import com.google.escapevelocity.TokenNode.IfTokenNode;
+import com.google.escapevelocity.TokenNode.MacroDefinitionTokenNode;
+import com.google.escapevelocity.TokenNode.NestedTokenNode;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A parser that reads input from the given {@link Reader} and parses it to produce a
+ * {@link Template}.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+class Parser {
+ private static final int EOF = -1;
+
+ private final LineNumberReader reader;
+ private final String resourceName;
+ private final Template.ResourceOpener resourceOpener;
+
+ /**
+ * The invariant of this parser is that {@code c} is always the next character of interest.
+ * This means that we never have to "unget" a character by reading too far. For example, after
+ * we parse an integer, {@code c} will be the first character after the integer, which is exactly
+ * the state we will be in when there are no more digits.
+ */
+ private int c;
+
+ Parser(Reader reader, String resourceName, Template.ResourceOpener resourceOpener)
+ throws IOException {
+ this.reader = new LineNumberReader(reader);
+ this.reader.setLineNumber(1);
+ next();
+ this.resourceName = resourceName;
+ this.resourceOpener = resourceOpener;
+ }
+
+ /**
+ * Parse the input completely to produce a {@link Template}.
+ *
+ *
Parsing happens in two phases. First, we parse a sequence of "tokens", where tokens include
+ * entire references such as
+ * ${x.foo()[23]}
+ *
or entire directives such as
+ * #set ($x = $y + $z)
+ *
But tokens do not span complex constructs. For example,
The second phase then takes the sequence of tokens and constructs a parse tree out of it.
+ * Some nodes in the parse tree will be unchanged from the token sequence, such as the
+ * ${x.foo()[23]}
+ * #set ($x = $y + $z)
+ *
examples above. But a construct such as the {@code #if ... #end} mentioned above will
+ * become a single IfNode in the parse tree in the second phase.
+ *
+ *
The main reason for this approach is that Velocity has two kinds of lexical contexts. At the
+ * top level, there can be arbitrary literal text; references like ${x.foo()}; and
+ * directives like {@code #if} or {@code #set}. Inside the parentheses of a directive, however,
+ * neither arbitrary text nor directives can appear, but expressions can, so we need to tokenize
+ * the inside of
+ * #if ($x == $a + $b)
+ *
as the five tokens "$x", "==", "$a", "+", "$b". Rather than having a classical
+ * parser/lexer combination, where the lexer would need to switch between these two modes, we
+ * replace the lexer with an ad-hoc parser that is the first phase described above, and we
+ * define a simple parser over the resultant tokens that is the second phase.
+ */
+ Template parse() throws IOException {
+ ImmutableList tokens = parseTokens();
+ return new Reparser(tokens).reparse();
+ }
+
+ private ImmutableList parseTokens() throws IOException {
+ ImmutableList.Builder tokens = ImmutableList.builder();
+ Node token;
+ do {
+ token = parseNode();
+ tokens.add(token);
+ } while (!(token instanceof EofNode));
+ return tokens.build();
+ }
+
+ private int lineNumber() {
+ return reader.getLineNumber();
+ }
+
+ /**
+ * Gets the next character from the reader and assigns it to {@code c}. If there are no more
+ * characters, sets {@code c} to {@link #EOF} if it is not already.
+ */
+ private void next() throws IOException {
+ if (c != EOF) {
+ c = reader.read();
+ }
+ }
+
+ /**
+ * If {@code c} is a space character, keeps reading until {@code c} is a non-space character or
+ * there are no more characters.
+ */
+ private void skipSpace() throws IOException {
+ while (Character.isWhitespace(c)) {
+ next();
+ }
+ }
+
+ /**
+ * Gets the next character from the reader, and if it is a space character, keeps reading until
+ * a non-space character is found.
+ */
+ private void nextNonSpace() throws IOException {
+ next();
+ skipSpace();
+ }
+
+ /**
+ * Skips any space in the reader, and then throws an exception if the first non-space character
+ * found is not the expected one. Sets {@code c} to the first character after that expected one.
+ */
+ private void expect(char expected) throws IOException {
+ skipSpace();
+ if (c == expected) {
+ next();
+ } else {
+ throw parseException("Expected " + expected);
+ }
+ }
+
+ /**
+ * Parses a single node from the reader, as part of the first parsing phase.
+ *
{@code
+ * -> |
+ * |
+ *
+ * }
+ */
+ private Node parseNode() throws IOException {
+ if (c == '#') {
+ next();
+ if (c == '#') {
+ return parseComment();
+ } else if (isAsciiLetter(c) || c == '{') {
+ return parseDirective();
+ } else if (c == '[') {
+ return parseHashSquare();
+ } else {
+ // For consistency with Velocity, we treat # not followed by # or a letter as a plain
+ // character, and we treat #$foo as a literal # followed by the reference $foo.
+ // But the # is its own ConstantExpressionNode; we don't try to merge it with adjacent text.
+ return new ConstantExpressionNode(resourceName, lineNumber(), "#");
+ }
+ }
+ if (c == EOF) {
+ return new EofNode(resourceName, lineNumber());
+ }
+ return parseNonDirective();
+ }
+
+ private Node parseHashSquare() throws IOException {
+ // We've just seen #[ which might be the start of a #[[quoted block]]#. If the next character
+ // is not another [ then it's not a quoted block, but it *is* a literal #[ followed by whatever
+ // that next character is.
+ assert c == '[';
+ next();
+ if (c != '[') {
+ return new ConstantExpressionNode(resourceName, lineNumber(), "#[");
+ }
+ next();
+ StringBuilder sb = new StringBuilder();
+ while (true) {
+ if (c == EOF) {
+ throw parseException("Unterminated #[[ - did not see matching ]]#");
+ }
+ if (c == '#') {
+ // This might be the last character of ]]# or it might just be a random #.
+ int len = sb.length();
+ if (len > 1 && sb.charAt(len - 1) == ']' && sb.charAt(len - 2) == ']') {
+ next();
+ break;
+ }
+ }
+ sb.append((char) c);
+ next();
+ }
+ String quoted = sb.substring(0, sb.length() - 2);
+ return new ConstantExpressionNode(resourceName, lineNumber(), quoted);
+ }
+
+ /**
+ * Parses a single non-directive node from the reader.
+ *
{@code
+ * -> |
+ *
+ * }
+ */
+ private Node parseNonDirective() throws IOException {
+ if (c == '$') {
+ next();
+ if (isAsciiLetter(c) || c == '{') {
+ return parseReference();
+ } else {
+ return parsePlainText('$');
+ }
+ } else {
+ int firstChar = c;
+ next();
+ return parsePlainText(firstChar);
+ }
+ }
+
+ /**
+ * Parses a single directive token from the reader. Directives can be spelled with or without
+ * braces, for example {@code #if} or {@code #{if}}. We omit the brace spelling in the productions
+ * here:
+ */
+ private Node parseDirective() throws IOException {
+ String directive;
+ if (c == '{') {
+ next();
+ directive = parseId("Directive inside #{...}");
+ expect('}');
+ } else {
+ directive = parseId("Directive");
+ }
+ Node node;
+ switch (directive) {
+ case "end":
+ node = new EndTokenNode(resourceName, lineNumber());
+ break;
+ case "if":
+ case "elseif":
+ node = parseIfOrElseIf(directive);
+ break;
+ case "else":
+ node = new ElseTokenNode(resourceName, lineNumber());
+ break;
+ case "foreach":
+ node = parseForEach();
+ break;
+ case "set":
+ node = parseSet();
+ break;
+ case "parse":
+ node = parseParse();
+ break;
+ case "macro":
+ node = parseMacroDefinition();
+ break;
+ default:
+ node = parsePossibleMacroCall(directive);
+ }
+ // Velocity skips a newline after any directive.
+ // TODO(emcmanus): in fact it also skips space before the newline, which should be implemented.
+ if (c == '\n') {
+ next();
+ }
+ return node;
+ }
+
+ /**
+ * Parses the condition following {@code #if} or {@code #elseif}.
+ *
{@code
+ * -> #if ( )
+ * -> #elseif ( )
+ * }
+ *
+ * @param directive either {@code "if"} or {@code "elseif"}.
+ */
+ private Node parseIfOrElseIf(String directive) throws IOException {
+ expect('(');
+ ExpressionNode condition = parseExpression();
+ expect(')');
+ return directive.equals("if") ? new IfTokenNode(condition) : new ElseIfTokenNode(condition);
+ }
+
+ /**
+ * Parses a {@code #foreach} token from the reader.
{@code
+ * -> #foreach ( $ in )
+ * }
+ */
+ private Node parseForEach() throws IOException {
+ expect('(');
+ expect('$');
+ String var = parseId("For-each variable");
+ skipSpace();
+ boolean bad = false;
+ if (c != 'i') {
+ bad = true;
+ } else {
+ next();
+ if (c != 'n') {
+ bad = true;
+ }
+ }
+ if (bad) {
+ throw parseException("Expected 'in' for #foreach");
+ }
+ next();
+ ExpressionNode collection = parseExpression();
+ expect(')');
+ return new ForEachTokenNode(var, collection);
+ }
+
+ /**
+ * Parses a {@code #set} token from the reader.
{@code
+ * -> #set ( $ = )
+ * }
+ */
+ private Node parseSet() throws IOException {
+ expect('(');
+ expect('$');
+ String var = parseId("#set variable");
+ expect('=');
+ ExpressionNode expression = parseExpression();
+ expect(')');
+ return new SetNode(var, expression);
+ }
+
+ /**
+ * Parses a {@code #parse} token from the reader.
{@code
+ * -> #parse ( )
+ * }
+ *
+ *
The way this works is inconsistent with Velocity. In Velocity, the {@code #parse} directive
+ * is evaluated when it is encountered during template evaluation. That means that the argument
+ * can be a variable, and it also means that you can use {@code #if} to choose whether or not
+ * to do the {@code #parse}. Neither of those is true in EscapeVelocity. The contents of the
+ * {@code #parse} are integrated into the containing template pretty much as if they had been
+ * written inline. That also means that EscapeVelocity allows forward references to macros
+ * inside {@code #parse} directives, which Velocity does not.
+ */
+ private Node parseParse() throws IOException {
+ expect('(');
+ skipSpace();
+ if (c != '"') {
+ throw parseException("#parse only supported with string literal argument");
+ }
+ String nestedResourceName = readStringLiteral();
+ expect(')');
+ try (Reader nestedReader = resourceOpener.openResource(nestedResourceName)) {
+ Parser nestedParser = new Parser(nestedReader, nestedResourceName, resourceOpener);
+ ImmutableList nestedTokens = nestedParser.parseTokens();
+ return new NestedTokenNode(nestedResourceName, nestedTokens);
+ }
+ }
+
+ /**
+ * Parses a {@code #macro} token from the reader.
{@code
+ * -> #macro ( )
+ * -> |
+ * $
+ * }
+ *
+ *
Macro parameters are not separated by commas, though method-reference parameters are.
+ */
+ private Node parseMacroDefinition() throws IOException {
+ expect('(');
+ skipSpace();
+ String name = parseId("Macro name");
+ ImmutableList.Builder parameterNames = ImmutableList.builder();
+ while (true) {
+ skipSpace();
+ if (c == ')') {
+ next();
+ break;
+ }
+ if (c != '$') {
+ throw parseException("Macro parameters should look like $name");
+ }
+ next();
+ parameterNames.add(parseId("Macro parameter name"));
+ }
+ return new MacroDefinitionTokenNode(resourceName, lineNumber(), name, parameterNames.build());
+ }
+
+ /**
+ * Parses an identifier after {@code #} that is not one of the standard directives. The assumption
+ * is that it is a call of a macro that is defined in the template. Macro definitions are
+ * extracted from the template during the second parsing phase (and not during evaluation of the
+ * template as you might expect). This means that a macro can be called before it is defined.
+ *
{@code
+ * -> # ( )
+ * -> |
+ *
+ * -> | ,
+ * }
+ */
+ private Node parsePossibleMacroCall(String directive) throws IOException {
+ skipSpace();
+ if (c != '(') {
+ throw parseException("Unrecognized directive #" + directive);
+ }
+ next();
+ ImmutableList.Builder parameterNodes = ImmutableList.builder();
+ while (true) {
+ skipSpace();
+ if (c == ')') {
+ next();
+ break;
+ }
+ parameterNodes.add(parsePrimary());
+ if (c == ',') {
+ // The documentation doesn't say so, but you can apparently have an optional comma in
+ // macro calls.
+ next();
+ }
+ }
+ return new DirectiveNode.MacroCallNode(
+ resourceName, lineNumber(), directive, parameterNodes.build());
+ }
+
+ /**
+ * Parses and discards a comment, which is {@code ##} followed by any number of characters up to
+ * and including the next newline.
+ */
+ private Node parseComment() throws IOException {
+ int lineNumber = lineNumber();
+ while (c != '\n' && c != EOF) {
+ next();
+ }
+ next();
+ return new CommentTokenNode(resourceName, lineNumber);
+ }
+
+ /**
+ * Parses plain text, which is text that contains neither {@code $} nor {@code #}. The given
+ * {@code firstChar} is the first character of the plain text, and {@link #c} is the second
+ * (if the plain text is more than one character).
+ */
+ private Node parsePlainText(int firstChar) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ sb.appendCodePoint(firstChar);
+
+ literal:
+ while (true) {
+ switch (c) {
+ case EOF:
+ case '$':
+ case '#':
+ break literal;
+ default:
+ // Just some random character.
+ }
+ sb.appendCodePoint(c);
+ next();
+ }
+ return new ConstantExpressionNode(resourceName, lineNumber(), sb.toString());
+ }
+
+ /**
+ * Parses a reference, which is everything that can start with a {@code $}. References can
+ * optionally be enclosed in braces, so {@code $x} and {@code ${x}} are the same. Braces are
+ * useful when text after the reference would otherwise be parsed as part of it. For example,
+ * {@code ${x}y} is a reference to the variable {@code $x}, followed by the plain text {@code y}.
+ * Of course {@code $xy} would be a reference to the variable {@code $xy}.
+ *
{@code
+ * -> $ |
+ * ${}
+ * }
+ *
+ *
On entry to this method, {@link #c} is the character immediately after the {@code $}.
+ */
+ private ReferenceNode parseReference() throws IOException {
+ if (c == '{') {
+ next();
+ ReferenceNode node = parseReferenceNoBrace();
+ expect('}');
+ return node;
+ } else {
+ return parseReferenceNoBrace();
+ }
+ }
+
+ /**
+ * Parses a reference, in the simple form without braces.
+ *
{@code
+ * ->
+ * }
+ */
+ private ReferenceNode parseReferenceNoBrace() throws IOException {
+ String id = parseId("Reference");
+ ReferenceNode lhs = new PlainReferenceNode(resourceName, lineNumber(), id);
+ return parseReferenceSuffix(lhs);
+ }
+
+ /**
+ * Parses the modifiers that can appear at the tail of a reference.
+ *
{@code
+ * -> |
+ * |
+ *
+ * }
+ *
+ * @param lhs the reference node representing the first part of the reference
+ * {@code $x} in {@code $x.foo} or {@code $x.foo()}, or later {@code $x.y} in {@code $x.y.z}.
+ */
+ private ReferenceNode parseReferenceSuffix(ReferenceNode lhs) throws IOException {
+ switch (c) {
+ case '.':
+ return parseReferenceMember(lhs);
+ case '[':
+ return parseReferenceIndex(lhs);
+ default:
+ return lhs;
+ }
+ }
+
+ /**
+ * Parses a reference member, which is either a property reference like {@code $x.y} or a method
+ * call like {@code $x.y($z)}.
+ *
{@code
+ * -> .
+ * -> |
+ * ( )
+ * }
+ *
+ * @param lhs the reference node representing what appears to the left of the dot, like the
+ * {@code $x} in {@code $x.foo} or {@code $x.foo()}.
+ */
+ private ReferenceNode parseReferenceMember(ReferenceNode lhs) throws IOException {
+ assert c == '.';
+ next();
+ String id = parseId("Member");
+ ReferenceNode reference;
+ if (c == '(') {
+ reference = parseReferenceMethodParams(lhs, id);
+ } else {
+ reference = new MemberReferenceNode(lhs, id);
+ }
+ return parseReferenceSuffix(reference);
+ }
+
+ /**
+ * Parses the parameters to a method reference, like {@code $foo.bar($a, $b)}.
+ *
{@code
+ * -> |
+ *
+ * -> |
+ * ,
+ * }
+ *
+ * @param lhs the reference node representing what appears to the left of the dot, like the
+ * {@code $x} in {@code $x.foo()}.
+ */
+ private ReferenceNode parseReferenceMethodParams(ReferenceNode lhs, String id)
+ throws IOException {
+ assert c == '(';
+ nextNonSpace();
+ ImmutableList.Builder args = ImmutableList.builder();
+ if (c != ')') {
+ args.add(parseExpression());
+ while (c == ',') {
+ nextNonSpace();
+ args.add(parseExpression());
+ }
+ if (c != ')') {
+ throw parseException("Expected )");
+ }
+ }
+ assert c == ')';
+ next();
+ return new MethodReferenceNode(lhs, id, args.build());
+ }
+
+ /**
+ * Parses an index suffix to a method, like {@code $x[$i]}.
+ *
{@code
+ * -> [ ]
+ * }
+ *
+ * @param lhs the reference node representing what appears to the left of the dot, like the
+ * {@code $x} in {@code $x[$i]}.
+ */
+ private ReferenceNode parseReferenceIndex(ReferenceNode lhs) throws IOException {
+ assert c == '[';
+ next();
+ ExpressionNode index = parseExpression();
+ if (c != ']') {
+ throw parseException("Expected ]");
+ }
+ next();
+ ReferenceNode reference = new IndexReferenceNode(lhs, index);
+ return parseReferenceSuffix(reference);
+ }
+
+ enum Operator {
+ /**
+ * A dummy operator with low precedence. When parsing subexpressions, we always stop when we
+ * reach an operator of lower precedence than the "current precedence". For example, when
+ * parsing {@code 1 + 2 * 3 + 4}, we'll stop parsing the subexpression {@code * 3 + 4} when
+ * we reach the {@code +} because it has lower precedence than {@code *}. This dummy operator,
+ * then, behaves like {@code +} when the minimum precedence is {@code *}. We also return it
+ * if we're looking for an operator and don't find one. If this operator is {@code ⊙}, it's as
+ * if our expressions are bracketed with it, like {@code ⊙ 1 + 2 * 3 + 4 ⊙}.
+ */
+ STOP("", 0),
+
+ // If a one-character operator is a prefix of a two-character operator, like < and <=, then
+ // the one-character operator must come first.
+ OR("||", 1),
+ AND("&&", 2),
+ EQUAL("==", 3), NOT_EQUAL("!=", 3),
+ LESS("<", 4), LESS_OR_EQUAL("<=", 4), GREATER(">", 4), GREATER_OR_EQUAL(">=", 4),
+ PLUS("+", 5), MINUS("-", 5),
+ TIMES("*", 6), DIVIDE("/", 6), REMAINDER("%", 6);
+
+ final String symbol;
+ final int precedence;
+
+ Operator(String symbol, int precedence) {
+ this.symbol = symbol;
+ this.precedence = precedence;
+ }
+
+ @Override
+ public String toString() {
+ return symbol;
+ }
+ }
+
+ /**
+ * Maps a code point to the operators that begin with that code point. For example, maps
+ * {@code <} to {@code LESS} and {@code LESS_OR_EQUAL}.
+ */
+ private static final Map> CODE_POINT_TO_OPERATORS;
+ static {
+ Map> map = new HashMap<>();
+ for (Operator operator : Operator.values()) {
+ if (operator != Operator.STOP) {
+ Integer key = operator.symbol.codePointAt(0);
+ if (!map.containsKey(key)) {
+ map.put(key, new ArrayList());
+ }
+ map.get(key).add(operator);
+ }
+ }
+ CODE_POINT_TO_OPERATORS = Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * Parses an expression, which can occur within a directive like {@code #if} or {@code #set},
+ * or within a reference like {@code $x[$a + $b]} or {@code $x.m($a + $b)}.
+ *
+ */
+ private ExpressionNode parseExpression() throws IOException {
+ ExpressionNode lhs = parseUnaryExpression();
+ return new OperatorParser().parse(lhs, 1);
+ }
+
+ /**
+ * An operator-precedence parser for the binary operations we understand. It implements an
+ * algorithm from Wikipedia
+ * that uses recursion rather than having an explicit stack of operators and values.
+ */
+ private class OperatorParser {
+ /**
+ * The operator we have just scanned, in the same way that {@link #c} is the character we have
+ * just read. If we were not able to scan an operator, this will be {@link Operator#STOP}.
+ */
+ private Operator currentOperator;
+
+ OperatorParser() throws IOException {
+ nextOperator();
+ }
+
+ /**
+ * Parse a subexpression whose left-hand side is {@code lhs} and where we only consider
+ * operators with precedence at least {@code minPrecedence}.
+ *
+ * @return the parsed subexpression
+ */
+ ExpressionNode parse(ExpressionNode lhs, int minPrecedence) throws IOException {
+ while (currentOperator.precedence >= minPrecedence) {
+ Operator operator = currentOperator;
+ ExpressionNode rhs = parseUnaryExpression();
+ nextOperator();
+ while (currentOperator.precedence > operator.precedence) {
+ rhs = parse(rhs, currentOperator.precedence);
+ }
+ lhs = new BinaryExpressionNode(lhs, operator, rhs);
+ }
+ return lhs;
+ }
+
+ /**
+ * Updates {@link #currentOperator} to be an operator read from the input,
+ * or {@link Operator#STOP} if there is none.
+ */
+ private void nextOperator() throws IOException {
+ skipSpace();
+ List possibleOperators = CODE_POINT_TO_OPERATORS.get(c);
+ if (possibleOperators == null) {
+ currentOperator = Operator.STOP;
+ return;
+ }
+ int firstChar = c;
+ next();
+ Operator operator = null;
+ for (Operator possibleOperator : possibleOperators) {
+ if (possibleOperator.symbol.length() == 1) {
+ assert operator == null;
+ operator = possibleOperator;
+ } else if (possibleOperator.symbol.charAt(1) == c) {
+ next();
+ operator = possibleOperator;
+ }
+ }
+ if (operator == null) {
+ throw parseException("Expected " + possibleOperators.get(0) + ", not just " + firstChar);
+ }
+ currentOperator = operator;
+ }
+ }
+
+ /**
+ * Parses an expression not containing any operators (except inside parentheses).
+ *
+ */
+ private ExpressionNode parsePrimary() throws IOException {
+ ExpressionNode node;
+ if (c == '$') {
+ next();
+ node = parseReference();
+ } else if (c == '"') {
+ node = parseStringLiteral();
+ } else if (c == '-') {
+ // Velocity does not have a negation operator. If we see '-' it must be the start of a
+ // negative integer literal.
+ next();
+ node = parseIntLiteral("-");
+ } else if (isAsciiDigit(c)) {
+ node = parseIntLiteral("");
+ } else if (isAsciiLetter(c)) {
+ node = parseBooleanLiteral();
+ } else {
+ throw parseException("Expected an expression");
+ }
+ skipSpace();
+ return node;
+ }
+
+ private ExpressionNode parseStringLiteral() throws IOException {
+ return new ConstantExpressionNode(resourceName, lineNumber(), readStringLiteral());
+ }
+
+ private String readStringLiteral() throws IOException {
+ assert c == '"';
+ StringBuilder sb = new StringBuilder();
+ next();
+ while (c != '"') {
+ if (c == '\n' || c == EOF) {
+ throw parseException("Unterminated string constant");
+ }
+ if (c == '$' || c == '\\') {
+ // In real Velocity, you can have a $ reference expanded inside a "" string literal.
+ // There are also '' string literals where that is not so. We haven't needed that yet
+ // so it's not supported.
+ throw parseException(
+ "Escapes or references in string constants are not currently supported");
+ }
+ sb.appendCodePoint(c);
+ next();
+ }
+ next();
+ return sb.toString();
+ }
+
+ private ExpressionNode parseIntLiteral(String prefix) throws IOException {
+ StringBuilder sb = new StringBuilder(prefix);
+ while (isAsciiDigit(c)) {
+ sb.appendCodePoint(c);
+ next();
+ }
+ int value;
+ try {
+ value = Integer.parseInt(sb.toString());
+ } catch (NumberFormatException e) {
+ throw parseException("Invalid integer: " + sb);
+ }
+ return new ConstantExpressionNode(resourceName, lineNumber(), value);
+ }
+
+ /**
+ * Parses a boolean literal, either {@code true} or {@code false}.
+ * -> true |
+ * false
+ */
+ private ExpressionNode parseBooleanLiteral() throws IOException {
+ String s = parseId("Identifier without $");
+ boolean value;
+ if (s.equals("true")) {
+ value = true;
+ } else if (s.equals("false")) {
+ value = false;
+ } else {
+ throw parseException("Identifier in expression must be preceded by $ or be true or false");
+ }
+ return new ConstantExpressionNode(resourceName, lineNumber(), value);
+ }
+
+ private static final ImmutableAsciiSet ASCII_LETTER =
+ ImmutableAsciiSet.ofRange('A', 'Z')
+ .union(ImmutableAsciiSet.ofRange('a', 'z'));
+
+ private static final ImmutableAsciiSet ASCII_DIGIT =
+ ImmutableAsciiSet.ofRange('0', '9');
+
+ private static final ImmutableAsciiSet ID_CHAR =
+ ASCII_LETTER
+ .union(ASCII_DIGIT)
+ .union(ImmutableAsciiSet.of('-'))
+ .union(ImmutableAsciiSet.of('_'));
+
+ private static boolean isAsciiLetter(int c) {
+ return ASCII_LETTER.contains(c);
+ }
+
+ private static boolean isAsciiDigit(int c) {
+ return ASCII_DIGIT.contains(c);
+ }
+
+ private static boolean isIdChar(int c) {
+ return ID_CHAR.contains(c);
+ }
+
+ /**
+ * Parse an identifier as specified by the
+ * VTL
+ * . Identifiers are ASCII: starts with a letter, then letters, digits, {@code -} and
+ * {@code _}.
+ */
+ private String parseId(String what) throws IOException {
+ if (!isAsciiLetter(c)) {
+ throw parseException(what + " should start with an ASCII letter");
+ }
+ StringBuilder id = new StringBuilder();
+ while (isIdChar(c)) {
+ id.appendCodePoint(c);
+ next();
+ }
+ return id.toString();
+ }
+
+ /**
+ * Returns an exception to be thrown describing a parse error with the given message, and
+ * including information about where it occurred.
+ */
+ private ParseException parseException(String message) throws IOException {
+ StringBuilder context = new StringBuilder();
+ if (c == EOF) {
+ context.append("EOF");
+ } else {
+ int count = 0;
+ while (c != EOF && count < 20) {
+ context.appendCodePoint(c);
+ next();
+ count++;
+ }
+ if (c != EOF) {
+ context.append("...");
+ }
+ }
+ return new ParseException(message, resourceName, lineNumber(), context.toString());
+ }
+}
diff --git a/src/main/java/com/google/escapevelocity/README.md b/src/main/java/com/google/escapevelocity/README.md
new file mode 100644
index 0000000..0e9ff1e
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/README.md
@@ -0,0 +1,378 @@
+# EscapeVelocity summary
+
+EscapeVelocity is a templating engine that can be used from Java. It is a reimplementation of a subset of
+functionality from [Apache Velocity](http://velocity.apache.org/).
+
+This is not a supported Google product.
+
+For a fuller explanation of Velocity's functioning, see its
+[User Guide](http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html)
+
+If EscapeVelocity successfully produces a result from a template evaluation, that result should be
+the exact same string that Velocity produces. If not, that is a bug.
+
+EscapeVelocity has no facilities for HTML escaping and it is not appropriate for producing
+HTML output that might include portions of untrusted input.
+
+## Motivation
+
+Velocity has a convenient templating language. It is easy to read, and it has widespread support
+from tools such as editors and coding websites. However, *using* Velocity can prove difficult.
+Its use to generate Java code in the [AutoValue][AutoValue] annotation processor required many
+[workarounds][VelocityHacks]. The way it dynamically loads classes as part of its standard operation
+makes it hard to [shade](https://maven.apache.org/plugins/maven-shade-plugin/) it, which in the case
+of AutoValue led to interference if Velocity was used elsewhere in a project.
+
+EscapeVelocity has a simple API that does not involve any class-loading or other sources of
+problems. It and its dependencies can be shaded with no difficulty.
+
+## Loading a template
+
+The entry point for EscapeVelocity is the `Template` class. To obtain an instance, use
+`Template.from(Reader)`. If a template is stored in a file, that file conventionally has the
+suffix `.vm` (for Velocity Macros). But since the argument is a `Reader`, you can also load
+a template directly from a Java string, using `StringReader`.
+
+Here's how you might make a `Template` instance from a template file that is packaged as a resource
+in the same package as the calling class:
+
+```java
+InputStream in = getClass().getResourceAsStream("foo.vm");
+if (in == null) {
+ throw new IllegalArgumentException("Could not find resource foo.vm");
+}
+Reader reader = new BufferedReader(new InputStreamReader(in));
+Template template = Template.parseFrom(reader);
+```
+
+## Expanding a template
+
+Once you have a `Template` object, you can use it to produce a string where the variables in the
+template are given the values you provide. You can do this any number of times, specifying the
+same or different values each time.
+
+Suppose you have this template:
+
+```
+The $language word for $original is $translated.
+```
+
+You might write this code:
+
+```java
+Map vars = new HashMap<>();
+vars.put("language", "French");
+vars.put("original", "toe");
+vars.put("translated", "orteil");
+String result = template.evaluate(vars);
+```
+
+The `result` string would then be: `The French word for toe is orteil.`
+
+## Comments
+
+The characters `##` introduce a comment. Characters from `##` up to and including the following
+newline are omitted from the template. This template has comments:
+
+```
+Line 1 ## with a comment
+Line 2
+```
+
+It is the same as this template:
+```
+Line 1 Line 2
+```
+
+## References
+
+EscapeVelocity supports most of the reference types described in the
+[Velocity User Guide](http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html#References)
+
+### Variables
+
+A variable has an ASCII name that starts with a letter (a-z or A-Z) and where any other characters
+are also letters or digits or hyphens (-) or underscores (_). A variable reference can be written
+as `$foo` or as `${foo}`. The value of a variable can be of any Java type. If the value `v` of
+variable `foo` is not a String then the result of `$foo` in a template will be `String.valueOf(v)`.
+Variables must be defined before they are referenced; otherwise an `EvaluationException` will be
+thrown.
+
+Variable names are case-sensitive: `$foo` is not the same variable as `$Foo` or `$FOO`.
+
+Initially the values of variables come from the Map that is passed to `Template.evaluate`. Those
+values can be changed, and new ones defined, using the `#set` directive in the template:
+
+```
+#set ($foo = "bar")
+```
+
+Setting a variable affects later references to it in the template, but has no effect on the
+`Map` that was passed in or on later template evaluations.
+
+### Properties
+
+If a reference looks like `$purchase.Total` then the value of the `$purchase` variable must be a
+Java object that has a public method `getTotal()` or `gettotal()`, or a method called `isTotal()` or
+`istotal()` that returns `boolean`. The result of `$purchase.Total` is then the result of calling
+that method on the `$purchase` object.
+
+If you want to have a period (`.`) after a variable reference *without* it being a property
+reference, you can use braces like this: `${purchase}.Total`. If, after a property reference, you
+have a further period, you can put braces around the reference like this:
+`${purchase.Total}.nonProperty`.
+
+### Methods
+
+If a reference looks like `$purchase.addItem("scones", 23)` then the value of the `$purchase`
+variable must be a Java object that has a public method `addItem` with two parameters that match
+the given values. Unlike Velocity, EscapeVelocity requires that there be exactly one such method.
+It is OK if there are other `addItem` methods provided they are not compatible with the
+arguments provided.
+
+Properties are in fact a special case of methods: instead of writing `$purchase.Total` you could
+write `$purchase.getTotal()`. Braces can be used to make the method invocation explicit
+(`${purchase.getTotal()}`) or to prevent method invocation (`${purchase}.getTotal()`).
+
+### Indexing
+
+If a reference looks like `$indexme[$i]` then the value of the `$indexme` variable must be a Java
+object that has a public `get` method that takes one argument that is compatible with the index.
+For example, `$indexme` might be a `List` and `$i` might be an integer. Then the reference would
+be the result of `List.get(int)` for that list and that integer. Or, `$indexme` might be a `Map`,
+and the reference would be the result of `Map.get(Object)` for the object `$i`. In general,
+`$indexme[$i]` is equivalent to `$indexme.get($i)`.
+
+Unlike Velocity, EscapeVelocity does not allow `$indexme` to be a Java array.
+
+### Undefined references
+
+If a variable has not been given a value, either by being in the initial Map argument or by being
+set in the template, then referencing it will provoke an `EvaluationException`. There is
+a special case for `#if`: if you write `#if ($var)` then it is allowed for `$var` not to be defined,
+and it is treated as false.
+
+### Setting properties and indexes: not supported
+
+Unlke Velocity, EscapeVelocity does not allow `#set` assignments with properties or indexes:
+
+```
+#set ($data.User = "jon") ## Allowed in Velocity but not in EscapeVelocity
+#set ($map["apple"] = "orange") ## Allowed in Velocity but not in EscapeVelocity
+```
+
+## Expressions
+
+In certain contexts, such as the `#set` directive we have just seen or certain other directives,
+EscapeVelocity can evaluate expressions. An expression can be any of these:
+
+* A reference, of the kind we have just seen. The value is the value of the reference.
+* A string literal enclosed in double quotes, like `"this"`. A string literal must appear on
+ one line. EscapeVelocity does not support the characters `$` or `\\` in a string literal.
+* An integer literal such as `23` or `-100`. EscapeVelocity does not support floating-point
+ literals.
+* A Boolean literal, `true` or `false`.
+* Simpler expressions joined together with operators that have the same meaning as in Java:
+ `!`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `+`, `-`, `*`, `/`, `%`. The operators have the
+ same precedence as in Java.
+* A simpler expression in parentheses, for example `(2 + 3)`.
+
+Velocity supports string literals with single quotes, like `'this`' and also references within
+strings, like `"a $reference in a string"`, but EscapeVelocity does not.
+
+## Directives
+
+A directive is introduced by a `#` character followed by a word. We have already seen the `#set`
+directive, which sets the value of a variable. The other directives are listed below.
+
+Directives can be spelled with or without braces, so `#set` or `#{set}`.
+
+### `#if`/`#elseif`/`#else`
+
+The `#if` directive selects parts of the template according as a condition is true or false.
+The simplest case looks like this:
+
+```
+#if ($condition) yes #end
+```
+
+This evaluates to the string ` yes ` if the variable `$condition` is defined and has a true value,
+and to the empty string otherwise. It is allowed for `$condition` not to be defined in this case,
+and then it is treated as false.
+
+The expression in `#if` (here `$condition`) is considered true if its value is not null and not
+equal to the Boolean value `false`.
+
+An `#if` directive can also have an `#else` part, for example:
+
+```
+#if ($condition) yes #else no #end
+```
+
+This evaluates to the string ` yes ` if the condition is true or the string ` no ` if it is not.
+
+An `#if` directive can have any number of `#elseif` parts. For example:
+
+```
+#if ($i == 0) zero #elseif ($i == 1) one #elseif ($i == 2) two #else many #end
+```
+
+### `#foreach`
+
+The `#foreach` directive repeats a part of the template once for each value in a list.
+
+```
+#foreach ($product in $allProducts)
+ ${product}!
+#end
+```
+
+This will produce one line for each value in the `$allProducts` variable. The value of
+`$allProducts` can be a Java `Iterable`, such as a `List` or `Set`; or it can be an object array;
+or it can be a Java `Map`. When it is a `Map` the `#foreach` directive loops over every *value*
+in the `Map`.
+
+If `$allProducts` is a `List` containing the strings `oranges` and `lemons` then the result of the
+`#foreach` would be this:
+
+```
+
+ oranges!
+
+
+ lemons!
+
+```
+
+When the `#foreach` completes, the loop variable (`$product` in the example) goes back to whatever
+value it had before, or to being undefined if it was undefined before.
+
+Within the `#foreach`, a special variable `$foreach` is defined, such that you can write
+`$foreach.hasNext`, which will be true if there are more values after this one or false if this
+is the last value. For example:
+
+```
+#foreach ($product in $allProducts)${product}#if ($foreach.hasNext), #end#end
+```
+
+This would produce the output `oranges, lemons` for the list above. (The example is scrunched up
+to avoid introducing extraneous spaces, as described in the [section](#spaces) on spaces
+below.)
+
+Velocity gives the `$foreach` variable other properties (`index` and `count`) but EscapeVelocity
+does not.
+
+### Macros
+
+A macro is a part of the template that can be reused in more than one place, potentially with
+different parameters each time. In the simplest case, a macro has no arguments:
+
+```
+#macro (hello) bonjour #end
+```
+
+Then the macro can be referenced by writing `#hello()` and the result will be the string ` bonjour `
+inserted at that point.
+
+Macros can also have parameters:
+
+```
+#macro (greet $hello $world) $hello, $world! #end
+```
+
+Then `#greet("bonjour", "monde")` would produce ` bonjour, monde! `. The comma is optional, so
+you could also write `#greet("bonjour" "monde")`.
+
+When a macro completes, the parameters (`$hello` and `$world` in the example) go back to whatever
+values they had before, or to being undefined if they were undefined before.
+
+All macro definitions take effect before the template is evaluated, so you can use a macro at a
+point in the template that is before the point where it is defined. This also means that you can't
+define a macro conditionally:
+
+```
+## This doesn't work!
+#if ($language == "French")
+#macro (hello) bonjour #end
+#else
+#macro (hello) hello #end
+#end
+```
+
+There is no particular reason to define the same macro more than once, but if you do it is the
+first definition that is retained. In the `#if` example just above, the `bonjour` version will
+always be used.
+
+Macros can make templates hard to understand. You may prefer to put the logic in a Java method
+rather than a macro, and call the method from the template using `$methods.doSomething("foo")`
+or whatever.
+
+## Block quoting
+
+If you have text that should be treated verbatim, you can enclose it in `#[[...]]#`. The text
+represented by `...` will be copied into the output. `#` and `$` characters will have no
+effect in that text.
+
+```
+#[[ This is not a #directive, and this is not a $variable. ]]#
+```
+
+## Including other templates
+
+If you want to include a template from another file, you can use the `#parse` directive.
+This can be useful if you have macros that are shared between templates, for example.
+
+```
+#set ($foo = "bar")
+#parse("macros.vm")
+#mymacro($foo) ## #mymacro defined in macros.vm
+```
+
+For this to work, you will need to tell EscapeVelocity how to find "resources" such as
+`macro.vm` in the example. You might use something like this:
+
+```
+ResourceOpener resourceOpener = resourceName -> {
+ InputStream inputStream = getClass().getResource(resourceName);
+ if (inputStream == null) {
+ throw new IOException("Unknown resource: " + resourceName);
+ }
+ return new BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8));
+};
+Template template = Template.parseFrom("foo.vm", resourceOpener);
+```
+
+In this case, the `resourceOpener` is used to find the main template `foo.vm`, as well as any
+templates it may reference in `#parse` directives.
+
+## Spaces
+
+For the most part, spaces and newlines in the template are preserved exactly in the output.
+To avoid unwanted newlines, you may end up using `##` comments. In the `#foreach` example above
+we had this:
+
+```
+#foreach ($product in $allProducts)${product}#if ($foreach.hasNext), #end#end
+```
+
+That was to avoid introducing unwanted spaces and newlines. A more readable way to achieve the same
+result is this:
+
+```
+#foreach ($product in $allProducts)##
+${product}##
+#if ($foreach.hasNext), #end##
+#end
+```
+
+Spaces are ignored between the `#` of a directive and the `)` that closes it, so there is no trace
+in the output of the spaces in `#foreach ($product in $allProducts)` or `#if ($foreach.hasNext)`.
+Spaces are also ignored inside references, such as `$indexme[ $i ]` or `$callme( $i , $j )`.
+
+If you are concerned about the detailed formatting of the text from the template, you may want to
+post-process it. For example, if it is Java code, you could use a formatter such as
+[google-java-format](https://github.com/google/google-java-format). Then you shouldn't have to
+worry about extraneous spaces.
+
+[VelocityHacks]: https://github.com/google/auto/blob/ca2384d5ad15a0c761b940384083cf5c50c6e839/value/src/main/java/com/google/auto/value/processor/TemplateVars.java#L54
+[AutoValue]: https://github.com/google/auto/tree/master/value
diff --git a/src/main/java/com/google/escapevelocity/ReferenceNode.java b/src/main/java/com/google/escapevelocity/ReferenceNode.java
new file mode 100644
index 0000000..865d02a
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/ReferenceNode.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2015 Google, Inc.
+ *
+ * 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.
+ */
+package com.google.escapevelocity;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A node in the parse tree that is a reference. A reference is anything beginning with {@code $},
+ * such as {@code $x} or {@code $x[$i].foo($j)}.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+abstract class ReferenceNode extends ExpressionNode {
+ ReferenceNode(String resourceName, int lineNumber) {
+ super(resourceName, lineNumber);
+ }
+
+ /**
+ * A node in the parse tree that is a plain reference such as {@code $x}. This node may appear
+ * inside a more complex reference like {@code $x.foo}.
+ */
+ static class PlainReferenceNode extends ReferenceNode {
+ final String id;
+
+ PlainReferenceNode(String resourceName, int lineNumber, String id) {
+ super(resourceName, lineNumber);
+ this.id = id;
+ }
+
+ @Override Object evaluate(EvaluationContext context) {
+ if (context.varIsDefined(id)) {
+ return context.getVar(id);
+ } else {
+ throw evaluationException("Undefined reference $" + id);
+ }
+ }
+
+ @Override
+ boolean isDefinedAndTrue(EvaluationContext context) {
+ if (context.varIsDefined(id)) {
+ return isTrue(context);
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * A node in the parse tree that is a reference to a property of another reference, like
+ * {@code $x.foo} or {@code $x[$i].foo}.
+ */
+ static class MemberReferenceNode extends ReferenceNode {
+ final ReferenceNode lhs;
+ final String id;
+
+ MemberReferenceNode(ReferenceNode lhs, String id) {
+ super(lhs.resourceName, lhs.lineNumber);
+ this.lhs = lhs;
+ this.id = id;
+ }
+
+ private static final String[] PREFIXES = {"get", "is"};
+ private static final boolean[] CHANGE_CASE = {false, true};
+
+ @Override Object evaluate(EvaluationContext context) {
+ Object lhsValue = lhs.evaluate(context);
+ if (lhsValue == null) {
+ throw evaluationException("Cannot get member " + id + " of null value");
+ }
+ // Velocity specifies that, given a reference .foo, it will first look for getfoo() and then
+ // for getFoo(), and likewise given .Foo it will look for getFoo() and then getfoo().
+ for (String prefix : PREFIXES) {
+ for (boolean changeCase : CHANGE_CASE) {
+ String baseId = changeCase ? changeInitialCase(id) : id;
+ String methodName = prefix + baseId;
+ Method method;
+ try {
+ method = lhsValue.getClass().getMethod(methodName);
+ if (!prefix.equals("is") || method.getReturnType().equals(boolean.class)) {
+ // Don't consider methods that happen to be called isFoo() but don't return boolean.
+ return invokeMethod(method, lhsValue, ImmutableList.of());
+ }
+ } catch (NoSuchMethodException e) {
+ // Continue with next possibility
+ }
+ }
+ }
+ throw evaluationException(
+ "Member " + id + " does not correspond to a public getter of " + lhsValue
+ + ", a " + lhsValue.getClass().getName());
+ }
+
+ private static String changeInitialCase(String id) {
+ int initial = id.codePointAt(0);
+ String rest = id.substring(Character.charCount(initial));
+ if (Character.isUpperCase(initial)) {
+ initial = Character.toLowerCase(initial);
+ } else if (Character.isLowerCase(initial)) {
+ initial = Character.toUpperCase(initial);
+ }
+ return new StringBuilder().appendCodePoint(initial).append(rest).toString();
+ }
+ }
+
+ /**
+ * A node in the parse tree that is an indexing of a reference, like {@code $x[0]} or
+ * {@code $x.foo[$i]}. Indexing is array indexing or calling the {@code get} method of a list
+ * or a map.
+ */
+ static class IndexReferenceNode extends ReferenceNode {
+ final ReferenceNode lhs;
+ final ExpressionNode index;
+
+ IndexReferenceNode(ReferenceNode lhs, ExpressionNode index) {
+ super(lhs.resourceName, lhs.lineNumber);
+ this.lhs = lhs;
+ this.index = index;
+ }
+
+ @Override Object evaluate(EvaluationContext context) {
+ Object lhsValue = lhs.evaluate(context);
+ if (lhsValue == null) {
+ throw evaluationException("Cannot index null value");
+ }
+ if (lhsValue instanceof List>) {
+ Object indexValue = index.evaluate(context);
+ if (!(indexValue instanceof Integer)) {
+ throw evaluationException("List index is not an integer: " + indexValue);
+ }
+ List> lhsList = (List>) lhsValue;
+ int i = (Integer) indexValue;
+ if (i < 0 || i >= lhsList.size()) {
+ throw evaluationException(
+ "List index " + i + " is not valid for list of size " + lhsList.size());
+ }
+ return lhsList.get(i);
+ } else if (lhsValue instanceof Map, ?>) {
+ Object indexValue = index.evaluate(context);
+ Map, ?> lhsMap = (Map, ?>) lhsValue;
+ return lhsMap.get(indexValue);
+ } else {
+ // In general, $x[$y] is equivalent to $x.get($y). We've covered the most common cases
+ // above, but for other cases like Multimap we resort to evaluating the equivalent form.
+ MethodReferenceNode node = new MethodReferenceNode(lhs, "get", ImmutableList.of(index));
+ return node.evaluate(context);
+ }
+ }
+ }
+
+ /**
+ * A node in the parse tree representing a method reference, like {@code $list.size()}.
+ */
+ static class MethodReferenceNode extends ReferenceNode {
+ final ReferenceNode lhs;
+ final String id;
+ final List args;
+
+ MethodReferenceNode(ReferenceNode lhs, String id, List args) {
+ super(lhs.resourceName, lhs.lineNumber);
+ this.lhs = lhs;
+ this.id = id;
+ this.args = args;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
Evaluating a method expression such as {@code $x.foo($y)} involves looking at the actual
+ * types of {@code $x} and {@code $y}. The type of {@code $x} must have a public method
+ * {@code foo} with a parameter type that is compatible with {@code $y}.
+ *
+ *
Currently we don't allow there to be more than one matching method. That is a difference
+ * from Velocity, which blithely allows you to invoke {@link List#remove(int)} even though it
+ * can't really know that you didn't mean to invoke {@link List#remove(Object)} with an Object
+ * that just happens to be an Integer.
+ *
+ *
The method to be invoked must be visible in a public class or interface that is either the
+ * class of {@code $x} itself or one of its supertypes. Allowing supertypes is important because
+ * you may want to invoke a public method like {@link List#size()} on a list whose class is not
+ * public, such as the list returned by {@link java.util.Collections#singletonList}.
+ */
+ @Override Object evaluate(EvaluationContext context) {
+ Object lhsValue = lhs.evaluate(context);
+ if (lhsValue == null) {
+ throw evaluationException("Cannot invoke method " + id + " on null value");
+ }
+ List