Support code blocks (#4090)
before <img src="https://github.com/tuskyapp/Tusky/assets/10157047/452b959f-7f97-4d04-a464-0dcf0bf56f79" width="380"/> after <img src="https://github.com/tuskyapp/Tusky/assets/10157047/0fb5b41c-dda3-4d46-878e-689d6ae51b0a" width="380"/>
This commit is contained in:
parent
ede66c4eb8
commit
6773342b60
|
@ -5,10 +5,7 @@ import android.graphics.Typeface.DEFAULT_BOLD
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.Html
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.text.style.CharacterStyle
|
import android.text.style.CharacterStyle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
@ -29,6 +26,7 @@ import com.keylesspalace.tusky.entity.StatusEdit
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
|
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
import com.keylesspalace.tusky.util.TuskyTagHandler
|
||||||
import com.keylesspalace.tusky.util.aspectRatios
|
import com.keylesspalace.tusky.util.aspectRatios
|
||||||
import com.keylesspalace.tusky.util.decodeBlurHash
|
import com.keylesspalace.tusky.util.decodeBlurHash
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
@ -118,7 +116,7 @@ class ViewEditsAdapter(
|
||||||
|
|
||||||
val emojifiedText = edit
|
val emojifiedText = edit
|
||||||
.content
|
.content
|
||||||
.parseAsMastodonHtml(TuskyTagHandler(context))
|
.parseAsMastodonHtml(EditsTagHandler(context))
|
||||||
.emojify(edit.emojis, binding.statusEditContent, animateEmojis)
|
.emojify(edit.emojis, binding.statusEditContent, animateEmojis)
|
||||||
|
|
||||||
setClickableText(binding.statusEditContent, emojifiedText, emptyList(), emptyList(), listener)
|
setClickableText(binding.statusEditContent, emojifiedText, emptyList(), emptyList(), listener)
|
||||||
|
@ -224,7 +222,7 @@ class ViewEditsAdapter(
|
||||||
* Handle XML tags created by [ViewEditsViewModel] and create custom spans to display inserted or
|
* Handle XML tags created by [ViewEditsViewModel] and create custom spans to display inserted or
|
||||||
* deleted text.
|
* deleted text.
|
||||||
*/
|
*/
|
||||||
class TuskyTagHandler(val context: Context) : Html.TagHandler {
|
class EditsTagHandler(val context: Context) : TuskyTagHandler() {
|
||||||
/** Class to mark the start of a span of deleted text */
|
/** Class to mark the start of a span of deleted text */
|
||||||
class Del
|
class Del
|
||||||
|
|
||||||
|
@ -255,34 +253,7 @@ class TuskyTagHandler(val context: Context) : Html.TagHandler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
else -> super.handleTag(opening, tag, output, xmlReader)
|
||||||
}
|
|
||||||
|
|
||||||
/** @return the last span in [text] of type [kind], or null if that kind is not in text */
|
|
||||||
private fun <T> getLast(text: Spanned, kind: Class<T>): Any? {
|
|
||||||
val spans = text.getSpans(0, text.length, kind)
|
|
||||||
return spans?.get(spans.size - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark the start of a span of [text] with [mark] so it can be discovered later by [end].
|
|
||||||
*/
|
|
||||||
private fun start(text: SpannableStringBuilder, mark: Any) {
|
|
||||||
val len = text.length
|
|
||||||
text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a [span] over the [text] most from the point recently marked with [mark] to the end
|
|
||||||
* of the text.
|
|
||||||
*/
|
|
||||||
private fun <T> end(text: SpannableStringBuilder, mark: Class<T>, span: Any) {
|
|
||||||
val len = text.length
|
|
||||||
val obj = getLast(text, mark)
|
|
||||||
val where = text.getSpanStart(obj)
|
|
||||||
text.removeSpan(obj)
|
|
||||||
if (where != len) {
|
|
||||||
text.setSpan(span, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@ package com.keylesspalace.tusky.components.viewthread.edits
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.connyduck.calladapter.networkresult.getOrElse
|
import at.connyduck.calladapter.networkresult.getOrElse
|
||||||
import com.keylesspalace.tusky.components.viewthread.edits.TuskyTagHandler.Companion.DELETED_TEXT_EL
|
import com.keylesspalace.tusky.components.viewthread.edits.EditsTagHandler.Companion.DELETED_TEXT_EL
|
||||||
import com.keylesspalace.tusky.components.viewthread.edits.TuskyTagHandler.Companion.INSERTED_TEXT_EL
|
import com.keylesspalace.tusky.components.viewthread.edits.EditsTagHandler.Companion.INSERTED_TEXT_EL
|
||||||
import com.keylesspalace.tusky.entity.StatusEdit
|
import com.keylesspalace.tusky.entity.StatusEdit
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
|
@ -17,21 +17,78 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.util
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
import android.text.Html.TagHandler
|
import android.text.Html.TagHandler
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import android.text.style.TypefaceSpan
|
||||||
import androidx.core.text.parseAsHtml
|
import androidx.core.text.parseAsHtml
|
||||||
|
import org.xml.sax.XMLReader
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* parse a String containing html from the Mastodon api to Spanned
|
* parse a String containing html from the Mastodon api to Spanned
|
||||||
*/
|
*/
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun String.parseAsMastodonHtml(tagHandler: TagHandler? = null): Spanned {
|
fun String.parseAsMastodonHtml(tagHandler: TagHandler? = tuskyTagHandler): Spanned {
|
||||||
return this.replace("<br> ", "<br> ")
|
return this.replace("<br> ", "<br> ")
|
||||||
.replace("<br /> ", "<br /> ")
|
.replace("<br /> ", "<br /> ")
|
||||||
.replace("<br/> ", "<br/> ")
|
.replace("<br/> ", "<br/> ")
|
||||||
|
.replace("\n", "<br/>")
|
||||||
.replace(" ", " ")
|
.replace(" ", " ")
|
||||||
.parseAsHtml(tagHandler = tagHandler)
|
.parseAsHtml(tagHandler = tagHandler)
|
||||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
||||||
* most status contents do, so it should be trimmed. */
|
* most status contents do, so it should be trimmed. */
|
||||||
.trimTrailingWhitespace()
|
.trimTrailingWhitespace()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val tuskyTagHandler = TuskyTagHandler()
|
||||||
|
|
||||||
|
open class TuskyTagHandler : TagHandler {
|
||||||
|
|
||||||
|
class Code
|
||||||
|
|
||||||
|
override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) {
|
||||||
|
when (tag) {
|
||||||
|
"code" -> {
|
||||||
|
if (opening) {
|
||||||
|
start(output as SpannableStringBuilder, Code())
|
||||||
|
} else {
|
||||||
|
end(
|
||||||
|
output as SpannableStringBuilder,
|
||||||
|
Code::class.java,
|
||||||
|
TypefaceSpan("monospace")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return the last span in [text] of type [kind], or null if that kind is not in text */
|
||||||
|
protected fun <T> getLast(text: Spanned, kind: Class<T>): Any? {
|
||||||
|
val spans = text.getSpans(0, text.length, kind)
|
||||||
|
return spans?.get(spans.size - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the start of a span of [text] with [mark] so it can be discovered later by [end].
|
||||||
|
*/
|
||||||
|
protected fun start(text: SpannableStringBuilder, mark: Any) {
|
||||||
|
val len = text.length
|
||||||
|
text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a [span] over the [text] from the point recently marked with [mark] to the end
|
||||||
|
* of the text.
|
||||||
|
*/
|
||||||
|
protected fun <T> end(text: SpannableStringBuilder, mark: Class<T>, span: Any) {
|
||||||
|
val len = text.length
|
||||||
|
val obj = getLast(text, mark)
|
||||||
|
val where = text.getSpanStart(obj)
|
||||||
|
text.removeSpan(obj)
|
||||||
|
if (where != len) {
|
||||||
|
text.setSpan(span, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue