Register Extensions
These extension points are currently available.
- Preprocessor
-
Processes the raw source lines before they are passed to the parser. See Preprocessor Example.
- Tree processor
-
Processes the Asciidoctor.Document (AST) once parsing is complete. See Tree Processor Example.
- Postprocessor
-
Processes the output after the document has been converted, but before it’s written to disk. See Postprocessor Example.
- Docinfo Processor
-
Adds additional content to the header or footer regions of the generated document. See Docinfo Processor Example.
- Block processor
-
Processes a block of content marked with a custom block style (i.e.,
[custom]). (similar to an AsciiDoc filter) See Block Processor Example. - Block macro processor
-
Registers a custom block macro and processes it (e.g.,
gist::12345[]). See Block Macro Processor Example. - Inline macro processor
-
Registers a custom inline macro and processes it (e.g.,
Save). See Inline Macro Processor Example. - Include processor
-
Processes the
include::<filename>[]directive. See Include Processor Example.
Register one or more extensions
You can register an extension globally as follows:
import { Extensions, convert } from '@asciidoctor/core'
Extensions.register(function () {
this.block(function () {
const self = this
self.named('callout')
self.onContext('paragraph')
self.process(function (parent, reader) {
const lines = reader.getLines()
return self.createBlock(parent, 'paragraph', lines, { role: 'callout' })
})
})
})
const text = `[callout]
Deploy to production only after all tests pass.`
const html = await convert(text)
console.log(html)
// <div class="paragraph callout">
// <p>Deploy to production only after all tests pass.</p>
// </div>
You can register more than one processor of each type, though you can only have one processor per custom block or macro. Each registered class is instantiated when the Asciidoctor.Document is created.
| There is currently no extension point for processing a built-in block, such as a normal paragraph. Look for that feature in a future Asciidoctor release. |
You can also create one or more registries. It can be useful when you want to convert the same text with different extensions enabled.
import { Extensions, convert } from '@asciidoctor/core'
const registryA = Extensions.create('callout-paragraph', function () {
this.block(function () {
const self = this
self.named('callout')
self.onContext('paragraph')
self.process(function (parent, reader) {
// Render as a paragraph with a CSS role
const lines = reader.getLines()
return self.createBlock(parent, 'paragraph', lines, { role: 'callout' })
})
})
})
const registryB = Extensions.create('callout-aside', function () {
this.block(function () {
const self = this
self.named('callout')
self.onContext('paragraph')
self.process(function (parent, reader) {
// Render as a semantic <aside> element
const lines = reader.getLines()
const html = `<aside class="callout">${lines.join('\n')}</aside>`
return self.createBlock(parent, 'pass', html)
})
})
})
const text = `[callout]
Deploy to production only after all tests pass.`
console.log(await convert(text, { extension_registry: registryA }))
console.log('')
console.log(await convert(text, { extension_registry: registryB }))
// <div class="paragraph callout">
// <p>Deploy to production only after all tests pass.</p>
// </div>
//
// <aside class="callout">Deploy to production only after all tests pass.</aside>
In the example above, we’ve created two registries:
-
registryA -
registryB
Both registries have a [callout] block extension registered with a specific implementation.
The first block extension is registered in registryA and renders the content as a paragraph with a callout CSS role.
The other is registered in registryB and wraps the content in a semantic <aside> element.
Reusing a registry across multiple conversions
A registry can be reused across multiple conversions by passing it as the extension_registry option.
However, the way you register extensions determines whether reuse is safe.
When a block function is passed to Extensions.create(), that block is stored internally as a group.
On each conversion, the registry resets its transient state and re-executes all groups, so extensions are correctly re-activated every time.
When extensions are registered directly on a registry instance (e.g. registry.preprocessor(fn) called outside any block), they are stored in transient state that is cleared on every activation.
Those registrations will be silently lost from the second conversion onwards.
import { Extensions, convert } from '@asciidoctor/core'
// The function passed to Extensions.create() is stored as a group.
// It is re-executed on each conversion, so the preprocessor is always active.
const registry = Extensions.create('my-ext', function () {
this.preprocessor(function () {
this.process(function (doc, reader) {
// ...
return reader
})
})
})
await convert('= First Doc', { extension_registry: registry }) (1)
await convert('= Second Doc', { extension_registry: registry }) (2)
// The preprocessor is active for both conversions.
| 1 | First conversion — registry is activated, group block is executed, preprocessor is registered. |
| 2 | Second conversion — registry resets, group block is re-executed, preprocessor is registered again. |
import { Extensions, convert } from '@asciidoctor/core'
const registry = Extensions.create()
// Registering directly on the instance stores the preprocessor in transient state.
registry.preprocessor(function () {
this.process(function (doc, reader) {
// ...
return reader
})
})
await convert('= First Doc', { extension_registry: registry }) (1)
await convert('= Second Doc', { extension_registry: registry }) (2)
// The preprocessor is only active for the first conversion!
| 1 | First conversion — preprocessor is in transient state, it runs. |
| 2 | Second conversion — registry resets, transient state is cleared, preprocessor is gone. |
This behaviour is identical to Ruby Asciidoctor.
A registry is not designed to be reused with direct registrations.
When in doubt, create a new registry per conversion, or use the block form of Extensions.create().
|
Use extensions with the CLI
The CLI provides two options for loading external files:
--extension-
Loads the file and registers it as an Asciidoctor extension. The file must export a
register(registry)function, which the CLI calls with a shared registry before conversion. This is the recommended way to load extensions. -r/--require-
Loads the file as a plain Node.js module, executing any top-level side effects. The CLI does not call any exported function. Use this for libraries that configure themselves on load (syntax highlighter plugins, polyfills, etc.). An extension can also be loaded this way if it self-registers as a side effect by calling
Extensions.register()at the top level, but--extensionis preferred when the file follows theregister(registry)export convention.
Extension file structure
An extension loaded via --extension must export a named register function.
Both ES module and CommonJS formats are supported.
export function register(registry) {
registry.block(function () {
this.named('callout')
this.onContext('paragraph')
this.process(function (parent, reader, attrs) {
const type = attrs.type || 'note'
const lines = reader.getLines()
const html = `<aside class="callout callout-${type}">${lines.join('\n')}</aside>`
return this.createBlock(parent, 'pass', html)
})
})
}
module.exports.register = function (registry) {
registry.block(function () {
// ...
})
}
$ asciidoctor --extension ./callout-block.js my-document.adoc
The --extension option can be repeated to load several extensions:
$ asciidoctor --extension ./callout-block.js --extension ./draft-preprocessor.js my-document.adoc