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 android.graphics.Color;
     20 import com.android.internal.util.ArrayUtils;
     21 import org.ccil.cowan.tagsoup.HTMLSchema;
     22 import org.ccil.cowan.tagsoup.Parser;
     23 import org.xml.sax.Attributes;
     24 import org.xml.sax.ContentHandler;
     25 import org.xml.sax.InputSource;
     26 import org.xml.sax.Locator;
     27 import org.xml.sax.SAXException;
     28 import org.xml.sax.XMLReader;
     29 
     30 import android.content.res.ColorStateList;
     31 import android.content.res.Resources;
     32 import android.graphics.Typeface;
     33 import android.graphics.drawable.Drawable;
     34 import android.text.style.AbsoluteSizeSpan;
     35 import android.text.style.AlignmentSpan;
     36 import android.text.style.CharacterStyle;
     37 import android.text.style.ForegroundColorSpan;
     38 import android.text.style.ImageSpan;
     39 import android.text.style.ParagraphStyle;
     40 import android.text.style.QuoteSpan;
     41 import android.text.style.RelativeSizeSpan;
     42 import android.text.style.StrikethroughSpan;
     43 import android.text.style.StyleSpan;
     44 import android.text.style.SubscriptSpan;
     45 import android.text.style.SuperscriptSpan;
     46 import android.text.style.TextAppearanceSpan;
     47 import android.text.style.TypefaceSpan;
     48 import android.text.style.URLSpan;
     49 import android.text.style.UnderlineSpan;
     50 
     51 import com.android.internal.util.XmlUtils;
     52 
     53 import java.io.IOException;
     54 import java.io.StringReader;
     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     /**
    152      * Returns an HTML escaped representation of the given plain text.
    153      */
    154     public static String escapeHtml(CharSequence text) {
    155         StringBuilder out = new StringBuilder();
    156         withinStyle(out, text, 0, text.length());
    157         return out.toString();
    158     }
    159 
    160     private static void withinHtml(StringBuilder out, Spanned text) {
    161         int len = text.length();
    162 
    163         int next;
    164         for (int i = 0; i < text.length(); i = next) {
    165             next = text.nextSpanTransition(i, len, ParagraphStyle.class);
    166             ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
    167             String elements = " ";
    168             boolean needDiv = false;
    169 
    170             for(int j = 0; j < style.length; j++) {
    171                 if (style[j] instanceof AlignmentSpan) {
    172                     Layout.Alignment align =
    173                         ((AlignmentSpan) style[j]).getAlignment();
    174                     needDiv = true;
    175                     if (align == Layout.Alignment.ALIGN_CENTER) {
    176                         elements = "align=\"center\" " + elements;
    177                     } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
    178                         elements = "align=\"right\" " + elements;
    179                     } else {
    180                         elements = "align=\"left\" " + elements;
    181                     }
    182                 }
    183             }
    184             if (needDiv) {
    185                 out.append("<div ").append(elements).append(">");
    186             }
    187 
    188             withinDiv(out, text, i, next);
    189 
    190             if (needDiv) {
    191                 out.append("</div>");
    192             }
    193         }
    194     }
    195 
    196     private static void withinDiv(StringBuilder out, Spanned text,
    197             int start, int end) {
    198         int next;
    199         for (int i = start; i < end; i = next) {
    200             next = text.nextSpanTransition(i, end, QuoteSpan.class);
    201             QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
    202 
    203             for (QuoteSpan quote : quotes) {
    204                 out.append("<blockquote>");
    205             }
    206 
    207             withinBlockquote(out, text, i, next);
    208 
    209             for (QuoteSpan quote : quotes) {
    210                 out.append("</blockquote>\n");
    211             }
    212         }
    213     }
    214 
    215     private static String getOpenParaTagWithDirection(Spanned text, int start, int end) {
    216         final int len = end - start;
    217         final byte[] levels = new byte[ArrayUtils.idealByteArraySize(len)];
    218         final char[] buffer = TextUtils.obtain(len);
    219         TextUtils.getChars(text, start, end, buffer, 0);
    220 
    221         int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len,
    222                 false /* no info */);
    223         switch(paraDir) {
    224             case Layout.DIR_RIGHT_TO_LEFT:
    225                 return "<p dir=\"rtl\">";
    226             case Layout.DIR_LEFT_TO_RIGHT:
    227             default:
    228                 return "<p dir=\"ltr\">";
    229         }
    230     }
    231 
    232     private static void withinBlockquote(StringBuilder out, Spanned text,
    233                                          int start, int end) {
    234         out.append(getOpenParaTagWithDirection(text, start, end));
    235 
    236         int next;
    237         for (int i = start; i < end; i = next) {
    238             next = TextUtils.indexOf(text, '\n', i, end);
    239             if (next < 0) {
    240                 next = end;
    241             }
    242 
    243             int nl = 0;
    244 
    245             while (next < end && text.charAt(next) == '\n') {
    246                 nl++;
    247                 next++;
    248             }
    249 
    250             withinParagraph(out, text, i, next - nl, nl, next == end);
    251         }
    252 
    253         out.append("</p>\n");
    254     }
    255 
    256     private static void withinParagraph(StringBuilder out, Spanned text,
    257                                         int start, int end, int nl,
    258                                         boolean last) {
    259         int next;
    260         for (int i = start; i < end; i = next) {
    261             next = text.nextSpanTransition(i, end, CharacterStyle.class);
    262             CharacterStyle[] style = text.getSpans(i, next,
    263                                                    CharacterStyle.class);
    264 
    265             for (int j = 0; j < style.length; j++) {
    266                 if (style[j] instanceof StyleSpan) {
    267                     int s = ((StyleSpan) style[j]).getStyle();
    268 
    269                     if ((s & Typeface.BOLD) != 0) {
    270                         out.append("<b>");
    271                     }
    272                     if ((s & Typeface.ITALIC) != 0) {
    273                         out.append("<i>");
    274                     }
    275                 }
    276                 if (style[j] instanceof TypefaceSpan) {
    277                     String s = ((TypefaceSpan) style[j]).getFamily();
    278 
    279                     if (s.equals("monospace")) {
    280                         out.append("<tt>");
    281                     }
    282                 }
    283                 if (style[j] instanceof SuperscriptSpan) {
    284                     out.append("<sup>");
    285                 }
    286                 if (style[j] instanceof SubscriptSpan) {
    287                     out.append("<sub>");
    288                 }
    289                 if (style[j] instanceof UnderlineSpan) {
    290                     out.append("<u>");
    291                 }
    292                 if (style[j] instanceof StrikethroughSpan) {
    293                     out.append("<strike>");
    294                 }
    295                 if (style[j] instanceof URLSpan) {
    296                     out.append("<a href=\"");
    297                     out.append(((URLSpan) style[j]).getURL());
    298                     out.append("\">");
    299                 }
    300                 if (style[j] instanceof ImageSpan) {
    301                     out.append("<img src=\"");
    302                     out.append(((ImageSpan) style[j]).getSource());
    303                     out.append("\">");
    304 
    305                     // Don't output the dummy character underlying the image.
    306                     i = next;
    307                 }
    308                 if (style[j] instanceof AbsoluteSizeSpan) {
    309                     out.append("<font size =\"");
    310                     out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
    311                     out.append("\">");
    312                 }
    313                 if (style[j] instanceof ForegroundColorSpan) {
    314                     out.append("<font color =\"#");
    315                     String color = Integer.toHexString(((ForegroundColorSpan)
    316                             style[j]).getForegroundColor() + 0x01000000);
    317                     while (color.length() < 6) {
    318                         color = "0" + color;
    319                     }
    320                     out.append(color);
    321                     out.append("\">");
    322                 }
    323             }
    324 
    325             withinStyle(out, text, i, next);
    326 
    327             for (int j = style.length - 1; j >= 0; j--) {
    328                 if (style[j] instanceof ForegroundColorSpan) {
    329                     out.append("</font>");
    330                 }
    331                 if (style[j] instanceof AbsoluteSizeSpan) {
    332                     out.append("</font>");
    333                 }
    334                 if (style[j] instanceof URLSpan) {
    335                     out.append("</a>");
    336                 }
    337                 if (style[j] instanceof StrikethroughSpan) {
    338                     out.append("</strike>");
    339                 }
    340                 if (style[j] instanceof UnderlineSpan) {
    341                     out.append("</u>");
    342                 }
    343                 if (style[j] instanceof SubscriptSpan) {
    344                     out.append("</sub>");
    345                 }
    346                 if (style[j] instanceof SuperscriptSpan) {
    347                     out.append("</sup>");
    348                 }
    349                 if (style[j] instanceof TypefaceSpan) {
    350                     String s = ((TypefaceSpan) style[j]).getFamily();
    351 
    352                     if (s.equals("monospace")) {
    353                         out.append("</tt>");
    354                     }
    355                 }
    356                 if (style[j] instanceof StyleSpan) {
    357                     int s = ((StyleSpan) style[j]).getStyle();
    358 
    359                     if ((s & Typeface.BOLD) != 0) {
    360                         out.append("</b>");
    361                     }
    362                     if ((s & Typeface.ITALIC) != 0) {
    363                         out.append("</i>");
    364                     }
    365                 }
    366             }
    367         }
    368 
    369         String p = last ? "" : "</p>\n" + getOpenParaTagWithDirection(text, start, end);
    370 
    371         if (nl == 1) {
    372             out.append("<br>\n");
    373         } else if (nl == 2) {
    374             out.append(p);
    375         } else {
    376             for (int i = 2; i < nl; i++) {
    377                 out.append("<br>");
    378             }
    379             out.append(p);
    380         }
    381     }
    382 
    383     private static void withinStyle(StringBuilder out, CharSequence text,
    384                                     int start, int end) {
    385         for (int i = start; i < end; i++) {
    386             char c = text.charAt(i);
    387 
    388             if (c == '<') {
    389                 out.append("&lt;");
    390             } else if (c == '>') {
    391                 out.append("&gt;");
    392             } else if (c == '&') {
    393                 out.append("&amp;");
    394             } else if (c >= 0xD800 && c <= 0xDFFF) {
    395                 if (c < 0xDC00 && i + 1 < end) {
    396                     char d = text.charAt(i + 1);
    397                     if (d >= 0xDC00 && d <= 0xDFFF) {
    398                         i++;
    399                         int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
    400                         out.append("&#").append(codepoint).append(";");
    401                     }
    402                 }
    403             } else if (c > 0x7E || c < ' ') {
    404                 out.append("&#").append((int) c).append(";");
    405             } else if (c == ' ') {
    406                 while (i + 1 < end && text.charAt(i + 1) == ' ') {
    407                     out.append("&nbsp;");
    408                     i++;
    409                 }
    410 
    411                 out.append(' ');
    412             } else {
    413                 out.append(c);
    414             }
    415         }
    416     }
    417 }
    418 
    419 class HtmlToSpannedConverter implements ContentHandler {
    420 
    421     private static final float[] HEADER_SIZES = {
    422         1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
    423     };
    424 
    425     private String mSource;
    426     private XMLReader mReader;
    427     private SpannableStringBuilder mSpannableStringBuilder;
    428     private Html.ImageGetter mImageGetter;
    429     private Html.TagHandler mTagHandler;
    430 
    431     public HtmlToSpannedConverter(
    432             String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
    433             Parser parser) {
    434         mSource = source;
    435         mSpannableStringBuilder = new SpannableStringBuilder();
    436         mImageGetter = imageGetter;
    437         mTagHandler = tagHandler;
    438         mReader = parser;
    439     }
    440 
    441     public Spanned convert() {
    442 
    443         mReader.setContentHandler(this);
    444         try {
    445             mReader.parse(new InputSource(new StringReader(mSource)));
    446         } catch (IOException e) {
    447             // We are reading from a string. There should not be IO problems.
    448             throw new RuntimeException(e);
    449         } catch (SAXException e) {
    450             // TagSoup doesn't throw parse exceptions.
    451             throw new RuntimeException(e);
    452         }
    453 
    454         // Fix flags and range for paragraph-type markup.
    455         Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
    456         for (int i = 0; i < obj.length; i++) {
    457             int start = mSpannableStringBuilder.getSpanStart(obj[i]);
    458             int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
    459 
    460             // If the last line of the range is blank, back off by one.
    461             if (end - 2 >= 0) {
    462                 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
    463                     mSpannableStringBuilder.charAt(end - 2) == '\n') {
    464                     end--;
    465                 }
    466             }
    467 
    468             if (end == start) {
    469                 mSpannableStringBuilder.removeSpan(obj[i]);
    470             } else {
    471                 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
    472             }
    473         }
    474 
    475         return mSpannableStringBuilder;
    476     }
    477 
    478     private void handleStartTag(String tag, Attributes attributes) {
    479         if (tag.equalsIgnoreCase("br")) {
    480             // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
    481             // so we can safely emite the linebreaks when we handle the close tag.
    482         } else if (tag.equalsIgnoreCase("p")) {
    483             handleP(mSpannableStringBuilder);
    484         } else if (tag.equalsIgnoreCase("div")) {
    485             handleP(mSpannableStringBuilder);
    486         } else if (tag.equalsIgnoreCase("strong")) {
    487             start(mSpannableStringBuilder, new Bold());
    488         } else if (tag.equalsIgnoreCase("b")) {
    489             start(mSpannableStringBuilder, new Bold());
    490         } else if (tag.equalsIgnoreCase("em")) {
    491             start(mSpannableStringBuilder, new Italic());
    492         } else if (tag.equalsIgnoreCase("cite")) {
    493             start(mSpannableStringBuilder, new Italic());
    494         } else if (tag.equalsIgnoreCase("dfn")) {
    495             start(mSpannableStringBuilder, new Italic());
    496         } else if (tag.equalsIgnoreCase("i")) {
    497             start(mSpannableStringBuilder, new Italic());
    498         } else if (tag.equalsIgnoreCase("big")) {
    499             start(mSpannableStringBuilder, new Big());
    500         } else if (tag.equalsIgnoreCase("small")) {
    501             start(mSpannableStringBuilder, new Small());
    502         } else if (tag.equalsIgnoreCase("font")) {
    503             startFont(mSpannableStringBuilder, attributes);
    504         } else if (tag.equalsIgnoreCase("blockquote")) {
    505             handleP(mSpannableStringBuilder);
    506             start(mSpannableStringBuilder, new Blockquote());
    507         } else if (tag.equalsIgnoreCase("tt")) {
    508             start(mSpannableStringBuilder, new Monospace());
    509         } else if (tag.equalsIgnoreCase("a")) {
    510             startA(mSpannableStringBuilder, attributes);
    511         } else if (tag.equalsIgnoreCase("u")) {
    512             start(mSpannableStringBuilder, new Underline());
    513         } else if (tag.equalsIgnoreCase("sup")) {
    514             start(mSpannableStringBuilder, new Super());
    515         } else if (tag.equalsIgnoreCase("sub")) {
    516             start(mSpannableStringBuilder, new Sub());
    517         } else if (tag.length() == 2 &&
    518                    Character.toLowerCase(tag.charAt(0)) == 'h' &&
    519                    tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
    520             handleP(mSpannableStringBuilder);
    521             start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
    522         } else if (tag.equalsIgnoreCase("img")) {
    523             startImg(mSpannableStringBuilder, attributes, mImageGetter);
    524         } else if (mTagHandler != null) {
    525             mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
    526         }
    527     }
    528 
    529     private void handleEndTag(String tag) {
    530         if (tag.equalsIgnoreCase("br")) {
    531             handleBr(mSpannableStringBuilder);
    532         } else if (tag.equalsIgnoreCase("p")) {
    533             handleP(mSpannableStringBuilder);
    534         } else if (tag.equalsIgnoreCase("div")) {
    535             handleP(mSpannableStringBuilder);
    536         } else if (tag.equalsIgnoreCase("strong")) {
    537             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
    538         } else if (tag.equalsIgnoreCase("b")) {
    539             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
    540         } else if (tag.equalsIgnoreCase("em")) {
    541             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    542         } else if (tag.equalsIgnoreCase("cite")) {
    543             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    544         } else if (tag.equalsIgnoreCase("dfn")) {
    545             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    546         } else if (tag.equalsIgnoreCase("i")) {
    547             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    548         } else if (tag.equalsIgnoreCase("big")) {
    549             end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
    550         } else if (tag.equalsIgnoreCase("small")) {
    551             end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
    552         } else if (tag.equalsIgnoreCase("font")) {
    553             endFont(mSpannableStringBuilder);
    554         } else if (tag.equalsIgnoreCase("blockquote")) {
    555             handleP(mSpannableStringBuilder);
    556             end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
    557         } else if (tag.equalsIgnoreCase("tt")) {
    558             end(mSpannableStringBuilder, Monospace.class,
    559                     new TypefaceSpan("monospace"));
    560         } else if (tag.equalsIgnoreCase("a")) {
    561             endA(mSpannableStringBuilder);
    562         } else if (tag.equalsIgnoreCase("u")) {
    563             end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
    564         } else if (tag.equalsIgnoreCase("sup")) {
    565             end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
    566         } else if (tag.equalsIgnoreCase("sub")) {
    567             end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
    568         } else if (tag.length() == 2 &&
    569                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
    570                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
    571             handleP(mSpannableStringBuilder);
    572             endHeader(mSpannableStringBuilder);
    573         } else if (mTagHandler != null) {
    574             mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
    575         }
    576     }
    577 
    578     private static void handleP(SpannableStringBuilder text) {
    579         int len = text.length();
    580 
    581         if (len >= 1 && text.charAt(len - 1) == '\n') {
    582             if (len >= 2 && text.charAt(len - 2) == '\n') {
    583                 return;
    584             }
    585 
    586             text.append("\n");
    587             return;
    588         }
    589 
    590         if (len != 0) {
    591             text.append("\n\n");
    592         }
    593     }
    594 
    595     private static void handleBr(SpannableStringBuilder text) {
    596         text.append("\n");
    597     }
    598 
    599     private static Object getLast(Spanned text, Class kind) {
    600         /*
    601          * This knows that the last returned object from getSpans()
    602          * will be the most recently added.
    603          */
    604         Object[] objs = text.getSpans(0, text.length(), kind);
    605 
    606         if (objs.length == 0) {
    607             return null;
    608         } else {
    609             return objs[objs.length - 1];
    610         }
    611     }
    612 
    613     private static void start(SpannableStringBuilder text, Object mark) {
    614         int len = text.length();
    615         text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
    616     }
    617 
    618     private static void end(SpannableStringBuilder text, Class kind,
    619                             Object repl) {
    620         int len = text.length();
    621         Object obj = getLast(text, kind);
    622         int where = text.getSpanStart(obj);
    623 
    624         text.removeSpan(obj);
    625 
    626         if (where != len) {
    627             text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    628         }
    629     }
    630 
    631     private static void startImg(SpannableStringBuilder text,
    632                                  Attributes attributes, Html.ImageGetter img) {
    633         String src = attributes.getValue("", "src");
    634         Drawable d = null;
    635 
    636         if (img != null) {
    637             d = img.getDrawable(src);
    638         }
    639 
    640         if (d == null) {
    641             d = Resources.getSystem().
    642                     getDrawable(com.android.internal.R.drawable.unknown_image);
    643             d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
    644         }
    645 
    646         int len = text.length();
    647         text.append("\uFFFC");
    648 
    649         text.setSpan(new ImageSpan(d, src), len, text.length(),
    650                      Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    651     }
    652 
    653     private static void startFont(SpannableStringBuilder text,
    654                                   Attributes attributes) {
    655         String color = attributes.getValue("", "color");
    656         String face = attributes.getValue("", "face");
    657 
    658         int len = text.length();
    659         text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
    660     }
    661 
    662     private static void endFont(SpannableStringBuilder text) {
    663         int len = text.length();
    664         Object obj = getLast(text, Font.class);
    665         int where = text.getSpanStart(obj);
    666 
    667         text.removeSpan(obj);
    668 
    669         if (where != len) {
    670             Font f = (Font) obj;
    671 
    672             if (!TextUtils.isEmpty(f.mColor)) {
    673                 if (f.mColor.startsWith("@")) {
    674                     Resources res = Resources.getSystem();
    675                     String name = f.mColor.substring(1);
    676                     int colorRes = res.getIdentifier(name, "color", "android");
    677                     if (colorRes != 0) {
    678                         ColorStateList colors = res.getColorStateList(colorRes);
    679                         text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
    680                                 where, len,
    681                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    682                     }
    683                 } else {
    684                     int c = Color.getHtmlColor(f.mColor);
    685                     if (c != -1) {
    686                         text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
    687                                 where, len,
    688                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    689                     }
    690                 }
    691             }
    692 
    693             if (f.mFace != null) {
    694                 text.setSpan(new TypefaceSpan(f.mFace), where, len,
    695                              Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    696             }
    697         }
    698     }
    699 
    700     private static void startA(SpannableStringBuilder text, Attributes attributes) {
    701         String href = attributes.getValue("", "href");
    702 
    703         int len = text.length();
    704         text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
    705     }
    706 
    707     private static void endA(SpannableStringBuilder text) {
    708         int len = text.length();
    709         Object obj = getLast(text, Href.class);
    710         int where = text.getSpanStart(obj);
    711 
    712         text.removeSpan(obj);
    713 
    714         if (where != len) {
    715             Href h = (Href) obj;
    716 
    717             if (h.mHref != null) {
    718                 text.setSpan(new URLSpan(h.mHref), where, len,
    719                              Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    720             }
    721         }
    722     }
    723 
    724     private static void endHeader(SpannableStringBuilder text) {
    725         int len = text.length();
    726         Object obj = getLast(text, Header.class);
    727 
    728         int where = text.getSpanStart(obj);
    729 
    730         text.removeSpan(obj);
    731 
    732         // Back off not to change only the text, not the blank line.
    733         while (len > where && text.charAt(len - 1) == '\n') {
    734             len--;
    735         }
    736 
    737         if (where != len) {
    738             Header h = (Header) obj;
    739 
    740             text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
    741                          where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    742             text.setSpan(new StyleSpan(Typeface.BOLD),
    743                          where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    744         }
    745     }
    746 
    747     public void setDocumentLocator(Locator locator) {
    748     }
    749 
    750     public void startDocument() throws SAXException {
    751     }
    752 
    753     public void endDocument() throws SAXException {
    754     }
    755 
    756     public void startPrefixMapping(String prefix, String uri) throws SAXException {
    757     }
    758 
    759     public void endPrefixMapping(String prefix) throws SAXException {
    760     }
    761 
    762     public void startElement(String uri, String localName, String qName, Attributes attributes)
    763             throws SAXException {
    764         handleStartTag(localName, attributes);
    765     }
    766 
    767     public void endElement(String uri, String localName, String qName) throws SAXException {
    768         handleEndTag(localName);
    769     }
    770 
    771     public void characters(char ch[], int start, int length) throws SAXException {
    772         StringBuilder sb = new StringBuilder();
    773 
    774         /*
    775          * Ignore whitespace that immediately follows other whitespace;
    776          * newlines count as spaces.
    777          */
    778 
    779         for (int i = 0; i < length; i++) {
    780             char c = ch[i + start];
    781 
    782             if (c == ' ' || c == '\n') {
    783                 char pred;
    784                 int len = sb.length();
    785 
    786                 if (len == 0) {
    787                     len = mSpannableStringBuilder.length();
    788 
    789                     if (len == 0) {
    790                         pred = '\n';
    791                     } else {
    792                         pred = mSpannableStringBuilder.charAt(len - 1);
    793                     }
    794                 } else {
    795                     pred = sb.charAt(len - 1);
    796                 }
    797 
    798                 if (pred != ' ' && pred != '\n') {
    799                     sb.append(' ');
    800                 }
    801             } else {
    802                 sb.append(c);
    803             }
    804         }
    805 
    806         mSpannableStringBuilder.append(sb);
    807     }
    808 
    809     public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
    810     }
    811 
    812     public void processingInstruction(String target, String data) throws SAXException {
    813     }
    814 
    815     public void skippedEntity(String name) throws SAXException {
    816     }
    817 
    818     private static class Bold { }
    819     private static class Italic { }
    820     private static class Underline { }
    821     private static class Big { }
    822     private static class Small { }
    823     private static class Monospace { }
    824     private static class Blockquote { }
    825     private static class Super { }
    826     private static class Sub { }
    827 
    828     private static class Font {
    829         public String mColor;
    830         public String mFace;
    831 
    832         public Font(String color, String face) {
    833             mColor = color;
    834             mFace = face;
    835         }
    836     }
    837 
    838     private static class Href {
    839         public String mHref;
    840 
    841         public Href(String href) {
    842             mHref = href;
    843         }
    844     }
    845 
    846     private static class Header {
    847         private int mLevel;
    848 
    849         public Header(int level) {
    850             mLevel = level;
    851         }
    852     }
    853 }
    854