A Java implementation of the Liquid templating engine backed up by an ANTLR grammar.
Add the repository:
repositories {
maven {
url "https://jitpack.io"
}
}
and then add the dependency:
dependencies {
compile 'com.github.bkiers:Liqp:0.6.4'
}
Add the following to your POM's <repositories>
tag:
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
then add the following dependency:
<dependency>
<groupId>com.github.bkiers</groupId>
<artifactId>Liqp</artifactId>
<version>0.6.4</version>
</dependency>
Or clone this repository and run: mvn install
which will create a JAR of Liqp
in your local Maven repository, as well as in the project's target/
folder.
This library can be used in two different ways:
- to construct an AST (abstract syntax tree) of some Liquid input
- to render Liquid input source (either files, or input strings)
To create an AST from input source, do the following:
String input =
"<ul id=\"products\"> \n" +
" {% for product in products %} \n" +
" <li> \n" +
" <h2>{{ product.name }}</h2> \n" +
" Only {{ product.price | price }} \n" +
" \n" +
" {{ product.description | prettyprint | paragraph }} \n" +
" </li> \n" +
" {% endfor %} \n" +
"</ul> \n";
Template template = Template.parse(input);
CommonTree root = template.getAST();
As you can see, the getAST()
method returns an instance of a
CommonTree
denoting the root
node of the input source. To see how the AST is built, you can use Template#toStringAST()
to print
an ASCII representation of the tree:
System.out.println(template.toStringAST());
/*
'- BLOCK
|- PLAIN='<ul id="products">'
|- FOR_ARRAY
| |- Id='product'
| |- LOOKUP
| | '- Id='products'
| |- BLOCK
| | |- PLAIN='<li> <h2>'
| | |- OUTPUT
| | | |- LOOKUP
| | | | |- Id='product'
| | | | '- Id='name'
| | | '- FILTERS
| | |- PLAIN='</h2> Only'
| | |- OUTPUT
| | | |- LOOKUP
| | | | |- Id='product'
| | | | '- Id='price'
| | | '- FILTERS
| | | '- FILTER
| | | |- Id='price'
| | | '- PARAMS
| | |- PLAIN=''
| | |- OUTPUT
| | | |- LOOKUP
| | | | |- Id='product'
| | | | '- Id='description'
| | | '- FILTERS
| | | |- FILTER
| | | | |- Id='prettyprint'
| | | | '- PARAMS
| | | '- FILTER
| | | |- Id='paragraph'
| | | '- PARAMS
| | '- PLAIN='</li>'
| '- ATTRIBUTES
'- PLAIN='</ul>'
*/
Checkout the ANTLR grammar to see what the AST looks like for each of the parser rules.
If you're not familiar with Liquid, have a look at their website: http://liquidmarkup.org.
In Ruby, you'd render a template like this:
@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
@template.render( 'name' => 'tobi' ) # Renders the output => "hi tobi"
With Liqp, the equivalent looks like this:
Template template = Template.parse("hi {{name}}");
String rendered = template.render("name", "tobi");
System.out.println(rendered);
/*
hi tobi
*/
The context provided as a parameter to render(...)
can be:
- a varargs where
the 0th, 2nd, 4th, ... indexes must be
String
literals denoting the keys. The values can be anyObject
. - a
Map<String, Object>
- or a JSON string
The following examples are equivalent to the previous Liqp example:
Template template = Template.parse("hi {{name}}");
Map<String, Object> map = new HashMap<String, Object>();
map.put("name", "tobi");
String rendered = template.render(map);
System.out.println(rendered);
/*
hi tobi
*/
Template template = Template.parse("hi {{name}}");
String rendered = template.render("{\"name\" : \"tobi\"}");
System.out.println(rendered);
/*
hi tobi
*/
Let's say you want to create a custom filters, called b
, that changes a string like
*text*
to <strong>text</strong>
.
You can do that as follows:
// first register your custom filter
Filter.registerFilter(new Filter("b"){
@Override
public Object apply(Object value, Object... params) {
// create a string from the value
String text = super.asString(value);
// replace and return *...* with <strong>...</strong>
return text.replaceAll("\\*(\\w(.*?\\w)?)\\*", "<strong>$1</strong>");
}
});
// use your filter
Template template = Template.parse("{{ wiki | b }}");
String rendered = template.render("{\"wiki\" : \"Some *bold* text *in here*.\"}");
System.out.println(rendered);
/*
Some <strong>bold</strong> text <strong>in here</strong>.
*/
And to use an optional parameter in your filter, do something like this:
// first register your custom filter
Filter.registerFilter(new Filter("repeat"){
@Override
public Object apply(Object value, Object... params) {
// get the text of the value
String text = super.asString(value);
// check if an optional parameter is provided
int times = params.length == 0 ? 1 : super.asNumber(params[0]).intValue();
StringBuilder builder = new StringBuilder();
while(times-- > 0) {
builder.append(text);
}
return builder.toString();
}
});
// use your filter
Template template = Template.parse("{{ 'a' | repeat }}\n{{ 'b' | repeat:5 }}");
String rendered = template.render();
System.out.println(rendered);
/*
a
bbbbb
*/
You can use an array (or list) as well, and can also return a numerical value:
Filter.registerFilter(new Filter("sum"){
@Override
public Object apply(Object value, Object... params) {
Object[] numbers = super.asArray(value);
double sum = 0;
for(Object obj : numbers) {
sum += super.asNumber(obj).doubleValue();
}
return sum;
}
});
Template template = Template.parse("{{ numbers | sum }}");
String rendered = template.render("{\"numbers\" : [1, 2, 3, 4, 5]}");
System.out.println(rendered);
/*
15.0
*/
Let's say you would like to create a tag that makes it easy to loop for a fixed amount of times, executing a block of Liquid code.
Here's a way to create, and use, such a custom loop
tag:
Tag.registerTag(new Tag("loop"){
@Override
public Object render(Map<String, Object> context, LNode... nodes) {
int n = super.asNumber(nodes[0].render(context)).intValue();
LNode block = nodes[1];
StringBuilder builder = new StringBuilder();
while(n-- > 0) {
builder.append(super.asString(block.render(context)));
}
return builder.toString();
}
});
Template template = Template.parse("{% loop 5 %}looping!\n{% endloop %}");
String rendered = template.render();
System.out.println(rendered);
/*
looping!
looping!
looping!
looping!
looping!
*/
Note that both Tag.registerTag(Tag)
and Filer.registerFilter(Filter)
will add
tags and filters per JVM instance. If you want templates to use specific filters,
create your Template
instance as follows:
Template.parse(source)
.with(filter);
Template.parse(source)
.with(tag);
// Or combine them:
Template.parse(source)
.with(filter)
.with(tag);
For example, using the sum
filter for just 1 template, would look like this:
Template template = Template.parse("{{ numbers | sum }}").with(new Filter("sum"){
@Override
public Object apply(Object value, Object... params) {
Object[] numbers = super.asArray(value);
double sum = 0;
for(Object obj : numbers) {
sum += super.asNumber(obj).doubleValue();
}
return sum;
}
});
String rendered = template.render("{\"numbers\" : [1, 2, 3, 4, 5]}");
System.out.println(rendered);
/*
15.0
*/