• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
getDrawable(String source)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          */
handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader)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 
Html()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
fromHtml(String source)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      */
fromHtml(String source, int flags)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
fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler)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      */
fromHtml(String source, int flags, ImageGetter imageGetter, TagHandler tagHandler)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
toHtml(Spanned text)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      */
toHtml(Spanned text, int option)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      */
escapeHtml(CharSequence text)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 
withinHtml(StringBuilder out, Spanned text, int option)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 
encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option)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 
withinDiv(StringBuilder out, Spanned text, int start, int end, int option)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 
getTextDirection(Spanned text, int start, int end)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 
getTextStyles(Spanned text, int start, int end, boolean forceNoVerticalMargin, boolean includeTextAlign)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 
withinBlockquote(StringBuilder out, Spanned text, int start, int end, int option)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 
withinBlockquoteIndividual(StringBuilder out, Spanned text, int start, int end)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 
withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start, int end)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 
withinParagraph(StringBuilder out, Spanned text, int start, int end)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 
withinStyle(StringBuilder out, CharSequence text, int start, int end)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 
getTextAlignPattern()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 
getForegroundColorPattern()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 
getBackgroundColorPattern()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 
getTextDecorationPattern()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 
HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser, int flags)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 
convert()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 
handleStartTag(String tag, Attributes attributes)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 
handleEndTag(String tag)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 
getMarginParagraph()912     private int getMarginParagraph() {
913         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH);
914     }
915 
getMarginHeading()916     private int getMarginHeading() {
917         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
918     }
919 
getMarginListItem()920     private int getMarginListItem() {
921         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM);
922     }
923 
getMarginList()924     private int getMarginList() {
925         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST);
926     }
927 
getMarginDiv()928     private int getMarginDiv() {
929         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV);
930     }
931 
getMarginBlockquote()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      */
getMargin(int flag)942     private int getMargin(int flag) {
943         if ((flag & mFlags) != 0) {
944             return 1;
945         }
946         return 2;
947     }
948 
appendNewlines(Editable text, int minNewline)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 
startBlockElement(Editable text, Attributes attributes, int margin)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 
endBlockElement(Editable text)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 
handleBr(Editable text)1002     private static void handleBr(Editable text) {
1003         text.append('\n');
1004     }
1005 
startLi(Editable text, Attributes attributes)1006     private void startLi(Editable text, Attributes attributes) {
1007         startBlockElement(text, attributes, getMarginListItem());
1008         start(text, new Bullet());
1009         startCssStyle(text, attributes);
1010     }
1011 
endLi(Editable text)1012     private static void endLi(Editable text) {
1013         endCssStyle(text);
1014         endBlockElement(text);
1015         end(text, Bullet.class, new BulletSpan());
1016     }
1017 
startBlockquote(Editable text, Attributes attributes)1018     private void startBlockquote(Editable text, Attributes attributes) {
1019         startBlockElement(text, attributes, getMarginBlockquote());
1020         start(text, new Blockquote());
1021     }
1022 
endBlockquote(Editable text)1023     private static void endBlockquote(Editable text) {
1024         endBlockElement(text);
1025         end(text, Blockquote.class, new QuoteSpan());
1026     }
1027 
startHeading(Editable text, Attributes attributes, int level)1028     private void startHeading(Editable text, Attributes attributes, int level) {
1029         startBlockElement(text, attributes, getMarginHeading());
1030         start(text, new Heading(level));
1031     }
1032 
endHeading(Editable text)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 
getLast(Spanned text, Class<T> kind)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 
setSpanFromMark(Spannable text, Object mark, Object... spans)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 
start(Editable text, Object mark)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 
end(Editable text, Class kind, Object repl)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 
startCssStyle(Editable text, Attributes attributes)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 
endCssStyle(Editable text)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 
startImg(Editable text, Attributes attributes, Html.ImageGetter img)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 
startFont(Editable text, Attributes attributes)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 
endFont(Editable text)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 
startA(Editable text, Attributes attributes)1179     private static void startA(Editable text, Attributes attributes) {
1180         String href = attributes.getValue("", "href");
1181         start(text, new Href(href));
1182     }
1183 
endA(Editable text)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 
getHtmlColor(String color)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 
setDocumentLocator(Locator locator)1204     public void setDocumentLocator(Locator locator) {
1205     }
1206 
startDocument()1207     public void startDocument() throws SAXException {
1208     }
1209 
endDocument()1210     public void endDocument() throws SAXException {
1211     }
1212 
startPrefixMapping(String prefix, String uri)1213     public void startPrefixMapping(String prefix, String uri) throws SAXException {
1214     }
1215 
endPrefixMapping(String prefix)1216     public void endPrefixMapping(String prefix) throws SAXException {
1217     }
1218 
startElement(String uri, String localName, String qName, Attributes attributes)1219     public void startElement(String uri, String localName, String qName, Attributes attributes)
1220             throws SAXException {
1221         handleStartTag(localName, attributes);
1222     }
1223 
endElement(String uri, String localName, String qName)1224     public void endElement(String uri, String localName, String qName) throws SAXException {
1225         handleEndTag(localName);
1226     }
1227 
characters(char ch[], int start, int length)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 
ignorableWhitespace(char ch[], int start, int length)1266     public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
1267     }
1268 
processingInstruction(String target, String data)1269     public void processingInstruction(String target, String data) throws SAXException {
1270     }
1271 
skippedEntity(String name)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 
Font(String face)1290         public Font(String face) {
1291             mFace = face;
1292         }
1293     }
1294 
1295     private static class Href {
1296         public String mHref;
1297 
Href(String href)1298         public Href(String href) {
1299             mHref = href;
1300         }
1301     }
1302 
1303     private static class Foreground {
1304         private int mForegroundColor;
1305 
Foreground(int foregroundColor)1306         public Foreground(int foregroundColor) {
1307             mForegroundColor = foregroundColor;
1308         }
1309     }
1310 
1311     private static class Background {
1312         private int mBackgroundColor;
1313 
Background(int backgroundColor)1314         public Background(int backgroundColor) {
1315             mBackgroundColor = backgroundColor;
1316         }
1317     }
1318 
1319     private static class Heading {
1320         private int mLevel;
1321 
Heading(int level)1322         public Heading(int level) {
1323             mLevel = level;
1324         }
1325     }
1326 
1327     private static class Newline {
1328         private int mNumNewlines;
1329 
Newline(int numNewlines)1330         public Newline(int numNewlines) {
1331             mNumNewlines = numNewlines;
1332         }
1333     }
1334 
1335     private static class Alignment {
1336         private Layout.Alignment mAlignment;
1337 
Alignment(Layout.Alignment alignment)1338         public Alignment(Layout.Alignment alignment) {
1339             mAlignment = alignment;
1340         }
1341     }
1342 }
1343