package de.lessvoid.nifty.html;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.builder.ElementBuilder;
import de.lessvoid.nifty.builder.ImageBuilder;
import de.lessvoid.nifty.builder.PanelBuilder;
import de.lessvoid.nifty.spi.render.RenderFont;
import org.htmlparser.Tag;
import org.htmlparser.Text;
import org.htmlparser.util.Translate;
import org.htmlparser.visitors.NodeVisitor;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* A NodeVisitor for the HTML Parser project that will visit all HTML tags
* and translate them into Nifty elements using the Nifty Builder pattern.
* @author void
*/
public class NiftyVisitor extends NodeVisitor {
// errors in processing are added to that list
@Nonnull
private final List<String> errors = new ArrayList<String>();
// the PanelBuilder for the body tag
private PanelBuilder bodyPanel;
// helper class to create new builders
private final NiftyBuilderFactory niftyBuilderFactory;
// to allow nested block level elements later we must stack them
@Nonnull
private final Stack<PanelBuilder> blockElementStack = new Stack<PanelBuilder>();
// the current block level element
@Nullable
private PanelBuilder currentBlockElement;
// default font we use for generating text elements
@Nullable
private final String defaultFontName;
private final String defaultFontBoldName;
// current color
@Nullable
private String currentColor;
// this will be set to a different font name when a corresponding tag is being processed
@Nullable
private String differentFont;
// we collect all text nodes into this string buffer
@Nonnull
private final StringBuffer currentText = new StringBuffer();
// table
@Nullable
private PanelBuilder table;
// table row
@Nullable
private PanelBuilder tableRow;
// table data
@Nullable
private PanelBuilder tableData;
@Nonnull
private String fontHeight;
/**
* Create the NiftyVisitor.
* @param nifty the Nifty instance
* @param niftyBuilderFactory a helper class to create Nifty Builders
*/
public NiftyVisitor(@Nonnull final Nifty nifty, final NiftyBuilderFactory niftyBuilderFactory, @Nullable final String defaultFontName, final String defaultFontBoldName) {
this.niftyBuilderFactory = niftyBuilderFactory;
this.defaultFontName = defaultFontName;
this.defaultFontBoldName = defaultFontBoldName;
fontHeight = "0";
if (defaultFontName != null) {
RenderFont defaultFont = nifty.createFont(defaultFontName);
if (defaultFont != null) {
fontHeight = Integer.toString(defaultFont.getHeight());
}
}
}
/*
* (non-Javadoc)
* @see org.htmlparser.visitors.NodeVisitor#beginParsing()
*/
@Override
public void beginParsing () {
errors.clear();
}
/*
* (non-Javadoc)
* @see org.htmlparser.visitors.NodeVisitor#visitTag(org.htmlparser.Tag)
*/
@Override
public void visitTag(@Nonnull final Tag tag) {
try {
if (isBody(tag)) {
// we'll add a main panel for the body which will be the parent element for everything we generate
// this way we can decide the childLayout and other properties of the body panel.
bodyPanel = niftyBuilderFactory.createBodyPanelBuilder();
} else if (isParagraph(tag)) {
assertBodyPanelNotNull();
currentBlockElement = niftyBuilderFactory.createParagraphPanelBuilder();
blockElementStack.push(currentBlockElement);
} else if (isImageTag(tag)) {
ImageBuilder image = niftyBuilderFactory.createImageBuilder(
tag.getAttribute("src"),
tag.getAttribute("align"),
tag.getAttribute("width"),
tag.getAttribute("height"),
tag.getAttribute("bgcolor"),
tag.getAttribute("vspace"));
if (currentBlockElement != null) {
currentBlockElement.image(image);
} else {
bodyPanel.image(image);
}
} else if (isBreak(tag)) {
if (currentBlockElement != null) {
currentText.append("\n");
} else {
PanelBuilder breakPanelBuilder = niftyBuilderFactory.createBreakPanelBuilder(fontHeight);
bodyPanel.panel(breakPanelBuilder);
}
} else if (isTableTag(tag)) {
assertBodyPanelNotNull();
table = niftyBuilderFactory.createTableTagPanelBuilder(
tag.getAttribute("width"),
tag.getAttribute("bgcolor"),
tag.getAttribute("border"),
tag.getAttribute("bordercolor"));
} else if (isTableRowTag(tag)) {
assertTableNotNull();
tableRow = niftyBuilderFactory.createTableRowPanelBuilder(
tag.getAttribute("width"),
tag.getAttribute("bgcolor"),
tag.getAttribute("border"),
tag.getAttribute("bordercolor"));
} else if (isTableDataTag(tag)) {
assertTableRowNotNull();
tableData = niftyBuilderFactory.createTableDataPanelBuilder(
tag.getAttribute("width"),
tag.getAttribute("bgcolor"),
tag.getAttribute("border"),
tag.getAttribute("bordercolor"));
} else if (isFontTag(tag)) {
String color = tag.getAttribute("color");
if (color != null) {
currentColor = color;
}
} else if (isStrongTag(tag)) {
differentFont = defaultFontBoldName;
}
} catch (Exception e) {
addError(e);
}
}
/*
* (non-Javadoc)
* @see org.htmlparser.visitors.NodeVisitor#visitEndTag(org.htmlparser.Tag)
*/
@SuppressWarnings("StatementWithEmptyBody")
@Override
public void visitEndTag(@Nonnull final Tag tag) {
try {
if (isBody(tag)) {
// currently there is nothing to do when a body tag ends
} else if (isParagraph(tag)) {
if (bodyPanel == null) {
throw new Exception("This looks like HTML with a missing <body> tag");
}
if (currentBlockElement == null) {
throw new Exception("This looks like broken HTML. currentBlockElement seems null. Maybe a duplicate close tag?");
}
String textElement = currentText.toString();
if (textElement.length() > 0) {
addTextElement(currentBlockElement, textElement);
currentText.setLength(0);
}
if (currentBlockElement.getElementBuilders().isEmpty()) {
currentBlockElement.height(fontHeight);
}
bodyPanel.panel(currentBlockElement);
currentBlockElement = blockElementStack.pop();
differentFont = null;
} else if (isImageTag(tag)) {
// nothing to do
} else if (isBreak(tag)) {
// nothing to do
} else if (isTableTag(tag)) {
if (bodyPanel == null || table == null) {
throw new Exception("This looks like HTML with a missing <body> tag");
}
bodyPanel.panel(table);
table = null;
} else if (isTableRowTag(tag)) {
if (table == null || tableRow == null) {
throw new Exception("This looks like a <tr> element with a missing <table> tag");
}
table.panel(tableRow);
tableRow = null;
} else if (isTableDataTag(tag)) {
if (tableRow == null) {
throw new Exception("This looks like a <td> element with a missing <tr> tag");
}
if (tableData == null) {
throw new Exception("This looks like a <td> element with a missing <tr> tag");
}
addTextElement(tableData, currentText.toString());
currentText.setLength(0);
tableRow.panel(tableData);
tableData = null;
differentFont = null;
} else if (isFontTag(tag)) {
currentColor = null;
}
} catch (Exception e) {
addError(e);
}
}
/*
* (non-Javadoc)
* @see org.htmlparser.visitors.NodeVisitor#visitStringNode(org.htmlparser.Text)
*/
@Override
public void visitStringNode(@Nonnull final Text textNode) {
if (tableData != null) {
appendText(textNode);
} else if (currentBlockElement != null) {
appendText(textNode);
}
}
private void appendText(@Nonnull final Text textNode) {
if (currentColor != null) {
currentText.append("\\");
currentText.append(currentColor);
currentText.append("#");
}
currentText.append(removeNewLineAndTabs(translateHTMLEntities(textNode.getText())));
}
public ElementBuilder builder() throws Exception {
try {
assertBodyPanelNotNull();
} catch (Exception e) {
addAsFirstError(e);
}
assertNoErrors();
return bodyPanel;
}
// private stuff
private void addError(@Nonnull final Exception e) {
if (!errors.contains(e.getMessage())) {
errors.add(e.getMessage());
}
}
private void addAsFirstError(@Nonnull final Exception e) {
if (!errors.contains(e.getMessage())) {
errors.add(0, e.getMessage());
}
}
private void assertBodyPanelNotNull() throws Exception {
if (bodyPanel == null) {
throw new Exception("This looks like HTML with a missing <body> tag");
}
}
private void assertTableNotNull() throws Exception {
if (table == null) {
throw new Exception("This looks like a <tr> element with a missing <table> tag");
}
}
private void assertTableRowNotNull() throws Exception {
if (tableRow == null) {
throw new Exception("This looks like a <td> element with a missing <tr> tag");
}
}
private void addTextElement(@Nonnull final PanelBuilder panelBuilder, @Nonnull final String text) {
String font = defaultFontName;
if (differentFont != null) {
font = differentFont;
}
if (font == null) {
return;
}
panelBuilder.text(niftyBuilderFactory.createTextBuilder(text, font, currentColor));
}
private void assertNoErrors() throws Exception {
if (!errors.isEmpty()) {
StringBuilder message = new StringBuilder();
for (int i=0; i<errors.size(); i++) {
message.append(errors.get(i));
message.append("\n");
}
throw new Exception(message.toString());
}
}
private boolean isBody(@Nonnull final Tag tag) {
return "BODY".equals(tag.getTagName());
}
private boolean isParagraph(@Nonnull final Tag tag) {
return "P".equals(tag.getTagName());
}
private boolean isBreak(@Nonnull final Tag tag) {
return "BR".equals(tag.getTagName());
}
private boolean isImageTag(@Nonnull final Tag tag) {
return "IMG".equals(tag.getTagName());
}
private boolean isTableTag(@Nonnull final Tag tag) {
return "TABLE".equals(tag.getTagName());
}
private boolean isTableRowTag(@Nonnull final Tag tag) {
return "TR".equals(tag.getTagName());
}
private boolean isTableDataTag(@Nonnull final Tag tag) {
return "TD".equals(tag.getTagName());
}
private boolean isFontTag(@Nonnull final Tag tag) {
return "FONT".equals(tag.getTagName());
}
private boolean isStrongTag(@Nonnull final Tag tag) {
return "STRONG".equals(tag.getTagName());
}
private String removeNewLineAndTabs(@Nonnull final String text) {
return text.replaceAll("\n", "").replaceAll("\t", "");
}
private String translateHTMLEntities(final String text) {
return Translate.decode(text);
}
}