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