Unverified Commit 892a3f7a authored by Lucio Maciel's avatar Lucio Maciel Committed by GitHub

Merge pull request #1697 from RocketChat/fix-markdown-mentions-and-emphasis

[FIX] Fix markdown parsing for mentions, emphasis and strong emphasis
parents 8ddbd933 a8de7cfd
package chat.rocket.android.chatroom.ui
import org.commonmark.ext.gfm.strikethrough.Strikethrough
import org.commonmark.node.Node
import org.commonmark.node.Text
import org.commonmark.parser.delimiter.DelimiterProcessor
import org.commonmark.parser.delimiter.DelimiterRun
class StrikethroughDelimiterProcessor : DelimiterProcessor {
override fun getOpeningCharacter(): Char {
return '~'
}
override fun getClosingCharacter(): Char {
return '~'
}
override fun getMinLength(): Int {
return 1
}
override fun getDelimiterUse(opener: DelimiterRun, closer: DelimiterRun): Int {
return if (opener.length() >= 2 && closer.length() >= 2) {
// Use exactly two delimiters even if we have more, and don't care about internal openers/closers.
2
} else {
1
}
}
override fun process(opener: Text, closer: Text, delimiterCount: Int) {
// Wrap nodes between delimiters in strikethrough.
val strikethrough = Strikethrough()
var tmp: Node? = opener.next
while (tmp != null && tmp !== closer) {
val next = tmp.next
strikethrough.appendChild(tmp)
tmp = next
}
opener.insertAfter(strikethrough)
}
}
...@@ -5,6 +5,7 @@ import android.content.Context ...@@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.text.style.ImageSpan import android.text.style.ImageSpan
...@@ -13,6 +14,7 @@ import android.util.Patterns ...@@ -13,6 +14,7 @@ import android.util.Patterns
import android.view.View import android.view.View
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.StrikethroughDelimiterProcessor
import chat.rocket.android.emoji.EmojiParser import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiRepository import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.EmojiTypefaceSpan import chat.rocket.android.emoji.EmojiTypefaceSpan
...@@ -21,16 +23,23 @@ import chat.rocket.android.server.domain.useRealName ...@@ -21,16 +23,23 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.openTabbedUrl import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.common.model.SimpleUser import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import org.commonmark.Extension
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.node.AbstractVisitor import org.commonmark.node.AbstractVisitor
import org.commonmark.node.Document import org.commonmark.node.Document
import org.commonmark.node.Emphasis
import org.commonmark.node.ListItem import org.commonmark.node.ListItem
import org.commonmark.node.Node import org.commonmark.node.Node
import org.commonmark.node.OrderedList import org.commonmark.node.OrderedList
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text import org.commonmark.node.Text
import ru.noties.markwon.Markwon import org.commonmark.parser.Parser
import ru.noties.markwon.SpannableBuilder import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import ru.noties.markwon.tasklist.TaskListExtension
import java.util.*
import javax.inject.Inject import javax.inject.Inject
class MessageParser @Inject constructor( class MessageParser @Inject constructor(
...@@ -39,8 +48,6 @@ class MessageParser @Inject constructor( ...@@ -39,8 +48,6 @@ class MessageParser @Inject constructor(
private val settings: PublicSettings private val settings: PublicSettings
) { ) {
private val parser = Markwon.createParser()
/** /**
* Render markdown and other rules on message to rich text with spans. * Render markdown and other rules on message to rich text with spans.
* *
...@@ -52,6 +59,16 @@ class MessageParser @Inject constructor( ...@@ -52,6 +59,16 @@ class MessageParser @Inject constructor(
fun render(message: Message, selfUsername: String? = null): CharSequence { fun render(message: Message, selfUsername: String? = null): CharSequence {
var text: String = message.message var text: String = message.message
val mentions = mutableListOf<String>() val mentions = mutableListOf<String>()
val parser = Parser.Builder()
.extensions(Arrays.asList<Extension>(
StrikethroughExtension.create(),
TablesExtension.create(),
TaskListExtension.create()
))
.customDelimiterProcessor(StrikethroughDelimiterProcessor())
.build()
message.mentions?.forEach { message.mentions?.forEach {
val mention = getMention(it) val mention = getMention(it)
mentions.add(mention) mentions.add(mention)
...@@ -59,12 +76,17 @@ class MessageParser @Inject constructor( ...@@ -59,12 +76,17 @@ class MessageParser @Inject constructor(
text = text.replace("@${it.username}", mention) text = text.replace("@${it.username}", mention)
} }
} }
val builder = SpannableBuilder() val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text) val content = EmojiRepository.shortnameToUnicode(text)
val parentNode = parser.parse(toLenientMarkdown(content)) val parentNode = parser.parse(content)
parentNode.accept(EmphasisVisitor())
parentNode.accept(StrongEmphasisVisitor())
parentNode.accept(MarkdownVisitor(configuration, builder)) parentNode.accept(MarkdownVisitor(configuration, builder))
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(context, configuration, builder)) parentNode.accept(EmojiVisitor(context, configuration, builder))
message.mentions?.let { message.mentions?.let {
parentNode.accept(MentionVisitor(context, builder, mentions, selfUsername)) parentNode.accept(MentionVisitor(context, builder, mentions, selfUsername))
} }
...@@ -72,13 +94,6 @@ class MessageParser @Inject constructor( ...@@ -72,13 +94,6 @@ class MessageParser @Inject constructor(
return builder.text() return builder.text()
} }
// Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
private fun toLenientMarkdown(text: String): String {
return text.trim().replace("\\*(.+)\\*".toRegex()) { "**${it.groupValues[1].trim()}**" }
.replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" }
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
}
private fun getMention(user: SimpleUser): String { private fun getMention(user: SimpleUser): String {
return if (settings.useRealName()) { return if (settings.useRealName()) {
user.name ?: "@${user.username}" user.name ?: "@${user.username}"
...@@ -87,6 +102,31 @@ class MessageParser @Inject constructor( ...@@ -87,6 +102,31 @@ class MessageParser @Inject constructor(
} }
} }
class EmphasisVisitor : AbstractVisitor() {
override fun visit(emphasis: Emphasis) {
if (emphasis.openingDelimiter == "*" && emphasis.firstChild != null) {
val child = emphasis.firstChild
val strongEmphasis = StrongEmphasis()
strongEmphasis.appendChild(child)
emphasis.insertBefore(strongEmphasis)
emphasis.unlink()
}
}
}
class StrongEmphasisVisitor : AbstractVisitor() {
override fun visit(strongEmphasis: StrongEmphasis) {
if (strongEmphasis.openingDelimiter == "__" && strongEmphasis.firstChild != null) {
val child = strongEmphasis.firstChild
val emphasis = Emphasis()
emphasis.appendChild(child)
strongEmphasis.insertBefore(emphasis)
strongEmphasis.unlink()
}
}
}
class MentionVisitor( class MentionVisitor(
context: Context, context: Context,
private val builder: SpannableBuilder, private val builder: SpannableBuilder,
...@@ -98,28 +138,28 @@ class MessageParser @Inject constructor( ...@@ -98,28 +138,28 @@ class MessageParser @Inject constructor(
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme) private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.colorWhite, context.theme) private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.colorWhite, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme) private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat() private val padding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat() private val radius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
override fun visit(t: Text) { override fun visit(document: Document) {
val text = t.literal val text = builder.text()
val mentionsList = mentions.toMutableList().also { val mentionsList = mentions.toMutableList().also {
it.add("@all") it.add("@all")
it.add("@here") it.add("@here")
}.toList() }.distinct()
mentionsList.forEach { mentionsList.forEach {
val mentionMe = it == currentUser || it == "@all" || it == "@here" val mentionMe = it == currentUser || it == "@all" || it == "@here"
var offset = text.indexOf(it, 0, true) var offset = text.indexOf(string = it, startIndex = 0, ignoreCase = false)
while (offset > -1) { while (offset > -1) {
val textColor = if (mentionMe) myselfTextColor else othersTextColor val textColor = if (mentionMe) myselfTextColor else othersTextColor
val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding, val usernameSpan = MentionSpan(backgroundColor, textColor, radius, padding,
mentionMe) mentionMe)
// Add 1 to end offset to include the @. // Add 1 to end offset to include the @.
val end = offset + it.length + 1 val end = offset + it.length
builder.setSpan(usernameSpan, offset, end, 0) builder.setSpan(usernameSpan, offset, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
offset = text.indexOf("@$it", end, true) offset = text.indexOf(string = "@$it", startIndex = end, ignoreCase = false)
} }
} }
} }
...@@ -179,7 +219,7 @@ class MessageParser @Inject constructor( ...@@ -179,7 +219,7 @@ class MessageParser @Inject constructor(
} }
private fun newLine() { private fun newLine() {
if (builder.length() > 0 && '\n' != builder.lastChar()) { if (builder.isNotEmpty() && '\n' != builder.lastChar()) {
builder.append('\n') builder.append('\n')
} }
} }
...@@ -204,7 +244,6 @@ class MessageParser @Inject constructor( ...@@ -204,7 +244,6 @@ class MessageParser @Inject constructor(
consumed.add(link) consumed.add(link)
} }
} }
visitChildren(text)
} }
} }
......
...@@ -48,7 +48,7 @@ ext { ...@@ -48,7 +48,7 @@ ext {
kotshi : '1.0.4', kotshi : '1.0.4',
frescoImageViewer : '0.5.1', frescoImageViewer : '0.5.1',
markwon : '1.1.1', markwon : '2.0.0',
aVLoadingIndicatorView: '2.1.3', aVLoadingIndicatorView: '2.1.3',
glide : '4.8.0', glide : '4.8.0',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment