Block Macro Processor

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.
There are many more methods available to create any type of AST node.

Now, once it is registered, we would be able to use the new block macro in our document as:

gist-macro.adoc
= Gist test

gist::myaccount/1234abcd[]

Attributes and Positional attributes

As a next step for the gist macro we might want to add support for GitLab Snippets, which are a similar system to Github Gists. The property whether we want to embed a Github Gist or a GitLab Snippet can be passed as the first attribute to the macro.

GitLab Snippets can also be part of a project. This project could be accepted as a second attribute.

That way Gists or Snippets could be embedded with our macro with these AsciiDoc block macros:

gist-marco-attributes.adoc
== Gists

gist::myaccount/1234abcd[]

gist::2228798[gitlab]

gist::1717978[gitlab,gitlab-org/gitlab-foss]

gist::1717979[gitlab,repo=gitlab-org/gitlab-foss]

The first macro shows our original notation for how to embed a Github Gist.

The second macro shows how to embed a Snippet from GitLab that is not associated with a project.

The third macro shows how to embed a Snippet from GitLab that is associated with the project gitlab-org/gitlab-foss.

The last macro shows how to embed a Snippet from GitLab that is associated with the project gitlab-org/gitlab-foss by not using a positional attribute, but instead naming it explicitly.

This can be achieved by the following extension:

GistBlockMacroPositionalAttributesProcessor
import org.asciidoctor.ast.StructuralNode;
import org.asciidoctor.extension.BlockMacroProcessor;
import org.asciidoctor.extension.Name;
import org.asciidoctor.extension.PositionalAttributes;

import java.util.Map;

@Name("gist")
@PositionalAttributes({"provider", "repo"})                  (1)
public class GistBlockMacroPositionalAttributesProcessor extends BlockMacroProcessor {

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

        String script;
        String provider = (String) attributes.get("provider");
        if (provider == null || "github".equals(provider)) { (2)
            script = String.format("<script src=\"https://gist.github.com/%s.js\"/></script>", target);
        } else if ("gitlab".equals(provider)) {
            String repo = (String) attributes.get("repo");
            if (repo == null) {
                script = String.format("<script src=\"https://gitlab.com/-/snippets/%s.js\"></script>", target);
            } else {
                script = String.format("<script src=\"https://gitlab.com/%s/-/snippets/%s.js\"></script>", repo, target);
            }
        } else {
            throw new IllegalArgumentException("Unknown provider " + provider);
        }

        String content = new StringBuilder()
            .append("<div class=\"openblock gist\">")
            .append("<div class=\"content\">")
            .append(script)
            .append("</div>")
            .append("</div>").toString();

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

}
1 The positional attributes for this are provider and repo in that order. These attributes can be either passed by their position, or by name.
2 Based on the values of the two attributes the HTML content to embed the Gist is computed.