diff --git a/app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt b/app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt
index 5c71823..d04d507 100644
--- a/app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt
+++ b/app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt
@@ -5,6 +5,7 @@ private val markdownLinkPattern = Regex("""\[([^\]]+)]\(https?://[^)]+\)""")
private val htmlImagePattern = Regex("""
]*>""", RegexOption.IGNORE_CASE)
private val htmlAltPattern = Regex("""\balt=["']([^"']*)["']""", RegexOption.IGNORE_CASE)
private val bareUrlPattern = Regex("""https?://[^\s)>\]]+""")
+private val inlineCodePattern = Regex("""`([^`\n]+)`""")
internal fun markdownBodyPreview(
rawBody: String,
@@ -13,6 +14,10 @@ internal fun markdownBodyPreview(
if (rawBody.isBlank()) return emptyText
return rawBody
+ .withoutCodeFences()
+ .replace(inlineCodePattern) { match ->
+ match.groups[1]?.value.orEmpty()
+ }
.replace(htmlImagePattern) { match ->
htmlAltPattern.find(match.value)?.groups?.get(1)?.value?.cleanBadgeLabel()?.let { "badge: $it" }.orEmpty()
}
@@ -32,6 +37,11 @@ internal fun markdownBodyPreview(
.ifBlank { emptyText }
}
+private fun String.withoutCodeFences(): String =
+ lines()
+ .filterNot { it.trim().startsWith("```") }
+ .joinToString("\n")
+
private fun String.cleanBadgeLabel(): String? {
val cleaned = trim()
.removeSuffix(" badge")
diff --git a/app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt b/app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt
index f270bfd..d373ce1 100644
--- a/app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt
+++ b/app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt
@@ -28,9 +28,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.bniladridas.diff.R
import com.bniladridas.diff.model.ChangedFile
@@ -64,6 +67,8 @@ import com.bniladridas.diff.ui.theme.DiffRedSoft
import com.bniladridas.diff.ui.theme.PanelRaised
import com.bniladridas.diff.ui.theme.TextMuted
+private val inlineCodePattern = Regex("""`([^`\n]+)`""")
+
@Composable
fun PanelCard(
modifier: Modifier = Modifier,
@@ -110,6 +115,111 @@ private fun NeutralTag(text: String) {
Tag(text, mutedText(0.72f), MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f))
}
+@Composable
+private fun MarkdownBody(
+ rawBody: String,
+ emptyText: String,
+ modifier: Modifier = Modifier,
+) {
+ val blocks = markdownBlocks(rawBody, emptyText)
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(7.dp),
+ ) {
+ blocks.forEach { block ->
+ if (block.isCode) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(7.dp),
+ color = codeSurface(),
+ border = BorderStroke(1.dp, outlineText(0.45f)),
+ ) {
+ Text(
+ block.text,
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
+ color = codeText(),
+ style = MaterialTheme.typography.bodySmall,
+ fontFamily = FontFamily.Monospace,
+ )
+ }
+ } else {
+ Text(
+ text = inlineCodeText(block.text),
+ color = bodyText(),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun inlineCodeText(text: String) = buildAnnotatedString {
+ var cursor = 0
+ inlineCodePattern.findAll(text).forEach { match ->
+ append(text.substring(cursor, match.range.first))
+ withStyle(
+ SpanStyle(
+ color = codeText(),
+ fontFamily = FontFamily.Monospace,
+ background = codeSurface(),
+ ),
+ ) {
+ append(match.groups[1]?.value.orEmpty())
+ }
+ cursor = match.range.last + 1
+ }
+ append(text.substring(cursor))
+}
+
+private data class MarkdownBlock(
+ val text: String,
+ val isCode: Boolean,
+)
+
+private fun markdownBlocks(
+ rawBody: String,
+ emptyText: String,
+): List {
+ if (rawBody.isBlank()) return listOf(MarkdownBlock(emptyText, isCode = false))
+
+ val blocks = mutableListOf()
+ val textLines = mutableListOf()
+ val codeLines = mutableListOf()
+ var inCode = false
+
+ fun flushText() {
+ val text = markdownBodyPreview(textLines.joinToString("\n"), "")
+ if (text.isNotBlank()) blocks += MarkdownBlock(text, isCode = false)
+ textLines.clear()
+ }
+
+ fun flushCode() {
+ val text = codeLines.joinToString("\n").trim()
+ if (text.isNotBlank()) blocks += MarkdownBlock(text, isCode = true)
+ codeLines.clear()
+ }
+
+ rawBody.lines().forEach { line ->
+ if (line.trim().startsWith("```")) {
+ if (inCode) {
+ flushCode()
+ } else {
+ flushText()
+ }
+ inCode = !inCode
+ } else if (inCode) {
+ codeLines += line
+ } else {
+ textLines += line
+ }
+ }
+
+ if (inCode) flushCode() else flushText()
+
+ return blocks.ifEmpty { listOf(MarkdownBlock(emptyText, isCode = false)) }
+}
+
@Composable
fun BrandMark() {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -456,13 +566,7 @@ fun CommentCard(
overflow = TextOverflow.Ellipsis,
)
}
- Text(
- markdownBodyPreview(comment.body, "No comment body."),
- color = bodyText(),
- style = MaterialTheme.typography.bodyMedium,
- maxLines = 10,
- overflow = TextOverflow.Ellipsis,
- )
+ MarkdownBody(comment.body, "No comment body.")
if (comment.path != null && (onJumpToDiff != null || onDraftFix != null)) {
Row(horizontalArrangement = Arrangement.spacedBy(7.dp)) {
onJumpToDiff?.let {
diff --git a/app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt b/app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt
index ba5c2b4..32a2e5c 100644
--- a/app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt
+++ b/app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt
@@ -37,6 +37,31 @@ class MarkdownPreviewTest {
)
}
+ @Test
+ fun `preserves inline code without backtick markers`() {
+ val body = "Run `./gradlew test` before merging."
+
+ assertEquals(
+ "Run ./gradlew test before merging.",
+ markdownBodyPreview(body, "empty"),
+ )
+ }
+
+ @Test
+ fun `preserves fenced code content without fence markers`() {
+ val body = """
+ Output:
+ ```text
+ BUILD SUCCESSFUL
+ ```
+ """.trimIndent()
+
+ assertEquals(
+ "Output:\nBUILD SUCCESSFUL",
+ markdownBodyPreview(body, "empty"),
+ )
+ }
+
@Test
fun `keeps blank previews calm`() {
assertEquals("No comment body.", markdownBodyPreview(" ", "No comment body."))