From a9fe21e583d678041e33900167727c1a0746d990 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 27 Jan 2022 18:17:23 +0100 Subject: [PATCH] Timeline html rendering: handle code tags --- .../timeline/factory/MessageItemFactory.kt | 60 ++----------- .../timeline/item/MessageBlockCodeItem.kt | 57 ------------ .../vector/app/features/html/CodeVisitor.kt | 55 ------------ .../app/features/html/EventHtmlRenderer.kt | 2 + .../app/features/html/HtmlCodeHandlers.kt | 60 +++++++++++++ .../vector/app/features/html/HtmlCodeSpan.kt | 86 +++++++++++++++++++ .../app/features/html/ParagraphHandler.kt | 18 ++-- 7 files changed, 161 insertions(+), 177 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt delete mode 100644 vector/src/main/java/im/vector/app/features/html/CodeVisitor.kt create mode 100644 vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt create mode 100644 vector/src/main/java/im/vector/app/features/html/HtmlCodeSpan.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index c004a2f928..2fc996c28d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -43,8 +43,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttrib import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem -import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem -import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem @@ -63,7 +61,6 @@ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequest import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify -import im.vector.app.features.html.CodeVisitor import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.SpanUtils @@ -71,7 +68,6 @@ import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import me.gujun.android.span.span -import org.commonmark.node.Document import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -454,46 +450,22 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { - val isFormatted = messageContent.matrixFormattedBody.isNullOrBlank().not() - return if (isFormatted) { - // First detect if the message contains some code block(s) or inline code - val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document - val codeVisitor = CodeVisitor() - codeVisitor.visit(localFormattedBody) - when (codeVisitor.codeKind) { - CodeVisitor.Kind.BLOCK -> { - val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) - if (codeFormattedBlock == null) { - buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) - } else { - buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes) - } - } - CodeVisitor.Kind.INLINE -> { - val codeFormatted = htmlRenderer.get().render(localFormattedBody) - if (codeFormatted == null) { - buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) - } else { - buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) - } - } - CodeVisitor.Kind.NONE -> { - buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) - } - } + val matrixFormattedBody = messageContent.matrixFormattedBody + return if (matrixFormattedBody != null) { + buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes) } else { buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } } - private fun buildFormattedTextItem(messageContent: MessageTextContent, + private fun buildFormattedTextItem(matrixFormattedBody: String, informationData: MessageInformationData, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { - val compressed = htmlCompressor.compress(messageContent.formattedBody!!) - val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) - return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes) + val compressed = htmlCompressor.compress(matrixFormattedBody) + val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned + return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes) } private fun buildMessageTextItem(body: CharSequence, @@ -526,24 +498,6 @@ class MessageItemFactory @Inject constructor( .movementMethod(createLinkMovementMethod(callback)) } - private fun buildCodeBlockItem(formattedBody: CharSequence, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? { - return MessageBlockCodeItem_() - .apply { - if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited("", callback, informationData) - editedSpan(spannable.toEpoxyCharSequence()) - } - } - .leftGuideline(avatarSizeProvider.leftGuideline) - .attributes(attributes) - .highlighted(highlight) - .message(formattedBody.toEpoxyCharSequence()) - } - private fun annotateWithEdited(linkifiedBody: CharSequence, callback: TimelineEventController.Callback?, informationData: MessageInformationData): Spannable { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt deleted file mode 100644 index 9e162a8f1e..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2019 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.home.room.detail.timeline.item - -import android.widget.TextView -import com.airbnb.epoxy.EpoxyAttribute -import com.airbnb.epoxy.EpoxyModelClass -import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence -import im.vector.app.core.epoxy.onClick -import im.vector.app.core.extensions.setTextOrHide -import me.saket.bettermovementmethod.BetterLinkMovementMethod - -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) -abstract class MessageBlockCodeItem : AbsMessageItem() { - - @EpoxyAttribute - var message: EpoxyCharSequence? = null - - @EpoxyAttribute - var editedSpan: EpoxyCharSequence? = null - - override fun bind(holder: Holder) { - super.bind(holder) - holder.messageView.text = message?.charSequence - renderSendState(holder.messageView, holder.messageView) - holder.messageView.onClick(attributes.itemClickListener) - holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) - holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance() - holder.editedView.setTextOrHide(editedSpan?.charSequence) - } - - override fun getViewStubId() = STUB_ID - - class Holder : AbsMessageItem.Holder(STUB_ID) { - val messageView by bind(R.id.codeBlockTextView) - val editedView by bind(R.id.codeBlockEditedView) - } - - companion object { - private const val STUB_ID = R.id.messageContentCodeBlockStub - } -} diff --git a/vector/src/main/java/im/vector/app/features/html/CodeVisitor.kt b/vector/src/main/java/im/vector/app/features/html/CodeVisitor.kt deleted file mode 100644 index f1612c3717..0000000000 --- a/vector/src/main/java/im/vector/app/features/html/CodeVisitor.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2019 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 org.commonmark.node.AbstractVisitor -import org.commonmark.node.Code -import org.commonmark.node.FencedCodeBlock -import org.commonmark.node.IndentedCodeBlock - -/** - * This class is in charge of visiting nodes and tells if we have some code nodes (inline or block). - */ -class CodeVisitor : AbstractVisitor() { - - var codeKind: Kind = Kind.NONE - private set - - override fun visit(fencedCodeBlock: FencedCodeBlock?) { - if (codeKind == Kind.NONE) { - codeKind = Kind.BLOCK - } - } - - override fun visit(indentedCodeBlock: IndentedCodeBlock?) { - if (codeKind == Kind.NONE) { - codeKind = Kind.BLOCK - } - } - - override fun visit(code: Code?) { - if (codeKind == Kind.NONE) { - codeKind = Kind.INLINE - } - } - - enum class Kind { - NONE, - INLINE, - BLOCK - } -} diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 5fc1306b42..7d78be3584 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -121,6 +121,8 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: C .addHandler(FontTagHandler()) .addHandler(ParagraphHandler(DimensionConverter(resources))) .addHandler(MxReplyTagHandler()) + .addHandler(CodePreTagHandler()) + .addHandler(CodeTagHandler()) .addHandler(SpanHandler(colorProvider)) } } diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt new file mode 100644 index 0000000000..1010625370 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 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.MarkwonVisitor +import io.noties.markwon.SpannableBuilder +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.MarkwonHtmlRenderer +import io.noties.markwon.html.TagHandler + +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() + ) + } + + override fun supportedTags(): List { + return listOf("code") + } +} + +/** + * Pre tag are already handled by HtmlPlugin to keep the formatting. + * We are only using it to check for
*
tags. + */ +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 + } + } + + override fun supportedTags(): List { + return listOf("pre") + } +} diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlCodeSpan.kt b/vector/src/main/java/im/vector/app/features/html/HtmlCodeSpan.kt new file mode 100644 index 0000000000..7f01321aab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/HtmlCodeSpan.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022 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 android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.text.Layout +import android.text.TextPaint +import android.text.style.LeadingMarginSpan +import android.text.style.MetricAffectingSpan +import io.noties.markwon.core.MarkwonTheme + +class HtmlCodeSpan(private val theme: MarkwonTheme, var isBlock: Boolean) : MetricAffectingSpan(), LeadingMarginSpan { + + private val rect = Rect() + private val paint = Paint() + + override fun updateDrawState(p: TextPaint) { + applyTextStyle(p) + if (!isBlock) { + p.bgColor = theme.getCodeBackgroundColor(p) + } + } + + override fun updateMeasureState(p: TextPaint) { + applyTextStyle(p) + } + + private fun applyTextStyle(p: TextPaint) { + if (isBlock) { + theme.applyCodeBlockTextStyle(p) + } else { + theme.applyCodeTextStyle(p) + } + } + + override fun getLeadingMargin(first: Boolean): Int { + return theme.codeBlockMargin + } + + override fun drawLeadingMargin( + c: Canvas, + p: Paint?, + x: Int, + dir: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence?, + start: Int, + end: Int, + first: Boolean, + layout: Layout? + ) { + if (!isBlock) return + + paint.style = Paint.Style.FILL + paint.color = theme.getCodeBlockBackgroundColor(p!!) + val left: Int + val right: Int + if (dir > 0) { + left = x + right = c.width + } else { + left = x - c.width + right = x + } + rect[left, top, right] = bottom + c.drawRect(rect, paint) + } +} diff --git a/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt b/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt index e62134ae7c..3dd1b4f091 100644 --- a/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt +++ b/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt @@ -16,7 +16,6 @@ package im.vector.app.features.html -import android.content.res.Resources import im.vector.app.core.utils.DimensionConverter import io.noties.markwon.MarkwonVisitor import io.noties.markwon.SpannableBuilder @@ -24,7 +23,6 @@ import io.noties.markwon.html.HtmlTag import io.noties.markwon.html.MarkwonHtmlRenderer import io.noties.markwon.html.TagHandler import me.gujun.android.span.style.VerticalPaddingSpan -import org.commonmark.node.BlockQuote class ParagraphHandler(private val dimensionConverter: DimensionConverter) : TagHandler() { @@ -34,15 +32,11 @@ class ParagraphHandler(private val dimensionConverter: DimensionConverter) : Tag if (tag.isBlock) { visitChildren(visitor, renderer, tag.asBlock) } - val configuration = visitor.configuration() - val factory = configuration.spansFactory().get(BlockQuote::class.java) - if (factory != null) { - SpannableBuilder.setSpans( - visitor.builder(), - VerticalPaddingSpan(dimensionConverter.dpToPx(16), 0), - tag.start(), - tag.end() - ) - } + SpannableBuilder.setSpans( + visitor.builder(), + VerticalPaddingSpan(dimensionConverter.dpToPx(4), dimensionConverter.dpToPx(4)), + tag.start(), + tag.end() + ) } }