Home | History | Annotate | Download | only in text
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.text;
     18 
     19 import org.ccil.cowan.tagsoup.HTMLSchema;
     20 import org.ccil.cowan.tagsoup.Parser;
     21 import org.xml.sax.Attributes;
     22 import org.xml.sax.ContentHandler;
     23 import org.xml.sax.InputSource;
     24 import org.xml.sax.Locator;
     25 import org.xml.sax.SAXException;
     26 import org.xml.sax.XMLReader;
     27 
     28 import android.content.res.ColorStateList;
     29 import android.content.res.Resources;
     30 import android.graphics.Typeface;
     31 import android.graphics.drawable.Drawable;
     32 import android.text.style.AbsoluteSizeSpan;
     33 import android.text.style.AlignmentSpan;
     34 import android.text.style.CharacterStyle;
     35 import android.text.style.ForegroundColorSpan;
     36 import android.text.style.ImageSpan;
     37 import android.text.style.ParagraphStyle;
     38 import android.text.style.QuoteSpan;
     39 import android.text.style.RelativeSizeSpan;
     40 import android.text.style.StrikethroughSpan;
     41 import android.text.style.StyleSpan;
     42 import android.text.style.SubscriptSpan;
     43 import android.text.style.SuperscriptSpan;
     44 import android.text.style.TextAppearanceSpan;
     45 import android.text.style.TypefaceSpan;
     46 import android.text.style.URLSpan;
     47 import android.text.style.UnderlineSpan;
     48 import android.util.Log;
     49 
     50 import com.android.internal.util.XmlUtils;
     51 
     52 import java.io.IOException;
     53 import java.io.StringReader;
     54 import java.nio.CharBuffer;
     55 import java.util.HashMap;
     56 
     57 /**
     58  * This class processes HTML strings into displayable styled text.
     59  * Not all HTML tags are supported.
     60  */
     61 public class Html {
     62     /**
     63      * Retrieves images for HTML <img> tags.
     64      */
     65     public static interface ImageGetter {
     66         /**
     67          * This methos is called when the HTML parser encounters an
     68          * &lt;img&gt; tag.  The <code>source</code> argument is the
     69          * string from the "src" attribute; the return value should be
     70          * a Drawable representation of the image or <code>null</code>
     71          * for a generic replacement image.  Make sure you call
     72          * setBounds() on your Drawable if it doesn't already have
     73          * its bounds set.
     74          */
     75         public Drawable getDrawable(String source);
     76     }
     77 
     78     /**
     79      * Is notified when HTML tags are encountered that the parser does
     80      * not know how to interpret.
     81      */
     82     public static interface TagHandler {
     83         /**
     84          * This method will be called whenn the HTML parser encounters
     85          * a tag that it does not know how to interpret.
     86          */
     87         public void handleTag(boolean opening, String tag,
     88                                  Editable output, XMLReader xmlReader);
     89     }
     90 
     91     private Html() { }
     92 
     93     /**
     94      * Returns displayable styled text from the provided HTML string.
     95      * Any &lt;img&gt; tags in the HTML will display as a generic
     96      * replacement image which your program can then go through and
     97      * replace with real images.
     98      *
     99      * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
    100      */
    101     public static Spanned fromHtml(String source) {
    102         return fromHtml(source, null, null);
    103     }
    104 
    105     /**
    106      * Lazy initialization holder for HTML parser. This class will
    107      * a) be preloaded by the zygote, or b) not loaded until absolutely
    108      * necessary.
    109      */
    110     private static class HtmlParser {
    111         private static final HTMLSchema schema = new HTMLSchema();
    112     }
    113 
    114     /**
    115      * Returns displayable styled text from the provided HTML string.
    116      * Any &lt;img&gt; tags in the HTML will use the specified ImageGetter
    117      * to request a representation of the image (use null if you don't
    118      * want this) and the specified TagHandler to handle unknown tags
    119      * (specify null if you don't want this).
    120      *
    121      * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
    122      */
    123     public static Spanned fromHtml(String source, ImageGetter imageGetter,
    124                                    TagHandler tagHandler) {
    125         Parser parser = new Parser();
    126         try {
    127             parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
    128         } catch (org.xml.sax.SAXNotRecognizedException e) {
    129             // Should not happen.
    130             throw new RuntimeException(e);
    131         } catch (org.xml.sax.SAXNotSupportedException e) {
    132             // Should not happen.
    133             throw new RuntimeException(e);
    134         }
    135 
    136         HtmlToSpannedConverter converter =
    137                 new HtmlToSpannedConverter(source, imageGetter, tagHandler,
    138                         parser);
    139         return converter.convert();
    140     }
    141 
    142     /**
    143      * Returns an HTML representation of the provided Spanned text.
    144      */
    145     public static String toHtml(Spanned text) {
    146         StringBuilder out = new StringBuilder();
    147         withinHtml(out, text);
    148         return out.toString();
    149     }
    150 
    151     private static void withinHtml(StringBuilder out, Spanned text) {
    152         int len = text.length();
    153 
    154         int next;
    155         for (int i = 0; i < text.length(); i = next) {
    156             next = text.nextSpanTransition(i, len, ParagraphStyle.class);
    157             ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
    158             String elements = " ";
    159             boolean needDiv = false;
    160 
    161             for(int j = 0; j < style.length; j++) {
    162                 if (style[j] instanceof AlignmentSpan) {
    163                     Layout.Alignment align =
    164                         ((AlignmentSpan) style[j]).getAlignment();
    165                     needDiv = true;
    166                     if (align == Layout.Alignment.ALIGN_CENTER) {
    167                         elements = "align=\"center\" " + elements;
    168                     } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
    169                         elements = "align=\"right\" " + elements;
    170                     } else {
    171                         elements = "align=\"left\" " + elements;
    172                     }
    173                 }
    174             }
    175             if (needDiv) {
    176                 out.append("<div " + elements + ">");
    177             }
    178 
    179             withinDiv(out, text, i, next);
    180 
    181             if (needDiv) {
    182                 out.append("</div>");
    183             }
    184         }
    185     }
    186 
    187     private static void withinDiv(StringBuilder out, Spanned text,
    188             int start, int end) {
    189         int next;
    190         for (int i = start; i < end; i = next) {
    191             next = text.nextSpanTransition(i, end, QuoteSpan.class);
    192             QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
    193 
    194             for (QuoteSpan quote: quotes) {
    195                 out.append("<blockquote>");
    196             }
    197 
    198             withinBlockquote(out, text, i, next);
    199 
    200             for (QuoteSpan quote: quotes) {
    201                 out.append("</blockquote>\n");
    202             }
    203         }
    204     }
    205 
    206     private static void withinBlockquote(StringBuilder out, Spanned text,
    207                                          int start, int end) {
    208         out.append("<p>");
    209 
    210         int next;
    211         for (int i = start; i < end; i = next) {
    212             next = TextUtils.indexOf(text, '\n', i, end);
    213             if (next < 0) {
    214                 next = end;
    215             }
    216 
    217             int nl = 0;
    218 
    219             while (next < end && text.charAt(next) == '\n') {
    220                 nl++;
    221                 next++;
    222             }
    223 
    224             withinParagraph(out, text, i, next - nl, nl, next == end);
    225         }
    226 
    227         out.append("</p>\n");
    228     }
    229 
    230     private static void withinParagraph(StringBuilder out, Spanned text,
    231                                         int start, int end, int nl,
    232                                         boolean last) {
    233         int next;
    234         for (int i = start; i < end; i = next) {
    235             next = text.nextSpanTransition(i, end, CharacterStyle.class);
    236             CharacterStyle[] style = text.getSpans(i, next,
    237                                                    CharacterStyle.class);
    238 
    239             for (int j = 0; j < style.length; j++) {
    240                 if (style[j] instanceof StyleSpan) {
    241                     int s = ((StyleSpan) style[j]).getStyle();
    242 
    243                     if ((s & Typeface.BOLD) != 0) {
    244                         out.append("<b>");
    245                     }
    246                     if ((s & Typeface.ITALIC) != 0) {
    247                         out.append("<i>");
    248                     }
    249                 }
    250                 if (style[j] instanceof TypefaceSpan) {
    251                     String s = ((TypefaceSpan) style[j]).getFamily();
    252 
    253                     if (s.equals("monospace")) {
    254                         out.append("<tt>");
    255                     }
    256                 }
    257                 if (style[j] instanceof SuperscriptSpan) {
    258                     out.append("<sup>");
    259                 }
    260                 if (style[j] instanceof SubscriptSpan) {
    261                     out.append("<sub>");
    262                 }
    263                 if (style[j] instanceof UnderlineSpan) {
    264                     out.append("<u>");
    265                 }
    266                 if (style[j] instanceof StrikethroughSpan) {
    267                     out.append("<strike>");
    268                 }
    269                 if (style[j] instanceof URLSpan) {
    270                     out.append("<a href=\"");
    271                     out.append(((URLSpan) style[j]).getURL());
    272                     out.append("\">");
    273                 }
    274                 if (style[j] instanceof ImageSpan) {
    275                     out.append("<img src=\"");
    276                     out.append(((ImageSpan) style[j]).getSource());
    277                     out.append("\">");
    278 
    279                     // Don't output the dummy character underlying the image.
    280                     i = next;
    281                 }
    282                 if (style[j] instanceof AbsoluteSizeSpan) {
    283                     out.append("<font size =\"");
    284                     out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
    285                     out.append("\">");
    286                 }
    287                 if (style[j] instanceof ForegroundColorSpan) {
    288                     out.append("<font color =\"#");
    289                     String color = Integer.toHexString(((ForegroundColorSpan)
    290                             style[j]).getForegroundColor() + 0x01000000);
    291                     while (color.length() < 6) {
    292                         color = "0" + color;
    293                     }
    294                     out.append(color);
    295                     out.append("\">");
    296                 }
    297             }
    298 
    299             withinStyle(out, text, i, next);
    300 
    301             for (int j = style.length - 1; j >= 0; j--) {
    302                 if (style[j] instanceof ForegroundColorSpan) {
    303                     out.append("</font>");
    304                 }
    305                 if (style[j] instanceof AbsoluteSizeSpan) {
    306                     out.append("</font>");
    307                 }
    308                 if (style[j] instanceof URLSpan) {
    309                     out.append("</a>");
    310                 }
    311                 if (style[j] instanceof StrikethroughSpan) {
    312                     out.append("</strike>");
    313                 }
    314                 if (style[j] instanceof UnderlineSpan) {
    315                     out.append("</u>");
    316                 }
    317                 if (style[j] instanceof SubscriptSpan) {
    318                     out.append("</sub>");
    319                 }
    320                 if (style[j] instanceof SuperscriptSpan) {
    321                     out.append("</sup>");
    322                 }
    323                 if (style[j] instanceof TypefaceSpan) {
    324                     String s = ((TypefaceSpan) style[j]).getFamily();
    325 
    326                     if (s.equals("monospace")) {
    327                         out.append("</tt>");
    328                     }
    329                 }
    330                 if (style[j] instanceof StyleSpan) {
    331                     int s = ((StyleSpan) style[j]).getStyle();
    332 
    333                     if ((s & Typeface.BOLD) != 0) {
    334                         out.append("</b>");
    335                     }
    336                     if ((s & Typeface.ITALIC) != 0) {
    337                         out.append("</i>");
    338                     }
    339                 }
    340             }
    341         }
    342 
    343         String p = last ? "" : "</p>\n<p>";
    344 
    345         if (nl == 1) {
    346             out.append("<br>\n");
    347         } else if (nl == 2) {
    348             out.append(p);
    349         } else {
    350             for (int i = 2; i < nl; i++) {
    351                 out.append("<br>");
    352             }
    353 
    354             out.append(p);
    355         }
    356     }
    357 
    358     private static void withinStyle(StringBuilder out, Spanned text,
    359                                     int start, int end) {
    360         for (int i = start; i < end; i++) {
    361             char c = text.charAt(i);
    362 
    363             if (c == '<') {
    364                 out.append("&lt;");
    365             } else if (c == '>') {
    366                 out.append("&gt;");
    367             } else if (c == '&') {
    368                 out.append("&amp;");
    369             } else if (c > 0x7E || c < ' ') {
    370                 out.append("&#" + ((int) c) + ";");
    371             } else if (c == ' ') {
    372                 while (i + 1 < end && text.charAt(i + 1) == ' ') {
    373                     out.append("&nbsp;");
    374                     i++;
    375                 }
    376 
    377                 out.append(' ');
    378             } else {
    379                 out.append(c);
    380             }
    381         }
    382     }
    383 }
    384 
    385 class HtmlToSpannedConverter implements ContentHandler {
    386 
    387     private static final float[] HEADER_SIZES = {
    388         1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
    389     };
    390 
    391     private String mSource;
    392     private XMLReader mReader;
    393     private SpannableStringBuilder mSpannableStringBuilder;
    394     private Html.ImageGetter mImageGetter;
    395     private Html.TagHandler mTagHandler;
    396 
    397     public HtmlToSpannedConverter(
    398             String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
    399             Parser parser) {
    400         mSource = source;
    401         mSpannableStringBuilder = new SpannableStringBuilder();
    402         mImageGetter = imageGetter;
    403         mTagHandler = tagHandler;
    404         mReader = parser;
    405     }
    406 
    407     public Spanned convert() {
    408 
    409         mReader.setContentHandler(this);
    410         try {
    411             mReader.parse(new InputSource(new StringReader(mSource)));
    412         } catch (IOException e) {
    413             // We are reading from a string. There should not be IO problems.
    414             throw new RuntimeException(e);
    415         } catch (SAXException e) {
    416             // TagSoup doesn't throw parse exceptions.
    417             throw new RuntimeException(e);
    418         }
    419 
    420         // Fix flags and range for paragraph-type markup.
    421         Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
    422         for (int i = 0; i < obj.length; i++) {
    423             int start = mSpannableStringBuilder.getSpanStart(obj[i]);
    424             int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
    425 
    426             // If the last line of the range is blank, back off by one.
    427             if (end - 2 >= 0) {
    428                 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
    429                     mSpannableStringBuilder.charAt(end - 2) == '\n') {
    430                     end--;
    431                 }
    432             }
    433 
    434             if (end == start) {
    435                 mSpannableStringBuilder.removeSpan(obj[i]);
    436             } else {
    437                 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
    438             }
    439         }
    440 
    441         return mSpannableStringBuilder;
    442     }
    443 
    444     private void handleStartTag(String tag, Attributes attributes) {
    445         if (tag.equalsIgnoreCase("br")) {
    446             // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
    447             // so we can safely emite the linebreaks when we handle the close tag.
    448         } else if (tag.equalsIgnoreCase("p")) {
    449             handleP(mSpannableStringBuilder);
    450         } else if (tag.equalsIgnoreCase("div")) {
    451             handleP(mSpannableStringBuilder);
    452         } else if (tag.equalsIgnoreCase("strong")) {
    453             start(mSpannableStringBuilder, new Bold());
    454         } else if (tag.equalsIgnoreCase("b")) {
    455             start(mSpannableStringBuilder, new Bold());
    456         } else if (tag.equalsIgnoreCase("em")) {
    457             start(mSpannableStringBuilder, new Italic());
    458         } else if (tag.equalsIgnoreCase("cite")) {
    459             start(mSpannableStringBuilder, new Italic());
    460         } else if (tag.equalsIgnoreCase("dfn")) {
    461             start(mSpannableStringBuilder, new Italic());
    462         } else if (tag.equalsIgnoreCase("i")) {
    463             start(mSpannableStringBuilder, new Italic());
    464         } else if (tag.equalsIgnoreCase("big")) {
    465             start(mSpannableStringBuilder, new Big());
    466         } else if (tag.equalsIgnoreCase("small")) {
    467             start(mSpannableStringBuilder, new Small());
    468         } else if (tag.equalsIgnoreCase("font")) {
    469             startFont(mSpannableStringBuilder, attributes);
    470         } else if (tag.equalsIgnoreCase("blockquote")) {
    471             handleP(mSpannableStringBuilder);
    472             start(mSpannableStringBuilder, new Blockquote());
    473         } else if (tag.equalsIgnoreCase("tt")) {
    474             start(mSpannableStringBuilder, new Monospace());
    475         } else if (tag.equalsIgnoreCase("a")) {
    476             startA(mSpannableStringBuilder, attributes);
    477         } else if (tag.equalsIgnoreCase("u")) {
    478             start(mSpannableStringBuilder, new Underline());
    479         } else if (tag.equalsIgnoreCase("sup")) {
    480             start(mSpannableStringBuilder, new Super());
    481         } else if (tag.equalsIgnoreCase("sub")) {
    482             start(mSpannableStringBuilder, new Sub());
    483         } else if (tag.length() == 2 &&
    484                    Character.toLowerCase(tag.charAt(0)) == 'h' &&
    485                    tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
    486             handleP(mSpannableStringBuilder);
    487             start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
    488         } else if (tag.equalsIgnoreCase("img")) {
    489             startImg(mSpannableStringBuilder, attributes, mImageGetter);
    490         } else if (mTagHandler != null) {
    491             mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
    492         }
    493     }
    494 
    495     private void handleEndTag(String tag) {
    496         if (tag.equalsIgnoreCase("br")) {
    497             handleBr(mSpannableStringBuilder);
    498         } else if (tag.equalsIgnoreCase("p")) {
    499             handleP(mSpannableStringBuilder);
    500         } else if (tag.equalsIgnoreCase("div")) {
    501             handleP(mSpannableStringBuilder);
    502         } else if (tag.equalsIgnoreCase("strong")) {
    503             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
    504         } else if (tag.equalsIgnoreCase("b")) {
    505             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
    506         } else if (tag.equalsIgnoreCase("em")) {
    507             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    508         } else if (tag.equalsIgnoreCase("cite")) {
    509             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    510         } else if (tag.equalsIgnoreCase("dfn")) {
    511             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    512         } else if (tag.equalsIgnoreCase("i")) {
    513             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    514         } else if (tag.equalsIgnoreCase("big")) {
    515             end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
    516         } else if (tag.equalsIgnoreCase("small")) {
    517             end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
    518         } else if (tag.equalsIgnoreCase("font")) {
    519             endFont(mSpannableStringBuilder);
    520         } else if (tag.equalsIgnoreCase("blockquote")) {
    521             handleP(mSpannableStringBuilder);
    522             end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
    523         } else if (tag.equalsIgnoreCase("tt")) {
    524             end(mSpannableStringBuilder, Monospace.class,
    525                     new TypefaceSpan("monospace"));
    526         } else if (tag.equalsIgnoreCase("a")) {
    527             endA(mSpannableStringBuilder);
    528         } else if (tag.equalsIgnoreCase("u")) {
    529             end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
    530         } else if (tag.equalsIgnoreCase("sup")) {
    531             end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
    532         } else if (tag.equalsIgnoreCase("sub")) {
    533             end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
    534         } else if (tag.length() == 2 &&
    535                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
    536                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
    537             handleP(mSpannableStringBuilder);
    538             endHeader(mSpannableStringBuilder);
    539         } else if (mTagHandler != null) {
    540             mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
    541         }
    542     }
    543 
    544     private static void handleP(SpannableStringBuilder text) {
    545         int len = text.length();
    546 
    547         if (len >= 1 && text.charAt(len - 1) == '\n') {
    548             if (len >= 2 && text.charAt(len - 2) == '\n') {
    549                 return;
    550             }
    551 
    552             text.append("\n");
    553             return;
    554         }
    555 
    556         if (len != 0) {
    557             text.append("\n\n");
    558         }
    559     }
    560 
    561     private static void handleBr(SpannableStringBuilder text) {
    562         text.append("\n");
    563     }
    564 
    565     private static Object getLast(Spanned text, Class kind) {
    566         /*
    567          * This knows that the last returned object from getSpans()
    568          * will be the most recently added.
    569          */
    570         Object[] objs = text.getSpans(0, text.length(), kind);
    571 
    572         if (objs.length == 0) {
    573             return null;
    574         } else {
    575             return objs[objs.length - 1];
    576         }
    577     }
    578 
    579     private static void start(SpannableStringBuilder text, Object mark) {
    580         int len = text.length();
    581         text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
    582     }
    583 
    584     private static void end(SpannableStringBuilder text, Class kind,
    585                             Object repl) {
    586         int len = text.length();
    587         Object obj = getLast(text, kind);
    588         int where = text.getSpanStart(obj);
    589 
    590         text.removeSpan(obj);
    591 
    592         if (where != len) {
    593             text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    594         }
    595 
    596         return;
    597     }
    598 
    599     private static void startImg(SpannableStringBuilder text,
    600                                  Attributes attributes, Html.ImageGetter img) {
    601         String src = attributes.getValue("", "src");
    602         Drawable d = null;
    603 
    604         if (img != null) {
    605             d = img.getDrawable(src);
    606         }
    607 
    608         if (d == null) {
    609             d = Resources.getSystem().
    610                     getDrawable(com.android.internal.R.drawable.unknown_image);
    611             d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
    612         }
    613 
    614         int len = text.length();
    615         text.append("\uFFFC");
    616 
    617         text.setSpan(new ImageSpan(d, src), len, text.length(),
    618                      Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    619     }
    620 
    621     private static void startFont(SpannableStringBuilder text,
    622                                   Attributes attributes) {
    623         String color = attributes.getValue("", "color");
    624         String face = attributes.getValue("", "face");
    625 
    626         int len = text.length();
    627         text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
    628     }
    629 
    630     private static void endFont(SpannableStringBuilder text) {
    631         int len = text.length();
    632         Object obj = getLast(text, Font.class);
    633         int where = text.getSpanStart(obj);
    634 
    635         text.removeSpan(obj);
    636 
    637         if (where != len) {
    638             Font f = (Font) obj;
    639 
    640             if (!TextUtils.isEmpty(f.mColor)) {
    641                 if (f.mColor.startsWith("@")) {
    642                     Resources res = Resources.getSystem();
    643                     String name = f.mColor.substring(1);
    644                     int colorRes = res.getIdentifier(name, "color", "android");
    645                     if (colorRes != 0) {
    646                         ColorStateList colors = res.getColorStateList(colorRes);
    647                         text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
    648                                 where, len,
    649                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    650                     }
    651                 } else {
    652                     int c = getHtmlColor(f.mColor);
    653                     if (c != -1) {
    654                         text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
    655                                 where, len,
    656                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    657                     }
    658                 }
    659             }
    660 
    661             if (f.mFace != null) {
    662                 text.setSpan(new TypefaceSpan(f.mFace), where, len,
    663                              Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    664             }
    665         }
    666     }
    667 
    668     private static void startA(SpannableStringBuilder text, Attributes attributes) {
    669         String href = attributes.getValue("", "href");
    670 
    671         int len = text.length();
    672         text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
    673     }
    674 
    675     private static void endA(SpannableStringBuilder text) {
    676         int len = text.length();
    677         Object obj = getLast(text, Href.class);
    678         int where = text.getSpanStart(obj);
    679 
    680         text.removeSpan(obj);
    681 
    682         if (where != len) {
    683             Href h = (Href) obj;
    684 
    685             if (h.mHref != null) {
    686                 text.setSpan(new URLSpan(h.mHref), where, len,
    687                              Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    688             }
    689         }
    690     }
    691 
    692     private static void endHeader(SpannableStringBuilder text) {
    693         int len = text.length();
    694         Object obj = getLast(text, Header.class);
    695 
    696         int where = text.getSpanStart(obj);
    697 
    698         text.removeSpan(obj);
    699 
    700         // Back off not to change only the text, not the blank line.
    701         while (len > where && text.charAt(len - 1) == '\n') {
    702             len--;
    703         }
    704 
    705         if (where != len) {
    706             Header h = (Header) obj;
    707 
    708             text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
    709                          where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    710             text.setSpan(new StyleSpan(Typeface.BOLD),
    711                          where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    712         }
    713     }
    714 
    715     public void setDocumentLocator(Locator locator) {
    716     }
    717 
    718     public void startDocument() throws SAXException {
    719     }
    720 
    721     public void endDocument() throws SAXException {
    722     }
    723 
    724     public void startPrefixMapping(String prefix, String uri) throws SAXException {
    725     }
    726 
    727     public void endPrefixMapping(String prefix) throws SAXException {
    728     }
    729 
    730     public void startElement(String uri, String localName, String qName, Attributes attributes)
    731             throws SAXException {
    732         handleStartTag(localName, attributes);
    733     }
    734 
    735     public void endElement(String uri, String localName, String qName) throws SAXException {
    736         handleEndTag(localName);
    737     }
    738 
    739     public void characters(char ch[], int start, int length) throws SAXException {
    740         StringBuilder sb = new StringBuilder();
    741 
    742         /*
    743          * Ignore whitespace that immediately follows other whitespace;
    744          * newlines count as spaces.
    745          */
    746 
    747         for (int i = 0; i < length; i++) {
    748             char c = ch[i + start];
    749 
    750             if (c == ' ' || c == '\n') {
    751                 char pred;
    752                 int len = sb.length();
    753 
    754                 if (len == 0) {
    755                     len = mSpannableStringBuilder.length();
    756 
    757                     if (len == 0) {
    758                         pred = '\n';
    759                     } else {
    760                         pred = mSpannableStringBuilder.charAt(len - 1);
    761                     }
    762                 } else {
    763                     pred = sb.charAt(len - 1);
    764                 }
    765 
    766                 if (pred != ' ' && pred != '\n') {
    767                     sb.append(' ');
    768                 }
    769             } else {
    770                 sb.append(c);
    771             }
    772         }
    773 
    774         mSpannableStringBuilder.append(sb);
    775     }
    776 
    777     public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
    778     }
    779 
    780     public void processingInstruction(String target, String data) throws SAXException {
    781     }
    782 
    783     public void skippedEntity(String name) throws SAXException {
    784     }
    785 
    786     private static class Bold { }
    787     private static class Italic { }
    788     private static class Underline { }
    789     private static class Big { }
    790     private static class Small { }
    791     private static class Monospace { }
    792     private static class Blockquote { }
    793     private static class Super { }
    794     private static class Sub { }
    795 
    796     private static class Font {
    797         public String mColor;
    798         public String mFace;
    799 
    800         public Font(String color, String face) {
    801             mColor = color;
    802             mFace = face;
    803         }
    804     }
    805 
    806     private static class Href {
    807         public String mHref;
    808 
    809         public Href(String href) {
    810             mHref = href;
    811         }
    812     }
    813 
    814     private static class Header {
    815         private int mLevel;
    816 
    817         public Header(int level) {
    818             mLevel = level;
    819         }
    820     }
    821 
    822     private static HashMap<String,Integer> COLORS = buildColorMap();
    823 
    824     private static HashMap<String,Integer> buildColorMap() {
    825         HashMap<String,Integer> map = new HashMap<String,Integer>();
    826         map.put("aqua", 0x00FFFF);
    827         map.put("black", 0x000000);
    828         map.put("blue", 0x0000FF);
    829         map.put("fuchsia", 0xFF00FF);
    830         map.put("green", 0x008000);
    831         map.put("grey", 0x808080);
    832         map.put("lime", 0x00FF00);
    833         map.put("maroon", 0x800000);
    834         map.put("navy", 0x000080);
    835         map.put("olive", 0x808000);
    836         map.put("purple", 0x800080);
    837         map.put("red", 0xFF0000);
    838         map.put("silver", 0xC0C0C0);
    839         map.put("teal", 0x008080);
    840         map.put("white", 0xFFFFFF);
    841         map.put("yellow", 0xFFFF00);
    842         return map;
    843     }
    844 
    845     /**
    846      * Converts an HTML color (named or numeric) to an integer RGB value.
    847      *
    848      * @param color Non-null color string.
    849      * @return A color value, or {@code -1} if the color string could not be interpreted.
    850      */
    851     private static int getHtmlColor(String color) {
    852         Integer i = COLORS.get(color.toLowerCase());
    853         if (i != null) {
    854             return i;
    855         } else {
    856             try {
    857                 return XmlUtils.convertValueToInt(color, -1);
    858             } catch (NumberFormatException nfe) {
    859                 return -1;
    860             }
    861         }
    862       }
    863 
    864 }
    865