/asciidoctorj-groovy-dsl

A Groovy DSL that allows for easy definition of Asciidoctor extensions

Primary LanguageGroovyApache License 2.0Apache-2.0

Asciidoctor Groovy DSL

The Asciidoctor Groovy DSL allows to define Asciidoctor extensions in Groovy.

Build Status

Quickstart

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.0.0.Alpha.2') // (1)
import org.asciidoctor.groovydsl.AsciidoctorExtensions
import org.asciidoctor.Asciidoctor

AsciidoctorExtensions.extensions{  //(2)
    block(name: 'BIG', contexts: [':paragraph']) {
        parent, reader, attributes ->
        def upperLines = reader.readLines()
        .collect {it.toUpperCase()}
        .inject('') {a, b -> a + '\n' + b}

        createBlock(parent, 'paragraph', [upperLines], attributes, [:])
    }
}
Asciidoctor.Factory.create().render('''
[BIG]
Hello World
''', [:]) // (3)
  1. Grab the module from jCenter. This fetches AsciidoctorJ transitively as well.

  2. Register as block extension that is defined inline in this example. Extensions can also be passed as files.

  3. Invoke AsciidoctorJ to render the passed string to HTML.

This results in:

<div class="paragraph">
<p>
HELLO WORLD</p>
</div>

Usage

To use the DSL you have to add a dependency on org.asciidoctor:asciidoctorj-groovy-dsl:1.0.0.preview2 on 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:1.5.0.Final'
    compile 'org.asciidoctor:asciidoctorj-groovy-dsl:1.0.0.preview1'
}

Extensions can be defined inline in a groovy closure or in separate files. Extensions have to configured at the class org.asciidoctor.groovydsl.AsciidoctorExtensions. Extensions always have to be registered before creating the Asciidoctor instance.

This is an example of how to define an extension inline in a groovy script and render a file:

AsciidoctorExtensions.extensions {
    block(name: 'BIG', contexts: [':paragraph']) {
        parent, reader, attributes ->
        def upperLines = reader.readLines()
        .collect {it.toUpperCase()}
        .inject('') {a, b -> a + '\n' + b}

        createBlock(parent, 'paragraph', [upperLines], attributes, [:])
    }
}

Asciidoctor.Factory.create().renderFile('mydocument.ad', [:])

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 upperLines = reader.readLines()
    .collect {it.toUpperCase()}
    .inject('') {a, b -> a + '\n' + b}

    createBlock(parent, 'paragraph', [upperLines], attributes, [:])
}
AsciidoctorExtensions.extensions(new File('bigblockextension.groovy'))
Asciidoctor.Factory.create().renderFile('mydocument.ad', [:])

Both examples will render the following document as shown below:

[BIG]
Hello, World!

This will result in:

HELLO, WORLD!

Description of the DSL

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 is the extension action. Every closure that is an extension action 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.

BlockProcessor

A BlockProcessor registers for a certain 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 upperLines = reader.readLines()
    .collect {it.toUpperCase()}
    .inject('') {a, b -> a + '\n' + b}

    createBlock(parent, 'paragraph', [upperLines], attributes, [:])
}

There is also a short form that only takes a block name and the closure. It automatically registers for 'open' and 'paragraph' blocks:

block('small') {
    parent, reader, attributes ->
    def lowerLines = reader.readLines()
    .collect {it.toLowerCase()}
    .inject('') {a, b -> a + '\n' + b}

    createBlock(parent, 'paragraph', [lowerLines], attributes, [:])
}

BlockMacroProcessor

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 the 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);
}

The extension will be called for a block like this:

gist::123456[]

The extension will create a passthrough block that finally gets rendered to this:

<div class="content"> <script src="https://gist.github.com/123456.js"></script> </div>

InlineMacroProcessor

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"]
    createInline(parent, "anchor", target, attributes, options).render()
}

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

Preprocessor extensions are registered using the function preprocessor. It does not require any additional options besides the extension action.

The following example simply removes the first line of the document.

preprocessor {
    document, reader ->
    reader.advance()
    reader
}

Postprocessor

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.

The following example assumes that the html backend is used 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

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.

The following example registers for all include macros that include resource starting with http.

String content = "The content of the URL"

include_processor (filter: {it.startsWith("http")}) {
    document, reader, target, attributes ->
    reader.push_include(content, target, target, 1, attributes);
}

Treeprocessor

Treeprocessor extensions are registered using the function treeprocessor.

The following example renders blocks that start with a $ sign as a listing.

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,[:])
        }
    }
}

DocinfoProcessor

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">'
}

To add content in the footer of the document pass the location option like this:

docinfo_processor( location : ':footer') {
    document -> '<div>FOOBAR</div>'
}