feat(jbm): include sequential parsers for emoji, channel, mention
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
dbdd5556bf
commit
f47b21aeec
|
|
@ -33,9 +33,9 @@ import chat.revolt.api.routes.channel.fetchSingleMessage
|
||||||
import chat.revolt.api.schemas.User
|
import chat.revolt.api.schemas.User
|
||||||
import chat.revolt.api.settings.Experiments
|
import chat.revolt.api.settings.Experiments
|
||||||
import chat.revolt.components.generic.UserAvatar
|
import chat.revolt.components.generic.UserAvatar
|
||||||
import chat.revolt.components.markdown.jbm.JBM
|
import chat.revolt.markdown.jbm.JBM
|
||||||
import chat.revolt.components.markdown.jbm.JBMRenderer
|
import chat.revolt.markdown.jbm.JBMRenderer
|
||||||
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
|
import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState
|
||||||
|
|
||||||
@OptIn(JBM::class)
|
@OptIn(JBM::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,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.markdown.jbm.JBM
|
||||||
import chat.revolt.components.markdown.jbm.JBMRenderer
|
import chat.revolt.markdown.jbm.JBMRenderer
|
||||||
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
|
import chat.revolt.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
|
||||||
|
|
|
||||||
|
|
@ -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.ast.ASTNode
|
||||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
|
||||||
import org.intellij.markdown.parser.MarkdownParser
|
import org.intellij.markdown.parser.MarkdownParser
|
||||||
|
|
||||||
@RequiresOptIn(message = "This API is experimental and has many TODOs.")
|
@RequiresOptIn(message = "This API is experimental and has many TODOs.")
|
||||||
|
|
@ -12,6 +11,6 @@ annotation class JBM
|
||||||
@JBM
|
@JBM
|
||||||
object JBMApi {
|
object JBMApi {
|
||||||
fun parse(src: String): ASTNode {
|
fun parse(src: String): ASTNode {
|
||||||
return MarkdownParser(GFMFlavourDescriptor()).buildMarkdownTreeFromString(src)
|
return MarkdownParser(RSMFlavourDescriptor()).buildMarkdownTreeFromString(src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package chat.revolt.components.markdown.jbm
|
package chat.revolt.markdown.jbm
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
|
@ -67,6 +67,7 @@ import chat.revolt.R
|
||||||
import chat.revolt.activities.InviteActivity
|
import chat.revolt.activities.InviteActivity
|
||||||
import chat.revolt.api.REVOLT_FILES
|
import chat.revolt.api.REVOLT_FILES
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.internals.isUlid
|
||||||
import chat.revolt.api.routes.custom.fetchEmoji
|
import chat.revolt.api.routes.custom.fetchEmoji
|
||||||
import chat.revolt.api.schemas.isInviteUri
|
import chat.revolt.api.schemas.isInviteUri
|
||||||
import chat.revolt.api.settings.LoadedSettings
|
import chat.revolt.api.settings.LoadedSettings
|
||||||
|
|
@ -177,6 +178,86 @@ private fun annotateText(
|
||||||
append(source)
|
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 -> {
|
MarkdownTokenTypes.ATX_HEADER -> {
|
||||||
// Do not need to do anything
|
// Do not need to do anything
|
||||||
}
|
}
|
||||||
|
|
@ -443,6 +524,36 @@ private fun JBMText(node: ASTNode, modifier: Modifier) {
|
||||||
|
|
||||||
return@handler true
|
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
|
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
|
return@handler false
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,9 +19,9 @@ 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.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.components.markdown.jbm.JBM
|
import chat.revolt.markdown.jbm.JBM
|
||||||
import chat.revolt.components.markdown.jbm.JBMRenderer
|
import chat.revolt.markdown.jbm.JBMRenderer
|
||||||
import chat.revolt.components.markdown.jbm.LocalJBMarkdownTreeState
|
import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState
|
||||||
import chat.revolt.settings.dsl.SettingsPage
|
import chat.revolt.settings.dsl.SettingsPage
|
||||||
|
|
||||||
@OptIn(JBM::class)
|
@OptIn(JBM::class)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue