/*
* 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();
}
}