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