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
package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.text.Selection
|
|
6
|
+
import android.text.Spannable
|
|
7
|
+
import android.text.method.LinkMovementMethod
|
|
8
|
+
import android.view.MotionEvent
|
|
9
|
+
import android.view.ViewConfiguration
|
|
10
|
+
import android.widget.TextView
|
|
11
|
+
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
12
|
+
import kotlin.math.abs
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Custom MovementMethod that handles both link clicks and long presses.
|
|
16
|
+
* Extends LinkMovementMethod to maintain click functionality while adding long press support.
|
|
17
|
+
*/
|
|
18
|
+
class LinkLongPressMovementMethod : LinkMovementMethod() {
|
|
19
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
20
|
+
private var longPressRunnable: Runnable? = null
|
|
21
|
+
|
|
22
|
+
private var startX = 0f
|
|
23
|
+
private var startY = 0f
|
|
24
|
+
|
|
25
|
+
override fun onTouchEvent(
|
|
26
|
+
widget: TextView,
|
|
27
|
+
buffer: Spannable,
|
|
28
|
+
event: MotionEvent,
|
|
29
|
+
): Boolean {
|
|
30
|
+
when (event.action) {
|
|
31
|
+
MotionEvent.ACTION_DOWN -> {
|
|
32
|
+
startX = event.x
|
|
33
|
+
startY = event.y
|
|
34
|
+
|
|
35
|
+
// Identify if a LinkSpan exists at the touch coordinates
|
|
36
|
+
findLinkSpan(widget, buffer, event)?.let { span ->
|
|
37
|
+
scheduleLongPress(widget, span)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
MotionEvent.ACTION_MOVE -> {
|
|
42
|
+
val config = ViewConfiguration.get(widget.context)
|
|
43
|
+
// Cancel if the finger moves beyond the standard system touch slop
|
|
44
|
+
if (abs(event.x - startX) > config.scaledTouchSlop ||
|
|
45
|
+
abs(event.y - startY) > config.scaledTouchSlop
|
|
46
|
+
) {
|
|
47
|
+
cancelLongPress()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
52
|
+
cancelLongPress()
|
|
53
|
+
// Clear text selection to prevent the "stuck" highlight look
|
|
54
|
+
if (widget.hasSelection()) {
|
|
55
|
+
Selection.removeSelection(buffer)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Let the parent LinkMovementMethod handle the standard click logic
|
|
61
|
+
val result = super.onTouchEvent(widget, buffer, event)
|
|
62
|
+
|
|
63
|
+
// LinkMovementMethod sets a Selection highlight around the link on ACTION_DOWN,
|
|
64
|
+
// which causes a visible selection color on the link text while pressed.
|
|
65
|
+
// We remove that selection immediately so the user never sees it.
|
|
66
|
+
if (event.action == MotionEvent.ACTION_DOWN) {
|
|
67
|
+
Selection.removeSelection(buffer)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private fun scheduleLongPress(
|
|
74
|
+
widget: TextView,
|
|
75
|
+
span: LinkSpan,
|
|
76
|
+
) {
|
|
77
|
+
cancelLongPress()
|
|
78
|
+
|
|
79
|
+
longPressRunnable =
|
|
80
|
+
Runnable {
|
|
81
|
+
if (widget.hasSelection()) {
|
|
82
|
+
Selection.removeSelection(widget.text as Spannable)
|
|
83
|
+
}
|
|
84
|
+
// Execute the long click logic on the span
|
|
85
|
+
if (span.onLongClick(widget)) {
|
|
86
|
+
// If consumed, cancel the system's own long-press logic (like context menus)
|
|
87
|
+
widget.cancelLongPress()
|
|
88
|
+
}
|
|
89
|
+
longPressRunnable = null
|
|
90
|
+
}.also {
|
|
91
|
+
handler.postDelayed(it, ViewConfiguration.getLongPressTimeout().toLong())
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private fun cancelLongPress() {
|
|
96
|
+
longPressRunnable?.let(handler::removeCallbacks)
|
|
97
|
+
longPressRunnable = null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private fun findLinkSpan(
|
|
101
|
+
widget: TextView,
|
|
102
|
+
buffer: Spannable,
|
|
103
|
+
event: MotionEvent,
|
|
104
|
+
): LinkSpan? {
|
|
105
|
+
// Adjust coordinates for padding and scroll
|
|
106
|
+
val x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX
|
|
107
|
+
val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY
|
|
108
|
+
|
|
109
|
+
val layout = widget.layout ?: return null
|
|
110
|
+
val line = layout.getLineForVertical(y)
|
|
111
|
+
val offset = layout.getOffsetForHorizontal(line, x.toFloat())
|
|
112
|
+
|
|
113
|
+
// Ensure the touch is within the character bounds
|
|
114
|
+
return buffer.getSpans(offset, offset, LinkSpan::class.java).firstOrNull()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
companion object {
|
|
118
|
+
@JvmStatic
|
|
119
|
+
fun createInstance(): LinkLongPressMovementMethod = LinkLongPressMovementMethod()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils
|
|
2
|
+
|
|
3
|
+
import android.text.Spannable
|
|
4
|
+
import android.text.style.UnderlineSpan
|
|
5
|
+
import android.widget.TextView
|
|
6
|
+
import com.swmansion.enriched.markdown.EnrichedMarkdownText
|
|
7
|
+
import com.swmansion.enriched.markdown.spans.BlockquoteSpan
|
|
8
|
+
import com.swmansion.enriched.markdown.spans.CodeBlockSpan
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.CodeSpan
|
|
10
|
+
import com.swmansion.enriched.markdown.spans.EmphasisSpan
|
|
11
|
+
import com.swmansion.enriched.markdown.spans.HeadingSpan
|
|
12
|
+
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
13
|
+
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
14
|
+
import com.swmansion.enriched.markdown.spans.OrderedListSpan
|
|
15
|
+
import com.swmansion.enriched.markdown.spans.StrikethroughSpan
|
|
16
|
+
import com.swmansion.enriched.markdown.spans.StrongSpan
|
|
17
|
+
import com.swmansion.enriched.markdown.spans.ThematicBreakSpan
|
|
18
|
+
import com.swmansion.enriched.markdown.spans.UnorderedListSpan
|
|
19
|
+
|
|
20
|
+
/** Extracts markdown from styled text (Spannable). */
|
|
21
|
+
object MarkdownExtractor {
|
|
22
|
+
/**
|
|
23
|
+
* Gets markdown for the current text selection.
|
|
24
|
+
* Full selection returns original markdown, partial reconstructs from spans.
|
|
25
|
+
*/
|
|
26
|
+
fun getMarkdownForSelection(textView: TextView): String? {
|
|
27
|
+
val start = textView.selectionStart
|
|
28
|
+
val end = textView.selectionEnd
|
|
29
|
+
if (start < 0 || end < 0 || start >= end) return null
|
|
30
|
+
|
|
31
|
+
val spannable = textView.text as? Spannable ?: return null
|
|
32
|
+
|
|
33
|
+
val isFullSelection = start == 0 && end >= textView.text.length - 1
|
|
34
|
+
if (isFullSelection && textView is EnrichedMarkdownText) {
|
|
35
|
+
val original = textView.currentMarkdown
|
|
36
|
+
if (original.isNotEmpty()) return original
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return extractFromSpannable(spannable, start, end)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Extracts markdown from a Spannable within a given range. */
|
|
43
|
+
fun extractFromSpannable(
|
|
44
|
+
spannable: Spannable,
|
|
45
|
+
start: Int,
|
|
46
|
+
end: Int,
|
|
47
|
+
): String {
|
|
48
|
+
val result = StringBuilder()
|
|
49
|
+
val state = ExtractionState()
|
|
50
|
+
val headingAccumulator = HeadingAccumulator()
|
|
51
|
+
|
|
52
|
+
var i = start
|
|
53
|
+
while (i < end) {
|
|
54
|
+
val nextTransition = spannable.nextSpanTransition(i, end, Any::class.java)
|
|
55
|
+
val segmentText = spannable.subSequence(i, nextTransition).toString()
|
|
56
|
+
|
|
57
|
+
val handled =
|
|
58
|
+
processSegment(
|
|
59
|
+
spannable = spannable,
|
|
60
|
+
segmentText = segmentText,
|
|
61
|
+
segmentStart = i,
|
|
62
|
+
segmentEnd = nextTransition,
|
|
63
|
+
result = result,
|
|
64
|
+
state = state,
|
|
65
|
+
headingAccumulator = headingAccumulator,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if (!handled) {
|
|
69
|
+
// Regular text segment - apply inline formatting and block prefixes
|
|
70
|
+
appendFormattedSegment(spannable, segmentText, i, nextTransition, result, state)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
i = nextTransition
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
headingAccumulator.flush(result, state)
|
|
77
|
+
return result.toString()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Returns true if segment was handled specially. */
|
|
81
|
+
private fun processSegment(
|
|
82
|
+
spannable: Spannable,
|
|
83
|
+
segmentText: String,
|
|
84
|
+
segmentStart: Int,
|
|
85
|
+
segmentEnd: Int,
|
|
86
|
+
result: StringBuilder,
|
|
87
|
+
state: ExtractionState,
|
|
88
|
+
headingAccumulator: HeadingAccumulator,
|
|
89
|
+
): Boolean {
|
|
90
|
+
// Check for thematic breaks first (uses " \n" as placeholder)
|
|
91
|
+
val thematicBreakSpans = spannable.getSpans(segmentStart, segmentEnd, ThematicBreakSpan::class.java)
|
|
92
|
+
if (thematicBreakSpans.isNotEmpty()) {
|
|
93
|
+
appendThematicBreak(result, state)
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (segmentText == "\uFFFC" || segmentText == "\u200B") {
|
|
98
|
+
val imageSpans = spannable.getSpans(segmentStart, segmentEnd, ImageSpan::class.java)
|
|
99
|
+
if (imageSpans.isNotEmpty()) {
|
|
100
|
+
appendImage(imageSpans[0], result, state)
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (segmentText.isEmpty()) return true
|
|
106
|
+
|
|
107
|
+
if (segmentText == "\n" || segmentText == "\n\n") {
|
|
108
|
+
handleNewline(spannable, segmentStart, segmentEnd, result, state)
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
val headingSpans = spannable.getSpans(segmentStart, segmentEnd, HeadingSpan::class.java)
|
|
113
|
+
if (headingSpans.isNotEmpty()) {
|
|
114
|
+
headingAccumulator.accumulate(headingSpans[0].level, segmentText, result, state)
|
|
115
|
+
return true
|
|
116
|
+
} else {
|
|
117
|
+
headingAccumulator.flush(result, state)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
val codeBlockSpans = spannable.getSpans(segmentStart, segmentEnd, CodeBlockSpan::class.java)
|
|
121
|
+
if (codeBlockSpans.isNotEmpty()) {
|
|
122
|
+
appendCodeBlock(segmentText, result, state)
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private fun appendFormattedSegment(
|
|
130
|
+
spannable: Spannable,
|
|
131
|
+
segmentText: String,
|
|
132
|
+
segmentStart: Int,
|
|
133
|
+
segmentEnd: Int,
|
|
134
|
+
result: StringBuilder,
|
|
135
|
+
state: ExtractionState,
|
|
136
|
+
) {
|
|
137
|
+
val blockquotePrefix = detectBlockquote(spannable, segmentStart, segmentEnd, state)
|
|
138
|
+
val listPrefix = detectList(spannable, segmentStart, segmentEnd, state)
|
|
139
|
+
var segment = applyInlineFormatting(spannable, segmentText, segmentStart, segmentEnd)
|
|
140
|
+
|
|
141
|
+
if (result.isAtLineStart() && !segmentText.startsWith("\n")) {
|
|
142
|
+
segment = buildBlockPrefix(blockquotePrefix, listPrefix) + segment
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (state.needsBlankLine && result.isNotEmpty()) {
|
|
146
|
+
result.ensureBlankLine()
|
|
147
|
+
state.needsBlankLine = false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
result.append(segment)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private fun appendImage(
|
|
154
|
+
img: ImageSpan,
|
|
155
|
+
result: StringBuilder,
|
|
156
|
+
state: ExtractionState,
|
|
157
|
+
) {
|
|
158
|
+
if (img.isInline) {
|
|
159
|
+
result.append("")
|
|
160
|
+
} else {
|
|
161
|
+
result.ensureBlankLine()
|
|
162
|
+
result.append("\n")
|
|
163
|
+
state.needsBlankLine = true
|
|
164
|
+
state.blockquoteDepth = -1
|
|
165
|
+
state.listDepth = -1
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private fun appendThematicBreak(
|
|
170
|
+
result: StringBuilder,
|
|
171
|
+
state: ExtractionState,
|
|
172
|
+
) {
|
|
173
|
+
result.ensureBlankLine()
|
|
174
|
+
result.append("---\n")
|
|
175
|
+
state.needsBlankLine = true
|
|
176
|
+
state.blockquoteDepth = -1
|
|
177
|
+
state.listDepth = -1
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun handleNewline(
|
|
181
|
+
spannable: Spannable,
|
|
182
|
+
start: Int,
|
|
183
|
+
end: Int,
|
|
184
|
+
result: StringBuilder,
|
|
185
|
+
state: ExtractionState,
|
|
186
|
+
) {
|
|
187
|
+
val inBlockquote = spannable.getSpans(start, end, BlockquoteSpan::class.java).isNotEmpty()
|
|
188
|
+
val inList =
|
|
189
|
+
spannable.getSpans(start, end, OrderedListSpan::class.java).isNotEmpty() ||
|
|
190
|
+
spannable.getSpans(start, end, UnorderedListSpan::class.java).isNotEmpty()
|
|
191
|
+
|
|
192
|
+
when {
|
|
193
|
+
!inBlockquote && state.blockquoteDepth >= 0 -> {
|
|
194
|
+
result.ensureBlankLine()
|
|
195
|
+
state.blockquoteDepth = -1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
!inList && state.listDepth >= 0 -> {
|
|
199
|
+
result.ensureBlankLine()
|
|
200
|
+
state.listDepth = -1
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
inBlockquote || inList -> {
|
|
204
|
+
if (!result.endsWith("\n")) result.append("\n")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
else -> {
|
|
208
|
+
result.ensureBlankLine()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private fun appendCodeBlock(
|
|
214
|
+
text: String,
|
|
215
|
+
result: StringBuilder,
|
|
216
|
+
state: ExtractionState,
|
|
217
|
+
) {
|
|
218
|
+
if (state.needsBlankLine) {
|
|
219
|
+
result.ensureBlankLine()
|
|
220
|
+
state.needsBlankLine = false
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
val needsFence = result.isEmpty() || result.endsWith("\n\n")
|
|
224
|
+
if (needsFence) result.append("```\n")
|
|
225
|
+
|
|
226
|
+
result.append(text)
|
|
227
|
+
|
|
228
|
+
if (text.endsWith("\n")) {
|
|
229
|
+
result.append("```\n")
|
|
230
|
+
state.needsBlankLine = true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private fun detectBlockquote(
|
|
235
|
+
spannable: Spannable,
|
|
236
|
+
start: Int,
|
|
237
|
+
end: Int,
|
|
238
|
+
state: ExtractionState,
|
|
239
|
+
): String? {
|
|
240
|
+
val spans = spannable.getSpans(start, end, BlockquoteSpan::class.java)
|
|
241
|
+
val depth = spans.maxOfOrNull { it.depth } ?: -1
|
|
242
|
+
|
|
243
|
+
return if (depth >= 0) {
|
|
244
|
+
state.blockquoteDepth = depth
|
|
245
|
+
"> ".repeat(depth + 1)
|
|
246
|
+
} else {
|
|
247
|
+
if (state.blockquoteDepth >= 0) state.blockquoteDepth = -1
|
|
248
|
+
null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun detectList(
|
|
253
|
+
spannable: Spannable,
|
|
254
|
+
start: Int,
|
|
255
|
+
end: Int,
|
|
256
|
+
state: ExtractionState,
|
|
257
|
+
): String? {
|
|
258
|
+
val orderedSpans = spannable.getSpans(start, end, OrderedListSpan::class.java)
|
|
259
|
+
val unorderedSpans = spannable.getSpans(start, end, UnorderedListSpan::class.java)
|
|
260
|
+
|
|
261
|
+
val orderedDepth = orderedSpans.maxOfOrNull { it.depth } ?: -1
|
|
262
|
+
val unorderedDepth = unorderedSpans.maxOfOrNull { it.depth } ?: -1
|
|
263
|
+
val depth = maxOf(orderedDepth, unorderedDepth)
|
|
264
|
+
|
|
265
|
+
return if (depth >= 0) {
|
|
266
|
+
state.listDepth = depth
|
|
267
|
+
val indent = " ".repeat(depth)
|
|
268
|
+
if (orderedSpans.isNotEmpty()) {
|
|
269
|
+
"$indent${orderedSpans[0].itemNumber}. "
|
|
270
|
+
} else {
|
|
271
|
+
"$indent- "
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
if (state.listDepth >= 0) state.listDepth = -1
|
|
275
|
+
null
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private fun applyInlineFormatting(
|
|
280
|
+
spannable: Spannable,
|
|
281
|
+
text: String,
|
|
282
|
+
start: Int,
|
|
283
|
+
end: Int,
|
|
284
|
+
): String {
|
|
285
|
+
val hasStrong = spannable.getSpans(start, end, StrongSpan::class.java).isNotEmpty()
|
|
286
|
+
val hasEmphasis = spannable.getSpans(start, end, EmphasisSpan::class.java).isNotEmpty()
|
|
287
|
+
val hasCode = spannable.getSpans(start, end, CodeSpan::class.java).isNotEmpty()
|
|
288
|
+
val hasStrikethrough = spannable.getSpans(start, end, StrikethroughSpan::class.java).isNotEmpty()
|
|
289
|
+
val hasUnderline = spannable.getSpans(start, end, UnderlineSpan::class.java).isNotEmpty()
|
|
290
|
+
val linkSpans = spannable.getSpans(start, end, LinkSpan::class.java)
|
|
291
|
+
|
|
292
|
+
var result = text
|
|
293
|
+
|
|
294
|
+
// Innermost first
|
|
295
|
+
if (hasCode && linkSpans.isEmpty()) {
|
|
296
|
+
result = "`$result`"
|
|
297
|
+
}
|
|
298
|
+
if (hasStrikethrough) {
|
|
299
|
+
result = "~~$result~~"
|
|
300
|
+
}
|
|
301
|
+
if (hasUnderline && linkSpans.isEmpty()) {
|
|
302
|
+
result = "<u>$result</u>"
|
|
303
|
+
}
|
|
304
|
+
if (hasEmphasis) {
|
|
305
|
+
result = "*$result*"
|
|
306
|
+
}
|
|
307
|
+
if (hasStrong) {
|
|
308
|
+
result = "**$result**"
|
|
309
|
+
}
|
|
310
|
+
if (linkSpans.isNotEmpty()) {
|
|
311
|
+
result = "[$text](${linkSpans[0].url})"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return result
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private fun buildBlockPrefix(
|
|
318
|
+
blockquotePrefix: String?,
|
|
319
|
+
listPrefix: String?,
|
|
320
|
+
): String =
|
|
321
|
+
buildString {
|
|
322
|
+
blockquotePrefix?.let { append(it) }
|
|
323
|
+
listPrefix?.let { append(it) }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private data class ExtractionState(
|
|
327
|
+
var blockquoteDepth: Int = -1,
|
|
328
|
+
var listDepth: Int = -1,
|
|
329
|
+
var needsBlankLine: Boolean = false,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
/** Accumulates heading content across multiple span segments. */
|
|
333
|
+
private class HeadingAccumulator {
|
|
334
|
+
private var level: Int? = null
|
|
335
|
+
private val content = StringBuilder()
|
|
336
|
+
|
|
337
|
+
fun accumulate(
|
|
338
|
+
newLevel: Int,
|
|
339
|
+
text: String,
|
|
340
|
+
result: StringBuilder,
|
|
341
|
+
state: ExtractionState,
|
|
342
|
+
) {
|
|
343
|
+
if (level != newLevel) {
|
|
344
|
+
flush(result, state)
|
|
345
|
+
level = newLevel
|
|
346
|
+
}
|
|
347
|
+
content.append(text.trim('\n'))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
fun flush(
|
|
351
|
+
result: StringBuilder,
|
|
352
|
+
state: ExtractionState,
|
|
353
|
+
) {
|
|
354
|
+
val currentLevel = level ?: return
|
|
355
|
+
if (content.isEmpty()) return
|
|
356
|
+
|
|
357
|
+
result.ensureBlankLine()
|
|
358
|
+
result.append("#".repeat(currentLevel))
|
|
359
|
+
result.append(" ")
|
|
360
|
+
result.append(content.toString().trim())
|
|
361
|
+
result.append("\n")
|
|
362
|
+
|
|
363
|
+
level = null
|
|
364
|
+
content.clear()
|
|
365
|
+
state.needsBlankLine = true
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private fun StringBuilder.ensureBlankLine() {
|
|
370
|
+
if (isEmpty() || endsWith("\n\n")) return
|
|
371
|
+
append(if (endsWith("\n")) "\n" else "\n\n")
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private fun StringBuilder.isAtLineStart(): Boolean = isEmpty() || endsWith("\n")
|
|
375
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils
|
|
2
|
+
|
|
3
|
+
import android.content.ClipData
|
|
4
|
+
import android.content.ClipboardManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.text.Spannable
|
|
7
|
+
import android.view.ActionMode
|
|
8
|
+
import android.view.Menu
|
|
9
|
+
import android.view.MenuItem
|
|
10
|
+
import android.widget.TextView
|
|
11
|
+
import com.swmansion.enriched.markdown.EnrichedMarkdownText
|
|
12
|
+
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
13
|
+
|
|
14
|
+
private const val MENU_ITEM_COPY_MARKDOWN = 1000
|
|
15
|
+
private const val MENU_ITEM_COPY_IMAGE_URL = 1001
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates an ActionMode.Callback that adds custom copy options and
|
|
19
|
+
* overrides the default "Copy" action to include HTML for rich text support.
|
|
20
|
+
*/
|
|
21
|
+
fun createSelectionActionModeCallback(textView: TextView): ActionMode.Callback =
|
|
22
|
+
object : ActionMode.Callback {
|
|
23
|
+
override fun onCreateActionMode(
|
|
24
|
+
mode: ActionMode?,
|
|
25
|
+
menu: Menu?,
|
|
26
|
+
): Boolean = true
|
|
27
|
+
|
|
28
|
+
override fun onPrepareActionMode(
|
|
29
|
+
mode: ActionMode?,
|
|
30
|
+
menu: Menu?,
|
|
31
|
+
): Boolean {
|
|
32
|
+
if (menu == null) return false
|
|
33
|
+
|
|
34
|
+
menu.removeItem(MENU_ITEM_COPY_MARKDOWN)
|
|
35
|
+
menu.removeItem(MENU_ITEM_COPY_IMAGE_URL)
|
|
36
|
+
|
|
37
|
+
if (textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart) {
|
|
38
|
+
menu.add(Menu.NONE, MENU_ITEM_COPY_MARKDOWN, Menu.NONE, "Copy as Markdown")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
val imageUrls = textView.getImageUrlsInSelection()
|
|
42
|
+
if (imageUrls.isNotEmpty()) {
|
|
43
|
+
val title =
|
|
44
|
+
if (imageUrls.size == 1) {
|
|
45
|
+
"Copy Image URL"
|
|
46
|
+
} else {
|
|
47
|
+
"Copy ${imageUrls.size} Image URLs"
|
|
48
|
+
}
|
|
49
|
+
menu.add(Menu.NONE, MENU_ITEM_COPY_IMAGE_URL, Menu.NONE, title)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override fun onActionItemClicked(
|
|
56
|
+
mode: ActionMode?,
|
|
57
|
+
item: MenuItem?,
|
|
58
|
+
): Boolean {
|
|
59
|
+
when (item?.itemId) {
|
|
60
|
+
android.R.id.copy -> {
|
|
61
|
+
textView.copyWithHTML()
|
|
62
|
+
mode?.finish()
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
MENU_ITEM_COPY_MARKDOWN -> {
|
|
67
|
+
textView.copyMarkdownToClipboard()
|
|
68
|
+
mode?.finish()
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
MENU_ITEM_COPY_IMAGE_URL -> {
|
|
73
|
+
textView.copyImageUrlsToClipboard()
|
|
74
|
+
mode?.finish()
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override fun onDestroyActionMode(mode: ActionMode?) {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Copies selection as both plain text and HTML with inline styles. */
|
|
85
|
+
private fun TextView.copyWithHTML() {
|
|
86
|
+
val start = selectionStart
|
|
87
|
+
val end = selectionEnd
|
|
88
|
+
if (start < 0 || end < 0 || start >= end) return
|
|
89
|
+
|
|
90
|
+
val spannable = text as? Spannable ?: return
|
|
91
|
+
val selectedText = spannable.subSequence(start, end)
|
|
92
|
+
val plainText = selectedText.toString()
|
|
93
|
+
|
|
94
|
+
val enrichedMarkdownText = this as? EnrichedMarkdownText
|
|
95
|
+
val styleConfig = enrichedMarkdownText?.markdownStyle
|
|
96
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
97
|
+
|
|
98
|
+
if (styleConfig != null && selectedText is Spannable) {
|
|
99
|
+
// Density values convert device pixels back to CSS pixels
|
|
100
|
+
val displayMetrics = context.resources.displayMetrics
|
|
101
|
+
val html =
|
|
102
|
+
HTMLGenerator.generateHTML(
|
|
103
|
+
selectedText,
|
|
104
|
+
styleConfig,
|
|
105
|
+
displayMetrics.scaledDensity,
|
|
106
|
+
displayMetrics.density,
|
|
107
|
+
)
|
|
108
|
+
clipboard.setPrimaryClip(ClipData.newHtmlText("EnrichedMarkdown", plainText, html))
|
|
109
|
+
} else {
|
|
110
|
+
clipboard.setPrimaryClip(ClipData.newPlainText("Text", plainText))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fun TextView.copyMarkdownToClipboard() {
|
|
115
|
+
val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: return
|
|
116
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
117
|
+
clipboard.setPrimaryClip(ClipData.newPlainText("Markdown", markdown))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Returns remote image URLs (http/https only) from the current selection. */
|
|
121
|
+
private fun TextView.getImageUrlsInSelection(): List<String> {
|
|
122
|
+
val start = selectionStart
|
|
123
|
+
val end = selectionEnd
|
|
124
|
+
if (start < 0 || end < 0 || start >= end) return emptyList()
|
|
125
|
+
|
|
126
|
+
val spannable = text as? Spannable ?: return emptyList()
|
|
127
|
+
return spannable
|
|
128
|
+
.getSpans(start, end, ImageSpan::class.java)
|
|
129
|
+
.mapNotNull { it.imageUrl }
|
|
130
|
+
.filter { it.startsWith("http://") || it.startsWith("https://") }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun TextView.copyImageUrlsToClipboard() {
|
|
134
|
+
val urls = getImageUrlsInSelection()
|
|
135
|
+
if (urls.isEmpty()) return
|
|
136
|
+
|
|
137
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
138
|
+
clipboard.setPrimaryClip(ClipData.newPlainText("Image URLs", urls.joinToString("\n")))
|
|
139
|
+
}
|