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