Remove tagsoup dependency. Improve mIRC color code parsing speed.

This commit is contained in:
liato 2011-03-18 01:23:57 +01:00 committed by Sebastian Kaspari
parent 7c4abe0c9c
commit 2189d2c05f
4 changed files with 98 additions and 1052 deletions

View File

@ -3,6 +3,5 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="lib" path="libs/tagsoup-1.2.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -23,15 +23,12 @@ package org.yaaic.model;
import java.util.Date;
import org.yaaic.utils.Colors;
import org.yaaic.utils.Html2;
import android.content.Context;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.widget.TextView;
@ -231,19 +228,12 @@ public class Message
String nick = hasSender() ? "<" + sender + "> " : "";
String timestamp = settings.showTimestamp() ? renderTimeStamp(settings.use24hFormat()) : "";
if (settings.showMircColors()) {
// Tagsoup doesn't like when a html string begins with a <font> tag so we'll surround the html with <pre> tags.
String entext = "<pre>"+TextUtils.htmlEncode(text).replaceAll(" ", "&nbsp;")+"</pre>";
String htmltext = Colors.mircColorParser(entext);
Spanned colortext = Html2.fromHtml(htmltext);
canvas = new SpannableString(prefix + timestamp + nick);
canvas = new SpannableString(TextUtils.concat(canvas, colortext));
canvas = new SpannableString(prefix + timestamp + nick + Colors.mircColorParserSpannable(text));
}
else {
canvas = new SpannableString(prefix + timestamp + nick + Colors.removeStyleAndColors(text));
}
if (hasSender()) {
int start = (prefix + timestamp).length() + 1;
int end = start + sender.length();
@ -259,7 +249,7 @@ public class Message
canvas.setSpan(new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (hasColor() && settings.showColors()) {
// Only apply the foreground color on areas that don't already have a foreground color.
// Only apply the foreground color to areas that don't already have a foreground color.
ForegroundColorSpan[] spans = canvas.getSpans(0, canvas.length(), ForegroundColorSpan.class);
int start = 0;
for (int i = 0; i < spans.length; i++) {

View File

@ -1,106 +1,141 @@
package org.yaaic.utils;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.util.Log;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
public class Colors {
/*
* Colors from the "Classic" theme in mIRC.
*/
public static final String[] colors = {
"#FFFFFF", // White
"#000000", // Black
"#00007F", // Blue (navy)
"#009300", // Green
"#FC0000", // Red
"#7F0000", // Brown (maroon)
"#9C009C", // Purple
"#FC7F00", // Orange (olive)
"#FFFF00", // Yellow
"#00FC00", // Light Green (lime)
"#008080", // Teal (a green/blue cyan)
"#00FFFF", // Light Cyan (cyan) (aqua)
"#0000FF", // Light Blue (royal)
"#FF00FF", // Pink (light purple) (fuchsia)
"#7F7F7F", // Grey
"#D2D2D2" // Light Grey (silver)
public static final int[] colors = {
0xFFFFFF, // White
0x000000, // Black
0x00007F, // Blue (navy)
0x009300, // Green
0xFC0000, // Red
0x7F0000, // Brown (maroon)
0x9C009C, // Purple
0xFC7F00, // Orange (olive)
0xFFFF00, // Yellow
0x00FC00, // Light Green (lime)
0x008080, // Teal (a green/blue cyan)
0x00FFFF, // Light Cyan (cyan) (aqua)
0x0000FF, // Light Blue (royal)
0xFF00FF, // Pink (light purple) (fuchsia)
0x7F7F7F, // Grey
0xD2D2D2 // Light Grey (silver)
};
private static final Pattern boldPattern = Pattern.compile("\\x02([^\\x02\\x0F]*)(?:\\x02|(\\x0F))?");
private static final Pattern underlinePattern = Pattern.compile("\\x1F([^\\x1F\\x0F]*)(?:\\x1F|(\\x0F))?");
private static final Pattern italicPattern = Pattern.compile("\\x1D([^\\x1D\\x0F]*)(?:\\x1D|(\\x0F))?");
private static final Pattern inversePattern = Pattern.compile("\\x16([^\\x16\\x0F]*)(?:\\x16|(\\x0F))?");
private static final Pattern boldPattern = Pattern.compile("\\x02([^\\x02\\x0F]*)(\\x02|(\\x0F))?");
private static final Pattern underlinePattern = Pattern.compile("\\x1F([^\\x1F\\x0F]*)(\\x1F|(\\x0F))?");
private static final Pattern italicPattern = Pattern.compile("\\x1D([^\\x1D\\x0F]*)(\\x1D|(\\x0F))?");
private static final Pattern inversePattern = Pattern.compile("\\x16([^\\x16\\x0F]*)(\\x16|(\\x0F))?");
private static final Pattern colorPattern = Pattern.compile("\\x03(\\d{1,2})(?:,(\\d{1,2}))?([^\\x03\\x0F]*)(\\x03|\\x0F)?");
private static final Pattern cleanupPattern = Pattern.compile("(?:\\x02|\\x1F|\\x1D|\\x0F|\\x16|\\x03(?:(?:\\d{1,2})(?:,\\d{1,2})?)?)");
private Colors() {}
/**
* Converts a string with mIRC color codes to a HTML string.
* Converts a string with mIRC style and color codes to a SpannableString with
* all the style and color codes applied.
*
* @param text A string with mIRC color codes.
* @return HTML string.
* @return A SpannableString with all the styles applied.
*/
public static String mircColorParser(String text) {
text = replaceControlCodes(boldPattern.matcher(text), "<b>", "</b>");
text = replaceControlCodes(underlinePattern.matcher(text), "<u>", "</u>");
text = replaceControlCodes(italicPattern.matcher(text), "<i>", "</i>");
// Inverse assumes that the background is black and the foreground is white.
text = replaceControlCodes(inversePattern.matcher(text), "<font bgcolor=\"" + colors[0] + "\" color=\"" + colors[1] + "\">", "</font>");
public static SpannableString mircColorParserSpannable(String text) {
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
replaceControlCodes(boldPattern.matcher(ssb), ssb, new StyleSpan(Typeface.BOLD));
replaceControlCodes(underlinePattern.matcher(ssb), ssb, new UnderlineSpan());
replaceControlCodes(italicPattern.matcher(ssb), ssb, new StyleSpan(Typeface.ITALIC));
StringBuffer sb = new StringBuffer(text);
StringBuilder ft = new StringBuilder();
Matcher m = colorPattern.matcher(text);
/*
* Inverse assumes that the background is black and the foreground is white.
* We apply the background color first and then apply the foreground color
* to all the parts where BackgroundColorSpans are found.
*/
replaceControlCodes(inversePattern.matcher(ssb), ssb, new BackgroundColorSpan(colors[0] | 0xFF000000));
BackgroundColorSpan[] inverseSpans = ssb.getSpans(0, ssb.length(), BackgroundColorSpan.class);
for (int i = 0; i < inverseSpans.length; i++) {
ssb.setSpan(new ForegroundColorSpan(colors[1] | 0xFF000000), ssb.getSpanStart(inverseSpans[i]),ssb.getSpanEnd(inverseSpans[i]), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
Matcher m = colorPattern.matcher(ssb);
while (m.find()) {
sb.delete(0, sb.length());
ft.delete(0, ft.length());
int start = m.start();
int end = m.end();
// Build the font tag
ft.append("<font");
Integer color = Integer.parseInt(m.group(1));
int codelength = m.group(1).length()+1;
if (color <= 15 && color >= 0) {
ft.append(" color=\""+colors[color]+"\"");
ssb.setSpan(new ForegroundColorSpan(colors[color] | 0xFF000000), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (m.group(2) != null) {
color = Integer.parseInt(m.group(2));
if (color <= 15 && color >= 0) {
ft.append(" bgcolor=\""+colors[color]+"\"");
ssb.setSpan(new BackgroundColorSpan(colors[color] | 0xFF000000), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
codelength = codelength + m.group(2).length() + 1;
}
ft.append(">");
ft.append(m.group(3));
ft.append("</font>");
if (m.group(4) != null) {
ft.append(m.group(4));
}
m.appendReplacement(sb, ft.toString());
m.appendTail(sb);
m.reset(sb.toString());
ssb.delete(start, start+codelength);
// Reset the matcher with the modified text so that the ending color code character can be matched again.
m.reset(ssb);
}
// Remove left over codes
Log.d("html", removeStyleAndColors(sb.toString()));
return removeStyleAndColors(sb.toString());
return new SpannableString(removeStyleAndColors(ssb));
}
private static String replaceControlCodes(Matcher m, String startTag, String endTag) {
/*
* matcher(...).replaceAll("<x>\1\2</x>") inserts "null" if the second
* capture group isn't found so we'll do it this way instead.
*/
StringBuffer sb = new StringBuffer();
private static void replaceControlCodes(Matcher m, SpannableStringBuilder ssb, CharacterStyle style) {
ArrayList<Integer> toremove = new ArrayList<Integer>();
while (m.find()) {
m.appendReplacement(sb, startTag+m.group(1)+endTag+(m.group(2) == null ? "" : m.group(2)));
toremove.add(0, m.start());
// Remove the ending control character unless it's \x0F
if (m.group(2) != null && m.group(2) != m.group(3)) {
toremove.add(0, m.end()-1);
}
ssb.setSpan(style, m.start(), m.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
for (Integer i : toremove) {
ssb.delete(i, i+1);
}
m.appendTail(sb);
return sb.toString();
}
/**
* Removes mIRC color and style codes and returns the message without them.
*
* @param text A message with mirc colors and styles.
* @return The same message with all the colors and styles removed.
*/
public static String removeStyleAndColors(String text) {
return cleanupPattern.matcher(text).replaceAll("");
}
/**
* Removes mIRC color and style codes and returns the message without them.
*
* @param text A message with mirc colors and styles.
* @return The same message with all the colors and styles removed.
*/
public static SpannableStringBuilder removeStyleAndColors(SpannableStringBuilder text) {
ArrayList<int[]> toremove = new ArrayList<int[]>();
Matcher m = cleanupPattern.matcher(text);
while (m.find()) {
toremove.add(0, new int[] {m.start(), m.end()});
}
for (int[] i : toremove) {
text.delete(i[0], i[1]);
}
return text;
}
}

View File

@ -1,978 +0,0 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.yaaic.utils;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import org.ccil.cowan.tagsoup.HTMLModels;
import org.ccil.cowan.tagsoup.HTMLSchema;
import org.ccil.cowan.tagsoup.Parser;
import org.ccil.cowan.tagsoup.Schema;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.yaaic.R;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.ParagraphStyle;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.TextAppearanceSpan;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.util.Log;
/**
* This class processes HTML strings into displayable styled text.
* Not all HTML tags are supported.
*
* The only difference between Html2 and android.text.Html is that Html2 adds support for
* a bgcolor attribute in the font tag.
*/
public class Html2 {
/**
* Retrieves images for HTML &lt;img&gt; tags.
*/
public static interface ImageGetter {
/**
* This methos is called when the HTML parser encounters an
* &lt;img&gt; tag. The <code>source</code> argument is the
* string from the "src" attribute; the return value should be
* a Drawable representation of the image or <code>null</code>
* for a generic replacement image. Make sure you call
* setBounds() on your Drawable if it doesn't already have
* its bounds set.
*/
public Drawable getDrawable(String source);
}
/**
* Is notified when HTML tags are encountered that the parser does
* not know how to interpret.
*/
public static interface TagHandler {
/**
* This method will be called when the HTML parser encounters
* a tag that it does not know how to interpret.
*/
public void handleTag(boolean opening, String tag,
Editable output, XMLReader xmlReader);
}
private Html2() { }
private static HTMLSchema schema = new HTMLSchema();
/**
* Returns displayable styled text from the provided HTML string.
* Any &lt;img&gt; tags in the HTML will display as a generic
* replacement image which your program can then go through and
* replace with real images.
*
* <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
*/
public static Spanned fromHtml(String source) {
return fromHtml(source, null, null);
}
/**
* Lazy initialization holder for HTML parser. This class will
* a) be preloaded by the zygote, or b) not loaded until absolutely
* necessary.
*/
private static class HtmlParser {
private final static HTMLSchema schema = new HTMLSchema();
}
/**
* Returns displayable styled text from the provided HTML string.
* Any &lt;img&gt; tags in the HTML will use the specified ImageGetter
* to request a representation of the image (use null if you don't
* want this) and the specified TagHandler to handle unknown tags
* (specify null if you don't want this).
*
* <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
*/
public static Spanned fromHtml(String source, ImageGetter imageGetter,
TagHandler tagHandler) {
Parser parser = new Parser();
// Make the font tag restartable
schema.elementType("font", Schema.M_PCDATA|HTMLModels.M_INLINE, HTMLModels.M_INLINE|HTMLModels.M_NOLINK, Schema.F_RESTART);
try {
parser.setProperty(Parser.schemaProperty, schema);
} catch (org.xml.sax.SAXNotRecognizedException e) {
// Should not happen.
throw new RuntimeException(e);
} catch (org.xml.sax.SAXNotSupportedException e) {
// Should not happen.
throw new RuntimeException(e);
}
HtmlToSpannedConverter converter =
new HtmlToSpannedConverter(source, imageGetter, tagHandler,
parser);
return converter.convert();
}
/**
* Returns an HTML representation of the provided Spanned text.
*/
public static String toHtml(Spanned text) {
StringBuilder out = new StringBuilder();
withinHtml(out, text);
return out.toString();
}
private static void withinHtml(StringBuilder out, Spanned text) {
int len = text.length();
int next;
for (int i = 0; i < text.length(); i = next) {
next = text.nextSpanTransition(i, len, ParagraphStyle.class);
ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
String elements = " ";
boolean needDiv = false;
for(int j = 0; j < style.length; j++) {
if (style[j] instanceof AlignmentSpan) {
Layout.Alignment align =
((AlignmentSpan) style[j]).getAlignment();
needDiv = true;
if (align == Layout.Alignment.ALIGN_CENTER) {
elements = "align=\"center\" " + elements;
} else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
elements = "align=\"right\" " + elements;
} else {
elements = "align=\"left\" " + elements;
}
}
}
if (needDiv) {
out.append("<div " + elements + ">");
}
withinDiv(out, text, i, next);
if (needDiv) {
out.append("</div>");
}
}
}
private static void withinDiv(StringBuilder out, Spanned text,
int start, int end) {
int next;
for (int i = start; i < end; i = next) {
next = text.nextSpanTransition(i, end, QuoteSpan.class);
QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
for (QuoteSpan quote: quotes) {
out.append("<blockquote>");
}
withinBlockquote(out, text, i, next);
for (QuoteSpan quote: quotes) {
out.append("</blockquote>\n");
}
}
}
private static void withinBlockquote(StringBuilder out, Spanned text,
int start, int end) {
out.append("<p>");
int next;
for (int i = start; i < end; i = next) {
next = TextUtils.indexOf(text, '\n', i, end);
if (next < 0) {
next = end;
}
int nl = 0;
while (next < end && text.charAt(next) == '\n') {
nl++;
next++;
}
withinParagraph(out, text, i, next - nl, nl, next == end);
}
out.append("</p>\n");
}
private static void withinParagraph(StringBuilder out, Spanned text,
int start, int end, int nl,
boolean last) {
int next;
for (int i = start; i < end; i = next) {
next = text.nextSpanTransition(i, end, CharacterStyle.class);
CharacterStyle[] style = text.getSpans(i, next,
CharacterStyle.class);
for (int j = 0; j < style.length; j++) {
if (style[j] instanceof StyleSpan) {
int s = ((StyleSpan) style[j]).getStyle();
if ((s & Typeface.BOLD) != 0) {
out.append("<b>");
}
if ((s & Typeface.ITALIC) != 0) {
out.append("<i>");
}
}
if (style[j] instanceof TypefaceSpan) {
String s = ((TypefaceSpan) style[j]).getFamily();
if (s.equals("monospace")) {
out.append("<tt>");
}
}
if (style[j] instanceof SuperscriptSpan) {
out.append("<sup>");
}
if (style[j] instanceof SubscriptSpan) {
out.append("<sub>");
}
if (style[j] instanceof UnderlineSpan) {
out.append("<u>");
}
if (style[j] instanceof StrikethroughSpan) {
out.append("<strike>");
}
if (style[j] instanceof URLSpan) {
out.append("<a href=\"");
out.append(((URLSpan) style[j]).getURL());
out.append("\">");
}
if (style[j] instanceof ImageSpan) {
out.append("<img src=\"");
out.append(((ImageSpan) style[j]).getSource());
out.append("\">");
// Don't output the dummy character underlying the image.
i = next;
}
if (style[j] instanceof AbsoluteSizeSpan) {
out.append("<font size =\"");
out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
out.append("\">");
}
if (style[j] instanceof ForegroundColorSpan) {
out.append("<font color =\"#");
String color = Integer.toHexString(((ForegroundColorSpan)
style[j]).getForegroundColor() + 0x01000000);
while (color.length() < 6) {
color = "0" + color;
}
out.append(color);
out.append("\">");
}
if (style[j] instanceof BackgroundColorSpan) {
out.append("<font bgcolor =\"#");
String color = Integer.toHexString(((BackgroundColorSpan)
style[j]).getBackgroundColor() + 0x01000000);
while (color.length() < 6) {
color = "0" + color;
}
out.append(color);
out.append("\">");
}
}
withinStyle(out, text, i, next);
for (int j = style.length - 1; j >= 0; j--) {
if (style[j] instanceof ForegroundColorSpan) {
out.append("</font>");
}
if (style[j] instanceof BackgroundColorSpan) {
out.append("</font>");
}
if (style[j] instanceof AbsoluteSizeSpan) {
out.append("</font>");
}
if (style[j] instanceof URLSpan) {
out.append("</a>");
}
if (style[j] instanceof StrikethroughSpan) {
out.append("</strike>");
}
if (style[j] instanceof UnderlineSpan) {
out.append("</u>");
}
if (style[j] instanceof SubscriptSpan) {
out.append("</sub>");
}
if (style[j] instanceof SuperscriptSpan) {
out.append("</sup>");
}
if (style[j] instanceof TypefaceSpan) {
String s = ((TypefaceSpan) style[j]).getFamily();
if (s.equals("monospace")) {
out.append("</tt>");
}
}
if (style[j] instanceof StyleSpan) {
int s = ((StyleSpan) style[j]).getStyle();
if ((s & Typeface.BOLD) != 0) {
out.append("</b>");
}
if ((s & Typeface.ITALIC) != 0) {
out.append("</i>");
}
}
}
}
String p = last ? "" : "</p>\n<p>";
if (nl == 1) {
out.append("<br>\n");
} else if (nl == 2) {
out.append(p);
} else {
for (int i = 2; i < nl; i++) {
out.append("<br>");
}
out.append(p);
}
}
private static void withinStyle(StringBuilder out, Spanned text,
int start, int end) {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
if (c == '<') {
out.append("&lt;");
} else if (c == '>') {
out.append("&gt;");
} else if (c == '&') {
out.append("&amp;");
} else if (c > 0x7E || c < ' ') {
out.append("&#" + ((int) c) + ";");
} else if (c == ' ') {
while (i + 1 < end && text.charAt(i + 1) == ' ') {
out.append("&nbsp;");
i++;
}
out.append(' ');
} else {
out.append(c);
}
}
}
}
class HtmlToSpannedConverter implements ContentHandler {
private static final float[] HEADER_SIZES = {
1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
};
private final String mSource;
private final XMLReader mReader;
private final SpannableStringBuilder mSpannableStringBuilder;
private final Html2.ImageGetter mImageGetter;
private final Html2.TagHandler mTagHandler;
public HtmlToSpannedConverter(
String source, Html2.ImageGetter imageGetter, Html2.TagHandler tagHandler,
Parser parser) {
mSource = source;
mSpannableStringBuilder = new SpannableStringBuilder();
mImageGetter = imageGetter;
mTagHandler = tagHandler;
mReader = parser;
}
public Spanned convert() {
mReader.setContentHandler(this);
try {
mReader.parse(new InputSource(new StringReader(mSource)));
} catch (IOException e) {
// We are reading from a string. There should not be IO problems.
throw new RuntimeException(e);
} catch (SAXException e) {
// TagSoup doesn't throw parse exceptions.
throw new RuntimeException(e);
}
// Fix flags and range for paragraph-type markup.
Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
for (int i = 0; i < obj.length; i++) {
int start = mSpannableStringBuilder.getSpanStart(obj[i]);
int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
// If the last line of the range is blank, back off by one.
if (end - 2 >= 0) {
if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
mSpannableStringBuilder.charAt(end - 2) == '\n') {
end--;
}
}
if (end == start) {
mSpannableStringBuilder.removeSpan(obj[i]);
} else {
mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
}
}
return mSpannableStringBuilder;
}
private void handleStartTag(String tag, Attributes attributes) {
Log.d("colors", "handleStartTag: "+tag);
if (tag.equalsIgnoreCase("br")) {
// We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
// so we can safely emite the linebreaks when we handle the close tag.
} else if (tag.equalsIgnoreCase("p")) {
handleP(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("div")) {
handleP(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("em")) {
start(mSpannableStringBuilder, new Bold());
} else if (tag.equalsIgnoreCase("b")) {
start(mSpannableStringBuilder, new Bold());
} else if (tag.equalsIgnoreCase("strong")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("cite")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("dfn")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("i")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("big")) {
start(mSpannableStringBuilder, new Big());
} else if (tag.equalsIgnoreCase("small")) {
start(mSpannableStringBuilder, new Small());
} else if (tag.equalsIgnoreCase("font")) {
startFont(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("blockquote")) {
handleP(mSpannableStringBuilder);
start(mSpannableStringBuilder, new Blockquote());
} else if (tag.equalsIgnoreCase("tt")) {
start(mSpannableStringBuilder, new Monospace());
} else if (tag.equalsIgnoreCase("a")) {
startA(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("u")) {
start(mSpannableStringBuilder, new Underline());
} else if (tag.equalsIgnoreCase("sup")) {
start(mSpannableStringBuilder, new Super());
} else if (tag.equalsIgnoreCase("sub")) {
start(mSpannableStringBuilder, new Sub());
} else if (tag.length() == 2 &&
Character.toLowerCase(tag.charAt(0)) == 'h' &&
tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
handleP(mSpannableStringBuilder);
start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
} else if (tag.equalsIgnoreCase("img")) {
startImg(mSpannableStringBuilder, attributes, mImageGetter);
} else if (mTagHandler != null) {
mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
}
}
private void handleEndTag(String tag) {
Log.d("colors", "handleEndTag: "+tag);
if (tag.equalsIgnoreCase("br")) {
handleBr(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("p")) {
handleP(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("div")) {
handleP(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("em")) {
end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
} else if (tag.equalsIgnoreCase("b")) {
end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
} else if (tag.equalsIgnoreCase("strong")) {
end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("cite")) {
end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("dfn")) {
end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("i")) {
end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("big")) {
end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
} else if (tag.equalsIgnoreCase("small")) {
end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
} else if (tag.equalsIgnoreCase("font")) {
endFont(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("blockquote")) {
handleP(mSpannableStringBuilder);
end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
} else if (tag.equalsIgnoreCase("tt")) {
end(mSpannableStringBuilder, Monospace.class,
new TypefaceSpan("monospace"));
} else if (tag.equalsIgnoreCase("a")) {
endA(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("u")) {
end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
} else if (tag.equalsIgnoreCase("sup")) {
end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
} else if (tag.equalsIgnoreCase("sub")) {
end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
} else if (tag.length() == 2 &&
Character.toLowerCase(tag.charAt(0)) == 'h' &&
tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
handleP(mSpannableStringBuilder);
endHeader(mSpannableStringBuilder);
} else if (mTagHandler != null) {
mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
}
}
private static void handleP(SpannableStringBuilder text) {
int len = text.length();
if (len >= 1 && text.charAt(len - 1) == '\n') {
if (len >= 2 && text.charAt(len - 2) == '\n') {
return;
}
text.append("\n");
return;
}
if (len != 0) {
text.append("\n\n");
}
}
private static void handleBr(SpannableStringBuilder text) {
text.append("\n");
}
private static Object getLast(Spanned text, Class kind) {
/*
* This knows that the last returned object from getSpans()
* will be the most recently added.
*/
Object[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) {
return null;
} else {
return objs[objs.length - 1];
}
}
private static void start(SpannableStringBuilder text, Object mark) {
int len = text.length();
text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
}
private static void end(SpannableStringBuilder text, Class kind,
Object repl) {
int len = text.length();
Object obj = getLast(text, kind);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len) {
text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return;
}
private static void startImg(SpannableStringBuilder text,
Attributes attributes, Html2.ImageGetter img) {
String src = attributes.getValue("", "src");
Drawable d = null;
if (img != null) {
d = img.getDrawable(src);
}
if (d == null) {
d = Resources.getSystem().
getDrawable(R.drawable.unknown_image);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
}
int len = text.length();
text.append("\uFFFC");
text.setSpan(new ImageSpan(d, src), len, text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private static void startFont(SpannableStringBuilder text,
Attributes attributes) {
String color = attributes.getValue("", "color");
String face = attributes.getValue("", "face");
String bgColor = attributes.getValue("", "bgcolor");
int len = text.length();
text.setSpan(new Font(color, face, bgColor), len, len, Spannable.SPAN_MARK_MARK);
}
private static void endFont(SpannableStringBuilder text) {
int len = text.length();
Object obj = getLast(text, Font.class);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len) {
Font f = (Font) obj;
if (!TextUtils.isEmpty(f.mColor)) {
if (f.mColor.startsWith("@")) {
Resources res = Resources.getSystem();
String name = f.mColor.substring(1);
int colorRes = res.getIdentifier(name, "color", "android");
if (colorRes != 0) {
ColorStateList colors = res.getColorStateList(colorRes);
text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
int c = getHtmlColor(f.mColor);
if (c != -1) {
text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
if (!TextUtils.isEmpty(f.mBgColor)) {
if (f.mBgColor.startsWith("@")) {
Resources res = Resources.getSystem();
String name = f.mBgColor.substring(1);
int colorRes = res.getIdentifier(name, "color", "android");
if (colorRes != 0) {
ColorStateList colors = res.getColorStateList(colorRes);
text.setSpan(new BackgroundColorSpan(colors.getDefaultColor()),
where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
int c = getHtmlColor(f.mBgColor);
if (c != -1) {
text.setSpan(new BackgroundColorSpan(c | 0xFF000000),
where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
if (f.mFace != null) {
text.setSpan(new TypefaceSpan(f.mFace), where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
private static void startA(SpannableStringBuilder text, Attributes attributes) {
String href = attributes.getValue("", "href");
int len = text.length();
text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
}
private static void endA(SpannableStringBuilder text) {
int len = text.length();
Object obj = getLast(text, Href.class);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len) {
Href h = (Href) obj;
if (h.mHref != null) {
text.setSpan(new URLSpan(h.mHref), where, len,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
private static void endHeader(SpannableStringBuilder text) {
int len = text.length();
Object obj = getLast(text, Header.class);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
// Back off not to change only the text, not the blank line.
while (len > where && text.charAt(len - 1) == '\n') {
len--;
}
if (where != len) {
Header h = (Header) obj;
text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new StyleSpan(Typeface.BOLD),
where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
@Override
public void setDocumentLocator(Locator locator) {
}
@Override
public void startDocument() throws SAXException {
}
@Override
public void endDocument() throws SAXException {
}
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
}
@Override
public void endPrefixMapping(String prefix) throws SAXException {
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
handleStartTag(localName, attributes);
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
handleEndTag(localName);
}
@Override
public void characters(char ch[], int start, int length) throws SAXException {
StringBuilder sb = new StringBuilder();
/*
* Ignore whitespace that immediately follows other whitespace;
* newlines count as spaces.
*/
for (int i = 0; i < length; i++) {
char c = ch[i + start];
if (c == ' ' || c == '\n') {
char pred;
int len = sb.length();
if (len == 0) {
len = mSpannableStringBuilder.length();
if (len == 0) {
pred = '\n';
} else {
pred = mSpannableStringBuilder.charAt(len - 1);
}
} else {
pred = sb.charAt(len - 1);
}
if (pred != ' ' && pred != '\n') {
sb.append(' ');
}
} else {
sb.append(c);
}
}
mSpannableStringBuilder.append(sb);
}
@Override
public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
}
@Override
public void processingInstruction(String target, String data) throws SAXException {
}
@Override
public void skippedEntity(String name) throws SAXException {
}
private static class Bold { }
private static class Italic { }
private static class Underline { }
private static class Big { }
private static class Small { }
private static class Monospace { }
private static class Blockquote { }
private static class Super { }
private static class Sub { }
private static class Font {
public String mColor;
public String mBgColor;
public String mFace;
public Font(String color, String face, String bgColor) {
mColor = color;
mFace = face;
mBgColor = bgColor;
}
}
private static class Href {
public String mHref;
public Href(String href) {
mHref = href;
}
}
private static class Header {
private final int mLevel;
public Header(int level) {
mLevel = level;
}
}
private static HashMap<String,Integer> COLORS = buildColorMap();
private static HashMap<String,Integer> buildColorMap() {
HashMap<String,Integer> map = new HashMap<String,Integer>();
map.put("aqua", 0x00FFFF);
map.put("black", 0x000000);
map.put("blue", 0x0000FF);
map.put("fuchsia", 0xFF00FF);
map.put("green", 0x008000);
map.put("grey", 0x808080);
map.put("lime", 0x00FF00);
map.put("maroon", 0x800000);
map.put("navy", 0x000080);
map.put("olive", 0x808000);
map.put("purple", 0x800080);
map.put("red", 0xFF0000);
map.put("silver", 0xC0C0C0);
map.put("teal", 0x008080);
map.put("white", 0xFFFFFF);
map.put("yellow", 0xFFFF00);
return map;
}
/**
* Converts an HTML color (named or numeric) to an integer RGB value.
*
* @param color Non-null color string.
* @return A color value, or {@code -1} if the color string could not be interpreted.
*/
private static int getHtmlColor(String color) {
Integer i = COLORS.get(color.toLowerCase());
if (i != null) {
return i;
} else {
try {
return convertValueToInt(color, -1);
} catch (NumberFormatException nfe) {
return -1;
}
}
}
/*
* Method ripped from com.android.internal.util.XmlUtils
*/
public static final int
convertValueToInt(CharSequence charSeq, int defaultValue)
{
if (null == charSeq) {
return defaultValue;
}
String nm = charSeq.toString();
// XXX This code is copied from Integer.decode() so we don't
// have to instantiate an Integer!
int value;
int sign = 1;
int index = 0;
int len = nm.length();
int base = 10;
if ('-' == nm.charAt(0)) {
sign = -1;
index++;
}
if ('0' == nm.charAt(index)) {
// Quick check for a zero by itself
if (index == (len - 1)) {
return 0;
}
char c = nm.charAt(index + 1);
if ('x' == c || 'X' == c) {
index += 2;
base = 16;
} else {
index++;
base = 8;
}
}
else if ('#' == nm.charAt(index))
{
index++;
base = 16;
}
return Integer.parseInt(nm.substring(index), base) * sign;
}
}