Last active
February 19, 2023 19:35
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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