/* * 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 android.text; //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 android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Typeface; import android.graphics.drawable.Drawable; 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 com.android.internal.util.XmlUtils; import java.io.IOException; import java.io.StringReader; import java.nio.CharBuffer; import java.util.HashMap; /** * This class processes HTML strings into displayable styled text. * Not all HTML tags are supported. */ public class Html { /** * Retrieves images for HTML <img> tags. */ public static interface ImageGetter { /** * This methos 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. */ public Drawable getDrawable(String source); } /** * Is notified when HTML tags are encountered that the parser does * not know how to interpret. */ public static interface TagHandler { /** * This method will be called whenn the HTML parser encounters * a tag that it does not know how to interpret. */ public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader); } 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 String 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 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 String fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) { /** * @j2sNative * source = new String(source); * source.isHtml = true; */{} // Parser parser = new Parser(); // try { // parser.setProperty(Parser.schemaProperty, HtmlParser.schema); // } catch (org.xml.sax.SAXNotRecognizedException e) { // // Should not happen. // throw new RuntimeException(e); // } catch (org.xml.sax.SAXNotSupportedException e) { // // Should not happen. // throw new RuntimeException(e); // } // // HtmlToSpannedConverter converter = // new HtmlToSpannedConverter(source, imageGetter, tagHandler, // parser); // return converter.convert(); return source; } /** * Returns an HTML representation of the provided Spanned text. */ public static String toHtml(Spanned text) { StringBuilder out = new StringBuilder(); withinHtml(out, text); 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 " + elements + ">"); } 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); withinBlockquote(out, text, i, next); } } private static void withinBlockquote(StringBuilder out, Spanned text, int start, int end) { int next; for (int i = start; i < end; i = next) { next = end; int nl = 0; while (next < end) { nl++; next++; } withinParagraph(out, text, i, next - nl, nl, next == end); } } private static void 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 (s.equals("monospace")) { 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>"); } } } } String p = ""; if (nl == 1) { out.append("<br>\n"); } else if (nl == 2) { out.append(p); } else { for (int i = 2; i < nl; i++) { out.append("<br>"); } out.append(p); } } private static void withinStyle(StringBuilder out, Spanned 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 > 0x7E || c < ' ') { out.append("&#" + ((int) c) + ";"); } 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 float[] HEADER_SIZES = { // 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, // }; // // private String mSource; // private XMLReader mReader; // private SpannableStringBuilder mSpannableStringBuilder; // private Html.ImageGetter mImageGetter; // private 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 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[] 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 (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("em")) { // start(mSpannableStringBuilder, new Bold()); // } else if (tag.equalsIgnoreCase("b")) { // start(mSpannableStringBuilder, new Bold()); // } else if (tag.equalsIgnoreCase("strong")) { // 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("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); // } else if (mTagHandler != null) { // mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader); // } // } // // private void handleEndTag(String tag) { // if (tag.equalsIgnoreCase("br")) { // handleBr(mSpannableStringBuilder); // } else if (tag.equalsIgnoreCase("p")) { // handleP(mSpannableStringBuilder); // } else if (tag.equalsIgnoreCase("div")) { // handleP(mSpannableStringBuilder); // } else if (tag.equalsIgnoreCase("em")) { // 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("strong")) { // 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("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); // } else if (mTagHandler != null) { // mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader); // } // } // // 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); // } // // return; // } // // 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(com.android.internal.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 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"); // if (colorRes != 0) { // ColorStateList colors = res.getColorStateList(colorRes); // text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null), // where, len, // Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // } // } else { // int c = getHtmlColor(f.mColor); // if (c != -1) { // text.setSpan(new ForegroundColorSpan(c | 0xFF000000), // where, len, // Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // } // } // } // // 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); // } // } // // 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 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; // } // } // // private static HashMap<String,Integer> COLORS = buildColorMap(); // // private static HashMap<String,Integer> buildColorMap() { // HashMap<String,Integer> map = new HashMap<String,Integer>(); // map.put("aqua", 0x00FFFF); // map.put("black", 0x000000); // map.put("blue", 0x0000FF); // map.put("fuchsia", 0xFF00FF); // map.put("green", 0x008000); // map.put("grey", 0x808080); // map.put("lime", 0x00FF00); // map.put("maroon", 0x800000); // map.put("navy", 0x000080); // map.put("olive", 0x808000); // map.put("purple", 0x800080); // map.put("red", 0xFF0000); // map.put("silver", 0xC0C0C0); // map.put("teal", 0x008080); // map.put("white", 0xFFFFFF); // map.put("yellow", 0xFFFF00); // return map; // } // // /** // * Converts an HTML color (named or numeric) to an integer RGB value. // * // * @param color Non-null color string. // * @return A color value, or {@code -1} if the color string could not be interpreted. // */ // private static int getHtmlColor(String color) { // Integer i = COLORS.get(color.toLowerCase()); // if (i != null) { // return i; // } else { // try { // return XmlUtils.convertValueToInt(color, -1); // } catch (NumberFormatException nfe) { // return -1; // } // } // } // //}