/*
* 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/>.
*/
package nya.miku.wishmaster.api.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.ListIterator;
/**
* Парсер HTML тэгов для последующей замены
* @author miku-nyan
*/
public class FastHtmlTagParser {
/**
* Класс определяет объект - пару строк (открытие-закрытие тэга)
* @author miku-nyan
*/
public static class TagsPair {
/** Открывающийся тэг */
public final String openTag;
/** Закрывающийся тэг */
public final String closeTag;
public TagsPair(String openTag, String closeTag) {
if (openTag == null || closeTag == null) throw new NullPointerException();
this.openTag = openTag;
this.closeTag = closeTag;
}
}
/** Интерфейс обработчика для замены тэгов */
public interface TagReplaceHandler {
/**
* метод замены тэга
* @param source найденный тэг (объект TagsPair)
* @return то, на что необходимо заменить найденный тэг, или null, если требуется оставить данный тэг без изменений
*/
TagsPair replace(TagsPair source);
}
/**
* Получить объект-парсер тэга <span>
*/
public static FastHtmlTagParser getSpanTagParser() {
if (spanTagParser == null) spanTagParser = new FastHtmlTagParser("span");
return spanTagParser;
}
/**
* Получить объект-парсер тэга <font>
*/
public static FastHtmlTagParser getFontTagParser() {
if (fontTagParser == null) fontTagParser = new FastHtmlTagParser("font");
return fontTagParser;
}
/**
* Получить объект-парсер тэга <p>
*/
public static FastHtmlTagParser getPTagParser() {
if (pTagParser == null) pTagParser = new FastHtmlTagParser("p");
return pTagParser;
}
/**
* Получить объект-обработчик тэгов, который удаляет тэги (заменяет на "")
*/
public static TagReplaceHandler getRemoveTagsReplaceHandler() {
if (removeTagsReplaceHandler == null) removeTagsReplaceHandler = new TagReplaceHandler() {
@Override
public TagsPair replace(TagsPair source) {
return new TagsPair("", "");
}
};
return removeTagsReplaceHandler;
}
private static FastHtmlTagParser spanTagParser = null;
private static FastHtmlTagParser fontTagParser = null;
private static FastHtmlTagParser pTagParser = null;
private static TagReplaceHandler removeTagsReplaceHandler = null;
/**
* Триммер html строки (удаляет лишние переносы строк и пробелы).
* @param source
* @return
*/
public static String fastTrim(String source) {
StringBuilder builder = new StringBuilder(source.length());
boolean lastSpace = true;
boolean noEOL = true;
for (int i = 0; i < source.length(); ++i) {
if (source.charAt(i) == '\r' || source.charAt(i) == '\n' || source.charAt(i) == ' ') {
if (source.charAt(i) != ' ') noEOL = false;
if (!lastSpace) builder.append(' ');
lastSpace = true;
} else {
builder.append(source.charAt(i));
lastSpace = false;
}
}
int len = builder.length();
if (len == 0) return "";
if (builder.charAt(len-1) == ' ') builder.setLength(--len);
if (noEOL && source.length() == len) return source;
return builder.toString();
}
/**
* Конструктор
* @param openFilter фильтр открывающегося тэга (напр "<span"), обязательно в нижнем регистре!
* @param closeFilter фильтр закрывающегося тэга (напр "</span>"), обязательно в нижнем регистре!
*/
public FastHtmlTagParser(char[] openFilter, char[] closeFilter) {
this.openFilter = openFilter;
this.closeFilter = closeFilter;
}
private FastHtmlTagParser(String tagName) {
this("<".concat(tagName).toCharArray(), "</".concat(tagName).concat(">").toCharArray());
}
private class Tag {
public int position;
public int length;
public boolean open;
public boolean processed = false;
public String newValue = null;
}
private final char[] openFilter;
private final char[] closeFilter;
private ArrayList<Tag> findAllTags(String source) throws IllegalStateException {
ArrayList<Tag> tagsArray = new ArrayList<Tag>();
int curPos = 0;
int openPos = 0;
int closePos = 0;
int check = 0; // проверка того, последовательность открывающихся и закрывающихся тэгов верна
// (закрывающийся тэг должен идти после открывающегося; их количества должны быть равны)
while (curPos < source.length()) {
if (openFilter[openPos] == Character.toLowerCase(source.charAt(curPos))) {
++openPos;
} else {
openPos = 0;
}
if (closeFilter[closePos] == Character.toLowerCase(source.charAt(curPos))) {
++closePos;
} else {
closePos = 0;
}
if (openPos == openFilter.length) {
Tag tag = new Tag();
tag.open = true;
tag.position = curPos + 1 - openFilter.length;
while (curPos < source.length() && source.charAt(++curPos) != '>') {}
tag.length = curPos + 1 - tag.position;
tagsArray.add(tag);
++check;
openPos = 0;
closePos = 0;
}
if (closePos == closeFilter.length) {
Tag tag = new Tag();
tag.open = false;
tag.position = curPos + 1 - closeFilter.length;
tag.length = closeFilter.length;
tagsArray.add(tag);
--check;
if (check < 0) {
throw new IllegalStateException();
}
openPos = 0;
closePos = 0;
}
++curPos;
}
if (check != 0) {
throw new IllegalStateException();
}
return tagsArray;
}
/**
* Произвести замену тэгов
* @param source исходная HTML строка
* @param replacer объект-обработчик для замены тэгов
* @return результирующая строка
* @throws IllegalStateException в случае, если исходная строка HTML некорректна,
* например, не каждому открывающемуся тэгу соответствует закрывающийся
*/
public String replace(String source, TagReplaceHandler replacer) throws IllegalStateException {
ArrayList<Tag> tags = findAllTags(source);
ListIterator<Tag> reverseIterator = tags.listIterator(tags.size());
int newLength = source.length();
while (reverseIterator.hasPrevious()) {
Tag openTag = reverseIterator.previous();
if (openTag.open && !openTag.processed) {
while (reverseIterator.hasNext()) {
Tag closeTag = reverseIterator.next();
if (!closeTag.open && !closeTag.processed) {
openTag.processed = true;
closeTag.processed = true;
TagsPair result = replacer.replace(new TagsPair(
source.substring(openTag.position, openTag.position + openTag.length),
source.substring(closeTag.position, closeTag.position + closeTag.length)));
if (result != null) {
openTag.newValue = result.openTag;
closeTag.newValue = result.closeTag;
newLength += openTag.newValue.length() - openTag.length + closeTag.newValue.length() - closeTag.length;
}
break;
}
}
continue;
}
}
boolean notRequired = true; //проверить, есть ли хоть один тэг на замену, в противном случае сразу вернуть исходную строку
for (Tag tag : tags) {
if (tag.newValue != null) {
notRequired = false;
break;
}
}
if (notRequired) return source;
Collections.sort(tags, new Comparator<Tag>() {
@Override
public int compare(Tag lhs, Tag rhs) {
return lhs.position - rhs.position;
}
});
StringBuilder builder = new StringBuilder(newLength);
int strPosition = 0;
for (Tag tag : tags) {
if (tag.newValue != null) {
builder.append(source.substring(strPosition, tag.position));
builder.append(tag.newValue);
} else {
builder.append(source.substring(strPosition, tag.position + tag.length));
}
strPosition = tag.position + tag.length;
}
builder.append(source.substring(strPosition));
return builder.toString();
}
}