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
@@ -0,0 +1,50 @@
1
+ #pragma once
2
+ #import "ENRMUIKit.h"
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ #ifdef __cplusplus
7
+ extern "C" {
8
+ #endif
9
+
10
+ /// Collects the chip rect of every link fragment laid out in `textView`, for
11
+ /// the `onLinkLayout` event. Each entry is
12
+ /// `@{linkIndex, url, text, x, y, width, height}` (NSNumber/NSString), so a
13
+ /// payload compares with `-isEqualToArray:` for deduplication.
14
+ ///
15
+ /// - `offset` is added to every rect (a segment's origin inside its container).
16
+ /// - Links are numbered in document order starting at `indexBase`; the number
17
+ /// of links found is written to `outLinkCount` so the next segment continues.
18
+ ///
19
+ /// Rects are in points relative to the text view, which the markdown views
20
+ /// configure with zero insets and no line-fragment padding.
21
+ NSArray<NSDictionary *> *ENRMCollectLinkLayoutFragments(ENRMPlatformTextView *textView, CGPoint offset,
22
+ NSInteger indexBase, NSInteger *_Nullable outLinkCount);
23
+
24
+ #ifdef __cplusplus
25
+ }
26
+ #endif
27
+
28
+ #ifdef __cplusplus
29
+ /// Converts collected fragments into a codegen `OnLinkLayout` payload. Templated
30
+ /// because each component has its own (structurally identical) event struct.
31
+ template <typename Event> inline Event ENRMLinkLayoutEventFromFragments(NSArray<NSDictionary *> *fragments)
32
+ {
33
+ Event event;
34
+ event.fragments.reserve(fragments.count);
35
+ for (NSDictionary *fragment in fragments) {
36
+ typename decltype(event.fragments)::value_type item;
37
+ item.linkIndex = [fragment[@"linkIndex"] intValue];
38
+ item.url = std::string([fragment[@"url"] UTF8String] ?: "");
39
+ item.text = std::string([fragment[@"text"] UTF8String] ?: "");
40
+ item.x = [fragment[@"x"] doubleValue];
41
+ item.y = [fragment[@"y"] doubleValue];
42
+ item.width = [fragment[@"width"] doubleValue];
43
+ item.height = [fragment[@"height"] doubleValue];
44
+ event.fragments.push_back(item);
45
+ }
46
+ return event;
47
+ }
48
+ #endif
49
+
50
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,45 @@
1
+ #import "ENRMLinkLayout.h"
2
+ #import "ENRMLinkChip.h"
3
+
4
+ NSArray<NSDictionary *> *ENRMCollectLinkLayoutFragments(ENRMPlatformTextView *textView, CGPoint offset,
5
+ NSInteger indexBase, NSInteger *outLinkCount)
6
+ {
7
+ NSLayoutManager *layoutManager = textView.layoutManager;
8
+ NSTextContainer *textContainer = textView.textContainer;
9
+ NSTextStorage *storage = layoutManager.textStorage;
10
+ NSMutableArray<NSDictionary *> *fragments = [NSMutableArray array];
11
+ __block NSInteger linkIndex = indexBase;
12
+
13
+ if (storage.length > 0 && textContainer != nil) {
14
+ // LinkRenderer attaches one ENRMLinkChip instance per link, so each
15
+ // attribute run is exactly one link (adjacent links never coalesce).
16
+ [storage enumerateAttribute:ENRMLinkChipAttributeName
17
+ inRange:NSMakeRange(0, storage.length)
18
+ options:0
19
+ usingBlock:^(ENRMLinkChip *chip, NSRange range, BOOL *stop) {
20
+ if (!chip)
21
+ return;
22
+ id link = [storage attribute:NSLinkAttributeName atIndex:range.location effectiveRange:NULL];
23
+ NSString *url = [link isKindOfClass:[NSURL class]] ? [(NSURL *)link absoluteString] : link;
24
+ NSString *text = ENRMPlainTextByRemovingLinkSpacers(storage, range);
25
+ NSNumber *index = @(linkIndex++);
26
+
27
+ ENRMEnumerateLinkChipFragments(layoutManager, textContainer, range, chip,
28
+ ^(CGRect rect, BOOL isFirst, BOOL isLast) {
29
+ [fragments addObject:@{
30
+ @"linkIndex" : index,
31
+ @"url" : url ?: @"",
32
+ @"text" : text,
33
+ @"x" : @(rect.origin.x + offset.x),
34
+ @"y" : @(rect.origin.y + offset.y),
35
+ @"width" : @(rect.size.width),
36
+ @"height" : @(rect.size.height),
37
+ }];
38
+ });
39
+ }];
40
+ }
41
+
42
+ if (outLinkCount)
43
+ *outLinkCount = linkIndex - indexBase;
44
+ return fragments;
45
+ }
@@ -0,0 +1,96 @@
1
+ #pragma once
2
+ #import "ENRMUIKit.h"
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ /// Keys of an `onSelectionChange` payload. Rects are `NSValue`-wrapped CGRects in
7
+ /// the reporting view's coordinates, so a payload compares with `-isEqual:` for
8
+ /// deduplication.
9
+ extern NSString *const ENRMSelectionActiveKey;
10
+ extern NSString *const ENRMSelectionSourceKey;
11
+ extern NSString *const ENRMSelectionTextKey;
12
+ extern NSString *const ENRMSelectionMarkdownKey;
13
+ extern NSString *const ENRMSelectionWholeKey;
14
+ extern NSString *const ENRMSelectionRectKey;
15
+ extern NSString *const ENRMSelectionStartRectKey;
16
+ extern NSString *const ENRMSelectionEndRectKey;
17
+ /// Only in `ENRMTextSelectionInTextView` results: whether the range spans the
18
+ /// whole text (same threshold `markdownForRange` uses for the cached markdown).
19
+ extern NSString *const ENRMSelectionFullKey;
20
+
21
+ #ifdef __cplusplus
22
+ extern "C" {
23
+ #endif
24
+
25
+ /// The payload reported once the selection is gone.
26
+ NSDictionary *ENRMInactiveSelectionPayload(void);
27
+
28
+ /// A long-pressed block (table / math / code): `whole`, every rect is `frame`.
29
+ NSDictionary *ENRMBlockSelectionPayload(NSString *source, NSString *markdown, NSString *text, CGRect frame);
30
+
31
+ #if !TARGET_OS_OSX
32
+ /// Text, full-selection flag and rects of `textView`'s selection, converted to
33
+ /// `targetView`'s coordinates; nil when nothing is selected. `rect` is the union
34
+ /// of the selection rects, `startRect` / `endRect` the rects holding its start
35
+ /// and end. The owner adds source, markdown and whole to build the payload.
36
+ NSDictionary *_Nullable ENRMTextSelectionInTextView(UITextView *textView, UIView *targetView);
37
+ #endif
38
+
39
+ #ifdef __cplusplus
40
+ }
41
+ #endif
42
+
43
+ /// Coalesces selection changes into at most one report per run loop turn. The
44
+ /// payload is read when the turn fires, so the latest state always reaches JS;
45
+ /// a cleared selection is reported right away and drops any pending update.
46
+ /// Identical payloads are sent once.
47
+ @interface ENRMSelectionChangeReporter : NSObject
48
+
49
+ - (instancetype)initWithSnapshot:(NSDictionary * (^)(void))snapshot
50
+ emit:(void (^)(NSDictionary *payload))emit NS_DESIGNATED_INITIALIZER;
51
+ - (instancetype)init NS_UNAVAILABLE;
52
+
53
+ /// Off by default. Toggling resets the last reported payload to inactive.
54
+ @property (nonatomic, assign) BOOL enabled;
55
+
56
+ - (void)schedule;
57
+ - (void)selectionChanged:(BOOL)hasSelection;
58
+ - (void)flushNow;
59
+ - (void)cancel;
60
+ /// Makes the next report go out even if it equals the last one — a block
61
+ /// long-pressed again is a new request for the app's menu, not a repeat.
62
+ - (void)forgetLastReported;
63
+
64
+ @end
65
+
66
+ #ifdef __cplusplus
67
+ /// Converts a payload into a codegen `OnSelectionChange` struct. Templated
68
+ /// because each component has its own (structurally identical) event struct.
69
+ template <typename Event> inline Event ENRMSelectionChangeEventFromPayload(NSDictionary *payload)
70
+ {
71
+ Event event;
72
+ event.active = [payload[ENRMSelectionActiveKey] boolValue];
73
+ event.source = std::string([payload[ENRMSelectionSourceKey] UTF8String] ?: "selection");
74
+ event.text = std::string([payload[ENRMSelectionTextKey] UTF8String] ?: "");
75
+ event.markdown = std::string([payload[ENRMSelectionMarkdownKey] UTF8String] ?: "");
76
+ event.whole = [payload[ENRMSelectionWholeKey] boolValue];
77
+
78
+ auto assign = [](auto &target, NSValue *value) {
79
+ #if TARGET_OS_OSX
80
+ CGRect rect = value ? NSRectToCGRect(value.rectValue) : CGRectZero;
81
+ #else
82
+ CGRect rect = value ? value.CGRectValue : CGRectZero;
83
+ #endif
84
+ target.x = rect.origin.x;
85
+ target.y = rect.origin.y;
86
+ target.width = rect.size.width;
87
+ target.height = rect.size.height;
88
+ };
89
+ assign(event.rect, payload[ENRMSelectionRectKey]);
90
+ assign(event.startRect, payload[ENRMSelectionStartRectKey]);
91
+ assign(event.endRect, payload[ENRMSelectionEndRectKey]);
92
+ return event;
93
+ }
94
+ #endif
95
+
96
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,204 @@
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 ENRMSelectionFullKey = @"full";
13
+
14
+ static NSValue *ENRMRectValue(CGRect rect)
15
+ {
16
+ #if TARGET_OS_OSX
17
+ return [NSValue valueWithRect:NSRectFromCGRect(rect)];
18
+ #else
19
+ return [NSValue valueWithCGRect:rect];
20
+ #endif
21
+ }
22
+
23
+ NSDictionary *ENRMInactiveSelectionPayload(void)
24
+ {
25
+ static NSDictionary *payload;
26
+ static dispatch_once_t onceToken;
27
+ dispatch_once(&onceToken, ^{
28
+ NSValue *zero = ENRMRectValue(CGRectZero);
29
+ payload = @{
30
+ ENRMSelectionActiveKey : @NO,
31
+ ENRMSelectionSourceKey : @"selection",
32
+ ENRMSelectionTextKey : @"",
33
+ ENRMSelectionMarkdownKey : @"",
34
+ ENRMSelectionWholeKey : @NO,
35
+ ENRMSelectionRectKey : zero,
36
+ ENRMSelectionStartRectKey : zero,
37
+ ENRMSelectionEndRectKey : zero,
38
+ };
39
+ });
40
+ return payload;
41
+ }
42
+
43
+ NSDictionary *ENRMBlockSelectionPayload(NSString *source, NSString *markdown, NSString *text, CGRect frame)
44
+ {
45
+ NSValue *rect = ENRMRectValue(frame);
46
+ return @{
47
+ ENRMSelectionActiveKey : @YES,
48
+ ENRMSelectionSourceKey : source,
49
+ ENRMSelectionTextKey : text ?: @"",
50
+ ENRMSelectionMarkdownKey : markdown ?: @"",
51
+ ENRMSelectionWholeKey : @YES,
52
+ ENRMSelectionRectKey : rect,
53
+ ENRMSelectionStartRectKey : rect,
54
+ ENRMSelectionEndRectKey : rect,
55
+ };
56
+ }
57
+
58
+ #if !TARGET_OS_OSX
59
+ NSDictionary *ENRMTextSelectionInTextView(UITextView *textView, UIView *targetView)
60
+ {
61
+ NSRange range = textView.selectedRange;
62
+ UITextRange *textRange = textView.selectedTextRange;
63
+ if (range.location == NSNotFound || range.length == 0 || textRange == nil || textRange.isEmpty) {
64
+ return nil;
65
+ }
66
+
67
+ NSAttributedString *attributedText = textView.attributedText;
68
+ if (NSMaxRange(range) > attributedText.length) {
69
+ return nil;
70
+ }
71
+
72
+ CGRect unionRect = CGRectNull;
73
+ CGRect startRect = CGRectNull;
74
+ CGRect endRect = CGRectNull;
75
+ CGRect firstRect = CGRectNull;
76
+ CGRect lastRect = CGRectNull;
77
+ for (UITextSelectionRect *selectionRect in [textView selectionRectsForRange:textRange]) {
78
+ CGRect rect = selectionRect.rect;
79
+ if (CGRectIsNull(rect) || CGRectIsInfinite(rect) || CGRectIsEmpty(rect)) {
80
+ continue;
81
+ }
82
+ unionRect = CGRectUnion(unionRect, rect);
83
+ if (CGRectIsNull(firstRect)) {
84
+ firstRect = rect;
85
+ }
86
+ lastRect = rect;
87
+ if (selectionRect.containsStart && CGRectIsNull(startRect)) {
88
+ startRect = rect;
89
+ }
90
+ if (selectionRect.containsEnd) {
91
+ endRect = rect;
92
+ }
93
+ }
94
+ // Not laid out yet: report once layout gives the range a position.
95
+ if (CGRectIsNull(unionRect)) {
96
+ return nil;
97
+ }
98
+ if (CGRectIsNull(startRect)) {
99
+ startRect = firstRect;
100
+ }
101
+ if (CGRectIsNull(endRect)) {
102
+ endRect = lastRect;
103
+ }
104
+
105
+ return @{
106
+ ENRMSelectionTextKey : ENRMPlainTextByRemovingLinkSpacers(attributedText, range),
107
+ ENRMSelectionFullKey : @(range.location == 0 && range.length >= attributedText.length - 1),
108
+ ENRMSelectionRectKey : ENRMRectValue([textView convertRect:unionRect toView:targetView]),
109
+ ENRMSelectionStartRectKey : ENRMRectValue([textView convertRect:startRect toView:targetView]),
110
+ ENRMSelectionEndRectKey : ENRMRectValue([textView convertRect:endRect toView:targetView]),
111
+ };
112
+ }
113
+ #endif
114
+
115
+ @implementation ENRMSelectionChangeReporter {
116
+ NSDictionary * (^_snapshot)(void);
117
+ void (^_emit)(NSDictionary *);
118
+ NSDictionary *_lastReported;
119
+ BOOL _pending;
120
+ // Bumped on cancel so a scheduled block that already left the queue is a no-op.
121
+ NSUInteger _generation;
122
+ }
123
+
124
+ - (instancetype)initWithSnapshot:(NSDictionary * (^)(void))snapshot emit:(void (^)(NSDictionary *))emit
125
+ {
126
+ if (self = [super init]) {
127
+ _snapshot = [snapshot copy];
128
+ _emit = [emit copy];
129
+ _lastReported = ENRMInactiveSelectionPayload();
130
+ }
131
+ return self;
132
+ }
133
+
134
+ - (void)setEnabled:(BOOL)enabled
135
+ {
136
+ if (_enabled == enabled) {
137
+ return;
138
+ }
139
+ _enabled = enabled;
140
+ [self cancel];
141
+ _lastReported = ENRMInactiveSelectionPayload();
142
+ if (enabled) {
143
+ [self schedule];
144
+ }
145
+ }
146
+
147
+ - (void)schedule
148
+ {
149
+ if (!_enabled || _pending) {
150
+ return;
151
+ }
152
+ _pending = YES;
153
+ NSUInteger generation = _generation;
154
+ __weak ENRMSelectionChangeReporter *weakSelf = self;
155
+ dispatch_async(dispatch_get_main_queue(), ^{
156
+ ENRMSelectionChangeReporter *strongSelf = weakSelf;
157
+ if (!strongSelf || !strongSelf->_pending || strongSelf->_generation != generation) {
158
+ return;
159
+ }
160
+ strongSelf->_pending = NO;
161
+ [strongSelf report];
162
+ });
163
+ }
164
+
165
+ - (void)selectionChanged:(BOOL)hasSelection
166
+ {
167
+ if (hasSelection) {
168
+ [self schedule];
169
+ } else {
170
+ [self flushNow];
171
+ }
172
+ }
173
+
174
+ - (void)flushNow
175
+ {
176
+ [self cancel];
177
+ [self report];
178
+ }
179
+
180
+ - (void)forgetLastReported
181
+ {
182
+ _lastReported = ENRMInactiveSelectionPayload();
183
+ }
184
+
185
+ - (void)cancel
186
+ {
187
+ _pending = NO;
188
+ _generation++;
189
+ }
190
+
191
+ - (void)report
192
+ {
193
+ if (!_enabled) {
194
+ return;
195
+ }
196
+ NSDictionary *current = _snapshot();
197
+ if ([current isEqualToDictionary:_lastReported]) {
198
+ return;
199
+ }
200
+ _lastReported = current;
201
+ _emit(current);
202
+ }
203
+
204
+ @end
@@ -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
 
@@ -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>
@@ -121,7 +122,9 @@ UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange ra
121
122
  ENRMSelectionMenuConfig selectionMenuConfig,
122
123
  ENRMCopyRequestHandler _Nullable onCopyRequest) API_AVAILABLE(ios(16.0))
123
124
  {
124
- 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]);
125
128
  NSString *markdown = markdownForRange(attributedText, range, cachedMarkdown);
126
129
  NSArray<NSString *> *imageURLs = imageURLsInRange(attributedText, range);
127
130
  // Same threshold markdownForRange uses to return the cached markdown.
@@ -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
@@ -1,6 +1,7 @@
1
1
  #import "RTFExportUtils.h"
2
2
  #import "BlockquoteBorder.h"
3
3
  #import "CodeBackground.h"
4
+ #import "ENRMLinkChip.h"
4
5
  #import "LastElementUtils.h"
5
6
  #import "ListItemRenderer.h"
6
7
  #import "RenderContext.h"
@@ -288,6 +289,7 @@ NSAttributedString *prepareAttributedStringForRTFExport(NSAttributedString *attr
288
289
  RCTUIColor *blockquoteBgColor = [styleConfig blockquoteBackgroundColor];
289
290
 
290
291
  processThematicBreaks(prepared);
292
+ ENRMApplyLinkChipBackgroundsAsTextBackground(prepared);
291
293
  processCodes(prepared, codeBgColor);
292
294
  processCodeBlocks(prepared, codeBlockBgColor);
293
295
  processBlockquotes(prepared, blockquoteBgColor);
@@ -523,6 +523,32 @@ BOOL applyMarkdownStyleToConfig(StyleConfig *config, const MarkdownStyle &newSty
523
523
  changed = YES;
524
524
  }
525
525
 
526
+ if (newStyle.link.borderRadius != oldStyle.link.borderRadius) {
527
+ [config setLinkBorderRadius:newStyle.link.borderRadius];
528
+ changed = YES;
529
+ }
530
+
531
+ if (newStyle.link.paddingHorizontal != oldStyle.link.paddingHorizontal) {
532
+ [config setLinkPaddingHorizontal:newStyle.link.paddingHorizontal];
533
+ changed = YES;
534
+ }
535
+
536
+ if (newStyle.link.paddingVertical != oldStyle.link.paddingVertical) {
537
+ [config setLinkPaddingVertical:newStyle.link.paddingVertical];
538
+ changed = YES;
539
+ }
540
+
541
+ if (newStyle.link.borderColor != oldStyle.link.borderColor) {
542
+ RCTUIColor *color = RCTUIColorFromSharedColor(newStyle.link.borderColor);
543
+ [config setLinkBorderColor:CGColorGetAlpha(color.CGColor) > 0 ? color : nil];
544
+ changed = YES;
545
+ }
546
+
547
+ if (newStyle.link.borderWidth != oldStyle.link.borderWidth) {
548
+ [config setLinkBorderWidth:newStyle.link.borderWidth];
549
+ changed = YES;
550
+ }
551
+
526
552
  {
527
553
  BOOL linkVariantsChanged = newStyle.linkVariants.size() != oldStyle.linkVariants.size();
528
554
  if (!linkVariantsChanged) {
@@ -530,7 +556,11 @@ BOOL applyMarkdownStyleToConfig(StyleConfig *config, const MarkdownStyle &newSty
530
556
  const auto &newVariant = newStyle.linkVariants[i];
531
557
  const auto &oldVariant = oldStyle.linkVariants[i];
532
558
  if (newVariant.pattern != oldVariant.pattern || newVariant.color != oldVariant.color ||
533
- newVariant.underline != oldVariant.underline || newVariant.backgroundColor != oldVariant.backgroundColor) {
559
+ newVariant.underline != oldVariant.underline || newVariant.backgroundColor != oldVariant.backgroundColor ||
560
+ newVariant.borderRadius != oldVariant.borderRadius ||
561
+ newVariant.paddingHorizontal != oldVariant.paddingHorizontal ||
562
+ newVariant.paddingVertical != oldVariant.paddingVertical ||
563
+ newVariant.borderColor != oldVariant.borderColor || newVariant.borderWidth != oldVariant.borderWidth) {
534
564
  linkVariantsChanged = YES;
535
565
  break;
536
566
  }
@@ -545,6 +575,12 @@ BOOL applyMarkdownStyleToConfig(StyleConfig *config, const MarkdownStyle &newSty
545
575
  variant.underline = entry.underline;
546
576
  RCTUIColor *backgroundColor = RCTUIColorFromSharedColor(entry.backgroundColor);
547
577
  variant.backgroundColor = CGColorGetAlpha(backgroundColor.CGColor) > 0 ? backgroundColor : nil;
578
+ variant.borderRadius = entry.borderRadius;
579
+ variant.paddingHorizontal = entry.paddingHorizontal;
580
+ variant.paddingVertical = entry.paddingVertical;
581
+ RCTUIColor *borderColor = RCTUIColorFromSharedColor(entry.borderColor);
582
+ variant.borderColor = CGColorGetAlpha(borderColor.CGColor) > 0 ? borderColor : nil;
583
+ variant.borderWidth = entry.borderWidth;
548
584
  [variants addObject:variant];
549
585
  }
550
586
  [config setLinkVariants:variants];
@@ -2,6 +2,7 @@
2
2
  #import "BlockquoteBorder.h"
3
3
  #import "CodeBackground.h"
4
4
  #import "CodeBlockBackground.h"
5
+ #import "ENRMLinkChip.h"
5
6
  #import "ListMarkerDrawer.h"
6
7
  #import "RuntimeKeys.h"
7
8
  #import "StyleConfig.h"
@@ -31,6 +32,8 @@
31
32
  // We fetch the objects into local variables FIRST to ensure they don't
32
33
  // disappear mid-method if setConfig: is called on another thread.
33
34
 
35
+ ENRMDrawLinkChipsForGlyphRange(self, textContainer, glyphsToShow, origin);
36
+
34
37
  CodeBackground *codeBg = [self getCodeBackgroundWithConfig:config];
35
38
  [codeBg drawBackgroundsForGlyphRange:glyphsToShow layoutManager:self textContainer:textContainer atPoint:origin];
36
39
 
@@ -45,6 +45,9 @@ typedef void (^ENRMCodeBlockCopyBlock)(NSString *code, NSString *language);
45
45
  /// Set by the host; receives the Copy item's content when
46
46
  /// `menuOptions.interceptCopy` is set.
47
47
  @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
48
+ /// Called instead of the menu on long press while `contextMenuHidden` is on, so
49
+ /// the owner can report the block (`onSelectionChange`) and show its own UI.
50
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onMenuSuppressed;
48
51
 
49
52
  // Fired when the code is copied (header button, context-menu Copy, or the
50
53
  // VoiceOver copy action); set by the host to bridge up to the JS onCopyPress
@@ -495,6 +495,9 @@ static BOOL ENRMColorIsDark(RCTUIColor *color)
495
495
  {
496
496
  // Returning nil is the UIKit-sanctioned way to suppress a context menu.
497
497
  if (_pending || self.contextMenuHidden) {
498
+ if (!_pending && self.onMenuSuppressed) {
499
+ self.onMenuSuppressed([self fencedMarkdown], _cachedCode ?: @"");
500
+ }
498
501
  return nil;
499
502
  }
500
503
  __weak __typeof(self) weakSelf = self;
@@ -42,6 +42,9 @@ NS_ASSUME_NONNULL_BEGIN
42
42
  /// Set by the host; receives the Copy item's content when
43
43
  /// `menuOptions.interceptCopy` is set.
44
44
  @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
45
+ /// Called instead of the menu on long press while `contextMenuHidden` is on, so
46
+ /// the owner can report the block (`onSelectionChange`) and show its own UI.
47
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onMenuSuppressed;
45
48
 
46
49
  @end
47
50
 
@@ -128,6 +128,9 @@
128
128
  {
129
129
  // Returning nil is the UIKit-sanctioned way to suppress a context menu.
130
130
  if (self.contextMenuHidden) {
131
+ if (self.onMenuSuppressed) {
132
+ self.onMenuSuppressed([NSString stringWithFormat:@"$$\n%@\n$$", _cachedLatex ?: @""], _cachedLatex ?: @"");
133
+ }
131
134
  return nil;
132
135
  }
133
136
  __weak __typeof(self) weakSelf = self;
@@ -63,8 +63,14 @@ typedef void (^TableLinkPressBlock)(NSString *url);
63
63
  /// Set by the host; receives the Copy item's content when
64
64
  /// `menuOptions.interceptCopy` is set.
65
65
  @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
66
+ /// Called instead of the menu on long press while `contextMenuHidden` is on, so
67
+ /// the owner can report the block (`onSelectionChange`) and show its own UI.
68
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onMenuSuppressed;
66
69
 
67
70
  @property (nonatomic, readonly) NSUInteger rowCount;
71
+ /// Number of links across all cells. Tables are not reported by `onLinkLayout`,
72
+ /// but their links still count so later link indices stay in document order.
73
+ @property (nonatomic, readonly) NSInteger linkCount;
68
74
 
69
75
  - (void)animateNewRowsFromPreviousCount:(NSUInteger)previousRowCount duration:(NSTimeInterval)duration;
70
76