feat(jbm): continue development
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
5497933589
commit
7cfbc3fdac
|
|
@ -269,6 +269,7 @@ dependencies {
|
||||||
|
|
||||||
// Markup
|
// Markup
|
||||||
implementation "org.jetbrains:markdown:0.7.3"
|
implementation "org.jetbrains:markdown:0.7.3"
|
||||||
|
implementation "dev.snipme:highlights:0.9.1"
|
||||||
|
|
||||||
// Livekit
|
// Livekit
|
||||||
// FIXME temporarily not included, re-add when realtime media is to be implemented
|
// FIXME temporarily not included, re-add when realtime media is to be implemented
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,17 @@ import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -78,6 +82,9 @@ import chat.revolt.components.generic.UserAvatar
|
||||||
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
|
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
|
||||||
import chat.revolt.components.markdown.LocalMarkdownTreeConfig
|
import chat.revolt.components.markdown.LocalMarkdownTreeConfig
|
||||||
import chat.revolt.components.markdown.RichMarkdown
|
import chat.revolt.components.markdown.RichMarkdown
|
||||||
|
import chat.revolt.components.markdown.jbm.JBM
|
||||||
|
import chat.revolt.components.markdown.jbm.JBMRenderer
|
||||||
|
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
|
||||||
import chat.revolt.internals.text.Gigamoji
|
import chat.revolt.internals.text.Gigamoji
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import chat.revolt.api.schemas.Message as MessageSchema
|
import chat.revolt.api.schemas.Message as MessageSchema
|
||||||
|
|
@ -167,7 +174,7 @@ fun formatLongAsTime(time: Long): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class, JBM::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Message(
|
fun Message(
|
||||||
message: MessageSchema,
|
message: MessageSchema,
|
||||||
|
|
@ -192,6 +199,8 @@ fun Message(
|
||||||
|
|
||||||
val authorIsBlocked = remember(author) { author.relationship == "Blocked" }
|
val authorIsBlocked = remember(author) { author.relationship == "Blocked" }
|
||||||
|
|
||||||
|
var __TEMPORARY_useJbm by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(Modifier.animateContentSize()) {
|
Column(Modifier.animateContentSize()) {
|
||||||
if (message.tail == false) {
|
if (message.tail == false) {
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
@ -361,17 +370,40 @@ fun Message(
|
||||||
message.content?.let {
|
message.content?.let {
|
||||||
if (message.content.isBlank()) return@let // if only an attachment is sent
|
if (message.content.isBlank()) return@let // if only an attachment is sent
|
||||||
|
|
||||||
CompositionLocalProvider(
|
Switch(
|
||||||
LocalMarkdownTreeConfig provides LocalMarkdownTreeConfig.current.copy(
|
checked = __TEMPORARY_useJbm,
|
||||||
currentServer = RevoltAPI.channelCache[message.channel]?.server,
|
onCheckedChange = { __TEMPORARY_useJbm = it },
|
||||||
fontSizeMultiplier = Gigamoji.useGigamojiForMessage(message.content)
|
)
|
||||||
.let {
|
|
||||||
if (it) 2f else 1f
|
if (__TEMPORARY_useJbm == false) {
|
||||||
}
|
CompositionLocalProvider(
|
||||||
)
|
LocalMarkdownTreeConfig provides LocalMarkdownTreeConfig.current.copy(
|
||||||
) {
|
currentServer = RevoltAPI.channelCache[message.channel]?.server,
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
fontSizeMultiplier = Gigamoji.useGigamojiForMessage(
|
||||||
RichMarkdown(input = message.content)
|
message.content
|
||||||
|
)
|
||||||
|
.let {
|
||||||
|
if (it) 2f else 1f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
RichMarkdown(input = message.content)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
|
||||||
|
fontSizeMultiplier = Gigamoji.useGigamojiForMessage(
|
||||||
|
message.content
|
||||||
|
)
|
||||||
|
.let {
|
||||||
|
if (it) 2f else 1f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
JBMRenderer(message.content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
package chat.revolt.components.markdown.jbm
|
package chat.revolt.components.markdown.jbm
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
import androidx.compose.foundation.text.appendInlineContent
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
|
@ -20,12 +28,14 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.structuralEqualityPolicy
|
import androidx.compose.runtime.structuralEqualityPolicy
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.Placeholder
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
|
@ -39,8 +49,17 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.revolt.api.settings.GlobalState
|
||||||
import chat.revolt.components.markdown.Annotations
|
import chat.revolt.components.markdown.Annotations
|
||||||
import chat.revolt.components.utils.detectTapGesturesConditionalConsume
|
import chat.revolt.components.utils.detectTapGesturesConditionalConsume
|
||||||
|
import chat.revolt.ui.theme.FragmentMono
|
||||||
|
import chat.revolt.ui.theme.isThemeDark
|
||||||
|
import dev.snipme.highlights.Highlights
|
||||||
|
import dev.snipme.highlights.model.BoldHighlight
|
||||||
|
import dev.snipme.highlights.model.CodeHighlight
|
||||||
|
import dev.snipme.highlights.model.ColorHighlight
|
||||||
|
import dev.snipme.highlights.model.SyntaxLanguage
|
||||||
|
import dev.snipme.highlights.model.SyntaxThemes
|
||||||
import org.intellij.markdown.MarkdownElementTypes
|
import org.intellij.markdown.MarkdownElementTypes
|
||||||
import org.intellij.markdown.MarkdownTokenTypes
|
import org.intellij.markdown.MarkdownTokenTypes
|
||||||
import org.intellij.markdown.ast.ASTNode
|
import org.intellij.markdown.ast.ASTNode
|
||||||
|
|
@ -50,10 +69,11 @@ import org.intellij.markdown.flavours.gfm.GFMTokenTypes
|
||||||
|
|
||||||
data class JBMarkdownTreeState(
|
data class JBMarkdownTreeState(
|
||||||
val sourceText: String = "",
|
val sourceText: String = "",
|
||||||
val ignoreLineBreaks: Boolean = false,
|
|
||||||
val listDepth: Int = 0,
|
val listDepth: Int = 0,
|
||||||
val fontSizeMultiplier: Float = 1f,
|
val fontSizeMultiplier: Float = 1f,
|
||||||
val linksClickable: Boolean = true
|
val linksClickable: Boolean = true,
|
||||||
|
val currentServer: String? = null,
|
||||||
|
val embedded: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val LocalJBMarkdownTreeState =
|
val LocalJBMarkdownTreeState =
|
||||||
|
|
@ -66,15 +86,21 @@ fun JBMRenderer(content: String, modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
LaunchedEffect(content) {
|
LaunchedEffect(content) {
|
||||||
tree = JBMApi.parse(content)
|
tree = JBMApi.parse(content)
|
||||||
|
|
||||||
Log.d("JBMRenderer", "Parsed tree: ${tree.children.map { it.type.name }}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalJBMarkdownTreeState provides JBMarkdownTreeState(content)
|
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
|
||||||
|
sourceText = content
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
tree.children.map {
|
if (LocalJBMarkdownTreeState.current.embedded) {
|
||||||
JBMBlock(it, modifier)
|
tree.children.getOrNull(0)?.let {
|
||||||
|
JBMBlock(it, modifier)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tree.children.map {
|
||||||
|
JBMBlock(it, modifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -89,12 +115,34 @@ private fun annotateText(
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
when (node.type) {
|
when (node.type) {
|
||||||
MarkdownTokenTypes.TEXT -> {
|
MarkdownTokenTypes.TEXT -> {
|
||||||
append(node.getTextInNode(sourceText))
|
val source = if (state.embedded) {
|
||||||
|
node.getTextInNode(sourceText).toString().replace("\n", " ")
|
||||||
|
} else {
|
||||||
|
node.getTextInNode(sourceText)
|
||||||
|
}
|
||||||
|
append(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownTokenTypes.ATX_HEADER -> {
|
||||||
|
// Do not need to do anything
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.ATX_1,
|
||||||
|
MarkdownElementTypes.ATX_2,
|
||||||
|
MarkdownElementTypes.ATX_3,
|
||||||
|
MarkdownElementTypes.ATX_4,
|
||||||
|
MarkdownElementTypes.ATX_5,
|
||||||
|
MarkdownElementTypes.ATX_6 -> {
|
||||||
|
for (child in node.children) {
|
||||||
|
append(annotateText(state, child))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkdownElementTypes.EMPH -> {
|
MarkdownElementTypes.EMPH -> {
|
||||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
|
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
|
||||||
for (child in node.children) {
|
// Skip the first child and the last child
|
||||||
|
// because they are the asterisk characters
|
||||||
|
for (child in node.children.subList(1, node.children.size - 1)) {
|
||||||
append(annotateText(state, child))
|
append(annotateText(state, child))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +150,9 @@ private fun annotateText(
|
||||||
|
|
||||||
MarkdownElementTypes.STRONG -> {
|
MarkdownElementTypes.STRONG -> {
|
||||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
for (child in node.children) {
|
// Skip the first two children and the last two children
|
||||||
|
// because they are the asterisk characters
|
||||||
|
for (child in node.children.subList(2, node.children.size - 2)) {
|
||||||
append(annotateText(state, child))
|
append(annotateText(state, child))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,8 +168,24 @@ private fun annotateText(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.CODE_SPAN -> {
|
||||||
|
withStyle(SpanStyle(fontFamily = FragmentMono)) {
|
||||||
|
val startsWithTwoBackticks =
|
||||||
|
node.children.getOrNull(1)?.type == MarkdownTokenTypes.BACKTICK
|
||||||
|
val removeItemCount = if (startsWithTwoBackticks) 2 else 1
|
||||||
|
// Skip the first and last 1 or 2 children
|
||||||
|
// because they are the backtick characters
|
||||||
|
for (child in node.children.subList(
|
||||||
|
removeItemCount,
|
||||||
|
node.children.size - removeItemCount
|
||||||
|
)) {
|
||||||
|
append(annotateText(state, child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MarkdownTokenTypes.LIST_BULLET -> {
|
MarkdownTokenTypes.LIST_BULLET -> {
|
||||||
append(" ".repeat(state.listDepth) + " " + if (state.listDepth % 2 == 0) "•" else "◦" + " ")
|
append(" ".repeat(state.listDepth) + " • ")
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkdownTokenTypes.LIST_NUMBER -> {
|
MarkdownTokenTypes.LIST_NUMBER -> {
|
||||||
|
|
@ -145,7 +211,9 @@ private fun annotateText(
|
||||||
append(" ")
|
append(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkdownElementTypes.PARAGRAPH, MarkdownElementTypes.HTML_BLOCK -> {
|
MarkdownElementTypes.PARAGRAPH,
|
||||||
|
MarkdownElementTypes.HTML_BLOCK,
|
||||||
|
MarkdownTokenTypes.ATX_CONTENT -> {
|
||||||
for (child in node.children) {
|
for (child in node.children) {
|
||||||
append(annotateText(state, child))
|
append(annotateText(state, child))
|
||||||
}
|
}
|
||||||
|
|
@ -167,15 +235,11 @@ private fun annotateText(
|
||||||
MarkdownTokenTypes.EOL,
|
MarkdownTokenTypes.EOL,
|
||||||
MarkdownTokenTypes.WHITE_SPACE,
|
MarkdownTokenTypes.WHITE_SPACE,
|
||||||
MarkdownTokenTypes.COLON,
|
MarkdownTokenTypes.COLON,
|
||||||
|
MarkdownTokenTypes.EMPH,
|
||||||
GFMTokenTypes.TILDE -> {
|
GFMTokenTypes.TILDE -> {
|
||||||
append(node.getTextInNode(sourceText))
|
append(node.getTextInNode(sourceText))
|
||||||
}
|
}
|
||||||
|
|
||||||
// no-op types
|
|
||||||
// for example, the special characters that are used to denote the markup are here
|
|
||||||
MarkdownTokenTypes.EMPH -> {
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
append("[${node.type.name}]{\n")
|
append("[${node.type.name}]{\n")
|
||||||
append(node.getTextInNode(sourceText))
|
append(node.getTextInNode(sourceText))
|
||||||
|
|
@ -292,14 +356,132 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun annotateHighlights(
|
||||||
|
source: String,
|
||||||
|
highlights: List<CodeHighlight>
|
||||||
|
): AnnotatedString {
|
||||||
|
val highlightStyles = highlights.map {
|
||||||
|
when (it) {
|
||||||
|
is BoldHighlight -> AnnotatedString.Range(
|
||||||
|
SpanStyle(fontWeight = FontWeight.Bold),
|
||||||
|
it.location.start,
|
||||||
|
it.location.end
|
||||||
|
)
|
||||||
|
|
||||||
|
is ColorHighlight -> {
|
||||||
|
AnnotatedString.Range(
|
||||||
|
SpanStyle(color = Color(0xFF000000 or it.rgb.toLong())),
|
||||||
|
it.location.start,
|
||||||
|
it.location.end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.filterNotNull()
|
||||||
|
|
||||||
|
return AnnotatedString(source, spanStyles = highlightStyles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======== TODO ========
|
||||||
|
// - Add aliases for languages. For example, "js" should be an alias for "javascript" and "ts" should be an alias for "typescript", etc.
|
||||||
|
// - Better looking language name display. Ideally a dictionary of language names to display names with proper brand casing.
|
||||||
|
@Composable
|
||||||
|
private fun JBMCodeBlockContent(node: ASTNode, modifier: Modifier) {
|
||||||
|
val state = LocalJBMarkdownTreeState.current
|
||||||
|
|
||||||
|
/*val colours = MaterialTheme.colorScheme
|
||||||
|
val contentColour = LocalContentColor.current
|
||||||
|
val syntaxTheme = remember {
|
||||||
|
SyntaxTheme(
|
||||||
|
key = "chat.revolt.M3Dynamic",
|
||||||
|
code = contentColour.toArgb() and 0xFFFFFF,
|
||||||
|
comment = colours.outline.toArgb() and 0xFFFFFF,
|
||||||
|
multilineComment = colours.outline.toArgb() and 0xFFFFFF,
|
||||||
|
keyword = colours.primary.toArgb() and 0xFFFFFF,
|
||||||
|
string = colours.secondary.toArgb() and 0xFFFFFF,
|
||||||
|
literal = colours.tertiary.toArgb() and 0xFFFFFF,
|
||||||
|
mark = colours.error.toArgb() and 0xFFFFFF,
|
||||||
|
punctuation = colours.inversePrimary.toArgb() and 0xFFFFFF,
|
||||||
|
metadata = colours.inverseSurface.toArgb() and 0xFFFFFF,
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val uiMode = LocalConfiguration.current.uiMode
|
||||||
|
val systemIsDark =
|
||||||
|
(uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
val themeIsDark = remember(GlobalState.theme) { isThemeDark(GlobalState.theme, systemIsDark) }
|
||||||
|
|
||||||
|
val codeFenceLanguage = remember(node) {
|
||||||
|
node.children.firstOrNull { it.type == MarkdownTokenTypes.FENCE_LANG }
|
||||||
|
?.getTextInNode(state.sourceText)?.toString()
|
||||||
|
}
|
||||||
|
val codeFenceContent = remember(node) {
|
||||||
|
node.children
|
||||||
|
.filter { it.type == MarkdownTokenTypes.CODE_FENCE_CONTENT || it.type == MarkdownTokenTypes.EOL }
|
||||||
|
.joinToString("") {
|
||||||
|
it.getTextInNode(state.sourceText).toString()
|
||||||
|
}
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
val annotatedContent = remember(codeFenceLanguage, codeFenceContent) {
|
||||||
|
val canAnnotate = codeFenceLanguage != null
|
||||||
|
val language = codeFenceLanguage?.let { SyntaxLanguage.getByName(it) }
|
||||||
|
val shouldAnnotate = language != null
|
||||||
|
|
||||||
|
if (canAnnotate && shouldAnnotate) {
|
||||||
|
buildAnnotatedString {
|
||||||
|
val highlights = Highlights.Builder().apply {
|
||||||
|
code(codeFenceContent)
|
||||||
|
language(language ?: SyntaxLanguage.DEFAULT)
|
||||||
|
theme(SyntaxThemes.notepad(themeIsDark))
|
||||||
|
//theme(syntaxTheme)
|
||||||
|
}.build()
|
||||||
|
append(annotateHighlights(codeFenceContent, highlights.getHighlights()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buildAnnotatedString {
|
||||||
|
append(codeFenceContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
if (codeFenceLanguage != null) {
|
||||||
|
Text(
|
||||||
|
text = codeFenceLanguage,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.horizontalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = annotatedContent,
|
||||||
|
fontFamily = FragmentMono,
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JBMBlock(node: ASTNode, modifier: Modifier) {
|
private fun JBMBlock(node: ASTNode, modifier: Modifier) {
|
||||||
|
val state = LocalJBMarkdownTreeState.current
|
||||||
|
|
||||||
when (node.type) {
|
when (node.type) {
|
||||||
MarkdownElementTypes.PARAGRAPH,
|
MarkdownElementTypes.PARAGRAPH,
|
||||||
MarkdownElementTypes.HTML_BLOCK -> {
|
MarkdownElementTypes.HTML_BLOCK -> {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||||
fontSize = LocalTextStyle.current.fontSize * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
fontSize = LocalTextStyle.current.fontSize * state.fontSizeMultiplier
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
JBMText(node, modifier)
|
JBMText(node, modifier)
|
||||||
|
|
@ -315,42 +497,44 @@ private fun JBMBlock(node: ASTNode, modifier: Modifier) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = when (node.type) {
|
fontSize = if (state.embedded) LocalTextStyle.current.fontSize
|
||||||
MarkdownElementTypes.ATX_1 -> 32.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
else when (node.type) {
|
||||||
MarkdownElementTypes.ATX_2 -> 24.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
MarkdownElementTypes.ATX_1 -> 32.sp * state.fontSizeMultiplier
|
||||||
MarkdownElementTypes.ATX_3 -> 20.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
MarkdownElementTypes.ATX_2 -> 24.sp * state.fontSizeMultiplier
|
||||||
MarkdownElementTypes.ATX_4 -> 16.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
MarkdownElementTypes.ATX_3 -> 20.sp * state.fontSizeMultiplier
|
||||||
MarkdownElementTypes.ATX_5 -> 14.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
MarkdownElementTypes.ATX_4 -> 16.sp * state.fontSizeMultiplier
|
||||||
else -> 12.sp * LocalJBMarkdownTreeState.current.fontSizeMultiplier
|
MarkdownElementTypes.ATX_5 -> 14.sp * state.fontSizeMultiplier
|
||||||
},
|
else -> 12.sp * state.fontSizeMultiplier
|
||||||
color = when (node.type) {
|
|
||||||
MarkdownElementTypes.ATX_1 -> Color(0xFFFF0000)
|
|
||||||
MarkdownElementTypes.ATX_2 -> Color(0xFF00FF00)
|
|
||||||
MarkdownElementTypes.ATX_3 -> Color(0xFF0000FF)
|
|
||||||
MarkdownElementTypes.ATX_4 -> Color(0xFFFF00FF)
|
|
||||||
MarkdownElementTypes.ATX_5 -> Color(0xFF00FFFF)
|
|
||||||
else -> Color(0xFFFFFF00)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (node.startOffset != 0) {
|
if (node.startOffset != 0) {
|
||||||
Box(Modifier.padding(top = 8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
JBMText(node, modifier)
|
JBMText(node, modifier)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkdownElementTypes.ORDERED_LIST,
|
MarkdownElementTypes.ORDERED_LIST,
|
||||||
MarkdownElementTypes.UNORDERED_LIST -> {
|
MarkdownElementTypes.UNORDERED_LIST -> {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
|
LocalJBMarkdownTreeState provides state.copy(
|
||||||
listDepth = LocalJBMarkdownTreeState.current.listDepth + 1
|
listDepth = state.listDepth + 1
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
JBMText(node, modifier)
|
JBMText(node, modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkdownTokenTypes.EOL -> {
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownElementTypes.CODE_FENCE -> {
|
||||||
|
JBMCodeBlockContent(node, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Text(
|
Text(
|
||||||
text = buildAnnotatedString {
|
text = buildAnnotatedString {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,27 @@
|
||||||
package chat.revolt.screens.labs.ui.sandbox
|
package chat.revolt.screens.labs.ui.sandbox
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.components.markdown.jbm.JBM
|
import chat.revolt.components.markdown.jbm.JBM
|
||||||
import chat.revolt.components.markdown.jbm.JBMRenderer
|
import chat.revolt.components.markdown.jbm.JBMRenderer
|
||||||
|
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
|
||||||
import chat.revolt.settings.dsl.SettingsPage
|
import chat.revolt.settings.dsl.SettingsPage
|
||||||
|
|
||||||
@OptIn(JBM::class)
|
@OptIn(JBM::class)
|
||||||
|
|
@ -21,6 +29,7 @@ import chat.revolt.settings.dsl.SettingsPage
|
||||||
fun JBMSandbox(navController: NavController) {
|
fun JBMSandbox(navController: NavController) {
|
||||||
var mdSource by remember { mutableStateOf("") }
|
var mdSource by remember { mutableStateOf("") }
|
||||||
var submitMdSource by remember { mutableStateOf<String?>(null) }
|
var submitMdSource by remember { mutableStateOf<String?>(null) }
|
||||||
|
var isEmbedded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
SettingsPage(
|
SettingsPage(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
|
@ -32,6 +41,20 @@ fun JBMSandbox(navController: NavController) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
Subcategory(
|
||||||
|
title = { Text("Options", maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = isEmbedded,
|
||||||
|
onCheckedChange = { isEmbedded = it }
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Embedded", maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
|
}
|
||||||
|
}
|
||||||
Subcategory(
|
Subcategory(
|
||||||
title = { Text("Source", maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
title = { Text("Source", maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
) {
|
) {
|
||||||
|
|
@ -47,12 +70,43 @@ fun JBMSandbox(navController: NavController) {
|
||||||
}) {
|
}) {
|
||||||
Text("Submit")
|
Text("Submit")
|
||||||
}
|
}
|
||||||
|
TextButton(onClick = {
|
||||||
|
submitMdSource = """# Full range of MD now supported!
|
||||||
|
1. Text with **bold**, *italics*, and ***both***!
|
||||||
|
2. You ~~can't see me~~.
|
||||||
|
3. [I'm a link to another website.](<https://revolt.chat>)
|
||||||
|
4. I'm a spoiler with ||**bold text inside it**||
|
||||||
|
- I'm a sub-item on this list...
|
||||||
|
- Let's go even deeper...
|
||||||
|
|
||||||
|
`Inline code`
|
||||||
|
|
||||||
|
```js
|
||||||
|
let x = "I'm a multi-line code block!";
|
||||||
|
```
|
||||||
|
|
||||||
|
> > ${'$'}${'$'}E = mc^2${'$'}${'$'}
|
||||||
|
>
|
||||||
|
> — Albert Einstein
|
||||||
|
|
||||||
|
| Timestamp | Mention | Channel Link | Message Link |
|
||||||
|
|:-:|:-:|:-:|:-:|
|
||||||
|
| <t:1663846662:f> | <@01EX2NCWQ0CHS3QJF0FEQS1GR4> | <#01H73F4RAHTPBHKJ1XBQDXK3NQ> | https://revolt.chat/server/01F7ZSBSFHQ8TA81725KQCSDDP/channel/01F92C5ZXBQWQ8KY7J8KY917NM/01J25XZM9JXVVJDDKFPB7Q48HZ |"""
|
||||||
|
}) {
|
||||||
|
Text("Submit test document")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Subcategory(
|
Subcategory(
|
||||||
title = { Text("Output", maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
title = { Text("Output", maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
) {
|
) {
|
||||||
submitMdSource?.let { JBMRenderer(it, Modifier) }
|
CompositionLocalProvider(
|
||||||
?: Text("Submit some Markdown and see the output.")
|
LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
|
||||||
|
embedded = isEmbedded
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
submitMdSource?.let { JBMRenderer(it, Modifier) }
|
||||||
|
?: Text("Submit some Markdown and see the output.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,4 +171,15 @@ object ClearRippleTheme : RippleTheme {
|
||||||
hoveredAlpha = 0.0f,
|
hoveredAlpha = 0.0f,
|
||||||
pressedAlpha = 0.0f,
|
pressedAlpha = 0.0f,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isThemeDark(theme: Theme, systemIsDark: Boolean): Boolean {
|
||||||
|
return when (theme) {
|
||||||
|
Theme.Revolt, Theme.Amoled -> true
|
||||||
|
Theme.Light -> false
|
||||||
|
Theme.M3Dynamic, Theme.None -> systemIsDark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun isThemeDark(theme: Theme) = isThemeDark(theme, isSystemInDarkTheme())
|
||||||
Loading…
Reference in New Issue