Dynamic AsciiDoc with Groovy and jBake

Für die Deutsche Version dieses Posts hier klicken

Did you ever wish to be able to create more "dynamic" documents with AsciiDoc?


Attributes are already quite nice. They let you create parameterized docs which you can easily update. One example on how to use attributes are URIs for links in your document.

For example, if you have a reference like

https://fiveandahalfstars.ninja/blog/2020/greenscreen_fuer_online-besprechungen_selbst_bauen[latest post by Johannes]

you can replace this with the following, more readable version

:uri1: https://fiveandahalfstars.ninja/blog/2020/greenscreen_fuer_online-besprechungen_selbst_bauen

{uri1}[latest post by Johannes]

And whenever the link has to be updated (because Johannes published something new), you just have to update the attribute and the link will change wherever it was used.

jBake Meta-Data

In additon to the AsciiDoc attributes, jBake comes with a Metadata-Header. We use this for instance to tag a post with an author. These meta-data snippets are also AsciiDoc attributes. In addtion, you can access them in the page templates. Here is a snippet from our postpreview.gsp template:

<img src="/images/profiles/${post.author}.jpg" alt="${post.author}">

jBake Data-Model

Another concept of jBake is to first parse and store all content in a local database and only then apply the templates.

This turns every blog post into one line of a data table.

Chunks of Data

Now, what happens if you have more data? Or maybe you don’t want to create an extra file for each data entry?

Maybe you want to display the latest measurements of your IoT device or you have a list of talks (like we have) and you want to render them?

In case of the talks, we started out to maintain them as pure AsciiDoc files. That is ok for a start. The list might grow quite quickly and as soon as you want to display such a (long) list in another way, you need to reformat all entries.

In case of the measurements of your IoT device, it is even more obvious - you just don’t want to type the measuremts by hand into an AsciiDoc table.

Two Solutions

To automate things, there are two solutions.

One: a Generator

You already build your docs, so why not convert some json formatted data into AsciiDoc before you hand it over to jBake? In fact, this is a pretty easy approach, depending on the language you use. And for both use cases, it is easier to maintain your data as json file than as AsciiDoc (which is in fact not a data-format 😁).

Code for this could look somthing like this:

//read data file
def json = new File(content.datafile).text
def data = new groovy.json.JsonSlurper().parseText(json)
//iterate over data and output list of talks
def asciidoc = ""
data.talks.each { talk ->
    asciidoc += """\
== ${talk.date} ${talk.title}


new File("talks.adoc").write(asciidoc)

You can then include the result in any .adoc-file. As you can see, it is now easy to (for instance) add the location where the talk will happen or any other detail.

Two: Dynamic AsciiDoc

Do you know PHP, JSP or GSP? With those server-side template languages, you have a syntax which lets you add dynamic elements to an otherwise static template. The generator code seen above could look like

:jbake-type: gsp
:jbake-dataFile: ./data/talks.json

<% talks.each { talk -> %>

== ${talk.date} ${talk.title}


<% } %>

The main difference is now, we inject code within <% …​ %> into our document instead the other way around. The code shown could be embedded in a larger document and since this is part of the document, you don’t have to start an additional build step before your build your docs.

The :jbake-dataFile: specifies where our data comes from.

jBake already implements GSP-Templates. Groovy Server Pages. These allow us to use Groovy code within our templates. And this allows us to use also the body of a document as GSP-Template.

Such a template looks like this:

<%include "header.gsp"%>
<body onload="prettyPrint()">
<div id="wrap">
    <%include "menu.gsp"%>
    <div class="container">

            def template
            def body = ""
            try {
                //read data file
                def json = new File(content.datafile).text
                def data = new groovy.json.JsonSlurper().parseText(json)
                //combine data and template
                def engine = new groovy.text.SimpleTemplateEngine()
                // clean up the body
                // (Asciidoctor replaces a bit too much)
                body = content.body
                        .replaceAll("&#8594;", "->")
                        .replaceAll("&#8656;", "<=")
                        .replaceAll("&lt;", "<")
                        .replaceAll("&gt;", ">")
                        .replaceAll("&amp;", "&")
                template = engine.createTemplate(body).make(data)
            } catch (Exception e) {
                out << "<pre>"
                out << e.message
                out << body
                out << e.getStackTrace().join("\n")
                out << "</pre>"

        <%= template %>

<%include "footer.gsp"%>

et voilà! Dynamic AsciiDoc!

The solution shown is a good solution for some use cases, like the ones mentioned above. Please keep in mind that the resulting AsciiDoc is not standard conform anymore and will only work with your jBake system. The generator approach might be a better solution in certain situations.
2020 11 teaser