feat(jbm): include sequential parsers for emoji, channel, mention

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-12-12 00:53:25 +01:00
parent dbdd5556bf
commit f47b21aeec
10 changed files with 311 additions and 54 deletions

View File

@ -33,9 +33,9 @@ import chat.revolt.api.routes.channel.fetchSingleMessage
import chat.revolt.api.schemas.User
import chat.revolt.api.settings.Experiments
import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.markdown.jbm.JBM
import chat.revolt.components.markdown.jbm.JBMRenderer
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
import chat.revolt.markdown.jbm.JBM
import chat.revolt.markdown.jbm.JBMRenderer
import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState
@OptIn(JBM::class)
@Composable

View File

@ -78,9 +78,9 @@ import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
import chat.revolt.components.markdown.LocalMarkdownTreeConfig
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.markdown.jbm.JBM
import chat.revolt.markdown.jbm.JBMRenderer
import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState
import chat.revolt.internals.text.Gigamoji
import kotlinx.coroutines.launch
import chat.revolt.api.schemas.Message as MessageSchema

View File

@ -1,7 +1,6 @@
package chat.revolt.components.markdown.jbm
package chat.revolt.markdown.jbm
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.parser.MarkdownParser
@RequiresOptIn(message = "This API is experimental and has many TODOs.")
@ -12,6 +11,6 @@ annotation class JBM
@JBM
object JBMApi {
fun parse(src: String): ASTNode {
return MarkdownParser(GFMFlavourDescriptor()).buildMarkdownTreeFromString(src)
return MarkdownParser(RSMFlavourDescriptor()).buildMarkdownTreeFromString(src)
}
}

View File

@ -1,4 +1,4 @@
package chat.revolt.components.markdown.jbm
package chat.revolt.markdown.jbm
import android.content.Intent
import android.content.res.Configuration
@ -67,6 +67,7 @@ import chat.revolt.R
import chat.revolt.activities.InviteActivity
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.isUlid
import chat.revolt.api.routes.custom.fetchEmoji
import chat.revolt.api.schemas.isInviteUri
import chat.revolt.api.settings.LoadedSettings
@ -177,6 +178,86 @@ private fun annotateText(
append(source)
}
RSMElementTypes.USER_MENTION -> {
val contents = node.getTextInNode(sourceText).toString()
val userId = contents.removeSurrounding("<@", ">")
if (userId == contents || !userId.isUlid()) {
// Invalid user mention. Append as if it were regular text.
for (child in node.children) {
append(annotateText(state, child))
}
} else {
// Now we're getting somewhere
pushStringAnnotation(
tag = JBMAnnotations.UserMention.tag,
annotation = userId
)
pushStyle(
SpanStyle(
color = state.colors.clickable,
background = state.colors.clickableBackground
)
)
val member = state.currentServer?.let { serverId ->
RevoltAPI.members.getMember(serverId, userId)
}
val mentionDisplay = member?.nickname?.let { nick -> "@$nick" }
?: RevoltAPI.userCache[userId]?.username?.let { username -> "@$username" }
?: "<@$userId>"
append(mentionDisplay)
pop()
pop()
}
}
RSMElementTypes.CHANNEL_MENTION -> {
val contents = node.getTextInNode(sourceText).toString()
val channelId = contents.removeSurrounding("<#", ">")
if (channelId == contents || !channelId.isUlid()) {
// Invalid channel mention. Append as if it were regular text.
for (child in node.children) {
append(annotateText(state, child))
}
} else {
// Now we're getting somewhere
pushStringAnnotation(
tag = JBMAnnotations.ChannelMention.tag,
annotation = channelId
)
pushStyle(
SpanStyle(
color = state.colors.clickable,
background = state.colors.clickableBackground
)
)
val channel = RevoltAPI.channelCache[channelId]
val mentionDisplay = channel?.name?.let { name -> "#$name" }
?: "<#$channelId>"
append(mentionDisplay)
pop()
pop()
}
}
RSMElementTypes.CUSTOM_EMOTE -> {
val contents = node.getTextInNode(sourceText).toString()
val emoteId = contents.removeSurrounding(":", ":")
if (emoteId == contents || !emoteId.isUlid()) {
// Invalid custom emote. Append as if it were regular text.
for (child in node.children) {
append(annotateText(state, child))
}
} else {
// Now we're getting somewhere
pushStringAnnotation(
tag = JBMAnnotations.CustomEmote.tag,
annotation = emoteId
)
appendInlineContent(JBMAnnotations.CustomEmote.tag, emoteId)
pop()
}
}
MarkdownTokenTypes.ATX_HEADER -> {
// Do not need to do anything
}
@ -443,6 +524,36 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
return@handler true
}
JBMAnnotations.UserMention.tag -> {
scope.launch {
ActionChannel.send(
Action.OpenUserSheet(
item,
mdState.currentServer
)
)
}
return@handler true
}
JBMAnnotations.ChannelMention.tag -> {
scope.launch {
ActionChannel.send(
Action.SwitchChannel(item)
)
}
return@handler true
}
JBMAnnotations.CustomEmote.tag -> {
scope.launch {
ActionChannel.send(
Action.EmoteInfo(item)
)
}
return@handler true
}
}
}
}
@ -464,47 +575,6 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
return@handler true
}
annotatedText.getStringAnnotations(
tag = Annotations.UserMention.tag,
start = offset,
end = offset
).firstOrNull()?.let { annotation ->
scope.launch {
ActionChannel.send(
Action.OpenUserSheet(
annotation.item,
mdState.currentServer
)
)
}
return@handler true
}
annotatedText.getStringAnnotations(
tag = Annotations.ChannelMention.tag,
start = offset,
end = offset
).firstOrNull()?.let { annotation ->
scope.launch {
ActionChannel.send(Action.SwitchChannel(annotation.item))
}
return@handler true
}
annotatedText.getStringAnnotations(
tag = Annotations.CustomEmote.tag,
start = offset,
end = offset
).firstOrNull()?.let { annotation ->
scope.launch {
ActionChannel.send(Action.EmoteInfo(annotation.item))
}
return@handler true
}
}
return@handler false

View File

@ -0,0 +1,18 @@
package chat.revolt.markdown.jbm
import org.intellij.markdown.IElementType
import org.intellij.markdown.MarkdownElementType
object RSMElementTypes {
@JvmField
val USER_MENTION: IElementType = MarkdownElementType("USER_MENTION")
@JvmField
val CHANNEL_MENTION: IElementType = MarkdownElementType("CHANNEL_MENTION")
@JvmField
val CUSTOM_EMOTE: IElementType = MarkdownElementType("EMOJI")
@JvmField
val TIMESTAMP: IElementType = MarkdownElementType("TIMESTAMP")
}

View File

@ -0,0 +1,38 @@
package chat.revolt.markdown.jbm
import chat.revolt.markdown.jbm.sequentialparsers.ChannelMentionParser
import chat.revolt.markdown.jbm.sequentialparsers.CustomEmoteParser
import chat.revolt.markdown.jbm.sequentialparsers.UserMentionParser
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.flavours.gfm.GFMTokenTypes
import org.intellij.markdown.flavours.gfm.StrikeThroughDelimiterParser
import org.intellij.markdown.parser.sequentialparsers.EmphasisLikeParser
import org.intellij.markdown.parser.sequentialparsers.SequentialParser
import org.intellij.markdown.parser.sequentialparsers.SequentialParserManager
import org.intellij.markdown.parser.sequentialparsers.impl.AutolinkParser
import org.intellij.markdown.parser.sequentialparsers.impl.BacktickParser
import org.intellij.markdown.parser.sequentialparsers.impl.EmphStrongDelimiterParser
import org.intellij.markdown.parser.sequentialparsers.impl.ImageParser
import org.intellij.markdown.parser.sequentialparsers.impl.InlineLinkParser
import org.intellij.markdown.parser.sequentialparsers.impl.MathParser
import org.intellij.markdown.parser.sequentialparsers.impl.ReferenceLinkParser
class RSMFlavourDescriptor : GFMFlavourDescriptor() {
override val sequentialParserManager = object : SequentialParserManager() {
override fun getParserSequence(): List<SequentialParser> {
return listOf(
UserMentionParser(),
ChannelMentionParser(),
CustomEmoteParser(),
AutolinkParser(listOf(MarkdownTokenTypes.AUTOLINK, GFMTokenTypes.GFM_AUTOLINK)),
BacktickParser(),
MathParser(),
ImageParser(),
InlineLinkParser(),
ReferenceLinkParser(),
EmphasisLikeParser(EmphStrongDelimiterParser(), StrikeThroughDelimiterParser())
)
}
}
}

View File

@ -0,0 +1,40 @@
package chat.revolt.markdown.jbm.sequentialparsers
import chat.revolt.markdown.jbm.RSMElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder
import org.intellij.markdown.parser.sequentialparsers.SequentialParser
import org.intellij.markdown.parser.sequentialparsers.TokensCache
class ChannelMentionParser : SequentialParser {
override fun parse(
tokens: TokensCache,
rangesToGlue: List<IntRange>
): SequentialParser.ParsingResult {
val result = SequentialParser.ParsingResultBuilder()
val delegateIndices = RangesListBuilder()
var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue)
while (iterator.type != null) {
if (iterator.type == MarkdownTokenTypes.LT && iterator.charLookup(1) == '#') {
val start = iterator.index
while (iterator.type != MarkdownTokenTypes.GT && iterator.type != null) {
iterator = iterator.advance()
}
if (iterator.type == MarkdownTokenTypes.GT) {
result.withNode(
SequentialParser.Node(
start..iterator.index + 1,
RSMElementTypes.USER_MENTION
)
)
}
} else {
delegateIndices.put(iterator.index)
}
iterator = iterator.advance()
}
return result.withFurtherProcessing(delegateIndices.get())
}
}

View File

@ -0,0 +1,52 @@
package chat.revolt.markdown.jbm.sequentialparsers
import chat.revolt.markdown.jbm.RSMElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder
import org.intellij.markdown.parser.sequentialparsers.SequentialParser
import org.intellij.markdown.parser.sequentialparsers.TokensCache
class CustomEmoteParser : SequentialParser {
override fun parse(
tokens: TokensCache,
rangesToGlue: List<IntRange>
): SequentialParser.ParsingResult {
val result = SequentialParser.ParsingResultBuilder()
val delegateIndices = RangesListBuilder()
var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue)
while (iterator.type != null) {
if (iterator.type == MarkdownTokenTypes.COLON) {
val endIterator = findNextColon(iterator.advance())
if (endIterator != null) {
result.withNode(
SequentialParser.Node(
iterator.index..endIterator.index + 1,
RSMElementTypes.CUSTOM_EMOTE
)
)
iterator = endIterator.advance()
continue
}
}
delegateIndices.put(iterator.index)
iterator = iterator.advance()
}
return result.withFurtherProcessing(delegateIndices.get())
}
private fun findNextColon(it: TokensCache.Iterator): TokensCache.Iterator? {
var iterator = it
while (iterator.type != null) {
if (iterator.type == MarkdownTokenTypes.COLON) {
return iterator
}
iterator = iterator.advance()
}
return null
}
}

View File

@ -0,0 +1,40 @@
package chat.revolt.markdown.jbm.sequentialparsers
import chat.revolt.markdown.jbm.RSMElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder
import org.intellij.markdown.parser.sequentialparsers.SequentialParser
import org.intellij.markdown.parser.sequentialparsers.TokensCache
class UserMentionParser : SequentialParser {
override fun parse(
tokens: TokensCache,
rangesToGlue: List<IntRange>
): SequentialParser.ParsingResult {
val result = SequentialParser.ParsingResultBuilder()
val delegateIndices = RangesListBuilder()
var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue)
while (iterator.type != null) {
if (iterator.type == MarkdownTokenTypes.LT && iterator.charLookup(1) == '@') {
val start = iterator.index
while (iterator.type != MarkdownTokenTypes.GT && iterator.type != null) {
iterator = iterator.advance()
}
if (iterator.type == MarkdownTokenTypes.GT) {
result.withNode(
SequentialParser.Node(
start..iterator.index + 1,
RSMElementTypes.USER_MENTION
)
)
}
} else {
delegateIndices.put(iterator.index)
}
iterator = iterator.advance()
}
return result.withFurtherProcessing(delegateIndices.get())
}
}

View File

@ -19,9 +19,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import chat.revolt.components.markdown.jbm.JBM
import chat.revolt.components.markdown.jbm.JBMRenderer
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
import chat.revolt.markdown.jbm.JBM
import chat.revolt.markdown.jbm.JBMRenderer
import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState
import chat.revolt.settings.dsl.SettingsPage
@OptIn(JBM::class)