Custom Syntax Highlighter

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

Overview

A syntax highlighter controls how source blocks are rendered. Asciidoctor.js ships with a highlight.js adapter that runs client-side by default and at conversion time when :highlightjs-mode: build is set (see Syntax Highlighting). You can provide your own implementation by extending SyntaxHighlighterBase.

There are two usage patterns:

Server-side highlighting

The highlighter processes the source at conversion time. Override handlesHighlighting()true and implement highlight().

Client-side highlighting

The highlighter injects scripts or stylesheets into the output document. Override hasDocinfo()true and implement docinfo().

Both patterns can also override format() to control the <pre><code> wrapper.

Server-side highlighting

In this pattern, the highlight() method receives the raw source text and returns the highlighted markup.

import { SyntaxHighlighterBase } from '@asciidoctor/core'

class UpperCaseHighlighter extends SyntaxHighlighterBase { (1)
  handlesHighlighting() {
    return true (2)
  }

  async highlight(node, source, lang, opts) {
    return source.toUpperCase() (3)
  }
}
1 Extend SyntaxHighlighterBase to inherit the default format() implementation.
2 Return true to tell Asciidoctor.js that this highlighter processes source at conversion time.
3 source is the raw source text; return the highlighted markup as a string. Return a [string, number] tuple if the source shifts by one or more lines (e.g. a line-numbers table is prepended); the number is the character offset at which the code lines begin, used to re-align callouts.

highlight() may be async — the core `await`s its result — so you can load a highlighting library on demand (e.g. an async engine such as Shiki or starry-night).

The opts argument carries the block’s highlighting options, including callouts (callout marks, already stripped from source), highlightLines (1-based line numbers to emphasize), numberLines ('table' or 'inline') and startLineNumber. Callouts are handled by the core: they are removed before highlight() runs and re-inserted line by line afterwards, so your highlighter should preserve line boundaries (do not add or remove lines unless you report the offset via the tuple).

Before writing your own, consider the built-in build mode: :highlightjs-mode: build gives you conversion-time highlight.js with callouts, line numbers, emphasized lines and theme embedding out of the box. See Build-time highlighting.

Client-side highlighting

In this pattern, the highlighter injects a script or stylesheet into the output document and leaves the source block as-is for the browser to process.

import { SyntaxHighlighterBase } from '@asciidoctor/core'

class PrismHighlighter extends SyntaxHighlighterBase {
  hasDocinfo(location) {
    return location === 'footer' (1)
  }

  docinfo(location, doc, opts) {
    return '<script src="https://cdn.example.com/prism.js"></script>' (2)
  }
}
1 Return true for the location slots where this highlighter needs to inject markup ('head' or 'footer').
2 Return the HTML markup to inject at the given location.

Like highlight(), docinfo() may be async — the core `await`s its result — so it can, for example, read a stylesheet from disk and inline it.

Customising the <pre><code> wrapper

Both patterns use the format() method to wrap the source in <pre><code> tags. The base class provides a default implementation; override it to change the wrapper.

format() may return a plain string or a Promise<string> — the caller always `await`s the result.

import { SyntaxHighlighterBase } from '@asciidoctor/core'

class MinimalHighlighter extends SyntaxHighlighterBase {
  async format(node, lang, opts) {
    const content = await node.content() (1)
    return `<pre><code>${content}</code></pre>` (2)
  }
}
1 Call node.content() to get the (possibly highlighted) source with all substitutions applied.
2 Return the final HTML wrapping the source block.

Registering a custom syntax highlighter

Per-conversion override with syntax_highlighters

Pass a syntax_highlighters map to load() or convert() to override a specific highlighter name for a single conversion. Setting a name to null disables that highlighter.

import { load } from '@asciidoctor/core'

const doc = await load(input, {
  safe: 'safe',
  syntax_highlighters: { 'my-hl': UpperCaseHighlighter }, (1)
  attributes: { 'source-highlighter': 'my-hl' },
})
1 Map the name used in :source-highlighter: to your class. Other highlighters (e.g. highlightjs) remain available.

To disable a highlighter:

const doc = await load(input, {
  safe: 'safe',
  syntax_highlighters: { 'highlightjs': null }, (1)
  attributes: { 'source-highlighter': 'highlightjs' },
})
// doc.syntaxHighlighter === null
1 Setting a name to null prevents the highlighter from being resolved.

Global registration with a custom factory

For full control over highlighter resolution, pass a syntax_highlighter_factory instance.

import { load, CustomFactory } from '@asciidoctor/core'

const factory = new CustomFactory({ 'my-hl': UpperCaseHighlighter }) (1)

const doc = await load(input, {
  safe: 'safe',
  syntax_highlighter_factory: factory,
  attributes: { 'source-highlighter': 'my-hl' },
})
1 CustomFactory takes a seed registry as a plain object. Only the names registered in this factory are available — built-in adapters are not inherited. Use syntax_highlighters instead if you want to keep built-in adapters available.