From 6773342b60107c2f05934aeb207e4392f9b59bcc Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 1 Nov 2023 09:22:23 +0100 Subject: [PATCH] Support code blocks (#4090) before after --- .../viewthread/edits/ViewEditsAdapter.kt | 37 ++---------- .../viewthread/edits/ViewEditsViewModel.kt | 4 +- .../tusky/util/StatusParsingHelper.kt | 59 ++++++++++++++++++- 3 files changed, 64 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt index 56bf1ffc4..3866bde59 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt @@ -5,10 +5,7 @@ import android.graphics.Typeface.DEFAULT_BOLD import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.text.Editable -import android.text.Html -import android.text.Spannable import android.text.SpannableStringBuilder -import android.text.Spanned import android.text.TextPaint import android.text.style.CharacterStyle import android.util.TypedValue @@ -29,6 +26,7 @@ import com.keylesspalace.tusky.entity.StatusEdit import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.AbsoluteTimeFormatter import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.TuskyTagHandler import com.keylesspalace.tusky.util.aspectRatios import com.keylesspalace.tusky.util.decodeBlurHash import com.keylesspalace.tusky.util.emojify @@ -118,7 +116,7 @@ class ViewEditsAdapter( val emojifiedText = edit .content - .parseAsMastodonHtml(TuskyTagHandler(context)) + .parseAsMastodonHtml(EditsTagHandler(context)) .emojify(edit.emojis, binding.statusEditContent, animateEmojis) 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 * 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 Del @@ -255,34 +253,7 @@ class TuskyTagHandler(val context: Context) : Html.TagHandler { ) } } - } - } - - /** @return the last span in [text] of type [kind], or null if that kind is not in text */ - private fun getLast(text: Spanned, kind: Class): 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 end(text: SpannableStringBuilder, mark: Class, 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) + else -> super.handleTag(opening, tag, output, xmlReader) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt index 85786efa2..d1f0bcae4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt @@ -18,8 +18,8 @@ package com.keylesspalace.tusky.components.viewthread.edits import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.TuskyTagHandler.Companion.INSERTED_TEXT_EL +import com.keylesspalace.tusky.components.viewthread.edits.EditsTagHandler.Companion.DELETED_TEXT_EL +import com.keylesspalace.tusky.components.viewthread.edits.EditsTagHandler.Companion.INSERTED_TEXT_EL import com.keylesspalace.tusky.entity.StatusEdit import com.keylesspalace.tusky.network.MastodonApi import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt index 117a44e28..56d60e954 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt @@ -17,21 +17,78 @@ package com.keylesspalace.tusky.util +import android.text.Editable import android.text.Html.TagHandler +import android.text.Spannable +import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.style.TypefaceSpan import androidx.core.text.parseAsHtml +import org.xml.sax.XMLReader /** * parse a String containing html from the Mastodon api to Spanned */ @JvmOverloads -fun String.parseAsMastodonHtml(tagHandler: TagHandler? = null): Spanned { +fun String.parseAsMastodonHtml(tagHandler: TagHandler? = tuskyTagHandler): Spanned { return this.replace("
", "
 ") .replace("
", "
 ") .replace("
", "
 ") + .replace("\n", "
") .replace(" ", "  ") .parseAsHtml(tagHandler = tagHandler) /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which * most status contents do, so it should be trimmed. */ .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 getLast(text: Spanned, kind: Class): 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 end(text: SpannableStringBuilder, mark: Class, 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) + } + } +}