handling more text parsing cases for fallbacks and urls

This commit is contained in:
Adam Brown 2022-10-24 22:03:53 +01:00
parent fddcdaa50c
commit 9476fc5814
6 changed files with 112 additions and 24 deletions

View File

@ -7,6 +7,7 @@ data class RichText(val parts: Set<Part>) {
data class Bold(val content: String) : Part
data class Italic(val content: String) : Part
data class BoldItalic(val content: String) : Part
data class Person(val userId: String) : Part
}
}

View File

@ -17,14 +17,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
@ -265,6 +262,10 @@ val hyperLinkStyle = SpanStyle(
textDecoration = TextDecoration.Underline
)
val nameStyle = SpanStyle(
color = Color(0xff64B5F6),
)
fun RichText.toAnnotatedText() = buildAnnotatedString {
parts.forEach {
when (it) {
@ -278,6 +279,9 @@ fun RichText.toAnnotatedText() = buildAnnotatedString {
}
is RichText.Part.Normal -> append(it.content)
is RichText.Part.Person -> withStyle(nameStyle) {
append("@${it.userId.substringBefore(':').removePrefix("@")}")
}
}
}
}

View File

@ -234,6 +234,7 @@ private fun RichText.toApp(): app.dapk.st.core.RichText {
is RichText.Part.Italic -> app.dapk.st.core.RichText.Part.Italic(it.content)
is RichText.Part.Link -> app.dapk.st.core.RichText.Part.Link(it.url, it.label)
is RichText.Part.Normal -> app.dapk.st.core.RichText.Part.Normal(it.content)
is RichText.Part.Person -> app.dapk.st.core.RichText.Part.Person(it.userId.value)
}
}.toSet())
}

View File

@ -21,6 +21,9 @@ data class RichText(@SerialName("parts") val parts: Set<Part>) {
@Serializable
data class BoldItalic(@SerialName("content") val content: String) : Part
@Serializable
data class Person(@SerialName("user_id") val userId: UserId, @SerialName("display_name") val displayName: String) : Part
}
companion object {
@ -35,5 +38,6 @@ fun RichText.asString() = parts.joinToString(separator = "") {
is RichText.Part.Italic -> it.content
is RichText.Part.Link -> it.label
is RichText.Part.Normal -> it.content
is RichText.Part.Person -> it.userId.value
}
}

View File

@ -2,10 +2,14 @@ package app.dapk.st.matrix.sync.internal.sync
import app.dapk.st.matrix.common.RichText
import app.dapk.st.matrix.common.RichText.Part.*
import app.dapk.st.matrix.common.UserId
class RichMessageParser {
fun parse(input: String): RichText {
fun parse(source: String): RichText {
val input = source
.removeHtmlEntities()
.dropTextFallback()
return kotlin.runCatching {
val buffer = mutableSetOf<RichText.Part>()
var openIndex = 0
@ -21,12 +25,23 @@ class RichMessageParser {
val wholeTag = input.substring(foundIndex, closeIndex + 1)
val tagName = wholeTag.substring(1, wholeTag.indexOfFirst { it == '>' || it == ' ' })
if (tagName.startsWith('@')) {
if (openIndex != foundIndex) {
buffer.add(Normal(input.substring(openIndex, foundIndex)))
}
buffer.add(Person(UserId(tagName), tagName))
println(tagName)
openIndex = foundIndex + wholeTag.length
lastStartIndex = openIndex
continue
}
if (tagName == "br") {
if (openIndex != foundIndex) {
buffer.add(Normal(input.substring(openIndex, foundIndex)))
}
buffer.add(Normal("\n"))
openIndex = foundIndex + "<br />".length
openIndex = foundIndex + wholeTag.length
lastStartIndex = openIndex
continue
}
@ -39,6 +54,14 @@ class RichMessageParser {
if (exitIndex == -1) {
openIndex++
} else {
when (tagName) {
"mx-reply" -> {
openIndex = exitIndex + exitTag.length
lastStartIndex = openIndex
continue
}
}
if (openIndex != foundIndex) {
buffer.add(Normal(input.substring(openIndex, foundIndex)))
}
@ -49,8 +72,17 @@ class RichMessageParser {
when (tagName) {
"a" -> {
val findHrefUrl = wholeTag.substringAfter("href=").replace("\"", "").removeSuffix(">")
if (findHrefUrl.startsWith("https://matrix.to/#/@")) {
val userId = UserId(findHrefUrl.substringAfter("https://matrix.to/#/").substringBeforeLast("\""))
buffer.add(Person(userId, "@${tagContent.removePrefix("@")}"))
if (input.getOrNull(openIndex) == ':') {
openIndex++
lastStartIndex = openIndex
}
} else {
buffer.add(Link(url = findHrefUrl, label = tagContent))
}
}
"b" -> buffer.add(Bold(tagContent))
"strong" -> buffer.add(Bold(tagContent))
@ -84,12 +116,6 @@ class RichMessageParser {
else -> {
val substring = input.substring(urlIndex, urlEndIndex)
val last = substring.last()
if (last == '.' || last == ',') {
substring.dropLast(1)
}
val url = substring.removeSuffix(".").removeSuffix(",")
buffer.add(Link(url = url, label = url))
openIndex = if (substring.endsWith('.') || substring.endsWith(',')) urlEndIndex - 1 else urlEndIndex
@ -114,3 +140,7 @@ class RichMessageParser {
}
}
private fun String.removeHtmlEntities() = this.replace("&quot;", "\"").replace("&#39;", "'")
private fun String.dropTextFallback() = this.lines().dropWhile { it.startsWith("> ") || it.isEmpty() }.joinToString("")

View File

@ -1,8 +1,8 @@
package app.dapk.st.matrix.sync.internal.sync
import app.dapk.st.matrix.common.RichText
import app.dapk.st.matrix.common.RichText.Part.Link
import app.dapk.st.matrix.common.RichText.Part.Normal
import app.dapk.st.matrix.common.RichText.Part.*
import fixture.aUserId
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Ignore
import org.junit.Test
@ -23,6 +23,30 @@ class RichMessageParserTest {
expected = RichText(setOf(Normal("Hello world! "), Normal("foo bar"), Normal(" after paragraph")))
)
@Test
fun `replaces quote entity`() = runParserTest(
input = "Hello world! &quot;foo bar&quot;",
expected = RichText(setOf(Normal("Hello world! \"foo bar\"")))
)
@Test
fun `replaces apostrophe entity`() = runParserTest(
input = "Hello world! foo&#39;s bar",
expected = RichText(setOf(Normal("Hello world! foo's bar")))
)
@Test
fun `replaces people`() = runParserTest(
input = "Hello <@my-name:a-domain.foo>!",
expected = RichText(setOf(Normal("Hello "), Person(aUserId("@my-name:a-domain.foo"), "@my-name:a-domain.foo"), Normal("!")))
)
@Test
fun `replaces matrixdotto with person`() = runParserTest(
input = """Hello <a href="https://matrix.to/#/@a-name:foo.bar>a-name</a>: world""",
expected = RichText(setOf(Normal("Hello "), Person(aUserId("@a-name:foo.bar"), "@a-name"), Normal(" world")))
)
@Test
fun `skips header tags`() = runParserTest(
Case(
@ -62,15 +86,39 @@ class RichMessageParserTest {
)
@Test
fun `parses styling text`() = runParserTest(
input = "<em>hello</em> <strong>world</strong>",
expected = RichText(setOf(RichText.Part.Italic("hello"), Normal(" "), RichText.Part.Bold("world")))
fun `removes reply fallback`() = runParserTest(
input = """
<mx-reply>
<blockquote>
Original message
</blockquote>
</mx-reply>
Reply to message
""".trimIndent(),
expected = RichText(setOf(Normal("Reply to message")))
)
@Test
fun `parses raw reply text`() = runParserTest(
input = "> <@a-matrix-id:domain.foo> This is a reply",
expected = RichText(setOf(Normal("> <@a-matrix-id:domain.foo> This is a reply")))
fun `removes text fallback`() = runParserTest(
input = """
> <@user:domain.foo> Original message
> Some more content
Reply to message
""".trimIndent(),
expected = RichText(setOf(Normal("Reply to message")))
)
@Test
fun `parses styling text`() = runParserTest(
input = "<em>hello</em> <strong>world</strong>",
expected = RichText(setOf(Italic("hello"), Normal(" "), Bold("world")))
)
@Test
fun `parses invalid tags text`() = runParserTest(
input = ">><foo> ><>> << more content",
expected = RichText(setOf(Normal(">><foo> ><>> << more content")))
)
@Test
@ -80,7 +128,7 @@ class RichMessageParserTest {
expected = RichText(
setOf(
Normal("hello "),
RichText.Part.Bold("wor"),
Bold("wor"),
Normal("ld"),
)
)
@ -94,7 +142,7 @@ class RichMessageParserTest {
expected = RichText(
setOf(
Normal("hello "),
RichText.Part.Italic("wor"),
Italic("wor"),
Normal("ld"),
)
)