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