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
|
@@ -2,6 +2,9 @@ package com.swmansion.enriched.markdown.views
|
|
|
2
2
|
|
|
3
3
|
import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
|
|
4
4
|
|
|
5
|
+
/** Receives an intercepted block Copy: the block's Markdown and its raw content. */
|
|
6
|
+
typealias BlockCopyRequest = (markdown: String, text: String) -> Unit
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Selection-menu options that table, math and code block views need for their
|
|
7
10
|
* own long-press menus. Every default keeps the legacy behavior, so a block view
|
|
@@ -14,17 +17,23 @@ data class BlockMenuOptions(
|
|
|
14
17
|
val copyAsMarkdownHidden: Boolean = false,
|
|
15
18
|
// Hides the copy button in the code block header.
|
|
16
19
|
val copyButtonHidden: Boolean = false,
|
|
20
|
+
// Set when `onCopy` is on: the Copy item hands the content here instead of
|
|
21
|
+
// writing the clipboard. The host binds it to the block's source.
|
|
22
|
+
val onCopyRequest: BlockCopyRequest? = null,
|
|
17
23
|
) {
|
|
18
24
|
val hasItems: Boolean get() = !copyHidden || !copyAsMarkdownHidden
|
|
19
25
|
|
|
20
26
|
companion object {
|
|
21
|
-
fun from(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
fun from(
|
|
28
|
+
config: SelectionMenuConfig,
|
|
29
|
+
onCopyRequest: BlockCopyRequest? = null,
|
|
30
|
+
) = BlockMenuOptions(
|
|
31
|
+
copyHidden = !config.copyEnabled,
|
|
32
|
+
copyWritesMarkdown = config.copyFormatMarkdown,
|
|
33
|
+
copyAsMarkdownHidden = !config.copyAsMarkdown,
|
|
34
|
+
copyButtonHidden = !config.codeBlockCopyButton,
|
|
35
|
+
onCopyRequest = onCopyRequest.takeIf { config.interceptCopy },
|
|
36
|
+
)
|
|
28
37
|
}
|
|
29
38
|
}
|
|
30
39
|
|
|
@@ -33,11 +42,21 @@ fun ContextMenuPopup.Builder.blockCopyItems(
|
|
|
33
42
|
options: BlockMenuOptions,
|
|
34
43
|
copyLabel: String,
|
|
35
44
|
copyAsMarkdownLabel: String,
|
|
45
|
+
// The block's Markdown and raw content, read only for an intercepted Copy.
|
|
46
|
+
markdown: () -> String,
|
|
47
|
+
rawText: () -> String,
|
|
36
48
|
copyRaw: () -> Unit,
|
|
37
49
|
copyMarkdown: () -> Unit,
|
|
38
50
|
) {
|
|
39
51
|
if (!options.copyHidden) {
|
|
40
|
-
|
|
52
|
+
val onCopyRequest = options.onCopyRequest
|
|
53
|
+
val onCopy =
|
|
54
|
+
when {
|
|
55
|
+
onCopyRequest != null -> { -> onCopyRequest(markdown(), rawText()) }
|
|
56
|
+
options.copyWritesMarkdown -> copyMarkdown
|
|
57
|
+
else -> copyRaw
|
|
58
|
+
}
|
|
59
|
+
item(ContextMenuPopup.Icon.COPY, copyLabel, onCopy)
|
|
41
60
|
}
|
|
42
61
|
if (!options.copyAsMarkdownHidden) {
|
|
43
62
|
item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel, copyMarkdown)
|
package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt
CHANGED
|
@@ -261,7 +261,15 @@ class CodeBlockContainerView(
|
|
|
261
261
|
private fun showContextMenu(anchor: View): Boolean {
|
|
262
262
|
if (pending || contextMenuHidden || !menuOptions.hasItems) return false
|
|
263
263
|
ContextMenuPopup.show(anchor, this) {
|
|
264
|
-
blockCopyItems(
|
|
264
|
+
blockCopyItems(
|
|
265
|
+
menuOptions,
|
|
266
|
+
copyLabel,
|
|
267
|
+
copyAsMarkdownLabel,
|
|
268
|
+
markdown = { CodeBlockNode.fencedMarkdown(code, language, fenceChar) },
|
|
269
|
+
rawText = { code },
|
|
270
|
+
copyRaw = ::copyCode,
|
|
271
|
+
copyMarkdown = ::copyFencedMarkdown,
|
|
272
|
+
)
|
|
265
273
|
}
|
|
266
274
|
return true
|
|
267
275
|
}
|
|
@@ -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
|
|
@@ -81,6 +82,19 @@ class TableContainerView(
|
|
|
81
82
|
|
|
82
83
|
val rowCount: Int get() = rows.size
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Number of links across all cells. Tables are not reported by `onLinkLayout`, but their links
|
|
87
|
+
* still count so later link indices stay in document order.
|
|
88
|
+
*/
|
|
89
|
+
val linkCount: Int
|
|
90
|
+
get() =
|
|
91
|
+
rows.sumOf { row ->
|
|
92
|
+
row.sumOf { cell ->
|
|
93
|
+
// LinkRenderer attaches one LinkChipBackgroundSpan per link.
|
|
94
|
+
cell.attributedText.getSpans(0, cell.attributedText.length, LinkChipBackgroundSpan::class.java).size
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
fun animateNewRows(
|
|
85
99
|
previousRowCount: Int,
|
|
86
100
|
durationMs: Long,
|
|
@@ -367,8 +381,8 @@ class TableContainerView(
|
|
|
367
381
|
if (contextMenuHidden) return
|
|
368
382
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
369
383
|
ContextMenuPopup.show(anchor, this) {
|
|
370
|
-
blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, copyRaw = {
|
|
371
|
-
val plainText =
|
|
384
|
+
blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = { tableMarkdown }, rawText = ::buildPlainText, copyRaw = {
|
|
385
|
+
val plainText = buildPlainText()
|
|
372
386
|
if (plainText.isNotEmpty()) {
|
|
373
387
|
val displayMetrics = context.resources.displayMetrics
|
|
374
388
|
val tableRows =
|
|
@@ -384,6 +398,8 @@ class TableContainerView(
|
|
|
384
398
|
}
|
|
385
399
|
}
|
|
386
400
|
|
|
401
|
+
private fun buildPlainText(): String = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
|
|
402
|
+
|
|
387
403
|
private fun buildMarkdownFromRows(): String =
|
|
388
404
|
rows.joinToString("") { row ->
|
|
389
405
|
val line = "| ${row.joinToString(" | ") { it.markdownText }} |\n"
|
|
@@ -122,7 +122,9 @@ class MathContainerView(
|
|
|
122
122
|
if (contextMenuHidden) return
|
|
123
123
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
124
124
|
ContextMenuPopup.show(anchor, this) {
|
|
125
|
-
blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel,
|
|
125
|
+
blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = {
|
|
126
|
+
"$$\n$cachedLatex\n$$"
|
|
127
|
+
}, rawText = { cachedLatex }, copyRaw = {
|
|
126
128
|
clipboard.setPrimaryClip(ClipData.newPlainText("Math", cachedLatex))
|
|
127
129
|
}, copyMarkdown = {
|
|
128
130
|
clipboard.setPrimaryClip(ClipData.newPlainText("Math", "$$\n$cachedLatex\n$$"))
|
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).
|
|
@@ -122,6 +175,54 @@ Only fires when `flavor="github"` — the copy button is part of the GitHub flav
|
|
|
122
175
|
/>
|
|
123
176
|
```
|
|
124
177
|
|
|
178
|
+
### `onCopy`
|
|
179
|
+
|
|
180
|
+
Takes over the **Copy** item of the text selection menu and of the table, math and code block menus. When set, native does **not** write the clipboard: it calls `onCopy` with the copied content and your handler writes whatever it wants — for example with [`expo-clipboard`](https://docs.expo.dev/versions/latest/sdk/clipboard/). Use it to post-process what users copy (strip formatting, drop link targets…) without a native change.
|
|
181
|
+
|
|
182
|
+
| Type | Default Value | Platform |
|
|
183
|
+
| ----------------------------- | ------------- | -------- |
|
|
184
|
+
| `(event: CopyEvent) => void` | - | Both |
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
interface CopyEvent {
|
|
188
|
+
/** Which menu the Copy item belongs to. */
|
|
189
|
+
source: 'selection' | 'math' | 'table' | 'code';
|
|
190
|
+
/** What `copy.format: 'markdown'` would write. */
|
|
191
|
+
markdown: string;
|
|
192
|
+
/** Rendered text of the selection, or the block's raw content. */
|
|
193
|
+
text: string;
|
|
194
|
+
/** The selection spans the whole component (`markdown` is the source). Always true for blocks. */
|
|
195
|
+
whole: boolean;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
| `source` | `markdown` | `text` |
|
|
200
|
+
| ----------- | -------------------------------------------------------------------------- | ----------------------------- |
|
|
201
|
+
| `selection` | Original source when `whole`, otherwise rebuilt from the rendered styles | Selected text as rendered |
|
|
202
|
+
| `math` | `$$…$$` | LaTeX |
|
|
203
|
+
| `table` | The table as Markdown | Tab-separated cells |
|
|
204
|
+
| `code` | Fenced code block | Raw code |
|
|
205
|
+
|
|
206
|
+
- `onCopy` overrides `selectionMenuConfig.copy.format`; `copy.enabled: false` still hides the item, so no event fires.
|
|
207
|
+
- **Copy as Markdown**, the code block header copy button and the VoiceOver copy action are unaffected. `onCopyPress` does not fire for an intercepted Copy.
|
|
208
|
+
- With `flavor="github"` the text is split into segments around blocks, so a selection is `whole` only when the markdown renders as a single text segment.
|
|
209
|
+
- A rebuilt `markdown` wraps each styled run separately (`**a *****b***** c**`) and does not escape literal `*` / `_`.
|
|
210
|
+
- Copying with a hardware keyboard (Cmd+C / Ctrl+C) does not go through the menu and is not intercepted.
|
|
211
|
+
- Ignored on macOS. On web the prop is not this API: it reaches the container `div` as the DOM `onCopy` clipboard event.
|
|
212
|
+
|
|
213
|
+
**Example:**
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
import * as Clipboard from 'expo-clipboard';
|
|
217
|
+
|
|
218
|
+
<EnrichedMarkdownText
|
|
219
|
+
markdown={content}
|
|
220
|
+
onCopy={({ source, markdown }) => {
|
|
221
|
+
Clipboard.setStringAsync(source === 'selection' ? stripFormatting(markdown) : markdown);
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
```
|
|
225
|
+
|
|
125
226
|
### `enableLinkPreview`
|
|
126
227
|
|
|
127
228
|
Controls the native link preview on long press (iOS only). Automatically set to `false` when `onLinkLongPress` is provided.
|
package/docs/COPY_OPTIONS.md
CHANGED
|
@@ -70,6 +70,20 @@ code and fire `onCopyPress`; `codeBlockCopyButton: { enabled: false }` hides the
|
|
|
70
70
|
button, and `copy: { enabled: false }` also removes the VoiceOver action.
|
|
71
71
|
`copy.enabled`, `copy.format` and `systemItems` are ignored on macOS.
|
|
72
72
|
|
|
73
|
+
### Handling Copy in JS
|
|
74
|
+
|
|
75
|
+
To decide in JS what lands on the clipboard, pass `onCopy`. The Copy item
|
|
76
|
+
(text selection and block menus) then writes nothing itself and hands you the
|
|
77
|
+
content — `markdown`, `text`, `source` and `whole` — to write with your
|
|
78
|
+
clipboard library. See [`onCopy`](API_REFERENCE.md#oncopy).
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<EnrichedMarkdownText
|
|
82
|
+
markdown={content}
|
|
83
|
+
onCopy={({ markdown }) => Clipboard.setStringAsync(markdown)}
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
73
87
|
To turn the menu off entirely rather than item by item, use `contextMenuHidden`:
|
|
74
88
|
|
|
75
89
|
```tsx
|
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.
|
package/ios/EnrichedMarkdown.mm
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
#import "SegmentReconciler.h"
|
|
39
39
|
#import "SegmentRenderer.h"
|
|
40
40
|
#import "SegmentViewRegistry.h"
|
|
41
|
+
#import "ENRMLinkLayout.h"
|
|
41
42
|
#import "SelectionColorUtils.h"
|
|
42
43
|
#import "StreamingMarkdownFilter.h"
|
|
43
44
|
#import "StyleConfig.h"
|
|
@@ -73,8 +74,10 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
73
74
|
+ (ENRMMd4cFlags *)flagsFromProps:(const EnrichedMarkdownMd4cFlagsStruct &)props;
|
|
74
75
|
- (void)emitLinkPress:(NSString *)url;
|
|
75
76
|
- (void)emitLinkLongPress:(NSString *)url;
|
|
77
|
+
- (void)emitLinkLayoutIfNeeded;
|
|
76
78
|
- (void)emitTaskListItemPress:(NSInteger)index checked:(BOOL)checked text:(NSString *)text;
|
|
77
79
|
- (void)emitCopyPress:(NSString *)code language:(NSString *)language;
|
|
80
|
+
- (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole;
|
|
78
81
|
- (void)emitContextMenuItemPress:(NSString *)itemText
|
|
79
82
|
selectedText:(NSString *)selectedText
|
|
80
83
|
selectionStart:(NSUInteger)selectionStart
|
|
@@ -122,6 +125,9 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
122
125
|
|
|
123
126
|
ENRMSpoilerOverlay _spoilerOverlay;
|
|
124
127
|
|
|
128
|
+
BOOL _emitLinkLayout;
|
|
129
|
+
NSArray<NSDictionary *> *_lastLinkLayout;
|
|
130
|
+
|
|
125
131
|
NSLineBreakStrategy _lineBreakStrategy;
|
|
126
132
|
|
|
127
133
|
ENRMWritingDirectionMode _writingDirectionMode;
|
|
@@ -512,6 +518,17 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
512
518
|
.copyWritesMarkdown = _selectionMenuConfig.copyFormatMarkdown,
|
|
513
519
|
.copyAsMarkdownHidden = !_selectionMenuConfig.copyAsMarkdown,
|
|
514
520
|
.copyButtonHidden = !_selectionMenuConfig.codeBlockCopyButton,
|
|
521
|
+
.interceptCopy = _selectionMenuConfig.interceptCopy,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
- (ENRMBlockCopyRequestBlock)blockCopyRequestForSource:(NSString *)source
|
|
526
|
+
{
|
|
527
|
+
__weak EnrichedMarkdown *weakSelf = self;
|
|
528
|
+
return ^(NSString *markdown, NSString *text) {
|
|
529
|
+
EnrichedMarkdown *strongSelf = weakSelf;
|
|
530
|
+
if (strongSelf)
|
|
531
|
+
[strongSelf emitCopyRequest:source markdown:markdown text:text whole:YES];
|
|
515
532
|
};
|
|
516
533
|
}
|
|
517
534
|
|
|
@@ -800,7 +817,7 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
800
817
|
selectionEnd:selectionEnd];
|
|
801
818
|
});
|
|
802
819
|
return buildEditMenuForSelection(textView.textStorage, textView.selectedRange, segmentMarkdown, strongSelf->_config,
|
|
803
|
-
@[ baseMenu ], customItems, strongSelf
|
|
820
|
+
@[ baseMenu ], customItems, strongSelf->_selectionMenuConfig);
|
|
804
821
|
}];
|
|
805
822
|
#endif
|
|
806
823
|
|
|
@@ -821,6 +838,7 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
821
838
|
tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
|
|
822
839
|
tableView.contextMenuHidden = _contextMenuHidden;
|
|
823
840
|
tableView.menuOptions = [self blockMenuOptions];
|
|
841
|
+
tableView.onCopyRequest = [self blockCopyRequestForSource:@"table"];
|
|
824
842
|
|
|
825
843
|
__weak EnrichedMarkdown *weakSelf = self;
|
|
826
844
|
|
|
@@ -862,6 +880,7 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
862
880
|
mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
|
|
863
881
|
mathView.contextMenuHidden = _contextMenuHidden;
|
|
864
882
|
mathView.menuOptions = [self blockMenuOptions];
|
|
883
|
+
mathView.onCopyRequest = [self blockCopyRequestForSource:@"math"];
|
|
865
884
|
[mathView applyLatex:mathSegment.latex];
|
|
866
885
|
return mathView;
|
|
867
886
|
}
|
|
@@ -874,6 +893,7 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
874
893
|
codeBlockView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
|
|
875
894
|
codeBlockView.contextMenuHidden = _contextMenuHidden;
|
|
876
895
|
codeBlockView.menuOptions = [self blockMenuOptions];
|
|
896
|
+
codeBlockView.onCopyRequest = [self blockCopyRequestForSource:@"code"];
|
|
877
897
|
|
|
878
898
|
__weak EnrichedMarkdown *weakSelf = self;
|
|
879
899
|
codeBlockView.onCopyPress = ^(NSString *code, NSString *language) {
|
|
@@ -926,6 +946,7 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
926
946
|
{
|
|
927
947
|
[super layoutSubviews];
|
|
928
948
|
[self computeSegmentLayoutForWidth:self.bounds.size.width applyFrames:YES];
|
|
949
|
+
[self emitLinkLayoutIfNeeded];
|
|
929
950
|
}
|
|
930
951
|
|
|
931
952
|
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
@@ -961,6 +982,12 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
961
982
|
|
|
962
983
|
_selectable = newViewProps.selectable;
|
|
963
984
|
|
|
985
|
+
if (newViewProps.emitLinkLayout != _emitLinkLayout) {
|
|
986
|
+
_emitLinkLayout = newViewProps.emitLinkLayout;
|
|
987
|
+
_lastLinkLayout = nil;
|
|
988
|
+
[self setNeedsLayout];
|
|
989
|
+
}
|
|
990
|
+
|
|
964
991
|
for (RCTUIView *segment in _segmentViews) {
|
|
965
992
|
if ([segment isKindOfClass:[EnrichedMarkdownInternalText class]]) {
|
|
966
993
|
EnrichedMarkdownInternalText *textSegment = (EnrichedMarkdownInternalText *)segment;
|
|
@@ -1156,6 +1183,8 @@ static char kENRMSegmentFadeAnimatorKey;
|
|
|
1156
1183
|
_cachedMarkdown = nil;
|
|
1157
1184
|
_renderedMarkdown = nil;
|
|
1158
1185
|
_config = nil;
|
|
1186
|
+
_emitLinkLayout = NO;
|
|
1187
|
+
_lastLinkLayout = nil;
|
|
1159
1188
|
_md4cFlags = [EnrichedMarkdown flagsFromProps:resetProps->md4cFlags];
|
|
1160
1189
|
_maxFontSizeMultiplier = 0;
|
|
1161
1190
|
_allowTrailingMargin = NO;
|
|
@@ -1266,6 +1295,40 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
|
|
|
1266
1295
|
emitter->onLinkPress({.url = std::string(url.UTF8String)});
|
|
1267
1296
|
}
|
|
1268
1297
|
|
|
1298
|
+
/// Reports link rects of every text segment (offset by the segment origin) to
|
|
1299
|
+
/// JS for `renderLinkDecoration`. Tables are skipped: their cells scroll
|
|
1300
|
+
/// horizontally, so rects would go stale. Runs after each segment layout pass;
|
|
1301
|
+
/// deduplicated so an unchanged layout sends nothing.
|
|
1302
|
+
- (void)emitLinkLayoutIfNeeded
|
|
1303
|
+
{
|
|
1304
|
+
if (!_emitLinkLayout || self.bounds.size.width <= 0)
|
|
1305
|
+
return;
|
|
1306
|
+
|
|
1307
|
+
auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
|
|
1308
|
+
if (!emitter)
|
|
1309
|
+
return;
|
|
1310
|
+
|
|
1311
|
+
NSMutableArray<NSDictionary *> *fragments = [NSMutableArray array];
|
|
1312
|
+
NSInteger linkCount = 0;
|
|
1313
|
+
for (RCTUIView *segment in _segmentViews) {
|
|
1314
|
+
if ([segment isKindOfClass:[TableContainerView class]]) {
|
|
1315
|
+
linkCount += ((TableContainerView *)segment).linkCount;
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
if (![segment isKindOfClass:[EnrichedMarkdownInternalText class]])
|
|
1319
|
+
continue;
|
|
1320
|
+
NSInteger segmentLinks = 0;
|
|
1321
|
+
[fragments addObjectsFromArray:ENRMCollectLinkLayoutFragments(((EnrichedMarkdownInternalText *)segment).textView,
|
|
1322
|
+
segment.frame.origin, linkCount, &segmentLinks)];
|
|
1323
|
+
linkCount += segmentLinks;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (_lastLinkLayout != nil && [fragments isEqualToArray:_lastLinkLayout])
|
|
1327
|
+
return;
|
|
1328
|
+
_lastLinkLayout = [fragments copy];
|
|
1329
|
+
emitter->onLinkLayout(ENRMLinkLayoutEventFromFragments<EnrichedMarkdownEventEmitter::OnLinkLayout>(fragments));
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1269
1332
|
- (void)emitLinkLongPress:(NSString *)url
|
|
1270
1333
|
{
|
|
1271
1334
|
auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
|
|
@@ -1288,6 +1351,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
|
|
|
1288
1351
|
{.code = std::string(code.UTF8String ?: ""), .language = std::string(language.UTF8String ?: "")});
|
|
1289
1352
|
}
|
|
1290
1353
|
|
|
1354
|
+
- (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole
|
|
1355
|
+
{
|
|
1356
|
+
auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
|
|
1357
|
+
if (emitter)
|
|
1358
|
+
emitter->onCopyRequest({
|
|
1359
|
+
.source = std::string(source.UTF8String),
|
|
1360
|
+
.markdown = std::string(markdown.UTF8String ?: ""),
|
|
1361
|
+
.text = std::string(text.UTF8String ?: ""),
|
|
1362
|
+
.whole = (bool)whole,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1291
1366
|
- (void)emitContextMenuItemPress:(NSString *)itemText
|
|
1292
1367
|
selectedText:(NSString *)selectedText
|
|
1293
1368
|
selectionStart:(NSUInteger)selectionStart
|
|
@@ -1355,8 +1430,22 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
|
|
|
1355
1430
|
ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler);
|
|
1356
1431
|
|
|
1357
1432
|
NSString *segmentMarkdown = extractMarkdownFromAttributedString(textView.attributedText, range);
|
|
1433
|
+
|
|
1434
|
+
// Segments only see their own text, so a selection is the whole component
|
|
1435
|
+
// only when it spans the sole segment; the markdown is then the original.
|
|
1436
|
+
ENRMCopyRequestHandler onCopyRequest = ^(NSString *markdown, NSString *text, BOOL fullSelection) {
|
|
1437
|
+
EnrichedMarkdown *strongSelf = weakSelf;
|
|
1438
|
+
if (!strongSelf)
|
|
1439
|
+
return;
|
|
1440
|
+
BOOL whole = fullSelection && strongSelf->_segmentViews.count == 1 && strongSelf->_cachedMarkdown.length > 0;
|
|
1441
|
+
[strongSelf emitCopyRequest:@"selection"
|
|
1442
|
+
markdown:whole ? strongSelf->_cachedMarkdown : markdown
|
|
1443
|
+
text:text
|
|
1444
|
+
whole:whole];
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1358
1447
|
return buildEditMenuForSelection(textView.attributedText, range, segmentMarkdown, _config, suggestedActions,
|
|
1359
|
-
customActions, _selectionMenuConfig);
|
|
1448
|
+
customActions, _selectionMenuConfig, onCopyRequest);
|
|
1360
1449
|
}
|
|
1361
1450
|
|
|
1362
1451
|
- (BOOL)textView:(UITextView *)textView
|