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