react-native-enriched-markdown 0.1.0 → 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.
Files changed (226) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +551 -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 +54 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +26 -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 +33 -0
  10. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
  11. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +82 -0
  12. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1388 -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 +220 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +270 -0
  22. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +15 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +173 -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 +385 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +23 -0
  29. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +31 -0
  30. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +62 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +84 -0
  33. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +104 -0
  34. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +36 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +16 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +27 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +70 -0
  38. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +68 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +16 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +29 -0
  41. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +59 -0
  43. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +76 -0
  44. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +103 -0
  45. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +80 -0
  46. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +109 -0
  47. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +86 -0
  48. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
  49. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +27 -0
  50. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +30 -0
  51. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +45 -0
  52. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -0
  53. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +136 -0
  54. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
  55. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
  56. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
  58. package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
  59. package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
  60. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +321 -0
  61. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -0
  62. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +51 -0
  63. package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
  64. package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
  65. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
  66. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
  67. package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
  68. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
  69. package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
  70. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +11 -0
  71. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +51 -0
  72. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +54 -0
  73. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
  74. package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
  75. package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +33 -0
  76. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +23 -0
  77. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
  78. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
  79. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +57 -0
  80. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +33 -0
  81. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
  82. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
  83. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +211 -0
  84. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +92 -0
  85. package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
  86. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
  87. package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -0
  88. package/android/src/main/java/com/swmansion/enriched/markdown/utils/AsyncDrawable.kt +91 -0
  89. package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +827 -0
  90. package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
  91. package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +375 -0
  92. package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
  93. package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +183 -0
  94. package/android/src/main/jni/CMakeLists.txt +70 -0
  95. package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
  96. package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
  97. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
  98. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
  99. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
  100. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +20 -0
  101. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +37 -0
  102. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +22 -0
  103. package/cpp/md4c/md4c.c +6492 -0
  104. package/cpp/md4c/md4c.h +402 -0
  105. package/cpp/parser/MD4CParser.cpp +327 -0
  106. package/cpp/parser/MD4CParser.hpp +27 -0
  107. package/cpp/parser/MarkdownASTNode.hpp +51 -0
  108. package/ios/EnrichedMarkdownText.h +18 -0
  109. package/ios/EnrichedMarkdownText.mm +1401 -0
  110. package/ios/attachments/EnrichedMarkdownImageAttachment.h +23 -0
  111. package/ios/attachments/EnrichedMarkdownImageAttachment.m +185 -0
  112. package/ios/attachments/ThematicBreakAttachment.h +15 -0
  113. package/ios/attachments/ThematicBreakAttachment.m +33 -0
  114. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
  115. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
  116. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +33 -0
  117. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
  118. package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +82 -0
  119. package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1388 -0
  120. package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
  121. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
  122. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
  123. package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
  124. package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
  125. package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
  126. package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
  127. package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
  128. package/ios/internals/EnrichedMarkdownTextState.h +24 -0
  129. package/ios/parser/MarkdownASTNode.h +35 -0
  130. package/ios/parser/MarkdownASTNode.m +32 -0
  131. package/ios/parser/MarkdownParser.h +17 -0
  132. package/ios/parser/MarkdownParser.mm +42 -0
  133. package/ios/parser/MarkdownParserBridge.mm +120 -0
  134. package/ios/renderer/AttributedRenderer.h +11 -0
  135. package/ios/renderer/AttributedRenderer.m +152 -0
  136. package/ios/renderer/BlockquoteRenderer.h +7 -0
  137. package/ios/renderer/BlockquoteRenderer.m +160 -0
  138. package/ios/renderer/CodeBlockRenderer.h +10 -0
  139. package/ios/renderer/CodeBlockRenderer.m +90 -0
  140. package/ios/renderer/CodeRenderer.h +10 -0
  141. package/ios/renderer/CodeRenderer.m +60 -0
  142. package/ios/renderer/EmphasisRenderer.h +6 -0
  143. package/ios/renderer/EmphasisRenderer.m +96 -0
  144. package/ios/renderer/HeadingRenderer.h +7 -0
  145. package/ios/renderer/HeadingRenderer.m +105 -0
  146. package/ios/renderer/ImageRenderer.h +12 -0
  147. package/ios/renderer/ImageRenderer.m +83 -0
  148. package/ios/renderer/LinkRenderer.h +7 -0
  149. package/ios/renderer/LinkRenderer.m +69 -0
  150. package/ios/renderer/ListItemRenderer.h +16 -0
  151. package/ios/renderer/ListItemRenderer.m +103 -0
  152. package/ios/renderer/ListRenderer.h +13 -0
  153. package/ios/renderer/ListRenderer.m +70 -0
  154. package/ios/renderer/NodeRenderer.h +8 -0
  155. package/ios/renderer/ParagraphRenderer.h +7 -0
  156. package/ios/renderer/ParagraphRenderer.m +80 -0
  157. package/ios/renderer/RenderContext.h +105 -0
  158. package/ios/renderer/RenderContext.m +312 -0
  159. package/ios/renderer/RendererFactory.h +12 -0
  160. package/ios/renderer/RendererFactory.m +116 -0
  161. package/ios/renderer/StrikethroughRenderer.h +6 -0
  162. package/ios/renderer/StrikethroughRenderer.m +40 -0
  163. package/ios/renderer/StrongRenderer.h +6 -0
  164. package/ios/renderer/StrongRenderer.m +83 -0
  165. package/ios/renderer/TextRenderer.h +6 -0
  166. package/ios/renderer/TextRenderer.m +16 -0
  167. package/ios/renderer/ThematicBreakRenderer.h +5 -0
  168. package/ios/renderer/ThematicBreakRenderer.m +53 -0
  169. package/ios/renderer/UnderlineRenderer.h +6 -0
  170. package/ios/renderer/UnderlineRenderer.m +39 -0
  171. package/ios/styles/StyleConfig.h +274 -0
  172. package/ios/styles/StyleConfig.mm +1806 -0
  173. package/ios/utils/AccessibilityInfo.h +35 -0
  174. package/ios/utils/AccessibilityInfo.m +24 -0
  175. package/ios/utils/BlockquoteBorder.h +20 -0
  176. package/ios/utils/BlockquoteBorder.m +92 -0
  177. package/ios/utils/CodeBackground.h +19 -0
  178. package/ios/utils/CodeBackground.m +191 -0
  179. package/ios/utils/CodeBlockBackground.h +17 -0
  180. package/ios/utils/CodeBlockBackground.m +82 -0
  181. package/ios/utils/EditMenuUtils.h +22 -0
  182. package/ios/utils/EditMenuUtils.m +118 -0
  183. package/ios/utils/FontUtils.h +25 -0
  184. package/ios/utils/FontUtils.m +27 -0
  185. package/ios/utils/HTMLGenerator.h +20 -0
  186. package/ios/utils/HTMLGenerator.m +793 -0
  187. package/ios/utils/LastElementUtils.h +53 -0
  188. package/ios/utils/ListMarkerDrawer.h +15 -0
  189. package/ios/utils/ListMarkerDrawer.m +127 -0
  190. package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
  191. package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
  192. package/ios/utils/MarkdownExtractor.h +17 -0
  193. package/ios/utils/MarkdownExtractor.m +308 -0
  194. package/ios/utils/ParagraphStyleUtils.h +21 -0
  195. package/ios/utils/ParagraphStyleUtils.m +111 -0
  196. package/ios/utils/PasteboardUtils.h +36 -0
  197. package/ios/utils/PasteboardUtils.m +134 -0
  198. package/ios/utils/RTFExportUtils.h +24 -0
  199. package/ios/utils/RTFExportUtils.m +297 -0
  200. package/ios/utils/RuntimeKeys.h +38 -0
  201. package/ios/utils/RuntimeKeys.m +11 -0
  202. package/ios/utils/TextViewLayoutManager.h +14 -0
  203. package/ios/utils/TextViewLayoutManager.mm +113 -0
  204. package/lib/module/EnrichedMarkdownText.js +65 -0
  205. package/lib/module/EnrichedMarkdownText.js.map +1 -0
  206. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +210 -0
  207. package/lib/module/index.js +4 -0
  208. package/lib/module/index.js.map +1 -0
  209. package/lib/module/normalizeMarkdownStyle.js +384 -0
  210. package/lib/module/normalizeMarkdownStyle.js.map +1 -0
  211. package/lib/module/package.json +1 -0
  212. package/lib/typescript/package.json +1 -0
  213. package/lib/typescript/src/EnrichedMarkdownText.d.ts +183 -0
  214. package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
  215. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +185 -0
  216. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
  217. package/lib/typescript/src/index.d.ts +4 -0
  218. package/lib/typescript/src/index.d.ts.map +1 -0
  219. package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
  220. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
  221. package/package.json +186 -1
  222. package/react-native.config.js +13 -0
  223. package/src/EnrichedMarkdownText.tsx +280 -0
  224. package/src/EnrichedMarkdownTextNativeComponent.ts +210 -0
  225. package/src/index.tsx +10 -0
  226. package/src/normalizeMarkdownStyle.ts +423 -0
@@ -0,0 +1,173 @@
1
+ package com.swmansion.enriched.markdown
2
+
3
+ import android.content.Context
4
+ import com.facebook.react.bridge.ReadableMap
5
+ import com.facebook.react.module.annotations.ReactModule
6
+ import com.facebook.react.uimanager.SimpleViewManager
7
+ import com.facebook.react.uimanager.ThemedReactContext
8
+ import com.facebook.react.uimanager.UIManagerHelper
9
+ import com.facebook.react.uimanager.ViewManagerDelegate
10
+ import com.facebook.react.uimanager.annotations.ReactProp
11
+ import com.facebook.react.viewmanagers.EnrichedMarkdownTextManagerDelegate
12
+ import com.facebook.react.viewmanagers.EnrichedMarkdownTextManagerInterface
13
+ import com.facebook.yoga.YogaMeasureMode
14
+ import com.swmansion.enriched.markdown.events.LinkLongPressEvent
15
+ import com.swmansion.enriched.markdown.events.LinkPressEvent
16
+ import com.swmansion.enriched.markdown.parser.Md4cFlags
17
+
18
+ @ReactModule(name = EnrichedMarkdownTextManager.NAME)
19
+ class EnrichedMarkdownTextManager :
20
+ SimpleViewManager<EnrichedMarkdownText>(),
21
+ EnrichedMarkdownTextManagerInterface<EnrichedMarkdownText> {
22
+ private val mDelegate: ViewManagerDelegate<EnrichedMarkdownText> = EnrichedMarkdownTextManagerDelegate(this)
23
+
24
+ override fun getDelegate(): ViewManagerDelegate<EnrichedMarkdownText>? = mDelegate
25
+
26
+ override fun getName(): String = NAME
27
+
28
+ override fun createViewInstance(reactContext: ThemedReactContext): EnrichedMarkdownText = EnrichedMarkdownText(reactContext)
29
+
30
+ override fun onDropViewInstance(view: EnrichedMarkdownText) {
31
+ super.onDropViewInstance(view)
32
+ MeasurementStore.clearFontScalingSettings(view.id)
33
+ view.layoutManager.releaseMeasurementStore()
34
+ }
35
+
36
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
37
+ val map = mutableMapOf<String, Any>()
38
+ map.put(LinkPressEvent.EVENT_NAME, mapOf("registrationName" to LinkPressEvent.EVENT_NAME))
39
+ map.put(LinkLongPressEvent.EVENT_NAME, mapOf("registrationName" to LinkLongPressEvent.EVENT_NAME))
40
+ return map
41
+ }
42
+
43
+ @ReactProp(name = "markdown")
44
+ override fun setMarkdown(
45
+ view: EnrichedMarkdownText?,
46
+ markdown: String?,
47
+ ) {
48
+ view?.setOnLinkPressCallback { url ->
49
+ emitOnLinkPress(view, url)
50
+ }
51
+
52
+ view?.setOnLinkLongPressCallback { url ->
53
+ emitOnLinkLongPress(view, url)
54
+ }
55
+
56
+ view?.setMarkdownContent(markdown ?: "No markdown content")
57
+ }
58
+
59
+ @ReactProp(name = "markdownStyle")
60
+ override fun setMarkdownStyle(
61
+ view: EnrichedMarkdownText?,
62
+ style: com.facebook.react.bridge.ReadableMap?,
63
+ ) {
64
+ view?.setMarkdownStyle(style)
65
+ }
66
+
67
+ @ReactProp(name = "selectable", defaultBoolean = true)
68
+ override fun setSelectable(
69
+ view: EnrichedMarkdownText?,
70
+ selectable: Boolean,
71
+ ) {
72
+ view?.setIsSelectable(selectable)
73
+ }
74
+
75
+ @ReactProp(name = "md4cFlags")
76
+ override fun setMd4cFlags(
77
+ view: EnrichedMarkdownText?,
78
+ flags: ReadableMap?,
79
+ ) {
80
+ val md4cFlags =
81
+ Md4cFlags(
82
+ underline = flags?.getBoolean("underline") ?: false,
83
+ )
84
+ view?.setMd4cFlags(md4cFlags)
85
+ }
86
+
87
+ @ReactProp(name = "allowFontScaling", defaultBoolean = true)
88
+ override fun setAllowFontScaling(
89
+ view: EnrichedMarkdownText?,
90
+ allowFontScaling: Boolean,
91
+ ) {
92
+ view?.setAllowFontScaling(allowFontScaling)
93
+ }
94
+
95
+ @ReactProp(name = "maxFontSizeMultiplier", defaultFloat = 0f)
96
+ override fun setMaxFontSizeMultiplier(
97
+ view: EnrichedMarkdownText?,
98
+ maxFontSizeMultiplier: Float,
99
+ ) {
100
+ view?.setMaxFontSizeMultiplier(maxFontSizeMultiplier)
101
+ }
102
+
103
+ @ReactProp(name = "allowTrailingMargin", defaultBoolean = false)
104
+ override fun setAllowTrailingMargin(
105
+ view: EnrichedMarkdownText?,
106
+ allowTrailingMargin: Boolean,
107
+ ) {
108
+ view?.setAllowTrailingMargin(allowTrailingMargin)
109
+ }
110
+
111
+ @ReactProp(name = "enableLinkPreview", defaultBoolean = true)
112
+ override fun setEnableLinkPreview(
113
+ view: EnrichedMarkdownText?,
114
+ enableLinkPreview: Boolean,
115
+ ) {
116
+ // This prop is only used on iOS (to control the system link preview on long press).
117
+ // Required by the codegen interface but is a no-op on Android.
118
+ }
119
+
120
+ override fun setPadding(
121
+ view: EnrichedMarkdownText,
122
+ left: Int,
123
+ top: Int,
124
+ right: Int,
125
+ bottom: Int,
126
+ ) {
127
+ super.setPadding(view, left, top, right, bottom)
128
+ view.setPadding(left, top, right, bottom)
129
+ }
130
+
131
+ private fun emitOnLinkPress(
132
+ view: EnrichedMarkdownText,
133
+ url: String,
134
+ ) {
135
+ val context = view.context as com.facebook.react.bridge.ReactContext
136
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
137
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
138
+ val event = LinkPressEvent(surfaceId, view.id, url)
139
+
140
+ eventDispatcher?.dispatchEvent(event)
141
+ }
142
+
143
+ private fun emitOnLinkLongPress(
144
+ view: EnrichedMarkdownText,
145
+ url: String,
146
+ ) {
147
+ val context = view.context as com.facebook.react.bridge.ReactContext
148
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
149
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
150
+ val event = LinkLongPressEvent(surfaceId, view.id, url)
151
+
152
+ eventDispatcher?.dispatchEvent(event)
153
+ }
154
+
155
+ override fun measure(
156
+ context: Context,
157
+ localData: ReadableMap?,
158
+ props: ReadableMap?,
159
+ state: ReadableMap?,
160
+ width: Float,
161
+ widthMode: YogaMeasureMode?,
162
+ height: Float,
163
+ heightMode: YogaMeasureMode?,
164
+ attachmentsPositions: FloatArray?,
165
+ ): Long {
166
+ val id = localData?.getInt("viewTag")
167
+ return MeasurementStore.getMeasureById(context, id, width, height, heightMode, props)
168
+ }
169
+
170
+ companion object {
171
+ const val NAME = "EnrichedMarkdownText"
172
+ }
173
+ }
@@ -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,385 @@
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 android.util.Log
10
+ import com.facebook.react.bridge.ReadableMap
11
+ import com.facebook.react.uimanager.PixelUtil
12
+ import com.facebook.yoga.YogaMeasureMode
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
21
+ import java.util.concurrent.ConcurrentHashMap
22
+ import kotlin.math.ceil
23
+
24
+ /**
25
+ * Manages text measurements for ShadowNode layout.
26
+ * Parses and renders markdown to Spannable at measure time for accurate height calculation.
27
+ */
28
+ object MeasurementStore {
29
+ private const val TAG = "MeasurementStore"
30
+
31
+ private data class PaintParams(
32
+ val typeface: Typeface,
33
+ val fontSize: Float,
34
+ )
35
+
36
+ private data class MeasurementParams(
37
+ val cachedWidth: Float,
38
+ val cachedSize: Long,
39
+ val spannable: CharSequence?,
40
+ val paintParams: PaintParams,
41
+ val markdownHash: Int,
42
+ )
43
+
44
+ private val data = ConcurrentHashMap<Int, MeasurementParams>()
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
+
71
+ private val measurePaint = TextPaint()
72
+ private val measureRenderer = Renderer()
73
+
74
+ @Volatile
75
+ private var lastKnownFontScale: Float = 1.0f
76
+
77
+ /** Updates measurement with rendered Spannable. Returns true if height changed. */
78
+ fun store(
79
+ id: Int,
80
+ spannable: CharSequence?,
81
+ paint: TextPaint,
82
+ ): Boolean {
83
+ val cached = data[id]
84
+ val width = cached?.cachedWidth ?: 0f
85
+ val oldSize = cached?.cachedSize ?: 0L
86
+ val existingHash = cached?.markdownHash ?: 0
87
+ val paintParams = PaintParams(paint.typeface ?: Typeface.DEFAULT, paint.textSize)
88
+
89
+ val newSize = measure(width, spannable, paint)
90
+ data[id] = MeasurementParams(width, newSize, spannable, paintParams, existingHash)
91
+ return oldSize != newSize
92
+ }
93
+
94
+ fun release(id: Int) {
95
+ data.remove(id)
96
+ }
97
+
98
+ /** Main entry point for ShadowNode measurement. */
99
+ fun getMeasureById(
100
+ context: Context,
101
+ id: Int?,
102
+ width: Float,
103
+ height: Float,
104
+ heightMode: YogaMeasureMode?,
105
+ props: ReadableMap?,
106
+ ): Long {
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)
114
+ val resultHeight = YogaMeasureOutput.getHeight(size)
115
+
116
+ if (heightMode === YogaMeasureMode.AT_MOST) {
117
+ val maxHeight = PixelUtil.toDIPFromPixel(height)
118
+ val finalHeight = resultHeight.coerceAtMost(maxHeight)
119
+ return YogaMeasureOutput.make(
120
+ YogaMeasureOutput.getWidth(size),
121
+ finalHeight,
122
+ )
123
+ }
124
+
125
+ return size
126
+ }
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
+
140
+ private fun getMeasureByIdInternal(
141
+ context: Context,
142
+ id: Int?,
143
+ width: Float,
144
+ props: ReadableMap?,
145
+ ): Long {
146
+ val (allowFontScaling, maxFontSizeMultiplier) = resolveFontScalingSettings(id, props)
147
+
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) {
161
+ val newSize = measure(width, cached.spannable, cached.paintParams)
162
+ data[safeId] = cached.copy(cachedWidth = width, cachedSize = newSize)
163
+ return newSize
164
+ }
165
+
166
+ return cached.cachedSize
167
+ }
168
+
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,
217
+ id: Int?,
218
+ width: Float,
219
+ props: ReadableMap?,
220
+ allowFontScaling: Boolean,
221
+ fontScale: Float,
222
+ maxFontSizeMultiplier: Float,
223
+ ): Long {
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)
236
+
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)
250
+
251
+ if (id != null) {
252
+ data[id] = MeasurementParams(width, adjustedSize, textToMeasure, PaintParams(Typeface.DEFAULT, fontSize), propsHash)
253
+ }
254
+
255
+ return adjustedSize
256
+ }
257
+
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)
300
+ }
301
+
302
+ private fun measure(
303
+ maxWidth: Float,
304
+ text: CharSequence?,
305
+ paintParams: PaintParams,
306
+ ): Long {
307
+ measurePaint.reset()
308
+ measurePaint.typeface = paintParams.typeface
309
+ measurePaint.textSize = paintParams.fontSize
310
+ return measure(maxWidth, text, measurePaint)
311
+ }
312
+
313
+ private fun measure(
314
+ maxWidth: Float,
315
+ text: CharSequence?,
316
+ paint: TextPaint,
317
+ ): Long {
318
+ val content = text ?: ""
319
+ val safeWidth = ceil(maxWidth).toInt().coerceAtLeast(1)
320
+
321
+ val builder =
322
+ StaticLayout.Builder
323
+ .obtain(content, 0, content.length, paint, safeWidth)
324
+ .setIncludePad(false)
325
+ .setLineSpacing(0f, 1f)
326
+
327
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
328
+ builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
329
+ }
330
+
331
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
332
+ builder.setUseLineSpacingFromFallbacks(true)
333
+ }
334
+
335
+ val layout = builder.build()
336
+ val measuredHeight = layout.height.toFloat()
337
+
338
+ // Calculate actual content width (widest line)
339
+ val measuredWidth = (0 until layout.lineCount).maxOfOrNull { layout.getLineWidth(it) } ?: 0f
340
+
341
+ return YogaMeasureOutput.make(
342
+ PixelUtil.toDIPFromPixel(ceil(measuredWidth)),
343
+ PixelUtil.toDIPFromPixel(measuredHeight),
344
+ )
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
+ }
385
+ }