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,85 @@
1
+ package com.swmansion.enriched.markdown.renderer
2
+
3
+ import android.graphics.Typeface
4
+ import com.swmansion.enriched.markdown.styles.StyleConfig
5
+
6
+ /** Shared style cache for spans to avoid redundant calculations. */
7
+ class SpanStyleCache(
8
+ style: StyleConfig,
9
+ ) {
10
+ // Colors to preserve when applying inline styles (links, code, strong, emphasis)
11
+ val colorsToPreserve: IntArray = buildColorsToPreserve(style)
12
+
13
+ val strongColor: Int? = style.strongStyle.color
14
+ val emphasisColor: Int? = style.emphasisStyle.color
15
+ val linkColor: Int = style.linkStyle.color
16
+ val linkUnderline: Boolean = style.linkStyle.underline
17
+ val codeColor: Int = style.codeStyle.color
18
+
19
+ private fun buildColorsToPreserve(style: StyleConfig): IntArray =
20
+ buildList {
21
+ style.strongStyle.color
22
+ ?.takeIf { it != 0 }
23
+ ?.let { add(it) }
24
+ style.emphasisStyle.color
25
+ ?.takeIf { it != 0 }
26
+ ?.let { add(it) }
27
+ style.linkStyle.color
28
+ .takeIf { it != 0 }
29
+ ?.let { add(it) }
30
+ style
31
+ .codeStyle
32
+ .color
33
+ .takeIf { it != 0 }
34
+ ?.let { add(it) }
35
+ }.toIntArray()
36
+
37
+ fun getStrongColorFor(blockColor: Int): Int = strongColor ?: blockColor
38
+
39
+ fun getEmphasisColorFor(
40
+ blockColor: Int,
41
+ currentColor: Int,
42
+ ): Int =
43
+ if (currentColor == blockColor) {
44
+ emphasisColor ?: blockColor
45
+ } else {
46
+ currentColor
47
+ }
48
+
49
+ companion object {
50
+ private val typefaceCache = mutableMapOf<String, Typeface>()
51
+
52
+ /** Cached typeface for font family + style (BOLD, ITALIC, BOLD_ITALIC) */
53
+ fun getTypeface(
54
+ fontFamily: String,
55
+ style: Int,
56
+ ): Typeface =
57
+ typefaceCache.getOrPut("$fontFamily|$style") {
58
+ val base =
59
+ fontFamily
60
+ .takeIf { it.isNotEmpty() }
61
+ ?.let { Typeface.create(it, Typeface.NORMAL) }
62
+ ?: Typeface.DEFAULT
63
+ Typeface.create(base, style)
64
+ }
65
+
66
+ /** Cached typeface using weight string (e.g., "bold", "700") */
67
+ fun getTypefaceWithWeight(
68
+ fontFamily: String,
69
+ fontWeight: String,
70
+ ): Typeface {
71
+ val style =
72
+ when (fontWeight.lowercase()) {
73
+ "bold", "700", "800", "900" -> Typeface.BOLD
74
+ else -> Typeface.NORMAL
75
+ }
76
+ return getTypeface(fontFamily, style)
77
+ }
78
+
79
+ /** Cached monospace typeface preserving bold/italic */
80
+ fun getMonospaceTypeface(currentStyle: Int): Typeface =
81
+ typefaceCache.getOrPut("monospace|$currentStyle") {
82
+ Typeface.create(Typeface.MONOSPACE, currentStyle)
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,26 @@
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.StrongSpan
6
+ import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
+
8
+ class StrongRenderer(
9
+ private val config: RendererConfig,
10
+ ) : NodeRenderer {
11
+ override fun render(
12
+ node: MarkdownASTNode,
13
+ builder: SpannableStringBuilder,
14
+ onLinkPress: ((String) -> Unit)?,
15
+ factory: RendererFactory,
16
+ ) {
17
+ factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress) }) { start, end, blockStyle ->
18
+ builder.setSpan(
19
+ StrongSpan(factory.styleCache, blockStyle),
20
+ start,
21
+ end,
22
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
23
+ )
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,29 @@
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.TextSpan
6
+ import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
+
8
+ class TextRenderer : NodeRenderer {
9
+ override fun render(
10
+ node: MarkdownASTNode,
11
+ builder: SpannableStringBuilder,
12
+ onLinkPress: ((String) -> Unit)?,
13
+ factory: RendererFactory,
14
+ ) {
15
+ val content = node.content
16
+ if (content.isEmpty()) return
17
+
18
+ val blockType = factory.blockStyleContext.currentBlockType
19
+
20
+ factory.renderWithSpan(builder, { builder.append(content) }) { start, end, blockStyle ->
21
+ builder.setSpan(
22
+ TextSpan(blockStyle, factory.context),
23
+ start,
24
+ end,
25
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
26
+ )
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,44 @@
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.ThematicBreakSpan
6
+ import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
+
8
+ class ThematicBreakRenderer(
9
+ private val config: RendererConfig,
10
+ ) : NodeRenderer {
11
+ override fun render(
12
+ node: MarkdownASTNode,
13
+ builder: SpannableStringBuilder,
14
+ onLinkPress: ((String) -> Unit)?,
15
+ factory: RendererFactory,
16
+ ) {
17
+ builder.ensureNewline()
18
+
19
+ val start = builder.length
20
+
21
+ builder.append(" \n")
22
+ val end = builder.length
23
+
24
+ val style = config.style.thematicBreakStyle
25
+
26
+ builder.setSpan(
27
+ ThematicBreakSpan(
28
+ style.color,
29
+ style.height,
30
+ style.marginTop,
31
+ style.marginBottom,
32
+ ),
33
+ start,
34
+ end,
35
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
36
+ )
37
+ }
38
+
39
+ private fun SpannableStringBuilder.ensureNewline() {
40
+ if (isNotEmpty() && this[length - 1] != '\n') {
41
+ append('\n')
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,136 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.graphics.Canvas
6
+ import android.graphics.Paint
7
+ import android.graphics.Typeface
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.MetricAffectingSpan
13
+ import com.swmansion.enriched.markdown.renderer.BlockStyle
14
+ import com.swmansion.enriched.markdown.renderer.SpanStyleCache
15
+ import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
16
+ import com.swmansion.enriched.markdown.utils.applyColorPreserving
17
+
18
+ abstract class BaseListSpan(
19
+ val depth: Int,
20
+ protected val context: Context,
21
+ protected val styleCache: SpanStyleCache,
22
+ protected val blockStyle: BlockStyle,
23
+ protected val marginLeft: Float,
24
+ protected val gapWidth: Float,
25
+ ) : MetricAffectingSpan(),
26
+ LeadingMarginSpan {
27
+ // Cache for shouldSkipDrawing to avoid repeated getSpans() calls during draw passes
28
+ private var cachedText: CharSequence? = null
29
+ private var cachedHasDeeperSpanByPosition = mutableMapOf<Int, Boolean>()
30
+
31
+ protected abstract fun getMarkerWidth(): Float
32
+
33
+ // --- MetricAffectingSpan Implementation ---
34
+
35
+ override fun updateMeasureState(tp: TextPaint) = applyTextStyle(tp)
36
+
37
+ override fun updateDrawState(tp: TextPaint) = applyTextStyle(tp)
38
+
39
+ // --- LeadingMarginSpan Implementation ---
40
+
41
+ override fun getLeadingMargin(first: Boolean): Int {
42
+ // Root items: just marker width + gap (flush to left edge)
43
+ // Nested items: add marginLeft for each nesting level
44
+ return if (depth == 0) {
45
+ val effectiveGap = gapWidth.coerceAtLeast(DEFAULT_MIN_GAP)
46
+ (getMarkerWidth() + effectiveGap).toInt()
47
+ } else {
48
+ marginLeft.toInt()
49
+ }
50
+ }
51
+
52
+ protected fun getEffectiveGapWidth(): Float = gapWidth.coerceAtLeast(DEFAULT_MIN_GAP)
53
+
54
+ override fun drawLeadingMargin(
55
+ c: Canvas,
56
+ p: Paint,
57
+ x: Int,
58
+ dir: Int,
59
+ top: Int,
60
+ baseline: Int,
61
+ bottom: Int,
62
+ text: CharSequence?,
63
+ start: Int,
64
+ end: Int,
65
+ first: Boolean,
66
+ layout: Layout?,
67
+ ) {
68
+ // Draw only on the first line of paragraphs that have content and are not nested deeper
69
+ if (!first || shouldSkipDrawing(text, start) || !hasContent(text, start, end)) return
70
+
71
+ val originalStyle = p.style
72
+ val originalColor = p.color
73
+ drawMarker(c, p, x, dir, top, baseline, bottom, layout, start)
74
+ p.style = originalStyle
75
+ p.color = originalColor
76
+ }
77
+
78
+ protected abstract fun drawMarker(
79
+ c: Canvas,
80
+ p: Paint,
81
+ x: Int,
82
+ dir: Int,
83
+ top: Int,
84
+ baseline: Int,
85
+ bottom: Int,
86
+ layout: Layout?,
87
+ start: Int,
88
+ )
89
+
90
+ @SuppressLint("WrongConstant") // Result of mask is always valid: 0, 1, 2, or 3
91
+ private fun applyTextStyle(tp: TextPaint) {
92
+ tp.textSize = blockStyle.fontSize
93
+
94
+ val preservedStyle = (tp.typeface?.style ?: 0) and BOLD_ITALIC_MASK
95
+ tp.applyBlockStyleFont(blockStyle, context)
96
+ if (preservedStyle != 0) {
97
+ tp.typeface?.let { base -> tp.typeface = Typeface.create(base, preservedStyle) }
98
+ }
99
+
100
+ tp.applyColorPreserving(blockStyle.color, *styleCache.colorsToPreserve)
101
+ }
102
+
103
+ companion object {
104
+ private const val BOLD_ITALIC_MASK = Typeface.BOLD or Typeface.ITALIC // 3
105
+ private const val DEFAULT_MIN_GAP = 4f
106
+ }
107
+
108
+ // --- Helper Methods ---
109
+
110
+ private fun hasContent(
111
+ text: CharSequence?,
112
+ start: Int,
113
+ end: Int,
114
+ ): Boolean {
115
+ if (text == null || end <= start) return false
116
+ // Check if there is at least one non-whitespace character in the range
117
+ return (start until end).any { !text[it].isWhitespace() }
118
+ }
119
+
120
+ private fun shouldSkipDrawing(
121
+ text: CharSequence?,
122
+ start: Int,
123
+ ): Boolean {
124
+ if (text !is Spanned) return false
125
+
126
+ if (cachedText !== text) {
127
+ cachedText = text
128
+ cachedHasDeeperSpanByPosition.clear()
129
+ }
130
+
131
+ return cachedHasDeeperSpanByPosition.getOrPut(start) {
132
+ val spans = text.getSpans(start, start + 1, BaseListSpan::class.java)
133
+ spans.any { it.depth > depth }
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,135 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Paint
8
+ import android.graphics.Typeface
9
+ import android.text.Layout
10
+ import android.text.Spanned
11
+ import android.text.TextPaint
12
+ import android.text.style.LeadingMarginSpan
13
+ import android.text.style.MetricAffectingSpan
14
+ import com.swmansion.enriched.markdown.renderer.BlockStyle
15
+ import com.swmansion.enriched.markdown.renderer.SpanStyleCache
16
+ import com.swmansion.enriched.markdown.styles.BlockquoteStyle
17
+ import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
18
+ import com.swmansion.enriched.markdown.utils.applyColorPreserving
19
+
20
+ class BlockquoteSpan(
21
+ private val blockquoteStyle: BlockquoteStyle,
22
+ val depth: Int,
23
+ private val context: Context,
24
+ private val styleCache: SpanStyleCache,
25
+ ) : MetricAffectingSpan(),
26
+ LeadingMarginSpan {
27
+ private val levelSpacing: Float = blockquoteStyle.borderWidth + blockquoteStyle.gapWidth
28
+ private val blockStyle =
29
+ BlockStyle(
30
+ fontSize = blockquoteStyle.fontSize,
31
+ fontFamily = blockquoteStyle.fontFamily,
32
+ fontWeight = blockquoteStyle.fontWeight,
33
+ color = blockquoteStyle.color,
34
+ )
35
+
36
+ // Cache for shouldSkipDrawing to avoid repeated getSpans() calls during draw passes
37
+ private var cachedText: CharSequence? = null
38
+ private var cachedMaxDepthByPosition = mutableMapOf<Int, Int>()
39
+
40
+ override fun updateMeasureState(tp: TextPaint) = applyTextStyle(tp)
41
+
42
+ override fun updateDrawState(tp: TextPaint) = applyTextStyle(tp)
43
+
44
+ override fun getLeadingMargin(first: Boolean): Int = levelSpacing.toInt()
45
+
46
+ override fun drawLeadingMargin(
47
+ c: Canvas,
48
+ p: Paint,
49
+ x: Int,
50
+ dir: Int,
51
+ top: Int,
52
+ baseline: Int,
53
+ bottom: Int,
54
+ text: CharSequence?,
55
+ start: Int,
56
+ end: Int,
57
+ first: Boolean,
58
+ layout: Layout?,
59
+ ) {
60
+ // Essential check from original: only the deepest span draws to prevent over-rendering background
61
+ if (shouldSkipDrawing(text, start)) return
62
+
63
+ drawBackground(c, top, bottom, layout)
64
+
65
+ val borderPaint = configureBorderPaint()
66
+ val borderTop = top.toFloat()
67
+ val borderBottom = bottom.toFloat()
68
+ val containerLeft = layout?.getLineLeft(0) ?: 0f
69
+
70
+ for (level in 0..depth) {
71
+ val borderX = containerLeft + (levelSpacing * level * dir)
72
+ val borderRight = borderX + (blockquoteStyle.borderWidth * dir)
73
+ c.drawRect(borderX, borderTop, borderRight, borderBottom, borderPaint)
74
+ }
75
+ }
76
+
77
+ @SuppressLint("WrongConstant") // Result of mask is always valid: 0, 1, 2, or 3
78
+ private fun applyTextStyle(tp: TextPaint) {
79
+ tp.textSize = blockStyle.fontSize
80
+ val preserved = (tp.typeface?.style ?: 0) and BOLD_ITALIC_MASK
81
+ tp.applyBlockStyleFont(blockStyle, context)
82
+ if (preserved != 0) {
83
+ tp.typeface = Typeface.create(tp.typeface ?: Typeface.DEFAULT, preserved)
84
+ }
85
+ tp.applyColorPreserving(blockStyle.color, *styleCache.colorsToPreserve)
86
+ }
87
+
88
+ companion object {
89
+ private const val BOLD_ITALIC_MASK = Typeface.BOLD or Typeface.ITALIC
90
+
91
+ private val sharedBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
92
+ private val sharedBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
93
+ }
94
+
95
+ private fun configureBorderPaint(): Paint =
96
+ sharedBorderPaint.apply {
97
+ color = blockquoteStyle.borderColor
98
+ }
99
+
100
+ private fun configureBackgroundPaint(bgColor: Int): Paint =
101
+ sharedBackgroundPaint.apply {
102
+ color = bgColor
103
+ }
104
+
105
+ private fun shouldSkipDrawing(
106
+ text: CharSequence?,
107
+ start: Int,
108
+ ): Boolean {
109
+ if (text !is Spanned) return false
110
+
111
+ if (cachedText !== text) {
112
+ cachedText = text
113
+ cachedMaxDepthByPosition.clear()
114
+ }
115
+
116
+ val maxDepth =
117
+ cachedMaxDepthByPosition.getOrPut(start) {
118
+ val spans = text.getSpans(start, start + 1, BlockquoteSpan::class.java)
119
+ spans.maxOfOrNull { it.depth } ?: -1
120
+ }
121
+
122
+ return maxDepth > depth
123
+ }
124
+
125
+ private fun drawBackground(
126
+ c: Canvas,
127
+ top: Int,
128
+ bottom: Int,
129
+ layout: Layout?,
130
+ ) {
131
+ val bgColor = blockquoteStyle.backgroundColor?.takeIf { it != Color.TRANSPARENT } ?: return
132
+ val backgroundPaint = configureBackgroundPaint(bgColor)
133
+ c.drawRect(0f, top.toFloat(), layout?.width?.toFloat() ?: 0f, bottom.toFloat(), backgroundPaint)
134
+ }
135
+ }
@@ -0,0 +1,180 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.Paint
5
+ import android.graphics.Path
6
+ import android.graphics.RectF
7
+ import android.text.Spanned
8
+ import android.text.StaticLayout
9
+ import android.text.TextPaint
10
+ import android.text.style.LineBackgroundSpan
11
+ import com.swmansion.enriched.markdown.styles.StyleConfig
12
+ import kotlin.math.max
13
+ import kotlin.math.min
14
+
15
+ class CodeBackgroundSpan(
16
+ private val styleConfig: StyleConfig,
17
+ ) : LineBackgroundSpan {
18
+ companion object {
19
+ private const val CORNER_RADIUS = 6.0f
20
+ private const val BORDER_WIDTH = 1.0f
21
+
22
+ private val sharedBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
23
+ private val sharedBorderPaint =
24
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
25
+ style = Paint.Style.STROKE
26
+ strokeWidth = BORDER_WIDTH
27
+ strokeJoin = Paint.Join.ROUND
28
+ strokeCap = Paint.Cap.ROUND
29
+ }
30
+ }
31
+
32
+ // Reusable drawing objects per instance
33
+ private val rect = RectF()
34
+ private val path = Path()
35
+
36
+ override fun drawBackground(
37
+ canvas: Canvas,
38
+ p: Paint,
39
+ left: Int,
40
+ right: Int,
41
+ top: Int,
42
+ baseline: Int,
43
+ bottom: Int,
44
+ text: CharSequence,
45
+ start: Int,
46
+ end: Int,
47
+ lineNum: Int,
48
+ ) {
49
+ if (text !is Spanned) return
50
+
51
+ val spanStart = text.getSpanStart(this)
52
+ val spanEnd = text.getSpanEnd(this)
53
+ if (spanStart !in 0 until spanEnd) return
54
+
55
+ // 1. Determine relative positioning
56
+ val isFirst = spanStart >= start
57
+ val isLast = spanEnd <= end
58
+
59
+ // 2. Calculate coordinates
60
+ val finalBottom = adjustBottomForMargin(text, end, bottom)
61
+ val startX = if (isFirst) getHorizontalOffset(text, start, end, spanStart, p) + left else left.toFloat()
62
+ val endX = if (isLast) getHorizontalOffset(text, start, end, spanEnd, p) + left else right.toFloat()
63
+
64
+ rect.set(min(startX, endX), top.toFloat(), max(startX, endX), finalBottom.toFloat())
65
+
66
+ // 3. Apply Style
67
+ val codeStyle = styleConfig.codeStyle
68
+ sharedBackgroundPaint.color = codeStyle.backgroundColor
69
+ sharedBorderPaint.color = codeStyle.borderColor
70
+
71
+ drawShapes(canvas, isFirst, isLast)
72
+ }
73
+
74
+ private fun getHorizontalOffset(
75
+ text: CharSequence,
76
+ lineStart: Int,
77
+ lineEnd: Int,
78
+ index: Int,
79
+ paint: Paint,
80
+ ): Float {
81
+ if (index <= lineStart) return 0f
82
+ val lineText = text.subSequence(lineStart, lineEnd)
83
+ val textPaint = paint as? TextPaint ?: TextPaint(paint)
84
+ val layout = StaticLayout.Builder.obtain(lineText, 0, lineText.length, textPaint, 10000).build()
85
+ return layout.getPrimaryHorizontal(index - lineStart)
86
+ }
87
+
88
+ private fun drawShapes(
89
+ canvas: Canvas,
90
+ isFirst: Boolean,
91
+ isLast: Boolean,
92
+ ) {
93
+ val radii = createRadii(isFirst, isLast)
94
+
95
+ path.reset()
96
+ path.addRoundRect(rect, radii, Path.Direction.CW)
97
+ canvas.drawPath(path, sharedBackgroundPaint)
98
+
99
+ if (isFirst && isLast) {
100
+ canvas.drawPath(path, sharedBorderPaint)
101
+ } else {
102
+ drawOpenBorders(canvas, isFirst, isLast)
103
+ }
104
+ }
105
+
106
+ private fun drawOpenBorders(
107
+ canvas: Canvas,
108
+ isFirst: Boolean,
109
+ isLast: Boolean,
110
+ ) {
111
+ val r = CORNER_RADIUS
112
+ path.reset()
113
+
114
+ if (isFirst) {
115
+ path.moveTo(rect.right, rect.top)
116
+ path.lineTo(rect.left + r, rect.top)
117
+ path.quadTo(rect.left, rect.top, rect.left, rect.top + r)
118
+ path.lineTo(rect.left, rect.bottom - r)
119
+ path.quadTo(rect.left, rect.bottom, rect.left + r, rect.bottom)
120
+ path.lineTo(rect.right, rect.bottom)
121
+ } else if (isLast) {
122
+ path.moveTo(rect.left, rect.top)
123
+ path.lineTo(rect.right - r, rect.top)
124
+ path.quadTo(rect.right, rect.top, rect.right, rect.top + r)
125
+ path.lineTo(rect.right, rect.bottom - r)
126
+ path.quadTo(rect.right, rect.bottom, rect.right - r, rect.bottom)
127
+ path.lineTo(rect.left, rect.bottom)
128
+ } else {
129
+ path.moveTo(rect.left, rect.top)
130
+ path.lineTo(rect.right, rect.top)
131
+ path.moveTo(rect.left, rect.bottom)
132
+ path.lineTo(rect.right, rect.bottom)
133
+ }
134
+ canvas.drawPath(path, sharedBorderPaint)
135
+ }
136
+
137
+ private fun createRadii(
138
+ isFirst: Boolean,
139
+ isLast: Boolean,
140
+ ) = when {
141
+ isFirst && isLast -> {
142
+ floatArrayOf(
143
+ CORNER_RADIUS,
144
+ CORNER_RADIUS,
145
+ CORNER_RADIUS,
146
+ CORNER_RADIUS,
147
+ CORNER_RADIUS,
148
+ CORNER_RADIUS,
149
+ CORNER_RADIUS,
150
+ CORNER_RADIUS,
151
+ )
152
+ }
153
+
154
+ isFirst -> {
155
+ floatArrayOf(CORNER_RADIUS, CORNER_RADIUS, 0f, 0f, 0f, 0f, CORNER_RADIUS, CORNER_RADIUS)
156
+ }
157
+
158
+ isLast -> {
159
+ floatArrayOf(0f, 0f, CORNER_RADIUS, CORNER_RADIUS, CORNER_RADIUS, CORNER_RADIUS, 0f, 0f)
160
+ }
161
+
162
+ else -> {
163
+ floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
164
+ }
165
+ }
166
+
167
+ private fun adjustBottomForMargin(
168
+ text: Spanned,
169
+ lineEnd: Int,
170
+ bottom: Int,
171
+ ): Int {
172
+ if (lineEnd <= 0 || lineEnd > text.length || text[lineEnd - 1] != '\n') return bottom
173
+ val marginSpans = text.getSpans(lineEnd - 1, lineEnd, MarginBottomSpan::class.java)
174
+ var adjusted = bottom
175
+ for (span in marginSpans) {
176
+ if (text.getSpanEnd(span) == lineEnd) adjusted -= span.marginBottom.toInt()
177
+ }
178
+ return adjusted
179
+ }
180
+ }