83. Example Extensions

Below are several examples of extensions and how they are registered.

83.1. Preprocessor Example

Purpose

Skim off front matter from the top of the document that gets used by site generators like Jekyll and Awestruct.

sample-with-front-matter.adoc
---
tags: [announcement, website]
---
= Document Title

content

[subs="attributes,specialcharacters"]
.Captured front matter
....
---
{front-matter}
---
....
FrontMatterPreprocessor
require 'asciidoctor'
require 'asciidoctor/extensions'

class FrontMatterPreprocessor < Asciidoctor::Extensions::Preprocessor
  def process document, reader
    lines = reader.lines # get raw lines
    return reader if lines.empty?
    front_matter = []
    if lines.first.chomp == '---'
      original_lines = lines.dup
      lines.shift
      while !lines.empty? && lines.first.chomp != '---'
        front_matter << lines.shift
      end

      if (first = lines.first).nil? || first.chomp != '---'
        lines = original_lines
      else
        lines.shift
        document.attributes['front-matter'] = front_matter.join.chomp
        # advance the reader by the number of lines taken
        (front_matter.length + 2).times { reader.advance }
      end
    end
    reader
  end
end
Usage
Asciidoctor::Extensions.register do
  preprocessor FrontMatterPreprocessor
end

Asciidoctor.convert_file 'sample-with-front-matter.adoc', :safe => :safe

83.2. Tree Processor Example

Purpose

Detect literal blocks that contain shell commands, strip the prompt character and style the command using CSS in such a way that the prompt character cannot be selected (as seen on help.github.com).

sample-with-shell-session.adoc
 $ echo "Hello, World!"
 > Hello, World!

 $ gem install asciidoctor
ShellSessionTreeProcessor
class ShellSessionTreeProcessor < Asciidoctor::Extensions::TreeProcessor
  def process document
    return unless document.blocks?
    process_blocks document
    nil
  end

  def process_blocks node
    node.blocks.each_with_index do |block, i|
      if block.context == :literal &&
          (((first_line = block.lines.first).start_with? '$ ') ||
            (first_line.start_with? '> '))
        node.blocks[i] = convert_to_terminal_listing block
      else
        process_blocks block if block.blocks?
      end
    end
  end

  def convert_to_terminal_listing block
    attrs = block.attributes
    attrs['role'] = 'terminal'
    prompt_attr = (attrs.has_key? 'prompt') ?
        %( data-prompt="#{block.sub_specialchars attrs['prompt']}") : nil
    lines = block.lines.map do |line|
      line = block.sub_specialchars line.chomp
      if line.start_with? '$ '
        %(<span class="command"#{prompt_attr}>#{line[2..-1]}</span>)
      elsif line.start_with? '&gt; '
        %(<span class="output">#{line[5..-1]}</span>)
      else
        line
      end
    end
    create_listing_block block.document, lines * EOL, attrs, subs: nil
  end
end
Usage
Asciidoctor::Extensions.register do
  treeprocessor ShellSessionTreeProcessor
end

Asciidoctor.convert_file 'sample-with-shell-session.adoc', :safe => :safe

83.3. Postprocessor Example

Purpose

Insert copyright text in the footer.

CopyrightFooterPostprocessor
class CopyrightFooterPostprocessor < Asciidoctor::Extensions::Postprocessor
  def process document, output
    content = (document.attr 'copyright') || 'Copyright Acme, Inc.'
    if document.basebackend? 'html'
      replacement = %(<div id="footer-text">\\1<br>\n#{content}\n</div>)
      output = output.sub(/<div id="footer-text">(.*?)<\/div>/m, replacement)
    elsif document.basebackend? 'docbook'
      replacement = %(<simpara>#{content}</simpara>\n\\1)
      output = output.sub(/(<\/(?:article|book)>)/, replacement)
    end
    output
  end
end
Usage
Asciidoctor::Extensions.register do
  postprocessor CopyrightFooterPostprocessor
end

Asciidoctor.convert_file 'sample-with-copyright-footer.adoc', :safe => :safe

83.4. Docinfo Processor Example

Purpose

Appends the Google Analytics tracking code to the bottom of an HTML document.

GoogleAnalyticsDocinfoProcessor
class GoogleAnalyticsDocinfoProcessor < Asciidoctor::Extensions::DocinfoProcessor
  use_dsl
  at_location :footer
  def process document
    return unless (ga_account_id = document.attr 'google-analytics-account')
    %(<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create','#{ga_account_id}','auto');
ga('send','pageview');
</script>)
  end
end
Usage
Asciidoctor::Extensions.register do
  docinfo_processor GoogleAnalyticsDocinfoProcessor
end

Asciidoctor.convert_file 'sample.adoc', :safe => :safe,
    :attributes => 'UA-ABCXYZ123'

83.5. Block Processor Example

Purpose

Register a custom block style named shout that uppercases all the words and converts periods to exclamation points.

sample-with-shout-block.adoc
[shout]
The time is now. Get a move on.
ShoutBlock
require 'asciidoctor'
require 'asciidoctor/extensions'

class ShoutBlock < Asciidoctor::Extensions::BlockProcessor
  PeriodRx = /\.(?= |$)/

  use_dsl

  named :shout
  on_context :paragraph
  name_positional_attributes 'vol'
  parse_content_as :simple

  def process parent, reader, attrs
    volume = ((attrs.delete 'vol') || 1).to_i
    create_paragraph parent, (reader.lines.map {|l| l.upcase.gsub PeriodRx, '!' * volume }), attrs
  end
end
Usage
Asciidoctor::Extensions.register do
  block ShoutBlock
end

Asciidoctor.convert_file 'sample-with-shout-block.adoc', :safe => :safe

83.6. Compound Block Processor Example

Purpose

Register a custom block named collapsiblelisting that transforms a listing block into a compound block composed of the following:

  • an example block with the collapsible option enabled

  • the original listing block

  • the listing block is promoted to a source block if a language is specified using the second positional attribute.

sample-with-collapsiblelisting-block.adoc
.Show JSON
[collapsiblelisting,json]
----
{
   "foo": "bar"
}
----
CollapsibleListingBlock
class CollapsibleListingBlock < Asciidoctor::Extensions::BlockProcessor
  enable_dsl
  on_context :listing
  positional_attributes 'language'

  def process parent, reader, attrs
    lang = attrs.delete 'language'
    attrs['title'] ||= 'Show Listing'
    example = create_example_block parent, [], attrs, content_model: :compound
    example.set_option 'collapsible'
    listing = create_listing_block example, reader.readlines, nil
    if lang
      listing.style = 'source'
      listing.set_attr 'language', lang
      listing.commit_subs
    end
    example << listing
    example
  end
end

Asciidoctor::Extensions.register do
  block CollapsibleListingBlock, :collapsiblelisting
end
Usage
$ asciidoctor -r ./collapsiblelisting.rb sample-with-collapsiblelisting-block.adoc
This extension mimics the builtin collapsible option on the example block, but consolidates it to a single block. The purpose of this extension is to show how to assemble a compound block in an extension.

83.7. Block Macro Processor Example

Purpose

Create a block macro named gist for embedding a gist.

sample-with-gist-macro.adoc
.My Gist
gist::123456[]
GistBlockMacro
require 'asciidoctor'
require 'asciidoctor/extensions'

class GistBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor
  use_dsl

  named :gist

  def process parent, target, attrs
    title_html = (attrs.has_key? 'title') ?
        %(<div class="title">#{attrs['title']}</div>\n) : nil

    html = %(<div class="openblock gist">
#{title_html}<div class="content">
<script src="https://gist.github.com/#{target}.js"></script>
</div>
</div>)

    create_pass_block parent, html, attrs, subs: nil
  end
end
Usage
Asciidoctor::Extensions.register do
  block_macro GistBlockMacro if document.basebackend? 'html'
end

Asciidoctor.convert_file 'sample-with-gist.adoc', :safe => :safe

83.8. Inline Macro Processor Example

Purpose

Create an inline macro named man that links to a man page.

sample-with-man-link.adoc
See man:gittutorial[7] to get started.
ManpageInlineMacro
require 'asciidoctor'
require 'asciidoctor/extensions'

class ManInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
  use_dsl

  named :man
  name_positional_attributes 'volnum'

  def process parent, target, attrs
    text = manname = target
    suffix = ''
    target = %(#{manname}.html)
    suffix = if (volnum = attrs['volnum'])
      "(#{volnum})"
    else
      nil
    end
    parent.document.register :links, target
    %(#{(create_anchor parent, text, type: :link, target: target).convert}#{suffix})
  end
end
Usage
Asciidoctor::Extensions.register do
  inline_macro ManInlineMacro
end

Asciidoctor.convert_file 'sample-with-man-link.adoc', :safe => :safe

83.9. Include Processor Example

Purpose

Include a file from a URI.

Asciidoctor supports including content from a URI out of the box if you set the allow-uri-read attribute (not available if the safe mode is secure).
sample-with-uri-include.adoc
:source-highlighter: coderay

.Gemfile
[source,ruby]
----
include::https://raw.githubusercontent.com/asciidoctor/asciidoctor/master/Gemfile[]
----
UriIncludeProcessor
require 'asciidoctor'
require 'asciidoctor/extensions'
require 'open-uri'

class UriIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
  def handles? target
    (target.start_with? 'http://') or (target.start_with? 'https://')
  end

  def process doc, reader, target, attributes
    content = (open target).readlines
    reader.push_include content, target, target, 1, attributes
    reader
  end
end
Usage
Asciidoctor::Extensions.register do
  include_processor UriIncludeProcessor
end

Asciidoctor.convert_file 'sample-with-uri-include.adoc', :safe => :safe

You can see plenty more extension examples in the extensions lab.