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