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