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 |
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:
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 |
|---|---|---|
|
(unset) |
Set to |
|
|
Any theme name from the highlight.js theme gallery (e.g. |
|
|
|
|
Line numbers render as a two-column CSS grid — an auto-sized gutter of |
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', '?')
----
-
linenumsadds line numbers (one row per line, number in a left gutter); add thenowrapoption (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.