| /* |
| * 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 android.text; |
| |
| import com.android.internal.util.ArrayUtils; |
| import org.ccil.cowan.tagsoup.HTMLSchema; |
| import org.ccil.cowan.tagsoup.Parser; |
| 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 android.content.res.ColorStateList; |
| import android.content.res.Resources; |
| import android.graphics.Typeface; |
| import android.graphics.drawable.Drawable; |
| import android.text.style.AbsoluteSizeSpan; |
| import android.text.style.AlignmentSpan; |
| 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 com.android.internal.util.XmlUtils; |
| |
| import java.io.IOException; |
| import java.io.StringReader; |
| import java.util.HashMap; |
| |
| /** |
| * This class processes HTML strings into displayable styled text. |
| * Not all HTML tags are supported. |
| */ |
| public class Html { |
| /** |
| * Retrieves images for HTML <img> tags. |
| */ |
| public static interface ImageGetter { |
| /** |
| * This methos is called when the HTML parser encounters an |
| * <img> 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 whenn 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 Html() { } |
| |
| /** |
| * Returns displayable styled text from the provided HTML string. |
| * Any <img> 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 static final HTMLSchema schema = new HTMLSchema(); |
| } |
| |
| /** |
| * Returns displayable styled text from the provided HTML string. |
| * Any <img> 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(); |
| try { |
| parser.setProperty(Parser.schemaProperty, HtmlParser.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 String getOpenParaTagWithDirection(Spanned text, int start, int end) { |
| final int len = end - start; |
| final byte[] levels = new byte[ArrayUtils.idealByteArraySize(len)]; |
| final char[] buffer = TextUtils.obtain(len); |
| TextUtils.getChars(text, start, end, buffer, 0); |
| |
| int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len, |
| false /* no info */); |
| switch(paraDir) { |
| case Layout.DIR_RIGHT_TO_LEFT: |
| return "<p dir=rtl>"; |
| case Layout.DIR_LEFT_TO_RIGHT: |
| default: |
| return "<p dir=ltr>"; |
| } |
| } |
| |
| private static void withinBlockquote(StringBuilder out, Spanned text, |
| int start, int end) { |
| out.append(getOpenParaTagWithDirection(text, start, end)); |
| |
| 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("\">"); |
| } |
| } |
| |
| 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 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" + getOpenParaTagWithDirection(text, start, end); |
| |
| 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("<"); |
| } else if (c == '>') { |
| out.append(">"); |
| } else if (c == '&') { |
| out.append("&"); |
| } else if (c > 0x7E || c < ' ') { |
| out.append("&#" + ((int) c) + ";"); |
| } else if (c == ' ') { |
| while (i + 1 < end && text.charAt(i + 1) == ' ') { |
| out.append(" "); |
| 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 String mSource; |
| private XMLReader mReader; |
| private SpannableStringBuilder mSpannableStringBuilder; |
| private Html.ImageGetter mImageGetter; |
| private Html.TagHandler mTagHandler; |
| |
| public HtmlToSpannedConverter( |
| String source, Html.ImageGetter imageGetter, Html.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) { |
| 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("strong")) { |
| start(mSpannableStringBuilder, new Bold()); |
| } else if (tag.equalsIgnoreCase("b")) { |
| start(mSpannableStringBuilder, new Bold()); |
| } else if (tag.equalsIgnoreCase("em")) { |
| 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) { |
| if (tag.equalsIgnoreCase("br")) { |
| handleBr(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("p")) { |
| handleP(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("div")) { |
| handleP(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("strong")) { |
| 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("em")) { |
| 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, Html.ImageGetter img) { |
| String src = attributes.getValue("", "src"); |
| Drawable d = null; |
| |
| if (img != null) { |
| d = img.getDrawable(src); |
| } |
| |
| if (d == null) { |
| d = Resources.getSystem(). |
| getDrawable(com.android.internal.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"); |
| |
| int len = text.length(); |
| text.setSpan(new Font(color, face), 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 (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); |
| } |
| } |
| |
| public void setDocumentLocator(Locator locator) { |
| } |
| |
| public void startDocument() throws SAXException { |
| } |
| |
| public void endDocument() throws SAXException { |
| } |
| |
| public void startPrefixMapping(String prefix, String uri) throws SAXException { |
| } |
| |
| public void endPrefixMapping(String prefix) throws SAXException { |
| } |
| |
| public void startElement(String uri, String localName, String qName, Attributes attributes) |
| throws SAXException { |
| handleStartTag(localName, attributes); |
| } |
| |
| public void endElement(String uri, String localName, String qName) throws SAXException { |
| handleEndTag(localName); |
| } |
| |
| 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); |
| } |
| |
| public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { |
| } |
| |
| public void processingInstruction(String target, String data) throws SAXException { |
| } |
| |
| 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 mFace; |
| |
| public Font(String color, String face) { |
| mColor = color; |
| mFace = face; |
| } |
| } |
| |
| private static class Href { |
| public String mHref; |
| |
| public Href(String href) { |
| mHref = href; |
| } |
| } |
| |
| private static class Header { |
| private 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 XmlUtils.convertValueToInt(color, -1); |
| } catch (NumberFormatException nfe) { |
| return -1; |
| } |
| } |
| } |
| |
| } |