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

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 (140) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +15 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +15 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +122 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +100 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +124 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +145 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +19 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +22 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +85 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +22 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +42 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
  17. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
  18. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
  19. package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
  20. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
  22. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
  23. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
  24. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +28 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
  26. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +4 -2
  29. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +230 -0
  30. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +11 -1
  31. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +22 -1
  32. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +8 -1
  33. package/docs/API_REFERENCE.md +111 -0
  34. package/docs/MENTIONS.md +8 -1
  35. package/docs/STYLES.md +27 -0
  36. package/docs/TEXT.md +2 -0
  37. package/ios/EnrichedMarkdown.mm +221 -1
  38. package/ios/EnrichedMarkdownText.mm +142 -1
  39. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +122 -0
  40. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +100 -0
  41. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  42. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +124 -0
  43. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  44. package/ios/internals/MeasurementCache.h +5 -0
  45. package/ios/renderer/LinkRenderer.m +47 -15
  46. package/ios/styles/StyleConfig.h +16 -0
  47. package/ios/styles/StyleConfig.mm +60 -0
  48. package/ios/utils/CodeBackground.m +3 -65
  49. package/ios/utils/ENRMChipDrawing.h +24 -0
  50. package/ios/utils/ENRMChipDrawing.m +83 -0
  51. package/ios/utils/ENRMLinkChip.h +70 -0
  52. package/ios/utils/ENRMLinkChip.m +194 -0
  53. package/ios/utils/ENRMLinkLayout.h +50 -0
  54. package/ios/utils/ENRMLinkLayout.m +45 -0
  55. package/ios/utils/ENRMSelectionChange.h +96 -0
  56. package/ios/utils/ENRMSelectionChange.m +204 -0
  57. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  58. package/ios/utils/EditMenuUtils.m +4 -1
  59. package/ios/utils/HTMLGenerator.m +2 -1
  60. package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
  61. package/ios/utils/MarkdownExtractor.m +2 -1
  62. package/ios/utils/RTFExportUtils.m +2 -0
  63. package/ios/utils/StylePropsUtils.h +37 -1
  64. package/ios/utils/TextViewLayoutManager.mm +3 -0
  65. package/ios/views/ENRMCodeBlockContainerView.h +3 -0
  66. package/ios/views/ENRMCodeBlockContainerView.m +3 -0
  67. package/ios/views/ENRMMathContainerView.h +3 -0
  68. package/ios/views/ENRMMathContainerView.m +3 -0
  69. package/ios/views/TableContainerView.h +6 -0
  70. package/ios/views/TableContainerView.m +24 -0
  71. package/lib/module/EnrichedMarkdownNativeComponent.ts +101 -1
  72. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +101 -1
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/jest/index.js +13 -0
  75. package/lib/module/jest/index.js.map +1 -1
  76. package/lib/module/native/EnrichedMarkdownText.js +138 -9
  77. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  78. package/lib/module/normalizeMarkdownStyle.js +13 -2
  79. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  80. package/lib/module/normalizeMarkdownStyle.web.js +13 -2
  81. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  82. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  83. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  84. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  85. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  86. package/lib/module/web/styles.js +18 -0
  87. package/lib/module/web/styles.js.map +1 -1
  88. package/lib/module/web/utils.js +13 -0
  89. package/lib/module/web/utils.js.map +1 -1
  90. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +93 -2
  91. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  92. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +93 -2
  93. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  94. package/lib/typescript/src/index.d.ts +2 -2
  95. package/lib/typescript/src/index.d.ts.map +1 -1
  96. package/lib/typescript/src/index.web.d.ts +1 -1
  97. package/lib/typescript/src/index.web.d.ts.map +1 -1
  98. package/lib/typescript/src/jest/index.d.ts +1 -1
  99. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  100. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  101. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  102. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  103. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  104. package/lib/typescript/src/types/MarkdownStyle.d.ts +28 -5
  105. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  106. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +10 -0
  107. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  108. package/lib/typescript/src/types/MarkdownTextProps.d.ts +76 -2
  109. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  110. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  111. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  112. package/lib/typescript/src/types/events.d.ts +36 -0
  113. package/lib/typescript/src/types/events.d.ts.map +1 -1
  114. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  115. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  116. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  117. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  118. package/lib/typescript/src/web/types.d.ts +4 -0
  119. package/lib/typescript/src/web/types.d.ts.map +1 -1
  120. package/lib/typescript/src/web/utils.d.ts +4 -0
  121. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  122. package/package.json +1 -1
  123. package/src/EnrichedMarkdownNativeComponent.ts +101 -1
  124. package/src/EnrichedMarkdownTextNativeComponent.ts +101 -1
  125. package/src/index.tsx +5 -0
  126. package/src/index.web.tsx +5 -1
  127. package/src/jest/index.tsx +21 -1
  128. package/src/native/EnrichedMarkdownText.tsx +181 -8
  129. package/src/normalizeMarkdownStyle.ts +13 -0
  130. package/src/normalizeMarkdownStyle.web.ts +11 -0
  131. package/src/types/MarkdownStyle.ts +29 -5
  132. package/src/types/MarkdownStyleInternal.ts +10 -0
  133. package/src/types/MarkdownTextProps.ts +85 -1
  134. package/src/types/MarkdownTextProps.web.ts +43 -1
  135. package/src/types/events.ts +38 -0
  136. package/src/web/EnrichedMarkdownText.tsx +10 -3
  137. package/src/web/renderers/InlineRenderers.tsx +111 -3
  138. package/src/web/styles.ts +25 -0
  139. package/src/web/types.ts +4 -0
  140. 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,6 +75,7 @@ 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;
78
81
  - (void)emitCopyRequest:(NSString *)source markdown:(NSString *)markdown text:(NSString *)text whole:(BOOL)whole;
@@ -123,6 +126,18 @@ static char kENRMSegmentFadeAnimatorKey;
123
126
 
124
127
  ENRMSpoilerOverlay _spoilerOverlay;
125
128
 
129
+ BOOL _emitLinkLayout;
130
+ NSArray<NSDictionary *> *_lastLinkLayout;
131
+
132
+ ENRMSelectionChangeReporter *_selectionReporter;
133
+ // At most one segment holds a selection; a new one clears the previous.
134
+ __weak ENRMPlatformTextView *_selectionOwner;
135
+ // A block long-pressed while contextMenuHidden is on, reported as the selection.
136
+ __weak RCTUIView *_activeBlockView;
137
+ NSString *_activeBlockSource;
138
+ NSString *_activeBlockMarkdown;
139
+ NSString *_activeBlockText;
140
+
126
141
  NSLineBreakStrategy _lineBreakStrategy;
127
142
 
128
143
  ENRMWritingDirectionMode _writingDirectionMode;
@@ -194,10 +209,117 @@ static char kENRMSegmentFadeAnimatorKey;
194
209
  [strongSelf renderMarkdownContent:strongSelf->_cachedMarkdown];
195
210
  }
196
211
  };
212
+
213
+ _selectionReporter = [[ENRMSelectionChangeReporter alloc]
214
+ initWithSnapshot:^NSDictionary * {
215
+ EnrichedMarkdown *strongSelf = weakSelf;
216
+ return strongSelf ? [strongSelf selectionPayload] : ENRMInactiveSelectionPayload();
217
+ }
218
+ emit:^(NSDictionary *payload) {
219
+ EnrichedMarkdown *strongSelf = weakSelf;
220
+ if (!strongSelf)
221
+ return;
222
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(strongSelf->_eventEmitter);
223
+ if (emitter)
224
+ emitter->onSelectionChange(
225
+ ENRMSelectionChangeEventFromPayload<EnrichedMarkdownEventEmitter::OnSelectionChange>(payload));
226
+ }];
197
227
  }
198
228
  return self;
199
229
  }
200
230
 
231
+ #pragma mark - Selection
232
+
233
+ /// The current selection (or active block) as an `onSelectionChange` payload.
234
+ /// Markdown and `whole` follow the same rules as the intercepted Copy.
235
+ - (NSDictionary *)selectionPayload
236
+ {
237
+ RCTUIView *blockView = _activeBlockView;
238
+ if (blockView != nil) {
239
+ if (blockView.superview != self)
240
+ return ENRMInactiveSelectionPayload();
241
+ return ENRMBlockSelectionPayload(_activeBlockSource, _activeBlockMarkdown, _activeBlockText, blockView.frame);
242
+ }
243
+
244
+ #if !TARGET_OS_OSX
245
+ UITextView *owner = _selectionOwner;
246
+ if (owner == nil || ![owner isDescendantOfView:self])
247
+ return ENRMInactiveSelectionPayload();
248
+ NSDictionary *selection = ENRMTextSelectionInTextView(owner, self);
249
+ if (!selection)
250
+ return ENRMInactiveSelectionPayload();
251
+
252
+ NSString *text = selection[ENRMSelectionTextKey];
253
+ // Segments only see their own text, so a selection is the whole component
254
+ // only when it spans the sole segment; the markdown is then the original.
255
+ BOOL whole = [selection[ENRMSelectionFullKey] boolValue] && _segmentViews.count == 1 && _cachedMarkdown.length > 0;
256
+ NSString *markdown =
257
+ whole ? _cachedMarkdown : extractMarkdownFromAttributedString(owner.attributedText, owner.selectedRange);
258
+ NSMutableDictionary *payload = [selection mutableCopy];
259
+ [payload removeObjectForKey:ENRMSelectionFullKey];
260
+ payload[ENRMSelectionActiveKey] = @YES;
261
+ payload[ENRMSelectionSourceKey] = @"selection";
262
+ payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
263
+ payload[ENRMSelectionWholeKey] = @(whole);
264
+ return payload;
265
+ #else
266
+ return ENRMInactiveSelectionPayload();
267
+ #endif
268
+ }
269
+
270
+ - (void)clearActiveBlock
271
+ {
272
+ _activeBlockView = nil;
273
+ _activeBlockSource = nil;
274
+ _activeBlockMarkdown = nil;
275
+ _activeBlockText = nil;
276
+ }
277
+
278
+ - (void)clearSegmentSelection:(ENRMPlatformTextView *)textView
279
+ {
280
+ #if !TARGET_OS_OSX
281
+ textView.selectedRange = NSMakeRange(0, 0);
282
+ [textView resignFirstResponder];
283
+ #endif
284
+ }
285
+
286
+ - (void)clearSelection
287
+ {
288
+ ENRMPlatformTextView *owner = _selectionOwner;
289
+ _selectionOwner = nil;
290
+ [self clearActiveBlock];
291
+ if (owner)
292
+ [self clearSegmentSelection:owner];
293
+ [_selectionReporter flushNow];
294
+ }
295
+
296
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
297
+ {
298
+ RCTEnrichedMarkdownHandleCommand(self, commandName, args);
299
+ }
300
+
301
+ - (ENRMBlockCopyRequestBlock)blockMenuSuppressedForSource:(NSString *)source view:(RCTUIView *)view
302
+ {
303
+ __weak EnrichedMarkdown *weakSelf = self;
304
+ __weak RCTUIView *weakView = view;
305
+ return ^(NSString *markdown, NSString *text) {
306
+ EnrichedMarkdown *strongSelf = weakSelf;
307
+ RCTUIView *strongView = weakView;
308
+ if (!strongSelf || !strongView || !strongSelf->_selectionReporter.enabled)
309
+ return;
310
+ ENRMPlatformTextView *owner = strongSelf->_selectionOwner;
311
+ strongSelf->_selectionOwner = nil;
312
+ if (owner)
313
+ [strongSelf clearSegmentSelection:owner];
314
+ strongSelf->_activeBlockView = strongView;
315
+ strongSelf->_activeBlockSource = source;
316
+ strongSelf->_activeBlockMarkdown = markdown;
317
+ strongSelf->_activeBlockText = text;
318
+ [strongSelf->_selectionReporter forgetLastReported];
319
+ [strongSelf->_selectionReporter flushNow];
320
+ };
321
+ }
322
+
201
323
  - (void)configureSegmentViewRegistry
202
324
  {
203
325
  __weak EnrichedMarkdown *weakSelf = self;
@@ -834,6 +956,7 @@ static char kENRMSegmentFadeAnimatorKey;
834
956
  tableView.contextMenuHidden = _contextMenuHidden;
835
957
  tableView.menuOptions = [self blockMenuOptions];
836
958
  tableView.onCopyRequest = [self blockCopyRequestForSource:@"table"];
959
+ tableView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"table" view:tableView];
837
960
 
838
961
  __weak EnrichedMarkdown *weakSelf = self;
839
962
 
@@ -876,6 +999,7 @@ static char kENRMSegmentFadeAnimatorKey;
876
999
  mathView.contextMenuHidden = _contextMenuHidden;
877
1000
  mathView.menuOptions = [self blockMenuOptions];
878
1001
  mathView.onCopyRequest = [self blockCopyRequestForSource:@"math"];
1002
+ mathView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"math" view:mathView];
879
1003
  [mathView applyLatex:mathSegment.latex];
880
1004
  return mathView;
881
1005
  }
@@ -889,6 +1013,7 @@ static char kENRMSegmentFadeAnimatorKey;
889
1013
  codeBlockView.contextMenuHidden = _contextMenuHidden;
890
1014
  codeBlockView.menuOptions = [self blockMenuOptions];
891
1015
  codeBlockView.onCopyRequest = [self blockCopyRequestForSource:@"code"];
1016
+ codeBlockView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"code" view:codeBlockView];
892
1017
 
893
1018
  __weak EnrichedMarkdown *weakSelf = self;
894
1019
  codeBlockView.onCopyPress = ^(NSString *code, NSString *language) {
@@ -941,6 +1066,9 @@ static char kENRMSegmentFadeAnimatorKey;
941
1066
  {
942
1067
  [super layoutSubviews];
943
1068
  [self computeSegmentLayoutForWidth:self.bounds.size.width applyFrames:YES];
1069
+ [self emitLinkLayoutIfNeeded];
1070
+ // Segments may have moved or been replaced; re-read the selection (deduplicated).
1071
+ [_selectionReporter schedule];
944
1072
  }
945
1073
 
946
1074
  - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
@@ -976,6 +1104,14 @@ static char kENRMSegmentFadeAnimatorKey;
976
1104
 
977
1105
  _selectable = newViewProps.selectable;
978
1106
 
1107
+ if (newViewProps.emitLinkLayout != _emitLinkLayout) {
1108
+ _emitLinkLayout = newViewProps.emitLinkLayout;
1109
+ _lastLinkLayout = nil;
1110
+ [self setNeedsLayout];
1111
+ }
1112
+
1113
+ _selectionReporter.enabled = newViewProps.emitSelectionChange;
1114
+
979
1115
  for (RCTUIView *segment in _segmentViews) {
980
1116
  if ([segment isKindOfClass:[EnrichedMarkdownInternalText class]]) {
981
1117
  EnrichedMarkdownInternalText *textSegment = (EnrichedMarkdownInternalText *)segment;
@@ -1171,6 +1307,11 @@ static char kENRMSegmentFadeAnimatorKey;
1171
1307
  _cachedMarkdown = nil;
1172
1308
  _renderedMarkdown = nil;
1173
1309
  _config = nil;
1310
+ _emitLinkLayout = NO;
1311
+ _lastLinkLayout = nil;
1312
+ _selectionReporter.enabled = NO;
1313
+ _selectionOwner = nil;
1314
+ [self clearActiveBlock];
1174
1315
  _md4cFlags = [EnrichedMarkdown flagsFromProps:resetProps->md4cFlags];
1175
1316
  _maxFontSizeMultiplier = 0;
1176
1317
  _allowTrailingMargin = NO;
@@ -1281,6 +1422,40 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1281
1422
  emitter->onLinkPress({.url = std::string(url.UTF8String)});
1282
1423
  }
1283
1424
 
1425
+ /// Reports link rects of every text segment (offset by the segment origin) to
1426
+ /// JS for `renderLinkDecoration`. Tables are skipped: their cells scroll
1427
+ /// horizontally, so rects would go stale. Runs after each segment layout pass;
1428
+ /// deduplicated so an unchanged layout sends nothing.
1429
+ - (void)emitLinkLayoutIfNeeded
1430
+ {
1431
+ if (!_emitLinkLayout || self.bounds.size.width <= 0)
1432
+ return;
1433
+
1434
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
1435
+ if (!emitter)
1436
+ return;
1437
+
1438
+ NSMutableArray<NSDictionary *> *fragments = [NSMutableArray array];
1439
+ NSInteger linkCount = 0;
1440
+ for (RCTUIView *segment in _segmentViews) {
1441
+ if ([segment isKindOfClass:[TableContainerView class]]) {
1442
+ linkCount += ((TableContainerView *)segment).linkCount;
1443
+ continue;
1444
+ }
1445
+ if (![segment isKindOfClass:[EnrichedMarkdownInternalText class]])
1446
+ continue;
1447
+ NSInteger segmentLinks = 0;
1448
+ [fragments addObjectsFromArray:ENRMCollectLinkLayoutFragments(((EnrichedMarkdownInternalText *)segment).textView,
1449
+ segment.frame.origin, linkCount, &segmentLinks)];
1450
+ linkCount += segmentLinks;
1451
+ }
1452
+
1453
+ if (_lastLinkLayout != nil && [fragments isEqualToArray:_lastLinkLayout])
1454
+ return;
1455
+ _lastLinkLayout = [fragments copy];
1456
+ emitter->onLinkLayout(ENRMLinkLayoutEventFromFragments<EnrichedMarkdownEventEmitter::OnLinkLayout>(fragments));
1457
+ }
1458
+
1284
1459
  - (void)emitLinkLongPress:(NSString *)url
1285
1460
  {
1286
1461
  auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(_eventEmitter);
@@ -1358,14 +1533,33 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1358
1533
  ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { [self emitLinkPress:url]; });
1359
1534
  }
1360
1535
 
1536
+ #if !TARGET_OS_OSX
1537
+ - (void)textViewDidChangeSelection:(UITextView *)textView
1538
+ {
1539
+ if (textView.selectedRange.length > 0) {
1540
+ // Take ownership first so the previous owner's own "cleared" callback is ignored.
1541
+ ENRMPlatformTextView *previous = _selectionOwner;
1542
+ _selectionOwner = textView;
1543
+ [self clearActiveBlock];
1544
+ if (previous && previous != textView)
1545
+ previous.selectedRange = NSMakeRange(0, 0);
1546
+ [_selectionReporter schedule];
1547
+ } else if (textView == _selectionOwner) {
1548
+ [_selectionReporter selectionChanged:NO];
1549
+ }
1550
+ }
1551
+ #endif
1552
+
1361
1553
  // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
1362
1554
  #if !TARGET_OS_OSX
1363
1555
  - (UIMenu *)textView:(UITextView *)textView
1364
1556
  editMenuForTextInRange:(NSRange)range
1365
1557
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
1366
1558
  {
1559
+ // An empty menu presents nothing; nil would make UIKit show its default menu
1560
+ // (the selection and handles stay either way).
1367
1561
  if (_contextMenuHidden) {
1368
- return nil;
1562
+ return [UIMenu menuWithChildren:@[]];
1369
1563
  }
1370
1564
 
1371
1565
  __weak EnrichedMarkdown *weakSelf = self;
@@ -1400,6 +1594,32 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1400
1594
  customActions, _selectionMenuConfig, onCopyRequest);
1401
1595
  }
1402
1596
 
1597
+ // Attachments other than images (inline math) are text, not items: without
1598
+ // this a long press on one in an unfocused text view starts UIKit's text item
1599
+ // interaction, which presents nothing and leaves the view unable to select.
1600
+ - (BOOL)textView:(UITextView *)textView
1601
+ shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment
1602
+ inRange:(NSRange)characterRange
1603
+ interaction:(UITextItemInteraction)interaction
1604
+ {
1605
+ if ([textAttachment isKindOfClass:[ENRMImageAttachment class]]) {
1606
+ return YES;
1607
+ }
1608
+ // A long press selects the attachment like a word, as it does once the view is focused.
1609
+ if (interaction == UITextItemInteractionPresentActions) {
1610
+ __weak UITextView *weakTextView = textView;
1611
+ dispatch_async(dispatch_get_main_queue(), ^{
1612
+ UITextView *strongTextView = weakTextView;
1613
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
1614
+ return;
1615
+ }
1616
+ [strongTextView becomeFirstResponder];
1617
+ strongTextView.selectedRange = characterRange;
1618
+ });
1619
+ }
1620
+ return NO;
1621
+ }
1622
+
1403
1623
  - (BOOL)textView:(UITextView *)textView
1404
1624
  shouldInteractWithURL:(NSURL *)URL
1405
1625
  inRange:(NSRange)characterRange
@@ -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,6 +59,7 @@ 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
@@ -110,6 +114,11 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
110
114
 
111
115
  ENRMSpoilerOverlayManager *_spoilerManager;
112
116
 
117
+ BOOL _emitLinkLayout;
118
+ NSArray<NSDictionary *> *_lastLinkLayout;
119
+
120
+ ENRMSelectionChangeReporter *_selectionReporter;
121
+
113
122
  NSLineBreakStrategy _lineBreakStrategy;
114
123
 
115
124
  ENRMWritingDirectionMode _writingDirectionMode;
@@ -183,6 +192,9 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
183
192
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
184
193
 
185
194
  _lastCommittedSize.store(CGSizeMake(layoutMetrics.frame.size.width, layoutMetrics.frame.size.height));
195
+ #if TARGET_OS_OSX
196
+ [self emitLinkLayoutIfNeeded];
197
+ #endif
186
198
 
187
199
  NSWritingDirection resolved = _resolvedLayoutDirection;
188
200
  if (layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
@@ -218,6 +230,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
218
230
  if (needsHeightUpdate(measured, self.bounds)) {
219
231
  [self requestHeightUpdate];
220
232
  }
233
+ [self emitLinkLayoutIfNeeded];
221
234
  }
222
235
  }
223
236
 
@@ -263,11 +276,68 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
263
276
  };
264
277
 
265
278
  [self setupTextView];
279
+ [self setupSelectionReporter];
266
280
  }
267
281
 
268
282
  return self;
269
283
  }
270
284
 
285
+ - (void)setupSelectionReporter
286
+ {
287
+ __weak EnrichedMarkdownText *weakSelf = self;
288
+ _selectionReporter = [[ENRMSelectionChangeReporter alloc]
289
+ initWithSnapshot:^NSDictionary * {
290
+ EnrichedMarkdownText *strongSelf = weakSelf;
291
+ return strongSelf ? [strongSelf selectionPayload] : ENRMInactiveSelectionPayload();
292
+ }
293
+ emit:^(NSDictionary *payload) {
294
+ EnrichedMarkdownText *strongSelf = weakSelf;
295
+ if (!strongSelf)
296
+ return;
297
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(strongSelf->_eventEmitter);
298
+ if (emitter)
299
+ emitter->onSelectionChange(
300
+ ENRMSelectionChangeEventFromPayload<EnrichedMarkdownTextEventEmitter::OnSelectionChange>(payload));
301
+ }];
302
+ }
303
+
304
+ /// The current selection as an `onSelectionChange` payload. Markdown and
305
+ /// `whole` follow the same rules as the intercepted Copy (`onCopyRequest`).
306
+ - (NSDictionary *)selectionPayload
307
+ {
308
+ #if !TARGET_OS_OSX
309
+ NSDictionary *selection = ENRMTextSelectionInTextView(_textView, self);
310
+ if (!selection)
311
+ return ENRMInactiveSelectionPayload();
312
+
313
+ NSString *text = selection[ENRMSelectionTextKey];
314
+ NSString *markdown = markdownForRange(_textView.attributedText, _textView.selectedRange, _cachedMarkdown);
315
+ NSMutableDictionary *payload = [selection mutableCopy];
316
+ [payload removeObjectForKey:ENRMSelectionFullKey];
317
+ payload[ENRMSelectionActiveKey] = @YES;
318
+ payload[ENRMSelectionSourceKey] = @"selection";
319
+ payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
320
+ payload[ENRMSelectionWholeKey] = @([selection[ENRMSelectionFullKey] boolValue] && _cachedMarkdown.length > 0);
321
+ return payload;
322
+ #else
323
+ return ENRMInactiveSelectionPayload();
324
+ #endif
325
+ }
326
+
327
+ - (void)clearSelection
328
+ {
329
+ #if !TARGET_OS_OSX
330
+ _textView.selectedRange = NSMakeRange(0, 0);
331
+ [_textView resignFirstResponder];
332
+ #endif
333
+ [_selectionReporter flushNow];
334
+ }
335
+
336
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
337
+ {
338
+ RCTEnrichedMarkdownTextHandleCommand(self, commandName, args);
339
+ }
340
+
271
341
  - (void)setupTextView
272
342
  {
273
343
  #if !TARGET_OS_OSX
@@ -464,8 +534,13 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
464
534
  if (forceHeightUpdate || needsHeightUpdate(measured, self.bounds)) {
465
535
  [self requestHeightUpdate];
466
536
  }
537
+
538
+ [self emitLinkLayoutIfNeeded];
467
539
  }
468
540
 
541
+ // New text drops the selection; setting attributedText doesn't call the delegate.
542
+ [_selectionReporter schedule];
543
+
469
544
  if (_textView.hidden) {
470
545
  dispatch_async(dispatch_get_main_queue(), ^{ self->_textView.hidden = NO; });
471
546
  }
@@ -484,6 +559,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
484
559
  {
485
560
  [super layoutSubviews];
486
561
  [_spoilerManager updateIfNeeded];
562
+ [self emitLinkLayoutIfNeeded];
487
563
  }
488
564
  #endif
489
565
 
@@ -521,6 +597,14 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
521
597
  }
522
598
  }
523
599
 
600
+ if (newViewProps.emitLinkLayout != _emitLinkLayout) {
601
+ _emitLinkLayout = newViewProps.emitLinkLayout;
602
+ _lastLinkLayout = nil;
603
+ [self emitLinkLayoutIfNeeded];
604
+ }
605
+
606
+ _selectionReporter.enabled = newViewProps.emitSelectionChange;
607
+
524
608
  if (_textView.selectable != newViewProps.selectable) {
525
609
  _textView.selectable = newViewProps.selectable;
526
610
  }
@@ -697,6 +781,9 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
697
781
  _accessibilityNeedsRebuild = NO;
698
782
  [_spoilerManager removeAllOverlays];
699
783
  _spoilerManager = nil;
784
+ _emitLinkLayout = NO;
785
+ _lastLinkLayout = nil;
786
+ _selectionReporter.enabled = NO;
700
787
  if (_textView != nil) {
701
788
  ENRMSetAttributedText(_textView, [[NSAttributedString alloc] initWithString:@""]);
702
789
  _textView.hidden = YES;
@@ -742,6 +829,25 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
742
829
  emitter->onLinkLongPress({.url = std::string(url.UTF8String)});
743
830
  }
744
831
 
832
+ /// Reports link rects to JS (for `renderLinkDecoration`). Called after every
833
+ /// render and layout change; deduplicated so an unchanged layout — the common
834
+ /// case during streaming — sends nothing.
835
+ - (void)emitLinkLayoutIfNeeded
836
+ {
837
+ if (!_emitLinkLayout || _renderedMarkdown == nil || self.bounds.size.width <= 0)
838
+ return;
839
+
840
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(_eventEmitter);
841
+ if (!emitter)
842
+ return;
843
+
844
+ NSArray<NSDictionary *> *fragments = ENRMCollectLinkLayoutFragments(_textView, CGPointZero, 0, NULL);
845
+ if (_lastLinkLayout != nil && [fragments isEqualToArray:_lastLinkLayout])
846
+ return;
847
+ _lastLinkLayout = fragments;
848
+ emitter->onLinkLayout(ENRMLinkLayoutEventFromFragments<EnrichedMarkdownTextEventEmitter::OnLinkLayout>(fragments));
849
+ }
850
+
745
851
  - (void)emitTaskListItemPress:(NSInteger)index checked:(BOOL)checked text:(NSString *)text
746
852
  {
747
853
  auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(_eventEmitter);
@@ -800,6 +906,32 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
800
906
  #pragma mark - UITextViewDelegate (Link Interaction)
801
907
 
802
908
  #if !TARGET_OS_OSX
909
+ // Attachments other than images (inline math) are text, not items: without
910
+ // this a long press on one in an unfocused text view starts UIKit's text item
911
+ // interaction, which presents nothing and leaves the view unable to select.
912
+ - (BOOL)textView:(UITextView *)textView
913
+ shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment
914
+ inRange:(NSRange)characterRange
915
+ interaction:(UITextItemInteraction)interaction
916
+ {
917
+ if ([textAttachment isKindOfClass:[ENRMImageAttachment class]]) {
918
+ return YES;
919
+ }
920
+ // A long press selects the attachment like a word, as it does once the view is focused.
921
+ if (interaction == UITextItemInteractionPresentActions) {
922
+ __weak UITextView *weakTextView = textView;
923
+ dispatch_async(dispatch_get_main_queue(), ^{
924
+ UITextView *strongTextView = weakTextView;
925
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
926
+ return;
927
+ }
928
+ [strongTextView becomeFirstResponder];
929
+ strongTextView.selectedRange = characterRange;
930
+ });
931
+ }
932
+ return NO;
933
+ }
934
+
803
935
  - (BOOL)textView:(ENRMPlatformTextView *)textView
804
936
  shouldInteractWithURL:(NSURL *)URL
805
937
  inRange:(NSRange)characterRange
@@ -819,6 +951,13 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
819
951
  return NO;
820
952
  }
821
953
 
954
+ #pragma mark - UITextViewDelegate (Selection)
955
+
956
+ - (void)textViewDidChangeSelection:(UITextView *)textView
957
+ {
958
+ [_selectionReporter selectionChanged:textView.selectedRange.length > 0];
959
+ }
960
+
822
961
  #pragma mark - UITextViewDelegate (Edit Menu)
823
962
 
824
963
  // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
@@ -826,8 +965,10 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
826
965
  editMenuForTextInRange:(NSRange)range
827
966
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
828
967
  {
968
+ // An empty menu presents nothing; nil would make UIKit show its default menu
969
+ // (the selection and handles stay either way).
829
970
  if (_contextMenuHidden) {
830
- return nil;
971
+ return [UIMenu menuWithChildren:@[]];
831
972
  }
832
973
 
833
974
  __weak EnrichedMarkdownText *weakSelf = self;