/*
* Overchan Android (Meta Imageboard Client)
* Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//исходная версия - android-22
package nya.miku.wishmaster.ui.presentation;
import android.graphics.Color;
import nya.miku.wishmaster.ui.CompatibilityUtils;
import nya.miku.wishmaster.ui.presentation.ClickableURLSpan.URLSpanClickListener;
import nya.miku.wishmaster.ui.theme.ThemeUtils;
import nya.miku.wishmaster.ui.theme.ThemeUtils.ThemeColors;
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.support.v4.content.res.ResourcesCompat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
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 java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Парсер HTML в Spanned-строки для отображения в TextView.<br>
* Основан на исходном коде {@link android.text.Html} (использует TagSoup) с изменениями
* @author miku-nyan
*
*/
public class HtmlParser {
/**
* Создать Spanned строку из html текста.
* @param subject тема (заголовок) сообщения, будет вставлена в начале итоговой Spanned строки.
* Может принимать null или пустую строку, тогда заголовок вставлен не будет
* @param source исходный html текст. Поддерживаются тэги, поддерживаемые {@link android.text.Html#fromHtml(String)}, а также:<ul>
* <li><b><ol></b>, <b><ul></b>, <b><li></b> - списки</li>
* <li><b><s></b>, <b><strike></b>, <b><del></b> - перечёркнутый текст</li>
* <li><b><code></b> - отображается моноширинным шрифтом</li>
* <li><b><blockquote class="unkfunc"></b> - форумная цитата (отображается цветом выбранной темы оформления), выделяется абзацами</li>
* <li><b><span class="unkfunc"></b>, <b><span class="quote"></b> - аналогично предыдущему, не выделяется абзацами</li>
* <li><b><span class="spoiler"></b> - спойлер, затемнённый текст (отображается цветами выбранной темы оформления)</li>
* <li><b><span class="s"></b> - перечёркнутый текст</li>
* <li><b><span class="u"></b> - подчёркнутый текст</li>
* <li><b><font style="..."></b> и <b><span style="..."></b> - CSS-стиль, поддерживаются color и background-color</li>
* <li><b><aibquote></b> и <b><aibspoiler></b> - аналогично <span class="unkfunc"> и <span class="spoiler">.
* Может использоваться, если форум выдаёт текст в нестандартном виде, в этом случае можно заменять (при загрузке и парсинге ответа)
* выделение цитат и спойлеров на данные псевдотэги, для однозначного соответствия</li></ul>
* @param spanClickListener обработчик нажатий на ссылки
* @param imageGetter обработчик загрузки изображений (тэг <img> внутри html текста)
* @param themeColors объект {@link ThemeUtils.ThemeColors} для текущей темы оформления
* @param openSpoilers отображать спойлеры открытыми
* @param referer ссылка на этот текущий пост (для задания referer у ссылок)
* @return объект SpannableStringBuilder
*/
public static SpannableStringBuilder createSpanned(String subject, String source,
URLSpanClickListener spanClickListener, ImageGetter imageGetter, ThemeColors themeColors, boolean openSpoilers, String referer) {
SpannableStringBuilder spanned = fromHtml(subject, source, themeColors, imageGetter, openSpoilers);
replaceUrls(spanned, spanClickListener, themeColors, referer);
if (!openSpoilers) fixSpoilerSpans(spanned, themeColors);
return spanned;
}
/**
* Заменить ссылки (URLSpan) на ClickableURLSpan со своим обработчиком нажатия
* @param listener обработчик нажатий на ссылки
*/
private static void replaceUrls(SpannableStringBuilder builder, URLSpanClickListener listener, ThemeColors themeColors, String referer) {
URLSpan[] spans = builder.getSpans(0, builder.length(), URLSpan.class);
if (spans.length > 0) {
for (URLSpan span : spans) {
ClickableURLSpan.replaceURLSpan(builder, span, themeColors.urlLinkForeground).setOnClickListener(listener).setReferer(referer);
}
}
}
/**
* Исправить расположение SpoilerSpan и ForegroundColorSpan цвета ссылок для корректной работы ссылок под спойлерами
*/
private static void fixSpoilerSpans(SpannableStringBuilder builder, ThemeColors themeColors) {
SpoilerSpan[] spoilers = builder.getSpans(0, builder.length(), SpoilerSpan.class);
for (SpoilerSpan span : spoilers) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
builder.removeSpan(span);
builder.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
ClickableURLSpan[] urls = builder.getSpans(0, builder.length(), ClickableURLSpan.class);
for (ClickableURLSpan span : urls) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
builder.setSpan(new ForegroundColorSpan(themeColors.urlLinkForeground), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
/**
* Обработчик-загрузчик картинок в HTML тэге <img>.
*/
public static interface ImageGetter {
/**
* Метод вызывается, когда HTML парсер встречает тэг <img>.
* @param source строка из аттрибута "src"
* @return должен вернуть представление картинки как Drawable или <code>null</code> для замены на общую картинку (ошибки).
* Убедитесь, что был вызван setBounds() на Drawable (если его bounds не были установлены)
*/
public Drawable getDrawable(String source);
}
private HtmlParser() { }
/**
* 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 HtmlParserHolder {
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.
*/
private static SpannableStringBuilder fromHtml(String subject, String source, ThemeColors colors, ImageGetter imageGetter, boolean openSpoilers) {
Parser parser = new Parser();
try {
parser.setProperty(Parser.schemaProperty, HtmlParserHolder.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(subject, source, colors, imageGetter, openSpoilers, parser);
return converter.convert();
}
}
class HtmlToSpannedConverter implements ContentHandler {
private static final float[] HEADER_SIZES = {
1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
};
private static final Pattern CSS_STYLE_COLOR_RGB_PATTERN = Pattern.compile(".*?color: ?rgb\\((\\d+), ?(\\d+), ?(\\d+)\\).*");
private static final Pattern CSS_STYLE_COLOR_COMMON_PATTERN = Pattern.compile(".*?color: ?(#?\\w+).*");
private String mSource;
private XMLReader mReader;
private SpannableStringBuilder mSpannableStringBuilder;
//костыли для правильной обработки (обрезки) <p>...</p> в начале и в конце
private int mStartLength = 0; //длина subject + '\n'
private int[] mLastPTagLength = new int[] {-1, -1}; //2 целых числа {before, after}, длина до и после обработки последнего тэга (</p>)
private LinkedList<Object> mListTags = new LinkedList<>();
private ThemeColors mColors;
private boolean mOpenSpoilers;
private HtmlParser.ImageGetter mImageGetter;
public HtmlToSpannedConverter(String subject, String source, ThemeColors colors, HtmlParser.ImageGetter imageGetter, boolean openSpoilers,
Parser parser) {
mSource = source;
mSpannableStringBuilder = new SpannableStringBuilder();
if (!TextUtils.isEmpty(subject)) {
mSpannableStringBuilder.append(subject);
int len = mSpannableStringBuilder.length();
mSpannableStringBuilder.setSpan(new RelativeSizeSpan(1.25f), 0, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mSpannableStringBuilder.setSpan(new StyleSpan(Typeface.BOLD), 0, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (colors != null) {
mSpannableStringBuilder.setSpan(new ForegroundColorSpan(colors.subjectForeground), 0, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mSpannableStringBuilder.append('\n');
mStartLength = mSpannableStringBuilder.length();
}
mColors = colors;
mOpenSpoilers = openSpoilers;
mImageGetter = imageGetter;
mReader = parser;
}
public SpannableStringBuilder 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);
}
}
if (mLastPTagLength[0] != mLastPTagLength[1] && mLastPTagLength[1] == mSpannableStringBuilder.length()) {
mSpannableStringBuilder.delete(mLastPTagLength[0], mLastPTagLength[1]);
}
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, mStartLength, mLastPTagLength);
} else if (tag.equalsIgnoreCase("div")) {
handleP(mSpannableStringBuilder, mStartLength, mLastPTagLength);
} 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("s")) {
start(mSpannableStringBuilder, new Strike());
} else if (tag.equalsIgnoreCase("strike")) {
start(mSpannableStringBuilder, new Strike());
} else if (tag.equalsIgnoreCase("del")) {
start(mSpannableStringBuilder, new Strike());
} 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")) {
String classAttr = attributes.getValue("", "class");
handleP(mSpannableStringBuilder, mStartLength, mLastPTagLength);
start(mSpannableStringBuilder, new Blockquote(classAttr != null && classAttr.equals("unkfunc")));
} else if (tag.equalsIgnoreCase("tt")) {
start(mSpannableStringBuilder, new Monospace());
} else if (tag.equalsIgnoreCase("code")) {
start(mSpannableStringBuilder, new Monospace());
} else if (tag.equalsIgnoreCase("ul")) {
mListTags.addFirst(new UlTag());
} else if (tag.equalsIgnoreCase("ol")) {
mListTags.addFirst(new OlTag());
} else if (tag.equalsIgnoreCase("li")) {
handleLi(mSpannableStringBuilder, mListTags.peek(), mListTags.size());
} else if (tag.equalsIgnoreCase("tr")) {
handleTr(mSpannableStringBuilder, true);
} else if (tag.equalsIgnoreCase("td")) {
handleTd(mSpannableStringBuilder, true);
} 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, mStartLength, mLastPTagLength);
start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
} else if (tag.equalsIgnoreCase("img")) {
startImg(mSpannableStringBuilder, attributes, mImageGetter);
} else if (tag.equalsIgnoreCase("span")) {
startSpan(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("aibquote")) {
start(mSpannableStringBuilder, new Aibquote());
} else if (tag.equalsIgnoreCase("aibspoiler")) {
start(mSpannableStringBuilder, new Aibspoiler());
}/* 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, mStartLength, mLastPTagLength);
} else if (tag.equalsIgnoreCase("div")) {
handleP(mSpannableStringBuilder, mStartLength, mLastPTagLength);
} 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("s")) {
end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan());
} else if (tag.equalsIgnoreCase("strike")) {
end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan());
} else if (tag.equalsIgnoreCase("del")) {
end(mSpannableStringBuilder, Strike.class, new StrikethroughSpan());
} 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, mStartLength, mLastPTagLength);
endBlockquote(mSpannableStringBuilder, mColors);
} else if (tag.equalsIgnoreCase("tt")) {
end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace"));
} else if (tag.equalsIgnoreCase("code")) {
end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace"));
} else if (tag.equalsIgnoreCase("ul")) {
if (!mListTags.isEmpty()) mListTags.removeFirst();
} else if (tag.equalsIgnoreCase("ol")) {
if (!mListTags.isEmpty()) mListTags.removeFirst();
} else if (tag.equalsIgnoreCase("li")) {
//обрабатывается только открывающийся <li>
} else if (tag.equalsIgnoreCase("tr")) {
handleTr(mSpannableStringBuilder, false);
} else if (tag.equalsIgnoreCase("td")) {
handleTd(mSpannableStringBuilder, false);
} 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, mStartLength, mLastPTagLength);
endHeader(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("span")) {
endSpan(mSpannableStringBuilder, mColors, mOpenSpoilers);
} else if (tag.equalsIgnoreCase("aibquote")) {
end(mSpannableStringBuilder, Aibquote.class, new ForegroundColorSpan(mColors != null ? mColors.quoteForeground : Color.GREEN));
} else if (tag.equalsIgnoreCase("aibspoiler")) {
endAibspoiler(mSpannableStringBuilder, mColors, mOpenSpoilers);
}/* else if (mTagHandler != null) {
mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
}*/
}
private static void handleP(SpannableStringBuilder text, int startLength, int[] lastPTagLengthRefs) {
lastPTagLengthRefs[0] = text.length();
int len = text.length() - startLength;
if (len >= 1 && text.charAt(text.length() - 1) == '\n') {
if (len >= 2 && text.charAt(text.length() - 2) == '\n') {
lastPTagLengthRefs[1] = text.length();
return;
}
text.append("\n");
lastPTagLengthRefs[1] = text.length();
return;
}
if (len != 0) {
text.append("\n\n");
}
lastPTagLengthRefs[1] = text.length();
}
private static void handleBr(SpannableStringBuilder text) {
text.append("\n");
}
private static void handleLi(SpannableStringBuilder text, Object tag, int level) {
if (tag == null) return;
int len = text.length();
if (len >= 1 && text.charAt(len - 1) != '\n') text.append("\n");
for (int i=1; i<level; ++i) text.append("\t");
if (tag instanceof OlTag) text.append(Integer.toString(((OlTag) tag).curIndex++) + ". ");
else if (tag instanceof UlTag) text.append("\u2022 ");
}
private static void handleTd(SpannableStringBuilder text, boolean open) {
if (!open) text.append(" | ");
}
private static void handleTr(SpannableStringBuilder text, boolean open) {
text.append(open ? "| " : "\n");
}
private static Object getLast(Spanned text, Class<?> kind) {
/*
* This knows that the last returned object from getSpans()
* will be the most recently added.
*/
Object[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) {
return null;
} else {
return objs[objs.length - 1];
}
}
private static void start(SpannableStringBuilder text, Object mark) {
int len = text.length();
text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
}
private static void end(SpannableStringBuilder text, Class<?> kind, Object repl) {
int len = text.length();
Object obj = getLast(text, kind);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len) {
text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private static void startImg(SpannableStringBuilder text, Attributes attributes, HtmlParser.ImageGetter img) {
String src = attributes.getValue("", "src");
Drawable d = null;
if (img != null) {
d = img.getDrawable(src);
}
if (d == null) {
d = ResourcesCompat.getDrawable(Resources.getSystem(), android.R.drawable.ic_menu_report_image, null);
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");
String style = attributes.getValue("", "style");
int len = text.length();
text.setSpan(new Font(color, face, style), 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 = CompatibilityUtils.getColorStateList(res, colorRes);
text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
int c = ColorHidden.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);
}
if (f.mStyle != null) {
List<Object> styleSpans = parseStyleAttributes(f.mStyle);
for (Object span : styleSpans) {
text.setSpan(span, 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);
}
}
private static void endAibspoiler(SpannableStringBuilder text, ThemeColors colors, boolean openSpoilers) {
int len = text.length();
Object obj = getLast(text, Aibspoiler.class);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len && colors != null) {
if (openSpoilers) {
text.setSpan(new ForegroundColorSpan(colors.spoilerForeground), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new BackgroundColorSpan(colors.spoilerBackground), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
text.setSpan(new SpoilerSpan(colors.spoilerForeground, colors.spoilerBackground), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
private static void endBlockquote(SpannableStringBuilder text, ThemeColors colors) {
int len = text.length();
Object obj = getLast(text, Blockquote.class);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len) {
Blockquote b = (Blockquote) obj;
if (b.mIsUnkfunc) {
if (colors != null) {
text.setSpan(new ForegroundColorSpan(colors.quoteForeground), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
text.setSpan(new QuoteSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
private static void startSpan(SpannableStringBuilder text, Attributes attributes) {
String style = attributes.getValue("", "style");
String classAttr = attributes.getValue("", "class");
boolean isAibquote = classAttr != null && (classAttr.equals("unkfunc") || classAttr.equals("quote"));
boolean isAibspoiler = classAttr != null && classAttr.equals("spoiler");
boolean isUnderline = classAttr != null && classAttr.equals("u");
boolean isStrike = classAttr != null && classAttr.equals("s");
int len = text.length();
text.setSpan(new Span(style, isAibquote, isAibspoiler, isUnderline, isStrike), len, len, Spannable.SPAN_MARK_MARK);
}
private static void endSpan(SpannableStringBuilder text, ThemeColors colors, boolean openSpoilers) {
int len = text.length();
Object obj = getLast(text, Span.class);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len) {
Span s = (Span) obj;
if (s.mStyle != null) {
List<Object> styleSpans = parseStyleAttributes(s.mStyle);
for (Object span : styleSpans) {
text.setSpan(span, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
if (colors != null && s.mIsAibquote) {
text.setSpan(new ForegroundColorSpan(colors.quoteForeground), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (colors != null && s.mIsAibspoiler) {
if (openSpoilers) {
text.setSpan(new ForegroundColorSpan(colors.spoilerForeground), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new BackgroundColorSpan(colors.spoilerBackground), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
text.setSpan(new SpoilerSpan(colors.spoilerForeground, colors.spoilerBackground), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
if (s.mIsUnderline) {
text.setSpan(new UnderlineSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (s.mIsStrike) {
text.setSpan(new StrikethroughSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
private static List<Object> parseStyleAttributes(String style) {
if (TextUtils.isEmpty(style)) return Collections.emptyList();
int foregroundColor = 0, backgroundColor = 0;
String[] cssStyle = style.split("[;]");
for (String s : cssStyle) {
int color = parseColor(s);
if (color != 0) {
if (s.toLowerCase(Locale.US).indexOf("background") != -1) backgroundColor = color; else foregroundColor = color;
}
}
if (foregroundColor == 0 && backgroundColor == 0) {
return Collections.emptyList();
} else if (backgroundColor == 0) {
return Collections.singletonList((Object) new ForegroundColorSpan(foregroundColor));
} else if (foregroundColor == 0) {
return Collections.singletonList((Object) new BackgroundColorSpan(backgroundColor));
} else {
List<Object> spans = new ArrayList<Object>(2);
spans.add(new ForegroundColorSpan(foregroundColor));
spans.add(new BackgroundColorSpan(backgroundColor));
return spans;
}
}
private static int parseColor(String css) {
if (TextUtils.isEmpty(css)) return 0;
try {
Matcher m = CSS_STYLE_COLOR_RGB_PATTERN.matcher(css);
if (m.find() && m.groupCount() == 3) {
int n1 = Integer.parseInt(m.group(1));
int n2 = Integer.parseInt(m.group(2));
int n3 = Integer.parseInt(m.group(3));
return Color.rgb(n1, n2, n3);
}
m = CSS_STYLE_COLOR_COMMON_PATTERN.matcher(css);
if (m.find() && m.groupCount() == 1) {
return Color.parseColor(m.group(1));
}
} catch (Exception e) { /*исключение во время парсинга (некорректное значение или неизвестный цвет)*/ }
return 0;
}
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 Super { }
private static class Sub { }
private static class Strike { }
private static class Aibquote { }
private static class Aibspoiler { }
private static class Font {
public String mColor;
public String mFace;
public String mStyle;
public Font(String color, String face, String style) {
mColor = color;
mFace = face;
mStyle = style;
}
}
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 class Blockquote {
public boolean mIsUnkfunc;
public Blockquote(boolean isUnkfunc) {
mIsUnkfunc = isUnkfunc;
}
}
private static class Span {
private String mStyle;
private boolean mIsAibquote;
private boolean mIsAibspoiler;
private boolean mIsUnderline;
private boolean mIsStrike;
public Span(String style, boolean isAibquote, boolean isAibspoiler, boolean isUnderline, boolean isStrike) {
mStyle = style;
mIsAibquote = isAibquote;
mIsAibspoiler = isAibspoiler;
mIsUnderline = isUnderline;
mIsStrike = isStrike;
}
}
private static class UlTag {}
private static class OlTag {
public int curIndex = 1;
}
}
//скрытый метод android.graphics.Color#getHtmlColor
class ColorHidden {
/**
* 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.
*/
public static int getHtmlColor(String color) {
Integer i = sColorNameMap.get(color.toLowerCase(Locale.US));
if (i != null) {
return i;
} else {
try {
return xmlUtils_convertValueToInt(color, -1);
} catch (NumberFormatException nfe) {
return -1;
}
}
}
private static final HashMap<String, Integer> sColorNameMap;
static {
sColorNameMap = new HashMap<String, Integer>();
sColorNameMap.put("black", Color.BLACK);
sColorNameMap.put("darkgray", Color.DKGRAY);
sColorNameMap.put("gray", Color.GRAY);
sColorNameMap.put("lightgray", Color.LTGRAY);
sColorNameMap.put("white", Color.WHITE);
sColorNameMap.put("red", Color.RED);
sColorNameMap.put("green", Color.GREEN);
sColorNameMap.put("blue", Color.BLUE);
sColorNameMap.put("yellow", Color.YELLOW);
sColorNameMap.put("cyan", Color.CYAN);
sColorNameMap.put("magenta", Color.MAGENTA);
sColorNameMap.put("aqua", 0xFF00FFFF);
sColorNameMap.put("fuchsia", 0xFFFF00FF);
sColorNameMap.put("darkgrey", Color.DKGRAY);
sColorNameMap.put("grey", Color.GRAY);
sColorNameMap.put("lightgrey", Color.LTGRAY);
sColorNameMap.put("lime", 0xFF00FF00);
sColorNameMap.put("maroon", 0xFF800000);
sColorNameMap.put("navy", 0xFF000080);
sColorNameMap.put("olive", 0xFF808000);
sColorNameMap.put("purple", 0xFF800080);
sColorNameMap.put("silver", 0xFFC0C0C0);
sColorNameMap.put("teal", 0xFF008080);
}
public static final int xmlUtils_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!
@SuppressWarnings("unused")
int value;
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;
}
}