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.
Files changed (211) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +479 -0
  3. package/ReactNativeEnrichedMarkdown.podspec +27 -0
  4. package/android/build.gradle +101 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +39 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +21 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  10. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
  11. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +57 -0
  12. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1164 -0
  13. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
  14. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
  15. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.cpp +16 -0
  16. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.h +20 -0
  17. package/android/gradle.properties +5 -0
  18. package/android/src/main/AndroidManifest.xml +2 -0
  19. package/android/src/main/baseline-prof.txt +65 -0
  20. package/android/src/main/cpp/jni-adapter.cpp +203 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +153 -0
  22. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +30 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +119 -0
  24. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextPackage.kt +17 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +165 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +23 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +29 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +48 -0
  29. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
  30. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +89 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +105 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +35 -0
  33. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +15 -0
  34. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +26 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +54 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +52 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +15 -0
  38. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +28 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +58 -0
  41. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +69 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +99 -0
  43. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +66 -0
  44. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +95 -0
  45. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +85 -0
  46. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +26 -0
  47. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +29 -0
  48. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +44 -0
  49. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +136 -0
  50. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
  51. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
  52. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
  53. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
  54. package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
  55. package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
  56. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +320 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +36 -0
  58. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +37 -0
  59. package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
  60. package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
  61. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
  62. package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
  63. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
  64. package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
  65. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +10 -0
  66. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +48 -0
  67. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +51 -0
  68. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
  69. package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
  70. package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +29 -0
  71. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +21 -0
  72. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
  73. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
  74. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +54 -0
  75. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +29 -0
  76. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
  77. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +180 -0
  78. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +75 -0
  79. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
  80. package/android/src/main/java/com/swmansion/enriched/markdown/utils/AsyncDrawable.kt +91 -0
  81. package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +809 -0
  82. package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +365 -0
  83. package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
  84. package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +181 -0
  85. package/android/src/main/jni/CMakeLists.txt +82 -0
  86. package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
  87. package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
  88. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
  89. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
  90. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
  91. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +33 -0
  92. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +49 -0
  93. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
  94. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +25 -0
  95. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +19 -0
  96. package/cpp/md4c/md4c.c +6492 -0
  97. package/cpp/md4c/md4c.h +402 -0
  98. package/cpp/parser/MD4CParser.cpp +314 -0
  99. package/cpp/parser/MD4CParser.hpp +23 -0
  100. package/cpp/parser/MarkdownASTNode.hpp +49 -0
  101. package/ios/EnrichedMarkdownText.h +18 -0
  102. package/ios/EnrichedMarkdownText.mm +1074 -0
  103. package/ios/attachments/ImageAttachment.h +23 -0
  104. package/ios/attachments/ImageAttachment.m +185 -0
  105. package/ios/attachments/ThematicBreakAttachment.h +15 -0
  106. package/ios/attachments/ThematicBreakAttachment.m +33 -0
  107. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
  108. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
  109. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  110. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
  111. package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +57 -0
  112. package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1164 -0
  113. package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
  114. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
  115. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
  116. package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
  117. package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
  118. package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
  119. package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
  120. package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
  121. package/ios/internals/EnrichedMarkdownTextState.h +24 -0
  122. package/ios/parser/MarkdownASTNode.h +33 -0
  123. package/ios/parser/MarkdownASTNode.m +32 -0
  124. package/ios/parser/MarkdownParser.h +8 -0
  125. package/ios/parser/MarkdownParser.mm +13 -0
  126. package/ios/parser/MarkdownParserBridge.mm +110 -0
  127. package/ios/renderer/AttributedRenderer.h +9 -0
  128. package/ios/renderer/AttributedRenderer.m +119 -0
  129. package/ios/renderer/BlockquoteRenderer.h +7 -0
  130. package/ios/renderer/BlockquoteRenderer.m +159 -0
  131. package/ios/renderer/CodeBlockRenderer.h +10 -0
  132. package/ios/renderer/CodeBlockRenderer.m +89 -0
  133. package/ios/renderer/CodeRenderer.h +10 -0
  134. package/ios/renderer/CodeRenderer.m +60 -0
  135. package/ios/renderer/EmphasisRenderer.h +6 -0
  136. package/ios/renderer/EmphasisRenderer.m +96 -0
  137. package/ios/renderer/HeadingRenderer.h +7 -0
  138. package/ios/renderer/HeadingRenderer.m +98 -0
  139. package/ios/renderer/ImageRenderer.h +12 -0
  140. package/ios/renderer/ImageRenderer.m +62 -0
  141. package/ios/renderer/LinkRenderer.h +7 -0
  142. package/ios/renderer/LinkRenderer.m +69 -0
  143. package/ios/renderer/ListItemRenderer.h +16 -0
  144. package/ios/renderer/ListItemRenderer.m +91 -0
  145. package/ios/renderer/ListRenderer.h +13 -0
  146. package/ios/renderer/ListRenderer.m +67 -0
  147. package/ios/renderer/NodeRenderer.h +8 -0
  148. package/ios/renderer/ParagraphRenderer.h +7 -0
  149. package/ios/renderer/ParagraphRenderer.m +69 -0
  150. package/ios/renderer/RenderContext.h +88 -0
  151. package/ios/renderer/RenderContext.m +248 -0
  152. package/ios/renderer/RendererFactory.h +12 -0
  153. package/ios/renderer/RendererFactory.m +110 -0
  154. package/ios/renderer/StrongRenderer.h +6 -0
  155. package/ios/renderer/StrongRenderer.m +83 -0
  156. package/ios/renderer/TextRenderer.h +6 -0
  157. package/ios/renderer/TextRenderer.m +16 -0
  158. package/ios/renderer/ThematicBreakRenderer.h +5 -0
  159. package/ios/renderer/ThematicBreakRenderer.m +53 -0
  160. package/ios/styles/StyleConfig.h +228 -0
  161. package/ios/styles/StyleConfig.mm +1467 -0
  162. package/ios/utils/BlockquoteBorder.h +20 -0
  163. package/ios/utils/BlockquoteBorder.m +92 -0
  164. package/ios/utils/CodeBackground.h +19 -0
  165. package/ios/utils/CodeBackground.m +191 -0
  166. package/ios/utils/CodeBlockBackground.h +17 -0
  167. package/ios/utils/CodeBlockBackground.m +87 -0
  168. package/ios/utils/EditMenuUtils.h +22 -0
  169. package/ios/utils/EditMenuUtils.m +118 -0
  170. package/ios/utils/FontUtils.h +20 -0
  171. package/ios/utils/FontUtils.m +13 -0
  172. package/ios/utils/HTMLGenerator.h +20 -0
  173. package/ios/utils/HTMLGenerator.m +779 -0
  174. package/ios/utils/LastElementUtils.h +53 -0
  175. package/ios/utils/ListMarkerDrawer.h +15 -0
  176. package/ios/utils/ListMarkerDrawer.m +127 -0
  177. package/ios/utils/MarkdownExtractor.h +17 -0
  178. package/ios/utils/MarkdownExtractor.m +295 -0
  179. package/ios/utils/ParagraphStyleUtils.h +13 -0
  180. package/ios/utils/ParagraphStyleUtils.m +56 -0
  181. package/ios/utils/PasteboardUtils.h +36 -0
  182. package/ios/utils/PasteboardUtils.m +134 -0
  183. package/ios/utils/RTFExportUtils.h +24 -0
  184. package/ios/utils/RTFExportUtils.m +297 -0
  185. package/ios/utils/RuntimeKeys.h +38 -0
  186. package/ios/utils/RuntimeKeys.m +11 -0
  187. package/ios/utils/TextViewLayoutManager.h +14 -0
  188. package/ios/utils/TextViewLayoutManager.mm +113 -0
  189. package/lib/module/EnrichedMarkdownText.js +34 -0
  190. package/lib/module/EnrichedMarkdownText.js.map +1 -0
  191. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +130 -0
  192. package/lib/module/index.js +5 -0
  193. package/lib/module/index.js.map +1 -0
  194. package/lib/module/normalizeMarkdownStyle.js +340 -0
  195. package/lib/module/normalizeMarkdownStyle.js.map +1 -0
  196. package/lib/module/package.json +1 -0
  197. package/lib/typescript/package.json +1 -0
  198. package/lib/typescript/src/EnrichedMarkdownText.d.ts +101 -0
  199. package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
  200. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +111 -0
  201. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
  202. package/lib/typescript/src/index.d.ts +5 -0
  203. package/lib/typescript/src/index.d.ts.map +1 -0
  204. package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
  205. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
  206. package/package.json +186 -1
  207. package/react-native.config.js +13 -0
  208. package/src/EnrichedMarkdownText.tsx +152 -0
  209. package/src/EnrichedMarkdownTextNativeComponent.ts +130 -0
  210. package/src/index.tsx +7 -0
  211. 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
+ }