blob: c80321cde1253900f35ab1a9c4806bb45b79e360 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Romain Guya8f6d5f2012-11-27 11:12:26 -080019import android.graphics.Color;
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -080020import com.android.internal.util.ArrayUtils;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import 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
Bjorn Bringert9cab7f72009-07-15 22:14:24 +010030import android.content.res.ColorStateList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080031import android.content.res.Resources;
32import android.graphics.Typeface;
33import android.graphics.drawable.Drawable;
The Android Open Source Project10592532009-03-18 17:39:46 -070034import android.text.style.AbsoluteSizeSpan;
35import android.text.style.AlignmentSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036import 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;
Bjorn Bringert9cab7f72009-07-15 22:14:24 +010046import android.text.style.TextAppearanceSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047import android.text.style.TypefaceSpan;
48import android.text.style.URLSpan;
49import android.text.style.UnderlineSpan;
Dianne Hackborn2269d1572010-02-24 19:54:22 -080050
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051import java.io.IOException;
52import java.io.StringReader;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053
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 methos 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.
141 */
142 public static String toHtml(Spanned text) {
143 StringBuilder out = new StringBuilder();
The Android Open Source Project10592532009-03-18 17:39:46 -0700144 withinHtml(out, text);
145 return out.toString();
146 }
147
Dianne Hackbornacb69bb2012-04-13 15:36:06 -0700148 /**
149 * Returns an HTML escaped representation of the given plain text.
150 */
151 public static String escapeHtml(CharSequence text) {
152 StringBuilder out = new StringBuilder();
153 withinStyle(out, text, 0, text.length());
154 return out.toString();
155 }
156
The Android Open Source Project10592532009-03-18 17:39:46 -0700157 private static void withinHtml(StringBuilder out, Spanned text) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800158 int len = text.length();
159
160 int next;
161 for (int i = 0; i < text.length(); i = next) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700162 next = text.nextSpanTransition(i, len, ParagraphStyle.class);
163 ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
Satoshi Kataoka32048302009-03-24 19:48:28 -0700164 String elements = " ";
Eric Fischer00ba7662009-03-25 16:08:50 -0700165 boolean needDiv = false;
166
The Android Open Source Project10592532009-03-18 17:39:46 -0700167 for(int j = 0; j < style.length; j++) {
168 if (style[j] instanceof AlignmentSpan) {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800169 Layout.Alignment align =
The Android Open Source Project10592532009-03-18 17:39:46 -0700170 ((AlignmentSpan) style[j]).getAlignment();
Eric Fischer00ba7662009-03-25 16:08:50 -0700171 needDiv = true;
The Android Open Source Project10592532009-03-18 17:39:46 -0700172 if (align == Layout.Alignment.ALIGN_CENTER) {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700173 elements = "align=\"center\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700174 } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700175 elements = "align=\"right\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700176 } else {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700177 elements = "align=\"left\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700178 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700179 }
180 }
Eric Fischer00ba7662009-03-25 16:08:50 -0700181 if (needDiv) {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800182 out.append("<div ").append(elements).append(">");
The Android Open Source Project10592532009-03-18 17:39:46 -0700183 }
184
185 withinDiv(out, text, i, next);
186
Eric Fischer00ba7662009-03-25 16:08:50 -0700187 if (needDiv) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700188 out.append("</div>");
189 }
190 }
191 }
192
193 private static void withinDiv(StringBuilder out, Spanned text,
194 int start, int end) {
195 int next;
196 for (int i = start; i < end; i = next) {
197 next = text.nextSpanTransition(i, end, QuoteSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800198 QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
199
Romain Guya8f6d5f2012-11-27 11:12:26 -0800200 for (QuoteSpan quote : quotes) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800201 out.append("<blockquote>");
202 }
203
204 withinBlockquote(out, text, i, next);
205
Romain Guya8f6d5f2012-11-27 11:12:26 -0800206 for (QuoteSpan quote : quotes) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800207 out.append("</blockquote>\n");
208 }
209 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800210 }
211
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800212 private static String getOpenParaTagWithDirection(Spanned text, int start, int end) {
213 final int len = end - start;
214 final byte[] levels = new byte[ArrayUtils.idealByteArraySize(len)];
215 final char[] buffer = TextUtils.obtain(len);
216 TextUtils.getChars(text, start, end, buffer, 0);
217
218 int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len,
219 false /* no info */);
220 switch(paraDir) {
221 case Layout.DIR_RIGHT_TO_LEFT:
Raph Levien286da7b2012-10-07 16:55:41 -0700222 return "<p dir=\"rtl\">";
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800223 case Layout.DIR_LEFT_TO_RIGHT:
224 default:
Raph Levien286da7b2012-10-07 16:55:41 -0700225 return "<p dir=\"ltr\">";
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800226 }
227 }
228
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800229 private static void withinBlockquote(StringBuilder out, Spanned text,
230 int start, int end) {
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800231 out.append(getOpenParaTagWithDirection(text, start, end));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800232
233 int next;
234 for (int i = start; i < end; i = next) {
235 next = TextUtils.indexOf(text, '\n', i, end);
236 if (next < 0) {
237 next = end;
238 }
239
240 int nl = 0;
241
242 while (next < end && text.charAt(next) == '\n') {
243 nl++;
244 next++;
245 }
246
247 withinParagraph(out, text, i, next - nl, nl, next == end);
248 }
249
250 out.append("</p>\n");
251 }
252
253 private static void withinParagraph(StringBuilder out, Spanned text,
254 int start, int end, int nl,
255 boolean last) {
256 int next;
257 for (int i = start; i < end; i = next) {
258 next = text.nextSpanTransition(i, end, CharacterStyle.class);
259 CharacterStyle[] style = text.getSpans(i, next,
260 CharacterStyle.class);
261
262 for (int j = 0; j < style.length; j++) {
263 if (style[j] instanceof StyleSpan) {
264 int s = ((StyleSpan) style[j]).getStyle();
265
266 if ((s & Typeface.BOLD) != 0) {
267 out.append("<b>");
268 }
269 if ((s & Typeface.ITALIC) != 0) {
270 out.append("<i>");
271 }
272 }
273 if (style[j] instanceof TypefaceSpan) {
274 String s = ((TypefaceSpan) style[j]).getFamily();
275
276 if (s.equals("monospace")) {
277 out.append("<tt>");
278 }
279 }
280 if (style[j] instanceof SuperscriptSpan) {
281 out.append("<sup>");
282 }
283 if (style[j] instanceof SubscriptSpan) {
284 out.append("<sub>");
285 }
286 if (style[j] instanceof UnderlineSpan) {
287 out.append("<u>");
288 }
289 if (style[j] instanceof StrikethroughSpan) {
290 out.append("<strike>");
291 }
292 if (style[j] instanceof URLSpan) {
293 out.append("<a href=\"");
294 out.append(((URLSpan) style[j]).getURL());
295 out.append("\">");
296 }
297 if (style[j] instanceof ImageSpan) {
298 out.append("<img src=\"");
299 out.append(((ImageSpan) style[j]).getSource());
300 out.append("\">");
301
302 // Don't output the dummy character underlying the image.
303 i = next;
304 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700305 if (style[j] instanceof AbsoluteSizeSpan) {
306 out.append("<font size =\"");
307 out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
308 out.append("\">");
309 }
310 if (style[j] instanceof ForegroundColorSpan) {
311 out.append("<font color =\"#");
312 String color = Integer.toHexString(((ForegroundColorSpan)
313 style[j]).getForegroundColor() + 0x01000000);
314 while (color.length() < 6) {
315 color = "0" + color;
316 }
317 out.append(color);
318 out.append("\">");
319 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800320 }
321
322 withinStyle(out, text, i, next);
323
324 for (int j = style.length - 1; j >= 0; j--) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700325 if (style[j] instanceof ForegroundColorSpan) {
326 out.append("</font>");
327 }
328 if (style[j] instanceof AbsoluteSizeSpan) {
329 out.append("</font>");
330 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800331 if (style[j] instanceof URLSpan) {
332 out.append("</a>");
333 }
334 if (style[j] instanceof StrikethroughSpan) {
335 out.append("</strike>");
336 }
337 if (style[j] instanceof UnderlineSpan) {
338 out.append("</u>");
339 }
340 if (style[j] instanceof SubscriptSpan) {
341 out.append("</sub>");
342 }
343 if (style[j] instanceof SuperscriptSpan) {
344 out.append("</sup>");
345 }
346 if (style[j] instanceof TypefaceSpan) {
347 String s = ((TypefaceSpan) style[j]).getFamily();
348
349 if (s.equals("monospace")) {
350 out.append("</tt>");
351 }
352 }
353 if (style[j] instanceof StyleSpan) {
354 int s = ((StyleSpan) style[j]).getStyle();
355
356 if ((s & Typeface.BOLD) != 0) {
357 out.append("</b>");
358 }
359 if ((s & Typeface.ITALIC) != 0) {
360 out.append("</i>");
361 }
362 }
363 }
364 }
365
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800366 String p = last ? "" : "</p>\n" + getOpenParaTagWithDirection(text, start, end);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800367
368 if (nl == 1) {
369 out.append("<br>\n");
370 } else if (nl == 2) {
371 out.append(p);
372 } else {
373 for (int i = 2; i < nl; i++) {
374 out.append("<br>");
375 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800376 out.append(p);
377 }
378 }
379
Dianne Hackbornacb69bb2012-04-13 15:36:06 -0700380 private static void withinStyle(StringBuilder out, CharSequence text,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800381 int start, int end) {
382 for (int i = start; i < end; i++) {
383 char c = text.charAt(i);
384
385 if (c == '<') {
386 out.append("&lt;");
387 } else if (c == '>') {
388 out.append("&gt;");
389 } else if (c == '&') {
390 out.append("&amp;");
Victoria Lease3d476412013-10-29 15:34:51 -0700391 } else if (c >= 0xD800 && c <= 0xDFFF) {
392 if (c < 0xDC00 && i + 1 < end) {
393 char d = text.charAt(i + 1);
394 if (d >= 0xDC00 && d <= 0xDFFF) {
395 i++;
396 int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
397 out.append("&#").append(codepoint).append(";");
398 }
399 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800400 } else if (c > 0x7E || c < ' ') {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800401 out.append("&#").append((int) c).append(";");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800402 } else if (c == ' ') {
403 while (i + 1 < end && text.charAt(i + 1) == ' ') {
404 out.append("&nbsp;");
405 i++;
406 }
407
408 out.append(' ');
409 } else {
410 out.append(c);
411 }
412 }
413 }
414}
415
416class HtmlToSpannedConverter implements ContentHandler {
417
418 private static final float[] HEADER_SIZES = {
419 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
420 };
421
422 private String mSource;
423 private XMLReader mReader;
424 private SpannableStringBuilder mSpannableStringBuilder;
425 private Html.ImageGetter mImageGetter;
426 private Html.TagHandler mTagHandler;
427
428 public HtmlToSpannedConverter(
429 String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
430 Parser parser) {
431 mSource = source;
432 mSpannableStringBuilder = new SpannableStringBuilder();
433 mImageGetter = imageGetter;
434 mTagHandler = tagHandler;
435 mReader = parser;
436 }
437
438 public Spanned convert() {
439
440 mReader.setContentHandler(this);
441 try {
442 mReader.parse(new InputSource(new StringReader(mSource)));
443 } catch (IOException e) {
444 // We are reading from a string. There should not be IO problems.
445 throw new RuntimeException(e);
446 } catch (SAXException e) {
447 // TagSoup doesn't throw parse exceptions.
448 throw new RuntimeException(e);
449 }
450
451 // Fix flags and range for paragraph-type markup.
452 Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
453 for (int i = 0; i < obj.length; i++) {
454 int start = mSpannableStringBuilder.getSpanStart(obj[i]);
455 int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
456
457 // If the last line of the range is blank, back off by one.
458 if (end - 2 >= 0) {
459 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
460 mSpannableStringBuilder.charAt(end - 2) == '\n') {
461 end--;
462 }
463 }
464
465 if (end == start) {
466 mSpannableStringBuilder.removeSpan(obj[i]);
467 } else {
468 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
469 }
470 }
471
472 return mSpannableStringBuilder;
473 }
474
475 private void handleStartTag(String tag, Attributes attributes) {
476 if (tag.equalsIgnoreCase("br")) {
477 // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
478 // so we can safely emite the linebreaks when we handle the close tag.
479 } else if (tag.equalsIgnoreCase("p")) {
480 handleP(mSpannableStringBuilder);
481 } else if (tag.equalsIgnoreCase("div")) {
482 handleP(mSpannableStringBuilder);
Romain Guy94d5e9a2011-08-29 11:12:19 -0700483 } else if (tag.equalsIgnoreCase("strong")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800484 start(mSpannableStringBuilder, new Bold());
485 } else if (tag.equalsIgnoreCase("b")) {
486 start(mSpannableStringBuilder, new Bold());
Romain Guy94d5e9a2011-08-29 11:12:19 -0700487 } else if (tag.equalsIgnoreCase("em")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800488 start(mSpannableStringBuilder, new Italic());
489 } else if (tag.equalsIgnoreCase("cite")) {
490 start(mSpannableStringBuilder, new Italic());
491 } else if (tag.equalsIgnoreCase("dfn")) {
492 start(mSpannableStringBuilder, new Italic());
493 } else if (tag.equalsIgnoreCase("i")) {
494 start(mSpannableStringBuilder, new Italic());
495 } else if (tag.equalsIgnoreCase("big")) {
496 start(mSpannableStringBuilder, new Big());
497 } else if (tag.equalsIgnoreCase("small")) {
498 start(mSpannableStringBuilder, new Small());
499 } else if (tag.equalsIgnoreCase("font")) {
500 startFont(mSpannableStringBuilder, attributes);
501 } else if (tag.equalsIgnoreCase("blockquote")) {
502 handleP(mSpannableStringBuilder);
503 start(mSpannableStringBuilder, new Blockquote());
504 } else if (tag.equalsIgnoreCase("tt")) {
505 start(mSpannableStringBuilder, new Monospace());
506 } else if (tag.equalsIgnoreCase("a")) {
507 startA(mSpannableStringBuilder, attributes);
508 } else if (tag.equalsIgnoreCase("u")) {
509 start(mSpannableStringBuilder, new Underline());
510 } else if (tag.equalsIgnoreCase("sup")) {
511 start(mSpannableStringBuilder, new Super());
512 } else if (tag.equalsIgnoreCase("sub")) {
513 start(mSpannableStringBuilder, new Sub());
514 } else if (tag.length() == 2 &&
515 Character.toLowerCase(tag.charAt(0)) == 'h' &&
516 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
517 handleP(mSpannableStringBuilder);
518 start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
519 } else if (tag.equalsIgnoreCase("img")) {
520 startImg(mSpannableStringBuilder, attributes, mImageGetter);
521 } else if (mTagHandler != null) {
522 mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
523 }
524 }
525
526 private void handleEndTag(String tag) {
527 if (tag.equalsIgnoreCase("br")) {
528 handleBr(mSpannableStringBuilder);
529 } else if (tag.equalsIgnoreCase("p")) {
530 handleP(mSpannableStringBuilder);
531 } else if (tag.equalsIgnoreCase("div")) {
532 handleP(mSpannableStringBuilder);
Romain Guydd808c02011-09-06 10:54:46 -0700533 } else if (tag.equalsIgnoreCase("strong")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800534 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
535 } else if (tag.equalsIgnoreCase("b")) {
536 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
Romain Guydd808c02011-09-06 10:54:46 -0700537 } else if (tag.equalsIgnoreCase("em")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800538 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
539 } else if (tag.equalsIgnoreCase("cite")) {
540 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
541 } else if (tag.equalsIgnoreCase("dfn")) {
542 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
543 } else if (tag.equalsIgnoreCase("i")) {
544 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
545 } else if (tag.equalsIgnoreCase("big")) {
546 end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
547 } else if (tag.equalsIgnoreCase("small")) {
548 end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
549 } else if (tag.equalsIgnoreCase("font")) {
550 endFont(mSpannableStringBuilder);
551 } else if (tag.equalsIgnoreCase("blockquote")) {
552 handleP(mSpannableStringBuilder);
553 end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
554 } else if (tag.equalsIgnoreCase("tt")) {
555 end(mSpannableStringBuilder, Monospace.class,
556 new TypefaceSpan("monospace"));
557 } else if (tag.equalsIgnoreCase("a")) {
558 endA(mSpannableStringBuilder);
559 } else if (tag.equalsIgnoreCase("u")) {
560 end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
561 } else if (tag.equalsIgnoreCase("sup")) {
562 end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
563 } else if (tag.equalsIgnoreCase("sub")) {
564 end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
565 } else if (tag.length() == 2 &&
566 Character.toLowerCase(tag.charAt(0)) == 'h' &&
567 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
568 handleP(mSpannableStringBuilder);
569 endHeader(mSpannableStringBuilder);
570 } else if (mTagHandler != null) {
571 mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
572 }
573 }
574
575 private static void handleP(SpannableStringBuilder text) {
576 int len = text.length();
577
578 if (len >= 1 && text.charAt(len - 1) == '\n') {
579 if (len >= 2 && text.charAt(len - 2) == '\n') {
580 return;
581 }
582
583 text.append("\n");
584 return;
585 }
586
587 if (len != 0) {
588 text.append("\n\n");
589 }
590 }
591
592 private static void handleBr(SpannableStringBuilder text) {
593 text.append("\n");
594 }
595
596 private static Object getLast(Spanned text, Class kind) {
597 /*
598 * This knows that the last returned object from getSpans()
599 * will be the most recently added.
600 */
601 Object[] objs = text.getSpans(0, text.length(), kind);
602
603 if (objs.length == 0) {
604 return null;
605 } else {
606 return objs[objs.length - 1];
607 }
608 }
609
610 private static void start(SpannableStringBuilder text, Object mark) {
611 int len = text.length();
612 text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
613 }
614
615 private static void end(SpannableStringBuilder text, Class kind,
616 Object repl) {
617 int len = text.length();
618 Object obj = getLast(text, kind);
619 int where = text.getSpanStart(obj);
620
621 text.removeSpan(obj);
622
623 if (where != len) {
624 text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
625 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800626 }
627
628 private static void startImg(SpannableStringBuilder text,
629 Attributes attributes, Html.ImageGetter img) {
630 String src = attributes.getValue("", "src");
631 Drawable d = null;
632
633 if (img != null) {
634 d = img.getDrawable(src);
635 }
636
637 if (d == null) {
638 d = Resources.getSystem().
639 getDrawable(com.android.internal.R.drawable.unknown_image);
640 d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
641 }
642
643 int len = text.length();
644 text.append("\uFFFC");
645
646 text.setSpan(new ImageSpan(d, src), len, text.length(),
647 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
648 }
649
650 private static void startFont(SpannableStringBuilder text,
651 Attributes attributes) {
652 String color = attributes.getValue("", "color");
653 String face = attributes.getValue("", "face");
654
655 int len = text.length();
656 text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
657 }
658
659 private static void endFont(SpannableStringBuilder text) {
660 int len = text.length();
661 Object obj = getLast(text, Font.class);
662 int where = text.getSpanStart(obj);
663
664 text.removeSpan(obj);
665
666 if (where != len) {
667 Font f = (Font) obj;
668
Bjorn Bringert9cab7f72009-07-15 22:14:24 +0100669 if (!TextUtils.isEmpty(f.mColor)) {
670 if (f.mColor.startsWith("@")) {
671 Resources res = Resources.getSystem();
672 String name = f.mColor.substring(1);
673 int colorRes = res.getIdentifier(name, "color", "android");
674 if (colorRes != 0) {
675 ColorStateList colors = res.getColorStateList(colorRes);
676 text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
677 where, len,
678 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800679 }
Bjorn Bringert9cab7f72009-07-15 22:14:24 +0100680 } else {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800681 int c = Color.getHtmlColor(f.mColor);
Bjorn Bringert9cab7f72009-07-15 22:14:24 +0100682 if (c != -1) {
683 text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
684 where, len,
685 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
686 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800687 }
688 }
689
690 if (f.mFace != null) {
691 text.setSpan(new TypefaceSpan(f.mFace), where, len,
692 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
693 }
694 }
695 }
696
697 private static void startA(SpannableStringBuilder text, Attributes attributes) {
698 String href = attributes.getValue("", "href");
699
700 int len = text.length();
701 text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
702 }
703
704 private static void endA(SpannableStringBuilder text) {
705 int len = text.length();
706 Object obj = getLast(text, Href.class);
707 int where = text.getSpanStart(obj);
708
709 text.removeSpan(obj);
710
711 if (where != len) {
712 Href h = (Href) obj;
713
714 if (h.mHref != null) {
715 text.setSpan(new URLSpan(h.mHref), where, len,
716 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
717 }
718 }
719 }
720
721 private static void endHeader(SpannableStringBuilder text) {
722 int len = text.length();
723 Object obj = getLast(text, Header.class);
724
725 int where = text.getSpanStart(obj);
726
727 text.removeSpan(obj);
728
729 // Back off not to change only the text, not the blank line.
730 while (len > where && text.charAt(len - 1) == '\n') {
731 len--;
732 }
733
734 if (where != len) {
735 Header h = (Header) obj;
736
737 text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
738 where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
739 text.setSpan(new StyleSpan(Typeface.BOLD),
740 where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
741 }
742 }
743
744 public void setDocumentLocator(Locator locator) {
745 }
746
747 public void startDocument() throws SAXException {
748 }
749
750 public void endDocument() throws SAXException {
751 }
752
753 public void startPrefixMapping(String prefix, String uri) throws SAXException {
754 }
755
756 public void endPrefixMapping(String prefix) throws SAXException {
757 }
758
759 public void startElement(String uri, String localName, String qName, Attributes attributes)
760 throws SAXException {
761 handleStartTag(localName, attributes);
762 }
763
764 public void endElement(String uri, String localName, String qName) throws SAXException {
765 handleEndTag(localName);
766 }
767
768 public void characters(char ch[], int start, int length) throws SAXException {
769 StringBuilder sb = new StringBuilder();
770
771 /*
772 * Ignore whitespace that immediately follows other whitespace;
773 * newlines count as spaces.
774 */
775
776 for (int i = 0; i < length; i++) {
777 char c = ch[i + start];
778
779 if (c == ' ' || c == '\n') {
780 char pred;
781 int len = sb.length();
782
783 if (len == 0) {
784 len = mSpannableStringBuilder.length();
785
786 if (len == 0) {
787 pred = '\n';
788 } else {
789 pred = mSpannableStringBuilder.charAt(len - 1);
790 }
791 } else {
792 pred = sb.charAt(len - 1);
793 }
794
795 if (pred != ' ' && pred != '\n') {
796 sb.append(' ');
797 }
798 } else {
799 sb.append(c);
800 }
801 }
802
803 mSpannableStringBuilder.append(sb);
804 }
805
806 public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
807 }
808
809 public void processingInstruction(String target, String data) throws SAXException {
810 }
811
812 public void skippedEntity(String name) throws SAXException {
813 }
814
815 private static class Bold { }
816 private static class Italic { }
817 private static class Underline { }
818 private static class Big { }
819 private static class Small { }
820 private static class Monospace { }
821 private static class Blockquote { }
822 private static class Super { }
823 private static class Sub { }
824
825 private static class Font {
826 public String mColor;
827 public String mFace;
828
829 public Font(String color, String face) {
830 mColor = color;
831 mFace = face;
832 }
833 }
834
835 private static class Href {
836 public String mHref;
837
838 public Href(String href) {
839 mHref = href;
840 }
841 }
842
843 private static class Header {
844 private int mLevel;
845
846 public Header(int level) {
847 mLevel = level;
848 }
849 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800850}