Refactor Confluence Code Blocks from XDOM
Last modified by Nikita Petrenko on 2025/02/12 12:25
![]() | Allows to refactor code blocks imported from Confluence from document contents |
Type | |
Category | Other |
Developed by | |
Rating | |
License | GNU Lesser General Public License 2.1 |
Table of contents
Description
This script allows to perform bulk refactoring of XWiki documents containing code macros in their content. It is particularily useful after content migrations from Confluence.
This snippet requires the Job Macro to run.
In a new page, copy-paste the following snippet :
{{velocity}}
#set ($spacePickerParams = {
'name': 'targetSpace',
'value': "$!{request.targetSpace}"
})
This script allows to perform bulk update of document contents to fix issues related to code blocks being scattered in multiple code macros. It is particularily useful after content migrations from Confluence.
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 removal job will execute for every document under the given space.</span>
</dt>
<dd>
#pagePicker($spacePickerParams)
</dd>
</dl>
<p>
<span class="buttonwrapper">
<input type="hidden" name="form_token" value="$!{services.csrf.token}"/>
<input type="hidden" name="savePages" value="save"/>
<input type="hidden" name="confirm" value="true"/>
<input class="button" type="submit" value="Refactor code blocks"/>
</span>
</p>
</form>
{{/html}}
{{/velocity}}
{{job id="refactorCodeBlocksFromXDOM" start="{{velocity}}$!{request.confirm}{{/velocity}}"}}
{{groovy}}
import org.apache.commons.lang3.StringUtils;
import java.lang.StringBuilder;
import org.xwiki.rendering.block.*;
import org.xwiki.rendering.block.match.ClassBlockMatcher;
import org.xwiki.rendering.macro.Macro;
import org.xwiki.rendering.transformation.MacroTransformationContext;
logger = services.logging.getLogger('RemoveCodeBlocksFromXDOM');
services.logging.setLevel('RemoveCodeBlocksFromXDOM', org.xwiki.logging.LogLevel.INFO);
componentManager = services.component.getComponentManager();
def verifyXDOM(xdom, syntaxId) {
def hasXDOMChanged = false;
// First, update any document macro that could contain nested content
xdom.getBlocks(new ClassBlockMatcher(MacroBlock.class), Block.Axes.DESCENDANT_OR_SELF).each { block ->
logger.debug('Checking macro [{}] - [{}]', block.getId(), block.getClass());
if (componentManager.hasComponent(Macro.class, block.getId())) {
// Check if the macro content is wiki syntax, in which case we'll also verify the contents of the macro
def macroContentDescriptor = componentManager.getInstance(Macro.class, block.getId()).getDescriptor().getContentDescriptor();
if (macroContentDescriptor != null && macroContentDescriptor.getType().equals(Block.LIST_BLOCK_TYPE) && StringUtils.isNotBlank(block.getContent())) {
// We will take a quick shortcut here and directly parse the macro content with the syntax of the document
logger.debug('Calling parse on [{}] with syntax [{}]', block.getId(), syntaxId)
def macroXDOM = services.rendering.parse(block.getContent(), syntaxId);
def hasMacroContentChanged = verifyXDOM(macroXDOM, syntaxId);
if (hasMacroContentChanged) {
logger.debug('The content of macro [{}] has changed', block.getId());
def newMacroContent = services.rendering.render(macroXDOM, syntaxId);
// Create a new macro block and swap it
def newMacroBlock = new MacroBlock(block.getId(), block.getParameters(), newMacroContent, block.isInline());
block.getParent().replaceChild(newMacroBlock, block);
hasXDOMChanged = true;
}
}
}
}
// Then, verify each group block present in the current xdom to check if it is subject to refactoring
xdom.getBlocks(new ClassBlockMatcher(GroupBlock.class), Block.Axes.DESCENDANT_OR_SELF).each { block ->
logger.debug('Checking block [{}] - [{}]', block.getParameters(), block.getClass());
if ((block.getParameters().containsKey('class') && StringUtils.contains(block.getParameters().get('class'), 'container')) || block.getParameters().containsKey('rowspan')) {
logger.debug('Found candidate group block [{}]', block.getParameters());
// Verify that each child of the current block is a group which contains multiple lines. If this is the case, then we need to perform the refactoring.
def hasOnlyLineChildBlocks = true;
block.getChildren().each { childBlock ->
logger.debug('Checking child [{}] - [{}]', childBlock.getParameters(), childBlock.getClass());
hasOnlyLineChildBlocks &= (childBlock instanceof GroupBlock && childBlock.getParameters().containsKey('class') && StringUtils.contains(childBlock.getParameters().get('class'), 'line'));
}
if (hasOnlyLineChildBlocks) {
logger.info('Block [{}] is matching constraints for a code block. Refactoring ...', block.getParameters());
// Start by creating the new macro that will be used for hosting the contents
def newMacroContent = new StringBuilder();
block.getChildren().each { lineBlock ->
lineBlock.getChildren().each { lineElementBlock ->
if (lineElementBlock instanceof MacroBlock && lineElementBlock.getId().equals('code')) {
newMacroContent.append(lineElementBlock.getContent());
} else if (lineElementBlock instanceof ParagraphBlock && lineElementBlock.getChildren().size() == 1 && lineElementBlock.getChildren().get(0) instanceof SpaceBlock) {
newMacroContent.append(' ');
} else {
// Let's just render the block in plain text
newMacroContent.append(services.rendering.render(lineElementBlock, 'plain/1.0'))
}
}
newMacroContent.append('\n');
}
def newCodeMacro = new MacroBlock('code', ['language': 'none'], newMacroContent.toString(), false);
if (block.getParameters().containsKey('class') && StringUtils.contains(block.getParameters().get('class'), 'container')) {
block.getParent().replaceChild(newCodeMacro, block);
} else {
while (block.getChildren().size() > 0) {
block.removeBlock(block.getChildren().get(0));
}
block.addChild(newCodeMacro);
}
hasXDOMChanged = true;
}
}
}
return hasXDOMChanged;
}
if (hasProgramming && services.csrf.isTokenValid(request.form_token)) {
if (request.targetSpace && StringUtils.isNotBlank(request.targetSpace)) {
def spacePrefix = "${StringUtils.removeEnd(request.targetSpace, 'WebHome')}%";
// Get every page matching the space
def documents = services.query.hql('select doc.fullName from XWikiDocument doc where doc.fullName like :spacePrefix').bindValue('spacePrefix', spacePrefix.toString()).execute();
logger.debug('Space prefix : [{}]', spacePrefix)
logger.debug('Found [{}] documents to verify', documents.size())
documents.each { documentFullName ->
try {
def document = xwiki.getDocument(documentFullName);
logger.info('Verifying document [{}]', document.getDocumentReference());
def xdom = document.getXDOM();
hasXDOMChanged = verifyXDOM(xdom, document.getSyntaxId());
if (hasXDOMChanged && 'save'.equals(request.savePages)) {
logger.info('XDOM has changed ; saving document [{}]', document.getDocumentReference());
document.setContent(xdom);
document.save('Update document content to merge code blocks.');
}
} catch (Exception e) {
logger.error('Uncaught exception [{}]', e);
}
}
} else {
logger.error('Insufficient parameters. Aborting.');
}
} else {
logger.error('Insufficient permissions or invalid CSRF token. Aborting.')
}
{{/groovy}}
{{/job}}