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