Wiki source code of Writing macro converters
Last modified by Raphaël Jakse on 2026/03/23 13:17
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | {{toc/}} | ||
| 2 | |||
| 3 | == Introduction == | ||
| 4 | |||
| 5 | Like XWiki, Confluence allows using macros, in both the old legacy confluence syntax and the Confluence XHTML syntax. | ||
| 6 | |||
| 7 | 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. | ||
| 8 | |||
| 9 | 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. | ||
| 10 | |||
| 11 | == The MacroConverter interface == | ||
| 12 | |||
| 13 | Macro converters implement the [[MacroConverter interface>>https://github.com/xwiki-contrib/confluence/blob/master/confluence-xml/src/main/java/org/xwiki/contrib/confluence/filter/MacroConverter.java]], which consists of three methods: | ||
| 14 | |||
| 15 | === toXWiki === | ||
| 16 | |||
| 17 | {{code language="java"}} | ||
| 18 | void toXWiki(String id, Map<String, String> parameters, String content, boolean inline, Listener listener); | ||
| 19 | |||
| 20 | {{/code}} | ||
| 21 | |||
| 22 | 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. | ||
| 23 | |||
| 24 | === toXWikiId === | ||
| 25 | |||
| 26 | {{code language="java"}} | ||
| 27 | default String toXWikiId(String id, Map<String, String> parameters, String content, boolean inline) | ||
| 28 | |||
| 29 | {{/code}} | ||
| 30 | |||
| 31 | 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. | ||
| 32 | |||
| 33 | === supportsInlineMode === | ||
| 34 | |||
| 35 | {{code language="java"}} | ||
| 36 | default InlineSupport supportsInlineMode(String id, Map<String, String> parameters, String content) | ||
| 37 | |||
| 38 | {{/code}} | ||
| 39 | |||
| 40 | 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. | ||
| 41 | |||
| 42 | 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>>https://github.com/xwiki-contrib/confluence/blob/a9155f502cf8ce24b5c96d799575b749877e3e8a/confluence-xml/src/main/java/org/xwiki/contrib/confluence/filter/internal/macros/DefaultMacroConverter.java#L131-L136]] 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: | ||
| 43 | |||
| 44 | * 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 | ||
| 45 | * This is potentially expensive | ||
| 46 | * 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. | ||
| 47 | |||
| 48 | == Implementing macro converters by extending AbstractMacroConverter == | ||
| 49 | |||
| 50 | You should not generally directly implement the MacroConverter interface. Instead, we strongly recommend that you extend the [[AbstractMacroConverter>>https://github.com/xwiki-contrib/confluence/blob/master/confluence-xml/src/main/java/org/xwiki/contrib/confluence/filter/internal/macros/AbstractMacroConverter.java]] class, which provides: | ||
| 51 | |||
| 52 | * convenient methods to convert to an XWiki macro without having to mess around with listeners | ||
| 53 | * 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: | ||
| 54 | ** issuing warnings when parameters have not been used ([[https:~~/~~/jira.xwiki.org/browse/CONFLUENCE-411>>https://jira.xwiki.org/browse/CONFLUENCE-411]]) | ||
| 55 | ** keeping, or not, the unused parameters or all original parameters, with the configured prefix for this ([[https:~~/~~/jira.xwiki.org/browse/CONFLUENCE-454>>https://jira.xwiki.org/browse/CONFLUENCE-454]]) | ||
| 56 | ** enforcing the implementation of the supportsInlineMode method (because it missing can lead to conversion bugs) | ||
| 57 | |||
| 58 | {{warning}} | ||
| 59 | AbstractMacroConverter[[ is currently an internal class>>https://jira.xwiki.org/browse/CONFLUENCE-240]]. [[We are working on this>>https://github.com/xwiki-contrib/confluence/pull/70]] and a public class which will be mostly but not totally backward-compatible should land soon. | ||
| 60 | |||
| 61 | Backward-compatibility breakages will be about enforcing some rules, there won't be surprising behavior change. If your macro converter compiles with the new public class unchanged, it will behave the same way. | ||
| 62 | |||
| 63 | We promise to give ample time for existing code to adjust by keeping the internal class around. It will be marked as deprecated so warnings show up in code editors. | ||
| 64 | {{/warning}} | ||
| 65 | |||
| 66 | Implementing a macro converter using AbstractMacro consist of the following steps. | ||
| 67 | |||
| 68 | === Implement toXWiki (optional) === | ||
| 69 | |||
| 70 | 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>>||anchor="HtoXWiki"]]. 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>>||anchor="HMarkingparametersasunhandled"]], and you [[abort the conversion>>||anchor="HAbortingtheconversion"]] if that know that you can't convert something meaningfully. | ||
| 71 | |||
| 72 | The default implementation will call the following methods. | ||
| 73 | |||
| 74 | === Override toXWikiId === | ||
| 75 | |||
| 76 | This is mandatory. This gives the name of the target macro. If you are not converting to a macro, return null. | ||
| 77 | |||
| 78 | === Override toXWikiContent (optional) === | ||
| 79 | |||
| 80 | 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. | ||
| 81 | |||
| 82 | === Override toXWikiParameters === | ||
| 83 | |||
| 84 | {{code language="java"}} | ||
| 85 | protected Map<String, String> toXWikiParameters(String confluenceId, Map<String, String> confluenceParameters, String content) | ||
| 86 | {{/code}} | ||
| 87 | |||
| 88 | This returns the target parameters with their values. Here are some advise to follow: | ||
| 89 | |||
| 90 | * 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. | ||
| 91 | * **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. | ||
| 92 | * 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>>||anchor="HMarkingparametersas28un29handled"]], it will help the aforementioned feature to do its job correctly. | ||
| 93 | * [[mark parameters or parameter values as (un)handled>>||anchor="HMarkingparametersas28un29handled"]] | ||
| 94 | * 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. | ||
| 95 | * If you know that you can't convert something, [[abort the conversion>>||anchor="HAbortingtheconversion"]] | ||
| 96 | * If you are not actually converting to a macro, you can return null. | ||
| 97 | |||
| 98 | === Marking parameters as (un)handled === | ||
| 99 | |||
| 100 | 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. | ||
| 101 | |||
| 102 | There are several cases where this can happen, for instance: | ||
| 103 | |||
| 104 | * 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 | ||
| 105 | * Nobody has gotten around to implement the conversion | ||
| 106 | |||
| 107 | 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>>https://github.com/xwiki-contrib/confluence/blob/master/confluence-xml/src/main/java/org/xwiki/contrib/confluence/filter/internal/macros/TracedMap.java]]. This means that in most cases, you don't have to think about marking your parameters as unhandled if you follow the recommendation. | ||
| 108 | |||
| 109 | In addition to this mechanism, there are several methods you can use to correctly mark the parameters and the parameters. | ||
| 110 | |||
| 111 | ==== ##markHandledParameter## ==== | ||
| 112 | |||
| 113 | {{code language="java"}} | ||
| 114 | protected void markHandledParameter(Map<String, String> confluenceParameters, String name, boolean handled) | ||
| 115 | {{/code}} | ||
| 116 | |||
| 117 | 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. | ||
| 118 | |||
| 119 | {{warning}} | ||
| 120 | You need to pass confluenceParameters as it it passed to your toXWikiParameters method. You can't pass a copy of it, or some other map. | ||
| 121 | |||
| 122 | This parameter is actually a traced map in which the information about which parameters are handled or not is stored. | ||
| 123 | {{/warning}} | ||
| 124 | |||
| 125 | {{info}} | ||
| 126 | If the parameter name is known but the *value* is unhandled, see the next section about ##markUnhandledParameterValue##. | ||
| 127 | {{/info}} | ||
| 128 | |||
| 129 | ==== ##markUnhandledParameterValue## ==== | ||
| 130 | |||
| 131 | {{code language="java"}} | ||
| 132 | protected void markUnhandledParameterValue(Map<String, String> confluenceParameters, String parameterName) | ||
| 133 | {{/code}} | ||
| 134 | |||
| 135 | 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. | ||
| 136 | |||
| 137 | == Aborting the conversion == | ||
| 138 | |||
| 139 | 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. | ||
| 140 | |||
| 141 | 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. | ||
| 142 | |||
| 143 | == Handling Confluence references and URLs == | ||
| 144 | |||
| 145 | 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. | ||
| 146 | |||
| 147 | To do this, you will need to inject [[ConfluenceConverter>>https://github.com/xwiki-contrib/confluence/tree/master/confluence-xml/src/main/java/org/xwiki/contrib/confluence/filter/internal/input/ConfluenceConverter.java]] and use its methods. | ||
| 148 | |||
| 149 | {{info}} | ||
| 150 | ConfluenceConverter is currently internal. [[We are renaming it to ConfluenceReferenceConverter and making it public.>>https://github.com/xwiki-contrib/confluence/pull/70]] | ||
| 151 | {{/info}} | ||
| 152 | |||
| 153 | (% class="wikigeneratedid" %) | ||
| 154 | To convert URLs, you will need to inject [[ConfluenceURLConverter>>https://github.com/xwiki-contrib/confluence/tree/master/confluence-syntax-xhtml/src/main/java/org/xwiki/contrib/confluence/parser/xhtml/ConfluenceURLConverter.java]] and use the convertURL method. | ||
| 155 | |||
| 156 | {{warning}} | ||
| 157 | It is sometimes not possible to find the right XWiki reference (because, for instance, stuff that is target is not yet imported in the wiki). In such cases, Confluence references will be returned, in the hope that they will be fixed later. These references will work most of the time as soon as the target of these references are made available in the wiki. They will usually not work in macro parameters unless the macro explicitly supports them, and a fix will need to be found after the import. | ||
| 158 | {{/warning}} | ||
| 159 | |||
| 160 | (% class="wikigeneratedid" %) | ||
| 161 | See also how [[Confluence references and URLs are handled>>xwiki:documentation.extensions.admin.confluence.reference-handling.WebHome]] for more information and background on this. | ||
| 162 | |||
| 163 | == Writing a Macro Converter test == | ||
| 164 | |||
| 165 | It is very important to test your macro converters and have good coverage. See writing tests for Confluence imports. |