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 equal to embedded if we convert to an embeddable document,
or document if we convert to a standalone document (i.e., standalone is true). |
To register a custom converter, use the register function on ConverterFactory:
import { ConverterFactory } from '@asciidoctor/core'
ConverterFactory.register(new CustomConverter(), ['html5']) (1)
| 1 | Instantiate the CustomConverter and register it for the html5 backend |
|
|
Before we continue, let’s create a little more advanced custom converter:
class CustomConverter {
async convert (node, transform) {
const nodeName = transform || node.getNodeName()
if (nodeName === 'embedded') {
return `<embedded>
${await node.getContent()}
</embedded>` (1)
} else if (nodeName === 'document') {
return `<document>
${await 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 converter is registered we can convert our document:
import { ConverterFactory, load } from '@asciidoctor/core'
ConverterFactory.register(new CustomConverter(), ['html5'])
const doc = await load(`= Title
== Section 1
== Section 2`)
console.log(await 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 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:
import { Html5Converter } from '@asciidoctor/core'
class SemanticParagraphConverter {
constructor () {
this.baseConverter = Html5Converter.create() (1)
}
async convert (node, transform, opts) {
if (node.getNodeName() === 'paragraph') {
return `<p>${await node.getContent()}</p>` (2)
}
return this.baseConverter.convert(node, transform, opts) (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 |
Inheritance pattern
As an alternative to composition, you can extend Html5Converter directly.
Because Html5Converter dispatches each node to a dedicated convert_<nodeName> method, you only need to override the method(s) you care about — all other nodes are handled automatically by the parent class:
import { Html5Converter } from '@asciidoctor/core'
class SemanticParagraphConverter extends Html5Converter { (1)
async convert_paragraph (node) { (2)
return `<p>${await node.getContent()}</p>`
}
}
| 1 | Extend the built-in HTML5 converter |
| 2 | Override only the paragraph node handler — all other nodes fall through to the parent class |
If you prefer to override the top-level convert method instead, call super.convert(node, transform) to delegate to the parent:
import { Html5Converter } from '@asciidoctor/core'
class SemanticParagraphConverter extends Html5Converter {
async convert (node, transform, opts) {
if (node.getNodeName() === 'paragraph') {
return `<p>${await node.getContent()}</p>`
}
return super.convert(node, transform, opts) (1)
}
}
| 1 | Delegate to the built-in HTML5 converter for every other node |