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