Extension Migration: 1.5.x to 1.6.x
AsciidoctorJ 1.6.0 fixed many issues with extensions, but also brought some incompatible changes with 1.5.x. In fact these incompatible changes were necessary to fix some of these bugs.
This guide explains how to migrate an existing extension for AsciidoctorJ 1.5.x to 1.6.0. We will take an existing extension from the 1.5.x test cases and migrate it step by step to 1.5.x.
If you want to migrate from 1.5.x to the latest version, i.e. 2.0.x, please follow all individual sections, i.e. first Extension Migration Guide: 1.5.x to 1.6.x and then this. |
The original extension
As an example we are taking the YellStaticBlock
extension.
This is a BlockProcessor that transforms the contents of the corresponding block to upper case.
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.asciidoctor.ast.AbstractBlock;
import org.asciidoctor.extension.BlockProcessor;
import org.asciidoctor.extension.Reader;
import static java.util.stream.Collectors.joining;
public class YellStaticBlock extends BlockProcessor {
private static Map<String, Object> configs = new HashMap<String, Object>() {{
put("contexts", Arrays.asList(":paragraph"));
put("content_model", ":simple");
}};
public YellStaticBlock(String name, Map<String, Object> config) {
super(name, configs);
}
@Override
public Object process(AbstractBlock parent, Reader reader, Map<String, Object> attributes) {
String upperLines = reader.readLines().stream() (1)
.map(String::toUpperCase)
.collect(joining("\n"));
return createBlock( (2)
parent,
"paragraph",
upperLines,
attributes,
new HashMap<>());
}
}
1 | Transform content to uppercase |
2 | Create new block that replaces the processed one. |
When you simply update your dependency on AsciidoctorJ from 1.5.x to 1.6.0 the compiler will complain with an error similar to this:
.../YellStaticBlock.java:8: error: cannot find symbol import org.asciidoctor.ast.AbstractBlock; ^ symbol: class AbstractBlock location: package org.asciidoctor.ast .../YellStaticBlock.java:24: error: cannot find symbol public Object process(AbstractBlock parent, Reader reader, Map<String, Object> attributes) { ^ symbol: class AbstractBlock location: class YellStaticBlock 2 errors
This is because the AST interfaces were renamed to better represent their purpose. The next section shows how these have to be updated.
Update to new AST class names
The following table shows the new AST class names with their counterparts in AsciidoctorJ 1.5.x. See Understanding the AST Classes for details about the purpose of the classes.
Name in 1.6.0 | Name in 1.5.x |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
As you can see not all AST classes were renamed, but in particular those classes that appear in the signatures of the processor classes were renamed.
After renaming the classes the new Processor looks like this:
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.asciidoctor.ast.StructuralNode;
import org.asciidoctor.extension.BlockProcessor;
import org.asciidoctor.extension.Reader;
public class YellStaticBlock extends BlockProcessor {
private static Map<String, Object> configs = new HashMap<String, Object>() {{
put("contexts", Arrays.asList(":paragraph"));
put("content_model", ":simple");
}};
public YellStaticBlock(String name, Map<String, Object> config) {
super(name, configs);
}
@Override
public Object process(StructuralNode parent, Reader reader, Map<String, Object> attributes) {
List<String> lines = reader.readLines();
String upperLines = null;
for (String line : lines) {
if (upperLines == null) {
upperLines = line.toUpperCase();
}
else {
upperLines = upperLines + "\n" + line.toUpperCase();
}
}
return createBlock(parent,
"paragraph",
Arrays.asList(upperLines),
attributes,
new HashMap<Object, Object>());
}
}
Together with the AST class names also the factory methods of the common interface of all extensions, org.asciidoctor.extension.Processor
were renamed.
While this isn’t a problem here, for example invocations of createInline()
have to be renamed to createPhraseNode()
according to the table above.
This extension will already run with AsciidoctorJ 1.6.0 and the following test will pass:
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
asciidoctor.javaExtensionRegistry().block("yell", YellStaticBlock.class);
final String doc = "[yell]\nHello World";
final String result = asciidoctor.convert(doc, OptionsBuilder.options());
Document htmlDoc = Jsoup.parse(result);
assertEquals("HELLO WORLD", htmlDoc.select("p").first().text());
There are some additional steps you can take to make this extension more concise.
The extension explicitly creates a map for its configuration, stores the values in it and passes it to the base class via the constructor. This configuration is static and never changes. Also the block name is passed when registering the extension which also might never change.
Finally it is rather ugly that the constructor has to take a parameter config
, that it completely ignores.
The next section shows how this can be done in a more concise way.
Instantiating and configuring extensions
The configuration of an extension has to be known at the time of registration. With AsciidoctorJ 1.5.x the way to define the configuration was to pass it to the super constructor and every extension type had to implement one certain constructor. For many extension type a block or macro name also has to be passed to the registration method.
This configuration is static most of the times and often extensions are registered as classes instead of instances:
asciidoctor.javaExtensionRegistry().block("yell", YellStaticBlock.class);
// instead of
asciidoctor.javaExtensionRegistry().block("yell", new YellStaticBlock(...));
When you register an extension as a class, AsciidoctorJ 1.6.0 allows to remove most of the boilerplate code to create the configuration by using Java annotations. Also block or macro names can be configured with annotations directly at the extension implementation itself.
This way the extension can become this:
import org.asciidoctor.ast.ContentModel;
import org.asciidoctor.ast.StructuralNode;
import org.asciidoctor.extension.BlockProcessor;
import org.asciidoctor.extension.Contexts;
import org.asciidoctor.extension.Name;
import org.asciidoctor.extension.Reader;
import java.util.HashMap;
import java.util.Map;
import static java.util.stream.Collectors.joining;
@Contexts(Contexts.PARAGRAPH)
@ContentModel(ContentModel.COMPOUND)
@Name("yell")
public class YellStaticBlock extends BlockProcessor {
@Override
public Object process(StructuralNode parent, Reader reader, Map<String, Object> attributes) {
String upperLines = reader.readLines().stream()
.map(String::toUpperCase)
.collect(joining("\n"));
return createBlock(parent, "paragraph", upperLines, attributes, new HashMap<Object, Object>());
}
}
Now the test case can be further simplified to this:
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
asciidoctor.javaExtensionRegistry().block(YellStaticBlock.class); (1)
final String doc = "[yell]\nHello World";
final String result = asciidoctor.convert(doc, OptionsBuilder.options());
Document htmlDoc = Jsoup.parse(result);
assertEquals("HELLO WORLD", htmlDoc.select("p").first().text());
1 | Passing the block name was removed and is taken from the annotation of the extension.
If you explicitly want a different block name, e.g. loud , it is still possible to pass it by calling JavaExtensionRegistry.block("loud", YellStaticBlock.class) . |
And this was already it. The extension is now compatible to AsciidoctorJ 1.6.0.
For further examples you might want to compare the following examples:
Name |
Extension Type |
||
YellBlock |
BlockProcessor |
||
ArrowsAndBoxesBlock |
BlockProcessor |
||
ManpageMacro |
InlineMacro |