/*
 * 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 android.app.ActivityThread;
import android.app.Application;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
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.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;

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.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

This class processes HTML strings into displayable styled text. Not all HTML tags are supported.
/** * This class processes HTML strings into displayable styled text. * Not all HTML tags are supported. */
public class Html {
Retrieves images for HTML <img> tags.
/** * Retrieves images for HTML &lt;img&gt; tags. */
public static interface ImageGetter {
This method is called when the HTML parser encounters an <img> tag. The source argument is the string from the "src" attribute; the return value should be a Drawable representation of the image or null for a generic replacement image. Make sure you call setBounds() on your Drawable if it doesn't already have its bounds set.
/** * This method is called when the HTML parser encounters an * &lt;img&gt; 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.
/** * 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.
/** * 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); }
Option for toHtml(Spanned, int): Wrap consecutive lines of text delimited by '\n' inside <p> elements. BulletSpans are ignored.
/** * Option for {@link #toHtml(Spanned, int)}: Wrap consecutive lines of text delimited by '\n' * inside &lt;p&gt; elements. {@link BulletSpan}s are ignored. */
public static final int TO_HTML_PARAGRAPH_LINES_CONSECUTIVE = 0x00000000;
Option for toHtml(Spanned, int): Wrap each line of text delimited by '\n' inside a <p> or a <li> element. This allows ParagraphStyles attached to be encoded as CSS styles within the corresponding <p> or <li> element.
/** * Option for {@link #toHtml(Spanned, int)}: Wrap each line of text delimited by '\n' inside a * &lt;p&gt; or a &lt;li&gt; element. This allows {@link ParagraphStyle}s attached to be * encoded as CSS styles within the corresponding &lt;p&gt; or &lt;li&gt; element. */
public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001;
Flag indicating that texts inside <p> elements will be separated from other texts with one newline character by default.
/** * Flag indicating that texts inside &lt;p&gt; elements will be separated from other texts with * one newline character by default. */
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH = 0x00000001;
Flag indicating that texts inside <h1>~<h6> elements will be separated from other texts with one newline character by default.
/** * Flag indicating that texts inside &lt;h1&gt;~&lt;h6&gt; elements will be separated from * other texts with one newline character by default. */
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_HEADING = 0x00000002;
Flag indicating that texts inside <li> elements will be separated from other texts with one newline character by default.
/** * Flag indicating that texts inside &lt;li&gt; elements will be separated from other texts * with one newline character by default. */
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM = 0x00000004;
Flag indicating that texts inside <ul> elements will be separated from other texts with one newline character by default.
/** * Flag indicating that texts inside &lt;ul&gt; elements will be separated from other texts * with one newline character by default. */
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST = 0x00000008;
Flag indicating that texts inside <div> elements will be separated from other texts with one newline character by default.
/** * Flag indicating that texts inside &lt;div&gt; elements will be separated from other texts * with one newline character by default. */
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_DIV = 0x00000010;
Flag indicating that texts inside <blockquote> elements will be separated from other texts with one newline character by default.
/** * Flag indicating that texts inside &lt;blockquote&gt; elements will be separated from other * texts with one newline character by default. */
public static final int FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE = 0x00000020;
Flag indicating that CSS color values should be used instead of those defined in Color.
/** * Flag indicating that CSS color values should be used instead of those defined in * {@link Color}. */
public static final int FROM_HTML_OPTION_USE_CSS_COLORS = 0x00000100;
Flags for fromHtml(String, int, ImageGetter, TagHandler): Separate block-level elements with blank lines (two newline characters) in between. This is the legacy behavior prior to N.
/** * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level * elements with blank lines (two newline characters) in between. This is the legacy behavior * prior to N. */
public static final int FROM_HTML_MODE_LEGACY = 0x00000000;
Flags for fromHtml(String, int, ImageGetter, TagHandler): Separate block-level elements with line breaks (single newline character) in between. This inverts the Spanned to HTML string conversion done with the option TO_HTML_PARAGRAPH_LINES_INDIVIDUAL.
/** * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level * elements with line breaks (single newline character) in between. This inverts the * {@link Spanned} to HTML string conversion done with the option * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}. */
public static final int FROM_HTML_MODE_COMPACT = FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH | FROM_HTML_SEPARATOR_LINE_BREAK_HEADING | FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM | FROM_HTML_SEPARATOR_LINE_BREAK_LIST | FROM_HTML_SEPARATOR_LINE_BREAK_DIV | FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE;
The bit which indicates if lines delimited by '\n' will be grouped into <p> elements.
/** * The bit which indicates if lines delimited by '\n' will be grouped into &lt;p&gt; elements. */
private static final int TO_HTML_PARAGRAPH_FLAG = 0x00000001; private Html() { }
Returns displayable styled text from the provided HTML string with the legacy flags FROM_HTML_MODE_LEGACY.
Deprecated:use fromHtml(String, int) instead.
/** * Returns displayable styled text from the provided HTML string with the legacy flags * {@link #FROM_HTML_MODE_LEGACY}. * * @deprecated use {@link #fromHtml(String, int)} instead. */
@Deprecated public static Spanned fromHtml(String source) { return fromHtml(source, FROM_HTML_MODE_LEGACY, null, null); }
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.

This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.

/** * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; 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, int flags) { return fromHtml(source, flags, null, null); }
Lazy initialization holder for HTML parser. This class will a) be preloaded by the zygote, or b) not loaded until absolutely necessary.
/** * 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 with the legacy flags FROM_HTML_MODE_LEGACY.
Deprecated:use fromHtml(String, int, ImageGetter, TagHandler) instead.
/** * Returns displayable styled text from the provided HTML string with the legacy flags * {@link #FROM_HTML_MODE_LEGACY}. * * @deprecated use {@link #fromHtml(String, int, ImageGetter, TagHandler)} instead. */
@Deprecated public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) { return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler); }
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).

This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.

/** * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; 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 Spanned fromHtml(String source, int flags, ImageGetter imageGetter, TagHandler tagHandler) { 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, flags); return converter.convert(); }
Deprecated:use toHtml(Spanned, int) instead.
/** * @deprecated use {@link #toHtml(Spanned, int)} instead. */
@Deprecated public static String toHtml(Spanned text) { return toHtml(text, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE); }
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.
Params:
Returns:string containing input converted to HTML
/** * 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 "&lt;" and "&amp;") within the input text are escaped. * * @param text input text to convert * @param option one of {@link #TO_HTML_PARAGRAPH_LINES_CONSECUTIVE} or * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL} * @return string containing input converted to HTML */
public static String toHtml(Spanned text, int option) { StringBuilder out = new StringBuilder(); withinHtml(out, text, option); return out.toString(); }
Returns an HTML escaped representation of the given plain text.
/** * 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 option) { if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) { encodeTextAlignmentByDiv(out, text, option); return; } withinDiv(out, text, 0, text.length(), option); } private static void encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option) { int len = text.length(); int next; for (int i = 0; i < len; 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, option); if (needDiv) { out.append("</div>"); } } } private static void withinDiv(StringBuilder out, Spanned text, int start, int end, int option) { 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, option); for (QuoteSpan quote : quotes) { out.append("</blockquote>\n"); } } } private static String getTextDirection(Spanned text, int start, int end) { if (TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(text, start, end - start)) { return " dir=\"rtl\""; } else { return " dir=\"ltr\""; } } private static String getTextStyles(Spanned text, int start, int end, boolean forceNoVerticalMargin, boolean includeTextAlign) { String margin = null; String textAlign = null; if (forceNoVerticalMargin) { margin = "margin-top:0; margin-bottom:0;"; } if (includeTextAlign) { final AlignmentSpan[] alignmentSpans = text.getSpans(start, end, AlignmentSpan.class); // Only use the last AlignmentSpan with flag SPAN_PARAGRAPH for (int i = alignmentSpans.length - 1; i >= 0; i--) { AlignmentSpan s = alignmentSpans[i]; if ((text.getSpanFlags(s) & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH) { final Layout.Alignment alignment = s.getAlignment(); if (alignment == Layout.Alignment.ALIGN_NORMAL) { textAlign = "text-align:start;"; } else if (alignment == Layout.Alignment.ALIGN_CENTER) { textAlign = "text-align:center;"; } else if (alignment == Layout.Alignment.ALIGN_OPPOSITE) { textAlign = "text-align:end;"; } break; } } } if (margin == null && textAlign == null) { return ""; } final StringBuilder style = new StringBuilder(" style=\""); if (margin != null && textAlign != null) { style.append(margin).append(" ").append(textAlign); } else if (margin != null) { style.append(margin); } else if (textAlign != null) { style.append(textAlign); } return style.append("\"").toString(); } private static void withinBlockquote(StringBuilder out, Spanned text, int start, int end, int option) { if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) { withinBlockquoteConsecutive(out, text, start, end); } else { withinBlockquoteIndividual(out, text, start, end); } } private static void withinBlockquoteIndividual(StringBuilder out, Spanned text, int start, int end) { boolean isInList = false; int next; for (int i = start; i <= end; i = next) { next = TextUtils.indexOf(text, '\n', i, end); if (next < 0) { next = end; } if (next == i) { if (isInList) { // Current paragraph is no longer a list item; close the previously opened list isInList = false; out.append("</ul>\n"); } out.append("<br>\n"); } else { boolean isListItem = false; ParagraphStyle[] paragraphStyles = text.getSpans(i, next, ParagraphStyle.class); for (ParagraphStyle paragraphStyle : paragraphStyles) { final int spanFlags = text.getSpanFlags(paragraphStyle); if ((spanFlags & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH && paragraphStyle instanceof BulletSpan) { isListItem = true; break; } } if (isListItem && !isInList) { // Current paragraph is the first item in a list isInList = true; out.append("<ul") .append(getTextStyles(text, i, next, true, false)) .append(">\n"); } if (isInList && !isListItem) { // Current paragraph is no longer a list item; close the previously opened list isInList = false; out.append("</ul>\n"); } String tagType = isListItem ? "li" : "p"; out.append("<").append(tagType) .append(getTextDirection(text, i, next)) .append(getTextStyles(text, i, next, !isListItem, true)) .append(">"); withinParagraph(out, text, i, next); out.append("</"); out.append(tagType); out.append(">\n"); if (next == end && isInList) { isInList = false; out.append("</ul>\n"); } } next++; } } private static void withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start, int end) { out.append("<p").append(getTextDirection(text, start, end)).append(">"); 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++; } withinParagraph(out, text, i, next - nl); if (nl == 1) { out.append("<br>\n"); } else { for (int j = 2; j < nl; j++) { out.append("<br>"); } if (next != end) { /* Paragraph should be closed and reopened */ out.append("</p>\n"); out.append("<p").append(getTextDirection(text, start, end)).append(">"); } } } out.append("</p>\n"); } private static void withinParagraph(StringBuilder out, Spanned text, int start, int end) { 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("<span style=\"text-decoration:line-through;\">"); } 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) { AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]); float sizeDip = s.getSize(); if (!s.getDip()) { Application application = ActivityThread.currentApplication(); sizeDip /= application.getResources().getDisplayMetrics().density; } // px in CSS is the equivalance of dip in Android out.append(String.format("<span style=\"font-size:%.0fpx\";>", sizeDip)); } if (style[j] instanceof RelativeSizeSpan) { float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange(); out.append(String.format("<span style=\"font-size:%.2fem;\">", sizeEm)); } if (style[j] instanceof ForegroundColorSpan) { int color = ((ForegroundColorSpan) style[j]).getForegroundColor(); out.append(String.format("<span style=\"color:#%06X;\">", 0xFFFFFF & color)); } if (style[j] instanceof BackgroundColorSpan) { int color = ((BackgroundColorSpan) style[j]).getBackgroundColor(); out.append(String.format("<span style=\"background-color:#%06X;\">", 0xFFFFFF & color)); } } withinStyle(out, text, i, next); for (int j = style.length - 1; j >= 0; j--) { if (style[j] instanceof BackgroundColorSpan) { out.append("</span>"); } if (style[j] instanceof ForegroundColorSpan) { out.append("</span>"); } if (style[j] instanceof RelativeSizeSpan) { out.append("</span>"); } if (style[j] instanceof AbsoluteSizeSpan) { out.append("</span>"); } if (style[j] instanceof URLSpan) { out.append("</a>"); } if (style[j] instanceof StrikethroughSpan) { out.append("</span>"); } 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>"); } } } } } 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("&lt;"); } else if (c == '>') { out.append("&gt;"); } else if (c == '&') { out.append("&amp;"); } 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("&nbsp;"); i++; } out.append(' '); } else { out.append(c); } } } } class HtmlToSpannedConverter implements ContentHandler { private static final float[] HEADING_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; private int mFlags; private static Pattern sTextAlignPattern; private static Pattern sForegroundColorPattern; private static Pattern sBackgroundColorPattern; private static Pattern sTextDecorationPattern;
Name-value mapping of HTML/CSS colors which have different values in Color.
/** * 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("darkgray", 0xFFA9A9A9); sColorMap.put("gray", 0xFF808080); sColorMap.put("lightgray", 0xFFD3D3D3); sColorMap.put("darkgrey", 0xFFA9A9A9); sColorMap.put("grey", 0xFF808080); sColorMap.put("lightgrey", 0xFFD3D3D3); sColorMap.put("green", 0xFF008000); } 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; } public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser, int flags) { mSource = source; mSpannableStringBuilder = new SpannableStringBuilder(); mImageGetter = imageGetter; mTagHandler = tagHandler; mReader = parser; mFlags = flags; } 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 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, mSpannableStringBuilder, mReader); } } private void handleEndTag(String tag) { if (tag.equalsIgnoreCase("br")) { handleBr(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("p")) { endCssStyle(mSpannableStringBuilder); endBlockElement(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("ul")) { endBlockElement(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("li")) { endLi(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("div")) { endBlockElement(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("span")) { endCssStyle(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")) { endBlockquote(mSpannableStringBuilder); } 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("del")) { end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan()); } else if (tag.equalsIgnoreCase("s")) { end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan()); } else if (tag.equalsIgnoreCase("strike")) { end(mSpannableStringBuilder, Strikethrough.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') { endHeading(mSpannableStringBuilder); } else if (mTagHandler != null) { mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader); } } private int getMarginParagraph() { return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH); } private int getMarginHeading() { return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING); } private int getMarginListItem() { return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM); } private int getMarginList() { return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST); } private int getMarginDiv() { return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV); } private int getMarginBlockquote() { return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE); }
Returns the minimum number of newline characters needed before and after a given block-level element.
Params:
  • flag – the corresponding option flag defined in Html of a block-level element
/** * 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 Html} of a block-level element */
private int getMargin(int flag) { if ((flag & mFlags) != 0) { return 1; } return 2; } private static 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 static void startBlockElement(Editable text, Attributes attributes, int margin) { final int len = text.length(); 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 static void endBlockElement(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(text, a, new AlignmentSpan.Standard(a.mAlignment)); } } private static 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 static void endLi(Editable text) { endCssStyle(text); endBlockElement(text); end(text, Bullet.class, new BulletSpan()); } private void startBlockquote(Editable text, Attributes attributes) { startBlockElement(text, attributes, getMarginBlockquote()); start(text, new Blockquote()); } private static void endBlockquote(Editable text) { endBlockElement(text); end(text, Blockquote.class, new QuoteSpan()); } private void startHeading(Editable text, Attributes attributes, int level) { startBlockElement(text, attributes, getMarginHeading()); start(text, new Heading(level)); } private static void endHeading(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(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]), new StyleSpan(Typeface.BOLD)); } endBlockElement(text); } private static <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 static void setSpanFromMark(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) { text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private static void start(Editable text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } private static void end(Editable text, Class kind, Object repl) { int len = text.length(); Object obj = getLast(text, kind); if (obj != null) { setSpanFromMark(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()); } } } } private static void endCssStyle(Editable text) { Strikethrough s = getLast(text, Strikethrough.class); if (s != null) { setSpanFromMark(text, s, new StrikethroughSpan()); } Background b = getLast(text, Background.class); if (b != null) { setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor)); } Foreground f = getLast(text, Foreground.class); if (f != null) { setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor)); } } private static void startImg(Editable 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 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 static void endFont(Editable text) { Font font = getLast(text, Font.class); if (font != null) { setSpanFromMark(text, font, new TypefaceSpan(font.mFace)); } Foreground foreground = getLast(text, Foreground.class); if (foreground != null) { setSpanFromMark(text, foreground, new ForegroundColorSpan(foreground.mForegroundColor)); } } private static void startA(Editable text, Attributes attributes) { String href = attributes.getValue("", "href"); start(text, new Href(href)); } private static void endA(Editable text) { Href h = getLast(text, Href.class); if (h != null) { if (h.mHref != null) { setSpanFromMark(text, h, new URLSpan((h.mHref))); } } } private int getHtmlColor(String color) { if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS) == Html.FROM_HTML_OPTION_USE_CSS_COLORS) { Integer i = sColorMap.get(color.toLowerCase(Locale.US)); if (i != null) { return i; } } return Color.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 { public String mFace; public Font(String face) { mFace = face; } } private static class Href { public String mHref; public Href(String href) { mHref = href; } } private static class Foreground { private int mForegroundColor; public Foreground(int foregroundColor) { mForegroundColor = foregroundColor; } } private static class Background { private int mBackgroundColor; public Background(int backgroundColor) { mBackgroundColor = backgroundColor; } } private static class Heading { private int mLevel; public Heading(int level) { mLevel = level; } } private static class Newline { private int mNumNewlines; public Newline(int numNewlines) { mNumNewlines = numNewlines; } } private static class Alignment { private Layout.Alignment mAlignment; public Alignment(Layout.Alignment alignment) { mAlignment = alignment; } } }