Html.java revision 3d4764159c78857bbd3305f4fa9c11c70e742d88
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 >= 0xD800 && c <= 0xDFFF) {
395                if (c < 0xDC00 && i + 1 < end) {
396                    char d = text.charAt(i + 1);
397                    if (d >= 0xDC00 && d <= 0xDFFF) {
398                        i++;
399                        int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
400                        out.append("&#").append(codepoint).append(";");
401                    }
402                }
403            } else if (c > 0x7E || c < ' ') {
404                out.append("&#").append((int) c).append(";");
405            } else if (c == ' ') {
406                while (i + 1 < end && text.charAt(i + 1) == ' ') {
407                    out.append("&nbsp;");
408                    i++;
409                }
410
411                out.append(' ');
412            } else {
413                out.append(c);
414            }
415        }
416    }
417}
418
419class HtmlToSpannedConverter implements ContentHandler {
420
421    private static final float[] HEADER_SIZES = {
422        1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
423    };
424
425    private String mSource;
426    private XMLReader mReader;
427    private SpannableStringBuilder mSpannableStringBuilder;
428    private Html.ImageGetter mImageGetter;
429    private Html.TagHandler mTagHandler;
430
431    public HtmlToSpannedConverter(
432            String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
433            Parser parser) {
434        mSource = source;
435        mSpannableStringBuilder = new SpannableStringBuilder();
436        mImageGetter = imageGetter;
437        mTagHandler = tagHandler;
438        mReader = parser;
439    }
440
441    public Spanned convert() {
442
443        mReader.setContentHandler(this);
444        try {
445            mReader.parse(new InputSource(new StringReader(mSource)));
446        } catch (IOException e) {
447            // We are reading from a string. There should not be IO problems.
448            throw new RuntimeException(e);
449        } catch (SAXException e) {
450            // TagSoup doesn't throw parse exceptions.
451            throw new RuntimeException(e);
452        }
453
454        // Fix flags and range for paragraph-type markup.
455        Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
456        for (int i = 0; i < obj.length; i++) {
457            int start = mSpannableStringBuilder.getSpanStart(obj[i]);
458            int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
459
460            // If the last line of the range is blank, back off by one.
461            if (end - 2 >= 0) {
462                if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
463                    mSpannableStringBuilder.charAt(end - 2) == '\n') {
464                    end--;
465                }
466            }
467
468            if (end == start) {
469                mSpannableStringBuilder.removeSpan(obj[i]);
470            } else {
471                mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
472            }
473        }
474
475        return mSpannableStringBuilder;
476    }
477
478    private void handleStartTag(String tag, Attributes attributes) {
479        if (tag.equalsIgnoreCase("br")) {
480            // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
481            // so we can safely emite the linebreaks when we handle the close tag.
482        } else if (tag.equalsIgnoreCase("p")) {
483            handleP(mSpannableStringBuilder);
484        } else if (tag.equalsIgnoreCase("div")) {
485            handleP(mSpannableStringBuilder);
486        } else if (tag.equalsIgnoreCase("strong")) {
487            start(mSpannableStringBuilder, new Bold());
488        } else if (tag.equalsIgnoreCase("b")) {
489            start(mSpannableStringBuilder, new Bold());
490        } else if (tag.equalsIgnoreCase("em")) {
491            start(mSpannableStringBuilder, new Italic());
492        } else if (tag.equalsIgnoreCase("cite")) {
493            start(mSpannableStringBuilder, new Italic());
494        } else if (tag.equalsIgnoreCase("dfn")) {
495            start(mSpannableStringBuilder, new Italic());
496        } else if (tag.equalsIgnoreCase("i")) {
497            start(mSpannableStringBuilder, new Italic());
498        } else if (tag.equalsIgnoreCase("big")) {
499            start(mSpannableStringBuilder, new Big());
500        } else if (tag.equalsIgnoreCase("small")) {
501            start(mSpannableStringBuilder, new Small());
502        } else if (tag.equalsIgnoreCase("font")) {
503            startFont(mSpannableStringBuilder, attributes);
504        } else if (tag.equalsIgnoreCase("blockquote")) {
505            handleP(mSpannableStringBuilder);
506            start(mSpannableStringBuilder, new Blockquote());
507        } else if (tag.equalsIgnoreCase("tt")) {
508            start(mSpannableStringBuilder, new Monospace());
509        } else if (tag.equalsIgnoreCase("a")) {
510            startA(mSpannableStringBuilder, attributes);
511        } else if (tag.equalsIgnoreCase("u")) {
512            start(mSpannableStringBuilder, new Underline());
513        } else if (tag.equalsIgnoreCase("sup")) {
514            start(mSpannableStringBuilder, new Super());
515        } else if (tag.equalsIgnoreCase("sub")) {
516            start(mSpannableStringBuilder, new Sub());
517        } else if (tag.length() == 2 &&
518                   Character.toLowerCase(tag.charAt(0)) == 'h' &&
519                   tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
520            handleP(mSpannableStringBuilder);
521            start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
522        } else if (tag.equalsIgnoreCase("img")) {
523            startImg(mSpannableStringBuilder, attributes, mImageGetter);
524        } else if (mTagHandler != null) {
525            mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
526        }
527    }
528
529    private void handleEndTag(String tag) {
530        if (tag.equalsIgnoreCase("br")) {
531            handleBr(mSpannableStringBuilder);
532        } else if (tag.equalsIgnoreCase("p")) {
533            handleP(mSpannableStringBuilder);
534        } else if (tag.equalsIgnoreCase("div")) {
535            handleP(mSpannableStringBuilder);
536        } else if (tag.equalsIgnoreCase("strong")) {
537            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
538        } else if (tag.equalsIgnoreCase("b")) {
539            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
540        } else if (tag.equalsIgnoreCase("em")) {
541            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
542        } else if (tag.equalsIgnoreCase("cite")) {
543            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
544        } else if (tag.equalsIgnoreCase("dfn")) {
545            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
546        } else if (tag.equalsIgnoreCase("i")) {
547            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
548        } else if (tag.equalsIgnoreCase("big")) {
549            end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
550        } else if (tag.equalsIgnoreCase("small")) {
551            end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
552        } else if (tag.equalsIgnoreCase("font")) {
553            endFont(mSpannableStringBuilder);
554        } else if (tag.equalsIgnoreCase("blockquote")) {
555            handleP(mSpannableStringBuilder);
556            end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
557        } else if (tag.equalsIgnoreCase("tt")) {
558            end(mSpannableStringBuilder, Monospace.class,
559                    new TypefaceSpan("monospace"));
560        } else if (tag.equalsIgnoreCase("a")) {
561            endA(mSpannableStringBuilder);
562        } else if (tag.equalsIgnoreCase("u")) {
563            end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
564        } else if (tag.equalsIgnoreCase("sup")) {
565            end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
566        } else if (tag.equalsIgnoreCase("sub")) {
567            end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
568        } else if (tag.length() == 2 &&
569                Character.toLowerCase(tag.charAt(0)) == 'h' &&
570                tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
571            handleP(mSpannableStringBuilder);
572            endHeader(mSpannableStringBuilder);
573        } else if (mTagHandler != null) {
574            mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
575        }
576    }
577
578    private static void handleP(SpannableStringBuilder text) {
579        int len = text.length();
580
581        if (len >= 1 && text.charAt(len - 1) == '\n') {
582            if (len >= 2 && text.charAt(len - 2) == '\n') {
583                return;
584            }
585
586            text.append("\n");
587            return;
588        }
589
590        if (len != 0) {
591            text.append("\n\n");
592        }
593    }
594
595    private static void handleBr(SpannableStringBuilder text) {
596        text.append("\n");
597    }
598
599    private static Object getLast(Spanned text, Class kind) {
600        /*
601         * This knows that the last returned object from getSpans()
602         * will be the most recently added.
603         */
604        Object[] objs = text.getSpans(0, text.length(), kind);
605
606        if (objs.length == 0) {
607            return null;
608        } else {
609            return objs[objs.length - 1];
610        }
611    }
612
613    private static void start(SpannableStringBuilder text, Object mark) {
614        int len = text.length();
615        text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
616    }
617
618    private static void end(SpannableStringBuilder text, Class kind,
619                            Object repl) {
620        int len = text.length();
621        Object obj = getLast(text, kind);
622        int where = text.getSpanStart(obj);
623
624        text.removeSpan(obj);
625
626        if (where != len) {
627            text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
628        }
629    }
630
631    private static void startImg(SpannableStringBuilder text,
632                                 Attributes attributes, Html.ImageGetter img) {
633        String src = attributes.getValue("", "src");
634        Drawable d = null;
635
636        if (img != null) {
637            d = img.getDrawable(src);
638        }
639
640        if (d == null) {
641            d = Resources.getSystem().
642                    getDrawable(com.android.internal.R.drawable.unknown_image);
643            d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
644        }
645
646        int len = text.length();
647        text.append("\uFFFC");
648
649        text.setSpan(new ImageSpan(d, src), len, text.length(),
650                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
651    }
652
653    private static void startFont(SpannableStringBuilder text,
654                                  Attributes attributes) {
655        String color = attributes.getValue("", "color");
656        String face = attributes.getValue("", "face");
657
658        int len = text.length();
659        text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
660    }
661
662    private static void endFont(SpannableStringBuilder text) {
663        int len = text.length();
664        Object obj = getLast(text, Font.class);
665        int where = text.getSpanStart(obj);
666
667        text.removeSpan(obj);
668
669        if (where != len) {
670            Font f = (Font) obj;
671
672            if (!TextUtils.isEmpty(f.mColor)) {
673                if (f.mColor.startsWith("@")) {
674                    Resources res = Resources.getSystem();
675                    String name = f.mColor.substring(1);
676                    int colorRes = res.getIdentifier(name, "color", "android");
677                    if (colorRes != 0) {
678                        ColorStateList colors = res.getColorStateList(colorRes);
679                        text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
680                                where, len,
681                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
682                    }
683                } else {
684                    int c = Color.getHtmlColor(f.mColor);
685                    if (c != -1) {
686                        text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
687                                where, len,
688                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
689                    }
690                }
691            }
692
693            if (f.mFace != null) {
694                text.setSpan(new TypefaceSpan(f.mFace), where, len,
695                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
696            }
697        }
698    }
699
700    private static void startA(SpannableStringBuilder text, Attributes attributes) {
701        String href = attributes.getValue("", "href");
702
703        int len = text.length();
704        text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
705    }
706
707    private static void endA(SpannableStringBuilder text) {
708        int len = text.length();
709        Object obj = getLast(text, Href.class);
710        int where = text.getSpanStart(obj);
711
712        text.removeSpan(obj);
713
714        if (where != len) {
715            Href h = (Href) obj;
716
717            if (h.mHref != null) {
718                text.setSpan(new URLSpan(h.mHref), where, len,
719                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
720            }
721        }
722    }
723
724    private static void endHeader(SpannableStringBuilder text) {
725        int len = text.length();
726        Object obj = getLast(text, Header.class);
727
728        int where = text.getSpanStart(obj);
729
730        text.removeSpan(obj);
731
732        // Back off not to change only the text, not the blank line.
733        while (len > where && text.charAt(len - 1) == '\n') {
734            len--;
735        }
736
737        if (where != len) {
738            Header h = (Header) obj;
739
740            text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
741                         where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
742            text.setSpan(new StyleSpan(Typeface.BOLD),
743                         where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
744        }
745    }
746
747    public void setDocumentLocator(Locator locator) {
748    }
749
750    public void startDocument() throws SAXException {
751    }
752
753    public void endDocument() throws SAXException {
754    }
755
756    public void startPrefixMapping(String prefix, String uri) throws SAXException {
757    }
758
759    public void endPrefixMapping(String prefix) throws SAXException {
760    }
761
762    public void startElement(String uri, String localName, String qName, Attributes attributes)
763            throws SAXException {
764        handleStartTag(localName, attributes);
765    }
766
767    public void endElement(String uri, String localName, String qName) throws SAXException {
768        handleEndTag(localName);
769    }
770
771    public void characters(char ch[], int start, int length) throws SAXException {
772        StringBuilder sb = new StringBuilder();
773
774        /*
775         * Ignore whitespace that immediately follows other whitespace;
776         * newlines count as spaces.
777         */
778
779        for (int i = 0; i < length; i++) {
780            char c = ch[i + start];
781
782            if (c == ' ' || c == '\n') {
783                char pred;
784                int len = sb.length();
785
786                if (len == 0) {
787                    len = mSpannableStringBuilder.length();
788
789                    if (len == 0) {
790                        pred = '\n';
791                    } else {
792                        pred = mSpannableStringBuilder.charAt(len - 1);
793                    }
794                } else {
795                    pred = sb.charAt(len - 1);
796                }
797
798                if (pred != ' ' && pred != '\n') {
799                    sb.append(' ');
800                }
801            } else {
802                sb.append(c);
803            }
804        }
805
806        mSpannableStringBuilder.append(sb);
807    }
808
809    public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
810    }
811
812    public void processingInstruction(String target, String data) throws SAXException {
813    }
814
815    public void skippedEntity(String name) throws SAXException {
816    }
817
818    private static class Bold { }
819    private static class Italic { }
820    private static class Underline { }
821    private static class Big { }
822    private static class Small { }
823    private static class Monospace { }
824    private static class Blockquote { }
825    private static class Super { }
826    private static class Sub { }
827
828    private static class Font {
829        public String mColor;
830        public String mFace;
831
832        public Font(String color, String face) {
833            mColor = color;
834            mFace = face;
835        }
836    }
837
838    private static class Href {
839        public String mHref;
840
841        public Href(String href) {
842            mHref = href;
843        }
844    }
845
846    private static class Header {
847        private int mLevel;
848
849        public Header(int level) {
850            mLevel = level;
851        }
852    }
853}
854