Html.java revision a8f6d5f0720f400b6f59b0809aaefea83c5f51d4
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 android.graphics.Color;
20import com.android.internal.util.ArrayUtils;
21import org.ccil.cowan.tagsoup.HTMLSchema;
22import org.ccil.cowan.tagsoup.Parser;
23import org.xml.sax.Attributes;
24import org.xml.sax.ContentHandler;
25import org.xml.sax.InputSource;
26import org.xml.sax.Locator;
27import org.xml.sax.SAXException;
28import org.xml.sax.XMLReader;
29
30import android.content.res.ColorStateList;
31import android.content.res.Resources;
32import android.graphics.Typeface;
33import android.graphics.drawable.Drawable;
34import android.text.style.AbsoluteSizeSpan;
35import android.text.style.AlignmentSpan;
36import android.text.style.CharacterStyle;
37import android.text.style.ForegroundColorSpan;
38import android.text.style.ImageSpan;
39import android.text.style.ParagraphStyle;
40import android.text.style.QuoteSpan;
41import android.text.style.RelativeSizeSpan;
42import android.text.style.StrikethroughSpan;
43import android.text.style.StyleSpan;
44import android.text.style.SubscriptSpan;
45import android.text.style.SuperscriptSpan;
46import android.text.style.TextAppearanceSpan;
47import android.text.style.TypefaceSpan;
48import android.text.style.URLSpan;
49import android.text.style.UnderlineSpan;
50
51import com.android.internal.util.XmlUtils;
52
53import java.io.IOException;
54import java.io.StringReader;
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    /**
152     * Returns an HTML escaped representation of the given plain text.
153     */
154    public static String escapeHtml(CharSequence text) {
155        StringBuilder out = new StringBuilder();
156        withinStyle(out, text, 0, text.length());
157        return out.toString();
158    }
159
160    private static void withinHtml(StringBuilder out, Spanned text) {
161        int len = text.length();
162
163        int next;
164        for (int i = 0; i < text.length(); i = next) {
165            next = text.nextSpanTransition(i, len, ParagraphStyle.class);
166            ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
167            String elements = " ";
168            boolean needDiv = false;
169
170            for(int j = 0; j < style.length; j++) {
171                if (style[j] instanceof AlignmentSpan) {
172                    Layout.Alignment align =
173                        ((AlignmentSpan) style[j]).getAlignment();
174                    needDiv = true;
175                    if (align == Layout.Alignment.ALIGN_CENTER) {
176                        elements = "align=\"center\" " + elements;
177                    } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
178                        elements = "align=\"right\" " + elements;
179                    } else {
180                        elements = "align=\"left\" " + elements;
181                    }
182                }
183            }
184            if (needDiv) {
185                out.append("<div ").append(elements).append(">");
186            }
187
188            withinDiv(out, text, i, next);
189
190            if (needDiv) {
191                out.append("</div>");
192            }
193        }
194    }
195
196    private static void withinDiv(StringBuilder out, Spanned text,
197            int start, int end) {
198        int next;
199        for (int i = start; i < end; i = next) {
200            next = text.nextSpanTransition(i, end, QuoteSpan.class);
201            QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
202
203            for (QuoteSpan quote : quotes) {
204                out.append("<blockquote>");
205            }
206
207            withinBlockquote(out, text, i, next);
208
209            for (QuoteSpan quote : quotes) {
210                out.append("</blockquote>\n");
211            }
212        }
213    }
214
215    private static String getOpenParaTagWithDirection(Spanned text, int start, int end) {
216        final int len = end - start;
217        final byte[] levels = new byte[ArrayUtils.idealByteArraySize(len)];
218        final char[] buffer = TextUtils.obtain(len);
219        TextUtils.getChars(text, start, end, buffer, 0);
220
221        int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len,
222                false /* no info */);
223        switch(paraDir) {
224            case Layout.DIR_RIGHT_TO_LEFT:
225                return "<p dir=\"rtl\">";
226            case Layout.DIR_LEFT_TO_RIGHT:
227            default:
228                return "<p dir=\"ltr\">";
229        }
230    }
231
232    private static void withinBlockquote(StringBuilder out, Spanned text,
233                                         int start, int end) {
234        out.append(getOpenParaTagWithDirection(text, start, end));
235
236        int next;
237        for (int i = start; i < end; i = next) {
238            next = TextUtils.indexOf(text, '\n', i, end);
239            if (next < 0) {
240                next = end;
241            }
242
243            int nl = 0;
244
245            while (next < end && text.charAt(next) == '\n') {
246                nl++;
247                next++;
248            }
249
250            withinParagraph(out, text, i, next - nl, nl, next == end);
251        }
252
253        out.append("</p>\n");
254    }
255
256    private static void withinParagraph(StringBuilder out, Spanned text,
257                                        int start, int end, int nl,
258                                        boolean last) {
259        int next;
260        for (int i = start; i < end; i = next) {
261            next = text.nextSpanTransition(i, end, CharacterStyle.class);
262            CharacterStyle[] style = text.getSpans(i, next,
263                                                   CharacterStyle.class);
264
265            for (int j = 0; j < style.length; j++) {
266                if (style[j] instanceof StyleSpan) {
267                    int s = ((StyleSpan) style[j]).getStyle();
268
269                    if ((s & Typeface.BOLD) != 0) {
270                        out.append("<b>");
271                    }
272                    if ((s & Typeface.ITALIC) != 0) {
273                        out.append("<i>");
274                    }
275                }
276                if (style[j] instanceof TypefaceSpan) {
277                    String s = ((TypefaceSpan) style[j]).getFamily();
278
279                    if (s.equals("monospace")) {
280                        out.append("<tt>");
281                    }
282                }
283                if (style[j] instanceof SuperscriptSpan) {
284                    out.append("<sup>");
285                }
286                if (style[j] instanceof SubscriptSpan) {
287                    out.append("<sub>");
288                }
289                if (style[j] instanceof UnderlineSpan) {
290                    out.append("<u>");
291                }
292                if (style[j] instanceof StrikethroughSpan) {
293                    out.append("<strike>");
294                }
295                if (style[j] instanceof URLSpan) {
296                    out.append("<a href=\"");
297                    out.append(((URLSpan) style[j]).getURL());
298                    out.append("\">");
299                }
300                if (style[j] instanceof ImageSpan) {
301                    out.append("<img src=\"");
302                    out.append(((ImageSpan) style[j]).getSource());
303                    out.append("\">");
304
305                    // Don't output the dummy character underlying the image.
306                    i = next;
307                }
308                if (style[j] instanceof AbsoluteSizeSpan) {
309                    out.append("<font size =\"");
310                    out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
311                    out.append("\">");
312                }
313                if (style[j] instanceof ForegroundColorSpan) {
314                    out.append("<font color =\"#");
315                    String color = Integer.toHexString(((ForegroundColorSpan)
316                            style[j]).getForegroundColor() + 0x01000000);
317                    while (color.length() < 6) {
318                        color = "0" + color;
319                    }
320                    out.append(color);
321                    out.append("\">");
322                }
323            }
324
325            withinStyle(out, text, i, next);
326
327            for (int j = style.length - 1; j >= 0; j--) {
328                if (style[j] instanceof ForegroundColorSpan) {
329                    out.append("</font>");
330                }
331                if (style[j] instanceof AbsoluteSizeSpan) {
332                    out.append("</font>");
333                }
334                if (style[j] instanceof URLSpan) {
335                    out.append("</a>");
336                }
337                if (style[j] instanceof StrikethroughSpan) {
338                    out.append("</strike>");
339                }
340                if (style[j] instanceof UnderlineSpan) {
341                    out.append("</u>");
342                }
343                if (style[j] instanceof SubscriptSpan) {
344                    out.append("</sub>");
345                }
346                if (style[j] instanceof SuperscriptSpan) {
347                    out.append("</sup>");
348                }
349                if (style[j] instanceof TypefaceSpan) {
350                    String s = ((TypefaceSpan) style[j]).getFamily();
351
352                    if (s.equals("monospace")) {
353                        out.append("</tt>");
354                    }
355                }
356                if (style[j] instanceof StyleSpan) {
357                    int s = ((StyleSpan) style[j]).getStyle();
358
359                    if ((s & Typeface.BOLD) != 0) {
360                        out.append("</b>");
361                    }
362                    if ((s & Typeface.ITALIC) != 0) {
363                        out.append("</i>");
364                    }
365                }
366            }
367        }
368
369        String p = last ? "" : "</p>\n" + getOpenParaTagWithDirection(text, start, end);
370
371        if (nl == 1) {
372            out.append("<br>\n");
373        } else if (nl == 2) {
374            out.append(p);
375        } else {
376            for (int i = 2; i < nl; i++) {
377                out.append("<br>");
378            }
379            out.append(p);
380        }
381    }
382
383    private static void withinStyle(StringBuilder out, CharSequence text,
384                                    int start, int end) {
385        for (int i = start; i < end; i++) {
386            char c = text.charAt(i);
387
388            if (c == '<') {
389                out.append("&lt;");
390            } else if (c == '>') {
391                out.append("&gt;");
392            } else if (c == '&') {
393                out.append("&amp;");
394            } else if (c > 0x7E || c < ' ') {
395                out.append("&#").append((int) c).append(";");
396            } else if (c == ' ') {
397                while (i + 1 < end && text.charAt(i + 1) == ' ') {
398                    out.append("&nbsp;");
399                    i++;
400                }
401
402                out.append(' ');
403            } else {
404                out.append(c);
405            }
406        }
407    }
408}
409
410class HtmlToSpannedConverter implements ContentHandler {
411
412    private static final float[] HEADER_SIZES = {
413        1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
414    };
415
416    private String mSource;
417    private XMLReader mReader;
418    private SpannableStringBuilder mSpannableStringBuilder;
419    private Html.ImageGetter mImageGetter;
420    private Html.TagHandler mTagHandler;
421
422    public HtmlToSpannedConverter(
423            String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
424            Parser parser) {
425        mSource = source;
426        mSpannableStringBuilder = new SpannableStringBuilder();
427        mImageGetter = imageGetter;
428        mTagHandler = tagHandler;
429        mReader = parser;
430    }
431
432    public Spanned convert() {
433
434        mReader.setContentHandler(this);
435        try {
436            mReader.parse(new InputSource(new StringReader(mSource)));
437        } catch (IOException e) {
438            // We are reading from a string. There should not be IO problems.
439            throw new RuntimeException(e);
440        } catch (SAXException e) {
441            // TagSoup doesn't throw parse exceptions.
442            throw new RuntimeException(e);
443        }
444
445        // Fix flags and range for paragraph-type markup.
446        Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
447        for (int i = 0; i < obj.length; i++) {
448            int start = mSpannableStringBuilder.getSpanStart(obj[i]);
449            int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
450
451            // If the last line of the range is blank, back off by one.
452            if (end - 2 >= 0) {
453                if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
454                    mSpannableStringBuilder.charAt(end - 2) == '\n') {
455                    end--;
456                }
457            }
458
459            if (end == start) {
460                mSpannableStringBuilder.removeSpan(obj[i]);
461            } else {
462                mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
463            }
464        }
465
466        return mSpannableStringBuilder;
467    }
468
469    private void handleStartTag(String tag, Attributes attributes) {
470        if (tag.equalsIgnoreCase("br")) {
471            // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
472            // so we can safely emite the linebreaks when we handle the close tag.
473        } else if (tag.equalsIgnoreCase("p")) {
474            handleP(mSpannableStringBuilder);
475        } else if (tag.equalsIgnoreCase("div")) {
476            handleP(mSpannableStringBuilder);
477        } else if (tag.equalsIgnoreCase("strong")) {
478            start(mSpannableStringBuilder, new Bold());
479        } else if (tag.equalsIgnoreCase("b")) {
480            start(mSpannableStringBuilder, new Bold());
481        } else if (tag.equalsIgnoreCase("em")) {
482            start(mSpannableStringBuilder, new Italic());
483        } else if (tag.equalsIgnoreCase("cite")) {
484            start(mSpannableStringBuilder, new Italic());
485        } else if (tag.equalsIgnoreCase("dfn")) {
486            start(mSpannableStringBuilder, new Italic());
487        } else if (tag.equalsIgnoreCase("i")) {
488            start(mSpannableStringBuilder, new Italic());
489        } else if (tag.equalsIgnoreCase("big")) {
490            start(mSpannableStringBuilder, new Big());
491        } else if (tag.equalsIgnoreCase("small")) {
492            start(mSpannableStringBuilder, new Small());
493        } else if (tag.equalsIgnoreCase("font")) {
494            startFont(mSpannableStringBuilder, attributes);
495        } else if (tag.equalsIgnoreCase("blockquote")) {
496            handleP(mSpannableStringBuilder);
497            start(mSpannableStringBuilder, new Blockquote());
498        } else if (tag.equalsIgnoreCase("tt")) {
499            start(mSpannableStringBuilder, new Monospace());
500        } else if (tag.equalsIgnoreCase("a")) {
501            startA(mSpannableStringBuilder, attributes);
502        } else if (tag.equalsIgnoreCase("u")) {
503            start(mSpannableStringBuilder, new Underline());
504        } else if (tag.equalsIgnoreCase("sup")) {
505            start(mSpannableStringBuilder, new Super());
506        } else if (tag.equalsIgnoreCase("sub")) {
507            start(mSpannableStringBuilder, new Sub());
508        } else if (tag.length() == 2 &&
509                   Character.toLowerCase(tag.charAt(0)) == 'h' &&
510                   tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
511            handleP(mSpannableStringBuilder);
512            start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
513        } else if (tag.equalsIgnoreCase("img")) {
514            startImg(mSpannableStringBuilder, attributes, mImageGetter);
515        } else if (mTagHandler != null) {
516            mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
517        }
518    }
519
520    private void handleEndTag(String tag) {
521        if (tag.equalsIgnoreCase("br")) {
522            handleBr(mSpannableStringBuilder);
523        } else if (tag.equalsIgnoreCase("p")) {
524            handleP(mSpannableStringBuilder);
525        } else if (tag.equalsIgnoreCase("div")) {
526            handleP(mSpannableStringBuilder);
527        } else if (tag.equalsIgnoreCase("strong")) {
528            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
529        } else if (tag.equalsIgnoreCase("b")) {
530            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
531        } else if (tag.equalsIgnoreCase("em")) {
532            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
533        } else if (tag.equalsIgnoreCase("cite")) {
534            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
535        } else if (tag.equalsIgnoreCase("dfn")) {
536            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
537        } else if (tag.equalsIgnoreCase("i")) {
538            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
539        } else if (tag.equalsIgnoreCase("big")) {
540            end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
541        } else if (tag.equalsIgnoreCase("small")) {
542            end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
543        } else if (tag.equalsIgnoreCase("font")) {
544            endFont(mSpannableStringBuilder);
545        } else if (tag.equalsIgnoreCase("blockquote")) {
546            handleP(mSpannableStringBuilder);
547            end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
548        } else if (tag.equalsIgnoreCase("tt")) {
549            end(mSpannableStringBuilder, Monospace.class,
550                    new TypefaceSpan("monospace"));
551        } else if (tag.equalsIgnoreCase("a")) {
552            endA(mSpannableStringBuilder);
553        } else if (tag.equalsIgnoreCase("u")) {
554            end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
555        } else if (tag.equalsIgnoreCase("sup")) {
556            end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
557        } else if (tag.equalsIgnoreCase("sub")) {
558            end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
559        } else if (tag.length() == 2 &&
560                Character.toLowerCase(tag.charAt(0)) == 'h' &&
561                tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
562            handleP(mSpannableStringBuilder);
563            endHeader(mSpannableStringBuilder);
564        } else if (mTagHandler != null) {
565            mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
566        }
567    }
568
569    private static void handleP(SpannableStringBuilder text) {
570        int len = text.length();
571
572        if (len >= 1 && text.charAt(len - 1) == '\n') {
573            if (len >= 2 && text.charAt(len - 2) == '\n') {
574                return;
575            }
576
577            text.append("\n");
578            return;
579        }
580
581        if (len != 0) {
582            text.append("\n\n");
583        }
584    }
585
586    private static void handleBr(SpannableStringBuilder text) {
587        text.append("\n");
588    }
589
590    private static Object getLast(Spanned text, Class kind) {
591        /*
592         * This knows that the last returned object from getSpans()
593         * will be the most recently added.
594         */
595        Object[] objs = text.getSpans(0, text.length(), kind);
596
597        if (objs.length == 0) {
598            return null;
599        } else {
600            return objs[objs.length - 1];
601        }
602    }
603
604    private static void start(SpannableStringBuilder text, Object mark) {
605        int len = text.length();
606        text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
607    }
608
609    private static void end(SpannableStringBuilder text, Class kind,
610                            Object repl) {
611        int len = text.length();
612        Object obj = getLast(text, kind);
613        int where = text.getSpanStart(obj);
614
615        text.removeSpan(obj);
616
617        if (where != len) {
618            text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
619        }
620    }
621
622    private static void startImg(SpannableStringBuilder text,
623                                 Attributes attributes, Html.ImageGetter img) {
624        String src = attributes.getValue("", "src");
625        Drawable d = null;
626
627        if (img != null) {
628            d = img.getDrawable(src);
629        }
630
631        if (d == null) {
632            d = Resources.getSystem().
633                    getDrawable(com.android.internal.R.drawable.unknown_image);
634            d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
635        }
636
637        int len = text.length();
638        text.append("\uFFFC");
639
640        text.setSpan(new ImageSpan(d, src), len, text.length(),
641                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
642    }
643
644    private static void startFont(SpannableStringBuilder text,
645                                  Attributes attributes) {
646        String color = attributes.getValue("", "color");
647        String face = attributes.getValue("", "face");
648
649        int len = text.length();
650        text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
651    }
652
653    private static void endFont(SpannableStringBuilder text) {
654        int len = text.length();
655        Object obj = getLast(text, Font.class);
656        int where = text.getSpanStart(obj);
657
658        text.removeSpan(obj);
659
660        if (where != len) {
661            Font f = (Font) obj;
662
663            if (!TextUtils.isEmpty(f.mColor)) {
664                if (f.mColor.startsWith("@")) {
665                    Resources res = Resources.getSystem();
666                    String name = f.mColor.substring(1);
667                    int colorRes = res.getIdentifier(name, "color", "android");
668                    if (colorRes != 0) {
669                        ColorStateList colors = res.getColorStateList(colorRes);
670                        text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
671                                where, len,
672                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
673                    }
674                } else {
675                    int c = Color.getHtmlColor(f.mColor);
676                    if (c != -1) {
677                        text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
678                                where, len,
679                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
680                    }
681                }
682            }
683
684            if (f.mFace != null) {
685                text.setSpan(new TypefaceSpan(f.mFace), where, len,
686                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
687            }
688        }
689    }
690
691    private static void startA(SpannableStringBuilder text, Attributes attributes) {
692        String href = attributes.getValue("", "href");
693
694        int len = text.length();
695        text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
696    }
697
698    private static void endA(SpannableStringBuilder text) {
699        int len = text.length();
700        Object obj = getLast(text, Href.class);
701        int where = text.getSpanStart(obj);
702
703        text.removeSpan(obj);
704
705        if (where != len) {
706            Href h = (Href) obj;
707
708            if (h.mHref != null) {
709                text.setSpan(new URLSpan(h.mHref), where, len,
710                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
711            }
712        }
713    }
714
715    private static void endHeader(SpannableStringBuilder text) {
716        int len = text.length();
717        Object obj = getLast(text, Header.class);
718
719        int where = text.getSpanStart(obj);
720
721        text.removeSpan(obj);
722
723        // Back off not to change only the text, not the blank line.
724        while (len > where && text.charAt(len - 1) == '\n') {
725            len--;
726        }
727
728        if (where != len) {
729            Header h = (Header) obj;
730
731            text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
732                         where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
733            text.setSpan(new StyleSpan(Typeface.BOLD),
734                         where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
735        }
736    }
737
738    public void setDocumentLocator(Locator locator) {
739    }
740
741    public void startDocument() throws SAXException {
742    }
743
744    public void endDocument() throws SAXException {
745    }
746
747    public void startPrefixMapping(String prefix, String uri) throws SAXException {
748    }
749
750    public void endPrefixMapping(String prefix) throws SAXException {
751    }
752
753    public void startElement(String uri, String localName, String qName, Attributes attributes)
754            throws SAXException {
755        handleStartTag(localName, attributes);
756    }
757
758    public void endElement(String uri, String localName, String qName) throws SAXException {
759        handleEndTag(localName);
760    }
761
762    public void characters(char ch[], int start, int length) throws SAXException {
763        StringBuilder sb = new StringBuilder();
764
765        /*
766         * Ignore whitespace that immediately follows other whitespace;
767         * newlines count as spaces.
768         */
769
770        for (int i = 0; i < length; i++) {
771            char c = ch[i + start];
772
773            if (c == ' ' || c == '\n') {
774                char pred;
775                int len = sb.length();
776
777                if (len == 0) {
778                    len = mSpannableStringBuilder.length();
779
780                    if (len == 0) {
781                        pred = '\n';
782                    } else {
783                        pred = mSpannableStringBuilder.charAt(len - 1);
784                    }
785                } else {
786                    pred = sb.charAt(len - 1);
787                }
788
789                if (pred != ' ' && pred != '\n') {
790                    sb.append(' ');
791                }
792            } else {
793                sb.append(c);
794            }
795        }
796
797        mSpannableStringBuilder.append(sb);
798    }
799
800    public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
801    }
802
803    public void processingInstruction(String target, String data) throws SAXException {
804    }
805
806    public void skippedEntity(String name) throws SAXException {
807    }
808
809    private static class Bold { }
810    private static class Italic { }
811    private static class Underline { }
812    private static class Big { }
813    private static class Small { }
814    private static class Monospace { }
815    private static class Blockquote { }
816    private static class Super { }
817    private static class Sub { }
818
819    private static class Font {
820        public String mColor;
821        public String mFace;
822
823        public Font(String color, String face) {
824            mColor = color;
825            mFace = face;
826        }
827    }
828
829    private static class Href {
830        public String mHref;
831
832        public Href(String href) {
833            mHref = href;
834        }
835    }
836
837    private static class Header {
838        private int mLevel;
839
840        public Header(int level) {
841            mLevel = level;
842        }
843    }
844}
845