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
@@ -6,7 +6,9 @@
6
6
  #import "ENRMAtomicSize.h"
7
7
  #import "ENRMContextMenuTextView+macOS.h"
8
8
  #import "ENRMImageAttachment.h"
9
+ #import "ENRMLinkLayout.h"
9
10
  #import "ENRMMarkdownParser.h"
11
+ #import "ENRMSelectionChange.h"
10
12
  #import "ENRMSpoilerOverlayManager.h"
11
13
  #import "ENRMSpoilerTapUtils.h"
12
14
  #import "ENRMTailFadeInAnimator.h"
@@ -25,6 +27,7 @@
25
27
  #import "MarkdownExtractor.h"
26
28
  #import "MeasurementCache.h"
27
29
  #import "ParagraphStyleUtils.h"
30
+ #import "PasteboardUtils.h"
28
31
  #import "RuntimeKeys.h"
29
32
  #import "SelectionColorUtils.h"
30
33
  #import "StylePropsUtils.h"
@@ -56,11 +59,13 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
56
59
  - (void)setupLayoutManager;
57
60
  - (void)emitLinkPress:(NSString *)url;
58
61
  - (void)emitLinkLongPress:(NSString *)url;
62
+ - (void)emitLinkLayoutIfNeeded;
59
63
  - (void)emitTaskListItemPress:(NSInteger)index checked:(BOOL)checked text:(NSString *)text;
60
64
  - (void)emitContextMenuItemPress:(NSString *)itemText
61
65
  selectedText:(NSString *)selectedText
62
66
  selectionStart:(NSUInteger)selectionStart
63
67
  selectionEnd:(NSUInteger)selectionEnd;
68
+ - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole;
64
69
  @end
65
70
 
66
71
  @implementation EnrichedMarkdownText {
@@ -82,6 +87,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
82
87
  CGFloat _lastElementMarginBottom;
83
88
  BOOL _allowTrailingMargin;
84
89
  BOOL _enableLinkPreview;
90
+ BOOL _linkLongPressSelects;
85
91
  BOOL _enableTaskListItemToggle;
86
92
  BOOL _contextMenuHidden;
87
93
  BOOL _streamingAnimation;
@@ -109,6 +115,14 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
109
115
 
110
116
  ENRMSpoilerOverlayManager *_spoilerManager;
111
117
 
118
+ BOOL _emitLinkLayout;
119
+ NSArray<NSDictionary *> *_lastLinkLayout;
120
+
121
+ ENRMSelectionChangeReporter *_selectionReporter;
122
+ #if !TARGET_OS_OSX
123
+ ENRMSelectionGestureTracker *_selectionGestures;
124
+ #endif
125
+
112
126
  NSLineBreakStrategy _lineBreakStrategy;
113
127
 
114
128
  ENRMWritingDirectionMode _writingDirectionMode;
@@ -182,6 +196,9 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
182
196
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
183
197
 
184
198
  _lastCommittedSize.store(CGSizeMake(layoutMetrics.frame.size.width, layoutMetrics.frame.size.height));
199
+ #if TARGET_OS_OSX
200
+ [self emitLinkLayoutIfNeeded];
201
+ #endif
185
202
 
186
203
  NSWritingDirection resolved = _resolvedLayoutDirection;
187
204
  if (layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
@@ -217,6 +234,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
217
234
  if (needsHeightUpdate(measured, self.bounds)) {
218
235
  [self requestHeightUpdate];
219
236
  }
237
+ [self emitLinkLayoutIfNeeded];
220
238
  }
221
239
  }
222
240
 
@@ -239,7 +257,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
239
257
  _enableTaskListItemToggle = YES;
240
258
  _forceHeightUpdateOnNextRender = NO;
241
259
  _contextMenuHidden = NO;
242
- _selectionMenuConfig = (ENRMSelectionMenuConfig){.copyAsMarkdown = YES, .copyImageURL = YES};
260
+ _selectionMenuConfig = ENRMDefaultSelectionMenuConfig();
243
261
  _lineBreakStrategy = NSLineBreakStrategyNone;
244
262
  _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
245
263
  _resolvedLayoutDirection =
@@ -262,11 +280,82 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
262
280
  };
263
281
 
264
282
  [self setupTextView];
283
+ [self setupSelectionReporter];
265
284
  }
266
285
 
267
286
  return self;
268
287
  }
269
288
 
289
+ - (void)setupSelectionReporter
290
+ {
291
+ __weak EnrichedMarkdownText *weakSelf = self;
292
+ _selectionReporter = [[ENRMSelectionChangeReporter alloc]
293
+ initWithSnapshot:^NSDictionary * {
294
+ EnrichedMarkdownText *strongSelf = weakSelf;
295
+ return strongSelf ? [strongSelf selectionPayload] : ENRMInactiveSelectionPayload();
296
+ }
297
+ emit:^(NSDictionary *payload) {
298
+ EnrichedMarkdownText *strongSelf = weakSelf;
299
+ if (!strongSelf)
300
+ return;
301
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(strongSelf->_eventEmitter);
302
+ if (emitter)
303
+ emitter->onSelectionChange(
304
+ ENRMSelectionChangeEventFromPayload<EnrichedMarkdownTextEventEmitter::OnSelectionChange>(payload));
305
+ }];
306
+ #if !TARGET_OS_OSX
307
+ // A handle drag starting or ending is reported right away, even when the
308
+ // selection itself is unchanged.
309
+ _selectionGestures = [[ENRMSelectionGestureTracker alloc] initWithChange:^{
310
+ EnrichedMarkdownText *strongSelf = weakSelf;
311
+ if (!strongSelf)
312
+ return;
313
+ [strongSelf->_selectionGestures trackTextView:strongSelf->_textView];
314
+ [strongSelf->_selectionReporter forgetLastReported];
315
+ [strongSelf->_selectionReporter flushNow];
316
+ }];
317
+ [_selectionGestures trackTextView:_textView];
318
+ #endif
319
+ }
320
+
321
+ /// The current selection as an `onSelectionChange` payload. Markdown and
322
+ /// `whole` follow the same rules as the intercepted Copy (`onCopyRequest`).
323
+ - (NSDictionary *)selectionPayload
324
+ {
325
+ #if !TARGET_OS_OSX
326
+ NSDictionary *selection = ENRMTextSelectionInTextView(_textView, self);
327
+ if (!selection)
328
+ return ENRMInactiveSelectionPayload();
329
+
330
+ NSString *text = selection[ENRMSelectionTextKey];
331
+ NSString *markdown = markdownForRange(_textView.attributedText, _textView.selectedRange, _cachedMarkdown);
332
+ NSMutableDictionary *payload = [selection mutableCopy];
333
+ [payload removeObjectForKey:ENRMSelectionFullKey];
334
+ payload[ENRMSelectionActiveKey] = @YES;
335
+ payload[ENRMSelectionSourceKey] = @"selection";
336
+ payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
337
+ payload[ENRMSelectionWholeKey] = @([selection[ENRMSelectionFullKey] boolValue] && _cachedMarkdown.length > 0);
338
+ payload[ENRMSelectionAdjustingKey] = @(_selectionGestures.adjusting);
339
+ return payload;
340
+ #else
341
+ return ENRMInactiveSelectionPayload();
342
+ #endif
343
+ }
344
+
345
+ - (void)clearSelection
346
+ {
347
+ #if !TARGET_OS_OSX
348
+ _textView.selectedRange = NSMakeRange(0, 0);
349
+ [_textView resignFirstResponder];
350
+ #endif
351
+ [_selectionReporter flushNow];
352
+ }
353
+
354
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
355
+ {
356
+ RCTEnrichedMarkdownTextHandleCommand(self, commandName, args);
357
+ }
358
+
270
359
  - (void)setupTextView
271
360
  {
272
361
  #if !TARGET_OS_OSX
@@ -463,8 +552,13 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
463
552
  if (forceHeightUpdate || needsHeightUpdate(measured, self.bounds)) {
464
553
  [self requestHeightUpdate];
465
554
  }
555
+
556
+ [self emitLinkLayoutIfNeeded];
466
557
  }
467
558
 
559
+ // New text drops the selection; setting attributedText doesn't call the delegate.
560
+ [_selectionReporter schedule];
561
+
468
562
  if (_textView.hidden) {
469
563
  dispatch_async(dispatch_get_main_queue(), ^{ self->_textView.hidden = NO; });
470
564
  }
@@ -483,6 +577,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
483
577
  {
484
578
  [super layoutSubviews];
485
579
  [_spoilerManager updateIfNeeded];
580
+ [self emitLinkLayoutIfNeeded];
486
581
  }
487
582
  #endif
488
583
 
@@ -520,6 +615,14 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
520
615
  }
521
616
  }
522
617
 
618
+ if (newViewProps.emitLinkLayout != _emitLinkLayout) {
619
+ _emitLinkLayout = newViewProps.emitLinkLayout;
620
+ _lastLinkLayout = nil;
621
+ [self emitLinkLayoutIfNeeded];
622
+ }
623
+
624
+ _selectionReporter.enabled = newViewProps.emitSelectionChange;
625
+
523
626
  if (_textView.selectable != newViewProps.selectable) {
524
627
  _textView.selectable = newViewProps.selectable;
525
628
  }
@@ -568,6 +671,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
568
671
  }
569
672
 
570
673
  _enableLinkPreview = newViewProps.enableLinkPreview;
674
+ _linkLongPressSelects = newViewProps.linkLongPressSelects;
571
675
  _enableTaskListItemToggle = newViewProps.enableTaskListItemToggle;
572
676
  _contextMenuHidden = newViewProps.contextMenuHidden;
573
677
 
@@ -577,9 +681,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
577
681
  }
578
682
 
579
683
  _selectionMenuLabels = ENRMParseSelectionMenuLabels(newViewProps.selectionMenuConfig);
580
- _selectionMenuConfig =
581
- ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig.copyAsMarkdown,
582
- newViewProps.selectionMenuConfig.copyImageUrl);
684
+ _selectionMenuConfig = ENRMBuildSelectionMenuConfig(_selectionMenuLabels, newViewProps.selectionMenuConfig);
583
685
 
584
686
  if (ENRMAccessibilityLabelsChanged(oldViewProps.accessibilityLabels, newViewProps.accessibilityLabels)) {
585
687
  _accessibilityLabels = [[ENRMAccessibilityLabels alloc] init];
@@ -698,6 +800,12 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
698
800
  _accessibilityNeedsRebuild = NO;
699
801
  [_spoilerManager removeAllOverlays];
700
802
  _spoilerManager = nil;
803
+ _emitLinkLayout = NO;
804
+ _lastLinkLayout = nil;
805
+ _selectionReporter.enabled = NO;
806
+ #if !TARGET_OS_OSX
807
+ [_selectionGestures reset];
808
+ #endif
701
809
  if (_textView != nil) {
702
810
  ENRMSetAttributedText(_textView, [[NSAttributedString alloc] initWithString:@""]);
703
811
  _textView.hidden = YES;
@@ -743,6 +851,25 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
743
851
  emitter->onLinkLongPress({.url = std::string(url.UTF8String)});
744
852
  }
745
853
 
854
+ /// Reports link rects to JS (for `renderLinkDecoration`). Called after every
855
+ /// render and layout change; deduplicated so an unchanged layout — the common
856
+ /// case during streaming — sends nothing.
857
+ - (void)emitLinkLayoutIfNeeded
858
+ {
859
+ if (!_emitLinkLayout || _renderedMarkdown == nil || self.bounds.size.width <= 0)
860
+ return;
861
+
862
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(_eventEmitter);
863
+ if (!emitter)
864
+ return;
865
+
866
+ NSArray<NSDictionary *> *fragments = ENRMCollectLinkLayoutFragments(_textView, CGPointZero, 0, NULL);
867
+ if (_lastLinkLayout != nil && [fragments isEqualToArray:_lastLinkLayout])
868
+ return;
869
+ _lastLinkLayout = fragments;
870
+ emitter->onLinkLayout(ENRMLinkLayoutEventFromFragments<EnrichedMarkdownTextEventEmitter::OnLinkLayout>(fragments));
871
+ }
872
+
746
873
  - (void)emitTaskListItemPress:(NSInteger)index checked:(BOOL)checked text:(NSString *)text
747
874
  {
748
875
  auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(_eventEmitter);
@@ -750,6 +877,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
750
877
  emitter->onTaskListItemPress({.index = (int)index, .checked = checked, .text = std::string(text.UTF8String ?: "")});
751
878
  }
752
879
 
880
+ - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole
881
+ {
882
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(_eventEmitter);
883
+ if (emitter)
884
+ emitter->onCopyRequest({
885
+ .source = std::string(source.UTF8String),
886
+ .markdown = std::string(markdown.UTF8String ?: ""),
887
+ .text = std::string(text.UTF8String ?: ""),
888
+ .whole = (bool)whole,
889
+ });
890
+ }
891
+
753
892
  - (void)emitContextMenuItemPress:(NSString *)itemText
754
893
  selectedText:(NSString *)selectedText
755
894
  selectionStart:(NSUInteger)selectionStart
@@ -789,6 +928,31 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
789
928
  #pragma mark - UITextViewDelegate (Link Interaction)
790
929
 
791
930
  #if !TARGET_OS_OSX
931
+ // Attachments other than images (inline math) are text, not items: without
932
+ // this a long press on one in an unfocused text view starts UIKit's text item
933
+ // interaction, which presents nothing and leaves the view unable to select.
934
+ - (BOOL)textView:(UITextView *)textView
935
+ shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment
936
+ inRange:(NSRange)characterRange
937
+ interaction:(UITextItemInteraction)interaction
938
+ {
939
+ if ([textAttachment isKindOfClass:[ENRMImageAttachment class]]) {
940
+ return YES;
941
+ }
942
+ // A long press selects the attachment like a word, as it does once the view is focused.
943
+ if (interaction == UITextItemInteractionPresentActions) {
944
+ __weak UITextView *weakTextView = textView;
945
+ dispatch_async(dispatch_get_main_queue(), ^{
946
+ UITextView *strongTextView = weakTextView;
947
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
948
+ return;
949
+ }
950
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
951
+ });
952
+ }
953
+ return NO;
954
+ }
955
+
792
956
  - (BOOL)textView:(ENRMPlatformTextView *)textView
793
957
  shouldInteractWithURL:(NSURL *)URL
794
958
  inRange:(NSRange)characterRange
@@ -800,6 +964,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
800
964
 
801
965
  NSString *urlString = linkURLAtRange(textView, characterRange);
802
966
 
967
+ if (urlString && _linkLongPressSelects) {
968
+ __weak UITextView *weakTextView = textView;
969
+ dispatch_async(dispatch_get_main_queue(), ^{
970
+ UITextView *strongTextView = weakTextView;
971
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
972
+ return;
973
+ }
974
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
975
+ });
976
+ return NO;
977
+ }
978
+
803
979
  if (!urlString || _enableLinkPreview) {
804
980
  return YES;
805
981
  }
@@ -808,6 +984,15 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
808
984
  return NO;
809
985
  }
810
986
 
987
+ #pragma mark - UITextViewDelegate (Selection)
988
+
989
+ - (void)textViewDidChangeSelection:(UITextView *)textView
990
+ {
991
+ [_selectionGestures trackTextView:textView];
992
+ ENRMDismissSelectionEditMenuIfMoved(textView);
993
+ [_selectionReporter selectionChanged:textView.selectedRange.length > 0];
994
+ }
995
+
811
996
  #pragma mark - UITextViewDelegate (Edit Menu)
812
997
 
813
998
  // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
@@ -815,8 +1000,10 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
815
1000
  editMenuForTextInRange:(NSRange)range
816
1001
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
817
1002
  {
1003
+ // An empty menu presents nothing; nil would make UIKit show its default menu
1004
+ // (the selection and handles stay either way).
818
1005
  if (_contextMenuHidden) {
819
- return nil;
1006
+ return [UIMenu menuWithChildren:@[]];
820
1007
  }
821
1008
 
822
1009
  __weak EnrichedMarkdownText *weakSelf = self;
@@ -832,8 +1019,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
832
1019
  NSMutableArray<UIAction *> *customActions =
833
1020
  ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler);
834
1021
 
1022
+ // A full selection already carries the cached (original) markdown.
1023
+ ENRMCopyRequestHandler onCopyRequest = ^(NSString *markdown, NSString *text, BOOL fullSelection) {
1024
+ EnrichedMarkdownText *strongSelf = weakSelf;
1025
+ if (strongSelf)
1026
+ [strongSelf emitCopyRequest:@"selection"
1027
+ markdown:markdown
1028
+ text:text
1029
+ whole:fullSelection && strongSelf->_cachedMarkdown.length > 0];
1030
+ };
1031
+
835
1032
  return buildEditMenuForSelection(textView.attributedText, range, _cachedMarkdown, _config, suggestedActions,
836
- customActions, _selectionMenuConfig);
1033
+ customActions, _selectionMenuConfig, onCopyRequest);
837
1034
  }
838
1035
  #endif
839
1036
 
@@ -29,7 +29,8 @@
29
29
  displayMode:NO
30
30
  fontSize:self.fontSize
31
31
  color:color
32
- bold:self.bold];
32
+ bold:self.bold
33
+ glyphOverflow:self.glyphOverflow];
33
34
  if (!result)
34
35
  return;
35
36
 
@@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
14
14
  /// Typeset the glyphs at bold weight (synthetic — see ENRMRaTeXBridge).
15
15
  @property (nonatomic, assign) BOOL bold;
16
16
 
17
+ /// Ink headroom above and below the formula box (`inlineMath.glyphOverflow`, points).
18
+ @property (nonatomic, assign) CGFloat glyphOverflow;
19
+
17
20
  /// YES if block math (`$$...$$`), NO for inline (`$...$`).
18
21
  @property (nonatomic, assign) BOOL displayMode;
19
22
 
@@ -42,7 +42,8 @@
42
42
  displayMode:NO
43
43
  fontSize:self.fontSize
44
44
  color:color
45
- bold:self.bold];
45
+ bold:self.bold
46
+ glyphOverflow:self.glyphOverflow];
46
47
  if (!result) {
47
48
  _fallbackSource = ENRMMathFallbackString(self.latex, @"$", self.fontSize, color);
48
49
  UIFont *font = [_fallbackSource attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
@@ -31,6 +31,68 @@ void EnrichedMarkdownEventEmitter::onLinkLongPress(OnLinkLongPress event) const
31
31
  }
32
32
 
33
33
 
34
+ void EnrichedMarkdownEventEmitter::onLinkLayout(OnLinkLayout event) const {
35
+ dispatchEvent("linkLayout", [event=std::move(event)](jsi::Runtime &runtime) {
36
+ auto payload = jsi::Object(runtime);
37
+
38
+ auto fragments = jsi::Array(runtime, event.fragments.size());
39
+ size_t fragmentsIndex = 0;
40
+ for (auto fragmentsValue : event.fragments) {
41
+ auto fragmentsObject = jsi::Object(runtime);
42
+ fragmentsObject.setProperty(runtime, "linkIndex", fragmentsValue.linkIndex);
43
+ fragmentsObject.setProperty(runtime, "url", fragmentsValue.url);
44
+ fragmentsObject.setProperty(runtime, "text", fragmentsValue.text);
45
+ fragmentsObject.setProperty(runtime, "x", fragmentsValue.x);
46
+ fragmentsObject.setProperty(runtime, "y", fragmentsValue.y);
47
+ fragmentsObject.setProperty(runtime, "width", fragmentsValue.width);
48
+ fragmentsObject.setProperty(runtime, "height", fragmentsValue.height);
49
+ fragments.setValueAtIndex(runtime, fragmentsIndex++, fragmentsObject);
50
+ }
51
+ payload.setProperty(runtime, "fragments", fragments);
52
+
53
+ return payload;
54
+ });
55
+ }
56
+
57
+
58
+ void EnrichedMarkdownEventEmitter::onSelectionChange(OnSelectionChange event) const {
59
+ dispatchEvent("selectionChange", [event=std::move(event)](jsi::Runtime &runtime) {
60
+ auto payload = jsi::Object(runtime);
61
+ payload.setProperty(runtime, "active", event.active);
62
+ payload.setProperty(runtime, "source", event.source);
63
+ payload.setProperty(runtime, "text", event.text);
64
+ payload.setProperty(runtime, "markdown", event.markdown);
65
+ payload.setProperty(runtime, "whole", event.whole);
66
+ {
67
+ auto rect = jsi::Object(runtime);
68
+ rect.setProperty(runtime, "x", event.rect.x);
69
+ rect.setProperty(runtime, "y", event.rect.y);
70
+ rect.setProperty(runtime, "width", event.rect.width);
71
+ rect.setProperty(runtime, "height", event.rect.height);
72
+ payload.setProperty(runtime, "rect", rect);
73
+ }
74
+ {
75
+ auto startRect = jsi::Object(runtime);
76
+ startRect.setProperty(runtime, "x", event.startRect.x);
77
+ startRect.setProperty(runtime, "y", event.startRect.y);
78
+ startRect.setProperty(runtime, "width", event.startRect.width);
79
+ startRect.setProperty(runtime, "height", event.startRect.height);
80
+ payload.setProperty(runtime, "startRect", startRect);
81
+ }
82
+ {
83
+ auto endRect = jsi::Object(runtime);
84
+ endRect.setProperty(runtime, "x", event.endRect.x);
85
+ endRect.setProperty(runtime, "y", event.endRect.y);
86
+ endRect.setProperty(runtime, "width", event.endRect.width);
87
+ endRect.setProperty(runtime, "height", event.endRect.height);
88
+ payload.setProperty(runtime, "endRect", endRect);
89
+ }
90
+ payload.setProperty(runtime, "adjusting", event.adjusting);
91
+ return payload;
92
+ });
93
+ }
94
+
95
+
34
96
  void EnrichedMarkdownEventEmitter::onTaskListItemPress(OnTaskListItemPress event) const {
35
97
  dispatchEvent("taskListItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
36
98
  auto payload = jsi::Object(runtime);
@@ -52,6 +114,18 @@ payload.setProperty(runtime, "language", event.language);
52
114
  }
53
115
 
54
116
 
117
+ void EnrichedMarkdownEventEmitter::onCopyRequest(OnCopyRequest event) const {
118
+ dispatchEvent("copyRequest", [event=std::move(event)](jsi::Runtime &runtime) {
119
+ auto payload = jsi::Object(runtime);
120
+ payload.setProperty(runtime, "source", event.source);
121
+ payload.setProperty(runtime, "markdown", event.markdown);
122
+ payload.setProperty(runtime, "text", event.text);
123
+ payload.setProperty(runtime, "whole", event.whole);
124
+ return payload;
125
+ });
126
+ }
127
+
128
+
55
129
  void EnrichedMarkdownEventEmitter::onContextMenuItemPress(OnContextMenuItemPress event) const {
56
130
  dispatchEvent("contextMenuItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
57
131
  auto payload = jsi::Object(runtime);
@@ -358,6 +432,68 @@ void EnrichedMarkdownTextEventEmitter::onLinkLongPress(OnLinkLongPress event) co
358
432
  }
359
433
 
360
434
 
435
+ void EnrichedMarkdownTextEventEmitter::onLinkLayout(OnLinkLayout event) const {
436
+ dispatchEvent("linkLayout", [event=std::move(event)](jsi::Runtime &runtime) {
437
+ auto payload = jsi::Object(runtime);
438
+
439
+ auto fragments = jsi::Array(runtime, event.fragments.size());
440
+ size_t fragmentsIndex = 0;
441
+ for (auto fragmentsValue : event.fragments) {
442
+ auto fragmentsObject = jsi::Object(runtime);
443
+ fragmentsObject.setProperty(runtime, "linkIndex", fragmentsValue.linkIndex);
444
+ fragmentsObject.setProperty(runtime, "url", fragmentsValue.url);
445
+ fragmentsObject.setProperty(runtime, "text", fragmentsValue.text);
446
+ fragmentsObject.setProperty(runtime, "x", fragmentsValue.x);
447
+ fragmentsObject.setProperty(runtime, "y", fragmentsValue.y);
448
+ fragmentsObject.setProperty(runtime, "width", fragmentsValue.width);
449
+ fragmentsObject.setProperty(runtime, "height", fragmentsValue.height);
450
+ fragments.setValueAtIndex(runtime, fragmentsIndex++, fragmentsObject);
451
+ }
452
+ payload.setProperty(runtime, "fragments", fragments);
453
+
454
+ return payload;
455
+ });
456
+ }
457
+
458
+
459
+ void EnrichedMarkdownTextEventEmitter::onSelectionChange(OnSelectionChange event) const {
460
+ dispatchEvent("selectionChange", [event=std::move(event)](jsi::Runtime &runtime) {
461
+ auto payload = jsi::Object(runtime);
462
+ payload.setProperty(runtime, "active", event.active);
463
+ payload.setProperty(runtime, "source", event.source);
464
+ payload.setProperty(runtime, "text", event.text);
465
+ payload.setProperty(runtime, "markdown", event.markdown);
466
+ payload.setProperty(runtime, "whole", event.whole);
467
+ {
468
+ auto rect = jsi::Object(runtime);
469
+ rect.setProperty(runtime, "x", event.rect.x);
470
+ rect.setProperty(runtime, "y", event.rect.y);
471
+ rect.setProperty(runtime, "width", event.rect.width);
472
+ rect.setProperty(runtime, "height", event.rect.height);
473
+ payload.setProperty(runtime, "rect", rect);
474
+ }
475
+ {
476
+ auto startRect = jsi::Object(runtime);
477
+ startRect.setProperty(runtime, "x", event.startRect.x);
478
+ startRect.setProperty(runtime, "y", event.startRect.y);
479
+ startRect.setProperty(runtime, "width", event.startRect.width);
480
+ startRect.setProperty(runtime, "height", event.startRect.height);
481
+ payload.setProperty(runtime, "startRect", startRect);
482
+ }
483
+ {
484
+ auto endRect = jsi::Object(runtime);
485
+ endRect.setProperty(runtime, "x", event.endRect.x);
486
+ endRect.setProperty(runtime, "y", event.endRect.y);
487
+ endRect.setProperty(runtime, "width", event.endRect.width);
488
+ endRect.setProperty(runtime, "height", event.endRect.height);
489
+ payload.setProperty(runtime, "endRect", endRect);
490
+ }
491
+ payload.setProperty(runtime, "adjusting", event.adjusting);
492
+ return payload;
493
+ });
494
+ }
495
+
496
+
361
497
  void EnrichedMarkdownTextEventEmitter::onTaskListItemPress(OnTaskListItemPress event) const {
362
498
  dispatchEvent("taskListItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
363
499
  auto payload = jsi::Object(runtime);
@@ -379,6 +515,18 @@ payload.setProperty(runtime, "language", event.language);
379
515
  }
380
516
 
381
517
 
518
+ void EnrichedMarkdownTextEventEmitter::onCopyRequest(OnCopyRequest event) const {
519
+ dispatchEvent("copyRequest", [event=std::move(event)](jsi::Runtime &runtime) {
520
+ auto payload = jsi::Object(runtime);
521
+ payload.setProperty(runtime, "source", event.source);
522
+ payload.setProperty(runtime, "markdown", event.markdown);
523
+ payload.setProperty(runtime, "text", event.text);
524
+ payload.setProperty(runtime, "whole", event.whole);
525
+ return payload;
526
+ });
527
+ }
528
+
529
+
382
530
  void EnrichedMarkdownTextEventEmitter::onContextMenuItemPress(OnContextMenuItemPress event) const {
383
531
  dispatchEvent("contextMenuItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
384
532
  auto payload = jsi::Object(runtime);