fixed #773
This commit is contained in:
parent
8359397929
commit
9c48ce3fe6
|
@ -1,277 +0,0 @@
|
||||||
/*
|
|
||||||
* Twidere - Twitter client for Android
|
|
||||||
*
|
|
||||||
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
||||||
*
|
|
||||||
* 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 org.mariotaku.twidere.util;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.mariotaku.commons.text.CodePointArray;
|
|
||||||
import org.mariotaku.twidere.model.SpanItem;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import kotlin.Pair;
|
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
import static org.mariotaku.twidere.util.HtmlEscapeHelper.escape;
|
|
||||||
import static org.mariotaku.twidere.util.HtmlEscapeHelper.unescape;
|
|
||||||
|
|
||||||
public class HtmlBuilder {
|
|
||||||
|
|
||||||
private final CodePointArray source;
|
|
||||||
private final int sourceLength;
|
|
||||||
private final boolean throwExceptions, sourceIsEscaped, shouldReEscape;
|
|
||||||
|
|
||||||
private final ArrayList<SpanSpec> spanSpecs = new ArrayList<>();
|
|
||||||
|
|
||||||
public HtmlBuilder(final String source, final boolean strict, final boolean sourceIsEscaped,
|
|
||||||
final boolean shouldReEscape) {
|
|
||||||
this(new CodePointArray(source), strict, sourceIsEscaped, shouldReEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HtmlBuilder(final CodePointArray source, final boolean strict, final boolean sourceIsEscaped,
|
|
||||||
final boolean shouldReEscape) {
|
|
||||||
if (source == null) throw new NullPointerException();
|
|
||||||
this.source = source;
|
|
||||||
this.sourceLength = source.length();
|
|
||||||
this.throwExceptions = strict;
|
|
||||||
this.sourceIsEscaped = sourceIsEscaped;
|
|
||||||
this.shouldReEscape = shouldReEscape;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean addLink(final String link, final String display, final int start, final int end) {
|
|
||||||
return addLink(link, display, start, end, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean addLink(final String link, final String display, final int start, final int end,
|
|
||||||
final boolean displayIsHtml) {
|
|
||||||
if (start < 0 || end < 0 || start > end || end > sourceLength) {
|
|
||||||
final String message = String.format(Locale.US, "text:%s, length:%d, start:%d, end:%d", source,
|
|
||||||
sourceLength, start, end);
|
|
||||||
if (throwExceptions) throw new StringIndexOutOfBoundsException(message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (hasLink(start, end)) {
|
|
||||||
final String message = String.format(Locale.US,
|
|
||||||
"link already added in this range! text:%s, link:%s, display:%s, start:%d, end:%d", source, link,
|
|
||||||
display, start, end);
|
|
||||||
if (throwExceptions) throw new IllegalArgumentException(message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return spanSpecs.add(new LinkSpec(link, display, start, end, displayIsHtml));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Pair<String, SpanItem[]> buildWithIndices() {
|
|
||||||
if (spanSpecs.isEmpty()) return new Pair<>(escapeSource(), new SpanItem[0]);
|
|
||||||
Collections.sort(spanSpecs);
|
|
||||||
final StringBuilder sb = new StringBuilder();
|
|
||||||
final int linksSize = spanSpecs.size();
|
|
||||||
SpanItem[] items = new SpanItem[linksSize];
|
|
||||||
for (int i = 0; i < linksSize; i++) {
|
|
||||||
final SpanSpec spec = spanSpecs.get(i);
|
|
||||||
final int start = spec.getStart(), end = spec.getEnd();
|
|
||||||
if (i == 0) {
|
|
||||||
if (start >= 0 && start <= sourceLength) {
|
|
||||||
appendSource(sb, 0, start, false, sourceIsEscaped);
|
|
||||||
}
|
|
||||||
} else if (i > 0) {
|
|
||||||
final int lastEnd = spanSpecs.get(i - 1).end;
|
|
||||||
if (lastEnd >= 0 && lastEnd <= start && start <= sourceLength) {
|
|
||||||
appendSource(sb, lastEnd, start, false, sourceIsEscaped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int spanStart = sb.length();
|
|
||||||
if (start >= 0 && start <= end && end <= sourceLength) {
|
|
||||||
spec.appendTo(sb);
|
|
||||||
}
|
|
||||||
final SpanItem item = new SpanItem();
|
|
||||||
item.start = spanStart;
|
|
||||||
item.end = sb.length();
|
|
||||||
item.orig_start = start;
|
|
||||||
item.orig_end = end;
|
|
||||||
if (spec instanceof LinkSpec) {
|
|
||||||
item.link = ((LinkSpec) spec).link;
|
|
||||||
}
|
|
||||||
items[i] = item;
|
|
||||||
if (i == linksSize - 1 && end >= 0 && end <= sourceLength) {
|
|
||||||
appendSource(sb, end, sourceLength, false, sourceIsEscaped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Pair<>(sb.toString(), items);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasLink(final int start, final int end) {
|
|
||||||
for (final SpanSpec spec : spanSpecs) {
|
|
||||||
final int specStart = spec.getStart(), specEnd = spec.getEnd();
|
|
||||||
if (start >= specStart && start <= specEnd || end >= specStart && end <= specEnd) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "HtmlBuilder{" +
|
|
||||||
", codePoints=" + source +
|
|
||||||
", codePointsLength=" + sourceLength +
|
|
||||||
", throwExceptions=" + throwExceptions +
|
|
||||||
", sourceIsEscaped=" + sourceIsEscaped +
|
|
||||||
", shouldReEscape=" + shouldReEscape +
|
|
||||||
", links=" + spanSpecs +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
|
|
||||||
private void appendSource(final StringBuilder builder, final int start, final int end, boolean escapeSource, boolean sourceEscaped) {
|
|
||||||
if (sourceEscaped == escapeSource) {
|
|
||||||
append(builder, source.substring(start, end), escapeSource, sourceEscaped);
|
|
||||||
} else if (escapeSource) {
|
|
||||||
append(builder, escape(source.substring(start, end)), true, sourceEscaped);
|
|
||||||
} else {
|
|
||||||
append(builder, unescape(source.substring(start, end)), false, sourceEscaped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void append(final StringBuilder builder, final String text, boolean escapeText, boolean textEscaped) {
|
|
||||||
if (textEscaped == escapeText) {
|
|
||||||
builder.append(text);
|
|
||||||
} else if (escapeText) {
|
|
||||||
builder.append(escape(text));
|
|
||||||
} else {
|
|
||||||
builder.append(unescape(text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String escapeSource() {
|
|
||||||
final String source = this.source.substring(0, this.source.length());
|
|
||||||
if (sourceIsEscaped == shouldReEscape) return source;
|
|
||||||
return shouldReEscape ? escape(source) : unescape(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
static abstract class SpanSpec implements Comparable<SpanSpec> {
|
|
||||||
|
|
||||||
final int start, end;
|
|
||||||
|
|
||||||
public final int getStart() {
|
|
||||||
return start;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final int getEnd() {
|
|
||||||
return end;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SpanSpec(int start, int end) {
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(@NonNull final SpanSpec that) {
|
|
||||||
return start - that.start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
SpanSpec spanSpec = (SpanSpec) o;
|
|
||||||
|
|
||||||
if (start != spanSpec.start) return false;
|
|
||||||
return end == spanSpec.end;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = start;
|
|
||||||
result = 31 * result + end;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "SpanSpec{" +
|
|
||||||
"start=" + start +
|
|
||||||
", end=" + end +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void appendTo(StringBuilder sb);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class LinkSpec extends SpanSpec {
|
|
||||||
|
|
||||||
final String link, display;
|
|
||||||
|
|
||||||
final boolean displayIsHtml;
|
|
||||||
|
|
||||||
LinkSpec(final String link, final String display, final int start, final int end, final boolean displayIsHtml) {
|
|
||||||
super(start, end);
|
|
||||||
this.link = link;
|
|
||||||
this.display = display;
|
|
||||||
this.displayIsHtml = displayIsHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void appendTo(StringBuilder sb) {
|
|
||||||
if (isEmpty(display)) {
|
|
||||||
append(sb, link, false, false);
|
|
||||||
} else {
|
|
||||||
append(sb, display, false, displayIsHtml);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
if (!super.equals(o)) return false;
|
|
||||||
|
|
||||||
LinkSpec linkSpec = (LinkSpec) o;
|
|
||||||
|
|
||||||
if (displayIsHtml != linkSpec.displayIsHtml) return false;
|
|
||||||
if (link != null ? !link.equals(linkSpec.link) : linkSpec.link != null) return false;
|
|
||||||
return display != null ? display.equals(linkSpec.display) : linkSpec.display == null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = super.hashCode();
|
|
||||||
result = 31 * result + (link != null ? link.hashCode() : 0);
|
|
||||||
result = 31 * result + (display != null ? display.hashCode() : 0);
|
|
||||||
result = 31 * result + (displayIsHtml ? 1 : 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "LinkSpec{" +
|
|
||||||
"link='" + link + '\'' +
|
|
||||||
", display='" + display + '\'' +
|
|
||||||
", displayIsHtml=" + displayIsHtml +
|
|
||||||
"} " + super.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* Twidere - Twitter client for Android
|
|
||||||
*
|
|
||||||
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
||||||
*
|
|
||||||
* 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 org.mariotaku.twidere.util;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringEscapeUtils;
|
|
||||||
import org.apache.commons.lang3.text.translate.AggregateTranslator;
|
|
||||||
import org.apache.commons.lang3.text.translate.CodePointTranslator;
|
|
||||||
import org.apache.commons.lang3.text.translate.EntityArrays;
|
|
||||||
import org.apache.commons.lang3.text.translate.LookupTranslator;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.Writer;
|
|
||||||
|
|
||||||
public class HtmlEscapeHelper {
|
|
||||||
|
|
||||||
public static final AggregateTranslator ESCAPE_HTML = new AggregateTranslator(StringEscapeUtils.ESCAPE_HTML4,
|
|
||||||
new UnicodeControlCharacterToHtmlTranslator());
|
|
||||||
public static final LookupTranslator ESCAPE_BASIC = new LookupTranslator(EntityArrays.BASIC_ESCAPE());
|
|
||||||
|
|
||||||
private HtmlEscapeHelper() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String escape(final CharSequence text) {
|
|
||||||
if (text == null) return null;
|
|
||||||
return ESCAPE_HTML.translate(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toPlainText(final String string) {
|
|
||||||
if (string == null) return null;
|
|
||||||
return unescape(string.replace("<br/>", "\n").replaceAll("<!--.*?-->|<[^>]+>", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String unescape(final String string) {
|
|
||||||
if (string == null) return null;
|
|
||||||
return StringEscapeUtils.unescapeHtml4(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String escapeBasic(CharSequence text) {
|
|
||||||
return ESCAPE_BASIC.translate(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class UnicodeControlCharacterToHtmlTranslator extends CodePointTranslator {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean translate(int codePoint, Writer out) throws IOException {
|
|
||||||
if (Character.isISOControl(codePoint)) {
|
|
||||||
out.append("&#x");
|
|
||||||
final char[] chars = Character.toChars(codePoint);
|
|
||||||
for (char c : chars) {
|
|
||||||
out.append(Integer.toHexString(c));
|
|
||||||
}
|
|
||||||
out.append(';');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,7 +25,6 @@ import org.mariotaku.twidere.util.database.FilterQueryBuilder;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import kotlin.Pair;
|
import kotlin.Pair;
|
||||||
|
|
||||||
|
@ -34,10 +33,7 @@ import kotlin.Pair;
|
||||||
*/
|
*/
|
||||||
public class InternalTwitterContentUtils {
|
public class InternalTwitterContentUtils {
|
||||||
|
|
||||||
public static final int TWITTER_BULK_QUERY_COUNT = 100;
|
|
||||||
private static final Pattern PATTERN_TWITTER_STATUS_LINK = Pattern.compile("https?://twitter\\.com/(?:#!/)?(\\w+)/status(es)?/(\\d+)");
|
|
||||||
private static final CharSequenceTranslator UNESCAPE_TWITTER_RAW_TEXT = new LookupTranslator(EntityArrays.BASIC_UNESCAPE());
|
private static final CharSequenceTranslator UNESCAPE_TWITTER_RAW_TEXT = new LookupTranslator(EntityArrays.BASIC_UNESCAPE());
|
||||||
private static final CharSequenceTranslator ESCAPE_TWITTER_RAW_TEXT = new LookupTranslator(EntityArrays.BASIC_ESCAPE());
|
|
||||||
|
|
||||||
private InternalTwitterContentUtils() {
|
private InternalTwitterContentUtils() {
|
||||||
}
|
}
|
||||||
|
@ -73,9 +69,8 @@ public class InternalTwitterContentUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isFiltered(final SQLiteDatabase database, final ParcelableStatus status,
|
public static boolean isFiltered(@NonNull final SQLiteDatabase database,
|
||||||
final boolean filterRTs) {
|
@NonNull final ParcelableStatus status, final boolean filterRTs) {
|
||||||
if (database == null || status == null) return false;
|
|
||||||
return isFiltered(database, status.user_key, status.text_plain, status.quoted_text_plain,
|
return isFiltered(database, status.user_key, status.text_plain, status.quoted_text_plain,
|
||||||
status.spans, status.quoted_spans, status.source, status.quoted_source,
|
status.spans, status.quoted_spans, status.source, status.quoted_source,
|
||||||
status.retweeted_by_user_key, status.quoted_user_key, filterRTs);
|
status.retweeted_by_user_key, status.quoted_user_key, filterRTs);
|
||||||
|
@ -93,6 +88,7 @@ public class InternalTwitterContentUtils {
|
||||||
return authority != null && authority.endsWith(".twimg.com") ? baseUrl + "/" + type : baseUrl;
|
return authority != null && authority.endsWith(".twimg.com") ? baseUrl + "/" + type : baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public static String getBestBannerType(final int width, int height) {
|
public static String getBestBannerType(final int width, int height) {
|
||||||
if (height > 0 && width / height >= 3) {
|
if (height > 0 && width / height >= 3) {
|
||||||
if (width <= 300) return "300x100";
|
if (width <= 300) return "300x100";
|
||||||
|
@ -115,9 +111,10 @@ public class InternalTwitterContentUtils {
|
||||||
final UrlEntity[] urls = user.getDescriptionEntities();
|
final UrlEntity[] urls = user.getDescriptionEntities();
|
||||||
if (urls != null) {
|
if (urls != null) {
|
||||||
for (final UrlEntity url : urls) {
|
for (final UrlEntity url : urls) {
|
||||||
final String expanded_url = url.getExpandedUrl();
|
final String expandedUrl = url.getExpandedUrl();
|
||||||
if (expanded_url != null) {
|
if (expandedUrl != null) {
|
||||||
builder.addLink(expanded_url, url.getDisplayUrl(), url.getStart(), url.getEnd());
|
builder.addLink(expandedUrl, url.getDisplayUrl(), url.getStart(), url.getEnd(),
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,7 +225,7 @@ public class InternalTwitterContentUtils {
|
||||||
final String mediaUrl = getMediaUrl(mediaEntity);
|
final String mediaUrl = getMediaUrl(mediaEntity);
|
||||||
if (mediaUrl != null && getStartEndForEntity(mediaEntity, startEnd)) {
|
if (mediaUrl != null && getStartEndForEntity(mediaEntity, startEnd)) {
|
||||||
builder.addLink(mediaEntity.getExpandedUrl(), mediaEntity.getDisplayUrl(),
|
builder.addLink(mediaEntity.getExpandedUrl(), mediaEntity.getDisplayUrl(),
|
||||||
startEnd[0], startEnd[1]);
|
startEnd[0], startEnd[1], false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,7 +235,7 @@ public class InternalTwitterContentUtils {
|
||||||
final String expandedUrl = urlEntity.getExpandedUrl();
|
final String expandedUrl = urlEntity.getExpandedUrl();
|
||||||
if (expandedUrl != null && getStartEndForEntity(urlEntity, startEnd)) {
|
if (expandedUrl != null && getStartEndForEntity(urlEntity, startEnd)) {
|
||||||
builder.addLink(expandedUrl, urlEntity.getDisplayUrl(), startEnd[0],
|
builder.addLink(expandedUrl, urlEntity.getDisplayUrl(), startEnd[0],
|
||||||
startEnd[1]);
|
startEnd[1], false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ fun Account.toParcelable(accountKey: UserKey, position: Long = 0): ParcelableUse
|
||||||
obj.description_plain = obj.description_unescaped
|
obj.description_plain = obj.description_unescaped
|
||||||
obj.description_spans = descriptionHtml?.spanItems
|
obj.description_spans = descriptionHtml?.spanItems
|
||||||
} else {
|
} else {
|
||||||
obj.description_unescaped = HtmlEscapeHelper.unescape(note)
|
obj.description_unescaped = note?.let(HtmlEscapeHelper::unescape)
|
||||||
obj.description_plain = obj.description_unescaped
|
obj.description_plain = obj.description_unescaped
|
||||||
}
|
}
|
||||||
obj.url = url
|
obj.url = url
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.mariotaku.twidere.util.HtmlEscapeHelper
|
||||||
* Created by mariotaku on 2017/4/19.
|
* Created by mariotaku on 2017/4/19.
|
||||||
*/
|
*/
|
||||||
val Application.sourceHtml: String get() {
|
val Application.sourceHtml: String get() {
|
||||||
if (website == null) return HtmlEscapeHelper.escape(name)
|
val name = this.name ?: return ""
|
||||||
|
val website = this.website ?: return name.let(HtmlEscapeHelper::escape).orEmpty()
|
||||||
return "<a href='${HtmlEscapeHelper.escape(website)}'>${HtmlEscapeHelper.escape(name)}</a>"
|
return "<a href='${HtmlEscapeHelper.escape(website)}'>${HtmlEscapeHelper.escape(name)}</a>"
|
||||||
}
|
}
|
|
@ -158,7 +158,9 @@ class AddStatusFilterDialogFragment : BaseDialogFragment() {
|
||||||
list.add(FilterItemInfo(FilterItemInfo.FILTER_TYPE_KEYWORD, hashtag))
|
list.add(FilterItemInfo(FilterItemInfo.FILTER_TYPE_KEYWORD, hashtag))
|
||||||
}
|
}
|
||||||
val source = HtmlEscapeHelper.toPlainText(status.source)
|
val source = HtmlEscapeHelper.toPlainText(status.source)
|
||||||
list.add(FilterItemInfo(FilterItemInfo.FILTER_TYPE_SOURCE, source))
|
if (source != null) {
|
||||||
|
list.add(FilterItemInfo(FilterItemInfo.FILTER_TYPE_SOURCE, source))
|
||||||
|
}
|
||||||
return list.toTypedArray()
|
return list.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
/*
|
||||||
|
* Twidere - Twitter client for Android
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 org.mariotaku.twidere.util
|
||||||
|
|
||||||
|
import org.mariotaku.commons.text.CodePointArray
|
||||||
|
import org.mariotaku.twidere.model.SpanItem
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class HtmlBuilder(
|
||||||
|
private val source: CodePointArray,
|
||||||
|
private val throwExceptions: Boolean,
|
||||||
|
private val sourceIsEscaped: Boolean,
|
||||||
|
private val shouldReEscape: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val sourceLength = source.length()
|
||||||
|
|
||||||
|
private val spanSpecs = ArrayList<SpanSpec>()
|
||||||
|
|
||||||
|
constructor(source: String, strict: Boolean, sourceIsEscaped: Boolean, shouldReEscape: Boolean)
|
||||||
|
: this(CodePointArray(source), strict, sourceIsEscaped, shouldReEscape)
|
||||||
|
|
||||||
|
fun addLink(link: String, display: String, start: Int, end: Int,
|
||||||
|
displayIsHtml: Boolean = false): Boolean {
|
||||||
|
if (start < 0 || end < 0 || start > end || end > sourceLength) {
|
||||||
|
if (throwExceptions) {
|
||||||
|
val message = "text:$source, length:$sourceLength, start:$start, end:$end"
|
||||||
|
throw StringIndexOutOfBoundsException(message)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (hasLink(start, end)) {
|
||||||
|
if (throwExceptions) {
|
||||||
|
val message = "link already added in this range! text:$source, link:$link, display:$display, start:$start, end:end"
|
||||||
|
throw IllegalArgumentException(message)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return spanSpecs.add(LinkSpec(link, display, start, end, displayIsHtml))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildWithIndices(): Pair<String, Array<SpanItem>> {
|
||||||
|
if (spanSpecs.isEmpty()) return Pair(escapeSource(), emptyArray())
|
||||||
|
Collections.sort(spanSpecs)
|
||||||
|
val sb = StringBuilder()
|
||||||
|
val linksSize = spanSpecs.size
|
||||||
|
val items = arrayOfNulls<SpanItem>(linksSize)
|
||||||
|
for (i in 0..linksSize - 1) {
|
||||||
|
val spec = spanSpecs[i]
|
||||||
|
val start = spec.start
|
||||||
|
val end = spec.end
|
||||||
|
if (i == 0) {
|
||||||
|
if (start in 0..sourceLength) {
|
||||||
|
appendSource(sb, 0, start, false, sourceIsEscaped)
|
||||||
|
}
|
||||||
|
} else if (i > 0) {
|
||||||
|
val lastEnd = spanSpecs[i - 1].end
|
||||||
|
if (lastEnd in 0..start && start <= sourceLength) {
|
||||||
|
appendSource(sb, lastEnd, start, false, sourceIsEscaped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val spanStart = sb.length
|
||||||
|
if (start in 0..end && end <= sourceLength) {
|
||||||
|
spec.appendTo(sb)
|
||||||
|
}
|
||||||
|
val item = SpanItem()
|
||||||
|
item.start = spanStart
|
||||||
|
item.end = sb.length
|
||||||
|
item.orig_start = start
|
||||||
|
item.orig_end = end
|
||||||
|
if (spec is LinkSpec) {
|
||||||
|
item.link = spec.link
|
||||||
|
}
|
||||||
|
items[i] = item
|
||||||
|
if (i == linksSize - 1 && end >= 0 && end <= sourceLength) {
|
||||||
|
appendSource(sb, end, sourceLength, false, sourceIsEscaped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair(sb.toString(), items.requireNoNulls())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasLink(start: Int, end: Int): Boolean {
|
||||||
|
for (spec in spanSpecs) {
|
||||||
|
val specStart = spec.start
|
||||||
|
val specEnd = spec.end
|
||||||
|
if (start in specStart..specEnd || end in specStart..specEnd) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "HtmlBuilder{" +
|
||||||
|
", codePoints=" + source +
|
||||||
|
", codePointsLength=" + sourceLength +
|
||||||
|
", throwExceptions=" + throwExceptions +
|
||||||
|
", sourceIsEscaped=" + sourceIsEscaped +
|
||||||
|
", shouldReEscape=" + shouldReEscape +
|
||||||
|
", links=" + spanSpecs +
|
||||||
|
'}'
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendSource(builder: StringBuilder, start: Int, end: Int, escapeSource: Boolean, sourceEscaped: Boolean) {
|
||||||
|
if (sourceEscaped == escapeSource) {
|
||||||
|
builder.append(source.substring(start, end), escapeSource, sourceEscaped)
|
||||||
|
} else if (escapeSource) {
|
||||||
|
builder.append(HtmlEscapeHelper.escape(source.substring(start, end)), true, sourceEscaped)
|
||||||
|
} else {
|
||||||
|
builder.append(HtmlEscapeHelper.unescape(source.substring(start, end)), false, sourceEscaped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun escapeSource(): String {
|
||||||
|
val source = this.source.substring(0, this.source.length())
|
||||||
|
if (sourceIsEscaped == shouldReEscape) return source
|
||||||
|
return if (shouldReEscape) HtmlEscapeHelper.escape(source) else HtmlEscapeHelper.unescape(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface SpanSpec : Comparable<SpanSpec> {
|
||||||
|
val start: Int
|
||||||
|
val end: Int
|
||||||
|
|
||||||
|
override fun compareTo(other: SpanSpec): Int {
|
||||||
|
return start - other.start
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendTo(sb: StringBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LinkSpec(
|
||||||
|
val link: String,
|
||||||
|
val display: String?,
|
||||||
|
override val start: Int,
|
||||||
|
override val end: Int,
|
||||||
|
val displayIsHtml: Boolean
|
||||||
|
) : SpanSpec {
|
||||||
|
|
||||||
|
override fun appendTo(sb: StringBuilder) {
|
||||||
|
if (display != null) {
|
||||||
|
sb.append(display, false, displayIsHtml)
|
||||||
|
} else {
|
||||||
|
sb.append(link, false, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private fun StringBuilder.append(text: String, escapeText: Boolean, textEscaped: Boolean) {
|
||||||
|
if (textEscaped == escapeText) {
|
||||||
|
append(text)
|
||||||
|
} else if (escapeText) {
|
||||||
|
append(HtmlEscapeHelper.escape(text))
|
||||||
|
} else {
|
||||||
|
append(HtmlEscapeHelper.unescape(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Twidere - Twitter client for Android
|
||||||
|
*
|
||||||
|
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 org.mariotaku.twidere.util
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringEscapeUtils
|
||||||
|
import org.apache.commons.lang3.text.translate.AggregateTranslator
|
||||||
|
import org.apache.commons.lang3.text.translate.CodePointTranslator
|
||||||
|
import org.apache.commons.lang3.text.translate.EntityArrays
|
||||||
|
import org.apache.commons.lang3.text.translate.LookupTranslator
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.Writer
|
||||||
|
|
||||||
|
object HtmlEscapeHelper {
|
||||||
|
|
||||||
|
val ESCAPE_HTML = AggregateTranslator(
|
||||||
|
StringEscapeUtils.ESCAPE_HTML4,
|
||||||
|
UnicodeControlCharacterToHtmlTranslator()
|
||||||
|
)
|
||||||
|
val ESCAPE_BASIC = LookupTranslator(*EntityArrays.BASIC_ESCAPE())
|
||||||
|
|
||||||
|
val UNESCAPE_HTML = AggregateTranslator(
|
||||||
|
StringEscapeUtils.UNESCAPE_HTML4,
|
||||||
|
LookupTranslator(*EntityArrays.APOS_UNESCAPE())
|
||||||
|
)
|
||||||
|
|
||||||
|
fun escape(text: CharSequence): String {
|
||||||
|
return ESCAPE_HTML.translate(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toPlainText(string: String): String {
|
||||||
|
return unescape(string.replace("<br/>", "\n").replace("<!--.*?-->|<[^>]+>".toRegex(), ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unescape(string: String): String {
|
||||||
|
return UNESCAPE_HTML.translate(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun escapeBasic(text: CharSequence): String {
|
||||||
|
return ESCAPE_BASIC.translate(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UnicodeControlCharacterToHtmlTranslator : CodePointTranslator() {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun translate(codePoint: Int, out: Writer): Boolean {
|
||||||
|
if (Character.isISOControl(codePoint)) {
|
||||||
|
out.append("&#x")
|
||||||
|
val chars = Character.toChars(codePoint)
|
||||||
|
for (c in chars) {
|
||||||
|
out.append(Integer.toHexString(c.toInt()))
|
||||||
|
}
|
||||||
|
out.append(';')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue