react-native-enriched-markdown 0.1.1 → 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 (127) hide show
  1. package/README.md +80 -8
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +17 -2
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +6 -1
  4. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +28 -3
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +225 -1
  8. package/android/src/main/cpp/jni-adapter.cpp +28 -11
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +132 -15
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +1 -16
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +67 -13
  12. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +241 -21
  13. package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +2 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +17 -3
  17. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +13 -18
  18. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +23 -24
  19. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +1 -0
  20. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +2 -1
  21. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +2 -1
  22. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +18 -2
  23. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +22 -6
  24. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +1 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +3 -2
  26. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +2 -1
  27. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +16 -9
  28. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +5 -1
  29. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +23 -9
  30. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +24 -10
  31. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +1 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
  33. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +2 -1
  34. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +1 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +1 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +1 -0
  38. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +8 -17
  39. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +19 -5
  40. package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +1 -1
  41. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +1 -0
  43. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +3 -0
  44. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +3 -0
  45. package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +5 -1
  46. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +3 -1
  47. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +3 -0
  48. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +5 -1
  49. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
  50. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +32 -1
  51. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +22 -5
  52. package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
  53. package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -0
  54. package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +23 -5
  55. package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
  56. package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +10 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +58 -56
  58. package/android/src/main/jni/CMakeLists.txt +1 -13
  59. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +0 -13
  60. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +2 -14
  61. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +3 -0
  62. package/cpp/parser/MD4CParser.cpp +21 -8
  63. package/cpp/parser/MD4CParser.hpp +5 -1
  64. package/cpp/parser/MarkdownASTNode.hpp +2 -0
  65. package/ios/EnrichedMarkdownText.mm +356 -29
  66. package/ios/attachments/{ImageAttachment.h → EnrichedMarkdownImageAttachment.h} +1 -1
  67. package/ios/attachments/{ImageAttachment.m → EnrichedMarkdownImageAttachment.m} +4 -4
  68. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
  69. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
  70. package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +28 -3
  71. package/ios/generated/EnrichedMarkdownTextSpec/Props.h +225 -1
  72. package/ios/parser/MarkdownASTNode.h +2 -0
  73. package/ios/parser/MarkdownParser.h +9 -0
  74. package/ios/parser/MarkdownParser.mm +31 -2
  75. package/ios/parser/MarkdownParserBridge.mm +13 -3
  76. package/ios/renderer/AttributedRenderer.h +2 -0
  77. package/ios/renderer/AttributedRenderer.m +52 -19
  78. package/ios/renderer/BlockquoteRenderer.m +7 -6
  79. package/ios/renderer/CodeBlockRenderer.m +9 -8
  80. package/ios/renderer/HeadingRenderer.m +31 -24
  81. package/ios/renderer/ImageRenderer.m +31 -10
  82. package/ios/renderer/ListItemRenderer.m +51 -39
  83. package/ios/renderer/ListRenderer.m +21 -18
  84. package/ios/renderer/ParagraphRenderer.m +27 -16
  85. package/ios/renderer/RenderContext.h +17 -0
  86. package/ios/renderer/RenderContext.m +66 -2
  87. package/ios/renderer/RendererFactory.m +6 -0
  88. package/ios/renderer/StrikethroughRenderer.h +6 -0
  89. package/ios/renderer/StrikethroughRenderer.m +40 -0
  90. package/ios/renderer/UnderlineRenderer.h +6 -0
  91. package/ios/renderer/UnderlineRenderer.m +39 -0
  92. package/ios/styles/StyleConfig.h +46 -0
  93. package/ios/styles/StyleConfig.mm +351 -12
  94. package/ios/utils/AccessibilityInfo.h +35 -0
  95. package/ios/utils/AccessibilityInfo.m +24 -0
  96. package/ios/utils/CodeBlockBackground.m +4 -9
  97. package/ios/utils/FontUtils.h +5 -0
  98. package/ios/utils/FontUtils.m +14 -0
  99. package/ios/utils/HTMLGenerator.m +21 -7
  100. package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
  101. package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
  102. package/ios/utils/MarkdownExtractor.m +18 -5
  103. package/ios/utils/ParagraphStyleUtils.h +10 -2
  104. package/ios/utils/ParagraphStyleUtils.m +57 -2
  105. package/ios/utils/PasteboardUtils.h +1 -1
  106. package/ios/utils/PasteboardUtils.m +3 -3
  107. package/lib/module/EnrichedMarkdownText.js +33 -2
  108. package/lib/module/EnrichedMarkdownText.js.map +1 -1
  109. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +83 -3
  110. package/lib/module/index.js +0 -1
  111. package/lib/module/index.js.map +1 -1
  112. package/lib/module/normalizeMarkdownStyle.js +58 -14
  113. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  114. package/lib/typescript/src/EnrichedMarkdownText.d.ts +85 -3
  115. package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -1
  116. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +75 -1
  117. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  118. package/lib/typescript/src/index.d.ts +2 -3
  119. package/lib/typescript/src/index.d.ts.map +1 -1
  120. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  121. package/package.json +1 -1
  122. package/src/EnrichedMarkdownText.tsx +133 -5
  123. package/src/EnrichedMarkdownTextNativeComponent.ts +83 -3
  124. package/src/index.tsx +5 -2
  125. package/src/normalizeMarkdownStyle.ts +46 -0
  126. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +0 -9
  127. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +0 -25
@@ -0,0 +1,27 @@
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.StrikethroughSpan
6
+ import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
+
8
+ class StrikethroughRenderer(
9
+ private val config: RendererConfig,
10
+ ) : NodeRenderer {
11
+ override fun render(
12
+ node: MarkdownASTNode,
13
+ builder: SpannableStringBuilder,
14
+ onLinkPress: ((String) -> Unit)?,
15
+ onLinkLongPress: ((String) -> Unit)?,
16
+ factory: RendererFactory,
17
+ ) {
18
+ factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress, onLinkLongPress) }) { start, end, blockStyle ->
19
+ builder.setSpan(
20
+ StrikethroughSpan(factory.styleCache.strikethroughColor),
21
+ start,
22
+ end,
23
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
24
+ )
25
+ }
26
+ }
27
+ }
@@ -12,9 +12,10 @@ class StrongRenderer(
12
12
  node: MarkdownASTNode,
13
13
  builder: SpannableStringBuilder,
14
14
  onLinkPress: ((String) -> Unit)?,
15
+ onLinkLongPress: ((String) -> Unit)?,
15
16
  factory: RendererFactory,
16
17
  ) {
17
- factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress) }) { start, end, blockStyle ->
18
+ factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress, onLinkLongPress) }) { start, end, blockStyle ->
18
19
  builder.setSpan(
19
20
  StrongSpan(factory.styleCache, blockStyle),
20
21
  start,
@@ -10,6 +10,7 @@ class TextRenderer : NodeRenderer {
10
10
  node: MarkdownASTNode,
11
11
  builder: SpannableStringBuilder,
12
12
  onLinkPress: ((String) -> Unit)?,
13
+ onLinkLongPress: ((String) -> Unit)?,
13
14
  factory: RendererFactory,
14
15
  ) {
15
16
  val content = node.content
@@ -12,6 +12,7 @@ class ThematicBreakRenderer(
12
12
  node: MarkdownASTNode,
13
13
  builder: SpannableStringBuilder,
14
14
  onLinkPress: ((String) -> Unit)?,
15
+ onLinkLongPress: ((String) -> Unit)?,
15
16
  factory: RendererFactory,
16
17
  ) {
17
18
  builder.ensureNewline()
@@ -0,0 +1,27 @@
1
+ package com.swmansion.enriched.markdown.renderer
2
+
3
+ import android.text.SpannableStringBuilder
4
+ import android.text.style.UnderlineSpan
5
+ import com.swmansion.enriched.markdown.parser.MarkdownASTNode
6
+ import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
+
8
+ class UnderlineRenderer(
9
+ private val config: RendererConfig,
10
+ ) : NodeRenderer {
11
+ override fun render(
12
+ node: MarkdownASTNode,
13
+ builder: SpannableStringBuilder,
14
+ onLinkPress: ((String) -> Unit)?,
15
+ onLinkLongPress: ((String) -> Unit)?,
16
+ factory: RendererFactory,
17
+ ) {
18
+ factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress, onLinkLongPress) }) { start, end, _ ->
19
+ builder.setSpan(
20
+ UnderlineSpan(),
21
+ start,
22
+ end,
23
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
24
+ )
25
+ }
26
+ }
27
+ }
@@ -28,6 +28,7 @@ class ImageSpan(
28
28
  val imageUrl: String,
29
29
  styleConfig: StyleConfig,
30
30
  val isInline: Boolean = false,
31
+ val altText: String = "",
31
32
  ) : AndroidImageSpan(
32
33
  createInitialDrawable(styleConfig, imageUrl, isInline),
33
34
  imageUrl,
@@ -2,35 +2,26 @@ package com.swmansion.enriched.markdown.spans
2
2
 
3
3
  import android.graphics.Paint
4
4
  import kotlin.math.ceil
5
- import kotlin.math.roundToInt
5
+ import kotlin.math.floor
6
6
  import android.text.style.LineHeightSpan as AndroidLineHeightSpan
7
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
8
  class LineHeightSpan(
13
- private val lineHeight: Float,
9
+ height: Float,
14
10
  ) : AndroidLineHeightSpan {
11
+ private val lineHeight: Int = ceil(height.toDouble()).toInt()
12
+
15
13
  override fun chooseHeight(
16
14
  text: CharSequence?,
17
15
  start: Int,
18
16
  end: Int,
19
17
  spanstartv: Int,
20
- lineHeight: Int,
18
+ v: Int,
21
19
  fm: Paint.FontMetricsInt?,
22
20
  ) {
23
21
  if (fm == null) return
24
22
 
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
23
+ val leading = lineHeight - ((-fm.ascent) + fm.descent)
24
+ fm.ascent -= ceil(leading / 2.0f).toInt()
25
+ fm.descent += floor(leading / 2.0f).toInt()
35
26
  }
36
27
  }
@@ -12,17 +12,31 @@ import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
12
12
  class LinkSpan(
13
13
  val url: String,
14
14
  private val onLinkPress: ((String) -> Unit)?,
15
+ private val onLinkLongPress: ((String) -> Unit)?,
15
16
  private val styleCache: SpanStyleCache,
16
17
  private val blockStyle: BlockStyle,
17
18
  private val context: Context,
18
19
  ) : ClickableSpan() {
20
+ @Volatile
21
+ private var longPressTriggered = false
22
+
19
23
  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)
24
+ if (longPressTriggered) {
25
+ longPressTriggered = false
26
+ return
25
27
  }
28
+
29
+ onLinkPress?.invoke(url) ?: (widget as? EnrichedMarkdownText)?.emitOnLinkPress(url)
30
+ }
31
+
32
+ fun onLongClick(widget: View): Boolean {
33
+ longPressTriggered = true
34
+
35
+ (widget as? EnrichedMarkdownText)?.emitOnLinkLongPress(url)
36
+
37
+ onLinkLongPress?.invoke(url)
38
+
39
+ return true
26
40
  }
27
41
 
28
42
  override fun updateDrawState(textPaint: TextPaint) {
@@ -9,7 +9,7 @@ import android.text.style.LineHeightSpan
9
9
  * For spacer lines (single newline), sets the line height to exactly marginBottom.
10
10
  * For regular lines, adds marginBottom only at paragraph boundaries to preserve lineHeight.
11
11
  *
12
- * @param marginBottom The margin in pixels to add below the block (must be > 0)
12
+ * @param marginBottom The margin in pixels to add below the block (0 = no margin)
13
13
  */
14
14
  class MarginBottomSpan(
15
15
  val marginBottom: Float,
@@ -0,0 +1,12 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.text.TextPaint
4
+ import android.text.style.CharacterStyle
5
+
6
+ class StrikethroughSpan(
7
+ private val strikethroughColor: Int,
8
+ ) : CharacterStyle() {
9
+ override fun updateDrawState(tp: TextPaint) {
10
+ tp.isStrikeThruText = true
11
+ }
12
+ }
@@ -5,6 +5,7 @@ interface BaseBlockStyle {
5
5
  val fontFamily: String
6
6
  val fontWeight: String
7
7
  val color: Int
8
+ val marginTop: Float
8
9
  val marginBottom: Float
9
10
  val lineHeight: Float
10
11
  }
@@ -7,6 +7,7 @@ data class BlockquoteStyle(
7
7
  override val fontFamily: String,
8
8
  override val fontWeight: String,
9
9
  override val color: Int,
10
+ override val marginTop: Float,
10
11
  override val marginBottom: Float,
11
12
  override val lineHeight: Float,
12
13
  val borderColor: Int,
@@ -23,6 +24,7 @@ data class BlockquoteStyle(
23
24
  val fontFamily = parser.parseString(map, "fontFamily")
24
25
  val fontWeight = parser.parseString(map, "fontWeight", "normal")
25
26
  val color = parser.parseColor(map, "color")
27
+ val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
26
28
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
27
29
  val lineHeightRaw = map.getDouble("lineHeight").toFloat()
28
30
  val lineHeight = parser.toPixelFromSP(lineHeightRaw)
@@ -36,6 +38,7 @@ data class BlockquoteStyle(
36
38
  fontFamily,
37
39
  fontWeight,
38
40
  color,
41
+ marginTop,
39
42
  marginBottom,
40
43
  lineHeight,
41
44
  borderColor,
@@ -7,6 +7,7 @@ data class CodeBlockStyle(
7
7
  override val fontFamily: String,
8
8
  override val fontWeight: String,
9
9
  override val color: Int,
10
+ override val marginTop: Float,
10
11
  override val marginBottom: Float,
11
12
  override val lineHeight: Float,
12
13
  val backgroundColor: Int,
@@ -24,6 +25,7 @@ data class CodeBlockStyle(
24
25
  val fontFamily = parser.parseString(map, "fontFamily")
25
26
  val fontWeight = parser.parseString(map, "fontWeight", "normal")
26
27
  val color = parser.parseColor(map, "color")
28
+ val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
27
29
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
28
30
  val lineHeightRaw = map.getDouble("lineHeight").toFloat()
29
31
  val lineHeight = parser.toPixelFromSP(lineHeightRaw)
@@ -38,6 +40,7 @@ data class CodeBlockStyle(
38
40
  fontFamily,
39
41
  fontWeight,
40
42
  color,
43
+ marginTop,
41
44
  marginBottom,
42
45
  lineHeight,
43
46
  backgroundColor,
@@ -7,8 +7,10 @@ data class HeadingStyle(
7
7
  override val fontFamily: String,
8
8
  override val fontWeight: String,
9
9
  override val color: Int,
10
+ override val marginTop: Float,
10
11
  override val marginBottom: Float,
11
12
  override val lineHeight: Float,
13
+ val textAlign: TextAlignment,
12
14
  ) : BaseBlockStyle {
13
15
  companion object {
14
16
  fun fromReadableMap(
@@ -19,11 +21,13 @@ data class HeadingStyle(
19
21
  val fontFamily = parser.parseString(map, "fontFamily")
20
22
  val fontWeight = parser.parseString(map, "fontWeight", "normal")
21
23
  val color = parser.parseColor(map, "color")
24
+ val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
22
25
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
23
26
  val lineHeightRaw = map.getDouble("lineHeight").toFloat()
24
27
  val lineHeight = parser.toPixelFromSP(lineHeightRaw)
28
+ val textAlign = parser.parseTextAlign(map, "textAlign")
25
29
 
26
- return HeadingStyle(fontSize, fontFamily, fontWeight, color, marginBottom, lineHeight)
30
+ return HeadingStyle(fontSize, fontFamily, fontWeight, color, marginTop, marginBottom, lineHeight, textAlign)
27
31
  }
28
32
  }
29
33
  }
@@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReadableMap
5
5
  data class ImageStyle(
6
6
  val height: Float,
7
7
  val borderRadius: Float,
8
+ val marginTop: Float,
8
9
  val marginBottom: Float,
9
10
  ) {
10
11
  companion object {
@@ -14,8 +15,9 @@ data class ImageStyle(
14
15
  ): ImageStyle {
15
16
  val height = parser.toPixelFromDIP(map.getDouble("height").toFloat())
16
17
  val borderRadius = parser.toPixelFromDIP(map.getDouble("borderRadius").toFloat())
18
+ val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
17
19
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
18
- return ImageStyle(height, borderRadius, marginBottom)
20
+ return ImageStyle(height, borderRadius, marginTop, marginBottom)
19
21
  }
20
22
  }
21
23
  }
@@ -7,6 +7,7 @@ data class ListStyle(
7
7
  override val fontFamily: String,
8
8
  override val fontWeight: String,
9
9
  override val color: Int,
10
+ override val marginTop: Float,
10
11
  override val marginBottom: Float,
11
12
  override val lineHeight: Float,
12
13
  val bulletColor: Int,
@@ -25,6 +26,7 @@ data class ListStyle(
25
26
  val fontFamily = parser.parseString(map, "fontFamily")
26
27
  val fontWeight = parser.parseString(map, "fontWeight", "normal")
27
28
  val color = parser.parseColor(map, "color")
29
+ val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
28
30
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
29
31
  val lineHeightRaw = map.getDouble("lineHeight").toFloat()
30
32
  val lineHeight = parser.toPixelFromSP(lineHeightRaw)
@@ -40,6 +42,7 @@ data class ListStyle(
40
42
  fontFamily,
41
43
  fontWeight,
42
44
  color,
45
+ marginTop,
43
46
  marginBottom,
44
47
  lineHeight,
45
48
  bulletColor,
@@ -7,8 +7,10 @@ data class ParagraphStyle(
7
7
  override val fontFamily: String,
8
8
  override val fontWeight: String,
9
9
  override val color: Int,
10
+ override val marginTop: Float,
10
11
  override val marginBottom: Float,
11
12
  override val lineHeight: Float,
13
+ val textAlign: TextAlignment,
12
14
  ) : BaseBlockStyle {
13
15
  companion object {
14
16
  fun fromReadableMap(
@@ -19,11 +21,13 @@ data class ParagraphStyle(
19
21
  val fontFamily = parser.parseString(map, "fontFamily")
20
22
  val fontWeight = parser.parseString(map, "fontWeight", "normal")
21
23
  val color = parser.parseColor(map, "color")
24
+ val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
22
25
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
23
26
  val lineHeightRaw = map.getDouble("lineHeight").toFloat()
24
27
  val lineHeight = parser.toPixelFromSP(lineHeightRaw)
28
+ val textAlign = parser.parseTextAlign(map, "textAlign")
25
29
 
26
- return ParagraphStyle(fontSize, fontFamily, fontWeight, color, marginBottom, lineHeight)
30
+ return ParagraphStyle(fontSize, fontFamily, fontWeight, color, marginTop, marginBottom, lineHeight, textAlign)
27
31
  }
28
32
  }
29
33
  }
@@ -0,0 +1,17 @@
1
+ package com.swmansion.enriched.markdown.styles
2
+
3
+ import com.facebook.react.bridge.ReadableMap
4
+
5
+ data class StrikethroughStyle(
6
+ val color: Int,
7
+ ) {
8
+ companion object {
9
+ fun fromReadableMap(
10
+ map: ReadableMap,
11
+ parser: StyleParser,
12
+ ): StrikethroughStyle {
13
+ val color = parser.parseColor(map, "color")
14
+ return StrikethroughStyle(color)
15
+ }
16
+ }
17
+ }
@@ -16,8 +16,10 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
16
16
  class StyleConfig(
17
17
  private val style: ReadableMap,
18
18
  context: Context,
19
+ allowFontScaling: Boolean,
20
+ maxFontSizeMultiplier: Float,
19
21
  ) {
20
- private val styleParser = StyleParser(context)
22
+ private val styleParser = StyleParser(context, allowFontScaling, maxFontSizeMultiplier)
21
23
  private val assets: AssetManager = context.assets
22
24
 
23
25
  val paragraphStyle: ParagraphStyle by lazy {
@@ -88,6 +90,22 @@ class StyleConfig(
88
90
  EmphasisStyle.fromReadableMap(map, styleParser)
89
91
  }
90
92
 
93
+ val strikethroughStyle: StrikethroughStyle by lazy {
94
+ val map =
95
+ requireNotNull(style.getMap("strikethrough")) {
96
+ "Strikethrough style not found. JS should always provide defaults."
97
+ }
98
+ StrikethroughStyle.fromReadableMap(map, styleParser)
99
+ }
100
+
101
+ val underlineStyle: UnderlineStyle by lazy {
102
+ val map =
103
+ requireNotNull(style.getMap("underline")) {
104
+ "Underline style not found. JS should always provide defaults."
105
+ }
106
+ UnderlineStyle.fromReadableMap(map, styleParser)
107
+ }
108
+
91
109
  val codeStyle: CodeStyle by lazy {
92
110
  val map =
93
111
  requireNotNull(style.getMap("code")) {
@@ -144,6 +162,15 @@ class StyleConfig(
144
162
  ThematicBreakStyle.fromReadableMap(map, styleParser)
145
163
  }
146
164
 
165
+ /**
166
+ * Returns true if any paragraph or heading style uses justify alignment.
167
+ * Used to enable justification mode on the TextView (API 26+).
168
+ */
169
+ val needsJustify: Boolean by lazy {
170
+ paragraphStyle.textAlign.needsJustify ||
171
+ headingStyles.filterNotNull().any { it.textAlign.needsJustify }
172
+ }
173
+
147
174
  override fun equals(other: Any?): Boolean {
148
175
  if (this === other) return true
149
176
  if (other !is StyleConfig) return false
@@ -153,6 +180,8 @@ class StyleConfig(
153
180
  linkStyle == other.linkStyle &&
154
181
  strongStyle == other.strongStyle &&
155
182
  emphasisStyle == other.emphasisStyle &&
183
+ strikethroughStyle == other.strikethroughStyle &&
184
+ underlineStyle == other.underlineStyle &&
156
185
  codeStyle == other.codeStyle &&
157
186
  imageStyle == other.imageStyle &&
158
187
  inlineImageStyle == other.inlineImageStyle &&
@@ -168,6 +197,8 @@ class StyleConfig(
168
197
  result = 31 * result + linkStyle.hashCode()
169
198
  result = 31 * result + strongStyle.hashCode()
170
199
  result = 31 * result + emphasisStyle.hashCode()
200
+ result = 31 * result + strikethroughStyle.hashCode()
201
+ result = 31 * result + underlineStyle.hashCode()
171
202
  result = 31 * result + codeStyle.hashCode()
172
203
  result = 31 * result + imageStyle.hashCode()
173
204
  result = 31 * result + inlineImageStyle.hashCode()
@@ -5,12 +5,10 @@ import com.facebook.react.bridge.ColorPropConverter
5
5
  import com.facebook.react.bridge.ReadableMap
6
6
  import com.facebook.react.uimanager.PixelUtil
7
7
 
8
- /**
9
- * Helper class for parsing style values from ReadableMap.
10
- * Provides common parsing utilities used by all style factory functions.
11
- */
12
8
  class StyleParser(
13
9
  private val context: Context,
10
+ private val allowFontScaling: Boolean,
11
+ private val maxFontSizeMultiplier: Float,
14
12
  ) {
15
13
  fun parseOptionalColor(
16
14
  map: ReadableMap,
@@ -69,7 +67,26 @@ class StyleParser(
69
67
  default
70
68
  }
71
69
 
72
- fun toPixelFromSP(value: Float): Float = PixelUtil.toPixelFromSP(value)
70
+ fun toPixelFromSP(value: Float): Float {
71
+ val metrics = context.resources.displayMetrics
72
+ val baseDensity = metrics.density
73
+
74
+ if (!allowFontScaling) {
75
+ return value * baseDensity
76
+ }
77
+
78
+ var fontScale = metrics.scaledDensity / baseDensity
79
+ if (maxFontSizeMultiplier >= 1.0f && fontScale > maxFontSizeMultiplier) {
80
+ fontScale = maxFontSizeMultiplier
81
+ }
82
+
83
+ return value * baseDensity * fontScale
84
+ }
73
85
 
74
86
  fun toPixelFromDIP(value: Float): Float = PixelUtil.toPixelFromDIP(value)
87
+
88
+ fun parseTextAlign(
89
+ map: ReadableMap,
90
+ key: String,
91
+ ): TextAlignment = TextAlignment.fromString(parseString(map, key, "left"))
75
92
  }
@@ -0,0 +1,32 @@
1
+ package com.swmansion.enriched.markdown.styles
2
+
3
+ import android.text.Layout
4
+
5
+ enum class TextAlignment(
6
+ val layoutAlignment: Layout.Alignment,
7
+ val needsJustify: Boolean,
8
+ ) {
9
+ LEFT(Layout.Alignment.ALIGN_NORMAL, false),
10
+ CENTER(Layout.Alignment.ALIGN_CENTER, false),
11
+ RIGHT(Layout.Alignment.ALIGN_OPPOSITE, false),
12
+ JUSTIFY(Layout.Alignment.ALIGN_NORMAL, true),
13
+ AUTO(Layout.Alignment.ALIGN_NORMAL, false),
14
+ ;
15
+
16
+ /**
17
+ * Whether an AlignmentSpan is needed.
18
+ * Only CENTER and RIGHT need explicit spans; LEFT/AUTO use default, JUSTIFY is handled at TextView level.
19
+ */
20
+ val needsAlignmentSpan: Boolean get() = this == CENTER || this == RIGHT
21
+
22
+ companion object {
23
+ fun fromString(value: String): TextAlignment =
24
+ when (value.lowercase()) {
25
+ "center" -> CENTER
26
+ "right" -> RIGHT
27
+ "justify" -> JUSTIFY
28
+ "auto" -> AUTO
29
+ else -> LEFT
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,17 @@
1
+ package com.swmansion.enriched.markdown.styles
2
+
3
+ import com.facebook.react.bridge.ReadableMap
4
+
5
+ data class UnderlineStyle(
6
+ val color: Int,
7
+ ) {
8
+ companion object {
9
+ fun fromReadableMap(
10
+ map: ReadableMap,
11
+ parser: StyleParser,
12
+ ): UnderlineStyle {
13
+ val color = parser.parseColor(map, "color")
14
+ return UnderlineStyle(color)
15
+ }
16
+ }
17
+ }
@@ -2,7 +2,6 @@ package com.swmansion.enriched.markdown.utils
2
2
 
3
3
  import android.graphics.Typeface
4
4
  import android.text.Spannable
5
- import android.text.style.StrikethroughSpan
6
5
  import android.text.style.StyleSpan
7
6
  import android.text.style.UnderlineSpan
8
7
  import com.swmansion.enriched.markdown.spans.BlockquoteSpan
@@ -13,6 +12,7 @@ import com.swmansion.enriched.markdown.spans.HeadingSpan
13
12
  import com.swmansion.enriched.markdown.spans.ImageSpan
14
13
  import com.swmansion.enriched.markdown.spans.LinkSpan
15
14
  import com.swmansion.enriched.markdown.spans.OrderedListSpan
15
+ import com.swmansion.enriched.markdown.spans.StrikethroughSpan
16
16
  import com.swmansion.enriched.markdown.spans.StrongSpan
17
17
  import com.swmansion.enriched.markdown.spans.UnorderedListSpan
18
18
  import com.swmansion.enriched.markdown.styles.StyleConfig
@@ -68,9 +68,11 @@ object HTMLGenerator {
68
68
  val linkColor: String
69
69
  val linkUnderline: Boolean
70
70
 
71
- // Strong/Emphasis
71
+ // Strong/Emphasis/Strikethrough/Underline
72
72
  val strongColor: String?
73
73
  val emphasisColor: String?
74
+ val strikethroughColor: String?
75
+ val underlineColor: String?
74
76
 
75
77
  // Image
76
78
  val imageMarginBottom: Int
@@ -135,11 +137,15 @@ object HTMLGenerator {
135
137
  linkColor = colorToCSS(style.linkStyle.color)
136
138
  linkUnderline = style.linkStyle.underline
137
139
 
138
- // Strong/Emphasis (nullable for inherit)
140
+ // Strong/Emphasis/Strikethrough/Underline (nullable for inherit)
139
141
  val sc = style.strongStyle.color
140
142
  strongColor = if (sc != null && sc != 0) colorToCSS(sc) else null
141
143
  val ec = style.emphasisStyle.color
142
144
  emphasisColor = if (ec != null && ec != 0) colorToCSS(ec) else null
145
+ val strikeColor = style.strikethroughStyle.color
146
+ strikethroughColor = if (strikeColor != 0) colorToCSS(strikeColor) else null
147
+ val underline = style.underlineStyle.color
148
+ underlineColor = if (underline != 0) colorToCSS(underline) else null
143
149
 
144
150
  // Image
145
151
  val imgStyle = style.imageStyle
@@ -710,8 +716,20 @@ object HTMLGenerator {
710
716
  }
711
717
  }
712
718
 
713
- if (isStrikethrough) html.append("<s>")
714
- if (isUnderline && link == null) html.append("<u>")
719
+ if (isStrikethrough) {
720
+ if (styles.strikethroughColor != null) {
721
+ html.append("<s style=\"text-decoration-color: ").append(styles.strikethroughColor).append(";\">")
722
+ } else {
723
+ html.append("<s>")
724
+ }
725
+ }
726
+ if (isUnderline && link == null) {
727
+ if (styles.underlineColor != null) {
728
+ html.append("<u style=\"text-decoration-color: ").append(styles.underlineColor).append(";\">")
729
+ } else {
730
+ html.append("<u>")
731
+ }
732
+ }
715
733
 
716
734
  escapeHTMLTo(html, content.trimEnd('\n'))
717
735