Added Markdown Text (#2860)

This commit is contained in:
Benjamin Faershtein
2025-08-26 17:27:21 -07:00
committed by GitHub
parent 41cfc316f2
commit 799933fcfb
3 changed files with 109 additions and 97 deletions

View File

@@ -1,95 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.components
import android.text.Spannable
import android.text.Spannable.Factory
import android.text.style.URLSpan
import android.text.util.Linkify
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.text.util.LinkifyCompat
import com.geeksville.mesh.ui.common.theme.HyperlinkBlue
private val DefaultTextLinkStyles = TextLinkStyles(
style = SpanStyle(
color = HyperlinkBlue,
textDecoration = TextDecoration.Underline,
)
)
@Composable
fun AutoLinkText(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default,
linkStyles: TextLinkStyles = DefaultTextLinkStyles,
color: Color = Color.Unspecified,
) {
val spannable = remember(text) {
linkify(text)
}
Text(
text = spannable.toAnnotatedString(linkStyles),
modifier = modifier,
style = style.copy(color = color),
)
}
private fun linkify(text: String) = Factory.getInstance().newSpannable(text).also {
LinkifyCompat.addLinks(it, Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)
}
private fun Spannable.toAnnotatedString(
linkStyles: TextLinkStyles,
): AnnotatedString = buildAnnotatedString {
val spannable = this@toAnnotatedString
var lastEnd = 0
spannable.getSpans(0, spannable.length, Any::class.java).forEach { span ->
val start = spannable.getSpanStart(span)
val end = spannable.getSpanEnd(span)
append(spannable.subSequence(lastEnd, start))
when (span) {
is URLSpan -> withLink(LinkAnnotation.Url(url = span.url, styles = linkStyles)) {
append(spannable.subSequence(start, end))
}
else -> append(spannable.subSequence(start, end))
}
lastEnd = end
}
append(spannable.subSequence(lastEnd, spannable.length))
}
@Preview(showBackground = true)
@Composable
private fun AutoLinkTextPreview() {
AutoLinkText("A text containing a link https://example.com")
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import com.geeksville.mesh.ui.common.theme.HyperlinkBlue
import com.mikepenz.markdown.compose.components.markdownComponents
import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.model.DefaultMarkdownColors
import com.mikepenz.markdown.model.DefaultMarkdownTypography
@Composable
fun MDText(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = MaterialTheme.typography.bodyMedium,
color: Color = Color.Unspecified,
) {
val colors =
DefaultMarkdownColors(
text = color,
codeText = MaterialTheme.colorScheme.onSurface,
inlineCodeText = MaterialTheme.colorScheme.onSurface,
linkText = HyperlinkBlue,
codeBackground = MaterialTheme.colorScheme.surfaceContainerHigh,
inlineCodeBackground = MaterialTheme.colorScheme.surfaceContainerHigh,
dividerColor = MaterialTheme.colorScheme.onSurface,
tableText = MaterialTheme.colorScheme.onSurface,
tableBackground = MaterialTheme.colorScheme.surfaceContainer,
)
val typography =
DefaultMarkdownTypography(
// Restrict max size of the text
h1 = MaterialTheme.typography.headlineMedium.copy(color = color),
h2 = MaterialTheme.typography.headlineMedium.copy(color = color),
h3 = MaterialTheme.typography.headlineSmall.copy(color = color),
h4 = MaterialTheme.typography.titleLarge.copy(color = color),
h5 = MaterialTheme.typography.titleMedium.copy(color = color),
h6 = MaterialTheme.typography.titleSmall.copy(color = color),
text = style,
code =
MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface,
),
inlineCode =
MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface,
background = MaterialTheme.colorScheme.surfaceContainerHigh,
),
quote = MaterialTheme.typography.bodyLarge.copy(color = color),
paragraph = MaterialTheme.typography.bodyMedium.copy(color = color),
ordered = MaterialTheme.typography.bodyMedium.copy(color = color),
bullet = MaterialTheme.typography.bodyMedium.copy(color = color),
list = MaterialTheme.typography.bodyMedium.copy(color = color),
link = TextStyle(color = HyperlinkBlue, textDecoration = TextDecoration.Underline),
textLink =
TextLinkStyles(style = SpanStyle(color = HyperlinkBlue, textDecoration = TextDecoration.Underline)),
table = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
)
// Custom Markdown components to disable image rendering
val customComponents = markdownComponents(image = { /* Empty composable to disable image rendering */ })
Markdown(
content = text,
modifier = modifier,
colors = colors,
typography = typography,
components = customComponents, // Use custom components
)
}
@Preview(showBackground = true)
@Composable
private fun AutoLinkTextPreview() {
MDText(
"A text containing a link https://example.com **bold** _Italics_" +
"\n # hello \n ## hello \n ### hello \n #### hello \n ##### hello \n ###### hello \n ```code```",
)
}

View File

@@ -54,7 +54,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.Reaction
import com.geeksville.mesh.model.Message
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.ui.common.components.AutoLinkText
import com.geeksville.mesh.ui.common.components.MDText
import com.geeksville.mesh.ui.common.components.Rssi
import com.geeksville.mesh.ui.common.components.Snr
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
@@ -164,7 +164,7 @@ internal fun MessageItem(
}
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
AutoLinkText(
MDText(
modifier = Modifier.fillMaxWidth(),
text = message.text,
style = MaterialTheme.typography.bodyMedium,