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