react-native-enriched-markdown-scaffold 1.0.2-scaffold.3 → 1.0.2-scaffold.5
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 +15 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +15 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +122 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +100 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +20 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +124 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +145 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +22 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +85 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +22 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +42 -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 +28 -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 +4 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +230 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +11 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +22 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +8 -1
- package/docs/API_REFERENCE.md +111 -0
- package/docs/MENTIONS.md +8 -1
- package/docs/STYLES.md +27 -0
- package/docs/TEXT.md +2 -0
- package/ios/EnrichedMarkdown.mm +221 -1
- package/ios/EnrichedMarkdownText.mm +142 -1
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +122 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +100 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +20 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +124 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
- 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/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/ENRMSelectionChange.h +96 -0
- package/ios/utils/ENRMSelectionChange.m +204 -0
- package/ios/utils/EditMenuUtils+macOS.m +4 -1
- package/ios/utils/EditMenuUtils.m +4 -1
- package/ios/utils/HTMLGenerator.m +2 -1
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -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 +3 -0
- package/ios/views/ENRMCodeBlockContainerView.m +3 -0
- package/ios/views/ENRMMathContainerView.h +3 -0
- package/ios/views/ENRMMathContainerView.m +3 -0
- package/ios/views/TableContainerView.h +6 -0
- package/ios/views/TableContainerView.m +24 -0
- package/lib/module/EnrichedMarkdownNativeComponent.ts +101 -1
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +101 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/jest/index.js +13 -0
- package/lib/module/jest/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +138 -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 +93 -2
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +93 -2
- 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/jest/index.d.ts +1 -1
- package/lib/typescript/src/jest/index.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 +76 -2
- 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 +36 -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 +101 -1
- package/src/EnrichedMarkdownTextNativeComponent.ts +101 -1
- package/src/index.tsx +5 -0
- package/src/index.web.tsx +5 -1
- package/src/jest/index.tsx +21 -1
- package/src/native/EnrichedMarkdownText.tsx +181 -8
- 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 +85 -1
- package/src/types/MarkdownTextProps.web.ts +43 -1
- package/src/types/events.ts +38 -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
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
|
|
@@ -211,7 +212,8 @@ private fun TextView.copyWithHTML() {
|
|
|
211
212
|
if (start < 0 || end < 0 || start >= end) return
|
|
212
213
|
|
|
213
214
|
val spannable = text as? Spannable ?: return
|
|
214
|
-
|
|
215
|
+
// Link spacers only reserve layout space; keep them out of every copied flavor.
|
|
216
|
+
val selectedText = LinkSpacers.strip(spannable.subSequence(start, end))
|
|
215
217
|
val plainText = selectedText.toString()
|
|
216
218
|
|
|
217
219
|
val styleConfig =
|
|
@@ -243,7 +245,7 @@ private fun TextView.requestCopy(onCopyRequest: (markdown: String, text: String,
|
|
|
243
245
|
val end = selectionEnd
|
|
244
246
|
if (start < 0 || end < 0 || start >= end) return
|
|
245
247
|
|
|
246
|
-
val selectedText = text.subSequence(start, end).toString()
|
|
248
|
+
val selectedText = LinkSpacers.strip(text.subSequence(start, end)).toString()
|
|
247
249
|
val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: selectedText
|
|
248
250
|
// Same threshold MarkdownExtractor uses to return the original markdown.
|
|
249
251
|
onCopyRequest(markdown, selectedText, start == 0 && end >= text.length - 1)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils.text.view
|
|
2
|
+
|
|
3
|
+
import android.text.Layout
|
|
4
|
+
import android.text.Selection
|
|
5
|
+
import android.text.Spannable
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.widget.TextView
|
|
8
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
9
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
10
|
+
import com.swmansion.enriched.markdown.utils.text.conversion.MarkdownExtractor
|
|
11
|
+
import kotlin.math.max
|
|
12
|
+
import kotlin.math.min
|
|
13
|
+
|
|
14
|
+
/** A rect in dp, relative to the reporting view. */
|
|
15
|
+
data class SelectionRect(
|
|
16
|
+
val x: Float,
|
|
17
|
+
val y: Float,
|
|
18
|
+
val width: Float,
|
|
19
|
+
val height: Float,
|
|
20
|
+
) {
|
|
21
|
+
companion object {
|
|
22
|
+
val ZERO = SelectionRect(0f, 0f, 0f, 0f)
|
|
23
|
+
|
|
24
|
+
/** Converts a px rect (already in the reporting view's coordinates) to dp. */
|
|
25
|
+
fun fromPx(
|
|
26
|
+
left: Float,
|
|
27
|
+
top: Float,
|
|
28
|
+
right: Float,
|
|
29
|
+
bottom: Float,
|
|
30
|
+
) = SelectionRect(
|
|
31
|
+
PixelUtil.toDIPFromPixel(min(left, right)),
|
|
32
|
+
PixelUtil.toDIPFromPixel(min(top, bottom)),
|
|
33
|
+
PixelUtil.toDIPFromPixel(kotlin.math.abs(right - left)),
|
|
34
|
+
PixelUtil.toDIPFromPixel(kotlin.math.abs(bottom - top)),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Payload of `onSelectionChange`. Compared structurally for deduplication. */
|
|
40
|
+
data class SelectionSnapshot(
|
|
41
|
+
val active: Boolean,
|
|
42
|
+
val source: String,
|
|
43
|
+
val text: String,
|
|
44
|
+
val markdown: String,
|
|
45
|
+
val whole: Boolean,
|
|
46
|
+
val rect: SelectionRect,
|
|
47
|
+
val startRect: SelectionRect,
|
|
48
|
+
val endRect: SelectionRect,
|
|
49
|
+
) {
|
|
50
|
+
companion object {
|
|
51
|
+
val INACTIVE =
|
|
52
|
+
SelectionSnapshot(false, "selection", "", "", false, SelectionRect.ZERO, SelectionRect.ZERO, SelectionRect.ZERO)
|
|
53
|
+
|
|
54
|
+
/** A long-pressed block: `whole`, and every rect is the block's frame. */
|
|
55
|
+
fun block(
|
|
56
|
+
source: String,
|
|
57
|
+
markdown: String,
|
|
58
|
+
text: String,
|
|
59
|
+
rect: SelectionRect,
|
|
60
|
+
) = SelectionSnapshot(true, source, text, markdown, true, rect, rect, rect)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A text view's current selection, before the owner applies its `whole` rule. */
|
|
65
|
+
data class TextSelection(
|
|
66
|
+
val text: String,
|
|
67
|
+
val markdown: String,
|
|
68
|
+
// Same threshold MarkdownExtractor uses to return the original markdown.
|
|
69
|
+
val fullSelection: Boolean,
|
|
70
|
+
val rect: SelectionRect,
|
|
71
|
+
val startRect: SelectionRect,
|
|
72
|
+
val endRect: SelectionRect,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reads the selection of this text view with its rects offset by [offsetXPx]/[offsetYPx]
|
|
77
|
+
* (a segment's position inside its container). `rect` spans the full content width when the
|
|
78
|
+
* selection covers several lines; `startRect`/`endRect` cover the selected part of the first
|
|
79
|
+
* and last line. Returns null when nothing is selected.
|
|
80
|
+
*/
|
|
81
|
+
fun TextView.currentTextSelection(
|
|
82
|
+
offsetXPx: Int = 0,
|
|
83
|
+
offsetYPx: Int = 0,
|
|
84
|
+
): TextSelection? {
|
|
85
|
+
val start = selectionStart
|
|
86
|
+
val end = selectionEnd
|
|
87
|
+
if (start < 0 || end <= start) return null
|
|
88
|
+
val layout = layout ?: return null
|
|
89
|
+
val content = text ?: return null
|
|
90
|
+
|
|
91
|
+
val selectedText = LinkSpacers.strip(content.subSequence(start, end)).toString()
|
|
92
|
+
val markdown = MarkdownExtractor.getMarkdownForSelection(this) ?: selectedText
|
|
93
|
+
|
|
94
|
+
val originX = (offsetXPx + totalPaddingLeft - scrollX).toFloat()
|
|
95
|
+
val originY = (offsetYPx + totalPaddingTop - scrollY).toFloat()
|
|
96
|
+
|
|
97
|
+
val startLine = layout.getLineForOffset(start)
|
|
98
|
+
// An end at a line break belongs to the line it closes, not the next one.
|
|
99
|
+
val endLine = layout.getLineForOffset(max(start, end - 1))
|
|
100
|
+
val startX = layout.getPrimaryHorizontal(start)
|
|
101
|
+
val endX = lineEndHorizontal(layout, endLine, end)
|
|
102
|
+
|
|
103
|
+
fun lineRect(
|
|
104
|
+
line: Int,
|
|
105
|
+
left: Float,
|
|
106
|
+
right: Float,
|
|
107
|
+
) = SelectionRect.fromPx(
|
|
108
|
+
left + originX,
|
|
109
|
+
layout.getLineTop(line) + originY,
|
|
110
|
+
right + originX,
|
|
111
|
+
layout.getLineBottom(line) + originY,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
val startRect: SelectionRect
|
|
115
|
+
val endRect: SelectionRect
|
|
116
|
+
val rect: SelectionRect
|
|
117
|
+
if (startLine == endLine) {
|
|
118
|
+
startRect = lineRect(startLine, startX, endX)
|
|
119
|
+
endRect = startRect
|
|
120
|
+
rect = startRect
|
|
121
|
+
} else {
|
|
122
|
+
startRect = lineRect(startLine, startX, layout.getLineRight(startLine))
|
|
123
|
+
endRect = lineRect(endLine, layout.getLineLeft(endLine), endX)
|
|
124
|
+
rect =
|
|
125
|
+
SelectionRect.fromPx(
|
|
126
|
+
originX,
|
|
127
|
+
layout.getLineTop(startLine) + originY,
|
|
128
|
+
layout.width + originX,
|
|
129
|
+
layout.getLineBottom(endLine) + originY,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return TextSelection(
|
|
134
|
+
text = selectedText,
|
|
135
|
+
markdown = markdown,
|
|
136
|
+
fullSelection = start == 0 && end >= content.length - 1,
|
|
137
|
+
rect = rect,
|
|
138
|
+
startRect = startRect,
|
|
139
|
+
endRect = endRect,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Clears this text view's selection, which also ends its selection ActionMode. */
|
|
144
|
+
fun TextView.clearTextSelection() {
|
|
145
|
+
val spannable = text as? Spannable ?: return
|
|
146
|
+
Selection.removeSelection(spannable)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* x of [offset] on [line]. At a line's end offset `getPrimaryHorizontal` reports the start
|
|
151
|
+
* of the next line, so a selection ending there uses the line's right edge.
|
|
152
|
+
*/
|
|
153
|
+
private fun lineEndHorizontal(
|
|
154
|
+
layout: Layout,
|
|
155
|
+
line: Int,
|
|
156
|
+
offset: Int,
|
|
157
|
+
): Float =
|
|
158
|
+
if (offset >= layout.getLineEnd(line) && line < layout.lineCount - 1) {
|
|
159
|
+
layout.getLineRight(line)
|
|
160
|
+
} else {
|
|
161
|
+
layout.getPrimaryHorizontal(offset)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Coalesces selection changes into at most one `onSelectionChange` per frame. The payload is
|
|
166
|
+
* read when the frame fires, so the latest state always reaches JS; a cleared selection is
|
|
167
|
+
* reported right away and drops any pending update. Identical payloads are sent once, so the
|
|
168
|
+
* `setText` passes of streaming don't repeat `active: false`.
|
|
169
|
+
*/
|
|
170
|
+
class SelectionChangeReporter(
|
|
171
|
+
private val view: View,
|
|
172
|
+
private val snapshot: () -> SelectionSnapshot,
|
|
173
|
+
private val emit: (SelectionSnapshot) -> Unit,
|
|
174
|
+
) {
|
|
175
|
+
var enabled = false
|
|
176
|
+
set(value) {
|
|
177
|
+
if (field == value) return
|
|
178
|
+
field = value
|
|
179
|
+
cancel()
|
|
180
|
+
lastReported = SelectionSnapshot.INACTIVE
|
|
181
|
+
if (value) schedule()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private var lastReported = SelectionSnapshot.INACTIVE
|
|
185
|
+
private var pending = false
|
|
186
|
+
private val flushRunnable =
|
|
187
|
+
Runnable {
|
|
188
|
+
pending = false
|
|
189
|
+
report()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Reports the current state on the next frame. */
|
|
193
|
+
fun schedule() {
|
|
194
|
+
if (!enabled || pending) return
|
|
195
|
+
pending = true
|
|
196
|
+
view.postOnAnimation(flushRunnable)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fun onSelectionChanged(hasSelection: Boolean) {
|
|
200
|
+
if (hasSelection) schedule() else flushNow()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Reports the current state right away, dropping any pending frame. */
|
|
204
|
+
fun flushNow() {
|
|
205
|
+
cancel()
|
|
206
|
+
report()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Makes the next report go out even if it equals the last one — a block long-pressed
|
|
211
|
+
* again is a new request for the app's menu, not a repeat.
|
|
212
|
+
*/
|
|
213
|
+
fun forgetLastReported() {
|
|
214
|
+
lastReported = SelectionSnapshot.INACTIVE
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fun cancel() {
|
|
218
|
+
if (!pending) return
|
|
219
|
+
pending = false
|
|
220
|
+
view.removeCallbacks(flushRunnable)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private fun report() {
|
|
224
|
+
if (!enabled) return
|
|
225
|
+
val current = snapshot()
|
|
226
|
+
if (current == lastReported) return
|
|
227
|
+
lastReported = current
|
|
228
|
+
emit(current)
|
|
229
|
+
}
|
|
230
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt
CHANGED
|
@@ -70,6 +70,10 @@ class CodeBlockContainerView(
|
|
|
70
70
|
// control, not a context menu, and stays.
|
|
71
71
|
var contextMenuHidden: Boolean = false
|
|
72
72
|
|
|
73
|
+
// Called instead of the menu on long press while contextMenuHidden is on, so
|
|
74
|
+
// the owner can report the block (onSelectionChange) and show its own UI.
|
|
75
|
+
var onMenuSuppressed: ((markdown: String, text: String) -> Unit)? = null
|
|
76
|
+
|
|
73
77
|
// Copy / Copy as Markdown visibility, Copy format and the header copy button.
|
|
74
78
|
// The header button always copies the raw code.
|
|
75
79
|
var menuOptions: BlockMenuOptions = BlockMenuOptions()
|
|
@@ -259,7 +263,13 @@ class CodeBlockContainerView(
|
|
|
259
263
|
// the event when there is one (a pending block has no menu yet, and a hidden
|
|
260
264
|
// one never does).
|
|
261
265
|
private fun showContextMenu(anchor: View): Boolean {
|
|
262
|
-
if (pending
|
|
266
|
+
if (pending) return false
|
|
267
|
+
if (contextMenuHidden) {
|
|
268
|
+
val onSuppressed = onMenuSuppressed ?: return false
|
|
269
|
+
onSuppressed(CodeBlockNode.fencedMarkdown(code, language, fenceChar), code)
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
if (!menuOptions.hasItems) return false
|
|
263
273
|
ContextMenuPopup.show(anchor, this) {
|
|
264
274
|
blockCopyItems(
|
|
265
275
|
menuOptions,
|
|
@@ -24,6 +24,7 @@ import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
|
|
|
24
24
|
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
25
25
|
import com.swmansion.enriched.markdown.parser.MarkdownASTNode.NodeType
|
|
26
26
|
import com.swmansion.enriched.markdown.renderer.Renderer
|
|
27
|
+
import com.swmansion.enriched.markdown.spans.LinkChipBackgroundSpan
|
|
27
28
|
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
28
29
|
import com.swmansion.enriched.markdown.styles.TableStyle
|
|
29
30
|
import com.swmansion.enriched.markdown.utils.common.layout.isLayoutRTL
|
|
@@ -66,6 +67,10 @@ class TableContainerView(
|
|
|
66
67
|
var contextMenuHidden: Boolean = false
|
|
67
68
|
var menuOptions: BlockMenuOptions = BlockMenuOptions()
|
|
68
69
|
|
|
70
|
+
// Called instead of the menu on long press while contextMenuHidden is on, so
|
|
71
|
+
// the owner can report the block (onSelectionChange) and show its own UI.
|
|
72
|
+
var onMenuSuppressed: ((markdown: String, text: String) -> Unit)? = null
|
|
73
|
+
|
|
69
74
|
private val scrollView =
|
|
70
75
|
HorizontalScrollView(context).apply {
|
|
71
76
|
isHorizontalScrollBarEnabled = true
|
|
@@ -81,6 +86,19 @@ class TableContainerView(
|
|
|
81
86
|
|
|
82
87
|
val rowCount: Int get() = rows.size
|
|
83
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Number of links across all cells. Tables are not reported by `onLinkLayout`, but their links
|
|
91
|
+
* still count so later link indices stay in document order.
|
|
92
|
+
*/
|
|
93
|
+
val linkCount: Int
|
|
94
|
+
get() =
|
|
95
|
+
rows.sumOf { row ->
|
|
96
|
+
row.sumOf { cell ->
|
|
97
|
+
// LinkRenderer attaches one LinkChipBackgroundSpan per link.
|
|
98
|
+
cell.attributedText.getSpans(0, cell.attributedText.length, LinkChipBackgroundSpan::class.java).size
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
fun animateNewRows(
|
|
85
103
|
previousRowCount: Int,
|
|
86
104
|
durationMs: Long,
|
|
@@ -364,7 +382,10 @@ class TableContainerView(
|
|
|
364
382
|
}
|
|
365
383
|
|
|
366
384
|
private fun showContextMenu(anchor: View) {
|
|
367
|
-
if (contextMenuHidden)
|
|
385
|
+
if (contextMenuHidden) {
|
|
386
|
+
onMenuSuppressed?.invoke(tableMarkdown, buildPlainText())
|
|
387
|
+
return
|
|
388
|
+
}
|
|
368
389
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
369
390
|
ContextMenuPopup.show(anchor, this) {
|
|
370
391
|
blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = { tableMarkdown }, rawText = ::buildPlainText, copyRaw = {
|
|
@@ -44,6 +44,10 @@ class MathContainerView(
|
|
|
44
44
|
var contextMenuHidden: Boolean = false
|
|
45
45
|
var menuOptions: BlockMenuOptions = BlockMenuOptions()
|
|
46
46
|
|
|
47
|
+
// Called instead of the menu on long press while contextMenuHidden is on, so
|
|
48
|
+
// the owner can report the block (onSelectionChange) and show its own UI.
|
|
49
|
+
var onMenuSuppressed: ((markdown: String, text: String) -> Unit)? = null
|
|
50
|
+
|
|
47
51
|
override val segmentMarginTop: Int get() = mathStyle.marginTop.toInt()
|
|
48
52
|
override val segmentMarginBottom: Int get() = mathStyle.marginBottom.toInt()
|
|
49
53
|
|
|
@@ -119,7 +123,10 @@ class MathContainerView(
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
private fun showContextMenu(anchor: View) {
|
|
122
|
-
if (contextMenuHidden)
|
|
126
|
+
if (contextMenuHidden) {
|
|
127
|
+
onMenuSuppressed?.invoke("$$\n$cachedLatex\n$$", cachedLatex)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
123
130
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
124
131
|
ContextMenuPopup.show(anchor, this) {
|
|
125
132
|
blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = {
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -69,6 +69,59 @@ Callback when a link is long pressed. Access URL via `event.url`. On iOS, automa
|
|
|
69
69
|
/>
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
### `renderLinkDecoration`
|
|
73
|
+
|
|
74
|
+
Renders a React Native view on top of each link — for example a badge or sparkle on a [link chip](STYLES.md#link-chips). Return `null` to leave a link undecorated.
|
|
75
|
+
|
|
76
|
+
| Type | Default Value | Platform |
|
|
77
|
+
| ------------------------------------------------------ | ------------- | -------- |
|
|
78
|
+
| `(info: LinkDecorationInfo) => ReactNode \| null` | - | Both |
|
|
79
|
+
|
|
80
|
+
`LinkDecorationInfo`:
|
|
81
|
+
|
|
82
|
+
| Field | Type | Description |
|
|
83
|
+
| ------- | ------------ | ----------- |
|
|
84
|
+
| `index` | `number` | Position of the link in the document (0-based). Stable key for the link |
|
|
85
|
+
| `url` | `string` | Link URL |
|
|
86
|
+
| `text` | `string` | Visible link text |
|
|
87
|
+
| `rect` | `LinkRect` | Anchor rect: the drawn chip (glyphs + horizontal/vertical padding) of the link's **last** line fragment |
|
|
88
|
+
| `rects` | `LinkRect[]` | Every line fragment, in order (several when the link wraps) |
|
|
89
|
+
|
|
90
|
+
`LinkRect` is `{ x, y, width, height }` in dp, relative to the component's content box (inside `containerStyle` padding).
|
|
91
|
+
|
|
92
|
+
The returned node is placed inside an absolutely positioned box matching `rect`, with `pointerEvents="box-none"`, so position it relative to that box. Negative offsets let it overflow the chip:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<EnrichedMarkdownText
|
|
96
|
+
markdown="Read the [guide](https://example.com/guide) first."
|
|
97
|
+
markdownStyle={{
|
|
98
|
+
link: {
|
|
99
|
+
backgroundColor: '#DCE2FF',
|
|
100
|
+
borderRadius: 5,
|
|
101
|
+
paddingHorizontal: 7,
|
|
102
|
+
underline: false,
|
|
103
|
+
},
|
|
104
|
+
}}
|
|
105
|
+
renderLinkDecoration={({ url }) =>
|
|
106
|
+
url.startsWith('https://') ? (
|
|
107
|
+
<View style={{ position: 'absolute', top: -8, right: -8 }}>
|
|
108
|
+
<SparkleIcon />
|
|
109
|
+
</View>
|
|
110
|
+
) : null
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Notes and limitations:
|
|
116
|
+
|
|
117
|
+
- When this prop is not set the component renders exactly as before: no wrapper view and no layout events.
|
|
118
|
+
- Setting it wraps the native view in a `View` (which takes `containerStyle`). Switching between set and unset remounts the native view.
|
|
119
|
+
- Rects are measured natively after layout and sent to JS, so decorations appear one frame after the text. Updates are deduplicated — streaming does not re-render decorations unless a link actually moves.
|
|
120
|
+
- Links inside tables (`flavor="github"`) are not reported.
|
|
121
|
+
- On Android, parts of a decoration that overflow the component's bounds do not receive touches.
|
|
122
|
+
- Web: `rect` is the link's whole inline box and `rects` is `[rect]`; for a wrapping link the decoration anchors to that box rather than the last line.
|
|
123
|
+
- Not supported by `EnrichedMarkdownTextInput`.
|
|
124
|
+
|
|
72
125
|
### `onTaskListItemPress`
|
|
73
126
|
|
|
74
127
|
Callback when a task list checkbox is tapped. Receives `index` (0-based), `checked` (new state after toggling), and `text` (item text).
|
|
@@ -170,6 +223,62 @@ import * as Clipboard from 'expo-clipboard';
|
|
|
170
223
|
/>
|
|
171
224
|
```
|
|
172
225
|
|
|
226
|
+
### `onSelectionChange`
|
|
227
|
+
|
|
228
|
+
Reports the text selection so the app can draw its own selection menu. Pair it with [`contextMenuHidden`](#contextmenuhidden) to replace the native menu entirely.
|
|
229
|
+
|
|
230
|
+
| Type | Default Value | Platform |
|
|
231
|
+
| ---------------------------------------- | ------------- | -------- |
|
|
232
|
+
| `(event: SelectionChangeEvent) => void` | - | Both |
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
interface SelectionRect { x: number; y: number; width: number; height: number }
|
|
236
|
+
|
|
237
|
+
interface SelectionChangeEvent {
|
|
238
|
+
/** `false` once the selection is gone; every other field is then empty / zero. */
|
|
239
|
+
active: boolean;
|
|
240
|
+
/** `'selection'` for text, or the kind of a long-pressed block. */
|
|
241
|
+
source: 'selection' | 'math' | 'table' | 'code';
|
|
242
|
+
/** Same as `CopyEvent.text` / `CopyEvent.markdown` / `CopyEvent.whole`. */
|
|
243
|
+
text: string;
|
|
244
|
+
markdown: string;
|
|
245
|
+
whole: boolean;
|
|
246
|
+
/** Bounding box (full content width when it spans several lines). */
|
|
247
|
+
rect: SelectionRect;
|
|
248
|
+
/** Selected part of the first / last line — anchor a menu above the start or below the end. */
|
|
249
|
+
startRect: SelectionRect;
|
|
250
|
+
endRect: SelectionRect;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
- Fires at most once per frame while the selection changes (payloads are deduplicated), and with `active: false` right away once it is gone — including when new `markdown` replaces the text.
|
|
255
|
+
- Rects are in dp, relative to the component's native view: add the origin from `ref.current.measureInWindow` to get window coordinates. Re-measure after scrolling instead of waiting for a new event.
|
|
256
|
+
- With `contextMenuHidden`, a long press on a table, math or code block (`flavor="github"`) fires the event with `source` set to the block kind, `whole: true` and every rect set to the block's frame. These blocks have no text selection, so without this they would offer no way to copy. Without `contextMenuHidden` the block menu shows as usual and no event fires.
|
|
257
|
+
- There is no "dragging" flag. Debounce before showing your menu, so it doesn't flicker while a handle is dragged.
|
|
258
|
+
- Within one component only one part holds a selection at a time: selecting in another text segment clears the previous one. To keep a single selection across several components, call `clearSelection()` on the previous one.
|
|
259
|
+
- Ignored on macOS and web.
|
|
260
|
+
|
|
261
|
+
**Imperative handle.** `ref` exposes `clearSelection()`, which clears the selection (and any long-pressed block) and fires `active: false`. It also forwards `measure`, `measureInWindow` and `measureLayout`.
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
const ref = useRef<EnrichedMarkdownTextInstance>(null);
|
|
265
|
+
|
|
266
|
+
<EnrichedMarkdownText
|
|
267
|
+
ref={ref}
|
|
268
|
+
markdown={content}
|
|
269
|
+
contextMenuHidden
|
|
270
|
+
onSelectionChange={(e) => {
|
|
271
|
+
if (!e.active) return hideMenu();
|
|
272
|
+
ref.current?.measureInWindow((x, y) =>
|
|
273
|
+
showMenuDebounced({ x: x + e.startRect.x, y: y + e.startRect.y }, e.markdown)
|
|
274
|
+
);
|
|
275
|
+
}}
|
|
276
|
+
/>;
|
|
277
|
+
|
|
278
|
+
// After the menu action:
|
|
279
|
+
ref.current?.clearSelection();
|
|
280
|
+
```
|
|
281
|
+
|
|
173
282
|
### `enableLinkPreview`
|
|
174
283
|
|
|
175
284
|
Controls the native link preview on long press (iOS only). Automatically set to `false` when `onLinkLongPress` is provided.
|
|
@@ -228,6 +337,8 @@ Notes:
|
|
|
228
337
|
prop suppresses it the same way.
|
|
229
338
|
- Applies to `EnrichedMarkdownText` only — it does not affect
|
|
230
339
|
`EnrichedMarkdownTextInput`.
|
|
340
|
+
- To show your own menu instead, use [`onSelectionChange`](#onselectionchange);
|
|
341
|
+
with this prop on, block long-presses are reported through it as well.
|
|
231
342
|
|
|
232
343
|
### `selectionColor`
|
|
233
344
|
|
package/docs/MENTIONS.md
CHANGED
|
@@ -69,7 +69,14 @@ linkVariants: {
|
|
|
69
69
|
}
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
Each key is a regex tested against the link URL. First match wins.
|
|
72
|
+
Each key is a regex tested against the link URL. First match wins. Patterns are auto-sorted longest-first.
|
|
73
|
+
|
|
74
|
+
Inheritance from the base `link` style is split in two groups:
|
|
75
|
+
|
|
76
|
+
- **Text** (`color`, `underline`): unspecified properties inherit from `link`.
|
|
77
|
+
- **Box** (`backgroundColor`, `borderRadius`, `paddingHorizontal`, `paddingVertical`, `borderColor`, `borderWidth`): never inherited. A variant that sets none of them renders without a chip, even when `link` is a chip.
|
|
78
|
+
|
|
79
|
+
`fontFamily` is only configurable on `link` and applies to every variant. See [Link chips](STYLES.md#link-chips) for the box properties.
|
|
73
80
|
|
|
74
81
|
## Positioning the Suggestion List
|
|
75
82
|
|
package/docs/STYLES.md
CHANGED
|
@@ -368,6 +368,33 @@ Per-token foreground colors for syntax highlighting, keyed on the highlight toke
|
|
|
368
368
|
| `fontFamily` | `string` | Font family for links. Overrides the parent block's font family when set |
|
|
369
369
|
| `color` | `string` | Link text color |
|
|
370
370
|
| `underline` | `boolean` | Show underline |
|
|
371
|
+
| `backgroundColor` | `string` | Chip background, drawn behind the link's glyphs (ascender to descender) plus padding |
|
|
372
|
+
| `borderRadius` | `number` | Chip corner radius |
|
|
373
|
+
| `paddingHorizontal` | `number` | Space added before and after the link text. It takes real room in the line, so it affects wrapping and measured size |
|
|
374
|
+
| `paddingVertical` | `number` | Extends the chip above and below the text. Drawn only: it does not change the line height, so it can overlap neighbouring lines (or be clipped at the component's top/bottom edge) if the line height is too small |
|
|
375
|
+
| `borderColor` | `string` | Chip border color |
|
|
376
|
+
| `borderWidth` | `number` | Chip border width, drawn inset (does not grow the chip) |
|
|
377
|
+
|
|
378
|
+
#### Link chips
|
|
379
|
+
|
|
380
|
+
Setting `backgroundColor`, `borderRadius`, padding or a border turns a link into a chip. When a link wraps, each line gets its own piece of the chip; only the outer ends are rounded and the border is left open where the link breaks.
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
link: {
|
|
384
|
+
color: '#3346C8',
|
|
385
|
+
underline: false,
|
|
386
|
+
backgroundColor: '#DCE2FF',
|
|
387
|
+
borderRadius: 5,
|
|
388
|
+
paddingHorizontal: 7,
|
|
389
|
+
paddingVertical: 4,
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Horizontal padding is reserved with an invisible non-breaking spacer at each end of the link, so the padding always stays attached to the text on wrap. The spacer is removed from copied text (plain, HTML, RTF, Markdown and `onCopy`) and is part of the link's tap area.
|
|
394
|
+
|
|
395
|
+
On iOS (and in RTF copies), links inside table cells fall back to a plain text background: `backgroundColor` and `paddingHorizontal` are kept (the background covers the horizontal padding), but `borderRadius`, `paddingVertical` and the border are not drawn.
|
|
396
|
+
|
|
397
|
+
Use [`renderLinkDecoration`](API_REFERENCE.md#renderlinkdecoration) to draw a React Native view on top of a chip (e.g. a badge or icon).
|
|
371
398
|
|
|
372
399
|
### Strong-specific
|
|
373
400
|
|
package/docs/TEXT.md
CHANGED
|
@@ -95,6 +95,8 @@ Links in Markdown are interactive and can be handled with the `onLinkPress` and
|
|
|
95
95
|
|
|
96
96
|
See the [API Reference](API_REFERENCE.md#onlinkpress) for detailed examples and usage.
|
|
97
97
|
|
|
98
|
+
Links can be styled as chips (background, radius, padding, border — see [Link chips](STYLES.md#link-chips)) and decorated with your own views via [`renderLinkDecoration`](API_REFERENCE.md#renderlinkdecoration).
|
|
99
|
+
|
|
98
100
|
## Supported Markdown Elements
|
|
99
101
|
|
|
100
102
|
`react-native-enriched-markdown-scaffold` supports a comprehensive set of Markdown elements. See [Element Structure](ELEMENTS_STRUCTURE.md) for a detailed overview of all supported elements, their syntax, block vs inline categorization, nesting behavior, and how elements inherit typography from their parent blocks.
|