You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Compose Web(HTML)应用中渲染Markdown?

在Compose Web中实现Markdown渲染的方案

下面是两种实用的实现方式,你可以根据需求选择:

方案一:基于CommonMark库自定义Compose可组合项

这种方式是将Markdown解析为AST(抽象语法树),然后遍历AST生成对应的Compose UI组件,完全贴合Compose的风格。

步骤1:添加依赖

在你的build.gradle.kts中引入CommonMark核心库及扩展(如果需要支持表格、代码块等扩展语法):

dependencies {
    implementation("org.commonmark:commonmark:0.21.0")
    // 可选扩展:GFM表格、代码围栏
    implementation("org.commonmark:commonmark-ext-gfm-tables:0.21.0")
    implementation("org.commonmark:commonmark-ext-code-fences:0.21.0")
}

步骤2:实现Markdown解析与渲染组件

编写一个可组合函数,负责解析Markdown并渲染为Compose UI:

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.text.BasicText
import org.commonmark.node.*
import org.commonmark.parser.Parser

@Composable
fun MarkdownText(markdown: String) {
    val parser = remember { Parser.builder().build() }
    val document = remember(markdown) { parser.parse(markdown) }
    
    RenderNode(document)
}

@Composable
private fun RenderNode(node: Node) {
    when (node) {
        is Document -> {
            var child = node.firstChild
            while (child != null) {
                RenderNode(child)
                child = child.next
            }
        }
        is Paragraph -> {
            BasicText(
                text = node.textContent,
                style = androidx.compose.ui.text.TextStyle(fontSize = 16.sp)
            )
            Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp))
        }
        is Heading -> {
            val textStyle = when (node.level) {
                1 -> SpanStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold)
                2 -> SpanStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold)
                3 -> SpanStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold)
                else -> SpanStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold)
            }
            BasicText(
                text = node.textContent,
                style = androidx.compose.ui.text.TextStyle(textStyle)
            )
            Spacer(modifier = androidx.compose.ui.Modifier.height(12.dp))
        }
        is BlockQuote -> {
            androidx.compose.foundation.layout.Box(
                modifier = androidx.compose.ui.Modifier
                    .background(Color.LightGray.copy(alpha = 0.3f))
                    .padding(8.dp)
            ) {
                var child = node.firstChild
                while (child != null) {
                    RenderNode(child)
                    child = child.next
                }
            }
            Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp))
        }
        is CodeBlock -> {
            androidx.compose.foundation.layout.Box(
                modifier = androidx.compose.ui.Modifier
                    .background(Color.DarkGray.copy(alpha = 0.1f))
                    .padding(12.dp)
            ) {
                BasicText(
                    text = node.literal,
                    style = androidx.compose.ui.text.TextStyle(
                        fontFamily = FontFamily.Monospace,
                        fontSize = 14.sp
                    )
                )
            }
            Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp))
        }
        is BulletList -> {
            Column {
                var child = node.firstChild
                while (child != null) {
                    RenderNode(child)
                    child = child.next
                }
            }
            Spacer(modifier = androidx.compose.ui.Modifier.height(8.dp))
        }
        is ListItem -> {
            Row(verticalAlignment = androidx.compose.ui.Alignment.Top) {
                BasicText(text = "• ", style = androidx.compose.ui.text.TextStyle(fontSize = 16.sp))
                Column {
                    var child = node.firstChild
                    while (child != null) {
                        RenderNode(child)
                        child = child.next
                    }
                }
            }
        }
        is Text -> {
            val style = when (node.parent) {
                is StrongEmphasis -> SpanStyle(fontWeight = FontWeight.Bold)
                is Emphasis -> SpanStyle(fontStyle = FontStyle.Italic)
                else -> SpanStyle()
            }
            BasicText(
                text = node.literal,
                style = androidx.compose.ui.text.TextStyle(style)
            )
        }
        // 可继续扩展支持链接、表格等节点类型
        else -> {
            BasicText(text = node.textContent)
        }
    }
}

步骤3:使用组件

在你的Compose Web应用中直接调用MarkdownText:

@Composable
fun MyApp() {
    val sampleMarkdown = """
        # 欢迎使用Markdown渲染
        ## 二级标题
        这里有**粗体**和*斜体*文本。
        
        > 这是一段引用内容
        
        - 列表项1
        - 列表项2
        
        ```kotlin
        fun main() {
            println("Hello Compose Web!")
        }
        ```
    """.trimIndent()
    
    MarkdownText(sampleMarkdown)
}

方案二:通过WebView渲染HTML

如果不想手动处理所有Markdown节点,可以先将Markdown转换为HTML,再用WebView加载渲染,开发成本更低。

实现代码

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.viewinterop.AndroidView
import android.webkit.WebView
import android.webkit.WebViewClient
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer

@Composable
fun MarkdownWebView(markdown: String) {
    val htmlContent = remember(markdown) {
        val parser = Parser.builder().build()
        val document = parser.parse(markdown)
        val renderer = HtmlRenderer.builder().build()
        renderer.render(document)
    }
    
    AndroidView(factory = { context ->
        WebView(context).apply {
            webViewClient = WebViewClient()
            settings.javaScriptEnabled = false
            loadData(htmlContent, "text/html", "UTF-8")
        }
    }, update = { webView ->
        webView.loadData(htmlContent, "text/html", "UTF-8")
    })
}

两种方案对比

方案优点缺点
自定义Compose渲染完全贴合Compose风格,样式可高度自定义,性能较好需要手动处理各类Markdown节点,扩展语法需额外开发
WebView渲染支持全量Markdown语法,开发成本低与Compose交互性差,样式可能与应用风格脱节,性能略逊

内容的提问来源于stack exchange,提问作者Serge

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 01:18:00