Treeprocessor

A Treeprocessor gets the whole AST and may do whatever it likes with the document tree. Examples for Treeprocessors could insert blocks, add roles to nodes with a certain content, etc.

Treeprocessors are called by Asciidoctor at the end of the loading process after Preprocessors, BlockProcessors, MacroProcessors and IncludeProcessors but before Postprocessors that are called after the conversion process.

Our example Treeprocessor will recognize paragraphs that contain terminal scripts like below, make listing blocks from them and add the role terminal. The custom role will allows us to customize the style.

Example AsciiDoc document containing a terminal script
To fetch the content of the URL invoke the following:

$ curl -v http://127.0.0.1:8080
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.41.0
> Host: 127.0.0.1:8080
> Accept: */*
>
< HTTP/1.1 200 OK
...

As the first line of the second block starts with a $ sign the whole block should become a listing block. The result when rendering this document with our Treeprocessor should be the same as if the document looked like this:

To fetch the content of the URL invoke the following:

[.terminal]
----
$ curl -v http://127.0.0.1:8080
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.41.0
> Host: 127.0.0.1:8080
> Accept: */*
>
< HTTP/1.1 200 OK
...
----

Note that a BlockProcessor would not work for this task, as a BlockProcessor requires a block name for which it is called. However, in this case the only way to identify this type of blocks is the beginning of the first line.

The Treeprocessor could look like this:

A Treeprocessor that processes terminal scripts.
import org.asciidoctor.ast.Block;
import org.asciidoctor.ast.Document;
import org.asciidoctor.ast.StructuralNode;
import org.asciidoctor.extension.Treeprocessor;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TerminalCommandTreeprocessor extends Treeprocessor {    (1)

    public TerminalCommandTreeprocessor() {}

    @Override
    public Document process(Document document) {
        processBlock((StructuralNode) document);                     (2)
        return document;
    }

    private void processBlock(StructuralNode block) {

        List<StructuralNode> blocks = block.getBlocks();

        for (int i = 0; i < blocks.size(); i++) {
            final StructuralNode currentBlock = blocks.get(i);
            if(currentBlock instanceof StructuralNode) {
                if ("paragraph".equals(currentBlock.getContext())) { (3)
                    List<String> lines = ((Block) currentBlock).getLines();
                    if (lines != null
                            && lines.size() > 0
                            && lines.get(0).startsWith("$")) {
                        blocks.set(i, convertToTerminalListing((Block) currentBlock));
                    }
                } else {
                    // It's not a paragraph, so recursively descend into the child node
                    processBlock(currentBlock);
                }
            }
        }
    }
    public Block convertToTerminalListing(Block originalBlock) {
        Map<Object, Object> options = new HashMap<Object, Object>();
        options.put("subs", ":specialcharacters");

        Block block = createBlock(                                   (4)
                (StructuralNode) originalBlock.getParent(),
                "listing",
                originalBlock.getLines(),
                originalBlock.getAttributes(),
                options);

        block.addRole("terminal");                                   (5)
        return block;
    }
}
1 Every Treeprocessor must extend org.asciidoctor.extension.Treeprocessor and implement the method process(Document).
2 The implementation basically iterates over the tree and invokes processBlock() for every node.
3 The method processBlock() checks for every node if it is a paragraph that has a first line beginning with a $. If it encounters such a block it replaces it with the block created in the method convertToTerminalListing(). Otherwise it descends into the AST searching for these blocks.
4 When creating the new block we reuse the parent of the original block. The context of the new block has to be listing to get a source block. The content can be simply taken from the original block. We add the option 'subs' with the value ':specialcharacters' so that special characters are substituted, i.e. > and < will be replaced with &gt; and &lt; respectively.
5 Finally, we add the role of the node to terminal, which will result in the div containing the listing having the class terminal.