recipe-kt/app/src/main/java/xyz/pixelatedw/recipe/utils/MarkdownParser.kt

192 lines
4.4 KiB
Kotlin

package xyz.pixelatedw.recipe.utils
import android.util.Log
import androidx.compose.material3.Typography
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
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.TextIndent
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.sp
import org.commonmark.node.BulletList
import org.commonmark.node.Document
import org.commonmark.node.Emphasis
import org.commonmark.node.HardLineBreak
import org.commonmark.node.Heading
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.SoftLineBreak
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text
import org.commonmark.node.ThematicBreak
import org.commonmark.parser.Parser
fun parseMarkdown(
markdown: String,
typography: Typography,
): AnnotatedString {
val parser = Parser.builder().build()
val document = parser.parse(markdown)
val annotatedString = buildAnnotatedString {
visitMarkdownNode(document, typography)
}
return annotatedString.trim() as AnnotatedString
}
private fun AnnotatedString.Builder.visitMarkdownNode(
node: Node,
typography: Typography,
) {
val headingColor = androidx.compose.ui.graphics.Color.White
when (node) {
is Heading -> {
val style = when (node.level) {
in 1..3 -> typography.titleLarge
4 -> typography.titleMedium
5 -> typography.bodySmall
else -> typography.bodySmall
}
withStyle(
style.toParagraphStyle().merge(ParagraphStyle(textAlign = TextAlign.Center))
) {
withStyle(style.toSpanStyle().copy(color = headingColor)) {
visitChildren(node, typography)
appendLine()
}
}
}
is Paragraph -> {
if (node.parents.any { it is BulletList || it is OrderedList }) {
visitChildren(node, typography)
} else {
withStyle(typography.bodyLarge.toParagraphStyle()) {
visitChildren(node, typography)
appendLine()
}
}
}
is Emphasis -> {
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
visitChildren(node, typography)
}
}
is StrongEmphasis -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
visitChildren(node, typography)
}
}
is Text -> {
append(node.literal)
visitChildren(node, typography)
}
is SoftLineBreak -> {
append(" ")
}
is HardLineBreak -> {
appendLine()
}
is ThematicBreak -> {
withStyle(ParagraphStyle(textAlign = TextAlign.Center)) {
withStyle(SpanStyle(letterSpacing = 0.sp)) {
appendLine("".repeat(10))
}
}
}
is OrderedList -> {
withStyle(
ParagraphStyle(
textIndent = TextIndent(
firstLine = 10.sp,
restLine = 20.sp
)
)
) {
visitChildren(node, typography)
}
}
is BulletList -> {
withStyle(
ParagraphStyle(
textIndent = TextIndent(
firstLine = 10.sp,
restLine = 20.sp
)
)
) {
visitChildren(node, typography)
}
}
is ListItem -> {
withStyle(ParagraphStyle(lineHeight = 18.sp)) {
if (node.parents.any { it is BulletList }) {
append("")
} else if (node.parents.any { it is OrderedList }) {
val startNumber =
(node.parents.first { it is OrderedList } as OrderedList).markerStartNumber
val index =
startNumber + node.previousSiblings.filterIsInstance<ListItem>().size
append("$index. ")
}
visitChildren(node, typography)
appendLine()
}
}
is Document -> {
visitChildren(node, typography)
}
else -> {
Log.e("MarkdownText", "Traversing unhandled node: $node")
visitChildren(node, typography)
}
}
}
private fun AnnotatedString.Builder.visitChildren(
node: Node,
typography: Typography,
) {
var child = node.firstChild
while (child != null) {
visitMarkdownNode(child, typography)
child = child.next
}
}
private val Node.parents: List<Node>
get() {
val list = mutableListOf<Node>()
var current = this
while (true) {
current = current.parent ?: return list
list += current
}
}
private val Node.previousSiblings: List<Node>
get() {
val list = mutableListOf<Node>()
var current = this
while (true) {
current = current.previous ?: return list
list += current
}
}