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: