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.
Files changed (135) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +134 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -7
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +4 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +13 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +42 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +12 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
  17. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
  18. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
  19. package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
  20. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
  22. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
  23. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
  24. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +33 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
  26. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +24 -2
  29. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +27 -8
  30. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +9 -1
  31. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +18 -2
  32. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
  33. package/docs/API_REFERENCE.md +101 -0
  34. package/docs/COPY_OPTIONS.md +14 -0
  35. package/docs/MENTIONS.md +8 -1
  36. package/docs/STYLES.md +27 -0
  37. package/docs/TEXT.md +2 -0
  38. package/ios/EnrichedMarkdown.mm +91 -2
  39. package/ios/EnrichedMarkdownText.mm +64 -3
  40. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
  41. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
  42. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  43. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +134 -0
  44. package/ios/internals/MeasurementCache.h +5 -0
  45. package/ios/renderer/LinkRenderer.m +47 -15
  46. package/ios/styles/StyleConfig.h +16 -0
  47. package/ios/styles/StyleConfig.mm +60 -0
  48. package/ios/utils/CodeBackground.m +3 -65
  49. package/ios/utils/ENRMBlockMenu.h +12 -3
  50. package/ios/utils/ENRMChipDrawing.h +24 -0
  51. package/ios/utils/ENRMChipDrawing.m +83 -0
  52. package/ios/utils/ENRMLinkChip.h +70 -0
  53. package/ios/utils/ENRMLinkChip.m +194 -0
  54. package/ios/utils/ENRMLinkLayout.h +50 -0
  55. package/ios/utils/ENRMLinkLayout.m +45 -0
  56. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  57. package/ios/utils/EditMenuUtils.h +10 -1
  58. package/ios/utils/EditMenuUtils.m +22 -5
  59. package/ios/utils/HTMLGenerator.m +2 -1
  60. package/ios/utils/MarkdownAccessibilityElementBuilder.m +4 -1
  61. package/ios/utils/MarkdownExtractor.m +2 -1
  62. package/ios/utils/RTFExportUtils.m +2 -0
  63. package/ios/utils/StylePropsUtils.h +37 -1
  64. package/ios/utils/TextViewLayoutManager.mm +3 -0
  65. package/ios/views/ENRMCodeBlockContainerView.h +4 -0
  66. package/ios/views/ENRMCodeBlockContainerView.m +8 -1
  67. package/ios/views/ENRMMathContainerView.h +4 -0
  68. package/ios/views/ENRMMathContainerView.m +8 -1
  69. package/ios/views/TableContainerView.h +7 -0
  70. package/ios/views/TableContainerView.m +30 -3
  71. package/lib/module/EnrichedMarkdownNativeComponent.ts +57 -0
  72. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +57 -0
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/native/EnrichedMarkdownText.js +119 -9
  75. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  76. package/lib/module/normalizeMarkdownStyle.js +13 -2
  77. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  78. package/lib/module/normalizeMarkdownStyle.web.js +13 -2
  79. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  80. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  81. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  82. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  83. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  84. package/lib/module/web/styles.js +18 -0
  85. package/lib/module/web/styles.js.map +1 -1
  86. package/lib/module/web/utils.js +13 -0
  87. package/lib/module/web/utils.js.map +1 -1
  88. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +53 -0
  89. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  90. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +53 -0
  91. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  92. package/lib/typescript/src/index.d.ts +2 -2
  93. package/lib/typescript/src/index.d.ts.map +1 -1
  94. package/lib/typescript/src/index.web.d.ts +1 -1
  95. package/lib/typescript/src/index.web.d.ts.map +1 -1
  96. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  97. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  98. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  99. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  100. package/lib/typescript/src/types/MarkdownStyle.d.ts +28 -5
  101. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  102. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +10 -0
  103. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  104. package/lib/typescript/src/types/MarkdownTextProps.d.ts +54 -1
  105. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  106. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  107. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  108. package/lib/typescript/src/types/events.d.ts +22 -0
  109. package/lib/typescript/src/types/events.d.ts.map +1 -1
  110. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  111. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  112. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  113. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  114. package/lib/typescript/src/web/types.d.ts +4 -0
  115. package/lib/typescript/src/web/types.d.ts.map +1 -1
  116. package/lib/typescript/src/web/utils.d.ts +4 -0
  117. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  118. package/package.json +1 -1
  119. package/src/EnrichedMarkdownNativeComponent.ts +57 -0
  120. package/src/EnrichedMarkdownTextNativeComponent.ts +57 -0
  121. package/src/index.tsx +3 -0
  122. package/src/index.web.tsx +5 -1
  123. package/src/native/EnrichedMarkdownText.tsx +127 -7
  124. package/src/normalizeMarkdownStyle.ts +13 -0
  125. package/src/normalizeMarkdownStyle.web.ts +11 -0
  126. package/src/types/MarkdownStyle.ts +29 -5
  127. package/src/types/MarkdownStyleInternal.ts +10 -0
  128. package/src/types/MarkdownTextProps.ts +56 -0
  129. package/src/types/MarkdownTextProps.web.ts +43 -1
  130. package/src/types/events.ts +23 -0
  131. package/src/web/EnrichedMarkdownText.tsx +10 -3
  132. package/src/web/renderers/InlineRenderers.tsx +111 -3
  133. package/src/web/styles.ts +25 -0
  134. package/src/web/types.ts +4 -0
  135. 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(config: SelectionMenuConfig) =
22
- BlockMenuOptions(
23
- copyHidden = !config.copyEnabled,
24
- copyWritesMarkdown = config.copyFormatMarkdown,
25
- copyAsMarkdownHidden = !config.copyAsMarkdown,
26
- copyButtonHidden = !config.codeBlockCopyButton,
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
- item(ContextMenuPopup.Icon.COPY, copyLabel, if (options.copyWritesMarkdown) copyMarkdown else copyRaw)
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)
@@ -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(menuOptions, copyLabel, copyAsMarkdownLabel, ::copyCode, ::copyFencedMarkdown)
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 = rows.joinToString("\n") { row -> row.joinToString("\t") { it.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, copyRaw = {
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$$"))
@@ -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.
@@ -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. Unspecified properties inherit from the base `link` style. Patterns are auto-sorted longest-first.
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.
@@ -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 -> _selectionMenuConfig);
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