Skip to content

asciidoctor/asciidoctorj-groovy-dsl

Repository files navigation

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.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)
  1. Grab the module from jCenter. This fetches AsciidoctorJ transitively as well.

  2. Register as block extension. Here, it is defined inline but extensions can also be passed as files or string values.

  3. Invoke AsciidoctorJ to convert the passed string to HTML in the console.

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.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:

  1. Creating an instance.
    This is the recommended method since is thread-safe. Once an instance is created, extensions can be registered using the addExtension 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, [:])
        }
    }
  2. 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.

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!

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 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.

BlockProcessor

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

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 an option map and the short form that only takes the name.

Long form: defining name with an options map
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)
}
Short form: defining name directly
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>

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.

Long form: defining name with an options map
inline_macro (name: 'man') {
    parent, target, attributes ->
    options = [type: ":link", target: target + ".html"]
    createPhraseNode(parent, "anchor", target, attributes, options)
}
Short form: defining name directly
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

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

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

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

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

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

About

A Groovy DSL that allows for easy definition of Asciidoctor extensions

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published