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