/*
* Copyright (C) 2015-2017 Emanuel Moecklin
*
* 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.onegravity.rteditor.converter;
import android.annotation.SuppressLint;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
import com.onegravity.rteditor.api.RTMediaFactory;
import com.onegravity.rteditor.api.format.RTFormat;
import com.onegravity.rteditor.api.format.RTHtml;
import com.onegravity.rteditor.api.format.RTSpanned;
import com.onegravity.rteditor.api.media.RTAudio;
import com.onegravity.rteditor.api.media.RTImage;
import com.onegravity.rteditor.api.media.RTVideo;
import com.onegravity.rteditor.converter.tagsoup.HTMLSchema;
import com.onegravity.rteditor.converter.tagsoup.Parser;
import com.onegravity.rteditor.fonts.FontManager;
import com.onegravity.rteditor.fonts.RTTypeface;
import com.onegravity.rteditor.spans.AbsoluteSizeSpan;
import com.onegravity.rteditor.spans.AlignmentSpan;
import com.onegravity.rteditor.spans.BackgroundColorSpan;
import com.onegravity.rteditor.spans.BoldSpan;
import com.onegravity.rteditor.spans.BulletSpan;
import com.onegravity.rteditor.spans.ForegroundColorSpan;
import com.onegravity.rteditor.spans.ImageSpan;
import com.onegravity.rteditor.spans.IndentationSpan;
import com.onegravity.rteditor.spans.ItalicSpan;
import com.onegravity.rteditor.spans.LinkSpan;
import com.onegravity.rteditor.spans.NumberSpan;
import com.onegravity.rteditor.spans.StrikethroughSpan;
import com.onegravity.rteditor.spans.SubscriptSpan;
import com.onegravity.rteditor.spans.SuperscriptSpan;
import com.onegravity.rteditor.spans.TypefaceSpan;
import com.onegravity.rteditor.spans.UnderlineSpan;
import com.onegravity.rteditor.utils.Helper;
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.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Converts html to Spanned text using TagSoup
*/
public class ConverterHtmlToSpanned implements ContentHandler {
private static final float[] HEADER_SIZES = {
1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
};
private String mSource;
private RTMediaFactory<? extends RTImage, ? extends RTAudio, ? extends RTVideo> mMediaFactory;
private Parser mParser;
private SpannableStringBuilder mResult;
private Stack<AccumulatedParagraphStyle> mParagraphStyles = new Stack<AccumulatedParagraphStyle>();
/**
* If this is set to True we ignore all characters till it's set to false again.
* This way be can ignore e.g. style information or even the whole header
*/
private boolean mIgnoreContent;
private static final Set<String> sIgnoreTags = new HashSet<String>();
static {
sIgnoreTags.add("header");
sIgnoreTags.add("style");
sIgnoreTags.add("meta");
}
/**
* Lazy initialization holder for HTML parser. This class will
* a) be pre-loaded by zygote, or
* b) not loaded until absolutely necessary.
*/
private static class HtmlParser {
private static final HTMLSchema SCHEMA = new HTMLSchema();
}
public RTSpanned convert(RTHtml<? extends RTImage, ? extends RTAudio, ? extends RTVideo> input,
RTMediaFactory<? extends RTImage, ? extends RTAudio, ? extends RTVideo> mediaFactory) {
mSource = input.getText();
mMediaFactory = mediaFactory;
mParser = new Parser();
try {
mParser.setProperty(Parser.schemaProperty, HtmlParser.SCHEMA);
} catch (SAXNotRecognizedException shouldNotHappen) {
throw new RuntimeException(shouldNotHappen);
} catch (SAXNotSupportedException shouldNotHappen) {
throw new RuntimeException(shouldNotHappen);
}
mResult = new SpannableStringBuilder();
mIgnoreContent = false;
mParagraphStyles.clear();
mParser.setContentHandler(this);
try {
mParser.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);
}
// remove trailing line breaks
removeTrailingLineBreaks();
// replace all TemporarySpans by the "real" spans
for (TemporarySpan span : mResult.getSpans(0, mResult.length(), TemporarySpan.class)) {
span.swapIn(mResult);
}
return new RTSpanned(mResult);
}
private void removeTrailingLineBreaks() {
int end = mResult.length();
while (end > 0 && mResult.charAt(end - 1) == '\n') {
end--;
}
if (end < mResult.length()) {
mResult = SpannableStringBuilder.valueOf(mResult.subSequence(0, end));
}
}
// ****************************************** org.xml.sax.ContentHandler *******************************************
@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 {
if (mIgnoreContent) return;
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 = mResult.length();
if (len == 0) {
pred = '\n';
} else {
pred = mResult.charAt(len - 1);
}
} else {
pred = sb.charAt(len - 1);
}
if (pred != ' ' && pred != '\n') {
sb.append(' ');
}
} else {
sb.append(c);
}
}
mResult.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 {
}
// ****************************************** Handle Tags *******************************************
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 omit the line breaks when we handle the close tag.
} else if (tag.equalsIgnoreCase("p")) {
handleP();
} else if (tag.equalsIgnoreCase("div")) {
startDiv(attributes);
} else if (tag.equalsIgnoreCase("ul")) {
startList(false, attributes);
} else if (tag.equalsIgnoreCase("ol")) {
startList(true, attributes);
} else if (tag.equalsIgnoreCase("li")) {
startList(attributes);
} else if (tag.equalsIgnoreCase("strong")) {
start(new Bold());
} else if (tag.equalsIgnoreCase("b")) {
start(new Bold());
} else if (tag.equalsIgnoreCase("em")) {
start(new Italic());
} else if (tag.equalsIgnoreCase("cite")) {
start(new Italic());
} else if (tag.equalsIgnoreCase("dfn")) {
start(new Italic());
} else if (tag.equalsIgnoreCase("i")) {
start(new Italic());
} else if (tag.equalsIgnoreCase("strike")) {
start(new Strikethrough());
} else if (tag.equalsIgnoreCase("del")) {
start(new Strikethrough());
} else if (tag.equalsIgnoreCase("big")) {
start(new Big());
} else if (tag.equalsIgnoreCase("small")) {
start(new Small());
} else if (tag.equalsIgnoreCase("font")) {
startFont(attributes);
} else if (tag.equalsIgnoreCase("blockquote")) {
handleP();
start(new Blockquote());
} else if (tag.equalsIgnoreCase("tt")) {
start(new Monospace());
} else if (tag.equalsIgnoreCase("a")) {
startAHref(attributes);
} else if (tag.equalsIgnoreCase("u")) {
start(new Underline());
} else if (tag.equalsIgnoreCase("sup")) {
start(new Super());
} else if (tag.equalsIgnoreCase("sub")) {
start(new Sub());
} else if (tag.length() == 2 &&
Character.toLowerCase(tag.charAt(0)) == 'h' &&
tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
handleP();
start(new Header(tag.charAt(1) - '1'));
} else if (tag.equalsIgnoreCase("img")) {
startImg(attributes);
} else if (tag.equalsIgnoreCase("video")) {
startVideo(attributes);
} else if (tag.equalsIgnoreCase("embed")) {
startAudio(attributes);
} else if (sIgnoreTags.contains(tag.toLowerCase(Locale.getDefault()))) {
mIgnoreContent = true;
}
}
private void handleEndTag(String tag) {
if (tag.equalsIgnoreCase("br")) {
handleBr();
} else if (tag.equalsIgnoreCase("p")) {
handleP();
} else if (tag.equalsIgnoreCase("div")) {
endDiv();
} else if (tag.equalsIgnoreCase("ul")) {
endList(false);
} else if (tag.equalsIgnoreCase("ol")) {
endList(true);
} else if (tag.equalsIgnoreCase("li")) {
endList();
} else if (tag.equalsIgnoreCase("strong")) {
end(Bold.class, new BoldSpan());
} else if (tag.equalsIgnoreCase("b")) {
end(Bold.class, new BoldSpan());
} else if (tag.equalsIgnoreCase("em")) {
end(Italic.class, new ItalicSpan());
} else if (tag.equalsIgnoreCase("cite")) {
end(Italic.class, new ItalicSpan());
} else if (tag.equalsIgnoreCase("dfn")) {
end(Italic.class, new ItalicSpan());
} else if (tag.equalsIgnoreCase("i")) {
end(Italic.class, new ItalicSpan());
} else if (tag.equalsIgnoreCase("strike")) {
end(Strikethrough.class, new StrikethroughSpan());
} else if (tag.equalsIgnoreCase("del")) {
end(Strikethrough.class, new StrikethroughSpan());
} else if (tag.equalsIgnoreCase("big")) {
int size = Helper.convertPxToSp(32);
end(Big.class, new AbsoluteSizeSpan(size));
} else if (tag.equalsIgnoreCase("small")) {
int size = Helper.convertPxToSp(14);
end(Small.class, new AbsoluteSizeSpan(size));
} else if (tag.equalsIgnoreCase("font")) {
endFont();
} else if (tag.equalsIgnoreCase("blockquote")) {
handleP();
end(Blockquote.class, new QuoteSpan());
} else if (tag.equalsIgnoreCase("a")) {
endAHref();
} else if (tag.equalsIgnoreCase("u")) {
end(Underline.class, new UnderlineSpan());
} else if (tag.equalsIgnoreCase("sup")) {
end(Super.class, new SuperscriptSpan());
} else if (tag.equalsIgnoreCase("sub")) {
end(Sub.class, new SubscriptSpan());
} else if (tag.length() == 2 &&
Character.toLowerCase(tag.charAt(0)) == 'h' &&
tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
handleP();
endHeader();
} else if (sIgnoreTags.contains(tag.toLowerCase(Locale.getDefault()))) {
mIgnoreContent = false;
}
}
private void startDiv(Attributes attributes) {
String sAlign = attributes.getValue("align");
int len = mResult.length();
mResult.setSpan(new Div(sAlign), len, len, Spanned.SPAN_MARK_MARK);
}
private void endDiv() {
int end = mResult.length();
Object obj = getLast(mResult, Div.class);
int start = mResult.getSpanStart(obj);
mResult.removeSpan(obj);
if (start != end) {
if (!checkDuplicateSpan(mResult, start, AlignmentSpan.class)) {
Div divObj = (Div) obj;
Layout.Alignment align = divObj.mAlign.equalsIgnoreCase("center") ? Layout.Alignment.ALIGN_CENTER :
divObj.mAlign.equalsIgnoreCase("right") ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL;
if (align != null) {
if (mResult.charAt(end - 1) != '\n') {
// yes we need that linefeed, or we will get crashes
mResult.append('\n');
}
// use SPAN_EXCLUSIVE_EXCLUSIVE here, will be replaced later anyway when the cleanup function is called
boolean isRTL = Helper.isRTL(mResult, start, end);
mResult.setSpan(new AlignmentSpan(align, isRTL), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
/**
* Handles OL and UL start tags
*/
private void startList(boolean isOrderedList, Attributes attributes) {
boolean isIndentation = isIndentation(attributes);
ParagraphType newType = isIndentation && isOrderedList ? ParagraphType.INDENTATION_OL :
isIndentation && !isOrderedList ? ParagraphType.INDENTATION_UL :
isOrderedList ? ParagraphType.NUMBERING :
ParagraphType.BULLET;
AccumulatedParagraphStyle currentStyle = mParagraphStyles.isEmpty() ? null : mParagraphStyles.peek();
if (currentStyle == null) {
// no previous style found -> create new AccumulatedParagraphStyle with indentations of 1
AccumulatedParagraphStyle newStyle = new AccumulatedParagraphStyle(newType, 1, 1);
mParagraphStyles.push(newStyle);
} else if (currentStyle.getType() == newType) {
// same style found -> increase indentations by 1
currentStyle.setAbsoluteIndent(currentStyle.getAbsoluteIndent() + 1);
currentStyle.setRelativeIndent(currentStyle.getRelativeIndent() + 1);
} else {
// different style found -> create new AccumulatedParagraphStyle with incremented indentations
AccumulatedParagraphStyle newStyle = new AccumulatedParagraphStyle(newType, currentStyle.getAbsoluteIndent() + 1, 1);
mParagraphStyles.push(newStyle);
}
}
/**
* Handles OL and UL end tags
*/
private void endList(boolean orderedList) {
if (!mParagraphStyles.isEmpty()) {
AccumulatedParagraphStyle style = mParagraphStyles.peek();
ParagraphType type = style.getType();
if ((orderedList && (type.isNumbering() || type == ParagraphType.INDENTATION_OL)) ||
(!orderedList && (type.isBullet() || type == ParagraphType.INDENTATION_UL))) {
// the end tag matches the current style
int indent = style.getRelativeIndent();
if (indent > 1) {
style.setRelativeIndent(indent - 1);
style.setAbsoluteIndent(style.getAbsoluteIndent() - 1);
} else {
mParagraphStyles.pop();
}
} else {
// the end tag doesn't match the current style
mParagraphStyles.pop();
endList(orderedList); // find the next matching style
}
}
}
/**
* Handles LI tags
*/
private void startList(Attributes attributes) {
List listTag = null;
if (!mParagraphStyles.isEmpty()) {
AccumulatedParagraphStyle currentStyle = mParagraphStyles.peek();
ParagraphType type = currentStyle.getType();
int indent = currentStyle.getAbsoluteIndent();
boolean isIndentation = isIndentation(attributes);
if (type.isIndentation() || isIndentation) {
listTag = new UL(indent, true);
} else if (type.isNumbering()) {
listTag = new OL(indent, false);
} else if (type.isBullet()) {
listTag = new UL(indent, false);
}
} else {
listTag = new UL(0, false);
}
if (listTag != null) start(listTag);
}
/**
* Handles LI tags
*/
private void endList() {
List list = (List) getLast(List.class);
if (list != null) {
if (mResult.length() == 0 || mResult.charAt(mResult.length() - 1) != '\n') {
mResult.append('\n');
}
int start = mResult.getSpanStart(list);
int end = mResult.length();
int nrOfIndents = list.mNrOfIndents;
if (!list.mIsIndentation) {
nrOfIndents--;
int margin = Helper.getLeadingMarging();
// use SPAN_EXCLUSIVE_EXCLUSIVE here, will be replaced later anyway when the cleanup function is called
Object span = list instanceof UL ?
new BulletSpan(margin, start == end, false, false) :
new NumberSpan(1, margin, start == end, false, false);
mResult.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (nrOfIndents > 0) {
int margin = nrOfIndents * Helper.getLeadingMarging();
// use SPAN_EXCLUSIVE_EXCLUSIVE here, will be replaced later anyway when the cleanup function is called
IndentationSpan span = new IndentationSpan(margin, start == end, false, false);
mResult.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mResult.removeSpan(list);
}
}
private boolean isIndentation(Attributes attributes) {
String style = attributes.getValue("style");
return style != null && style.toLowerCase(Locale.US).contains("list-style-type:none");
}
private boolean checkDuplicateSpan(SpannableStringBuilder text, int where, Class<?> kind) {
Object[] spans = text.getSpans(where, where, kind);
if (spans != null && spans.length > 0) {
for (int i = 0; i < spans.length; i++) {
if (text.getSpanStart(spans[i]) == where) {
return true;
}
}
}
return false;
}
private 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);
return objs.length == 0 ? null : objs[objs.length - 1];
}
private void handleP() {
int len = mResult.length();
if (len >= 1 && mResult.charAt(len - 1) == '\n') {
if (len < 2 || mResult.charAt(len - 2) != '\n') {
mResult.append("\n");
}
} else if (len != 0) {
mResult.append("\n\n");
}
}
private void handleBr() {
mResult.append("\n");
}
private Object getLast(Class<? extends Object> kind) {
/*
* This knows that the last returned object from getSpans()
* will be the most recently added.
*/
Object[] objs = mResult.getSpans(0, mResult.length(), kind);
return objs.length == 0 ? null : objs[objs.length - 1];
}
private void start(Object mark) {
int len = mResult.length();
mResult.setSpan(mark, len, len, Spanned.SPAN_MARK_MARK);
}
private void end(Class<? extends Object> kind, Object repl) {
int len = mResult.length();
Object obj = getLast(kind);
int where = mResult.getSpanStart(obj);
mResult.removeSpan(obj);
if (where != len) {
// Note: use SPAN_EXCLUSIVE_EXCLUSIVE, the TemporarySpan will be replaced by a SPAN_EXCLUSIVE_INCLUSIVE span
mResult.setSpan(new TemporarySpan(repl), where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private void startImg(Attributes attributes) {
int len = mResult.length();
String src = attributes.getValue("", "src");
RTImage image = mMediaFactory.createImage(src);
if (image != null && image.exists()) {
String path = image.getFilePath(RTFormat.SPANNED);
File file = new File(path);
if (file.isDirectory()) {
// there were crashes when an image was a directory all of a sudden...
// the root cause is unknown and this is a desparate work around
return;
}
// Unicode Character 'OBJECT REPLACEMENT CHARACTER' (U+FFFC)
// see http://www.fileformat.info/info/unicode/char/fffc/index.htm
mResult.append("\uFFFC");
ImageSpan imageSpan = new ImageSpan(image, true);
mResult.setSpan(imageSpan, len, len + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private void startVideo(Attributes attributes) {
}
private void startAudio(Attributes attributes) {
}
/*
* Examples:
* <font style="font-size:25px;background-color:#00ff00;color:#ff0000">This is heading 1</font>
* <font style="font-size:50px;background-color:#0000FF;color:#FFFF00">This is heading 2</font>
*/
private static final Pattern FONT_SIZE = Pattern.compile("\\d+");
private static final Pattern FONT_COLOR = Pattern.compile("#[a-f0-9]+");
private void startFont(Attributes attributes) {
int size = Integer.MIN_VALUE;
String fgColor = null;
String bgColor = null;
String fontName = null;
String style = attributes.getValue("", "style");
if (style != null) {
for (String part : style.toLowerCase(Locale.ENGLISH).split(";")) {
if (part.startsWith("font-size")) {
Matcher matcher = FONT_SIZE.matcher(part);
if (matcher.find(0)) {
int start = matcher.start();
int end = matcher.end();
try {
size = Integer.parseInt(part.substring(start, end));
} catch (NumberFormatException ignore) {
}
}
} else if (part.startsWith("color")) {
Matcher matcher = FONT_COLOR.matcher(part);
if (matcher.find(0)) {
int start = matcher.start();
int end = matcher.end();
fgColor = part.substring(start, end);
}
} else if (part.startsWith("background-color")) {
Matcher matcher = FONT_COLOR.matcher(part);
if (matcher.find(0)) {
int start = matcher.start();
int end = matcher.end();
bgColor = part.substring(start, end);
}
}
}
}
fontName = attributes.getValue("", "face");
int len = mResult.length();
Font font = new Font()
.setSize(size)
.setFGColor(fgColor)
.setBGColor(bgColor)
.setFontFace(fontName);
mResult.setSpan(font, len, len, Spanned.SPAN_MARK_MARK);
}
private void endFont() {
int len = mResult.length();
Object obj = getLast(Font.class);
int where = mResult.getSpanStart(obj);
mResult.removeSpan(obj);
if (where != len) {
Font font = (Font) obj;
// font type face
if (font.hasFontFace()) {
// Note: use SPAN_EXCLUSIVE_EXCLUSIVE, the TemporarySpan will be replaced by a SPAN_EXCLUSIVE_INCLUSIVE span
RTTypeface typeface = FontManager.getTypeface(font.mFontFace);
if (typeface != null) {
TemporarySpan span = new TemporarySpan(new TypefaceSpan(typeface));
mResult.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
// text size
if (font.hasSize()) {
// Note: use SPAN_EXCLUSIVE_EXCLUSIVE, the TemporarySpan will later be replaced by a SPAN_EXCLUSIVE_INCLUSIVE span
int size = Helper.convertPxToSp(font.mSize);
TemporarySpan span = new TemporarySpan(new AbsoluteSizeSpan(size));
mResult.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// font color
if (font.hasFGColor()) {
int c = getHtmlColor(font.mFGColor);
if (c != -1) {
// Note: use SPAN_EXCLUSIVE_EXCLUSIVE, the TemporarySpan will be replaced by a SPAN_EXCLUSIVE_INCLUSIVE span
TemporarySpan span = new TemporarySpan(new ForegroundColorSpan(c | 0xFF000000));
mResult.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
// font background color
if (font.hasBGColor()) {
int c = getHtmlColor(font.mBGColor);
if (c != -1) {
// Note: use SPAN_EXCLUSIVE_EXCLUSIVE, the TemporarySpan will be replaced by a SPAN_EXCLUSIVE_INCLUSIVE span
TemporarySpan span = new TemporarySpan(new BackgroundColorSpan(c | 0xFF000000));
mResult.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
private void startAHref(Attributes attributes) {
String href = attributes.getValue("", "href");
int len = mResult.length();
mResult.setSpan(new Href(href), len, len, Spanned.SPAN_MARK_MARK);
}
private void endAHref() {
int len = mResult.length();
Object obj = getLast(Href.class);
int where = mResult.getSpanStart(obj);
mResult.removeSpan(obj);
if (where != len) {
Href h = (Href) obj;
if (h.mHref != null) {
mResult.setSpan(new LinkSpan(h.mHref), where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
private void endHeader() {
int len = mResult.length();
Object obj = getLast(Header.class);
int where = mResult.getSpanStart(obj);
mResult.removeSpan(obj);
// Back off not to change only the text, not the blank line.
while (len > where && mResult.charAt(len - 1) == '\n') {
len--;
}
if (where != len) {
Header h = (Header) obj;
mResult.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]), where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mResult.setSpan(new BoldSpan(), where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
// ****************************************** Helper Data Structures *******************************************
/*
* While the spanned text is build we need to use SPAN_EXCLUSIVE_EXCLUSIVE instead of SPAN_EXCLUSIVE_INCLUSIVE
* or each span would expand to the end of the text as we append more text.
* Therefore we use a TemporarySpan which will be replaced by the "real" span once the full spanned text is built.
*/
private static class TemporarySpan {
Object mSpan;
TemporarySpan(Object span) {
mSpan = span;
}
void swapIn(SpannableStringBuilder builder) {
int start = builder.getSpanStart(this);
int end = builder.getSpanEnd(this);
builder.removeSpan(this);
if (start >= 0 && end > start && end <= builder.length()) {
builder.setSpan(mSpan, start, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
}
}
}
private static class Div {
String mAlign = "left";
Div(String align) {
if (align != null) mAlign = align;
}
}
private static class Bold {}
private static class Italic {}
private static class Underline {}
private static class Strikethrough {}
private static class Super {}
private static class Sub {}
private static class Big {}
private static class Small {}
private static class Monospace {}
private static class Blockquote {}
private abstract static class List {
int mNrOfIndents;
boolean mIsIndentation;
List(int nrOfIndents, boolean isIndentation) {
mNrOfIndents = nrOfIndents;
mIsIndentation = isIndentation;
}
}
private static class UL extends List {
UL(int nrOfIndents, boolean isIndentation) {
super(nrOfIndents, isIndentation);
}
}
private static class OL extends List {
OL(int nrOfIndents, boolean isIndentation) {
super(nrOfIndents, isIndentation);
}
}
private static class Font {
int mSize = Integer.MIN_VALUE;
String mFGColor;
String mBGColor;
String mFontFace;
Font setSize(int size) {
mSize = size;
return this;
}
Font setFGColor(String color) {
mFGColor = color;
return this;
}
Font setBGColor(String color) {
mBGColor = color;
return this;
}
private Font setFontFace(String fontFace) {
mFontFace = fontFace;
return this;
}
boolean hasSize() {
return mSize > 0;
}
boolean hasFGColor() {
return !TextUtils.isEmpty(mFGColor);
}
boolean hasBGColor() {
return !TextUtils.isEmpty(mBGColor);
}
boolean hasFontFace() {
return !TextUtils.isEmpty(mFontFace);
}
}
private static class Href {
String mHref;
Href(String href) {
mHref = href;
}
}
private static class Header {
int mLevel;
Header(int level) {
mLevel = level;
}
}
// ****************************************** Color Methods *******************************************
private static HashMap<String, Integer> COLORS = new HashMap<String, Integer>();
static {
COLORS.put("aqua", 0x00FFFF);
COLORS.put("black", 0x000000);
COLORS.put("blue", 0x0000FF);
COLORS.put("fuchsia", 0xFF00FF);
COLORS.put("green", 0x008000);
COLORS.put("grey", 0x808080);
COLORS.put("lime", 0x00FF00);
COLORS.put("maroon", 0x800000);
COLORS.put("navy", 0x000080);
COLORS.put("olive", 0x808000);
COLORS.put("purple", 0x800080);
COLORS.put("red", 0xFF0000);
COLORS.put("silver", 0xC0C0C0);
COLORS.put("teal", 0x008080);
COLORS.put("white", 0xFFFFFF);
COLORS.put("yellow", 0xFFFF00);
}
/**
* 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.
*/
@SuppressLint("DefaultLocale")
private static int getHtmlColor(String color) {
Integer i = COLORS.get(color.toLowerCase());
if (i != null) {
return i;
} else {
try {
return convertValueToInt(color, -1);
} catch (NumberFormatException nfe) {
return -1;
}
}
}
private static final int convertValueToInt(CharSequence charSeq, int defaultValue) {
if (null == charSeq)
return defaultValue;
String nm = charSeq.toString();
// XXX This code is copied from Integer.decode() so we don't
// have to instantiate an Integer!
int sign = 1;
int index = 0;
int len = nm.length();
int base = 10;
if ('-' == nm.charAt(0)) {
sign = -1;
index++;
}
if ('0' == nm.charAt(index)) {
// Quick check for a zero by itself
if (index == (len - 1))
return 0;
char c = nm.charAt(index + 1);
if ('x' == c || 'X' == c) {
index += 2;
base = 16;
} else {
index++;
base = 8;
}
} else if ('#' == nm.charAt(index)) {
index++;
base = 16;
}
return Integer.parseInt(nm.substring(index), base) * sign;
}
}