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.
---
tags: [announcement, website]
---
= Document Title
content
[subs="attributes,specialcharacters"]
.Captured front matter
....
---
{front-matter}
---
....
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
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).
$ echo "Hello, World!"
> Hello, World!
$ gem install asciidoctor
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? '> '
%(<span class="output">#{line[5..-1]}</span>)
else
line
end
end
create_listing_block block.document, lines * EOL, attrs, subs: nil
end
end
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.
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
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.
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
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
shoutthat uppercases all the words and converts periods to exclamation points.
[shout]
The time is now. Get a move on.
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
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
collapsiblelistingthat 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.
-
.Show JSON
[collapsiblelisting,json]
----
{
"foo": "bar"
}
----
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
$ 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
gistfor embedding a gist.
.My Gist
gist::123456[]
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
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
manthat links to a man page.
See man:gittutorial[7] to get started.
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
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).
|
:source-highlighter: coderay
.Gemfile
[source,ruby]
----
include::https://raw.githubusercontent.com/asciidoctor/asciidoctor/master/Gemfile[]
----
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
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.