Replace macros using Macro Converters from XDOM

Last modified by Raphaël Jakse on 2026/08/05 17:15

cogAllows to convert macros that are unsupported but handled by macro converters
TypeSnippet
CategoryOther
Developed by

Raphaël Jakse

Rating
0 Votes
LicenseGNU Lesser General Public License 2.1

Table of contents

Description

This script allows to perform bulk conversion of macros within XWiki documents. It is particularily useful after content migrations.

This snippet requires the Job Macro to run. If you want to convert Confluence macros, you will need converters from extension Confluence Migrator Pro - Converters.

In a new page, copy-paste the following snippet:

{{groovy output="false"}}
import org.xwiki.contrib.confluence.filter.MacroConverter
macrosToConvert = request.macrosToConvert;
if (!macrosToConvert) {
  SortedSet<String> macroConverters = new TreeSet(services.component.getComponentManager().getInstanceMap(MacroConverter.class).keySet());
  macroConverters.remove('include');
  macroConverters.remove('mention');
  macroConverters.remove('task');
  macroConverters.remove('default');
  macrosToConvert = macroConverters.join(",");
}
{{/groovy}}

{{velocity}}
#set ($spacePickerParams = {
  'name': 'targetSpace',
  'value': "$!{request.targetSpace}"
})

This script allows to perform bulk conversion of macros within XWiki documents. It is particularily useful after content migrations.

For each document part of a given space, XWiki will look for macros that are part of a provided list and convert them.

Programming rights are required to use this script.

{{html clean="false"}}
<form class="xform" action="#" method="post">
    <dl>
        <dt>
            <label for="targetSpace">Space</label>
            <span class="xHint">The macro replace job will execute for every document under the given space.</span>
        </dt>
        <dd>
            #pagePicker($spacePickerParams)
        </dd>
        <dt>
            <input type="checkbox" name="allSpaces" id="allSpaces" />
            <label for="allSpaces">All spaces</label>
            <span class="xHint">The macro replace job will execute for every document in all spaces (excerpt the XWiki system space).</span>
        </dt>
        <dt>
            <label for="macrosToConvert">Macros to be converted</label>
            <span class="xHint">Provide a comma-separated list of macros to remove.</span>
        </dt>
        <dd>
            <input type="text" name="macrosToConvert" id="macrosToConvert" value="$!{escapetool.xml($macrosToConvert)}" required="required"/>
        </dd>
        <dt>
            <input id="savePages" name="savePages" type="checkbox" value="save"/> <label for="savePages">Save pages</label>
            <span class="xHint">By default, this script will execute in dry-mode, and will not save pages.</span>
        </dt>
    </dl>
    <p>
    <span class="buttonwrapper">
      <input type="hidden" name="form_token" value="$!{services.csrf.token}"/>
      <input type="hidden" name="confirm" value="true"/>
      <input class="button" type="submit" value="Convert macros"/>
    </span>
    </p>
</form>
{{/html}}
{{/velocity}}

{{job id="convertMacrosFromXDOM" start="{{velocity}}$!{request.confirm}{{/velocity}}"}}
{{groovy}}
import com.xpn.xwiki.api.Document
import org.apache.commons.lang3.StringUtils
import org.xwiki.contrib.confluence.filter.MacroConverter
import org.xwiki.contrib.confluence.filter.input.ConfluenceInputContext
import org.xwiki.contrib.confluence.filter.input.ConfluenceInputProperties
import org.xwiki.contrib.confluence.filter.input.ConfluenceXMLPackage
import org.xwiki.contrib.confluence.filter.internal.input.DefaultConfluenceInputContext
import org.xwiki.logging.LogLevel
import org.xwiki.logging.Logger
import org.xwiki.rendering.block.Block
import org.xwiki.rendering.block.CompositeBlock
import org.xwiki.rendering.block.MacroBlock
import org.xwiki.rendering.block.XDOM
import org.xwiki.rendering.block.match.ClassBlockMatcher
import org.xwiki.rendering.block.match.MacroBlockMatcher
import org.xwiki.rendering.internal.parser.XDOMGeneratorListener
import org.xwiki.rendering.listener.Listener
import org.xwiki.rendering.macro.Macro
import org.xwiki.rendering.util.ParserUtils

logger = services.logging.getLogger('RemoveMacrosFromXDOM');
services.logging.setLevel('RemoveMacrosFromXDOM', LogLevel.INFO);
macrosToConvert = request.macrosToConvert && StringUtils.isNotBlank(request.macrosToConvert)
        ? Arrays.asList(request.macrosToConvert.split("\s*,\s*"))
        : [];

componentManager = services.component.getComponentManager();

XDOM getMacroXDOM(MacroBlock macroBlock, String syntaxId)
{
    if (componentManager.hasComponent(Macro.class, macroBlock.getId())) {
        def macroContentDescriptor =
                componentManager.getInstance(Macro.class, macroBlock.getId()).getDescriptor().getContentDescriptor();
        if (macroContentDescriptor != null && macroContentDescriptor.getType().equals(Block.LIST_BLOCK_TYPE) &&
                StringUtils.isNotBlank(macroBlock.getContent()))
        {
            // We will take a quick shortcut here and directly parse the macro content with the syntax of the document
            return services.rendering.parse(macroBlock.getContent(), syntaxId);
        }
    } else if (StringUtils.isNotBlank(macroBlock.getContent())) {
        // Just assume that the macro content is wiki syntax if we don't know the macro.
        logger.debug('Calling parse on unknown macro [{}] with syntax [{}]', macroBlock.getId(), syntaxId)
        return services.rendering.parse(macroBlock.getContent(), syntaxId);
    }
    return null;
}

MacroBlock convertContent(MacroBlock block, String syntaxId)
{
    XDOM macroXDOM = getMacroXDOM(block, syntaxId);
    if (macroXDOM != null) {
        def hasMacroContentChanged = verifyXDOM(macroXDOM, syntaxId);
        if (hasMacroContentChanged) {
            logger.debug('The content of macro [{}] has changed', block.getId());
            String newMacroContent = services.rendering.render(macroXDOM, syntaxId);
            return new MacroBlock(
                    block.getId(),
                    block.getParameters(),
                    newMacroContent,
                    block.isInline()
            );
        }
    }
    return null;
}

boolean verifyXDOM(xdom, String syntaxId)
{
    boolean hasXDOMChanged = false;
    for (MacroBlock block : xdom.getBlocks(new ClassBlockMatcher(MacroBlock.class), Block.Axes.DESCENDANT_OR_SELF)) {
        logger.debug('Checking block [{}] - [{}]', block.getId(), block.getClass());
        MacroBlock newMacroBlock = null;
        if (macrosToConvert.contains(block.getId())) {
            logger.info("Converting macro [{}]", block.getId())
            def converter = componentManager.getInstance(MacroConverter.class, block.getId());

            Listener listener = new XDOMGeneratorListener();
            converter.toXWiki(
                    block.getId(),
                    new HashMap<>(block.getParameters()), // work around https://jira.xwiki.org/browse/CONFLUENCE-246
                    block.getContent(),
                    block.isInline(),
                    listener
            );
            XDOM xdomWithMacro = listener.getXDOM();
            MacroBlock convertedBlock = xdomWithMacro.getFirstBlock(new ClassBlockMatcher(MacroBlock.class),
                    Block.Axes.DESCENDANT_OR_SELF);

            newMacroBlock = convertContent(convertedBlock, syntaxId);
            if (newMacroBlock == null) {
                // no content to convert in the end
                newMacroBlock = convertedBlock;
            }
        } else {
            newMacroBlock = convertContent(block, syntaxId);
        }

        if (newMacroBlock != null) {
            block.getParent().replaceChild(newMacroBlock, block);
            hasXDOMChanged = true;
        }
    }
    return hasXDOMChanged;
}

def perform()
{
    if (!hasProgramming) {
        logger.error('This script requires programming rights. Aborting.');
        return;
    }

    if (!services.csrf.isTokenValid(request.form_token)) {
        logger.error('Invalid CSRF token. Aborting.');
        return;
    }

    boolean allSpaces = request.allSpaces && request.allSpaces != '0' && request.allSpaces != 'false' && request.allSpaces != 'off';
    if ((!request.targetSpace || StringUtils.isBlank(request.targetSpace)) && !allSpaces) {
        logger.error('Missing a target space. Aborting.');
        return;
    }

    if (macrosToConvert.isEmpty()) {
        logger.error('Please provide a list of macros to convert. Aborting.');
        return;
    }

    for (String macroId : macrosToConvert) {
        if (!componentManager.hasComponent(MacroConverter.class, macroId)) {
            logger.error('Unable to find a converter for macro [{}]. Aborting', macroId);
            return;
        }
    }

    String spacePrefix = "${StringUtils.removeEnd(request.targetSpace, 'WebHome')}%";
    logger.info('Space prefix: [{}]', spacePrefix)
    logger.info('Macros to convert: [{}]', macrosToConvert);

    // Get every page matching the space
    List<String> documents = (
            allSpaces
                ?  services.query
                    .hql("select doc.fullName from XWikiDocument doc where doc.fullName not like 'XWiki.%'")
                    .execute()
                : services.query
                    .hql('select doc.fullName from XWikiDocument doc where doc.fullName like :spacePrefix')
                    .bindValue('spacePrefix', spacePrefix.toString())
                    .execute()
    );

    DefaultConfluenceInputContext confluenceInputContext = componentManager.getInstance(ConfluenceInputContext.class);
    // ConfluenceConverter assumes its running in a migration and has access to migration properties
    // So let's give it properties :-)
    // We need to figure out how
    confluenceInputContext.set(new ConfluenceXMLPackage(), new ConfluenceInputProperties());
    logger.debug('Found [{}] documents to verify', documents.size())
    for (String documentFullName : documents) {
        try {
            Document document = xwiki.getDocument(documentFullName);
            logger.info('Verifying document [{}]', document.getDocumentReference());
            def xdom = document.getXDOM();
            hasXDOMChanged = verifyXDOM(xdom, document.getSyntax().toIdString());
            if (hasXDOMChanged) {
                if ('save'.equals(request.savePages)) {
                    logger.info('Saving converted document [{}]', document.getDocumentReference());
                    document.setContent(xdom);
                    document.save('Convert macros');
                } else {
                    logger.info('Document [{}] would be modified and saved', document.getDocumentReference());
                }
            }
        } catch (Exception e) {
            logger.error('Uncaught exception [{}]', e);
        }
    }
}

perform();
{{/groovy}}
{{/job}}

Get Connected