193 lines
6.4 KiB
Kotlin
193 lines
6.4 KiB
Kotlin
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2015 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.graphics.Typeface
|
|
import android.net.Uri
|
|
import android.text.Editable
|
|
import android.text.Spannable
|
|
import android.text.SpannableStringBuilder
|
|
import android.text.Spanned
|
|
import android.text.style.StyleSpan
|
|
import android.text.style.URLSpan
|
|
import org.attoparser.ParseException
|
|
import org.attoparser.config.ParseConfiguration
|
|
import org.attoparser.simple.AbstractSimpleMarkupHandler
|
|
import org.attoparser.simple.SimpleMarkupParser
|
|
import java.util.*
|
|
|
|
/**
|
|
* Created by mariotaku on 15/11/4.
|
|
*/
|
|
object HtmlSpanBuilder {
|
|
|
|
private val PARSER = SimpleMarkupParser(ParseConfiguration.htmlConfiguration())
|
|
|
|
@Throws(HtmlParseException::class)
|
|
fun fromHtml(html: String, processor: SpanProcessor? = null): Spannable {
|
|
val handler = HtmlSpanHandler(processor)
|
|
try {
|
|
PARSER.parse(html, handler)
|
|
} catch (e: ParseException) {
|
|
throw HtmlParseException(e)
|
|
}
|
|
|
|
return handler.text
|
|
}
|
|
|
|
fun fromHtml(html: String?, fallback: CharSequence?, processor: SpanProcessor? = null): CharSequence? {
|
|
if (html == null) return fallback
|
|
try {
|
|
return fromHtml(html, processor)
|
|
} catch (e: HtmlParseException) {
|
|
return fallback
|
|
}
|
|
|
|
}
|
|
|
|
private fun applyTag(sb: SpannableStringBuilder, start: Int, end: Int, info: TagInfo,
|
|
processor: SpanProcessor?) {
|
|
if (processor?.applySpan(sb, start, end, info) ?: false) return
|
|
if (info.nameLower == "br") {
|
|
sb.append('\n')
|
|
} else {
|
|
val span = createSpan(info) ?: return
|
|
sb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
}
|
|
}
|
|
|
|
private fun createSpan(info: TagInfo): Any? {
|
|
when (info.nameLower) {
|
|
"a" -> {
|
|
return URLSpan(info.getAttribute("href"))
|
|
}
|
|
"b", "strong" -> {
|
|
return StyleSpan(Typeface.BOLD)
|
|
}
|
|
"em", "cite", "dfn", "i" -> {
|
|
return StyleSpan(Typeface.ITALIC)
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
private fun lastIndexOfTag(info: List<TagInfo>, name: String): Int {
|
|
return info.indexOfLast { it.name.equals(name, ignoreCase = true) }
|
|
}
|
|
|
|
interface SpanProcessor {
|
|
|
|
/**
|
|
* @param text Text before content in [buffer] appended
|
|
* @param buffer Raw html buffer
|
|
* @param start Start index of text to append in [buffer]
|
|
* @param len Length of text to append in [buffer]
|
|
*/
|
|
fun appendText(text: Editable, buffer: CharArray, start: Int, len: Int): Boolean = false
|
|
|
|
/**
|
|
* @param text Text to apply span from [info]
|
|
* @param start Start index for applying span
|
|
* @param end End index for applying span
|
|
* @param info Tag info
|
|
*/
|
|
fun applySpan(text: Editable, start: Int, end: Int, info: TagInfo): Boolean = false
|
|
|
|
}
|
|
|
|
data class TagInfo(val start: Int, val name: String, val attributes: Map<String, String>?) {
|
|
|
|
val nameLower = name.toLowerCase(Locale.US)
|
|
|
|
fun getAttribute(attr: String): String? {
|
|
return attributes?.get(attr)
|
|
}
|
|
}
|
|
|
|
private class HtmlParseException : RuntimeException {
|
|
|
|
internal constructor() : super()
|
|
|
|
internal constructor(detailMessage: String) : super(detailMessage)
|
|
internal constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable)
|
|
|
|
|
|
internal constructor(throwable: Throwable) : super(throwable)
|
|
|
|
}
|
|
|
|
private class HtmlSpanHandler(
|
|
val processor: SpanProcessor?
|
|
) : AbstractSimpleMarkupHandler() {
|
|
|
|
private val sb = SpannableStringBuilder()
|
|
private var tagInfo = ArrayList<TagInfo>()
|
|
private var lastTag: TagInfo? = null
|
|
|
|
override fun handleText(buffer: CharArray, offset: Int, len: Int, line: Int, col: Int) {
|
|
var cur = offset
|
|
while (cur < offset + len) {
|
|
// Find first line break
|
|
var lineBreakIndex = cur
|
|
while (lineBreakIndex < offset + len) {
|
|
if (buffer[lineBreakIndex] == '\n') break
|
|
lineBreakIndex++
|
|
}
|
|
if (!(processor?.appendText(sb, buffer, cur, lineBreakIndex - cur) ?: false)) {
|
|
sb.append(HtmlEscapeHelper.unescape(String(buffer, cur, lineBreakIndex - cur)))
|
|
}
|
|
cur = lineBreakIndex + 1
|
|
}
|
|
lastTag = null
|
|
}
|
|
|
|
override fun handleCloseElement(elementName: String, line: Int, col: Int) {
|
|
val lastIndex = lastIndexOfTag(tagInfo, elementName)
|
|
if (lastIndex == -1) return
|
|
val info = tagInfo[lastIndex]
|
|
applyTag(sb, info.start, sb.length, info, processor)
|
|
tagInfo.removeAt(lastIndex)
|
|
lastTag = info
|
|
}
|
|
|
|
override fun handleOpenElement(elementName: String, attributes: Map<String, String>?,
|
|
line: Int, col: Int) {
|
|
val info = TagInfo(sb.length, elementName, attributes)
|
|
tagInfo.add(info)
|
|
|
|
// Mastodon case, insert 2 breaks between two <p> tag
|
|
if ("p" == info.nameLower && "p" == lastTag?.nameLower) {
|
|
sb.append("\n\n")
|
|
}
|
|
}
|
|
|
|
@Throws(ParseException::class)
|
|
override fun handleStandaloneElement(elementName: String, attributes: Map<String, String>?,
|
|
minimized: Boolean, line: Int, col: Int) {
|
|
val info = TagInfo(sb.length, elementName, attributes)
|
|
applyTag(sb, info.start, sb.length, info, processor)
|
|
}
|
|
|
|
val text: Spannable
|
|
get() = sb
|
|
}
|
|
|
|
}
|