Getting Started with Asciidoctor in Java using AsciidoctorJ

AsciidoctorJ is the official library for running Asciidoctor on the JVM. Using AsciidoctorJ, you can convert AsciiDoc content or analyze the structure of a parsed AsciiDoc document from Java and other JVM languages.

Distribution

The version of AsciidoctorJ matches the version of Asciidoctor RubyGem it bundles. AsciidoctorJ is published to Maven Central and Bintray. The artifact information can be found in the tables below.

Table 1. Artifact information for AsciidoctorJ in jCenter (Bintray)
Group Id Artifact Id Version Download

org.asciidoctor

asciidoctorj

1.5.4

pom jar javadoc (jar) sources (jar)

org.asciidoctor

asciidoctorj-epub3

1.5.0-alpha.6

org.asciidoctor

asciidoctorj-pdf

1.5.0-alpha.11

Table 2. Artifact information for AsciidoctorJ in Maven Central
Group Id Artifact Id Version Download

org.asciidoctor

asciidoctorj

1.5.4

pom jar javadoc (jar) sources (jar)

org.asciidoctor

asciidoctorj-epub3

1.5.0-alpha.6

org.asciidoctor

asciidoctorj-pdf

1.5.0-alpha.11

The artifactId changed from asciidoctor-java-integration to asciidoctorj starting in 1.5.0.

Installation

AsciidoctorJ is a standard .jar file. To start using it, you need to add the library to your project’s classpath. To start using it under WildFly AS, you can’t just use it, you also have to modify your WildFly installation due to classpath loading issues; see Running AsciidoctorJ on WildFly AS.

Declaring the dependency in a Maven build file (i.e., pom.xml)
<dependencies>
  <dependency>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctorj</artifactId>
    <version>1.5.4</version> (1)
  </dependency>
</dependencies>
Declaring the dependency in a Gradle build file (e.g., build.gradle)
dependencies {
  compile 'org.asciidoctor:asciidoctorj:1.5.4'
}
Declaring the dependency in an SBT build file (e.g., build.sbt)
libraryDependencies += "org.asciidoctor" % "asciidoctorj" % "1.5.4" (1)
1 Specifying the version of AsciidoctorJ implicitly selects the version of Asciidoctor
Declaring the dependency in a Leiningen build file (e.g., project.clj)
:dependencies [[org.asciidoctor/asciidoctorj "1.5.4"]]

In addition to using AsciidoctorJ directly, you can invoke it as part of your build using the Maven or Gradle plugin.

Converting documents

The main entry point for AsciidoctorJ is the Asciidoctor Java interface. This interface provides four methods for converting AsciiDoc content.

  • convert

  • convertFile

  • convertFiles

  • convertDirectory

You’ll learn about these methods in the converting documents section.

Prior to Asciidoctor 1.5.0, the term render was used in these method names instead of convert (i.e., render, renderFile, renderFiles and renderDirectory). AsciidoctorJ continues to support the old method names for backwards compatibility.
Table 3. Convert methods on the Asciidoctor interface
Method Name Return Type Description

convert

String

Parses AsciiDoc content read from a string or stream and converts it to the format specified by the backend option.

convertFile

String

Parses AsciiDoc content read from a file and converts it to the format specified by the backend option.

convertFiles

String[]

Parses a collection of AsciiDoc files and converts them to the format specified by the backend option.

convertDirectory

String[]

Parses all AsciiDoc files found in the specified directory (using the provided strategy) and converts them to the format specified by the backend option.

All the methods listed in Table 3 are overloaded to accomodate various input types and options.

You retrieve an instance of the Asciidoctor interface from the factory method provided.

Creating an Asciidoctor instance from Asciidoctor.Factory
import static org.asciidoctor.Asciidoctor.Factory.create;
import org.asciidoctor.Asciidoctor;

Asciidoctor asciidoctor = create();

Once you retrieve an instance of the Asciidoctor interface, you can use it to convert AsciiDoc content. Here’s an example of using AsciidoctorJ to convert an AsciiDoc string.

The following convertFile or convertFiles methods will only return a converted String object or array if you disable writing to a file, which is enabled by default. To disable writing to a file, create a new Options object, disable the option to create a new file with option.setToFile(false), and then pass the object as a parameter to convertFile or convertFiles.
Converting an AsciiDoc string
//...
import java.util.HashMap;
//...

String html = asciidoctor.convert(
    "Writing AsciiDoc is _easy_!",
    new HashMap<String, Object>());
System.out.println(html);

The convertFile method will convert the contents of an AsciiDoc file.

Converting an AsciiDoc file
//...
import java.util.HashMap;
//...

String html = asciidoctor.convertFile(
    new File("sample.adoc"),
    new HashMap<String, Object>());
System.out.println(html);

The convertFiles method will convert a collection of AsciiDoc files:

Converting a collection of AsciiDoc files
//...
import java.util.Arrays;
//...

String[] result = asciidoctor.convertFiles(
    Arrays.asList(new File("sample.adoc")),
    new HashMap<String, Object>());

for (String html : result) {
    System.out.println(html);
}
If the converted content is written to files, the convertFiles method will return a String Array (i.e., String[]) with the names of all the converted documents.

Another method provided by the Asciidoctor interface is convertDirectory. This method converts all of the files with AsciiDoc extensions (.adoc (preferred), .ad, .asciidoc, .asc) that are present within a specified folder and following given strategy.

An instance of the DirectoryWalker interface, which provides a strategy for locating files to process, must be passed as the first parameter of the convertDirectory method. Currently Asciidoctor provides two built-in implementations of the DirectoryWalker interface:

Table 4. Built-in DirectoryWalker implementations
Class Description

AsciiDocDirectoryWalker

Converts all files of given folder and all its subfolders. Ignores files starting with underscore (_).

GlobDirectoryWalker

Converts all files of given folder following a glob expression.

If the converted content is not written into files, convertDirectory will return an array listing all the documents converted.

Converting all AsciiDoc files in a directory
//...
import org.asciidoctor.AsciiDocDirectoryWalker;
//...

String[] result = asciidoctor.convertDirectory(
    new AsciiDocDirectoryWalker("src/asciidoc"),
    new HashMap<String, Object>());

for (String html : result) {
    System.out.println(html);
}

Another way to convert AsciiDoc content is by calling the convert method and providing a standard Java java.io.Reader and java.io.Writer. The Reader interface is used as the source, and the converted content is written to the Writer interface.

Converting content read from a java.io.Reader to a java.io.Writer
//...
import java.io.FileReader;
import java.io.StringWriter;
//...

FileReader reader = new FileReader(new File("sample.adoc"));
StringWriter writer = new StringWriter();

asciidoctor.convert(reader, writer, options().asMap());

StringBuffer htmlBuffer = writer.getBuffer();
System.out.println(htmlBuffer.toString());

Safe mode and file system access

Asciidoctor provides security levels that control the read and write access of attributes, the include directive, macros, and scripts while a document is processing. Each level includes the restrictions enabled in the prior security level.

When Asciidoctor (and AsciidoctorJ) is used as API, it uses SECURE safe mode by default. This mode is the most restrictive one and in summary it disallows the document from attempting to read files from the file system and including their contents into the document.

We recommend you to set SAFE safe mode when rendering AsciiDoc documents using AsciidoctorJ to have almost all Asciidoctor features such as icons, include directive or retrieving content from URIs enabled.

Safe mode is set as option when a document is rendered. For example:

import static org.asciidoctor.OptionsBuilder.options;

Map<String, Object> options = options().safe(SafeMode.SAFE)
                                       .asMap();

String outfile = asciidoctor.convertFile(new File("sample.adoc"), options);

We are going to explain in more detail options in next section.

Conversion options

Asciidoctor supports numerous options, such as:

in_place

Converts the output to a file adjacent to the input file.

template_dirs

Specifies a directory of Tilt-compatible templates to be used instead of the default built-in templates

attributes

A Hash (key-value pairs) of attributes to configure various aspects of the AsciiDoc processor

The second parameter of the convert method is java.util.Map. The options listed above can be set in java.util.Map.

Using the in_place option and the backend attribute
Map<String, Object> attributes = new HashMap<String, Object>();
attributes.put("backend", "docbook"); (1)

Map<String, Object> options = new HashMap<String, Object>();
options.put("attributes", attributes); (2)
options.put("in_place", true); (3)

String outfile = asciidoctor.convertFile(new File("sample.adoc"), options);
1 Defines the backend attribute as docbook in the attributes map
2 Registers the attributes map as the attributes option in the options map
3 Defines the in_place option in the options map

Another way for setting options is by using org.asciidoctor.Options class. Options is a simple Java class which contains methods for setting required options. Note that related with org.asciidoctor.Options class, there is org.asciidoctor.Attributes class, which can be used for setting attributes.

The convert method is overloaded so org.asciidoctor.Options can be passed instead of a java.util.Map.

Using the in_place option and the backend attribute
Attributes attributes = new Attributes();
attributes.setBackend("docbook"); (1)

Options options = new Options();
options.setAttributes(attributes); (2)
options.setInPlace(true); (3)

String outfile = asciidoctor.convertFile(new File("sample.adoc"), options);
1 Defines the backend attribute as docbook in the attributes class
2 Registers the attributes class as the attributes option in the options class
3 Defines the in_place option in the options class

AsciidoctorJ also provides two builder classes to create these maps and classes in a more readable form.

AttributesBuilder

Used to define attributes with a fluent API

OptionsBuilder

Used to define options with a fluent API

The code below results in the same output as the previous example but uses the builder classes.

Setting attributes and options with the builder classes
import static org.asciidoctor.AttributesBuilder.attributes;
import static org.asciidoctor.OptionsBuilder.options;

//...
Map<String, Object> attributes = attributes().backend("docbook") (1)
                                             .asMap();

Map<String, Object> options = options().inPlace(true)
                                       .attributes(attributes) (2)
                                       .asMap(); (3)

String outfile = asciidoctor.convertFile(new File("sample.adoc"), options);
1 Defines the backend attribute as docbook using fluent API.
2 Registers the attributes map as attributes.
3 Converts options to java.util.Map instance.
Setting attributes and options with the builder classes
import static org.asciidoctor.AttributesBuilder.attributes;
import static org.asciidoctor.OptionsBuilder.options;

//...
Attributes attributes = attributes().backend("docbook").get(); (1)
Options options = options().inPlace(true).attributes(attributes).get(); (2)

String outfile = asciidoctor.convertFile(new File("sample.adoc"), options); (3)
1 Defines and returns an Attributes class instead of java.util.Map by calling get() method instead of asMap().
2 Defines and returns an Options class instead of java.util.Map by calling get() method instead of asMap().
3 Converts the document passing Options class.
All methods used to convert content are overloaded with OptionsBuilder parameter, so it is no longer required to call get nor asMap methods.
The icons attribute requires a String to set the value used to “draw” icons. At this time, you can use two constants org.asciidoctor.Attributes.IMAGE_ICONS for using the same approach as AsciiDoc, that is using img tags, or org.asciidoctor.Attributes.FONT_ICONS for using icons from Font Awesome.

Attributes can be specified as String or Array instead of pair key/value by using org.asciidoctor.Attributes.setAttributes(String) or org.asciidoctor.Attributes.setAttributes(String...) and AttributesBuilder methods.

Passing attributes as a string
//...
Attributes attributes = attributes().attributes("toc numbered").get();
Options options = options().attributes(attributes).get();

Passing attributes as a string is equivalent to passing individual attributes.

Passing individual attributes
//...
Attributes attributes = attributes().tableOfContents(true).sectionNumbers(true).get();
Options options = options().attributes(attributes).get();

You can also use an array.

Passing attributes as an array
//...
String[] attributesArray = new String[]{"toc", "source-highlighter=coderay"};
Attributes attributes = attributes().attributes(attributesArray).sectionNumbers(true).get();
Options options = options().attributes(attributes).get();

Passing attributes as an array is equivalent to passing individual attribute.

Passing individual attributes
//...
Attributes attributes = attributes().tableOfContents(true).sectionNumbers(true).sourceHighlighter("coderay").get();
Options options = options().attributes(attributes).get();

Locating files

A utility class AsciiDocDirectoryWalker is available for searching the AsciiDoc files present in a root folder and its subfolders. AsciiDocDirectoryWalker locates all files that end with .adoc, .ad, .asciidoc or .asc. Also it ignores all files starting with underscore (_).

Locating AsciiDoc files with AsciiDocDirectoryWalker
import java.util.List;
import org.asciidoctor.AsciiDocDirectoryWalker;

DirectoryWalker directoryWalker = new AsciiDocDirectoryWalker("docs"); (1)
List<File> asciidocFiles = directoryWalker.scan(); (2)
1 Defines which parent directory is used for searching.
2 Returns a list of all AsciiDoc files found in root folder and its subfolders.

A utility class GlobDirectoryWalker is available for searching the AsciiDoc files present in a root folder and scanning using a Glob expression. GlobDirectoryWalker locates all files that end with .adoc, .ad, .asciidoc or .asc.

Locating AsciiDoc files with GlobDirectoryWalker
import java.util.List;
import org.asciidoctor.GlobDirectoryWalker;

DirectoryWalker directoryWalker = new GlobDirectoryWalker("docs", "**/*.adoc"); (1)
List<File> asciidocFiles = directoryWalker.scan(); (2)
1 Defines which parent directory is used for searching and the glob expression.
2 Returns a list of all AsciiDoc files matching given glob expression.

Reading the document tree

Instead of converting an AsciiDoc document, you may want to parse the document to read information it contains or navigate the document’s structure. AsciidoctorJ let’s you do this!

There are two approaches you can take to read the structure of an AsciiDoc document with AsciidoctorJ.

Using wrapper classes not connected with Ruby internal model

The structure is copied in Java non-proxied classes so any change does not modify the original document.

Using JRuby Java wrapper classes

The Java classes are linked to Ruby internal classes. Any modifications done here are reflected to the original document.

Wrapping classes

Example AsciiDoc document with header information
= Sample Document
Doc Writer <doc.writer@asciidoc.org>; John Smith <john.smith@asciidoc.org>
v1.0, 2013-05-20: First draft
:title: Sample Document
:tags: [document, example]

Preamble...

The readDocumentHeader method on the Asciidoctor interface retrieves information from the header of an AsciiDoc document without parsing or converting the entire document. This method returns an instance of org.asciidoctor.ast.DocumentHeader with all information from the header filled.

Reading header information from the AsciiDoc document
//...
import org.asciidoctor.ast.DocumentHeader;

//...
DocumentHeader header = asciidoctor.readDocumentHeader(
    new File("header-sample.adoc"));

System.out.println(header.getDocumentTitle().getMain()); (1)

Author author = header.getAuthor(); (2)
System.out.println(author.getEmail()); (3)
System.out.println(author.getFullName()); (4)

RevisionInfo revisionInfo = header.getRevisionInfo();

System.out.println(revisionInfo.getDate()); (5)
System.out.println(revisionInfo.getNumber()); (6)
System.out.println(revisionInfo.getRemark()); (7)
1 prints Sample Document
2 prints Doc Writer
3 prints doc.writer@asciidoc.org
4 prints Doc Writer
5 prints 2013-05-20
6 prints 1.0
7 prints First draft

The readDocumentHeader method can be extremely useful for building an index of documents.

import java.io.File;
import java.util.HashSet;
import java.util.Set;
import org.asciidoctor.Asciidoctor;
import org.asciidoctor.AsciiDocDirectoryWalker;
import org.asciidoctor.DirectoryWalker;
import org.asciidoctor.DocumentHeader;

//...

Asciidoctor asciidoctor = Asciidoctor.Factory.create();
Set<DocumentHeader> documentIndex = new HashSet<DocumentHeader>();
DirectoryWalker directoryWalker = new AsciiDocDirectoryWalker("docs"); (1)

for (File file : directoryWalker.scan()) {
  documentIndex.add(asciidoctor.readDocumentHeader(file));
}
1 Converts all files in the docs folder and its subfolders.

You can also load the document inside a Document object. This object represents the whole document, including its headings. You can use it to navigate through the internals of a parsed document. To load a document, use the load or loadFile methods.

The readDocumentStructure method provides a useful way of parsing an AsciiDoc file into the structured object. First, it gathers the same information as readDocumentHeader and puts it in the header field of the StructuredDocument object. The actual content of the file is split into separate ContentParts based on blocks of the content.

This feature provides several use cases.

AsciiDoc document with two blocks defined by section titles
= Sample Document

== Section one
This is content of section one

== Section two
And content of section two

...

Each section defines new content part. List of all parts can be get by getParts method on StructuredDocument. Each part will than contain of title (i.e. "Section one") and converted text content as html.

Print content of each part
for (ContentPart part : document.getParts()){
  System.out.println(part.getTitle());
  System.out.println("----");
  System.out.println(part.getContent);
  System.out.println("----");
}
AsciiDoc document with two blocks defined by styles
= Sample Document

[style one]
This is content of first content part

[[partId]]
[style two,role=partRole]
--
And content of second content part

This block can be as long as you want.
--

This way you can then use methods like getPartByStyle to retrieve particular content parts.

Retrieve content part by style
ContentPart style_two = document.getPartByStyle("style two");
// other possible way of retrieving parts:
ContentPart style_two = document.getPartById("partId")
ContentPart style_two = document.getPartByRole("partRole")

//and also for lists
List<ContentPart> parts = document.getPartsByStyle("style two");
List<ContentPart> parts = document.getPartsByRole("partRole");
List<ContentPart> parts = document.getPartsByContext("open");

Really nice thing about it is possibility to parse images to Image object that you can use later to embed in html page directly from your java code or manipulate in any other way.

Define images
[Images]
image::src/some{sp}image{sp}1.JPG[TODO title1,link="link1.html"]
image::src/some{sp}image{sp}2.JPG[TODO title2,link="link2.html"]

to get a list of images defined in the document and then to process images:

Retrieve image information
List<ContentPart> images = document.getPartsByContext("image");
for (ContentPart image : images){
  String src = (String) image.getAttributes().get("target");
  String alt = (String) image.getAttributes().get("alt");
  String link = (String) image.getAttributes().get("link");
}

As of final example consider following complete use case:

AsciiDoc document with product definition
= Sample product
v1.0, 2013-10-12
:hardbreaks:

:price: 70 pln
:smallImage: photos/small/small_image.jpg

[Description]
short product description

[Images]
image::photos/image1.jpg[title]
image::photos/image2.jpg[title]


[Detail]
--
Detail information about product. Note that you can use all asciidoc features here like:
.simple list
* lists
* images
* titles
* further blocks

[role=text-center]
also you can also add css style by assigning role to the text.
--

and the way it can be than transformed to java object:

Java method for getting product
Product product = new Product();
product.setTitle(document.getHeader().getDocumentTitle());
product.setPrice(new Price((String) document.getHeader().getAttributes().get("price")));
product.setSmallImage(new Image((String)document.getHeader().getAttributes().get("smallImage"),product.getTitle()));

product.setDescription(document.getPartByStyle("description").getContent());

List<ContentPart> images = document.getPartsByContext("image");
for (ContentPart image : images) {
  Image image = new Image();
  image.setSrc((String) image.getAttributes().get("target"));
  image.setAlt((String) image.getAttributes().get("alt"));
  product.getImages().add(image);
}

product.setDetail(document.getPartByStyle("detail").getContent());

Last feature of structure document is possibility to configure how deeply should blocks be processed. Default is one level only so if you want to have more nested structure add STRUCTURE_MAX_LEVEL parameter to processing options.

Configuration of the structure document processing
Map<String,Object> parameters = new HashMap<String, Object>();
parameters.put(Asciidoctor.STRUCTURE_MAX_LEVEL, 2);
StructuredDocument document = asciidoctor.readDocumentStructure(
    new File("target/test-classes/documentblocks.asciidoc"),
    parameters);

JRuby wrapping classes

import org.asciidoctor.ast.Document;

//...

Document document = asciidoctor.load(DOCUMENT, new HashMap<String, Object>()); (1)
assertThat(document.doctitle(), is("Document Title")); (2)
1 Document from an String is loaded into Document object.
2 Title of the document is retrieved.

But also all blocks that conforms the document can be retrieved. Currently there are support for three kinds of blocks. Blocks itself, Section for sections of the document and AbstractBlock which is the base type where all kind of blocks (including those not mapped as Java class) are mapped.

import org.asciidoctor.ast.Document;
import org.asciidoctor.ast.Section;

//...

Document document = asciidoctor.load(DOCUMENT, new HashMap<String, Object>()); (1)
Section section = (Section) document.blocks().get(1); (2)

assertThat(section.index(), is(0)); (3)
assertThat(section.sectname(), is("sect1"));
assertThat(section.special(), is(false));
1 Document from an String is loaded into Document object.
2 All blocks are get and because in this example the first block is a Section block, we cast it directly.
3 Concrete methods for sections can be called.

Blocks can also be retrieved from query using findBy method.

Document document = asciidoctor.load(DOCUMENT, new HashMap<String, Object>());
Map<Object, Object> selector = new HashMap<Object, Object>(); (1)
selector.put("context", ":image"); (2)

List<AbstractBlock> findBy = document.findBy(selector);
assertThat(findBy, hasSize(2)); (3)
1 To make queries you need to use a map approach. Currently this is because of the Asciidoctor API but it will change in near future.
2 In this example all blocks with context as image is returned. Notice that the colon (:) must be added in the value part.
3 Document used as example contains two images.

Extension API

One of the major improvements to Asciidoctor recently is the extensions API. AsciidoctorJ brings this extension API to the JVM environment. AsciidoctorJ allows us to write extensions in Java instead of Ruby.

Asciidoctor provides seven types of extension points. Each extension point has an abstract class in Java that maps to the extension API in Ruby.

Table 5. AsciidoctorJ extension APIs
Name Class

Preprocessor

org.asciidoctor.extension.Preprocessor

Treeprocessor

org.asciidoctor.extension.Treeprocessor

Postprocessor

org.asciidoctor.extension.Postprocessor

Block processor

org.asciidoctor.extension.BlockProcessor

Block macro processor

org.asciidoctor.extension.BlockMacroProcessor

Inline macro processor

org.asciidoctor.extension.InlineMacroProcessor

Include processor

org.asciidoctor.extension.IncludeProcessor

Docinfo processor

org.asciidoctor.extension.DocinfoProcessor

To create an extension two things are required:

  1. Create a class implementing an extension class (this will depend on the kind of interface being developed)

  2. Register your class using the JavaExtensionRegistry class

An extension can be registered by:

  • Passing as String a fully qualified class.

  • Passing a Class object of the extension.

  • Passing an instance of the extension object.

In the first two cases, the lifecycle of the instance is managed by JRuby. In the last case, the caller is the owner of the lifecycle of the object.
Using an already created instance as an extension is useful when using CDI. For example, you can inject any extension inside the code and then register that instance as an extension.

Preprocessor

This extension updates an attribute value defined in a document.

Preprocessor example
public class ChangeAttributeValuePreprocessor extends Preprocessor { (1)

        public ChangeAttributeValuePreprocessor(Map<String, Object> config) { (2)
                super(config);
        }

        @Override
        public PreprocessorReader process(Document document, PreprocessorReader reader) { (3)
                document.getAttributes().put("content", "Alex");
                return reader;
        }

}
1 Class must extend from Preprocessor.
2 A constructor receiving a Map must be provided in case you want to send options to the preprocessor.
3 The process method receives a Document and PreprocessorReader.
Register a Preprocessor
JavaExtensionRegistry extensionRegistry = this.asciidoctor.javaExtensionRegistry(); (1)

extensionRegistry.preprocessor(ChangeAttributeValuePreprocessor.class); (2)

String content = asciidoctor.convertFile(new File(
                "target/test-classes/changeattribute.adoc"),
                new Options()); (3)
1 JavaExtensionRegistry class is created.
2 Preprocessor extension is registered by using Class approach.
3 We can call any convert method as usual; no extra parameters are required.

Treeprocessor

This extension detects literal blocks that contain terminal commands. It strips the prompt character and styles the command.

Treeprocessor example
public class TerminalCommandTreeprocessor extends Treeprocessor { (1)

  private Document document;

  public TerminalCommandTreeprocessor(Map<String, Object> config) {
      super(config);
  }

  @Override
  public Document process(Document document) {

    this.document = document;

    final List<AbstractBlock> blocks = this.document.blocks(); (2)

    for (int i = 0; i < blocks.size(); i++) {
        final AbstractBlock currentBlock = blocks.get(i);
        if(currentBlock instanceof Block) {
            Block block = (Block)currentBlock;
            List<String> lines = block.lines(); (3)
            if (lines.size() > 0 && lines.get(0).startsWith("$")) {
                blocks.set(i, convertToTerminalListing(block));
            }
        }
    }

    return this.document;
  }

  public Block convertToTerminalListing(Block block) {

    Map<String, Object> attributes = block.getAttributes();
    attributes.put("role", "terminal");
    StringBuilder resultLines = new StringBuilder();

    List<String> lines = block.lines();

    for (String line : lines) {
        if (line.startsWith("$")) {
            resultLines.append("<span class=\"command\">")
                       .append(line.substring(2, line.length()))
                       .append("</command");
        }
        else {
            resultLines.append(line);
        }
    }

    return createBlock(this.document, "listing", Arrays.asList(resultLines.toString()), attributes,
              new HashMap<Object, Object>()); (4)
  }

}
1 Class must extend from Treeprocessor.
2 The document instance can be used to retrieve all blocks of the current document.
3 All of the select block’s lines are retrieved.
4 To create a new block, you must use the createBlock method. Set the parent document, the context (listing), the text content, attributes and options.
Register a Treeprocessor
JavaExtensionRegistry extensionRegistry = this.asciidoctor.javaExtensionRegistry(); (1)

extensionRegistry.treeprocessor("org.asciidoctor.extension.TerminalCommandTreeprocessor"); (2)

String content = asciidoctor.convertFile(new File(
                "target/test-classes/sample-with-terminal-command.adoc"),
                new Options()); (3)
1 JavaExtensionRegistry class is created.
2 Treeprocessor extension is registered using fully qualified class name as String.
3 We can call any convert method as usually, no extra parameters are required.

Postprocessor

This extension inserts custom footer text.

Postprocessor example
public class CustomFooterPostProcessor extends Postprocessor { (1)

  public CustomFooterPostProcessor(Map<String, Object> config) {
      super(config);
  }

  @Override
  public String process(Document document, String output) { (2)

      String copyright  = "Copyright Acme, Inc.";

      if(document.basebackend("html")) {
          org.jsoup.nodes.Document doc = Jsoup.parse(output, "UTF-8");

          Element contentElement = doc.getElementById("footer-text");
          contentElement.append(copyright);

          output = doc.html();

      }
      return output; (3)
  }

}
1 Class must extend from Postprocessor.
2 process method receives the Document instance and the document converted as String.
3 The content that will be written in document is returned.
Register a Postprocessor
JavaExtensionRegistry extensionRegistry = this.asciidoctor.javaExtensionRegistry(); (1)

extensionRegistry.postprocessor(new CustomFooterPostProcessor(
                                                        new HashMap<String, Object>())); (2)

String content = asciidoctor.convertFile(new File(
              "sample.adoc"),
              options); (3)
1 JavaExtensionRegistry class is created.
2 Postprocessor extension is registered using an already created instance approach.
3 We can call any convert method as usually, no extra parameters are required.

DocinfoProcessor

This extension inserts custom content on header or footer of the document. For example can be used for adding meta tags on <head> tags.

DocinfoProcessor example
public class MetaRobotsDocinfoProcessor extends DocinfoProcessor { (1)

    public MetaRobotsDocinfoProcessor() {
        super();
    }

    public MetaRobotsDocinfoProcessor(Map<String, Object> config) {
        super(config);
    }

    @Override
    public String process(Document document) {
        return "<meta name=\"robots\" content=\"index,follow\">"; (2)
    }
}
1 Class must extend from DocinfoProcessor.
2 In this case a meta tag is returned. By default docinfo is placed on header.

To register the DocinfoProcessor extension just use the JavaExtensionRegistry.

Register a DocinfoProcessor
JavaExtensionRegistry javaExtensionRegistry = this.asciidoctor.javaExtensionRegistry(); (1)

javaExtensionRegistry.docinfoProcessor(MetaRobotsDocinfoProcessor.class); (2)

String content = asciidoctor.renderFile(
                classpath.getResource("simple.adoc"),
                options().headerFooter(true).safe(SafeMode.SERVER).toFile(false).get());
1 JavaExtensionRegistry class is created.
2 DocinfoProcessor extension is registered using an already created instance approach.

If you want to place docinfo on footer you need to set location option to footer.

Register a DocinfoProcessor
JavaExtensionRegistry javaExtensionRegistry = this.asciidoctor.javaExtensionRegistry();

Map<String, Object> options = new HashMap<String, Object>();
options.put("location", ":footer"); (1)
MetaRobotsDocinfoProcessor metaRobotsDocinfoProcessor = new MetaRobotsDocinfoProcessor(options);

javaExtensionRegistry.docinfoProcessor(metaRobotsDocinfoProcessor);

String content = asciidoctor.renderFile(
                classpath.getResource("simple.adoc"),
                options().headerFooter(true).safe(SafeMode.SERVER).toFile(false).get());
1 Sets the location option to footer. Note that a colon (:) is placed before footer.

Block processor

This extension registers a custom block style named yell that uppercases all the words.

Block processor example
public class YellBlock extends BlockProcessor { (1)

  public YellBlock(String name, Map<String, Object> config) { (2)
      super(name, config);
  }

  @Override
  public Object process(AbstractBlock parent, Reader reader, Map<String, Object> attributes) { (3)
      List<String> lines = reader.readLines();
      String upperLines = null;
      for (String line : lines) {
          if (upperLines == null) {
              upperLines = line.toUpperCase();
          }
          else {
              upperLines = upperLines + "\n" + line.toUpperCase();
          }
      }

                  return createBlock(parent, "paragraph", Arrays.asList(upperLines), attributes, new HashMap<Object, Object>()); (4)
  }

}
1 Class must extend from BlockProcessor.
2 Constructor must receive the name of the block and a Map for sending options to block.
3 process method receives the parent block, a reader, and attributes defined in block.
4 To create a new block we must use createBlock method. We must set the parent document, the context (listing), the text content, attributes and options.
Register a Block processor
JavaExtensionRegistry extensionRegistry = this.asciidoctor.javaExtensionRegistry(); (1)

extensionRegistry.block("yell", YellBlock.class); (2)

String content = asciidoctor.convertFile(new File(
                "target/test-classes/sample-with-yell-block.adoc"),
                new Options()); (3)
1 JavaExtensionRegistry class is created.
2 BlockProcessor extension is registered with the context of block.
3 We can call any convert method as usually, no extra parameters are required.
Example of Block processor
[yell] (1)
The time is now. Get a move on.
1 Note that yell is the context where block lives and is the same as the first parameter of block method of ExtensionRegistry class.

Block macro processor

This extension creates a block macro named gist for embedding a gist.

Block macro processor example
public class GistMacro extends BlockMacroProcessor { (1)

  public GistMacro(String macroName, Map<String, Object> config) { (2)
      super(macroName, config);
  }

  @Override
  public Block process(AbstractBlock parent, String target,
                        Map<String, Object> attributes) { (3)

    String content = "<div class=\"content\">\n" +
                       "<script src=\"https://gist.github.com/"+target+".js\"></script>\n" +
                       "</div>";

    return createBlock(parent, "pass", Arrays.asList(content), attributes,
                        this.getConfig()); (4)
  }

}
1 Class must extend from BlockMacroProcessor.
2 Constructor must receive the macro name, and a Map for sending options to block..
3 process method receives the parent document, the content of the macro, and attributes defined in macro.
4 To create a new block we must use createBlock method. We must set the parent document, the context (listing), the text content, attributes and options.
Register a Block macro processor
JavaExtensionRegistry extensionRegistry = this.asciidoctor.javaExtensionRegistry(); (1)

extensionRegistry.blockMacro("gist", GistMacro.class); (2)

String content = asciidoctor.convertFile(new File(
                "target/test-classes/sample-with-gist-macro.adoc"),
                new Options()); (3)
1 JavaExtensionRegistry class is created.
2 BlockMacroProcessor extension is registered with the name of the macro.
3 We can call any convert method as usually, no extra parameters are required.
Example of Block macro processor
.My Gist
gist::123456[] (1)
1 Note that gist is the name of the macro and is the same as the first parameter of blockMacro method of ExtensionRegistry class.

Inline macro processor

This extension creates an inline macro named man that links to a manpage.

Inline macro processor example
public class ManpageMacro extends InlineMacroProcessor { (1)

  public ManpageMacro(String macroName, Map<String, Object> config) { (2)
      super(macroName, config);
  }

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

      Map<String, Object> options = new HashMap<String, Object>();
      options.put("type", ":link");
      options.put("target", target + ".html");
      return createInline(parent, "anchor", target, attributes, options).convert(); (4)
  }

}
1 Class must extend from InlineMacroProcessor.
2 Constructor must receive the macro name, and a Map for passing options to extension.
3 process method receives the parent document, the content of the macro, and attributes defined in macro.
4 Because it is an inline macro, only a replacement string must be returned.
Register an Inline macro processor
JavaExtensionRegistry extensionRegistry = this.asciidoctor.javaExtensionRegistry(); (1)

extensionRegistry.inlineMacro("man", ManpageMacro.class); (2)

String content = asciidoctor.convertFile(new File(
                "target/test-classes/sample-with-man-link.adoc"),
                new Options()); (3)
1 ExtensionRegistry class is created.
2 InlineMacroProcessor extension is registered with the name of the macro.
3 We can call any convert method as usually, no extra parameters are required.
Example of Inline macro processor
See man:gittutorial[7] to get started. (1)
1 Note that man is the name of the macro and is the same as the first parameter of inlineMacro method of ExtensionRegistry class.

Include processor

Include a file from a URI.

Include processor example
public class UriIncludeProcessor extends IncludeProcessor { (1)

  public UriIncludeProcessor(Map<String, Object> config) {
    super(config);
  }

  @Override
  public boolean handles(String target) {
    return target.startsWith("http://") || target.startsWith("https://"); (2)
  }

  @Override
  public void process(DocumentRuby document, PreprocessorReader reader, String target,
            Map<String, Object> attributes) {

    StringBuilder content = readContent(target);
    reader.push_include(content.toString(), target, target, 1, attributes); (3)

  }

  private StringBuilder readContent(String target) {

    StringBuilder content = new StringBuilder();

    try {

      URL url = new URL(target);
      InputStream openStream = url.openStream();

      BufferedReader bufferedReader = new BufferedReader(
                    new InputStreamReader(openStream));

      String line = null;
      while ((line = bufferedReader.readLine()) != null) {
        content.append(line);
      }

      bufferedReader.close();

      } catch (MalformedURLException e) {
          throw new IllegalArgumentException(e);
      } catch (IOException e) {
          throw new IllegalArgumentException(e);
      }
      return content;
  }

}
1 Class must extend from IncludeProcessor.
2 handles method is used by processor to decide if included element should be converted by this processor or not. target attribute is the value of include macro.
3 push_include method inserts new content (retrieved from the url) in current position of document.
Register an Inline macro processor
JavaExtensionRegistry extensionRegistry = this.asciidoctor.javaExtensionRegistry(); (1)

extensionRegistry.includeProcessor(UriIncludeProcessor.class); (2)

String content = asciidoctor.convertFile(new File(
                "target/test-classes/sample-with-uri-include.adoc"),
                new Options()); (3)
1 ExtensionRegistry class is created.
2 IncludeProcessor extension is registered.
3 We can call any convert method as usually; no extra parameters are required.
Example of include processor
= Example of URI

.Gemfile
[source,ruby]
----
include::https://raw.githubusercontent.com/asciidoctor/asciidoctor/master/Gemfile[]
----

Unregistering extensions

You can unregister all extensions by calling the org.asciidoctor.Asciidoctor.unregisterAllExtensions() method.

Ruby extensions

You can even register extensions written in Ruby using AsciidoctorJ. To register a Ruby extension you must get a RubyExtensionRegistry class instead of JavaExtensionRegistry.

Register a Ruby extension in Java
RubyExtensionRegistry rubyExtensionRegistry = this.asciidoctor.rubyExtensionRegistry(); (1)
rubyExtensionRegistry.loadClass(Class.class.getResourceAsStream("/YellRubyBlock.rb")).block("rubyyell", "YellRubyBlock"); (2)

String content = asciidoctor.convertFile(new File(
                "target/test-classes/sample-with-ruby-yell-block.ad"),
                options().toFile(false).get());
1 rubyExtensionRegistry method is called to get a rubyExtensionRegistry instance.
2 Ruby file containing a class implementing a Block extension is loaded inside the Ruby runtime. Then the block is registered with a name (rubyyell), and we pass the name of the class to be instantiated.
YellBlock.rb
require 'asciidoctor'
require 'asciidoctor/extensions'

class YellRubyBlock < Asciidoctor::Extensions::BlockProcessor
  option :contexts, [:paragraph]
  option :content_model, :simple

  def process parent, reader, attributes
    lines = reader.lines.map {|line| line.upcase.gsub(/\.( |$)/, '!\\1') }
    Asciidoctor::Block.new parent, :paragraph, :source => lines, :attributes => attributes
  end
end

Extension SPI

In previous examples, the extensions were registered manually. However, AsciidoctorJ provides another way to register extensions. If any implementation of the SPI interface is present on the classpath, it will be executed.

To create an autoloadable extension you should do next steps.

Create a class that implements org.asciidoctor.extension.spi.ExtensionRegistry.

org.asciidoctor.extension.ArrowsAndBoxesExtension.java
public class ArrowsAndBoxesExtension implements ExtensionRegistry { (1)

  @Override
  public void register(Asciidoctor asciidoctor) { (2)

    JavaExtensionRegistry javaExtensionRegistry = asciidoctor.javaExtensionRegistry();
    javaExtensionRegistry.postprocessor(ArrowsAndBoxesIncludesPostProcessor.class); (3)
    javaExtensionRegistry.block("arrowsAndBoxes", ArrowsAndBoxesBlock.class);

  }

}
1 To autoload extensions you need to implement ExtensionRegistry.
2 AsciidoctorJ will automatically run the register method. The method is responsible for registering all extensions.
3 All required Java extensions are registered.

Next, you need to create a file called org.asciidoctor.extension.spi.ExtensionRegistry inside META-INF/services with the implementation’s full qualified name.

META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry
org.asciidoctor.extension.ArrowsAndBoxesExtension

And that’s all. Now when a .jar file containing the previous structure is dropped inside classpath of AsciidoctorJ, the register method will be executed automatically and the extensions will be registered.

Converting to EPUB3

The Asciidoctor EPUB3 gem (asciidoctor-epub3) is bundled inside the AsciidoctorJ EPUB3 jar (asciidoctorj-epub3). To use it, simply add the asciidoctorj-epub3 jar to your dependencies. The version of the AsciidoctorJ EPUB3 jar aligns with the version of the Asciidoctor EPUB3 gem.

Here’s how you can add the AsciidoctorJ EPUB3 jar to your Maven dependencies:

<dependencies>
  <dependency>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctorj-epub3</artifactId>
    <version>1.5.0-alpha.4</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>

Once you’ve added the AsciidoctorJ EPUB3 jar to your classpath, you can set the backend attribute to epub3. The document will be converted to the EPUB3 format.

The asciidoctor-epub3 gem is alpha. While it can be used successfully, there may be bugs and its functionality may change in incompatible ways before the first stable release. In other words, by using it, you are also testing it ;)

Let’s see an example of how to use AsciidoctorJ with the EPUB3 converter.

spine.adoc
= Book Title
Author Name
:imagesdir: images (1)

include::content-document.adoc[] (2)
1 The EPUB3 converter requires the value of the imagesdir attribute to be images.
2 The EPUB3 converter must be run on a spine document that has at least one include directive (and no other body content) in order to function properly.
content-document.adoc
= Content Title
Author Name

[abstract]
This is the actual content.

== First Section

And off we go.

And finally we can convert the document to EPUB3 using AsciidoctorJ.

asciidoctor.convertFile(new File("spine.adoc"),
                options().safe(SafeMode.SAFE).backend("epub3").get());  (1) (2)

assertThat(new File("target/test-classes/index.epub").exists(), is(true));
1 Currently, the EPUB3 converter must be run in SAFE or UNSAFE mode due to a bug
2 epub3 is the name of the backend that must be set to convert to EPUB3.

Loading Ruby libraries

Simple extensions may be fully implemented in Java, but if you want to create complex extensions you can mix Ruby and Java code. This means that you may need to execute a Ruby file or a RubyGem (i.e., gem) inside your extension.

To load a Ruby file inside the Ruby runtime, you can use org.asciidoctor.internal.RubyUtils.loadRubyClass(Ruby, InputStream). You can also load a gem using an API that wraps Ruby’s require command. The gem must be available inside the classpath. Next run org.asciidoctor.internal.RubyUtils.requireLibrary(Ruby, String), passing the name of the gem as the second argument.

JRuby instance

Sometimes you may need the Ruby runtime used inside AsciidoctorJ. One reason is because you are using JRuby outside AsciidoctorJ and you want to reuse the same instance. Another reason is that you need to instantiate by yourself an Asciidoctor Ruby object.

To get this instance you can use org.asciidoctor.internal.JRubyRuntimeContext.get() to get it.

GEM_PATH

By default, AsciidoctorJ comes with all required gems bundled within the jar. In certain circumstances, you may want to load gems from an external folder. To accomplish this scenario, create method provides a parameter to set folder where gems are present. Internally, AsciidoctorJ will set GEM_PATH environment variable to given path.

Example of setting GEM_PATH
import static org.asciidoctor.Asciidoctor.Factory.create;
import org.asciidoctor.Asciidoctor;

Asciidoctor asciidoctor = create("/my/gem/path"); (1)
1 Creates an Asciidoctor instance with given GEM_PATH location.

Using AsciidoctorJ in an OSGi environment

In a non OSGi context, the following snippet will successfully create an Asciidoctor object:

import static org.asciidoctor.Asciidoctor.Factory.create;
import org.asciidoctor.Asciidoctor;

Asciidoctor asciidoctor = create();

In an OSGi context it will not work because JRuby needs some paths to find the gems (the Asciidoctor ones and the Ruby themselves ones). In order to make it work, you will need two more classes (RubyInstanceConfig and JavaEmbedUtils) and a small modification of the previous snippet of code. The modifications take care of the class loaders because in OSGi, which are a key point in OSGi:

import static org.asciidoctor.Asciidoctor.Factory.create;
import org.asciidoctor.Asciidoctor;

RubyInstanceConfig config = new RubyInstanceConfig();
config.setLoader(this.getClass().getClassLoader()); (1)

JavaEmbedUtils.initialize(Arrays.asList("META-INF/jruby.home/lib/ruby/2.0", "gems/asciidoctor-1.5.4/lib"), config); (2) (3) (4)

Asciidoctor asciidoctor = create(this.getClass().getClassLoader()); (5)
1 The RubyInstanceConfig will use the class loader of the OSGi bundle ;
2 The JavaEmbedUtils will specify the load paths of the required gems. If they are not specified, you will get JRuby exceptions ;
3 META-INF/jruby.home/lib/ruby/2.0 specifies where the Ruby gems are located. Actually this path is located inside the jruby-complete-<version>.jar file. Without having this path specified you may get an org.jruby.exceptions.RaiseException: (LoadError) no such file to load — set error ;
4 gems/asciidoctor-<version>/lib specifies where the gems for Asciidoctor are located. Actually this path is located inside the asciidoctorj-<version>.jar file ;
5 The factory for the Asciidoctor object also specify the class loader to use.
We consider this code to be placed inside an OSGi bundle

This solution has pros and cons:

  • Pros: you don’t need to extract the gems located in the asciidoctorj binary ;

  • Cons:

    • the version of asciidoctor is hard coded ;

    • the version of ruby is hard coded.

Optimization

JRuby may start slower than expected versus the C-based Ruby implementation (MRI). Fortunately, JRuby offers flags that can improve the start time and tune applications. Several Java flags can also be used in conjunction with or apart from the JRuby flags in order to improve the start time even more.

For small tasks such as converting an AsciiDoc document, two JRuby flags can drastically improve the start time:

Table 6. JRuby flags
Name Value

jruby.compat.version

RUBY1_9

jruby.compile.mode

OFF

Both flags are set by default inside AsciidoctorJ.

The Java flags available for improving start time depend on whether your working on a 32- or 64-bit processor and your JDK version. These flags are set by using the JRUBY_OPTS environment variable. Let’s see a summary of these flags and in which environments they can be used.

Table 7. Java flags
Name JDK

-client

32 bit Java

-Xverify:none

32/64 bit Java

-XX:+TieredCompilation

32/64 bit Java SE 7

-XX:TieredStopAtLevel=1

32/64 bit Java SE 7

Setting flags for Java SE 6
export JRUBY_OPTS="-J-Xverify:none -J-client" (1)
1 Note that you should add -J before the flag.

You can find a full explanation on how to improve the start time of JRuby applications in Optimization.

Running AsciidoctorJ on WildFly AS

If you want to use AsciidoctorJ in an application deployed on WildFly AS, you have to install AsciidoctorJ as a JBoss Module.

Follow the steps below:

  1. Create an Asciidoctor module for WildFly AS.

  2. Create the following folder tree: $JBOSS_HOME/modules/org/asciidoctor/main.

  3. Create the module descriptor file module.xml.

    Asciidoctor module descriptor for WildFly AS
    <?xml version="1.0" encoding="UTF-8"?>
    <module xmlns="urn:jboss:module:1.0" name="org.asciidoctor">
        <resources>
            <resource-root path="asciidoctorj-1.5.4.jar"/>
            <resource-root path="jcommander-1.35.jar"/>
            <resource-root path="jruby-complete-1.7.21.jar"/>
        </resources>
        <dependencies>
            <module name="sun.jdk" export="true">
                <imports>
                    <include path="sun/misc/Unsafe" />
                </imports>
            </module>
            <module name="javax.management.j2ee.api"/>
            <module name="javax.api"/>
            <module name="org.slf4j"/>
        </dependencies>
    </module>
  4. Copy the jar files into the same folder as the module.xml file.

  5. Make sure the version numbers of the jar files agree with what’s in the current set. Restart WildFly for the new module to take effect.

  6. Add a dependency on your Java archive to this WildFly module using one of the following options:

    1. Add the dependency just into the MANIFEST.MF file.

      MANIFEST.MF file example with dependency to Asciidoctor module
      Manifest-Version: 1.0
      Dependencies: org.asciidoctor
      ...
    2. Or, configure the dependency into the pom.xml with the Maven JAR/WAR plugin.

      pom.xml file example with Maven WAR plugin configuration to add a dependency
      ...
      <dependencies>
        <dependency>
          <groupId>org.asciidoctor</groupId>
          <artifactId>asciidoctorj</artifactId>
          <version>1.5.4</version>
          <scope>provided</scope> (1)
          ...
        </dependency>
      </dependencies>
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-war-plugin</artifactId>
          <version>2.4</version>
          <configuration>
            <archive>
              <manifestEntries>
                <Dependencies>org.asciidoctor</Dependencies> (2)
              </manifestEntries>
            </archive>
          </configuration>
      </plugin>
      ...
      1 The AsciidoctorJ dependency and the transitives dependencies don’t need to be added to the final WAR since all JARs are available through the module.
      2 The module dependency will be added to the MANIFEST.MF file.

Using a pre-release version

Pre-release versions of AsciidoctorJ are published to Bintray. You can find them in https://bintray.com/asciidoctor/maven/asciidoctorj/view. Final releases are released to both Maven Central and Bintray.

Here’s how to use a pre-release version in Maven:

<repositories>
  <repository>
    <id>central</id>
    <name>bintray</name>
    <url>http://dl.bintray.com/asciidoctor/maven</url>
    <snapshots>
      <enabled>false</enabled>
    </snapshots>
  </repository>
</repositories>

Resources

The source code for AsciidoctorJ, including the latest developments and issues, can be found in the project’s repository on GitHub. If you identify an issue while using AsciidoctorJ, please don’t hesitate to file a bug report. Also, don’t forget to join the Asciidoctor discussion list, where you can ask questions and leave comments.