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    }