The Asciidoctor Groovy DSL allows to define Asciidoctor extensions in Groovy.
To see the DSL in action at once simply fire up groovyConsole
.
Then execute this code:
@GrabConfig(systemClassLoader=true)
@Grab(group='org.asciidoctor', module='asciidoctorj-groovy-dsl', version='1.6.0') // (1)
import org.asciidoctor.groovydsl.AsciidoctorExtensions
import org.asciidoctor.Asciidoctor
AsciidoctorExtensions.extensions { //(2)
block(name: 'BIG', contexts: [':paragraph']) {
parent, reader, attributes ->
def uppercaseLines = reader.readLines()
.collect {it.toUpperCase()}
.inject('') {a, b -> a + '\n' + b}
createBlock(parent, 'paragraph', [uppercaseLines], attributes, [:])
}
}
println Asciidoctor.Factory.create().convert('''
[BIG]
Hello World
''', [:]) // (3)
-
Grab the module from jCenter. This fetches AsciidoctorJ transitively as well.
-
Register as block extension. Here, it is defined inline but extensions can also be passed as files or string values.
-
Invoke AsciidoctorJ to convert the passed string to HTML in the console.
This results in:
<div class="paragraph">
<p>
HELLO WORLD</p>
</div>
To use the DSL you have to add a dependency on org.asciidoctor:asciidoctorj-groovy-dsl:1.6.0
from jCenter.
The integration into a Gradle project is straightforward. To use AsciidoctorJ you also add the JCenter repository and add the respective dependency.
repositories {
jcenter()
}
dependencies {
compile 'org.asciidoctor:asciidoctorj:2.2.0'
compile 'org.asciidoctor:asciidoctorj-groovy-dsl:1.6.0'
}
Extensions can be defined inline in a groovy closure, and also in a separate file or string value.
All extensions must be configured at the class org.asciidoctor.groovydsl.AsciidoctorExtensions
, and always be registered before creating the Asciidoctor
instance.
There are two ways to register extensions using AsciidoctorExtensions:
-
Creating an instance.
This is the recommended method since is thread-safe. Once an instance is created, extensions can be registered using theaddExtension
method passing a closure, file or string value.def extensions = new AsciidoctorExtensions() extensions.addExtension { block(name: 'BIG', contexts: [':paragraph']) { parent, reader, attributes -> def uppercaseLines = reader.readLines() .collect {it.toUpperCase()} .inject('') {a, b -> a + '\n' + b} createBlock(parent, 'paragraph', [uppercaseLines], attributes, [:]) } }
-
Static registration. This method is offered for convenience and ease of use. The example below shows how to define an extension inline in a groovy script and convert a file:
AsciidoctorExtensions.extensions { block(name: 'BIG', contexts: [':paragraph']) { parent, reader, attributes -> def uppercaseLines = reader.readLines() .collect {it.toUpperCase()} .inject('') {a, b -> a + '\n' + b} createBlock(parent, 'paragraph', [uppercaseLines], attributes, [:]) } } Asciidoctor.Factory.create().convertFile('mydocument.ad', [:])
As mentioned, extensions can be also defined from other sources.
This is an example of how to define an extension in a separate file bigblockextension.groovy
.
block(name: 'BIG', contexts: [':paragraph']) {
parent, reader, attributes ->
def uppercaseLines = reader.readLines()
.collect {it.toUpperCase()}
.inject('') {a, b -> a + '\n' + b}
createBlock(parent, 'paragraph', [uppercaseLines], attributes, [:])
}
new AsciidoctorExtensions().addExtension(new File('bigblockextension.groovy'))
Asciidoctor.Factory.create().convertFile('mydocument.ad', [:])
All examples seen in this section will convert the following document as shown below:
[BIG]
Hello, World!
This will result in all text in the [BIG]
block to be converted to upper case:
HELLO, WORLD!
For every Processor class in AsciidoctorJ there is a function offered by the DSL to simply define such an extension. The following sections show examples for each kind of extension. Basically every extension is defined by calling the correct function for the extension type, passing options and a closure that holds the extension logic.
Under the hood, every closure has an instance of the respective Processor class as its delegate.
That means that all methods provided by org.asciidoctor.extensions.Processor
and its subclasses are directly available.
Block processors are registered using the function block
and it must define at least the block name and context.
The result of the closure will replace the original block.
The following example registers an extension for paragraphs having the block name BIG
:
block(name: 'BIG', contexts: [':paragraph']) {
parent, reader, attributes ->
def uppercaseLines = reader.readLines()
.collect {it.toUpperCase()}
.inject('') {a, b -> a + '\n' + b}
createBlock(parent, 'paragraph', [uppercaseLines], attributes, [:])
}
There is also a short form that only takes a block name and the closure. It automatically registers for 'open' and 'paragraph' block’s context:
block('small') {
parent, reader, attributes ->
def lowercaseLines = reader.readLines()
.collect {it.toLowerCase()}
.inject('') {a, b -> a + '\n' + b}
createBlock(parent, 'paragraph', [lowercaseLines], attributes, [:])
}
Block macros processors are registered using the function block_macro
.
It requires the option name
that defines the macro name.
There is also the long form taking an option map and the short form that only takes the name.
block_macro (name: 'gist') {
parent, target, attributes ->
String content = """<div class="content">
<script src="https://gist.github.com/${target}.js"></script>
</div>"""
createBlock(parent, "pass", [content], attributes, config)
}
block_macro ('gist') {
parent, target, attributes ->
String content = """<div class="content">
<script src="https://gist.github.com/${target}.js"></script>
</div>"""
createBlock(parent, "pass", [content], attributes, config)
}
The extension will be called for a block like this:
gist::123456[]
The extension will create a passthrough block that finally gets converted to this:
<div class="content"> <script src="https://gist.github.com/123456.js"></script> </div>
Inline macro processors are registered using the function inline_macro
.
It also requires the name
option or the name given as the only additional parameter to the closure.
inline_macro (name: 'man') {
parent, target, attributes ->
options = [type: ":link", target: target + ".html"]
createPhraseNode(parent, "anchor", target, attributes, options)
}
inline_macro ('man') {
parent, target, attributes ->
options = ["type": ":link", target: target + ".html"]
createPhraseNode(parent, "anchor", target, attributes, options)
}
The extension will be called for text like this:
See man:gittutorial[7] to get started.
The extension will create a link to the gittutorial.html.
Preprocessor extensions are registered using the function preprocessor
.
It does not require any additional options besides the extension action.
The following example will simply remove the first line of the document.
preprocessor {
document, reader ->
reader.advance()
reader
}
Postprocessor extensions are registered using the function postprocessor
.
It does not require any additional options besides the extension action.
The task action must return the resulting string.
Note that postprocessors are dependant on the specific backend being used (html, pdf, etc.). The following example assumes we are converting to HTML and adds a copyright notice at the end of the document:
import org.jsoup.*
String copyright = "Copyright Acme, Inc."
postprocessor {
document, output ->
if (document.basebackend("html")) {
org.jsoup.nodes.Document doc = Jsoup.parse(output, "UTF-8")
def contentElement = doc.getElementsByTag("body")
contentElement.append(copyright)
doc.html()
} else {
throw new IllegalArgumentException("Expected html!")
}
}
IncludeProcessor extensions are registered using the function include_processor
.
The options must contain an entry for the key filter
that points to a closure that decides whether to call this extension for the current include macro.
This closure receives the value of the include
The following extension registers for all include macros whose resource starts with https
like the one in the example below.
String content = "The content of the URL"
include_processor (filter: {it.startsWith("https")}) {
document, reader, target, attributes ->
reader.push_include(content, target, target, 1, attributes)
}
This is a remote secures resource to incude:
link:https://github.com/asciidoctor/asciidoctorj-groovy-dsl/blob/master/README.adoc[role=include]
Treeprocessor extensions are registered using the function treeprocessor
.
The following example converts blocks that start with a $
sign as a listing with the role "command".
treeprocessor {
document ->
List blocks = document.blocks()
(0..<blocks.length).each {
def block = blocks[it]
def lines = block.lines()
if (lines.size() > 0 && lines[0].startsWith('$')) {
Map attributes = block.attributes()
attributes["role"] = "terminal"
def resultLines = lines.collect {
it.startsWith('$') ? "<span class=\"command\">${it.substring(2)}</span>" : it
}
blocks[it] = createBlock(document, "listing", resultLines, attributes,[:])
}
}
}
Here is an example of the source document.
$ echo "Hello, World!"
$ gem install asciidoctor
DocinfoProcessor extensions are registered using the function docinfo_processor
.
The following example adds a meta tag to the HTML head element to allow robots to follow links.
docinfo_processor {
document -> '<meta name="robots" content="index,follow">'
}
Additionally, to add content in the footer of the document pass the location option like this:
docinfo_processor (location : ':footer') {
document -> '<div>FOOBAR</div>'
}