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,196 @@
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.Path
7
+ import android.graphics.RectF
8
+ import android.text.Layout
9
+ import android.text.Spanned
10
+ import android.text.TextPaint
11
+ import android.text.style.LeadingMarginSpan
12
+ import android.text.style.LineBackgroundSpan
13
+ import android.text.style.MetricAffectingSpan
14
+ import androidx.core.graphics.withSave
15
+ import com.swmansion.enriched.markdown.renderer.BlockStyle
16
+ import com.swmansion.enriched.markdown.renderer.SpanStyleCache
17
+ import com.swmansion.enriched.markdown.styles.CodeBlockStyle
18
+ import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
19
+ import com.swmansion.enriched.markdown.utils.applyColorPreserving
20
+
21
+ class CodeBlockSpan(
22
+ private val codeBlockStyle: CodeBlockStyle,
23
+ private val context: Context,
24
+ private val styleCache: SpanStyleCache,
25
+ ) : MetricAffectingSpan(),
26
+ LineBackgroundSpan,
27
+ LeadingMarginSpan {
28
+ private val blockStyle =
29
+ BlockStyle(
30
+ fontSize = codeBlockStyle.fontSize,
31
+ fontFamily = codeBlockStyle.fontFamily,
32
+ fontWeight = codeBlockStyle.fontWeight,
33
+ color = codeBlockStyle.color,
34
+ )
35
+
36
+ private val path = Path()
37
+ private val rect = RectF()
38
+ private val arcRect = RectF()
39
+ private val radiiArray = FloatArray(8)
40
+
41
+ companion object {
42
+ private val sharedBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
43
+ private val sharedBorderPaint =
44
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
45
+ style = Paint.Style.STROKE
46
+ strokeCap = Paint.Cap.BUTT
47
+ strokeJoin = Paint.Join.ROUND
48
+ }
49
+ }
50
+
51
+ private fun configureBackgroundPaint(): Paint =
52
+ sharedBackgroundPaint.apply {
53
+ color = codeBlockStyle.backgroundColor
54
+ }
55
+
56
+ private fun configureBorderPaint(): Paint =
57
+ sharedBorderPaint.apply {
58
+ strokeWidth = codeBlockStyle.borderWidth
59
+ color = codeBlockStyle.borderColor
60
+ }
61
+
62
+ override fun getLeadingMargin(first: Boolean): Int = codeBlockStyle.padding.toInt()
63
+
64
+ override fun drawLeadingMargin(
65
+ c: Canvas?,
66
+ p: Paint?,
67
+ x: Int,
68
+ dir: Int,
69
+ top: Int,
70
+ baseline: Int,
71
+ bottom: Int,
72
+ text: CharSequence?,
73
+ start: Int,
74
+ end: Int,
75
+ first: Boolean,
76
+ layout: Layout?,
77
+ ) { /* Leading margin is handled by getLeadingMargin */ }
78
+
79
+ override fun updateMeasureState(tp: TextPaint) = applyTextStyle(tp)
80
+
81
+ override fun updateDrawState(tp: TextPaint) = applyTextStyle(tp)
82
+
83
+ override fun drawBackground(
84
+ canvas: Canvas,
85
+ p: Paint,
86
+ left: Int,
87
+ right: Int,
88
+ top: Int,
89
+ baseline: Int,
90
+ bottom: Int,
91
+ text: CharSequence,
92
+ start: Int,
93
+ end: Int,
94
+ lineNum: Int,
95
+ ) {
96
+ if (text !is Spanned) return
97
+
98
+ val spanStart = text.getSpanStart(this)
99
+ val spanEnd = text.getSpanEnd(this)
100
+ if (spanStart !in 0 until spanEnd) return
101
+
102
+ val isFirstLine = start == spanStart
103
+ val isLastLine = end == spanEnd || (spanEnd <= end && (spanEnd == text.length || text[spanEnd - 1] == '\n'))
104
+
105
+ val inset = codeBlockStyle.borderWidth / 2f
106
+
107
+ rect.set(
108
+ left.toFloat() + inset,
109
+ top.toFloat() + (if (isFirstLine) inset else 0f),
110
+ right.toFloat() - inset,
111
+ bottom.toFloat() - (if (isLastLine) inset else 0f),
112
+ )
113
+
114
+ val radius = codeBlockStyle.borderRadius
115
+ val adjRadius = if (radius > inset) radius - inset else radius
116
+
117
+ // Reset and fill radii array based on boundary state
118
+ radiiArray.fill(0f)
119
+ if (isFirstLine) {
120
+ radiiArray[0] = adjRadius
121
+ radiiArray[1] = adjRadius // Top-Left
122
+ radiiArray[2] = adjRadius
123
+ radiiArray[3] = adjRadius // Top-Right
124
+ }
125
+ if (isLastLine) {
126
+ radiiArray[4] = adjRadius
127
+ radiiArray[5] = adjRadius // Bottom-Right
128
+ radiiArray[6] = adjRadius
129
+ radiiArray[7] = adjRadius // Bottom-Left
130
+ }
131
+
132
+ path.reset()
133
+ path.addRoundRect(rect, radiiArray, Path.Direction.CW)
134
+
135
+ val backgroundPaint = configureBackgroundPaint()
136
+ val borderPaint = configureBorderPaint()
137
+
138
+ canvas.withSave {
139
+ drawPath(path, backgroundPaint)
140
+
141
+ if (codeBlockStyle.borderWidth > 0) {
142
+ val bLeft = rect.left
143
+ val bRight = rect.right
144
+ val bTop = rect.top
145
+ val bBottom = rect.bottom
146
+
147
+ when {
148
+ // Case: Single-line code block
149
+ isFirstLine && isLastLine -> {
150
+ drawPath(path, borderPaint)
151
+ }
152
+
153
+ // Case: Top of a multi-line block
154
+ isFirstLine -> {
155
+ drawLine(bLeft + adjRadius, bTop, bRight - adjRadius, bTop, borderPaint)
156
+ drawLine(bLeft, bTop + adjRadius, bLeft, bBottom, borderPaint)
157
+ drawLine(bRight, bTop + adjRadius, bRight, bBottom, borderPaint)
158
+
159
+ arcRect.set(bLeft, bTop, bLeft + 2 * adjRadius, bTop + 2 * adjRadius)
160
+ drawArc(arcRect, 180f, 90f, false, borderPaint)
161
+
162
+ arcRect.set(bRight - 2 * adjRadius, bTop, bRight, bTop + 2 * adjRadius)
163
+ drawArc(arcRect, 270f, 90f, false, borderPaint)
164
+ }
165
+
166
+ // Case: Bottom of a multi-line block
167
+ isLastLine -> {
168
+ drawLine(bLeft + adjRadius, bBottom, bRight - adjRadius, bBottom, borderPaint)
169
+ drawLine(bLeft, bTop, bLeft, bBottom - adjRadius, borderPaint)
170
+ drawLine(bRight, bTop, bRight, bBottom - adjRadius, borderPaint)
171
+
172
+ arcRect.set(bLeft, bBottom - 2 * adjRadius, bLeft + 2 * adjRadius, bBottom)
173
+ drawArc(arcRect, 90f, 90f, false, borderPaint)
174
+
175
+ arcRect.set(bRight - 2 * adjRadius, bBottom - 2 * adjRadius, bRight, bBottom)
176
+ drawArc(arcRect, 0f, 90f, false, borderPaint)
177
+ }
178
+
179
+ // Case: Middle lines only need vertical sides
180
+ else -> {
181
+ drawLine(bLeft, bTop, bLeft, bBottom, borderPaint)
182
+ drawLine(bRight, bTop, bRight, bBottom, borderPaint)
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ private fun applyTextStyle(tp: TextPaint) {
190
+ tp.textSize = blockStyle.fontSize
191
+
192
+ tp.applyBlockStyleFont(blockStyle, context)
193
+
194
+ tp.applyColorPreserving(blockStyle.color, *styleCache.colorsToPreserve)
195
+ }
196
+ }
@@ -0,0 +1,27 @@
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
+
9
+ class CodeSpan(
10
+ private val styleCache: SpanStyleCache,
11
+ private val blockStyle: BlockStyle,
12
+ ) : MetricAffectingSpan() {
13
+ override fun updateDrawState(tp: TextPaint) {
14
+ applyMonospacedFont(tp)
15
+ tp.color = styleCache.codeColor
16
+ }
17
+
18
+ override fun updateMeasureState(tp: TextPaint) {
19
+ applyMonospacedFont(tp)
20
+ }
21
+
22
+ private fun applyMonospacedFont(paint: TextPaint) {
23
+ paint.textSize = blockStyle.fontSize * 0.85f
24
+ val preservedStyle = (paint.typeface?.style ?: 0) and (Typeface.BOLD or Typeface.ITALIC)
25
+ paint.typeface = SpanStyleCache.getMonospaceTypeface(preservedStyle)
26
+ }
27
+ }
@@ -0,0 +1,34 @@
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 EmphasisSpan(
11
+ private val styleCache: SpanStyleCache,
12
+ private val blockStyle: BlockStyle,
13
+ ) : MetricAffectingSpan() {
14
+ override fun updateDrawState(tp: TextPaint) {
15
+ applyEmphasisStyle(tp)
16
+ applyEmphasisColor(tp)
17
+ }
18
+
19
+ override fun updateMeasureState(tp: TextPaint) {
20
+ applyEmphasisStyle(tp)
21
+ }
22
+
23
+ private fun applyEmphasisStyle(tp: TextPaint) {
24
+ val currentTypeface = tp.typeface ?: Typeface.DEFAULT
25
+ val isBold = (currentTypeface.style) and Typeface.BOLD != 0
26
+ val style = if (isBold) Typeface.BOLD_ITALIC else Typeface.ITALIC
27
+ tp.typeface = Typeface.create(currentTypeface, style)
28
+ }
29
+
30
+ private fun applyEmphasisColor(tp: TextPaint) {
31
+ val colorToUse = styleCache.getEmphasisColorFor(blockStyle.color, tp.color)
32
+ tp.applyColorPreserving(colorToUse, *styleCache.colorsToPreserve)
33
+ }
34
+ }
@@ -0,0 +1,38 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.graphics.Typeface
5
+ import android.text.TextPaint
6
+ import android.text.style.MetricAffectingSpan
7
+ import com.swmansion.enriched.markdown.styles.StyleConfig
8
+
9
+ class HeadingSpan(
10
+ val level: Int,
11
+ styleConfig: StyleConfig,
12
+ ) : MetricAffectingSpan() {
13
+ private val fontSize: Float = styleConfig.headingStyles[level]!!.fontSize
14
+ private val color: Int = styleConfig.headingStyles[level]!!.color
15
+ private val cachedTypeface: Typeface? = styleConfig.headingTypefaces[level]
16
+
17
+ override fun updateDrawState(tp: TextPaint) {
18
+ applyHeadingStyle(tp)
19
+ tp.color = color
20
+ }
21
+
22
+ override fun updateMeasureState(tp: TextPaint) {
23
+ applyHeadingStyle(tp)
24
+ }
25
+
26
+ @SuppressLint("WrongConstant") // Result of mask is always valid: 0, 1, 2, or 3
27
+ private fun applyHeadingStyle(tp: TextPaint) {
28
+ tp.textSize = fontSize
29
+ cachedTypeface?.let { base ->
30
+ val preserved = (tp.typeface?.style ?: 0) and BOLD_ITALIC_MASK
31
+ tp.typeface = if (preserved != 0) Typeface.create(base, preserved) else base
32
+ }
33
+ }
34
+
35
+ companion object {
36
+ private const val BOLD_ITALIC_MASK = Typeface.BOLD or Typeface.ITALIC
37
+ }
38
+ }
@@ -0,0 +1,320 @@
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
+ ) : AndroidImageSpan(
32
+ createInitialDrawable(styleConfig, imageUrl, isInline),
33
+ imageUrl,
34
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ALIGN_CENTER else ALIGN_BASELINE,
35
+ ),
36
+ AndroidLineHeightSpan {
37
+ private var loadedDrawable: Drawable? = null
38
+ private val imageStyle = styleConfig.imageStyle
39
+ private val height: Int = if (isInline) calculateInlineImageSize(styleConfig) else imageStyle.height.toInt()
40
+ private val borderRadiusPx: Int = (imageStyle.borderRadius * context.resources.displayMetrics.density).toInt()
41
+
42
+ private var cachedWidth: Int = MINIMUM_VALID_DIMENSION
43
+ private val initialDrawable: Drawable = super.getDrawable()
44
+ private var viewRef: WeakReference<EnrichedMarkdownText>? = null
45
+
46
+ init {
47
+ setupLoadingLogic()
48
+ }
49
+
50
+ private fun setupLoadingLogic() {
51
+ val d = initialDrawable
52
+ if (d is AsyncDrawable) {
53
+ // Set up the callback immediately. If already loaded, it triggers next frame.
54
+ d.onLoaded = { handleImageLoaded(d) }
55
+ if (d.isLoaded) handleImageLoaded(d)
56
+ } else if (d.intrinsicWidth > 0) {
57
+ // Local file or resource
58
+ wrapAndAssignDrawable(d)
59
+ }
60
+ }
61
+
62
+ private fun handleImageLoaded(asyncDrawable: AsyncDrawable) {
63
+ val rawDrawable = asyncDrawable.internalDrawable
64
+ wrapAndAssignDrawable(rawDrawable)
65
+ }
66
+
67
+ private fun wrapAndAssignDrawable(base: Drawable) {
68
+ val view = viewRef?.get()
69
+ val targetWidth =
70
+ if (isInline) {
71
+ height
72
+ } else {
73
+ val available = view?.let { getAvailableWidth(it) } ?: cachedWidth
74
+ available.coerceAtLeast(MINIMUM_VALID_DIMENSION)
75
+ }
76
+
77
+ loadedDrawable =
78
+ ScaledImageDrawable(
79
+ imageDrawable = base,
80
+ targetWidth = targetWidth,
81
+ targetHeight = height,
82
+ borderRadius = borderRadiusPx,
83
+ isBlockImage = !isInline,
84
+ )
85
+ requestReflow()
86
+ }
87
+
88
+ private fun requestReflow() {
89
+ val view = viewRef?.get() ?: return
90
+ val text = view.text
91
+ if (text is Spannable) {
92
+ val start = text.getSpanStart(this)
93
+ val end = text.getSpanEnd(this)
94
+ if (start != -1 && end != -1) {
95
+ // Notifying the spannable that the span changed triggers a re-layout
96
+ text.setSpan(this, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
97
+ }
98
+ } else {
99
+ view.invalidate()
100
+ view.requestLayout()
101
+ }
102
+ }
103
+
104
+ fun registerTextView(view: EnrichedMarkdownText) {
105
+ viewRef = WeakReference(view)
106
+ if (!isInline) {
107
+ val availableWidth = getAvailableWidth(view)
108
+ if (availableWidth > MINIMUM_VALID_DIMENSION) {
109
+ updateWidthAndRecreate(availableWidth)
110
+ }
111
+ // Ensure we catch the width after the first layout pass
112
+ view.post {
113
+ val postWidth = getAvailableWidth(view)
114
+ if (postWidth != cachedWidth) updateWidthAndRecreate(postWidth)
115
+ }
116
+ }
117
+ }
118
+
119
+ private fun updateWidthAndRecreate(newWidth: Int) {
120
+ if (newWidth <= MINIMUM_VALID_DIMENSION || cachedWidth == newWidth) return
121
+ cachedWidth = newWidth
122
+
123
+ // If we already have a loaded source, recreate the scaled wrapper with new width
124
+ val base = (initialDrawable as? AsyncDrawable)?.internalDrawable ?: initialDrawable
125
+ if (base.intrinsicWidth > 0) {
126
+ wrapAndAssignDrawable(base)
127
+ }
128
+ }
129
+
130
+ private fun getAvailableWidth(view: EnrichedMarkdownText): Int = view.layout?.width ?: view.width
131
+
132
+ override fun getDrawable(): Drawable {
133
+ val drawable = loadedDrawable ?: initialDrawable
134
+ if (drawable !is ScaledImageDrawable) {
135
+ val dWidth = if (isInline) height else cachedWidth.takeIf { it > 0 } ?: drawable.intrinsicWidth
136
+ val dHeight = if (isInline) height else height
137
+ drawable.setBounds(0, 0, dWidth.coerceAtLeast(0), dHeight.coerceAtLeast(0))
138
+ }
139
+ return drawable
140
+ }
141
+
142
+ override fun getSize(
143
+ paint: Paint,
144
+ text: CharSequence?,
145
+ start: Int,
146
+ end: Int,
147
+ fm: Paint.FontMetricsInt?,
148
+ ): Int = getDrawable().bounds.right
149
+
150
+ override fun chooseHeight(
151
+ text: CharSequence?,
152
+ start: Int,
153
+ end: Int,
154
+ spanstartv: Int,
155
+ lineHeight: Int,
156
+ fm: Paint.FontMetricsInt?,
157
+ ) {
158
+ if (fm == null || isInline) return
159
+ val currentLineHeight = fm.descent - fm.ascent
160
+ if (height > currentLineHeight) {
161
+ val extraHeight = height - currentLineHeight
162
+ fm.descent += extraHeight
163
+ fm.bottom += extraHeight
164
+ }
165
+ }
166
+
167
+ override fun draw(
168
+ canvas: Canvas,
169
+ text: CharSequence?,
170
+ start: Int,
171
+ end: Int,
172
+ x: Float,
173
+ top: Int,
174
+ y: Int,
175
+ bottom: Int,
176
+ paint: Paint,
177
+ ) {
178
+ val drawable = getDrawable()
179
+ canvas.withSave {
180
+ if (isInline) {
181
+ val imageHeight = drawable.bounds.height()
182
+ translate(x, (y - imageHeight + (imageHeight * 0.1f)))
183
+ } else {
184
+ translate(x, top.toFloat())
185
+ }
186
+ drawable.draw(this)
187
+ }
188
+ }
189
+
190
+ // --- Helper Classes ---
191
+
192
+ private class ScaledImageDrawable(
193
+ private val imageDrawable: Drawable,
194
+ private val targetWidth: Int,
195
+ private val targetHeight: Int,
196
+ private val borderRadius: Int,
197
+ isBlockImage: Boolean,
198
+ ) : Drawable() {
199
+ private val clipPath: Path? =
200
+ if (borderRadius > 0) {
201
+ Path().apply {
202
+ addRoundRect(
203
+ 0f,
204
+ 0f,
205
+ targetWidth.toFloat(),
206
+ targetHeight.toFloat(),
207
+ borderRadius.toFloat(),
208
+ borderRadius.toFloat(),
209
+ Path.Direction.CW,
210
+ )
211
+ }
212
+ } else {
213
+ null
214
+ }
215
+
216
+ init {
217
+ setBounds(0, 0, targetWidth, targetHeight)
218
+ val iW = imageDrawable.intrinsicWidth
219
+ val iH = imageDrawable.intrinsicHeight
220
+
221
+ val (sW, sH) =
222
+ if (iW > 0 && iH > 0) {
223
+ if (isBlockImage) {
224
+ val scale = targetWidth.toFloat() / iW
225
+ targetWidth to (iH * scale).toInt()
226
+ } else {
227
+ val scale = minOf(targetWidth.toFloat() / iW, targetHeight.toFloat() / iH)
228
+ (iW * scale).toInt() to (iH * scale).toInt()
229
+ }
230
+ } else {
231
+ targetWidth to targetHeight
232
+ }
233
+
234
+ val left = (targetWidth - sW) / 2
235
+ val top = (targetHeight - sH) / 2
236
+ imageDrawable.setBounds(left, top, left + sW, top + sH)
237
+ }
238
+
239
+ override fun draw(canvas: Canvas) {
240
+ if (clipPath != null) {
241
+ canvas.withSave {
242
+ clipPath(clipPath)
243
+ imageDrawable.draw(canvas)
244
+ }
245
+ } else {
246
+ imageDrawable.draw(canvas)
247
+ }
248
+ }
249
+
250
+ override fun setAlpha(alpha: Int) {
251
+ imageDrawable.alpha = alpha
252
+ }
253
+
254
+ override fun setColorFilter(cf: android.graphics.ColorFilter?) {
255
+ imageDrawable.colorFilter = cf
256
+ }
257
+
258
+ @Suppress("DEPRECATION")
259
+ @Deprecated("Deprecated in Java")
260
+ override fun getOpacity(): Int = imageDrawable.opacity
261
+
262
+ override fun getIntrinsicWidth(): Int = targetWidth
263
+
264
+ override fun getIntrinsicHeight(): Int = targetHeight
265
+ }
266
+
267
+ companion object {
268
+ private const val MINIMUM_VALID_DIMENSION = 0
269
+
270
+ private fun calculateInlineImageSize(style: StyleConfig): Int = style.inlineImageStyle.size.toInt()
271
+
272
+ private fun createInitialDrawable(
273
+ style: StyleConfig,
274
+ url: String,
275
+ isInline: Boolean,
276
+ ): Drawable {
277
+ val imgStyle = style.imageStyle
278
+ val size = if (isInline) calculateInlineImageSize(style) else imgStyle.height.toInt()
279
+
280
+ return prepareDrawable(url, size, size) ?: PlaceholderDrawable(size, size)
281
+ }
282
+
283
+ private fun prepareDrawable(
284
+ src: String,
285
+ tw: Int,
286
+ th: Int,
287
+ ): Drawable? {
288
+ if (src.startsWith("http")) {
289
+ return AsyncDrawable(src).apply { setBounds(0, 0, tw, th) }
290
+ }
291
+ val path = src.removePrefix("file://")
292
+ return try {
293
+ BitmapFactory.decodeFile(path)?.toDrawable(Resources.getSystem())?.apply {
294
+ setBounds(0, 0, intrinsicWidth, intrinsicHeight)
295
+ }
296
+ } catch (e: Exception) {
297
+ Log.w("ImageSpan", "Failed to load local image: $path", e)
298
+ null
299
+ }
300
+ }
301
+
302
+ private class PlaceholderDrawable(
303
+ private val w: Int,
304
+ private val h: Int,
305
+ ) : Drawable() {
306
+ override fun draw(canvas: Canvas) {}
307
+
308
+ override fun setAlpha(alpha: Int) {}
309
+
310
+ override fun setColorFilter(cf: android.graphics.ColorFilter?) {}
311
+
312
+ @Deprecated("Deprecated in Java")
313
+ override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSLUCENT
314
+
315
+ override fun getIntrinsicWidth(): Int = w
316
+
317
+ override fun getIntrinsicHeight(): Int = h
318
+ }
319
+ }
320
+ }
@@ -0,0 +1,36 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.graphics.Paint
4
+ import kotlin.math.ceil
5
+ import kotlin.math.roundToInt
6
+ import android.text.style.LineHeightSpan as AndroidLineHeightSpan
7
+
8
+ /**
9
+ * Custom LineHeightSpan for Android API levels below 29.
10
+ * Matches LineHeightSpan.Standard behavior for consistent rendering across all API levels.
11
+ */
12
+ class LineHeightSpan(
13
+ private val lineHeight: Float,
14
+ ) : AndroidLineHeightSpan {
15
+ override fun chooseHeight(
16
+ text: CharSequence?,
17
+ start: Int,
18
+ end: Int,
19
+ spanstartv: Int,
20
+ lineHeight: Int,
21
+ fm: Paint.FontMetricsInt?,
22
+ ) {
23
+ if (fm == null) return
24
+
25
+ val targetHeight = ceil(this.lineHeight.toDouble()).toInt()
26
+ val originHeight = fm.descent - fm.ascent
27
+
28
+ if (originHeight <= 0) {
29
+ return
30
+ }
31
+
32
+ val ratio = targetHeight.toFloat() / originHeight
33
+ fm.descent = (fm.descent * ratio).roundToInt()
34
+ fm.ascent = fm.descent - targetHeight
35
+ }
36
+ }
@@ -0,0 +1,37 @@
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 styleCache: SpanStyleCache,
16
+ private val blockStyle: BlockStyle,
17
+ private val context: Context,
18
+ ) : ClickableSpan() {
19
+ override fun onClick(widget: View) {
20
+ if (onLinkPress != null) {
21
+ onLinkPress(url)
22
+ } else if (widget is EnrichedMarkdownText) {
23
+ // Emit event directly from view (enriched pattern)
24
+ widget.emitOnLinkPress(url)
25
+ }
26
+ }
27
+
28
+ override fun updateDrawState(textPaint: TextPaint) {
29
+ super.updateDrawState(textPaint)
30
+
31
+ textPaint.textSize = blockStyle.fontSize
32
+ textPaint.applyBlockStyleFont(blockStyle, context)
33
+
34
+ textPaint.color = styleCache.linkColor
35
+ textPaint.isUnderlineText = styleCache.linkUnderline
36
+ }
37
+ }