react-native-enriched-markdown-scaffold 1.0.2-scaffold.1 → 1.0.2-scaffold.10

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 (163) hide show
  1. package/android/build.gradle +2 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +18 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +4 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +18 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +4 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +30 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +210 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +176 -2
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +45 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +35 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +112 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +34 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +46 -8
  16. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
  18. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +43 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +25 -3
  20. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +7 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
  22. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
  23. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
  24. package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
  27. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +16 -1
  28. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
  29. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
  30. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +3 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +51 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +34 -2
  33. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
  34. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/HeadlessActionMode.kt +88 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +53 -14
  38. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +232 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +64 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +29 -3
  41. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +30 -7
  42. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +1 -0
  43. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +13 -6
  44. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathMeasureHelper.kt +2 -1
  45. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +25 -9
  46. package/docs/API_REFERENCE.md +183 -4
  47. package/docs/COPY_OPTIONS.md +40 -0
  48. package/docs/INPUT.md +3 -1
  49. package/docs/MENTIONS.md +8 -1
  50. package/docs/STYLES.md +27 -0
  51. package/docs/TEXT.md +2 -0
  52. package/docs/WEB.md +1 -0
  53. package/ios/EnrichedMarkdown.mm +321 -9
  54. package/ios/EnrichedMarkdownText.mm +203 -6
  55. package/ios/attachments/ENRMMathInlineAttachment+macOS.m +2 -1
  56. package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
  57. package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
  58. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
  59. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
  60. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +30 -0
  61. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +210 -0
  62. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  63. package/ios/internals/MeasurementCache.h +8 -2
  64. package/ios/math/ENRMRaTeXBridge.swift +23 -5
  65. package/ios/renderer/ENRMMathInlineRenderer.m +1 -0
  66. package/ios/renderer/LinkRenderer.m +47 -15
  67. package/ios/styles/StyleConfig.h +20 -0
  68. package/ios/styles/StyleConfig.mm +84 -0
  69. package/ios/utils/CodeBackground.m +3 -65
  70. package/ios/utils/ENRMBlockMenu.h +66 -0
  71. package/ios/utils/ENRMChipDrawing.h +24 -0
  72. package/ios/utils/ENRMChipDrawing.m +83 -0
  73. package/ios/utils/ENRMLinkChip.h +70 -0
  74. package/ios/utils/ENRMLinkChip.m +213 -0
  75. package/ios/utils/ENRMLinkLayout.h +50 -0
  76. package/ios/utils/ENRMLinkLayout.m +45 -0
  77. package/ios/utils/ENRMSelectionChange.h +120 -0
  78. package/ios/utils/ENRMSelectionChange.m +282 -0
  79. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  80. package/ios/utils/EditMenuUtils.h +35 -6
  81. package/ios/utils/EditMenuUtils.m +171 -15
  82. package/ios/utils/HTMLGenerator.m +2 -1
  83. package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
  84. package/ios/utils/MarkdownExtractor.m +2 -1
  85. package/ios/utils/RTFExportUtils.m +2 -0
  86. package/ios/utils/StylePropsUtils.h +47 -1
  87. package/ios/utils/TextViewLayoutManager.mm +3 -0
  88. package/ios/views/ENRMCodeBlockContainerView.h +16 -2
  89. package/ios/views/ENRMCodeBlockContainerView.m +22 -19
  90. package/ios/views/ENRMMathContainerView.h +11 -0
  91. package/ios/views/ENRMMathContainerView.m +17 -20
  92. package/ios/views/TableContainerView.h +14 -0
  93. package/ios/views/TableContainerView.m +34 -18
  94. package/lib/module/EnrichedMarkdownNativeComponent.ts +138 -1
  95. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +138 -1
  96. package/lib/module/index.js.map +1 -1
  97. package/lib/module/jest/index.js +14 -0
  98. package/lib/module/jest/index.js.map +1 -1
  99. package/lib/module/native/EnrichedMarkdownText.js +169 -11
  100. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  101. package/lib/module/normalizeMarkdownStyle.js +17 -4
  102. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  103. package/lib/module/normalizeMarkdownStyle.web.js +17 -4
  104. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  105. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  106. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  107. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  108. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  109. package/lib/module/web/styles.js +18 -0
  110. package/lib/module/web/styles.js.map +1 -1
  111. package/lib/module/web/utils.js +13 -0
  112. package/lib/module/web/utils.js.map +1 -1
  113. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +123 -2
  114. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  115. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +123 -2
  116. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  117. package/lib/typescript/src/index.d.ts +2 -2
  118. package/lib/typescript/src/index.d.ts.map +1 -1
  119. package/lib/typescript/src/index.web.d.ts +1 -1
  120. package/lib/typescript/src/index.web.d.ts.map +1 -1
  121. package/lib/typescript/src/jest/index.d.ts +1 -1
  122. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  123. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  124. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  125. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  126. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  127. package/lib/typescript/src/types/MarkdownStyle.d.ts +50 -5
  128. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  129. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +12 -0
  130. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  131. package/lib/typescript/src/types/MarkdownTextProps.d.ts +144 -11
  132. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  133. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  134. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  135. package/lib/typescript/src/types/events.d.ts +66 -0
  136. package/lib/typescript/src/types/events.d.ts.map +1 -1
  137. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  138. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  139. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  140. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  141. package/lib/typescript/src/web/types.d.ts +4 -0
  142. package/lib/typescript/src/web/types.d.ts.map +1 -1
  143. package/lib/typescript/src/web/utils.d.ts +4 -0
  144. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  145. package/package.json +1 -1
  146. package/src/EnrichedMarkdownNativeComponent.ts +138 -1
  147. package/src/EnrichedMarkdownTextNativeComponent.ts +138 -1
  148. package/src/index.tsx +6 -0
  149. package/src/index.web.tsx +5 -1
  150. package/src/jest/index.tsx +22 -1
  151. package/src/native/EnrichedMarkdownText.tsx +220 -9
  152. package/src/normalizeMarkdownStyle.ts +15 -0
  153. package/src/normalizeMarkdownStyle.web.ts +13 -0
  154. package/src/types/MarkdownStyle.ts +51 -5
  155. package/src/types/MarkdownStyleInternal.ts +12 -0
  156. package/src/types/MarkdownTextProps.ts +151 -11
  157. package/src/types/MarkdownTextProps.web.ts +43 -1
  158. package/src/types/events.ts +69 -0
  159. package/src/web/EnrichedMarkdownText.tsx +10 -3
  160. package/src/web/renderers/InlineRenderers.tsx +111 -3
  161. package/src/web/styles.ts +25 -0
  162. package/src/web/types.ts +4 -0
  163. package/src/web/utils.ts +8 -0
@@ -16,6 +16,7 @@ class MathInlineSpan(
16
16
  private val textColor: Int,
17
17
  private val paddingVertical: Float,
18
18
  private val fontWeight: String = "normal",
19
+ private val glyphOverflow: Float = 0f,
19
20
  ) : ReplacementSpan() {
20
21
  private var cachedBitmap: Bitmap? = null
21
22
  private var cachedWidth = 0
@@ -31,18 +32,24 @@ class MathInlineSpan(
31
32
  val displayList = RaTeXEngine.parseBlocking(latex, displayMode = false, color = textColor)
32
33
  val renderer = RaTeXRenderer(displayList, fontSize, ratexTypefaceLoader(fontWeight))
33
34
 
34
- cachedWidth = renderer.widthPx.toInt().coerceAtLeast(1)
35
+ cachedWidth = ceil(renderer.widthPx).toInt().coerceAtLeast(1)
35
36
  mathAscent = renderer.heightPx
36
37
  mathDescent = renderer.depthPx
37
38
 
39
+ // RaTeX reports TeX metric boxes, which digit and capital outlines (and the
40
+ // fake-bold stroke) overshoot; the bitmap keeps glyphOverflow px of headroom
41
+ // on both sides so that ink is not cut off.
42
+ val overflowPx = glyphOverflow.toInt()
38
43
  val bitmap =
39
44
  Bitmap.createBitmap(
40
45
  cachedWidth,
41
- ceil(renderer.totalHeightPx).toInt().coerceAtLeast(1),
46
+ ceil(renderer.totalHeightPx).toInt().coerceAtLeast(1) + overflowPx * 2,
42
47
  Bitmap.Config.ARGB_8888,
43
48
  )
44
49
 
45
- renderer.draw(Canvas(bitmap))
50
+ val canvas = Canvas(bitmap)
51
+ canvas.translate(0f, overflowPx.toFloat())
52
+ renderer.draw(canvas)
46
53
  cachedBitmap = bitmap
47
54
  } catch (_: Exception) {
48
55
  renderFailed = true
@@ -66,9 +73,9 @@ class MathInlineSpan(
66
73
  // Must stay identical to MathInlinePlaceholderSpan.getSize: that span measures
67
74
  // this one off the main thread, and a box that disagrees by even a pixel either
68
75
  // clips the formula or eats the padding this field is here to add.
69
- ascent = -ceil(mathAscent + paddingVertical).toInt()
76
+ ascent = -ceil(mathAscent + (paddingVertical + glyphOverflow)).toInt()
70
77
  top = ascent
71
- descent = ceil(mathDescent + paddingVertical).toInt()
78
+ descent = ceil(mathDescent + (paddingVertical + glyphOverflow)).toInt()
72
79
  bottom = descent
73
80
  }
74
81
 
@@ -88,7 +95,7 @@ class MathInlineSpan(
88
95
  ) {
89
96
  prepareResources()
90
97
  cachedBitmap?.let {
91
- val bitmapY = y - ceil(mathAscent)
98
+ val bitmapY = y - ceil(mathAscent) - glyphOverflow
92
99
  canvas.drawBitmap(it, x, bitmapY, paint)
93
100
  }
94
101
  }
@@ -5,6 +5,7 @@ import android.util.Log
5
5
  import io.ratex.RaTeXEngine
6
6
  import io.ratex.RaTeXFontLoader
7
7
  import io.ratex.RaTeXRenderer
8
+ import kotlin.math.ceil
8
9
 
9
10
  object MathMeasureHelper {
10
11
  @JvmStatic
@@ -37,7 +38,7 @@ object MathMeasureHelper {
37
38
  }
38
39
 
39
40
  return MathMetrics(
40
- renderer.widthPx.toInt(),
41
+ ceil(renderer.widthPx).toInt().coerceAtLeast(1),
41
42
  renderer.heightPx,
42
43
  renderer.depthPx,
43
44
  )
@@ -42,6 +42,11 @@ class MathContainerView(
42
42
  var copyLabel: String = ""
43
43
  var copyAsMarkdownLabel: String = ""
44
44
  var contextMenuHidden: Boolean = false
45
+ var menuOptions: BlockMenuOptions = BlockMenuOptions()
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
45
50
 
46
51
  override val segmentMarginTop: Int get() = mathStyle.marginTop.toInt()
47
52
  override val segmentMarginBottom: Int get() = mathStyle.marginBottom.toInt()
@@ -53,7 +58,7 @@ class MathContainerView(
53
58
  else -> Gravity.CENTER_HORIZONTAL
54
59
  }
55
60
 
56
- private val mathView = RaTeXCanvasView(context)
61
+ private val mathView = RaTeXCanvasView(context, mathStyle.glyphOverflow)
57
62
 
58
63
  init {
59
64
  setBackgroundColor(mathStyle.backgroundColor)
@@ -118,20 +123,27 @@ class MathContainerView(
118
123
  }
119
124
 
120
125
  private fun showContextMenu(anchor: View) {
121
- if (contextMenuHidden) return
126
+ if (contextMenuHidden) {
127
+ onMenuSuppressed?.invoke("$$\n$cachedLatex\n$$", cachedLatex)
128
+ return
129
+ }
122
130
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
123
131
  ContextMenuPopup.show(anchor, this) {
124
- item(ContextMenuPopup.Icon.COPY, copyLabel) {
132
+ blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = {
133
+ "$$\n$cachedLatex\n$$"
134
+ }, rawText = { cachedLatex }, copyRaw = {
125
135
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", cachedLatex))
126
- }
127
- item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) {
136
+ }, copyMarkdown = {
128
137
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", "$$\n$cachedLatex\n$$"))
129
- }
138
+ })
130
139
  }
131
140
  }
132
141
 
142
+ // glyphOverflow px of headroom above and below the RaTeX box: digit and capital
143
+ // outlines (and the fake-bold stroke) overshoot it, and this view clips to its size.
133
144
  private class RaTeXCanvasView(
134
145
  context: Context,
146
+ private val glyphOverflow: Float,
135
147
  ) : View(context) {
136
148
  var renderer: RaTeXRenderer? = null
137
149
 
@@ -144,12 +156,16 @@ class MathContainerView(
144
156
  ?: return setMeasuredDimension(0, 0)
145
157
  setMeasuredDimension(
146
158
  ceil(currentRenderer.widthPx).toInt().coerceAtLeast(1),
147
- ceil(currentRenderer.totalHeightPx).toInt().coerceAtLeast(1),
159
+ ceil(currentRenderer.totalHeightPx).toInt().coerceAtLeast(1) + (glyphOverflow * 2).toInt(),
148
160
  )
149
161
  }
150
162
 
151
163
  override fun onDraw(canvas: Canvas) {
152
- renderer?.draw(canvas)
164
+ val currentRenderer = renderer ?: return
165
+ canvas.save()
166
+ canvas.translate(0f, glyphOverflow)
167
+ currentRenderer.draw(canvas)
168
+ canvas.restore()
153
169
  }
154
170
  }
155
171
 
@@ -166,7 +182,7 @@ class MathContainerView(
166
182
  mode = MathRenderMode.Display,
167
183
  )
168
184
  val metrics = MathMeasureHelper.measure(context, listOf(request)).first()
169
- return ceil(metrics.ascent + metrics.descent).toInt() + (mathStyle.padding * 2)
185
+ return ceil(metrics.ascent + metrics.descent).toInt() + (mathStyle.padding * 2) + (mathStyle.glyphOverflow * 2)
170
186
  }
171
187
 
172
188
  private const val TAG = "MathContainerView"
@@ -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,112 @@ 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
+
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
+ /** `true` while a selection handle is dragged (iOS; always `false` on Android). */
252
+ adjusting: boolean;
253
+ }
254
+ ```
255
+
256
+ - 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.
257
+ - 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.
258
+ - 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.
259
+ - On iOS the selection isn't reported while a handle is dragged: the event fires with `adjusting: true` (and the selection as it was) when the drag begins, and with `adjusting: false` and the new selection when the finger lifts. Hide your menu while `adjusting` is `true`. Android has no such flag (`adjusting` is always `false`) but reports every step of the drag, so debounce before showing the menu there too.
260
+ - 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.
261
+ - Ignored on macOS and web.
262
+
263
+ **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`.
264
+
265
+ ```tsx
266
+ const ref = useRef<EnrichedMarkdownTextInstance>(null);
267
+
268
+ <EnrichedMarkdownText
269
+ ref={ref}
270
+ markdown={content}
271
+ contextMenuHidden
272
+ onSelectionChange={(e) => {
273
+ if (!e.active || e.adjusting) return hideMenu();
274
+ ref.current?.measureInWindow((x, y) =>
275
+ showMenuDebounced({ x: x + e.startRect.x, y: y + e.startRect.y }, e.markdown)
276
+ );
277
+ }}
278
+ />;
279
+
280
+ // After the menu action:
281
+ ref.current?.clearSelection();
282
+ ```
283
+
125
284
  ### `enableLinkPreview`
126
285
 
127
286
  Controls the native link preview on long press (iOS only). Automatically set to `false` when `onLinkLongPress` is provided.
@@ -142,6 +301,16 @@ You can also control this behavior explicitly without providing a handler:
142
301
  />
143
302
  ```
144
303
 
304
+ ### `linkLongPressSelects`
305
+
306
+ Long-pressing a link selects its label and shows the selection edit menu (the same one a manual selection gets), instead of the system link preview (iOS only). Takes precedence over `enableLinkPreview` and `onLinkLongPress`.
307
+
308
+ | Type | Default Value | Platform |
309
+ | --------- | ------------- | -------- |
310
+ | `boolean` | `false` | iOS |
311
+
312
+ Only the link label is selected, not the chip padding; the selection handles can be dragged to extend it. Long-pressing inline math always behaves this way, without the prop.
313
+
145
314
  ### `selectable`
146
315
 
147
316
  Whether text can be selected.
@@ -180,6 +349,8 @@ Notes:
180
349
  prop suppresses it the same way.
181
350
  - Applies to `EnrichedMarkdownText` only — it does not affect
182
351
  `EnrichedMarkdownTextInput`.
352
+ - To show your own menu instead, use [`onSelectionChange`](#onselectionchange);
353
+ with this prop on, block long-presses are reported through it as well.
183
354
 
184
355
  ### `selectionColor`
185
356
 
@@ -469,7 +640,7 @@ Controls built-in actions added to the native text selection menu. Custom app-pr
469
640
  | -------------------- | ---------------------------------------------- | -------- |
470
641
  | `SelectionMenuConfig` | `{}` (see shape below for per-field defaults) | iOS, Android, macOS |
471
642
 
472
- Each item takes an object: `{ enabled }` toggles visibility (the system `copy` item can't be hidden — only relabeled) and `label` overrides the English default. The labels apply to the main text selection menu as well as the table and math block copy menus.
643
+ Each item takes an object: `{ enabled }` toggles visibility and `label` overrides the English default. Items and labels apply to the main text selection menu as well as the table, math and code block copy menus. `copy.enabled`, `copy.format` and `systemItems` are ignored on macOS.
473
644
 
474
645
  > **Deprecation:** the previous boolean shape (`copyAsMarkdown: false`) is still accepted at runtime for backward compatibility but logs a one-time warning. It will be removed in 0.8 — migrate to `{ enabled: false }`.
475
646
 
@@ -477,9 +648,13 @@ Each item takes an object: `{ enabled }` toggles visibility (the system `copy` i
477
648
 
478
649
  ```ts
479
650
  interface SelectionMenuConfig {
480
- /** System "Copy" item — can't be hidden, only relabeled. @default { label: "Copy" } */
481
- copy?: { label?: string };
482
- /** "Copy as Markdown" action. @default { enabled: true, label: "Copy as Markdown" } */
651
+ /**
652
+ * "Copy" item. `format: 'markdown'` copies Markdown as plain text instead of
653
+ * rich text (text selections) or the raw content (blocks).
654
+ * @default { enabled: true, label: "Copy", format: "rich" }
655
+ */
656
+ copy?: { enabled?: boolean; label?: string; format?: 'rich' | 'markdown' };
657
+ /** "Copy as Markdown" action, in the text and block menus. @default { enabled: true, label: "Copy as Markdown" } */
483
658
  copyAsMarkdown?: { enabled?: boolean; label?: string };
484
659
  /** "Copy Image URL" action, shown when the selection contains images. */
485
660
  copyImageUrl?: {
@@ -489,6 +664,10 @@ interface SelectionMenuConfig {
489
664
  /** Forms for multiple images, chosen with Intl.PluralRules. @default { other: "Copy {count} Image URLs" } */
490
665
  pluralLabels?: SelectionMenuPluralLabels;
491
666
  };
667
+ /** `'none'` drops every system item (Select All, Look Up, Share, Translate…). @default 'default' */
668
+ systemItems?: 'default' | 'none';
669
+ /** Copy button in the code block header. @default { enabled: true } */
670
+ codeBlockCopyButton?: { enabled?: boolean };
492
671
  }
493
672
 
494
673
  interface SelectionMenuPluralLabels {
@@ -44,6 +44,46 @@ Use `selectionMenuConfig` to hide built-in selection menu actions while keeping
44
44
  />
45
45
  ```
46
46
 
47
+ ### Copy as Markdown only, no system items
48
+
49
+ `copy` can also be hidden (`{ enabled: false }`) or switched to Markdown with
50
+ `format: 'markdown'`, and `systemItems: 'none'` drops everything the OS adds
51
+ (Select All, Look Up, Share, Translate, app text actions…):
52
+
53
+ ```tsx
54
+ <EnrichedMarkdownText
55
+ markdown={content}
56
+ selectionMenuConfig={{
57
+ copy: { format: 'markdown' }, // inline math → $…$, plain text only
58
+ copyAsMarkdown: { enabled: false },
59
+ systemItems: 'none',
60
+ codeBlockCopyButton: { enabled: false },
61
+ }}
62
+ />
63
+ ```
64
+
65
+ The same `copy` and `copyAsMarkdown` settings drive the long-press menus on
66
+ table, math and code blocks: with `format: 'markdown'` their Copy item writes
67
+ the block's Markdown (`$$…$$`, a fenced code block, a Markdown table). On code
68
+ blocks, the header copy button and the VoiceOver copy action always copy the raw
69
+ code and fire `onCopyPress`; `codeBlockCopyButton: { enabled: false }` hides the
70
+ button, and `copy: { enabled: false }` also removes the VoiceOver action.
71
+ `copy.enabled`, `copy.format` and `systemItems` are ignored on macOS.
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
+
47
87
  To turn the menu off entirely rather than item by item, use `contextMenuHidden`:
48
88
 
49
89
  ```tsx
package/docs/INPUT.md CHANGED
@@ -283,11 +283,13 @@ Because nothing is written until you resolve, an async decision is safe: the sel
283
283
  | | Behavior |
284
284
  | --- | --- |
285
285
  | **iOS** | Context menu and hardware ⌘V. `markdown` is set when the copy source was an `EnrichedMarkdownTextInput`. |
286
- | **Android** | Context menu, hardware Ctrl+V and `KEYCODE_PASTE`. `markdown` is always `''`, and `resolvePaste` inserts `text` verbatim — so an intercepted copy-then-paste inside the app lands as plain text, even though the un-intercepted paste restores the formatting. |
286
+ | **Android** | Context menu, hardware Ctrl+V, `KEYCODE_PASTE`, and a keyboard clipboard chip (Gboard, Samsung Keyboard) — see below. `markdown` is always `''`, and `resolvePaste` inserts `text` verbatim — so an intercepted copy-then-paste inside the app lands as plain text, even though the un-intercepted paste restores the formatting. |
287
287
  | **macOS** | `interceptPaste` is ignored, `onPaste` never fires, and pastes insert inline as usual. |
288
288
 
289
289
  Only clipboard content is intercepted. Drag-and-drop, autofill and IME content insertion bypass `onPaste` on both platforms and insert normally — on Android they are filtered out explicitly, so an autofill never arrives looking like a paste.
290
290
 
291
+ On Android a keyboard clipboard chip does not paste through the menu: the keyboard types the text in through the input connection. The input reports it as a paste when the committed text is exactly the current clipboard text and is at least 16 characters long or spans more than one line. Shorter single-line chip pastes, entries from the keyboard's clipboard history other than the current clip, and text the keyboard changes on the way insert inline without `onPaste`.
292
+
291
293
  ## Style Detection
292
294
 
293
295
  All of the above styles can be detected with the use of [onChangeState](API_REFERENCE.md#onchangestate) callback payload.
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.
package/docs/WEB.md CHANGED
@@ -37,6 +37,7 @@ The web implementation also exports `WebMarkdownTextProps` which extends `Enrich
37
37
  |---|---|
38
38
  | `flavor` | The web renderer always uses full GFM capabilities. On native, `flavor` controls whether a single `TextView` (CommonMark) or container-based renderer (GitHub) is used; the DOM has no such constraint. |
39
39
  | `enableLinkPreview` | iOS-only feature (native link preview on long press). |
40
+ | `linkLongPressSelects` | iOS-only feature (selects a link on long press instead of the link preview). |
40
41
  | `allowFontScaling` / `maxFontSizeMultiplier` | React Native text scaling props. Browsers handle font scaling natively via OS accessibility settings. |
41
42
  | `streamingAnimation` | Native-only tail fade-in animation. Not yet implemented on web. |
42
43
  | `streamingConfig` | Native-only streaming table configuration. Not yet implemented on web. |