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