package com.pixplicity.htmlcompat; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.Layout; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.BulletSpan; import android.text.style.ForegroundColorSpan; import android.text.style.ImageSpan; import android.text.style.ParagraphStyle; import android.text.style.QuoteSpan; import android.text.style.RelativeSizeSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.SubscriptSpan; import android.text.style.SuperscriptSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import org.ccil.cowan.tagsoup.Parser; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import java.io.IOException; import java.io.StringReader; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; class HtmlToSpannedConverter implements ContentHandler { private static final float[] HEADING_SIZES = { 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, }; private final Context mContext; private String mSource; private final HtmlCompat.SpanCallback mSpanCallback; private XMLReader mReader; private SpannableStringBuilder mSpannableStringBuilder; private HtmlCompat.ImageGetter mImageGetter; private HtmlCompat.TagHandler mTagHandler; private int mFlags; private static Pattern sTextAlignPattern; private static Pattern sForegroundColorPattern; private static Pattern sBackgroundColorPattern; private static Pattern sTextDecorationPattern; private static Pattern sTextFontSizePattern; /** * Name-value mapping of HTML/CSS colors which have different values in {@link Color}. */ private static final Map<String, Integer> sColorMap; static { sColorMap = new HashMap<>(); sColorMap.put("aliceblue", 0xFFF0F8FF); sColorMap.put("antiquewhite", 0xFFFAEBD7); sColorMap.put("aqua", 0xFF00FFFF); sColorMap.put("aquamarine", 0xFF7FFFD4); sColorMap.put("azure", 0xFFF0FFFF); sColorMap.put("beige", 0xFFF5F5DC); sColorMap.put("bisque", 0xFFFFE4C4); sColorMap.put("black", 0xFF000000); sColorMap.put("blanchedalmond", 0xFFFFEBCD); sColorMap.put("blue", 0xFF0000FF); sColorMap.put("blueviolet", 0xFF8A2BE2); sColorMap.put("brown", 0xFFA52A2A); sColorMap.put("burlywood", 0xFFDEB887); sColorMap.put("cadetblue", 0xFF5F9EA0); sColorMap.put("chartreuse", 0xFF7FFF00); sColorMap.put("chocolate", 0xFFD2691E); sColorMap.put("coral", 0xFFFF7F50); sColorMap.put("cornflowerblue", 0xFF6495ED); sColorMap.put("cornsilk", 0xFFFFF8DC); sColorMap.put("crimson", 0xFFDC143C); sColorMap.put("cyan", 0xFF00FFFF); sColorMap.put("darkblue", 0xFF00008B); sColorMap.put("darkcyan", 0xFF008B8B); sColorMap.put("darkgoldenrod", 0xFFB8860B); sColorMap.put("darkgray", 0xFFA9A9A9); sColorMap.put("darkgrey", 0xFFA9A9A9); sColorMap.put("darkgreen", 0xFF006400); sColorMap.put("darkkhaki", 0xFFBDB76B); sColorMap.put("darkmagenta", 0xFF8B008B); sColorMap.put("darkolivegreen", 0xFF556B2F); sColorMap.put("darkorange", 0xFFFF8C00); sColorMap.put("darkorchid", 0xFF9932CC); sColorMap.put("darkred", 0xFF8B0000); sColorMap.put("darksalmon", 0xFFE9967A); sColorMap.put("darkseagreen", 0xFF8FBC8F); sColorMap.put("darkslateblue", 0xFF483D8B); sColorMap.put("darkslategray", 0xFF2F4F4F); sColorMap.put("darkslategrey", 0xFF2F4F4F); sColorMap.put("darkturquoise", 0xFF00CED1); sColorMap.put("darkviolet", 0xFF9400D3); sColorMap.put("deeppink", 0xFFFF1493); sColorMap.put("deepskyblue", 0xFF00BFFF); sColorMap.put("dimgray", 0xFF696969); sColorMap.put("dimgrey", 0xFF696969); sColorMap.put("dodgerblue", 0xFF1E90FF); sColorMap.put("firebrick", 0xFFB22222); sColorMap.put("floralwhite", 0xFFFFFAF0); sColorMap.put("forestgreen", 0xFF228B22); sColorMap.put("fuchsia", 0xFFFF00FF); sColorMap.put("gainsboro", 0xFFDCDCDC); sColorMap.put("ghostwhite", 0xFFF8F8FF); sColorMap.put("gold", 0xFFFFD700); sColorMap.put("goldenrod", 0xFFDAA520); sColorMap.put("gray", 0xFF808080); sColorMap.put("grey", 0xFF808080); sColorMap.put("green", 0xFF008000); sColorMap.put("greenyellow", 0xFFADFF2F); sColorMap.put("honeydew", 0xFFF0FFF0); sColorMap.put("hotpink", 0xFFFF69B4); sColorMap.put("indianred ", 0xFFCD5C5C); sColorMap.put("indigo ", 0xFF4B0082); sColorMap.put("ivory", 0xFFFFFFF0); sColorMap.put("khaki", 0xFFF0E68C); sColorMap.put("lavender", 0xFFE6E6FA); sColorMap.put("lavenderblush", 0xFFFFF0F5); sColorMap.put("lawngreen", 0xFF7CFC00); sColorMap.put("lemonchiffon", 0xFFFFFACD); sColorMap.put("lightblue", 0xFFADD8E6); sColorMap.put("lightcoral", 0xFFF08080); sColorMap.put("lightcyan", 0xFFE0FFFF); sColorMap.put("lightgoldenrodyellow", 0xFFFAFAD2); sColorMap.put("lightgray", 0xFFD3D3D3); sColorMap.put("lightgrey", 0xFFD3D3D3); sColorMap.put("lightgreen", 0xFF90EE90); sColorMap.put("lightpink", 0xFFFFB6C1); sColorMap.put("lightsalmon", 0xFFFFA07A); sColorMap.put("lightseagreen", 0xFF20B2AA); sColorMap.put("lightskyblue", 0xFF87CEFA); sColorMap.put("lightslategray", 0xFF778899); sColorMap.put("lightslategrey", 0xFF778899); sColorMap.put("lightsteelblue", 0xFFB0C4DE); sColorMap.put("lightyellow", 0xFFFFFFE0); sColorMap.put("lime", 0xFF00FF00); sColorMap.put("limegreen", 0xFF32CD32); sColorMap.put("linen", 0xFFFAF0E6); sColorMap.put("magenta", 0xFFFF00FF); sColorMap.put("maroon", 0xFF800000); sColorMap.put("mediumaquamarine", 0xFF66CDAA); sColorMap.put("mediumblue", 0xFF0000CD); sColorMap.put("mediumorchid", 0xFFBA55D3); sColorMap.put("mediumpurple", 0xFF9370DB); sColorMap.put("mediumseagreen", 0xFF3CB371); sColorMap.put("mediumslateblue", 0xFF7B68EE); sColorMap.put("mediumspringgreen", 0xFF00FA9A); sColorMap.put("mediumturquoise", 0xFF48D1CC); sColorMap.put("mediumvioletred", 0xFFC71585); sColorMap.put("midnightblue", 0xFF191970); sColorMap.put("mintcream", 0xFFF5FFFA); sColorMap.put("mistyrose", 0xFFFFE4E1); sColorMap.put("moccasin", 0xFFFFE4B5); sColorMap.put("navajowhite", 0xFFFFDEAD); sColorMap.put("navy", 0xFF000080); sColorMap.put("oldlace", 0xFFFDF5E6); sColorMap.put("olive", 0xFF808000); sColorMap.put("olivedrab", 0xFF6B8E23); sColorMap.put("orange", 0xFFFFA500); sColorMap.put("orangered", 0xFFFF4500); sColorMap.put("orchid", 0xFFDA70D6); sColorMap.put("palegoldenrod", 0xFFEEE8AA); sColorMap.put("palegreen", 0xFF98FB98); sColorMap.put("paleturquoise", 0xFFAFEEEE); sColorMap.put("palevioletred", 0xFFDB7093); sColorMap.put("papayawhip", 0xFFFFEFD5); sColorMap.put("peachpuff", 0xFFFFDAB9); sColorMap.put("peru", 0xFFCD853F); sColorMap.put("pink", 0xFFFFC0CB); sColorMap.put("plum", 0xFFDDA0DD); sColorMap.put("powderblue", 0xFFB0E0E6); sColorMap.put("purple", 0xFF800080); sColorMap.put("rebeccapurple", 0xFF663399); sColorMap.put("red", 0xFFFF0000); sColorMap.put("rosybrown", 0xFFBC8F8F); sColorMap.put("royalblue", 0xFF4169E1); sColorMap.put("saddlebrown", 0xFF8B4513); sColorMap.put("salmon", 0xFFFA8072); sColorMap.put("sandybrown", 0xFFF4A460); sColorMap.put("seagreen", 0xFF2E8B57); sColorMap.put("seashell", 0xFFFFF5EE); sColorMap.put("sienna", 0xFFA0522D); sColorMap.put("silver", 0xFFC0C0C0); sColorMap.put("skyblue", 0xFF87CEEB); sColorMap.put("slateblue", 0xFF6A5ACD); sColorMap.put("slategray", 0xFF708090); sColorMap.put("slategrey", 0xFF708090); sColorMap.put("snow", 0xFFFFFAFA); sColorMap.put("springgreen", 0xFF00FF7F); sColorMap.put("steelblue", 0xFF4682B4); sColorMap.put("tan", 0xFFD2B48C); sColorMap.put("teal", 0xFF008080); sColorMap.put("thistle", 0xFFD8BFD8); sColorMap.put("tomato", 0xFFFF6347); sColorMap.put("turquoise", 0xFF40E0D0); sColorMap.put("violet", 0xFFEE82EE); sColorMap.put("wheat", 0xFFF5DEB3); sColorMap.put("white", 0xFFFFFFFF); sColorMap.put("whitesmoke", 0xFFF5F5F5); sColorMap.put("yellow", 0xFFFFFF00); sColorMap.put("yellowgreen", 0xFF9ACD32); } private static Pattern getTextAlignPattern() { if (sTextAlignPattern == null) { sTextAlignPattern = Pattern.compile("(?:\\s+|\\A)text-align\\s*:\\s*(\\S*)\\b"); } return sTextAlignPattern; } private static Pattern getForegroundColorPattern() { if (sForegroundColorPattern == null) { sForegroundColorPattern = Pattern.compile( "(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b"); } return sForegroundColorPattern; } private static Pattern getBackgroundColorPattern() { if (sBackgroundColorPattern == null) { sBackgroundColorPattern = Pattern.compile( "(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b"); } return sBackgroundColorPattern; } private static Pattern getTextDecorationPattern() { if (sTextDecorationPattern == null) { sTextDecorationPattern = Pattern.compile( "(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b"); } return sTextDecorationPattern; } private static Pattern getFontSizePattern() { if (sTextFontSizePattern == null) { sTextFontSizePattern = Pattern.compile( "(?:\\s+|\\A)font-size\\s*:\\s*(\\S*)\\b"); } return sTextFontSizePattern; } HtmlToSpannedConverter(Context context, String source, HtmlCompat.ImageGetter imageGetter, HtmlCompat.TagHandler tagHandler, HtmlCompat.SpanCallback spanCallback, Parser parser, int flags) { mContext = context; mSource = source; mSpannableStringBuilder = new SpannableStringBuilder(); mImageGetter = imageGetter; mTagHandler = tagHandler; mSpanCallback = spanCallback; mReader = parser; mFlags = flags; } Spanned convert() { mReader.setContentHandler(this); try { mReader.parse(new InputSource(new StringReader(mSource))); } catch (IOException e) { // We are reading from a string. There should not be IO problems. throw new RuntimeException(e); } catch (SAXException e) { // TagSoup doesn't throw parse exceptions. throw new RuntimeException(e); } // Fix flags and range for paragraph-type markup. Object[] spans = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class); for (Object span : spans) { int start = mSpannableStringBuilder.getSpanStart(span); int end = mSpannableStringBuilder.getSpanEnd(span); // If the last line of the range is blank, back off by one. if (end - 2 >= 0) { if (mSpannableStringBuilder.charAt(end - 1) == '\n' && mSpannableStringBuilder.charAt(end - 2) == '\n') { end--; } } if (end == start) { mSpannableStringBuilder.removeSpan(span); } else { mSpannableStringBuilder.setSpan(span, start, end, Spannable.SPAN_PARAGRAPH); } } return mSpannableStringBuilder; } private void handleStartTag(String tag, Attributes attributes) { if (tag.equalsIgnoreCase("br")) { // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br> // so we can safely emit the linebreaks when we handle the close tag. } else if (tag.equalsIgnoreCase("p")) { startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph()); startCssStyle(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("ul")) { startBlockElement(mSpannableStringBuilder, attributes, getMarginList()); } else if (tag.equalsIgnoreCase("li")) { startLi(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("div")) { startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv()); } else if (tag.equalsIgnoreCase("span")) { startCssStyle(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("strong")) { start(mSpannableStringBuilder, new Bold()); } else if (tag.equalsIgnoreCase("b")) { start(mSpannableStringBuilder, new Bold()); } else if (tag.equalsIgnoreCase("em")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("cite")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("dfn")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("i")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("big")) { start(mSpannableStringBuilder, new Big()); } else if (tag.equalsIgnoreCase("small")) { start(mSpannableStringBuilder, new Small()); } else if (tag.equalsIgnoreCase("font")) { startFont(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("blockquote")) { startBlockquote(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("tt")) { start(mSpannableStringBuilder, new Monospace()); } else if (tag.equalsIgnoreCase("a")) { startA(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("u")) { start(mSpannableStringBuilder, new Underline()); } else if (tag.equalsIgnoreCase("del")) { start(mSpannableStringBuilder, new Strikethrough()); } else if (tag.equalsIgnoreCase("s")) { start(mSpannableStringBuilder, new Strikethrough()); } else if (tag.equalsIgnoreCase("strike")) { start(mSpannableStringBuilder, new Strikethrough()); } else if (tag.equalsIgnoreCase("sup")) { start(mSpannableStringBuilder, new Super()); } else if (tag.equalsIgnoreCase("sub")) { start(mSpannableStringBuilder, new Sub()); } else if (tag.length() == 2 && Character.toLowerCase(tag.charAt(0)) == 'h' && tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1'); } else if (tag.equalsIgnoreCase("img")) { startImg(mSpannableStringBuilder, attributes, mImageGetter); } else if (mTagHandler != null) { mTagHandler.handleTag(true, tag, attributes, mSpannableStringBuilder, mReader); } } private void handleEndTag(String tag) { if (tag.equalsIgnoreCase("br")) { handleBr(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("p")) { endCssStyle(tag, mSpannableStringBuilder); endBlockElement(tag, mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("ul")) { endBlockElement(tag, mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("li")) { endLi(tag, mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("div")) { endBlockElement(tag, mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("span")) { endCssStyle(tag, mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("strong")) { end(tag, mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } else if (tag.equalsIgnoreCase("b")) { end(tag, mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } else if (tag.equalsIgnoreCase("em")) { end(tag, mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("cite")) { end(tag, mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("dfn")) { end(tag, mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("i")) { end(tag, mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("big")) { end(tag, mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f)); } else if (tag.equalsIgnoreCase("small")) { end(tag, mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f)); } else if (tag.equalsIgnoreCase("font")) { endFont(tag, mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("blockquote")) { endBlockquote(tag, mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("tt")) { end(tag, mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace")); } else if (tag.equalsIgnoreCase("a")) { endA(tag, mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("u")) { end(tag, mSpannableStringBuilder, Underline.class, new UnderlineSpan()); } else if (tag.equalsIgnoreCase("del")) { end(tag, mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan()); } else if (tag.equalsIgnoreCase("s")) { end(tag, mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan()); } else if (tag.equalsIgnoreCase("strike")) { end(tag, mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan()); } else if (tag.equalsIgnoreCase("sup")) { end(tag, mSpannableStringBuilder, Super.class, new SuperscriptSpan()); } else if (tag.equalsIgnoreCase("sub")) { end(tag, mSpannableStringBuilder, Sub.class, new SubscriptSpan()); } else if (tag.length() == 2 && Character.toLowerCase(tag.charAt(0)) == 'h' && tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { endHeading(tag, mSpannableStringBuilder); } else if (mTagHandler != null) { mTagHandler.handleTag(false, tag, null, mSpannableStringBuilder, mReader); } } private int getMarginParagraph() { return getMargin(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH); } private int getMarginHeading() { return getMargin(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING); } private int getMarginListItem() { return getMargin(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM); } private int getMarginList() { return getMargin(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST); } private int getMarginDiv() { return getMargin(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV); } private int getMarginBlockquote() { return getMargin(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE); } /** * Returns the minimum number of newline characters needed before and after a given block-level * element. * * @param flag the corresponding option flag defined in {@link HtmlCompat} of a block-level element */ private int getMargin(int flag) { if ((flag & mFlags) != 0) { return 1; } return 2; } private void appendNewlines(Editable text, int minNewline) { final int len = text.length(); if (len == 0) { return; } int existingNewlines = 0; for (int i = len - 1; i >= 0 && text.charAt(i) == '\n'; i--) { existingNewlines++; } for (int j = existingNewlines; j < minNewline; j++) { text.append("\n"); } } private void startBlockElement(Editable text, Attributes attributes, int margin) { if (margin > 0) { appendNewlines(text, margin); start(text, new Newline(margin)); } String style = attributes.getValue("", "style"); if (style != null) { Matcher m = getTextAlignPattern().matcher(style); if (m.find()) { String alignment = m.group(1); if (alignment.equalsIgnoreCase("start")) { start(text, new Alignment(Layout.Alignment.ALIGN_NORMAL)); } else if (alignment.equalsIgnoreCase("center")) { start(text, new Alignment(Layout.Alignment.ALIGN_CENTER)); } else if (alignment.equalsIgnoreCase("end")) { start(text, new Alignment(Layout.Alignment.ALIGN_OPPOSITE)); } } } } private void endBlockElement(String tag, Editable text) { Newline n = getLast(text, Newline.class); if (n != null) { appendNewlines(text, n.mNumNewlines); text.removeSpan(n); } Alignment a = getLast(text, Alignment.class); if (a != null) { setSpanFromMark(tag, text, a, new AlignmentSpan.Standard(a.mAlignment)); } } private void handleBr(Editable text) { text.append('\n'); } private void startLi(Editable text, Attributes attributes) { startBlockElement(text, attributes, getMarginListItem()); start(text, new Bullet()); startCssStyle(text, attributes); } private void endLi(String tag, Editable text) { endCssStyle(tag, text); endBlockElement(tag, text); end(tag, text, Bullet.class, new BulletSpan()); } private void startBlockquote(Editable text, Attributes attributes) { startBlockElement(text, attributes, getMarginBlockquote()); start(text, new Blockquote()); } private void endBlockquote(String tag, Editable text) { endBlockElement(tag, text); end(tag, text, Blockquote.class, new QuoteSpan()); } private void startHeading(Editable text, Attributes attributes, int level) { startBlockElement(text, attributes, getMarginHeading()); start(text, new Heading(level)); } private void endHeading(String tag, Editable text) { // RelativeSizeSpan and StyleSpan are CharacterStyles // Their ranges should not include the newlines at the end Heading h = getLast(text, Heading.class); if (h != null) { setSpanFromMark(tag, text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]), new StyleSpan(Typeface.BOLD)); } endBlockElement(tag, text); } private <T> T getLast(Spanned text, Class<T> kind) { /* * This knows that the last returned object from getSpans() * will be the most recently added. */ T[] objs = text.getSpans(0, text.length(), kind); if (objs.length == 0) { return null; } else { return objs[objs.length - 1]; } } private void setSpanFromMark(String tag, Spannable text, Object mark, Object... spans) { int where = text.getSpanStart(mark); text.removeSpan(mark); int len = text.length(); if (where != len) { for (Object span : spans) { if (mSpanCallback != null) { span = mSpanCallback.onSpanCreated(tag, span); } text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private void start(Editable text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } private void end(String tag, Editable text, Class kind, Object repl) { Object obj = getLast(text, kind); if (obj != null) { setSpanFromMark(tag, text, obj, repl); } } private void startCssStyle(Editable text, Attributes attributes) { String style = attributes.getValue("", "style"); if (style != null) { Matcher m = getForegroundColorPattern().matcher(style); if (m.find()) { int c = getHtmlColor(m.group(1)); if (c != -1) { start(text, new Foreground(c | 0xFF000000)); } } m = getBackgroundColorPattern().matcher(style); if (m.find()) { int c = getHtmlColor(m.group(1)); if (c != -1) { start(text, new Background(c | 0xFF000000)); } } m = getTextDecorationPattern().matcher(style); if (m.find()) { String textDecoration = m.group(1); if (textDecoration.equalsIgnoreCase("line-through")) { start(text, new Strikethrough()); } } m = getFontSizePattern().matcher(style); if (m.find()) { String textSizeString = m.group(1); if (!TextUtils.isEmpty(textSizeString)) { if (textSizeString.contains("px")) { int textSize = Integer.valueOf(textSizeString.replaceAll("\\D+", "")); textSize *= mContext.getResources().getDisplayMetrics().density; start(text, new AbsoluteSize(textSize)); } if (textSizeString.contains("em")) { float textSize = Float.valueOf(textSizeString.replaceAll("\\D+", "")); start(text, new RelativeSize(textSize)); } } } } } private void endCssStyle(String tag, Editable text) { Strikethrough s = getLast(text, Strikethrough.class); if (s != null) { setSpanFromMark(tag, text, s, new StrikethroughSpan()); } Background b = getLast(text, Background.class); if (b != null) { setSpanFromMark(tag, text, b, new BackgroundColorSpan(b.mBackgroundColor)); } Foreground f = getLast(text, Foreground.class); if (f != null) { setSpanFromMark(tag, text, f, new ForegroundColorSpan(f.mForegroundColor)); } AbsoluteSize a = getLast(text, AbsoluteSize.class); if (a != null) { setSpanFromMark(tag, text, a, new AbsoluteSizeSpan(a.getTextSize())); } RelativeSize r = getLast(text, RelativeSize.class); if (r != null) { setSpanFromMark(tag, text, r, new RelativeSizeSpan(r.getTextProportion())); } } private void startImg(Editable text, Attributes attributes, HtmlCompat.ImageGetter img) { String src = attributes.getValue("", "src"); Drawable d = null; if (img != null) { d = img.getDrawable(src, attributes); } if (d == null) { Resources res = mContext.getResources(); d = res.getDrawable(R.drawable.unknown_image); d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); } int len = text.length(); text.append("\uFFFC"); text.setSpan(new ImageSpan(d, src), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } private void startFont(Editable text, Attributes attributes) { String color = attributes.getValue("", "color"); String face = attributes.getValue("", "face"); if (!TextUtils.isEmpty(color)) { int c = getHtmlColor(color); if (c != -1) { start(text, new Foreground(c | 0xFF000000)); } } if (!TextUtils.isEmpty(face)) { start(text, new Font(face)); } } private void endFont(String tag, Editable text) { Font font = getLast(text, Font.class); if (font != null) { setSpanFromMark(tag, text, font, new TypefaceSpan(font.mFace)); } Foreground foreground = getLast(text, Foreground.class); if (foreground != null) { setSpanFromMark(tag, text, foreground, new ForegroundColorSpan(foreground.mForegroundColor)); } } private void startA(Editable text, Attributes attributes) { String href = attributes.getValue("", "href"); start(text, new Href(href)); } private void endA(String tag, Editable text) { Href h = getLast(text, Href.class); if (h != null) { if (h.mHref != null) { setSpanFromMark(tag, text, h, new HtmlCompat.DefensiveURLSpan((h.mHref))); } } } private int getHtmlColor(String color) { if ((mFlags & HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS) == HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS) { Integer i = sColorMap.get(color.toLowerCase(Locale.US)); if (i != null) { return i; } } return ColorUtils.getHtmlColor(color); } public void setDocumentLocator(Locator locator) { } public void startDocument() throws SAXException { } public void endDocument() throws SAXException { } public void startPrefixMapping(String prefix, String uri) throws SAXException { } public void endPrefixMapping(String prefix) throws SAXException { } public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { handleStartTag(localName, attributes); } public void endElement(String uri, String localName, String qName) throws SAXException { handleEndTag(localName); } public void characters(char ch[], int start, int length) throws SAXException { StringBuilder sb = new StringBuilder(); /* * Ignore whitespace that immediately follows other whitespace; * newlines count as spaces. */ for (int i = 0; i < length; i++) { char c = ch[i + start]; if (c == ' ' || c == '\n') { char pred; int len = sb.length(); if (len == 0) { len = mSpannableStringBuilder.length(); if (len == 0) { pred = '\n'; } else { pred = mSpannableStringBuilder.charAt(len - 1); } } else { pred = sb.charAt(len - 1); } if (pred != ' ' && pred != '\n') { sb.append(' '); } } else { sb.append(c); } } mSpannableStringBuilder.append(sb); } public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { } public void processingInstruction(String target, String data) throws SAXException { } public void skippedEntity(String name) throws SAXException { } private static class Bold { } private static class Italic { } private static class Underline { } private static class Strikethrough { } private static class Big { } private static class Small { } private static class Monospace { } private static class Blockquote { } private static class Super { } private static class Sub { } private static class Bullet { } private static class Font { String mFace; Font(String face) { mFace = face; } } private static class Href { String mHref; Href(String href) { mHref = href; } } private static class Foreground { private int mForegroundColor; Foreground(int foregroundColor) { mForegroundColor = foregroundColor; } } private static class Background { private int mBackgroundColor; Background(int backgroundColor) { mBackgroundColor = backgroundColor; } } private static class Heading { private int mLevel; Heading(int level) { mLevel = level; } } private static class AbsoluteSize { private int mTextSize; AbsoluteSize(int textSize) { mTextSize = textSize; } public int getTextSize() { return mTextSize; } } private static class RelativeSize { private float mTextProportion; RelativeSize(float textProportion) { mTextProportion = textProportion; } public float getTextProportion() { return mTextProportion; } } private static class Newline { private int mNumNewlines; Newline(int numNewlines) { mNumNewlines = numNewlines; } } private static class Alignment { private Layout.Alignment mAlignment; Alignment(Layout.Alignment alignment) { mAlignment = alignment; } } }