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
@@ -0,0 +1,282 @@
1
+ #import "ENRMSelectionChange.h"
2
+ #import "ENRMLinkChip.h"
3
+
4
+ NSString *const ENRMSelectionActiveKey = @"active";
5
+ NSString *const ENRMSelectionSourceKey = @"source";
6
+ NSString *const ENRMSelectionTextKey = @"text";
7
+ NSString *const ENRMSelectionMarkdownKey = @"markdown";
8
+ NSString *const ENRMSelectionWholeKey = @"whole";
9
+ NSString *const ENRMSelectionRectKey = @"rect";
10
+ NSString *const ENRMSelectionStartRectKey = @"startRect";
11
+ NSString *const ENRMSelectionEndRectKey = @"endRect";
12
+ NSString *const ENRMSelectionAdjustingKey = @"adjusting";
13
+ NSString *const ENRMSelectionFullKey = @"full";
14
+
15
+ static NSValue *ENRMRectValue(CGRect rect)
16
+ {
17
+ #if TARGET_OS_OSX
18
+ return [NSValue valueWithRect:NSRectFromCGRect(rect)];
19
+ #else
20
+ return [NSValue valueWithCGRect:rect];
21
+ #endif
22
+ }
23
+
24
+ NSDictionary *ENRMInactiveSelectionPayload(void)
25
+ {
26
+ static NSDictionary *payload;
27
+ static dispatch_once_t onceToken;
28
+ dispatch_once(&onceToken, ^{
29
+ NSValue *zero = ENRMRectValue(CGRectZero);
30
+ payload = @{
31
+ ENRMSelectionActiveKey : @NO,
32
+ ENRMSelectionSourceKey : @"selection",
33
+ ENRMSelectionTextKey : @"",
34
+ ENRMSelectionMarkdownKey : @"",
35
+ ENRMSelectionWholeKey : @NO,
36
+ ENRMSelectionRectKey : zero,
37
+ ENRMSelectionStartRectKey : zero,
38
+ ENRMSelectionEndRectKey : zero,
39
+ };
40
+ });
41
+ return payload;
42
+ }
43
+
44
+ NSDictionary *ENRMBlockSelectionPayload(NSString *source, NSString *markdown, NSString *text, CGRect frame)
45
+ {
46
+ NSValue *rect = ENRMRectValue(frame);
47
+ return @{
48
+ ENRMSelectionActiveKey : @YES,
49
+ ENRMSelectionSourceKey : source,
50
+ ENRMSelectionTextKey : text ?: @"",
51
+ ENRMSelectionMarkdownKey : markdown ?: @"",
52
+ ENRMSelectionWholeKey : @YES,
53
+ ENRMSelectionRectKey : rect,
54
+ ENRMSelectionStartRectKey : rect,
55
+ ENRMSelectionEndRectKey : rect,
56
+ };
57
+ }
58
+
59
+ #if !TARGET_OS_OSX
60
+ NSDictionary *ENRMTextSelectionInTextView(UITextView *textView, UIView *targetView)
61
+ {
62
+ NSRange range = textView.selectedRange;
63
+ UITextRange *textRange = textView.selectedTextRange;
64
+ if (range.location == NSNotFound || range.length == 0 || textRange == nil || textRange.isEmpty) {
65
+ return nil;
66
+ }
67
+
68
+ NSAttributedString *attributedText = textView.attributedText;
69
+ if (NSMaxRange(range) > attributedText.length) {
70
+ return nil;
71
+ }
72
+
73
+ CGRect unionRect = CGRectNull;
74
+ CGRect startRect = CGRectNull;
75
+ CGRect endRect = CGRectNull;
76
+ CGRect firstRect = CGRectNull;
77
+ CGRect lastRect = CGRectNull;
78
+ for (UITextSelectionRect *selectionRect in [textView selectionRectsForRange:textRange]) {
79
+ CGRect rect = selectionRect.rect;
80
+ if (CGRectIsNull(rect) || CGRectIsInfinite(rect) || CGRectIsEmpty(rect)) {
81
+ continue;
82
+ }
83
+ unionRect = CGRectUnion(unionRect, rect);
84
+ if (CGRectIsNull(firstRect)) {
85
+ firstRect = rect;
86
+ }
87
+ lastRect = rect;
88
+ if (selectionRect.containsStart && CGRectIsNull(startRect)) {
89
+ startRect = rect;
90
+ }
91
+ if (selectionRect.containsEnd) {
92
+ endRect = rect;
93
+ }
94
+ }
95
+ // Not laid out yet: report once layout gives the range a position.
96
+ if (CGRectIsNull(unionRect)) {
97
+ return nil;
98
+ }
99
+ if (CGRectIsNull(startRect)) {
100
+ startRect = firstRect;
101
+ }
102
+ if (CGRectIsNull(endRect)) {
103
+ endRect = lastRect;
104
+ }
105
+
106
+ return @{
107
+ ENRMSelectionTextKey : ENRMPlainTextByRemovingLinkSpacers(attributedText, range),
108
+ ENRMSelectionFullKey : @(range.location == 0 && range.length >= attributedText.length - 1),
109
+ ENRMSelectionRectKey : ENRMRectValue([textView convertRect:unionRect toView:targetView]),
110
+ ENRMSelectionStartRectKey : ENRMRectValue([textView convertRect:startRect toView:targetView]),
111
+ ENRMSelectionEndRectKey : ENRMRectValue([textView convertRect:endRect toView:targetView]),
112
+ };
113
+ }
114
+ #endif
115
+
116
+ @implementation ENRMSelectionChangeReporter {
117
+ NSDictionary * (^_snapshot)(void);
118
+ void (^_emit)(NSDictionary *);
119
+ NSDictionary *_lastReported;
120
+ BOOL _pending;
121
+ // Bumped on cancel so a scheduled block that already left the queue is a no-op.
122
+ NSUInteger _generation;
123
+ }
124
+
125
+ - (instancetype)initWithSnapshot:(NSDictionary * (^)(void))snapshot emit:(void (^)(NSDictionary *))emit
126
+ {
127
+ if (self = [super init]) {
128
+ _snapshot = [snapshot copy];
129
+ _emit = [emit copy];
130
+ _lastReported = ENRMInactiveSelectionPayload();
131
+ }
132
+ return self;
133
+ }
134
+
135
+ - (void)setEnabled:(BOOL)enabled
136
+ {
137
+ if (_enabled == enabled) {
138
+ return;
139
+ }
140
+ _enabled = enabled;
141
+ [self cancel];
142
+ _lastReported = ENRMInactiveSelectionPayload();
143
+ if (enabled) {
144
+ [self schedule];
145
+ }
146
+ }
147
+
148
+ - (void)schedule
149
+ {
150
+ if (!_enabled || _pending) {
151
+ return;
152
+ }
153
+ _pending = YES;
154
+ NSUInteger generation = _generation;
155
+ __weak ENRMSelectionChangeReporter *weakSelf = self;
156
+ dispatch_async(dispatch_get_main_queue(), ^{
157
+ ENRMSelectionChangeReporter *strongSelf = weakSelf;
158
+ if (!strongSelf || !strongSelf->_pending || strongSelf->_generation != generation) {
159
+ return;
160
+ }
161
+ strongSelf->_pending = NO;
162
+ [strongSelf report];
163
+ });
164
+ }
165
+
166
+ - (void)selectionChanged:(BOOL)hasSelection
167
+ {
168
+ if (hasSelection) {
169
+ [self schedule];
170
+ } else {
171
+ [self flushNow];
172
+ }
173
+ }
174
+
175
+ - (void)flushNow
176
+ {
177
+ [self cancel];
178
+ [self report];
179
+ }
180
+
181
+ - (void)forgetLastReported
182
+ {
183
+ _lastReported = ENRMInactiveSelectionPayload();
184
+ }
185
+
186
+ - (void)cancel
187
+ {
188
+ _pending = NO;
189
+ _generation++;
190
+ }
191
+
192
+ - (void)report
193
+ {
194
+ if (!_enabled) {
195
+ return;
196
+ }
197
+ NSDictionary *current = _snapshot();
198
+ if ([current isEqualToDictionary:_lastReported]) {
199
+ return;
200
+ }
201
+ _lastReported = current;
202
+ _emit(current);
203
+ }
204
+
205
+ @end
206
+
207
+ #if !TARGET_OS_OSX
208
+ static void ENRMCollectGestures(UIView *view, NSMutableArray<UIGestureRecognizer *> *into)
209
+ {
210
+ for (UIGestureRecognizer *recognizer in view.gestureRecognizers) {
211
+ [into addObject:recognizer];
212
+ }
213
+ for (UIView *subview in view.subviews) {
214
+ ENRMCollectGestures(subview, into);
215
+ }
216
+ }
217
+
218
+ @implementation ENRMSelectionGestureTracker {
219
+ void (^_change)(void);
220
+ NSHashTable<UIGestureRecognizer *> *_gestures;
221
+ }
222
+
223
+ - (instancetype)initWithChange:(void (^)(void))change
224
+ {
225
+ if (self = [super init]) {
226
+ _change = [change copy];
227
+ _gestures = [NSHashTable weakObjectsHashTable];
228
+ }
229
+ return self;
230
+ }
231
+
232
+ - (void)dealloc
233
+ {
234
+ // Recognizers don't retain their targets.
235
+ for (UIGestureRecognizer *recognizer in _gestures) {
236
+ [recognizer removeTarget:self action:@selector(gestureChanged:)];
237
+ }
238
+ }
239
+
240
+ - (void)trackTextView:(UITextView *)textView
241
+ {
242
+ NSMutableArray<UIGestureRecognizer *> *all = [NSMutableArray array];
243
+ ENRMCollectGestures(textView, all);
244
+ for (UIGestureRecognizer *recognizer in all) {
245
+ if ([_gestures containsObject:recognizer]) {
246
+ continue;
247
+ }
248
+ if (recognizer == textView.panGestureRecognizer || recognizer == textView.pinchGestureRecognizer) {
249
+ continue;
250
+ }
251
+ if ([recognizer isKindOfClass:[UITapGestureRecognizer class]]) {
252
+ continue;
253
+ }
254
+ [_gestures addObject:recognizer];
255
+ [recognizer addTarget:self action:@selector(gestureChanged:)];
256
+ }
257
+ }
258
+
259
+ - (void)reset
260
+ {
261
+ _adjusting = NO;
262
+ }
263
+
264
+ - (void)gestureChanged:(UIGestureRecognizer *)recognizer
265
+ {
266
+ BOOL adjusting = NO;
267
+ for (UIGestureRecognizer *tracked in _gestures) {
268
+ UIGestureRecognizerState state = tracked.state;
269
+ if (state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged) {
270
+ adjusting = YES;
271
+ break;
272
+ }
273
+ }
274
+ if (adjusting == _adjusting) {
275
+ return;
276
+ }
277
+ _adjusting = adjusting;
278
+ _change();
279
+ }
280
+
281
+ @end
282
+ #endif
@@ -1,4 +1,5 @@
1
1
  #import "ContextMenuUtils.h"
2
+ #import "ENRMLinkChip.h"
2
3
  #import "ENRMMenuAction.h"
3
4
  #import "EditMenuUtils.h"
4
5
  #import "PasteboardUtils.h"
@@ -19,7 +20,9 @@ NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText,
19
20
  return menu;
20
21
  }
21
22
 
22
- NSAttributedString *selectedText = [attributedText attributedSubstringFromRange:range];
23
+ // Link spacers only reserve layout space; keep them out of every copied flavor.
24
+ NSAttributedString *selectedText =
25
+ ENRMAttributedStringByRemovingLinkSpacers([attributedText attributedSubstringFromRange:range]);
23
26
  NSString *markdown = markdownForRange(attributedText, range, cachedMarkdown);
24
27
  NSArray<NSString *> *imageURLs = imageURLsInRange(attributedText, range);
25
28
 
@@ -9,6 +9,15 @@ NS_ASSUME_NONNULL_BEGIN
9
9
  typedef struct {
10
10
  BOOL copyAsMarkdown;
11
11
  BOOL copyImageURL;
12
+ BOOL copyEnabled;
13
+ // Copy writes Markdown as plain text instead of rich text.
14
+ BOOL copyFormatMarkdown;
15
+ // Drop every system-provided item from the text selection menu.
16
+ BOOL systemItemsNone;
17
+ BOOL codeBlockCopyButton;
18
+ // Copy hands the selection to the owner (JS `onCopy`) instead of writing the
19
+ // pasteboard.
20
+ BOOL interceptCopy;
12
21
  // The owner must keep these strings alive for the duration of the call (the
13
22
  // view holds them in strong ivars).
14
23
  __unsafe_unretained NSString *_Nullable copyLabel;
@@ -18,6 +27,10 @@ typedef struct {
18
27
  __unsafe_unretained NSArray<NSString *> *_Nullable copyImageUrlPluralTemplates;
19
28
  } ENRMSelectionMenuConfig;
20
29
 
30
+ /// Receives an intercepted Copy. `fullSelection` is whether the range spans the
31
+ /// whole attributed text, in which case `markdown` is the cached markdown.
32
+ typedef void (^ENRMCopyRequestHandler)(NSString *markdown, NSString *text, BOOL fullSelection);
33
+
21
34
  /// Resolves the "Copy Image URL(s)" menu title for a given image count.
22
35
  /// Uses the precomputed plural templates (indexed by count, 0..100) when present;
23
36
  /// counts > 100 use the "other" form (copyImageUrlsLabel). Otherwise the
@@ -36,6 +49,13 @@ static inline NSString *ENRMResolveImageURLsTitle(ENRMSelectionMenuConfig config
36
49
  return [titleTemplate stringByReplacingOccurrencesOfString:@"{count}" withString:[@(count) stringValue]];
37
50
  }
38
51
 
52
+ /// Defaults matching the JS-side defaults, used until the first props update.
53
+ static inline ENRMSelectionMenuConfig ENRMDefaultSelectionMenuConfig(void)
54
+ {
55
+ return (ENRMSelectionMenuConfig){
56
+ .copyAsMarkdown = YES, .copyImageURL = YES, .copyEnabled = YES, .codeBlockCopyButton = YES};
57
+ }
58
+
39
59
  #ifdef __cplusplus
40
60
 
41
61
  /// Strong owners for the label strings in ENRMSelectionMenuConfig.
@@ -63,13 +83,18 @@ template <typename T> static inline ENRMSelectionMenuLabels ENRMParseSelectionMe
63
83
  };
64
84
  }
65
85
 
66
- /// Builds an ENRMSelectionMenuConfig from pre-parsed labels and boolean flags.
67
- static inline ENRMSelectionMenuConfig ENRMBuildSelectionMenuConfig(const ENRMSelectionMenuLabels &labels,
68
- bool copyAsMarkdown, bool copyImageUrl)
86
+ /// Builds an ENRMSelectionMenuConfig from pre-parsed labels and the codegen struct's flags.
87
+ template <typename T>
88
+ static inline ENRMSelectionMenuConfig ENRMBuildSelectionMenuConfig(const ENRMSelectionMenuLabels &labels, const T &src)
69
89
  {
70
90
  return (ENRMSelectionMenuConfig){
71
- .copyAsMarkdown = copyAsMarkdown,
72
- .copyImageURL = copyImageUrl,
91
+ .copyAsMarkdown = src.copyAsMarkdown,
92
+ .copyImageURL = src.copyImageUrl,
93
+ .copyEnabled = src.copyEnabled,
94
+ .copyFormatMarkdown = src.copyFormatMarkdown,
95
+ .systemItemsNone = src.systemItemsNone,
96
+ .codeBlockCopyButton = src.codeBlockCopyButton,
97
+ .interceptCopy = src.interceptCopy,
73
98
  .copyLabel = labels.copyLabel,
74
99
  .copyAsMarkdownLabel = labels.copyAsMarkdownLabel,
75
100
  .copyImageUrlLabel = labels.copyImageUrlLabel,
@@ -90,7 +115,11 @@ extern "C" {
90
115
  UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown,
91
116
  StyleConfig *styleConfig, NSArray<UIMenuElement *> *suggestedActions,
92
117
  NSArray<UIAction *> *_Nullable customActions,
93
- ENRMSelectionMenuConfig selectionMenuConfig) API_AVAILABLE(ios(16.0));
118
+ ENRMSelectionMenuConfig selectionMenuConfig,
119
+ ENRMCopyRequestHandler _Nullable onCopyRequest) API_AVAILABLE(ios(16.0));
120
+
121
+ void ENRMSelectRangeAndPresentEditMenu(UITextView *textView, NSRange range);
122
+ void ENRMDismissSelectionEditMenuIfMoved(UITextView *textView);
94
123
  #else
95
124
  NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range,
96
125
  NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig,
@@ -1,4 +1,5 @@
1
1
  #import "EditMenuUtils.h"
2
+ #import "ENRMLinkChip.h"
2
3
  #import "PasteboardUtils.h"
3
4
  #import "StyleConfig.h"
4
5
  #include <TargetConditionals.h>
@@ -10,14 +11,34 @@ static NSString *const kActionIdentifierCopy = @"com.swmansion.enriched.markdown
10
11
  static NSString *const kActionIdentifierCopyMarkdown = @"com.swmansion.enriched.markdown.copyMarkdown";
11
12
  static NSString *const kActionIdentifierCopyImageURL = @"com.swmansion.enriched.markdown.copyImageURL";
12
13
 
13
- static UIAction *createCopyAction(NSAttributedString *selectedText, NSString *markdown, StyleConfig *styleConfig,
14
- NSString *copyLabel)
14
+ static UIAction *_Nullable createCopyAction(NSAttributedString *selectedText, NSString *markdown, BOOL fullSelection,
15
+ StyleConfig *styleConfig, ENRMSelectionMenuConfig selectionMenuConfig,
16
+ ENRMCopyRequestHandler _Nullable onCopyRequest)
15
17
  {
16
- return [UIAction actionWithTitle:copyLabel
18
+ if (!selectionMenuConfig.copyEnabled)
19
+ return nil;
20
+
21
+ if (selectionMenuConfig.interceptCopy && onCopyRequest) {
22
+ NSString *text = selectedText.string;
23
+ NSString *copied = markdown.length > 0 ? markdown : text;
24
+ return [UIAction actionWithTitle:selectionMenuConfig.copyLabel
25
+ image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
26
+ identifier:kActionIdentifierCopy
27
+ handler:^(__kindof UIAction *action) { onCopyRequest(copied, text, fullSelection); }];
28
+ }
29
+
30
+ BOOL writesMarkdown = selectionMenuConfig.copyFormatMarkdown;
31
+ return [UIAction actionWithTitle:selectionMenuConfig.copyLabel
17
32
  image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
18
33
  identifier:kActionIdentifierCopy
19
34
  handler:^(__kindof UIAction *action) {
20
- copyAttributedStringToPasteboard(selectedText, markdown, styleConfig);
35
+ if (!writesMarkdown) {
36
+ copyAttributedStringToPasteboard(selectedText, markdown, styleConfig);
37
+ } else {
38
+ // Plain text only: with HTML/RTF alongside, rich-paste targets
39
+ // would take those and drop the Markdown.
40
+ copyStringToPasteboard(markdown.length > 0 ? markdown : selectedText.string);
41
+ }
21
42
  }];
22
43
  }
23
44
 
@@ -48,10 +69,11 @@ static UIAction *_Nullable createCopyImageURLAction(NSArray<NSString *> *imageUR
48
69
  }
49
70
 
50
71
  /// Rebuilds the standard-edit menu with the system Copy swapped for the enhanced
51
- /// copy action. All other system items are kept — dropping them would remove
52
- /// Select All, which on iOS 16+ is the only way to start a selection from the
53
- /// long-press menu of a non-editable text view.
54
- static UIMenu *createEnhancedStandardEditMenu(UIMenu *originalMenu, UIAction *copyAction)
72
+ /// copy action, or dropped when `copyAction` is nil (Copy disabled). All other
73
+ /// system items are kept — dropping them would remove Select All, which on
74
+ /// iOS 16+ is the only way to start a selection from the long-press menu of a
75
+ /// non-editable text view.
76
+ static UIMenu *createEnhancedStandardEditMenu(UIMenu *originalMenu, UIAction *_Nullable copyAction)
55
77
  {
56
78
  NSMutableArray<UIMenuElement *> *children = [NSMutableArray arrayWithCapacity:originalMenu.children.count];
57
79
  BOOL replacedCopy = NO;
@@ -59,14 +81,16 @@ static UIMenu *createEnhancedStandardEditMenu(UIMenu *originalMenu, UIAction *co
59
81
  for (UIMenuElement *element in originalMenu.children) {
60
82
  if (!replacedCopy && [element isKindOfClass:[UICommand class]] &&
61
83
  ((UICommand *)element).action == @selector(copy:)) {
62
- [children addObject:copyAction];
84
+ if (copyAction) {
85
+ [children addObject:copyAction];
86
+ }
63
87
  replacedCopy = YES;
64
88
  continue;
65
89
  }
66
90
  [children addObject:element];
67
91
  }
68
92
 
69
- if (!replacedCopy) {
93
+ if (!replacedCopy && copyAction) {
70
94
  [children insertObject:copyAction atIndex:0];
71
95
  }
72
96
 
@@ -95,13 +119,19 @@ static void insertOptionalAction(NSMutableArray<UIMenuElement *> *array, UIActio
95
119
  UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown,
96
120
  StyleConfig *styleConfig, NSArray<UIMenuElement *> *suggestedActions,
97
121
  NSArray<UIAction *> *_Nullable customActions,
98
- ENRMSelectionMenuConfig selectionMenuConfig) API_AVAILABLE(ios(16.0))
122
+ ENRMSelectionMenuConfig selectionMenuConfig,
123
+ ENRMCopyRequestHandler _Nullable onCopyRequest) API_AVAILABLE(ios(16.0))
99
124
  {
100
- NSAttributedString *selectedText = [attributedText attributedSubstringFromRange:range];
125
+ // Link spacers only reserve layout space; keep them out of every copied flavor.
126
+ NSAttributedString *selectedText =
127
+ ENRMAttributedStringByRemovingLinkSpacers([attributedText attributedSubstringFromRange:range]);
101
128
  NSString *markdown = markdownForRange(attributedText, range, cachedMarkdown);
102
129
  NSArray<NSString *> *imageURLs = imageURLsInRange(attributedText, range);
130
+ // Same threshold markdownForRange uses to return the cached markdown.
131
+ BOOL fullSelection = range.location == 0 && range.length >= attributedText.length - 1;
103
132
 
104
- UIAction *copyAction = createCopyAction(selectedText, markdown, styleConfig, selectionMenuConfig.copyLabel);
133
+ UIAction *copyAction =
134
+ createCopyAction(selectedText, markdown, fullSelection, styleConfig, selectionMenuConfig, onCopyRequest);
105
135
  UIAction *copyMarkdownAction = selectionMenuConfig.copyAsMarkdown
106
136
  ? createCopyMarkdownAction(markdown, selectionMenuConfig.copyAsMarkdownLabel)
107
137
  : nil;
@@ -109,6 +139,16 @@ UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange ra
109
139
  selectionMenuConfig.copyImageURL ? createCopyImageURLAction(imageURLs, selectionMenuConfig) : nil;
110
140
 
111
141
  NSMutableArray<UIMenuElement *> *result = [NSMutableArray array];
142
+
143
+ if (selectionMenuConfig.systemItemsNone) {
144
+ // Skip suggestedActions entirely. An empty result still yields an (empty)
145
+ // menu: returning nil would make UIKit present its default menu.
146
+ addOptionalAction(result, copyAction);
147
+ addOptionalAction(result, copyMarkdownAction);
148
+ addOptionalAction(result, copyImageURLAction);
149
+ return [UIMenu menuWithChildren:[(customActions ?: @[]) arrayByAddingObjectsFromArray:result]];
150
+ }
151
+
112
152
  BOOL foundStandardEditMenu = NO;
113
153
 
114
154
  for (UIMenuElement *element in suggestedActions) {
@@ -128,8 +168,8 @@ UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange ra
128
168
  }
129
169
 
130
170
  if (!foundStandardEditMenu) {
131
- [result insertObject:copyAction atIndex:0];
132
- insertOptionalAction(result, copyMarkdownAction, 1);
171
+ insertOptionalAction(result, copyMarkdownAction, 0);
172
+ insertOptionalAction(result, copyAction, 0);
133
173
  addOptionalAction(result, copyImageURLAction);
134
174
  }
135
175
 
@@ -140,4 +180,120 @@ UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange ra
140
180
  return [UIMenu menuWithChildren:result];
141
181
  }
142
182
 
183
+ #import <objc/runtime.h>
184
+
185
+ static const void *kSelectionEditMenuPresenterKey = &kSelectionEditMenuPresenterKey;
186
+
187
+ API_AVAILABLE(ios(16.0))
188
+ @interface ENRMSelectionEditMenuPresenter : NSObject <UIEditMenuInteractionDelegate>
189
+ @property (nonatomic, weak) UITextView *textView;
190
+ @property (nonatomic, strong) UIEditMenuInteraction *interaction;
191
+ @property (nonatomic, assign) NSRange range;
192
+ @end
193
+
194
+ @implementation ENRMSelectionEditMenuPresenter
195
+
196
+ - (UIMenu *)editMenuInteraction:(UIEditMenuInteraction *)interaction
197
+ menuForConfiguration:(UIEditMenuConfiguration *)configuration
198
+ suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions
199
+ {
200
+ UITextView *textView = self.textView;
201
+ id<UITextViewDelegate> delegate = textView.delegate;
202
+ if (!textView || ![delegate respondsToSelector:@selector(textView:editMenuForTextInRange:suggestedActions:)]) {
203
+ return nil;
204
+ }
205
+ return [delegate textView:textView editMenuForTextInRange:textView.selectedRange suggestedActions:suggestedActions];
206
+ }
207
+
208
+ - (CGRect)editMenuInteraction:(UIEditMenuInteraction *)interaction
209
+ targetRectForConfiguration:(UIEditMenuConfiguration *)configuration
210
+ {
211
+ UITextView *textView = self.textView;
212
+ UITextRange *textRange = textView.selectedTextRange;
213
+ if (!textView || !textRange) {
214
+ return CGRectNull;
215
+ }
216
+ return [textView firstRectForRange:textRange];
217
+ }
218
+
219
+ @end
220
+
221
+ static BOOL selectionEditMenuTextViewHasActiveGesture(UITextView *textView)
222
+ {
223
+ for (UIGestureRecognizer *recognizer in textView.gestureRecognizers) {
224
+ if (recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged) {
225
+ return YES;
226
+ }
227
+ }
228
+ return NO;
229
+ }
230
+
231
+ static void presentSelectionEditMenu(UITextView *textView)
232
+ {
233
+ UITextRange *textRange = textView.selectedTextRange;
234
+ if (!textRange || textRange.isEmpty) {
235
+ return;
236
+ }
237
+ CGRect rect = [textView firstRectForRange:textRange];
238
+ if (CGRectIsNull(rect) || CGRectIsEmpty(rect)) {
239
+ return;
240
+ }
241
+ if (@available(iOS 16.0, *)) {
242
+ ENRMSelectionEditMenuPresenter *presenter = objc_getAssociatedObject(textView, kSelectionEditMenuPresenterKey);
243
+ if (!presenter) {
244
+ presenter = [[ENRMSelectionEditMenuPresenter alloc] init];
245
+ presenter.textView = textView;
246
+ presenter.interaction = [[UIEditMenuInteraction alloc] initWithDelegate:presenter];
247
+ [textView addInteraction:presenter.interaction];
248
+ objc_setAssociatedObject(textView, kSelectionEditMenuPresenterKey, presenter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
249
+ }
250
+ presenter.range = textView.selectedRange;
251
+ UIEditMenuConfiguration *configuration =
252
+ [UIEditMenuConfiguration configurationWithIdentifier:nil
253
+ sourcePoint:CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect))];
254
+ [presenter.interaction presentEditMenuWithConfiguration:configuration];
255
+ return;
256
+ }
257
+ #pragma clang diagnostic push
258
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
259
+ [[UIMenuController sharedMenuController] showMenuFromView:textView rect:rect];
260
+ #pragma clang diagnostic pop
261
+ }
262
+
263
+ static void presentSelectionEditMenuWhenTouchEnds(UITextView *textView, NSRange range, NSUInteger attemptsLeft)
264
+ {
265
+ if (!NSEqualRanges(textView.selectedRange, range)) {
266
+ return;
267
+ }
268
+ if (attemptsLeft > 0 && selectionEditMenuTextViewHasActiveGesture(textView)) {
269
+ __weak UITextView *weakTextView = textView;
270
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(50 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
271
+ UITextView *strongTextView = weakTextView;
272
+ if (strongTextView) {
273
+ presentSelectionEditMenuWhenTouchEnds(strongTextView, range, attemptsLeft - 1);
274
+ }
275
+ });
276
+ return;
277
+ }
278
+ presentSelectionEditMenu(textView);
279
+ }
280
+
281
+ void ENRMSelectRangeAndPresentEditMenu(UITextView *textView, NSRange range)
282
+ {
283
+ [textView becomeFirstResponder];
284
+ textView.selectedRange = range;
285
+ presentSelectionEditMenuWhenTouchEnds(textView, range, 200);
286
+ }
287
+
288
+ void ENRMDismissSelectionEditMenuIfMoved(UITextView *textView)
289
+ {
290
+ if (@available(iOS 16.0, *)) {
291
+ ENRMSelectionEditMenuPresenter *presenter = objc_getAssociatedObject(textView, kSelectionEditMenuPresenterKey);
292
+ if (presenter && !NSEqualRanges(textView.selectedRange, presenter.range)) {
293
+ presenter.range = NSMakeRange(NSNotFound, 0);
294
+ [presenter.interaction dismissMenu];
295
+ }
296
+ }
297
+ }
298
+
143
299
  #endif
@@ -3,6 +3,7 @@
3
3
  #import "CodeBackground.h"
4
4
  #import "ENRMFeatureFlags.h"
5
5
  #import "ENRMImageAttachment.h"
6
+ #import "ENRMLinkChip.h"
6
7
  #import "HighlightRenderer.h"
7
8
  #if ENRICHED_MARKDOWN_MATH
8
9
  #import "ENRMMathInlineAttachment.h"
@@ -460,7 +461,7 @@ static void generateInlineHTML(NSMutableString *html, NSAttributedString *attrib
460
461
  usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
461
462
  NSString *text = [string substringWithRange:attrRange];
462
463
 
463
- if ([text isEqualToString:@"\n"])
464
+ if ([text isEqualToString:@"\n"] || attrs[ENRMLinkSpacerAttributeName])
464
465
  return;
465
466
 
466
467
  if ([text containsString:kObjectReplacementChar]) {
@@ -2,6 +2,7 @@
2
2
  #import "AccessibilityInfo.h"
3
3
  #import "BlockquoteBorder.h"
4
4
  #import "ENRMAccessibilityLabels.h"
5
+ #import "ENRMLinkChip.h"
5
6
  #include <TargetConditionals.h>
6
7
 
7
8
  typedef NS_ENUM(NSInteger, ElementType) { ElementTypeText, ElementTypeLink, ElementTypeImage };
@@ -113,7 +114,8 @@ static const CGFloat kFocusRectPadding = 2.0;
113
114
  }
114
115
 
115
116
  BOOL isImg = item[@"altText"] != nil;
116
- NSString *label = isImg ? item[@"altText"] : [fullText substringWithRange:itemRange];
117
+ // Link chips pad their text with spacer characters; keep them out of the spoken label.
118
+ NSString *label = isImg ? item[@"altText"] : ENRMPlainTextByRemovingLinkSpacers(textView.attributedText, itemRange);
117
119
  [elements addObject:[self createElementForRange:itemRange
118
120
  type:isImg ? ElementTypeImage : ElementTypeLink
119
121
  text:label
@@ -2,6 +2,7 @@
2
2
  #import "BlockquoteBorder.h"
3
3
  #import "ENRMFeatureFlags.h"
4
4
  #import "ENRMImageAttachment.h"
5
+ #import "ENRMLinkChip.h"
5
6
  #import "ENRMUIKit.h"
6
7
  #import "HighlightRenderer.h"
7
8
  #include <TargetConditionals.h>
@@ -152,7 +153,7 @@ NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attr
152
153
  options:0
153
154
  usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
154
155
  NSString *text = [[attributedText attributedSubstringFromRange:attrRange] string];
155
- if (text.length == 0)
156
+ if (text.length == 0 || attrs[ENRMLinkSpacerAttributeName])
156
157
  return;
157
158
 
158
159
  // Images and Thematic Breaks