Skip to content

Latest commit

 

History

History
2400 lines (1873 loc) · 98.2 KB

integrator-guide.adoc

File metadata and controls

2400 lines (1873 loc) · 98.2 KB

AsciidoctorJ: Integrator’s guide

This guide explains how you can embed Asciidoctor into your own Java programs and in how you can extend it so that Asciidoctor matches all your needs for a publishing chain. This guide assumes that you have a basic knowledge about the Asciidoctor format.

Introduction

Asciidoctor is an implementation of the AsciiDoc format in Ruby. Thanks to the JRuby, an implementation of the Ruby runtime in Java, Asciidoctor can also be executed on a JVM. AsciidoctorJ bundles all gems that are required for executing Asciidoctor and wraps it into a Java API so that Asciidoctor can be used in Java like any other Java library. Additionally there is a distribution that you can simply download, unzip and execute without worrying about installing the right Ruby runtime, installing gems etc.

This guide will not go into details of the distribution. Instead you will learn how you can embed and leverage Asciidoctor by embedding it into your own code.

The following sections will show:

  • how to render AsciiDoc content to HTML via AsciidoctorJ

  • how you can extend AsciidoctorJ to extend the AsciiDoc format and modify the way documents are rendered

  • how you can write a converter for your own custom target format

  • how you can capture Asciidoctor messages to monitor the rendering process

Integrate AsciidoctorJ: Render documents via the API

This section shows you how you can use AsciidoctorJ to render AsciiDoc documents to HTML from within your own code. An introductory example shows the first steps necessary. The rest of this section will show what options you can use to influence the way Asciidoctor renders your documents.

First steps: Rendering a document

The very first step to integrate AsciidoctorJ is to add the required dependencies to your project. Depending on your build system you have to add a dependency on the artifact asciidoctorj with the group id org.asciidoctor to your build file. The following snippets show what you have to add in case you use Maven, Gradle, Ivy or SBT.

Declaring the dependency in a Maven build file (i.e., pom.xml)
<dependencies>
  <dependency>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctorj</artifactId>
    <version>2.2.0</version> <!--(1)-->
  </dependency>
</dependencies>
Declaring the dependency in a Gradle build file (e.g., build.gradle)
dependencies {
  compile 'org.asciidoctor:asciidoctorj:2.2.0' // (1)
}
Declaring the dependency in an Ivy dependency descriptor (e.g., ivy.xml)
<dependency org="org.asciidoctor" name="asciidoctorj" rev="2.2.0" /> <!--(1)-->
Declaring the dependency in an SBT build file (e.g., build.sbt)
libraryDependencies += "org.asciidoctor" % "asciidoctorj" % "2.2.0" // (1)
  1. Specifying the version of AsciidoctorJ implicitly selects the version of Asciidoctor

The dependency on AsciidoctorJ will transitively add a dependency on the module jruby-complete with the group id org.jruby.

The following Java program shows how to convert an arbitrary AsciiDoc file to an HTML file. AsciidoctorJ will already fully embedded in your Java program and it looks like any other Java library, so there is no need to fear Ruby. If you execute the following Java program and have an AsciiDoc file document.adoc in your current working directory you should see the rendered result document.html afterwards next to your original document.

Converting an AsciiDoc file to an HTML file
package org.asciidoctor.integrationguide;

import java.io.File;

import org.asciidoctor.Asciidoctor;
import org.asciidoctor.OptionsBuilder;
import org.asciidoctor.SafeMode;

public class SimpleAsciidoctorRendering {

    public static void main(String[] args) {
        Asciidoctor asciidoctor = Asciidoctor.Factory.create(); // (1)
        asciidoctor.convertFile(                                // (2)
                new File(args[0]),
                OptionsBuilder.options()                        // (3)
                        .toFile(true)
                        .safe(SafeMode.UNSAFE));
    }
}
  1. The static method Asciidoctor.Factory.create() creates a new Asciidoctor instance. This is the door to all interactions with Asciidoctor.

  2. The method convertFile takes a File and conversion options. Depending on the options it will create a new file or return the rendered content. In this case a new file is created and the method returns null.

  3. The conversion options define via toFile(true) that the result should be written to a new file. The option safe imposes security constraints on the rendering process. safe(SafeMode.UNSAFE) defines the least restricting constraints and allows for example inserting the beautiful asciidoctor.css stylesheet into the resulting document.

The Asciidoctor interface

The main entry point for AsciidoctorJ is the Asciidoctor Java interface. You obtain an instance from the factory class Asciidoctor.Factory that provides a simple create method.

If you need to get an instance of Asciidoctor using a certain gem path the factory org.asciidoctor.jruby.AsciidoctorJRuby.Factory provides multiple create methods to get an instance with a certain gem or load path:

Table 1. Methods to create an Asciidoctor instance
Method Name Description

org.asciidoctor.Asciidoctor.Factory
.create()

The default create method that is used most often. Only use other methods if you want to load extensions that are not on the classpath.

org.asciidoctor.jruby.AsciidoctorJRuby.Factory
.create(String gemPath)

Creates a new Asciidoctor instance and sets the global variable GEM_PATH to the given String. The variable GEM_PATH defines where Ruby looks for installed gems.

org.asciidoctor.jruby.AsciidoctorJRuby.Factory
.create(List<String> loadPaths)

Creates a new Asciidoctor instance and set the global variable LOAD_PATH to the given Strings. The variable LOAD_PATH defines where Ruby looks for files.

So most of the time you simply get an Asciidoctor instance like this:

Default way to create an Asciidoctor instance
Asciidoctor asciidoctor = Asciidoctor.Factory.create();

As Asciidoctor instances can be created they can also be explicitly destroyed to free resources used in particular by the Ruby runtime associated with it. Therefore the Asciidoctor interface offers the method shutdown. After calling this method every other method call on the instance will fail!

Destroying an Asciidoctor instance
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
asciidoctor.shutdown();

The Asciidoctor interface also implements the interface java.io.AutoCloseable which also shuts down the Ruby runtime. Therefore the previous example is equivalent to this:

Automatically destroying an Asciidoctor instance
try (Asciidoctor asciidoctor = Asciidoctor.Factory.create()) {
    asciidoctor.convert("Hello World", OptionsBuilder.options());
}

To convert AsciiDoc documents the Asciidoctor interface provides four methods:

  • convert

  • convertFile

  • convertFiles

  • convertDirectory

Important
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 2. 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.

Here’s an example of using AsciidoctorJ to convert an AsciiDoc string.

Note
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. You will learn more about the conversion options in Conversion options 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
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
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
String[] result = asciidoctor.convertFiles(
    Arrays.asList(new File("sample.adoc")),
    new HashMap<String, Object>());

for (String html : result) {
    System.out.println(html);
}
Warning
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 3. 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
String[] result = asciidoctor.convertDirectory(
    new AsciiDocDirectoryWalker("src/asciidoc"),
    new HashMap<String, Object>());

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

Conversion options

Asciidoctor provides many options that can be passed when converting content. This section explains these options as they might be important when converting Asciidoctor content yourself.

The options for conversion of a document are held in an instance of the class org.asciidoctor.Options. A builder allows for simple configuration of that instance that can be passed to the respective method of the Asciidoctor interface. The following example shows how to set the options so that the resulting HTML document is rendered for embedding it into another document. That means that the result only contains the content of a HTML body element:

Example for converting to an embeddable document
        String result =
                asciidoctor.convert(
                        "Hello World",
                        OptionsBuilder.options()     // (1)
                                .headerFooter(false) // (2)
                                .get());             // (3)

        assertThat(result, startsWith("<div "));
  1. Create a new OptionsBuilder that is used to prepare the options with a fluent API.

  2. Set the option header_footer to false, meaning that an embeddable document will be rendered,

  3. Get the built Options instance and pass it to the conversion method.

The most important options are explained below.

toFile

Via the option toFile it is possible to define if a document should be written to a file at all and to which file.

To make the API return the converted document and not write to a file set OptionsBuilder.toFile(false).

To make Asciidoctor write to the default file set OptionsBuilder.toFile(true). The default file is computed by taking the base name of the input file and adding the default suffix for the target format like .html or .pdf. That is for the input file test.adoc the resulting file would be in the same directory with the name test.html.
This is also the way the CLI behaves.

To write to a certain file set OptionsBuilder.toFile(targetFile). This is also necessary if you want to convert string content to files.

The following example shows how to convert content to a dedicated file:

Example for converting to a dedicated file
        File targetFile = //...
        asciidoctor.convert(
                "Hello World",
                OptionsBuilder.options()
                        .toFile(targetFile)    // (1)
                        .safe(SafeMode.UNSAFE) // (2)
                        .get());

        assertTrue(targetFile.exists());
        assertThat(
                IOUtils.toString(new FileReader(targetFile)),
                containsString("<p>Hello World"));
  1. Set the option toFile so that the result will be written to the file pointed to by targetFile.

  2. Set the safe mode to UNSAFE so that files can be written. See safe for a description of this option.

safe

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. All safe modes are defined by the enum org.asciidoctor.SafeMode. The safe modes in order from most insecure to most secure are:

UNSAFE

A safe mode level that disables any security features enforced by Asciidoctor.

This is the default safe mode for the CLI.

SAFE

This safe mode level prevents access to files which reside outside of the parent directory of the source file. It disables all macros, except the include directive. The paths to include files must be within the parent directory. It allows assets to be embedded in the document.

SERVER

A safe mode level that disallows the document from setting attributes that would affect the rendering of the document. This level trims the attribute docfile to its relative path and prevents the document from:

  • setting source-highlighter, doctype, docinfo and backend

  • seeing docdir

It allows icons and linkcss.

SECURE

A safe mode level that disallows the document from attempting to read files from the file system and including their contents into the document. Additionally, it:

  • disables icons

  • disables the include directive

  • data can not be retrieved from URIs

  • prevents access to stylesheets and JavaScripts

  • sets the backend to html5

  • disables docinfo files

  • disables data-uri

  • disables docdir and docfile

  • disables source highlighting

Asciidoctor extensions may still embed content into the document depending whether they honor the safe mode setting.

This is the default safe mode for the API.

So if you want to render documents in the same way as the CLI does you have to set the safe mode to Unsafe. Without it you will for example not get the stylesheet embedded into the resulting document.

Convert a document in unsafe mode
        File sourceFile =
            new File("includingcontent.adoc");
        String result = asciidoctor.convertFile(
                sourceFile,
                OptionsBuilder.options()
                        .safe(SafeMode.UNSAFE) // (1)
                        .toFile(false)         // (2)
                        .get());

        assertThat(result, containsString("This is included content"));
  1. Sets the safe mode from SECURE to UNSAFE.

  2. Don’t convert the file to another file but to a string so that we can easier verify the contents.

The example above will succeed with these two asciidoc files:

includingcontent.adoc
  = Including content

  include::includedcontent.adoc[]
includedcontent.adoc
  This is included content

backend

This option defines the target format for which the document should be converted. Among the possible values are pdf or docbook.

Render a document to PDF
File targetFile = // ...
asciidoctor.convert(
        "Hello World",
        OptionsBuilder.options()
                .backend("pdf")
                .toFile(targetFile)
                .safe(SafeMode.UNSAFE)
                .get());

assertThat(targetFile.length(), greaterThan(0L));

attributes

This option allows to define document attributes externally. Attributes are defined just like options, but using the AttributesBuilder to build instance of it. For many attributes used by Asciidoctor there are predefined methods. The method AttributesBuilder.attribute(key, value) allows for defining arbitrary attributes.

To enable the use of font-awesome icons the attribute icons has to be set to the value font in the document. From the API this is done like this:

Enable use of font-awesome icons
String result =
    asciidoctor.convert(
        "NOTE: Asciidoctor supports font-based admonition icons!\n" +
            "\n" +
            "{foo}",
        OptionsBuilder.options()
                .toFile(false)
                .headerFooter(false)
                .attributes(
                        AttributesBuilder.attributes()        // (1)
                                .icons(Attributes.FONT_ICONS) // (2)
                                .attribute("foo", "bar")      // (3)
                                .get())
                .get());
assertThat(result, containsString("<i class=\"fa icon-note\" title=\"Note\"></i>"));
assertThat(result, containsString("<p>bar</p>"));
  1. Create a builder for attributes and pass the resulting Attributes instance to the options.

  2. Define the attribute supported by Asciidoctor to use the font awesome icons.

  3. Define the custom attribute foo to the value bar.

Ruby runtime

Asciidoctor itself is implemented in Ruby and AsciidoctorJ is a wrapper that encapsulates Asciidoctor in a JRuby runtime. Even though AsciidoctorJ tries to hide as much as possible there are some points that you have to know and consider when using AsciidoctorJ.

Every Asciidoctor instance uses and initializes its own Ruby runtime. As booting a Ruby runtime takes a considerable amount of time it is wise to either use a single instance or pool multiple instances in case your program wants to render multiple documents instead of creating one Asciidoctor instance per conversion. Asciidoctor itself is threadsafe, so from this point of view there is no issue in starting only one instance.

The JRuby runtime can be configured in numerous ways to change the behavior as well as the performance. As the performance requirements vary between a program that only render a single document and quit and server application that run for a long time you should consider modifying these options for your own use case. AsciidoctorJ itself does not make any configurations so that you can modify like you think. A full overview of the options is available at https://github.com/jruby/jruby/wiki/ConfiguringJRuby.

To change the configuration of the JRuby instance you have to set the corresponding options as system properties before creating the Asciidoctor instance.

So to create an Asciidoctor instance for single use that does not try to JIT compile the Ruby code the option compile.mode should be set to OFF. That means that you have to set the system property jruby.compile.mode to OFF:

Create an Asciidoctor instance for single use
System.setProperty("jruby.compile.mode", "OFF");
Asciidoctor asciidoctor = Asciidoctor.Factory.create();

The default for this value is JIT which is already a reasonable value for multiple uses of the Asciidoctor instance.

In case you want to have direct access to the Ruby runtime instance that is used by a certain Asciidoctor instance you can use the class JRubyRuntimeContext to obtain the org.jruby.Ruby instance:

Obtaining the Ruby instance associated with an Asciidoctor instance
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
Ruby ruby = JRubyRuntimeContext.get(asciidoctor);

Extend AsciidoctorJ: Write your own extensions

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 4. AsciidoctorJ extension APIs
Name Class Description

Include processor

org.asciidoctor.extension.IncludeProcessor

intercepts include::[] lines

Preprocessor

org.asciidoctor.extension.Preprocessor

Allows you to modify the asciidoc text before parsing

Block macro processor

org.asciidoctor.extension.BlockMacroProcessor

Processes block macros like bibliography::[]

Block processor

org.asciidoctor.extension.BlockProcessor

Processes an arbitary block based on it’s style such as

[prohibited]
--
Do not enter
--

Treeprocessor

org.asciidoctor.extension.Treeprocessor

Modify the AST after parsing.

Inline macro processor

org.asciidoctor.extension.InlineMacroProcessor

Processes inline macros like btn:[].

Postprocessor

org.asciidoctor.extension.Postprocessor

Modifies the backend-specific output document.

DocinfoProcessor

org.asciidoctor.extension.DocinfoProcessor

Insert content into the header element or the end of the body element (html), or the info element or at the end of the document (docbook).

Note
Order of execution

The extension types are called during the conversion process in the order shown in the table. Within each type:

  • Include processors are called in an arbitrary and changeable order. The first processor to accept the include line is the only one that is used.

  • Block macro, inline macro, and block processors are called in the order that they appear in the document.

  • Prepocessors, Treeprocessors, Postprocessors and DocinfoProcessors are called in an arbitrary and changeable order.

To create an extension two things are required:

  1. Create a class extending one of the extension classes from above

  2. Register your class using the JavaExtensionRegistry class

But before starting to write your first extension it is essential to understand how Asciidoctor treats the document: The raw text content is parsed into a tree structure which is then transformed into the target format. Therefore this section first goes into the details of this tree structure before explaining what extensions are possible and how to implement them.

Understanding the AST classes

To write extensions or converters for AsciidoctorJ understanding the Abstract Syntax Tree (AST) classes is key. The AST classes are the intermediate representation of the document that Asciidoctor creates before rendering to the target format.

The following example document demonstrates how an AST will look like to give you an idea how the document and the AST are connected.

Example document for the AST
= Test document
Foo Bar <foo@bar.com>

This document demonstrates the AST of an Asciidoctor document

== The first section

A section has some nice paragraphs and maybe lists:

=== A subsection

- One
- Two
- Three

Or even tables

|===
| Key | Value
|===

and sources as well

[source,ruby]
----
puts 'Hello, World!'
----

The following image shows the AST and some selected members of the node objects. The indentation of a line visualizes the nesting of the nodes like a tree.

AST for the example document
Document             context: document
  Block              context: preamble
    Block            context: paragraph
                    This document demon...
  Section            context: section    level: 1
    Block            context: paragraph
                    A section has some ...
    Section          context: section    level: 2
      List           context: ulist
        ListItem     context: list_item
                    One
        ListItem     context: list_item
                    Two
        ListItem     context: list_item
                    Three
      Block          context: paragraph
                    Or even tables
      Table          context: table      style: table
      Block          context: paragraph
                    and sources as well
      Block          context: listing    style: source
                    puts 'Hello, World!'

The AST is built from the following types:

org.asciidoctor.ast.Document

This is always the root of the document. It owns the blocks and sections that make up the document and holds the document attributes.

org.asciidoctor.ast.Section

This class model sections in the document. The member level indicates the nesting level of this section, that is if level is 1 the section is a section, with level 2 it is a subsection etc.

org.asciidoctor.ast.Block

Blocks are content in a section, like paragraphs, source listings, images, etc. The concrete form of the block is available in the field context. Among the possible values are:

  1. paragraph

  2. listing

  3. literal

  4. open

  5. example

  6. pass

org.asciidoctor.ast.List

The list node is the container for ordered and unordered lists. The type of list is available in the field context, with the content ulist for unordered lists, olist for ordered lists.

org.asciidoctor.ast.ListItem

A list item represents a single item of a list.

org.asciidoctor.ast.DescriptionList

The description list node is the container for description lists. The context of the node is dlist.

org.asciidoctor.ast.DescriptionListEntry

A list entry represents a single item of a description list. It has multiple terms that are again instances of org.asciidoctor.ast.ListItem and a description that is also an instance of org.asciidoctor.ast.ListItem.

org.asciidoctor.ast.Table

This represents a table and is probably the most complex node type. It owns a list of columns and lists of header, body and footer rows.

org.asciidoctor.ast.Column

A column defines the style for the column of a table, the width and alignments.

org.asciidoctor.ast.Row

A row in a table is only a simple owner of a list of table cells.

org.asciidoctor.ast.Cell

A cell in a table holds the cell content and formatting attributes like colspan, rowspan and alignment as appropriate. A special case are cells that have the asciidoctor style. These do not contain simple text content, but have another full Document in their member innerDocument.

org.asciidoctor.ast.PhraseNode

This type is a special case. It does not appear in the AST itself as Asciidoctor does not really parse into the block itself. Phrase nodes are usually created by inline macro extensions that process macros like issue:1234[] and create links from them.

Nodes are in general only created from within extensions. Therefore the abstract base class of all extensions, org.asciidoctor.extension.Processor, has factory methods for every node type.

Now that you have learned about the AST structure you can go into the details of the extensions.

Block Macro Processors

A block macro is a block having a content like this: gist::mygithubaccount/8810011364687d7bec2c[]. During the rendering process of the document Asciidoctor invokes a BlockMacroProcessor that has to create a block computed from this macro.

The structure is always like this:

  1. Macro name, e.g. gist

  2. Two colons ::

  3. A target, mygithubaccount/8810011364687d7bec2c

  4. Attributes, that are empty in this case, []

Our example block macro should embed the GitHub gist that would be available at the URL https://gist.github.com/mygithubaccount/8810011364687d7bec2c.

The following block macro processor replaces such a macro with the <script> element that you can also pick from https://gist.github.com for a certain gist.

A BlockMacroProcessor that replaces gist block macros
import org.asciidoctor.ast.StructuralNode;
import org.asciidoctor.extension.BlockMacroProcessor;
import org.asciidoctor.extension.Name;

import java.util.Map;

@Name("gist")                                                          // (1)
public class GistBlockMacroProcessor extends BlockMacroProcessor {     // (2)

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

        String content = new StringBuilder()
            .append("<div class=\"openblock gist\">")
            .append("<div class=\"content\">")
            .append("<script src=\"https://gist.github.com/")
                .append(target)                                        // (4)
                .append(".js\"></script>")
            .append("</div>")
            .append("</div>").toString();

        return createBlock(parent, "pass", content);                   // (5)
    }

}
  1. The @Name annotation defines the macro name this BlockMacroProcessor should be called for. In this case this instance will be called for all block macros that have the name gist.

  2. All BlockMacroProcessors must extend the class org.asciidoctor.extension.BlockMacroProcessor.

  3. A BlockMacroProcessor must implement the abstract method process that is called by Asciidoctor. The method must return a new block that is used be Asciidoctor instead of the block containing the block macro.

  4. The implementation constructs the HTML content that should go into the final HTML document. That means that the content has to be directly passed through into the result. Having said that this example does not work when generating PDF content.

  5. The processor creates a new block via the inherited method createBlock(). The parent of the new block, a context and the content must be passed. As we want to pass through the content directly into the result the context must be pass and the content is the computed HTML string.

Note
There are many more methods available to create any type of AST node.

Now we want to make this block macro processor work on the block macro in our document:

gist-macro.adoc
= Gist test

gist::myaccount/1234abcd[]

To make AsciidoctorJ use our processor it has to be registered at the JavaExtensionRegistry:

Register and execute a BlockMacroProcessor
File gistmacro_adoc = //...
asciidoctor.javaExtensionRegistry().blockMacro(GistBlockMacroProcessor.class);      // (1)

String result = asciidoctor.convertFile(gistmacro_adoc, OptionsBuilder.options().toFile(false));

assertThat(
        result,
        containsString(
                "<script src=\"https://gist.github.com/myaccount/1234abcd.js\">")); // (2)
  1. The block macro processor is registered at the JavaExtensionRegistry of the Asciidoctor instance.

  2. Check that the resulting HTML contains the <script> element that you also get from the https://gist.github.com when you get the HTML snippet to embed a gist.

Inline Macro Processors

An inline macro is very similar to a block macro. But instead of being replaced by a block created by a BlockMacroProcessor it is replaced by a phrase node that is simply a part of a block, e.g. in the middle of a sentence. An example for an inline macro is issue:333[repo=asciidoctor/asciidoctorj].

The structure is always like this:

  1. Macro name, e.g. issue

  2. One colon, i.e. :. This is what distinguishes it from a block macro even if it is alone in a paragraph.

  3. An optional target, e.g. 333

  4. Optional attributes, e.g. [repo=asciidoctor/asciidoctorj].

Our example inline macro processor should create a link to the issue #333 of the repository asciidoctor/asciidoctorj on GitHub. If the attribute repo in the macro is empty it should fall back to the document attribute repo.

So for the following document our inline macro processor should create links to the issue #333 of the repository asciidoctor/asciidoctorj and to the issue #2 for the repository asciidoctor/asciidoctorj-groovy-dsl.

issue-inline-macro.adoc
= InlineMacroProcessor Test Document
:repo: asciidoctor/asciidoctorj-groovy-dsl

You might want to take a look at the issue issue:333[repo=asciidoctor/asciidoctorj] and issue:2[].

The InlineMacroProcessor for these macros looks like this:

An InlineMacroProcessor that replaces issue macros with links
import org.asciidoctor.ast.ContentNode;
import org.asciidoctor.extension.InlineMacroProcessor;
import org.asciidoctor.extension.Name;

import java.util.HashMap;
import java.util.Map;

@Name("issue")                                                           // (1)
public class IssueInlineMacroProcessor extends InlineMacroProcessor {    // (2)

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

        String href =
                new StringBuilder()
                    .append("https://github.com/")
                    .append(attributes.containsKey("repo") ?
                            attributes.get("repo") :
                            parent.getDocument().getAttribute("repo"))
                    .append("/issues/")
                    .append(target).toString();

        Map<String, Object> options = new HashMap<>();
        options.put("type", ":link");
        options.put("target", href);
        return createPhraseNode(parent, "anchor", target, attributes, options); // (4)
    }

}
  1. The @Name annotation defines the macro name this InlineMacroProcessor should be called for. In this case this instance will be called for all inline macros that have the name issue.

  2. All InlineMacroProcessors must extend the class org.asciidoctor.extension.InlineMacroProcessor.

  3. A InlineMacroProcessor must implement the abstract method process that is called by Asciidoctor. The method must return the rendered result of this macro.

  4. The implementation constructs and returns a new phrase node that is a link, i.e. an anchor via the method createPhraseNode(). The third parameter target defines that the text to render this link is the target of the macro, that means that the link will be rendered as 333 or 2. The last parameter, the options, must contain the target of the line, i.e. the referenced URL, and that the type of the anchor is a link. It could also be a ':xref', a ':ref', or a ':bibref'.

To make AsciidoctorJ use our processor it has to be registered at the JavaExtensionRegistry:

Register and execute a InlineMacroProcessor
File issueinlinemacro_adoc = //...
asciidoctor.javaExtensionRegistry().inlineMacro(IssueInlineMacroProcessor.class);       // (1)

String result = asciidoctor.convertFile(issueinlinemacro_adoc, OptionsBuilder.options().toFile(false));

assertThat(
        result,
        containsString(
                "<a href=\"https://github.com/asciidoctor/asciidoctorj/issues/333\"")); // (2)

assertThat(
        result,
        containsString(                                                                 // (2)
                "<a href=\"https://github.com/asciidoctor/asciidoctorj-groovy-dsl/issues/2\""));
  1. The inline macro processor is registered at the JavaExtensionRegistry of the Asciidoctor instance.

  2. Check that the resulting HTML contains the two anchor elements.

The example above has shown how to create a link from a macro. But there are several other things that an InlineMacroProcessor can create like icons, inline images etc. Even though the following examples might not make much sense, they show how phrase nodes have to be created for the different use cases.

To create keyboard icons like Ctrl+T which can be created directly in Asciidoctor via kbd:[Ctrl+T] you create the PhraseNode as shown below. The example assumes that the macro is called with the macro name ctrl and a key as the target, e.g. \ctrl:S[], and creates Ctrl+S from it.

Create a phrase node for keys
@Name("ctrl")
public class KeyboardInlineMacroProcessor extends InlineMacroProcessor {

    @Override
    public Object process(ContentNode parent, String target, Map<String, Object> attributes) {
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("keys", Arrays.asList("Ctrl", target));             // (1)
        return createPhraseNode(parent, "kbd", (String) null, attrs); // (2)
    }
}
  1. The attributes of the PhraseNode must contain the keys to be shown as a list for the attribute key keys.

  2. Create a PhraseNode with context kbd and no text and return it.

To create a menu selection as described at http://asciidoctor.org/docs/user-manual/#menu-selections a processor would create a PhraseNode with the menu context. The following processor would render the macro rightclick:New|Class[] like this: New  Class.

Create a phrase node for menu selections.
@Name("rightclick")
public class ContextMenuInlineMacroProcessor extends InlineMacroProcessor {

    @Override
    public Object process(ContentNode parent, String target, Map<String, Object> attributes) {
        String[] items = target.split("\\|");
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("menu", "Right click");                              // (1)
        List<String> submenus = new ArrayList<String>();
        for (int i = 0; i < items.length - 1; i++) {
            submenus.add(items[i]);
        }
        attrs.put("submenus", submenus);
        attrs.put("menuitem", items[items.length - 1]);

        return createPhraseNode(parent, "menu", (String) null, attrs); // (2)
    }
}
  1. The attributes of the PhraseNode must contain the key menu referring to the first menu selection, submenus referring to a possibly empty list of submenu selections, and finally the key menuitem referring to the final menu item selection.

  2. Create and return an PhraseNode with context menu and no text.

To create an inline image the PhraseNode must have the context image. The following example assumes that there is a site http://foo.bar that serves images given as the target of the macro. That means the MacroProcessor should replace the macro foo:1234 to an image element that refers to http://foo.bar/1234.

Create a PhraseNode for inline image.
@Name("foo")
public class ImageInlineMacroProcessor extends InlineMacroProcessor {

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

        Map<String, Object> options = new HashMap<String, Object>();
        options.put("type", "image");                                            // (1)
        options.put("target", "http://foo.bar/" + target);                       // (2)

        String[] items = target.split("\\|");
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("alt", "Image not available");                                 // (3)
        attrs.put("width", "64");
        attrs.put("height", "64");

        return createPhraseNode(parent, "image", (String) null, attrs, options); // (4)
    }
}
  1. For an inline image the option type must have the value image.

  2. The URL of the image must be set via the option target.

  3. Optional attributes alt for alternative text, width and height are set in the node attributes. Other possible attributes include title to define the title attribute of the img element when rendering to HTML. When setting the attribute link to any value the node will be converted to a link to that image, where the window can be defined via the attribute window.

  4. Create and return a PhraseNode with context image and no text.

We said at the start of this section that the target (the x in menu:x[]) is optional. If you want a macro that does not have a target (for example cite:[brown79]) add the following annotation to your class:

@Name("cite")
@Format(SHORT)
class CiteInlineMacroProcessor extends InlineMacroProcessor {
  ...
}

With the SHORT format, the attributes are not parsed, and the 'target' that is passed to your macro is the value between the brackets ("brown79").

Block Processors

A block processor is very similar to a block macro processor. But in contrast to a block macro a block processor is called for a block having a certain name instead of a macro invocation. Therefore block processors rather transform blocks instead of creating them as block macro processors do.

The following example shows a block processor that converts the whole text of a block to upper case if it has the name yell. That means that our block processor will convert blocks like this:

yell-block.adoc
[yell]
I really mean it

After the processing this block will look like this

I REALLY MEAN IT

The BlockProcessor looks like this:

A BlockProcessor that transforms the content of a block to upper case
@Name("yell")                                              // (1)
@Contexts({Contexts.PARAGRAPH})                            // (2)
@ContentModel(ContentModel.SIMPLE)                         // (3)
public class YellBlockProcessor extends BlockProcessor {   // (4)

    @Override
    public Object process(                                 // (5)
            StructuralNode parent, Reader reader, Map<String, Object> attributes) {

        String content = reader.read();
        String yellContent = content.toUpperCase();

        return createBlock(parent, "paragraph", yellContent, attributes);
    }

}
  1. The annotation @Name defines the block name that this block processor handles.

  2. The annotation @Contexts defines the block types that this block processor handles like paragraphs, listing blocks, or open blocks. Constants for all contexts are also defined in this annotation. Note that this annotation takes a list of block types, so that a block processor can process paragraph blocks as well as example blocks with the same block name.

  3. The annotation @ContentModel defines what this processor produces. Constants for all contexts are also defined for the annotation class. In this case the block processor creates a simple paragraph, therefore the content model ContentModel.SIMPLE is defined.

  4. All block processors must extend org.asciidoctor.extension.BlockProcessor.

  5. A block processor must implement the method process(). Here the implementation gets the raw block content from the reader, transforms it and creates and returns a new block that contains the transformed content.

To make AsciidoctorJ use our processor it also has to be registered at the JavaExtensionRegistry:

File yellblock_adoc = //...

asciidoctor.javaExtensionRegistry().block(YellBlockProcessor.class); // (1)

String result = asciidoctor.convertFile(yellblock_adoc, OptionsBuilder.options().toFile(false));

assertThat(result, containsString("I REALLY MEAN IT"));              // (2)
  1. The block processor is registered at the JavaExtensionRegistry of the Asciidoctor instance.

  2. Check that the resulting HTML contains the text as upper-case letters.

Include Processors

Asciidoctor supports include other documents via the include directive: You can simply write include::other.adoc[] to include the contents of the file other.adoc. Include Processors allow to intercept this mechanism and for instance include the content over the network. For example an Include Processor could resolve the include directive include::ls[] could insert the contents of the current directory.

Our example will replace the include directive include::ls[] with the directory contents of the current directory, one line for every file. That is the document below will render a listing with the directory contents:

ls-include.adoc
----
include::ls[]
----

The processor could look like this:

LsIncludeProcessor.java
import org.asciidoctor.ast.Document;
import org.asciidoctor.extension.IncludeProcessor;
import org.asciidoctor.extension.PreprocessorReader;

import java.io.File;
import java.util.Map;

public class LsIncludeProcessor extends IncludeProcessor {    // (1)

    @Override
    public boolean handles(String target) {                   // (2)
        return "ls".equals(target);
    }

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

        StringBuilder sb = new StringBuilder();

        for (File f: new File(".").listFiles()) {
            sb.append(f.getName()).append("\n");
        }

        reader.push_include(                                  // (4)
                sb.toString(),
                target,
                new File(".").getAbsolutePath(),
                1,
                attributes);
    }
}
  1. Every Include Processor must extend the class org.asciidoctor.extension.IncludeProcessor.

  2. Asciidoctor calls the method handles() with the target for every include directive it finds. The method must return true if it feels responsible for this directive. In our case it returns true if the target is ls.

  3. The implementation of the method process() lists the directory contents of the current directory and creates a string with one line per file.

  4. Finally the call to the method push_include inserts the contents. The second and third parameters contain the 'file name' of the include content. In our example this will be basically the name ls and the path of the current directory. The parameter 1 is the line number of the first line of the included content. This makes the most sense when partial content is included.

To make AsciidoctorJ use our processor it also has to be registered at the JavaExtensionRegistry:

File lsinclude_adoc = //...

String firstFileName = new File(".").listFiles()[0].getName();

asciidoctor.javaExtensionRegistry().includeProcessor(LsIncludeProcessor.class);       // (1)

String result = asciidoctor.convertFile(lsinclude_adoc, OptionsBuilder.options().toFile(false));

assertThat(
        result,
        containsString(firstFileName));
  1. The Include Processor is registered at the JavaExtensionRegistry of the Asciidoctor instance.

Preprocessors

Preprocessors allow to process the raw asciidoctor sources before Asciidoctor parses and converts them. A preprocessor could for example make comments visible that should be rendered in drafts.

Our example preprocessor does exactly that and will render the comment in the following document as a note.

comment.adoc
Normal content.

////
RP: This is a comment and should only appear in draft documents
////

The preprocessor will render the document as if it looked like this:

comment-with-note.adoc
Normal content.

[NOTE]
--
RP: This is a comment and should only appear in draft documents
--

The implementation of the preprocessor simply gets the AST node for the document to be created as well as a PreprocessorReader. A PreprocessorReader gives access to the raw input line by line allowing to fetch and restore content. And this is exactly what our Preprocessor does: it fetches the raw content, modifies it and stores it back so that Asciidoctor will only see our modified content.

A Preprocessor that renders comments as notes
import org.asciidoctor.ast.Document;
import org.asciidoctor.extension.Preprocessor;
import org.asciidoctor.extension.PreprocessorReader;

import java.util.ArrayList;
import java.util.List;

public class CommentPreprocessor extends Preprocessor {   // (1)

    @Override
    public void process(Document document, PreprocessorReader reader) {

        List<String> lines = reader.readLines();          // (2)
        List<String> newLines = new ArrayList<String>();

        boolean inComment = false;

        for (String line: lines) {                        // (3)
            if (line.trim().equals("////")) {
                if (!inComment) {
                   newLines.add("[NOTE]");
                }
                newLines.add("--");
                inComment = !inComment;
            } else {
                newLines.add(line);
            }
        }

        reader.restoreLines(newLines);                    // (4)
    }
}
  1. All Preprocessors must extend the class org.asciidoctor.extension.Preprocessor and implement the method process().

  2. The implementation gets the whole Asciidoctor source as an array of Strings where each entry corresponds to one line.

  3. Every odd occurrence of a comment start is replaced by opening an admonition block, every even occurrence is closing it. The new content is collected in a new list.

  4. The processed content is restored to the original PreprocessorReader so that it replaces the content that was already consumed at the beginning of the method.

To make AsciidoctorJ use our processor it also has to be registered at the JavaExtensionRegistry:

File comment_adoc = //...
File comment_with_note_adoc = //...
asciidoctor.javaExtensionRegistry().preprocessor(CommentPreprocessor.class);      // (1)

String result1 = asciidoctor.convertFile(comment_adoc, OptionsBuilder.options().toFile(false));
String result2 = asciidoctor.convertFile(comment_with_note_adoc, OptionsBuilder.options().toFile(false));

assertThat(result1, is(result2)); // (2)
  1. The preprocessor is registered at the JavaExtensionRegistry of the Asciidoctor instance.

  2. Check that the resulting HTML is the same as if a document with an admonition block would have been rendered.

There may be multiple Preprocessors registered and every Preprocessor will be called. But the order in which the Preprocessors are called is undefined so that all Preprocessors should be independent of each other.

Postprocessors

Postprocessors are called when Asciidoctor has converted the document to its target format and have the chance to modify the result. A Postprocessor could for example insert a custom copyright notice into the footer element of the resulting HTML document.

Note
Postprocessors in AsciidoctorJ currently only supports String based target formats. That means it is not possible at the moment to write Postprocessors for binary formats like PDF or EPUB.

A Postprocessor that adds a copyright notice would look like this:

A Postprocessor that inserts a copyright notice in the footer element
import org.asciidoctor.ast.Document;
import org.asciidoctor.extension.Postprocessor;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;

public class CopyrightFooterPostprocessor extends Postprocessor {    // (1)

    static final String COPYRIGHT_NOTICE = "Copyright Acme, Inc.";

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

        org.jsoup.nodes.Document doc = Jsoup.parse(output, "UTF-8"); // (2)

        Element contentElement = doc.getElementById("footer-text");  // (3)
        if (contentElement != null) {
            contentElement.text(contentElement.ownText() + " | " + COPYRIGHT_NOTICE);
        }
        output = doc.html();                                         // (4)

        return output;
    }
}
  1. All Preprocessors must extend the class org.asciidoctor.extension.Postprocessor and implement the method process().

  2. The processor parses the resulting HTML text using the Jsoup library. This returns the document as a data structure.

  3. Find the element with the ID footer-text. This element contains the footer text, which usually contains the document generation timestamp. If this element is available its text is modified by appending the copyright notice.

  4. Finally convert the modified document back to the HTML string and let the processor return it.

To make AsciidoctorJ use our processor it also has to be registered at the JavaExtensionRegistry:

File doc = //...
asciidoctor.javaExtensionRegistry().postprocessor(CopyrightFooterPostprocessor.class); // (1)

String result =
        asciidoctor.convertFile(doc,
                OptionsBuilder.options()
                        .headerFooter(true)                                            // (2)
                        .toFile(false));

assertThat(result, containsString(CopyrightFooterPostprocessor.COPYRIGHT_NOTICE));
  1. The postprocessor is registered at the JavaExtensionRegistry of the Asciidoctor instance.

  2. To make Asciidoctor generate the footer element the option headerFooter must be activated.

Treeprocessors

A Treeprocessor gets the whole AST and may do whatever it likes with the document tree. Examples for Treeprocessors could insert blocks, add roles to nodes with a certain content, etc.

Treeprocessors are called by Asciidoctor at the end of the loading process after Preprocessors, Block processors, Macro processors and Include processors but before Postprocessors that are called after the conversion process.

Our example Treeprocessor will recognize paragraphs that contain terminal scripts like below and make listing blocks from them and add the role terminal that can be styled in an own way.

Example AsciiDoc document containing a terminal script
To fetch the content of the URL invoke the following:

$ curl -v http://127.0.0.1:8080
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.41.0
> Host: 127.0.0.1:8080
> Accept: */*
>
< HTTP/1.1 200 OK
...

As the first line of the second block starts with a $ sign the whole block should become a listing block. The result when rendering this document with our Treeprocessor should be the same as if the document looked like this:

To fetch the content of the URL invoke the following:

[.terminal]
----
$ curl -v http://127.0.0.1:8080
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.41.0
> Host: 127.0.0.1:8080
> Accept: */*
>
< HTTP/1.1 200 OK
...
----

Note that a Blockprocessor would not work for this task, as a Blockprocessor requires a block name for which it is called, but in this case the only way to identify this type of blocks is the beginning of the first line.

The Treeprocessor could look like this:

A Treeprocessor that processes terminal scripts.
import org.asciidoctor.ast.Block;
import org.asciidoctor.ast.Document;
import org.asciidoctor.ast.StructuralNode;
import org.asciidoctor.extension.Treeprocessor;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TerminalCommandTreeprocessor extends Treeprocessor {    // (1)

    public TerminalCommandTreeprocessor() {}

    @Override
    public Document process(Document document) {
        processBlock((StructuralNode) document);                     // (2)
        return document;
    }

    private void processBlock(StructuralNode block) {

        List<StructuralNode> blocks = block.getBlocks();

        for (int i = 0; i < blocks.size(); i++) {
            final StructuralNode currentBlock = blocks.get(i);
            if(currentBlock instanceof StructuralNode) {
                if ("paragraph".equals(currentBlock.getContext())) { // (3)
                    List<String> lines = ((Block) currentBlock).getLines();
                    if (lines != null
                            && lines.size() > 0
                            && lines.get(0).startsWith("$")) {
                        blocks.set(i, convertToTerminalListing((Block) currentBlock));
                    }
                } else {
                    // It's not a paragraph, so recursively descend into the child node
                    processBlock(currentBlock);
                }
            }
        }
    }
    public Block convertToTerminalListing(Block originalBlock) {
        Map<Object, Object> options = new HashMap<Object, Object>();
        options.put("subs", ":specialcharacters");

        Block block = createBlock(                                   // (4)
                (StructuralNode) originalBlock.getParent(),
                "listing",
                originalBlock.getLines(),
                originalBlock.getAttributes(),
                options);

        block.addRole("terminal");                                   // (5)
        return block;
    }
}
  1. Every Treeprocessor must extend org.asciidoctor.extension.Treeprocessor and implement the method process(Document).

  2. The implementation basically iterates over the tree and invokes processBlock() for every node.

  3. The method processBlock() checks for every node if it is a paragraph that has a first line beginning with a $. If it encounters such a block it replaces it with the block created in the method convertToTerminalListing(). Otherwise it descends into the AST searching for these blocks.

  4. When creating the new block we reuse the parent of the original block. The context of the new block has to be listing to get a source block. The content can be simply taken from the original block. We add the option 'subs' with the value ':specialcharacters' so that special characters are substituted, i.e. > and < will be replaced with &gt; and &lt; respectively.

  5. Finally we add the role of the node to terminal, which will result in the div containing the listing having the class terminal.

After that we can simply use that Treeprocessor by registering it at the JavaExtensionRegistry.

File src = //...
asciidoctor.javaExtensionRegistry()
        .treeprocessor(TerminalCommandTreeprocessor.class); // (1)
String result = asciidoctor.convertFile(
        src,
        OptionsBuilder.options()
                .headerFooter(false)
                .toFile(false));
  1. The Treeprocessor is registered at the JavaExtensionRegistry of the Asciidoctor instance.

Docinfo Processors

Docinfo Processors are primarily targeted for the HTML and DocBook5 target format. A Docinfo Processor basically allows to add content to the HTML header or at the end of the HTML body. For the DocBook5 target format a Docinfo Processor can add content to the info element or at the very end of the document just before the closing tag of the root element.

Our example Docinfo Processor will add a robots meta tag to the head of the generated HTML document:

A Docinfo Processor that adds a robots meta tag
import org.asciidoctor.ast.Document;
import org.asciidoctor.extension.DocinfoProcessor;
import org.asciidoctor.extension.Location;
import org.asciidoctor.extension.LocationType;

@Location(LocationType.HEADER)                                    // (1)
public class RobotsDocinfoProcessor extends DocinfoProcessor {    // (2)

    @Override
    public String process(Document document) {
        return "<meta name=\"robots\" content=\"index,follow\">"; // (3)
    }
}
  1. The Location annotation defines whether the result of this Docinfo Processor should be added to the header or the footer of the document. Content is added to the header via LocationType.HEADER and to the footer via LocationType.FOOTER.

  2. Every Docinfo Processor must extend the class DocinfoProcessor and implement the process() method.

  3. Our example implementation simply returns the meta tag as a string.

To make AsciidoctorJ use our processor it also has to be registered at the JavaExtensionRegistry.

String src = "= Irrelevant content";

asciidoctor.javaExtensionRegistry()
        .docinfoProcessor(RobotsDocinfoProcessor.class); // (1)

String result = asciidoctor.convert(
        src,
        OptionsBuilder.options()
                .headerFooter(true)                      // (2)
                .safe(SafeMode.SERVER)                   // (3)
                .toFile(false));

org.jsoup.nodes.Document document = Jsoup.parse(result); // (4)
Element metaElement = document.head().children().last();
assertThat(metaElement.tagName(), is("meta"));
assertThat(metaElement.attr("name"), is("robots"));
assertThat(metaElement.attr("content"), is("index,follow"));
  1. The Docinfo Processor implementation is registered at the JavaExtensionRegistry of the Asciidoctor instance.

  2. We render our document with header and footer instead of an embeddable document. Otherwise there is no header where the doc info can be added to.

  3. Docinfo Processors will only be called by Asciidoctor if the safe mode is at least SECURE.

  4. Test via the Jsoup HTML parsing library that our meta tag was correctly added to the resulting document.

Automatically loading extensions

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 the next steps:

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

org.asciidoctor.extension.integratorguide.TerminalCommandExtension.java
import org.asciidoctor.jruby.extension.spi.ExtensionRegistry;

public class TerminalCommandExtension implements ExtensionRegistry { // (1)
  @Override
  public void register(Asciidoctor asciidoctor) { // (2)
    JavaExtensionRegistry javaExtensionRegistry = asciidoctor.javaExtensionRegistry();
    javaExtensionRegistry.treeprocessor(TerminalCommandTreeprocessor.class); // (3)
  }
}
  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.jruby.extension.spi.ExtensionRegistry inside META-INF/services with the implementation’s full qualified name.

META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry
org.asciidoctor.integrationguide.extension.TerminalCommandExtension

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

Note

If you have installed AsciidoctorJ as recommended, the asciidoctorj command will be on the path, and you can use:

asciidoctorj -cp=lib/myextension.jar test.adoc

If you have downloaded the distribution jars only, use a command like:

java -cp lib/jruby-complete-{jruby-version}.jar;lib/asciidoctor-api-{artifact-version}.jar;lib/asciidoctor-core-{artifact-version}.jar;lib/jcommander-{jcommander-version}.jar;lib/myextension.jar org.asciidoctor.jruby.cli.AsciidoctorInvoker test.adoc

Publish everywhere: Adapt Asciidoctor to your own target format

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

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

    File test_adoc = //...

    String result = asciidoctor.convertFile(
            test_adoc,
            OptionsBuilder.options()
                    .backend("text")                                   // (1)
                    .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/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, OptionsBuilder.options().backend("text"));

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.

Logs handling API

Note

This API is inspired by Java Logging API (JUL). If you are familiar with java.util.logging.* you will see familiar analogies with some of its components.

AciidoctorJ (v1.5.7+) offers the possibility to capture messages generated during document rendering. These messages correspond to logging information and are organized in 6 severity levels:

  1. DEBUG

  2. INFO

  3. WARN

  4. ERROR

  5. FATAL

  6. UNKNOWN

The easiest way to capture messages is registering a LogHandler through the Asciidoctor instance.

Registering a LogHandler
Asciidoctor asciidoctor = Asciidoctor.Factory.create();

asciidoctor.registerLogHandler(new LogHandler() { // (1)
    @Override
    public void log(LogRecord logRecord) {
        System.out.println(logRecord.getMessage());
    }
});
  1. Use registerLogHandler to register one or more handlers.

The log method in the org.asciidoctor.log.LogHandler interface provides a org.asciidoctor.log.LogRecord that exposes the following information:

Severity severity

Severity level of the current record.

Cursor cursor

Information about the location of the event, contains:

  • LineNumber: relative to the file where the message occurred.

  • Path: source file simple name, or <stdin> value when rending from a String.

  • Dir: absolute path to the source file parent directory, or the execution path when rending from a String.

  • File: absolute path to the source file, or null when rending from a String.
    These will point to the correct source file, even when this is included from another.

String message

Descriptive message about the event.

String sourceFileName

Contains the value <script>.
For the source filename see Cursor above.

String sourceMethodName

The Asciidoctor Ruby engine method used to render the file; convertFile or convert whether you are rending a File or a String.

Log Handling SPI

Similarly to AsciidoctorJ extensions, the Log Handling API provides an alternate method to register Handlers without accessing Asciidoctor instance.

Start creating a normal LogHandler implementation.

package my.asciidoctor.log.MemoryLogHandler;

import java.util.ArrayList;
import java.util.List;
import org.asciidoctor.log.LogHandler;
import org.asciidoctor.log.LogRecord;

/**
 * Stores LogRecords in memory for later analysis.
 */
public class MemoryLogHandler extends LogHandler {

  private List<LogRecord> logRecords = new ArrayList<>();

  @Override
  public void log(LogRecord logRecord) {
    logRecords.add(record);
  }

  public List<LogRecord> getLogRecords() {
    return logRecords;
  }
}

Next, create a file called org.asciidoctor.log.LogHandler inside META-INF/services with the implementation’s full qualified name.

META-INF/services/org.asciidoctor.log.LogHandler
my.asciidoctor.log.MemoryLogHandler

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

Syntax Highlighter API

Asciidoctor supports a range of different syntax highlighters: Coderay, HighlightJs, Rouge, Pygments and Prettify. Since version 2.0.0 Asciidoctor also supports to adapt and plug in other syntax highlighters when rendering to HTML.

AsciidoctorJ offers this as well since version 2.1.0.

Adapting a syntax highlighter to Asciidoctor involves a subset of the following tasks:

  • Include certain stylesheets and scripts into the resulting HTML document.

  • Create stylesheet and script resources in the filesystem in case the document is rendered to a file and it’s converted with the attributes ':linkcss' and ':copycss'.

  • Format the source block element, by wrapping it into <pre/> and <code/> elements with certain attributes. This means to convert the source text puts "Hello World" to the HTML <pre class="highlightme"><code>puts "Hello World"</code></pre>.

  • Format the source text itself by mapping it to span elements. This means to convert the source text puts "Hello World" to the HTML <span class="id">puts</span> <span class="stringliteral">"Hello World"</span>. This result still has to be formatted, which usually means to wrap it in <pre/> and <code/> elements.

To adapt to a certain syntax highlighter some of these tasks have to be implemented. The following sections show how to write an adapter for a custom syntax highlighter.

Implement a syntax highlighter adapter

A syntax highlighter must implement the interface org.asciidoctor.syntaxhighlighter.SyntaxHighlighterAdapter. This has to be registered at the Asciidoctor instance, so that it can be used by using the corresponding value for the attribute :source-highlighter.

A SyntaxHighlighterAdapter must implement methods to add stylesheets and scripts to the resulting HTML document. This is considered as a core functionality that every syntax highlighter requires.

The following example shows a very simplistic syntax highlighter that uses highlight.js:

import org.asciidoctor.extension.LocationType;
import org.asciidoctor.syntaxhighlighter.SyntaxHighlighterAdapter;

import java.util.Map;

public class HighlightJsHighlighter implements SyntaxHighlighterAdapter { // (1)

    @Override
    public boolean hasDocInfo(LocationType location) {
        return location == LocationType.FOOTER;         // (2)
    }

    @Override
    public String getDocinfo(LocationType location, Document document, Map<String, Object> options) { // (3)
        return "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/github.min.css\">\n" +
            "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js\"></script>\n" +
            "<script>hljs.initHighlighting()</script>";
    }

}
  1. Every syntax highlighter must implement the interface org.asciidoctor.syntaxhighlighter.SyntaxHighlighterAdapter.

  2. The method hasDocInfo indicates that this highlighter only wants to add DocInfo to the footer of the document.

  3. The method getDocInfo is only called to return the DocInfo for the footer of the document. It returns references to the required css and js sources and starts highlight.js.

Let’s say we want to convert this document:

sources.adoc
= Syntax Highlighter Test

== Some sources

[source,java]
----
public static class Test {
  public static void main(String[] args) {
    System.out.println("Hello World");
  }
}
----

Now this document can be converted using our highlighter after registering it with Asciidoctor:

        File sources_adoc = //...

        asciidoctor.syntaxHighlighterRegistry()
            .register(HighlightJsHighlighter.class, "myhighlightjs"); // (1)

        String result = asciidoctor.convertFile(sources_adoc,
            OptionsBuilder.options()
                .headerFooter(true) // (2)
                .toFile(false)
                .attributes(AttributesBuilder.attributes().sourceHighlighter("myhighlightjs"))); // (3)

        assertThat(result,
            containsString("<script>hljs.initHighlighting()</script>"));
  1. Register the adapter class using a well defined name.

  2. Docinfo is only written if the document is converted with the option :header_footer.

  3. The well defined name that was used to register the syntax highlighter must be used in the attribute :source-highlighter.

Lifecycle of a SyntaxHighlighterAdapter

AsciidoctorJ will create an own instance for every document that it converts.

It will try to instantiate the class by calling a constructor that has three parameters:

name

The name of the syntax highlighter as it was referenced by the :source-highlighter attribute. In the previous example this was "myhighlightjs".

backend

The name of the backend used to convert the document. This is for example "html5". Note that SyntaxHighlighters can only be used for HTML based backends.

options

A map containing options for this syntax highlighter. Currently this contains the current org.asciidoctor.ast.Document for the key "document".

This means the syntax highlighter class could also have this constructor:

    public HighlightJsHighlighter(String name, String backend, Map<String, Object> options) {
        assertEquals("myhighlightjs", name);
        assertEquals("html5", backend);
        Document document = (Document) options.get("document");
        assertEquals("Syntax Highlighter Test", document.getDoctitle());
    }

A constructor with only the first two, or only the first parameter is also allowed. AsciidoctorJ will call the constructor with the most matching parameters.

Formatting the source block element

highlight.js tries to automatically determine the source language, which might fail or result in wrong matches. To help highlight.js in identifying the correct source language the <code/> element can be annotated with the language as a class.

That means instead of simply wrapping the code in <pre/> and <code/> elements we want to wrap it inside <pre> and <code class="java"/>. To allow Asciidoctor to apply the styles to properly embed a source block inside the document, the <pre/> element should also have the class highlight. Therefore we want to wrap the source text inside this construct:

<pre class="highlight">
  <code class="java">
    ...
  </code>
</pre>

To allow a syntax highlighter to create this construct it also has to implement the interface org.asciidoctor.syntaxhighlighter.Formatter:

import org.asciidoctor.syntaxhighlighter.Formatter;
import org.asciidoctor.syntaxhighlighter.SyntaxHighlighterAdapter;

import java.util.Map;

public class HighlightJsWithLanguageHighlighter implements SyntaxHighlighterAdapter, Formatter { // (1)

    // Methods hasDocInfo() and getDocInfo()

    @Override
    public String format(Block node, String lang, Map<String, Object> opts) {
        return "<pre class='highlight'><code class='" + lang + "'>" // (2)
            + node.getContent()                                     // (3)
            + "</code></pre>";
    }
}
  1. The SyntaxHighlighterAdapter also has to implement the interface Formatter. This interface only requires the implementation of the method format() that receives the org.asciidoctor.ast.Block that is highlighted, the source language, and additional options.

  2. The implementation of format() wraps everything in a <pre/> and <code/> element with the required classes. The value for the class of the <code/> element is the source language which is passed as an argument to the method.

  3. The source text to be nested into the <pre/> and <code/> elements has to be obtained using node.getContent(). This guarantees that further processing like substitutions work properly.

In Implement a syntax highlighter adapter we have seen how to implement a basic syntax highlighter that embeds all required resources as DocInfo in the document. When the document is converted with the attributes :linkcss and :copycss we expect though that these resources are also written to disk next to the document, and that the document only references them.

Looking at our current example of the highlight.js adapter we referenced the resources from the internet. For scenarios where it should also be possible to read the document while offline, the syntax highlighter can implement the interface org.asciidoctor.syntaxhighlighter.StylesheetWriter:

public class HighlightJsWithOfflineStylesHighlighter implements SyntaxHighlighterAdapter, Formatter, StylesheetWriter { // (1)

    @Override
    public boolean hasDocInfo(LocationType location) {
        return location == LocationType.FOOTER;
    }

    @Override
    public String getDocinfo(LocationType location, Document document, Map<String, Object> options) {
        if (document.hasAttribute("linkcss") && document.hasAttribute("copycss")) { // (2)
            return "<link rel=\"stylesheet\" href=\"github.min.css\">\n" +
                "<script src=\"highlight.min.js\"></script>\n" +
                "<script>hljs.initHighlighting()</script>";
        } else {
            return "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/github.min.css\">\n" +
                "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js\"></script>\n" +
                "<script>hljs.initHighlighting()</script>";
        }
    }

    @Override
    public String format(Block node, String lang, Map<String, Object> opts) {
        return "<pre class='highlight'><code class='" + lang + "'>"
            + node.getContent()
            + "</code></pre>";
    }

    @Override
    public boolean isWriteStylesheet(Document doc) {
        return true; // (3)
    }

    @Override
    public void writeStylesheet(Document doc, File toDir) {
        try {    // (4)
            URL url1 = new URL("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/github.min.css");
            URL url2 = new URL("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js");

            try (InputStream in1 = url1.openStream();
                 OutputStream fout1 = new FileOutputStream(new File(toDir, "github.min.css"))) {
                IOUtils.copy(in1, fout1);
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }

            try (InputStream in2 = url2.openStream();
                 OutputStream fout2 = new FileOutputStream(new File(toDir, "highlight.min.js"))) {
                IOUtils.copy(in2, fout2);
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }

        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }
}
  1. A syntax highlighter that writes additional resources to the filesystem next to the document must implement the interface org.asciidoctor.syntaxhighlighter.StylesheetWriter.

  2. If the document is converted with the attributes :copycss and :linkcss the DocInfo that is added to the converted document should link to the local resources.

  3. The syntax highlighter should return if it wants to write stylesheets in isWriteStylesheet(). This method could for example examine the document if it really needs external resources and return the corresponding result.

  4. The method writeStylesheet() gets the org.asciidoctor.ast.Document and the File for the target directory where the document should be written. External resources should be written to this directory as well.

This highlighter writes the css and js resources to files in the same directory as the document if it is converted with the attributes :linkcss and :copycss:

        File toDir = // ...

        asciidoctor.syntaxHighlighterRegistry()
            .register(HighlightJsWithOfflineStylesHighlighter.class, "myhighlightjs");

        asciidoctor.convertFile(sources_adoc,
            OptionsBuilder.options()
                .headerFooter(true)
                .toDir(toDir)              // (1)
                .safe(SafeMode.UNSAFE)
                .attributes(AttributesBuilder.attributes()
                    .sourceHighlighter("myhighlightjs")
                    .copyCss(true)         // (1)
                    .linkCss(true)));

        File docFile = new File(toDir, "sources.html");
        assertTrue(docFile.exists());

        File cssFile = new File(toDir, "github.min.css");
        assertTrue(cssFile.exists());

        File jsFile = new File(toDir, "highlight.min.js");
        assertTrue(jsFile.exists());

        try (FileReader docReader = new FileReader(new File(toDir, "sources.html"))) {
            String html = IOUtils.toString(docReader);
            assertThat(html, containsString("<link rel=\"stylesheet\" href=\"github.min.css\">"));
            assertThat(html, containsString("<script src=\"highlight.min.js\"></script>"));
        }
  1. External stylesheets are only written when converting to a file, not when converting to a stream or a string, and when the attributes :linkcss and :copycss are set.

Static syntax highlighting during conversion

The examples we looked at until now did the actual syntax highlighting in the browser. But there are also cases where it is desirable to highlight the source during conversion, either because the syntax highlighter is implemented in Java, or syntax highlighting should also work when Javascript is not enabled at the client. The following example uses prism.js to show how to achieve this:

When a SyntaxHighlighterAdapter also implements the interface org.asciidoctor.syntaxhighlighter.Highlighter it will be called to convert the raw source text to HTML. The example uses prism.js which is also a Javascript library. But now we will call this library during document conversion and only add the css part in the resulting HTML, so that the highlighted source will appear correctly even if Javascript is disabled on the client.

public class PrismJsHighlighter implements SyntaxHighlighterAdapter, Formatter, StylesheetWriter, Highlighter { // (1)

    private final ScriptEngine scriptEngine;

    public PrismJsHighlighter() {
        ScriptEngineFactory engine = new NashornScriptEngineFactory(); // (2)
        this.scriptEngine = engine.getScriptEngine();
        try {
            this.scriptEngine.eval(new InputStreamReader(getClass().getResourceAsStream("/prismjs/prism.js")));
        } catch (ScriptException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean hasDocInfo(LocationType location) {
        return location == LocationType.HEADER;
    }

    @Override
    public String getDocinfo(LocationType location, Document document, Map<String, Object> options) {
        if (document.hasAttribute("linkcss") && document.hasAttribute("copycss")) { // (3)
            return "<link href=\"prism.css\" rel=\"stylesheet\" />";
        } else {
            try (InputStream in = getClass().getResourceAsStream("/prismjs/prism.css")) {
                String css = IOUtils.toString(in);
                return "<style>\n" + css + "\n</style>";
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public String format(Block node, String lang, Map<String, Object> opts) {
        return "<pre class='highlight'><code>"  // (3)
            + node.getContent()
            + "</code></pre>";
    }

    @Override
    public boolean isWriteStylesheet(Document doc) {
        return doc.hasAttribute("linkcss") && doc.hasAttribute("copycss");     // (3)
    }

    @Override
    public void writeStylesheet(Document doc, File toDir) {
        try (InputStream in1 = getClass().getResourceAsStream("/prismjs/prism.css"); // (3)
             OutputStream fout1 = new FileOutputStream(new File(toDir, "prism.css"))) {
            IOUtils.copy(in1, fout1);
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
    }

    @Override
    public HighlightResult highlight(Block node,
                                     String source,
                                     String lang,
                                     Map<String, Object> options) {
        ScriptContext ctx = scriptEngine.getContext();                                     // (4)
        Bindings bindings = ctx.getBindings(ScriptContext.ENGINE_SCOPE);
        bindings.put("text", source);
        bindings.put("language", lang);

        try {
            String result = (String) scriptEngine.eval(
                "Prism.highlight(text, Prism.languages[language], language)", bindings);
            return new HighlightResult(result);
        } catch (ScriptException e) {
            throw new RuntimeException(e);
        }
    }
}
  1. A syntax highlighter that wants to statically convert the source text has to implement the interface org.asciidoctor.syntaxhighlighter.Highlighter.

  2. We use the Nashorn Javascript engine to run prism.js.

  3. When rendering to a file and the attributes :linkcss and :copycss are set the css file of prism.js should be written to disk. Otherwise we include the content in a <style/> element.

  4. highlight() is the only method required by the Highlighter interface. It gets the node to be converted, the source, the language and additional options. Here we invoke the prism.js API to convert the plain source text to static HTML, that uses the classes defined in the css. This is returned in a HighlightResult.

Then we can use the highlighter just like in the previous examples. We just have to register it and use the correct value for the attribute :source-highlighter:

        File sources_adoc = //...
        File toDir = // ...

        asciidoctor.syntaxHighlighterRegistry()
            .register(PrismJsHighlighter.class, "prismjs"); // (1)

        asciidoctor.convertFile(sources_adoc,
            OptionsBuilder.options()
                .headerFooter(true)
                .toDir(toDir)
                .safe(SafeMode.UNSAFE)
                .attributes(AttributesBuilder.attributes()
                    .sourceHighlighter("prismjs")           // (1)
                    .copyCss(true)
                    .linkCss(true)));

        File docFile = new File(toDir, "sources.html");

        Document document = Jsoup.parse(new File(toDir, "sources.html"), "UTF-8");
        Elements keywords = document.select("div.content pre.highlight code span.token.keyword"); // (2)
        assertThat(keywords, not(empty()));
        assertThat(keywords.first().text(), is("public"));
  1. Register our prism.js highlighter and set the attribute :source-highlighter to its name to use it.

  2. Test that the source code has been formatted statically to <span/> elements.

Invocation order

This section explains the order of method invocations on a syntax highlighter. For a concrete example we use this document:

= Syntax Highlighter Test

== Some sources

[source,java]
----
System.out.println("Hello Java");
----

[source,go]
----
fmt.Println("Hello Go")
----

For this document the calls to a syntax highlighter will happen in this order:

  1. New SyntaxHighlighter

  2. hasDocInfo for HEADER

  3. getDocInfo for HEADER

  4. format java

  5. highlight System.out.println("Hello Java");

  6. format go

  7. highlight fmt.Println("Hello Go")

  8. hasDocInfo for FOOTER

  9. getDocInfo for FOOTER

  10. isWriteStylesheet

  11. writeStylesheet

Automatically loading syntax highlighters

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

To create an autoloadable extension you should do the next steps:

Create a class that implements org.asciidoctor.jruby.syntaxhighlighter.spi.SyntaxHighlighterRegistry.

org.asciidoctor.integrationguide.syntaxhighlighter.HighlightJsExtension.java
import org.asciidoctor.jruby.syntaxhighlighter.spi.SyntaxHighlighterRegistry;

public class HighlightJsExtension implements SyntaxHighlighterRegistry { // (1)
    @Override
    public void register(Asciidoctor asciidoctor) { // (2)
        asciidoctor.syntaxHighlighterRegistry()     // (3)
            .register(HighlightJsHighlighter.class, "autoloadedHighlightJs");
    }
}
  1. To autoload extensions you need to implement SyntaxHighlighterRegistry.

  2. AsciidoctorJ will automatically run the register method. The method is responsible for registering all extensions.

  3. All required syntax highlighters are registered.

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

META-INF/services/org.asciidoctor.jruby.syntaxhighlighter.spi.SyntaxHighlighterRegistry
org.asciidoctor.integrationguide.syntaxhighlighter.HighlightJsExtension

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

Note

If you have installed AsciidoctorJ as recommended, the asciidoctorj command will be on the path, and you can use:

asciidoctorj -cp=lib/myextension.jar test.adoc

If you have downloaded the distribution jars only, use a command like:

java -cp lib/jruby-complete-{jruby-version}.jar;lib/asciidoctor-api-{artifact-version}.jar;lib/asciidoctor-core-{artifact-version}.jar;lib/jcommander-{jcommander-version}.jar;lib/myextension.jar org.asciidoctor.jruby.cli.AsciidoctorInvoker test.adoc