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 package app.dapk.st.core
data class RichText(val parts: Set<Part>) { data class RichText(val parts: List<Part>) {
sealed interface Part { sealed interface Part {
data class Normal(val content: String) : Part data class Normal(val content: String) : Part
data class Link(val url: String, val label: 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.compose.rememberAsyncImagePainter
import coil.request.ImageRequest 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 { sealed interface BubbleModel {
val event: Event 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.Normal -> app.dapk.st.core.RichText.Part.Normal(it.content)
is RichText.Part.Person -> app.dapk.st.core.RichText.Part.Person(it.userId.value) is RichText.Part.Person -> app.dapk.st.core.RichText.Part.Person(it.userId.value)
} }
}.toSet()) })
} }
@Composable @Composable

View File

@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class RichText(@SerialName("parts") val parts: Set<Part>) { data class RichText(@SerialName("parts") val parts: List<Part>) {
@Serializable @Serializable
sealed interface Part { sealed interface Part {
@Serializable @Serializable
@ -27,7 +27,7 @@ data class RichText(@SerialName("parts") val parts: Set<Part>) {
} }
companion object { 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 { internal class HtmlParser {
fun test(startingFrom: Int, input: String): Int { fun test(startingFrom: Int, input: String) = input.indexOf(TAG_OPEN, startingFrom)
return input.indexOf(TAG_OPEN, startingFrom)
}
fun parseHtmlTags(input: String, searchIndex: Int, builder: PartBuilder, nestingLevel: Int = 0): SearchIndex = input.findTag( fun parseHtmlTags(input: String, searchIndex: Int, builder: PartBuilder, nestingLevel: Int = 0): SearchIndex = input.findTag(
fromIndex = searchIndex, fromIndex = searchIndex,
@ -32,19 +30,19 @@ internal class HtmlParser {
tagClose.next() 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( private fun parseTagWithContent(
tagName: String,
input: String, input: String,
tagName: String,
tagClose: Int, tagClose: Int,
builder: PartBuilder,
searchIndex: Int, searchIndex: Int,
tagOpen: Int, tagOpen: Int,
wholeTag: String, wholeTag: String,
builder: PartBuilder,
nestingLevel: Int nestingLevel: Int
): Int { ): Int {
val exitTag = "</$tagName>" val exitTag = "</$tagName>"
@ -124,7 +122,7 @@ internal class HtmlParser {
} }
"h1", "h2", "h3", "h4", "h5" -> { "h1", "h2", "h3", "h4", "h5" -> {
builder.appendBold(tagContent) builder.appendBold(tagContent.trim())
builder.appendNewline() builder.appendNewline()
exitTagCloseIndex exitTagCloseIndex
} }

View File

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

View File

@ -15,69 +15,73 @@ class RichMessageParserTest {
@Test @Test
fun `parses plain text`() = runParserTest( fun `parses plain text`() = runParserTest(
input = "Hello world!", input = "Hello world!",
expected = RichText(setOf(Normal("Hello world!"))) expected = RichText(listOf(Normal("Hello world!")))
) )
@Test @Test
fun `parses p tags`() = runParserTest( fun `parses p tags`() = runParserTest(
input = "<p>Hello world!</p><p>foo bar</p>after paragraph", 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 @Test
fun `parses nesting within p tags`() = runParserTest( fun `parses nesting within p tags`() = runParserTest(
input = "<p><b>Hello world!</b></p>", input = "<p><b>Hello world!</b></p>",
expected = RichText(setOf(Bold("Hello world!"))) expected = RichText(listOf(Bold("Hello world!")))
) )
@Test @Test
fun `replaces quote entity`() = runParserTest( fun `replaces quote entity`() = runParserTest(
input = "Hello world! &quot;foo bar&quot;", input = "Hello world! &quot;foo bar&quot;",
expected = RichText(setOf(Normal("Hello world! \"foo bar\""))) expected = RichText(listOf(Normal("Hello world! \"foo bar\"")))
) )
@Test @Test
fun `replaces apostrophe entity`() = runParserTest( fun `replaces apostrophe entity`() = runParserTest(
input = "Hello world! foo&#39;s bar", 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 @Test
fun `replaces people`() = runParserTest( fun `replaces people`() = runParserTest(
input = "Hello <@my-name:a-domain.foo>!", 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 @Test
fun `replaces matrixdotto with person`() = runParserTest( fun `replaces matrixdotto with person`() = runParserTest(
input = """Hello <a href="https://matrix.to/#/@a-name:foo.bar">a-name</a>: world""", 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 @Test
fun `parses header tags`() = runParserTest( fun `parses header tags`() = runParserTest(
Case( Case(
input = "<h1>hello</h1>", input = "<h1>hello</h1>",
expected = RichText(setOf(Bold("hello"))) expected = RichText(listOf(Bold("hello")))
), ),
Case( Case(
input = "<h1>hello</h1>text after title", 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( Case(
input = "<h2>hello</h2>", input = "<h2>hello</h2>",
expected = RichText(setOf(Bold("hello"))) expected = RichText(listOf(Bold("hello")))
), ),
Case( Case(
input = "<h3>hello</h3>", 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 @Test
fun `replaces br tags`() = runParserTest( fun `replaces br tags`() = runParserTest(
input = "Hello world!<br />next line<br />another line", 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( fun `parses lists`() = runParserTest(
Case( Case(
input = "<ul><li>content in list item</li><li>another item in list</li></ul>", 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( Case(
input = "<ol><li>content in list item</li><li>another item in list</li></ol>", 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( Case(
input = """<ol><li value="5">content in list item</li><li>another item in list</li></ol>""", 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( 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>""", 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( fun `parses urls`() = runParserTest(
Case( Case(
input = "https://google.com", 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( Case(
input = "https://google.com. after link", 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( Case(
input = "ending sentence with url https://google.com.", 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( Case(
input = "https://google.com<br>html after url", 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> </mx-reply>
Reply to message Reply to message
""".trimIndent(), """.trimIndent(),
expected = RichText(setOf(Normal("Reply to message"))) expected = RichText(listOf(Normal("Reply to message")))
) )
@Test @Test
@ -142,19 +146,19 @@ class RichMessageParserTest {
Reply to message Reply to message
""".trimIndent(), """.trimIndent(),
expected = RichText(setOf(Normal("Reply to message"))) expected = RichText(listOf(Normal("Reply to message")))
) )
@Test @Test
fun `parses styling text`() = runParserTest( fun `parses styling text`() = runParserTest(
input = "<em>hello</em> <strong>world</strong>", input = "<em>hello</em> <strong>world</strong>",
expected = RichText(setOf(Italic("hello"), Normal(" "), Bold("world"))) expected = RichText(listOf(Italic("hello"), Normal(" "), Bold("world")))
) )
@Test @Test
fun `parses invalid tags text`() = runParserTest( fun `parses invalid tags text`() = runParserTest(
input = ">><foo> ><>> << more content", input = ">><foo> ><>> << more content",
expected = RichText(setOf(Normal(">><foo> ><>> << more content"))) expected = RichText(listOf(Normal(">><foo> ><>> << more content")))
) )
@Test @Test
@ -162,7 +166,7 @@ class RichMessageParserTest {
Case( Case(
input = """hello <strong>wor</strong>ld""", input = """hello <strong>wor</strong>ld""",
expected = RichText( expected = RichText(
setOf( listOf(
Normal("hello "), Normal("hello "),
Bold("wor"), Bold("wor"),
Normal("ld"), Normal("ld"),
@ -176,7 +180,7 @@ class RichMessageParserTest {
Case( Case(
input = """hello <em>wor</em>ld""", input = """hello <em>wor</em>ld""",
expected = RichText( expected = RichText(
setOf( listOf(
Normal("hello "), Normal("hello "),
Italic("wor"), Italic("wor"),
Normal("ld"), Normal("ld"),
@ -191,7 +195,7 @@ class RichMessageParserTest {
Case( Case(
input = """hello <b><i>wor<i/><b/>ld""", input = """hello <b><i>wor<i/><b/>ld""",
expected = RichText( expected = RichText(
setOf( listOf(
Normal("hello "), Normal("hello "),
BoldItalic("wor"), BoldItalic("wor"),
Normal("ld"), Normal("ld"),
@ -201,7 +205,7 @@ class RichMessageParserTest {
Case( Case(
input = """<a href="www.google.com"><a href="www.google.com">www.google.com<a/><a/>""", input = """<a href="www.google.com"><a href="www.google.com">www.google.com<a/><a/>""",
expected = RichText( expected = RichText(
setOf( listOf(
Link(url = "www.google.com", label = "www.google.com"), Link(url = "www.google.com", label = "www.google.com"),
Link(url = "www.bing.com", label = "www.bing.com"), Link(url = "www.bing.com", label = "www.bing.com"),
) )
@ -214,7 +218,7 @@ class RichMessageParserTest {
Case( Case(
input = """hello world <a href="www.google.com">a link!</a> more content.""", input = """hello world <a href="www.google.com">a link!</a> more content.""",
expected = RichText( expected = RichText(
setOf( listOf(
Normal("hello world "), Normal("hello world "),
Link(url = "www.google.com", label = "a link!"), Link(url = "www.google.com", label = "a link!"),
Normal(" more content."), Normal(" more content."),
@ -224,7 +228,7 @@ class RichMessageParserTest {
Case( Case(
input = """<a href="www.google.com">www.google.com</a><a href="www.bing.com">www.bing.com</a>""", input = """<a href="www.google.com">www.google.com</a><a href="www.bing.com">www.bing.com</a>""",
expected = RichText( expected = RichText(
setOf( listOf(
Link(url = "www.google.com", label = "www.google.com"), Link(url = "www.google.com", label = "www.google.com"),
Link(url = "www.bing.com", label = "www.bing.com"), Link(url = "www.bing.com", label = "www.bing.com"),
) )