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