react-native-enriched-markdown-scaffold 1.0.2-scaffold.2 → 1.0.2-scaffold.4
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/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +134 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +13 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +42 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +24 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +27 -8
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +9 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +18 -2
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
- package/docs/API_REFERENCE.md +101 -0
- package/docs/COPY_OPTIONS.md +14 -0
- package/docs/MENTIONS.md +8 -1
- package/docs/STYLES.md +27 -0
- package/docs/TEXT.md +2 -0
- package/ios/EnrichedMarkdown.mm +91 -2
- package/ios/EnrichedMarkdownText.mm +64 -3
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +134 -0
- package/ios/internals/MeasurementCache.h +5 -0
- package/ios/renderer/LinkRenderer.m +47 -15
- package/ios/styles/StyleConfig.h +16 -0
- package/ios/styles/StyleConfig.mm +60 -0
- package/ios/utils/CodeBackground.m +3 -65
- package/ios/utils/ENRMBlockMenu.h +12 -3
- package/ios/utils/ENRMChipDrawing.h +24 -0
- package/ios/utils/ENRMChipDrawing.m +83 -0
- package/ios/utils/ENRMLinkChip.h +70 -0
- package/ios/utils/ENRMLinkChip.m +194 -0
- package/ios/utils/ENRMLinkLayout.h +50 -0
- package/ios/utils/ENRMLinkLayout.m +45 -0
- package/ios/utils/EditMenuUtils+macOS.m +4 -1
- package/ios/utils/EditMenuUtils.h +10 -1
- package/ios/utils/EditMenuUtils.m +22 -5
- package/ios/utils/HTMLGenerator.m +2 -1
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +4 -1
- package/ios/utils/MarkdownExtractor.m +2 -1
- package/ios/utils/RTFExportUtils.m +2 -0
- package/ios/utils/StylePropsUtils.h +37 -1
- package/ios/utils/TextViewLayoutManager.mm +3 -0
- package/ios/views/ENRMCodeBlockContainerView.h +4 -0
- package/ios/views/ENRMCodeBlockContainerView.m +8 -1
- package/ios/views/ENRMMathContainerView.h +4 -0
- package/ios/views/ENRMMathContainerView.m +8 -1
- package/ios/views/TableContainerView.h +7 -0
- package/ios/views/TableContainerView.m +30 -3
- package/lib/module/EnrichedMarkdownNativeComponent.ts +57 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +57 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +119 -9
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +13 -2
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +13 -2
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/module/web/EnrichedMarkdownText.js +6 -3
- package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/web/renderers/InlineRenderers.js +96 -3
- package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
- package/lib/module/web/styles.js +18 -0
- package/lib/module/web/styles.js.map +1 -1
- package/lib/module/web/utils.js +13 -0
- package/lib/module/web/utils.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +53 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +53 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +1 -1
- package/lib/typescript/src/index.web.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyle.d.ts +28 -5
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +10 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +54 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +22 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
- package/lib/typescript/src/web/styles.d.ts.map +1 -1
- package/lib/typescript/src/web/types.d.ts +4 -0
- package/lib/typescript/src/web/types.d.ts.map +1 -1
- package/lib/typescript/src/web/utils.d.ts +4 -0
- package/lib/typescript/src/web/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownNativeComponent.ts +57 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +57 -0
- package/src/index.tsx +3 -0
- package/src/index.web.tsx +5 -1
- package/src/native/EnrichedMarkdownText.tsx +127 -7
- package/src/normalizeMarkdownStyle.ts +13 -0
- package/src/normalizeMarkdownStyle.web.ts +11 -0
- package/src/types/MarkdownStyle.ts +29 -5
- package/src/types/MarkdownStyleInternal.ts +10 -0
- package/src/types/MarkdownTextProps.ts +56 -0
- package/src/types/MarkdownTextProps.web.ts +43 -1
- package/src/types/events.ts +23 -0
- package/src/web/EnrichedMarkdownText.tsx +10 -3
- package/src/web/renderers/InlineRenderers.tsx +111 -3
- package/src/web/styles.ts +25 -0
- package/src/web/types.ts +4 -0
- package/src/web/utils.ts +8 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Canvas
|
|
4
|
+
import android.graphics.Paint
|
|
5
|
+
import android.graphics.Path
|
|
6
|
+
import android.graphics.RectF
|
|
7
|
+
import android.text.Spanned
|
|
8
|
+
import android.text.StaticLayout
|
|
9
|
+
import android.text.TextPaint
|
|
10
|
+
import android.text.style.LeadingMarginSpan
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shared geometry and drawing for inline "chips" painted from a [android.text.style.LineBackgroundSpan]
|
|
14
|
+
* (inline code, link chips). For a chip that wraps, only the outer ends are rounded and the border
|
|
15
|
+
* stays open where a fragment is cut.
|
|
16
|
+
*/
|
|
17
|
+
internal object InlineChipPainter {
|
|
18
|
+
/**
|
|
19
|
+
* Returns the x position of [index] relative to the line's left edge, including any
|
|
20
|
+
* leading margin. The measuring StaticLayout is built from a subSequence that keeps
|
|
21
|
+
* all spans, so LeadingMarginSpans (lists, blockquotes) are already applied to
|
|
22
|
+
* getPrimaryHorizontal; adding the margin again on top would shift the background
|
|
23
|
+
* right by the indent. The margin is only added explicitly in the early-return case,
|
|
24
|
+
* where no layout is built.
|
|
25
|
+
*/
|
|
26
|
+
fun horizontalOffset(
|
|
27
|
+
text: CharSequence,
|
|
28
|
+
lineStart: Int,
|
|
29
|
+
lineEnd: Int,
|
|
30
|
+
index: Int,
|
|
31
|
+
paint: Paint,
|
|
32
|
+
leadingMargin: Int,
|
|
33
|
+
): Float {
|
|
34
|
+
if (index <= lineStart) return leadingMargin.toFloat()
|
|
35
|
+
val lineText = text.subSequence(lineStart, lineEnd)
|
|
36
|
+
val textPaint = paint as? TextPaint ?: TextPaint(paint)
|
|
37
|
+
val layout = StaticLayout.Builder.obtain(lineText, 0, lineText.length, textPaint, 10000).build()
|
|
38
|
+
return layout.getPrimaryHorizontal(index - lineStart)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fun leadingMarginAt(
|
|
42
|
+
text: Spanned,
|
|
43
|
+
lineStart: Int,
|
|
44
|
+
): Int {
|
|
45
|
+
if (lineStart >= text.length) return 0
|
|
46
|
+
val spans = text.getSpans(lineStart, lineStart + 1, LeadingMarginSpan::class.java)
|
|
47
|
+
var margin = 0
|
|
48
|
+
for (span in spans) {
|
|
49
|
+
margin += span.getLeadingMargin(false)
|
|
50
|
+
}
|
|
51
|
+
return margin
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fills [rect] and strokes its border. [strokeInset] keeps a closed border inside [rect]
|
|
56
|
+
* (link chips); otherwise it is centered on the edge (inline code, kept for pixel parity).
|
|
57
|
+
* [path] is caller-owned scratch space to avoid per-draw allocations.
|
|
58
|
+
*/
|
|
59
|
+
fun draw(
|
|
60
|
+
canvas: Canvas,
|
|
61
|
+
rect: RectF,
|
|
62
|
+
path: Path,
|
|
63
|
+
cornerRadius: Float,
|
|
64
|
+
backgroundPaint: Paint?,
|
|
65
|
+
borderPaint: Paint?,
|
|
66
|
+
isFirst: Boolean,
|
|
67
|
+
isLast: Boolean,
|
|
68
|
+
strokeInset: Boolean,
|
|
69
|
+
) {
|
|
70
|
+
val r = minOf(maxOf(cornerRadius, 0f), minOf(rect.width(), rect.height()) / 2f)
|
|
71
|
+
|
|
72
|
+
if (backgroundPaint != null) {
|
|
73
|
+
path.reset()
|
|
74
|
+
path.addRoundRect(rect, radii(r, isFirst, isLast), Path.Direction.CW)
|
|
75
|
+
canvas.drawPath(path, backgroundPaint)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (borderPaint == null || borderPaint.strokeWidth <= 0f) return
|
|
79
|
+
|
|
80
|
+
path.reset()
|
|
81
|
+
if (isFirst && isLast) {
|
|
82
|
+
if (strokeInset) {
|
|
83
|
+
val inset = borderPaint.strokeWidth / 2f
|
|
84
|
+
val insetRect = RectF(rect.left + inset, rect.top + inset, rect.right - inset, rect.bottom - inset)
|
|
85
|
+
path.addRoundRect(insetRect, radii(maxOf(r - inset, 0f), true, true), Path.Direction.CW)
|
|
86
|
+
} else {
|
|
87
|
+
path.addRoundRect(rect, radii(r, true, true), Path.Direction.CW)
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
val inset = if (strokeInset) borderPaint.strokeWidth / 2f else 0f
|
|
91
|
+
addOpenBorder(path, rect, r, inset, isFirst, isLast)
|
|
92
|
+
}
|
|
93
|
+
canvas.drawPath(path, borderPaint)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun addOpenBorder(
|
|
97
|
+
path: Path,
|
|
98
|
+
rect: RectF,
|
|
99
|
+
r: Float,
|
|
100
|
+
inset: Float,
|
|
101
|
+
isFirst: Boolean,
|
|
102
|
+
isLast: Boolean,
|
|
103
|
+
) {
|
|
104
|
+
val left = rect.left + inset
|
|
105
|
+
val top = rect.top + inset
|
|
106
|
+
val right = rect.right - inset
|
|
107
|
+
val bottom = rect.bottom - inset
|
|
108
|
+
|
|
109
|
+
if (isFirst) {
|
|
110
|
+
path.moveTo(rect.right, top)
|
|
111
|
+
path.lineTo(left + r, top)
|
|
112
|
+
path.quadTo(left, top, left, top + r)
|
|
113
|
+
path.lineTo(left, bottom - r)
|
|
114
|
+
path.quadTo(left, bottom, left + r, bottom)
|
|
115
|
+
path.lineTo(rect.right, bottom)
|
|
116
|
+
} else if (isLast) {
|
|
117
|
+
path.moveTo(rect.left, top)
|
|
118
|
+
path.lineTo(right - r, top)
|
|
119
|
+
path.quadTo(right, top, right, top + r)
|
|
120
|
+
path.lineTo(right, bottom - r)
|
|
121
|
+
path.quadTo(right, bottom, right - r, bottom)
|
|
122
|
+
path.lineTo(rect.left, bottom)
|
|
123
|
+
} else {
|
|
124
|
+
path.moveTo(rect.left, top)
|
|
125
|
+
path.lineTo(rect.right, top)
|
|
126
|
+
path.moveTo(rect.left, bottom)
|
|
127
|
+
path.lineTo(rect.right, bottom)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun radii(
|
|
132
|
+
r: Float,
|
|
133
|
+
isFirst: Boolean,
|
|
134
|
+
isLast: Boolean,
|
|
135
|
+
): FloatArray {
|
|
136
|
+
val left = if (isFirst) r else 0f
|
|
137
|
+
val right = if (isLast) r else 0f
|
|
138
|
+
return floatArrayOf(left, left, right, right, right, right, left, left)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Canvas
|
|
4
|
+
import android.graphics.Paint
|
|
5
|
+
import android.graphics.Path
|
|
6
|
+
import android.graphics.RectF
|
|
7
|
+
import android.text.SpannableStringBuilder
|
|
8
|
+
import android.text.Spanned
|
|
9
|
+
import android.text.TextPaint
|
|
10
|
+
import android.text.style.LineBackgroundSpan
|
|
11
|
+
import android.text.style.ReplacementSpan
|
|
12
|
+
import com.swmansion.enriched.markdown.styles.LinkChipStyle
|
|
13
|
+
import kotlin.math.max
|
|
14
|
+
import kotlin.math.min
|
|
15
|
+
import kotlin.math.roundToInt
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reserves a link's `paddingHorizontal` at one outer edge. It covers one [LinkSpacers.CHAR]
|
|
19
|
+
* inserted by `LinkRenderer`: U+00A0 is line-break class GL, so a break is allowed before it
|
|
20
|
+
* after a space but never after it, and never before it after a non-space — the spacer always
|
|
21
|
+
* travels with the link's first (or last) glyph, never orphaned at a wrap.
|
|
22
|
+
*
|
|
23
|
+
* Layout-only: every text exit point strips spacers by this span ([LinkSpacers.strip]).
|
|
24
|
+
*/
|
|
25
|
+
class LinkSpacerSpan(
|
|
26
|
+
private val width: Float,
|
|
27
|
+
) : ReplacementSpan() {
|
|
28
|
+
override fun getSize(
|
|
29
|
+
paint: Paint,
|
|
30
|
+
text: CharSequence?,
|
|
31
|
+
start: Int,
|
|
32
|
+
end: Int,
|
|
33
|
+
fm: Paint.FontMetricsInt?,
|
|
34
|
+
): Int {
|
|
35
|
+
// Keep the line metrics of the surrounding text.
|
|
36
|
+
if (fm != null) paint.getFontMetricsInt(fm)
|
|
37
|
+
return width.roundToInt()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun draw(
|
|
41
|
+
canvas: Canvas,
|
|
42
|
+
text: CharSequence?,
|
|
43
|
+
start: Int,
|
|
44
|
+
end: Int,
|
|
45
|
+
x: Float,
|
|
46
|
+
top: Int,
|
|
47
|
+
y: Int,
|
|
48
|
+
bottom: Int,
|
|
49
|
+
paint: Paint,
|
|
50
|
+
) = Unit
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
object LinkSpacers {
|
|
54
|
+
const val CHAR = '\u00A0'
|
|
55
|
+
|
|
56
|
+
/** [text] without link spacers (returns [text] itself when it has none). */
|
|
57
|
+
fun strip(text: CharSequence): CharSequence {
|
|
58
|
+
if (text !is Spanned) return text
|
|
59
|
+
val spacers = text.getSpans(0, text.length, LinkSpacerSpan::class.java)
|
|
60
|
+
if (spacers.isEmpty()) return text
|
|
61
|
+
val builder = SpannableStringBuilder(text)
|
|
62
|
+
spacers
|
|
63
|
+
.map { builder.getSpanStart(it) to builder.getSpanEnd(it) }
|
|
64
|
+
.filter { (start, end) -> start in 0 until end }
|
|
65
|
+
.sortedByDescending { it.first }
|
|
66
|
+
.forEach { (start, end) -> builder.delete(start, end) }
|
|
67
|
+
return builder
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when [start, end) lies inside a spacer (a segment exporters must skip). */
|
|
71
|
+
fun isSpacer(
|
|
72
|
+
text: Spanned,
|
|
73
|
+
start: Int,
|
|
74
|
+
end: Int,
|
|
75
|
+
): Boolean = end > start && text.getSpans(start, end, LinkSpacerSpan::class.java).isNotEmpty()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Paints a link chip (background + border) behind the link glyphs. One instance per link,
|
|
80
|
+
* spanning the link including its spacers, so the chip already covers `paddingHorizontal`.
|
|
81
|
+
* Vertically the chip spans the link font's ascent..descent ± `paddingVertical`; the vertical
|
|
82
|
+
* padding only bleeds into the surrounding line spacing and never changes the line box.
|
|
83
|
+
*/
|
|
84
|
+
class LinkChipBackgroundSpan(
|
|
85
|
+
val chip: LinkChipStyle,
|
|
86
|
+
) : LineBackgroundSpan {
|
|
87
|
+
private val rect = RectF()
|
|
88
|
+
private val path = Path()
|
|
89
|
+
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
|
|
90
|
+
private val borderPaint =
|
|
91
|
+
Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
92
|
+
style = Paint.Style.STROKE
|
|
93
|
+
strokeJoin = Paint.Join.ROUND
|
|
94
|
+
strokeCap = Paint.Cap.ROUND
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun drawBackground(
|
|
98
|
+
canvas: Canvas,
|
|
99
|
+
p: Paint,
|
|
100
|
+
left: Int,
|
|
101
|
+
right: Int,
|
|
102
|
+
top: Int,
|
|
103
|
+
baseline: Int,
|
|
104
|
+
bottom: Int,
|
|
105
|
+
text: CharSequence,
|
|
106
|
+
start: Int,
|
|
107
|
+
end: Int,
|
|
108
|
+
lineNum: Int,
|
|
109
|
+
) {
|
|
110
|
+
if (!chip.isVisible || text !is Spanned) return
|
|
111
|
+
|
|
112
|
+
val spanStart = text.getSpanStart(this)
|
|
113
|
+
val spanEnd = text.getSpanEnd(this)
|
|
114
|
+
if (spanStart !in 0 until spanEnd || spanEnd <= start || spanStart >= end) return
|
|
115
|
+
|
|
116
|
+
val isFirst = spanStart >= start
|
|
117
|
+
val isLast = spanEnd <= end
|
|
118
|
+
|
|
119
|
+
val leadingMargin = InlineChipPainter.leadingMarginAt(text, start)
|
|
120
|
+
val fragmentStart = max(spanStart, start)
|
|
121
|
+
val fragmentEnd = trimTrailingWhitespace(text, fragmentStart, min(spanEnd, end))
|
|
122
|
+
val startX = InlineChipPainter.horizontalOffset(text, start, end, fragmentStart, p, leadingMargin) + left
|
|
123
|
+
val endX = InlineChipPainter.horizontalOffset(text, start, end, fragmentEnd, p, leadingMargin) + left
|
|
124
|
+
|
|
125
|
+
val (ascent, descent) = linkFontMetrics(text, spanStart, spanEnd, p)
|
|
126
|
+
rect.set(
|
|
127
|
+
min(startX, endX),
|
|
128
|
+
baseline + ascent - chip.paddingVertical,
|
|
129
|
+
max(startX, endX),
|
|
130
|
+
baseline + descent + chip.paddingVertical,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
backgroundPaint.color = chip.backgroundColor
|
|
134
|
+
borderPaint.color = chip.borderColor
|
|
135
|
+
borderPaint.strokeWidth = chip.borderWidth
|
|
136
|
+
InlineChipPainter.draw(
|
|
137
|
+
canvas,
|
|
138
|
+
rect,
|
|
139
|
+
path,
|
|
140
|
+
chip.borderRadius,
|
|
141
|
+
backgroundPaint.takeIf { android.graphics.Color.alpha(chip.backgroundColor) > 0 },
|
|
142
|
+
borderPaint.takeIf { android.graphics.Color.alpha(chip.borderColor) > 0 && chip.borderWidth > 0f },
|
|
143
|
+
isFirst,
|
|
144
|
+
isLast,
|
|
145
|
+
strokeInset = true,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
companion object {
|
|
150
|
+
/** A wrapped fragment ends before the whitespace the line break hangs. */
|
|
151
|
+
fun trimTrailingWhitespace(
|
|
152
|
+
text: CharSequence,
|
|
153
|
+
start: Int,
|
|
154
|
+
end: Int,
|
|
155
|
+
): Int {
|
|
156
|
+
var index = end
|
|
157
|
+
while (index > start && text[index - 1] != LinkSpacers.CHAR && text[index - 1].isWhitespace()) index--
|
|
158
|
+
return index
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Ascent/descent of the link's own font: [LinkSpan] sets its size and typeface at draw
|
|
163
|
+
* time only, so the base paint [p] alone would report the paragraph's metrics.
|
|
164
|
+
*/
|
|
165
|
+
fun linkFontMetrics(
|
|
166
|
+
text: Spanned,
|
|
167
|
+
spanStart: Int,
|
|
168
|
+
spanEnd: Int,
|
|
169
|
+
p: Paint,
|
|
170
|
+
): Pair<Float, Float> {
|
|
171
|
+
val paint = TextPaint(p)
|
|
172
|
+
text.getSpans(spanStart, spanEnd, LinkSpan::class.java).firstOrNull()?.updateDrawState(paint)
|
|
173
|
+
val metrics = paint.fontMetrics
|
|
174
|
+
return metrics.ascent to metrics.descent
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package com.swmansion.enriched.markdown.spans
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import android.graphics.Color
|
|
5
4
|
import android.text.TextPaint
|
|
6
5
|
import android.text.style.ClickableSpan
|
|
7
6
|
import android.view.View
|
|
@@ -57,10 +56,6 @@ class LinkSpan(
|
|
|
57
56
|
|
|
58
57
|
textPaint.color = variant?.color ?: styleCache.linkColor
|
|
59
58
|
textPaint.isUnderlineText = variant?.underline ?: styleCache.linkUnderline
|
|
60
|
-
|
|
61
|
-
val backgroundColor = variant?.backgroundColor ?: styleCache.linkBackgroundColor
|
|
62
|
-
if (Color.alpha(backgroundColor) > 0) {
|
|
63
|
-
textPaint.bgColor = backgroundColor
|
|
64
|
-
}
|
|
59
|
+
// The background is painted as a chip by LinkChipBackgroundSpan.
|
|
65
60
|
}
|
|
66
61
|
}
|
|
@@ -7,6 +7,7 @@ data class LinkStyle(
|
|
|
7
7
|
val color: Int,
|
|
8
8
|
val underline: Boolean,
|
|
9
9
|
val backgroundColor: Int,
|
|
10
|
+
val chip: LinkChipStyle,
|
|
10
11
|
) {
|
|
11
12
|
companion object {
|
|
12
13
|
fun fromReadableMap(
|
|
@@ -17,7 +18,41 @@ data class LinkStyle(
|
|
|
17
18
|
val color = parser.parseColor(map, "color")
|
|
18
19
|
val underline = map.getBoolean("underline")
|
|
19
20
|
val backgroundColor = parser.parseColor(map, "backgroundColor")
|
|
20
|
-
return LinkStyle(fontFamily, color, underline, backgroundColor)
|
|
21
|
+
return LinkStyle(fontFamily, color, underline, backgroundColor, LinkChipStyle.fromReadableMap(map, parser, backgroundColor))
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Chip ("box") properties of a link, in pixels. Never inherited from the base link
|
|
28
|
+
* style by variants — the JS normalizer fills missing values with 0 / transparent.
|
|
29
|
+
*/
|
|
30
|
+
data class LinkChipStyle(
|
|
31
|
+
val backgroundColor: Int,
|
|
32
|
+
val borderColor: Int,
|
|
33
|
+
val borderRadius: Float,
|
|
34
|
+
val borderWidth: Float,
|
|
35
|
+
val paddingHorizontal: Float,
|
|
36
|
+
val paddingVertical: Float,
|
|
37
|
+
) {
|
|
38
|
+
val isVisible: Boolean
|
|
39
|
+
get() =
|
|
40
|
+
android.graphics.Color.alpha(backgroundColor) > 0 ||
|
|
41
|
+
(android.graphics.Color.alpha(borderColor) > 0 && borderWidth > 0f)
|
|
42
|
+
|
|
43
|
+
companion object {
|
|
44
|
+
fun fromReadableMap(
|
|
45
|
+
map: ReadableMap,
|
|
46
|
+
parser: StyleParser,
|
|
47
|
+
backgroundColor: Int,
|
|
48
|
+
): LinkChipStyle =
|
|
49
|
+
LinkChipStyle(
|
|
50
|
+
backgroundColor = backgroundColor,
|
|
51
|
+
borderColor = parser.parseOptionalColor(map, "borderColor") ?: 0,
|
|
52
|
+
borderRadius = parser.toPixelFromDIP(parser.parseOptionalDouble(map, "borderRadius").toFloat()),
|
|
53
|
+
borderWidth = parser.toPixelFromDIP(parser.parseOptionalDouble(map, "borderWidth").toFloat()),
|
|
54
|
+
paddingHorizontal = parser.toPixelFromDIP(parser.parseOptionalDouble(map, "paddingHorizontal").toFloat()),
|
|
55
|
+
paddingVertical = parser.toPixelFromDIP(parser.parseOptionalDouble(map, "paddingVertical").toFloat()),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -13,17 +13,21 @@ data class LinkVariantEntry(
|
|
|
13
13
|
val color: Int,
|
|
14
14
|
val underline: Boolean,
|
|
15
15
|
val backgroundColor: Int,
|
|
16
|
+
val chip: LinkChipStyle,
|
|
16
17
|
) {
|
|
17
18
|
companion object {
|
|
18
19
|
fun fromReadableMap(
|
|
19
20
|
map: ReadableMap,
|
|
20
21
|
parser: StyleParser,
|
|
21
|
-
): LinkVariantEntry
|
|
22
|
-
|
|
22
|
+
): LinkVariantEntry {
|
|
23
|
+
val backgroundColor = parser.parseColor(map, "backgroundColor")
|
|
24
|
+
return LinkVariantEntry(
|
|
23
25
|
pattern = map.getString("pattern") ?: "",
|
|
24
26
|
color = parser.parseColor(map, "color"),
|
|
25
27
|
underline = parser.parseBoolean(map, "underline"),
|
|
26
|
-
backgroundColor =
|
|
28
|
+
backgroundColor = backgroundColor,
|
|
29
|
+
chip = LinkChipStyle.fromReadableMap(map, parser, backgroundColor),
|
|
27
30
|
)
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
}
|
|
@@ -7,10 +7,13 @@ import com.facebook.react.uimanager.UIManagerHelper
|
|
|
7
7
|
import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
|
|
8
8
|
import com.swmansion.enriched.markdown.events.ContextMenuItemPressEvent
|
|
9
9
|
import com.swmansion.enriched.markdown.events.CopyPressEvent
|
|
10
|
+
import com.swmansion.enriched.markdown.events.CopyRequestEvent
|
|
11
|
+
import com.swmansion.enriched.markdown.events.LinkLayoutEvent
|
|
10
12
|
import com.swmansion.enriched.markdown.events.LinkLongPressEvent
|
|
11
13
|
import com.swmansion.enriched.markdown.events.LinkPressEvent
|
|
12
14
|
import com.swmansion.enriched.markdown.events.TaskListItemPressEvent
|
|
13
15
|
import com.swmansion.enriched.markdown.parser.Md4cFlags
|
|
16
|
+
import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutFragment
|
|
14
17
|
import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
|
|
15
18
|
|
|
16
19
|
fun markdownEventTypeConstants(): MutableMap<String, Any> {
|
|
@@ -22,11 +25,25 @@ fun markdownEventTypeConstants(): MutableMap<String, Any> {
|
|
|
22
25
|
mapOf("registrationName" to TaskListItemPressEvent.EVENT_NAME)
|
|
23
26
|
map[CopyPressEvent.EVENT_NAME] =
|
|
24
27
|
mapOf("registrationName" to CopyPressEvent.EVENT_NAME)
|
|
28
|
+
map[CopyRequestEvent.EVENT_NAME] =
|
|
29
|
+
mapOf("registrationName" to CopyRequestEvent.EVENT_NAME)
|
|
25
30
|
map[ContextMenuItemPressEvent.EVENT_NAME] =
|
|
26
31
|
mapOf("registrationName" to ContextMenuItemPressEvent.EVENT_NAME)
|
|
32
|
+
map[LinkLayoutEvent.EVENT_NAME] =
|
|
33
|
+
mapOf("registrationName" to LinkLayoutEvent.EVENT_NAME)
|
|
27
34
|
return map
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
fun emitLinkLayout(
|
|
38
|
+
view: View,
|
|
39
|
+
fragments: List<LinkLayoutFragment>,
|
|
40
|
+
) {
|
|
41
|
+
val context = view.context as com.facebook.react.bridge.ReactContext
|
|
42
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
43
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
44
|
+
eventDispatcher?.dispatchEvent(LinkLayoutEvent(surfaceId, view.id, fragments))
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
fun emitLinkPress(
|
|
31
48
|
view: View,
|
|
32
49
|
url: String,
|
|
@@ -74,6 +91,21 @@ fun emitCopyPress(
|
|
|
74
91
|
)
|
|
75
92
|
}
|
|
76
93
|
|
|
94
|
+
fun emitCopyRequest(
|
|
95
|
+
view: View,
|
|
96
|
+
source: String,
|
|
97
|
+
markdown: String,
|
|
98
|
+
text: String,
|
|
99
|
+
whole: Boolean,
|
|
100
|
+
) {
|
|
101
|
+
val context = view.context as com.facebook.react.bridge.ReactContext
|
|
102
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
103
|
+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
104
|
+
eventDispatcher?.dispatchEvent(
|
|
105
|
+
CopyRequestEvent(surfaceId, view.id, source, markdown, text, whole),
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
fun emitContextMenuItemPress(
|
|
78
110
|
view: View,
|
|
79
111
|
itemText: String,
|
|
@@ -132,6 +164,7 @@ fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig {
|
|
|
132
164
|
copyFormatMarkdown = value.getBooleanOrDefault("copyFormatMarkdown", false),
|
|
133
165
|
systemItemsNone = value.getBooleanOrDefault("systemItemsNone", false),
|
|
134
166
|
codeBlockCopyButton = value.getBooleanOrDefault("codeBlockCopyButton", true),
|
|
167
|
+
interceptCopy = value.getBooleanOrDefault("interceptCopy", false),
|
|
135
168
|
)
|
|
136
169
|
}
|
|
137
170
|
|
package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt
CHANGED
|
@@ -12,6 +12,7 @@ import com.swmansion.enriched.markdown.spans.EmphasisSpan
|
|
|
12
12
|
import com.swmansion.enriched.markdown.spans.HeadingSpan
|
|
13
13
|
import com.swmansion.enriched.markdown.spans.HighlightSpan
|
|
14
14
|
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
15
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
15
16
|
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
16
17
|
import com.swmansion.enriched.markdown.spans.OrderedListSpan
|
|
17
18
|
import com.swmansion.enriched.markdown.spans.StrikethroughSpan
|
|
@@ -676,7 +677,7 @@ object HTMLGenerator {
|
|
|
676
677
|
val segmentEnd = minOf(text.nextSpanTransition(i, end, Any::class.java), end)
|
|
677
678
|
val segmentText = text.subSequence(i, segmentEnd).toString()
|
|
678
679
|
|
|
679
|
-
if (segmentText.isNotEmpty() && segmentText != "\n") {
|
|
680
|
+
if (segmentText.isNotEmpty() && segmentText != "\n" && !LinkSpacers.isSpacer(text, i, segmentEnd)) {
|
|
680
681
|
appendStyledSegment(html, text, i, segmentEnd, segmentText, styles, isCodeBlock)
|
|
681
682
|
}
|
|
682
683
|
i = segmentEnd
|
|
@@ -12,6 +12,7 @@ import com.swmansion.enriched.markdown.spans.EmphasisSpan
|
|
|
12
12
|
import com.swmansion.enriched.markdown.spans.HeadingSpan
|
|
13
13
|
import com.swmansion.enriched.markdown.spans.HighlightSpan
|
|
14
14
|
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
15
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
15
16
|
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
16
17
|
import com.swmansion.enriched.markdown.spans.OrderedListSpan
|
|
17
18
|
import com.swmansion.enriched.markdown.spans.StrikethroughSpan
|
|
@@ -55,6 +56,11 @@ object MarkdownExtractor {
|
|
|
55
56
|
var i = start
|
|
56
57
|
while (i < end) {
|
|
57
58
|
val nextTransition = spannable.nextSpanTransition(i, end, Any::class.java)
|
|
59
|
+
// Link spacers only reserve layout space for chip padding.
|
|
60
|
+
if (LinkSpacers.isSpacer(spannable, i, nextTransition)) {
|
|
61
|
+
i = nextTransition
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
58
64
|
val segmentText = spannable.subSequence(i, nextTransition).toString()
|
|
59
65
|
|
|
60
66
|
val handled =
|
package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils.text.view
|
|
2
|
+
|
|
3
|
+
import android.text.Layout
|
|
4
|
+
import android.text.Spanned
|
|
5
|
+
import android.widget.TextView
|
|
6
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
7
|
+
import com.swmansion.enriched.markdown.spans.LinkChipBackgroundSpan
|
|
8
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
10
|
+
import kotlin.math.max
|
|
11
|
+
import kotlin.math.min
|
|
12
|
+
|
|
13
|
+
/** One laid-out line of a link, in dp relative to the reporting view. Covers the drawn chip. */
|
|
14
|
+
data class LinkLayoutFragment(
|
|
15
|
+
val linkIndex: Int,
|
|
16
|
+
val url: String,
|
|
17
|
+
val text: String,
|
|
18
|
+
val x: Float,
|
|
19
|
+
val y: Float,
|
|
20
|
+
val width: Float,
|
|
21
|
+
val height: Float,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
object LinkLayoutCollector {
|
|
25
|
+
/**
|
|
26
|
+
* Collects the chip rect of every link fragment laid out in [textView], for `onLinkLayout`.
|
|
27
|
+
* Rects are offset by the text view's own padding plus [offsetXPx]/[offsetYPx] (a segment's
|
|
28
|
+
* position inside its container), then converted to dp. Links are numbered in document order
|
|
29
|
+
* from [indexBase]; returns the fragments and the number of links found.
|
|
30
|
+
*/
|
|
31
|
+
fun collect(
|
|
32
|
+
textView: TextView,
|
|
33
|
+
offsetXPx: Int = 0,
|
|
34
|
+
offsetYPx: Int = 0,
|
|
35
|
+
indexBase: Int = 0,
|
|
36
|
+
): Pair<List<LinkLayoutFragment>, Int> {
|
|
37
|
+
val layout = textView.layout ?: return emptyList<LinkLayoutFragment>() to 0
|
|
38
|
+
val text = textView.text as? Spanned ?: return emptyList<LinkLayoutFragment>() to 0
|
|
39
|
+
|
|
40
|
+
// LinkRenderer sets one LinkChipBackgroundSpan per link, spacers included.
|
|
41
|
+
val chips =
|
|
42
|
+
text
|
|
43
|
+
.getSpans(0, text.length, LinkChipBackgroundSpan::class.java)
|
|
44
|
+
.sortedBy { text.getSpanStart(it) }
|
|
45
|
+
val originX = offsetXPx + textView.totalPaddingLeft
|
|
46
|
+
val originY = offsetYPx + textView.totalPaddingTop
|
|
47
|
+
|
|
48
|
+
val fragments = mutableListOf<LinkLayoutFragment>()
|
|
49
|
+
chips.forEachIndexed { i, chipSpan ->
|
|
50
|
+
val start = text.getSpanStart(chipSpan)
|
|
51
|
+
val end = text.getSpanEnd(chipSpan)
|
|
52
|
+
if (start !in 0 until end) return@forEachIndexed
|
|
53
|
+
|
|
54
|
+
val url = text.getSpans(start, end, LinkSpan::class.java).firstOrNull()?.url ?: ""
|
|
55
|
+
val linkText = LinkSpacers.strip(text.subSequence(start, end)).toString()
|
|
56
|
+
val (ascent, descent) = LinkChipBackgroundSpan.linkFontMetrics(text, start, end, textView.paint)
|
|
57
|
+
val paddingVertical = chipSpan.chip.paddingVertical
|
|
58
|
+
|
|
59
|
+
for (line in layout.getLineForOffset(start)..layout.getLineForOffset(end - 1)) {
|
|
60
|
+
val fragmentStart = max(start, layout.getLineStart(line))
|
|
61
|
+
val fragmentEnd =
|
|
62
|
+
LinkChipBackgroundSpan.trimTrailingWhitespace(text, fragmentStart, min(end, layout.getLineEnd(line)))
|
|
63
|
+
if (fragmentEnd <= fragmentStart) continue
|
|
64
|
+
|
|
65
|
+
val left = layout.getPrimaryHorizontal(fragmentStart)
|
|
66
|
+
val right = lineEndHorizontal(layout, line, fragmentEnd)
|
|
67
|
+
val baseline = layout.getLineBaseline(line).toFloat()
|
|
68
|
+
val top = baseline + ascent - paddingVertical
|
|
69
|
+
val bottom = baseline + descent + paddingVertical
|
|
70
|
+
|
|
71
|
+
fragments +=
|
|
72
|
+
LinkLayoutFragment(
|
|
73
|
+
linkIndex = indexBase + i,
|
|
74
|
+
url = url,
|
|
75
|
+
text = linkText,
|
|
76
|
+
x = PixelUtil.toDIPFromPixel(min(left, right) + originX),
|
|
77
|
+
y = PixelUtil.toDIPFromPixel(top + originY),
|
|
78
|
+
width = PixelUtil.toDIPFromPixel(kotlin.math.abs(right - left)),
|
|
79
|
+
height = PixelUtil.toDIPFromPixel(bottom - top),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return fragments to chips.size
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* x of [offset] on [line]. At a line's end offset `getPrimaryHorizontal` reports the start
|
|
88
|
+
* of the next line (the same offset), so a fragment cut mid-word uses the line's right edge.
|
|
89
|
+
*/
|
|
90
|
+
private fun lineEndHorizontal(
|
|
91
|
+
layout: Layout,
|
|
92
|
+
line: Int,
|
|
93
|
+
offset: Int,
|
|
94
|
+
): Float =
|
|
95
|
+
if (offset >= layout.getLineEnd(line) && line < layout.lineCount - 1) {
|
|
96
|
+
layout.getLineRight(line)
|
|
97
|
+
} else {
|
|
98
|
+
layout.getPrimaryHorizontal(offset)
|
|
99
|
+
}
|
|
100
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt
CHANGED
|
@@ -12,6 +12,7 @@ import android.widget.TextView
|
|
|
12
12
|
import com.swmansion.enriched.markdown.EnrichedMarkdown
|
|
13
13
|
import com.swmansion.enriched.markdown.EnrichedMarkdownText
|
|
14
14
|
import com.swmansion.enriched.markdown.spans.ImageSpan
|
|
15
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
15
16
|
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
16
17
|
import com.swmansion.enriched.markdown.utils.common.layout.isLayoutRTL
|
|
17
18
|
import com.swmansion.enriched.markdown.utils.text.conversion.HTMLGenerator
|
|
@@ -38,6 +39,8 @@ data class SelectionMenuConfig(
|
|
|
38
39
|
// Drop every system-provided item (Select all, Share, text assist, PROCESS_TEXT…).
|
|
39
40
|
val systemItemsNone: Boolean = false,
|
|
40
41
|
val codeBlockCopyButton: Boolean = true,
|
|
42
|
+
// Copy hands the selection to onCopyRequest instead of writing the clipboard.
|
|
43
|
+
val interceptCopy: Boolean = false,
|
|
41
44
|
)
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -69,6 +72,9 @@ fun createSelectionActionModeCallback(
|
|
|
69
72
|
onActionModeChanged: (mode: ActionMode?) -> Unit = {},
|
|
70
73
|
onCustomItemPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit =
|
|
71
74
|
{ _, _, _, _ -> },
|
|
75
|
+
// Receives the intercepted Copy when `interceptCopy` is set. `fullSelection`
|
|
76
|
+
// is whether the selection spans the whole text view.
|
|
77
|
+
onCopyRequest: (markdown: String, text: String, fullSelection: Boolean) -> Unit = { _, _, _ -> },
|
|
72
78
|
): ActionMode.Callback =
|
|
73
79
|
object : ActionMode.Callback {
|
|
74
80
|
override fun onCreateActionMode(
|
|
@@ -157,7 +163,10 @@ fun createSelectionActionModeCallback(
|
|
|
157
163
|
|
|
158
164
|
when (itemId) {
|
|
159
165
|
android.R.id.copy -> {
|
|
160
|
-
|
|
166
|
+
val config = getSelectionMenuConfig()
|
|
167
|
+
if (config.interceptCopy) {
|
|
168
|
+
textView.requestCopy(onCopyRequest)
|
|
169
|
+
} else if (!config.copyFormatMarkdown || !textView.copyMarkdownToClipboard()) {
|
|
161
170
|
textView.copyWithHTML()
|
|
162
171
|
}
|
|
163
172
|
mode?.finish()
|
|
@@ -203,7 +212,8 @@ private fun TextView.copyWithHTML() {
|
|
|
203
212
|
if (start < 0 || end < 0 || start >= end) return
|
|
204
213
|
|
|
205
214
|
val spannable = text as? Spannable ?: return
|
|
206
|
-
|
|
215
|
+
// Link spacers only reserve layout space; keep them out of every copied flavor.
|
|
216
|
+
val selectedText = LinkSpacers.strip(spannable.subSequence(start, end))
|
|
207
217
|
val plainText = selectedText.toString()
|
|
208
218
|
|
|
209
219
|
val styleConfig =
|
|
@@ -229,6 +239,18 @@ private fun TextView.copyWithHTML() {
|
|
|
229
239
|
}
|
|
230
240
|
}
|
|
231
241
|
|
|
242
|
+
/** Hands the selection to [onCopyRequest] without touching the clipboard. */
|
|
243
|
+
private fun TextView.requestCopy(onCopyRequest: (markdown: String, text: String, fullSelection: Boolean) -> Unit) {
|
|
244
|
+
val start = selectionStart
|
|
245
|
+
val end = selectionEnd
|
|
246
|
+
if (start < 0 || end < 0 || start >= end) return
|
|
247
|
+
|
|
248
|
+
val selectedText = LinkSpacers.strip(text.subSequence(start, end)).toString()
|
|
249
|
+
val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: selectedText
|
|
250
|
+
// Same threshold MarkdownExtractor uses to return the original markdown.
|
|
251
|
+
onCopyRequest(markdown, selectedText, start == 0 && end >= text.length - 1)
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
/** Returns false when the selection has no Markdown to copy. */
|
|
233
255
|
private fun TextView.copyMarkdownToClipboard(): Boolean {
|
|
234
256
|
val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: return false
|