This commit is contained in:
Mariotaku Lee 2017-04-23 14:39:49 +08:00
parent 8359397929
commit 9c48ce3fe6
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
8 changed files with 269 additions and 367 deletions

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
} }
} }
} }

View File

@ -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

View File

@ -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>"
} }

View File

@ -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()
} }

View File

@ -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))
}
}
}
}

View File

@ -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
}
}
}