Custom Converter

On this page, you’ll learn how to create and register a custom converter.

Data structure

But first let’s briefly explain how the Asciidoctor.js processor works. When you convert a document with Asciidoctor.js, the processor will create a tree representation of your document called an Abstract Syntax Tree (AST).

This tree or data structure consists of nodes. Let’s take a concrete example with the following document:

= Title

== Section 1

== Section 2

In the example above we have a document with two sections. Quite logically our tree representation will consist of a Document node that contains two Section nodes. As you can see the structure is hierarchical as the two Section nodes are the children of the Document node and the Document node itself is the root of the tree.

If we convert this document, the converter will be responsible to convert every node in your document. In the example above, the task of the converter will be to convert a Document node and two Section nodes.

Custom converter class

Now let’s see how to create a custom converter.

A custom converter is a JavaScript class with a convert function:

class CustomConverter {
  convert (node, transform) { (1) (2)
    return node.getContent()
  }
}
1 node is a node that extends AbstractNode.
2 transform will only be defined when the node is a Document.
The value will be equals to embedded if we convert to an embeddable document, or document if we convert to a standalone document (ie. standalone is equals to true).

To register a custom converter we can use the function register on the ConverterFactory:

asciidoctor.ConverterFactory.register(new CustomConverter(), ['html5']) (1)
1 Instantiate the CustomConverter and register it for the html5 backend

The html5 is the default backend, so the above code will effectively replace the built-in HTML5 converter provided by Asciidoctor.js

Before we continue, let’s create a little more advanced custom converter:

class CustomConverter {
  convert (node, transform) {
    const nodeName = transform || node.getNodeName()
    if (nodeName === 'embedded') {
      return `<embedded>
${node.getContent()}
</embedded>` (1)
    } else if (nodeName === 'document') {
      return `<document>
${node.getContent()}
</document>` (2)
    } else if (nodeName === 'section') {
      return `${node.getTitle()}` (3)
    }
    return '' (4)
  }
}
1 If the node is an embedded document we return the document content inside a <embedded> tag
2 If the node is a standalone document we return the document content inside a <document> tag
3 If the node is a section we return the section’s title
4 Otherwise we return an empty string

Once the custom registered we can convert our document:

const doc = asciidoctor.load(`= Title

== Section 1

== Section 2`)

console.log(doc.convert())
// Prints:
// <embedded>
// Section 1
// Section 2
// </embedded>

Here’s the complete list of node’s name:

  • document

  • embedded

  • outline

  • section

  • admonition

  • audio

  • colist

  • dlist

  • example

  • floating-title

  • image

  • listing

  • literal

  • stem

  • olist

  • open

  • page_break

  • paragraph

  • preamble

  • quote

  • thematic_break

  • sidebar

  • table

  • toc

  • ulist

  • verse

  • video

  • inline_anchor

  • inline_break

  • inline_button

  • inline_callout

  • inline_footnote

  • inline_image

  • inline_indexterm

  • inline_kbd

  • inline_menu

  • inline_quoted

Composition pattern

In the previous section, we saw how to create and register a standalone custom converter. But you might want to use your custom converter only on some nodes and delegate the rest to an another converter (for instance the built-in converter).

In the example below we will use a custom converter to convert paragraph but the other nodes will be converted using the built-in HTML5 converter:

const asciidoctor = require('asciidoctor')()

class SemanticParagraphConverter {
  constructor () {
    this.baseConverter = asciidoctor.Html5Converter.$new() (1)
  }

  convert (node, transform) {
    if (node.getNodeName() === 'paragraph') {
      return `<p>${node.getContent()}</p>` (2)
    }
    return this.baseConverter.convert(node, transform) (3)
  }
}
1 Instantiate the built-in HTML5 converter
2 Define how the paragraph node will be converted
3 By default call the built-in HTML5 converter