react-native-enriched-markdown-scaffold 1.0.2-scaffold.2 → 1.0.2-scaffold.4

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 (135) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +134 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -7
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +4 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +13 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +42 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +12 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -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 +33 -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 +24 -2
  29. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +27 -8
  30. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +9 -1
  31. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +18 -2
  32. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
  33. package/docs/API_REFERENCE.md +101 -0
  34. package/docs/COPY_OPTIONS.md +14 -0
  35. package/docs/MENTIONS.md +8 -1
  36. package/docs/STYLES.md +27 -0
  37. package/docs/TEXT.md +2 -0
  38. package/ios/EnrichedMarkdown.mm +91 -2
  39. package/ios/EnrichedMarkdownText.mm +64 -3
  40. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +72 -0
  41. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +50 -0
  42. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  43. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +134 -0
  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/ENRMBlockMenu.h +12 -3
  50. package/ios/utils/ENRMChipDrawing.h +24 -0
  51. package/ios/utils/ENRMChipDrawing.m +83 -0
  52. package/ios/utils/ENRMLinkChip.h +70 -0
  53. package/ios/utils/ENRMLinkChip.m +194 -0
  54. package/ios/utils/ENRMLinkLayout.h +50 -0
  55. package/ios/utils/ENRMLinkLayout.m +45 -0
  56. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  57. package/ios/utils/EditMenuUtils.h +10 -1
  58. package/ios/utils/EditMenuUtils.m +22 -5
  59. package/ios/utils/HTMLGenerator.m +2 -1
  60. package/ios/utils/MarkdownAccessibilityElementBuilder.m +4 -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 +4 -0
  66. package/ios/views/ENRMCodeBlockContainerView.m +8 -1
  67. package/ios/views/ENRMMathContainerView.h +4 -0
  68. package/ios/views/ENRMMathContainerView.m +8 -1
  69. package/ios/views/TableContainerView.h +7 -0
  70. package/ios/views/TableContainerView.m +30 -3
  71. package/lib/module/EnrichedMarkdownNativeComponent.ts +57 -0
  72. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +57 -0
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/native/EnrichedMarkdownText.js +119 -9
  75. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  76. package/lib/module/normalizeMarkdownStyle.js +13 -2
  77. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  78. package/lib/module/normalizeMarkdownStyle.web.js +13 -2
  79. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  80. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  81. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  82. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  83. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  84. package/lib/module/web/styles.js +18 -0
  85. package/lib/module/web/styles.js.map +1 -1
  86. package/lib/module/web/utils.js +13 -0
  87. package/lib/module/web/utils.js.map +1 -1
  88. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +53 -0
  89. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  90. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +53 -0
  91. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  92. package/lib/typescript/src/index.d.ts +2 -2
  93. package/lib/typescript/src/index.d.ts.map +1 -1
  94. package/lib/typescript/src/index.web.d.ts +1 -1
  95. package/lib/typescript/src/index.web.d.ts.map +1 -1
  96. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  97. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  98. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  99. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  100. package/lib/typescript/src/types/MarkdownStyle.d.ts +28 -5
  101. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  102. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +10 -0
  103. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  104. package/lib/typescript/src/types/MarkdownTextProps.d.ts +54 -1
  105. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  106. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  107. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  108. package/lib/typescript/src/types/events.d.ts +22 -0
  109. package/lib/typescript/src/types/events.d.ts.map +1 -1
  110. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  111. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  112. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  113. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  114. package/lib/typescript/src/web/types.d.ts +4 -0
  115. package/lib/typescript/src/web/types.d.ts.map +1 -1
  116. package/lib/typescript/src/web/utils.d.ts +4 -0
  117. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  118. package/package.json +1 -1
  119. package/src/EnrichedMarkdownNativeComponent.ts +57 -0
  120. package/src/EnrichedMarkdownTextNativeComponent.ts +57 -0
  121. package/src/index.tsx +3 -0
  122. package/src/index.web.tsx +5 -1
  123. package/src/native/EnrichedMarkdownText.tsx +127 -7
  124. package/src/normalizeMarkdownStyle.ts +13 -0
  125. package/src/normalizeMarkdownStyle.web.ts +11 -0
  126. package/src/types/MarkdownStyle.ts +29 -5
  127. package/src/types/MarkdownStyleInternal.ts +10 -0
  128. package/src/types/MarkdownTextProps.ts +56 -0
  129. package/src/types/MarkdownTextProps.web.ts +43 -1
  130. package/src/types/events.ts +23 -0
  131. package/src/web/EnrichedMarkdownText.tsx +10 -3
  132. package/src/web/renderers/InlineRenderers.tsx +111 -3
  133. package/src/web/styles.ts +25 -0
  134. package/src/web/types.ts +4 -0
  135. package/src/web/utils.ts +8 -0
@@ -0,0 +1,194 @@
1
+ #import "ENRMLinkChip.h"
2
+ #import "ENRMChipDrawing.h"
3
+ #import <CoreText/CoreText.h>
4
+
5
+ NSString *const ENRMLinkChipAttributeName = @"ENRMLinkChip";
6
+ NSString *const ENRMLinkSpacerAttributeName = @"ENRMLinkSpacer";
7
+
8
+ static const unichar kSpacerChar = 0x00A0;
9
+ static const CGFloat kSpacerFontSize = 1.0;
10
+
11
+ @implementation ENRMLinkChip
12
+
13
+ - (BOOL)isVisible
14
+ {
15
+ return self.backgroundColor != nil || (self.borderColor != nil && self.borderWidth > 0);
16
+ }
17
+
18
+ @end
19
+
20
+ #pragma mark - Spacers
21
+
22
+ static CGFloat spacerAdvance(UIFont *font)
23
+ {
24
+ CTFontRef ctFont = (__bridge CTFontRef)font;
25
+ UniChar character = kSpacerChar;
26
+ CGGlyph glyph = 0;
27
+ if (!CTFontGetGlyphsForCharacters(ctFont, &character, &glyph, 1))
28
+ return 0;
29
+ CGSize advance = CGSizeZero;
30
+ CTFontGetAdvancesForGlyphs(ctFont, kCTFontOrientationHorizontal, &glyph, &advance, 1);
31
+ return advance.width;
32
+ }
33
+
34
+ NSAttributedString *ENRMLinkSpacerString(NSDictionary<NSAttributedStringKey, id> *baseAttributes, CGFloat width)
35
+ {
36
+ static UIFont *spacerFont;
37
+ static CGFloat spacerFontAdvance;
38
+ static dispatch_once_t onceToken;
39
+ dispatch_once(&onceToken, ^{
40
+ spacerFont = [UIFont systemFontOfSize:kSpacerFontSize];
41
+ spacerFontAdvance = spacerAdvance(spacerFont);
42
+ });
43
+
44
+ NSMutableDictionary *attributes = [baseAttributes mutableCopy] ?: [NSMutableDictionary dictionary];
45
+ attributes[NSFontAttributeName] = spacerFont;
46
+ attributes[NSKernAttributeName] = @(width - spacerFontAdvance);
47
+ attributes[ENRMLinkSpacerAttributeName] = @YES;
48
+ // Padding is not text: keep decorations and attachments off it.
49
+ [attributes removeObjectForKey:NSUnderlineStyleAttributeName];
50
+ [attributes removeObjectForKey:NSStrikethroughStyleAttributeName];
51
+ [attributes removeObjectForKey:NSAttachmentAttributeName];
52
+
53
+ NSString *spacer = [NSString stringWithCharacters:&kSpacerChar length:1];
54
+ return [[NSAttributedString alloc] initWithString:spacer attributes:attributes];
55
+ }
56
+
57
+ NSAttributedString *ENRMAttributedStringByRemovingLinkSpacers(NSAttributedString *string)
58
+ {
59
+ __block NSMutableAttributedString *stripped = nil;
60
+ [string enumerateAttribute:ENRMLinkSpacerAttributeName
61
+ inRange:NSMakeRange(0, string.length)
62
+ options:NSAttributedStringEnumerationReverse
63
+ usingBlock:^(id value, NSRange range, BOOL *stop) {
64
+ if (!value)
65
+ return;
66
+ if (!stripped)
67
+ stripped = [string mutableCopy];
68
+ [stripped deleteCharactersInRange:range];
69
+ }];
70
+ return stripped ?: string;
71
+ }
72
+
73
+ NSString *ENRMPlainTextByRemovingLinkSpacers(NSAttributedString *string, NSRange range)
74
+ {
75
+ if (range.location == NSNotFound || NSMaxRange(range) > string.length)
76
+ return @"";
77
+ return ENRMAttributedStringByRemovingLinkSpacers([string attributedSubstringFromRange:range]).string;
78
+ }
79
+
80
+ void ENRMApplyLinkChipBackgroundsAsTextBackground(NSMutableAttributedString *string)
81
+ {
82
+ [string enumerateAttribute:ENRMLinkChipAttributeName
83
+ inRange:NSMakeRange(0, string.length)
84
+ options:0
85
+ usingBlock:^(ENRMLinkChip *chip, NSRange range, BOOL *stop) {
86
+ if (chip.backgroundColor) {
87
+ [string addAttribute:NSBackgroundColorAttributeName value:chip.backgroundColor range:range];
88
+ }
89
+ }];
90
+ }
91
+
92
+ #pragma mark - Geometry
93
+
94
+ /// The font of the first non-spacer character of `charRange` — the spacer's
95
+ /// 1pt font must not drive the chip's height.
96
+ static UIFont *_Nullable chipFontInRange(NSTextStorage *storage, NSRange charRange)
97
+ {
98
+ __block UIFont *font = nil;
99
+ [storage enumerateAttributesInRange:charRange
100
+ options:0
101
+ usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange range, BOOL *stop) {
102
+ if (attrs[ENRMLinkSpacerAttributeName])
103
+ return;
104
+ font = attrs[NSFontAttributeName];
105
+ *stop = font != nil;
106
+ }];
107
+ return font;
108
+ }
109
+
110
+ void ENRMEnumerateLinkChipFragments(NSLayoutManager *layoutManager, NSTextContainer *textContainer, NSRange charRange,
111
+ ENRMLinkChip *chip, void (^block)(CGRect rect, BOOL isFirst, BOOL isLast))
112
+ {
113
+ NSTextStorage *storage = layoutManager.textStorage;
114
+ if (charRange.length == 0 || NSMaxRange(charRange) > storage.length)
115
+ return;
116
+
117
+ NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:charRange actualCharacterRange:NULL];
118
+ if (glyphRange.location == NSNotFound || glyphRange.length == 0)
119
+ return;
120
+
121
+ CGFloat paddingHorizontal = chip.paddingHorizontal;
122
+ CGFloat paddingVertical = chip.paddingVertical;
123
+
124
+ [layoutManager
125
+ enumerateLineFragmentsForGlyphRange:glyphRange
126
+ usingBlock:^(CGRect lineRect, CGRect usedRect, NSTextContainer *tc, NSRange lineRange,
127
+ BOOL *stop) {
128
+ NSRange intersect = NSIntersectionRange(lineRange, glyphRange);
129
+ if (intersect.length == 0)
130
+ return;
131
+
132
+ BOOL isFirst = intersect.location == glyphRange.location;
133
+ BOOL isLast = NSMaxRange(intersect) == NSMaxRange(glyphRange);
134
+
135
+ CGRect textRect = [layoutManager boundingRectForGlyphRange:intersect
136
+ inTextContainer:textContainer];
137
+ CGFloat left = CGRectGetMinX(textRect);
138
+ CGFloat right = CGRectGetMaxX(textRect);
139
+
140
+ // The trailing spacer's kern is its advance; don't rely on the
141
+ // glyph bounds including it.
142
+ if (isLast && paddingHorizontal > 0) {
143
+ NSUInteger lastGlyph = NSMaxRange(intersect) - 1;
144
+ CGFloat spacerX =
145
+ lineRect.origin.x + [layoutManager locationForGlyphAtIndex:lastGlyph].x;
146
+ right = MAX(right, spacerX + paddingHorizontal);
147
+ }
148
+
149
+ NSRange fragmentChars = [layoutManager characterRangeForGlyphRange:intersect
150
+ actualGlyphRange:NULL];
151
+ UIFont *font = chipFontInRange(storage, fragmentChars);
152
+ CGFloat top = CGRectGetMinY(textRect);
153
+ CGFloat bottom = CGRectGetMaxY(textRect);
154
+ if (font) {
155
+ // locationForGlyphAtIndex: y is the baseline within the line fragment.
156
+ CGFloat baseline =
157
+ lineRect.origin.y + [layoutManager locationForGlyphAtIndex:intersect.location].y;
158
+ top = baseline - font.ascender;
159
+ bottom = baseline - font.descender;
160
+ }
161
+ top -= paddingVertical;
162
+ bottom += paddingVertical;
163
+
164
+ block(CGRectMake(left, top, right - left, bottom - top), isFirst, isLast);
165
+ }];
166
+ }
167
+
168
+ #pragma mark - Drawing
169
+
170
+ void ENRMDrawLinkChipsForGlyphRange(NSLayoutManager *layoutManager, NSTextContainer *textContainer,
171
+ NSRange glyphsToShow, CGPoint origin)
172
+ {
173
+ NSTextStorage *storage = layoutManager.textStorage;
174
+ NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphsToShow actualGlyphRange:NULL];
175
+ if (charRange.location == NSNotFound || charRange.length == 0)
176
+ return;
177
+
178
+ // Enumerate the whole storage (like CodeBackground) so a chip that starts
179
+ // before the dirty range still gets its full rect.
180
+ [storage enumerateAttribute:ENRMLinkChipAttributeName
181
+ inRange:NSMakeRange(0, storage.length)
182
+ options:0
183
+ usingBlock:^(ENRMLinkChip *chip, NSRange range, BOOL *stop) {
184
+ if (!chip.isVisible || NSIntersectionRange(range, charRange).length == 0)
185
+ return;
186
+
187
+ ENRMEnumerateLinkChipFragments(
188
+ layoutManager, textContainer, range, chip, ^(CGRect rect, BOOL isFirst, BOOL isLast) {
189
+ ENRMDrawChipFragment(CGRectOffset(rect, origin.x, origin.y), chip.backgroundColor,
190
+ chip.borderColor, chip.borderWidth, chip.borderRadius, isFirst, isLast,
191
+ YES);
192
+ });
193
+ }];
194
+ }
@@ -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(
28
+ layoutManager, textContainer, range, chip, ^(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
+ }
@@ -1,6 +1,7 @@
1
1
  #import "ContextMenuUtils.h"
2
2
  #import "ENRMMenuAction.h"
3
3
  #import "EditMenuUtils.h"
4
+ #import "ENRMLinkChip.h"
4
5
  #import "PasteboardUtils.h"
5
6
  #import "StyleConfig.h"
6
7
  #include <TargetConditionals.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
 
@@ -15,6 +15,9 @@ typedef struct {
15
15
  // Drop every system-provided item from the text selection menu.
16
16
  BOOL systemItemsNone;
17
17
  BOOL codeBlockCopyButton;
18
+ // Copy hands the selection to the owner (JS `onCopy`) instead of writing the
19
+ // pasteboard.
20
+ BOOL interceptCopy;
18
21
  // The owner must keep these strings alive for the duration of the call (the
19
22
  // view holds them in strong ivars).
20
23
  __unsafe_unretained NSString *_Nullable copyLabel;
@@ -24,6 +27,10 @@ typedef struct {
24
27
  __unsafe_unretained NSArray<NSString *> *_Nullable copyImageUrlPluralTemplates;
25
28
  } ENRMSelectionMenuConfig;
26
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
+
27
34
  /// Resolves the "Copy Image URL(s)" menu title for a given image count.
28
35
  /// Uses the precomputed plural templates (indexed by count, 0..100) when present;
29
36
  /// counts > 100 use the "other" form (copyImageUrlsLabel). Otherwise the
@@ -87,6 +94,7 @@ static inline ENRMSelectionMenuConfig ENRMBuildSelectionMenuConfig(const ENRMSel
87
94
  .copyFormatMarkdown = src.copyFormatMarkdown,
88
95
  .systemItemsNone = src.systemItemsNone,
89
96
  .codeBlockCopyButton = src.codeBlockCopyButton,
97
+ .interceptCopy = src.interceptCopy,
90
98
  .copyLabel = labels.copyLabel,
91
99
  .copyAsMarkdownLabel = labels.copyAsMarkdownLabel,
92
100
  .copyImageUrlLabel = labels.copyImageUrlLabel,
@@ -107,7 +115,8 @@ extern "C" {
107
115
  UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown,
108
116
  StyleConfig *styleConfig, NSArray<UIMenuElement *> *suggestedActions,
109
117
  NSArray<UIAction *> *_Nullable customActions,
110
- ENRMSelectionMenuConfig selectionMenuConfig) API_AVAILABLE(ios(16.0));
118
+ ENRMSelectionMenuConfig selectionMenuConfig,
119
+ ENRMCopyRequestHandler _Nullable onCopyRequest) API_AVAILABLE(ios(16.0));
111
120
  #else
112
121
  NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range,
113
122
  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,12 +11,22 @@ 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 *_Nullable createCopyAction(NSAttributedString *selectedText, NSString *markdown,
14
- StyleConfig *styleConfig, ENRMSelectionMenuConfig selectionMenuConfig)
14
+ static UIAction *_Nullable createCopyAction(NSAttributedString *selectedText, NSString *markdown, BOOL fullSelection,
15
+ StyleConfig *styleConfig, ENRMSelectionMenuConfig selectionMenuConfig,
16
+ ENRMCopyRequestHandler _Nullable onCopyRequest)
15
17
  {
16
18
  if (!selectionMenuConfig.copyEnabled)
17
19
  return nil;
18
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
+
19
30
  BOOL writesMarkdown = selectionMenuConfig.copyFormatMarkdown;
20
31
  return [UIAction actionWithTitle:selectionMenuConfig.copyLabel
21
32
  image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
@@ -108,13 +119,19 @@ static void insertOptionalAction(NSMutableArray<UIMenuElement *> *array, UIActio
108
119
  UIMenu *buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range, NSString *_Nullable cachedMarkdown,
109
120
  StyleConfig *styleConfig, NSArray<UIMenuElement *> *suggestedActions,
110
121
  NSArray<UIAction *> *_Nullable customActions,
111
- ENRMSelectionMenuConfig selectionMenuConfig) API_AVAILABLE(ios(16.0))
122
+ ENRMSelectionMenuConfig selectionMenuConfig,
123
+ ENRMCopyRequestHandler _Nullable onCopyRequest) API_AVAILABLE(ios(16.0))
112
124
  {
113
- 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]);
114
128
  NSString *markdown = markdownForRange(attributedText, range, cachedMarkdown);
115
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;
116
132
 
117
- UIAction *copyAction = createCopyAction(selectedText, markdown, styleConfig, selectionMenuConfig);
133
+ UIAction *copyAction =
134
+ createCopyAction(selectedText, markdown, fullSelection, styleConfig, selectionMenuConfig, onCopyRequest);
118
135
  UIAction *copyMarkdownAction = selectionMenuConfig.copyAsMarkdown
119
136
  ? createCopyMarkdownAction(markdown, selectionMenuConfig.copyAsMarkdownLabel)
120
137
  : nil;
@@ -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,9 @@ 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"]
119
+ : ENRMPlainTextByRemovingLinkSpacers(textView.attributedText, itemRange);
117
120
  [elements addObject:[self createElementForRange:itemRange
118
121
  type:isImg ? ElementTypeImage : ElementTypeLink
119
122
  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
 
@@ -42,6 +42,10 @@ typedef void (^ENRMCodeBlockCopyBlock)(NSString *code, NSString *language);
42
42
  /// the header button and VoiceOver action always copy the raw code.
43
43
  @property (nonatomic, assign) ENRMBlockMenuOptions menuOptions;
44
44
 
45
+ /// Set by the host; receives the Copy item's content when
46
+ /// `menuOptions.interceptCopy` is set.
47
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
48
+
45
49
  // Fired when the code is copied (header button, context-menu Copy, or the
46
50
  // VoiceOver copy action); set by the host to bridge up to the JS onCopyPress
47
51
  // event. Not fired for "Copy as Markdown", nor for the context-menu Copy when
@@ -500,7 +500,14 @@ static BOOL ENRMColorIsDark(RCTUIColor *color)
500
500
  __weak __typeof(self) weakSelf = self;
501
501
  return ENRMBlockContextMenuConfiguration(
502
502
  self.menuOptions, self.copyLabel, self.copyAsMarkdownLabel, ^{ [weakSelf copyCodeToPasteboard]; },
503
- ^{ [weakSelf copyMarkdownToPasteboard]; });
503
+ ^{ [weakSelf copyMarkdownToPasteboard]; }, ^{ [weakSelf requestCopy]; });
504
+ }
505
+
506
+ - (void)requestCopy
507
+ {
508
+ if (self.onCopyRequest) {
509
+ self.onCopyRequest([self fencedMarkdown], _cachedCode ?: @"");
510
+ }
504
511
  }
505
512
  #endif
506
513
 
@@ -39,6 +39,10 @@ NS_ASSUME_NONNULL_BEGIN
39
39
  /// Copy / Copy as Markdown visibility and Copy format from `selectionMenuConfig`.
40
40
  @property (nonatomic, assign) ENRMBlockMenuOptions menuOptions;
41
41
 
42
+ /// Set by the host; receives the Copy item's content when
43
+ /// `menuOptions.interceptCopy` is set.
44
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
45
+
42
46
  @end
43
47
 
44
48
  NS_ASSUME_NONNULL_END
@@ -133,7 +133,14 @@
133
133
  __weak __typeof(self) weakSelf = self;
134
134
  return ENRMBlockContextMenuConfiguration(
135
135
  self.menuOptions, self.copyLabel, self.copyAsMarkdownLabel, ^{ [weakSelf copyLatexToPasteboard]; },
136
- ^{ [weakSelf copyMarkdownToPasteboard]; });
136
+ ^{ [weakSelf copyMarkdownToPasteboard]; }, ^{ [weakSelf requestCopy]; });
137
+ }
138
+
139
+ - (void)requestCopy
140
+ {
141
+ if (self.onCopyRequest) {
142
+ self.onCopyRequest([NSString stringWithFormat:@"$$\n%@\n$$", _cachedLatex ?: @""], _cachedLatex ?: @"");
143
+ }
137
144
  }
138
145
  #endif
139
146
 
@@ -60,7 +60,14 @@ typedef void (^TableLinkPressBlock)(NSString *url);
60
60
  /// Copy / Copy as Markdown visibility and Copy format from `selectionMenuConfig`.
61
61
  @property (nonatomic, assign) ENRMBlockMenuOptions menuOptions;
62
62
 
63
+ /// Set by the host; receives the Copy item's content when
64
+ /// `menuOptions.interceptCopy` is set.
65
+ @property (nonatomic, copy, nullable) ENRMBlockCopyRequestBlock onCopyRequest;
66
+
63
67
  @property (nonatomic, readonly) NSUInteger rowCount;
68
+ /// Number of links across all cells. Tables are not reported by `onLinkLayout`,
69
+ /// but their links still count so later link indices stay in document order.
70
+ @property (nonatomic, readonly) NSInteger linkCount;
64
71
 
65
72
  - (void)animateNewRowsFromPreviousCount:(NSUInteger)previousRowCount duration:(NSTimeInterval)duration;
66
73