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,321 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.content.Context
4
+ import android.content.res.Resources
5
+ import android.graphics.BitmapFactory
6
+ import android.graphics.Canvas
7
+ import android.graphics.Paint
8
+ import android.graphics.Path
9
+ import android.graphics.drawable.Drawable
10
+ import android.os.Build
11
+ import android.text.Spannable
12
+ import android.util.Log
13
+ import androidx.core.graphics.drawable.toDrawable
14
+ import androidx.core.graphics.withSave
15
+ import com.swmansion.enriched.markdown.EnrichedMarkdownText
16
+ import com.swmansion.enriched.markdown.styles.StyleConfig
17
+ import com.swmansion.enriched.markdown.utils.AsyncDrawable
18
+ import java.lang.ref.WeakReference
19
+ import android.text.style.ImageSpan as AndroidImageSpan
20
+ import android.text.style.LineHeightSpan as AndroidLineHeightSpan
21
+
22
+ /**
23
+ * Custom ImageSpan for rendering markdown images.
24
+ * Handles both inline and block images with async loading support.
25
+ */
26
+ class ImageSpan(
27
+ context: Context,
28
+ val imageUrl: String,
29
+ styleConfig: StyleConfig,
30
+ val isInline: Boolean = false,
31
+ val altText: String = "",
32
+ ) : AndroidImageSpan(
33
+ createInitialDrawable(styleConfig, imageUrl, isInline),
34
+ imageUrl,
35
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ALIGN_CENTER else ALIGN_BASELINE,
36
+ ),
37
+ AndroidLineHeightSpan {
38
+ private var loadedDrawable: Drawable? = null
39
+ private val imageStyle = styleConfig.imageStyle
40
+ private val height: Int = if (isInline) calculateInlineImageSize(styleConfig) else imageStyle.height.toInt()
41
+ private val borderRadiusPx: Int = (imageStyle.borderRadius * context.resources.displayMetrics.density).toInt()
42
+
43
+ private var cachedWidth: Int = MINIMUM_VALID_DIMENSION
44
+ private val initialDrawable: Drawable = super.getDrawable()
45
+ private var viewRef: WeakReference<EnrichedMarkdownText>? = null
46
+
47
+ init {
48
+ setupLoadingLogic()
49
+ }
50
+
51
+ private fun setupLoadingLogic() {
52
+ val d = initialDrawable
53
+ if (d is AsyncDrawable) {
54
+ // Set up the callback immediately. If already loaded, it triggers next frame.
55
+ d.onLoaded = { handleImageLoaded(d) }
56
+ if (d.isLoaded) handleImageLoaded(d)
57
+ } else if (d.intrinsicWidth > 0) {
58
+ // Local file or resource
59
+ wrapAndAssignDrawable(d)
60
+ }
61
+ }
62
+
63
+ private fun handleImageLoaded(asyncDrawable: AsyncDrawable) {
64
+ val rawDrawable = asyncDrawable.internalDrawable
65
+ wrapAndAssignDrawable(rawDrawable)
66
+ }
67
+
68
+ private fun wrapAndAssignDrawable(base: Drawable) {
69
+ val view = viewRef?.get()
70
+ val targetWidth =
71
+ if (isInline) {
72
+ height
73
+ } else {
74
+ val available = view?.let { getAvailableWidth(it) } ?: cachedWidth
75
+ available.coerceAtLeast(MINIMUM_VALID_DIMENSION)
76
+ }
77
+
78
+ loadedDrawable =
79
+ ScaledImageDrawable(
80
+ imageDrawable = base,
81
+ targetWidth = targetWidth,
82
+ targetHeight = height,
83
+ borderRadius = borderRadiusPx,
84
+ isBlockImage = !isInline,
85
+ )
86
+ requestReflow()
87
+ }
88
+
89
+ private fun requestReflow() {
90
+ val view = viewRef?.get() ?: return
91
+ val text = view.text
92
+ if (text is Spannable) {
93
+ val start = text.getSpanStart(this)
94
+ val end = text.getSpanEnd(this)
95
+ if (start != -1 && end != -1) {
96
+ // Notifying the spannable that the span changed triggers a re-layout
97
+ text.setSpan(this, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
98
+ }
99
+ } else {
100
+ view.invalidate()
101
+ view.requestLayout()
102
+ }
103
+ }
104
+
105
+ fun registerTextView(view: EnrichedMarkdownText) {
106
+ viewRef = WeakReference(view)
107
+ if (!isInline) {
108
+ val availableWidth = getAvailableWidth(view)
109
+ if (availableWidth > MINIMUM_VALID_DIMENSION) {
110
+ updateWidthAndRecreate(availableWidth)
111
+ }
112
+ // Ensure we catch the width after the first layout pass
113
+ view.post {
114
+ val postWidth = getAvailableWidth(view)
115
+ if (postWidth != cachedWidth) updateWidthAndRecreate(postWidth)
116
+ }
117
+ }
118
+ }
119
+
120
+ private fun updateWidthAndRecreate(newWidth: Int) {
121
+ if (newWidth <= MINIMUM_VALID_DIMENSION || cachedWidth == newWidth) return
122
+ cachedWidth = newWidth
123
+
124
+ // If we already have a loaded source, recreate the scaled wrapper with new width
125
+ val base = (initialDrawable as? AsyncDrawable)?.internalDrawable ?: initialDrawable
126
+ if (base.intrinsicWidth > 0) {
127
+ wrapAndAssignDrawable(base)
128
+ }
129
+ }
130
+
131
+ private fun getAvailableWidth(view: EnrichedMarkdownText): Int = view.layout?.width ?: view.width
132
+
133
+ override fun getDrawable(): Drawable {
134
+ val drawable = loadedDrawable ?: initialDrawable
135
+ if (drawable !is ScaledImageDrawable) {
136
+ val dWidth = if (isInline) height else cachedWidth.takeIf { it > 0 } ?: drawable.intrinsicWidth
137
+ val dHeight = if (isInline) height else height
138
+ drawable.setBounds(0, 0, dWidth.coerceAtLeast(0), dHeight.coerceAtLeast(0))
139
+ }
140
+ return drawable
141
+ }
142
+
143
+ override fun getSize(
144
+ paint: Paint,
145
+ text: CharSequence?,
146
+ start: Int,
147
+ end: Int,
148
+ fm: Paint.FontMetricsInt?,
149
+ ): Int = getDrawable().bounds.right
150
+
151
+ override fun chooseHeight(
152
+ text: CharSequence?,
153
+ start: Int,
154
+ end: Int,
155
+ spanstartv: Int,
156
+ lineHeight: Int,
157
+ fm: Paint.FontMetricsInt?,
158
+ ) {
159
+ if (fm == null || isInline) return
160
+ val currentLineHeight = fm.descent - fm.ascent
161
+ if (height > currentLineHeight) {
162
+ val extraHeight = height - currentLineHeight
163
+ fm.descent += extraHeight
164
+ fm.bottom += extraHeight
165
+ }
166
+ }
167
+
168
+ override fun draw(
169
+ canvas: Canvas,
170
+ text: CharSequence?,
171
+ start: Int,
172
+ end: Int,
173
+ x: Float,
174
+ top: Int,
175
+ y: Int,
176
+ bottom: Int,
177
+ paint: Paint,
178
+ ) {
179
+ val drawable = getDrawable()
180
+ canvas.withSave {
181
+ if (isInline) {
182
+ val imageHeight = drawable.bounds.height()
183
+ translate(x, (y - imageHeight + (imageHeight * 0.1f)))
184
+ } else {
185
+ translate(x, top.toFloat())
186
+ }
187
+ drawable.draw(this)
188
+ }
189
+ }
190
+
191
+ // --- Helper Classes ---
192
+
193
+ private class ScaledImageDrawable(
194
+ private val imageDrawable: Drawable,
195
+ private val targetWidth: Int,
196
+ private val targetHeight: Int,
197
+ private val borderRadius: Int,
198
+ isBlockImage: Boolean,
199
+ ) : Drawable() {
200
+ private val clipPath: Path? =
201
+ if (borderRadius > 0) {
202
+ Path().apply {
203
+ addRoundRect(
204
+ 0f,
205
+ 0f,
206
+ targetWidth.toFloat(),
207
+ targetHeight.toFloat(),
208
+ borderRadius.toFloat(),
209
+ borderRadius.toFloat(),
210
+ Path.Direction.CW,
211
+ )
212
+ }
213
+ } else {
214
+ null
215
+ }
216
+
217
+ init {
218
+ setBounds(0, 0, targetWidth, targetHeight)
219
+ val iW = imageDrawable.intrinsicWidth
220
+ val iH = imageDrawable.intrinsicHeight
221
+
222
+ val (sW, sH) =
223
+ if (iW > 0 && iH > 0) {
224
+ if (isBlockImage) {
225
+ val scale = targetWidth.toFloat() / iW
226
+ targetWidth to (iH * scale).toInt()
227
+ } else {
228
+ val scale = minOf(targetWidth.toFloat() / iW, targetHeight.toFloat() / iH)
229
+ (iW * scale).toInt() to (iH * scale).toInt()
230
+ }
231
+ } else {
232
+ targetWidth to targetHeight
233
+ }
234
+
235
+ val left = (targetWidth - sW) / 2
236
+ val top = (targetHeight - sH) / 2
237
+ imageDrawable.setBounds(left, top, left + sW, top + sH)
238
+ }
239
+
240
+ override fun draw(canvas: Canvas) {
241
+ if (clipPath != null) {
242
+ canvas.withSave {
243
+ clipPath(clipPath)
244
+ imageDrawable.draw(canvas)
245
+ }
246
+ } else {
247
+ imageDrawable.draw(canvas)
248
+ }
249
+ }
250
+
251
+ override fun setAlpha(alpha: Int) {
252
+ imageDrawable.alpha = alpha
253
+ }
254
+
255
+ override fun setColorFilter(cf: android.graphics.ColorFilter?) {
256
+ imageDrawable.colorFilter = cf
257
+ }
258
+
259
+ @Suppress("DEPRECATION")
260
+ @Deprecated("Deprecated in Java")
261
+ override fun getOpacity(): Int = imageDrawable.opacity
262
+
263
+ override fun getIntrinsicWidth(): Int = targetWidth
264
+
265
+ override fun getIntrinsicHeight(): Int = targetHeight
266
+ }
267
+
268
+ companion object {
269
+ private const val MINIMUM_VALID_DIMENSION = 0
270
+
271
+ private fun calculateInlineImageSize(style: StyleConfig): Int = style.inlineImageStyle.size.toInt()
272
+
273
+ private fun createInitialDrawable(
274
+ style: StyleConfig,
275
+ url: String,
276
+ isInline: Boolean,
277
+ ): Drawable {
278
+ val imgStyle = style.imageStyle
279
+ val size = if (isInline) calculateInlineImageSize(style) else imgStyle.height.toInt()
280
+
281
+ return prepareDrawable(url, size, size) ?: PlaceholderDrawable(size, size)
282
+ }
283
+
284
+ private fun prepareDrawable(
285
+ src: String,
286
+ tw: Int,
287
+ th: Int,
288
+ ): Drawable? {
289
+ if (src.startsWith("http")) {
290
+ return AsyncDrawable(src).apply { setBounds(0, 0, tw, th) }
291
+ }
292
+ val path = src.removePrefix("file://")
293
+ return try {
294
+ BitmapFactory.decodeFile(path)?.toDrawable(Resources.getSystem())?.apply {
295
+ setBounds(0, 0, intrinsicWidth, intrinsicHeight)
296
+ }
297
+ } catch (e: Exception) {
298
+ Log.w("ImageSpan", "Failed to load local image: $path", e)
299
+ null
300
+ }
301
+ }
302
+
303
+ private class PlaceholderDrawable(
304
+ private val w: Int,
305
+ private val h: Int,
306
+ ) : Drawable() {
307
+ override fun draw(canvas: Canvas) {}
308
+
309
+ override fun setAlpha(alpha: Int) {}
310
+
311
+ override fun setColorFilter(cf: android.graphics.ColorFilter?) {}
312
+
313
+ @Deprecated("Deprecated in Java")
314
+ override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSLUCENT
315
+
316
+ override fun getIntrinsicWidth(): Int = w
317
+
318
+ override fun getIntrinsicHeight(): Int = h
319
+ }
320
+ }
321
+ }
@@ -0,0 +1,27 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.graphics.Paint
4
+ import kotlin.math.ceil
5
+ import kotlin.math.floor
6
+ import android.text.style.LineHeightSpan as AndroidLineHeightSpan
7
+
8
+ class LineHeightSpan(
9
+ height: Float,
10
+ ) : AndroidLineHeightSpan {
11
+ private val lineHeight: Int = ceil(height.toDouble()).toInt()
12
+
13
+ override fun chooseHeight(
14
+ text: CharSequence?,
15
+ start: Int,
16
+ end: Int,
17
+ spanstartv: Int,
18
+ v: Int,
19
+ fm: Paint.FontMetricsInt?,
20
+ ) {
21
+ if (fm == null) return
22
+
23
+ val leading = lineHeight - ((-fm.ascent) + fm.descent)
24
+ fm.ascent -= ceil(leading / 2.0f).toInt()
25
+ fm.descent += floor(leading / 2.0f).toInt()
26
+ }
27
+ }
@@ -0,0 +1,51 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.content.Context
4
+ import android.text.TextPaint
5
+ import android.text.style.ClickableSpan
6
+ import android.view.View
7
+ import com.swmansion.enriched.markdown.EnrichedMarkdownText
8
+ import com.swmansion.enriched.markdown.renderer.BlockStyle
9
+ import com.swmansion.enriched.markdown.renderer.SpanStyleCache
10
+ import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
11
+
12
+ class LinkSpan(
13
+ val url: String,
14
+ private val onLinkPress: ((String) -> Unit)?,
15
+ private val onLinkLongPress: ((String) -> Unit)?,
16
+ private val styleCache: SpanStyleCache,
17
+ private val blockStyle: BlockStyle,
18
+ private val context: Context,
19
+ ) : ClickableSpan() {
20
+ @Volatile
21
+ private var longPressTriggered = false
22
+
23
+ override fun onClick(widget: View) {
24
+ if (longPressTriggered) {
25
+ longPressTriggered = false
26
+ return
27
+ }
28
+
29
+ onLinkPress?.invoke(url) ?: (widget as? EnrichedMarkdownText)?.emitOnLinkPress(url)
30
+ }
31
+
32
+ fun onLongClick(widget: View): Boolean {
33
+ longPressTriggered = true
34
+
35
+ (widget as? EnrichedMarkdownText)?.emitOnLinkLongPress(url)
36
+
37
+ onLinkLongPress?.invoke(url)
38
+
39
+ return true
40
+ }
41
+
42
+ override fun updateDrawState(textPaint: TextPaint) {
43
+ super.updateDrawState(textPaint)
44
+
45
+ textPaint.textSize = blockStyle.fontSize
46
+ textPaint.applyBlockStyleFont(blockStyle, context)
47
+
48
+ textPaint.color = styleCache.linkColor
49
+ textPaint.isUnderlineText = styleCache.linkUnderline
50
+ }
51
+ }
@@ -0,0 +1,76 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.graphics.Paint
4
+ import android.text.style.LineHeightSpan
5
+
6
+ /**
7
+ * Adds bottom margin to a block element (paragraphs/headings) using LineHeightSpan.
8
+ *
9
+ * For spacer lines (single newline), sets the line height to exactly marginBottom.
10
+ * For regular lines, adds marginBottom only at paragraph boundaries to preserve lineHeight.
11
+ *
12
+ * @param marginBottom The margin in pixels to add below the block (0 = no margin)
13
+ */
14
+ class MarginBottomSpan(
15
+ val marginBottom: Float,
16
+ ) : LineHeightSpan {
17
+ override fun chooseHeight(
18
+ text: CharSequence,
19
+ start: Int,
20
+ end: Int,
21
+ spanstartv: Int,
22
+ lineHeight: Int,
23
+ fm: Paint.FontMetricsInt,
24
+ ) {
25
+ // Only process lines that end with a newline
26
+ if (end <= start || text[end - 1] != '\n') {
27
+ return
28
+ }
29
+
30
+ val marginPixels = marginBottom.toInt()
31
+
32
+ // Handle spacer lines (single newline character)
33
+ if (end - start == 1 && text[start] == '\n') {
34
+ if (hasContentAfter(text, end)) {
35
+ // Set line height to exactly marginBottom for spacer lines
36
+ fm.top = 0
37
+ fm.ascent = 0
38
+ fm.descent = marginPixels
39
+ fm.bottom = marginPixels
40
+ } else {
41
+ // No content after - collapse the spacer line to zero height
42
+ fm.top = 0
43
+ fm.ascent = 0
44
+ fm.descent = 0
45
+ fm.bottom = 0
46
+ }
47
+ return
48
+ }
49
+
50
+ // For regular lines, add spacing only if there's content after
51
+ if (hasContentAfter(text, end)) {
52
+ fm.descent += marginPixels
53
+ fm.bottom += marginPixels
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Checks if there's non-newline content after the given position.
59
+ * Used to determine if spacing should be applied (between items) or skipped (after last item).
60
+ */
61
+ private fun hasContentAfter(
62
+ text: CharSequence,
63
+ pos: Int,
64
+ ): Boolean {
65
+ if (pos >= text.length) return false
66
+
67
+ // If the next character is a newline, check the character after that
68
+ if (text[pos] == '\n') {
69
+ val nextPos = pos + 1
70
+ if (nextPos >= text.length) return false
71
+ return text[nextPos] != '\n' // Non-newline = content exists
72
+ }
73
+
74
+ return true // Non-newline content immediately after
75
+ }
76
+ }
@@ -0,0 +1,87 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Paint
6
+ import android.graphics.Typeface
7
+ import android.text.Layout
8
+ import android.text.TextPaint
9
+ import com.facebook.react.common.ReactConstants
10
+ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
11
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
12
+ import com.swmansion.enriched.markdown.renderer.BlockStyle
13
+ import com.swmansion.enriched.markdown.renderer.SpanStyleCache
14
+ import com.swmansion.enriched.markdown.styles.ListStyle
15
+
16
+ class OrderedListSpan(
17
+ private val listStyle: ListStyle,
18
+ depth: Int,
19
+ context: Context,
20
+ styleCache: SpanStyleCache,
21
+ ) : BaseListSpan(
22
+ depth = depth,
23
+ context = context,
24
+ styleCache = styleCache,
25
+ blockStyle =
26
+ BlockStyle(
27
+ fontSize = listStyle.fontSize,
28
+ fontFamily = listStyle.fontFamily,
29
+ fontWeight = listStyle.fontWeight,
30
+ color = listStyle.color,
31
+ ),
32
+ marginLeft = listStyle.marginLeft,
33
+ gapWidth = listStyle.gapWidth,
34
+ ) {
35
+ companion object {
36
+ private val sharedMarkerPaint = TextPaint().apply { isAntiAlias = true }
37
+ }
38
+
39
+ private val markerTypeface: Typeface =
40
+ run {
41
+ val fontFamily = listStyle.fontFamily.takeIf { it.isNotEmpty() }
42
+ val fontWeight = parseFontWeight(listStyle.markerFontWeight)
43
+ applyStyles(null, ReactConstants.UNSET, fontWeight, fontFamily, context.assets)
44
+ }
45
+
46
+ private fun configureMarkerPaint(): TextPaint =
47
+ sharedMarkerPaint.apply {
48
+ textSize = listStyle.fontSize
49
+ color = listStyle.markerColor
50
+ typeface = markerTypeface
51
+ }
52
+
53
+ override fun getMarkerWidth(): Float {
54
+ val paint = configureMarkerPaint()
55
+ return paint.measureText("99.")
56
+ }
57
+
58
+ var itemNumber: Int = 1
59
+ private set
60
+
61
+ override fun drawMarker(
62
+ c: Canvas,
63
+ p: Paint,
64
+ x: Int,
65
+ dir: Int,
66
+ top: Int,
67
+ baseline: Int,
68
+ bottom: Int,
69
+ layout: Layout?,
70
+ start: Int,
71
+ ) {
72
+ val markerPaint = configureMarkerPaint()
73
+ val text = "$itemNumber."
74
+ val textWidth = markerPaint.measureText(text)
75
+
76
+ // Calculate marker position based on depth
77
+ // depth 0: markerWidth, depth 1: marginLeft + markerWidth, etc.
78
+ val markerRightEdge = (depth * marginLeft + getMarkerWidth()) * dir
79
+ val markerX = markerRightEdge - textWidth * dir
80
+
81
+ c.drawText(text, markerX, baseline.toFloat(), markerPaint)
82
+ }
83
+
84
+ fun setItemNumber(number: Int) {
85
+ itemNumber = number
86
+ }
87
+ }
@@ -0,0 +1,12 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.text.TextPaint
4
+ import android.text.style.CharacterStyle
5
+
6
+ class StrikethroughSpan(
7
+ private val strikethroughColor: Int,
8
+ ) : CharacterStyle() {
9
+ override fun updateDrawState(tp: TextPaint) {
10
+ tp.isStrikeThruText = true
11
+ }
12
+ }
@@ -0,0 +1,37 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.graphics.Typeface
4
+ import android.text.TextPaint
5
+ import android.text.style.MetricAffectingSpan
6
+ import com.swmansion.enriched.markdown.renderer.BlockStyle
7
+ import com.swmansion.enriched.markdown.renderer.SpanStyleCache
8
+ import com.swmansion.enriched.markdown.utils.applyColorPreserving
9
+
10
+ class StrongSpan(
11
+ private val styleCache: SpanStyleCache,
12
+ private val blockStyle: BlockStyle,
13
+ ) : MetricAffectingSpan() {
14
+ private val strongColor = styleCache.getStrongColorFor(blockStyle.color)
15
+
16
+ override fun updateDrawState(tp: TextPaint) {
17
+ applyStrongStyle(tp)
18
+ tp.applyColorPreserving(strongColor, *styleCache.colorsToPreserve)
19
+ }
20
+
21
+ override fun updateMeasureState(tp: TextPaint) {
22
+ applyStrongStyle(tp)
23
+ }
24
+
25
+ private fun applyStrongStyle(tp: TextPaint) {
26
+ // Preserve code fontSize if code is nested inside strong text
27
+ val codeFontSize = blockStyle.fontSize * 0.85f
28
+ if (kotlin.math.abs(tp.textSize - codeFontSize) > 0.1f) {
29
+ tp.textSize = blockStyle.fontSize
30
+ }
31
+
32
+ val currentTypeface = tp.typeface ?: Typeface.DEFAULT
33
+ val isItalic = (currentTypeface.style) and Typeface.ITALIC != 0
34
+ val style = if (isItalic) Typeface.BOLD_ITALIC else Typeface.BOLD
35
+ tp.typeface = Typeface.create(currentTypeface, style)
36
+ }
37
+ }
@@ -0,0 +1,26 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.content.Context
4
+ import android.text.TextPaint
5
+ import android.text.style.MetricAffectingSpan
6
+ import com.swmansion.enriched.markdown.renderer.BlockStyle
7
+ import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
8
+
9
+ class TextSpan(
10
+ private val blockStyle: BlockStyle,
11
+ private val context: Context,
12
+ ) : MetricAffectingSpan() {
13
+ override fun updateDrawState(tp: TextPaint) {
14
+ applyBlockStyle(tp)
15
+ }
16
+
17
+ override fun updateMeasureState(tp: TextPaint) {
18
+ applyBlockStyle(tp)
19
+ }
20
+
21
+ private fun applyBlockStyle(tp: TextPaint) {
22
+ tp.textSize = blockStyle.fontSize
23
+ tp.color = blockStyle.color
24
+ tp.applyBlockStyleFont(blockStyle, context)
25
+ }
26
+ }
@@ -0,0 +1,69 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.Paint
5
+ import android.text.style.ReplacementSpan
6
+
7
+ class ThematicBreakSpan(
8
+ private val lineColor: Int,
9
+ private val lineHeight: Float,
10
+ private val marginTop: Float,
11
+ private val marginBottom: Float,
12
+ ) : ReplacementSpan() {
13
+ override fun getSize(
14
+ paint: Paint,
15
+ text: CharSequence?,
16
+ start: Int,
17
+ end: Int,
18
+ fm: Paint.FontMetricsInt?,
19
+ ): Int {
20
+ val totalHeight = (marginTop + lineHeight + marginBottom).toInt()
21
+
22
+ fm?.apply {
23
+ ascent = -totalHeight
24
+ top = -totalHeight
25
+ descent = 0
26
+ bottom = 0
27
+ }
28
+
29
+ return 0
30
+ }
31
+
32
+ override fun draw(
33
+ canvas: Canvas,
34
+ text: CharSequence?,
35
+ start: Int,
36
+ end: Int,
37
+ x: Float,
38
+ top: Int,
39
+ y: Int,
40
+ bottom: Int,
41
+ paint: Paint,
42
+ ) {
43
+ paint.withStyle(lineColor, lineHeight) {
44
+ val lineY = top + marginTop + (lineHeight / 2f)
45
+
46
+ canvas.drawLine(0f, lineY, canvas.width.toFloat(), lineY, paint)
47
+ }
48
+ }
49
+
50
+ private inline fun Paint.withStyle(
51
+ color: Int,
52
+ width: Float,
53
+ action: () -> Unit,
54
+ ) {
55
+ val oldColor = this.color
56
+ val oldWidth = this.strokeWidth
57
+ val oldStyle = this.style
58
+
59
+ this.color = color
60
+ this.strokeWidth = width
61
+ this.style = Paint.Style.STROKE
62
+
63
+ action()
64
+
65
+ this.color = oldColor
66
+ this.strokeWidth = oldWidth
67
+ this.style = oldStyle
68
+ }
69
+ }