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