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.
- package/README.md +80 -8
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +17 -2
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +6 -1
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +28 -3
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +225 -1
- package/android/src/main/cpp/jni-adapter.cpp +28 -11
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +132 -15
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +1 -16
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +67 -13
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +241 -21
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +17 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +13 -18
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +23 -24
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +18 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +22 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +16 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +23 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +24 -10
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +8 -17
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +19 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +3 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +32 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +22 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +23 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +58 -56
- package/android/src/main/jni/CMakeLists.txt +1 -13
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +0 -13
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +2 -14
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +3 -0
- package/cpp/parser/MD4CParser.cpp +21 -8
- package/cpp/parser/MD4CParser.hpp +5 -1
- package/cpp/parser/MarkdownASTNode.hpp +2 -0
- package/ios/EnrichedMarkdownText.mm +356 -29
- package/ios/attachments/{ImageAttachment.h → EnrichedMarkdownImageAttachment.h} +1 -1
- package/ios/attachments/{ImageAttachment.m → EnrichedMarkdownImageAttachment.m} +4 -4
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +28 -3
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +225 -1
- package/ios/parser/MarkdownASTNode.h +2 -0
- package/ios/parser/MarkdownParser.h +9 -0
- package/ios/parser/MarkdownParser.mm +31 -2
- package/ios/parser/MarkdownParserBridge.mm +13 -3
- package/ios/renderer/AttributedRenderer.h +2 -0
- package/ios/renderer/AttributedRenderer.m +52 -19
- package/ios/renderer/BlockquoteRenderer.m +7 -6
- package/ios/renderer/CodeBlockRenderer.m +9 -8
- package/ios/renderer/HeadingRenderer.m +31 -24
- package/ios/renderer/ImageRenderer.m +31 -10
- package/ios/renderer/ListItemRenderer.m +51 -39
- package/ios/renderer/ListRenderer.m +21 -18
- package/ios/renderer/ParagraphRenderer.m +27 -16
- package/ios/renderer/RenderContext.h +17 -0
- package/ios/renderer/RenderContext.m +66 -2
- package/ios/renderer/RendererFactory.m +6 -0
- package/ios/renderer/StrikethroughRenderer.h +6 -0
- package/ios/renderer/StrikethroughRenderer.m +40 -0
- package/ios/renderer/UnderlineRenderer.h +6 -0
- package/ios/renderer/UnderlineRenderer.m +39 -0
- package/ios/styles/StyleConfig.h +46 -0
- package/ios/styles/StyleConfig.mm +351 -12
- package/ios/utils/AccessibilityInfo.h +35 -0
- package/ios/utils/AccessibilityInfo.m +24 -0
- package/ios/utils/CodeBlockBackground.m +4 -9
- package/ios/utils/FontUtils.h +5 -0
- package/ios/utils/FontUtils.m +14 -0
- package/ios/utils/HTMLGenerator.m +21 -7
- package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
- package/ios/utils/MarkdownExtractor.m +18 -5
- package/ios/utils/ParagraphStyleUtils.h +10 -2
- package/ios/utils/ParagraphStyleUtils.m +57 -2
- package/ios/utils/PasteboardUtils.h +1 -1
- package/ios/utils/PasteboardUtils.m +3 -3
- package/lib/module/EnrichedMarkdownText.js +33 -2
- package/lib/module/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +83 -3
- package/lib/module/index.js +0 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +58 -14
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownText.d.ts +85 -3
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +75 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownText.tsx +133 -5
- package/src/EnrichedMarkdownTextNativeComponent.ts +83 -3
- package/src/index.tsx +5 -2
- package/src/normalizeMarkdownStyle.ts +46 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +0 -9
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +0 -25
package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
59
|
-
if (depth == 0
|
|
60
|
-
|
|
61
|
-
builder.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
48
|
+
contentStart,
|
|
46
49
|
end,
|
|
47
50
|
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
48
51
|
)
|
|
49
52
|
|
|
50
|
-
//
|
|
53
|
+
// Apply vertical padding via line height manipulation
|
|
51
54
|
builder.setSpan(
|
|
52
55
|
CodeBlockBoundaryPaddingSpan(padding),
|
|
53
|
-
|
|
56
|
+
contentStart,
|
|
54
57
|
end,
|
|
55
58
|
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
56
59
|
)
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
26
|
+
// Append Object Replacement Character as the span anchor
|
|
26
27
|
builder.append("\uFFFC")
|
|
27
28
|
val end = builder.length
|
|
28
29
|
|
|
29
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
50
|
-
|
|
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
|
}
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
//
|
|
59
|
-
if (depth == 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,
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
val spanEnd = builder.getSpanEnd(lastSpan)
|
|
68
|
+
lastElementMarginBottom = lastSpan?.marginBottom ?: 0f
|
|
64
69
|
|
|
65
|
-
//
|
|
66
|
-
while (builder.
|
|
70
|
+
// Trim trailing newlines
|
|
71
|
+
while (builder.endsWith('\n')) {
|
|
67
72
|
builder.delete(builder.length - 1, builder.length)
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
if
|
|
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
|