Loading...

August 28, 2014

Awesome Asciidoctor: Write Extensions Using Groovy (or Java)

We can write extension for Asciidoctor using the extension API in Groovy (or any other JVM language) if we run Asciidoctor on the Java platform using AsciidoctorJ. Extensions could also be written in Ruby of course, but in this post we see how to write a simple inline macro with Groovy.

The extension API has several extension points (Source):

  • Preprocessor: Processes the raw source lines before they are passed to the parser
  • Treeprocessor: Processes the Document (AST) once parsing is complete
  • Postprocessor: Processes the output after the Document has been rendered, before it's gets written to file
  • Block processor: Processes a block of content marked with a custom style (i.e., name) (equivalent to filters in AsciiDoc)
  • Block macro processor: Registers a custom block macro and process it (e.g., gist::12345[])
  • Inline macro processor: Registers a custom inline macro and process it (e.g., btn:[Save])
  • Include processor: Processes the include::[] macro

To write an extension in Groovy (or Java) we must write our implementation class for a specific extension point and we must register the class so AsciidoctorJ knows the class can be used. Registering the implementation is very simple, because it is using the Java Service Provider. This means we have to place a file in the META-INF/services directory on the classpath. The contents of the file is the class name of the implementation class.

Let's start with the Asciidoc markup and then write an implementation to process the inline macro twitter that is used:

= Groovy Inline Macro

Sample document to show extension for Asciidoctor written in Groovy.

// Here we use the twitter: macro.
// The implementation is done in Groovy.
With the twitter macro we can create links to the user's Twitter page like twitter:mrhaki[].

To implement an inline macro we create a new class and extend InlineMacroProcessor. We override the process method to return the value that needs to replace the inline macro in our Asciidoc markup.

// File: src/main/groovy/com/mrhaki/asciidoctor/extension/TwitterMacro.groovy
package com.mrhaki.asciidoctor.extension

import org.asciidoctor.extension.*
import org.asciidoctor.ast.*

import groovy.transform.CompileStatic

@CompileStatic
class TwitterMacro extends InlineMacroProcessor {

    TwitterMacro(final String name, final Map<String, Object> config) {
        super(name, config)
    }

    @Override
    protected Object process(final AbstractBlock parent, 
        final String twitterHandle, final Map<String, Object> attributes) {

        // Define options for an 'anchor' element.
        final Map options = [
            type: ':link',
            target: "http://www.twitter.com/${twitterHandle}".toString()
        ] as Map<String, Object>

        // Prepend twitterHandle with @ as text link.
        final Inline inlineTwitterLink = createInline(parent, 'anchor', "@${twitterHandle}", attributes, options)

        // Convert to String value.
        inlineTwitterLink.convert()
    }

}

We have the implementation class so now we can register the class with Asciidoctor. To register our custom extensions we need to implement the ExtensionRegistry interface. We implement the register method where we can couple our extension class to Asciidoctor.

// File: src/main/groovy/com/mrhaki/asciidoctor/extension/TwitterMacroExtension.groovy
package com.mrhaki.asciidoctor.extension

import org.asciidoctor.extension.spi.ExtensionRegistry
import org.asciidoctor.extension.JavaExtensionRegistry
import org.asciidoctor.Asciidoctor

import groovy.transform.CompileStatic

@CompileStatic
class TwitterMacroExtension implements ExtensionRegistry {

    @Override
    void register(final Asciidoctor asciidoctor) {
        final JavaExtensionRegistry javaExtensionRegistry = asciidoctor.javaExtensionRegistry()
        javaExtensionRegistry.inlineMacro 'twitter', TwitterMacro
    }

}

The class that registers our extension must be available via the Java Service Provider so it is automatically registered within the JVM used to run Asciidoctor. Therefore we need to create the file META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry with the following contents:

# File: src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry
com.mrhaki.asciidoctor.extension.TwitterMacroExtension

We have taken all steps necessary to have our inline macro implementation. We must compile the Groovy classes and add those with the Java Service Provider file to the classpath. We can package the files in a JAR file and define a dependency on the JAR file in our project. If we use Gradle and the Gradle Asciidoctor plugin we can also add the source files to the buildSrc directory of our project. The files will be compiled and added to the classpath of the Gradle project.

With the following Gradle build file we can process Asciidoc markup and execute the twitter inline macro. We store the source files in the buildSrc directory.

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.0'
    }
}

apply plugin: 'org.asciidoctor.gradle.asciidoctor'

The build file in the buildSrc directory has a dependency on AsciidoctorJ. This module makes it possible to run Asciidoctor on the JVM.

// File: buildSrc/build.gradle.
apply plugin: 'groovy'

repositories {
    jcenter()
}

dependencies {
    compile 'org.asciidoctor:asciidoctorj:1.5.0'
}

Let's see part of the HTML that is generated if we transform the Asciidoc markup that is shown at the beginning of this blog post. The twitter inline macro is transformed into a link to the Twitter page of the user:

...
<div class="paragraph">
<p>With the twitter macro we can create links to the user’s Twitter page like <a href="http://www.twitter.com/mrhaki">@mrhaki</a>.</p>
</div>
...

Andres Almiray also wrote about writing extensions with Gradle.

Written with Asciidoctor 1.5.0 and Gradle 2.0.