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