Asciidoctor BlockMacro to generate subblocks
Asciidoctor - and most of its bindings - is a great tool but its API is not always super friendly (convert/convertFile,render,...). It is probably worse when it comes to extensions. Let's see some hidden features of block macro API and how to simply your writing even more.
First, let's have a look to the block macro feature, the overall goal is to implement a block in a custom manner.
So you write:
[my_extension,some_config=some_value]
--
something
--
And it will render an awesome content (graphs, diagrams, paragraphs etc...).
When the content is repeated or can be generated, it is tempting to use that as a macro (isn't the name suggesting it?) to replace repetitive tasks instead of generating the sources. For example if you run N benchmark scenarii and want an aggregated report you could do a benchmark_report macro:
[benchmark_report,scenarii=http_with_auth]
--
--
This macro can generate the CPU, Memory, I/O, Disk graphs and also render aggregates in a table (TPS, Latency, ....). All that with just this new macro :).
Tip: indeed, an inline macro could be more appropriated for this particular example but it is a detail for this post.
In terms of API, the block macro processor requires to implement:
Object process(
StructuralNode parent,
Reader reader,
Map<String, Object> attributes);
If you review most examples out there - or even all official ones for the java binding ;) - you will notice the returned value is another block - or at least an instance created with one of the createXXX methods:
return createBlock(parent, "paragraph", "my text");
Now, if you want to generate a section, you will naturally call createSection....and it wil fail because Section does not implement the right methods.
So does it mean macro can't create subsection or complex asciidoctor content in a portable way (we don't want to use pass indeed)?
Actually it can but you must build the content and chain it yourself.
So first, let's create our section:
final var sectNums = parent.getDocument().hasAttribute("sectnums");
final var level = parent.getLevel() + 1;
final var section = createSection(parent, level, sectNums, emptyMap());
section.setTitle("My Title");
Then you just have to add this section to the parent:
parent.append(section); // parent << section in ruby
But what should we return? Since we appended ourself the content, we just need to skip the return value so we can return null/nil :).
At that stage, we are able to create more or less what we want but the issue is that we must create the sub-document manually. A helper method is there to make it simpler, reusing Asciidoctor parser: parseContent.
You give it an asciidoctor split by line and a parent to append the sub-document to and it will do the work for you:
final List<String> content = generateContent();
parseContent(parent, content);
Personally I combine both technics, building manually the sections and appending the content of the section with parseContent:
@Name("my_block")
@Contexts(Contexts.OPEN)
@ContentModel(ContentModel.ATTRIBUTES)
public class MyBlock extends BlockProcessor {
@Override
public Object process(final StructuralNode parent, final Reader reader,
final Map<String, Object> attributes) {
final boolean sectNums = parent.getDocument().hasAttribute("sectnums");
final var rootSectionLevel = parent.getLevel() + 1;
final var rootSection = createSection(parent, rootSectionLevel, sectNums, emptyMap());
rootSection.setTitle("SubSection1");
getDataToGenerateASectionFor().stream()
.forEach(p -> {
final var section = createSection(parent, rootSectionLevel + 1, sectNums, emptyMap());
section.setTitle("SubSection2 for " + p.getId());
parseContent(section, generateFor(p));
rootSection.append(section);
});
parent.append(rootSection);
return null;
}
// getDataToGenerateASectionFor will return a list of items to generate a section for
// generateFor will generate the List<String> containing the adoc lines for one data
}
And here we are, we generated sub-sections for a set of items in 20 lines. Works even with asciidoctor-pdf!
From the same author:
In the same category: