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