From e13ce95b833c87cabef91e34f0fa475ed69f8c2d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 23 Oct 2022 20:04:38 +0100 Subject: [PATCH] adding super naive html parsing for style options --- .../app/dapk/st/matrix/common/RichText.kt | 19 +++ .../sync/internal/sync/RichMessageParser.kt | 57 +++++++++ .../internal/sync/RichMessageParserTest.kt | 116 ++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RichText.kt create mode 100644 matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParser.kt create mode 100644 matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParserTest.kt diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RichText.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RichText.kt new file mode 100644 index 0000000..1db3d4e --- /dev/null +++ b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/RichText.kt @@ -0,0 +1,19 @@ +package app.dapk.st.matrix.common + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RichText(@SerialName("parts") val parts: Set) { + @Serializable + sealed interface Part { + @Serializable + data class Normal(@SerialName("content") val content: String) : Part + data class Link(@SerialName("url") val url: String, @SerialName("label") val label: String) : Part + data class Bold(@SerialName("content") val content: String) : Part + data class Italic(@SerialName("content") val content: String) : Part + data class BoldItalic(@SerialName("content") val content: String) : Part + } +} + +fun RichText.asString() = parts.joinToString(separator = "") \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParser.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParser.kt new file mode 100644 index 0000000..8545842 --- /dev/null +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParser.kt @@ -0,0 +1,57 @@ +package app.dapk.st.matrix.sync.internal.sync + +import app.dapk.st.matrix.common.RichText + +data class Tag(val name: String, val inner: String, val content: String) + +class RichMessageParser { + + fun parse(input: String): RichText { + val buffer = mutableSetOf() + var openIndex = 0 + var closeIndex = 0 + while (openIndex != -1) { + val foundIndex = input.indexOf('<', startIndex = openIndex) + if (foundIndex != -1) { + closeIndex = input.indexOf('>', startIndex = openIndex) + + if (closeIndex == -1) { + openIndex++ + } else { + val wholeTag = input.substring(foundIndex, closeIndex + 1) + val tagName = wholeTag.substring(1, wholeTag.indexOfFirst { it == '>' || it == ' ' }) + val exitTag = "<$tagName/>" + val exitIndex = input.indexOf(exitTag, startIndex = closeIndex + 1) + val tagContent = input.substring(closeIndex + 1, exitIndex) + + val tag = Tag(name = tagName, wholeTag, tagContent) + + println("found $tag") + if (openIndex != foundIndex) { + buffer.add(RichText.Part.Normal(input.substring(openIndex, foundIndex))) + } + openIndex = exitIndex + exitTag.length + + when (tagName) { + "a" -> { + val findHrefUrl = wholeTag.substringAfter("href=").replace("\"", "").removeSuffix(">") + buffer.add(RichText.Part.Link(url = findHrefUrl, label = tag.content)) + } + + "b" -> buffer.add(RichText.Part.Bold(tagContent)) + "i" -> buffer.add(RichText.Part.Italic(tagContent)) + } + } + } else { + // exit + if (openIndex < input.length) { + buffer.add(RichText.Part.Normal(input.substring(openIndex))) + } + break + } + } + + return RichText(buffer) + } + +} diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParserTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParserTest.kt new file mode 100644 index 0000000..2cbd6c1 --- /dev/null +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParserTest.kt @@ -0,0 +1,116 @@ +package app.dapk.st.matrix.sync.internal.sync + +import app.dapk.st.matrix.common.RichText +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Ignore +import org.junit.Test + +class RichMessageParserTest { + + private val parser = RichMessageParser() + + @Test + fun `parses plain text`() = runParserTest( + input = "Hello world!", + expected = RichText(setOf(RichText.Part.Normal("Hello world!"))) + ) + + @Test + fun `parses nested b tags`() = runParserTest( + Case( + input = """hello world""", + expected = RichText( + setOf( + RichText.Part.Normal("hello "), + RichText.Part.Bold("wor"), + RichText.Part.Normal("ld"), + ) + ) + ), + ) + + @Test + fun `parses nested i tags`() = runParserTest( + Case( + input = """hello world""", + expected = RichText( + setOf( + RichText.Part.Normal("hello "), + RichText.Part.Italic("wor"), + RichText.Part.Normal("ld"), + ) + ) + ), + ) + + @Ignore // TODO + @Test + fun `parses nested tags`() = runParserTest( + Case( + input = """hello world""", + expected = RichText( + setOf( + RichText.Part.Normal("hello "), + RichText.Part.BoldItalic("wor"), + RichText.Part.Normal("ld"), + ) + ) + ), + Case( + input = """www.google.com""", + expected = RichText( + setOf( + RichText.Part.Link(url = "www.google.com", label = "www.google.com"), + RichText.Part.Link(url = "www.bing.com", label = "www.bing.com"), + ) + ) + ) + ) + + @Test + fun `parses 'a' tags`() = runParserTest( + Case( + input = """hello world a link! more content.""", + expected = RichText( + setOf( + RichText.Part.Normal("hello world "), + RichText.Part.Link(url = "www.google.com", label = "a link!"), + RichText.Part.Normal(" more content."), + ) + ) + ), + Case( + input = """www.google.comwww.bing.com""", + expected = RichText( + setOf( + RichText.Part.Link(url = "www.google.com", label = "www.google.com"), + RichText.Part.Link(url = "www.bing.com", label = "www.bing.com"), + ) + ) + ), + ) + + private fun runParserTest(vararg cases: Case) { + val errors = mutableListOf() + cases.forEach { + runCatching { runParserTest(it.input, it.expected) }.onFailure { errors.add(it) } + } + if (errors.isNotEmpty()) { + throw CompositeThrowable(errors) + } + } + + private fun runParserTest(input: String, expected: RichText) { + val result = parser.parse(input) + + result shouldBeEqualTo expected + } +} + +private data class Case(val input: String, val expected: RichText) + +class CompositeThrowable(inner: List) : Throwable() { + init { + inner.forEach { addSuppressed(it) } + } +} \ No newline at end of file