Skip to content

pwall567/mustache-k

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mustache-k

Build Status License: MIT Kotlin Maven Central

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.

Usage

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.

Directory Specified by File

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.

Directory Specified by Path

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"))

Directory Specified by URL

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.

Directory Specified by Custom Lambda

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")
        }
    }

Parser Functions

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

Template functions

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.

Reference

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.

Tags

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.)

Variables

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:

Hello {{who}}

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 &lt; and & is converted to &amp;.

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.

Sections

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 content
  • Map: the content of the section is processed for each entry in the map, with the Map.Entry used as the context object for the nested content
  • Enum: 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 of CharSequence): the content of the section is processed if the string is not empty
  • Int, Long, Short, Byte, Double, Float, BigInteger and BigDecimal: the content of the section is processed if the value is non-zero
  • Boolean: the content of the section is processed if the value is true
  • 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 collection
  • last (Boolean) – true if the item is the last in the collection
  • index (Int) – the index (zero-based) of the item in the collection
  • index1 (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).

Inverted Sections

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 empty
  • Enum values: if the enum does not have the specified value
  • String etc.: if the string is empty
  • number types: if the number is zero
  • Boolean: if the value is false

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.

Partials

“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.

Set Delimiter

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.

Name Resolution

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 is null
  • 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.

Whitespace

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.

Enums

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

Dependency Specification

The latest version of the library is 3.3, and it may be obtained from the Maven Central repository.

Maven

    <dependency>
      <groupId>io.kjson</groupId>
      <artifactId>mustache-k</artifactId>
      <version>3.3</version>
    </dependency>

Gradle

    implementation 'io.kjson:mustache-k:3.3'

Gradle (kts)

    implementation("io.kjson:mustache-k:3.3")

Peter Wall

2024-11-13

About

Mustache template processor for Kotlin

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages