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