This documentation covers a prerelease version of the software. Follow this link to view the documentation for the stable version (3.0) instead.

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.

Safe — block is stored as a group and re-executed on every conversion
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.
Unsafe — direct registration is lost after the first conversion
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 --extension is preferred when the file follows the register(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.

ES module (recommended)
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)
    })
  })
}
CommonJS
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