/*
* Copyright (c) 2014-2016 Jan Strauß <jan[at]over9000.eu>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package eu.over9000.skadi.util;
import eu.over9000.cathode.data.PanelData;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;
import org.pegdown.Extensions;
import org.pegdown.PegDownProcessor;
import org.pegdown.ast.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Set;
import java.util.TreeSet;
/**
* Markdown handling based on bitbucket.org/shemnon/flowdown/
*/
public class PanelUtil {
public static final String STYLE_CLASS_EMPH = "md-emph";
public static final String STYLE_CLASS_STRONG = "md-strong";
public static final String STYLE_CLASS_STRIKE = "md-strike";
public static final String STYLE_CLASS_HEADER = "md-header";
public static final String STYLE_CLASS_HEADER_BASE = "md-header-";
public static final String STYLE_CLASS_VERBATIM = "md-verbatim";
public static final String STYLE_CLASS_PARA = "md-para";
public static final String STYLE_CLASS_BLOCKQUOTE = "md-blockquote";
public static final String STYLE_CLASS_ORDERED_LIST = "md-ordered-list";
public static final String STYLE_CLASS_UNORDERED_LIST = "md-unordered-list";
public static final String STYLE_CLASS_BULLET = "md-bullet";
public static final String STYLE_CLASS_LIST_CONTENT = "md-list-content";
public static final String STYLE_CLASS_SEPARATOR = "md-separator";
private static final Logger LOGGER = LoggerFactory.getLogger(PanelUtil.class);
public static VBox buildPanel(final PanelData panel) {
final VBox box = new VBox();
box.setMaxWidth(200);
final Label lbTitle = new Label(panel.getTitle());
lbTitle.setFont(new Font(18));
box.getChildren().add(lbTitle);
if ((panel.getLink() != null) && !panel.getLink().isEmpty() && (panel.getImage() != null) && !panel.getImage().isEmpty()) {
final ImageView img = new ImageView(ImageUtil.getImageInternal(panel.getImage()));
img.setPreserveRatio(true);
img.setFitWidth(200);
final Hyperlink banner = new Hyperlink(null, img);
banner.setTooltip(new Tooltip(panel.getLink()));
banner.setOnAction(event -> DesktopUtil.openWebpage(panel.getLink()));
box.getChildren().add(banner);
} else if ((panel.getImage() != null) && !panel.getImage().isEmpty()) {
final ImageView img = new ImageView(ImageUtil.getImageInternal(panel.getImage()));
img.setPreserveRatio(true);
img.setFitWidth(200);
box.getChildren().add(img);
}
if ((panel.getDescription() != null) && !panel.getDescription().isEmpty()) {
box.getChildren().add(parseDescriptionFromMarkdown(panel.getDescription()));
}
return box;
}
private static VBox parseDescriptionFromMarkdown(final String markdown) {
final VBox result = new VBox();
final PegDownProcessor processor = new PegDownProcessor(Extensions.STRIKETHROUGH | Extensions.FENCED_CODE_BLOCKS);
final RootNode rootNode = processor.parseMarkdown(markdown.toCharArray());
// PanelUtil.visit(rootNode, "");
final MarkdownVisitor visitor = new MarkdownVisitor();
visitor.pushNode(result);
rootNode.accept(visitor);
result.getStylesheets().add("/styles/markdown.css");
result.setMaxWidth(200);
result.layout();
return result;
}
/**
* Debug: print the ast
*/
@SuppressWarnings("unused")
private static void visit(final Node node, final String intend) {
LOGGER.debug(intend + node);
node.getChildren().forEach(c -> visit(c, intend + " "));
}
private static class MarkdownVisitor implements Visitor {
final Set<String> cssClasses = new TreeSet<>();
final LinkedList<Integer> listCount = new LinkedList<>();
private final Deque<Pane> nodeStack = new LinkedList<>();
private Pane currentCollector;
private boolean isHyperlinkChild = false;
private String currentHyperlinkURL = null;
void buildLinkNode(String text, final String url) {
while (text.endsWith("\n")) {
text = text.substring(0, text.length() - 1);
}
final Hyperlink link = new Hyperlink(text);
link.setMaxWidth(200);
link.setTooltip(new Tooltip(url));
link.setWrapText(true);
link.setOnAction(event -> DesktopUtil.openWebpage(url));
currentCollector.getChildren().add(link);
}
void buildTextNode(String text) {
while (text.endsWith("\n")) {
text = text.substring(0, text.length() - 1);
}
final Text textNode = new Text(text);
textNode.getStyleClass().setAll(cssClasses);
textNode.getStyleClass().add("md-text");
currentCollector.getChildren().add(textNode);
}
public void popNode() {
if (!nodeStack.isEmpty()) {
nodeStack.pop();
}
currentCollector = nodeStack.peek();
}
public void pushNode(final Pane n) {
nodeStack.push(n);
currentCollector = n;
}
void visitChildren(final Node node) {
if (node == null) {
return; // defensive parsing
}
for (final Node child : node.getChildren()) {
child.accept(this);
}
}
void startListBox(final String cssClass) {
final VBox vbox = new VBox();
vbox.getStyleClass().setAll(cssClasses);
vbox.getStyleClass().add(cssClass);
currentCollector.getChildren().add(vbox);
vbox.setMinHeight(Region.USE_PREF_SIZE);
vbox.setMaxHeight(Region.USE_PREF_SIZE);
pushNode(vbox);
}
void startListRow(final String bullet) {
final Text bt = new Text(bullet);
bt.setTextAlignment(TextAlignment.RIGHT);
bt.setTextOrigin(VPos.BASELINE);
bt.getStyleClass().setAll(cssClasses);
bt.getStyleClass().add(STYLE_CLASS_BULLET);
final VBox bulletContent = new VBox();
bulletContent.setMinHeight(Region.USE_PREF_SIZE);
bulletContent.setMaxHeight(Region.USE_PREF_SIZE);
bulletContent.getStyleClass().setAll(cssClasses);
bulletContent.getStyleClass().add(STYLE_CLASS_LIST_CONTENT);
final HBox hb = new HBox();
hb.setMinHeight(Region.USE_PREF_SIZE);
hb.setMaxHeight(Region.USE_PREF_SIZE);
hb.setAlignment(Pos.BASELINE_LEFT);
hb.getChildren().setAll(bt, bulletContent);
currentCollector.getChildren().add(hb);
pushNode(bulletContent);
}
void stopListBox() {
popNode();
}
void stopListRow() {
popNode();
}
@Override
public void visit(final AbbreviationNode node) {
}
@Override
public void visit(final AnchorLinkNode node) {
buildTextNode(node.getText());
}
@Override
public void visit(final AutoLinkNode node) {
buildLinkNode(node.getText(), node.getText());
}
@Override
public void visit(final BlockQuoteNode node) {
final VBox vBox = new VBox();
vBox.getStyleClass().setAll(cssClasses);
vBox.getStyleClass().add(STYLE_CLASS_BLOCKQUOTE);
currentCollector.getChildren().add(vBox);
pushNode(vBox);
visitChildren(node);
popNode();
}
@Override
public void visit(final BulletListNode node) {
startListBox(STYLE_CLASS_UNORDERED_LIST);
listCount.push(null);
visitChildren(node);
listCount.pop();
stopListBox();
}
@Override
public void visit(final CodeNode node) {
}
@Override
public void visit(final DefinitionListNode node) {
visitChildren(node);
}
@Override
public void visit(final DefinitionNode node) {
visitChildren(node);
}
@Override
public void visit(final DefinitionTermNode node) {
visitChildren(node);
}
@Override
public void visit(final ExpImageNode node) {
}
@Override
public void visit(final ExpLinkNode node) {
isHyperlinkChild = true;
currentHyperlinkURL = node.url;
visitChildren(node);
isHyperlinkChild = false;
}
@Override
public void visit(final HeaderNode node) {
cssClasses.add(STYLE_CLASS_HEADER_BASE + node.getLevel());
cssClasses.add(STYLE_CLASS_HEADER);
final TextFlow fp = new TextFlow();
fp.getStyleClass().setAll(cssClasses);
currentCollector.getChildren().add(fp);
pushNode(fp);
visitChildren(node);
popNode();
cssClasses.remove(STYLE_CLASS_HEADER_BASE + node.getLevel());
cssClasses.remove(STYLE_CLASS_HEADER);
}
@Override
public void visit(final HtmlBlockNode node) {
}
@Override
public void visit(final InlineHtmlNode node) {
}
@Override
public void visit(final ListItemNode node) {
String bullet = "\u2022 ";
if (listCount.peek() != null) {
int i = listCount.pop();
bullet = Integer.toString(i) + ". ";
listCount.push(++i);
}
startListRow(bullet);
visitChildren(node);
stopListRow();
}
@Override
public void visit(final MailLinkNode node) {
}
@Override
public void visit(final Node node) {
}
@Override
public void visit(final OrderedListNode node) {
startListBox(STYLE_CLASS_ORDERED_LIST);
listCount.push(1);
visitChildren(node);
listCount.pop();
stopListBox();
}
@Override
public void visit(final ParaNode node) {
final VBox vbox = new VBox();
vbox.getStyleClass().setAll(cssClasses);
vbox.getStyleClass().add(STYLE_CLASS_PARA);
currentCollector.getChildren().add(vbox);
pushNode(vbox);
visitChildren(node);
popNode();
}
@Override
public void visit(final QuotedNode node) {
switch (node.getType()) {
case DoubleAngle:
buildTextNode("\u00AB");
visitChildren(node);
buildTextNode("\u00BB");
break;
case Double:
buildTextNode("\u201C");
visitChildren(node);
buildTextNode("\u201D");
break;
case Single:
buildTextNode("\u2018");
visitChildren(node);
buildTextNode("\u2019");
break;
}
}
@Override
public void visit(final ReferenceNode node) {
}
@Override
public void visit(final RefImageNode node) {
}
@Override
public void visit(final RefLinkNode node) {
}
@Override
public void visit(final RootNode node) {
visitChildren(node);
}
@Override
public void visit(final SimpleNode node) {
switch (node.getType()) {
case Apostrophe:
buildTextNode("\u2019");
break;
case Ellipsis:
buildTextNode("\u2026");
break;
case Emdash:
buildTextNode("\u2014");
break;
case Endash:
buildTextNode("\u2013");
break;
case Nbsp:
buildTextNode("\u00a0");
break;
case Linebreak:
popNode();
final TextFlow tf = new TextFlow();
tf.getStyleClass().setAll(cssClasses);
currentCollector.getChildren().add(tf);
pushNode(tf);
break;
case HRule:
final Separator sep = new Separator();
sep.getStyleClass().add(STYLE_CLASS_SEPARATOR);
currentCollector.getChildren().add(sep);
break;
}
}
@Override
public void visit(final SpecialTextNode node) {
buildTextNode(node.getText());
}
@Override
public void visit(final StrikeNode node) {
cssClasses.add(STYLE_CLASS_STRIKE);
visitChildren(node);
cssClasses.remove(STYLE_CLASS_STRIKE);
}
@Override
public void visit(final StrongEmphSuperNode node) {
if (node.isStrong()) {
cssClasses.add(STYLE_CLASS_STRONG);
visitChildren(node);
cssClasses.remove(STYLE_CLASS_STRONG);
} else {
cssClasses.add(STYLE_CLASS_EMPH);
visitChildren(node);
cssClasses.remove(STYLE_CLASS_EMPH);
}
}
@Override
public void visit(final SuperNode node) {
final TextFlow tf = new TextFlow();
currentCollector.getChildren().add(tf);
pushNode(tf);
visitChildren(node);
popNode();
}
@Override
public void visit(final TableBodyNode node) {
}
@Override
public void visit(final TableCaptionNode node) {
}
@Override
public void visit(final TableCellNode node) {
}
@Override
public void visit(final TableColumnNode node) {
}
@Override
public void visit(final TableHeaderNode node) {
}
@Override
public void visit(final TableNode node) {
}
@Override
public void visit(final TableRowNode node) {
}
@Override
public void visit(final TextNode node) {
if (isHyperlinkChild) {
buildLinkNode(node.getText(), currentHyperlinkURL);
} else {
buildTextNode(node.getText());
}
}
@Override
public void visit(final VerbatimNode node) {
cssClasses.add(STYLE_CLASS_VERBATIM);
cssClasses.add(STYLE_CLASS_VERBATIM + "-" + node.getType());
final TextFlow tf = new TextFlow();
tf.getStyleClass().setAll(cssClasses);
pushNode(tf);
buildTextNode(node.getText());
popNode();
currentCollector.getChildren().add(tf);
cssClasses.remove(STYLE_CLASS_VERBATIM);
cssClasses.remove(STYLE_CLASS_VERBATIM + "-" + node.getType());
}
@Override
public void visit(final WikiLinkNode node) {
}
}
}