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 c2bdd24e..8f5de1c8 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 @@ -63,7 +63,7 @@ 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+(.*)""") +private val ORDERED_RE = Regex("""^\s*(\d+)\.\s+(.*)""") /** * Renders chat assistant text as Markdown — but ONLY once the message has finished streaming. @@ -754,12 +754,25 @@ internal fun parseBlocks(src: String): List { } ORDERED_RE.matches(line) -> { - val items = mutableListOf() - while (i < lines.size && ORDERED_RE.matches(lines[i])) { - items.add(ORDERED_RE.find(lines[i])!!.groupValues[1]) - i++ + // Capture the source number so custom/loose lists keep their order. + // A blank line only breaks the list when the next non-blank line is NOT an + // ordered item — loose lists (blank lines between items) stay one list. + val items = mutableListOf>() + var j = i + while (j < lines.size) { + val m = ORDERED_RE.find(lines[j]) + if (m != null) { + val n = m.groupValues[1].toIntOrNull() ?: (items.size + 1) + items.add(n to m.groupValues[2]) + j++ + } else if (lines[j].isBlank() && j + 1 < lines.size && ORDERED_RE.matches(lines[j + 1])) { + j++ + } else { + break + } } - items.forEachIndexed { idx, t -> blocks.add(MdBlock.Ordered(idx + 1, t)) } + items.forEach { (n, t) -> blocks.add(MdBlock.Ordered(n, t)) } + i = j } // Standalone Markdown image: ![alt](uri) on its own line. diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextFeatureTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextFeatureTest.kt index 08745f49..636e7366 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextFeatureTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/MarkdownTextFeatureTest.kt @@ -288,4 +288,51 @@ class MarkdownTextFeatureTest { assertTrue(videoAttachment.isVideo) assertFalse(videoAttachment.isImage) } + + // 16. ORDERED LIST — loose lists must preserve numbering (issue #959) + @Test + fun testOrderedList_looseListPreservesNumbers() { + val md = + """ + 1. First + + 2. Second + + 3. Third + """.trimIndent() + val blocks = parseBlocks(md).filterIsInstance() + assertEquals(3, blocks.size) + assertEquals(1, blocks[0].index) + assertEquals(2, blocks[1].index) + assertEquals(3, blocks[2].index) + assertEquals("First", blocks[0].text) + assertEquals("Third", blocks[2].text) + } + + // 16b. ORDERED LIST — custom starting number is preserved (issue #959) + @Test + fun testOrderedList_customStartNumberPreserved() { + val md = + """ + 2. Two + 3. Three + """.trimIndent() + val blocks = parseBlocks(md).filterIsInstance() + assertEquals(2, blocks.size) + assertEquals(2, blocks[0].index) + assertEquals(3, blocks[1].index) + } + + // 16c. ORDERED LIST — tight list still numbers sequentially + @Test + fun testOrderedList_tightList() { + val md = + """ + 1. one + 2. two + 3. three + """.trimIndent() + val blocks = parseBlocks(md).filterIsInstance() + assertEquals(listOf(1, 2, 3), blocks.map { it.index }) + } }