react-native-enriched-markdown 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +479 -0
- package/ReactNativeEnrichedMarkdown.podspec +27 -0
- package/android/build.gradle +101 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +39 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +21 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +57 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1164 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.h +20 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/baseline-prof.txt +65 -0
- package/android/src/main/cpp/jni-adapter.cpp +203 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +153 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +119 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextPackage.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +165 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +89 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +105 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +52 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +28 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +58 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +99 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +66 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +95 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +85 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +44 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +136 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +320 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +180 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +75 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +809 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +365 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +181 -0
- package/android/src/main/jni/CMakeLists.txt +82 -0
- package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
- package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +33 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +49 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +25 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +19 -0
- package/cpp/md4c/md4c.c +6492 -0
- package/cpp/md4c/md4c.h +402 -0
- package/cpp/parser/MD4CParser.cpp +314 -0
- package/cpp/parser/MD4CParser.hpp +23 -0
- package/cpp/parser/MarkdownASTNode.hpp +49 -0
- package/ios/EnrichedMarkdownText.h +18 -0
- package/ios/EnrichedMarkdownText.mm +1074 -0
- package/ios/attachments/ImageAttachment.h +23 -0
- package/ios/attachments/ImageAttachment.m +185 -0
- package/ios/attachments/ThematicBreakAttachment.h +15 -0
- package/ios/attachments/ThematicBreakAttachment.m +33 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +57 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1164 -0
- package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
- package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
- package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
- package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
- package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
- package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
- package/ios/internals/EnrichedMarkdownTextState.h +24 -0
- package/ios/parser/MarkdownASTNode.h +33 -0
- package/ios/parser/MarkdownASTNode.m +32 -0
- package/ios/parser/MarkdownParser.h +8 -0
- package/ios/parser/MarkdownParser.mm +13 -0
- package/ios/parser/MarkdownParserBridge.mm +110 -0
- package/ios/renderer/AttributedRenderer.h +9 -0
- package/ios/renderer/AttributedRenderer.m +119 -0
- package/ios/renderer/BlockquoteRenderer.h +7 -0
- package/ios/renderer/BlockquoteRenderer.m +159 -0
- package/ios/renderer/CodeBlockRenderer.h +10 -0
- package/ios/renderer/CodeBlockRenderer.m +89 -0
- package/ios/renderer/CodeRenderer.h +10 -0
- package/ios/renderer/CodeRenderer.m +60 -0
- package/ios/renderer/EmphasisRenderer.h +6 -0
- package/ios/renderer/EmphasisRenderer.m +96 -0
- package/ios/renderer/HeadingRenderer.h +7 -0
- package/ios/renderer/HeadingRenderer.m +98 -0
- package/ios/renderer/ImageRenderer.h +12 -0
- package/ios/renderer/ImageRenderer.m +62 -0
- package/ios/renderer/LinkRenderer.h +7 -0
- package/ios/renderer/LinkRenderer.m +69 -0
- package/ios/renderer/ListItemRenderer.h +16 -0
- package/ios/renderer/ListItemRenderer.m +91 -0
- package/ios/renderer/ListRenderer.h +13 -0
- package/ios/renderer/ListRenderer.m +67 -0
- package/ios/renderer/NodeRenderer.h +8 -0
- package/ios/renderer/ParagraphRenderer.h +7 -0
- package/ios/renderer/ParagraphRenderer.m +69 -0
- package/ios/renderer/RenderContext.h +88 -0
- package/ios/renderer/RenderContext.m +248 -0
- package/ios/renderer/RendererFactory.h +12 -0
- package/ios/renderer/RendererFactory.m +110 -0
- package/ios/renderer/StrongRenderer.h +6 -0
- package/ios/renderer/StrongRenderer.m +83 -0
- package/ios/renderer/TextRenderer.h +6 -0
- package/ios/renderer/TextRenderer.m +16 -0
- package/ios/renderer/ThematicBreakRenderer.h +5 -0
- package/ios/renderer/ThematicBreakRenderer.m +53 -0
- package/ios/styles/StyleConfig.h +228 -0
- package/ios/styles/StyleConfig.mm +1467 -0
- package/ios/utils/BlockquoteBorder.h +20 -0
- package/ios/utils/BlockquoteBorder.m +92 -0
- package/ios/utils/CodeBackground.h +19 -0
- package/ios/utils/CodeBackground.m +191 -0
- package/ios/utils/CodeBlockBackground.h +17 -0
- package/ios/utils/CodeBlockBackground.m +87 -0
- package/ios/utils/EditMenuUtils.h +22 -0
- package/ios/utils/EditMenuUtils.m +118 -0
- package/ios/utils/FontUtils.h +20 -0
- package/ios/utils/FontUtils.m +13 -0
- package/ios/utils/HTMLGenerator.h +20 -0
- package/ios/utils/HTMLGenerator.m +779 -0
- package/ios/utils/LastElementUtils.h +53 -0
- package/ios/utils/ListMarkerDrawer.h +15 -0
- package/ios/utils/ListMarkerDrawer.m +127 -0
- package/ios/utils/MarkdownExtractor.h +17 -0
- package/ios/utils/MarkdownExtractor.m +295 -0
- package/ios/utils/ParagraphStyleUtils.h +13 -0
- package/ios/utils/ParagraphStyleUtils.m +56 -0
- package/ios/utils/PasteboardUtils.h +36 -0
- package/ios/utils/PasteboardUtils.m +134 -0
- package/ios/utils/RTFExportUtils.h +24 -0
- package/ios/utils/RTFExportUtils.m +297 -0
- package/ios/utils/RuntimeKeys.h +38 -0
- package/ios/utils/RuntimeKeys.m +11 -0
- package/ios/utils/TextViewLayoutManager.h +14 -0
- package/ios/utils/TextViewLayoutManager.mm +113 -0
- package/lib/module/EnrichedMarkdownText.js +34 -0
- package/lib/module/EnrichedMarkdownText.js.map +1 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +130 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalizeMarkdownStyle.js +340 -0
- package/lib/module/normalizeMarkdownStyle.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts +101 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +111 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
- package/package.json +186 -1
- package/react-native.config.js +13 -0
- package/src/EnrichedMarkdownText.tsx +152 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +130 -0
- package/src/index.tsx +7 -0
- package/src/normalizeMarkdownStyle.ts +377 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
import java.util.ArrayList
|
|
8
|
+
|
|
9
|
+
class EnrichedMarkdownTextPackage : ReactPackage {
|
|
10
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
11
|
+
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
|
|
12
|
+
viewManagers.add(EnrichedMarkdownTextManager())
|
|
13
|
+
return viewManagers
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = emptyList()
|
|
17
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import android.graphics.text.LineBreaker
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.text.StaticLayout
|
|
8
|
+
import android.text.TextPaint
|
|
9
|
+
import com.facebook.react.bridge.ReadableMap
|
|
10
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
11
|
+
import com.facebook.yoga.YogaMeasureMode
|
|
12
|
+
import com.facebook.yoga.YogaMeasureOutput
|
|
13
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
14
|
+
import kotlin.math.ceil
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Manages text measurements for ShadowNode layout.
|
|
18
|
+
* Initial estimate uses raw markdown; accurate measurement after rendering via store().
|
|
19
|
+
*/
|
|
20
|
+
object MeasurementStore {
|
|
21
|
+
private data class PaintParams(
|
|
22
|
+
val typeface: Typeface,
|
|
23
|
+
val fontSize: Float,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
private data class MeasurementParams(
|
|
27
|
+
val cachedWidth: Float,
|
|
28
|
+
val cachedSize: Long,
|
|
29
|
+
val spannable: CharSequence?,
|
|
30
|
+
val paintParams: PaintParams,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
private val data = ConcurrentHashMap<Int, MeasurementParams>()
|
|
34
|
+
|
|
35
|
+
private val measurePaint = TextPaint()
|
|
36
|
+
|
|
37
|
+
/** Updates measurement with rendered Spannable. Returns true if height changed. */
|
|
38
|
+
fun store(
|
|
39
|
+
id: Int,
|
|
40
|
+
spannable: CharSequence?,
|
|
41
|
+
paint: TextPaint,
|
|
42
|
+
): Boolean {
|
|
43
|
+
val cached = data[id]
|
|
44
|
+
val width = cached?.cachedWidth ?: 0f
|
|
45
|
+
val oldSize = cached?.cachedSize ?: 0L
|
|
46
|
+
val paintParams = PaintParams(paint.typeface ?: Typeface.DEFAULT, paint.textSize)
|
|
47
|
+
|
|
48
|
+
val newSize = measure(width, spannable, paint)
|
|
49
|
+
data[id] = MeasurementParams(width, newSize, spannable, paintParams)
|
|
50
|
+
return oldSize != newSize
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun release(id: Int) {
|
|
54
|
+
data.remove(id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Main entry point for ShadowNode measurement. */
|
|
58
|
+
fun getMeasureById(
|
|
59
|
+
context: Context,
|
|
60
|
+
id: Int?,
|
|
61
|
+
width: Float,
|
|
62
|
+
height: Float,
|
|
63
|
+
heightMode: YogaMeasureMode?,
|
|
64
|
+
props: ReadableMap?,
|
|
65
|
+
): Long {
|
|
66
|
+
val size = getMeasureByIdInternal(id, width, props)
|
|
67
|
+
val resultHeight = YogaMeasureOutput.getHeight(size)
|
|
68
|
+
|
|
69
|
+
if (heightMode === YogaMeasureMode.AT_MOST) {
|
|
70
|
+
val maxHeight = PixelUtil.toDIPFromPixel(height)
|
|
71
|
+
val finalHeight = resultHeight.coerceAtMost(maxHeight)
|
|
72
|
+
return YogaMeasureOutput.make(
|
|
73
|
+
YogaMeasureOutput.getWidth(size),
|
|
74
|
+
finalHeight,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return size
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun getMeasureByIdInternal(
|
|
82
|
+
id: Int?,
|
|
83
|
+
width: Float,
|
|
84
|
+
props: ReadableMap?,
|
|
85
|
+
): Long {
|
|
86
|
+
val safeId = id ?: return initialMeasure(null, width, props)
|
|
87
|
+
val cached = data[safeId] ?: return initialMeasure(safeId, width, props)
|
|
88
|
+
|
|
89
|
+
// Width changed or not yet measured - re-measure with cached content
|
|
90
|
+
if (cached.cachedWidth != width || cached.cachedSize == 0L) {
|
|
91
|
+
val newSize = measure(width, cached.spannable, cached.paintParams)
|
|
92
|
+
data[safeId] = MeasurementParams(width, newSize, cached.spannable, cached.paintParams)
|
|
93
|
+
return newSize
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return cached.cachedSize
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Fast estimate using raw markdown text. */
|
|
100
|
+
private fun initialMeasure(
|
|
101
|
+
id: Int?,
|
|
102
|
+
width: Float,
|
|
103
|
+
props: ReadableMap?,
|
|
104
|
+
): Long {
|
|
105
|
+
val markdown = props?.getString("markdown")?.ifEmpty { "I" } ?: "I"
|
|
106
|
+
val fontSize = getInitialFontSize(props)
|
|
107
|
+
val paintParams = PaintParams(Typeface.DEFAULT, fontSize)
|
|
108
|
+
|
|
109
|
+
val size = measure(width, markdown, paintParams)
|
|
110
|
+
|
|
111
|
+
if (id != null) {
|
|
112
|
+
data[id] = MeasurementParams(width, size, markdown, paintParams)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return size
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private fun getInitialFontSize(props: ReadableMap?): Float {
|
|
119
|
+
val styleMap = props?.getMap("markdownStyle")
|
|
120
|
+
val fontSize = styleMap?.getMap("paragraph")?.getDouble("fontSize")?.toFloat() ?: 16f
|
|
121
|
+
return ceil(PixelUtil.toPixelFromSP(fontSize))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private fun measure(
|
|
125
|
+
maxWidth: Float,
|
|
126
|
+
text: CharSequence?,
|
|
127
|
+
paintParams: PaintParams,
|
|
128
|
+
): Long {
|
|
129
|
+
measurePaint.reset()
|
|
130
|
+
measurePaint.typeface = paintParams.typeface
|
|
131
|
+
measurePaint.textSize = paintParams.fontSize
|
|
132
|
+
return measure(maxWidth, text, measurePaint)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private fun measure(
|
|
136
|
+
maxWidth: Float,
|
|
137
|
+
text: CharSequence?,
|
|
138
|
+
paint: TextPaint,
|
|
139
|
+
): Long {
|
|
140
|
+
val content = text ?: ""
|
|
141
|
+
val safeWidth = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
142
|
+
|
|
143
|
+
val builder =
|
|
144
|
+
StaticLayout.Builder
|
|
145
|
+
.obtain(content, 0, content.length, paint, safeWidth)
|
|
146
|
+
.setIncludePad(false)
|
|
147
|
+
.setLineSpacing(0f, 1f)
|
|
148
|
+
|
|
149
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
150
|
+
builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
154
|
+
builder.setUseLineSpacingFromFallbacks(true)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
val layout = builder.build()
|
|
158
|
+
val measuredHeight = layout.height.toFloat()
|
|
159
|
+
|
|
160
|
+
return YogaMeasureOutput.make(
|
|
161
|
+
PixelUtil.toDIPFromPixel(maxWidth),
|
|
162
|
+
PixelUtil.toDIPFromPixel(measuredHeight),
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
|
|
7
|
+
class LinkPressEvent(
|
|
8
|
+
surfaceId: Int,
|
|
9
|
+
viewId: Int,
|
|
10
|
+
private val url: String,
|
|
11
|
+
) : Event<LinkPressEvent>(surfaceId, viewId) {
|
|
12
|
+
override fun getEventName(): String = EVENT_NAME
|
|
13
|
+
|
|
14
|
+
override fun getEventData(): WritableMap {
|
|
15
|
+
val eventData: WritableMap = Arguments.createMap()
|
|
16
|
+
eventData.putString("url", url)
|
|
17
|
+
return eventData
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
const val EVENT_NAME: String = "onLinkPress"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.parser
|
|
2
|
+
|
|
3
|
+
data class MarkdownASTNode(
|
|
4
|
+
val type: NodeType,
|
|
5
|
+
val content: String = "",
|
|
6
|
+
val attributes: Map<String, String> = emptyMap(),
|
|
7
|
+
val children: List<MarkdownASTNode> = emptyList(),
|
|
8
|
+
) {
|
|
9
|
+
enum class NodeType {
|
|
10
|
+
Document,
|
|
11
|
+
Paragraph,
|
|
12
|
+
Text,
|
|
13
|
+
Link,
|
|
14
|
+
Heading,
|
|
15
|
+
LineBreak,
|
|
16
|
+
Strong,
|
|
17
|
+
Emphasis,
|
|
18
|
+
Code,
|
|
19
|
+
Image,
|
|
20
|
+
Blockquote,
|
|
21
|
+
UnorderedList,
|
|
22
|
+
OrderedList,
|
|
23
|
+
ListItem,
|
|
24
|
+
CodeBlock,
|
|
25
|
+
ThematicBreak,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun getAttribute(key: String): String? = attributes[key]
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.parser
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
class Parser {
|
|
6
|
+
companion object {
|
|
7
|
+
init {
|
|
8
|
+
try {
|
|
9
|
+
// Library name must follow react_codegen_<ComponentName> convention
|
|
10
|
+
// required by React Native Fabric's CMake build system.
|
|
11
|
+
// md4c parser is bundled in the same shared library to avoid
|
|
12
|
+
// multiple library loading and complex linking dependencies.
|
|
13
|
+
System.loadLibrary("react_codegen_EnrichedMarkdownTextSpec")
|
|
14
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
15
|
+
Log.e("MarkdownParser", "Failed to load native library", e)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@JvmStatic
|
|
20
|
+
private external fun nativeParseMarkdown(markdown: String): MarkdownASTNode?
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Shared parser instance. Parser is stateless and thread-safe, so it can be reused
|
|
24
|
+
* across all EnrichedMarkdownText instances to avoid unnecessary allocations.
|
|
25
|
+
*/
|
|
26
|
+
val shared: Parser = Parser()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fun parseMarkdown(markdown: String): MarkdownASTNode? {
|
|
30
|
+
if (markdown.isBlank()) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
val ast = nativeParseMarkdown(markdown)
|
|
36
|
+
|
|
37
|
+
if (ast != null) {
|
|
38
|
+
return ast
|
|
39
|
+
} else {
|
|
40
|
+
Log.w("MarkdownParser", "Native parser returned null")
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
} catch (e: Exception) {
|
|
44
|
+
Log.e("MarkdownParser", "MD4C parsing failed: ${e.message}", e)
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import com.swmansion.enriched.markdown.styles.BaseBlockStyle
|
|
4
|
+
import com.swmansion.enriched.markdown.styles.BlockquoteStyle
|
|
5
|
+
import com.swmansion.enriched.markdown.styles.CodeBlockStyle
|
|
6
|
+
import com.swmansion.enriched.markdown.styles.HeadingStyle
|
|
7
|
+
import com.swmansion.enriched.markdown.styles.ListStyle
|
|
8
|
+
import com.swmansion.enriched.markdown.styles.ParagraphStyle
|
|
9
|
+
|
|
10
|
+
enum class BlockType {
|
|
11
|
+
NONE,
|
|
12
|
+
PARAGRAPH,
|
|
13
|
+
HEADING,
|
|
14
|
+
BLOCKQUOTE,
|
|
15
|
+
UNORDERED_LIST,
|
|
16
|
+
ORDERED_LIST,
|
|
17
|
+
CODE_BLOCK,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
data class BlockStyle(
|
|
21
|
+
val fontSize: Float,
|
|
22
|
+
val fontFamily: String,
|
|
23
|
+
val fontWeight: String,
|
|
24
|
+
val color: Int,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
private class MutableBlockStyle {
|
|
28
|
+
var fontSize: Float = 0f
|
|
29
|
+
var fontFamily: String = ""
|
|
30
|
+
var fontWeight: String = ""
|
|
31
|
+
var color: Int = 0
|
|
32
|
+
var isDirty: Boolean = false
|
|
33
|
+
|
|
34
|
+
fun updateFrom(style: BaseBlockStyle) {
|
|
35
|
+
fontSize = style.fontSize
|
|
36
|
+
fontFamily = style.fontFamily
|
|
37
|
+
fontWeight = style.fontWeight
|
|
38
|
+
color = style.color
|
|
39
|
+
isDirty = true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fun toImmutable(): BlockStyle = BlockStyle(fontSize, fontFamily, fontWeight, color)
|
|
43
|
+
|
|
44
|
+
fun clear() {
|
|
45
|
+
isDirty = false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class BlockStyleContext {
|
|
50
|
+
var currentBlockType = BlockType.NONE
|
|
51
|
+
private set
|
|
52
|
+
|
|
53
|
+
private val mutableBlockStyle = MutableBlockStyle()
|
|
54
|
+
private var cachedBlockStyle: BlockStyle? = null
|
|
55
|
+
private var currentHeadingLevel = 0
|
|
56
|
+
|
|
57
|
+
var blockquoteDepth = 0
|
|
58
|
+
var listDepth = 0
|
|
59
|
+
var listType: ListType? = null
|
|
60
|
+
var listItemNumber = 0
|
|
61
|
+
|
|
62
|
+
// Optimization: ArrayDeque is more efficient for stack operations than MutableList
|
|
63
|
+
private val orderedListItemNumbers = ArrayDeque<Int>()
|
|
64
|
+
|
|
65
|
+
enum class ListType { UNORDERED, ORDERED }
|
|
66
|
+
|
|
67
|
+
private fun updateBlockStyle(
|
|
68
|
+
type: BlockType,
|
|
69
|
+
style: BaseBlockStyle,
|
|
70
|
+
headingLevel: Int = 0,
|
|
71
|
+
) {
|
|
72
|
+
currentBlockType = type
|
|
73
|
+
currentHeadingLevel = headingLevel
|
|
74
|
+
// Update mutable style in place - no allocation here
|
|
75
|
+
mutableBlockStyle.updateFrom(style)
|
|
76
|
+
// Invalidate cached immutable copy
|
|
77
|
+
cachedBlockStyle = null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Unified Setters
|
|
81
|
+
fun setParagraphStyle(style: ParagraphStyle) = updateBlockStyle(BlockType.PARAGRAPH, style)
|
|
82
|
+
|
|
83
|
+
fun setHeadingStyle(
|
|
84
|
+
style: HeadingStyle,
|
|
85
|
+
level: Int,
|
|
86
|
+
) = updateBlockStyle(BlockType.HEADING, style, level)
|
|
87
|
+
|
|
88
|
+
fun setBlockquoteStyle(style: BlockquoteStyle) = updateBlockStyle(BlockType.BLOCKQUOTE, style)
|
|
89
|
+
|
|
90
|
+
fun setUnorderedListStyle(style: ListStyle) {
|
|
91
|
+
listType = ListType.UNORDERED
|
|
92
|
+
updateBlockStyle(BlockType.UNORDERED_LIST, style)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fun setOrderedListStyle(style: ListStyle) {
|
|
96
|
+
listType = ListType.ORDERED
|
|
97
|
+
updateBlockStyle(BlockType.ORDERED_LIST, style)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun setCodeBlockStyle(style: CodeBlockStyle) = updateBlockStyle(BlockType.CODE_BLOCK, style)
|
|
101
|
+
|
|
102
|
+
// List State Management
|
|
103
|
+
fun isInsideBlockElement(): Boolean = blockquoteDepth > 0 || listDepth > 0
|
|
104
|
+
|
|
105
|
+
fun incrementListItemNumber() {
|
|
106
|
+
listItemNumber++
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fun resetListItemNumber() {
|
|
110
|
+
listItemNumber = 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Using ArrayDeque methods for clarity: addLast/removeLast
|
|
114
|
+
fun pushOrderedListItemNumber() {
|
|
115
|
+
orderedListItemNumbers.addLast(listItemNumber)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fun popOrderedListItemNumber() {
|
|
119
|
+
if (orderedListItemNumbers.isNotEmpty()) {
|
|
120
|
+
listItemNumber = orderedListItemNumbers.removeLast()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fun clearListStyle() {
|
|
125
|
+
// Only trigger full reset when we have completely exited all nested lists
|
|
126
|
+
if (listDepth == 0) {
|
|
127
|
+
reset()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun reset() {
|
|
132
|
+
clearBlockStyle()
|
|
133
|
+
listType = null
|
|
134
|
+
listItemNumber = 0
|
|
135
|
+
orderedListItemNumbers.clear()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun requireBlockStyle(): BlockStyle {
|
|
139
|
+
if (!mutableBlockStyle.isDirty) {
|
|
140
|
+
throw IllegalStateException(
|
|
141
|
+
"BlockStyle is null. Inline renderers must be used within a block context.",
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
// Create immutable copy only when needed, cache for reuse within same block
|
|
145
|
+
return cachedBlockStyle ?: mutableBlockStyle.toImmutable().also { cachedBlockStyle = it }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun clearBlockStyle() {
|
|
149
|
+
currentBlockType = BlockType.NONE
|
|
150
|
+
mutableBlockStyle.clear()
|
|
151
|
+
cachedBlockStyle = null
|
|
152
|
+
currentHeadingLevel = 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fun resetForNewRender() {
|
|
156
|
+
currentBlockType = BlockType.NONE
|
|
157
|
+
mutableBlockStyle.clear()
|
|
158
|
+
cachedBlockStyle = null
|
|
159
|
+
currentHeadingLevel = 0
|
|
160
|
+
blockquoteDepth = 0
|
|
161
|
+
listDepth = 0
|
|
162
|
+
listType = null
|
|
163
|
+
listItemNumber = 0
|
|
164
|
+
orderedListItemNumbers.clear()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.text.SpannableStringBuilder
|
|
4
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
5
|
+
import com.swmansion.enriched.markdown.spans.BlockquoteSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.spans.MarginBottomSpan
|
|
7
|
+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
8
|
+
import com.swmansion.enriched.markdown.utils.createLineHeightSpan
|
|
9
|
+
|
|
10
|
+
class BlockquoteRenderer(
|
|
11
|
+
private val config: RendererConfig,
|
|
12
|
+
) : NodeRenderer {
|
|
13
|
+
override fun render(
|
|
14
|
+
node: MarkdownASTNode,
|
|
15
|
+
builder: SpannableStringBuilder,
|
|
16
|
+
onLinkPress: ((String) -> Unit)?,
|
|
17
|
+
factory: RendererFactory,
|
|
18
|
+
) {
|
|
19
|
+
val start = builder.length
|
|
20
|
+
val style = config.style.blockquoteStyle
|
|
21
|
+
val context = factory.blockStyleContext
|
|
22
|
+
val depth = context.blockquoteDepth
|
|
23
|
+
|
|
24
|
+
// 1. Context management
|
|
25
|
+
context.blockquoteDepth = depth + 1
|
|
26
|
+
context.setBlockquoteStyle(style)
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
factory.renderChildren(node, builder, onLinkPress)
|
|
30
|
+
} finally {
|
|
31
|
+
context.clearBlockStyle()
|
|
32
|
+
context.blockquoteDepth = depth
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (builder.length == start) return
|
|
36
|
+
val end = builder.length
|
|
37
|
+
|
|
38
|
+
// 2. Identify Nested Ranges (Essential for excluding them from parent-level styles)
|
|
39
|
+
val nestedRanges =
|
|
40
|
+
builder
|
|
41
|
+
.getSpans(start, end, BlockquoteSpan::class.java)
|
|
42
|
+
.filter { it.depth == depth + 1 }
|
|
43
|
+
.map { builder.getSpanStart(it) to builder.getSpanEnd(it) }
|
|
44
|
+
.sortedBy { it.first }
|
|
45
|
+
|
|
46
|
+
// 3. Apply the Accent Bar Span (Must cover the full range for continuity)
|
|
47
|
+
builder.setSpan(
|
|
48
|
+
BlockquoteSpan(style, depth, factory.context, factory.styleCache),
|
|
49
|
+
start,
|
|
50
|
+
end,
|
|
51
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// 4. Fragmented Styling Logic
|
|
55
|
+
// We apply line height and margin specifically to the segments that are NOT nested quotes.
|
|
56
|
+
applySpansExcludingNested(builder, nestedRanges, start, end, createLineHeightSpan(style.lineHeight))
|
|
57
|
+
|
|
58
|
+
// 5. Root-level Spacing
|
|
59
|
+
if (depth == 0 && style.marginBottom > 0) {
|
|
60
|
+
val spacerLocation = builder.length
|
|
61
|
+
builder.append("\n") // Physical break
|
|
62
|
+
builder.setSpan(
|
|
63
|
+
MarginBottomSpan(style.marginBottom),
|
|
64
|
+
spacerLocation,
|
|
65
|
+
builder.length,
|
|
66
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private fun applySpansExcludingNested(
|
|
72
|
+
builder: SpannableStringBuilder,
|
|
73
|
+
nestedRanges: List<Pair<Int, Int>>,
|
|
74
|
+
start: Int,
|
|
75
|
+
end: Int,
|
|
76
|
+
span: Any, // Changed to Any to handle both LineHeight and MarginBottom spans
|
|
77
|
+
) {
|
|
78
|
+
var currentPos = start
|
|
79
|
+
for ((nestedStart, nestedEnd) in nestedRanges) {
|
|
80
|
+
if (currentPos < nestedStart) {
|
|
81
|
+
builder.setSpan(span, currentPos, nestedStart, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
|
|
82
|
+
}
|
|
83
|
+
currentPos = nestedEnd
|
|
84
|
+
}
|
|
85
|
+
if (currentPos < end) {
|
|
86
|
+
builder.setSpan(span, currentPos, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.graphics.Paint
|
|
4
|
+
import android.text.SpannableStringBuilder
|
|
5
|
+
import android.text.Spanned
|
|
6
|
+
import android.text.style.LineHeightSpan
|
|
7
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
8
|
+
import com.swmansion.enriched.markdown.spans.CodeBlockSpan
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.MarginBottomSpan
|
|
10
|
+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
11
|
+
|
|
12
|
+
class CodeBlockRenderer(
|
|
13
|
+
private val config: RendererConfig,
|
|
14
|
+
) : NodeRenderer {
|
|
15
|
+
override fun render(
|
|
16
|
+
node: MarkdownASTNode,
|
|
17
|
+
builder: SpannableStringBuilder,
|
|
18
|
+
onLinkPress: ((String) -> Unit)?,
|
|
19
|
+
factory: RendererFactory,
|
|
20
|
+
) {
|
|
21
|
+
val start = builder.length
|
|
22
|
+
val style = config.style.codeBlockStyle
|
|
23
|
+
val context = factory.blockStyleContext
|
|
24
|
+
|
|
25
|
+
// Set code block style in context for children to inherit
|
|
26
|
+
context.setCodeBlockStyle(style)
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Render children (code content)
|
|
30
|
+
factory.renderChildren(node, builder, onLinkPress)
|
|
31
|
+
} finally {
|
|
32
|
+
context.clearBlockStyle()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Safety check for empty code blocks
|
|
36
|
+
if (builder.length == start) return
|
|
37
|
+
|
|
38
|
+
val end = builder.length
|
|
39
|
+
val padding = style.padding.toInt()
|
|
40
|
+
|
|
41
|
+
// 1. Apply CodeBlockSpan (Handles Background, Borders, and Horizontal Padding)
|
|
42
|
+
// Matches the logic in the updated CodeBlockSpan for full-width support
|
|
43
|
+
builder.setSpan(
|
|
44
|
+
CodeBlockSpan(style, factory.context, factory.styleCache),
|
|
45
|
+
start,
|
|
46
|
+
end,
|
|
47
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// 2. Apply Boundary Vertical Padding
|
|
51
|
+
builder.setSpan(
|
|
52
|
+
CodeBlockBoundaryPaddingSpan(padding),
|
|
53
|
+
start,
|
|
54
|
+
end,
|
|
55
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// 3. Apply External Margin Bottom
|
|
59
|
+
if (style.marginBottom > 0) {
|
|
60
|
+
val marginStart = builder.length
|
|
61
|
+
builder.append("\n")
|
|
62
|
+
builder.setSpan(
|
|
63
|
+
MarginBottomSpan(style.marginBottom),
|
|
64
|
+
marginStart,
|
|
65
|
+
builder.length,
|
|
66
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Internal span to handle top/bottom padding by modifying font metrics.
|
|
73
|
+
*/
|
|
74
|
+
private class CodeBlockBoundaryPaddingSpan(
|
|
75
|
+
private val padding: Int,
|
|
76
|
+
) : LineHeightSpan {
|
|
77
|
+
override fun chooseHeight(
|
|
78
|
+
text: CharSequence,
|
|
79
|
+
startLine: Int,
|
|
80
|
+
endLine: Int,
|
|
81
|
+
spanstartv: Int,
|
|
82
|
+
v: Int,
|
|
83
|
+
fm: Paint.FontMetricsInt,
|
|
84
|
+
) {
|
|
85
|
+
if (text !is Spanned) return
|
|
86
|
+
|
|
87
|
+
val spanStart = text.getSpanStart(this)
|
|
88
|
+
val spanEnd = text.getSpanEnd(this)
|
|
89
|
+
|
|
90
|
+
// Apply top vertical padding to the first line fragment
|
|
91
|
+
if (startLine == spanStart) {
|
|
92
|
+
fm.ascent -= padding
|
|
93
|
+
fm.top -= padding
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Apply bottom vertical padding to the last line fragment
|
|
97
|
+
// Checks for both character index and trailing newlines to ensure a tight fit
|
|
98
|
+
val isLastLine = endLine == spanEnd || (spanEnd <= endLine && text[spanEnd - 1] == '\n')
|
|
99
|
+
if (isLastLine) {
|
|
100
|
+
fm.descent += padding
|
|
101
|
+
fm.bottom += padding
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.text.SpannableStringBuilder
|
|
4
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
5
|
+
import com.swmansion.enriched.markdown.spans.CodeBackgroundSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.spans.CodeSpan
|
|
7
|
+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
8
|
+
|
|
9
|
+
class CodeRenderer(
|
|
10
|
+
private val config: RendererConfig,
|
|
11
|
+
) : NodeRenderer {
|
|
12
|
+
override fun render(
|
|
13
|
+
node: MarkdownASTNode,
|
|
14
|
+
builder: SpannableStringBuilder,
|
|
15
|
+
onLinkPress: ((String) -> Unit)?,
|
|
16
|
+
factory: RendererFactory,
|
|
17
|
+
) {
|
|
18
|
+
if (node.children.isEmpty() || node.children.all { it.content.isEmpty() }) return
|
|
19
|
+
|
|
20
|
+
factory.renderWithSpan(builder, { node.children.forEach { builder.append(it.content) } }) { start, end, blockStyle ->
|
|
21
|
+
builder.setSpan(
|
|
22
|
+
CodeSpan(factory.styleCache, blockStyle),
|
|
23
|
+
start,
|
|
24
|
+
end,
|
|
25
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
26
|
+
)
|
|
27
|
+
builder.setSpan(
|
|
28
|
+
CodeBackgroundSpan(config.style),
|
|
29
|
+
start,
|
|
30
|
+
end,
|
|
31
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.text.SpannableStringBuilder
|
|
4
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
5
|
+
|
|
6
|
+
class DocumentRenderer : NodeRenderer {
|
|
7
|
+
override fun render(
|
|
8
|
+
node: MarkdownASTNode,
|
|
9
|
+
builder: SpannableStringBuilder,
|
|
10
|
+
onLinkPress: ((String) -> Unit)?,
|
|
11
|
+
factory: RendererFactory,
|
|
12
|
+
) {
|
|
13
|
+
factory.renderChildren(node, builder, onLinkPress)
|
|
14
|
+
}
|
|
15
|
+
}
|