Script macros allow you to include dynamic content through scripts.

Currently only a custom script macro is provided.

Let’s experiment by creating a trivial macro. Go to Admin → Script Macros, click on the Custom Script Macro link and fill the form out as follows:

trivial macro

The purpose of this macro is just to output a button and apply some style.

Now go to a Confluence editor, (e.g. create a page), and use the macro browser to choose this macro.

If you already had the editor open, you will need to shift-refresh after editing macros definitions.

After adding the macro to the page, you should see something like this:

trivial macro edit

and once saved, the page should display:

trivial macro view

You can experiment with changing the content, the style and the behaviour by using a different macro, javascript and css code.

It is also possible to change the body type to Rich Text, and the output type to Inline. Also change the script to:

"<b>" + body + "</b>"

Now this macro should allow you to enter a body, and take the full width of the editor.

Block and inline control the layout flow. Body refers to whether you can type in the body of the macro. Usually you will want a body type: None.

Macros with Parameters

We’ll now create a macro that accepts parameters. For this we’ll use a wholly contrived example whereby a macro will output an AUI message.

Create a new script macro and set up the first lot of parameters to look like this:

macro with params 1

We would like to allow the users to specify:

  • the title of the message

  • the severity of the message (e.g. info, warning, error)

  • the body of the message

So the macro will have a body, and two arguments. The title is a string, and because we only accept a finite list for the level the type is enum. You can find out more about different parameter types here.

Click the + Parameter button, and fill out the form like this. After selecting enum for the second one you will click the + Enum Value to add the three enum values

macro with params 2

For the script, you can type in the following or put it in a file and enter the relative path to the file, under the script root (as usual).

"""<div class="aui-message aui-message-${parameters.level}">
    <p class="title">
        <strong>${parameters.title}</strong>
    </p>
    $body
</div>"""

The use of a triple-quoted string is intentional, it allows us to easily output a single HTML string. Note how you access the provided user-parameter values, i.e. parameters.key.

Because we marked at least one parameter as required, when using the macro the Macro Browser dialog will pop up. You can experiment with changing the parameters in the Macro Browser:

macro with params 3
Rather than returning an HTML string, we would recommend the user of a MarkupBuilder. This will ensure the output HTML is well-formed, e.g. you haven’t left any open tags, which will break the formatting of your page.

Binding Variables

Your script have access to the following binding variables, without the need for them to be declared:

parameters

A Map<String, String> for accessing user-provided macro parameters. The map’s keys are the parameter keys, so to get the color parameter use parameters.color.

body

The body of the macro. Will be null if there is no body.

context

A ConversionContext. This contains information about the current page and output device type (e.g. desktop or mobile etc). You will use this for methods of XhtmlContent.

Lazy Loaded Macros

If you have a macro that takes more than a second or so to run you should consider checking the Lazy Loaded box. This will mean that rendering of the page is not delayed until the macro has executed. Instead the traditional "spinning gears" icon will be shown, and the macro content will be loaded asynchronously via REST.

Take for example the following macro, which displays the latest version of each Atlassian product. Because it makes a number of calls to the Atlassian marketplace REST API, it takes a couple of seconds to complete:

atl versions macro

The code is:

import com.atlassian.confluence.xhtml.api.XhtmlContent
import com.atlassian.sal.api.component.ComponentLocator
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import groovy.xml.MarkupBuilder
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method

def httpBuilder = new HTTPBuilder("https://marketplace.atlassian.com")
def xhtmlContent = ComponentLocator.getComponent(XhtmlContent)

def rt = httpBuilder.request(Method.GET, ContentType.JSON) {
    uri.path = "/rest/1.0/applications"
}

List<Map> apps = rt.applications

def writer = new StringWriter()
def builder = new MarkupBuilder(writer)

builder.table {
    tbody {
        tr {
            th { p("App") }
            th { p("Latest version") }
        }

        apps.sort {it.order}.each { app ->
            rt = httpBuilder.request(Method.GET, ContentType.JSON) {
                uri.path = "/rest/1.0/applications/${app.key}"
            }

            tr {
                td { p(app.name) }
                td { p(rt.versions.first().version) }
            }
        }
    }
}

xhtmlContent.convertStorageToView(writer.toString(), context)

and produces:

atl versions macro view

Re-using existing macros

Some existing macros have many and complex parameters. You may want to get some standardisation over their usage. Script macros provide an easy way to do this.

To take as example the Table of Contents macro. (Although this has many parameters, this is probably not a good candidate for wrapping like this, but as an example it will do).

First of all, in a sample page, use the {toc} macro as you would want your users to use it, i.e. configure all the parameters. Then, view the storage format of the page. For this you will need the Confluence Source Editor plugin. Now copy the macro XHTML, it should look something like this:

source editor

Now create a new script macro with no body and no parameters. The script would be something like:

import com.atlassian.confluence.xhtml.api.XhtmlContent
import com.atlassian.sal.api.component.ComponentLocator

def xhtmlContent = ComponentLocator.getComponent(XhtmlContent)

xhtmlContent.convertStorageToView("""
  <ac:structured-macro ac:name="toc" ac:schema-version="1">
    <ac:parameter ac:name="maxLevel">3</ac:parameter>
    <ac:parameter ac:name="indent">12px</ac:parameter>
    <ac:parameter ac:name="class">my-class</ac:parameter>
    <ac:parameter ac:name="printable">false</ac:parameter>
  </ac:structured-macro>
""", context)

When used it will be as those the {toc} macro was used with those parameters.

You must use the convertStorageToView method as shown, to convert from storage format to HTML.

Including All Child Pages

This macro includes the content of all child pages. It is a good example of a common technique in macros, which is to write the XML for one or more other macros, and pass the resulting XML through the rendering engine which converts storage XML to view format.

import com.atlassian.confluence.pages.Page
import com.atlassian.confluence.xhtml.api.MacroDefinition
import com.atlassian.confluence.xhtml.api.XhtmlContent
import com.atlassian.renderer.v2.RenderUtils
import com.atlassian.sal.api.component.ComponentLocator

def xhtmlContent = ComponentLocator.getComponent(XhtmlContent)

def entity = context.getEntity()
if (entity instanceof Page) {

    def concat = (entity as Page).children.collect { child ->
        "<h2>${child.title}</h2>" + (1)
            xhtmlContent.convertMacroDefinitionToStorage(new MacroDefinition("include", null, null, [ (2)
                "0"       : "${context.spaceKey}:${child.title}".toString(),
            ]), context)
    }.join("\n\n")

    return xhtmlContent.convertStorageToView(concat, context) (3)
}
else {
    RenderUtils.blockError("Error", "Should only be used on pages, not comments, blog posts, etc")
}
1 separate each child page content with an <h2> and the page title
2 create the storage format XML for the include macro
3 convert XML string to view format

We simply take the immediately descending child pages and write the include page macro XML. We could pick and choose sub-pages, e.g. selecting just those with labels.

To make this include children recursively, use descendants instead of children.

Other configuration:

include children config

Further examples

CQL Search Macro

The following example providers a CQL Search macro, that allows users to enter some CQL:

cql search macro

Source:

import com.atlassian.confluence.api.model.Expansion
import com.atlassian.confluence.api.model.content.Content
import com.atlassian.confluence.api.model.pagination.PageResponse
import com.atlassian.confluence.api.model.pagination.SimplePageRequest
import com.atlassian.confluence.api.model.search.SearchContext
import com.atlassian.confluence.api.service.search.CQLSearchService
import com.atlassian.confluence.xhtml.api.XhtmlContent
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import groovy.xml.MarkupBuilder

def cqlSearchService = ScriptRunnerImpl.getOsgiService(CQLSearchService)
def xhtmlContent = ScriptRunnerImpl.getOsgiService(XhtmlContent)
def maxResults = parameters.maxResults as Integer ?: 10

def cqlQuery = parameters.cql as String

def pageRequest = new SimplePageRequest(0, maxResults)
def searchResult = cqlSearchService.searchContent(cqlQuery, SearchContext.builder().build(), pageRequest, Expansion.combine("space")) as PageResponse<Content>

def writer = new StringWriter()
def builder = new MarkupBuilder(writer)

if (searchResult.size()) {
    builder.table {
        tbody {
            tr {
                th { p("Title") }
                th { p("Space") }
            }
            searchResult.results.each { content ->
                tr {
                    td {
                        p {
                            "ac:link" {
                                "ri:page"("ri:content-title": content.title, "ri:space-key": content.space.key)
                            }
                        }
                    }
                    td { p(content.space.name) }
                }
            }
        }
    }

    if (searchResult.respondsTo("totalSize")) { // confl 5.8+
        builder.p("Showing ${Math.min(maxResults, searchResult.totalSize())} of ${searchResult.totalSize()}")
    }
}
else {
    builder.p("No results")
}

def view = xhtmlContent.convertStorageToView(writer.toString(), context)
view

For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.

Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.