Skip to content

Instantly share code, notes, and snippets.

@LennyLizowzskiy
Last active February 19, 2023 19:35
Show Gist options
  • Save LennyLizowzskiy/099db3520b35811bac73937d391ef701 to your computer and use it in GitHub Desktop.
Save LennyLizowzskiy/099db3520b35811bac73937d391ef701 to your computer and use it in GitHub Desktop.
Jetpack Compose / MarkdownText composable that supports only *italic*, **bold**, `code` and [link](https://example.org)
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
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.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import org.commonmark.node.Code
import org.commonmark.node.Emphasis
import org.commonmark.node.Image
import org.commonmark.node.Link
import org.commonmark.node.Node
import org.commonmark.node.Paragraph
import org.commonmark.node.StrongEmphasis
import org.commonmark.parser.Parser
import org.commonmark.node.Text as NodeText
private const val ANNOTATED_STRING_URL_TAG = "url"
private val parser = Parser.builder().build()
/**
* Composable Material3.Text with markdown syntax. Supports only `*italic*, **bold**, `code` and [link](https://example.org)`
*
* Required dependencies:
* * `implementation("androidx.compose.ui:ui:VERSION")` // i used version 1.3.3
* * `implementation("androidx.compose.material3:material3:VERSION")` // i used version 1.0.1
* * `implementation("com.atlassian.commonmark:commonmark:VERSION")` // i used version 0.15.2
*
* Adaptation of `MarkdownText` and `appendMarkdownChildren` from [Markdown Composer](https://github.com/ErikHellman/MarkdownComposer) library without Coil usage or some advanced syntax support.
* If you need to build UI completely from markdown then pay attention to this library.
*/
@Composable
fun MarkdownText(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
textColor: Color = Color.Unspecified,
linkColor: Color = Color.Unspecified,
textAlign: TextAlign = TextAlign.Left
) {
val uriHandler = LocalUriHandler.current
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
val styledText = buildAnnotatedString {
val root = parser.parse(text)
withStyle(style.toSpanStyle()) {
appendMarkdownChildren(root, textColor, linkColor)
}
}
Text(
text = styledText,
modifier = modifier.pointerInput(Unit) {
detectTapGestures { offset ->
layoutResult.value?.let { layoutResult ->
val position = layoutResult.getOffsetForPosition(offset)
styledText.getStringAnnotations(position, position).firstOrNull()?.let { annotatedString ->
if (annotatedString.tag == ANNOTATED_STRING_URL_TAG)
uriHandler.openUri(annotatedString.item)
}
}
}
},
textAlign = textAlign,
style = style,
onTextLayout = { layoutResult.value = it }
)
}
private fun AnnotatedString.Builder.appendMarkdownChildren(
parent: Node, textColor: Color, linkColor: Color
) {
var child = parent.firstChild
if (child is Paragraph) { child = child.firstChild }
while (child != null) {
when (child) {
is NodeText -> append(child.literal)
is Emphasis -> {
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
appendMarkdownChildren(child, textColor, linkColor)
}
}
is StrongEmphasis -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendMarkdownChildren(child, textColor, linkColor)
}
}
is Code -> {
withStyle(TextStyle(fontFamily = FontFamily.Monospace).toSpanStyle()) {
append((child as Code).literal)
}
}
is Link, is Image -> {
pushStyle(SpanStyle(linkColor, textDecoration = TextDecoration.Underline))
pushStringAnnotation(ANNOTATED_STRING_URL_TAG, (child as Link).destination)
appendMarkdownChildren(child, textColor, linkColor)
pop()
pop()
}
}
child = child.next
}
}
// result - https://i.imgur.com/WhGBiNo.png
@Preview(
showBackground = true,
showSystemUi = true,
backgroundColor = 0xFFFFFFFF
)
@Composable
private fun MarkdownTextPreview() {
MarkdownText(
text = "Text with *italic*, **bold**, ***italic and bold***, [link](https://example.org)",
textColor = Color.Black,
linkColor = Color.Magenta
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment