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