react-native-enriched-markdown 0.1.1 → 0.2.0
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/README.md +80 -8
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +17 -2
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +6 -1
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +28 -3
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +225 -1
- package/android/src/main/cpp/jni-adapter.cpp +28 -11
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +132 -15
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +1 -16
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +67 -13
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +241 -21
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +17 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +13 -18
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +23 -24
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +18 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +22 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +16 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +23 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +24 -10
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +8 -17
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +19 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +3 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +32 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +22 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +23 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +58 -56
- package/android/src/main/jni/CMakeLists.txt +1 -13
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +0 -13
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +2 -14
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +3 -0
- package/cpp/parser/MD4CParser.cpp +21 -8
- package/cpp/parser/MD4CParser.hpp +5 -1
- package/cpp/parser/MarkdownASTNode.hpp +2 -0
- package/ios/EnrichedMarkdownText.mm +356 -29
- package/ios/attachments/{ImageAttachment.h → EnrichedMarkdownImageAttachment.h} +1 -1
- package/ios/attachments/{ImageAttachment.m → EnrichedMarkdownImageAttachment.m} +4 -4
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +28 -3
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +225 -1
- package/ios/parser/MarkdownASTNode.h +2 -0
- package/ios/parser/MarkdownParser.h +9 -0
- package/ios/parser/MarkdownParser.mm +31 -2
- package/ios/parser/MarkdownParserBridge.mm +13 -3
- package/ios/renderer/AttributedRenderer.h +2 -0
- package/ios/renderer/AttributedRenderer.m +52 -19
- package/ios/renderer/BlockquoteRenderer.m +7 -6
- package/ios/renderer/CodeBlockRenderer.m +9 -8
- package/ios/renderer/HeadingRenderer.m +31 -24
- package/ios/renderer/ImageRenderer.m +31 -10
- package/ios/renderer/ListItemRenderer.m +51 -39
- package/ios/renderer/ListRenderer.m +21 -18
- package/ios/renderer/ParagraphRenderer.m +27 -16
- package/ios/renderer/RenderContext.h +17 -0
- package/ios/renderer/RenderContext.m +66 -2
- package/ios/renderer/RendererFactory.m +6 -0
- package/ios/renderer/StrikethroughRenderer.h +6 -0
- package/ios/renderer/StrikethroughRenderer.m +40 -0
- package/ios/renderer/UnderlineRenderer.h +6 -0
- package/ios/renderer/UnderlineRenderer.m +39 -0
- package/ios/styles/StyleConfig.h +46 -0
- package/ios/styles/StyleConfig.mm +351 -12
- package/ios/utils/AccessibilityInfo.h +35 -0
- package/ios/utils/AccessibilityInfo.m +24 -0
- package/ios/utils/CodeBlockBackground.m +4 -9
- package/ios/utils/FontUtils.h +5 -0
- package/ios/utils/FontUtils.m +14 -0
- package/ios/utils/HTMLGenerator.m +21 -7
- package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
- package/ios/utils/MarkdownExtractor.m +18 -5
- package/ios/utils/ParagraphStyleUtils.h +10 -2
- package/ios/utils/ParagraphStyleUtils.m +57 -2
- package/ios/utils/PasteboardUtils.h +1 -1
- package/ios/utils/PasteboardUtils.m +3 -3
- package/lib/module/EnrichedMarkdownText.js +33 -2
- package/lib/module/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +83 -3
- package/lib/module/index.js +0 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +58 -14
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownText.d.ts +85 -3
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +75 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownText.tsx +133 -5
- package/src/EnrichedMarkdownTextNativeComponent.ts +83 -3
- package/src/index.tsx +5 -2
- package/src/normalizeMarkdownStyle.ts +46 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +0 -9
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +0 -25
|
@@ -6,18 +6,28 @@ import android.graphics.text.LineBreaker
|
|
|
6
6
|
import android.os.Build
|
|
7
7
|
import android.text.StaticLayout
|
|
8
8
|
import android.text.TextPaint
|
|
9
|
+
import android.util.Log
|
|
9
10
|
import com.facebook.react.bridge.ReadableMap
|
|
10
11
|
import com.facebook.react.uimanager.PixelUtil
|
|
11
12
|
import com.facebook.yoga.YogaMeasureMode
|
|
12
13
|
import com.facebook.yoga.YogaMeasureOutput
|
|
14
|
+
import com.swmansion.enriched.markdown.parser.Md4cFlags
|
|
15
|
+
import com.swmansion.enriched.markdown.parser.Parser
|
|
16
|
+
import com.swmansion.enriched.markdown.renderer.Renderer
|
|
17
|
+
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
18
|
+
import com.swmansion.enriched.markdown.utils.getBooleanOrDefault
|
|
19
|
+
import com.swmansion.enriched.markdown.utils.getMapOrNull
|
|
20
|
+
import com.swmansion.enriched.markdown.utils.getStringOrDefault
|
|
13
21
|
import java.util.concurrent.ConcurrentHashMap
|
|
14
22
|
import kotlin.math.ceil
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Manages text measurements for ShadowNode layout.
|
|
18
|
-
*
|
|
26
|
+
* Parses and renders markdown to Spannable at measure time for accurate height calculation.
|
|
19
27
|
*/
|
|
20
28
|
object MeasurementStore {
|
|
29
|
+
private const val TAG = "MeasurementStore"
|
|
30
|
+
|
|
21
31
|
private data class PaintParams(
|
|
22
32
|
val typeface: Typeface,
|
|
23
33
|
val fontSize: Float,
|
|
@@ -28,11 +38,41 @@ object MeasurementStore {
|
|
|
28
38
|
val cachedSize: Long,
|
|
29
39
|
val spannable: CharSequence?,
|
|
30
40
|
val paintParams: PaintParams,
|
|
41
|
+
val markdownHash: Int,
|
|
31
42
|
)
|
|
32
43
|
|
|
33
44
|
private val data = ConcurrentHashMap<Int, MeasurementParams>()
|
|
34
45
|
|
|
46
|
+
// Store font scaling settings per view ID
|
|
47
|
+
private data class FontScalingSettings(
|
|
48
|
+
val allowFontScaling: Boolean = true,
|
|
49
|
+
val maxFontSizeMultiplier: Float = 0f,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
private val fontScalingSettings = ConcurrentHashMap<Int, FontScalingSettings>()
|
|
53
|
+
|
|
54
|
+
private fun resolveFontScalingSettings(
|
|
55
|
+
viewId: Int?,
|
|
56
|
+
props: ReadableMap?,
|
|
57
|
+
): FontScalingSettings {
|
|
58
|
+
val stored = viewId?.let { fontScalingSettings[it] }
|
|
59
|
+
return FontScalingSettings(
|
|
60
|
+
allowFontScaling =
|
|
61
|
+
props?.takeIf { it.hasKey("allowFontScaling") }?.getBoolean("allowFontScaling")
|
|
62
|
+
?: stored?.allowFontScaling
|
|
63
|
+
?: true,
|
|
64
|
+
maxFontSizeMultiplier =
|
|
65
|
+
props?.takeIf { it.hasKey("maxFontSizeMultiplier") }?.getDouble("maxFontSizeMultiplier")?.toFloat()
|
|
66
|
+
?: stored?.maxFontSizeMultiplier
|
|
67
|
+
?: 0f,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
private val measurePaint = TextPaint()
|
|
72
|
+
private val measureRenderer = Renderer()
|
|
73
|
+
|
|
74
|
+
@Volatile
|
|
75
|
+
private var lastKnownFontScale: Float = 1.0f
|
|
36
76
|
|
|
37
77
|
/** Updates measurement with rendered Spannable. Returns true if height changed. */
|
|
38
78
|
fun store(
|
|
@@ -43,10 +83,11 @@ object MeasurementStore {
|
|
|
43
83
|
val cached = data[id]
|
|
44
84
|
val width = cached?.cachedWidth ?: 0f
|
|
45
85
|
val oldSize = cached?.cachedSize ?: 0L
|
|
86
|
+
val existingHash = cached?.markdownHash ?: 0
|
|
46
87
|
val paintParams = PaintParams(paint.typeface ?: Typeface.DEFAULT, paint.textSize)
|
|
47
88
|
|
|
48
89
|
val newSize = measure(width, spannable, paint)
|
|
49
|
-
data[id] = MeasurementParams(width, newSize, spannable, paintParams)
|
|
90
|
+
data[id] = MeasurementParams(width, newSize, spannable, paintParams, existingHash)
|
|
50
91
|
return oldSize != newSize
|
|
51
92
|
}
|
|
52
93
|
|
|
@@ -63,7 +104,13 @@ object MeasurementStore {
|
|
|
63
104
|
heightMode: YogaMeasureMode?,
|
|
64
105
|
props: ReadableMap?,
|
|
65
106
|
): Long {
|
|
66
|
-
|
|
107
|
+
// Early exit for empty markdown
|
|
108
|
+
val markdown = props.getStringOrDefault("markdown", "")
|
|
109
|
+
if (markdown.isEmpty()) {
|
|
110
|
+
return YogaMeasureOutput.make(PixelUtil.toDIPFromPixel(width), 0f)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
val size = getMeasureByIdInternal(context, id, width, props)
|
|
67
114
|
val resultHeight = YogaMeasureOutput.getHeight(size)
|
|
68
115
|
|
|
69
116
|
if (heightMode === YogaMeasureMode.AT_MOST) {
|
|
@@ -78,47 +125,178 @@ object MeasurementStore {
|
|
|
78
125
|
return size
|
|
79
126
|
}
|
|
80
127
|
|
|
128
|
+
fun updateFontScalingSettings(
|
|
129
|
+
viewId: Int,
|
|
130
|
+
allowFontScaling: Boolean,
|
|
131
|
+
maxFontSizeMultiplier: Float,
|
|
132
|
+
) {
|
|
133
|
+
fontScalingSettings[viewId] = FontScalingSettings(allowFontScaling, maxFontSizeMultiplier)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fun clearFontScalingSettings(viewId: Int) {
|
|
137
|
+
fontScalingSettings.remove(viewId)
|
|
138
|
+
}
|
|
139
|
+
|
|
81
140
|
private fun getMeasureByIdInternal(
|
|
141
|
+
context: Context,
|
|
82
142
|
id: Int?,
|
|
83
143
|
width: Float,
|
|
84
144
|
props: ReadableMap?,
|
|
85
145
|
): Long {
|
|
86
|
-
val
|
|
87
|
-
val cached = data[safeId] ?: return initialMeasure(safeId, width, props)
|
|
146
|
+
val (allowFontScaling, maxFontSizeMultiplier) = resolveFontScalingSettings(id, props)
|
|
88
147
|
|
|
89
|
-
|
|
90
|
-
|
|
148
|
+
val fontScale = checkAndUpdateFontScale(context, allowFontScaling, maxFontSizeMultiplier)
|
|
149
|
+
|
|
150
|
+
val safeId = id ?: return measureAndCache(context, null, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
151
|
+
val cached = data[safeId] ?: return measureAndCache(context, safeId, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
152
|
+
|
|
153
|
+
val currentHash = computePropsHash(props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
154
|
+
|
|
155
|
+
if (cached.markdownHash != currentHash) {
|
|
156
|
+
return measureAndCache(context, safeId, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Width changed - re-measure with cached spannable
|
|
160
|
+
if (cached.cachedWidth != width) {
|
|
91
161
|
val newSize = measure(width, cached.spannable, cached.paintParams)
|
|
92
|
-
data[safeId] =
|
|
162
|
+
data[safeId] = cached.copy(cachedWidth = width, cachedSize = newSize)
|
|
93
163
|
return newSize
|
|
94
164
|
}
|
|
95
165
|
|
|
96
166
|
return cached.cachedSize
|
|
97
167
|
}
|
|
98
168
|
|
|
99
|
-
|
|
100
|
-
|
|
169
|
+
private fun computePropsHash(
|
|
170
|
+
props: ReadableMap?,
|
|
171
|
+
allowFontScaling: Boolean,
|
|
172
|
+
fontScale: Float,
|
|
173
|
+
maxFontSizeMultiplier: Float,
|
|
174
|
+
): Int {
|
|
175
|
+
val markdown = props.getStringOrDefault("markdown", "")
|
|
176
|
+
val styleMap = props.getMapOrNull("markdownStyle")
|
|
177
|
+
val md4cFlagsMap = props.getMapOrNull("md4cFlags")
|
|
178
|
+
val allowTrailingMargin = props.getBooleanOrDefault("allowTrailingMargin", false)
|
|
179
|
+
var result = markdown.hashCode()
|
|
180
|
+
result = 31 * result + (styleMap?.hashCode() ?: 0)
|
|
181
|
+
result = 31 * result + (md4cFlagsMap?.hashCode() ?: 0)
|
|
182
|
+
result = 31 * result + fontScale.toBits()
|
|
183
|
+
result = 31 * result + allowFontScaling.hashCode()
|
|
184
|
+
result = 31 * result + maxFontSizeMultiplier.toBits()
|
|
185
|
+
result = 31 * result + allowTrailingMargin.hashCode()
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun checkAndUpdateFontScale(
|
|
190
|
+
context: Context,
|
|
191
|
+
allowFontScaling: Boolean,
|
|
192
|
+
maxFontSizeMultiplier: Float,
|
|
193
|
+
): Float {
|
|
194
|
+
if (!allowFontScaling) {
|
|
195
|
+
// Clear cache if we switched from scaling to non-scaling
|
|
196
|
+
if (lastKnownFontScale != 1.0f) {
|
|
197
|
+
lastKnownFontScale = 1.0f
|
|
198
|
+
data.clear()
|
|
199
|
+
}
|
|
200
|
+
return 1.0f
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
var currentFontScale = context.resources.configuration.fontScale
|
|
204
|
+
|
|
205
|
+
if (maxFontSizeMultiplier >= 1.0f && currentFontScale > maxFontSizeMultiplier) {
|
|
206
|
+
currentFontScale = maxFontSizeMultiplier
|
|
207
|
+
}
|
|
208
|
+
if (currentFontScale != lastKnownFontScale) {
|
|
209
|
+
lastKnownFontScale = currentFontScale
|
|
210
|
+
data.clear()
|
|
211
|
+
}
|
|
212
|
+
return currentFontScale
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private fun measureAndCache(
|
|
216
|
+
context: Context,
|
|
101
217
|
id: Int?,
|
|
102
218
|
width: Float,
|
|
103
219
|
props: ReadableMap?,
|
|
220
|
+
allowFontScaling: Boolean,
|
|
221
|
+
fontScale: Float,
|
|
222
|
+
maxFontSizeMultiplier: Float,
|
|
104
223
|
): Long {
|
|
105
|
-
|
|
106
|
-
val
|
|
107
|
-
val
|
|
224
|
+
// 1. Extract Props & Setup
|
|
225
|
+
val markdown = props.getStringOrDefault("markdown", "")
|
|
226
|
+
val styleMap = props.getMapOrNull("markdownStyle")
|
|
227
|
+
val md4cFlags = Md4cFlags(underline = props.getMapOrNull("md4cFlags").getBooleanOrDefault("underline", false))
|
|
228
|
+
|
|
229
|
+
val fontSize = getInitialFontSize(styleMap, context, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
230
|
+
val propsHash = computePropsHash(props, allowFontScaling, fontScale, maxFontSizeMultiplier)
|
|
231
|
+
|
|
232
|
+
// 2. Render & Measure
|
|
233
|
+
val spannable = tryRenderMarkdown(markdown, styleMap, context, md4cFlags, allowFontScaling, maxFontSizeMultiplier)
|
|
234
|
+
val textToMeasure = spannable ?: markdown
|
|
235
|
+
val (size, _) = measureWithLayout(width, textToMeasure, measurePaint)
|
|
108
236
|
|
|
109
|
-
|
|
237
|
+
// 3. Calculate Margin
|
|
238
|
+
val allowTrailingMargin = props.getBooleanOrDefault("allowTrailingMargin", false)
|
|
239
|
+
val marginBottom =
|
|
240
|
+
if (allowTrailingMargin && spannable != null) {
|
|
241
|
+
PixelUtil.toDIPFromPixel(measureRenderer.getLastElementMarginBottom())
|
|
242
|
+
} else {
|
|
243
|
+
0f
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 4. Finalize Height
|
|
247
|
+
val currentWidth = YogaMeasureOutput.getWidth(size)
|
|
248
|
+
val currentHeight = YogaMeasureOutput.getHeight(size)
|
|
249
|
+
val adjustedSize = YogaMeasureOutput.make(currentWidth, currentHeight + marginBottom)
|
|
110
250
|
|
|
111
251
|
if (id != null) {
|
|
112
|
-
data[id] = MeasurementParams(width,
|
|
252
|
+
data[id] = MeasurementParams(width, adjustedSize, textToMeasure, PaintParams(Typeface.DEFAULT, fontSize), propsHash)
|
|
113
253
|
}
|
|
114
254
|
|
|
115
|
-
return
|
|
255
|
+
return adjustedSize
|
|
116
256
|
}
|
|
117
257
|
|
|
118
|
-
private fun
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
258
|
+
private fun tryRenderMarkdown(
|
|
259
|
+
markdown: String,
|
|
260
|
+
styleMap: ReadableMap?,
|
|
261
|
+
context: Context,
|
|
262
|
+
md4cFlags: Md4cFlags,
|
|
263
|
+
allowFontScaling: Boolean,
|
|
264
|
+
maxFontSizeMultiplier: Float,
|
|
265
|
+
): CharSequence? {
|
|
266
|
+
if (styleMap == null) return null
|
|
267
|
+
|
|
268
|
+
return try {
|
|
269
|
+
val ast = Parser.shared.parseMarkdown(markdown, md4cFlags) ?: return null
|
|
270
|
+
val style = StyleConfig(styleMap, context, allowFontScaling, maxFontSizeMultiplier)
|
|
271
|
+
measureRenderer.configure(style, context)
|
|
272
|
+
measureRenderer.renderDocument(ast, null)
|
|
273
|
+
} catch (e: Exception) {
|
|
274
|
+
Log.w(TAG, "Failed to render markdown for measurement, falling back to raw text", e)
|
|
275
|
+
null
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private fun getInitialFontSize(
|
|
280
|
+
styleMap: ReadableMap?,
|
|
281
|
+
context: Context,
|
|
282
|
+
allowFontScaling: Boolean,
|
|
283
|
+
fontScale: Float,
|
|
284
|
+
maxFontSizeMultiplier: Float,
|
|
285
|
+
): Float {
|
|
286
|
+
val fontSizeSp = styleMap?.getMap("paragraph")?.getDouble("fontSize")?.toFloat() ?: 16f
|
|
287
|
+
val density = context.resources.displayMetrics.density
|
|
288
|
+
|
|
289
|
+
if (!allowFontScaling) {
|
|
290
|
+
return ceil(fontSizeSp * density)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
val cappedFontScale =
|
|
294
|
+
if (maxFontSizeMultiplier >= 1.0f && fontScale > maxFontSizeMultiplier) {
|
|
295
|
+
maxFontSizeMultiplier
|
|
296
|
+
} else {
|
|
297
|
+
fontScale
|
|
298
|
+
}
|
|
299
|
+
return ceil(fontSizeSp * cappedFontScale * density)
|
|
122
300
|
}
|
|
123
301
|
|
|
124
302
|
private fun measure(
|
|
@@ -157,9 +335,51 @@ object MeasurementStore {
|
|
|
157
335
|
val layout = builder.build()
|
|
158
336
|
val measuredHeight = layout.height.toFloat()
|
|
159
337
|
|
|
338
|
+
// Calculate actual content width (widest line)
|
|
339
|
+
val measuredWidth = (0 until layout.lineCount).maxOfOrNull { layout.getLineWidth(it) } ?: 0f
|
|
340
|
+
|
|
160
341
|
return YogaMeasureOutput.make(
|
|
161
|
-
PixelUtil.toDIPFromPixel(
|
|
342
|
+
PixelUtil.toDIPFromPixel(ceil(measuredWidth)),
|
|
162
343
|
PixelUtil.toDIPFromPixel(measuredHeight),
|
|
163
344
|
)
|
|
164
345
|
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Measures text and returns both the size and the layout for calculating last line descent.
|
|
349
|
+
*/
|
|
350
|
+
private fun measureWithLayout(
|
|
351
|
+
maxWidth: Float,
|
|
352
|
+
text: CharSequence?,
|
|
353
|
+
paint: TextPaint,
|
|
354
|
+
): Pair<Long, StaticLayout> {
|
|
355
|
+
val content = text ?: ""
|
|
356
|
+
val widthPx = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
357
|
+
|
|
358
|
+
val layout =
|
|
359
|
+
StaticLayout.Builder
|
|
360
|
+
.obtain(content, 0, content.length, paint, widthPx)
|
|
361
|
+
.setIncludePad(false)
|
|
362
|
+
.setLineSpacing(0f, 1f)
|
|
363
|
+
.apply {
|
|
364
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
365
|
+
setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
|
|
366
|
+
}
|
|
367
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
368
|
+
setUseLineSpacingFromFallbacks(true)
|
|
369
|
+
}
|
|
370
|
+
}.build()
|
|
371
|
+
|
|
372
|
+
// Find the widest line to get the actual content width
|
|
373
|
+
val maxLineWidth =
|
|
374
|
+
(0 until layout.lineCount)
|
|
375
|
+
.maxOfOrNull { layout.getLineWidth(it) } ?: 0f
|
|
376
|
+
|
|
377
|
+
val size =
|
|
378
|
+
YogaMeasureOutput.make(
|
|
379
|
+
PixelUtil.toDIPFromPixel(ceil(maxLineWidth)),
|
|
380
|
+
PixelUtil.toDIPFromPixel(layout.height.toFloat()),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return size to layout
|
|
384
|
+
}
|
|
165
385
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.accessibility
|
|
2
|
+
|
|
3
|
+
import android.graphics.Rect
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import android.text.Spanned
|
|
6
|
+
import android.widget.TextView
|
|
7
|
+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
|
8
|
+
import androidx.customview.widget.ExploreByTouchHelper
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.BaseListSpan
|
|
10
|
+
import com.swmansion.enriched.markdown.spans.HeadingSpan
|
|
11
|
+
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
12
|
+
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
13
|
+
import com.swmansion.enriched.markdown.spans.OrderedListSpan
|
|
14
|
+
import com.swmansion.enriched.markdown.spans.UnorderedListSpan
|
|
15
|
+
|
|
16
|
+
class MarkdownAccessibilityHelper(
|
|
17
|
+
private val textView: TextView,
|
|
18
|
+
) : ExploreByTouchHelper(textView) {
|
|
19
|
+
private var accessibilityItems: List<AccessibilityItem> = emptyList()
|
|
20
|
+
private var needsRebuild = false
|
|
21
|
+
private var lastLayoutHashCode = 0
|
|
22
|
+
|
|
23
|
+
data class AccessibilityItem(
|
|
24
|
+
val id: Int,
|
|
25
|
+
val text: String,
|
|
26
|
+
val start: Int,
|
|
27
|
+
val end: Int,
|
|
28
|
+
val headingLevel: Int = 0,
|
|
29
|
+
val linkUrl: String? = null,
|
|
30
|
+
val listInfo: ListItemInfo? = null,
|
|
31
|
+
val imageAltText: String? = null,
|
|
32
|
+
) {
|
|
33
|
+
val isHeading get() = headingLevel > 0
|
|
34
|
+
val isLink get() = linkUrl != null
|
|
35
|
+
val isListItem get() = listInfo != null
|
|
36
|
+
val isImage get() = imageAltText != null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
data class ListItemInfo(
|
|
40
|
+
val isOrdered: Boolean,
|
|
41
|
+
val itemNumber: Int,
|
|
42
|
+
val depth: Int,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
private data class SpanRange(
|
|
46
|
+
val start: Int,
|
|
47
|
+
val end: Int,
|
|
48
|
+
val headingLevel: Int = 0,
|
|
49
|
+
val linkUrl: String? = null,
|
|
50
|
+
val imageAltText: String? = null,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
fun invalidateAccessibilityItems() {
|
|
54
|
+
needsRebuild = true
|
|
55
|
+
rebuildIfNeeded()
|
|
56
|
+
invalidateRoot()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private fun rebuildIfNeeded() {
|
|
60
|
+
val layout = textView.layout ?: return
|
|
61
|
+
if (needsRebuild || lastLayoutHashCode != layout.hashCode()) {
|
|
62
|
+
accessibilityItems = buildAccessibilityItems()
|
|
63
|
+
needsRebuild = false
|
|
64
|
+
lastLayoutHashCode = layout.hashCode()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun buildAccessibilityItems(): List<AccessibilityItem> {
|
|
69
|
+
val spanned = textView.text as? Spanned ?: return emptyList()
|
|
70
|
+
if (spanned.isEmpty()) return emptyList()
|
|
71
|
+
|
|
72
|
+
val items = mutableListOf<AccessibilityItem>()
|
|
73
|
+
var nextId = 0
|
|
74
|
+
|
|
75
|
+
// Consolidated span collection using functional mapping
|
|
76
|
+
val semanticSpans =
|
|
77
|
+
(
|
|
78
|
+
spanned.getSpans(0, spanned.length, HeadingSpan::class.java).map {
|
|
79
|
+
SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), headingLevel = it.level)
|
|
80
|
+
} +
|
|
81
|
+
spanned.getSpans(0, spanned.length, LinkSpan::class.java).map {
|
|
82
|
+
SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), linkUrl = it.url)
|
|
83
|
+
} +
|
|
84
|
+
spanned.getSpans(0, spanned.length, ImageSpan::class.java).map {
|
|
85
|
+
SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), imageAltText = it.altText)
|
|
86
|
+
}
|
|
87
|
+
).sortedBy { it.start }
|
|
88
|
+
|
|
89
|
+
var currentPos = 0
|
|
90
|
+
for (span in semanticSpans) {
|
|
91
|
+
if (span.start < currentPos) continue
|
|
92
|
+
|
|
93
|
+
if (currentPos < span.start) {
|
|
94
|
+
nextId = addTextSegments(items, spanned, currentPos, span.start, nextId)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
val content = span.imageAltText?.ifEmpty { "Image" } ?: spanned.substring(span.start, span.end).trim()
|
|
98
|
+
|
|
99
|
+
if (content.isNotEmpty()) {
|
|
100
|
+
val listContext =
|
|
101
|
+
if (span.headingLevel > 0 || span.imageAltText != null) {
|
|
102
|
+
null
|
|
103
|
+
} else {
|
|
104
|
+
getListInfoAt(spanned, span.start, span.linkUrl == null)
|
|
105
|
+
}
|
|
106
|
+
items.add(
|
|
107
|
+
AccessibilityItem(
|
|
108
|
+
nextId++,
|
|
109
|
+
content,
|
|
110
|
+
span.start,
|
|
111
|
+
span.end,
|
|
112
|
+
span.headingLevel,
|
|
113
|
+
span.linkUrl,
|
|
114
|
+
listContext,
|
|
115
|
+
span.imageAltText,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
currentPos = span.end
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (currentPos < spanned.length) addTextSegments(items, spanned, currentPos, spanned.length, nextId)
|
|
123
|
+
return items.ifEmpty { listOf(AccessibilityItem(0, spanned.toString().trim(), 0, spanned.length)) }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fun getListInfoAt(
|
|
127
|
+
spanned: Spanned,
|
|
128
|
+
position: Int,
|
|
129
|
+
requireStart: Boolean,
|
|
130
|
+
): ListItemInfo? {
|
|
131
|
+
val deepest = spanned.getSpans(position, position + 1, BaseListSpan::class.java).maxByOrNull { it.depth } ?: return null
|
|
132
|
+
if (requireStart) {
|
|
133
|
+
val start = spanned.getSpanStart(deepest)
|
|
134
|
+
val firstChar = (start until minOf(start + 10, spanned.length)).firstOrNull { !spanned[it].isWhitespace() } ?: start
|
|
135
|
+
if (position > firstChar + 1) return null
|
|
136
|
+
}
|
|
137
|
+
return ListItemInfo(deepest is OrderedListSpan, (deepest as? OrderedListSpan)?.itemNumber ?: 0, deepest.depth)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun addTextSegments(
|
|
141
|
+
items: MutableList<AccessibilityItem>,
|
|
142
|
+
spanned: Spanned,
|
|
143
|
+
start: Int,
|
|
144
|
+
end: Int,
|
|
145
|
+
startId: Int,
|
|
146
|
+
): Int {
|
|
147
|
+
var cid = startId
|
|
148
|
+
val layout = textView.layout ?: return cid
|
|
149
|
+
for (line in layout.getLineForOffset(start)..layout.getLineForOffset(end)) {
|
|
150
|
+
val s = maxOf(start, layout.getLineStart(line))
|
|
151
|
+
val e = minOf(end, layout.getLineEnd(line))
|
|
152
|
+
if (s >= e) continue
|
|
153
|
+
|
|
154
|
+
val raw = spanned.substring(s, e)
|
|
155
|
+
val first = raw.indexOfFirst { !it.isWhitespace() }
|
|
156
|
+
if (first != -1) {
|
|
157
|
+
val last = raw.indexOfLast { !it.isWhitespace() }
|
|
158
|
+
val absoluteStart = s + first
|
|
159
|
+
items.add(
|
|
160
|
+
AccessibilityItem(cid++, raw.trim(), absoluteStart, s + last + 1, listInfo = getListInfoAt(spanned, absoluteStart, true)),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return cid
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override fun getVirtualViewAt(
|
|
168
|
+
x: Float,
|
|
169
|
+
y: Float,
|
|
170
|
+
): Int {
|
|
171
|
+
rebuildIfNeeded()
|
|
172
|
+
val offset = getOffsetForPosition(x, y)
|
|
173
|
+
return accessibilityItems
|
|
174
|
+
.filter { offset in it.start until it.end }
|
|
175
|
+
.minByOrNull {
|
|
176
|
+
when {
|
|
177
|
+
it.isLink -> 0
|
|
178
|
+
it.isImage -> 1
|
|
179
|
+
it.isHeading -> 2
|
|
180
|
+
it.isListItem -> 3
|
|
181
|
+
else -> 4
|
|
182
|
+
}
|
|
183
|
+
}?.id ?: HOST_ID
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
override fun getVisibleVirtualViews(ids: MutableList<Int>) {
|
|
187
|
+
rebuildIfNeeded()
|
|
188
|
+
accessibilityItems.forEach { ids.add(it.id) }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
override fun onPopulateNodeForVirtualView(
|
|
192
|
+
id: Int,
|
|
193
|
+
node: AccessibilityNodeInfoCompat,
|
|
194
|
+
) {
|
|
195
|
+
val item = accessibilityItems.find { it.id == id } ?: return
|
|
196
|
+
node.apply {
|
|
197
|
+
text = item.text
|
|
198
|
+
contentDescription = item.text
|
|
199
|
+
isFocusable = true
|
|
200
|
+
isScreenReaderFocusable = true
|
|
201
|
+
setBoundsInParent(getBoundsForRange(item.start, item.end))
|
|
202
|
+
|
|
203
|
+
item.listInfo?.let { info ->
|
|
204
|
+
setCollectionItemInfo(
|
|
205
|
+
AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(info.itemNumber - 1, 1, 0, 1, false, false),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
val prefix = if (item.listInfo?.depth ?: 0 > 0) "nested " else ""
|
|
210
|
+
val listText = if (item.listInfo?.isOrdered == true) "list item ${item.listInfo.itemNumber}" else "bullet point"
|
|
211
|
+
|
|
212
|
+
when {
|
|
213
|
+
item.isHeading -> {
|
|
214
|
+
isHeading = true
|
|
215
|
+
contentDescription = "${item.text}, heading level ${item.headingLevel}"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
item.isImage -> {
|
|
219
|
+
roleDescription = "image"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
item.isLink -> {
|
|
223
|
+
isClickable = true
|
|
224
|
+
addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
|
|
225
|
+
roleDescription = item.listInfo?.let { "link, $prefix$listText" } ?: "link"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
item.isListItem -> {
|
|
229
|
+
roleDescription = "$prefix$listText"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
override fun onPerformActionForVirtualView(
|
|
236
|
+
id: Int,
|
|
237
|
+
action: Int,
|
|
238
|
+
args: Bundle?,
|
|
239
|
+
): Boolean {
|
|
240
|
+
val item = accessibilityItems.find { it.id == id } ?: return false
|
|
241
|
+
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && item.isLink) {
|
|
242
|
+
(textView.text as? Spanned)?.getSpans(item.start, item.end, LinkSpan::class.java)?.firstOrNull()?.onClick(textView)
|
|
243
|
+
?: return false
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private fun getOffsetForPosition(
|
|
250
|
+
x: Float,
|
|
251
|
+
y: Float,
|
|
252
|
+
): Int {
|
|
253
|
+
val layout = textView.layout ?: return 0
|
|
254
|
+
return layout.getOffsetForHorizontal(layout.getLineForVertical(y.toInt()).coerceIn(0, layout.lineCount - 1), x)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private fun getBoundsForRange(
|
|
258
|
+
start: Int,
|
|
259
|
+
end: Int,
|
|
260
|
+
): Rect {
|
|
261
|
+
val layout = textView.layout ?: return Rect()
|
|
262
|
+
val line = layout.getLineForOffset(start)
|
|
263
|
+
val left = layout.getPrimaryHorizontal(start).toInt() + textView.paddingLeft
|
|
264
|
+
val right =
|
|
265
|
+
if (layout.getPrimaryHorizontal(end) <=
|
|
266
|
+
layout.getPrimaryHorizontal(start)
|
|
267
|
+
) {
|
|
268
|
+
layout.getLineRight(line).toInt() + textView.paddingLeft
|
|
269
|
+
} else {
|
|
270
|
+
layout.getPrimaryHorizontal(end).toInt() + textView.paddingLeft
|
|
271
|
+
}
|
|
272
|
+
return Rect(
|
|
273
|
+
left,
|
|
274
|
+
layout.getLineTop(line) + textView.paddingTop,
|
|
275
|
+
right,
|
|
276
|
+
layout.getLineBottom(layout.getLineForOffset(end)) + textView.paddingTop,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -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 LinkLongPressEvent(
|
|
8
|
+
surfaceId: Int,
|
|
9
|
+
viewId: Int,
|
|
10
|
+
private val url: String,
|
|
11
|
+
) : Event<LinkLongPressEvent>(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 = "onLinkLongPress"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -2,6 +2,14 @@ package com.swmansion.enriched.markdown.parser
|
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
|
|
5
|
+
data class Md4cFlags(
|
|
6
|
+
val underline: Boolean = false,
|
|
7
|
+
) {
|
|
8
|
+
companion object {
|
|
9
|
+
val DEFAULT = Md4cFlags()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
class Parser {
|
|
6
14
|
companion object {
|
|
7
15
|
init {
|
|
@@ -17,7 +25,10 @@ class Parser {
|
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
@JvmStatic
|
|
20
|
-
private external fun nativeParseMarkdown(
|
|
28
|
+
private external fun nativeParseMarkdown(
|
|
29
|
+
markdown: String,
|
|
30
|
+
flags: Md4cFlags,
|
|
31
|
+
): MarkdownASTNode?
|
|
21
32
|
|
|
22
33
|
/**
|
|
23
34
|
* Shared parser instance. Parser is stateless and thread-safe, so it can be reused
|
|
@@ -26,13 +37,16 @@ class Parser {
|
|
|
26
37
|
val shared: Parser = Parser()
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
fun parseMarkdown(
|
|
40
|
+
fun parseMarkdown(
|
|
41
|
+
markdown: String,
|
|
42
|
+
flags: Md4cFlags = Md4cFlags.DEFAULT,
|
|
43
|
+
): MarkdownASTNode? {
|
|
30
44
|
if (markdown.isBlank()) {
|
|
31
45
|
return null
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
try {
|
|
35
|
-
val ast = nativeParseMarkdown(markdown)
|
|
49
|
+
val ast = nativeParseMarkdown(markdown, flags)
|
|
36
50
|
|
|
37
51
|
if (ast != null) {
|
|
38
52
|
return ast
|