Mustache template processor for Kotlin
Why is this included among a set of Kotlin JSON libraries? Mustache is often used in conjunction with JSON – even if only for debugging – and it seemed like a good fit.
This library also operates on structures created by the kjson-core
library,
and that library is therefore a dependency of this one.
The mustache-k
library is derived from the earlier kotlin-mustache
library; most of the template functionality is the same but the handling of partial resolution is much improved.
Templates are loaded into memory using the Parser
class, and there are several option for constructing a Parser
,
depending on how the templates are stored.
Templates may invoke other templates (the term used by Mustache is “Partial”), and the Parser needs to have
a means of resolving the Partial by name.
(For simplicity, the term template is used here to refer to a template or a partial – in effect they are the
same.)
The Parser
class has four alternative public constructors.
If the templates are held in a directory in the main file system, the Parser may be constructed with a File
describing
the directory, as follows:
val parser = Parser(File("/path/to/directory"))
Partial references will cause the parser to look for a file in the nominated directory.
The java.nio.file.Path
class may be used to specify the directory containing the templates:
val parser = Parser(FileSystems.getDefault().getPath("/path/to/directory"))
The Parser
may also take a URL as a directory specifier.
The most familiar form of a URL is the web reference, for example https://github.com/pwall567/mustache-k
, and the
parser will take a URL of this form and use it to create a specific locator for the template.
val parser = Parser(URL("https://raw.githubusercontent.com/pwall567/mustache-k/main/src/test/resources/templates/"))
(Note the ‘/’ on the end of the URL – this will cause this location to be regarded as a directory.)
There is also the file:
form of URL, and the parser will take this type of URL instead of a file or path reference as
described above.
val parser = Parser(URL("file:/path/to/directory/"))
But perhaps the most useful form of URL is the type returned by the Class.getResource()
function – this will
look up the specified directory on the classpath, and return either a file:
URL for a directory in the main file
system, or a jar:
URL for the directory within the JAR file where the templates have been combined with the classes.
This means that a combined JAR can be created containing the program classes and the templates, and the same code will
read the templates regardless of whether they are in the main file system or in a JAR.
To use this type of URL:
val parser = Parser(Resource.classPathURL("/templates") ?: throw RuntimeException("templates not found"))
The throw
is required because the getResource()
function may return null.
Finally, if none of the above lookup mechanisms are suitable, the Parser
may be constructed with a lambda to resolve a
partial name to a Reader
which is used to read the partial:
val parser = Parser { name ->
when (name) {
"alpha" -> File("alpha.template").reader()
"beta" -> File("beta.template").reader()
else -> throw IllegalArgumentException("Unrecognised name - $name")
}
}
Having constructed a Parser
, there are several functions to parse a template, taking a String
, a Reader
, an
InputStream
or a File
.
val template = parser.parse(string)
val template = parser.parse(reader)
val template = parser.parse(inputStream)
val template = parser.parse(file)
And to parse a template using the same naming system as for partials, using the directory specified in the constructor:
val template = parser.parseByName(name)
When searching for partial definitions, regardless of how the directory is specified, the parser will add the extension
mustache
.
To change this to use a different extension, use:
parser.extension = "hbs"
The functions which read from an InputStream
take an optional Charset
parameter.
The parser will use a default of Charsets.UTF_8
if the parameter is not supplied; to change this default:
parser.charset = Charsets.ISO_8859_1
To render a template to a String
:
val resultString = template.render(contextObject)
The context object is the object that will be used to resolve variables and the controlling values of sections.
To render to an Appendable
(this is often more efficient than rendering to a string since it avoids the creation of an
intermediate output object):
template.renderTo(appendable, contextObject)
And from version 1.4, you can render to a non-blocking data stream:
template.coRender(contextObject) { /* output to non-blocking channel */ }
The Template
companion object also has functions to simplify template parsing, avoiding the need to create a Parser
.
To parse a template from a String
:
val template = Template.parse("This is a template.\n")
From a Reader
:
val template = Template.parse(reader)
From an InputStream
:
val template = Template.parse(inputStream)
Or from a File
:
val template = Template.parse(File("/path/to/template"))
In this last case, a Parser
will be created with the parent directory of the file as its location for partials.
The reference Mustache Specification contains the definitive description of the Mustache template system. This implementation does not include all features covered by the specification; the differences are detailed below.
A Mustache template is a block of text to be copied to the output, interspersed with “tags” that control the substitution of values from a supplied object – the “context object”. All text that is not part of a tag is copied without modification to the generated output. The tags are described below.
To distinguish tags from general text, the original authors chose the open brace (mustache) character.
Tags are opened by two left brace characters ({{
) and closed by two right brace characters (}}
).
(These delimiters may be changed if the use of these characters would cause a conflict – see
below.)
The simplest tag type is the variable, which is replaced in the output by the result of resolving the variable name in
the context object.
If the context object is a Map
, the variable name is used as a key to locate an entry; otherwise the variable name is
used to locate a property in the Kotlin object.
See Name Resolution below for more detail.
For example, given the template:
Then either:
val contextObject = mapOf("who" to "World")
template.processToString(contextObject)
Or:
data class ContextObject(val who: String)
val contextObject = ContextObject("World")
template.processToString(contextObject)
Will cause the generated output to be:
Hello World
If the name is not located – the key is not found in the map or the class does not have a variable with the
specified name – no text will be output.
The name .
will select the entire context object.
By default, Mustache performs HTML escaping on the substituted text – that is, HTML special characters are
converted to the HTML entity references for those characters.
For example, <
is converted to <
and &
is converted to &
.
If this HTML escaping is not required, the literal form of the variable tag may be used – either {{{name}}}
(three braces instead of two) or {{&name}}
(ampersand before the variable name).
The name may be a structured name – see Name Resolution below.
A section is a block of text to be processed zero or more times depending on the value nominated by the section name.
Sections are introduced by a opening tag which has a #
following the two left braces, and closed by a tag with a /
following the left braces, for example {{#person}}...{{/person}}
.
Sections may be nested, and closing tags must match the most recent unmatched opening tag.
The content of the section is processed conditionally, depending on the type of the value (the value is determined using the rules detailed below):
Iterable
(e.g.List
),Array
: the content of the section is processed for each entry in the collection, with the individual entry used as the context object for the nested contentMap
: the content of the section is processed for each entry in the map, with theMap.Entry
used as the context object for the nested contentEnum
: the content of the section is processed with the enum as the context object, allowing the individual enum values to be used to control nested sections (see Enums below)Enum
values: the content of the section is processed if the context object is an enum with the specified value (see Enums below)String
(and other forms ofCharSequence
): the content of the section is processed if the string is not emptyInt
,Long
,Short
,Byte
,Double
,Float
,BigInteger
andBigDecimal
: the content of the section is processed if the value is non-zeroBoolean
: the content of the section is processed if the value istrue
- anything else: the content of the section is processed if the value is not
null
, with the value used as the context object for the nested content (this is how properties of nested objects may be accessed)
When processing an Iterable
, additional names are available in the context for each item processed:
first
(Boolean
) –true
if the item is the first in the collectionlast
(Boolean
) –true
if the item is the last in the collectionindex
(Int
) – the index (zero-based) of the item in the collectionindex1
(Int
) – the index (one-based) of the item in the collection
When rendering to a non-blocking data stream, the section tag can iterate over a Channel
or a Flow
.
In this case, the rendering function will suspend until the next element becomes available, and will terminate only when
the Channel
or Flow
is closed.
The additional names in the context are available as for Iterable
, but the last
property will never be set to true
(since that would require lookahead to see if another element was going to be available).
An inverted section is a block of text to be processed once, if the value is null or the following:
Iterable
(List
etc.),Array
,Map
: if the collection or map is emptyEnum
values: if the enum does not have the specified valueString
etc.: if the string is empty- number types: if the number is zero
Boolean
: if the value isfalse
Inverted sections are introduced by a opening tag which has a ^
following the two left braces, and closed by a tag
with a /
following the left braces.
Sections and inverted sections may nest within each other.
“Partial” is the term given to a nested section – for example a template in a separate file. Partials are introduced by a tag with '>' following the two left braces – the remainder of the tag is the name of an external file.
In the most common usage, the directory and extension of the first template will be stored and used as the directory and extension for any partials encountered in that template.
Recursive data structures may be processed by recursive templates.
For cases where the standard double-brace delimiters clash with the text being processed, the delimiters may be changed to some other combination of characters for the remainder of the current template. A “Set Delimiter” tag consists of the current opening delimiter, immediately followed by an equals sign '=', then the new opening delimiter, one or more spaces, the new closing delimiter, an equals sign '=' again, and finally the current closing delimiter.
For example, to change the delimiters to <%
and %>
:
{{=<% %>=}}
The delimiters may not contain spaces or the equals sign.
The variable and section (including inverted section) tags use the following rules to determine the value to be used for the tag:
- If the context object is
null
, the result value isnull
- If the context object is a
Map
containing a key with the tag name, the value associated with that key is used - If the context object has a property with the tag name, that property is used
- If the current context is a nested context (part of a section or inverted section), the context object of the outer context is searched using these same rules (repeatedly up to the outermost context)
- If nothing is found,
null
is returned
The variable or section may be specified in a structured form, e.g. person.firstName
.
In this case, the above rules are used for the first part of the name (the part before the first dot), but for the
subsequent parts only the result of the first part is searched and the enclosing contexts do not form part of the
resolution process.
This implementation treats all whitespace as significant, and copies it to the output. This includes the newline at the end of a file, so if, for example, you have a partial to substitute a word or phrase into the middle of a line, then to preserve line formatting the partial must not have a newline at the end of the file.
One very powerful way in which this library differs from other Mustache implementations is in its handling of enums.
If an enum value is used in a Section, the contents of the section are processed with the context object
for the nested content set to a special object that yields true
for the name corresponding to the value of the enum,
and false
for all other values.
For example:
enum class Type { CREDIT, DEBIT }
data class Transaction(val type: Type, val amount: Int)
val template = Template.parse("{{#type}}{{#CREDIT}}+{{/CREDIT}}{{#DEBIT}}-{{/DEBIT}}{{/type}}{{&amount}}")
println(template.render(Transaction(Type.DEBIT, 100))) // will print -100
The latest version of the library is 3.3, and it may be obtained from the Maven Central repository.
<dependency>
<groupId>io.kjson</groupId>
<artifactId>mustache-k</artifactId>
<version>3.3</version>
</dependency>
implementation 'io.kjson:mustache-k:3.3'
implementation("io.kjson:mustache-k:3.3")
Peter Wall
2024-11-13