Writing macro converters
Reference
Introduction
Like XWiki, Confluence allows using macros, in both the old legacy confluence syntax and the Confluence XHTML syntax.
Macros are converted at Confluence XML import time, by macro converters implementing MacroConverter. onMacro events are intercepted in ConfluenceConverterListener, that transforms events produced by legacy confluence syntax and the Confluence XHTML syntax parsers when importing a confluence package. ConfluenceConverterListener calls DefaultMacroConverter, which finds a specific implementation of MacroConverter with a hint corresponding to the name of the confluence macro. If it finds one, the macro is converted by this specific macro converter. Otherwise, the macro is transferred as is, with all its parameters. By default, a confluence_ prefix is added to macro names, hoping for a bridge implementation, which is supposed to be a thin wrapper that turns the macro call into an equivalent XWiki feature.
In this document, we describe how to implement a macro converter, the best practices around this, and common features that come with macro converters extending AbstractMacroConverter.
The MacroConverter interface
Macro converters implement the MacroConverter interface, which consists of three methods:
toXWiki
void toXWiki(String id, Map<String, String> parameters, String content, boolean inline, Listener listener);
This method takes the name of the macro to convert, the parameters, the content, whether the macro call is in an inline context and the listener to which events to which the macro call must be converted must be sent.
toXWikiId
default String toXWikiId(String id, Map<String, String> parameters, String content, boolean inline)
The name of the target macro. Usually, macro calls are converted to calls to XWiki macros and this method is supposed to give the target macro name if it's the case, or null if the Confluence macro call is not converted to a XWiki macro call. This is used for instance to determine whether the target macro supports inline, if this information is not provided by the support inline mode. Macro Converter should implement this. The method is only default for backward compatibility reasons. The defaut is to run the toXWiki method and figure out which macro is produced by listening to the produced events. This is potentially expensive.
supportsInlineMode
default InlineSupport supportsInlineMode(String id, Map<String, String> parameters, String content)
Whether the produced events are suitable for being put into an inline context. This can be used to decide who to convert a macro in an optimal way when converting the Confluence syntax, for instance to issues new line characters around a macro call that would not support inline, or by allowing some macro to be produced inside a paragraph without issuing new line characters allow it.
This method should be provided. Its default implementation is only there for backward compatibility reasons. It returns MAYBE, which should be avoided. MAYBE makes Confluence XML call toXWikiId to get the name of the target macro and query the wiki to try to determine whether the target macro supports and this is not ideal:
- We fully expect that target macros may not be installed on the target wiki, so this information might not be present at the time of the Confluence XML package import
- This is potentially expensive
- The macro converter might not be converting the Confluence macro to a XWiki macro, and we don't currently have anything fancy to determine whether any arbitrary sequence of events supports inline. It might not be possible in the general case anyway.
Implementing macro converters by extending AbstractMacroConverter
You should not generally directly implement the MacroConverter interface. Instead, we strongly recommend that you extend the AbstractMacroConverter class, which provides:
- convenient methods to convert to an XWiki macro without having to mess around with listeners
- all sorts of default mechanisms to honor the numerous input parameters of a Confluence XML package import at no cost, as well as various expected features, including:
- issuing warnings when parameters have not been used (https://jira.xwiki.org/browse/CONFLUENCE-411)
- keeping, or not, the unused parameters or all original parameters, with the configured prefix for this (https://jira.xwiki.org/browse/CONFLUENCE-454)
- enforcing the implementation of the supportsInlineMode method (because it missing can lead to conversion bugs)
Implementing a macro converter using AbstractMacro consist of the following steps.
Implement toXWiki (optional)
if your needs are advanced and you don't want to convert to a macro, you'll need to override toXWiki. See the description of the toXWiki method of MacroConverter. You will lose a part of the default behavior of AbstractMacroConverter and you are a bit more on your own if you do this. Please make sure you mark the parameters you don't use as unhandled, and you abort the conversion if that know that you can't convert something meaningfully.
The default implementation will call the following methods.
Override toXWikiId
This is mandatory. This gives the name of the target macro. If you are not converting to a macro, return null.
Override toXWikiContent (optional)
If you want to tweak the content of the macro, you can do so with this method. Usually you don't need to do this. Examples include latex related macros, where we need to fiddle with latex references and surround some macro content around equation environments.
Override toXWikiParameters
protected Map<String, String> toXWikiParameters(String confluenceId, Map<String, String> confluenceParameters, String content)This returns the target parameters with their values. Here are some advise to follow:
- You should use an ordered map, otherwise the order of the parameters in the results might not be stable, which might break tests for nothing. We will probably enforce an ordered map automatically at some point to avoid such inconveniences.
- Don't mutate confluenceParameters. Return your parameters in a new map, which just the parameter you want to return. It was possible before and caused issues and crashes. The code now explicitly trows if you try doing this.
- Don't return the original parameters "just in case". We now have a parameter that allows the user to keep the unhandled parameters if they are willing to have this. Instead, mark your parameters or parameter values as unhandled, it will help the aforementioned feature to do its job correctly.
- mark parameters or parameter values as (un)handled
- Be especially careful with links to Confluence objects in parameters (spaces, pages, etc). Depending on how the original macro is built, sometimes they are not converted, sometimes they are, sometimes they are converted to confluence references, sometimes to XWiki references. Things are unfortunately inconsistent and only trial and error will give you the right results.
- If you know that you can't convert something, abort the conversion
- If you are not actually converting to a macro, you can return null.
Marking parameters as (un)handled
Some users rely on parsing the logs to find out which macro parameters are not handled. This allows doing quality checks on the migrated content, and noticing features that need to be implemented. Noticing which parameters are not supported helps improving our conversion. It is therefore important that you mark the parameters and parameter values you handle as handled, and those you don't handle as unhandled.
There are several cases where this can happen, for instance:
- The target doesn't have the feature needed to implement the conversion of a specific parameter, or used to not have the feature and the corresponding macro converter was not updated since then
- Nobody has gotten around to implement the conversion
We recommend that when you implement toXWikiParameters, you only get the parameters you want to convert (i.e. you don't loop over all the parameters explicitly). This way, the parameters you don't use will be automatically marked as unhandled. This works because the confluenceParameters parameter passed by AbstractMacroConverter to your toXWikiParameters method is actually a map that traces the uses of its entries. This means that in most cases, you don't have to think about marking your parameters as unhandled if you follow the recommendation.
In addition to this mechanism, there are several methods you can use to correctly mark the parameters and the parameters.
markHandledParameter
protected void markHandledParameter(Map<String, String> confluenceParameters, String name, boolean handled)Mark a specific parameter as (un)handled. Marking a parameter as unhandled should be rarely needed if you follow the recommendations. However, if you now that it is useless to warn against a parameter being unhandled, you can call this method with handled set to true.
markUnhandledParameterValue
protected void markUnhandledParameterValue(Map<String, String> confluenceParameters, String parameterName)This method is useful when you got a parameter value you can't convert, because it is a known value for which we have no (straightforward) equivalence, or if the value is unexpected. Call this method to convey the fact that you know this parameter, but you don't handle its specific value. It is more precise and correct than to mark the parameter as unhandled.
Aborting the conversion
Sometimes, the conversion can't go on. This happens, for instance, if a critical parameter is missing or malformed, or if we know that we are in an unsupported case. In this case, it is better not to perform the macro conversion and to revert to outputting a bridge.
This is currently done by throwing a RuntimeException. This is not very pretty, but this is the backward compatible way we found to provide this feature. We shall improve this at some point.
Handling Confluence references and URLs
When writing a macro converter, you will likely need to convert things you have in the parameters (like page ids, space keys, page titles) to XWiki references.
To do this, you will need to inject ConfluenceConverter and use its methods.
To convert URLs, you will need to inject ConfluenceURLConverter and use the convertURL method.
See also how Confluence references and URLs are handled for more information and background on this.
Writing a Macro Converter test
It is very important to test your macro converters and have good coverage. See writing tests for Confluence imports.