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,321 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Resources
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.Paint
|
|
8
|
+
import android.graphics.Path
|
|
9
|
+
import android.graphics.drawable.Drawable
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import android.text.Spannable
|
|
12
|
+
import android.util.Log
|
|
13
|
+
import androidx.core.graphics.drawable.toDrawable
|
|
14
|
+
import androidx.core.graphics.withSave
|
|
15
|
+
import com.swmansion.enriched.markdown.EnrichedMarkdownText
|
|
16
|
+
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
17
|
+
import com.swmansion.enriched.markdown.utils.AsyncDrawable
|
|
18
|
+
import java.lang.ref.WeakReference
|
|
19
|
+
import android.text.style.ImageSpan as AndroidImageSpan
|
|
20
|
+
import android.text.style.LineHeightSpan as AndroidLineHeightSpan
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Custom ImageSpan for rendering markdown images.
|
|
24
|
+
* Handles both inline and block images with async loading support.
|
|
25
|
+
*/
|
|
26
|
+
class ImageSpan(
|
|
27
|
+
context: Context,
|
|
28
|
+
val imageUrl: String,
|
|
29
|
+
styleConfig: StyleConfig,
|
|
30
|
+
val isInline: Boolean = false,
|
|
31
|
+
val altText: String = "",
|
|
32
|
+
) : AndroidImageSpan(
|
|
33
|
+
createInitialDrawable(styleConfig, imageUrl, isInline),
|
|
34
|
+
imageUrl,
|
|
35
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ALIGN_CENTER else ALIGN_BASELINE,
|
|
36
|
+
),
|
|
37
|
+
AndroidLineHeightSpan {
|
|
38
|
+
private var loadedDrawable: Drawable? = null
|
|
39
|
+
private val imageStyle = styleConfig.imageStyle
|
|
40
|
+
private val height: Int = if (isInline) calculateInlineImageSize(styleConfig) else imageStyle.height.toInt()
|
|
41
|
+
private val borderRadiusPx: Int = (imageStyle.borderRadius * context.resources.displayMetrics.density).toInt()
|
|
42
|
+
|
|
43
|
+
private var cachedWidth: Int = MINIMUM_VALID_DIMENSION
|
|
44
|
+
private val initialDrawable: Drawable = super.getDrawable()
|
|
45
|
+
private var viewRef: WeakReference<EnrichedMarkdownText>? = null
|
|
46
|
+
|
|
47
|
+
init {
|
|
48
|
+
setupLoadingLogic()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private fun setupLoadingLogic() {
|
|
52
|
+
val d = initialDrawable
|
|
53
|
+
if (d is AsyncDrawable) {
|
|
54
|
+
// Set up the callback immediately. If already loaded, it triggers next frame.
|
|
55
|
+
d.onLoaded = { handleImageLoaded(d) }
|
|
56
|
+
if (d.isLoaded) handleImageLoaded(d)
|
|
57
|
+
} else if (d.intrinsicWidth > 0) {
|
|
58
|
+
// Local file or resource
|
|
59
|
+
wrapAndAssignDrawable(d)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun handleImageLoaded(asyncDrawable: AsyncDrawable) {
|
|
64
|
+
val rawDrawable = asyncDrawable.internalDrawable
|
|
65
|
+
wrapAndAssignDrawable(rawDrawable)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun wrapAndAssignDrawable(base: Drawable) {
|
|
69
|
+
val view = viewRef?.get()
|
|
70
|
+
val targetWidth =
|
|
71
|
+
if (isInline) {
|
|
72
|
+
height
|
|
73
|
+
} else {
|
|
74
|
+
val available = view?.let { getAvailableWidth(it) } ?: cachedWidth
|
|
75
|
+
available.coerceAtLeast(MINIMUM_VALID_DIMENSION)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
loadedDrawable =
|
|
79
|
+
ScaledImageDrawable(
|
|
80
|
+
imageDrawable = base,
|
|
81
|
+
targetWidth = targetWidth,
|
|
82
|
+
targetHeight = height,
|
|
83
|
+
borderRadius = borderRadiusPx,
|
|
84
|
+
isBlockImage = !isInline,
|
|
85
|
+
)
|
|
86
|
+
requestReflow()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun requestReflow() {
|
|
90
|
+
val view = viewRef?.get() ?: return
|
|
91
|
+
val text = view.text
|
|
92
|
+
if (text is Spannable) {
|
|
93
|
+
val start = text.getSpanStart(this)
|
|
94
|
+
val end = text.getSpanEnd(this)
|
|
95
|
+
if (start != -1 && end != -1) {
|
|
96
|
+
// Notifying the spannable that the span changed triggers a re-layout
|
|
97
|
+
text.setSpan(this, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
view.invalidate()
|
|
101
|
+
view.requestLayout()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fun registerTextView(view: EnrichedMarkdownText) {
|
|
106
|
+
viewRef = WeakReference(view)
|
|
107
|
+
if (!isInline) {
|
|
108
|
+
val availableWidth = getAvailableWidth(view)
|
|
109
|
+
if (availableWidth > MINIMUM_VALID_DIMENSION) {
|
|
110
|
+
updateWidthAndRecreate(availableWidth)
|
|
111
|
+
}
|
|
112
|
+
// Ensure we catch the width after the first layout pass
|
|
113
|
+
view.post {
|
|
114
|
+
val postWidth = getAvailableWidth(view)
|
|
115
|
+
if (postWidth != cachedWidth) updateWidthAndRecreate(postWidth)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fun updateWidthAndRecreate(newWidth: Int) {
|
|
121
|
+
if (newWidth <= MINIMUM_VALID_DIMENSION || cachedWidth == newWidth) return
|
|
122
|
+
cachedWidth = newWidth
|
|
123
|
+
|
|
124
|
+
// If we already have a loaded source, recreate the scaled wrapper with new width
|
|
125
|
+
val base = (initialDrawable as? AsyncDrawable)?.internalDrawable ?: initialDrawable
|
|
126
|
+
if (base.intrinsicWidth > 0) {
|
|
127
|
+
wrapAndAssignDrawable(base)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun getAvailableWidth(view: EnrichedMarkdownText): Int = view.layout?.width ?: view.width
|
|
132
|
+
|
|
133
|
+
override fun getDrawable(): Drawable {
|
|
134
|
+
val drawable = loadedDrawable ?: initialDrawable
|
|
135
|
+
if (drawable !is ScaledImageDrawable) {
|
|
136
|
+
val dWidth = if (isInline) height else cachedWidth.takeIf { it > 0 } ?: drawable.intrinsicWidth
|
|
137
|
+
val dHeight = if (isInline) height else height
|
|
138
|
+
drawable.setBounds(0, 0, dWidth.coerceAtLeast(0), dHeight.coerceAtLeast(0))
|
|
139
|
+
}
|
|
140
|
+
return drawable
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override fun getSize(
|
|
144
|
+
paint: Paint,
|
|
145
|
+
text: CharSequence?,
|
|
146
|
+
start: Int,
|
|
147
|
+
end: Int,
|
|
148
|
+
fm: Paint.FontMetricsInt?,
|
|
149
|
+
): Int = getDrawable().bounds.right
|
|
150
|
+
|
|
151
|
+
override fun chooseHeight(
|
|
152
|
+
text: CharSequence?,
|
|
153
|
+
start: Int,
|
|
154
|
+
end: Int,
|
|
155
|
+
spanstartv: Int,
|
|
156
|
+
lineHeight: Int,
|
|
157
|
+
fm: Paint.FontMetricsInt?,
|
|
158
|
+
) {
|
|
159
|
+
if (fm == null || isInline) return
|
|
160
|
+
val currentLineHeight = fm.descent - fm.ascent
|
|
161
|
+
if (height > currentLineHeight) {
|
|
162
|
+
val extraHeight = height - currentLineHeight
|
|
163
|
+
fm.descent += extraHeight
|
|
164
|
+
fm.bottom += extraHeight
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override fun draw(
|
|
169
|
+
canvas: Canvas,
|
|
170
|
+
text: CharSequence?,
|
|
171
|
+
start: Int,
|
|
172
|
+
end: Int,
|
|
173
|
+
x: Float,
|
|
174
|
+
top: Int,
|
|
175
|
+
y: Int,
|
|
176
|
+
bottom: Int,
|
|
177
|
+
paint: Paint,
|
|
178
|
+
) {
|
|
179
|
+
val drawable = getDrawable()
|
|
180
|
+
canvas.withSave {
|
|
181
|
+
if (isInline) {
|
|
182
|
+
val imageHeight = drawable.bounds.height()
|
|
183
|
+
translate(x, (y - imageHeight + (imageHeight * 0.1f)))
|
|
184
|
+
} else {
|
|
185
|
+
translate(x, top.toFloat())
|
|
186
|
+
}
|
|
187
|
+
drawable.draw(this)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Helper Classes ---
|
|
192
|
+
|
|
193
|
+
private class ScaledImageDrawable(
|
|
194
|
+
private val imageDrawable: Drawable,
|
|
195
|
+
private val targetWidth: Int,
|
|
196
|
+
private val targetHeight: Int,
|
|
197
|
+
private val borderRadius: Int,
|
|
198
|
+
isBlockImage: Boolean,
|
|
199
|
+
) : Drawable() {
|
|
200
|
+
private val clipPath: Path? =
|
|
201
|
+
if (borderRadius > 0) {
|
|
202
|
+
Path().apply {
|
|
203
|
+
addRoundRect(
|
|
204
|
+
0f,
|
|
205
|
+
0f,
|
|
206
|
+
targetWidth.toFloat(),
|
|
207
|
+
targetHeight.toFloat(),
|
|
208
|
+
borderRadius.toFloat(),
|
|
209
|
+
borderRadius.toFloat(),
|
|
210
|
+
Path.Direction.CW,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
init {
|
|
218
|
+
setBounds(0, 0, targetWidth, targetHeight)
|
|
219
|
+
val iW = imageDrawable.intrinsicWidth
|
|
220
|
+
val iH = imageDrawable.intrinsicHeight
|
|
221
|
+
|
|
222
|
+
val (sW, sH) =
|
|
223
|
+
if (iW > 0 && iH > 0) {
|
|
224
|
+
if (isBlockImage) {
|
|
225
|
+
val scale = targetWidth.toFloat() / iW
|
|
226
|
+
targetWidth to (iH * scale).toInt()
|
|
227
|
+
} else {
|
|
228
|
+
val scale = minOf(targetWidth.toFloat() / iW, targetHeight.toFloat() / iH)
|
|
229
|
+
(iW * scale).toInt() to (iH * scale).toInt()
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
targetWidth to targetHeight
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
val left = (targetWidth - sW) / 2
|
|
236
|
+
val top = (targetHeight - sH) / 2
|
|
237
|
+
imageDrawable.setBounds(left, top, left + sW, top + sH)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
override fun draw(canvas: Canvas) {
|
|
241
|
+
if (clipPath != null) {
|
|
242
|
+
canvas.withSave {
|
|
243
|
+
clipPath(clipPath)
|
|
244
|
+
imageDrawable.draw(canvas)
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
imageDrawable.draw(canvas)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
override fun setAlpha(alpha: Int) {
|
|
252
|
+
imageDrawable.alpha = alpha
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
override fun setColorFilter(cf: android.graphics.ColorFilter?) {
|
|
256
|
+
imageDrawable.colorFilter = cf
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@Suppress("DEPRECATION")
|
|
260
|
+
@Deprecated("Deprecated in Java")
|
|
261
|
+
override fun getOpacity(): Int = imageDrawable.opacity
|
|
262
|
+
|
|
263
|
+
override fun getIntrinsicWidth(): Int = targetWidth
|
|
264
|
+
|
|
265
|
+
override fun getIntrinsicHeight(): Int = targetHeight
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
companion object {
|
|
269
|
+
private const val MINIMUM_VALID_DIMENSION = 0
|
|
270
|
+
|
|
271
|
+
private fun calculateInlineImageSize(style: StyleConfig): Int = style.inlineImageStyle.size.toInt()
|
|
272
|
+
|
|
273
|
+
private fun createInitialDrawable(
|
|
274
|
+
style: StyleConfig,
|
|
275
|
+
url: String,
|
|
276
|
+
isInline: Boolean,
|
|
277
|
+
): Drawable {
|
|
278
|
+
val imgStyle = style.imageStyle
|
|
279
|
+
val size = if (isInline) calculateInlineImageSize(style) else imgStyle.height.toInt()
|
|
280
|
+
|
|
281
|
+
return prepareDrawable(url, size, size) ?: PlaceholderDrawable(size, size)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private fun prepareDrawable(
|
|
285
|
+
src: String,
|
|
286
|
+
tw: Int,
|
|
287
|
+
th: Int,
|
|
288
|
+
): Drawable? {
|
|
289
|
+
if (src.startsWith("http")) {
|
|
290
|
+
return AsyncDrawable(src).apply { setBounds(0, 0, tw, th) }
|
|
291
|
+
}
|
|
292
|
+
val path = src.removePrefix("file://")
|
|
293
|
+
return try {
|
|
294
|
+
BitmapFactory.decodeFile(path)?.toDrawable(Resources.getSystem())?.apply {
|
|
295
|
+
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
|
296
|
+
}
|
|
297
|
+
} catch (e: Exception) {
|
|
298
|
+
Log.w("ImageSpan", "Failed to load local image: $path", e)
|
|
299
|
+
null
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private class PlaceholderDrawable(
|
|
304
|
+
private val w: Int,
|
|
305
|
+
private val h: Int,
|
|
306
|
+
) : Drawable() {
|
|
307
|
+
override fun draw(canvas: Canvas) {}
|
|
308
|
+
|
|
309
|
+
override fun setAlpha(alpha: Int) {}
|
|
310
|
+
|
|
311
|
+
override fun setColorFilter(cf: android.graphics.ColorFilter?) {}
|
|
312
|
+
|
|
313
|
+
@Deprecated("Deprecated in Java")
|
|
314
|
+
override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSLUCENT
|
|
315
|
+
|
|
316
|
+
override fun getIntrinsicWidth(): Int = w
|
|
317
|
+
|
|
318
|
+
override fun getIntrinsicHeight(): Int = h
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Paint
|
|
4
|
+
import kotlin.math.ceil
|
|
5
|
+
import kotlin.math.floor
|
|
6
|
+
import android.text.style.LineHeightSpan as AndroidLineHeightSpan
|
|
7
|
+
|
|
8
|
+
class LineHeightSpan(
|
|
9
|
+
height: Float,
|
|
10
|
+
) : AndroidLineHeightSpan {
|
|
11
|
+
private val lineHeight: Int = ceil(height.toDouble()).toInt()
|
|
12
|
+
|
|
13
|
+
override fun chooseHeight(
|
|
14
|
+
text: CharSequence?,
|
|
15
|
+
start: Int,
|
|
16
|
+
end: Int,
|
|
17
|
+
spanstartv: Int,
|
|
18
|
+
v: Int,
|
|
19
|
+
fm: Paint.FontMetricsInt?,
|
|
20
|
+
) {
|
|
21
|
+
if (fm == null) return
|
|
22
|
+
|
|
23
|
+
val leading = lineHeight - ((-fm.ascent) + fm.descent)
|
|
24
|
+
fm.ascent -= ceil(leading / 2.0f).toInt()
|
|
25
|
+
fm.descent += floor(leading / 2.0f).toInt()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.text.TextPaint
|
|
5
|
+
import android.text.style.ClickableSpan
|
|
6
|
+
import android.view.View
|
|
7
|
+
import com.swmansion.enriched.markdown.EnrichedMarkdownText
|
|
8
|
+
import com.swmansion.enriched.markdown.renderer.BlockStyle
|
|
9
|
+
import com.swmansion.enriched.markdown.renderer.SpanStyleCache
|
|
10
|
+
import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
|
|
11
|
+
|
|
12
|
+
class LinkSpan(
|
|
13
|
+
val url: String,
|
|
14
|
+
private val onLinkPress: ((String) -> Unit)?,
|
|
15
|
+
private val onLinkLongPress: ((String) -> Unit)?,
|
|
16
|
+
private val styleCache: SpanStyleCache,
|
|
17
|
+
private val blockStyle: BlockStyle,
|
|
18
|
+
private val context: Context,
|
|
19
|
+
) : ClickableSpan() {
|
|
20
|
+
@Volatile
|
|
21
|
+
private var longPressTriggered = false
|
|
22
|
+
|
|
23
|
+
override fun onClick(widget: View) {
|
|
24
|
+
if (longPressTriggered) {
|
|
25
|
+
longPressTriggered = false
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onLinkPress?.invoke(url) ?: (widget as? EnrichedMarkdownText)?.emitOnLinkPress(url)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun onLongClick(widget: View): Boolean {
|
|
33
|
+
longPressTriggered = true
|
|
34
|
+
|
|
35
|
+
(widget as? EnrichedMarkdownText)?.emitOnLinkLongPress(url)
|
|
36
|
+
|
|
37
|
+
onLinkLongPress?.invoke(url)
|
|
38
|
+
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun updateDrawState(textPaint: TextPaint) {
|
|
43
|
+
super.updateDrawState(textPaint)
|
|
44
|
+
|
|
45
|
+
textPaint.textSize = blockStyle.fontSize
|
|
46
|
+
textPaint.applyBlockStyleFont(blockStyle, context)
|
|
47
|
+
|
|
48
|
+
textPaint.color = styleCache.linkColor
|
|
49
|
+
textPaint.isUnderlineText = styleCache.linkUnderline
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Paint
|
|
4
|
+
import android.text.style.LineHeightSpan
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adds bottom margin to a block element (paragraphs/headings) using LineHeightSpan.
|
|
8
|
+
*
|
|
9
|
+
* For spacer lines (single newline), sets the line height to exactly marginBottom.
|
|
10
|
+
* For regular lines, adds marginBottom only at paragraph boundaries to preserve lineHeight.
|
|
11
|
+
*
|
|
12
|
+
* @param marginBottom The margin in pixels to add below the block (0 = no margin)
|
|
13
|
+
*/
|
|
14
|
+
class MarginBottomSpan(
|
|
15
|
+
val marginBottom: Float,
|
|
16
|
+
) : LineHeightSpan {
|
|
17
|
+
override fun chooseHeight(
|
|
18
|
+
text: CharSequence,
|
|
19
|
+
start: Int,
|
|
20
|
+
end: Int,
|
|
21
|
+
spanstartv: Int,
|
|
22
|
+
lineHeight: Int,
|
|
23
|
+
fm: Paint.FontMetricsInt,
|
|
24
|
+
) {
|
|
25
|
+
// Only process lines that end with a newline
|
|
26
|
+
if (end <= start || text[end - 1] != '\n') {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
val marginPixels = marginBottom.toInt()
|
|
31
|
+
|
|
32
|
+
// Handle spacer lines (single newline character)
|
|
33
|
+
if (end - start == 1 && text[start] == '\n') {
|
|
34
|
+
if (hasContentAfter(text, end)) {
|
|
35
|
+
// Set line height to exactly marginBottom for spacer lines
|
|
36
|
+
fm.top = 0
|
|
37
|
+
fm.ascent = 0
|
|
38
|
+
fm.descent = marginPixels
|
|
39
|
+
fm.bottom = marginPixels
|
|
40
|
+
} else {
|
|
41
|
+
// No content after - collapse the spacer line to zero height
|
|
42
|
+
fm.top = 0
|
|
43
|
+
fm.ascent = 0
|
|
44
|
+
fm.descent = 0
|
|
45
|
+
fm.bottom = 0
|
|
46
|
+
}
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// For regular lines, add spacing only if there's content after
|
|
51
|
+
if (hasContentAfter(text, end)) {
|
|
52
|
+
fm.descent += marginPixels
|
|
53
|
+
fm.bottom += marginPixels
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Checks if there's non-newline content after the given position.
|
|
59
|
+
* Used to determine if spacing should be applied (between items) or skipped (after last item).
|
|
60
|
+
*/
|
|
61
|
+
private fun hasContentAfter(
|
|
62
|
+
text: CharSequence,
|
|
63
|
+
pos: Int,
|
|
64
|
+
): Boolean {
|
|
65
|
+
if (pos >= text.length) return false
|
|
66
|
+
|
|
67
|
+
// If the next character is a newline, check the character after that
|
|
68
|
+
if (text[pos] == '\n') {
|
|
69
|
+
val nextPos = pos + 1
|
|
70
|
+
if (nextPos >= text.length) return false
|
|
71
|
+
return text[nextPos] != '\n' // Non-newline = content exists
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return true // Non-newline content immediately after
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Paint
|
|
6
|
+
import android.graphics.Typeface
|
|
7
|
+
import android.text.Layout
|
|
8
|
+
import android.text.TextPaint
|
|
9
|
+
import com.facebook.react.common.ReactConstants
|
|
10
|
+
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
|
|
11
|
+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
|
|
12
|
+
import com.swmansion.enriched.markdown.renderer.BlockStyle
|
|
13
|
+
import com.swmansion.enriched.markdown.renderer.SpanStyleCache
|
|
14
|
+
import com.swmansion.enriched.markdown.styles.ListStyle
|
|
15
|
+
|
|
16
|
+
class OrderedListSpan(
|
|
17
|
+
private val listStyle: ListStyle,
|
|
18
|
+
depth: Int,
|
|
19
|
+
context: Context,
|
|
20
|
+
styleCache: SpanStyleCache,
|
|
21
|
+
) : BaseListSpan(
|
|
22
|
+
depth = depth,
|
|
23
|
+
context = context,
|
|
24
|
+
styleCache = styleCache,
|
|
25
|
+
blockStyle =
|
|
26
|
+
BlockStyle(
|
|
27
|
+
fontSize = listStyle.fontSize,
|
|
28
|
+
fontFamily = listStyle.fontFamily,
|
|
29
|
+
fontWeight = listStyle.fontWeight,
|
|
30
|
+
color = listStyle.color,
|
|
31
|
+
),
|
|
32
|
+
marginLeft = listStyle.marginLeft,
|
|
33
|
+
gapWidth = listStyle.gapWidth,
|
|
34
|
+
) {
|
|
35
|
+
companion object {
|
|
36
|
+
private val sharedMarkerPaint = TextPaint().apply { isAntiAlias = true }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private val markerTypeface: Typeface =
|
|
40
|
+
run {
|
|
41
|
+
val fontFamily = listStyle.fontFamily.takeIf { it.isNotEmpty() }
|
|
42
|
+
val fontWeight = parseFontWeight(listStyle.markerFontWeight)
|
|
43
|
+
applyStyles(null, ReactConstants.UNSET, fontWeight, fontFamily, context.assets)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun configureMarkerPaint(): TextPaint =
|
|
47
|
+
sharedMarkerPaint.apply {
|
|
48
|
+
textSize = listStyle.fontSize
|
|
49
|
+
color = listStyle.markerColor
|
|
50
|
+
typeface = markerTypeface
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun getMarkerWidth(): Float {
|
|
54
|
+
val paint = configureMarkerPaint()
|
|
55
|
+
return paint.measureText("99.")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var itemNumber: Int = 1
|
|
59
|
+
private set
|
|
60
|
+
|
|
61
|
+
override fun drawMarker(
|
|
62
|
+
c: Canvas,
|
|
63
|
+
p: Paint,
|
|
64
|
+
x: Int,
|
|
65
|
+
dir: Int,
|
|
66
|
+
top: Int,
|
|
67
|
+
baseline: Int,
|
|
68
|
+
bottom: Int,
|
|
69
|
+
layout: Layout?,
|
|
70
|
+
start: Int,
|
|
71
|
+
) {
|
|
72
|
+
val markerPaint = configureMarkerPaint()
|
|
73
|
+
val text = "$itemNumber."
|
|
74
|
+
val textWidth = markerPaint.measureText(text)
|
|
75
|
+
|
|
76
|
+
// Calculate marker position based on depth
|
|
77
|
+
// depth 0: markerWidth, depth 1: marginLeft + markerWidth, etc.
|
|
78
|
+
val markerRightEdge = (depth * marginLeft + getMarkerWidth()) * dir
|
|
79
|
+
val markerX = markerRightEdge - textWidth * dir
|
|
80
|
+
|
|
81
|
+
c.drawText(text, markerX, baseline.toFloat(), markerPaint)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun setItemNumber(number: Int) {
|
|
85
|
+
itemNumber = number
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.text.TextPaint
|
|
4
|
+
import android.text.style.CharacterStyle
|
|
5
|
+
|
|
6
|
+
class StrikethroughSpan(
|
|
7
|
+
private val strikethroughColor: Int,
|
|
8
|
+
) : CharacterStyle() {
|
|
9
|
+
override fun updateDrawState(tp: TextPaint) {
|
|
10
|
+
tp.isStrikeThruText = true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Typeface
|
|
4
|
+
import android.text.TextPaint
|
|
5
|
+
import android.text.style.MetricAffectingSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.renderer.BlockStyle
|
|
7
|
+
import com.swmansion.enriched.markdown.renderer.SpanStyleCache
|
|
8
|
+
import com.swmansion.enriched.markdown.utils.applyColorPreserving
|
|
9
|
+
|
|
10
|
+
class StrongSpan(
|
|
11
|
+
private val styleCache: SpanStyleCache,
|
|
12
|
+
private val blockStyle: BlockStyle,
|
|
13
|
+
) : MetricAffectingSpan() {
|
|
14
|
+
private val strongColor = styleCache.getStrongColorFor(blockStyle.color)
|
|
15
|
+
|
|
16
|
+
override fun updateDrawState(tp: TextPaint) {
|
|
17
|
+
applyStrongStyle(tp)
|
|
18
|
+
tp.applyColorPreserving(strongColor, *styleCache.colorsToPreserve)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override fun updateMeasureState(tp: TextPaint) {
|
|
22
|
+
applyStrongStyle(tp)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private fun applyStrongStyle(tp: TextPaint) {
|
|
26
|
+
// Preserve code fontSize if code is nested inside strong text
|
|
27
|
+
val codeFontSize = blockStyle.fontSize * 0.85f
|
|
28
|
+
if (kotlin.math.abs(tp.textSize - codeFontSize) > 0.1f) {
|
|
29
|
+
tp.textSize = blockStyle.fontSize
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
val currentTypeface = tp.typeface ?: Typeface.DEFAULT
|
|
33
|
+
val isItalic = (currentTypeface.style) and Typeface.ITALIC != 0
|
|
34
|
+
val style = if (isItalic) Typeface.BOLD_ITALIC else Typeface.BOLD
|
|
35
|
+
tp.typeface = Typeface.create(currentTypeface, style)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.text.TextPaint
|
|
5
|
+
import android.text.style.MetricAffectingSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.renderer.BlockStyle
|
|
7
|
+
import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
|
|
8
|
+
|
|
9
|
+
class TextSpan(
|
|
10
|
+
private val blockStyle: BlockStyle,
|
|
11
|
+
private val context: Context,
|
|
12
|
+
) : MetricAffectingSpan() {
|
|
13
|
+
override fun updateDrawState(tp: TextPaint) {
|
|
14
|
+
applyBlockStyle(tp)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override fun updateMeasureState(tp: TextPaint) {
|
|
18
|
+
applyBlockStyle(tp)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private fun applyBlockStyle(tp: TextPaint) {
|
|
22
|
+
tp.textSize = blockStyle.fontSize
|
|
23
|
+
tp.color = blockStyle.color
|
|
24
|
+
tp.applyBlockStyleFont(blockStyle, context)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Canvas
|
|
4
|
+
import android.graphics.Paint
|
|
5
|
+
import android.text.style.ReplacementSpan
|
|
6
|
+
|
|
7
|
+
class ThematicBreakSpan(
|
|
8
|
+
private val lineColor: Int,
|
|
9
|
+
private val lineHeight: Float,
|
|
10
|
+
private val marginTop: Float,
|
|
11
|
+
private val marginBottom: Float,
|
|
12
|
+
) : ReplacementSpan() {
|
|
13
|
+
override fun getSize(
|
|
14
|
+
paint: Paint,
|
|
15
|
+
text: CharSequence?,
|
|
16
|
+
start: Int,
|
|
17
|
+
end: Int,
|
|
18
|
+
fm: Paint.FontMetricsInt?,
|
|
19
|
+
): Int {
|
|
20
|
+
val totalHeight = (marginTop + lineHeight + marginBottom).toInt()
|
|
21
|
+
|
|
22
|
+
fm?.apply {
|
|
23
|
+
ascent = -totalHeight
|
|
24
|
+
top = -totalHeight
|
|
25
|
+
descent = 0
|
|
26
|
+
bottom = 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override fun draw(
|
|
33
|
+
canvas: Canvas,
|
|
34
|
+
text: CharSequence?,
|
|
35
|
+
start: Int,
|
|
36
|
+
end: Int,
|
|
37
|
+
x: Float,
|
|
38
|
+
top: Int,
|
|
39
|
+
y: Int,
|
|
40
|
+
bottom: Int,
|
|
41
|
+
paint: Paint,
|
|
42
|
+
) {
|
|
43
|
+
paint.withStyle(lineColor, lineHeight) {
|
|
44
|
+
val lineY = top + marginTop + (lineHeight / 2f)
|
|
45
|
+
|
|
46
|
+
canvas.drawLine(0f, lineY, canvas.width.toFloat(), lineY, paint)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private inline fun Paint.withStyle(
|
|
51
|
+
color: Int,
|
|
52
|
+
width: Float,
|
|
53
|
+
action: () -> Unit,
|
|
54
|
+
) {
|
|
55
|
+
val oldColor = this.color
|
|
56
|
+
val oldWidth = this.strokeWidth
|
|
57
|
+
val oldStyle = this.style
|
|
58
|
+
|
|
59
|
+
this.color = color
|
|
60
|
+
this.strokeWidth = width
|
|
61
|
+
this.style = Paint.Style.STROKE
|
|
62
|
+
|
|
63
|
+
action()
|
|
64
|
+
|
|
65
|
+
this.color = oldColor
|
|
66
|
+
this.strokeWidth = oldWidth
|
|
67
|
+
this.style = oldStyle
|
|
68
|
+
}
|
|
69
|
+
}
|