aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: e716ecff81e8573a34607db16b15f35c00b41453 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# 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.


## 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