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.
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
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.
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.
<dependencies>
<dependency>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctorj</artifactId>
<version>1.5.7</version> <!--(1)-->
</dependency>
</dependencies>
dependencies {
compile 'org.asciidoctor:asciidoctorj:1.5.7' // (1)
}
<dependency org="org.asciidoctor" name="asciidoctorj" rev="1.5.7" /> <!--(1)-->
libraryDependencies += "org.asciidoctor" % "asciidoctorj" % "1.5.7" // (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.
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));
}
}
-
The static method
Asciidoctor.Factory.create()
creates a new Asciidoctor instance. This is the door to all interactions with Asciidoctor. -
The method
convertFile
takes aFile
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 returnsnull
. -
The conversion options define via
toFile(true)
that the result should be written to a new file. The optionsafe
imposes security constraints on the rendering process.safe(SafeMode.UNSAFE)
defines the least restricting constraints and allows for example inserting the beautifulasciidoctor.css
stylesheet into the resulting document.
The main entry point for AsciidoctorJ is the Asciidoctor
Java interface.
You obtain an instance from the factory class Asciidoctor.Factory
that provides multiple overloaded create methods:
Method Name | Description |
---|---|
|
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. |
|
Creates a new Asciidoctor instance and sets the global variable |
|
Creates a new Asciidoctor instance and set the global variable |
So most of the time you simply get an Asciidoctor instance like this:
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 destroy. After calling this method every other method call on the instance will fail!
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
asciidoctor.shutdown();
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.
|
Asciidoctor
interface
Method Name | Return Type | Description |
---|---|---|
|
|
Parses AsciiDoc content read from a string or stream and converts it to the format specified by the |
|
|
Parses AsciiDoc content read from a file and converts it to the format specified by the |
|
|
Parses a collection of AsciiDoc files and converts them to the format specified by the |
|
|
Parses all AsciiDoc files found in the specified directory (using the provided strategy) and converts them to the format specified by the |
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 .
|
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.
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:
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:
DirectoryWalker
implementations
Class | Description |
---|---|
|
Converts all files of given folder and all its subfolders. Ignores files starting with underscore (_). |
|
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.
String[] result = asciidoctor.convertDirectory(
new AsciiDocDirectoryWalker("src/asciidoc"),
new HashMap<String, Object>());
for (String html : result) {
System.out.println(html);
}
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:
String result =
asciidoctor.convert(
"Hello World",
OptionsBuilder.options() // (1)
.headerFooter(false) // (2)
.get()); // (3)
assertThat(result, startsWith("<div "));
-
Create a new
OptionsBuilder
that is used to prepare the options with a fluent API. -
Set the option
header_footer
tofalse
, meaning that an embeddable document will be rendered, -
Get the built
Options
instance and pass it to the conversion method.
The most important options are explained below.
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:
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"));
-
Set the option
toFile
so that the result will be written to the file pointed to bytargetFile
. -
Set the safe mode to
UNSAFE
so that files can be written. See safe for a description of this option.
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
anddocfile
-
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.
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"));
-
Sets the safe mode from
SECURE
toUNSAFE
. -
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:
= Including content
include::includedcontent.adoc[]
This is included content
This option defines the target format for which the document should be converted.
Among the possible values are pdf
or docbook
.
File targetFile = // ...
asciidoctor.convert(
"Hello World",
OptionsBuilder.options()
.backend("pdf")
.toFile(targetFile)
.safe(SafeMode.UNSAFE)
.get());
assertThat(targetFile.length(), greaterThan(0L));
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:
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>"));
-
Create a builder for attributes and pass the resulting
Attributes
instance to the options. -
Define the attribute supported by Asciidoctor to use the font awesome icons.
-
Define the custom attribute
foo
to the valuebar
.
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
:
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:
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
Ruby ruby = JRubyRuntimeContext.get(asciidoctor);
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.
Name | Class |
---|---|
|
org.asciidoctor.extension.Preprocessor |
|
org.asciidoctor.extension.DocinfoProcessor |
|
org.asciidoctor.extension.Treeprocessor |
|
org.asciidoctor.extension.Postprocessor |
|
org.asciidoctor.extension.BlockProcessor |
|
org.asciidoctor.extension.BlockMacroProcessor |
|
org.asciidoctor.extension.InlineMacroProcessor |
|
org.asciidoctor.extension.IncludeProcessor |
To create an extension two things are required:
-
Create a class extending one of the extension classes from above
-
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.
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.
= 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.
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:-
paragraph
-
listing
-
literal
-
open
-
example
-
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 contentulist
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 oforg.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 fullDocument
in their memberinnerDocument
. 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.
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:
-
Macro name, e.g.
gist
-
Two colons
::
-
A target,
mygithubaccount/8810011364687d7bec2c
-
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.
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)
}
}
-
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 namegist
. -
All BlockMacroProcessors must extend the class
org.asciidoctor.extension.BlockMacroProcessor
. -
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. -
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.
-
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 bepass
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 test
gist::myaccount/1234abcd[]
To make AsciidoctorJ use our processor it has to be registered at the JavaExtensionRegistry
:
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)
-
The block macro processor is registered at the
JavaExtensionRegistry
of the Asciidoctor instance. -
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.
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:
-
Macro name, e.g.
issue
-
One colon, i.e.
:
. This is what distinguishes it from a block macro even if being alone in a paragraph. -
A target, e.g.
333
-
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
.
= 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:
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().getAttr("repo"))
.append("/issues/")
.append(target).toString();
Map<String, Object> options = new HashMap<String, Object>();
options.put("type", ":link");
options.put("target", href);
return createPhraseNode(parent, "anchor", target, attributes, options) // (4)
.convert(); // (5)
}
}
-
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 nameissue
. -
All InlineMacroProcessors must extend the class
org.asciidoctor.extension.InlineMacroProcessor
. -
A InlineMacroProcessor must implement the abstract method
process
that is called by Asciidoctor. The method must return the rendered result of this macro. -
The implementation constructs a new phrase node that is a link, i.e. an
anchor
via the methodcreatePhraseNode()
. The third parametertarget
defines that the text to render this link is the target of the macro, that means that the link will be rendered as333
or2
. 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'. -
Instead of returning the created AST node, the converted result is returned. The method
convert()
will invoke the correct converter, so that this also works when rendering to PDF.
To make AsciidoctorJ use our processor it has to be registered at the JavaExtensionRegistry
:
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\""));
-
The inline macro processor is registered at the
JavaExtensionRegistry
of the Asciidoctor instance. -
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.
@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)
.convert(); // (3)
}
}
-
The attributes of the PhraseNode must contain the keys to be shown as a list for the attribute key
keys
. -
Create a PhraseNode with context
kbd
and no text. -
The macro processor has to return the converted PhraseNode.
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.
-
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 keymenuitem
referring to the final menu item selection. -
Create an PhraseNode with context
menu
and no text. -
The macro processor has to return the converted PhraseNode.
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.
@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)
.convert(); // (5)
}
}
-
For an inline image the option
type
must have the valueimage
. -
The URL of the image must be set via the option
target
. -
Optional attributes
alt
for alternative text,width
andheight
are set in the node attributes. Other possible attributes includetitle
to define the title attribute of theimg
element when rendering to HTML. When setting the attributelink
to any value the node will be converted to a link to that image, where the window can be defined via the attributewindow
. -
Create a PhraseNode with context
image
and no text. -
The macro processor has to return the converted PhraseNode.
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]
I really mean it
After the processing this block will look like this
I REALLY MEAN IT
The BlockProcessor looks like this:
@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);
}
}
-
The annotation
@Name
defines the block name that this block processor handles. -
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. -
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 modelContentModel.SIMPLE
is defined. -
All block processors must extend
org.asciidoctor.extension.BlockProcessor
. -
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)
-
The block processor is registered at the
JavaExtensionRegistry
of the Asciidoctor instance. -
Check that the resulting HTML contains the text as upper-case letters.
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:
----
include::ls[]
----
The processor could look like this:
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);
}
}
-
Every Include Processor must extend the class
org.asciidoctor.extension.IncludeProcessor
. -
Asciidoctor calls the method
handles()
with the target for every include directive it finds. The method must returntrue
if it feels responsible for this directive. In our case it returnstrue
if the target isls
. -
The implementation of the method
process()
lists the directory contents of the current directory and creates a string with one line per file. -
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 namels
and the path of the current directory. The parameter1
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));
-
The Include Processor is registered at the
JavaExtensionRegistry
of the Asciidoctor instance.
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.
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:
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.
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)
}
}
-
All Preprocessors must extend the class
org.asciidoctor.extension.Preprocessor
and implement the methodprocess()
. -
The implementation gets the whole Asciidoctor source as an array of Strings where each entry corresponds to one line.
-
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.
-
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)
-
The preprocessor is registered at the
JavaExtensionRegistry
of the Asciidoctor instance. -
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 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:
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;
}
}
-
All Preprocessors must extend the class
org.asciidoctor.extension.Postprocessor
and implement the methodprocess()
. -
The processor parses the resulting HTML text using the Jsoup library. This returns the document as a data structure.
-
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. -
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));
-
The postprocessor is registered at the
JavaExtensionRegistry
of the Asciidoctor instance. -
To make Asciidoctor generate the footer element the option
headerFooter
must be activated.
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.
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:
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;
}
}
-
Every Treeprocessor must extend
org.asciidoctor.extension.Treeprocessor
and implement the methodprocess(Document)
. -
The implementation basically iterates over the tree and invokes
processBlock()
for every node. -
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 methodconvertToTerminalListing()
. Otherwise it descends into the AST searching for these blocks. -
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>
and<
respectively. -
Finally we add the role of the node to
terminal
, which will result in the div containing the listing having the classterminal
.
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));
-
The Treeprocessor is registered at the
JavaExtensionRegistry
of the Asciidoctor instance.
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:
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)
}
}
-
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 viaLocationType.FOOTER
. -
Every Docinfo Processor must extend the class
DocinfoProcessor
and implement theprocess()
method. -
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"));
-
The Docinfo Processor implementation is registered at the
JavaExtensionRegistry
of the Asciidoctor instance. -
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.
-
Docinfo Processors will only be called by Asciidoctor if the safe mode is at least
SECURE
. -
Test via the Jsoup HTML parsing library that our meta tag was correctly added to the resulting document.
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:
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;
}
}
-
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 nametext
this converter will be used for conversion. -
A converter must implement this constructor, because AsciidoctorJ will call the constructor with this signature. For every conversion a new instance will be created.
-
The method
convert()
is called with the AST object for the document, i.e. aDocument
instance, when a document is rendered. -
The optional parameter
transform
hints at the transformation to be executed. This could be for example the valueembedded
to indicate that the resulting document should be without headers and footers. If it isnull
the transformation usually is defined by the node type and name. -
Calls to the method
getContent()
of a node will recursively call the methodconvert()
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:
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));
-
Registers the converter class
TextConverter
for this Asciidoctor instance. The given converter is responsible for converting to the target formattext
because the@ConverterFor
annotation of the converter class defines this name. -
The conversion options
backend
is set to the valuetext
so that ourTextConverter
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:
package org.asciidoctor.integrationguide.converter;
import org.asciidoctor.Asciidoctor;
import org.asciidoctor.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:
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
.
Note
|
This API is inspired by Java Logging API (JUL).
If you are familiar with |
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:
-
DEBUG
-
INFO
-
WARN
-
ERROR
-
FATAL
-
UNKNOWN
The easiest way to capture messages is registering a LogHandler
through the Asciidoctor
instance.
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
asciidoctor.registerLogHandler(new LogHandler() { // (1)
@Override
public void log(LogRecord logRecord) {
System.out.println(logRecord.getMessage());
}
});
-
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:
|
String message |
Descriptive message about the event. |
String sourceFileName |
Contains the value |
String sourceMethodName |
The Asciidoctor Ruby engine method used to render the file; |
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.
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.