handling more text parsing cases for fallbacks and urls
This commit is contained in:
parent
fddcdaa50c
commit
9476fc5814
|
@ -7,6 +7,7 @@ data class RichText(val parts: Set<Part>) {
|
||||||
data class Bold(val content: String) : Part
|
data class Bold(val content: String) : Part
|
||||||
data class Italic(val content: String) : Part
|
data class Italic(val content: String) : Part
|
||||||
data class BoldItalic(val content: String) : Part
|
data class BoldItalic(val content: String) : Part
|
||||||
|
data class Person(val userId: String) : Part
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,14 +17,11 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.*
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.withStyle
|
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -265,6 +262,10 @@ val hyperLinkStyle = SpanStyle(
|
||||||
textDecoration = TextDecoration.Underline
|
textDecoration = TextDecoration.Underline
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val nameStyle = SpanStyle(
|
||||||
|
color = Color(0xff64B5F6),
|
||||||
|
)
|
||||||
|
|
||||||
fun RichText.toAnnotatedText() = buildAnnotatedString {
|
fun RichText.toAnnotatedText() = buildAnnotatedString {
|
||||||
parts.forEach {
|
parts.forEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
@ -278,6 +279,9 @@ fun RichText.toAnnotatedText() = buildAnnotatedString {
|
||||||
}
|
}
|
||||||
|
|
||||||
is RichText.Part.Normal -> append(it.content)
|
is RichText.Part.Normal -> append(it.content)
|
||||||
|
is RichText.Part.Person -> withStyle(nameStyle) {
|
||||||
|
append("@${it.userId.substringBefore(':').removePrefix("@")}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,4 +299,4 @@ private fun AuthorName(event: BubbleModel.Event, bubble: BubbleMeta) {
|
||||||
@Composable
|
@Composable
|
||||||
private fun BubbleMeta.textColor(): Color {
|
private fun BubbleMeta.textColor(): Color {
|
||||||
return if (this.isSelf) SmallTalkTheme.extendedColors.onSelfBubble else SmallTalkTheme.extendedColors.onOthersBubble
|
return if (this.isSelf) SmallTalkTheme.extendedColors.onSelfBubble else SmallTalkTheme.extendedColors.onOthersBubble
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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.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.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())
|
}.toSet())
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@ data class RichText(@SerialName("parts") val parts: Set<Part>) {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BoldItalic(@SerialName("content") val content: String) : Part
|
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 {
|
companion object {
|
||||||
|
@ -35,5 +38,6 @@ fun RichText.asString() = parts.joinToString(separator = "") {
|
||||||
is RichText.Part.Italic -> it.content
|
is RichText.Part.Italic -> it.content
|
||||||
is RichText.Part.Link -> it.label
|
is RichText.Part.Link -> it.label
|
||||||
is RichText.Part.Normal -> it.content
|
is RichText.Part.Normal -> it.content
|
||||||
|
is RichText.Part.Person -> it.userId.value
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
import app.dapk.st.matrix.common.RichText.Part.*
|
import app.dapk.st.matrix.common.RichText.Part.*
|
||||||
|
import app.dapk.st.matrix.common.UserId
|
||||||
|
|
||||||
class RichMessageParser {
|
class RichMessageParser {
|
||||||
|
|
||||||
fun parse(input: String): RichText {
|
fun parse(source: String): RichText {
|
||||||
|
val input = source
|
||||||
|
.removeHtmlEntities()
|
||||||
|
.dropTextFallback()
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
val buffer = mutableSetOf<RichText.Part>()
|
val buffer = mutableSetOf<RichText.Part>()
|
||||||
var openIndex = 0
|
var openIndex = 0
|
||||||
|
@ -21,12 +25,23 @@ class RichMessageParser {
|
||||||
val wholeTag = input.substring(foundIndex, closeIndex + 1)
|
val wholeTag = input.substring(foundIndex, closeIndex + 1)
|
||||||
val tagName = wholeTag.substring(1, wholeTag.indexOfFirst { it == '>' || it == ' ' })
|
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 (tagName == "br") {
|
||||||
if (openIndex != foundIndex) {
|
if (openIndex != foundIndex) {
|
||||||
buffer.add(Normal(input.substring(openIndex, foundIndex)))
|
buffer.add(Normal(input.substring(openIndex, foundIndex)))
|
||||||
}
|
}
|
||||||
buffer.add(Normal("\n"))
|
buffer.add(Normal("\n"))
|
||||||
openIndex = foundIndex + "<br />".length
|
openIndex = foundIndex + wholeTag.length
|
||||||
lastStartIndex = openIndex
|
lastStartIndex = openIndex
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -39,6 +54,14 @@ class RichMessageParser {
|
||||||
if (exitIndex == -1) {
|
if (exitIndex == -1) {
|
||||||
openIndex++
|
openIndex++
|
||||||
} else {
|
} else {
|
||||||
|
when (tagName) {
|
||||||
|
"mx-reply" -> {
|
||||||
|
openIndex = exitIndex + exitTag.length
|
||||||
|
lastStartIndex = openIndex
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (openIndex != foundIndex) {
|
if (openIndex != foundIndex) {
|
||||||
buffer.add(Normal(input.substring(openIndex, foundIndex)))
|
buffer.add(Normal(input.substring(openIndex, foundIndex)))
|
||||||
}
|
}
|
||||||
|
@ -49,7 +72,16 @@ class RichMessageParser {
|
||||||
when (tagName) {
|
when (tagName) {
|
||||||
"a" -> {
|
"a" -> {
|
||||||
val findHrefUrl = wholeTag.substringAfter("href=").replace("\"", "").removeSuffix(">")
|
val findHrefUrl = wholeTag.substringAfter("href=").replace("\"", "").removeSuffix(">")
|
||||||
buffer.add(Link(url = findHrefUrl, label = tagContent))
|
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))
|
"b" -> buffer.add(Bold(tagContent))
|
||||||
|
@ -84,12 +116,6 @@ class RichMessageParser {
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val substring = input.substring(urlIndex, urlEndIndex)
|
val substring = input.substring(urlIndex, urlEndIndex)
|
||||||
|
|
||||||
val last = substring.last()
|
|
||||||
if (last == '.' || last == ',') {
|
|
||||||
substring.dropLast(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = substring.removeSuffix(".").removeSuffix(",")
|
val url = substring.removeSuffix(".").removeSuffix(",")
|
||||||
buffer.add(Link(url = url, label = url))
|
buffer.add(Link(url = url, label = url))
|
||||||
openIndex = if (substring.endsWith('.') || substring.endsWith(',')) urlEndIndex - 1 else urlEndIndex
|
openIndex = if (substring.endsWith('.') || substring.endsWith(',')) urlEndIndex - 1 else urlEndIndex
|
||||||
|
@ -114,3 +140,7 @@ class RichMessageParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.removeHtmlEntities() = this.replace(""", "\"").replace("'", "'")
|
||||||
|
|
||||||
|
private fun String.dropTextFallback() = this.lines().dropWhile { it.startsWith("> ") || it.isEmpty() }.joinToString("")
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package app.dapk.st.matrix.sync.internal.sync
|
package app.dapk.st.matrix.sync.internal.sync
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.RichText
|
import app.dapk.st.matrix.common.RichText
|
||||||
import app.dapk.st.matrix.common.RichText.Part.Link
|
import app.dapk.st.matrix.common.RichText.Part.*
|
||||||
import app.dapk.st.matrix.common.RichText.Part.Normal
|
import fixture.aUserId
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -23,6 +23,30 @@ class RichMessageParserTest {
|
||||||
expected = RichText(setOf(Normal("Hello world! "), Normal("foo bar"), Normal(" after paragraph")))
|
expected = RichText(setOf(Normal("Hello world! "), Normal("foo bar"), Normal(" after paragraph")))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `replaces quote entity`() = runParserTest(
|
||||||
|
input = "Hello world! "foo bar"",
|
||||||
|
expected = RichText(setOf(Normal("Hello world! \"foo bar\"")))
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `replaces apostrophe entity`() = runParserTest(
|
||||||
|
input = "Hello world! foo'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
|
@Test
|
||||||
fun `skips header tags`() = runParserTest(
|
fun `skips header tags`() = runParserTest(
|
||||||
Case(
|
Case(
|
||||||
|
@ -62,15 +86,39 @@ class RichMessageParserTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `parses styling text`() = runParserTest(
|
fun `removes reply fallback`() = runParserTest(
|
||||||
input = "<em>hello</em> <strong>world</strong>",
|
input = """
|
||||||
expected = RichText(setOf(RichText.Part.Italic("hello"), Normal(" "), RichText.Part.Bold("world")))
|
<mx-reply>
|
||||||
|
<blockquote>
|
||||||
|
Original message
|
||||||
|
</blockquote>
|
||||||
|
</mx-reply>
|
||||||
|
Reply to message
|
||||||
|
""".trimIndent(),
|
||||||
|
expected = RichText(setOf(Normal("Reply to message")))
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `parses raw reply text`() = runParserTest(
|
fun `removes text fallback`() = runParserTest(
|
||||||
input = "> <@a-matrix-id:domain.foo> This is a reply",
|
input = """
|
||||||
expected = RichText(setOf(Normal("> <@a-matrix-id:domain.foo> This is a reply")))
|
> <@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
|
@Test
|
||||||
|
@ -80,7 +128,7 @@ class RichMessageParserTest {
|
||||||
expected = RichText(
|
expected = RichText(
|
||||||
setOf(
|
setOf(
|
||||||
Normal("hello "),
|
Normal("hello "),
|
||||||
RichText.Part.Bold("wor"),
|
Bold("wor"),
|
||||||
Normal("ld"),
|
Normal("ld"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -94,7 +142,7 @@ class RichMessageParserTest {
|
||||||
expected = RichText(
|
expected = RichText(
|
||||||
setOf(
|
setOf(
|
||||||
Normal("hello "),
|
Normal("hello "),
|
||||||
RichText.Part.Italic("wor"),
|
Italic("wor"),
|
||||||
Normal("ld"),
|
Normal("ld"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue