allowing duplicate text content and handling header line breaks

This commit is contained in:
Adam Brown 2022-10-29 13:59:29 +01:00
parent 2fab30060f
commit 918f186560
7 changed files with 49 additions and 45 deletions

View File

@ -1,6 +1,6 @@
package app.dapk.st.core
data class RichText(val parts: Set<Part>) {
data class RichText(val parts: List<Part>) {
sealed interface Part {
data class Normal(val content: String) : Part
data class Link(val url: String, val label: String) : Part

View File

@ -30,7 +30,7 @@ import app.dapk.st.core.RichText
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
private val ENCRYPTED_MESSAGE = RichText(setOf(RichText.Part.Normal("Encrypted message")))
private val ENCRYPTED_MESSAGE = RichText(listOf(RichText.Part.Normal("Encrypted message")))
sealed interface BubbleModel {
val event: Event

View File

@ -238,7 +238,7 @@ private fun RichText.toApp(): app.dapk.st.core.RichText {
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())
})
}
@Composable

View File

@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class RichText(@SerialName("parts") val parts: Set<Part>) {
data class RichText(@SerialName("parts") val parts: List<Part>) {
@Serializable
sealed interface Part {
@Serializable
@ -27,7 +27,7 @@ data class RichText(@SerialName("parts") val parts: Set<Part>) {
}
companion object {
fun of(text: String) = RichText(setOf(RichText.Part.Normal(text)))
fun of(text: String) = RichText(listOf(RichText.Part.Normal(text)))
}
}

View File

@ -9,9 +9,7 @@ private val SKIPPED_TAGS = setOf("mx-reply")
internal class HtmlParser {
fun test(startingFrom: Int, input: String): Int {
return input.indexOf(TAG_OPEN, startingFrom)
}
fun test(startingFrom: Int, input: String) = input.indexOf(TAG_OPEN, startingFrom)
fun parseHtmlTags(input: String, searchIndex: Int, builder: PartBuilder, nestingLevel: Int = 0): SearchIndex = input.findTag(
fromIndex = searchIndex,
@ -32,19 +30,19 @@ internal class HtmlParser {
tagClose.next()
}
else -> parseTagWithContent(tagName, input, tagClose, builder, searchIndex, tagOpen, wholeTag, nestingLevel)
else -> parseTagWithContent(input, tagName, tagClose, searchIndex, tagOpen, wholeTag, builder, nestingLevel)
}
}
)
private fun parseTagWithContent(
tagName: String,
input: String,
tagName: String,
tagClose: Int,
builder: PartBuilder,
searchIndex: Int,
tagOpen: Int,
wholeTag: String,
builder: PartBuilder,
nestingLevel: Int
): Int {
val exitTag = "</$tagName>"
@ -124,7 +122,7 @@ internal class HtmlParser {
}
"h1", "h2", "h3", "h4", "h5" -> {
builder.appendBold(tagContent)
builder.appendBold(tagContent.trim())
builder.appendNewline()
exitTagCloseIndex
}

View File

@ -7,9 +7,11 @@ internal class PartBuilder {
private var normalBuffer = StringBuilder()
private val parts = mutableSetOf<RichText.Part>()
private val parts = mutableListOf<RichText.Part>()
fun appendText(value: String) {
println("append text")
normalBuffer.append(value.cleanFirstTextLine())
}
@ -35,11 +37,11 @@ internal class PartBuilder {
parts.add(RichText.Part.Link(url, label ?: url))
}
fun build(): Set<RichText.Part> {
fun build(): List<RichText.Part> {
flushNormalBuffer()
val last = parts.last()
if (last is RichText.Part.Normal) {
parts.remove(last)
parts.removeLast()
val newContent = last.content.trimEnd()
if (newContent.isNotEmpty()) {
parts.add(last.copy(content = newContent))

View File

@ -15,69 +15,73 @@ class RichMessageParserTest {
@Test
fun `parses plain text`() = runParserTest(
input = "Hello world!",
expected = RichText(setOf(Normal("Hello world!")))
expected = RichText(listOf(Normal("Hello world!")))
)
@Test
fun `parses p tags`() = runParserTest(
input = "<p>Hello world!</p><p>foo bar</p>after paragraph",
expected = RichText(setOf(Normal("Hello world!\nfoo bar\nafter paragraph")))
expected = RichText(listOf(Normal("Hello world!\nfoo bar\nafter paragraph")))
)
@Test
fun `parses nesting within p tags`() = runParserTest(
input = "<p><b>Hello world!</b></p>",
expected = RichText(setOf(Bold("Hello world!")))
expected = RichText(listOf(Bold("Hello world!")))
)
@Test
fun `replaces quote entity`() = runParserTest(
input = "Hello world! &quot;foo bar&quot;",
expected = RichText(setOf(Normal("Hello world! \"foo bar\"")))
expected = RichText(listOf(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")))
expected = RichText(listOf(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("!")))
expected = RichText(listOf(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")))
expected = RichText(listOf(Normal("Hello "), Person(aUserId("@a-name:foo.bar"), "@a-name"), Normal(" world")))
)
@Test
fun `parses header tags`() = runParserTest(
Case(
input = "<h1>hello</h1>",
expected = RichText(setOf(Bold("hello")))
expected = RichText(listOf(Bold("hello")))
),
Case(
input = "<h1>hello</h1>text after title",
expected = RichText(setOf(Bold("hello"), Normal("\ntext after title")))
expected = RichText(listOf(Bold("hello"), Normal("\ntext after title")))
),
Case(
input = "<h2>hello</h2>",
expected = RichText(setOf(Bold("hello")))
expected = RichText(listOf(Bold("hello")))
),
Case(
input = "<h3>hello</h3>",
expected = RichText(setOf(Bold("hello")))
expected = RichText(listOf(Bold("hello")))
),
Case(
input = "<h1>1</h1>\n<h2>1</h2>\n<h3>1</h3>\n",
expected = RichText(listOf(Bold("1"), Normal("\n\n"), Bold("1"), Normal("\n\n"), Bold("1")))
),
)
@Test
fun `replaces br tags`() = runParserTest(
input = "Hello world!<br />next line<br />another line",
expected = RichText(setOf(Normal("Hello world!\nnext line\nanother line")))
expected = RichText(listOf(Normal("Hello world!\nnext line\nanother line")))
)
@ -85,19 +89,19 @@ class RichMessageParserTest {
fun `parses lists`() = runParserTest(
Case(
input = "<ul><li>content in list item</li><li>another item in list</li></ul>",
expected = RichText(setOf(Normal("- content in list item\n- another item in list")))
expected = RichText(listOf(Normal("- content in list item\n- another item in list")))
),
Case(
input = "<ol><li>content in list item</li><li>another item in list</li></ol>",
expected = RichText(setOf(Normal("1. content in list item\n2. another item in list")))
expected = RichText(listOf(Normal("1. content in list item\n2. another item in list")))
),
Case(
input = """<ol><li value="5">content in list item</li><li>another item in list</li></ol>""",
expected = RichText(setOf(Normal("5. content in list item\n6. another item in list")))
expected = RichText(listOf(Normal("5. content in list item\n6. another item in list")))
),
Case(
input = """<ol><li value="3">content in list item</li><li>another item in list</li><li value="10">another change</li><li>without value</li></ol>""",
expected = RichText(setOf(Normal("3. content in list item\n4. another item in list\n10. another change\n11. without value")))
expected = RichText(listOf(Normal("3. content in list item\n4. another item in list\n10. another change\n11. without value")))
),
)
@ -105,19 +109,19 @@ class RichMessageParserTest {
fun `parses urls`() = runParserTest(
Case(
input = "https://google.com",
expected = RichText(setOf(Link("https://google.com", "https://google.com")))
expected = RichText(listOf(Link("https://google.com", "https://google.com")))
),
Case(
input = "https://google.com. after link",
expected = RichText(setOf(Link("https://google.com", "https://google.com"), Normal(". after link")))
expected = RichText(listOf(Link("https://google.com", "https://google.com"), Normal(". after link")))
),
Case(
input = "ending sentence with url https://google.com.",
expected = RichText(setOf(Normal("ending sentence with url "), Link("https://google.com", "https://google.com"), Normal(".")))
expected = RichText(listOf(Normal("ending sentence with url "), Link("https://google.com", "https://google.com"), Normal(".")))
),
Case(
input = "https://google.com<br>html after url",
expected = RichText(setOf(Link("https://google.com", "https://google.com"), Normal("\nhtml after url")))
expected = RichText(listOf(Link("https://google.com", "https://google.com"), Normal("\nhtml after url")))
),
)
@ -131,7 +135,7 @@ class RichMessageParserTest {
</mx-reply>
Reply to message
""".trimIndent(),
expected = RichText(setOf(Normal("Reply to message")))
expected = RichText(listOf(Normal("Reply to message")))
)
@Test
@ -142,19 +146,19 @@ class RichMessageParserTest {
Reply to message
""".trimIndent(),
expected = RichText(setOf(Normal("Reply to message")))
expected = RichText(listOf(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")))
expected = RichText(listOf(Italic("hello"), Normal(" "), Bold("world")))
)
@Test
fun `parses invalid tags text`() = runParserTest(
input = ">><foo> ><>> << more content",
expected = RichText(setOf(Normal(">><foo> ><>> << more content")))
expected = RichText(listOf(Normal(">><foo> ><>> << more content")))
)
@Test
@ -162,7 +166,7 @@ class RichMessageParserTest {
Case(
input = """hello <strong>wor</strong>ld""",
expected = RichText(
setOf(
listOf(
Normal("hello "),
Bold("wor"),
Normal("ld"),
@ -176,7 +180,7 @@ class RichMessageParserTest {
Case(
input = """hello <em>wor</em>ld""",
expected = RichText(
setOf(
listOf(
Normal("hello "),
Italic("wor"),
Normal("ld"),
@ -191,7 +195,7 @@ class RichMessageParserTest {
Case(
input = """hello <b><i>wor<i/><b/>ld""",
expected = RichText(
setOf(
listOf(
Normal("hello "),
BoldItalic("wor"),
Normal("ld"),
@ -201,7 +205,7 @@ class RichMessageParserTest {
Case(
input = """<a href="www.google.com"><a href="www.google.com">www.google.com<a/><a/>""",
expected = RichText(
setOf(
listOf(
Link(url = "www.google.com", label = "www.google.com"),
Link(url = "www.bing.com", label = "www.bing.com"),
)
@ -214,7 +218,7 @@ class RichMessageParserTest {
Case(
input = """hello world <a href="www.google.com">a link!</a> more content.""",
expected = RichText(
setOf(
listOf(
Normal("hello world "),
Link(url = "www.google.com", label = "a link!"),
Normal(" more content."),
@ -224,7 +228,7 @@ class RichMessageParserTest {
Case(
input = """<a href="www.google.com">www.google.com</a><a href="www.bing.com">www.bing.com</a>""",
expected = RichText(
setOf(
listOf(
Link(url = "www.google.com", label = "www.google.com"),
Link(url = "www.bing.com", label = "www.bing.com"),
)