001 package net.databinder.components;
002
003 import java.awt.font.TextAttribute;
004 import java.text.AttributedCharacterIterator;
005 import java.text.AttributedString;
006 import java.util.ArrayList;
007 import java.util.List;
008 import java.util.regex.Matcher;
009 import java.util.regex.Pattern;
010
011 import net.databinder.components.RenderedLabel.RenderedTextImageResource;
012
013 import org.apache.wicket.util.string.Strings;
014
015 /**
016 * Base class for rendered labels formated with a Markdown subset including **bold**
017 * __bold__ *italic* _italic_ and [link] appearance, as well as hard returns (space-space-newline)
018 * and paragraphs (newline-newline). Subclasses apply attributes to
019 * an AttributedString in the abstract attributeBold/Italic/Link methods.
020 * @see AttributedString
021 * @author Nathan Hamblen
022 */
023 public abstract class FormattedRenderedTextImageResource extends RenderedTextImageResource {
024 //matches links patters like `[foo]: http://example.com/ "Optional Title Here"`
025 private static Pattern footnoteLinks = Pattern.compile("^ *\\[.+\\]\\:\\s.+\n", Pattern.MULTILINE);
026
027 // matches single newlines that do not have two spaces before them
028 private static Pattern strayNewlines = Pattern.compile("(?<!( )|\n)\n(?!\n)");
029
030 // group 1: either beginning of string or not a \
031 // group 2: beginning format element, to be expelled
032 // group 3: reluuctantly matched string inside formatters
033 // group 4: ending format element, to be expelled
034 private static Pattern boldFormat = Pattern.compile("(\\A|[^\\\\])(_{2}|\\*{2})(.+?)(\\2)", Pattern.DOTALL);
035 private static Pattern italicFormat = Pattern.compile("(\\A|[^\\\\])(\\*|_)(.+?)(\\2)", Pattern.DOTALL);
036 private static Pattern linkFormat = Pattern.compile("(\\A|[^\\\\])(\\[)(.+?)(\\](\\(|\\[).+?(\\)|\\]))", Pattern.DOTALL);
037
038 // matches a slash used for escaping, to be expelled
039 private static Pattern escapedCharacter = Pattern.compile("(\\\\)[^\\\\]");
040
041 private enum Style {BOLD, ITALIC, LINK};
042
043 private static class Range{
044 Style style;
045 int start;
046 int end;
047 }
048
049 private static class MutableRangeString {
050 List<Range> ranges = new ArrayList<Range>(10);
051 StringBuilder string;
052 public MutableRangeString(String str) {
053 string = new StringBuilder(str);
054 }
055 void expell(int start, int end) {
056 string.delete(start, end);
057 for(Range r : ranges) {
058 if (r.end > start) {
059 r.end = r.end + start - end;
060 if (r.start > start)
061 r.start = r.start + start - end;
062 }
063 }
064 }
065 }
066
067 /** Apply style markers to ranges matching the given format pattern. */
068 private static void process(MutableRangeString rangeStr, Pattern p, Style style) {
069 int delta = 0;
070 Matcher m = p.matcher(rangeStr.string.toString());
071 while (m.find()) {
072 Range r = new Range();
073 r.style = style;
074 r.start = m.start(3) - delta;
075 r.end = m.end(3) - delta;
076
077 rangeStr.ranges.add(r);
078
079 rangeStr.expell(m.start(2) - delta, m.end(2) - delta);
080 delta += m.end(2) - m.start(2);
081 rangeStr.expell(m.start(4) - delta, m.end(4) - delta);
082 delta += m.end(4) - m.start(4);
083 }
084 }
085
086 /** @return string formatted with markdown subset */
087 protected String getFormattedTextString() {
088 return text;
089 }
090
091 /** @return string with attributes derived from formatting in getFormattedTextString() */
092 @Override
093 protected List<AttributedCharacterIterator> getAttributedLines() {
094 String markedtext = getFormattedTextString();
095 if (Strings.isEmpty(markedtext))
096 return null;
097
098 markedtext = footnoteLinks.matcher(markedtext).replaceAll("");
099 markedtext = strayNewlines.matcher(markedtext.trim()).replaceAll("");
100
101 MutableRangeString rangeStr = new MutableRangeString(markedtext);
102
103 process(rangeStr, boldFormat, Style.BOLD);
104 process(rangeStr, italicFormat, Style.ITALIC);
105 process(rangeStr, linkFormat, Style.LINK);
106
107 int delta = 0;
108 Matcher m = escapedCharacter.matcher(rangeStr.string.toString());
109 while (m.find()) {
110 rangeStr.expell(m.start(1) - delta, m.end(1) - delta);
111 delta++;
112 }
113
114 String text = rangeStr.string.toString();
115 AttributedString attributedText = new AttributedString(text);
116 attributedText.addAttribute(TextAttribute.FONT, font);
117
118 for (Range r : rangeStr.ranges) {
119 if (r.style == Style.BOLD)
120 attributeBold(attributedText, r.start, r.end);
121 else if (r.style == Style.ITALIC)
122 attributeItalic(attributedText, r.start, r.end);
123 else if (r.style == Style.LINK)
124 attributeLink(attributedText, r.start, r.end);
125 }
126 return splitAtNewlines(attributedText, text);
127 }
128
129 abstract void attributeBold(AttributedString string, int start, int end);
130 abstract void attributeItalic(AttributedString string, int start, int end);
131 abstract void attributeLink(AttributedString string, int start, int end);
132 }