Initial version.

This is forked from the code built in to AutoValue, with the following non-trivial changes:

(1) Package changed from com.google.auto.value.processor.escapevelocity to com.google.escapevelocity.

(2) New pom.xml.

(3) Code rewritten to remove Guava dependency, so no shading or diamond dependency problems.
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 <https://cla.developers.google.com/> 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.
+
+<!-- MOE:begin_strip -->
+[TOC]
+<!-- MOE:end_strip -->
+
+## 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<String, String> 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.
+
+## <a name="spaces"></a> 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>com.google.escapevelocity</groupId>
+  <artifactId>escapevelocity</artifactId>
+  <version>0.9-SNAPSHOT</version>
+  <name>EscapeVelocity</name>
+  <description>
+    A reimplementation of a subset of the Apache Velocity templating system.
+  </description>
+
+  <!-- TODO(emcmanus)
+  <scm>
+    <url>http://github.com/google/auto</url>
+    <connection>scm:git:git://github.com/google/auto.git</connection>
+    <developerConnection>scm:git:ssh://git@github.com/google/auto.git</developerConnection>
+    <tag>HEAD</tag>
+  </scm>
+  -->
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <version>23.5-jre</version>
+      <scope>test</scope>
+    </dependency>
+    <!-- test dependencies -->
+    <dependency>
+      <groupId>org.apache.velocity</groupId>
+      <artifactId>velocity</artifactId>
+      <version>1.7</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava-testlib</artifactId>
+      <version>23.5-jre</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <version>0.36</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.7.0</version>
+        <configuration>
+          <source>1.7</source>
+          <target>1.7</target>
+          <compilerArgument>-Xlint:all</compilerArgument>
+          <showWarnings>true</showWarnings>
+          <showDeprecation>true</showDeprecation>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <version>3.0.2</version>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-invoker-plugin</artifactId>
+        <version>3.0.1</version>
+      </plugin>
+    </plugins>
+  </build>
+</project>
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...
+ * <pre>{@code
+ * abc#{if}($x == 5)def#{end}xyz
+ * }</pre>
+ * ...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.
+   *
+   * <p>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 <i>true</i> subtree and a <i>false</i> 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.
+   *
+   * <p>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<Node> thunks;
+    private Macro macro;
+
+    MacroCallNode(
+        String resourceName,
+        int lineNumber,
+        String name,
+        ImmutableList<Node> 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<String, Object> vars;
+
+    PlainEvaluationContext(Map<String, ?> vars) {
+      this.vars = new TreeMap<String, Object>(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
+   * <a href="http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html#Conditionals">
+   * rules</a>.  A value is false if it is null or equal to Boolean.FALSE.
+   * Every other value is true.
+   *
+   * <p>Note that the text at the similar link
+   * <a href="http://velocity.apache.org/engine/devel/user-guide.html#Conditionals">here</a>
+   * 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.
+     *
+     * <p>Velocity's <a
+     * href="http://velocity.apache.org/engine/releases/velocity-1.7/vtl-reference-guide.html#aifelseifelse_-_Output_conditional_on_truth_of_statements">definition
+     * of equality</a> 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<Integer> {
+  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<Integer> iterator() {
+    return new Iterator<Integer>() {
+      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<E> extends AbstractList<E> {
+  private static final ImmutableList<?> EMPTY = new ImmutableList<>(new Object[0]);
+
+  private final E[] elements;
+
+  private ImmutableList(E[] elements) {
+    this.elements = elements;
+  }
+
+  @Override
+  public Iterator<E> 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 <E> ImmutableList<E> of() {
+    @SuppressWarnings("unchecked")
+    ImmutableList<E> empty = (ImmutableList<E>) EMPTY;
+    return empty;
+  }
+
+  @SafeVarargs
+  static <E> ImmutableList<E> of(E... elements) {
+    return new ImmutableList<>(elements.clone());
+  }
+
+  static <E> ImmutableList<E> copyOf(List<E> list) {
+    @SuppressWarnings("unchecked")
+    E[] elements = (E[]) new Object[list.size()];
+    list.toArray(elements);
+    return new ImmutableList<>(elements);
+  }
+
+  static <E> Builder<E> builder() {
+    return new Builder<E>();
+  }
+
+  static class Builder<E> {
+    private final List<E> list = new ArrayList<>();
+
+    void add(E element) {
+      list.add(element);
+    }
+
+    ImmutableList<E> 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<E> extends AbstractSet<E> {
+  private final E[] elements;
+
+  private ImmutableSet(E[] elements) {
+    this.elements = elements;
+  }
+
+  @Override
+  public Iterator<E> iterator() {
+    return Arrays.asList(elements).iterator();
+  }
+
+  @Override
+  public int size() {
+    return elements.length;
+  }
+
+  @SafeVarargs
+  static <E> ImmutableSet<E> 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<String> parameterNames;
+  private final Node body;
+
+  Macro(int definitionLineNumber, String name, List<String> 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<Node> thunks) {
+    try {
+      assert thunks.size() == parameterNames.size() : "Argument mistmatch for " + name;
+      Map<String, Node> 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...
+   * <pre>{@code
+   * #macro (mymacro $x)
+   * $x $x
+   * #end
+   * #mymacro($foo.bar(23))
+   * }</pre>
+   * ...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 <i>thunk</i>.
+   * 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<String, Node> parameterThunks;
+    private final EvaluationContext originalEvaluationContext;
+
+    MacroEvaluationContext(
+        Map<String, Node> 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.<Node>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<Node> nodes) {
+    return new Cons(resourceName, lineNumber, nodes);
+  }
+
+  private static final class Cons extends Node {
+    private final ImmutableList<Node> nodes;
+
+    Cons(String resourceName, int lineNumber, ImmutableList<Node> 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}.
+   *
+   * <p>Parsing happens in two phases. First, we parse a sequence of "tokens", where tokens include
+   * entire references such as <pre>
+   *    ${x.foo()[23]}
+   * </pre>or entire directives such as<pre>
+   *    #set ($x = $y + $z)
+   * </pre>But tokens do not span complex constructs. For example,<pre>
+   *    #if ($x == $y) something #end
+   * </pre>is three tokens:<pre>
+   *    #if ($x == $y)
+   *    (literal text " something ")
+   *   #end
+   * </pre>
+   *
+   * <p>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 <pre>
+   *    ${x.foo()[23]}
+   *    #set ($x = $y + $z)
+   * </pre> 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.
+   *
+   * <p>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 <code>${x.foo()}</code>; 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 <pre>
+   *    #if ($x == $a + $b)
+   * </pre> 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<Node> tokens = parseTokens();
+    return new Reparser(tokens).reparse();
+  }
+
+  private ImmutableList<Node> parseTokens() throws IOException {
+    ImmutableList.Builder<Node> 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.
+   * <pre>{@code
+   * <template> -> <empty> |
+   *               <directive> <template> |
+   *               <non-directive> <template>
+   * }</pre>
+   */
+  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.
+   * <pre>{@code
+   * <non-directive> -> <reference> |
+   *                    <text containing neither $ nor #>
+   * }</pre>
+   */
+  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: <pre>{@code
+   * <directive> -> <if-token> |
+   *                <else-token> |
+   *                <elseif-token> |
+   *                <end-token> |
+   *                <foreach-token> |
+   *                <set-token> |
+   *                <parse-token> |
+   *                <macro-token> |
+   *                <macro-call> |
+   *                <comment>
+   * }</pre>
+   */
+  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}.
+   * <pre>{@code
+   * <if-token> -> #if ( <condition> )
+   * <elseif-token> -> #elseif ( <condition> )
+   * }</pre>
+   *
+   * @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. <pre>{@code
+   * <foreach-token> -> #foreach ( $<id> in <expression> )
+   * }</pre>
+   */
+  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. <pre>{@code
+   * <set-token> -> #set ( $<id> = <expression>)
+   * }</pre>
+   */
+  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. <pre>{@code
+   * <parse-token> -> #parse ( <string-literal> )
+   * }</pre>
+   *
+   * <p>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<Node> nestedTokens = nestedParser.parseTokens();
+      return new NestedTokenNode(nestedResourceName, nestedTokens);
+    }
+  }
+
+  /**
+   * Parses a {@code #macro} token from the reader. <pre>{@code
+   * <macro-token> -> #macro ( <id> <macro-parameter-list> )
+   * <macro-parameter-list> -> <empty> |
+   *                           $<id> <macro-parameter-list>
+   * }</pre>
+   *
+   * <p>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<String> 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.
+   * <pre>{@code
+   * <macro-call> -> # <id> ( <expression-list> )
+   * <expression-list> -> <empty> |
+   *                      <expression> <optional-comma> <expression-list>
+   * <optional-comma> -> <empty> | ,
+   * }</pre>
+   */
+  private Node parsePossibleMacroCall(String directive) throws IOException {
+    skipSpace();
+    if (c != '(') {
+      throw parseException("Unrecognized directive #" + directive);
+    }
+    next();
+    ImmutableList.Builder<Node> 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}.
+   * <pre>{@code
+   * <reference> -> $<reference-no-brace> |
+   *                ${<reference-no-brace>}
+   * }</pre>
+   *
+   * <p>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.
+   * <pre>{@code
+   * <reference-no-brace> -> <id><reference-suffix>
+   * }</pre>
+   */
+  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.
+   * <pre>{@code
+   * <reference-suffix> -> <empty> |
+   *                       <reference-member> |
+   *                       <reference-index>
+   * }</pre>
+   *
+   * @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)}.
+   * <pre>{@code
+   * <reference-member> -> .<id><reference-property-or-method><reference-suffix>
+   * <reference-property-or-method> -> <id> |
+   *                                   <id> ( <method-parameter-list> )
+   * }</pre>
+   *
+   * @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)}.
+   * <pre>{@code
+   * <method-parameter-list> -> <empty> |
+   *                            <non-empty-method-parameter-list>
+   * <non-empty-method-parameter-list> -> <expression> |
+   *                                      <expression> , <non-empty-method-parameter-list>
+   * }</pre>
+   *
+   * @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<ExpressionNode> 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]}.
+   * <pre>{@code
+   * <reference-index> -> [ <expression> ]
+   * }</pre>
+   *
+   * @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<Integer, List<Operator>> CODE_POINT_TO_OPERATORS;
+  static {
+    Map<Integer, List<Operator>> 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<Operator>());
+        }
+        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)}.
+   * <pre>{@code
+   * <expression> -> <and-expression> |
+   *                 <expression> || <and-expression>
+   * <and-expression> -> <relational-expression> |
+   *                     <and-expression> && <relational-expression>
+   * <equality-exression> -> <relational-expression> |
+   *                         <equality-expression> <equality-op> <relational-expression>
+   * <equality-op> -> == | !=
+   * <relational-expression> -> <additive-expression> |
+   *                            <relational-expression> <relation> <additive-expression>
+   * <relation> -> < | <= | > | >=
+   * <additive-expression> -> <multiplicative-expression> |
+   *                          <additive-expression> <add-op> <multiplicative-expression>
+   * <add-op> -> + | -
+   * <multiplicative-expression> -> <unary-expression> |
+   *                                <multiplicative-expression> <mult-op> <unary-expression>
+   * <mult-op> -> * | / | %
+   * }</pre>
+   */
+  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
+   * <a href="http://en.wikipedia.org/wiki/Operator-precedence_parser">algorithm</a> 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<Operator> 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).
+   * <pre>{@code
+   * <unary-expression> -> <primary> |
+   *                       ( <expression> ) |
+   *                       ! <unary-expression>
+   * }</pre>
+   */
+  private ExpressionNode parseUnaryExpression() throws IOException {
+    skipSpace();
+    ExpressionNode node;
+    if (c == '(') {
+      nextNonSpace();
+      node = parseExpression();
+      expect(')');
+      skipSpace();
+      return node;
+    } else if (c == '!') {
+      next();
+      node = new NotExpressionNode(parseUnaryExpression());
+      skipSpace();
+      return node;
+    } else {
+      return parsePrimary();
+    }
+  }
+
+
+  /**
+   * Parses an expression containing only literals or references.
+   * <pre>{@code
+   * <primary> -> <reference> |
+   *              <string-literal> |
+   *              <integer-literal> |
+   *              <boolean-literal>
+   * }</pre>
+   */
+  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}.
+   * <boolean-literal> -> 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
+   * <a href="http://velocity.apache.org/engine/devel/vtl-reference-guide.html#Variables">VTL
+   * </a>. 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<String, String> 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.
+
+## <a name="spaces"></a> 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<ExpressionNode> args;
+
+    MethodReferenceNode(ReferenceNode lhs, String id, List<ExpressionNode> args) {
+      super(lhs.resourceName, lhs.lineNumber);
+      this.lhs = lhs;
+      this.id = id;
+      this.args = args;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>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}.
+     *
+     * <p>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.
+     *
+     * <p>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<Object> argValues = new ArrayList<>();
+      for (ExpressionNode arg : args) {
+        argValues.add(arg.evaluate(context));
+      }
+      List<Method> methodsWithName = new ArrayList<>();
+      for (Method method : lhsValue.getClass().getMethods()) {
+        if (method.getName().equals(id) && !method.isSynthetic()) {
+          methodsWithName.add(method);
+        }
+      }
+      if (methodsWithName.isEmpty()) {
+        throw evaluationException("No method " + id + " in " + lhsValue.getClass().getName());
+      }
+      List<Method> compatibleMethods = new ArrayList<>();
+      for (Method method : methodsWithName) {
+        // TODO(emcmanus): support varargs, if it's useful
+        if (compatibleArgs(method.getParameterTypes(), argValues)) {
+          compatibleMethods.add(method);
+        }
+      }
+      switch (compatibleMethods.size()) {
+        case 0:
+          throw evaluationException(
+              "Parameters for method " + id + " have wrong types: " + argValues);
+        case 1:
+          return invokeMethod(compatibleMethods.get(0), lhsValue, argValues);
+        default:
+          StringBuilder error = new StringBuilder("Ambiguous method invocation, could be one of:");
+          for (Method method : compatibleMethods) {
+            error.append("\n  ").append(method);
+          }
+          throw evaluationException(error.toString());
+      }
+    }
+
+    /**
+     * Determines if the given argument list is compatible with the given parameter types. This
+     * includes an {@code Integer} argument being compatible with a parameter of type {@code int} or
+     * {@code long}, for example.
+     */
+    static boolean compatibleArgs(Class<?>[] paramTypes, List<Object> argValues) {
+      if (paramTypes.length != argValues.size()) {
+        return false;
+      }
+      for (int i = 0; i < paramTypes.length; i++) {
+        Class<?> paramType = paramTypes[i];
+        Object argValue = argValues.get(i);
+        if (paramType.isPrimitive()) {
+          return primitiveIsCompatible(paramType, argValue);
+        } else if (!paramType.isInstance(argValue)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    private static final Map<Class<?>, Class<?>> BOXED_TO_UNBOXED;
+    static {
+      Map<Class<?>, Class<?>> map = new HashMap<>();
+      map.put(Byte.class, byte.class);
+      map.put(Short.class, short.class);
+      map.put(Integer.class, int.class);
+      map.put(Long.class, long.class);
+      map.put(Float.class, float.class);
+      map.put(Double.class, double.class);
+      map.put(Character.class, char.class);
+      map.put(Boolean.class, boolean.class);
+      BOXED_TO_UNBOXED = Collections.unmodifiableMap(map);
+    }
+
+    private static boolean primitiveIsCompatible(Class<?> primitive, Object value) {
+      if (value == null) {
+        return false;
+      }
+      Class<?> unboxed = BOXED_TO_UNBOXED.get(value.getClass());
+      if (unboxed == null) {
+        return false;
+      }
+      return primitiveTypeIsAssignmentCompatible(primitive, unboxed);
+    }
+
+    private static final ImmutableList<Class<?>> NUMERICAL_PRIMITIVES = ImmutableList.<Class<?>>of(
+        byte.class, short.class, int.class, long.class, float.class, double.class);
+    private static final int INDEX_OF_INT = NUMERICAL_PRIMITIVES.indexOf(int.class);
+
+    /**
+     * Returns true if {@code from} can be assigned to {@code to} according to
+     * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.2">Widening
+     * Primitive Conversion</a>.
+     */
+    static boolean primitiveTypeIsAssignmentCompatible(Class<?> to, Class<?> from) {
+      // To restate the JLS rules, f can be assigned to t if:
+      // - they are the same; or
+      // - f is char and t is a numeric type at least as wide as int; or
+      // - f comes before t in the order byte, short, int, long, float, double.
+      if (to == from) {
+        return true;
+      }
+      int toI = NUMERICAL_PRIMITIVES.indexOf(to);
+      if (toI < 0) {
+        return false;
+      }
+      if (from == char.class) {
+        return toI >= INDEX_OF_INT;
+      }
+      int fromI = NUMERICAL_PRIMITIVES.indexOf(from);
+      if (fromI < 0) {
+        return false;
+      }
+      return toI >= fromI;
+    }
+  }
+
+  /**
+   * Invoke the given method on the given target with the given arguments. The method is expected
+   * to be public, but the class it is in might not be. In that case we will search up the
+   * hierarchy for an ancestor that is public and has the same method, and use that to invoke the
+   * method. Otherwise we would get an {@link IllegalAccessException}. More than one ancestor might
+   * define the method, but it doesn't matter which one we invoke since ultimately the code that
+   * will run will be the same.
+   */
+  Object invokeMethod(Method method, Object target, List<Object> argValues) {
+    if (!classIsPublic(target.getClass())) {
+      method = visibleMethod(method, target.getClass());
+      if (method == null) {
+        throw evaluationException(
+            "Method is not visible in class " + target.getClass().getName() + ": " + method);
+      }
+    }
+    try {
+      return method.invoke(target, argValues.toArray());
+    } catch (InvocationTargetException e) {
+      throw evaluationException(e.getCause());
+    } catch (Exception e) {
+      throw evaluationException(e);
+    }
+  }
+
+  private static String packageNameOf(Class<?> c) {
+    String name = c.getName();
+    int lastDot = name.lastIndexOf('.');
+    if (lastDot > 0) {
+      return name.substring(0, lastDot);
+    } else {
+      return "";
+    }
+  }
+
+  private static final String THIS_PACKAGE = packageNameOf(Node.class) + ".";
+
+  /**
+   * Returns a Method with the same name and parameter types as the given one, but that is in a
+   * public class or interface. This might be the given method, or it might be a method in a
+   * superclass or superinterface.
+   *
+   * @return a public method in a public class or interface, or null if none was found.
+   */
+  static Method visibleMethod(Method method, Class<?> in) {
+    if (in == null) {
+      return null;
+    }
+    Method methodInClass;
+    try {
+      methodInClass = in.getMethod(method.getName(), method.getParameterTypes());
+    } catch (NoSuchMethodException e) {
+      return null;
+    }
+    if (classIsPublic(in) || in.getName().startsWith(THIS_PACKAGE)) {
+      // The second disjunct is a hack to allow us to use the methods of $foreach without having
+      // to make the ForEachVar class public. We can invoke those methods from here since they
+      // are in the same package.
+      return methodInClass;
+    }
+    Method methodSuper = visibleMethod(method, in.getSuperclass());
+    if (methodSuper != null) {
+      return methodSuper;
+    }
+    for (Class<?> intf : in.getInterfaces()) {
+      Method methodIntf = visibleMethod(method, intf);
+      if (methodIntf != null) {
+        return methodIntf;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns whether the given class is public as seen from this class. Prior to Java 9, a class
+   * was either public or not public. But with the introduction of modules in Java 9, a class can
+   * be marked public and yet not be visible, if it is not exported from the module it appears in.
+   * So, on Java 9, we perform an additional check on class {@code c}, which is effectively
+   * {@code c.getModule().isExported(c.getPackageName())}. We use reflection so that the code can
+   * compile on earlier Java versions.
+   */
+  private static boolean classIsPublic(Class<?> c) {
+    if (!Modifier.isPublic(c.getModifiers())) {
+      return false;
+    }
+    if (CLASS_GET_MODULE_METHOD != null) {
+      return classIsExported(c);
+    }
+    return true;
+  }
+
+  private static boolean classIsExported(Class<?> c) {
+    try {
+      String pkg = packageNameOf(c);
+      Object module = CLASS_GET_MODULE_METHOD.invoke(c);
+      return (Boolean) MODULE_IS_EXPORTED_METHOD.invoke(module, pkg);
+    } catch (Exception e) {
+      return false;
+    }
+  }
+
+  private static final Method CLASS_GET_MODULE_METHOD;
+  private static final Method MODULE_IS_EXPORTED_METHOD;
+
+  static {
+    Method classGetModuleMethod;
+    Method moduleIsExportedMethod;
+    try {
+      classGetModuleMethod = Class.class.getMethod("getModule");
+      Class<?> moduleClass = classGetModuleMethod.getReturnType();
+      moduleIsExportedMethod = moduleClass.getMethod("isExported", String.class);
+    } catch (Exception e) {
+      classGetModuleMethod = null;
+      moduleIsExportedMethod = null;
+    }
+    CLASS_GET_MODULE_METHOD = classGetModuleMethod;
+    MODULE_IS_EXPORTED_METHOD = moduleIsExportedMethod;
+  }
+}
diff --git a/src/main/java/com/google/escapevelocity/Reparser.java b/src/main/java/com/google/escapevelocity/Reparser.java
new file mode 100644
index 0000000..6235bc4
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/Reparser.java
@@ -0,0 +1,288 @@
+/*
+ * 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 static com.google.escapevelocity.Node.emptyNode;
+
+import com.google.escapevelocity.DirectiveNode.ForEachNode;
+import com.google.escapevelocity.DirectiveNode.IfNode;
+import com.google.escapevelocity.DirectiveNode.MacroCallNode;
+import com.google.escapevelocity.DirectiveNode.SetNode;
+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.IfOrElseIfTokenNode;
+import com.google.escapevelocity.TokenNode.IfTokenNode;
+import com.google.escapevelocity.TokenNode.MacroDefinitionTokenNode;
+import com.google.escapevelocity.TokenNode.NestedTokenNode;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * The second phase of parsing. See {@link Parser#parse()} for a description of the phases and why
+ * we need them.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+class Reparser {
+  private static final ImmutableSet<Class<? extends TokenNode>> END_SET =
+      ImmutableSet.<Class<? extends TokenNode>>of(EndTokenNode.class);
+  private static final ImmutableSet<Class<? extends TokenNode>> EOF_SET =
+      ImmutableSet.<Class<? extends TokenNode>>of(EofNode.class);
+  private static final ImmutableSet<Class<? extends TokenNode>> ELSE_ELSE_IF_END_SET =
+      ImmutableSet.<Class<? extends TokenNode>>of(
+          ElseTokenNode.class, ElseIfTokenNode.class, EndTokenNode.class);
+
+  /**
+   * The nodes that make up the input sequence. Nodes are removed one by one from this list as
+   * parsing proceeds. At any time, {@link #currentNode} is the node being examined.
+   */
+  private final ImmutableList<Node> nodes;
+
+  /**
+   * The index of the node we are currently looking at while parsing.
+   */
+  private int nodeIndex;
+
+  /**
+   * Macros are removed from the input as they are found. They do not appear in the output parse
+   * tree. Macro definitions are not executed in place but are all applied before template rendering
+   * starts. This means that a macro can be referenced before it is defined.
+   */
+  private final Map<String, Macro> macros;
+
+  Reparser(ImmutableList<Node> nodes) {
+    this(nodes, new TreeMap<String, Macro>());
+  }
+
+  private Reparser(ImmutableList<Node> nodes, Map<String, Macro> macros) {
+    this.nodes = removeSpaceBeforeSet(nodes);
+    this.nodeIndex = 0;
+    this.macros = macros;
+  }
+
+  Template reparse() {
+    Node root = reparseNodes();
+    linkMacroCalls();
+    return new Template(root);
+  }
+
+  private Node reparseNodes() {
+    return parseTo(EOF_SET, new EofNode((String) null, 1));
+  }
+
+  /**
+   * Returns a copy of the given list where spaces have been moved where appropriate after {@code
+   * #set}. This hack is needed to match what appears to be special treatment in Apache Velocity of
+   * spaces before {@code #set} directives. If you have <i>thing</i> <i>whitespace</i> {@code #set},
+   * then the whitespace is deleted if the <i>thing</i> is a comment ({@code ##...\n}); a reference
+   * ({@code $x} or {@code $x.foo} etc); a macro definition; or another {@code #set}.
+   */
+  private static ImmutableList<Node> removeSpaceBeforeSet(ImmutableList<Node> nodes) {
+    assert nodes.get(nodes.size() - 1) instanceof EofNode : nodes.get(nodes.size() - 1);
+    // Since the last node is EofNode, the i + 1 and i + 2 accesses below are safe.
+    ImmutableList.Builder<Node> newNodes = ImmutableList.builder();
+    for (int i = 0; i < nodes.size(); i++) {
+      Node nodeI = nodes.get(i);
+      newNodes.add(nodeI);
+      if (shouldDeleteSpaceBetweenThisAndSet(nodeI)
+          && isWhitespaceLiteral(nodes.get(i + 1))
+          && nodes.get(i + 2) instanceof SetNode) {
+        // Skip the space.
+        i++;
+      }
+    }
+    return newNodes.build();
+  }
+
+  private static boolean shouldDeleteSpaceBetweenThisAndSet(Node node) {
+    return node instanceof CommentTokenNode
+        || node instanceof ReferenceNode
+        || node instanceof SetNode
+        || node instanceof MacroDefinitionTokenNode;
+  }
+
+  private static boolean isWhitespaceLiteral(Node node) {
+    if (node instanceof ConstantExpressionNode) {
+      Object constant = node.evaluate(null);
+      if (constant instanceof String) {
+        String s = (String) constant;
+        int i = 0;
+        while (i < s.length()) {
+          int c = s.codePointAt(i);
+          if (!Character.isWhitespace(c)) {
+            return false;
+          }
+          i += Character.charCount(c);
+        }
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Parse subtrees until one of the token types in {@code stopSet} is encountered.
+   * If this is the top level, {@code stopSet} will include {@link EofNode} so parsing will stop
+   * when it reaches the end of the input. Otherwise, if an {@code EofNode} is encountered it is an
+   * error because we have something like {@code #if} without {@code #end}.
+   *
+   * @param stopSet the kinds of tokens that will stop the parse. For example, if we are parsing
+   *     after an {@code #if}, we will stop at any of {@code #else}, {@code #elseif},
+   *     or {@code #end}.
+   * @param forWhat the token that triggered this call, for example the {@code #if} whose
+   *     {@code #end} etc we are looking for.
+   *
+   * @return a Node that is the concatenation of the parsed subtrees
+   */
+  private Node parseTo(Set<Class<? extends TokenNode>> stopSet, TokenNode forWhat) {
+    ImmutableList.Builder<Node> nodeList = ImmutableList.builder();
+    while (true) {
+      Node currentNode = currentNode();
+      if (stopSet.contains(currentNode.getClass())) {
+        break;
+      }
+      if (currentNode instanceof EofNode) {
+        throw new ParseException(
+            "Reached end of file while parsing " + forWhat.name(),
+            forWhat.resourceName,
+            forWhat.lineNumber);
+      }
+      Node parsed;
+      if (currentNode instanceof TokenNode) {
+        parsed = parseTokenNode();
+      } else {
+        parsed = currentNode;
+        nextNode();
+      }
+      nodeList.add(parsed);
+    }
+    return Node.cons(forWhat.resourceName, forWhat.lineNumber, nodeList.build());
+  }
+
+  private Node currentNode() {
+    return nodes.get(nodeIndex);
+  }
+
+  private Node nextNode() {
+    Node currentNode = currentNode();
+    if (currentNode instanceof EofNode) {
+      return currentNode;
+    } else {
+      nodeIndex++;
+      return currentNode();
+    }
+  }
+
+  private Node parseTokenNode() {
+    TokenNode tokenNode = (TokenNode) currentNode();
+    nextNode();
+    if (tokenNode instanceof CommentTokenNode) {
+      return emptyNode(tokenNode.resourceName, tokenNode.lineNumber);
+    } else if (tokenNode instanceof IfTokenNode) {
+      return parseIfOrElseIf((IfTokenNode) tokenNode);
+    } else if (tokenNode instanceof ForEachTokenNode) {
+      return parseForEach((ForEachTokenNode) tokenNode);
+    } else if (tokenNode instanceof NestedTokenNode) {
+      return parseNested((NestedTokenNode) tokenNode);
+    } else if (tokenNode instanceof MacroDefinitionTokenNode) {
+      return parseMacroDefinition((MacroDefinitionTokenNode) tokenNode);
+    } else {
+      throw new IllegalArgumentException(
+          "Unexpected token: " + tokenNode.name() + " on line " + tokenNode.lineNumber);
+    }
+  }
+
+  private Node parseForEach(ForEachTokenNode forEach) {
+    Node body = parseTo(END_SET, forEach);
+    nextNode();  // Skip #end
+    return new ForEachNode(
+        forEach.resourceName, forEach.lineNumber, forEach.var, forEach.collection, body);
+  }
+
+  private Node parseIfOrElseIf(IfOrElseIfTokenNode ifOrElseIf) {
+    Node truePart = parseTo(ELSE_ELSE_IF_END_SET, ifOrElseIf);
+    Node falsePart;
+    Node token = currentNode();
+    nextNode();  // Skip #else or #elseif (cond) or #end.
+    if (token instanceof EndTokenNode) {
+      falsePart = emptyNode(token.resourceName, token.lineNumber);
+    } else if (token instanceof ElseTokenNode) {
+      falsePart = parseTo(END_SET, ifOrElseIf);
+      nextNode();  // Skip #end
+    } else if (token instanceof ElseIfTokenNode) {
+      // We've seen #if (condition1) ... #elseif (condition2). currentToken is the first token
+      // after (condition2). We pretend that we've just seen #if (condition2) and parse out
+      // the remainder (which might have further #elseif and final #else). Then we pretend that
+      // we actually saw #if (condition1) ... #else #if (condition2) ...remainder ... #end #end.
+      falsePart = parseIfOrElseIf((ElseIfTokenNode) token);
+    } else {
+      throw new AssertionError(currentNode());
+    }
+    return new IfNode(
+        ifOrElseIf.resourceName, ifOrElseIf.lineNumber, ifOrElseIf.condition, truePart, falsePart);
+  }
+
+  // This is a #parse("foo.vm") directive. We've already done the first phase of parsing on the
+  // contents of foo.vm. Now we need to do the second phase, and insert the result into the
+  // reparsed nodes. We can call Reparser recursively, but we must ensure that any macros found
+  // are added to the containing Reparser's macro definitions.
+  private Node parseNested(NestedTokenNode nested) {
+    Reparser reparser = new Reparser(nested.nodes, this.macros);
+    return reparser.reparseNodes();
+  }
+
+  private Node parseMacroDefinition(MacroDefinitionTokenNode macroDefinition) {
+    Node body = parseTo(END_SET, macroDefinition);
+    nextNode();  // Skip #end
+    if (!macros.containsKey(macroDefinition.name)) {
+      Macro macro = new Macro(
+          macroDefinition.lineNumber, macroDefinition.name, macroDefinition.parameterNames, body);
+      macros.put(macroDefinition.name, macro);
+    }
+    return emptyNode(macroDefinition.resourceName, macroDefinition.lineNumber);
+  }
+
+  private void linkMacroCalls() {
+    for (Node node : nodes) {
+      if (node instanceof MacroCallNode) {
+        linkMacroCall((MacroCallNode) node);
+      }
+    }
+  }
+
+  private void linkMacroCall(MacroCallNode macroCall) {
+    Macro macro = macros.get(macroCall.name());
+    if (macro == null) {
+      throw new ParseException(
+          "#" + macroCall.name()
+              + " is neither a standard directive nor a macro that has been defined",
+          macroCall.resourceName,
+          macroCall.lineNumber);
+    }
+    if (macro.parameterCount() != macroCall.argumentCount()) {
+      throw new ParseException(
+          "Wrong number of arguments to #" + macroCall.name()
+              + ": expected " + macro.parameterCount()
+              + ", got " + macroCall.argumentCount(),
+          macroCall.resourceName,
+          macroCall.lineNumber);
+    }
+    macroCall.setMacro(macro);
+  }
+}
diff --git a/src/main/java/com/google/escapevelocity/Template.java b/src/main/java/com/google/escapevelocity/Template.java
new file mode 100644
index 0000000..646c42b
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/Template.java
@@ -0,0 +1,112 @@
+/*
+ * 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.EvaluationContext.PlainEvaluationContext;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Map;
+
+/**
+ * A template expressed in EscapeVelocity, a subset of the Velocity Template Language (VTL) from
+ * Apache. The intent of this implementation is that if a template is accepted and successfully
+ * produces output, that output will be identical to what Velocity would have produced for the same
+ * template and input variables.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+// TODO(emcmanus): spell out exactly what Velocity features are unsupported.
+public class Template {
+  private final Node root;
+
+  /**
+   * Used to resolve references to resources in the template, through {@code #parse} directives.
+   *
+   * <p>Here is an example that opens nested templates as resources relative to the calling class:
+   *
+   * <pre>
+   *   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));
+   *   };
+   * </pre>
+   */
+  @FunctionalInterface
+  public interface ResourceOpener {
+
+    /**
+     * Returns a Reader that will be used to read the given resource, then closed.
+     *
+     * @param resourceName the name of the resource to be read. This will never be null.
+     */
+    Reader openResource(String resourceName) throws IOException;
+  }
+
+  /**
+   * Parses a VTL template from the given {@code Reader}. The given Reader will be closed on
+   * return from this method.
+   */
+  public static Template parseFrom(final Reader reader) throws IOException {
+    ResourceOpener resourceOpener = new ResourceOpener() {
+      @Override
+      public Reader openResource(String resourceName) throws IOException {
+        if (resourceName == null) {
+          return reader;
+        } else {
+          throw new IOException("No ResourceOpener has been configured to read " + resourceName);
+        }
+      }
+    };
+    try {
+      return parseFrom((String) null, resourceOpener);
+    } finally {
+      reader.close();
+    }
+  }
+
+  /**
+   * Parse a VTL template of the given name using the given {@code ResourceOpener}.
+   *
+   * @param resourceName name of the resource. May be null.
+   * @param resourceOpener used to open included files for {@code #parse} directives in the
+   *     template.
+   */
+  public static Template parseFrom(
+      String resourceName, ResourceOpener resourceOpener) throws IOException {
+    try (Reader reader = resourceOpener.openResource(resourceName)) {
+      return new Parser(reader, resourceName, resourceOpener).parse();
+    }
+  }
+
+  Template(Node root) {
+    this.root = root;
+  }
+
+  /**
+   * Evaluate the given template with the given initial set of variables.
+   *
+   * @param vars a map where the keys are variable names and the values are the corresponding
+   *     variable values. For example, if {@code "x"} maps to 23, then {@code $x} in the template
+   *     will expand to 23.
+   *
+   * @return the string result of evaluating the template.
+   */
+  public String evaluate(Map<String, ?> vars) {
+    EvaluationContext evaluationContext = new PlainEvaluationContext(vars);
+    return String.valueOf(root.evaluate(evaluationContext));
+  }
+}
diff --git a/src/main/java/com/google/escapevelocity/TokenNode.java b/src/main/java/com/google/escapevelocity/TokenNode.java
new file mode 100644
index 0000000..1e92109
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/TokenNode.java
@@ -0,0 +1,167 @@
+/*
+ * 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.List;
+
+/**
+ * A parsing node that will be deleted during the construction of the parse tree, to be replaced
+ * by a higher-level construct such as {@link DirectiveNode.IfNode}. See {@link Parser#parse()}
+ * for a description of the way these tokens work.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+abstract class TokenNode extends Node {
+  TokenNode(String resourceName, int lineNumber) {
+    super(resourceName, lineNumber);
+  }
+
+  /**
+   * This method always throws an exception because a node like this should never be found in the
+   * final parse tree.
+   */
+  @Override Object evaluate(EvaluationContext vars) {
+    throw new UnsupportedOperationException(getClass().getName());
+  }
+
+  /**
+   * The name of the token, for use in parse error messages.
+   */
+  abstract String name();
+
+  /**
+   * A synthetic node that represents the end of the input. This node is the last one in the
+   * initial token string and also the last one in the parse tree.
+   */
+  static final class EofNode extends TokenNode {
+    EofNode(String resourceName, int lineNumber) {
+      super(resourceName, lineNumber);
+    }
+
+    @Override
+    String name() {
+      return "end of file";
+    }
+  }
+
+  static final class EndTokenNode extends TokenNode {
+    EndTokenNode(String resourceName, int lineNumber) {
+      super(resourceName, lineNumber);
+    }
+
+    @Override String name() {
+      return "#end";
+    }
+  }
+
+  /**
+   * A node in the parse tree representing a comment. Comments are introduced by {@code ##} and
+   * extend to the end of the line. The only reason for recording comment nodes is so that we can
+   * skip space between a comment and a following {@code #set}, to be compatible with Velocity
+   * behaviour.
+   */
+  static class CommentTokenNode extends TokenNode {
+    CommentTokenNode(String resourceName, int lineNumber) {
+      super(resourceName, lineNumber);
+    }
+
+    @Override String name() {
+      return "##";
+    }
+  }
+
+  abstract static class IfOrElseIfTokenNode extends TokenNode {
+    final ExpressionNode condition;
+
+    IfOrElseIfTokenNode(ExpressionNode condition) {
+      super(condition.resourceName, condition.lineNumber);
+      this.condition = condition;
+    }
+  }
+
+  static final class IfTokenNode extends IfOrElseIfTokenNode {
+    IfTokenNode(ExpressionNode condition) {
+      super(condition);
+    }
+
+    @Override String name() {
+      return "#if";
+    }
+  }
+
+  static final class ElseIfTokenNode extends IfOrElseIfTokenNode {
+    ElseIfTokenNode(ExpressionNode condition) {
+      super(condition);
+    }
+
+    @Override String name() {
+      return "#elseif";
+    }
+  }
+
+  static final class ElseTokenNode extends TokenNode {
+    ElseTokenNode(String resourceName, int lineNumber) {
+      super(resourceName, lineNumber);
+    }
+
+    @Override String name() {
+      return "#else";
+    }
+  }
+
+  static final class ForEachTokenNode extends TokenNode {
+    final String var;
+    final ExpressionNode collection;
+
+    ForEachTokenNode(String var, ExpressionNode collection) {
+      super(collection.resourceName, collection.lineNumber);
+      this.var = var;
+      this.collection = collection;
+    }
+
+    @Override String name() {
+      return "#foreach";
+    }
+  }
+
+  static final class NestedTokenNode extends TokenNode {
+    final ImmutableList<Node> nodes;
+
+    NestedTokenNode(String resourceName, ImmutableList<Node> nodes) {
+      super(resourceName, 1);
+      this.nodes = nodes;
+    }
+
+    @Override String name() {
+      return "#parse(\"" + resourceName + "\")";
+    }
+  }
+
+  static final class MacroDefinitionTokenNode extends TokenNode {
+    final String name;
+    final ImmutableList<String> parameterNames;
+
+    MacroDefinitionTokenNode(
+        String resourceName, int lineNumber, String name, List<String> parameterNames) {
+      super(resourceName, lineNumber);
+      this.name = name;
+      this.parameterNames = ImmutableList.copyOf(parameterNames);
+    }
+
+    @Override String name() {
+      return "#macro(" + name + ")";
+    }
+  }
+}
+
diff --git a/src/test/java/com/google/escapevelocity/ImmutableSetTest.java b/src/test/java/com/google/escapevelocity/ImmutableSetTest.java
new file mode 100644
index 0000000..b0283dd
--- /dev/null
+++ b/src/test/java/com/google/escapevelocity/ImmutableSetTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+@RunWith(JUnit4.class)
+public class ImmutableSetTest {
+  @Test
+  public void empty() {
+    ImmutableSet<String> empty = ImmutableSet.of();
+    assertThat(empty).isEmpty();
+    assertThat(empty).doesNotContain("");
+  }
+
+  @Test
+  public void duplicates() {
+    ImmutableSet<Integer> ints = ImmutableSet.of(1, 2, 3, 2, 1, 2, 3, 3);
+    assertThat(ints).hasSize(3);
+    assertThat(ints).containsExactly(1, 2, 3);
+
+    ImmutableSet<Integer> ints2 = ImmutableSet.of(1, 2, 3, 4, 5, 3);
+    assertThat(ints2).containsExactly(1, 2, 3, 4, 5);
+  }
+}
diff --git a/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java b/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java
new file mode 100644
index 0000000..660c237
--- /dev/null
+++ b/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.primitives.Primitives;
+import com.google.common.truth.Expect;
+import com.google.escapevelocity.ReferenceNode.MethodReferenceNode;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link ReferenceNode}.
+ *
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+@RunWith(JUnit4.class)
+public class ReferenceNodeTest {
+  @Rule public Expect expect = Expect.create();
+
+  // This is the exhaustive list from
+  // https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.2.
+  // We put the "from" type first for consistency with that list, even though that is inconsistent
+  // with our method order (which is itself consistent with assignment, "to" on the left).
+  private static final ImmutableSet<ImmutableList<Class<?>>> ASSIGNMENT_COMPATIBLE =
+      makeAssignmentCompatibleSet();
+  private static ImmutableSet<ImmutableList<Class<?>>> makeAssignmentCompatibleSet() {
+    Class<?>[][] pairs = {
+        {byte.class, short.class},
+        {byte.class, int.class},
+        {byte.class, long.class},
+        {byte.class, float.class},
+        {byte.class, double.class},
+        {short.class, int.class},
+        {short.class, long.class},
+        {short.class, float.class},
+        {short.class, double.class},
+        {char.class, int.class},
+        {char.class, long.class},
+        {char.class, float.class},
+        {char.class, double.class},
+        {int.class, long.class},
+        {int.class, float.class},
+        {int.class, double.class},
+        {long.class, float.class},
+        {long.class, double.class},
+        {float.class, double.class},
+    };
+    ImmutableSet.Builder<ImmutableList<Class<?>>> builder = ImmutableSet.builder();
+    for (Class<?>[] pair : pairs) {
+      builder.add(ImmutableList.copyOf(pair));
+    }
+    return builder.build();
+  }
+
+  @Test
+  public void testPrimitiveTypeIsAssignmentCompatible() {
+    for (Class<?> from : Primitives.allPrimitiveTypes()) {
+      for (Class<?> to : Primitives.allPrimitiveTypes()) {
+        boolean expected =
+            (from == to || ASSIGNMENT_COMPATIBLE.contains(ImmutableList.of(from, to)));
+        boolean actual =
+            MethodReferenceNode.primitiveTypeIsAssignmentCompatible(to, from);
+        expect
+            .withMessage(from + " assignable to " + to)
+            .that(expected).isEqualTo(actual);
+      }
+    }
+  }
+
+  @Test
+  public void testVisibleMethod() throws Exception {
+    Map<String, String> map = Collections.singletonMap("foo", "bar");
+    Class<?> mapClass = map.getClass();
+    assertThat(Modifier.isPublic(mapClass.getModifiers())).isFalse();
+    Method size = map.getClass().getMethod("size");
+    Method visibleSize = ReferenceNode.visibleMethod(size, mapClass);
+    assertThat(visibleSize.invoke(map)).isEqualTo(1);
+  }
+
+  @Test
+  public void testCompatibleArgs() {
+    assertThat(MethodReferenceNode.compatibleArgs(
+        new Class<?>[]{int.class}, ImmutableList.of((Object) 5))).isTrue();
+  }
+}
diff --git a/src/test/java/com/google/escapevelocity/TemplateTest.java b/src/test/java/com/google/escapevelocity/TemplateTest.java
new file mode 100644
index 0000000..bd769d6
--- /dev/null
+++ b/src/test/java/com/google/escapevelocity/TemplateTest.java
@@ -0,0 +1,653 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.truth.Expect;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeInstance;
+import org.apache.velocity.runtime.log.NullLogChute;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * @author emcmanus@google.com (Éamonn McManus)
+ */
+@RunWith(JUnit4.class)
+public class TemplateTest {
+  @Rule public TestName testName = new TestName();
+  @Rule public Expect expect = Expect.create();
+  @Rule public ExpectedException thrown = ExpectedException.none();
+
+  private RuntimeInstance velocityRuntimeInstance;
+
+  @Before
+  public void setUp() {
+    velocityRuntimeInstance = new RuntimeInstance();
+
+    // Ensure that $undefinedvar will produce an exception rather than outputting $undefinedvar.
+    velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true");
+    velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
+        new NullLogChute());
+
+    // Disable any logging that Velocity might otherwise see fit to do.
+    velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute());
+
+    velocityRuntimeInstance.init();
+  }
+
+  private void compare(String template) {
+    compare(template, ImmutableMap.<String, Object>of());
+  }
+
+  private void compare(String template, Map<String, ?> vars) {
+    compare(template, Suppliers.ofInstance(vars));
+  }
+
+  /**
+   * Checks that the given template and the given variables produce identical results with
+   * Velocity and EscapeVelocity. This uses a {@code Supplier} to define the variables to cover
+   * test cases that involve modifying the values of the variables. Otherwise the run using
+   * Velocity would change those values so that the run using EscapeVelocity would not be starting
+   * from the same point.
+   */
+  private void compare(String template, Supplier<? extends Map<String, ?>> varsSupplier) {
+    Map<String, ?> velocityVars = varsSupplier.get();
+    String velocityRendered = velocityRender(template, velocityVars);
+    Map<String, ?> escapeVelocityVars = varsSupplier.get();
+    String escapeVelocityRendered;
+    try {
+      escapeVelocityRendered =
+          Template.parseFrom(new StringReader(template)).evaluate(escapeVelocityVars);
+    } catch (IOException e) {
+      throw new AssertionError(e);
+    }
+    String failure = "from velocity: <" + velocityRendered + ">\n"
+        + "from escape velocity: <" + escapeVelocityRendered + ">\n";
+    expect.withMessage(failure).that(escapeVelocityRendered).isEqualTo(velocityRendered);
+  }
+
+  private String velocityRender(String template, Map<String, ?> vars) {
+    VelocityContext velocityContext = new VelocityContext(new TreeMap<>(vars));
+    StringWriter writer = new StringWriter();
+    SimpleNode parsedTemplate;
+    try {
+      parsedTemplate = velocityRuntimeInstance.parse(
+          new StringReader(template), testName.getMethodName());
+    } catch (org.apache.velocity.runtime.parser.ParseException e) {
+      throw new AssertionError(e);
+    }
+    boolean rendered = velocityRuntimeInstance.render(
+        velocityContext, writer, parsedTemplate.getTemplateName(), parsedTemplate);
+    assertThat(rendered).isTrue();
+    return writer.toString();
+  }
+
+  @Test
+  public void empty() {
+    compare("");
+  }
+
+  @Test
+  public void literalOnly() {
+    compare("In the reign of James the Second \n It was generally reckoned\n");
+  }
+
+  @Test
+  public void comment() {
+    compare("line 1 ##\n  line 2");
+  }
+
+  @Test
+  public void substituteNoBraces() {
+    compare(" $x ", ImmutableMap.of("x", 1729));
+    compare(" ! $x ! ", ImmutableMap.of("x", 1729));
+  }
+
+  @Test
+  public void substituteWithBraces() {
+    compare("a${x}\nb", ImmutableMap.of("x", "1729"));
+  }
+
+  @Test
+  public void substitutePropertyNoBraces() {
+    compare("=$t.name=", ImmutableMap.of("t", Thread.currentThread()));
+  }
+
+  @Test
+  public void substitutePropertyWithBraces() {
+    compare("=${t.name}=", ImmutableMap.of("t", Thread.currentThread()));
+  }
+
+  @Test
+  public void substituteNestedProperty() {
+    compare("\n$t.name.empty\n", ImmutableMap.of("t", Thread.currentThread()));
+  }
+
+  @Test
+  public void substituteMethodNoArgs() {
+    compare("<$c.size()>", ImmutableMap.of("c", ImmutableMap.of()));
+  }
+
+  @Test
+  public void substituteMethodOneArg() {
+    compare("<$list.get(0)>", ImmutableMap.of("list", ImmutableList.of("foo")));
+  }
+
+  @Test
+  public void substituteMethodTwoArgs() {
+    compare("\n$s.indexOf(\"bar\", 2)\n", ImmutableMap.of("s", "barbarbar"));
+  }
+
+  @Test
+  public void substituteMethodNoSynthetic() {
+    // If we aren't careful, we'll see both the inherited `Set<K> keySet()` from Map
+    // and the overridden `ImmutableSet<K> keySet()` in ImmutableMap.
+    compare("$map.keySet()", ImmutableMap.of("map", ImmutableMap.of("foo", "bar")));
+  }
+
+  @Test
+  public void substituteIndexNoBraces() {
+    compare("<$map[\"x\"]>", ImmutableMap.of("map", ImmutableMap.of("x", "y")));
+  }
+
+  @Test
+  public void substituteIndexWithBraces() {
+    compare("<${map[\"x\"]}>", ImmutableMap.of("map", ImmutableMap.of("x", "y")));
+  }
+
+  @Test
+  public void substituteIndexThenProperty() {
+    compare("<$map[2].name>", ImmutableMap.of("map", ImmutableMap.of(2, getClass())));
+  }
+
+  @Test
+  public void variableNameCantStartWithNonAscii() {
+    compare("<$Éamonn>", ImmutableMap.<String, Object>of());
+  }
+
+  @Test
+  public void variableNamesAreAscii() {
+    compare("<$Pádraig>", ImmutableMap.of("P", "(P)"));
+  }
+
+  @Test
+  public void variableNameCharacters() {
+    compare("<AZaz-foo_bar23>", ImmutableMap.of("AZaz-foo_bar23", "(P)"));
+  }
+
+  public static class Indexable {
+    public String get(String y) {
+      return "[" + y + "]";
+    }
+  }
+
+  @Test
+  public void substituteExoticIndex() {
+    // Any class with a get(X) method can be used with $x[i]
+    compare("<$x[\"foo\"]>", ImmutableMap.of("x", new Indexable()));
+  }
+
+  @Test
+  public void simpleSet() {
+    compare("$x#set ($x = 17)#set ($y = 23) ($x, $y)", ImmutableMap.of("x", 1));
+  }
+
+  @Test
+  public void newlineAfterSet() {
+    compare("foo #set ($x = 17)\nbar", ImmutableMap.<String, Object>of());
+  }
+
+  @Test
+  public void newlineInSet() {
+    compare("foo #set ($x\n  = 17)\nbar $x", ImmutableMap.<String, Object>of());
+  }
+
+  @Test
+  public void expressions() {
+    compare("#set ($x = 1 + 1) $x");
+    compare("#set ($x = 1 + 2 * 3) $x");
+    compare("#set ($x = (1 + 1 == 2)) $x");
+    compare("#set ($x = (1 + 1 != 2)) $x");
+    compare("#set ($x = 22 - 7) $x");
+    compare("#set ($x = 22 / 7) $x");
+    compare("#set ($x = 22 % 7) $x");
+  }
+
+  @Test
+  public void associativity() {
+    compare("#set ($x = 3 - 2 - 1) $x");
+    compare("#set ($x = 16 / 4 / 4) $x");
+  }
+
+  @Test
+  public void precedence() {
+    compare("#set ($x = 1 + 2 + 3 * 4 * 5 + 6) $x");
+    compare("#set($x=1+2+3*4*5+6)$x");
+    compare("#set ($x = 1 + 2 * 3 == 3 * 2 + 1) $x");
+  }
+
+  @Test
+  public void and() {
+    compare("#set ($x = false && false) $x");
+    compare("#set ($x = false && true) $x");
+    compare("#set ($x = true && false) $x");
+    compare("#set ($x = true && true) $x");
+  }
+
+  @Test
+  public void or() {
+    compare("#set ($x = false || false) $x");
+    compare("#set ($x = false || true) $x");
+    compare("#set ($x = true || false) $x");
+    compare("#set ($x = true || true) $x");
+  }
+
+  @Test
+  public void not() {
+    compare("#set ($x = !true) $x");
+    compare("#set ($x = !false) $x");
+  }
+
+  @Test
+  public void truthValues() {
+    compare("#set ($x = $true && true) $x", ImmutableMap.of("true", true));
+    compare("#set ($x = $false && true) $x", ImmutableMap.of("false", false));
+    compare("#set ($x = $emptyCollection && true) $x",
+        ImmutableMap.of("emptyCollection", ImmutableList.of()));
+    compare("#set ($x = $emptyString && true) $x", ImmutableMap.of("emptyString", ""));
+  }
+
+  @Test
+  public void numbers() {
+    compare("#set ($x = 0) $x");
+    compare("#set ($x = -1) $x");
+    compare("#set ($x = " + Integer.MAX_VALUE + ") $x");
+    compare("#set ($x = " + Integer.MIN_VALUE + ") $x");
+  }
+
+  private static final String[] RELATIONS = {"==", "!=", "<", ">", "<=", ">="};
+
+  @Test
+  public void intRelations() {
+    int[] numbers = {-1, 0, 1, 17};
+    for (String relation : RELATIONS) {
+      for (int a : numbers) {
+        for (int b : numbers) {
+          compare("#set ($x = $a " + relation + " $b) $x",
+              ImmutableMap.<String, Object>of("a", a, "b", b));
+        }
+      }
+    }
+  }
+
+  @Test
+  public void relationPrecedence() {
+    compare("#set ($x = 1 < 2 == 2 < 1) $x");
+    compare("#set ($x = 2 < 1 == 2 < 1) $x");
+  }
+
+  /**
+   * Tests the surprising definition of equality mentioned in
+   * {@link ExpressionNode.EqualsExpressionNode}.
+   */
+  @Test
+  public void funkyEquals() {
+    compare("#set ($t = (123 == \"123\")) $t");
+    compare("#set ($f = (123 == \"1234\")) $f");
+    compare("#set ($x = ($sb1 == $sb2)) $x", ImmutableMap.of(
+        "sb1", (Object) new StringBuilder("123"),
+        "sb2", (Object) new StringBuilder("123")));
+  }
+
+  @Test
+  public void ifTrueNoElse() {
+    compare("x#if (true)y #end z");
+    compare("x#if (true)y #end  z");
+    compare("x#if (true)y #end\nz");
+    compare("x#if (true)y #end\n z");
+    compare("x#if (true) y #end\nz");
+    compare("x#if (true)\ny #end\nz");
+    compare("x#if (true) y #end\nz");
+    compare("$x #if (true) y #end $x ", ImmutableMap.of("x", "!"));
+  }
+
+  @Test
+  public void ifFalseNoElse() {
+    compare("x#if (false)y #end z");
+    compare("x#if (false)y #end\nz");
+    compare("x#if (false)y #end\n z");
+    compare("x#if (false) y #end\nz");
+    compare("x#if (false)\ny #end\nz");
+    compare("x#if (false) y #end\nz");
+  }
+
+  @Test
+  public void ifTrueWithElse() {
+    compare("x#if (true) a #else b #end z");
+  }
+
+  @Test
+  public void ifFalseWithElse() {
+    compare("x#if (false) a #else b #end z");
+  }
+
+  @Test
+  public void ifTrueWithElseIf() {
+    compare("x#if (true) a #elseif (true) b #else c #end z");
+  }
+
+  @Test
+  public void ifFalseWithElseIfTrue() {
+    compare("x#if (false) a #elseif (true) b #else c #end z");
+  }
+
+  @Test
+  public void ifFalseWithElseIfFalse() {
+    compare("x#if (false) a #elseif (false) b #else c #end z");
+  }
+
+  @Test
+  public void ifBraces() {
+    compare("x#{if}(false)a#{elseif}(false)b #{else}c#{end}z");
+  }
+  @Test
+  public void ifUndefined() {
+    compare("#if ($undefined) really? #else indeed #end");
+  }
+
+  @Test
+  public void forEach() {
+    compare("x#foreach ($x in $c) <$x> #end y",
+        ImmutableMap.of("c", ImmutableList.of()));
+    compare("x#foreach ($x in $c) <$x> #end y",
+        ImmutableMap.of("c", ImmutableList.of("foo", "bar", "baz")));
+    compare("x#foreach ($x in $c) <$x> #end y",
+        ImmutableMap.of("c", new String[] {"foo", "bar", "baz"}));
+    compare("x#foreach ($x in $c) <$x> #end y",
+        ImmutableMap.of("c", ImmutableMap.of("foo", "bar", "baz", "buh")));
+  }
+
+  @Test
+  public void forEachHasNext() {
+    compare("x#foreach ($x in $c) <$x#if ($foreach.hasNext), #end> #end y",
+        ImmutableMap.of("c", ImmutableList.of()));
+    compare("x#foreach ($x in $c) <$x#if ($foreach.hasNext), #end> #end y",
+        ImmutableMap.of("c", ImmutableList.of("foo", "bar", "baz")));
+  }
+
+  @Test
+  public void nestedForEach() {
+    String template =
+        "$x #foreach ($x in $listOfLists)\n"
+        + "  #foreach ($y in $x)\n"
+        + "    ($y)#if ($foreach.hasNext), #end\n"
+        + "  #end#if ($foreach.hasNext); #end\n"
+        + "#end\n"
+        + "$x\n";
+    Object listOfLists = ImmutableList.of(
+        ImmutableList.of("foo", "bar", "baz"), ImmutableList.of("fred", "jim", "sheila"));
+    compare(template, ImmutableMap.of("x", 23, "listOfLists", listOfLists));
+  }
+
+  @Test
+  public void forEachScope() {
+    String template =
+        "$x #foreach ($x in $list)\n"
+        + "[$x]\n"
+        + "#set ($x = \"bar\")\n"
+        + "#set ($othervar = \"baz\")\n"
+        + "#end\n"
+        + "$x $othervar";
+    compare(
+        template, ImmutableMap.of("x", "foo", "list", ImmutableList.of("blim", "blam", "blum")));
+  }
+
+  @Test
+  public void setSpacing() {
+    // The spacing in the output from #set is eccentric.
+    compare("x#set ($x = 0)x");
+    compare("x #set ($x = 0)x");
+    compare("x #set ($x = 0) x");
+    compare("$x#set ($x = 0)x", ImmutableMap.of("x", "!"));
+
+    // Velocity WTF: the #set eats the space after $x and other references, so the output is <!x>.
+    compare("$x  #set ($x = 0)x", ImmutableMap.of("x", "!"));
+    compare("$x.length()  #set ($x = 0)x", ImmutableMap.of("x", "!"));
+    compare("$x.empty  #set ($x = 0)x", ImmutableMap.of("x", "!"));
+    compare("$x[0]  #set ($x = 0)x", ImmutableMap.of("x", ImmutableList.of("!")));
+
+    compare("x#set ($x = 0)\n  $x!");
+
+    compare("x  #set($x = 0)  #set($x = 0)  #set($x = 0)  y");
+
+    compare("x ## comment\n  #set($x = 0)  y");
+  }
+
+  @Test
+  public void simpleMacro() {
+    String template =
+        "xyz\n"
+        + "#macro (m)\n"
+        + "hello world\n"
+        + "#end\n"
+        + "#m() abc #m()\n";
+    compare(template);
+  }
+
+  @Test
+  public void macroWithArgs() {
+    String template =
+        "$x\n"
+        + "#macro (m $x $y)\n"
+        + "  #if ($x < $y) less #else greater #end\n"
+        + "#end\n"
+        + "#m(17 23) #m(23 17) #m(17 17)\n"
+        + "$x";
+    compare(template, ImmutableMap.of("x", "tiddly"));
+  }
+
+  /**
+   * Tests defining a macro inside a conditional. This proves that macros are not evaluated in the
+   * main control flow, but rather are extracted at parse time. It also tests what happens if there
+   * is more than one definition of the same macro. (It is not apparent from the test, but it is the
+   * first definition that is retained.)
+   */
+  @Test
+  public void conditionalMacroDefinition() {
+    String templateFalse =
+        "#if (false)\n"
+        + "  #macro (m) foo #end\n"
+        + "#else\n"
+        + "  #macro (m) bar #end\n"
+        + "#end\n"
+        + "#m()\n";
+    compare(templateFalse);
+
+    String templateTrue =
+        "#if (true)\n"
+        + "  #macro (m) foo #end\n"
+        + "#else\n"
+        + "  #macro (m) bar #end\n"
+        + "#end\n"
+        + "#m()\n";
+    compare(templateTrue);
+  }
+
+  /**
+   * Tests referencing a macro before it is defined. Since macros are extracted at parse time but
+   * references are only used at evaluation time, this works.
+   */
+  @Test
+  public void forwardMacroReference() {
+    String template =
+        "#m(17)\n"
+        + "#macro (m $x)\n"
+        + "  !$x!\n"
+        + "#end";
+    compare(template);
+  }
+
+  @Test
+  public void macroArgsSeparatedBySpaces() {
+    String template =
+        "#macro (sum $x $y $z)\n"
+        + "  #set ($sum = $x + $y + $z)\n"
+        + "  $sum\n"
+        + "#end\n"
+        + "#sum ($list[0] $list.get(1) 5)\n";
+    compare(template, ImmutableMap.of("list", ImmutableList.of(3, 4)));
+  }
+
+  @Test
+  public void macroArgsSeparatedByCommas() {
+    String template =
+        "#macro (sum $x $y $z)\n"
+        + "  #set ($sum = $x + $y + $z)\n"
+        + "  $sum\n"
+        + "#end\n"
+        + "#sum ($list[0],$list.get(1),5)\n";
+    compare(template, ImmutableMap.of("list", ImmutableList.of(3, 4)));
+  }
+
+  // The following tests are based on http://wiki.apache.org/velocity/MacroEvaluationStrategy.
+  // They verify some of the trickier details of Velocity's call-by-name semantics.
+
+  @Test
+  public void callBySharing() {
+    // The example on the web page is wrong because $map.put('x', 'a') evaluates to null, which
+    // Velocity rejects as a render error. We fix this by ensuring that the returned previous value
+    // is not null.
+    // Here, the value of $y should not be affected by #set($x = "a"), even though the name passed
+    // to $x is $y.
+    String template =
+        "#macro(callBySharing $x $map)\n"
+        + "  #set($x = \"a\")\n"
+        + "  $map.put(\"x\", \"a\")\n"
+        + "#end\n"
+        + "#callBySharing($y $map)\n"
+        + "y is $y\n"
+        + "map[x] is $map[\"x\"]\n";
+    Supplier<Map<String, Object>> makeMap = new Supplier<Map<String, Object>>() {
+      @Override public Map<String, Object> get() {
+        return ImmutableMap.<String, Object>of(
+            "y", "y",
+            "map", new HashMap<String, Object>(ImmutableMap.of("x", (Object) "foo")));
+      }
+    };
+    compare(template, makeMap);
+  }
+
+  @Test
+  public void callByMacro() {
+    // Since #callByMacro1 never references its argument, $x.add("t") is never evaluated during it.
+    // Since #callByMacro2 references its argument twice, $x.add("t") is evaluated twice during it.
+    String template =
+        "#macro(callByMacro1 $p)\n"
+        + "  not using\n"
+        + "#end\n"
+        + "#macro(callByMacro2 $p)\n"
+        + "  using: $p\n"
+        + "  using again: $p\n"
+        + "  using again: $p\n"
+        + "#end\n"
+        + "#callByMacro1($x.add(\"t\"))\n"
+        + "x = $x\n"
+        + "#callByMacro2($x.add(\"t\"))\n"
+        + "x = $x\n";
+    Supplier<Map<String, Object>> makeMap = new Supplier<Map<String, Object>>() {
+      @Override public Map<String, Object> get() {
+        return ImmutableMap.<String, Object>of("x", new ArrayList<Object>());
+      }
+    };
+    compare(template, makeMap);
+  }
+
+  @Test
+  public void callByValue() {
+    // The assignments to the macro parameters $a and $b cause those parameters to be shadowed,
+    // so the output is: a b becomes b a.
+    String template =
+        "#macro(callByValueSwap $a $b)\n"
+        + "  $a $b becomes\n"
+        + "  #set($tmp = $a)\n"
+        + "  #set($a = $b)\n"
+        + "  #set($b = $tmp)\n"
+        + "  $a $b\n"
+        + "#end"
+        + "#callByValueSwap(\"a\", \"b\")";
+    compare(template);
+  }
+
+  // First "Call by macro expansion example" doesn't apply as long as we don't have map literals.
+
+  @Test
+  public void nameCaptureSwap() {
+    // Here, the arguments $a and $b are variables rather than literals, which means that their
+    // values change when we set those variables. #set($tmp = $a) changes the meaning of $b since
+    // $b is the name $tmp. So #set($a = $b) shadows parameter $a with the value of $tmp, which we
+    // have just set to "a". Then #set($b = $tmp) shadows parameter $b also with the value of $tmp.
+    // The end result is: a b becomes a a.
+    String template =
+        "#macro(nameCaptureSwap $a $b)\n"
+        + "  $a $b becomes\n"
+        + "  #set($tmp = $a)\n"
+        + "  #set($a = $b)\n"
+        + "  #set($b = $tmp)\n"
+        + "  $a $b\n"
+        + "#end\n"
+        + "#set($x = \"a\")\n"
+        + "#set($tmp = \"b\")\n"
+        + "#nameCaptureSwap($x $tmp)";
+    compare(template);
+  }
+
+  @Test
+  public void undefinedMacro() throws IOException {
+    String template = "#oops()";
+    thrown.expect(ParseException.class);
+    thrown.expectMessage("#oops is neither a standard directive nor a macro that has been defined");
+    Template.parseFrom(new StringReader(template));
+  }
+
+  @Test
+  public void macroArgumentMismatch() throws IOException {
+    String template =
+        "#macro (twoArgs $a $b) $a $b #end\n"
+        + "#twoArgs(23)\n";
+    thrown.expect(ParseException.class);
+    thrown.expectMessage("Wrong number of arguments to #twoArgs: expected 2, got 1");
+    Template.parseFrom(new StringReader(template));
+  }
+
+}