[Rich text editor] Add inline code to rich text editor (#8011)

Also:
- Fixes https://github.com/vector-im/element-android/issues/7975
- See https://github.com/noties/Markwon/issues/423
This commit is contained in:
jonnyandrew 2023-01-30 17:35:29 +00:00 committed by GitHub
parent 156f4f71f9
commit 00f9c362da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 339 additions and 110 deletions

1
changelog.d/7975.bugfix Normal file
View File

@ -0,0 +1 @@
Fix extra new lines added to inline code

1
changelog.d/8011.feature Normal file
View File

@ -0,0 +1 @@
[Rich text editor] Add inline code to rich text editor

View File

@ -3502,6 +3502,7 @@
<string name="rich_text_editor_link">Set link</string>
<string name="rich_text_editor_numbered_list">Toggle numbered list</string>
<string name="rich_text_editor_bullet_list">Toggle bullet list</string>
<string name="rich_text_editor_inline_code">Apply inline code format</string>
<string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string>
<string name="set_link_text">Text</string>

View File

@ -19,9 +19,10 @@ package im.vector.app.core.utils
import android.graphics.Canvas
import android.graphics.Paint
import android.text.Layout
import android.text.Spannable
import android.text.Spanned
import androidx.core.text.getSpans
import im.vector.app.features.html.HtmlCodeSpan
import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.slot
@ -31,9 +32,9 @@ import io.noties.markwon.core.spans.OrderedListItemSpan
import io.noties.markwon.core.spans.StrongEmphasisSpan
import me.gujun.android.span.style.CustomTypefaceSpan
fun Spannable.toTestSpan(): String {
fun Spanned.toTestSpan(): String {
var output = toString()
readSpansWithContent().forEach {
readSpansWithContent().reversed().forEach {
val tags = it.span.readTags()
val remappedContent = it.span.remapContent(source = this, originalContent = it.content)
output = output.replace(it.content, "${tags.open}$remappedContent${tags.close}")
@ -41,7 +42,7 @@ fun Spannable.toTestSpan(): String {
return output
}
private fun Spannable.readSpansWithContent() = getSpans<Any>().map { span ->
private fun Spanned.readSpansWithContent() = getSpans<Any>().map { span ->
val start = getSpanStart(span)
val end = getSpanEnd(span)
SpanWithContent(
@ -51,12 +52,24 @@ private fun Spannable.readSpansWithContent() = getSpans<Any>().map { span ->
}.reversed()
private fun Any.readTags(): SpanTags {
return when (this::class) {
OrderedListItemSpan::class -> SpanTags("[list item]", "[/list item]")
HtmlCodeSpan::class -> SpanTags("[code]", "[/code]")
StrongEmphasisSpan::class -> SpanTags("[bold]", "[/bold]")
EmphasisSpan::class, CustomTypefaceSpan::class -> SpanTags("[italic]", "[/italic]")
else -> throw IllegalArgumentException("Unknown ${this::class}")
val tagName = when (this::class) {
OrderedListItemSpan::class -> "list item"
HtmlCodeSpan::class ->
if ((this as HtmlCodeSpan).isBlock) "code block" else "inline code"
StrongEmphasisSpan::class -> "bold"
EmphasisSpan::class, CustomTypefaceSpan::class -> "italic"
InlineCodeSpan::class -> "inline code"
else -> if (this::class.qualifiedName!!.startsWith("android.widget")) {
null
} else {
throw IllegalArgumentException("Unknown ${this::class}")
}
}
return if (tagName == null) {
SpanTags("", "")
} else {
SpanTags("[$tagName]", "[/$tagName]")
}
}

View File

@ -16,7 +16,8 @@
package im.vector.app.features.html
import androidx.core.text.toSpannable
import android.widget.TextView
import androidx.core.text.toSpanned
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.ColorProvider
@ -36,16 +37,19 @@ class EventHtmlRendererTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val fakeVectorPreferences = mockk<VectorPreferences>().also {
every { it.latexMathsIsEnabled() } returns false
every { it.isRichTextEditorEnabled() } returns false
}
private val fakeSessionHolder = mockk<ActiveSessionHolder>()
private val renderer = EventHtmlRenderer(
MatrixHtmlPluginConfigure(ColorProvider(context), context.resources),
MatrixHtmlPluginConfigure(ColorProvider(context), context.resources, fakeVectorPreferences),
context,
fakeVectorPreferences,
fakeSessionHolder,
)
private val textView: TextView = TextView(context)
@Test
fun takesInitialListPositionIntoAccount() {
val result = """<ol start="5"><li>first entry<li></ol>""".renderAsTestSpan()
@ -57,7 +61,7 @@ class EventHtmlRendererTest {
fun doesNotProcessMarkdownWithinCodeBlocks() {
val result = """<code>__italic__ **bold**</code>""".renderAsTestSpan()
result shouldBeEqualTo "[code]__italic__ **bold**[/code]"
result shouldBeEqualTo "[inline code]__italic__ **bold**[/inline code]"
}
@Test
@ -71,7 +75,15 @@ class EventHtmlRendererTest {
fun processesHtmlWithinCodeBlocks() {
val result = """<code><i>italic</i> <b>bold</b></code>""".renderAsTestSpan()
result shouldBeEqualTo "[code][italic]italic[/italic] [bold]bold[/bold][/code]"
result shouldBeEqualTo "[inline code][italic]italic[/italic] [bold]bold[/bold][/inline code]"
}
@Test
fun processesHtmlWithinCodeBlocks_givenRichTextEditorEnabled() {
every { fakeVectorPreferences.isRichTextEditorEnabled() } returns true
val result = """<code><i>italic</i> <b>bold</b></code>""".renderAsTestSpan()
result shouldBeEqualTo "[inline code][italic]italic[/italic] [bold]bold[/bold][/inline code]"
}
@Test
@ -81,5 +93,9 @@ class EventHtmlRendererTest {
result shouldBeEqualTo """& < > ' """"
}
private fun String.renderAsTestSpan() = renderer.render(this).toSpannable().toTestSpan()
private fun String.renderAsTestSpan(): String {
textView.text = renderer.render(this).toSpanned()
renderer.plugins.forEach { markwonPlugin -> markwonPlugin.afterSetText(textView) }
return textView.text.toSpanned().toTestSpan()
}
}

View File

@ -246,6 +246,9 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
addRichTextMenuItem(R.drawable.ic_composer_numbered_list, R.string.rich_text_editor_numbered_list, ComposerAction.ORDERED_LIST) {
views.richTextComposerEditText.toggleList(ordered = true)
}
addRichTextMenuItem(R.drawable.ic_composer_inline_code, R.string.rich_text_editor_inline_code, ComposerAction.INLINE_CODE) {
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.InlineCode)
}
}
fun setLink(link: String?) =

View File

@ -160,6 +160,9 @@ class MessageItemFactory @Inject constructor(
textRendererFactory.create(roomId)
}
private val useRichTextEditorStyle: Boolean get() =
vectorPreferences.isRichTextEditorEnabled()
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
val highlight = params.isHighlighted
@ -480,6 +483,7 @@ class MessageItemFactory @Inject constructor(
highlight,
callback,
attributes,
useRichTextEditorStyle = vectorPreferences.isRichTextEditorEnabled(),
)
}
@ -586,7 +590,7 @@ class MessageItemFactory @Inject constructor(
val replyToContent = messageContent.relatesTo?.inReplyTo
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes, replyToContent)
} else {
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes, useRichTextEditorStyle)
}
}
@ -610,6 +614,7 @@ class MessageItemFactory @Inject constructor(
highlight,
callback,
attributes,
useRichTextEditorStyle,
)
}
@ -620,6 +625,7 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
useRichTextEditorStyle: Boolean,
): MessageTextItem? {
val renderedBody = textRenderer.render(body)
val bindingOptions = spanUtils.getBindingOptions(renderedBody)
@ -640,6 +646,7 @@ class MessageItemFactory @Inject constructor(
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
.useRichTextEditorStyle(useRichTextEditorStyle)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)

View File

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.text.Spanned
import android.text.method.MovementMethod
import android.view.ViewStub
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.PrecomputedTextCompat
import androidx.core.view.isVisible
@ -67,6 +68,9 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var markwonPlugins: (List<MarkwonPlugin>)? = null
@EpoxyAttribute
var useRichTextEditorStyle: Boolean = false
private val previewUrlViewUpdater = PreviewUrlViewUpdater()
override fun bind(holder: Holder) {
@ -82,27 +86,28 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
holder.previewUrlView.delegate = previewUrlCallback
holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout)
val messageView: AppCompatTextView = if (useRichTextEditorStyle) holder.richMessageView else holder.plainMessageView
if (useBigFont) {
holder.messageView.textSize = 44F
messageView.textSize = 44F
} else {
holder.messageView.textSize = 15.5F
messageView.textSize = 15.5F
}
if (searchForPills) {
message?.charSequence?.findPillsAndProcess(coroutineScope) {
// mmm.. not sure this is so safe in regards to cell reuse
it.bind(holder.messageView)
it.bind(messageView)
}
}
message?.charSequence.let { charSequence ->
markwonPlugins?.forEach { plugin -> plugin.beforeSetText(holder.messageView, charSequence as Spanned) }
markwonPlugins?.forEach { plugin -> plugin.beforeSetText(messageView, charSequence as Spanned) }
}
super.bind(holder)
holder.messageView.movementMethod = movementMethod
renderSendState(holder.messageView, holder.messageView)
holder.messageView.onClick(attributes.itemClickListener)
holder.messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)
holder.messageView.setTextWithEmojiSupport(message?.charSequence, bindingOptions)
markwonPlugins?.forEach { plugin -> plugin.afterSetText(holder.messageView) }
messageView.movementMethod = movementMethod
renderSendState(messageView, messageView)
messageView.onClick(attributes.itemClickListener)
messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)
messageView.setTextWithEmojiSupport(message?.charSequence, bindingOptions)
markwonPlugins?.forEach { plugin -> plugin.afterSetText(messageView) }
}
private fun AppCompatTextView.setTextWithEmojiSupport(message: CharSequence?, bindingOptions: BindingOptions?) {
@ -125,8 +130,15 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
val previewUrlView by bind<PreviewUrlView>(R.id.messageUrlPreview)
private val richMessageStub by bind<ViewStub>(R.id.richMessageTextViewStub)
private val plainMessageStub by bind<ViewStub>(R.id.plainMessageTextViewStub)
val richMessageView: AppCompatTextView by lazy {
richMessageStub.inflate().findViewById(R.id.messageTextView)
}
val plainMessageView: AppCompatTextView by lazy {
plainMessageStub.inflate().findViewById(R.id.messageTextView)
}
}
inner class PreviewUrlViewUpdater : PreviewUrlRetriever.PreviewUrlRetrieverListener {

View File

@ -30,6 +30,8 @@ import android.content.res.Resources
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.widget.TextView
import androidx.core.text.toSpannable
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
@ -38,6 +40,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.settings.VectorPreferences
import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonPlugin
@ -64,8 +67,8 @@ import javax.inject.Singleton
@Singleton
class EventHtmlRenderer @Inject constructor(
htmlConfigure: MatrixHtmlPluginConfigure,
context: Context,
vectorPreferences: VectorPreferences,
private val context: Context,
private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder
) {
@ -73,73 +76,121 @@ class EventHtmlRenderer @Inject constructor(
fun afterRender(renderedText: Spannable)
}
private val builder = Markwon.builder(context)
.usePlugin(HtmlPlugin.create(htmlConfigure))
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
val url = drawable.destination
if (url.isMxcUrl()) {
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val imageUrl = contentUrlResolver.resolveFullSize(url)
// Override size to avoid crashes for huge pictures
return Glide.with(context).load(imageUrl).override(500)
}
// We don't want to support other url schemes here, so just return a request for null
return Glide.with(context).load(null as String?)
}
private val glidePlugin = GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
val url = drawable.destination
if (url.isMxcUrl()) {
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val imageUrl = contentUrlResolver.resolveFullSize(url)
// Override size to avoid crashes for huge pictures
return Glide.with(context).load(imageUrl).override(500)
}
// We don't want to support other url schemes here, so just return a request for null
return Glide.with(context).load(null as String?)
}
override fun cancel(target: Target<*>) {
Glide.with(context).clear(target)
}
}))
override fun cancel(target: Target<*>) {
Glide.with(context).clear(target)
}
})
private val markwon = if (vectorPreferences.latexMathsIsEnabled()) {
// If latex maths is enabled in app preferences, refomat it so Markwon recognises it
// It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex
builder
.usePlugin(object : AbstractMarkwonPlugin() {
override fun processMarkdown(markdown: String): String {
return markdown
.replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) { matchResult ->
"$$" + matchResult.groupValues[1] + "$$"
}
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) { matchResult ->
"\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
}
}
})
.usePlugin(JLatexMathPlugin.create(44F) { builder ->
builder.inlinesEnabled(true)
builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(24, 8))
})
} else {
builder
}
.usePlugin(
MarkwonInlineParserPlugin.create(
/* Configuring the Markwon inline formatting processor.
* Default settings are all Markdown features. Turn those off, only using the
* inline HTML processor and HTML entities processor.
*/
MarkwonInlineParser.factoryBuilderNoDefaults()
.addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor
.addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor
)
)
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder.setFactory(
Emphasis::class.java
) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) }
private val latexPlugins = listOf(
object : AbstractMarkwonPlugin() {
override fun processMarkdown(markdown: String): String {
return markdown
.replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) { matchResult ->
"$$" + matchResult.groupValues[1] + "$$"
}
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) { matchResult ->
"\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
}
}
},
JLatexMathPlugin.create(44F) { builder ->
builder.inlinesEnabled(true)
builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(24, 8))
}
)
override fun configureParser(builder: Parser.Builder) {
/* Configuring the Markwon block formatting processor.
* Default settings are all Markdown blocks. Turn those off.
private val markwonInlineParserPlugin =
MarkwonInlineParserPlugin.create(
/* Configuring the Markwon inline formatting processor.
* Default settings are all Markdown features. Turn those off, only using the
* inline HTML processor and HTML entities processor.
*/
builder.enabledBlockTypes(kotlin.collections.emptySet())
MarkwonInlineParser.factoryBuilderNoDefaults()
.addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor
.addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor
)
private val italicPlugin = object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder.setFactory(
Emphasis::class.java
) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) }
}
override fun configureParser(builder: Parser.Builder) {
/* Configuring the Markwon block formatting processor.
* Default settings are all Markdown blocks. Turn those off.
*/
builder.enabledBlockTypes(emptySet())
}
}
private val cleanUpIntermediateCodePlugin = object : AbstractMarkwonPlugin() {
override fun afterSetText(textView: TextView) {
super.afterSetText(textView)
// Remove any intermediate spans
val text = textView.text.toSpannable()
text.getSpans(0, text.length, IntermediateCodeSpan::class.java)
.forEach { span ->
text.removeSpan(span)
}
}
}
/**
* Workaround for https://github.com/noties/Markwon/issues/423
*/
private val removeLeadingNewlineForInlineCode = object : AbstractMarkwonPlugin() {
override fun afterSetText(textView: TextView) {
super.afterSetText(textView)
val text = SpannableStringBuilder(textView.text.toSpannable())
val inlineCodeSpans = text.getSpans(0, textView.length(), InlineCodeSpan::class.java).toList()
val legacyInlineCodeSpans = text.getSpans(0, textView.length(), HtmlCodeSpan::class.java).filter { !it.isBlock }
val spans = inlineCodeSpans + legacyInlineCodeSpans
if (spans.isEmpty()) return
spans.forEach { span ->
val start = text.getSpanStart(span)
if (text[start] == '\n') {
text.replace(start, start + 1, "")
}
})
}
textView.text = text
}
}
private val markwon = Markwon.builder(context)
.usePlugin(HtmlRootTagPlugin())
.usePlugin(HtmlPlugin.create(htmlConfigure))
.usePlugin(removeLeadingNewlineForInlineCode)
.usePlugin(glidePlugin)
.apply {
if (vectorPreferences.latexMathsIsEnabled()) {
// If latex maths is enabled in app preferences, refomat it so Markwon recognises it
// It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex
latexPlugins.forEach(::usePlugin)
}
}
.usePlugin(markwonInlineParserPlugin)
.usePlugin(italicPlugin)
.usePlugin(cleanUpIntermediateCodePlugin)
.textSetter(PrecomputedFutureTextSetterCompat.create())
.build()
@ -185,7 +236,11 @@ class EventHtmlRenderer @Inject constructor(
}
}
class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider, private val resources: Resources) : HtmlPlugin.HtmlConfigure {
class MatrixHtmlPluginConfigure @Inject constructor(
private val colorProvider: ColorProvider,
private val resources: Resources,
private val vectorPreferences: VectorPreferences,
) : HtmlPlugin.HtmlConfigure {
override fun configureHtml(plugin: HtmlPlugin) {
plugin
@ -193,6 +248,7 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: C
.addHandler(FontTagHandler())
.addHandler(ParagraphHandler(DimensionConverter(resources)))
.addHandler(MxReplyTagHandler())
.addHandler(CodePostProcessorTagHandler(vectorPreferences))
.addHandler(CodePreTagHandler())
.addHandler(CodeTagHandler())
.addHandler(SpanHandler(colorProvider))

View File

@ -16,20 +16,29 @@
package im.vector.app.features.html
import im.vector.app.features.settings.VectorPreferences
import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
import io.noties.markwon.core.MarkwonTheme
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.TagHandler
class CodeTagHandler : TagHandler() {
/**
* Span to be added to any <code> found during initial pass.
* The actual code spans can then be added on a second pass using this
* span as a reference.
*/
internal class IntermediateCodeSpan(
var isBlock: Boolean
)
internal class CodeTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
SpannableBuilder.setSpans(
visitor.builder(),
HtmlCodeSpan(visitor.configuration().theme(), false),
tag.start(),
tag.end()
visitor.builder(), IntermediateCodeSpan(isBlock = false), tag.start(), tag.end()
)
}
@ -42,15 +51,13 @@ class CodeTagHandler : TagHandler() {
* Pre tag are already handled by HtmlPlugin to keep the formatting.
* We are only using it to check for <pre><code>*</code></pre> tags.
*/
class CodePreTagHandler : TagHandler() {
internal class CodePreTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val htmlCodeSpan = visitor.builder()
.getSpans(tag.start(), tag.end())
.firstOrNull {
it.what is HtmlCodeSpan
}
if (htmlCodeSpan != null) {
(htmlCodeSpan.what as HtmlCodeSpan).isBlock = true
val codeSpan = visitor.builder().getSpans(tag.start(), tag.end()).firstOrNull {
it.what is IntermediateCodeSpan
}
if (codeSpan != null) {
(codeSpan.what as IntermediateCodeSpan).isBlock = true
}
}
@ -58,3 +65,42 @@ class CodePreTagHandler : TagHandler() {
return listOf("pre")
}
}
internal class CodePostProcessorTagHandler(
private val vectorPreferences: VectorPreferences,
) : TagHandler() {
override fun supportedTags() = listOf(HtmlRootTagPlugin.ROOT_TAG_NAME)
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
if (tag.attributes()[HtmlRootTagPlugin.ROOT_ATTRIBUTE] == null) {
return
}
if (tag.isBlock) {
visitChildren(visitor, renderer, tag.asBlock)
}
// Replace any intermediate code spans with the real formatting spans
visitor.builder()
.getSpans(tag.start(), tag.end())
.filter {
it.what is IntermediateCodeSpan
}.forEach { code ->
val intermediateCodeSpan = code.what as IntermediateCodeSpan
val theme = visitor.configuration().theme()
val span = intermediateCodeSpan.toFinalCodeSpan(theme)
SpannableBuilder.setSpans(
visitor.builder(), span, code.start, code.end
)
}
}
private fun IntermediateCodeSpan.toFinalCodeSpan(
markwonTheme: MarkwonTheme
): Any = if (vectorPreferences.isRichTextEditorEnabled() && !isBlock) {
InlineCodeSpan()
} else {
HtmlCodeSpan(markwonTheme, isBlock)
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.html
import io.noties.markwon.AbstractMarkwonPlugin
/**
* A root node enables post-processing of optionally nested tags.
* See: [im.vector.app.features.html.CodePostProcessorTagHandler]
*/
internal class HtmlRootTagPlugin : AbstractMarkwonPlugin() {
companion object {
const val ROOT_ATTRIBUTE = "data-root"
const val ROOT_TAG_NAME = "div"
}
override fun processMarkdown(html: String): String {
return "<$ROOT_TAG_NAME $ROOT_ATTRIBUTE>$html</$ROOT_TAG_NAME>"
}
}

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:pathData="M24.958,15.621C25.117,15.092 24.816,14.534 24.287,14.375C23.758,14.217 23.201,14.517 23.042,15.046L19.042,28.379C18.883,28.908 19.184,29.466 19.713,29.624C20.242,29.783 20.799,29.483 20.958,28.954L24.958,15.621Z"
android:fillColor="#8D97A5"/>
<path
android:pathData="M15.974,17.232C15.549,16.878 14.919,16.936 14.565,17.36L11.232,21.36C10.923,21.731 10.923,22.269 11.232,22.64L14.565,26.64C14.919,27.065 15.549,27.122 15.974,26.768C16.398,26.415 16.455,25.784 16.102,25.36L13.302,22L16.102,18.64C16.455,18.216 16.398,17.585 15.974,17.232Z"
android:fillColor="#8D97A5"/>
<path
android:pathData="M28.027,17.232C28.451,16.878 29.081,16.936 29.435,17.36L32.768,21.36C33.077,21.731 33.077,22.269 32.768,22.64L29.435,26.64C29.081,27.065 28.451,27.122 28.027,26.768C27.602,26.415 27.545,25.784 27.898,25.36L30.698,22L27.898,18.64C27.545,18.216 27.602,17.585 28.027,17.232Z"
android:fillColor="#8D97A5"/>
</vector>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Message view to be used when the rich text editor is not enabled -->
<androidx.appcompat.widget.AppCompatTextView android:id="@+id/messageTextView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textColor="?vctr_content_primary"
tools:text="@sample/messages.json/data/message"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" />

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Message view to be used when the rich text editor is enabled -->
<io.element.android.wysiwyg.EditorStyledTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageTextView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textColor="?vctr_content_primary"
tools:text="@sample/messages.json/data/message" />

View File

@ -7,14 +7,17 @@
android:orientation="vertical"
tools:viewBindingIgnore="true">
<TextView
android:id="@+id/messageTextView"
style="@style/Widget.Vector.TextView.Body"
<ViewStub
android:id="@+id/plainMessageTextViewStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textColor="?vctr_content_primary"
tools:text="@sample/messages.json/data/message" />
android:layout="@layout/item_timeline_event_text_message_plain_stub" />
<ViewStub
android:id="@+id/richMessageTextViewStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_text_message_rich_stub" />
<im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
android:id="@+id/messageUrlPreview"