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.app.ActivityThread; 30 import android.app.Application; 31 import android.content.res.ColorStateList; 32 import android.content.res.Resources; 33 import android.graphics.Color; 34 import android.graphics.Typeface; 35 import android.graphics.drawable.Drawable; 36 import android.text.style.AbsoluteSizeSpan; 37 import android.text.style.AlignmentSpan; 38 import android.text.style.BackgroundColorSpan; 39 import android.text.style.BulletSpan; 40 import android.text.style.CharacterStyle; 41 import android.text.style.ForegroundColorSpan; 42 import android.text.style.ImageSpan; 43 import android.text.style.ParagraphStyle; 44 import android.text.style.QuoteSpan; 45 import android.text.style.RelativeSizeSpan; 46 import android.text.style.StrikethroughSpan; 47 import android.text.style.StyleSpan; 48 import android.text.style.SubscriptSpan; 49 import android.text.style.SuperscriptSpan; 50 import android.text.style.TextAppearanceSpan; 51 import android.text.style.TypefaceSpan; 52 import android.text.style.URLSpan; 53 import android.text.style.UnderlineSpan; 54 55 import java.io.IOException; 56 import java.io.StringReader; 57 import java.util.HashMap; 58 import java.util.Locale; 59 import java.util.Map; 60 import java.util.regex.Matcher; 61 import java.util.regex.Pattern; 62 63 /** 64 * This class processes HTML strings into displayable styled text. 65 * Not all HTML tags are supported. 66 */ 67 public class Html { 68 /** 69 * Retrieves images for HTML <img> tags. 70 */ 71 public static interface ImageGetter { 72 /** 73 * This method is called when the HTML parser encounters an 74 * <img> tag. The <code>source</code> argument is the 75 * string from the "src" attribute; the return value should be 76 * a Drawable representation of the image or <code>null</code> 77 * for a generic replacement image. Make sure you call 78 * setBounds() on your Drawable if it doesn't already have 79 * its bounds set. 80 */ getDrawable(String source)81 public Drawable getDrawable(String source); 82 } 83 84 /** 85 * Is notified when HTML tags are encountered that the parser does 86 * not know how to interpret. 87 */ 88 public static interface TagHandler { 89 /** 90 * This method will be called whenn the HTML parser encounters 91 * a tag that it does not know how to interpret. 92 */ handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader)93 public void handleTag(boolean opening, String tag, 94 Editable output, XMLReader xmlReader); 95 } 96 97 /** 98 * Option for {@link #toHtml(Spanned, int)}: Wrap consecutive lines of text delimited by '\n' 99 * inside <p> elements. {@link BulletSpan}s are ignored. 100 */ 101 public static final int TO_HTML_PARAGRAPH_LINES_CONSECUTIVE = 0x00000000; 102 103 /** 104 * Option for {@link #toHtml(Spanned, int)}: Wrap each line of text delimited by '\n' inside a 105 * <p> or a <li> element. This allows {@link ParagraphStyle}s attached to be 106 * encoded as CSS styles within the corresponding <p> or <li> element. 107 */ 108 public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001; 109 110 /** 111 * Flag indicating that texts inside <p> elements will be separated from other texts with 112 * one newline character by default. 113 */ 114 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH = 0x00000001; 115 116 /** 117 * Flag indicating that texts inside <h1>~<h6> elements will be separated from 118 * other texts with one newline character by default. 119 */ 120 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_HEADING = 0x00000002; 121 122 /** 123 * Flag indicating that texts inside <li> elements will be separated from other texts 124 * with one newline character by default. 125 */ 126 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM = 0x00000004; 127 128 /** 129 * Flag indicating that texts inside <ul> elements will be separated from other texts 130 * with one newline character by default. 131 */ 132 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST = 0x00000008; 133 134 /** 135 * Flag indicating that texts inside <div> elements will be separated from other texts 136 * with one newline character by default. 137 */ 138 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_DIV = 0x00000010; 139 140 /** 141 * Flag indicating that texts inside <blockquote> elements will be separated from other 142 * texts with one newline character by default. 143 */ 144 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE = 0x00000020; 145 146 /** 147 * Flag indicating that CSS color values should be used instead of those defined in 148 * {@link Color}. 149 */ 150 public static final int FROM_HTML_OPTION_USE_CSS_COLORS = 0x00000100; 151 152 /** 153 * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level 154 * elements with blank lines (two newline characters) in between. This is the legacy behavior 155 * prior to N. 156 */ 157 public static final int FROM_HTML_MODE_LEGACY = 0x00000000; 158 159 /** 160 * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level 161 * elements with line breaks (single newline character) in between. This inverts the 162 * {@link Spanned} to HTML string conversion done with the option 163 * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}. 164 */ 165 public static final int FROM_HTML_MODE_COMPACT = 166 FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH 167 | FROM_HTML_SEPARATOR_LINE_BREAK_HEADING 168 | FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM 169 | FROM_HTML_SEPARATOR_LINE_BREAK_LIST 170 | FROM_HTML_SEPARATOR_LINE_BREAK_DIV 171 | FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE; 172 173 /** 174 * The bit which indicates if lines delimited by '\n' will be grouped into <p> elements. 175 */ 176 private static final int TO_HTML_PARAGRAPH_FLAG = 0x00000001; 177 Html()178 private Html() { } 179 180 /** 181 * Returns displayable styled text from the provided HTML string with the legacy flags 182 * {@link #FROM_HTML_MODE_LEGACY}. 183 * 184 * @deprecated use {@link #fromHtml(String, int)} instead. 185 */ 186 @Deprecated fromHtml(String source)187 public static Spanned fromHtml(String source) { 188 return fromHtml(source, FROM_HTML_MODE_LEGACY, null, null); 189 } 190 191 /** 192 * Returns displayable styled text from the provided HTML string. Any <img> tags in the 193 * HTML will display as a generic replacement image which your program can then go through and 194 * replace with real images. 195 * 196 * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. 197 */ fromHtml(String source, int flags)198 public static Spanned fromHtml(String source, int flags) { 199 return fromHtml(source, flags, null, null); 200 } 201 202 /** 203 * Lazy initialization holder for HTML parser. This class will 204 * a) be preloaded by the zygote, or b) not loaded until absolutely 205 * necessary. 206 */ 207 private static class HtmlParser { 208 private static final HTMLSchema schema = new HTMLSchema(); 209 } 210 211 /** 212 * Returns displayable styled text from the provided HTML string with the legacy flags 213 * {@link #FROM_HTML_MODE_LEGACY}. 214 * 215 * @deprecated use {@link #fromHtml(String, int, ImageGetter, TagHandler)} instead. 216 */ 217 @Deprecated fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler)218 public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) { 219 return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler); 220 } 221 222 /** 223 * Returns displayable styled text from the provided HTML string. Any <img> tags in the 224 * HTML will use the specified ImageGetter to request a representation of the image (use null 225 * if you don't want this) and the specified TagHandler to handle unknown tags (specify null if 226 * you don't want this). 227 * 228 * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. 229 */ fromHtml(String source, int flags, ImageGetter imageGetter, TagHandler tagHandler)230 public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter, 231 TagHandler tagHandler) { 232 Parser parser = new Parser(); 233 try { 234 parser.setProperty(Parser.schemaProperty, HtmlParser.schema); 235 } catch (org.xml.sax.SAXNotRecognizedException e) { 236 // Should not happen. 237 throw new RuntimeException(e); 238 } catch (org.xml.sax.SAXNotSupportedException e) { 239 // Should not happen. 240 throw new RuntimeException(e); 241 } 242 243 HtmlToSpannedConverter converter = 244 new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags); 245 return converter.convert(); 246 } 247 248 /** 249 * @deprecated use {@link #toHtml(Spanned, int)} instead. 250 */ 251 @Deprecated toHtml(Spanned text)252 public static String toHtml(Spanned text) { 253 return toHtml(text, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE); 254 } 255 256 /** 257 * Returns an HTML representation of the provided Spanned text. A best effort is 258 * made to add HTML tags corresponding to spans. Also note that HTML metacharacters 259 * (such as "<" and "&") within the input text are escaped. 260 * 261 * @param text input text to convert 262 * @param option one of {@link #TO_HTML_PARAGRAPH_LINES_CONSECUTIVE} or 263 * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL} 264 * @return string containing input converted to HTML 265 */ toHtml(Spanned text, int option)266 public static String toHtml(Spanned text, int option) { 267 StringBuilder out = new StringBuilder(); 268 withinHtml(out, text, option); 269 return out.toString(); 270 } 271 272 /** 273 * Returns an HTML escaped representation of the given plain text. 274 */ escapeHtml(CharSequence text)275 public static String escapeHtml(CharSequence text) { 276 StringBuilder out = new StringBuilder(); 277 withinStyle(out, text, 0, text.length()); 278 return out.toString(); 279 } 280 withinHtml(StringBuilder out, Spanned text, int option)281 private static void withinHtml(StringBuilder out, Spanned text, int option) { 282 if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) { 283 encodeTextAlignmentByDiv(out, text, option); 284 return; 285 } 286 287 withinDiv(out, text, 0, text.length(), option); 288 } 289 encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option)290 private static void encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option) { 291 int len = text.length(); 292 293 int next; 294 for (int i = 0; i < len; i = next) { 295 next = text.nextSpanTransition(i, len, ParagraphStyle.class); 296 ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class); 297 String elements = " "; 298 boolean needDiv = false; 299 300 for(int j = 0; j < style.length; j++) { 301 if (style[j] instanceof AlignmentSpan) { 302 Layout.Alignment align = 303 ((AlignmentSpan) style[j]).getAlignment(); 304 needDiv = true; 305 if (align == Layout.Alignment.ALIGN_CENTER) { 306 elements = "align=\"center\" " + elements; 307 } else if (align == Layout.Alignment.ALIGN_OPPOSITE) { 308 elements = "align=\"right\" " + elements; 309 } else { 310 elements = "align=\"left\" " + elements; 311 } 312 } 313 } 314 if (needDiv) { 315 out.append("<div ").append(elements).append(">"); 316 } 317 318 withinDiv(out, text, i, next, option); 319 320 if (needDiv) { 321 out.append("</div>"); 322 } 323 } 324 } 325 withinDiv(StringBuilder out, Spanned text, int start, int end, int option)326 private static void withinDiv(StringBuilder out, Spanned text, int start, int end, 327 int option) { 328 int next; 329 for (int i = start; i < end; i = next) { 330 next = text.nextSpanTransition(i, end, QuoteSpan.class); 331 QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class); 332 333 for (QuoteSpan quote : quotes) { 334 out.append("<blockquote>"); 335 } 336 337 withinBlockquote(out, text, i, next, option); 338 339 for (QuoteSpan quote : quotes) { 340 out.append("</blockquote>\n"); 341 } 342 } 343 } 344 getTextDirection(Spanned text, int start, int end)345 private static String getTextDirection(Spanned text, int start, int end) { 346 final int len = end - start; 347 final byte[] levels = ArrayUtils.newUnpaddedByteArray(len); 348 final char[] buffer = TextUtils.obtain(len); 349 TextUtils.getChars(text, start, end, buffer, 0); 350 351 int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len, 352 false /* no info */); 353 switch(paraDir) { 354 case Layout.DIR_RIGHT_TO_LEFT: 355 return " dir=\"rtl\""; 356 case Layout.DIR_LEFT_TO_RIGHT: 357 default: 358 return " dir=\"ltr\""; 359 } 360 } 361 getTextStyles(Spanned text, int start, int end, boolean forceNoVerticalMargin, boolean includeTextAlign)362 private static String getTextStyles(Spanned text, int start, int end, 363 boolean forceNoVerticalMargin, boolean includeTextAlign) { 364 String margin = null; 365 String textAlign = null; 366 367 if (forceNoVerticalMargin) { 368 margin = "margin-top:0; margin-bottom:0;"; 369 } 370 if (includeTextAlign) { 371 final AlignmentSpan[] alignmentSpans = text.getSpans(start, end, AlignmentSpan.class); 372 373 // Only use the last AlignmentSpan with flag SPAN_PARAGRAPH 374 for (int i = alignmentSpans.length - 1; i >= 0; i--) { 375 AlignmentSpan s = alignmentSpans[i]; 376 if ((text.getSpanFlags(s) & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH) { 377 final Layout.Alignment alignment = s.getAlignment(); 378 if (alignment == Layout.Alignment.ALIGN_NORMAL) { 379 textAlign = "text-align:start;"; 380 } else if (alignment == Layout.Alignment.ALIGN_CENTER) { 381 textAlign = "text-align:center;"; 382 } else if (alignment == Layout.Alignment.ALIGN_OPPOSITE) { 383 textAlign = "text-align:end;"; 384 } 385 break; 386 } 387 } 388 } 389 390 if (margin == null && textAlign == null) { 391 return ""; 392 } 393 394 final StringBuilder style = new StringBuilder(" style=\""); 395 if (margin != null && textAlign != null) { 396 style.append(margin).append(" ").append(textAlign); 397 } else if (margin != null) { 398 style.append(margin); 399 } else if (textAlign != null) { 400 style.append(textAlign); 401 } 402 403 return style.append("\"").toString(); 404 } 405 withinBlockquote(StringBuilder out, Spanned text, int start, int end, int option)406 private static void withinBlockquote(StringBuilder out, Spanned text, int start, int end, 407 int option) { 408 if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) { 409 withinBlockquoteConsecutive(out, text, start, end); 410 } else { 411 withinBlockquoteIndividual(out, text, start, end); 412 } 413 } 414 withinBlockquoteIndividual(StringBuilder out, Spanned text, int start, int end)415 private static void withinBlockquoteIndividual(StringBuilder out, Spanned text, int start, 416 int end) { 417 boolean isInList = false; 418 int next; 419 for (int i = start; i <= end; i = next) { 420 next = TextUtils.indexOf(text, '\n', i, end); 421 if (next < 0) { 422 next = end; 423 } 424 425 if (next == i) { 426 if (isInList) { 427 // Current paragraph is no longer a list item; close the previously opened list 428 isInList = false; 429 out.append("</ul>\n"); 430 } 431 out.append("<br>\n"); 432 } else { 433 boolean isListItem = false; 434 ParagraphStyle[] paragraphStyles = text.getSpans(i, next, ParagraphStyle.class); 435 for (ParagraphStyle paragraphStyle : paragraphStyles) { 436 final int spanFlags = text.getSpanFlags(paragraphStyle); 437 if ((spanFlags & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH 438 && paragraphStyle instanceof BulletSpan) { 439 isListItem = true; 440 break; 441 } 442 } 443 444 if (isListItem && !isInList) { 445 // Current paragraph is the first item in a list 446 isInList = true; 447 out.append("<ul") 448 .append(getTextStyles(text, i, next, true, false)) 449 .append(">\n"); 450 } 451 452 if (isInList && !isListItem) { 453 // Current paragraph is no longer a list item; close the previously opened list 454 isInList = false; 455 out.append("</ul>\n"); 456 } 457 458 String tagType = isListItem ? "li" : "p"; 459 out.append("<").append(tagType) 460 .append(getTextDirection(text, i, next)) 461 .append(getTextStyles(text, i, next, !isListItem, true)) 462 .append(">"); 463 464 withinParagraph(out, text, i, next); 465 466 out.append("</"); 467 out.append(tagType); 468 out.append(">\n"); 469 470 if (next == end && isInList) { 471 isInList = false; 472 out.append("</ul>\n"); 473 } 474 } 475 476 next++; 477 } 478 } 479 withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start, int end)480 private static void withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start, 481 int end) { 482 out.append("<p").append(getTextDirection(text, start, end)).append(">"); 483 484 int next; 485 for (int i = start; i < end; i = next) { 486 next = TextUtils.indexOf(text, '\n', i, end); 487 if (next < 0) { 488 next = end; 489 } 490 491 int nl = 0; 492 493 while (next < end && text.charAt(next) == '\n') { 494 nl++; 495 next++; 496 } 497 498 withinParagraph(out, text, i, next - nl); 499 500 if (nl == 1) { 501 out.append("<br>\n"); 502 } else { 503 for (int j = 2; j < nl; j++) { 504 out.append("<br>"); 505 } 506 if (next != end) { 507 /* Paragraph should be closed and reopened */ 508 out.append("</p>\n"); 509 out.append("<p").append(getTextDirection(text, start, end)).append(">"); 510 } 511 } 512 } 513 514 out.append("</p>\n"); 515 } 516 withinParagraph(StringBuilder out, Spanned text, int start, int end)517 private static void withinParagraph(StringBuilder out, Spanned text, int start, int end) { 518 int next; 519 for (int i = start; i < end; i = next) { 520 next = text.nextSpanTransition(i, end, CharacterStyle.class); 521 CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class); 522 523 for (int j = 0; j < style.length; j++) { 524 if (style[j] instanceof StyleSpan) { 525 int s = ((StyleSpan) style[j]).getStyle(); 526 527 if ((s & Typeface.BOLD) != 0) { 528 out.append("<b>"); 529 } 530 if ((s & Typeface.ITALIC) != 0) { 531 out.append("<i>"); 532 } 533 } 534 if (style[j] instanceof TypefaceSpan) { 535 String s = ((TypefaceSpan) style[j]).getFamily(); 536 537 if ("monospace".equals(s)) { 538 out.append("<tt>"); 539 } 540 } 541 if (style[j] instanceof SuperscriptSpan) { 542 out.append("<sup>"); 543 } 544 if (style[j] instanceof SubscriptSpan) { 545 out.append("<sub>"); 546 } 547 if (style[j] instanceof UnderlineSpan) { 548 out.append("<u>"); 549 } 550 if (style[j] instanceof StrikethroughSpan) { 551 out.append("<span style=\"text-decoration:line-through;\">"); 552 } 553 if (style[j] instanceof URLSpan) { 554 out.append("<a href=\""); 555 out.append(((URLSpan) style[j]).getURL()); 556 out.append("\">"); 557 } 558 if (style[j] instanceof ImageSpan) { 559 out.append("<img src=\""); 560 out.append(((ImageSpan) style[j]).getSource()); 561 out.append("\">"); 562 563 // Don't output the dummy character underlying the image. 564 i = next; 565 } 566 if (style[j] instanceof AbsoluteSizeSpan) { 567 AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]); 568 float sizeDip = s.getSize(); 569 if (!s.getDip()) { 570 Application application = ActivityThread.currentApplication(); 571 sizeDip /= application.getResources().getDisplayMetrics().density; 572 } 573 574 // px in CSS is the equivalance of dip in Android 575 out.append(String.format("<span style=\"font-size:%.0fpx\";>", sizeDip)); 576 } 577 if (style[j] instanceof RelativeSizeSpan) { 578 float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange(); 579 out.append(String.format("<span style=\"font-size:%.2fem;\">", sizeEm)); 580 } 581 if (style[j] instanceof ForegroundColorSpan) { 582 int color = ((ForegroundColorSpan) style[j]).getForegroundColor(); 583 out.append(String.format("<span style=\"color:#%06X;\">", 0xFFFFFF & color)); 584 } 585 if (style[j] instanceof BackgroundColorSpan) { 586 int color = ((BackgroundColorSpan) style[j]).getBackgroundColor(); 587 out.append(String.format("<span style=\"background-color:#%06X;\">", 588 0xFFFFFF & color)); 589 } 590 } 591 592 withinStyle(out, text, i, next); 593 594 for (int j = style.length - 1; j >= 0; j--) { 595 if (style[j] instanceof BackgroundColorSpan) { 596 out.append("</span>"); 597 } 598 if (style[j] instanceof ForegroundColorSpan) { 599 out.append("</span>"); 600 } 601 if (style[j] instanceof RelativeSizeSpan) { 602 out.append("</span>"); 603 } 604 if (style[j] instanceof AbsoluteSizeSpan) { 605 out.append("</span>"); 606 } 607 if (style[j] instanceof URLSpan) { 608 out.append("</a>"); 609 } 610 if (style[j] instanceof StrikethroughSpan) { 611 out.append("</span>"); 612 } 613 if (style[j] instanceof UnderlineSpan) { 614 out.append("</u>"); 615 } 616 if (style[j] instanceof SubscriptSpan) { 617 out.append("</sub>"); 618 } 619 if (style[j] instanceof SuperscriptSpan) { 620 out.append("</sup>"); 621 } 622 if (style[j] instanceof TypefaceSpan) { 623 String s = ((TypefaceSpan) style[j]).getFamily(); 624 625 if (s.equals("monospace")) { 626 out.append("</tt>"); 627 } 628 } 629 if (style[j] instanceof StyleSpan) { 630 int s = ((StyleSpan) style[j]).getStyle(); 631 632 if ((s & Typeface.BOLD) != 0) { 633 out.append("</b>"); 634 } 635 if ((s & Typeface.ITALIC) != 0) { 636 out.append("</i>"); 637 } 638 } 639 } 640 } 641 } 642 withinStyle(StringBuilder out, CharSequence text, int start, int end)643 private static void withinStyle(StringBuilder out, CharSequence text, 644 int start, int end) { 645 for (int i = start; i < end; i++) { 646 char c = text.charAt(i); 647 648 if (c == '<') { 649 out.append("<"); 650 } else if (c == '>') { 651 out.append(">"); 652 } else if (c == '&') { 653 out.append("&"); 654 } else if (c >= 0xD800 && c <= 0xDFFF) { 655 if (c < 0xDC00 && i + 1 < end) { 656 char d = text.charAt(i + 1); 657 if (d >= 0xDC00 && d <= 0xDFFF) { 658 i++; 659 int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00; 660 out.append("&#").append(codepoint).append(";"); 661 } 662 } 663 } else if (c > 0x7E || c < ' ') { 664 out.append("&#").append((int) c).append(";"); 665 } else if (c == ' ') { 666 while (i + 1 < end && text.charAt(i + 1) == ' ') { 667 out.append(" "); 668 i++; 669 } 670 671 out.append(' '); 672 } else { 673 out.append(c); 674 } 675 } 676 } 677 } 678 679 class HtmlToSpannedConverter implements ContentHandler { 680 681 private static final float[] HEADING_SIZES = { 682 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, 683 }; 684 685 private String mSource; 686 private XMLReader mReader; 687 private SpannableStringBuilder mSpannableStringBuilder; 688 private Html.ImageGetter mImageGetter; 689 private Html.TagHandler mTagHandler; 690 private int mFlags; 691 692 private static Pattern sTextAlignPattern; 693 private static Pattern sForegroundColorPattern; 694 private static Pattern sBackgroundColorPattern; 695 private static Pattern sTextDecorationPattern; 696 697 /** 698 * Name-value mapping of HTML/CSS colors which have different values in {@link Color}. 699 */ 700 private static final Map<String, Integer> sColorMap; 701 702 static { 703 sColorMap = new HashMap<>(); 704 sColorMap.put("darkgray", 0xFFA9A9A9); 705 sColorMap.put("gray", 0xFF808080); 706 sColorMap.put("lightgray", 0xFFD3D3D3); 707 sColorMap.put("darkgrey", 0xFFA9A9A9); 708 sColorMap.put("grey", 0xFF808080); 709 sColorMap.put("lightgrey", 0xFFD3D3D3); 710 sColorMap.put("green", 0xFF008000); 711 } 712 getTextAlignPattern()713 private static Pattern getTextAlignPattern() { 714 if (sTextAlignPattern == null) { 715 sTextAlignPattern = Pattern.compile("(?:\\s+|\\A)text-align\\s*:\\s*(\\S*)\\b"); 716 } 717 return sTextAlignPattern; 718 } 719 getForegroundColorPattern()720 private static Pattern getForegroundColorPattern() { 721 if (sForegroundColorPattern == null) { 722 sForegroundColorPattern = Pattern.compile( 723 "(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b"); 724 } 725 return sForegroundColorPattern; 726 } 727 getBackgroundColorPattern()728 private static Pattern getBackgroundColorPattern() { 729 if (sBackgroundColorPattern == null) { 730 sBackgroundColorPattern = Pattern.compile( 731 "(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b"); 732 } 733 return sBackgroundColorPattern; 734 } 735 getTextDecorationPattern()736 private static Pattern getTextDecorationPattern() { 737 if (sTextDecorationPattern == null) { 738 sTextDecorationPattern = Pattern.compile( 739 "(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b"); 740 } 741 return sTextDecorationPattern; 742 } 743 HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser, int flags)744 public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, 745 Html.TagHandler tagHandler, Parser parser, int flags) { 746 mSource = source; 747 mSpannableStringBuilder = new SpannableStringBuilder(); 748 mImageGetter = imageGetter; 749 mTagHandler = tagHandler; 750 mReader = parser; 751 mFlags = flags; 752 } 753 convert()754 public Spanned convert() { 755 756 mReader.setContentHandler(this); 757 try { 758 mReader.parse(new InputSource(new StringReader(mSource))); 759 } catch (IOException e) { 760 // We are reading from a string. There should not be IO problems. 761 throw new RuntimeException(e); 762 } catch (SAXException e) { 763 // TagSoup doesn't throw parse exceptions. 764 throw new RuntimeException(e); 765 } 766 767 // Fix flags and range for paragraph-type markup. 768 Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class); 769 for (int i = 0; i < obj.length; i++) { 770 int start = mSpannableStringBuilder.getSpanStart(obj[i]); 771 int end = mSpannableStringBuilder.getSpanEnd(obj[i]); 772 773 // If the last line of the range is blank, back off by one. 774 if (end - 2 >= 0) { 775 if (mSpannableStringBuilder.charAt(end - 1) == '\n' && 776 mSpannableStringBuilder.charAt(end - 2) == '\n') { 777 end--; 778 } 779 } 780 781 if (end == start) { 782 mSpannableStringBuilder.removeSpan(obj[i]); 783 } else { 784 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); 785 } 786 } 787 788 return mSpannableStringBuilder; 789 } 790 handleStartTag(String tag, Attributes attributes)791 private void handleStartTag(String tag, Attributes attributes) { 792 if (tag.equalsIgnoreCase("br")) { 793 // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br> 794 // so we can safely emit the linebreaks when we handle the close tag. 795 } else if (tag.equalsIgnoreCase("p")) { 796 startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph()); 797 startCssStyle(mSpannableStringBuilder, attributes); 798 } else if (tag.equalsIgnoreCase("ul")) { 799 startBlockElement(mSpannableStringBuilder, attributes, getMarginList()); 800 } else if (tag.equalsIgnoreCase("li")) { 801 startLi(mSpannableStringBuilder, attributes); 802 } else if (tag.equalsIgnoreCase("div")) { 803 startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv()); 804 } else if (tag.equalsIgnoreCase("span")) { 805 startCssStyle(mSpannableStringBuilder, attributes); 806 } else if (tag.equalsIgnoreCase("strong")) { 807 start(mSpannableStringBuilder, new Bold()); 808 } else if (tag.equalsIgnoreCase("b")) { 809 start(mSpannableStringBuilder, new Bold()); 810 } else if (tag.equalsIgnoreCase("em")) { 811 start(mSpannableStringBuilder, new Italic()); 812 } else if (tag.equalsIgnoreCase("cite")) { 813 start(mSpannableStringBuilder, new Italic()); 814 } else if (tag.equalsIgnoreCase("dfn")) { 815 start(mSpannableStringBuilder, new Italic()); 816 } else if (tag.equalsIgnoreCase("i")) { 817 start(mSpannableStringBuilder, new Italic()); 818 } else if (tag.equalsIgnoreCase("big")) { 819 start(mSpannableStringBuilder, new Big()); 820 } else if (tag.equalsIgnoreCase("small")) { 821 start(mSpannableStringBuilder, new Small()); 822 } else if (tag.equalsIgnoreCase("font")) { 823 startFont(mSpannableStringBuilder, attributes); 824 } else if (tag.equalsIgnoreCase("blockquote")) { 825 startBlockquote(mSpannableStringBuilder, attributes); 826 } else if (tag.equalsIgnoreCase("tt")) { 827 start(mSpannableStringBuilder, new Monospace()); 828 } else if (tag.equalsIgnoreCase("a")) { 829 startA(mSpannableStringBuilder, attributes); 830 } else if (tag.equalsIgnoreCase("u")) { 831 start(mSpannableStringBuilder, new Underline()); 832 } else if (tag.equalsIgnoreCase("del")) { 833 start(mSpannableStringBuilder, new Strikethrough()); 834 } else if (tag.equalsIgnoreCase("s")) { 835 start(mSpannableStringBuilder, new Strikethrough()); 836 } else if (tag.equalsIgnoreCase("strike")) { 837 start(mSpannableStringBuilder, new Strikethrough()); 838 } else if (tag.equalsIgnoreCase("sup")) { 839 start(mSpannableStringBuilder, new Super()); 840 } else if (tag.equalsIgnoreCase("sub")) { 841 start(mSpannableStringBuilder, new Sub()); 842 } else if (tag.length() == 2 && 843 Character.toLowerCase(tag.charAt(0)) == 'h' && 844 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { 845 startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1'); 846 } else if (tag.equalsIgnoreCase("img")) { 847 startImg(mSpannableStringBuilder, attributes, mImageGetter); 848 } else if (mTagHandler != null) { 849 mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader); 850 } 851 } 852 handleEndTag(String tag)853 private void handleEndTag(String tag) { 854 if (tag.equalsIgnoreCase("br")) { 855 handleBr(mSpannableStringBuilder); 856 } else if (tag.equalsIgnoreCase("p")) { 857 endCssStyle(mSpannableStringBuilder); 858 endBlockElement(mSpannableStringBuilder); 859 } else if (tag.equalsIgnoreCase("ul")) { 860 endBlockElement(mSpannableStringBuilder); 861 } else if (tag.equalsIgnoreCase("li")) { 862 endLi(mSpannableStringBuilder); 863 } else if (tag.equalsIgnoreCase("div")) { 864 endBlockElement(mSpannableStringBuilder); 865 } else if (tag.equalsIgnoreCase("span")) { 866 endCssStyle(mSpannableStringBuilder); 867 } else if (tag.equalsIgnoreCase("strong")) { 868 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); 869 } else if (tag.equalsIgnoreCase("b")) { 870 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); 871 } else if (tag.equalsIgnoreCase("em")) { 872 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); 873 } else if (tag.equalsIgnoreCase("cite")) { 874 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); 875 } else if (tag.equalsIgnoreCase("dfn")) { 876 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); 877 } else if (tag.equalsIgnoreCase("i")) { 878 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); 879 } else if (tag.equalsIgnoreCase("big")) { 880 end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f)); 881 } else if (tag.equalsIgnoreCase("small")) { 882 end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f)); 883 } else if (tag.equalsIgnoreCase("font")) { 884 endFont(mSpannableStringBuilder); 885 } else if (tag.equalsIgnoreCase("blockquote")) { 886 endBlockquote(mSpannableStringBuilder); 887 } else if (tag.equalsIgnoreCase("tt")) { 888 end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace")); 889 } else if (tag.equalsIgnoreCase("a")) { 890 endA(mSpannableStringBuilder); 891 } else if (tag.equalsIgnoreCase("u")) { 892 end(mSpannableStringBuilder, Underline.class, new UnderlineSpan()); 893 } else if (tag.equalsIgnoreCase("del")) { 894 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan()); 895 } else if (tag.equalsIgnoreCase("s")) { 896 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan()); 897 } else if (tag.equalsIgnoreCase("strike")) { 898 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan()); 899 } else if (tag.equalsIgnoreCase("sup")) { 900 end(mSpannableStringBuilder, Super.class, new SuperscriptSpan()); 901 } else if (tag.equalsIgnoreCase("sub")) { 902 end(mSpannableStringBuilder, Sub.class, new SubscriptSpan()); 903 } else if (tag.length() == 2 && 904 Character.toLowerCase(tag.charAt(0)) == 'h' && 905 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { 906 endHeading(mSpannableStringBuilder); 907 } else if (mTagHandler != null) { 908 mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader); 909 } 910 } 911 getMarginParagraph()912 private int getMarginParagraph() { 913 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH); 914 } 915 getMarginHeading()916 private int getMarginHeading() { 917 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING); 918 } 919 getMarginListItem()920 private int getMarginListItem() { 921 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM); 922 } 923 getMarginList()924 private int getMarginList() { 925 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST); 926 } 927 getMarginDiv()928 private int getMarginDiv() { 929 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV); 930 } 931 getMarginBlockquote()932 private int getMarginBlockquote() { 933 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE); 934 } 935 936 /** 937 * Returns the minimum number of newline characters needed before and after a given block-level 938 * element. 939 * 940 * @param flag the corresponding option flag defined in {@link Html} of a block-level element 941 */ getMargin(int flag)942 private int getMargin(int flag) { 943 if ((flag & mFlags) != 0) { 944 return 1; 945 } 946 return 2; 947 } 948 appendNewlines(Editable text, int minNewline)949 private static void appendNewlines(Editable text, int minNewline) { 950 final int len = text.length(); 951 952 if (len == 0) { 953 return; 954 } 955 956 int existingNewlines = 0; 957 for (int i = len - 1; i >= 0 && text.charAt(i) == '\n'; i--) { 958 existingNewlines++; 959 } 960 961 for (int j = existingNewlines; j < minNewline; j++) { 962 text.append("\n"); 963 } 964 } 965 startBlockElement(Editable text, Attributes attributes, int margin)966 private static void startBlockElement(Editable text, Attributes attributes, int margin) { 967 final int len = text.length(); 968 if (margin > 0) { 969 appendNewlines(text, margin); 970 start(text, new Newline(margin)); 971 } 972 973 String style = attributes.getValue("", "style"); 974 if (style != null) { 975 Matcher m = getTextAlignPattern().matcher(style); 976 if (m.find()) { 977 String alignment = m.group(1); 978 if (alignment.equalsIgnoreCase("start")) { 979 start(text, new Alignment(Layout.Alignment.ALIGN_NORMAL)); 980 } else if (alignment.equalsIgnoreCase("center")) { 981 start(text, new Alignment(Layout.Alignment.ALIGN_CENTER)); 982 } else if (alignment.equalsIgnoreCase("end")) { 983 start(text, new Alignment(Layout.Alignment.ALIGN_OPPOSITE)); 984 } 985 } 986 } 987 } 988 endBlockElement(Editable text)989 private static void endBlockElement(Editable text) { 990 Newline n = getLast(text, Newline.class); 991 if (n != null) { 992 appendNewlines(text, n.mNumNewlines); 993 text.removeSpan(n); 994 } 995 996 Alignment a = getLast(text, Alignment.class); 997 if (a != null) { 998 setSpanFromMark(text, a, new AlignmentSpan.Standard(a.mAlignment)); 999 } 1000 } 1001 handleBr(Editable text)1002 private static void handleBr(Editable text) { 1003 text.append('\n'); 1004 } 1005 startLi(Editable text, Attributes attributes)1006 private void startLi(Editable text, Attributes attributes) { 1007 startBlockElement(text, attributes, getMarginListItem()); 1008 start(text, new Bullet()); 1009 startCssStyle(text, attributes); 1010 } 1011 endLi(Editable text)1012 private static void endLi(Editable text) { 1013 endCssStyle(text); 1014 endBlockElement(text); 1015 end(text, Bullet.class, new BulletSpan()); 1016 } 1017 startBlockquote(Editable text, Attributes attributes)1018 private void startBlockquote(Editable text, Attributes attributes) { 1019 startBlockElement(text, attributes, getMarginBlockquote()); 1020 start(text, new Blockquote()); 1021 } 1022 endBlockquote(Editable text)1023 private static void endBlockquote(Editable text) { 1024 endBlockElement(text); 1025 end(text, Blockquote.class, new QuoteSpan()); 1026 } 1027 startHeading(Editable text, Attributes attributes, int level)1028 private void startHeading(Editable text, Attributes attributes, int level) { 1029 startBlockElement(text, attributes, getMarginHeading()); 1030 start(text, new Heading(level)); 1031 } 1032 endHeading(Editable text)1033 private static void endHeading(Editable text) { 1034 // RelativeSizeSpan and StyleSpan are CharacterStyles 1035 // Their ranges should not include the newlines at the end 1036 Heading h = getLast(text, Heading.class); 1037 if (h != null) { 1038 setSpanFromMark(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]), 1039 new StyleSpan(Typeface.BOLD)); 1040 } 1041 1042 endBlockElement(text); 1043 } 1044 getLast(Spanned text, Class<T> kind)1045 private static <T> T getLast(Spanned text, Class<T> kind) { 1046 /* 1047 * This knows that the last returned object from getSpans() 1048 * will be the most recently added. 1049 */ 1050 T[] objs = text.getSpans(0, text.length(), kind); 1051 1052 if (objs.length == 0) { 1053 return null; 1054 } else { 1055 return objs[objs.length - 1]; 1056 } 1057 } 1058 setSpanFromMark(Spannable text, Object mark, Object... spans)1059 private static void setSpanFromMark(Spannable text, Object mark, Object... spans) { 1060 int where = text.getSpanStart(mark); 1061 text.removeSpan(mark); 1062 int len = text.length(); 1063 if (where != len) { 1064 for (Object span : spans) { 1065 text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1066 } 1067 } 1068 } 1069 start(Editable text, Object mark)1070 private static void start(Editable text, Object mark) { 1071 int len = text.length(); 1072 text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 1073 } 1074 end(Editable text, Class kind, Object repl)1075 private static void end(Editable text, Class kind, Object repl) { 1076 int len = text.length(); 1077 Object obj = getLast(text, kind); 1078 if (obj != null) { 1079 setSpanFromMark(text, obj, repl); 1080 } 1081 } 1082 startCssStyle(Editable text, Attributes attributes)1083 private void startCssStyle(Editable text, Attributes attributes) { 1084 String style = attributes.getValue("", "style"); 1085 if (style != null) { 1086 Matcher m = getForegroundColorPattern().matcher(style); 1087 if (m.find()) { 1088 int c = getHtmlColor(m.group(1)); 1089 if (c != -1) { 1090 start(text, new Foreground(c | 0xFF000000)); 1091 } 1092 } 1093 1094 m = getBackgroundColorPattern().matcher(style); 1095 if (m.find()) { 1096 int c = getHtmlColor(m.group(1)); 1097 if (c != -1) { 1098 start(text, new Background(c | 0xFF000000)); 1099 } 1100 } 1101 1102 m = getTextDecorationPattern().matcher(style); 1103 if (m.find()) { 1104 String textDecoration = m.group(1); 1105 if (textDecoration.equalsIgnoreCase("line-through")) { 1106 start(text, new Strikethrough()); 1107 } 1108 } 1109 } 1110 } 1111 endCssStyle(Editable text)1112 private static void endCssStyle(Editable text) { 1113 Strikethrough s = getLast(text, Strikethrough.class); 1114 if (s != null) { 1115 setSpanFromMark(text, s, new StrikethroughSpan()); 1116 } 1117 1118 Background b = getLast(text, Background.class); 1119 if (b != null) { 1120 setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor)); 1121 } 1122 1123 Foreground f = getLast(text, Foreground.class); 1124 if (f != null) { 1125 setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor)); 1126 } 1127 } 1128 startImg(Editable text, Attributes attributes, Html.ImageGetter img)1129 private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) { 1130 String src = attributes.getValue("", "src"); 1131 Drawable d = null; 1132 1133 if (img != null) { 1134 d = img.getDrawable(src); 1135 } 1136 1137 if (d == null) { 1138 d = Resources.getSystem(). 1139 getDrawable(com.android.internal.R.drawable.unknown_image); 1140 d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 1141 } 1142 1143 int len = text.length(); 1144 text.append("\uFFFC"); 1145 1146 text.setSpan(new ImageSpan(d, src), len, text.length(), 1147 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1148 } 1149 startFont(Editable text, Attributes attributes)1150 private void startFont(Editable text, Attributes attributes) { 1151 String color = attributes.getValue("", "color"); 1152 String face = attributes.getValue("", "face"); 1153 1154 if (!TextUtils.isEmpty(color)) { 1155 int c = getHtmlColor(color); 1156 if (c != -1) { 1157 start(text, new Foreground(c | 0xFF000000)); 1158 } 1159 } 1160 1161 if (!TextUtils.isEmpty(face)) { 1162 start(text, new Font(face)); 1163 } 1164 } 1165 endFont(Editable text)1166 private static void endFont(Editable text) { 1167 Font font = getLast(text, Font.class); 1168 if (font != null) { 1169 setSpanFromMark(text, font, new TypefaceSpan(font.mFace)); 1170 } 1171 1172 Foreground foreground = getLast(text, Foreground.class); 1173 if (foreground != null) { 1174 setSpanFromMark(text, foreground, 1175 new ForegroundColorSpan(foreground.mForegroundColor)); 1176 } 1177 } 1178 startA(Editable text, Attributes attributes)1179 private static void startA(Editable text, Attributes attributes) { 1180 String href = attributes.getValue("", "href"); 1181 start(text, new Href(href)); 1182 } 1183 endA(Editable text)1184 private static void endA(Editable text) { 1185 Href h = getLast(text, Href.class); 1186 if (h != null) { 1187 if (h.mHref != null) { 1188 setSpanFromMark(text, h, new URLSpan((h.mHref))); 1189 } 1190 } 1191 } 1192 getHtmlColor(String color)1193 private int getHtmlColor(String color) { 1194 if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS) 1195 == Html.FROM_HTML_OPTION_USE_CSS_COLORS) { 1196 Integer i = sColorMap.get(color.toLowerCase(Locale.US)); 1197 if (i != null) { 1198 return i; 1199 } 1200 } 1201 return Color.getHtmlColor(color); 1202 } 1203 setDocumentLocator(Locator locator)1204 public void setDocumentLocator(Locator locator) { 1205 } 1206 startDocument()1207 public void startDocument() throws SAXException { 1208 } 1209 endDocument()1210 public void endDocument() throws SAXException { 1211 } 1212 startPrefixMapping(String prefix, String uri)1213 public void startPrefixMapping(String prefix, String uri) throws SAXException { 1214 } 1215 endPrefixMapping(String prefix)1216 public void endPrefixMapping(String prefix) throws SAXException { 1217 } 1218 startElement(String uri, String localName, String qName, Attributes attributes)1219 public void startElement(String uri, String localName, String qName, Attributes attributes) 1220 throws SAXException { 1221 handleStartTag(localName, attributes); 1222 } 1223 endElement(String uri, String localName, String qName)1224 public void endElement(String uri, String localName, String qName) throws SAXException { 1225 handleEndTag(localName); 1226 } 1227 characters(char ch[], int start, int length)1228 public void characters(char ch[], int start, int length) throws SAXException { 1229 StringBuilder sb = new StringBuilder(); 1230 1231 /* 1232 * Ignore whitespace that immediately follows other whitespace; 1233 * newlines count as spaces. 1234 */ 1235 1236 for (int i = 0; i < length; i++) { 1237 char c = ch[i + start]; 1238 1239 if (c == ' ' || c == '\n') { 1240 char pred; 1241 int len = sb.length(); 1242 1243 if (len == 0) { 1244 len = mSpannableStringBuilder.length(); 1245 1246 if (len == 0) { 1247 pred = '\n'; 1248 } else { 1249 pred = mSpannableStringBuilder.charAt(len - 1); 1250 } 1251 } else { 1252 pred = sb.charAt(len - 1); 1253 } 1254 1255 if (pred != ' ' && pred != '\n') { 1256 sb.append(' '); 1257 } 1258 } else { 1259 sb.append(c); 1260 } 1261 } 1262 1263 mSpannableStringBuilder.append(sb); 1264 } 1265 ignorableWhitespace(char ch[], int start, int length)1266 public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { 1267 } 1268 processingInstruction(String target, String data)1269 public void processingInstruction(String target, String data) throws SAXException { 1270 } 1271 skippedEntity(String name)1272 public void skippedEntity(String name) throws SAXException { 1273 } 1274 1275 private static class Bold { } 1276 private static class Italic { } 1277 private static class Underline { } 1278 private static class Strikethrough { } 1279 private static class Big { } 1280 private static class Small { } 1281 private static class Monospace { } 1282 private static class Blockquote { } 1283 private static class Super { } 1284 private static class Sub { } 1285 private static class Bullet { } 1286 1287 private static class Font { 1288 public String mFace; 1289 Font(String face)1290 public Font(String face) { 1291 mFace = face; 1292 } 1293 } 1294 1295 private static class Href { 1296 public String mHref; 1297 Href(String href)1298 public Href(String href) { 1299 mHref = href; 1300 } 1301 } 1302 1303 private static class Foreground { 1304 private int mForegroundColor; 1305 Foreground(int foregroundColor)1306 public Foreground(int foregroundColor) { 1307 mForegroundColor = foregroundColor; 1308 } 1309 } 1310 1311 private static class Background { 1312 private int mBackgroundColor; 1313 Background(int backgroundColor)1314 public Background(int backgroundColor) { 1315 mBackgroundColor = backgroundColor; 1316 } 1317 } 1318 1319 private static class Heading { 1320 private int mLevel; 1321 Heading(int level)1322 public Heading(int level) { 1323 mLevel = level; 1324 } 1325 } 1326 1327 private static class Newline { 1328 private int mNumNewlines; 1329 Newline(int numNewlines)1330 public Newline(int numNewlines) { 1331 mNumNewlines = numNewlines; 1332 } 1333 } 1334 1335 private static class Alignment { 1336 private Layout.Alignment mAlignment; 1337 Alignment(Layout.Alignment alignment)1338 public Alignment(Layout.Alignment alignment) { 1339 mAlignment = alignment; 1340 } 1341 } 1342 } 1343