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