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