Skip to content

Commit 87c0872

Browse files
authored
refactor: streamline markdown rendering by introducing RenderedSegment and MarkdownSegmentRenderer (#240)
1 parent ba4bccd commit 87c0872

3 files changed

Lines changed: 93 additions & 69 deletions

File tree

android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,18 @@ import android.content.res.Configuration
55
import android.os.Build
66
import android.os.Handler
77
import android.os.Looper
8-
import android.text.SpannableString
98
import android.util.AttributeSet
109
import android.util.Log
1110
import android.view.View
1211
import android.widget.FrameLayout
1312
import com.facebook.react.bridge.ReadableMap
14-
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
1513
import com.swmansion.enriched.markdown.parser.Md4cFlags
1614
import com.swmansion.enriched.markdown.parser.Parser
17-
import com.swmansion.enriched.markdown.renderer.Renderer
18-
import com.swmansion.enriched.markdown.spans.ImageSpan
1915
import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay
2016
import com.swmansion.enriched.markdown.styles.StyleConfig
2117
import com.swmansion.enriched.markdown.utils.common.FeatureFlags
22-
import com.swmansion.enriched.markdown.utils.common.MarkdownSegment
18+
import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer
19+
import com.swmansion.enriched.markdown.utils.common.RenderedSegment
2320
import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments
2421
import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent
2522
import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent
@@ -28,23 +25,6 @@ import com.swmansion.enriched.markdown.views.TableContainerView
2825
import java.util.concurrent.ExecutorService
2926
import java.util.concurrent.Executors
3027

31-
private sealed interface RenderSegment {
32-
data class Text(
33-
val styledText: SpannableString,
34-
val imageSpans: List<ImageSpan>,
35-
val needsJustify: Boolean,
36-
val lastElementMarginBottom: Float,
37-
) : RenderSegment
38-
39-
data class Table(
40-
val node: MarkdownASTNode,
41-
) : RenderSegment
42-
43-
data class Math(
44-
val latex: String,
45-
) : RenderSegment
46-
}
47-
4828
class EnrichedMarkdown
4929
@JvmOverloads
5030
constructor(
@@ -200,57 +180,43 @@ class EnrichedMarkdown
200180
return@execute
201181
}
202182

203-
val processedSegments =
204-
splitASTIntoSegments(ast).map { segment ->
205-
when (segment) {
206-
is MarkdownSegment.Text -> renderTextSegment(segment.nodes, style)
207-
is MarkdownSegment.Table -> RenderSegment.Table(segment.node)
208-
is MarkdownSegment.Math -> RenderSegment.Math(segment.latex)
209-
}
210-
}
211-
212-
postToMain(renderId) { applyRenderedSegments(processedSegments, style) }
183+
val segments = splitASTIntoSegments(ast)
184+
val renderedSegments =
185+
MarkdownSegmentRenderer.render(
186+
segments,
187+
style,
188+
context,
189+
onLinkPressCallback,
190+
onLinkLongPressCallback,
191+
)
192+
193+
postToMain(renderId) { applyRenderedSegments(renderedSegments, style) }
213194
} catch (e: Exception) {
214195
Log.e(TAG, "Render failed", e)
215196
postToMain(renderId) { clearSegments() }
216197
}
217198
}
218199
}
219200

220-
private fun renderTextSegment(
221-
nodes: List<MarkdownASTNode>,
222-
style: StyleConfig,
223-
): RenderSegment.Text {
224-
val documentWrapper = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes)
225-
val renderer = Renderer().apply { configure(style, context) }
226-
227-
return RenderSegment.Text(
228-
styledText = renderer.renderDocument(documentWrapper, onLinkPressCallback, onLinkLongPressCallback),
229-
imageSpans = renderer.getCollectedImageSpans().toList(),
230-
needsJustify = style.needsJustify,
231-
lastElementMarginBottom = renderer.getLastElementMarginBottom(),
232-
)
233-
}
234-
235201
private fun applyRenderedSegments(
236-
renderedSegments: List<RenderSegment>,
202+
renderedSegments: List<RenderedSegment>,
237203
style: StyleConfig,
238204
) {
239205
clearSegments()
240206
renderedSegments.forEach { segment ->
241207
val view =
242208
when (segment) {
243-
is RenderSegment.Text -> createTextView(segment)
244-
is RenderSegment.Table -> createTableView(segment, style)
245-
is RenderSegment.Math -> createMathView(segment, style)
209+
is RenderedSegment.Text -> createTextView(segment)
210+
is RenderedSegment.Table -> createTableView(segment, style)
211+
is RenderedSegment.Math -> createMathView(segment, style)
246212
}
247213
segmentViews.add(view)
248214
addView(view)
249215
}
250216
layoutSegments()
251217
}
252218

253-
private fun createTextView(segment: RenderSegment.Text) =
219+
private fun createTextView(segment: RenderedSegment.Text) =
254220
EnrichedMarkdownInternalText(context).apply {
255221
spoilerOverlay = this@EnrichedMarkdown.spoilerOverlay
256222
setIsSelectable(selectable)
@@ -271,7 +237,7 @@ class EnrichedMarkdown
271237
}
272238

273239
private fun createTableView(
274-
segment: RenderSegment.Table,
240+
segment: RenderedSegment.Table,
275241
style: StyleConfig,
276242
) = TableContainerView(context, style).apply {
277243
allowFontScaling = this@EnrichedMarkdown.allowFontScaling
@@ -282,7 +248,7 @@ class EnrichedMarkdown
282248
}
283249

284250
private fun createMathView(
285-
segment: RenderSegment.Math,
251+
segment: RenderedSegment.Math,
286252
style: StyleConfig,
287253
): android.view.View {
288254
if (!FeatureFlags.IS_MATH_ENABLED) return android.view.View(context)

android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import com.facebook.react.bridge.ReadableMap
1212
import com.facebook.react.uimanager.PixelUtil
1313
import com.facebook.yoga.YogaMeasureMode
1414
import com.facebook.yoga.YogaMeasureOutput
15-
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
1615
import com.swmansion.enriched.markdown.parser.Md4cFlags
1716
import com.swmansion.enriched.markdown.parser.Parser
1817
import com.swmansion.enriched.markdown.renderer.Renderer
@@ -21,7 +20,8 @@ import com.swmansion.enriched.markdown.spans.MathMetrics
2120
import com.swmansion.enriched.markdown.spans.MathRenderMode
2221
import com.swmansion.enriched.markdown.styles.StyleConfig
2322
import com.swmansion.enriched.markdown.utils.common.FeatureFlags
24-
import com.swmansion.enriched.markdown.utils.common.MarkdownSegment
23+
import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer
24+
import com.swmansion.enriched.markdown.utils.common.RenderedSegment
2525
import com.swmansion.enriched.markdown.utils.common.getBooleanOrDefault
2626
import com.swmansion.enriched.markdown.utils.common.getMapOrNull
2727
import com.swmansion.enriched.markdown.utils.common.getStringOrDefault
@@ -307,12 +307,13 @@ object MeasurementStore {
307307

308308
val style = StyleConfig(styleMap, context, allowFontScaling, maxFontSizeMultiplier)
309309
val segments = splitASTIntoSegments(ast)
310+
val renderedSegments = MarkdownSegmentRenderer.render(segments, style, context, null, null)
310311

311312
val mathHeightByIndex = HashMap<Int, Float>()
312313
val mathSegmentIndices = mutableListOf<Int>()
313314
val mathRequests = mutableListOf<MathMeasureRequest>()
314-
for ((i, segment) in segments.withIndex()) {
315-
if (segment is MarkdownSegment.Math) {
315+
for ((i, segment) in renderedSegments.withIndex()) {
316+
if (segment is RenderedSegment.Math) {
316317
mathSegmentIndices.add(i)
317318
mathRequests.add(
318319
MathMeasureRequest(
@@ -333,33 +334,30 @@ object MeasurementStore {
333334
}
334335

335336
val widthPx = width.toInt().coerceAtLeast(1)
336-
val lastIndex = segments.lastIndex
337+
val lastIndex = renderedSegments.lastIndex
337338
var totalHeightPx = 0f
338339
var maxContentWidthPx = 0f
339340

340-
for ((index, segment) in segments.withIndex()) {
341+
for ((index, segment) in renderedSegments.withIndex()) {
341342
val isLastSegment = index == lastIndex
342343
val includeBottomMargin = if (isLastSegment) allowTrailingMargin else true
343344

344345
when (segment) {
345-
is MarkdownSegment.Text -> {
346-
val segmentRenderer = Renderer().apply { configure(style, context) }
347-
val tempDoc = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = segment.nodes)
348-
val styledText = segmentRenderer.renderDocument(tempDoc, null)
349-
styledText.replaceMathSpansWithPlaceholders(context)
346+
is RenderedSegment.Text -> {
347+
segment.styledText.replaceMathSpansWithPlaceholders(context)
350348

351-
val layout = createStaticLayout(styledText, fontSize, widthPx)
349+
val layout = createStaticLayout(segment.styledText, fontSize, widthPx)
352350
totalHeightPx += layout.height
353351

354352
val segmentMaxLineWidth = (0 until layout.lineCount).maxOfOrNull { layout.getLineWidth(it) } ?: 0f
355353
maxContentWidthPx = maxOf(maxContentWidthPx, ceil(segmentMaxLineWidth))
356354

357355
if (includeBottomMargin) {
358-
totalHeightPx += segmentRenderer.getLastElementMarginBottom()
356+
totalHeightPx += segment.lastElementMarginBottom
359357
}
360358
}
361359

362-
is MarkdownSegment.Table -> {
360+
is RenderedSegment.Table -> {
363361
totalHeightPx += style.tableStyle.marginTop
364362
totalHeightPx += TableContainerView.measureTableNodeHeight(segment.node, style, context)
365363
maxContentWidthPx = width
@@ -368,7 +366,7 @@ object MeasurementStore {
368366
}
369367
}
370368

371-
is MarkdownSegment.Math -> {
369+
is RenderedSegment.Math -> {
372370
totalHeightPx += style.mathStyle.marginTop
373371
totalHeightPx += mathHeightByIndex[index] ?: 0f
374372
maxContentWidthPx = width
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.swmansion.enriched.markdown.utils.common
2+
3+
import android.content.Context
4+
import android.text.SpannableString
5+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
6+
import com.swmansion.enriched.markdown.renderer.Renderer
7+
import com.swmansion.enriched.markdown.spans.ImageSpan
8+
import com.swmansion.enriched.markdown.styles.StyleConfig
9+
10+
sealed interface RenderedSegment {
11+
data class Text(
12+
val styledText: SpannableString,
13+
val imageSpans: List<ImageSpan>,
14+
val needsJustify: Boolean,
15+
val lastElementMarginBottom: Float,
16+
) : RenderedSegment
17+
18+
data class Table(
19+
val node: MarkdownASTNode,
20+
) : RenderedSegment
21+
22+
data class Math(
23+
val latex: String,
24+
) : RenderedSegment
25+
}
26+
27+
object MarkdownSegmentRenderer {
28+
fun render(
29+
segments: List<MarkdownSegment>,
30+
style: StyleConfig,
31+
context: Context,
32+
onLinkPress: ((String) -> Unit)?,
33+
onLinkLongPress: ((String) -> Unit)?,
34+
): List<RenderedSegment> =
35+
segments.map { segment ->
36+
when (segment) {
37+
is MarkdownSegment.Text -> renderTextSegment(segment.nodes, style, context, onLinkPress, onLinkLongPress)
38+
is MarkdownSegment.Table -> RenderedSegment.Table(segment.node)
39+
is MarkdownSegment.Math -> RenderedSegment.Math(segment.latex)
40+
}
41+
}
42+
43+
private fun renderTextSegment(
44+
nodes: List<MarkdownASTNode>,
45+
style: StyleConfig,
46+
context: Context,
47+
onLinkPress: ((String) -> Unit)?,
48+
onLinkLongPress: ((String) -> Unit)?,
49+
): RenderedSegment.Text {
50+
val documentWrapper = MarkdownASTNode(type = MarkdownASTNode.NodeType.Document, children = nodes)
51+
val renderer = Renderer().apply { configure(style, context) }
52+
53+
return RenderedSegment.Text(
54+
styledText = renderer.renderDocument(documentWrapper, onLinkPress, onLinkLongPress),
55+
imageSpans = renderer.getCollectedImageSpans().toList(),
56+
needsJustify = style.needsJustify,
57+
lastElementMarginBottom = renderer.getLastElementMarginBottom(),
58+
)
59+
}
60+
}

0 commit comments

Comments
 (0)