diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt index d5c5c3f6..b2573442 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt @@ -50,6 +50,9 @@ import com.m57.hermescontrol.theme.searchHighlightColors private val URL_PATTERN = Regex("""https?://[^\s)>\[\]"'‘’]+""") private val TABLE_COL_WIDTH = 160.dp private val FN_DEF_RE = Regex("""^\[\^([^\]]+)\]:\s*(.*)$""") +private val BULLET_RE = Regex("""^\s*[-*+]\s+(.*)""") +private val TASK_RE = Regex("""^\s*[-*+]\s+\[([ xX])\]\s+(.*)""") +private val ORDERED_RE = Regex("""^\s*\d+\.\s+(.*)""") /** * Renders chat assistant text as Markdown — but ONLY once the message has finished streaming. @@ -408,9 +411,6 @@ internal fun parseBlocks(src: String): List { val lines = src.lines() val blocks = mutableListOf() val footnotes = mutableListOf() - val bulletRe = Regex("""^\s*[-*+]\s+(.*)""") - val taskRe = Regex("""^\s*[-*+]\s+\[([ xX])\]\s+(.*)""") - val orderedRe = Regex("""^\s*\d+\.\s+(.*)""") var i = 0 while (i < lines.size) { @@ -479,7 +479,7 @@ internal fun parseBlocks(src: String): List { } // Definition list: term line followed by one+ ": definition" lines - isDefListStart(lines, i, bulletRe, orderedRe) -> { + isDefListStart(lines, i) -> { val term = line.trim() val defs = mutableListOf() i++ @@ -490,32 +490,32 @@ internal fun parseBlocks(src: String): List { blocks.add(MdBlock.DefList(listOf(DefItem(term, defs)))) } - taskRe.matches(line) -> { - val m = taskRe.find(line)!! + TASK_RE.matches(line) -> { + val m = TASK_RE.find(line)!! blocks.add(MdBlock.Task(m.groupValues[1].equals("x", ignoreCase = true), m.groupValues[2])) i++ } - bulletRe.matches(line) -> { + BULLET_RE.matches(line) -> { val items = mutableListOf() - while (i < lines.size && bulletRe.matches(lines[i]) && !taskRe.matches(lines[i])) { - items.add(bulletRe.find(lines[i])!!.groupValues[1]) + while (i < lines.size && BULLET_RE.matches(lines[i]) && !TASK_RE.matches(lines[i])) { + items.add(BULLET_RE.find(lines[i])!!.groupValues[1]) i++ } items.forEach { blocks.add(MdBlock.Bullet(it)) } } - orderedRe.matches(line) -> { + ORDERED_RE.matches(line) -> { val items = mutableListOf() - while (i < lines.size && orderedRe.matches(lines[i])) { - items.add(orderedRe.find(lines[i])!!.groupValues[1]) + while (i < lines.size && ORDERED_RE.matches(lines[i])) { + items.add(ORDERED_RE.find(lines[i])!!.groupValues[1]) i++ } items.forEachIndexed { idx, t -> blocks.add(MdBlock.Ordered(idx + 1, t)) } } else -> { - i = fallthroughToParagraph(lines, i, bulletRe, orderedRe, blocks) + i = fallthroughToParagraph(lines, i, blocks) } } } @@ -585,12 +585,10 @@ private fun splitRow(line: String): List { private fun isDefListStart( lines: List, i: Int, - bulletRe: Regex, - orderedRe: Regex, ): Boolean { val l = lines[i].trim() if (l.isBlank() || l.startsWith("#") || l.startsWith(">") || l.startsWith("```")) return false - if (bulletRe.matches(lines[i]) || orderedRe.matches(lines[i])) return false + if (BULLET_RE.matches(lines[i]) || ORDERED_RE.matches(lines[i])) return false if (i + 1 >= lines.size) return false return lines[i + 1].trim().startsWith(":") } @@ -604,8 +602,6 @@ private fun isValidHeading(line: String): Boolean { private fun fallthroughToParagraph( lines: List, start: Int, - bulletRe: Regex, - orderedRe: Regex, blocks: MutableList, ): Int { var i = start @@ -616,11 +612,11 @@ private fun fallthroughToParagraph( !lines[i].startsWith("```") && !isValidHeading(lines[i]) && !lines[i].startsWith(">") && - !bulletRe.matches(lines[i]) && - !orderedRe.matches(lines[i]) && + !BULLET_RE.matches(lines[i]) && + !ORDERED_RE.matches(lines[i]) && !isHorizontalRule(lines[i]) && !isTableStart(lines, i) && - !isDefListStart(lines, i, bulletRe, orderedRe) && + !isDefListStart(lines, i) && FN_DEF_RE.matchAt(lines[i], 0) == null ) { para.add(lines[i]) diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextBenchmark.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextBenchmark.kt new file mode 100644 index 00000000..0ea45c65 --- /dev/null +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextBenchmark.kt @@ -0,0 +1,49 @@ +package com.m57.hermescontrol.ui.chat + +import org.junit.Test +import kotlin.system.measureTimeMillis + +class MarkdownTextBenchmark { + @Test + fun benchmarkParseBlocks() { + val md = + """ + # Header + - item 1 + - item 2 + - item 3 + + 1. ordered 1 + 2. ordered 2 + + - [ ] task 1 + - [x] task 2 + + Some text with some paragraphs and things. + It's a long test string to give the parser something to do. + + ```kotlin + fun main() { + println("hello") + } + ``` + + > A quote + > Another quote + """.trimIndent() + + // Warmup + for (i in 1..500) { + parseBlocks(md) + } + + val time = + measureTimeMillis { + for (i in 1..10000) { + parseBlocks(md) + } + } + + println("BENCHMARK_RESULT: parseBlocks 10000 iterations took ${time}ms") + } +}