Converter Templates

All output produced by Asciidoctor is customizable. One way to customize the output is to use a custom converter. Converter templates offer a simpler way.

The built-in converters, and others that have the supports_templates trait enabled, allow you to replace the convert handler for any convertible context with a template. The goal of this mechanism is to make it easier to customize the output of a converter (e.g., HTML) without having to develop, register, and use a custom converter. In other words, you can customize the output of a converter à la carte without having to write code (aside from what’s in the template).

This page explains how the converter template mechanism works in Asciidoctor and how you can make use of it to customize the output of a converter that supports it.

What is a template?

A template is a type of source file that’s focused on producing output. When we use the term template in this context, we’re specifically referring to the source file for a Ruby template engine.

You can think of template as source code in which the output text forms the primary structure and the programming logic is weaved in between. If you come from a programming background, it’s as though the program logic and strings are flipped.

When you write a template, you begin with static text. Then you intersperse logic around regions of that text. This logic allows you to either conditionally select text or repeat it based on information provided by a backing data model. The syntax of that interspersed logic is the template language.

The most common template engine in Ruby is ERB because it’s built into the language. Here’s an example of an ERB template that outputs a paragraph element using the content provided by the backing data model:

<p><%= content %></p>

Some template languages, such as Slim and Haml, are more structured. Here’s the previous example written in Slim:

p =content

and in Haml:

%p =content

Notice there are no angled brackets in these templates. That’s because Slim and Haml assume that the start of each statement is the name of an XML element (Haml requires a leading %). The equals sign tells Slim and Haml to evaluate the Ruby expression that follows and insert the result into the output. In this case, the template is reading the variable content, technically a property of the backing data model. Slim and Haml use indentation to infer nesting in the HTML structure (much like in YAML).

At runtime, the template engine transforms the template into runnable code and invokes it. We refer to this operation as invoking the template. Effectively, this operation invokes the logic and expands variable references in the template to produce the resolved output.

Now that you’re familiar with the basic concept of a template, let’s look at how templates are used in Asciidoctor.

Templates in Asciidoctor

Templates are used in Asciidoctor to customize the output generated by a converter (as long as the converter has the supports_templates trait enabled). We refer to these as converter templates. Converter templates work in conjunction with the converter over which they are applied.

You can reuse the same set of templates you develop for Asciidoctor with AsciidoctorJ, making the templates portable between the two runtimes. Asciidoctor.js provides its own template converter, which means you have to develop a different set of templates if you’re using Asciidoctor.js.

There are three keys points to understand about using converter templates in Asciidoctor:

  • How Asciidoctor selects the template engine to use.

  • What backing data model Asciidoctor passes to the template.

  • The available template names.

Let’s study templates through the lens of Asciidoctor, then explore common APIs, helpers, and debugging.

Template engine selection

Asciidoctor uses Tilt to load and invoke templates. Tilt is a generic interface to multiple Ruby template engines and is provided by the required tilt gem.

You can compose templates in any template language that’s supported by Tilt. If you use a template engine that requires additional libraries (i.e., gems), you must install them first. For example, to use templates written in Haml, you must install the haml gem.

Tilt examines the file extension of the template, matches it to a registered and available template engine, and passes the template to the engine to be invoked. If the file extension is not recognized, or the template engine is not installed, this process will fail.

A template has a double file extension (e.g., .html.haml). The outer file extension is the file extension of the template. In other words, it identifies the template language. The inner file extension is the file extension of the output file. In other words, it identifies the output format.

Let’s assume you have a template named paragraph.html.haml. The .haml file extension tells Tilt to delegate to Haml. The remaining part of the filename (e.g., paragraph.html) would be used as the output file. However, in Asciidoctor, the result of the template isn’t output to a file. Instead, it is combined with the rest of the output of the converter. But the file extension should still match the file extension of the output file that Asciidoctor produces.

You can find a list of template engines that Tilt supports, along with any required libraries, in the Tilt README. The most popular template engines for this purpose are Slim, Haml, and ERB. We strongly recommend using Slim. ERB is also a solid choice since it’s built into the Ruby language and thus doesn’t have any dependencies. You’re encouraged to read the documentation for the template engine of your choice to understand how to use that particular template language.

Available template names

When we talk about the name of a template, we’re talking about the basename of the template minus the double file extension. For example, the name of the template paragraph.html.slim is paragraph. This name is significant because it maps 1-to-1 with the convertible contexts in Asciidoctor (excluding the leading colon). The convertible contexts are roughly the nodes in the parsed AsciiDoc document.

If the name of a template matches the name of a convertible context, Asciidoctor will use that template to produce the output for any node with that context when the convert method is called on that node. You can create a template for as many or as few contexts as you like. If Asciidoctor is unable to locate a template for a convertible context, it will fall back to using the handler provided by the converter. By using templates, you can customize some or all of the output produced by a converter.

Recall that templates can only be used if supported by the converter. All built-in converters in Asciidoctor support the use of templates to customize the converter.

The outer template is either named document or embedded, depending on whether or not the document is being converted in standalone mode. Asciidoctor doesn’t walk the document tree itself and invoke the corresponding templates. Rather, it’s up to the template to trigger the other templates by invoking the content method on block nodes. So, if you replace the document or embedded template, and don’t invoke the content method, that explains why no other templates get called.

Let’s look at the backend data model to understand the logic that can be used in a template.

Backing data model

The backing data model for a template in Asciidoctor is always the node being converted (specifically an AbstractNode). (A node in the Asciidoctor document model is similar to an XML DOM node). For example, when writing a template for a paragraph, the backing data model is an instance of Block with the context :paragraph.

You can access the node itself using the self keyword.

- puts self

In a Slim template, a line that starts with - executes a Ruby statement. An expression that starts with = (either at the start of the line or following a tag name) invokes a method and inserts the return value into the template.

Within the template, you can access all the instance variables and methods of the node using the name of that member, just as you would inside a method call (e.g., @id or title). (You can think of the template as a method call on the node object). When referencing an accessor method, the name of the member is synonymous with a template variable.

Accessing instance variables of the node from the template (e.g., @id) is generally discouraged as it tightly couples your template to the internal model. It’s better to stick with using public accessors and methods.

To access the converted content of the node, you use the template variable content.

p =content

The syntax =content invokes the content accessor method on the node and inserts the result into the template.

You can access the document for all nodes from the template using the document property. The document is most commonly used for looking up document attributes, as shown here:

p lang=(document.attr 'lang') =content

You can also use the document object to lookup other nodes in the document using Document#find_by.

Let’s look at a complete example of a paragraph template that mimics the output of the built-in HTML converter.

div id=id class=['paragraph', role]
  - if title?
    .title =title
  p =content

Assuming id is "hello", title is "Hello, World!", role is nil, and content is "Your first template!", this template will produce the following HTML:

<div id="hello" class="paragraph">
  <div class="title">Hello, World!</div>
  <p>Your first template!</p>

This template uses the id, role, title, and content properties, as well as the title? method. You may notice that Slim infers some logic for you. If the role is not set, it will drop that entry from the array, join the remaining entries on a space, and output the class attribute. If the id property were nil instead of "hello", the template will not output the id attribute.

To help you understand what properties and methods are available on a node, you can print them using the following expression:

- pp (public_methods - Object.instance_methods).reject {|it| it.end_with? '=' }

All properties are reported as methods, which is why this statement uses public_methods.

You can inspect all the attributes available on the current node and current document as follows:

- pp attributes
- pp document.attributes

To discover more about these properties and methods, and what they return, refer to the API docs. Also refer to Debugging to learn more about how to inspect the backing data model.

Common APIs

Each node shares a common set of properties, such as id, role, attributes, context, its parent node, and the document node. Block and inline nodes have additional properties that are specific to their purpose. For example, a block node has a content property to access its converted content, a list node has an items property to access the list items, and an inline node has a text property to access the converted text.

Each template has access to any API in the document model that is accessible from the node being converted. The following table provides a list of the APIs you’ll likely use most often.

Name Example (Slim) Description


- if document.attr 'icons', 'font'

A reference to the current document (and all of its nodes).



Converts the children of this block node (if any) and returns the result.


- items.each do |item|

Provides access to the items in a list node. Note that the list template must process its own items.



Returns the converted text of this inline node.



Returns the converted target of this inline node, if applicable (e.g., anchor, image).


div id=id

The id assigned to the block or nil if no id is assigned.


div class=role

A convenience method that returns the role attribute for the block, or nil if the block does not have a role.


if role? 'lead'

A convenience method to check whether the block has the role attribute.


div class=(attr 'toc-class', 'toc')

Retrieves the value of the specified attribute on the element, using the name as a key. If the name is written as a symbol, it will be automatically converted to a string before lookup. The second argument is a fallback value if the attribute is not set.


- if attr? 'icons'

Checks whether the specified attribute exists on the element, using the name as the key. If the name is written as a symbol, it will be automatically converted to a string before lookup. If the second argument is provided (a match), it additionally checks whether the attribute value matches the specified value.


- if style == 'source'

Retrieves the style (qualifier) for a block node. If the block does not have a style, nil is returned.



Retrieves the title of the block with normal substitutions (escape XML, render links, etc) applied.


- if title?

Checks whether a title has been assigned to this block. This method does not have side effects (e.g., checks for existence only, does not apply substitutions)



Retrieves the title of the block with caption and normal substitutions (escape XML, render links, etc) applied.


video autoplay=(option? 'autoplay')

A convenience method to check whether the specified option attribute (e.g., autoplay-option) is present.


- if type == :xref

Returns the node variant for inline nodes that have variants (e.g., anchor, quoted, etc).


img src=(image_uri attr 'target')

Converts the path into an image URI (reference or embedded data) to be used in an HTML img element. Applies security restrictions, cleans path and can embed image data if :data-uri: attribute is enabled on document. Always use this method when dealing with image references. Relative image paths are resolved relative to document directory unless overridden using :imagesdir:


img src=(icon_uri attr 'target')

Same as image_uri except it specifically works with icons. By default, it will look in the subdirectory images/icons, unless overridden using :iconsdir:


audio src=(media_uri attr 'target')

Similar to image_uri, except it does not support embedding the data into the document. Intended for video and audio paths.


link href=(normalize_web_path attr 'stylesheet')

Joins the path to the relative_root and normalizes parent and self references. Access to parent directories may be restricted based on safe mode setting.


As previously stated, the backing data model for a template primarily consists of the properties and methods of the node being converted. Helpers provided by the template engine are also available as top-level functions in the template. Refer to the documentation for the template engine for details.

If you find yourself putting a lot of logic in the template, you may want to extract that logic into custom helper functions. When using Haml or Slim, you can define these helper functions in the file helper.rb located in the same folder as the templates. These helper functions can simplify reoccurring elements that appear across multiple templates.

The helper file must define the Ruby module Haml::Helpers or Slim::Helpers, depending on which template engine your templates target. Every method defined in that module becomes a top-level function in the template. The method is effectively mixed into the node, so the self reference in the function is the node itself.

Helpers provided by the template engine are also available as top-level functions. For example, Haml provides the html_tag helper for creating an HTML element dynamically. Refer to the documentation for the template engine for details.

Let’s assume that we’re creating a template for sections, and we want to output the section title with the section number, but only if automatic section numbering is enabled. We can create a helper function for this purpose:

module Slim::Helpers
  def section_title
    if caption
    elsif numbered && level <= (document.attr :sectnumlevels, 3).to_i
      if level < 2 && document.doctype == 'book'
        case sectname
        when 'chapter'
          %(#{(signifier = document.attr 'chapter-signifier') ? signifier.to_s + ' ' : ''}#{sectnum} #{title})
        when 'part'
          %(#{(signifier = document.attr 'part-signifier') ? signifier.to_s + ' ' : ''}#{sectnum nil, ':'} #{title})
          %(#{sectnum} #{title})
        %(#{sectnum} #{title})

You can now use this helper in your section template as follows:

*{ tag: %(h#{level + 1}) } =section_title (1)
=content (2)
1 We’re leveraging a special syntax in Slim to create the HTML heading element dynamically.
2 It’s necessary to invoke the content method to convert the child nodes of a node that contains other nodes.

If you prefer your helpers to be pure functions, you can pass in the node as the first argument and only use that reference to access properties of the backing data model.

helpers.rb using pure functions
module Slim::Helpers
  def section_title node = self
    if node.caption
    elsif node.numbered && node.level <= (node.document.attr :sectnumlevels, 3).to_i

Which style you use to write your helpers is up to you. But if you find that you need to reuse a function for different scenarios, you might find the investment in pure functions to be worthwhile.


There are two approaches to debug a template by exploring the backing model:

  • print messages and return values to STDOUT using puts or pp

  • jump into the context of the template using an interactive debugger

To print the current node in string form to STDOUT, you can use the following statement in your template:

- puts self

You can print structured information about the current node using pp:

- pp self

However, since a node has circular references, that output can be extremely verbose. You might find it more useful to print more specific information.

You can see what attributes are available on the current node and document using these statements:

- pp attributes
- pp document.attributes

You can see what properties and methods are available on a node using the following expression:

- pp (public_methods - Object.instance_methods).reject {|it| it.end_with? '=' }

Using print statements, you have to update the template and rerun Asciidoctor each time you want to further your inspection. A more efficient approach is to use an interactive debugger.

Use an interactive debugger

Pry is a powerful debugger for Ruby that features syntax highlighting, tab-completion, and documentation and source code browsing. You can use it to interactively discover the object hierarchy of the backing model available to an Asciidoctor template.

To use Pry, you first need to install it, either using gem install:

$ gem install pry

or by adding it to your Gemfile and running bundle.

In order to be dropped into the debugger at a specific point in a template, add the following two lines to the template you want to inspect:

- require 'pry'
- binding.pry

When you run Asciidoctor, it will pause in the template and give you an interactive console.

From: /path/to/templates/html5/paragraph.html.slim:7 self.__tilt_800:

    1: - require 'pry'
 => 2: - binding.pry

[1] pry(#<Asciidoctor::Block>)>

From there, you can inspect the objects in the backend model.

[1] pry(#<Asciidoctor::Block>)> attributes

You can also query Asciidoctor’s API documentation:

[1] pry(#<Asciidoctor::Block>)> ? find_by

Type exit to leave the interactive console:

[1] pry(#<Asciidoctor::Block>)> exit

To learn more about what you can do with Pry, we recommend watching the introductory screencast. Refer to the Pry wiki for details about how to use it.

How to use templates

Now that you know what templates are and how to make them, let’s look at how to use them in Asciidoctor.

Organize your templates

You should group templates for a specific backend together in a single folder. In that folder, each template file should be named using the pattern <context><output-ext><template-ext>, where context is the name of a convertible context, output-ext is the file extension of the output file, and template-ext is the file extension for the template language (e.g., paragraph.html.slim). Those are the only requirement you have to follow in order for Asciidoctor to discover and load your templates.

If you’re creating templates for multiple backends, you may decide to further group your templates in folders named after the backend (and perhaps even an additional folder for the template language, not shown here).

📒 templates (1)
  📂 html5 (2)
    📄 paragraph.html.slim (3)
1 The folder containing the templates for various backends.
2 The folder containing the templates for the html5 backend.
3 The converter template for paragraphs.

If you’re only targeting a single backend, you can simply name the folder templates.

📒 templates
  📄 paragraph.html.slim

Recall that the backend is a moniker for the expected output format, and in turn, the converter that produces it.

Install the template engine

To use converter templates, you must always install the tilt gem. If you’re using a template engine that has one or more required libraries, you must first install those libraries. Once the library is installed, Asciidoctor will use Tilt to load it on demand.

If you write your templates in ERB, no additional libraries are required.

Let’s assume you’re writing your templates in Slim (which the template engine we most recommend). You will need both the tilt and slim gems installed.

If you’re using Bundler, you install gems first by declaring them in Gemfile.

gem 'tilt'
gem 'slim'

Then, you install the gems using Bundler:

$ bundle

If you’re not using Bundler, and you have configured Ruby to install gems in your user/home directory, then you can use the gem command instead:

$ gem install tilt slim

Either way, the tilt and slim gems must be available on the load path when running Asciidoctor in order to use templates written in the Slim template language.

Apply your templates

Instructing Asciidoctor to apply your templates is the easiest part. You only need to tell Asciidoctor where the templates are located and which template engine you’re using. (Technically, you don’t need to specify the template engine. But, by doing so, it makes the scan more efficient and deterministic.)

If you’re using the CLI, you specify the template directory using the -T option (longhand: --template-dir) and the template engine using the -E option (longhand: --template-engine).

$ asciidoctor -T /path/to/templates -E slim doc.adoc

If you’re using the API, you specify the template directory (or directories) using the :template_dirs option and the template engine using the :template_engine option.

Asciidoctor.convert_file 'doc.adoc', safe: :safe,
  template_dirs: ['/path/to/templates'], template_engine: 'slim'

Notice that we didn’t specify the segment html5 in the path where the templates are located. That’s because Asciidoctor automatically looks for a folder that matches the backend name when scanning for templates (e.g., /path/to/templates/html5). However, you can include the segment for the backend in the path if you prefer.

Use multiple template directories

You can distribute templates for a single backend across multiple directories. For example, you may have a set of common templates for all projects (e.g., /path/to/common-templates) and a set of specialized templates that supplement and/or override those templates for a particular project (e.g., /path/to/specialized-templates).

To load templates from multiple directories when using the CLI, you can pass each directory to Asciidoctor by specifying the -T option multiple times:

$ asciidoctor -T /path/to/common-templates -T /path/to/specialized-templates -E slim doc.adoc

When using the API, you add all the template directories to the array value of the :template_dirs option:

Asciidoctor.convert_file 'doc.adoc', safe: :safe,
  template_dirs: ['/path/to/common-templates', '/path/to/specialized-templates'], template_engine: 'slim'

In both cases, if the same template is found in more than one location, the template discovered in the directory listed later in the list will be used.

Tutorial: Your first converter template

This section provides a tutorial you can follow to quickly learn how to write and use your first converter template. In this tutorial, you’ll create a converter template to customize the HTML produced by the built-in HTML converter for unordered lists. You’ll compose the template in the Slim template language. You’ll then observe the result of this customization by using the template when converting the AsciiDoc document to HTML with Asciidoctor.

Add and install required gems

You first need to install the required libraries (i.e., gems) for the template engine. Since you’ll be using Slim, you need to install the slim gem. You also need to install the tilt gem, which provides Tilt, the generic interface for Ruby template engines that Asciidoctor uses to load and invoke templates. Although not required, Asciidoctor will prompt you to also install the concurrent-ruby gem to properly implement the template cache.

The preferred way of installing a gem is to add it to the Gemfile in your project.

source ''

gem 'asciidoctor'
# ...any other gems you are using
gem 'tilt'
gem 'slim'
gem 'concurrent-ruby'

Run Bundler to install the gems into your project.

$ bundle

If you’re not using Bundler, and you have configured Ruby to install gems in your user/home directory, then you can use the gem command instead:

$ gem install tilt slim concurrent-ruby

Now that you’ve installed Tilt and the Slim template engine, you can get started writing templates.

Create templates folder

Next, create a new folder named templates to store your templates. We also recommend creating a nested folder named html5 to organize the templates by backend.

$ mkdir -p templates/html5

You can further organize templates into folders by engine, though that’s not required.

Compose a template

Let’s compose the template to customize the HTML for unordered lists. Since the context for unordered lists is :ulist (see Convertible Contexts), you’ll name the template ulist.html.slim.

- if title?
  figure.list.unordered id=id
    ul class=[style, role]
      - items.each do |_item|
          - if _item.blocks?
- else
  ul id=id class=[style, role]
    - items.each do |_item|
        - if _item.blocks?

Apply the templates

The final step is to use the templates when you invoke Asciidoctor. Create an AsciiDoc file named doc.adoc that contains an unordered list.

* cats
* dogs
* birds

You can now instruct Asciidoctor to convert this list using your template by passing the directory containing the templates using the -T option and the name of the template engine using the -E option. If you used Bundler to install gems, run Asciidoctor as follows:

$ bundle exec asciidoctor -T templates -E slim doc.adoc

Otherwise, you can drop the bundle exec prefix:

$ asciidoctor -T templates -E slim doc.adoc

Now that you’ve created your first converter template, you’re well on your way to customizing the HTML that Asciidoctor produces to suit your own needs!

A quick review

Here’s a quick review for how to start using templates written in Slim to customize the output of the built-in HTML 5 converter.

  1. Install the tilt, slim, and concurrent-ruby gems using bundle or gem install.

  2. Create a folder named templates/html5 to store the templates.

  3. Create a template named paragraph.html.slim in that folder.

  4. Populate the template with your own template logic. Here’s a simple example:

    p id=id role=role =content
  5. Load the templates using the -T flag from the CLI:

    $ bundle exec asciidoctor -T path/to/templates -E slim doc.adoc


    $ asciidoctor -T path/to/templates -E slim doc.adoc
When invoking Asciidoctor via the API, you load the templates by passing the path to the :templates option.

We hope you’ll agree that using templates makes it easy to customize the output that Asciidoctor produces.