Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/java/com/bniladridas/diff/ui/MarkdownPreview.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ private val markdownLinkPattern = Regex("""\[([^\]]+)]\(https?://[^)]+\)""")
private val htmlImagePattern = Regex("""<img\b[^>]*>""", 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,
Expand All @@ -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()
}
Expand All @@ -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")
Expand Down
118 changes: 111 additions & 7 deletions app/src/main/java/com/bniladridas/diff/ui/components/DiffComponents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<MarkdownBlock> {
if (rawBody.isBlank()) return listOf(MarkdownBlock(emptyText, isCode = false))

val blocks = mutableListOf<MarkdownBlock>()
val textLines = mutableListOf<String>()
val codeLines = mutableListOf<String>()
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) {
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions app/src/test/java/com/bniladridas/diff/ui/MarkdownPreviewTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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."))
Expand Down