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,279 @@
1
+ package com.swmansion.enriched.markdown.accessibility
2
+
3
+ import android.graphics.Rect
4
+ import android.os.Bundle
5
+ import android.text.Spanned
6
+ import android.widget.TextView
7
+ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
8
+ import androidx.customview.widget.ExploreByTouchHelper
9
+ import com.swmansion.enriched.markdown.spans.BaseListSpan
10
+ import com.swmansion.enriched.markdown.spans.HeadingSpan
11
+ import com.swmansion.enriched.markdown.spans.ImageSpan
12
+ import com.swmansion.enriched.markdown.spans.LinkSpan
13
+ import com.swmansion.enriched.markdown.spans.OrderedListSpan
14
+ import com.swmansion.enriched.markdown.spans.UnorderedListSpan
15
+
16
+ class MarkdownAccessibilityHelper(
17
+ private val textView: TextView,
18
+ ) : ExploreByTouchHelper(textView) {
19
+ private var accessibilityItems: List<AccessibilityItem> = emptyList()
20
+ private var needsRebuild = false
21
+ private var lastLayoutHashCode = 0
22
+
23
+ data class AccessibilityItem(
24
+ val id: Int,
25
+ val text: String,
26
+ val start: Int,
27
+ val end: Int,
28
+ val headingLevel: Int = 0,
29
+ val linkUrl: String? = null,
30
+ val listInfo: ListItemInfo? = null,
31
+ val imageAltText: String? = null,
32
+ ) {
33
+ val isHeading get() = headingLevel > 0
34
+ val isLink get() = linkUrl != null
35
+ val isListItem get() = listInfo != null
36
+ val isImage get() = imageAltText != null
37
+ }
38
+
39
+ data class ListItemInfo(
40
+ val isOrdered: Boolean,
41
+ val itemNumber: Int,
42
+ val depth: Int,
43
+ )
44
+
45
+ private data class SpanRange(
46
+ val start: Int,
47
+ val end: Int,
48
+ val headingLevel: Int = 0,
49
+ val linkUrl: String? = null,
50
+ val imageAltText: String? = null,
51
+ )
52
+
53
+ fun invalidateAccessibilityItems() {
54
+ needsRebuild = true
55
+ rebuildIfNeeded()
56
+ invalidateRoot()
57
+ }
58
+
59
+ private fun rebuildIfNeeded() {
60
+ val layout = textView.layout ?: return
61
+ if (needsRebuild || lastLayoutHashCode != layout.hashCode()) {
62
+ accessibilityItems = buildAccessibilityItems()
63
+ needsRebuild = false
64
+ lastLayoutHashCode = layout.hashCode()
65
+ }
66
+ }
67
+
68
+ private fun buildAccessibilityItems(): List<AccessibilityItem> {
69
+ val spanned = textView.text as? Spanned ?: return emptyList()
70
+ if (spanned.isEmpty()) return emptyList()
71
+
72
+ val items = mutableListOf<AccessibilityItem>()
73
+ var nextId = 0
74
+
75
+ // Consolidated span collection using functional mapping
76
+ val semanticSpans =
77
+ (
78
+ spanned.getSpans(0, spanned.length, HeadingSpan::class.java).map {
79
+ SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), headingLevel = it.level)
80
+ } +
81
+ spanned.getSpans(0, spanned.length, LinkSpan::class.java).map {
82
+ SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), linkUrl = it.url)
83
+ } +
84
+ spanned.getSpans(0, spanned.length, ImageSpan::class.java).map {
85
+ SpanRange(spanned.getSpanStart(it), spanned.getSpanEnd(it), imageAltText = it.altText)
86
+ }
87
+ ).sortedBy { it.start }
88
+
89
+ var currentPos = 0
90
+ for (span in semanticSpans) {
91
+ if (span.start < currentPos) continue
92
+
93
+ if (currentPos < span.start) {
94
+ nextId = addTextSegments(items, spanned, currentPos, span.start, nextId)
95
+ }
96
+
97
+ val content = span.imageAltText?.ifEmpty { "Image" } ?: spanned.substring(span.start, span.end).trim()
98
+
99
+ if (content.isNotEmpty()) {
100
+ val listContext =
101
+ if (span.headingLevel > 0 || span.imageAltText != null) {
102
+ null
103
+ } else {
104
+ getListInfoAt(spanned, span.start, span.linkUrl == null)
105
+ }
106
+ items.add(
107
+ AccessibilityItem(
108
+ nextId++,
109
+ content,
110
+ span.start,
111
+ span.end,
112
+ span.headingLevel,
113
+ span.linkUrl,
114
+ listContext,
115
+ span.imageAltText,
116
+ ),
117
+ )
118
+ }
119
+ currentPos = span.end
120
+ }
121
+
122
+ if (currentPos < spanned.length) addTextSegments(items, spanned, currentPos, spanned.length, nextId)
123
+ return items.ifEmpty { listOf(AccessibilityItem(0, spanned.toString().trim(), 0, spanned.length)) }
124
+ }
125
+
126
+ private fun getListInfoAt(
127
+ spanned: Spanned,
128
+ position: Int,
129
+ requireStart: Boolean,
130
+ ): ListItemInfo? {
131
+ val deepest = spanned.getSpans(position, position + 1, BaseListSpan::class.java).maxByOrNull { it.depth } ?: return null
132
+ if (requireStart) {
133
+ val start = spanned.getSpanStart(deepest)
134
+ val firstChar = (start until minOf(start + 10, spanned.length)).firstOrNull { !spanned[it].isWhitespace() } ?: start
135
+ if (position > firstChar + 1) return null
136
+ }
137
+ return ListItemInfo(deepest is OrderedListSpan, (deepest as? OrderedListSpan)?.itemNumber ?: 0, deepest.depth)
138
+ }
139
+
140
+ private fun addTextSegments(
141
+ items: MutableList<AccessibilityItem>,
142
+ spanned: Spanned,
143
+ start: Int,
144
+ end: Int,
145
+ startId: Int,
146
+ ): Int {
147
+ var cid = startId
148
+ val layout = textView.layout ?: return cid
149
+ for (line in layout.getLineForOffset(start)..layout.getLineForOffset(end)) {
150
+ val s = maxOf(start, layout.getLineStart(line))
151
+ val e = minOf(end, layout.getLineEnd(line))
152
+ if (s >= e) continue
153
+
154
+ val raw = spanned.substring(s, e)
155
+ val first = raw.indexOfFirst { !it.isWhitespace() }
156
+ if (first != -1) {
157
+ val last = raw.indexOfLast { !it.isWhitespace() }
158
+ val absoluteStart = s + first
159
+ items.add(
160
+ AccessibilityItem(cid++, raw.trim(), absoluteStart, s + last + 1, listInfo = getListInfoAt(spanned, absoluteStart, true)),
161
+ )
162
+ }
163
+ }
164
+ return cid
165
+ }
166
+
167
+ override fun getVirtualViewAt(
168
+ x: Float,
169
+ y: Float,
170
+ ): Int {
171
+ rebuildIfNeeded()
172
+ val offset = getOffsetForPosition(x, y)
173
+ return accessibilityItems
174
+ .filter { offset in it.start until it.end }
175
+ .minByOrNull {
176
+ when {
177
+ it.isLink -> 0
178
+ it.isImage -> 1
179
+ it.isHeading -> 2
180
+ it.isListItem -> 3
181
+ else -> 4
182
+ }
183
+ }?.id ?: HOST_ID
184
+ }
185
+
186
+ override fun getVisibleVirtualViews(ids: MutableList<Int>) {
187
+ rebuildIfNeeded()
188
+ accessibilityItems.forEach { ids.add(it.id) }
189
+ }
190
+
191
+ override fun onPopulateNodeForVirtualView(
192
+ id: Int,
193
+ node: AccessibilityNodeInfoCompat,
194
+ ) {
195
+ val item = accessibilityItems.find { it.id == id } ?: return
196
+ node.apply {
197
+ text = item.text
198
+ contentDescription = item.text
199
+ isFocusable = true
200
+ isScreenReaderFocusable = true
201
+ setBoundsInParent(getBoundsForRange(item.start, item.end))
202
+
203
+ item.listInfo?.let { info ->
204
+ setCollectionItemInfo(
205
+ AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(info.itemNumber - 1, 1, 0, 1, false, false),
206
+ )
207
+ }
208
+
209
+ val prefix = if (item.listInfo?.depth ?: 0 > 0) "nested " else ""
210
+ val listText = if (item.listInfo?.isOrdered == true) "list item ${item.listInfo.itemNumber}" else "bullet point"
211
+
212
+ when {
213
+ item.isHeading -> {
214
+ isHeading = true
215
+ contentDescription = "${item.text}, heading level ${item.headingLevel}"
216
+ }
217
+
218
+ item.isImage -> {
219
+ roleDescription = "image"
220
+ }
221
+
222
+ item.isLink -> {
223
+ isClickable = true
224
+ addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
225
+ roleDescription = item.listInfo?.let { "link, $prefix$listText" } ?: "link"
226
+ }
227
+
228
+ item.isListItem -> {
229
+ roleDescription = "$prefix$listText"
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ override fun onPerformActionForVirtualView(
236
+ id: Int,
237
+ action: Int,
238
+ args: Bundle?,
239
+ ): Boolean {
240
+ val item = accessibilityItems.find { it.id == id } ?: return false
241
+ if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && item.isLink) {
242
+ (textView.text as? Spanned)?.getSpans(item.start, item.end, LinkSpan::class.java)?.firstOrNull()?.onClick(textView)
243
+ ?: return false
244
+ return true
245
+ }
246
+ return false
247
+ }
248
+
249
+ private fun getOffsetForPosition(
250
+ x: Float,
251
+ y: Float,
252
+ ): Int {
253
+ val layout = textView.layout ?: return 0
254
+ return layout.getOffsetForHorizontal(layout.getLineForVertical(y.toInt()).coerceIn(0, layout.lineCount - 1), x)
255
+ }
256
+
257
+ private fun getBoundsForRange(
258
+ start: Int,
259
+ end: Int,
260
+ ): Rect {
261
+ val layout = textView.layout ?: return Rect()
262
+ val line = layout.getLineForOffset(start)
263
+ val left = layout.getPrimaryHorizontal(start).toInt() + textView.paddingLeft
264
+ val right =
265
+ if (layout.getPrimaryHorizontal(end) <=
266
+ layout.getPrimaryHorizontal(start)
267
+ ) {
268
+ layout.getLineRight(line).toInt() + textView.paddingLeft
269
+ } else {
270
+ layout.getPrimaryHorizontal(end).toInt() + textView.paddingLeft
271
+ }
272
+ return Rect(
273
+ left,
274
+ layout.getLineTop(line) + textView.paddingTop,
275
+ right,
276
+ layout.getLineBottom(layout.getLineForOffset(end)) + textView.paddingTop,
277
+ )
278
+ }
279
+ }
@@ -0,0 +1,23 @@
1
+ package com.swmansion.enriched.markdown.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class LinkLongPressEvent(
8
+ surfaceId: Int,
9
+ viewId: Int,
10
+ private val url: String,
11
+ ) : Event<LinkLongPressEvent>(surfaceId, viewId) {
12
+ override fun getEventName(): String = EVENT_NAME
13
+
14
+ override fun getEventData(): WritableMap {
15
+ val eventData: WritableMap = Arguments.createMap()
16
+ eventData.putString("url", url)
17
+ return eventData
18
+ }
19
+
20
+ companion object {
21
+ const val EVENT_NAME: String = "onLinkLongPress"
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ package com.swmansion.enriched.markdown.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class LinkPressEvent(
8
+ surfaceId: Int,
9
+ viewId: Int,
10
+ private val url: String,
11
+ ) : Event<LinkPressEvent>(surfaceId, viewId) {
12
+ override fun getEventName(): String = EVENT_NAME
13
+
14
+ override fun getEventData(): WritableMap {
15
+ val eventData: WritableMap = Arguments.createMap()
16
+ eventData.putString("url", url)
17
+ return eventData
18
+ }
19
+
20
+ companion object {
21
+ const val EVENT_NAME: String = "onLinkPress"
22
+ }
23
+ }
@@ -0,0 +1,31 @@
1
+ package com.swmansion.enriched.markdown.parser
2
+
3
+ data class MarkdownASTNode(
4
+ val type: NodeType,
5
+ val content: String = "",
6
+ val attributes: Map<String, String> = emptyMap(),
7
+ val children: List<MarkdownASTNode> = emptyList(),
8
+ ) {
9
+ enum class NodeType {
10
+ Document,
11
+ Paragraph,
12
+ Text,
13
+ Link,
14
+ Heading,
15
+ LineBreak,
16
+ Strong,
17
+ Emphasis,
18
+ Strikethrough,
19
+ Underline,
20
+ Code,
21
+ Image,
22
+ Blockquote,
23
+ UnorderedList,
24
+ OrderedList,
25
+ ListItem,
26
+ CodeBlock,
27
+ ThematicBreak,
28
+ }
29
+
30
+ fun getAttribute(key: String): String? = attributes[key]
31
+ }
@@ -0,0 +1,62 @@
1
+ package com.swmansion.enriched.markdown.parser
2
+
3
+ import android.util.Log
4
+
5
+ data class Md4cFlags(
6
+ val underline: Boolean = false,
7
+ ) {
8
+ companion object {
9
+ val DEFAULT = Md4cFlags()
10
+ }
11
+ }
12
+
13
+ class Parser {
14
+ companion object {
15
+ init {
16
+ try {
17
+ // Library name must follow react_codegen_<ComponentName> convention
18
+ // required by React Native Fabric's CMake build system.
19
+ // md4c parser is bundled in the same shared library to avoid
20
+ // multiple library loading and complex linking dependencies.
21
+ System.loadLibrary("react_codegen_EnrichedMarkdownTextSpec")
22
+ } catch (e: UnsatisfiedLinkError) {
23
+ Log.e("MarkdownParser", "Failed to load native library", e)
24
+ }
25
+ }
26
+
27
+ @JvmStatic
28
+ private external fun nativeParseMarkdown(
29
+ markdown: String,
30
+ flags: Md4cFlags,
31
+ ): MarkdownASTNode?
32
+
33
+ /**
34
+ * Shared parser instance. Parser is stateless and thread-safe, so it can be reused
35
+ * across all EnrichedMarkdownText instances to avoid unnecessary allocations.
36
+ */
37
+ val shared: Parser = Parser()
38
+ }
39
+
40
+ fun parseMarkdown(
41
+ markdown: String,
42
+ flags: Md4cFlags = Md4cFlags.DEFAULT,
43
+ ): MarkdownASTNode? {
44
+ if (markdown.isBlank()) {
45
+ return null
46
+ }
47
+
48
+ try {
49
+ val ast = nativeParseMarkdown(markdown, flags)
50
+
51
+ if (ast != null) {
52
+ return ast
53
+ } else {
54
+ Log.w("MarkdownParser", "Native parser returned null")
55
+ return null
56
+ }
57
+ } catch (e: Exception) {
58
+ Log.e("MarkdownParser", "MD4C parsing failed: ${e.message}", e)
59
+ return null
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,166 @@
1
+ package com.swmansion.enriched.markdown.renderer
2
+
3
+ import com.swmansion.enriched.markdown.styles.BaseBlockStyle
4
+ import com.swmansion.enriched.markdown.styles.BlockquoteStyle
5
+ import com.swmansion.enriched.markdown.styles.CodeBlockStyle
6
+ import com.swmansion.enriched.markdown.styles.HeadingStyle
7
+ import com.swmansion.enriched.markdown.styles.ListStyle
8
+ import com.swmansion.enriched.markdown.styles.ParagraphStyle
9
+
10
+ enum class BlockType {
11
+ NONE,
12
+ PARAGRAPH,
13
+ HEADING,
14
+ BLOCKQUOTE,
15
+ UNORDERED_LIST,
16
+ ORDERED_LIST,
17
+ CODE_BLOCK,
18
+ }
19
+
20
+ data class BlockStyle(
21
+ val fontSize: Float,
22
+ val fontFamily: String,
23
+ val fontWeight: String,
24
+ val color: Int,
25
+ )
26
+
27
+ private class MutableBlockStyle {
28
+ var fontSize: Float = 0f
29
+ var fontFamily: String = ""
30
+ var fontWeight: String = ""
31
+ var color: Int = 0
32
+ var isDirty: Boolean = false
33
+
34
+ fun updateFrom(style: BaseBlockStyle) {
35
+ fontSize = style.fontSize
36
+ fontFamily = style.fontFamily
37
+ fontWeight = style.fontWeight
38
+ color = style.color
39
+ isDirty = true
40
+ }
41
+
42
+ fun toImmutable(): BlockStyle = BlockStyle(fontSize, fontFamily, fontWeight, color)
43
+
44
+ fun clear() {
45
+ isDirty = false
46
+ }
47
+ }
48
+
49
+ class BlockStyleContext {
50
+ var currentBlockType = BlockType.NONE
51
+ private set
52
+
53
+ private val mutableBlockStyle = MutableBlockStyle()
54
+ private var cachedBlockStyle: BlockStyle? = null
55
+ private var currentHeadingLevel = 0
56
+
57
+ var blockquoteDepth = 0
58
+ var listDepth = 0
59
+ var listType: ListType? = null
60
+ var listItemNumber = 0
61
+
62
+ // Optimization: ArrayDeque is more efficient for stack operations than MutableList
63
+ private val orderedListItemNumbers = ArrayDeque<Int>()
64
+
65
+ enum class ListType { UNORDERED, ORDERED }
66
+
67
+ private fun updateBlockStyle(
68
+ type: BlockType,
69
+ style: BaseBlockStyle,
70
+ headingLevel: Int = 0,
71
+ ) {
72
+ currentBlockType = type
73
+ currentHeadingLevel = headingLevel
74
+ // Update mutable style in place - no allocation here
75
+ mutableBlockStyle.updateFrom(style)
76
+ // Invalidate cached immutable copy
77
+ cachedBlockStyle = null
78
+ }
79
+
80
+ // Unified Setters
81
+ fun setParagraphStyle(style: ParagraphStyle) = updateBlockStyle(BlockType.PARAGRAPH, style)
82
+
83
+ fun setHeadingStyle(
84
+ style: HeadingStyle,
85
+ level: Int,
86
+ ) = updateBlockStyle(BlockType.HEADING, style, level)
87
+
88
+ fun setBlockquoteStyle(style: BlockquoteStyle) = updateBlockStyle(BlockType.BLOCKQUOTE, style)
89
+
90
+ fun setUnorderedListStyle(style: ListStyle) {
91
+ listType = ListType.UNORDERED
92
+ updateBlockStyle(BlockType.UNORDERED_LIST, style)
93
+ }
94
+
95
+ fun setOrderedListStyle(style: ListStyle) {
96
+ listType = ListType.ORDERED
97
+ updateBlockStyle(BlockType.ORDERED_LIST, style)
98
+ }
99
+
100
+ fun setCodeBlockStyle(style: CodeBlockStyle) = updateBlockStyle(BlockType.CODE_BLOCK, style)
101
+
102
+ // List State Management
103
+ fun isInsideBlockElement(): Boolean = blockquoteDepth > 0 || listDepth > 0
104
+
105
+ fun incrementListItemNumber() {
106
+ listItemNumber++
107
+ }
108
+
109
+ fun resetListItemNumber() {
110
+ listItemNumber = 0
111
+ }
112
+
113
+ // Using ArrayDeque methods for clarity: addLast/removeLast
114
+ fun pushOrderedListItemNumber() {
115
+ orderedListItemNumbers.addLast(listItemNumber)
116
+ }
117
+
118
+ fun popOrderedListItemNumber() {
119
+ if (orderedListItemNumbers.isNotEmpty()) {
120
+ listItemNumber = orderedListItemNumbers.removeLast()
121
+ }
122
+ }
123
+
124
+ fun clearListStyle() {
125
+ // Only trigger full reset when we have completely exited all nested lists
126
+ if (listDepth == 0) {
127
+ reset()
128
+ }
129
+ }
130
+
131
+ private fun reset() {
132
+ clearBlockStyle()
133
+ listType = null
134
+ listItemNumber = 0
135
+ orderedListItemNumbers.clear()
136
+ }
137
+
138
+ fun requireBlockStyle(): BlockStyle {
139
+ if (!mutableBlockStyle.isDirty) {
140
+ throw IllegalStateException(
141
+ "BlockStyle is null. Inline renderers must be used within a block context.",
142
+ )
143
+ }
144
+ // Create immutable copy only when needed, cache for reuse within same block
145
+ return cachedBlockStyle ?: mutableBlockStyle.toImmutable().also { cachedBlockStyle = it }
146
+ }
147
+
148
+ fun clearBlockStyle() {
149
+ currentBlockType = BlockType.NONE
150
+ mutableBlockStyle.clear()
151
+ cachedBlockStyle = null
152
+ currentHeadingLevel = 0
153
+ }
154
+
155
+ fun resetForNewRender() {
156
+ currentBlockType = BlockType.NONE
157
+ mutableBlockStyle.clear()
158
+ cachedBlockStyle = null
159
+ currentHeadingLevel = 0
160
+ blockquoteDepth = 0
161
+ listDepth = 0
162
+ listType = null
163
+ listItemNumber = 0
164
+ orderedListItemNumbers.clear()
165
+ }
166
+ }
@@ -0,0 +1,84 @@
1
+ package com.swmansion.enriched.markdown.renderer
2
+
3
+ import android.text.SpannableStringBuilder
4
+ import com.swmansion.enriched.markdown.parser.MarkdownASTNode
5
+ import com.swmansion.enriched.markdown.spans.BlockquoteSpan
6
+ import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
+ import com.swmansion.enriched.markdown.utils.applyMarginBottom
8
+ import com.swmansion.enriched.markdown.utils.applyMarginTop
9
+ import com.swmansion.enriched.markdown.utils.createLineHeightSpan
10
+
11
+ class BlockquoteRenderer(
12
+ private val config: RendererConfig,
13
+ ) : NodeRenderer {
14
+ override fun render(
15
+ node: MarkdownASTNode,
16
+ builder: SpannableStringBuilder,
17
+ onLinkPress: ((String) -> Unit)?,
18
+ onLinkLongPress: ((String) -> Unit)?,
19
+ factory: RendererFactory,
20
+ ) {
21
+ val start = builder.length
22
+ val style = config.style.blockquoteStyle
23
+ val context = factory.blockStyleContext
24
+ val depth = context.blockquoteDepth
25
+
26
+ // Track depth to handle nested indentation levels
27
+ context.blockquoteDepth = depth + 1
28
+ context.setBlockquoteStyle(style)
29
+
30
+ try {
31
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
32
+ } finally {
33
+ context.clearBlockStyle()
34
+ context.blockquoteDepth = depth
35
+ }
36
+
37
+ if (builder.length == start) return
38
+ val end = builder.length
39
+
40
+ // Find immediately nested quotes to exclude them from this level's line-height/margins
41
+ val nestedRanges =
42
+ builder
43
+ .getSpans(start, end, BlockquoteSpan::class.java)
44
+ .filter { it.depth == depth + 1 }
45
+ .map { builder.getSpanStart(it) to builder.getSpanEnd(it) }
46
+ .sortedBy { it.first }
47
+
48
+ // The Accent Bar Span covers the full range for visual continuity
49
+ builder.setSpan(
50
+ BlockquoteSpan(style, depth, factory.context, factory.styleCache),
51
+ start,
52
+ end,
53
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
54
+ )
55
+
56
+ // Apply styling only to segments that are NOT nested quotes
57
+ applySpansExcludingNested(builder, nestedRanges, start, end, createLineHeightSpan(style.lineHeight))
58
+
59
+ // Margins are only applied by the outermost (root) quote
60
+ if (depth == 0) {
61
+ applyMarginTop(builder, start, style.marginTop)
62
+ applyMarginBottom(builder, style.marginBottom)
63
+ }
64
+ }
65
+
66
+ private fun applySpansExcludingNested(
67
+ builder: SpannableStringBuilder,
68
+ nestedRanges: List<Pair<Int, Int>>,
69
+ start: Int,
70
+ end: Int,
71
+ span: Any,
72
+ ) {
73
+ var currentPos = start
74
+ for ((nestedStart, nestedEnd) in nestedRanges) {
75
+ if (currentPos < nestedStart) {
76
+ builder.setSpan(span, currentPos, nestedStart, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
77
+ }
78
+ currentPos = nestedEnd
79
+ }
80
+ if (currentPos < end) {
81
+ builder.setSpan(span, currentPos, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
82
+ }
83
+ }
84
+ }