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
@@ -3,8 +3,9 @@ package com.swmansion.enriched.markdown.renderer
3
3
  import android.text.SpannableStringBuilder
4
4
  import com.swmansion.enriched.markdown.parser.MarkdownASTNode
5
5
  import com.swmansion.enriched.markdown.spans.BlockquoteSpan
6
- import com.swmansion.enriched.markdown.spans.MarginBottomSpan
7
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
8
9
  import com.swmansion.enriched.markdown.utils.createLineHeightSpan
9
10
 
10
11
  class BlockquoteRenderer(
@@ -14,6 +15,7 @@ class BlockquoteRenderer(
14
15
  node: MarkdownASTNode,
15
16
  builder: SpannableStringBuilder,
16
17
  onLinkPress: ((String) -> Unit)?,
18
+ onLinkLongPress: ((String) -> Unit)?,
17
19
  factory: RendererFactory,
18
20
  ) {
19
21
  val start = builder.length
@@ -21,12 +23,12 @@ class BlockquoteRenderer(
21
23
  val context = factory.blockStyleContext
22
24
  val depth = context.blockquoteDepth
23
25
 
24
- // 1. Context management
26
+ // Track depth to handle nested indentation levels
25
27
  context.blockquoteDepth = depth + 1
26
28
  context.setBlockquoteStyle(style)
27
29
 
28
30
  try {
29
- factory.renderChildren(node, builder, onLinkPress)
31
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
30
32
  } finally {
31
33
  context.clearBlockStyle()
32
34
  context.blockquoteDepth = depth
@@ -35,7 +37,7 @@ class BlockquoteRenderer(
35
37
  if (builder.length == start) return
36
38
  val end = builder.length
37
39
 
38
- // 2. Identify Nested Ranges (Essential for excluding them from parent-level styles)
40
+ // Find immediately nested quotes to exclude them from this level's line-height/margins
39
41
  val nestedRanges =
40
42
  builder
41
43
  .getSpans(start, end, BlockquoteSpan::class.java)
@@ -43,7 +45,7 @@ class BlockquoteRenderer(
43
45
  .map { builder.getSpanStart(it) to builder.getSpanEnd(it) }
44
46
  .sortedBy { it.first }
45
47
 
46
- // 3. Apply the Accent Bar Span (Must cover the full range for continuity)
48
+ // The Accent Bar Span covers the full range for visual continuity
47
49
  builder.setSpan(
48
50
  BlockquoteSpan(style, depth, factory.context, factory.styleCache),
49
51
  start,
@@ -51,20 +53,13 @@ class BlockquoteRenderer(
51
53
  SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
52
54
  )
53
55
 
54
- // 4. Fragmented Styling Logic
55
- // We apply line height and margin specifically to the segments that are NOT nested quotes.
56
+ // Apply styling only to segments that are NOT nested quotes
56
57
  applySpansExcludingNested(builder, nestedRanges, start, end, createLineHeightSpan(style.lineHeight))
57
58
 
58
- // 5. Root-level Spacing
59
- if (depth == 0 && style.marginBottom > 0) {
60
- val spacerLocation = builder.length
61
- builder.append("\n") // Physical break
62
- builder.setSpan(
63
- MarginBottomSpan(style.marginBottom),
64
- spacerLocation,
65
- builder.length,
66
- SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
67
- )
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)
68
63
  }
69
64
  }
70
65
 
@@ -73,7 +68,7 @@ class BlockquoteRenderer(
73
68
  nestedRanges: List<Pair<Int, Int>>,
74
69
  start: Int,
75
70
  end: Int,
76
- span: Any, // Changed to Any to handle both LineHeight and MarginBottom spans
71
+ span: Any,
77
72
  ) {
78
73
  var currentPos = start
79
74
  for ((nestedStart, nestedEnd) in nestedRanges) {
@@ -8,6 +8,7 @@ import com.swmansion.enriched.markdown.parser.MarkdownASTNode
8
8
  import com.swmansion.enriched.markdown.spans.CodeBlockSpan
9
9
  import com.swmansion.enriched.markdown.spans.MarginBottomSpan
10
10
  import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
11
+ import com.swmansion.enriched.markdown.utils.applyMarginTop
11
12
 
12
13
  class CodeBlockRenderer(
13
14
  private val config: RendererConfig,
@@ -16,56 +17,55 @@ class CodeBlockRenderer(
16
17
  node: MarkdownASTNode,
17
18
  builder: SpannableStringBuilder,
18
19
  onLinkPress: ((String) -> Unit)?,
20
+ onLinkLongPress: ((String) -> Unit)?,
19
21
  factory: RendererFactory,
20
22
  ) {
21
23
  val start = builder.length
22
24
  val style = config.style.codeBlockStyle
23
25
  val context = factory.blockStyleContext
24
26
 
25
- // Set code block style in context for children to inherit
27
+ applyMarginTop(builder, start, style.marginTop)
28
+
29
+ // Content starts after the potential 1-character marginTop spacer
30
+ val contentStart = start + (if (style.marginTop > 0) 1 else 0)
31
+
26
32
  context.setCodeBlockStyle(style)
27
33
 
28
34
  try {
29
- // Render children (code content)
30
- factory.renderChildren(node, builder, onLinkPress)
35
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
31
36
  } finally {
32
37
  context.clearBlockStyle()
33
38
  }
34
39
 
35
- // Safety check for empty code blocks
36
- if (builder.length == start) return
40
+ if (builder.length == contentStart) return
37
41
 
38
42
  val end = builder.length
39
43
  val padding = style.padding.toInt()
40
44
 
41
- // 1. Apply CodeBlockSpan (Handles Background, Borders, and Horizontal Padding)
42
- // Matches the logic in the updated CodeBlockSpan for full-width support
45
+ // Apply background, borders, and horizontal padding to content only
43
46
  builder.setSpan(
44
47
  CodeBlockSpan(style, factory.context, factory.styleCache),
45
- start,
48
+ contentStart,
46
49
  end,
47
50
  SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
48
51
  )
49
52
 
50
- // 2. Apply Boundary Vertical Padding
53
+ // Apply vertical padding via line height manipulation
51
54
  builder.setSpan(
52
55
  CodeBlockBoundaryPaddingSpan(padding),
53
- start,
56
+ contentStart,
54
57
  end,
55
58
  SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
56
59
  )
57
60
 
58
- // 3. Apply External Margin Bottom
59
- if (style.marginBottom > 0) {
60
- val marginStart = builder.length
61
- builder.append("\n")
62
- builder.setSpan(
63
- MarginBottomSpan(style.marginBottom),
64
- marginStart,
65
- builder.length,
66
- SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
67
- )
68
- }
61
+ val marginStart = builder.length
62
+ builder.append("\n")
63
+ builder.setSpan(
64
+ MarginBottomSpan(style.marginBottom),
65
+ marginStart,
66
+ builder.length,
67
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
68
+ )
69
69
  }
70
70
 
71
71
  /**
@@ -87,14 +87,13 @@ class CodeBlockRenderer(
87
87
  val spanStart = text.getSpanStart(this)
88
88
  val spanEnd = text.getSpanEnd(this)
89
89
 
90
- // Apply top vertical padding to the first line fragment
90
+ // Adjust ascent/top for the first line to create internal top padding
91
91
  if (startLine == spanStart) {
92
92
  fm.ascent -= padding
93
93
  fm.top -= padding
94
94
  }
95
95
 
96
- // Apply bottom vertical padding to the last line fragment
97
- // Checks for both character index and trailing newlines to ensure a tight fit
96
+ // Adjust descent/bottom for the last line (handling trailing newlines)
98
97
  val isLastLine = endLine == spanEnd || (spanEnd <= endLine && text[spanEnd - 1] == '\n')
99
98
  if (isLastLine) {
100
99
  fm.descent += padding
@@ -13,6 +13,7 @@ class CodeRenderer(
13
13
  node: MarkdownASTNode,
14
14
  builder: SpannableStringBuilder,
15
15
  onLinkPress: ((String) -> Unit)?,
16
+ onLinkLongPress: ((String) -> Unit)?,
16
17
  factory: RendererFactory,
17
18
  ) {
18
19
  if (node.children.isEmpty() || node.children.all { it.content.isEmpty() }) return
@@ -8,8 +8,9 @@ class DocumentRenderer : NodeRenderer {
8
8
  node: MarkdownASTNode,
9
9
  builder: SpannableStringBuilder,
10
10
  onLinkPress: ((String) -> Unit)?,
11
+ onLinkLongPress: ((String) -> Unit)?,
11
12
  factory: RendererFactory,
12
13
  ) {
13
- factory.renderChildren(node, builder, onLinkPress)
14
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
14
15
  }
15
16
  }
@@ -12,9 +12,10 @@ class EmphasisRenderer(
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
  EmphasisSpan(factory.styleCache, blockStyle),
20
21
  start,
@@ -1,10 +1,12 @@
1
1
  package com.swmansion.enriched.markdown.renderer
2
2
 
3
3
  import android.text.SpannableStringBuilder
4
+ import android.text.style.AlignmentSpan
4
5
  import com.swmansion.enriched.markdown.parser.MarkdownASTNode
5
6
  import com.swmansion.enriched.markdown.spans.HeadingSpan
6
7
  import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
8
  import com.swmansion.enriched.markdown.utils.applyMarginBottom
9
+ import com.swmansion.enriched.markdown.utils.applyMarginTop
8
10
  import com.swmansion.enriched.markdown.utils.createLineHeightSpan
9
11
 
10
12
  class HeadingRenderer(
@@ -14,6 +16,7 @@ class HeadingRenderer(
14
16
  node: MarkdownASTNode,
15
17
  builder: SpannableStringBuilder,
16
18
  onLinkPress: ((String) -> Unit)?,
19
+ onLinkLongPress: ((String) -> Unit)?,
17
20
  factory: RendererFactory,
18
21
  ) {
19
22
  val level = node.getAttribute("level")?.toIntOrNull() ?: 1
@@ -23,13 +26,14 @@ class HeadingRenderer(
23
26
  factory.blockStyleContext.setHeadingStyle(headingStyle, level)
24
27
 
25
28
  try {
26
- factory.renderChildren(node, builder, onLinkPress)
29
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
27
30
  } finally {
28
31
  factory.blockStyleContext.clearBlockStyle()
29
32
  }
30
33
 
31
34
  val end = builder.length
32
35
  val contentLength = end - start
36
+
33
37
  if (contentLength > 0) {
34
38
  builder.setSpan(
35
39
  HeadingSpan(
@@ -48,7 +52,19 @@ class HeadingRenderer(
48
52
  SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
49
53
  )
50
54
 
51
- applyMarginBottom(builder, start, headingStyle.marginBottom)
55
+ // Only apply AlignmentSpan for non-default alignments (Center/Right).
56
+ // Justify is handled at the TextView level (API 26+).
57
+ if (headingStyle.textAlign.needsAlignmentSpan) {
58
+ builder.setSpan(
59
+ AlignmentSpan.Standard(headingStyle.textAlign.layoutAlignment),
60
+ start,
61
+ end,
62
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
63
+ )
64
+ }
65
+
66
+ applyMarginTop(builder, start, headingStyle.marginTop)
67
+ applyMarginBottom(builder, headingStyle.marginBottom)
52
68
  }
53
69
  }
54
70
  }
@@ -15,6 +15,7 @@ class ImageRenderer(
15
15
  node: MarkdownASTNode,
16
16
  builder: SpannableStringBuilder,
17
17
  onLinkPress: ((String) -> Unit)?,
18
+ onLinkLongPress: ((String) -> Unit)?,
18
19
  factory: RendererFactory,
19
20
  ) {
20
21
  val imageUrl = node.getAttribute("url") ?: return
@@ -22,20 +23,21 @@ class ImageRenderer(
22
23
  val isInline = builder.isInlineImage()
23
24
  val start = builder.length
24
25
 
25
- // 1. Append the placeholder character
26
+ // Append Object Replacement Character as the span anchor
26
27
  builder.append("\uFFFC")
27
28
  val end = builder.length
28
29
 
29
- // 2. Create the Span
30
+ val altText = extractTextFromNode(node)
31
+
30
32
  val span =
31
33
  ImageSpan(
32
34
  context = context,
33
35
  imageUrl = imageUrl,
34
36
  styleConfig = config.style,
35
37
  isInline = isInline,
38
+ altText = altText,
36
39
  )
37
40
 
38
- // 3. Attach it to the builder
39
41
  builder.setSpan(
40
42
  span,
41
43
  start,
@@ -43,10 +45,24 @@ class ImageRenderer(
43
45
  SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
44
46
  )
45
47
 
46
- // 4. REPORT the span to the collector via the factory
48
+ // Notify factory for external span tracking/collection
47
49
  factory.registerImageSpan(span)
50
+ }
51
+
52
+ /**
53
+ * Recursively extracts text content from children to use as alt text.
54
+ */
55
+ private fun extractTextFromNode(node: MarkdownASTNode): String {
56
+ val buffer = StringBuilder()
57
+ appendChildText(node, buffer)
58
+ return buffer.toString().trim()
59
+ }
48
60
 
49
- // Note: marginBottom for images is handled by ParagraphRenderer when the paragraph contains only an image
50
- // This ensures consistent spacing behavior and prevents paragraph's marginBottom from affecting images
61
+ private fun appendChildText(
62
+ node: MarkdownASTNode,
63
+ buffer: StringBuilder,
64
+ ) {
65
+ node.content?.let { buffer.append(it) }
66
+ node.children.forEach { child -> appendChildText(child, buffer) }
51
67
  }
52
68
  }
@@ -8,6 +8,7 @@ class LineBreakRenderer : NodeRenderer {
8
8
  node: MarkdownASTNode,
9
9
  builder: SpannableStringBuilder,
10
10
  onLinkPress: ((String) -> Unit)?,
11
+ onLinkLongPress: ((String) -> Unit)?,
11
12
  factory: RendererFactory,
12
13
  ) {
13
14
  builder.append("\n")
@@ -12,13 +12,14 @@ class LinkRenderer(
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
  val url = node.getAttribute("url") ?: return
18
19
 
19
- factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress) }) { start, end, blockStyle ->
20
+ factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress, onLinkLongPress) }) { start, end, blockStyle ->
20
21
  builder.setSpan(
21
- LinkSpan(url, onLinkPress, factory.styleCache, blockStyle, factory.context),
22
+ LinkSpan(url, onLinkPress, onLinkLongPress, factory.styleCache, blockStyle, factory.context),
22
23
  start,
23
24
  end,
24
25
  SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
@@ -13,6 +13,7 @@ class ListItemRenderer(
13
13
  node: MarkdownASTNode,
14
14
  builder: SpannableStringBuilder,
15
15
  onLinkPress: ((String) -> Unit)?,
16
+ onLinkLongPress: ((String) -> Unit)?,
16
17
  factory: RendererFactory,
17
18
  ) {
18
19
  val styleContext = factory.blockStyleContext
@@ -25,7 +26,7 @@ class ListItemRenderer(
25
26
  }
26
27
 
27
28
  // 2. Render Children
28
- factory.renderChildren(node, builder, onLinkPress)
29
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
29
30
 
30
31
  // 3. Normalize whitespace: Ensure the item ends with exactly one newline
31
32
  if (builder.length == start || builder.substring(start).isBlank()) return
@@ -4,6 +4,7 @@ import android.text.SpannableStringBuilder
4
4
  import com.swmansion.enriched.markdown.parser.MarkdownASTNode
5
5
  import com.swmansion.enriched.markdown.spans.MarginBottomSpan
6
6
  import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
+ import com.swmansion.enriched.markdown.utils.applyMarginTop
7
8
  import com.swmansion.enriched.markdown.utils.createLineHeightSpan
8
9
 
9
10
  class ListRenderer(
@@ -14,30 +15,37 @@ class ListRenderer(
14
15
  node: MarkdownASTNode,
15
16
  builder: SpannableStringBuilder,
16
17
  onLinkPress: ((String) -> Unit)?,
18
+ onLinkLongPress: ((String) -> Unit)?,
17
19
  factory: RendererFactory,
18
20
  ) {
19
21
  val start = builder.length
20
22
  val listStyle = config.style.listStyle
21
23
  val listType = if (isOrdered) BlockStyleContext.ListType.ORDERED else BlockStyleContext.ListType.UNORDERED
22
24
 
23
- // 1. Context Lifecycle Management
24
25
  val contextManager = ListContextManager(factory.blockStyleContext, config.style)
25
26
  val entryState = contextManager.enterList(listType, listStyle)
26
27
 
27
- // 2. Nested List Isolation
28
+ // For top-level lists, insert marginTop spacer before rendering content
29
+ if (entryState.previousDepth == 0) {
30
+ applyMarginTop(builder, start, listStyle.marginTop)
31
+ }
32
+
33
+ // Track start index after the potential 1-character marginTop spacer
34
+ val contentStart = if (entryState.previousDepth == 0 && listStyle.marginTop > 0) start + 1 else start
35
+
36
+ // Ensure nested lists start on a new line if the parent hasn't provided one
28
37
  if (entryState.previousDepth > 0 && builder.isNotEmpty() && builder.last() != '\n') {
29
38
  builder.append("\n")
30
39
  }
31
40
 
32
41
  try {
33
- factory.renderChildren(node, builder, onLinkPress)
42
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
34
43
  } finally {
35
44
  contextManager.exitList(entryState)
36
45
  }
37
46
 
38
- // 3. Spacing & Styling
39
- if (builder.length > start) {
40
- applyListSpacing(builder, start, entryState.previousDepth, listStyle)
47
+ if (builder.length > contentStart) {
48
+ applyListSpacing(builder, contentStart, entryState.previousDepth, listStyle)
41
49
  }
42
50
  }
43
51
 
@@ -47,7 +55,6 @@ class ListRenderer(
47
55
  depth: Int,
48
56
  style: com.swmansion.enriched.markdown.styles.BaseBlockStyle,
49
57
  ) {
50
- // Apply line height to the entire list block
51
58
  builder.setSpan(
52
59
  createLineHeightSpan(style.lineHeight),
53
60
  start,
@@ -55,8 +62,8 @@ class ListRenderer(
55
62
  SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
56
63
  )
57
64
 
58
- // Only apply bottom margin for top-level lists
59
- if (depth == 0 && style.marginBottom > 0) {
65
+ // External bottom margin is only handled by the root-level list
66
+ if (depth == 0) {
60
67
  builder.append("\n")
61
68
  builder.setSpan(
62
69
  MarginBottomSpan(style.marginBottom),
@@ -11,6 +11,7 @@ interface NodeRenderer {
11
11
  node: MarkdownASTNode,
12
12
  builder: SpannableStringBuilder,
13
13
  onLinkPress: ((String) -> Unit)?,
14
+ onLinkLongPress: ((String) -> Unit)?,
14
15
  factory: RendererFactory,
15
16
  )
16
17
  }
@@ -49,6 +50,8 @@ class RendererFactory(
49
50
  MarkdownASTNode.NodeType.Link to LinkRenderer(config),
50
51
  MarkdownASTNode.NodeType.Strong to StrongRenderer(config),
51
52
  MarkdownASTNode.NodeType.Emphasis to EmphasisRenderer(config),
53
+ MarkdownASTNode.NodeType.Strikethrough to StrikethroughRenderer(config),
54
+ MarkdownASTNode.NodeType.Underline to UnderlineRenderer(config),
52
55
  MarkdownASTNode.NodeType.Code to CodeRenderer(config),
53
56
  MarkdownASTNode.NodeType.Image to ImageRenderer(config, context),
54
57
  MarkdownASTNode.NodeType.LineBreak to lineBreakRenderer,
@@ -73,9 +76,10 @@ class RendererFactory(
73
76
  node: MarkdownASTNode,
74
77
  builder: SpannableStringBuilder,
75
78
  onLinkPress: ((String) -> Unit)?,
79
+ onLinkLongPress: ((String) -> Unit)?,
76
80
  ) {
77
81
  node.children.forEach { child ->
78
- getRenderer(child).render(child, builder, onLinkPress, this)
82
+ getRenderer(child).render(child, builder, onLinkPress, onLinkLongPress, this)
79
83
  }
80
84
  }
81
85
 
@@ -1,13 +1,14 @@
1
1
  package com.swmansion.enriched.markdown.renderer
2
2
 
3
3
  import android.text.SpannableStringBuilder
4
+ import android.text.style.AlignmentSpan
4
5
  import com.swmansion.enriched.markdown.parser.MarkdownASTNode
5
6
  import com.swmansion.enriched.markdown.styles.ParagraphStyle
6
7
  import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
7
8
  import com.swmansion.enriched.markdown.utils.applyMarginBottom
9
+ import com.swmansion.enriched.markdown.utils.applyMarginTop
8
10
  import com.swmansion.enriched.markdown.utils.containsBlockImage
9
11
  import com.swmansion.enriched.markdown.utils.createLineHeightSpan
10
- import com.swmansion.enriched.markdown.utils.getMarginBottomForParagraph
11
12
 
12
13
  class ParagraphRenderer(
13
14
  private val config: RendererConfig,
@@ -16,29 +17,28 @@ class ParagraphRenderer(
16
17
  node: MarkdownASTNode,
17
18
  builder: SpannableStringBuilder,
18
19
  onLinkPress: ((String) -> Unit)?,
20
+ onLinkLongPress: ((String) -> Unit)?,
19
21
  factory: RendererFactory,
20
22
  ) {
21
23
  val context = factory.blockStyleContext
22
24
 
23
- // If nested, just render content and a newline
25
+ // If nested (e.g., inside a list or blockquote), render content simply with a newline
24
26
  if (context.isInsideBlockElement()) {
25
- factory.renderChildren(node, builder, onLinkPress)
27
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
26
28
  builder.append("\n")
27
29
  return
28
30
  }
29
31
 
30
- // Top-level paragraph rendering
31
32
  val start = builder.length
32
33
  val style = config.style.paragraphStyle
33
34
 
34
35
  context.setParagraphStyle(style)
35
36
  try {
36
- factory.renderChildren(node, builder, onLinkPress)
37
+ factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
37
38
  } finally {
38
39
  context.clearBlockStyle()
39
40
  }
40
41
 
41
- // Apply spans only if content was actually added
42
42
  if (builder.length > start) {
43
43
  builder.applySpans(node, style, start)
44
44
  }
@@ -49,8 +49,9 @@ class ParagraphRenderer(
49
49
  style: ParagraphStyle,
50
50
  start: Int,
51
51
  ) {
52
- val end = length // Current length is the end point
52
+ val end = length
53
53
 
54
+ // LineHeightSpan is avoided for block images to prevent clipping/overlapping
54
55
  if (!node.containsBlockImage()) {
55
56
  setSpan(
56
57
  createLineHeightSpan(style.lineHeight),
@@ -60,7 +61,20 @@ class ParagraphRenderer(
60
61
  )
61
62
  }
62
63
 
63
- val margin = getMarginBottomForParagraph(node, style, config.style)
64
- applyMarginBottom(this, start, margin)
64
+ // Only apply AlignmentSpan for non-default alignments (Center/Right)
65
+ if (style.textAlign.needsAlignmentSpan) {
66
+ setSpan(
67
+ AlignmentSpan.Standard(style.textAlign.layoutAlignment),
68
+ start,
69
+ end,
70
+ SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
71
+ )
72
+ }
73
+
74
+ val marginTop = if (node.containsBlockImage()) config.style.imageStyle.marginTop else style.marginTop
75
+ applyMarginTop(this, start, marginTop)
76
+
77
+ val marginBottom = if (node.containsBlockImage()) config.style.imageStyle.marginBottom else style.marginBottom
78
+ applyMarginBottom(this, marginBottom)
65
79
  }
66
80
  }
@@ -14,6 +14,7 @@ class Renderer {
14
14
  private var cachedContext: Context? = null
15
15
 
16
16
  private val collectedImageSpans = mutableListOf<ImageSpan>()
17
+ private var lastElementMarginBottom: Float = 0f
17
18
 
18
19
  fun configure(
19
20
  style: StyleConfig,
@@ -33,6 +34,7 @@ class Renderer {
33
34
  fun renderDocument(
34
35
  document: MarkdownASTNode,
35
36
  onLinkPress: ((String) -> Unit)? = null,
37
+ onLinkLongPress: ((String) -> Unit)? = null,
36
38
  ): SpannableString {
37
39
  val factory =
38
40
  requireNotNull(cachedFactory) {
@@ -41,10 +43,11 @@ class Renderer {
41
43
 
42
44
  factory.resetForNewRender()
43
45
  collectedImageSpans.clear()
46
+ lastElementMarginBottom = 0f
44
47
 
45
48
  val builder = SpannableStringBuilder()
46
49
 
47
- renderNode(document, builder, onLinkPress, factory)
50
+ renderNode(document, builder, onLinkPress, onLinkLongPress, factory)
48
51
 
49
52
  // Remove trailing margin from last block element
50
53
  removeTrailingMargin(builder)
@@ -52,33 +55,44 @@ class Renderer {
52
55
  return SpannableString(builder)
53
56
  }
54
57
 
55
- /** Removes trailing margin to eliminate bottom spacing */
58
+ /** Removes trailing newlines and captures the margin of the final element. */
56
59
  private fun removeTrailingMargin(builder: SpannableStringBuilder) {
57
60
  if (builder.isEmpty()) return
58
61
 
59
- val spans = builder.getSpans(0, builder.length, MarginBottomSpan::class.java)
60
- if (spans.isEmpty()) return
62
+ // Identify the last margin span and store its value
63
+ val lastSpan =
64
+ builder
65
+ .getSpans(0, builder.length, MarginBottomSpan::class.java)
66
+ .maxByOrNull { builder.getSpanEnd(it) }
61
67
 
62
- val lastSpan = spans.maxByOrNull { builder.getSpanEnd(it) } ?: return
63
- val spanEnd = builder.getSpanEnd(lastSpan)
68
+ lastElementMarginBottom = lastSpan?.marginBottom ?: 0f
64
69
 
65
- // Remove trailing newlines (added for block spacing)
66
- while (builder.isNotEmpty() && builder.last() == '\n') {
70
+ // Trim trailing newlines
71
+ while (builder.endsWith('\n')) {
67
72
  builder.delete(builder.length - 1, builder.length)
68
73
  }
69
74
 
70
- if (spanEnd >= builder.length) {
75
+ // Clean up the span if it no longer covers any text
76
+ if (lastSpan != null && builder.getSpanEnd(lastSpan) >= builder.length) {
71
77
  builder.removeSpan(lastSpan)
72
78
  }
73
79
  }
74
80
 
81
+ /**
82
+ * Returns the marginBottom value of the last element in the document.
83
+ * This is dynamically determined from the actual last element (paragraph, image, heading, etc.)
84
+ * and can be used in MeasurementStore to adjust the measured height.
85
+ */
86
+ fun getLastElementMarginBottom(): Float = lastElementMarginBottom
87
+
75
88
  private fun renderNode(
76
89
  node: MarkdownASTNode,
77
90
  builder: SpannableStringBuilder,
78
91
  onLinkPress: ((String) -> Unit)?,
92
+ onLinkLongPress: ((String) -> Unit)?,
79
93
  factory: RendererFactory,
80
94
  ) {
81
- factory.getRenderer(node).render(node, builder, onLinkPress, factory)
95
+ factory.getRenderer(node).render(node, builder, onLinkPress, onLinkLongPress, factory)
82
96
  }
83
97
 
84
98
  /**
@@ -12,6 +12,7 @@ class SpanStyleCache(
12
12
 
13
13
  val strongColor: Int? = style.strongStyle.color
14
14
  val emphasisColor: Int? = style.emphasisStyle.color
15
+ val strikethroughColor: Int = style.strikethroughStyle.color
15
16
  val linkColor: Int = style.linkStyle.color
16
17
  val linkUnderline: Boolean = style.linkStyle.underline
17
18
  val codeColor: Int = style.codeStyle.color