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