Custom Syntax Highlighter Adapter

You can integrate additional syntax highlighters into Asciidoctor by implementing and registering a syntax highlighter adapter. You can either write a new adapter from scratch or you can extend and even replace one of the built-in adapters.

Create a new adapter

To implement a new adapter, you must create a class that extends the Asciidoctor::SyntaxHighlighter::Base class, register the adapter for a value of the source-highlighter attribute, and implement the required methods. Which methods are required depends on whether the adapter is for a client-side (runs in the browser) or build-time (runs when the document is converted) syntax highlighter.

Here’s an example of how to write and register a syntax highlighter adapter for the Prism.js syntax highlighting library. Prism.js is a client-side syntax highlighter, meaning it runs in the browser. That means the adapter only has to implement methods that pertain to client-side syntax highlighting, which include format, docinfo?, and docinfo.

Example 1. Syntax highlighter adapter for Prism.js
class PrismSyntaxHighlighter < Asciidoctor::SyntaxHighlighter::Base
  register_for 'prism'

  def format node, lang, opts
    opts[:transform] = proc do |pre, code|
      code['class'] = %(language-#{lang}) if lang
    end
    super
  end

  def docinfo? location
    location == :footer
  end

  def docinfo location, doc, opts
    base_url = doc.attr 'prismdir', %(#{opts[:cdn_base_url]}/prism/1.15.0)
    slash = opts[:self_closing_tag_slash]
    unless (theme_name = doc.attr 'prism-style', 'prism') == 'prism'
      theme_name = %(prism-#{theme_name})
    end
    %(<link rel="stylesheet" href="#{base_url}/themes/#{theme_name}.min.css"#{slash}>
<script src="#{base_url}/prism.min.js"></script>
<script src="#{base_url}/components/prism-ruby.min.js"></script>)
  end
end

Save this code to a file named prism-syntax-highlighter.rb. Then, require this file when invoking Asciidoctor and set source-highlighter=prism to activate it:

$ asciidoctor -r ./prism-syntax-highlighter -a source-highlighter=prism document.adoc

You can also define an adapter for a syntax highlighter that runs during conversion. We’ll look at doing that while also extending a built-in adapter.

Extend an existing adapter

Instead of creating a new adapter, you can customize a built-in adapter by extending it, overriding its behavior, and optionally replacing it.

To extend an adapter, you need to look up a reference to the built-in adapter by name using the Asciidoctor::SyntaxHighlighter.for method, create a class that extends it, register the adapter with a unique name (or the same name, if you want to replace it), and override any methods that provide the behavior you want to modify.

Here’s the basic template for customizing an existing adapter:

class CustomAdapter < (Asciidoctor::SyntaxHighlighter.for 'rouge')
  register_for 'rouge'

  # override methods go here
end

Let’s look at some examples of how to customize a built-in adapter.

docinfo

Let’s override the adapter for Pygments to prevent it from adding a stylesheet to the HTML (presumably because the styles will be provided by a different stylesheet).

Example 2. Extended syntax highlighter adapter for Pygments
class ExtendedPygmentsSyntaxHighlighter < (Asciidoctor::SyntaxHighlighter.for 'pygments')
  register_for 'pygments'

  def docinfo? location
    false
  end
end

Save this code to a file named extended-pygments-syntax-highlighter.rb. Then, require this file when invoking Asciidoctor, setting source-highlighter=pygments to activate it, as you would normally do:

$ asciidoctor -r ./extended-pygments-syntax-highlighter.rb -a source-highlighter=pygments document.adoc

If, instead, you wanted to modify the built-in adapter to honor the location of a custom stylesheet specified by the pygments-stylesheet attribute, you can do so by extending the adapter and overriding the docinfo method.

class ExtendedPygmentsSyntaxHighlighter < (Asciidoctor::SyntaxHighlighter.for 'pygments')
  register_for :pygments

  def docinfo location, doc, opts
    stylesheet = doc.attr 'pygments-stylesheet', './pygments.css'
    if opts[:linkcss]
      slash = opts[:self_closing_tag_slash]
      %(<link rel="stylesheet" href="#{stylesheet}"#{slash}>)
    else
      stylesheet = doc.normalize_system_path stylesheet
      %(<style>
#{doc.read_asset stylesheet, label: 'stylesheet', normalize: true}
</style>)
    end
  end
end

If you want to decorate built-in behavior, you can invoke the super method anywhere inside the method to delegate to the behavior provided by the built-in adapter.

highlight

Let’s say you always want lines to be numbered, regardless of the setting in the document. You can do so by overriding the highlight method, setting the :number_lines key on the opts argument, then delegating back to the built-in adapter using super.

def highlight node, source, lang, opts
  opts[:number_lines] = true
  super
end

create_formatter (Rouge)

When using Rouge as the syntax highlighter, you can customize the formatter by overriding the create_formatter method. This allows you to add custom logic for handling certain tokens in the source language.

Let’s assume that you want to look for bare URLs in code comments and translate them into links (i.e., autolinks), just like in AsciiDoc. You can do that by weaving extra logic into the formatter that looks for tokens in the Comment category and applies a substitution to the value.

class ExtendedRougeSyntaxHighlighter < (Asciidoctor::SyntaxHighlighter.for 'rouge')
  register_for 'rouge'

  def create_formatter node, source, lang, opts
    formatter = super
    formatter.singleton_class.prepend (Module.new do
      def safe_span tok, safe_val
        if tok.token_chain[0].matches? ::Rouge::Token::Tokens::Comment
          safe_val = safe_val.gsub(/https?:\/\/\S+/, '<a href="\&">\&</a>')
        end
        super
      end
    end)
    formatter
  end
end

Since the formatter has access to all the tokens in the code identified by the syntax highlighter, this technique opens up a lot of possibilities. For example, you could look for the Type token in the Keyword category in Java code and create a link to the API docs. The lang argument to the create_formatter method lets you know the source language (e.g., java) to which the tokens belong.

To study the logic you may be interesting in overriding, browse the code for the built-in syntax highlighter adapters.