Syntax Highlighting

This page explains how to enable syntax highlighting for source blocks in Asciidoctor.js.

Default behavior

Without a syntax highlighter configured, Asciidoctor.js renders a source block like this:

[source,java]
----
public class Kalle {
  public Kalle(final String pelle) {}
}
----

as plain HTML with language metadata on the <code> element:

<div class="content">
  <pre class="highlight">
    <code class="language-java" data-lang="java">public class Kalle {
  public Kalle(final String pelle) {}
}</code>
  </pre>
</div>

The class and data-lang attributes are present, but there is no highlighted markup. To get visual syntax highlighting you need either a client-side library that runs in the browser, or a server-side highlighter that processes the source at conversion time.

Client-side highlighting with highlight.js

Asciidoctor.js ships with a built-in highlight.js adapter. When enabled, Asciidoctor.js injects the highlight.js stylesheet and script into the output document, and the browser applies highlighting when the page loads.

Standalone documents

To use the built-in adapter, convert with standalone: true and set the source-highlighter attribute to highlightjs (or highlight.js):

import { convert } from '@asciidoctor/core'

const content = `= Sample Java
[source,java]
----
public class Kalle {
  public Kalle(final String pelle) {}
}
----`

const html = await convert(content, {
  standalone: true, (1)
  attributes: { 'source-highlighter': 'highlightjs' }, (2)
})
1 The standalone option wraps the body in a full HTML document (<html>, <head>, <body>). The highlight.js <link> and <script> tags are injected via the docinfo mechanism, which only runs in standalone mode.
2 Tells Asciidoctor.js to use the built-in highlight.js adapter.

The generated document loads highlight.js from a CDN and calls hljs.highlightBlock() on every pre.highlight > code[data-lang] element.

If you set source-highlighter but omit standalone: true (which is the default for convert()), the docinfo scripts are not injected and highlighting will not work.

Customizing the theme

Override the default github theme by setting the highlightjs-theme attribute:

await convert(content, {
  standalone: true,
  attributes: {
    'source-highlighter': 'highlightjs',
    'highlightjs-theme': 'monokai',
  },
})

Any theme name from the highlight.js theme gallery is accepted.

Self-hosted highlight.js

To serve highlight.js from your own host instead of the CDN, set the highlightjsdir attribute to the base URL of your installation:

await convert(content, {
  standalone: true,
  attributes: {
    'source-highlighter': 'highlightjs',
    'highlightjsdir': '/assets/highlight.js',
  },
})

Asciidoctor.js will load styles/github.min.css and highlight.min.js relative to that URL.

Embedding in an existing page

If you are inserting the converted fragment into an existing HTML page (i.e. without standalone: true), you must include highlight.js yourself and initialize it after inserting the fragment:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"></script>
import { convert } from '@asciidoctor/core'

const html = await convert(content) // standalone is false by default

document.getElementById('content').innerHTML = html

// Highlight all source blocks that Asciidoctor.js produced
document.querySelectorAll('pre.highlight > code[data-lang]').forEach((el) => {
  hljs.highlightBlock(el)
})

Build-time (server-side) highlighting with highlight.js

Build-time highlighting processes the source at conversion time, so the highlighted markup is embedded directly in the output HTML. The result is self-contained: no highlight.js script runs in the browser, and no client-side JavaScript is required to see the colors.

The built-in highlightjs adapter can run in this mode. Enable it by setting the highlightjs-mode attribute to build:

import { convert } from '@asciidoctor/core'

const content = `= Sample Java
[source,java]
----
public class Kalle {
  public Kalle(final String pelle) {}
}
----`

const html = await convert(content, {
  standalone: true, (1)
  attributes: {
    'source-highlighter': 'highlightjs', (2)
    'highlightjs-mode': 'build', (3)
  },
})
1 The theme stylesheet is injected via the docinfo mechanism, which only runs in standalone mode. The highlighted markup itself is produced regardless of standalone — see Embedding in an existing page.
2 Use the built-in highlight.js adapter.
3 Colorize source blocks at conversion time instead of in the browser.

Instead of the raw source, the <code> element now contains highlight.js markup (<span class="hljs-…">…), and the theme stylesheet is embedded in a <style> tag in the document head.

highlight.js is an optional peer dependency: it is not installed with Asciidoctor.js. To use build mode, install it in your project:

$ npm install highlight.js

It is loaded only when build mode is used. If it is missing, conversion fails with a clear error. Build mode runs on server runtimes (Node.js, Deno, Bun). In the browser it is not available: the adapter logs a warning and falls back to client-side highlighting.

Options

Build mode is configured with the following document attributes:

Attribute Default Description

highlightjs-mode

(unset)

Set to build to highlight at conversion time. When unset, the adapter highlights client-side (see above).

highlightjs-theme

github

Any theme name from the highlight.js theme gallery (e.g. github-dark, monokai).

highlightjs-stylesheet

embed

embed reads the theme CSS from the installed highlight.js package and inlines it in a <style> tag (self-contained output). link emits a <link> to the CDN (or to highlightjsdir) instead. If a theme cannot be read in embed mode (e.g. an unknown theme name), a warning is logged and the CDN stylesheet is linked instead.

Line numbers render as a two-column CSS grid — an auto-sized gutter of counter()-generated numbers and the code — with no <table> and no number text in the DOM (so numbers are never selected or copied). Whether a long line wraps (staying clear of the number gutter) or scrolls horizontally follows the block’s standard nowrap option (or the prewrap document attribute) — the same control as any other source block. The highlightjs-linenums-mode attribute (Rouge’s table/inline distinction) is therefore not used by this adapter.

The usual source block features are supported through the standard AsciiDoc syntax:

[source,ruby,linenums,highlight=2..3,start=10]
----
require 'json'
data = JSON.parse(input)
puts data.fetch('name', '?')
----
  • linenums adds line numbers (one row per line, number in a left gutter); add the nowrap option (e.g. [source%nowrap,ruby,linenums]) to scroll long lines instead of wrapping them.

  • highlight= emphasizes one or more lines or ranges (e.g. highlight=2..3).

  • start= sets the first line number.

  • Callouts (<1>, <.>, …) are stripped before highlighting and re-inserted afterwards, so a conum never ends up colored as part of the code — even inside a multi-line comment.

Embedding in an existing page

The highlighted markup is embedded in the <code> element whether or not you convert in standalone mode, so highlighting works even when you insert the fragment into an existing page. Only the theme stylesheet requires standalone: true (it is added via docinfo). When embedding a fragment, add the theme stylesheet to your page yourself, for example:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css">

Unlike client-side mode, you do not need to load the highlight.js script or call hljs.highlightBlock() — the colors are already in the markup.

Implementing your own server-side highlighter

To highlight with a different library, extend SyntaxHighlighterBase and override handlesHighlighting()true and highlight(). Here is a minimal example:

import { load, SyntaxHighlighterBase } from '@asciidoctor/core'
import hljs from 'highlight.js' // npm install highlight.js

class HljsServerHighlighter extends SyntaxHighlighterBase {
  handlesHighlighting() {
    return true (1)
  }

  async highlight(node, source, lang, opts) {
    if (lang && hljs.getLanguage(lang)) {
      return hljs.highlight(source, { language: lang }).value (2)
    }
    return hljs.highlightAuto(source).value
  }
}

const doc = await load(content, {
  safe: 'safe',
  syntax_highlighters: { 'hljs-server': HljsServerHighlighter }, (3)
  attributes: { 'source-highlighter': 'hljs-server' },
})
const html = await doc.convert()
1 Returning true tells Asciidoctor.js that this highlighter processes source at conversion time.
2 source is the raw source text; return the highlighted markup as a string. highlight() may be async (the return value is awaited).
3 Register the highlighter under the name used in source-highlighter.

See Custom Syntax Highlighter for the full API.