/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hippo.text; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; 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.CharacterStyle; 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.TextAppearanceSpan; import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Log; import org.ccil.cowan.tagsoup.HTMLSchema; 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.text.ParseException; import java.util.HashMap; import java.util.Locale; /** * This class processes HTML strings into displayable styled text. * Not all HTML tags are supported. */ public final class Html { /** * Retrieves images for HTML <img> tags. */ public interface ImageGetter { /** * This method is called when the HTML parser encounters an * <img> tag. The <code>source</code> argument is the * string from the "src" attribute; the return value should be * a Drawable representation of the image or <code>null</code> * for a generic replacement image. Make sure you call * setBounds() on your Drawable if it doesn't already have * its bounds set. */ Drawable getDrawable(String source); } /** * Is notified when HTML tags are encountered that the parser does * not know how to interpret. */ public interface TagHandler { /** * This method will be called when the HTML parser encounters * a tag. * * @return True if the tag was handled, false otherwise. */ boolean handleTag(boolean opening, String tag, SpannableStringBuilder output, XMLReader xmlReader, Attributes attributes); } private Html() { } /** * Returns displayable styled text from the provided HTML string. * Any <img> tags in the HTML will display as a generic * replacement image which your program can then go through and * replace with real images. * * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. */ public static Spanned fromHtml(String source) { return fromHtml(source, null, null); } /** * Lazy initialization holder for HTML parser. This class will * a) be preloaded by the zygote, or b) not loaded until absolutely * necessary. */ private static final class HtmlParser { private static final HTMLSchema schema = new HTMLSchema(); } /** * Returns displayable styled text from the provided HTML string. * Any <img> tags in the HTML will use the specified ImageGetter * to request a representation of the image (use null if you don't * want this) and the specified TagHandler to handle unknown tags * (specify null if you don't want this). * * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. */ public static SpannableStringBuilder fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) { Parser parser = new Parser(); try { parser.setProperty(Parser.schemaProperty, HtmlParser.schema); } catch (org.xml.sax.SAXNotRecognizedException | org.xml.sax.SAXNotSupportedException e) { // Should not happen. throw new RuntimeException(e); } HtmlToSpannedConverter converter = new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser); return converter.convert(); } /** * Returns an HTML representation of the provided Spanned text. A best effort is * made to add HTML tags corresponding to spans. Also note that HTML metacharacters * (such as "<" and "&") within the input text are escaped. * * @param text input text to convert * @return string containing input converted to HTML */ public static String toHtml(Spanned text) { StringBuilder out = new StringBuilder(); withinHtml(out, text); return out.toString(); } /** * Returns an HTML escaped representation of the given plain text. */ public static String escapeHtml(CharSequence text) { StringBuilder out = new StringBuilder(); withinStyle(out, text, 0, text.length()); return out.toString(); } private static void withinHtml(StringBuilder out, Spanned text) { int len = text.length(); int next; for (int i = 0; i < text.length(); i = next) { next = text.nextSpanTransition(i, len, ParagraphStyle.class); ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class); String elements = " "; boolean needDiv = false; for(int j = 0; j < style.length; j++) { if (style[j] instanceof AlignmentSpan) { Layout.Alignment align = ((AlignmentSpan) style[j]).getAlignment(); needDiv = true; if (align == Layout.Alignment.ALIGN_CENTER) { elements = "align=\"center\" " + elements; } else if (align == Layout.Alignment.ALIGN_OPPOSITE) { elements = "align=\"right\" " + elements; } else { elements = "align=\"left\" " + elements; } } } if (needDiv) { out.append("<div ").append(elements).append(">"); } withinDiv(out, text, i, next); if (needDiv) { out.append("</div>"); } } } private static void withinDiv(StringBuilder out, Spanned text, int start, int end) { int next; for (int i = start; i < end; i = next) { next = text.nextSpanTransition(i, end, QuoteSpan.class); QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class); for (QuoteSpan quote : quotes) { out.append("<blockquote>"); } withinBlockquote(out, text, i, next); for (QuoteSpan quote : quotes) { out.append("</blockquote>\n"); } } } private static String getOpenParaTagWithDirection(Spanned text, int start, int end) { //final int len = end - start; //final byte[] levels = new byte[len]; //final char[] buffer = new char[len]; //TextUtils.getChars(text, start, end, buffer, 0); //int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len, // false /* no info */); //switch(paraDir) { // case Layout.DIR_RIGHT_TO_LEFT: // return "<p dir=\"rtl\">"; // case Layout.DIR_LEFT_TO_RIGHT: // default: // return "<p dir=\"ltr\">"; //} return "<p dir=\"ltr\">"; } private static void withinBlockquote(StringBuilder out, Spanned text, int start, int end) { out.append(getOpenParaTagWithDirection(text, start, end)); int next; for (int i = start; i < end; i = next) { next = TextUtils.indexOf(text, '\n', i, end); if (next < 0) { next = end; } int nl = 0; while (next < end && text.charAt(next) == '\n') { nl++; next++; } if (withinParagraph(out, text, i, next - nl, nl, next == end)) { /* Paragraph should be closed */ out.append("</p>\n"); out.append(getOpenParaTagWithDirection(text, next, end)); } } out.append("</p>\n"); } /* Returns true if the caller should close and reopen the paragraph. */ private static boolean withinParagraph(StringBuilder out, Spanned text, int start, int end, int nl, boolean last) { int next; for (int i = start; i < end; i = next) { next = text.nextSpanTransition(i, end, CharacterStyle.class); CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class); for (int j = 0; j < style.length; j++) { if (style[j] instanceof StyleSpan) { int s = ((StyleSpan) style[j]).getStyle(); if ((s & Typeface.BOLD) != 0) { out.append("<b>"); } if ((s & Typeface.ITALIC) != 0) { out.append("<i>"); } } if (style[j] instanceof TypefaceSpan) { String s = ((TypefaceSpan) style[j]).getFamily(); if ("monospace".equals(s)) { out.append("<tt>"); } } if (style[j] instanceof SuperscriptSpan) { out.append("<sup>"); } if (style[j] instanceof SubscriptSpan) { out.append("<sub>"); } if (style[j] instanceof UnderlineSpan) { out.append("<u>"); } if (style[j] instanceof StrikethroughSpan) { out.append("<strike>"); } if (style[j] instanceof URLSpan) { out.append("<a href=\""); out.append(((URLSpan) style[j]).getURL()); out.append("\">"); } if (style[j] instanceof ImageSpan) { out.append("<img src=\""); out.append(((ImageSpan) style[j]).getSource()); out.append("\">"); // Don't output the dummy character underlying the image. i = next; } if (style[j] instanceof AbsoluteSizeSpan) { out.append("<font size =\""); out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6); out.append("\">"); } if (style[j] instanceof ForegroundColorSpan) { out.append("<font color =\"#"); String color = Integer.toHexString(((ForegroundColorSpan) style[j]).getForegroundColor() + 0x01000000); while (color.length() < 6) { color = "0" + color; } out.append(color); out.append("\">"); } } withinStyle(out, text, i, next); for (int j = style.length - 1; j >= 0; j--) { if (style[j] instanceof ForegroundColorSpan) { out.append("</font>"); } if (style[j] instanceof AbsoluteSizeSpan) { out.append("</font>"); } if (style[j] instanceof URLSpan) { out.append("</a>"); } if (style[j] instanceof StrikethroughSpan) { out.append("</strike>"); } if (style[j] instanceof UnderlineSpan) { out.append("</u>"); } if (style[j] instanceof SubscriptSpan) { out.append("</sub>"); } if (style[j] instanceof SuperscriptSpan) { out.append("</sup>"); } if (style[j] instanceof TypefaceSpan) { String s = ((TypefaceSpan) style[j]).getFamily(); if (s.equals("monospace")) { out.append("</tt>"); } } if (style[j] instanceof StyleSpan) { int s = ((StyleSpan) style[j]).getStyle(); if ((s & Typeface.BOLD) != 0) { out.append("</b>"); } if ((s & Typeface.ITALIC) != 0) { out.append("</i>"); } } } } if (nl == 1) { out.append("<br>\n"); return false; } else { for (int i = 2; i < nl; i++) { out.append("<br>"); } return !last; } } private static void withinStyle(StringBuilder out, CharSequence text, int start, int end) { for (int i = start; i < end; i++) { char c = text.charAt(i); if (c == '<') { out.append("<"); } else if (c == '>') { out.append(">"); } else if (c == '&') { out.append("&"); } else if (c >= 0xD800 && c <= 0xDFFF) { if (c < 0xDC00 && i + 1 < end) { char d = text.charAt(i + 1); if (d >= 0xDC00 && d <= 0xDFFF) { i++; int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00; out.append("&#").append(codepoint).append(";"); } } } else if (c > 0x7E || c < ' ') { out.append("&#").append((int) c).append(";"); } else if (c == ' ') { while (i + 1 < end && text.charAt(i + 1) == ' ') { out.append(" "); i++; } out.append(' '); } else { out.append(c); } } } } class HtmlToSpannedConverter implements ContentHandler { private static final String TAG = HtmlToSpannedConverter.class.getSimpleName(); private static final float[] HEADER_SIZES = { 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, }; private final String mSource; private final XMLReader mReader; private final SpannableStringBuilder mSpannableStringBuilder; private final Html.ImageGetter mImageGetter; private final Html.TagHandler mTagHandler; public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser) { mSource = source; mSpannableStringBuilder = new SpannableStringBuilder(); mImageGetter = imageGetter; mTagHandler = tagHandler; mReader = parser; } public SpannableStringBuilder convert() { mReader.setContentHandler(this); try { mReader.parse(new InputSource(new StringReader(mSource))); } catch (IOException | SAXException e) { // We are reading from a string. There should not be IO problems. throw new RuntimeException(e); } // Fix flags and range for paragraph-type markup. Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class); for (int i = 0; i < obj.length; i++) { int start = mSpannableStringBuilder.getSpanStart(obj[i]); int end = mSpannableStringBuilder.getSpanEnd(obj[i]); // 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(obj[i]); } else { mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); } } return mSpannableStringBuilder; } private void handleStartTag(String tag, Attributes attributes) { if (mTagHandler == null || !mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader, 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 emite the linebreaks when we handle the close tag. } else if (tag.equalsIgnoreCase("p")) { handleP(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("div")) { handleP(mSpannableStringBuilder); } 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")) { handleP(mSpannableStringBuilder); start(mSpannableStringBuilder, new Blockquote()); } 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("ins")) { start(mSpannableStringBuilder, new Underline()); } else if (tag.equalsIgnoreCase("strike")) { start(mSpannableStringBuilder, new Strike()); } else if (tag.equalsIgnoreCase("s")) { start(mSpannableStringBuilder, new Strike()); } else if (tag.equalsIgnoreCase("del")) { start(mSpannableStringBuilder, new Strike()); } 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') { handleP(mSpannableStringBuilder); start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1')); } else if (tag.equalsIgnoreCase("img")) { startImg(mSpannableStringBuilder, attributes, mImageGetter); } } } private void handleEndTag(String tag) { if (mTagHandler == null || !mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader, null)) { if (tag.equalsIgnoreCase("br")) { handleBr(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("p")) { handleP(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("div")) { handleP(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("strong")) { end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } else if (tag.equalsIgnoreCase("b")) { end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } else if (tag.equalsIgnoreCase("em")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("cite")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("dfn")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("i")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("big")) { end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f)); } else if (tag.equalsIgnoreCase("small")) { end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f)); } else if (tag.equalsIgnoreCase("font")) { endFont(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("blockquote")) { handleP(mSpannableStringBuilder); end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan()); } else if (tag.equalsIgnoreCase("tt")) { end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace")); } else if (tag.equalsIgnoreCase("a")) { endA(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("u")) { end(mSpannableStringBuilder, Underline.class, new UnderlineSpan()); } else if (tag.equalsIgnoreCase("ins")) { end(mSpannableStringBuilder, Underline.class, new UnderlineSpan()); } else if (tag.equalsIgnoreCase("strike")) { end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan()); } else if (tag.equalsIgnoreCase("s")) { end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan()); } else if (tag.equalsIgnoreCase("del")) { end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan()); } else if (tag.equalsIgnoreCase("sup")) { end(mSpannableStringBuilder, Super.class, new SuperscriptSpan()); } else if (tag.equalsIgnoreCase("sub")) { end(mSpannableStringBuilder, Sub.class, new SubscriptSpan()); } else if (tag.length() == 2 && Character.toLowerCase(tag.charAt(0)) == 'h' && tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { handleP(mSpannableStringBuilder); endHeader(mSpannableStringBuilder); } } } private static void handleP(SpannableStringBuilder text) { int len = text.length(); if (len >= 1 && text.charAt(len - 1) == '\n') { if (len >= 2 && text.charAt(len - 2) == '\n') { return; } text.append("\n"); return; } if (len != 0) { text.append("\n\n"); } } private static void handleBr(SpannableStringBuilder text) { text.append("\n"); } private static Object getLast(Spanned text, Class kind) { /* * This knows that the last returned object from getSpans() * will be the most recently added. */ Object[] objs = text.getSpans(0, text.length(), kind); if (objs.length == 0) { return null; } else { return objs[objs.length - 1]; } } private static void start(SpannableStringBuilder text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); } private static void end(SpannableStringBuilder text, Class kind, Object repl) { int len = text.length(); Object obj = getLast(text, kind); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } private static void startImg(SpannableStringBuilder text, Attributes attributes, Html.ImageGetter img) { String src = attributes.getValue("", "src"); Drawable d = null; if (img != null) { d = img.getDrawable(src); } if (d == null) { d = Resources.getSystem(). getDrawable(android.R.drawable.ic_menu_gallery); 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 static void startFont(SpannableStringBuilder text, Attributes attributes) { String color = attributes.getValue("", "color"); String face = attributes.getValue("", "face"); int len = text.length(); text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK); } private static void endFont(SpannableStringBuilder text) { int len = text.length(); Object obj = getLast(text, Font.class); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { Font f = (Font) obj; if (!TextUtils.isEmpty(f.mColor)) { if (f.mColor.startsWith("@")) { Resources res = Resources.getSystem(); String name = f.mColor.substring(1); int colorRes = res.getIdentifier(name, "color", "android"); text.setSpan(new TextAppearanceSpan(null, 0, 0, ColorStateList.valueOf(res.getColor(colorRes)), null), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else { try { int c = getHtmlColor(f.mColor); text.setSpan(new ForegroundColorSpan(c | 0xFF000000), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } catch (ParseException e) { // Ignore } } } if (f.mFace != null) { text.setSpan(new TypefaceSpan(f.mFace), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private static void startA(SpannableStringBuilder text, Attributes attributes) { String href = attributes.getValue("", "href"); int len = text.length(); text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK); } private static void endA(SpannableStringBuilder text) { int len = text.length(); Object obj = getLast(text, Href.class); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { Href h = (Href) obj; if (h.mHref != null) { text.setSpan(new URLSpan(h.mHref), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private static void endHeader(SpannableStringBuilder text) { int len = text.length(); Object obj = getLast(text, Header.class); int where = text.getSpanStart(obj); text.removeSpan(obj); // Back off not to change only the text, not the blank line. while (len > where && text.charAt(len - 1) == '\n') { len--; } if (where != len) { Header h = (Header) obj; text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(new StyleSpan(Typeface.BOLD), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } @Override public void setDocumentLocator(Locator locator) { } @Override public void startDocument() throws SAXException { } @Override public void endDocument() throws SAXException { } @Override public void startPrefixMapping(String prefix, String uri) throws SAXException { } @Override public void endPrefixMapping(String prefix) throws SAXException { } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { handleStartTag(localName, attributes); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { handleEndTag(localName); } @Override 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); } @Override public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { } @Override public void processingInstruction(String target, String data) throws SAXException { } @Override public void skippedEntity(String name) throws SAXException { } private static class Bold { } private static class Italic { } private static class Underline { } private static class Strike { } 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 Font { public String mColor; public String mFace; public Font(String color, String face) { mColor = color; mFace = face; } } private static class Href { public String mHref; public Href(String href) { mHref = href; } } private static class Header { private int mLevel; public Header(int level) { mLevel = level; } } /** * Parse the color string, and return the corresponding color-int. * If the string cannot be parsed, return -1. Supported formats are: * #RRGGBB * #AARRGGBB * rgb(255, 255, 255) * or color name */ @ColorInt public static int getHtmlColor(@NonNull String colorString) throws ParseException { colorString = colorString.trim(); try { return getHtmlColorInternal(colorString); } catch (NumberFormatException e) { Log.e(TAG, "Unknown color: " + colorString); throw new ParseException("Unknown color: " + colorString, 0); } } private static int getHtmlColorInternal(@NonNull String colorString) throws NumberFormatException, ParseException { if (colorString.charAt(0) == '#') { // Use a long to avoid rollovers on #ffXXXXXX long color = Long.parseLong(colorString.substring(1), 16); color |= 0x00000000ff000000; return (int)color; } else if (colorString.startsWith("rgb(") && colorString.endsWith(")")) { String str = colorString.substring(4, colorString.length() - 1); String[] colors = str.split("[\\s]*,[\\s]*"); if (colors.length == 3) { return Color.argb(0xff, Integer.valueOf(colors[0]), Integer.valueOf(colors[1]), Integer.valueOf(colors[2])); } } else { Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.US)); if (color != null) { return color; } } Log.e(TAG, "Unknown color: " + colorString); throw new ParseException("Unknown color: " + colorString, 0); } private static final HashMap<String, Integer> sColorNameMap; static { sColorNameMap = new HashMap<>(); sColorNameMap.put("aliceblue", 0xFFF0F8FF); sColorNameMap.put("antiquewhite", 0xFFFAEBD7); sColorNameMap.put("aqua", 0xFF00FFFF); sColorNameMap.put("aquamarine", 0xFF7FFFD4); sColorNameMap.put("azure", 0xFFF0FFFF); sColorNameMap.put("beige", 0xFFF5F5DC); sColorNameMap.put("bisque", 0xFFFFE4C4); sColorNameMap.put("black", 0xFF000000); sColorNameMap.put("blanchedalmond", 0xFFFFEBCD); sColorNameMap.put("blue", 0xFF0000FF); sColorNameMap.put("blueviolet", 0xFF8A2BE2); sColorNameMap.put("brown", 0xFFA52A2A); sColorNameMap.put("burlywood", 0xFFDEB887); sColorNameMap.put("cadetblue", 0xFF5F9EA0); sColorNameMap.put("chartreuse", 0xFF7FFF00); sColorNameMap.put("chocolate", 0xFFD2691E); sColorNameMap.put("coral", 0xFFFF7F50); sColorNameMap.put("cornflowerblue", 0xFF6495ED); sColorNameMap.put("cornsilk", 0xFFFFF8DC); sColorNameMap.put("crimson", 0xFFDC143C); sColorNameMap.put("cyan", 0xFF00FFFF); sColorNameMap.put("darkblue", 0xFF00008B); sColorNameMap.put("darkcyan", 0xFF008B8B); sColorNameMap.put("darkgoldenrod", 0xFFB8860B); sColorNameMap.put("darkgray", 0xFFA9A9A9); sColorNameMap.put("darkgrey", 0xFFA9A9A9); sColorNameMap.put("darkgreen", 0xFF006400); sColorNameMap.put("darkkhaki", 0xFFBDB76B); sColorNameMap.put("darkmagenta", 0xFF8B008B); sColorNameMap.put("darkolivegreen", 0xFF556B2F); sColorNameMap.put("darkorange", 0xFFFF8C00); sColorNameMap.put("darkorchid", 0xFF9932CC); sColorNameMap.put("darkred", 0xFF8B0000); sColorNameMap.put("darksalmon", 0xFFE9967A); sColorNameMap.put("darkseagreen", 0xFF8FBC8F); sColorNameMap.put("darkslateblue", 0xFF483D8B); sColorNameMap.put("darkslategray", 0xFF2F4F4F); sColorNameMap.put("darkslategrey", 0xFF2F4F4F); sColorNameMap.put("darkturquoise", 0xFF00CED1); sColorNameMap.put("darkviolet", 0xFF9400D3); sColorNameMap.put("deeppink", 0xFFFF1493); sColorNameMap.put("deepskyblue", 0xFF00BFFF); sColorNameMap.put("dimgray", 0xFF696969); sColorNameMap.put("dimgrey", 0xFF696969); sColorNameMap.put("dodgerblue", 0xFF1E90FF); sColorNameMap.put("firebrick", 0xFFB22222); sColorNameMap.put("floralwhite", 0xFFFFFAF0); sColorNameMap.put("forestgreen", 0xFF228B22); sColorNameMap.put("fuchsia", 0xFFFF00FF); sColorNameMap.put("gainsboro", 0xFFDCDCDC); sColorNameMap.put("ghostwhite", 0xFFF8F8FF); sColorNameMap.put("gold", 0xFFFFD700); sColorNameMap.put("goldenrod", 0xFFDAA520); sColorNameMap.put("gray", 0xFF808080); sColorNameMap.put("grey", 0xFF808080); sColorNameMap.put("green", 0xFF008000); sColorNameMap.put("greenyellow", 0xFFADFF2F); sColorNameMap.put("honeydew", 0xFFF0FFF0); sColorNameMap.put("hotpink", 0xFFFF69B4); sColorNameMap.put("indianred", 0xFFCD5C5C); sColorNameMap.put("indigo", 0xFF4B0082); sColorNameMap.put("ivory", 0xFFFFFFF0); sColorNameMap.put("khaki", 0xFFF0E68C); sColorNameMap.put("lavender", 0xFFE6E6FA); sColorNameMap.put("lavenderblush", 0xFFFFF0F5); sColorNameMap.put("lawngreen", 0xFF7CFC00); sColorNameMap.put("lemonchiffon", 0xFFFFFACD); sColorNameMap.put("lightblue", 0xFFADD8E6); sColorNameMap.put("lightcoral", 0xFFF08080); sColorNameMap.put("lightcyan", 0xFFE0FFFF); sColorNameMap.put("lightgoldenrodyellow", 0xFFFAFAD2); sColorNameMap.put("lightgray", 0xFFD3D3D3); sColorNameMap.put("lightgrey", 0xFFD3D3D3); sColorNameMap.put("lightgreen", 0xFF90EE90); sColorNameMap.put("lightpink", 0xFFFFB6C1); sColorNameMap.put("lightsalmon", 0xFFFFA07A); sColorNameMap.put("lightseagreen", 0xFF20B2AA); sColorNameMap.put("lightskyblue", 0xFF87CEFA); sColorNameMap.put("lightslategray", 0xFF778899); sColorNameMap.put("lightslategrey", 0xFF778899); sColorNameMap.put("lightsteelblue", 0xFFB0C4DE); sColorNameMap.put("lightyellow", 0xFFFFFFE0); sColorNameMap.put("lime", 0xFF00FF00); sColorNameMap.put("limegreen", 0xFF32CD32); sColorNameMap.put("linen", 0xFFFAF0E6); sColorNameMap.put("magenta", 0xFFFF00FF); sColorNameMap.put("maroon", 0xFF800000); sColorNameMap.put("mediumaquamarine", 0xFF66CDAA); sColorNameMap.put("mediumblue", 0xFF0000CD); sColorNameMap.put("mediumorchid", 0xFFBA55D3); sColorNameMap.put("mediumpurple", 0xFF9370DB); sColorNameMap.put("mediumseagreen", 0xFF3CB371); sColorNameMap.put("mediumslateblue", 0xFF7B68EE); sColorNameMap.put("mediumspringgreen", 0xFF00FA9A); sColorNameMap.put("mediumturquoise", 0xFF48D1CC); sColorNameMap.put("mediumvioletred", 0xFFC71585); sColorNameMap.put("midnightblue", 0xFF191970); sColorNameMap.put("mintcream", 0xFFF5FFFA); sColorNameMap.put("mistyrose", 0xFFFFE4E1); sColorNameMap.put("moccasin", 0xFFFFE4B5); sColorNameMap.put("navajowhite", 0xFFFFDEAD); sColorNameMap.put("navy", 0xFF000080); sColorNameMap.put("oldlace", 0xFFFDF5E6); sColorNameMap.put("olive", 0xFF808000); sColorNameMap.put("olivedrab", 0xFF6B8E23); sColorNameMap.put("orange", 0xFFFFA500); sColorNameMap.put("orangered", 0xFFFF4500); sColorNameMap.put("orchid", 0xFFDA70D6); sColorNameMap.put("palegoldenrod", 0xFFEEE8AA); sColorNameMap.put("palegreen", 0xFF98FB98); sColorNameMap.put("paleturquoise", 0xFFAFEEEE); sColorNameMap.put("palevioletred", 0xFFDB7093); sColorNameMap.put("papayawhip", 0xFFFFEFD5); sColorNameMap.put("peachpuff", 0xFFFFDAB9); sColorNameMap.put("peru", 0xFFCD853F); sColorNameMap.put("pink", 0xFFFFC0CB); sColorNameMap.put("plum", 0xFFDDA0DD); sColorNameMap.put("powderblue", 0xFFB0E0E6); sColorNameMap.put("purple", 0xFF800080); sColorNameMap.put("rebeccapurple", 0xFF663399); sColorNameMap.put("red", 0xFFFF0000); sColorNameMap.put("rosybrown", 0xFFBC8F8F); sColorNameMap.put("royalblue", 0xFF4169E1); sColorNameMap.put("saddlebrown", 0xFF8B4513); sColorNameMap.put("salmon", 0xFFFA8072); sColorNameMap.put("sandybrown", 0xFFF4A460); sColorNameMap.put("seagreen", 0xFF2E8B57); sColorNameMap.put("seashell", 0xFFFFF5EE); sColorNameMap.put("sienna", 0xFFA0522D); sColorNameMap.put("silver", 0xFFC0C0C0); sColorNameMap.put("skyblue", 0xFF87CEEB); sColorNameMap.put("slateblue", 0xFF6A5ACD); sColorNameMap.put("slategray", 0xFF708090); sColorNameMap.put("slategrey", 0xFF708090); sColorNameMap.put("snow", 0xFFFFFAFA); sColorNameMap.put("springgreen", 0xFF00FF7F); sColorNameMap.put("steelblue", 0xFF4682B4); sColorNameMap.put("tan", 0xFFD2B48C); sColorNameMap.put("teal", 0xFF008080); sColorNameMap.put("thistle", 0xFFD8BFD8); sColorNameMap.put("tomato", 0xFFFF6347); sColorNameMap.put("turquoise", 0xFF40E0D0); sColorNameMap.put("violet", 0xFFEE82EE); sColorNameMap.put("wheat", 0xFFF5DEB3); sColorNameMap.put("white", 0xFFFFFFFF); sColorNameMap.put("whitesmoke", 0xFFF5F5F5); sColorNameMap.put("yellow", 0xFFFFFF00); sColorNameMap.put("yellowgreen", 0xFF9ACD32); } }