react-native-enriched-markdown 0.1.0 → 0.1.1
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 +479 -0
- package/ReactNativeEnrichedMarkdown.podspec +27 -0
- package/android/build.gradle +101 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +39 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +21 -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 +24 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +57 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1164 -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 +203 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +153 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +119 -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 +165 -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 +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +48 -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 +89 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +105 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +52 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +28 -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 +58 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +99 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +66 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +95 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +85 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +44 -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 +320 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +37 -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/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 +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +51 -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 +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +21 -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 +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +29 -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 +180 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +75 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -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 +809 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +365 -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 +181 -0
- package/android/src/main/jni/CMakeLists.txt +82 -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 +33 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +49 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +25 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +19 -0
- package/cpp/md4c/md4c.c +6492 -0
- package/cpp/md4c/md4c.h +402 -0
- package/cpp/parser/MD4CParser.cpp +314 -0
- package/cpp/parser/MD4CParser.hpp +23 -0
- package/cpp/parser/MarkdownASTNode.hpp +49 -0
- package/ios/EnrichedMarkdownText.h +18 -0
- package/ios/EnrichedMarkdownText.mm +1074 -0
- package/ios/attachments/ImageAttachment.h +23 -0
- package/ios/attachments/ImageAttachment.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 +24 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +57 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1164 -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 +33 -0
- package/ios/parser/MarkdownASTNode.m +32 -0
- package/ios/parser/MarkdownParser.h +8 -0
- package/ios/parser/MarkdownParser.mm +13 -0
- package/ios/parser/MarkdownParserBridge.mm +110 -0
- package/ios/renderer/AttributedRenderer.h +9 -0
- package/ios/renderer/AttributedRenderer.m +119 -0
- package/ios/renderer/BlockquoteRenderer.h +7 -0
- package/ios/renderer/BlockquoteRenderer.m +159 -0
- package/ios/renderer/CodeBlockRenderer.h +10 -0
- package/ios/renderer/CodeBlockRenderer.m +89 -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 +98 -0
- package/ios/renderer/ImageRenderer.h +12 -0
- package/ios/renderer/ImageRenderer.m +62 -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 +91 -0
- package/ios/renderer/ListRenderer.h +13 -0
- package/ios/renderer/ListRenderer.m +67 -0
- package/ios/renderer/NodeRenderer.h +8 -0
- package/ios/renderer/ParagraphRenderer.h +7 -0
- package/ios/renderer/ParagraphRenderer.m +69 -0
- package/ios/renderer/RenderContext.h +88 -0
- package/ios/renderer/RenderContext.m +248 -0
- package/ios/renderer/RendererFactory.h +12 -0
- package/ios/renderer/RendererFactory.m +110 -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/styles/StyleConfig.h +228 -0
- package/ios/styles/StyleConfig.mm +1467 -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 +87 -0
- package/ios/utils/EditMenuUtils.h +22 -0
- package/ios/utils/EditMenuUtils.m +118 -0
- package/ios/utils/FontUtils.h +20 -0
- package/ios/utils/FontUtils.m +13 -0
- package/ios/utils/HTMLGenerator.h +20 -0
- package/ios/utils/HTMLGenerator.m +779 -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/MarkdownExtractor.h +17 -0
- package/ios/utils/MarkdownExtractor.m +295 -0
- package/ios/utils/ParagraphStyleUtils.h +13 -0
- package/ios/utils/ParagraphStyleUtils.m +56 -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 +34 -0
- package/lib/module/EnrichedMarkdownText.js.map +1 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +130 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalizeMarkdownStyle.js +340 -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 +101 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +111 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -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 +152 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +130 -0
- package/src/index.tsx +7 -0
- package/src/normalizeMarkdownStyle.ts +377 -0
|
@@ -0,0 +1,196 @@
|
|
|
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.Path
|
|
7
|
+
import android.graphics.RectF
|
|
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.LineBackgroundSpan
|
|
13
|
+
import android.text.style.MetricAffectingSpan
|
|
14
|
+
import androidx.core.graphics.withSave
|
|
15
|
+
import com.swmansion.enriched.markdown.renderer.BlockStyle
|
|
16
|
+
import com.swmansion.enriched.markdown.renderer.SpanStyleCache
|
|
17
|
+
import com.swmansion.enriched.markdown.styles.CodeBlockStyle
|
|
18
|
+
import com.swmansion.enriched.markdown.utils.applyBlockStyleFont
|
|
19
|
+
import com.swmansion.enriched.markdown.utils.applyColorPreserving
|
|
20
|
+
|
|
21
|
+
class CodeBlockSpan(
|
|
22
|
+
private val codeBlockStyle: CodeBlockStyle,
|
|
23
|
+
private val context: Context,
|
|
24
|
+
private val styleCache: SpanStyleCache,
|
|
25
|
+
) : MetricAffectingSpan(),
|
|
26
|
+
LineBackgroundSpan,
|
|
27
|
+
LeadingMarginSpan {
|
|
28
|
+
private val blockStyle =
|
|
29
|
+
BlockStyle(
|
|
30
|
+
fontSize = codeBlockStyle.fontSize,
|
|
31
|
+
fontFamily = codeBlockStyle.fontFamily,
|
|
32
|
+
fontWeight = codeBlockStyle.fontWeight,
|
|
33
|
+
color = codeBlockStyle.color,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
private val path = Path()
|
|
37
|
+
private val rect = RectF()
|
|
38
|
+
private val arcRect = RectF()
|
|
39
|
+
private val radiiArray = FloatArray(8)
|
|
40
|
+
|
|
41
|
+
companion object {
|
|
42
|
+
private val sharedBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
|
|
43
|
+
private val sharedBorderPaint =
|
|
44
|
+
Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
45
|
+
style = Paint.Style.STROKE
|
|
46
|
+
strokeCap = Paint.Cap.BUTT
|
|
47
|
+
strokeJoin = Paint.Join.ROUND
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private fun configureBackgroundPaint(): Paint =
|
|
52
|
+
sharedBackgroundPaint.apply {
|
|
53
|
+
color = codeBlockStyle.backgroundColor
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun configureBorderPaint(): Paint =
|
|
57
|
+
sharedBorderPaint.apply {
|
|
58
|
+
strokeWidth = codeBlockStyle.borderWidth
|
|
59
|
+
color = codeBlockStyle.borderColor
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun getLeadingMargin(first: Boolean): Int = codeBlockStyle.padding.toInt()
|
|
63
|
+
|
|
64
|
+
override fun drawLeadingMargin(
|
|
65
|
+
c: Canvas?,
|
|
66
|
+
p: Paint?,
|
|
67
|
+
x: Int,
|
|
68
|
+
dir: Int,
|
|
69
|
+
top: Int,
|
|
70
|
+
baseline: Int,
|
|
71
|
+
bottom: Int,
|
|
72
|
+
text: CharSequence?,
|
|
73
|
+
start: Int,
|
|
74
|
+
end: Int,
|
|
75
|
+
first: Boolean,
|
|
76
|
+
layout: Layout?,
|
|
77
|
+
) { /* Leading margin is handled by getLeadingMargin */ }
|
|
78
|
+
|
|
79
|
+
override fun updateMeasureState(tp: TextPaint) = applyTextStyle(tp)
|
|
80
|
+
|
|
81
|
+
override fun updateDrawState(tp: TextPaint) = applyTextStyle(tp)
|
|
82
|
+
|
|
83
|
+
override fun drawBackground(
|
|
84
|
+
canvas: Canvas,
|
|
85
|
+
p: Paint,
|
|
86
|
+
left: Int,
|
|
87
|
+
right: Int,
|
|
88
|
+
top: Int,
|
|
89
|
+
baseline: Int,
|
|
90
|
+
bottom: Int,
|
|
91
|
+
text: CharSequence,
|
|
92
|
+
start: Int,
|
|
93
|
+
end: Int,
|
|
94
|
+
lineNum: Int,
|
|
95
|
+
) {
|
|
96
|
+
if (text !is Spanned) return
|
|
97
|
+
|
|
98
|
+
val spanStart = text.getSpanStart(this)
|
|
99
|
+
val spanEnd = text.getSpanEnd(this)
|
|
100
|
+
if (spanStart !in 0 until spanEnd) return
|
|
101
|
+
|
|
102
|
+
val isFirstLine = start == spanStart
|
|
103
|
+
val isLastLine = end == spanEnd || (spanEnd <= end && (spanEnd == text.length || text[spanEnd - 1] == '\n'))
|
|
104
|
+
|
|
105
|
+
val inset = codeBlockStyle.borderWidth / 2f
|
|
106
|
+
|
|
107
|
+
rect.set(
|
|
108
|
+
left.toFloat() + inset,
|
|
109
|
+
top.toFloat() + (if (isFirstLine) inset else 0f),
|
|
110
|
+
right.toFloat() - inset,
|
|
111
|
+
bottom.toFloat() - (if (isLastLine) inset else 0f),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
val radius = codeBlockStyle.borderRadius
|
|
115
|
+
val adjRadius = if (radius > inset) radius - inset else radius
|
|
116
|
+
|
|
117
|
+
// Reset and fill radii array based on boundary state
|
|
118
|
+
radiiArray.fill(0f)
|
|
119
|
+
if (isFirstLine) {
|
|
120
|
+
radiiArray[0] = adjRadius
|
|
121
|
+
radiiArray[1] = adjRadius // Top-Left
|
|
122
|
+
radiiArray[2] = adjRadius
|
|
123
|
+
radiiArray[3] = adjRadius // Top-Right
|
|
124
|
+
}
|
|
125
|
+
if (isLastLine) {
|
|
126
|
+
radiiArray[4] = adjRadius
|
|
127
|
+
radiiArray[5] = adjRadius // Bottom-Right
|
|
128
|
+
radiiArray[6] = adjRadius
|
|
129
|
+
radiiArray[7] = adjRadius // Bottom-Left
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
path.reset()
|
|
133
|
+
path.addRoundRect(rect, radiiArray, Path.Direction.CW)
|
|
134
|
+
|
|
135
|
+
val backgroundPaint = configureBackgroundPaint()
|
|
136
|
+
val borderPaint = configureBorderPaint()
|
|
137
|
+
|
|
138
|
+
canvas.withSave {
|
|
139
|
+
drawPath(path, backgroundPaint)
|
|
140
|
+
|
|
141
|
+
if (codeBlockStyle.borderWidth > 0) {
|
|
142
|
+
val bLeft = rect.left
|
|
143
|
+
val bRight = rect.right
|
|
144
|
+
val bTop = rect.top
|
|
145
|
+
val bBottom = rect.bottom
|
|
146
|
+
|
|
147
|
+
when {
|
|
148
|
+
// Case: Single-line code block
|
|
149
|
+
isFirstLine && isLastLine -> {
|
|
150
|
+
drawPath(path, borderPaint)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Case: Top of a multi-line block
|
|
154
|
+
isFirstLine -> {
|
|
155
|
+
drawLine(bLeft + adjRadius, bTop, bRight - adjRadius, bTop, borderPaint)
|
|
156
|
+
drawLine(bLeft, bTop + adjRadius, bLeft, bBottom, borderPaint)
|
|
157
|
+
drawLine(bRight, bTop + adjRadius, bRight, bBottom, borderPaint)
|
|
158
|
+
|
|
159
|
+
arcRect.set(bLeft, bTop, bLeft + 2 * adjRadius, bTop + 2 * adjRadius)
|
|
160
|
+
drawArc(arcRect, 180f, 90f, false, borderPaint)
|
|
161
|
+
|
|
162
|
+
arcRect.set(bRight - 2 * adjRadius, bTop, bRight, bTop + 2 * adjRadius)
|
|
163
|
+
drawArc(arcRect, 270f, 90f, false, borderPaint)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Case: Bottom of a multi-line block
|
|
167
|
+
isLastLine -> {
|
|
168
|
+
drawLine(bLeft + adjRadius, bBottom, bRight - adjRadius, bBottom, borderPaint)
|
|
169
|
+
drawLine(bLeft, bTop, bLeft, bBottom - adjRadius, borderPaint)
|
|
170
|
+
drawLine(bRight, bTop, bRight, bBottom - adjRadius, borderPaint)
|
|
171
|
+
|
|
172
|
+
arcRect.set(bLeft, bBottom - 2 * adjRadius, bLeft + 2 * adjRadius, bBottom)
|
|
173
|
+
drawArc(arcRect, 90f, 90f, false, borderPaint)
|
|
174
|
+
|
|
175
|
+
arcRect.set(bRight - 2 * adjRadius, bBottom - 2 * adjRadius, bRight, bBottom)
|
|
176
|
+
drawArc(arcRect, 0f, 90f, false, borderPaint)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Case: Middle lines only need vertical sides
|
|
180
|
+
else -> {
|
|
181
|
+
drawLine(bLeft, bTop, bLeft, bBottom, borderPaint)
|
|
182
|
+
drawLine(bRight, bTop, bRight, bBottom, borderPaint)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun applyTextStyle(tp: TextPaint) {
|
|
190
|
+
tp.textSize = blockStyle.fontSize
|
|
191
|
+
|
|
192
|
+
tp.applyBlockStyleFont(blockStyle, context)
|
|
193
|
+
|
|
194
|
+
tp.applyColorPreserving(blockStyle.color, *styleCache.colorsToPreserve)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
|
|
9
|
+
class CodeSpan(
|
|
10
|
+
private val styleCache: SpanStyleCache,
|
|
11
|
+
private val blockStyle: BlockStyle,
|
|
12
|
+
) : MetricAffectingSpan() {
|
|
13
|
+
override fun updateDrawState(tp: TextPaint) {
|
|
14
|
+
applyMonospacedFont(tp)
|
|
15
|
+
tp.color = styleCache.codeColor
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override fun updateMeasureState(tp: TextPaint) {
|
|
19
|
+
applyMonospacedFont(tp)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private fun applyMonospacedFont(paint: TextPaint) {
|
|
23
|
+
paint.textSize = blockStyle.fontSize * 0.85f
|
|
24
|
+
val preservedStyle = (paint.typeface?.style ?: 0) and (Typeface.BOLD or Typeface.ITALIC)
|
|
25
|
+
paint.typeface = SpanStyleCache.getMonospaceTypeface(preservedStyle)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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 EmphasisSpan(
|
|
11
|
+
private val styleCache: SpanStyleCache,
|
|
12
|
+
private val blockStyle: BlockStyle,
|
|
13
|
+
) : MetricAffectingSpan() {
|
|
14
|
+
override fun updateDrawState(tp: TextPaint) {
|
|
15
|
+
applyEmphasisStyle(tp)
|
|
16
|
+
applyEmphasisColor(tp)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun updateMeasureState(tp: TextPaint) {
|
|
20
|
+
applyEmphasisStyle(tp)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private fun applyEmphasisStyle(tp: TextPaint) {
|
|
24
|
+
val currentTypeface = tp.typeface ?: Typeface.DEFAULT
|
|
25
|
+
val isBold = (currentTypeface.style) and Typeface.BOLD != 0
|
|
26
|
+
val style = if (isBold) Typeface.BOLD_ITALIC else Typeface.ITALIC
|
|
27
|
+
tp.typeface = Typeface.create(currentTypeface, style)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private fun applyEmphasisColor(tp: TextPaint) {
|
|
31
|
+
val colorToUse = styleCache.getEmphasisColorFor(blockStyle.color, tp.color)
|
|
32
|
+
tp.applyColorPreserving(colorToUse, *styleCache.colorsToPreserve)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import android.text.TextPaint
|
|
6
|
+
import android.text.style.MetricAffectingSpan
|
|
7
|
+
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
8
|
+
|
|
9
|
+
class HeadingSpan(
|
|
10
|
+
val level: Int,
|
|
11
|
+
styleConfig: StyleConfig,
|
|
12
|
+
) : MetricAffectingSpan() {
|
|
13
|
+
private val fontSize: Float = styleConfig.headingStyles[level]!!.fontSize
|
|
14
|
+
private val color: Int = styleConfig.headingStyles[level]!!.color
|
|
15
|
+
private val cachedTypeface: Typeface? = styleConfig.headingTypefaces[level]
|
|
16
|
+
|
|
17
|
+
override fun updateDrawState(tp: TextPaint) {
|
|
18
|
+
applyHeadingStyle(tp)
|
|
19
|
+
tp.color = color
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override fun updateMeasureState(tp: TextPaint) {
|
|
23
|
+
applyHeadingStyle(tp)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@SuppressLint("WrongConstant") // Result of mask is always valid: 0, 1, 2, or 3
|
|
27
|
+
private fun applyHeadingStyle(tp: TextPaint) {
|
|
28
|
+
tp.textSize = fontSize
|
|
29
|
+
cachedTypeface?.let { base ->
|
|
30
|
+
val preserved = (tp.typeface?.style ?: 0) and BOLD_ITALIC_MASK
|
|
31
|
+
tp.typeface = if (preserved != 0) Typeface.create(base, preserved) else base
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
companion object {
|
|
36
|
+
private const val BOLD_ITALIC_MASK = Typeface.BOLD or Typeface.ITALIC
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
) : AndroidImageSpan(
|
|
32
|
+
createInitialDrawable(styleConfig, imageUrl, isInline),
|
|
33
|
+
imageUrl,
|
|
34
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ALIGN_CENTER else ALIGN_BASELINE,
|
|
35
|
+
),
|
|
36
|
+
AndroidLineHeightSpan {
|
|
37
|
+
private var loadedDrawable: Drawable? = null
|
|
38
|
+
private val imageStyle = styleConfig.imageStyle
|
|
39
|
+
private val height: Int = if (isInline) calculateInlineImageSize(styleConfig) else imageStyle.height.toInt()
|
|
40
|
+
private val borderRadiusPx: Int = (imageStyle.borderRadius * context.resources.displayMetrics.density).toInt()
|
|
41
|
+
|
|
42
|
+
private var cachedWidth: Int = MINIMUM_VALID_DIMENSION
|
|
43
|
+
private val initialDrawable: Drawable = super.getDrawable()
|
|
44
|
+
private var viewRef: WeakReference<EnrichedMarkdownText>? = null
|
|
45
|
+
|
|
46
|
+
init {
|
|
47
|
+
setupLoadingLogic()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private fun setupLoadingLogic() {
|
|
51
|
+
val d = initialDrawable
|
|
52
|
+
if (d is AsyncDrawable) {
|
|
53
|
+
// Set up the callback immediately. If already loaded, it triggers next frame.
|
|
54
|
+
d.onLoaded = { handleImageLoaded(d) }
|
|
55
|
+
if (d.isLoaded) handleImageLoaded(d)
|
|
56
|
+
} else if (d.intrinsicWidth > 0) {
|
|
57
|
+
// Local file or resource
|
|
58
|
+
wrapAndAssignDrawable(d)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private fun handleImageLoaded(asyncDrawable: AsyncDrawable) {
|
|
63
|
+
val rawDrawable = asyncDrawable.internalDrawable
|
|
64
|
+
wrapAndAssignDrawable(rawDrawable)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private fun wrapAndAssignDrawable(base: Drawable) {
|
|
68
|
+
val view = viewRef?.get()
|
|
69
|
+
val targetWidth =
|
|
70
|
+
if (isInline) {
|
|
71
|
+
height
|
|
72
|
+
} else {
|
|
73
|
+
val available = view?.let { getAvailableWidth(it) } ?: cachedWidth
|
|
74
|
+
available.coerceAtLeast(MINIMUM_VALID_DIMENSION)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
loadedDrawable =
|
|
78
|
+
ScaledImageDrawable(
|
|
79
|
+
imageDrawable = base,
|
|
80
|
+
targetWidth = targetWidth,
|
|
81
|
+
targetHeight = height,
|
|
82
|
+
borderRadius = borderRadiusPx,
|
|
83
|
+
isBlockImage = !isInline,
|
|
84
|
+
)
|
|
85
|
+
requestReflow()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun requestReflow() {
|
|
89
|
+
val view = viewRef?.get() ?: return
|
|
90
|
+
val text = view.text
|
|
91
|
+
if (text is Spannable) {
|
|
92
|
+
val start = text.getSpanStart(this)
|
|
93
|
+
val end = text.getSpanEnd(this)
|
|
94
|
+
if (start != -1 && end != -1) {
|
|
95
|
+
// Notifying the spannable that the span changed triggers a re-layout
|
|
96
|
+
text.setSpan(this, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
view.invalidate()
|
|
100
|
+
view.requestLayout()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fun registerTextView(view: EnrichedMarkdownText) {
|
|
105
|
+
viewRef = WeakReference(view)
|
|
106
|
+
if (!isInline) {
|
|
107
|
+
val availableWidth = getAvailableWidth(view)
|
|
108
|
+
if (availableWidth > MINIMUM_VALID_DIMENSION) {
|
|
109
|
+
updateWidthAndRecreate(availableWidth)
|
|
110
|
+
}
|
|
111
|
+
// Ensure we catch the width after the first layout pass
|
|
112
|
+
view.post {
|
|
113
|
+
val postWidth = getAvailableWidth(view)
|
|
114
|
+
if (postWidth != cachedWidth) updateWidthAndRecreate(postWidth)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun updateWidthAndRecreate(newWidth: Int) {
|
|
120
|
+
if (newWidth <= MINIMUM_VALID_DIMENSION || cachedWidth == newWidth) return
|
|
121
|
+
cachedWidth = newWidth
|
|
122
|
+
|
|
123
|
+
// If we already have a loaded source, recreate the scaled wrapper with new width
|
|
124
|
+
val base = (initialDrawable as? AsyncDrawable)?.internalDrawable ?: initialDrawable
|
|
125
|
+
if (base.intrinsicWidth > 0) {
|
|
126
|
+
wrapAndAssignDrawable(base)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private fun getAvailableWidth(view: EnrichedMarkdownText): Int = view.layout?.width ?: view.width
|
|
131
|
+
|
|
132
|
+
override fun getDrawable(): Drawable {
|
|
133
|
+
val drawable = loadedDrawable ?: initialDrawable
|
|
134
|
+
if (drawable !is ScaledImageDrawable) {
|
|
135
|
+
val dWidth = if (isInline) height else cachedWidth.takeIf { it > 0 } ?: drawable.intrinsicWidth
|
|
136
|
+
val dHeight = if (isInline) height else height
|
|
137
|
+
drawable.setBounds(0, 0, dWidth.coerceAtLeast(0), dHeight.coerceAtLeast(0))
|
|
138
|
+
}
|
|
139
|
+
return drawable
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
override fun getSize(
|
|
143
|
+
paint: Paint,
|
|
144
|
+
text: CharSequence?,
|
|
145
|
+
start: Int,
|
|
146
|
+
end: Int,
|
|
147
|
+
fm: Paint.FontMetricsInt?,
|
|
148
|
+
): Int = getDrawable().bounds.right
|
|
149
|
+
|
|
150
|
+
override fun chooseHeight(
|
|
151
|
+
text: CharSequence?,
|
|
152
|
+
start: Int,
|
|
153
|
+
end: Int,
|
|
154
|
+
spanstartv: Int,
|
|
155
|
+
lineHeight: Int,
|
|
156
|
+
fm: Paint.FontMetricsInt?,
|
|
157
|
+
) {
|
|
158
|
+
if (fm == null || isInline) return
|
|
159
|
+
val currentLineHeight = fm.descent - fm.ascent
|
|
160
|
+
if (height > currentLineHeight) {
|
|
161
|
+
val extraHeight = height - currentLineHeight
|
|
162
|
+
fm.descent += extraHeight
|
|
163
|
+
fm.bottom += extraHeight
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override fun draw(
|
|
168
|
+
canvas: Canvas,
|
|
169
|
+
text: CharSequence?,
|
|
170
|
+
start: Int,
|
|
171
|
+
end: Int,
|
|
172
|
+
x: Float,
|
|
173
|
+
top: Int,
|
|
174
|
+
y: Int,
|
|
175
|
+
bottom: Int,
|
|
176
|
+
paint: Paint,
|
|
177
|
+
) {
|
|
178
|
+
val drawable = getDrawable()
|
|
179
|
+
canvas.withSave {
|
|
180
|
+
if (isInline) {
|
|
181
|
+
val imageHeight = drawable.bounds.height()
|
|
182
|
+
translate(x, (y - imageHeight + (imageHeight * 0.1f)))
|
|
183
|
+
} else {
|
|
184
|
+
translate(x, top.toFloat())
|
|
185
|
+
}
|
|
186
|
+
drawable.draw(this)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- Helper Classes ---
|
|
191
|
+
|
|
192
|
+
private class ScaledImageDrawable(
|
|
193
|
+
private val imageDrawable: Drawable,
|
|
194
|
+
private val targetWidth: Int,
|
|
195
|
+
private val targetHeight: Int,
|
|
196
|
+
private val borderRadius: Int,
|
|
197
|
+
isBlockImage: Boolean,
|
|
198
|
+
) : Drawable() {
|
|
199
|
+
private val clipPath: Path? =
|
|
200
|
+
if (borderRadius > 0) {
|
|
201
|
+
Path().apply {
|
|
202
|
+
addRoundRect(
|
|
203
|
+
0f,
|
|
204
|
+
0f,
|
|
205
|
+
targetWidth.toFloat(),
|
|
206
|
+
targetHeight.toFloat(),
|
|
207
|
+
borderRadius.toFloat(),
|
|
208
|
+
borderRadius.toFloat(),
|
|
209
|
+
Path.Direction.CW,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
init {
|
|
217
|
+
setBounds(0, 0, targetWidth, targetHeight)
|
|
218
|
+
val iW = imageDrawable.intrinsicWidth
|
|
219
|
+
val iH = imageDrawable.intrinsicHeight
|
|
220
|
+
|
|
221
|
+
val (sW, sH) =
|
|
222
|
+
if (iW > 0 && iH > 0) {
|
|
223
|
+
if (isBlockImage) {
|
|
224
|
+
val scale = targetWidth.toFloat() / iW
|
|
225
|
+
targetWidth to (iH * scale).toInt()
|
|
226
|
+
} else {
|
|
227
|
+
val scale = minOf(targetWidth.toFloat() / iW, targetHeight.toFloat() / iH)
|
|
228
|
+
(iW * scale).toInt() to (iH * scale).toInt()
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
targetWidth to targetHeight
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
val left = (targetWidth - sW) / 2
|
|
235
|
+
val top = (targetHeight - sH) / 2
|
|
236
|
+
imageDrawable.setBounds(left, top, left + sW, top + sH)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
override fun draw(canvas: Canvas) {
|
|
240
|
+
if (clipPath != null) {
|
|
241
|
+
canvas.withSave {
|
|
242
|
+
clipPath(clipPath)
|
|
243
|
+
imageDrawable.draw(canvas)
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
imageDrawable.draw(canvas)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
override fun setAlpha(alpha: Int) {
|
|
251
|
+
imageDrawable.alpha = alpha
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
override fun setColorFilter(cf: android.graphics.ColorFilter?) {
|
|
255
|
+
imageDrawable.colorFilter = cf
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Suppress("DEPRECATION")
|
|
259
|
+
@Deprecated("Deprecated in Java")
|
|
260
|
+
override fun getOpacity(): Int = imageDrawable.opacity
|
|
261
|
+
|
|
262
|
+
override fun getIntrinsicWidth(): Int = targetWidth
|
|
263
|
+
|
|
264
|
+
override fun getIntrinsicHeight(): Int = targetHeight
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
companion object {
|
|
268
|
+
private const val MINIMUM_VALID_DIMENSION = 0
|
|
269
|
+
|
|
270
|
+
private fun calculateInlineImageSize(style: StyleConfig): Int = style.inlineImageStyle.size.toInt()
|
|
271
|
+
|
|
272
|
+
private fun createInitialDrawable(
|
|
273
|
+
style: StyleConfig,
|
|
274
|
+
url: String,
|
|
275
|
+
isInline: Boolean,
|
|
276
|
+
): Drawable {
|
|
277
|
+
val imgStyle = style.imageStyle
|
|
278
|
+
val size = if (isInline) calculateInlineImageSize(style) else imgStyle.height.toInt()
|
|
279
|
+
|
|
280
|
+
return prepareDrawable(url, size, size) ?: PlaceholderDrawable(size, size)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private fun prepareDrawable(
|
|
284
|
+
src: String,
|
|
285
|
+
tw: Int,
|
|
286
|
+
th: Int,
|
|
287
|
+
): Drawable? {
|
|
288
|
+
if (src.startsWith("http")) {
|
|
289
|
+
return AsyncDrawable(src).apply { setBounds(0, 0, tw, th) }
|
|
290
|
+
}
|
|
291
|
+
val path = src.removePrefix("file://")
|
|
292
|
+
return try {
|
|
293
|
+
BitmapFactory.decodeFile(path)?.toDrawable(Resources.getSystem())?.apply {
|
|
294
|
+
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
|
295
|
+
}
|
|
296
|
+
} catch (e: Exception) {
|
|
297
|
+
Log.w("ImageSpan", "Failed to load local image: $path", e)
|
|
298
|
+
null
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private class PlaceholderDrawable(
|
|
303
|
+
private val w: Int,
|
|
304
|
+
private val h: Int,
|
|
305
|
+
) : Drawable() {
|
|
306
|
+
override fun draw(canvas: Canvas) {}
|
|
307
|
+
|
|
308
|
+
override fun setAlpha(alpha: Int) {}
|
|
309
|
+
|
|
310
|
+
override fun setColorFilter(cf: android.graphics.ColorFilter?) {}
|
|
311
|
+
|
|
312
|
+
@Deprecated("Deprecated in Java")
|
|
313
|
+
override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSLUCENT
|
|
314
|
+
|
|
315
|
+
override fun getIntrinsicWidth(): Int = w
|
|
316
|
+
|
|
317
|
+
override fun getIntrinsicHeight(): Int = h
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Paint
|
|
4
|
+
import kotlin.math.ceil
|
|
5
|
+
import kotlin.math.roundToInt
|
|
6
|
+
import android.text.style.LineHeightSpan as AndroidLineHeightSpan
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom LineHeightSpan for Android API levels below 29.
|
|
10
|
+
* Matches LineHeightSpan.Standard behavior for consistent rendering across all API levels.
|
|
11
|
+
*/
|
|
12
|
+
class LineHeightSpan(
|
|
13
|
+
private val lineHeight: Float,
|
|
14
|
+
) : AndroidLineHeightSpan {
|
|
15
|
+
override fun chooseHeight(
|
|
16
|
+
text: CharSequence?,
|
|
17
|
+
start: Int,
|
|
18
|
+
end: Int,
|
|
19
|
+
spanstartv: Int,
|
|
20
|
+
lineHeight: Int,
|
|
21
|
+
fm: Paint.FontMetricsInt?,
|
|
22
|
+
) {
|
|
23
|
+
if (fm == null) return
|
|
24
|
+
|
|
25
|
+
val targetHeight = ceil(this.lineHeight.toDouble()).toInt()
|
|
26
|
+
val originHeight = fm.descent - fm.ascent
|
|
27
|
+
|
|
28
|
+
if (originHeight <= 0) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
val ratio = targetHeight.toFloat() / originHeight
|
|
33
|
+
fm.descent = (fm.descent * ratio).roundToInt()
|
|
34
|
+
fm.ascent = fm.descent - targetHeight
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 styleCache: SpanStyleCache,
|
|
16
|
+
private val blockStyle: BlockStyle,
|
|
17
|
+
private val context: Context,
|
|
18
|
+
) : ClickableSpan() {
|
|
19
|
+
override fun onClick(widget: View) {
|
|
20
|
+
if (onLinkPress != null) {
|
|
21
|
+
onLinkPress(url)
|
|
22
|
+
} else if (widget is EnrichedMarkdownText) {
|
|
23
|
+
// Emit event directly from view (enriched pattern)
|
|
24
|
+
widget.emitOnLinkPress(url)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun updateDrawState(textPaint: TextPaint) {
|
|
29
|
+
super.updateDrawState(textPaint)
|
|
30
|
+
|
|
31
|
+
textPaint.textSize = blockStyle.fontSize
|
|
32
|
+
textPaint.applyBlockStyleFont(blockStyle, context)
|
|
33
|
+
|
|
34
|
+
textPaint.color = styleCache.linkColor
|
|
35
|
+
textPaint.isUnderlineText = styleCache.linkUnderline
|
|
36
|
+
}
|
|
37
|
+
}
|