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.annotation.UnsupportedAppUsage;
     20 import android.app.ActivityThread;
     21 import android.app.Application;
     22 import android.content.res.Resources;
     23 import android.graphics.Color;
     24 import android.graphics.Typeface;
     25 import android.graphics.drawable.Drawable;
     26 import android.text.style.AbsoluteSizeSpan;
     27 import android.text.style.AlignmentSpan;
     28 import android.text.style.BackgroundColorSpan;
     29 import android.text.style.BulletSpan;
     30 import android.text.style.CharacterStyle;
     31 import android.text.style.ForegroundColorSpan;
     32 import android.text.style.ImageSpan;
     33 import android.text.style.ParagraphStyle;
     34 import android.text.style.QuoteSpan;
     35 import android.text.style.RelativeSizeSpan;
     36 import android.text.style.StrikethroughSpan;
     37 import android.text.style.StyleSpan;
     38 import android.text.style.SubscriptSpan;
     39 import android.text.style.SuperscriptSpan;
     40 import android.text.style.TypefaceSpan;
     41 import android.text.style.URLSpan;
     42 import android.text.style.UnderlineSpan;
     43 
     44 import org.ccil.cowan.tagsoup.HTMLSchema;
     45 import org.ccil.cowan.tagsoup.Parser;
     46 import org.xml.sax.Attributes;
     47 import org.xml.sax.ContentHandler;
     48 import org.xml.sax.InputSource;
     49 import org.xml.sax.Locator;
     50 import org.xml.sax.SAXException;
     51 import org.xml.sax.XMLReader;
     52 
     53 import java.io.IOException;
     54 import java.io.StringReader;
     55 import java.util.HashMap;
     56 import java.util.Locale;
     57 import java.util.Map;
     58 import java.util.regex.Matcher;
     59 import java.util.regex.Pattern;
     60 
     61 /**
     62  * This class processes HTML strings into displayable styled text.
     63  * Not all HTML tags are supported.
     64  */
     65 public class Html {
     66     /**
     67      * Retrieves images for HTML <img> tags.
     68      */
     69     public static interface ImageGetter {
     70         /**
     71          * This method is called when the HTML parser encounters an
     72          * &lt;img&gt; tag.  The <code>source</code> argument is the
     73          * string from the "src" attribute; the return value should be
     74          * a Drawable representation of the image or <code>null</code>
     75          * for a generic replacement image.  Make sure you call
     76          * setBounds() on your Drawable if it doesn't already have
     77          * its bounds set.
     78          */
     79         public Drawable getDrawable(String source);
     80     }
     81 
     82     /**
     83      * Is notified when HTML tags are encountered that the parser does
     84      * not know how to interpret.
     85      */
     86     public static interface TagHandler {
     87         /**
     88          * This method will be called whenn the HTML parser encounters
     89          * a tag that it does not know how to interpret.
     90          */
     91         public void handleTag(boolean opening, String tag,
     92                                  Editable output, XMLReader xmlReader);
     93     }
     94 
     95     /**
     96      * Option for {@link #toHtml(Spanned, int)}: Wrap consecutive lines of text delimited by '\n'
     97      * inside &lt;p&gt; elements. {@link BulletSpan}s are ignored.
     98      */
     99     public static final int TO_HTML_PARAGRAPH_LINES_CONSECUTIVE = 0x00000000;
    100 
    101     /**
    102      * Option for {@link #toHtml(Spanned, int)}: Wrap each line of text delimited by '\n' inside a
    103      * &lt;p&gt; or a &lt;li&gt; element. This allows {@link ParagraphStyle}s attached to be
    104      * encoded as CSS styles within the corresponding &lt;p&gt; or &lt;li&gt; element.
    105      */
    106     public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001;
    107 
    108     /**
    109      * Flag indicating that texts inside &lt;p&gt; elements will be separated from other texts with
    110      * one newline character by default.
    111      */
    112     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH = 0x00000001;
    113 
    114     /**
    115      * Flag indicating that texts inside &lt;h1&gt;~&lt;h6&gt; elements will be separated from
    116      * other texts with one newline character by default.
    117      */
    118     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_HEADING = 0x00000002;
    119 
    120     /**
    121      * Flag indicating that texts inside &lt;li&gt; elements will be separated from other texts
    122      * with one newline character by default.
    123      */
    124     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM = 0x00000004;
    125 
    126     /**
    127      * Flag indicating that texts inside &lt;ul&gt; elements will be separated from other texts
    128      * with one newline character by default.
    129      */
    130     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST = 0x00000008;
    131 
    132     /**
    133      * Flag indicating that texts inside &lt;div&gt; elements will be separated from other texts
    134      * with one newline character by default.
    135      */
    136     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_DIV = 0x00000010;
    137 
    138     /**
    139      * Flag indicating that texts inside &lt;blockquote&gt; elements will be separated from other
    140      * texts with one newline character by default.
    141      */
    142     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE = 0x00000020;
    143 
    144     /**
    145      * Flag indicating that CSS color values should be used instead of those defined in
    146      * {@link Color}.
    147      */
    148     public static final int FROM_HTML_OPTION_USE_CSS_COLORS = 0x00000100;
    149 
    150     /**
    151      * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level
    152      * elements with blank lines (two newline characters) in between. This is the legacy behavior
    153      * prior to N.
    154      */
    155     public static final int FROM_HTML_MODE_LEGACY = 0x00000000;
    156 
    157     /**
    158      * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level
    159      * elements with line breaks (single newline character) in between. This inverts the
    160      * {@link Spanned} to HTML string conversion done with the option
    161      * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}.
    162      */
    163     public static final int FROM_HTML_MODE_COMPACT =
    164             FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH
    165             | FROM_HTML_SEPARATOR_LINE_BREAK_HEADING
    166             | FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM
    167             | FROM_HTML_SEPARATOR_LINE_BREAK_LIST
    168             | FROM_HTML_SEPARATOR_LINE_BREAK_DIV
    169             | FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE;
    170 
    171     /**
    172      * The bit which indicates if lines delimited by '\n' will be grouped into &lt;p&gt; elements.
    173      */
    174     private static final int TO_HTML_PARAGRAPH_FLAG = 0x00000001;
    175 
    176     private Html() { }
    177 
    178     /**
    179      * Returns displayable styled text from the provided HTML string with the legacy flags
    180      * {@link #FROM_HTML_MODE_LEGACY}.
    181      *
    182      * @deprecated use {@link #fromHtml(String, int)} instead.
    183      */
    184     @Deprecated
    185     public static Spanned fromHtml(String source) {
    186         return fromHtml(source, FROM_HTML_MODE_LEGACY, null, null);
    187     }
    188 
    189     /**
    190      * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; tags in the
    191      * HTML will display as a generic replacement image which your program can then go through and
    192      * replace with real images.
    193      *
    194      * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
    195      */
    196     public static Spanned fromHtml(String source, int flags) {
    197         return fromHtml(source, flags, null, null);
    198     }
    199 
    200     /**
    201      * Lazy initialization holder for HTML parser. This class will
    202      * a) be preloaded by the zygote, or b) not loaded until absolutely
    203      * necessary.
    204      */
    205     private static class HtmlParser {
    206         private static final HTMLSchema schema = new HTMLSchema();
    207     }
    208 
    209     /**
    210      * Returns displayable styled text from the provided HTML string with the legacy flags
    211      * {@link #FROM_HTML_MODE_LEGACY}.
    212      *
    213      * @deprecated use {@link #fromHtml(String, int, ImageGetter, TagHandler)} instead.
    214      */
    215     @Deprecated
    216     public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
    217         return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler);
    218     }
    219 
    220     /**
    221      * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; tags in the
    222      * HTML will use the specified ImageGetter to request a representation of the image (use null
    223      * if you don't want this) and the specified TagHandler to handle unknown tags (specify null if
    224      * you don't want this).
    225      *
    226      * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
    227      */
    228     public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter,
    229             TagHandler tagHandler) {
    230         Parser parser = new Parser();
    231         try {
    232             parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
    233         } catch (org.xml.sax.SAXNotRecognizedException e) {
    234             // Should not happen.
    235             throw new RuntimeException(e);
    236         } catch (org.xml.sax.SAXNotSupportedException e) {
    237             // Should not happen.
    238             throw new RuntimeException(e);
    239         }
    240 
    241         HtmlToSpannedConverter converter =
    242                 new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags);
    243         return converter.convert();
    244     }
    245 
    246     /**
    247      * @deprecated use {@link #toHtml(Spanned, int)} instead.
    248      */
    249     @Deprecated
    250     public static String toHtml(Spanned text) {
    251         return toHtml(text, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE);
    252     }
    253 
    254     /**
    255      * Returns an HTML representation of the provided Spanned text. A best effort is
    256      * made to add HTML tags corresponding to spans. Also note that HTML metacharacters
    257      * (such as "&lt;" and "&amp;") within the input text are escaped.
    258      *
    259      * @param text input text to convert
    260      * @param option one of {@link #TO_HTML_PARAGRAPH_LINES_CONSECUTIVE} or
    261      *     {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}
    262      * @return string containing input converted to HTML
    263      */
    264     public static String toHtml(Spanned text, int option) {
    265         StringBuilder out = new StringBuilder();
    266         withinHtml(out, text, option);
    267         return out.toString();
    268     }
    269 
    270     /**
    271      * Returns an HTML escaped representation of the given plain text.
    272      */
    273     public static String escapeHtml(CharSequence text) {
    274         StringBuilder out = new StringBuilder();
    275         withinStyle(out, text, 0, text.length());
    276         return out.toString();
    277     }
    278 
    279     private static void withinHtml(StringBuilder out, Spanned text, int option) {
    280         if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) {
    281             encodeTextAlignmentByDiv(out, text, option);
    282             return;
    283         }
    284 
    285         withinDiv(out, text, 0, text.length(), option);
    286     }
    287 
    288     private static void encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option) {
    289         int len = text.length();
    290 
    291         int next;
    292         for (int i = 0; i < len; i = next) {
    293             next = text.nextSpanTransition(i, len, ParagraphStyle.class);
    294             ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
    295             String elements = " ";
    296             boolean needDiv = false;
    297 
    298             for(int j = 0; j < style.length; j++) {
    299                 if (style[j] instanceof AlignmentSpan) {
    300                     Layout.Alignment align =
    301                         ((AlignmentSpan) style[j]).getAlignment();
    302                     needDiv = true;
    303                     if (align == Layout.Alignment.ALIGN_CENTER) {
    304                         elements = "align=\"center\" " + elements;
    305                     } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
    306                         elements = "align=\"right\" " + elements;
    307                     } else {
    308                         elements = "align=\"left\" " + elements;
    309                     }
    310                 }
    311             }
    312             if (needDiv) {
    313                 out.append("<div ").append(elements).append(">");
    314             }
    315 
    316             withinDiv(out, text, i, next, option);
    317 
    318             if (needDiv) {
    319                 out.append("</div>");
    320             }
    321         }
    322     }
    323 
    324     private static void withinDiv(StringBuilder out, Spanned text, int start, int end,
    325             int option) {
    326         int next;
    327         for (int i = start; i < end; i = next) {
    328             next = text.nextSpanTransition(i, end, QuoteSpan.class);
    329             QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
    330 
    331             for (QuoteSpan quote : quotes) {
    332                 out.append("<blockquote>");
    333             }
    334 
    335             withinBlockquote(out, text, i, next, option);
    336 
    337             for (QuoteSpan quote : quotes) {
    338                 out.append("</blockquote>\n");
    339             }
    340         }
    341     }
    342 
    343     private static String getTextDirection(Spanned text, int start, int end) {
    344         if (TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(text, start, end - start)) {
    345             return " dir=\"rtl\"";
    346         } else {
    347             return " dir=\"ltr\"";
    348         }
    349     }
    350 
    351     private static String getTextStyles(Spanned text, int start, int end,
    352             boolean forceNoVerticalMargin, boolean includeTextAlign) {
    353         String margin = null;
    354         String textAlign = null;
    355 
    356         if (forceNoVerticalMargin) {
    357             margin = "margin-top:0; margin-bottom:0;";
    358         }
    359         if (includeTextAlign) {
    360             final AlignmentSpan[] alignmentSpans = text.getSpans(start, end, AlignmentSpan.class);
    361 
    362             // Only use the last AlignmentSpan with flag SPAN_PARAGRAPH
    363             for (int i = alignmentSpans.length - 1; i >= 0; i--) {
    364                 AlignmentSpan s = alignmentSpans[i];
    365                 if ((text.getSpanFlags(s) & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH) {
    366                     final Layout.Alignment alignment = s.getAlignment();
    367                     if (alignment == Layout.Alignment.ALIGN_NORMAL) {
    368                         textAlign = "text-align:start;";
    369                     } else if (alignment == Layout.Alignment.ALIGN_CENTER) {
    370                         textAlign = "text-align:center;";
    371                     } else if (alignment == Layout.Alignment.ALIGN_OPPOSITE) {
    372                         textAlign = "text-align:end;";
    373                     }
    374                     break;
    375                 }
    376             }
    377         }
    378 
    379         if (margin == null && textAlign == null) {
    380             return "";
    381         }
    382 
    383         final StringBuilder style = new StringBuilder(" style=\"");
    384         if (margin != null && textAlign != null) {
    385             style.append(margin).append(" ").append(textAlign);
    386         } else if (margin != null) {
    387             style.append(margin);
    388         } else if (textAlign != null) {
    389             style.append(textAlign);
    390         }
    391 
    392         return style.append("\"").toString();
    393     }
    394 
    395     private static void withinBlockquote(StringBuilder out, Spanned text, int start, int end,
    396             int option) {
    397         if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) {
    398             withinBlockquoteConsecutive(out, text, start, end);
    399         } else {
    400             withinBlockquoteIndividual(out, text, start, end);
    401         }
    402     }
    403 
    404     private static void withinBlockquoteIndividual(StringBuilder out, Spanned text, int start,
    405             int end) {
    406         boolean isInList = false;
    407         int next;
    408         for (int i = start; i <= end; i = next) {
    409             next = TextUtils.indexOf(text, '\n', i, end);
    410             if (next < 0) {
    411                 next = end;
    412             }
    413 
    414             if (next == i) {
    415                 if (isInList) {
    416                     // Current paragraph is no longer a list item; close the previously opened list
    417                     isInList = false;
    418                     out.append("</ul>\n");
    419                 }
    420                 out.append("<br>\n");
    421             } else {
    422                 boolean isListItem = false;
    423                 ParagraphStyle[] paragraphStyles = text.getSpans(i, next, ParagraphStyle.class);
    424                 for (ParagraphStyle paragraphStyle : paragraphStyles) {
    425                     final int spanFlags = text.getSpanFlags(paragraphStyle);
    426                     if ((spanFlags & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH
    427                             && paragraphStyle instanceof BulletSpan) {
    428                         isListItem = true;
    429                         break;
    430                     }
    431                 }
    432 
    433                 if (isListItem && !isInList) {
    434                     // Current paragraph is the first item in a list
    435                     isInList = true;
    436                     out.append("<ul")
    437                             .append(getTextStyles(text, i, next, true, false))
    438                             .append(">\n");
    439                 }
    440 
    441                 if (isInList && !isListItem) {
    442                     // Current paragraph is no longer a list item; close the previously opened list
    443                     isInList = false;
    444                     out.append("</ul>\n");
    445                 }
    446 
    447                 String tagType = isListItem ? "li" : "p";
    448                 out.append("<").append(tagType)
    449                         .append(getTextDirection(text, i, next))
    450                         .append(getTextStyles(text, i, next, !isListItem, true))
    451                         .append(">");
    452 
    453                 withinParagraph(out, text, i, next);
    454 
    455                 out.append("</");
    456                 out.append(tagType);
    457                 out.append(">\n");
    458 
    459                 if (next == end && isInList) {
    460                     isInList = false;
    461                     out.append("</ul>\n");
    462                 }
    463             }
    464 
    465             next++;
    466         }
    467     }
    468 
    469     private static void withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start,
    470             int end) {
    471         out.append("<p").append(getTextDirection(text, start, end)).append(">");
    472 
    473         int next;
    474         for (int i = start; i < end; i = next) {
    475             next = TextUtils.indexOf(text, '\n', i, end);
    476             if (next < 0) {
    477                 next = end;
    478             }
    479 
    480             int nl = 0;
    481 
    482             while (next < end && text.charAt(next) == '\n') {
    483                 nl++;
    484                 next++;
    485             }
    486 
    487             withinParagraph(out, text, i, next - nl);
    488 
    489             if (nl == 1) {
    490                 out.append("<br>\n");
    491             } else {
    492                 for (int j = 2; j < nl; j++) {
    493                     out.append("<br>");
    494                 }
    495                 if (next != end) {
    496                     /* Paragraph should be closed and reopened */
    497                     out.append("</p>\n");
    498                     out.append("<p").append(getTextDirection(text, start, end)).append(">");
    499                 }
    500             }
    501         }
    502 
    503         out.append("</p>\n");
    504     }
    505 
    506     private static void withinParagraph(StringBuilder out, Spanned text, int start, int end) {
    507         int next;
    508         for (int i = start; i < end; i = next) {
    509             next = text.nextSpanTransition(i, end, CharacterStyle.class);
    510             CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
    511 
    512             for (int j = 0; j < style.length; j++) {
    513                 if (style[j] instanceof StyleSpan) {
    514                     int s = ((StyleSpan) style[j]).getStyle();
    515 
    516                     if ((s & Typeface.BOLD) != 0) {
    517                         out.append("<b>");
    518                     }
    519                     if ((s & Typeface.ITALIC) != 0) {
    520                         out.append("<i>");
    521                     }
    522                 }
    523                 if (style[j] instanceof TypefaceSpan) {
    524                     String s = ((TypefaceSpan) style[j]).getFamily();
    525 
    526                     if ("monospace".equals(s)) {
    527                         out.append("<tt>");
    528                     }
    529                 }
    530                 if (style[j] instanceof SuperscriptSpan) {
    531                     out.append("<sup>");
    532                 }
    533                 if (style[j] instanceof SubscriptSpan) {
    534                     out.append("<sub>");
    535                 }
    536                 if (style[j] instanceof UnderlineSpan) {
    537                     out.append("<u>");
    538                 }
    539                 if (style[j] instanceof StrikethroughSpan) {
    540                     out.append("<span style=\"text-decoration:line-through;\">");
    541                 }
    542                 if (style[j] instanceof URLSpan) {
    543                     out.append("<a href=\"");
    544                     out.append(((URLSpan) style[j]).getURL());
    545                     out.append("\">");
    546                 }
    547                 if (style[j] instanceof ImageSpan) {
    548                     out.append("<img src=\"");
    549                     out.append(((ImageSpan) style[j]).getSource());
    550                     out.append("\">");
    551 
    552                     // Don't output the dummy character underlying the image.
    553                     i = next;
    554                 }
    555                 if (style[j] instanceof AbsoluteSizeSpan) {
    556                     AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
    557                     float sizeDip = s.getSize();
    558                     if (!s.getDip()) {
    559                         Application application = ActivityThread.currentApplication();
    560                         sizeDip /= application.getResources().getDisplayMetrics().density;
    561                     }
    562 
    563                     // px in CSS is the equivalance of dip in Android
    564                     out.append(String.format("<span style=\"font-size:%.0fpx\";>", sizeDip));
    565                 }
    566                 if (style[j] instanceof RelativeSizeSpan) {
    567                     float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
    568                     out.append(String.format("<span style=\"font-size:%.2fem;\">", sizeEm));
    569                 }
    570                 if (style[j] instanceof ForegroundColorSpan) {
    571                     int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
    572                     out.append(String.format("<span style=\"color:#%06X;\">", 0xFFFFFF & color));
    573                 }
    574                 if (style[j] instanceof BackgroundColorSpan) {
    575                     int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
    576                     out.append(String.format("<span style=\"background-color:#%06X;\">",
    577                             0xFFFFFF & color));
    578                 }
    579             }
    580 
    581             withinStyle(out, text, i, next);
    582 
    583             for (int j = style.length - 1; j >= 0; j--) {
    584                 if (style[j] instanceof BackgroundColorSpan) {
    585                     out.append("</span>");
    586                 }
    587                 if (style[j] instanceof ForegroundColorSpan) {
    588                     out.append("</span>");
    589                 }
    590                 if (style[j] instanceof RelativeSizeSpan) {
    591                     out.append("</span>");
    592                 }
    593                 if (style[j] instanceof AbsoluteSizeSpan) {
    594                     out.append("</span>");
    595                 }
    596                 if (style[j] instanceof URLSpan) {
    597                     out.append("</a>");
    598                 }
    599                 if (style[j] instanceof StrikethroughSpan) {
    600                     out.append("</span>");
    601                 }
    602                 if (style[j] instanceof UnderlineSpan) {
    603                     out.append("</u>");
    604                 }
    605                 if (style[j] instanceof SubscriptSpan) {
    606                     out.append("</sub>");
    607                 }
    608                 if (style[j] instanceof SuperscriptSpan) {
    609                     out.append("</sup>");
    610                 }
    611                 if (style[j] instanceof TypefaceSpan) {
    612                     String s = ((TypefaceSpan) style[j]).getFamily();
    613 
    614                     if (s.equals("monospace")) {
    615                         out.append("</tt>");
    616                     }
    617                 }
    618                 if (style[j] instanceof StyleSpan) {
    619                     int s = ((StyleSpan) style[j]).getStyle();
    620 
    621                     if ((s & Typeface.BOLD) != 0) {
    622                         out.append("</b>");
    623                     }
    624                     if ((s & Typeface.ITALIC) != 0) {
    625                         out.append("</i>");
    626                     }
    627                 }
    628             }
    629         }
    630     }
    631 
    632     @UnsupportedAppUsage
    633     private static void withinStyle(StringBuilder out, CharSequence text,
    634                                     int start, int end) {
    635         for (int i = start; i < end; i++) {
    636             char c = text.charAt(i);
    637 
    638             if (c == '<') {
    639                 out.append("&lt;");
    640             } else if (c == '>') {
    641                 out.append("&gt;");
    642             } else if (c == '&') {
    643                 out.append("&amp;");
    644             } else if (c >= 0xD800 && c <= 0xDFFF) {
    645                 if (c < 0xDC00 && i + 1 < end) {
    646                     char d = text.charAt(i + 1);
    647                     if (d >= 0xDC00 && d <= 0xDFFF) {
    648                         i++;
    649                         int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
    650                         out.append("&#").append(codepoint).append(";");
    651                     }
    652                 }
    653             } else if (c > 0x7E || c < ' ') {
    654                 out.append("&#").append((int) c).append(";");
    655             } else if (c == ' ') {
    656                 while (i + 1 < end && text.charAt(i + 1) == ' ') {
    657                     out.append("&nbsp;");
    658                     i++;
    659                 }
    660 
    661                 out.append(' ');
    662             } else {
    663                 out.append(c);
    664             }
    665         }
    666     }
    667 }
    668 
    669 class HtmlToSpannedConverter implements ContentHandler {
    670 
    671     private static final float[] HEADING_SIZES = {
    672         1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
    673     };
    674 
    675     private String mSource;
    676     private XMLReader mReader;
    677     private SpannableStringBuilder mSpannableStringBuilder;
    678     private Html.ImageGetter mImageGetter;
    679     private Html.TagHandler mTagHandler;
    680     private int mFlags;
    681 
    682     private static Pattern sTextAlignPattern;
    683     private static Pattern sForegroundColorPattern;
    684     private static Pattern sBackgroundColorPattern;
    685     private static Pattern sTextDecorationPattern;
    686 
    687     /**
    688      * Name-value mapping of HTML/CSS colors which have different values in {@link Color}.
    689      */
    690     private static final Map<String, Integer> sColorMap;
    691 
    692     static {
    693         sColorMap = new HashMap<>();
    694         sColorMap.put("darkgray", 0xFFA9A9A9);
    695         sColorMap.put("gray", 0xFF808080);
    696         sColorMap.put("lightgray", 0xFFD3D3D3);
    697         sColorMap.put("darkgrey", 0xFFA9A9A9);
    698         sColorMap.put("grey", 0xFF808080);
    699         sColorMap.put("lightgrey", 0xFFD3D3D3);
    700         sColorMap.put("green", 0xFF008000);
    701     }
    702 
    703     private static Pattern getTextAlignPattern() {
    704         if (sTextAlignPattern == null) {
    705             sTextAlignPattern = Pattern.compile("(?:\\s+|\\A)text-align\\s*:\\s*(\\S*)\\b");
    706         }
    707         return sTextAlignPattern;
    708     }
    709 
    710     private static Pattern getForegroundColorPattern() {
    711         if (sForegroundColorPattern == null) {
    712             sForegroundColorPattern = Pattern.compile(
    713                     "(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b");
    714         }
    715         return sForegroundColorPattern;
    716     }
    717 
    718     private static Pattern getBackgroundColorPattern() {
    719         if (sBackgroundColorPattern == null) {
    720             sBackgroundColorPattern = Pattern.compile(
    721                     "(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b");
    722         }
    723         return sBackgroundColorPattern;
    724     }
    725 
    726     private static Pattern getTextDecorationPattern() {
    727         if (sTextDecorationPattern == null) {
    728             sTextDecorationPattern = Pattern.compile(
    729                     "(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b");
    730         }
    731         return sTextDecorationPattern;
    732     }
    733 
    734     public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter,
    735             Html.TagHandler tagHandler, Parser parser, int flags) {
    736         mSource = source;
    737         mSpannableStringBuilder = new SpannableStringBuilder();
    738         mImageGetter = imageGetter;
    739         mTagHandler = tagHandler;
    740         mReader = parser;
    741         mFlags = flags;
    742     }
    743 
    744     public Spanned convert() {
    745 
    746         mReader.setContentHandler(this);
    747         try {
    748             mReader.parse(new InputSource(new StringReader(mSource)));
    749         } catch (IOException e) {
    750             // We are reading from a string. There should not be IO problems.
    751             throw new RuntimeException(e);
    752         } catch (SAXException e) {
    753             // TagSoup doesn't throw parse exceptions.
    754             throw new RuntimeException(e);
    755         }
    756 
    757         // Fix flags and range for paragraph-type markup.
    758         Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
    759         for (int i = 0; i < obj.length; i++) {
    760             int start = mSpannableStringBuilder.getSpanStart(obj[i]);
    761             int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
    762 
    763             // If the last line of the range is blank, back off by one.
    764             if (end - 2 >= 0) {
    765                 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
    766                     mSpannableStringBuilder.charAt(end - 2) == '\n') {
    767                     end--;
    768                 }
    769             }
    770 
    771             if (end == start) {
    772                 mSpannableStringBuilder.removeSpan(obj[i]);
    773             } else {
    774                 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
    775             }
    776         }
    777 
    778         return mSpannableStringBuilder;
    779     }
    780 
    781     private void handleStartTag(String tag, Attributes attributes) {
    782         if (tag.equalsIgnoreCase("br")) {
    783             // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
    784             // so we can safely emit the linebreaks when we handle the close tag.
    785         } else if (tag.equalsIgnoreCase("p")) {
    786             startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());
    787             startCssStyle(mSpannableStringBuilder, attributes);
    788         } else if (tag.equalsIgnoreCase("ul")) {
    789             startBlockElement(mSpannableStringBuilder, attributes, getMarginList());
    790         } else if (tag.equalsIgnoreCase("li")) {
    791             startLi(mSpannableStringBuilder, attributes);
    792         } else if (tag.equalsIgnoreCase("div")) {
    793             startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv());
    794         } else if (tag.equalsIgnoreCase("span")) {
    795             startCssStyle(mSpannableStringBuilder, attributes);
    796         } else if (tag.equalsIgnoreCase("strong")) {
    797             start(mSpannableStringBuilder, new Bold());
    798         } else if (tag.equalsIgnoreCase("b")) {
    799             start(mSpannableStringBuilder, new Bold());
    800         } else if (tag.equalsIgnoreCase("em")) {
    801             start(mSpannableStringBuilder, new Italic());
    802         } else if (tag.equalsIgnoreCase("cite")) {
    803             start(mSpannableStringBuilder, new Italic());
    804         } else if (tag.equalsIgnoreCase("dfn")) {
    805             start(mSpannableStringBuilder, new Italic());
    806         } else if (tag.equalsIgnoreCase("i")) {
    807             start(mSpannableStringBuilder, new Italic());
    808         } else if (tag.equalsIgnoreCase("big")) {
    809             start(mSpannableStringBuilder, new Big());
    810         } else if (tag.equalsIgnoreCase("small")) {
    811             start(mSpannableStringBuilder, new Small());
    812         } else if (tag.equalsIgnoreCase("font")) {
    813             startFont(mSpannableStringBuilder, attributes);
    814         } else if (tag.equalsIgnoreCase("blockquote")) {
    815             startBlockquote(mSpannableStringBuilder, attributes);
    816         } else if (tag.equalsIgnoreCase("tt")) {
    817             start(mSpannableStringBuilder, new Monospace());
    818         } else if (tag.equalsIgnoreCase("a")) {
    819             startA(mSpannableStringBuilder, attributes);
    820         } else if (tag.equalsIgnoreCase("u")) {
    821             start(mSpannableStringBuilder, new Underline());
    822         } else if (tag.equalsIgnoreCase("del")) {
    823             start(mSpannableStringBuilder, new Strikethrough());
    824         } else if (tag.equalsIgnoreCase("s")) {
    825             start(mSpannableStringBuilder, new Strikethrough());
    826         } else if (tag.equalsIgnoreCase("strike")) {
    827             start(mSpannableStringBuilder, new Strikethrough());
    828         } else if (tag.equalsIgnoreCase("sup")) {
    829             start(mSpannableStringBuilder, new Super());
    830         } else if (tag.equalsIgnoreCase("sub")) {
    831             start(mSpannableStringBuilder, new Sub());
    832         } else if (tag.length() == 2 &&
    833                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
    834                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
    835             startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1');
    836         } else if (tag.equalsIgnoreCase("img")) {
    837             startImg(mSpannableStringBuilder, attributes, mImageGetter);
    838         } else if (mTagHandler != null) {
    839             mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
    840         }
    841     }
    842 
    843     private void handleEndTag(String tag) {
    844         if (tag.equalsIgnoreCase("br")) {
    845             handleBr(mSpannableStringBuilder);
    846         } else if (tag.equalsIgnoreCase("p")) {
    847             endCssStyle(mSpannableStringBuilder);
    848             endBlockElement(mSpannableStringBuilder);
    849         } else if (tag.equalsIgnoreCase("ul")) {
    850             endBlockElement(mSpannableStringBuilder);
    851         } else if (tag.equalsIgnoreCase("li")) {
    852             endLi(mSpannableStringBuilder);
    853         } else if (tag.equalsIgnoreCase("div")) {
    854             endBlockElement(mSpannableStringBuilder);
    855         } else if (tag.equalsIgnoreCase("span")) {
    856             endCssStyle(mSpannableStringBuilder);
    857         } else if (tag.equalsIgnoreCase("strong")) {
    858             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
    859         } else if (tag.equalsIgnoreCase("b")) {
    860             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
    861         } else if (tag.equalsIgnoreCase("em")) {
    862             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    863         } else if (tag.equalsIgnoreCase("cite")) {
    864             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    865         } else if (tag.equalsIgnoreCase("dfn")) {
    866             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    867         } else if (tag.equalsIgnoreCase("i")) {
    868             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
    869         } else if (tag.equalsIgnoreCase("big")) {
    870             end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
    871         } else if (tag.equalsIgnoreCase("small")) {
    872             end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
    873         } else if (tag.equalsIgnoreCase("font")) {
    874             endFont(mSpannableStringBuilder);
    875         } else if (tag.equalsIgnoreCase("blockquote")) {
    876             endBlockquote(mSpannableStringBuilder);
    877         } else if (tag.equalsIgnoreCase("tt")) {
    878             end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace"));
    879         } else if (tag.equalsIgnoreCase("a")) {
    880             endA(mSpannableStringBuilder);
    881         } else if (tag.equalsIgnoreCase("u")) {
    882             end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
    883         } else if (tag.equalsIgnoreCase("del")) {
    884             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
    885         } else if (tag.equalsIgnoreCase("s")) {
    886             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
    887         } else if (tag.equalsIgnoreCase("strike")) {
    888             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
    889         } else if (tag.equalsIgnoreCase("sup")) {
    890             end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
    891         } else if (tag.equalsIgnoreCase("sub")) {
    892             end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
    893         } else if (tag.length() == 2 &&
    894                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
    895                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
    896             endHeading(mSpannableStringBuilder);
    897         } else if (mTagHandler != null) {
    898             mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
    899         }
    900     }
    901 
    902     private int getMarginParagraph() {
    903         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH);
    904     }
    905 
    906     private int getMarginHeading() {
    907         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
    908     }
    909 
    910     private int getMarginListItem() {
    911         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM);
    912     }
    913 
    914     private int getMarginList() {
    915         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST);
    916     }
    917 
    918     private int getMarginDiv() {
    919         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV);
    920     }
    921 
    922     private int getMarginBlockquote() {
    923         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE);
    924     }
    925 
    926     /**
    927      * Returns the minimum number of newline characters needed before and after a given block-level
    928      * element.
    929      *
    930      * @param flag the corresponding option flag defined in {@link Html} of a block-level element
    931      */
    932     private int getMargin(int flag) {
    933         if ((flag & mFlags) != 0) {
    934             return 1;
    935         }
    936         return 2;
    937     }
    938 
    939     private static void appendNewlines(Editable text, int minNewline) {
    940         final int len = text.length();
    941 
    942         if (len == 0) {
    943             return;
    944         }
    945 
    946         int existingNewlines = 0;
    947         for (int i = len - 1; i >= 0 && text.charAt(i) == '\n'; i--) {
    948             existingNewlines++;
    949         }
    950 
    951         for (int j = existingNewlines; j < minNewline; j++) {
    952             text.append("\n");
    953         }
    954     }
    955 
    956     private static void startBlockElement(Editable text, Attributes attributes, int margin) {
    957         final int len = text.length();
    958         if (margin > 0) {
    959             appendNewlines(text, margin);
    960             start(text, new Newline(margin));
    961         }
    962 
    963         String style = attributes.getValue("", "style");
    964         if (style != null) {
    965             Matcher m = getTextAlignPattern().matcher(style);
    966             if (m.find()) {
    967                 String alignment = m.group(1);
    968                 if (alignment.equalsIgnoreCase("start")) {
    969                     start(text, new Alignment(Layout.Alignment.ALIGN_NORMAL));
    970                 } else if (alignment.equalsIgnoreCase("center")) {
    971                     start(text, new Alignment(Layout.Alignment.ALIGN_CENTER));
    972                 } else if (alignment.equalsIgnoreCase("end")) {
    973                     start(text, new Alignment(Layout.Alignment.ALIGN_OPPOSITE));
    974                 }
    975             }
    976         }
    977     }
    978 
    979     private static void endBlockElement(Editable text) {
    980         Newline n = getLast(text, Newline.class);
    981         if (n != null) {
    982             appendNewlines(text, n.mNumNewlines);
    983             text.removeSpan(n);
    984         }
    985 
    986         Alignment a = getLast(text, Alignment.class);
    987         if (a != null) {
    988             setSpanFromMark(text, a, new AlignmentSpan.Standard(a.mAlignment));
    989         }
    990     }
    991 
    992     private static void handleBr(Editable text) {
    993         text.append('\n');
    994     }
    995 
    996     private void startLi(Editable text, Attributes attributes) {
    997         startBlockElement(text, attributes, getMarginListItem());
    998         start(text, new Bullet());
    999         startCssStyle(text, attributes);
   1000     }
   1001 
   1002     private static void endLi(Editable text) {
   1003         endCssStyle(text);
   1004         endBlockElement(text);
   1005         end(text, Bullet.class, new BulletSpan());
   1006     }
   1007 
   1008     private void startBlockquote(Editable text, Attributes attributes) {
   1009         startBlockElement(text, attributes, getMarginBlockquote());
   1010         start(text, new Blockquote());
   1011     }
   1012 
   1013     private static void endBlockquote(Editable text) {
   1014         endBlockElement(text);
   1015         end(text, Blockquote.class, new QuoteSpan());
   1016     }
   1017 
   1018     private void startHeading(Editable text, Attributes attributes, int level) {
   1019         startBlockElement(text, attributes, getMarginHeading());
   1020         start(text, new Heading(level));
   1021     }
   1022 
   1023     private static void endHeading(Editable text) {
   1024         // RelativeSizeSpan and StyleSpan are CharacterStyles
   1025         // Their ranges should not include the newlines at the end
   1026         Heading h = getLast(text, Heading.class);
   1027         if (h != null) {
   1028             setSpanFromMark(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]),
   1029                     new StyleSpan(Typeface.BOLD));
   1030         }
   1031 
   1032         endBlockElement(text);
   1033     }
   1034 
   1035     private static <T> T getLast(Spanned text, Class<T> kind) {
   1036         /*
   1037          * This knows that the last returned object from getSpans()
   1038          * will be the most recently added.
   1039          */
   1040         T[] objs = text.getSpans(0, text.length(), kind);
   1041 
   1042         if (objs.length == 0) {
   1043             return null;
   1044         } else {
   1045             return objs[objs.length - 1];
   1046         }
   1047     }
   1048 
   1049     private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {
   1050         int where = text.getSpanStart(mark);
   1051         text.removeSpan(mark);
   1052         int len = text.length();
   1053         if (where != len) {
   1054             for (Object span : spans) {
   1055                 text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   1056             }
   1057         }
   1058     }
   1059 
   1060     private static void start(Editable text, Object mark) {
   1061         int len = text.length();
   1062         text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
   1063     }
   1064 
   1065     private static void end(Editable text, Class kind, Object repl) {
   1066         int len = text.length();
   1067         Object obj = getLast(text, kind);
   1068         if (obj != null) {
   1069             setSpanFromMark(text, obj, repl);
   1070         }
   1071     }
   1072 
   1073     private void startCssStyle(Editable text, Attributes attributes) {
   1074         String style = attributes.getValue("", "style");
   1075         if (style != null) {
   1076             Matcher m = getForegroundColorPattern().matcher(style);
   1077             if (m.find()) {
   1078                 int c = getHtmlColor(m.group(1));
   1079                 if (c != -1) {
   1080                     start(text, new Foreground(c | 0xFF000000));
   1081                 }
   1082             }
   1083 
   1084             m = getBackgroundColorPattern().matcher(style);
   1085             if (m.find()) {
   1086                 int c = getHtmlColor(m.group(1));
   1087                 if (c != -1) {
   1088                     start(text, new Background(c | 0xFF000000));
   1089                 }
   1090             }
   1091 
   1092             m = getTextDecorationPattern().matcher(style);
   1093             if (m.find()) {
   1094                 String textDecoration = m.group(1);
   1095                 if (textDecoration.equalsIgnoreCase("line-through")) {
   1096                     start(text, new Strikethrough());
   1097                 }
   1098             }
   1099         }
   1100     }
   1101 
   1102     private static void endCssStyle(Editable text) {
   1103         Strikethrough s = getLast(text, Strikethrough.class);
   1104         if (s != null) {
   1105             setSpanFromMark(text, s, new StrikethroughSpan());
   1106         }
   1107 
   1108         Background b = getLast(text, Background.class);
   1109         if (b != null) {
   1110             setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor));
   1111         }
   1112 
   1113         Foreground f = getLast(text, Foreground.class);
   1114         if (f != null) {
   1115             setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));
   1116         }
   1117     }
   1118 
   1119     private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) {
   1120         String src = attributes.getValue("", "src");
   1121         Drawable d = null;
   1122 
   1123         if (img != null) {
   1124             d = img.getDrawable(src);
   1125         }
   1126 
   1127         if (d == null) {
   1128             d = Resources.getSystem().
   1129                     getDrawable(com.android.internal.R.drawable.unknown_image);
   1130             d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
   1131         }
   1132 
   1133         int len = text.length();
   1134         text.append("\uFFFC");
   1135 
   1136         text.setSpan(new ImageSpan(d, src), len, text.length(),
   1137                      Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1138     }
   1139 
   1140     private void startFont(Editable text, Attributes attributes) {
   1141         String color = attributes.getValue("", "color");
   1142         String face = attributes.getValue("", "face");
   1143 
   1144         if (!TextUtils.isEmpty(color)) {
   1145             int c = getHtmlColor(color);
   1146             if (c != -1) {
   1147                 start(text, new Foreground(c | 0xFF000000));
   1148             }
   1149         }
   1150 
   1151         if (!TextUtils.isEmpty(face)) {
   1152             start(text, new Font(face));
   1153         }
   1154     }
   1155 
   1156     private static void endFont(Editable text) {
   1157         Font font = getLast(text, Font.class);
   1158         if (font != null) {
   1159             setSpanFromMark(text, font, new TypefaceSpan(font.mFace));
   1160         }
   1161 
   1162         Foreground foreground = getLast(text, Foreground.class);
   1163         if (foreground != null) {
   1164             setSpanFromMark(text, foreground,
   1165                     new ForegroundColorSpan(foreground.mForegroundColor));
   1166         }
   1167     }
   1168 
   1169     private static void startA(Editable text, Attributes attributes) {
   1170         String href = attributes.getValue("", "href");
   1171         start(text, new Href(href));
   1172     }
   1173 
   1174     private static void endA(Editable text) {
   1175         Href h = getLast(text, Href.class);
   1176         if (h != null) {
   1177             if (h.mHref != null) {
   1178                 setSpanFromMark(text, h, new URLSpan((h.mHref)));
   1179             }
   1180         }
   1181     }
   1182 
   1183     private int getHtmlColor(String color) {
   1184         if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS)
   1185                 == Html.FROM_HTML_OPTION_USE_CSS_COLORS) {
   1186             Integer i = sColorMap.get(color.toLowerCase(Locale.US));
   1187             if (i != null) {
   1188                 return i;
   1189             }
   1190         }
   1191         return Color.getHtmlColor(color);
   1192     }
   1193 
   1194     public void setDocumentLocator(Locator locator) {
   1195     }
   1196 
   1197     public void startDocument() throws SAXException {
   1198     }
   1199 
   1200     public void endDocument() throws SAXException {
   1201     }
   1202 
   1203     public void startPrefixMapping(String prefix, String uri) throws SAXException {
   1204     }
   1205 
   1206     public void endPrefixMapping(String prefix) throws SAXException {
   1207     }
   1208 
   1209     public void startElement(String uri, String localName, String qName, Attributes attributes)
   1210             throws SAXException {
   1211         handleStartTag(localName, attributes);
   1212     }
   1213 
   1214     public void endElement(String uri, String localName, String qName) throws SAXException {
   1215         handleEndTag(localName);
   1216     }
   1217 
   1218     public void characters(char ch[], int start, int length) throws SAXException {
   1219         StringBuilder sb = new StringBuilder();
   1220 
   1221         /*
   1222          * Ignore whitespace that immediately follows other whitespace;
   1223          * newlines count as spaces.
   1224          */
   1225 
   1226         for (int i = 0; i < length; i++) {
   1227             char c = ch[i + start];
   1228 
   1229             if (c == ' ' || c == '\n') {
   1230                 char pred;
   1231                 int len = sb.length();
   1232 
   1233                 if (len == 0) {
   1234                     len = mSpannableStringBuilder.length();
   1235 
   1236                     if (len == 0) {
   1237                         pred = '\n';
   1238                     } else {
   1239                         pred = mSpannableStringBuilder.charAt(len - 1);
   1240                     }
   1241                 } else {
   1242                     pred = sb.charAt(len - 1);
   1243                 }
   1244 
   1245                 if (pred != ' ' && pred != '\n') {
   1246                     sb.append(' ');
   1247                 }
   1248             } else {
   1249                 sb.append(c);
   1250             }
   1251         }
   1252 
   1253         mSpannableStringBuilder.append(sb);
   1254     }
   1255 
   1256     public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
   1257     }
   1258 
   1259     public void processingInstruction(String target, String data) throws SAXException {
   1260     }
   1261 
   1262     public void skippedEntity(String name) throws SAXException {
   1263     }
   1264 
   1265     private static class Bold { }
   1266     private static class Italic { }
   1267     private static class Underline { }
   1268     private static class Strikethrough { }
   1269     private static class Big { }
   1270     private static class Small { }
   1271     private static class Monospace { }
   1272     private static class Blockquote { }
   1273     private static class Super { }
   1274     private static class Sub { }
   1275     private static class Bullet { }
   1276 
   1277     private static class Font {
   1278         public String mFace;
   1279 
   1280         public Font(String face) {
   1281             mFace = face;
   1282         }
   1283     }
   1284 
   1285     private static class Href {
   1286         public String mHref;
   1287 
   1288         public Href(String href) {
   1289             mHref = href;
   1290         }
   1291     }
   1292 
   1293     private static class Foreground {
   1294         private int mForegroundColor;
   1295 
   1296         public Foreground(int foregroundColor) {
   1297             mForegroundColor = foregroundColor;
   1298         }
   1299     }
   1300 
   1301     private static class Background {
   1302         private int mBackgroundColor;
   1303 
   1304         public Background(int backgroundColor) {
   1305             mBackgroundColor = backgroundColor;
   1306         }
   1307     }
   1308 
   1309     private static class Heading {
   1310         private int mLevel;
   1311 
   1312         public Heading(int level) {
   1313             mLevel = level;
   1314         }
   1315     }
   1316 
   1317     private static class Newline {
   1318         private int mNumNewlines;
   1319 
   1320         public Newline(int numNewlines) {
   1321             mNumNewlines = numNewlines;
   1322         }
   1323     }
   1324 
   1325     private static class Alignment {
   1326         private Layout.Alignment mAlignment;
   1327 
   1328         public Alignment(Layout.Alignment alignment) {
   1329             mAlignment = alignment;
   1330         }
   1331     }
   1332 }
   1333