react-native-enriched-markdown 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +551 -0
- package/ReactNativeEnrichedMarkdown.podspec +27 -0
- package/android/build.gradle +101 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +54 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +26 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +33 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +82 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1388 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.h +20 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/baseline-prof.txt +65 -0
- package/android/src/main/cpp/jni-adapter.cpp +220 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +270 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +173 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextPackage.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +385 -0
- 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/events/LinkPressEvent.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +31 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +62 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +84 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +104 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +70 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +68 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +59 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +103 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +80 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +109 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +86 -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 +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +45 -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/BaseListSpan.kt +136 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +321 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +57 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +211 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +92 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -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/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +827 -0
- 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 +375 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +183 -0
- package/android/src/main/jni/CMakeLists.txt +70 -0
- package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
- package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +20 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +37 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +22 -0
- package/cpp/md4c/md4c.c +6492 -0
- package/cpp/md4c/md4c.h +402 -0
- package/cpp/parser/MD4CParser.cpp +327 -0
- package/cpp/parser/MD4CParser.hpp +27 -0
- package/cpp/parser/MarkdownASTNode.hpp +51 -0
- package/ios/EnrichedMarkdownText.h +18 -0
- package/ios/EnrichedMarkdownText.mm +1401 -0
- package/ios/attachments/EnrichedMarkdownImageAttachment.h +23 -0
- package/ios/attachments/EnrichedMarkdownImageAttachment.m +185 -0
- package/ios/attachments/ThematicBreakAttachment.h +15 -0
- package/ios/attachments/ThematicBreakAttachment.m +33 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +33 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +82 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1388 -0
- package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
- package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
- package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
- package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
- package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
- package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
- package/ios/internals/EnrichedMarkdownTextState.h +24 -0
- package/ios/parser/MarkdownASTNode.h +35 -0
- package/ios/parser/MarkdownASTNode.m +32 -0
- package/ios/parser/MarkdownParser.h +17 -0
- package/ios/parser/MarkdownParser.mm +42 -0
- package/ios/parser/MarkdownParserBridge.mm +120 -0
- package/ios/renderer/AttributedRenderer.h +11 -0
- package/ios/renderer/AttributedRenderer.m +152 -0
- package/ios/renderer/BlockquoteRenderer.h +7 -0
- package/ios/renderer/BlockquoteRenderer.m +160 -0
- package/ios/renderer/CodeBlockRenderer.h +10 -0
- package/ios/renderer/CodeBlockRenderer.m +90 -0
- package/ios/renderer/CodeRenderer.h +10 -0
- package/ios/renderer/CodeRenderer.m +60 -0
- package/ios/renderer/EmphasisRenderer.h +6 -0
- package/ios/renderer/EmphasisRenderer.m +96 -0
- package/ios/renderer/HeadingRenderer.h +7 -0
- package/ios/renderer/HeadingRenderer.m +105 -0
- package/ios/renderer/ImageRenderer.h +12 -0
- package/ios/renderer/ImageRenderer.m +83 -0
- package/ios/renderer/LinkRenderer.h +7 -0
- package/ios/renderer/LinkRenderer.m +69 -0
- package/ios/renderer/ListItemRenderer.h +16 -0
- package/ios/renderer/ListItemRenderer.m +103 -0
- package/ios/renderer/ListRenderer.h +13 -0
- package/ios/renderer/ListRenderer.m +70 -0
- package/ios/renderer/NodeRenderer.h +8 -0
- package/ios/renderer/ParagraphRenderer.h +7 -0
- package/ios/renderer/ParagraphRenderer.m +80 -0
- package/ios/renderer/RenderContext.h +105 -0
- package/ios/renderer/RenderContext.m +312 -0
- package/ios/renderer/RendererFactory.h +12 -0
- package/ios/renderer/RendererFactory.m +116 -0
- package/ios/renderer/StrikethroughRenderer.h +6 -0
- package/ios/renderer/StrikethroughRenderer.m +40 -0
- package/ios/renderer/StrongRenderer.h +6 -0
- package/ios/renderer/StrongRenderer.m +83 -0
- package/ios/renderer/TextRenderer.h +6 -0
- package/ios/renderer/TextRenderer.m +16 -0
- package/ios/renderer/ThematicBreakRenderer.h +5 -0
- package/ios/renderer/ThematicBreakRenderer.m +53 -0
- package/ios/renderer/UnderlineRenderer.h +6 -0
- package/ios/renderer/UnderlineRenderer.m +39 -0
- package/ios/styles/StyleConfig.h +274 -0
- package/ios/styles/StyleConfig.mm +1806 -0
- package/ios/utils/AccessibilityInfo.h +35 -0
- package/ios/utils/AccessibilityInfo.m +24 -0
- package/ios/utils/BlockquoteBorder.h +20 -0
- package/ios/utils/BlockquoteBorder.m +92 -0
- package/ios/utils/CodeBackground.h +19 -0
- package/ios/utils/CodeBackground.m +191 -0
- package/ios/utils/CodeBlockBackground.h +17 -0
- package/ios/utils/CodeBlockBackground.m +82 -0
- package/ios/utils/EditMenuUtils.h +22 -0
- package/ios/utils/EditMenuUtils.m +118 -0
- package/ios/utils/FontUtils.h +25 -0
- package/ios/utils/FontUtils.m +27 -0
- package/ios/utils/HTMLGenerator.h +20 -0
- package/ios/utils/HTMLGenerator.m +793 -0
- package/ios/utils/LastElementUtils.h +53 -0
- package/ios/utils/ListMarkerDrawer.h +15 -0
- package/ios/utils/ListMarkerDrawer.m +127 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
- package/ios/utils/MarkdownExtractor.h +17 -0
- package/ios/utils/MarkdownExtractor.m +308 -0
- package/ios/utils/ParagraphStyleUtils.h +21 -0
- package/ios/utils/ParagraphStyleUtils.m +111 -0
- package/ios/utils/PasteboardUtils.h +36 -0
- package/ios/utils/PasteboardUtils.m +134 -0
- package/ios/utils/RTFExportUtils.h +24 -0
- package/ios/utils/RTFExportUtils.m +297 -0
- package/ios/utils/RuntimeKeys.h +38 -0
- package/ios/utils/RuntimeKeys.m +11 -0
- package/ios/utils/TextViewLayoutManager.h +14 -0
- package/ios/utils/TextViewLayoutManager.mm +113 -0
- package/lib/module/EnrichedMarkdownText.js +65 -0
- package/lib/module/EnrichedMarkdownText.js.map +1 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +210 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalizeMarkdownStyle.js +384 -0
- package/lib/module/normalizeMarkdownStyle.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts +183 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +185 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
- package/package.json +186 -1
- package/react-native.config.js +13 -0
- package/src/EnrichedMarkdownText.tsx +280 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +210 -0
- package/src/index.tsx +10 -0
- package/src/normalizeMarkdownStyle.ts +423 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.text.SpannableStringBuilder
|
|
5
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
6
|
+
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
7
|
+
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
8
|
+
|
|
9
|
+
interface NodeRenderer {
|
|
10
|
+
fun render(
|
|
11
|
+
node: MarkdownASTNode,
|
|
12
|
+
builder: SpannableStringBuilder,
|
|
13
|
+
onLinkPress: ((String) -> Unit)?,
|
|
14
|
+
onLinkLongPress: ((String) -> Unit)?,
|
|
15
|
+
factory: RendererFactory,
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
data class RendererConfig(
|
|
20
|
+
val style: StyleConfig,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
class RendererFactory(
|
|
24
|
+
private val config: RendererConfig,
|
|
25
|
+
val context: Context,
|
|
26
|
+
private val onImageSpanCreated: (ImageSpan) -> Unit,
|
|
27
|
+
) {
|
|
28
|
+
val blockStyleContext = BlockStyleContext()
|
|
29
|
+
|
|
30
|
+
val styleCache = SpanStyleCache(config.style)
|
|
31
|
+
|
|
32
|
+
fun resetForNewRender() {
|
|
33
|
+
blockStyleContext.resetForNewRender()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private val textRenderer = TextRenderer()
|
|
37
|
+
private val lineBreakRenderer = LineBreakRenderer()
|
|
38
|
+
|
|
39
|
+
private val renderers: Map<MarkdownASTNode.NodeType, NodeRenderer> by lazy {
|
|
40
|
+
mapOf(
|
|
41
|
+
MarkdownASTNode.NodeType.Document to DocumentRenderer(),
|
|
42
|
+
MarkdownASTNode.NodeType.Paragraph to ParagraphRenderer(config),
|
|
43
|
+
MarkdownASTNode.NodeType.Heading to HeadingRenderer(config),
|
|
44
|
+
MarkdownASTNode.NodeType.Blockquote to BlockquoteRenderer(config),
|
|
45
|
+
MarkdownASTNode.NodeType.CodeBlock to CodeBlockRenderer(config),
|
|
46
|
+
MarkdownASTNode.NodeType.UnorderedList to ListRenderer(config, isOrdered = false),
|
|
47
|
+
MarkdownASTNode.NodeType.OrderedList to ListRenderer(config, isOrdered = true),
|
|
48
|
+
MarkdownASTNode.NodeType.ListItem to ListItemRenderer(config),
|
|
49
|
+
MarkdownASTNode.NodeType.Text to textRenderer,
|
|
50
|
+
MarkdownASTNode.NodeType.Link to LinkRenderer(config),
|
|
51
|
+
MarkdownASTNode.NodeType.Strong to StrongRenderer(config),
|
|
52
|
+
MarkdownASTNode.NodeType.Emphasis to EmphasisRenderer(config),
|
|
53
|
+
MarkdownASTNode.NodeType.Strikethrough to StrikethroughRenderer(config),
|
|
54
|
+
MarkdownASTNode.NodeType.Underline to UnderlineRenderer(config),
|
|
55
|
+
MarkdownASTNode.NodeType.Code to CodeRenderer(config),
|
|
56
|
+
MarkdownASTNode.NodeType.Image to ImageRenderer(config, context),
|
|
57
|
+
MarkdownASTNode.NodeType.LineBreak to lineBreakRenderer,
|
|
58
|
+
MarkdownASTNode.NodeType.ThematicBreak to ThematicBreakRenderer(config),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Called by ImageRenderer to report a new span to the collector.
|
|
64
|
+
*/
|
|
65
|
+
fun registerImageSpan(span: ImageSpan) {
|
|
66
|
+
onImageSpanCreated(span)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun getRenderer(node: MarkdownASTNode): NodeRenderer =
|
|
70
|
+
renderers[node.type] ?: run {
|
|
71
|
+
android.util.Log.w("RendererFactory", "No renderer for: ${node.type}")
|
|
72
|
+
textRenderer
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun renderChildren(
|
|
76
|
+
node: MarkdownASTNode,
|
|
77
|
+
builder: SpannableStringBuilder,
|
|
78
|
+
onLinkPress: ((String) -> Unit)?,
|
|
79
|
+
onLinkLongPress: ((String) -> Unit)?,
|
|
80
|
+
) {
|
|
81
|
+
node.children.forEach { child ->
|
|
82
|
+
getRenderer(child).render(child, builder, onLinkPress, onLinkLongPress, this)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Improved helper for applying spans to blocks of text.
|
|
88
|
+
*/
|
|
89
|
+
inline fun renderWithSpan(
|
|
90
|
+
builder: SpannableStringBuilder,
|
|
91
|
+
renderContent: () -> Unit,
|
|
92
|
+
applySpan: (start: Int, end: Int, blockStyle: BlockStyle) -> Unit,
|
|
93
|
+
) {
|
|
94
|
+
val start = builder.length
|
|
95
|
+
renderContent()
|
|
96
|
+
val end = builder.length
|
|
97
|
+
|
|
98
|
+
if (end > start) {
|
|
99
|
+
val blockStyle = blockStyleContext.requireBlockStyle()
|
|
100
|
+
applySpan(start, end, blockStyle)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.text.SpannableStringBuilder
|
|
4
|
+
import android.text.style.AlignmentSpan
|
|
5
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
6
|
+
import com.swmansion.enriched.markdown.styles.ParagraphStyle
|
|
7
|
+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
8
|
+
import com.swmansion.enriched.markdown.utils.applyMarginBottom
|
|
9
|
+
import com.swmansion.enriched.markdown.utils.applyMarginTop
|
|
10
|
+
import com.swmansion.enriched.markdown.utils.containsBlockImage
|
|
11
|
+
import com.swmansion.enriched.markdown.utils.createLineHeightSpan
|
|
12
|
+
|
|
13
|
+
class ParagraphRenderer(
|
|
14
|
+
private val config: RendererConfig,
|
|
15
|
+
) : NodeRenderer {
|
|
16
|
+
override fun render(
|
|
17
|
+
node: MarkdownASTNode,
|
|
18
|
+
builder: SpannableStringBuilder,
|
|
19
|
+
onLinkPress: ((String) -> Unit)?,
|
|
20
|
+
onLinkLongPress: ((String) -> Unit)?,
|
|
21
|
+
factory: RendererFactory,
|
|
22
|
+
) {
|
|
23
|
+
val context = factory.blockStyleContext
|
|
24
|
+
|
|
25
|
+
// If nested (e.g., inside a list or blockquote), render content simply with a newline
|
|
26
|
+
if (context.isInsideBlockElement()) {
|
|
27
|
+
factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
|
|
28
|
+
builder.append("\n")
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
val start = builder.length
|
|
33
|
+
val style = config.style.paragraphStyle
|
|
34
|
+
|
|
35
|
+
context.setParagraphStyle(style)
|
|
36
|
+
try {
|
|
37
|
+
factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
|
|
38
|
+
} finally {
|
|
39
|
+
context.clearBlockStyle()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (builder.length > start) {
|
|
43
|
+
builder.applySpans(node, style, start)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private fun SpannableStringBuilder.applySpans(
|
|
48
|
+
node: MarkdownASTNode,
|
|
49
|
+
style: ParagraphStyle,
|
|
50
|
+
start: Int,
|
|
51
|
+
) {
|
|
52
|
+
val end = length
|
|
53
|
+
|
|
54
|
+
// LineHeightSpan is avoided for block images to prevent clipping/overlapping
|
|
55
|
+
if (!node.containsBlockImage()) {
|
|
56
|
+
setSpan(
|
|
57
|
+
createLineHeightSpan(style.lineHeight),
|
|
58
|
+
start,
|
|
59
|
+
end,
|
|
60
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
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)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.text.SpannableString
|
|
5
|
+
import android.text.SpannableStringBuilder
|
|
6
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
7
|
+
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
8
|
+
import com.swmansion.enriched.markdown.spans.MarginBottomSpan
|
|
9
|
+
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
10
|
+
|
|
11
|
+
class Renderer {
|
|
12
|
+
private var cachedFactory: RendererFactory? = null
|
|
13
|
+
private var cachedStyle: StyleConfig? = null
|
|
14
|
+
private var cachedContext: Context? = null
|
|
15
|
+
|
|
16
|
+
private val collectedImageSpans = mutableListOf<ImageSpan>()
|
|
17
|
+
private var lastElementMarginBottom: Float = 0f
|
|
18
|
+
|
|
19
|
+
fun configure(
|
|
20
|
+
style: StyleConfig,
|
|
21
|
+
context: Context,
|
|
22
|
+
) {
|
|
23
|
+
if (cachedStyle === style && cachedContext === context) return
|
|
24
|
+
|
|
25
|
+
cachedStyle = style
|
|
26
|
+
cachedContext = context
|
|
27
|
+
cachedFactory =
|
|
28
|
+
RendererFactory(
|
|
29
|
+
RendererConfig(style),
|
|
30
|
+
context,
|
|
31
|
+
) { span -> reportImageSpan(span) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fun renderDocument(
|
|
35
|
+
document: MarkdownASTNode,
|
|
36
|
+
onLinkPress: ((String) -> Unit)? = null,
|
|
37
|
+
onLinkLongPress: ((String) -> Unit)? = null,
|
|
38
|
+
): SpannableString {
|
|
39
|
+
val factory =
|
|
40
|
+
requireNotNull(cachedFactory) {
|
|
41
|
+
"Renderer must be configured with a style before calling renderDocument."
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
factory.resetForNewRender()
|
|
45
|
+
collectedImageSpans.clear()
|
|
46
|
+
lastElementMarginBottom = 0f
|
|
47
|
+
|
|
48
|
+
val builder = SpannableStringBuilder()
|
|
49
|
+
|
|
50
|
+
renderNode(document, builder, onLinkPress, onLinkLongPress, factory)
|
|
51
|
+
|
|
52
|
+
// Remove trailing margin from last block element
|
|
53
|
+
removeTrailingMargin(builder)
|
|
54
|
+
|
|
55
|
+
return SpannableString(builder)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Removes trailing newlines and captures the margin of the final element. */
|
|
59
|
+
private fun removeTrailingMargin(builder: SpannableStringBuilder) {
|
|
60
|
+
if (builder.isEmpty()) return
|
|
61
|
+
|
|
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) }
|
|
67
|
+
|
|
68
|
+
lastElementMarginBottom = lastSpan?.marginBottom ?: 0f
|
|
69
|
+
|
|
70
|
+
// Trim trailing newlines
|
|
71
|
+
while (builder.endsWith('\n')) {
|
|
72
|
+
builder.delete(builder.length - 1, builder.length)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Clean up the span if it no longer covers any text
|
|
76
|
+
if (lastSpan != null && builder.getSpanEnd(lastSpan) >= builder.length) {
|
|
77
|
+
builder.removeSpan(lastSpan)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
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
|
+
|
|
88
|
+
private fun renderNode(
|
|
89
|
+
node: MarkdownASTNode,
|
|
90
|
+
builder: SpannableStringBuilder,
|
|
91
|
+
onLinkPress: ((String) -> Unit)?,
|
|
92
|
+
onLinkLongPress: ((String) -> Unit)?,
|
|
93
|
+
factory: RendererFactory,
|
|
94
|
+
) {
|
|
95
|
+
factory.getRenderer(node).render(node, builder, onLinkPress, onLinkLongPress, factory)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Internal helper used by the Factory's lambda to collect spans.
|
|
100
|
+
*/
|
|
101
|
+
private fun reportImageSpan(span: ImageSpan) {
|
|
102
|
+
collectedImageSpans.add(span)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Provides the EnrichedMarkdownText with the exact list of spans that need registration.
|
|
107
|
+
*/
|
|
108
|
+
fun getCollectedImageSpans(): List<ImageSpan> = collectedImageSpans
|
|
109
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.graphics.Typeface
|
|
4
|
+
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
5
|
+
|
|
6
|
+
/** Shared style cache for spans to avoid redundant calculations. */
|
|
7
|
+
class SpanStyleCache(
|
|
8
|
+
style: StyleConfig,
|
|
9
|
+
) {
|
|
10
|
+
// Colors to preserve when applying inline styles (links, code, strong, emphasis)
|
|
11
|
+
val colorsToPreserve: IntArray = buildColorsToPreserve(style)
|
|
12
|
+
|
|
13
|
+
val strongColor: Int? = style.strongStyle.color
|
|
14
|
+
val emphasisColor: Int? = style.emphasisStyle.color
|
|
15
|
+
val strikethroughColor: Int = style.strikethroughStyle.color
|
|
16
|
+
val linkColor: Int = style.linkStyle.color
|
|
17
|
+
val linkUnderline: Boolean = style.linkStyle.underline
|
|
18
|
+
val codeColor: Int = style.codeStyle.color
|
|
19
|
+
|
|
20
|
+
private fun buildColorsToPreserve(style: StyleConfig): IntArray =
|
|
21
|
+
buildList {
|
|
22
|
+
style.strongStyle.color
|
|
23
|
+
?.takeIf { it != 0 }
|
|
24
|
+
?.let { add(it) }
|
|
25
|
+
style.emphasisStyle.color
|
|
26
|
+
?.takeIf { it != 0 }
|
|
27
|
+
?.let { add(it) }
|
|
28
|
+
style.linkStyle.color
|
|
29
|
+
.takeIf { it != 0 }
|
|
30
|
+
?.let { add(it) }
|
|
31
|
+
style
|
|
32
|
+
.codeStyle
|
|
33
|
+
.color
|
|
34
|
+
.takeIf { it != 0 }
|
|
35
|
+
?.let { add(it) }
|
|
36
|
+
}.toIntArray()
|
|
37
|
+
|
|
38
|
+
fun getStrongColorFor(blockColor: Int): Int = strongColor ?: blockColor
|
|
39
|
+
|
|
40
|
+
fun getEmphasisColorFor(
|
|
41
|
+
blockColor: Int,
|
|
42
|
+
currentColor: Int,
|
|
43
|
+
): Int =
|
|
44
|
+
if (currentColor == blockColor) {
|
|
45
|
+
emphasisColor ?: blockColor
|
|
46
|
+
} else {
|
|
47
|
+
currentColor
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
companion object {
|
|
51
|
+
private val typefaceCache = mutableMapOf<String, Typeface>()
|
|
52
|
+
|
|
53
|
+
/** Cached typeface for font family + style (BOLD, ITALIC, BOLD_ITALIC) */
|
|
54
|
+
fun getTypeface(
|
|
55
|
+
fontFamily: String,
|
|
56
|
+
style: Int,
|
|
57
|
+
): Typeface =
|
|
58
|
+
typefaceCache.getOrPut("$fontFamily|$style") {
|
|
59
|
+
val base =
|
|
60
|
+
fontFamily
|
|
61
|
+
.takeIf { it.isNotEmpty() }
|
|
62
|
+
?.let { Typeface.create(it, Typeface.NORMAL) }
|
|
63
|
+
?: Typeface.DEFAULT
|
|
64
|
+
Typeface.create(base, style)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Cached typeface using weight string (e.g., "bold", "700") */
|
|
68
|
+
fun getTypefaceWithWeight(
|
|
69
|
+
fontFamily: String,
|
|
70
|
+
fontWeight: String,
|
|
71
|
+
): Typeface {
|
|
72
|
+
val style =
|
|
73
|
+
when (fontWeight.lowercase()) {
|
|
74
|
+
"bold", "700", "800", "900" -> Typeface.BOLD
|
|
75
|
+
else -> Typeface.NORMAL
|
|
76
|
+
}
|
|
77
|
+
return getTypeface(fontFamily, style)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Cached monospace typeface preserving bold/italic */
|
|
81
|
+
fun getMonospaceTypeface(currentStyle: Int): Typeface =
|
|
82
|
+
typefaceCache.getOrPut("monospace|$currentStyle") {
|
|
83
|
+
Typeface.create(Typeface.MONOSPACE, currentStyle)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt
ADDED
|
@@ -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
|
+
}
|
|
@@ -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.StrongSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
7
|
+
|
|
8
|
+
class StrongRenderer(
|
|
9
|
+
private val config: RendererConfig,
|
|
10
|
+
) : NodeRenderer {
|
|
11
|
+
override fun render(
|
|
12
|
+
node: MarkdownASTNode,
|
|
13
|
+
builder: SpannableStringBuilder,
|
|
14
|
+
onLinkPress: ((String) -> Unit)?,
|
|
15
|
+
onLinkLongPress: ((String) -> Unit)?,
|
|
16
|
+
factory: RendererFactory,
|
|
17
|
+
) {
|
|
18
|
+
factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress, onLinkLongPress) }) { start, end, blockStyle ->
|
|
19
|
+
builder.setSpan(
|
|
20
|
+
StrongSpan(factory.styleCache, blockStyle),
|
|
21
|
+
start,
|
|
22
|
+
end,
|
|
23
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.text.SpannableStringBuilder
|
|
4
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
5
|
+
import com.swmansion.enriched.markdown.spans.TextSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
7
|
+
|
|
8
|
+
class TextRenderer : NodeRenderer {
|
|
9
|
+
override fun render(
|
|
10
|
+
node: MarkdownASTNode,
|
|
11
|
+
builder: SpannableStringBuilder,
|
|
12
|
+
onLinkPress: ((String) -> Unit)?,
|
|
13
|
+
onLinkLongPress: ((String) -> Unit)?,
|
|
14
|
+
factory: RendererFactory,
|
|
15
|
+
) {
|
|
16
|
+
val content = node.content
|
|
17
|
+
if (content.isEmpty()) return
|
|
18
|
+
|
|
19
|
+
val blockType = factory.blockStyleContext.currentBlockType
|
|
20
|
+
|
|
21
|
+
factory.renderWithSpan(builder, { builder.append(content) }) { start, end, blockStyle ->
|
|
22
|
+
builder.setSpan(
|
|
23
|
+
TextSpan(blockStyle, factory.context),
|
|
24
|
+
start,
|
|
25
|
+
end,
|
|
26
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.renderer
|
|
2
|
+
|
|
3
|
+
import android.text.SpannableStringBuilder
|
|
4
|
+
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
5
|
+
import com.swmansion.enriched.markdown.spans.ThematicBreakSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
7
|
+
|
|
8
|
+
class ThematicBreakRenderer(
|
|
9
|
+
private val config: RendererConfig,
|
|
10
|
+
) : NodeRenderer {
|
|
11
|
+
override fun render(
|
|
12
|
+
node: MarkdownASTNode,
|
|
13
|
+
builder: SpannableStringBuilder,
|
|
14
|
+
onLinkPress: ((String) -> Unit)?,
|
|
15
|
+
onLinkLongPress: ((String) -> Unit)?,
|
|
16
|
+
factory: RendererFactory,
|
|
17
|
+
) {
|
|
18
|
+
builder.ensureNewline()
|
|
19
|
+
|
|
20
|
+
val start = builder.length
|
|
21
|
+
|
|
22
|
+
builder.append(" \n")
|
|
23
|
+
val end = builder.length
|
|
24
|
+
|
|
25
|
+
val style = config.style.thematicBreakStyle
|
|
26
|
+
|
|
27
|
+
builder.setSpan(
|
|
28
|
+
ThematicBreakSpan(
|
|
29
|
+
style.color,
|
|
30
|
+
style.height,
|
|
31
|
+
style.marginTop,
|
|
32
|
+
style.marginBottom,
|
|
33
|
+
),
|
|
34
|
+
start,
|
|
35
|
+
end,
|
|
36
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private fun SpannableStringBuilder.ensureNewline() {
|
|
41
|
+
if (isNotEmpty() && this[length - 1] != '\n') {
|
|
42
|
+
append('\n')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.Typeface
|
|
8
|
+
import android.text.Layout
|
|
9
|
+
import android.text.Spanned
|
|
10
|
+
import android.text.TextPaint
|
|
11
|
+
import android.text.style.LeadingMarginSpan
|
|
12
|
+
import android.text.style.MetricAffectingSpan
|
|
13
|
+
import com.swmansion.enriched.markdown.renderer.BlockStyle
|
|
14
|
+
import com.swmansion.enriched.markdown.renderer.SpanStyleCache
|
|
15
|
+
import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
|
|
16
|
+
import com.swmansion.enriched.markdown.utils.applyColorPreserving
|
|
17
|
+
|
|
18
|
+
abstract class BaseListSpan(
|
|
19
|
+
val depth: Int,
|
|
20
|
+
protected val context: Context,
|
|
21
|
+
protected val styleCache: SpanStyleCache,
|
|
22
|
+
protected val blockStyle: BlockStyle,
|
|
23
|
+
protected val marginLeft: Float,
|
|
24
|
+
protected val gapWidth: Float,
|
|
25
|
+
) : MetricAffectingSpan(),
|
|
26
|
+
LeadingMarginSpan {
|
|
27
|
+
// Cache for shouldSkipDrawing to avoid repeated getSpans() calls during draw passes
|
|
28
|
+
private var cachedText: CharSequence? = null
|
|
29
|
+
private var cachedHasDeeperSpanByPosition = mutableMapOf<Int, Boolean>()
|
|
30
|
+
|
|
31
|
+
protected abstract fun getMarkerWidth(): Float
|
|
32
|
+
|
|
33
|
+
// --- MetricAffectingSpan Implementation ---
|
|
34
|
+
|
|
35
|
+
override fun updateMeasureState(tp: TextPaint) = applyTextStyle(tp)
|
|
36
|
+
|
|
37
|
+
override fun updateDrawState(tp: TextPaint) = applyTextStyle(tp)
|
|
38
|
+
|
|
39
|
+
// --- LeadingMarginSpan Implementation ---
|
|
40
|
+
|
|
41
|
+
override fun getLeadingMargin(first: Boolean): Int {
|
|
42
|
+
// Root items: just marker width + gap (flush to left edge)
|
|
43
|
+
// Nested items: add marginLeft for each nesting level
|
|
44
|
+
return if (depth == 0) {
|
|
45
|
+
val effectiveGap = gapWidth.coerceAtLeast(DEFAULT_MIN_GAP)
|
|
46
|
+
(getMarkerWidth() + effectiveGap).toInt()
|
|
47
|
+
} else {
|
|
48
|
+
marginLeft.toInt()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected fun getEffectiveGapWidth(): Float = gapWidth.coerceAtLeast(DEFAULT_MIN_GAP)
|
|
53
|
+
|
|
54
|
+
override fun drawLeadingMargin(
|
|
55
|
+
c: Canvas,
|
|
56
|
+
p: Paint,
|
|
57
|
+
x: Int,
|
|
58
|
+
dir: Int,
|
|
59
|
+
top: Int,
|
|
60
|
+
baseline: Int,
|
|
61
|
+
bottom: Int,
|
|
62
|
+
text: CharSequence?,
|
|
63
|
+
start: Int,
|
|
64
|
+
end: Int,
|
|
65
|
+
first: Boolean,
|
|
66
|
+
layout: Layout?,
|
|
67
|
+
) {
|
|
68
|
+
// Draw only on the first line of paragraphs that have content and are not nested deeper
|
|
69
|
+
if (!first || shouldSkipDrawing(text, start) || !hasContent(text, start, end)) return
|
|
70
|
+
|
|
71
|
+
val originalStyle = p.style
|
|
72
|
+
val originalColor = p.color
|
|
73
|
+
drawMarker(c, p, x, dir, top, baseline, bottom, layout, start)
|
|
74
|
+
p.style = originalStyle
|
|
75
|
+
p.color = originalColor
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected abstract fun drawMarker(
|
|
79
|
+
c: Canvas,
|
|
80
|
+
p: Paint,
|
|
81
|
+
x: Int,
|
|
82
|
+
dir: Int,
|
|
83
|
+
top: Int,
|
|
84
|
+
baseline: Int,
|
|
85
|
+
bottom: Int,
|
|
86
|
+
layout: Layout?,
|
|
87
|
+
start: Int,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@SuppressLint("WrongConstant") // Result of mask is always valid: 0, 1, 2, or 3
|
|
91
|
+
private fun applyTextStyle(tp: TextPaint) {
|
|
92
|
+
tp.textSize = blockStyle.fontSize
|
|
93
|
+
|
|
94
|
+
val preservedStyle = (tp.typeface?.style ?: 0) and BOLD_ITALIC_MASK
|
|
95
|
+
tp.applyBlockStyleFont(blockStyle, context)
|
|
96
|
+
if (preservedStyle != 0) {
|
|
97
|
+
tp.typeface?.let { base -> tp.typeface = Typeface.create(base, preservedStyle) }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
tp.applyColorPreserving(blockStyle.color, *styleCache.colorsToPreserve)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
companion object {
|
|
104
|
+
private const val BOLD_ITALIC_MASK = Typeface.BOLD or Typeface.ITALIC // 3
|
|
105
|
+
private const val DEFAULT_MIN_GAP = 4f
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Helper Methods ---
|
|
109
|
+
|
|
110
|
+
private fun hasContent(
|
|
111
|
+
text: CharSequence?,
|
|
112
|
+
start: Int,
|
|
113
|
+
end: Int,
|
|
114
|
+
): Boolean {
|
|
115
|
+
if (text == null || end <= start) return false
|
|
116
|
+
// Check if there is at least one non-whitespace character in the range
|
|
117
|
+
return (start until end).any { !text[it].isWhitespace() }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fun shouldSkipDrawing(
|
|
121
|
+
text: CharSequence?,
|
|
122
|
+
start: Int,
|
|
123
|
+
): Boolean {
|
|
124
|
+
if (text !is Spanned) return false
|
|
125
|
+
|
|
126
|
+
if (cachedText !== text) {
|
|
127
|
+
cachedText = text
|
|
128
|
+
cachedHasDeeperSpanByPosition.clear()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return cachedHasDeeperSpanByPosition.getOrPut(start) {
|
|
132
|
+
val spans = text.getSpans(start, start + 1, BaseListSpan::class.java)
|
|
133
|
+
spans.any { it.depth > depth }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|