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
@@ -19,6 +19,8 @@
19
19
  #import "ENRMMathContainerView.h"
20
20
  #endif
21
21
  #import "ENRMCodeBlockContainerView.h"
22
+ #import "ENRMLinkLayout.h"
23
+ #import "ENRMSelectionChange.h"
22
24
  #import "ENRMSpoilerCapable.h"
23
25
  #import "ENRMSpoilerOverlayView.h"
24
26
  #import "ENRMSpoilerTapUtils.h"
@@ -73,8 +75,10 @@ static char kENRMSegmentFadeAnimatorKey;
73
75
  + (ENRMMd4cFlags *)flagsFromProps:(const EnrichedMarkdownMd4cFlagsStruct &)props;
74
76
  - (void)emitLinkPress:(NSString *)url;
75
77
  - (void)emitLinkLongPress:(NSString *)url;
78
+ - (void)emitLinkLayoutIfNeeded;
76
79
  - (void)emitTaskListItemPress:(NSInteger)index checked:(BOOL)checked text:(NSString *)text;
77
80
  - (void)emitCopyPress:(NSString *)code language:(NSString *)language;
81
+ - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole;
78
82
  - (void)emitContextMenuItemPress:(NSString *)itemText
79
83
  selectedText:(NSString *)selectedText
80
84
  selectionStart:(NSUInteger)selectionStart
@@ -103,6 +107,7 @@ static char kENRMSegmentFadeAnimatorKey;
103
107
  BOOL _allowTrailingMargin;
104
108
  BOOL _selectable;
105
109
  BOOL _enableLinkPreview;
110
+ BOOL _linkLongPressSelects;
106
111
  BOOL _enableTaskListItemToggle;
107
112
  BOOL _contextMenuHidden;
108
113
  BOOL _streamingAnimation;
@@ -122,6 +127,21 @@ static char kENRMSegmentFadeAnimatorKey;
122
127
 
123
128
  ENRMSpoilerOverlay _spoilerOverlay;
124
129
 
130
+ BOOL _emitLinkLayout;
131
+ NSArray<NSDictionary *> *_lastLinkLayout;
132
+
133
+ ENRMSelectionChangeReporter *_selectionReporter;
134
+ #if !TARGET_OS_OSX
135
+ ENRMSelectionGestureTracker *_selectionGestures;
136
+ #endif
137
+ // At most one segment holds a selection; a new one clears the previous.
138
+ __weak ENRMPlatformTextView *_selectionOwner;
139
+ // A block long-pressed while contextMenuHidden is on, reported as the selection.
140
+ __weak RCTUIView *_activeBlockView;
141
+ NSString *_activeBlockSource;
142
+ NSString *_activeBlockMarkdown;
143
+ NSString *_activeBlockText;
144
+
125
145
  NSLineBreakStrategy _lineBreakStrategy;
126
146
 
127
147
  ENRMWritingDirectionMode _writingDirectionMode;
@@ -173,7 +193,7 @@ static char kENRMSegmentFadeAnimatorKey;
173
193
  _tableStreamingMode = ENRMTableStreamingModeProgressive;
174
194
  _codeBlockStreamingMode = ENRMCodeBlockStreamingModeProgressive;
175
195
  _contextMenuHidden = NO;
176
- _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES};
196
+ _selectionMenuConfig = ENRMDefaultSelectionMenuConfig();
177
197
  _lineBreakStrategy = NSLineBreakStrategyNone;
178
198
  _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
179
199
  _resolvedLayoutDirection =
@@ -193,10 +213,132 @@ static char kENRMSegmentFadeAnimatorKey;
193
213
  [strongSelf renderMarkdownContent:strongSelf->_cachedMarkdown];
194
214
  }
195
215
  };
216
+
217
+ _selectionReporter = [[ENRMSelectionChangeReporter alloc]
218
+ initWithSnapshot:^NSDictionary * {
219
+ EnrichedMarkdown *strongSelf = weakSelf;
220
+ return strongSelf ? [strongSelf selectionPayload] : ENRMInactiveSelectionPayload();
221
+ }
222
+ emit:^(NSDictionary *payload) {
223
+ EnrichedMarkdown *strongSelf = weakSelf;
224
+ if (!strongSelf)
225
+ return;
226
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(strongSelf->_eventEmitter);
227
+ if (emitter)
228
+ emitter->onSelectionChange(
229
+ ENRMSelectionChangeEventFromPayload<EnrichedMarkdownEventEmitter::OnSelectionChange>(payload));
230
+ }];
231
+ #if !TARGET_OS_OSX
232
+ // A handle drag starting or ending is reported right away, even when the
233
+ // selection itself is unchanged.
234
+ _selectionGestures = [[ENRMSelectionGestureTracker alloc] initWithChange:^{
235
+ EnrichedMarkdown *strongSelf = weakSelf;
236
+ if (!strongSelf)
237
+ return;
238
+ UITextView *owner = strongSelf->_selectionOwner;
239
+ if (owner)
240
+ [strongSelf->_selectionGestures trackTextView:owner];
241
+ [strongSelf->_selectionReporter forgetLastReported];
242
+ [strongSelf->_selectionReporter flushNow];
243
+ }];
244
+ #endif
196
245
  }
197
246
  return self;
198
247
  }
199
248
 
249
+ #pragma mark - Selection
250
+
251
+ /// The current selection (or active block) as an `onSelectionChange` payload.
252
+ /// Markdown and `whole` follow the same rules as the intercepted Copy.
253
+ - (NSDictionary *)selectionPayload
254
+ {
255
+ RCTUIView *blockView = _activeBlockView;
256
+ if (blockView != nil) {
257
+ if (blockView.superview != self)
258
+ return ENRMInactiveSelectionPayload();
259
+ return ENRMBlockSelectionPayload(_activeBlockSource, _activeBlockMarkdown, _activeBlockText, blockView.frame);
260
+ }
261
+
262
+ #if !TARGET_OS_OSX
263
+ UITextView *owner = _selectionOwner;
264
+ if (owner == nil || ![owner isDescendantOfView:self])
265
+ return ENRMInactiveSelectionPayload();
266
+ NSDictionary *selection = ENRMTextSelectionInTextView(owner, self);
267
+ if (!selection)
268
+ return ENRMInactiveSelectionPayload();
269
+
270
+ NSString *text = selection[ENRMSelectionTextKey];
271
+ // Segments only see their own text, so a selection is the whole component
272
+ // only when it spans the sole segment; the markdown is then the original.
273
+ BOOL whole = [selection[ENRMSelectionFullKey] boolValue] && _segmentViews.count == 1 && _cachedMarkdown.length > 0;
274
+ NSString *markdown =
275
+ whole ? _cachedMarkdown : extractMarkdownFromAttributedString(owner.attributedText, owner.selectedRange);
276
+ NSMutableDictionary *payload = [selection mutableCopy];
277
+ [payload removeObjectForKey:ENRMSelectionFullKey];
278
+ payload[ENRMSelectionActiveKey] = @YES;
279
+ payload[ENRMSelectionSourceKey] = @"selection";
280
+ payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
281
+ payload[ENRMSelectionWholeKey] = @(whole);
282
+ payload[ENRMSelectionAdjustingKey] = @(_selectionGestures.adjusting);
283
+ return payload;
284
+ #else
285
+ return ENRMInactiveSelectionPayload();
286
+ #endif
287
+ }
288
+
289
+ - (void)clearActiveBlock
290
+ {
291
+ _activeBlockView = nil;
292
+ _activeBlockSource = nil;
293
+ _activeBlockMarkdown = nil;
294
+ _activeBlockText = nil;
295
+ }
296
+
297
+ - (void)clearSegmentSelection:(ENRMPlatformTextView *)textView
298
+ {
299
+ #if !TARGET_OS_OSX
300
+ textView.selectedRange = NSMakeRange(0, 0);
301
+ [textView resignFirstResponder];
302
+ #endif
303
+ }
304
+
305
+ - (void)clearSelection
306
+ {
307
+ ENRMPlatformTextView *owner = _selectionOwner;
308
+ _selectionOwner = nil;
309
+ [self clearActiveBlock];
310
+ if (owner)
311
+ [self clearSegmentSelection:owner];
312
+ [_selectionReporter flushNow];
313
+ }
314
+
315
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
316
+ {
317
+ RCTEnrichedMarkdownHandleCommand(self, commandName, args);
318
+ }
319
+
320
+ - (ENRMBlockCopyRequestBlock)blockMenuSuppressedForSource:(NSString *)source view:(RCTUIView *)view
321
+ {
322
+ __weak EnrichedMarkdown *weakSelf = self;
323
+ __weak RCTUIView *weakView = view;
324
+ return ^(NSString *markdown, NSString *text) {
325
+ EnrichedMarkdown *strongSelf = weakSelf;
326
+ RCTUIView *strongView = weakView;
327
+ if (!strongSelf || !strongView || !strongSelf->_selectionReporter.enabled)
328
+ return;
329
+ ENRMPlatformTextView *owner = strongSelf->_selectionOwner;
330
+ strongSelf->_selectionOwner = nil;
331
+ if (owner)
332
+ [strongSelf clearSegmentSelection:owner];
333
+ strongSelf->_activeBlockView = strongView;
334
+ strongSelf->_activeBlockSource = source;
335
+ strongSelf->_activeBlockMarkdown = markdown;
336
+ strongSelf->_activeBlockText = text;
337
+ [strongSelf->_selectionReporter forgetLastReported];
338
+ [strongSelf->_selectionReporter flushNow];
339
+ };
340
+ }
341
+
200
342
  - (void)configureSegmentViewRegistry
201
343
  {
202
344
  __weak EnrichedMarkdown *weakSelf = self;
@@ -505,9 +647,30 @@ static char kENRMSegmentFadeAnimatorKey;
505
647
  }
506
648
  }
507
649
 
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.
650
+ - (ENRMBlockMenuOptions)blockMenuOptions
651
+ {
652
+ return (ENRMBlockMenuOptions){
653
+ .copyHidden = !_selectionMenuConfig.copyEnabled,
654
+ .copyWritesMarkdown = _selectionMenuConfig.copyFormatMarkdown,
655
+ .copyAsMarkdownHidden = !_selectionMenuConfig.copyAsMarkdown,
656
+ .copyButtonHidden = !_selectionMenuConfig.codeBlockCopyButton,
657
+ .interceptCopy = _selectionMenuConfig.interceptCopy,
658
+ };
659
+ }
660
+
661
+ - (ENRMBlockCopyRequestBlock)blockCopyRequestForSource:(NSString *)source
662
+ {
663
+ __weak EnrichedMarkdown *weakSelf = self;
664
+ return ^(NSString *markdown, NSString *text) {
665
+ EnrichedMarkdown *strongSelf = weakSelf;
666
+ if (strongSelf)
667
+ [strongSelf emitCopyRequest:source markdown:markdown text:text whole:YES];
668
+ };
669
+ }
670
+
671
+ // Table, math and code block views cache the copy labels and menu options at
672
+ // creation time, so re-push them on prop updates (e.g. a language change
673
+ // without a remount) to avoid stale values.
511
674
  - (void)pushSelectionMenuLabelsToSegments
512
675
  {
513
676
  for (RCTUIView *segment in _segmentViews) {
@@ -516,6 +679,7 @@ static char kENRMSegmentFadeAnimatorKey;
516
679
  tableView.copyLabel = _selectionMenuLabels.copyLabel;
517
680
  tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
518
681
  tableView.contextMenuHidden = _contextMenuHidden;
682
+ tableView.menuOptions = [self blockMenuOptions];
519
683
  }
520
684
  #if ENRICHED_MARKDOWN_MATH
521
685
  else if ([segment isKindOfClass:[ENRMMathContainerView class]]) {
@@ -523,6 +687,7 @@ static char kENRMSegmentFadeAnimatorKey;
523
687
  mathView.copyLabel = _selectionMenuLabels.copyLabel;
524
688
  mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
525
689
  mathView.contextMenuHidden = _contextMenuHidden;
690
+ mathView.menuOptions = [self blockMenuOptions];
526
691
  }
527
692
  #endif
528
693
  else if ([segment isKindOfClass:[ENRMCodeBlockContainerView class]]) {
@@ -530,6 +695,7 @@ static char kENRMSegmentFadeAnimatorKey;
530
695
  codeBlockView.copyLabel = _selectionMenuLabels.copyLabel;
531
696
  codeBlockView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
532
697
  codeBlockView.contextMenuHidden = _contextMenuHidden;
698
+ codeBlockView.menuOptions = [self blockMenuOptions];
533
699
  }
534
700
  }
535
701
  }
@@ -767,6 +933,7 @@ static char kENRMSegmentFadeAnimatorKey;
767
933
 
768
934
  #if !TARGET_OS_OSX
769
935
  view.textView.delegate = self;
936
+ [_selectionGestures trackTextView:view.textView];
770
937
  #else
771
938
  __weak EnrichedMarkdown *weakSelf = self;
772
939
  [view setContextMenuProvider:^NSMenu *_Nullable(NSMenu *baseMenu, NSTextView *textView) {
@@ -807,6 +974,9 @@ static char kENRMSegmentFadeAnimatorKey;
807
974
  tableView.copyLabel = _selectionMenuLabels.copyLabel;
808
975
  tableView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
809
976
  tableView.contextMenuHidden = _contextMenuHidden;
977
+ tableView.menuOptions = [self blockMenuOptions];
978
+ tableView.onCopyRequest = [self blockCopyRequestForSource:@"table"];
979
+ tableView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"table" view:tableView];
810
980
 
811
981
  __weak EnrichedMarkdown *weakSelf = self;
812
982
 
@@ -847,6 +1017,9 @@ static char kENRMSegmentFadeAnimatorKey;
847
1017
  mathView.copyLabel = _selectionMenuLabels.copyLabel;
848
1018
  mathView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
849
1019
  mathView.contextMenuHidden = _contextMenuHidden;
1020
+ mathView.menuOptions = [self blockMenuOptions];
1021
+ mathView.onCopyRequest = [self blockCopyRequestForSource:@"math"];
1022
+ mathView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"math" view:mathView];
850
1023
  [mathView applyLatex:mathSegment.latex];
851
1024
  return mathView;
852
1025
  }
@@ -858,6 +1031,9 @@ static char kENRMSegmentFadeAnimatorKey;
858
1031
  codeBlockView.copyLabel = _selectionMenuLabels.copyLabel;
859
1032
  codeBlockView.copyAsMarkdownLabel = _selectionMenuLabels.copyAsMarkdownLabel;
860
1033
  codeBlockView.contextMenuHidden = _contextMenuHidden;
1034
+ codeBlockView.menuOptions = [self blockMenuOptions];
1035
+ codeBlockView.onCopyRequest = [self blockCopyRequestForSource:@"code"];
1036
+ codeBlockView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"code" view:codeBlockView];
861
1037
 
862
1038
  __weak EnrichedMarkdown *weakSelf = self;
863
1039
  codeBlockView.onCopyPress = ^(NSString *code, NSString *language) {
@@ -910,6 +1086,9 @@ static char kENRMSegmentFadeAnimatorKey;
910
1086
  {
911
1087
  [super layoutSubviews];
912
1088
  [self computeSegmentLayoutForWidth:self.bounds.size.width applyFrames:YES];
1089
+ [self emitLinkLayoutIfNeeded];
1090
+ // Segments may have moved or been replaced; re-read the selection (deduplicated).
1091
+ [_selectionReporter schedule];
913
1092
  }
914
1093
 
915
1094
  - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
@@ -945,6 +1124,14 @@ static char kENRMSegmentFadeAnimatorKey;
945
1124
 
946
1125
  _selectable = newViewProps.selectable;
947
1126
 
1127
+ if (newViewProps.emitLinkLayout != _emitLinkLayout) {
1128
+ _emitLinkLayout = newViewProps.emitLinkLayout;
1129
+ _lastLinkLayout = nil;
1130
+ [self setNeedsLayout];
1131
+ }
1132
+
1133
+ _selectionReporter.enabled = newViewProps.emitSelectionChange;
1134
+
948
1135
  for (RCTUIView *segment in _segmentViews) {
949
1136
  if ([segment isKindOfClass:[EnrichedMarkdownInternalText class]]) {
950
1137
  EnrichedMarkdownInternalText *textSegment = (EnrichedMarkdownInternalText *)segment;
@@ -986,6 +1173,7 @@ static char kENRMSegmentFadeAnimatorKey;
986
1173
  }
987
1174
 
988
1175
  _enableLinkPreview = newViewProps.enableLinkPreview;
1176
+ _linkLongPressSelects = newViewProps.linkLongPressSelects;
989
1177
  _enableTaskListItemToggle = newViewProps.enableTaskListItemToggle;
990
1178
  _contextMenuHidden = newViewProps.contextMenuHidden;
991
1179
 
@@ -1026,9 +1214,7 @@ static char kENRMSegmentFadeAnimatorKey;
1026
1214
  }
1027
1215
 
1028
1216
  _selectionMenuLabels = ENRMParseSelectionMenuLabels(newViewProps.selectionMenuConfig);
1029
- _selectionMenuConfig =
1030
- ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig.copyAsMarkdown,
1031
- newViewProps.selectionMenuConfig.copyImageUrl);
1217
+ _selectionMenuConfig = ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig);
1032
1218
  [self pushSelectionMenuLabelsToSegments];
1033
1219
 
1034
1220
  if (ENRMAccessibilityLabelsChanged(oldViewProps.accessibilityLabels, newViewProps.accessibilityLabels)) {
@@ -1142,6 +1328,14 @@ static char kENRMSegmentFadeAnimatorKey;
1142
1328
  _cachedMarkdown = nil;
1143
1329
  _renderedMarkdown = nil;
1144
1330
  _config = nil;
1331
+ _emitLinkLayout = NO;
1332
+ _lastLinkLayout = nil;
1333
+ _selectionReporter.enabled = NO;
1334
+ #if !TARGET_OS_OSX
1335
+ [_selectionGestures reset];
1336
+ #endif
1337
+ _selectionOwner = nil;
1338
+ [self clearActiveBlock];
1145
1339
  _md4cFlags = [EnrichedMarkdown flagsFromProps:resetProps->md4cFlags];
1146
1340
  _maxFontSizeMultiplier = 0;
1147
1341
  _allowTrailingMargin = NO;
@@ -1252,6 +1446,40 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1252
1446
  emitter->onLinkPress({.url = std::string(url.UTF8String)});
1253
1447
  }
1254
1448
 
1449
+ /// Reports link rects of every text segment (offset by the segment origin) to
1450
+ /// JS for `renderLinkDecoration`. Tables are skipped: their cells scroll
1451
+ /// horizontally, so rects would go stale. Runs after each segment layout pass;
1452
+ /// deduplicated so an unchanged layout sends nothing.
1453
+ - (void)emitLinkLayoutIfNeeded
1454
+ {
1455
+ if (!_emitLinkLayout || self.bounds.size.width <= 0)
1456
+ return;
1457
+
1458
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
1459
+ if (!emitter)
1460
+ return;
1461
+
1462
+ NSMutableArray<NSDictionary *> *fragments = [NSMutableArray array];
1463
+ NSInteger linkCount = 0;
1464
+ for (RCTUIView *segment in _segmentViews) {
1465
+ if ([segment isKindOfClass:[TableContainerView class]]) {
1466
+ linkCount += ((TableContainerView *)segment).linkCount;
1467
+ continue;
1468
+ }
1469
+ if (![segment isKindOfClass:[EnrichedMarkdownInternalText class]])
1470
+ continue;
1471
+ NSInteger segmentLinks = 0;
1472
+ [fragments addObjectsFromArray:ENRMCollectLinkLayoutFragments(((EnrichedMarkdownInternalText *)segment).textView,
1473
+ segment.frame.origin, linkCount, &segmentLinks)];
1474
+ linkCount += segmentLinks;
1475
+ }
1476
+
1477
+ if (_lastLinkLayout != nil && [fragments isEqualToArray:_lastLinkLayout])
1478
+ return;
1479
+ _lastLinkLayout = [fragments copy];
1480
+ emitter->onLinkLayout(ENRMLinkLayoutEventFromFragments<EnrichedMarkdownEventEmitter::OnLinkLayout>(fragments));
1481
+ }
1482
+
1255
1483
  - (void)emitLinkLongPress:(NSString *)url
1256
1484
  {
1257
1485
  auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
@@ -1274,6 +1502,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1274
1502
  {.code = std::string(code.UTF8String ?: ""), .language = std::string(language.UTF8String ?: "")});
1275
1503
  }
1276
1504
 
1505
+ - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole
1506
+ {
1507
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
1508
+ if (emitter)
1509
+ emitter->onCopyRequest({
1510
+ .source = std::string(source.UTF8String),
1511
+ .markdown = std::string(markdown.UTF8String ?: ""),
1512
+ .text = std::string(text.UTF8String ?: ""),
1513
+ .whole = (bool)whole,
1514
+ });
1515
+ }
1516
+
1277
1517
  - (void)emitContextMenuItemPress:(NSString *)itemText
1278
1518
  selectedText:(NSString *)selectedText
1279
1519
  selectionStart:(NSUInteger)selectionStart
@@ -1317,14 +1557,35 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1317
1557
  ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { [self emitLinkPress:url]; });
1318
1558
  }
1319
1559
 
1560
+ #if !TARGET_OS_OSX
1561
+ - (void)textViewDidChangeSelection:(UITextView *)textView
1562
+ {
1563
+ [_selectionGestures trackTextView:textView];
1564
+ ENRMDismissSelectionEditMenuIfMoved(textView);
1565
+ if (textView.selectedRange.length > 0) {
1566
+ // Take ownership first so the previous owner's own "cleared" callback is ignored.
1567
+ ENRMPlatformTextView *previous = _selectionOwner;
1568
+ _selectionOwner = textView;
1569
+ [self clearActiveBlock];
1570
+ if (previous && previous != textView)
1571
+ previous.selectedRange = NSMakeRange(0, 0);
1572
+ [_selectionReporter schedule];
1573
+ } else if (textView == _selectionOwner) {
1574
+ [_selectionReporter selectionChanged:NO];
1575
+ }
1576
+ }
1577
+ #endif
1578
+
1320
1579
  // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
1321
1580
  #if !TARGET_OS_OSX
1322
1581
  - (UIMenu *)textView:(UITextView *)textView
1323
1582
  editMenuForTextInRange:(NSRange)range
1324
1583
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
1325
1584
  {
1585
+ // An empty menu presents nothing; nil would make UIKit show its default menu
1586
+ // (the selection and handles stay either way).
1326
1587
  if (_contextMenuHidden) {
1327
- return nil;
1588
+ return [UIMenu menuWithChildren:@[]];
1328
1589
  }
1329
1590
 
1330
1591
  __weak EnrichedMarkdown *weakSelf = self;
@@ -1341,8 +1602,47 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1341
1602
  ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler);
1342
1603
 
1343
1604
  NSString *segmentMarkdown = extractMarkdownFromAttributedString(textView.attributedText, range);
1605
+
1606
+ // Segments only see their own text, so a selection is the whole component
1607
+ // only when it spans the sole segment; the markdown is then the original.
1608
+ ENRMCopyRequestHandler onCopyRequest = ^(NSString *markdown, NSString *text, BOOL fullSelection) {
1609
+ EnrichedMarkdown *strongSelf = weakSelf;
1610
+ if (!strongSelf)
1611
+ return;
1612
+ BOOL whole = fullSelection && strongSelf->_segmentViews.count == 1 && strongSelf->_cachedMarkdown.length > 0;
1613
+ [strongSelf emitCopyRequest:@"selection"
1614
+ markdown:whole ? strongSelf->_cachedMarkdown : markdown
1615
+ text:text
1616
+ whole:whole];
1617
+ };
1618
+
1344
1619
  return buildEditMenuForSelection(textView.attributedText, range, segmentMarkdown, _config, suggestedActions,
1345
- customActions, _selectionMenuConfig);
1620
+ customActions, _selectionMenuConfig, onCopyRequest);
1621
+ }
1622
+
1623
+ // Attachments other than images (inline math) are text, not items: without
1624
+ // this a long press on one in an unfocused text view starts UIKit's text item
1625
+ // interaction, which presents nothing and leaves the view unable to select.
1626
+ - (BOOL)textView:(UITextView *)textView
1627
+ shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment
1628
+ inRange:(NSRange)characterRange
1629
+ interaction:(UITextItemInteraction)interaction
1630
+ {
1631
+ if ([textAttachment isKindOfClass:[ENRMImageAttachment class]]) {
1632
+ return YES;
1633
+ }
1634
+ // A long press selects the attachment like a word, as it does once the view is focused.
1635
+ if (interaction == UITextItemInteractionPresentActions) {
1636
+ __weak UITextView *weakTextView = textView;
1637
+ dispatch_async(dispatch_get_main_queue(), ^{
1638
+ UITextView *strongTextView = weakTextView;
1639
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
1640
+ return;
1641
+ }
1642
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
1643
+ });
1644
+ }
1645
+ return NO;
1346
1646
  }
1347
1647
 
1348
1648
  - (BOOL)textView:(UITextView *)textView
@@ -1356,6 +1656,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1356
1656
 
1357
1657
  NSString *urlString = linkURLAtRange(textView, characterRange);
1358
1658
 
1659
+ if (urlString && _linkLongPressSelects) {
1660
+ __weak UITextView *weakTextView = textView;
1661
+ dispatch_async(dispatch_get_main_queue(), ^{
1662
+ UITextView *strongTextView = weakTextView;
1663
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
1664
+ return;
1665
+ }
1666
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
1667
+ });
1668
+ return NO;
1669
+ }
1670
+
1359
1671
  if (!urlString || _enableLinkPreview) {
1360
1672
  return YES;
1361
1673
  }