/* * Copyright 2011 Luke Usherwood. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package net.bettyluke.tracinstant.ui; import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.bettyluke.tracinstant.data.Ticket; import net.bettyluke.tracinstant.prefs.TracInstantProperties; public class HtmlFormatter { private static final int MAX_DESCRIPTIONS = 50; private static final Pattern BUG_PATTERN = Pattern.compile("#([0-9]{1,8}+)"); private static final URL STYLESHEET_TRAC_RESOURCE = HtmlFormatter.class.getResource("res/trac.css"); private final static String HTML_HEADER = "<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"" + STYLESHEET_TRAC_RESOURCE + "\">"; private final static String HTML_END = "</html>"; /** * Remove a style that Java can't display, so that closed tickets display crossed out */ protected static String fixHyperlinks(String text) { return text.replaceAll("class=\\\"closed ticket", "class=\"closed"); } protected static String buildDescription(Ticket[] tickets, SearchTerm[] searchTerms) { if (tickets.length == 0) { return ""; } String body = buildBody(tickets); String highlighted = highlightMatches(body, searchTerms); return HTML_HEADER + fixHyperlinks(highlighted) + HTML_END; } private static String buildBody(Ticket[] tickets) { StringBuilder body = new StringBuilder("<body style=\"margin:0;\">"); int count = 0; for (Ticket ticket : tickets) { if ((++count) > MAX_DESCRIPTIONS) { body.append("<br><i>Limit of " + MAX_DESCRIPTIONS + " tickets reached."); break; } String background = (count % 2 == 0) ? "#ffffd0" : "#ffffff"; body.append("<div style=\"background:" + background + "; padding:3px;\">"); String heading = makeHyperlinkedHeading(ticket); if (heading != null) { body.append(heading); } String description = ticket.getValue("description"); if (description == null) { body.append("<br><i>Trac query in progress...</i><br>  "); body.append("</div>"); break; } body.append(description); body.append("</div>"); } body.append("</body>"); return body.toString(); } private static String highlightMatches(String body, SearchTerm[] searchTerms) { Pattern superPattern = createSuperPattern(searchTerms); if (superPattern == null) { return body; } StringBuilder bb = new StringBuilder(); int rangeStart = 0, rangeEnd = 0, replacements = 0; while (true) { rangeEnd = body.indexOf('>', rangeStart) + 1; if (rangeEnd == -1) { break; } // Append up to end of a tag without modification. bb.append(body.substring(rangeStart, rangeEnd)); rangeStart = rangeEnd; rangeEnd = body.indexOf('<', rangeStart); if (rangeEnd == -1) { break; } // Append a text segment with any matching highlighting marked-up String text = body.substring(rangeStart, rangeEnd); Matcher m = superPattern.matcher(text); replacements += appendHighlightedText(bb, m, text); // Limit to a reasonable number of highlights; not just so that this method // doesn't spiral out of control (time, memory) but also the HTML renderer. if (replacements > 800) { break; } rangeStart = rangeEnd; } bb.append(body.substring(rangeStart, body.length())); return bb.toString(); } /** Like <code>Matcher.replaceAll()</code> but also counting replacements. */ private static int appendHighlightedText(StringBuilder bb, Matcher m, String text) { int replacements = 0; if (m.find()) { StringBuffer sb = new StringBuffer(); do { m.appendReplacement(sb, "<font color=\"white\" bgcolor=\"#66dd88\">$0</font>"); ++replacements; } while (m.find()); m.appendTail(sb); bb.append(sb.toString()); } else { bb.append(text); } return replacements; } private static Pattern createSuperPattern(SearchTerm[] searchTerms) { StringBuilder sb = new StringBuilder(); String pipe = ""; sb.append('('); for (SearchTerm term : searchTerms) { // TODO: create constants for special fields such as "description" if (term.field == null || "description".startsWith(term.field.toLowerCase())) { sb.append(pipe).append(term.pattern.toString()); pipe = "|"; } } sb.append(')'); if (sb.length() == 2) { return null; } return Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE); } private static String makeHyperlinkedHeading(Ticket ticket) { String title = ticket.getValue("title"); if (title == null) { return null; } title = BUG_PATTERN.matcher(title).replaceAll( "<a style=\"text-decoration: none;\" " + "href=\"" + TracInstantProperties.getURL() + "/ticket/$1\">#$1</a>"); return "<h2 style=\"color:#770044;\">" + title + "</h2>"; } }