diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt index 48811cec..dceb56a7 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt @@ -5,21 +5,18 @@ import android.content.res.Configuration import android.os.Build import android.os.Handler import android.os.Looper -import android.text.SpannableString import android.util.AttributeSet import android.util.Log import android.view.View import android.widget.FrameLayout import com.facebook.react.bridge.ReadableMap -import com.swmansion.enriched.markdown.parser.MarkdownASTNode import com.swmansion.enriched.markdown.parser.Md4cFlags import com.swmansion.enriched.markdown.parser.Parser -import com.swmansion.enriched.markdown.renderer.Renderer -import com.swmansion.enriched.markdown.spans.ImageSpan import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.utils.common.FeatureFlags -import com.swmansion.enriched.markdown.utils.common.MarkdownSegment +import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer +import com.swmansion.enriched.markdown.utils.common.RenderedSegment import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent @@ -28,23 +25,6 @@ import com.swmansion.enriched.markdown.views.TableContainerView import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -private sealed interface RenderSegment { - data class Text( - val styledText: SpannableString, - val imageSpans: List, - val needsJustify: Boolean, - val lastElementMarginBottom: Float, - ) : RenderSegment - - data class Table( - val node: MarkdownASTNode, - ) : RenderSegment - - data class Math( - val latex: String, - ) : RenderSegment -} - class EnrichedMarkdown @JvmOverloads constructor( @@ -200,16 +180,17 @@ class EnrichedMarkdown return@execute } - val processedSegments = - splitASTIntoSegments(ast).map { segment -> - when (segment) { - is MarkdownSegment.Text -> renderTextSegment(segment.nodes, style) - is MarkdownSegment.Table -> RenderSegment.Table(segment.node) - is MarkdownSegment.Math -> RenderSegment.Math(segment.latex) - } - } - - postToMain(renderId) { applyRenderedSegments(processedSegments, style) } + val segments = splitASTIntoSegments(ast) + val renderedSegments = + MarkdownSegmentRenderer.render( + segments, + style, + context, + onLinkPressCallback, + onLinkLongPressCallback, + ) + + postToMain(renderId) { applyRenderedSegments(renderedSegments, style) } } catch (e: Exception) { Log.e(TAG, "Render failed", e) postToMain(renderId) { clearSegments() } @@ -217,32 +198,17 @@ class EnrichedMarkdown } } - private fun renderTextSegment( - nodes: List, - style: StyleConfig, - ): RenderSegment.Text { - val documentWrapper = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes) - val renderer = Renderer().apply { configure(style, context) } - - return RenderSegment.Text( - styledText = renderer.renderDocument(documentWrapper, onLinkPressCallback, onLinkLongPressCallback), - imageSpans = renderer.getCollectedImageSpans().toList(), - needsJustify = style.needsJustify, - lastElementMarginBottom = renderer.getLastElementMarginBottom(), - ) - } - private fun applyRenderedSegments( - renderedSegments: List, + renderedSegments: List, style: StyleConfig, ) { clearSegments() renderedSegments.forEach { segment -> val view = when (segment) { - is RenderSegment.Text -> createTextView(segment) - is RenderSegment.Table -> createTableView(segment, style) - is RenderSegment.Math -> createMathView(segment, style) + is RenderedSegment.Text -> createTextView(segment) + is RenderedSegment.Table -> createTableView(segment, style) + is RenderedSegment.Math -> createMathView(segment, style) } segmentViews.add(view) addView(view) @@ -250,7 +216,7 @@ class EnrichedMarkdown layoutSegments() } - private fun createTextView(segment: RenderSegment.Text) = + private fun createTextView(segment: RenderedSegment.Text) = EnrichedMarkdownInternalText(context).apply { spoilerOverlay = this@EnrichedMarkdown.spoilerOverlay setIsSelectable(selectable) @@ -271,7 +237,7 @@ class EnrichedMarkdown } private fun createTableView( - segment: RenderSegment.Table, + segment: RenderedSegment.Table, style: StyleConfig, ) = TableContainerView(context, style).apply { allowFontScaling = this@EnrichedMarkdown.allowFontScaling @@ -282,7 +248,7 @@ class EnrichedMarkdown } private fun createMathView( - segment: RenderSegment.Math, + segment: RenderedSegment.Math, style: StyleConfig, ): android.view.View { if (!FeatureFlags.IS_MATH_ENABLED) return android.view.View(context) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt index 8d381e15..0abbba98 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt @@ -12,7 +12,6 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.PixelUtil import com.facebook.yoga.YogaMeasureMode import com.facebook.yoga.YogaMeasureOutput -import com.swmansion.enriched.markdown.parser.MarkdownASTNode import com.swmansion.enriched.markdown.parser.Md4cFlags import com.swmansion.enriched.markdown.parser.Parser import com.swmansion.enriched.markdown.renderer.Renderer @@ -21,7 +20,8 @@ import com.swmansion.enriched.markdown.spans.MathMetrics import com.swmansion.enriched.markdown.spans.MathRenderMode import com.swmansion.enriched.markdown.styles.StyleConfig import com.swmansion.enriched.markdown.utils.common.FeatureFlags -import com.swmansion.enriched.markdown.utils.common.MarkdownSegment +import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer +import com.swmansion.enriched.markdown.utils.common.RenderedSegment import com.swmansion.enriched.markdown.utils.common.getBooleanOrDefault import com.swmansion.enriched.markdown.utils.common.getMapOrNull import com.swmansion.enriched.markdown.utils.common.getStringOrDefault @@ -307,12 +307,13 @@ object MeasurementStore { val style = StyleConfig(styleMap, context, allowFontScaling, maxFontSizeMultiplier) val segments = splitASTIntoSegments(ast) + val renderedSegments = MarkdownSegmentRenderer.render(segments, style, context, null, null) val mathHeightByIndex = HashMap() val mathSegmentIndices = mutableListOf() val mathRequests = mutableListOf() - for ((i, segment) in segments.withIndex()) { - if (segment is MarkdownSegment.Math) { + for ((i, segment) in renderedSegments.withIndex()) { + if (segment is RenderedSegment.Math) { mathSegmentIndices.add(i) mathRequests.add( MathMeasureRequest( @@ -333,33 +334,30 @@ object MeasurementStore { } val widthPx = width.toInt().coerceAtLeast(1) - val lastIndex = segments.lastIndex + val lastIndex = renderedSegments.lastIndex var totalHeightPx = 0f var maxContentWidthPx = 0f - for ((index, segment) in segments.withIndex()) { + for ((index, segment) in renderedSegments.withIndex()) { val isLastSegment = index == lastIndex val includeBottomMargin = if (isLastSegment) allowTrailingMargin else true when (segment) { - is MarkdownSegment.Text -> { - val segmentRenderer = Renderer().apply { configure(style, context) } - val tempDoc = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = segment.nodes) - val styledText = segmentRenderer.renderDocument(tempDoc, null) - styledText.replaceMathSpansWithPlaceholders(context) + is RenderedSegment.Text -> { + segment.styledText.replaceMathSpansWithPlaceholders(context) - val layout = createStaticLayout(styledText, fontSize, widthPx) + val layout = createStaticLayout(segment.styledText, fontSize, widthPx) totalHeightPx += layout.height val segmentMaxLineWidth = (0 until layout.lineCount).maxOfOrNull { layout.getLineWidth(it) } ?: 0f maxContentWidthPx = maxOf(maxContentWidthPx, ceil(segmentMaxLineWidth)) if (includeBottomMargin) { - totalHeightPx += segmentRenderer.getLastElementMarginBottom() + totalHeightPx += segment.lastElementMarginBottom } } - is MarkdownSegment.Table -> { + is RenderedSegment.Table -> { totalHeightPx += style.tableStyle.marginTop totalHeightPx += TableContainerView.measureTableNodeHeight(segment.node, style, context) maxContentWidthPx = width @@ -368,7 +366,7 @@ object MeasurementStore { } } - is MarkdownSegment.Math -> { + is RenderedSegment.Math -> { totalHeightPx += style.mathStyle.marginTop totalHeightPx += mathHeightByIndex[index] ?: 0f maxContentWidthPx = width diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt new file mode 100644 index 00000000..0120d3f4 --- /dev/null +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt @@ -0,0 +1,60 @@ +package com.swmansion.enriched.markdown.utils.common + +import android.content.Context +import android.text.SpannableString +import com.swmansion.enriched.markdown.parser.MarkdownASTNode +import com.swmansion.enriched.markdown.renderer.Renderer +import com.swmansion.enriched.markdown.spans.ImageSpan +import com.swmansion.enriched.markdown.styles.StyleConfig + +sealed interface RenderedSegment { + data class Text( + val styledText: SpannableString, + val imageSpans: List, + val needsJustify: Boolean, + val lastElementMarginBottom: Float, + ) : RenderedSegment + + data class Table( + val node: MarkdownASTNode, + ) : RenderedSegment + + data class Math( + val latex: String, + ) : RenderedSegment +} + +object MarkdownSegmentRenderer { + fun render( + segments: List, + style: StyleConfig, + context: Context, + onLinkPress: ((String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + ): List = + segments.map { segment -> + when (segment) { + is MarkdownSegment.Text -> renderTextSegment(segment.nodes, style, context, onLinkPress, onLinkLongPress) + is MarkdownSegment.Table -> RenderedSegment.Table(segment.node) + is MarkdownSegment.Math -> RenderedSegment.Math(segment.latex) + } + } + + private fun renderTextSegment( + nodes: List, + style: StyleConfig, + context: Context, + onLinkPress: ((String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + ): RenderedSegment.Text { + val documentWrapper = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes) + val renderer = Renderer().apply { configure(style, context) } + + return RenderedSegment.Text( + styledText = renderer.renderDocument(documentWrapper, onLinkPress, onLinkLongPress), + imageSpans = renderer.getCollectedImageSpans().toList(), + needsJustify = style.needsJustify, + lastElementMarginBottom = renderer.getLastElementMarginBottom(), + ) + } +}