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