Skip to main content
Home page
Example scripts
To homepage
Confluence
Cloud icon
Cloud

Bulk convert migrated built-in macros

Created 8 month(s) ago, Updated 22 day(s) ago
App in script
ScriptRunner For Confluence
ScriptRunner For Confluence
by Adaptavist
Compatibility
compatibility bullet
Confluence
Language |
groovy
import java.util.regex.Matcher

def spaceKeys = "ds,ds1" // Replace the spaceKeys to your own value
logger.info("Fetching spaces - ${spaceKeys}")
def spaces = Spaces.search("space.key in (${spaceKeys})")
spaces.each { space ->
    logger.info("Fetching all pages for space - ${space.key}")
    def pages = Spaces.getByKey(space.key).getAllPages(){
        setBodyFormat("storage")
    }
    pages.each { page ->
        logger.info("Extracting body contents for page '${page.title}'")
        def storage = page.body.storage.value
        if (!storage) {
            logger.info("Not updating page '${page.title}' (empty body)")
            return
        }
        def updatedMacro = replaceStructuredMacroAcName(storage)
        updatedMacro = replaceEmptyAcNameForParameter(updatedMacro)
        updatedMacro = convertMarkdownMacros(updatedMacro)
        updatedMacro = convertCreatePageMacros(updatedMacro)
        updatedMacro = convertVersionHistoryMacros(updatedMacro)
        if (updatedMacro == storage) {
            logger.info("Not updating page '${page.title}'")
            return
        }
        logger.info("Updating page '${page.title}' with new body")
        page.update(){
            title = page.title
            status = "current"
            body = updatedMacro
        }
        logger.info("Page '${page.title}' has been updated.")
    }
}

static def replaceStructuredMacroAcName(String storage) {
    def SEARCH = ["ac:name=\"add-label\"", "ac:name=\"choose-label\"", "ac:name=\"page-info\"",
                  "ac:name=\"cql-search\""]
    def REPLACEMENT = ["ac:name=\"sr-add-labels-macro\"", "ac:name=\"sr-choose-label-macro\"",
                       "ac:name=\"sr-page-info-macro\"", "ac:name=\"sr-cql-search-macro\""]
    def modifiedStorage = storage
    SEARCH.eachWithIndex { acName, index ->
        modifiedStorage = modifiedStorage.replace(acName, REPLACEMENT[index])
    }
    modifiedStorage
}

static def replaceEmptyAcNameForParameter(String storage) {
    def EMPTY_AC_FORMAT = "ac:name=\"\">"
    def SEARCH = ["current-version", "title", "created-by", "created-date", "diffs", "labels", "modified-by",
                  "modified-date", "modified-users", "page-id", "participants", "versions"]
    def REPLACED_AC_FORMAT = "ac:name=\"infoType\">"
    def REPLACEMENT = ["Current version", "Title", "Created by", "Created date", "Diffs", "Labels", "Modified by",
                       "Modified date", "Modified users", "Page id", "Participants", "Versions"]

    def modifiedStorage = storage
    SEARCH.eachWithIndex { acName, index ->
        def fullSearchString = EMPTY_AC_FORMAT + acName
        def fullReplacementString = REPLACED_AC_FORMAT + REPLACEMENT[index]
        modifiedStorage = modifiedStorage.replace(fullSearchString, fullReplacementString)
    }
    modifiedStorage
}

static def convertMarkdownMacros(String storage) {
    def pattern = ~/(?s)(<ac:structured-macro[^>]*\bac:name=")markdown("[^>]*>)(.*?)(<\/ac:structured-macro>)/
    storage.replaceAll(pattern) { full, prefix, suffix, body, close ->
        def newBody = body
                .replaceAll(
                        /(?s)(<ac:parameter ac:name="url">)\s*<ri:url\s+ri:value="([^"]+)"[^>]*\/?\s*>(?:\s*<\/ri:url>)?\s*(<\/ac:parameter>)/,
                        '$1$2$3'
                )
                .replaceAll(
                        /(<ac:parameter ac:name="url">)UrlResourceIdentifier\[url=([^\]]+)\](<\/ac:parameter>)/,
                        '$1$2$3'
                )
                .replaceAll(/(?s)<ac:plain-text-body>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/ac:plain-text-body>/) { m, markdown ->
                    markdown ? "<ac:parameter ac:name=\"markdown\">${escapeXml(markdown)}</ac:parameter>" : ''
                }
        "${prefix}sr-markdown-macro${suffix}${newBody}${close}"
    }
}

static def escapeXml(String value) {
    value.replace("&", "&")
            .replace("<", "<")
            .replace(">", "&gt;")
            .replace("\"", "&quot;")
}

static def convertCreatePageMacros(String storage) {
    def pattern = ~/(?s)(<ac:structured-macro[^>]*\bac:name=")create-page("[^>]*>)(.*?)(<\/ac:structured-macro>)/
    storage.replaceAll(pattern) { full, prefix, suffix, body, close ->
        if (shouldSkipCreatePageConversion(body)) {
            return full
        }
        "${prefix}sr-create-page-macro${suffix}${convertCreatePageBody(body)}${close}"
    }
}

static def shouldSkipCreatePageConversion(String body) {
    if (hasNonBlankCreatePageParam(body, "fromPage") || hasNonBlankCreatePageParam(body, "template")) {
        return true
    }
    def parent = firstParamValue(body, "parent")
    parent && !isCreatePageSelfParent(parent)
}

static def hasNonBlankCreatePageParam(String body, String name) {
    def value = firstParamValue(body, name)
    value != null && !value.isEmpty()
}

static def firstParamValue(String body, String name) {
    def matcher = body =~ /(?s)<ac:parameter ac:name="${name}">(.*?)<\/ac:parameter>/
    matcher.find() ? matcher.group(1).trim() : null
}

static def isCreatePageSelfParent(String parent) {
    def normalized = parent.toLowerCase().trim()
    if (normalized == '$self') {
        return true
    }
    def colonIndex = normalized.indexOf(':')
    if (colonIndex <= 0 || colonIndex >= normalized.length() - 1) {
        return false
    }
    def spaceKey = normalized.substring(0, colonIndex).trim()
    def title = normalized.substring(colonIndex + 1).trim()
    spaceKey && title && !(spaceKey =~ /\s/) && title == '$self'
}

static def convertCreatePageBody(String body) {
    def newBody = body
    def label = null
    newBody = newBody.replaceAll(/(?s)<ac:plain-text-body>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/ac:plain-text-body>/) { m, text ->
        def trimmed = text.trim()
        if (trimmed) {
            label = trimmed
        }
        ''
    }
    ["parent", "fromPage", "template", "prompt", "auiClass", "css", "suppressNotification"].each { name ->
        newBody = newBody.replaceAll(/(?s)\s*<ac:parameter ac:name="${name}">.*?<\/ac:parameter>/, '')
    }
    newBody = newBody.replaceAll(/<ac:parameter ac:name="addspace">/, '<ac:parameter ac:name="addSpace">')
    if (label) {
        newBody = "<ac:parameter ac:name=\"label\">${escapeXml(label)}</ac:parameter>" + newBody
    }
    newBody
}

static def convertVersionHistoryMacros(String storage) {
    def pattern = ~/(?s)(<ac:structured-macro[^>]*\bac:name=")version-history("[^>]*>)(.*?)(<\/ac:structured-macro>)/
    storage.replaceAll(pattern) { full, prefix, suffix, body, close ->
        def page = (firstParamValue(body, "page") ?: "").toLowerCase().trim()
        def isParent = page == '$parent'
        def isSelf = !page || page == '$self'
        if (page && !isSelf && !isParent) {
            return Matcher.quoteReplacement(full)
        }

        def pageJson = escapeXml('{"type":"' + (isParent ? 'parent' : 'self') + '"}')
        def newBody = body.replaceAll(/(?s)\s*<ac:parameter ac:name="page">.*?<\/ac:parameter>/, '')
        Matcher.quoteReplacement(
                prefix + "sr-version-history-macro" + suffix +
                        '<ac:parameter ac:name="page">' + pageJson + '</ac:parameter>' +
                        newBody + close
        )
    }
}
Having an issue with this script?
Report it here