package org.bsc.markdown;
import org.pegdown.ast.AbbreviationNode;
import org.pegdown.ast.AnchorLinkNode;
import org.pegdown.ast.AutoLinkNode;
import org.pegdown.ast.BlockQuoteNode;
import org.pegdown.ast.BulletListNode;
import org.pegdown.ast.CodeNode;
import org.pegdown.ast.DefinitionListNode;
import org.pegdown.ast.DefinitionNode;
import org.pegdown.ast.DefinitionTermNode;
import org.pegdown.ast.ExpImageNode;
import org.pegdown.ast.ExpLinkNode;
import org.pegdown.ast.HeaderNode;
import org.pegdown.ast.HtmlBlockNode;
import org.pegdown.ast.InlineHtmlNode;
import org.pegdown.ast.ListItemNode;
import org.pegdown.ast.MailLinkNode;
import org.pegdown.ast.Node;
import org.pegdown.ast.OrderedListNode;
import org.pegdown.ast.ParaNode;
import org.pegdown.ast.QuotedNode;
import org.pegdown.ast.RefImageNode;
import org.pegdown.ast.RefLinkNode;
import org.pegdown.ast.ReferenceNode;
import org.pegdown.ast.RootNode;
import org.pegdown.ast.SimpleNode;
import org.pegdown.ast.SpecialTextNode;
import org.pegdown.ast.StrikeNode;
import org.pegdown.ast.StrongEmphSuperNode;
import org.pegdown.ast.SuperNode;
import org.pegdown.ast.TableBodyNode;
import org.pegdown.ast.TableCaptionNode;
import org.pegdown.ast.TableCellNode;
import org.pegdown.ast.TableColumnNode;
import org.pegdown.ast.TableHeaderNode;
import org.pegdown.ast.TableNode;
import org.pegdown.ast.TableRowNode;
import org.pegdown.ast.TextNode;
import org.pegdown.ast.VerbatimNode;
import org.pegdown.ast.Visitor;
import org.pegdown.ast.WikiLinkNode;
import static java.lang.String.format;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.bsc.functional.F;
import org.pegdown.Extensions;
import static java.lang.String.format;
import org.parboiled.common.StringUtils;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
/**
*
* @author bsorrentino
*/
public abstract class ToConfluenceSerializer implements Visitor {
//list level
private int listLevel = 0;
// referenceNodes
private HashMap<String, ReferenceNode> referenceNodes = new HashMap<String, ReferenceNode>();
private StringBuilder _buffer = new StringBuilder( 500 * 1024 );
private final java.util.Stack<Node> nodeStack = new java.util.Stack<Node>();
public static int extensions() {
int EXT = Extensions.NONE;
EXT |= Extensions.FENCED_CODE_BLOCKS;
EXT |= Extensions.TABLES;
EXT |= Extensions.STRIKETHROUGH;
// EXT |= Extensions.SMARTS; // Breaks link including a dash -
return EXT;
}
/**
*
* @param text
* @param node
* @return [line,col]
*/
public static int[] lineAndColFromNode(String text, Node node) {
int lastEOL = 0;
int prevEOL = 0;
int length = text.length();
int pos = 0;
int line = 0;
int col = 0;
int offset = node.getStartIndex();
if (offset > length) {
offset = length;
}
while (pos < length) {
pos = text.indexOf('\n', pos);
if (pos == -1) break; // no EOL at the end or we hit the end
prevEOL = lastEOL;
lastEOL = pos;
if (pos > offset) {
break;
}
line++;
pos++;
}
if (prevEOL < offset && lastEOL >= offset) {
// offset is on this line
col = offset - prevEOL;
}
line++;
//System.out.println("offset : " + offset + " = (" + String.valueOf(line) + ":" + String.valueOf(col) + ")");
return new int[] {line,col};
}
@Override
public String toString() {
return _buffer.toString();
}
/**
* The home page title useful to manage #RefLinkNode
*
* @return home page title. nullable
*/
protected String getHomePageTitle() {
return null;
}
protected abstract void notImplementedYet( Node node );
protected StringBuilder bufferVisit( F<Void,Void> closure ) {
return bufferVisit( new StringBuilder(), closure );
}
protected StringBuilder bufferVisit( final StringBuilder _sb, F<Void,Void> closure ) {
final StringBuilder _original = _buffer;
_buffer = _sb;
try {
closure.call(null);
}
finally {
_buffer = _original;
}
return _sb;
}
protected interface FindPredicate<T extends Node> {
boolean f( T node, Node parent, int index );
}
/**
* process:
* note,warning,info,tip
*/
protected class SpecialPanelProcessor {
private String element;
private String title;
final FindPredicate<TextNode> isSpecialPanelText = new FindPredicate<TextNode>() {
@Override
public boolean f(TextNode p, Node parent, int index ) {
if( index != 0 ) return false;
if( "note:".equalsIgnoreCase(p.getText()) ) {
element = "note"; // SET ELEMENT TAG
return true;
}
if( "warning:".equalsIgnoreCase(p.getText()) ) {
element = "warning"; // SET ELEMENT TAG
return true;
}
if( "info:".equalsIgnoreCase(p.getText()) ) {
element = "info"; // SET ELEMENT TAG
return true;
}
if( "tip:".equalsIgnoreCase(p.getText()) ) {
element = "tip"; // SET ELEMENT TAG
return true;
}
return false;
}
};
boolean apply( BlockQuoteNode bqn ) {
element = null;
title = null;
java.util.List<Node> children = bqn.getChildren();
if( children.size() < 2 ) {
// We are sure to not have a title tag
return false;
}
final boolean result =
findByClass(bqn.getChildren().get(0),
StrongEmphSuperNode.class,
new FindPredicate<StrongEmphSuperNode>() {
@Override
public boolean f(StrongEmphSuperNode p, final Node parent, final int index) {
if( index!=0 || !p.isStrong() ) return false;
boolean found = findByClass(p,
TextNode.class, isSpecialPanelText );
if( found ) { // GET ELEMENT TITLE
final StringBuilder _sb = bufferVisit(new F<Void,Void>() {
@Override
public Void call(Void p) {
parent.getChildren().remove(0);
visitChildren(parent);
return null;
}
});
title = _sb.toString().trim();
}
return found;
}
});
if( result ) {
_buffer.append( format("\n{%s%s}\n", element, isNotBlank(title)? ":title=" + title : ""));
bqn.getChildren().remove(0);
visitChildren(bqn);
_buffer.append( format("\n{%s}\n", element) ).append('\n');
}
return result;
}
}
protected <T extends Node> void forEachChild( T node, FindPredicate<T> cb ) {
final java.util.List<Node> children = node.getChildren();
for (int index = 0 ; index < children.size() ; ++index) {
final Node child = children.get(index);
if( !cb.f( (T)child, node, index ) ) {
return ;
}
}
}
protected <T extends Node> void visitChildren(T node) {
for (Node child : node.getChildren()) {
child.accept(this);
}
}
protected <T extends Node, R extends Node> boolean findByClass(T node, final Class<R> clazz, final FindPredicate<R> predicate ) {
boolean result = false;
final java.util.List<Node> children = node.getChildren();
for (int index = 0 ; index < children.size() ; ++index) {
final Node child = children.get(index);
if( clazz.isInstance(child) && predicate.f(clazz.cast(child), node, index)) {
result = true;
} else {
result = findByClass( child, clazz, predicate);
}
if( result ) break;
}
return result;
}
final SpecialPanelProcessor specialPanelProcessor = new SpecialPanelProcessor();
@Override
public void visit(RootNode rn) {
for (final ReferenceNode referenceNode : rn.getReferences()) {
String ref = bufferVisit( new F<Void, Void>() {
@Override
public Void call(Void p) {
visitChildren(referenceNode);
return null;
}
}).toString();
referenceNodes.put(ref, referenceNode);
}
visitChildren(rn);
}
@Override
public void visit(SuperNode sn) {
visitChildren(sn);
}
@Override
public void visit(ParaNode pn) {
visitChildren(pn);
_buffer.append('\n');
}
@Override
public void visit(HeaderNode hn) {
_buffer.append( format( "\n\nh%s.", hn.getLevel()) );
visitChildren(hn);
_buffer.append("\n\n");
}
@Override
public void visit(final BlockQuoteNode bqn) {
final String text = bufferVisit(new F<Void,Void>() {
@Override
public Void call(Void p) {
visitChildren(bqn);
return null;
}
}).toString();
final String lines[] = text.split("\n");
if( lines.length == 1 ) {
_buffer.append('\n')
.append( "bq. ")
.append( text )
.append('\n');
return;
}
if( specialPanelProcessor.apply(bqn) ) return;
_buffer.append('\n')
.append( "{quote}" )
.append('\n')
.append( text )
.append('\n')
.append( "{quote}" )
.append('\n');
}
@Override
public void visit(TextNode tn) {
_buffer.append( tn.getText() );
}
@Override
public void visit(ExpLinkNode eln) {
_buffer.append( '[');
visitChildren(eln);
_buffer.append(format("|%s|%s]", eln.url, eln.title));
}
@Override
public void visit(VerbatimNode vn) {
final String lines[] = vn.getText().split("\n");
if( lines.length == 1 ) {
_buffer.append( "\n{noformat}")
.append(vn.getText())
.append( "{noformat}\n\n");
return;
}
if( vn.getType()==null || vn.getType().isEmpty() ) {
_buffer.append( "\n{noformat}\n")
.append(vn.getText())
.append("\n{noformat}\n\n")
;
return;
}
_buffer.append( format("\n{code:%s}\n", vn.getType()) )
.append(vn.getText())
.append("\n{code}\n\n")
;
}
@Override
public void visit(CodeNode cn) {
final String text = cn.getText();
final String lines[] = text.split("\n");
if( lines.length == 1 ) {
_buffer.append( "{{")
.append(text
.replace("{", "\\{")
.replace("}", "\\}"))
.append( "}}");
return;
}
_buffer
.append( "\n{code}")
.append('\n')
.append(text)
.append( "{code}")
.append('\n')
;
}
@Override
public void visit(StrongEmphSuperNode sesn) {
char sym = '*';
if( !sesn.isStrong() ) {
final String chars = sesn.getChars();
if( chars.equals("_")) sym = '_';
}
_buffer.append( sym);
visitChildren(sesn);
_buffer.append( sym );
}
@Override
public void visit(StrikeNode sn) {
_buffer.append("-");
visitChildren(sn);
_buffer.append("-");
}
@Override
public void visit(ListItemNode lin) {
visitChildren(lin);
}
@Override
public void visit(final ExpImageNode ein) {
// We always have a URL, relative or not
final ArrayList<String> alt = new ArrayList<String>();
boolean found = findByClass(ein, TextNode.class, new FindPredicate<TextNode>() {
@Override
public boolean f(TextNode node, Node parent, int index) {
alt.add(node.getText());
return true;
}
});
if (!found) {
throw new IllegalStateException("The alt name should be mandatory in Markdown for images: " + ein.url);
}
String titlePart = isNotBlank(ein.title) ? format("|title=\"%s\"", ein.title) : "";
_buffer.append( format( "!%s|alt=\"%s\"%s!", ein.url, alt.get(0), titlePart));
}
@Override
public void visit(final RefImageNode rin) {
final ArrayList<String> alt = new ArrayList<String>();
boolean found = findByClass(rin, TextNode.class, new FindPredicate<TextNode>() {
@Override
public boolean f(TextNode node, Node parent, int index) {
alt.add(node.getText());
return true;
}
});
if (!found) {
throw new IllegalStateException("The alt name should be mandatory in Markdown for images. ");
}
final SuperNode referenceKey = rin.referenceKey;
final String ref = getRefString(rin, referenceKey);
String url = ref;
String title = null;
ReferenceNode referenceNode = referenceNodes.get(ref);
if (referenceNode != null) {
if (isNotBlank(referenceNode.getUrl())) {
url = referenceNode.getUrl();
}
title = referenceNode.getTitle();
}
String titlePart = isNotBlank(title) ? format("|title=\"%s\"", title) : "";
_buffer.append( format( "!%s|alt=\"%s\"%s!", url, alt.get(0), titlePart));
}
private String getRefString(final SuperNode refnode, final SuperNode referenceKey) {
final String ref;
if( referenceKey != null ) {
ref = bufferVisit(new F<Void, Void>() {
@Override
public Void call(Void p) {
visitChildren(referenceKey);
return null;
}
}).toString();
} else {
// in case the refkey is not with the link, we use the references found in the root node
ref = bufferVisit(new F<Void, Void>() {
@Override
public Void call(Void p) {
visitChildren(refnode);
return null;
}
}).toString();
}
return ref;
}
private static boolean isNotBlank(String str) {
return str != null && str.length() > 0;
}
@Override
public void visit(HtmlBlockNode hbn) {
}
@Override
public void visit(InlineHtmlNode ihn) {
}
@Override
public void visit(TableHeaderNode thn) {
nodeStack.push(thn);
try {
visitChildren(thn);
}
finally {
assert thn == nodeStack.pop();
}
}
@Override
public void visit(TableBodyNode tbn) {
nodeStack.push(tbn);
try {
visitChildren(tbn);
}
finally {
assert tbn == nodeStack.pop();
}
}
@Override
public void visit(TableRowNode trn) {
final Node n = nodeStack.peek();
if( n instanceof TableHeaderNode )
_buffer.append("||");
else if( n instanceof TableBodyNode )
_buffer.append('|');
visitChildren(trn);
_buffer.append('\n');
}
@Override
public void visit(TableCaptionNode tcn) {
notImplementedYet(tcn);
}
@Override
public void visit(TableCellNode tcn) {
final Node n = nodeStack.peek();
visitChildren(tcn);
if( n instanceof TableHeaderNode )
_buffer.append("||");
else if( n instanceof TableBodyNode )
_buffer.append('|');
}
@Override
public void visit(final RefLinkNode rln) {
_buffer.append( '[' );
visitChildren(rln);
_buffer.append('|');
final String ref = getRefString(rln, rln.referenceKey);
final String parentPageTitle = getHomePageTitle();
String url = ref;
ReferenceNode referenceNode = referenceNodes.get(ref);
if (referenceNode != null && referenceNode.getUrl() != null && url.length() > 0) {
url = referenceNode.getUrl();
}
// If URL is a relative URL, we will create a link to the project
try {
new URL(url);
} catch (MalformedURLException e) {
// not a valid URL (hence a relative link)
if( parentPageTitle != null && !url.startsWith(parentPageTitle)) {
_buffer.append(parentPageTitle).append(" - ");
}
}
_buffer.append(url);
if (referenceNode != null && referenceNode.getTitle() != null) {
_buffer.append('|').append(referenceNode.getTitle());
}
_buffer.append( ']' );
}
@Override
public void visit(TableColumnNode tcn) {
notImplementedYet(tcn);
}
@Override
public void visit(TableNode tn) {
visitChildren(tn);
}
@Override
public void visit(AnchorLinkNode aln) {
_buffer.append( aln.getText() );
}
@Override
public void visit(SpecialTextNode stn) {
_buffer.append(stn.getText());
}
@Override
public void visit(AbbreviationNode an) {
notImplementedYet(an);
}
@Override
public void visit(AutoLinkNode aln) {
notImplementedYet(aln);
}
@Override
public void visit(DefinitionListNode dln) {
notImplementedYet(dln);
}
@Override
public void visit(DefinitionNode dn) {
notImplementedYet(dn);
}
@Override
public void visit(DefinitionTermNode dtn) {
notImplementedYet(dtn);
}
@Override
public void visit(MailLinkNode mln) {
notImplementedYet(mln);
}
@Override
public void visit(OrderedListNode oln) {
++listLevel;
try {
_buffer.append('\n');
for (Node child : oln.getChildren()) {
_buffer.append( StringUtils.repeat('#', listLevel) ).append(' ');
child.accept(this);
_buffer.append('\n');
}
_buffer.append('\n');
}finally {
--listLevel;
}
}
@Override
public void visit(BulletListNode bln) {
++listLevel;
try {
_buffer.append('\n');
for (Node child : bln.getChildren()) {
_buffer.append( StringUtils.repeat('*', listLevel) ).append(' ');
child.accept(this);
_buffer.append('\n');
}
_buffer.append('\n');
}finally {
--listLevel;
}
}
@Override
public void visit(QuotedNode qn) {
notImplementedYet(qn);
}
@Override
public void visit(ReferenceNode rn) {
// nothing to do. already done in RootNode
}
@Override
public void visit(SimpleNode sn) {
switch (sn.getType()) {
case HRule:
_buffer.append("----\n");
break;
case Linebreak:
_buffer.append("\n");
break;
case Nbsp:
_buffer.append(" ");
break;
case Emdash:
_buffer.append("—");
break;
case Endash:
_buffer.append("–");
break;
case Ellipsis:
_buffer.append("…");
break;
default:
notImplementedYet(sn);
}
}
@Override
public void visit(WikiLinkNode wln) {
notImplementedYet(wln);
}
@Override
public void visit(Node node) {
notImplementedYet(node);
}
}