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