/* * Copyright (C) 2003-2010 eXo Platform SAS. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see<http://www.gnu.org/licenses/>. */ package org.exoplatform.wiki.rendering.render.confluence; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.xwiki.rendering.listener.Format; import org.xwiki.rendering.listener.HeaderLevel; import org.xwiki.rendering.listener.ListType; import org.xwiki.rendering.listener.chaining.BlockStateChainingListener; import org.xwiki.rendering.listener.chaining.ListenerChain; import org.xwiki.rendering.listener.chaining.StackableChainingListener; import org.xwiki.rendering.listener.reference.ResourceReference; import org.xwiki.rendering.renderer.AbstractChainingPrintRenderer; import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter; import org.xwiki.rendering.renderer.printer.VoidWikiPrinter; import org.xwiki.rendering.renderer.printer.WikiPrinter; import org.xwiki.rendering.renderer.reference.ResourceReferenceSerializer; /** * Created by The eXo Platform SAS * Author : viet nguyen * viet.nguyen@exoplatform.com * Jul 2, 2010 */ /** * Convert listener events to Confluence Syntax 1.0 output. */ public class ConfluenceSyntaxChainingRenderer extends AbstractChainingPrintRenderer implements StackableChainingListener { private ConfluenceSyntaxLinkRenderer linkRenderer; private ConfluenceSyntaxImageRenderer imageRenderer; private ConfluenceSyntaxMacroRenderer macroPrinter; private ResourceReferenceSerializer linkReferenceSerializer; // Custom States private boolean isFirstElementRendered = false; private StringBuffer listStyle = new StringBuffer(); private Map<String, String> previousFormatParameters; public ConfluenceSyntaxChainingRenderer(ListenerChain listenerChain, ResourceReferenceSerializer linkReferenceSerializer) { setListenerChain(listenerChain); this.linkReferenceSerializer = linkReferenceSerializer; this.linkRenderer = new ConfluenceSyntaxLinkRenderer(getConfluenceSyntaxListenerChain(), linkReferenceSerializer); this.imageRenderer = new ConfluenceSyntaxImageRenderer(); this.macroPrinter = new ConfluenceSyntaxMacroRenderer(); } // State private BlockStateChainingListener getBlockState() { return getConfluenceSyntaxListenerChain().getBlockStateChainingListener(); } /** * {@inheritDoc} * * @see StackableChainingListener#createChainingListenerInstance() */ public StackableChainingListener createChainingListenerInstance() { ConfluenceSyntaxChainingRenderer renderer = new ConfluenceSyntaxChainingRenderer(getListenerChain(), this.linkReferenceSerializer); renderer.setPrinter(getPrinter()); return renderer; } private ConfluenceSyntaxListenerChain getConfluenceSyntaxListenerChain() { return (ConfluenceSyntaxListenerChain) getListenerChain(); } private ConfluenceSyntaxLinkRenderer getLinkRenderer() { return this.linkRenderer; } private ConfluenceSyntaxImageRenderer getImageRenderer() { return this.imageRenderer; } private ConfluenceSyntaxMacroRenderer getMacroPrinter() { return this.macroPrinter; } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.chaining.AbstractChainingListener#beginGroup(Map) */ @Override public void beginGroup(Map<String, String> parameters) { if (!getBlockState().isInLine()) { printEmptyLine(); } if (parameters.size() > 0) { printParameters(parameters, true); } print("{group}"); // Create a new listener stack in order to preserve current states, to // handle the group. getListenerChain().pushAllStackableListeners(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.chaining.AbstractChainingListener#endDocument(java.util.Map) */ @Override public void endDocument(Map<String, String> parameters) { // Ensure that all data in the escape printer have been flushed getConfluencePrinter().flush(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.chaining.AbstractChainingListener#endGroup(Map) */ @Override public void endGroup(Map<String, String> parameters) { print("{group}"); // Restore previous listeners that were stacked getListenerChain().popAllStackableListeners(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#beginLink(org.xwiki.rendering.listener.Link, * boolean, java.util.Map) */ @Override public void beginLink(ResourceReference link, boolean isFreeStandingURI, Map<String, String> parameters) { // Flush test content before the link. // TODO: improve the block state renderer to be able to make the difference between what is bufferized // before the link and what in the label link getConfluencePrinter().setBeforeLink(true); // escape open link syntax when before a link if (getLinkRenderer().forceFullSyntax(getConfluencePrinter(), isFreeStandingURI, parameters) && getConfluencePrinter().getBuffer().length() > 0 && getConfluencePrinter().getBuffer() .charAt(getConfluencePrinter().getBuffer().length() - 1) == '[') { getConfluencePrinter().setEscapeLastChar(true); } getConfluencePrinter().flush(); getConfluencePrinter().setBeforeLink(false); int linkDepth = getBlockState().getLinkDepth(); // If we are at a depth of 2 or greater it means we're in a link inside a link and in this case we // shouldn't output the nested link as a link unless it's a free standing link. if (linkDepth < 2) { getLinkRenderer().beginRenderLink(getConfluencePrinter(), link, isFreeStandingURI, parameters); ConfluenceSyntaxEscapeWikiPrinter linkLabelPrinter = new ConfluenceSyntaxEscapeWikiPrinter(new DefaultWikiPrinter(), getConfluenceSyntaxListenerChain()); // Make sure the escape handler knows there is already characters before linkLabelPrinter.setOnNewLine(getConfluencePrinter().isOnNewLine()); // Defer printing the link content since we need to gather all nested // elements pushPrinter(linkLabelPrinter); } else if (isFreeStandingURI) { print(getLinkRenderer().serialize(link)); } } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#endLink(org.xwiki.rendering.listener.Link, * boolean, java.util.Map) */ @Override public void endLink(ResourceReference link, boolean isFreeStandingURI, Map<String, String> parameters) { // The links in a top level link label are not rendered as link (only the // label is printed) if (getBlockState().getLinkDepth() == 1) { ConfluenceSyntaxEscapeWikiPrinter linkBlocksPrinter = getConfluencePrinter(); linkBlocksPrinter.flush(); String content = linkBlocksPrinter.toString(); popPrinter(); getLinkRenderer().renderLinkContent(getConfluencePrinter(), content); getLinkRenderer().endRenderLink(getConfluencePrinter(), link, isFreeStandingURI, parameters); } } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#beginFormat(org.xwiki.rendering.listener.Format, * java.util.Map) */ @Override public void beginFormat(Format format, Map<String, String> parameters) { // If the previous format had parameters and the parameters are different // from the current ones then close them if (this.previousFormatParameters != null) { if (parameters.isEmpty()) { // print("(%%)"); // this.previousFormatParameters = null; } else if (!this.previousFormatParameters.equals(parameters)) { this.previousFormatParameters = null; printParameters(parameters, false); } else { this.previousFormatParameters = null; } } else if (this.previousFormatParameters == null) { printParameters(parameters, false); } switch (format) { case BOLD: // Handle empty formatting parameters. if (this.previousFormatParameters != null) { getPrinter().print("(%%)"); this.previousFormatParameters = null; } getConfluencePrinter().printBeginBold(); break; case ITALIC: // Handle empty formatting parameters. if (this.previousFormatParameters != null) { getPrinter().print("(%%)"); this.previousFormatParameters = null; } getConfluencePrinter().printBeginItalic(); break; case STRIKEDOUT: print("-"); break; case UNDERLINED: print("+"); break; case SUPERSCRIPT: print("^"); break; case SUBSCRIPT: print("~"); break; case MONOSPACE: print("{{"); break; } } /** * {@inheritDoc} * * @see AbstractChainingPrintRenderer#endFormat(org.xwiki.rendering.listener.Format, * java.util.Map) */ @Override public void endFormat(Format format, Map<String, String> parameters) { switch (format) { case BOLD: print("*"); break; case ITALIC: getConfluencePrinter().printEndItalic(); break; case STRIKEDOUT: print("-"); break; case UNDERLINED: print("+"); break; case SUPERSCRIPT: print("^"); break; case SUBSCRIPT: print("~"); break; case MONOSPACE: print("}}"); break; } StringBuffer parametersStr = new StringBuffer(); for (Map.Entry<String, String> entry : parameters.entrySet()) { String value = entry.getValue(); String key = entry.getKey(); if (key != null && value != null) { if ("style".equals(key)) parametersStr.append("{span}"); } } print(parametersStr.toString()); if (!parameters.isEmpty()) { this.previousFormatParameters = parameters; } } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.PrintRenderer#beginParagraph(java.util.Map) */ @Override public void beginParagraph(Map<String, String> parameters) { printEmptyLine(); printParameters(parameters); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.PrintRenderer#endParagraph(java.util.Map) */ @Override public void endParagraph(Map<String, String> parameters) { this.previousFormatParameters = null; StringBuffer parametersStr = new StringBuffer(); for (Map.Entry<String, String> entry : parameters.entrySet()) { String value = entry.getValue(); String key = entry.getKey(); if (key != null && value != null) { if ("style".equals(key)) parametersStr.append("{div}"); } } print(parametersStr.toString()); // Ensure that any not printed characters are flushed. // TODO: Fix this better by introducing a state listener to handle escapes getConfluencePrinter().flush(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.PrintRenderer#onNewLine() */ @Override public void onNewLine() { // - If we're inside a table cell, a paragraph, a list or a section header then if we have already outputted // a new line before then this new line should be a line break in order not to break the table cell, // paragraph, list or section header. // - If the new line is the last element of the paragraph, list or section header then it should be a line break // as otherwise it'll be considered as an empty line event next time the generated syntax is read by the Confluence // parser. if (getBlockState().isInLine()) { if (getBlockState().isInTableCell()) { print("\\\\"); } else if (getConfluenceSyntaxListenerChain().getConsecutiveNewLineStateChainingListener() .getNewLineCount() > 1) { print("\\\\"); } else if (getConfluenceSyntaxListenerChain().getLookaheadChainingListener().getNextEvent().eventType.isInlineEnd()) { print("\\\\"); } else { print("\n"); } } else { print("\n"); } } /** * {@inheritDoc} * * @see AbstractChainingPrintRenderer#onMacro(String, java.util.Map, String, * boolean) */ @Override public void onMacro(String id, Map<String, String> parameters, String content, boolean isInline) { if (!isInline) { printEmptyLine(); print(getMacroPrinter().renderMacro(id, parameters, content, isInline)); } else { getConfluencePrinter().printInlineMacro(getMacroPrinter().renderMacro(id, parameters, content, isInline)); } } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#beginHeader(org.xwiki.rendering.listener.HeaderLevel, * String, java.util.Map) */ @Override public void beginHeader(HeaderLevel level, String id, Map<String, String> parameters) { printEmptyLine(); printParameters(parameters); print("h" + level.getAsInt() + ". "); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#endHeader(org.xwiki.rendering.listener.HeaderLevel, * String, java.util.Map) */ @Override public void endHeader(HeaderLevel level, String id, Map<String, String> parameters) { } /** * {@inheritDoc} * * @see AbstractChainingPrintRenderer#onWord(String) */ @Override public void onWord(String word) { printDelayed(word); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.PrintRenderer#onSpace() */ @Override public void onSpace() { printDelayed(" "); } /** * {@inheritDoc} * * @see AbstractChainingPrintRenderer#onSpecialSymbol(char) */ @Override public void onSpecialSymbol(char symbol) { printDelayed("" + symbol); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#beginList(org.xwiki.rendering.listener.ListType, * java.util.Map) */ @Override public void beginList(ListType listType, Map<String, String> parameters) { if (getBlockState().getListDepth() == 1) { printEmptyLine(); } else { getPrinter().print("\n"); } if (listType == ListType.BULLETED) { this.listStyle.append("*"); } else { this.listStyle.append("#"); } printParameters(parameters); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.PrintRenderer#beginListItem() */ @Override public void beginListItem() { if (getBlockState().getListItemIndex() > 0) { getPrinter().print("\n"); } print(this.listStyle.toString()); if (StringUtils.contains(this.listStyle.toString(), '#')) { //print("."); } print(" "); } /** * {@inheritDoc} * * @see AbstractChainingPrintRenderer#endList(org.xwiki.rendering.listener.ListType, * java.util.Map) */ @Override public void endList(ListType listType, Map<String, String> parameters) { this.listStyle.setLength(this.listStyle.length() - 1); // Ensure that any not printed characters are flushed. // TODO: Fix this better by introducing a state listener to handle escapes getConfluencePrinter().flush(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.chaining.AbstractChainingListener#endListItem() */ @Override public void endListItem() { this.previousFormatParameters = null; } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#beginMacroMarker(String, * java.util.Map, String, boolean) */ @Override public void beginMacroMarker(String name, Map<String, String> parameters, String content, boolean isInline) { if (!isInline) { printEmptyLine(); } // When we encounter a macro marker we ignore all other blocks inside since we're going to use the macro // definition wrapped by the macro marker to construct the confluence syntax. pushPrinter(new ConfluenceSyntaxEscapeWikiPrinter(VoidWikiPrinter.VOIDWIKIPRINTER, getConfluenceSyntaxListenerChain())); } /** * {@inheritDoc} * * @see AbstractChainingPrintRenderer#endMacroMarker(String, java.util.Map, * String, boolean) */ @Override public void endMacroMarker(String name, Map<String, String> parameters, String content, boolean isInline) { this.previousFormatParameters = null; popPrinter(); print(getMacroPrinter().renderMacro(name, parameters, content, isInline)); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#onId(String) */ @Override public void onId(String name) { print("{{id name=\"" + name + "\"}}"); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.Renderer#onHorizontalLine(Map) */ @Override public void onHorizontalLine(Map<String, String> parameters) { printEmptyLine(); printParameters(parameters); print("----"); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.Renderer#onVerbatim(String, boolean, Map) */ @Override public void onVerbatim(String protectedString, boolean isInline, Map<String, String> parameters) { if (!isInline) { printEmptyLine(); } printParameters(parameters); print("{{{"); getConfluencePrinter().printVerbatimContent(protectedString); print("}}}"); } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.Renderer#onEmptyLines(int) */ @Override public void onEmptyLines(int count) { print(StringUtils.repeat("\n", count)); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginDefinitionList(java.util.Map) */ @Override public void beginDefinitionList(Map<String, String> parameters) { if (getBlockState().getDefinitionListDepth() == 1 && !getBlockState().isInList()) { printEmptyLine(); } else { print("\n"); } printParameters(parameters); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginDefinitionTerm() */ @Override public void beginDefinitionTerm() { if (getBlockState().getDefinitionListItemIndex() > 0) { getPrinter().print("\n"); } if (this.listStyle.length() > 0) { print(this.listStyle.toString()); if (this.listStyle.charAt(0) == '#') { //print("."); } } print(StringUtils.repeat(":", getBlockState().getDefinitionListDepth() - 1)); print("; "); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginDefinitionDescription() */ @Override public void beginDefinitionDescription() { if (getBlockState().getDefinitionListItemIndex() > 0) { getPrinter().print("\n"); } if (this.listStyle.length() > 0) { print(this.listStyle.toString()); if (this.listStyle.charAt(0) == '#') { //print("."); } } print(StringUtils.repeat(":", getBlockState().getDefinitionListDepth() - 1)); print(": "); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.chaining.AbstractChainingListener#endDefinitionDescription() */ @Override public void endDefinitionDescription() { this.previousFormatParameters = null; getConfluencePrinter().flush(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.chaining.AbstractChainingListener#endDefinitionTerm() */ @Override public void endDefinitionTerm() { this.previousFormatParameters = null; getConfluencePrinter().flush(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginQuotation(java.util.Map) */ @Override public void beginQuotation(Map<String, String> parameters) { if (!getBlockState().isInQuotationLine()) { printEmptyLine(); } if (!parameters.isEmpty()) { printParameters(parameters); } } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginQuotationLine() */ @Override public void beginQuotationLine() { if (getBlockState().getQuotationLineIndex() > 0) { getPrinter().print("\n"); } print("bq. "); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.chaining.AbstractChainingListener#endQuotationLine() */ @Override public void endQuotationLine() { this.previousFormatParameters = null; getConfluencePrinter().flush(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginTable(java.util.Map) */ @Override public void beginTable(Map<String, String> parameters) { printEmptyLine(); if (!parameters.isEmpty()) { printParameters(parameters); } } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginTableCell(java.util.Map) */ @Override public void beginTableCell(Map<String, String> parameters) { print("|"); printParameters(parameters, false); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginTableHeadCell(java.util.Map) */ @Override public void beginTableHeadCell(Map<String, String> parameters) { print("||"); printParameters(parameters, false); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#beginTableRow(java.util.Map) */ @Override public void beginTableRow(Map<String, String> parameters) { if (getBlockState().getCellRow() > 0) { print("\n"); } printParameters(parameters, false); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#endTableCell(java.util.Map) */ @Override public void endTableCell(Map<String, String> parameters) { this.previousFormatParameters = null; // Ensure that any not printed characters are flushed. // TODO: Fix this better by introducing a state listener to handle escapes getConfluencePrinter().flush(); } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#endTableHeadCell(java.util.Map) */ @Override public void endTableHeadCell(Map<String, String> parameters) { this.previousFormatParameters = null; } /** * {@inheritDoc} * * @see org.xwiki.rendering.listener.Listener#onImage(org.xwiki.rendering.listener.Image, * boolean, Map) */ @Override public void onImage(ResourceReference image, boolean isFreeStandingURI, Map<String, String> parameters) { getImageRenderer().beginRenderImage(getConfluencePrinter()); getImageRenderer().renderImageContent(getConfluencePrinter(), getImageRenderer().renderImage(image)); getImageRenderer().endRenderImage(getConfluencePrinter(), parameters); } protected void printParameters(Map<String, String> parameters) { printParameters(parameters, true); } protected void printParameters(Map<String, String> parameters, boolean newLine) { StringBuffer parametersStr = new StringBuffer(); for (Map.Entry<String, String> entry : parameters.entrySet()) { String value = entry.getValue(); String key = entry.getKey(); if (key != null && value != null) { if ("style".equals(key)) if (!newLine) { parametersStr.append("{span:").append("style=\"").append(value).append("\"}"); } else { parametersStr.append("{div:").append("style=\"").append(value).append("\"}"); } } } print(parametersStr.toString()); } private void printDelayed(String text) { print(text, true); } private void print(String text) { print(text, false); } private void print(String text, boolean isDelayed) { // Handle empty formatting parameters. if (this.previousFormatParameters != null) { this.previousFormatParameters = null; } if (isDelayed) { getConfluencePrinter().printDelayed(text); } else { getPrinter().print(text); } } private void printEmptyLine() { if (this.isFirstElementRendered) { print("\n\n"); } else { this.isFirstElementRendered = true; } } /** * {@inheritDoc} * * @see org.xwiki.rendering.renderer.AbstractChainingPrintRenderer#setPrinter(org.xwiki.rendering.renderer.printer.WikiPrinter) */ public void setPrinter(WikiPrinter printer) { // If the printer is already a Confluence Syntax Escape printer don't wrap it again. This case happens when // the createChainingListenerInstance() method is called, ie when this renderer's state is stacked // (for example when a Group event is being handled). if (printer instanceof ConfluenceSyntaxEscapeWikiPrinter) { super.setPrinter(printer); } else { super.setPrinter(new ConfluenceSyntaxEscapeWikiPrinter(printer, (ConfluenceSyntaxListenerChain) getListenerChain())); } } /** * Allows exposing the additional methods of {@link ConfluenceSyntaxEscapeWikiPrinter}, namely the ability to delay * printing some text and the ability to escape characters that would otherwise have a meaning in Confluence syntax. */ public ConfluenceSyntaxEscapeWikiPrinter getConfluencePrinter() { return (ConfluenceSyntaxEscapeWikiPrinter) super.getPrinter(); } @Override protected void popPrinter() { // Ensure that any not printed characters are flushed getConfluencePrinter().flush(); super.popPrinter(); } }