Write a Custom Converter

For output formats that are not natively supported by Asciidoctor it is possible to write an own converter in Java. To get your own converter that creates string content running in AsciidoctorJ these steps are required:

  • Implement the converter as a subclass of org.asciidoctor.converter.StringConverter. Annotate it as a converter for your target format using the annotation @org.asciidoctor.converter.ConverterFor.

  • Register the converter at the ConverterRegistry.

  • Pass the target format name to the Asciidoctor instance when rendering a source file.

A basic converter that converts to an own text format looks like this:

org.asciidoctor.converter.TextConverter.java
import org.asciidoctor.ast.ContentNode;
import org.asciidoctor.ast.Document;
import org.asciidoctor.ast.Section;
import org.asciidoctor.ast.StructuralNode;
import org.asciidoctor.converter.ConverterFor;
import org.asciidoctor.converter.StringConverter;
import org.asciidoctor.log.LogRecord;
import org.asciidoctor.log.Severity;

import java.util.Map;

@ConverterFor("text")                                                      (1)
public class TextConverter extends StringConverter {

    private String LINE_SEPARATOR = "\n";

    public TextConverter(String backend, Map<String, Object> opts) {       (2)
        super(backend, opts);
    }

    @Override
    public String convert(
            ContentNode node, String transform, Map<Object, Object> o) {   (3)

        if (transform == null) {                                           (4)
            transform = node.getNodeName();
        }

        if (node instanceof Document) {
            Document document = (Document) node;
            return document.getContent().toString();                       (5)
        } else if (node instanceof Section) {
            Section section = (Section) node;
            return new StringBuilder()
                    .append("== ").append(section.getTitle()).append(" ==")
                    .append(LINE_SEPARATOR).append(LINE_SEPARATOR)
                    .append(section.getContent()).toString();              (5)
        } else if (transform.equals("paragraph")) {
            StructuralNode block = (StructuralNode) node;
            String content = (String) block.getContent();
            return new StringBuilder(content.replaceAll(LINE_SEPARATOR, " "))
                    .append(LINE_SEPARATOR).toString();                    (5)
        } else {
            log(new LogRecord(Severity.WARN, "Unexpected node")); (6)
        }
        return null;
    }

}
1 The annotation @ConverterFor binds the converter to the given target format. That means that when this converter is registered and a document should be rendered with the backend name text this converter will be used for conversion.
2 A converter must implement this constructor, because AsciidoctorJ will call the constructor with this signature. For every conversion a new instance will be created.
3 The method convert() is called with the AST object for the document, i.e. a Document instance, when a document is rendered.
4 The optional parameter transform hints at the transformation to be executed. This could be for example the value embedded to indicate that the resulting document should be without headers and footers. If it is null the transformation usually is defined by the node type and name.
5 Calls to the method getContent() of a node will recursively call the method convert() with the child nodes again. Thereby the converter can collect the rendered child nodes, merge them appropriately and return the rendering of the whole node.
6 Converters can log messages in the same way as extensions. These messages will also be forwarded to build tools like the Asciidoctor Maven plugin and allow failing the build on certain messages.

Finally, the converter can be registered and used for conversion of AsciiDoc documents:

Use the TextConverter
File test_adoc = //...

asciidoctor.javaConverterRegistry().register(TextConverter.class); (1)

String result = asciidoctor.convertFile(
        test_adoc,
        OptionsBuilder.options()
                .backend("text")                                   (2)
                .toFile(false));
1 Registers the converter class TextConverter for this Asciidoctor instance. The given converter is responsible for converting to the target format text because the @ConverterFor annotation of the converter class defines this name.
2 The conversion options backend is set to the value text so that our TextConverter will be used.

Alternatively the converter can be registered automatically once the jar file containing the converter is available on the classpath. Therefore a service implementation for the interface org.asciidoctor.converter.spi.ConverterRegistry has to be in the same jar file. For the TextConverter this implementation could look like this:

org.asciidoctor.integrationguide.converter.TextConverterRegistry
package org.asciidoctor.integrationguide.converter;

import org.asciidoctor.Asciidoctor;
import org.asciidoctor.jruby.converter.spi.ConverterRegistry;

public class TextConverterRegistry implements ConverterRegistry {
    @Override
    public void register(Asciidoctor asciidoctor) {

        asciidoctor.javaConverterRegistry().register(TextConverter.class);

    }
}

The jar file must also contain the services file containing the fully qualified class name of the ConverterRegistry implementation to make this service implementation available:

META-INF/services/org.asciidoctor.jruby.converter.spi.ConverterRegistry
org.asciidoctor.integrationguide.converter.TextConverterRegistry

To render a document with this converter the target format name text has to be passed via the option backend. But note that it is no longer necessary to explicitly register the converter for the target format.

File adocFile = ...
asciidoctor.convertFile(adocFile, Options.builder().backend("text").build());

It is also possible to provide converters for binary formats. In this case the converter should extend the generic class org.asciidoctor.converter.AbstractConverter<T> where T is the return type of the method convert(). StringConverter is actually a concrete subclass for the type String.

Asciidoctor makes some useful information available to the converter via the catalog. The catalog is exposed in AsciidoctorJ under the Document via getCatalog() and offers :

  • getFootnotes() - returns a list of footnotes that occur in the document. Footnotes are available after Document getContent() has been called. A converter will typically use this data to render footnotes at the bottom of a document.

  • getRefs() - returns a map of ids to document elements. Ids are used as a document element target reference 1) to link to and/or 2) for styling, for example by CSS. By default, ids are automatically generated and assigned to sections. They can also be explicitly assigned by the document author to any document element. A converter will typically use this data to lookup ids in support of rendering inline anchors.