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