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