Inline Macro Processor

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:

  1. Macro name, e.g. issue

  2. One colon, i.e. :. This is what distinguishes it from a block macro even if it is alone in a paragraph.

  3. An optional target, e.g. 333

  4. Optional 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.

issue-inline-macro.adoc
= 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:

An InlineMacroProcessor that replaces issue macros with links
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().getAttribute("repo"))
                    .append("/issues/")
                    .append(target).toString();

        Map<String, Object> options = new HashMap<>();
        options.put("type", ":link");
        options.put("target", href);
        return createPhraseNode(parent, "anchor", target, attributes, options); (4)
    }

}
1 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 name issue.
2 All InlineMacroProcessors must extend the class org.asciidoctor.extension.InlineMacroProcessor.
3 A InlineMacroProcessor must implement the abstract method process that is called by Asciidoctor. The method must return the rendered result of this macro.
4 The implementation constructs and returns a new phrase node that is a link, i.e. an anchor via the method createPhraseNode(). The third parameter target defines that the text to render this link is the target of the macro, that means that the link will be rendered as 333 or 2. 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'.

Creating phrase nodes

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.

Create keyboard macros

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.

Create a phrase node for keys
@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)
    }
}
1 The attributes of the PhraseNode must contain the keys to be shown as a list for the attribute key keys.
2 Create a PhraseNode with context kbd and no text and return it.

Create button or menu selection macros

To create a menu selection as described at Button and Menu UI Macros a processor would create a PhraseNode with the menu context. The following processor would render the macro rightclick:New|Class[] like this: New  Class.

Create a phrase node for menu selections.
@Name("rightclick")
public class ContextMenuInlineMacroProcessor extends InlineMacroProcessor {

    @Override
    public Object process(ContentNode parent, String target, Map<String, Object> attributes) {
        String[] items = target.split("\\|");
        Map<String, Object> attrs = new HashMap<String, Object>();
        attrs.put("menu", "Right click");                              (1)
        List<String> submenus = new ArrayList<String>();
        for (int i = 0; i < items.length - 1; i++) {
            submenus.add(items[i]);
        }
        attrs.put("submenus", submenus);
        attrs.put("menuitem", items[items.length - 1]);

        return createPhraseNode(parent, "menu", (String) null, attrs); (2)
    }
}
1 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 key menuitem referring to the final menu item selection.
2 Create and return an PhraseNode with context menu and no text.

Create inline images

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.

Create a PhraseNode for inline image.
@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)
    }
}
1 For an inline image the option type must have the value image.
2 The URL of the image must be set via the option target.
3 Optional attributes alt for alternative text, width and height are set in the node attributes. Other possible attributes include title to define the title attribute of the img element when rendering to HTML. When setting the attribute link to any value the node will be converted to a link to that image, where the window can be defined via the attribute window.
4 Create and return a PhraseNode with context image and no text.

We said at the start of this section that the target (the x in menu:x[]) is optional. If you want a macro that does not have a target (for example cite:[brown79]), add the following annotation to your class:

@Name("cite")
@Format(SHORT)
class CiteInlineMacroProcessor extends InlineMacroProcessor {
  ...
}

With the SHORT format, the attributes are not parsed, and the 'target' that is passed to your macro is the value between the brackets ("brown79").

Positional attributes

The first example here has shown how to access named attributes. But AsciiDoc also supports positional attributes where the meaning is implicitly derived from the position in the attribute list. In that example the attribute repo might also be defined as the first attribute so that the inline macro might also be written as issue:333[asciidoctor/asciidoctorj].

The following extension accepts the attribute repo as a positional attribute:

Accept positional attributes in an inline macro
import org.asciidoctor.ast.ContentNode;
import org.asciidoctor.extension.InlineMacroProcessor;
import org.asciidoctor.extension.Name;
import org.asciidoctor.extension.PositionalAttributes;

@Name("issue")
@PositionalAttributes({"repo"})     (1)
public class IssueInlineMacroPositionalAttributesProcessor extends InlineMacroProcessor {

    @Override
    public Object process(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().getAttribute("repo"))
                    .append("/issues/")
                    .append(target).toString();

        Map<String, Object> options = new HashMap<>();
        options.put("type", ":link");
        options.put("target", href);
        return createPhraseNode(parent, "anchor", target, attributes, options); (4)
    }

}
1 The annotation @PositionalAttributes defines the positional attributes and their order. If the macro accepted a second positional attribute comment the annotation would be @PositionalAttributes({"repo", "comment"}).

This extension will accept the macros in this document:

= InlineMacroProcessor Test Document
:repo: asciidoctor/asciidoctorj-groovy-dsl

You might want to take a look at the issue issue:334[asciidoctor/asciidoctorj] and issue:3[].