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

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 (55) hide show
  1. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  2. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +18 -0
  3. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +60 -0
  4. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +31 -2
  5. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +4 -0
  6. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +5 -0
  7. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +5 -0
  8. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +4 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +23 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +50 -13
  12. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +64 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +19 -3
  14. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +7 -5
  15. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +6 -4
  16. package/docs/API_REFERENCE.md +60 -4
  17. package/docs/COPY_OPTIONS.md +40 -0
  18. package/ios/EnrichedMarkdown.mm +63 -8
  19. package/ios/EnrichedMarkdownText.mm +26 -5
  20. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  21. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +18 -0
  22. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +60 -0
  23. package/ios/utils/ENRMBlockMenu.h +66 -0
  24. package/ios/utils/EditMenuUtils.h +32 -6
  25. package/ios/utils/EditMenuUtils.m +51 -14
  26. package/ios/views/ENRMCodeBlockContainerView.h +13 -2
  27. package/ios/views/ENRMCodeBlockContainerView.m +19 -19
  28. package/ios/views/ENRMMathContainerView.h +8 -0
  29. package/ios/views/ENRMMathContainerView.m +10 -18
  30. package/ios/views/TableContainerView.h +8 -0
  31. package/ios/views/TableContainerView.m +10 -18
  32. package/lib/module/EnrichedMarkdownNativeComponent.ts +24 -0
  33. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +24 -0
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/native/EnrichedMarkdownText.js +29 -2
  36. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  37. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +17 -0
  38. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  39. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +17 -0
  40. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  41. package/lib/typescript/src/index.d.ts +1 -1
  42. package/lib/typescript/src/index.d.ts.map +1 -1
  43. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +3 -3
  44. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  45. package/lib/typescript/src/types/MarkdownTextProps.d.ts +53 -10
  46. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  47. package/lib/typescript/src/types/events.d.ts +22 -0
  48. package/lib/typescript/src/types/events.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/EnrichedMarkdownNativeComponent.ts +24 -0
  51. package/src/EnrichedMarkdownTextNativeComponent.ts +24 -0
  52. package/src/index.tsx +1 -0
  53. package/src/native/EnrichedMarkdownText.tsx +37 -1
  54. package/src/types/MarkdownTextProps.ts +50 -10
  55. package/src/types/events.ts +23 -0
@@ -64,6 +64,7 @@ class TableContainerView(
64
64
  var copyLabel: String = ""
65
65
  var copyAsMarkdownLabel: String = ""
66
66
  var contextMenuHidden: Boolean = false
67
+ var menuOptions: BlockMenuOptions = BlockMenuOptions()
67
68
 
68
69
  private val scrollView =
69
70
  HorizontalScrollView(context).apply {
@@ -366,8 +367,8 @@ class TableContainerView(
366
367
  if (contextMenuHidden) return
367
368
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
368
369
  ContextMenuPopup.show(anchor, this) {
369
- item(ContextMenuPopup.Icon.COPY, copyLabel) {
370
- val plainText = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
370
+ blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = { tableMarkdown }, rawText = ::buildPlainText, copyRaw = {
371
+ val plainText = buildPlainText()
371
372
  if (plainText.isNotEmpty()) {
372
373
  val displayMetrics = context.resources.displayMetrics
373
374
  val tableRows =
@@ -377,13 +378,14 @@ class TableContainerView(
377
378
  val html = HTMLGenerator.generateTableHTML(tableRows, styleConfig, displayMetrics.scaledDensity, displayMetrics.density)
378
379
  clipboard.setPrimaryClip(ClipData.newHtmlText("Table", plainText, html))
379
380
  }
380
- }
381
- item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) {
381
+ }, copyMarkdown = {
382
382
  if (tableMarkdown.isNotEmpty()) clipboard.setPrimaryClip(ClipData.newPlainText("Table", tableMarkdown))
383
- }
383
+ })
384
384
  }
385
385
  }
386
386
 
387
+ private fun buildPlainText(): String = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
388
+
387
389
  private fun buildMarkdownFromRows(): String =
388
390
  rows.joinToString("") { row ->
389
391
  val line = "| ${row.joinToString(" | ") { it.markdownText }} |\n"
@@ -42,6 +42,7 @@ class MathContainerView(
42
42
  var copyLabel: String = ""
43
43
  var copyAsMarkdownLabel: String = ""
44
44
  var contextMenuHidden: Boolean = false
45
+ var menuOptions: BlockMenuOptions = BlockMenuOptions()
45
46
 
46
47
  override val segmentMarginTop: Int get() = mathStyle.marginTop.toInt()
47
48
  override val segmentMarginBottom: Int get() = mathStyle.marginBottom.toInt()
@@ -121,12 +122,13 @@ class MathContainerView(
121
122
  if (contextMenuHidden) return
122
123
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
123
124
  ContextMenuPopup.show(anchor, this) {
124
- item(ContextMenuPopup.Icon.COPY, copyLabel) {
125
+ blockCopyItems(menuOptions, copyLabel, copyAsMarkdownLabel, markdown = {
126
+ "$$\n$cachedLatex\n$$"
127
+ }, rawText = { cachedLatex }, copyRaw = {
125
128
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", cachedLatex))
126
- }
127
- item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) {
129
+ }, copyMarkdown = {
128
130
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", "$$\n$cachedLatex\n$$"))
129
- }
131
+ })
130
132
  }
131
133
  }
132
134
 
@@ -122,6 +122,54 @@ Only fires when `flavor="github"` — the copy button is part of the GitHub flav
122
122
  />
123
123
  ```
124
124
 
125
+ ### `onCopy`
126
+
127
+ 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.
128
+
129
+ | Type | Default Value | Platform |
130
+ | ----------------------------- | ------------- | -------- |
131
+ | `(event: CopyEvent) => void` | - | Both |
132
+
133
+ ```ts
134
+ interface CopyEvent {
135
+ /** Which menu the Copy item belongs to. */
136
+ source: 'selection' | 'math' | 'table' | 'code';
137
+ /** What `copy.format: 'markdown'` would write. */
138
+ markdown: string;
139
+ /** Rendered text of the selection, or the block's raw content. */
140
+ text: string;
141
+ /** The selection spans the whole component (`markdown` is the source). Always true for blocks. */
142
+ whole: boolean;
143
+ }
144
+ ```
145
+
146
+ | `source` | `markdown` | `text` |
147
+ | ----------- | -------------------------------------------------------------------------- | ----------------------------- |
148
+ | `selection` | Original source when `whole`, otherwise rebuilt from the rendered styles | Selected text as rendered |
149
+ | `math` | `$$…$$` | LaTeX |
150
+ | `table` | The table as Markdown | Tab-separated cells |
151
+ | `code` | Fenced code block | Raw code |
152
+
153
+ - `onCopy` overrides `selectionMenuConfig.copy.format`; `copy.enabled: false` still hides the item, so no event fires.
154
+ - **Copy as Markdown**, the code block header copy button and the VoiceOver copy action are unaffected. `onCopyPress` does not fire for an intercepted Copy.
155
+ - 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.
156
+ - A rebuilt `markdown` wraps each styled run separately (`**a *****b***** c**`) and does not escape literal `*` / `_`.
157
+ - Copying with a hardware keyboard (Cmd+C / Ctrl+C) does not go through the menu and is not intercepted.
158
+ - Ignored on macOS. On web the prop is not this API: it reaches the container `div` as the DOM `onCopy` clipboard event.
159
+
160
+ **Example:**
161
+
162
+ ```tsx
163
+ import * as Clipboard from 'expo-clipboard';
164
+
165
+ <EnrichedMarkdownText
166
+ markdown={content}
167
+ onCopy={({ source, markdown }) => {
168
+ Clipboard.setStringAsync(source === 'selection' ? stripFormatting(markdown) : markdown);
169
+ }}
170
+ />
171
+ ```
172
+
125
173
  ### `enableLinkPreview`
126
174
 
127
175
  Controls the native link preview on long press (iOS only). Automatically set to `false` when `onLinkLongPress` is provided.
@@ -469,7 +517,7 @@ Controls built-in actions added to the native text selection menu. Custom app-pr
469
517
  | -------------------- | ---------------------------------------------- | -------- |
470
518
  | `SelectionMenuConfig` | `{}` (see shape below for per-field defaults) | iOS, Android, macOS |
471
519
 
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.
520
+ 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
521
 
474
522
  > **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
523
 
@@ -477,9 +525,13 @@ Each item takes an object: `{ enabled }` toggles visibility (the system `copy` i
477
525
 
478
526
  ```ts
479
527
  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" } */
528
+ /**
529
+ * "Copy" item. `format: 'markdown'` copies Markdown as plain text instead of
530
+ * rich text (text selections) or the raw content (blocks).
531
+ * @default { enabled: true, label: "Copy", format: "rich" }
532
+ */
533
+ copy?: { enabled?: boolean; label?: string; format?: 'rich' | 'markdown' };
534
+ /** "Copy as Markdown" action, in the text and block menus. @default { enabled: true, label: "Copy as Markdown" } */
483
535
  copyAsMarkdown?: { enabled?: boolean; label?: string };
484
536
  /** "Copy Image URL" action, shown when the selection contains images. */
485
537
  copyImageUrl?: {
@@ -489,6 +541,10 @@ interface SelectionMenuConfig {
489
541
  /** Forms for multiple images, chosen with Intl.PluralRules. @default { other: "Copy {count} Image URLs" } */
490
542
  pluralLabels?: SelectionMenuPluralLabels;
491
543
  };
544
+ /** `'none'` drops every system item (Select All, Look Up, Share, Translate…). @default 'default' */
545
+ systemItems?: 'default' | 'none';
546
+ /** Copy button in the code block header. @default { enabled: true } */
547
+ codeBlockCopyButton?: { enabled?: boolean };
492
548
  }
493
549
 
494
550
  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
@@ -75,6 +75,7 @@ static char kENRMSegmentFadeAnimatorKey;
75
75
  - (void)emitLinkLongPress:(NSString *)url;
76
76
  - (void)emitTaskListItemPress:(NSInteger)index checked:(BOOL)checked text:(NSString *)text;
77
77
  - (void)emitCopyPress:(NSString *)code language:(NSString *)language;
78
+ - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole;
78
79
  - (void)emitContextMenuItemPress:(NSString *)itemText
79
80
  selectedText:(NSString *)selectedText
80
81
  selectionStart:(NSUInteger)selectionStart
@@ -173,7 +174,7 @@ static char kENRMSegmentFadeAnimatorKey;
173
174
  _tableStreamingMode = ENRMTableStreamingModeProgressive;
174
175
  _codeBlockStreamingMode = ENRMCodeBlockStreamingModeProgressive;
175
176
  _contextMenuHidden = NO;
176
- _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES};
177
+ _selectionMenuConfig = ENRMDefaultSelectionMenuConfig();
177
178
  _lineBreakStrategy = NSLineBreakStrategyNone;
178
179
  _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
179
180
  _resolvedLayoutDirection =
@@ -505,9 +506,30 @@ static char kENRMSegmentFadeAnimatorKey;
505
506
  }
506
507
  }
507
508
 
508
- // Table and math views cache the copy labels at creation time, so re-push them
509
- // on prop updates (e.g. a language change without a remount) to avoid stale
510
- // labels. Only the copy/copy-as-markdown labels apply to these block menus.
509
+ - (ENRMBlockMenuOptions)blockMenuOptions
510
+ {
511
+ return (ENRMBlockMenuOptions){
512
+ .copyHidden = !_selectionMenuConfig.copyEnabled,
513
+ .copyWritesMarkdown = _selectionMenuConfig.copyFormatMarkdown,
514
+ .copyAsMarkdownHidden = !_selectionMenuConfig.copyAsMarkdown,
515
+ .copyButtonHidden = !_selectionMenuConfig.codeBlockCopyButton,
516
+ .interceptCopy = _selectionMenuConfig.interceptCopy,
517
+ };
518
+ }
519
+
520
+ - (ENRMBlockCopyRequestBlock)blockCopyRequestForSource:(NSString *)source
521
+ {
522
+ __weak EnrichedMarkdown *weakSelf = self;
523
+ return ^(NSString *markdown, NSString *text) {
524
+ EnrichedMarkdown *strongSelf = weakSelf;
525
+ if (strongSelf)
526
+ [strongSelf emitCopyRequest:source markdown:markdown text:text whole:YES];
527
+ };
528
+ }
529
+
530
+ // Table, math and code block views cache the copy labels and menu options at
531
+ // creation time, so re-push them on prop updates (e.g. a language change
532
+ // without a remount) to avoid stale values.
511
533
  - (void)pushSelectionMenuLabelsToSegments
512
534
  {
513
535
  for (RCTUIView *segment in _segmentViews) {
@@ -516,6 +538,7 @@ static char kENRMSegmentFadeAnimatorKey;
516
538
  tableView.copyLabel = _selectionMenuLabels.copyLabel;
517
539
  tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
518
540
  tableView.contextMenuHidden = _contextMenuHidden;
541
+ tableView.menuOptions = [self blockMenuOptions];
519
542
  }
520
543
  #if ENRICHED_MARKDOWN_MATH
521
544
  else if ([segment isKindOfClass:[ENRMMathContainerView class]]) {
@@ -523,6 +546,7 @@ static char kENRMSegmentFadeAnimatorKey;
523
546
  mathView.copyLabel = _selectionMenuLabels.copyLabel;
524
547
  mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
525
548
  mathView.contextMenuHidden = _contextMenuHidden;
549
+ mathView.menuOptions = [self blockMenuOptions];
526
550
  }
527
551
  #endif
528
552
  else if ([segment isKindOfClass:[ENRMCodeBlockContainerView class]]) {
@@ -530,6 +554,7 @@ static char kENRMSegmentFadeAnimatorKey;
530
554
  codeBlockView.copyLabel = _selectionMenuLabels.copyLabel;
531
555
  codeBlockView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
532
556
  codeBlockView.contextMenuHidden = _contextMenuHidden;
557
+ codeBlockView.menuOptions = [self blockMenuOptions];
533
558
  }
534
559
  }
535
560
  }
@@ -807,6 +832,8 @@ static char kENRMSegmentFadeAnimatorKey;
807
832
  tableView.copyLabel = _selectionMenuLabels.copyLabel;
808
833
  tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
809
834
  tableView.contextMenuHidden = _contextMenuHidden;
835
+ tableView.menuOptions = [self blockMenuOptions];
836
+ tableView.onCopyRequest = [self blockCopyRequestForSource:@"table"];
810
837
 
811
838
  __weak EnrichedMarkdown *weakSelf = self;
812
839
 
@@ -847,6 +874,8 @@ static char kENRMSegmentFadeAnimatorKey;
847
874
  mathView.copyLabel = _selectionMenuLabels.copyLabel;
848
875
  mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
849
876
  mathView.contextMenuHidden = _contextMenuHidden;
877
+ mathView.menuOptions = [self blockMenuOptions];
878
+ mathView.onCopyRequest = [self blockCopyRequestForSource:@"math"];
850
879
  [mathView applyLatex:mathSegment.latex];
851
880
  return mathView;
852
881
  }
@@ -858,6 +887,8 @@ static char kENRMSegmentFadeAnimatorKey;
858
887
  codeBlockView.copyLabel = _selectionMenuLabels.copyLabel;
859
888
  codeBlockView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
860
889
  codeBlockView.contextMenuHidden = _contextMenuHidden;
890
+ codeBlockView.menuOptions = [self blockMenuOptions];
891
+ codeBlockView.onCopyRequest = [self blockCopyRequestForSource:@"code"];
861
892
 
862
893
  __weak EnrichedMarkdown *weakSelf = self;
863
894
  codeBlockView.onCopyPress = ^(NSString *code, NSString *language) {
@@ -1026,9 +1057,7 @@ static char kENRMSegmentFadeAnimatorKey;
1026
1057
  }
1027
1058
 
1028
1059
  _selectionMenuLabels = ENRMParseSelectionMenuLabels(newViewProps.selectionMenuConfig);
1029
- _selectionMenuConfig =
1030
- ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig.copyAsMarkdown,
1031
- newViewProps.selectionMenuConfig.copyImageUrl);
1060
+ _selectionMenuConfig = ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig);
1032
1061
  [self pushSelectionMenuLabelsToSegments];
1033
1062
 
1034
1063
  if (ENRMAccessibilityLabelsChanged(oldViewProps.accessibilityLabels, newViewProps.accessibilityLabels)) {
@@ -1274,6 +1303,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1274
1303
  {.code = std::string(code.UTF8String ?: ""), .language = std::string(language.UTF8String ?: "")});
1275
1304
  }
1276
1305
 
1306
+ - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole
1307
+ {
1308
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
1309
+ if (emitter)
1310
+ emitter->onCopyRequest({
1311
+ .source = std::string(source.UTF8String),
1312
+ .markdown = std::string(markdown.UTF8String ?: ""),
1313
+ .text = std::string(text.UTF8String ?: ""),
1314
+ .whole = (bool)whole,
1315
+ });
1316
+ }
1317
+
1277
1318
  - (void)emitContextMenuItemPress:(NSString *)itemText
1278
1319
  selectedText:(NSString *)selectedText
1279
1320
  selectionStart:(NSUInteger)selectionStart
@@ -1341,8 +1382,22 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1341
1382
  ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler);
1342
1383
 
1343
1384
  NSString *segmentMarkdown = extractMarkdownFromAttributedString(textView.attributedText, range);
1385
+
1386
+ // Segments only see their own text, so a selection is the whole component
1387
+ // only when it spans the sole segment; the markdown is then the original.
1388
+ ENRMCopyRequestHandler onCopyRequest = ^(NSString *markdown, NSString *text, BOOL fullSelection) {
1389
+ EnrichedMarkdown *strongSelf = weakSelf;
1390
+ if (!strongSelf)
1391
+ return;
1392
+ BOOL whole = fullSelection && strongSelf->_segmentViews.count == 1 && strongSelf->_cachedMarkdown.length > 0;
1393
+ [strongSelf emitCopyRequest:@"selection"
1394
+ markdown:whole ? strongSelf->_cachedMarkdown : markdown
1395
+ text:text
1396
+ whole:whole];
1397
+ };
1398
+
1344
1399
  return buildEditMenuForSelection(textView.attributedText, range, segmentMarkdown, _config, suggestedActions,
1345
- customActions, _selectionMenuConfig);
1400
+ customActions, _selectionMenuConfig, onCopyRequest);
1346
1401
  }
1347
1402
 
1348
1403
  - (BOOL)textView:(UITextView *)textView
@@ -61,6 +61,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
61
61
  selectedText:(NSString *)selectedText
62
62
  selectionStart:(NSUInteger)selectionStart
63
63
  selectionEnd:(NSUInteger)selectionEnd;
64
+ - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole;
64
65
  @end
65
66
 
66
67
  @implementation EnrichedMarkdownText {
@@ -239,7 +240,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
239
240
  _enableTaskListItemToggle = YES;
240
241
  _forceHeightUpdateOnNextRender = NO;
241
242
  _contextMenuHidden = NO;
242
- _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES};
243
+ _selectionMenuConfig = ENRMDefaultSelectionMenuConfig();
243
244
  _lineBreakStrategy = NSLineBreakStrategyNone;
244
245
  _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
245
246
  _resolvedLayoutDirection =
@@ -577,9 +578,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
577
578
  }
578
579
 
579
580
  _selectionMenuLabels = ENRMParseSelectionMenuLabels(newViewProps.selectionMenuConfig);
580
- _selectionMenuConfig =
581
- ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig.copyAsMarkdown,
582
- newViewProps.selectionMenuConfig.copyImageUrl);
581
+ _selectionMenuConfig = ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig);
583
582
 
584
583
  if (ENRMAccessibilityLabelsChanged(oldViewProps.accessibilityLabels, newViewProps.accessibilityLabels)) {
585
584
  _accessibilityLabels = [[ENRMAccessibilityLabels alloc] init];
@@ -750,6 +749,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
750
749
  emitter->onTaskListItemPress({.index = (int)index, .checked = checked, .text = std::string(text.UTF8String ?: "")});
751
750
  }
752
751
 
752
+ - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole
753
+ {
754
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(_eventEmitter);
755
+ if (emitter)
756
+ emitter->onCopyRequest({
757
+ .source = std::string(source.UTF8String),
758
+ .markdown = std::string(markdown.UTF8String ?: ""),
759
+ .text = std::string(text.UTF8String ?: ""),
760
+ .whole = (bool)whole,
761
+ });
762
+ }
763
+
753
764
  - (void)emitContextMenuItemPress:(NSString *)itemText
754
765
  selectedText:(NSString *)selectedText
755
766
  selectionStart:(NSUInteger)selectionStart
@@ -832,8 +843,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
832
843
  NSMutableArray<UIAction *> *customActions =
833
844
  ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler);
834
845
 
846
+ // A full selection already carries the cached (original) markdown.
847
+ ENRMCopyRequestHandler onCopyRequest = ^(NSString *markdown, NSString *text, BOOL fullSelection) {
848
+ EnrichedMarkdownText *strongSelf = weakSelf;
849
+ if (strongSelf)
850
+ [strongSelf emitCopyRequest:@"selection"
851
+ markdown:markdown
852
+ text:text
853
+ whole:fullSelection && strongSelf->_cachedMarkdown.length > 0];
854
+ };
855
+
835
856
  return buildEditMenuForSelection(textView.attributedText, range, _cachedMarkdown, _config, suggestedActions,
836
- customActions, _selectionMenuConfig);
857
+ customActions, _selectionMenuConfig, onCopyRequest);
837
858
  }
838
859
  #endif
839
860
 
@@ -52,6 +52,18 @@ payload.setProperty(runtime, "language", event.language);
52
52
  }
53
53
 
54
54
 
55
+ void EnrichedMarkdownEventEmitter::onCopyRequest(OnCopyRequest event) const {
56
+ dispatchEvent("copyRequest", [event=std::move(event)](jsi::Runtime &runtime) {
57
+ auto payload = jsi::Object(runtime);
58
+ payload.setProperty(runtime, "source", event.source);
59
+ payload.setProperty(runtime, "markdown", event.markdown);
60
+ payload.setProperty(runtime, "text", event.text);
61
+ payload.setProperty(runtime, "whole", event.whole);
62
+ return payload;
63
+ });
64
+ }
65
+
66
+
55
67
  void EnrichedMarkdownEventEmitter::onContextMenuItemPress(OnContextMenuItemPress event) const {
56
68
  dispatchEvent("contextMenuItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
57
69
  auto payload = jsi::Object(runtime);
@@ -379,6 +391,18 @@ payload.setProperty(runtime, "language", event.language);
379
391
  }
380
392
 
381
393
 
394
+ void EnrichedMarkdownTextEventEmitter::onCopyRequest(OnCopyRequest event) const {
395
+ dispatchEvent("copyRequest", [event=std::move(event)](jsi::Runtime &runtime) {
396
+ auto payload = jsi::Object(runtime);
397
+ payload.setProperty(runtime, "source", event.source);
398
+ payload.setProperty(runtime, "markdown", event.markdown);
399
+ payload.setProperty(runtime, "text", event.text);
400
+ payload.setProperty(runtime, "whole", event.whole);
401
+ return payload;
402
+ });
403
+ }
404
+
405
+
382
406
  void EnrichedMarkdownTextEventEmitter::onContextMenuItemPress(OnContextMenuItemPress event) const {
383
407
  dispatchEvent("contextMenuItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
384
408
  auto payload = jsi::Object(runtime);
@@ -36,6 +36,13 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
36
36
  std::string language;
37
37
  };
38
38
 
39
+ struct OnCopyRequest {
40
+ std::string source;
41
+ std::string markdown;
42
+ std::string text;
43
+ bool whole;
44
+ };
45
+
39
46
  struct OnContextMenuItemPress {
40
47
  std::string itemText;
41
48
  std::string selectedText;
@@ -50,6 +57,8 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
50
57
 
51
58
  void onCopyPress(OnCopyPress value) const;
52
59
 
60
+ void onCopyRequest(OnCopyRequest value) const;
61
+
53
62
  void onContextMenuItemPress(OnContextMenuItemPress value) const;
54
63
  };
55
64
  class EnrichedMarkdownTextInputEventEmitter : public ViewEventEmitter {
@@ -303,6 +312,13 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
303
312
  std::string language;
304
313
  };
305
314
 
315
+ struct OnCopyRequest {
316
+ std::string source;
317
+ std::string markdown;
318
+ std::string text;
319
+ bool whole;
320
+ };
321
+
306
322
  struct OnContextMenuItemPress {
307
323
  std::string itemText;
308
324
  std::string selectedText;
@@ -317,6 +333,8 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
317
333
 
318
334
  void onCopyPress(OnCopyPress value) const;
319
335
 
336
+ void onCopyRequest(OnCopyRequest value) const;
337
+
320
338
  void onContextMenuItemPress(OnContextMenuItemPress value) const;
321
339
  };
322
340
  } // namespace facebook::react
@@ -2462,6 +2462,11 @@ struct EnrichedMarkdownSelectionMenuConfigStruct {
2462
2462
  std::string copyImageUrlLabel{};
2463
2463
  std::string copyImageUrlsLabel{};
2464
2464
  std::vector<std::string> copyImageUrlPluralTemplates{};
2465
+ bool copyEnabled{false};
2466
+ bool copyFormatMarkdown{false};
2467
+ bool systemItemsNone{false};
2468
+ bool codeBlockCopyButton{false};
2469
+ bool interceptCopy{false};
2465
2470
 
2466
2471
 
2467
2472
  #ifdef RN_SERIALIZABLE_STATE
@@ -2476,6 +2481,11 @@ struct EnrichedMarkdownSelectionMenuConfigStruct {
2476
2481
  result["copyImageUrlLabel"] = copyImageUrlLabel;
2477
2482
  result["copyImageUrlsLabel"] = copyImageUrlsLabel;
2478
2483
  result["copyImageUrlPluralTemplates"] = ::facebook::react::toDynamic(copyImageUrlPluralTemplates);
2484
+ result["copyEnabled"] = copyEnabled;
2485
+ result["copyFormatMarkdown"] = copyFormatMarkdown;
2486
+ result["systemItemsNone"] = systemItemsNone;
2487
+ result["codeBlockCopyButton"] = codeBlockCopyButton;
2488
+ result["interceptCopy"] = interceptCopy;
2479
2489
  return result;
2480
2490
  }
2481
2491
  #endif
@@ -2512,6 +2522,26 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
2512
2522
  if (tmp_copyImageUrlPluralTemplates != map.end()) {
2513
2523
  fromRawValue(context, tmp_copyImageUrlPluralTemplates->second, result.copyImageUrlPluralTemplates);
2514
2524
  }
2525
+ auto tmp_copyEnabled = map.find("copyEnabled");
2526
+ if (tmp_copyEnabled != map.end()) {
2527
+ fromRawValue(context, tmp_copyEnabled->second, result.copyEnabled);
2528
+ }
2529
+ auto tmp_copyFormatMarkdown = map.find("copyFormatMarkdown");
2530
+ if (tmp_copyFormatMarkdown != map.end()) {
2531
+ fromRawValue(context, tmp_copyFormatMarkdown->second, result.copyFormatMarkdown);
2532
+ }
2533
+ auto tmp_systemItemsNone = map.find("systemItemsNone");
2534
+ if (tmp_systemItemsNone != map.end()) {
2535
+ fromRawValue(context, tmp_systemItemsNone->second, result.systemItemsNone);
2536
+ }
2537
+ auto tmp_codeBlockCopyButton = map.find("codeBlockCopyButton");
2538
+ if (tmp_codeBlockCopyButton != map.end()) {
2539
+ fromRawValue(context, tmp_codeBlockCopyButton->second, result.codeBlockCopyButton);
2540
+ }
2541
+ auto tmp_interceptCopy = map.find("interceptCopy");
2542
+ if (tmp_interceptCopy != map.end()) {
2543
+ fromRawValue(context, tmp_interceptCopy->second, result.interceptCopy);
2544
+ }
2515
2545
  }
2516
2546
 
2517
2547
  static inline std::string toString(const EnrichedMarkdownSelectionMenuConfigStruct &value) {
@@ -6415,6 +6445,11 @@ struct EnrichedMarkdownTextSelectionMenuConfigStruct {
6415
6445
  std::string copyImageUrlLabel{};
6416
6446
  std::string copyImageUrlsLabel{};
6417
6447
  std::vector<std::string> copyImageUrlPluralTemplates{};
6448
+ bool copyEnabled{false};
6449
+ bool copyFormatMarkdown{false};
6450
+ bool systemItemsNone{false};
6451
+ bool codeBlockCopyButton{false};
6452
+ bool interceptCopy{false};
6418
6453
 
6419
6454
 
6420
6455
  #ifdef RN_SERIALIZABLE_STATE
@@ -6429,6 +6464,11 @@ struct EnrichedMarkdownTextSelectionMenuConfigStruct {
6429
6464
  result["copyImageUrlLabel"] = copyImageUrlLabel;
6430
6465
  result["copyImageUrlsLabel"] = copyImageUrlsLabel;
6431
6466
  result["copyImageUrlPluralTemplates"] = ::facebook::react::toDynamic(copyImageUrlPluralTemplates);
6467
+ result["copyEnabled"] = copyEnabled;
6468
+ result["copyFormatMarkdown"] = copyFormatMarkdown;
6469
+ result["systemItemsNone"] = systemItemsNone;
6470
+ result["codeBlockCopyButton"] = codeBlockCopyButton;
6471
+ result["interceptCopy"] = interceptCopy;
6432
6472
  return result;
6433
6473
  }
6434
6474
  #endif
@@ -6465,6 +6505,26 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
6465
6505
  if (tmp_copyImageUrlPluralTemplates != map.end()) {
6466
6506
  fromRawValue(context, tmp_copyImageUrlPluralTemplates->second, result.copyImageUrlPluralTemplates);
6467
6507
  }
6508
+ auto tmp_copyEnabled = map.find("copyEnabled");
6509
+ if (tmp_copyEnabled != map.end()) {
6510
+ fromRawValue(context, tmp_copyEnabled->second, result.copyEnabled);
6511
+ }
6512
+ auto tmp_copyFormatMarkdown = map.find("copyFormatMarkdown");
6513
+ if (tmp_copyFormatMarkdown != map.end()) {
6514
+ fromRawValue(context, tmp_copyFormatMarkdown->second, result.copyFormatMarkdown);
6515
+ }
6516
+ auto tmp_systemItemsNone = map.find("systemItemsNone");
6517
+ if (tmp_systemItemsNone != map.end()) {
6518
+ fromRawValue(context, tmp_systemItemsNone->second, result.systemItemsNone);
6519
+ }
6520
+ auto tmp_codeBlockCopyButton = map.find("codeBlockCopyButton");
6521
+ if (tmp_codeBlockCopyButton != map.end()) {
6522
+ fromRawValue(context, tmp_codeBlockCopyButton->second, result.codeBlockCopyButton);
6523
+ }
6524
+ auto tmp_interceptCopy = map.find("interceptCopy");
6525
+ if (tmp_interceptCopy != map.end()) {
6526
+ fromRawValue(context, tmp_interceptCopy->second, result.interceptCopy);
6527
+ }
6468
6528
  }
6469
6529
 
6470
6530
  static inline std::string toString(const EnrichedMarkdownTextSelectionMenuConfigStruct &value) {