180 lines
6.3 KiB
Kotlin
180 lines
6.3 KiB
Kotlin
/*
|
|
* 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 until linksSize) {
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|